Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Configuration Guide

nano-ros config is language-agnostic and has one home per concern — no setting lives in two places, and nothing is merged across files. The authored surfaces are Cargo.toml metadata (Rust) or the CMake nano_ros_* functions (C/C++), a universal system.toml, package.xml, and launch XML — plus Kconfig for the embedded build (Zephyr). This guide covers what each file owns, the embedded deploy config both languages bake at build time, and the standalone config.toml for hand-written no_std apps.

Two files you may find in old material are retired: the nros.toml per-package/workspace file (rejected by the CLI; see below) and the old config.toml [network]/[zenoh]/[scheduling] schema. Design of record: RFC-0004.

One home per concern

FileOwnsPer
Cargo.tomlRust build: crate, language deps, the RMW feature menu (rmw-zenoh/rmw-cyclonedds/rmw-xrce); node identity via [package.metadata.nros.node]; entry/boot via [package.metadata.nros.entry]; embedded net config via [package.metadata.nros.deploy.<target>]; workspace membership via [workspace.metadata.nros]Rust project
CMakeLists.txtC/C++ build: targets, language deps, the NROS_RMW option; node/entry registration via nano_ros_auto_add_library + nros_components_register_node (RFC-0057; nano_ros_node_register remains as the compat spelling) / nano_ros_entryC/C++ project
.cargo/config.toml[patch.crates-io] dependency injection only (written by nros sync; local crate + generated-msg paths), plus the cargo [build]/[target]/[env] knobs (target triple, runner, rustflags). No nano-ros runtime config.Rust project
package.xmlROS package identity + msg <depend>s (codegen input for nros generate)all
system.tomlSystem topology — components, deploy targets, domain, RMW, capability axes ([safety], [param_services]), tiers. The language-agnostic universal descriptor (same schema for Rust/C/C++). Optional for single-node (the toolchain synthesises an implicit 1-component system when absent).bringup pkg
config.toml (standalone)Hand-written no_std direct-mode apps only[node] / [[transport]] / [node.rt], compile-baked via Config::from_toml(include_str!(…)). Apps that use nros::main!() / codegen do not have one — they use the deploy metadata above.embedded single-node app (no codegen)

Boundary rule. If a knob changes what is compiled/linked, it lives in the build file (Cargo.toml feature / CMakeLists.txt option). If it changes the system topology (components, deploy, domain, RMW), it lives in system.toml. If it is the physical link + router address a target boots with, it lives in the deploy config ([package.metadata.nros.deploy.<t>] for Rust / the package.xml <export><nano_ros deploy=… rmw=…/> tuple for C/C++).

Config home by language × scale

Mirrors RFC-0004 §3:

Single-nodeWorkspace
RustCargo.toml [package.metadata.nros.{node,entry,deploy.<t>}] (+ nros::main!); optional system.toml to pin rmw/domainroot [workspace.metadata.nros] + node [package.metadata.nros.node] + entry [package.metadata.nros.entry] + bringup system.toml
C / C++CMakeLists.txt + package.xml (the <nano_ros deploy=… rmw=…/> tuple); optional system.tomlnros_components_register_node / nano_ros_entry per pkg + same system.toml + package.xml

Where a concern has both a native-idiom projection and a system.toml, the resolution is a fixed precedence ladder, not a merge: explicit CLI/build flag (--rmw / -DNANO_ROS_*) > system.toml ([image.<id>] > [system])

the per-package projection ([package.metadata.nros.*] / CMake) > built-in default. nros config show prints the resolved effective config with per-value provenance; nros check flags values still sourced from legacy files.

Full design rationale: RFC-0004 (configuration & transports); RMW backend selection & lowering is RFC-0031. For the multi-RMW runtime topic-forwarding bridge (a separate file/feature), see nros-bridge.toml.

Embedded deploy config (codegen apps — the common case)

An embedded app built through nros::main!() (Rust) or the CMake entry codegen (C/C++) declares its network + router config per deploy target in its build manifest. The toolchain bakes it at compile time — there is no config file on the device and nothing is parsed at runtime.

Rust[package.metadata.nros.deploy.<target>] in the app’s Cargo.toml. nros::main!() bakes the block into a DeployOverlay that BoardEntry::run_with_deploy applies onto the board’s boot Config:

# Cargo.toml (e.g. examples/qemu-arm-baremetal/rust/talker)
[package.metadata.nros.node]
class = "talker_pkg::Talker"
name  = "talker"

[package.metadata.nros.deploy.mps2-an385]
board   = "mps2-an385"         # board crate (optional where unambiguous)
rmw     = "zenoh"
locator = "tcp/192.168.1.1:7447"
ip      = "192.168.1.10"
gateway = "192.168.1.1"
netmask = "255.255.255.0"
# domain_id = 0

C/C++ — the <export><nano_ros …/> tuple in the package’s package.xml (RFC-0048 §4; the retired nano_ros_deploy() cmake call’s successor). Domain/locator ride the build config (config.toml / -DNROS_ENTRY_LOCATOR), not the tuple:

<!-- package.xml (e.g. examples/qemu-arm-freertos/c/talker) -->
<export>
  <build_type>ament_cmake</build_type>
  <nano_ros deploy="freertos" board="mps2-an385-freertos" rmw="zenoh"/>
</export>

RT/stack/priority for a single-node embedded app comes from board-crate Cargo features + Kconfig (prj*.conf on Zephyr), not a config file. Multi-node RT is declared in system.toml ([tiers.<name>.<rtos>] + [[node_overrides]]) — see RFC-0015.

Periodic timers after a stall

If a tier is preempted past several periods of a periodic timer, the backlog is coalesced, not replayed: the timer fires ONCE and the missed periods are counted. Phase is preserved, so activations stay on the declared cadence grid rather than drifting by each stall. This is the default (TimerOverrunPolicy::Skip) and matches rclcpp and Zephyr’s k_timer.

Replay is available — Executor::set_timer_overrun_policy(.., CatchUp) — and is the right choice for a counter or accumulator, where every tick carries state that must not be lost. It is the wrong choice for a control loop: the replayed activations compute with stale inputs and emit a burst of commands inside a fraction of a period.

Overruns are counted per timer (Executor::timer_overruns) and reported as the timer-overrun-runtime violation. Watch that rather than a publish-rate check: under CatchUp a replayed burst makes the achieved rate look nominal — measured 100.03 Hz on a declared 100 Hz loop while the tier was stalling for up to 611 ms at a time — so a rate monitor reports health during exactly the fault it exists to catch. Rationale and the measurements: RFC-0002 § 4.4a.

Timer periods are microseconds throughout, including across the C ABI.

Standalone config.toml — hand-written no_std apps (no codegen)

A hand-written single-node app that writes its own main() and bypasses nros::main!() keeps its net config in a file, not hardcoded in Rust: a sibling config.toml carrying [node] / [[transport]] / [node.rt], compile-baked with include_str! and parsed by the board crate’s Config::from_toml.

# config.toml

[node]
domain_id = 0              # ROS 2 domain ID (0–232)
# namespace = "/"
# rmw = "zenoh"            # ACTIVE backend; must match a LINKED backend
                           # (the build file picks what is linked)

# One transport per session. A single ethernet/wifi/serial entry is the
# common case. In-process topic forwarding is the separate [[bridge]] path.
[[transport]]
kind    = "ethernet"       # ethernet | wifi | serial | can
ip      = "10.0.2.10/24"   # CIDR — the prefix rides the address
mac     = "02:00:00:00:00:00"
gateway = "10.0.2.2"
locator = "tcp/10.0.2.2:7447"

[node.rt]                  # scheduling / real-time (RTOS); omit for defaults
app_priority         = 12
app_stack_bytes      = 65536
zenoh_read_priority  = 16
zenoh_lease_priority = 16
poll_priority        = 16
poll_interval_ms     = 5

Per-kind transport fields:

kindfields
ethernetip (CIDR), mac, gateway, interface
wifissid, password, optional static ip/gateway
serial / candevice, baudrate
allid, rmw, locator

ip is CIDR (10.0.2.10/24) — the board derives the prefix or netmask from it. A serial locator carrying # (serial/UART_0#baudrate=115200) is fine — quote it.

Consumption (compile-baked; no filesystem on the target):

use nros_board_mps2_an385::{Config, run};

fn main() -> ! {
    run(Config::from_toml(include_str!("../config.toml")), |config| {
        let exec = ExecutorConfig::new(config.zenoh_locator)
            .domain_id(config.domain_id);
        // ...
    })
}

Pick by whether the app uses codegen: nros::main!() app → deploy metadata (above); hand-written main()config.toml. Only the old config.toml schema ([network]/[zenoh]/[scheduling]) is retired.

Retired files

  • nros.toml — retired in full. A workspace-root nros.toml is rejected by the CLI (NrosTomlNotSupported; migrate with the nros-v0.5.0 tag’s nros migrate workspace — the one-shot verb is retired on newer trees, #186); the legacy per-package overlay is a deprecated fallback that nros check flags; the embedded-runtime role never shipped (no example ever declared one). If a doc tells you to write nros.toml, it predates the migration — the content belongs in deploy metadata or system.toml.
  • Old config.toml schema ([network]/[zenoh]/[scheduling]) — retired; superseded by the deploy class (net) + board features / Kconfig (RT). The direct-mode config.toml above is the kept, supported shape.

Build-time environment variables

Read during cargo build (by build.rs) or by justfile recipes. Set in .env or export in your shell.

SDK paths

Auto-resolved by nros setup <board>; override if SDKs live elsewhere.

VariableDefaultDescription
FREERTOS_DIRthird-party/freertos/kernelFreeRTOS kernel source
FREERTOS_PORTGCC/ARM_CM3FreeRTOS portable layer
LWIP_DIRthird-party/freertos/lwiplwIP source
FREERTOS_CONFIG_DIRBoard crate’s config/FreeRTOSConfig.h
NUTTX_DIR / NUTTX_APPS_DIRthird-party/nuttx/…NuttX RTOS + apps
THREADX_DIR / NETX_DIRthird-party/threadx/…ThreadX + NetX Duo
THREADX_CONFIG_DIR / NETX_CONFIG_DIRBoard crate’s config/tx_user.h / nx_user.h

Buffer-tuning vars (ZPICO_*, XRCE_*, NROS_*) are optional — see the Environment Variables Reference.

Binary-size knobs (embedded)

On a constrained MCU, two build-time env vars (set in the example’s .cargo/config.toml [env], like the other NROS_* tuning) shed the parts a brokered client doesn’t need:

VariableDefaultEffect
NROS_LINK_IPonNROS_LINK_IP=0 on a serial-only node drops the IP link layer — zenoh-pico’s TCP/UDP link C (Z_FEATURE_LINK_TCP/UDP=0) and (with --gc-sections) the smoltcp platform impl. Serial link stays.
NROS_SMOLTCP_MAX_SOCKETS / NROS_SMOLTCP_MAX_UDP_SOCKETS4 / 2Sized for DDS RTPS (3 UDP/participant). A zenoh/XRCE client multiplexes everything over one session → set both to 1 to drop the spare socket buffers (≈8 KB each).
NROS_HEAP_SIZEper-platform-crate (64 KB nros-platform-mps2-an385, 32 KB nros-platform-stm32f4)Decimal bytes for the bare-metal static heap. The defaults are generous; size to the RMW’s working set (table below). E.g. NROS_HEAP_SIZE = "24576" on a zenoh-pico node cut the mps2-an385 .data 66 → 25 KB (−41 KB).

Static-heap sizing by backend (bare-metal FreeListHeap, set via NROS_HEAP_SIZE):

BackendPeak working setRecommended heapNotes
zenoh-pico (TCP)~16 KB24–32 KB (≈2× for fragmentation)peer middleware; alloc-based session/buffers
zenoh-pico (serial)lighter than TCP16–24 KBno TCP link buffers; verified running at 16 KB
XRCE (Micro-XRCE-DDS)~3 KB (micro-ROS figure)~8 KBstatic pools, discovery offloaded to the agent — the RAM-minimal backend; a measured bare-metal XRCE figure is pending an example (no bare-metal XRCE example ships yet — XRCE bare-metal needs a custom-transport injection)

Measured footprint

Honest, reproducible numbers per (platform, transport, backend, profile) — built with the in-tree examples, the release profile is cargo’s default (opt-3), the size profile is the scaffolded [profile.size] (opt-s + lto

  • strip, see the size-tuned release profile notes). RAM = data + bss. All cells are after --gc-sections + the size knobs above are applied where noted; the serial cell ships with the recipe below.
platformtransportbackendprofiletext (flash code)databssRAM total
qemu-arm-baremetal (mps2-an385, cortex-m3)ethernet (smoltcp)zenoh-picorelease177.4 KB67.0 KB91.7 KB158.7 KB
qemu-arm-baremetalethernetzenoh-picosize158.3 KB67.0 KB91.7 KB158.7 KB
qemu-arm-baremetalserial (no IP stack)zenoh-picorelease128.6 KB25.2 KB75.8 KB101.0 KB
qemu-arm-baremetalserialzenoh-picosize + recipe116.1 KB25.2 KB75.8 KB101.0 KB
stm32f4 (thumbv7em-eabihf, cortex-m4) ¹ethernetzenoh-picorelease186.9 KB13.7 KB123.0 KB136.7 KB
stm32f4 ¹ethernetzenoh-picosize138.1 KB13.7 KB123.0 KB136.7 KB
qemu-arm-freertos (cortex-m3 + lwIP, RTOS-reused stack)ethernet (lwIP)zenoh-picorelease240.6 KB10.7 KB3.3 MB3.3 MB
qemu-arm-baremetal (serial)serial (custom XRCE transport)XRCEsize, heap 24 KB, tight XRCE pools60.3 KB25.2 KB (heap 24 KB)8.8 KB~34 KB
micro-ROS reference (XRCE)serialXRCE-DDS Client-Os< 75 KB~3 KB~3 KB peak

¹ The two stm32f4 rows are historical: the board crates left the tree in a later cleanup, so nothing in-tree reproduces them today. They stay because a Cortex-M4F-with-its-own-stack data point is still the closest published figure for that class, and deleting a measurement is worse than dating it. The chip crate the numbers were built against (nros-platform-stm32f4) is still here.

The XRCE row uses tight per-session XRCE pools — set in the example’s .cargo/config.toml [env] and read by nros-rmw-xrce-cffi’s build.rs: NROS_XRCE_STREAM_HISTORY=4, NROS_XRCE_CUSTOM_TRANSPORT_MTU=512, NROS_XRCE_MAX_SUBSCRIBERS=1, NROS_XRCE_MAX_SERVICE_SERVERS=1, NROS_XRCE_MAX_SERVICE_CLIENTS=1, NROS_XRCE_SUBSCRIBER_RING_DEPTH=1, NROS_XRCE_BUFFER_SIZE=256. Vendor defaults grow xrce_session_state_t to ~390 KB (which wouldn’t fit a 24 KB heap); these knobs drop it to ~12 KB. Defaults are unchanged for hosted / non-tight-RAM consumers — the env vars are pure opt-in.

How to read this:

  • The size profile (opt-s) shrinks .text by ~10–26 % with .bss/.data unchanged (opt-level doesn’t touch static buffers — those are the env knobs above). -Oz is not used — on smoltcp examples it grows .bss +24 KB by defeating opt-3’s per-socket dead-buffer DCE (see the size-tuning notes above).
  • Switching ethernet → serial sheds ~50 KB text + ~42 KB .data (no smoltcp stack, no IP link C, tuned heap) — the structural lever.
  • FreeRTOS + lwIP cells .bss is dominated by lwIP’s heap + FreeRTOS task stacks (3 MB is the configured headroom, not nano-ros overhead).
  • The micro-ROS / XRCE row is a reference, not a nano-ros measurement — no bare-metal XRCE example ships yet (needs a custom-transport injection); the path to parity is XRCE + serial + static pools.

Size-minimal recipe

Smallest measured nano-ros configuration today (qemu-arm-baremetal serial talker, 116 KB text / 101 KB RAM):

# Cargo.toml
[profile.size]
inherits = "release"
opt-level = "s"
lto = "fat"
codegen-units = 1
debug = false
strip = true
# .cargo/config.toml — gc + serial knobs
[target.thumbv7m-none-eabi]
rustflags = [
    "-C", "link-arg=--gc-sections",   # 204.8 — strip unreferenced fns/data
    "-C", "link-arg=-Tlink.x",
]

[env]
NROS_LINK_IP        = "0"      # 204.7 — drop zenoh-pico TCP/UDP link C
ZPICO_NO_SMOLTCP    = "1"      # skip smoltcp glue on bare-metal
# Heap floor: the per-entry executor backing is a single ~75 KB
# allocation, so a `nros::main!` image needs ≥128 KB (the #176 board
# default) — the pre-271 24 KB "zenoh-pico working set" figure OOMs at
# boot. HEAP is `.bss` (no flash cost); shrink below the default only on
# a non-`nros::main!` direct-mode image with a measured smaller peak.
NROS_HEAP_SIZE      = "131072"
NROS_SMOLTCP_MAX_SOCKETS     = "1"   # 204.2 — brokered client multiplexes
NROS_SMOLTCP_MAX_UDP_SOCKETS = "1"

Build with cargo build --profile size, or fleet-wide via NROS_CARGO_PROFILE=size just <plat> build. nros new --platform baremetal already scaffolds the [profile.size] + the .cargo/config.toml shape (Phase 204.7/204.8); uncomment the serial block when you swap to a serial transport.

The deeper RAM win waits on XRCE on bare-metal (the ~3 KB-class client + static pools, with discovery offloaded to the agent) — tracked separately; zenoh-pico’s SUBSCRIBER_BUFFERS + alloc-based session are what keep this row’s .bss ~76 KB.

Cargo features (which RMW/platform is linked)

Features select the linked RMW backend, platform, and ROS edition. The deploy config’s rmw (or system.toml [system].rmw) picks which linked backend is active — the two are different layers (link vs run). Matrix + mutual-exclusion rules: Platform Model.

[dependencies]
nros = { path = "…/nros", default-features = false, features = [
    "rmw-cffi",            # generic C-vtable runtime registry
    "platform-bare-metal", # or platform-{freertos,nuttx,threadx,zephyr,posix}
    "ros-humble",          # or ros-iron
    "std", "alloc",        # optional, target-dependent
] }
# Exactly one RMW backend crate; its registration runs before main:
nros-rmw-zenoh = { path = "…/nros-rmw-zenoh", features = ["platform-bare-metal", "link-tcp", "ros-humble"] }
# …or nros-rmw-xrce-cffi / the cyclonedds CMake backend

Runtime environment (native host only)

On the native host build, ExecutorConfig::from_env() reads at process start (embedded targets bake their config at build time instead):

VariableDescriptionDefault
ROS_DOMAIN_IDROS 2 domain ID0
NROS_LOCATORRouter address (legacy alias ZENOH_LOCATOR)tcp/127.0.0.1:7447
NROS_SESSION_MODEclient / peer (legacy alias ZENOH_MODE)client
ZENOH_TLS_ROOT_CA_CERTIFICATE*TLS CA cert (path / base64)(none)

By deployment scenario

ScenarioConfig sourceCargo featuresNotes
Desktop (POSIX)env (ExecutorConfig::from_env())rmw-cffi, platform-posix, std + zenoh deprun the router locally (ros2 run rmw_zenoh_cpp rmw_zenohd, ZENOH_CONFIG_OVERRIDE='listen/endpoints=["tcp/127.0.0.1:7447"];scouting/multicast/enabled=false')
QEMU bare-metal[package.metadata.nros.deploy.<t>] ip/mac/gateway/locatorrmw-cffi, platform-bare-metal, ros-humble + zenohTAP/slirp bridge
FreeRTOS hardwaredeploy metadata / the package.xml tuple + board features (RT)…, platform-freertos, …FREERTOS_DIR/LWIP_DIR
ESP32 WiFideploy metadata (ssid/password via build env)…, platform-bare-metal, …SSID/PASSWORD build env
Zephyr moduleKconfig overlay (prj-<rmw>.conf)(Kconfig → features)
Hand-written no_std (no codegen)standalone config.tomlConfig::from_tomlper-boardnet config in a file, not in code
Minimal RAM (XRCE serial)config.toml [[transport]] kind="serial" or deploy + xrce depXRCE_* buffer tuning

.env

cp .env.example .env   # uncomment + adjust; gitignored; auto-loaded by just + direnv