forked from auracaster/bumble_mirror
L2CAP: FCS Implementation
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
341
bumble/l2cap.py
341
bumble/l2cap.py
@@ -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,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:
|
||||||
@@ -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,69 +1339,93 @@ 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]:
|
||||||
self.peer_mtu = struct.unpack('<H', option[1])[0]
|
case L2CAP_Configure_Request.ParameterType.MTU:
|
||||||
logger.debug('Peer MTU = %d', self.peer_mtu)
|
self.peer_mtu = struct.unpack('<H', option[1])[0]
|
||||||
replied_options.append(option)
|
logger.debug('Peer MTU = %d', self.peer_mtu)
|
||||||
elif (
|
|
||||||
option[0]
|
|
||||||
== 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])
|
|
||||||
logger.debug(
|
|
||||||
'Peer requests Retransmission or Flow Control: mode=%s, tx_window_size=%s, retransmission_timeout=%s, monitor_timeout=%s, mps=%s',
|
|
||||||
TransmissionMode(mode).name,
|
|
||||||
peer_tx_window_size,
|
|
||||||
peer_max_retransmission,
|
|
||||||
peer_retransmission_timeout,
|
|
||||||
peer_monitor_timeout,
|
|
||||||
peer_mps,
|
|
||||||
)
|
|
||||||
if mode == TransmissionMode.BASIC:
|
|
||||||
self.processor = BaseProcessor(self)
|
|
||||||
replied_options.append(option)
|
replied_options.append(option)
|
||||||
elif mode == TransmissionMode.ENHANCED_RETRANSMISSION:
|
case (
|
||||||
if (
|
L2CAP_Configure_Request.ParameterType.RETRANSMISSION_AND_FLOW_CONTROL
|
||||||
L2CAP_Information_Request.ExtendedFeatures.ENHANCED_RETRANSMISSION_MODE
|
):
|
||||||
in self.manager.extended_features
|
(
|
||||||
):
|
mode,
|
||||||
self.processor = EnhancedRetransmissionProcessor(self)
|
peer_tx_window_size,
|
||||||
self.processor.peer_mps = peer_mps
|
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)
|
replied_options.append(option)
|
||||||
else:
|
else:
|
||||||
logger.error("Enhanced Retransmission Mode is not enabled")
|
logger.error("Mode %s is not supported", new_mode.name)
|
||||||
result = L2CAP_Configure_Response.Result.FAILURE_REJECTED
|
self._abort_connection_result(
|
||||||
replied_options.clear()
|
'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)
|
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
|
break
|
||||||
else:
|
case _:
|
||||||
logger.error(
|
logger.debug(
|
||||||
"Mode %s is not supported", TransmissionMode(mode).name
|
"Reject unimplemented option %s[%s]",
|
||||||
|
option[0].name,
|
||||||
|
option[1].hex(),
|
||||||
)
|
)
|
||||||
result = (
|
result = L2CAP_Configure_Response.Result.FAILURE_UNKNOWN_OPTIONS
|
||||||
L2CAP_Configure_Response.Result.FAILURE_UNACCEPTABLE_PARAMETERS
|
replied_options = [option]
|
||||||
)
|
|
||||||
replied_options.clear()
|
|
||||||
replied_options.append(option)
|
|
||||||
break
|
break
|
||||||
else:
|
|
||||||
logger.debug(
|
|
||||||
"Reject unimplemented option %s[%s]",
|
|
||||||
option[0].name,
|
|
||||||
option[1].hex(),
|
|
||||||
)
|
|
||||||
result = L2CAP_Configure_Response.Result.FAILURE_UNKNOWN_OPTIONS
|
|
||||||
replied_options.clear()
|
|
||||||
replied_options.append(option)
|
|
||||||
break
|
|
||||||
|
|
||||||
self.send_control_frame(
|
self.send_control_frame(
|
||||||
L2CAP_Configure_Response(
|
L2CAP_Configure_Response(
|
||||||
@@ -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,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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user