Attention
This is older information related to ROS2 Humble. Newer versions, such as Jazzy, are part of the ongoing versions of this tutorial.
Warning
This topic is under heavy construction. Don’t forget your PPE if you’re venturing forward.
Parameters: creating configurable Nodes
The Nodes we have made in the past few sections are interesting because they take advantage of the interprocess communication provided by ROS2.
Other capabilities of ROS2 that we must take advantage of are ROS2 parameters and ROS2 launch files. We can use them to modify the behavior of Nodes without having to modify their source code.
For Python users, that might sound less appealing than for users of compiled languages. However, users of your package might not want nor be able to modify the source code directly, if the package is installable or part of a larger system with multiple users.
Create the package
First, let us create an ament_python package that depends on our packages_with_interfaces and build from there.
cd ~/ros2_tutorial_workspace/src
ros2 pkg create python_package_that_uses_parameters_and_launch_files \
--build-type ament_python \
--dependencies rclpy package_with_interfaces
Overview
Before we start exploring the elements of the package, let us
Create the Node with a configurable publisher using parameters, mostly as we saw in Create the Node with a publisher.
Create a launch file to configure the Node without modifying its source code.
Create the Node using parameters
TL;DR Using parameters in a Node
Declare the parameter with
Node.declare_parameter(), usually in the class’s__init__.Get the parameter with
Node.get_parameter()either once or continuously.
In this step, we’ll work on this.
src/python_package_that_uses_parameters_and_launch_files
└── python_package_that_uses_parameters_and_launch_files/
└── __init__.py
└── amazing_quote_configurable_publisher_node.py
For the sake of the example, let us suppose that we want to make an AmazingQuote publisher that is, now, configurable.
Let’s start by creating an amazing_quote_configurable_publisher_node.py in python_package_that_uses_parameters_and_launch_files/python_package_that_uses_parameters_and_launch_files with the following contents
amazing_quote_configurable_publisher_node.py
1import rclpy
2from rclpy.node import Node
3from package_with_interfaces.msg import AmazingQuote
4
5
6class AmazingQuoteConfigurablePublisherNode(Node):
7 """A configurable ROS2 Node that publishes a configurable amazing quote."""
8
9 def __init__(self):
10 super().__init__('amazing_quote_configurable_publisher_node')
11
12 # Periodically-obtained parameters
13 self.declare_parameter('quote', 'Use the force, Pikachu!')
14 self.declare_parameter('philosopher_name', 'Uncle Ben')
15
16 # One-off parameters
17 self.declare_parameter('topic_name', 'amazing_quote')
18 topic_name: str = self.get_parameter('topic_name').get_parameter_value().string_value
19 self.declare_parameter('period', 0.5)
20 timer_period: float = self.get_parameter('period').get_parameter_value().double_value
21
22 self.configurable_amazing_quote_publisher = self.create_publisher(
23 msg_type=AmazingQuote,
24 topic=topic_name,
25 qos_profile=1)
26
27 self.timer = self.create_timer(timer_period, self.timer_callback)
28
29 self.incremental_id: int = 0
30
31 def timer_callback(self):
32 """Method that is periodically called by the timer."""
33
34 quote: str = self.get_parameter('quote').get_parameter_value().string_value
35 philosopher_name: str = self.get_parameter('philosopher_name').get_parameter_value().string_value
36
37 amazing_quote = AmazingQuote()
38 amazing_quote.id = self.incremental_id
39 amazing_quote.quote = quote
40 amazing_quote.philosopher_name = philosopher_name
41
42 self.configurable_amazing_quote_publisher.publish(amazing_quote)
43
44 self.incremental_id = self.incremental_id + 1
45
46
47def main(args=None):
48 """
49 The main function.
50 :param args: Not used directly by the user, but used by ROS2 to configure
51 certain aspects of the Node.
52 """
53 try:
54 rclpy.init(args=args)
55
56 amazing_quote_configurable_publisher_node = AmazingQuoteConfigurablePublisherNode()
57
58 rclpy.spin(amazing_quote_configurable_publisher_node)
59 except KeyboardInterrupt:
60 pass
61 except Exception as e:
62 print(e)
63
64
65if __name__ == '__main__':
66 main()
Don’t forget to declare the parameter!
Note
According to the official documentation, it is possible to work with undeclared parameters, but I recommend against this for basic usage.
It’s easy to forget it, but Node.get_parameter() will not work if the parameter was not first declared with Node.declare_parameter(). Don’t forget it!
One-off parameters
For one-off parameters, we just get them once after declaring them. Because we’re using those attributes directly in the __init__ method, they are not made attributes of the class, but they could be.
# One-off parameters
self.declare_parameter('topic_name', 'amazing_quote')
topic_name: str = self.get_parameter('topic_name').get_parameter_value().string_value
self.declare_parameter('period', 0.5)
timer_period: float = self.get_parameter('period').get_parameter_value().double_value
self.configurable_amazing_quote_publisher = self.create_publisher(
msg_type=AmazingQuote,
topic=topic_name,
qos_profile=1)
self.timer = self.create_timer(timer_period, self.timer_callback)
In this case, we’re making the topic name and publication periodicity as one-off configurable parameters.
Continuously-obtained parameters
Note
According to the official documentation, it is possible to assign callbacks to manage changes in parameters. It is not the best-documented feature and has some caveats, so we will skip that for now.
For parameters that we obtain continuously through the lifetime of the Node, we can, for example, declare them in the __init__ method, like so
# Periodically-obtained parameters
self.declare_parameter('quote', 'Use the force, Pikachu!')
self.declare_parameter('philosopher_name', 'Uncle Ben')
then obtain them in another method, like so
def timer_callback(self):
"""Method that is periodically called by the timer."""
quote: str = self.get_parameter('quote').get_parameter_value().string_value
philosopher_name: str = self.get_parameter('philosopher_name').get_parameter_value().string_value
amazing_quote = AmazingQuote()
amazing_quote.id = self.incremental_id
amazing_quote.quote = quote
amazing_quote.philosopher_name = philosopher_name
self.configurable_amazing_quote_publisher.publish(amazing_quote)
self.incremental_id = self.incremental_id + 1
In this example, we are making the quote and the philosopher_name as configurable parameters that can be changed continuously, during the lifetime of the Node. After they are changed, the node will publish a message with different contents.
Truly configurable: using _launch.py files
TL;DR Using launch files
(Once) Create a
launchfolder in the project.Create the launch file named as
launch/<something>_launch.py.(Once) modify the
setup.pyto correctly install launch files.
Differently from ROS1, in ROS2 we can use Python launch files. They are quite powerful, well documented, and mentioned first in the official documentation, so we will use them instead of XML or YAML files.
(Once) create the launch folder
In this step, we’ll work on this.
src/python_package_that_uses_parameters_and_launch_files
└── python_package_that_uses_parameters_and_launch_files/
└── __init__.py
└── amazing_quote_configurable_publisher_node.py
└── launch
Well, without further ado
cd ~/ros2_tutorial_workspace/src/python_package_that_uses_parameters_and_launch_files
mkdir launch
Create the launch file
In this step, we’ll work on this.
src/python_package_that_uses_parameters_and_launch_files
└── python_package_that_uses_parameters_and_launch_files/
└── __init__.py
└── amazing_quote_configurable_publisher_node.py
└── launch
└── peanut_butter_falcon_quote_publisher_launch.py
Suppose that we are tired of all the meme quotes and want to make our Node publish a truly inspirational quote. We start by making the launch file named peanut_butter_falcon_quote_publisher_launch.py within the launch folder we just created, with the following contents
peanut_butter_falcon_quote_publisher_launch.py
1from launch import LaunchDescription
2from launch_ros.actions import Node
3
4
5def generate_launch_description():
6 return LaunchDescription([
7 Node(
8 output='screen',
9 emulate_tty=True,
10 package='python_package_that_uses_parameters_and_launch_files',
11 executable='amazing_quote_configurable_publisher_node',
12 name='peanut_butter_falcon_quote_publisher_node',
13 parameters=[{
14 "topic_name": "truly_inspirational_quote",
15 "period": 0.25,
16 "quote": "Yeah, you're gonna die, it's a matter of time. That ain't the question. The question's, "
17 "whether they're gonna have a good story to tell about you when you're gone",
18 "philosopher_name": "Tyler",
19 }]
20 )
21 ])
We’re relying on the LaunchDescription, which expects a list of launch_ros.actions.
from launch import LaunchDescription
from launch_ros.actions import Node
When using a launch_ros.actions.Node, we need to define which package it belongs to and the executable which must match the name
we set for the executable in the setup.py
package='python_package_that_uses_parameters_and_launch_files',
executable='amazing_quote_configurable_publisher_node',
Besides the parameters, we can configure the name of the Node, such that each is unique
name='peanut_butter_falcon_quote_publisher_node',
Finally, our parameters are defined using a dictionary within a list, namely
"topic_name": "truly_inspirational_quote",
"period": 0.25,
"quote": "Yeah, you're gonna die, it's a matter of time. That ain't the question. The question's, "
"whether they're gonna have a good story to tell about you when you're gone",
"philosopher_name": "Tyler",
The setup.py
In this step, we’ll work on this.
src/python_package_that_uses_parameters_and_launch_files
└── python_package_that_uses_parameters_and_launch_files/
└── __init__.py
└── amazing_quote_configurable_publisher_node.py
└── launch
└── peanut_butter_falcon_quote_publisher_launch.py
setup.py
Modify the setup.py to look like this
1import os
2from glob import glob
3from setuptools import setup
4
5package_name = 'python_package_that_uses_parameters_and_launch_files'
6
7setup(
8 name=package_name,
9 version='0.0.0',
10 packages=[package_name],
11 data_files=[
12 ('share/ament_index/resource_index/packages',
13 ['resource/' + package_name]),
14 ('share/' + package_name, ['package.xml']),
15 (os.path.join('share', package_name, 'launch'), glob(os.path.join('launch', '*launch.[pxy][yma]*'))),
16 ],
17 install_requires=['setuptools'],
18 zip_safe=True,
19 maintainer='murilo',
20 maintainer_email='murilomarinho@ieee.org',
21 description='TODO: Package description',
22 license='TODO: License declaration',
23 tests_require=['pytest'],
24 entry_points={
25 'console_scripts': [
26 'amazing_quote_configurable_publisher_node = '
27 'python_package_that_uses_parameters_and_launch_files.amazing_quote_configurable_publisher_node:main',
28 ],
29 },
30)
We have already seen a setup.py so many times we’re almost calling it Wilson.
The only difference is emphasized above inside the data_files, which is the line that will specify that launch files will be installed as well. Notice that
the setup.py looks for files with a specific pattern in the folder launch, so be sure that your launch files
have the correct name otherwise they might not be installed as expected.
Build and source
Before we proceed, let us build and source once.
cd ~/ros2_tutorial_workspace
colcon build
source install/setup.bash
Note
If you don’t remember why we’re building with these commands, see Always source after you build.