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/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,
|
|
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:
|
|
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:
|
|
236
|
+
preset_records: dict[int, PresetRecord] # key is the preset index
|
|
237
237
|
read_presets_request_in_progress: bool
|
|
238
238
|
|
|
239
|
-
|
|
240
|
-
|
|
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:
|
|
246
|
+
currently_connected_clients: set[Connection]
|
|
245
247
|
|
|
246
248
|
def __init__(
|
|
247
|
-
self, device: Device, features: HearingAidFeatures, presets:
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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,
|
|
534
|
-
await self.set_active_preset(
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
575
|
+
self, _: Connection, value: bytes
|
|
578
576
|
):
|
|
579
577
|
if (
|
|
580
578
|
self.server_features.preset_synchronization_support
|
|
581
|
-
== PresetSynchronizationSupport.
|
|
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(
|
|
585
|
-
|
|
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,
|
|
587
|
+
self, _: Connection, __value__: bytes
|
|
589
588
|
):
|
|
590
589
|
if (
|
|
591
590
|
self.server_features.preset_synchronization_support
|
|
592
|
-
== PresetSynchronizationSupport.
|
|
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(
|
|
596
|
-
|
|
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,
|
|
599
|
+
self, _: Connection, __value__: bytes
|
|
600
600
|
):
|
|
601
601
|
if (
|
|
602
602
|
self.server_features.preset_synchronization_support
|
|
603
|
-
== PresetSynchronizationSupport.
|
|
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(
|
|
607
|
-
|
|
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
|
# -----------------------------------------------------------------------------
|
bumble/profiles/le_audio.py
CHANGED
|
@@ -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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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[
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
) ->
|
|
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) ->
|
|
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:
|
|
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) ->
|
|
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[
|
|
775
|
-
dlcs:
|
|
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:
|
|
1092
|
-
self.dlc_configs:
|
|
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[
|
|
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:
|
|
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,
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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) ->
|
|
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) ->
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
936
|
+
self.connection.cancel_on_disconnection(prompt())
|
|
940
937
|
|
|
941
|
-
def display_passkey(self) -> None:
|
|
942
|
-
#
|
|
943
|
-
self.passkey =
|
|
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,7 @@ 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
|
-
|
|
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
|
+
await self.pairing_config.delegate.display_number(self.passkey, digits=6)
|
|
961
950
|
|
|
962
951
|
def input_passkey(self, next_steps: Optional[Callable[[], None]] = None) -> None:
|
|
963
952
|
# Prompt the user for the passkey displayed on the peer
|
|
@@ -979,9 +968,16 @@ class Session:
|
|
|
979
968
|
self, next_steps: Optional[Callable[[], None]] = None
|
|
980
969
|
) -> None:
|
|
981
970
|
if self.passkey_display:
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
971
|
+
|
|
972
|
+
async def display_passkey():
|
|
973
|
+
await self.display_passkey()
|
|
974
|
+
if next_steps is not None:
|
|
975
|
+
next_steps()
|
|
976
|
+
|
|
977
|
+
try:
|
|
978
|
+
self.connection.cancel_on_disconnection(display_passkey())
|
|
979
|
+
except Exception as error:
|
|
980
|
+
logger.warning(f'exception while displaying passkey: {error}')
|
|
985
981
|
else:
|
|
986
982
|
self.input_passkey(next_steps)
|
|
987
983
|
|
|
@@ -1051,7 +1047,7 @@ class Session:
|
|
|
1051
1047
|
)
|
|
1052
1048
|
|
|
1053
1049
|
# Perform the next steps asynchronously in case we need to wait for input
|
|
1054
|
-
|
|
1050
|
+
self.connection.cancel_on_disconnection(next_steps())
|
|
1055
1051
|
else:
|
|
1056
1052
|
confirm_value = crypto.c1(
|
|
1057
1053
|
self.tk,
|
|
@@ -1174,8 +1170,8 @@ class Session:
|
|
|
1174
1170
|
self.connection.transport == PhysicalTransport.BR_EDR
|
|
1175
1171
|
and self.initiator_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
|
|
1176
1172
|
):
|
|
1177
|
-
self.ctkd_task =
|
|
1178
|
-
self.
|
|
1173
|
+
self.ctkd_task = self.connection.cancel_on_disconnection(
|
|
1174
|
+
self.get_link_key_and_derive_ltk()
|
|
1179
1175
|
)
|
|
1180
1176
|
elif not self.sc:
|
|
1181
1177
|
# Distribute the LTK, EDIV and RAND
|
|
@@ -1213,8 +1209,8 @@ class Session:
|
|
|
1213
1209
|
self.connection.transport == PhysicalTransport.BR_EDR
|
|
1214
1210
|
and self.responder_key_distribution & SMP_ENC_KEY_DISTRIBUTION_FLAG
|
|
1215
1211
|
):
|
|
1216
|
-
self.ctkd_task =
|
|
1217
|
-
self.
|
|
1212
|
+
self.ctkd_task = self.connection.cancel_on_disconnection(
|
|
1213
|
+
self.get_link_key_and_derive_ltk()
|
|
1218
1214
|
)
|
|
1219
1215
|
# Distribute the LTK, EDIV and RAND
|
|
1220
1216
|
elif not self.sc:
|
|
@@ -1269,7 +1265,7 @@ class Session:
|
|
|
1269
1265
|
f'{[c.__name__ for c in self.peer_expected_distributions]}'
|
|
1270
1266
|
)
|
|
1271
1267
|
|
|
1272
|
-
def check_key_distribution(self, command_class:
|
|
1268
|
+
def check_key_distribution(self, command_class: type[SMP_Command]) -> None:
|
|
1273
1269
|
# First, check that the connection is encrypted
|
|
1274
1270
|
if not self.connection.is_encrypted:
|
|
1275
1271
|
logger.warning(
|
|
@@ -1306,9 +1302,7 @@ class Session:
|
|
|
1306
1302
|
|
|
1307
1303
|
# Wait for the pairing process to finish
|
|
1308
1304
|
assert self.pairing_result
|
|
1309
|
-
await
|
|
1310
|
-
self.connection, 'disconnection', self.pairing_result
|
|
1311
|
-
)
|
|
1305
|
+
await self.connection.cancel_on_disconnection(self.pairing_result)
|
|
1312
1306
|
|
|
1313
1307
|
def on_disconnection(self, _: int) -> None:
|
|
1314
1308
|
self.connection.remove_listener(
|
|
@@ -1329,7 +1323,7 @@ class Session:
|
|
|
1329
1323
|
if self.is_initiator:
|
|
1330
1324
|
self.distribute_keys()
|
|
1331
1325
|
|
|
1332
|
-
|
|
1326
|
+
self.connection.cancel_on_disconnection(self.on_pairing())
|
|
1333
1327
|
|
|
1334
1328
|
def on_connection_encryption_change(self) -> None:
|
|
1335
1329
|
if self.connection.is_encrypted and not self.completed:
|
|
@@ -1440,10 +1434,8 @@ class Session:
|
|
|
1440
1434
|
def on_smp_pairing_request_command(
|
|
1441
1435
|
self, command: SMP_Pairing_Request_Command
|
|
1442
1436
|
) -> None:
|
|
1443
|
-
|
|
1444
|
-
self.
|
|
1445
|
-
'disconnection',
|
|
1446
|
-
self.on_smp_pairing_request_command_async(command),
|
|
1437
|
+
self.connection.cancel_on_disconnection(
|
|
1438
|
+
self.on_smp_pairing_request_command_async(command)
|
|
1447
1439
|
)
|
|
1448
1440
|
|
|
1449
1441
|
async def on_smp_pairing_request_command_async(
|
|
@@ -1507,7 +1499,7 @@ class Session:
|
|
|
1507
1499
|
# Display a passkey if we need to
|
|
1508
1500
|
if not self.sc:
|
|
1509
1501
|
if self.pairing_method == PairingMethod.PASSKEY and self.passkey_display:
|
|
1510
|
-
self.display_passkey()
|
|
1502
|
+
await self.display_passkey()
|
|
1511
1503
|
|
|
1512
1504
|
# Respond
|
|
1513
1505
|
self.send_pairing_response_command()
|
|
@@ -1689,7 +1681,7 @@ class Session:
|
|
|
1689
1681
|
):
|
|
1690
1682
|
return
|
|
1691
1683
|
elif self.pairing_method == PairingMethod.PASSKEY:
|
|
1692
|
-
assert self.passkey and self.confirm_value
|
|
1684
|
+
assert self.passkey is not None and self.confirm_value is not None
|
|
1693
1685
|
# Check that the random value matches what was committed to earlier
|
|
1694
1686
|
confirm_verifier = crypto.f4(
|
|
1695
1687
|
self.pkb,
|
|
@@ -1718,7 +1710,7 @@ class Session:
|
|
|
1718
1710
|
):
|
|
1719
1711
|
self.send_pairing_random_command()
|
|
1720
1712
|
elif self.pairing_method == PairingMethod.PASSKEY:
|
|
1721
|
-
assert self.passkey and self.confirm_value
|
|
1713
|
+
assert self.passkey is not None and self.confirm_value is not None
|
|
1722
1714
|
# Check that the random value matches what was committed to earlier
|
|
1723
1715
|
confirm_verifier = crypto.f4(
|
|
1724
1716
|
self.pka,
|
|
@@ -1755,7 +1747,7 @@ class Session:
|
|
|
1755
1747
|
ra = bytes(16)
|
|
1756
1748
|
rb = ra
|
|
1757
1749
|
elif self.pairing_method == PairingMethod.PASSKEY:
|
|
1758
|
-
assert self.passkey
|
|
1750
|
+
assert self.passkey is not None
|
|
1759
1751
|
ra = self.passkey.to_bytes(16, byteorder='little')
|
|
1760
1752
|
rb = ra
|
|
1761
1753
|
elif self.pairing_method == PairingMethod.OOB:
|
|
@@ -1854,19 +1846,23 @@ class Session:
|
|
|
1854
1846
|
elif self.pairing_method == PairingMethod.PASSKEY:
|
|
1855
1847
|
self.send_pairing_confirm_command()
|
|
1856
1848
|
else:
|
|
1857
|
-
if self.pairing_method == PairingMethod.PASSKEY:
|
|
1858
|
-
self.display_or_input_passkey()
|
|
1859
1849
|
|
|
1860
|
-
|
|
1861
|
-
|
|
1850
|
+
def next_steps() -> None:
|
|
1851
|
+
# Send our public key back to the initiator
|
|
1852
|
+
self.send_public_key_command()
|
|
1862
1853
|
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1854
|
+
if self.pairing_method in (
|
|
1855
|
+
PairingMethod.JUST_WORKS,
|
|
1856
|
+
PairingMethod.NUMERIC_COMPARISON,
|
|
1857
|
+
PairingMethod.OOB,
|
|
1858
|
+
):
|
|
1859
|
+
# We can now send the confirmation value
|
|
1860
|
+
self.send_pairing_confirm_command()
|
|
1861
|
+
|
|
1862
|
+
if self.pairing_method == PairingMethod.PASSKEY:
|
|
1863
|
+
self.display_or_input_passkey(next_steps)
|
|
1864
|
+
else:
|
|
1865
|
+
next_steps()
|
|
1870
1866
|
|
|
1871
1867
|
def on_smp_pairing_dhkey_check_command(
|
|
1872
1868
|
self, command: SMP_Pairing_DHKey_Check_Command
|
|
@@ -1888,7 +1884,7 @@ class Session:
|
|
|
1888
1884
|
self.wait_before_continuing = None
|
|
1889
1885
|
self.send_pairing_dhkey_check_command()
|
|
1890
1886
|
|
|
1891
|
-
|
|
1887
|
+
self.connection.cancel_on_disconnection(next_steps())
|
|
1892
1888
|
else:
|
|
1893
1889
|
self.send_pairing_dhkey_check_command()
|
|
1894
1890
|
else:
|
|
@@ -1938,9 +1934,9 @@ class Manager(utils.EventEmitter):
|
|
|
1938
1934
|
'''
|
|
1939
1935
|
|
|
1940
1936
|
device: Device
|
|
1941
|
-
sessions:
|
|
1937
|
+
sessions: dict[int, Session]
|
|
1942
1938
|
pairing_config_factory: Callable[[Connection], PairingConfig]
|
|
1943
|
-
session_proxy:
|
|
1939
|
+
session_proxy: type[Session]
|
|
1944
1940
|
_ecc_key: Optional[crypto.EccKey]
|
|
1945
1941
|
|
|
1946
1942
|
def __init__(
|
bumble/tools/rtk_util.py
CHANGED
|
@@ -50,7 +50,7 @@ def do_parse(firmware_path):
|
|
|
50
50
|
|
|
51
51
|
# -----------------------------------------------------------------------------
|
|
52
52
|
async def do_load(usb_transport, force):
|
|
53
|
-
async with await transport.
|
|
53
|
+
async with await transport.open_transport(usb_transport) as (
|
|
54
54
|
hci_source,
|
|
55
55
|
hci_sink,
|
|
56
56
|
):
|
|
@@ -69,7 +69,7 @@ async def do_load(usb_transport, force):
|
|
|
69
69
|
|
|
70
70
|
# -----------------------------------------------------------------------------
|
|
71
71
|
async def do_drop(usb_transport):
|
|
72
|
-
async with await transport.
|
|
72
|
+
async with await transport.open_transport(usb_transport) as (
|
|
73
73
|
hci_source,
|
|
74
74
|
hci_sink,
|
|
75
75
|
):
|