Shared ROS 2 infrastructure for every Reiser Lab rig: the typed message contracts
(rig_interfaces), device adapters and their simulators, MCAP recording primitives, process
supervision primitives, the Zenoh configuration and the benchmarks behind the design choices.
Created 2026-10-26 by moving the shared part of webDisplayTools' rig/ workspace here with its
history (decision record: webDisplayTools
docs/development/rig-software-repos-and-runners.md,
ticket LAB-338). What a graph may rely on is written down in CONTRACT.md.
Dependencies point downward only. This repository imports nothing of ours. The arena rig
(webDisplayTools: the Studio's gateway bridge_node, the arena composition rig.py, the arena
visualization) and the Python arena library + headless runner (LED-Display_pyDisplayTools) depend
on the packages built here. A package lives here only when it carries no arena semantics in its
code (message contracts may) and a second consumer confirms the abstraction; anything doubtful
stays with its application until a consumer asks.
Setting up a machine (install recipe, host table, per-host quirks for the Mac, ww18, the Bergamo PC and ww14): SETUP.md. Every install or QC pass updates its host's row there.
pixi.toml the SOURCE workspace (RoboStack + rmw_zenoh + the two packages built from src/; the ROS distribution is a feature: `jazzy` default, `lyrical` opt-in)
src/rig_interfaces/ ament_cmake message package: FicTracFrame, ArenaFrame, ConnectorReply, HostStats (+ DiskStats, NetStats, GpuStats, ProcStats; 0.3.0) (pixi-build-ros)
src/rig_nodes/ Python package (pixi-build-python, hatchling); console scripts rig-*
rig_nodes/_common.py clocks (precise on Windows), QoS, the adapter envelope, spin, typed-interfaces probe
rig_nodes/supervise.py process groups, portable signals, port preflight, Child supervision (ex rig.py)
rig_nodes/fictrac.py FicTrac's line grammar + column names (pure)
rig_nodes/fictrac_udp_adapter.py, kinefly_udp_adapter.py stream adapters → /<src>/raw (+ /fictrac/frame typed)
rig_nodes/bias_node.py, bias_cmd.py, bias_health.py BIAS camera: /bias/cmd|reply|status|frames
rig_nodes/host_monitor.py, host_sampler.py the PC's pressure (CPU per core mean+peak, memory, disks, network, NVIDIA GPUs, top processes) on /rig/host at 1 Hz (0.3.0)
rig_nodes/recorder.py one MCAP bag per run (rosbag2_py), a writer library — the lifecycle belongs to the application
rig_nodes/mcap_index.py reindex a finished bag; recover a torn one
rig_nodes/cdr.py decode CDR from the ros2msg schema text in a bag (no ROS process)
rig_nodes/foxglove_json.py String-topic bag → Foxglove-plottable JSON MCAP (generic; applications add `enrich` / `derive` hooks)
rig_nodes/graph_dump.py the live graph as Mermaid / DOT / a rate table
rig_nodes/build_interfaces.py colcon build of src/rig_interfaces (the .msg author's route) + `--check`
rig_nodes/sims/ fictrac_sim, kinefly_sim, bias_sim — the device programs, stood in
rig_nodes/zenoh.py + config/ the loopback-only Zenoh configs (one pair for every distro today; `config/<distro>/` wins when present), shipped in the package; `rig-router` starts rmw_zenohd with them
rig_nodes/runtime.py what this process runs on (ROS distro, rmw, RoboStack set, Python, package set) — the recorder writes it into every bag's metadata
channel/ the published conda channel: README always; payloads only on a tagged release commit
tests/ test_pure.py (no graph), test_host_sampler.py (no graph), test_device_nodes.py (sims → nodes → topics → loss accounting)
bench/ shm_bench.py, shm_bench_qpc.py, mcap_bench.py, sizing/ (measurements behind docs/)
docs/ shm-bench.md, typed-distribution-decision.md, typed-windows.md, host-monitor.md, ros-distro-switch.md (the distro switch + the Lyrical rehearsal)
foxglove/ generic Foxglove layouts (none yet; the arena layouts stay in webDisplayTools)
.github/workflows/package-set.yml build both packages on win-64 / osx-arm64 / linux-64, index the channel, install-test it; the allowed-to-fail `lyrical` job builds + tests the same tree on ROS 2 Lyrical
pixi install # solves RoboStack Jazzy and BUILDS src/rig_interfaces + src/rig_nodes in place (pixi-build)
pixi install -e lyrical # the same on RoboStack Lyrical (Python 3.14); every task below takes `-e lyrical` (docs/ros-distro-switch.md)
pixi run test-pure # the pure modules, no router needed
pixi run test-host # the host sampler on a fake psutil, no router needed
pixi run test-sim # the FicTrac simulator (record packing, replay, model fly)
pixi run test # the device-node conformance run (starts its own Zenoh router on 127.0.0.1:7447)
pixi run router # a router for manual runs; then e.g. `pixi run fictrac-sim` + `pixi run fictrac-adapter`
pixi run graph # the live graph as Mermaid
pixi run runtime # what this environment runs on — the dict the recorder writes into every bagThe ROS distribution is one switch: a [feature.<distro>] in pixi.toml (channel, mutex pin,
Python pin); the package manifests under src/ name no distro and are built for the set of the
environment that builds them. docs/ros-distro-switch.md is the record (what differs between
sets, what the Lyrical rehearsal found, what blocks a migration).
Building rig_interfaces from source needs a C/C++ compiler (rosidl generates C typesupport and
a Python extension): clang on macOS and GCC on Linux come from conda-forge's cxx-compiler;
Windows needs the MSVC v143 toolset and a short build path (pixi config set --local detached-environments C:\px), see docs/typed-windows.md. Rigs never build it: they install the
channel packages.
Only one ROS graph per host at a time: Zenoh discovery crosses workspaces, so a device-node test here and the arena harness in a webDisplayTools checkout must not run at the same time.
A consumer's pixi.toml adds the channel and pins the package set:
[workspace]
channels = ["conda-forge", "robostack-jazzy", "https://raw.githubusercontent.com/reiserlab/rig-nodes/v0.4.0/channel"]
[dependencies]
ros2-distro-mutex = "0.19.*" # the RoboStack set the packages were built against (macOS ≥ 14 for osx-arm64); the channel is Jazzy-only
rig_nodes = "==0.4.0"
rig_interfaces = "==0.4.0" # optional: without it the nodes publish String JSON (`--typed auto`)and starts nodes with their console scripts (rig-fictrac-adapter, rig-bias-node, …) or
python -m rig_nodes.<module>; launchers import the supervision primitives from
rig_nodes.supervise. Developer path without a release: a sibling checkout of this repository
and a feature that swaps the two pins for path dependencies (webDisplayTools' rig/pixi.toml
has one: pixi install -e dev). Package versions belong in every run's metadata (consumer side).
One version for both packages, one tag, one channel directory (record §7.6, §7.9).
- Bump the version in four places:
src/rig_interfaces/package.xml,src/rig_interfaces/pixi.toml,src/rig_nodes/pixi.toml,src/rig_nodes/pyproject.toml(andrig_nodes/__init__.py,pixi.toml's workspace version, the top of CONTRACT.md). - Push; the
package-setworkflow builds both packages on the three platforms, indexes the channel, checks every package'sros2-distro-mutexand Python pins against the expected set, and installs the channel in a clean directory on each platform and runs the tests against the installed packages (the release gate). - Download the
rig-nodes-channelartifact of that run intochannel/(gh run download <id> -n rig-nodes-channel -D channel), updatechannel/README.md(files, sha256s, pins), commit asrelease: package set vX.Y.Z— withgit add -f channel/*/: the payload directories are gitignored, and a plaingit addcommits the README alone (the first v0.3.0 commit, 2026-10-28, did; the tag had to be moved to the commit that carries the files), tagvX.Y.Zon that commit, push with the tag. The payload is committed once, on that commit, and removed again in the next commit; git history serves the pinnedraw.githubusercontent.com/…/vX.Y.Z/channelURL. On the tag push the workflow also uploads to a prefix.dev channel when the repository variableRIG_NODES_CHANNELand the secretPREFIX_API_KEYexist (the lab does not own one yet). - Consumers re-pin and re-lock (webDisplayTools:
rig/pixi.toml+pixi lock, through its own release tiers), and their CI's cross-repository smoke runs against the new set.
Keep an installable copy of each released channel on the group share, so a channel outage never blocks a rig rebuild.
Device-node conformance lives here (tests/test_device_nodes.py: simulator → node → topics →
loss accounting, plus the recorder on real CDR). Runner conformance belongs to pyDisplayTools and
browser-contract conformance to webDisplayTools, whose harness (rig/tests/test_ros2_poc.py)
runs the whole arena graph against the installed packages and is the cross-repository smoke.
Moved from webDisplayTools (rig/, formerly ros2-poc/; fictrac-bridge/fictrac_sim.py) on
2026-10-26 with git filter-repo; the move list is the record's §3.3. The arena_interfaces →
rig_interfaces rename (2026-10-26, webDisplayTools #270) preceded the move so that no released
bag carries a typed topic under the old name. The measurement records in docs/ keep their
original paths (rig/…, ros2-poc/…) as history.