bumble 0.0.219__py3-none-any.whl → 0.0.221__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 +5 -5
- bumble/apps/auracast.py +746 -479
- bumble/apps/bench.py +4 -5
- bumble/apps/console.py +5 -10
- bumble/apps/controller_info.py +12 -7
- bumble/apps/controller_loopback.py +1 -2
- bumble/apps/device_info.py +2 -3
- bumble/apps/gatt_dump.py +0 -1
- bumble/apps/lea_unicast/app.py +1 -1
- bumble/apps/pair.py +49 -46
- bumble/apps/pandora_server.py +2 -2
- bumble/apps/player/player.py +10 -12
- bumble/apps/rfcomm_bridge.py +10 -11
- bumble/apps/scan.py +1 -3
- bumble/apps/speaker/speaker.py +3 -4
- bumble/at.py +4 -5
- bumble/att.py +91 -25
- bumble/audio/io.py +8 -6
- bumble/avc.py +1 -2
- bumble/avctp.py +2 -3
- bumble/avdtp.py +53 -57
- bumble/avrcp.py +25 -27
- bumble/codecs.py +15 -15
- bumble/colors.py +7 -8
- bumble/controller.py +1201 -643
- bumble/core.py +41 -49
- bumble/crypto/__init__.py +2 -1
- bumble/crypto/builtin.py +2 -8
- bumble/data_types.py +2 -1
- bumble/decoder.py +2 -3
- bumble/device.py +278 -325
- bumble/drivers/__init__.py +3 -2
- bumble/drivers/intel.py +6 -8
- bumble/drivers/rtk.py +1 -1
- bumble/gatt.py +9 -9
- bumble/gatt_adapters.py +6 -6
- bumble/gatt_client.py +110 -60
- bumble/gatt_server.py +209 -139
- bumble/hci.py +87 -74
- bumble/helpers.py +5 -5
- bumble/hfp.py +27 -26
- bumble/hid.py +9 -9
- bumble/host.py +44 -50
- bumble/keys.py +17 -17
- bumble/l2cap.py +1015 -218
- bumble/link.py +54 -284
- bumble/ll.py +200 -0
- bumble/lmp.py +324 -0
- bumble/pairing.py +14 -15
- bumble/pandora/__init__.py +2 -2
- bumble/pandora/device.py +6 -4
- bumble/pandora/host.py +19 -10
- bumble/pandora/l2cap.py +8 -9
- bumble/pandora/security.py +18 -16
- bumble/pandora/utils.py +4 -4
- bumble/profiles/aics.py +6 -8
- bumble/profiles/ams.py +3 -5
- bumble/profiles/ancs.py +11 -11
- bumble/profiles/ascs.py +5 -5
- bumble/profiles/asha.py +10 -9
- bumble/profiles/bass.py +9 -3
- bumble/profiles/battery_service.py +1 -2
- bumble/profiles/csip.py +9 -10
- bumble/profiles/device_information_service.py +16 -17
- bumble/profiles/gap.py +3 -4
- bumble/profiles/gatt_service.py +0 -1
- bumble/profiles/gmap.py +12 -13
- bumble/profiles/hap.py +3 -3
- bumble/profiles/heart_rate_service.py +7 -8
- bumble/profiles/le_audio.py +1 -1
- bumble/profiles/mcp.py +28 -28
- bumble/profiles/pacs.py +13 -17
- bumble/profiles/pbp.py +16 -0
- bumble/profiles/vcs.py +2 -2
- bumble/profiles/vocs.py +6 -9
- bumble/rfcomm.py +19 -18
- bumble/sdp.py +12 -11
- bumble/smp.py +20 -30
- bumble/snoop.py +12 -5
- bumble/tools/generate_company_id_list.py +1 -1
- bumble/tools/intel_util.py +2 -2
- bumble/tools/rtk_fw_download.py +1 -1
- bumble/tools/rtk_util.py +1 -1
- bumble/transport/__init__.py +1 -2
- bumble/transport/android_emulator.py +2 -3
- bumble/transport/android_netsim.py +49 -40
- bumble/transport/common.py +9 -9
- bumble/transport/file.py +1 -2
- bumble/transport/hci_socket.py +2 -3
- bumble/transport/pty.py +3 -5
- bumble/transport/pyusb.py +8 -5
- bumble/transport/serial.py +1 -2
- bumble/transport/vhci.py +1 -2
- bumble/transport/ws_server.py +2 -3
- bumble/utils.py +23 -14
- bumble/vendor/android/hci.py +4 -2
- {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/METADATA +4 -3
- bumble-0.0.221.dist-info/RECORD +185 -0
- bumble-0.0.219.dist-info/RECORD +0 -183
- {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/WHEEL +0 -0
- {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/licenses/LICENSE +0 -0
- {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/top_level.txt +0 -0
bumble/lmp.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# Copyright 2021-2025 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
# -----------------------------------------------------------------------------
|
|
16
|
+
# Imports
|
|
17
|
+
# -----------------------------------------------------------------------------
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import struct
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import TypeVar
|
|
23
|
+
|
|
24
|
+
from bumble import hci, utils
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Opcode(utils.OpenIntEnum):
|
|
28
|
+
'''
|
|
29
|
+
See Bluetooth spec @ Vol 2, Part C - 5.1 PDU summary.
|
|
30
|
+
|
|
31
|
+
Follow the alphabetical order defined there.
|
|
32
|
+
'''
|
|
33
|
+
|
|
34
|
+
# fmt: off
|
|
35
|
+
LMP_ACCEPTED = 3
|
|
36
|
+
LMP_ACCEPTED_EXT = 127 << 8 + 1
|
|
37
|
+
LMP_AU_RAND = 11
|
|
38
|
+
LMP_AUTO_RATE = 35
|
|
39
|
+
LMP_CHANNEL_CLASSIFICATION = 127 << 8 + 17
|
|
40
|
+
LMP_CHANNEL_CLASSIFICATION_REQ = 127 << 8 + 16
|
|
41
|
+
LMP_CLK_ADJ = 127 << 8 + 5
|
|
42
|
+
LMP_CLK_ADJ_ACK = 127 << 8 + 6
|
|
43
|
+
LMP_CLK_ADJ_REQ = 127 << 8 + 7
|
|
44
|
+
LMP_CLKOFFSET_REQ = 5
|
|
45
|
+
LMP_CLKOFFSET_RES = 6
|
|
46
|
+
LMP_COMB_KEY = 9
|
|
47
|
+
LMP_DECR_POWER_REQ = 32
|
|
48
|
+
LMP_DETACH = 7
|
|
49
|
+
LMP_DHKEY_CHECK = 65
|
|
50
|
+
LMP_ENCAPSULATED_HEADER = 61
|
|
51
|
+
LMP_ENCAPSULATED_PAYLOAD = 62
|
|
52
|
+
LMP_ENCRYPTION_KEY_SIZE_MASK_REQ= 58
|
|
53
|
+
LMP_ENCRYPTION_KEY_SIZE_MASK_RES= 59
|
|
54
|
+
LMP_ENCRYPTION_KEY_SIZE_REQ = 16
|
|
55
|
+
LMP_ENCRYPTION_MODE_REQ = 15
|
|
56
|
+
LMP_ESCO_LINK_REQ = 127 << 8 + 12
|
|
57
|
+
LMP_FEATURES_REQ = 39
|
|
58
|
+
LMP_FEATURES_REQ_EXT = 127 << 8 + 3
|
|
59
|
+
LMP_FEATURES_RES = 40
|
|
60
|
+
LMP_FEATURES_RES_EXT = 127 << 8 + 4
|
|
61
|
+
LMP_HOLD = 20
|
|
62
|
+
LMP_HOLD_REQ = 21
|
|
63
|
+
LMP_HOST_CONNECTION_REQ = 51
|
|
64
|
+
LMP_IN_RAND = 8
|
|
65
|
+
LMP_INCR_POWER_REQ = 31
|
|
66
|
+
LMP_IO_CAPABILITY_REQ = 127 << 8 + 25
|
|
67
|
+
LMP_IO_CAPABILITY_RES = 127 << 8 + 26
|
|
68
|
+
LMP_KEYPRESS_NOTIFICATION = 127 << 8 + 30
|
|
69
|
+
LMP_MAX_POWER = 33
|
|
70
|
+
LMP_MAX_SLOT = 45
|
|
71
|
+
LMP_MAX_SLOT_REQ = 46
|
|
72
|
+
LMP_MIN_POWER = 34
|
|
73
|
+
LMP_NAME_REQ = 1
|
|
74
|
+
LMP_NAME_RES = 2
|
|
75
|
+
LMP_NOT_ACCEPTED = 4
|
|
76
|
+
LMP_NOT_ACCEPTED_EXT = 127 << 8 + 2
|
|
77
|
+
LMP_NUMERIC_COMPARISON_FAILED = 127 << 8 + 27
|
|
78
|
+
LMP_OOB_FAILED = 127 << 8 + 29
|
|
79
|
+
LMP_PACKET_TYPE_TABLE_REQ = 127 << 8 + 11
|
|
80
|
+
LMP_PAGE_MODE_REQ = 53
|
|
81
|
+
LMP_PAGE_SCAN_MODE_REQ = 54
|
|
82
|
+
LMP_PASSKEY_FAILED = 127 << 8 + 28
|
|
83
|
+
LMP_PAUSE_ENCRYPTION_AES_REQ = 66
|
|
84
|
+
LMP_PAUSE_ENCRYPTION_REQ = 127 << 8 + 23
|
|
85
|
+
LMP_PING_REQ = 127 << 8 + 33
|
|
86
|
+
LMP_PING_RES = 127 << 8 + 34
|
|
87
|
+
LMP_POWER_CONTROL_REQ = 127 << 8 + 31
|
|
88
|
+
LMP_POWER_CONTROL_RES = 127 << 8 + 32
|
|
89
|
+
LMP_PREFERRED_RATE = 36
|
|
90
|
+
LMP_QUALITY_OF_SERVICE = 41
|
|
91
|
+
LMP_QUALITY_OF_SERVICE_REQ = 42
|
|
92
|
+
LMP_REMOVE_ESCO_LINK_REQ = 127 << 8 + 13
|
|
93
|
+
LMP_REMOVE_SCO_LINK_REQ = 44
|
|
94
|
+
LMP_RESUME_ENCRYPTION_REQ = 127 << 8 + 24
|
|
95
|
+
LMP_SAM_DEFINE_MAP = 127 << 8 + 36
|
|
96
|
+
LMP_SAM_SET_TYPE0 = 127 << 8 + 35
|
|
97
|
+
LMP_SAM_SWITCH = 127 << 8 + 37
|
|
98
|
+
LMP_SCO_LINK_REQ = 43
|
|
99
|
+
LMP_SET_AFH = 60
|
|
100
|
+
LMP_SETUP_COMPLETE = 49
|
|
101
|
+
LMP_SIMPLE_PAIRING_CONFIRM = 63
|
|
102
|
+
LMP_SIMPLE_PAIRING_NUMBER = 64
|
|
103
|
+
LMP_SLOT_OFFSET = 52
|
|
104
|
+
LMP_SNIFF_REQ = 23
|
|
105
|
+
LMP_SNIFF_SUBRATING_REQ = 127 << 8 + 21
|
|
106
|
+
LMP_SNIFF_SUBRATING_RES = 127 << 8 + 22
|
|
107
|
+
LMP_SRES = 12
|
|
108
|
+
LMP_START_ENCRYPTION_REQ = 17
|
|
109
|
+
LMP_STOP_ENCRYPTION_REQ = 18
|
|
110
|
+
LMP_SUPERVISION_TIMEOUT = 55
|
|
111
|
+
LMP_SWITCH_REQ = 19
|
|
112
|
+
LMP_TEMP_KEY = 14
|
|
113
|
+
LMP_TEMP_RAND = 13
|
|
114
|
+
LMP_TEST_ACTIVATE = 56
|
|
115
|
+
LMP_TEST_CONTROL = 57
|
|
116
|
+
LMP_TIMING_ACCURACY_REQ = 47
|
|
117
|
+
LMP_TIMING_ACCURACY_RES = 48
|
|
118
|
+
LMP_UNIT_KEY = 10
|
|
119
|
+
LMP_UNSNIFF_REQ = 24
|
|
120
|
+
LMP_USE_SEMI_PERMANENT_KEY = 50
|
|
121
|
+
LMP_VERSION_REQ = 37
|
|
122
|
+
LMP_VERSION_RES = 38
|
|
123
|
+
# fmt: on
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def parse_from(cls, data: bytes, offset: int = 0) -> tuple[int, Opcode]:
|
|
127
|
+
opcode = data[offset]
|
|
128
|
+
if opcode in (124, 127):
|
|
129
|
+
opcode = struct.unpack('>H', data)[0]
|
|
130
|
+
return offset + 2, Opcode(opcode)
|
|
131
|
+
return offset + 1, Opcode(opcode)
|
|
132
|
+
|
|
133
|
+
def __bytes__(self) -> bytes:
|
|
134
|
+
if self.value >> 8:
|
|
135
|
+
return struct.pack('>H', self.value)
|
|
136
|
+
return bytes([self.value])
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def type_metadata(cls):
|
|
140
|
+
return hci.metadata(
|
|
141
|
+
{
|
|
142
|
+
'serializer': bytes,
|
|
143
|
+
'parser': lambda data, offset: (Opcode.parse_from(data, offset)),
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class Packet:
|
|
149
|
+
'''
|
|
150
|
+
See Bluetooth spec @ Vol 2, Part C - 5.1 PDU summary
|
|
151
|
+
'''
|
|
152
|
+
|
|
153
|
+
subclasses: dict[int, type[Packet]] = {}
|
|
154
|
+
opcode: Opcode
|
|
155
|
+
fields: hci.Fields = ()
|
|
156
|
+
_payload: bytes = b''
|
|
157
|
+
|
|
158
|
+
_Packet = TypeVar("_Packet", bound="Packet")
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def subclass(cls, subclass: type[_Packet]) -> type[_Packet]:
|
|
162
|
+
# Register a factory for this class
|
|
163
|
+
cls.subclasses[subclass.opcode] = subclass
|
|
164
|
+
subclass.fields = hci.HCI_Object.fields_from_dataclass(subclass)
|
|
165
|
+
|
|
166
|
+
return subclass
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def from_bytes(cls, data: bytes) -> Packet:
|
|
170
|
+
offset, opcode = Opcode.parse_from(data)
|
|
171
|
+
if not (subclass := cls.subclasses.get(opcode)):
|
|
172
|
+
instance = Packet()
|
|
173
|
+
instance.opcode = opcode
|
|
174
|
+
else:
|
|
175
|
+
instance = subclass(
|
|
176
|
+
**hci.HCI_Object.dict_from_bytes(data, offset, subclass.fields)
|
|
177
|
+
)
|
|
178
|
+
instance.payload = data[offset:]
|
|
179
|
+
return instance
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def payload(self) -> bytes:
|
|
183
|
+
if self._payload is None:
|
|
184
|
+
self._payload = hci.HCI_Object.dict_to_bytes(self.__dict__, self.fields)
|
|
185
|
+
return self._payload
|
|
186
|
+
|
|
187
|
+
@payload.setter
|
|
188
|
+
def payload(self, value: bytes) -> None:
|
|
189
|
+
self._payload = value
|
|
190
|
+
|
|
191
|
+
def __bytes__(self) -> bytes:
|
|
192
|
+
return bytes(self.opcode) + self.payload
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@Packet.subclass
|
|
196
|
+
@dataclass
|
|
197
|
+
class LmpAccepted(Packet):
|
|
198
|
+
opcode = Opcode.LMP_ACCEPTED
|
|
199
|
+
|
|
200
|
+
response_opcode: Opcode = field(metadata=Opcode.type_metadata())
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@Packet.subclass
|
|
204
|
+
@dataclass
|
|
205
|
+
class LmpNotAccepted(Packet):
|
|
206
|
+
opcode = Opcode.LMP_NOT_ACCEPTED
|
|
207
|
+
|
|
208
|
+
response_opcode: Opcode = field(metadata=Opcode.type_metadata())
|
|
209
|
+
error_code: int = field(metadata=hci.metadata(1))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@Packet.subclass
|
|
213
|
+
@dataclass
|
|
214
|
+
class LmpAcceptedExt(Packet):
|
|
215
|
+
opcode = Opcode.LMP_ACCEPTED_EXT
|
|
216
|
+
|
|
217
|
+
response_opcode: Opcode = field(metadata=Opcode.type_metadata())
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@Packet.subclass
|
|
221
|
+
@dataclass
|
|
222
|
+
class LmpNotAcceptedExt(Packet):
|
|
223
|
+
opcode = Opcode.LMP_NOT_ACCEPTED_EXT
|
|
224
|
+
|
|
225
|
+
response_opcode: Opcode = field(metadata=Opcode.type_metadata())
|
|
226
|
+
error_code: int = field(metadata=hci.metadata(1))
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@Packet.subclass
|
|
230
|
+
@dataclass
|
|
231
|
+
class LmpAuRand(Packet):
|
|
232
|
+
opcode = Opcode.LMP_AU_RAND
|
|
233
|
+
|
|
234
|
+
random_number: bytes = field(metadata=hci.metadata(16))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@Packet.subclass
|
|
238
|
+
@dataclass
|
|
239
|
+
class LmpDetach(Packet):
|
|
240
|
+
opcode = Opcode.LMP_DETACH
|
|
241
|
+
|
|
242
|
+
error_code: int = field(metadata=hci.metadata(1))
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@Packet.subclass
|
|
246
|
+
@dataclass
|
|
247
|
+
class LmpEscoLinkReq(Packet):
|
|
248
|
+
opcode = Opcode.LMP_ESCO_LINK_REQ
|
|
249
|
+
|
|
250
|
+
esco_handle: int = field(metadata=hci.metadata(1))
|
|
251
|
+
esco_lt_addr: int = field(metadata=hci.metadata(1))
|
|
252
|
+
timing_control_flags: int = field(metadata=hci.metadata(1))
|
|
253
|
+
d_esco: int = field(metadata=hci.metadata(1))
|
|
254
|
+
t_esco: int = field(metadata=hci.metadata(1))
|
|
255
|
+
w_esco: int = field(metadata=hci.metadata(1))
|
|
256
|
+
esco_packet_type_c_to_p: int = field(metadata=hci.metadata(1))
|
|
257
|
+
esco_packet_type_p_to_c: int = field(metadata=hci.metadata(1))
|
|
258
|
+
packet_length_c_to_p: int = field(metadata=hci.metadata(2))
|
|
259
|
+
packet_length_p_to_c: int = field(metadata=hci.metadata(2))
|
|
260
|
+
air_mode: int = field(metadata=hci.metadata(1))
|
|
261
|
+
negotiation_state: int = field(metadata=hci.metadata(1))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@Packet.subclass
|
|
265
|
+
@dataclass
|
|
266
|
+
class LmpHostConnectionReq(Packet):
|
|
267
|
+
opcode = Opcode.LMP_HOST_CONNECTION_REQ
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@Packet.subclass
|
|
271
|
+
@dataclass
|
|
272
|
+
class LmpRemoveEscoLinkReq(Packet):
|
|
273
|
+
opcode = Opcode.LMP_REMOVE_ESCO_LINK_REQ
|
|
274
|
+
|
|
275
|
+
esco_handle: int = field(metadata=hci.metadata(1))
|
|
276
|
+
error_code: int = field(metadata=hci.metadata(1))
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
@Packet.subclass
|
|
280
|
+
@dataclass
|
|
281
|
+
class LmpRemoveScoLinkReq(Packet):
|
|
282
|
+
opcode = Opcode.LMP_REMOVE_SCO_LINK_REQ
|
|
283
|
+
|
|
284
|
+
sco_handle: int = field(metadata=hci.metadata(1))
|
|
285
|
+
error_code: int = field(metadata=hci.metadata(1))
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@Packet.subclass
|
|
289
|
+
@dataclass
|
|
290
|
+
class LmpScoLinkReq(Packet):
|
|
291
|
+
opcode = Opcode.LMP_SCO_LINK_REQ
|
|
292
|
+
|
|
293
|
+
sco_handle: int = field(metadata=hci.metadata(1))
|
|
294
|
+
timing_control_flags: int = field(metadata=hci.metadata(1))
|
|
295
|
+
d_sco: int = field(metadata=hci.metadata(1))
|
|
296
|
+
t_sco: int = field(metadata=hci.metadata(1))
|
|
297
|
+
sco_packet: int = field(metadata=hci.metadata(1))
|
|
298
|
+
air_mode: int = field(metadata=hci.metadata(1))
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@Packet.subclass
|
|
302
|
+
@dataclass
|
|
303
|
+
class LmpSwitchReq(Packet):
|
|
304
|
+
opcode = Opcode.LMP_SWITCH_REQ
|
|
305
|
+
|
|
306
|
+
switch_instant: int = field(metadata=hci.metadata(4), default=0)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@Packet.subclass
|
|
310
|
+
@dataclass
|
|
311
|
+
class LmpNameReq(Packet):
|
|
312
|
+
opcode = Opcode.LMP_NAME_REQ
|
|
313
|
+
|
|
314
|
+
name_offset: int = field(metadata=hci.metadata(2))
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@Packet.subclass
|
|
318
|
+
@dataclass
|
|
319
|
+
class LmpNameRes(Packet):
|
|
320
|
+
opcode = Opcode.LMP_NAME_RES
|
|
321
|
+
|
|
322
|
+
name_offset: int = field(metadata=hci.metadata(2))
|
|
323
|
+
name_length: int = field(metadata=hci.metadata(3))
|
|
324
|
+
name_fregment: bytes = field(metadata=hci.metadata('*'))
|
bumble/pairing.py
CHANGED
|
@@ -20,7 +20,6 @@ from __future__ import annotations
|
|
|
20
20
|
import enum
|
|
21
21
|
import secrets
|
|
22
22
|
from dataclasses import dataclass
|
|
23
|
-
from typing import Optional
|
|
24
23
|
|
|
25
24
|
from bumble import hci
|
|
26
25
|
from bumble.core import AdvertisingData, LeRole
|
|
@@ -45,16 +44,16 @@ from bumble.smp import (
|
|
|
45
44
|
class OobData:
|
|
46
45
|
"""OOB data that can be sent from one device to another."""
|
|
47
46
|
|
|
48
|
-
address:
|
|
49
|
-
role:
|
|
50
|
-
shared_data:
|
|
51
|
-
legacy_context:
|
|
47
|
+
address: hci.Address | None = None
|
|
48
|
+
role: LeRole | None = None
|
|
49
|
+
shared_data: OobSharedData | None = None
|
|
50
|
+
legacy_context: OobLegacyContext | None = None
|
|
52
51
|
|
|
53
52
|
@classmethod
|
|
54
53
|
def from_ad(cls, ad: AdvertisingData) -> OobData:
|
|
55
54
|
instance = cls()
|
|
56
|
-
shared_data_c:
|
|
57
|
-
shared_data_r:
|
|
55
|
+
shared_data_c: bytes | None = None
|
|
56
|
+
shared_data_r: bytes | None = None
|
|
58
57
|
for ad_type, ad_data in ad.ad_structures:
|
|
59
58
|
if ad_type == AdvertisingData.LE_BLUETOOTH_DEVICE_ADDRESS:
|
|
60
59
|
instance.address = hci.Address(ad_data)
|
|
@@ -181,14 +180,14 @@ class PairingDelegate:
|
|
|
181
180
|
"""Compare two numbers."""
|
|
182
181
|
return True
|
|
183
182
|
|
|
184
|
-
async def get_number(self) ->
|
|
183
|
+
async def get_number(self) -> int | None:
|
|
185
184
|
"""
|
|
186
185
|
Return an optional number as an answer to a passkey request.
|
|
187
186
|
Returning `None` will result in a negative reply.
|
|
188
187
|
"""
|
|
189
188
|
return 0
|
|
190
189
|
|
|
191
|
-
async def get_string(self, max_length: int) ->
|
|
190
|
+
async def get_string(self, max_length: int) -> str | None:
|
|
192
191
|
"""
|
|
193
192
|
Return a string whose utf-8 encoding is up to max_length bytes.
|
|
194
193
|
"""
|
|
@@ -239,18 +238,18 @@ class PairingConfig:
|
|
|
239
238
|
class OobConfig:
|
|
240
239
|
"""Config for OOB pairing."""
|
|
241
240
|
|
|
242
|
-
our_context:
|
|
243
|
-
peer_data:
|
|
244
|
-
legacy_context:
|
|
241
|
+
our_context: OobContext | None
|
|
242
|
+
peer_data: OobSharedData | None
|
|
243
|
+
legacy_context: OobLegacyContext | None
|
|
245
244
|
|
|
246
245
|
def __init__(
|
|
247
246
|
self,
|
|
248
247
|
sc: bool = True,
|
|
249
248
|
mitm: bool = True,
|
|
250
249
|
bonding: bool = True,
|
|
251
|
-
delegate:
|
|
252
|
-
identity_address_type:
|
|
253
|
-
oob:
|
|
250
|
+
delegate: PairingDelegate | None = None,
|
|
251
|
+
identity_address_type: AddressType | None = None,
|
|
252
|
+
oob: OobConfig | None = None,
|
|
254
253
|
) -> None:
|
|
255
254
|
self.sc = sc
|
|
256
255
|
self.mitm = mitm
|
bumble/pandora/__init__.py
CHANGED
|
@@ -19,7 +19,7 @@ This module implement the Pandora Bluetooth test APIs for the Bumble stack.
|
|
|
19
19
|
|
|
20
20
|
__version__ = "0.0.1"
|
|
21
21
|
|
|
22
|
-
from
|
|
22
|
+
from collections.abc import Callable
|
|
23
23
|
|
|
24
24
|
import grpc
|
|
25
25
|
import grpc.aio
|
|
@@ -58,7 +58,7 @@ def register_servicer_hook(
|
|
|
58
58
|
async def serve(
|
|
59
59
|
bumble: PandoraDevice,
|
|
60
60
|
config: Config = Config(),
|
|
61
|
-
grpc_server:
|
|
61
|
+
grpc_server: grpc.aio.Server | None = None,
|
|
62
62
|
port: int = 0,
|
|
63
63
|
) -> None:
|
|
64
64
|
# initialize a gRPC server if not provided.
|
bumble/pandora/device.py
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
-
from typing import Any
|
|
19
|
+
from typing import Any
|
|
20
20
|
|
|
21
21
|
from bumble import transport
|
|
22
22
|
from bumble.core import (
|
|
@@ -54,7 +54,7 @@ class PandoraDevice:
|
|
|
54
54
|
|
|
55
55
|
# HCI transport name & instance.
|
|
56
56
|
_hci_name: str
|
|
57
|
-
_hci:
|
|
57
|
+
_hci: transport.Transport | None # type: ignore[name-defined]
|
|
58
58
|
|
|
59
59
|
def __init__(self, config: dict[str, Any]) -> None:
|
|
60
60
|
self.config = config
|
|
@@ -74,7 +74,9 @@ class PandoraDevice:
|
|
|
74
74
|
|
|
75
75
|
# open HCI transport & set device host.
|
|
76
76
|
self._hci = await transport.open_transport(self._hci_name)
|
|
77
|
-
self.device.host = Host(
|
|
77
|
+
self.device.host = Host(
|
|
78
|
+
controller_source=self._hci.source, controller_sink=self._hci.sink
|
|
79
|
+
) # type: ignore[no-untyped-call]
|
|
78
80
|
|
|
79
81
|
# power-on.
|
|
80
82
|
await self.device.power_on()
|
|
@@ -96,7 +98,7 @@ class PandoraDevice:
|
|
|
96
98
|
await self.close()
|
|
97
99
|
await self.open()
|
|
98
100
|
|
|
99
|
-
def info(self) ->
|
|
101
|
+
def info(self) -> dict[str, str] | None:
|
|
100
102
|
return {
|
|
101
103
|
'public_bd_address': str(self.device.public_address),
|
|
102
104
|
'random_address': str(self.device.random_address),
|
bumble/pandora/host.py
CHANGED
|
@@ -17,12 +17,15 @@ from __future__ import annotations
|
|
|
17
17
|
import asyncio
|
|
18
18
|
import logging
|
|
19
19
|
import struct
|
|
20
|
-
from
|
|
20
|
+
from collections.abc import AsyncGenerator
|
|
21
|
+
from typing import cast
|
|
21
22
|
|
|
22
23
|
import grpc
|
|
23
24
|
import grpc.aio
|
|
24
|
-
from google.protobuf import
|
|
25
|
-
|
|
25
|
+
from google.protobuf import (
|
|
26
|
+
any_pb2, # pytype: disable=pyi-error
|
|
27
|
+
empty_pb2, # pytype: disable=pyi-error
|
|
28
|
+
)
|
|
26
29
|
from pandora import host_pb2
|
|
27
30
|
from pandora.host_grpc_aio import HostServicer
|
|
28
31
|
from pandora.host_pb2 import (
|
|
@@ -302,7 +305,9 @@ class HostService(HostServicer):
|
|
|
302
305
|
await disconnection_future
|
|
303
306
|
self.log.debug("Disconnected")
|
|
304
307
|
finally:
|
|
305
|
-
connection.remove_listener(
|
|
308
|
+
connection.remove_listener(
|
|
309
|
+
connection.EVENT_DISCONNECTION, on_disconnection
|
|
310
|
+
) # type: ignore
|
|
306
311
|
|
|
307
312
|
return empty_pb2.Empty()
|
|
308
313
|
|
|
@@ -539,7 +544,7 @@ class HostService(HostServicer):
|
|
|
539
544
|
await bumble.utils.cancel_on_event(
|
|
540
545
|
self.device, 'flush', self.device.stop_advertising()
|
|
541
546
|
)
|
|
542
|
-
except:
|
|
547
|
+
except Exception:
|
|
543
548
|
pass
|
|
544
549
|
|
|
545
550
|
@utils.rpc
|
|
@@ -609,7 +614,7 @@ class HostService(HostServicer):
|
|
|
609
614
|
await bumble.utils.cancel_on_event(
|
|
610
615
|
self.device, 'flush', self.device.stop_scanning()
|
|
611
616
|
)
|
|
612
|
-
except:
|
|
617
|
+
except Exception:
|
|
613
618
|
pass
|
|
614
619
|
|
|
615
620
|
@utils.rpc
|
|
@@ -619,7 +624,7 @@ class HostService(HostServicer):
|
|
|
619
624
|
self.log.debug('Inquiry')
|
|
620
625
|
|
|
621
626
|
inquiry_queue: asyncio.Queue[
|
|
622
|
-
|
|
627
|
+
tuple[Address, int, AdvertisingData, int] | None
|
|
623
628
|
] = asyncio.Queue()
|
|
624
629
|
complete_handler = self.device.on(
|
|
625
630
|
self.device.EVENT_INQUIRY_COMPLETE, lambda: inquiry_queue.put_nowait(None)
|
|
@@ -644,14 +649,18 @@ class HostService(HostServicer):
|
|
|
644
649
|
)
|
|
645
650
|
|
|
646
651
|
finally:
|
|
647
|
-
self.device.remove_listener(
|
|
648
|
-
|
|
652
|
+
self.device.remove_listener(
|
|
653
|
+
self.device.EVENT_INQUIRY_COMPLETE, complete_handler
|
|
654
|
+
) # type: ignore
|
|
655
|
+
self.device.remove_listener(
|
|
656
|
+
self.device.EVENT_INQUIRY_RESULT, result_handler
|
|
657
|
+
) # type: ignore
|
|
649
658
|
try:
|
|
650
659
|
self.log.debug('Stop inquiry')
|
|
651
660
|
await bumble.utils.cancel_on_event(
|
|
652
661
|
self.device, 'flush', self.device.stop_discovery()
|
|
653
662
|
)
|
|
654
|
-
except:
|
|
663
|
+
except Exception:
|
|
655
664
|
pass
|
|
656
665
|
|
|
657
666
|
@utils.rpc
|
bumble/pandora/l2cap.py
CHANGED
|
@@ -18,15 +18,15 @@ import json
|
|
|
18
18
|
import logging
|
|
19
19
|
from asyncio import Future
|
|
20
20
|
from asyncio import Queue as AsyncQueue
|
|
21
|
+
from collections.abc import AsyncGenerator
|
|
21
22
|
from dataclasses import dataclass
|
|
22
|
-
from typing import AsyncGenerator, Optional, Union
|
|
23
23
|
|
|
24
24
|
import grpc
|
|
25
25
|
from google.protobuf import any_pb2, empty_pb2 # pytype: disable=pyi-error
|
|
26
26
|
from pandora.l2cap_grpc_aio import L2CAPServicer # pytype: disable=pyi-error
|
|
27
|
-
from pandora.l2cap_pb2 import COMMAND_NOT_UNDERSTOOD, INVALID_CID_IN_REQUEST
|
|
28
|
-
from pandora.l2cap_pb2 import Channel as PandoraChannel # pytype: disable=pyi-error
|
|
29
27
|
from pandora.l2cap_pb2 import (
|
|
28
|
+
COMMAND_NOT_UNDERSTOOD,
|
|
29
|
+
INVALID_CID_IN_REQUEST,
|
|
30
30
|
ConnectRequest,
|
|
31
31
|
ConnectResponse,
|
|
32
32
|
CreditBasedChannelRequest,
|
|
@@ -41,6 +41,7 @@ from pandora.l2cap_pb2 import (
|
|
|
41
41
|
WaitDisconnectionRequest,
|
|
42
42
|
WaitDisconnectionResponse,
|
|
43
43
|
)
|
|
44
|
+
from pandora.l2cap_pb2 import Channel as PandoraChannel # pytype: disable=pyi-error
|
|
44
45
|
|
|
45
46
|
from bumble.core import InvalidArgumentError, OutOfResourcesError
|
|
46
47
|
from bumble.device import Device
|
|
@@ -55,7 +56,7 @@ from bumble.l2cap import (
|
|
|
55
56
|
from bumble.pandora import utils
|
|
56
57
|
from bumble.pandora.config import Config
|
|
57
58
|
|
|
58
|
-
L2capChannel =
|
|
59
|
+
L2capChannel = ClassicChannel | LeCreditBasedChannel
|
|
59
60
|
|
|
60
61
|
|
|
61
62
|
@dataclass
|
|
@@ -106,10 +107,8 @@ class L2CAPService(L2CAPServicer):
|
|
|
106
107
|
oneof = request.WhichOneof('type')
|
|
107
108
|
self.log.debug(f'WaitConnection channel request type: {oneof}.')
|
|
108
109
|
channel_type = getattr(request, oneof)
|
|
109
|
-
spec:
|
|
110
|
-
l2cap_server:
|
|
111
|
-
Union[ClassicChannelServer, LeCreditBasedChannelServer]
|
|
112
|
-
] = None
|
|
110
|
+
spec: ClassicChannelSpec | LeCreditBasedChannelSpec | None = None
|
|
111
|
+
l2cap_server: ClassicChannelServer | LeCreditBasedChannelServer | None = None
|
|
113
112
|
if isinstance(channel_type, CreditBasedChannelRequest):
|
|
114
113
|
spec = LeCreditBasedChannelSpec(
|
|
115
114
|
psm=channel_type.spsm,
|
|
@@ -216,7 +215,7 @@ class L2CAPService(L2CAPServicer):
|
|
|
216
215
|
oneof = request.WhichOneof('type')
|
|
217
216
|
self.log.debug(f'Channel request type: {oneof}.')
|
|
218
217
|
channel_type = getattr(request, oneof)
|
|
219
|
-
spec:
|
|
218
|
+
spec: ClassicChannelSpec | LeCreditBasedChannelSpec | None = None
|
|
220
219
|
if isinstance(channel_type, CreditBasedChannelRequest):
|
|
221
220
|
spec = LeCreditBasedChannelSpec(
|
|
222
221
|
psm=channel_type.spsm,
|
bumble/pandora/security.py
CHANGED
|
@@ -17,13 +17,15 @@ from __future__ import annotations
|
|
|
17
17
|
import asyncio
|
|
18
18
|
import contextlib
|
|
19
19
|
import logging
|
|
20
|
-
from collections.abc import Awaitable
|
|
21
|
-
from typing import Any
|
|
20
|
+
from collections.abc import AsyncGenerator, AsyncIterator, Awaitable, Callable
|
|
21
|
+
from typing import Any
|
|
22
22
|
|
|
23
23
|
import grpc
|
|
24
|
-
from google.protobuf import
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
from google.protobuf import (
|
|
25
|
+
any_pb2, # pytype: disable=pyi-error
|
|
26
|
+
empty_pb2, # pytype: disable=pyi-error
|
|
27
|
+
wrappers_pb2, # pytype: disable=pyi-error
|
|
28
|
+
)
|
|
27
29
|
from pandora.host_pb2 import Connection
|
|
28
30
|
from pandora.security_grpc_aio import SecurityServicer, SecurityStorageServicer
|
|
29
31
|
from pandora.security_pb2 import (
|
|
@@ -64,7 +66,7 @@ class PairingDelegate(BasePairingDelegate):
|
|
|
64
66
|
def __init__(
|
|
65
67
|
self,
|
|
66
68
|
connection: BumbleConnection,
|
|
67
|
-
service:
|
|
69
|
+
service: SecurityService,
|
|
68
70
|
io_capability: BasePairingDelegate.IoCapability = BasePairingDelegate.NO_OUTPUT_NO_INPUT,
|
|
69
71
|
local_initiator_key_distribution: BasePairingDelegate.KeyDistribution = BasePairingDelegate.DEFAULT_KEY_DISTRIBUTION,
|
|
70
72
|
local_responder_key_distribution: BasePairingDelegate.KeyDistribution = BasePairingDelegate.DEFAULT_KEY_DISTRIBUTION,
|
|
@@ -130,7 +132,7 @@ class PairingDelegate(BasePairingDelegate):
|
|
|
130
132
|
assert answer.answer_variant() == 'confirm' and answer.confirm is not None
|
|
131
133
|
return answer.confirm
|
|
132
134
|
|
|
133
|
-
async def get_number(self) ->
|
|
135
|
+
async def get_number(self) -> int | None:
|
|
134
136
|
self.log.debug(
|
|
135
137
|
f"Pairing event: `passkey_entry_request` (io_capability: {self.io_capability})"
|
|
136
138
|
)
|
|
@@ -147,7 +149,7 @@ class PairingDelegate(BasePairingDelegate):
|
|
|
147
149
|
assert answer.answer_variant() == 'passkey'
|
|
148
150
|
return answer.passkey
|
|
149
151
|
|
|
150
|
-
async def get_string(self, max_length: int) ->
|
|
152
|
+
async def get_string(self, max_length: int) -> str | None:
|
|
151
153
|
self.log.debug(
|
|
152
154
|
f"Pairing event: `pin_code_request` (io_capability: {self.io_capability})"
|
|
153
155
|
)
|
|
@@ -195,8 +197,8 @@ class SecurityService(SecurityServicer):
|
|
|
195
197
|
self.log = utils.BumbleServerLoggerAdapter(
|
|
196
198
|
logging.getLogger(), {'service_name': 'Security', 'device': device}
|
|
197
199
|
)
|
|
198
|
-
self.event_queue:
|
|
199
|
-
self.event_answer:
|
|
200
|
+
self.event_queue: asyncio.Queue[PairingEvent] | None = None
|
|
201
|
+
self.event_answer: AsyncIterator[PairingEventAnswer] | None = None
|
|
200
202
|
self.device = device
|
|
201
203
|
self.config = config
|
|
202
204
|
|
|
@@ -231,7 +233,7 @@ class SecurityService(SecurityServicer):
|
|
|
231
233
|
if level == LEVEL2:
|
|
232
234
|
return connection.encryption != 0 and connection.authenticated
|
|
233
235
|
|
|
234
|
-
link_key_type:
|
|
236
|
+
link_key_type: int | None = None
|
|
235
237
|
if (keystore := connection.device.keystore) and (
|
|
236
238
|
keys := await keystore.get(str(connection.peer_address))
|
|
237
239
|
):
|
|
@@ -410,8 +412,8 @@ class SecurityService(SecurityServicer):
|
|
|
410
412
|
wait_for_security: asyncio.Future[str] = (
|
|
411
413
|
asyncio.get_running_loop().create_future()
|
|
412
414
|
)
|
|
413
|
-
authenticate_task:
|
|
414
|
-
pair_task:
|
|
415
|
+
authenticate_task: asyncio.Future[None] | None = None
|
|
416
|
+
pair_task: asyncio.Future[None] | None = None
|
|
415
417
|
|
|
416
418
|
async def authenticate() -> None:
|
|
417
419
|
if (encryption := connection.encryption) != 0:
|
|
@@ -455,9 +457,9 @@ class SecurityService(SecurityServicer):
|
|
|
455
457
|
|
|
456
458
|
def pair(*_: Any) -> None:
|
|
457
459
|
if self.need_pairing(connection, level):
|
|
458
|
-
|
|
460
|
+
bumble.utils.AsyncRunner.spawn(connection.pair())
|
|
459
461
|
|
|
460
|
-
listeners: dict[str, Callable[...,
|
|
462
|
+
listeners: dict[str, Callable[..., None | Awaitable[None]]] = {
|
|
461
463
|
'disconnection': set_failure('connection_died'),
|
|
462
464
|
'pairing_failure': set_failure('pairing_failure'),
|
|
463
465
|
'connection_authentication_failure': set_failure('authentication_failure'),
|
|
@@ -500,7 +502,7 @@ class SecurityService(SecurityServicer):
|
|
|
500
502
|
return WaitSecurityResponse(**kwargs)
|
|
501
503
|
|
|
502
504
|
async def reached_security_level(
|
|
503
|
-
self, connection: BumbleConnection, level:
|
|
505
|
+
self, connection: BumbleConnection, level: SecurityLevel | LESecurityLevel
|
|
504
506
|
) -> bool:
|
|
505
507
|
self.log.debug(
|
|
506
508
|
str(
|