bumble 0.0.180__py3-none-any.whl → 0.0.181__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/host.py CHANGED
@@ -21,7 +21,7 @@ import collections
21
21
  import logging
22
22
  import struct
23
23
 
24
- from typing import Optional, TYPE_CHECKING, Dict, Callable, Awaitable, cast
24
+ from typing import Any, Awaitable, Callable, Deque, Dict, Optional, cast, TYPE_CHECKING
25
25
 
26
26
  from bumble.colors import color
27
27
  from bumble.l2cap import L2CAP_PDU
@@ -32,8 +32,8 @@ from .hci import (
32
32
  Address,
33
33
  HCI_ACL_DATA_PACKET,
34
34
  HCI_COMMAND_PACKET,
35
- HCI_COMMAND_COMPLETE_EVENT,
36
35
  HCI_EVENT_PACKET,
36
+ HCI_ISO_DATA_PACKET,
37
37
  HCI_LE_READ_BUFFER_SIZE_COMMAND,
38
38
  HCI_LE_READ_LOCAL_SUPPORTED_FEATURES_COMMAND,
39
39
  HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND,
@@ -52,6 +52,7 @@ from .hci import (
52
52
  HCI_Constant,
53
53
  HCI_Error,
54
54
  HCI_Event,
55
+ HCI_IsoDataPacket,
55
56
  HCI_LE_Long_Term_Key_Request_Negative_Reply_Command,
56
57
  HCI_LE_Long_Term_Key_Request_Reply_Command,
57
58
  HCI_LE_Read_Buffer_Size_Command,
@@ -75,7 +76,6 @@ from .core import (
75
76
  BT_LE_TRANSPORT,
76
77
  ConnectionPHY,
77
78
  ConnectionParameters,
78
- InvalidStateError,
79
79
  )
80
80
  from .utils import AbortableEventEmitter
81
81
  from .transport.common import TransportLostError
@@ -91,16 +91,49 @@ logger = logging.getLogger(__name__)
91
91
 
92
92
 
93
93
  # -----------------------------------------------------------------------------
94
- # Constants
95
- # -----------------------------------------------------------------------------
96
- # fmt: off
94
+ class AclPacketQueue:
95
+ max_packet_size: int
96
+
97
+ def __init__(
98
+ self,
99
+ max_packet_size: int,
100
+ max_in_flight: int,
101
+ send: Callable[[HCI_Packet], None],
102
+ ) -> None:
103
+ self.max_packet_size = max_packet_size
104
+ self.max_in_flight = max_in_flight
105
+ self.in_flight = 0
106
+ self.send = send
107
+ self.packets: Deque[HCI_AclDataPacket] = collections.deque()
108
+
109
+ def enqueue(self, packet: HCI_AclDataPacket) -> None:
110
+ self.packets.appendleft(packet)
111
+ self.check_queue()
112
+
113
+ if self.packets:
114
+ logger.debug(
115
+ f'{self.in_flight} ACL packets in flight, '
116
+ f'{len(self.packets)} in queue'
117
+ )
118
+
119
+ def check_queue(self) -> None:
120
+ while self.packets and self.in_flight < self.max_in_flight:
121
+ packet = self.packets.pop()
122
+ self.send(packet)
123
+ self.in_flight += 1
97
124
 
98
- HOST_DEFAULT_HC_LE_ACL_DATA_PACKET_LENGTH = 27
99
- HOST_HC_TOTAL_NUM_LE_ACL_DATA_PACKETS = 1
100
- HOST_DEFAULT_HC_ACL_DATA_PACKET_LENGTH = 27
101
- HOST_HC_TOTAL_NUM_ACL_DATA_PACKETS = 1
125
+ def on_packets_completed(self, packet_count: int) -> None:
126
+ if packet_count > self.in_flight:
127
+ logger.warning(
128
+ color(
129
+ '!!! {packet_count} completed but only '
130
+ f'{self.in_flight} in flight'
131
+ )
132
+ )
133
+ packet_count = self.in_flight
102
134
 
103
- # fmt: on
135
+ self.in_flight -= packet_count
136
+ self.check_queue()
104
137
 
105
138
 
106
139
  # -----------------------------------------------------------------------------
@@ -111,6 +144,13 @@ class Connection:
111
144
  self.peer_address = peer_address
112
145
  self.assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu)
113
146
  self.transport = transport
147
+ acl_packet_queue: Optional[AclPacketQueue] = (
148
+ host.le_acl_packet_queue
149
+ if transport == BT_LE_TRANSPORT
150
+ else host.acl_packet_queue
151
+ )
152
+ assert acl_packet_queue
153
+ self.acl_packet_queue = acl_packet_queue
114
154
 
115
155
  def on_hci_acl_data_packet(self, packet: HCI_AclDataPacket) -> None:
116
156
  self.assembler.feed_packet(packet)
@@ -123,8 +163,10 @@ class Connection:
123
163
  # -----------------------------------------------------------------------------
124
164
  class Host(AbortableEventEmitter):
125
165
  connections: Dict[int, Connection]
126
- acl_packet_queue: collections.deque[HCI_AclDataPacket]
127
- hci_sink: TransportSink
166
+ acl_packet_queue: Optional[AclPacketQueue] = None
167
+ le_acl_packet_queue: Optional[AclPacketQueue] = None
168
+ hci_sink: Optional[TransportSink] = None
169
+ hci_metadata: Dict[str, Any]
128
170
  long_term_key_provider: Optional[
129
171
  Callable[[int, bytes, int], Awaitable[Optional[bytes]]]
130
172
  ]
@@ -137,18 +179,11 @@ class Host(AbortableEventEmitter):
137
179
  ) -> None:
138
180
  super().__init__()
139
181
 
140
- self.hci_metadata = None
182
+ self.hci_metadata = {}
141
183
  self.ready = False # True when we can accept incoming packets
142
- self.reset_done = False
143
184
  self.connections = {} # Connections, by connection handle
144
185
  self.pending_command = None
145
186
  self.pending_response = None
146
- self.hc_le_acl_data_packet_length = HOST_DEFAULT_HC_LE_ACL_DATA_PACKET_LENGTH
147
- self.hc_total_num_le_acl_data_packets = HOST_HC_TOTAL_NUM_LE_ACL_DATA_PACKETS
148
- self.hc_acl_data_packet_length = HOST_DEFAULT_HC_ACL_DATA_PACKET_LENGTH
149
- self.hc_total_num_acl_data_packets = HOST_HC_TOTAL_NUM_ACL_DATA_PACKETS
150
- self.acl_packet_queue = collections.deque()
151
- self.acl_packets_in_flight = 0
152
187
  self.local_version = None
153
188
  self.local_supported_commands = bytes(64)
154
189
  self.local_le_features = 0
@@ -162,10 +197,7 @@ class Host(AbortableEventEmitter):
162
197
 
163
198
  # Connect to the source and sink if specified
164
199
  if controller_source:
165
- controller_source.set_packet_sink(self)
166
- self.hci_metadata = getattr(
167
- controller_source, 'metadata', self.hci_metadata
168
- )
200
+ self.set_packet_source(controller_source)
169
201
  if controller_sink:
170
202
  self.set_packet_sink(controller_sink)
171
203
 
@@ -200,17 +232,21 @@ class Host(AbortableEventEmitter):
200
232
  self.ready = False
201
233
  await self.flush()
202
234
 
203
- await self.send_command(HCI_Reset_Command(), check_result=True)
204
- self.ready = True
205
-
206
235
  # Instantiate and init a driver for the host if needed.
207
236
  # NOTE: we don't keep a reference to the driver here, because we don't
208
237
  # currently have a need for the driver later on. But if the driver interface
209
238
  # evolves, it may be required, then, to store a reference to the driver in
210
239
  # an object property.
240
+ reset_needed = True
211
241
  if driver_factory is not None:
212
242
  if driver := await driver_factory(self):
213
243
  await driver.init_controller()
244
+ reset_needed = False
245
+
246
+ # Send a reset command unless a driver has already done so.
247
+ if reset_needed:
248
+ await self.send_command(HCI_Reset_Command(), check_result=True)
249
+ self.ready = True
214
250
 
215
251
  response = await self.send_command(
216
252
  HCI_Read_Local_Supported_Commands_Command(), check_result=True
@@ -243,7 +279,7 @@ class Host(AbortableEventEmitter):
243
279
  # understand
244
280
  le_event_mask = bytes.fromhex('1F00000000000000')
245
281
  else:
246
- le_event_mask = bytes.fromhex('FFFFF00000000000')
282
+ le_event_mask = bytes.fromhex('FFFFFFFF00000000')
247
283
 
248
284
  await self.send_command(
249
285
  HCI_LE_Set_Event_Mask_Command(le_event_mask=le_event_mask)
@@ -253,46 +289,54 @@ class Host(AbortableEventEmitter):
253
289
  response = await self.send_command(
254
290
  HCI_Read_Buffer_Size_Command(), check_result=True
255
291
  )
256
- self.hc_acl_data_packet_length = (
292
+ hc_acl_data_packet_length = (
257
293
  response.return_parameters.hc_acl_data_packet_length
258
294
  )
259
- self.hc_total_num_acl_data_packets = (
295
+ hc_total_num_acl_data_packets = (
260
296
  response.return_parameters.hc_total_num_acl_data_packets
261
297
  )
262
298
 
263
299
  logger.debug(
264
300
  'HCI ACL flow control: '
265
- f'hc_acl_data_packet_length={self.hc_acl_data_packet_length},'
266
- f'hc_total_num_acl_data_packets={self.hc_total_num_acl_data_packets}'
301
+ f'hc_acl_data_packet_length={hc_acl_data_packet_length},'
302
+ f'hc_total_num_acl_data_packets={hc_total_num_acl_data_packets}'
303
+ )
304
+
305
+ self.acl_packet_queue = AclPacketQueue(
306
+ max_packet_size=hc_acl_data_packet_length,
307
+ max_in_flight=hc_total_num_acl_data_packets,
308
+ send=self.send_hci_packet,
267
309
  )
268
310
 
311
+ hc_le_acl_data_packet_length = 0
312
+ hc_total_num_le_acl_data_packets = 0
269
313
  if self.supports_command(HCI_LE_READ_BUFFER_SIZE_COMMAND):
270
314
  response = await self.send_command(
271
315
  HCI_LE_Read_Buffer_Size_Command(), check_result=True
272
316
  )
273
- self.hc_le_acl_data_packet_length = (
317
+ hc_le_acl_data_packet_length = (
274
318
  response.return_parameters.hc_le_acl_data_packet_length
275
319
  )
276
- self.hc_total_num_le_acl_data_packets = (
320
+ hc_total_num_le_acl_data_packets = (
277
321
  response.return_parameters.hc_total_num_le_acl_data_packets
278
322
  )
279
323
 
280
324
  logger.debug(
281
325
  'HCI LE ACL flow control: '
282
- f'hc_le_acl_data_packet_length={self.hc_le_acl_data_packet_length},'
283
- 'hc_total_num_le_acl_data_packets='
284
- f'{self.hc_total_num_le_acl_data_packets}'
326
+ f'hc_le_acl_data_packet_length={hc_le_acl_data_packet_length},'
327
+ f'hc_total_num_le_acl_data_packets={hc_total_num_le_acl_data_packets}'
285
328
  )
286
329
 
287
- if (
288
- response.return_parameters.hc_le_acl_data_packet_length == 0
289
- or response.return_parameters.hc_total_num_le_acl_data_packets == 0
290
- ):
291
- # LE and Classic share the same values
292
- self.hc_le_acl_data_packet_length = self.hc_acl_data_packet_length
293
- self.hc_total_num_le_acl_data_packets = (
294
- self.hc_total_num_acl_data_packets
295
- )
330
+ if hc_le_acl_data_packet_length == 0 or hc_total_num_le_acl_data_packets == 0:
331
+ # LE and Classic share the same queue
332
+ self.le_acl_packet_queue = self.acl_packet_queue
333
+ else:
334
+ # Create a separate queue for LE
335
+ self.le_acl_packet_queue = AclPacketQueue(
336
+ max_packet_size=hc_le_acl_data_packet_length,
337
+ max_in_flight=hc_total_num_le_acl_data_packets,
338
+ send=self.send_hci_packet,
339
+ )
296
340
 
297
341
  if self.supports_command(
298
342
  HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND
@@ -313,29 +357,31 @@ class Host(AbortableEventEmitter):
313
357
  )
314
358
  )
315
359
 
316
- self.reset_done = True
317
-
318
360
  @property
319
- def controller(self) -> TransportSink:
361
+ def controller(self) -> Optional[TransportSink]:
320
362
  return self.hci_sink
321
363
 
322
364
  @controller.setter
323
- def controller(self, controller):
365
+ def controller(self, controller) -> None:
324
366
  self.set_packet_sink(controller)
325
367
  if controller:
326
368
  controller.set_packet_sink(self)
327
369
 
328
- def set_packet_sink(self, sink: TransportSink) -> None:
370
+ def set_packet_sink(self, sink: Optional[TransportSink]) -> None:
329
371
  self.hci_sink = sink
330
372
 
373
+ def set_packet_source(self, source: TransportSource) -> None:
374
+ source.set_packet_sink(self)
375
+ self.hci_metadata = getattr(source, 'metadata', self.hci_metadata)
376
+
331
377
  def send_hci_packet(self, packet: HCI_Packet) -> None:
378
+ logger.debug(f'{color("### HOST -> CONTROLLER", "blue")}: {packet}')
332
379
  if self.snooper:
333
380
  self.snooper.snoop(bytes(packet), Snooper.Direction.HOST_TO_CONTROLLER)
334
- self.hci_sink.on_packet(bytes(packet))
381
+ if self.hci_sink:
382
+ self.hci_sink.on_packet(bytes(packet))
335
383
 
336
384
  async def send_command(self, command, check_result=False):
337
- logger.debug(f'{color("### HOST -> CONTROLLER", "blue")}: {command}')
338
-
339
385
  # Wait until we can send (only one pending command at a time)
340
386
  async with self.command_semaphore:
341
387
  assert self.pending_command is None
@@ -383,6 +429,17 @@ class Host(AbortableEventEmitter):
383
429
  asyncio.create_task(send_command(command))
384
430
 
385
431
  def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
432
+ if not (connection := self.connections.get(connection_handle)):
433
+ logger.warning(f'connection 0x{connection_handle:04X} not found')
434
+ return
435
+ packet_queue = connection.acl_packet_queue
436
+ if packet_queue is None:
437
+ logger.warning(
438
+ f'no ACL packet queue for connection 0x{connection_handle:04X}'
439
+ )
440
+ return
441
+
442
+ # Create a PDU
386
443
  l2cap_pdu = bytes(L2CAP_PDU(cid, pdu))
387
444
 
388
445
  # Send the data to the controller via ACL packets
@@ -390,8 +447,7 @@ class Host(AbortableEventEmitter):
390
447
  offset = 0
391
448
  pb_flag = 0
392
449
  while bytes_remaining:
393
- # TODO: support different LE/Classic lengths
394
- data_total_length = min(bytes_remaining, self.hc_le_acl_data_packet_length)
450
+ data_total_length = min(bytes_remaining, packet_queue.max_packet_size)
395
451
  acl_packet = HCI_AclDataPacket(
396
452
  connection_handle=connection_handle,
397
453
  pb_flag=pb_flag,
@@ -399,34 +455,12 @@ class Host(AbortableEventEmitter):
399
455
  data_total_length=data_total_length,
400
456
  data=l2cap_pdu[offset : offset + data_total_length],
401
457
  )
402
- logger.debug(
403
- f'{color("### HOST -> CONTROLLER", "blue")}: (CID={cid}) {acl_packet}'
404
- )
405
- self.queue_acl_packet(acl_packet)
458
+ logger.debug(f'>>> ACL packet enqueue: (CID={cid}) {acl_packet}')
459
+ packet_queue.enqueue(acl_packet)
406
460
  pb_flag = 1
407
461
  offset += data_total_length
408
462
  bytes_remaining -= data_total_length
409
463
 
410
- def queue_acl_packet(self, acl_packet: HCI_AclDataPacket) -> None:
411
- self.acl_packet_queue.appendleft(acl_packet)
412
- self.check_acl_packet_queue()
413
-
414
- if len(self.acl_packet_queue):
415
- logger.debug(
416
- f'{self.acl_packets_in_flight} ACL packets in flight, '
417
- f'{len(self.acl_packet_queue)} in queue'
418
- )
419
-
420
- def check_acl_packet_queue(self) -> None:
421
- # Send all we can (TODO: support different LE/Classic limits)
422
- while (
423
- len(self.acl_packet_queue) > 0
424
- and self.acl_packets_in_flight < self.hc_total_num_le_acl_data_packets
425
- ):
426
- packet = self.acl_packet_queue.pop()
427
- self.send_hci_packet(packet)
428
- self.acl_packets_in_flight += 1
429
-
430
464
  def supports_command(self, command):
431
465
  # Find the support flag position for this command
432
466
  for octet, flags in enumerate(HCI_SUPPORTED_COMMANDS_FLAGS):
@@ -495,6 +529,8 @@ class Host(AbortableEventEmitter):
495
529
  self.on_hci_acl_data_packet(cast(HCI_AclDataPacket, packet))
496
530
  elif packet.hci_packet_type == HCI_SYNCHRONOUS_DATA_PACKET:
497
531
  self.on_hci_sco_data_packet(cast(HCI_SynchronousDataPacket, packet))
532
+ elif packet.hci_packet_type == HCI_ISO_DATA_PACKET:
533
+ self.on_hci_iso_data_packet(cast(HCI_IsoDataPacket, packet))
498
534
  else:
499
535
  logger.warning(f'!!! unknown packet type {packet.hci_packet_type}')
500
536
 
@@ -515,6 +551,10 @@ class Host(AbortableEventEmitter):
515
551
  # Experimental
516
552
  self.emit('sco_packet', packet.connection_handle, packet)
517
553
 
554
+ def on_hci_iso_data_packet(self, packet: HCI_IsoDataPacket) -> None:
555
+ # Experimental
556
+ self.emit('iso_packet', packet.connection_handle, packet)
557
+
518
558
  def on_l2cap_pdu(self, connection: Connection, cid: int, pdu: bytes) -> None:
519
559
  self.emit('l2cap_pdu', connection.handle, cid, pdu)
520
560
 
@@ -543,7 +583,7 @@ class Host(AbortableEventEmitter):
543
583
  # This is used just for the Num_HCI_Command_Packets field, not related to
544
584
  # an actual command
545
585
  logger.debug('no-command event')
546
- return None
586
+ return
547
587
 
548
588
  return self.on_command_processed(event)
549
589
 
@@ -551,18 +591,17 @@ class Host(AbortableEventEmitter):
551
591
  return self.on_command_processed(event)
552
592
 
553
593
  def on_hci_number_of_completed_packets_event(self, event):
554
- total_packets = sum(event.num_completed_packets)
555
- if total_packets <= self.acl_packets_in_flight:
556
- self.acl_packets_in_flight -= total_packets
557
- self.check_acl_packet_queue()
558
- else:
559
- logger.warning(
560
- color(
561
- '!!! {total_packets} completed but only '
562
- f'{self.acl_packets_in_flight} in flight'
594
+ for connection_handle, num_completed_packets in zip(
595
+ event.connection_handles, event.num_completed_packets
596
+ ):
597
+ if not (connection := self.connections.get(connection_handle)):
598
+ logger.warning(
599
+ 'received packet completion event for unknown handle '
600
+ f'0x{connection_handle:04X}'
563
601
  )
564
- )
565
- self.acl_packets_in_flight = 0
602
+ continue
603
+
604
+ connection.acl_packet_queue.on_packets_completed(num_completed_packets)
566
605
 
567
606
  # Classic only
568
607
  def on_hci_connection_request_event(self, event):
@@ -715,6 +754,32 @@ class Host(AbortableEventEmitter):
715
754
  def on_hci_le_extended_advertising_report_event(self, event):
716
755
  self.on_hci_le_advertising_report_event(event)
717
756
 
757
+ def on_hci_le_advertising_set_terminated_event(self, event):
758
+ self.emit(
759
+ 'advertising_set_termination',
760
+ event.status,
761
+ event.advertising_handle,
762
+ event.connection_handle,
763
+ )
764
+
765
+ def on_hci_le_cis_request_event(self, event):
766
+ self.emit(
767
+ 'cis_request',
768
+ event.acl_connection_handle,
769
+ event.cis_connection_handle,
770
+ event.cig_id,
771
+ event.cis_id,
772
+ )
773
+
774
+ def on_hci_le_cis_established_event(self, event):
775
+ # The remaining parameters are unused for now.
776
+ if event.status == HCI_SUCCESS:
777
+ self.emit('cis_establishment', event.connection_handle)
778
+ else:
779
+ self.emit(
780
+ 'cis_establishment_failure', event.connection_handle, event.status
781
+ )
782
+
718
783
  def on_hci_le_remote_connection_parameter_request_event(self, event):
719
784
  if event.connection_handle not in self.connections:
720
785
  logger.warning('!!! REMOTE CONNECTION PARAMETER REQUEST: unknown handle')
bumble/l2cap.py CHANGED
@@ -151,8 +151,8 @@ L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_CREDITS = 65535
151
151
  L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MTU = 23
152
152
  L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MPS = 23
153
153
  L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_MPS = 65533
154
- L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU = 2046
155
- L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS = 2048
154
+ L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU = 2048
155
+ L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS = 2046
156
156
  L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_INITIAL_CREDITS = 256
157
157
 
158
158
  L2CAP_MAXIMUM_TRANSMISSION_UNIT_CONFIGURATION_OPTION_TYPE = 0x01
@@ -1926,7 +1926,7 @@ class ChannelManager:
1926
1926
  supervision_timeout=request.timeout,
1927
1927
  min_ce_length=0,
1928
1928
  max_ce_length=0,
1929
- ) # type: ignore[call-arg]
1929
+ )
1930
1930
  )
1931
1931
  else:
1932
1932
  self.send_control_frame(
@@ -18,7 +18,7 @@
18
18
  # -----------------------------------------------------------------------------
19
19
  import struct
20
20
  import logging
21
- from typing import List
21
+ from typing import List, Optional
22
22
 
23
23
  from bumble import l2cap
24
24
  from ..core import AdvertisingData
@@ -67,7 +67,7 @@ class AshaService(TemplateService):
67
67
  self.emit('volume', connection, value[0])
68
68
 
69
69
  # Handler for audio control commands
70
- def on_audio_control_point_write(connection: Connection, value):
70
+ def on_audio_control_point_write(connection: Optional[Connection], value):
71
71
  logger.info(f'--- AUDIO CONTROL POINT Write:{value.hex()}')
72
72
  opcode = value[0]
73
73
  if opcode == AshaService.OPCODE_START: