ados-sdk crate. The other agent-half language is
Python, with ados.sdk; both speak the same
length-prefixed msgpack wire and capability-token handshake to the
plugin host, so the choice is per plugin. A Rust agent half and the
plugin host (Rust or Python) interoperate byte-for-byte: the wire lives
in ados-protocol and is reused unchanged on both sides.
The crate mirrors the ados.sdk Python package surface for surface: the
IPC client, the plugin-facing context facade, the six driver traits, the
vision client, a test harness, and the agent capability catalog
re-exported from ados-protocol.
When to pick Rust
Python is the right pick when the plugin leans on the Python ecosystem (machine-learning inference, scripting, fast hardware bring-up). Reach for Rust when:- The plugin is a tight, long-running service. A Rust binary has no interpreter to start and a small idle footprint, so a control loop or a frame consumer that runs for the whole flight pays less overhead.
- You ship a vendor binary. A Rust agent half is one statically
linked binary placed at the manifest entrypoint. The host execs it
directly under systemd with no venv to provision, and the
ctx.processfacade authorizes any extra vendor binary the plugin spawns. - You want a single artifact. The pack step cross-compiles to
aarch64and drops the binary into the.adosplugarchive at the entrypoint path. There is nothing else to install on the board.
Layout
A Rust agent half is its own crate atagent/ inside the extension
directory:
extensions/_hello-rust.
Depending on the SDK
The crate depends onados-sdk, which brings the IPC client, the
context facade, the driver traits, and the lifecycle runner. It pulls in
ados-protocol (the frame, envelope, and capability-token wire,
including the vision framebus contract) transitively, so a crate that
names only ados-sdk also gets the protocol types.
In the
altnautica/ADOSExtensions
monorepo, the Rust agent halves form a cargo workspace alongside the
existing pnpm (TypeScript GCS halves) and uv (Python agent halves)
workspaces. The SDK dependency is declared once in the root
Cargo.toml and a member crate inherits it:
ados-sdk as a path
dependency to a sibling checkout of the agent repository:
There is no crates.io publish. CI builds against a pinned git revision
of the same crate: a per-crate
Cargo.toml overrides the dependency
with a git source and rev = "<commit>". Develop against the path
dependency, pin a rev for a release build.ados-protocol directly for the framebus
value types the SDK does not re-export (FrameDescriptor,
FrameFormat, Detection, BoundingBox, ModelMetadata). It is the
same crate the SDK already pulls in, so this only widens what the plugin
imports, not what it links.
The plugin shape
A Rust plugin implements thePlugin trait and hands the type to
run_plugin from main. The trait has one required method, new,
which constructs the plugin; every lifecycle hook defaults to a no-op,
so a plugin overrides only what it needs.
The entry is a plain generic function, not a proc-macro. A
#[ados_plugin] attribute would expand to little more than
fn main() { run_plugin::<P>() }, so the SDK keeps the call site
explicit and skips the macro crate.The runner
run_plugin parses argv and the environment, connects the IPC client,
builds the PluginContext, and drives the hooks. It reads --socket,
--token, and --agent-id off the command line, falling back to the
ADOS_PLUGIN_SOCKET, ADOS_PLUGIN_TOKEN, and ADOS_PLUGIN_AGENT_ID
environment variables (the exact contract the host passes a Rust
binary). The hook order matches the Python runner:
run_plugin_with takes pre-parsed RunnerArgs, for a test or a host
harness that supplies arguments without touching the process
environment.
PluginContext
The object handed to every hook. Each field is a small capability-gated
facade over one IPC client; the facade shapes the request and decodes
the reply, and the host enforces the capability before the call reaches
a handler.
| Field | What it does | Capability |
|---|---|---|
ctx.events | publish(topic, payload) and subscribe(pattern, cb) on the event bus | event.publish / event.subscribe |
ctx.mavlink | send(bytes, component_id), subscribe(msg_name, cb), register_component(id, kind) | mavlink.read / mavlink.write / mavlink.component.* |
ctx.telemetry | extend(channel, payload) to add a channel to the heartbeat the GCS reads | telemetry.extend |
ctx.peripheral_manager | register_*_driver(ref) for each driver kind, unregister(handle), claim_camera(path, exclusive) | sensor.<kind>.register |
ctx.camera | claim(path, exclusive), release(path), get_frame(path, format, timeout_ms) | sensor.camera.register |
ctx.vision | frame subscription, model registration, inference, detection publishing, pose injection | vision.* |
ctx.config | static_get(key) (manifest config, sync), get(key, default) / set(key, value, scope) (live kv) | none |
ctx.process | spawn(basename, args, env) to authorize a vendor-binary launch | process.spawn |
ctx.lifecycle | on_pause(cb) / on_resume(cb) for GCS-side mount events | none |
ctx.peripherals is an alias for ctx.peripheral_manager, matching the
Python back-compat alias. ctx.ping_supervisor() is a health probe; for
any method not yet on a facade, ctx.client() returns the shared IPC
client.
A subscription callback runs on the IPC reader task, so it must not
block. Compute cheaply in the callback and offload heavy work (an async
RPC, inference) to a spawned task, as the object detector example does.
Driver traits
For a hardware-driver plugin, the SDK ships six object-safe async traits, one per kind. Each mirrors the matching Python base class method for method:CameraDriver,GimbalDriver,LidarDriver,GpsDriver,EscDriver,PayloadActuatorDriver.
discover honestly, opens a session on demand, and
yields samples until the session closes. The Python AsyncIterator
return becomes a SampleStream<T> (a boxed Stream), and a Session
associated type stands in for the opaque Python session object: the host
treats it as a token and only hands it back to the driver’s own methods.
A plugin registers a driver instance from on_start through
ctx.peripheral_manager.register_*_driver, passing an opaque reference
id; the driver itself keeps running in the plugin process and the host
routes by that id. See the driver layer
page for the contract and the per-kind candidate, capability, and sample
types.
Vision
ctx.vision is the engine facade. Frames never ride the RPC envelope:
the engine writes normalized frames into a shared-memory ring and
publishes a small descriptor, and the SDK memory-maps the named
/dev/shm ring read-only and reads each slot through the per-slot
seqlock the framebus contract defines, dropping any torn or stale read
(latest-wins).
register_model, infer,
publish_detection / publish_one, and the visual-odometry helpers
register_vio_component, inject_pose, and inject_odometry. The pose
helpers build a VISION_POSITION_ESTIMATE (from a Pose) or an
ODOMETRY message (from an Odometry, pose plus body-frame twist) and
send it to the flight controller under the visual-odometry component id
(VIO_COMPONENT_ID), so a VIO plugin never hand-builds a MAVLink frame.
Frame, Pose, Odometry, VisionClient, and VIO_COMPONENT_ID are
re-exported from the crate root; the framebus value types
(Detection, BoundingBox, FrameDescriptor, FrameFormat,
ModelMetadata) come from ados-protocol.
The full worked vision plugins are
object-detector-rs
(a cheap heuristic detector that proves the frame and detection path on
any board) and
vision-nav
(optical flow and visual-inertial odometry feeding pose to the
autopilot EKF).
Testing
Theados_sdk::testing module mirrors the Python harness for vision
plugins. FakeVisionEngine emits synthetic frames into the same
FrameCallback a plugin registers with ctx.vision.subscribe_frames,
and captures the detection batches the plugin would publish, with no
real host, vision engine, shared memory, or socket. It builds a real
frame ring through the framebus contract and resolves each synthetic
frame the same way the production client does, so the plugin’s
frame-handling path is exercised end to end.
push_frame, push_solid, and push_dir enqueue frames (the last
reads *.bin / *.raw files in name order); deliver_one /
deliver_all drive them through the resolver; detection_sink and
captured_detections collect what the plugin published.
Capabilities
The SDK re-exports the generated agent capability catalog fromados-protocol as ados_sdk::capabilities, so a plugin author
references one source of truth for capability ids. The source of truth
is crates/ados-protocol/capabilities.toml. There are 43 agent
capabilities, of which three are enforced at runtime by the host
(event.publish, event.subscribe, hardware.gpio_out). See the
permissions reference for the full list and
what each grants.
A method whose capability the manifest did not request is rejected
before it runs: the SDK surfaces it as ClientError::CapabilityDenied
(naming the missing capability), and a vendor-binary spawn the manifest
allowlist refuses surfaces as ClientError::AllowlistViolation.
IPC
The wire isados-protocol unchanged: length-prefixed msgpack envelope
frames over the per-plugin Unix domain socket, gated by a pipe-delimited
HMAC capability token. The handshake echoes the token the supervisor
minted; the client then serves request and response on one flow (each
call mints a fresh r<n> request id) and event, MAVLink, and vision
pushes on the other, dispatched to the topic-matched callbacks. The
per-request timeout defaults to five seconds. This is the same envelope
and capability gate the Python SDK and the GCS bridge speak, so a Rust
plugin and a Python host interoperate without translation.
Manifest
A Rust agent half setsruntime: rust and points the entrypoint at the
binary path inside the installed plugin tree:
aarch64 and places the
binary at agent.entrypoint in the .adosplug archive. The host execs
that binary directly under systemd (not through the Python runner),
passing the per-plugin socket on the command line and the capability
token and agent id through the unit environment. The manifest, signing,
and packing flow is otherwise identical to a Python plugin; see the
manifest reference and
signing keys.
A plugin that spawns a vendor binary sets contains_vendor_binary: true
and lists each one under vendor_attribution (name, license, source
url), so the installer surfaces the attribution at install time. The
vision-nav example does this for its visual-inertial odometry engines.