Merge pull request #779 from zxzxwu/l2cap

L2CAP Enhanced Retransmission mode
This commit is contained in:
zxzxwu
2025-12-03 21:57:48 +08:00
committed by GitHub
6 changed files with 857 additions and 114 deletions

View File

@@ -2080,6 +2080,12 @@ class DeviceConfiguration:
io_capability: int = pairing.PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT io_capability: int = pairing.PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT
gap_service_enabled: bool = True gap_service_enabled: bool = True
gatt_service_enabled: bool = True gatt_service_enabled: bool = True
enhanced_retransmission_supported: bool = False
l2cap_extended_features: Sequence[int] = (
l2cap.L2CAP_Information_Request.ExtendedFeatures.FIXED_CHANNELS,
l2cap.L2CAP_Information_Request.ExtendedFeatures.FCS_OPTION,
l2cap.L2CAP_Information_Request.ExtendedFeatures.ENHANCED_RETRANSMISSION_MODE,
)
def __post_init__(self) -> None: def __post_init__(self) -> None:
self.gatt_services: list[dict[str, Any]] = [] self.gatt_services: list[dict[str, Any]] = []
@@ -2349,6 +2355,10 @@ class Device(utils.CompositeEventEmitter):
) -> None: ) -> None:
super().__init__() super().__init__()
# Use the initial config or a default
config = config or DeviceConfiguration()
self.config = config
self._host = None self._host = None
self.powered_on = False self.powered_on = False
self.auto_restart_inquiry = True self.auto_restart_inquiry = True
@@ -2356,7 +2366,7 @@ class Device(utils.CompositeEventEmitter):
self.gatt_server = gatt_server.Server(self) self.gatt_server = gatt_server.Server(self)
self.sdp_server = sdp.Server(self) self.sdp_server = sdp.Server(self)
self.l2cap_channel_manager = l2cap.ChannelManager( self.l2cap_channel_manager = l2cap.ChannelManager(
[l2cap.L2CAP_Information_Request.EXTENDED_FEATURE_FIXED_CHANNELS] config.l2cap_extended_features
) )
self.advertisement_accumulators = {} # Accumulators, by address self.advertisement_accumulators = {} # Accumulators, by address
self.periodic_advertising_syncs = [] self.periodic_advertising_syncs = []
@@ -2387,10 +2397,6 @@ class Device(utils.CompositeEventEmitter):
# Own address type cache # Own address type cache
self.connect_own_address_type = None self.connect_own_address_type = None
# Use the initial config or a default
config = config or DeviceConfiguration()
self.config = config
self.name = config.name self.name = config.name
self.public_address = hci.Address.ANY self.public_address = hci.Address.ANY
self.random_address = config.address self.random_address = config.address

View File

@@ -707,7 +707,7 @@ class Host(utils.EventEmitter):
asyncio.create_task(send_command(command)) asyncio.create_task(send_command(command))
def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None: def send_acl_sdu(self, connection_handle: int, sdu: bytes) -> None:
if not (connection := self.connections.get(connection_handle)): if not (connection := self.connections.get(connection_handle)):
logger.warning(f'connection 0x{connection_handle:04X} not found') logger.warning(f'connection 0x{connection_handle:04X} not found')
return return
@@ -718,27 +718,24 @@ class Host(utils.EventEmitter):
) )
return return
# Create a PDU
l2cap_pdu = bytes(L2CAP_PDU(cid, pdu))
# Send the data to the controller via ACL packets # Send the data to the controller via ACL packets
bytes_remaining = len(l2cap_pdu) max_packet_size = packet_queue.max_packet_size
offset = 0 for offset in range(0, len(sdu), max_packet_size):
pb_flag = 0 pdu = sdu[offset : offset + max_packet_size]
while bytes_remaining:
data_total_length = min(bytes_remaining, packet_queue.max_packet_size)
acl_packet = hci.HCI_AclDataPacket( acl_packet = hci.HCI_AclDataPacket(
connection_handle=connection_handle, connection_handle=connection_handle,
pb_flag=pb_flag, pb_flag=1 if offset > 0 else 0,
bc_flag=0, bc_flag=0,
data_total_length=data_total_length, data_total_length=len(pdu),
data=l2cap_pdu[offset : offset + data_total_length], data=pdu,
)
logger.debug(
'>>> ACL packet enqueue: (Handle=0x%04X) %s', connection_handle, pdu
) )
logger.debug(f'>>> ACL packet enqueue: (CID={cid}) {acl_packet}')
packet_queue.enqueue(acl_packet, connection_handle) packet_queue.enqueue(acl_packet, connection_handle)
pb_flag = 1
offset += data_total_length def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
bytes_remaining -= data_total_length self.send_acl_sdu(connection_handle, bytes(L2CAP_PDU(cid, pdu)))
def get_data_packet_queue(self, connection_handle: int) -> DataPacketQueue | None: def get_data_packet_queue(self, connection_handle: int) -> DataPacketQueue | None:
if connection := self.connections.get(connection_handle): if connection := self.connections.get(connection_handle):

View File

@@ -23,18 +23,10 @@ import enum
import logging import logging
import struct import struct
from collections import deque from collections import deque
from collections.abc import Sequence from collections.abc import Callable, Iterable, Sequence
from typing import ( from typing import TYPE_CHECKING, Any, ClassVar, Optional, SupportsBytes, TypeVar, Union
TYPE_CHECKING,
Any, from typing_extensions import override
Callable,
ClassVar,
Iterable,
Optional,
SupportsBytes,
TypeVar,
Union,
)
from bumble import hci, utils from bumble import hci, utils
from bumble.colors import color from bumble.colors import color
@@ -69,7 +61,12 @@ L2CAP_MIN_LE_MTU = 23
L2CAP_MIN_BR_EDR_MTU = 48 L2CAP_MIN_BR_EDR_MTU = 48
L2CAP_MAX_BR_EDR_MTU = 65535 L2CAP_MAX_BR_EDR_MTU = 65535
L2CAP_DEFAULT_MTU = 2048 # Default value for the MTU we are willing to accept L2CAP_DEFAULT_MTU = 2048 # Default value for the MTU we are willing to accept
L2CAP_DEFAULT_MPS = 1010 # Default value for the MPS we are willing to accept
DEFAULT_TX_WINDOW_SIZE = 63
DEFAULT_MAX_RETRANSMISSION = 1
DEFAULT_RETRANSMISSION_TIMEOUT = 2.0
DEFAULT_MONITOR_TIMEOUT = 12.0
L2CAP_DEFAULT_CONNECTIONLESS_MTU = 1024 L2CAP_DEFAULT_CONNECTIONLESS_MTU = 1024
@@ -133,24 +130,60 @@ L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MTU = 2048
L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS = 2048 L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_MPS = 2048
L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_INITIAL_CREDITS = 256 L2CAP_LE_CREDIT_BASED_CONNECTION_DEFAULT_INITIAL_CREDITS = 256
L2CAP_MAXIMUM_TRANSMISSION_UNIT_CONFIGURATION_OPTION_TYPE = 0x01
L2CAP_MTU_CONFIGURATION_PARAMETER_TYPE = 0x01
# fmt: on # fmt: on
# pylint: enable=line-too-long # pylint: enable=line-too-long
class TransmissionMode(utils.OpenIntEnum):
'''See Bluetooth spec @ Vol 3, Part A - 5.4. Retransmission and Flow Control option'''
BASIC = 0x00
RETRANSMISSION = 0x01
FLOW_CONTROL = 0x02
ENHANCED_RETRANSMISSION = 0x03
STREAMING = 0x04
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Classes # Classes
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# pylint: disable=invalid-name # pylint: disable=invalid-name
class L2capError(ProtocolError):
def __init__(self, error_code, error_name='', details=''):
super().__init__(error_code, 'L2CAP', error_name, details)
@dataclasses.dataclass @dataclasses.dataclass
class ClassicChannelSpec: class ClassicChannelSpec:
'''Spec of L2CAP Channel over Classic Transport.
Attributes:
psm: PSM of channel. This is optional for server, and when it is None, a PSM
will be allocated.
mtu: Maximum Transmission Unit.
mps: Maximum PDU payload Size.
tx_window_size: The size of the transmission window for Flow Control mode,
Retransmission mode, and Enhanced Retransmission mode.
max_retransmission: The number of transmissions of a single I-frame that L2CAP
is allowed to try in Retransmission mode and Enhanced Retransmission mode.
retransmission_timeout: The timeout of retransmission in seconds.
monitor_timeout: The interval at which S-frames should be transmitted on the
return channel when no frames are received on the forward channel.
mode: The transmission mode to use.
fcs_enabled: Whether to enable FCS (Frame Check Sequence).
'''
psm: Optional[int] = None psm: Optional[int] = None
mtu: int = L2CAP_DEFAULT_MTU mtu: int = L2CAP_DEFAULT_MTU
mps: int = L2CAP_DEFAULT_MPS
tx_window_size: int = DEFAULT_TX_WINDOW_SIZE
max_retransmission: int = DEFAULT_MAX_RETRANSMISSION
retransmission_timeout: float = DEFAULT_RETRANSMISSION_TIMEOUT
monitor_timeout: float = DEFAULT_MONITOR_TIMEOUT
mode: TransmissionMode = TransmissionMode.BASIC
fcs_enabled: bool = False
@dataclasses.dataclass @dataclasses.dataclass
@@ -183,20 +216,29 @@ class L2CAP_PDU:
See Bluetooth spec @ Vol 3, Part A - 3 DATA PACKET FORMAT See Bluetooth spec @ Vol 3, Part A - 3 DATA PACKET FORMAT
''' '''
@staticmethod @classmethod
def from_bytes(data: bytes) -> L2CAP_PDU: def from_bytes(cls, data: bytes) -> L2CAP_PDU:
# Check parameters # Check parameters
if len(data) < 4: if len(data) < 4:
raise InvalidPacketError('not enough data for L2CAP header') raise InvalidPacketError('not enough data for L2CAP header')
_, l2cap_pdu_cid = struct.unpack_from('<HH', data, 0) length, l2cap_pdu_cid = struct.unpack_from('<HH', data, 0)
l2cap_pdu_payload = data[4:] l2cap_pdu_payload = data[4 : 4 + length]
return L2CAP_PDU(l2cap_pdu_cid, l2cap_pdu_payload) return cls(l2cap_pdu_cid, l2cap_pdu_payload)
def __bytes__(self) -> bytes: def __bytes__(self) -> bytes:
header = struct.pack('<HH', len(self.payload), self.cid) return self.to_bytes(with_fcs=False)
return header + self.payload
def to_bytes(self, with_fcs: bool = False) -> bytes:
length = len(self.payload)
if with_fcs:
length += 2
header = struct.pack('<HH', length, self.cid)
body = header + self.payload
if with_fcs:
body += struct.pack('<H', utils.crc_16(body))
return body
def __init__(self, cid: int, payload: bytes) -> None: def __init__(self, cid: int, payload: bytes) -> None:
self.cid = cid self.cid = cid
@@ -206,6 +248,120 @@ class L2CAP_PDU:
return f'{color("L2CAP", "green")} [CID={self.cid}]: {self.payload.hex()}' return f'{color("L2CAP", "green")} [CID={self.cid}]: {self.payload.hex()}'
class ControlField:
'''
See Bluetooth spec @ Vol 3, Part A - 3.3.2 Control field.
'''
class FieldType(utils.OpenIntEnum):
I_FRAME = 0x00
S_FRAME = 0x01
class SegmentationAndReassembly(utils.OpenIntEnum):
UNSEGMENTED = 0x00
START = 0x01
END = 0x02
CONTINUATION = 0x03
class SupervisoryFunction(utils.OpenIntEnum):
# Receiver Ready
RR = 0
# Reject
REJ = 1
# Receiver Not Ready
RNR = 2
# Select Reject
SREJ = 3
class RetransmissionBit(utils.OpenIntEnum):
NORMAL = 0x00
RETRANSMISSION = 0x01
req_seq: int
frame_type: ClassVar[FieldType]
def __bytes__(self) -> bytes:
raise NotImplementedError()
class EnhancedControlField(ControlField):
"""Base control field used in Enhanced Retransmission and Streaming Mode."""
final: int
@classmethod
def from_bytes(cls, data: bytes) -> EnhancedControlField:
frame_type = data[0] & 0x01
if frame_type == cls.FieldType.I_FRAME:
return InformationEnhancedControlField.from_bytes(data)
elif frame_type == cls.FieldType.S_FRAME:
return SupervisoryEnhancedControlField.from_bytes(data)
else:
raise InvalidArgumentError(f'Invalid frame type: {frame_type}')
@dataclasses.dataclass
class InformationEnhancedControlField(EnhancedControlField):
tx_seq: int = 0
req_seq: int = 0
segmentation_and_reassembly: int = (
EnhancedControlField.SegmentationAndReassembly.UNSEGMENTED
)
final: int = 1
frame_type = EnhancedControlField.FieldType.I_FRAME
@classmethod
def from_bytes(cls, data: bytes) -> EnhancedControlField:
return cls(
tx_seq=(data[0] >> 1) & 0b0111111,
final=(data[0] >> 7) & 0b1,
req_seq=(data[1] & 0b001111111),
segmentation_and_reassembly=(data[1] >> 6) & 0b11,
)
def __bytes__(self) -> bytes:
return bytes(
[
self.frame_type | (self.tx_seq << 1) | (self.final << 7),
self.req_seq | (self.segmentation_and_reassembly << 6),
]
)
@dataclasses.dataclass
class SupervisoryEnhancedControlField(EnhancedControlField):
supervision_function: int = ControlField.SupervisoryFunction.RR
poll: int = 0
req_seq: int = 0
final: int = 0
frame_type = EnhancedControlField.FieldType.S_FRAME
@classmethod
def from_bytes(cls, data: bytes) -> EnhancedControlField:
return cls(
supervision_function=(data[0] >> 2) & 0b11,
poll=(data[0] >> 4) & 0b1,
final=(data[0] >> 7) & 0b1,
req_seq=(data[1] & 0b1111111),
)
def __bytes__(self) -> bytes:
return bytes(
[
(
self.frame_type
| (self.supervision_function << 2)
| self.poll << 7
| (self.final << 7)
),
self.req_seq,
]
)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@dataclasses.dataclass @dataclasses.dataclass
class L2CAP_Control_Frame: class L2CAP_Control_Frame:
@@ -248,14 +404,16 @@ class L2CAP_Control_Frame:
return frame return frame
@staticmethod @staticmethod
def decode_configuration_options(data: bytes) -> list[tuple[int, bytes]]: def decode_configuration_options(
data: bytes,
) -> list[tuple[L2CAP_Configure_Request.ParameterType, bytes]]:
options = [] options = []
while len(data) >= 2: while len(data) >= 2:
value_type = data[0] value_type = data[0]
length = data[1] length = data[1]
value = data[2 : 2 + length] value = data[2 : 2 + length]
data = data[2 + length :] data = data[2 + length :]
options.append((value_type, value)) options.append((L2CAP_Configure_Request.ParameterType(value_type), value))
return options return options
@@ -398,6 +556,15 @@ class L2CAP_Configure_Request(L2CAP_Control_Frame):
See Bluetooth spec @ Vol 3, Part A - 4.4 CONFIGURATION REQUEST See Bluetooth spec @ Vol 3, Part A - 4.4 CONFIGURATION REQUEST
''' '''
class ParameterType(utils.OpenIntEnum):
MTU = 0x01
FLUSH_TIMEOUT = 0x02
QOS = 0x03
RETRANSMISSION_AND_FLOW_CONTROL = 0x04
FCS = 0x05
EXTENDED_FLOW_SPEC = 0x06
EXTENDED_WINDOW_SIZE = 0x07
destination_cid: int = dataclasses.field(metadata=hci.metadata(2)) destination_cid: int = dataclasses.field(metadata=hci.metadata(2))
flags: int = dataclasses.field(metadata=hci.metadata(2)) flags: int = dataclasses.field(metadata=hci.metadata(2))
options: bytes = dataclasses.field(metadata=hci.metadata('*')) options: bytes = dataclasses.field(metadata=hci.metadata('*'))
@@ -484,17 +651,18 @@ class L2CAP_Information_Request(L2CAP_Control_Frame):
EXTENDED_FEATURES_SUPPORTED = 0x0002 EXTENDED_FEATURES_SUPPORTED = 0x0002
FIXED_CHANNELS_SUPPORTED = 0x0003 FIXED_CHANNELS_SUPPORTED = 0x0003
EXTENDED_FEATURE_FLOW_MODE_CONTROL = 0x0001 class ExtendedFeatures(hci.SpecableFlag):
EXTENDED_FEATURE_RETRANSMISSION_MODE = 0x0002 FLOW_MODE_CONTROL = 0x0001
EXTENDED_FEATURE_BIDIRECTIONAL_QOS = 0x0004 RETRANSMISSION_MODE = 0x0002
EXTENDED_FEATURE_ENHANCED_RETRANSMISSION_MODE = 0x0008 BIDIRECTIONAL_QOS = 0x0004
EXTENDED_FEATURE_STREAMING_MODE = 0x0010 ENHANCED_RETRANSMISSION_MODE = 0x0008
EXTENDED_FEATURE_FCS_OPTION = 0x0020 STREAMING_MODE = 0x0010
EXTENDED_FEATURE_EXTENDED_FLOW_SPEC = 0x0040 FCS_OPTION = 0x0020
EXTENDED_FEATURE_FIXED_CHANNELS = 0x0080 EXTENDED_FLOW_SPEC = 0x0040
EXTENDED_FEATURE_EXTENDED_WINDOW_SIZE = 0x0100 FIXED_CHANNELS = 0x0080
EXTENDED_FEATURE_UNICAST_CONNECTIONLESS_DATA = 0x0200 EXTENDED_WINDOW_SIZE = 0x0100
EXTENDED_FEATURE_ENHANCED_CREDIT_BASE_FLOW_CONTROL = 0x0400 UNICAST_CONNECTIONLESS_DATA = 0x0200
ENHANCED_CREDIT_BASE_FLOW_CONTROL = 0x0400
info_type: int = dataclasses.field(metadata=InfoType.type_metadata(2)) info_type: int = dataclasses.field(metadata=InfoType.type_metadata(2))
@@ -702,6 +870,218 @@ class L2CAP_Credit_Based_Reconfigure_Response(L2CAP_Control_Frame):
result: int = dataclasses.field(metadata=Result.type_metadata(2)) result: int = dataclasses.field(metadata=Result.type_metadata(2))
# -----------------------------------------------------------------------------
class Processor:
def __init__(self, channel: ClassicChannel) -> None:
self.channel = channel
def send_sdu(self, sdu: bytes) -> None:
self.channel.send_pdu(sdu)
def on_pdu(self, pdu: bytes) -> None:
self.channel.on_sdu(pdu)
# TODO: Handle retransmission
class EnhancedRetransmissionProcessor(Processor):
MAX_SEQ_NUM = 64
@dataclasses.dataclass
class _PendingPdu:
payload: bytes
tx_seq: int
req_seq: int = 0
def __bytes__(self) -> bytes:
return (
bytes(
InformationEnhancedControlField(
tx_seq=self.tx_seq, req_seq=self.req_seq
)
)
+ self.payload
)
_expected_ack_seq: int = 0
_next_tx_seq: int = 0
_last_tx_seq: int = 0
_req_seq_num: int = 0
_next_seq_num: int = 0
_remote_is_busy: bool = False
_num_receiver_ready_polls_sent: int = 0
_pending_pdus: list[_PendingPdu]
_monitor_handle: Optional[asyncio.TimerHandle] = None
_receiver_ready_poll_handle: Optional[asyncio.TimerHandle] = None
# Timeout, in seconds.
monitor_timeout: float
retransmission_timeout: float
@classmethod
def _num_frames_between(cls, low: int, high: int) -> int:
if high < low:
high += cls.MAX_SEQ_NUM
return high - low
def __init__(
self,
channel: ClassicChannel,
peer_tx_window_size: int = DEFAULT_TX_WINDOW_SIZE,
peer_max_retransmission: int = DEFAULT_MAX_RETRANSMISSION,
peer_mps: int = L2CAP_DEFAULT_MPS,
):
spec = channel.spec
self.mps = spec.mps
self.peer_mps = peer_mps
self.peer_tx_window_size = peer_tx_window_size
self._pending_pdus = []
self.monitor_timeout = spec.monitor_timeout
self.channel = channel
self.retransmission_timeout = spec.retransmission_timeout
self.peer_max_retransmission = peer_max_retransmission
def _monitor(self) -> None:
if (
self.peer_max_retransmission <= 0
or self._num_receiver_ready_polls_sent < self.peer_max_retransmission
):
self._send_receiver_ready_poll()
self._start_monitor()
else:
logger.error("Max retransmission exceeded")
def _receiver_ready_poll(self) -> None:
self._send_receiver_ready_poll()
self._start_monitor()
def _start_monitor(self) -> None:
if self._monitor_handle:
self._monitor_handle.cancel()
self._monitor_handle = asyncio.get_running_loop().call_later(
self.monitor_timeout, self._monitor
)
def _start_receiver_ready_poll(self) -> None:
if self._receiver_ready_poll_handle:
self._receiver_ready_poll_handle.cancel()
self._num_receiver_ready_polls_sent = 0
self._receiver_ready_poll_handle = asyncio.get_running_loop().call_later(
self.retransmission_timeout, self._receiver_ready_poll
)
def _send_receiver_ready_poll(self) -> None:
self._num_receiver_ready_polls_sent += 1
self.channel.send_pdu(
SupervisoryEnhancedControlField(
supervision_function=SupervisoryEnhancedControlField.SupervisoryFunction.RR,
final=1,
req_seq=self._next_seq_num,
)
)
def _get_next_tx_seq(self) -> int:
seq_num = self._next_tx_seq
self._next_tx_seq = (self._next_tx_seq + 1) % self.MAX_SEQ_NUM
return seq_num
@override
def send_sdu(self, sdu: bytes) -> None:
if len(sdu) > self.peer_mps:
raise InvalidArgumentError(
f'SDU size({len(sdu)}) exceeds channel MPS {self.peer_mps}'
)
pdu = self._PendingPdu(payload=sdu, tx_seq=self._get_next_tx_seq())
self._pending_pdus.append(pdu)
self._process_output()
@override
def on_pdu(self, pdu: bytes) -> None:
control_field = EnhancedControlField.from_bytes(pdu)
self._update_ack_seq(control_field.req_seq, control_field.final != 0)
if isinstance(control_field, InformationEnhancedControlField):
if control_field.tx_seq != self._next_seq_num:
return
self._next_seq_num = (self._next_seq_num + 1) % self.MAX_SEQ_NUM
self._req_seq_num = self._next_seq_num
ack_frame = SupervisoryEnhancedControlField(
supervision_function=SupervisoryEnhancedControlField.SupervisoryFunction.RR,
req_seq=self._next_seq_num,
)
self.channel.send_pdu(ack_frame)
self.channel.on_sdu(pdu[2:])
elif isinstance(control_field, SupervisoryEnhancedControlField):
self._remote_is_busy = (
control_field.supervision_function
== SupervisoryEnhancedControlField.SupervisoryFunction.RNR
)
if control_field.supervision_function in (
SupervisoryEnhancedControlField.SupervisoryFunction.RR,
SupervisoryEnhancedControlField.SupervisoryFunction.RNR,
):
if control_field.poll:
self.channel.send_pdu(
SupervisoryEnhancedControlField(
supervision_function=SupervisoryEnhancedControlField.SupervisoryFunction.RR,
final=1,
req_seq=self._next_seq_num,
)
)
else:
# TODO: Handle Retransmission.
pass
def _process_output(self) -> None:
if self._remote_is_busy or self._monitor_handle:
return
for pdu in self._pending_pdus:
if self._num_unacked_frames >= self.peer_tx_window_size:
return
self._send_pdu(pdu)
self._last_tx_seq = pdu.tx_seq
@property
def _num_unacked_frames(self) -> int:
if not self._pending_pdus:
return 0
return self._num_frames_between(self._expected_ack_seq, self._last_tx_seq + 1)
def _send_pdu(self, pdu: _PendingPdu) -> None:
pdu.req_seq = self._req_seq_num
self._start_receiver_ready_poll()
self.channel.send_pdu(bytes(pdu))
def _update_ack_seq(self, new_seq: int, is_poll_response: bool) -> None:
num_frames_acked = self._num_frames_between(self._expected_ack_seq, new_seq)
if num_frames_acked > self._num_unacked_frames:
logger.error(
"Received acknowledgment for %d frames but only %d frames are pending",
num_frames_acked,
self._num_unacked_frames,
)
return
if is_poll_response and self._monitor_handle:
self._monitor_handle.cancel()
self._monitor_handle = None
del self._pending_pdus[:num_frames_acked]
self._expected_ack_seq = new_seq
if (
self._expected_ack_seq == self._next_tx_seq
and self._receiver_ready_poll_handle
):
self._receiver_ready_poll_handle.cancel()
self._receiver_ready_poll_handle = None
self._process_output()
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class ClassicChannel(utils.EventEmitter): class ClassicChannel(utils.EventEmitter):
class State(enum.IntEnum): class State(enum.IntEnum):
@@ -739,6 +1119,7 @@ class ClassicChannel(utils.EventEmitter):
connection: Connection connection: Connection
mtu: int mtu: int
peer_mtu: int peer_mtu: int
processor: Processor
def __init__( def __init__(
self, self,
@@ -747,14 +1128,14 @@ class ClassicChannel(utils.EventEmitter):
signaling_cid: int, signaling_cid: int,
psm: int, psm: int,
source_cid: int, source_cid: int,
mtu: int, spec: ClassicChannelSpec,
) -> None: ) -> None:
super().__init__() super().__init__()
self.manager = manager self.manager = manager
self.connection = connection self.connection = connection
self.signaling_cid = signaling_cid self.signaling_cid = signaling_cid
self.state = self.State.CLOSED self.state = self.State.CLOSED
self.mtu = mtu self.mtu = spec.mtu
self.peer_mtu = L2CAP_MIN_BR_EDR_MTU self.peer_mtu = L2CAP_MIN_BR_EDR_MTU
self.psm = psm self.psm = psm
self.source_cid = source_cid self.source_cid = source_cid
@@ -762,26 +1143,47 @@ class ClassicChannel(utils.EventEmitter):
self.connection_result = None self.connection_result = None
self.disconnection_result = None self.disconnection_result = None
self.sink = None self.sink = None
self.fcs_enabled = spec.fcs_enabled
self.spec = spec
self.mode = spec.mode
# Configure mode-specific processor later on configure request.
self.processor = Processor(self)
if self.mode not in (
TransmissionMode.BASIC,
TransmissionMode.ENHANCED_RETRANSMISSION,
):
raise InvalidArgumentError(f"Mode {spec.mode} is not supported")
def _change_state(self, new_state: State) -> None: def _change_state(self, new_state: State) -> None:
logger.debug(f'{self} state change -> {color(new_state.name, "cyan")}') logger.debug(f'{self} state change -> {color(new_state.name, "cyan")}')
self.state = new_state self.state = new_state
def write(self, sdu: bytes) -> None:
self.processor.send_sdu(sdu)
def send_pdu(self, pdu: Union[SupportsBytes, bytes]) -> None: def send_pdu(self, pdu: Union[SupportsBytes, bytes]) -> None:
if self.state != self.State.OPEN: if self.state != self.State.OPEN:
raise InvalidStateError('channel not open') raise InvalidStateError('channel not open')
self.manager.send_pdu(self.connection, self.destination_cid, pdu) self.manager.send_pdu(
self.connection, self.destination_cid, pdu, self.fcs_enabled
)
def send_control_frame(self, frame: L2CAP_Control_Frame) -> None: def send_control_frame(self, frame: L2CAP_Control_Frame) -> None:
self.manager.send_control_frame(self.connection, self.signaling_cid, frame) self.manager.send_control_frame(self.connection, self.signaling_cid, frame)
def on_pdu(self, pdu: bytes) -> None: def on_pdu(self, pdu: bytes) -> None:
if self.fcs_enabled:
# Drop FCS.
pdu = pdu[:-2]
self.processor.on_pdu(pdu)
def on_sdu(self, sdu: bytes) -> None:
if self.sink: if self.sink:
# pylint: disable=not-callable # pylint: disable=not-callable
self.sink(pdu) self.sink(sdu)
else: else:
logger.warning( logger.warning(
color('received pdu without a pending request or sink', 'red') color('received sdu without a pending request or sink', 'red')
) )
async def connect(self) -> None: async def connect(self) -> None:
@@ -811,10 +1213,8 @@ class ClassicChannel(utils.EventEmitter):
finally: finally:
self.connection_result = None self.connection_result = None
async def disconnect(self) -> None: def _disconnect_sync(self) -> None:
if self.state != self.State.OPEN: """For internal sync disconnection."""
raise InvalidStateError('invalid state')
self._change_state(self.State.WAIT_DISCONNECT) self._change_state(self.State.WAIT_DISCONNECT)
self.send_control_frame( self.send_control_frame(
L2CAP_Disconnection_Request( L2CAP_Disconnection_Request(
@@ -827,7 +1227,21 @@ class ClassicChannel(utils.EventEmitter):
# Create a future to wait for the state machine to get to a success or error # Create a future to wait for the state machine to get to a success or error
# state # state
self.disconnection_result = asyncio.get_running_loop().create_future() self.disconnection_result = asyncio.get_running_loop().create_future()
return await self.disconnection_result
def _abort_connection_result(self, message: str = 'Connection failure') -> None:
# Cancel pending connection result.
if self.connection_result and not self.connection_result.done():
self.connection_result.set_exception(
L2capError(error_code=0, error_name=message)
)
async def disconnect(self) -> None:
if self.state != self.State.OPEN:
raise InvalidStateError('invalid state')
self._disconnect_sync()
if self.disconnection_result:
return await self.disconnection_result
def abort(self) -> None: def abort(self) -> None:
if self.state == self.State.OPEN: if self.state == self.State.OPEN:
@@ -835,20 +1249,40 @@ class ClassicChannel(utils.EventEmitter):
self.emit(self.EVENT_CLOSE) self.emit(self.EVENT_CLOSE)
def send_configure_request(self) -> None: def send_configure_request(self) -> None:
options = L2CAP_Control_Frame.encode_configuration_options( options: list[tuple[int, bytes]] = [
[ (
L2CAP_Configure_Request.ParameterType.MTU,
struct.pack('<H', self.mtu),
)
]
if self.mode == TransmissionMode.ENHANCED_RETRANSMISSION:
options.append(
( (
L2CAP_MAXIMUM_TRANSMISSION_UNIT_CONFIGURATION_OPTION_TYPE, L2CAP_Configure_Request.ParameterType.RETRANSMISSION_AND_FLOW_CONTROL,
struct.pack('<H', self.mtu), struct.pack(
'<BBBHHH',
TransmissionMode.ENHANCED_RETRANSMISSION,
self.spec.tx_window_size,
self.spec.max_retransmission,
int(self.spec.retransmission_timeout * 1000),
int(self.spec.monitor_timeout * 1000),
self.spec.mps,
),
) )
] )
) if self.fcs_enabled:
options.append(
(
L2CAP_Configure_Request.ParameterType.FCS,
bytes([1 if self.fcs_enabled else 0]),
)
)
self.send_control_frame( self.send_control_frame(
L2CAP_Configure_Request( L2CAP_Configure_Request(
identifier=self.manager.next_identifier(self.connection), identifier=self.manager.next_identifier(self.connection),
destination_cid=self.destination_cid, destination_cid=self.destination_cid,
flags=0x0000, flags=0x0000,
options=options, options=L2CAP_Control_Frame.encode_configuration_options(options),
) )
) )
@@ -884,9 +1318,8 @@ class ClassicChannel(utils.EventEmitter):
self._change_state(self.State.CLOSED) self._change_state(self.State.CLOSED)
if self.connection_result: if self.connection_result:
self.connection_result.set_exception( self.connection_result.set_exception(
ProtocolError( L2capError(
response.result, response.result,
'l2cap',
L2CAP_Connection_Response.Result(response.result).name, L2CAP_Connection_Response.Result(response.result).name,
) )
) )
@@ -903,20 +1336,111 @@ class ClassicChannel(utils.EventEmitter):
# Decode the options # Decode the options
options = L2CAP_Control_Frame.decode_configuration_options(request.options) options = L2CAP_Control_Frame.decode_configuration_options(request.options)
# Result to options
replied_options = list[tuple[int, bytes]]()
result = L2CAP_Configure_Response.Result.SUCCESS
new_mode = TransmissionMode.BASIC
for option in options: for option in options:
if option[0] == L2CAP_MTU_CONFIGURATION_PARAMETER_TYPE: match option[0]:
self.peer_mtu = struct.unpack('<H', option[1])[0] case L2CAP_Configure_Request.ParameterType.MTU:
logger.debug(f'peer MTU = {self.peer_mtu}') self.peer_mtu = struct.unpack('<H', option[1])[0]
logger.debug('Peer MTU = %d', self.peer_mtu)
replied_options.append(option)
case (
L2CAP_Configure_Request.ParameterType.RETRANSMISSION_AND_FLOW_CONTROL
):
(
mode,
peer_tx_window_size,
peer_max_retransmission,
peer_retransmission_timeout,
peer_monitor_timeout,
peer_mps,
) = struct.unpack_from('<BBBHHH', option[1])
new_mode = TransmissionMode(mode)
logger.debug(
'Peer requests Retransmission or Flow Control: mode=%s,'
' tx_window_size=%s,'
' max_retransmission=%s,'
' retransmission_timeout=%s,'
' monitor_timeout=%s,'
' mps=%s',
new_mode.name,
peer_tx_window_size,
peer_max_retransmission,
peer_retransmission_timeout,
peer_monitor_timeout,
peer_mps,
)
if new_mode != self.mode:
logger.error('Mode mismatch, abort connection')
self._abort_connection_result(
'Abort on configuration - mode mismatch'
)
self._disconnect_sync()
return
if new_mode == TransmissionMode.BASIC:
replied_options.append(option)
elif new_mode == TransmissionMode.ENHANCED_RETRANSMISSION:
self.processor = self.manager.make_mode_processor(
self,
mode=new_mode,
peer_tx_window_size=peer_tx_window_size,
peer_max_retransmission=peer_max_retransmission,
peer_monitor_timeout=peer_monitor_timeout,
peer_retransmission_timeout=peer_retransmission_timeout,
peer_mps=peer_mps,
)
replied_options.append(option)
else:
logger.error("Mode %s is not supported", new_mode.name)
self._abort_connection_result(
'Abort on configuration - unsupported mode'
)
self._disconnect_sync()
return
case L2CAP_Configure_Request.ParameterType.FCS:
enabled = option[1][0] != 0
logger.debug("Peer requests FCS: %s", enabled)
if (
L2CAP_Information_Request.ExtendedFeatures.FCS_OPTION
in self.manager.extended_features
):
self.fcs_enabled = enabled
replied_options.append(option)
else:
logger.error("Frame Check Sequence is not supported")
result = (
L2CAP_Configure_Response.Result.FAILURE_UNACCEPTABLE_PARAMETERS
)
replied_options = [option]
break
case _:
logger.debug(
"Reject unimplemented option %s[%s]",
option[0].name,
option[1].hex(),
)
result = L2CAP_Configure_Response.Result.FAILURE_UNKNOWN_OPTIONS
replied_options = [option]
break
self.send_control_frame( self.send_control_frame(
L2CAP_Configure_Response( L2CAP_Configure_Response(
identifier=request.identifier, identifier=request.identifier,
source_cid=self.destination_cid, source_cid=self.destination_cid,
flags=0x0000, flags=0x0000,
result=L2CAP_Configure_Response.Result.SUCCESS, result=result,
options=request.options, # TODO: don't accept everything blindly options=L2CAP_Control_Frame.encode_configuration_options(
replied_options
),
) )
) )
if result != L2CAP_Configure_Response.Result.SUCCESS:
return
if self.state == self.State.WAIT_CONFIG: if self.state == self.State.WAIT_CONFIG:
self._change_state(self.State.WAIT_SEND_CONFIG) self._change_state(self.State.WAIT_SEND_CONFIG)
self.send_configure_request() self.send_configure_request()
@@ -969,25 +1493,19 @@ class ClassicChannel(utils.EventEmitter):
# TODO: decide how to fail gracefully # TODO: decide how to fail gracefully
def on_disconnection_request(self, request: L2CAP_Disconnection_Request) -> None: def on_disconnection_request(self, request: L2CAP_Disconnection_Request) -> None:
if self.state in (self.State.OPEN, self.State.WAIT_DISCONNECT): self.send_control_frame(
self.send_control_frame( L2CAP_Disconnection_Response(
L2CAP_Disconnection_Response( identifier=request.identifier,
identifier=request.identifier, destination_cid=request.destination_cid,
destination_cid=request.destination_cid, source_cid=request.source_cid,
source_cid=request.source_cid,
)
) )
self._change_state(self.State.CLOSED) )
self.emit(self.EVENT_CLOSE) self._abort_connection_result()
self.manager.on_channel_closed(self) self._change_state(self.State.CLOSED)
else: self.emit(self.EVENT_CLOSE)
logger.warning(color('invalid state', 'red')) self.manager.on_channel_closed(self)
def on_disconnection_response(self, response: L2CAP_Disconnection_Response) -> None: def on_disconnection_response(self, response: L2CAP_Disconnection_Response) -> None:
if self.state != self.State.WAIT_DISCONNECT:
logger.warning(color('invalid state', 'red'))
return
if ( if (
response.destination_cid != self.destination_cid response.destination_cid != self.destination_cid
or response.source_cid != self.source_cid or response.source_cid != self.source_cid
@@ -1242,9 +1760,8 @@ class LeCreditBasedChannel(utils.EventEmitter):
self._change_state(self.State.CONNECTED) self._change_state(self.State.CONNECTED)
else: else:
self.connection_result.set_exception( self.connection_result.set_exception(
ProtocolError( L2capError(
response.result, response.result,
'l2cap',
L2CAP_LE_Credit_Based_Connection_Response.Result( L2CAP_LE_Credit_Based_Connection_Response.Result(
response.result response.result
).name, ).name,
@@ -1383,13 +1900,13 @@ class ClassicChannelServer(utils.EventEmitter):
manager: ChannelManager, manager: ChannelManager,
psm: int, psm: int,
handler: Optional[Callable[[ClassicChannel], Any]], handler: Optional[Callable[[ClassicChannel], Any]],
mtu: int, spec: ClassicChannelSpec,
) -> None: ) -> None:
super().__init__() super().__init__()
self.manager = manager self.manager = manager
self.handler = handler self.handler = handler
self.psm = psm self.psm = psm
self.mtu = mtu self.spec = spec
def on_connection(self, channel: ClassicChannel) -> None: def on_connection(self, channel: ClassicChannel) -> None:
self.emit(self.EVENT_CONNECTION, channel) self.emit(self.EVENT_CONNECTION, channel)
@@ -1462,7 +1979,7 @@ class ChannelManager:
) # LE CoC channels, mapped by connection and destination cid ) # LE CoC channels, mapped by connection and destination cid
self.le_coc_servers = {} # LE CoC - Servers accepting connections, by PSM self.le_coc_servers = {} # LE CoC - Servers accepting connections, by PSM
self.le_coc_requests = {} # LE CoC connection requests, by identifier self.le_coc_requests = {} # LE CoC connection requests, by identifier
self.extended_features = extended_features self.extended_features = set(extended_features)
self.connectionless_mtu = connectionless_mtu self.connectionless_mtu = connectionless_mtu
self.connection_parameters_update_response = None self.connection_parameters_update_response = None
@@ -1566,7 +2083,7 @@ class ChannelManager:
raise InvalidArgumentError('invalid PSM') raise InvalidArgumentError('invalid PSM')
check >>= 8 check >>= 8
self.servers[spec.psm] = ClassicChannelServer(self, spec.psm, handler, spec.mtu) self.servers[spec.psm] = ClassicChannelServer(self, spec.psm, handler, spec)
return self.servers[spec.psm] return self.servers[spec.psm]
@@ -1615,7 +2132,13 @@ class ChannelManager:
if connection_handle in self.identifiers: if connection_handle in self.identifiers:
del self.identifiers[connection_handle] del self.identifiers[connection_handle]
def send_pdu(self, connection, cid: int, pdu: Union[SupportsBytes, bytes]) -> None: def send_pdu(
self,
connection: Connection,
cid: int,
pdu: Union[SupportsBytes, bytes],
with_fcs: bool = False,
) -> None:
pdu_str = pdu.hex() if isinstance(pdu, bytes) else str(pdu) pdu_str = pdu.hex() if isinstance(pdu, bytes) else str(pdu)
pdu_bytes = bytes(pdu) pdu_bytes = bytes(pdu)
logger.debug( logger.debug(
@@ -1623,7 +2146,9 @@ class ChannelManager:
f'on connection [0x{connection.handle:04X}] (CID={cid}) ' f'on connection [0x{connection.handle:04X}] (CID={cid}) '
f'{connection.peer_address}: {len(pdu_bytes)} bytes, {pdu_str}' f'{connection.peer_address}: {len(pdu_bytes)} bytes, {pdu_str}'
) )
self.host.send_l2cap_pdu(connection.handle, cid, pdu_bytes) self.host.send_acl_sdu(
connection.handle, L2CAP_PDU(cid, bytes(pdu)).to_bytes(with_fcs=with_fcs)
)
def on_pdu(self, connection: Connection, cid: int, pdu: bytes) -> None: def on_pdu(self, connection: Connection, cid: int, pdu: bytes) -> None:
if cid in (L2CAP_SIGNALING_CID, L2CAP_LE_SIGNALING_CID): if cid in (L2CAP_SIGNALING_CID, L2CAP_LE_SIGNALING_CID):
@@ -1729,7 +2254,7 @@ class ChannelManager:
f'creating server channel with cid={source_cid} for psm {request.psm}' f'creating server channel with cid={source_cid} for psm {request.psm}'
) )
channel = ClassicChannel( channel = ClassicChannel(
self, connection, cid, request.psm, source_cid, server.mtu self, connection, cid, request.psm, source_cid, server.spec
) )
connection_channels[source_cid] = channel connection_channels[source_cid] = channel
@@ -2187,12 +2712,12 @@ class ChannelManager:
f'creating client channel with cid={source_cid} for psm {spec.psm}' f'creating client channel with cid={source_cid} for psm {spec.psm}'
) )
channel = ClassicChannel( channel = ClassicChannel(
self, manager=self,
connection, connection=connection,
L2CAP_SIGNALING_CID, signaling_cid=L2CAP_SIGNALING_CID,
spec.psm, psm=spec.psm,
source_cid, source_cid=source_cid,
spec.mtu, spec=spec,
) )
connection_channels[source_cid] = channel connection_channels[source_cid] = channel
@@ -2200,7 +2725,27 @@ class ChannelManager:
try: try:
await channel.connect() await channel.connect()
except BaseException as e: except BaseException as e:
del connection_channels[source_cid] connection_channels.pop(source_cid, None)
raise e raise e
return channel return channel
@classmethod
def make_mode_processor(
self,
channel: ClassicChannel,
mode: TransmissionMode,
peer_tx_window_size: int,
peer_max_retransmission: int,
peer_retransmission_timeout: int,
peer_monitor_timeout: int,
peer_mps: int,
) -> Processor:
del peer_retransmission_timeout, peer_monitor_timeout # Unused.
if mode == TransmissionMode.BASIC:
return Processor(channel)
elif mode == TransmissionMode.ENHANCED_RETRANSMISSION:
return EnhancedRetransmissionProcessor(
channel, peer_tx_window_size, peer_max_retransmission, peer_mps
)
raise InvalidArgumentError("Mode %s is not implemented", mode.name)

View File

@@ -533,3 +533,20 @@ class IntConvertible(Protocol):
def __init__(self, value: int) -> None: ... def __init__(self, value: int) -> None: ...
def __int__(self) -> int: ... def __int__(self) -> int: ...
# -----------------------------------------------------------------------------
def crc_16(data: bytes) -> int:
"""Calculate CRC-16-IBM of given data.
Polynomial = x^16 + x^15 + x^2 + 1 = 0x8005 or 0xA001(Reversed)
"""
crc = 0x0000
for byte in data:
crc ^= byte
for _ in range(8):
if (crc & 0x0001) > 0:
crc = (crc >> 1) ^ 0xA001
else:
crc = crc >> 1
return crc

View File

@@ -0,0 +1,107 @@
# Copyright 2021-2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# -----------------------------------------------------------------------------
# Imports
# -----------------------------------------------------------------------------
from __future__ import annotations
import argparse
import asyncio
import sys
import bumble.logging
from bumble import core, l2cap
from bumble.device import Device
from bumble.transport import open_transport
# -----------------------------------------------------------------------------
async def main(
config_file: str, transport: str, mode: int, peer_address: str, psm: int
) -> None:
print('<<< connecting to HCI...')
async with await open_transport(transport) as hci_transport:
print('<<< connected')
# Create a device
device = Device.from_config_file_with_hci(
config_file, hci_transport.source, hci_transport.sink
)
device.classic_enabled = True
device.l2cap_channel_manager.extended_features.add(
l2cap.L2CAP_Information_Request.ExtendedFeatures.ENHANCED_RETRANSMISSION_MODE
)
device.l2cap_channel_manager.extended_features.add(
l2cap.L2CAP_Information_Request.ExtendedFeatures.FCS_OPTION
)
# Start the controller
await device.power_on()
# Start being discoverable and connectable
await device.set_discoverable(True)
await device.set_connectable(True)
active_channel: l2cap.ClassicChannel | None = None
def on_connection(channel: l2cap.ClassicChannel):
def on_sdu(sdu: bytes):
print(f'<<< {sdu.decode()}')
channel.sink = on_sdu
nonlocal active_channel
active_channel = channel
server = device.create_l2cap_server(
spec=l2cap.ClassicChannelSpec(
mode=l2cap.TransmissionMode(mode), psm=psm if psm else None
),
handler=on_connection,
)
print(f'Listen L2CAP on channel {server.psm}')
if peer_address:
connection = await device.connect(
peer_address, transport=core.PhysicalTransport.BR_EDR
)
channel = await connection.create_l2cap_channel(
spec=l2cap.ClassicChannelSpec(
mode=l2cap.TransmissionMode(mode), psm=psm
)
)
active_channel = channel
while sdu := await asyncio.to_thread(lambda: input('>>> ')):
if active_channel:
active_channel.write(sdu.encode())
await hci_transport.source.terminated
# -----------------------------------------------------------------------------
bumble.logging.setup_basic_logging('INFO')
parser = argparse.ArgumentParser()
parser.add_argument('config')
parser.add_argument('transport')
parser.add_argument('-p', '--peer_address', default='')
parser.add_argument(
'-m', '--mode', default=l2cap.TransmissionMode.ENHANCED_RETRANSMISSION
)
parser.add_argument('--psm', default=0)
args = parser.parse_args(sys.argv[1:])
asyncio.run(main(args.config, args.transport, args.mode, args.peer_address, args.psm))

View File

@@ -19,6 +19,7 @@ import asyncio
import logging import logging
import os import os
import random import random
import struct
import pytest import pytest
@@ -342,6 +343,76 @@ async def test_mtu():
assert client_channel.peer_mtu == 345 assert client_channel.peer_mtu == 345
# -----------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_enhanced_retransmission_mode():
devices = TwoDevices()
await devices.setup_connection()
server_channels = asyncio.Queue[l2cap.ClassicChannel]()
server = devices.devices[1].create_l2cap_server(
spec=l2cap.ClassicChannelSpec(
mode=l2cap.TransmissionMode.ENHANCED_RETRANSMISSION
),
handler=server_channels.put_nowait,
)
client_channel = await devices.connections[0].create_l2cap_channel(
spec=l2cap.ClassicChannelSpec(
server.psm, mode=l2cap.TransmissionMode.ENHANCED_RETRANSMISSION
)
)
server_channel = await server_channels.get()
sinks = [asyncio.Queue[bytes]() for _ in range(2)]
server_channel.sink = sinks[0].put_nowait
client_channel.sink = sinks[1].put_nowait
for i in range(128):
server_channel.write(struct.pack('<I', i))
for i in range(128):
assert (await sinks[1].get()) == struct.pack('<I', i)
for i in range(128):
client_channel.write(struct.pack('<I', i))
for i in range(128):
assert (await sinks[0].get()) == struct.pack('<I', i)
# -----------------------------------------------------------------------------
@pytest.mark.parametrize(
'server_mode, client_mode',
[
(l2cap.TransmissionMode.BASIC, l2cap.TransmissionMode.ENHANCED_RETRANSMISSION),
(l2cap.TransmissionMode.ENHANCED_RETRANSMISSION, l2cap.TransmissionMode.BASIC),
],
)
@pytest.mark.asyncio
async def test_mode_mismatching(server_mode, client_mode):
devices = TwoDevices()
await devices.setup_connection()
server = devices.devices[1].create_l2cap_server(
spec=l2cap.ClassicChannelSpec(mode=server_mode)
)
with pytest.raises(l2cap.L2capError):
await devices.connections[0].create_l2cap_channel(
spec=l2cap.ClassicChannelSpec(psm=server.psm, mode=client_mode)
)
# -----------------------------------------------------------------------------
@pytest.mark.parametrize(
'cid, payload, expected',
[
(0x0040, '020000010203040506070809', '0E0040000200000102030405060708093861'),
(0x0040, '0101', '040040000101D414'),
],
)
def test_fcs(cid: int, payload: str, expected: str):
'''Core Spec 6.1, Vol 3, Part A, 3.3.5. Frame Check Sequence.'''
pdu = l2cap.L2CAP_PDU(cid, bytes.fromhex(payload))
assert pdu.to_bytes(with_fcs=True) == bytes.fromhex(expected)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def run(): async def run():
test_helpers() test_helpers()