bumble 0.0.212__py3-none-any.whl → 0.0.213__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 +6 -0
- bumble/apps/README.md +0 -3
- bumble/apps/auracast.py +11 -9
- bumble/apps/bench.py +480 -31
- bumble/apps/console.py +3 -3
- bumble/apps/controller_info.py +47 -10
- bumble/apps/controller_loopback.py +7 -3
- bumble/apps/controllers.py +2 -2
- bumble/apps/device_info.py +2 -2
- bumble/apps/gatt_dump.py +2 -2
- bumble/apps/gg_bridge.py +2 -2
- bumble/apps/hci_bridge.py +2 -2
- bumble/apps/l2cap_bridge.py +2 -2
- bumble/apps/lea_unicast/app.py +6 -1
- bumble/apps/pair.py +19 -11
- bumble/apps/pandora_server.py +2 -2
- bumble/apps/rfcomm_bridge.py +1 -1
- bumble/apps/scan.py +2 -2
- bumble/apps/show.py +4 -2
- bumble/apps/speaker/speaker.html +1 -0
- bumble/apps/speaker/speaker.js +113 -62
- bumble/apps/speaker/speaker.py +126 -18
- bumble/at.py +4 -4
- bumble/att.py +2 -6
- bumble/avc.py +7 -7
- bumble/avctp.py +3 -3
- bumble/avdtp.py +16 -20
- bumble/avrcp.py +41 -53
- bumble/colors.py +2 -2
- bumble/controller.py +84 -23
- bumble/device.py +348 -182
- bumble/drivers/__init__.py +2 -2
- bumble/drivers/common.py +0 -2
- bumble/drivers/intel.py +37 -40
- bumble/drivers/rtk.py +28 -35
- bumble/gatt.py +4 -4
- bumble/gatt_adapters.py +4 -5
- bumble/gatt_client.py +26 -31
- bumble/gatt_server.py +7 -11
- bumble/hci.py +2601 -2909
- bumble/helpers.py +4 -5
- bumble/hfp.py +32 -37
- bumble/host.py +94 -35
- bumble/keys.py +5 -5
- bumble/l2cap.py +310 -394
- bumble/link.py +6 -270
- bumble/pairing.py +23 -20
- bumble/pandora/__init__.py +1 -1
- bumble/pandora/config.py +2 -2
- bumble/pandora/device.py +6 -6
- bumble/pandora/host.py +27 -28
- bumble/pandora/l2cap.py +2 -2
- bumble/pandora/security.py +6 -6
- bumble/pandora/utils.py +3 -3
- bumble/profiles/ascs.py +132 -131
- bumble/profiles/asha.py +2 -2
- bumble/profiles/bap.py +3 -4
- bumble/profiles/csip.py +2 -2
- bumble/profiles/device_information_service.py +2 -2
- bumble/profiles/gap.py +2 -2
- bumble/profiles/hap.py +34 -33
- bumble/profiles/le_audio.py +4 -4
- bumble/profiles/mcp.py +4 -4
- bumble/profiles/vcs.py +3 -5
- bumble/rfcomm.py +10 -10
- bumble/rtp.py +1 -2
- bumble/sdp.py +2 -2
- bumble/smp.py +57 -61
- bumble/tools/rtk_util.py +2 -2
- bumble/transport/__init__.py +2 -16
- bumble/transport/android_netsim.py +5 -5
- bumble/transport/common.py +4 -4
- bumble/transport/pyusb.py +2 -2
- bumble/utils.py +2 -5
- bumble/vendor/android/hci.py +118 -200
- bumble/vendor/zephyr/hci.py +32 -27
- {bumble-0.0.212.dist-info → bumble-0.0.213.dist-info}/METADATA +2 -2
- {bumble-0.0.212.dist-info → bumble-0.0.213.dist-info}/RECORD +83 -86
- {bumble-0.0.212.dist-info → bumble-0.0.213.dist-info}/WHEEL +1 -1
- {bumble-0.0.212.dist-info → bumble-0.0.213.dist-info}/entry_points.txt +0 -1
- bumble/apps/link_relay/__init__.py +0 -0
- bumble/apps/link_relay/link_relay.py +0 -289
- bumble/apps/link_relay/logging.yml +0 -21
- {bumble-0.0.212.dist-info → bumble-0.0.213.dist-info}/licenses/LICENSE +0 -0
- {bumble-0.0.212.dist-info → bumble-0.0.213.dist-info}/top_level.txt +0 -0
bumble/link.py
CHANGED
|
@@ -17,26 +17,20 @@
|
|
|
17
17
|
# -----------------------------------------------------------------------------
|
|
18
18
|
import logging
|
|
19
19
|
import asyncio
|
|
20
|
-
from functools import partial
|
|
21
20
|
|
|
22
|
-
from bumble
|
|
23
|
-
PhysicalTransport,
|
|
24
|
-
InvalidStateError,
|
|
25
|
-
)
|
|
26
|
-
from bumble.colors import color
|
|
21
|
+
from bumble import core
|
|
27
22
|
from bumble.hci import (
|
|
28
23
|
Address,
|
|
29
24
|
Role,
|
|
30
25
|
HCI_SUCCESS,
|
|
31
26
|
HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR,
|
|
32
|
-
HCI_CONNECTION_TIMEOUT_ERROR,
|
|
33
27
|
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
|
34
28
|
HCI_PAGE_TIMEOUT_ERROR,
|
|
35
29
|
HCI_Connection_Complete_Event,
|
|
36
30
|
)
|
|
37
31
|
from bumble import controller
|
|
38
32
|
|
|
39
|
-
from typing import Optional
|
|
33
|
+
from typing import Optional
|
|
40
34
|
|
|
41
35
|
# -----------------------------------------------------------------------------
|
|
42
36
|
# Logging
|
|
@@ -65,7 +59,7 @@ class LocalLink:
|
|
|
65
59
|
Link bus for controllers to communicate with each other
|
|
66
60
|
'''
|
|
67
61
|
|
|
68
|
-
controllers:
|
|
62
|
+
controllers: set[controller.Controller]
|
|
69
63
|
|
|
70
64
|
def __init__(self):
|
|
71
65
|
self.controllers = set()
|
|
@@ -115,10 +109,10 @@ class LocalLink:
|
|
|
115
109
|
|
|
116
110
|
def send_acl_data(self, sender_controller, destination_address, transport, data):
|
|
117
111
|
# Send the data to the first controller with a matching address
|
|
118
|
-
if transport == PhysicalTransport.LE:
|
|
112
|
+
if transport == core.PhysicalTransport.LE:
|
|
119
113
|
destination_controller = self.find_controller(destination_address)
|
|
120
114
|
source_address = sender_controller.random_address
|
|
121
|
-
elif transport == PhysicalTransport.BR_EDR:
|
|
115
|
+
elif transport == core.PhysicalTransport.BR_EDR:
|
|
122
116
|
destination_controller = self.find_classic_controller(destination_address)
|
|
123
117
|
source_address = sender_controller.public_address
|
|
124
118
|
else:
|
|
@@ -274,7 +268,7 @@ class LocalLink:
|
|
|
274
268
|
|
|
275
269
|
responder_controller.on_classic_connection_request(
|
|
276
270
|
initiator_controller.public_address,
|
|
277
|
-
HCI_Connection_Complete_Event.
|
|
271
|
+
HCI_Connection_Complete_Event.LinkType.ACL,
|
|
278
272
|
)
|
|
279
273
|
|
|
280
274
|
def classic_accept_connection(
|
|
@@ -384,261 +378,3 @@ class LocalLink:
|
|
|
384
378
|
responder_controller.on_classic_sco_connection_complete(
|
|
385
379
|
initiator_controller.public_address, HCI_SUCCESS, link_type
|
|
386
380
|
)
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
# -----------------------------------------------------------------------------
|
|
390
|
-
class RemoteLink:
|
|
391
|
-
'''
|
|
392
|
-
A Link implementation that communicates with other virtual controllers via a
|
|
393
|
-
WebSocket relay
|
|
394
|
-
'''
|
|
395
|
-
|
|
396
|
-
def __init__(self, uri):
|
|
397
|
-
self.controller = None
|
|
398
|
-
self.uri = uri
|
|
399
|
-
self.execution_queue = asyncio.Queue()
|
|
400
|
-
self.websocket = asyncio.get_running_loop().create_future()
|
|
401
|
-
self.rpc_result = None
|
|
402
|
-
self.pending_connection = None
|
|
403
|
-
self.central_connections = set() # List of addresses that we have connected to
|
|
404
|
-
self.peripheral_connections = (
|
|
405
|
-
set()
|
|
406
|
-
) # List of addresses that have connected to us
|
|
407
|
-
|
|
408
|
-
# Connect and run asynchronously
|
|
409
|
-
asyncio.create_task(self.run_connection())
|
|
410
|
-
asyncio.create_task(self.run_executor_loop())
|
|
411
|
-
|
|
412
|
-
def add_controller(self, controller):
|
|
413
|
-
if self.controller:
|
|
414
|
-
raise InvalidStateError('controller already set')
|
|
415
|
-
self.controller = controller
|
|
416
|
-
|
|
417
|
-
def remove_controller(self, controller):
|
|
418
|
-
if self.controller != controller:
|
|
419
|
-
raise InvalidStateError('controller mismatch')
|
|
420
|
-
self.controller = None
|
|
421
|
-
|
|
422
|
-
def get_pending_connection(self):
|
|
423
|
-
return self.pending_connection
|
|
424
|
-
|
|
425
|
-
def get_pending_classic_connection(self):
|
|
426
|
-
return self.pending_classic_connection
|
|
427
|
-
|
|
428
|
-
async def wait_until_connected(self):
|
|
429
|
-
await self.websocket
|
|
430
|
-
|
|
431
|
-
def execute(self, async_function):
|
|
432
|
-
self.execution_queue.put_nowait(async_function())
|
|
433
|
-
|
|
434
|
-
async def run_executor_loop(self):
|
|
435
|
-
logger.debug('executor loop starting')
|
|
436
|
-
while True:
|
|
437
|
-
item = await self.execution_queue.get()
|
|
438
|
-
try:
|
|
439
|
-
await item
|
|
440
|
-
except Exception as error:
|
|
441
|
-
logger.warning(
|
|
442
|
-
f'{color("!!! Exception in async handler:", "red")} {error}'
|
|
443
|
-
)
|
|
444
|
-
|
|
445
|
-
async def run_connection(self):
|
|
446
|
-
import websockets # lazy import
|
|
447
|
-
|
|
448
|
-
# Connect to the relay
|
|
449
|
-
logger.debug(f'connecting to {self.uri}')
|
|
450
|
-
# pylint: disable-next=no-member
|
|
451
|
-
websocket = await websockets.connect(self.uri)
|
|
452
|
-
self.websocket.set_result(websocket)
|
|
453
|
-
logger.debug(f'connected to {self.uri}')
|
|
454
|
-
|
|
455
|
-
while True:
|
|
456
|
-
message = await websocket.recv()
|
|
457
|
-
logger.debug(f'received message: {message}')
|
|
458
|
-
keyword, *payload = message.split(':', 1)
|
|
459
|
-
|
|
460
|
-
handler_name = f'on_{keyword}_received'
|
|
461
|
-
handler = getattr(self, handler_name, None)
|
|
462
|
-
if handler:
|
|
463
|
-
await handler(payload[0] if payload else None)
|
|
464
|
-
|
|
465
|
-
def close(self):
|
|
466
|
-
if self.websocket.done():
|
|
467
|
-
logger.debug('closing websocket')
|
|
468
|
-
websocket = self.websocket.result()
|
|
469
|
-
asyncio.create_task(websocket.close())
|
|
470
|
-
|
|
471
|
-
async def on_result_received(self, result):
|
|
472
|
-
if self.rpc_result:
|
|
473
|
-
self.rpc_result.set_result(result)
|
|
474
|
-
|
|
475
|
-
async def on_left_received(self, address):
|
|
476
|
-
if address in self.central_connections:
|
|
477
|
-
self.controller.on_link_peripheral_disconnected(Address(address))
|
|
478
|
-
self.central_connections.remove(address)
|
|
479
|
-
|
|
480
|
-
if address in self.peripheral_connections:
|
|
481
|
-
self.controller.on_link_central_disconnected(
|
|
482
|
-
address, HCI_CONNECTION_TIMEOUT_ERROR
|
|
483
|
-
)
|
|
484
|
-
self.peripheral_connections.remove(address)
|
|
485
|
-
|
|
486
|
-
async def on_unreachable_received(self, target):
|
|
487
|
-
await self.on_left_received(target)
|
|
488
|
-
|
|
489
|
-
async def on_message_received(self, message):
|
|
490
|
-
sender, *payload = message.split('/', 1)
|
|
491
|
-
if payload:
|
|
492
|
-
keyword, *payload = payload[0].split(':', 1)
|
|
493
|
-
handler_name = f'on_{keyword}_message_received'
|
|
494
|
-
handler = getattr(self, handler_name, None)
|
|
495
|
-
if handler:
|
|
496
|
-
await handler(sender, payload[0] if payload else None)
|
|
497
|
-
|
|
498
|
-
async def on_advertisement_message_received(self, sender, advertisement):
|
|
499
|
-
try:
|
|
500
|
-
self.controller.on_link_advertising_data(
|
|
501
|
-
Address(sender), bytes.fromhex(advertisement)
|
|
502
|
-
)
|
|
503
|
-
except Exception:
|
|
504
|
-
logger.exception('exception')
|
|
505
|
-
|
|
506
|
-
async def on_acl_message_received(self, sender, acl_data):
|
|
507
|
-
try:
|
|
508
|
-
self.controller.on_link_acl_data(Address(sender), bytes.fromhex(acl_data))
|
|
509
|
-
except Exception:
|
|
510
|
-
logger.exception('exception')
|
|
511
|
-
|
|
512
|
-
async def on_connect_message_received(self, sender, _):
|
|
513
|
-
# Remember the connection
|
|
514
|
-
self.peripheral_connections.add(sender)
|
|
515
|
-
|
|
516
|
-
# Notify the controller
|
|
517
|
-
logger.debug(f'connection from central {sender}')
|
|
518
|
-
self.controller.on_link_central_connected(Address(sender))
|
|
519
|
-
|
|
520
|
-
# Accept the connection by responding to it
|
|
521
|
-
await self.send_targeted_message(sender, 'connected')
|
|
522
|
-
|
|
523
|
-
async def on_connected_message_received(self, sender, _):
|
|
524
|
-
if not self.pending_connection:
|
|
525
|
-
logger.warning('received a connection ack, but no connection is pending')
|
|
526
|
-
return
|
|
527
|
-
|
|
528
|
-
# Remember the connection
|
|
529
|
-
self.central_connections.add(sender)
|
|
530
|
-
|
|
531
|
-
# Notify the controller
|
|
532
|
-
logger.debug(f'connected to peripheral {self.pending_connection.peer_address}')
|
|
533
|
-
self.controller.on_link_peripheral_connection_complete(
|
|
534
|
-
self.pending_connection, HCI_SUCCESS
|
|
535
|
-
)
|
|
536
|
-
|
|
537
|
-
async def on_disconnect_message_received(self, sender, message):
|
|
538
|
-
# Notify the controller
|
|
539
|
-
params = parse_parameters(message)
|
|
540
|
-
reason = int(params.get('reason', str(HCI_CONNECTION_TIMEOUT_ERROR)))
|
|
541
|
-
self.controller.on_link_central_disconnected(Address(sender), reason)
|
|
542
|
-
|
|
543
|
-
# Forget the connection
|
|
544
|
-
if sender in self.peripheral_connections:
|
|
545
|
-
self.peripheral_connections.remove(sender)
|
|
546
|
-
|
|
547
|
-
async def on_encrypted_message_received(self, sender, _):
|
|
548
|
-
# TODO parse params to get real args
|
|
549
|
-
self.controller.on_link_encrypted(Address(sender), bytes(8), 0, bytes(16))
|
|
550
|
-
|
|
551
|
-
async def send_rpc_command(self, command):
|
|
552
|
-
# Ensure we have a connection
|
|
553
|
-
websocket = await self.websocket
|
|
554
|
-
|
|
555
|
-
# Create a future value to hold the eventual result
|
|
556
|
-
assert self.rpc_result is None
|
|
557
|
-
self.rpc_result = asyncio.get_running_loop().create_future()
|
|
558
|
-
|
|
559
|
-
# Send the command
|
|
560
|
-
await websocket.send(command)
|
|
561
|
-
|
|
562
|
-
# Wait for the result
|
|
563
|
-
rpc_result = await self.rpc_result
|
|
564
|
-
self.rpc_result = None
|
|
565
|
-
logger.debug(f'rpc_result: {rpc_result}')
|
|
566
|
-
|
|
567
|
-
# TODO: parse the result
|
|
568
|
-
|
|
569
|
-
async def send_targeted_message(self, target, message):
|
|
570
|
-
# Ensure we have a connection
|
|
571
|
-
websocket = await self.websocket
|
|
572
|
-
|
|
573
|
-
# Send the message
|
|
574
|
-
await websocket.send(f'@{target} {message}')
|
|
575
|
-
|
|
576
|
-
async def notify_address_changed(self):
|
|
577
|
-
await self.send_rpc_command(f'/set-address {self.controller.random_address}')
|
|
578
|
-
|
|
579
|
-
def on_address_changed(self, controller):
|
|
580
|
-
logger.info(f'address changed for {controller}: {controller.random_address}')
|
|
581
|
-
|
|
582
|
-
# Notify the relay of the change
|
|
583
|
-
self.execute(self.notify_address_changed)
|
|
584
|
-
|
|
585
|
-
async def send_advertising_data_to_relay(self, data):
|
|
586
|
-
await self.send_targeted_message('*', f'advertisement:{data.hex()}')
|
|
587
|
-
|
|
588
|
-
def send_advertising_data(self, _, data):
|
|
589
|
-
self.execute(partial(self.send_advertising_data_to_relay, data))
|
|
590
|
-
|
|
591
|
-
async def send_acl_data_to_relay(self, peer_address, data):
|
|
592
|
-
await self.send_targeted_message(peer_address, f'acl:{data.hex()}')
|
|
593
|
-
|
|
594
|
-
def send_acl_data(self, _, peer_address, _transport, data):
|
|
595
|
-
# TODO: handle different transport
|
|
596
|
-
self.execute(partial(self.send_acl_data_to_relay, peer_address, data))
|
|
597
|
-
|
|
598
|
-
async def send_connection_request_to_relay(self, peer_address):
|
|
599
|
-
await self.send_targeted_message(peer_address, 'connect')
|
|
600
|
-
|
|
601
|
-
def connect(self, _, le_create_connection_command):
|
|
602
|
-
if self.pending_connection:
|
|
603
|
-
logger.warning('connection already pending')
|
|
604
|
-
return
|
|
605
|
-
self.pending_connection = le_create_connection_command
|
|
606
|
-
self.execute(
|
|
607
|
-
partial(
|
|
608
|
-
self.send_connection_request_to_relay,
|
|
609
|
-
str(le_create_connection_command.peer_address),
|
|
610
|
-
)
|
|
611
|
-
)
|
|
612
|
-
|
|
613
|
-
def on_disconnection_complete(self, disconnect_command):
|
|
614
|
-
self.controller.on_link_peripheral_disconnection_complete(
|
|
615
|
-
disconnect_command, HCI_SUCCESS
|
|
616
|
-
)
|
|
617
|
-
|
|
618
|
-
def disconnect(self, central_address, peripheral_address, disconnect_command):
|
|
619
|
-
logger.debug(
|
|
620
|
-
f'disconnect {central_address} -> '
|
|
621
|
-
f'{peripheral_address}: reason = {disconnect_command.reason}'
|
|
622
|
-
)
|
|
623
|
-
self.execute(
|
|
624
|
-
partial(
|
|
625
|
-
self.send_targeted_message,
|
|
626
|
-
peripheral_address,
|
|
627
|
-
f'disconnect:reason={disconnect_command.reason}',
|
|
628
|
-
)
|
|
629
|
-
)
|
|
630
|
-
asyncio.get_running_loop().call_soon(
|
|
631
|
-
self.on_disconnection_complete, disconnect_command
|
|
632
|
-
)
|
|
633
|
-
|
|
634
|
-
def on_connection_encrypted(self, _, peripheral_address, rand, ediv, ltk):
|
|
635
|
-
asyncio.get_running_loop().call_soon(
|
|
636
|
-
self.controller.on_link_encrypted, peripheral_address, rand, ediv, ltk
|
|
637
|
-
)
|
|
638
|
-
self.execute(
|
|
639
|
-
partial(
|
|
640
|
-
self.send_targeted_message,
|
|
641
|
-
peripheral_address,
|
|
642
|
-
f'encrypted:ltk={ltk.hex()}',
|
|
643
|
-
)
|
|
644
|
-
)
|
bumble/pairing.py
CHANGED
|
@@ -18,15 +18,10 @@
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
import enum
|
|
20
20
|
from dataclasses import dataclass
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
|
26
|
-
HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
|
27
|
-
HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
|
28
|
-
HCI_KEYBOARD_ONLY_IO_CAPABILITY,
|
|
29
|
-
)
|
|
21
|
+
import secrets
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
from bumble import hci
|
|
30
25
|
from bumble.smp import (
|
|
31
26
|
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
|
32
27
|
SMP_KEYBOARD_ONLY_IO_CAPABILITY,
|
|
@@ -49,7 +44,7 @@ from bumble.core import AdvertisingData, LeRole
|
|
|
49
44
|
class OobData:
|
|
50
45
|
"""OOB data that can be sent from one device to another."""
|
|
51
46
|
|
|
52
|
-
address: Optional[Address] = None
|
|
47
|
+
address: Optional[hci.Address] = None
|
|
53
48
|
role: Optional[LeRole] = None
|
|
54
49
|
shared_data: Optional[OobSharedData] = None
|
|
55
50
|
legacy_context: Optional[OobLegacyContext] = None
|
|
@@ -61,7 +56,7 @@ class OobData:
|
|
|
61
56
|
shared_data_r: Optional[bytes] = None
|
|
62
57
|
for ad_type, ad_data in ad.ad_structures:
|
|
63
58
|
if ad_type == AdvertisingData.LE_BLUETOOTH_DEVICE_ADDRESS:
|
|
64
|
-
instance.address = Address(ad_data)
|
|
59
|
+
instance.address = hci.Address(ad_data)
|
|
65
60
|
elif ad_type == AdvertisingData.LE_ROLE:
|
|
66
61
|
instance.role = LeRole(ad_data[0])
|
|
67
62
|
elif ad_type == AdvertisingData.LE_SECURE_CONNECTIONS_CONFIRMATION_VALUE:
|
|
@@ -129,11 +124,11 @@ class PairingDelegate:
|
|
|
129
124
|
# Default mapping from abstract to Classic I/O capabilities.
|
|
130
125
|
# Subclasses may override this if they prefer a different mapping.
|
|
131
126
|
CLASSIC_IO_CAPABILITIES_MAP = {
|
|
132
|
-
NO_OUTPUT_NO_INPUT:
|
|
133
|
-
KEYBOARD_INPUT_ONLY:
|
|
134
|
-
DISPLAY_OUTPUT_ONLY:
|
|
135
|
-
DISPLAY_OUTPUT_AND_YES_NO_INPUT:
|
|
136
|
-
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT:
|
|
127
|
+
NO_OUTPUT_NO_INPUT: hci.IoCapability.NO_INPUT_NO_OUTPUT,
|
|
128
|
+
KEYBOARD_INPUT_ONLY: hci.IoCapability.KEYBOARD_ONLY,
|
|
129
|
+
DISPLAY_OUTPUT_ONLY: hci.IoCapability.DISPLAY_ONLY,
|
|
130
|
+
DISPLAY_OUTPUT_AND_YES_NO_INPUT: hci.IoCapability.DISPLAY_YES_NO,
|
|
131
|
+
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT: hci.IoCapability.DISPLAY_YES_NO,
|
|
137
132
|
}
|
|
138
133
|
|
|
139
134
|
io_capability: IoCapability
|
|
@@ -159,7 +154,7 @@ class PairingDelegate:
|
|
|
159
154
|
|
|
160
155
|
# pylint: disable=line-too-long
|
|
161
156
|
return self.CLASSIC_IO_CAPABILITIES_MAP.get(
|
|
162
|
-
self.io_capability,
|
|
157
|
+
self.io_capability, hci.IoCapability.NO_INPUT_NO_OUTPUT
|
|
163
158
|
)
|
|
164
159
|
|
|
165
160
|
@property
|
|
@@ -205,7 +200,7 @@ class PairingDelegate:
|
|
|
205
200
|
# [LE only]
|
|
206
201
|
async def key_distribution_response(
|
|
207
202
|
self, peer_initiator_key_distribution: int, peer_responder_key_distribution: int
|
|
208
|
-
) ->
|
|
203
|
+
) -> tuple[int, int]:
|
|
209
204
|
"""
|
|
210
205
|
Return the key distribution response in an SMP protocol context.
|
|
211
206
|
|
|
@@ -222,14 +217,22 @@ class PairingDelegate:
|
|
|
222
217
|
),
|
|
223
218
|
)
|
|
224
219
|
|
|
220
|
+
async def generate_passkey(self) -> int:
|
|
221
|
+
"""
|
|
222
|
+
Return a passkey value between 0 and 999999 (inclusive).
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
# By default, generate a random passkey.
|
|
226
|
+
return secrets.randbelow(1000000)
|
|
227
|
+
|
|
225
228
|
|
|
226
229
|
# -----------------------------------------------------------------------------
|
|
227
230
|
class PairingConfig:
|
|
228
231
|
"""Configuration for the Pairing protocol."""
|
|
229
232
|
|
|
230
233
|
class AddressType(enum.IntEnum):
|
|
231
|
-
PUBLIC = Address.PUBLIC_DEVICE_ADDRESS
|
|
232
|
-
RANDOM = Address.RANDOM_DEVICE_ADDRESS
|
|
234
|
+
PUBLIC = hci.Address.PUBLIC_DEVICE_ADDRESS
|
|
235
|
+
RANDOM = hci.Address.RANDOM_DEVICE_ADDRESS
|
|
233
236
|
|
|
234
237
|
@dataclass
|
|
235
238
|
class OobConfig:
|
bumble/pandora/__init__.py
CHANGED
bumble/pandora/config.py
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
from bumble.pairing import PairingConfig, PairingDelegate
|
|
17
17
|
from dataclasses import dataclass
|
|
18
|
-
from typing import Any
|
|
18
|
+
from typing import Any
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@dataclass
|
|
@@ -32,7 +32,7 @@ class Config:
|
|
|
32
32
|
PairingDelegate.DEFAULT_KEY_DISTRIBUTION
|
|
33
33
|
)
|
|
34
34
|
|
|
35
|
-
def load_from_dict(self, config:
|
|
35
|
+
def load_from_dict(self, config: dict[str, Any]) -> None:
|
|
36
36
|
io_capability_name: str = config.get(
|
|
37
37
|
'io_capability', 'no_output_no_input'
|
|
38
38
|
).upper()
|
bumble/pandora/device.py
CHANGED
|
@@ -32,7 +32,7 @@ from bumble.sdp import (
|
|
|
32
32
|
DataElement,
|
|
33
33
|
ServiceAttribute,
|
|
34
34
|
)
|
|
35
|
-
from typing import Any,
|
|
35
|
+
from typing import Any, Optional
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
# Default rootcanal HCI TCP address
|
|
@@ -49,13 +49,13 @@ class PandoraDevice:
|
|
|
49
49
|
|
|
50
50
|
# Bumble device instance & configuration.
|
|
51
51
|
device: Device
|
|
52
|
-
config:
|
|
52
|
+
config: dict[str, Any]
|
|
53
53
|
|
|
54
54
|
# HCI transport name & instance.
|
|
55
55
|
_hci_name: str
|
|
56
56
|
_hci: Optional[transport.Transport] # type: ignore[name-defined]
|
|
57
57
|
|
|
58
|
-
def __init__(self, config:
|
|
58
|
+
def __init__(self, config: dict[str, Any]) -> None:
|
|
59
59
|
self.config = config
|
|
60
60
|
self.device = _make_device(config)
|
|
61
61
|
self._hci_name = config.get(
|
|
@@ -95,14 +95,14 @@ class PandoraDevice:
|
|
|
95
95
|
await self.close()
|
|
96
96
|
await self.open()
|
|
97
97
|
|
|
98
|
-
def info(self) -> Optional[
|
|
98
|
+
def info(self) -> Optional[dict[str, str]]:
|
|
99
99
|
return {
|
|
100
100
|
'public_bd_address': str(self.device.public_address),
|
|
101
101
|
'random_address': str(self.device.random_address),
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
|
|
105
|
-
def _make_device(config:
|
|
105
|
+
def _make_device(config: dict[str, Any]) -> Device:
|
|
106
106
|
"""Initialize an idle Bumble device instance."""
|
|
107
107
|
|
|
108
108
|
# initialize bumble device.
|
|
@@ -117,7 +117,7 @@ def _make_device(config: Dict[str, Any]) -> Device:
|
|
|
117
117
|
|
|
118
118
|
|
|
119
119
|
# TODO(b/267540823): remove when Pandora A2dp is supported
|
|
120
|
-
def _make_sdp_records(rfcomm_channel: int) ->
|
|
120
|
+
def _make_sdp_records(rfcomm_channel: int) -> dict[int, list[ServiceAttribute]]:
|
|
121
121
|
return {
|
|
122
122
|
0x00010001: [
|
|
123
123
|
ServiceAttribute(
|
bumble/pandora/host.py
CHANGED
|
@@ -73,7 +73,6 @@ from pandora.host_pb2 import (
|
|
|
73
73
|
ConnectResponse,
|
|
74
74
|
DataTypes,
|
|
75
75
|
DisconnectRequest,
|
|
76
|
-
DiscoverabilityMode,
|
|
77
76
|
InquiryResponse,
|
|
78
77
|
PrimaryPhy,
|
|
79
78
|
ReadLocalAddressResponse,
|
|
@@ -86,9 +85,9 @@ from pandora.host_pb2 import (
|
|
|
86
85
|
WaitConnectionResponse,
|
|
87
86
|
WaitDisconnectionRequest,
|
|
88
87
|
)
|
|
89
|
-
from typing import AsyncGenerator,
|
|
88
|
+
from typing import AsyncGenerator, Optional, cast
|
|
90
89
|
|
|
91
|
-
PRIMARY_PHY_MAP:
|
|
90
|
+
PRIMARY_PHY_MAP: dict[int, PrimaryPhy] = {
|
|
92
91
|
# Default value reported by Bumble for legacy Advertising reports.
|
|
93
92
|
# FIXME(uael): `None` might be a better value, but Bumble need to change accordingly.
|
|
94
93
|
0: PRIMARY_1M,
|
|
@@ -96,26 +95,26 @@ PRIMARY_PHY_MAP: Dict[int, PrimaryPhy] = {
|
|
|
96
95
|
3: PRIMARY_CODED,
|
|
97
96
|
}
|
|
98
97
|
|
|
99
|
-
SECONDARY_PHY_MAP:
|
|
98
|
+
SECONDARY_PHY_MAP: dict[int, SecondaryPhy] = {
|
|
100
99
|
0: SECONDARY_NONE,
|
|
101
100
|
1: SECONDARY_1M,
|
|
102
101
|
2: SECONDARY_2M,
|
|
103
102
|
3: SECONDARY_CODED,
|
|
104
103
|
}
|
|
105
104
|
|
|
106
|
-
PRIMARY_PHY_TO_BUMBLE_PHY_MAP:
|
|
105
|
+
PRIMARY_PHY_TO_BUMBLE_PHY_MAP: dict[PrimaryPhy, Phy] = {
|
|
107
106
|
PRIMARY_1M: Phy.LE_1M,
|
|
108
107
|
PRIMARY_CODED: Phy.LE_CODED,
|
|
109
108
|
}
|
|
110
109
|
|
|
111
|
-
SECONDARY_PHY_TO_BUMBLE_PHY_MAP:
|
|
110
|
+
SECONDARY_PHY_TO_BUMBLE_PHY_MAP: dict[SecondaryPhy, Phy] = {
|
|
112
111
|
SECONDARY_NONE: Phy.LE_1M,
|
|
113
112
|
SECONDARY_1M: Phy.LE_1M,
|
|
114
113
|
SECONDARY_2M: Phy.LE_2M,
|
|
115
114
|
SECONDARY_CODED: Phy.LE_CODED,
|
|
116
115
|
}
|
|
117
116
|
|
|
118
|
-
OWN_ADDRESS_MAP:
|
|
117
|
+
OWN_ADDRESS_MAP: dict[host_pb2.OwnAddressType, OwnAddressType] = {
|
|
119
118
|
host_pb2.PUBLIC: OwnAddressType.PUBLIC,
|
|
120
119
|
host_pb2.RANDOM: OwnAddressType.RANDOM,
|
|
121
120
|
host_pb2.RESOLVABLE_OR_PUBLIC: OwnAddressType.RESOLVABLE_OR_PUBLIC,
|
|
@@ -124,7 +123,7 @@ OWN_ADDRESS_MAP: Dict[host_pb2.OwnAddressType, OwnAddressType] = {
|
|
|
124
123
|
|
|
125
124
|
|
|
126
125
|
class HostService(HostServicer):
|
|
127
|
-
waited_connections:
|
|
126
|
+
waited_connections: set[int]
|
|
128
127
|
|
|
129
128
|
def __init__(
|
|
130
129
|
self, grpc_server: grpc.aio.Server, device: Device, config: Config
|
|
@@ -618,7 +617,7 @@ class HostService(HostServicer):
|
|
|
618
617
|
self.log.debug('Inquiry')
|
|
619
618
|
|
|
620
619
|
inquiry_queue: asyncio.Queue[
|
|
621
|
-
Optional[
|
|
620
|
+
Optional[tuple[Address, int, AdvertisingData, int]]
|
|
622
621
|
] = asyncio.Queue()
|
|
623
622
|
complete_handler = self.device.on(
|
|
624
623
|
self.device.EVENT_INQUIRY_COMPLETE, lambda: inquiry_queue.put_nowait(None)
|
|
@@ -670,10 +669,10 @@ class HostService(HostServicer):
|
|
|
670
669
|
return empty_pb2.Empty()
|
|
671
670
|
|
|
672
671
|
def unpack_data_types(self, dt: DataTypes) -> AdvertisingData:
|
|
673
|
-
ad_structures:
|
|
672
|
+
ad_structures: list[tuple[int, bytes]] = []
|
|
674
673
|
|
|
675
|
-
uuids:
|
|
676
|
-
datas:
|
|
674
|
+
uuids: list[str]
|
|
675
|
+
datas: dict[str, bytes]
|
|
677
676
|
|
|
678
677
|
def uuid128_from_str(uuid: str) -> bytes:
|
|
679
678
|
"""Decode a 128-bit uuid encoded as XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
|
|
@@ -887,50 +886,50 @@ class HostService(HostServicer):
|
|
|
887
886
|
|
|
888
887
|
def pack_data_types(self, ad: AdvertisingData) -> DataTypes:
|
|
889
888
|
dt = DataTypes()
|
|
890
|
-
uuids:
|
|
889
|
+
uuids: list[UUID]
|
|
891
890
|
s: str
|
|
892
891
|
i: int
|
|
893
|
-
ij:
|
|
894
|
-
uuid_data:
|
|
892
|
+
ij: tuple[int, int]
|
|
893
|
+
uuid_data: tuple[UUID, bytes]
|
|
895
894
|
data: bytes
|
|
896
895
|
|
|
897
896
|
if uuids := cast(
|
|
898
|
-
|
|
897
|
+
list[UUID],
|
|
899
898
|
ad.get(AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS),
|
|
900
899
|
):
|
|
901
900
|
dt.incomplete_service_class_uuids16.extend(
|
|
902
901
|
list(map(lambda x: x.to_hex_str('-'), uuids))
|
|
903
902
|
)
|
|
904
903
|
if uuids := cast(
|
|
905
|
-
|
|
904
|
+
list[UUID],
|
|
906
905
|
ad.get(AdvertisingData.COMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS),
|
|
907
906
|
):
|
|
908
907
|
dt.complete_service_class_uuids16.extend(
|
|
909
908
|
list(map(lambda x: x.to_hex_str('-'), uuids))
|
|
910
909
|
)
|
|
911
910
|
if uuids := cast(
|
|
912
|
-
|
|
911
|
+
list[UUID],
|
|
913
912
|
ad.get(AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS),
|
|
914
913
|
):
|
|
915
914
|
dt.incomplete_service_class_uuids32.extend(
|
|
916
915
|
list(map(lambda x: x.to_hex_str('-'), uuids))
|
|
917
916
|
)
|
|
918
917
|
if uuids := cast(
|
|
919
|
-
|
|
918
|
+
list[UUID],
|
|
920
919
|
ad.get(AdvertisingData.COMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS),
|
|
921
920
|
):
|
|
922
921
|
dt.complete_service_class_uuids32.extend(
|
|
923
922
|
list(map(lambda x: x.to_hex_str('-'), uuids))
|
|
924
923
|
)
|
|
925
924
|
if uuids := cast(
|
|
926
|
-
|
|
925
|
+
list[UUID],
|
|
927
926
|
ad.get(AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS),
|
|
928
927
|
):
|
|
929
928
|
dt.incomplete_service_class_uuids128.extend(
|
|
930
929
|
list(map(lambda x: x.to_hex_str('-'), uuids))
|
|
931
930
|
)
|
|
932
931
|
if uuids := cast(
|
|
933
|
-
|
|
932
|
+
list[UUID],
|
|
934
933
|
ad.get(AdvertisingData.COMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS),
|
|
935
934
|
):
|
|
936
935
|
dt.complete_service_class_uuids128.extend(
|
|
@@ -945,42 +944,42 @@ class HostService(HostServicer):
|
|
|
945
944
|
if i := cast(int, ad.get(AdvertisingData.CLASS_OF_DEVICE)):
|
|
946
945
|
dt.class_of_device = i
|
|
947
946
|
if ij := cast(
|
|
948
|
-
|
|
947
|
+
tuple[int, int],
|
|
949
948
|
ad.get(AdvertisingData.PERIPHERAL_CONNECTION_INTERVAL_RANGE),
|
|
950
949
|
):
|
|
951
950
|
dt.peripheral_connection_interval_min = ij[0]
|
|
952
951
|
dt.peripheral_connection_interval_max = ij[1]
|
|
953
952
|
if uuids := cast(
|
|
954
|
-
|
|
953
|
+
list[UUID],
|
|
955
954
|
ad.get(AdvertisingData.LIST_OF_16_BIT_SERVICE_SOLICITATION_UUIDS),
|
|
956
955
|
):
|
|
957
956
|
dt.service_solicitation_uuids16.extend(
|
|
958
957
|
list(map(lambda x: x.to_hex_str('-'), uuids))
|
|
959
958
|
)
|
|
960
959
|
if uuids := cast(
|
|
961
|
-
|
|
960
|
+
list[UUID],
|
|
962
961
|
ad.get(AdvertisingData.LIST_OF_32_BIT_SERVICE_SOLICITATION_UUIDS),
|
|
963
962
|
):
|
|
964
963
|
dt.service_solicitation_uuids32.extend(
|
|
965
964
|
list(map(lambda x: x.to_hex_str('-'), uuids))
|
|
966
965
|
)
|
|
967
966
|
if uuids := cast(
|
|
968
|
-
|
|
967
|
+
list[UUID],
|
|
969
968
|
ad.get(AdvertisingData.LIST_OF_128_BIT_SERVICE_SOLICITATION_UUIDS),
|
|
970
969
|
):
|
|
971
970
|
dt.service_solicitation_uuids128.extend(
|
|
972
971
|
list(map(lambda x: x.to_hex_str('-'), uuids))
|
|
973
972
|
)
|
|
974
973
|
if uuid_data := cast(
|
|
975
|
-
|
|
974
|
+
tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_16_BIT_UUID)
|
|
976
975
|
):
|
|
977
976
|
dt.service_data_uuid16[uuid_data[0].to_hex_str('-')] = uuid_data[1]
|
|
978
977
|
if uuid_data := cast(
|
|
979
|
-
|
|
978
|
+
tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_32_BIT_UUID)
|
|
980
979
|
):
|
|
981
980
|
dt.service_data_uuid32[uuid_data[0].to_hex_str('-')] = uuid_data[1]
|
|
982
981
|
if uuid_data := cast(
|
|
983
|
-
|
|
982
|
+
tuple[UUID, bytes], ad.get(AdvertisingData.SERVICE_DATA_128_BIT_UUID)
|
|
984
983
|
):
|
|
985
984
|
dt.service_data_uuid128[uuid_data[0].to_hex_str('-')] = uuid_data[1]
|
|
986
985
|
if data := cast(bytes, ad.get(AdvertisingData.PUBLIC_TARGET_ADDRESS, raw=True)):
|