From 2cebc70f0281899031313fd9238c0aa7ee611e9e Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Sun, 20 Jul 2025 20:13:20 +1000 Subject: [PATCH 1/4] tools: credentials: subcommand to print API key Add a subcommand to print the current API key value. Signed-off-by: Jordan Yates --- src/infuse_iot/tools/credentials.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/infuse_iot/tools/credentials.py b/src/infuse_iot/tools/credentials.py index 266bebd..5c75b6c 100644 --- a/src/infuse_iot/tools/credentials.py +++ b/src/infuse_iot/tools/credentials.py @@ -20,6 +20,7 @@ class SubCommand(InfuseCommand): @classmethod def add_parser(cls, parser): parser.add_argument("--api-key", type=str, help="Set Infuse-IoT API key") + parser.add_argument("--api-key-print", action="store_true", help="Print Infuse-IoT API key") parser.add_argument("--network", type=ValidFile, help="Load network credentials from file") def __init__(self, args): @@ -28,6 +29,8 @@ def __init__(self, args): def run(self): if self.args.api_key is not None: credentials.set_api_key(self.args.api_key) + if self.args.api_key_print: + print(f"API Key: {credentials.get_api_key()}") if self.args.network is not None: # Read the file with self.args.network.open("r") as f: From de090342a82d33a011da09dc31e3354b3ac7f37b Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Sun, 20 Jul 2025 20:13:50 +1000 Subject: [PATCH 2/4] scripts: apn_set: fix Fix the script to work with the VLA decoding changes. Signed-off-by: Jordan Yates --- scripts/apn_set.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/apn_set.py b/scripts/apn_set.py index 321f086..302cde6 100755 --- a/scripts/apn_set.py +++ b/scripts/apn_set.py @@ -48,7 +48,8 @@ def progress_table(self): class response(VLACompatLittleEndianStruct): _fields_ = [] - vla_field = ("rc", ctypes.c_int16) + vla_field = ("rc", 0 * ctypes.c_int16) + _pack_ = 1 def state_update(self, live: Live, state: str): self.state = state @@ -75,7 +76,7 @@ def announce_observed(self, live: Live, infuse_id: int, pkt: readings.announce): params = bytes(rpc.kv_write.request(1)) + all_vals hdr, rsp = rpc_client.run_standard_cmd( - rpc.kv_write.COMMAND_ID, Auth.DEVICE, params, self.response.from_buffer_copy + rpc.kv_write.COMMAND_ID, Auth.DEVICE, params, self.response.vla_from_buffer_copy ) if hdr.return_code == 0: assert rsp is not None and hasattr(rsp, "rc") From cc88a7c636e68bee9e5dd68413fca4c2b5fca494 Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Sun, 20 Jul 2025 20:14:23 +1000 Subject: [PATCH 3/4] tools: gateway: broadcast `ConnectionDropped` event If the connected basestation reports that a connection has dropped, forward that event to connected clients. Signed-off-by: Jordan Yates --- src/infuse_iot/epacket/interface.py | 7 +++++++ src/infuse_iot/tools/gateway.py | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/infuse_iot/epacket/interface.py b/src/infuse_iot/epacket/interface.py index 8c6fc63..f178195 100644 --- a/src/infuse_iot/epacket/interface.py +++ b/src/infuse_iot/epacket/interface.py @@ -6,6 +6,7 @@ from typing_extensions import Self import infuse_iot.generated.rpc_definitions as rpc_defs +import infuse_iot.generated.tdf_definitions as tdf_defs from infuse_iot.epacket.common import Serializable from infuse_iot.util.ctypes import bytes_to_uint8 @@ -83,6 +84,12 @@ def from_rpc_struct(cls, struct: rpc_defs.rpc_struct_bt_addr_le): return cls(struct.type, int.from_bytes(struct.val, "little")) + @classmethod + def from_tdf_struct(cls, struct: tdf_defs.structs.tdf_struct_bt_addr_le): + """Create instance from the common TDF address structure""" + + return cls(struct.type, struct.val) + def __init__(self, val): self.val = val diff --git a/src/infuse_iot/tools/gateway.py b/src/infuse_iot/tools/gateway.py index e58b068..3910b3c 100644 --- a/src/infuse_iot/tools/gateway.py +++ b/src/infuse_iot/tools/gateway.py @@ -21,7 +21,8 @@ import infuse_iot.epacket.interface as interface import infuse_iot.generated.rpc_definitions as defs -from infuse_iot import rpc +import infuse_iot.generated.tdf_definitions as tdf_defs +from infuse_iot import rpc, tdf from infuse_iot.commands import InfuseCommand from infuse_iot.common import InfuseID, InfuseType from infuse_iot.database import ( @@ -39,6 +40,7 @@ from infuse_iot.socket_comms import ( ClientNotification, ClientNotificationConnectionCreated, + ClientNotificationConnectionDropped, ClientNotificationConnectionFailed, ClientNotificationEpacketReceived, GatewayRequestConnectionRelease, @@ -153,6 +155,7 @@ def __init__(self, common: CommonThreadState, log: io.TextIOWrapper): self._line = "" self._log = log self._next_ping = 0.0 + self._tdf_decoder = tdf.TDF() super().__init__(self._iter) def _iter(self) -> None: @@ -190,6 +193,17 @@ class memfault_chunk_header(ctypes.LittleEndianStructure): p = p[3 + hdr.len :] print(f"Memfault Chunk {hdr.cnt:3d}: {base64.b64encode(chunk).decode('utf-8')}") + def _handle_local_tdf(self, pkt: PacketReceived): + if self._common.server is None: + # No-one to broadcast events to + return + for reading in self._tdf_decoder.decode(pkt.payload): + if isinstance(reading.data[0], tdf_defs.readings.bluetooth_connection) and reading.data[0].connected == 0: + if_addr = interface.Address.BluetoothLeAddr.from_tdf_struct(reading.data[0].address) + infuse_id = self._common.ddb.infuse_id_from_bluetooth(if_addr) + if infuse_id: + self._common.server.broadcast(ClientNotificationConnectionDropped(infuse_id)) + def _handle_serial_frame(self, frame: bytearray): try: # Decode the serial packet @@ -216,6 +230,9 @@ def _handle_serial_frame(self, frame: bytearray): # Iterate over all contained subpackets for pkt in decoded: Console.log_rx(pkt.ptype, len(frame)) + # Handle any local TDFs + if len(pkt.route) == 1 and pkt.ptype == InfuseType.TDF: + self._handle_local_tdf(pkt) # Handle any local RPC responses self._common.rpc.handle(pkt) # Handle any Memfault chunks From bfc99a01d842d5dc6d2aaabddb0f21c25c1bb42a Mon Sep 17 00:00:00 2001 From: Jordan Yates Date: Sun, 20 Jul 2025 22:37:33 +1000 Subject: [PATCH 4/4] tools: ota_upgrade: support list of devices in file Support providing a list of devices to upgrade in a file. Signed-off-by: Jordan Yates --- src/infuse_iot/tools/ota_upgrade.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/infuse_iot/tools/ota_upgrade.py b/src/infuse_iot/tools/ota_upgrade.py index 1512f23..f31308d 100644 --- a/src/infuse_iot/tools/ota_upgrade.py +++ b/src/infuse_iot/tools/ota_upgrade.py @@ -26,7 +26,7 @@ LocalClient, default_multicast_address, ) -from infuse_iot.util.argparse import ValidRelease +from infuse_iot.util.argparse import ValidFile, ValidRelease class SubCommand(InfuseCommand): @@ -38,7 +38,7 @@ def __init__(self, args): self._client = LocalClient(default_multicast_address(), 1.0) self._conn_timeout = args.conn_timeout self._min_rssi: int | None = args.rssi - self._single_id: int | None = args.id + self._explicit_ids: list[int] = [] self._release: ValidRelease = args.release self._app_name = self._release.metadata["application"]["primary"] self._app_id = self._release.metadata["application"]["id"] @@ -63,17 +63,26 @@ def __init__(self, args): else: self._log = open(args.log, "+a", encoding="utf-8") # noqa: SIM115 + if args.id is not None: + self._explicit_ids.append(args.id) + elif args.list is not None: + with args.list.open("r") as f: + for line in f.readlines(): + self._explicit_ids.append(int(line.strip(), 0)) + @classmethod def add_parser(cls, parser): parser.add_argument( "--release", "-r", type=ValidRelease, required=True, help="Application release to upgrade to" ) parser.add_argument("--rssi", type=int, help="Minimum RSSI to attempt upgrade process") - parser.add_argument("--id", type=lambda x: int(x, 0), help="Single device to upgrade") parser.add_argument("--log", type=str, help="File to write upgrade results to") parser.add_argument( "--conn-timeout", type=int, default=10000, help="Timeout to wait for a connection to the device (ms)" ) + explicit = parser.add_mutually_exclusive_group() + explicit.add_argument("--id", type=lambda x: int(x, 0), help="Single device to upgrade") + explicit.add_argument("--list", type=ValidFile, help="File containing a list of IDs to upgrade") def progress_table(self): table = Table() @@ -111,14 +120,16 @@ def run(self): with Live(self.progress_table(), refresh_per_second=4) as live: for source, announce in self._client.observe_announce(): self.state_update(live, "Scanning") - if announce.application != self._app_id and not self._single_id: - continue - if self._single_id: - if self._single_id != source.infuse_id: + if len(self._explicit_ids): + if source.infuse_id not in self._explicit_ids: continue - if self._single_id in self._handled: - # The one device we care about has been upgraded + if len(self._handled) == len(self._explicit_ids): + # We've handled all devices + self.state_update(live, "All devices updated") return + else: + if announce.application != self._app_id: + continue if source.infuse_id in self._handled: continue v = announce.version