bumble 0.0.204__py3-none-any.whl → 0.0.207__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
@@ -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
106
+
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()
80
117
 
81
- if self.packets:
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.
92
127
 
93
- def on_packets_completed(self, packet_count: int) -> None:
94
- if packet_count > self.in_flight:
128
+ All packets associated with the connection that are in flight are implicitly
129
+ marked as completed, but no 'flow' event is emitted.
130
+ """
131
+
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
161
+
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
102
181
 
103
- self.in_flight -= packet_count
104
- self.check_queue()
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]] = {} # BIG Handle to BIS Handles
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,6 +257,7 @@ 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
173
262
  self.pending_command = None
174
263
  self.pending_response: Optional[asyncio.Future[Any]] = None
@@ -387,6 +476,12 @@ class Host(AbortableEventEmitter):
387
476
  hci.HCI_LE_TRANSMIT_POWER_REPORTING_EVENT,
388
477
  hci.HCI_LE_BIGINFO_ADVERTISING_REPORT_EVENT,
389
478
  hci.HCI_LE_SUBRATE_CHANGE_EVENT,
479
+ hci.HCI_LE_CS_READ_REMOTE_SUPPORTED_CAPABILITIES_COMPLETE_EVENT,
480
+ hci.HCI_LE_CS_PROCEDURE_ENABLE_COMPLETE_EVENT,
481
+ hci.HCI_LE_CS_SECURITY_ENABLE_COMPLETE_EVENT,
482
+ hci.HCI_LE_CS_CONFIG_COMPLETE_EVENT,
483
+ hci.HCI_LE_CS_SUBEVENT_RESULT_EVENT,
484
+ hci.HCI_LE_CS_SUBEVENT_RESULT_CONTINUE_EVENT,
390
485
  ]
391
486
  )
392
487
 
@@ -411,39 +506,70 @@ class Host(AbortableEventEmitter):
411
506
  f'hc_total_num_acl_data_packets={hc_total_num_acl_data_packets}'
412
507
  )
413
508
 
414
- self.acl_packet_queue = AclPacketQueue(
509
+ self.acl_packet_queue = DataPacketQueue(
415
510
  max_packet_size=hc_acl_data_packet_length,
416
511
  max_in_flight=hc_total_num_acl_data_packets,
417
512
  send=self.send_hci_packet,
418
513
  )
419
514
 
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):
515
+ le_acl_data_packet_length = 0
516
+ total_num_le_acl_data_packets = 0
517
+ iso_data_packet_length = 0
518
+ total_num_iso_data_packets = 0
519
+ if self.supports_command(hci.HCI_LE_READ_BUFFER_SIZE_V2_COMMAND):
520
+ response = await self.send_command(
521
+ hci.HCI_LE_Read_Buffer_Size_V2_Command(), check_result=True
522
+ )
523
+ le_acl_data_packet_length = (
524
+ response.return_parameters.le_acl_data_packet_length
525
+ )
526
+ total_num_le_acl_data_packets = (
527
+ response.return_parameters.total_num_le_acl_data_packets
528
+ )
529
+ iso_data_packet_length = response.return_parameters.iso_data_packet_length
530
+ total_num_iso_data_packets = (
531
+ response.return_parameters.total_num_iso_data_packets
532
+ )
533
+
534
+ logger.debug(
535
+ 'HCI LE flow control: '
536
+ f'le_acl_data_packet_length={le_acl_data_packet_length},'
537
+ f'total_num_le_acl_data_packets={total_num_le_acl_data_packets}'
538
+ f'iso_data_packet_length={iso_data_packet_length},'
539
+ f'total_num_iso_data_packets={total_num_iso_data_packets}'
540
+ )
541
+ elif self.supports_command(hci.HCI_LE_READ_BUFFER_SIZE_COMMAND):
423
542
  response = await self.send_command(
424
543
  hci.HCI_LE_Read_Buffer_Size_Command(), check_result=True
425
544
  )
426
- hc_le_acl_data_packet_length = (
427
- response.return_parameters.hc_le_acl_data_packet_length
545
+ le_acl_data_packet_length = (
546
+ response.return_parameters.le_acl_data_packet_length
428
547
  )
429
- hc_total_num_le_acl_data_packets = (
430
- response.return_parameters.hc_total_num_le_acl_data_packets
548
+ total_num_le_acl_data_packets = (
549
+ response.return_parameters.total_num_le_acl_data_packets
431
550
  )
432
551
 
433
552
  logger.debug(
434
553
  '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}'
554
+ f'le_acl_data_packet_length={le_acl_data_packet_length},'
555
+ f'total_num_le_acl_data_packets={total_num_le_acl_data_packets}'
437
556
  )
438
557
 
439
- if hc_le_acl_data_packet_length == 0 or hc_total_num_le_acl_data_packets == 0:
558
+ if le_acl_data_packet_length == 0 or total_num_le_acl_data_packets == 0:
440
559
  # LE and Classic share the same queue
441
560
  self.le_acl_packet_queue = self.acl_packet_queue
442
561
  else:
443
562
  # 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,
563
+ self.le_acl_packet_queue = DataPacketQueue(
564
+ max_packet_size=le_acl_data_packet_length,
565
+ max_in_flight=total_num_le_acl_data_packets,
566
+ send=self.send_hci_packet,
567
+ )
568
+
569
+ if iso_data_packet_length and total_num_iso_data_packets:
570
+ self.iso_packet_queue = DataPacketQueue(
571
+ max_packet_size=iso_data_packet_length,
572
+ max_in_flight=total_num_iso_data_packets,
447
573
  send=self.send_hci_packet,
448
574
  )
449
575
 
@@ -595,11 +721,78 @@ class Host(AbortableEventEmitter):
595
721
  data=l2cap_pdu[offset : offset + data_total_length],
596
722
  )
597
723
  logger.debug(f'>>> ACL packet enqueue: (CID={cid}) {acl_packet}')
598
- packet_queue.enqueue(acl_packet)
724
+ packet_queue.enqueue(acl_packet, connection_handle)
599
725
  pb_flag = 1
600
726
  offset += data_total_length
601
727
  bytes_remaining -= data_total_length
602
728
 
729
+ def get_data_packet_queue(self, connection_handle: int) -> DataPacketQueue | None:
730
+ if connection := self.connections.get(connection_handle):
731
+ return connection.acl_packet_queue
732
+
733
+ if iso_link := self.cis_links.get(connection_handle) or self.bis_links.get(
734
+ connection_handle
735
+ ):
736
+ return iso_link.packet_queue
737
+
738
+ return None
739
+
740
+ def send_iso_sdu(self, connection_handle: int, sdu: bytes) -> None:
741
+ if not (
742
+ iso_link := self.cis_links.get(connection_handle)
743
+ or self.bis_links.get(connection_handle)
744
+ ):
745
+ logger.warning(f"no ISO link for connection handle {connection_handle}")
746
+ return
747
+
748
+ if iso_link.packet_queue is None:
749
+ logger.warning("ISO link has no data packet queue")
750
+ return
751
+
752
+ bytes_remaining = len(sdu)
753
+ offset = 0
754
+ while bytes_remaining:
755
+ is_first_fragment = offset == 0
756
+ header_length = 4 if is_first_fragment else 0
757
+ assert iso_link.packet_queue.max_packet_size > header_length
758
+ fragment_length = min(
759
+ bytes_remaining, iso_link.packet_queue.max_packet_size - header_length
760
+ )
761
+ is_last_fragment = bytes_remaining == fragment_length
762
+ iso_sdu_fragment = sdu[offset : offset + fragment_length]
763
+ iso_link.packet_queue.enqueue(
764
+ (
765
+ hci.HCI_IsoDataPacket(
766
+ connection_handle=connection_handle,
767
+ data_total_length=header_length + fragment_length,
768
+ packet_sequence_number=iso_link.packet_sequence_number,
769
+ pb_flag=0b10 if is_last_fragment else 0b00,
770
+ packet_status_flag=0,
771
+ iso_sdu_length=len(sdu),
772
+ iso_sdu_fragment=iso_sdu_fragment,
773
+ )
774
+ if is_first_fragment
775
+ else hci.HCI_IsoDataPacket(
776
+ connection_handle=connection_handle,
777
+ data_total_length=fragment_length,
778
+ pb_flag=0b11 if is_last_fragment else 0b01,
779
+ iso_sdu_fragment=iso_sdu_fragment,
780
+ )
781
+ ),
782
+ connection_handle,
783
+ )
784
+
785
+ offset += fragment_length
786
+ bytes_remaining -= fragment_length
787
+
788
+ iso_link.packet_sequence_number = (iso_link.packet_sequence_number + 1) & 0xFFFF
789
+
790
+ def remove_big(self, big_handle: int) -> None:
791
+ if big := self.bigs.pop(big_handle, None):
792
+ for connection_handle in big:
793
+ if bis_link := self.bis_links.pop(connection_handle, None):
794
+ bis_link.packet_queue.flush(bis_link.handle)
795
+
603
796
  def supports_command(self, op_code: int) -> bool:
604
797
  return (
605
798
  self.local_supported_commands
@@ -727,16 +920,17 @@ class Host(AbortableEventEmitter):
727
920
  def on_hci_command_status_event(self, event):
728
921
  return self.on_command_processed(event)
729
922
 
730
- def on_hci_number_of_completed_packets_event(self, event):
923
+ def on_hci_number_of_completed_packets_event(
924
+ self, event: hci.HCI_Number_Of_Completed_Packets_Event
925
+ ) -> None:
731
926
  for connection_handle, num_completed_packets in zip(
732
927
  event.connection_handles, event.num_completed_packets
733
928
  ):
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
- ):
929
+ if queue := self.get_data_packet_queue(connection_handle):
930
+ queue.on_packets_completed(num_completed_packets, connection_handle)
931
+ continue
932
+
933
+ if connection_handle not in self.sco_links:
740
934
  logger.warning(
741
935
  'received packet completion event for unknown handle '
742
936
  f'0x{connection_handle:04X}'
@@ -854,11 +1048,7 @@ class Host(AbortableEventEmitter):
854
1048
  return
855
1049
 
856
1050
  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
- )
1051
+ logger.debug(f'### DISCONNECTION: {connection}, reason={event.reason}')
862
1052
 
863
1053
  # Notify the listeners
864
1054
  self.emit('disconnection', handle, event.reason)
@@ -869,6 +1059,12 @@ class Host(AbortableEventEmitter):
869
1059
  or self.cis_links.pop(handle, 0)
870
1060
  or self.sco_links.pop(handle, 0)
871
1061
  )
1062
+
1063
+ # Flush the data queues
1064
+ self.acl_packet_queue.flush(handle)
1065
+ self.le_acl_packet_queue.flush(handle)
1066
+ if self.iso_packet_queue:
1067
+ self.iso_packet_queue.flush(handle)
872
1068
  else:
873
1069
  logger.debug(f'### DISCONNECTION FAILED: {event.status}')
874
1070
 
@@ -953,12 +1149,94 @@ class Host(AbortableEventEmitter):
953
1149
  event.cis_id,
954
1150
  )
955
1151
 
1152
+ def on_hci_le_create_big_complete_event(self, event):
1153
+ self.bigs[event.big_handle] = set(event.connection_handle)
1154
+ if self.iso_packet_queue is None:
1155
+ logger.warning("BIS established but ISO packets not supported")
1156
+
1157
+ for connection_handle in event.connection_handle:
1158
+ self.bis_links[connection_handle] = IsoLink(
1159
+ connection_handle, self.iso_packet_queue
1160
+ )
1161
+
1162
+ self.emit(
1163
+ 'big_establishment',
1164
+ event.status,
1165
+ event.big_handle,
1166
+ event.connection_handle,
1167
+ event.big_sync_delay,
1168
+ event.transport_latency_big,
1169
+ event.phy,
1170
+ event.nse,
1171
+ event.bn,
1172
+ event.pto,
1173
+ event.irc,
1174
+ event.max_pdu,
1175
+ event.iso_interval,
1176
+ )
1177
+
1178
+ def on_hci_le_big_sync_established_event(self, event):
1179
+ self.bigs[event.big_handle] = set(event.connection_handle)
1180
+ for connection_handle in event.connection_handle:
1181
+ self.bis_links[connection_handle] = IsoLink(
1182
+ connection_handle, self.iso_packet_queue
1183
+ )
1184
+
1185
+ self.emit(
1186
+ 'big_sync_establishment',
1187
+ event.status,
1188
+ event.big_handle,
1189
+ event.transport_latency_big,
1190
+ event.nse,
1191
+ event.bn,
1192
+ event.pto,
1193
+ event.irc,
1194
+ event.max_pdu,
1195
+ event.iso_interval,
1196
+ event.connection_handle,
1197
+ )
1198
+
1199
+ def on_hci_le_big_sync_lost_event(self, event):
1200
+ self.remove_big(event.big_handle)
1201
+ self.emit('big_sync_lost', event.big_handle, event.reason)
1202
+
1203
+ def on_hci_le_terminate_big_complete_event(self, event):
1204
+ self.remove_big(event.big_handle)
1205
+ self.emit('big_termination', event.reason, event.big_handle)
1206
+
1207
+ def on_hci_le_periodic_advertising_sync_transfer_received_event(self, event):
1208
+ self.emit(
1209
+ 'periodic_advertising_sync_transfer',
1210
+ event.status,
1211
+ event.connection_handle,
1212
+ event.sync_handle,
1213
+ event.advertising_sid,
1214
+ event.advertiser_address,
1215
+ event.advertiser_phy,
1216
+ event.periodic_advertising_interval,
1217
+ event.advertiser_clock_accuracy,
1218
+ )
1219
+
1220
+ def on_hci_le_periodic_advertising_sync_transfer_received_v2_event(self, event):
1221
+ self.emit(
1222
+ 'periodic_advertising_sync_transfer',
1223
+ event.status,
1224
+ event.connection_handle,
1225
+ event.sync_handle,
1226
+ event.advertising_sid,
1227
+ event.advertiser_address,
1228
+ event.advertiser_phy,
1229
+ event.periodic_advertising_interval,
1230
+ event.advertiser_clock_accuracy,
1231
+ )
1232
+
956
1233
  def on_hci_le_cis_established_event(self, event):
957
1234
  # The remaining parameters are unused for now.
958
1235
  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,
1236
+ if self.iso_packet_queue is None:
1237
+ logger.warning("CIS established but ISO packets not supported")
1238
+ self.cis_links[event.connection_handle] = IsoLink(
1239
+ handle=event.connection_handle, packet_queue=self.iso_packet_queue
962
1240
  )
963
1241
  self.emit('cis_establishment', event.connection_handle)
964
1242
  else:
@@ -1028,7 +1306,7 @@ class Host(AbortableEventEmitter):
1028
1306
 
1029
1307
  self.sco_links[event.connection_handle] = ScoLink(
1030
1308
  peer_address=event.bd_addr,
1031
- handle=event.connection_handle,
1309
+ connection_handle=event.connection_handle,
1032
1310
  )
1033
1311
 
1034
1312
  # Notify the client
@@ -1249,5 +1527,23 @@ class Host(AbortableEventEmitter):
1249
1527
  int.from_bytes(event.le_features, 'little'),
1250
1528
  )
1251
1529
 
1530
+ def on_hci_le_cs_read_remote_supported_capabilities_complete_event(self, event):
1531
+ self.emit('cs_remote_supported_capabilities', event)
1532
+
1533
+ def on_hci_le_cs_security_enable_complete_event(self, event):
1534
+ self.emit('cs_security', event)
1535
+
1536
+ def on_hci_le_cs_config_complete_event(self, event):
1537
+ self.emit('cs_config', event)
1538
+
1539
+ def on_hci_le_cs_procedure_enable_complete_event(self, event):
1540
+ self.emit('cs_procedure', event)
1541
+
1542
+ def on_hci_le_cs_subevent_result_event(self, event):
1543
+ self.emit('cs_subevent_result', event)
1544
+
1545
+ def on_hci_le_cs_subevent_result_continue_event(self, event):
1546
+ self.emit('cs_subevent_result_continue', event)
1547
+
1252
1548
  def on_hci_vendor_event(self, event):
1253
1549
  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: