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

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 (west module) 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 (zenohd for 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 zephyr recipe still works and now delegates to nros setup zephyr under 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 the module’s nros-nsos snippet (zephyr/snippets/nros-nsos/nsos.conf), which board zephyr names, so every example on it gets it with no file of its own; anything else passes -S nros-nsos:

CONFIG_ETH_DRIVER=n
CONFIG_NET_DRIVERS=y
CONFIG_NET_SOCKETS_OFFLOAD=y
CONFIG_NET_NATIVE_OFFLOADED_SOCKETS=y

ETH_DRIVER=n rather than naming the TAP driver, because that symbol is ETH_NATIVE_POSIX on 3.7 and ETH_NATIVE_TAP on 4.4 (issue 2157).

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 configure` 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).
#
# The sync also runs the source-metadata probe over the leaf's `node/`
# package, a host build of the component. That writes `node/metadata/*.json`,
# which the configure composes into the image's pool sizes (issue 1662).
# Without it the pools fall back to crate defaults, and the XRCE talker's
# session no longer fits its heap (issue 2133). Do not pass `--no-metadata`.
nros configure ../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

How an image’s system.toml becomes the Zephyr build’s Kconfig — and why a prj*.conf never carries a CONFIG_NROS_* line — is user material: Zephyr (west module) § Configuring an image. For the in-tree examples, each leaf’s system.toml declares one image per RMW; pick one with -DNROS_IMAGE=<id>:

west build -b native_sim/native/64 ../examples/zephyr/c/talker -- -DNROS_IMAGE=zephyr_xrce

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

OptionTypeDefaultDescription
CONFIG_NROSboolnEnable nros module
CONFIG_NROS_RUST_APIboolyUse Rust API
CONFIG_NROS_C_APIboolnUse C API
CONFIG_NROS_DOMAIN_IDint0ROS 2 domain ID
CONFIG_NROS_INIT_DELAY_MSint2000Network init wait (ms)

Zenoh Options (visible when CONFIG_NROS_RMW_ZENOH=y)

OptionTypeDefaultDescription
CONFIG_NROS_ZENOH_LOCATORstring"tcp/127.0.0.1:7447"Router address
CONFIG_NROS_ZENOH_MULTI_THREADboolyZenoh-pico multithreading
CONFIG_NROS_ZENOH_PUBLICATIONboolyPublication support
CONFIG_NROS_ZENOH_SUBSCRIPTIONboolySubscription support
CONFIG_NROS_ZENOH_QUERYboolyService client support
CONFIG_NROS_ZENOH_QUERYABLEboolyService server support
CONFIG_NROS_ZENOH_LINK_TCPboolyTCP transport link
CONFIG_NROS_MAX_PUBLISHERSint8Max concurrent publishers
CONFIG_NROS_MAX_SUBSCRIBERSint8Max concurrent subscribers
CONFIG_NROS_MAX_QUERYABLESint-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_SIZEint2048Max reassembled message size
CONFIG_NROS_BATCH_UNICAST_SIZEint1024Max unicast batch size
CONFIG_NROS_SUBSCRIBER_BUFFER_SIZEint-1 (derive)Per-subscriber buffer, small payload class
CONFIG_NROS_MAX_LARGE_SUBSCRIBERSint-1 (derive)Blocks in the large payload class
CONFIG_NROS_SUBSCRIBER_LARGE_SIZEint-1 (derive)Per-sample capacity of the large class
CONFIG_NROS_SERVICE_BUFFER_SIZEint1024Per-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 .msg or an inline cap in nros-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)

OptionTypeDefaultDescription
CONFIG_NROS_XRCE_AGENT_ADDRstring"127.0.0.1"Agent IP address
CONFIG_NROS_XRCE_AGENT_PORTint2018Agent UDP port
CONFIG_NROS_XRCE_TRANSPORT_MTUint512Transport MTU
CONFIG_NROS_XRCE_MAX_SUBSCRIBERSint-1 (derive)Max concurrent subscribers
CONFIG_NROS_XRCE_MAX_SERVICE_SERVERSint-1 (derive)Max service servers
CONFIG_NROS_XRCE_MAX_SERVICE_CLIENTSint-1 (derive)Max service clients
CONFIG_NROS_XRCE_SUBSCRIBER_RING_DEPTHint32Queued samples per subscriber
CONFIG_NROS_XRCE_BUFFER_SIZEint1024Per-slot buffer size; the default of the three below
CONFIG_NROS_XRCE_SUBSCRIBER_BUFFER_SIZEint-1 (derive)Subscriber ring entry size
CONFIG_NROS_XRCE_SERVICE_REQUEST_BUFFER_SIZEint-1 (= BUFFER_SIZE)Service-server request entry size
CONFIG_NROS_XRCE_SERVICE_REPLY_BUFFER_SIZEint-1 (= BUFFER_SIZE)Service-client reply slot size
CONFIG_NROS_XRCE_STREAM_HISTORYint16Reliable 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)

OptionTypeDefaultDescription
CONFIG_NROS_C_MAX_HANDLESint16Max executor handles
CONFIG_NROS_C_MAX_SUBSCRIPTIONSint8Max subscriptions
CONFIG_NROS_C_MAX_TIMERSint8Max timers
CONFIG_NROS_C_MAX_SERVICESint4Max services

CI lanes

The copy-out, snippet, patch-apply, and dual-line (Zephyr 3.7 LTS and 4.x) build flows the user book’s Zephyr chapter describes are exercised in CI by just zephyr ci-both and just zephyr check-copy-out.

ARM FVP resolution

scripts/zephyr/resolve-fvp-bin.sh resolves the FVP_BaseR_AEMv8R model, in order: ARMFVP_BIN_PATH, ARM_FVP_DIR, FVP_BaseR_AEMv8R on PATH, then the store copy (nros dev sdk path arm-fvp). The just zephyr run-fvp-* / verify-fvp-runtime recipes export the result as ARMFVP_BIN_PATH at BUILD time as well as run time, because Zephyr’s armfvp.cmake caches the location at configure. When nothing resolves, a recipe you NAMED fails with the remedy (nros setup --tool arm-fvp); reached through a fixture fan-out it skips and is reported.

For a model downloaded by hand, scripts/installers/arm-fvp-installer.sh (with ARM_FVP_DIR set to the extraction root) symlinks it to ~/.nros/sdks/arm-fvp/current/ and prints the export ARMFVP_BIN_PATH=… line. The user-facing variables are in the user book’s Environment Variables.

E2E Testing

Contributors: the in-tree fixture/test lanes for this platform are in Per-Platform Contributor Lanes.

Troubleshooting

IssueSolution
west: command not foundRun pip3 install --user west and add ~/.local/bin to PATH
Connection refusedStart zenohd / MicroXRCEAgent on the host loopback (e.g. tcp/127.0.0.1:7447)
Build failsSource environment: source zephyr-workspace/env.sh
XRCE Agent not foundProvision the xrce daemon: nros setup zephyr --rmw xrce
Zenoh mutex exhaustionIncrease 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.