first implementation (+1 squashed commit)
Squashed commits:
[ee00d67] wip
diff --git a/apps/bench.py b/apps/bench.py
index 8b37883..de14eee 100644
--- a/apps/bench.py
+++ b/apps/bench.py
@@ -77,6 +77,7 @@
 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_MAX_CREDITS = 128
 DEFAULT_L2CAP_MTU = 1022
@@ -128,11 +129,16 @@
     if connection.transport == BT_LE_TRANSPORT:
         phy_state = (
             'PHY='
-            f'RX:{le_phy_name(connection.phy.rx_phy)}/'
-            f'TX:{le_phy_name(connection.phy.tx_phy)}'
+            f'TX:{le_phy_name(connection.phy.tx_phy)}/'
+            f'RX:{le_phy_name(connection.phy.rx_phy)}'
         )
 
-        data_length = f'DL={connection.data_length}'
+        data_length = (
+            'DL=('
+            f'TX:{connection.data_length[0]}/{connection.data_length[1]},'
+            f'RX:{connection.data_length[2]}/{connection.data_length[3]}'
+            ')'
+        )
         connection_parameters = (
             'Parameters='
             f'{connection.parameters.connection_interval * 1.25:.2f}/'
@@ -169,9 +175,7 @@
             ),
             ServiceAttribute(
                 SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
-                DataElement.sequence(
-                    [DataElement.uuid(UUID('E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'))]
-                ),
+                DataElement.sequence([DataElement.uuid(UUID(DEFAULT_RFCOMM_UUID))]),
             ),
             ServiceAttribute(
                 SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
@@ -224,7 +228,7 @@
 
         if self.tx_start_delay:
             print(color(f'*** Startup delay: {self.tx_start_delay}', 'blue'))
-            await asyncio.sleep(self.tx_start_delay)  # FIXME
+            await asyncio.sleep(self.tx_start_delay)
 
         print(color('=== Sending RESET', 'magenta'))
         await self.packet_io.send_packet(bytes([PacketType.RESET]))
@@ -364,7 +368,7 @@
 
         if self.tx_start_delay:
             print(color(f'*** Startup delay: {self.tx_start_delay}', 'blue'))
-            await asyncio.sleep(self.tx_start_delay)  # FIXME
+            await asyncio.sleep(self.tx_start_delay)
 
         print(color('=== Sending RESET', 'magenta'))
         await self.packet_io.send_packet(bytes([PacketType.RESET]))
@@ -710,14 +714,14 @@
         self.l2cap_channel = None
         self.ready = asyncio.Event()
 
-        # Listen for incoming L2CAP CoC connections
+        # Listen for incoming L2CAP connections
         device.create_l2cap_server(
             spec=l2cap.LeCreditBasedChannelSpec(
                 psm=psm, mtu=mtu, mps=mps, max_credits=max_credits
             ),
             handler=self.on_l2cap_channel,
         )
-        print(color(f'### Listening for CoC connection on PSM {psm}', 'yellow'))
+        print(color(f'### Listening for L2CAP connection on PSM {psm}', 'yellow'))
 
     async def on_connection(self, connection):
         connection.on('disconnection', self.on_disconnection)
@@ -743,9 +747,10 @@
 # RfcommClient
 # -----------------------------------------------------------------------------
 class RfcommClient(StreamedPacketIO):
-    def __init__(self, device):
+    def __init__(self, device, channel):
         super().__init__()
         self.device = device
+        self.channel = channel
         self.ready = asyncio.Event()
 
     async def on_connection(self, connection):
@@ -757,10 +762,9 @@
         rfcomm_mux = await rfcomm_client.start()
         print(color('*** Started', 'blue'))
 
-        channel = DEFAULT_RFCOMM_CHANNEL
-        print(color(f'### Opening session for channel {channel}...', 'yellow'))
+        print(color(f'### Opening session for channel {self.channel}...', 'yellow'))
         try:
-            rfcomm_session = await rfcomm_mux.open_dlc(channel)
+            rfcomm_session = await rfcomm_mux.open_dlc(self.channel)
             print(color('### Session open', 'yellow'), rfcomm_session)
         except bumble.core.ConnectionError as error:
             print(color(f'!!! Session open failed: {error}', 'red'))
@@ -780,7 +784,7 @@
 # RfcommServer
 # -----------------------------------------------------------------------------
 class RfcommServer(StreamedPacketIO):
-    def __init__(self, device):
+    def __init__(self, device, channel):
         super().__init__()
         self.ready = asyncio.Event()
 
@@ -788,7 +792,7 @@
         rfcomm_server = bumble.rfcomm.Server(device)
 
         # Listen for incoming DLC connections
-        channel_number = rfcomm_server.listen(self.on_dlc, DEFAULT_RFCOMM_CHANNEL)
+        channel_number = rfcomm_server.listen(self.on_dlc, channel)
 
         # Setup the SDP to advertise this channel
         device.sdp_service_records = make_sdp_records(channel_number)
@@ -825,6 +829,9 @@
         mode_factory,
         connection_interval,
         phy,
+        authenticate,
+        encrypt,
+        extended_data_length,
     ):
         super().__init__()
         self.transport = transport
@@ -832,6 +839,9 @@
         self.classic = classic
         self.role_factory = role_factory
         self.mode_factory = mode_factory
+        self.authenticate = authenticate
+        self.encrypt = encrypt or authenticate
+        self.extended_data_length = extended_data_length
         self.device = None
         self.connection = None
 
@@ -904,7 +914,26 @@
             self.connection.listener = self
             print_connection(self.connection)
 
-            await mode.on_connection(self.connection)
+            # Request a new data length if requested
+            if self.extended_data_length:
+                print(color('+++ Requesting extended data length', 'cyan'))
+                await self.connection.set_data_length(
+                    self.extended_data_length[0], self.extended_data_length[1]
+                )
+
+            # Authenticate if requested
+            if self.authenticate:
+                # Request authentication
+                print(color('*** Authenticating...', 'cyan'))
+                await self.connection.authenticate()
+                print(color('*** Authenticated', 'cyan'))
+
+            # Encrypt if requested
+            if self.encrypt:
+                # Enable encryption
+                print(color('*** Enabling encryption...', 'cyan'))
+                await self.connection.encrypt()
+                print(color('*** Encryption on', 'cyan'))
 
             # Set the PHY if requested
             if self.phy is not None:
@@ -919,6 +948,8 @@
                         )
                     )
 
+            await mode.on_connection(self.connection)
+
             await role.run()
             await asyncio.sleep(DEFAULT_LINGER_TIME)
 
@@ -943,9 +974,12 @@
 # Peripheral
 # -----------------------------------------------------------------------------
 class Peripheral(Device.Listener, Connection.Listener):
-    def __init__(self, transport, classic, role_factory, mode_factory):
+    def __init__(
+        self, transport, classic, extended_data_length, role_factory, mode_factory
+    ):
         self.transport = transport
         self.classic = classic
+        self.extended_data_length = extended_data_length
         self.role_factory = role_factory
         self.role = None
         self.mode_factory = mode_factory
@@ -1006,6 +1040,15 @@
         self.connection = connection
         self.connected.set()
 
+        # Request a new data length if needed
+        if self.extended_data_length:
+            print("+++ Requesting extended data length")
+            AsyncRunner.spawn(
+                connection.set_data_length(
+                    self.extended_data_length[0], self.extended_data_length[1]
+                )
+            )
+
     def on_disconnection(self, reason):
         print(color(f'!!! Disconnection: reason={reason}', 'red'))
         self.connection = None
@@ -1038,16 +1081,16 @@
             return GattServer(device)
 
         if mode == 'l2cap-client':
-            return L2capClient(device)
+            return L2capClient(device, psm=ctx.obj['l2cap_psm'])
 
         if mode == 'l2cap-server':
-            return L2capServer(device)
+            return L2capServer(device, psm=ctx.obj['l2cap_psm'])
 
         if mode == 'rfcomm-client':
-            return RfcommClient(device)
+            return RfcommClient(device, channel=ctx.obj['rfcomm_channel'])
 
         if mode == 'rfcomm-server':
-            return RfcommServer(device)
+            return RfcommServer(device, channel=ctx.obj['rfcomm_channel'])
 
         raise ValueError('invalid mode')
 
@@ -1114,6 +1157,22 @@
     help='GATT MTU (gatt-client mode)',
 )
 @click.option(
+    '--extended-data-length',
+    help='Request a data length upon connection, specified as tx_octets/tx_time',
+)
[email protected](
+    '--rfcomm-channel',
+    type=int,
+    default=DEFAULT_RFCOMM_CHANNEL,
+    help='RFComm channel to use',
+)
[email protected](
+    '--l2cap-psm',
+    type=int,
+    default=DEFAULT_L2CAP_PSM,
+    help='L2CAP PSM to use',
+)
[email protected](
     '--packet-size',
     '-s',
     metavar='SIZE',
@@ -1139,17 +1198,34 @@
 )
 @click.pass_context
 def bench(
-    ctx, device_config, role, mode, att_mtu, packet_size, packet_count, start_delay
+    ctx,
+    device_config,
+    role,
+    mode,
+    att_mtu,
+    extended_data_length,
+    packet_size,
+    packet_count,
+    start_delay,
+    rfcomm_channel,
+    l2cap_psm,
 ):
     ctx.ensure_object(dict)
     ctx.obj['device_config'] = device_config
     ctx.obj['role'] = role
     ctx.obj['mode'] = mode
     ctx.obj['att_mtu'] = att_mtu
+    ctx.obj['rfcomm_channel'] = rfcomm_channel
+    ctx.obj['l2cap_psm'] = l2cap_psm
     ctx.obj['packet_size'] = packet_size
     ctx.obj['packet_count'] = packet_count
     ctx.obj['start_delay'] = start_delay
 
+    ctx.obj['extended_data_length'] = (
+        [int(x) for x in extended_data_length.split('/')]
+        if extended_data_length
+        else None
+    )
     ctx.obj['classic'] = mode in ('rfcomm-client', 'rfcomm-server')
 
 
@@ -1170,8 +1246,12 @@
     help='Connection interval (in ms)',
 )
 @click.option('--phy', type=click.Choice(['1m', '2m', 'coded']), help='PHY to use')
[email protected]('--authenticate', is_flag=True, help='Authenticate (RFComm only)')
[email protected]('--encrypt', is_flag=True, help='Encrypt the connection (RFComm only)')
 @click.pass_context
-def central(ctx, transport, peripheral_address, connection_interval, phy):
+def central(
+    ctx, transport, peripheral_address, connection_interval, phy, authenticate, encrypt
+):
     """Run as a central (initiates the connection)"""
     role_factory = create_role_factory(ctx, 'sender')
     mode_factory = create_mode_factory(ctx, 'gatt-client')
@@ -1186,6 +1266,9 @@
             mode_factory,
             connection_interval,
             phy,
+            authenticate,
+            encrypt or authenticate,
+            ctx.obj['extended_data_length'],
         ).run()
     )
 
@@ -1199,7 +1282,13 @@
     mode_factory = create_mode_factory(ctx, 'gatt-server')
 
     asyncio.run(
-        Peripheral(transport, ctx.obj['classic'], role_factory, mode_factory).run()
+        Peripheral(
+            transport,
+            ctx.obj['classic'],
+            ctx.obj['extended_data_length'],
+            role_factory,
+            mode_factory,
+        ).run()
     )