bumble 0.0.220__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 -473
- 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 +5 -3
- 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 +663 -391
- 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 +171 -142
- 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 +26 -159
- bumble/ll.py +200 -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 +2 -1
- 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 +22 -9
- bumble/vendor/android/hci.py +4 -2
- {bumble-0.0.220.dist-info → bumble-0.0.221.dist-info}/METADATA +3 -2
- {bumble-0.0.220.dist-info → bumble-0.0.221.dist-info}/RECORD +102 -101
- {bumble-0.0.220.dist-info → bumble-0.0.221.dist-info}/WHEEL +0 -0
- {bumble-0.0.220.dist-info → bumble-0.0.221.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.220.dist-info → bumble-0.0.221.dist-info}/licenses/LICENSE +0 -0
- {bumble-0.0.220.dist-info → bumble-0.0.221.dist-info}/top_level.txt +0 -0
bumble/link.py
CHANGED
|
@@ -19,9 +19,12 @@ import asyncio
|
|
|
19
19
|
# Imports
|
|
20
20
|
# -----------------------------------------------------------------------------
|
|
21
21
|
import logging
|
|
22
|
-
from typing import
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
23
|
|
|
24
|
-
from bumble import
|
|
24
|
+
from bumble import core, hci, ll, lmp
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from bumble import controller
|
|
25
28
|
|
|
26
29
|
# -----------------------------------------------------------------------------
|
|
27
30
|
# Logging
|
|
@@ -29,11 +32,6 @@ from bumble import controller, core, hci, lmp
|
|
|
29
32
|
logger = logging.getLogger(__name__)
|
|
30
33
|
|
|
31
34
|
|
|
32
|
-
# -----------------------------------------------------------------------------
|
|
33
|
-
# Utils
|
|
34
|
-
# -----------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
|
|
37
35
|
# -----------------------------------------------------------------------------
|
|
38
36
|
# TODO: add more support for various LL exchanges
|
|
39
37
|
# (see Vol 6, Part B - 2.4 DATA CHANNEL PDU)
|
|
@@ -47,7 +45,6 @@ class LocalLink:
|
|
|
47
45
|
|
|
48
46
|
def __init__(self):
|
|
49
47
|
self.controllers = set()
|
|
50
|
-
self.pending_connection = None
|
|
51
48
|
self.pending_classic_connection = None
|
|
52
49
|
|
|
53
50
|
############################################################
|
|
@@ -61,23 +58,21 @@ class LocalLink:
|
|
|
61
58
|
def remove_controller(self, controller: controller.Controller):
|
|
62
59
|
self.controllers.remove(controller)
|
|
63
60
|
|
|
64
|
-
def
|
|
61
|
+
def find_le_controller(self, address: hci.Address) -> controller.Controller | None:
|
|
65
62
|
for controller in self.controllers:
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
for connection in controller.le_connections.values():
|
|
64
|
+
if connection.self_address == address:
|
|
65
|
+
return controller
|
|
68
66
|
return None
|
|
69
67
|
|
|
70
68
|
def find_classic_controller(
|
|
71
69
|
self, address: hci.Address
|
|
72
|
-
) ->
|
|
70
|
+
) -> controller.Controller | None:
|
|
73
71
|
for controller in self.controllers:
|
|
74
72
|
if controller.public_address == address:
|
|
75
73
|
return controller
|
|
76
74
|
return None
|
|
77
75
|
|
|
78
|
-
def get_pending_connection(self):
|
|
79
|
-
return self.pending_connection
|
|
80
|
-
|
|
81
76
|
############################################################
|
|
82
77
|
# LE handlers
|
|
83
78
|
############################################################
|
|
@@ -85,12 +80,6 @@ class LocalLink:
|
|
|
85
80
|
def on_address_changed(self, controller):
|
|
86
81
|
pass
|
|
87
82
|
|
|
88
|
-
def send_advertising_data(self, sender_address: hci.Address, data: bytes):
|
|
89
|
-
# Send the advertising data to all controllers, except the sender
|
|
90
|
-
for controller in self.controllers:
|
|
91
|
-
if controller.random_address != sender_address:
|
|
92
|
-
controller.on_link_advertising_data(sender_address, data)
|
|
93
|
-
|
|
94
83
|
def send_acl_data(
|
|
95
84
|
self,
|
|
96
85
|
sender_controller: controller.Controller,
|
|
@@ -100,7 +89,7 @@ class LocalLink:
|
|
|
100
89
|
):
|
|
101
90
|
# Send the data to the first controller with a matching address
|
|
102
91
|
if transport == core.PhysicalTransport.LE:
|
|
103
|
-
destination_controller = self.
|
|
92
|
+
destination_controller = self.find_le_controller(destination_address)
|
|
104
93
|
source_address = sender_controller.random_address
|
|
105
94
|
elif transport == core.PhysicalTransport.BR_EDR:
|
|
106
95
|
destination_controller = self.find_classic_controller(destination_address)
|
|
@@ -115,152 +104,30 @@ class LocalLink:
|
|
|
115
104
|
)
|
|
116
105
|
)
|
|
117
106
|
|
|
118
|
-
def
|
|
119
|
-
# Check that we expect this call
|
|
120
|
-
if not self.pending_connection:
|
|
121
|
-
logger.warning('on_connection_complete with no pending connection')
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
central_address, le_create_connection_command = self.pending_connection
|
|
125
|
-
self.pending_connection = None
|
|
126
|
-
|
|
127
|
-
# Find the controller that initiated the connection
|
|
128
|
-
if not (central_controller := self.find_controller(central_address)):
|
|
129
|
-
logger.warning('!!! Initiating controller not found')
|
|
130
|
-
return
|
|
131
|
-
|
|
132
|
-
# Connect to the first controller with a matching address
|
|
133
|
-
if peripheral_controller := self.find_controller(
|
|
134
|
-
le_create_connection_command.peer_address
|
|
135
|
-
):
|
|
136
|
-
central_controller.on_link_peripheral_connection_complete(
|
|
137
|
-
le_create_connection_command, hci.HCI_SUCCESS
|
|
138
|
-
)
|
|
139
|
-
peripheral_controller.on_link_central_connected(central_address)
|
|
140
|
-
return
|
|
141
|
-
|
|
142
|
-
# No peripheral found
|
|
143
|
-
central_controller.on_link_peripheral_connection_complete(
|
|
144
|
-
le_create_connection_command, hci.HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
def connect(
|
|
107
|
+
def send_advertising_pdu(
|
|
148
108
|
self,
|
|
149
|
-
|
|
150
|
-
|
|
109
|
+
sender_controller: controller.Controller,
|
|
110
|
+
packet: ll.AdvertisingPdu,
|
|
151
111
|
):
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
self.pending_connection = (central_address, le_create_connection_command)
|
|
157
|
-
asyncio.get_running_loop().call_soon(self.on_connection_complete)
|
|
112
|
+
loop = asyncio.get_running_loop()
|
|
113
|
+
for c in self.controllers:
|
|
114
|
+
if c != sender_controller:
|
|
115
|
+
loop.call_soon(c.on_ll_advertising_pdu, packet)
|
|
158
116
|
|
|
159
|
-
def
|
|
117
|
+
def send_ll_control_pdu(
|
|
160
118
|
self,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
119
|
+
sender_address: hci.Address,
|
|
120
|
+
receiver_address: hci.Address,
|
|
121
|
+
packet: ll.ControlPdu,
|
|
164
122
|
):
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return
|
|
169
|
-
|
|
170
|
-
# Disconnect from the first controller with a matching address
|
|
171
|
-
if target_controller := self.find_controller(target_address):
|
|
172
|
-
target_controller.on_link_disconnected(
|
|
173
|
-
initiating_address, disconnect_command.reason
|
|
123
|
+
if not (receiver_controller := self.find_le_controller(receiver_address)):
|
|
124
|
+
raise core.InvalidArgumentError(
|
|
125
|
+
f"Unable to find controller for address {receiver_address}"
|
|
174
126
|
)
|
|
175
|
-
|
|
176
|
-
initiating_controller.on_link_disconnection_complete(
|
|
177
|
-
disconnect_command, hci.HCI_SUCCESS
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
def disconnect(
|
|
181
|
-
self,
|
|
182
|
-
initiating_address: hci.Address,
|
|
183
|
-
target_address: hci.Address,
|
|
184
|
-
disconnect_command: hci.HCI_Disconnect_Command,
|
|
185
|
-
):
|
|
186
|
-
logger.debug(
|
|
187
|
-
f'$$$ DISCONNECTION {initiating_address} -> '
|
|
188
|
-
f'{target_address}: reason = {disconnect_command.reason}'
|
|
189
|
-
)
|
|
190
127
|
asyncio.get_running_loop().call_soon(
|
|
191
|
-
lambda:
|
|
192
|
-
initiating_address, target_address, disconnect_command
|
|
193
|
-
)
|
|
128
|
+
lambda: receiver_controller.on_ll_control_pdu(sender_address, packet)
|
|
194
129
|
)
|
|
195
130
|
|
|
196
|
-
def on_connection_encrypted(
|
|
197
|
-
self,
|
|
198
|
-
central_address: hci.Address,
|
|
199
|
-
peripheral_address: hci.Address,
|
|
200
|
-
rand: bytes,
|
|
201
|
-
ediv: int,
|
|
202
|
-
ltk: bytes,
|
|
203
|
-
):
|
|
204
|
-
logger.debug(f'*** ENCRYPTION {central_address} -> {peripheral_address}')
|
|
205
|
-
|
|
206
|
-
if central_controller := self.find_controller(central_address):
|
|
207
|
-
central_controller.on_link_encrypted(peripheral_address, rand, ediv, ltk)
|
|
208
|
-
|
|
209
|
-
if peripheral_controller := self.find_controller(peripheral_address):
|
|
210
|
-
peripheral_controller.on_link_encrypted(central_address, rand, ediv, ltk)
|
|
211
|
-
|
|
212
|
-
def create_cis(
|
|
213
|
-
self,
|
|
214
|
-
central_controller: controller.Controller,
|
|
215
|
-
peripheral_address: hci.Address,
|
|
216
|
-
cig_id: int,
|
|
217
|
-
cis_id: int,
|
|
218
|
-
) -> None:
|
|
219
|
-
logger.debug(
|
|
220
|
-
f'$$$ CIS Request {central_controller.random_address} -> {peripheral_address}'
|
|
221
|
-
)
|
|
222
|
-
if peripheral_controller := self.find_controller(peripheral_address):
|
|
223
|
-
asyncio.get_running_loop().call_soon(
|
|
224
|
-
peripheral_controller.on_link_cis_request,
|
|
225
|
-
central_controller.random_address,
|
|
226
|
-
cig_id,
|
|
227
|
-
cis_id,
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
def accept_cis(
|
|
231
|
-
self,
|
|
232
|
-
peripheral_controller: controller.Controller,
|
|
233
|
-
central_address: hci.Address,
|
|
234
|
-
cig_id: int,
|
|
235
|
-
cis_id: int,
|
|
236
|
-
) -> None:
|
|
237
|
-
logger.debug(
|
|
238
|
-
f'$$$ CIS Accept {peripheral_controller.random_address} -> {central_address}'
|
|
239
|
-
)
|
|
240
|
-
if central_controller := self.find_controller(central_address):
|
|
241
|
-
loop = asyncio.get_running_loop()
|
|
242
|
-
loop.call_soon(central_controller.on_link_cis_established, cig_id, cis_id)
|
|
243
|
-
loop.call_soon(
|
|
244
|
-
peripheral_controller.on_link_cis_established, cig_id, cis_id
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
def disconnect_cis(
|
|
248
|
-
self,
|
|
249
|
-
initiator_controller: controller.Controller,
|
|
250
|
-
peer_address: hci.Address,
|
|
251
|
-
cig_id: int,
|
|
252
|
-
cis_id: int,
|
|
253
|
-
) -> None:
|
|
254
|
-
logger.debug(
|
|
255
|
-
f'$$$ CIS Disconnect {initiator_controller.random_address} -> {peer_address}'
|
|
256
|
-
)
|
|
257
|
-
if peer_controller := self.find_controller(peer_address):
|
|
258
|
-
loop = asyncio.get_running_loop()
|
|
259
|
-
loop.call_soon(
|
|
260
|
-
initiator_controller.on_link_cis_disconnected, cig_id, cis_id
|
|
261
|
-
)
|
|
262
|
-
loop.call_soon(peer_controller.on_link_cis_disconnected, cig_id, cis_id)
|
|
263
|
-
|
|
264
131
|
############################################################
|
|
265
132
|
# Classic handlers
|
|
266
133
|
############################################################
|
bumble/ll.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
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 dataclasses
|
|
21
|
+
from typing import ClassVar
|
|
22
|
+
|
|
23
|
+
from bumble import hci
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# -----------------------------------------------------------------------------
|
|
27
|
+
# Advertising PDU
|
|
28
|
+
# -----------------------------------------------------------------------------
|
|
29
|
+
class AdvertisingPdu:
|
|
30
|
+
"""Base Advertising Physical Channel PDU class.
|
|
31
|
+
|
|
32
|
+
See Core Spec 6.0, Volume 6, Part B, 2.3. Advertising physical channel PDU.
|
|
33
|
+
|
|
34
|
+
Currently these messages don't really follow the LL spec, because LL protocol is
|
|
35
|
+
context-aware and we don't have real physical transport.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclasses.dataclass
|
|
40
|
+
class ConnectInd(AdvertisingPdu):
|
|
41
|
+
initiator_address: hci.Address
|
|
42
|
+
advertiser_address: hci.Address
|
|
43
|
+
interval: int
|
|
44
|
+
latency: int
|
|
45
|
+
timeout: int
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclasses.dataclass
|
|
49
|
+
class AdvInd(AdvertisingPdu):
|
|
50
|
+
advertiser_address: hci.Address
|
|
51
|
+
data: bytes
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclasses.dataclass
|
|
55
|
+
class AdvDirectInd(AdvertisingPdu):
|
|
56
|
+
advertiser_address: hci.Address
|
|
57
|
+
target_address: hci.Address
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclasses.dataclass
|
|
61
|
+
class AdvNonConnInd(AdvertisingPdu):
|
|
62
|
+
advertiser_address: hci.Address
|
|
63
|
+
data: bytes
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclasses.dataclass
|
|
67
|
+
class AdvExtInd(AdvertisingPdu):
|
|
68
|
+
advertiser_address: hci.Address
|
|
69
|
+
data: bytes
|
|
70
|
+
|
|
71
|
+
target_address: hci.Address | None = None
|
|
72
|
+
adi: int | None = None
|
|
73
|
+
tx_power: int | None = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# -----------------------------------------------------------------------------
|
|
77
|
+
# LL Control PDU
|
|
78
|
+
# -----------------------------------------------------------------------------
|
|
79
|
+
class ControlPdu:
|
|
80
|
+
"""Base LL Control PDU Class.
|
|
81
|
+
|
|
82
|
+
See Core Spec 6.0, Volume 6, Part B, 2.4.2. LL Control PDU.
|
|
83
|
+
|
|
84
|
+
Currently these messages don't really follow the LL spec, because LL protocol is
|
|
85
|
+
context-aware and we don't have real physical transport.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
class Opcode(hci.SpecableEnum):
|
|
89
|
+
LL_CONNECTION_UPDATE_IND = 0x00
|
|
90
|
+
LL_CHANNEL_MAP_IND = 0x01
|
|
91
|
+
LL_TERMINATE_IND = 0x02
|
|
92
|
+
LL_ENC_REQ = 0x03
|
|
93
|
+
LL_ENC_RSP = 0x04
|
|
94
|
+
LL_START_ENC_REQ = 0x05
|
|
95
|
+
LL_START_ENC_RSP = 0x06
|
|
96
|
+
LL_UNKNOWN_RSP = 0x07
|
|
97
|
+
LL_FEATURE_REQ = 0x08
|
|
98
|
+
LL_FEATURE_RSP = 0x09
|
|
99
|
+
LL_PAUSE_ENC_REQ = 0x0A
|
|
100
|
+
LL_PAUSE_ENC_RSP = 0x0B
|
|
101
|
+
LL_VERSION_IND = 0x0C
|
|
102
|
+
LL_REJECT_IND = 0x0D
|
|
103
|
+
LL_PERIPHERAL_FEATURE_REQ = 0x0E
|
|
104
|
+
LL_CONNECTION_PARAM_REQ = 0x0F
|
|
105
|
+
LL_CONNECTION_PARAM_RSP = 0x10
|
|
106
|
+
LL_REJECT_EXT_IND = 0x11
|
|
107
|
+
LL_PING_REQ = 0x12
|
|
108
|
+
LL_PING_RSP = 0x13
|
|
109
|
+
LL_LENGTH_REQ = 0x14
|
|
110
|
+
LL_LENGTH_RSP = 0x15
|
|
111
|
+
LL_PHY_REQ = 0x16
|
|
112
|
+
LL_PHY_RSP = 0x17
|
|
113
|
+
LL_PHY_UPDATE_IND = 0x18
|
|
114
|
+
LL_MIN_USED_CHANNELS_IND = 0x19
|
|
115
|
+
LL_CTE_REQ = 0x1A
|
|
116
|
+
LL_CTE_RSP = 0x1B
|
|
117
|
+
LL_PERIODIC_SYNC_IND = 0x1C
|
|
118
|
+
LL_CLOCK_ACCURACY_REQ = 0x1D
|
|
119
|
+
LL_CLOCK_ACCURACY_RSP = 0x1E
|
|
120
|
+
LL_CIS_REQ = 0x1F
|
|
121
|
+
LL_CIS_RSP = 0x20
|
|
122
|
+
LL_CIS_IND = 0x21
|
|
123
|
+
LL_CIS_TERMINATE_IND = 0x22
|
|
124
|
+
LL_POWER_CONTROL_REQ = 0x23
|
|
125
|
+
LL_POWER_CONTROL_RSP = 0x24
|
|
126
|
+
LL_POWER_CHANGE_IND = 0x25
|
|
127
|
+
LL_SUBRATE_REQ = 0x26
|
|
128
|
+
LL_SUBRATE_IND = 0x27
|
|
129
|
+
LL_CHANNEL_REPORTING_IND = 0x28
|
|
130
|
+
LL_CHANNEL_STATUS_IND = 0x29
|
|
131
|
+
LL_PERIODIC_SYNC_WR_IND = 0x2A
|
|
132
|
+
LL_FEATURE_EXT_REQ = 0x2B
|
|
133
|
+
LL_FEATURE_EXT_RSP = 0x2C
|
|
134
|
+
LL_CS_SEC_RSP = 0x2D
|
|
135
|
+
LL_CS_CAPABILITIES_REQ = 0x2E
|
|
136
|
+
LL_CS_CAPABILITIES_RSP = 0x2F
|
|
137
|
+
LL_CS_CONFIG_REQ = 0x30
|
|
138
|
+
LL_CS_CONFIG_RSP = 0x31
|
|
139
|
+
LL_CS_REQ = 0x32
|
|
140
|
+
LL_CS_RSP = 0x33
|
|
141
|
+
LL_CS_IND = 0x34
|
|
142
|
+
LL_CS_TERMINATE_REQ = 0x35
|
|
143
|
+
LL_CS_FAE_REQ = 0x36
|
|
144
|
+
LL_CS_FAE_RSP = 0x37
|
|
145
|
+
LL_CS_CHANNEL_MAP_IND = 0x38
|
|
146
|
+
LL_CS_SEC_REQ = 0x39
|
|
147
|
+
LL_CS_TERMINATE_RSP = 0x3A
|
|
148
|
+
LL_FRAME_SPACE_REQ = 0x3B
|
|
149
|
+
LL_FRAME_SPACE_RSP = 0x3C
|
|
150
|
+
|
|
151
|
+
opcode: ClassVar[Opcode]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@dataclasses.dataclass
|
|
155
|
+
class TerminateInd(ControlPdu):
|
|
156
|
+
opcode = ControlPdu.Opcode.LL_TERMINATE_IND
|
|
157
|
+
|
|
158
|
+
error_code: int
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclasses.dataclass
|
|
162
|
+
class EncReq(ControlPdu):
|
|
163
|
+
opcode = ControlPdu.Opcode.LL_ENC_REQ
|
|
164
|
+
|
|
165
|
+
rand: bytes
|
|
166
|
+
ediv: int
|
|
167
|
+
ltk: bytes
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclasses.dataclass
|
|
171
|
+
class CisReq(ControlPdu):
|
|
172
|
+
opcode = ControlPdu.Opcode.LL_CIS_REQ
|
|
173
|
+
|
|
174
|
+
cig_id: int
|
|
175
|
+
cis_id: int
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclasses.dataclass
|
|
179
|
+
class CisRsp(ControlPdu):
|
|
180
|
+
opcode = ControlPdu.Opcode.LL_CIS_REQ
|
|
181
|
+
|
|
182
|
+
cig_id: int
|
|
183
|
+
cis_id: int
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dataclasses.dataclass
|
|
187
|
+
class CisInd(ControlPdu):
|
|
188
|
+
opcode = ControlPdu.Opcode.LL_CIS_REQ
|
|
189
|
+
|
|
190
|
+
cig_id: int
|
|
191
|
+
cis_id: int
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclasses.dataclass
|
|
195
|
+
class CisTerminateInd(ControlPdu):
|
|
196
|
+
opcode = ControlPdu.Opcode.LL_CIS_TERMINATE_IND
|
|
197
|
+
|
|
198
|
+
cig_id: int
|
|
199
|
+
cis_id: int
|
|
200
|
+
error_code: int
|
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
|