TL;DR
- Install ROS 2 Jazzy on Ubuntu 24.04 with
sudo apt install ros-jazzy-desktop - Core concepts: nodes (processes), topics (pub/sub), services (RPC), actions (long-running tasks)
- Write a Python/C++ publisher/subscriber in 5 minutes
- Use
ros2 launchfor multi-node orchestration - Debug with
rqt,ros2 bag, andrviz2
## 1. Installing ROS 2 (Jazzy) on Ubuntu
Prerequisites
- Ubuntu 24.04 (Noble Numbat) ROS 2 Jazzy requirements
- 64-bit system
- 20GB+ disk space (for full desktop install)
Step 1: Set up locale and repositories
sudo apt update && sudo apt install locales
sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8
sudo apt install software-properties-common
sudo add-apt-repository universe
Step 2: Add ROS 2 repository
sudo apt update && sudo apt install curl -y
curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
sudo apt update
Step 3: Install ROS 2 Jazzy
# Desktop install (recommended): ROS, RViz, demos, tutorials
sudo apt install ros-jazzy-desktop
# Base install (minimal): just ROS and command-line tools
# sudo apt install ros-jazzy-ros-base
# Development tools (optional)
sudo apt install ros-dev-tools
Step 4: Set up environment
# Source ROS 2 setup file (add to ~/.bashrc for persistence)
source /opt/ros/jazzy/setup.bash
# Verify installation
ros2 --version
# Expected output: ros2 version 26.0.0 (or higher)
Gotchas:
- If
ros2 --versionfails, ensure/opt/ros/jazzy/setup.bashis sourced. - For Python packages, install
python3-colcon-common-extensionsif missing.
## 2. Nodes, Topics, Services, and Actions
Core Concepts
| Concept | Description | <a href="/services/physical-ai-robotics">physical ai</a> Stack Mapping |
|---|---|---|
| Node | A process that performs computation (e.g., sensor driver, planner). | COMPUTE |
| Topic | Asynchronous pub/sub communication (e.g., /scan for LiDAR data). | CONNECT |
| Service | Synchronous request/response (e.g., /reset_pose for localization). | REASON |
| Action | Long-running task with feedback (e.g., /navigate_to_pose for navigation). | ACT |
CLI Commands
# List nodes
ros2 node list
# List topics
ros2 topic list
# Echo a topic
ros2 topic echo /scan
# Call a service
ros2 service call /reset_pose std_srvs/srv/Empty
# List actions
ros2 action list
## 3. Writing Your First Publisher/Subscriber
Python Example
Publisher (publisher.py)
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MinimalPublisher(Node):
def __init__(self):
super().__init__('minimal_publisher')
self.publisher_ = self.create_publisher(String, 'topic', 10)
timer_period = 0.5 # seconds
self.timer = self.create_timer(timer_period, self.timer_callback)
self.i = 0
def timer_callback(self):
msg = String()
msg.data = f'Hello ROS 2: {self.i}'
self.publisher_.publish(msg)
self.get_logger().info(f'Publishing: "{msg.data}"')
self.i += 1
def main(args=None):
rclpy.init(args=args)
minimal_publisher = MinimalPublisher()
rclpy.spin(minimal_publisher)
minimal_publisher.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Subscriber (subscriber.py)
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MinimalSubscriber(Node):
def __init__(self):
super().__init__('minimal_subscriber')
self.subscription = self.create_subscription(
String,
'topic',
self.listener_callback,
10)
self.subscription # prevent unused variable warning
def listener_callback(self, msg):
self.get_logger().info(f'I heard: "{msg.data}"')
def main(args=None):
rclpy.init(args=args)
minimal_subscriber = MinimalSubscriber()
rclpy.spin(minimal_subscriber)
minimal_subscriber.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Run the Example
# Terminal 1: Run publisher
python3 publisher.py
# Terminal 2: Run subscriber
python3 subscriber.py
# Expected output (Terminal 2):
# [INFO] [minimal_subscriber]: I heard: "Hello ROS 2: 0"
# [INFO] [minimal_subscriber]: I heard: "Hello ROS 2: 1"
C++ Example
Publisher (publisher.cpp)
#include <chrono>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
class MinimalPublisher : public rclcpp::Node {
public:
MinimalPublisher() : Node("minimal_publisher"), count_(0) {
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
timer_ = this->create_wall_timer(
500ms, std::bind(&MinimalPublisher::timer_callback, this));
}
private:
void timer_callback() {
auto message = std_msgs::msg::String();
message.data = "Hello ROS 2: " + std::to_string(count_++);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
publisher_->publish(message);
}
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
size_t count_;
};
int main(int argc, char * argv[]) {
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalPublisher>());
rclcpp::shutdown();
return 0;
}
Build and Run
# Create a workspace
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake cpp_pubsub
# Replace the generated src/cpp_pubsub/src/publisher_member_function.cpp with the code above
# Build
cd ~/ros2_ws
colcon build --packages-select cpp_pubsub
# Source the workspace
source install/setup.bash
# Run publisher
ros2 run cpp_pubsub talker
## 4. Launch Files and Parameters
Launch File Example (launch_example.py)
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='cpp_pubsub',
executable='talker',
name='talker_node',
parameters=[{'use_sim_time': True}]
),
Node(
package='demo_nodes_py',
executable='listener',
name='listener_node'
)
])
Run the Launch File
ros2 launch launch_example.py
Parameters
# List parameters
ros2 param list
# Get a parameter
ros2 param get /talker_node use_sim_time
# Set a parameter
ros2 param set /talker_node use_sim_time false
## 5. DDS, QoS, and Real-Time Considerations
QoS Profiles
ROS 2 uses Quality of Service (QoS) policies to configure communication reliability, durability, and latency.
| Policy | Options | Use Case |
|---|---|---|
| Reliability | RELIABLE, BEST_EFFORT | Sensor data (BEST_EFFORT), commands (RELIABLE) |
| Durability | VOLATILE, TRANSIENT_LOCAL | Map data (TRANSIENT_LOCAL), telemetry (VOLATILE) |
| History | KEEP_LAST(n), KEEP_ALL | Limited buffer (KEEP_LAST), full replay (KEEP_ALL) |
Example: Configuring QoS in Python
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy
qos_profile = QoSProfile(
reliability=ReliabilityPolicy.RELIABLE,
history=HistoryPolicy.KEEP_LAST,
depth=10
)
self.publisher = self.create_publisher(
String,
'topic',
qos_profile
)
Real-Time Setup
- Use a real-time kernel:
sudo apt install linux-image-rt-amd64 - Configure DDS for real-time:
- Use
rmw_connextddsorrmw_cycloneddswith real-time settings. - Example
cyclonedds.xml:<CycloneDDS> <Domain> <General> <NetworkInterfaceAddress>lo</NetworkInterfaceAddress> <AllowMulticast>false</AllowMulticast> </General> <Tracing> <Verbosity>warning</Verbosity> </Tracing> </Domain> </CycloneDDS>
- Use
- Set thread priorities:
rclcpp::ExecutorOptions options; options.context = rclcpp::Context::make_shared(); options.context->init(0, nullptr, rclcpp::InitOptions().use_real_time()); rclcpp::executors::SingleThreadedExecutor executor(options);
## 6. ros2_control and Hardware Interfaces
Overview
ros2_control is a framework for real-time control of robot hardware. It separates hardware interfaces from control algorithms.
Example: URDF with ros2_control
<robot name="my_robot">
<ros2_control name="MyRobotSystem" type="system">
<hardware>
<plugin>my_robot_hardware/MyRobotSystemHardware</plugin>
</hardware>
<joint name="joint1">
<command_interface name="position"/>
<state_interface name="position"/>
<state_interface name="velocity"/>
</joint>
</ros2_control>
</robot>
Launch File for ros2_control
from launch import LaunchDescription
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
from launch.substitutions import PathJoinSubstitution
def generate_launch_description():
return LaunchDescription([
Node(
package='controller_manager',
executable='ros2_control_node',
parameters=[PathJoinSubstitution([
FindPackageShare('my_robot_description'),
'config',
'my_robot_controllers.yaml'
])]
),
Node(
package='controller_manager',
executable='spawner',
arguments=['joint_state_broadcaster']
),
Node(
package='controller_manager',
executable='spawner',
arguments=['joint_trajectory_controller']
)
])
## 7. Debugging with rqt, ros2 bag, and rviz2
rqt (GUI Tools)
# Launch rqt
rqt
# Common plugins:
# - Topic Monitor (/rqt_topic)
# - Service Caller (/rqt_service_caller)
# - Node Graph (/rqt_graph)
ros2 bag (Recording/Playback)
# Record topics
ros2 bag record /scan /odom
# Play back
ros2 bag play rosbag2_2026_06_02-12_00_00
# Info
ros2 bag info rosbag2_2026_06_02-12_00_00
rviz2 (Visualization)
# Launch RViz2
rviz2
