Skip to main content

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

Returns the drone-local facts plus live session state: 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 a 503.
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 needs X-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

The compute daemon writes its cluster and queue status to a heartbeat sidecar (/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.