Documentation Index
Fetch the complete documentation index at: https://docs.altnautica.com/llms.txt
Use this file to discover all available pages before exploring further.
Architecture
Vision Navigation is built around a single design rule: the camera, the IMU, the rangefinder, and the estimator are four independent modules. Any estimator runs on any combination of camera, IMU, and optional rangefinder; new estimators plug in behind a stable contract without rewiring the plugin. This page is for developers who want to add a new estimator (a stereo VIO, a future ORB-SLAM3, a learned monocular depth network), extend the calibration loader, or audit the data flow.Module map (agent half)
imu/ that
subclasses BaseImuSource.
The EstimatorOutput contract
Every estimator returns the same record:output_mode and dispatches to either the
OF emitter (component 198, OPTICAL_FLOW_RAD) or the VIO emitter
(component 197, VISION_POSITION_ESTIMATE). The two halves never
both carry data on the same record.
Sequence: a frame becomes a MAVLink message
The simplified flow for one frame inoptical_flow mode:
vio_openvins mode the path changes at step 4 to push the frame
into the shim’s SHM ring and the IMU samples to the shim’s UDS
control channel. The vendor binary returns pose messages
asynchronously; the estimator drains them per tick and produces an
EstimatorOutput with the pose six-tuple filled. The router emits
on component 197 instead of 198.
The calibration runner
calibration/runner.py is the agent-side counterpart to the Mission
Control wizard. It runs in-process inside the plugin’s asyncio loop
and is subscribed to the vision-nav.start_calibration event topic.
The runner walks five substeps, publishing
vision-nav.calibration_progress events after each:
- Decode the bundle. The GCS event carries 20 to 30 base64-encoded PNG frames plus an IMU recording window. The runner decodes frames via OpenCV and rejects any that fail decode.
- Detect AprilTags.
cv2.aruco.ArucoDetectorwith thet36h11family extracts corner positions per tag. Frames with fewer than eight detected tags are rejected. - Solve intrinsics.
cv2.calibrateCamerafits the pinhole camera matrix K and the radial-tangential distortion vector against the per-frame corner correspondences. - Recover per-frame poses. Each frame’s
(rvec, tvec)from the intrinsics solve is the camera-target pose at that frame’s timestamp. - Fit the camera-IMU timeshift. A golden-section search over
[-200 ms, +200 ms]minimises the residual between the camera-derived angular velocity (from consecutive frame rotations) and the IMU’s gyro trace in the shifted window. The detail is in Calibration math.
<plugin_data_dir>/camchain.yaml in
the Kalibr-compatible format and publishes
vision-nav.calibration_complete with the result. The plugin’s
event handler applies the new timeshift_cam_imu to the live
TimeAligner and flips cameraIntrinsicsLoaded to true on the
heartbeat so the GCS pre-arm card refreshes.
A new calibration plugged in via the YAML upload path skips the
runner entirely: the event handler validates the YAML, persists it,
and applies the new timeshift directly.
Auto-detect on start-up
Theautodetect subpackage probes the host’s hardware once at
on_start and merges the result into the live config:
optical_flow is selected but no rangefinder responded, the
plugin’s _auto_flip_mode_for_hardware rewrites the active mode
to optical_flow_degraded before opening the capture. The flip is
logged so an operator can trace why the estimator state shows
“degraded” on a fresh install.
The scale ladder
MavlinkScaleLadder (in scale/mavlink_ladder.py) walks four rungs
on every pick() call:
degraded when
the static rung is active so the GCS surfaces the warn banner.
A future scale source (vision-only depth, or a stereo-VIO derived
scale) plugs in behind the same BaseScaleSource ABC. The estimator
does not need to know which rung produced the number.
The shim and the vendor binaries
The VIO modes spawn an out-of-process vendor binary via the plugin SDK’sprocess.spawn allowlist. The plugin host’s subprocess sandbox
checks that the basename is declared in the manifest’s
subprocess_spawn list and that the binary lives under
<install_dir>/vendor/. The signed .adosplug archive bundles the
binary; on install the supervisor extracts it under the plugin’s
cgroup slice.
OpenVinsEngine and
VinsFusionEngine differ only in the binary basename and the
engine-id string they advertise on the handshake. Adding a third VIO
engine (ORB-SLAM3, for example) is a new subclass of BaseVioEngine
plus a registry entry plus a vendor binary that speaks the IPC v1
protocol.
The IPC protocol is documented inline in shim/engine.py. The wire
format is length-prefixed msgpack on a SOCK_SEQPACKET UDS (with a
SOCK_STREAM fallback). The control channel handles
hello/hello_ack/config/imu/pose/alive/log/shutdown messages; the
SHM ring carries frames zero-copy.
The pre-arm gate
PreArmGate.evaluate() is a pure function over PreArmInputs. It
produces a PreArmReport with a list of individual checks, each
carrying a severity (ok / pending / blocking) and an
operator-readable detail string.
The gate is mode-aware. Adding a new mode means:
- Adding the mode key to the registry in
estimators/__init__.py. - Adding the mode literal to
config.py(VisionNavConfig.mode). - Adding a branch to
PreArmGate.evaluate()that picks the right check set for the new mode. - Adding the mode label to the GCS
ModeCardand thenavigationModeBadgehelper.
How a heartbeat is built
Every tick,HealthPublisher.snapshot() returns:
cmd_droneStatus.navigation field. The
Mission Control normaliser reads the field through
infer-capabilities.ts:normalizeNavigation and routes it into the
per-drone agent-capabilities-store. Every UI surface
(NavigationTab, ModeCard, SensorsCard, EstimatorCard,
FallbackBanner, EkfSourceSwitcher, PreArmStatus, the drone-card
pill, and the fleet GPS-denied count) reads from that store.
Adding a new estimator
The full recipe:- Implement
BaseEstimatorin a new file underestimators/. - Register the class in the
ESTIMATOR_REGISTRYdict inestimators/__init__.py. The registry key must equal the class’sestimator_idattribute (a contract test enforces this). - Extend the
modeliteral inconfig.py(VisionNavConfig.mode). - Add a branch to
PreArmGate.evaluate()that lists the pre-arm checks the new estimator needs. - Update the GCS:
- Add the mode to
ALL_MODESinModeCard.tsxwith a description and hardware-requirements string. - Add the mode to
EstimatorModeintypes.ts. - Add the mode badge to
navigationModeBadgein the host GCS so the drone-card pill renders correctly.
- Add the mode to
- Write tests. The existing
tests/test_estimators_scaffold.pypatterns cover the contract; the new estimator’s tests verify the state machine + output shape.