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 +2 -2
- bumble/a2dp.py +83 -68
- bumble/apps/bench.py +180 -24
- bumble/apps/controller_info.py +14 -0
- bumble/apps/pair.py +9 -2
- bumble/avdtp.py +3 -3
- bumble/crypto.py +82 -66
- bumble/device.py +247 -23
- bumble/gatt.py +117 -7
- bumble/gatt_client.py +56 -20
- bumble/hci.py +351 -78
- bumble/helpers.py +67 -42
- bumble/hid.py +8 -7
- bumble/l2cap.py +8 -0
- bumble/profiles/csip.py +147 -0
- bumble/rfcomm.py +2 -3
- bumble/sdp.py +4 -4
- bumble/smp.py +66 -43
- bumble/transport/common.py +1 -1
- bumble/transport/usb.py +58 -61
- bumble/utils.py +17 -1
- {bumble-0.0.178.dist-info → bumble-0.0.180.dist-info}/METADATA +1 -1
- {bumble-0.0.178.dist-info → bumble-0.0.180.dist-info}/RECORD +27 -26
- {bumble-0.0.178.dist-info → bumble-0.0.180.dist-info}/WHEEL +1 -1
- {bumble-0.0.178.dist-info → bumble-0.0.180.dist-info}/LICENSE +0 -0
- {bumble-0.0.178.dist-info → bumble-0.0.180.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.178.dist-info → bumble-0.0.180.dist-info}/top_level.txt +0 -0
bumble/_version.py
CHANGED
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
|
|
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
|
-
|
|
243
|
-
|
|
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) ->
|
|
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
|
-
|
|
371
|
-
|
|
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) ->
|
|
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,
|
|
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(
|
|
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
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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__(
|
|
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'
|
|
132
|
-
f'
|
|
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 =
|
|
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)
|
|
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)
|
|
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
|
|
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
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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__(
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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
|
|