Files
mcbebop/tests/test_arsdk_framing.py
T
rsp2k 96ab07889d arsdk: discovery, transport, session, and the ported simulator
The transport is threaded and the session puts an async face on it, so
pings and acks are answered whether or not anyone is awaiting a
coroutine. Telemetry is stored per key with a timestamp, because a drone
that has stopped reporting otherwise reads identically to one repeating
itself.

Encoding goes through protocol/codec lazily, so this lands without
waiting for that stream; the sim carries a small encoder of its own for
the events it sends, which also keeps it from agreeing with the client
about a shared mistake.

Ported from bebop-2's sim.py, retargeted at the vendored XML and with
the identity burst now sent per controller attach rather than once per
process.
2026-10-02 00:22:06 -06:00

110 lines
3.9 KiB
Python

"""Framing, with no sockets involved.
These are the cases that cost real debugging time on the live aircraft: a
datagram carrying several frames, a size field that would never advance the
cursor, and the command id being two bytes rather than one.
"""
import struct
import pytest
from mcbebop.arsdk.types import COMMAND_HEADER, FRAME_HEADER, BufferId, DataType, Frame
def test_header_is_seven_bytes():
# Everything about the size field depends on this, including the sim.
assert FRAME_HEADER.size == 7
assert COMMAND_HEADER.size == 4
def test_encode_declares_size_including_header():
frame = Frame(DataType.DATA, BufferId.C2D_NON_ACK, 3, b"abcd")
raw = frame.encode()
data_type, buffer_id, seq, size = FRAME_HEADER.unpack_from(raw)
assert (data_type, buffer_id, seq) == (2, 10, 3)
assert size == len(raw) == 11
def test_round_trip():
frame = Frame(DataType.DATA_WITH_ACK, BufferId.C2D_ACK, 255, bytes(range(16)))
assert Frame.decode_all(frame.encode()) == [frame]
def test_empty_payload_round_trips():
frame = Frame(DataType.ACK, 139, 0, b"")
assert Frame.decode_all(frame.encode()) == [frame]
def test_several_frames_in_one_datagram():
frames = [
Frame(DataType.DATA, BufferId.D2C_NON_ACK, 1, b"\x01\x02"),
Frame(DataType.DATA_WITH_ACK, BufferId.D2C_ACK, 2, b"xyz"),
Frame(DataType.DATA, BufferId.PING, 3, struct.pack("<Q", 9)),
]
assert Frame.decode_all(b"".join(f.encode() for f in frames)) == frames
def test_zero_size_does_not_loop_forever():
# A size below the header length can never advance the cursor. The parse
# has to stop rather than spin, which is why decode_all checks it.
bogus = FRAME_HEADER.pack(2, 126, 0, 0) + b"junk"
assert Frame.decode_all(bogus) == []
def test_size_shorter_than_header_after_a_good_frame():
good = Frame(DataType.DATA, BufferId.D2C_NON_ACK, 7, b"ok")
data = good.encode() + FRAME_HEADER.pack(2, 126, 8, 3)
assert Frame.decode_all(data) == [good]
def test_truncated_frame_is_dropped_not_guessed():
# A frame claiming more than the datagram holds is a short read, and
# half a command decodes into a plausible-looking wrong one.
whole = Frame(DataType.DATA, BufferId.D2C_NON_ACK, 1, b"0123456789").encode()
assert Frame.decode_all(whole[:-4]) == []
def test_trailing_bytes_below_header_length_are_ignored():
good = Frame(DataType.DATA, BufferId.D2C_NON_ACK, 1, b"hi")
assert Frame.decode_all(good.encode() + b"\x02\x7e") == [good]
@pytest.mark.parametrize(
("buffer_id", "ack_buffer"),
[
(BufferId.C2D_ACK, 139), # the one pyparrot hardcodes, so a good check
(BufferId.C2D_NON_ACK, 138),
(BufferId.C2D_HIGH_PRIO, 140),
(BufferId.D2C_ACK, 255), # 127 + 128 lands on the last valid buffer id
(BufferId.PING, 128),
],
)
def test_ack_buffer_is_offset_by_128(buffer_id, ack_buffer):
assert BufferId.ack_for(buffer_id) == ack_buffer
def test_ack_buffer_wraps_rather_than_overflowing():
# 128 upwards would exceed the one-byte field; the modulo is the point.
assert BufferId.ack_for(200) == 72
assert all(0 <= BufferId.ack_for(b) < 256 for b in range(256))
def test_command_id_is_two_bytes():
# ardrone3.MediaStreaming.VideoEnable is (1, 21, 0); the ids that matter
# for this are the ones above 255, which a u8 command field would mangle.
raw = COMMAND_HEADER.pack(1, 21, 0)
assert COMMAND_HEADER.unpack(raw) == (1, 21, 0)
assert COMMAND_HEADER.unpack(COMMAND_HEADER.pack(0, 5, 300)) == (0, 5, 300)
def test_command_payload_sits_inside_a_frame():
payload = COMMAND_HEADER.pack(0, 4, 0) # common.Common.AllStates
frame = Frame(DataType.DATA_WITH_ACK, BufferId.C2D_ACK, 0, payload)
(decoded,) = Frame.decode_all(frame.encode())
assert COMMAND_HEADER.unpack(decoded.payload) == (0, 4, 0)
def test_data_type_numbers_match_the_captures():
assert (DataType.ACK, DataType.DATA, DataType.LOW_LATENCY, DataType.DATA_WITH_ACK) == (1, 2, 3, 4)