bumble 0.0.204__py3-none-any.whl → 0.0.208__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 +9 -4
- bumble/apps/auracast.py +631 -98
- bumble/apps/bench.py +238 -157
- bumble/apps/console.py +19 -12
- bumble/apps/controller_info.py +23 -7
- bumble/apps/device_info.py +50 -4
- bumble/apps/gg_bridge.py +1 -1
- bumble/apps/lea_unicast/app.py +61 -201
- bumble/att.py +51 -37
- bumble/audio/__init__.py +17 -0
- bumble/audio/io.py +553 -0
- bumble/controller.py +24 -9
- bumble/core.py +305 -156
- bumble/device.py +1090 -99
- bumble/gatt.py +36 -226
- bumble/gatt_adapters.py +374 -0
- bumble/gatt_client.py +52 -33
- bumble/gatt_server.py +5 -5
- bumble/hci.py +812 -14
- bumble/host.py +367 -65
- bumble/l2cap.py +3 -16
- bumble/pairing.py +5 -5
- bumble/pandora/host.py +7 -12
- bumble/profiles/aics.py +48 -57
- bumble/profiles/ascs.py +8 -19
- bumble/profiles/asha.py +16 -14
- bumble/profiles/bass.py +16 -22
- bumble/profiles/battery_service.py +13 -3
- bumble/profiles/device_information_service.py +16 -14
- bumble/profiles/gap.py +12 -8
- bumble/profiles/gatt_service.py +167 -0
- bumble/profiles/gmap.py +198 -0
- bumble/profiles/hap.py +8 -6
- bumble/profiles/heart_rate_service.py +20 -4
- bumble/profiles/le_audio.py +87 -4
- bumble/profiles/mcp.py +11 -9
- bumble/profiles/pacs.py +61 -16
- bumble/profiles/tmap.py +8 -12
- bumble/profiles/{vcp.py → vcs.py} +35 -29
- bumble/profiles/vocs.py +62 -85
- bumble/sdp.py +223 -93
- bumble/smp.py +1 -1
- bumble/utils.py +12 -2
- bumble/vendor/android/hci.py +1 -1
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/METADATA +13 -11
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/RECORD +50 -46
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/WHEEL +1 -1
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/entry_points.txt +1 -0
- bumble/apps/lea_unicast/liblc3.wasm +0 -0
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/LICENSE +0 -0
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/top_level.txt +0 -0
bumble/host.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021-
|
|
1
|
+
# Copyright 2021-2025 Google LLC
|
|
2
2
|
#
|
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
# you may not use this file except in compliance with the License.
|
|
@@ -34,6 +34,8 @@ from typing import (
|
|
|
34
34
|
TYPE_CHECKING,
|
|
35
35
|
)
|
|
36
36
|
|
|
37
|
+
import pyee
|
|
38
|
+
|
|
37
39
|
from bumble.colors import color
|
|
38
40
|
from bumble.l2cap import L2CAP_PDU
|
|
39
41
|
from bumble.snoop import Snooper
|
|
@@ -59,7 +61,19 @@ logger = logging.getLogger(__name__)
|
|
|
59
61
|
|
|
60
62
|
|
|
61
63
|
# -----------------------------------------------------------------------------
|
|
62
|
-
class
|
|
64
|
+
class DataPacketQueue(pyee.EventEmitter):
|
|
65
|
+
"""
|
|
66
|
+
Flow-control queue for host->controller data packets (ACL, ISO).
|
|
67
|
+
|
|
68
|
+
The queue holds packets associated with a connection handle. The packets
|
|
69
|
+
are sent to the controller, up to a maximum total number of packets in flight.
|
|
70
|
+
A packet is considered to be "in flight" when it has been sent to the controller
|
|
71
|
+
but not completed yet. Packets are no longer "in flight" when the controller
|
|
72
|
+
declares them as completed.
|
|
73
|
+
|
|
74
|
+
The queue emits a 'flow' event whenever one or more packets are completed.
|
|
75
|
+
"""
|
|
76
|
+
|
|
63
77
|
max_packet_size: int
|
|
64
78
|
|
|
65
79
|
def __init__(
|
|
@@ -68,40 +82,105 @@ class AclPacketQueue:
|
|
|
68
82
|
max_in_flight: int,
|
|
69
83
|
send: Callable[[hci.HCI_Packet], None],
|
|
70
84
|
) -> None:
|
|
85
|
+
super().__init__()
|
|
71
86
|
self.max_packet_size = max_packet_size
|
|
72
87
|
self.max_in_flight = max_in_flight
|
|
73
|
-
self.
|
|
74
|
-
self.
|
|
75
|
-
|
|
88
|
+
self._in_flight = 0 # Total number of packets in flight across all connections
|
|
89
|
+
self._in_flight_per_connection: dict[int, int] = collections.defaultdict(
|
|
90
|
+
int
|
|
91
|
+
) # Number of packets in flight per connection
|
|
92
|
+
self._send = send
|
|
93
|
+
self._packets: Deque[tuple[hci.HCI_Packet, int]] = collections.deque()
|
|
94
|
+
self._queued = 0
|
|
95
|
+
self._completed = 0
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def queued(self) -> int:
|
|
99
|
+
"""Total number of packets queued since creation."""
|
|
100
|
+
return self._queued
|
|
76
101
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
102
|
+
@property
|
|
103
|
+
def completed(self) -> int:
|
|
104
|
+
"""Total number of packets completed since creation."""
|
|
105
|
+
return self._completed
|
|
80
106
|
|
|
81
|
-
|
|
107
|
+
@property
|
|
108
|
+
def pending(self) -> int:
|
|
109
|
+
"""Number of packets that have been queued but not completed."""
|
|
110
|
+
return self._queued - self._completed
|
|
111
|
+
|
|
112
|
+
def enqueue(self, packet: hci.HCI_Packet, connection_handle: int) -> None:
|
|
113
|
+
"""Enqueue a packet associated with a connection"""
|
|
114
|
+
self._packets.appendleft((packet, connection_handle))
|
|
115
|
+
self._queued += 1
|
|
116
|
+
self._check_queue()
|
|
117
|
+
|
|
118
|
+
if self._packets:
|
|
82
119
|
logger.debug(
|
|
83
|
-
f'{self.
|
|
84
|
-
f'{len(self.
|
|
120
|
+
f'{self._in_flight} packets in flight, '
|
|
121
|
+
f'{len(self._packets)} in queue'
|
|
85
122
|
)
|
|
86
123
|
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
124
|
+
def flush(self, connection_handle: int) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Remove all packets associated with a connection.
|
|
127
|
+
|
|
128
|
+
All packets associated with the connection that are in flight are implicitly
|
|
129
|
+
marked as completed, but no 'flow' event is emitted.
|
|
130
|
+
"""
|
|
92
131
|
|
|
93
|
-
|
|
94
|
-
|
|
132
|
+
packets_to_keep = [
|
|
133
|
+
(packet, handle)
|
|
134
|
+
for (packet, handle) in self._packets
|
|
135
|
+
if handle != connection_handle
|
|
136
|
+
]
|
|
137
|
+
if flushed_count := len(self._packets) - len(packets_to_keep):
|
|
138
|
+
self._completed += flushed_count
|
|
139
|
+
self._packets = collections.deque(packets_to_keep)
|
|
140
|
+
|
|
141
|
+
if connection_handle in self._in_flight_per_connection:
|
|
142
|
+
in_flight = self._in_flight_per_connection[connection_handle]
|
|
143
|
+
self._completed += in_flight
|
|
144
|
+
self._in_flight -= in_flight
|
|
145
|
+
del self._in_flight_per_connection[connection_handle]
|
|
146
|
+
|
|
147
|
+
def _check_queue(self) -> None:
|
|
148
|
+
while self._packets and self._in_flight < self.max_in_flight:
|
|
149
|
+
packet, connection_handle = self._packets.pop()
|
|
150
|
+
self._send(packet)
|
|
151
|
+
self._in_flight += 1
|
|
152
|
+
self._in_flight_per_connection[connection_handle] += 1
|
|
153
|
+
|
|
154
|
+
def on_packets_completed(self, packet_count: int, connection_handle: int) -> None:
|
|
155
|
+
"""Mark one or more packets associated with a connection as completed."""
|
|
156
|
+
if connection_handle not in self._in_flight_per_connection:
|
|
95
157
|
logger.warning(
|
|
96
|
-
|
|
97
|
-
'!!! {packet_count} completed but only '
|
|
98
|
-
f'{self.in_flight} in flight'
|
|
99
|
-
)
|
|
158
|
+
f'received completion for unknown connection {connection_handle}'
|
|
100
159
|
)
|
|
101
|
-
|
|
160
|
+
return
|
|
102
161
|
|
|
103
|
-
self.
|
|
104
|
-
|
|
162
|
+
in_flight_for_connection = self._in_flight_per_connection[connection_handle]
|
|
163
|
+
if packet_count <= in_flight_for_connection:
|
|
164
|
+
self._in_flight_per_connection[connection_handle] -= packet_count
|
|
165
|
+
else:
|
|
166
|
+
logger.warning(
|
|
167
|
+
f'{packet_count} completed for {connection_handle} '
|
|
168
|
+
f'but only {in_flight_for_connection} in flight'
|
|
169
|
+
)
|
|
170
|
+
self._in_flight_per_connection[connection_handle] = 0
|
|
171
|
+
|
|
172
|
+
if packet_count <= self._in_flight:
|
|
173
|
+
self._in_flight -= packet_count
|
|
174
|
+
self._completed += packet_count
|
|
175
|
+
else:
|
|
176
|
+
logger.warning(
|
|
177
|
+
f'{packet_count} completed but only {self._in_flight} in flight'
|
|
178
|
+
)
|
|
179
|
+
self._in_flight = 0
|
|
180
|
+
self._completed = self._queued
|
|
181
|
+
|
|
182
|
+
self._check_queue()
|
|
183
|
+
self.emit('flow')
|
|
105
184
|
|
|
106
185
|
|
|
107
186
|
# -----------------------------------------------------------------------------
|
|
@@ -114,7 +193,7 @@ class Connection:
|
|
|
114
193
|
self.peer_address = peer_address
|
|
115
194
|
self.assembler = hci.HCI_AclDataPacketAssembler(self.on_acl_pdu)
|
|
116
195
|
self.transport = transport
|
|
117
|
-
acl_packet_queue: Optional[
|
|
196
|
+
acl_packet_queue: Optional[DataPacketQueue] = (
|
|
118
197
|
host.le_acl_packet_queue
|
|
119
198
|
if transport == BT_LE_TRANSPORT
|
|
120
199
|
else host.acl_packet_queue
|
|
@@ -129,28 +208,37 @@ class Connection:
|
|
|
129
208
|
l2cap_pdu = L2CAP_PDU.from_bytes(pdu)
|
|
130
209
|
self.host.on_l2cap_pdu(self, l2cap_pdu.cid, l2cap_pdu.payload)
|
|
131
210
|
|
|
211
|
+
def __str__(self) -> str:
|
|
212
|
+
return (
|
|
213
|
+
f'Connection(transport={self.transport}, peer_address={self.peer_address})'
|
|
214
|
+
)
|
|
215
|
+
|
|
132
216
|
|
|
133
217
|
# -----------------------------------------------------------------------------
|
|
134
218
|
@dataclasses.dataclass
|
|
135
219
|
class ScoLink:
|
|
136
220
|
peer_address: hci.Address
|
|
137
|
-
|
|
221
|
+
connection_handle: int
|
|
138
222
|
|
|
139
223
|
|
|
140
224
|
# -----------------------------------------------------------------------------
|
|
141
225
|
@dataclasses.dataclass
|
|
142
|
-
class
|
|
143
|
-
peer_address: hci.Address
|
|
226
|
+
class IsoLink:
|
|
144
227
|
handle: int
|
|
228
|
+
packet_queue: DataPacketQueue = dataclasses.field(repr=False)
|
|
229
|
+
packet_sequence_number: int = 0
|
|
145
230
|
|
|
146
231
|
|
|
147
232
|
# -----------------------------------------------------------------------------
|
|
148
233
|
class Host(AbortableEventEmitter):
|
|
149
234
|
connections: Dict[int, Connection]
|
|
150
|
-
cis_links: Dict[int,
|
|
235
|
+
cis_links: Dict[int, IsoLink]
|
|
236
|
+
bis_links: Dict[int, IsoLink]
|
|
151
237
|
sco_links: Dict[int, ScoLink]
|
|
152
|
-
|
|
153
|
-
|
|
238
|
+
bigs: dict[int, set[int]]
|
|
239
|
+
acl_packet_queue: Optional[DataPacketQueue] = None
|
|
240
|
+
le_acl_packet_queue: Optional[DataPacketQueue] = None
|
|
241
|
+
iso_packet_queue: Optional[DataPacketQueue] = None
|
|
154
242
|
hci_sink: Optional[TransportSink] = None
|
|
155
243
|
hci_metadata: Dict[str, Any]
|
|
156
244
|
long_term_key_provider: Optional[
|
|
@@ -169,7 +257,9 @@ class Host(AbortableEventEmitter):
|
|
|
169
257
|
self.ready = False # True when we can accept incoming packets
|
|
170
258
|
self.connections = {} # Connections, by connection handle
|
|
171
259
|
self.cis_links = {} # CIS links, by connection handle
|
|
260
|
+
self.bis_links = {} # BIS links, by connection handle
|
|
172
261
|
self.sco_links = {} # SCO links, by connection handle
|
|
262
|
+
self.bigs = {} # BIG Handle to BIS Handles
|
|
173
263
|
self.pending_command = None
|
|
174
264
|
self.pending_response: Optional[asyncio.Future[Any]] = None
|
|
175
265
|
self.number_of_supported_advertising_sets = 0
|
|
@@ -387,6 +477,12 @@ class Host(AbortableEventEmitter):
|
|
|
387
477
|
hci.HCI_LE_TRANSMIT_POWER_REPORTING_EVENT,
|
|
388
478
|
hci.HCI_LE_BIGINFO_ADVERTISING_REPORT_EVENT,
|
|
389
479
|
hci.HCI_LE_SUBRATE_CHANGE_EVENT,
|
|
480
|
+
hci.HCI_LE_CS_READ_REMOTE_SUPPORTED_CAPABILITIES_COMPLETE_EVENT,
|
|
481
|
+
hci.HCI_LE_CS_PROCEDURE_ENABLE_COMPLETE_EVENT,
|
|
482
|
+
hci.HCI_LE_CS_SECURITY_ENABLE_COMPLETE_EVENT,
|
|
483
|
+
hci.HCI_LE_CS_CONFIG_COMPLETE_EVENT,
|
|
484
|
+
hci.HCI_LE_CS_SUBEVENT_RESULT_EVENT,
|
|
485
|
+
hci.HCI_LE_CS_SUBEVENT_RESULT_CONTINUE_EVENT,
|
|
390
486
|
]
|
|
391
487
|
)
|
|
392
488
|
|
|
@@ -411,39 +507,70 @@ class Host(AbortableEventEmitter):
|
|
|
411
507
|
f'hc_total_num_acl_data_packets={hc_total_num_acl_data_packets}'
|
|
412
508
|
)
|
|
413
509
|
|
|
414
|
-
self.acl_packet_queue =
|
|
510
|
+
self.acl_packet_queue = DataPacketQueue(
|
|
415
511
|
max_packet_size=hc_acl_data_packet_length,
|
|
416
512
|
max_in_flight=hc_total_num_acl_data_packets,
|
|
417
513
|
send=self.send_hci_packet,
|
|
418
514
|
)
|
|
419
515
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
516
|
+
le_acl_data_packet_length = 0
|
|
517
|
+
total_num_le_acl_data_packets = 0
|
|
518
|
+
iso_data_packet_length = 0
|
|
519
|
+
total_num_iso_data_packets = 0
|
|
520
|
+
if self.supports_command(hci.HCI_LE_READ_BUFFER_SIZE_V2_COMMAND):
|
|
521
|
+
response = await self.send_command(
|
|
522
|
+
hci.HCI_LE_Read_Buffer_Size_V2_Command(), check_result=True
|
|
523
|
+
)
|
|
524
|
+
le_acl_data_packet_length = (
|
|
525
|
+
response.return_parameters.le_acl_data_packet_length
|
|
526
|
+
)
|
|
527
|
+
total_num_le_acl_data_packets = (
|
|
528
|
+
response.return_parameters.total_num_le_acl_data_packets
|
|
529
|
+
)
|
|
530
|
+
iso_data_packet_length = response.return_parameters.iso_data_packet_length
|
|
531
|
+
total_num_iso_data_packets = (
|
|
532
|
+
response.return_parameters.total_num_iso_data_packets
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
logger.debug(
|
|
536
|
+
'HCI LE flow control: '
|
|
537
|
+
f'le_acl_data_packet_length={le_acl_data_packet_length},'
|
|
538
|
+
f'total_num_le_acl_data_packets={total_num_le_acl_data_packets}'
|
|
539
|
+
f'iso_data_packet_length={iso_data_packet_length},'
|
|
540
|
+
f'total_num_iso_data_packets={total_num_iso_data_packets}'
|
|
541
|
+
)
|
|
542
|
+
elif self.supports_command(hci.HCI_LE_READ_BUFFER_SIZE_COMMAND):
|
|
423
543
|
response = await self.send_command(
|
|
424
544
|
hci.HCI_LE_Read_Buffer_Size_Command(), check_result=True
|
|
425
545
|
)
|
|
426
|
-
|
|
427
|
-
response.return_parameters.
|
|
546
|
+
le_acl_data_packet_length = (
|
|
547
|
+
response.return_parameters.le_acl_data_packet_length
|
|
428
548
|
)
|
|
429
|
-
|
|
430
|
-
response.return_parameters.
|
|
549
|
+
total_num_le_acl_data_packets = (
|
|
550
|
+
response.return_parameters.total_num_le_acl_data_packets
|
|
431
551
|
)
|
|
432
552
|
|
|
433
553
|
logger.debug(
|
|
434
554
|
'HCI LE ACL flow control: '
|
|
435
|
-
f'
|
|
436
|
-
f'
|
|
555
|
+
f'le_acl_data_packet_length={le_acl_data_packet_length},'
|
|
556
|
+
f'total_num_le_acl_data_packets={total_num_le_acl_data_packets}'
|
|
437
557
|
)
|
|
438
558
|
|
|
439
|
-
if
|
|
559
|
+
if le_acl_data_packet_length == 0 or total_num_le_acl_data_packets == 0:
|
|
440
560
|
# LE and Classic share the same queue
|
|
441
561
|
self.le_acl_packet_queue = self.acl_packet_queue
|
|
442
562
|
else:
|
|
443
563
|
# Create a separate queue for LE
|
|
444
|
-
self.le_acl_packet_queue =
|
|
445
|
-
max_packet_size=
|
|
446
|
-
max_in_flight=
|
|
564
|
+
self.le_acl_packet_queue = DataPacketQueue(
|
|
565
|
+
max_packet_size=le_acl_data_packet_length,
|
|
566
|
+
max_in_flight=total_num_le_acl_data_packets,
|
|
567
|
+
send=self.send_hci_packet,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
if iso_data_packet_length and total_num_iso_data_packets:
|
|
571
|
+
self.iso_packet_queue = DataPacketQueue(
|
|
572
|
+
max_packet_size=iso_data_packet_length,
|
|
573
|
+
max_in_flight=total_num_iso_data_packets,
|
|
447
574
|
send=self.send_hci_packet,
|
|
448
575
|
)
|
|
449
576
|
|
|
@@ -595,11 +722,78 @@ class Host(AbortableEventEmitter):
|
|
|
595
722
|
data=l2cap_pdu[offset : offset + data_total_length],
|
|
596
723
|
)
|
|
597
724
|
logger.debug(f'>>> ACL packet enqueue: (CID={cid}) {acl_packet}')
|
|
598
|
-
packet_queue.enqueue(acl_packet)
|
|
725
|
+
packet_queue.enqueue(acl_packet, connection_handle)
|
|
599
726
|
pb_flag = 1
|
|
600
727
|
offset += data_total_length
|
|
601
728
|
bytes_remaining -= data_total_length
|
|
602
729
|
|
|
730
|
+
def get_data_packet_queue(self, connection_handle: int) -> DataPacketQueue | None:
|
|
731
|
+
if connection := self.connections.get(connection_handle):
|
|
732
|
+
return connection.acl_packet_queue
|
|
733
|
+
|
|
734
|
+
if iso_link := self.cis_links.get(connection_handle) or self.bis_links.get(
|
|
735
|
+
connection_handle
|
|
736
|
+
):
|
|
737
|
+
return iso_link.packet_queue
|
|
738
|
+
|
|
739
|
+
return None
|
|
740
|
+
|
|
741
|
+
def send_iso_sdu(self, connection_handle: int, sdu: bytes) -> None:
|
|
742
|
+
if not (
|
|
743
|
+
iso_link := self.cis_links.get(connection_handle)
|
|
744
|
+
or self.bis_links.get(connection_handle)
|
|
745
|
+
):
|
|
746
|
+
logger.warning(f"no ISO link for connection handle {connection_handle}")
|
|
747
|
+
return
|
|
748
|
+
|
|
749
|
+
if iso_link.packet_queue is None:
|
|
750
|
+
logger.warning("ISO link has no data packet queue")
|
|
751
|
+
return
|
|
752
|
+
|
|
753
|
+
bytes_remaining = len(sdu)
|
|
754
|
+
offset = 0
|
|
755
|
+
while bytes_remaining:
|
|
756
|
+
is_first_fragment = offset == 0
|
|
757
|
+
header_length = 4 if is_first_fragment else 0
|
|
758
|
+
assert iso_link.packet_queue.max_packet_size > header_length
|
|
759
|
+
fragment_length = min(
|
|
760
|
+
bytes_remaining, iso_link.packet_queue.max_packet_size - header_length
|
|
761
|
+
)
|
|
762
|
+
is_last_fragment = bytes_remaining == fragment_length
|
|
763
|
+
iso_sdu_fragment = sdu[offset : offset + fragment_length]
|
|
764
|
+
iso_link.packet_queue.enqueue(
|
|
765
|
+
(
|
|
766
|
+
hci.HCI_IsoDataPacket(
|
|
767
|
+
connection_handle=connection_handle,
|
|
768
|
+
data_total_length=header_length + fragment_length,
|
|
769
|
+
packet_sequence_number=iso_link.packet_sequence_number,
|
|
770
|
+
pb_flag=0b10 if is_last_fragment else 0b00,
|
|
771
|
+
packet_status_flag=0,
|
|
772
|
+
iso_sdu_length=len(sdu),
|
|
773
|
+
iso_sdu_fragment=iso_sdu_fragment,
|
|
774
|
+
)
|
|
775
|
+
if is_first_fragment
|
|
776
|
+
else hci.HCI_IsoDataPacket(
|
|
777
|
+
connection_handle=connection_handle,
|
|
778
|
+
data_total_length=fragment_length,
|
|
779
|
+
pb_flag=0b11 if is_last_fragment else 0b01,
|
|
780
|
+
iso_sdu_fragment=iso_sdu_fragment,
|
|
781
|
+
)
|
|
782
|
+
),
|
|
783
|
+
connection_handle,
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
offset += fragment_length
|
|
787
|
+
bytes_remaining -= fragment_length
|
|
788
|
+
|
|
789
|
+
iso_link.packet_sequence_number = (iso_link.packet_sequence_number + 1) & 0xFFFF
|
|
790
|
+
|
|
791
|
+
def remove_big(self, big_handle: int) -> None:
|
|
792
|
+
if big := self.bigs.pop(big_handle, None):
|
|
793
|
+
for connection_handle in big:
|
|
794
|
+
if bis_link := self.bis_links.pop(connection_handle, None):
|
|
795
|
+
bis_link.packet_queue.flush(bis_link.handle)
|
|
796
|
+
|
|
603
797
|
def supports_command(self, op_code: int) -> bool:
|
|
604
798
|
return (
|
|
605
799
|
self.local_supported_commands
|
|
@@ -727,16 +921,17 @@ class Host(AbortableEventEmitter):
|
|
|
727
921
|
def on_hci_command_status_event(self, event):
|
|
728
922
|
return self.on_command_processed(event)
|
|
729
923
|
|
|
730
|
-
def on_hci_number_of_completed_packets_event(
|
|
924
|
+
def on_hci_number_of_completed_packets_event(
|
|
925
|
+
self, event: hci.HCI_Number_Of_Completed_Packets_Event
|
|
926
|
+
) -> None:
|
|
731
927
|
for connection_handle, num_completed_packets in zip(
|
|
732
928
|
event.connection_handles, event.num_completed_packets
|
|
733
929
|
):
|
|
734
|
-
if
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
):
|
|
930
|
+
if queue := self.get_data_packet_queue(connection_handle):
|
|
931
|
+
queue.on_packets_completed(num_completed_packets, connection_handle)
|
|
932
|
+
continue
|
|
933
|
+
|
|
934
|
+
if connection_handle not in self.sco_links:
|
|
740
935
|
logger.warning(
|
|
741
936
|
'received packet completion event for unknown handle '
|
|
742
937
|
f'0x{connection_handle:04X}'
|
|
@@ -854,11 +1049,7 @@ class Host(AbortableEventEmitter):
|
|
|
854
1049
|
return
|
|
855
1050
|
|
|
856
1051
|
if event.status == hci.HCI_SUCCESS:
|
|
857
|
-
logger.debug(
|
|
858
|
-
f'### DISCONNECTION: [0x{handle:04X}] '
|
|
859
|
-
f'{connection.peer_address} '
|
|
860
|
-
f'reason={event.reason}'
|
|
861
|
-
)
|
|
1052
|
+
logger.debug(f'### DISCONNECTION: {connection}, reason={event.reason}')
|
|
862
1053
|
|
|
863
1054
|
# Notify the listeners
|
|
864
1055
|
self.emit('disconnection', handle, event.reason)
|
|
@@ -869,6 +1060,14 @@ class Host(AbortableEventEmitter):
|
|
|
869
1060
|
or self.cis_links.pop(handle, 0)
|
|
870
1061
|
or self.sco_links.pop(handle, 0)
|
|
871
1062
|
)
|
|
1063
|
+
|
|
1064
|
+
# Flush the data queues
|
|
1065
|
+
if self.acl_packet_queue:
|
|
1066
|
+
self.acl_packet_queue.flush(handle)
|
|
1067
|
+
if self.le_acl_packet_queue:
|
|
1068
|
+
self.le_acl_packet_queue.flush(handle)
|
|
1069
|
+
if self.iso_packet_queue:
|
|
1070
|
+
self.iso_packet_queue.flush(handle)
|
|
872
1071
|
else:
|
|
873
1072
|
logger.debug(f'### DISCONNECTION FAILED: {event.status}')
|
|
874
1073
|
|
|
@@ -902,8 +1101,11 @@ class Host(AbortableEventEmitter):
|
|
|
902
1101
|
|
|
903
1102
|
# Notify the client
|
|
904
1103
|
if event.status == hci.HCI_SUCCESS:
|
|
905
|
-
|
|
906
|
-
|
|
1104
|
+
self.emit(
|
|
1105
|
+
'connection_phy_update',
|
|
1106
|
+
connection.handle,
|
|
1107
|
+
ConnectionPHY(event.tx_phy, event.rx_phy),
|
|
1108
|
+
)
|
|
907
1109
|
else:
|
|
908
1110
|
self.emit('connection_phy_update_failure', connection.handle, event.status)
|
|
909
1111
|
|
|
@@ -953,12 +1155,94 @@ class Host(AbortableEventEmitter):
|
|
|
953
1155
|
event.cis_id,
|
|
954
1156
|
)
|
|
955
1157
|
|
|
1158
|
+
def on_hci_le_create_big_complete_event(self, event):
|
|
1159
|
+
self.bigs[event.big_handle] = set(event.connection_handle)
|
|
1160
|
+
if self.iso_packet_queue is None:
|
|
1161
|
+
logger.warning("BIS established but ISO packets not supported")
|
|
1162
|
+
|
|
1163
|
+
for connection_handle in event.connection_handle:
|
|
1164
|
+
self.bis_links[connection_handle] = IsoLink(
|
|
1165
|
+
connection_handle, self.iso_packet_queue
|
|
1166
|
+
)
|
|
1167
|
+
|
|
1168
|
+
self.emit(
|
|
1169
|
+
'big_establishment',
|
|
1170
|
+
event.status,
|
|
1171
|
+
event.big_handle,
|
|
1172
|
+
event.connection_handle,
|
|
1173
|
+
event.big_sync_delay,
|
|
1174
|
+
event.transport_latency_big,
|
|
1175
|
+
event.phy,
|
|
1176
|
+
event.nse,
|
|
1177
|
+
event.bn,
|
|
1178
|
+
event.pto,
|
|
1179
|
+
event.irc,
|
|
1180
|
+
event.max_pdu,
|
|
1181
|
+
event.iso_interval,
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1184
|
+
def on_hci_le_big_sync_established_event(self, event):
|
|
1185
|
+
self.bigs[event.big_handle] = set(event.connection_handle)
|
|
1186
|
+
for connection_handle in event.connection_handle:
|
|
1187
|
+
self.bis_links[connection_handle] = IsoLink(
|
|
1188
|
+
connection_handle, self.iso_packet_queue
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1191
|
+
self.emit(
|
|
1192
|
+
'big_sync_establishment',
|
|
1193
|
+
event.status,
|
|
1194
|
+
event.big_handle,
|
|
1195
|
+
event.transport_latency_big,
|
|
1196
|
+
event.nse,
|
|
1197
|
+
event.bn,
|
|
1198
|
+
event.pto,
|
|
1199
|
+
event.irc,
|
|
1200
|
+
event.max_pdu,
|
|
1201
|
+
event.iso_interval,
|
|
1202
|
+
event.connection_handle,
|
|
1203
|
+
)
|
|
1204
|
+
|
|
1205
|
+
def on_hci_le_big_sync_lost_event(self, event):
|
|
1206
|
+
self.remove_big(event.big_handle)
|
|
1207
|
+
self.emit('big_sync_lost', event.big_handle, event.reason)
|
|
1208
|
+
|
|
1209
|
+
def on_hci_le_terminate_big_complete_event(self, event):
|
|
1210
|
+
self.remove_big(event.big_handle)
|
|
1211
|
+
self.emit('big_termination', event.reason, event.big_handle)
|
|
1212
|
+
|
|
1213
|
+
def on_hci_le_periodic_advertising_sync_transfer_received_event(self, event):
|
|
1214
|
+
self.emit(
|
|
1215
|
+
'periodic_advertising_sync_transfer',
|
|
1216
|
+
event.status,
|
|
1217
|
+
event.connection_handle,
|
|
1218
|
+
event.sync_handle,
|
|
1219
|
+
event.advertising_sid,
|
|
1220
|
+
event.advertiser_address,
|
|
1221
|
+
event.advertiser_phy,
|
|
1222
|
+
event.periodic_advertising_interval,
|
|
1223
|
+
event.advertiser_clock_accuracy,
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
def on_hci_le_periodic_advertising_sync_transfer_received_v2_event(self, event):
|
|
1227
|
+
self.emit(
|
|
1228
|
+
'periodic_advertising_sync_transfer',
|
|
1229
|
+
event.status,
|
|
1230
|
+
event.connection_handle,
|
|
1231
|
+
event.sync_handle,
|
|
1232
|
+
event.advertising_sid,
|
|
1233
|
+
event.advertiser_address,
|
|
1234
|
+
event.advertiser_phy,
|
|
1235
|
+
event.periodic_advertising_interval,
|
|
1236
|
+
event.advertiser_clock_accuracy,
|
|
1237
|
+
)
|
|
1238
|
+
|
|
956
1239
|
def on_hci_le_cis_established_event(self, event):
|
|
957
1240
|
# The remaining parameters are unused for now.
|
|
958
1241
|
if event.status == hci.HCI_SUCCESS:
|
|
959
|
-
self.
|
|
960
|
-
|
|
961
|
-
|
|
1242
|
+
if self.iso_packet_queue is None:
|
|
1243
|
+
logger.warning("CIS established but ISO packets not supported")
|
|
1244
|
+
self.cis_links[event.connection_handle] = IsoLink(
|
|
1245
|
+
handle=event.connection_handle, packet_queue=self.iso_packet_queue
|
|
962
1246
|
)
|
|
963
1247
|
self.emit('cis_establishment', event.connection_handle)
|
|
964
1248
|
else:
|
|
@@ -1028,7 +1312,7 @@ class Host(AbortableEventEmitter):
|
|
|
1028
1312
|
|
|
1029
1313
|
self.sco_links[event.connection_handle] = ScoLink(
|
|
1030
1314
|
peer_address=event.bd_addr,
|
|
1031
|
-
|
|
1315
|
+
connection_handle=event.connection_handle,
|
|
1032
1316
|
)
|
|
1033
1317
|
|
|
1034
1318
|
# Notify the client
|
|
@@ -1249,5 +1533,23 @@ class Host(AbortableEventEmitter):
|
|
|
1249
1533
|
int.from_bytes(event.le_features, 'little'),
|
|
1250
1534
|
)
|
|
1251
1535
|
|
|
1536
|
+
def on_hci_le_cs_read_remote_supported_capabilities_complete_event(self, event):
|
|
1537
|
+
self.emit('cs_remote_supported_capabilities', event)
|
|
1538
|
+
|
|
1539
|
+
def on_hci_le_cs_security_enable_complete_event(self, event):
|
|
1540
|
+
self.emit('cs_security', event)
|
|
1541
|
+
|
|
1542
|
+
def on_hci_le_cs_config_complete_event(self, event):
|
|
1543
|
+
self.emit('cs_config', event)
|
|
1544
|
+
|
|
1545
|
+
def on_hci_le_cs_procedure_enable_complete_event(self, event):
|
|
1546
|
+
self.emit('cs_procedure', event)
|
|
1547
|
+
|
|
1548
|
+
def on_hci_le_cs_subevent_result_event(self, event):
|
|
1549
|
+
self.emit('cs_subevent_result', event)
|
|
1550
|
+
|
|
1551
|
+
def on_hci_le_cs_subevent_result_continue_event(self, event):
|
|
1552
|
+
self.emit('cs_subevent_result_continue', event)
|
|
1553
|
+
|
|
1252
1554
|
def on_hci_vendor_event(self, event):
|
|
1253
1555
|
self.emit('vendor_event', event)
|
bumble/l2cap.py
CHANGED
|
@@ -773,7 +773,6 @@ class ClassicChannel(EventEmitter):
|
|
|
773
773
|
self.psm = psm
|
|
774
774
|
self.source_cid = source_cid
|
|
775
775
|
self.destination_cid = 0
|
|
776
|
-
self.response = None
|
|
777
776
|
self.connection_result = None
|
|
778
777
|
self.disconnection_result = None
|
|
779
778
|
self.sink = None
|
|
@@ -783,27 +782,15 @@ class ClassicChannel(EventEmitter):
|
|
|
783
782
|
self.state = new_state
|
|
784
783
|
|
|
785
784
|
def send_pdu(self, pdu: Union[SupportsBytes, bytes]) -> None:
|
|
785
|
+
if self.state != self.State.OPEN:
|
|
786
|
+
raise InvalidStateError('channel not open')
|
|
786
787
|
self.manager.send_pdu(self.connection, self.destination_cid, pdu)
|
|
787
788
|
|
|
788
789
|
def send_control_frame(self, frame: L2CAP_Control_Frame) -> None:
|
|
789
790
|
self.manager.send_control_frame(self.connection, self.signaling_cid, frame)
|
|
790
791
|
|
|
791
|
-
async def send_request(self, request: SupportsBytes) -> bytes:
|
|
792
|
-
# Check that there isn't already a request pending
|
|
793
|
-
if self.response:
|
|
794
|
-
raise InvalidStateError('request already pending')
|
|
795
|
-
if self.state != self.State.OPEN:
|
|
796
|
-
raise InvalidStateError('channel not open')
|
|
797
|
-
|
|
798
|
-
self.response = asyncio.get_running_loop().create_future()
|
|
799
|
-
self.send_pdu(request)
|
|
800
|
-
return await self.response
|
|
801
|
-
|
|
802
792
|
def on_pdu(self, pdu: bytes) -> None:
|
|
803
|
-
if self.
|
|
804
|
-
self.response.set_result(pdu)
|
|
805
|
-
self.response = None
|
|
806
|
-
elif self.sink:
|
|
793
|
+
if self.sink:
|
|
807
794
|
# pylint: disable=not-callable
|
|
808
795
|
self.sink(pdu)
|
|
809
796
|
else:
|
bumble/pairing.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021-
|
|
1
|
+
# Copyright 2021-2025 Google LLC
|
|
2
2
|
#
|
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
# you may not use this file except in compliance with the License.
|
|
@@ -76,18 +76,18 @@ class OobData:
|
|
|
76
76
|
return instance
|
|
77
77
|
|
|
78
78
|
def to_ad(self) -> AdvertisingData:
|
|
79
|
-
ad_structures = []
|
|
79
|
+
ad_structures: list[tuple[int, bytes]] = []
|
|
80
80
|
if self.address is not None:
|
|
81
81
|
ad_structures.append(
|
|
82
|
-
(AdvertisingData.LE_BLUETOOTH_DEVICE_ADDRESS, bytes(self.address))
|
|
82
|
+
(AdvertisingData.Type.LE_BLUETOOTH_DEVICE_ADDRESS, bytes(self.address))
|
|
83
83
|
)
|
|
84
84
|
if self.role is not None:
|
|
85
|
-
ad_structures.append((AdvertisingData.LE_ROLE, bytes([self.role])))
|
|
85
|
+
ad_structures.append((AdvertisingData.Type.LE_ROLE, bytes([self.role])))
|
|
86
86
|
if self.shared_data is not None:
|
|
87
87
|
ad_structures.extend(self.shared_data.to_ad().ad_structures)
|
|
88
88
|
if self.legacy_context is not None:
|
|
89
89
|
ad_structures.append(
|
|
90
|
-
(AdvertisingData.SECURITY_MANAGER_TK_VALUE, self.legacy_context.tk)
|
|
90
|
+
(AdvertisingData.Type.SECURITY_MANAGER_TK_VALUE, self.legacy_context.tk)
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
return AdvertisingData(ad_structures)
|