diff --git a/src/infuse_iot/commands.py b/src/infuse_iot/commands.py index 47ff6f2..c9bf8d7 100644 --- a/src/infuse_iot/commands.py +++ b/src/infuse_iot/commands.py @@ -46,6 +46,7 @@ def DESCRIPTION(self) -> str: class InfuseRpcCommand: RPC_DATA_SEND = False + RPC_DATA_SEND_CHUNKED = False RPC_DATA_RECEIVE = False @classmethod @@ -71,6 +72,10 @@ def data_payload(self) -> bytes: """Payload to send with RPC_DATA""" raise NotImplementedError + def data_payload_chunked(self) -> list[bytes]: + """Payloads to send with RPC_DATA""" + raise NotImplementedError + def data_payload_recv_len(self) -> int: """Length of payload to receive with RPC_DATA""" return 0xFFFFFFFF diff --git a/src/infuse_iot/generated/rpc_definitions.py b/src/infuse_iot/generated/rpc_definitions.py index ff7da25..cf5ca4f 100644 --- a/src/infuse_iot/generated/rpc_definitions.py +++ b/src/infuse_iot/generated/rpc_definitions.py @@ -986,7 +986,9 @@ class data_receiver: COMMAND_ID = 32766 class request(VLACompatLittleEndianStruct): - _fields_ = [] + _fields_ = [ + ("unaligned_input", ctypes.c_uint8), + ] _pack_ = 1 class response(VLACompatLittleEndianStruct): diff --git a/src/infuse_iot/rpc_client.py b/src/infuse_iot/rpc_client.py index 6a7c3fe..e3945d4 100644 --- a/src/infuse_iot/rpc_client.py +++ b/src/infuse_iot/rpc_client.py @@ -84,19 +84,21 @@ def _wait_rpc_rsp(self) -> PacketReceived: continue return rsp.epacket - def run_data_send_cmd( + def _run_data_send_core( self, cmd_id: int, auth: Auth, params: bytes, - data: bytes, + data: list[bytes], + total_size: int, + packet_idx: bool, progress_cb: Callable[[int], None] | None, rsp_decoder: Callable[[bytes], ctypes.LittleEndianStructure], ) -> tuple[rpc.ResponseHeader, ctypes.LittleEndianStructure | None]: self._request_id += 1 ack_period = 1 header = rpc.RequestHeader(self._request_id, cmd_id) # type: ignore - data_hdr = rpc.RequestDataHeader(len(data), ack_period) + data_hdr = rpc.RequestDataHeader(total_size, ack_period) request_packet = bytes(header) + bytes(data_hdr) + params pkt = PacketOutput( @@ -113,18 +115,12 @@ def run_data_send_cmd( if recv.ptype == InfuseType.RPC_RSP: return self._finalise_command(recv, rsp_decoder) - # Send data payloads with maximum interface size + # Send data payloads chunked as requested ack_cnt = -ack_period offset = 0 - size = self._max_payload - ctypes.sizeof(rpc.DataHeader) - # Round payload down to multiple of 4 bytes - size -= size % 4 - while len(data) > 0: - size = min(size, len(data)) - payload = data[:size] - - hdr = rpc.DataHeader(self._request_id, offset) - pkt_bytes = bytes(hdr) + payload + for chunk_id, chunk in enumerate(data): + hdr = rpc.DataHeader(self._request_id, chunk_id if packet_idx else offset) + pkt_bytes = bytes(hdr) + chunk pkt = PacketOutput( self._id, auth, @@ -141,14 +137,42 @@ def run_data_send_cmd( return self._finalise_command(recv, rsp_decoder) ack_cnt = 0 - offset += size - data = data[size:] + offset += len(chunk) if progress_cb: - progress_cb(offset) + progress_cb(chunk_id + 1 if packet_idx else offset) recv = self._wait_rpc_rsp() return self._finalise_command(recv, rsp_decoder) + def run_data_send_cmd( + self, + cmd_id: int, + auth: Auth, + params: bytes, + data: bytes, + progress_cb: Callable[[int], None] | None, + rsp_decoder: Callable[[bytes], ctypes.LittleEndianStructure], + ) -> tuple[rpc.ResponseHeader, ctypes.LittleEndianStructure | None]: + # Maxmimum payload size of interface + size = self._max_payload - ctypes.sizeof(rpc.DataHeader) + # Round payload down to multiple of 4 bytes + size -= size % 4 + # itertools.batched once Python 3.12 is the minimum version + chunks = [data[i : i + size] for i in range(0, len(data), size)] + # Run with pre-computed chunks + return self._run_data_send_core(cmd_id, auth, params, chunks, len(data), False, progress_cb, rsp_decoder) + + def run_data_send_cmd_chunked( + self, + cmd_id: int, + auth: Auth, + params: bytes, + data: list[bytes], + progress_cb: Callable[[int], None] | None, + rsp_decoder: Callable[[bytes], ctypes.LittleEndianStructure], + ) -> tuple[rpc.ResponseHeader, ctypes.LittleEndianStructure | None]: + return self._run_data_send_core(cmd_id, auth, params, data, len(data), True, progress_cb, rsp_decoder) + def run_data_recv_cmd( self, cmd_id: int, diff --git a/src/infuse_iot/rpc_wrappers/lte_at_cmd.py b/src/infuse_iot/rpc_wrappers/lte_at_cmd.py index 73b6597..090ec42 100644 --- a/src/infuse_iot/rpc_wrappers/lte_at_cmd.py +++ b/src/infuse_iot/rpc_wrappers/lte_at_cmd.py @@ -29,9 +29,12 @@ def request_json(self): return {"cmd": self.args.cmd} def handle_response(self, return_code, response): + if response: + # Print returned strings even on failure + response_bytes = bytes(response.rsp) + if len(response_bytes): + decoded = bytes(response.rsp).decode("utf-8").strip() + print(decoded) + # Notification that command failed if return_code != 0: print(f"Failed to run command ({errno.strerror(-return_code)})") - return - decoded = bytes(response.rsp).decode("utf-8").strip() - - print(decoded) diff --git a/src/infuse_iot/tools/ota_upgrade.py b/src/infuse_iot/tools/ota_upgrade.py index 794d51c..2e6ec86 100644 --- a/src/infuse_iot/tools/ota_upgrade.py +++ b/src/infuse_iot/tools/ota_upgrade.py @@ -49,9 +49,12 @@ def __init__(self, args): 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") + # Try the next level up + diff_folder = diff_folder.parent + if diff_folder.name != "diffs": + raise argparse.ArgumentTypeError(f"{args.single} is not in a diff (sub)folder") + release_folder = diff_folder.parent self._release = ValidRelease(str(release_folder)) self._single_diff = args.single else: @@ -251,13 +254,6 @@ 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? @@ -269,6 +265,14 @@ def run(self): self.state_update(live, "Scanning") continue + 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 + # Is signal strong enough to connect? if self._min_rssi and source.rssi < self._min_rssi: continue diff --git a/src/infuse_iot/tools/rpc.py b/src/infuse_iot/tools/rpc.py index 9061d94..0ebf6e8 100644 --- a/src/infuse_iot/tools/rpc.py +++ b/src/infuse_iot/tools/rpc.py @@ -97,6 +97,15 @@ def run(self): self._command.data_progress_cb, decode_fn, ) + elif self._command.RPC_DATA_SEND_CHUNKED: + hdr, rsp = rpc_client.run_data_send_cmd_chunked( + self._command.COMMAND_ID, # type: ignore + self._command.auth_level(), + params, + self._command.data_payload_chunked(), + self._command.data_progress_cb, + decode_fn, + ) elif self._command.RPC_DATA_RECEIVE: hdr, rsp = rpc_client.run_data_recv_cmd( self._command.COMMAND_ID, # type: ignore