From 96644477ff610554c9a0612ed1f006ad790dcec1 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Wed, 11 Feb 2026 13:42:11 +1000 Subject: [PATCH 1/5] tools: bt_log: make `--id` required The Infuse ID is a required argument for operation. Signed-off-by: Jordan Yates --- src/infuse_iot/tools/bt_log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/infuse_iot/tools/bt_log.py b/src/infuse_iot/tools/bt_log.py index 49a34bb..56553e1 100644 --- a/src/infuse_iot/tools/bt_log.py +++ b/src/infuse_iot/tools/bt_log.py @@ -35,7 +35,7 @@ def __init__(self, args): @classmethod def add_parser(cls, parser): - parser.add_argument("--id", type=lambda x: int(x, 0), help="Infuse ID to receive logs for") + parser.add_argument("--id", type=lambda x: int(x, 0), required=True, help="Infuse ID to receive logs for") parser.add_argument("--data", action="store_true", help="Subscribe to the data characteristic as well") parser.add_argument( "--conn-timeout", type=int, default=10000, help="Timeout to wait for a connection to the device (ms)" From 6d8c1da5eea1efbcd9d428f522179a462f4d54ad Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Wed, 11 Feb 2026 13:42:53 +1000 Subject: [PATCH 2/5] tools: gateway: handle positive error codes Any non-zero response from the connect RPC is an error, not just negative values. Signed-off-by: Jordan Yates --- src/infuse_iot/tools/gateway.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/infuse_iot/tools/gateway.py b/src/infuse_iot/tools/gateway.py index ba1ed58..be27f4e 100644 --- a/src/infuse_iot/tools/gateway.py +++ b/src/infuse_iot/tools/gateway.py @@ -114,7 +114,7 @@ def handle(self, pkt: PacketReceived): infuse_id = self._ddb.infuse_id_from_bluetooth(if_addr) if infuse_id is None: Console.log_error(f"Infuse ID of {if_addr} not known") - else: + elif header.return_code == 0: self._ddb.observe_security_state( infuse_id, bytes(resp.cloud_public_key), @@ -369,7 +369,7 @@ def _bt_connect_cb(self, pkt: PacketReceived, rc: int, response: bytes): assert infuse_id is not None, "ID was required to initiate connection?" assert self._common.server is not None - if rc < 0: + if rc != 0: rsp = ClientNotificationConnectionFailed(infuse_id) self._common.notification_broadcast(rsp) return From 07a5bbf27096d6b8efe3f70d42582eff19553044 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Wed, 11 Feb 2026 13:43:49 +1000 Subject: [PATCH 3/5] rpc_client: internal timeouts Add internal timeouts when waiting for `RPC_RSP` and `RPC_DATA_ACK` packets. Signed-off-by: Jordan Yates --- scripts/apn_set.py | 2 ++ scripts/reboot_count_reset.py | 2 ++ src/infuse_iot/rpc_client.py | 34 ++++++++++++++++++++++------- src/infuse_iot/tools/ota_upgrade.py | 13 +++++++---- src/infuse_iot/tools/rpc.py | 3 ++- 5 files changed, 41 insertions(+), 13 deletions(-) diff --git a/scripts/apn_set.py b/scripts/apn_set.py index 4d81c04..4de53d2 100755 --- a/scripts/apn_set.py +++ b/scripts/apn_set.py @@ -78,6 +78,8 @@ def announce_observed(self, live: Live, infuse_id: int, pkt: readings.announce | hdr, rsp = rpc_client.run_standard_cmd( rpc.kv_write.COMMAND_ID, Auth.DEVICE, params, self.response.vla_from_buffer_copy ) + if hdr is None: + return if hdr.return_code == 0: assert rsp is not None and hasattr(rsp, "rc") if rsp.rc[0] == 0: diff --git a/scripts/reboot_count_reset.py b/scripts/reboot_count_reset.py index 9694145..c135058 100755 --- a/scripts/reboot_count_reset.py +++ b/scripts/reboot_count_reset.py @@ -67,6 +67,8 @@ def announce_observed(self, live: Live, infuse_id: int, pkt: readings.announce | hdr, _ = rpc_client.run_standard_cmd( rpc.kv_write.COMMAND_ID, Auth.DEVICE, params, rpc.kv_write.response.from_buffer_copy ) + if hdr is None: + return if hdr.return_code == 0: self.updated.append(infuse_id) diff --git a/src/infuse_iot/rpc_client.py b/src/infuse_iot/rpc_client.py index d5ca801..5ac46ad 100644 --- a/src/infuse_iot/rpc_client.py +++ b/src/infuse_iot/rpc_client.py @@ -2,6 +2,7 @@ import ctypes import random +import time from collections.abc import Callable from infuse_iot import rpc @@ -25,11 +26,15 @@ def __init__( rx_cb: Callable[[ClientNotification], None] | None = None, ): self._request_id = random.randint(0, 2**31 - 1) + self._timeout = 10.0 self._client = client self._id = infuse_id self._max_payload = max_payload self._rx_cb = rx_cb + def set_timeout(self, timeout: float): + self._timeout = timeout + def _finalise_command( self, rpc_rsp: PacketReceived, rsp_decoder: Callable[[bytes], ctypes.LittleEndianStructure] ) -> tuple[rpc.ResponseHeader, ctypes.LittleEndianStructure | None]: @@ -48,8 +53,9 @@ def _client_recv(self) -> ClientNotification | None: self._rx_cb(rsp) return rsp - def _wait_data_ack(self) -> PacketReceived: - while True: + def _wait_data_ack(self) -> PacketReceived | None: + timeout = time.time() + self._timeout + while time.time() < timeout: rsp = self._client_recv() if rsp is None: continue @@ -66,10 +72,12 @@ def _wait_data_ack(self) -> PacketReceived: if data_ack.request_id != self._request_id: continue return rsp.epacket + return None - def _wait_rpc_rsp(self) -> PacketReceived: + def _wait_rpc_rsp(self) -> PacketReceived | None: + timeout = time.time() + self._timeout # Wait for responses - while True: + while time.time() < timeout: rsp = self._client_recv() if rsp is None: continue @@ -83,6 +91,7 @@ def _wait_rpc_rsp(self) -> PacketReceived: if rsp_header.request_id != self._request_id: continue return rsp.epacket + return None def _run_data_send_core( self, @@ -94,7 +103,7 @@ def _run_data_send_core( packet_idx: bool, progress_cb: Callable[[int], None] | None, rsp_decoder: Callable[[bytes], ctypes.LittleEndianStructure], - ) -> tuple[rpc.ResponseHeader, ctypes.LittleEndianStructure | None]: + ) -> tuple[rpc.ResponseHeader | None, ctypes.LittleEndianStructure | None]: self._request_id += 1 ack_period = 2 header = rpc.RequestHeader(self._request_id, cmd_id) # type: ignore @@ -112,6 +121,8 @@ def _run_data_send_core( # Wait for initial ACK recv = self._wait_data_ack() + if recv is None: + return None, None if recv.ptype == InfuseType.RPC_RSP: return self._finalise_command(recv, rsp_decoder) @@ -133,6 +144,8 @@ def _run_data_send_core( # Wait for ACKs at the period if ack_cnt == ack_period: recv = self._wait_data_ack() + if recv is None: + return None, None if recv.ptype == InfuseType.RPC_RSP: return self._finalise_command(recv, rsp_decoder) ack_cnt = 0 @@ -142,6 +155,9 @@ def _run_data_send_core( progress_cb(chunk_id + 1 if packet_idx else offset) recv = self._wait_rpc_rsp() + if recv is None: + return None, None + return self._finalise_command(recv, rsp_decoder) def run_data_send_cmd( @@ -152,7 +168,7 @@ def run_data_send_cmd( data: bytes, progress_cb: Callable[[int], None] | None, rsp_decoder: Callable[[bytes], ctypes.LittleEndianStructure], - ) -> tuple[rpc.ResponseHeader, ctypes.LittleEndianStructure | None]: + ) -> tuple[rpc.ResponseHeader | None, ctypes.LittleEndianStructure | None]: # Maxmimum payload size of interface size = self._max_payload - ctypes.sizeof(rpc.DataHeader) # Round payload down to multiple of 4 bytes @@ -170,7 +186,7 @@ def run_data_send_cmd_chunked( data: list[bytes], progress_cb: Callable[[int], None] | None, rsp_decoder: Callable[[bytes], ctypes.LittleEndianStructure], - ) -> tuple[rpc.ResponseHeader, ctypes.LittleEndianStructure | None]: + ) -> tuple[rpc.ResponseHeader | None, 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( @@ -225,7 +241,7 @@ def run_data_recv_cmd( def run_standard_cmd( self, cmd_id: int, auth: Auth, params: bytes, rsp_decoder: Callable[[bytes], ctypes.LittleEndianStructure] - ) -> tuple[rpc.ResponseHeader, ctypes.LittleEndianStructure | None]: + ) -> tuple[rpc.ResponseHeader | None, ctypes.LittleEndianStructure | None]: self._request_id += 1 header = rpc.RequestHeader(self._request_id, cmd_id) # type: ignore @@ -239,4 +255,6 @@ def run_standard_cmd( req = GatewayRequestEpacketSend(pkt) self._client.send(req) recv = self._wait_rpc_rsp() + if recv is None: + return None, None return self._finalise_command(recv, rsp_decoder) diff --git a/src/infuse_iot/tools/ota_upgrade.py b/src/infuse_iot/tools/ota_upgrade.py index db0f512..cba1dd9 100644 --- a/src/infuse_iot/tools/ota_upgrade.py +++ b/src/infuse_iot/tools/ota_upgrade.py @@ -153,8 +153,9 @@ def gateway_diff_load(self): 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)}))") + return_code = hdr.return_code if hdr else -1 + if return_code != 0: + sys.exit(f"Failed to save diff file to gateway (({errno.strerror(-return_code)}))") print(f"'{self._single_diff}' written to gateway") def run_file_upload(self, live: Live, mtu: int, source: HopReceived): @@ -172,7 +173,9 @@ def run_file_upload(self, live: Live, mtu: int, source: HopReceived): file_write_basic.response.from_buffer_copy, ) - if hdr.return_code == 0: + if hdr is None: + self._failed += 1 + elif hdr.return_code == 0: self._pending[source.infuse_id] = time.time() + 60 def run_file_copy(self, live: Live, mtu: int, source: HopReceived): @@ -195,7 +198,9 @@ def run_file_copy(self, live: Live, mtu: int, source: HopReceived): bytes(params), bt_file_copy_basic.response.from_buffer_copy, ) - if hdr.return_code == 0: + if hdr is None: + self._failed += 1 + elif hdr.return_code == 0: self._pending[source.infuse_id] = time.time() + 60 def run(self): diff --git a/src/infuse_iot/tools/rpc.py b/src/infuse_iot/tools/rpc.py index c8542fe..a2b52b1 100644 --- a/src/infuse_iot/tools/rpc.py +++ b/src/infuse_iot/tools/rpc.py @@ -126,8 +126,9 @@ def run(self): params, decode_fn, ) + return_code = hdr.return_code if hdr else -1 # Handle response - self._command.handle_response(hdr.return_code, rsp) + self._command.handle_response(return_code, rsp) if self._args.conn_log: while True: From e7e072e15264177e89afedd8f4156956d7fb6422 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Thu, 12 Feb 2026 10:09:46 +1000 Subject: [PATCH 4/5] tools: tdf_list: device ID filtering Add the option to filter TDFs by Infuse device ID. Signed-off-by: Jordan Yates --- src/infuse_iot/tools/tdf_list.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/infuse_iot/tools/tdf_list.py b/src/infuse_iot/tools/tdf_list.py index 2ac9eef..b65a2d1 100644 --- a/src/infuse_iot/tools/tdf_list.py +++ b/src/infuse_iot/tools/tdf_list.py @@ -30,11 +30,15 @@ class SubCommand(InfuseCommand): @classmethod def add_parser(cls, parser): parser.add_argument("--array-all", action="store_true", help="Display all array values, not just the last") + parser.add_argument( + "--id", type=lambda x: int(x, 0), action="append", default=[], help="Limit displayed TDFs by device ID" + ) def __init__(self, args): self._client = LocalClient(default_multicast_address(), 1.0) self._decoder = TDF() self._array_all = args.array_all + self._ids = args.id def append_tdf( self, @@ -111,13 +115,16 @@ def run(self) -> None: continue source = msg.epacket.route[0] + if len(self._ids) > 0 and source.infuse_id not in self._ids: + continue + table: list[tuple[str | None, str | None, str, str, str]] = [] tdf: TDF.Reading for tdf in self._decoder.decode(msg.epacket.payload): self.append_readings(table, tdf) - print(f"Infuse ID: {source.infuse_id:016x}") + print(f"Infuse ID: 0x{source.infuse_id:016x}") print(f"Interface: {source.interface.name}") print(f" Address: {source.interface_address}") print(f" RSSI: {source.rssi} dBm") From a421fe93107641577813d3362ac2bbb07e97182f Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Thu, 12 Feb 2026 10:11:51 +1000 Subject: [PATCH 5/5] tools: tdf_list: packet RSSI filtering Add the option to filter TDFs by packet RSSI. Signed-off-by: Jordan Yates --- src/infuse_iot/tools/tdf_list.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/infuse_iot/tools/tdf_list.py b/src/infuse_iot/tools/tdf_list.py index b65a2d1..ef11a55 100644 --- a/src/infuse_iot/tools/tdf_list.py +++ b/src/infuse_iot/tools/tdf_list.py @@ -33,12 +33,14 @@ def add_parser(cls, parser): parser.add_argument( "--id", type=lambda x: int(x, 0), action="append", default=[], help="Limit displayed TDFs by device ID" ) + parser.add_argument("--min-rssi", type=int, help="Minimum RSSI to display TDF") def __init__(self, args): self._client = LocalClient(default_multicast_address(), 1.0) self._decoder = TDF() self._array_all = args.array_all self._ids = args.id + self._min_rssi = args.min_rssi def append_tdf( self, @@ -117,6 +119,8 @@ def run(self) -> None: if len(self._ids) > 0 and source.infuse_id not in self._ids: continue + if self._min_rssi is not None and source.rssi < self._min_rssi: + continue table: list[tuple[str | None, str | None, str, str, str]] = []