bumble 0.0.181__py3-none-any.whl → 0.0.182__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/apps/bench.py +302 -67
- bumble/apps/controller_info.py +30 -4
- bumble/apps/controller_loopback.py +200 -0
- bumble/apps/l2cap_bridge.py +32 -24
- bumble/controller.py +131 -23
- bumble/device.py +15 -3
- bumble/hci.py +37 -1
- bumble/l2cap.py +10 -5
- bumble/link.py +55 -1
- bumble/profiles/csip.py +60 -8
- bumble/rfcomm.py +7 -3
- bumble/transport/__init__.py +2 -1
- {bumble-0.0.181.dist-info → bumble-0.0.182.dist-info}/METADATA +1 -1
- {bumble-0.0.181.dist-info → bumble-0.0.182.dist-info}/RECORD +19 -18
- {bumble-0.0.181.dist-info → bumble-0.0.182.dist-info}/LICENSE +0 -0
- {bumble-0.0.181.dist-info → bumble-0.0.182.dist-info}/WHEEL +0 -0
- {bumble-0.0.181.dist-info → bumble-0.0.182.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.181.dist-info → bumble-0.0.182.dist-info}/top_level.txt +0 -0
bumble/apps/controller_info.py
CHANGED
|
@@ -18,9 +18,11 @@
|
|
|
18
18
|
import asyncio
|
|
19
19
|
import os
|
|
20
20
|
import logging
|
|
21
|
+
import time
|
|
22
|
+
|
|
21
23
|
import click
|
|
22
|
-
from bumble.company_ids import COMPANY_IDENTIFIERS
|
|
23
24
|
|
|
25
|
+
from bumble.company_ids import COMPANY_IDENTIFIERS
|
|
24
26
|
from bumble.colors import color
|
|
25
27
|
from bumble.core import name_or_number
|
|
26
28
|
from bumble.hci import (
|
|
@@ -48,6 +50,7 @@ from bumble.hci import (
|
|
|
48
50
|
HCI_LE_Read_Maximum_Advertising_Data_Length_Command,
|
|
49
51
|
HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND,
|
|
50
52
|
HCI_LE_Read_Suggested_Default_Data_Length_Command,
|
|
53
|
+
HCI_Read_Local_Version_Information_Command,
|
|
51
54
|
)
|
|
52
55
|
from bumble.host import Host
|
|
53
56
|
from bumble.transport import open_transport_or_link
|
|
@@ -166,7 +169,7 @@ async def get_acl_flow_control_info(host: Host) -> None:
|
|
|
166
169
|
|
|
167
170
|
|
|
168
171
|
# -----------------------------------------------------------------------------
|
|
169
|
-
async def async_main(transport):
|
|
172
|
+
async def async_main(latency_probes, transport):
|
|
170
173
|
print('<<< connecting to HCI...')
|
|
171
174
|
async with await open_transport_or_link(transport) as (hci_source, hci_sink):
|
|
172
175
|
print('<<< connected')
|
|
@@ -174,6 +177,23 @@ async def async_main(transport):
|
|
|
174
177
|
host = Host(hci_source, hci_sink)
|
|
175
178
|
await host.reset()
|
|
176
179
|
|
|
180
|
+
# Measure the latency if requested
|
|
181
|
+
latencies = []
|
|
182
|
+
if latency_probes:
|
|
183
|
+
for _ in range(latency_probes):
|
|
184
|
+
start = time.time()
|
|
185
|
+
await host.send_command(HCI_Read_Local_Version_Information_Command())
|
|
186
|
+
latencies.append(1000 * (time.time() - start))
|
|
187
|
+
print(
|
|
188
|
+
color('HCI Command Latency:', 'yellow'),
|
|
189
|
+
(
|
|
190
|
+
f'min={min(latencies):.2f}, '
|
|
191
|
+
f'max={max(latencies):.2f}, '
|
|
192
|
+
f'average={sum(latencies)/len(latencies):.2f}'
|
|
193
|
+
),
|
|
194
|
+
'\n',
|
|
195
|
+
)
|
|
196
|
+
|
|
177
197
|
# Print version
|
|
178
198
|
print(color('Version:', 'yellow'))
|
|
179
199
|
print(
|
|
@@ -209,10 +229,16 @@ async def async_main(transport):
|
|
|
209
229
|
|
|
210
230
|
# -----------------------------------------------------------------------------
|
|
211
231
|
@click.command()
|
|
232
|
+
@click.option(
|
|
233
|
+
'--latency-probes',
|
|
234
|
+
metavar='N',
|
|
235
|
+
type=int,
|
|
236
|
+
help='Send N commands to measure HCI transport latency statistics',
|
|
237
|
+
)
|
|
212
238
|
@click.argument('transport')
|
|
213
|
-
def main(transport):
|
|
239
|
+
def main(latency_probes, transport):
|
|
214
240
|
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
|
215
|
-
asyncio.run(async_main(transport))
|
|
241
|
+
asyncio.run(async_main(latency_probes, transport))
|
|
216
242
|
|
|
217
243
|
|
|
218
244
|
# -----------------------------------------------------------------------------
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Copyright 2024 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
|
+
import asyncio
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
import time
|
|
22
|
+
from typing import Optional
|
|
23
|
+
from bumble.colors import color
|
|
24
|
+
from bumble.hci import (
|
|
25
|
+
HCI_READ_LOOPBACK_MODE_COMMAND,
|
|
26
|
+
HCI_Read_Loopback_Mode_Command,
|
|
27
|
+
HCI_WRITE_LOOPBACK_MODE_COMMAND,
|
|
28
|
+
HCI_Write_Loopback_Mode_Command,
|
|
29
|
+
LoopbackMode,
|
|
30
|
+
)
|
|
31
|
+
from bumble.host import Host
|
|
32
|
+
from bumble.transport import open_transport_or_link
|
|
33
|
+
import click
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Loopback:
|
|
37
|
+
"""Send and receive ACL data packets in local loopback mode"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, packet_size: int, packet_count: int, transport: str):
|
|
40
|
+
self.transport = transport
|
|
41
|
+
self.packet_size = packet_size
|
|
42
|
+
self.packet_count = packet_count
|
|
43
|
+
self.connection_handle: Optional[int] = None
|
|
44
|
+
self.connection_event = asyncio.Event()
|
|
45
|
+
self.done = asyncio.Event()
|
|
46
|
+
self.expected_cid = 0
|
|
47
|
+
self.bytes_received = 0
|
|
48
|
+
self.start_timestamp = 0.0
|
|
49
|
+
self.last_timestamp = 0.0
|
|
50
|
+
|
|
51
|
+
def on_connection(self, connection_handle: int, *args):
|
|
52
|
+
"""Retrieve connection handle from new connection event"""
|
|
53
|
+
if not self.connection_event.is_set():
|
|
54
|
+
# save first connection handle for ACL
|
|
55
|
+
# subsequent connections are SCO
|
|
56
|
+
self.connection_handle = connection_handle
|
|
57
|
+
self.connection_event.set()
|
|
58
|
+
|
|
59
|
+
def on_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes):
|
|
60
|
+
"""Calculate packet receive speed"""
|
|
61
|
+
now = time.time()
|
|
62
|
+
print(f'<<< Received packet {cid}: {len(pdu)} bytes')
|
|
63
|
+
assert connection_handle == self.connection_handle
|
|
64
|
+
assert cid == self.expected_cid
|
|
65
|
+
self.expected_cid += 1
|
|
66
|
+
if cid == 0:
|
|
67
|
+
self.start_timestamp = now
|
|
68
|
+
else:
|
|
69
|
+
elapsed_since_start = now - self.start_timestamp
|
|
70
|
+
elapsed_since_last = now - self.last_timestamp
|
|
71
|
+
self.bytes_received += len(pdu)
|
|
72
|
+
instant_rx_speed = len(pdu) / elapsed_since_last
|
|
73
|
+
average_rx_speed = self.bytes_received / elapsed_since_start
|
|
74
|
+
print(
|
|
75
|
+
color(
|
|
76
|
+
f'@@@ RX speed: instant={instant_rx_speed:.4f},'
|
|
77
|
+
f' average={average_rx_speed:.4f}',
|
|
78
|
+
'cyan',
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
self.last_timestamp = now
|
|
83
|
+
|
|
84
|
+
if self.expected_cid == self.packet_count:
|
|
85
|
+
print(color('@@@ Received last packet', 'green'))
|
|
86
|
+
self.done.set()
|
|
87
|
+
|
|
88
|
+
async def run(self):
|
|
89
|
+
"""Run a loopback throughput test"""
|
|
90
|
+
print(color('>>> Connecting to HCI...', 'green'))
|
|
91
|
+
async with await open_transport_or_link(self.transport) as (
|
|
92
|
+
hci_source,
|
|
93
|
+
hci_sink,
|
|
94
|
+
):
|
|
95
|
+
print(color('>>> Connected', 'green'))
|
|
96
|
+
|
|
97
|
+
host = Host(hci_source, hci_sink)
|
|
98
|
+
await host.reset()
|
|
99
|
+
|
|
100
|
+
# make sure data can fit in one l2cap pdu
|
|
101
|
+
l2cap_header_size = 4
|
|
102
|
+
max_packet_size = host.acl_packet_queue.max_packet_size - l2cap_header_size
|
|
103
|
+
if self.packet_size > max_packet_size:
|
|
104
|
+
print(
|
|
105
|
+
color(
|
|
106
|
+
f'!!! Packet size ({self.packet_size}) larger than max supported'
|
|
107
|
+
f' size ({max_packet_size})',
|
|
108
|
+
'red',
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
if not host.supports_command(
|
|
114
|
+
HCI_WRITE_LOOPBACK_MODE_COMMAND
|
|
115
|
+
) or not host.supports_command(HCI_READ_LOOPBACK_MODE_COMMAND):
|
|
116
|
+
print(color('!!! Loopback mode not supported', 'red'))
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
# set event callbacks
|
|
120
|
+
host.on('connection', self.on_connection)
|
|
121
|
+
host.on('l2cap_pdu', self.on_l2cap_pdu)
|
|
122
|
+
|
|
123
|
+
loopback_mode = LoopbackMode.LOCAL
|
|
124
|
+
|
|
125
|
+
print(color('### Setting loopback mode', 'blue'))
|
|
126
|
+
await host.send_command(
|
|
127
|
+
HCI_Write_Loopback_Mode_Command(loopback_mode=LoopbackMode.LOCAL),
|
|
128
|
+
check_result=True,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
print(color('### Checking loopback mode', 'blue'))
|
|
132
|
+
response = await host.send_command(
|
|
133
|
+
HCI_Read_Loopback_Mode_Command(), check_result=True
|
|
134
|
+
)
|
|
135
|
+
if response.return_parameters.loopback_mode != loopback_mode:
|
|
136
|
+
print(color('!!! Loopback mode mismatch', 'red'))
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
await self.connection_event.wait()
|
|
140
|
+
print(color('### Connected', 'cyan'))
|
|
141
|
+
|
|
142
|
+
print(color('=== Start sending', 'magenta'))
|
|
143
|
+
start_time = time.time()
|
|
144
|
+
bytes_sent = 0
|
|
145
|
+
for cid in range(0, self.packet_count):
|
|
146
|
+
# using the cid as an incremental index
|
|
147
|
+
host.send_l2cap_pdu(
|
|
148
|
+
self.connection_handle, cid, bytes(self.packet_size)
|
|
149
|
+
)
|
|
150
|
+
print(
|
|
151
|
+
color(
|
|
152
|
+
f'>>> Sending packet {cid}: {self.packet_size} bytes', 'yellow'
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
bytes_sent += self.packet_size # don't count L2CAP or HCI header sizes
|
|
156
|
+
await asyncio.sleep(0) # yield to allow packet receive
|
|
157
|
+
|
|
158
|
+
await self.done.wait()
|
|
159
|
+
print(color('=== Done!', 'magenta'))
|
|
160
|
+
|
|
161
|
+
elapsed = time.time() - start_time
|
|
162
|
+
average_tx_speed = bytes_sent / elapsed
|
|
163
|
+
print(
|
|
164
|
+
color(
|
|
165
|
+
f'@@@ TX speed: average={average_tx_speed:.4f} ({bytes_sent} bytes'
|
|
166
|
+
f' in {elapsed:.2f} seconds)',
|
|
167
|
+
'green',
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# -----------------------------------------------------------------------------
|
|
173
|
+
@click.command()
|
|
174
|
+
@click.option(
|
|
175
|
+
'--packet-size',
|
|
176
|
+
'-s',
|
|
177
|
+
metavar='SIZE',
|
|
178
|
+
type=click.IntRange(8, 4096),
|
|
179
|
+
default=500,
|
|
180
|
+
help='Packet size',
|
|
181
|
+
)
|
|
182
|
+
@click.option(
|
|
183
|
+
'--packet-count',
|
|
184
|
+
'-c',
|
|
185
|
+
metavar='COUNT',
|
|
186
|
+
type=int,
|
|
187
|
+
default=10,
|
|
188
|
+
help='Packet count',
|
|
189
|
+
)
|
|
190
|
+
@click.argument('transport')
|
|
191
|
+
def main(packet_size, packet_count, transport):
|
|
192
|
+
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
|
193
|
+
|
|
194
|
+
loopback = Loopback(packet_size, packet_count, transport)
|
|
195
|
+
asyncio.run(loopback.run())
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# -----------------------------------------------------------------------------
|
|
199
|
+
if __name__ == '__main__':
|
|
200
|
+
main()
|
bumble/apps/l2cap_bridge.py
CHANGED
|
@@ -49,14 +49,16 @@ class ServerBridge:
|
|
|
49
49
|
self.tcp_port = tcp_port
|
|
50
50
|
|
|
51
51
|
async def start(self, device: Device) -> None:
|
|
52
|
-
# Listen for incoming L2CAP
|
|
52
|
+
# Listen for incoming L2CAP channel connections
|
|
53
53
|
device.create_l2cap_server(
|
|
54
54
|
spec=l2cap.LeCreditBasedChannelSpec(
|
|
55
55
|
psm=self.psm, mtu=self.mtu, mps=self.mps, max_credits=self.max_credits
|
|
56
56
|
),
|
|
57
|
-
handler=self.
|
|
57
|
+
handler=self.on_channel,
|
|
58
|
+
)
|
|
59
|
+
print(
|
|
60
|
+
color(f'### Listening for channel connection on PSM {self.psm}', 'yellow')
|
|
58
61
|
)
|
|
59
|
-
print(color(f'### Listening for CoC connection on PSM {self.psm}', 'yellow'))
|
|
60
62
|
|
|
61
63
|
def on_ble_connection(connection):
|
|
62
64
|
def on_ble_disconnection(reason):
|
|
@@ -73,7 +75,7 @@ class ServerBridge:
|
|
|
73
75
|
await device.start_advertising(auto_restart=True)
|
|
74
76
|
|
|
75
77
|
# Called when a new L2CAP connection is established
|
|
76
|
-
def
|
|
78
|
+
def on_channel(self, l2cap_channel):
|
|
77
79
|
print(color('*** L2CAP channel:', 'cyan'), l2cap_channel)
|
|
78
80
|
|
|
79
81
|
class Pipe:
|
|
@@ -83,7 +85,7 @@ class ServerBridge:
|
|
|
83
85
|
self.l2cap_channel = l2cap_channel
|
|
84
86
|
|
|
85
87
|
l2cap_channel.on('close', self.on_l2cap_close)
|
|
86
|
-
l2cap_channel.sink = self.
|
|
88
|
+
l2cap_channel.sink = self.on_channel_sdu
|
|
87
89
|
|
|
88
90
|
async def connect_to_tcp(self):
|
|
89
91
|
# Connect to the TCP server
|
|
@@ -128,7 +130,7 @@ class ServerBridge:
|
|
|
128
130
|
if self.tcp_transport is not None:
|
|
129
131
|
self.tcp_transport.close()
|
|
130
132
|
|
|
131
|
-
def
|
|
133
|
+
def on_channel_sdu(self, sdu):
|
|
132
134
|
print(color(f'<<< [L2CAP SDU]: {len(sdu)} bytes', 'cyan'))
|
|
133
135
|
if self.tcp_transport is None:
|
|
134
136
|
print(color('!!! TCP socket not open, dropping', 'red'))
|
|
@@ -183,7 +185,7 @@ class ClientBridge:
|
|
|
183
185
|
peer_name = writer.get_extra_info('peer_name')
|
|
184
186
|
print(color(f'<<< TCP connection from {peer_name}', 'magenta'))
|
|
185
187
|
|
|
186
|
-
def
|
|
188
|
+
def on_channel_sdu(sdu):
|
|
187
189
|
print(color(f'<<< [L2CAP SDU]: {len(sdu)} bytes', 'cyan'))
|
|
188
190
|
l2cap_to_tcp_pipe.write(sdu)
|
|
189
191
|
|
|
@@ -209,7 +211,7 @@ class ClientBridge:
|
|
|
209
211
|
writer.close()
|
|
210
212
|
return
|
|
211
213
|
|
|
212
|
-
l2cap_channel.sink =
|
|
214
|
+
l2cap_channel.sink = on_channel_sdu
|
|
213
215
|
l2cap_channel.on('close', on_l2cap_close)
|
|
214
216
|
|
|
215
217
|
# Start a flow control pipe from L2CAP to TCP
|
|
@@ -274,23 +276,29 @@ async def run(device_config, hci_transport, bridge):
|
|
|
274
276
|
@click.pass_context
|
|
275
277
|
@click.option('--device-config', help='Device configuration file', required=True)
|
|
276
278
|
@click.option('--hci-transport', help='HCI transport', required=True)
|
|
277
|
-
@click.option('--psm', help='PSM for L2CAP
|
|
279
|
+
@click.option('--psm', help='PSM for L2CAP', type=int, default=1234)
|
|
278
280
|
@click.option(
|
|
279
|
-
'--l2cap-
|
|
280
|
-
help='Maximum L2CAP
|
|
281
|
+
'--l2cap-max-credits',
|
|
282
|
+
help='Maximum L2CAP Credits',
|
|
281
283
|
type=click.IntRange(1, 65535),
|
|
282
284
|
default=128,
|
|
283
285
|
)
|
|
284
286
|
@click.option(
|
|
285
|
-
'--l2cap-
|
|
286
|
-
help='L2CAP
|
|
287
|
-
type=click.IntRange(
|
|
288
|
-
|
|
287
|
+
'--l2cap-mtu',
|
|
288
|
+
help='L2CAP MTU',
|
|
289
|
+
type=click.IntRange(
|
|
290
|
+
l2cap.L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MTU,
|
|
291
|
+
l2cap.L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_MTU,
|
|
292
|
+
),
|
|
293
|
+
default=1024,
|
|
289
294
|
)
|
|
290
295
|
@click.option(
|
|
291
|
-
'--l2cap-
|
|
292
|
-
help='L2CAP
|
|
293
|
-
type=click.IntRange(
|
|
296
|
+
'--l2cap-mps',
|
|
297
|
+
help='L2CAP MPS',
|
|
298
|
+
type=click.IntRange(
|
|
299
|
+
l2cap.L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MPS,
|
|
300
|
+
l2cap.L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_MPS,
|
|
301
|
+
),
|
|
294
302
|
default=1024,
|
|
295
303
|
)
|
|
296
304
|
def cli(
|
|
@@ -298,17 +306,17 @@ def cli(
|
|
|
298
306
|
device_config,
|
|
299
307
|
hci_transport,
|
|
300
308
|
psm,
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
309
|
+
l2cap_max_credits,
|
|
310
|
+
l2cap_mtu,
|
|
311
|
+
l2cap_mps,
|
|
304
312
|
):
|
|
305
313
|
context.ensure_object(dict)
|
|
306
314
|
context.obj['device_config'] = device_config
|
|
307
315
|
context.obj['hci_transport'] = hci_transport
|
|
308
316
|
context.obj['psm'] = psm
|
|
309
|
-
context.obj['max_credits'] =
|
|
310
|
-
context.obj['mtu'] =
|
|
311
|
-
context.obj['mps'] =
|
|
317
|
+
context.obj['max_credits'] = l2cap_max_credits
|
|
318
|
+
context.obj['mtu'] = l2cap_mtu
|
|
319
|
+
context.obj['mps'] = l2cap_mps
|
|
312
320
|
|
|
313
321
|
|
|
314
322
|
# -----------------------------------------------------------------------------
|
bumble/controller.py
CHANGED
|
@@ -19,6 +19,7 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
21
|
import asyncio
|
|
22
|
+
import dataclasses
|
|
22
23
|
import itertools
|
|
23
24
|
import random
|
|
24
25
|
import struct
|
|
@@ -42,6 +43,7 @@ from bumble.hci import (
|
|
|
42
43
|
HCI_LE_1M_PHY,
|
|
43
44
|
HCI_SUCCESS,
|
|
44
45
|
HCI_UNKNOWN_HCI_COMMAND_ERROR,
|
|
46
|
+
HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
|
45
47
|
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
|
46
48
|
HCI_VERSION_BLUETOOTH_CORE_5_0,
|
|
47
49
|
Address,
|
|
@@ -53,6 +55,7 @@ from bumble.hci import (
|
|
|
53
55
|
HCI_Connection_Request_Event,
|
|
54
56
|
HCI_Disconnection_Complete_Event,
|
|
55
57
|
HCI_Encryption_Change_Event,
|
|
58
|
+
HCI_Synchronous_Connection_Complete_Event,
|
|
56
59
|
HCI_LE_Advertising_Report_Event,
|
|
57
60
|
HCI_LE_Connection_Complete_Event,
|
|
58
61
|
HCI_LE_Read_Remote_Features_Complete_Event,
|
|
@@ -60,10 +63,11 @@ from bumble.hci import (
|
|
|
60
63
|
HCI_Packet,
|
|
61
64
|
HCI_Role_Change_Event,
|
|
62
65
|
)
|
|
63
|
-
from typing import Optional, Union, Dict, TYPE_CHECKING
|
|
66
|
+
from typing import Optional, Union, Dict, Any, TYPE_CHECKING
|
|
64
67
|
|
|
65
68
|
if TYPE_CHECKING:
|
|
66
|
-
from bumble.
|
|
69
|
+
from bumble.link import LocalLink
|
|
70
|
+
from bumble.transport.common import TransportSink
|
|
67
71
|
|
|
68
72
|
# -----------------------------------------------------------------------------
|
|
69
73
|
# Logging
|
|
@@ -79,15 +83,18 @@ class DataObject:
|
|
|
79
83
|
|
|
80
84
|
|
|
81
85
|
# -----------------------------------------------------------------------------
|
|
86
|
+
@dataclasses.dataclass
|
|
82
87
|
class Connection:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
controller: Controller
|
|
89
|
+
handle: int
|
|
90
|
+
role: int
|
|
91
|
+
peer_address: Address
|
|
92
|
+
link: Any
|
|
93
|
+
transport: int
|
|
94
|
+
link_type: int
|
|
95
|
+
|
|
96
|
+
def __post_init__(self):
|
|
89
97
|
self.assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu)
|
|
90
|
-
self.transport = transport
|
|
91
98
|
|
|
92
99
|
def on_hci_acl_data_packet(self, packet):
|
|
93
100
|
self.assembler.feed_packet(packet)
|
|
@@ -106,10 +113,10 @@ class Connection:
|
|
|
106
113
|
class Controller:
|
|
107
114
|
def __init__(
|
|
108
115
|
self,
|
|
109
|
-
name,
|
|
116
|
+
name: str,
|
|
110
117
|
host_source=None,
|
|
111
118
|
host_sink: Optional[TransportSink] = None,
|
|
112
|
-
link=None,
|
|
119
|
+
link: Optional[LocalLink] = None,
|
|
113
120
|
public_address: Optional[Union[bytes, str, Address]] = None,
|
|
114
121
|
):
|
|
115
122
|
self.name = name
|
|
@@ -359,12 +366,13 @@ class Controller:
|
|
|
359
366
|
if connection is None:
|
|
360
367
|
connection_handle = self.allocate_connection_handle()
|
|
361
368
|
connection = Connection(
|
|
362
|
-
self,
|
|
363
|
-
connection_handle,
|
|
364
|
-
BT_PERIPHERAL_ROLE,
|
|
365
|
-
peer_address,
|
|
366
|
-
self.link,
|
|
367
|
-
BT_LE_TRANSPORT,
|
|
369
|
+
controller=self,
|
|
370
|
+
handle=connection_handle,
|
|
371
|
+
role=BT_PERIPHERAL_ROLE,
|
|
372
|
+
peer_address=peer_address,
|
|
373
|
+
link=self.link,
|
|
374
|
+
transport=BT_LE_TRANSPORT,
|
|
375
|
+
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
|
368
376
|
)
|
|
369
377
|
self.peripheral_connections[peer_address] = connection
|
|
370
378
|
logger.debug(f'New PERIPHERAL connection handle: 0x{connection_handle:04X}')
|
|
@@ -418,12 +426,13 @@ class Controller:
|
|
|
418
426
|
if connection is None:
|
|
419
427
|
connection_handle = self.allocate_connection_handle()
|
|
420
428
|
connection = Connection(
|
|
421
|
-
self,
|
|
422
|
-
connection_handle,
|
|
423
|
-
BT_CENTRAL_ROLE,
|
|
424
|
-
peer_address,
|
|
425
|
-
self.link,
|
|
426
|
-
BT_LE_TRANSPORT,
|
|
429
|
+
controller=self,
|
|
430
|
+
handle=connection_handle,
|
|
431
|
+
role=BT_CENTRAL_ROLE,
|
|
432
|
+
peer_address=peer_address,
|
|
433
|
+
link=self.link,
|
|
434
|
+
transport=BT_LE_TRANSPORT,
|
|
435
|
+
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
|
427
436
|
)
|
|
428
437
|
self.central_connections[peer_address] = connection
|
|
429
438
|
logger.debug(
|
|
@@ -568,6 +577,7 @@ class Controller:
|
|
|
568
577
|
peer_address=peer_address,
|
|
569
578
|
link=self.link,
|
|
570
579
|
transport=BT_BR_EDR_TRANSPORT,
|
|
580
|
+
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
|
571
581
|
)
|
|
572
582
|
self.classic_connections[peer_address] = connection
|
|
573
583
|
logger.debug(
|
|
@@ -621,6 +631,42 @@ class Controller:
|
|
|
621
631
|
)
|
|
622
632
|
)
|
|
623
633
|
|
|
634
|
+
def on_classic_sco_connection_complete(
|
|
635
|
+
self, peer_address: Address, status: int, link_type: int
|
|
636
|
+
):
|
|
637
|
+
if status == HCI_SUCCESS:
|
|
638
|
+
# Allocate (or reuse) a connection handle
|
|
639
|
+
connection_handle = self.allocate_connection_handle()
|
|
640
|
+
connection = Connection(
|
|
641
|
+
controller=self,
|
|
642
|
+
handle=connection_handle,
|
|
643
|
+
# Role doesn't matter in SCO.
|
|
644
|
+
role=BT_CENTRAL_ROLE,
|
|
645
|
+
peer_address=peer_address,
|
|
646
|
+
link=self.link,
|
|
647
|
+
transport=BT_BR_EDR_TRANSPORT,
|
|
648
|
+
link_type=link_type,
|
|
649
|
+
)
|
|
650
|
+
self.classic_connections[peer_address] = connection
|
|
651
|
+
logger.debug(f'New SCO connection handle: 0x{connection_handle:04X}')
|
|
652
|
+
else:
|
|
653
|
+
connection_handle = 0
|
|
654
|
+
|
|
655
|
+
self.send_hci_packet(
|
|
656
|
+
HCI_Synchronous_Connection_Complete_Event(
|
|
657
|
+
status=status,
|
|
658
|
+
connection_handle=connection_handle,
|
|
659
|
+
bd_addr=peer_address,
|
|
660
|
+
link_type=link_type,
|
|
661
|
+
# TODO: Provide SCO connection parameters.
|
|
662
|
+
transmission_interval=0,
|
|
663
|
+
retransmission_window=0,
|
|
664
|
+
rx_packet_length=0,
|
|
665
|
+
tx_packet_length=0,
|
|
666
|
+
air_mode=0,
|
|
667
|
+
)
|
|
668
|
+
)
|
|
669
|
+
|
|
624
670
|
############################################################
|
|
625
671
|
# Advertising support
|
|
626
672
|
############################################################
|
|
@@ -740,6 +786,68 @@ class Controller:
|
|
|
740
786
|
)
|
|
741
787
|
self.link.classic_accept_connection(self, command.bd_addr, command.role)
|
|
742
788
|
|
|
789
|
+
def on_hci_enhanced_setup_synchronous_connection_command(self, command):
|
|
790
|
+
'''
|
|
791
|
+
See Bluetooth spec Vol 4, Part E - 7.1.45 Enhanced Setup Synchronous Connection command
|
|
792
|
+
'''
|
|
793
|
+
|
|
794
|
+
if self.link is None:
|
|
795
|
+
return
|
|
796
|
+
|
|
797
|
+
if not (
|
|
798
|
+
connection := self.find_classic_connection_by_handle(
|
|
799
|
+
command.connection_handle
|
|
800
|
+
)
|
|
801
|
+
):
|
|
802
|
+
self.send_hci_packet(
|
|
803
|
+
HCI_Command_Status_Event(
|
|
804
|
+
status=HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
|
805
|
+
num_hci_command_packets=1,
|
|
806
|
+
command_opcode=command.op_code,
|
|
807
|
+
)
|
|
808
|
+
)
|
|
809
|
+
return
|
|
810
|
+
|
|
811
|
+
self.send_hci_packet(
|
|
812
|
+
HCI_Command_Status_Event(
|
|
813
|
+
status=HCI_SUCCESS,
|
|
814
|
+
num_hci_command_packets=1,
|
|
815
|
+
command_opcode=command.op_code,
|
|
816
|
+
)
|
|
817
|
+
)
|
|
818
|
+
self.link.classic_sco_connect(
|
|
819
|
+
self, connection.peer_address, HCI_Connection_Complete_Event.ESCO_LINK_TYPE
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
def on_hci_enhanced_accept_synchronous_connection_request_command(self, command):
|
|
823
|
+
'''
|
|
824
|
+
See Bluetooth spec Vol 4, Part E - 7.1.46 Enhanced Accept Synchronous Connection Request command
|
|
825
|
+
'''
|
|
826
|
+
|
|
827
|
+
if self.link is None:
|
|
828
|
+
return
|
|
829
|
+
|
|
830
|
+
if not (connection := self.find_classic_connection_by_address(command.bd_addr)):
|
|
831
|
+
self.send_hci_packet(
|
|
832
|
+
HCI_Command_Status_Event(
|
|
833
|
+
status=HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
|
|
834
|
+
num_hci_command_packets=1,
|
|
835
|
+
command_opcode=command.op_code,
|
|
836
|
+
)
|
|
837
|
+
)
|
|
838
|
+
return
|
|
839
|
+
|
|
840
|
+
self.send_hci_packet(
|
|
841
|
+
HCI_Command_Status_Event(
|
|
842
|
+
status=HCI_SUCCESS,
|
|
843
|
+
num_hci_command_packets=1,
|
|
844
|
+
command_opcode=command.op_code,
|
|
845
|
+
)
|
|
846
|
+
)
|
|
847
|
+
self.link.classic_accept_sco_connection(
|
|
848
|
+
self, connection.peer_address, HCI_Connection_Complete_Event.ESCO_LINK_TYPE
|
|
849
|
+
)
|
|
850
|
+
|
|
743
851
|
def on_hci_switch_role_command(self, command):
|
|
744
852
|
'''
|
|
745
853
|
See Bluetooth spec Vol 4, Part E - 7.2.8 Switch Role command
|
bumble/device.py
CHANGED
|
@@ -61,7 +61,6 @@ from .hci import (
|
|
|
61
61
|
HCI_LE_1M_PHY_BIT,
|
|
62
62
|
HCI_LE_2M_PHY,
|
|
63
63
|
HCI_LE_2M_PHY_LE_SUPPORTED_FEATURE,
|
|
64
|
-
HCI_LE_CLEAR_RESOLVING_LIST_COMMAND,
|
|
65
64
|
HCI_LE_CODED_PHY,
|
|
66
65
|
HCI_LE_CODED_PHY_BIT,
|
|
67
66
|
HCI_LE_CODED_PHY_LE_SUPPORTED_FEATURE,
|
|
@@ -86,7 +85,7 @@ from .hci import (
|
|
|
86
85
|
HCI_Constant,
|
|
87
86
|
HCI_Create_Connection_Cancel_Command,
|
|
88
87
|
HCI_Create_Connection_Command,
|
|
89
|
-
|
|
88
|
+
HCI_Connection_Complete_Event,
|
|
90
89
|
HCI_Disconnect_Command,
|
|
91
90
|
HCI_Encryption_Change_Event,
|
|
92
91
|
HCI_Error,
|
|
@@ -3319,8 +3318,21 @@ class Device(CompositeEventEmitter):
|
|
|
3319
3318
|
def on_connection_request(self, bd_addr, class_of_device, link_type):
|
|
3320
3319
|
logger.debug(f'*** Connection request: {bd_addr}')
|
|
3321
3320
|
|
|
3321
|
+
# Handle SCO request.
|
|
3322
|
+
if link_type in (
|
|
3323
|
+
HCI_Connection_Complete_Event.SCO_LINK_TYPE,
|
|
3324
|
+
HCI_Connection_Complete_Event.ESCO_LINK_TYPE,
|
|
3325
|
+
):
|
|
3326
|
+
if connection := self.find_connection_by_bd_addr(
|
|
3327
|
+
bd_addr, transport=BT_BR_EDR_TRANSPORT
|
|
3328
|
+
):
|
|
3329
|
+
self.emit('sco_request', connection, link_type)
|
|
3330
|
+
else:
|
|
3331
|
+
logger.error(f'SCO request from a non-connected device {bd_addr}')
|
|
3332
|
+
return
|
|
3333
|
+
|
|
3322
3334
|
# match a pending future using `bd_addr`
|
|
3323
|
-
|
|
3335
|
+
elif bd_addr in self.classic_pending_accepts:
|
|
3324
3336
|
future, *_ = self.classic_pending_accepts.pop(bd_addr)
|
|
3325
3337
|
future.set_result((bd_addr, class_of_device, link_type))
|
|
3326
3338
|
|