Zephyr (contributor / in-tree workflow)
Looking for the user-facing path? This page covers building nano-ros’s own Zephyr examples from this repository. If you’re consuming nano-ros as a Zephyr module in YOUR Zephyr workspace, see Integration: Zephyr (
westmodule) instead.
Complete setup procedure for Zephyr native_sim testing. Networking uses
NSOS (Native Sim Offloaded Sockets) — each socket call is forwarded to
the host kernel, so tests run on 127.0.0.1 without TAP devices, bridges,
or sudo.
Overview
nros uses an in-tree Zephyr workspace at zephyr-workspace/ (gitignored).
Set $NROS_ZEPHYR_WORKSPACE to install elsewhere.
nros/
├── scripts/zephyr/
│ ├── setup.sh # Initialize workspace
│ ├── migrate-workspace.sh # Move legacy sibling install in-tree
│ ├── downloads/ # SDK tarball cache (gitignored)
│ └── sdk/ # Installed Zephyr SDK (gitignored)
├── zephyr/ # Zephyr module definition
│ ├── Kconfig # RMW backend, API selection, tuning
│ ├── CMakeLists.txt # Transport C sources + nros-c build
│ └── cmake/ # nros_cargo_build(), nros_generate_interfaces()
├── examples/zephyr/
│ ├── rust/ # Rust + zenoh (talker, listener, ...)
│ ├── rust/xrce/ # Rust + XRCE-DDS (talker, listener)
│ ├── c/ # C + zenoh (talker, listener)
│ └── c/xrce/ # C + XRCE-DDS (talker, listener)
├── west.yml # West manifest
└── zephyr-workspace/ # Created by setup.sh (gitignored)
├── nros -> ../ # Symlink back to repo root
├── zephyr/ # Zephyr RTOS v3.7.0
└── modules/ # HALs, zephyr-lang-rust
Migrating from the legacy sibling layout
Legacy setups put the workspace at ../nano-ros-workspace/ with an
in-tree symlink. Both layouts work (contributors: the in-tree
just zephyr recipes auto-detect either), but to consolidate run:
./scripts/zephyr/migrate-workspace.sh --dry-run # preview
./scripts/zephyr/migrate-workspace.sh # execute
Prerequisites
Build the in-tree nros CLI (Phase 218), then let it provision the toolchain:
./scripts/bootstrap.sh # builds packages/cli/target/release/nros
source ./activate.sh # OR: direnv allow / source ./activate.fish
nros setup provides most components prebuilt per platform per RMW — the
cross-compiler, emulator, RMW host daemon, and SDK sources (including the Zephyr
west workspace + Zephyr SDK bits) are fetched from a pinned index into a shared
store at ${NROS_HOME:-~/.nros}/sdk. You do not hand-install a cross-toolchain, and you do not
need ROS 2 installed.
Packages are prebuilt where the index has a binary for your host and built from
source otherwise — zenohd is the notable source build, and zenoh is the default
--rmw. --dry-run prints the plan for your host. See
Installation for the full explanation.
Step 1: Initialize Workspace (One-Time)
nros setup zephyr --rmw zenoh # --rmw defaults to zenoh; xrce | cyclonedds also valid
source ./activate.sh
This provisions:
- The Zephyr west workspace + Zephyr SDK bits
- The emulator
- The RMW host daemon (
zenohdfor zenoh, the Micro-XRCE-DDS agent for xrce) - The in-tree workspace at
zephyr-workspace/(gitignored; auto-detects legacy../nano-ros-workspace/), with nros symlinked in - Rust embedded targets
Contributors: the in-tree
just setup zephyrrecipe still works and now delegates tonros setup zephyrunder the hood.
The RMW host daemon must be running before any example: zenohd for zenoh,
the Micro-XRCE-DDS agent for xrce. nros setup zephyr --rmw <rmw> installs it.
Step 2: Networking
No network setup is required. native_sim uses the NSOS offloaded-sockets
driver, enabled by boards/native_sim_native_64.conf in each example:
CONFIG_ETH_NATIVE_POSIX=n
CONFIG_NET_SOCKETS_OFFLOAD=y
CONFIG_NET_NATIVE_OFFLOADED_SOCKETS=y
With NSOS, Zephyr’s socket API goes straight to host syscalls. Bind to
127.0.0.1 and reach zenohd / the XRCE Agent on the host loopback just
like any other native test. Multiple native_sim processes can coexist
without bridge configuration.
Step 3: Build and Run Zephyr Examples
# Source environment
source zephyr-workspace/env.sh
cd zephyr-workspace
# Rust — `nros sync` first, once per checkout location. It writes the
# generated message bindings and the [patch.crates-io] table the leaf's
# (GITIGNORED, never committed — phase-445 W6) .cargo/config.toml
# includes. Zephyr is the one lane that still needs that file rather than
# build/<image>/nros-cargo.toml: west drives cargo through
# zephyr-lang-rust's rust_cargo_application, which passes no --config
# (issue 1288). This applies to cargo under WEST too: west
# drives the same leaf, so without sync the build fails while PARSING the
# manifest, with an error that never names sync (issue 0694).
#
# `--no-metadata` because the source-metadata probe builds a host binary that
# path-deps this leaf, dragging in the `zephyr` crate — whose build.rs needs a
# DOTCONFIG from a Zephyr cmake configure that has not run yet, and cannot,
# since this sync is its prerequisite (issue 0318). Without the flag the sync
# still succeeds but prints a "no producer" error the reader cannot act on.
# Codegen and the patch table — the reason to run it — are unaffected.
nros sync --no-metadata ../examples/zephyr/rust/talker
# Build Zephyr talker (Rust + zenoh, default backend).
#
# `../examples/...` rather than `<name>/examples/...`: the workspace's link
# back to the checkout is named after YOUR checkout directory
# (`scripts/zephyr/setup.sh` uses `basename "$NROS_REPO_DIR"`), so it reads
# `nano-ros/` for a plain `git clone` and `nros/` only if you renamed the
# directory. For the in-tree workspace this page sets up, `..` IS the
# checkout and needs no name at all.
west build -b native_sim/native/64 ../examples/zephyr/rust/talker
# Run (no sudo needed)
./build/zephyr/zephyr.exe
Configuring an image: system.toml, not prj.conf
An application’s nano-ros configuration lives in ONE place, its
system.toml (RFC-0098 D10, phase-481). The nano-ros Zephyr module renders
the selected image into a Kconfig fragment and merges it last, after every
conf file. So the prj*.conf files hold Zephyr’s own settings only (networking,
POSIX, stacks, heaps) and never a CONFIG_NROS_* line: one there is either
dead (the image overrides it) or a second source that can disagree with the
first. A gate (check-leaf-conf-nros-knobs) refuses one in the tree.
What the module renders, and from where:
You write in system.toml | The build gets |
|---|---|
[image.<id>] rmw = "xrce" | CONFIG_NROS_RMW_XRCE=y |
| (nothing — the package’s language) | CONFIG_NROS_{C,CPP,RUST}_API=y |
locator = "tcp/…" (zenoh) | CONFIG_NROS_ZENOH_LOCATOR |
locator = "udp/<host>:<port>" (XRCE) | CONFIG_NROS_XRCE_AGENT_ADDR + _PORT |
domain_id = 7 (image, [defaults] or [system]) | CONFIG_NROS_DOMAIN_ID |
[image.<id>.knobs.zenoh] max_queryables = 12 | CONFIG_NROS_MAX_QUERYABLES=12 — a registry knob, spelled as a board’s [knobs] is (see Configuration) |
[image.<id>.knobs.xrce] max_subscribers = 0 | CONFIG_NROS_XRCE_MAX_SUBSCRIBERS=0 — likewise |
env = { NROS_ZEPHYR_TASK_STACK_SIZE = "16384" } | CONFIG_NROS_ZEPHYR_TASK_STACK_SIZE=16384 — any other option in the reference below, spelled without CONFIG_ |
conf = ["prj-xrce.conf"] | that Zephyr-native fragment, added ahead of your own EXTRA_CONF_FILE |
An env row with no Kconfig symbol is not lost: it reaches the image’s cargo
builds as an [env] row instead (issue 1712). A row whose type does not fit
its symbol, or that only exists under another RMW, is refused at configure
time, naming the system.toml line.
One image per RMW
An application built for several backends states one image per RMW, and
picks one at build time. From examples/zephyr/c/talker/system.toml:
[system]
name = "nros_zephyr_talker_c"
rmw = "zenoh"
domain_id = 0
default_images = ["zephyr_zenoh"]
[image.zephyr_zenoh]
board = "zephyr"
rmw = "zenoh"
conf = ["prj-zenoh.conf"]
[image.zephyr_xrce]
board = "zephyr"
rmw = "xrce"
conf = ["prj-xrce.conf"]
locator = "udp/127.0.0.1:2018"
[image.zephyr_xrce.knobs.xrce]
max_subscribers = 0
max_service_servers = 0
max_service_clients = 0
[image.zephyr_cyclonedds]
board = "zephyr"
rmw = "cyclonedds"
conf = ["prj-cyclonedds.conf"]
west build -b native_sim/native/64 ../examples/zephyr/c/talker # default_images: zenoh
west build -b native_sim/native/64 ../examples/zephyr/c/talker -- -DNROS_IMAGE=zephyr_xrce
-DNROS_IMAGE is a CMake cache variable like any -D: re-running west build
on the same build directory WITHOUT it keeps the image it last built, exactly
as -b keeps the board. Pass it every time, or use -p when switching. A name
the system.toml does not declare is refused, listing the ones it does.
Precedence, last wins: the conf files, then a 4.x -S nros-<rmw> snippet,
then the image’s rendering, then any -DCONFIG_… on the command line (how a
test harness passes a per-run port). A snippet naming a DIFFERENT RMW than the
image is refused rather than silently overridden; one that agrees is
redundant. An application with no system.toml gets no rendering, so for it
the snippets and Kconfig defaults still choose.
What each RMW’s Zephyr-native fragment needs
The image’s conf file carries what the backend needs from Zephyr itself:
- Zenoh (the Kconfig default): TCP and POSIX threads for zenoh-pico —
CONFIG_NET_TCP=y,CONFIG_POSIX_API=y, and pthread pools big enough for it (examples/zephyr/rust/talker/prj-zenoh.confusesCONFIG_MAX_PTHREAD_MUTEX_COUNT=64,CONFIG_MAX_PTHREAD_COND_COUNT=16). - XRCE-DDS:
CONFIG_NET_TCP=y, on top of the BSD sockets (CONFIG_NET_SOCKETS=y) every image’s sharedprj.confenables. The agent endpoint is the image’slocator. - Cyclone DDS: brokerless RTPS, wire-compatible with stock ROS 2
(
rmw_cyclonedds_cpp). Its source is C++, soCONFIG_CPP=yeven for Rust callers; it needs a large stack (CONFIG_MAIN_STACK_SIZE=524288), pthread pools, andCONFIG_NET_IPV4_IGMP=yfor SPDP multicast. The bool prerequisites (thread-local storage, dynamic threads,NET_TCP, …) areselected byCONFIG_NROS_RMW_CYCLONEDDS. Itsddsrt_mallocdraws on the nano-ros platform heap, 1 MiB by default under Cyclone — raise it with[image.<id>.knobs.zephyr] heap_size = …on the image, not in the conf.
On native_sim, add the NSOS host-socket offload
(CONFIG_NET_SOCKETS_OFFLOAD=y + CONFIG_NET_NATIVE_OFFLOADED_SOCKETS=y) so
discovery uses host BSD sockets instead of zeth/TAP. See
examples/zephyr/rust/talker/prj-cyclonedds.conf for the full fragment.
API Selection
The language API comes from the package itself — the module renders
CONFIG_NROS_RUST_API, CONFIG_NROS_C_API or CONFIG_NROS_CPP_API from the
package’s entry language, so you never set it. What differs is the
CMakeLists.txt.
Rust
prj.conf needs Zephyr’s own Rust support (CONFIG_RUST=y,
CONFIG_RUST_ALLOC=y), and CMakeLists.txt uses rust_cargo_application():
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(my_example)
rust_cargo_application()
C
CMakeLists.txt uses nros_generate_interfaces():
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(my_example)
nros_generate_interfaces(std_msgs "msg/Int32.msg")
target_sources(app PRIVATE src/main.c)
Kconfig Reference
All options are under menuconfig NROS in zephyr/Kconfig. They are what the
module RENDERS from system.toml, never from a conf file: a registry knob as
[image.<id>.knobs.<group>] <name> ([image.zephyr.knobs.zenoh] max_publishers = 4), any other option as an image env row with the
CONFIG_ dropped (env = { NROS_BOOT_REPORT = "1" }). An env row naming a
registry knob is refused, naming its knobs spelling. The RMW, API, endpoint and domain rows have their own system.toml
keys (table above).
Common Options
| Option | Type | Default | Description |
|---|---|---|---|
CONFIG_NROS | bool | n | Enable nros module |
CONFIG_NROS_RUST_API | bool | y | Use Rust API |
CONFIG_NROS_C_API | bool | n | Use C API |
CONFIG_NROS_DOMAIN_ID | int | 0 | ROS 2 domain ID |
CONFIG_NROS_INIT_DELAY_MS | int | 2000 | Network init wait (ms) |
Zenoh Options (visible when CONFIG_NROS_RMW_ZENOH=y)
| Option | Type | Default | Description |
|---|---|---|---|
CONFIG_NROS_ZENOH_LOCATOR | string | "tcp/127.0.0.1:7447" | Router address |
CONFIG_NROS_ZENOH_MULTI_THREAD | bool | y | Zenoh-pico multithreading |
CONFIG_NROS_ZENOH_PUBLICATION | bool | y | Publication support |
CONFIG_NROS_ZENOH_SUBSCRIPTION | bool | y | Subscription support |
CONFIG_NROS_ZENOH_QUERY | bool | y | Service client support |
CONFIG_NROS_ZENOH_QUERYABLE | bool | y | Service server support |
CONFIG_NROS_ZENOH_LINK_TCP | bool | y | TCP transport link |
CONFIG_NROS_MAX_PUBLISHERS | int | 8 | Max concurrent publishers |
CONFIG_NROS_MAX_SUBSCRIBERS | int | 8 | Max concurrent subscribers |
CONFIG_NROS_MAX_QUERYABLES | int | -1 (derive) | Max concurrent queryables (service servers). the param_services capability costs 6 per node, [lifecycle] 5 once — a value below that floor fails the build, naming this knob (phase-426 W3) |
CONFIG_NROS_FRAG_MAX_SIZE | int | 2048 | Max reassembled message size |
CONFIG_NROS_BATCH_UNICAST_SIZE | int | 1024 | Max unicast batch size |
CONFIG_NROS_SUBSCRIBER_BUFFER_SIZE | int | -1 (derive) | Per-subscriber buffer, small payload class |
CONFIG_NROS_MAX_LARGE_SUBSCRIBERS | int | -1 (derive) | Blocks in the large payload class |
CONFIG_NROS_SUBSCRIBER_LARGE_SIZE | int | -1 (derive) | Per-sample capacity of the large class |
CONFIG_NROS_SERVICE_BUFFER_SIZE | int | 1024 | Per-service buffer |
-1 means “derive it from the message bounds”
Three of the rows above, plus CONFIG_NROS_SUBSCRIPTION_BUFFER_SIZE, default to
-1. That is not a size: it is how an image says nothing here chose a number,
work it out (phase-403 W8, issue 0940).
Codegen derives an exact serialized-size bound for every message type it
generates and writes it beside the code as nros_message_bounds.cmake. The
build composes those, and the configure prints what it concluded:
-- nros: message-bound sizing DERIVED from 84 types in 11 interface packages (all bounded)
-- nros: largest type std_msgs/msg/Float64MultiArray at 1496 B -> NROS_SUBSCRIPTION_BUFFER_SIZE
-- nros: 0 types over the 2048 B ceiling -> NROS_MAX_LARGE_SUBSCRIBERS
Three things to know before you rely on it:
- Anything you state wins. A value in your image’s
env, a board.conf, or an environment override outranks the derivation, silently and in both directions. The derived number is a default, never an override. - It is an UPPER BOUND. The inventory covers every type in the interface
closure you LINK, not the ones you subscribe to, so the number is the largest
message the image could receive. That is the safe direction, and it can
still be far above what you need: the line above names
std_msgs/Float64MultiArray, which that image never receives. When the type it names is not one of yours, either state your own value or stop linking the package. - One unbounded type refuses the lot. If any type in the closure has no
derived bound, nothing is derived, every knob keeps its configured value, and
the configure names the types and the members that cost them their bound.
Deriving over the rest would publish a maximum a real sample can exceed, and
that drop is silent on the C++ arena path. The remedy is a bound in the
.msgor aninlinecap innros-codegen.toml; one cap on a declaring type ("std_msgs/Header.frame_id") reaches every message that nests it.
The answer, and why it came out that way, is written to
<build>/nros/message_bound_knobs.cmake.
XRCE Options (visible when CONFIG_NROS_RMW_XRCE=y)
| Option | Type | Default | Description |
|---|---|---|---|
CONFIG_NROS_XRCE_AGENT_ADDR | string | "127.0.0.1" | Agent IP address |
CONFIG_NROS_XRCE_AGENT_PORT | int | 2018 | Agent UDP port |
CONFIG_NROS_XRCE_TRANSPORT_MTU | int | 512 | Transport MTU |
CONFIG_NROS_XRCE_MAX_SUBSCRIBERS | int | -1 (derive) | Max concurrent subscribers |
CONFIG_NROS_XRCE_MAX_SERVICE_SERVERS | int | -1 (derive) | Max service servers |
CONFIG_NROS_XRCE_MAX_SERVICE_CLIENTS | int | 4 | Max service clients |
CONFIG_NROS_XRCE_SUBSCRIBER_RING_DEPTH | int | 32 | Queued samples per subscriber |
CONFIG_NROS_XRCE_BUFFER_SIZE | int | 1024 | Per-slot buffer size; the default of the three below |
CONFIG_NROS_XRCE_SUBSCRIBER_BUFFER_SIZE | int | -1 (derive) | Subscriber ring entry size |
CONFIG_NROS_XRCE_SERVICE_REQUEST_BUFFER_SIZE | int | -1 (= BUFFER_SIZE) | Service-server request entry size |
CONFIG_NROS_XRCE_SERVICE_REPLY_BUFFER_SIZE | int | -1 (= BUFFER_SIZE) | Service-client reply slot size |
CONFIG_NROS_XRCE_STREAM_HISTORY | int | 16 | Reliable stream depth (2-16) |
Every option in this table except the agent address and port is a
knob-registry row: a project with a system.toml states it as
[image.<id>.knobs.xrce] <name> — the option without CONFIG_NROS_XRCE_,
lowercased (transport_mtu = 512, subscriber_ring_depth = 1) — not in
prj.conf, where the symbol has no prompt.
C API Options (visible when CONFIG_NROS_C_API=y)
| Option | Type | Default | Description |
|---|---|---|---|
CONFIG_NROS_C_MAX_HANDLES | int | 16 | Max executor handles |
CONFIG_NROS_C_MAX_SUBSCRIPTIONS | int | 8 | Max subscriptions |
CONFIG_NROS_C_MAX_TIMERS | int | 8 | Max timers |
CONFIG_NROS_C_MAX_SERVICES | int | 4 | Max services |
E2E Testing
Contributors: the in-tree fixture/test lanes for this platform are in Per-Platform Contributor Lanes.
Troubleshooting
| Issue | Solution |
|---|---|
west: command not found | Run pip3 install --user west and add ~/.local/bin to PATH |
Connection refused | Start zenohd / MicroXRCEAgent on the host loopback (e.g. tcp/127.0.0.1:7447) |
Build fails | Source environment: source zephyr-workspace/env.sh |
XRCE Agent not found | Provision the xrce daemon: nros setup zephyr --rmw xrce |
| Zenoh mutex exhaustion | Increase CONFIG_MAX_PTHREAD_MUTEX_COUNT (default 5 is too low) |
Network Architecture
With NSOS, Zephyr sockets are forwarded to host syscalls — there is no emulated L2/L3 stack to configure, no static IP, and no bridge.
┌─────────────────────────────────────────────────────────────┐
│ Host (Linux) │
│ │
│ ┌────────────────────┐ ┌────────────────────────┐ │
│ │ zephyr.exe talker │ │ zephyr.exe listener │ │
│ │ (native_sim+NSOS) │ │ (native_sim+NSOS) │ │
│ └─────────┬──────────┘ └──────────┬─────────────┘ │
│ │ host socket() via NSOS │ │
│ ▼ ▼ │
│ 127.0.0.1 (loopback) │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────────────┐ ┌────────────────────────┐ │
│ │ zenohd │ │ MicroXRCEAgent │ │
│ │ tcp/127.0.0.1:7447 │ │ udp/127.0.0.1:2018 │ │
│ └────────────────────┘ └────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Updating the Workspace
To update Zephyr and modules to latest versions specified in west.yml:
cd zephyr-workspace
west update
To completely recreate the workspace:
Contributors: the in-tree workspace-recreate lane is in Per-Platform Contributor Lanes.