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/profiles/hap.py CHANGED
@@ -20,7 +20,7 @@ import asyncio
20
20
  import functools
21
21
  from dataclasses import dataclass, field
22
22
  import logging
23
- from typing import Any, Dict, List, Optional, Set, Union
23
+ from typing import Any, Optional, Union
24
24
 
25
25
  from bumble import att, gatt, gatt_adapters, gatt_client
26
26
  from bumble.core import InvalidArgumentError, InvalidStateError
@@ -228,23 +228,25 @@ class HearingAccessService(gatt.TemplateService):
228
228
  hearing_aid_preset_control_point: gatt.Characteristic[bytes]
229
229
  active_preset_index_characteristic: gatt.Characteristic[bytes]
230
230
  active_preset_index: int
231
- active_preset_index_per_device: Dict[Address, int]
231
+ active_preset_index_per_device: dict[Address, int]
232
232
 
233
233
  device: Device
234
234
 
235
235
  server_features: HearingAidFeatures
236
- preset_records: Dict[int, PresetRecord] # key is the preset index
236
+ preset_records: dict[int, PresetRecord] # key is the preset index
237
237
  read_presets_request_in_progress: bool
238
238
 
239
- preset_changed_operations_history_per_device: Dict[
240
- Address, List[PresetChangedOperation]
239
+ other_server_in_binaural_set: Optional[HearingAccessService] = None
240
+
241
+ preset_changed_operations_history_per_device: dict[
242
+ Address, list[PresetChangedOperation]
241
243
  ]
242
244
 
243
245
  # Keep an updated list of connected client to send notification to
244
- currently_connected_clients: Set[Connection]
246
+ currently_connected_clients: set[Connection]
245
247
 
246
248
  def __init__(
247
- self, device: Device, features: HearingAidFeatures, presets: List[PresetRecord]
249
+ self, device: Device, features: HearingAidFeatures, presets: list[PresetRecord]
248
250
  ) -> None:
249
251
  self.active_preset_index_per_device = {}
250
252
  self.read_presets_request_in_progress = False
@@ -333,7 +335,7 @@ class HearingAccessService(gatt.TemplateService):
333
335
  # Update the active preset index if needed
334
336
  await self.notify_active_preset_for_connection(connection)
335
337
 
336
- utils.cancel_on_event(connection, 'disconnection', on_connection_async())
338
+ connection.cancel_on_disconnection(on_connection_async())
337
339
 
338
340
  def _on_read_active_preset_index(self, connection: Connection) -> bytes:
339
341
  del connection # Unused
@@ -379,7 +381,7 @@ class HearingAccessService(gatt.TemplateService):
379
381
  utils.AsyncRunner.spawn(self._read_preset_response(connection, presets))
380
382
 
381
383
  async def _read_preset_response(
382
- self, connection: Connection, presets: List[PresetRecord]
384
+ self, connection: Connection, presets: list[PresetRecord]
383
385
  ):
384
386
  # If the ATT bearer is terminated before all notifications or indications are sent, then the server shall consider the Read Presets Request operation aborted and shall not either continue or restart the operation when the client reconnects.
385
387
  try:
@@ -513,7 +515,7 @@ class HearingAccessService(gatt.TemplateService):
513
515
  for connection in self.currently_connected_clients:
514
516
  await self.notify_active_preset_for_connection(connection)
515
517
 
516
- async def set_active_preset(self, connection: Connection, value: bytes) -> None:
518
+ async def set_active_preset(self, value: bytes) -> None:
517
519
  index = value[1]
518
520
  preset = self.preset_records.get(index, None)
519
521
  if (
@@ -530,10 +532,10 @@ class HearingAccessService(gatt.TemplateService):
530
532
  self.active_preset_index = index
531
533
  await self.notify_active_preset()
532
534
 
533
- async def _on_set_active_preset(self, connection: Connection, value: bytes):
534
- await self.set_active_preset(connection, value)
535
+ async def _on_set_active_preset(self, _: Connection, value: bytes):
536
+ await self.set_active_preset(value)
535
537
 
536
- async def set_next_or_previous_preset(self, connection: Connection, is_previous):
538
+ async def set_next_or_previous_preset(self, is_previous):
537
539
  '''Set the next or the previous preset as active'''
538
540
 
539
541
  if self.active_preset_index == 0x00:
@@ -563,48 +565,47 @@ class HearingAccessService(gatt.TemplateService):
563
565
  self.active_preset_index = first_preset.index
564
566
  await self.notify_active_preset()
565
567
 
566
- async def _on_set_next_preset(
567
- self, connection: Connection, __value__: bytes
568
- ) -> None:
569
- await self.set_next_or_previous_preset(connection, False)
568
+ async def _on_set_next_preset(self, _: Connection, __value__: bytes) -> None:
569
+ await self.set_next_or_previous_preset(False)
570
570
 
571
- async def _on_set_previous_preset(
572
- self, connection: Connection, __value__: bytes
573
- ) -> None:
574
- await self.set_next_or_previous_preset(connection, True)
571
+ async def _on_set_previous_preset(self, _: Connection, __value__: bytes) -> None:
572
+ await self.set_next_or_previous_preset(True)
575
573
 
576
574
  async def _on_set_active_preset_synchronized_locally(
577
- self, connection: Connection, value: bytes
575
+ self, _: Connection, value: bytes
578
576
  ):
579
577
  if (
580
578
  self.server_features.preset_synchronization_support
581
- == PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_SUPPORTED
579
+ == PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
582
580
  ):
583
581
  raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED)
584
- await self.set_active_preset(connection, value)
585
- # TODO (low priority) inform other server of the change
582
+ await self.set_active_preset(value)
583
+ if self.other_server_in_binaural_set:
584
+ await self.other_server_in_binaural_set.set_active_preset(value)
586
585
 
587
586
  async def _on_set_next_preset_synchronized_locally(
588
- self, connection: Connection, __value__: bytes
587
+ self, _: Connection, __value__: bytes
589
588
  ):
590
589
  if (
591
590
  self.server_features.preset_synchronization_support
592
- == PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_SUPPORTED
591
+ == PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
593
592
  ):
594
593
  raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED)
595
- await self.set_next_or_previous_preset(connection, False)
596
- # TODO (low priority) inform other server of the change
594
+ await self.set_next_or_previous_preset(False)
595
+ if self.other_server_in_binaural_set:
596
+ await self.other_server_in_binaural_set.set_next_or_previous_preset(False)
597
597
 
598
598
  async def _on_set_previous_preset_synchronized_locally(
599
- self, connection: Connection, __value__: bytes
599
+ self, _: Connection, __value__: bytes
600
600
  ):
601
601
  if (
602
602
  self.server_features.preset_synchronization_support
603
- == PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_SUPPORTED
603
+ == PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
604
604
  ):
605
605
  raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED)
606
- await self.set_next_or_previous_preset(connection, True)
607
- # TODO (low priority) inform other server of the change
606
+ await self.set_next_or_previous_preset(True)
607
+ if self.other_server_in_binaural_set:
608
+ await self.other_server_in_binaural_set.set_next_or_previous_preset(True)
608
609
 
609
610
 
610
611
  # -----------------------------------------------------------------------------
@@ -19,7 +19,7 @@ from __future__ import annotations
19
19
  import dataclasses
20
20
  import enum
21
21
  import struct
22
- from typing import Any, List, Type
22
+ from typing import Any
23
23
  from typing_extensions import Self
24
24
 
25
25
  from bumble.profiles import bap
@@ -108,13 +108,13 @@ class Metadata:
108
108
  return self.data
109
109
 
110
110
  @classmethod
111
- def from_bytes(cls: Type[Self], data: bytes) -> Self:
111
+ def from_bytes(cls: type[Self], data: bytes) -> Self:
112
112
  return cls(tag=Metadata.Tag(data[0]), data=data[1:])
113
113
 
114
114
  def __bytes__(self) -> bytes:
115
115
  return bytes([len(self.data) + 1, self.tag]) + self.data
116
116
 
117
- entries: List[Entry] = dataclasses.field(default_factory=list)
117
+ entries: list[Entry] = dataclasses.field(default_factory=list)
118
118
 
119
119
  def pretty_print(self, indent: str) -> str:
120
120
  """Convenience method to generate a string with one key-value pair per line."""
@@ -140,7 +140,7 @@ class Metadata:
140
140
  )
141
141
 
142
142
  @classmethod
143
- def from_bytes(cls: Type[Self], data: bytes) -> Self:
143
+ def from_bytes(cls: type[Self], data: bytes) -> Self:
144
144
  entries = []
145
145
  offset = 0
146
146
  length = len(data)
bumble/profiles/mcp.py CHANGED
@@ -29,7 +29,7 @@ from bumble import gatt
29
29
  from bumble import gatt_client
30
30
  from bumble import utils
31
31
 
32
- from typing import Type, Optional, ClassVar, Dict, TYPE_CHECKING
32
+ from typing import Optional, ClassVar, TYPE_CHECKING
33
33
  from typing_extensions import Self
34
34
 
35
35
  # -----------------------------------------------------------------------------
@@ -167,7 +167,7 @@ class ObjectId(int):
167
167
  '''See Media Control Service 4.4.2. Object ID field.'''
168
168
 
169
169
  @classmethod
170
- def create_from_bytes(cls: Type[Self], data: bytes) -> Self:
170
+ def create_from_bytes(cls: type[Self], data: bytes) -> Self:
171
171
  return cls(int.from_bytes(data, byteorder='little', signed=False))
172
172
 
173
173
  def __bytes__(self) -> bytes:
@@ -182,7 +182,7 @@ class GroupObjectType:
182
182
  object_id: ObjectId
183
183
 
184
184
  @classmethod
185
- def from_bytes(cls: Type[Self], data: bytes) -> Self:
185
+ def from_bytes(cls: type[Self], data: bytes) -> Self:
186
186
  return cls(
187
187
  object_type=ObjectType(data[0]),
188
188
  object_id=ObjectId.create_from_bytes(data[1:]),
@@ -310,7 +310,7 @@ class MediaControlServiceProxy(
310
310
  ):
311
311
  SERVICE_CLASS = MediaControlService
312
312
 
313
- _CHARACTERISTICS: ClassVar[Dict[str, core.UUID]] = {
313
+ _CHARACTERISTICS: ClassVar[dict[str, core.UUID]] = {
314
314
  'media_player_name': gatt.GATT_MEDIA_PLAYER_NAME_CHARACTERISTIC,
315
315
  'media_player_icon_object_id': gatt.GATT_MEDIA_PLAYER_ICON_OBJECT_ID_CHARACTERISTIC,
316
316
  'media_player_icon_url': gatt.GATT_MEDIA_PLAYER_ICON_URL_CHARACTERISTIC,
bumble/profiles/vcs.py CHANGED
@@ -20,7 +20,7 @@ from __future__ import annotations
20
20
  import dataclasses
21
21
  import enum
22
22
 
23
- from typing import Optional, Sequence
23
+ from typing import Sequence
24
24
 
25
25
  from bumble import att
26
26
  from bumble import utils
@@ -161,10 +161,8 @@ class VolumeControlService(gatt.TemplateService):
161
161
  handler = getattr(self, '_on_' + opcode.name.lower())
162
162
  if handler(*value[2:]):
163
163
  self.change_counter = (self.change_counter + 1) % 256
164
- utils.cancel_on_event(
165
- connection,
166
- 'disconnection',
167
- connection.device.notify_subscribers(attribute=self.volume_state),
164
+ connection.cancel_on_disconnection(
165
+ connection.device.notify_subscribers(attribute=self.volume_state)
168
166
  )
169
167
  self.emit(self.EVENT_VOLUME_STATE_CHANGE)
170
168
 
bumble/rfcomm.py CHANGED
@@ -22,7 +22,7 @@ import asyncio
22
22
  import collections
23
23
  import dataclasses
24
24
  import enum
25
- from typing import Callable, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
25
+ from typing import Callable, Optional, Union, TYPE_CHECKING
26
26
  from typing_extensions import Self
27
27
 
28
28
 
@@ -123,7 +123,7 @@ RFCOMM_DYNAMIC_CHANNEL_NUMBER_END = 30
123
123
  # -----------------------------------------------------------------------------
124
124
  def make_service_sdp_records(
125
125
  service_record_handle: int, channel: int, uuid: Optional[UUID] = None
126
- ) -> List[sdp.ServiceAttribute]:
126
+ ) -> list[sdp.ServiceAttribute]:
127
127
  """
128
128
  Create SDP records for an RFComm service given a channel number and an
129
129
  optional UUID. A Service Class Attribute is included only if the UUID is not None.
@@ -169,7 +169,7 @@ def make_service_sdp_records(
169
169
 
170
170
 
171
171
  # -----------------------------------------------------------------------------
172
- async def find_rfcomm_channels(connection: Connection) -> Dict[int, List[UUID]]:
172
+ async def find_rfcomm_channels(connection: Connection) -> dict[int, list[UUID]]:
173
173
  """Searches all RFCOMM channels and their associated UUID from SDP service records.
174
174
 
175
175
  Args:
@@ -188,7 +188,7 @@ async def find_rfcomm_channels(connection: Connection) -> Dict[int, List[UUID]]:
188
188
  ],
189
189
  )
190
190
  for attribute_lists in search_result:
191
- service_classes: List[UUID] = []
191
+ service_classes: list[UUID] = []
192
192
  channel: Optional[int] = None
193
193
  for attribute in attribute_lists:
194
194
  # The layout is [[L2CAP_PROTOCOL], [RFCOMM_PROTOCOL, RFCOMM_CHANNEL]].
@@ -275,7 +275,7 @@ class RFCOMM_Frame:
275
275
  self.fcs = compute_fcs(bytes([self.address, self.control]) + self.length)
276
276
 
277
277
  @staticmethod
278
- def parse_mcc(data) -> Tuple[int, bool, bytes]:
278
+ def parse_mcc(data) -> tuple[int, bool, bytes]:
279
279
  mcc_type = data[0] >> 2
280
280
  c_r = bool((data[0] >> 1) & 1)
281
281
  length = data[1]
@@ -771,8 +771,8 @@ class Multiplexer(utils.EventEmitter):
771
771
  connection_result: Optional[asyncio.Future]
772
772
  disconnection_result: Optional[asyncio.Future]
773
773
  open_result: Optional[asyncio.Future]
774
- acceptor: Optional[Callable[[int], Optional[Tuple[int, int]]]]
775
- dlcs: Dict[int, DLC]
774
+ acceptor: Optional[Callable[[int], Optional[tuple[int, int]]]]
775
+ dlcs: dict[int, DLC]
776
776
 
777
777
  def __init__(self, l2cap_channel: l2cap.ClassicChannel, role: Role) -> None:
778
778
  super().__init__()
@@ -1088,8 +1088,8 @@ class Server(utils.EventEmitter):
1088
1088
  ) -> None:
1089
1089
  super().__init__()
1090
1090
  self.device = device
1091
- self.acceptors: Dict[int, Callable[[DLC], None]] = {}
1092
- self.dlc_configs: Dict[int, Tuple[int, int]] = {}
1091
+ self.acceptors: dict[int, Callable[[DLC], None]] = {}
1092
+ self.dlc_configs: dict[int, tuple[int, int]] = {}
1093
1093
 
1094
1094
  # Register ourselves with the L2CAP channel manager
1095
1095
  self.l2cap_server = device.create_l2cap_server(
@@ -1144,7 +1144,7 @@ class Server(utils.EventEmitter):
1144
1144
  # Notify
1145
1145
  self.emit(self.EVENT_START, multiplexer)
1146
1146
 
1147
- def accept_dlc(self, channel_number: int) -> Optional[Tuple[int, int]]:
1147
+ def accept_dlc(self, channel_number: int) -> Optional[tuple[int, int]]:
1148
1148
  return self.dlc_configs.get(channel_number)
1149
1149
 
1150
1150
  def on_dlc(self, dlc: DLC) -> None:
bumble/rtp.py CHANGED
@@ -17,7 +17,6 @@
17
17
  # -----------------------------------------------------------------------------
18
18
  from __future__ import annotations
19
19
  import struct
20
- from typing import List
21
20
 
22
21
 
23
22
  # -----------------------------------------------------------------------------
@@ -60,7 +59,7 @@ class MediaPacket:
60
59
  sequence_number: int,
61
60
  timestamp: int,
62
61
  ssrc: int,
63
- csrc_list: List[int],
62
+ csrc_list: list[int],
64
63
  payload_type: int,
65
64
  payload: bytes,
66
65
  ) -> None:
bumble/sdp.py CHANGED
@@ -19,7 +19,7 @@ from __future__ import annotations
19
19
  import asyncio
20
20
  import logging
21
21
  import struct
22
- from typing import Iterable, NewType, Optional, Union, Sequence, Type, TYPE_CHECKING
22
+ from typing import Iterable, NewType, Optional, Union, Sequence, TYPE_CHECKING
23
23
  from typing_extensions import Self
24
24
 
25
25
  from bumble import core, l2cap
@@ -547,7 +547,7 @@ class SDP_PDU:
547
547
  SDP_SERVICE_ATTRIBUTE_REQUEST: SDP_SERVICE_ATTRIBUTE_RESPONSE,
548
548
  SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST: SDP_SERVICE_SEARCH_ATTRIBUTE_RESPONSE,
549
549
  }
550
- sdp_pdu_classes: dict[int, Type[SDP_PDU]] = {}
550
+ sdp_pdu_classes: dict[int, type[SDP_PDU]] = {}
551
551
  name = None
552
552
  pdu_id = 0
553
553
 
bumble/smp.py CHANGED
@@ -26,18 +26,13 @@ from __future__ import annotations
26
26
  import logging
27
27
  import asyncio
28
28
  import enum
29
- import secrets
30
29
  from dataclasses import dataclass
31
30
  from typing import (
32
31
  TYPE_CHECKING,
33
32
  Any,
34
33
  Awaitable,
35
34
  Callable,
36
- Dict,
37
- List,
38
35
  Optional,
39
- Tuple,
40
- Type,
41
36
  cast,
42
37
  )
43
38
 
@@ -210,7 +205,7 @@ class SMP_Command:
210
205
  See Bluetooth spec @ Vol 3, Part H - 3 SECURITY MANAGER PROTOCOL
211
206
  '''
212
207
 
213
- smp_classes: Dict[int, Type[SMP_Command]] = {}
208
+ smp_classes: dict[int, type[SMP_Command]] = {}
214
209
  fields: Any
215
210
  code = 0
216
211
  name = ''
@@ -254,7 +249,7 @@ class SMP_Command:
254
249
 
255
250
  @staticmethod
256
251
  def key_distribution_str(value: int) -> str:
257
- key_types: List[str] = []
252
+ key_types: list[str] = []
258
253
  if value & SMP_ENC_KEY_DISTRIBUTION_FLAG:
259
254
  key_types.append('ENC')
260
255
  if value & SMP_ID_KEY_DISTRIBUTION_FLAG:
@@ -706,7 +701,7 @@ class Session:
706
701
  self.peer_identity_resolving_key = None
707
702
  self.peer_bd_addr: Optional[Address] = None
708
703
  self.peer_signature_key = None
709
- self.peer_expected_distributions: List[Type[SMP_Command]] = []
704
+ self.peer_expected_distributions: list[type[SMP_Command]] = []
710
705
  self.dh_key = b''
711
706
  self.confirm_value = None
712
707
  self.passkey: Optional[int] = None
@@ -767,7 +762,9 @@ class Session:
767
762
 
768
763
  # OOB
769
764
  self.oob_data_flag = (
770
- 1 if pairing_config.oob and pairing_config.oob.peer_data else 0
765
+ 1
766
+ if pairing_config.oob and (not self.sc or pairing_config.oob.peer_data)
767
+ else 0
771
768
  )
772
769
 
773
770
  # Set up addresses
@@ -814,7 +811,7 @@ class Session:
814
811
  self.tk = bytes(16)
815
812
 
816
813
  @property
817
- def pkx(self) -> Tuple[bytes, bytes]:
814
+ def pkx(self) -> tuple[bytes, bytes]:
818
815
  return (self.ecc_key.x[::-1], self.peer_public_key_x)
819
816
 
820
817
  @property
@@ -826,7 +823,7 @@ class Session:
826
823
  return self.pkx[0 if self.is_responder else 1]
827
824
 
828
825
  @property
829
- def nx(self) -> Tuple[bytes, bytes]:
826
+ def nx(self) -> tuple[bytes, bytes]:
830
827
  assert self.peer_random_value
831
828
  return (self.r, self.peer_random_value)
832
829
 
@@ -900,7 +897,7 @@ class Session:
900
897
 
901
898
  self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
902
899
 
903
- utils.cancel_on_event(self.connection, 'disconnection', prompt())
900
+ self.connection.cancel_on_disconnection(prompt())
904
901
 
905
902
  def prompt_user_for_numeric_comparison(
906
903
  self, code: int, next_steps: Callable[[], None]
@@ -919,7 +916,7 @@ class Session:
919
916
 
920
917
  self.send_pairing_failed(SMP_CONFIRM_VALUE_FAILED_ERROR)
921
918
 
922
- utils.cancel_on_event(self.connection, 'disconnection', prompt())
919
+ self.connection.cancel_on_disconnection(prompt())
923
920
 
924
921
  def prompt_user_for_number(self, next_steps: Callable[[int], None]) -> None:
925
922
  async def prompt() -> None:
@@ -936,12 +933,11 @@ class Session:
936
933
  logger.warning(f'exception while prompting: {error}')
937
934
  self.send_pairing_failed(SMP_PASSKEY_ENTRY_FAILED_ERROR)
938
935
 
939
- utils.cancel_on_event(self.connection, 'disconnection', prompt())
936
+ self.connection.cancel_on_disconnection(prompt())
940
937
 
941
- def display_passkey(self) -> None:
942
- # Generate random Passkey/PIN code
943
- self.passkey = secrets.randbelow(1000000)
944
- assert self.passkey is not None
938
+ async def display_passkey(self) -> None:
939
+ # Get the passkey value from the delegate
940
+ self.passkey = await self.pairing_config.delegate.generate_passkey()
945
941
  logger.debug(f'Pairing PIN CODE: {self.passkey:06}')
946
942
  self.passkey_ready.set()
947
943
 
@@ -950,14 +946,9 @@ class Session:
950
946
  self.tk = self.passkey.to_bytes(16, byteorder='little')
951
947
  logger.debug(f'TK from passkey = {self.tk.hex()}')
952
948
 
953
- try:
954
- utils.cancel_on_event(
955
- self.connection,
956
- 'disconnection',
957
- self.pairing_config.delegate.display_number(self.passkey, digits=6),
958
- )
959
- except Exception as error:
960
- logger.warning(f'exception while displaying number: {error}')
949
+ self.connection.cancel_on_disconnection(
950
+ self.pairing_config.delegate.display_number(self.passkey, digits=6)
951
+ )
961
952
 
962
953
  def input_passkey(self, next_steps: Optional[Callable[[], None]] = None) -> None:
963
954
  # Prompt the user for the passkey displayed on the peer
@@ -979,9 +970,16 @@ class Session:
979
970
  self, next_steps: Optional[Callable[[], None]] = None
980
971
  ) -> None:
981
972
  if self.passkey_display:
982
- self.display_passkey()
983
- if next_steps is not None:
984
- next_steps()
973
+
974
+ async def display_passkey():
975
+ await self.display_passkey()
976
+ if next_steps is not None:
977
+ next_steps()
978
+
979
+ try:
980
+ self.connection.cancel_on_disconnection(display_passkey())
981
+ except Exception as error:
982
+ logger.warning(f'exception while displaying passkey: {error}')
985
983
  else:
986
984
  self.input_passkey(next_steps)
987
985
 
@@ -1051,7 +1049,7 @@ class Session:
1051
1049
  )
1052
1050
 
1053
1051
  # Perform the next steps asynchronously in case we need to wait for input
1054
- utils.cancel_on_event(self.connection, 'disconnection', next_steps())
1052
+ self.connection.cancel_on_disconnection(next_steps())
1055
1053
  else:
1056
1054
  confirm_value = crypto.c1(
1057
1055
  self.tk,
@@ -1174,8 +1172,8 @@ class Session:
1174
1172
  self.connection.transport == PhysicalTransport.BR_EDR
1175
1173
  and self.initiator_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
1176
1174
  ):
1177
- self.ctkd_task = utils.cancel_on_event(
1178
- self.connection, 'disconnection', self.get_link_key_and_derive_ltk()
1175
+ self.ctkd_task = self.connection.cancel_on_disconnection(
1176
+ self.get_link_key_and_derive_ltk()
1179
1177
  )
1180
1178
  elif not self.sc:
1181
1179
  # Distribute the LTK, EDIV and RAND
@@ -1213,8 +1211,8 @@ class Session:
1213
1211
  self.connection.transport == PhysicalTransport.BR_EDR
1214
1212
  and self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
1215
1213
  ):
1216
- self.ctkd_task = utils.cancel_on_event(
1217
- self.connection, 'disconnection', self.get_link_key_and_derive_ltk()
1214
+ self.ctkd_task = self.connection.cancel_on_disconnection(
1215
+ self.get_link_key_and_derive_ltk()
1218
1216
  )
1219
1217
  # Distribute the LTK, EDIV and RAND
1220
1218
  elif not self.sc:
@@ -1269,7 +1267,7 @@ class Session:
1269
1267
  f'{[c.__name__ for c in self.peer_expected_distributions]}'
1270
1268
  )
1271
1269
 
1272
- def check_key_distribution(self, command_class: Type[SMP_Command]) -> None:
1270
+ def check_key_distribution(self, command_class: type[SMP_Command]) -> None:
1273
1271
  # First, check that the connection is encrypted
1274
1272
  if not self.connection.is_encrypted:
1275
1273
  logger.warning(
@@ -1306,9 +1304,7 @@ class Session:
1306
1304
 
1307
1305
  # Wait for the pairing process to finish
1308
1306
  assert self.pairing_result
1309
- await utils.cancel_on_event(
1310
- self.connection, 'disconnection', self.pairing_result
1311
- )
1307
+ await self.connection.cancel_on_disconnection(self.pairing_result)
1312
1308
 
1313
1309
  def on_disconnection(self, _: int) -> None:
1314
1310
  self.connection.remove_listener(
@@ -1329,7 +1325,7 @@ class Session:
1329
1325
  if self.is_initiator:
1330
1326
  self.distribute_keys()
1331
1327
 
1332
- utils.cancel_on_event(self.connection, 'disconnection', self.on_pairing())
1328
+ self.connection.cancel_on_disconnection(self.on_pairing())
1333
1329
 
1334
1330
  def on_connection_encryption_change(self) -> None:
1335
1331
  if self.connection.is_encrypted and not self.completed:
@@ -1440,10 +1436,8 @@ class Session:
1440
1436
  def on_smp_pairing_request_command(
1441
1437
  self, command: SMP_Pairing_Request_Command
1442
1438
  ) -> None:
1443
- utils.cancel_on_event(
1444
- self.connection,
1445
- 'disconnection',
1446
- self.on_smp_pairing_request_command_async(command),
1439
+ self.connection.cancel_on_disconnection(
1440
+ self.on_smp_pairing_request_command_async(command)
1447
1441
  )
1448
1442
 
1449
1443
  async def on_smp_pairing_request_command_async(
@@ -1507,7 +1501,7 @@ class Session:
1507
1501
  # Display a passkey if we need to
1508
1502
  if not self.sc:
1509
1503
  if self.pairing_method == PairingMethod.PASSKEY and self.passkey_display:
1510
- self.display_passkey()
1504
+ await self.display_passkey()
1511
1505
 
1512
1506
  # Respond
1513
1507
  self.send_pairing_response_command()
@@ -1577,11 +1571,12 @@ class Session:
1577
1571
  if self.pairing_method == PairingMethod.CTKD_OVER_CLASSIC:
1578
1572
  # Authentication is already done in SMP, so remote shall start keys distribution immediately
1579
1573
  return
1580
- elif self.sc:
1581
- if self.pairing_method == PairingMethod.PASSKEY:
1582
- self.display_or_input_passkey()
1583
1574
 
1575
+ if self.sc:
1584
1576
  self.send_public_key_command()
1577
+
1578
+ if self.pairing_method == PairingMethod.PASSKEY:
1579
+ self.display_or_input_passkey()
1585
1580
  else:
1586
1581
  if self.pairing_method == PairingMethod.PASSKEY:
1587
1582
  self.display_or_input_passkey(self.send_pairing_confirm_command)
@@ -1689,7 +1684,7 @@ class Session:
1689
1684
  ):
1690
1685
  return
1691
1686
  elif self.pairing_method == PairingMethod.PASSKEY:
1692
- assert self.passkey and self.confirm_value
1687
+ assert self.passkey is not None and self.confirm_value is not None
1693
1688
  # Check that the random value matches what was committed to earlier
1694
1689
  confirm_verifier = crypto.f4(
1695
1690
  self.pkb,
@@ -1718,7 +1713,7 @@ class Session:
1718
1713
  ):
1719
1714
  self.send_pairing_random_command()
1720
1715
  elif self.pairing_method == PairingMethod.PASSKEY:
1721
- assert self.passkey and self.confirm_value
1716
+ assert self.passkey is not None and self.confirm_value is not None
1722
1717
  # Check that the random value matches what was committed to earlier
1723
1718
  confirm_verifier = crypto.f4(
1724
1719
  self.pka,
@@ -1755,7 +1750,7 @@ class Session:
1755
1750
  ra = bytes(16)
1756
1751
  rb = ra
1757
1752
  elif self.pairing_method == PairingMethod.PASSKEY:
1758
- assert self.passkey
1753
+ assert self.passkey is not None
1759
1754
  ra = self.passkey.to_bytes(16, byteorder='little')
1760
1755
  rb = ra
1761
1756
  elif self.pairing_method == PairingMethod.OOB:
@@ -1854,19 +1849,23 @@ class Session:
1854
1849
  elif self.pairing_method == PairingMethod.PASSKEY:
1855
1850
  self.send_pairing_confirm_command()
1856
1851
  else:
1857
- if self.pairing_method == PairingMethod.PASSKEY:
1858
- self.display_or_input_passkey()
1859
-
1860
1852
  # Send our public key back to the initiator
1861
1853
  self.send_public_key_command()
1862
1854
 
1863
- if self.pairing_method in (
1864
- PairingMethod.JUST_WORKS,
1865
- PairingMethod.NUMERIC_COMPARISON,
1866
- PairingMethod.OOB,
1867
- ):
1868
- # We can now send the confirmation value
1869
- self.send_pairing_confirm_command()
1855
+ def next_steps() -> None:
1856
+
1857
+ if self.pairing_method in (
1858
+ PairingMethod.JUST_WORKS,
1859
+ PairingMethod.NUMERIC_COMPARISON,
1860
+ PairingMethod.OOB,
1861
+ ):
1862
+ # We can now send the confirmation value
1863
+ self.send_pairing_confirm_command()
1864
+
1865
+ if self.pairing_method == PairingMethod.PASSKEY:
1866
+ self.display_or_input_passkey(next_steps)
1867
+ else:
1868
+ next_steps()
1870
1869
 
1871
1870
  def on_smp_pairing_dhkey_check_command(
1872
1871
  self, command: SMP_Pairing_DHKey_Check_Command
@@ -1888,7 +1887,7 @@ class Session:
1888
1887
  self.wait_before_continuing = None
1889
1888
  self.send_pairing_dhkey_check_command()
1890
1889
 
1891
- utils.cancel_on_event(self.connection, 'disconnection', next_steps())
1890
+ self.connection.cancel_on_disconnection(next_steps())
1892
1891
  else:
1893
1892
  self.send_pairing_dhkey_check_command()
1894
1893
  else:
@@ -1938,9 +1937,9 @@ class Manager(utils.EventEmitter):
1938
1937
  '''
1939
1938
 
1940
1939
  device: Device
1941
- sessions: Dict[int, Session]
1940
+ sessions: dict[int, Session]
1942
1941
  pairing_config_factory: Callable[[Connection], PairingConfig]
1943
- session_proxy: Type[Session]
1942
+ session_proxy: type[Session]
1944
1943
  _ecc_key: Optional[crypto.EccKey]
1945
1944
 
1946
1945
  def __init__(