The CKCM log format turned out to be documented in a file deleted from
Parrot's ulog repository in 2017, so it is cited rather than claimed as
reverse engineering, with four points the capture settled that the source
leaves open or states wrongly.
The COMMANDS tag is deliberately NOT resolved to protocol commands: measured,
only 4 of 11 symbols match the XML by name, covering 3.5% of the tag, and a
resolver would read as authoritative while guessing, with its silence on the
rest reading as 'not a command'.
Tested on the aircraft. A second ARSDK handshake is accepted and telemetry is
redirected to it; the first session's frames stop while it still reports
connected = True. So the claim inherited from pyparrot's error text, which had
reached our error messages, tool descriptions, simulator behaviour and a test
name, was wrong in the most misleading direction: a refusal would be loud, and
this is silent.
The simulator now models the takeover by default; refusal stays available
because a client must handle a non-zero status anyway.
read_log fetches the live log over FTP and filters by tag, severity and
message before it returns anything, because half an hour of uptime is
five thousand entries. It excludes shp_usbmode by default, a five-second
USB poll that can be a third of the log, and says in the result that it
did so. Only the current file is reachable: the archives the drone
rotates into are a previous owner's sessions and carry network and
location data, which is a decision for a person, not a tool.
bebop://commands is the resource worth having, because it needs no
drone: the whole catalogue is readable with the aircraft switched off.
The ones that do need a link return an explanation rather than raising,
since a resource that errors looks broken while one that explains itself
is empty for a reason. bebop://state also reports telemetry age, because
the aircraft accepts a second controller and silently redirects
telemetry to it, so a connected flag proves nothing.
The log says nothing resolvable about ARSDK, measured rather than
assumed: no command ids appear anywhere in it, and the COMMANDS function
names match arsdk-xml for 4 of 11 symbols, 3.5% of the tag. So nothing
here resolves them, and read_log says where to look instead.
_host moves into tools/_common as drone_host now that three tool modules
need it, and head/tail in the shell allow-list gains the reason they
must take a file argument: ld.so.preload makes SIGPIPE fatal, so a pipe
into head writes a crash report to the aircraft's flash.
The format is Parrot's own, from ulogcat/libulogcat_ckcm.c, which was
deleted from Parrot-Developers/ulog in 2017; the module docstring cites
it and records what the 560 KB capture settles that the source leaves
open, notably that this firmware's stamps are monotonic microseconds
since boot with no wall clock anywhere.
Robustness is the point as much as the layout. The log is live and grew
from 445 KB to 560 KB inside a minute, so a fetch landing mid-record is
the normal case and ends the parse quietly. The markers are not escaped
either, so the lengths are what the parser trusts.
Parses the capture to the byte: 5520 entries, no resyncs, no residue.
15 commands go on the unacknowledged buffer, so acked=false is their normal
outcome rather than a failure. Reporting it bare made a working camera move
look broken; the note now says to confirm with get_state instead.
list_dir and fetch take a name and resolve it themselves; the tools layer
resolved it first and handed over the object, so every call failed with
'No FTP area named Area(...)'. Found by calling the tool for real.
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.
protocol/codec.py landed with decode_event returning ids and a values
dict, so the session looks the command name up in the index rather than
expecting it in the result. Verified end to end against that stream's
codec in a stitched tree: the sim encodes events through it, the session
decodes through it, and the test that skips until it exists now passes.
The argument name cannot be recovered by splitting a telemetry key,
because an argument can contain an underscore, so the expectation
predicate matches on the suffix instead.
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.
Bebop-era snapshot of common.xml and ardrone3.xml (BSD-3-Clause, Parrot SA),
with pyparrot's <myclass> rename reverted to Parrot's <class>. Provenance and
the comparison against upstream master are in arsdk-xml/PROVENANCE.md.