diff --git a/src/infuse_iot/generated/kv_definitions.py b/src/infuse_iot/generated/kv_definitions.py index 14e682d..1636ff0 100644 --- a/src/infuse_iot/generated/kv_definitions.py +++ b/src/infuse_iot/generated/kv_definitions.py @@ -306,6 +306,17 @@ class lora_config(VLACompatLittleEndianStruct): ] _pack_ = 1 + class bluetooth_throughput_limit(VLACompatLittleEndianStruct): + """Request connected Bluetooth peers to limit throughtput""" + + NAME = "BLUETOOTH_THROUGHPUT_LIMIT" + BASE_ID = 52 + RANGE = 1 + _fields_ = [ + ("limit_kbps", ctypes.c_uint16), + ] + _pack_ = 1 + class gravity_reference(VLACompatLittleEndianStruct): """Reference gravity vector for tilt calculations""" @@ -395,6 +406,7 @@ class secure_storage_reserved(VLACompatLittleEndianStruct): 46: lte_networking_modes, 50: bluetooth_peer, 51: lora_config, + 52: bluetooth_throughput_limit, 60: gravity_reference, 100: geofence, 101: geofence, diff --git a/src/infuse_iot/generated/rpc_definitions.py b/src/infuse_iot/generated/rpc_definitions.py index 2bd9efa..267c5c8 100644 --- a/src/infuse_iot/generated/rpc_definitions.py +++ b/src/infuse_iot/generated/rpc_definitions.py @@ -201,6 +201,7 @@ class rpc_enum_file_action(enum.IntEnum): APP_CPATCH = 11 BT_CTLR_CPATCH = 12 NRF91_MODEM_DIFF = 20 + FILE_FOR_COPY = 30 class rpc_enum_infuse_bt_characteristic(enum.IntEnum): @@ -665,6 +666,32 @@ class response(VLACompatLittleEndianStruct): _pack_ = 1 +class data_logger_read_available: + """Read data from data logger, with auto-updating start_block""" + + HELP = "Read data from data logger, with auto-updating start_block" + DESCRIPTION = "Read data from data logger, with auto-updating start_block" + COMMAND_ID = 22 + + class request(VLACompatLittleEndianStruct): + _fields_ = [ + ("logger", ctypes.c_uint8), + ("start_block", ctypes.c_uint32), + ("num_blocks", ctypes.c_uint32), + ] + _pack_ = 1 + + class response(VLACompatLittleEndianStruct): + _fields_ = [ + ("sent_len", ctypes.c_uint32), + ("sent_crc", ctypes.c_uint32), + ("current_block", ctypes.c_uint32), + ("start_block_actual", ctypes.c_uint32), + ("block_size", ctypes.c_uint16), + ] + _pack_ = 1 + + class coap_download: """Download a file from a COAP server (Infuse-IoT DTLS protected)""" @@ -812,6 +839,30 @@ class response(VLACompatLittleEndianStruct): _pack_ = 1 +class bt_file_copy_basic: + """Copy a local file to a remote device over Bluetooth""" + + HELP = "Copy a local file to a remote device over Bluetooth" + DESCRIPTION = "Copy a local file to a remote device over Bluetooth" + COMMAND_ID = 52 + + class request(VLACompatLittleEndianStruct): + _fields_ = [ + ("peer", rpc_struct_bt_addr_le), + ("action", ctypes.c_uint8), + ("file_idx", ctypes.c_uint8), + ("file_len", ctypes.c_uint32), + ("file_crc", ctypes.c_uint32), + ("ack_period", ctypes.c_uint8), + ("pipelining", ctypes.c_uint8), + ] + _pack_ = 1 + + class response(VLACompatLittleEndianStruct): + _fields_ = [] + _pack_ = 1 + + class gravity_reference_update: """Store the current accelerometer vector as the gravity reference""" diff --git a/src/infuse_iot/rpc_wrappers/data_logger_read.py b/src/infuse_iot/rpc_wrappers/data_logger_read.py index 2cced3a..0cc12f9 100644 --- a/src/infuse_iot/rpc_wrappers/data_logger_read.py +++ b/src/infuse_iot/rpc_wrappers/data_logger_read.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import binascii +import time import infuse_iot.generated.rpc_definitions as defs from infuse_iot.commands import InfuseRpcCommand @@ -31,6 +32,7 @@ def __init__(self, args): raise NotImplementedError self.expected_offset = 0 self.output = b"" + self.start_time = time.time() def request_struct(self): return self.request(self.logger, self.start, self.last) @@ -39,6 +41,8 @@ def request_json(self): return {"logger": self.logger.name, "start_block": self.start, "last_block": self.last} def data_recv_cb(self, offset: int, data: bytes) -> None: + if self.expected_offset == 0: + self.start_time = time.time() if offset != self.expected_offset: missing = offset - self.expected_offset print(f"Missed {missing:d} bytes from offset 0x{self.expected_offset:08x}") @@ -49,6 +53,7 @@ def data_recv_cb(self, offset: int, data: bytes) -> None: self.expected_offset = offset + len(data) def handle_response(self, return_code, response): + end_time = time.time() if return_code != 0: print(f"Failed to read data logger ({errno.strerror(-return_code)})") return @@ -59,8 +64,11 @@ def handle_response(self, return_code, response): if response.sent_crc != binascii.crc32(self.output): print(f"Unexpected received CRC ({response.sent_crc:08x} != {binascii.crc32(self.output):08x})") + duration = end_time - self.start_time + bitrate = (len(self.output) * 8) / duration / 1024 + file_prefix = f"{self.infuse_id:016x}" if self.infuse_id else "gateway" output_file = f"{file_prefix}_{self.logger.name}.bin" with open(output_file, "wb") as f: f.write(self.output) - print(f"Wrote {response.sent_len:d} bytes to {output_file}") + print(f"Wrote {response.sent_len:d} bytes to {output_file} in {duration:.2f} sec ({bitrate:.3f} kbps)") diff --git a/src/infuse_iot/rpc_wrappers/file_write_basic.py b/src/infuse_iot/rpc_wrappers/file_write_basic.py index 8b1c496..2374eef 100644 --- a/src/infuse_iot/rpc_wrappers/file_write_basic.py +++ b/src/infuse_iot/rpc_wrappers/file_write_basic.py @@ -69,6 +69,13 @@ 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.file = args.file diff --git a/src/infuse_iot/tools/ota_upgrade.py b/src/infuse_iot/tools/ota_upgrade.py index f31308d..04f7655 100644 --- a/src/infuse_iot/tools/ota_upgrade.py +++ b/src/infuse_iot/tools/ota_upgrade.py @@ -5,7 +5,9 @@ __author__ = "Jordan Yates" __copyright__ = "Copyright 2024, Embeint Inc" +import argparse import binascii +import sys import time from rich.live import Live @@ -18,8 +20,9 @@ from rich.table import Table from infuse_iot.commands import InfuseCommand -from infuse_iot.epacket.packet import Auth -from infuse_iot.generated.rpc_definitions import file_write_basic, rpc_enum_file_action +from infuse_iot.common import InfuseID +from infuse_iot.epacket.packet import Auth, HopReceived +from infuse_iot.generated.rpc_definitions import bt_file_copy_basic, file_write_basic, rpc_enum_file_action from infuse_iot.rpc_client import RpcClient from infuse_iot.socket_comms import ( GatewayRequestConnectionRequest, @@ -27,6 +30,7 @@ default_multicast_address, ) from infuse_iot.util.argparse import ValidFile, ValidRelease +from infuse_iot.zephyr.errno import errno class SubCommand(InfuseCommand): @@ -39,7 +43,19 @@ def __init__(self, args): self._conn_timeout = args.conn_timeout self._min_rssi: int | None = args.rssi self._explicit_ids: list[int] = [] - self._release: ValidRelease = args.release + if args.release: + self._release: ValidRelease = args.release + self._single_diff = None + elif args.single: + # Find the associated release + diff_folder = args.single.parent + release_folder = diff_folder.parent + if diff_folder.name != "diffs": + raise argparse.ArgumentTypeError(f"{args.single} is not in a diff folder") + self._release = ValidRelease(str(release_folder)) + self._single_diff = args.single + else: + raise NotImplementedError("Unknow upgrade type") self._app_name = self._release.metadata["application"]["primary"] self._app_id = self._release.metadata["application"]["id"] self._new_ver = self._release.metadata["application"]["version"] @@ -72,9 +88,9 @@ def __init__(self, args): @classmethod def add_parser(cls, parser): - parser.add_argument( - "--release", "-r", type=ValidRelease, required=True, help="Application release to upgrade to" - ) + upgrade_type = parser.add_mutually_exclusive_group(required=True) + upgrade_type.add_argument("--release", "-r", type=ValidRelease, help="Application release to upgrade to") + upgrade_type.add_argument("--single", type=ValidFile, help="Single diff") parser.add_argument("--rssi", type=int, help="Minimum RSSI to attempt upgrade process") parser.add_argument("--log", type=str, help="File to write upgrade results to") parser.add_argument( @@ -116,7 +132,73 @@ def data_progress_cb(self, offset): self.task = self.progress.add_task("", total=len(self.patch_file)) self.progress.update(self.task, completed=offset) + def gateway_diff_load(self): + assert self._single_diff is not None + with self._single_diff.open("rb") as f: + patch_file = f.read() + + with self._client.connection(InfuseID.GATEWAY, GatewayRequestConnectionRequest.DataType.COMMAND, 10) as _mtu: + rpc_client = RpcClient(self._client, _mtu, InfuseID.GATEWAY) + params = file_write_basic.request(rpc_enum_file_action.FILE_FOR_COPY, binascii.crc32(patch_file)) + + print(f"Writing '{self._single_diff}' to gateway") + hdr, _rsp = rpc_client.run_data_send_cmd( + file_write_basic.COMMAND_ID, + Auth.DEVICE, + bytes(params), + patch_file, + None, + file_write_basic.response.from_buffer_copy, + ) + if hdr.return_code != 0: + sys.exit(f"Failed to save diff file to gateway (({errno.strerror(-hdr.return_code)}))") + print(f"'{self._single_diff}' written to gateway") + + def run_file_upload(self, live: Live, mtu: int, source: HopReceived): + self.state_update(live, f"Uploading patch file to {source.infuse_id:016X}") + rpc_client = RpcClient(self._client, mtu, source.infuse_id) + + params = file_write_basic.request(rpc_enum_file_action.APP_CPATCH, binascii.crc32(self.patch_file)) + + hdr, _rsp = rpc_client.run_data_send_cmd( + file_write_basic.COMMAND_ID, + Auth.DEVICE, + bytes(params), + self.patch_file, + self.data_progress_cb, + file_write_basic.response.from_buffer_copy, + ) + + if hdr.return_code == 0: + self._pending[source.infuse_id] = time.time() + 60 + + def run_file_copy(self, live: Live, mtu: int, source: HopReceived): + self.state_update(live, f"Copying patch file to {source.infuse_id:016X}") + rpc_client = RpcClient(self._client, mtu, InfuseID.GATEWAY) + + params = bt_file_copy_basic.request( + source.interface_address.val.to_rpc_struct(), + rpc_enum_file_action.APP_CPATCH, + 0, + len(self.patch_file), + binascii.crc32(self.patch_file), + 1, + 3, + ) + + hdr, _rsp = rpc_client.run_standard_cmd( + bt_file_copy_basic.COMMAND_ID, + Auth.DEVICE, + bytes(params), + bt_file_copy_basic.response.from_buffer_copy, + ) + if hdr.return_code == 0: + self._pending[source.infuse_id] = time.time() + 60 + def run(self): + if self._single_diff: + self.gateway_diff_load() + with Live(self.progress_table(), refresh_per_second=4) as live: for source, announce in self._client.observe_announce(): self.state_update(live, "Scanning") @@ -169,6 +251,14 @@ def run(self): # Do we have a valid diff? diff_file = self._release.dir / "diffs" / f"{v_str}.bin" + if self._single_diff and self._single_diff != diff_file: + # Not the file we've copied to the gateway flash + self._missing_diffs.add(v_str) + self._handled.append(source.infuse_id) + self._no_diff += 1 + self.state_update(live, "Scanning") + continue + if not diff_file.exists(): # Is this a single diff from a different application we know about? diff_file = self._release.dir / "diffs" / f"0x{announce.application:08x}" / f"{v_str}.bin" @@ -193,24 +283,10 @@ def run(self): with self._client.connection( source.infuse_id, GatewayRequestConnectionRequest.DataType.COMMAND, self._conn_timeout ) as mtu: - self.state_update(live, f"Uploading patch file to {source.infuse_id:016X}") - rpc_client = RpcClient(self._client, mtu, source.infuse_id) - - params = file_write_basic.request( - rpc_enum_file_action.APP_CPATCH, binascii.crc32(self.patch_file) - ) - - hdr, _rsp = rpc_client.run_data_send_cmd( - file_write_basic.COMMAND_ID, - Auth.DEVICE, - bytes(params), - self.patch_file, - self.data_progress_cb, - file_write_basic.response.from_buffer_copy, - ) - - if hdr.return_code == 0: - self._pending[source.infuse_id] = time.time() + 60 + if self._single_diff: + self.run_file_copy(live, mtu, source) + else: + self.run_file_upload(live, mtu, source) except ConnectionRefusedError: self.state_update(live, "Scanning") diff --git a/src/infuse_iot/util/soc/nrf.py b/src/infuse_iot/util/soc/nrf.py index 5558aac..91abd6a 100644 --- a/src/infuse_iot/util/soc/nrf.py +++ b/src/infuse_iot/util/soc/nrf.py @@ -141,7 +141,7 @@ def unique_device_id_len(self) -> int: def unique_device_id(self) -> int: device_id_addr = self.family.FICR_ADDRESS + self.family.DEVICE_ID_OFFSET - result = self._exec(["x-read", "--address", hex(device_id_addr), "--bytes", "8", "--direct"]) + result = self._exec(["read", "--address", hex(device_id_addr), "--bytes", "8", "--direct"]) data_bytes = result[0]["devices"][0]["memoryData"][0]["values"] dev_id_bytes = bytes(data_bytes) return int.from_bytes(dev_id_bytes, "big") @@ -149,7 +149,7 @@ def unique_device_id(self) -> int: def read_provisioned_data(self, num: int) -> bytes: customer_addr = self.uicr_base + self.family.CUSTOMER_OFFSET - result = self._exec(["x-read", "--address", hex(customer_addr), "--bytes", str(num), "--direct"]) + result = self._exec(["read", "--address", hex(customer_addr), "--bytes", str(num), "--direct"]) data_bytes = result[0]["devices"][0]["memoryData"][0]["values"] return bytes(data_bytes) @@ -164,4 +164,4 @@ def write_provisioning_data(self, data: bytes): chunk_bytes += b"\xff" * (4 - len(chunk_bytes)) data_word = int.from_bytes(chunk_bytes, byteorder="little") - self._exec(["x-write", "--address", hex(customer_addr + offset), "--value", hex(data_word)]) + self._exec(["write", "--address", hex(customer_addr + offset), "--value", hex(data_word)])