The frame contract
The engine downscales and converts each camera’s native format to a normalized frame, writes the pixels into a shared-memory ring (one ring per camera), and publishes a small frame descriptor on thevision.frame topic. The pixels
never ride the RPC envelope. Your SDK resolves a descriptor to pixels for you
by mapping the named ring read-only and reading the slot the descriptor points
at.
A resolved Frame carries the descriptor plus the pixel bytes. Descriptor
fields:
The
Frame also gives you the pixel bytes: len(pixels) equals the
descriptor’s byte_len, the valid pixel data for width x height in
format.
Default working resolution is 640x480; the engine downscales further per the
active model’s input size. Drop policy is latest-wins: a slow consumer drops
frames, it never blocks the engine. A torn or stale read (the writer recycled
the slot mid-copy) is dropped silently and your callback does not fire for it.
Subscribing to frames
In Python,ctx.vision is a VisionClient. Subscribe with a callback that
runs once per resolved frame:
subscribe_frames is gated on vision.frame.read. The callback may be sync or
async. Keep it cheap: it runs on the delivery path, so offload heavy inference
to a task or queue rather than blocking in the callback.
The Rust SDK is the same surface: ados_sdk::vision::VisionClient, reached via
ctx.vision, with subscribe_frames(camera_id, callback) and the same
Frame shape. Types come from ados_sdk re-exports backed by
ados_protocol::framebus, so a Python plugin and a Rust plugin read the same
wire byte for byte. A Rust plugin’s entry point is
ados_sdk::run_plugin::<MyPlugin, S>(...).
Running inference
There are two paths, and a plugin can use either. Both share the same accelerator, which the engine arbitrates. Engine-run. Register a model file plus its metadata with the engine. The engine loads it, runs it on the shared backend, and publishes detections itself. Your plugin only declares the model and then consumes detections like any other consumer.register_model is gated on vision.model.register.
Plugin-side. Run your own model in the plugin and publish the results. You
can also ask the engine to run a registered model against one frame on the
shared backend with ctx.vision.infer(model_id, frame), which returns the
detections without pixels crossing the RPC envelope.
Detections
ADetection has:
bbox: pixel-space box{x, y, width, height}, origin top-left, in the frame’s own resolution.class_label: the class string.confidence: the model’s score.track_id: optional stable id across frames, for tracking models.
DetectionBatch on the vision.detection topic,
labelled by model_id plus the source camera_id, frame_id, and ts_ms so a
consumer can align the boxes to the exact frame they came from.
publish_detection (and the publish_one convenience) are gated on
vision.detection.publish. Built-in consumers, the video overlay and mission
logic, pick the batches up. The Rust SDK exposes the same publish_detection /
publish_one on VisionClient.
Consuming detections
A plugin that acts on detections, rather than producing them, subscribes to the batches the engine publishes. The callback receives aDetectionBatch; pass
camera_id to filter to one camera:
subscribe_detections is gated on vision.detection.subscribe.
Designating a follow target
The engine runs one single-object tracker per camera.designate_track sets
the box it locks onto, overriding the auto-lock, the same way an operator’s
click-to-follow does:
designate_track is gated on vision.track.designate. Designation alone does
not command the vehicle; a follow behavior that flies toward the target uses
guided setpoints.
Capabilities
Declare the ones you use underagent.permissions in the manifest. Risk levels
are on Permissions.
A worked extension: Vision Navigation
The first-party Vision Navigation extension (vision-nav) is a reference for
frame-bus work. It provides GPS-denied position hold from optical flow: a Rust
agent half reads engine frames (vision.frame.read) and emits the flow message
the configured firmware fuses over MAVLink (OPTICAL_FLOW for ArduPilot and
iNav, OPTICAL_FLOW_RAD for PX4, plus DISTANCE_SENSOR when a rangefinder is
wired to the companion). It supports ArduPilot 4.5+, PX4 1.14+ and iNav 7.0+.
It does not inject pose through the visual-odometry helpers.
Look at extensions/vision-nav/ in the ADOSExtensions repository for a real
manifest.yaml, config schema and agent/GCS split. Operator setup is in
Vision Navigation.
Multiple cameras
When more than one camera is attached, the engine publishes a separate ring and descriptor stream per camera. Filter to the one you want by passingcamera_id:
Some("uvc-0") for the camera_id argument. A
None filter receives every camera’s frames; tag your detections with the
frame’s camera_id so consumers know which camera each box came from.
Testing
Exercise the frame and detection path without a real host, engine, shared memory, or socket using the real test harness. The fake builds a real frame ring through the same frame-transport contract and resolves each synthetic frame the way the production client does, so your frame-handling path runs end to end; only the host and the OS shared-memory object are faked. In Python, useados.sdk.testing.FakeVisionEngine:
VisionClient (the ring lives in a temp
directory the client maps read-only, and the descriptor rides a real delivery
event into the client’s resolver), build the engine with a file-backed ring and
attach your callback:
engine.captured_detections().
The Rust harness mirrors this: ados_sdk::testing::FakeVisionEngine emits
synthetic frames into the same FrameCallback your plugin registers and
captures the detection batches it publishes.
Shipping models
A plugin gets its model weights onto the node in one of three ways. Bundled in the archive. Place the file under the plugin tree (for exampleagent/models/weeds.onnx). The archive’s SIGNATURE covers every entry, so the
weights are protected by the same signature as the rest of the plugin. For an
engine-run model, pass the file’s path as model_path on the model metadata.
By reference. Declare the model under agent.contributes.vision.models.
The agent fetches it, verifies it against the pinned sha256 and caches it.
The digest sits in the signed manifest, so a fetched model is as trusted as a
bundled one:
vision.read_model host
method, gated on vision.model.read. It returns one entry per declared model
with state (resolved, needs-model or verify-failed), model_id,
runtime, path when resolved, and reason otherwise. In Rust it is
vision_read_model() on the SDK client.
As an install payload. agent.payloads lists files the agent downloads at
install instead of shipping them in the archive. Each entry pins path
(archive-relative destination), source (an https:// URL on the download
allowlist), sha256 and size_bytes, and can narrow to node profiles or an
arch_os. See Manifest.
Do not fetch weights yourself at runtime; the plugin’s network.outbound
policy blocks that by default.
Vendor binary models
Closed-source vendor binaries (for example, proprietary thermal AGC processing as a.so) ship under vendor/ per the
vendor binaries doc. The plugin loads them with
ctypes or cffi and calls them from its own inference path, then publishes
the results as a DetectionBatch like any other plugin-side model.
Next steps
MAVLink extensions
Send results to the flight controller and inject visual odometry.
Performance and budgets
CPU, memory and accelerator limits for vision workloads.
State and storage
Where a plugin keeps data on the node.
Permissions
Every capability and its risk level.