RobotForge
Published·~14 min

Nodes, topics, and messages: the ROS 2 mental model

Three primitives. Master them and ROS 2 becomes library lookup. Get them wrong and every ROS project feels like fighting the framework.

by RobotForge
#ros2#fundamentals#beginner

The first ROS 2 lesson got you a turtle moving. This one gets you the mental model — the three primitives every ROS system is made of. Read this once and 80% of ROS 2 reading comprehension is yours forever.

Node: a process that does one job

A ROS 2 node is just a process. It does one thing — read a camera, drive a motor, plan a path — and communicates with other nodes by sending messages. A typical robot runs 10–50 nodes at once. The camera driver, the odometry estimator, the path planner, the motion controller, each in its own node.

Two crucial consequences of the one-node-per-job rule:

  • Crashes are contained. If the path planner segfaults, the motor controller keeps running. A monolithic binary doesn't have this property.
  • Languages mix freely. Your perception node can be Python while your real-time controller is C++. They don't know or care.

Topic: a named channel

A topic is a named pub/sub channel. /camera/image_raw carries camera frames. /cmd_vel carries velocity commands. /odom carries odometry. Any node can publish to a topic. Any node can subscribe. No node "owns" a topic — it's just a name with a type attached.

Topics are asynchronous and many-to-many. You can have five publishers on one topic (three sensors fusing into one stream) and three subscribers (logger, planner, safety monitor). No one needs permission.

Message: the typed payload

A message is the data that flows on a topic, defined by a strongly-typed schema. ROS ships a catalog of standard types:

  • std_msgs/String — a string. Useful for debugging, rarely for production.
  • geometry_msgs/Twist — linear + angular velocity, i.e., "drive forward while turning."
  • geometry_msgs/PoseStamped — a 6-DOF pose with a timestamp and a frame.
  • sensor_msgs/Image — a camera frame with encoding info.
  • sensor_msgs/LaserScan — a lidar sweep.
  • nav_msgs/Odometry — pose + velocity + covariance.

You can define your own (.msg files compile at build time), but start with the standards. Custom types are where ecosystem interop breaks.

Why this scales to real robots

Three primitives might sound too simple to run a real robot. It isn't. The Mars helicopter, the B1 quadruped, and every Waymo prototype run the same three primitives. The reason: the pattern composes.

  • Perception pipeline? Camera node → image_proc node → detection node, connected by sensor_msgs/Image and a custom Detection2DArray.
  • Navigation? SLAM node publishes nav_msgs/OccupancyGrid; planner subscribes, publishes nav_msgs/Path; controller subscribes, publishes geometry_msgs/Twist; base driver subscribes and turns wheels.
  • Manipulation? Same pattern with sensor_msgs/JointState in, trajectory_msgs/JointTrajectory out.

Every "big" ROS system is this pattern repeated.

Discovery: how nodes find each other

In ROS 1, a central roscore held the directory. ROS 2 eliminated that — discovery is decentralized, using the DDS middleware that ships underneath. Start a node; it announces itself over UDP multicast; other nodes that care subscribe. No master, no single point of failure.

The price: network misconfigurations silently break discovery. In 2026 most people set ROS_LOCALHOST_ONLY=1 during development, then switch to the Zenoh RMW for multi-host deployments.

QoS: the knob that bites every new ROS 2 user

ROS 2 added Quality of Service profiles to topics — reliability, durability, history depth. A publisher and subscriber with incompatible QoS look like they're connected but no data flows. This is the #1 gotcha of ROS 2 in 2026.

Rule of thumb:

  • Sensor data (camera, lidar): rclpy.qos.qos_profile_sensor_data — best-effort, low depth. Drop frames rather than block.
  • Command data (cmd_vel, setpoints): rclpy.qos.qos_profile_default — reliable. Don't drop commands.
  • Latched data (maps, calibration): durability=TRANSIENT_LOCAL — last value persists for late joiners.

Common mistakes I see in first ROS 2 nodes

  • Publishing at 1000 Hz from Python. Python's GIL caps you at ~300 Hz honestly; for high-rate loops use C++ or a RT component.
  • Forgetting to call spin() or spin_once(). Callbacks don't fire unless the executor runs.
  • Mixing QoS profiles between publisher and subscriber. Symptoms: topic shows up in ros2 topic list but ros2 topic echo is silent.
  • Using std_msgs/String for structured data. Define a real message or use json in a pinch.

What to do next

Next lesson: write your first publisher and subscriber in Python (and compare the C++ version). That turns this mental model into muscle memory.

Comments

    Sign in to post a comment.