From b5facaedb807a1975e269be5bdc88cfbf62269fe Mon Sep 17 00:00:00 2001 From: Aeyohan Furtado Date: Fri, 10 Jul 2026 14:19:31 +1000 Subject: [PATCH 1/3] util: argparse: add helpers for `--server-port` Add an argparse helper to parse port numbers and preprocess them into a socket (addr/port tuple). Add an argparse helper to add a `--server-port` argument to tools that need it. This can be used to specify a different UDP port to allow for multiple gateway scripts and corresponding connections to run simultaneously. Signed-off-by: Aeyohan Furtado --- src/infuse_iot/util/argparse.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/infuse_iot/util/argparse.py b/src/infuse_iot/util/argparse.py index 36c2d14..f357ca2 100644 --- a/src/infuse_iot/util/argparse.py +++ b/src/infuse_iot/util/argparse.py @@ -8,6 +8,7 @@ import yaml from infuse_iot.definitions.rpc import rpc_enum_bt_le_addr_type, rpc_struct_bt_addr_le +from infuse_iot.socket_comms import default_multicast_address from infuse_iot.util.ctypes import bytes_to_uint8 @@ -99,3 +100,28 @@ def __new__(cls, string: str) -> bytes: # type: ignore return bytes.fromhex(string) except ValueError as e: raise argparse.ArgumentTypeError(f"{string} is not a valid hex ID") from e + +class ServerPort: + """Server port number to socket tuple""" + + def __new__(cls, string: str) -> tuple[str, int]: # type: ignore + try: + port = int(string) + except ValueError as e: + raise argparse.ArgumentTypeError(f"{string} is not a valid port number") from e + if not (0 < port <= 65535): + raise argparse.ArgumentTypeError(f"{string} is not a valid port number") + if port % 2 == 0: + raise argparse.ArgumentError(None, f"`--server-port` must be odd: {port}") + return default_multicast_address(port) + +def add_server_port_parser(parser: argparse.ArgumentParser, multi_port: bool = False): + """Register `--server-port`with an argument parser. `multi_port` allows multiple port(s)""" + parser.add_argument( + '--server-port', + dest='server_sock', + default=[default_multicast_address()] if multi_port else default_multicast_address(), + type=ServerPort, + nargs= '+' if multi_port else None, + help="Alternate port to use for Gateway connections (default 8751)" + ) From c7e42421b2364f10b54bb61f71c6482b2e790e3c Mon Sep 17 00:00:00 2001 From: Aeyohan Furtado Date: Mon, 13 Jul 2026 11:14:18 +1000 Subject: [PATCH 2/3] tests: util: argparse: add server port tests Added tests for the `add_server_port_parser` function for registering the `--server-port` argument. Tests include checking correct single and multi-port handling and default values for each. Added test for the `ServerPort` type to ensure that it correctly parses valid and invalid port numbers. Signed-off-by: Aeyohan Furtado --- tests/util/test_argparse.py | 53 ++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/tests/util/test_argparse.py b/tests/util/test_argparse.py index 45a2d03..64831fd 100644 --- a/tests/util/test_argparse.py +++ b/tests/util/test_argparse.py @@ -6,7 +6,15 @@ import pytest -from infuse_iot.util.argparse import BtLeAddress, HexString, InfuseDeviceId, ValidDir, ValidFile +from infuse_iot.util.argparse import ( + BtLeAddress, + HexString, + InfuseDeviceId, + ServerPort, + ValidDir, + ValidFile, + add_server_port_parser, +) assert "TOXTEMPDIR" in os.environ, "you must run these tests using tox" @@ -72,3 +80,46 @@ def test_hexstring(): assert HexString("AABB") == b"\xaa\xbb" assert HexString("aa00bb") == b"\xaa\x00\xbb" assert HexString("00AABB") == b"\x00\xaa\xbb" + +def test_server_port(): + with pytest.raises(argparse.ArgumentTypeError): + ServerPort("NotAnInt") + with pytest.raises(argparse.ArgumentTypeError): + ServerPort("-1") + with pytest.raises(argparse.ArgumentTypeError): + ServerPort("65536") + with pytest.raises(argparse.ArgumentError): + # Must be odd + ServerPort("2") + with pytest.raises(argparse.ArgumentTypeError): + # Cannot be wildcard port + ServerPort("0") + assert ServerPort("1") == ("224.1.1.1", 1) + assert ServerPort("8751") == ("224.1.1.1", 8751) + assert ServerPort("65535") == ("224.1.1.1", 65535) + +def test_server_port_parser(capsys): + parser = argparse.ArgumentParser() + add_server_port_parser(parser) + + args = parser.parse_args([]) + assert args.server_sock == ("224.1.1.1", 8751) + args = parser.parse_args(["--server-port", "8751"]) + assert args.server_sock == ("224.1.1.1", 8751) + args = parser.parse_args(["--server-port", "8753"]) + assert args.server_sock == ("224.1.1.1", 8753) + with pytest.raises(SystemExit): + # Additional port supplied when not supported + args = parser.parse_args(["--server-port", "8751", "8753"]) + + parser = argparse.ArgumentParser() + add_server_port_parser(parser, multi_port=True) + + args = parser.parse_args([]) + assert args.server_sock == [("224.1.1.1", 8751)] + args = parser.parse_args(["--server-port", "8751"]) + assert args.server_sock == [("224.1.1.1", 8751)] + args = parser.parse_args(["--server-port", "8753"]) + assert args.server_sock == [("224.1.1.1", 8753)] + args = parser.parse_args(["--server-port", "8751", "8753"]) + assert args.server_sock == [("224.1.1.1", 8751), ("224.1.1.1", 8753)] From f560e6d7711029583aa26b2fff6d76f5ab0489b6 Mon Sep 17 00:00:00 2001 From: Aeyohan Furtado Date: Fri, 10 Jul 2026 14:22:03 +1000 Subject: [PATCH 3/3] tools: enable `--server-port` arguments Added the `--server-port` argument to any tools that act or interact with a gateway. Signed-off-by: Aeyohan Furtado --- src/infuse_iot/tools/annotate_events.py | 7 ++++--- src/infuse_iot/tools/audio_record.py | 7 ++++--- src/infuse_iot/tools/auto_activate.py | 6 ++++-- src/infuse_iot/tools/bt_log.py | 6 +++--- src/infuse_iot/tools/data_logger_sync.py | 7 ++++--- src/infuse_iot/tools/gateway.py | 8 +++----- src/infuse_iot/tools/localhost.py | 5 +++-- src/infuse_iot/tools/native_bt.py | 6 +++--- src/infuse_iot/tools/ota_upgrade.py | 7 ++++--- src/infuse_iot/tools/rpc.py | 7 ++++--- src/infuse_iot/tools/serial_throughput.py | 5 +++-- src/infuse_iot/tools/tdf_csv.py | 5 +++-- src/infuse_iot/tools/tdf_list.py | 6 +++--- 13 files changed, 45 insertions(+), 37 deletions(-) diff --git a/src/infuse_iot/tools/annotate_events.py b/src/infuse_iot/tools/annotate_events.py index a099318..61408dc 100644 --- a/src/infuse_iot/tools/annotate_events.py +++ b/src/infuse_iot/tools/annotate_events.py @@ -25,10 +25,9 @@ ClientNotificationConnectionDropped, GatewayRequestConnectionRequest, LocalClient, - default_multicast_address, ) from infuse_iot.time import InfuseTime -from infuse_iot.util.argparse import InfuseDeviceId, ValidFile +from infuse_iot.util.argparse import InfuseDeviceId, ValidFile, add_server_port_parser from infuse_iot.util.console import choose_one from infuse_iot.zephyr.errno import errno @@ -121,6 +120,8 @@ def add_parser(cls, parser): parser.add_argument("--id", type=InfuseDeviceId, help="Device to log events to") + add_server_port_parser(parser) + def __init__(self, args): self._label_type = args.labels self._time_check = args.time or TimeCheckType.DEFAULT @@ -153,7 +154,7 @@ def __init__(self, args): self._labels = [] self._logger: rpc_enum_data_logger = args.logger - self._client = LocalClient(default_multicast_address(), 1.0) + self._client = LocalClient(args.server_sock, 1.0) self._device_id = args.id self.rpc_client: RpcClient | None = None self.connected = False diff --git a/src/infuse_iot/tools/audio_record.py b/src/infuse_iot/tools/audio_record.py index 4d040a1..00a75f4 100644 --- a/src/infuse_iot/tools/audio_record.py +++ b/src/infuse_iot/tools/audio_record.py @@ -19,10 +19,9 @@ ClientNotificationEpacketReceived, GatewayRequestConnectionRequest, LocalClient, - default_multicast_address, ) from infuse_iot.tdf import TDF -from infuse_iot.util.argparse import InfuseDeviceId +from infuse_iot.util.argparse import InfuseDeviceId, add_server_port_parser from infuse_iot.util.console import Console @@ -32,7 +31,7 @@ class SubCommand(InfuseCommand): DESCRIPTION = "Record audio data to a file from TDF" def __init__(self, args): - self._client = LocalClient(default_multicast_address(), 1.0) + self._client = LocalClient(args.server_sock, 1.0) self._decoder = TDF() if args.gateway: self._id = InfuseID.GATEWAY @@ -54,6 +53,8 @@ def add_parser(cls, parser): ) parser.add_argument("--name", type=str, help="Filename prefix") + add_server_port_parser(parser) + def handle_channel(self, channel: str, stack: ExitStack, tdf: TDF.Reading): if channel == "left": chan = self._left diff --git a/src/infuse_iot/tools/auto_activate.py b/src/infuse_iot/tools/auto_activate.py index bdde0af..6389910 100644 --- a/src/infuse_iot/tools/auto_activate.py +++ b/src/infuse_iot/tools/auto_activate.py @@ -18,9 +18,9 @@ from infuse_iot.socket_comms import ( GatewayRequestConnectionRequest, LocalClient, - default_multicast_address, ) from infuse_iot.tdf import TDF +from infuse_iot.util.argparse import add_server_port_parser class SubCommand(InfuseCommand): @@ -31,7 +31,7 @@ class SubCommand(InfuseCommand): def __init__(self, args): self.app_ids = args.app self.active = args.active or False - self.client = LocalClient(default_multicast_address(), 1.0) + self.client = LocalClient(args.server_sock, 1.0) self.decoder = TDF() self.state = "Scanning" self.name = "Active" if args.active else "Inactive" @@ -47,6 +47,8 @@ def add_parser(cls, parser): mode_group.add_argument("--active", action="store_true", help="Move all devices to active state") mode_group.add_argument("--inactive", action="store_true", help="Move all devices to inactive state") + add_server_port_parser(parser) + def progress_table(self): table = Table() table.add_column() diff --git a/src/infuse_iot/tools/bt_log.py b/src/infuse_iot/tools/bt_log.py index 9a93a67..478a089 100644 --- a/src/infuse_iot/tools/bt_log.py +++ b/src/infuse_iot/tools/bt_log.py @@ -15,10 +15,9 @@ ClientNotificationEpacketReceived, GatewayRequestConnectionRequest, LocalClient, - default_multicast_address, ) from infuse_iot.tdf import TDF -from infuse_iot.util.argparse import InfuseDeviceId +from infuse_iot.util.argparse import InfuseDeviceId, add_server_port_parser from infuse_iot.util.console import Console @@ -28,7 +27,7 @@ class SubCommand(InfuseCommand): DESCRIPTION = "Connect to remote Bluetooth device serial logs" def __init__(self, args): - self._client = LocalClient(default_multicast_address(), 1.0) + self._client = LocalClient(args.server_sock, 1.0) self._decoder = TDF() self._id = args.id self._data = args.data @@ -41,6 +40,7 @@ def add_parser(cls, parser): parser.add_argument( "--conn-timeout", type=int, default=10000, help="Timeout to wait for a connection to the device (ms)" ) + add_server_port_parser(parser) def run(self): if not self._client.comms_check(): diff --git a/src/infuse_iot/tools/data_logger_sync.py b/src/infuse_iot/tools/data_logger_sync.py index e08de7d..54dc5c5 100644 --- a/src/infuse_iot/tools/data_logger_sync.py +++ b/src/infuse_iot/tools/data_logger_sync.py @@ -28,9 +28,8 @@ from infuse_iot.socket_comms import ( GatewayRequestConnectionRequest, LocalClient, - default_multicast_address, ) -from infuse_iot.util.argparse import ValidDir +from infuse_iot.util.argparse import ValidDir, add_server_port_parser class DeviceState: @@ -65,7 +64,7 @@ class SubCommand(InfuseCommand): DESCRIPTION = "Synchronise data logger state from remote devices" def __init__(self, args): - self._client = LocalClient(default_multicast_address(), 1.0) + self._client = LocalClient(args.server_sock, 1.0) self._min_rssi: int | None = args.rssi self._app = args.app self._out = args.out @@ -112,6 +111,8 @@ def add_parser(cls, parser): help="Synchronise removable loggers", ) + add_server_port_parser(parser) + def progress_table(self): table = Table() table.add_column("Device ID") diff --git a/src/infuse_iot/tools/gateway.py b/src/infuse_iot/tools/gateway.py index 20c7ba7..da86bf1 100644 --- a/src/infuse_iot/tools/gateway.py +++ b/src/infuse_iot/tools/gateway.py @@ -51,9 +51,8 @@ GatewayRequestEpacketSend, GatewayRequestObservedDevices, LocalServer, - default_multicast_address, ) -from infuse_iot.util.argparse import ValidFile +from infuse_iot.util.argparse import ValidFile, add_server_port_parser from infuse_iot.util.console import Console from infuse_iot.util.os import is_wsl from infuse_iot.util.threading import SignaledThread @@ -529,7 +528,7 @@ def add_parser(cls, parser): ) parser.add_argument("--baud", type=int, default=115200, help="Baudrate for serial port") parser.add_argument("--root", type=ValidFile, help="Root identity certificate to use instead of cloud") - parser.add_argument("--server-port", type=int, help="Alternate multicast port to use") + add_server_port_parser(parser) def __init__(self, args: argparse.Namespace): self.port: SerialLike @@ -545,8 +544,7 @@ def __init__(self, args: argparse.Namespace): if args.display_only: self.server = None else: - addr = default_multicast_address(args.server_port) if args.server_port else default_multicast_address() - self.server = LocalServer(addr) + self.server = LocalServer(args.server_sock) self.rpc_server = LocalRpcServer(self.ddb) self._common = CommonThreadState(self.server, self.port, self.ddb, self.rpc_server) self.log = args.log diff --git a/src/infuse_iot/tools/localhost.py b/src/infuse_iot/tools/localhost.py index 68aad5f..c530a7d 100644 --- a/src/infuse_iot/tools/localhost.py +++ b/src/infuse_iot/tools/localhost.py @@ -26,10 +26,10 @@ from infuse_iot.socket_comms import ( ClientNotificationEpacketReceived, LocalClient, - default_multicast_address, ) from infuse_iot.tdf import TDF from infuse_iot.time import InfuseTime +from infuse_iot.util.argparse import add_server_port_parser from infuse_iot.util.console import Console from infuse_iot.util.threading import SignaledThread @@ -42,6 +42,7 @@ class SubCommand(InfuseCommand): @classmethod def add_parser(cls, parser): parser.add_argument("--port", "-p", type=int, default=8080, help="Port number for localhost server") + add_server_port_parser(parser) def __init__(self, args): self._data_lock = threading.Lock() @@ -51,7 +52,7 @@ def __init__(self, args): self._data: dict[int, dict] = {} self._port: int = args.port - self._client = LocalClient(default_multicast_address(), 1.0) + self._client = LocalClient(args.server_sock, 1.0) self._decoder = TDF() # Serve the HTML file diff --git a/src/infuse_iot/tools/native_bt.py b/src/infuse_iot/tools/native_bt.py index c7f52f1..7a1b8e2 100644 --- a/src/infuse_iot/tools/native_bt.py +++ b/src/infuse_iot/tools/native_bt.py @@ -43,9 +43,8 @@ GatewayRequestConnectionRequest, GatewayRequestEpacketSend, LocalServer, - default_multicast_address, ) -from infuse_iot.util.argparse import BtLeAddress, ValidFile +from infuse_iot.util.argparse import BtLeAddress, ValidFile, add_server_port_parser from infuse_iot.util.console import Console @@ -212,11 +211,12 @@ class SubCommand(InfuseCommand): @classmethod def add_parser(cls, parser): parser.add_argument("--root", type=ValidFile, help="Root identity certificate to use instead of cloud") + add_server_port_parser(parser) def __init__(self, args: argparse.Namespace): self.infuse_manu = 0x0DE4 self.database = DeviceDatabase(args.root) - self.server = LocalServer(default_multicast_address()) + self.server = LocalServer(args.server_sock) self.bleak_mapping: dict[int, BLEDevice] = {} self.unknown_networks: set[int] = set() Console.init() diff --git a/src/infuse_iot/tools/ota_upgrade.py b/src/infuse_iot/tools/ota_upgrade.py index 42a63d2..ebed674 100644 --- a/src/infuse_iot/tools/ota_upgrade.py +++ b/src/infuse_iot/tools/ota_upgrade.py @@ -28,9 +28,8 @@ from infuse_iot.socket_comms import ( GatewayRequestConnectionRequest, LocalClient, - default_multicast_address, ) -from infuse_iot.util.argparse import InfuseDeviceId, ValidFile, ValidRelease +from infuse_iot.util.argparse import InfuseDeviceId, ValidFile, ValidRelease, add_server_port_parser from infuse_iot.util.crc import crc16_ccitt from infuse_iot.zephyr.errno import errno @@ -41,7 +40,7 @@ class SubCommand(InfuseCommand): DESCRIPTION = "Automatically OTA upgrade observed devices" def __init__(self, args): - self._client = LocalClient(default_multicast_address(), 1.0) + self._client = LocalClient(args.server_sock, 1.0) self._conn_timeout = args.conn_timeout self._min_rssi: int | None = args.rssi self._explicit_ids: list[int] = [] @@ -107,6 +106,8 @@ def add_parser(cls, parser): explicit.add_argument("--id", type=InfuseDeviceId, help="Single device to upgrade") explicit.add_argument("--list", type=ValidFile, help="File containing a list of IDs to upgrade") + add_server_port_parser(parser) + def progress_table(self): table = Table() table.add_column(f"{self._app_name}\n{self._new_ver}") diff --git a/src/infuse_iot/tools/rpc.py b/src/infuse_iot/tools/rpc.py index f2e8d0d..59dd0d4 100644 --- a/src/infuse_iot/tools/rpc.py +++ b/src/infuse_iot/tools/rpc.py @@ -20,9 +20,8 @@ ClientNotificationEpacketReceived, GatewayRequestConnectionRequest, LocalClient, - default_multicast_address, ) -from infuse_iot.util.argparse import InfuseDeviceId +from infuse_iot.util.argparse import InfuseDeviceId, add_server_port_parser class SubCommand(InfuseCommand): @@ -56,9 +55,11 @@ def add_parser(cls, parser): cmd_parser.set_defaults(rpc_class=cmd_cls) cmd_cls.add_parser(cmd_parser) + add_server_port_parser(parser) + def __init__(self, args: argparse.Namespace): self._args = args - self._client = LocalClient(default_multicast_address(), 1.0) + self._client = LocalClient(args.server_sock, 1.0) self._command: InfuseRpcCommand = args.rpc_class(args) self._request_id = random.randint(0, 2**32 - 1) self._max_payload = 0 diff --git a/src/infuse_iot/tools/serial_throughput.py b/src/infuse_iot/tools/serial_throughput.py index 9330b03..2d8f3da 100644 --- a/src/infuse_iot/tools/serial_throughput.py +++ b/src/infuse_iot/tools/serial_throughput.py @@ -19,9 +19,9 @@ ClientNotificationEpacketReceived, GatewayRequestEpacketSend, LocalClient, - default_multicast_address, ) from infuse_iot.time import InfuseTime +from infuse_iot.util.argparse import add_server_port_parser class SubCommand(InfuseCommand): @@ -37,9 +37,10 @@ def add_parser(cls, parser): default=20, help="Number of times to send each sized packet", ) + add_server_port_parser(parser) def __init__(self, args): - self._client = LocalClient(default_multicast_address(), 1.0) + self._client = LocalClient(args.server_sock, 1.0) self._iterations = args.iterations def run_time_set(self): diff --git a/src/infuse_iot/tools/tdf_csv.py b/src/infuse_iot/tools/tdf_csv.py index 915f8fb..1d90bbc 100644 --- a/src/infuse_iot/tools/tdf_csv.py +++ b/src/infuse_iot/tools/tdf_csv.py @@ -14,10 +14,10 @@ from infuse_iot.socket_comms import ( ClientNotificationEpacketReceived, LocalClient, - default_multicast_address, ) from infuse_iot.tdf import TDF from infuse_iot.time import InfuseTime +from infuse_iot.util.argparse import add_server_port_parser def _to_str(unix_time: float) -> str: @@ -32,9 +32,10 @@ class SubCommand(InfuseCommand): @classmethod def add_parser(cls, parser): parser.add_argument("--unix", action="store_true", help="Save timestamps as unix") + add_server_port_parser(parser) def __init__(self, args): - self._client = LocalClient(default_multicast_address(), 1.0) + self._client = LocalClient(args.server_sock, 1.0) self._decoder = TDF() self.args = args diff --git a/src/infuse_iot/tools/tdf_list.py b/src/infuse_iot/tools/tdf_list.py index e1469a4..09d2e08 100644 --- a/src/infuse_iot/tools/tdf_list.py +++ b/src/infuse_iot/tools/tdf_list.py @@ -16,11 +16,10 @@ from infuse_iot.socket_comms import ( ClientNotificationEpacketReceived, LocalClient, - default_multicast_address, ) from infuse_iot.tdf import TDF from infuse_iot.time import InfuseTime -from infuse_iot.util.argparse import InfuseDeviceId +from infuse_iot.util.argparse import InfuseDeviceId, add_server_port_parser class SubCommand(InfuseCommand): @@ -35,9 +34,10 @@ def add_parser(cls, parser): "--id", type=InfuseDeviceId, action="append", default=[], help="Limit displayed TDFs by device ID" ) parser.add_argument("--min-rssi", type=int, help="Minimum RSSI to display TDF") + add_server_port_parser(parser) def __init__(self, args): - self._client = LocalClient(default_multicast_address(), 1.0) + self._client = LocalClient(args.server_sock, 1.0) self._decoder = TDF() self._array_all = args.array_all self._ids = args.id