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 a universal system.toml, package.xml, and launch XML — plus
Kconfig for the embedded build (Zephyr). Cargo.toml and CMakeLists.txt
carry language-toolchain facts and nothing else. This guide covers what each
file owns, the network identity an embedded image bakes at build time, and the
standalone config.toml for hand-written no_std apps.
Retired surfaces you may find in old material: the
nros.tomlper-package/workspace file (rejected by the CLI), the oldconfig.toml[network]/[zenoh]/[scheduling]schema, every[package.metadata.nros.{node,entry,component,deploy.*}]table, and thepackage.xml<nano_ros deploy=… board=… rmw=…/>tuple. The last two moved intosystem.toml; readers refuse them rather than reading them. The design records are listed under Design background.
One home per concern
| File | Owns | Per |
|---|---|---|
system.toml | Everything about the deployment — [system] (RMW, domain, capability axes like features = ["param_services"] and the [safety] block, tiers), [[component]] (what runs, and its entities), and one [image.<id>] per target (board, locator, the board network file holding its address, os_config = ["board-net.toml"], and how it runs, is flashed and is watched, [image.<id>.run], [image.<id>.flash], [image.<id>.console]). One schema for Rust, C and C++, and the same file whether the project is one package or a workspace. | package, or bringup pkg |
Cargo.toml | Rust toolchain facts only: crate, language deps, the RMW feature menu (rmw-zenoh/rmw-cyclonedds/rmw-xrce) the generated entry selects from. No board, no target triple, no deploy block. | Rust project |
CMakeLists.txt | C/C++ toolchain facts only: targets and language deps, via find_package(nano_ros) + nano_ros_add_executable / nano_ros_auto_add_library + nros_components_register_node (nano_ros_node_register remains as the compat spelling) / nano_ros_entry. No platform, board or RMW set(). | C/C++ project |
build/<image>/nros-cargo.toml | GENERATED, never edited — every cargo setting the board choice implies: [build] target, [target.*] runner and rustflags, the cross compiler, the derived pool sizes as [env], the nano-ros profiles, and the [patch.crates-io] rows that resolve nano-ros’s registry-style names into your checkout. Written by nros build (or nros configure alone), passed to cargo with --config. | image |
package.xml | ROS package identity + msg <depend>s (codegen input for nros interface generate) | all |
config.toml (standalone) | Hand-written no_std direct-mode apps only — [node] / [eth0] / [network.<name>], compile-baked via Config::from_toml(include_str!(…)). Apps that use nros::main!() / codegen do not have one — they use [image.<id>] above. | embedded single-node app (no codegen) |
Boundary rule. If a knob changes what is compiled/linked as a language choice — a C++ standard, an RMW feature the entry may select — it lives in the build file. Everything else about the deployment (components, board, domain, RMW, and the physical link + router address a target boots with) lives in
system.toml. Nothing a board implies is authored anywhere: it is derived from the board descriptor into the generated settings file.
Config home by language × scale
This table has one answer in all four cells, which is the point of it:
| One node | Several nodes | |
|---|---|---|
| Rust | system.toml in the bringup pkg | system.toml in the bringup pkg |
| C / C++ | system.toml in the bringup pkg | system.toml in the bringup pkg |
A system.toml beside a Cargo.toml or CMakeLists.txt is refused, so the
resolver has one input shape and you have one thing to learn. Resolution is a
fixed precedence ladder, not a merge: explicit CLI/build flag (--rmw /
-DNROS_*) > system.toml ([image.<id>] > [system]) > the board
descriptor’s [knobs] > built-in default. nros config show prints the
resolved effective config with per-value provenance; nros check flags values
still sourced from legacy files.
For the multi-RMW runtime topic-forwarding bridge (a separate file/feature), see
nros-bridge.toml.
Embedded image config (codegen apps — the common case)
An embedded app built through nros::main!() (Rust) or the CMake entry
codegen (C/C++) declares its board and its network + router config per
image, in system.toml. The toolchain bakes it at compile time — there is
no config file on the device and nothing is parsed at runtime. The file is the
same in both languages:
# system.toml (e.g. examples/mps2-an385-baremetal/rust/talker)
[system]
name = "qemu_bsp_talker"
rmw = "zenoh"
domain_id = 0
[[component]]
pkg = "qemu_bsp_talker"
class = "qemu_bsp_talker::Talker"
name = "talker"
dispatch = "deferred" # optional: callbacks hand off to a ring
[image.mps2-an385-baremetal]
board = "mps2-an385-baremetal"
locator = "tcp/10.0.2.2:10500"
net = { eth0 = { network = "qemu", host = 10 } }
The [image.<id>] key is the image’s name — what you pass to
nros build <image> — and board is the only thing that has to change to
retarget it. nros::main!() bakes the image’s network fields into a
DeployOverlay that BoardEntry::run_with_deploy applies onto the board’s
boot Config; the C/C++ entry codegen bakes the same values into its
generated glue. Nothing is written in Cargo.toml, in package.xml or on a
cmake command line.
Networks and addresses
An image’s address is a layer-1 fact: the operating system owns the
network device. On Linux that is the kernel; on Zephyr and NuttX it is their
own configuration (Kconfig, devicetree, defconfig); on a board where nano-ros
is the OS (the reference FreeRTOS, ThreadX and bare-metal boards) it is the
board package, through a small board network file the image names with
os_config — one table per interface:
[image.talker]
board = "mps2-an385-baremetal"
os_config = ["board-net.toml"]
# board-net.toml, beside system.toml
[eth0]
network = "qemu"
host = 10 # 10.0.2.10 on QEMU's network
qemu is built in: QEMU’s own user-mode (SLIRP) network, 10.0.2.0/24 with
gateway and router host 10.0.2.2. Every -nic user makes a private copy of
it, so two images on qemu never meet. Any other network is defined in the
board network file beside the interface on it, and the image states its
router as its own locator:
# board-net-sensor.toml
[eth0]
network = "lab"
address = "192.168.50.20"
[network.lab] # e.g. a switch or a Linux bridge with boards on it
subnet = "192.168.50.0/24"
gateway = "192.168.50.1"
[image.sensor]
board = "mps2-an385-baremetal"
os_config = ["board-net-sensor.toml"]
locator = "tcp/192.168.50.1:7447"
Two images on one network each define it in their own file, and the files must agree: one network has one subnet, gateway and lease policy.
Per interface:
networkdefaults to the board’s (qemuon every QEMU board), so[eth0] host = 10is enough there.hostis the last octet within the network’s subnet;addressis the full IPv4, or"dhcp"on a network withdhcp = true(and a board whose interface runs a DHCP client). An interface onqemuthat states no address takes10.0.2.15, the address a plain-nic userguest picks.macis optional. Left out, it is derived from the board interface’smac_prefixand the system, image, network and interface names, so two images never share one by accident.- The gateway and netmask come from the network, so an interface never states them.
- An image dials its own
locator, else[system] locator. On a network other thanqemuone of the two must be stated.
A board may have several interfaces. An image addresses each on its own
network, one table each: [eth0], [eth1].
These fail the build, naming the image (and, for a duplicate, both images):
- an address outside its subnet, equal to the gateway, or on a network no file defines;
- two images with the same static address, or the same MAC, on one network
other than
qemu, or two files defining one network differently; - a board network file on a board whose network belongs to the host operating
system (
linux,freertos-posix, threadx-linux), or on a board that declares no interfaces (Zephyr boards take their address from Kconfig); - the retired spellings, each refused naming its new home:
[network.<name>]and[image.<id>.net.<if>]insystem.toml, and the flatip/gateway/netmaskkeys on[image.<id>].
The board’s half is [board.net] in its descriptor: stack (who owns the
network: host, os or board), links, network (its default network) and
interfaces, each a kind (eth0 = "ethernet") or a table
(eth0 = { kind = "ethernet", mac_prefix = "52:54:00", dhcp = true }). A board
states no address: [board.net] ip / mac are refused.
The resolved answer for an image is written to
build/nros/net/<image>.toml (under the workspace) and reaches the firmware as
NROS_IMAGE_NET.
How an emulated image joins its network is a fact about how it is RUN, not built, so it has its own table, read only by the QEMU launchers and the test harness:
[image.sensor.run]
nic = { bridge = "qemu-br" } # QEMU: -nic bridge,br=qemu-br,…
nic is "slirp" (QEMU’s own user-mode network, the default on qemu),
{ bridge = "<dev>" } or { tap = "<dev>" } on the host, and applies to the
image’s first interface. An image on any other network with no nic is
physical only, and launching it under QEMU is refused. nros run passes the
-nic value to QEMU when it boots the image.
The same goes for a board that is flashed and watched on a serial port: the
board’s descriptor states its flasher and console ([board.flash],
[board.console], see
the descriptor section),
and an image overrides any key of them in its own tables:
[image.lab.flash]
command = "pyocd flash -t stm32f767zi {hex}" # replaces the board's method
prepare = [{ say = "Set BOOT0 to 1, then press RESET", wait_for = "usb:0483:df11" }]
[image.lab.console]
baud = 921600 # kind stays the board's
Both are checked when system.toml is read (an unknown key, wait_for event
or {placeholder} is refused) and are read by nros flash and nros monitor.
The middleware’s own file
How a session finds its peers — which router to dial, which endpoints to
listen on — is the middleware’s own configuration, written in its own
language. A bringup ships it beside its system.toml, and
it applies to every image of that backend:
| Backend | File | The same document ROS 2 reads as |
|---|---|---|
| zenoh | rmw/zenoh.json5 | ZENOH_SESSION_CONFIG_URI |
| CycloneDDS | rmw/cyclonedds.xml | CYCLONEDDS_URI |
// rmw/zenoh.json5
{
mode: "client",
connect: { endpoints: ["tcp/192.168.50.1:7447"] },
}
zenoh-pico has no json5 parser, so the build reads the file on the host,
lowers each key to a zenoh-pico session property and bakes the result into the
image. A key zenoh-pico cannot take is refused, naming the key — never
dropped. With a file, the file IS the wiring: the image’s connect list
replaces the default router, and an image that also states a locator is
refused (a [system] locator is simply not used). On a hosted build
ZENOH_SESSION_CONFIG_URI replaces the baked file at run time, whole, as it
does for rmw_zenoh.
The flat rmw/zenoh.conf list (key = value) is retired and refused, naming
the json5 that replaced it.
The names above are the default. To ship a different file — one per site, one for a QEMU run and one for the lab — name it:
[system]
rmw_config = "rmw/zenoh-lab.json5" # every image's default session
[image.qemu]
board = "mps2-an385-freertos"
rmw_config = "rmw/zenoh-qemu.json5" # this image's own
A named file that does not exist is refused. XRCE has no file format; its
endpoint is the image’s locator.
A bridge opens one session per [[domain]], and each domain names its own
file, so one image can speak to a CAN zenoh network and an Ethernet DDS one
with two documents:
[[domain]]
name = "zen"
rmw = "zenoh"
config = "rmw/zenoh-zen.json5"
[[domain]]
name = "dds"
rmw = "cyclonedds"
id = 5
config = "rmw/cyclonedds-dds.xml"
[[bridge]]
name = "gw"
from = "zenoh:zen"
to = "cyclonedds:dds"
A domain with a config states no locator (the file is its wiring), and a
domain config that no [[bridge]] opens is refused, since nothing would
read it. An image overrides one domain’s file with a table,
rmw_config = { dds = "rmw/cyclonedds-lab.xml" }: the build then writes that
image its own bridge configuration (nros-bridge.<image>.toml) beside the
bringup’s, and the other images keep the shared one. At run time
ZENOH_SESSION_CONFIG_URI and CYCLONEDDS_URI still replace a hosted
bridge’s files, and NROS_BRIDGE_<SESSION>_LOCATOR replaces that session’s
file whole.
Per-image build knobs
A build knob that one image needs, and that its board does not state, goes in
[image.<id>.knobs.<group>]. The table has the same shape as a board
descriptor’s [knobs.<group>]:
[image.zephyr_xrce.knobs.zephyr]
heap_size = 98304 # NROS_ZEPHYR_HEAP_SIZE
[image.zephyr_xrce.knobs.xrce]
max_subscribers = 1 # NROS_XRCE_MAX_SUBSCRIBERS (-1, the default, derives it)
[image.linux.knobs.log]
max_level = "warn" # NROS_LOG_MAX_LEVEL (a level name or 0..6)
[image.linux.knobs.executor]
max_cbs = 16 # NROS_EXECUTOR_MAX_CBS
Each key is a row of the knob registry
(packages/tooling/nros-zephyr-build/knobs.toml). The key is the row’s board
key where it has one (executor.max_cbs, zenoh.tx.batch); otherwise it is
the env name without NROS_, split at the first _ (NROS_ZEPHYR_HEAP_SIZE
becomes zephyr.heap_size). An unknown key, a value of the wrong type or a
value outside the knob’s range stops nros build with an error
that names the key. The value reaches the build exactly as the knob’s
NROS_* variable would: through the generated cargo config on the cargo
road, the image env file on the cmake road, and the Kconfig defaults on west.
It sits on the image rung (above the board, below an exported variable).
[image.<id>] env = { NAME = "value" } still carries an application’s own
build variable, a name the registry does not hold. An env row that names a
registry knob is an error that names its knobs spelling (release 0.6.0
accepted it with a warning).
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 ([tasks.<name>.<rtos>] +
[[node_overrides]]).
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. The rationale and the measurements are in the execution-model
design record (see Design background).
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, compile-baked with include_str! and parsed by the
board crate’s Config::from_toml. It is the board package’s network file
— the same [<interface>] / [network.<name>] shape an image’s
os_config file has — plus a [node] table that stands in for the session a
system.toml would declare.
# config.toml
[node]
domain_id = 0 # ROS 2 domain ID (0–232)
locator = "tcp/10.0.2.2:7447" # the router the firmware dials
[eth0]
network = "qemu" # built in: QEMU user-mode, 10.0.2.0/24, gateway 10.0.2.2
host = 10 # the address within that network → 10.0.2.10
# address = "10.0.2.10" # or the whole address
mac = "02:00:00:00:00:00"
# A network of your own: name it on the interface instead of `qemu`.
# [network.lab]
# subnet = "192.168.50.0/24"
# gateway = "192.168.50.1"
[node.rt.freertos] # FreeRTOS boards: raw task priorities; omit for defaults
app_priority = 3
zenoh_read_priority = 4
A board reads its own facts from the interface table: a serial link’s
[uart0] baud, ThreadX Linux’s host veth as [eth0] device. One interface is
brought up; a direct-mode board takes no DHCP lease, so state the address.
A file in the retired shape ([[transport]] with ip, gateway and
locator) panics at boot naming the new spelling, rather than booting on
the board’s defaults: ip = "10.0.2.10/24" + gateway = "10.0.2.2" is
[eth0] network = "qemu" + host = 10, and the locator moves to [node].
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 → system.toml
[image.<id>] (above); hand-written main() → config.toml.
Files that used to carry configuration and no longer do are listed in Retired configuration files.
Build-time environment variables
Read during cargo build (by build.rs). Export them in your shell.
SDK paths
You normally set none of these. nros setup puts each vendored source tree
in the shared store, and every build finds it through one ladder: an environment override, then your project’s [sources] in
system.toml, then a checkout submodule you are editing, then the store,
then the checkout’s own copy. Ask where a tree came from with
nros locate <name> --why.
Set one of these variables only to build against a tree of your own:
| Variable | Tree |
|---|---|
FREERTOS_DIR / LWIP_DIR | FreeRTOS kernel / lwIP |
THREADX_DIR / NETX_DIR | ThreadX / NetX Duo |
NUTTX_DIR / NUTTX_APPS_DIR | NuttX RTOS / apps |
CYCLONEDDS_SOURCE_DIR | Cyclone DDS source |
For a tree that belongs to one project, prefer [sources] <name> = "<path>"
in its system.toml (project-relative) over an exported variable, which
every build in that shell inherits.
FREERTOS_PORT, FREERTOS_CONFIG_DIR, THREADX_CONFIG_DIR and
NETX_CONFIG_DIR are not yours to set: they are facts about the board, stated
in its nros-board.toml and carried to the build from there.
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 — carried in the image’s
generated build/<image>/nros-cargo.toml [env], like the other NROS_*
tuning, and set there from the board descriptor’s [knobs] or from your own
environment — shed the parts a brokered client doesn’t need:
| Variable | Default | Effect |
|---|---|---|
NROS_LINK_IP | on | NROS_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_SOCKETS | 4 / 2 | Sized 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_SIZE | per-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):
| Backend | Peak working set | Recommended heap | Notes |
|---|---|---|---|
| zenoh-pico (TCP) | ~16 KB | 24–32 KB (≈2× for fragmentation) | peer middleware; alloc-based session/buffers |
| zenoh-pico (serial) | lighter than TCP | 16–24 KB | no TCP link buffers; verified running at 16 KB |
| XRCE (Micro-XRCE-DDS) | ~3 KB (micro-ROS figure) | ~8 KB | static 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.
| platform | transport | backend | profile | text (flash code) | data | bss | RAM total |
|---|---|---|---|---|---|---|---|
| mps2-an385-baremetal (cortex-m3) | ethernet (smoltcp) | zenoh-pico | release | 177.4 KB | 67.0 KB | 91.7 KB | 158.7 KB |
| mps2-an385-baremetal | ethernet | zenoh-pico | size | 158.3 KB | 67.0 KB | 91.7 KB | 158.7 KB |
| mps2-an385-baremetal | serial (no IP stack) | zenoh-pico | release | 128.6 KB | 25.2 KB | 75.8 KB | 101.0 KB |
| mps2-an385-baremetal | serial | zenoh-pico | size + recipe | 116.1 KB | 25.2 KB | 75.8 KB | 101.0 KB |
| stm32f4 (thumbv7em-eabihf, cortex-m4) ¹ | ethernet | zenoh-pico | release | 186.9 KB | 13.7 KB | 123.0 KB | 136.7 KB |
| stm32f4 ¹ | ethernet | zenoh-pico | size | 138.1 KB | 13.7 KB | 123.0 KB | 136.7 KB |
| mps2-an385-freertos (cortex-m3 + lwIP, RTOS-reused stack) | ethernet (lwIP) | zenoh-pico | release | 240.6 KB | 10.7 KB | 3.3 MB | 3.3 MB |
| mps2-an385-baremetal (serial) | serial (custom XRCE transport) | XRCE | size, heap 24 KB, tight XRCE pools | 60.3 KB | 25.2 KB (heap 24 KB) | 8.8 KB | ~34 KB |
| micro-ROS reference (XRCE) | serial | XRCE-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 — delivered through the
image’s generated [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.textby ~10–26 % with.bss/.dataunchanged (opt-level doesn’t touch static buffers — those are the env knobs above).-Ozis 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
.bssis 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 (mps2-an385-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
The gc-sections and link-script rustflags are not yours to write — they are
the board’s, and they arrive in the generated
build/<image>/nros-cargo.toml [target.<triple>] block:
# build/<image>/nros-cargo.toml — GENERATED, shown for orientation
[target.thumbv7m-none-eabi]
rustflags = [
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=--gc-sections", # 204.8 — strip unreferenced fns/data
]
The knobs that are yours are ordinary environment variables. The generated
[env] never uses force, so a value you set in the calling environment wins:
NROS_LINK_IP=0 \
ZPICO_NO_SMOLTCP=1 \
NROS_HEAP_SIZE=131072 \
NROS_SMOLTCP_MAX_SOCKETS=1 \
NROS_SMOLTCP_MAX_UDP_SOCKETS=1 \
nros build <image>
The profile is not one of them: name it on the image, as
[image.<id>] profile = "size", and nros build passes it to whichever tool
it drives.
NROS_LINK_IP=0 drops zenoh-pico’s TCP/UDP link C and
ZPICO_NO_SMOLTCP=1 skips the smoltcp glue on bare-metal. On the heap floor:
the per-entry executor backing is a single ~75 KB allocation, so a
nros::main! image needs ≥128 KB (the board default) — the older 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. A knob you always want for a board belongs in that
board’s descriptor [knobs], not on every command line.
nros new --platform baremetal already scaffolds the [profile.size] shape.
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
system.toml rmw ([image.<id>], else [system]) 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):
| Variable | Description | Default |
|---|---|---|
ROS_DOMAIN_ID | ROS 2 domain ID | 0 |
NROS_LOCATOR | Router address | tcp/127.0.0.1:7447 |
NROS_SESSION_MODE | client / peer | client |
ZENOH_TLS_ROOT_CA_CERTIFICATE* | TLS CA cert (path / base64) | (none) |
By deployment scenario
| Scenario | Config source | Cargo features | Notes |
|---|---|---|---|
| Desktop (POSIX) | env (ExecutorConfig::from_env()) | rmw-cffi, platform-posix, std + zenoh dep | run 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 | system.toml [image.<id>] ip/mac/gateway/locator | rmw-cffi, platform-bare-metal, ros-humble + zenoh | TAP/slirp bridge |
| FreeRTOS hardware | [image.<id>] + board features (RT) | …, platform-freertos, … | FREERTOS_DIR/LWIP_DIR |
| ESP32 WiFi | [image.<id>] (ssid/password via build env) | …, platform-bare-metal, … | SSID/PASSWORD build env |
| Zephyr module | Kconfig overlay (prj-<rmw>.conf) | (Kconfig → features) | |
Hand-written no_std (no codegen) | standalone config.toml → Config::from_toml | per-board | net config in a file, not in code |
| Minimal RAM (XRCE serial) | config.toml [uart0] baud + [node] locator or [image.<id>] | … + xrce dep | XRCE_* buffer tuning |
Design background
- RFC-0004: configuration and transports
- RFC-0098: generated leaf build config
- RFC-0031: RMW selection and lowering
- RFC-0109: network config layers
- RFC-0015: RTOS orchestration (multi-node RT tiers)
- RFC-0002: RT execution model (timer overrun policy, § 4.4a)
- RFC-0103: one owner per fact (source lookup ladder)