Spec version: v1.0.1 (semver — see Versioning and deprecation)
This is the versioned specification of AirStack's interchange points — the narrow waists where modules meet: canonical topic/service/action names, message types, QoS profiles, TF frames and units, and rate classes. It is the citable contract behind the topic tables of the Module Integration Checklist; the checklist remains the step-by-step integration workflow.
Documentation, not enforcement. This spec is documentation that modules
default to and conformance tests check — it is never input to any wiring
machinery. No schema compiles against it, no resolver reads it, and no code
is generated from it. A module's launch file exposes every topic endpoint as
a launch arg and defaults it to the canonical name below; in a
conventional stack, including the module therefore requires zero remaps, and
only deviations appear in stack entry files — which is what keeps them
skimmable. Enforcement is by test and by observation: the system-test suite
doubles as conformance tests, each stack's generated wiring.md is the
observed truth, and airstack doctor --live diffs reality against it.
Conventions in this table are verified against the observed graph — types
and QoS below come from stacks/full_default/wiring.md (the committed
wiring-snapshot of the running reference stack), not from memory. Where any
other document disagrees with a column here, the observed graph wins.
All names are relative to the robot namespace: canonical topic
odometry_conversion/odometry means /{robot_name}/odometry_conversion/odometry
at runtime (ROBOT_NAME namespacing is pushed by the launch preamble).
- QoS — publisher profile as observed: RELIABLE or BEST_EFFORT reliability; durability is VOLATILE unless noted (TRANSIENT_LOCAL is called out explicitly). QoS is named because it is a classic silent failure: a best-effort subscriber under a reliable-only publisher (or vice versa) receives nothing, with no error anywhere.
- Rate class — qualitative bands, not measured guarantees:
state(~10–100 Hz),sensor(~10–30 Hz),plan(~0.1–2 Hz),event(on change / on command),latched(transient-local state). - Placement —
onboard-onlymarks interchanges that must never cross a machine boundary: the controller and the safety executive stay on the vehicle so link loss leaves it able to failsafe.doctorhard-errors whencontrol_setpointor trajectory-group names appear in any split stack'sbridge.yaml— one of doctor's two enumerated hard gates; everywhere else it observes and reports.
Sensor topics are namespaced by sensor id: sensors/<sensor_id>/<signal>.
Sensor ids are first-class in the vehicle manifest, where
each id pairs the real driver with its sim representation; wiring snapshots
normalize driver nodes to these ids so sim baselines diff cleanly against
hardware bring-ups.
| Canonical name | Type | QoS | Rate class | Notes |
|---|---|---|---|---|
sensors/front_stereo/left/image_rect |
sensor_msgs/msg/Image |
BEST_EFFORT | sensor | rectified; right/ mirrors |
sensors/front_stereo/left/camera_info |
sensor_msgs/msg/CameraInfo |
BEST_EFFORT | sensor | frame = the camera's optical frame |
sensors/ouster/point_cloud |
sensor_msgs/msg/PointCloud2 |
RELIABLE | sensor | filtered lidar cloud (post lidar_point_cloud_filter); raw is sensors/ouster/point_cloud_raw |
sensors/lidar/point_cloud |
sensor_msgs/msg/PointCloud2 |
— | sensor | generic lidar slot (sim publishes here when ENABLE_LIDAR) |
Units: SI throughout (meters, seconds); image encodings per ROS convention.
| Canonical name | Type | QoS | Rate class | Placement |
|---|---|---|---|---|
odometry_conversion/odometry (v1 canonical) |
nav_msgs/msg/Odometry |
RELIABLE | state | produced onboard |
v2 target: plain
odometry(/{robot_name}/odometry) is the intended canonical name; today every consumer (safety monitor, PID, DROAN, random_walk, trajectory controller, task servers) subscribes toodometry_conversion/odometry, so v1 records reality. Renaming is a spec-major change (see deprecation policy) with a coexistence window.
Frames/units: pose in the map frame (ENU, meters); twist in the body
frame (child_frame_id); yaw right-handed about +Z.
| Canonical name | Type | QoS | Rate class | Notes |
|---|---|---|---|---|
vdb_mapping/vdb_map_visualization |
visualization_msgs/msg/Marker |
RELIABLE | plan | today's de facto map interchange — the reference global planner consumes it |
vdb_mapping/vdb_map_updates / _sections / _overwrites |
vdb_mapping_interfaces/msg/UpdateGrid |
RELIABLE | plan | remote/split map synchronization |
vdb_mapping/vdb_map_pointcloud |
sensor_msgs/msg/PointCloud2 |
RELIABLE | plan | point-cloud export |
The map lives in the map frame. A structured (non-visualization) map
interchange is an acknowledged v2 candidate; v1 documents what the running
graph does.
| Canonical name | Type | QoS | Rate class |
|---|---|---|---|
global_plan |
nav_msgs/msg/Path |
RELIABLE | plan |
Frames: map (ENU, meters). Producer: the global planner (onboard in
full_default, offboard in lite_offload_global); consumers: the local
planner, gossip, keepalive. global_plan is the interchange that MAY cross
a machine boundary — it is the entire point of the global-offload split.
Contrast §5.
All names live under the trajectory_controller/ namespace (served by
relative name inside it). None of these may appear in a bridge.yaml —
a doctor hard gate: global_plan crosses,
trajectory commands don't.
| Canonical name | Kind | Type | QoS | Rate class | Direction |
|---|---|---|---|---|---|
trajectory_controller/trajectory_override |
topic | airstack_msgs/msg/TrajectoryXYZVYaw |
RELIABLE | event | any module → controller (replaces current trajectory) |
trajectory_controller/trajectory_segment_to_add |
topic | airstack_msgs/msg/TrajectoryXYZVYaw |
RELIABLE | plan | local planner → controller (appends) |
trajectory_controller/set_trajectory_mode |
service | airstack_msgs/srv/TrajectoryMode |
(service) | event | task servers → controller |
trajectory_controller/tracking_point |
topic | airstack_msgs/msg/Odometry |
RELIABLE | state | controller → PID/planners (note: airstack_msgs, not nav_msgs, and not PointStamped) |
trajectory_controller/look_ahead |
topic | airstack_msgs/msg/Odometry |
RELIABLE | state | controller → local planner |
trajectory_controller/trajectory_completion_percentage |
topic | std_msgs/msg/Float32 |
RELIABLE | state | controller → task servers |
Safety floor: command authority flows through the trajectory controller —
a module emitting trajectory_override inherits arming, safety monitoring,
and takeover for free (that is the selling point). Publishing
tracking_point/look_ahead from anything but the controller is
impersonation; doctor --live flags it loudly.
| Canonical name | Type | QoS | Rate class | Placement |
|---|---|---|---|---|
interface/cmd_roll_pitch_yawrate_thrust |
mav_msgs/msg/RollPitchYawrateThrust |
RELIABLE | state | onboard-only; blessed publisher: the PID controller |
interface/cmd_pose, interface/cmd_velocity |
geometry_msgs/msg/PoseStamped / TwistStamped |
RELIABLE | state | onboard-only; alternate command dialects into robot_interface |
control_setpoint is the spec name for this interchange point; the rows
above are its concrete v1 spellings. Never bridged, never remapped offboard —
the second doctor hard gate covers these alongside the trajectory group.
| Canonical name | Type | QoS | Rate class |
|---|---|---|---|
interface/is_armed |
std_msgs/msg/Bool |
RELIABLE | state |
interface/has_control |
std_msgs/msg/Bool |
RELIABLE | state |
interface/mavros/state |
mavros_msgs/msg/State |
RELIABLE, TRANSIENT_LOCAL | latched |
interface/mavros/extended_state |
mavros_msgs/msg/ExtendedState |
RELIABLE, TRANSIENT_LOCAL | latched |
interface/mavros/global_position/global |
sensor_msgs/msg/NavSatFix |
BEST_EFFORT | sensor |
interface/robot_command |
service airstack_msgs/srv/RobotCommand |
(service) | event |
Late-joining subscribers rely on the TRANSIENT_LOCAL rows — a VOLATILE subscriber there works, but a VOLATILE re-publisher silently loses the latch.
Every task executor's action server is exposed at tasks/<task_name>
(remapped there in launch; see the
add-task-executor
skill). All types come from task_msgs:
| Canonical name | Action type | v1 server |
|---|---|---|
tasks/takeoff |
task_msgs/action/TakeoffTask |
takeoff_landing_planner (onboard) |
tasks/land |
task_msgs/action/LandTask |
takeoff_landing_planner (onboard) |
tasks/navigate |
task_msgs/action/NavigateTask |
droan (local planner, onboard) |
tasks/fixed_trajectory |
task_msgs/action/FixedTrajectoryTask |
trajectory_controller pkg (onboard) |
tasks/exploration |
task_msgs/action/ExplorationTask |
random_walk (global planner) |
tasks/semantic_search |
task_msgs/action/SemanticSearchTask |
(module-provided) |
tasks/coverage |
task_msgs/action/CoverageTask |
(defined in task_msgs; no shipped executor) |
tasks/chat |
task_msgs/action/ChatTask |
(defined in task_msgs; no shipped executor) |
Related service: takeoff_landing_planner/set_takeoff_landing_command
(airstack_msgs/srv/TakeoffLandingCommand) — the GCS-facing takeoff/land
command. Task goals MAY cross machine boundaries (they are high-level
intents, not control): a split stack lists the crossing actions in its
bridge.yaml.
| Canonical name | Type | QoS | Rate class | Placement |
|---|---|---|---|---|
behavior/drone_safety_monitor/state_estimate_timed_out |
std_msgs/msg/Bool |
RELIABLE | state | onboard-only |
behavior/drone_safety_monitor/command |
std_msgs/msg/String |
RELIABLE | event | onboard-only |
The safety executive (drone_safety_monitor + the interface's takeover path) is marked onboard-only: link loss must leave the robot able to failsafe without any ground host in the loop.
Gossip runs on its own DDS domain (default 99) so peer discovery does not flood per-robot domains; the dedicated gossip router bridges it (never the robot↔GCS router — double-bridging amplifies).
| Canonical name | Type | QoS | Rate class | Notes |
|---|---|---|---|---|
/gossip/peers |
coordination_msgs/msg/PeerProfile |
BEST_EFFORT | state | global (unnamespaced), domain 99 |
coordination/peer_registry |
coordination_msgs/msg/PeerProfile |
RELIABLE, TRANSIENT_LOCAL | latched | per-robot registry output |
Custom payloads: see the attach-gossip-payload skill.
| Frame | Parent | Convention |
|---|---|---|
world |
— | fixed origin; world → map published as a static identity by the launch preamble |
map |
world |
ENU, meters; the planning/state frame (odometry.pose, global_plan, the map) |
base_link (via robot_description) |
map (through the state estimate) |
body frame; odometry.twist lives here |
ENU vs NED is the classic silent failure at the PX4 boundary: MAVROS performs the NED↔ENU conversion — everything ROS-side in this spec is ENU. Angles in radians; right-handed; yaw about +Z.
/tf and /tf_static are wiring: the wiring snapshot deliberately keeps
them (frame plumbing drifts too).
This spec is public API even though nothing compiles against it — modules' launch-arg defaults and the conformance tests encode it. Changing a canonical name, type, QoS profile, or frame convention requires:
- a semver-major bump of this spec,
- a coexistence window (old and new names both served/accepted),
- a short written proposal in the registry repo (
rfcs/— the deprecation registry; until the registry repo exists, proposals live as GitHub Discussions on the AirStack repo).
Additions (new interchange points) are semver-minor and are discovered
through drift reports: three forks patching the same tap point = a missing
convention. The doctor hard-gate list (dep conflicts;
control/trajectory names in bridge.yaml) grows only through the same
proposal process.
| Spec | Date | Change |
|---|---|---|
| v1.0.1 | 2026-08-25 | §8: added tasks/coverage and tasks/chat rows so the table covers all eight task_msgs actions; both are defined in task_msgs with no shipped executor. Documentation-only. |
| v1.0.0 | 2026-08-20 | Initial versioned spec, recorded from full_default's observed wiring. Known v2 candidates: plain odometry as the canonical state topic; a structured global_map interchange. |