Building ROS 2 Packages
Learning Objectives
By the end of this lesson, you will be able to:
- Create ROS 2 packages using Python and C++
- Organize code with proper package structure and dependencies
- Build and install packages using colcon
- Configure packages with parameters and launch files
- Share packages via version control and distribution
What is a ROS 2 Package?
A package is the fundamental unit of organization in ROS 2. It contains:
- Nodes: Executable programs
- Launch files: Scripts to start multiple nodes
- Configuration: Parameters, YAML files
- Messages/Services: Custom data types
- Documentation: README, examples
Package Types
| Type | Language | Build System | Use Case |
|---|---|---|---|
| ament_python | Python | setuptools | Rapid prototyping, research |
| ament_cmake | C++ | CMake | Performance-critical, production |
| ament_cmake (Python) | Python | CMake | Mixed C++/Python projects |
Creating a Python Package
Step 1: Create Workspace
# Create workspace directory
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
Step 2: Create Package
ros2 pkg create my_robot_controller \
--build-type ament_python \
--dependencies rclpy geometry_msgs sensor_msgs
Generated structure:
my_robot_controller/
├── package.xml # Package metadata and dependencies
├── setup.py # Python build configuration
├── setup.cfg # Install configuration
├── resource/ # Resource index marker
├── test/ # Unit tests
└── my_robot_controller/ # Python module
└── __init__.py
Step 3: Add Node Code
Create my_robot_controller/my_robot_controller/teleop_node.py:
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
class TeleopNode(Node):
def __init__(self):
super().__init__('teleop_node')
self.publisher_ = self.create_publisher(Twist, '/cmd_vel', 10)
# Parameters
self.declare_parameter('linear_speed', 0.5)
self.declare_parameter('angular_speed', 1.0)
self.get_logger().info('TeleopNode started')
def move_forward(self):
msg = Twist()
msg.linear.x = self.get_parameter('linear_speed').value
self.publisher_.publish(msg)
def main(args=None):
rclpy.init(args=args)
node = TeleopNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Step 4: Configure setup.py
Edit setup.py to register the node as an executable:
from setuptools import setup
package_name = 'my_robot_controller'
setup(
name=package_name,
version='0.0.1',
packages=[package_name],
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='Your Name',
maintainer_email='you@example.com',
description='Robot teleoperation controller',
license='Apache License 2.0',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'teleop = my_robot_controller.teleop_node:main',
],
},
)
The entry_points section creates an executable called teleop that runs the main() function.
Step 5: Update package.xml
Ensure dependencies are declared:
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>my_robot_controller</name>
<version>0.0.1</version>
<description>Robot teleoperation controller</description>
<maintainer email="you@example.com">Your Name</maintainer>
<license>Apache License 2.0</license>
<!-- Build dependencies -->
<buildtool_depend>ament_python</buildtool_depend>
<!-- Runtime dependencies -->
<depend>rclpy</depend>
<depend>geometry_msgs</depend>
<depend>sensor_msgs</depend>
<!-- Test dependencies -->
<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>python3-pytest</test_depend>
<export>
<build_type>ament_python</build_type>
</export>
</package>
Step 6: Build Package
cd ~/ros2_ws
colcon build --packages-select my_robot_controller
# Source the workspace
source install/setup.bash
Step 7: Run the Node
ros2 run my_robot_controller teleop
Creating a C++ Package
Step 1: Create Package
cd ~/ros2_ws/src
ros2 pkg create my_cpp_controller \
--build-type ament_cmake \
--dependencies rclcpp std_msgs
Generated structure:
my_cpp_controller/
├── CMakeLists.txt # Build configuration
├── package.xml # Package metadata
├── include/my_cpp_controller/ # Header files
└── src/ # Source files
Step 2: Add Node Code
Create src/simple_publisher.cpp:
#include <chrono>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
class SimplePublisher : public rclcpp::Node
{
public:
SimplePublisher()
: Node("simple_publisher"), count_(0)
{
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
timer_ = this->create_wall_timer(
500ms, std::bind(&SimplePublisher::timer_callback, this));
RCLCPP_INFO(this->get_logger(), "SimplePublisher started");
}
private:
void timer_callback()
{
auto message = std_msgs::msg::String();
message.data = "Hello, ROS 2! " + std::to_string(count_++);
publisher_->publish(message);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
}
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<SimplePublisher>());
rclcpp::shutdown();
return 0;
}
Step 3: Configure CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(my_cpp_controller)
# Compiler settings
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
# Find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
# Add executable
add_executable(simple_publisher src/simple_publisher.cpp)
# Link dependencies
ament_target_dependencies(simple_publisher
rclcpp
std_msgs
)
# Install executable
install(TARGETS
simple_publisher
DESTINATION lib/${PROJECT_NAME}
)
# Export dependencies
ament_package()
Step 4: Build and Run
cd ~/ros2_ws
colcon build --packages-select my_cpp_controller
source install/setup.bash
ros2 run my_cpp_controller simple_publisher
Working with Parameters
Parameters allow runtime configuration without recompiling code.
Python Parameters
class ConfigurableNode(Node):
def __init__(self):
super().__init__('configurable_node')
# Declare parameters with defaults
self.declare_parameter('robot_name', 'default_robot')
self.declare_parameter('max_speed', 2.0)
self.declare_parameter('enable_logging', True)
def use_parameters(self):
name = self.get_parameter('robot_name').get_parameter_value().string_value
max_speed = self.get_parameter('max_speed').get_parameter_value().double_value
logging = self.get_parameter('enable_logging').get_parameter_value().bool_value
self.get_logger().info(f'Robot: {name}, Max Speed: {max_speed}, Logging: {logging}')
Run with parameters:
# Command-line parameters
ros2 run my_package configurable_node --ros-args \
-p robot_name:=my_robot \
-p max_speed:=3.5 \
-p enable_logging:=false
# YAML file parameters
ros2 run my_package configurable_node --ros-args \
--params-file config/params.yaml
YAML Parameter File
Create config/params.yaml:
configurable_node:
ros__parameters:
robot_name: "autonomous_bot"
max_speed: 2.5
enable_logging: true
sensor_rate: 50
Custom Messages and Services
Creating Custom Messages
Step 1: Create Package for Interfaces
ros2 pkg create my_robot_interfaces \
--build-type ament_cmake
Step 2: Define Message
Create msg/RobotStatus.msg:
# RobotStatus.msg
string robot_name
float64 battery_level
float64 temperature
bool is_moving
geometry_msgs/Pose current_pose
Step 3: Update CMakeLists.txt
find_package(rosidl_default_generators REQUIRED)
find_package(geometry_msgs REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/RobotStatus.msg"
DEPENDENCIES geometry_msgs
)
ament_package()
Step 4: Update package.xml
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
<depend>geometry_msgs</depend>
Step 5: Build
colcon build --packages-select my_robot_interfaces
source install/setup.bash
Using Custom Messages
from my_robot_interfaces.msg import RobotStatus
class StatusPublisher(Node):
def __init__(self):
super().__init__('status_publisher')
self.publisher_ = self.create_publisher(RobotStatus, '/robot_status', 10)
def publish_status(self):
msg = RobotStatus()
msg.robot_name = "R2D2"
msg.battery_level = 85.5
msg.temperature = 42.3
msg.is_moving = True
self.publisher_.publish(msg)
Package Dependencies
Dependency Types
<!-- Build-time only -->
<build_depend>some_package</build_depend>
<!-- Runtime only -->
<exec_depend>some_package</exec_depend>
<!-- Both build and runtime -->
<depend>some_package</depend>
<!-- Testing only -->
<test_depend>pytest</test_depend>
<!-- Build tool -->
<buildtool_depend>ament_cmake</buildtool_depend>
Installing Dependencies
# Install all dependencies for workspace
cd ~/ros2_ws
rosdep install --from-paths src --ignore-src -r -y
Colcon Build System
Basic Commands
# Build all packages
colcon build
# Build specific package
colcon build --packages-select my_package
# Build with verbose output
colcon build --event-handlers console_direct+
# Build in parallel
colcon build --parallel-workers 4
# Clean build
rm -rf build install log
colcon build
# Build with debug symbols
colcon build --cmake-args -DCMAKE_BUILD_TYPE=Debug
Source Setup Script
# Source workspace
source install/setup.bash
# Add to .bashrc for automatic sourcing
echo "source ~/ros2_ws/install/setup.bash" >> ~/.bashrc
Package Best Practices
1. Organized Structure
my_robot_package/
├── config/ # YAML configuration files
│ ├── params.yaml
│ └── rviz_config.rviz
├── launch/ # Launch files
│ ├── robot.launch.py
│ └── simulation.launch.py
├── msg/ # Custom messages
│ └── CustomMsg.msg
├── srv/ # Custom services
│ └── CustomSrv.srv
├── my_robot_package/ # Python source
│ ├── __init__.py
│ ├── node1.py
│ └── utils.py
├── test/ # Tests
│ ├── test_node1.py
│ └── test_integration.py
├── package.xml
├── setup.py
└── README.md
2. Versioning
Use semantic versioning in package.xml:
<version>1.2.3</version>
<!-- MAJOR.MINOR.PATCH -->
<!-- MAJOR: Breaking changes -->
<!-- MINOR: New features (backward compatible) -->
<!-- PATCH: Bug fixes -->
3. Documentation
# my_robot_controller
Robot teleoperation controller with keyboard and gamepad support.
## Dependencies
- ROS 2 Humble
- Python 3.10+
## Installation
```bash
cd ~/ros2_ws/src
git clone https://github.com/user/my_robot_controller.git
cd ~/ros2_ws
rosdep install --from-paths src --ignore-src -r -y
colcon build --packages-select my_robot_controller
Usage
ros2 run my_robot_controller teleop
Parameters
linear_speed(double, default: 0.5): Max linear velocityangular_speed(double, default: 1.0): Max angular velocity
### 4. **Testing**
Add unit tests in `test/`:
```python
# test/test_teleop.py
import pytest
from my_robot_controller.teleop_node import TeleopNode
def test_node_creation():
node = TeleopNode()
assert node.get_name() == 'teleop_node'
Run tests:
colcon test --packages-select my_robot_controller
colcon test-result --verbose
Hands-On Exercise: Multi-Node Package
Goal: Create a package with 3 nodes (publisher, processor, subscriber) and custom message.
Requirements:
- Custom Message:
SensorData.msgwithtemperature,humidity,timestamp - Publisher Node: Publishes random sensor data at 10 Hz
- Processor Node: Subscribes to sensor data, calculates moving average, publishes to
/processed_data - Logger Node: Subscribes to
/processed_data, logs to file - Launch File: Starts all 3 nodes
- Config File: Adjustable sampling rate and window size
Package structure:
sensor_package/
├── msg/
│ └── SensorData.msg
├── config/
│ └── sensor_params.yaml
├── launch/
│ └── sensor_system.launch.py
├── sensor_package/
│ ├── publisher_node.py
│ ├── processor_node.py
│ └── logger_node.py
├── package.xml
└── setup.py
Test:
ros2 launch sensor_package sensor_system.launch.py
ros2 topic echo /processed_data
Key Takeaways
✅ Packages organize code: Nodes, messages, launch files, config
✅ Python packages use ament_python: Fast iteration, research
✅ C++ packages use ament_cmake: Performance, production
✅ Custom messages: Define in separate package, share across nodes
✅ Colcon builds workspaces: colcon build, source install/setup.bash
✅ Parameters for configurability: YAML files, command-line args
What's Next?
With packages built, let's orchestrate multi-node systems with launch files:
👉 Launch Files & Parameters — Automate node startup