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.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user