Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/infuse_iot/generated/rpc_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,38 @@ class response(VLACompatLittleEndianStruct):
_pack_ = 1


class bt_file_copy_coap:
"""Copy a file fetched from COAP to a remote device over Bluetooth"""

HELP = "Copy a file fetched from COAP to a remote device over Bluetooth"
DESCRIPTION = "Copy a file fetched from COAP to a remote device over Bluetooth"
COMMAND_ID = 53

class request(VLACompatLittleEndianStruct):
_fields_ = [
("peer", rpc_struct_bt_addr_le),
("conn_timeout_ms", ctypes.c_uint16),
("action", ctypes.c_uint8),
("file_idx", ctypes.c_uint8),
("ack_period", ctypes.c_uint8),
("pipelining", ctypes.c_uint8),
("server_address", 48 * ctypes.c_char),
("server_port", ctypes.c_uint16),
("block_timeout_ms", ctypes.c_uint16),
("resource_len", ctypes.c_uint32),
("resource_crc", ctypes.c_uint32),
]
vla_field = ("resource", 0 * ctypes.c_char)
_pack_ = 1

class response(VLACompatLittleEndianStruct):
_fields_ = [
("resource_len", ctypes.c_uint32),
("resource_crc", ctypes.c_uint32),
]
_pack_ = 1


class gravity_reference_update:
"""Store the current accelerometer vector as the gravity reference"""

Expand Down
120 changes: 120 additions & 0 deletions src/infuse_iot/rpc_wrappers/bt_file_copy_coap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env python3

import ctypes

import infuse_iot.generated.rpc_definitions as defs
from infuse_iot.commands import InfuseRpcCommand
from infuse_iot.generated.rpc_definitions import rpc_enum_bt_le_addr_type, rpc_enum_file_action, rpc_struct_bt_addr_le
from infuse_iot.rpc_wrappers.coap_download import coap_download, coap_server_file_stats
from infuse_iot.util.argparse import BtLeAddress
from infuse_iot.util.ctypes import bytes_to_uint8
from infuse_iot.zephyr.errno import errno


class bt_file_copy_coap(InfuseRpcCommand, defs.bt_file_copy_coap):
@classmethod
def add_parser(cls, parser):
parser.add_argument(
"--server",
type=str,
default=coap_download.INFUSE_COAP_SERVER_ADDR,
help="COAP server name",
)
parser.add_argument(
"--port",
type=int,
default=coap_download.INFUSE_COAP_SERVER_PORT,
help="COAP server port",
)
parser.add_argument(
"--resource",
"-r",
type=str,
required=True,
help="Resource path",
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
"--discard",
dest="action",
action="store_const",
const=rpc_enum_file_action.DISCARD,
help="Download file and discard without action",
)
group.add_argument(
"--dfu",
dest="action",
action="store_const",
const=rpc_enum_file_action.APP_IMG,
help="Download complete image file and perform DFU",
)
group.add_argument(
"--cpatch",
dest="action",
action="store_const",
const=rpc_enum_file_action.APP_CPATCH,
help="Download CPatch binary diff and perform DFU",
)
group.add_argument(
"--nrf91-modem",
dest="action",
action="store_const",
const=rpc_enum_file_action.NRF91_MODEM_DIFF,
help="nRF91 LTE modem diff upgrade",
)
addr_group = parser.add_mutually_exclusive_group(required=True)
addr_group.add_argument("--public", type=BtLeAddress, help="Public Bluetooth address")
addr_group.add_argument("--random", type=BtLeAddress, help="Random Bluetooth address")
parser.add_argument("--conn-timeout", type=int, default=5000, help="Connection timeout (ms)")
parser.add_argument("--bt-pipelining", type=int, default=4, help="Bluetooth data pipelining")

def __init__(self, args):
self.server = args.server.encode("utf-8")
self.port = args.port
self.resource = args.resource.encode("utf-8")
self.action = args.action
self.conn_timeout = args.conn_timeout
self.pipelining = args.bt_pipelining
if args.public:
self.peer = rpc_struct_bt_addr_le(
rpc_enum_bt_le_addr_type.PUBLIC,
bytes_to_uint8(args.public.to_bytes(6, "little")),
)
else:
self.peer = rpc_struct_bt_addr_le(
rpc_enum_bt_le_addr_type.RANDOM,
bytes_to_uint8(args.random.to_bytes(6, "little")),
)
self.file_len, self.file_crc = coap_server_file_stats(args.server, args.resource)

def request_struct(self):
class request(ctypes.LittleEndianStructure):
_fields_ = [
*self.request._fields_,
("resource", (len(self.resource) + 1) * ctypes.c_char),
]
_pack_ = 1

return request(
self.peer,
self.conn_timeout,
self.action,
0,
1,
self.pipelining,
self.server,
self.port,
2000,
self.file_len,
self.file_crc,
self.resource,
)

def handle_response(self, return_code, response):
if return_code != 0:
print(f"Failed to download file ({errno.strerror(-return_code)})")
return
else:
print("File downloaded and copied")
print(f"\tLength: {response.resource_len}")
print(f"\t CRC: 0x{response.resource_crc:08x}")
50 changes: 44 additions & 6 deletions src/infuse_iot/rpc_wrappers/coap_download.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,57 @@
#!/usr/bin/env python3

import ctypes
import sys
from http import HTTPStatus
from json import loads

import infuse_iot.generated.rpc_definitions as defs
from infuse_iot.api_client import Client
from infuse_iot.api_client.api.coap import get_coap_file_stats
from infuse_iot.commands import InfuseRpcCommand
from infuse_iot.credentials import get_api_key
from infuse_iot.generated.rpc_definitions import rpc_enum_file_action
from infuse_iot.util.ctypes import UINT32_MAX
from infuse_iot.zephyr.errno import errno


def coap_server_file_stats(server: str, resource: str) -> tuple[int, int]:
if server == coap_download.INFUSE_COAP_SERVER_ADDR:
# Validate file prefix
if not resource.startswith("file/"):
sys.exit("Infuse-IoT COAP files start with 'file/'")
api_filename = resource.removeprefix("file/")
# Get COAP file information
client = Client(base_url="https://api.infuse-iot.com").with_headers({"x-api-key": f"Bearer {get_api_key()}"})
with client as client:
response = get_coap_file_stats.sync_detailed(client=client, filename=api_filename)
decoded = loads(response.content.decode("utf-8"))
if response.status_code != HTTPStatus.OK:
sys.exit(f"<{response.status_code}>: {decoded['message']}")
return (decoded["len"], decoded["crc"])
else:
# Unknown, let the COAP download automatically determine
# This does mean that duplicate file are not detected
print("Custom COAP server, duplicate file detection disabled")
return (UINT32_MAX, UINT32_MAX)


class coap_download(InfuseRpcCommand, defs.coap_download):
INFUSE_COAP_SERVER_ADDR = "coap.dev.infuse-iot.com"
INFUSE_COAP_SERVER_PORT = 5684

@classmethod
def add_parser(cls, parser):
parser.add_argument(
"--server",
type=str,
default="coap.dev.infuse-iot.com",
default=cls.INFUSE_COAP_SERVER_ADDR,
help="COAP server name",
)
parser.add_argument(
"--port",
type=int,
default=5684,
default=cls.INFUSE_COAP_SERVER_PORT,
help="COAP server port",
)
parser.add_argument(
Expand Down Expand Up @@ -60,12 +90,20 @@ def add_parser(cls, parser):
const=rpc_enum_file_action.NRF91_MODEM_DIFF,
help="nRF91 LTE modem diff upgrade",
)
group.add_argument(
"--for-copy",
dest="action",
action="store_const",
const=rpc_enum_file_action.FILE_FOR_COPY,
help="File to copy to other device",
)

def __init__(self, args):
self.server = args.server.encode("utf-8")
self.port = args.port
self.resource = args.resource.encode("utf-8")
self.action = args.action
self.file_len, self.file_crc = coap_server_file_stats(args.server, args.resource)

def request_struct(self):
class request(ctypes.LittleEndianStructure):
Expand All @@ -85,8 +123,8 @@ class request(ctypes.LittleEndianStructure):
self.port,
2000,
self.action,
UINT32_MAX,
UINT32_MAX,
self.file_len,
self.file_crc,
self.resource,
)

Expand All @@ -96,8 +134,8 @@ def request_json(self):
"server_port": str(self.port),
"block_timeout_ms": "2000",
"action": self.action.name,
"resource_len": str(UINT32_MAX),
"resource_crc": str(UINT32_MAX),
"resource_len": str(self.file_len),
"resource_crc": str(self.file_crc),
"resource": self.resource.decode("utf-8"),
}

Expand Down
12 changes: 9 additions & 3 deletions src/infuse_iot/tools/rpc_cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,17 @@ def queue(self, client: Client):
assert hasattr(command, "COMMAND_ID")

try:
# Get the human readable arguments if implementated
params = RPCParams.from_dict(command.request_json())
rpc_req = NewRPCReq(command_id=command.COMMAND_ID, params=params)
except NotImplementedError:
sys.exit(f"Command '{command.__class__.__name__}' has not implemented cloud support")
req = NewRPCMessage(infuse_id, NewRPCReq(command.COMMAND_ID, params=params), timeout_ms)
rsp = send_rpc.sync(client=client, body=req)
# Otherwise, encode the raw binary struct
struct_bytes = bytes(command.request_struct())
params_encoded = base64.b64encode(struct_bytes).decode("utf-8")
rpc_req = NewRPCReq(command_id=command.COMMAND_ID, params_encoded=params_encoded)

rpc_msg = NewRPCMessage(infuse_id, rpc_req, timeout_ms)
rsp = send_rpc.sync(client=client, body=rpc_msg)
if isinstance(rsp, Error) or rsp is None:
sys.exit(f"Failed to queue RPC ({rsp})")
print(f"Queued RPC ID: {rsp.id}")
Expand Down
Loading