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; no router needs to be pre-installed (the repo ships zenohd).

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

Prereqs

Pick one path from a fresh checkout — just is NOT a prereq.

A. Bare machine (no Rust, no just, no cargo):

./scripts/bootstrap.sh base

Installs rustup, just, builds the in-tree nros CLI at packages/cli/target/release/nros, leaves the binary on PATH for this shell.

B. Already have cargo (most contributors):

cargo build --release --manifest-path packages/cli/Cargo.toml --bin nros
export PATH="$PWD/packages/cli/target/release:$PATH"

C. Tagged release, no Rust at all:

./scripts/install-nros-prebuilt.sh

Downloads the matching nros-<triple>.tar.gz from the GitHub release, sha256-verifies, installs to packages/cli/target/release/nros.

Every subsequent shell sources the workspace env via one of:

direnv allow                  # if you use direnv
source ./activate.sh          # bash / zsh
source ./activate.fish        # fish

Then provision the native host (installs the zenoh router zenohd into a shared store — no ROS 2 needed):

nros setup native --rmw zenoh

See Install + first build (Linux) for more.

Project layout

The talker is a standalone Cargo package that pulls nano-ros in via a path dependency. Three files matter:

examples/native/rust/talker/
├── Cargo.toml          # path dep on `nros` + `nros-rmw-zenoh`
├── package.xml         # ROS-style manifest (drives codegen tooling)
├── generated/          # auto-generated message bindings (gitignored)
└── src/
    └── main.rs         # 60-line talker

POSIX talkers read the locator / domain from environment variables (NROS_LOCATOR — legacy alias ZENOH_LOCATOR — and ROS_DOMAIN_ID) — no nros.toml is needed. The nros.toml shape used by embedded targets shows up under the Embedded Starters section.

The Cargo.toml is the contract that wires nano-ros into your package. The in-tree talker is a member of the nano-ros workspace, so it does NOT carry a [workspace] table — cargo walks up and picks up the root Cargo.toml. Verbatim from examples/native/rust/talker/Cargo.toml (trimmed to the docs-relevant fields — the in-tree file also exposes rmw-cyclonedds / rmw-xrce features for the multi-RMW build path):

[package]
name    = "native-rs-talker"
version = "0.1.0"
edition = "2024"
license = "MIT OR Apache-2.0"
publish = false

[[bin]]
name = "talker"
path = "src/main.rs"

[features]
default   = ["rmw-zenoh"]
rmw-zenoh = ["dep:nros-rmw-zenoh"]

[dependencies]
nros = { path = "../../../../packages/core/nros",
         default-features = false,
         features = ["std", "rmw-cffi", "platform-posix"] }
nros-platform-cffi = { path = "../../../../packages/core/nros-platform-cffi",
                       features = ["posix-c-port"] }
nros-rmw-zenoh = { path = "../../../../packages/zpico/nros-rmw-zenoh",
                   features = ["std", "platform-posix", "ros-humble"],
                   optional = true }
std_msgs = { version = "*", default-features = false }
log = "0.4"
env_logger = "0.11"

Copying this out of the workspace? Once you move the directory elsewhere on disk, the path deps no longer resolve and there is no parent workspace to inherit from. Two options:

  1. Replace each path = "../../../../packages/..." with an absolute path to your nano-ros checkout, AND add an empty [workspace] table to stop cargo walking further up the filesystem.
  2. Keep it inside examples/ in your own fork of nano-ros — the simpler path while you’re learning the API.

Configure

Three runtime knobs, each overridable at three layers (defaults → config.toml → env vars):

KnobDefaultEnv override
Zenoh locatortcp/127.0.0.1:7447NROS_LOCATOR (legacy alias: ZENOH_LOCATOR)
ROS domain ID0ROS_DOMAIN_ID
Zenoh modeclientNROS_SESSION_MODE (legacy alias: ZENOH_MODE)

config.toml (optional, alongside Cargo.toml):

[zenoh]
locator   = "tcp/127.0.0.1:7447"
domain_id = 0

Build

cd examples/native/rust/talker
cargo build           # or: cargo build --release

First build pulls dependencies (~3 minutes). Re-builds finish in seconds.

Run

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

# Terminal 1 — zenoh router. Blocks the shell until Ctrl-C.
zenohd                               # installed by `nros setup native`

# Terminal 2 — the talker. The talker logs via `log::info!`, so set
# RUST_LOG=info — without it `env_logger` only shows errors and the
# `Published: N` lines stay hidden.
cd examples/native/rust/talker
RUST_LOG=info cargo run
# Expected output (on stderr):
#   [INFO  native_rs_talker] nros Native Talker (Zenoh Transport)
#   [INFO  native_rs_talker] =========================================
#   [INFO  native_rs_talker] Published: 0
#   [INFO  native_rs_talker] Published: 1
#   …

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. NOTE: rmw_zenoh_cpp
# uses its OWN router daemon (`ros2 run rmw_zenoh_cpp rmw_zenohd`),
# NOT the in-tree zenohd from terminal 1. They need to peer with
# each other, or both clients need to point at the same router.
# Simplest: stop terminal 1 and run only `rmw_zenohd` instead, then
# launch the talker pointing at rmw_zenohd's port (default 7447).
source /opt/ros/humble/setup.bash
export RMW_IMPLEMENTATION=rmw_zenoh_cpp
ros2 run rmw_zenoh_cpp rmw_zenohd &       # in its own subshell
# 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/Int32 --qos-reliability best_effort

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

Readiness signal. Within 5 seconds of RUST_LOG=info cargo run, the talker should print Published: 0 (the Rust talker pre-publishes 0 before the counter advances). If no Published: line in 30 seconds:

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

GitHub source

Canonical, copy-out: examples/native/rust/talker/

Copy the directory, rename the package, and your starter is ready to modify.

Next

  • Add a subscription: examples/native/rust/listener/
  • 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).