diff --git a/src/infuse_iot/app/main.py b/src/infuse_iot/app/main.py index 465c263..23e895e 100644 --- a/src/infuse_iot/app/main.py +++ b/src/infuse_iot/app/main.py @@ -73,7 +73,8 @@ def _load_tools(self, parser: argparse.ArgumentParser): module = importlib.util.module_from_spec(spec) try: spec.loader.exec_module(module) - except Exception as _: + except Exception as e: + print(f"Failed to import '{name}': {str(e)}") continue if hasattr(module, "SubCommand"): self._load_from_module(tools_parser, module) diff --git a/src/infuse_iot/credentials.py b/src/infuse_iot/credentials.py index 5ce12a8..180c58f 100644 --- a/src/infuse_iot/credentials.py +++ b/src/infuse_iot/credentials.py @@ -3,6 +3,11 @@ import keyring import yaml +DEFAULT_NETWORK_KEY = ( + b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" + b"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" +) + def set_api_key(api_key: str) -> None: """ @@ -36,10 +41,14 @@ def save_network(network_id: int, network_info: str) -> None: keyring.set_password("infuse-iot", username, network_info) -def load_network(network_id: int): +def load_network(network_id: int) -> dict: """ Retrieve an Infuse-IoT network key from the keyring module """ + if network_id == 0x000000: + # Default network + return {"id": 0, "key": DEFAULT_NETWORK_KEY} + username = f"network-{network_id:06x}" key = keyring.get_password("infuse-iot", username) if key is None: diff --git a/src/infuse_iot/database.py b/src/infuse_iot/database.py index 53f46ca..78298d7 100644 --- a/src/infuse_iot/database.py +++ b/src/infuse_iot/database.py @@ -34,10 +34,7 @@ class DeviceKeyChangedError(KeyError): class DeviceDatabase: """Database of current device state""" - _network_keys = { - 0x000000: b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" - b"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f", - } + _network_keys: dict[int, bytes] = {} _derived_keys: dict[tuple[int, bytes, int], bytes] = {} class DeviceState: diff --git a/src/infuse_iot/generated/tdf_definitions.py b/src/infuse_iot/generated/tdf_definitions.py index 7f954de..b4ea10f 100644 --- a/src/infuse_iot/generated/tdf_definitions.py +++ b/src/infuse_iot/generated/tdf_definitions.py @@ -1603,7 +1603,7 @@ class pcm_16bit_chan_left(TdfReadingBase): } class pcm_16bit_chan_right(TdfReadingBase): - """Duration an Infuse-IoT application state was asserted for""" + """16bit PCM (Audio) data for the right channel""" ID = 59 NAME = "PCM_16BIT_CHAN_RIGHT" @@ -1618,6 +1618,25 @@ class pcm_16bit_chan_right(TdfReadingBase): "val": "{}", } + class pcm_16bit_chan_dual(TdfReadingBase): + """16bit PCM (Audio) data for both the left and right channels""" + + ID = 60 + NAME = "PCM_16BIT_CHAN_DUAL" + _fields_ = [ + ("left", ctypes.c_int16), + ("right", ctypes.c_int16), + ] + _pack_ = 1 + _postfix_ = { + "left": "", + "right": "", + } + _display_fmt_ = { + "left": "{}", + "right": "{}", + } + id_type_mapping: dict[int, type[TdfReadingBase]] = { readings.announce.ID: readings.announce, @@ -1676,6 +1695,7 @@ class pcm_16bit_chan_right(TdfReadingBase): readings.state_duration.ID: readings.state_duration, readings.pcm_16bit_chan_left.ID: readings.pcm_16bit_chan_left, readings.pcm_16bit_chan_right.ID: readings.pcm_16bit_chan_right, + readings.pcm_16bit_chan_dual.ID: readings.pcm_16bit_chan_dual, } __all__ = [ diff --git a/src/infuse_iot/tools/gateway.py b/src/infuse_iot/tools/gateway.py index 08839b9..284d56e 100644 --- a/src/infuse_iot/tools/gateway.py +++ b/src/infuse_iot/tools/gateway.py @@ -43,9 +43,11 @@ ClientNotificationConnectionDropped, ClientNotificationConnectionFailed, ClientNotificationEpacketReceived, + ClientNotificationObservedDevices, GatewayRequestConnectionRelease, GatewayRequestConnectionRequest, GatewayRequestEpacketSend, + GatewayRequestObservedDevices, LocalServer, default_multicast_address, ) @@ -388,6 +390,21 @@ def _handle_conn_release(self, req: GatewayRequestConnectionRelease): Console.log_tx(cmd.ptype, len(encrypted)) self._common.port.write(encrypted) + def _handle_observed_devices(self): + if self._common.server is None: + raise RuntimeError + observed_devices = {} + for device, state in self._common.ddb.devices.items(): + info = {} + if state.network_id is not None: + info["network_id"] = state.network_id + if state.device_id is not None: + info["device_id"] = state.device_id + if self._common.ddb.gateway == device: + info["gateway"] = True + observed_devices[device] = info + self._common.server.broadcast(ClientNotificationObservedDevices(observed_devices)) + def _iter(self) -> None: if self._common.server is None: time.sleep(1.0) @@ -401,6 +418,8 @@ def _iter(self) -> None: self._handle_conn_request(req) elif isinstance(req, GatewayRequestConnectionRelease): self._handle_conn_release(req) + elif isinstance(req, GatewayRequestObservedDevices): + self._handle_observed_devices() else: Console.log_error(f"Unhandled request {type(req)}") diff --git a/src/infuse_iot/util/crypto.py b/src/infuse_iot/util/crypto.py index 07d03a5..f1e03c8 100644 --- a/src/infuse_iot/util/crypto.py +++ b/src/infuse_iot/util/crypto.py @@ -5,7 +5,7 @@ from cryptography.hazmat.primitives.kdf.hkdf import HKDF -def hkdf_derive(input_key, salt, info): +def hkdf_derive(input_key: bytes, salt: bytes, info: bytes) -> bytes: """Derive a cryptographic key using HKDF-SHA256""" hkdf = HKDF( algorithm=hashes.SHA256(), @@ -16,13 +16,13 @@ def hkdf_derive(input_key, salt, info): return hkdf.derive(input_key) -def chachapoly_encrypt(key: bytes, associated_data: bytes, nonce: bytes, payload: bytes) -> bytes: +def chachapoly_encrypt(key: bytes, associated_data: bytes | None, nonce: bytes, payload: bytes) -> bytes: """Encrypt a payload using ChaCha20-Poly1305""" cipher = ChaCha20Poly1305(key) return cipher.encrypt(nonce, payload, associated_data) -def chachapoly_decrypt(key: bytes, associated_data: bytes, nonce: bytes, payload: bytes) -> bytes: +def chachapoly_decrypt(key: bytes, associated_data: bytes | None, nonce: bytes, payload: bytes) -> bytes: """Decrypt a payload using ChaCha20-Poly1305""" cipher = ChaCha20Poly1305(key) return cipher.decrypt(nonce, payload, associated_data)