Building ROS 2 Packages with Python
Learning Objectives
By the end of this section, you will be able to:
- Create ROS 2 packages using Python
- Understand the package structure and dependencies
- Build and run Python-based ROS 2 nodes
- Use ament_python build system effectively
Package Creation
Using ros2 pkg create
The easiest way to create a new ROS 2 package is using the ros2 pkg create command:
ros2 pkg create --build-type ament_python my_robot_package
This creates a basic package structure with the necessary files for a Python-based ROS 2 package.
Package Structure
A typical ROS 2 Python package has the following structure:
my_robot_package/
├── my_robot_package/ # Python package directory
│ ├── __init__.py # Python package initialization
│ ├── my_node.py # Python node implementation
│ └── my_module.py # Additional Python modules
├── setup.py # Python setup script
├── setup.cfg # Configuration for installation
├── package.xml # Package metadata
├── resource/ # Resource files
└── test/ # Test files
└── test_copyright.py
└── test_flake8.py
└── test_pep257.py
Package Configuration Files
package.xml
The package.xml file contains metadata about the package:
<?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_package</name>
<version>0.0.0</version>
<description>Example ROS 2 package using Python</description>
<maintainer email="user@example.com">User Name</maintainer>
<license>Apache License 2.0</license>
<depend>rclpy</depend>
<depend>std_msgs</depend>
<depend>sensor_msgs</depend>
<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>
setup.py
The setup.py file configures how the package is built and installed:
from setuptools import setup
import os
from glob import glob
package_name = 'my_robot_package'
setup(
name=package_name,
version='0.0.0',
packages=[package_name],
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
# Include all launch files
(os.path.join('share', package_name, 'launch'), glob('launch/*launch.[pxy][yma]*')),
# Include all config files
(os.path.join('share', package_name, 'config'), glob('config/*')),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='User Name',
maintainer_email='user@example.com',
description='Example ROS 2 package using Python',
license='Apache License 2.0',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'my_node = my_robot_package.my_node:main',
'another_node = my_robot_package.another_node:main',
],
},
)
setup.cfg
The setup.cfg file specifies installation options:
[develop]
script-dir=$base/lib/my_robot_package
[install]
install-scripts=$base/lib/my_robot_package
Creating Python Nodes
Basic Node Structure
A Python ROS 2 node follows this basic structure:
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
class MyNode(Node):
def __init__(self):
super().__init__('my_node_name')
self.get_logger().info('MyNode has been started')
def main(args=None):
rclpy.init(args=args)
node = MyNode()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Node with Publishers and Subscribers
Here's a more complete example with communication:
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
from sensor_msgs.msg import LaserScan
import math
class RobotController(Node):
def __init__(self):
super().__init__('robot_controller')
# Create publisher
self.publisher = self.create_publisher(String, 'robot_commands', 10)
# Create subscriber
self.subscription = self.create_subscription(
LaserScan,
'laser_scan',
self.laser_callback,
10
)
# Create timer
self.timer = self.create_timer(0.1, self.timer_callback)
self.get_logger().info('Robot Controller Node has been started')
def laser_callback(self, msg):
# Process laser scan data
min_distance = min(msg.ranges)
self.get_logger().info(f'Min distance: {min_distance:.2f}')
def timer_callback(self):
# Send a command
msg = String()
msg.data = 'move_forward'
self.publisher.publish(msg)
def main(args=None):
rclpy.init(args=args)
node = RobotController()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Dependencies Management
ROS Dependencies
In package.xml, specify ROS dependencies:
<depend>rclpy</depend>
<depend>std_msgs</depend>
<depend>geometry_msgs</depend>
<depend>nav_msgs</depend>
<depend>sensor_msgs</depend>
Python Dependencies
For non-ROS Python packages, add them to setup.py:
setup(
# ... other parameters
install_requires=[
'setuptools',
'numpy',
'opencv-python',
'scipy',
# Add other Python dependencies here
],
# ... rest of setup
)
Building and Running
Building the Package
To build a Python-based ROS 2 package:
colcon build --packages-select my_robot_package
Or to build all packages in the workspace:
colcon build
Sourcing the Environment
After building, source the environment:
source install/setup.bash
Running the Node
Run a specific node from the package:
ros2 run my_robot_package my_node
Launch Files
Launch files allow you to start multiple nodes at once. Create a launch directory in your package:
Python Launch File
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='my_robot_package',
executable='my_node',
name='robot_controller',
parameters=[
{'param1': 'value1'},
{'param2': 42},
],
remappings=[
('original_topic', 'remapped_topic'),
],
output='screen'
),
Node(
package='my_robot_package',
executable='another_node',
name='sensor_processor',
output='screen'
)
])
Running Launch Files
Run the launch file:
ros2 launch my_robot_package my_launch_file.py
Parameters
Parameter Declaration and Usage
import rclpy
from rclpy.node import Node
from rclpy.parameter import Parameter
class ParameterNode(Node):
def __init__(self):
super().__init__('parameter_node')
# Declare parameters with default values
self.declare_parameter('robot_name', 'my_robot')
self.declare_parameter('max_velocity', 1.0)
self.declare_parameter('use_sim_time', False)
# Get parameter values
self.robot_name = self.get_parameter('robot_name').value
self.max_velocity = self.get_parameter('max_velocity').value
self.use_sim_time = self.get_parameter('use_sim_time').value
self.get_logger().info(f'Robot name: {self.robot_name}')
self.get_logger().info(f'Max velocity: {self.max_velocity}')
def main(args=None):
rclpy.init(args=args)
node = ParameterNode()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
YAML Parameter Files
Create parameter files in a config directory:
# config/robot_params.yaml
/**:
ros__parameters:
robot_name: 'turtlebot4'
max_velocity: 0.5
use_sim_time: false
sensors:
laser_scan_topic: '/scan'
camera_topic: '/camera/image_raw'
navigation:
planner_frequency: 5.0
controller_frequency: 20.0
Use the parameter file:
ros2 run my_robot_package my_node --ros-args --params-file config/robot_params.yaml
Testing
Unit Tests
Create tests in the test directory:
# test/test_my_node.py
import unittest
import rclpy
from my_robot_package.my_node import MyNode
class TestMyNode(unittest.TestCase):
def setUp(self):
rclpy.init()
def tearDown(self):
rclpy.shutdown()
def test_node_creation(self):
node = MyNode()
self.assertEqual(node.get_name(), 'my_node_name')
node.destroy_node()
if __name__ == '__main__':
unittest.main()
Running Tests
Run tests using colcon:
colcon test --packages-select my_robot_package
colcon test-result --all
Best Practices
Code Structure
- Separate concerns: Keep node logic separate from business logic
- Use modules: Break large nodes into smaller modules
- Follow naming conventions: Use snake_case for Python functions and variables
- Document code: Use docstrings for classes and functions
Error Handling
def safe_divide(self, a, b):
if b == 0:
self.get_logger().error('Division by zero attempted')
return None
return a / b
Logging
Use appropriate log levels:
self.get_logger().debug('Debug information')
self.get_logger().info('General information')
self.get_logger().warn('Warning message')
self.get_logger().error('Error message')
self.get_logger().fatal('Fatal error message')
Interactive Elements
ROS 2 Python Package Quiz
What is the correct build type to use in package.xml for a Python-based ROS 2 package?
Summary
Building ROS 2 packages with Python involves understanding the package structure, configuration files, and the ament_python build system. Proper package organization, dependency management, and following best practices are essential for creating maintainable and reusable ROS 2 packages. The combination of Python's simplicity with ROS 2's powerful communication patterns makes it an excellent choice for robotics development.
In the next section, we'll explore launch files and parameter management in more detail.