diff --git a/src/infuse_iot/generated/rpc_definitions.py b/src/infuse_iot/generated/rpc_definitions.py index 3b78489..45c1ae6 100644 --- a/src/infuse_iot/generated/rpc_definitions.py +++ b/src/infuse_iot/generated/rpc_definitions.py @@ -161,6 +161,18 @@ class rpc_struct_infuse_state(VLACompatLittleEndianStruct): _pack_ = 1 +class rpc_struct_sockaddr(VLACompatLittleEndianStruct): + """`struct sockaddr_in` or `struct sockaddr_in6` compatible address""" + + _fields_ = [ + ("sin_family", ctypes.c_uint8), + ("sin_port", ctypes.c_uint16), + ("sin_addr", 16 * ctypes.c_uint8), + ("scope_id", ctypes.c_uint8), + ] + _pack_ = 1 + + class rpc_enum_bt_le_addr_type(enum.IntEnum): """Bluetooth LE address type""" @@ -194,6 +206,16 @@ class rpc_enum_data_logger(enum.IntEnum): FLASH_REMOVABLE = 2 +class rpc_enum_zperf_data_source(enum.IntEnum): + """Source for zperf data upload""" + + CONSTANT = 0 + RANDOM = 1 + FLASH_ONBOARD = 2 + FLASH_REMOVABLE = 3 + ENCRYPT = 128 + + class reboot: """Reboot the device after a delay""" @@ -634,6 +656,40 @@ class response(VLACompatLittleEndianStruct): _pack_ = 1 +class zperf_upload: + """Network upload bandwidth testing using zperf/iperf""" + + HELP = "Network upload bandwidth testing using zperf/iperf" + DESCRIPTION = "Network upload bandwidth testing using zperf/iperf" + COMMAND_ID = 31 + + class request(VLACompatLittleEndianStruct): + _fields_ = [ + ("peer_address", rpc_struct_sockaddr), + ("sock_type", ctypes.c_uint8), + ("data_source", ctypes.c_uint8), + ("duration_ms", ctypes.c_uint32), + ("rate_kbps", ctypes.c_uint32), + ("packet_size", ctypes.c_uint16), + ] + _pack_ = 1 + + class response(VLACompatLittleEndianStruct): + _fields_ = [ + ("nb_packets_sent", ctypes.c_uint32), + ("nb_packets_rcvd", ctypes.c_uint32), + ("nb_packets_lost", ctypes.c_uint32), + ("nb_packets_outorder", ctypes.c_uint32), + ("total_len", ctypes.c_uint64), + ("time_in_us", ctypes.c_uint64), + ("jitter_in_us", ctypes.c_uint32), + ("client_time_in_us", ctypes.c_uint64), + ("packet_size", ctypes.c_uint32), + ("nb_packets_errors", ctypes.c_uint32), + ] + _pack_ = 1 + + class file_write_basic: """Write a file to the device""" diff --git a/src/infuse_iot/rpc_wrappers/zperf_upload.py b/src/infuse_iot/rpc_wrappers/zperf_upload.py new file mode 100644 index 0000000..d2f4ffb --- /dev/null +++ b/src/infuse_iot/rpc_wrappers/zperf_upload.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 + +import ctypes +import ipaddress +import socket + +import tabulate + +import infuse_iot.generated.rpc_definitions as defs +from infuse_iot.commands import InfuseRpcCommand +from infuse_iot.zephyr.errno import errno +from infuse_iot.zephyr.net import AddressFamily, SockType + + +class zperf_upload(InfuseRpcCommand, defs.zperf_upload): + @classmethod + def add_parser(cls, parser): + parser.add_argument("--address", "-a", type=str, required=True, help="Peer IP address") + parser.add_argument("--port", "-p", type=int, default=5001, help="Peer port") + socket_group = parser.add_mutually_exclusive_group(required=True) + socket_group.add_argument( + "--tcp", + dest="sock_type", + action="store_const", + const=SockType.SOCK_STREAM, + help="TCP protocol", + ) + socket_group.add_argument( + "--udp", + dest="sock_type", + action="store_const", + const=SockType.SOCK_DGRAM, + help="UDP protocol", + ) + source_group = parser.add_mutually_exclusive_group() + source_group.add_argument( + "--constant", + dest="data_source", + action="store_const", + const=defs.rpc_enum_zperf_data_source.CONSTANT, + default=defs.rpc_enum_zperf_data_source.CONSTANT, + help="Constant data payload ('z')", + ) + source_group.add_argument( + "--random", + dest="data_source", + action="store_const", + const=defs.rpc_enum_zperf_data_source.RANDOM, + help="Random data payload", + ) + source_group.add_argument( + "--onboard", + dest="data_source", + action="store_const", + const=defs.rpc_enum_zperf_data_source.FLASH_ONBOARD, + help="Read from onboard flash logger", + ) + source_group.add_argument( + "--removable", + dest="data_source", + action="store_const", + const=defs.rpc_enum_zperf_data_source.FLASH_REMOVABLE, + help="Read from removable flash logger", + ) + parser.add_argument("--encrypt", action="store_true", help="Encrypt payloads before transmission") + parser.add_argument("--duration", type=int, default=5000, help="Duration to run test over in milliseconds") + parser.add_argument("--rate-kbps", type=int, default=0, help="Desired upload rate in kbps") + parser.add_argument("--payload-size", type=int, default=512, help="Payload size") + + def __init__(self, args): + self.peer_addr = ipaddress.ip_address(args.address) + self.peer_port = args.port + self.sock_type = args.sock_type + self.data_source = args.data_source + if args.encrypt: + self.data_source |= defs.rpc_enum_zperf_data_source.ENCRYPT + self.duration = args.duration + self.rate = args.rate_kbps + self.packet_size = args.payload_size + if self.sock_type == SockType.SOCK_DGRAM: + # Add the UDP client header size to the requested payload size + self.packet_size += 40 + + def request_struct(self): + peer_family = ( + AddressFamily.AF_INET if isinstance(self.peer_addr, ipaddress.IPv4Address) else AddressFamily.AF_INET6 + ) + addr_bytes = (16 * ctypes.c_uint8)(*self.peer_addr.packed) + peer = defs.rpc_struct_sockaddr( + sin_family=peer_family, + sin_port=socket.htons(self.peer_port), + sin_addr=addr_bytes, + scope_id=0, + ) + + return self.request( + peer_address=peer, + sock_type=self.sock_type, + data_source=self.data_source, + duration_ms=self.duration, + rate_kbps=self.rate, + packet_size=self.packet_size, + ) + + def request_json(self): + return {} + + def handle_response(self, return_code, response): + if return_code != 0: + print(f"Failed to run zperf ({errno.strerror(-return_code)})") + return + + throughput_bps = 8 * response.total_len / (response.client_time_in_us / 1000) + print(f"Average Throughput: {throughput_bps / 1000:.3f} kbps") + if self.sock_type == SockType.SOCK_DGRAM: + recv = 100 * response.nb_packets_rcvd / response.nb_packets_sent + loss = 100 * response.nb_packets_lost / response.nb_packets_sent + print(f" Packet Recv: {recv:6.2f}%") + print(f" Packet Loss: {loss:6.2f}%") + results = [] + for field_name, _ in response._fields_: + results.append([field_name, getattr(response, field_name)]) + print(tabulate.tabulate(results)) diff --git a/src/infuse_iot/tools/bt_log.py b/src/infuse_iot/tools/bt_log.py index 4140e87..0226158 100644 --- a/src/infuse_iot/tools/bt_log.py +++ b/src/infuse_iot/tools/bt_log.py @@ -8,6 +8,7 @@ from infuse_iot.commands import InfuseCommand from infuse_iot.common import InfuseType +from infuse_iot.epacket import interface from infuse_iot.socket_comms import ( ClientNotificationConnectionDropped, ClientNotificationEpacketReceived, @@ -15,6 +16,7 @@ LocalClient, default_multicast_address, ) +from infuse_iot.tdf import TDF class SubCommand(InfuseCommand): @@ -24,24 +26,44 @@ class SubCommand(InfuseCommand): def __init__(self, args): self._client = LocalClient(default_multicast_address(), 60.0) + self._decoder = TDF() self._id = args.id + self._data = args.data @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("--data", action="store_true", help="Subscribe to the data characteristic as well") def run(self): try: - with self._client.connection(self._id, GatewayRequestConnectionRequest.DataType.LOGGING) as _: - while rsp := self._client.receive(): - if isinstance(rsp, ClientNotificationConnectionDropped): + types = GatewayRequestConnectionRequest.DataType.LOGGING + if self._data: + types |= GatewayRequestConnectionRequest.DataType.DATA + with self._client.connection(self._id, types) as _: + while evt := self._client.receive(): + if evt is None: + continue + if isinstance(evt, ClientNotificationConnectionDropped): print(f"Connection to {self._id:016x} lost") break - if ( - isinstance(rsp, ClientNotificationEpacketReceived) - and rsp.epacket.ptype == InfuseType.SERIAL_LOG - ): - print(rsp.epacket.payload.decode("utf-8"), end="") + if not isinstance(evt, ClientNotificationEpacketReceived): + continue + source = evt.epacket.route[0] + if source.infuse_id != self._id: + continue + if source.interface != interface.ID.BT_CENTRAL: + continue + + if evt.epacket.ptype == InfuseType.SERIAL_LOG: + print(evt.epacket.payload.decode("utf-8"), end="") + if evt.epacket.ptype == InfuseType.TDF: + for tdf in self._decoder.decode(evt.epacket.payload): + t = tdf.data[-1] + if len(tdf.data) > 1: + print(f"{tdf.time:.3f} TDF: {t.name}[{len(tdf.data)}]") + else: + print(f"{tdf.time:.3f} TDF: {t.name}") except KeyboardInterrupt: print(f"Disconnecting from {self._id:016x}") diff --git a/src/infuse_iot/util/soc/nrf.py b/src/infuse_iot/util/soc/nrf.py index c6317d7..5558aac 100644 --- a/src/infuse_iot/util/soc/nrf.py +++ b/src/infuse_iot/util/soc/nrf.py @@ -36,7 +36,7 @@ def soc(device_info): class nRF53(NRFFamily): - FICT_ADDRESS = 0x00FF0000 + FICR_ADDRESS = 0x00FF0000 DEVICE_ID_OFFSET = 0x204 CUSTOMER_OFFSET = 0x100 diff --git a/src/infuse_iot/zephyr/net.py b/src/infuse_iot/zephyr/net.py new file mode 100644 index 0000000..f305e4f --- /dev/null +++ b/src/infuse_iot/zephyr/net.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +import enum + + +class IPProtocol(enum.IntEnum): + IPPROTO_IP = 0 + IPPROTO_ICMP = 1 + IPPROTO_IGMP = 2 + IPPROTO_ETH_P_ALL = 3 + IPPROTO_IPIP = 4 + IPPROTO_TCP = 6 + IPPROTO_UDP = 17 + IPPROTO_IPV6 = 41 + IPPROTO_ICMPV6 = 58 + IPPROTO_RAW = 255 + + +class ProtocolFamily(enum.IntEnum): + PF_UNSPEC = 0 + PF_INET = 1 + PF_INET6 = 2 + PF_PACKET = 3 + PF_CAN = 4 + PF_NET_MGMT = 5 + PF_LOCAL = 6 + PF_UNIX = PF_LOCAL + + +class AddressFamily(enum.IntEnum): + AF_UNSPEC = ProtocolFamily.PF_UNSPEC + AF_INET = ProtocolFamily.PF_INET + AF_INET6 = ProtocolFamily.PF_INET6 + AF_PACKET = ProtocolFamily.PF_PACKET + AF_CAN = ProtocolFamily.PF_CAN + AF_NET_MGMT = ProtocolFamily.PF_NET_MGMT + AF_LOCAL = ProtocolFamily.PF_LOCAL + AF_UNIX = ProtocolFamily.PF_UNIX + + +class SockType(enum.IntEnum): + SOCK_STREAM = 1 + SOCK_DGRAM = 2 + SOCK_RAW = 3