Acknowledge on data type alone, never on buffer id. A live Bebop 2 sends
DATA_WITH_ACK on buffer 126 and plain DATA on 127, the opposite of what the
buffer names imply, so requiring both to agree meant nothing was ever
acknowledged. The drone resent its state instead of continuing: 35 telemetry
keys where there should be 192, and a connect that took 4 s instead of 1.7.
Record argument-less events. Eight events carry no arguments and their
arrival is the whole message, including AllStatesChanged and
AllSettingsChanged, which mark the end of a state dump. Keying only by
argument decoded them to an empty dict and lost them.
Wait for those terminators instead of a quiet period. The drone streams
attitude at about 5 Hz throughout, so the link is never quiet and the wait
always ran to its timeout with a partial burst.
Also: preflight no longer reports ready when a blocking check has no data.
It answered 'ready' on an aircraft it knew almost nothing about, which is
worse than refusing to answer.
connect/disconnect/status, list_commands/command_info, the generic
send_command, get_state/watch_state/preflight_check, camera snapshot and
record, FTP listing and fetch, the read-only shell, and arm/disarm.
The safety gate is tested exhaustively rather than by sample: every motion
and envelope command is asserted to refuse while unarmed, and Landing and
Emergency are asserted to work without arming, because refusing to bring
down an airborne aircraft is the more dangerous answer.
preflight_check reads the per-element sensor keys, so a single failing
self-test is visible instead of being hidden by whichever arrived last.
17 events are MAP_ITEM or LIST_ITEM: they arrive once per element, all
carrying the same argument names, so a flat store kept only whichever landed
last. SensorsStatesListChanged reports six sensors that way, and the
simulator's deliberate magnetometer fault was invisible through state() as a
result, which is exactly the fact preflight_check needs to see.
Entries are now keyed Command[element]_arg. Parrot marks these events but
never names the key; it is the first argument by convention.
76 tests. Implements the protocol directly rather than through pyparrot,
whose receive thread prints to stdout and would corrupt JSON-RPC.
Two corrections to the observed notes, both adopted: the emergency buffer
carries DATA_WITH_ACK with unlimited retries in libARController rather than
the low-latency type pyparrot sends, and fire-and-forget is the wrong
property for the command that cuts the motors; and the handshake reply must
be read until its NUL terminator rather than from a single recv, because it
has grown across firmware versions.
decode_event raises on an id triple the XML does not carry, and the XML
is missing at least FlatTrim, so this happens on a real aircraft. The
receive loop already counted and carried on; now something checks it.
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.
116 tests. Video is the port of what was proven against the aircraft: no
RTSP anywhere, SDP describing our own port, ffmpeg bound before the stream
is enabled, -c copy for recording.
Changes the stream made and justified: the video session takes a sender
callback rather than owning a connection, so media/ imports nothing from
arsdk or protocol; blocking subprocess waits moved off the event loop;
exposure is restored even when entering the session fails.
FTP exposes media, flightplans and logs. Port 51 is the firmware-write
channel and is deliberately unreachable. The shell is an allow-list of
command names with shell metacharacters refused outright, because the
drone's telnet login is an unauthenticated root shell.
Ports bebop-2's working video.py, which was verified against the live
aircraft. Firmware 4.7.1 serves no RTSP, so there is no URL and no RTSP
path here: we describe our own receiving port in an SDP and let ffmpeg
bind it before VideoEnable goes out, because RTP is connectionless and
packets that arrive before the sink is listening are gone.
Three changes on top of the port. StreamSession no longer owns the drone
link, since the tools layer holds a long-lived session; it takes an async
sender callable instead, which also keeps media/ loadable while arsdk/
and protocol/ are still being written. The context manager is async for
the same reason, with the blocking subprocess waits moved off the event
loop. And downscale() shrinks a frame before it reaches a model, because
a full 856x480 is most of a context window spent on pixels nobody asked
for.
FTP exposes media (21), flightplans (61) and logs (21, scoped to the
Debug tree). Port 51 is deliberately absent: it serves /update as root,
it is how firmware is pushed, it has no read use case, and the drone's
Wi-Fi is open. Fetches stream to capture_dir under a size cap so a 1080p
recording cannot be pulled into a tool result.
The shell is an allow-list of eleven read-only command names rather than
a deny-list, because the login is `exec /bin/sh -l` with no password and
deny-lists on shells leak. Arguments carrying shell metacharacters are
refused before the socket opens. Output is bracketed between two echoed
nonce markers rather than trimmed by prompt pattern, since telnetd's pty
echoes our input with the prompt glued to the front and sends all of it
before anything runs.
90 tests. Verified against real data from the aircraft: VideoEnable encodes
to the bytes we actually sent, enums encode as 4-byte indices (pyparrot read
one byte and misaligned everything after), and events decode to the same
<Command>_<arg> keys our captures use.
Two counts in the brief were wrong and the stream corrected them with tests:
31 commands state neither result nor triggered (not 33), and 39 give no
answer on Bebop 2 support (not 32).
uv_build packages only src/mcbebop, so arsdk-xml/ at the repo root was
absent from the wheel and every install would have failed to find the
command definitions. Caught during the protocol build.
Parse Parrot's common.xml and ardrone3.xml into CommandSpec objects rather
than transcribing 264 commands by hand. Direction is derived from
comment result=/triggered= with a class-name-suffix fallback for the 31
commands that state neither, giving 101 to-drone and 163 from-drone.
ardrone3.Piloting.FlatTrim is injected: the firmware accepts it but neither
the vendored snapshot nor upstream defines it.
codec encodes the <BBH header (the command id is 16 bits, not 8) and reads
enums as i32 and strings as NUL-terminated, both of which pyparrot gets
wrong. safety assigns the arming tier by class, with Mavlink and Calibration
counted as motion because they move the aircraft. expectations parses the
undocumented #p-c-m grammar including | alternatives and this.<arg> echoes.
90 tests, counts asserted so a bad parse fails loudly.
protocol/types.py and arsdk/types.py are the interfaces the parallel streams
build against: command/arg/enum/expectation specs and the safety tiers on one
side, frame encoding and the buffer conventions on the other.
Logging goes to stderr throughout, since stdout carries JSON-RPC. That is also
why this package will speak ARSDK itself rather than through pyparrot, which
prints from its receive thread.