bumble 0.0.218__py3-none-any.whl → 0.0.220__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 +57 -18
- bumble/apps/auracast.py +7 -13
- bumble/audio/io.py +3 -3
- bumble/avctp.py +8 -12
- bumble/avdtp.py +584 -533
- bumble/avrcp.py +20 -20
- bumble/controller.py +993 -507
- bumble/device.py +107 -183
- bumble/drivers/rtk.py +3 -1
- bumble/hci.py +38 -0
- bumble/hid.py +1 -1
- bumble/link.py +68 -165
- bumble/lmp.py +324 -0
- bumble/rfcomm.py +7 -3
- bumble/snoop.py +10 -4
- bumble/transport/common.py +6 -3
- bumble/transport/ws_client.py +2 -2
- bumble/transport/ws_server.py +16 -8
- bumble/utils.py +1 -5
- {bumble-0.0.218.dist-info → bumble-0.0.220.dist-info}/METADATA +3 -3
- {bumble-0.0.218.dist-info → bumble-0.0.220.dist-info}/RECORD +26 -25
- {bumble-0.0.218.dist-info → bumble-0.0.220.dist-info}/WHEEL +0 -0
- {bumble-0.0.218.dist-info → bumble-0.0.220.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.218.dist-info → bumble-0.0.220.dist-info}/licenses/LICENSE +0 -0
- {bumble-0.0.218.dist-info → bumble-0.0.220.dist-info}/top_level.txt +0 -0
bumble/link.py
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
|
+
from __future__ import annotations
|
|
14
15
|
|
|
15
16
|
import asyncio
|
|
16
17
|
|
|
@@ -20,16 +21,7 @@ import asyncio
|
|
|
20
21
|
import logging
|
|
21
22
|
from typing import Optional
|
|
22
23
|
|
|
23
|
-
from bumble import controller, core
|
|
24
|
-
from bumble.hci import (
|
|
25
|
-
HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR,
|
|
26
|
-
HCI_PAGE_TIMEOUT_ERROR,
|
|
27
|
-
HCI_SUCCESS,
|
|
28
|
-
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
|
29
|
-
Address,
|
|
30
|
-
HCI_Connection_Complete_Event,
|
|
31
|
-
Role,
|
|
32
|
-
)
|
|
24
|
+
from bumble import controller, core, hci, lmp
|
|
33
25
|
|
|
34
26
|
# -----------------------------------------------------------------------------
|
|
35
27
|
# Logging
|
|
@@ -40,13 +32,6 @@ logger = logging.getLogger(__name__)
|
|
|
40
32
|
# -----------------------------------------------------------------------------
|
|
41
33
|
# Utils
|
|
42
34
|
# -----------------------------------------------------------------------------
|
|
43
|
-
def parse_parameters(params_str):
|
|
44
|
-
result = {}
|
|
45
|
-
for param_str in params_str.split(','):
|
|
46
|
-
if '=' in param_str:
|
|
47
|
-
key, value = param_str.split('=')
|
|
48
|
-
result[key] = value
|
|
49
|
-
return result
|
|
50
35
|
|
|
51
36
|
|
|
52
37
|
# -----------------------------------------------------------------------------
|
|
@@ -69,21 +54,21 @@ class LocalLink:
|
|
|
69
54
|
# Common utils
|
|
70
55
|
############################################################
|
|
71
56
|
|
|
72
|
-
def add_controller(self, controller):
|
|
57
|
+
def add_controller(self, controller: controller.Controller):
|
|
73
58
|
logger.debug(f'new controller: {controller}')
|
|
74
59
|
self.controllers.add(controller)
|
|
75
60
|
|
|
76
|
-
def remove_controller(self, controller):
|
|
61
|
+
def remove_controller(self, controller: controller.Controller):
|
|
77
62
|
self.controllers.remove(controller)
|
|
78
63
|
|
|
79
|
-
def find_controller(self, address):
|
|
64
|
+
def find_controller(self, address: hci.Address) -> controller.Controller | None:
|
|
80
65
|
for controller in self.controllers:
|
|
81
66
|
if controller.random_address == address:
|
|
82
67
|
return controller
|
|
83
68
|
return None
|
|
84
69
|
|
|
85
70
|
def find_classic_controller(
|
|
86
|
-
self, address: Address
|
|
71
|
+
self, address: hci.Address
|
|
87
72
|
) -> Optional[controller.Controller]:
|
|
88
73
|
for controller in self.controllers:
|
|
89
74
|
if controller.public_address == address:
|
|
@@ -100,13 +85,19 @@ class LocalLink:
|
|
|
100
85
|
def on_address_changed(self, controller):
|
|
101
86
|
pass
|
|
102
87
|
|
|
103
|
-
def send_advertising_data(self, sender_address, data):
|
|
88
|
+
def send_advertising_data(self, sender_address: hci.Address, data: bytes):
|
|
104
89
|
# Send the advertising data to all controllers, except the sender
|
|
105
90
|
for controller in self.controllers:
|
|
106
91
|
if controller.random_address != sender_address:
|
|
107
92
|
controller.on_link_advertising_data(sender_address, data)
|
|
108
93
|
|
|
109
|
-
def send_acl_data(
|
|
94
|
+
def send_acl_data(
|
|
95
|
+
self,
|
|
96
|
+
sender_controller: controller.Controller,
|
|
97
|
+
destination_address: hci.Address,
|
|
98
|
+
transport: core.PhysicalTransport,
|
|
99
|
+
data: bytes,
|
|
100
|
+
):
|
|
110
101
|
# Send the data to the first controller with a matching address
|
|
111
102
|
if transport == core.PhysicalTransport.LE:
|
|
112
103
|
destination_controller = self.find_controller(destination_address)
|
|
@@ -118,9 +109,13 @@ class LocalLink:
|
|
|
118
109
|
raise ValueError("unsupported transport type")
|
|
119
110
|
|
|
120
111
|
if destination_controller is not None:
|
|
121
|
-
|
|
112
|
+
asyncio.get_running_loop().call_soon(
|
|
113
|
+
lambda: destination_controller.on_link_acl_data(
|
|
114
|
+
source_address, transport, data
|
|
115
|
+
)
|
|
116
|
+
)
|
|
122
117
|
|
|
123
|
-
def on_connection_complete(self):
|
|
118
|
+
def on_connection_complete(self) -> None:
|
|
124
119
|
# Check that we expect this call
|
|
125
120
|
if not self.pending_connection:
|
|
126
121
|
logger.warning('on_connection_complete with no pending connection')
|
|
@@ -139,17 +134,21 @@ class LocalLink:
|
|
|
139
134
|
le_create_connection_command.peer_address
|
|
140
135
|
):
|
|
141
136
|
central_controller.on_link_peripheral_connection_complete(
|
|
142
|
-
le_create_connection_command, HCI_SUCCESS
|
|
137
|
+
le_create_connection_command, hci.HCI_SUCCESS
|
|
143
138
|
)
|
|
144
139
|
peripheral_controller.on_link_central_connected(central_address)
|
|
145
140
|
return
|
|
146
141
|
|
|
147
142
|
# No peripheral found
|
|
148
143
|
central_controller.on_link_peripheral_connection_complete(
|
|
149
|
-
le_create_connection_command, HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR
|
|
144
|
+
le_create_connection_command, hci.HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR
|
|
150
145
|
)
|
|
151
146
|
|
|
152
|
-
def connect(
|
|
147
|
+
def connect(
|
|
148
|
+
self,
|
|
149
|
+
central_address: hci.Address,
|
|
150
|
+
le_create_connection_command: hci.HCI_LE_Create_Connection_Command,
|
|
151
|
+
):
|
|
153
152
|
logger.debug(
|
|
154
153
|
f'$$$ CONNECTION {central_address} -> '
|
|
155
154
|
f'{le_create_connection_command.peer_address}'
|
|
@@ -158,7 +157,10 @@ class LocalLink:
|
|
|
158
157
|
asyncio.get_running_loop().call_soon(self.on_connection_complete)
|
|
159
158
|
|
|
160
159
|
def on_disconnection_complete(
|
|
161
|
-
self,
|
|
160
|
+
self,
|
|
161
|
+
initiating_address: hci.Address,
|
|
162
|
+
target_address: hci.Address,
|
|
163
|
+
disconnect_command: hci.HCI_Disconnect_Command,
|
|
162
164
|
):
|
|
163
165
|
# Find the controller that initiated the disconnection
|
|
164
166
|
if not (initiating_controller := self.find_controller(initiating_address)):
|
|
@@ -172,20 +174,32 @@ class LocalLink:
|
|
|
172
174
|
)
|
|
173
175
|
|
|
174
176
|
initiating_controller.on_link_disconnection_complete(
|
|
175
|
-
disconnect_command, HCI_SUCCESS
|
|
177
|
+
disconnect_command, hci.HCI_SUCCESS
|
|
176
178
|
)
|
|
177
179
|
|
|
178
|
-
def disconnect(
|
|
180
|
+
def disconnect(
|
|
181
|
+
self,
|
|
182
|
+
initiating_address: hci.Address,
|
|
183
|
+
target_address: hci.Address,
|
|
184
|
+
disconnect_command: hci.HCI_Disconnect_Command,
|
|
185
|
+
):
|
|
179
186
|
logger.debug(
|
|
180
187
|
f'$$$ DISCONNECTION {initiating_address} -> '
|
|
181
188
|
f'{target_address}: reason = {disconnect_command.reason}'
|
|
182
189
|
)
|
|
183
|
-
|
|
184
|
-
|
|
190
|
+
asyncio.get_running_loop().call_soon(
|
|
191
|
+
lambda: self.on_disconnection_complete(
|
|
192
|
+
initiating_address, target_address, disconnect_command
|
|
193
|
+
)
|
|
194
|
+
)
|
|
185
195
|
|
|
186
|
-
# pylint: disable=too-many-arguments
|
|
187
196
|
def on_connection_encrypted(
|
|
188
|
-
self,
|
|
197
|
+
self,
|
|
198
|
+
central_address: hci.Address,
|
|
199
|
+
peripheral_address: hci.Address,
|
|
200
|
+
rand: bytes,
|
|
201
|
+
ediv: int,
|
|
202
|
+
ltk: bytes,
|
|
189
203
|
):
|
|
190
204
|
logger.debug(f'*** ENCRYPTION {central_address} -> {peripheral_address}')
|
|
191
205
|
|
|
@@ -198,7 +212,7 @@ class LocalLink:
|
|
|
198
212
|
def create_cis(
|
|
199
213
|
self,
|
|
200
214
|
central_controller: controller.Controller,
|
|
201
|
-
peripheral_address: Address,
|
|
215
|
+
peripheral_address: hci.Address,
|
|
202
216
|
cig_id: int,
|
|
203
217
|
cis_id: int,
|
|
204
218
|
) -> None:
|
|
@@ -216,7 +230,7 @@ class LocalLink:
|
|
|
216
230
|
def accept_cis(
|
|
217
231
|
self,
|
|
218
232
|
peripheral_controller: controller.Controller,
|
|
219
|
-
central_address: Address,
|
|
233
|
+
central_address: hci.Address,
|
|
220
234
|
cig_id: int,
|
|
221
235
|
cis_id: int,
|
|
222
236
|
) -> None:
|
|
@@ -224,17 +238,16 @@ class LocalLink:
|
|
|
224
238
|
f'$$$ CIS Accept {peripheral_controller.random_address} -> {central_address}'
|
|
225
239
|
)
|
|
226
240
|
if central_controller := self.find_controller(central_address):
|
|
227
|
-
asyncio.get_running_loop()
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
asyncio.get_running_loop().call_soon(
|
|
241
|
+
loop = asyncio.get_running_loop()
|
|
242
|
+
loop.call_soon(central_controller.on_link_cis_established, cig_id, cis_id)
|
|
243
|
+
loop.call_soon(
|
|
231
244
|
peripheral_controller.on_link_cis_established, cig_id, cis_id
|
|
232
245
|
)
|
|
233
246
|
|
|
234
247
|
def disconnect_cis(
|
|
235
248
|
self,
|
|
236
249
|
initiator_controller: controller.Controller,
|
|
237
|
-
peer_address: Address,
|
|
250
|
+
peer_address: hci.Address,
|
|
238
251
|
cig_id: int,
|
|
239
252
|
cis_id: int,
|
|
240
253
|
) -> None:
|
|
@@ -242,138 +255,28 @@ class LocalLink:
|
|
|
242
255
|
f'$$$ CIS Disconnect {initiator_controller.random_address} -> {peer_address}'
|
|
243
256
|
)
|
|
244
257
|
if peer_controller := self.find_controller(peer_address):
|
|
245
|
-
asyncio.get_running_loop()
|
|
258
|
+
loop = asyncio.get_running_loop()
|
|
259
|
+
loop.call_soon(
|
|
246
260
|
initiator_controller.on_link_cis_disconnected, cig_id, cis_id
|
|
247
261
|
)
|
|
248
|
-
|
|
249
|
-
peer_controller.on_link_cis_disconnected, cig_id, cis_id
|
|
250
|
-
)
|
|
262
|
+
loop.call_soon(peer_controller.on_link_cis_disconnected, cig_id, cis_id)
|
|
251
263
|
|
|
252
264
|
############################################################
|
|
253
265
|
# Classic handlers
|
|
254
266
|
############################################################
|
|
255
267
|
|
|
256
|
-
def
|
|
257
|
-
logger.debug(
|
|
258
|
-
f'[Classic] {initiator_controller.public_address} connects to {responder_address}'
|
|
259
|
-
)
|
|
260
|
-
responder_controller = self.find_classic_controller(responder_address)
|
|
261
|
-
if responder_controller is None:
|
|
262
|
-
initiator_controller.on_classic_connection_complete(
|
|
263
|
-
responder_address, HCI_PAGE_TIMEOUT_ERROR
|
|
264
|
-
)
|
|
265
|
-
return
|
|
266
|
-
self.pending_classic_connection = (initiator_controller, responder_controller)
|
|
267
|
-
|
|
268
|
-
responder_controller.on_classic_connection_request(
|
|
269
|
-
initiator_controller.public_address,
|
|
270
|
-
HCI_Connection_Complete_Event.LinkType.ACL,
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
def classic_accept_connection(
|
|
274
|
-
self, responder_controller, initiator_address, responder_role
|
|
275
|
-
):
|
|
276
|
-
logger.debug(
|
|
277
|
-
f'[Classic] {responder_controller.public_address} accepts to connect {initiator_address}'
|
|
278
|
-
)
|
|
279
|
-
initiator_controller = self.find_classic_controller(initiator_address)
|
|
280
|
-
if initiator_controller is None:
|
|
281
|
-
responder_controller.on_classic_connection_complete(
|
|
282
|
-
responder_controller.public_address, HCI_PAGE_TIMEOUT_ERROR
|
|
283
|
-
)
|
|
284
|
-
return
|
|
285
|
-
|
|
286
|
-
async def task():
|
|
287
|
-
if responder_role != Role.PERIPHERAL:
|
|
288
|
-
initiator_controller.on_classic_role_change(
|
|
289
|
-
responder_controller.public_address, int(not (responder_role))
|
|
290
|
-
)
|
|
291
|
-
initiator_controller.on_classic_connection_complete(
|
|
292
|
-
responder_controller.public_address, HCI_SUCCESS
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
asyncio.create_task(task())
|
|
296
|
-
responder_controller.on_classic_role_change(
|
|
297
|
-
initiator_controller.public_address, responder_role
|
|
298
|
-
)
|
|
299
|
-
responder_controller.on_classic_connection_complete(
|
|
300
|
-
initiator_controller.public_address, HCI_SUCCESS
|
|
301
|
-
)
|
|
302
|
-
self.pending_classic_connection = None
|
|
303
|
-
|
|
304
|
-
def classic_disconnect(self, initiator_controller, responder_address, reason):
|
|
305
|
-
logger.debug(
|
|
306
|
-
f'[Classic] {initiator_controller.public_address} disconnects {responder_address}'
|
|
307
|
-
)
|
|
308
|
-
responder_controller = self.find_classic_controller(responder_address)
|
|
309
|
-
|
|
310
|
-
async def task():
|
|
311
|
-
initiator_controller.on_classic_disconnected(responder_address, reason)
|
|
312
|
-
|
|
313
|
-
asyncio.create_task(task())
|
|
314
|
-
responder_controller.on_classic_disconnected(
|
|
315
|
-
initiator_controller.public_address, reason
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
def classic_switch_role(
|
|
319
|
-
self, initiator_controller, responder_address, initiator_new_role
|
|
320
|
-
):
|
|
321
|
-
responder_controller = self.find_classic_controller(responder_address)
|
|
322
|
-
if responder_controller is None:
|
|
323
|
-
return
|
|
324
|
-
|
|
325
|
-
async def task():
|
|
326
|
-
initiator_controller.on_classic_role_change(
|
|
327
|
-
responder_address, initiator_new_role
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
asyncio.create_task(task())
|
|
331
|
-
responder_controller.on_classic_role_change(
|
|
332
|
-
initiator_controller.public_address, int(not (initiator_new_role))
|
|
333
|
-
)
|
|
334
|
-
|
|
335
|
-
def classic_sco_connect(
|
|
336
|
-
self,
|
|
337
|
-
initiator_controller: controller.Controller,
|
|
338
|
-
responder_address: Address,
|
|
339
|
-
link_type: int,
|
|
340
|
-
):
|
|
341
|
-
logger.debug(
|
|
342
|
-
f'[Classic] {initiator_controller.public_address} connects SCO to {responder_address}'
|
|
343
|
-
)
|
|
344
|
-
responder_controller = self.find_classic_controller(responder_address)
|
|
345
|
-
# Initiator controller should handle it.
|
|
346
|
-
assert responder_controller
|
|
347
|
-
|
|
348
|
-
responder_controller.on_classic_connection_request(
|
|
349
|
-
initiator_controller.public_address,
|
|
350
|
-
link_type,
|
|
351
|
-
)
|
|
352
|
-
|
|
353
|
-
def classic_accept_sco_connection(
|
|
268
|
+
def send_lmp_packet(
|
|
354
269
|
self,
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
270
|
+
sender_controller: controller.Controller,
|
|
271
|
+
receiver_address: hci.Address,
|
|
272
|
+
packet: lmp.Packet,
|
|
358
273
|
):
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
initiator_controller = self.find_classic_controller(initiator_address)
|
|
363
|
-
if initiator_controller is None:
|
|
364
|
-
responder_controller.on_classic_sco_connection_complete(
|
|
365
|
-
responder_controller.public_address,
|
|
366
|
-
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
|
367
|
-
link_type,
|
|
274
|
+
if not (receiver_controller := self.find_classic_controller(receiver_address)):
|
|
275
|
+
raise core.InvalidArgumentError(
|
|
276
|
+
f"Unable to find controller for address {receiver_address}"
|
|
368
277
|
)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
initiator_controller.on_classic_sco_connection_complete(
|
|
373
|
-
responder_controller.public_address, HCI_SUCCESS, link_type
|
|
278
|
+
asyncio.get_running_loop().call_soon(
|
|
279
|
+
lambda: receiver_controller.on_lmp_packet(
|
|
280
|
+
sender_controller.public_address, packet
|
|
374
281
|
)
|
|
375
|
-
|
|
376
|
-
asyncio.create_task(task())
|
|
377
|
-
responder_controller.on_classic_sco_connection_complete(
|
|
378
|
-
initiator_controller.public_address, HCI_SUCCESS, link_type
|
|
379
282
|
)
|
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('*'))
|