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

Anatomy of What You Just Built

Four directories, three roles, one configuration file. Everything you will ever add to this project is another instance of one of these three roles — the shape never changes, it only gains packages.

If you come from ROS 2, each role is something you already know:

Your workspaceRoleIn rclcpp terms
src/talker_pkg/, src/listener_pkg/Node packagea composable node (rclcpp_components)
src/demo_bringup/Bringup package<pkg>_bringup — launch files + config, no code
(generated under build/)Entrythe composition container / main()

Node packages — the code

A node package is one node as a reusable library. The C++ shape is a class with a configure(rclcpp::Node&) method that binds real member callbacks:

src/talker_pkg/
├── CMakeLists.txt          # nano_ros component library
├── package.xml             # ROS package manifest — deps live here
├── include/talker_pkg/Talker.hpp
└── src/Talker.cpp          # create_wall_timer<C,&C::m> → publish on /chatter

package.xml is standard ROS: message dependencies (std_msgs) are declared there and code-generated during the build. You never hand-write message types.

The bringup package — the one configuration file

src/demo_bringup/ holds no code: a launch file and system.toml. This is where every configuration axis lives:

[system]
name = "demo"
rmw = "cyclonedds"                   # the RMW axis
domain_id = 0
default_launch = "system.launch.xml"

[[component]]                        # the topology: which nodes exist
pkg = "talker_pkg"
class = "talker_pkg::Talker"
name = "talker"

[image.linux]                       # one block per BUILDABLE image
board = "linux"

[host.native]                        # WHERE the nodes run; no `nodes` = all

The launch file is the ROS 2 launch XML schema, verbatim — <node>, <param>, <remap>, <group>, <include> with $(find) / $(var) substitutions. A launch file from nav2 or Autoware pastes in unchanged (Python .launch.py is not supported). The scaffold’s launch even carries a per-topic QoS override to show the plumbing:

<node pkg="talker_pkg" exec="talker" name="talker">
  <param name="qos_overrides./chatter.publisher.depth" value="5"/>
</node>

An override changes how the two ends MATCH as well as how one end behaves. Setting the publisher to reliability = best_effort here would look harmless and deliver nothing on CycloneDDS: DDS never pairs a best-effort writer with a reliable reader, and the listener’s subscription is reliable by default. (ros2 topic echo --qos-reliability best_effort is the same rule seen from the other side.)

When you later target hardware, you add an [image.<id>] block per board — the code and topology stay put, and no new directory appears. That is the growth rule: configuration is a new block, never a restructure.

The entry — the deliverable, and it is generated

There is no entry directory under src/. nros build reads each [image.<id>] in system.toml and generates its entry under build/ (for the scaffold’s [image.linux], the binary is build/posix-cyclonedds-linux/cmake/linux_entry). That entry resolves the bringup’s launch file and has a main() that constructs each declared component, applies launch parameters and QoS overrides, and runs them on one executor — the launch product is the binary. There is no separate ros2 launch step; running the entry is launching the system.

One binary per deployable image — and image is a row, not a directory. A robot with a perception image and a control image is one workspace, one bringup, and two [image.*] blocks in its system.toml; nros build derives each entry from (launch, args, board) and generates it under build/. Everything a hand-written entry used to carry — its board, its RMW locator — is a key in that block. See Images.

What was generated vs. what is yours

Everything under src/ is yours to edit. The build tree (build/) holds everything derived: generated message code, the resolved system model, the entry’s generated main(). Nothing derived is ever committed — if you can regenerate it, it does not belong in git.

The same anatomy in Rust

nros new --workspace --lang rust produces the identical shape: node packages are library crates with nros::node!(Talker), the entry is a one-line nros::main!(launch = "demo_bringup", spin = "forever"), and the bringup directory is byte-for-byte the same idea. One difference: Rust message codegen runs as a pre-build step (nros sync) instead of inside CMake.

Next