L2CAP: FCS Implementation

This commit is contained in:
Josh Wu
2025-09-26 21:59:56 +08:00
parent 57e05781ad
commit 456cb59b48
6 changed files with 335 additions and 170 deletions

View File

@@ -2071,6 +2071,8 @@ class DeviceConfiguration:
enhanced_retransmission_supported: bool = False enhanced_retransmission_supported: bool = False
l2cap_extended_features: Sequence[int] = ( l2cap_extended_features: Sequence[int] = (
l2cap.L2CAP_Information_Request.ExtendedFeatures.FIXED_CHANNELS, 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:

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,8 @@ 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,
Callable,
ClassVar,
Iterable,
Optional,
SupportsBytes,
TypeVar,
Union,
)
from typing_extensions import override from typing_extensions import override
@@ -73,8 +63,8 @@ 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 L2CAP_DEFAULT_MPS = 1010 # Default value for the MPS we are willing to accept
DEFAULT_TX_WINDOW_SIZE = 10 DEFAULT_TX_WINDOW_SIZE = 63
DEFAULT_MAX_RETRANSMISSION = 10 DEFAULT_MAX_RETRANSMISSION = 1
DEFAULT_RETRANSMISSION_TIMEOUT = 2.0 DEFAULT_RETRANSMISSION_TIMEOUT = 2.0
DEFAULT_MONITOR_TIMEOUT = 12.0 DEFAULT_MONITOR_TIMEOUT = 12.0
@@ -160,6 +150,11 @@ class TransmissionMode(utils.OpenIntEnum):
# 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. '''Spec of L2CAP Channel over Classic Transport.
@@ -177,16 +172,18 @@ class ClassicChannelSpec:
monitor_timeout: The interval at which S-frames should be transmitted on the monitor_timeout: The interval at which S-frames should be transmitted on the
return channel when no frames are received on the forward channel. return channel when no frames are received on the forward channel.
mode: The transmission mode to use. 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_MTU mps: int = L2CAP_DEFAULT_MPS
tx_window_size: int = DEFAULT_TX_WINDOW_SIZE tx_window_size: int = DEFAULT_TX_WINDOW_SIZE
max_retransmission: int = DEFAULT_MAX_RETRANSMISSION max_retransmission: int = DEFAULT_MAX_RETRANSMISSION
retransmission_timeout: float = DEFAULT_RETRANSMISSION_TIMEOUT retransmission_timeout: float = DEFAULT_RETRANSMISSION_TIMEOUT
monitor_timeout: float = DEFAULT_MONITOR_TIMEOUT monitor_timeout: float = DEFAULT_MONITOR_TIMEOUT
mode: TransmissionMode = TransmissionMode.BASIC mode: TransmissionMode = TransmissionMode.BASIC
fcs_enabled: bool = False
@dataclasses.dataclass @dataclasses.dataclass
@@ -219,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
@@ -865,7 +871,7 @@ class L2CAP_Credit_Based_Reconfigure_Response(L2CAP_Control_Frame):
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class BaseProcessor: class Processor:
def __init__(self, channel: ClassicChannel) -> None: def __init__(self, channel: ClassicChannel) -> None:
self.channel = channel self.channel = channel
@@ -877,7 +883,7 @@ class BaseProcessor:
# TODO: Handle retransmission # TODO: Handle retransmission
class EnhancedRetransmissionProcessor(BaseProcessor): class EnhancedRetransmissionProcessor(Processor):
MAX_SEQ_NUM = 64 MAX_SEQ_NUM = 64
@@ -916,24 +922,30 @@ class EnhancedRetransmissionProcessor(BaseProcessor):
@classmethod @classmethod
def _num_frames_between(cls, low: int, high: int) -> int: def _num_frames_between(cls, low: int, high: int) -> int:
if high < low: if high < low:
high += cls.MAX_SEQ_NUM + 1 high += cls.MAX_SEQ_NUM
return high - low return high - low
def __init__(self, channel: ClassicChannel): 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 spec = channel.spec
self.mps = spec.mps self.mps = spec.mps
self.peer_mps = 0 self.peer_mps = peer_mps
self.tx_window_size = spec.tx_window_size self.peer_tx_window_size = peer_tx_window_size
self._pending_pdus = [] self._pending_pdus = []
self.monitor_timeout = spec.monitor_timeout self.monitor_timeout = spec.monitor_timeout
self.channel = channel self.channel = channel
self.retransmission_timeout = spec.retransmission_timeout self.retransmission_timeout = spec.retransmission_timeout
self.max_retransmission = spec.max_retransmission self.peer_max_retransmission = peer_max_retransmission
def _monitor(self) -> None: def _monitor(self) -> None:
if ( if (
self.max_retransmission <= 0 self.peer_max_retransmission <= 0
or self._num_receiver_ready_polls_sent < self.max_retransmission or self._num_receiver_ready_polls_sent < self.peer_max_retransmission
): ):
self._send_receiver_ready_poll() self._send_receiver_ready_poll()
self._start_monitor() self._start_monitor()
@@ -1028,7 +1040,7 @@ class EnhancedRetransmissionProcessor(BaseProcessor):
return return
for pdu in self._pending_pdus: for pdu in self._pending_pdus:
if self._num_unacked_frames >= self.tx_window_size: if self._num_unacked_frames >= self.peer_tx_window_size:
return return
self._send_pdu(pdu) self._send_pdu(pdu)
self._last_tx_seq = pdu.tx_seq self._last_tx_seq = pdu.tx_seq
@@ -1107,7 +1119,7 @@ class ClassicChannel(utils.EventEmitter):
connection: Connection connection: Connection
mtu: int mtu: int
peer_mtu: int peer_mtu: int
processor: BaseProcessor processor: Processor
def __init__( def __init__(
self, self,
@@ -1131,12 +1143,15 @@ 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.spec = spec
if spec.mode == TransmissionMode.BASIC: self.mode = spec.mode
self.processor = BaseProcessor(self) # Configure mode-specific processor later on configure request.
elif spec.mode == TransmissionMode.ENHANCED_RETRANSMISSION: self.processor = Processor(self)
self.processor = EnhancedRetransmissionProcessor(self) if self.mode not in (
else: TransmissionMode.BASIC,
TransmissionMode.ENHANCED_RETRANSMISSION,
):
raise InvalidArgumentError(f"Mode {spec.mode} is not supported") 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:
@@ -1149,12 +1164,17 @@ class ClassicChannel(utils.EventEmitter):
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) self.processor.on_pdu(pdu)
def on_sdu(self, sdu: bytes) -> None: def on_sdu(self, sdu: bytes) -> None:
@@ -1193,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(
@@ -1209,6 +1227,20 @@ 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()
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 return await self.disconnection_result
def abort(self) -> None: def abort(self) -> None:
@@ -1223,21 +1255,28 @@ class ClassicChannel(utils.EventEmitter):
struct.pack('<H', self.mtu), struct.pack('<H', self.mtu),
) )
] ]
if isinstance(self.processor, EnhancedRetransmissionProcessor): if self.mode == TransmissionMode.ENHANCED_RETRANSMISSION:
options.append( options.append(
( (
L2CAP_Configure_Request.ParameterType.RETRANSMISSION_AND_FLOW_CONTROL, L2CAP_Configure_Request.ParameterType.RETRANSMISSION_AND_FLOW_CONTROL,
struct.pack( struct.pack(
'<BBBHHH', '<BBBHHH',
TransmissionMode.ENHANCED_RETRANSMISSION, TransmissionMode.ENHANCED_RETRANSMISSION,
self.processor.tx_window_size, self.spec.tx_window_size,
self.processor.max_retransmission, self.spec.max_retransmission,
int(self.processor.retransmission_timeout * 1000), int(self.spec.retransmission_timeout * 1000),
int(self.processor.monitor_timeout * 1000), int(self.spec.monitor_timeout * 1000),
self.processor.mps, 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),
@@ -1279,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,
) )
) )
@@ -1301,14 +1339,15 @@ class ClassicChannel(utils.EventEmitter):
# Result to options # Result to options
replied_options = list[tuple[int, bytes]]() replied_options = list[tuple[int, bytes]]()
result = L2CAP_Configure_Response.Result.SUCCESS result = L2CAP_Configure_Response.Result.SUCCESS
new_mode = TransmissionMode.BASIC
for option in options: for option in options:
if option[0] == L2CAP_Configure_Request.ParameterType.MTU: match option[0]:
case L2CAP_Configure_Request.ParameterType.MTU:
self.peer_mtu = struct.unpack('<H', option[1])[0] self.peer_mtu = struct.unpack('<H', option[1])[0]
logger.debug('Peer MTU = %d', self.peer_mtu) logger.debug('Peer MTU = %d', self.peer_mtu)
replied_options.append(option) replied_options.append(option)
elif ( case (
option[0] L2CAP_Configure_Request.ParameterType.RETRANSMISSION_AND_FLOW_CONTROL
== L2CAP_Configure_Request.ParameterType.RETRANSMISSION_AND_FLOW_CONTROL
): ):
( (
mode, mode,
@@ -1318,51 +1357,74 @@ class ClassicChannel(utils.EventEmitter):
peer_monitor_timeout, peer_monitor_timeout,
peer_mps, peer_mps,
) = struct.unpack_from('<BBBHHH', option[1]) ) = struct.unpack_from('<BBBHHH', option[1])
new_mode = TransmissionMode(mode)
logger.debug( logger.debug(
'Peer requests Retransmission or Flow Control: mode=%s, tx_window_size=%s, retransmission_timeout=%s, monitor_timeout=%s, mps=%s', 'Peer requests Retransmission or Flow Control: mode=%s,'
TransmissionMode(mode).name, ' tx_window_size=%s,'
' max_retransmission=%s,'
' retransmission_timeout=%s,'
' monitor_timeout=%s,'
' mps=%s',
new_mode.name,
peer_tx_window_size, peer_tx_window_size,
peer_max_retransmission, peer_max_retransmission,
peer_retransmission_timeout, peer_retransmission_timeout,
peer_monitor_timeout, peer_monitor_timeout,
peer_mps, peer_mps,
) )
if mode == TransmissionMode.BASIC: if new_mode != self.mode:
self.processor = BaseProcessor(self) 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) replied_options.append(option)
elif mode == TransmissionMode.ENHANCED_RETRANSMISSION: 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 ( if (
L2CAP_Information_Request.ExtendedFeatures.ENHANCED_RETRANSMISSION_MODE L2CAP_Information_Request.ExtendedFeatures.FCS_OPTION
in self.manager.extended_features in self.manager.extended_features
): ):
self.processor = EnhancedRetransmissionProcessor(self) self.fcs_enabled = enabled
self.processor.peer_mps = peer_mps
replied_options.append(option) replied_options.append(option)
else: else:
logger.error("Enhanced Retransmission Mode is not enabled") logger.error("Frame Check Sequence is not supported")
result = L2CAP_Configure_Response.Result.FAILURE_REJECTED
replied_options.clear()
replied_options.append(option)
break
else:
logger.error(
"Mode %s is not supported", TransmissionMode(mode).name
)
result = ( result = (
L2CAP_Configure_Response.Result.FAILURE_UNACCEPTABLE_PARAMETERS L2CAP_Configure_Response.Result.FAILURE_UNACCEPTABLE_PARAMETERS
) )
replied_options.clear() replied_options = [option]
replied_options.append(option)
break break
else: case _:
logger.debug( logger.debug(
"Reject unimplemented option %s[%s]", "Reject unimplemented option %s[%s]",
option[0].name, option[0].name,
option[1].hex(), option[1].hex(),
) )
result = L2CAP_Configure_Response.Result.FAILURE_UNKNOWN_OPTIONS result = L2CAP_Configure_Response.Result.FAILURE_UNKNOWN_OPTIONS
replied_options.clear() replied_options = [option]
replied_options.append(option)
break break
self.send_control_frame( self.send_control_frame(
@@ -1376,6 +1438,8 @@ class ClassicChannel(utils.EventEmitter):
), ),
) )
) )
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)
@@ -1429,7 +1493,6 @@ 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,
@@ -1437,17 +1500,12 @@ class ClassicChannel(utils.EventEmitter):
source_cid=request.source_cid, source_cid=request.source_cid,
) )
) )
self._abort_connection_result()
self._change_state(self.State.CLOSED) self._change_state(self.State.CLOSED)
self.emit(self.EVENT_CLOSE) self.emit(self.EVENT_CLOSE)
self.manager.on_channel_closed(self) self.manager.on_channel_closed(self)
else:
logger.warning(color('invalid state', 'red'))
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
@@ -1702,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,
@@ -2075,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(
@@ -2083,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):
@@ -2660,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

@@ -12,35 +12,42 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Imports # Imports
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
from __future__ import annotations
import argparse
import asyncio import asyncio
import sys import sys
from typing import Optional
import bumble.logging import bumble.logging
from bumble import core, l2cap
from bumble.device import Device from bumble.device import Device
from bumble import l2cap
from bumble.transport import open_transport from bumble.transport import open_transport
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
async def main() -> None: async def main(
config_file: str, transport: str, mode: int, peer_address: str, psm: int
) -> None:
print('<<< connecting to HCI...') print('<<< connecting to HCI...')
async with await open_transport(sys.argv[2]) as hci_transport: async with await open_transport(transport) as hci_transport:
print('<<< connected') print('<<< connected')
# Create a device # Create a device
device = Device.from_config_file_with_hci( device = Device.from_config_file_with_hci(
sys.argv[1], hci_transport.source, hci_transport.sink config_file, hci_transport.source, hci_transport.sink
) )
device.classic_enabled = True device.classic_enabled = True
device.l2cap_channel_manager.extended_features.add( device.l2cap_channel_manager.extended_features.add(
l2cap.L2CAP_Information_Request.ExtendedFeatures.ENHANCED_RETRANSMISSION_MODE 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 # Start the controller
await device.power_on() await device.power_on()
@@ -49,7 +56,7 @@ async def main() -> None:
await device.set_discoverable(True) await device.set_discoverable(True)
await device.set_connectable(True) await device.set_connectable(True)
channels: list[l2cap.ClassicChannel] = [] active_channel: l2cap.ClassicChannel | None = None
def on_connection(channel: l2cap.ClassicChannel): def on_connection(channel: l2cap.ClassicChannel):
@@ -57,25 +64,44 @@ async def main() -> None:
print(f'<<< {sdu.decode()}') print(f'<<< {sdu.decode()}')
channel.sink = on_sdu channel.sink = on_sdu
if channels: nonlocal active_channel
channels.clear() active_channel = channel
channels.append(channel)
server = device.create_l2cap_server( server = device.create_l2cap_server(
spec=l2cap.ClassicChannelSpec( spec=l2cap.ClassicChannelSpec(
mode=l2cap.TransmissionMode.ENHANCED_RETRANSMISSION mode=l2cap.TransmissionMode(mode), psm=psm if psm else None
), ),
handler=on_connection, handler=on_connection,
) )
print(f'Listen L2CAP on channel {server.psm}') 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('>>> ')): while sdu := await asyncio.to_thread(lambda: input('>>> ')):
if channels: if active_channel:
channels[0].write(sdu.encode()) active_channel.write(sdu.encode())
await hci_transport.source.terminated await hci_transport.source.terminated
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
bumble.logging.setup_basic_logging('INFO') bumble.logging.setup_basic_logging('INFO')
asyncio.run(main()) 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
@@ -344,13 +345,16 @@ async def test_mtu():
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_enhanced_retransmission_channel(): async def test_enhanced_retransmission_mode():
devices = TwoDevices() devices = TwoDevices()
await devices.setup_connection() await devices.setup_connection()
server_channels = asyncio.Queue[l2cap.ClassicChannel]() server_channels = asyncio.Queue[l2cap.ClassicChannel]()
server = devices.devices[1].create_l2cap_server( server = devices.devices[1].create_l2cap_server(
spec=l2cap.ClassicChannelSpec(), handler=server_channels.put_nowait spec=l2cap.ClassicChannelSpec(
mode=l2cap.TransmissionMode.ENHANCED_RETRANSMISSION
),
handler=server_channels.put_nowait,
) )
client_channel = await devices.connections[0].create_l2cap_channel( client_channel = await devices.connections[0].create_l2cap_channel(
spec=l2cap.ClassicChannelSpec( spec=l2cap.ClassicChannelSpec(
@@ -358,21 +362,55 @@ async def test_enhanced_retransmission_channel():
) )
) )
server_channel = await server_channels.get() server_channel = await server_channels.get()
assert isinstance(client_channel.processor, l2cap.EnhancedRetransmissionProcessor)
assert isinstance(server_channel.processor, l2cap.EnhancedRetransmissionProcessor)
sinks = [asyncio.Queue[bytes]() for _ in range(2)] sinks = [asyncio.Queue[bytes]() for _ in range(2)]
server_channel.sink = sinks[0].put_nowait server_channel.sink = sinks[0].put_nowait
client_channel.sink = sinks[1].put_nowait client_channel.sink = sinks[1].put_nowait
for _ in range(128): for i in range(128):
server_channel.write(b'123') server_channel.write(struct.pack('<I', i))
for _ in range(128): for i in range(128):
assert (await sinks[1].get()) == b'123' assert (await sinks[1].get()) == struct.pack('<I', i)
for _ in range(128): for i in range(128):
client_channel.write(b'456') client_channel.write(struct.pack('<I', i))
for _ in range(128): for i in range(128):
assert (await sinks[0].get()) == b'456' 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)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------