diff --git a/apps/bench.py b/apps/bench.py index b99cc11..ca30cb9 100644 --- a/apps/bench.py +++ b/apps/bench.py @@ -80,10 +80,10 @@ SPEED_TX_UUID = 'E789C754-41A1-45F4-A948-A0A1A90DBA53' SPEED_RX_UUID = '016A2CC7-E14B-4819-935F-1F56EAE4098D' DEFAULT_RFCOMM_UUID = 'E6D55659-C8B4-4B85-96BB-B1143AF6D3AE' -DEFAULT_L2CAP_PSM = 1234 +DEFAULT_L2CAP_PSM = 128 DEFAULT_L2CAP_MAX_CREDITS = 128 DEFAULT_L2CAP_MTU = 1024 -DEFAULT_L2CAP_MPS = 1022 +DEFAULT_L2CAP_MPS = 1024 DEFAULT_LINGER_TIME = 1.0 DEFAULT_POST_CONNECTION_WAIT_TIME = 1.0 @@ -240,6 +240,23 @@ async def find_rfcomm_channel_with_uuid(connection: Connection, uuid: str) -> in return 0 +def log_stats(title, stats): + stats_min = min(stats) + stats_max = max(stats) + stats_avg = sum(stats) / len(stats) + logging.info( + color( + ( + f'### {title} stats: ' + f'min={stats_min:.2f}, ' + f'max={stats_max:.2f}, ' + f'average={stats_avg:.2f}' + ), + 'cyan', + ) + ) + + class PacketType(enum.IntEnum): RESET = 0 SEQUENCE = 1 @@ -253,14 +270,27 @@ PACKET_FLAG_LAST = 1 # Sender # ----------------------------------------------------------------------------- class Sender: - def __init__(self, packet_io, start_delay, packet_size, packet_count): + def __init__( + self, + packet_io, + start_delay, + repeat, + repeat_delay, + pace, + packet_size, + packet_count, + ): self.tx_start_delay = start_delay self.tx_packet_size = packet_size self.tx_packet_count = packet_count self.packet_io = packet_io self.packet_io.packet_listener = self + self.repeat = repeat + self.repeat_delay = repeat_delay + self.pace = pace self.start_time = 0 self.bytes_sent = 0 + self.stats = [] self.done = asyncio.Event() def reset(self): @@ -271,27 +301,57 @@ class Sender: await self.packet_io.ready.wait() logging.info(color('--- Go!', 'blue')) - if self.tx_start_delay: - logging.info(color(f'*** Startup delay: {self.tx_start_delay}', 'blue')) - await asyncio.sleep(self.tx_start_delay) + for run in range(self.repeat + 1): + self.done.clear() - logging.info(color('=== Sending RESET', 'magenta')) - await self.packet_io.send_packet(bytes([PacketType.RESET])) - self.start_time = time.time() - for tx_i in range(self.tx_packet_count): - packet_flags = PACKET_FLAG_LAST if tx_i == self.tx_packet_count - 1 else 0 - packet = struct.pack( - '>bbI', - PacketType.SEQUENCE, - packet_flags, - tx_i, - ) + bytes(self.tx_packet_size - 6) - logging.info(color(f'Sending packet {tx_i}: {len(packet)} bytes', 'yellow')) - self.bytes_sent += len(packet) - await self.packet_io.send_packet(packet) + if run > 0 and self.repeat and self.repeat_delay: + logging.info(color(f'*** Repeat delay: {self.repeat_delay}', 'green')) + await asyncio.sleep(self.repeat_delay) - await self.done.wait() - logging.info(color('=== Done!', 'magenta')) + if self.tx_start_delay: + logging.info(color(f'*** Startup delay: {self.tx_start_delay}', 'blue')) + await asyncio.sleep(self.tx_start_delay) + + logging.info(color('=== Sending RESET', 'magenta')) + await self.packet_io.send_packet(bytes([PacketType.RESET])) + self.start_time = time.time() + self.bytes_sent = 0 + for tx_i in range(self.tx_packet_count): + packet_flags = ( + PACKET_FLAG_LAST if tx_i == self.tx_packet_count - 1 else 0 + ) + packet = struct.pack( + '>bbI', + PacketType.SEQUENCE, + packet_flags, + tx_i, + ) + bytes(self.tx_packet_size - 6 - self.packet_io.overhead_size) + logging.info( + color( + f'Sending packet {tx_i}: {self.tx_packet_size} bytes', 'yellow' + ) + ) + self.bytes_sent += len(packet) + await self.packet_io.send_packet(packet) + + if self.pace is None: + continue + + if self.pace > 0: + await asyncio.sleep(self.pace / 1000) + else: + await self.packet_io.drain() + + await self.done.wait() + + run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else '' + logging.info(color(f'=== {run_counter} Done!', 'magenta')) + + if self.repeat: + log_stats('Run', self.stats) + + if self.repeat: + logging.info(color('--- End of runs', 'blue')) def on_packet_received(self, packet): try: @@ -302,6 +362,7 @@ class Sender: if packet_type == PacketType.ACK: elapsed = time.time() - self.start_time average_tx_speed = self.bytes_sent / elapsed + self.stats.append(average_tx_speed) logging.info( color( f'@@@ Received ACK. Speed: average={average_tx_speed:.4f}' @@ -320,17 +381,17 @@ class Receiver: start_timestamp: float last_timestamp: float - def __init__(self, packet_io): + def __init__(self, packet_io, linger): self.reset() self.packet_io = packet_io self.packet_io.packet_listener = self + self.linger = linger self.done = asyncio.Event() def reset(self): self.expected_packet_index = 0 - self.start_timestamp = 0.0 - self.last_timestamp = 0.0 - self.bytes_received = 0 + self.measurements = [(time.time(), 0)] + self.total_bytes_received = 0 def on_packet_received(self, packet): try: @@ -338,12 +399,9 @@ class Receiver: except ValueError: return - now = time.time() - if packet_type == PacketType.RESET: logging.info(color('=== Received RESET', 'magenta')) self.reset() - self.start_timestamp = now return try: @@ -352,7 +410,8 @@ class Receiver: return logging.info( f'<<< Received packet {packet_index}: ' - f'flags=0x{packet_flags:02X}, {len(packet)} bytes' + f'flags=0x{packet_flags:02X}, ' + f'{len(packet) + self.packet_io.overhead_size} bytes' ) if packet_index != self.expected_packet_index: @@ -363,19 +422,27 @@ class Receiver: ) ) - elapsed_since_start = now - self.start_timestamp - elapsed_since_last = now - self.last_timestamp - self.bytes_received += len(packet) + now = time.time() + elapsed_since_start = now - self.measurements[0][0] + elapsed_since_last = now - self.measurements[-1][0] + self.measurements.append((now, len(packet))) + self.total_bytes_received += len(packet) instant_rx_speed = len(packet) / elapsed_since_last - average_rx_speed = self.bytes_received / elapsed_since_start + average_rx_speed = self.total_bytes_received / elapsed_since_start + window = self.measurements[-64:] + windowed_rx_speed = sum(measurement[1] for measurement in window[1:]) / ( + window[-1][0] - window[0][0] + ) logging.info( color( - f'Speed: instant={instant_rx_speed:.4f}, average={average_rx_speed:.4f}', + 'Speed: ' + f'instant={instant_rx_speed:.4f}, ' + f'windowed={windowed_rx_speed:.4f}, ' + f'average={average_rx_speed:.4f}', 'yellow', ) ) - self.last_timestamp = now self.expected_packet_index = packet_index + 1 if packet_flags & PACKET_FLAG_LAST: @@ -385,7 +452,8 @@ class Receiver: ) ) logging.info(color('@@@ Received last packet', 'green')) - self.done.set() + if not self.linger: + self.done.set() async def run(self): await self.done.wait() @@ -396,16 +464,31 @@ class Receiver: # Ping # ----------------------------------------------------------------------------- class Ping: - def __init__(self, packet_io, start_delay, packet_size, packet_count): + def __init__( + self, + packet_io, + start_delay, + repeat, + repeat_delay, + pace, + packet_size, + packet_count, + ): self.tx_start_delay = start_delay self.tx_packet_size = packet_size self.tx_packet_count = packet_count self.packet_io = packet_io self.packet_io.packet_listener = self + self.repeat = repeat + self.repeat_delay = repeat_delay + self.pace = pace self.done = asyncio.Event() self.current_packet_index = 0 self.ping_sent_time = 0.0 self.latencies = [] + self.min_stats = [] + self.max_stats = [] + self.avg_stats = [] def reset(self): pass @@ -415,21 +498,56 @@ class Ping: await self.packet_io.ready.wait() logging.info(color('--- Go!', 'blue')) - if self.tx_start_delay: - logging.info(color(f'*** Startup delay: {self.tx_start_delay}', 'blue')) - await asyncio.sleep(self.tx_start_delay) + for run in range(self.repeat + 1): + self.done.clear() - logging.info(color('=== Sending RESET', 'magenta')) - await self.packet_io.send_packet(bytes([PacketType.RESET])) + if run > 0 and self.repeat and self.repeat_delay: + logging.info(color(f'*** Repeat delay: {self.repeat_delay}', 'green')) + await asyncio.sleep(self.repeat_delay) - await self.send_next_ping() + if self.tx_start_delay: + logging.info(color(f'*** Startup delay: {self.tx_start_delay}', 'blue')) + await asyncio.sleep(self.tx_start_delay) - await self.done.wait() - average_latency = sum(self.latencies) / len(self.latencies) - logging.info(color(f'@@@ Average latency: {average_latency:.2f}')) - logging.info(color('=== Done!', 'magenta')) + logging.info(color('=== Sending RESET', 'magenta')) + await self.packet_io.send_packet(bytes([PacketType.RESET])) + + self.current_packet_index = 0 + await self.send_next_ping() + + await self.done.wait() + + min_latency = min(self.latencies) + max_latency = max(self.latencies) + avg_latency = sum(self.latencies) / len(self.latencies) + logging.info( + color( + '@@@ Latencies: ' + f'min={min_latency:.2f}, ' + f'max={max_latency:.2f}, ' + f'average={avg_latency:.2f}' + ) + ) + + self.min_stats.append(min_latency) + self.max_stats.append(max_latency) + self.avg_stats.append(avg_latency) + + run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else '' + logging.info(color(f'=== {run_counter} Done!', 'magenta')) + + if self.repeat: + log_stats('Min Latency', self.min_stats) + log_stats('Max Latency', self.max_stats) + log_stats('Average Latency', self.avg_stats) + + if self.repeat: + logging.info(color('--- End of runs', 'blue')) async def send_next_ping(self): + if self.pace: + await asyncio.sleep(self.pace / 1000) + packet = struct.pack( '>bbI', PacketType.SEQUENCE, @@ -488,10 +606,11 @@ class Ping: class Pong: expected_packet_index: int - def __init__(self, packet_io): + def __init__(self, packet_io, linger): self.reset() self.packet_io = packet_io self.packet_io.packet_listener = self + self.linger = linger self.done = asyncio.Event() def reset(self): @@ -536,7 +655,7 @@ class Pong: ) ) - if packet_flags & PACKET_FLAG_LAST: + if packet_flags & PACKET_FLAG_LAST and not self.linger: self.done.set() async def run(self): @@ -554,6 +673,7 @@ class GattClient: self.speed_tx = None self.packet_listener = None self.ready = asyncio.Event() + self.overhead_size = 0 async def on_connection(self, connection): peer = Peer(connection) @@ -603,6 +723,9 @@ class GattClient: async def send_packet(self, packet): await self.speed_tx.write_value(packet) + async def drain(self): + pass + # ----------------------------------------------------------------------------- # GattServer @@ -612,6 +735,7 @@ class GattServer: self.device = device self.packet_listener = None self.ready = asyncio.Event() + self.overhead_size = 0 # Setup the GATT service self.speed_tx = Characteristic( @@ -653,6 +777,9 @@ class GattServer: async def send_packet(self, packet): await self.device.notify_subscribers(self.speed_rx, packet) + async def drain(self): + pass + # ----------------------------------------------------------------------------- # StreamedPacketIO @@ -664,6 +791,7 @@ class StreamedPacketIO: self.rx_packet = b'' self.rx_packet_header = b'' self.rx_packet_need = 0 + self.overhead_size = 2 def on_packet(self, packet): while packet: @@ -715,6 +843,7 @@ class L2capClient(StreamedPacketIO): self.max_credits = max_credits self.mtu = mtu self.mps = mps + self.l2cap_channel = None self.ready = asyncio.Event() async def on_connection(self, connection: Connection) -> None: @@ -736,9 +865,10 @@ class L2capClient(StreamedPacketIO): logging.info(color(f'!!! Connection failed: {error}', 'red')) return - l2cap_channel.sink = self.on_packet - l2cap_channel.on('close', self.on_l2cap_close) self.io_sink = l2cap_channel.write + self.l2cap_channel = l2cap_channel + l2cap_channel.on('close', self.on_l2cap_close) + l2cap_channel.sink = self.on_packet self.ready.set() @@ -748,6 +878,10 @@ class L2capClient(StreamedPacketIO): def on_l2cap_close(self): logging.info(color('*** L2CAP channel closed', 'red')) + async def drain(self): + assert self.l2cap_channel + await self.l2cap_channel.drain() + # ----------------------------------------------------------------------------- # L2capServer @@ -786,6 +920,7 @@ class L2capServer(StreamedPacketIO): logging.info(color(f'*** L2CAP channel: {l2cap_channel}', 'cyan')) self.io_sink = l2cap_channel.write + self.l2cap_channel = l2cap_channel l2cap_channel.on('close', self.on_l2cap_close) l2cap_channel.sink = self.on_packet @@ -795,6 +930,10 @@ class L2capServer(StreamedPacketIO): logging.info(color('*** L2CAP channel closed', 'red')) self.l2cap_channel = None + async def drain(self): + assert self.l2cap_channel + await self.l2cap_channel.drain() + # ----------------------------------------------------------------------------- # RfcommClient @@ -805,6 +944,7 @@ class RfcommClient(StreamedPacketIO): self.device = device self.channel = channel self.uuid = uuid + self.rfcomm_session = None self.ready = asyncio.Event() async def on_connection(self, connection): @@ -840,12 +980,17 @@ class RfcommClient(StreamedPacketIO): rfcomm_session.sink = self.on_packet self.io_sink = rfcomm_session.write + self.rfcomm_session = rfcomm_session self.ready.set() def on_disconnection(self, _): pass + async def drain(self): + assert self.rfcomm_session + await self.rfcomm_session.drain() + # ----------------------------------------------------------------------------- # RfcommServer @@ -853,6 +998,7 @@ class RfcommClient(StreamedPacketIO): class RfcommServer(StreamedPacketIO): def __init__(self, device, channel): super().__init__() + self.dlc = None self.ready = asyncio.Event() # Create and register a server @@ -881,6 +1027,11 @@ class RfcommServer(StreamedPacketIO): logging.info(color(f'*** DLC connected: {dlc}', 'blue')) dlc.sink = self.on_packet self.io_sink = dlc.write + self.dlc = dlc + + async def drain(self): + assert self.dlc + await self.dlc.drain() # ----------------------------------------------------------------------------- @@ -1030,6 +1181,7 @@ class Central(Connection.Listener): await role.run() await asyncio.sleep(DEFAULT_LINGER_TIME) + await self.connection.disconnect() def on_disconnection(self, reason): logging.info(color(f'!!! Disconnection: reason={reason}', 'red')) @@ -1120,12 +1272,8 @@ class Peripheral(Device.Listener, Connection.Listener): # Stop being discoverable and connectable if self.classic: - - async def stop_being_discoverable_connectable(): - await self.device.set_discoverable(False) - await self.device.set_connectable(False) - - AsyncRunner.spawn(stop_being_discoverable_connectable()) + AsyncRunner.spawn(self.device.set_discoverable(False)) + AsyncRunner.spawn(self.device.set_connectable(False)) # Request a new data length if needed if self.extended_data_length: @@ -1141,6 +1289,10 @@ class Peripheral(Device.Listener, Connection.Listener): self.connection = None self.role.reset() + if self.classic: + AsyncRunner.spawn(self.device.set_discoverable(True)) + AsyncRunner.spawn(self.device.set_connectable(True)) + def on_connection_parameters_update(self): print_connection(self.connection) @@ -1168,10 +1320,22 @@ def create_mode_factory(ctx, default_mode): return GattServer(device) if mode == 'l2cap-client': - return L2capClient(device, psm=ctx.obj['l2cap_psm']) + return L2capClient( + device, + psm=ctx.obj['l2cap_psm'], + mtu=ctx.obj['l2cap_mtu'], + mps=ctx.obj['l2cap_mps'], + max_credits=ctx.obj['l2cap_max_credits'], + ) if mode == 'l2cap-server': - return L2capServer(device, psm=ctx.obj['l2cap_psm']) + return L2capServer( + device, + psm=ctx.obj['l2cap_psm'], + mtu=ctx.obj['l2cap_mtu'], + mps=ctx.obj['l2cap_mps'], + max_credits=ctx.obj['l2cap_max_credits'], + ) if mode == 'rfcomm-client': return RfcommClient( @@ -1197,23 +1361,29 @@ def create_role_factory(ctx, default_role): return Sender( packet_io, start_delay=ctx.obj['start_delay'], + repeat=ctx.obj['repeat'], + repeat_delay=ctx.obj['repeat_delay'], + pace=ctx.obj['pace'], packet_size=ctx.obj['packet_size'], packet_count=ctx.obj['packet_count'], ) if role == 'receiver': - return Receiver(packet_io) + return Receiver(packet_io, ctx.obj['linger']) if role == 'ping': return Ping( packet_io, start_delay=ctx.obj['start_delay'], + repeat=ctx.obj['repeat'], + repeat_delay=ctx.obj['repeat_delay'], + pace=ctx.obj['pace'], packet_size=ctx.obj['packet_size'], packet_count=ctx.obj['packet_count'], ) if role == 'pong': - return Pong(packet_io) + return Pong(packet_io, ctx.obj['linger']) raise ValueError('invalid role') @@ -1258,7 +1428,7 @@ def create_role_factory(ctx, default_role): @click.option( '--rfcomm-uuid', default=DEFAULT_RFCOMM_UUID, - help='RFComm service UUID to use (ignored is --rfcomm-channel is not 0)', + help='RFComm service UUID to use (ignored if --rfcomm-channel is not 0)', ) @click.option( '--l2cap-psm', @@ -1266,13 +1436,31 @@ def create_role_factory(ctx, default_role): default=DEFAULT_L2CAP_PSM, help='L2CAP PSM to use', ) +@click.option( + '--l2cap-mtu', + type=int, + default=DEFAULT_L2CAP_MTU, + help='L2CAP MTU to use', +) +@click.option( + '--l2cap-mps', + type=int, + default=DEFAULT_L2CAP_MPS, + help='L2CAP MPS to use', +) +@click.option( + '--l2cap-max-credits', + type=int, + default=DEFAULT_L2CAP_MAX_CREDITS, + help='L2CAP maximum number of credits allowed for the peer', +) @click.option( '--packet-size', '-s', metavar='SIZE', type=click.IntRange(8, 4096), default=500, - help='Packet size (server role)', + help='Packet size (client or ping role)', ) @click.option( '--packet-count', @@ -1280,7 +1468,7 @@ def create_role_factory(ctx, default_role): metavar='COUNT', type=int, default=10, - help='Packet count (server role)', + help='Packet count (client or ping role)', ) @click.option( '--start-delay', @@ -1288,7 +1476,39 @@ def create_role_factory(ctx, default_role): metavar='SECONDS', type=int, default=1, - help='Start delay (server role)', + help='Start delay (client or ping role)', +) +@click.option( + '--repeat', + metavar='N', + type=int, + default=0, + help=( + 'Repeat the run N times (client and ping roles)' + '(0, which is the fault, to run just once) ' + ), +) +@click.option( + '--repeat-delay', + metavar='SECONDS', + type=int, + default=1, + help=('Delay, in seconds, between repeats'), +) +@click.option( + '--pace', + metavar='MILLISECONDS', + type=int, + default=0, + help=( + 'Wait N milliseconds between packets ' + '(0, which is the fault, to send as fast as possible) ' + ), +) +@click.option( + '--linger', + is_flag=True, + help="Don't exit at the end of a run (server and pong roles)", ) @click.pass_context def bench( @@ -1301,9 +1521,16 @@ def bench( packet_size, packet_count, start_delay, + repeat, + repeat_delay, + pace, + linger, rfcomm_channel, rfcomm_uuid, l2cap_psm, + l2cap_mtu, + l2cap_mps, + l2cap_max_credits, ): ctx.ensure_object(dict) ctx.obj['device_config'] = device_config @@ -1313,9 +1540,16 @@ def bench( ctx.obj['rfcomm_channel'] = rfcomm_channel ctx.obj['rfcomm_uuid'] = rfcomm_uuid ctx.obj['l2cap_psm'] = l2cap_psm + ctx.obj['l2cap_mtu'] = l2cap_mtu + ctx.obj['l2cap_mps'] = l2cap_mps + ctx.obj['l2cap_max_credits'] = l2cap_max_credits ctx.obj['packet_size'] = packet_size ctx.obj['packet_count'] = packet_count ctx.obj['start_delay'] = start_delay + ctx.obj['repeat'] = repeat + ctx.obj['repeat_delay'] = repeat_delay + ctx.obj['pace'] = pace + ctx.obj['linger'] = linger ctx.obj['extended_data_length'] = ( [int(x) for x in extended_data_length.split('/')] diff --git a/apps/l2cap_bridge.py b/apps/l2cap_bridge.py index 14bd759..7d744bc 100644 --- a/apps/l2cap_bridge.py +++ b/apps/l2cap_bridge.py @@ -49,14 +49,16 @@ class ServerBridge: self.tcp_port = tcp_port async def start(self, device: Device) -> None: - # Listen for incoming L2CAP CoC connections + # Listen for incoming L2CAP channel connections device.create_l2cap_server( spec=l2cap.LeCreditBasedChannelSpec( psm=self.psm, mtu=self.mtu, mps=self.mps, max_credits=self.max_credits ), - handler=self.on_coc, + handler=self.on_channel, + ) + print( + color(f'### Listening for channel connection on PSM {self.psm}', 'yellow') ) - print(color(f'### Listening for CoC connection on PSM {self.psm}', 'yellow')) def on_ble_connection(connection): def on_ble_disconnection(reason): @@ -73,7 +75,7 @@ class ServerBridge: await device.start_advertising(auto_restart=True) # Called when a new L2CAP connection is established - def on_coc(self, l2cap_channel): + def on_channel(self, l2cap_channel): print(color('*** L2CAP channel:', 'cyan'), l2cap_channel) class Pipe: @@ -83,7 +85,7 @@ class ServerBridge: self.l2cap_channel = l2cap_channel l2cap_channel.on('close', self.on_l2cap_close) - l2cap_channel.sink = self.on_coc_sdu + l2cap_channel.sink = self.on_channel_sdu async def connect_to_tcp(self): # Connect to the TCP server @@ -128,7 +130,7 @@ class ServerBridge: if self.tcp_transport is not None: self.tcp_transport.close() - def on_coc_sdu(self, sdu): + def on_channel_sdu(self, sdu): print(color(f'<<< [L2CAP SDU]: {len(sdu)} bytes', 'cyan')) if self.tcp_transport is None: print(color('!!! TCP socket not open, dropping', 'red')) @@ -183,7 +185,7 @@ class ClientBridge: peer_name = writer.get_extra_info('peer_name') print(color(f'<<< TCP connection from {peer_name}', 'magenta')) - def on_coc_sdu(sdu): + def on_channel_sdu(sdu): print(color(f'<<< [L2CAP SDU]: {len(sdu)} bytes', 'cyan')) l2cap_to_tcp_pipe.write(sdu) @@ -209,7 +211,7 @@ class ClientBridge: writer.close() return - l2cap_channel.sink = on_coc_sdu + l2cap_channel.sink = on_channel_sdu l2cap_channel.on('close', on_l2cap_close) # Start a flow control pipe from L2CAP to TCP @@ -274,23 +276,29 @@ async def run(device_config, hci_transport, bridge): @click.pass_context @click.option('--device-config', help='Device configuration file', required=True) @click.option('--hci-transport', help='HCI transport', required=True) -@click.option('--psm', help='PSM for L2CAP CoC', type=int, default=1234) +@click.option('--psm', help='PSM for L2CAP', type=int, default=1234) @click.option( - '--l2cap-coc-max-credits', - help='Maximum L2CAP CoC Credits', + '--l2cap-max-credits', + help='Maximum L2CAP Credits', type=click.IntRange(1, 65535), default=128, ) @click.option( - '--l2cap-coc-mtu', - help='L2CAP CoC MTU', - type=click.IntRange(23, 65535), - default=1022, + '--l2cap-mtu', + help='L2CAP MTU', + type=click.IntRange( + l2cap.L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MTU, + l2cap.L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_MTU, + ), + default=1024, ) @click.option( - '--l2cap-coc-mps', - help='L2CAP CoC MPS', - type=click.IntRange(23, 65533), + '--l2cap-mps', + help='L2CAP MPS', + type=click.IntRange( + l2cap.L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MPS, + l2cap.L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_MPS, + ), default=1024, ) def cli( @@ -298,17 +306,17 @@ def cli( device_config, hci_transport, psm, - l2cap_coc_max_credits, - l2cap_coc_mtu, - l2cap_coc_mps, + l2cap_max_credits, + l2cap_mtu, + l2cap_mps, ): context.ensure_object(dict) context.obj['device_config'] = device_config context.obj['hci_transport'] = hci_transport context.obj['psm'] = psm - context.obj['max_credits'] = l2cap_coc_max_credits - context.obj['mtu'] = l2cap_coc_mtu - context.obj['mps'] = l2cap_coc_mps + context.obj['max_credits'] = l2cap_max_credits + context.obj['mtu'] = l2cap_mtu + context.obj['mps'] = l2cap_mps # ----------------------------------------------------------------------------- diff --git a/bumble/l2cap.py b/bumble/l2cap.py index 7dc45d8..c1fbdf6 100644 --- a/bumble/l2cap.py +++ b/bumble/l2cap.py @@ -149,10 +149,11 @@ L2CAP_INVALID_CID_IN_REQUEST_REASON = 0x0002 L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_CREDITS = 65535 L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MTU = 23 +L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_MTU = 65535 L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MPS = 23 L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_MPS = 65533 L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU = 2048 -L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS = 2046 +L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS = 2048 L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_INITIAL_CREDITS = 256 L2CAP_MAXIMUM_TRANSMISSION_UNIT_CONFIGURATION_OPTION_TYPE = 0x01 @@ -188,8 +189,11 @@ class LeCreditBasedChannelSpec: or self.max_credits > L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_CREDITS ): raise ValueError('max credits out of range') - if self.mtu < L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MTU: - raise ValueError('MTU too small') + if ( + self.mtu < L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MTU + or self.mtu > L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_MTU + ): + raise ValueError('MTU out of range') if ( self.mps < L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MPS or self.mps > L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_MPS @@ -1644,12 +1648,13 @@ class ChannelManager: def send_pdu(self, connection, cid: int, pdu: Union[SupportsBytes, bytes]) -> None: pdu_str = pdu.hex() if isinstance(pdu, bytes) else str(pdu) + pdu_bytes = bytes(pdu) logger.debug( f'{color(">>> Sending L2CAP PDU", "blue")} ' f'on connection [0x{connection.handle:04X}] (CID={cid}) ' - f'{connection.peer_address}: {pdu_str}' + f'{connection.peer_address}: {len(pdu_bytes)} bytes, {pdu_str}' ) - self.host.send_l2cap_pdu(connection.handle, cid, bytes(pdu)) + self.host.send_l2cap_pdu(connection.handle, cid, pdu_bytes) def on_pdu(self, connection: Connection, cid: int, pdu: bytes) -> None: if cid in (L2CAP_SIGNALING_CID, L2CAP_LE_SIGNALING_CID): diff --git a/bumble/rfcomm.py b/bumble/rfcomm.py index 09bc2f2..53bbe53 100644 --- a/bumble/rfcomm.py +++ b/bumble/rfcomm.py @@ -454,6 +454,8 @@ class DLC(EventEmitter): self.c_r = 1 if self.role == Multiplexer.Role.INITIATOR else 0 self.sink = None self.connection_result = None + self.drained = asyncio.Event() + self.drained.set() # Compute the MTU max_overhead = 4 + 1 # header with 2-byte length + fcs @@ -633,6 +635,8 @@ class DLC(EventEmitter): ) rx_credits_needed = 0 + if not self.tx_buffer: + self.drained.set() # Stream protocol def write(self, data: Union[bytes, str]) -> None: @@ -645,11 +649,11 @@ class DLC(EventEmitter): raise ValueError('write only accept bytes or strings') self.tx_buffer += data + self.drained.clear() self.process_tx() - def drain(self) -> None: - # TODO - pass + async def drain(self) -> None: + await self.drained.wait() def __str__(self) -> str: return f'DLC(dlci={self.dlci},state={self.state.name})' diff --git a/docs/mkdocs/src/apps_and_tools/bench.md b/docs/mkdocs/src/apps_and_tools/bench.md index db785d6..be68161 100644 --- a/docs/mkdocs/src/apps_and_tools/bench.md +++ b/docs/mkdocs/src/apps_and_tools/bench.md @@ -7,16 +7,36 @@ throughput and/or latency between two devices. # General Usage ``` -Usage: bench.py [OPTIONS] COMMAND [ARGS]... +Usage: bumble-bench [OPTIONS] COMMAND [ARGS]... Options: --device-config FILENAME Device configuration file --role [sender|receiver|ping|pong] --mode [gatt-client|gatt-server|l2cap-client|l2cap-server|rfcomm-client|rfcomm-server] --att-mtu MTU GATT MTU (gatt-client mode) [23<=x<=517] - -s, --packet-size SIZE Packet size (server role) [8<=x<=4096] - -c, --packet-count COUNT Packet count (server role) - -sd, --start-delay SECONDS Start delay (server role) + --extended-data-length TEXT Request a data length upon connection, + specified as tx_octets/tx_time + --rfcomm-channel INTEGER RFComm channel to use + --rfcomm-uuid TEXT RFComm service UUID to use (ignored if + --rfcomm-channel is not 0) + --l2cap-psm INTEGER L2CAP PSM to use + --l2cap-mtu INTEGER L2CAP MTU to use + --l2cap-mps INTEGER L2CAP MPS to use + --l2cap-max-credits INTEGER L2CAP maximum number of credits allowed for + the peer + -s, --packet-size SIZE Packet size (client or ping role) + [8<=x<=4096] + -c, --packet-count COUNT Packet count (client or ping role) + -sd, --start-delay SECONDS Start delay (client or ping role) + --repeat N Repeat the run N times (client and ping + roles)(0, which is the fault, to run just + once) + --repeat-delay SECONDS Delay, in seconds, between repeats + --pace MILLISECONDS Wait N milliseconds between packets (0, + which is the fault, to send as fast as + possible) + --linger Don't exit at the end of a run (server and + pong roles) --help Show this message and exit. Commands: @@ -35,17 +55,18 @@ Options: --connection-interval, --ci CONNECTION_INTERVAL Connection interval (in ms) --phy [1m|2m|coded] PHY to use + --authenticate Authenticate (RFComm only) + --encrypt Encrypt the connection (RFComm only) --help Show this message and exit. ``` - -To test once device against another, one of the two devices must be running +To test once device against another, one of the two devices must be running the ``peripheral`` command and the other the ``central`` command. The device running the ``peripheral`` command will accept connections from the device running the ``central`` command. When using Bluetooth LE (all modes except for ``rfcomm-server`` and ``rfcomm-client``utils), -the default addresses configured in the tool should be sufficient. But when using -Bluetooth Classic, the address of the Peripheral must be specified on the Central +the default addresses configured in the tool should be sufficient. But when using +Bluetooth Classic, the address of the Peripheral must be specified on the Central using the ``--peripheral`` option. The address will be printed by the Peripheral when it starts. @@ -83,7 +104,7 @@ the other on `usb:1`, and two consoles/terminals. We will run a command in each. $ bumble-bench central usb:1 ``` - In this default configuration, the Central runs a Sender, as a GATT client, + In this default configuration, the Central runs a Sender, as a GATT client, connecting to the Peripheral running a Receiver, as a GATT server. !!! example "L2CAP Throughput" diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capClient.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capClient.kt index 4cf07d3..95cdae6 100644 --- a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capClient.kt +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/L2capClient.kt @@ -74,11 +74,13 @@ class L2capClient( gatt: BluetoothGatt?, status: Int, newState: Int ) { if (gatt != null && newState == BluetoothProfile.STATE_CONNECTED) { - gatt.setPreferredPhy( - BluetoothDevice.PHY_LE_2M_MASK, - BluetoothDevice.PHY_LE_2M_MASK, - BluetoothDevice.PHY_OPTION_NO_PREFERRED - ) + if (viewModel.use2mPhy) { + gatt.setPreferredPhy( + BluetoothDevice.PHY_LE_2M_MASK, + BluetoothDevice.PHY_LE_2M_MASK, + BluetoothDevice.PHY_OPTION_NO_PREFERRED + ) + } gatt.readPhy() // Request an MTU update, even though we don't use GATT, because Android diff --git a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Model.kt b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Model.kt index ed92abf..1a8cd6d 100644 --- a/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Model.kt +++ b/extras/android/BtBench/app/src/main/java/com/github/google/bumble/btbench/Model.kt @@ -27,11 +27,12 @@ val DEFAULT_RFCOMM_UUID: UUID = UUID.fromString("E6D55659-C8B4-4B85-96BB-B1143AF const val DEFAULT_PEER_BLUETOOTH_ADDRESS = "AA:BB:CC:DD:EE:FF" const val DEFAULT_SENDER_PACKET_COUNT = 100 const val DEFAULT_SENDER_PACKET_SIZE = 1024 +const val DEFAULT_PSM = 128 class AppViewModel : ViewModel() { private var preferences: SharedPreferences? = null var peerBluetoothAddress by mutableStateOf(DEFAULT_PEER_BLUETOOTH_ADDRESS) - var l2capPsm by mutableIntStateOf(0) + var l2capPsm by mutableIntStateOf(DEFAULT_PSM) var use2mPhy by mutableStateOf(true) var mtu by mutableIntStateOf(0) var rxPhy by mutableIntStateOf(0)