bumble 0.0.220__py3-none-any.whl → 0.0.222__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 (102) hide show
  1. bumble/_version.py +2 -2
  2. bumble/a2dp.py +5 -5
  3. bumble/apps/auracast.py +746 -473
  4. bumble/apps/bench.py +4 -5
  5. bumble/apps/console.py +5 -10
  6. bumble/apps/controller_info.py +12 -7
  7. bumble/apps/controller_loopback.py +1 -2
  8. bumble/apps/device_info.py +2 -3
  9. bumble/apps/gatt_dump.py +0 -1
  10. bumble/apps/lea_unicast/app.py +1 -1
  11. bumble/apps/pair.py +49 -46
  12. bumble/apps/pandora_server.py +2 -2
  13. bumble/apps/player/player.py +10 -12
  14. bumble/apps/rfcomm_bridge.py +10 -11
  15. bumble/apps/scan.py +1 -3
  16. bumble/apps/speaker/speaker.py +3 -4
  17. bumble/at.py +4 -5
  18. bumble/att.py +91 -25
  19. bumble/audio/io.py +5 -3
  20. bumble/avc.py +1 -2
  21. bumble/avctp.py +2 -3
  22. bumble/avdtp.py +53 -57
  23. bumble/avrcp.py +25 -27
  24. bumble/codecs.py +15 -15
  25. bumble/colors.py +7 -8
  26. bumble/controller.py +663 -391
  27. bumble/core.py +41 -49
  28. bumble/crypto/__init__.py +2 -1
  29. bumble/crypto/builtin.py +2 -8
  30. bumble/data_types.py +2 -1
  31. bumble/decoder.py +2 -3
  32. bumble/device.py +171 -142
  33. bumble/drivers/__init__.py +3 -2
  34. bumble/drivers/intel.py +6 -8
  35. bumble/drivers/rtk.py +1 -1
  36. bumble/gatt.py +9 -9
  37. bumble/gatt_adapters.py +6 -6
  38. bumble/gatt_client.py +110 -60
  39. bumble/gatt_server.py +209 -139
  40. bumble/hci.py +87 -74
  41. bumble/helpers.py +5 -5
  42. bumble/hfp.py +27 -26
  43. bumble/hid.py +9 -9
  44. bumble/host.py +44 -50
  45. bumble/keys.py +17 -17
  46. bumble/l2cap.py +1070 -218
  47. bumble/link.py +26 -159
  48. bumble/ll.py +200 -0
  49. bumble/pairing.py +14 -15
  50. bumble/pandora/__init__.py +2 -2
  51. bumble/pandora/device.py +6 -4
  52. bumble/pandora/host.py +19 -10
  53. bumble/pandora/l2cap.py +8 -9
  54. bumble/pandora/security.py +18 -16
  55. bumble/pandora/utils.py +4 -4
  56. bumble/profiles/aics.py +6 -8
  57. bumble/profiles/ams.py +3 -5
  58. bumble/profiles/ancs.py +11 -11
  59. bumble/profiles/ascs.py +5 -5
  60. bumble/profiles/asha.py +10 -9
  61. bumble/profiles/bass.py +9 -3
  62. bumble/profiles/battery_service.py +1 -2
  63. bumble/profiles/csip.py +9 -10
  64. bumble/profiles/device_information_service.py +16 -17
  65. bumble/profiles/gap.py +3 -4
  66. bumble/profiles/gatt_service.py +0 -1
  67. bumble/profiles/gmap.py +12 -13
  68. bumble/profiles/hap.py +3 -3
  69. bumble/profiles/heart_rate_service.py +7 -8
  70. bumble/profiles/le_audio.py +1 -1
  71. bumble/profiles/mcp.py +28 -28
  72. bumble/profiles/pacs.py +13 -17
  73. bumble/profiles/pbp.py +16 -0
  74. bumble/profiles/vcs.py +2 -2
  75. bumble/profiles/vocs.py +6 -9
  76. bumble/rfcomm.py +19 -18
  77. bumble/sdp.py +12 -11
  78. bumble/smp.py +20 -30
  79. bumble/snoop.py +2 -1
  80. bumble/tools/generate_company_id_list.py +1 -1
  81. bumble/tools/intel_util.py +2 -2
  82. bumble/tools/rtk_fw_download.py +1 -1
  83. bumble/tools/rtk_util.py +1 -1
  84. bumble/transport/__init__.py +1 -2
  85. bumble/transport/android_emulator.py +2 -3
  86. bumble/transport/android_netsim.py +49 -40
  87. bumble/transport/common.py +9 -9
  88. bumble/transport/file.py +1 -2
  89. bumble/transport/hci_socket.py +2 -3
  90. bumble/transport/pty.py +3 -5
  91. bumble/transport/pyusb.py +8 -5
  92. bumble/transport/serial.py +1 -2
  93. bumble/transport/vhci.py +1 -2
  94. bumble/transport/ws_server.py +2 -3
  95. bumble/utils.py +22 -9
  96. bumble/vendor/android/hci.py +4 -2
  97. {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/METADATA +3 -2
  98. {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/RECORD +102 -101
  99. {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/WHEEL +0 -0
  100. {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/entry_points.txt +0 -0
  101. {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/licenses/LICENSE +0 -0
  102. {bumble-0.0.220.dist-info → bumble-0.0.222.dist-info}/top_level.txt +0 -0
bumble/l2cap.py CHANGED
@@ -20,21 +20,14 @@ from __future__ import annotations
20
20
  import asyncio
21
21
  import dataclasses
22
22
  import enum
23
+ import itertools
23
24
  import logging
24
25
  import struct
25
26
  from collections import deque
26
- from collections.abc import Sequence
27
- from typing import (
28
- TYPE_CHECKING,
29
- Any,
30
- Callable,
31
- ClassVar,
32
- Iterable,
33
- Optional,
34
- SupportsBytes,
35
- TypeVar,
36
- Union,
37
- )
27
+ from collections.abc import Callable, Iterable, Sequence
28
+ from typing import TYPE_CHECKING, Any, ClassVar, SupportsBytes, TypeVar
29
+
30
+ from typing_extensions import override
38
31
 
39
32
  from bumble import hci, utils
40
33
  from bumble.colors import color
@@ -69,7 +62,12 @@ L2CAP_MIN_LE_MTU = 23
69
62
  L2CAP_MIN_BR_EDR_MTU = 48
70
63
  L2CAP_MAX_BR_EDR_MTU = 65535
71
64
 
72
- L2CAP_DEFAULT_MTU = 2048 # Default value for the MTU we are willing to accept
65
+ L2CAP_DEFAULT_MTU = 2048 # Default value for the MTU we are willing to accept
66
+ L2CAP_DEFAULT_MPS = 1010 # Default value for the MPS we are willing to accept
67
+ DEFAULT_TX_WINDOW_SIZE = 63
68
+ DEFAULT_MAX_RETRANSMISSION = 1
69
+ DEFAULT_RETRANSMISSION_TIMEOUT = 2.0
70
+ DEFAULT_MONITOR_TIMEOUT = 12.0
73
71
 
74
72
  L2CAP_DEFAULT_CONNECTIONLESS_MTU = 1024
75
73
 
@@ -133,29 +131,65 @@ L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU = 2048
133
131
  L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS = 2048
134
132
  L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_INITIAL_CREDITS = 256
135
133
 
136
- L2CAP_MAXIMUM_TRANSMISSION_UNIT_CONFIGURATION_OPTION_TYPE = 0x01
137
-
138
- L2CAP_MTU_CONFIGURATION_PARAMETER_TYPE = 0x01
139
-
140
134
  # fmt: on
141
135
  # pylint: enable=line-too-long
142
136
 
143
137
 
138
+ class TransmissionMode(utils.OpenIntEnum):
139
+ '''See Bluetooth spec @ Vol 3, Part A - 5.4. Retransmission and Flow Control option'''
140
+
141
+ BASIC = 0x00
142
+ RETRANSMISSION = 0x01
143
+ FLOW_CONTROL = 0x02
144
+ ENHANCED_RETRANSMISSION = 0x03
145
+ STREAMING = 0x04
146
+
147
+
144
148
  # -----------------------------------------------------------------------------
145
149
  # Classes
146
150
  # -----------------------------------------------------------------------------
147
151
  # pylint: disable=invalid-name
148
152
 
149
153
 
154
+ class L2capError(ProtocolError):
155
+ def __init__(self, error_code, error_name='', details=''):
156
+ super().__init__(error_code, 'L2CAP', error_name, details)
157
+
158
+
150
159
  @dataclasses.dataclass
151
160
  class ClassicChannelSpec:
152
- psm: Optional[int] = None
161
+ '''Spec of L2CAP Channel over Classic Transport.
162
+
163
+ Attributes:
164
+ psm: PSM of channel. This is optional for server, and when it is None, a PSM
165
+ will be allocated.
166
+ mtu: Maximum Transmission Unit.
167
+ mps: Maximum PDU payload Size.
168
+ tx_window_size: The size of the transmission window for Flow Control mode,
169
+ Retransmission mode, and Enhanced Retransmission mode.
170
+ max_retransmission: The number of transmissions of a single I-frame that L2CAP
171
+ is allowed to try in Retransmission mode and Enhanced Retransmission mode.
172
+ retransmission_timeout: The timeout of retransmission in seconds.
173
+ monitor_timeout: The interval at which S-frames should be transmitted on the
174
+ return channel when no frames are received on the forward channel.
175
+ mode: The transmission mode to use.
176
+ fcs_enabled: Whether to enable FCS (Frame Check Sequence).
177
+ '''
178
+
179
+ psm: int | None = None
153
180
  mtu: int = L2CAP_DEFAULT_MTU
181
+ mps: int = L2CAP_DEFAULT_MPS
182
+ tx_window_size: int = DEFAULT_TX_WINDOW_SIZE
183
+ max_retransmission: int = DEFAULT_MAX_RETRANSMISSION
184
+ retransmission_timeout: float = DEFAULT_RETRANSMISSION_TIMEOUT
185
+ monitor_timeout: float = DEFAULT_MONITOR_TIMEOUT
186
+ mode: TransmissionMode = TransmissionMode.BASIC
187
+ fcs_enabled: bool = False
154
188
 
155
189
 
156
190
  @dataclasses.dataclass
157
191
  class LeCreditBasedChannelSpec:
158
- psm: Optional[int] = None
192
+ psm: int | None = None
159
193
  mtu: int = L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU
160
194
  mps: int = L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS
161
195
  max_credits: int = L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_INITIAL_CREDITS
@@ -183,20 +217,29 @@ class L2CAP_PDU:
183
217
  See Bluetooth spec @ Vol 3, Part A - 3 DATA PACKET FORMAT
184
218
  '''
185
219
 
186
- @staticmethod
187
- def from_bytes(data: bytes) -> L2CAP_PDU:
220
+ @classmethod
221
+ def from_bytes(cls, data: bytes) -> L2CAP_PDU:
188
222
  # Check parameters
189
223
  if len(data) < 4:
190
224
  raise InvalidPacketError('not enough data for L2CAP header')
191
225
 
192
- _, l2cap_pdu_cid = struct.unpack_from('<HH', data, 0)
193
- l2cap_pdu_payload = data[4:]
226
+ length, l2cap_pdu_cid = struct.unpack_from('<HH', data, 0)
227
+ l2cap_pdu_payload = data[4 : 4 + length]
194
228
 
195
- return L2CAP_PDU(l2cap_pdu_cid, l2cap_pdu_payload)
229
+ return cls(l2cap_pdu_cid, l2cap_pdu_payload)
196
230
 
197
231
  def __bytes__(self) -> bytes:
198
- header = struct.pack('<HH', len(self.payload), self.cid)
199
- return header + self.payload
232
+ return self.to_bytes(with_fcs=False)
233
+
234
+ def to_bytes(self, with_fcs: bool = False) -> bytes:
235
+ length = len(self.payload)
236
+ if with_fcs:
237
+ length += 2
238
+ header = struct.pack('<HH', length, self.cid)
239
+ body = header + self.payload
240
+ if with_fcs:
241
+ body += struct.pack('<H', utils.crc_16(body))
242
+ return body
200
243
 
201
244
  def __init__(self, cid: int, payload: bytes) -> None:
202
245
  self.cid = cid
@@ -206,6 +249,117 @@ class L2CAP_PDU:
206
249
  return f'{color("L2CAP", "green")} [CID={self.cid}]: {self.payload.hex()}'
207
250
 
208
251
 
252
+ class ControlField:
253
+ '''
254
+ See Bluetooth spec @ Vol 3, Part A - 3.3.2 Control field.
255
+ '''
256
+
257
+ class FieldType(utils.OpenIntEnum):
258
+ I_FRAME = 0x00
259
+ S_FRAME = 0x01
260
+
261
+ class SegmentationAndReassembly(utils.OpenIntEnum):
262
+ UNSEGMENTED = 0x00
263
+ START = 0x01
264
+ END = 0x02
265
+ CONTINUATION = 0x03
266
+
267
+ class SupervisoryFunction(utils.OpenIntEnum):
268
+ # Receiver Ready
269
+ RR = 0
270
+ # Reject
271
+ REJ = 1
272
+ # Receiver Not Ready
273
+ RNR = 2
274
+ # Select Reject
275
+ SREJ = 3
276
+
277
+ class RetransmissionBit(utils.OpenIntEnum):
278
+ NORMAL = 0x00
279
+ RETRANSMISSION = 0x01
280
+
281
+ req_seq: int
282
+ frame_type: ClassVar[FieldType]
283
+
284
+ def __bytes__(self) -> bytes:
285
+ raise NotImplementedError()
286
+
287
+
288
+ class EnhancedControlField(ControlField):
289
+ """Base control field used in Enhanced Retransmission and Streaming Mode."""
290
+
291
+ final: int
292
+
293
+ @classmethod
294
+ def from_bytes(cls, data: bytes) -> EnhancedControlField:
295
+ frame_type = data[0] & 0x01
296
+ if frame_type == cls.FieldType.I_FRAME:
297
+ return InformationEnhancedControlField.from_bytes(data)
298
+ elif frame_type == cls.FieldType.S_FRAME:
299
+ return SupervisoryEnhancedControlField.from_bytes(data)
300
+ else:
301
+ raise InvalidArgumentError(f'Invalid frame type: {frame_type}')
302
+
303
+
304
+ @dataclasses.dataclass
305
+ class InformationEnhancedControlField(EnhancedControlField):
306
+ tx_seq: int
307
+ sar: int
308
+ req_seq: int = 0
309
+ final: int = 1
310
+
311
+ frame_type = EnhancedControlField.FieldType.I_FRAME
312
+
313
+ @classmethod
314
+ def from_bytes(cls, data: bytes) -> EnhancedControlField:
315
+ return cls(
316
+ tx_seq=(data[0] >> 1) & 0b0111111,
317
+ final=(data[0] >> 7) & 0b1,
318
+ req_seq=(data[1] & 0b00111111),
319
+ sar=(data[1] >> 6) & 0b11,
320
+ )
321
+
322
+ def __bytes__(self) -> bytes:
323
+ return bytes(
324
+ [
325
+ self.frame_type | (self.tx_seq << 1) | (self.final << 7),
326
+ self.req_seq | (self.sar << 6),
327
+ ]
328
+ )
329
+
330
+
331
+ @dataclasses.dataclass
332
+ class SupervisoryEnhancedControlField(EnhancedControlField):
333
+ supervision_function: int = ControlField.SupervisoryFunction.RR
334
+ poll: int = 0
335
+ req_seq: int = 0
336
+ final: int = 0
337
+
338
+ frame_type = EnhancedControlField.FieldType.S_FRAME
339
+
340
+ @classmethod
341
+ def from_bytes(cls, data: bytes) -> EnhancedControlField:
342
+ return cls(
343
+ supervision_function=(data[0] >> 2) & 0b11,
344
+ poll=(data[0] >> 4) & 0b1,
345
+ final=(data[0] >> 7) & 0b1,
346
+ req_seq=(data[1] & 0b1111111),
347
+ )
348
+
349
+ def __bytes__(self) -> bytes:
350
+ return bytes(
351
+ [
352
+ (
353
+ self.frame_type
354
+ | (self.supervision_function << 2)
355
+ | self.poll << 7
356
+ | (self.final << 7)
357
+ ),
358
+ self.req_seq,
359
+ ]
360
+ )
361
+
362
+
209
363
  # -----------------------------------------------------------------------------
210
364
  @dataclasses.dataclass
211
365
  class L2CAP_Control_Frame:
@@ -217,7 +371,7 @@ class L2CAP_Control_Frame:
217
371
  fields: ClassVar[hci.Fields] = ()
218
372
  code: int = dataclasses.field(default=0, init=False)
219
373
  name: str = dataclasses.field(default='', init=False)
220
- _payload: Optional[bytes] = dataclasses.field(default=None, init=False)
374
+ _payload: bytes | None = dataclasses.field(default=None, init=False)
221
375
 
222
376
  identifier: int
223
377
 
@@ -248,14 +402,16 @@ class L2CAP_Control_Frame:
248
402
  return frame
249
403
 
250
404
  @staticmethod
251
- def decode_configuration_options(data: bytes) -> list[tuple[int, bytes]]:
405
+ def decode_configuration_options(
406
+ data: bytes,
407
+ ) -> list[tuple[L2CAP_Configure_Request.ParameterType, bytes]]:
252
408
  options = []
253
409
  while len(data) >= 2:
254
410
  value_type = data[0]
255
411
  length = data[1]
256
412
  value = data[2 : 2 + length]
257
413
  data = data[2 + length :]
258
- options.append((value_type, value))
414
+ options.append((L2CAP_Configure_Request.ParameterType(value_type), value))
259
415
 
260
416
  return options
261
417
 
@@ -398,6 +554,15 @@ class L2CAP_Configure_Request(L2CAP_Control_Frame):
398
554
  See Bluetooth spec @ Vol 3, Part A - 4.4 CONFIGURATION REQUEST
399
555
  '''
400
556
 
557
+ class ParameterType(utils.OpenIntEnum):
558
+ MTU = 0x01
559
+ FLUSH_TIMEOUT = 0x02
560
+ QOS = 0x03
561
+ RETRANSMISSION_AND_FLOW_CONTROL = 0x04
562
+ FCS = 0x05
563
+ EXTENDED_FLOW_SPEC = 0x06
564
+ EXTENDED_WINDOW_SIZE = 0x07
565
+
401
566
  destination_cid: int = dataclasses.field(metadata=hci.metadata(2))
402
567
  flags: int = dataclasses.field(metadata=hci.metadata(2))
403
568
  options: bytes = dataclasses.field(metadata=hci.metadata('*'))
@@ -484,17 +649,18 @@ class L2CAP_Information_Request(L2CAP_Control_Frame):
484
649
  EXTENDED_FEATURES_SUPPORTED = 0x0002
485
650
  FIXED_CHANNELS_SUPPORTED = 0x0003
486
651
 
487
- EXTENDED_FEATURE_FLOW_MODE_CONTROL = 0x0001
488
- EXTENDED_FEATURE_RETRANSMISSION_MODE = 0x0002
489
- EXTENDED_FEATURE_BIDIRECTIONAL_QOS = 0x0004
490
- EXTENDED_FEATURE_ENHANCED_RETRANSMISSION_MODE = 0x0008
491
- EXTENDED_FEATURE_STREAMING_MODE = 0x0010
492
- EXTENDED_FEATURE_FCS_OPTION = 0x0020
493
- EXTENDED_FEATURE_EXTENDED_FLOW_SPEC = 0x0040
494
- EXTENDED_FEATURE_FIXED_CHANNELS = 0x0080
495
- EXTENDED_FEATURE_EXTENDED_WINDOW_SIZE = 0x0100
496
- EXTENDED_FEATURE_UNICAST_CONNECTIONLESS_DATA = 0x0200
497
- EXTENDED_FEATURE_ENHANCED_CREDIT_BASE_FLOW_CONTROL = 0x0400
652
+ class ExtendedFeatures(hci.SpecableFlag):
653
+ FLOW_MODE_CONTROL = 0x0001
654
+ RETRANSMISSION_MODE = 0x0002
655
+ BIDIRECTIONAL_QOS = 0x0004
656
+ ENHANCED_RETRANSMISSION_MODE = 0x0008
657
+ STREAMING_MODE = 0x0010
658
+ FCS_OPTION = 0x0020
659
+ EXTENDED_FLOW_SPEC = 0x0040
660
+ FIXED_CHANNELS = 0x0080
661
+ EXTENDED_WINDOW_SIZE = 0x0100
662
+ UNICAST_CONNECTIONLESS_DATA = 0x0200
663
+ ENHANCED_CREDIT_BASE_FLOW_CONTROL = 0x0400
498
664
 
499
665
  info_type: int = dataclasses.field(metadata=InfoType.type_metadata(2))
500
666
 
@@ -661,7 +827,7 @@ class L2CAP_Credit_Based_Connection_Response(L2CAP_Control_Frame):
661
827
  mtu: int = dataclasses.field(metadata=hci.metadata(2))
662
828
  mps: int = dataclasses.field(metadata=hci.metadata(2))
663
829
  initial_credits: int = dataclasses.field(metadata=hci.metadata(2))
664
- result: int = dataclasses.field(metadata=Result.type_metadata(2))
830
+ result: Result = dataclasses.field(metadata=Result.type_metadata(2))
665
831
  destination_cid: Sequence[int] = dataclasses.field(
666
832
  metadata=L2CAP_Credit_Based_Connection_Request.CID_METADATA
667
833
  )
@@ -702,6 +868,273 @@ class L2CAP_Credit_Based_Reconfigure_Response(L2CAP_Control_Frame):
702
868
  result: int = dataclasses.field(metadata=Result.type_metadata(2))
703
869
 
704
870
 
871
+ # -----------------------------------------------------------------------------
872
+ class Processor:
873
+ def __init__(self, channel: ClassicChannel) -> None:
874
+ self.channel = channel
875
+
876
+ def send_sdu(self, sdu: bytes) -> None:
877
+ self.channel.send_pdu(sdu)
878
+
879
+ def on_pdu(self, pdu: bytes) -> None:
880
+ self.channel.on_sdu(pdu)
881
+
882
+
883
+ # TODO: Handle retransmission
884
+ class EnhancedRetransmissionProcessor(Processor):
885
+ MAX_SEQ_NUM = 64
886
+
887
+ @dataclasses.dataclass
888
+ class _PendingPdu:
889
+ payload: bytes
890
+ tx_seq: int
891
+ sar: InformationEnhancedControlField.SegmentationAndReassembly
892
+ sdu_length: int = 0
893
+ req_seq: int = 0
894
+
895
+ def __bytes__(self) -> bytes:
896
+ return (
897
+ bytes(
898
+ InformationEnhancedControlField(
899
+ tx_seq=self.tx_seq,
900
+ req_seq=self.req_seq,
901
+ sar=self.sar,
902
+ )
903
+ )
904
+ + (
905
+ struct.pack('<H', self.sdu_length)
906
+ if self.sar
907
+ == InformationEnhancedControlField.SegmentationAndReassembly.START
908
+ else b''
909
+ )
910
+ + self.payload
911
+ )
912
+
913
+ _last_acked_tx_seq: int = 0
914
+ _last_acked_rx_seq: int = 0
915
+ _next_tx_seq: int = 0
916
+ _req_seq_num: int = 0
917
+ _remote_is_busy: bool = False
918
+ _in_sdu: bytes = b''
919
+
920
+ _num_receiver_ready_polls_sent: int = 0
921
+ _pending_pdus: list[_PendingPdu]
922
+ _tx_window: list[_PendingPdu]
923
+ _monitor_handle: asyncio.TimerHandle | None = None
924
+ _receiver_ready_poll_handle: asyncio.TimerHandle | None = None
925
+
926
+ # Timeout, in seconds.
927
+ monitor_timeout: float
928
+ retransmission_timeout: float
929
+
930
+ def __init__(
931
+ self,
932
+ channel: ClassicChannel,
933
+ peer_tx_window_size: int = DEFAULT_TX_WINDOW_SIZE,
934
+ peer_max_retransmission: int = DEFAULT_MAX_RETRANSMISSION,
935
+ peer_mps: int = L2CAP_DEFAULT_MPS,
936
+ ):
937
+ spec = channel.spec
938
+ self.mps = spec.mps
939
+ self.peer_mps = peer_mps
940
+ self.peer_tx_window_size = peer_tx_window_size
941
+ self._pending_pdus = []
942
+ self._tx_window = []
943
+ self.monitor_timeout = spec.monitor_timeout
944
+ self.channel = channel
945
+ self.retransmission_timeout = spec.retransmission_timeout
946
+ self.peer_max_retransmission = peer_max_retransmission
947
+
948
+ def _monitor(self) -> None:
949
+ if (
950
+ self.peer_max_retransmission <= 0
951
+ or self._num_receiver_ready_polls_sent < self.peer_max_retransmission
952
+ ):
953
+ self._send_receiver_ready_poll()
954
+ self._start_monitor()
955
+ else:
956
+ logger.error("Max retransmission exceeded")
957
+
958
+ def _receiver_ready_poll(self) -> None:
959
+ self._send_receiver_ready_poll()
960
+ self._start_monitor()
961
+
962
+ def _start_monitor(self) -> None:
963
+ if self._monitor_handle:
964
+ self._monitor_handle.cancel()
965
+ self._monitor_handle = asyncio.get_running_loop().call_later(
966
+ self.monitor_timeout, self._monitor
967
+ )
968
+
969
+ def _start_receiver_ready_poll(self) -> None:
970
+ if self._receiver_ready_poll_handle:
971
+ self._receiver_ready_poll_handle.cancel()
972
+ self._num_receiver_ready_polls_sent = 0
973
+
974
+ self._receiver_ready_poll_handle = asyncio.get_running_loop().call_later(
975
+ self.retransmission_timeout, self._receiver_ready_poll
976
+ )
977
+
978
+ def _send_receiver_ready_poll(self) -> None:
979
+ self._num_receiver_ready_polls_sent += 1
980
+ self._send_s_frame(
981
+ supervision_function=SupervisoryEnhancedControlField.SupervisoryFunction.RR,
982
+ final=1,
983
+ )
984
+
985
+ def _get_next_tx_seq(self) -> int:
986
+ seq_num = self._next_tx_seq
987
+ self._next_tx_seq = (self._next_tx_seq + 1) % self.MAX_SEQ_NUM
988
+ return seq_num
989
+
990
+ @override
991
+ def send_sdu(self, sdu: bytes) -> None:
992
+ if len(sdu) <= self.peer_mps:
993
+ pdu = self._PendingPdu(
994
+ payload=sdu,
995
+ tx_seq=self._get_next_tx_seq(),
996
+ req_seq=self._req_seq_num,
997
+ sar=InformationEnhancedControlField.SegmentationAndReassembly.UNSEGMENTED,
998
+ )
999
+ self._pending_pdus.append(pdu)
1000
+ else:
1001
+ for offset in range(0, len(sdu), self.peer_mps):
1002
+ payload = sdu[offset : offset + self.peer_mps]
1003
+ if offset == 0:
1004
+ sar = (
1005
+ InformationEnhancedControlField.SegmentationAndReassembly.START
1006
+ )
1007
+ elif offset + len(payload) >= len(sdu):
1008
+ sar = InformationEnhancedControlField.SegmentationAndReassembly.END
1009
+ else:
1010
+ sar = (
1011
+ InformationEnhancedControlField.SegmentationAndReassembly.CONTINUATION
1012
+ )
1013
+ pdu = self._PendingPdu(
1014
+ payload=payload,
1015
+ tx_seq=self._get_next_tx_seq(),
1016
+ req_seq=self._req_seq_num,
1017
+ sar=sar,
1018
+ sdu_length=len(sdu),
1019
+ )
1020
+ self._pending_pdus.append(pdu)
1021
+ self._process_output()
1022
+
1023
+ @override
1024
+ def on_pdu(self, pdu: bytes) -> None:
1025
+ control_field = EnhancedControlField.from_bytes(pdu)
1026
+ self._update_ack_seq(control_field.req_seq, control_field.final != 0)
1027
+ if isinstance(control_field, InformationEnhancedControlField):
1028
+ if control_field.tx_seq != self._req_seq_num:
1029
+ logger.error(
1030
+ "tx_seq != self._req_seq_num, tx_seq: %d, self._req_seq_num: %d",
1031
+ control_field.tx_seq,
1032
+ self._req_seq_num,
1033
+ )
1034
+ return
1035
+ self._req_seq_num = (control_field.tx_seq + 1) % self.MAX_SEQ_NUM
1036
+
1037
+ if (
1038
+ control_field.sar
1039
+ == InformationEnhancedControlField.SegmentationAndReassembly.START
1040
+ ):
1041
+ # Drop Control Field(2) + SDU Length(2)
1042
+ self._in_sdu += pdu[4:]
1043
+ else:
1044
+ # Drop Control Field(2)
1045
+ self._in_sdu += pdu[2:]
1046
+ if control_field.sar in (
1047
+ InformationEnhancedControlField.SegmentationAndReassembly.END,
1048
+ InformationEnhancedControlField.SegmentationAndReassembly.UNSEGMENTED,
1049
+ ):
1050
+ self.channel.on_sdu(self._in_sdu)
1051
+ self._in_sdu = b''
1052
+
1053
+ # If sink doesn't trigger any I-frame, ack this frame.
1054
+ if self._req_seq_num != self._last_acked_rx_seq:
1055
+ self._send_s_frame(
1056
+ supervision_function=SupervisoryEnhancedControlField.SupervisoryFunction.RR,
1057
+ final=0,
1058
+ )
1059
+ elif isinstance(control_field, SupervisoryEnhancedControlField):
1060
+ self._remote_is_busy = (
1061
+ control_field.supervision_function
1062
+ == SupervisoryEnhancedControlField.SupervisoryFunction.RNR
1063
+ )
1064
+
1065
+ if control_field.supervision_function in (
1066
+ SupervisoryEnhancedControlField.SupervisoryFunction.RR,
1067
+ SupervisoryEnhancedControlField.SupervisoryFunction.RNR,
1068
+ ):
1069
+ if control_field.poll:
1070
+ self._send_s_frame(
1071
+ supervision_function=SupervisoryEnhancedControlField.SupervisoryFunction.RR,
1072
+ final=1,
1073
+ )
1074
+ else:
1075
+ # TODO: Handle Retransmission.
1076
+ pass
1077
+
1078
+ def _process_output(self) -> None:
1079
+ if self._remote_is_busy:
1080
+ logger.debug("Remote is busy")
1081
+ return
1082
+ if self._monitor_handle:
1083
+ logger.debug("Monitor handle is not None")
1084
+ return
1085
+
1086
+ pdu_to_send = self.peer_tx_window_size - len(self._tx_window)
1087
+ for pdu in itertools.islice(self._pending_pdus, pdu_to_send):
1088
+ self._send_i_frame(pdu)
1089
+ self._pending_pdus = self._pending_pdus[pdu_to_send:]
1090
+
1091
+ def _send_i_frame(self, pdu: _PendingPdu) -> None:
1092
+ pdu.req_seq = self._req_seq_num
1093
+
1094
+ self._start_receiver_ready_poll()
1095
+ self._tx_window.append(pdu)
1096
+ self.channel.send_pdu(bytes(pdu))
1097
+ self._last_acked_rx_seq = self._req_seq_num
1098
+
1099
+ def _send_s_frame(
1100
+ self,
1101
+ supervision_function: SupervisoryEnhancedControlField.SupervisoryFunction,
1102
+ final: int,
1103
+ ) -> None:
1104
+ self.channel.send_pdu(
1105
+ SupervisoryEnhancedControlField(
1106
+ supervision_function=supervision_function,
1107
+ final=final,
1108
+ req_seq=self._req_seq_num,
1109
+ )
1110
+ )
1111
+ self._last_acked_rx_seq = self._req_seq_num
1112
+
1113
+ def _update_ack_seq(self, new_seq: int, is_poll_response: bool) -> None:
1114
+ num_frames_acked = (new_seq - self._last_acked_tx_seq) % self.MAX_SEQ_NUM
1115
+ if num_frames_acked > len(self._tx_window):
1116
+ logger.error(
1117
+ "Received acknowledgment for %d frames but only %d frames are pending",
1118
+ num_frames_acked,
1119
+ len(self._tx_window),
1120
+ )
1121
+ return
1122
+ if is_poll_response and self._monitor_handle:
1123
+ self._monitor_handle.cancel()
1124
+ self._monitor_handle = None
1125
+
1126
+ del self._tx_window[:num_frames_acked]
1127
+ self._last_acked_tx_seq = new_seq
1128
+ if (
1129
+ self._last_acked_tx_seq == self._next_tx_seq
1130
+ and self._receiver_ready_poll_handle
1131
+ ):
1132
+ self._receiver_ready_poll_handle.cancel()
1133
+ self._receiver_ready_poll_handle = None
1134
+
1135
+ self._process_output()
1136
+
1137
+
705
1138
  # -----------------------------------------------------------------------------
706
1139
  class ClassicChannel(utils.EventEmitter):
707
1140
  class State(enum.IntEnum):
@@ -731,14 +1164,15 @@ class ClassicChannel(utils.EventEmitter):
731
1164
  EVENT_OPEN = "open"
732
1165
  EVENT_CLOSE = "close"
733
1166
 
734
- connection_result: Optional[asyncio.Future[None]]
735
- disconnection_result: Optional[asyncio.Future[None]]
736
- response: Optional[asyncio.Future[bytes]]
737
- sink: Optional[Callable[[bytes], Any]]
1167
+ connection_result: asyncio.Future[None] | None
1168
+ disconnection_result: asyncio.Future[None] | None
1169
+ response: asyncio.Future[bytes] | None
1170
+ sink: Callable[[bytes], Any] | None
738
1171
  state: State
739
1172
  connection: Connection
740
1173
  mtu: int
741
1174
  peer_mtu: int
1175
+ processor: Processor
742
1176
 
743
1177
  def __init__(
744
1178
  self,
@@ -747,14 +1181,14 @@ class ClassicChannel(utils.EventEmitter):
747
1181
  signaling_cid: int,
748
1182
  psm: int,
749
1183
  source_cid: int,
750
- mtu: int,
1184
+ spec: ClassicChannelSpec,
751
1185
  ) -> None:
752
1186
  super().__init__()
753
1187
  self.manager = manager
754
1188
  self.connection = connection
755
1189
  self.signaling_cid = signaling_cid
756
1190
  self.state = self.State.CLOSED
757
- self.mtu = mtu
1191
+ self.mtu = spec.mtu
758
1192
  self.peer_mtu = L2CAP_MIN_BR_EDR_MTU
759
1193
  self.psm = psm
760
1194
  self.source_cid = source_cid
@@ -762,26 +1196,47 @@ class ClassicChannel(utils.EventEmitter):
762
1196
  self.connection_result = None
763
1197
  self.disconnection_result = None
764
1198
  self.sink = None
1199
+ self.fcs_enabled = spec.fcs_enabled
1200
+ self.spec = spec
1201
+ self.mode = spec.mode
1202
+ # Configure mode-specific processor later on configure request.
1203
+ self.processor = Processor(self)
1204
+ if self.mode not in (
1205
+ TransmissionMode.BASIC,
1206
+ TransmissionMode.ENHANCED_RETRANSMISSION,
1207
+ ):
1208
+ raise InvalidArgumentError(f"Mode {spec.mode} is not supported")
765
1209
 
766
1210
  def _change_state(self, new_state: State) -> None:
767
1211
  logger.debug(f'{self} state change -> {color(new_state.name, "cyan")}')
768
1212
  self.state = new_state
769
1213
 
770
- def send_pdu(self, pdu: Union[SupportsBytes, bytes]) -> None:
1214
+ def write(self, sdu: bytes) -> None:
1215
+ self.processor.send_sdu(sdu)
1216
+
1217
+ def send_pdu(self, pdu: SupportsBytes | bytes) -> None:
771
1218
  if self.state != self.State.OPEN:
772
1219
  raise InvalidStateError('channel not open')
773
- self.manager.send_pdu(self.connection, self.destination_cid, pdu)
1220
+ self.manager.send_pdu(
1221
+ self.connection, self.destination_cid, pdu, self.fcs_enabled
1222
+ )
774
1223
 
775
1224
  def send_control_frame(self, frame: L2CAP_Control_Frame) -> None:
776
1225
  self.manager.send_control_frame(self.connection, self.signaling_cid, frame)
777
1226
 
778
1227
  def on_pdu(self, pdu: bytes) -> None:
1228
+ if self.fcs_enabled:
1229
+ # Drop FCS.
1230
+ pdu = pdu[:-2]
1231
+ self.processor.on_pdu(pdu)
1232
+
1233
+ def on_sdu(self, sdu: bytes) -> None:
779
1234
  if self.sink:
780
1235
  # pylint: disable=not-callable
781
- self.sink(pdu)
1236
+ self.sink(sdu)
782
1237
  else:
783
1238
  logger.warning(
784
- color('received pdu without a pending request or sink', 'red')
1239
+ color('received sdu without a pending request or sink', 'red')
785
1240
  )
786
1241
 
787
1242
  async def connect(self) -> None:
@@ -811,10 +1266,8 @@ class ClassicChannel(utils.EventEmitter):
811
1266
  finally:
812
1267
  self.connection_result = None
813
1268
 
814
- async def disconnect(self) -> None:
815
- if self.state != self.State.OPEN:
816
- raise InvalidStateError('invalid state')
817
-
1269
+ def _disconnect_sync(self) -> None:
1270
+ """For internal sync disconnection."""
818
1271
  self._change_state(self.State.WAIT_DISCONNECT)
819
1272
  self.send_control_frame(
820
1273
  L2CAP_Disconnection_Request(
@@ -827,7 +1280,21 @@ class ClassicChannel(utils.EventEmitter):
827
1280
  # Create a future to wait for the state machine to get to a success or error
828
1281
  # state
829
1282
  self.disconnection_result = asyncio.get_running_loop().create_future()
830
- return await self.disconnection_result
1283
+
1284
+ def _abort_connection_result(self, message: str = 'Connection failure') -> None:
1285
+ # Cancel pending connection result.
1286
+ if self.connection_result and not self.connection_result.done():
1287
+ self.connection_result.set_exception(
1288
+ L2capError(error_code=0, error_name=message)
1289
+ )
1290
+
1291
+ async def disconnect(self) -> None:
1292
+ if self.state != self.State.OPEN:
1293
+ raise InvalidStateError('invalid state')
1294
+
1295
+ self._disconnect_sync()
1296
+ if self.disconnection_result:
1297
+ return await self.disconnection_result
831
1298
 
832
1299
  def abort(self) -> None:
833
1300
  if self.state == self.State.OPEN:
@@ -835,20 +1302,40 @@ class ClassicChannel(utils.EventEmitter):
835
1302
  self.emit(self.EVENT_CLOSE)
836
1303
 
837
1304
  def send_configure_request(self) -> None:
838
- options = L2CAP_Control_Frame.encode_configuration_options(
839
- [
1305
+ options: list[tuple[int, bytes]] = [
1306
+ (
1307
+ L2CAP_Configure_Request.ParameterType.MTU,
1308
+ struct.pack('<H', self.mtu),
1309
+ )
1310
+ ]
1311
+ if self.mode == TransmissionMode.ENHANCED_RETRANSMISSION:
1312
+ options.append(
840
1313
  (
841
- L2CAP_MAXIMUM_TRANSMISSION_UNIT_CONFIGURATION_OPTION_TYPE,
842
- struct.pack('<H', self.mtu),
1314
+ L2CAP_Configure_Request.ParameterType.RETRANSMISSION_AND_FLOW_CONTROL,
1315
+ struct.pack(
1316
+ '<BBBHHH',
1317
+ TransmissionMode.ENHANCED_RETRANSMISSION,
1318
+ self.spec.tx_window_size,
1319
+ self.spec.max_retransmission,
1320
+ int(self.spec.retransmission_timeout * 1000),
1321
+ int(self.spec.monitor_timeout * 1000),
1322
+ self.spec.mps,
1323
+ ),
843
1324
  )
844
- ]
845
- )
1325
+ )
1326
+ if self.fcs_enabled:
1327
+ options.append(
1328
+ (
1329
+ L2CAP_Configure_Request.ParameterType.FCS,
1330
+ bytes([1 if self.fcs_enabled else 0]),
1331
+ )
1332
+ )
846
1333
  self.send_control_frame(
847
1334
  L2CAP_Configure_Request(
848
1335
  identifier=self.manager.next_identifier(self.connection),
849
1336
  destination_cid=self.destination_cid,
850
1337
  flags=0x0000,
851
- options=options,
1338
+ options=L2CAP_Control_Frame.encode_configuration_options(options),
852
1339
  )
853
1340
  )
854
1341
 
@@ -884,9 +1371,8 @@ class ClassicChannel(utils.EventEmitter):
884
1371
  self._change_state(self.State.CLOSED)
885
1372
  if self.connection_result:
886
1373
  self.connection_result.set_exception(
887
- ProtocolError(
1374
+ L2capError(
888
1375
  response.result,
889
- 'l2cap',
890
1376
  L2CAP_Connection_Response.Result(response.result).name,
891
1377
  )
892
1378
  )
@@ -903,20 +1389,111 @@ class ClassicChannel(utils.EventEmitter):
903
1389
 
904
1390
  # Decode the options
905
1391
  options = L2CAP_Control_Frame.decode_configuration_options(request.options)
1392
+ # Result to options
1393
+ replied_options = list[tuple[int, bytes]]()
1394
+ result = L2CAP_Configure_Response.Result.SUCCESS
1395
+ new_mode = TransmissionMode.BASIC
906
1396
  for option in options:
907
- if option[0] == L2CAP_MTU_CONFIGURATION_PARAMETER_TYPE:
908
- self.peer_mtu = struct.unpack('<H', option[1])[0]
909
- logger.debug(f'peer MTU = {self.peer_mtu}')
1397
+ match option[0]:
1398
+ case L2CAP_Configure_Request.ParameterType.MTU:
1399
+ self.peer_mtu = struct.unpack('<H', option[1])[0]
1400
+ logger.debug('Peer MTU = %d', self.peer_mtu)
1401
+ replied_options.append(option)
1402
+ case (
1403
+ L2CAP_Configure_Request.ParameterType.RETRANSMISSION_AND_FLOW_CONTROL
1404
+ ):
1405
+ (
1406
+ mode,
1407
+ peer_tx_window_size,
1408
+ peer_max_retransmission,
1409
+ peer_retransmission_timeout,
1410
+ peer_monitor_timeout,
1411
+ peer_mps,
1412
+ ) = struct.unpack_from('<BBBHHH', option[1])
1413
+ new_mode = TransmissionMode(mode)
1414
+ logger.debug(
1415
+ 'Peer requests Retransmission or Flow Control: mode=%s,'
1416
+ ' tx_window_size=%s,'
1417
+ ' max_retransmission=%s,'
1418
+ ' retransmission_timeout=%s,'
1419
+ ' monitor_timeout=%s,'
1420
+ ' mps=%s',
1421
+ new_mode.name,
1422
+ peer_tx_window_size,
1423
+ peer_max_retransmission,
1424
+ peer_retransmission_timeout,
1425
+ peer_monitor_timeout,
1426
+ peer_mps,
1427
+ )
1428
+ if new_mode != self.mode:
1429
+ logger.error('Mode mismatch, abort connection')
1430
+ self._abort_connection_result(
1431
+ 'Abort on configuration - mode mismatch'
1432
+ )
1433
+ self._disconnect_sync()
1434
+ return
1435
+
1436
+ if new_mode == TransmissionMode.BASIC:
1437
+ replied_options.append(option)
1438
+ elif new_mode == TransmissionMode.ENHANCED_RETRANSMISSION:
1439
+ self.processor = self.manager.make_mode_processor(
1440
+ self,
1441
+ mode=new_mode,
1442
+ peer_tx_window_size=peer_tx_window_size,
1443
+ peer_max_retransmission=peer_max_retransmission,
1444
+ peer_monitor_timeout=peer_monitor_timeout,
1445
+ peer_retransmission_timeout=peer_retransmission_timeout,
1446
+ peer_mps=peer_mps,
1447
+ )
1448
+ replied_options.append(option)
1449
+ else:
1450
+ logger.error("Mode %s is not supported", new_mode.name)
1451
+ self._abort_connection_result(
1452
+ 'Abort on configuration - unsupported mode'
1453
+ )
1454
+ self._disconnect_sync()
1455
+ return
1456
+
1457
+ case L2CAP_Configure_Request.ParameterType.FCS:
1458
+ enabled = option[1][0] != 0
1459
+ logger.debug("Peer requests FCS: %s", enabled)
1460
+ if (
1461
+ L2CAP_Information_Request.ExtendedFeatures.FCS_OPTION
1462
+ in self.manager.extended_features
1463
+ ):
1464
+ self.fcs_enabled = enabled
1465
+ replied_options.append(option)
1466
+ else:
1467
+ logger.error("Frame Check Sequence is not supported")
1468
+ result = (
1469
+ L2CAP_Configure_Response.Result.FAILURE_UNACCEPTABLE_PARAMETERS
1470
+ )
1471
+ replied_options = [option]
1472
+ break
1473
+ case _:
1474
+ logger.debug(
1475
+ "Reject unimplemented option %s[%s]",
1476
+ option[0].name,
1477
+ option[1].hex(),
1478
+ )
1479
+ result = L2CAP_Configure_Response.Result.FAILURE_UNKNOWN_OPTIONS
1480
+ replied_options = [option]
1481
+ break
910
1482
 
911
1483
  self.send_control_frame(
912
1484
  L2CAP_Configure_Response(
913
1485
  identifier=request.identifier,
914
1486
  source_cid=self.destination_cid,
915
1487
  flags=0x0000,
916
- result=L2CAP_Configure_Response.Result.SUCCESS,
917
- options=request.options, # TODO: don't accept everything blindly
1488
+ result=result,
1489
+ options=L2CAP_Control_Frame.encode_configuration_options(
1490
+ replied_options
1491
+ ),
918
1492
  )
919
1493
  )
1494
+ if result != L2CAP_Configure_Response.Result.SUCCESS:
1495
+ return
1496
+
920
1497
  if self.state == self.State.WAIT_CONFIG:
921
1498
  self._change_state(self.State.WAIT_SEND_CONFIG)
922
1499
  self.send_configure_request()
@@ -969,25 +1546,19 @@ class ClassicChannel(utils.EventEmitter):
969
1546
  # TODO: decide how to fail gracefully
970
1547
 
971
1548
  def on_disconnection_request(self, request: L2CAP_Disconnection_Request) -> None:
972
- if self.state in (self.State.OPEN, self.State.WAIT_DISCONNECT):
973
- self.send_control_frame(
974
- L2CAP_Disconnection_Response(
975
- identifier=request.identifier,
976
- destination_cid=request.destination_cid,
977
- source_cid=request.source_cid,
978
- )
1549
+ self.send_control_frame(
1550
+ L2CAP_Disconnection_Response(
1551
+ identifier=request.identifier,
1552
+ destination_cid=request.destination_cid,
1553
+ source_cid=request.source_cid,
979
1554
  )
980
- self._change_state(self.State.CLOSED)
981
- self.emit(self.EVENT_CLOSE)
982
- self.manager.on_channel_closed(self)
983
- else:
984
- logger.warning(color('invalid state', 'red'))
1555
+ )
1556
+ self._abort_connection_result()
1557
+ self._change_state(self.State.CLOSED)
1558
+ self.emit(self.EVENT_CLOSE)
1559
+ self.manager.on_channel_closed(self)
985
1560
 
986
1561
  def on_disconnection_response(self, response: L2CAP_Disconnection_Response) -> None:
987
- if self.state != self.State.WAIT_DISCONNECT:
988
- logger.warning(color('invalid state', 'red'))
989
- return
990
-
991
1562
  if (
992
1563
  response.destination_cid != self.destination_cid
993
1564
  or response.source_cid != self.source_cid
@@ -1026,22 +1597,23 @@ class LeCreditBasedChannel(utils.EventEmitter):
1026
1597
  CONNECTION_ERROR = 5
1027
1598
 
1028
1599
  out_queue: deque[bytes]
1029
- connection_result: Optional[asyncio.Future[LeCreditBasedChannel]]
1030
- disconnection_result: Optional[asyncio.Future[None]]
1031
- in_sdu: Optional[bytes]
1032
- out_sdu: Optional[bytes]
1600
+ connection_result: asyncio.Future[LeCreditBasedChannel] | None
1601
+ disconnection_result: asyncio.Future[None] | None
1602
+ in_sdu: bytes | None
1603
+ out_sdu: bytes | None
1033
1604
  state: State
1034
1605
  connection: Connection
1035
- sink: Optional[Callable[[bytes], Any]]
1606
+ sink: Callable[[bytes], Any] | None
1036
1607
 
1037
1608
  EVENT_OPEN = "open"
1038
1609
  EVENT_CLOSE = "close"
1610
+ EVENT_ATT_MTU_UPDATE = "att_mtu_update"
1039
1611
 
1040
1612
  def __init__(
1041
1613
  self,
1042
1614
  manager: ChannelManager,
1043
1615
  connection: Connection,
1044
- le_psm: int,
1616
+ psm: int,
1045
1617
  source_cid: int,
1046
1618
  destination_cid: int,
1047
1619
  mtu: int,
@@ -1055,7 +1627,7 @@ class LeCreditBasedChannel(utils.EventEmitter):
1055
1627
  super().__init__()
1056
1628
  self.manager = manager
1057
1629
  self.connection = connection
1058
- self.le_psm = le_psm
1630
+ self.psm = psm
1059
1631
  self.source_cid = source_cid
1060
1632
  self.destination_cid = destination_cid
1061
1633
  self.mtu = mtu
@@ -1075,6 +1647,7 @@ class LeCreditBasedChannel(utils.EventEmitter):
1075
1647
  self.connection_result = None
1076
1648
  self.disconnection_result = None
1077
1649
  self.drained = asyncio.Event()
1650
+ self.att_mtu = 0 # Filled by GATT client or server later.
1078
1651
 
1079
1652
  self.drained.set()
1080
1653
 
@@ -1092,7 +1665,7 @@ class LeCreditBasedChannel(utils.EventEmitter):
1092
1665
  elif new_state == self.State.DISCONNECTED:
1093
1666
  self.emit(self.EVENT_CLOSE)
1094
1667
 
1095
- def send_pdu(self, pdu: Union[SupportsBytes, bytes]) -> None:
1668
+ def send_pdu(self, pdu: SupportsBytes | bytes) -> None:
1096
1669
  self.manager.send_pdu(self.connection, self.destination_cid, pdu)
1097
1670
 
1098
1671
  def send_control_frame(self, frame: L2CAP_Control_Frame) -> None:
@@ -1111,7 +1684,7 @@ class LeCreditBasedChannel(utils.EventEmitter):
1111
1684
  self._change_state(self.State.CONNECTING)
1112
1685
  request = L2CAP_LE_Credit_Based_Connection_Request(
1113
1686
  identifier=identifier,
1114
- le_psm=self.le_psm,
1687
+ le_psm=self.psm,
1115
1688
  source_cid=self.source_cid,
1116
1689
  mtu=self.mtu,
1117
1690
  mps=self.mps,
@@ -1149,6 +1722,9 @@ class LeCreditBasedChannel(utils.EventEmitter):
1149
1722
  def abort(self) -> None:
1150
1723
  if self.state == self.State.CONNECTED:
1151
1724
  self._change_state(self.State.DISCONNECTED)
1725
+ if self.state == self.State.CONNECTING:
1726
+ if self.connection_result is not None:
1727
+ self.connection_result.cancel()
1152
1728
 
1153
1729
  def on_pdu(self, pdu: bytes) -> None:
1154
1730
  if self.sink is None:
@@ -1239,9 +1815,8 @@ class LeCreditBasedChannel(utils.EventEmitter):
1239
1815
  self._change_state(self.State.CONNECTED)
1240
1816
  else:
1241
1817
  self.connection_result.set_exception(
1242
- ProtocolError(
1818
+ L2capError(
1243
1819
  response.result,
1244
- 'l2cap',
1245
1820
  L2CAP_LE_Credit_Based_Connection_Response.Result(
1246
1821
  response.result
1247
1822
  ).name,
@@ -1252,6 +1827,22 @@ class LeCreditBasedChannel(utils.EventEmitter):
1252
1827
  # Cleanup
1253
1828
  self.connection_result = None
1254
1829
 
1830
+ def on_enhanced_connection_response(
1831
+ self, destination_cid: int, response: L2CAP_Credit_Based_Connection_Response
1832
+ ) -> None:
1833
+ if (
1834
+ response.result
1835
+ == L2CAP_Credit_Based_Connection_Response.Result.ALL_CONNECTIONS_SUCCESSFUL
1836
+ ):
1837
+ self.destination_cid = destination_cid
1838
+ self.peer_mtu = response.mtu
1839
+ self.peer_mps = response.mps
1840
+ self.credits = response.initial_credits
1841
+ self.connected = True
1842
+ self._change_state(self.State.CONNECTED)
1843
+ else:
1844
+ self._change_state(self.State.CONNECTION_ERROR)
1845
+
1255
1846
  def on_credits(self, credits: int) -> None: # pylint: disable=redefined-builtin
1256
1847
  self.credits += credits
1257
1848
  logger.debug(f'received {credits} credits, total = {self.credits}')
@@ -1287,6 +1878,10 @@ class LeCreditBasedChannel(utils.EventEmitter):
1287
1878
  self.disconnection_result.set_result(None)
1288
1879
  self.disconnection_result = None
1289
1880
 
1881
+ def on_att_mtu_update(self, mtu: int) -> None:
1882
+ self.att_mtu = mtu
1883
+ self.emit(self.EVENT_ATT_MTU_UPDATE, mtu)
1884
+
1290
1885
  def flush_output(self) -> None:
1291
1886
  self.out_queue.clear()
1292
1887
  self.out_sdu = None
@@ -1364,7 +1959,7 @@ class LeCreditBasedChannel(utils.EventEmitter):
1364
1959
  return (
1365
1960
  f'CoC({self.source_cid}->{self.destination_cid}, '
1366
1961
  f'State={self.state.name}, '
1367
- f'PSM={self.le_psm}, '
1962
+ f'PSM={self.psm}, '
1368
1963
  f'MTU={self.mtu}/{self.peer_mtu}, '
1369
1964
  f'MPS={self.mps}/{self.peer_mps}, '
1370
1965
  f'credits={self.credits}/{self.peer_credits})'
@@ -1379,14 +1974,14 @@ class ClassicChannelServer(utils.EventEmitter):
1379
1974
  self,
1380
1975
  manager: ChannelManager,
1381
1976
  psm: int,
1382
- handler: Optional[Callable[[ClassicChannel], Any]],
1383
- mtu: int,
1977
+ handler: Callable[[ClassicChannel], Any] | None,
1978
+ spec: ClassicChannelSpec,
1384
1979
  ) -> None:
1385
1980
  super().__init__()
1386
1981
  self.manager = manager
1387
1982
  self.handler = handler
1388
1983
  self.psm = psm
1389
- self.mtu = mtu
1984
+ self.spec = spec
1390
1985
 
1391
1986
  def on_connection(self, channel: ClassicChannel) -> None:
1392
1987
  self.emit(self.EVENT_CONNECTION, channel)
@@ -1406,7 +2001,7 @@ class LeCreditBasedChannelServer(utils.EventEmitter):
1406
2001
  self,
1407
2002
  manager: ChannelManager,
1408
2003
  psm: int,
1409
- handler: Optional[Callable[[LeCreditBasedChannel], Any]],
2004
+ handler: Callable[[LeCreditBasedChannel], Any] | None,
1410
2005
  max_credits: int,
1411
2006
  mtu: int,
1412
2007
  mps: int,
@@ -1432,14 +2027,24 @@ class LeCreditBasedChannelServer(utils.EventEmitter):
1432
2027
  # -----------------------------------------------------------------------------
1433
2028
  class ChannelManager:
1434
2029
  identifiers: dict[int, int]
1435
- channels: dict[int, dict[int, Union[ClassicChannel, LeCreditBasedChannel]]]
2030
+ channels: dict[int, dict[int, ClassicChannel | LeCreditBasedChannel]]
1436
2031
  servers: dict[int, ClassicChannelServer]
1437
2032
  le_coc_channels: dict[int, dict[int, LeCreditBasedChannel]]
1438
2033
  le_coc_servers: dict[int, LeCreditBasedChannelServer]
1439
2034
  le_coc_requests: dict[int, L2CAP_LE_Credit_Based_Connection_Request]
1440
- fixed_channels: dict[int, Optional[Callable[[int, bytes], Any]]]
1441
- _host: Optional[Host]
1442
- connection_parameters_update_response: Optional[asyncio.Future[int]]
2035
+ fixed_channels: dict[int, Callable[[int, bytes], Any] | None]
2036
+ pending_credit_based_connections: dict[
2037
+ int,
2038
+ dict[
2039
+ int,
2040
+ tuple[
2041
+ asyncio.Future[None],
2042
+ list[LeCreditBasedChannel],
2043
+ ],
2044
+ ],
2045
+ ]
2046
+ _host: Host | None
2047
+ connection_parameters_update_response: asyncio.Future[int] | None
1443
2048
 
1444
2049
  def __init__(
1445
2050
  self,
@@ -1459,7 +2064,10 @@ class ChannelManager:
1459
2064
  ) # LE CoC channels, mapped by connection and destination cid
1460
2065
  self.le_coc_servers = {} # LE CoC - Servers accepting connections, by PSM
1461
2066
  self.le_coc_requests = {} # LE CoC connection requests, by identifier
1462
- self.extended_features = extended_features
2067
+ self.pending_credit_based_connections = (
2068
+ {}
2069
+ ) # Credit-based connection request contexts, by connection handle and identifier
2070
+ self.extended_features = set(extended_features)
1463
2071
  self.connectionless_mtu = connectionless_mtu
1464
2072
  self.connection_parameters_update_response = None
1465
2073
 
@@ -1501,18 +2109,26 @@ class ChannelManager:
1501
2109
 
1502
2110
  raise OutOfResourcesError('no free CID available')
1503
2111
 
1504
- @staticmethod
1505
- def find_free_le_cid(channels: Iterable[int]) -> int:
2112
+ @classmethod
2113
+ def find_free_le_cid(cls, channels: Iterable[int]) -> int | None:
2114
+ cids = cls.find_free_le_cids(channels, 1)
2115
+ return cids[0] if cids else None
2116
+
2117
+ @classmethod
2118
+ def find_free_le_cids(cls, channels: Iterable[int], count: int) -> list[int]:
1506
2119
  # Pick the smallest valid CID that's not already in the list
1507
2120
  # (not necessarily the most efficient algorithm, but the list of CID is
1508
2121
  # very small in practice)
2122
+ cids: list[int] = []
1509
2123
  for cid in range(
1510
2124
  L2CAP_LE_U_DYNAMIC_CID_RANGE_START, L2CAP_LE_U_DYNAMIC_CID_RANGE_END + 1
1511
2125
  ):
1512
2126
  if cid not in channels:
1513
- return cid
2127
+ cids.append(cid)
2128
+ if len(cids) == count:
2129
+ return cids
1514
2130
 
1515
- raise OutOfResourcesError('no free CID')
2131
+ return []
1516
2132
 
1517
2133
  def next_identifier(self, connection: Connection) -> int:
1518
2134
  identifier = (self.identifiers.setdefault(connection.handle, 0) + 1) % 256
@@ -1534,7 +2150,7 @@ class ChannelManager:
1534
2150
  def create_classic_server(
1535
2151
  self,
1536
2152
  spec: ClassicChannelSpec,
1537
- handler: Optional[Callable[[ClassicChannel], Any]] = None,
2153
+ handler: Callable[[ClassicChannel], Any] | None = None,
1538
2154
  ) -> ClassicChannelServer:
1539
2155
  if not spec.psm:
1540
2156
  # Find a free PSM
@@ -1563,14 +2179,14 @@ class ChannelManager:
1563
2179
  raise InvalidArgumentError('invalid PSM')
1564
2180
  check >>= 8
1565
2181
 
1566
- self.servers[spec.psm] = ClassicChannelServer(self, spec.psm, handler, spec.mtu)
2182
+ self.servers[spec.psm] = ClassicChannelServer(self, spec.psm, handler, spec)
1567
2183
 
1568
2184
  return self.servers[spec.psm]
1569
2185
 
1570
2186
  def create_le_credit_based_server(
1571
2187
  self,
1572
2188
  spec: LeCreditBasedChannelSpec,
1573
- handler: Optional[Callable[[LeCreditBasedChannel], Any]] = None,
2189
+ handler: Callable[[LeCreditBasedChannel], Any] | None = None,
1574
2190
  ) -> LeCreditBasedChannelServer:
1575
2191
  if not spec.psm:
1576
2192
  # Find a free PSM
@@ -1599,20 +2215,30 @@ class ChannelManager:
1599
2215
 
1600
2216
  return self.le_coc_servers[spec.psm]
1601
2217
 
1602
- def on_disconnection(self, connection_handle: int, _reason: int) -> None:
1603
- logger.debug(f'disconnection from {connection_handle}, cleaning up channels')
1604
- if connection_handle in self.channels:
1605
- for _, channel in self.channels[connection_handle].items():
2218
+ def on_disconnection(self, connection_handle: int, reason: int) -> None:
2219
+ del reason # unused.
2220
+ logger.debug('disconnection from %d, cleaning up channels', connection_handle)
2221
+ if channels := self.channels.pop(connection_handle, None):
2222
+ for channel in channels.values():
1606
2223
  channel.abort()
1607
- del self.channels[connection_handle]
1608
- if connection_handle in self.le_coc_channels:
1609
- for _, channel in self.le_coc_channels[connection_handle].items():
1610
- channel.abort()
1611
- del self.le_coc_channels[connection_handle]
1612
- if connection_handle in self.identifiers:
1613
- del self.identifiers[connection_handle]
2224
+ if le_coc_channels := self.le_coc_channels.pop(connection_handle, None):
2225
+ for le_coc_channel in le_coc_channels.values():
2226
+ le_coc_channel.abort()
2227
+ if pending_credit_based_connections := self.pending_credit_based_connections.pop(
2228
+ connection_handle, None
2229
+ ):
2230
+ for future, _ in pending_credit_based_connections.values():
2231
+ if not future.done():
2232
+ future.cancel("ACL disconnected")
2233
+ self.identifiers.pop(connection_handle, None)
1614
2234
 
1615
- def send_pdu(self, connection, cid: int, pdu: Union[SupportsBytes, bytes]) -> None:
2235
+ def send_pdu(
2236
+ self,
2237
+ connection: Connection,
2238
+ cid: int,
2239
+ pdu: SupportsBytes | bytes,
2240
+ with_fcs: bool = False,
2241
+ ) -> None:
1616
2242
  pdu_str = pdu.hex() if isinstance(pdu, bytes) else str(pdu)
1617
2243
  pdu_bytes = bytes(pdu)
1618
2244
  logger.debug(
@@ -1620,7 +2246,9 @@ class ChannelManager:
1620
2246
  f'on connection [0x{connection.handle:04X}] (CID={cid}) '
1621
2247
  f'{connection.peer_address}: {len(pdu_bytes)} bytes, {pdu_str}'
1622
2248
  )
1623
- self.host.send_l2cap_pdu(connection.handle, cid, pdu_bytes)
2249
+ self.host.send_acl_sdu(
2250
+ connection.handle, L2CAP_PDU(cid, bytes(pdu)).to_bytes(with_fcs=with_fcs)
2251
+ )
1624
2252
 
1625
2253
  def on_pdu(self, connection: Connection, cid: int, pdu: bytes) -> None:
1626
2254
  if cid in (L2CAP_SIGNALING_CID, L2CAP_LE_SIGNALING_CID):
@@ -1714,7 +2342,6 @@ class ChannelManager:
1714
2342
  identifier=request.identifier,
1715
2343
  destination_cid=request.source_cid,
1716
2344
  source_cid=0,
1717
- # pylint: disable=line-too-long
1718
2345
  result=L2CAP_Connection_Response.Result.CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE,
1719
2346
  status=0x0000,
1720
2347
  ),
@@ -1726,7 +2353,7 @@ class ChannelManager:
1726
2353
  f'creating server channel with cid={source_cid} for psm {request.psm}'
1727
2354
  )
1728
2355
  channel = ClassicChannel(
1729
- self, connection, cid, request.psm, source_cid, server.mtu
2356
+ self, connection, cid, request.psm, source_cid, server.spec
1730
2357
  )
1731
2358
  connection_channels[source_cid] = channel
1732
2359
 
@@ -1745,7 +2372,6 @@ class ChannelManager:
1745
2372
  identifier=request.identifier,
1746
2373
  destination_cid=request.source_cid,
1747
2374
  source_cid=0,
1748
- # pylint: disable=line-too-long
1749
2375
  result=L2CAP_Connection_Response.Result.CONNECTION_REFUSED_PSM_NOT_SUPPORTED,
1750
2376
  status=0x0000,
1751
2377
  ),
@@ -1974,106 +2600,100 @@ class ChannelManager:
1974
2600
  cid: int,
1975
2601
  request: L2CAP_LE_Credit_Based_Connection_Request,
1976
2602
  ) -> None:
1977
- if request.le_psm in self.le_coc_servers:
1978
- server = self.le_coc_servers[request.le_psm]
1979
-
1980
- # Check that the CID isn't already used
1981
- le_connection_channels = self.le_coc_channels.setdefault(
1982
- connection.handle, {}
1983
- )
1984
- if request.source_cid in le_connection_channels:
1985
- logger.warning(f'source CID {request.source_cid} already in use')
1986
- self.send_control_frame(
1987
- connection,
1988
- cid,
1989
- L2CAP_LE_Credit_Based_Connection_Response(
1990
- identifier=request.identifier,
1991
- destination_cid=0,
1992
- mtu=server.mtu,
1993
- mps=server.mps,
1994
- initial_credits=0,
1995
- # pylint: disable=line-too-long
1996
- result=L2CAP_LE_Credit_Based_Connection_Response.Result.CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED,
1997
- ),
1998
- )
1999
- return
2000
-
2001
- # Find a free CID for this new channel
2002
- connection_channels = self.channels.setdefault(connection.handle, {})
2003
- source_cid = self.find_free_le_cid(connection_channels)
2004
- if source_cid is None: # Should never happen!
2005
- self.send_control_frame(
2006
- connection,
2007
- cid,
2008
- L2CAP_LE_Credit_Based_Connection_Response(
2009
- identifier=request.identifier,
2010
- destination_cid=0,
2011
- mtu=server.mtu,
2012
- mps=server.mps,
2013
- initial_credits=0,
2014
- # pylint: disable=line-too-long
2015
- result=L2CAP_LE_Credit_Based_Connection_Response.Result.CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE,
2016
- ),
2017
- )
2018
- return
2019
-
2020
- # Create a new channel
2021
- logger.debug(
2022
- f'creating LE CoC server channel with cid={source_cid} for psm '
2023
- f'{request.le_psm}'
2603
+ if not (server := self.le_coc_servers.get(request.le_psm)):
2604
+ logger.info(
2605
+ f'No LE server for connection 0x{connection.handle:04X} '
2606
+ f'on PSM {request.le_psm}'
2024
2607
  )
2025
- channel = LeCreditBasedChannel(
2026
- self,
2608
+ self.send_control_frame(
2027
2609
  connection,
2028
- request.le_psm,
2029
- source_cid,
2030
- request.source_cid,
2031
- server.mtu,
2032
- server.mps,
2033
- request.initial_credits,
2034
- request.mtu,
2035
- request.mps,
2036
- server.max_credits,
2037
- True,
2610
+ cid,
2611
+ L2CAP_LE_Credit_Based_Connection_Response(
2612
+ identifier=request.identifier,
2613
+ destination_cid=0,
2614
+ mtu=L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU,
2615
+ mps=L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS,
2616
+ initial_credits=0,
2617
+ result=L2CAP_LE_Credit_Based_Connection_Response.Result.CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED,
2618
+ ),
2038
2619
  )
2039
- connection_channels[source_cid] = channel
2040
- le_connection_channels[request.source_cid] = channel
2620
+ return
2041
2621
 
2042
- # Respond
2622
+ # Check that the CID isn't already used
2623
+ le_connection_channels = self.le_coc_channels.setdefault(connection.handle, {})
2624
+ if request.source_cid in le_connection_channels:
2625
+ logger.warning(f'source CID {request.source_cid} already in use')
2043
2626
  self.send_control_frame(
2044
2627
  connection,
2045
2628
  cid,
2046
2629
  L2CAP_LE_Credit_Based_Connection_Response(
2047
2630
  identifier=request.identifier,
2048
- destination_cid=source_cid,
2631
+ destination_cid=0,
2049
2632
  mtu=server.mtu,
2050
2633
  mps=server.mps,
2051
- initial_credits=server.max_credits,
2052
- # pylint: disable=line-too-long
2053
- result=L2CAP_LE_Credit_Based_Connection_Response.Result.CONNECTION_SUCCESSFUL,
2634
+ initial_credits=0,
2635
+ result=L2CAP_LE_Credit_Based_Connection_Response.Result.CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED,
2054
2636
  ),
2055
2637
  )
2638
+ return
2056
2639
 
2057
- # Notify
2058
- server.on_connection(channel)
2059
- else:
2060
- logger.info(
2061
- f'No LE server for connection 0x{connection.handle:04X} '
2062
- f'on PSM {request.le_psm}'
2063
- )
2640
+ # Find a free CID for this new channel
2641
+ connection_channels = self.channels.setdefault(connection.handle, {})
2642
+ source_cid = self.find_free_le_cid(connection_channels)
2643
+ if source_cid is None: # Should never happen!
2064
2644
  self.send_control_frame(
2065
2645
  connection,
2066
2646
  cid,
2067
2647
  L2CAP_LE_Credit_Based_Connection_Response(
2068
2648
  identifier=request.identifier,
2069
2649
  destination_cid=0,
2070
- mtu=L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU,
2071
- mps=L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS,
2650
+ mtu=server.mtu,
2651
+ mps=server.mps,
2072
2652
  initial_credits=0,
2073
- # pylint: disable=line-too-long
2074
- result=L2CAP_LE_Credit_Based_Connection_Response.Result.CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED,
2653
+ result=L2CAP_LE_Credit_Based_Connection_Response.Result.CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE,
2075
2654
  ),
2076
2655
  )
2656
+ return
2657
+
2658
+ # Create a new channel
2659
+ logger.debug(
2660
+ f'creating LE CoC server channel with cid={source_cid} for psm '
2661
+ f'{request.le_psm}'
2662
+ )
2663
+ channel = LeCreditBasedChannel(
2664
+ self,
2665
+ connection,
2666
+ request.le_psm,
2667
+ source_cid,
2668
+ request.source_cid,
2669
+ server.mtu,
2670
+ server.mps,
2671
+ request.initial_credits,
2672
+ request.mtu,
2673
+ request.mps,
2674
+ server.max_credits,
2675
+ True,
2676
+ )
2677
+ connection_channels[source_cid] = channel
2678
+ le_connection_channels[request.source_cid] = channel
2679
+
2680
+ # Respond
2681
+ self.send_control_frame(
2682
+ connection,
2683
+ cid,
2684
+ L2CAP_LE_Credit_Based_Connection_Response(
2685
+ identifier=request.identifier,
2686
+ destination_cid=source_cid,
2687
+ mtu=server.mtu,
2688
+ mps=server.mps,
2689
+ initial_credits=server.max_credits,
2690
+ # pylint: disable=line-too-long
2691
+ result=L2CAP_LE_Credit_Based_Connection_Response.Result.CONNECTION_SUCCESSFUL,
2692
+ ),
2693
+ )
2694
+
2695
+ # Notify
2696
+ server.on_connection(channel)
2077
2697
 
2078
2698
  def on_l2cap_le_credit_based_connection_response(
2079
2699
  self,
@@ -2082,11 +2702,9 @@ class ChannelManager:
2082
2702
  response: L2CAP_LE_Credit_Based_Connection_Response,
2083
2703
  ) -> None:
2084
2704
  # Find the pending request by identifier
2085
- request = self.le_coc_requests.get(response.identifier)
2086
- if request is None:
2705
+ if not (request := self.le_coc_requests.pop(response.identifier, None)):
2087
2706
  logger.warning(color('!!! received response for unknown request', 'red'))
2088
2707
  return
2089
- del self.le_coc_requests[response.identifier]
2090
2708
 
2091
2709
  # Find the channel for this request
2092
2710
  channel = self.find_channel(connection.handle, request.source_cid)
@@ -2103,6 +2721,147 @@ class ChannelManager:
2103
2721
  # Process the response
2104
2722
  channel.on_connection_response(response)
2105
2723
 
2724
+ def on_l2cap_credit_based_connection_request(
2725
+ self,
2726
+ connection: Connection,
2727
+ cid: int,
2728
+ request: L2CAP_Credit_Based_Connection_Request,
2729
+ ) -> None:
2730
+ if not (server := self.le_coc_servers.get(request.spsm)):
2731
+ logger.info(
2732
+ 'No LE server for connection 0x%04X ' 'on PSM %d',
2733
+ connection.handle,
2734
+ request.spsm,
2735
+ )
2736
+ self.send_control_frame(
2737
+ connection,
2738
+ cid,
2739
+ L2CAP_Credit_Based_Connection_Response(
2740
+ identifier=request.identifier,
2741
+ destination_cid=[],
2742
+ mtu=L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU,
2743
+ mps=L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS,
2744
+ initial_credits=0,
2745
+ result=L2CAP_Credit_Based_Connection_Response.Result.ALL_CONNECTIONS_REFUSED_SPSM_NOT_SUPPORTED,
2746
+ ),
2747
+ )
2748
+ return
2749
+
2750
+ # Check that the CID isn't already used
2751
+ le_connection_channels = self.le_coc_channels.setdefault(connection.handle, {})
2752
+ if cid_in_use := set(request.source_cid).intersection(
2753
+ set(le_connection_channels)
2754
+ ):
2755
+ logger.warning('source CID already in use: %s', cid_in_use)
2756
+ self.send_control_frame(
2757
+ connection,
2758
+ cid,
2759
+ L2CAP_Credit_Based_Connection_Response(
2760
+ identifier=request.identifier,
2761
+ mtu=server.mtu,
2762
+ mps=server.mps,
2763
+ initial_credits=0,
2764
+ result=L2CAP_Credit_Based_Connection_Response.Result.SOME_CONNECTIONS_REFUSED_SOURCE_CID_ALREADY_ALLOCATED,
2765
+ destination_cid=[],
2766
+ ),
2767
+ )
2768
+ return
2769
+
2770
+ # Find free CIDs for new channels
2771
+ connection_channels = self.channels.setdefault(connection.handle, {})
2772
+ source_cids = self.find_free_le_cids(
2773
+ connection_channels, len(request.source_cid)
2774
+ )
2775
+ if not source_cids:
2776
+ self.send_control_frame(
2777
+ connection,
2778
+ cid,
2779
+ L2CAP_Credit_Based_Connection_Response(
2780
+ identifier=request.identifier,
2781
+ destination_cid=[],
2782
+ mtu=server.mtu,
2783
+ mps=server.mps,
2784
+ initial_credits=server.max_credits,
2785
+ result=L2CAP_Credit_Based_Connection_Response.Result.SOME_CONNECTIONS_REFUSED_INSUFFICIENT_RESOURCES_AVAILABLE,
2786
+ ),
2787
+ )
2788
+ return
2789
+
2790
+ for destination_cid in request.source_cid:
2791
+ # TODO: Handle Classic channels.
2792
+ if not (source_cid := self.find_free_le_cid(connection_channels)):
2793
+ logger.warning("No free CIDs available")
2794
+ break
2795
+ # Create a new channel
2796
+ logger.debug(
2797
+ 'creating LE CoC server channel with cid=%s for psm %s',
2798
+ source_cid,
2799
+ request.spsm,
2800
+ )
2801
+ channel = LeCreditBasedChannel(
2802
+ self,
2803
+ connection,
2804
+ request.spsm,
2805
+ source_cid,
2806
+ destination_cid,
2807
+ server.mtu,
2808
+ server.mps,
2809
+ request.initial_credits,
2810
+ request.mtu,
2811
+ request.mps,
2812
+ server.max_credits,
2813
+ True,
2814
+ )
2815
+ connection_channels[source_cid] = channel
2816
+ le_connection_channels[source_cid] = channel
2817
+ server.on_connection(channel)
2818
+
2819
+ # Respond
2820
+ self.send_control_frame(
2821
+ connection,
2822
+ cid,
2823
+ L2CAP_Credit_Based_Connection_Response(
2824
+ identifier=request.identifier,
2825
+ destination_cid=source_cids,
2826
+ mtu=server.mtu,
2827
+ mps=server.mps,
2828
+ initial_credits=server.max_credits,
2829
+ result=L2CAP_Credit_Based_Connection_Response.Result.ALL_CONNECTIONS_SUCCESSFUL,
2830
+ ),
2831
+ )
2832
+
2833
+ def on_l2cap_credit_based_connection_response(
2834
+ self,
2835
+ connection: Connection,
2836
+ _cid: int,
2837
+ response: L2CAP_Credit_Based_Connection_Response,
2838
+ ) -> None:
2839
+ # Find the pending request by identifier
2840
+ pending_connections = self.pending_credit_based_connections.setdefault(
2841
+ connection.handle, {}
2842
+ )
2843
+ if not (
2844
+ pending_connection := pending_connections.pop(response.identifier, None)
2845
+ ):
2846
+ logger.warning(color('!!! received response for unknown request', 'red'))
2847
+ return
2848
+
2849
+ connection_result, channels = pending_connection
2850
+
2851
+ # Process the response
2852
+ for channel, destination_cid in zip(channels, response.destination_cid):
2853
+ channel.on_enhanced_connection_response(destination_cid, response)
2854
+
2855
+ if (
2856
+ response.result
2857
+ == L2CAP_Credit_Based_Connection_Response.Result.ALL_CONNECTIONS_SUCCESSFUL
2858
+ ):
2859
+ connection_result.set_result(None)
2860
+ else:
2861
+ connection_result.set_exception(
2862
+ L2capError(response.result, response.result.name)
2863
+ )
2864
+
2106
2865
  def on_l2cap_le_flow_control_credit(
2107
2866
  self, connection: Connection, _cid: int, credit: L2CAP_LE_Flow_Control_Credit
2108
2867
  ) -> None:
@@ -2138,7 +2897,7 @@ class ChannelManager:
2138
2897
  channel = LeCreditBasedChannel(
2139
2898
  manager=self,
2140
2899
  connection=connection,
2141
- le_psm=spec.psm,
2900
+ psm=spec.psm,
2142
2901
  source_cid=source_cid,
2143
2902
  destination_cid=0,
2144
2903
  mtu=spec.mtu,
@@ -2184,12 +2943,12 @@ class ChannelManager:
2184
2943
  f'creating client channel with cid={source_cid} for psm {spec.psm}'
2185
2944
  )
2186
2945
  channel = ClassicChannel(
2187
- self,
2188
- connection,
2189
- L2CAP_SIGNALING_CID,
2190
- spec.psm,
2191
- source_cid,
2192
- spec.mtu,
2946
+ manager=self,
2947
+ connection=connection,
2948
+ signaling_cid=L2CAP_SIGNALING_CID,
2949
+ psm=spec.psm,
2950
+ source_cid=source_cid,
2951
+ spec=spec,
2193
2952
  )
2194
2953
  connection_channels[source_cid] = channel
2195
2954
 
@@ -2197,7 +2956,100 @@ class ChannelManager:
2197
2956
  try:
2198
2957
  await channel.connect()
2199
2958
  except BaseException as e:
2200
- del connection_channels[source_cid]
2959
+ connection_channels.pop(source_cid, None)
2201
2960
  raise e
2202
2961
 
2203
2962
  return channel
2963
+
2964
+ async def create_enhanced_credit_based_channels(
2965
+ self,
2966
+ connection: Connection,
2967
+ spec: LeCreditBasedChannelSpec,
2968
+ count: int,
2969
+ ) -> list[LeCreditBasedChannel]:
2970
+ # Find a free CID for the new channel
2971
+ connection_channels = self.channels.setdefault(connection.handle, {})
2972
+ source_cids = self.find_free_le_cids(connection_channels, count)
2973
+ if not source_cids: # Should never happen!
2974
+ raise OutOfResourcesError('all CIDs already in use')
2975
+
2976
+ if spec.psm is None:
2977
+ raise InvalidArgumentError('PSM cannot be None')
2978
+
2979
+ # Create the channel
2980
+ logger.debug(
2981
+ 'creating coc channel with cid=%s for psm %s', source_cids, spec.psm
2982
+ )
2983
+ channels: list[LeCreditBasedChannel] = []
2984
+ for source_cid in source_cids:
2985
+ channel = LeCreditBasedChannel(
2986
+ manager=self,
2987
+ connection=connection,
2988
+ psm=spec.psm,
2989
+ source_cid=source_cid,
2990
+ destination_cid=0,
2991
+ mtu=spec.mtu,
2992
+ mps=spec.mps,
2993
+ credits=0,
2994
+ peer_mtu=0,
2995
+ peer_mps=0,
2996
+ peer_credits=spec.max_credits,
2997
+ connected=False,
2998
+ )
2999
+ connection_channels[source_cid] = channel
3000
+ channels.append(channel)
3001
+
3002
+ identifier = self.next_identifier(connection)
3003
+ request = L2CAP_Credit_Based_Connection_Request(
3004
+ identifier=identifier,
3005
+ spsm=spec.psm,
3006
+ mtu=spec.mtu,
3007
+ mps=spec.mps,
3008
+ initial_credits=spec.max_credits,
3009
+ source_cid=source_cids,
3010
+ )
3011
+ connection_result = asyncio.get_running_loop().create_future()
3012
+ pending_connections = self.pending_credit_based_connections.setdefault(
3013
+ connection.handle, {}
3014
+ )
3015
+ pending_connections[identifier] = (connection_result, channels)
3016
+ self.send_control_frame(
3017
+ connection,
3018
+ L2CAP_LE_SIGNALING_CID,
3019
+ request,
3020
+ )
3021
+ # Connect
3022
+ try:
3023
+ await connection_result
3024
+ except Exception:
3025
+ logger.exception('connection failed')
3026
+ for cid in source_cids:
3027
+ del connection_channels[cid]
3028
+ raise
3029
+
3030
+ # Remember the channel by source CID and destination CID
3031
+ le_connection_channels = self.le_coc_channels.setdefault(connection.handle, {})
3032
+ for channel in channels:
3033
+ le_connection_channels[channel.destination_cid] = channel
3034
+
3035
+ return channels
3036
+
3037
+ @classmethod
3038
+ def make_mode_processor(
3039
+ self,
3040
+ channel: ClassicChannel,
3041
+ mode: TransmissionMode,
3042
+ peer_tx_window_size: int,
3043
+ peer_max_retransmission: int,
3044
+ peer_retransmission_timeout: int,
3045
+ peer_monitor_timeout: int,
3046
+ peer_mps: int,
3047
+ ) -> Processor:
3048
+ del peer_retransmission_timeout, peer_monitor_timeout # Unused.
3049
+ if mode == TransmissionMode.BASIC:
3050
+ return Processor(channel)
3051
+ elif mode == TransmissionMode.ENHANCED_RETRANSMISSION:
3052
+ return EnhancedRetransmissionProcessor(
3053
+ channel, peer_tx_window_size, peer_max_retransmission, peer_mps
3054
+ )
3055
+ raise InvalidArgumentError("Mode %s is not implemented", mode.name)