bumble 0.0.178__py3-none-any.whl → 0.0.180__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bumble/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.0.178'
16
- __version_tuple__ = version_tuple = (0, 0, 178)
15
+ __version__ = version = '0.0.180'
16
+ __version_tuple__ = version_tuple = (0, 0, 180)
bumble/a2dp.py CHANGED
@@ -15,9 +15,13 @@
15
15
  # -----------------------------------------------------------------------------
16
16
  # Imports
17
17
  # -----------------------------------------------------------------------------
18
+ from __future__ import annotations
19
+
20
+ import dataclasses
18
21
  import struct
19
22
  import logging
20
- from collections import namedtuple
23
+ from collections.abc import AsyncGenerator
24
+ from typing import List, Callable, Awaitable
21
25
 
22
26
  from .company_ids import COMPANY_IDENTIFIERS
23
27
  from .sdp import (
@@ -239,24 +243,20 @@ def make_audio_sink_service_sdp_records(service_record_handle, version=(1, 3)):
239
243
 
240
244
 
241
245
  # -----------------------------------------------------------------------------
242
- class SbcMediaCodecInformation(
243
- namedtuple(
244
- 'SbcMediaCodecInformation',
245
- [
246
- 'sampling_frequency',
247
- 'channel_mode',
248
- 'block_length',
249
- 'subbands',
250
- 'allocation_method',
251
- 'minimum_bitpool_value',
252
- 'maximum_bitpool_value',
253
- ],
254
- )
255
- ):
246
+ @dataclasses.dataclass
247
+ class SbcMediaCodecInformation:
256
248
  '''
257
249
  A2DP spec - 4.3.2 Codec Specific Information Elements
258
250
  '''
259
251
 
252
+ sampling_frequency: int
253
+ channel_mode: int
254
+ block_length: int
255
+ subbands: int
256
+ allocation_method: int
257
+ minimum_bitpool_value: int
258
+ maximum_bitpool_value: int
259
+
260
260
  SAMPLING_FREQUENCY_BITS = {16000: 1 << 3, 32000: 1 << 2, 44100: 1 << 1, 48000: 1}
261
261
  CHANNEL_MODE_BITS = {
262
262
  SBC_MONO_CHANNEL_MODE: 1 << 3,
@@ -272,7 +272,7 @@ class SbcMediaCodecInformation(
272
272
  }
273
273
 
274
274
  @staticmethod
275
- def from_bytes(data: bytes) -> 'SbcMediaCodecInformation':
275
+ def from_bytes(data: bytes) -> SbcMediaCodecInformation:
276
276
  sampling_frequency = (data[0] >> 4) & 0x0F
277
277
  channel_mode = (data[0] >> 0) & 0x0F
278
278
  block_length = (data[1] >> 4) & 0x0F
@@ -293,14 +293,14 @@ class SbcMediaCodecInformation(
293
293
  @classmethod
294
294
  def from_discrete_values(
295
295
  cls,
296
- sampling_frequency,
297
- channel_mode,
298
- block_length,
299
- subbands,
300
- allocation_method,
301
- minimum_bitpool_value,
302
- maximum_bitpool_value,
303
- ):
296
+ sampling_frequency: int,
297
+ channel_mode: int,
298
+ block_length: int,
299
+ subbands: int,
300
+ allocation_method: int,
301
+ minimum_bitpool_value: int,
302
+ maximum_bitpool_value: int,
303
+ ) -> SbcMediaCodecInformation:
304
304
  return SbcMediaCodecInformation(
305
305
  sampling_frequency=cls.SAMPLING_FREQUENCY_BITS[sampling_frequency],
306
306
  channel_mode=cls.CHANNEL_MODE_BITS[channel_mode],
@@ -314,14 +314,14 @@ class SbcMediaCodecInformation(
314
314
  @classmethod
315
315
  def from_lists(
316
316
  cls,
317
- sampling_frequencies,
318
- channel_modes,
319
- block_lengths,
320
- subbands,
321
- allocation_methods,
322
- minimum_bitpool_value,
323
- maximum_bitpool_value,
324
- ):
317
+ sampling_frequencies: List[int],
318
+ channel_modes: List[int],
319
+ block_lengths: List[int],
320
+ subbands: List[int],
321
+ allocation_methods: List[int],
322
+ minimum_bitpool_value: int,
323
+ maximum_bitpool_value: int,
324
+ ) -> SbcMediaCodecInformation:
325
325
  return SbcMediaCodecInformation(
326
326
  sampling_frequency=sum(
327
327
  cls.SAMPLING_FREQUENCY_BITS[x] for x in sampling_frequencies
@@ -348,7 +348,7 @@ class SbcMediaCodecInformation(
348
348
  ]
349
349
  )
350
350
 
351
- def __str__(self):
351
+ def __str__(self) -> str:
352
352
  channel_modes = ['MONO', 'DUAL_CHANNEL', 'STEREO', 'JOINT_STEREO']
353
353
  allocation_methods = ['SNR', 'Loudness']
354
354
  return '\n'.join(
@@ -367,16 +367,19 @@ class SbcMediaCodecInformation(
367
367
 
368
368
 
369
369
  # -----------------------------------------------------------------------------
370
- class AacMediaCodecInformation(
371
- namedtuple(
372
- 'AacMediaCodecInformation',
373
- ['object_type', 'sampling_frequency', 'channels', 'rfa', 'vbr', 'bitrate'],
374
- )
375
- ):
370
+ @dataclasses.dataclass
371
+ class AacMediaCodecInformation:
376
372
  '''
377
373
  A2DP spec - 4.5.2 Codec Specific Information Elements
378
374
  '''
379
375
 
376
+ object_type: int
377
+ sampling_frequency: int
378
+ channels: int
379
+ rfa: int
380
+ vbr: int
381
+ bitrate: int
382
+
380
383
  OBJECT_TYPE_BITS = {
381
384
  MPEG_2_AAC_LC_OBJECT_TYPE: 1 << 7,
382
385
  MPEG_4_AAC_LC_OBJECT_TYPE: 1 << 6,
@@ -400,7 +403,7 @@ class AacMediaCodecInformation(
400
403
  CHANNELS_BITS = {1: 1 << 1, 2: 1}
401
404
 
402
405
  @staticmethod
403
- def from_bytes(data: bytes) -> 'AacMediaCodecInformation':
406
+ def from_bytes(data: bytes) -> AacMediaCodecInformation:
404
407
  object_type = data[0]
405
408
  sampling_frequency = (data[1] << 4) | ((data[2] >> 4) & 0x0F)
406
409
  channels = (data[2] >> 2) & 0x03
@@ -413,8 +416,13 @@ class AacMediaCodecInformation(
413
416
 
414
417
  @classmethod
415
418
  def from_discrete_values(
416
- cls, object_type, sampling_frequency, channels, vbr, bitrate
417
- ):
419
+ cls,
420
+ object_type: int,
421
+ sampling_frequency: int,
422
+ channels: int,
423
+ vbr: int,
424
+ bitrate: int,
425
+ ) -> AacMediaCodecInformation:
418
426
  return AacMediaCodecInformation(
419
427
  object_type=cls.OBJECT_TYPE_BITS[object_type],
420
428
  sampling_frequency=cls.SAMPLING_FREQUENCY_BITS[sampling_frequency],
@@ -425,7 +433,14 @@ class AacMediaCodecInformation(
425
433
  )
426
434
 
427
435
  @classmethod
428
- def from_lists(cls, object_types, sampling_frequencies, channels, vbr, bitrate):
436
+ def from_lists(
437
+ cls,
438
+ object_types: List[int],
439
+ sampling_frequencies: List[int],
440
+ channels: List[int],
441
+ vbr: int,
442
+ bitrate: int,
443
+ ) -> AacMediaCodecInformation:
429
444
  return AacMediaCodecInformation(
430
445
  object_type=sum(cls.OBJECT_TYPE_BITS[x] for x in object_types),
431
446
  sampling_frequency=sum(
@@ -449,7 +464,7 @@ class AacMediaCodecInformation(
449
464
  ]
450
465
  )
451
466
 
452
- def __str__(self):
467
+ def __str__(self) -> str:
453
468
  object_types = [
454
469
  'MPEG_2_AAC_LC',
455
470
  'MPEG_4_AAC_LC',
@@ -474,26 +489,26 @@ class AacMediaCodecInformation(
474
489
  )
475
490
 
476
491
 
492
+ @dataclasses.dataclass
477
493
  # -----------------------------------------------------------------------------
478
494
  class VendorSpecificMediaCodecInformation:
479
495
  '''
480
496
  A2DP spec - 4.7.2 Codec Specific Information Elements
481
497
  '''
482
498
 
499
+ vendor_id: int
500
+ codec_id: int
501
+ value: bytes
502
+
483
503
  @staticmethod
484
- def from_bytes(data):
504
+ def from_bytes(data: bytes) -> VendorSpecificMediaCodecInformation:
485
505
  (vendor_id, codec_id) = struct.unpack_from('<IH', data, 0)
486
506
  return VendorSpecificMediaCodecInformation(vendor_id, codec_id, data[6:])
487
507
 
488
- def __init__(self, vendor_id, codec_id, value):
489
- self.vendor_id = vendor_id
490
- self.codec_id = codec_id
491
- self.value = value
492
-
493
- def __bytes__(self):
508
+ def __bytes__(self) -> bytes:
494
509
  return struct.pack('<IH', self.vendor_id, self.codec_id, self.value)
495
510
 
496
- def __str__(self):
511
+ def __str__(self) -> str:
497
512
  # pylint: disable=line-too-long
498
513
  return '\n'.join(
499
514
  [
@@ -506,29 +521,27 @@ class VendorSpecificMediaCodecInformation:
506
521
 
507
522
 
508
523
  # -----------------------------------------------------------------------------
524
+ @dataclasses.dataclass
509
525
  class SbcFrame:
510
- def __init__(
511
- self, sampling_frequency, block_count, channel_mode, subband_count, payload
512
- ):
513
- self.sampling_frequency = sampling_frequency
514
- self.block_count = block_count
515
- self.channel_mode = channel_mode
516
- self.subband_count = subband_count
517
- self.payload = payload
526
+ sampling_frequency: int
527
+ block_count: int
528
+ channel_mode: int
529
+ subband_count: int
530
+ payload: bytes
518
531
 
519
532
  @property
520
- def sample_count(self):
533
+ def sample_count(self) -> int:
521
534
  return self.subband_count * self.block_count
522
535
 
523
536
  @property
524
- def bitrate(self):
537
+ def bitrate(self) -> int:
525
538
  return 8 * ((len(self.payload) * self.sampling_frequency) // self.sample_count)
526
539
 
527
540
  @property
528
- def duration(self):
541
+ def duration(self) -> float:
529
542
  return self.sample_count / self.sampling_frequency
530
543
 
531
- def __str__(self):
544
+ def __str__(self) -> str:
532
545
  return (
533
546
  f'SBC(sf={self.sampling_frequency},'
534
547
  f'cm={self.channel_mode},'
@@ -540,12 +553,12 @@ class SbcFrame:
540
553
 
541
554
  # -----------------------------------------------------------------------------
542
555
  class SbcParser:
543
- def __init__(self, read):
556
+ def __init__(self, read: Callable[[int], Awaitable[bytes]]) -> None:
544
557
  self.read = read
545
558
 
546
559
  @property
547
- def frames(self):
548
- async def generate_frames():
560
+ def frames(self) -> AsyncGenerator[SbcFrame, None]:
561
+ async def generate_frames() -> AsyncGenerator[SbcFrame, None]:
549
562
  while True:
550
563
  # Read 4 bytes of header
551
564
  header = await self.read(4)
@@ -589,7 +602,9 @@ class SbcParser:
589
602
 
590
603
  # -----------------------------------------------------------------------------
591
604
  class SbcPacketSource:
592
- def __init__(self, read, mtu, codec_capabilities):
605
+ def __init__(
606
+ self, read: Callable[[int], Awaitable[bytes]], mtu: int, codec_capabilities
607
+ ) -> None:
593
608
  self.read = read
594
609
  self.mtu = mtu
595
610
  self.codec_capabilities = codec_capabilities
bumble/apps/bench.py CHANGED
@@ -50,8 +50,10 @@ from bumble.sdp import (
50
50
  SDP_PUBLIC_BROWSE_ROOT,
51
51
  SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
52
52
  SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
53
+ SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
53
54
  DataElement,
54
55
  ServiceAttribute,
56
+ Client as SdpClient,
55
57
  )
56
58
  from bumble.transport import open_transport_or_link
57
59
  import bumble.rfcomm
@@ -77,6 +79,7 @@ SPEED_SERVICE_UUID = '50DB505C-8AC4-4738-8448-3B1D9CC09CC5'
77
79
  SPEED_TX_UUID = 'E789C754-41A1-45F4-A948-A0A1A90DBA53'
78
80
  SPEED_RX_UUID = '016A2CC7-E14B-4819-935F-1F56EAE4098D'
79
81
 
82
+ DEFAULT_RFCOMM_UUID = 'E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'
80
83
  DEFAULT_L2CAP_PSM = 1234
81
84
  DEFAULT_L2CAP_MAX_CREDITS = 128
82
85
  DEFAULT_L2CAP_MTU = 1022
@@ -128,11 +131,16 @@ def print_connection(connection):
128
131
  if connection.transport == BT_LE_TRANSPORT:
129
132
  phy_state = (
130
133
  'PHY='
131
- f'RX:{le_phy_name(connection.phy.rx_phy)}/'
132
- f'TX:{le_phy_name(connection.phy.tx_phy)}'
134
+ f'TX:{le_phy_name(connection.phy.tx_phy)}/'
135
+ f'RX:{le_phy_name(connection.phy.rx_phy)}'
133
136
  )
134
137
 
135
- data_length = f'DL={connection.data_length}'
138
+ data_length = (
139
+ 'DL=('
140
+ f'TX:{connection.data_length[0]}/{connection.data_length[1]},'
141
+ f'RX:{connection.data_length[2]}/{connection.data_length[3]}'
142
+ ')'
143
+ )
136
144
  connection_parameters = (
137
145
  'Parameters='
138
146
  f'{connection.parameters.connection_interval * 1.25:.2f}/'
@@ -169,9 +177,7 @@ def make_sdp_records(channel):
169
177
  ),
170
178
  ServiceAttribute(
171
179
  SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
172
- DataElement.sequence(
173
- [DataElement.uuid(UUID('E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'))]
174
- ),
180
+ DataElement.sequence([DataElement.uuid(UUID(DEFAULT_RFCOMM_UUID))]),
175
181
  ),
176
182
  ServiceAttribute(
177
183
  SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
@@ -191,6 +197,48 @@ def make_sdp_records(channel):
191
197
  }
192
198
 
193
199
 
200
+ async def find_rfcomm_channel_with_uuid(connection: Connection, uuid: str) -> int:
201
+ # Connect to the SDP Server
202
+ sdp_client = SdpClient(connection)
203
+ await sdp_client.connect()
204
+
205
+ # Search for services with an L2CAP service attribute
206
+ search_result = await sdp_client.search_attributes(
207
+ [BT_L2CAP_PROTOCOL_ID],
208
+ [
209
+ SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
210
+ SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
211
+ SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
212
+ ],
213
+ )
214
+ for attribute_list in search_result:
215
+ service_uuid = None
216
+ service_class_id_list = ServiceAttribute.find_attribute_in_list(
217
+ attribute_list, SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID
218
+ )
219
+ if service_class_id_list:
220
+ if service_class_id_list.value:
221
+ for service_class_id in service_class_id_list.value:
222
+ service_uuid = service_class_id.value
223
+ if str(service_uuid) != uuid:
224
+ # This service doesn't have a UUID or isn't the right one.
225
+ continue
226
+
227
+ # Look for the RFCOMM Channel number
228
+ protocol_descriptor_list = ServiceAttribute.find_attribute_in_list(
229
+ attribute_list, SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID
230
+ )
231
+ if protocol_descriptor_list:
232
+ for protocol_descriptor in protocol_descriptor_list.value:
233
+ if len(protocol_descriptor.value) >= 2:
234
+ if protocol_descriptor.value[0].value == BT_RFCOMM_PROTOCOL_ID:
235
+ await sdp_client.disconnect()
236
+ return protocol_descriptor.value[1].value
237
+
238
+ await sdp_client.disconnect()
239
+ return 0
240
+
241
+
194
242
  class PacketType(enum.IntEnum):
195
243
  RESET = 0
196
244
  SEQUENCE = 1
@@ -224,7 +272,7 @@ class Sender:
224
272
 
225
273
  if self.tx_start_delay:
226
274
  print(color(f'*** Startup delay: {self.tx_start_delay}', 'blue'))
227
- await asyncio.sleep(self.tx_start_delay) # FIXME
275
+ await asyncio.sleep(self.tx_start_delay)
228
276
 
229
277
  print(color('=== Sending RESET', 'magenta'))
230
278
  await self.packet_io.send_packet(bytes([PacketType.RESET]))
@@ -364,7 +412,7 @@ class Ping:
364
412
 
365
413
  if self.tx_start_delay:
366
414
  print(color(f'*** Startup delay: {self.tx_start_delay}', 'blue'))
367
- await asyncio.sleep(self.tx_start_delay) # FIXME
415
+ await asyncio.sleep(self.tx_start_delay)
368
416
 
369
417
  print(color('=== Sending RESET', 'magenta'))
370
418
  await self.packet_io.send_packet(bytes([PacketType.RESET]))
@@ -710,14 +758,14 @@ class L2capServer(StreamedPacketIO):
710
758
  self.l2cap_channel = None
711
759
  self.ready = asyncio.Event()
712
760
 
713
- # Listen for incoming L2CAP CoC connections
761
+ # Listen for incoming L2CAP connections
714
762
  device.create_l2cap_server(
715
763
  spec=l2cap.LeCreditBasedChannelSpec(
716
764
  psm=psm, mtu=mtu, mps=mps, max_credits=max_credits
717
765
  ),
718
766
  handler=self.on_l2cap_channel,
719
767
  )
720
- print(color(f'### Listening for CoC connection on PSM {psm}', 'yellow'))
768
+ print(color(f'### Listening for L2CAP connection on PSM {psm}', 'yellow'))
721
769
 
722
770
  async def on_connection(self, connection):
723
771
  connection.on('disconnection', self.on_disconnection)
@@ -743,21 +791,35 @@ class L2capServer(StreamedPacketIO):
743
791
  # RfcommClient
744
792
  # -----------------------------------------------------------------------------
745
793
  class RfcommClient(StreamedPacketIO):
746
- def __init__(self, device):
794
+ def __init__(self, device, channel, uuid):
747
795
  super().__init__()
748
796
  self.device = device
797
+ self.channel = channel
798
+ self.uuid = uuid
749
799
  self.ready = asyncio.Event()
750
800
 
751
801
  async def on_connection(self, connection):
752
802
  connection.on('disconnection', self.on_disconnection)
753
803
 
804
+ # Find the channel number if not specified
805
+ channel = self.channel
806
+ if channel == 0:
807
+ print(
808
+ color(f'@@@ Discovering channel number from UUID {self.uuid}', 'cyan')
809
+ )
810
+ channel = await find_rfcomm_channel_with_uuid(connection, self.uuid)
811
+ print(color(f'@@@ Channel number = {channel}', 'cyan'))
812
+ if channel == 0:
813
+ print(color('!!! No RFComm service with this UUID found', 'red'))
814
+ await connection.disconnect()
815
+ return
816
+
754
817
  # Create a client and start it
755
818
  print(color('*** Starting RFCOMM client...', 'blue'))
756
- rfcomm_client = bumble.rfcomm.Client(self.device, connection)
819
+ rfcomm_client = bumble.rfcomm.Client(connection)
757
820
  rfcomm_mux = await rfcomm_client.start()
758
821
  print(color('*** Started', 'blue'))
759
822
 
760
- channel = DEFAULT_RFCOMM_CHANNEL
761
823
  print(color(f'### Opening session for channel {channel}...', 'yellow'))
762
824
  try:
763
825
  rfcomm_session = await rfcomm_mux.open_dlc(channel)
@@ -780,7 +842,7 @@ class RfcommClient(StreamedPacketIO):
780
842
  # RfcommServer
781
843
  # -----------------------------------------------------------------------------
782
844
  class RfcommServer(StreamedPacketIO):
783
- def __init__(self, device):
845
+ def __init__(self, device, channel):
784
846
  super().__init__()
785
847
  self.ready = asyncio.Event()
786
848
 
@@ -788,7 +850,7 @@ class RfcommServer(StreamedPacketIO):
788
850
  rfcomm_server = bumble.rfcomm.Server(device)
789
851
 
790
852
  # Listen for incoming DLC connections
791
- channel_number = rfcomm_server.listen(self.on_dlc, DEFAULT_RFCOMM_CHANNEL)
853
+ channel_number = rfcomm_server.listen(self.on_dlc, channel)
792
854
 
793
855
  # Setup the SDP to advertise this channel
794
856
  device.sdp_service_records = make_sdp_records(channel_number)
@@ -825,6 +887,9 @@ class Central(Connection.Listener):
825
887
  mode_factory,
826
888
  connection_interval,
827
889
  phy,
890
+ authenticate,
891
+ encrypt,
892
+ extended_data_length,
828
893
  ):
829
894
  super().__init__()
830
895
  self.transport = transport
@@ -832,6 +897,9 @@ class Central(Connection.Listener):
832
897
  self.classic = classic
833
898
  self.role_factory = role_factory
834
899
  self.mode_factory = mode_factory
900
+ self.authenticate = authenticate
901
+ self.encrypt = encrypt or authenticate
902
+ self.extended_data_length = extended_data_length
835
903
  self.device = None
836
904
  self.connection = None
837
905
 
@@ -904,7 +972,26 @@ class Central(Connection.Listener):
904
972
  self.connection.listener = self
905
973
  print_connection(self.connection)
906
974
 
907
- await mode.on_connection(self.connection)
975
+ # Request a new data length if requested
976
+ if self.extended_data_length:
977
+ print(color('+++ Requesting extended data length', 'cyan'))
978
+ await self.connection.set_data_length(
979
+ self.extended_data_length[0], self.extended_data_length[1]
980
+ )
981
+
982
+ # Authenticate if requested
983
+ if self.authenticate:
984
+ # Request authentication
985
+ print(color('*** Authenticating...', 'cyan'))
986
+ await self.connection.authenticate()
987
+ print(color('*** Authenticated', 'cyan'))
988
+
989
+ # Encrypt if requested
990
+ if self.encrypt:
991
+ # Enable encryption
992
+ print(color('*** Enabling encryption...', 'cyan'))
993
+ await self.connection.encrypt()
994
+ print(color('*** Encryption on', 'cyan'))
908
995
 
909
996
  # Set the PHY if requested
910
997
  if self.phy is not None:
@@ -919,6 +1006,8 @@ class Central(Connection.Listener):
919
1006
  )
920
1007
  )
921
1008
 
1009
+ await mode.on_connection(self.connection)
1010
+
922
1011
  await role.run()
923
1012
  await asyncio.sleep(DEFAULT_LINGER_TIME)
924
1013
 
@@ -943,9 +1032,12 @@ class Central(Connection.Listener):
943
1032
  # Peripheral
944
1033
  # -----------------------------------------------------------------------------
945
1034
  class Peripheral(Device.Listener, Connection.Listener):
946
- def __init__(self, transport, classic, role_factory, mode_factory):
1035
+ def __init__(
1036
+ self, transport, classic, extended_data_length, role_factory, mode_factory
1037
+ ):
947
1038
  self.transport = transport
948
1039
  self.classic = classic
1040
+ self.extended_data_length = extended_data_length
949
1041
  self.role_factory = role_factory
950
1042
  self.role = None
951
1043
  self.mode_factory = mode_factory
@@ -1006,6 +1098,15 @@ class Peripheral(Device.Listener, Connection.Listener):
1006
1098
  self.connection = connection
1007
1099
  self.connected.set()
1008
1100
 
1101
+ # Request a new data length if needed
1102
+ if self.extended_data_length:
1103
+ print("+++ Requesting extended data length")
1104
+ AsyncRunner.spawn(
1105
+ connection.set_data_length(
1106
+ self.extended_data_length[0], self.extended_data_length[1]
1107
+ )
1108
+ )
1109
+
1009
1110
  def on_disconnection(self, reason):
1010
1111
  print(color(f'!!! Disconnection: reason={reason}', 'red'))
1011
1112
  self.connection = None
@@ -1038,16 +1139,18 @@ def create_mode_factory(ctx, default_mode):
1038
1139
  return GattServer(device)
1039
1140
 
1040
1141
  if mode == 'l2cap-client':
1041
- return L2capClient(device)
1142
+ return L2capClient(device, psm=ctx.obj['l2cap_psm'])
1042
1143
 
1043
1144
  if mode == 'l2cap-server':
1044
- return L2capServer(device)
1145
+ return L2capServer(device, psm=ctx.obj['l2cap_psm'])
1045
1146
 
1046
1147
  if mode == 'rfcomm-client':
1047
- return RfcommClient(device)
1148
+ return RfcommClient(
1149
+ device, channel=ctx.obj['rfcomm_channel'], uuid=ctx.obj['rfcomm_uuid']
1150
+ )
1048
1151
 
1049
1152
  if mode == 'rfcomm-server':
1050
- return RfcommServer(device)
1153
+ return RfcommServer(device, channel=ctx.obj['rfcomm_channel'])
1051
1154
 
1052
1155
  raise ValueError('invalid mode')
1053
1156
 
@@ -1113,6 +1216,27 @@ def create_role_factory(ctx, default_role):
1113
1216
  type=click.IntRange(23, 517),
1114
1217
  help='GATT MTU (gatt-client mode)',
1115
1218
  )
1219
+ @click.option(
1220
+ '--extended-data-length',
1221
+ help='Request a data length upon connection, specified as tx_octets/tx_time',
1222
+ )
1223
+ @click.option(
1224
+ '--rfcomm-channel',
1225
+ type=int,
1226
+ default=DEFAULT_RFCOMM_CHANNEL,
1227
+ help='RFComm channel to use',
1228
+ )
1229
+ @click.option(
1230
+ '--rfcomm-uuid',
1231
+ default=DEFAULT_RFCOMM_UUID,
1232
+ help='RFComm service UUID to use (ignored is --rfcomm-channel is not 0)',
1233
+ )
1234
+ @click.option(
1235
+ '--l2cap-psm',
1236
+ type=int,
1237
+ default=DEFAULT_L2CAP_PSM,
1238
+ help='L2CAP PSM to use',
1239
+ )
1116
1240
  @click.option(
1117
1241
  '--packet-size',
1118
1242
  '-s',
@@ -1139,17 +1263,36 @@ def create_role_factory(ctx, default_role):
1139
1263
  )
1140
1264
  @click.pass_context
1141
1265
  def bench(
1142
- ctx, device_config, role, mode, att_mtu, packet_size, packet_count, start_delay
1266
+ ctx,
1267
+ device_config,
1268
+ role,
1269
+ mode,
1270
+ att_mtu,
1271
+ extended_data_length,
1272
+ packet_size,
1273
+ packet_count,
1274
+ start_delay,
1275
+ rfcomm_channel,
1276
+ rfcomm_uuid,
1277
+ l2cap_psm,
1143
1278
  ):
1144
1279
  ctx.ensure_object(dict)
1145
1280
  ctx.obj['device_config'] = device_config
1146
1281
  ctx.obj['role'] = role
1147
1282
  ctx.obj['mode'] = mode
1148
1283
  ctx.obj['att_mtu'] = att_mtu
1284
+ ctx.obj['rfcomm_channel'] = rfcomm_channel
1285
+ ctx.obj['rfcomm_uuid'] = rfcomm_uuid
1286
+ ctx.obj['l2cap_psm'] = l2cap_psm
1149
1287
  ctx.obj['packet_size'] = packet_size
1150
1288
  ctx.obj['packet_count'] = packet_count
1151
1289
  ctx.obj['start_delay'] = start_delay
1152
1290
 
1291
+ ctx.obj['extended_data_length'] = (
1292
+ [int(x) for x in extended_data_length.split('/')]
1293
+ if extended_data_length
1294
+ else None
1295
+ )
1153
1296
  ctx.obj['classic'] = mode in ('rfcomm-client', 'rfcomm-server')
1154
1297
 
1155
1298
 
@@ -1170,8 +1313,12 @@ def bench(
1170
1313
  help='Connection interval (in ms)',
1171
1314
  )
1172
1315
  @click.option('--phy', type=click.Choice(['1m', '2m', 'coded']), help='PHY to use')
1316
+ @click.option('--authenticate', is_flag=True, help='Authenticate (RFComm only)')
1317
+ @click.option('--encrypt', is_flag=True, help='Encrypt the connection (RFComm only)')
1173
1318
  @click.pass_context
1174
- def central(ctx, transport, peripheral_address, connection_interval, phy):
1319
+ def central(
1320
+ ctx, transport, peripheral_address, connection_interval, phy, authenticate, encrypt
1321
+ ):
1175
1322
  """Run as a central (initiates the connection)"""
1176
1323
  role_factory = create_role_factory(ctx, 'sender')
1177
1324
  mode_factory = create_mode_factory(ctx, 'gatt-client')
@@ -1186,6 +1333,9 @@ def central(ctx, transport, peripheral_address, connection_interval, phy):
1186
1333
  mode_factory,
1187
1334
  connection_interval,
1188
1335
  phy,
1336
+ authenticate,
1337
+ encrypt or authenticate,
1338
+ ctx.obj['extended_data_length'],
1189
1339
  ).run()
1190
1340
  )
1191
1341
 
@@ -1199,7 +1349,13 @@ def peripheral(ctx, transport):
1199
1349
  mode_factory = create_mode_factory(ctx, 'gatt-server')
1200
1350
 
1201
1351
  asyncio.run(
1202
- Peripheral(transport, ctx.obj['classic'], role_factory, mode_factory).run()
1352
+ Peripheral(
1353
+ transport,
1354
+ ctx.obj['classic'],
1355
+ ctx.obj['extended_data_length'],
1356
+ role_factory,
1357
+ mode_factory,
1358
+ ).run()
1203
1359
  )
1204
1360
 
1205
1361