RFC: Heterogeneous AirStack — vehicle configs, fleets, top-level configuration, and cross-embodiment platforms #380
Replies: 2 comments
Design history: what Rev 1 proposed here, and why it changedThe RFC body above is Rev 2 (2026-08-19), rewritten as a standalone alongside #379's revision — see #379's design-history comment for the full account of the rejected slot/
Unchanged from Rev 1, because they never depended on slots: §1 vehicle types/units (data modules, generated URDF, per-serial calibration overlays), §3 |
Changelog: consistency pass (2026-08-20)Updated in place alongside #379's consistency pass — no design decisions changed. What moved:
The Directory Atlas (#385) was updated to match. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
RFC: Heterogeneous AirStack — vehicle configs, fleets, top-level configuration, and cross-embodiment platforms
Companion to RFC #379: Modular AirStack. #379 defines how modules are packaged, distributed, and tested (small manifests, stack folders of standard ROS 2 launch files, interface conventions, generated
wiring.md,doctor --live, conformance marks, registry); this RFC builds on that machinery to configure heterogeneous deployments, at three scales:Scales 1–2 land with #379's Phase 3 (they consume stack folders and the conventions spec defined there). Scale 3 is sequenced only after #379's phases 1–4 are proven. A visual companion, #385: Directory Atlas, shows the resulting directory layouts for every repo shape both RFCs define.
The problem at scales 1–2: today three env vars blur these concerns (
URDF_FILE,NUM_ROBOTS,ISAAC_SIM_SCRIPT_NAME) and composedeploy.replicascan only stamp out identical containers. The hierarchy that untangles them: platform class (code) → vehicle type (data) → vehicle unit (calibration) → robot instance → fleet. Every level is a readable file; a robot in a fleet is the composition vehicle × stack folder × unit, with hardware, software, and placement heterogeneity as independent axes.1. Vehicle types and units
iris_with_sensors.pegasus.robot.urdf.ids feed thesensors/*topic naming convention (Request for Comments: Modular AirStack — module repos, slot contracts, marketplace, and distributed CI/docs #379 §4.2), and wiring snapshots normalize sensor-driver nodes to theseids — the declared driver/sim pairing is what lets a sim-generatedwiring.mddiff cleanly against a hardware bring-up (Request for Comments: Modular AirStack — module repos, slot contracts, marketplace, and distributed CI/docs #379 §4).type: data). Controller tuning is parameterized by vehicle (like PX4 airframe files), not baked into platform code.config/local/calibration/<serial>/), so recalibrating never edits the shared package.2. Fleets
Key properties:
launch/onboard.launch.xml,launch/offboard.launch.xml— plus abridge.yamlexplicitly listing every topic/action that crosses the machine boundary (name, type, direction). The bridge list feeds DDS-router config generation, and it is the split, readable in source — explicit beats derived. Today's coarseAUTONOMY_ROLEtrichotomy (full/onboard/offboard) becomes "which entry point does this host run," derived per robot fromhosts:; arbitrary splits replace the one hardcoded two-way split — a third machine is just another launch entry point + bridge section.doctorerrors ifcontrol_setpointor trajectory-group topics appear in anybridge.yaml— this is one ofdoctor's two enumerated hard gates (Request for Comments: Modular AirStack — module repos, slot contracts, marketplace, and distributed CI/docs #379 §4); everywhere else it observes and reports.doctor --liveverifies each host's running graph against that host's entry-point subgraph of the stack's singlewiring.md(one per stack, nodes grouped by host, bridge edges marked — Request for Comments: Modular AirStack — module repos, slot contracts, marketplace, and distributed CI/docs #379 §4) plus the bridge list — a lint over an explicit file, nothing inferred.airstack fleet generateemits each host's services from thehosts:entries that name it.defaults:keeps homogeneous fleets terse (a robot can be one line + serial).spawn:/sim.scenefeed a generic Isaac fleet spawner (replacing the hardcoded one-/multi-drone launch scripts) and are ignored on hardware — sim vs. real is a deployment mode of the same file. Test harness:--num-robots Nbecomes--fleet <preset>, and heterogeneous system tests — including split placements — become expressible.overrides:are leaf values only (ROS params, launch args) — the same anti-variant-explosion logic as shared includable launch files, one level up. A robot needing different topology points at a different stack folder; that division is what keeps fleet files skimmable.robot_name_mapresolver extends to resolve the whole fleet entry (name, domain, vehicle, stack, hosts). Compose: keepdeploy.replicasfor homogeneous fleets;airstack fleet generateemits per-robot and per-ground-host services when heterogeneous (required anyway — different stacks mean different composed images, Request for Comments: Modular AirStack — module repos, slot contracts, marketplace, and distributed CI/docs #379 §6).wiring.mdconsumessensors/front_stereo/*→ the vehicle manifest must carry a sensor withid: front_stereo→ missing = named error before launch. Plus: ahosts:key naming no ground entry, a robot naming a stack with no matching entry points for its hosts, duplicate domains, aunitwith no calibration dir.3. Top level:
airstack.yamlOne file answers "what does this checkout run?" — everything else is derived or pointed to:
airstack syncresolvesrelease+modulesagainst the registry and the active stacks' pinned.reposfiles → writesmodules.lock(exact refs + image digest chain), fetchesmodules/, fetches declared external stack repos into gitignoredstacks/.external/<alias>/, generates.env. A fleet'sstack:values resolve first as paths in the checkout (stacks/...), then as<alias>/<stack>against the declared stack repos — how a robot points at a stack "wherever it's hosted" without a fork. Config tree:Module-pin resolution (low → high): release-set defaults → the stack's
modules.repos→airstack.yamlmodules:entries. Pins resolve per stack: a stack's pins are its tested-together claim, so they beat release defaults for that stack's own composition, and two robots' stacks may legitimately pin the same module differently (each stack's image chain uses its own pins, #379 §6). Anairstack.yamlentry overrides both — the checkout owner's deliberate act — but voids the affected stacks' tested-together claim for that run:syncnames every deviation it resolves (never silently merges), andmodules.lock+effective_config.yamlrecord the override so the run can't masquerade as the badged set.Precedence (low → high): release-set defaults → stack/vehicle files → fleet per-robot overrides →
config/local/overrides.yaml→ CLI/env flags. Rule: CLI/env may select among declared configs and set leaf values, never define new structure — the discipline today's.envlacks. CI configs and developer configs become the same artifact: a failing CI run is reproduced by copying oneairstack.yaml.Migration of today's
.envvars:VERSION→release:(.envgenerated);NUM_ROBOTS→ implicit in fleet;ROBOT_NAME_MAP_CONFIG_FILE→ absorbed into fleet;AUTONOMY_ROLE→ derived fromhosts:+ the stack's entry points (§2);URDF_FILE→ generated from vehicle;ISAAC_SIM_SCRIPT_NAME→ gone (fleet spawner).4. One-off CLI overrides
For quick experiments, a fifth precedence layer with footprints:
airstack up --set robots.wanda.overrides.takeoff_landing_planner.takeoff_altitude=3.0 airstack up --set robots.pietro.stack=stacks/my_experiment airstack up --robots wanda --sim msairsim --with dfm2_disturbances --without gossip # sugardoctorruns — a faster way to edit config, never a bypass; violations fail identically to file edits..airstack/runs/<ts>/effective_config.yaml);airstack config freeze --from-last-run > config/fleets/my_experiment.yamlpromotes a one-off to a named preset. No lost incantations.metrics.jsonrecords the override set so overridden runs can't masquerade as baselines; values-not-structure (defining a new robot inline is refused); nothing persists withoutfreeze.Part 2 — Cross-embodiment (future work)
Generalizing across platform classes: quadrupeds, wheeled robots, humanoids. The narrow-waist conventions from #379 fit, but they force honesty about which conventions are universal and which are secretly multirotor-shaped.
5. What varies by embodiment vs. what doesn't
The reusability gradient down the stack:
tasks/navigate) — fully portable: "go to this pose" means the same thing to a drone, quadruped, or rover.ros2_controlvs. vendor SDKs (Unitree, Spot); "land" vs. "stop" vs. "sit."Design goal: keep the top of the stack shared; make the bottom swappable as a unit.
6. Embodiment = a
platformmodule typeThe platform is a first-class module — the biggest one, but structurally just another module:
A platform module ships the interface + controller + safety-behavior layer and its own reference stack folders — a stack targets a platform by including that platform's bringup launch. Division of labor with §1: a platform module defines a class of machine (code: interface, controller, safety behaviors, command dialects); vehicle configs describe instances within the class;
fleet.yamlcomposes deployments. The current PX4/multirotor stack becomes the reference platform module — a quadruped is just another platform + vehicle once both layers exist.7. Compatibility by traits, not platform enumeration
Modules declaring
embodiments: [multirotor, quadruped, ...]rots the day someone adds a hexapod. Instead, modules declare requirements on traits anddoctormatches them against the platform:{workspace: se3, hover_capable: true}→ rejected on the rover, with a reason.{workspace: se2*}→ runs on rover and quadruped; on a drone only via an adapter (a fixed-altitude SE(2)→SE(3) shim — adapters are ordinary small modules, wired in by the stack that needs them).tasks/navigate→ runs everywhere.Payoff: latent assumptions become declared ones — "this planner assumes it can hover" moves from someone's head to a manifest line and a sync-time error. Prior art:
ros2_control's hardware-interface abstraction; Nav2 is the cautionary tale of SE(2) assumptions baked in rather than declared. (Traits are the one place a manifest carries semantic claims, and they gate compatibility warnings, never wiring —doctorreports, the stack author decides.)Two forced generalizations, both healthy:
tasks/takeoffdoesn't exist on a rover; behavior trees are composed against the platform'sprovides_tasks, verified statically by doctor ("this BT calls takeoff; platform doesn't provide it").8. Testing and migration path
tests/waypoint_checker.pyis already embodiment-neutral — it judges an odometry track against corridors and tolerances, indifferent to whether the track was flown, walked, or driven. Conformance marks become trait-parameterized:waypoint_navigationruns on anything providingtasks/navigate;takeoff_hover_landgates onhover_capableand isn't in a rover's conformance set. Each platform ships its sim integration, so the same harness brings up "platform X + module Y"; platform-compat badges join version badges in the registry.Migration — do NOT build the trait system speculatively:
platform: px4_multirotorreveals the real seams — every place the refactor hurts is a hidden multirotor assumption in a "shared" layer.Split out from the design session behind #379 (2026-08-04), revised alongside it (2026-08-19); consistency pass 2026-08-20 (module-pin resolution rule, external stack repos via
stacks:, split-stackwiring.mdclarified, gate-vs-observe cross-refs — changelog in comments) — earlier drafts and the rejected alternatives are summarized in the design-history comment below. Comments welcome — especially from anyone with quadruped/rover/humanoid integration experience who can pressure-test the trait list, and from anyone running offboard splits who can pressure-test the bridge-list model.All reactions