Commit Graph
11 Commits
Author SHA1 Message Date
rsp2k b044edbd87 Merge ARSDK client: discovery, connection, session, simulator
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.
2026-10-02 00:34:03 -06:00
rsp2k 376ccaa732 arsdk: prove one unnameable event does not take the link down
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.
2026-10-02 00:33:15 -06:00
rsp2k 21bae69275 arsdk: read the codec's (ids, values) decode shape
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.
2026-10-02 00:29:20 -06:00
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
rsp2k 455cd80b0a Merge media and files: ARStream2 video, FTP, read-only shell
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.
2026-10-02 00:14:19 -06:00
rsp2k d38b215348 io: ARStream2 video, read-only FTP areas, allow-listed debug 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.
2026-10-02 00:12:18 -06:00
rsp2k 0d467e03b0 Merge protocol layer: XML index, codec, safety tiers, expectations
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).
2026-10-02 00:09:39 -06:00
rsp2k 1477bb0c82 Vendor the XML inside the package so it ships in the wheel
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.
2026-10-02 00:09:03 -06:00
rsp2k ef8c4658e0 Protocol layer: XML index, codec, safety tiers, expectations
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.
2026-10-02 00:07:43 -06:00
rsp2k 5732befe82 Scaffold: package, settings, errors, server factory, shared contracts
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.
2026-10-01 23:53:15 -06:00
rsp2k 40814040a2 Vendor Parrot ARSDK protocol definitions
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.
2026-10-01 23:50:54 -06:00