bumble 0.0.212__py3-none-any.whl → 0.0.214__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.
Files changed (92) hide show
  1. bumble/_version.py +2 -2
  2. bumble/a2dp.py +6 -0
  3. bumble/apps/README.md +0 -3
  4. bumble/apps/auracast.py +14 -11
  5. bumble/apps/bench.py +482 -37
  6. bumble/apps/console.py +3 -3
  7. bumble/apps/controller_info.py +44 -12
  8. bumble/apps/controller_loopback.py +7 -7
  9. bumble/apps/controllers.py +4 -5
  10. bumble/apps/device_info.py +4 -5
  11. bumble/apps/gatt_dump.py +5 -5
  12. bumble/apps/gg_bridge.py +5 -5
  13. bumble/apps/hci_bridge.py +5 -4
  14. bumble/apps/l2cap_bridge.py +5 -5
  15. bumble/apps/lea_unicast/app.py +8 -3
  16. bumble/apps/pair.py +19 -11
  17. bumble/apps/pandora_server.py +2 -2
  18. bumble/apps/player/player.py +2 -3
  19. bumble/apps/rfcomm_bridge.py +3 -4
  20. bumble/apps/scan.py +4 -5
  21. bumble/apps/show.py +6 -4
  22. bumble/apps/speaker/speaker.html +1 -0
  23. bumble/apps/speaker/speaker.js +113 -62
  24. bumble/apps/speaker/speaker.py +123 -19
  25. bumble/apps/unbond.py +2 -3
  26. bumble/apps/usb_probe.py +2 -3
  27. bumble/at.py +4 -4
  28. bumble/att.py +2 -6
  29. bumble/avc.py +7 -7
  30. bumble/avctp.py +3 -3
  31. bumble/avdtp.py +16 -20
  32. bumble/avrcp.py +42 -54
  33. bumble/colors.py +2 -2
  34. bumble/controller.py +174 -45
  35. bumble/device.py +398 -182
  36. bumble/drivers/__init__.py +2 -2
  37. bumble/drivers/common.py +0 -2
  38. bumble/drivers/intel.py +37 -40
  39. bumble/drivers/rtk.py +28 -35
  40. bumble/gatt.py +4 -4
  41. bumble/gatt_adapters.py +4 -5
  42. bumble/gatt_client.py +26 -31
  43. bumble/gatt_server.py +7 -11
  44. bumble/hci.py +2648 -2909
  45. bumble/helpers.py +4 -5
  46. bumble/hfp.py +32 -37
  47. bumble/host.py +104 -35
  48. bumble/keys.py +5 -5
  49. bumble/l2cap.py +312 -409
  50. bumble/link.py +16 -280
  51. bumble/logging.py +65 -0
  52. bumble/pairing.py +23 -20
  53. bumble/pandora/__init__.py +2 -2
  54. bumble/pandora/config.py +2 -2
  55. bumble/pandora/device.py +6 -6
  56. bumble/pandora/host.py +27 -28
  57. bumble/pandora/l2cap.py +2 -2
  58. bumble/pandora/security.py +6 -6
  59. bumble/pandora/utils.py +3 -3
  60. bumble/profiles/ams.py +404 -0
  61. bumble/profiles/ascs.py +142 -131
  62. bumble/profiles/asha.py +2 -2
  63. bumble/profiles/bap.py +3 -4
  64. bumble/profiles/csip.py +2 -2
  65. bumble/profiles/device_information_service.py +2 -2
  66. bumble/profiles/gap.py +2 -2
  67. bumble/profiles/hap.py +34 -33
  68. bumble/profiles/le_audio.py +4 -4
  69. bumble/profiles/mcp.py +4 -4
  70. bumble/profiles/vcs.py +3 -5
  71. bumble/rfcomm.py +10 -10
  72. bumble/rtp.py +1 -2
  73. bumble/sdp.py +2 -2
  74. bumble/smp.py +62 -63
  75. bumble/tools/intel_util.py +3 -2
  76. bumble/tools/rtk_util.py +6 -5
  77. bumble/transport/__init__.py +2 -16
  78. bumble/transport/android_netsim.py +5 -5
  79. bumble/transport/common.py +4 -4
  80. bumble/transport/pyusb.py +2 -2
  81. bumble/utils.py +2 -5
  82. bumble/vendor/android/hci.py +118 -200
  83. bumble/vendor/zephyr/hci.py +32 -27
  84. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/METADATA +4 -3
  85. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/RECORD +89 -90
  86. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/WHEEL +1 -1
  87. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/entry_points.txt +0 -1
  88. bumble/apps/link_relay/__init__.py +0 -0
  89. bumble/apps/link_relay/link_relay.py +0 -289
  90. bumble/apps/link_relay/logging.yml +0 -21
  91. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/licenses/LICENSE +0 -0
  92. {bumble-0.0.212.dist-info → bumble-0.0.214.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.core import (
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, Set
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: Set[controller.Controller]
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:
@@ -165,29 +159,29 @@ class LocalLink:
165
159
  asyncio.get_running_loop().call_soon(self.on_connection_complete)
166
160
 
167
161
  def on_disconnection_complete(
168
- self, central_address, peripheral_address, disconnect_command
162
+ self, initiating_address, target_address, disconnect_command
169
163
  ):
170
164
  # Find the controller that initiated the disconnection
171
- if not (central_controller := self.find_controller(central_address)):
165
+ if not (initiating_controller := self.find_controller(initiating_address)):
172
166
  logger.warning('!!! Initiating controller not found')
173
167
  return
174
168
 
175
169
  # Disconnect from the first controller with a matching address
176
- if peripheral_controller := self.find_controller(peripheral_address):
177
- peripheral_controller.on_link_central_disconnected(
178
- central_address, disconnect_command.reason
170
+ if target_controller := self.find_controller(target_address):
171
+ target_controller.on_link_disconnected(
172
+ initiating_address, disconnect_command.reason
179
173
  )
180
174
 
181
- central_controller.on_link_peripheral_disconnection_complete(
175
+ initiating_controller.on_link_disconnection_complete(
182
176
  disconnect_command, HCI_SUCCESS
183
177
  )
184
178
 
185
- def disconnect(self, central_address, peripheral_address, disconnect_command):
179
+ def disconnect(self, initiating_address, target_address, disconnect_command):
186
180
  logger.debug(
187
- f'$$$ DISCONNECTION {central_address} -> '
188
- f'{peripheral_address}: reason = {disconnect_command.reason}'
181
+ f'$$$ DISCONNECTION {initiating_address} -> '
182
+ f'{target_address}: reason = {disconnect_command.reason}'
189
183
  )
190
- args = [central_address, peripheral_address, disconnect_command]
184
+ args = [initiating_address, target_address, disconnect_command]
191
185
  asyncio.get_running_loop().call_soon(self.on_disconnection_complete, *args)
192
186
 
193
187
  # pylint: disable=too-many-arguments
@@ -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.ACL_LINK_TYPE,
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/logging.py ADDED
@@ -0,0 +1,65 @@
1
+ # Copyright 2025 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
+ # -----------------------------------------------------------------------------
16
+ # Imports
17
+ # -----------------------------------------------------------------------------
18
+ import functools
19
+ import logging
20
+ import os
21
+
22
+ from bumble import colors
23
+
24
+
25
+ # -----------------------------------------------------------------------------
26
+ class ColorFormatter(logging.Formatter):
27
+ _colorizers = {
28
+ logging.DEBUG: functools.partial(colors.color, fg="white"),
29
+ logging.INFO: functools.partial(colors.color, fg="green"),
30
+ logging.WARNING: functools.partial(colors.color, fg="yellow"),
31
+ logging.ERROR: functools.partial(colors.color, fg="red"),
32
+ logging.CRITICAL: functools.partial(colors.color, fg="black", bg="red"),
33
+ }
34
+
35
+ _formatters = {
36
+ level: logging.Formatter(
37
+ fmt=colorizer("{asctime}.{msecs:03.0f} {levelname:.1} {name}: ")
38
+ + "{message}",
39
+ datefmt="%H:%M:%S",
40
+ style="{",
41
+ )
42
+ for level, colorizer in _colorizers.items()
43
+ }
44
+
45
+ def format(self, record: logging.LogRecord) -> str:
46
+ return self._formatters[record.levelno].format(record)
47
+
48
+
49
+ def setup_basic_logging(default_level: str = "INFO") -> None:
50
+ """
51
+ Set up basic logging with logging.basicConfig, configured with a simple formatter
52
+ that prints out the date and log level in color.
53
+ If the BUMBLE_LOGLEVEL environment variable is set to the name of a log level, it
54
+ is used. Otherwise the default_level argument is used.
55
+
56
+ Args:
57
+ default_level: default logging level
58
+
59
+ """
60
+ handler = logging.StreamHandler()
61
+ handler.setFormatter(ColorFormatter())
62
+ logging.basicConfig(
63
+ level=os.environ.get("BUMBLE_LOGLEVEL", default_level).upper(),
64
+ handlers=[handler],
65
+ )
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
- from typing import Optional, Tuple
22
-
23
- from bumble.hci import (
24
- Address,
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: HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
133
- KEYBOARD_INPUT_ONLY: HCI_KEYBOARD_ONLY_IO_CAPABILITY,
134
- DISPLAY_OUTPUT_ONLY: HCI_DISPLAY_ONLY_IO_CAPABILITY,
135
- DISPLAY_OUTPUT_AND_YES_NO_INPUT: HCI_DISPLAY_YES_NO_IO_CAPABILITY,
136
- DISPLAY_OUTPUT_AND_KEYBOARD_INPUT: HCI_DISPLAY_YES_NO_IO_CAPABILITY,
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, HCI_NO_INPUT_NO_OUTPUT_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
- ) -> Tuple[int, int]:
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:
@@ -45,11 +45,11 @@ __all__ = [
45
45
 
46
46
 
47
47
  # Add servicers hooks.
48
- _SERVICERS_HOOKS: List[Callable[[PandoraDevice, Config, grpc.aio.Server], None]] = []
48
+ _SERVICERS_HOOKS: list[Callable[[PandoraDevice, Config, grpc.aio.Server], None]] = []
49
49
 
50
50
 
51
51
  def register_servicer_hook(
52
- hook: Callable[[PandoraDevice, Config, grpc.aio.Server], None]
52
+ hook: Callable[[PandoraDevice, Config, grpc.aio.Server], None],
53
53
  ) -> None:
54
54
  _SERVICERS_HOOKS.append(hook)
55
55
 
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, Dict
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: Dict[str, Any]) -> None:
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, Dict, List, Optional
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: Dict[str, Any]
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: Dict[str, Any]) -> None:
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[Dict[str, str]]:
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: Dict[str, Any]) -> Device:
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) -> Dict[int, List[ServiceAttribute]]:
120
+ def _make_sdp_records(rfcomm_channel: int) -> dict[int, list[ServiceAttribute]]:
121
121
  return {
122
122
  0x00010001: [
123
123
  ServiceAttribute(