Zephyr (west module)
Single-node starter on Zephyr via the in-tree zephyr/
west module. nano-ros ships as a Zephyr module — west discovers it
from your workspace’s west.yml, drops in a prj.conf Kconfig
surface, and the standard west build / west flash flow takes
care of the rest.
Contributor path? Building nano-ros’s own Zephyr examples straight from this repository (no west-managed workspace) is covered at Zephyr (contributor). The page below is the canonical user entry.
Just want a working starter? Clone the
nano-ros-zephyr-examplerepo (west init -m …) — a manifest + zenoh talker app pinned to a tested Zephyr, with the same quickstart as below baked in. The steps here explain what it does so you can adapt it to your own workspace.
Prereqs. Run
nros setup zephyr --rmw <rmw>once (see Prerequisites below) — it provisions the Zephyr west workspace + Zephyr SDK bits, the emulator, and your RMW’s host daemon into the shared store. No hand-installed Zephyr SDK,west, or cross-toolchain, and no ROS 2 install required. Python: 3.10+ on Zephyr 3.7 LTS, but ≥ 3.12 on Zephyr 4.x (4.x’sfind_package(Python3)requires 3.12 — see the version matrix below). nano-ros’s imported west fragmentzephyr/west.ymlis a manifest-only file — it does NOT pull Zephyr itself; that has to be in your parent manifest (zephyrproject-rtos/zephyr).
Which Zephyr? — 3.7 LTS and 4.x both supported
nano-ros consumes as a module on both the 3.7 LTS line (supported to Jan 2027; the safety-island default) and the current 4.x rolling line. You build against whatever Zephyr your workspace already pins — nano-ros adapts. The two lines differ only in how you select an RMW and apply nano-ros’s Zephyr patches:
| Capability | Zephyr 3.7 LTS | Zephyr 4.x |
|---|---|---|
| Min Python | 3.10 | 3.12 (find_package(Python3)) |
| RMW selection | the image’s rmw in system.toml | the image’s rmw in system.toml (an app with no system.toml: the -S nros-<rmw> snippet) |
| nano-ros patches | applied during nros setup zephyr provisioning | applied during nros setup zephyr provisioning |
| Examples as samples / Twister | — | samples: + Twister (sample.nano-ros.*) |
| zenoh (native_sim) | ✅ build + e2e | ✅ build + e2e |
| cyclonedds (native_sim) | ✅ build + e2e | ✅ build · publish · receive · multicast-join (stable 2-node run pending a tracked k_mutex fix) |
| xrce | ✅ | build path WIP |
native_sim networking uses NSOS (host loopback) on both lines — no
TAP/bridge/root. The copy-out, snippet, patch-apply, and dual-line build
flows are exercised in CI (contributor lanes just zephyr ci-both,
just zephyr check-copy-out).
Project layout
A Zephyr workspace using nano-ros looks like any other Zephyr project — the nano-ros module sits beside Zephyr, your application sits beside both:
my_zephyr_ws/
├── .west/
├── zephyr/ # cloned by `west init`
├── modules/
│ └── nano-ros/ # imported via west.yml
└── apps/
└── my_app/ # your application
├── CMakeLists.txt
├── system.toml # nano-ros config: RMW, endpoint, knobs
├── prj.conf # Zephyr's own Kconfig (+ CONFIG_NROS=y)
├── west.yml # (optional) per-app manifest
└── src/
└── main.c # nros user code
The application CMakeLists.txt is a stock Zephyr app — find_package(Zephyr)
target_sources. Noadd_subdirectory(<nano-ros>)is needed; the module shell handles it onceCONFIG_NROS=yflips on.
A minimal apps/my_app/ looks like this — the shipped
examples/zephyr/c/talker/
quoted directly (only the project/class names are trimmed to my_app), so
this page can’t drift from the real API surface. It is a stateful C
component (a struct + a configure function, RFC-0043 / phase-244.C2) —
not a hand-written main() — because the Zephyr typed carrier generates
the entry point and calls into the component by identity:
# apps/my_app/CMakeLists.txt
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(my_app)
set(NROS_PLATFORM zephyr)
include("${NROS_REPO_DIR}/cmake/NanoRosNodeRegister.cmake")
include("${NROS_REPO_DIR}/cmake/NanoRosVerbs.cmake")
nano_ros_auto_add_library(talker_lib STATIC src/Talker.c)
nros_components_register_node(talker_lib
PLUGIN my_app::Talker
EXECUTABLE talker
SHAPE configure
TYPED
DEPLOY zephyr)
// apps/my_app/src/Talker.c
// `talker_configure` creates a publisher on `/chatter` + a timer that
// publishes an Int32 counter each tick, with the C API. `RCLC_C_COMPONENT`
// emits the C-ABI factory/configure the Zephyr typed Entry carrier calls.
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <rclc/rclc.h>
#include <std_msgs/msg/int32.h>
typedef struct {
rcl_publisher_t pub;
rcl_timer_t timer;
int32_t count;
} talker_t;
static void on_tick(rcl_timer_t* timer, void* ctx) {
(void)timer;
talker_t* self = (talker_t*)ctx;
std_msgs__msg__Int32 msg = {.data = self->count};
if (rcl_publish(&self->pub, &msg, NULL) == RCL_RET_OK) {
printf("Published: %d\n", (int)self->count);
}
self->count++;
}
static rcl_ret_t talker_configure(rcl_node_t* node, rclc_executor_t* executor, talker_t* self) {
self->count = 0;
self->pub = rcl_get_zero_initialized_publisher();
rcl_ret_t rc = rclc_publisher_init_default(
&self->pub, node, ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Int32), "/chatter");
if (rc != RCL_RET_OK) {
return rc;
}
self->timer = rcl_get_zero_initialized_timer();
rc = rclc_timer_init_with_context(&self->timer, 500000000ULL, on_tick, self);
if (rc != RCL_RET_OK) {
return rc;
}
return rclc_executor_add_timer(executor, &self->timer);
}
RCLC_C_COMPONENT(talker_t, talker_configure)
See examples/templates/zephyr-byo/app/src/Talker.c
for the up-to-date source (this is that BYO starter’s talker, only stripped
of its file header comment; the canonical in-tree
examples/zephyr/c/talker/
publishes std_msgs/String the same way). The component uses the C API only:
configure is handed an rcl_node_t * and an rclc_executor_t * over the
carrier’s own node and executor, so it shares both with a C++ component.
Its configuration is in Configure below. You do not choose the
language API: the module renders it from the app, and the typed Zephyr carrier
that drives this component’s configure is C++, so the nros-cpp header
surface is on the app include path even though Talker.c itself is C.
Prerequisites
nros setup provisions the parts nano-ros owns — the RMW host daemon
(zenohd / Micro-XRCE-DDS agent) and the RMW transport submodules
(zenoh-pico + mbedtls for zenoh, the cyclonedds fork) — from a pinned index into
${NROS_HOME:-~/.nros}/sdk / the nano-ros checkout. It does not replace Zephyr’s own SDK,
and interface codegen still needs the ROS message definitions.
- Build the in-tree
nrosCLI (Phase 218, from the nano-ros checkout):./scripts/bootstrap.sh # builds packages/cli/target/release/nros source ./activate.sh # OR: direnv allow / source ./activate.fish - Provision the RMW (daemon + transports) from the nano-ros checkout:
( cd modules/nano-ros && nros setup zephyr --rmw zenoh ) # zenohd + zenoh-pico + mbedtls ( cd modules/nano-ros && nros setup --source px4-rs ) # workspace cargo-load dep - Install the Zephyr SDK the standard Zephyr way (
nros setupdoes not provide it) and expose it —export ZEPHYR_SDK_INSTALL_DIR=/path/to/zephyr-sdk-<ver>(or register it via the SDK’ssetup.sh -c). - Message definitions for codegen. The interface codegen resolves a
package’s
msg/*.msgfromNROS_<PKG>_DIR(e.g.export NROS_STD_MSGS_DIR=/opt/ros/humble/share/std_msgs) — point it at a ROS install or any dir holding the.msgfiles.
The RMW host daemon must be running before an example connects
(for zenoh: ZENOH_CONFIG_OVERRIDE='listen/endpoints=["tcp/127.0.0.1:7447"];scouting/multicast/enabled=false' ros2 run rmw_zenoh_cpp rmw_zenohd; the Micro-XRCE-DDS agent for xrce).
Configure
Add nano-ros (and a Zephyr pin — zephyr/west.yml is manifest-only;
it does not pull Zephyr itself) to your workspace west.yml:
manifest:
remotes:
- name: nano-ros
url-base: https://github.com/NEWSLabNTU
- name: zephyr
url-base: https://github.com/zephyrproject-rtos
projects:
- name: zephyr
remote: zephyr
revision: v3.7.0 # or your chosen 3.7 LTS / 4.x SHA
path: zephyr
import: true # pulls Zephyr's own modules
- name: nano-ros
remote: nano-ros
revision: nros-v0.6.0 # pin a release tag (west needs SOME
# revision — its default `master` does
# not exist here). `main` works for
# tracking tip, but a tag keeps your
# build reproducible.
path: modules/nano-ros
import:
file: zephyr/west.yml # pulls nano-ros's transport deps
Then, per application, nano-ros’s half of the configuration goes in a
system.toml beside the CMakeLists.txt (RFC-0098 D10, phase-481), and
Zephyr’s own half in prj.conf:
# system.toml
[system]
name = "my_app"
rmw = "zenoh"
domain_id = 0
[image.zephyr]
board = "zephyr" # the concrete board is west's `-b`
rmw = "zenoh" # zenoh | xrce | cyclonedds
# locator = "tcp/192.0.2.10:7447"
# [image.zephyr.knobs.zenoh]
# max_publishers = 4
# prj.conf — Zephyr settings only; never a CONFIG_NROS_* knob here
CONFIG_NROS=y # enables the module; everything else is the image's
CONFIG_NETWORKING=y
CONFIG_NET_IPV4=y
CONFIG_NET_TCP=y
The module renders the image into a Kconfig fragment merged after prj.conf:
the RMW from rmw, the language API from your app, the endpoint from
locator, the domain from domain_id, and every env knob (any symbol in the
Kconfig reference, spelled without
CONFIG_). A CONFIG_NROS_* line in prj.conf would be overridden by it, so
it is at best dead. Several images in one file — one per RMW — are picked with
west build … -- -DNROS_IMAGE=<id>; see
Configuring an image.
An app with NO system.toml gets no rendering and is configured by Kconfig
defaults and the RMW snippets alone.
CONFIG_NROS=y activates the shell, which maps Kconfig values to
NROS_* CMake cache vars and add_subdirectory()s the root
nano-ros CMake. NanoRos::NanoRos is linked into your app
library transparently.
Build
If your workspace is a fresh manifest-only dir (no .west/), initialise
it first so west knows which west.yml is the manifest:
cd my_zephyr_ws
west init -l . # one-time; points west at the
# local west.yml in cwd
west update # clones nano-ros + Zephyr into the workspace
(If you started from west init -m <remote>, both calls above are
already done — go straight to west build below.)
nros build on Zephyr: it hands off, it does not take over
The other platforms let nros build own the build: it generates a cargo or
CMake root from your [image.*] declarations and runs the tool. Zephyr is not
like that, on purpose.
You own the west workspace and the application. west init / west update
populate the workspace, your app carries its own CMakeLists.txt and
prj.conf, and west only runs from inside that workspace. None of that is
ours to generate — an app’s Kconfig is authored, not derived (RFC-0065 D5).
So for a Zephyr image nros build:
- resolves the application — the entry package for that image’s board, not the bringup;
- resolves your Zephyr —
$ZEPHYR_BASE, else$NROS_ZEPHYR_WORKSPACE, else an in-treezephyr-workspace/, else a sibling../nano-ros-workspace. Note it is a Zephyr that is needed, not a.west/directory: withZEPHYR_BASEset,west buildruns from anywhere, which is what makes a freestanding application — one outside the west workspace — work; - runs
west buildfrom there with the image’s overlays (EXTRA_CONF_FILE,APPLICATION_CONFIG_DIR) already applied.
If it cannot find a workspace it does not guess. It prints the command and where to run it:
$ nros build demo_bringup:zephyr
Error: no west workspace found, so `west build` cannot be run for `zephyr`.
Zephyr differs from the other drivers: YOU own the west workspace and the
application, and `west` only runs inside a workspace …
Run this from your west workspace:
west build -b native_sim/native/64 …/src/zephyr_entry
Point nros at your workspace:
NROS_ZEPHYR_WORKSPACE=<dir> # the dir containing zephyr/
That is deliberate and it is the same boundary nros setup --system draws when
it composes an install command and prints it rather than running sudo for you:
where the tool is yours, nano-ros hands you the command instead of assuming it
may act.
The plain west build below keeps working and is still the primary flow.
nros build is the convenience that applies an image’s overlays for you; it
never becomes a required layer between you and west.
Starting from nothing: nros new node and nros new entry
A node package is board- and RMW-agnostic — it names no platform, which is what lets the same package be deployed to Linux and to a Cortex-M without an edit:
nros new node talker_pkg
It writes the package and adds the [[component]] row to your bringup. (Use
nros new <name> --platform linux only for a standalone runnable project —
it pins a board crate, so an entry on another platform cannot link it.)
The entry: nros new entry
You do not have to write the entry package by hand:
nros new entry zephyr_entry --platform zephyr
nros new entry: scaffolded ./src/zephyr_entry (7 file(s))
declared [image.zephyr_entry] in ./src/demo_bringup/system.toml
Next:
nros sync
nros build demo_bringup:zephyr_entry --zephyr-workspace <dir>
It writes the package — Cargo.toml, CMakeLists.txt, build.rs,
src/lib.rs, prj.conf, prj-zenoh.conf, boards/<board>.conf — and three
things outside it that are easy to miss and invisible until they fail:
- the
[image.*]block, withboard,entryand theconfoverlay the entry’s ownCMakeLists.txtrequires; - an
excludeentry in every enclosing cargo workspace, because a west-builtstaticlibmust not be an ordinary cargo member; - a path dependency per node package the bringup declares, since the entry links them.
--board, --rmw and --bringup override the defaults
(native_sim/native/64, zenoh, and the workspace’s single bringup).
The whole flow, end to end
Five commands, run from your workspace. This is the examples/workspaces/rust
transcript, verbatim:
# 0. tell nros where your west workspace is — the directory containing
# `zephyr/`. On the command line:
#
# nros build demo_bringup:zephyr --zephyr-workspace /path/to/zephyr-workspace
#
# or once for the shell, if you would rather not repeat it:
export NROS_ZEPHYR_WORKSPACE=/path/to/zephyr-workspace
# 1. generate the message bindings this workspace's packages depend on.
# `nros build` refuses with "this workspace has never been synced" until
# you do — it will not silently build against absent bindings.
nros sync
# 2. build. This resolves the application, applies the image's overlays and
# runs `west build` for you.
nros build demo_bringup:zephyr
# 3. start the router ROS itself uses — the one `rmw_zenoh_cpp` ships, so it
# cannot drift from the RMW your ROS 2 nodes use. Separate terminal; it
# keeps running. Listens on tcp/127.0.0.1:7447.
ros2 run rmw_zenoh_cpp rmw_zenohd
# 4. run the image (its own build dir: build/<coordinate>/<bringup>__<image>_west).
./build/zephyr-zenoh/demo_bringup__zephyr_west/zephyr/zephyr.exe
*** Booting Zephyr OS build v3.7.0 ***
<inf> nros_net_wait: Network ready (NSOS — host kernel sockets)
<inf> rust: rustapp: nros: zephyr workspace entry up (2 nodes)
<inf> rust: talker_pkg: talker publishing chatter seq=0
<inf> rust: talker_pkg: talker publishing chatter seq=1
and from a ROS 2 shell on the same router, the nodes are ordinary ROS nodes:
$ export RMW_IMPLEMENTATION=rmw_zenoh_cpp
$ ros2 topic echo /chatter --once
data: 6
---
Step 3 is not optional and its ORDER does not matter, but the locator does: zenoh-pico is a client, so with no router the image gets
<err> rust: nros: zephyr entry — executor open failed: Transport(ConnectionFailed)
which names no port. Both sides default to tcp/127.0.0.1:7447 — the port
rmw_zenohd listens on and a rmw_zenoh_cpp node connects to. Override with
the image’s locator = "tcp/…" in system.toml and
ZENOH_CONFIG_OVERRIDE='listen/endpoints=["tcp/127.0.0.1:<port>"]' on the
router, and change both.
What an image declares
The Zephyr images in examples/workspaces/* are the worked examples:
[image.zephyr]
board = "zephyr"
entry = "zephyr_entry" # only when several entries match, see below
conf = ["prj-zenoh.conf"] # the RMW overlay this app requires
confnames the app’s own Kconfig fragments, in order. They are looked for beside the board config dir, then beside the application, then beside the bringup — the app rung matters because a Zephyr app keeps itsprj-<rmw>.confnext to theCMakeLists.txtwest builds. These entriesFATAL_ERRORwithout an RMW overlay, so the image has to say which one.boardis a nano-ros board id, which nano-ros resolves to the Zephyr board target. See below.entrynames the application package. Normally leave it out: an entry declares the deploy target it serves — its ownsystem.tomlin Rust,nano_ros_add_executable(... DEPLOY zephyr)inCMakeLists.txt— and one package usually claims a given board. Set it when the derivation cannot answer, which happens two ways and says which:- several packages claim the board — deriving would be a coin flip, so
nros buildlists the candidates rather than picking one. Reachable for C/C++ entries, which are matched by theirDEPLOYtoken; - none does —
nros buildfalls back to the Bringup directory, and you see a conf fragment “not found” in a path that is not your application’s. A hand-written application that picks its board withnano_ros_use_board(...)inside its ownCMakeLists.txtis this case: theDEPLOYscan does not read that call (issue 1517). Usually the better fix is to delete the application —nros buildgenerates one and hands it the board package’s own Kconfig and devicetree overlay, which is allnano_ros_use_boardadded (issue 1288).
- several packages claim the board — deriving would be a coin flip, so
If you live in west: west nros
Everything above assumes you stand in your nano-ros workspace and name the
Zephyr one. If you work the other way round — inside the west workspace — west
already knows where Zephyr is, so it can tell nros for you:
cd ~/zephyr-workspace
west nros build --workspace ~/my_robot demo_bringup:zephyr
No ZEPHYR_BASE, no NROS_ZEPHYR_WORKSPACE, no --zephyr-workspace. Every
argument after nros is passed straight through, so anything nros accepts
works here unchanged.
It picks up automatically in any workspace whose manifest lists nano-ros.
west nros is now the only nano-ros west extension: west fvp shared that
route until RFC-0064 revision 5 retired it, since its body was environment
wiring in front of the stock west build -t run. Two things worth knowing:
- It only exists inside a west workspace. West loads extension commands
from the manifest, so from an unrelated directory you get
west: unknown command "nros". That is why this is an addition rather than a replacement: it swaps which of the two trees you have to name, trading the Zephyr path (which differs per machine) for your own workspace path (which you chose). - It never overwrites a
ZEPHYR_BASEyou set yourself — an exported one is a deliberate choice, and this only fills in a missing answer.
Which boards you can name
board names a board in nano-ros’s catalog, and the board’s descriptor
states the Zephyr id west build -b receives:
# packages/boards/zephyr/nros-board.toml
names = ["zephyr"] # what your images say
[board.zephyr]
west_board = "native_sim/native/64" # what west is given
So an image writes board = "zephyr", never board = "native_sim/native/64":
the Zephyr spelling is the descriptor’s to state, and an image that authors it
is refused with the id to write instead (issue 1519).
Nor is it free-form. A board the catalog does not know is refused with the
list of ones it does, because the descriptor supplies more than the -b string
— the platform, the toolchain, the entry kind, the declared capabilities. There
is no safe default for those.
A board is a package — put it in your workspace. Drop an nros-board.toml
beside a package’s package.xml and it joins the catalog like any other
package in src/:
my_robot/src/
├── talker_pkg/
└── my_board/
├── package.xml
└── nros-board.toml
# src/my_board/nros-board.toml
[[board]]
names = ["my-board"] # what your images say
west_board = "qemu_cortex_m3" # what west is given
platform = "zephyr"
toolchain = "stable"
platform_feature = "platform-zephyr"
link_kind = "none"
entry_kind = "zephyr-staticlib"
# src/demo_bringup/system.toml
[image.zephyr]
board = "my-board"
No environment variable, nothing outside the tree, nothing copied into the
nano-ros checkout. west_board is what lets the friendly name be the one your
workspace uses, which is also what the shipped Zephyr descriptors do (under
[board.zephyr]). Leave it out only when your board name already is the west
board target and you have no other name for it.
For a board shared by several workspaces there is no single workspace to
put it in, and $NROS_EXTRA_BOARD_PATH still covers that: PATH-style, naming
directories shaped like packages/boards/.
nros new board <name> --for-platform zephyr scaffolds the descriptor. Note
this is the nano-ros descriptor; an out-of-tree Zephyr board definition
(its devicetree and board.yml) is contributed the Zephyr way, through your
module’s board_root.
Adding your own drivers, modules and boards
Everything Zephyr lets you extend, you still extend the Zephyr way. The
application CMakeLists.txt is yours (RFC-0065 D5) and nros build runs
west build against it, so the extension points are untouched:
| you want | you do | nros build involvement |
|---|---|---|
| an out-of-tree module | list(APPEND ZEPHYR_EXTRA_MODULES <dir>) before find_package(Zephyr) | none |
| a driver in that module | zephyr_library() + zephyr_library_sources(), gated on your own Kconfig | none |
| its devicetree binding | dts/bindings/ + settings: {dts_root: .} in the module’s zephyr/module.yml | none |
| an out-of-tree board / SoC / arch / snippet | the matching *_root in the same module.yml (board_root, soc_root, arch_root, snippet_root, module_ext_root) | none |
| turning your Kconfig on for one image | a conf fragment on [image.*] | passed as EXTRA_CONF_FILE |
| a devicetree overlay for one image | an .overlay in the same conf list | passed as EXTRA_DTC_OVERLAY_FILE |
Only the last two rows are ours, and they are how an IMAGE differs from its
siblings — the module itself is a property of the application, not of one
image, so it belongs in the app’s CMakeLists.txt where you would have put it
anyway.
nros build passes exactly four things to west of its own —
APPLICATION_CONFIG_DIR, EXTRA_CONF_FILE, EXTRA_DTC_OVERLAY_FILE,
FILE_SUFFIX, all Zephyr’s own variables — and there is deliberately no
image key for arbitrary -D defines. Something your application always needs
belongs in its CMakeLists.txt, which keeps one place to look.
Customizing the west command itself
Anything after -- goes to west. west build has two argument zones, and
nros build routes each token to the right one using west’s own flag list:
nros build demo_bringup:zephyr -- --pristine # a west option
nros build demo_bringup:zephyr -- -t run # build, then run it
nros build demo_bringup:zephyr -- -DMY_OPT=1 # a cmake option
nros build demo_bringup:zephyr -- -p always -DX=1 # both, in one go
which becomes, respectively, the west option zone and the cmake zone:
west build -b <board> <app> -p always -- -DEXTRA_CONF_FILE=… -DX=1
nros build --dry-run prints that line without running it, so you can always
see exactly what west is being asked to do — and copy it if you would rather
drive west yourself.
Two flags are refused rather than routed, because the image already decides them and a build must not be able to disagree with its own declaration:
$ nros build demo_bringup:zephyr -- --sysbuild
Error: `--sysbuild` is decided by the image, not by the command line.
It comes from the presence of a `sysbuild.conf` beside the application.
Change it there, so one build cannot disagree with the declaration it was
resolved from.
The other is -b/--board, which comes from the image’s board.
Verified by building one. An out-of-tree module with its own Kconfig, a driver
source gated on it, a devicetree overlay declaring a node and a binding shipped
from the module’s dts_root, added to examples/workspaces/rust’s entry and
enabled through the image:
[image.zephyr]
conf = ["prj-zenoh.conf", "prj-user-extra.conf", "user-widget.overlay"]
*** Booting Zephyr OS build v3.7.0 ***
user_extra: out-of-tree driver init, widget-id=42
<inf> rust: rustapp: nros: zephyr workspace entry up (2 nodes)
<inf> rust: talker_pkg: talker publishing chatter seq=0
The driver’s BUILD_ASSERT(DT_NODE_EXISTS(DT_NODELABEL(user_widget))) is what
makes that a test rather than a screenshot: without the overlay reaching the
build, it fails to compile instead of quietly doing nothing.
A mistake in your module surfaces as Zephyr’s own error, unmediated — a wrong
kconfig: path in module.yml reports
ERROR: "kconfig" key in …/extra_module/zephyr/module.yml has value
"../Kconfig" which does not point to a valid Kconfig file.
which is the message you would get from a plain west build, because it is a
plain west build.
The transports + px4-rs come from the prerequisites step
(west update clones nano-ros but not its submodules). With the Zephyr SDK +
NROS_STD_MSGS_DIR exported (also prerequisites), build your app — nros on
PATH is auto-resolved as the codegen tool:
# native_sim (POSIX, no QEMU). The 3.7 line needs the NSOS line overlay; apply
# the NSOS patches first (see "apply nano-ros's patches" below).
overlay="$PWD/modules/nano-ros/cmake/zephyr/native-sim-line-3.7.conf"
west build -b native_sim/native/64 apps/my_app -- -DCONF_FILE="prj.conf;$overlay"
# A real board, e.g. Cortex-A9 (no native_sim overlay):
west build -b qemu_cortex_a9 apps/my_app
(Verified end-to-end on a fresh BYO west workspace: this builds to
zephyr.exe and runs to Published: 0 against a router started with
ZENOH_CONFIG_OVERRIDE='listen/endpoints=["tcp/127.0.0.1:7447"];scouting/multicast/enabled=false' ros2 run rmw_zenoh_cpp rmw_zenohd.
On the Zephyr 4.4 line, find_package(Python3) requires ≥ 3.12 and you
select the RMW with -S nros-zenoh instead of the overlay.)
For a quick sanity check that the module is wired correctly:
west build -t menuconfig # confirm CONFIG_NROS=y is visible
Rust applications
Two things differ for a Rust app (C/C++ apps skip this section):
-
The Rust crate’s
[lib]must be namedrustapp(crate-type = ["staticlib"]) — azephyr-lang-rustcontract: itsrust_cargo_application()linkslibrustapp.a. The Cargo package name is free. -
Generate the interface crates and the dependency wiring for YOUR layout. From your app dir, run (after the Prerequisites
nros setup, which provides the codegen toolchain + message sources):nros sync --nano-ros-path <path-to-your-nano-ros-module>This writes
generated/<pkg>/(the message crates) and a.cargo/config.tomlwhose[patch.crates-io]points thenros-*crates at yourmodules/nano-ros/packages/core/*and the generated interfaces atgenerated/*. Adjust--nano-ros-pathto your workspace’smodules/nano-ros/packages/core(the dir holdingnros-core,nros-node, …). In-tree examples commit no.cargo/config.tomlat all (phase-445 W6): their equivalent is generated intobuild/<image>/nros-cargo.tomland is likewise repo-relative, so it is no more copyable than the old file was.
Run
# 1. Start the router (ROS's `rmw_zenohd` — nano-ros ships no router
# of its own) on port 7447, the port your app dials by default
# (an image with no `locator` gets "tcp/127.0.0.1:7447"; change
# either side to match):
ZENOH_CONFIG_OVERRIDE='listen/endpoints=["tcp/0.0.0.0:7447"];scouting/multicast/enabled=false' \
ros2 run rmw_zenoh_cpp rmw_zenohd &
# 2. Boot the app from your BYO west workspace:
# QEMU Cortex-A9:
west build -t run
# native_sim:
./build/zephyr/zephyr.exe
# 3. Verify from stock ROS 2 in another terminal. Match the message
# type to the app you booted: the canonical in-tree talker publishes
# std_msgs/String; the raw BYO Talker.c shown above publishes
# std_msgs/Int32.
source /opt/ros/humble/setup.bash
export RMW_IMPLEMENTATION=rmw_zenoh_cpp
# Talker publishes best-effort; stock `ros2 topic echo` defaults to
# RELIABLE, so the QoS-mismatched echo silently delivers nothing.
# Force best-effort to receive:
ros2 topic echo /chatter std_msgs/msg/String --qos-reliability best_effort # in-tree talker
# ros2 topic echo /chatter std_msgs/msg/Int32 --qos-reliability best_effort # BYO my_app
Contributors: nano-ros’s own zephyr talker has a matching in-tree recipe for the canonical
native_simbuild path — see Per-Platform Contributor Lanes.
The Zephyr boot banner runs first, then the talker fires: the
canonical in-tree talker prints Publishing: 'Hello World: 1',
Publishing: 'Hello World: 2', … (Rust + C + C++ all count from
1, matching the official ROS 2 demo talker); the raw BYO Talker.c
above prints Published: 0, Published: 1, …
Readiness signal. On native_sim, expect the first publish line
(Publishing: 'Hello World: 1' for the in-tree talker;
Published: 0 for the BYO app) within 5 seconds of boot (or
./build/zephyr/zephyr.exe); on qemu_cortex_a9 expect it within
~15 seconds (QEMU cold boot + Zephyr init). If no publish line
in 30 seconds:
- Confirm
CONFIG_NROS=ylit up viawest build -t menuconfig; without it the module shell neveradd_subdirectory’s nano-ros. - Check
CONFIG_NETWORKING=y,CONFIG_NET_IPV4=y,CONFIG_NET_TCP=yinprj.conf— Zephyr networking is opt-in. - Confirm
zenohdreachable from the simulated network (Slirp needs10.0.2.2:7447on QEMU; native_sim uses host loopback). - See Troubleshooting — First 10 Minutes.
Zephyr 4.x build gotchas.
Could NOT find Python3 ... required is at least "3.12"— 4.x needs Python ≥ 3.12. Provision one without sudo (e.g.uv venv --python 3.12 .venv312 && uv pip install west -r zephyr/scripts/requirements.txt) and run west through it (.venv312/bin/python -m west build ...), so the ROS descriptor-codegen subprocess still uses the system ROS Python.attempt to assign the value ... to the undefined symbol ETH_NATIVE_POSIX— that symbol was renamedETH_NATIVE_TAPin 4.x; the version-aware NSOS overlay handles it (contributors: the in-treejust zephyr build-onerecipe applies it automatically).
Zephyr 4.x: select the RMW with a snippet
On 4.x, nano-ros ships west snippets so you pick the RMW on the
build line instead of hand-writing the overlay:
west build -b native_sim/native/64 -S nros-cyclonedds apps/my_app
# ^^^^^^^^^^^^^^^^^^^ nros-zenoh | nros-cyclonedds | nros-xrce
The snippet (shipped via the module’s snippet_root) carries the
RMW-common Kconfig — equivalent to merging prj-<rmw>.conf. It is for an
app with NO system.toml: with one, the image’s rmw decides and merges
after the snippet, so a snippet that agrees is redundant and one that names a
different RMW is refused at configure time (phase-481). The
prj-<rmw>.conf / -DCONF_FILE path still works (and is the only option
on 3.7).
nano-ros patches into your workspace
nano-ros needs a few patches to Zephyr’s native_sim NSOS driver
(recvmsg, IPv4-multicast) so the host-loopback-based examples work.
Both supported lines (3.7 LTS and 4.x) take the same path —
nros setup zephyr reads zephyr/patches.yml and applies each patch
against the workspace’s Zephyr tree, sha256-checked. No extra step
on your side beyond the provisioning command (already run during
Prerequisites).
To re-apply (e.g. after a west update reset the tree):
nros setup zephyr --rmw zenoh
(Earlier nano-ros revisions documented a west patch apply flow on
4.x — that required a workspace-side west extension that doesn’t ship
with stock Zephyr. Phase 208.D.7 / E.9 unified both lines on the
provisioner. Cyclone-DDS-on-Zephyr patches stay baked into the pinned
cyclonedds submodule, not delivered through patches.yml.)
Copy out an example as your starting point
The examples/zephyr/<lang>/<role>/ dirs are copy-out clean — copy one
into your own app tree and it builds against the nano-ros module with no
reference back into the nano-ros repo:
cp -r modules/nano-ros/examples/zephyr/c/talker apps/my_app
# cyclonedds examples need the host idlc + the ROS message dirs:
export NROS_STD_MSGS_DIR=/opt/ros/humble/share/std_msgs # PKG_DIR contract
west build -b native_sim/native/64 -S nros-zenoh apps/my_app
Cyclone idlc and the descriptor-gen scripts are located via the module’s
exported cache vars (NROS_CYCLONE_IDLC, NROS_CYCLONE_SCRIPTS_DIR,
NROS_CYCLONE_CMAKE_DIR); message-package dirs come from NROS_<PKG>_DIR
env (defaulting to /opt/ros/humble/share/<pkg>). No /opt/ros or
repo-relative paths are baked into the example.
See the zephyr/
module dir + its Kconfig
for the canonical in-repo surface.
GitHub source
- Zephyr module shell:
zephyr/ - Worked examples:
examples/zephyr/rust/,examples/zephyr/c/,examples/zephyr/cpp/ - Module manifest:
zephyr/module.yml - Kconfig surface (canonical post-Phase-208.D.7 fold — every
CONFIG_NROS*symbol the doc cites lives here):zephyr/Kconfig - Patches applied by
nros setup zephyr(thewest patchflow on this page was retired in Phase 208.E.9):zephyr/patches.yml
Next
- Pick a real board (Nordic, NXP, STM32, …): swap
-b <board>and add a board-specific overlay to yourprj.conf. - Cyclone DDS on Cortex-A/R: see the DDS section of Choosing an RMW Backend for the required Kconfig deltas.
- Build nano-ros’s own Zephyr examples without west: Zephyr (contributor).