bumble 0.0.180__py3-none-any.whl → 0.0.181__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/apps/bench.py +110 -81
- bumble/apps/ble_rpa_tool.py +63 -0
- bumble/apps/console.py +4 -4
- bumble/apps/controller_info.py +34 -2
- bumble/apps/pair.py +6 -8
- bumble/att.py +53 -11
- bumble/controller.py +28 -1
- bumble/crypto.py +10 -0
- bumble/device.py +566 -111
- bumble/drivers/__init__.py +27 -31
- bumble/drivers/common.py +45 -0
- bumble/drivers/rtk.py +11 -4
- bumble/gatt.py +66 -51
- bumble/gatt_server.py +30 -22
- bumble/hci.py +223 -92
- bumble/helpers.py +14 -0
- bumble/hfp.py +37 -27
- bumble/hid.py +282 -61
- bumble/host.py +158 -93
- bumble/l2cap.py +3 -3
- bumble/profiles/asha_service.py +2 -2
- bumble/profiles/bap.py +1247 -0
- bumble/profiles/cap.py +52 -0
- bumble/profiles/csip.py +62 -4
- bumble/rfcomm.py +24 -17
- bumble/smp.py +1 -1
- bumble/transport/__init__.py +49 -21
- bumble/transport/android_emulator.py +1 -1
- bumble/transport/common.py +2 -1
- bumble/transport/hci_socket.py +1 -4
- bumble/transport/usb.py +1 -1
- bumble/utils.py +3 -6
- {bumble-0.0.180.dist-info → bumble-0.0.181.dist-info}/METADATA +1 -1
- {bumble-0.0.180.dist-info → bumble-0.0.181.dist-info}/RECORD +39 -35
- {bumble-0.0.180.dist-info → bumble-0.0.181.dist-info}/entry_points.txt +1 -0
- {bumble-0.0.180.dist-info → bumble-0.0.181.dist-info}/LICENSE +0 -0
- {bumble-0.0.180.dist-info → bumble-0.0.181.dist-info}/WHEEL +0 -0
- {bumble-0.0.180.dist-info → bumble-0.0.181.dist-info}/top_level.txt +0 -0
bumble/_version.py
CHANGED
bumble/apps/bench.py
CHANGED
|
@@ -82,10 +82,11 @@ SPEED_RX_UUID = '016A2CC7-E14B-4819-935F-1F56EAE4098D'
|
|
|
82
82
|
DEFAULT_RFCOMM_UUID = 'E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'
|
|
83
83
|
DEFAULT_L2CAP_PSM = 1234
|
|
84
84
|
DEFAULT_L2CAP_MAX_CREDITS = 128
|
|
85
|
-
DEFAULT_L2CAP_MTU =
|
|
86
|
-
DEFAULT_L2CAP_MPS =
|
|
85
|
+
DEFAULT_L2CAP_MTU = 1024
|
|
86
|
+
DEFAULT_L2CAP_MPS = 1022
|
|
87
87
|
|
|
88
88
|
DEFAULT_LINGER_TIME = 1.0
|
|
89
|
+
DEFAULT_POST_CONNECTION_WAIT_TIME = 1.0
|
|
89
90
|
|
|
90
91
|
DEFAULT_RFCOMM_CHANNEL = 8
|
|
91
92
|
|
|
@@ -95,7 +96,7 @@ DEFAULT_RFCOMM_CHANNEL = 8
|
|
|
95
96
|
# -----------------------------------------------------------------------------
|
|
96
97
|
def parse_packet(packet):
|
|
97
98
|
if len(packet) < 1:
|
|
98
|
-
|
|
99
|
+
logging.info(
|
|
99
100
|
color(f'!!! Packet too short (got {len(packet)} bytes, need >= 1)', 'red')
|
|
100
101
|
)
|
|
101
102
|
raise ValueError('packet too short')
|
|
@@ -103,7 +104,7 @@ def parse_packet(packet):
|
|
|
103
104
|
try:
|
|
104
105
|
packet_type = PacketType(packet[0])
|
|
105
106
|
except ValueError:
|
|
106
|
-
|
|
107
|
+
logging.info(color(f'!!! Invalid packet type 0x{packet[0]:02X}', 'red'))
|
|
107
108
|
raise
|
|
108
109
|
|
|
109
110
|
return (packet_type, packet[1:])
|
|
@@ -111,7 +112,7 @@ def parse_packet(packet):
|
|
|
111
112
|
|
|
112
113
|
def parse_packet_sequence(packet_data):
|
|
113
114
|
if len(packet_data) < 5:
|
|
114
|
-
|
|
115
|
+
logging.info(
|
|
115
116
|
color(
|
|
116
117
|
f'!!!Packet too short (got {len(packet_data)} bytes, need >= 5)',
|
|
117
118
|
'red',
|
|
@@ -155,7 +156,7 @@ def print_connection(connection):
|
|
|
155
156
|
|
|
156
157
|
mtu = connection.att_mtu
|
|
157
158
|
|
|
158
|
-
|
|
159
|
+
logging.info(
|
|
159
160
|
f'{color("@@@ Connection:", "yellow")} '
|
|
160
161
|
f'{connection_parameters} '
|
|
161
162
|
f'{data_length} '
|
|
@@ -266,15 +267,15 @@ class Sender:
|
|
|
266
267
|
pass
|
|
267
268
|
|
|
268
269
|
async def run(self):
|
|
269
|
-
|
|
270
|
+
logging.info(color('--- Waiting for I/O to be ready...', 'blue'))
|
|
270
271
|
await self.packet_io.ready.wait()
|
|
271
|
-
|
|
272
|
+
logging.info(color('--- Go!', 'blue'))
|
|
272
273
|
|
|
273
274
|
if self.tx_start_delay:
|
|
274
|
-
|
|
275
|
+
logging.info(color(f'*** Startup delay: {self.tx_start_delay}', 'blue'))
|
|
275
276
|
await asyncio.sleep(self.tx_start_delay)
|
|
276
277
|
|
|
277
|
-
|
|
278
|
+
logging.info(color('=== Sending RESET', 'magenta'))
|
|
278
279
|
await self.packet_io.send_packet(bytes([PacketType.RESET]))
|
|
279
280
|
self.start_time = time.time()
|
|
280
281
|
for tx_i in range(self.tx_packet_count):
|
|
@@ -285,12 +286,12 @@ class Sender:
|
|
|
285
286
|
packet_flags,
|
|
286
287
|
tx_i,
|
|
287
288
|
) + bytes(self.tx_packet_size - 6)
|
|
288
|
-
|
|
289
|
+
logging.info(color(f'Sending packet {tx_i}: {len(packet)} bytes', 'yellow'))
|
|
289
290
|
self.bytes_sent += len(packet)
|
|
290
291
|
await self.packet_io.send_packet(packet)
|
|
291
292
|
|
|
292
293
|
await self.done.wait()
|
|
293
|
-
|
|
294
|
+
logging.info(color('=== Done!', 'magenta'))
|
|
294
295
|
|
|
295
296
|
def on_packet_received(self, packet):
|
|
296
297
|
try:
|
|
@@ -301,7 +302,7 @@ class Sender:
|
|
|
301
302
|
if packet_type == PacketType.ACK:
|
|
302
303
|
elapsed = time.time() - self.start_time
|
|
303
304
|
average_tx_speed = self.bytes_sent / elapsed
|
|
304
|
-
|
|
305
|
+
logging.info(
|
|
305
306
|
color(
|
|
306
307
|
f'@@@ Received ACK. Speed: average={average_tx_speed:.4f}'
|
|
307
308
|
f' ({self.bytes_sent} bytes in {elapsed:.2f} seconds)',
|
|
@@ -315,6 +316,10 @@ class Sender:
|
|
|
315
316
|
# Receiver
|
|
316
317
|
# -----------------------------------------------------------------------------
|
|
317
318
|
class Receiver:
|
|
319
|
+
expected_packet_index: int
|
|
320
|
+
start_timestamp: float
|
|
321
|
+
last_timestamp: float
|
|
322
|
+
|
|
318
323
|
def __init__(self, packet_io):
|
|
319
324
|
self.reset()
|
|
320
325
|
self.packet_io = packet_io
|
|
@@ -336,7 +341,7 @@ class Receiver:
|
|
|
336
341
|
now = time.time()
|
|
337
342
|
|
|
338
343
|
if packet_type == PacketType.RESET:
|
|
339
|
-
|
|
344
|
+
logging.info(color('=== Received RESET', 'magenta'))
|
|
340
345
|
self.reset()
|
|
341
346
|
self.start_timestamp = now
|
|
342
347
|
return
|
|
@@ -345,13 +350,13 @@ class Receiver:
|
|
|
345
350
|
packet_flags, packet_index = parse_packet_sequence(packet_data)
|
|
346
351
|
except ValueError:
|
|
347
352
|
return
|
|
348
|
-
|
|
353
|
+
logging.info(
|
|
349
354
|
f'<<< Received packet {packet_index}: '
|
|
350
355
|
f'flags=0x{packet_flags:02X}, {len(packet)} bytes'
|
|
351
356
|
)
|
|
352
357
|
|
|
353
358
|
if packet_index != self.expected_packet_index:
|
|
354
|
-
|
|
359
|
+
logging.info(
|
|
355
360
|
color(
|
|
356
361
|
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
|
357
362
|
f'but received {packet_index}'
|
|
@@ -363,7 +368,7 @@ class Receiver:
|
|
|
363
368
|
self.bytes_received += len(packet)
|
|
364
369
|
instant_rx_speed = len(packet) / elapsed_since_last
|
|
365
370
|
average_rx_speed = self.bytes_received / elapsed_since_start
|
|
366
|
-
|
|
371
|
+
logging.info(
|
|
367
372
|
color(
|
|
368
373
|
f'Speed: instant={instant_rx_speed:.4f}, average={average_rx_speed:.4f}',
|
|
369
374
|
'yellow',
|
|
@@ -379,12 +384,12 @@ class Receiver:
|
|
|
379
384
|
struct.pack('>bbI', PacketType.ACK, packet_flags, packet_index)
|
|
380
385
|
)
|
|
381
386
|
)
|
|
382
|
-
|
|
387
|
+
logging.info(color('@@@ Received last packet', 'green'))
|
|
383
388
|
self.done.set()
|
|
384
389
|
|
|
385
390
|
async def run(self):
|
|
386
391
|
await self.done.wait()
|
|
387
|
-
|
|
392
|
+
logging.info(color('=== Done!', 'magenta'))
|
|
388
393
|
|
|
389
394
|
|
|
390
395
|
# -----------------------------------------------------------------------------
|
|
@@ -406,23 +411,23 @@ class Ping:
|
|
|
406
411
|
pass
|
|
407
412
|
|
|
408
413
|
async def run(self):
|
|
409
|
-
|
|
414
|
+
logging.info(color('--- Waiting for I/O to be ready...', 'blue'))
|
|
410
415
|
await self.packet_io.ready.wait()
|
|
411
|
-
|
|
416
|
+
logging.info(color('--- Go!', 'blue'))
|
|
412
417
|
|
|
413
418
|
if self.tx_start_delay:
|
|
414
|
-
|
|
419
|
+
logging.info(color(f'*** Startup delay: {self.tx_start_delay}', 'blue'))
|
|
415
420
|
await asyncio.sleep(self.tx_start_delay)
|
|
416
421
|
|
|
417
|
-
|
|
422
|
+
logging.info(color('=== Sending RESET', 'magenta'))
|
|
418
423
|
await self.packet_io.send_packet(bytes([PacketType.RESET]))
|
|
419
424
|
|
|
420
425
|
await self.send_next_ping()
|
|
421
426
|
|
|
422
427
|
await self.done.wait()
|
|
423
428
|
average_latency = sum(self.latencies) / len(self.latencies)
|
|
424
|
-
|
|
425
|
-
|
|
429
|
+
logging.info(color(f'@@@ Average latency: {average_latency:.2f}'))
|
|
430
|
+
logging.info(color('=== Done!', 'magenta'))
|
|
426
431
|
|
|
427
432
|
async def send_next_ping(self):
|
|
428
433
|
packet = struct.pack(
|
|
@@ -433,7 +438,7 @@ class Ping:
|
|
|
433
438
|
else 0,
|
|
434
439
|
self.current_packet_index,
|
|
435
440
|
) + bytes(self.tx_packet_size - 6)
|
|
436
|
-
|
|
441
|
+
logging.info(color(f'Sending packet {self.current_packet_index}', 'yellow'))
|
|
437
442
|
self.ping_sent_time = time.time()
|
|
438
443
|
await self.packet_io.send_packet(packet)
|
|
439
444
|
|
|
@@ -453,7 +458,7 @@ class Ping:
|
|
|
453
458
|
if packet_type == PacketType.ACK:
|
|
454
459
|
latency = elapsed * 1000
|
|
455
460
|
self.latencies.append(latency)
|
|
456
|
-
|
|
461
|
+
logging.info(
|
|
457
462
|
color(
|
|
458
463
|
f'<<< Received ACK [{packet_index}], latency={latency:.2f}ms',
|
|
459
464
|
'green',
|
|
@@ -463,7 +468,7 @@ class Ping:
|
|
|
463
468
|
if packet_index == self.current_packet_index:
|
|
464
469
|
self.current_packet_index += 1
|
|
465
470
|
else:
|
|
466
|
-
|
|
471
|
+
logging.info(
|
|
467
472
|
color(
|
|
468
473
|
f'!!! Unexpected packet, expected {self.current_packet_index} '
|
|
469
474
|
f'but received {packet_index}'
|
|
@@ -481,6 +486,8 @@ class Ping:
|
|
|
481
486
|
# Pong
|
|
482
487
|
# -----------------------------------------------------------------------------
|
|
483
488
|
class Pong:
|
|
489
|
+
expected_packet_index: int
|
|
490
|
+
|
|
484
491
|
def __init__(self, packet_io):
|
|
485
492
|
self.reset()
|
|
486
493
|
self.packet_io = packet_io
|
|
@@ -497,7 +504,7 @@ class Pong:
|
|
|
497
504
|
return
|
|
498
505
|
|
|
499
506
|
if packet_type == PacketType.RESET:
|
|
500
|
-
|
|
507
|
+
logging.info(color('=== Received RESET', 'magenta'))
|
|
501
508
|
self.reset()
|
|
502
509
|
return
|
|
503
510
|
|
|
@@ -505,7 +512,7 @@ class Pong:
|
|
|
505
512
|
packet_flags, packet_index = parse_packet_sequence(packet_data)
|
|
506
513
|
except ValueError:
|
|
507
514
|
return
|
|
508
|
-
|
|
515
|
+
logging.info(
|
|
509
516
|
color(
|
|
510
517
|
f'<<< Received packet {packet_index}: '
|
|
511
518
|
f'flags=0x{packet_flags:02X}, {len(packet)} bytes',
|
|
@@ -514,7 +521,7 @@ class Pong:
|
|
|
514
521
|
)
|
|
515
522
|
|
|
516
523
|
if packet_index != self.expected_packet_index:
|
|
517
|
-
|
|
524
|
+
logging.info(
|
|
518
525
|
color(
|
|
519
526
|
f'!!! Unexpected packet, expected {self.expected_packet_index} '
|
|
520
527
|
f'but received {packet_index}'
|
|
@@ -534,7 +541,7 @@ class Pong:
|
|
|
534
541
|
|
|
535
542
|
async def run(self):
|
|
536
543
|
await self.done.wait()
|
|
537
|
-
|
|
544
|
+
logging.info(color('=== Done!', 'magenta'))
|
|
538
545
|
|
|
539
546
|
|
|
540
547
|
# -----------------------------------------------------------------------------
|
|
@@ -552,36 +559,36 @@ class GattClient:
|
|
|
552
559
|
peer = Peer(connection)
|
|
553
560
|
|
|
554
561
|
if self.att_mtu:
|
|
555
|
-
|
|
562
|
+
logging.info(color(f'*** Requesting MTU update: {self.att_mtu}', 'blue'))
|
|
556
563
|
await peer.request_mtu(self.att_mtu)
|
|
557
564
|
|
|
558
|
-
|
|
565
|
+
logging.info(color('*** Discovering services...', 'blue'))
|
|
559
566
|
await peer.discover_services()
|
|
560
567
|
|
|
561
568
|
speed_services = peer.get_services_by_uuid(SPEED_SERVICE_UUID)
|
|
562
569
|
if not speed_services:
|
|
563
|
-
|
|
570
|
+
logging.info(color('!!! Speed Service not found', 'red'))
|
|
564
571
|
return
|
|
565
572
|
speed_service = speed_services[0]
|
|
566
|
-
|
|
573
|
+
logging.info(color('*** Discovering characteristics...', 'blue'))
|
|
567
574
|
await speed_service.discover_characteristics()
|
|
568
575
|
|
|
569
576
|
speed_txs = speed_service.get_characteristics_by_uuid(SPEED_TX_UUID)
|
|
570
577
|
if not speed_txs:
|
|
571
|
-
|
|
578
|
+
logging.info(color('!!! Speed TX not found', 'red'))
|
|
572
579
|
return
|
|
573
580
|
self.speed_tx = speed_txs[0]
|
|
574
581
|
|
|
575
582
|
speed_rxs = speed_service.get_characteristics_by_uuid(SPEED_RX_UUID)
|
|
576
583
|
if not speed_rxs:
|
|
577
|
-
|
|
584
|
+
logging.info(color('!!! Speed RX not found', 'red'))
|
|
578
585
|
return
|
|
579
586
|
self.speed_rx = speed_rxs[0]
|
|
580
587
|
|
|
581
|
-
|
|
588
|
+
logging.info(color('*** Subscribing to RX', 'blue'))
|
|
582
589
|
await self.speed_rx.subscribe(self.on_packet_received)
|
|
583
590
|
|
|
584
|
-
|
|
591
|
+
logging.info(color('*** Discovery complete', 'blue'))
|
|
585
592
|
|
|
586
593
|
connection.on('disconnection', self.on_disconnection)
|
|
587
594
|
self.ready.set()
|
|
@@ -633,10 +640,10 @@ class GattServer:
|
|
|
633
640
|
|
|
634
641
|
def on_rx_subscription(self, _connection, notify_enabled, _indicate_enabled):
|
|
635
642
|
if notify_enabled:
|
|
636
|
-
|
|
643
|
+
logging.info(color('*** RX subscription', 'blue'))
|
|
637
644
|
self.ready.set()
|
|
638
645
|
else:
|
|
639
|
-
|
|
646
|
+
logging.info(color('*** RX un-subscription', 'blue'))
|
|
640
647
|
self.ready.clear()
|
|
641
648
|
|
|
642
649
|
def on_tx_write(self, _, value):
|
|
@@ -684,7 +691,7 @@ class StreamedPacketIO:
|
|
|
684
691
|
|
|
685
692
|
async def send_packet(self, packet):
|
|
686
693
|
if not self.io_sink:
|
|
687
|
-
|
|
694
|
+
logging.info(color('!!! No sink, dropping packet', 'red'))
|
|
688
695
|
return
|
|
689
696
|
|
|
690
697
|
# pylint: disable-next=not-callable
|
|
@@ -714,7 +721,7 @@ class L2capClient(StreamedPacketIO):
|
|
|
714
721
|
connection.on('disconnection', self.on_disconnection)
|
|
715
722
|
|
|
716
723
|
# Connect a new L2CAP channel
|
|
717
|
-
|
|
724
|
+
logging.info(color(f'>>> Opening L2CAP channel on PSM = {self.psm}', 'yellow'))
|
|
718
725
|
try:
|
|
719
726
|
l2cap_channel = await connection.create_l2cap_channel(
|
|
720
727
|
spec=l2cap.LeCreditBasedChannelSpec(
|
|
@@ -724,9 +731,9 @@ class L2capClient(StreamedPacketIO):
|
|
|
724
731
|
mps=self.mps,
|
|
725
732
|
)
|
|
726
733
|
)
|
|
727
|
-
|
|
734
|
+
logging.info(color(f'*** L2CAP channel: {l2cap_channel}', 'cyan'))
|
|
728
735
|
except Exception as error:
|
|
729
|
-
|
|
736
|
+
logging.info(color(f'!!! Connection failed: {error}', 'red'))
|
|
730
737
|
return
|
|
731
738
|
|
|
732
739
|
l2cap_channel.sink = self.on_packet
|
|
@@ -739,7 +746,7 @@ class L2capClient(StreamedPacketIO):
|
|
|
739
746
|
pass
|
|
740
747
|
|
|
741
748
|
def on_l2cap_close(self):
|
|
742
|
-
|
|
749
|
+
logging.info(color('*** L2CAP channel closed', 'red'))
|
|
743
750
|
|
|
744
751
|
|
|
745
752
|
# -----------------------------------------------------------------------------
|
|
@@ -765,7 +772,9 @@ class L2capServer(StreamedPacketIO):
|
|
|
765
772
|
),
|
|
766
773
|
handler=self.on_l2cap_channel,
|
|
767
774
|
)
|
|
768
|
-
|
|
775
|
+
logging.info(
|
|
776
|
+
color(f'### Listening for L2CAP connection on PSM {psm}', 'yellow')
|
|
777
|
+
)
|
|
769
778
|
|
|
770
779
|
async def on_connection(self, connection):
|
|
771
780
|
connection.on('disconnection', self.on_disconnection)
|
|
@@ -774,7 +783,7 @@ class L2capServer(StreamedPacketIO):
|
|
|
774
783
|
pass
|
|
775
784
|
|
|
776
785
|
def on_l2cap_channel(self, l2cap_channel):
|
|
777
|
-
|
|
786
|
+
logging.info(color(f'*** L2CAP channel: {l2cap_channel}', 'cyan'))
|
|
778
787
|
|
|
779
788
|
self.io_sink = l2cap_channel.write
|
|
780
789
|
l2cap_channel.on('close', self.on_l2cap_close)
|
|
@@ -783,7 +792,7 @@ class L2capServer(StreamedPacketIO):
|
|
|
783
792
|
self.ready.set()
|
|
784
793
|
|
|
785
794
|
def on_l2cap_close(self):
|
|
786
|
-
|
|
795
|
+
logging.info(color('*** L2CAP channel closed', 'red'))
|
|
787
796
|
self.l2cap_channel = None
|
|
788
797
|
|
|
789
798
|
|
|
@@ -804,28 +813,28 @@ class RfcommClient(StreamedPacketIO):
|
|
|
804
813
|
# Find the channel number if not specified
|
|
805
814
|
channel = self.channel
|
|
806
815
|
if channel == 0:
|
|
807
|
-
|
|
816
|
+
logging.info(
|
|
808
817
|
color(f'@@@ Discovering channel number from UUID {self.uuid}', 'cyan')
|
|
809
818
|
)
|
|
810
819
|
channel = await find_rfcomm_channel_with_uuid(connection, self.uuid)
|
|
811
|
-
|
|
820
|
+
logging.info(color(f'@@@ Channel number = {channel}', 'cyan'))
|
|
812
821
|
if channel == 0:
|
|
813
|
-
|
|
822
|
+
logging.info(color('!!! No RFComm service with this UUID found', 'red'))
|
|
814
823
|
await connection.disconnect()
|
|
815
824
|
return
|
|
816
825
|
|
|
817
826
|
# Create a client and start it
|
|
818
|
-
|
|
827
|
+
logging.info(color('*** Starting RFCOMM client...', 'blue'))
|
|
819
828
|
rfcomm_client = bumble.rfcomm.Client(connection)
|
|
820
829
|
rfcomm_mux = await rfcomm_client.start()
|
|
821
|
-
|
|
830
|
+
logging.info(color('*** Started', 'blue'))
|
|
822
831
|
|
|
823
|
-
|
|
832
|
+
logging.info(color(f'### Opening session for channel {channel}...', 'yellow'))
|
|
824
833
|
try:
|
|
825
834
|
rfcomm_session = await rfcomm_mux.open_dlc(channel)
|
|
826
|
-
|
|
835
|
+
logging.info(color(f'### Session open: {rfcomm_session}', 'yellow'))
|
|
827
836
|
except bumble.core.ConnectionError as error:
|
|
828
|
-
|
|
837
|
+
logging.info(color(f'!!! Session open failed: {error}', 'red'))
|
|
829
838
|
await rfcomm_mux.disconnect()
|
|
830
839
|
return
|
|
831
840
|
|
|
@@ -855,7 +864,7 @@ class RfcommServer(StreamedPacketIO):
|
|
|
855
864
|
# Setup the SDP to advertise this channel
|
|
856
865
|
device.sdp_service_records = make_sdp_records(channel_number)
|
|
857
866
|
|
|
858
|
-
|
|
867
|
+
logging.info(
|
|
859
868
|
color(
|
|
860
869
|
f'### Listening for RFComm connection on channel {channel_number}',
|
|
861
870
|
'yellow',
|
|
@@ -869,7 +878,7 @@ class RfcommServer(StreamedPacketIO):
|
|
|
869
878
|
pass
|
|
870
879
|
|
|
871
880
|
def on_dlc(self, dlc):
|
|
872
|
-
|
|
881
|
+
logging.info(color(f'*** DLC connected: {dlc}', 'blue'))
|
|
873
882
|
dlc.sink = self.on_packet
|
|
874
883
|
self.io_sink = dlc.write
|
|
875
884
|
|
|
@@ -935,12 +944,12 @@ class Central(Connection.Listener):
|
|
|
935
944
|
self.connection_parameter_preferences = None
|
|
936
945
|
|
|
937
946
|
async def run(self):
|
|
938
|
-
|
|
947
|
+
logging.info(color('>>> Connecting to HCI...', 'green'))
|
|
939
948
|
async with await open_transport_or_link(self.transport) as (
|
|
940
949
|
hci_source,
|
|
941
950
|
hci_sink,
|
|
942
951
|
):
|
|
943
|
-
|
|
952
|
+
logging.info(color('>>> Connected', 'green'))
|
|
944
953
|
|
|
945
954
|
central_address = DEFAULT_CENTRAL_ADDRESS
|
|
946
955
|
self.device = Device.with_hci(
|
|
@@ -952,7 +961,13 @@ class Central(Connection.Listener):
|
|
|
952
961
|
|
|
953
962
|
await self.device.power_on()
|
|
954
963
|
|
|
955
|
-
|
|
964
|
+
if self.classic:
|
|
965
|
+
await self.device.set_discoverable(False)
|
|
966
|
+
await self.device.set_connectable(False)
|
|
967
|
+
|
|
968
|
+
logging.info(
|
|
969
|
+
color(f'### Connecting to {self.peripheral_address}...', 'cyan')
|
|
970
|
+
)
|
|
956
971
|
try:
|
|
957
972
|
self.connection = await self.device.connect(
|
|
958
973
|
self.peripheral_address,
|
|
@@ -960,21 +975,26 @@ class Central(Connection.Listener):
|
|
|
960
975
|
transport=BT_BR_EDR_TRANSPORT if self.classic else BT_LE_TRANSPORT,
|
|
961
976
|
)
|
|
962
977
|
except CommandTimeoutError:
|
|
963
|
-
|
|
978
|
+
logging.info(color('!!! Connection timed out', 'red'))
|
|
964
979
|
return
|
|
965
980
|
except bumble.core.ConnectionError as error:
|
|
966
|
-
|
|
981
|
+
logging.info(color(f'!!! Connection error: {error}', 'red'))
|
|
967
982
|
return
|
|
968
983
|
except HCI_StatusError as error:
|
|
969
|
-
|
|
984
|
+
logging.info(color(f'!!! Connection failed: {error.error_name}'))
|
|
970
985
|
return
|
|
971
|
-
|
|
986
|
+
logging.info(color('### Connected', 'cyan'))
|
|
972
987
|
self.connection.listener = self
|
|
973
988
|
print_connection(self.connection)
|
|
974
989
|
|
|
990
|
+
# Wait a bit after the connection, some controllers aren't very good when
|
|
991
|
+
# we start sending data right away while some connection parameters are
|
|
992
|
+
# updated post connection
|
|
993
|
+
await asyncio.sleep(DEFAULT_POST_CONNECTION_WAIT_TIME)
|
|
994
|
+
|
|
975
995
|
# Request a new data length if requested
|
|
976
996
|
if self.extended_data_length:
|
|
977
|
-
|
|
997
|
+
logging.info(color('+++ Requesting extended data length', 'cyan'))
|
|
978
998
|
await self.connection.set_data_length(
|
|
979
999
|
self.extended_data_length[0], self.extended_data_length[1]
|
|
980
1000
|
)
|
|
@@ -982,16 +1002,16 @@ class Central(Connection.Listener):
|
|
|
982
1002
|
# Authenticate if requested
|
|
983
1003
|
if self.authenticate:
|
|
984
1004
|
# Request authentication
|
|
985
|
-
|
|
1005
|
+
logging.info(color('*** Authenticating...', 'cyan'))
|
|
986
1006
|
await self.connection.authenticate()
|
|
987
|
-
|
|
1007
|
+
logging.info(color('*** Authenticated', 'cyan'))
|
|
988
1008
|
|
|
989
1009
|
# Encrypt if requested
|
|
990
1010
|
if self.encrypt:
|
|
991
1011
|
# Enable encryption
|
|
992
|
-
|
|
1012
|
+
logging.info(color('*** Enabling encryption...', 'cyan'))
|
|
993
1013
|
await self.connection.encrypt()
|
|
994
|
-
|
|
1014
|
+
logging.info(color('*** Encryption on', 'cyan'))
|
|
995
1015
|
|
|
996
1016
|
# Set the PHY if requested
|
|
997
1017
|
if self.phy is not None:
|
|
@@ -1000,7 +1020,7 @@ class Central(Connection.Listener):
|
|
|
1000
1020
|
tx_phys=[self.phy], rx_phys=[self.phy]
|
|
1001
1021
|
)
|
|
1002
1022
|
except HCI_Error as error:
|
|
1003
|
-
|
|
1023
|
+
logging.info(
|
|
1004
1024
|
color(
|
|
1005
1025
|
f'!!! Unable to set the PHY: {error.error_name}', 'yellow'
|
|
1006
1026
|
)
|
|
@@ -1012,7 +1032,7 @@ class Central(Connection.Listener):
|
|
|
1012
1032
|
await asyncio.sleep(DEFAULT_LINGER_TIME)
|
|
1013
1033
|
|
|
1014
1034
|
def on_disconnection(self, reason):
|
|
1015
|
-
|
|
1035
|
+
logging.info(color(f'!!! Disconnection: reason={reason}', 'red'))
|
|
1016
1036
|
self.connection = None
|
|
1017
1037
|
|
|
1018
1038
|
def on_connection_parameters_update(self):
|
|
@@ -1047,12 +1067,12 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|
|
1047
1067
|
self.connected = asyncio.Event()
|
|
1048
1068
|
|
|
1049
1069
|
async def run(self):
|
|
1050
|
-
|
|
1070
|
+
logging.info(color('>>> Connecting to HCI...', 'green'))
|
|
1051
1071
|
async with await open_transport_or_link(self.transport) as (
|
|
1052
1072
|
hci_source,
|
|
1053
1073
|
hci_sink,
|
|
1054
1074
|
):
|
|
1055
|
-
|
|
1075
|
+
logging.info(color('>>> Connected', 'green'))
|
|
1056
1076
|
|
|
1057
1077
|
peripheral_address = DEFAULT_PERIPHERAL_ADDRESS
|
|
1058
1078
|
self.device = Device.with_hci(
|
|
@@ -1072,7 +1092,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|
|
1072
1092
|
await self.device.start_advertising(auto_restart=True)
|
|
1073
1093
|
|
|
1074
1094
|
if self.classic:
|
|
1075
|
-
|
|
1095
|
+
logging.info(
|
|
1076
1096
|
color(
|
|
1077
1097
|
'### Waiting for connection on'
|
|
1078
1098
|
f' {self.device.public_address}...',
|
|
@@ -1080,14 +1100,14 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|
|
1080
1100
|
)
|
|
1081
1101
|
)
|
|
1082
1102
|
else:
|
|
1083
|
-
|
|
1103
|
+
logging.info(
|
|
1084
1104
|
color(
|
|
1085
1105
|
f'### Waiting for connection on {peripheral_address}...',
|
|
1086
1106
|
'cyan',
|
|
1087
1107
|
)
|
|
1088
1108
|
)
|
|
1089
1109
|
await self.connected.wait()
|
|
1090
|
-
|
|
1110
|
+
logging.info(color('### Connected', 'cyan'))
|
|
1091
1111
|
|
|
1092
1112
|
await self.mode.on_connection(self.connection)
|
|
1093
1113
|
await self.role.run()
|
|
@@ -1098,9 +1118,18 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|
|
1098
1118
|
self.connection = connection
|
|
1099
1119
|
self.connected.set()
|
|
1100
1120
|
|
|
1121
|
+
# Stop being discoverable and connectable
|
|
1122
|
+
if self.classic:
|
|
1123
|
+
|
|
1124
|
+
async def stop_being_discoverable_connectable():
|
|
1125
|
+
await self.device.set_discoverable(False)
|
|
1126
|
+
await self.device.set_connectable(False)
|
|
1127
|
+
|
|
1128
|
+
AsyncRunner.spawn(stop_being_discoverable_connectable())
|
|
1129
|
+
|
|
1101
1130
|
# Request a new data length if needed
|
|
1102
1131
|
if self.extended_data_length:
|
|
1103
|
-
|
|
1132
|
+
logging.info("+++ Requesting extended data length")
|
|
1104
1133
|
AsyncRunner.spawn(
|
|
1105
1134
|
connection.set_data_length(
|
|
1106
1135
|
self.extended_data_length[0], self.extended_data_length[1]
|
|
@@ -1108,7 +1137,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|
|
1108
1137
|
)
|
|
1109
1138
|
|
|
1110
1139
|
def on_disconnection(self, reason):
|
|
1111
|
-
|
|
1140
|
+
logging.info(color(f'!!! Disconnection: reason={reason}', 'red'))
|
|
1112
1141
|
self.connection = None
|
|
1113
1142
|
self.role.reset()
|
|
1114
1143
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Copyright 2023 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
from bumble.colors import color
|
|
17
|
+
from bumble.hci import Address
|
|
18
|
+
from bumble.helpers import generate_irk, verify_rpa_with_irk
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.group()
|
|
22
|
+
def cli():
|
|
23
|
+
'''
|
|
24
|
+
This is a tool for generating IRK, RPA,
|
|
25
|
+
and verifying IRK/RPA pairs
|
|
26
|
+
'''
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@click.command()
|
|
30
|
+
def gen_irk() -> None:
|
|
31
|
+
print(generate_irk().hex())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@click.command()
|
|
35
|
+
@click.argument("irk", type=str)
|
|
36
|
+
def gen_rpa(irk: str) -> None:
|
|
37
|
+
irk_bytes = bytes.fromhex(irk)
|
|
38
|
+
rpa = Address.generate_private_address(irk_bytes)
|
|
39
|
+
print(rpa.to_string(with_type_qualifier=False))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@click.command()
|
|
43
|
+
@click.argument("irk", type=str)
|
|
44
|
+
@click.argument("rpa", type=str)
|
|
45
|
+
def verify_rpa(irk: str, rpa: str) -> None:
|
|
46
|
+
address = Address(rpa)
|
|
47
|
+
irk_bytes = bytes.fromhex(irk)
|
|
48
|
+
if verify_rpa_with_irk(address, irk_bytes):
|
|
49
|
+
print(color("Verified", "green"))
|
|
50
|
+
else:
|
|
51
|
+
print(color("Not Verified", "red"))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main():
|
|
55
|
+
cli.add_command(gen_irk)
|
|
56
|
+
cli.add_command(gen_rpa)
|
|
57
|
+
cli.add_command(verify_rpa)
|
|
58
|
+
cli()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# -----------------------------------------------------------------------------
|
|
62
|
+
if __name__ == '__main__':
|
|
63
|
+
main()
|