ROS 2 Core Concepts Explained for Beginners
ROS 2 (Robot Operating System 2) is the framework that most professional robotics teams use to build their software. It is not an operating system in the traditional sense. It is a middleware layer -- a set of libraries, tools, and conventions that handle the plumbing of a robotics system so you can focus on the interesting parts: perception, planning, and control.
If you are new to ROS 2, the terminology can feel overwhelming. Nodes, topics, services, actions, executors, QoS, DDS -- there is a lot of vocabulary to absorb before you can write your first program. This tutorial cuts through the noise and explains each concept clearly, with Python code examples and practical context.
Why ROS 2 Exists
A modern robot runs dozens of independent software components: a camera driver, an object detector, a path planner, a motor controller, a battery monitor, a safety watchdog, and more. These components need to communicate with each other, and they need to do so reliably, efficiently, and without tight coupling.
You could write all of this from scratch using raw sockets, shared memory, or inter-process communication primitives. But then you would spend more time on infrastructure than on robotics. ROS 2 provides standardized communication patterns, a package system for reusing code, and tooling for debugging, visualization, and simulation.
ROS 2 replaced ROS 1 because ROS 1 had fundamental limitations: it depended on a single master node (single point of failure), it was not real-time capable, it had no built-in security, and its communication was not configurable for different network environments. ROS 2 addresses all of these.
Nodes
A node is the basic unit of computation in ROS 2. Each node is a single process (or a component within a process) that does one specific thing. A camera driver is a node. A SLAM algorithm is a node. A motor controller is a node.
Nodes communicate with each other through topics, services, and actions (described below). They do not call each other's functions directly. This loose coupling means you can replace, restart, or update any node without affecting the rest of the system.
Here is a minimal ROS 2 node in Python:
import rclpy
from rclpy.node import Node
class MyNode(Node):
def __init__(self):
super().__init__("my_node")
self.get_logger().info("Node started.")
# Create a timer that fires every 1 second
self.timer = self.create_timer(1.0, self.timer_callback)
self.count = 0
def timer_callback(self):
self.count += 1
self.get_logger().info(f"Timer fired: count={self.count}")
def main():
rclpy.init()
node = MyNode()
rclpy.spin(node) # Keep the node running
node.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()Key points:
rclpy.init()initializes the ROS 2 client library.- The node name (
"my_node") must be unique on the network. rclpy.spin(node)hands control to the ROS 2 executor, which processes callbacks (timers, incoming messages, service requests) until the node is shut down.- Nodes use a logger instead of print statements, which integrates with ROS 2 logging infrastructure.
Topics
Topics are the most common communication mechanism in ROS 2. A topic is a named bus for sending messages. Nodes publish messages to a topic and subscribe to receive messages from a topic.
The key property of topics is that they are many-to-many. Any number of publishers can send messages to the same topic, and any number of subscribers can receive them. Publishers and subscribers do not know about each other -- they only know about the topic name and message type.
Topics are best for continuous data streams: sensor readings, robot state, control commands, and processed results.
Publisher Example
from std_msgs.msg import String
class PublisherNode(Node):
def __init__(self):
super().__init__("publisher_node")
self.publisher = self.create_publisher(String, "chatter", 10)
self.timer = self.create_timer(0.5, self.publish_message)
self.count = 0
def publish_message(self):
msg = String()
msg.data = f"Hello, ROS 2! Message #{self.count}"
self.publisher.publish(msg)
self.get_logger().info(f"Published: {msg.data}")
self.count += 1The create_publisher call takes three arguments: the message type (String), the topic name ("chatter"), and the queue size (10). The queue size determines how many messages are buffered if the subscriber is slower than the publisher.
Subscriber Example
class SubscriberNode(Node):
def __init__(self):
super().__init__("subscriber_node")
self.subscription = self.create_subscription(
String, "chatter", self.on_message, 10
)
def on_message(self, msg: String):
self.get_logger().info(f"Received: {msg.data}")When a message arrives on the "chatter" topic, the on_message callback fires automatically.
Message Types
ROS 2 messages are defined in .msg files and compiled into Python/C++ classes. Common built-in message types include:
std_msgs/String,std_msgs/Int32,std_msgs/Float64-- simple typesgeometry_msgs/Twist-- linear and angular velocity (used to command mobile robots)sensor_msgs/Image-- camera imagessensor_msgs/LaserScan-- LiDAR scansnav_msgs/Odometry-- robot position and velocity estimate
You can define custom message types for your application.
Services
Services are synchronous request-response communication. A service has a server (which provides a capability) and one or more clients (which call it). The client sends a request, waits, and receives a response.
Services are best for one-shot operations: "take a picture," "save the map," "set a parameter," "calculate a trajectory."
Service Definition
Services are defined in .srv files with a request and response separated by ---:
# AddTwoInts.srv
int64 a
int64 b
---
int64 sum
Service Server
from example_interfaces.srv import AddTwoInts
class AddServer(Node):
def __init__(self):
super().__init__("add_server")
self.service = self.create_service(
AddTwoInts, "add_two_ints", self.handle_add
)
def handle_add(self, request, response):
response.sum = request.a + request.b
self.get_logger().info(f"{request.a} + {request.b} = {response.sum}")
return responseService Client
class AddClient(Node):
def __init__(self):
super().__init__("add_client")
self.client = self.create_client(AddTwoInts, "add_two_ints")
# Wait for the service to become available
while not self.client.wait_for_service(timeout_sec=1.0):
self.get_logger().info("Waiting for service...")
def send_request(self, a: int, b: int):
request = AddTwoInts.Request()
request.a = a
request.b = b
future = self.client.call_async(request)
return futureNote that call_async returns a future, not the response directly. This prevents blocking the executor while waiting for the server.
Actions
Actions are for long-running tasks that need progress feedback and can be canceled. They combine the request-response pattern of services with the streaming pattern of topics.
An action has three parts:
- Goal: The client sends a goal (e.g., "navigate to position (3.0, 5.0)").
- Feedback: The server sends periodic updates while working on the goal (e.g., "current position is (1.5, 2.8), ETA 12 seconds").
- Result: The server sends a final result when the goal is complete (e.g., "arrived at (3.0, 5.0), total time 24 seconds").
Actions are best for tasks like navigation, arm motion execution, and sensor calibration -- anything that takes more than a fraction of a second and where the client needs progress updates or the ability to cancel.
Action Definition
# Navigate.action
# Goal
float64 target_x
float64 target_y
---
# Result
float64 total_time
bool success
---
# Feedback
float64 current_x
float64 current_y
float64 distance_remaining
Action servers and clients are more complex than services, but the pattern is consistent: the client sends a goal, receives feedback callbacks, and eventually gets a result.
Quality of Service (QoS)
QoS profiles control how messages are delivered. This is one of the most important and most frequently misunderstood features in ROS 2.
Key QoS settings:
- Reliability:
RELIABLE(retransmits lost messages, like TCP) orBEST_EFFORT(no retransmission, like UDP). Use RELIABLE for commands and critical data. Use BEST_EFFORT for high-frequency sensor data where occasional drops are acceptable. - Durability:
TRANSIENT_LOCAL(late-joining subscribers receive the last published message) orVOLATILE(they do not). Use TRANSIENT_LOCAL for parameters and static maps. Use VOLATILE for real-time sensor data. - History depth: How many messages to keep in the queue. A depth of 1 means the subscriber always gets the most recent message. A larger depth buffers older messages.
A common pitfall for beginners: if your subscriber is not receiving messages, check that its QoS profile is compatible with the publisher's. A BEST_EFFORT subscriber cannot receive from a RELIABLE publisher, and vice versa.
from rclpy.qos import QoSProfile, ReliabilityPolicy, DurabilityPolicy
sensor_qos = QoSProfile(
reliability=ReliabilityPolicy.BEST_EFFORT,
durability=DurabilityPolicy.VOLATILE,
depth=5,
)
# Use this QoS when subscribing to sensor topics
self.create_subscription(LaserScan, "scan", self.on_scan, sensor_qos)DDS: The Communication Layer
Under the hood, ROS 2 uses DDS (Data Distribution Service), an industry-standard middleware protocol for real-time publish-subscribe communication. DDS provides:
- Automatic peer discovery (no master node needed, unlike ROS 1)
- Configurable QoS
- Support for multiple network topologies
- Security (authentication, encryption, access control)
You can choose from several DDS implementations: Fast DDS (default), Cyclone DDS (popular for its simplicity), and Connext DDS (commercial, used in some industrial deployments). The choice is made by setting the RMW_IMPLEMENTATION environment variable.
For beginners, the DDS layer is mostly invisible. You interact with nodes, topics, and services through the rclpy (Python) or rclcpp (C++) API, and DDS handles the networking underneath.
Packages and Workspaces
ROS 2 code is organized into packages. A package is a directory containing:
- Source code (Python or C++)
- A package manifest (
package.xml) listing dependencies - A build file (
setup.pyfor Python,CMakeLists.txtfor C++) - Launch files, configuration files, and message definitions
A workspace is a directory that contains one or more packages. You build the workspace with colcon build, which compiles all packages and sets up the environment.
# Create a workspace
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
# Create a Python package
ros2 pkg create --build-type ament_python my_robot --dependencies rclpy std_msgs
# Build the workspace
cd ~/ros2_ws
colcon build
# Source the workspace
source install/setup.bashAfter sourcing, you can run nodes from your package with ros2 run my_robot my_node.
Launch Files
For any real robot, you need to start many nodes at once with specific parameters and remappings. Launch files handle this.
ROS 2 launch files are written in Python (preferred), XML, or YAML:
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package="my_robot",
executable="camera_driver",
name="front_camera",
parameters=[{"resolution": "640x480", "fps": 30}],
remappings=[("image_raw", "front_camera/image")],
),
Node(
package="my_robot",
executable="object_detector",
name="detector",
remappings=[("image", "front_camera/image")],
),
Node(
package="my_robot",
executable="motor_controller",
name="motors",
),
])Launch files let you:
- Start multiple nodes in a single command
- Pass parameters to nodes
- Remap topic names (so you can reuse the same node code with different topic configurations)
- Set environment variables
- Include other launch files
Run a launch file with: ros2 launch my_robot my_launch.py
ROS 2 vs ROS 1: Key Differences
If you are coming from ROS 1, here are the most important changes:
| Feature | ROS 1 | ROS 2 |
|---|---|---|
| Master node | Required (roscore) | None (DDS peer-to-peer) |
| Communication | Custom protocol (TCPROS) | DDS standard |
| QoS | Not configurable | Fully configurable |
| Real-time | Not supported | Supported (with C++) |
| Security | None built-in | DDS security (SROS2) |
| Build system | catkin | colcon + ament |
| Python support | rospy (limited) | rclpy (full feature parity) |
| Lifecycle | None | Managed node lifecycle |
| Multi-robot | Difficult | Native support via DDS domains |
| OS support | Linux only (officially) | Linux, macOS, Windows |
The biggest practical difference is the removal of the master node. In ROS 1, if roscore crashes, the entire system goes down. In ROS 2, nodes discover each other through DDS multicast, so there is no single point of failure.
Essential Command-Line Tools
ROS 2 comes with powerful CLI tools for debugging and inspection:
# List all running nodes
ros2 node list
# Get info about a node (publishers, subscribers, services)
ros2 node info /my_node
# List all active topics
ros2 topic list
# See messages on a topic in real time
ros2 topic echo /chatter
# Publish a message from the command line
ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \
"{linear: {x: 0.5}, angular: {z: 0.3}}"
# List all services
ros2 service list
# Call a service from the command line
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts \
"{a: 3, b: 5}"
# View the node graph
ros2 run rqt_graph rqt_graph
# Record and play back messages
ros2 bag record -a
ros2 bag play my_recording/These tools are invaluable during development. Before writing any debugging code, check if a CLI tool already shows you what you need.
A Practical Mental Model
Think of a ROS 2 system as a network of independent workers (nodes) communicating through a postal system:
- Topics are bulletin boards: anyone can post a message, and anyone can read it. Great for continuous streams of data.
- Services are help desks: you walk up, ask a question, and wait for the answer. Great for one-shot requests.
- Actions are job orders: you submit a job, get progress updates, and eventually receive the completed work. You can also cancel the job mid-way.
All communication happens over DDS, which handles the networking, discovery, and reliability for you.
Summary
ROS 2 is a large framework, but its core concepts are straightforward:
- Nodes are independent processes that do one thing each.
- Topics carry streaming data between publishers and subscribers.
- Services handle request-response interactions.
- Actions manage long-running tasks with feedback and cancellation.
- QoS controls message delivery guarantees.
- DDS provides the underlying peer-to-peer communication layer.
- Packages organize code, and launch files orchestrate multi-node systems.
Understanding these seven concepts gives you the foundation to build any ROS 2 application. The rest -- specific message types, navigation stacks, manipulation libraries -- builds on top of this foundation.
Want to go deeper? Our ROS 2 Fundamentals lesson walks you through building a complete publisher-subscriber system from scratch, with an interactive code editor where you can experiment with topics, services, and QoS settings directly in the browser.