Skip to content

Commit 44955d1

Browse files
committed
L2CAP: FCS Implementation
1 parent e186dd5 commit 44955d1

File tree

4 files changed

+136
-39
lines changed

4 files changed

+136
-39
lines changed

bumble/host.py

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,9 @@ async def send_command(command: hci.HCI_Command) -> None:
707707

708708
asyncio.create_task(send_command(command))
709709

710-
def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
710+
def send_l2cap_pdu(
711+
self, connection_handle: int, cid: int, pdu: bytes, with_fcs: bool = False
712+
) -> None:
711713
if not (connection := self.connections.get(connection_handle)):
712714
logger.warning(f'connection 0x{connection_handle:04X} not found')
713715
return
@@ -719,26 +721,23 @@ def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
719721
return
720722

721723
# Create a PDU
722-
l2cap_pdu = bytes(L2CAP_PDU(cid, pdu))
724+
l2cap_pdu = L2CAP_PDU(cid, pdu).to_bytes(with_fcs)
723725

724726
# Send the data to the controller via ACL packets
725-
bytes_remaining = len(l2cap_pdu)
726-
offset = 0
727-
pb_flag = 0
728-
while bytes_remaining:
729-
data_total_length = min(bytes_remaining, packet_queue.max_packet_size)
727+
max_packet_size = packet_queue.max_packet_size
728+
for offset in range(0, len(l2cap_pdu), max_packet_size):
729+
pdu = l2cap_pdu[offset : offset + max_packet_size]
730730
acl_packet = hci.HCI_AclDataPacket(
731731
connection_handle=connection_handle,
732-
pb_flag=pb_flag,
732+
pb_flag=1 if offset > 0 else 0,
733733
bc_flag=0,
734-
data_total_length=data_total_length,
735-
data=l2cap_pdu[offset : offset + data_total_length],
734+
data_total_length=len(pdu),
735+
data=pdu,
736+
)
737+
logger.debug(
738+
'>>> ACL packet enqueue: (Handle=0x%04X) %s', connection_handle, pdu
736739
)
737-
logger.debug(f'>>> ACL packet enqueue: (CID={cid}) {acl_packet}')
738740
packet_queue.enqueue(acl_packet, connection_handle)
739-
pb_flag = 1
740-
offset += data_total_length
741-
bytes_remaining -= data_total_length
742741

743742
def get_data_packet_queue(self, connection_handle: int) -> DataPacketQueue | None:
744743
if connection := self.connections.get(connection_handle):

bumble/l2cap.py

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -219,20 +219,41 @@ class L2CAP_PDU:
219219
See Bluetooth spec @ Vol 3, Part A - 3 DATA PACKET FORMAT
220220
'''
221221

222-
@staticmethod
223-
def from_bytes(data: bytes) -> L2CAP_PDU:
222+
@classmethod
223+
def from_bytes(cls, data: bytes) -> L2CAP_PDU:
224224
# Check parameters
225225
if len(data) < 4:
226226
raise InvalidPacketError('not enough data for L2CAP header')
227227

228-
_, l2cap_pdu_cid = struct.unpack_from('<HH', data, 0)
229-
l2cap_pdu_payload = data[4:]
228+
length, l2cap_pdu_cid = struct.unpack_from('<HH', data, 0)
229+
l2cap_pdu_payload = data[4 : 4 + length]
230230

231-
return L2CAP_PDU(l2cap_pdu_cid, l2cap_pdu_payload)
231+
return cls(l2cap_pdu_cid, l2cap_pdu_payload)
232232

233233
def __bytes__(self) -> bytes:
234-
header = struct.pack('<HH', len(self.payload), self.cid)
235-
return header + self.payload
234+
return self.to_bytes(with_fcs=False)
235+
236+
@classmethod
237+
def crc16(self, data: bytes) -> int:
238+
crc = 0x0000
239+
for byte in data:
240+
crc ^= byte
241+
for _ in range(8):
242+
if (crc & 0x0001) > 0:
243+
crc = (crc >> 1) ^ 0xA001
244+
else:
245+
crc = crc >> 1
246+
return crc
247+
248+
def to_bytes(self, with_fcs: bool = False) -> bytes:
249+
length = len(self.payload)
250+
if with_fcs:
251+
length += 2
252+
header = struct.pack('<HH', length, self.cid)
253+
body = header + self.payload
254+
if with_fcs:
255+
body += struct.pack('<H', self.crc16(body))
256+
return body
236257

237258
def __init__(self, cid: int, payload: bytes) -> None:
238259
self.cid = cid
@@ -1120,6 +1141,7 @@ def __init__(
11201141
self.connection_result = None
11211142
self.disconnection_result = None
11221143
self.sink = None
1144+
self.fcs_enabled = False
11231145
self.spec = spec
11241146
if spec.mode == TransmissionMode.BASIC:
11251147
self.processor = BaseProcessor(self)
@@ -1138,12 +1160,17 @@ def write(self, sdu: bytes) -> None:
11381160
def send_pdu(self, pdu: Union[SupportsBytes, bytes]) -> None:
11391161
if self.state != self.State.OPEN:
11401162
raise InvalidStateError('channel not open')
1141-
self.manager.send_pdu(self.connection, self.destination_cid, pdu)
1163+
self.manager.send_pdu(
1164+
self.connection, self.destination_cid, pdu, self.fcs_enabled
1165+
)
11421166

11431167
def send_control_frame(self, frame: L2CAP_Control_Frame) -> None:
11441168
self.manager.send_control_frame(self.connection, self.signaling_cid, frame)
11451169

11461170
def on_pdu(self, pdu: bytes) -> None:
1171+
if self.fcs_enabled:
1172+
# Drop FCS.
1173+
pdu = pdu[:-2]
11471174
self.processor.on_pdu(pdu)
11481175

11491176
def on_sdu(self, sdu: bytes) -> None:
@@ -1308,7 +1335,12 @@ def on_configure_request(self, request: L2CAP_Configure_Request) -> None:
13081335
peer_mps,
13091336
) = struct.unpack_from('<BBBHHH', option[1])
13101337
logger.debug(
1311-
'Peer requests Retransmission or Flow Control: mode=%s, tx_window_size=%s, retransmission_timeout=%s, monitor_timeout=%s, mps=%s',
1338+
'Peer requests Retransmission or Flow Control: mode=%s,'
1339+
' tx_window_size=%s,'
1340+
' max_retransmission=%s,'
1341+
' retransmission_timeout=%s,'
1342+
' monitor_timeout=%s,'
1343+
' mps=%s',
13121344
TransmissionMode(mode).name,
13131345
peer_tx_window_size,
13141346
peer_max_retransmission,
@@ -1330,8 +1362,7 @@ def on_configure_request(self, request: L2CAP_Configure_Request) -> None:
13301362
else:
13311363
logger.error("Enhanced Retransmission Mode is not enabled")
13321364
result = L2CAP_Configure_Response.Result.FAILURE_REJECTED
1333-
replied_options.clear()
1334-
replied_options.append(option)
1365+
replied_options = [option]
13351366
break
13361367
else:
13371368
logger.error(
@@ -1340,8 +1371,23 @@ def on_configure_request(self, request: L2CAP_Configure_Request) -> None:
13401371
result = (
13411372
L2CAP_Configure_Response.Result.FAILURE_UNACCEPTABLE_PARAMETERS
13421373
)
1343-
replied_options.clear()
1374+
replied_options = [option]
1375+
break
1376+
elif option[0] == L2CAP_Configure_Request.ParameterType.FCS:
1377+
enabled = option[1][0] != 0
1378+
logger.debug("Peer requests FCS: %s", enabled)
1379+
if (
1380+
L2CAP_Information_Request.ExtendedFeatures.FCS_OPTION
1381+
in self.manager.extended_features
1382+
):
1383+
self.fcs_enabled = enabled
13441384
replied_options.append(option)
1385+
else:
1386+
logger.error("Frame Check Sequence is not supported")
1387+
result = (
1388+
L2CAP_Configure_Response.Result.FAILURE_UNACCEPTABLE_PARAMETERS
1389+
)
1390+
replied_options = [option]
13451391
break
13461392
else:
13471393
logger.debug(
@@ -1350,8 +1396,7 @@ def on_configure_request(self, request: L2CAP_Configure_Request) -> None:
13501396
option[1].hex(),
13511397
)
13521398
result = L2CAP_Configure_Response.Result.FAILURE_UNKNOWN_OPTIONS
1353-
replied_options.clear()
1354-
replied_options.append(option)
1399+
replied_options = [option]
13551400
break
13561401

13571402
self.send_control_frame(
@@ -2061,15 +2106,21 @@ def on_disconnection(self, connection_handle: int, _reason: int) -> None:
20612106
if connection_handle in self.identifiers:
20622107
del self.identifiers[connection_handle]
20632108

2064-
def send_pdu(self, connection, cid: int, pdu: Union[SupportsBytes, bytes]) -> None:
2109+
def send_pdu(
2110+
self,
2111+
connection,
2112+
cid: int,
2113+
pdu: Union[SupportsBytes, bytes],
2114+
with_fcs: bool = False,
2115+
) -> None:
20652116
pdu_str = pdu.hex() if isinstance(pdu, bytes) else str(pdu)
20662117
pdu_bytes = bytes(pdu)
20672118
logger.debug(
20682119
f'{color(">>> Sending L2CAP PDU", "blue")} '
20692120
f'on connection [0x{connection.handle:04X}] (CID={cid}) '
20702121
f'{connection.peer_address}: {len(pdu_bytes)} bytes, {pdu_str}'
20712122
)
2072-
self.host.send_l2cap_pdu(connection.handle, cid, pdu_bytes)
2123+
self.host.send_l2cap_pdu(connection.handle, cid, pdu_bytes, with_fcs)
20732124

20742125
def on_pdu(self, connection: Connection, cid: int, pdu: bytes) -> None:
20752126
if cid in (L2CAP_SIGNALING_CID, L2CAP_LE_SIGNALING_CID):

examples/run_l2cap_server.py renamed to examples/run_classic_l2cap.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,35 @@
1717
# -----------------------------------------------------------------------------
1818
import asyncio
1919
import sys
20-
21-
from typing import Optional
20+
import argparse
2221

2322
import bumble.logging
24-
from bumble.device import Device
23+
from bumble import core
2524
from bumble import l2cap
25+
from bumble.device import Device
2626
from bumble.transport import open_transport
2727

2828

2929
# -----------------------------------------------------------------------------
30-
async def main() -> None:
30+
async def main(
31+
config_file: str, transport: str, mode: int, peer_address: str, psm: int
32+
) -> None:
3133

3234
print('<<< connecting to HCI...')
33-
async with await open_transport(sys.argv[2]) as hci_transport:
35+
async with await open_transport(transport) as hci_transport:
3436
print('<<< connected')
3537

3638
# Create a device
3739
device = Device.from_config_file_with_hci(
38-
sys.argv[1], hci_transport.source, hci_transport.sink
40+
config_file, hci_transport.source, hci_transport.sink
3941
)
4042
device.classic_enabled = True
4143
device.l2cap_channel_manager.extended_features.add(
4244
l2cap.L2CAP_Information_Request.ExtendedFeatures.ENHANCED_RETRANSMISSION_MODE
4345
)
46+
device.l2cap_channel_manager.extended_features.add(
47+
l2cap.L2CAP_Information_Request.ExtendedFeatures.FCS_OPTION
48+
)
4449

4550
# Start the controller
4651
await device.power_on()
@@ -63,12 +68,22 @@ def on_sdu(sdu: bytes):
6368

6469
server = device.create_l2cap_server(
6570
spec=l2cap.ClassicChannelSpec(
66-
mode=l2cap.TransmissionMode.ENHANCED_RETRANSMISSION
71+
mode=l2cap.TransmissionMode(mode), psm=psm if psm else None
6772
),
6873
handler=on_connection,
6974
)
7075
print(f'Listen L2CAP on channel {server.psm}')
7176

77+
if peer_address:
78+
connection = await device.connect(
79+
peer_address, transport=core.PhysicalTransport.BR_EDR
80+
)
81+
connection.create_l2cap_channel(
82+
spec=l2cap.ClassicChannelSpec(
83+
mode=l2cap.TransmissionMode(mode), psm=psm
84+
)
85+
)
86+
7287
while sdu := await asyncio.to_thread(lambda: input('>>> ')):
7388
if channels:
7489
channels[0].write(sdu.encode())
@@ -78,4 +93,13 @@ def on_sdu(sdu: bytes):
7893

7994
# -----------------------------------------------------------------------------
8095
bumble.logging.setup_basic_logging('INFO')
81-
asyncio.run(main())
96+
parser = argparse.ArgumentParser()
97+
parser.add_argument('config')
98+
parser.add_argument('transport')
99+
parser.add_argument('-p', '--peer_address', default='')
100+
parser.add_argument(
101+
'-m', '--mode', default=l2cap.TransmissionMode.ENHANCED_RETRANSMISSION
102+
)
103+
parser.add_argument('--psm', default=0)
104+
args = parser.parse_args(sys.argv[1:])
105+
asyncio.run(main(args.config, args.transport, args.mode, args.peer_address, args.psm))

tests/l2cap_test.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,10 +347,19 @@ def on_channel(channel):
347347
async def test_enhanced_retransmission_channel():
348348
devices = TwoDevices()
349349
await devices.setup_connection()
350+
devices[0].l2cap_channel_manager.extended_features.add(
351+
l2cap.L2CAP_Information_Request.ExtendedFeatures.ENHANCED_RETRANSMISSION_MODE
352+
)
353+
devices[1].l2cap_channel_manager.extended_features.add(
354+
l2cap.L2CAP_Information_Request.ExtendedFeatures.ENHANCED_RETRANSMISSION_MODE
355+
)
350356

351357
server_channels = asyncio.Queue[l2cap.ClassicChannel]()
352358
server = devices.devices[1].create_l2cap_server(
353-
spec=l2cap.ClassicChannelSpec(), handler=server_channels.put_nowait
359+
spec=l2cap.ClassicChannelSpec(
360+
mode=l2cap.TransmissionMode.ENHANCED_RETRANSMISSION
361+
),
362+
handler=server_channels.put_nowait,
354363
)
355364
client_channel = await devices.connections[0].create_l2cap_channel(
356365
spec=l2cap.ClassicChannelSpec(
@@ -375,6 +384,20 @@ async def test_enhanced_retransmission_channel():
375384
assert (await sinks[0].get()) == b'456'
376385

377386

387+
# -----------------------------------------------------------------------------
388+
@pytest.mark.parametrize(
389+
'cid, payload, expected',
390+
[
391+
(0x0040, '020000010203040506070809', '0E0040000200000102030405060708093861'),
392+
(0x0040, '0101', '040040000101D414'),
393+
],
394+
)
395+
def test_fcs(cid: int, payload: str, expected: str):
396+
'''Core Spec 6.1, Vol 3, Part A, 3.3.5. Frame Check Sequence.'''
397+
pdu = l2cap.L2CAP_PDU(cid, bytes.fromhex(payload))
398+
assert pdu.to_bytes(with_fcs=True) == bytes.fromhex(expected)
399+
400+
378401
# -----------------------------------------------------------------------------
379402
async def run():
380403
test_helpers()

0 commit comments

Comments
 (0)