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.
Files changed (51) hide show
  1. bumble/_version.py +9 -4
  2. bumble/apps/auracast.py +631 -98
  3. bumble/apps/bench.py +238 -157
  4. bumble/apps/console.py +19 -12
  5. bumble/apps/controller_info.py +23 -7
  6. bumble/apps/device_info.py +50 -4
  7. bumble/apps/gg_bridge.py +1 -1
  8. bumble/apps/lea_unicast/app.py +61 -201
  9. bumble/att.py +51 -37
  10. bumble/audio/__init__.py +17 -0
  11. bumble/audio/io.py +553 -0
  12. bumble/controller.py +24 -9
  13. bumble/core.py +305 -156
  14. bumble/device.py +1090 -99
  15. bumble/gatt.py +36 -226
  16. bumble/gatt_adapters.py +374 -0
  17. bumble/gatt_client.py +52 -33
  18. bumble/gatt_server.py +5 -5
  19. bumble/hci.py +812 -14
  20. bumble/host.py +367 -65
  21. bumble/l2cap.py +3 -16
  22. bumble/pairing.py +5 -5
  23. bumble/pandora/host.py +7 -12
  24. bumble/profiles/aics.py +48 -57
  25. bumble/profiles/ascs.py +8 -19
  26. bumble/profiles/asha.py +16 -14
  27. bumble/profiles/bass.py +16 -22
  28. bumble/profiles/battery_service.py +13 -3
  29. bumble/profiles/device_information_service.py +16 -14
  30. bumble/profiles/gap.py +12 -8
  31. bumble/profiles/gatt_service.py +167 -0
  32. bumble/profiles/gmap.py +198 -0
  33. bumble/profiles/hap.py +8 -6
  34. bumble/profiles/heart_rate_service.py +20 -4
  35. bumble/profiles/le_audio.py +87 -4
  36. bumble/profiles/mcp.py +11 -9
  37. bumble/profiles/pacs.py +61 -16
  38. bumble/profiles/tmap.py +8 -12
  39. bumble/profiles/{vcp.py → vcs.py} +35 -29
  40. bumble/profiles/vocs.py +62 -85
  41. bumble/sdp.py +223 -93
  42. bumble/smp.py +1 -1
  43. bumble/utils.py +12 -2
  44. bumble/vendor/android/hci.py +1 -1
  45. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/METADATA +13 -11
  46. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/RECORD +50 -46
  47. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/WHEEL +1 -1
  48. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/entry_points.txt +1 -0
  49. bumble/apps/lea_unicast/liblc3.wasm +0 -0
  50. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/LICENSE +0 -0
  51. {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-2022 Google LLC
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 AclPacketQueue:
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.in_flight = 0
74
- self.send = send
75
- self.packets: Deque[hci.HCI_AclDataPacket] = collections.deque()
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
- def enqueue(self, packet: hci.HCI_AclDataPacket) -> None:
78
- self.packets.appendleft(packet)
79
- self.check_queue()
102
+ @property
103
+ def completed(self) -> int:
104
+ """Total number of packets completed since creation."""
105
+ return self._completed
80
106
 
81
- if self.packets:
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.in_flight} ACL packets in flight, '
84
- f'{len(self.packets)} in queue'
120
+ f'{self._in_flight} packets in flight, '
121
+ f'{len(self._packets)} in queue'
85
122
  )
86
123
 
87
- def check_queue(self) -> None:
88
- while self.packets and self.in_flight < self.max_in_flight:
89
- packet = self.packets.pop()
90
- self.send(packet)
91
- self.in_flight += 1
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
- def on_packets_completed(self, packet_count: int) -> None:
94
- if packet_count > self.in_flight:
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
- color(
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
- packet_count = self.in_flight
160
+ return
102
161
 
103
- self.in_flight -= packet_count
104
- self.check_queue()
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[AclPacketQueue] = (
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
- handle: int
221
+ connection_handle: int
138
222
 
139
223
 
140
224
  # -----------------------------------------------------------------------------
141
225
  @dataclasses.dataclass
142
- class CisLink:
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, CisLink]
235
+ cis_links: Dict[int, IsoLink]
236
+ bis_links: Dict[int, IsoLink]
151
237
  sco_links: Dict[int, ScoLink]
152
- acl_packet_queue: Optional[AclPacketQueue] = None
153
- le_acl_packet_queue: Optional[AclPacketQueue] = None
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 = AclPacketQueue(
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
- hc_le_acl_data_packet_length = 0
421
- hc_total_num_le_acl_data_packets = 0
422
- if self.supports_command(hci.HCI_LE_READ_BUFFER_SIZE_COMMAND):
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
- hc_le_acl_data_packet_length = (
427
- response.return_parameters.hc_le_acl_data_packet_length
546
+ le_acl_data_packet_length = (
547
+ response.return_parameters.le_acl_data_packet_length
428
548
  )
429
- hc_total_num_le_acl_data_packets = (
430
- response.return_parameters.hc_total_num_le_acl_data_packets
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'hc_le_acl_data_packet_length={hc_le_acl_data_packet_length},'
436
- f'hc_total_num_le_acl_data_packets={hc_total_num_le_acl_data_packets}'
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 hc_le_acl_data_packet_length == 0 or hc_total_num_le_acl_data_packets == 0:
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 = AclPacketQueue(
445
- max_packet_size=hc_le_acl_data_packet_length,
446
- max_in_flight=hc_total_num_le_acl_data_packets,
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(self, 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 connection := self.connections.get(connection_handle):
735
- connection.acl_packet_queue.on_packets_completed(num_completed_packets)
736
- elif not (
737
- self.cis_links.get(connection_handle)
738
- or self.sco_links.get(connection_handle)
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
- connection_phy = ConnectionPHY(event.tx_phy, event.rx_phy)
906
- self.emit('connection_phy_update', connection.handle, connection_phy)
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.cis_links[event.connection_handle] = CisLink(
960
- handle=event.connection_handle,
961
- peer_address=hci.Address.ANY,
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
- handle=event.connection_handle,
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.response:
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-2023 Google LLC
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)