bumble 0.0.219__py3-none-any.whl → 0.0.221__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 (104) hide show
  1. bumble/_version.py +2 -2
  2. bumble/a2dp.py +5 -5
  3. bumble/apps/auracast.py +746 -479
  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 +8 -6
  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 +1201 -643
  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 +278 -325
  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 +1015 -218
  47. bumble/link.py +54 -284
  48. bumble/ll.py +200 -0
  49. bumble/lmp.py +324 -0
  50. bumble/pairing.py +14 -15
  51. bumble/pandora/__init__.py +2 -2
  52. bumble/pandora/device.py +6 -4
  53. bumble/pandora/host.py +19 -10
  54. bumble/pandora/l2cap.py +8 -9
  55. bumble/pandora/security.py +18 -16
  56. bumble/pandora/utils.py +4 -4
  57. bumble/profiles/aics.py +6 -8
  58. bumble/profiles/ams.py +3 -5
  59. bumble/profiles/ancs.py +11 -11
  60. bumble/profiles/ascs.py +5 -5
  61. bumble/profiles/asha.py +10 -9
  62. bumble/profiles/bass.py +9 -3
  63. bumble/profiles/battery_service.py +1 -2
  64. bumble/profiles/csip.py +9 -10
  65. bumble/profiles/device_information_service.py +16 -17
  66. bumble/profiles/gap.py +3 -4
  67. bumble/profiles/gatt_service.py +0 -1
  68. bumble/profiles/gmap.py +12 -13
  69. bumble/profiles/hap.py +3 -3
  70. bumble/profiles/heart_rate_service.py +7 -8
  71. bumble/profiles/le_audio.py +1 -1
  72. bumble/profiles/mcp.py +28 -28
  73. bumble/profiles/pacs.py +13 -17
  74. bumble/profiles/pbp.py +16 -0
  75. bumble/profiles/vcs.py +2 -2
  76. bumble/profiles/vocs.py +6 -9
  77. bumble/rfcomm.py +19 -18
  78. bumble/sdp.py +12 -11
  79. bumble/smp.py +20 -30
  80. bumble/snoop.py +12 -5
  81. bumble/tools/generate_company_id_list.py +1 -1
  82. bumble/tools/intel_util.py +2 -2
  83. bumble/tools/rtk_fw_download.py +1 -1
  84. bumble/tools/rtk_util.py +1 -1
  85. bumble/transport/__init__.py +1 -2
  86. bumble/transport/android_emulator.py +2 -3
  87. bumble/transport/android_netsim.py +49 -40
  88. bumble/transport/common.py +9 -9
  89. bumble/transport/file.py +1 -2
  90. bumble/transport/hci_socket.py +2 -3
  91. bumble/transport/pty.py +3 -5
  92. bumble/transport/pyusb.py +8 -5
  93. bumble/transport/serial.py +1 -2
  94. bumble/transport/vhci.py +1 -2
  95. bumble/transport/ws_server.py +2 -3
  96. bumble/utils.py +23 -14
  97. bumble/vendor/android/hci.py +4 -2
  98. {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/METADATA +4 -3
  99. bumble-0.0.221.dist-info/RECORD +185 -0
  100. bumble-0.0.219.dist-info/RECORD +0 -183
  101. {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/WHEEL +0 -0
  102. {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/entry_points.txt +0 -0
  103. {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/licenses/LICENSE +0 -0
  104. {bumble-0.0.219.dist-info → bumble-0.0.221.dist-info}/top_level.txt +0 -0
bumble/l2cap.py CHANGED
@@ -23,18 +23,10 @@ import enum
23
23
  import logging
24
24
  import struct
25
25
  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
- )
26
+ from collections.abc import Callable, Iterable, Sequence
27
+ from typing import TYPE_CHECKING, Any, ClassVar, SupportsBytes, TypeVar
28
+
29
+ from typing_extensions import override
38
30
 
39
31
  from bumble import hci, utils
40
32
  from bumble.colors import color
@@ -69,7 +61,12 @@ L2CAP_MIN_LE_MTU = 23
69
61
  L2CAP_MIN_BR_EDR_MTU = 48
70
62
  L2CAP_MAX_BR_EDR_MTU = 65535
71
63
 
72
- L2CAP_DEFAULT_MTU = 2048 # Default value for the MTU we are willing to accept
64
+ L2CAP_DEFAULT_MTU = 2048 # Default value for the MTU we are willing to accept
65
+ L2CAP_DEFAULT_MPS = 1010 # Default value for the MPS we are willing to accept
66
+ DEFAULT_TX_WINDOW_SIZE = 63
67
+ DEFAULT_MAX_RETRANSMISSION = 1
68
+ DEFAULT_RETRANSMISSION_TIMEOUT = 2.0
69
+ DEFAULT_MONITOR_TIMEOUT = 12.0
73
70
 
74
71
  L2CAP_DEFAULT_CONNECTIONLESS_MTU = 1024
75
72
 
@@ -133,29 +130,65 @@ L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU = 2048
133
130
  L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS = 2048
134
131
  L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_INITIAL_CREDITS = 256
135
132
 
136
- L2CAP_MAXIMUM_TRANSMISSION_UNIT_CONFIGURATION_OPTION_TYPE = 0x01
137
-
138
- L2CAP_MTU_CONFIGURATION_PARAMETER_TYPE = 0x01
139
-
140
133
  # fmt: on
141
134
  # pylint: enable=line-too-long
142
135
 
143
136
 
137
+ class TransmissionMode(utils.OpenIntEnum):
138
+ '''See Bluetooth spec @ Vol 3, Part A - 5.4. Retransmission and Flow Control option'''
139
+
140
+ BASIC = 0x00
141
+ RETRANSMISSION = 0x01
142
+ FLOW_CONTROL = 0x02
143
+ ENHANCED_RETRANSMISSION = 0x03
144
+ STREAMING = 0x04
145
+
146
+
144
147
  # -----------------------------------------------------------------------------
145
148
  # Classes
146
149
  # -----------------------------------------------------------------------------
147
150
  # pylint: disable=invalid-name
148
151
 
149
152
 
153
+ class L2capError(ProtocolError):
154
+ def __init__(self, error_code, error_name='', details=''):
155
+ super().__init__(error_code, 'L2CAP', error_name, details)
156
+
157
+
150
158
  @dataclasses.dataclass
151
159
  class ClassicChannelSpec:
152
- psm: Optional[int] = None
160
+ '''Spec of L2CAP Channel over Classic Transport.
161
+
162
+ Attributes:
163
+ psm: PSM of channel. This is optional for server, and when it is None, a PSM
164
+ will be allocated.
165
+ mtu: Maximum Transmission Unit.
166
+ mps: Maximum PDU payload Size.
167
+ tx_window_size: The size of the transmission window for Flow Control mode,
168
+ Retransmission mode, and Enhanced Retransmission mode.
169
+ max_retransmission: The number of transmissions of a single I-frame that L2CAP
170
+ is allowed to try in Retransmission mode and Enhanced Retransmission mode.
171
+ retransmission_timeout: The timeout of retransmission in seconds.
172
+ monitor_timeout: The interval at which S-frames should be transmitted on the
173
+ return channel when no frames are received on the forward channel.
174
+ mode: The transmission mode to use.
175
+ fcs_enabled: Whether to enable FCS (Frame Check Sequence).
176
+ '''
177
+
178
+ psm: int | None = None
153
179
  mtu: int = L2CAP_DEFAULT_MTU
180
+ mps: int = L2CAP_DEFAULT_MPS
181
+ tx_window_size: int = DEFAULT_TX_WINDOW_SIZE
182
+ max_retransmission: int = DEFAULT_MAX_RETRANSMISSION
183
+ retransmission_timeout: float = DEFAULT_RETRANSMISSION_TIMEOUT
184
+ monitor_timeout: float = DEFAULT_MONITOR_TIMEOUT
185
+ mode: TransmissionMode = TransmissionMode.BASIC
186
+ fcs_enabled: bool = False
154
187
 
155
188
 
156
189
  @dataclasses.dataclass
157
190
  class LeCreditBasedChannelSpec:
158
- psm: Optional[int] = None
191
+ psm: int | None = None
159
192
  mtu: int = L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU
160
193
  mps: int = L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS
161
194
  max_credits: int = L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_INITIAL_CREDITS
@@ -183,20 +216,29 @@ class L2CAP_PDU:
183
216
  See Bluetooth spec @ Vol 3, Part A - 3 DATA PACKET FORMAT
184
217
  '''
185
218
 
186
- @staticmethod
187
- def from_bytes(data: bytes) -> L2CAP_PDU:
219
+ @classmethod
220
+ def from_bytes(cls, data: bytes) -> L2CAP_PDU:
188
221
  # Check parameters
189
222
  if len(data) < 4:
190
223
  raise InvalidPacketError('not enough data for L2CAP header')
191
224
 
192
- _, l2cap_pdu_cid = struct.unpack_from('<HH', data, 0)
193
- l2cap_pdu_payload = data[4:]
225
+ length, l2cap_pdu_cid = struct.unpack_from('<HH', data, 0)
226
+ l2cap_pdu_payload = data[4 : 4 + length]
194
227
 
195
- return L2CAP_PDU(l2cap_pdu_cid, l2cap_pdu_payload)
228
+ return cls(l2cap_pdu_cid, l2cap_pdu_payload)
196
229
 
197
230
  def __bytes__(self) -> bytes:
198
- header = struct.pack('<HH', len(self.payload), self.cid)
199
- return header + self.payload
231
+ return self.to_bytes(with_fcs=False)
232
+
233
+ def to_bytes(self, with_fcs: bool = False) -> bytes:
234
+ length = len(self.payload)
235
+ if with_fcs:
236
+ length += 2
237
+ header = struct.pack('<HH', length, self.cid)
238
+ body = header + self.payload
239
+ if with_fcs:
240
+ body += struct.pack('<H', utils.crc_16(body))
241
+ return body
200
242
 
201
243
  def __init__(self, cid: int, payload: bytes) -> None:
202
244
  self.cid = cid
@@ -206,6 +248,119 @@ class L2CAP_PDU:
206
248
  return f'{color("L2CAP", "green")} [CID={self.cid}]: {self.payload.hex()}'
207
249
 
208
250
 
251
+ class ControlField:
252
+ '''
253
+ See Bluetooth spec @ Vol 3, Part A - 3.3.2 Control field.
254
+ '''
255
+
256
+ class FieldType(utils.OpenIntEnum):
257
+ I_FRAME = 0x00
258
+ S_FRAME = 0x01
259
+
260
+ class SegmentationAndReassembly(utils.OpenIntEnum):
261
+ UNSEGMENTED = 0x00
262
+ START = 0x01
263
+ END = 0x02
264
+ CONTINUATION = 0x03
265
+
266
+ class SupervisoryFunction(utils.OpenIntEnum):
267
+ # Receiver Ready
268
+ RR = 0
269
+ # Reject
270
+ REJ = 1
271
+ # Receiver Not Ready
272
+ RNR = 2
273
+ # Select Reject
274
+ SREJ = 3
275
+
276
+ class RetransmissionBit(utils.OpenIntEnum):
277
+ NORMAL = 0x00
278
+ RETRANSMISSION = 0x01
279
+
280
+ req_seq: int
281
+ frame_type: ClassVar[FieldType]
282
+
283
+ def __bytes__(self) -> bytes:
284
+ raise NotImplementedError()
285
+
286
+
287
+ class EnhancedControlField(ControlField):
288
+ """Base control field used in Enhanced Retransmission and Streaming Mode."""
289
+
290
+ final: int
291
+
292
+ @classmethod
293
+ def from_bytes(cls, data: bytes) -> EnhancedControlField:
294
+ frame_type = data[0] & 0x01
295
+ if frame_type == cls.FieldType.I_FRAME:
296
+ return InformationEnhancedControlField.from_bytes(data)
297
+ elif frame_type == cls.FieldType.S_FRAME:
298
+ return SupervisoryEnhancedControlField.from_bytes(data)
299
+ else:
300
+ raise InvalidArgumentError(f'Invalid frame type: {frame_type}')
301
+
302
+
303
+ @dataclasses.dataclass
304
+ class InformationEnhancedControlField(EnhancedControlField):
305
+ tx_seq: int = 0
306
+ req_seq: int = 0
307
+ segmentation_and_reassembly: int = (
308
+ EnhancedControlField.SegmentationAndReassembly.UNSEGMENTED
309
+ )
310
+ final: int = 1
311
+
312
+ frame_type = EnhancedControlField.FieldType.I_FRAME
313
+
314
+ @classmethod
315
+ def from_bytes(cls, data: bytes) -> EnhancedControlField:
316
+ return cls(
317
+ tx_seq=(data[0] >> 1) & 0b0111111,
318
+ final=(data[0] >> 7) & 0b1,
319
+ req_seq=(data[1] & 0b001111111),
320
+ segmentation_and_reassembly=(data[1] >> 6) & 0b11,
321
+ )
322
+
323
+ def __bytes__(self) -> bytes:
324
+ return bytes(
325
+ [
326
+ self.frame_type | (self.tx_seq << 1) | (self.final << 7),
327
+ self.req_seq | (self.segmentation_and_reassembly << 6),
328
+ ]
329
+ )
330
+
331
+
332
+ @dataclasses.dataclass
333
+ class SupervisoryEnhancedControlField(EnhancedControlField):
334
+ supervision_function: int = ControlField.SupervisoryFunction.RR
335
+ poll: int = 0
336
+ req_seq: int = 0
337
+ final: int = 0
338
+
339
+ frame_type = EnhancedControlField.FieldType.S_FRAME
340
+
341
+ @classmethod
342
+ def from_bytes(cls, data: bytes) -> EnhancedControlField:
343
+ return cls(
344
+ supervision_function=(data[0] >> 2) & 0b11,
345
+ poll=(data[0] >> 4) & 0b1,
346
+ final=(data[0] >> 7) & 0b1,
347
+ req_seq=(data[1] & 0b1111111),
348
+ )
349
+
350
+ def __bytes__(self) -> bytes:
351
+ return bytes(
352
+ [
353
+ (
354
+ self.frame_type
355
+ | (self.supervision_function << 2)
356
+ | self.poll << 7
357
+ | (self.final << 7)
358
+ ),
359
+ self.req_seq,
360
+ ]
361
+ )
362
+
363
+
209
364
  # -----------------------------------------------------------------------------
210
365
  @dataclasses.dataclass
211
366
  class L2CAP_Control_Frame:
@@ -217,7 +372,7 @@ class L2CAP_Control_Frame:
217
372
  fields: ClassVar[hci.Fields] = ()
218
373
  code: int = dataclasses.field(default=0, init=False)
219
374
  name: str = dataclasses.field(default='', init=False)
220
- _payload: Optional[bytes] = dataclasses.field(default=None, init=False)
375
+ _payload: bytes | None = dataclasses.field(default=None, init=False)
221
376
 
222
377
  identifier: int
223
378
 
@@ -248,14 +403,16 @@ class L2CAP_Control_Frame:
248
403
  return frame
249
404
 
250
405
  @staticmethod
251
- def decode_configuration_options(data: bytes) -> list[tuple[int, bytes]]:
406
+ def decode_configuration_options(
407
+ data: bytes,
408
+ ) -> list[tuple[L2CAP_Configure_Request.ParameterType, bytes]]:
252
409
  options = []
253
410
  while len(data) >= 2:
254
411
  value_type = data[0]
255
412
  length = data[1]
256
413
  value = data[2 : 2 + length]
257
414
  data = data[2 + length :]
258
- options.append((value_type, value))
415
+ options.append((L2CAP_Configure_Request.ParameterType(value_type), value))
259
416
 
260
417
  return options
261
418
 
@@ -398,6 +555,15 @@ class L2CAP_Configure_Request(L2CAP_Control_Frame):
398
555
  See Bluetooth spec @ Vol 3, Part A - 4.4 CONFIGURATION REQUEST
399
556
  '''
400
557
 
558
+ class ParameterType(utils.OpenIntEnum):
559
+ MTU = 0x01
560
+ FLUSH_TIMEOUT = 0x02
561
+ QOS = 0x03
562
+ RETRANSMISSION_AND_FLOW_CONTROL = 0x04
563
+ FCS = 0x05
564
+ EXTENDED_FLOW_SPEC = 0x06
565
+ EXTENDED_WINDOW_SIZE = 0x07
566
+
401
567
  destination_cid: int = dataclasses.field(metadata=hci.metadata(2))
402
568
  flags: int = dataclasses.field(metadata=hci.metadata(2))
403
569
  options: bytes = dataclasses.field(metadata=hci.metadata('*'))
@@ -484,17 +650,18 @@ class L2CAP_Information_Request(L2CAP_Control_Frame):
484
650
  EXTENDED_FEATURES_SUPPORTED = 0x0002
485
651
  FIXED_CHANNELS_SUPPORTED = 0x0003
486
652
 
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
653
+ class ExtendedFeatures(hci.SpecableFlag):
654
+ FLOW_MODE_CONTROL = 0x0001
655
+ RETRANSMISSION_MODE = 0x0002
656
+ BIDIRECTIONAL_QOS = 0x0004
657
+ ENHANCED_RETRANSMISSION_MODE = 0x0008
658
+ STREAMING_MODE = 0x0010
659
+ FCS_OPTION = 0x0020
660
+ EXTENDED_FLOW_SPEC = 0x0040
661
+ FIXED_CHANNELS = 0x0080
662
+ EXTENDED_WINDOW_SIZE = 0x0100
663
+ UNICAST_CONNECTIONLESS_DATA = 0x0200
664
+ ENHANCED_CREDIT_BASE_FLOW_CONTROL = 0x0400
498
665
 
499
666
  info_type: int = dataclasses.field(metadata=InfoType.type_metadata(2))
500
667
 
@@ -661,7 +828,7 @@ class L2CAP_Credit_Based_Connection_Response(L2CAP_Control_Frame):
661
828
  mtu: int = dataclasses.field(metadata=hci.metadata(2))
662
829
  mps: int = dataclasses.field(metadata=hci.metadata(2))
663
830
  initial_credits: int = dataclasses.field(metadata=hci.metadata(2))
664
- result: int = dataclasses.field(metadata=Result.type_metadata(2))
831
+ result: Result = dataclasses.field(metadata=Result.type_metadata(2))
665
832
  destination_cid: Sequence[int] = dataclasses.field(
666
833
  metadata=L2CAP_Credit_Based_Connection_Request.CID_METADATA
667
834
  )
@@ -702,6 +869,217 @@ class L2CAP_Credit_Based_Reconfigure_Response(L2CAP_Control_Frame):
702
869
  result: int = dataclasses.field(metadata=Result.type_metadata(2))
703
870
 
704
871
 
872
+ # -----------------------------------------------------------------------------
873
+ class Processor:
874
+ def __init__(self, channel: ClassicChannel) -> None:
875
+ self.channel = channel
876
+
877
+ def send_sdu(self, sdu: bytes) -> None:
878
+ self.channel.send_pdu(sdu)
879
+
880
+ def on_pdu(self, pdu: bytes) -> None:
881
+ self.channel.on_sdu(pdu)
882
+
883
+
884
+ # TODO: Handle retransmission
885
+ class EnhancedRetransmissionProcessor(Processor):
886
+ MAX_SEQ_NUM = 64
887
+
888
+ @dataclasses.dataclass
889
+ class _PendingPdu:
890
+ payload: bytes
891
+ tx_seq: int
892
+ req_seq: int = 0
893
+
894
+ def __bytes__(self) -> bytes:
895
+ return (
896
+ bytes(
897
+ InformationEnhancedControlField(
898
+ tx_seq=self.tx_seq, req_seq=self.req_seq
899
+ )
900
+ )
901
+ + self.payload
902
+ )
903
+
904
+ _expected_ack_seq: int = 0
905
+ _next_tx_seq: int = 0
906
+ _last_tx_seq: int = 0
907
+ _req_seq_num: int = 0
908
+ _next_seq_num: int = 0
909
+ _remote_is_busy: bool = False
910
+
911
+ _num_receiver_ready_polls_sent: int = 0
912
+ _pending_pdus: list[_PendingPdu]
913
+ _monitor_handle: asyncio.TimerHandle | None = None
914
+ _receiver_ready_poll_handle: asyncio.TimerHandle | None = None
915
+
916
+ # Timeout, in seconds.
917
+ monitor_timeout: float
918
+ retransmission_timeout: float
919
+
920
+ @classmethod
921
+ def _num_frames_between(cls, low: int, high: int) -> int:
922
+ if high < low:
923
+ high += cls.MAX_SEQ_NUM
924
+ return high - low
925
+
926
+ def __init__(
927
+ self,
928
+ channel: ClassicChannel,
929
+ peer_tx_window_size: int = DEFAULT_TX_WINDOW_SIZE,
930
+ peer_max_retransmission: int = DEFAULT_MAX_RETRANSMISSION,
931
+ peer_mps: int = L2CAP_DEFAULT_MPS,
932
+ ):
933
+ spec = channel.spec
934
+ self.mps = spec.mps
935
+ self.peer_mps = peer_mps
936
+ self.peer_tx_window_size = peer_tx_window_size
937
+ self._pending_pdus = []
938
+ self.monitor_timeout = spec.monitor_timeout
939
+ self.channel = channel
940
+ self.retransmission_timeout = spec.retransmission_timeout
941
+ self.peer_max_retransmission = peer_max_retransmission
942
+
943
+ def _monitor(self) -> None:
944
+ if (
945
+ self.peer_max_retransmission <= 0
946
+ or self._num_receiver_ready_polls_sent < self.peer_max_retransmission
947
+ ):
948
+ self._send_receiver_ready_poll()
949
+ self._start_monitor()
950
+ else:
951
+ logger.error("Max retransmission exceeded")
952
+
953
+ def _receiver_ready_poll(self) -> None:
954
+ self._send_receiver_ready_poll()
955
+ self._start_monitor()
956
+
957
+ def _start_monitor(self) -> None:
958
+ if self._monitor_handle:
959
+ self._monitor_handle.cancel()
960
+ self._monitor_handle = asyncio.get_running_loop().call_later(
961
+ self.monitor_timeout, self._monitor
962
+ )
963
+
964
+ def _start_receiver_ready_poll(self) -> None:
965
+ if self._receiver_ready_poll_handle:
966
+ self._receiver_ready_poll_handle.cancel()
967
+ self._num_receiver_ready_polls_sent = 0
968
+
969
+ self._receiver_ready_poll_handle = asyncio.get_running_loop().call_later(
970
+ self.retransmission_timeout, self._receiver_ready_poll
971
+ )
972
+
973
+ def _send_receiver_ready_poll(self) -> None:
974
+ self._num_receiver_ready_polls_sent += 1
975
+ self.channel.send_pdu(
976
+ SupervisoryEnhancedControlField(
977
+ supervision_function=SupervisoryEnhancedControlField.SupervisoryFunction.RR,
978
+ final=1,
979
+ req_seq=self._next_seq_num,
980
+ )
981
+ )
982
+
983
+ def _get_next_tx_seq(self) -> int:
984
+ seq_num = self._next_tx_seq
985
+ self._next_tx_seq = (self._next_tx_seq + 1) % self.MAX_SEQ_NUM
986
+ return seq_num
987
+
988
+ @override
989
+ def send_sdu(self, sdu: bytes) -> None:
990
+ if len(sdu) > self.peer_mps:
991
+ raise InvalidArgumentError(
992
+ f'SDU size({len(sdu)}) exceeds channel MPS {self.peer_mps}'
993
+ )
994
+ pdu = self._PendingPdu(payload=sdu, tx_seq=self._get_next_tx_seq())
995
+ self._pending_pdus.append(pdu)
996
+ self._process_output()
997
+
998
+ @override
999
+ def on_pdu(self, pdu: bytes) -> None:
1000
+ control_field = EnhancedControlField.from_bytes(pdu)
1001
+ self._update_ack_seq(control_field.req_seq, control_field.final != 0)
1002
+ if isinstance(control_field, InformationEnhancedControlField):
1003
+ if control_field.tx_seq != self._next_seq_num:
1004
+ return
1005
+ self._next_seq_num = (self._next_seq_num + 1) % self.MAX_SEQ_NUM
1006
+ self._req_seq_num = self._next_seq_num
1007
+
1008
+ ack_frame = SupervisoryEnhancedControlField(
1009
+ supervision_function=SupervisoryEnhancedControlField.SupervisoryFunction.RR,
1010
+ req_seq=self._next_seq_num,
1011
+ )
1012
+ self.channel.send_pdu(ack_frame)
1013
+ self.channel.on_sdu(pdu[2:])
1014
+ elif isinstance(control_field, SupervisoryEnhancedControlField):
1015
+ self._remote_is_busy = (
1016
+ control_field.supervision_function
1017
+ == SupervisoryEnhancedControlField.SupervisoryFunction.RNR
1018
+ )
1019
+
1020
+ if control_field.supervision_function in (
1021
+ SupervisoryEnhancedControlField.SupervisoryFunction.RR,
1022
+ SupervisoryEnhancedControlField.SupervisoryFunction.RNR,
1023
+ ):
1024
+ if control_field.poll:
1025
+ self.channel.send_pdu(
1026
+ SupervisoryEnhancedControlField(
1027
+ supervision_function=SupervisoryEnhancedControlField.SupervisoryFunction.RR,
1028
+ final=1,
1029
+ req_seq=self._next_seq_num,
1030
+ )
1031
+ )
1032
+ else:
1033
+ # TODO: Handle Retransmission.
1034
+ pass
1035
+
1036
+ def _process_output(self) -> None:
1037
+ if self._remote_is_busy or self._monitor_handle:
1038
+ return
1039
+
1040
+ for pdu in self._pending_pdus:
1041
+ if self._num_unacked_frames >= self.peer_tx_window_size:
1042
+ return
1043
+ self._send_pdu(pdu)
1044
+ self._last_tx_seq = pdu.tx_seq
1045
+
1046
+ @property
1047
+ def _num_unacked_frames(self) -> int:
1048
+ if not self._pending_pdus:
1049
+ return 0
1050
+ return self._num_frames_between(self._expected_ack_seq, self._last_tx_seq + 1)
1051
+
1052
+ def _send_pdu(self, pdu: _PendingPdu) -> None:
1053
+ pdu.req_seq = self._req_seq_num
1054
+
1055
+ self._start_receiver_ready_poll()
1056
+ self.channel.send_pdu(bytes(pdu))
1057
+
1058
+ def _update_ack_seq(self, new_seq: int, is_poll_response: bool) -> None:
1059
+ num_frames_acked = self._num_frames_between(self._expected_ack_seq, new_seq)
1060
+ if num_frames_acked > self._num_unacked_frames:
1061
+ logger.error(
1062
+ "Received acknowledgment for %d frames but only %d frames are pending",
1063
+ num_frames_acked,
1064
+ self._num_unacked_frames,
1065
+ )
1066
+ return
1067
+ if is_poll_response and self._monitor_handle:
1068
+ self._monitor_handle.cancel()
1069
+ self._monitor_handle = None
1070
+
1071
+ del self._pending_pdus[:num_frames_acked]
1072
+ self._expected_ack_seq = new_seq
1073
+ if (
1074
+ self._expected_ack_seq == self._next_tx_seq
1075
+ and self._receiver_ready_poll_handle
1076
+ ):
1077
+ self._receiver_ready_poll_handle.cancel()
1078
+ self._receiver_ready_poll_handle = None
1079
+
1080
+ self._process_output()
1081
+
1082
+
705
1083
  # -----------------------------------------------------------------------------
706
1084
  class ClassicChannel(utils.EventEmitter):
707
1085
  class State(enum.IntEnum):
@@ -731,14 +1109,15 @@ class ClassicChannel(utils.EventEmitter):
731
1109
  EVENT_OPEN = "open"
732
1110
  EVENT_CLOSE = "close"
733
1111
 
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]]
1112
+ connection_result: asyncio.Future[None] | None
1113
+ disconnection_result: asyncio.Future[None] | None
1114
+ response: asyncio.Future[bytes] | None
1115
+ sink: Callable[[bytes], Any] | None
738
1116
  state: State
739
1117
  connection: Connection
740
1118
  mtu: int
741
1119
  peer_mtu: int
1120
+ processor: Processor
742
1121
 
743
1122
  def __init__(
744
1123
  self,
@@ -747,14 +1126,14 @@ class ClassicChannel(utils.EventEmitter):
747
1126
  signaling_cid: int,
748
1127
  psm: int,
749
1128
  source_cid: int,
750
- mtu: int,
1129
+ spec: ClassicChannelSpec,
751
1130
  ) -> None:
752
1131
  super().__init__()
753
1132
  self.manager = manager
754
1133
  self.connection = connection
755
1134
  self.signaling_cid = signaling_cid
756
1135
  self.state = self.State.CLOSED
757
- self.mtu = mtu
1136
+ self.mtu = spec.mtu
758
1137
  self.peer_mtu = L2CAP_MIN_BR_EDR_MTU
759
1138
  self.psm = psm
760
1139
  self.source_cid = source_cid
@@ -762,26 +1141,47 @@ class ClassicChannel(utils.EventEmitter):
762
1141
  self.connection_result = None
763
1142
  self.disconnection_result = None
764
1143
  self.sink = None
1144
+ self.fcs_enabled = spec.fcs_enabled
1145
+ self.spec = spec
1146
+ self.mode = spec.mode
1147
+ # Configure mode-specific processor later on configure request.
1148
+ self.processor = Processor(self)
1149
+ if self.mode not in (
1150
+ TransmissionMode.BASIC,
1151
+ TransmissionMode.ENHANCED_RETRANSMISSION,
1152
+ ):
1153
+ raise InvalidArgumentError(f"Mode {spec.mode} is not supported")
765
1154
 
766
1155
  def _change_state(self, new_state: State) -> None:
767
1156
  logger.debug(f'{self} state change -> {color(new_state.name, "cyan")}')
768
1157
  self.state = new_state
769
1158
 
770
- def send_pdu(self, pdu: Union[SupportsBytes, bytes]) -> None:
1159
+ def write(self, sdu: bytes) -> None:
1160
+ self.processor.send_sdu(sdu)
1161
+
1162
+ def send_pdu(self, pdu: SupportsBytes | bytes) -> None:
771
1163
  if self.state != self.State.OPEN:
772
1164
  raise InvalidStateError('channel not open')
773
- self.manager.send_pdu(self.connection, self.destination_cid, pdu)
1165
+ self.manager.send_pdu(
1166
+ self.connection, self.destination_cid, pdu, self.fcs_enabled
1167
+ )
774
1168
 
775
1169
  def send_control_frame(self, frame: L2CAP_Control_Frame) -> None:
776
1170
  self.manager.send_control_frame(self.connection, self.signaling_cid, frame)
777
1171
 
778
1172
  def on_pdu(self, pdu: bytes) -> None:
1173
+ if self.fcs_enabled:
1174
+ # Drop FCS.
1175
+ pdu = pdu[:-2]
1176
+ self.processor.on_pdu(pdu)
1177
+
1178
+ def on_sdu(self, sdu: bytes) -> None:
779
1179
  if self.sink:
780
1180
  # pylint: disable=not-callable
781
- self.sink(pdu)
1181
+ self.sink(sdu)
782
1182
  else:
783
1183
  logger.warning(
784
- color('received pdu without a pending request or sink', 'red')
1184
+ color('received sdu without a pending request or sink', 'red')
785
1185
  )
786
1186
 
787
1187
  async def connect(self) -> None:
@@ -811,10 +1211,8 @@ class ClassicChannel(utils.EventEmitter):
811
1211
  finally:
812
1212
  self.connection_result = None
813
1213
 
814
- async def disconnect(self) -> None:
815
- if self.state != self.State.OPEN:
816
- raise InvalidStateError('invalid state')
817
-
1214
+ def _disconnect_sync(self) -> None:
1215
+ """For internal sync disconnection."""
818
1216
  self._change_state(self.State.WAIT_DISCONNECT)
819
1217
  self.send_control_frame(
820
1218
  L2CAP_Disconnection_Request(
@@ -827,7 +1225,21 @@ class ClassicChannel(utils.EventEmitter):
827
1225
  # Create a future to wait for the state machine to get to a success or error
828
1226
  # state
829
1227
  self.disconnection_result = asyncio.get_running_loop().create_future()
830
- return await self.disconnection_result
1228
+
1229
+ def _abort_connection_result(self, message: str = 'Connection failure') -> None:
1230
+ # Cancel pending connection result.
1231
+ if self.connection_result and not self.connection_result.done():
1232
+ self.connection_result.set_exception(
1233
+ L2capError(error_code=0, error_name=message)
1234
+ )
1235
+
1236
+ async def disconnect(self) -> None:
1237
+ if self.state != self.State.OPEN:
1238
+ raise InvalidStateError('invalid state')
1239
+
1240
+ self._disconnect_sync()
1241
+ if self.disconnection_result:
1242
+ return await self.disconnection_result
831
1243
 
832
1244
  def abort(self) -> None:
833
1245
  if self.state == self.State.OPEN:
@@ -835,20 +1247,40 @@ class ClassicChannel(utils.EventEmitter):
835
1247
  self.emit(self.EVENT_CLOSE)
836
1248
 
837
1249
  def send_configure_request(self) -> None:
838
- options = L2CAP_Control_Frame.encode_configuration_options(
839
- [
1250
+ options: list[tuple[int, bytes]] = [
1251
+ (
1252
+ L2CAP_Configure_Request.ParameterType.MTU,
1253
+ struct.pack('<H', self.mtu),
1254
+ )
1255
+ ]
1256
+ if self.mode == TransmissionMode.ENHANCED_RETRANSMISSION:
1257
+ options.append(
840
1258
  (
841
- L2CAP_MAXIMUM_TRANSMISSION_UNIT_CONFIGURATION_OPTION_TYPE,
842
- struct.pack('<H', self.mtu),
1259
+ L2CAP_Configure_Request.ParameterType.RETRANSMISSION_AND_FLOW_CONTROL,
1260
+ struct.pack(
1261
+ '<BBBHHH',
1262
+ TransmissionMode.ENHANCED_RETRANSMISSION,
1263
+ self.spec.tx_window_size,
1264
+ self.spec.max_retransmission,
1265
+ int(self.spec.retransmission_timeout * 1000),
1266
+ int(self.spec.monitor_timeout * 1000),
1267
+ self.spec.mps,
1268
+ ),
843
1269
  )
844
- ]
845
- )
1270
+ )
1271
+ if self.fcs_enabled:
1272
+ options.append(
1273
+ (
1274
+ L2CAP_Configure_Request.ParameterType.FCS,
1275
+ bytes([1 if self.fcs_enabled else 0]),
1276
+ )
1277
+ )
846
1278
  self.send_control_frame(
847
1279
  L2CAP_Configure_Request(
848
1280
  identifier=self.manager.next_identifier(self.connection),
849
1281
  destination_cid=self.destination_cid,
850
1282
  flags=0x0000,
851
- options=options,
1283
+ options=L2CAP_Control_Frame.encode_configuration_options(options),
852
1284
  )
853
1285
  )
854
1286
 
@@ -884,9 +1316,8 @@ class ClassicChannel(utils.EventEmitter):
884
1316
  self._change_state(self.State.CLOSED)
885
1317
  if self.connection_result:
886
1318
  self.connection_result.set_exception(
887
- ProtocolError(
1319
+ L2capError(
888
1320
  response.result,
889
- 'l2cap',
890
1321
  L2CAP_Connection_Response.Result(response.result).name,
891
1322
  )
892
1323
  )
@@ -903,20 +1334,111 @@ class ClassicChannel(utils.EventEmitter):
903
1334
 
904
1335
  # Decode the options
905
1336
  options = L2CAP_Control_Frame.decode_configuration_options(request.options)
1337
+ # Result to options
1338
+ replied_options = list[tuple[int, bytes]]()
1339
+ result = L2CAP_Configure_Response.Result.SUCCESS
1340
+ new_mode = TransmissionMode.BASIC
906
1341
  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}')
1342
+ match option[0]:
1343
+ case L2CAP_Configure_Request.ParameterType.MTU:
1344
+ self.peer_mtu = struct.unpack('<H', option[1])[0]
1345
+ logger.debug('Peer MTU = %d', self.peer_mtu)
1346
+ replied_options.append(option)
1347
+ case (
1348
+ L2CAP_Configure_Request.ParameterType.RETRANSMISSION_AND_FLOW_CONTROL
1349
+ ):
1350
+ (
1351
+ mode,
1352
+ peer_tx_window_size,
1353
+ peer_max_retransmission,
1354
+ peer_retransmission_timeout,
1355
+ peer_monitor_timeout,
1356
+ peer_mps,
1357
+ ) = struct.unpack_from('<BBBHHH', option[1])
1358
+ new_mode = TransmissionMode(mode)
1359
+ logger.debug(
1360
+ 'Peer requests Retransmission or Flow Control: mode=%s,'
1361
+ ' tx_window_size=%s,'
1362
+ ' max_retransmission=%s,'
1363
+ ' retransmission_timeout=%s,'
1364
+ ' monitor_timeout=%s,'
1365
+ ' mps=%s',
1366
+ new_mode.name,
1367
+ peer_tx_window_size,
1368
+ peer_max_retransmission,
1369
+ peer_retransmission_timeout,
1370
+ peer_monitor_timeout,
1371
+ peer_mps,
1372
+ )
1373
+ if new_mode != self.mode:
1374
+ logger.error('Mode mismatch, abort connection')
1375
+ self._abort_connection_result(
1376
+ 'Abort on configuration - mode mismatch'
1377
+ )
1378
+ self._disconnect_sync()
1379
+ return
1380
+
1381
+ if new_mode == TransmissionMode.BASIC:
1382
+ replied_options.append(option)
1383
+ elif new_mode == TransmissionMode.ENHANCED_RETRANSMISSION:
1384
+ self.processor = self.manager.make_mode_processor(
1385
+ self,
1386
+ mode=new_mode,
1387
+ peer_tx_window_size=peer_tx_window_size,
1388
+ peer_max_retransmission=peer_max_retransmission,
1389
+ peer_monitor_timeout=peer_monitor_timeout,
1390
+ peer_retransmission_timeout=peer_retransmission_timeout,
1391
+ peer_mps=peer_mps,
1392
+ )
1393
+ replied_options.append(option)
1394
+ else:
1395
+ logger.error("Mode %s is not supported", new_mode.name)
1396
+ self._abort_connection_result(
1397
+ 'Abort on configuration - unsupported mode'
1398
+ )
1399
+ self._disconnect_sync()
1400
+ return
1401
+
1402
+ case L2CAP_Configure_Request.ParameterType.FCS:
1403
+ enabled = option[1][0] != 0
1404
+ logger.debug("Peer requests FCS: %s", enabled)
1405
+ if (
1406
+ L2CAP_Information_Request.ExtendedFeatures.FCS_OPTION
1407
+ in self.manager.extended_features
1408
+ ):
1409
+ self.fcs_enabled = enabled
1410
+ replied_options.append(option)
1411
+ else:
1412
+ logger.error("Frame Check Sequence is not supported")
1413
+ result = (
1414
+ L2CAP_Configure_Response.Result.FAILURE_UNACCEPTABLE_PARAMETERS
1415
+ )
1416
+ replied_options = [option]
1417
+ break
1418
+ case _:
1419
+ logger.debug(
1420
+ "Reject unimplemented option %s[%s]",
1421
+ option[0].name,
1422
+ option[1].hex(),
1423
+ )
1424
+ result = L2CAP_Configure_Response.Result.FAILURE_UNKNOWN_OPTIONS
1425
+ replied_options = [option]
1426
+ break
910
1427
 
911
1428
  self.send_control_frame(
912
1429
  L2CAP_Configure_Response(
913
1430
  identifier=request.identifier,
914
1431
  source_cid=self.destination_cid,
915
1432
  flags=0x0000,
916
- result=L2CAP_Configure_Response.Result.SUCCESS,
917
- options=request.options, # TODO: don't accept everything blindly
1433
+ result=result,
1434
+ options=L2CAP_Control_Frame.encode_configuration_options(
1435
+ replied_options
1436
+ ),
918
1437
  )
919
1438
  )
1439
+ if result != L2CAP_Configure_Response.Result.SUCCESS:
1440
+ return
1441
+
920
1442
  if self.state == self.State.WAIT_CONFIG:
921
1443
  self._change_state(self.State.WAIT_SEND_CONFIG)
922
1444
  self.send_configure_request()
@@ -969,25 +1491,19 @@ class ClassicChannel(utils.EventEmitter):
969
1491
  # TODO: decide how to fail gracefully
970
1492
 
971
1493
  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
- )
1494
+ self.send_control_frame(
1495
+ L2CAP_Disconnection_Response(
1496
+ identifier=request.identifier,
1497
+ destination_cid=request.destination_cid,
1498
+ source_cid=request.source_cid,
979
1499
  )
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'))
1500
+ )
1501
+ self._abort_connection_result()
1502
+ self._change_state(self.State.CLOSED)
1503
+ self.emit(self.EVENT_CLOSE)
1504
+ self.manager.on_channel_closed(self)
985
1505
 
986
1506
  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
1507
  if (
992
1508
  response.destination_cid != self.destination_cid
993
1509
  or response.source_cid != self.source_cid
@@ -1026,22 +1542,23 @@ class LeCreditBasedChannel(utils.EventEmitter):
1026
1542
  CONNECTION_ERROR = 5
1027
1543
 
1028
1544
  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]
1545
+ connection_result: asyncio.Future[LeCreditBasedChannel] | None
1546
+ disconnection_result: asyncio.Future[None] | None
1547
+ in_sdu: bytes | None
1548
+ out_sdu: bytes | None
1033
1549
  state: State
1034
1550
  connection: Connection
1035
- sink: Optional[Callable[[bytes], Any]]
1551
+ sink: Callable[[bytes], Any] | None
1036
1552
 
1037
1553
  EVENT_OPEN = "open"
1038
1554
  EVENT_CLOSE = "close"
1555
+ EVENT_ATT_MTU_UPDATE = "att_mtu_update"
1039
1556
 
1040
1557
  def __init__(
1041
1558
  self,
1042
1559
  manager: ChannelManager,
1043
1560
  connection: Connection,
1044
- le_psm: int,
1561
+ psm: int,
1045
1562
  source_cid: int,
1046
1563
  destination_cid: int,
1047
1564
  mtu: int,
@@ -1055,7 +1572,7 @@ class LeCreditBasedChannel(utils.EventEmitter):
1055
1572
  super().__init__()
1056
1573
  self.manager = manager
1057
1574
  self.connection = connection
1058
- self.le_psm = le_psm
1575
+ self.psm = psm
1059
1576
  self.source_cid = source_cid
1060
1577
  self.destination_cid = destination_cid
1061
1578
  self.mtu = mtu
@@ -1075,6 +1592,7 @@ class LeCreditBasedChannel(utils.EventEmitter):
1075
1592
  self.connection_result = None
1076
1593
  self.disconnection_result = None
1077
1594
  self.drained = asyncio.Event()
1595
+ self.att_mtu = 0 # Filled by GATT client or server later.
1078
1596
 
1079
1597
  self.drained.set()
1080
1598
 
@@ -1092,7 +1610,7 @@ class LeCreditBasedChannel(utils.EventEmitter):
1092
1610
  elif new_state == self.State.DISCONNECTED:
1093
1611
  self.emit(self.EVENT_CLOSE)
1094
1612
 
1095
- def send_pdu(self, pdu: Union[SupportsBytes, bytes]) -> None:
1613
+ def send_pdu(self, pdu: SupportsBytes | bytes) -> None:
1096
1614
  self.manager.send_pdu(self.connection, self.destination_cid, pdu)
1097
1615
 
1098
1616
  def send_control_frame(self, frame: L2CAP_Control_Frame) -> None:
@@ -1111,7 +1629,7 @@ class LeCreditBasedChannel(utils.EventEmitter):
1111
1629
  self._change_state(self.State.CONNECTING)
1112
1630
  request = L2CAP_LE_Credit_Based_Connection_Request(
1113
1631
  identifier=identifier,
1114
- le_psm=self.le_psm,
1632
+ le_psm=self.psm,
1115
1633
  source_cid=self.source_cid,
1116
1634
  mtu=self.mtu,
1117
1635
  mps=self.mps,
@@ -1149,6 +1667,9 @@ class LeCreditBasedChannel(utils.EventEmitter):
1149
1667
  def abort(self) -> None:
1150
1668
  if self.state == self.State.CONNECTED:
1151
1669
  self._change_state(self.State.DISCONNECTED)
1670
+ if self.state == self.State.CONNECTING:
1671
+ if self.connection_result is not None:
1672
+ self.connection_result.cancel()
1152
1673
 
1153
1674
  def on_pdu(self, pdu: bytes) -> None:
1154
1675
  if self.sink is None:
@@ -1239,9 +1760,8 @@ class LeCreditBasedChannel(utils.EventEmitter):
1239
1760
  self._change_state(self.State.CONNECTED)
1240
1761
  else:
1241
1762
  self.connection_result.set_exception(
1242
- ProtocolError(
1763
+ L2capError(
1243
1764
  response.result,
1244
- 'l2cap',
1245
1765
  L2CAP_LE_Credit_Based_Connection_Response.Result(
1246
1766
  response.result
1247
1767
  ).name,
@@ -1252,6 +1772,22 @@ class LeCreditBasedChannel(utils.EventEmitter):
1252
1772
  # Cleanup
1253
1773
  self.connection_result = None
1254
1774
 
1775
+ def on_enhanced_connection_response(
1776
+ self, destination_cid: int, response: L2CAP_Credit_Based_Connection_Response
1777
+ ) -> None:
1778
+ if (
1779
+ response.result
1780
+ == L2CAP_Credit_Based_Connection_Response.Result.ALL_CONNECTIONS_SUCCESSFUL
1781
+ ):
1782
+ self.destination_cid = destination_cid
1783
+ self.peer_mtu = response.mtu
1784
+ self.peer_mps = response.mps
1785
+ self.credits = response.initial_credits
1786
+ self.connected = True
1787
+ self._change_state(self.State.CONNECTED)
1788
+ else:
1789
+ self._change_state(self.State.CONNECTION_ERROR)
1790
+
1255
1791
  def on_credits(self, credits: int) -> None: # pylint: disable=redefined-builtin
1256
1792
  self.credits += credits
1257
1793
  logger.debug(f'received {credits} credits, total = {self.credits}')
@@ -1287,6 +1823,10 @@ class LeCreditBasedChannel(utils.EventEmitter):
1287
1823
  self.disconnection_result.set_result(None)
1288
1824
  self.disconnection_result = None
1289
1825
 
1826
+ def on_att_mtu_update(self, mtu: int) -> None:
1827
+ self.att_mtu = mtu
1828
+ self.emit(self.EVENT_ATT_MTU_UPDATE, mtu)
1829
+
1290
1830
  def flush_output(self) -> None:
1291
1831
  self.out_queue.clear()
1292
1832
  self.out_sdu = None
@@ -1364,7 +1904,7 @@ class LeCreditBasedChannel(utils.EventEmitter):
1364
1904
  return (
1365
1905
  f'CoC({self.source_cid}->{self.destination_cid}, '
1366
1906
  f'State={self.state.name}, '
1367
- f'PSM={self.le_psm}, '
1907
+ f'PSM={self.psm}, '
1368
1908
  f'MTU={self.mtu}/{self.peer_mtu}, '
1369
1909
  f'MPS={self.mps}/{self.peer_mps}, '
1370
1910
  f'credits={self.credits}/{self.peer_credits})'
@@ -1379,14 +1919,14 @@ class ClassicChannelServer(utils.EventEmitter):
1379
1919
  self,
1380
1920
  manager: ChannelManager,
1381
1921
  psm: int,
1382
- handler: Optional[Callable[[ClassicChannel], Any]],
1383
- mtu: int,
1922
+ handler: Callable[[ClassicChannel], Any] | None,
1923
+ spec: ClassicChannelSpec,
1384
1924
  ) -> None:
1385
1925
  super().__init__()
1386
1926
  self.manager = manager
1387
1927
  self.handler = handler
1388
1928
  self.psm = psm
1389
- self.mtu = mtu
1929
+ self.spec = spec
1390
1930
 
1391
1931
  def on_connection(self, channel: ClassicChannel) -> None:
1392
1932
  self.emit(self.EVENT_CONNECTION, channel)
@@ -1406,7 +1946,7 @@ class LeCreditBasedChannelServer(utils.EventEmitter):
1406
1946
  self,
1407
1947
  manager: ChannelManager,
1408
1948
  psm: int,
1409
- handler: Optional[Callable[[LeCreditBasedChannel], Any]],
1949
+ handler: Callable[[LeCreditBasedChannel], Any] | None,
1410
1950
  max_credits: int,
1411
1951
  mtu: int,
1412
1952
  mps: int,
@@ -1432,14 +1972,24 @@ class LeCreditBasedChannelServer(utils.EventEmitter):
1432
1972
  # -----------------------------------------------------------------------------
1433
1973
  class ChannelManager:
1434
1974
  identifiers: dict[int, int]
1435
- channels: dict[int, dict[int, Union[ClassicChannel, LeCreditBasedChannel]]]
1975
+ channels: dict[int, dict[int, ClassicChannel | LeCreditBasedChannel]]
1436
1976
  servers: dict[int, ClassicChannelServer]
1437
1977
  le_coc_channels: dict[int, dict[int, LeCreditBasedChannel]]
1438
1978
  le_coc_servers: dict[int, LeCreditBasedChannelServer]
1439
1979
  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]]
1980
+ fixed_channels: dict[int, Callable[[int, bytes], Any] | None]
1981
+ pending_credit_based_connections: dict[
1982
+ int,
1983
+ dict[
1984
+ int,
1985
+ tuple[
1986
+ asyncio.Future[None],
1987
+ list[LeCreditBasedChannel],
1988
+ ],
1989
+ ],
1990
+ ]
1991
+ _host: Host | None
1992
+ connection_parameters_update_response: asyncio.Future[int] | None
1443
1993
 
1444
1994
  def __init__(
1445
1995
  self,
@@ -1459,7 +2009,10 @@ class ChannelManager:
1459
2009
  ) # LE CoC channels, mapped by connection and destination cid
1460
2010
  self.le_coc_servers = {} # LE CoC - Servers accepting connections, by PSM
1461
2011
  self.le_coc_requests = {} # LE CoC connection requests, by identifier
1462
- self.extended_features = extended_features
2012
+ self.pending_credit_based_connections = (
2013
+ {}
2014
+ ) # Credit-based connection request contexts, by connection handle and identifier
2015
+ self.extended_features = set(extended_features)
1463
2016
  self.connectionless_mtu = connectionless_mtu
1464
2017
  self.connection_parameters_update_response = None
1465
2018
 
@@ -1501,18 +2054,26 @@ class ChannelManager:
1501
2054
 
1502
2055
  raise OutOfResourcesError('no free CID available')
1503
2056
 
1504
- @staticmethod
1505
- def find_free_le_cid(channels: Iterable[int]) -> int:
2057
+ @classmethod
2058
+ def find_free_le_cid(cls, channels: Iterable[int]) -> int | None:
2059
+ cids = cls.find_free_le_cids(channels, 1)
2060
+ return cids[0] if cids else None
2061
+
2062
+ @classmethod
2063
+ def find_free_le_cids(cls, channels: Iterable[int], count: int) -> list[int]:
1506
2064
  # Pick the smallest valid CID that's not already in the list
1507
2065
  # (not necessarily the most efficient algorithm, but the list of CID is
1508
2066
  # very small in practice)
2067
+ cids: list[int] = []
1509
2068
  for cid in range(
1510
2069
  L2CAP_LE_U_DYNAMIC_CID_RANGE_START, L2CAP_LE_U_DYNAMIC_CID_RANGE_END + 1
1511
2070
  ):
1512
2071
  if cid not in channels:
1513
- return cid
2072
+ cids.append(cid)
2073
+ if len(cids) == count:
2074
+ return cids
1514
2075
 
1515
- raise OutOfResourcesError('no free CID')
2076
+ return []
1516
2077
 
1517
2078
  def next_identifier(self, connection: Connection) -> int:
1518
2079
  identifier = (self.identifiers.setdefault(connection.handle, 0) + 1) % 256
@@ -1534,7 +2095,7 @@ class ChannelManager:
1534
2095
  def create_classic_server(
1535
2096
  self,
1536
2097
  spec: ClassicChannelSpec,
1537
- handler: Optional[Callable[[ClassicChannel], Any]] = None,
2098
+ handler: Callable[[ClassicChannel], Any] | None = None,
1538
2099
  ) -> ClassicChannelServer:
1539
2100
  if not spec.psm:
1540
2101
  # Find a free PSM
@@ -1563,14 +2124,14 @@ class ChannelManager:
1563
2124
  raise InvalidArgumentError('invalid PSM')
1564
2125
  check >>= 8
1565
2126
 
1566
- self.servers[spec.psm] = ClassicChannelServer(self, spec.psm, handler, spec.mtu)
2127
+ self.servers[spec.psm] = ClassicChannelServer(self, spec.psm, handler, spec)
1567
2128
 
1568
2129
  return self.servers[spec.psm]
1569
2130
 
1570
2131
  def create_le_credit_based_server(
1571
2132
  self,
1572
2133
  spec: LeCreditBasedChannelSpec,
1573
- handler: Optional[Callable[[LeCreditBasedChannel], Any]] = None,
2134
+ handler: Callable[[LeCreditBasedChannel], Any] | None = None,
1574
2135
  ) -> LeCreditBasedChannelServer:
1575
2136
  if not spec.psm:
1576
2137
  # Find a free PSM
@@ -1599,20 +2160,30 @@ class ChannelManager:
1599
2160
 
1600
2161
  return self.le_coc_servers[spec.psm]
1601
2162
 
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():
1606
- 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():
2163
+ def on_disconnection(self, connection_handle: int, reason: int) -> None:
2164
+ del reason # unused.
2165
+ logger.debug('disconnection from %d, cleaning up channels', connection_handle)
2166
+ if channels := self.channels.pop(connection_handle, None):
2167
+ for channel in channels.values():
1610
2168
  channel.abort()
1611
- del self.le_coc_channels[connection_handle]
1612
- if connection_handle in self.identifiers:
1613
- del self.identifiers[connection_handle]
2169
+ if le_coc_channels := self.le_coc_channels.pop(connection_handle, None):
2170
+ for le_coc_channel in le_coc_channels.values():
2171
+ le_coc_channel.abort()
2172
+ if pending_credit_based_connections := self.pending_credit_based_connections.pop(
2173
+ connection_handle, None
2174
+ ):
2175
+ for future, _ in pending_credit_based_connections.values():
2176
+ if not future.done():
2177
+ future.cancel("ACL disconnected")
2178
+ self.identifiers.pop(connection_handle, None)
1614
2179
 
1615
- def send_pdu(self, connection, cid: int, pdu: Union[SupportsBytes, bytes]) -> None:
2180
+ def send_pdu(
2181
+ self,
2182
+ connection: Connection,
2183
+ cid: int,
2184
+ pdu: SupportsBytes | bytes,
2185
+ with_fcs: bool = False,
2186
+ ) -> None:
1616
2187
  pdu_str = pdu.hex() if isinstance(pdu, bytes) else str(pdu)
1617
2188
  pdu_bytes = bytes(pdu)
1618
2189
  logger.debug(
@@ -1620,7 +2191,9 @@ class ChannelManager:
1620
2191
  f'on connection [0x{connection.handle:04X}] (CID={cid}) '
1621
2192
  f'{connection.peer_address}: {len(pdu_bytes)} bytes, {pdu_str}'
1622
2193
  )
1623
- self.host.send_l2cap_pdu(connection.handle, cid, pdu_bytes)
2194
+ self.host.send_acl_sdu(
2195
+ connection.handle, L2CAP_PDU(cid, bytes(pdu)).to_bytes(with_fcs=with_fcs)
2196
+ )
1624
2197
 
1625
2198
  def on_pdu(self, connection: Connection, cid: int, pdu: bytes) -> None:
1626
2199
  if cid in (L2CAP_SIGNALING_CID, L2CAP_LE_SIGNALING_CID):
@@ -1714,7 +2287,6 @@ class ChannelManager:
1714
2287
  identifier=request.identifier,
1715
2288
  destination_cid=request.source_cid,
1716
2289
  source_cid=0,
1717
- # pylint: disable=line-too-long
1718
2290
  result=L2CAP_Connection_Response.Result.CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE,
1719
2291
  status=0x0000,
1720
2292
  ),
@@ -1726,7 +2298,7 @@ class ChannelManager:
1726
2298
  f'creating server channel with cid={source_cid} for psm {request.psm}'
1727
2299
  )
1728
2300
  channel = ClassicChannel(
1729
- self, connection, cid, request.psm, source_cid, server.mtu
2301
+ self, connection, cid, request.psm, source_cid, server.spec
1730
2302
  )
1731
2303
  connection_channels[source_cid] = channel
1732
2304
 
@@ -1745,7 +2317,6 @@ class ChannelManager:
1745
2317
  identifier=request.identifier,
1746
2318
  destination_cid=request.source_cid,
1747
2319
  source_cid=0,
1748
- # pylint: disable=line-too-long
1749
2320
  result=L2CAP_Connection_Response.Result.CONNECTION_REFUSED_PSM_NOT_SUPPORTED,
1750
2321
  status=0x0000,
1751
2322
  ),
@@ -1974,106 +2545,100 @@ class ChannelManager:
1974
2545
  cid: int,
1975
2546
  request: L2CAP_LE_Credit_Based_Connection_Request,
1976
2547
  ) -> 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}'
2548
+ if not (server := self.le_coc_servers.get(request.le_psm)):
2549
+ logger.info(
2550
+ f'No LE server for connection 0x{connection.handle:04X} '
2551
+ f'on PSM {request.le_psm}'
2024
2552
  )
2025
- channel = LeCreditBasedChannel(
2026
- self,
2553
+ self.send_control_frame(
2027
2554
  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,
2555
+ cid,
2556
+ L2CAP_LE_Credit_Based_Connection_Response(
2557
+ identifier=request.identifier,
2558
+ destination_cid=0,
2559
+ mtu=L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU,
2560
+ mps=L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS,
2561
+ initial_credits=0,
2562
+ result=L2CAP_LE_Credit_Based_Connection_Response.Result.CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED,
2563
+ ),
2038
2564
  )
2039
- connection_channels[source_cid] = channel
2040
- le_connection_channels[request.source_cid] = channel
2565
+ return
2041
2566
 
2042
- # Respond
2567
+ # Check that the CID isn't already used
2568
+ le_connection_channels = self.le_coc_channels.setdefault(connection.handle, {})
2569
+ if request.source_cid in le_connection_channels:
2570
+ logger.warning(f'source CID {request.source_cid} already in use')
2043
2571
  self.send_control_frame(
2044
2572
  connection,
2045
2573
  cid,
2046
2574
  L2CAP_LE_Credit_Based_Connection_Response(
2047
2575
  identifier=request.identifier,
2048
- destination_cid=source_cid,
2576
+ destination_cid=0,
2049
2577
  mtu=server.mtu,
2050
2578
  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,
2579
+ initial_credits=0,
2580
+ result=L2CAP_LE_Credit_Based_Connection_Response.Result.CONNECTION_REFUSED_SOURCE_CID_ALREADY_ALLOCATED,
2054
2581
  ),
2055
2582
  )
2583
+ return
2056
2584
 
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
- )
2585
+ # Find a free CID for this new channel
2586
+ connection_channels = self.channels.setdefault(connection.handle, {})
2587
+ source_cid = self.find_free_le_cid(connection_channels)
2588
+ if source_cid is None: # Should never happen!
2064
2589
  self.send_control_frame(
2065
2590
  connection,
2066
2591
  cid,
2067
2592
  L2CAP_LE_Credit_Based_Connection_Response(
2068
2593
  identifier=request.identifier,
2069
2594
  destination_cid=0,
2070
- mtu=L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU,
2071
- mps=L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS,
2595
+ mtu=server.mtu,
2596
+ mps=server.mps,
2072
2597
  initial_credits=0,
2073
- # pylint: disable=line-too-long
2074
- result=L2CAP_LE_Credit_Based_Connection_Response.Result.CONNECTION_REFUSED_LE_PSM_NOT_SUPPORTED,
2598
+ result=L2CAP_LE_Credit_Based_Connection_Response.Result.CONNECTION_REFUSED_NO_RESOURCES_AVAILABLE,
2075
2599
  ),
2076
2600
  )
2601
+ return
2602
+
2603
+ # Create a new channel
2604
+ logger.debug(
2605
+ f'creating LE CoC server channel with cid={source_cid} for psm '
2606
+ f'{request.le_psm}'
2607
+ )
2608
+ channel = LeCreditBasedChannel(
2609
+ self,
2610
+ connection,
2611
+ request.le_psm,
2612
+ source_cid,
2613
+ request.source_cid,
2614
+ server.mtu,
2615
+ server.mps,
2616
+ request.initial_credits,
2617
+ request.mtu,
2618
+ request.mps,
2619
+ server.max_credits,
2620
+ True,
2621
+ )
2622
+ connection_channels[source_cid] = channel
2623
+ le_connection_channels[request.source_cid] = channel
2624
+
2625
+ # Respond
2626
+ self.send_control_frame(
2627
+ connection,
2628
+ cid,
2629
+ L2CAP_LE_Credit_Based_Connection_Response(
2630
+ identifier=request.identifier,
2631
+ destination_cid=source_cid,
2632
+ mtu=server.mtu,
2633
+ mps=server.mps,
2634
+ initial_credits=server.max_credits,
2635
+ # pylint: disable=line-too-long
2636
+ result=L2CAP_LE_Credit_Based_Connection_Response.Result.CONNECTION_SUCCESSFUL,
2637
+ ),
2638
+ )
2639
+
2640
+ # Notify
2641
+ server.on_connection(channel)
2077
2642
 
2078
2643
  def on_l2cap_le_credit_based_connection_response(
2079
2644
  self,
@@ -2082,11 +2647,9 @@ class ChannelManager:
2082
2647
  response: L2CAP_LE_Credit_Based_Connection_Response,
2083
2648
  ) -> None:
2084
2649
  # Find the pending request by identifier
2085
- request = self.le_coc_requests.get(response.identifier)
2086
- if request is None:
2650
+ if not (request := self.le_coc_requests.pop(response.identifier, None)):
2087
2651
  logger.warning(color('!!! received response for unknown request', 'red'))
2088
2652
  return
2089
- del self.le_coc_requests[response.identifier]
2090
2653
 
2091
2654
  # Find the channel for this request
2092
2655
  channel = self.find_channel(connection.handle, request.source_cid)
@@ -2103,6 +2666,147 @@ class ChannelManager:
2103
2666
  # Process the response
2104
2667
  channel.on_connection_response(response)
2105
2668
 
2669
+ def on_l2cap_credit_based_connection_request(
2670
+ self,
2671
+ connection: Connection,
2672
+ cid: int,
2673
+ request: L2CAP_Credit_Based_Connection_Request,
2674
+ ) -> None:
2675
+ if not (server := self.le_coc_servers.get(request.spsm)):
2676
+ logger.info(
2677
+ 'No LE server for connection 0x%04X ' 'on PSM %d',
2678
+ connection.handle,
2679
+ request.spsm,
2680
+ )
2681
+ self.send_control_frame(
2682
+ connection,
2683
+ cid,
2684
+ L2CAP_Credit_Based_Connection_Response(
2685
+ identifier=request.identifier,
2686
+ destination_cid=[],
2687
+ mtu=L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU,
2688
+ mps=L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS,
2689
+ initial_credits=0,
2690
+ result=L2CAP_Credit_Based_Connection_Response.Result.ALL_CONNECTIONS_REFUSED_SPSM_NOT_SUPPORTED,
2691
+ ),
2692
+ )
2693
+ return
2694
+
2695
+ # Check that the CID isn't already used
2696
+ le_connection_channels = self.le_coc_channels.setdefault(connection.handle, {})
2697
+ if cid_in_use := set(request.source_cid).intersection(
2698
+ set(le_connection_channels)
2699
+ ):
2700
+ logger.warning('source CID already in use: %s', cid_in_use)
2701
+ self.send_control_frame(
2702
+ connection,
2703
+ cid,
2704
+ L2CAP_Credit_Based_Connection_Response(
2705
+ identifier=request.identifier,
2706
+ mtu=server.mtu,
2707
+ mps=server.mps,
2708
+ initial_credits=0,
2709
+ result=L2CAP_Credit_Based_Connection_Response.Result.SOME_CONNECTIONS_REFUSED_SOURCE_CID_ALREADY_ALLOCATED,
2710
+ destination_cid=[],
2711
+ ),
2712
+ )
2713
+ return
2714
+
2715
+ # Find free CIDs for new channels
2716
+ connection_channels = self.channels.setdefault(connection.handle, {})
2717
+ source_cids = self.find_free_le_cids(
2718
+ connection_channels, len(request.source_cid)
2719
+ )
2720
+ if not source_cids:
2721
+ self.send_control_frame(
2722
+ connection,
2723
+ cid,
2724
+ L2CAP_Credit_Based_Connection_Response(
2725
+ identifier=request.identifier,
2726
+ destination_cid=[],
2727
+ mtu=server.mtu,
2728
+ mps=server.mps,
2729
+ initial_credits=server.max_credits,
2730
+ result=L2CAP_Credit_Based_Connection_Response.Result.SOME_CONNECTIONS_REFUSED_INSUFFICIENT_RESOURCES_AVAILABLE,
2731
+ ),
2732
+ )
2733
+ return
2734
+
2735
+ for destination_cid in request.source_cid:
2736
+ # TODO: Handle Classic channels.
2737
+ if not (source_cid := self.find_free_le_cid(connection_channels)):
2738
+ logger.warning("No free CIDs available")
2739
+ break
2740
+ # Create a new channel
2741
+ logger.debug(
2742
+ 'creating LE CoC server channel with cid=%s for psm %s',
2743
+ source_cid,
2744
+ request.spsm,
2745
+ )
2746
+ channel = LeCreditBasedChannel(
2747
+ self,
2748
+ connection,
2749
+ request.spsm,
2750
+ source_cid,
2751
+ destination_cid,
2752
+ server.mtu,
2753
+ server.mps,
2754
+ request.initial_credits,
2755
+ request.mtu,
2756
+ request.mps,
2757
+ server.max_credits,
2758
+ True,
2759
+ )
2760
+ connection_channels[source_cid] = channel
2761
+ le_connection_channels[source_cid] = channel
2762
+ server.on_connection(channel)
2763
+
2764
+ # Respond
2765
+ self.send_control_frame(
2766
+ connection,
2767
+ cid,
2768
+ L2CAP_Credit_Based_Connection_Response(
2769
+ identifier=request.identifier,
2770
+ destination_cid=source_cids,
2771
+ mtu=server.mtu,
2772
+ mps=server.mps,
2773
+ initial_credits=server.max_credits,
2774
+ result=L2CAP_Credit_Based_Connection_Response.Result.ALL_CONNECTIONS_SUCCESSFUL,
2775
+ ),
2776
+ )
2777
+
2778
+ def on_l2cap_credit_based_connection_response(
2779
+ self,
2780
+ connection: Connection,
2781
+ _cid: int,
2782
+ response: L2CAP_Credit_Based_Connection_Response,
2783
+ ) -> None:
2784
+ # Find the pending request by identifier
2785
+ pending_connections = self.pending_credit_based_connections.setdefault(
2786
+ connection.handle, {}
2787
+ )
2788
+ if not (
2789
+ pending_connection := pending_connections.pop(response.identifier, None)
2790
+ ):
2791
+ logger.warning(color('!!! received response for unknown request', 'red'))
2792
+ return
2793
+
2794
+ connection_result, channels = pending_connection
2795
+
2796
+ # Process the response
2797
+ for channel, destination_cid in zip(channels, response.destination_cid):
2798
+ channel.on_enhanced_connection_response(destination_cid, response)
2799
+
2800
+ if (
2801
+ response.result
2802
+ == L2CAP_Credit_Based_Connection_Response.Result.ALL_CONNECTIONS_SUCCESSFUL
2803
+ ):
2804
+ connection_result.set_result(None)
2805
+ else:
2806
+ connection_result.set_exception(
2807
+ L2capError(response.result, response.result.name)
2808
+ )
2809
+
2106
2810
  def on_l2cap_le_flow_control_credit(
2107
2811
  self, connection: Connection, _cid: int, credit: L2CAP_LE_Flow_Control_Credit
2108
2812
  ) -> None:
@@ -2138,7 +2842,7 @@ class ChannelManager:
2138
2842
  channel = LeCreditBasedChannel(
2139
2843
  manager=self,
2140
2844
  connection=connection,
2141
- le_psm=spec.psm,
2845
+ psm=spec.psm,
2142
2846
  source_cid=source_cid,
2143
2847
  destination_cid=0,
2144
2848
  mtu=spec.mtu,
@@ -2184,12 +2888,12 @@ class ChannelManager:
2184
2888
  f'creating client channel with cid={source_cid} for psm {spec.psm}'
2185
2889
  )
2186
2890
  channel = ClassicChannel(
2187
- self,
2188
- connection,
2189
- L2CAP_SIGNALING_CID,
2190
- spec.psm,
2191
- source_cid,
2192
- spec.mtu,
2891
+ manager=self,
2892
+ connection=connection,
2893
+ signaling_cid=L2CAP_SIGNALING_CID,
2894
+ psm=spec.psm,
2895
+ source_cid=source_cid,
2896
+ spec=spec,
2193
2897
  )
2194
2898
  connection_channels[source_cid] = channel
2195
2899
 
@@ -2197,7 +2901,100 @@ class ChannelManager:
2197
2901
  try:
2198
2902
  await channel.connect()
2199
2903
  except BaseException as e:
2200
- del connection_channels[source_cid]
2904
+ connection_channels.pop(source_cid, None)
2201
2905
  raise e
2202
2906
 
2203
2907
  return channel
2908
+
2909
+ async def create_enhanced_credit_based_channels(
2910
+ self,
2911
+ connection: Connection,
2912
+ spec: LeCreditBasedChannelSpec,
2913
+ count: int,
2914
+ ) -> list[LeCreditBasedChannel]:
2915
+ # Find a free CID for the new channel
2916
+ connection_channels = self.channels.setdefault(connection.handle, {})
2917
+ source_cids = self.find_free_le_cids(connection_channels, count)
2918
+ if not source_cids: # Should never happen!
2919
+ raise OutOfResourcesError('all CIDs already in use')
2920
+
2921
+ if spec.psm is None:
2922
+ raise InvalidArgumentError('PSM cannot be None')
2923
+
2924
+ # Create the channel
2925
+ logger.debug(
2926
+ 'creating coc channel with cid=%s for psm %s', source_cids, spec.psm
2927
+ )
2928
+ channels: list[LeCreditBasedChannel] = []
2929
+ for source_cid in source_cids:
2930
+ channel = LeCreditBasedChannel(
2931
+ manager=self,
2932
+ connection=connection,
2933
+ psm=spec.psm,
2934
+ source_cid=source_cid,
2935
+ destination_cid=0,
2936
+ mtu=spec.mtu,
2937
+ mps=spec.mps,
2938
+ credits=0,
2939
+ peer_mtu=0,
2940
+ peer_mps=0,
2941
+ peer_credits=spec.max_credits,
2942
+ connected=False,
2943
+ )
2944
+ connection_channels[source_cid] = channel
2945
+ channels.append(channel)
2946
+
2947
+ identifier = self.next_identifier(connection)
2948
+ request = L2CAP_Credit_Based_Connection_Request(
2949
+ identifier=identifier,
2950
+ spsm=spec.psm,
2951
+ mtu=spec.mtu,
2952
+ mps=spec.mps,
2953
+ initial_credits=spec.max_credits,
2954
+ source_cid=source_cids,
2955
+ )
2956
+ connection_result = asyncio.get_running_loop().create_future()
2957
+ pending_connections = self.pending_credit_based_connections.setdefault(
2958
+ connection.handle, {}
2959
+ )
2960
+ pending_connections[identifier] = (connection_result, channels)
2961
+ self.send_control_frame(
2962
+ connection,
2963
+ L2CAP_LE_SIGNALING_CID,
2964
+ request,
2965
+ )
2966
+ # Connect
2967
+ try:
2968
+ await connection_result
2969
+ except Exception:
2970
+ logger.exception('connection failed')
2971
+ for cid in source_cids:
2972
+ del connection_channels[cid]
2973
+ raise
2974
+
2975
+ # Remember the channel by source CID and destination CID
2976
+ le_connection_channels = self.le_coc_channels.setdefault(connection.handle, {})
2977
+ for channel in channels:
2978
+ le_connection_channels[channel.destination_cid] = channel
2979
+
2980
+ return channels
2981
+
2982
+ @classmethod
2983
+ def make_mode_processor(
2984
+ self,
2985
+ channel: ClassicChannel,
2986
+ mode: TransmissionMode,
2987
+ peer_tx_window_size: int,
2988
+ peer_max_retransmission: int,
2989
+ peer_retransmission_timeout: int,
2990
+ peer_monitor_timeout: int,
2991
+ peer_mps: int,
2992
+ ) -> Processor:
2993
+ del peer_retransmission_timeout, peer_monitor_timeout # Unused.
2994
+ if mode == TransmissionMode.BASIC:
2995
+ return Processor(channel)
2996
+ elif mode == TransmissionMode.ENHANCED_RETRANSMISSION:
2997
+ return EnhancedRetransmissionProcessor(
2998
+ channel, peer_tx_window_size, peer_max_retransmission, peer_mps
2999
+ )
3000
+ raise InvalidArgumentError("Mode %s is not implemented", mode.name)