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

First Node — Rust (Linux)

Build, run, and verify a single nano-ros publisher node on Linux in about ten minutes. Uses the canonical Zenoh backend. The zenoh router comes from ROS 2 (ros2 run rmw_zenoh_cpp rmw_zenohd) — nano-ros no longer ships one. Rather not run a router? Use --rmw cyclonedds instead: it needs no router at all (see Choosing an RMW). Either way the build generates its messages from your ROS 2 install, so you need ROS 2 sourced for the first build.

Stuck? See Troubleshooting — First 10 Minutes for the common first-build errors.

Prereqs

The nros CLI on PATH (Installation — install.sh puts it in ~/.nros/bin), then provision the Linux host (installs the zenoh client stack into a shared store; the router itself comes from your ROS 2 install):

nros setup linux --rmw zenoh

Scaffold the talker

nros new talker_ws --board linux --template talker --lang rust --rmw zenoh
cd talker_ws

That wrote a workspace with two packages:

talker_ws/
└── src/
    ├── talker_node/            # node package: the code
    │   ├── package.xml         # ROS manifest: <depend>std_msgs</depend>
    │   ├── Cargo.toml          # a library crate; names no board and no RMW
    │   └── src/lib.rs          # the Talker: a 1 s timer publishing /chatter
    └── talker_node_bringup/    # bringup package: what runs where
        ├── package.xml
        └── system.toml         # the board, the RMW, the node it runs

The node package

talker_node is a library. It has no main(), and nothing in it names a board, an RMW or a ROS edition. src/lib.rs declares what the node creates (a publisher on /chatter, a 1 s timer) and what the timer callback does, and its last line exports the node:

#![allow(unused)]
fn main() {
nros::node!(Talker);
}

The program’s main() is generated by the build, from the bringup. That is what lets the same node package go into a Linux image today and a Cortex-M image later without a second copy. Growing your workspace walks through lib.rs line by line.

Cargo.toml holds Rust facts only: the [lib] target, the class the package registers, and its dependencies.

[lib]
path = "src/lib.rs"

[package.metadata.nros.node]
class = "talker_node::Talker"
name = "talker_node"

[dependencies]
nros = { version = "*", default-features = false, features = ["rmw-cffi", "macros"] }
std_msgs = { path = "../../generated/std_msgs", default-features = false }  # nros-managed

nano-ros crates are declared registry-style (version = "*"), but nothing of nano-ros is on crates.io: the build points them at the SDK the nros on PATH carries. The settings that takes (the [patch.crates-io] table, the target triple, linker flags) are generated under build/ from the bringup, never written by hand and never committed. No path in the package names where nano-ros lives, so the workspace copies anywhere.

Message packages are not listed by hand either: package.xml’s <depend>std_msgs</depend> is the one statement, and the build writes the matching # nros-managed row. To use another message package, add its <depend>; the next nros build adds the row.

The bringup package

talker_node_bringup/system.toml is the whole deployment statement:

[system]
name = "talker_node"
rmw = "zenoh"
ros_edition = "humble"
domain_id = 0

[[component]]
pkg = "talker_node"
class = "talker_node::Talker"
name = "talker_node"

[image.linux]
board = "linux"

The [[component]] row says which node runs; [image.linux] says it runs on this machine. The --rmw you passed to nros new lives here and nowhere else, so switching RMW or ROS edition is editing rmw or ros_edition and building again.

Native (host) images read the locator and domain from environment variables (NROS_LOCATOR — the retired ZENOH_LOCATOR is refused at startup — and ROS_DOMAIN_ID). Embedded images bake theirs from the same [image.<id>] table (locator, and the address in net) — the shape shows up under the Embedded Starters section.

This is the same project as examples/linux/rust/talker/ on GitHub, the copy-out example for Rust on Linux: one node package and a bringup with no launch file of its own, since the build generates it. Its bringup goes further than the scaffold, with one image per RMW and ROS edition.

Configure

Three runtime knobs, each an env override on a built-in default:

KnobDefaultEnv override
Zenoh locatortcp/127.0.0.1:7447NROS_LOCATOR
ROS domain ID0ROS_DOMAIN_ID
Zenoh modeclientNROS_SESSION_MODE

On native these stay environment variables — [image.linux] declares no locator. An embedded image bakes them from its own [image.<id>] table instead; see the Configuration Guide.

Build

The build generates std_msgs from your ROS 2 install, so source it first. This first build records the ROS 2 prefix in the workspace; later builds and runs of talker_ws need no sourced shell.

source /opt/ros/humble/setup.bash
nros build            # builds the image `[image.linux]` declares

nros build prepares everything first: the message bindings package.xml declares, the generated main() for [image.linux], and the cargo settings derived from board = "linux", all under build/. Then it runs cargo. There is no step to run before it; when nothing changed, the preparation costs a comparison.

First build pulls dependencies (~3 minutes). Re-builds finish in seconds. When it finishes, nros build prints where the binary landed (under build/, never beside your sources) and how to run it.

Driving cargo yourself (an IDE, a CI step, --release) is supported: nros build --dry-run prints the exact cargo line, against the generated entry under build/. Before you open the workspace in an IDE, run the preparation on its own so rust-analyzer finds the generated crates:

nros configure

What it writes under build/ is regenerated on every build; never edit it, and never add a .cargo/config.toml of your own to carry board facts. Your own Rust-toolchain preferences go where cargo already looks for them — a parent directory or $CARGO_HOME.

Run

Two terminals (each command below blocks; keep them open):

# Terminal 1 — zenoh router (ROS 2's own). Blocks the shell until Ctrl-C.
source /opt/ros/humble/setup.bash
ros2 run rmw_zenoh_cpp rmw_zenohd

# Terminal 2 — the talker. The talker logs via `log::info!`, so set
# RUST_LOG=info — without it `env_logger` only shows errors and the
# `Publishing:` lines stay hidden.
cd talker_ws
RUST_LOG=info nros run talker_node
# Expected output (on stderr):
#   [INFO  talker_node] Publishing: 'Hello World: 1'
#   [INFO  talker_node] Publishing: 'Hello World: 2'
#   …

nros run talker_node runs the bringup whose only node is talker_node, rebuilding first if anything changed. It keeps running until Ctrl-C.

That’s the nano-ros side fully working. Optional step: verify interop with stock ROS 2.

# Terminal 3 — stock ROS 2 with rmw_zenoh_cpp. It talks to the SAME
# router already running in terminal 1 (default tcp/127.0.0.1:7447).
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

If ros2 topic echo shows no output despite the talker printing Publishing:, the routers aren’t peering — confirm both processes point at the same port (default tcp/127.0.0.1:7447).

Readiness signal. Within ~6 seconds of the program starting (session open + the first 1 s timer tick), the talker should print Publishing: 'Hello World: 1' — the count starts at 1, matching the official ROS 2 demo_nodes_cpp talker. If no Publishing: line in 30 seconds:

  1. Confirm RUST_LOG is set. Without RUST_LOG=info (or debug), env_logger filters out the Publishing: lines and the run looks silent even when it’s working.
  2. Confirm the router is running (terminal 1). Without it, the talker blocks on Executor::open indefinitely.
  3. Re-run with RUST_LOG=debug and look for “RMW session open failed” — usually a wrong locator or wrong port.
  4. See Troubleshooting — First 10 Minutes.

Retargeting

To run the same node on a board, add an image for it to src/talker_node_bringup/system.toml, named by that board’s id (Supported Boards), and provision it:

[image.baremetal]
board = "mps2-an385-baremetal"
locator = "serial/UART_0#baudrate=115200"
nros setup mps2-an385-baremetal --rmw zenoh
nros run talker_node_bringup:baremetal

Nothing in src/talker_node/ changes: the board’s descriptor supplies the target triple, the board crate and the memory budgets. With two images the short nros run talker_node no longer picks one, so name the image you mean; nros run lists the spellings when it refuses. The platform chapters scaffold with --board <board> from the start, which writes the image for you.

Next

  • Add a subscription: nros new listener_node --template listener --to talker_node_bringup from the workspace root adds a second node package and its component row (Growing your workspace).
  • Generate bindings for custom .msg / .srv / .action files: Message Generation
  • Cross-compile for an RTOS: pick the right Embedded Starter from the next section (FreeRTOS / Zephyr / NuttX / ThreadX / ESP32 / Bare-metal Cortex-M3).

Design background