diff --git a/src/infuse_iot/commands.py b/src/infuse_iot/commands.py index 6529975..ebc07ff 100644 --- a/src/infuse_iot/commands.py +++ b/src/infuse_iot/commands.py @@ -8,7 +8,7 @@ import argparse import ctypes -from infuse_iot.epacket import Auth +from infuse_iot.epacket.packet import Auth class InfuseCommand: diff --git a/src/infuse_iot/common.py b/src/infuse_iot/common.py new file mode 100644 index 0000000..13fc517 --- /dev/null +++ b/src/infuse_iot/common.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + +import enum + + +class InfuseType(enum.Enum): + """Infuse Data Types""" + + ECHO_REQ = 0 + ECHO_RSP = 1 + TDF = 2 + RPC_CMD = 3 + RPC_DATA = 4 + RPC_DATA_ACK = 5 + RPC_RSP = 6 + RECEIVED_EPACKET = 7 + ACK = 8 + SERIAL_LOG = 10 + MEMFAULT_CHUNK = 30 + + KEY_IDS = 127 diff --git a/src/infuse_iot/database.py b/src/infuse_iot/database.py index d0a11c2..bc5fe7f 100644 --- a/src/infuse_iot/database.py +++ b/src/infuse_iot/database.py @@ -8,6 +8,7 @@ from infuse_iot.api_client.api.default import get_shared_secret from infuse_iot.api_client.models import Key from infuse_iot.util.crypto import hkdf_derive +from infuse_iot.epacket.interface import Address as InterfaceAddress from infuse_iot.credentials import get_api_key, load_network @@ -52,13 +53,20 @@ def __init__(self, address, network_id=None, device_id=None): def __init__(self): self.gateway = None self.devices: Dict[int, DeviceDatabase.DeviceState] = {} - - def observe_serial( - self, address: int, network_id: int | None = None, device_id: int | None = None + self.bt_addr: Dict[InterfaceAddress.BluetoothLeAddr, int] = {} + + def observe_device( + self, + address: int, + network_id: int | None = None, + device_id: int | None = None, + bt_addr: InterfaceAddress.BluetoothLeAddr | None = None, ): """Update device state based on observed packet""" if self.gateway is None: self.gateway = address + if bt_addr is not None: + self.bt_addr[bt_addr] = address if address not in self.devices: self.devices[address] = self.DeviceState(address) if network_id is not None: @@ -118,6 +126,9 @@ def _serial_key(self, base, time_idx): def _bt_adv_key(self, base, time_idx): return hkdf_derive(base, time_idx.to_bytes(4, "little"), b"bt_adv") + def _bt_gatt_key(self, base, time_idx): + return hkdf_derive(base, time_idx.to_bytes(4, "little"), b"bt_gatt") + def has_public_key(self, address: int): """Does the database have the public key for this device?""" if address not in self.devices: @@ -130,6 +141,12 @@ def has_network_id(self, address: int): return False return self.devices[address].network_id is not None + def infuse_id_from_bluetooth( + self, bt_addr: InterfaceAddress.BluetoothLeAddr + ) -> int | None: + """Get Bluetooth address associated with device""" + return self.bt_addr.get(bt_addr, None) + def serial_network_key(self, address: int, gps_time: int): """Network key for serial interface""" if address not in self.devices: @@ -173,3 +190,25 @@ def bt_adv_device_key(self, address: int, gps_time: int): time_idx = gps_time // (60 * 60 * 24) return self._bt_adv_key(base, time_idx) + + def bt_gatt_network_key(self, address: int, gps_time: int): + """Network key for Bluetooth advertising interface""" + if address not in self.devices: + raise DeviceUnknownNetworkKey + network_id = self.devices[address].network_id + + return self._network_key(network_id, b"bt_gatt", gps_time) + + def bt_gatt_device_key(self, address: int, gps_time: int): + """Device key for Bluetooth advertising interface""" + if address not in self.devices: + raise DeviceUnknownDeviceKey + d = self.devices[address] + if d.device_id is None: + raise DeviceUnknownDeviceKey + base = self.devices[address].shared_key + if base is None: + raise DeviceUnknownDeviceKey + time_idx = gps_time // (60 * 60 * 24) + + return self._bt_gatt_key(base, time_idx) diff --git a/src/infuse_iot/epacket/__init__.py b/src/infuse_iot/epacket/__init__.py new file mode 100644 index 0000000..e5a0d9b --- /dev/null +++ b/src/infuse_iot/epacket/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python3 diff --git a/src/infuse_iot/epacket/common.py b/src/infuse_iot/epacket/common.py new file mode 100644 index 0000000..9eb9186 --- /dev/null +++ b/src/infuse_iot/epacket/common.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 + +from typing import Dict +from typing_extensions import Self + + +class Serializable: + def to_json(self) -> Dict: + """Convert class to json dictionary""" + raise NotImplementedError + + @classmethod + def from_json(cls, values: Dict) -> Self: + """Reconstruct class from json dictionary""" + raise NotImplementedError diff --git a/src/infuse_iot/epacket/interface.py b/src/infuse_iot/epacket/interface.py new file mode 100644 index 0000000..55d01b0 --- /dev/null +++ b/src/infuse_iot/epacket/interface.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 + +import ctypes +import enum + +from typing import Dict +from typing_extensions import Self + +from infuse_iot.util.ctypes import bytes_to_uint8 +from infuse_iot.epacket.common import Serializable +import infuse_iot.generated.rpc_definitions as rpc_defs + + +class ID(enum.Enum): + """Interface identifier""" + + SERIAL = 0 + UDP = 1 + BT_ADV = 2 + BT_PERIPHERAL = 3 + BT_CENTRAL = 4 + + +class Address(Serializable): + class SerialAddr(Serializable): + def __str__(self): + return "" + + def len(self): + return 0 + + def to_json(self) -> Dict: + return {"i": "SERIAL"} + + @classmethod + def from_json(cls, values: Dict) -> Self: + return cls() + + class BluetoothLeAddr(Serializable): + class CtypesFormat(ctypes.LittleEndianStructure): + _fields_ = [ + ("type", ctypes.c_uint8), + ("addr", 6 * ctypes.c_uint8), + ] + _pack_ = 1 + + def __init__(self, addr_type: int, addr_val: int): + self.addr_type = addr_type + self.addr_val = addr_val + + def __hash__(self) -> int: + return (self.addr_type << 48) + self.addr_val + + def __eq__(self, another) -> bool: + return ( + self.addr_type == another.addr_type + and self.addr_val == another.addr_val + ) + + def __str__(self) -> str: + t = "random" if self.addr_type == 1 else "public" + v = ":".join([f"{x:02x}" for x in self.addr_val.to_bytes(6, "big")]) + return f"{v} ({t})" + + def len(self): + return ctypes.sizeof(self.CtypesFormat) + + def to_json(self) -> Dict: + return {"i": "BT", "t": self.addr_type, "v": self.addr_val} + + @classmethod + def from_json(cls, values: Dict) -> Self: + return cls(values["t"], values["v"]) + + def to_rpc_struct(self) -> rpc_defs.rpc_struct_bt_addr_le: + """Convert the address to the common RPC address structure""" + + return rpc_defs.rpc_struct_bt_addr_le( + self.addr_type, bytes_to_uint8(self.addr_val.to_bytes(6, "little")) + ) + + @classmethod + def from_rpc_struct(cls, struct: rpc_defs.rpc_struct_bt_addr_le): + """Create instance from the common RPC address structure""" + + return cls(struct.type, int.from_bytes(struct.val, "little")) + + def __init__(self, val): + self.val = val + + def __str__(self): + return str(self.val) + + def len(self): + return self.val.len() + + def to_json(self) -> Dict: + return self.val.to_json() + + @classmethod + def from_json(cls, values: Dict) -> Self: + if values["i"] == "BT": + return cls(cls.BluetoothLeAddr.from_json(values)) + elif values["i"] == "SERIAL": + return cls(cls.SerialAddr()) + raise NotImplementedError("Unknown address type") + + @classmethod + def from_bytes(cls, interface: ID, stream: bytes) -> Self: + assert interface in [ + ID.BT_ADV, + ID.BT_PERIPHERAL, + ID.BT_CENTRAL, + ] + + c = cls.BluetoothLeAddr.CtypesFormat.from_buffer_copy(stream) + return cls.BluetoothLeAddr(c.type, int.from_bytes(bytes(c.addr), "little")) diff --git a/src/infuse_iot/epacket.py b/src/infuse_iot/epacket/packet.py similarity index 83% rename from src/infuse_iot/epacket.py rename to src/infuse_iot/epacket/packet.py index 0f06643..7436ab0 100644 --- a/src/infuse_iot/epacket.py +++ b/src/infuse_iot/epacket/packet.py @@ -9,47 +9,15 @@ from typing import List, Dict, Tuple from typing_extensions import Self +from infuse_iot.common import InfuseType +from infuse_iot.epacket.common import Serializable +from infuse_iot.epacket.interface import ID as Interface +from infuse_iot.epacket.interface import Address from infuse_iot.util.crypto import chachapoly_decrypt, chachapoly_encrypt from infuse_iot.database import DeviceDatabase, NoKeyError from infuse_iot.time import InfuseTime -class Serializable: - def to_json(self) -> Dict: - """Convert class to json dictionary""" - raise NotImplementedError - - @classmethod - def from_json(cls, values: Dict) -> Self: - """Reconstruct class from json dictionary""" - raise NotImplementedError - - -class InfuseType(enum.Enum): - """Infuse Data Types""" - - ECHO_REQ = 0 - ECHO_RSP = 1 - TDF = 2 - RPC_CMD = 3 - RPC_DATA = 4 - RPC_DATA_ACK = 5 - RPC_RSP = 6 - RECEIVED_EPACKET = 7 - ACK = 8 - MEMFAULT_CHUNK = 30 - - KEY_IDS = 127 - - -class Interface(enum.Enum): - """Interface options""" - - SERIAL = 0 - UDP = 1 - BT_ADV = 2 - - class Auth(enum.Enum): """Authorisation options""" @@ -89,7 +57,16 @@ def __init__(self, addr_type, addr_val): self.addr_type = addr_type self.addr_val = addr_val - def __str__(self): + def __hash__(self) -> int: + return (self.addr_type << 48) + self.addr_val + + def __eq__(self, another) -> bool: + return ( + self.addr_type == another.addr_type + and self.addr_val == another.addr_val + ) + + def __str__(self) -> str: t = "random" if self.addr_type == 1 else "public" v = ":".join([f"{x:02x}" for x in self.addr_val.to_bytes(6, "big")]) return f"{v} ({t})" @@ -126,7 +103,11 @@ def from_json(cls, values: Dict) -> Self: @classmethod def from_bytes(cls, interface: Interface, stream: bytes) -> Self: - assert interface == Interface.BT_ADV + assert interface in [ + Interface.BT_ADV, + Interface.BT_PERIPHERAL, + Interface.BT_CENTRAL, + ] c = cls.BluetoothLeAddr.CtypesFormat.from_buffer_copy(stream) return cls.BluetoothLeAddr(c.type, int.from_bytes(bytes(c.addr), "little")) @@ -251,8 +232,14 @@ def from_serial(cls, database: DeviceDatabase, serial_frame: bytes) -> List[Self del packet_bytes[: ctypes.sizeof(common_header)] # Only Bluetooth advertising supported for now - if common_header.interface != Interface.BT_ADV: + decode_mapping = { + Interface.BT_ADV: CtypeBtAdvFrame, + Interface.BT_PERIPHERAL: CtypeBtGattFrame, + Interface.BT_CENTRAL: CtypeBtGattFrame, + } + if common_header.interface not in decode_mapping: raise NotImplementedError + frame_type = decode_mapping[common_header.interface] # Extract interface address (Only Bluetooth supported) addr = InterfaceAddress.from_bytes(common_header.interface, packet_bytes) @@ -261,30 +248,30 @@ def from_serial(cls, database: DeviceDatabase, serial_frame: bytes) -> List[Self # Decrypting packet if common_header.encrypted: try: - bt_header, bt_decrypted = CtypeBtAdvFrame.decrypt( - database, packet_bytes + f_header, f_decrypted = frame_type.decrypt( + database, addr, packet_bytes ) except NoKeyError: continue bt_hop = HopReceived( - bt_header.device_id, + f_header.device_id, common_header.interface, addr, ( Auth.DEVICE - if bt_header.flags & Flags.ENCR_DEVICE + if f_header.flags & Flags.ENCR_DEVICE else Auth.NETWORK ), - bt_header.key_metadata, - bt_header.gps_time, - bt_header.sequence, + f_header.key_metadata, + f_header.gps_time, + f_header.sequence, common_header.rssi, ) packet = cls( [bt_hop, header.hop_received()], - bt_header.type, - bytes(bt_decrypted), + f_header.type, + bytes(f_decrypted), ) else: # Extract payload metadata @@ -293,6 +280,9 @@ def from_serial(cls, database: DeviceDatabase, serial_frame: bytes) -> List[Self ) del packet_bytes[: ctypes.sizeof(decr_header)] + # Notify database of BT Addr -> Infuse ID mapping + database.observe_device(decr_header.device_id, bt_addr=addr) + bt_hop = HopReceived( decr_header.device_id, common_header.interface, @@ -377,7 +367,7 @@ def from_json(cls, values: Dict) -> Self: ) -class CtypeV0VersionedFrame(ctypes.Structure): +class CtypeV0VersionedFrame(ctypes.LittleEndianStructure): _fields_ = [ ("version", ctypes.c_uint8), ("_type", ctypes.c_uint8), @@ -441,10 +431,10 @@ def hop_received(self) -> HopReceived: def decrypt(cls, database: DeviceDatabase, frame: bytes): header = cls.from_buffer_copy(frame) if header.flags & Flags.ENCR_DEVICE: - database.observe_serial(header.device_id, device_id=header.key_metadata) + database.observe_device(header.device_id, device_id=header.key_metadata) key = database.serial_device_key(header.device_id, header.gps_time) else: - database.observe_serial(header.device_id, network_id=header.key_metadata) + database.observe_device(header.device_id, network_id=header.key_metadata) key = database.serial_network_key(header.device_id, header.gps_time) decrypted = chachapoly_decrypt(key, frame[:11], frame[11:23], frame[23:]) @@ -455,18 +445,45 @@ class CtypeBtAdvFrame(CtypeV0VersionedFrame): """Bluetooth Advertising packet header""" @classmethod - def decrypt(cls, database: DeviceDatabase, frame: bytes): + def decrypt( + cls, database: DeviceDatabase, bt_addr: Address.BluetoothLeAddr, frame: bytes + ): header = cls.from_buffer_copy(frame) if header.flags & Flags.ENCR_DEVICE: raise NotImplementedError else: - database.observe_serial(header.device_id, network_id=header.key_metadata) + database.observe_device( + header.device_id, network_id=header.key_metadata, bt_addr=bt_addr + ) key = database.bt_adv_network_key(header.device_id, header.gps_time) decrypted = chachapoly_decrypt(key, frame[:11], frame[11:23], frame[23:]) return header, decrypted +class CtypeBtGattFrame(CtypeV0VersionedFrame): + """Bluetooth GATT packet header""" + + @classmethod + def decrypt( + cls, database: DeviceDatabase, bt_addr: Address.BluetoothLeAddr, frame: bytes + ): + header = cls.from_buffer_copy(frame) + if header.flags & Flags.ENCR_DEVICE: + database.observe_device( + header.device_id, device_id=header.key_metadata, bt_addr=bt_addr + ) + key = database.bt_gatt_device_key(header.device_id, header.gps_time) + else: + database.observe_device( + header.device_id, network_id=header.key_metadata, bt_addr=bt_addr + ) + key = database.bt_gatt_network_key(header.device_id, header.gps_time) + + decrypted = chachapoly_decrypt(key, frame[:11], frame[11:23], frame[23:]) + return header, decrypted + + class CtypePacketReceived: class CommonHeader(ctypes.Structure): _fields_ = [ diff --git a/src/infuse_iot/generated/rpc_definitions.py b/src/infuse_iot/generated/rpc_definitions.py index 5a9b193..a7e6215 100644 --- a/src/infuse_iot/generated/rpc_definitions.py +++ b/src/infuse_iot/generated/rpc_definitions.py @@ -584,6 +584,7 @@ class request(ctypes.LittleEndianStructure): class response(ctypes.LittleEndianStructure): _fields_ = [ + ("peer", rpc_struct_bt_addr_le), ("cloud_public_key", 32 * ctypes.c_uint8), ("device_public_key", 32 * ctypes.c_uint8), ("network_id", ctypes.c_uint32), diff --git a/src/infuse_iot/rpc.py b/src/infuse_iot/rpc.py new file mode 100644 index 0000000..ace2671 --- /dev/null +++ b/src/infuse_iot/rpc.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 + +import ctypes + + +class RequestHeader(ctypes.LittleEndianStructure): + """RPC_CMD packet header""" + + _fields_ = [ + ("request_id", ctypes.c_uint32), + ("command_id", ctypes.c_uint16), + ] + _pack_ = 1 + + +class RequestDataHeader(ctypes.LittleEndianStructure): + """RPC_CMD additional header for RPC_DATA""" + + _fields_ = [ + ("size", ctypes.c_uint32), + ("rx_ack_period", ctypes.c_uint8), + ] + _pack_ = 1 + + +class DataHeader(ctypes.LittleEndianStructure): + """RPC_DATA header""" + + _fields_ = [ + ("request_id", ctypes.c_uint32), + ("offset", ctypes.c_uint32), + ] + _pack_ = 1 + + +class DataAck(ctypes.LittleEndianStructure): + """RPC_DATA_ACK payload""" + + _fields_ = [ + ("request_id", ctypes.c_uint32), + ("offset", 0 * ctypes.c_uint32), + ] + _pack_ = 1 + + +class ResponseHeader(ctypes.LittleEndianStructure): + """RPC_RSP packet header""" + + _fields_ = [ + ("request_id", ctypes.c_uint32), + ("command_id", ctypes.c_uint16), + ("return_code", ctypes.c_int16), + ] + _pack_ = 1 diff --git a/src/infuse_iot/rpc_wrappers/security_state.py b/src/infuse_iot/rpc_wrappers/security_state.py index abd513a..cd8b914 100644 --- a/src/infuse_iot/rpc_wrappers/security_state.py +++ b/src/infuse_iot/rpc_wrappers/security_state.py @@ -11,7 +11,7 @@ from infuse_iot.util.argparse import ValidFile from infuse_iot.util.ctypes import bytes_to_uint8 from infuse_iot.commands import InfuseRpcCommand -from infuse_iot.epacket import Auth +from infuse_iot.epacket.packet import Auth import infuse_iot.generated.rpc_definitions as defs diff --git a/src/infuse_iot/socket_comms.py b/src/infuse_iot/socket_comms.py index 13a5a16..cd65d26 100644 --- a/src/infuse_iot/socket_comms.py +++ b/src/infuse_iot/socket_comms.py @@ -4,7 +4,7 @@ import struct import json -from infuse_iot.epacket import PacketReceived, PacketOutput +from infuse_iot.epacket.packet import PacketReceived, PacketOutput def default_multicast_address(): diff --git a/src/infuse_iot/tools/gateway.py b/src/infuse_iot/tools/gateway.py index 6115137..1b87332 100644 --- a/src/infuse_iot/tools/gateway.py +++ b/src/infuse_iot/tools/gateway.py @@ -18,6 +18,7 @@ from infuse_iot.util.argparse import ValidFile from infuse_iot.util.console import Console +from infuse_iot.common import InfuseType from infuse_iot.commands import InfuseCommand from infuse_iot.serial_comms import RttPort, SerialPort, SerialFrame from infuse_iot.socket_comms import LocalServer, default_multicast_address @@ -26,17 +27,16 @@ NoKeyError, ) -from infuse_iot.epacket import ( - InfuseType, +from infuse_iot.epacket.packet import ( Auth, PacketReceived, PacketOutput, - Interface, HopOutput, ) +import infuse_iot.epacket.interface as interface -# from infuse_iot.rpc_wrappers.security_state import security_state -from infuse_iot.tools.rpc import SubCommand as RpcSubCommand +from infuse_iot import rpc +import infuse_iot.generated.rpc_definitions as defs class LocalRpcServer: @@ -49,7 +49,7 @@ def __init__(self, database: DeviceDatabase): def generate(self, command: int, args: bytes, cb): """Generate RPC packet from arguments""" - cmd_bytes = bytes(RpcSubCommand.rpc_request_header(self._cnt, command)) + args + cmd_bytes = bytes(rpc.RequestHeader(self._cnt, command)) + args cmd_pkt = PacketOutput( [HopOutput.serial(Auth.NETWORK)], InfuseType.RPC_CMD, @@ -66,8 +66,27 @@ def handle(self, pkt: PacketReceived): if pkt.ptype != InfuseType.RPC_RSP: return + # Inspect the response header + header = rpc.ResponseHeader.from_buffer_copy(pkt.payload) + + # Was this a BT connect response with key information? + if header.command_id == defs.bt_connect_infuse.COMMAND_ID: + resp = defs.bt_connect_infuse.response.from_buffer_copy( + pkt.payload[ctypes.sizeof(header) :] + ) + if_addr = interface.Address.BluetoothLeAddr.from_rpc_struct(resp.peer) + 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: + self._ddb.observe_security_state( + infuse_id, + bytes(resp.cloud_public_key), + bytes(resp.device_public_key), + resp.network_id, + ) + # Determine if the response is to a command we initiated - header = RpcSubCommand.rpc_response_header.from_buffer_copy(pkt.payload) if header.request_id not in self._queued: return @@ -83,12 +102,12 @@ def __init__( server: LocalServer, port: SerialPort, ddb: DeviceDatabase, - rpc: LocalRpcServer, + rpc_server: LocalRpcServer, ): self.server = server self.port = port self.ddb = ddb - self.rpc = rpc + self.rpc = rpc_server def query_device_key(self, cb_event: threading.Event = None): def security_state_done(pkt: PacketReceived, _: int, response: bytes): @@ -245,7 +264,7 @@ def _iter(self): continue # Set gateway address - assert pkt.route[0].interface == Interface.SERIAL + assert pkt.route[0].interface == interface.ID.SERIAL pkt.route[0].infuse_id = self._common.ddb.gateway # Do we have the device public keys we need? diff --git a/src/infuse_iot/tools/localhost.py b/src/infuse_iot/tools/localhost.py index 4d3ee51..500d4b9 100644 --- a/src/infuse_iot/tools/localhost.py +++ b/src/infuse_iot/tools/localhost.py @@ -15,11 +15,12 @@ from aiohttp.web_runner import GracefulExit from infuse_iot.util.console import Console -from infuse_iot.epacket import InfuseType, Interface +from infuse_iot.common import InfuseType from infuse_iot.commands import InfuseCommand from infuse_iot.socket_comms import LocalClient, default_multicast_address from infuse_iot.tdf import TDF from infuse_iot.time import InfuseTime +import infuse_iot.epacket.interface as interface class SubCommand(InfuseCommand): @@ -176,7 +177,7 @@ def recv_thread(self): self._data[source.infuse_id]["time"] = InfuseTime.utc_time_string( time.time() ) - if source.interface == Interface.BT_ADV: + if source.interface == interface.ID.BT_ADV: addr_bytes = source.interface_address.val.addr_val.to_bytes(6, "big") addr_str = ":".join([f"{x:02x}" for x in addr_bytes]) self._data[source.infuse_id]["bt_addr"] = addr_str diff --git a/src/infuse_iot/tools/native_bt.py b/src/infuse_iot/tools/native_bt.py index 6088769..c748824 100644 --- a/src/infuse_iot/tools/native_bt.py +++ b/src/infuse_iot/tools/native_bt.py @@ -13,11 +13,20 @@ from infuse_iot.util.argparse import BtLeAddress from infuse_iot.util.console import Console -from infuse_iot.epacket import CtypeBtAdvFrame, PacketReceived, HopReceived, Interface, Auth, Flags, InterfaceAddress +from infuse_iot.epacket.packet import ( + CtypeBtAdvFrame, + PacketReceived, + HopReceived, + Auth, + Flags, +) from infuse_iot.commands import InfuseCommand from infuse_iot.socket_comms import LocalServer, default_multicast_address from infuse_iot.database import DeviceDatabase +import infuse_iot.epacket.interface as interface + + class SubCommand(InfuseCommand): NAME = "native_bt" HELP = "Native Bluetooth gateway" @@ -29,32 +38,28 @@ def add_parser(cls, parser): def __init__(self, args): self.infuse_manu = 0x0DE4 - self.infuse_service = '0000fc74-0000-1000-8000-00805f9b34fb' + self.infuse_service = "0000fc74-0000-1000-8000-00805f9b34fb" self.database = DeviceDatabase() Console.init() def simple_callback(self, device: BLEDevice, data: AdvertisementData): - addr = InterfaceAddress.BluetoothLeAddr(0, BtLeAddress(device.address)) + addr = interface.Address.BluetoothLeAddr(0, BtLeAddress(device.address)) rssi = data.rssi payload = data.manufacturer_data[self.infuse_manu] hdr, decr = CtypeBtAdvFrame.decrypt(self.database, payload) hop = HopReceived( - hdr.device_id, - Interface.BT_ADV, - addr, - ( - Auth.DEVICE - if hdr.flags & Flags.ENCR_DEVICE - else Auth.NETWORK - ), - hdr.key_metadata, - hdr.gps_time, - hdr.sequence, - rssi, - ) - + hdr.device_id, + interface.ID.BT_ADV, + addr, + (Auth.DEVICE if hdr.flags & Flags.ENCR_DEVICE else Auth.NETWORK), + hdr.key_metadata, + hdr.gps_time, + hdr.sequence, + rssi, + ) + Console.log_rx(hdr.type, len(payload)) pkt = PacketReceived([hop], hdr.type, decr) self.server.broadcast(pkt) @@ -63,7 +68,7 @@ async def async_run(self): self.server = LocalServer(default_multicast_address()) scanner = BleakScanner( - self.simple_callback, [self.infuse_service] , cb=dict(use_bdaddr=True) + self.simple_callback, [self.infuse_service], cb=dict(use_bdaddr=True) ) while True: diff --git a/src/infuse_iot/tools/rpc.py b/src/infuse_iot/tools/rpc.py index e1c7dfc..04585eb 100644 --- a/src/infuse_iot/tools/rpc.py +++ b/src/infuse_iot/tools/rpc.py @@ -11,9 +11,11 @@ import importlib import pkgutil -from infuse_iot.epacket import InfuseType, PacketOutput, HopOutput +from infuse_iot.common import InfuseType +from infuse_iot.epacket.packet import PacketOutput, HopOutput from infuse_iot.commands import InfuseCommand, InfuseRpcCommand from infuse_iot.socket_comms import LocalClient, default_multicast_address +from infuse_iot import rpc import infuse_iot.rpc_wrappers as wrappers @@ -23,52 +25,6 @@ class SubCommand(InfuseCommand): HELP = "Run remote procedure calls on devices" DESCRIPTION = "Run remote procedure calls on devices" - class rpc_request_header(ctypes.LittleEndianStructure): - """RPC_CMD packet header""" - - _fields_ = [ - ("request_id", ctypes.c_uint32), - ("command_id", ctypes.c_uint16), - ] - _pack_ = 1 - - class rpc_request_data_header(ctypes.LittleEndianStructure): - """RPC_CMD additional header for RPC_DATA""" - - _fields_ = [ - ("size", ctypes.c_uint32), - ("rx_ack_period", ctypes.c_uint8), - ] - _pack_ = 1 - - class rpc_data_header(ctypes.LittleEndianStructure): - """RPC_DATA header""" - - _fields_ = [ - ("request_id", ctypes.c_uint32), - ("offset", ctypes.c_uint32), - ] - _pack_ = 1 - - class rpc_data_ack(ctypes.LittleEndianStructure): - """RPC_DATA_ACK payload""" - - _fields_ = [ - ("request_id", ctypes.c_uint32), - ("offset", 0 * ctypes.c_uint32), - ] - _pack_ = 1 - - class rpc_response_header(ctypes.LittleEndianStructure): - """Serial packet header""" - - _fields_ = [ - ("request_id", ctypes.c_uint32), - ("command_id", ctypes.c_uint16), - ("return_code", ctypes.c_int16), - ] - _pack_ = 1 - @classmethod def add_parser(cls, parser): command_list_parser = parser.add_subparsers( @@ -100,7 +56,7 @@ def _wait_data_ack(self): while rsp := self._client.receive(): if rsp.ptype != InfuseType.RPC_DATA_ACK: continue - data_ack = self.rpc_data_ack.from_buffer_copy(rsp.payload) + data_ack = rpc.DataAck.from_buffer_copy(rsp.payload) # Response to the request we sent if data_ack.request_id != self._request_id: continue @@ -112,13 +68,13 @@ def _wait_rpc_rsp(self): # RPC response packet if rsp.ptype != InfuseType.RPC_RSP: continue - rsp_header = self.rpc_response_header.from_buffer_copy(rsp.payload) + rsp_header = rpc.ResponseHeader.from_buffer_copy(rsp.payload) # Response to the request we sent if rsp_header.request_id != self._request_id: continue # Convert response bytes back to struct form rsp_data = self._command.response.from_buffer_copy( - rsp.payload[ctypes.sizeof(self.rpc_response_header) :] + rsp.payload[ctypes.sizeof(rpc.ResponseHeader) :] ) # Handle the response print(f"INFUSE ID: {rsp.route[0].infuse_id:016x}") @@ -127,10 +83,10 @@ def _wait_rpc_rsp(self): def _run_data_cmd(self): ack_period = 1 - header = self.rpc_request_header(self._request_id, self._command.COMMAND_ID) + header = rpc.RequestHeader(self._request_id, self._command.COMMAND_ID) params = self._command.request_struct() data = self._command.data_payload() - data_hdr = self.rpc_request_data_header(len(data), ack_period) + data_hdr = rpc.RequestDataHeader(len(data), ack_period) request_packet = bytes(header) + bytes(data_hdr) + bytes(params) pkt = PacketOutput( @@ -151,7 +107,7 @@ def _run_data_cmd(self): size = min(size, len(data)) payload = data[:size] - hdr = self.rpc_data_header(self._request_id, offset) + hdr = rpc.DataHeader(self._request_id, offset) pkt_bytes = bytes(hdr) + payload pkt = PacketOutput( [HopOutput.serial(self._command.auth_level())], @@ -173,7 +129,7 @@ def _run_data_cmd(self): self._wait_rpc_rsp() def _run_standard_cmd(self): - header = self.rpc_request_header(self._request_id, self._command.COMMAND_ID) + header = rpc.RequestHeader(self._request_id, self._command.COMMAND_ID) params = self._command.request_struct() request_packet = bytes(header) + bytes(params) diff --git a/src/infuse_iot/tools/serial_throughput.py b/src/infuse_iot/tools/serial_throughput.py index 4b0a2a6..05f3f27 100644 --- a/src/infuse_iot/tools/serial_throughput.py +++ b/src/infuse_iot/tools/serial_throughput.py @@ -8,7 +8,8 @@ import random import time -from infuse_iot.epacket import InfuseType, PacketOutput, HopOutput +from infuse_iot.common import InfuseType +from infuse_iot.epacket.packet import PacketOutput, HopOutput from infuse_iot.commands import InfuseCommand from infuse_iot.socket_comms import LocalClient, default_multicast_address diff --git a/src/infuse_iot/tools/tdf_csv.py b/src/infuse_iot/tools/tdf_csv.py index eee3a02..c711232 100644 --- a/src/infuse_iot/tools/tdf_csv.py +++ b/src/infuse_iot/tools/tdf_csv.py @@ -8,7 +8,7 @@ import os import time -from infuse_iot.epacket import InfuseType +from infuse_iot.common import InfuseType from infuse_iot.commands import InfuseCommand from infuse_iot.socket_comms import LocalClient, default_multicast_address from infuse_iot.tdf import TDF @@ -44,7 +44,6 @@ def run(self): source = msg.route[0] for tdf in decoded: - # Construct reading strings lines = [] reading_time = tdf.time diff --git a/src/infuse_iot/tools/tdf_list.py b/src/infuse_iot/tools/tdf_list.py index 591c134..7572765 100644 --- a/src/infuse_iot/tools/tdf_list.py +++ b/src/infuse_iot/tools/tdf_list.py @@ -7,7 +7,7 @@ import tabulate -from infuse_iot.epacket import InfuseType +from infuse_iot.common import InfuseType from infuse_iot.commands import InfuseCommand from infuse_iot.socket_comms import LocalClient, default_multicast_address from infuse_iot.tdf import TDF