Quick Reference
The .env file
All environment variables can be set in a .env file at the checkout root:
cp .env.example .env
# Edit .env — uncomment and adjust values as needed
- justfile —
.envis auto-loaded. Missing file silently ignored. - direnv —
.envrcsources.envif present. - Manual —
set -a; source .env; set +abeforecargo build.
Variables in .env take precedence over justfile defaults but are overridden
by explicit shell exports. The variables themselves are listed in the user
book’s Environment Variables.
Finding Commands
Root commands are grouped by audience:
just --list # grouped root command overview
just --groups # group names
just --group main --list # normal development workflows
just --group full-matrix --list # build-all / fixtures / test-all / ci
just --group setup --list # setup and doctor entry points
just --group maintenance --list # clean and generated-binding commands
just --group docs --list # book and API docs
Platform and backend commands stay namespaced:
just --group full-matrix --list zephyr
just zephyr build-fixtures
just zephyr build-all
just qemu setup-network # QEMU TAP networking only
just zephyr help # Zephyr-specific help
Install the local book tooling before previewing docs:
just setup-docs # mdbook + mdbook-mermaid
just book-serve # serve book/src with live reload
just book # full deployed preview with API docs
API documentation
The API reference the user book links (book/src/api/…) is Doxygen and
rustdoc output built from the tree:
just doc # all of the below
just doc-rust # rustdoc
just doc-c # target/doxygen/c/
just doc-cpp # C++ API
just doc-rmw-cffi # RMW C ABI
just doc-platform-cffi # target/doxygen/platform-cffi/
The Doxygen recipes need doxygen and skip with a warning when it is not
installed (nros setup --system composes the install command).
Test profiling & slow-test reporting
just test and just test-all always print the slowest tests after the
run summary — the top 20 by duration (binary, test name, time, status),
parsed from the active profile’s target/nextest/<profile>/junit.xml. No
flag needed; it is part of the normal output.
Deeper profiling is opt-in and adds nextest’s experimental event/output recording plus artifact export. It preserves the normal nextest execution model (same filters, cargo profile, parallelism) — it only records and exports, so leave it off for routine runs:
NROS_NEXTEST_RECORD=1 just test
NROS_NEXTEST_RECORD=1 just test-all
Each profiled run writes a timestamped directory under tmp/ with a
stable -latest symlink:
tmp/nextest-profile-test-YYYYMMDD-HHMMSS/ tmp/nextest-profile-test-latest -> …
tmp/nextest-profile-test-all-YYYYMMDD-HHMMSS/ tmp/nextest-profile-test-all-latest -> …
Artifacts in that directory:
nextest-run.zip— portable recording archive; replay with full captured output (incl. successful tests) viacargo nextest replay.nextest-trace.json— Chrome/Perfetto timeline (slot/group occupancy, idle slots, retries, long-pole tests). Canonical concurrency artifact.junit.xml— copy of the run’s JUnit report.env.txt,command.txt— the knobs and exact command used.
Knobs:
| Variable | Effect |
|---|---|
NROS_NEXTEST_RECORD=1 | Enable recording + artifact export. |
NROS_NEXTEST_RECORD_DIR=<path> | Override the output dir (and its -latest link). |
NROS_NEXTEST_TRACE_GROUP_BY=slot|binary | Perfetto grouping; default slot (wall-clock/concurrency view). |
NROS_NEXTEST_REPLAY_LOG=1 | Also write nextest-replay.log (full captured stdout/stderr — can be large on chatty tests). Off by default; rely on the portable archive otherwise. |
NROS_NEXTEST_RECORD_KEEP_STATE=1 | Keep the temp NEXTEST_STATE_DIR (<dir>/state); removed after export otherwise. |
NROS_NEXTEST_RUN_PROFILE=fail-fast | Stop at the first failure instead of the default --no-fail-fast full report (uses target/nextest/fail-fast/junit.xml). |
Overhead and retention: recording adds event/output-store writes during
the run, and nextest-run.zip (plus an enabled nextest-replay.log) can
get sizable on output-heavy suites — keep it opt-in for local runs and
prune old tmp/nextest-profile-* directories. Recording uses a
profile-local NEXTEST_STATE_DIR, so it never pollutes the user’s global
nextest record store. Do not reach for --no-capture to inspect
output: it serializes execution and skews every timing. Use the replay
archive instead.
Manual Testing
# Base setup builds/checks the in-tree zenoh router.
just setup base
source ./activate.sh
# Terminal 1: Router
ZENOH_CONFIG_OVERRIDE='listen/endpoints=["tcp/127.0.0.1:7447"];scouting/multicast/enabled=false' ros2 run rmw_zenoh_cpp rmw_zenohd
# Terminal 2: Talker
cd examples/linux/rust/talker && nros configure && nros build
RUST_LOG=info ./build/linux/target/debug/talker
# Terminal 3: Listener
cd examples/linux/rust/listener && nros configure && nros build
RUST_LOG=info ./build/linux/target/debug/listener
Each leaf’s RMW comes from its system.toml [system] rmw, and its
artifact path is build/<image-id>/target/[<triple>/]<profile>/<bin>.
UDP Transport
On the native (host) build, zenoh-pico has built-in UDP support via OS sockets:
# Use UDP instead of TCP for the zenoh locator
NROS_LOCATOR=udp/127.0.0.1:7447 ./build/linux/target/debug/talker
On bare-metal, enable the link-udp-unicast feature to use UDP over smoltcp:
nros = { features = ["rmw-zenoh", "platform-bare-metal", "link-tcp", "link-udp-unicast"] }
The
rmw-zenohfeature here is the lowering of the declared RMW — you declare the backend once insystem.toml([system].rmw/[image.<id>].rmw) and the toolchain sets the cargo feature; the feature is what the build uses, not the user-facing selector. See RFC-0031.
TLS Transport
TLS layers on top of TCP using mbedTLS. Requires a self-signed certificate (or real CA cert) and the link-tls Cargo feature.
Generate a test certificate:
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
-keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=localhost"
Native (host) (requires libmbedtls-dev – installed by just setup base):
# Terminal 1: Router with TLS
ZENOH_CONFIG_OVERRIDE='listen/endpoints=["tls/localhost:7447"];scouting/multicast/enabled=false' ros2 run rmw_zenoh_cpp rmw_zenohd \
--cfg 'transport/link/tls/listen_certificate:"cert.pem"' \
--cfg 'transport/link/tls/listen_private_key:"key.pem"'
# Terminal 2: Talker with TLS
cd examples/linux/rust/talker && nros configure
cd .. && cargo build --manifest-path talker/Cargo.toml \
--config talker/build/linux/nros-cargo.toml --features link-tls
NROS_LOCATOR=tls/localhost:7447 \
ZENOH_TLS_ROOT_CA_CERTIFICATE=cert.pem \
./talker/build/linux/target/debug/talker
link-tls is a transport feature of the leaf’s own crate, so it stays a
--features flag; the RMW is not — that is [system] rmw in system.toml.
Bare-metal (QEMU ARM):
On bare-metal, only base64-encoded certificates are supported (no filesystem).
Build with --features link-tls:
# Build TLS-enabled examples
cd examples/mps2-an385-baremetal/rust/talker && nros configure
cd .. && cargo build --release --features link-tls \
--manifest-path talker/Cargo.toml \
--config talker/build/mps2-an385-baremetal/nros-cargo.toml
The CA certificate must be passed via ZENOH_TLS_ROOT_CA_CERTIFICATE_BASE64 at runtime
(through the zenoh config), or embedded in the binary at build time.
ROS 2 Interop
# Terminal 1: Router
ZENOH_CONFIG_OVERRIDE='listen/endpoints=["tcp/127.0.0.1:7447"];scouting/multicast/enabled=false' ros2 run rmw_zenoh_cpp rmw_zenohd
# Terminal 2: nros talker
cd examples/linux/rust/talker && nros configure && nros build
RUST_LOG=info ./build/linux/target/debug/talker
# Terminal 3: ROS 2 listener
source /opt/ros/humble/setup.bash
export RMW_IMPLEMENTATION=rmw_zenoh_cpp
ros2 topic echo /chatter std_msgs/msg/String --qos-reliability best_effort
Actions
# Terminal 1: Router
ZENOH_CONFIG_OVERRIDE='listen/endpoints=["tcp/127.0.0.1:7447"];scouting/multicast/enabled=false' ros2 run rmw_zenoh_cpp rmw_zenohd
# Terminal 2: Action server (Fibonacci example)
cd examples/workspaces/rust && nros run action_bringup:linux_action_server
# Terminal 3: Action client
cd examples/workspaces/rust && nros run action_bringup:linux_action_client
The action pair is two images of the Rust workspace’s action_bringup
(phase-535 W4). nros run builds an image if it is stale.
Zephyr action tests:
just zephyr build-all # Build all Zephyr examples (Rust + C + C++ + XRCE)
just zephyr test # Run zenoh E2E tests (covers actions)
Docker Development Environment
Docker provides QEMU 7.2 (from Debian bookworm) which fixes TAP networking issues present in Ubuntu 22.04’s QEMU 6.2.
# One-time setup: add yourself to docker group
sudo usermod -aG docker $USER
# Log out and back in, or run: newgrp docker
# Build and use Docker environment
just docker build # Build nano-ros-qemu image
just docker shell # Interactive shell
just docker test-qemu # Run QEMU tests in container
just docker help # Show all Docker commands
QEMU Bare-Metal Testing
Run bare-metal Cortex-M3 examples on QEMU (MPS2-AN385 machine with LAN9118 Ethernet).
# Build prerequisites
just qemu build-zenoh-pico # Build zenoh-pico for ARM Cortex-M3
just qemu build # Build all QEMU examples
# Non-networked tests (no setup required)
just qemu test # Bare-metal QEMU integration tests
# Networked talker/listener test (Docker Compose - recommended)
just docker test-qemu # Runs zenohd, talker, listener in separate containers
Docker Compose Architecture:
┌─────────────────────────────────────────────────────────────┐
│ Docker Network: 172.20.0.0/24 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ zenohd │ │ talker │ │ listener │ │
│ │ 172.20.0.2 │ │ 172.20.0.10 │ │ 172.20.0.11 │ │
│ │ │ │ ┌───────┐ │ │ ┌───────────────┐ │ │
│ │ │ │ │ QEMU │ │ │ │ QEMU │ │ │
│ │ │ │ │ ARM │──┼──┼──│ ARM │ │ │
│ │ │ │ │ TAP │ │ │ │ TAP │ │ │
│ │ │ │ └───────┘ │ │ └───────────────┘ │ │
│ └──────▲──────┘ └──────┼──────┘ └─────────┼───────────┘ │
│ └────────────────┴───────────────────┘ │
│ NAT to zenohd │
└─────────────────────────────────────────────────────────────┘
Manual networked test (3 terminals, requires host TAP setup):
# Terminal 1: Setup network + start router
just qemu setup-network # Requires sudo
ZENOH_CONFIG_OVERRIDE='listen/endpoints=["tcp/0.0.0.0:7447"];scouting/multicast/enabled=false' ros2 run rmw_zenoh_cpp rmw_zenohd
# Terminal 2: Talker (192.0.2.10)
bin=build/cargo-fixtures/baremetal/thumbv7m-none-eabi/nros-relwithdebinfo
./scripts/qemu/launch-mps2-an385.sh --tap tap-qemu0 --binary "$bin/qemu-bsp-talker"
# Terminal 3: Listener (192.0.2.11)
./scripts/qemu/launch-mps2-an385.sh --tap tap-qemu1 --binary "$bin/qemu-bsp-listener"
Build those first with just qemu build-fixtures (or just build-test-fixtures lane=linux).
The artifact directory is computed, not fixed. Since phase-340 P2 a
platform’s cargo fixtures build into a shared group directory under
build/cargo-fixtures/<group>/ rather than each leaf’s own target/,
and the profile is nros-relwithdebinfo, not release. The group name
can carry a hash suffix when a row builds with distinct env, so treat
the path above as what it is on a default build, not as a guarantee —
scripts/build/fixtures-target-dir.sh (nros_fixture_row_artifact_dir)
is the single source of truth, and it is what the test side uses to
find the same file. That helper takes the row’s cargo args and env as
arguments, so a script that did not itself run the build should ask for
the row by id — nros_fixture_row_artifact_dir_by_id <row-id> reads all
three key fields from the manifest, and cannot disagree with the builder
about which group a row landed in (issue 1025).
Run just qemu help for more options.
Zephyr Setup
just setup zephyr # Initialize workspace at $repo/zephyr-workspace/
just zephyr test # Run zenoh tests (native_sim uses NSOS on host loopback)
just zephyr test-xrce # Run XRCE tests
The workspace lives at $repo/zephyr-workspace/ by default (gitignored).
Set $NROS_ZEPHYR_WORKSPACE to install elsewhere. Legacy sibling installs
at ../nano-ros-workspace/ are auto-detected; run
./scripts/zephyr/migrate-workspace.sh to consolidate.
See Zephyr for full details.