Skip to content
christomaszewskiPublic

About

Vehicle-level sensor-stack orchestrator: a loop + a manifest that delegates bring-up to each service's own launcher

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

rig — vehicle-level stack orchestrator (infra · sensors · autonomy)

rig brings up and manages every stack on a robot/vehicle computer or replay host — shared infra (zenoh router, bag loggers, dashboard), sensors (cameras, GNSS/INS, lidar), and autonomy (planners, SLAM, perception) — driven by config. It is "a loop + a manifest": it reads a vehicle manifest and delegates the bring-up of each stack to that service's own launcher (<service>-up), in tier order. It never reimplements per-stack logic. Stacks come from sibling checkouts or from package registries (pinned services, profiles, overlays, suites, and — since v0.2.17 — whole-vehicle plans), and a deployment bakes into a tagged artifact that runs on bare Docker.

The dependency is strictly one-way: rig depends on the service repos; a service never knows about rig. rig learns each service only through its rigging.yaml descriptor + the launcher CLI, so services evolve independently and new ones drop in by adding two files (a launcher + a rigging.yaml — rig rigify generates both onto existing software).

   registries (public / internal)                vehicle.yaml                 services.yaml
   services · profiles · overlays ·            which stacks, in 3 tiers;     where each repo lives
   suites · vehicle plans (pinned)             identity, platform, ROS env   (checkout or vendored)
                │  rig pkg add / upgrade               │                            │
                └──────────────────────────────────────► rig ◄──────────────────────┘
                                                         │   per stack: <launcher> <config> <verb>
                                                         │   + fleet env (ROS_DOMAIN_ID, RMW, VEHICLE_ID,
                                                         │     RIG_IMAGE_REGISTRY/TAG, RIG_TARGET_PLATFORM, RIG_DATA_DIR)
        ┌────────────────────────────────────────────────┼────────────────────────────────────────────────┐
        ▼ infra (up FIRST, down last)                    ▼ sensors (producers)                            ▼ autonomy (up LAST, down FIRST)
   zenoh-router-up · dash-up · bag-logger-up        cam-up · novatel-up · sbg-up · lidar-up          planner-up · slam-up · perception-up
   docker compose — one project per instance        docker compose — one project per instance        docker compose — one project per instance
                                                         │
                                 rig bake ──► tagged artifact (configs + vendored launchers + compose-only
                                              scripts + rig) ──► ./run.sh up on the vehicle, bare Docker

Each launcher is the service's own (cam-up is the exemplar; rig rigify scaffolds one onto existing software); rig only sequences the tiers, exports the fleet env, and aggregates status.

Start with the documentation index for deployment, replay and simulation workflows, current validation status, and the historical design notes.

What rig owns vs. what the launcher owns

  • Launcher (<service>-up) owns everything per-stack: parse the config, derive the ROS namespace (/<name>), render driver params, select/parameterize its static compose, wire devices/network, run docker compose. It honors the rig-provided COMPOSE_PROJECT_NAME (never passes -p), falling back to its own <service>_<name> only when run standalone.
  • rig owns the cross-cutting concerns: which stacks run (the manifest, in three tiers), globally-unique instance names, the compose project name (<name>-vehicle-<vehicle_id>), bring-up order (infra → sensors → autonomy; reversed on the way down, so the decider dies before its eyes), the fleet env (ROS_DOMAIN_ID/RMW_IMPLEMENTATION, VEHICLE_ID, RIG_IMAGE_REGISTRY/ RIG_IMAGE_TAG, RIG_TARGET_PLATFORM + each service's declared platform override env, RIG_DATA_DIR), per-host identity and platform (vehicle.yaml + the machine tier written by rig provision), run directories (one session = one folder), status/health aggregation, and lifecycle: external-volume GC on final teardown (down --purge) and full decommission (rig cleanup — images + volumes off the host, never containers, never data).

Install

  • Ubuntu/Debian (incl. Jetson): download rig_<v>_all.deb from the latest release → sudo apt install ./rig_<v>_all.deb (requires python3 + python3-yaml). For offline installation, have those dependencies installed/cached first.
  • macOS: brew install christomaszewski/rig/rig-cli (formula rig-cli, binary rig; the tap tracks releases automatically)
  • anywhere with pipx/uv: pipx install git+https://github.com/christomaszewski/rig
  • from a checkout: ./rig … works as-is; rig setup --shell puts it on PATH

Then, per user: rig setup (creates ~/.rig + subscribes the default public registry). On a vehicle, also provision its identity once: sudo rig provision --id 7 --name skiff-07 (writes /etc/rig/vehicle.local.yaml — every fleet artifact on the machine reads it).

TAB completion (verbs, instance names, package refs, registries): deb and brew installs ship it for bash + zsh — new shells just have it. pipx/checkout installs: rig setup --shell wires it into your rc (or eval rig completion bash|zsh yourself).

Upgrades ride the package manager (apt/brew upgrade) — rig never self-updates. Uninstall: rig setup --purge (user state), then the package manager (the package never touches ~/.rig). Releases are cut by tag: every vX.Y.Z tag publishes the deb/wheel/sdist and bumps the Homebrew formula automatically.

Quick start

# host needs Python 3 + PyYAML and Docker (compose v2). For local dev:
python3 -m venv .venv && .venv/bin/pip install pyyaml

# authoring — build a deployment (see docs/CHEATSHEET.md for the full flow)
./rig init my-vehicle --vehicle-id 1 --infra zenoh-router --discover   # scaffold: wired infra + a menu
                          #   (--vehicle-id pins a single-vehicle identity; without it the tree
                          #   carries per-host MARKERS, supplied by `rig provision`/RIG_VEHICLE_ID)
./rig add ../novatel      # wire ONE more service into an existing deployment (path or bare name)
./rig fetch               # hand-wrote vehicle.yaml rows? materialize their configs from examples
./rig rigify ../my-sw     # make EXISTING software rig-compatible (descriptor + launcher + example)

# the package registry — no checkouts needed (CHEATSHEET §1.5; public seed registry on GitHub)
./rig setup               # once per machine: ~/.rig + the default `public` registry
./rig registry sync       # then offline: pinned installs, vendored surfaces, rig.lock (a service's
                          #   first install at a new pin fetches its repo into the local git mirror)
./rig init my-vehicle --infra public/zenoh-router --add public/sbg  # scaffold straight from a registry
./rig add public/zenoh-router          # infra from the registry, at an exact pin
./rig add sensor:zr30                  # profile match -> service + editable config, hash-anchored
./rig config diff         # git-status for configs
./rig pkg save zr30       # publish the edits into the package they CAME FROM (render identical);
                          #   `pkg promote` is for NEW packages/forks; `pkg list` = the inventory
./rig pkg outdated        # dependency drift across the registries (repair: pkg repin / rebase)

# fleet vehicles — one artifact, N vehicles (CHEATSHEET §1.6)
# reference per-vehicle values as {{vehicle_id}} etc. in configs; `rig bake` auto-detects the
# templates and defers rendering to the vehicle, which supplies identity via `rig provision`

# lifecycle — run the vehicle
./rig doctor              # read-only preflight (unique names, one ROS distro, launchers present, ...)
./rig doctor --deep       # + certify each service's launcher against the contract (runs `config`)
./rig certify             # launcher-contract conformance under a poisoned fleet env (see below)
./rig up --dry-run        # print the exact launcher invocations + fleet ROS env, run nothing
./rig up                  # infra → sensors → autonomy; equal order within a tier starts concurrently
./rig pull                # pre-pull enabled stacks' images, NO container changes
./rig pull bag_player     # explicitly pull a disabled replay player (use its actual instance name)
./rig status             # one rolled-up row per sensor (state + health from compose ps; OP =
                          #   observed operational state for services declaring the state verbs —
                          #   read OP and HEALTH as a pair)
./rig status -v           # expand per-container detail
./rig standby             # park declared stacks (reverse order): ready but quiet — lifecycle idle,
                          #   devices low-power (ouster: motor stopped, laser off); HEALTH unaffected.
                          #   Services without the verbs are always-active, skipped. `up --standby`
                          #   comes up parked (RIG_TARGET_STATE beats each config's initial_state)
./rig activate            # wake declared stacks (producers first; launchers own the budgets —
                          #   device spin-up can take a minute). Trigger-style stacks (SLAM) may
                          #   instead be activated by the mission layer via ROS lifecycle directly
./rig logs cam_front -f   # follow one sensor's logs
./rig config gnss_primary # render a sensor's merged compose (delegates to the launcher's `config`)
./rig graph               # observed pub/sub/service topology from a run's graph epochs (the bag
                          #   logger's `graph:` sidecar, rig-infra ≥ v1.7.0); --check compares vs the
                          #   riggings' declared interface: (WARN-only), --contract <instance>
                          #   scaffolds that block from observation, -o writes the union YAML
./rig run rm <id> / run import <path>  # registry lifecycle: reclaim disk (sealed freely,
                          #   interrupted --force, the OPEN run never) / adopt archived runs so
                          #   id-based verbs + TAB cover them. The registry home: `rig setup
                          #   --data-dir` (this user, no sudo — a first interactive `rig setup`
                          #   asks) or `sudo rig provision --data-dir` (this machine); minted
                          #   lazily. `rig setup --show` = where everything is, one screen;
                          #   `--data-dir NEW --migrate` moves a registry (symlink left behind).
                          #   A tree with its OWN data_dir (vehicle.local.yaml — a reconstruct
                          #   workspace) sees the host registry READ-THROUGH too (runs/TAB/
                          #   refs); writes stay in its own, and no tree sees another's
./rig run archive <id…> --to /Volumes/nas/runs  # move a sealed run's BYTES off the registry
                          #   (<to>/<vehicle>/<id>) and keep a LINKED entry: runs/replay/TAB still
                          #   resolve it, `run rm` unlinks, an unmounted drive lists as dangling
./rig run tag <run> site:mojave event:demo-day  # tags live in the run's own manifest and travel
                          #   with it (run untag removes); `rig catalog --tag site:` finds them
./rig catalog [query] [--tag T --label L --vehicle V --since DATE]  # EVERY run rig knows about:
                          #   the machine registry, every registry it opened/imported into,
                          #   fleet-sync trees, reconstruct workspaces (`catalog roots|add|remove`).
                          #   `rig runs`/TAB stay scoped: this deployment's registry + the host's
./rig run export <run> --profile review  # a SLIM copy of a sealed run under <run>/exports/review/
                          #   made ON the vehicle: the profile's `omit` globs leave files out
                          #   (video), services whose rigging declares `export:` re-write their
                          #   own data smaller (the bag logger: zstd_small, topics dropped), the
                          #   rest is hardlinked. `rig fleet sync --profile review` pulls it
./rig run export <run> --service camera-service  # camera-owned default h264 profile: service-only
                          #   proxies under RUN/exports/services/camera-service/h264/<export-id>/;
                          #   originals/bags stay intact; --instance selects recorded names,
                          #   --force regenerates without deleting previous generations
./rig run export <run> --in-place        # RECLAIM disk instead: the same exporters REWRITE their data
                          #   inside the run (bags re-written at zstd_small, per-topic counts
                          #   verified against the original, then swapped in). Lossless with no
                          #   --profile; a profile that drops topics needs --lossy; nothing is
                          #   ever omitted/deleted; any run that holds the data works — old
                          #   ones and other deployments' included. The manifest records it
./rig reconstruct <run-dir>  # a run dir back into a runnable tree, anywhere: every opened run
                          #   captures the deployment (surfaces + configs + rig, no image bytes)
                          #   into .rig/artifact.tar.gz — extract, verify, overlay a config
                          #   snapshot, localize. Pre-capture runs: `rig run retrofit` stamps them
                          #   with the deploy artifact their manifest names. Opt-out: run_capture
                          #   --registry HOST localizes images.registry too (a bench's mirror;
                          #   machine-wide: `provision --registry`); --enable-replay <path|ref>
                          #   wires the SIL player into a tree that flew without it (opt-in)
./rig replay <run> planner  # SIL: NEW provenance-linked run; the named instances come up LIVE and
                          #   the current ros2-bag-player plays broad recorded data inputs;
                          #   optional ownership evidence excludes regenerated outputs.
                          #   --selection recorded retains recorded-subscription selection.
                          #   The bag-logger records the new outputs: source bag vs replay bag is
                          #   the A/B pair. Refuses while stacks run; teardown = down --end-run
                          #   --from S / --to S replay a SECTION (seconds from bag start — the
                          #   call-script zero; latches restored; --auto-end composes for sweeps)
./rig run replay <run> --skip-service camera-service --auto-end
                          # replay without camera services/video; skips every camera instance
                          # and its bag topics. Repeat --skip-service for other services.
                          # `rig replay` and `rig run replay` are equivalent; --dry-run previews.
./rig down                # reverse groups (autonomy FIRST); equal-order peers stop concurrently
                          #   --purge also GCs external volumes after teardown
./rig replay check <run>   # inspect a hand-built deployment's readiness, without writes
./rig replay prepare <run> --recipe field-test
                          # --player /path/to/rig-infra/ros2-bag-player adds a missing player row,
                          # route and config; kept disabled for ordinary up, selected by replay
./rig replay --recipe field-test
# Author replay/analysis.yaml using the recipe example in docs/REPLAY.md, then:
./rig run export <run> --recipe replay/analysis.yaml --dry-run
./rig run export <run> --recipe replay/analysis.yaml
./rig replay prepare <run> --recipe replay/analysis.yaml
./rig replay --recipe replay/analysis.yaml
                          # one recipe selects bags, per-camera originals/proxies and exact services;
                          # recipe inputs need no --use-proxies. Missing proxies fail explicitly
./rig sim check --recipe warehouse  # inspect an already running Isaac ROS graph and /clock
./rig sim up --recipe warehouse     # attach selected infra/autonomy using sessions/warehouse.yaml
./rig sim down --end-run            # stop the saved Rig session; its recording replays normally
./rig fleet sync --into fleet-runs --include-service-exports
                          #   include committed camera/service exports; default sync excludes exports.
                          #   Bag proxies under exports/inputs/bags need a complete run copy or re-export
./rig swap <svc> <path|ref>  # re-point an INSTALLED service at different code — a local checkout
                          #   (routed live: edit, rebuild, re-up) or a registry ref (vendored +
                          #   pinned). Rows, working configs and overlays are KEPT; config-key
                          #   drift vs the new example is reported. The reconstructed-tree SIL
                          #   swap: same config, same data, different code
./rig cleanup             # decommission: remove this deployment's images + volumes from the host
                          #   (after the final down, before deleting the tree; --dry-run to preview)
./rig up cam_front ins_main   # operate on a subset by name

./rig uses the system python3; on a host where PyYAML lives in a venv, run .venv/bin/python rig … (and point each launcher's *_PYTHON env at that interpreter), or apt install python3-yaml on the robot.

Deploy to a vehicle

Build images into a registry the vehicle can reach, bake a tagged artifact, ship it, run it — no driver source or internet on the vehicle. bake auto-vendors each service's launch surface (no manual rig vendor step) and digest-pins images against the registry from vehicle.yaml (images.registry) — pass --registry <ip:5000> only to override it:

rig build                                   # all declared runtimes and CPU architectures, build/push
rig build --scope host --tag iteration-1      # this machine's provisioned runtime + Docker CPU
rig build --scope deployment,host            # vehicle target plus this host's native runtime
rig build --scope deployment,replay          # vehicle target plus supported replay companions
rig build --platform dev --arch amd64 --tag iteration-1   # faster targeted iteration
rig build --no-cache                         # disable instruction caching in every participating build
rig build sbg novatel                        # only these services; a failure prints the retry command
rig src sync                                 # mirror every git source the builds need (~/.rig/cache/git)
rig build --offline                          # then build without remote git or upstream registries
rig image audit                             # inspect what the stacks will RUN: one ROS distro, the
                                            #   declared rmw installed, ros-* versions agree across
                                            #   images; warns on images built from local checkouts
rig bake --tag v1                           # -> var/artifacts/v1.tar.gz (auto-vendored + digest-pinned)
# rig bake --tag v1 --include-replay         # auto-wire a missing player + validate onboard replay
# combine --include-replay --bundle-images for offline startup AND replay images
# rig bake                                 # same when vehicle.yaml has images.tag: v1
scp var/artifacts/v1.tar.gz vehicle:~/      # on the vehicle: `tar xzf v1.tar.gz && cd v1 && ./run.sh up`

Portable builds and replay covers the build contract, Buildx setup, companion digests, reconstruction onto another host, replay startup policy, and migration of old runs. Onboard replay covers replaying local recordings from a shipped artifact, with registry access by default and optional offline replay images. Coordinated startup covers opt-in held replay, consumer readiness, common release, startup timeouts and compatible companion images. Replay preparation covers instance replay.start, local recipes, check/prepare, renamed services, exact bag selection, and source-preserving legacy imports. No pub/sub declarations are required; packaging recipes with suites is deferred. Isaac simulation covers attaching selected services to an existing Isaac ROS graph, shared version-2 session recipes, clock checks, recording and replay. Version-1 replay recipes retain their existing behavior. Offline Isaac walkthrough covers preparing a compatible Jazzy/Zenoh deployment, local scene/assets and image caches, then camera capture and replay with a shared recipe. Isaac attachment is experimental (since v0.2.73); live GPU acceptance is still pending. Hardware runtimes use separate tags (v1-jp6m, v1-dev); multiarch indexes select the CPU within a runtime. rig reconstruct RUN --platform dev selects the software runtime; --platform recorded keeps the source runtime. Doodle's updated descriptor omits live radio startup during replay while preserving recorded topics; rig replay RUN --include-service doodle opts back in.

--tag overrides the artifact name; when omitted, it defaults to the literal images.tag in vehicle.yaml. A missing or templated image tag still needs --tag; host-local overrides do not choose the artifact name. This does not change the images' tags. Reusing an existing artifact tag prompts before replacement; non-interactive jobs must pass --overwrite. Publication is atomic, so a failed archive write preserves the previous archive.

rig artifact ls                             # target, provenance and archive/staging/extracted sizes
rig artifact show v1                         # target, bundled image IDs and complete release catalog
rig artifact show v1 --format json
rig artifact rm v1 --dry-run                 # preview removal of the archive and generated staging
rig artifact rm v1                           # confirm interactively; --yes for automation
rig artifact prune --keep 3 --dry-run         # preview removal of all but the 3 newest archives

Removal preserves extracted trees by default; --include-unbaked explicitly includes matching var/unbaked/<tag> trees, which may contain edits. Pruning orders archives by file modification time and removes their matching staging trees. Orphaned staging trees are listed and can be removed by tag with artifact rm. These commands leave Docker images and recorded runs alone; rig cleanup and rig run rm manage those separately.

Bake uses deployment declarations and explicit flags; workspace/machine vehicle.local.yaml, shell configuration overrides and generated fleet/replay state cannot leak into it. platform selects the runtime and optional arch: arm64|amd64 selects the CPU. --platform/--arch override them for one bake; missing values are inferred only when the service matrices have one common choice.

The artifact bundles the resolved configs + vendored surfaces + rig + a compose-only form that runs on just Docker (graceful fallback when Python/PyYAML are absent). --bundle-images additionally docker-saves the deployment runtime/CPU image set into the artifact (multi-GB) for zero-registry deploys; with --include-replay, it also includes the selected replay image set (Docker API 1.48+). The complete release catalog remains metadata. Content aliases preserve identity across save/load. up.sh checks the target and compares local image IDs with the bundle, reloads missing or different images, verifies the result, and starts Compose without pulling. ./run.sh load forces a reload and the same identity verification. Re-baking inside an extracted artifact (on the vehicle, after field edits) records the parent artifact in metadata.yaml, so save-points form a lineage. Full offline / local-registry flow: docs/HOST_SETUP.md.

Fleet artifacts need no flag: if the deployment references {{var}} values (e.g. rtsp://10.160.{{vehicle_id}}.80/…), rig bake stages the tree unresolved and each vehicle renders it locally from its provisioned identity — one artifact serves the whole fleet (sudo ./provision.sh --id 7 --name skiff-07 once per vehicle, then ./run.sh up forever). CHEATSHEET §1.6 has the full lifecycle. Bundled fleet artifacts require each enabled service's verbs.images planner so images can be selected without resolving vehicle identity. The contract and scopes describe the workflow and defaults.

Registries & package kinds

Stacks install from package registries (a git repo or shared folder of manifests — the public seed is rig-registry-public; rig registry init scaffolds your own, with tools/validate + CI wrappers). Five kinds:

kind what it is install shape
service a code pointer (repo + full-SHA rev) and/or a digest-pinned image rig add internal/lidarish — vendors the launch surface, routes it, instance from its example
profile a complete nameless config for one service (requires.service), keyed service:short rig add sensor:zr30 / rig add camera-service:siyi-zr30 — the instance's editable working config, base pinned + hash-anchored
overlay a versioned delta on top of a profile-based instance (ordered bindings, last wins; local still beats overlays) rig overlay apply <instance> <ref> — bindings only
suite references only: members at exact pins (services/profiles/overlays + at most one vehicle plan) rig pkg add internal/gideon-boat — atomic, all-or-rollback
vehicle a suite's instance plan (v0.2.17): a template vehicle.yaml whose rows carry YOUR instance names, order, enabled, tiers, per-row overlay bindings, fleet defaults (platform, images, data_dir…); per-host identity stays markers never standalone — captured with its suite (promote --all --suite S --vehicle V), installed through it into an empty rig init tree

The capture/reproduce loop: tune a deployment → rig pkg promote --all --suite <s> --vehicle <plan> --to internal [--adopt] (dirty instances become overlays; hand-authored ones become adopted profiles with --adopt) → on a fresh tree rig pkg add internal/<s> gives the vehicle back, identity supplied per host. Exact pins are snapshots (v0.2.19): a member's later release never breaks the suites or profiles that pin it — registry validate warns, installs serve the pinned version from the registry's git history, rig pkg outdated reports drift (exit 1, CI-able) and rig pkg repin refreshes. CHEATSHEET §1.5 is the daily loop; docs/DESIGN.md has the decision log.

Certify a launcher (the contract, executable)

doctor checks the vehicle (manifest composition); certify checks a service — it runs the launcher's config verb under poison env values (certify.invalid:5000, certify-tag-x, instance certifyname0) and asserts the contract held: project name honored (no -p), images pulled from RIG_IMAGE_REGISTRY, built images pulled as :RIG_IMAGE_TAG (build/pull agreement; composed <tag>-<platform> for services declaring a build.platforms matrix), the declared platform honored (RIG_TARGET_PLATFORM/override env — every matrix entry must render on any host, never host-probe), fleet ROS env reaching containers unmangled, deterministic output, identity fully derived from the config name, clean stdout, parseable status. Run it in a deployment (rig certify [name…], or rig doctor --deep) or in a service repo's CI with no deployment at all:

rig certify --repo . --config core-driver/config/usb-real.yaml          # the service's own CI gate
rig certify --repo . --require-build-check   # also require a completed isolated cache/platform check
rig certify cam_front --emit /tmp/mac.yaml                              # then the same on the vehicle...
rig certify --diff /tmp/mac.yaml /tmp/orin.yaml   # identical = `config` output is host-independent, so a
                                                  # dev-box bake is provably correct for the target

Layout

vehicle.yaml            # which stacks THIS machine runs + fleet-wide ROS settings (+ vars:/env:
                        #   fleet defaults; {{var}} markers = supplied per vehicle)
vehicle.local.yaml      # OPTIONAL, gitignored, never baked: THIS machine's identity/values for
                        #   bench trees; vehicles use /etc/rig/vehicle.local.yaml (rig provision)
services.yaml           # catalog: service routing key -> where its repo lives
config/sensors/*.yaml   # one config per sensor (the single source of truth for that stack)
config/infra/*.yaml     # one config per shared infra service (zenoh router, bag logger, …)
config/autonomy/*.yaml  # one config per autonomy stack (planner, SLAM, perception, …)
replay/analysis.yaml    # authored replay recipe; version 2 can select exact session.instances
sessions/*.yaml         # optional location for recipes shared by replay and simulation
var/replay/<recipe>/    # generated local bindings/plans; re-prepare after changing a recipe
services/               # VENDORED launch surfaces (`rig vendor`; bake auto-vendors; `rig add
                        #   public/<svc>` vendors at the registry pin) — for dev, point services.yaml
                        #   at sibling checkouts instead. Never source/submodules.
config/.pins/           # GENERATED pristine base copies (registry installs) — what `config diff`,
                        #   `pkg upgrade`, and `pkg promote` measure against. Commit them.
config/.overlays/       # GENERATED bound-overlay payload copies (deployment stays self-contained)
rig.lock                # GENERATED pins: registries@commit, packages+hashes, instance anchors,
                        #   image digests (bake). Commit it; `rig add <ref> --locked` reproduces.
rig, rig_cli/           # the CLI (thin shim + package: manifest/catalog/dispatch/status/doctor/certify/
                        #   build/bake/init/rigify/runs/…)
docs/                   # index · command reference · worked workflows · design/history

vehicle.yaml (per machine)

Lists active stacks (name, service, config, enabled, order) and the fleet ROS settings, in three tiers: infra: (substrate — routers, loggers, dashboards; up FIRST, down last), sensors: (producers), and autonomy: (graph consumers — planners, SLAM, perception; up after ALL sensors, down FIRST, so the decider dies before its eyes). The tier partition is hard; per-entry order sorts within a tier only. For up, entries with the same tier and order start concurrently, without a flag. Rig waits for every launcher in that group to finish before starting the next order or tier. Assign independent sensors the same order to overlap their startup waits; keep different orders for dependencies or services sharing an exclusive resource. For example:

sensors:
  - {name: lidar, service: ouster, config: config/sensors/lidar.yaml, order: 10}
  - {name: camera, service: camera-service, config: config/sensors/camera.yaml, order: 10}
  - {name: consumer, service: processor, config: config/sensors/consumer.yaml, order: 20}

lidar and camera start together; consumer waits for both launchers to return. up --dry-run shows concurrent groups in a stable order. down reverses the groups: autonomy → sensors → infra, with higher orders first within each tier. Equal-order peers stop concurrently, and the next group waits for all of them. down --dry-run shows that reverse schedule.

This also works in newly baked artifacts: the bundled rig, run.sh up / run.sh down, and generated up.sh / down.sh preserve the same groups. Re-bake existing artifacts to include the new scheduler. Compose-only scripts wait for Compose startup; launcher-based startup also includes any sensor-state waits implemented by each launcher. Neither adds a universal sensor health check, and consumers must still retry as discovery is dynamic.

Ordinary rig up / rig down attempt remaining groups after launcher failures and report failures at the end. Compose-only artifact startup still stops on failure, after waiting for all peers already started in the failing group; artifact teardown continues through all groups and reports any failure. down --end-run captures logs before teardown and seals only after success; --purge runs after all teardown groups finish. Ctrl-C stops in-flight launcher processes without rolling back completed container operations. Use rig status to inspect the result. Other verbs, replay, and simulation retain their existing ordering. Disable a stack with enabled: false rather than deleting its config. name must be unique across the vehicle — it keys the compose project, external volumes, and ROS namespace.

Per-vehicle values flow through {{var}} markers (never ${VAR} — that's compose's): configs and manifest scalars may reference {{vehicle_id}}, vars: entries, etc., resolved at render from shell (RIG_VAR_*) > vehicle.local.yaml > /etc/rig/vehicle.local.yaml > vehicle.yaml defaults. A self-marker (vehicle_id: "{{vehicle_id}}") makes the value MANDATORY per vehicle — and since v0.2.20 rig init scaffolds vehicle/vehicle_id as markers by default (nothing comes up as "vehicle 1" by accident; --vehicle-id N pins a single-vehicle literal). platform: (e.g. jp7) declares the deployment runtime → RIG_TARGET_PLATFORM; optional arch: arm64|amd64 declares the CPU → RIG_TARGET_ARCH. Services with a build matrix pull <image>:<tag>-<platform> and images.tag means VERSION only. Both identity and platform are per-host facts the machine tier (sudo rig provision --id 7 --name skiff-07 --platform jp7) may override during local execution. Build scopes and bake use the separate deployment/host selection rules above; an artifact's recorded target is validated before startup. An env: map exports extra (interpolated) variables to every launcher. One mapping form, {{map <list_var> <template_var>}}, builds lists (e.g. zenoh peer endpoints from {{fleet_peer_ids}} — the fleet minus THIS vehicle, derived). CHEATSHEET §1.6.

services.yaml (catalog)

Maps each service routing key to its repo path (resolved relative to this repo). The key may differ from the repo dir name (service sbg → repo sbg_driver).

config/sensors/<name>.yaml

Thin ROS 2 drivers share a generic schema — service, name, connection (tcp/udp/serial/file), ros.namespace, and an opaque driver_params block the launcher renders into the driver's ROS 2 params. The rich camera-service camera uses its own service-specific schema (rig hands it to cam-up as-is).

A config can instead be a nameless profile reused across instances via a per-sensor overrides: patch in vehicle.yaml — rig deep-merges the patch, stamps in name, and renders the result to var/rendered/<name>.yaml before handing it to the launcher (a complete named config with no overrides is passed through untouched). See config/sensors/camera.profile.yaml and docs/ROADMAP.md §1.

Shared infra services (rig-infra)

Ready-to-use shared (infra:) services live in their own repo — clone it beside your deployment, point services.yaml at a service dir (../rig-infra/zenoh-router) and add an infra: entry, or let rig init --infra zenoh-router (bare name — the workspace scan finds it; or the registry form --infra public/zenoh-router, no checkout needed) wire it for you; in an EXISTING deployment, rig add <name|path> does the same wiring after the fact (infra arrives enabled, sensor/autonomy services get a commented menu row to uncomment):

  • zenoh-router/ — the vehicle's shared rmw_zenoh router (order 0). Default: the fleet-ros base image running rmw_zenohd, so the router and the ROS sessions share one distro's zenoh packages by construction. Optional inline router_config: renders to a mounted zenohd.json5.
  • ros2-bag-logger/ / ros1-bag-logger/ — record the ROS telemetry graph to ${RIG_DATA_DIR}/current/bags/<name> (the open run — ROADMAP §3c; flat bags/<name> without a registry). Config: record.mode: all|allow|exclude (+ exclude_images, default true — image streams are huge over ROS and already recorded compressed at the camera source) and an output block (storage/compression/split). Place at order ~1 so it records from startup. The ros2 logger defaults to fleet-ros (rosbag2 + mcap + rmw_zenoh, ~1 GB — no camera image on camera-less vehicles).
  • base/ — the fleet-ros image; rig build builds + pushes it via the riggings' build: declaration, and certify enforces the composes pull the same tag. The zenoh-router and ros2-bag-logger riggings mark it provides: base, making it the DEPLOYMENT's base image: rig build builds it first and exports it to every other build (and to launchers) as RIG_BASE_IMAGE, so one image pins the fleet's distro+rmw packages; vehicle.yaml images.base (or rig build --base-image) overrides it with an external ref.
  • msgs/ — the fleet-ros-msgs overlay: base + the union of the interface packages the fleet's services declare in their riggings' msgs: blocks. rosbag2 cannot record a topic whose message package isn't installed in the recorder's image (it logs "unknown type" and keeps going), so a fleet with custom types silently gets bags missing them. When services declare msgs: and a base provider declares build.msgs_overlay, rig build renders the union manifest (RIG_MSGS_MANIFEST), builds the overlay right after the base (FROM RIG_BASE_IMAGE — an external images.base gets an overlay too), and exports the ref as RIG_MSGS_IMAGE; the bag logger's compose prefers it over the bare base. No declarations → no overlay → the bare base, which is correct. Under rig, the source: repos are exported on the host from rig's local git mirrors (no clone in the builder, so rig build --offline builds the overlay too). An overlay built from local checkouts (rig-infra's FLEET_MSGS_LOCAL) is labelled io.rig.build.local: rig image audit warns about it and rig bake refuses it without --allow-local.

Each is just a launcher + compose around a stock tool — the same contract any service meets; rig-infra's CI runs rig certify against every one.

The contract: rigging.yaml

A repo is rig-compatible when its launcher exposes up/down/status/logs/config on one config, accepts a config at any host path, honors fleet ROS env, observes stdout/stderr discipline (machine output on stdout, human lines on stderr), and ships a rigging.yaml (the legacy name deploy.yaml is still accepted). Optionally it also implements the operational-state trio — standby/activate/state, declared in verbs: all-three-or-none (the declaration is the support claim; contract + reference adoption: the service-state adoption prompt in ~/ws/infra, ouster ≥ v0.2.0) — and honors RIG_TARGET_STATE at up. Start from rig rigify <dir>: it generates the descriptor + a contract-correct launcher skeleton + an example config in an existing software dir, pre-wired from a read-only analysis (found composes get -f-wired; ports/volumes/images/Dockerfiles become commented host_ports/external_volumes/mirror/ build: hints). The onboarding arc is rig rigify → rig certify --repo (until green) → rig add:

service: novatel
launcher: novatel-up                 # default: <service>-up
verbs: { status: ps }                # adapt logical verbs -> launcher args (defaults shown in descriptor.py)
ros_distro: lyrical
# ros_namespace: "/robot/{name}"     # optional: actual ROS NODE namespace; fixed /control/relay also works
#                                    #   identifies graph ownership; does not launch/remap nodes
tier: sensor                         # optional: "infra" = shared, up-first (routers, loggers, dashboards);
                                     #   "autonomy" = graph consumer, up-last / down-first (planners, SLAM)
examples: [sensors/novatel.example.yaml]     # optional: example configs — init/add/fetch copy them;
                                             #   `rig certify --repo` uses the first as its default --config
launch_surface:                              # the minimal file set `rig vendor`/`bake` copy to LAUNCH
  - novatel-up                               #   this service (never its source)
  - docker/compose/compose.deploy.yaml
# build: { command: tools/build-images.sh, images: [novatel] }   # `rig build` runs `<cmd> <registry> [tag]`
#                                            #   (ROS_DISTRO exported from vehicle.yaml ros.distro)
# build: { command: ..., images: [...], platforms: [jp7, jp6] }   # a build MATRIX: distinct image sets per
#                                            #   hardware/OS target — rig composes pulls as <tag>-<platform>
# build: { command: ../base/build.sh, images: [fleet-ros], provides: base }  # this build produces the
#                                            #   deployment's BASE image: rig builds it FIRST and exports
#                                            #   <registry>/<images[0]>:<tag> as RIG_BASE_IMAGE to every
#                                            #   other build + launcher (vehicle.yaml images.base overrides).
#                                            #   Providers of one base must agree on build.platforms AND
#                                            #   the build script — rig refuses rather than pick by order.
# build: { command: ../base/build.sh, images: [fleet-ros], provides: base,
#          msgs_overlay: { command: ../msgs/build-msgs.sh, image: fleet-ros-msgs } }  # base providers may
#                                            #   also declare the msgs OVERLAY build: base + the union of
#                                            #   the riggings' `msgs:` blocks, exported as RIG_MSGS_IMAGE
# build: { command: ..., sources: [{name: sbgecom, repo: <git url>, ref: <tag|branch|sha>}] }
#                                            #   repos the build would fetch itself: rig mirrors them
#                                            #   and hands each over as $RIG_BUILD_SOURCES/<name> (pass
#                                            #   --build-context <name>=...), so `rig build --offline` works
#                                            #   (the bag logger prefers it over the bare base)
# msgs:                                      # interface packages THIS service's topics use (beyond
#   apt: [mavros_msgs]                       #   ros-base/common_interfaces) — rosbag2 can't record a topic
#   source:                                  #   whose message package isn't installed in the recorder's
#     - repo: https://github.com/PX4/px4_msgs.git   # image. ROS names (underscores) in apt:; source pins
#       ref: v1.16.0                         #   are MANDATORY and must equal the pin the service builds
#       packages: [px4_msgs]                 #   against. rig unions the blocks fleet-wide; one repo at two
#                                            #   refs is refused ("align the riggings"), never guessed.
# export: { data: "bags/{name}" }             # this service can SLIM the run data it wrote (under
#                                            #   <run>/<data>) for `rig run export`: rig calls the
#                                            #   launcher's `export` verb with RIG_EXPORT_SOURCE (run),
#                                            #   RIG_EXPORT_DEST (write <data> under it), RIG_EXPORT_OPTIONS
#                                            #   (the profile's block for this instance, a YAML file rig
#                                            #   never reads), RIG_EXPORT_PROFILE, RIG_EXPORT_FORCE
# replay: { sim_time: true,                 # the launcher wires use_sim_time from RIG_SIM_TIME (rig's
#                                            #   one clock token under `rig replay`; explicit config wins
#                                            #   both ways). Undeclared services under test WARN at replay
#           service_introspection: true }    # + CONTENTS-level service introspection on its servers/
#                                            #   clients: calls RECORD into bags (record.services) and
#                                            #   REPLAY at this service (RIG_REPLAY_SERVICES / --calls)
# interface:                                 # the service's declared topic/service contract — checked
#   publishes:                               #   WARN-only against a run's OBSERVED graph epochs
#     - {topic: fix, type: sensor_msgs/msg/NavSatFix}   # relative = instance-namespace; absolute
#     - /tf                                  #   (/tf) = shared-bus; bare string = name only. Bootstrap
#   subscribes: [{topic: rtcm}]              #   from observation: `rig graph --contract <instance>`
#   provides: [{service: reset, type: std_srvs/srv/Trigger}]   # service servers
#   requires: []                             #   service clients (what this service NEEDS)
# platform: { auto_detect: /etc/nv_tegra_release, override_env: CAM_PLATFORM }  # the launcher's standalone
#                                            #   host probe + the env var it honors; rig mirrors the vehicle's
#                                            #   declared `platform:` into it (RIG_TARGET_PLATFORM sibling)
# mirror: [eclipse/zenoh:1.2.1]              # third-party images `rig build` copies into the registry
external_volumes: ["novatel_{name}_data"]    # optional: GC'd by `rig down --purge` (final teardown only)
host_ports: ["plugins[name=webrtc-bridge,enabled=true].params.port"]  # optional: rig validates these don't clash

The optional top-level ros_namespace declares the service's actual ROS node namespace, without a publisher/subscriber inventory. Use an absolute, non-root namespace such as /control/relay, or a quoted template such as "/robot/{name}"; {name} is the only supported placeholder. This is association metadata, not a launcher setting or a ROS remap. rig graph expands {name} with the current instance name. Replay uses the recorded identity (instances.NAME.recorded_as, otherwise the destination name), and associates observed topics and service servers using that namespace. A recipe's instances.NAME.recorded_namespace takes precedence for historical namespace changes; undeclared services keep the /instance fallback. Distinct instances sharing a fixed namespace have ambiguous ownership: use a per-instance template or explicit recipe namespaces. Graph evidence is still required to identify regenerated outputs such as status topics. Recorded calls default to unambiguous servers started by replay, excluding targets with a known selected client, after request-content inspection. Use recipe playback.services: [] to disable recorded calls or a nonempty list to select targets manually.

cam-up, novatel-up, and sbg-up all satisfy this. See docs/DESIGN.md for the full rationale.

About

Vehicle-level sensor-stack orchestrator: a loop + a manifest that delegates bring-up to each service's own launcher

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages