Skip to main content

Chapter 5: Building ROS 2 Packages and Applications

5.1 ROS 2 Package Structure

In ROS 2, a package is the fundamental unit for organizing software. A package can contain ROS nodes, libraries, configuration files, message definitions, and other related artifacts. They promote modularity and reusability, making it easier to share and distribute ROS 2 code.

Overview of colcon Workspaces

ROS 2 uses colcon as its primary build tool. colcon operates on the concept of a workspace, which is a directory containing one or more ROS 2 packages. A typical workspace structure looks like this:

my_ros2_workspace/
├── src/ # Source directory where packages reside
│ ├── my_python_package/
│ └── my_cpp_package/
├── build/ # Build directory (generated by colcon)
├── install/ # Install directory (generated by colcon)
└── log/ # Log directory (generated by colcon)

The src directory is where you place your package source folders. When you run colcon build from the workspace root, colcon will detect the packages in src, build them, and place the compiled binaries and installed files into the build and install directories, respectively.

Standard Package Directory Layout

While not strictly enforced, a common and recommended layout for a ROS 2 package includes:

  • package.xml: Defines metadata about the package (name, version, description, maintainers, dependencies).
  • CMakeLists.txt (for C++ packages) or setup.py (for Python packages): Build instructions for colcon.
  • src/: Contains C++ source files.
  • include/: Contains C++ header files.
  • my_python_package/: Contains Python modules.
  • launch/: Contains launch files (.launch.py or .launch.xml).
  • config/: Contains YAML configuration files.
  • share/: Contains non-code resources like message definitions, launch files, or configuration files that need to be "installed" for other packages to find.
  • test/: Contains unit and integration tests.

5.2 Creating ROS 2 Packages in Python

Python is a popular language for ROS 2 development due to its ease of use and extensive libraries. The ros2 pkg create command is used to generate a basic package template.

Using ros2 pkg create

To create a new Python package, navigate to the src directory of your workspace and run:

ros2 pkg create --build-type ament_python my_python_package

This command creates a directory my_python_package with a package.xml and setup.py file configured for a Python package.

setup.py and package.xml Configuration

  • package.xml: Contains essential metadata and build_depends (dependencies needed to build the package) and exec_depends (dependencies needed to run the package). For a Python package, ament_python is a crucial build tool dependency.
  • setup.py: This Python script instructs colcon how to build and install your Python modules and executables. It defines entry points for your nodes and lists your package's Python modules.

Writing Simple Publisher/Subscriber Nodes

A simple Python node will typically:

  1. Import rclpy and other necessary ROS 2 message types.
  2. Initialize rclpy and create a node.
  3. Create a publisher or subscriber.
  4. Implement a callback function for subscribers, or a timer callback for publishers.
  5. Spin the node to keep it alive and process events.

Conceptual example of a Python publisher:

# Minimal Publisher (Python)
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 = 'Hello ROS 2: %d' % self.i
self.publisher_.publish(msg)
self.get_logger().info('Publishing: "%s"' % 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()

5.3 Creating ROS 2 Packages in C++

C++ is another primary language for ROS 2 development, especially for performance-critical components. Similar to Python, ros2 pkg create is used for scaffolding C++ packages.

Using ros2 pkg create

To create a new C++ package, navigate to the src directory of your workspace and run:

ros2 pkg create --build-type ament_cmake my_cpp_package --dependencies rclcpp std_msgs

This command creates a directory my_cpp_package with a package.xml and CMakeLists.txt file, pre-configured for a C++ package and including rclcpp and std_msgs as dependencies.

CMakeLists.txt and package.xml Configuration

  • package.xml: Similar to Python, defines metadata and dependencies. For C++ packages, ament_cmake is the build tool dependency, and you'll typically depend on rclcpp (ROS 2 C++ client library) and message packages like std_msgs.
  • CMakeLists.txt: This CMake script instructs colcon how to compile your C++ source files into executables or libraries, handle dependencies, and install artifacts. It uses ament_cmake functions for ROS 2-specific build steps.

Writing Simple Publisher/Subscriber Nodes

A simple C++ node will generally:

  1. Include rclcpp/rclcpp.hpp and other necessary ROS 2 message headers.
  2. Initialize rclcpp and create a node.
  3. Create a publisher or subscriber object.
  4. Implement a callback function for subscribers, or a timer callback for publishers.
  5. Spin the node to keep it alive and process events.

Conceptual example of a C++ publisher:

// Minimal Publisher (C++)
#include <chrono>
#include <functional>
#include <memory>
#include <string>

#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;
}

5.4 Launch Files and Parameter Management

ROS 2 launch files provide a powerful way to start, configure, and manage multiple nodes simultaneously. They are typically written in Python (.launch.py) or XML (.launch.xml) and allow for complex system orchestration.

Introduction to Launch System (ros2 launch)

Launch files can:

  • Start multiple nodes (executables).
  • Set node parameters.
  • Remap topics or services.
  • Include other launch files.
  • Conditionally execute nodes based on arguments.

Example of a simple Python launch file (my_launch_file.launch.py):

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
return LaunchDescription([
Node(
package='my_python_package',
executable='talker',
name='sim_talker',
output='screen',
emulate_tty=True
),
Node(
package='my_python_package',
executable='listener',
name='sim_listener',
output='screen',
emulate_tty=True
)
])

To run this launch file: ros2 launch my_python_package my_launch_file.launch.py

Using YAML for Parameter Files

Parameters can be set directly in launch files or, more commonly, loaded from external YAML files. This allows for easy modification of node behavior without changing code or launch files.

Example config/params.yaml:

my_node:
ros__parameters:
some_int_param: 10
some_string_param: "hello"

To load parameters from a launch file:

from launch import LaunchDescription
from launch_ros.actions import Node
from ament_index_python.packages import get_package_share_directory
import os

def generate_launch_description():
params_file = os.path.join(
get_package_share_directory('my_python_package'),
'config',
'params.yaml'
)
return LaunchDescription([
Node(
package='my_python_package',
executable='my_node_with_params',
name='my_configured_node',
output='screen',
emulate_tty=True,
parameters=[params_file]
)
])

Passing Arguments to Launch Files

Launch files can accept arguments, making them highly flexible.

5.5 Building and Running Packages with colcon

colcon is the command-line tool used to build, test, and install ROS 2 packages within a workspace.

colcon build, colcon test, colcon clean

  • colcon build: Builds all packages in the workspace, or specific packages if --packages-select is used.
    colcon build
    colcon build --packages-select my_python_package
  • colcon test: Runs tests for packages.
    colcon test
  • colcon clean: Removes build, install, and log directories.
    colcon clean

Sourcing the Workspace

After building, you must "source" the workspace's install/setup.bash (or setup.ps1 for PowerShell, setup.zsh for Zsh) file to make the executables, libraries, and environment variables of your installed packages available to your shell.

source install/setup.bash

It's common to add this command to your ~/.bashrc for convenience.

Running Executables

Once sourced, you can run nodes using ros2 run:

ros2 run my_python_package my_talker_node
ros2 run my_cpp_package my_listener_node

5.6 Debugging ROS 2 Applications

Debugging ROS 2 applications often involves a combination of logging, introspection tools, and standard debugging techniques.

Logging Mechanisms (rclpy.logging, rclcpp::Logger)

  • ROS 2 provides built-in logging capabilities in both Python (rclpy.logging) and C++ (rclcpp::Logger).
  • You can set log levels (DEBUG, INFO, WARN, ERROR, FATAL) to control verbosity.

Example (Python logging):

self.get_logger().info('This is an info message.')
self.get_logger().warn('Something unexpected happened!')

Using rqt_graph, rqt_console

  • rqt_graph: A graphical tool that visualizes the ROS 2 computation graph (nodes, topics, services, actions, and their connections). Invaluable for understanding complex systems.
  • rqt_console: Displays ROS 2 log messages in a GUI, allowing for filtering and highlighting.

Basic Troubleshooting Techniques

  • Check if nodes are running: ros2 node list
  • Inspect active topics: ros2 topic list, ros2 topic info <topic_name>, ros2 topic echo <topic_name>
  • Inspect active services: ros2 service list, ros2 service info <service_name>
  • Inspect active actions: ros2 action list, ros2 action info <action_name>
  • Check parameters: ros2 param list, ros2 param get <node_name> <param_name>
  • Use standard debugger (gdb for C++, pdb for Python) or IDE debuggers (e.g., VS Code).