World Model and Compute Offload
World-model capture is split in two on purpose. The drone does the light half: it selects pose-tagged keyframes from its camera stream and publishes the keyframe, pose and capture-state streams. A separate compute node reconstructs the 3D model from them. The drone does no reconstruction and no training. It never needs to.
Both units self-gate: with
atlas.enabled false, ados-atlas exits cleanly and
the unit is a clean no-op, so an un-opted node is byte-unchanged.
Capture on the drone
ados-atlas subscribes to the vision frame ring, decides which frames are worth
a keyframe, and publishes the keyframe plus pose plus capture-state streams.
Three pieces make up the capture core:
- The rig: one camera up to an all-sides set. The camera count is configurable from 1 to N and drives one flow at any count.
- The keyframe selector: a per-camera gate that fires on pose delta or elapsed time.
- The capture session: a state machine that takes pose-tagged frames, always feeds the roughly 10 Hz pose stream, and emits a keyframe envelope whenever a camera’s selector fires.
Configuration
Pose source resolution, as Mission Control reads it:
Readiness
enabled,
cameras_configured, capture_profile, pose_source, the live state,
capturing, session_id and service_running, and the compute_node_id the
egress forwarder is actively streaming to.
That last field is freshness-gated. A forwarder handoff not re-written within
15 seconds is treated as absent, so a dead forwarder never leaves a stale
compute node on the readiness surface.
Compute-node reachability is Mission Control’s own concern (it already knows its
paired workstation nodes), so it is not re-probed here. Only the node the
forwarder actually resolved is surfaced.
Write the per-drone config
PUT /api/atlas/config has patch semantics: only the keys you send are written,
so toggling enabled alone never wipes the cameras or the profile. It writes the
atlas: block into /etc/ados/config.yaml with a surgical merge that preserves
every other key, then restarts ados-atlas.
Validation happens at the API edge rather than at write time, because an
out-of-set value would fail the whole
atlas: block’s parse and silently default
it to disabled: a status surface that lies.
Drive a capture session
Four routes, each forwarding to the capture service’s control socket and returning the resulting capture status. An unreachable service is a503.
stop finalizes and bags the session, which is what triggers the compute node to
reconstruct.
Capture states: idle, capturing, paused, finalizing, bagged.
The config and capture routes are writes, so the LAN edge requires the pairing
key when the node is paired, the same posture as /api/command.
The compute node
ados-compute is the Rust core of the workstation and compute profiles: a
SQLite-backed job store, a queue and scheduler with a worker model, the
reconstructor and perception-offload traits, and the master/slave cluster.
There is always one master, the single logical endpoint a drone or Mission
Control pairs with and the scheduler. Extra nodes slave to it and offer their
workers. A lone node is the master.
The real reconstructors and detectors are third-party binaries the workers shell
out to, behind the reconstructor and detector traits. Mock backends keep the
whole engine testable with no GPU, no camera and no network, which is what CI
runs. Which backend binaries you install is your decision, not the agent’s.
Job kinds
Job lifecycle:
queued, running, then one of completed, failed,
cancelled.
A submission carries job_id, kind, an optional dataset_ref (a bag handle,
or a live session id for a streaming offload) and a params object. Status
carries state, progress in 0.0 to 1.0, an optional result_ref and an
error when it failed. The heavy result is delivered out of band; the job
interface carries the small request and status only.
The job API
The daemon serves one TCP listener. Reach is gated by the pairing posture: unpaired is open, paired plus on-box is open, paired plus off-box needsX-ADOS-Key. That is what makes binding a non-loopback address safe.
A worker claims the next job under the engine lock, releases the lock, then runs
the backend without it, so a reconstruction that takes minutes never blocks the
API. It re-acquires the lock only briefly to record the terminal state. A cancel
that lands during the run wins: the scheduler refuses to overwrite a job that is
no longer running.
Environment
The daemon reads its configuration from the environment so the install layer can set it without a config-file dependency.
A compute node advertises itself over mDNS with its job port, and a drone-side
caller browses for a
profile=workstation node.
Status from Mission Control
/run/ados/compute-heartbeat.json), rewritten roughly every 5 seconds. The
agent’s control front serves it so the Mission Control compute card renders
local-first, fresher than the cloud heartbeat.
An absent, stale (older than 20 seconds) or unreadable sidecar is a 404: this
node is not a compute profile, or its daemon is not running. It is never a 500.
The served body includes a host gpu block with identity and live utilisation.
Every field in it is optional and honest: an unknown field is null, never
fabricated, so on a host where a probe tool is missing the whole block degrades
to all-null rather than reporting a guess.
Perception offload
Separate from the world model, and the reason an NPU-less drone can still run a detector. Two sides, two config blocks. Drone side:auto offloads when the board has no NPU and a workstation is reachable on the
LAN. on forces it, off never offloads.
Workstation side:
auto auto-accepts and serves LAN offload. Both blocks default so a fresh agent
needs no setup: a no-NPU drone and a workstation on one LAN offload hands-free.