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.
- bumble/_version.py +2 -2
- bumble/a2dp.py +6 -0
- bumble/apps/README.md +0 -3
- bumble/apps/auracast.py +14 -11
- bumble/apps/bench.py +482 -37
- bumble/apps/console.py +3 -3
- bumble/apps/controller_info.py +44 -12
- bumble/apps/controller_loopback.py +7 -7
- bumble/apps/controllers.py +4 -5
- bumble/apps/device_info.py +4 -5
- bumble/apps/gatt_dump.py +5 -5
- bumble/apps/gg_bridge.py +5 -5
- bumble/apps/hci_bridge.py +5 -4
- bumble/apps/l2cap_bridge.py +5 -5
- bumble/apps/lea_unicast/app.py +8 -3
- bumble/apps/pair.py +19 -11
- bumble/apps/pandora_server.py +2 -2
- bumble/apps/player/player.py +2 -3
- bumble/apps/rfcomm_bridge.py +3 -4
- bumble/apps/scan.py +4 -5
- bumble/apps/show.py +6 -4
- bumble/apps/speaker/speaker.html +1 -0
- bumble/apps/speaker/speaker.js +113 -62
- bumble/apps/speaker/speaker.py +123 -19
- bumble/apps/unbond.py +2 -3
- bumble/apps/usb_probe.py +2 -3
- 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 +42 -54
- bumble/colors.py +2 -2
- bumble/controller.py +174 -45
- bumble/device.py +398 -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 +2648 -2909
- bumble/helpers.py +4 -5
- bumble/hfp.py +32 -37
- bumble/host.py +104 -35
- bumble/keys.py +5 -5
- bumble/l2cap.py +312 -409
- bumble/link.py +16 -280
- bumble/logging.py +65 -0
- bumble/pairing.py +23 -20
- bumble/pandora/__init__.py +2 -2
- 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/ams.py +404 -0
- bumble/profiles/ascs.py +142 -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 +62 -63
- bumble/tools/intel_util.py +3 -2
- bumble/tools/rtk_util.py +6 -5
- 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.214.dist-info}/METADATA +4 -3
- {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/RECORD +89 -90
- {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/WHEEL +1 -1
- {bumble-0.0.212.dist-info → bumble-0.0.214.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.214.dist-info}/licenses/LICENSE +0 -0
- {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,
|
|
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,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
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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
|
-
|
|
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 =
|
|
1178
|
-
self.
|
|
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 =
|
|
1217
|
-
self.
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1444
|
-
self.
|
|
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
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
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
|
-
|
|
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:
|
|
1940
|
+
sessions: dict[int, Session]
|
|
1942
1941
|
pairing_config_factory: Callable[[Connection], PairingConfig]
|
|
1943
|
-
session_proxy:
|
|
1942
|
+
session_proxy: type[Session]
|
|
1944
1943
|
_ecc_key: Optional[crypto.EccKey]
|
|
1945
1944
|
|
|
1946
1945
|
def __init__(
|