Dynamic Reconfigure
Change parameters on the fly during runtime with the help of dynamic_reconfigure and never ever restart your whole setup to tune your hardcoded parameters.
We left dynamic_reconfigure
behind, when we migrated to ROS2.
ROS2 has built-in support for reconfigurable parameters.
What’s left is the rqt
plugin which still is named dynamic_reconfigure
.
When we mention dynamic_reconfigure
we either refer to this plugin or we refer to the mechanism of reconfiguring parameters during the runtime of the node.
Prerequisites
a ROS package for which you want to use
dynamic_reconfigure
and implement reconfigurable ROS parameters.
Attention
In this example this package will be called awesome_package
. Replace occurences of this package name with your own package name. Do not copy-paste.
See also
You are not quite sure what the necessary steps to create a new package are? Go back to ROS Package.
Scenario
Imagine we have something like a PID-Controller (yeah, we thought it would be nice to have very applied example, i.e. it is very close to what you want to achieve yourself during this course) and we want to change the gains and maybe some other parameters as well while the node is running. Things we would like to configure could be:
PID gains
limits for the integral term (there are many more anti-windup strategies. Use your preferred search engine for some inspiration.)
Activate/Deactivate the integrator so the integrator is not accumulating control errors while the controller is not active.
Preparation
We create a PID controller node called my_controller.py
.
Make it exectubale.
$ chmod +x ~/fav/ros2/src/awesome_package/nodes/my_controller.py
Add the node the list of nodes to install in the CMakeLists.txt
.
1install(PROGRAMS
2 ...
3 nodes/some_other_node_that_might_also_be_in_this_list.py
4 nodes/setpoint_publisher.py
5 ...
6 DESTINATION lib/${PROJECT_NAME}
7)
Build the workspace. Keep in mind this is only necessary because we changed something in CMakeLists.txt
.
No rebuilt is required if we only modify an already existing node.
Python code does not get compiled.
$ build_ros
See also
See Run A Node for more elaborate instructions for creating a node.
Write the Code
Boilerplate
Starting from a basic node setup:
1#!/usr/bin/env python3
2
3import rclpy
4from rclpy.node import Node
5
6
7class MyControlNode(Node):
8
9 def __init__(self):
10 super().__init__(node_name='my_controller')
11
12
13def main():
14 rclpy.init()
15 node = MyControlNode()
16 try:
17 rclpy.spin(node)
18 except KeyboardInterrupt:
19 pass
20
21
22if __name__ == '__main__':
23 main()
Note
Wondering what the try
statement is good for?
When hitting Ctrl + C, a keyboard interrupt is triggered.
If unhandled this will cause annoying exceptions to be printed in the terminal, even though nothing bad happened.
Hence the except KeyboardInterrupt
will catch this exceptions.
The pass
keyboard instructs the python interpreter to do nothing.
In this example we won’t implement a PID controller. The node will only store the configuration parameters in variables and print them for demonstration purpose.
Declare Parameters
So let’s start with the parameters. We declare them but do not set default values. That is a choice we are making. This way we can make sure, that the values have to be provided when starting the node.
1#!/usr/bin/env python3
2
3import rclpy
4from hippo_msgs.msg import Float64Stamped
5from rclpy.node import Node
6
7
8class MyControlNode(Node):
9
10 def __init__(self):
11 super().__init__(node_name='my_controller')
12 self.init_params()
13
14 def init_params(self):
15 self.declare_parameters(
16 namespace='',
17 parameters=[
18 ('gains.p', rclpy.Parameter.Type.DOUBLE),
19 ('gains.i', rclpy.Parameter.Type.DOUBLE),
20 ('gains.d', rclpy.Parameter.Type.DOUBLE),
21 ],
22 )
23 # the calls to get_parameter will raise an exception if the paramter
24 # value is not provided when the node is started.
25 param = self.get_parameter('gains.p')
26 self.get_logger().info(f'{param.name}={param.value}')
27 self.p_gain = param.value
28
29 param = self.get_parameter('gains.i')
30 self.get_logger().info(f'{param.name}={param.value}')
31 self.i_gain = param.value
32
33 param = self.get_parameter('gains.d')
34 self.get_logger().info(f'{param.name}={param.value}')
35 self.d_gain = param.value
36
37
38def main():
39 rclpy.init()
40 node = MyControlNode()
41 try:
42 rclpy.spin(node)
43 except KeyboardInterrupt:
44 pass
45
46
47if __name__ == '__main__':
48 main()
If we now run the node with
$ ros2 run awesome_package my_controller.py
an exception will be raised.
rclpy.exceptions.ParameterUninitializedException: The parameter 'gains.p' is not initialized
[ros2run]: Process exited with failure 1
This was to be expected.
To test our node before we integrate it in a launch file setup, we can pass the arguments in the terminal with --ros-args
and -p.
$ ros2 run awesome_package my_controller.py --ros-args -p gains.p:=1.0 -p gains.i:=0.01 -p gains.d:=0.0
No exception is raised! And the logs reflect our manually set parameter values 🥳.
Note
We will set these parameters in a more convenient way later on when we create a launch file setup.
We can see the parameters while the node is running
$ ros2 param list
yielding
$ ros2 param list
/my_controller:
gains.d
gains.i
gains.p
start_type_description_service
use_sim_time
Get a parameter value
$ ros2 param get /my_controller gains.p
Double value is: 1.0
Hint
For almost any command you can run my_command --help
to get information on how to use the command.
This is also true for ros2 param
, ros2 param get
, ros2 param set
, etc.
For example
$ ros2 param get --help
Use this kind of help. This way you can figure out yourself how to use all these tools without relying on other people’s tutorials.
We can also set the parameter value, for example
$ ros2 param set /my_controller gains.p 3.0
But currently our node does not handle these changes of the parameters.
Handle Parameter Changes
1#!/usr/bin/env python3
2
3import rclpy
4from hippo_msgs.msg import Float64Stamped
5from rclpy.node import Node
6from rcl_interfaces.msg import SetParametersResult
7
8
9class MyControlNode(Node):
10
11 def __init__(self):
12 super().__init__(node_name='my_controller')
13 self.init_params()
14
15 def init_params(self):
16 self.declare_parameters(
17 namespace='',
18 parameters=[
19 ('gains.p', rclpy.Parameter.Type.DOUBLE),
20 ('gains.i', rclpy.Parameter.Type.DOUBLE),
21 ('gains.d', rclpy.Parameter.Type.DOUBLE),
22 ],
23 )
24 # the calls to get_parameter will raise an exception if the paramter
25 # value is not provided when the node is started.
26 param = self.get_parameter('gains.p')
27 self.get_logger().info(f'{param.name}={param.value}')
28 self.p_gain = param.value
29
30 param = self.get_parameter('gains.i')
31 self.get_logger().info(f'{param.name}={param.value}')
32 self.i_gain = param.value
33
34 param = self.get_parameter('gains.d')
35 self.get_logger().info(f'{param.name}={param.value}')
36 self.d_gain = param.value
37
38 self.add_on_set_parameters_callback(self.on_params_changed)
39
40 def on_params_changed(self, params):
41 param: rclpy.Parameter
42 for param in params:
43 self.get_logger().info(f'Try to set [{param.name}] = {param.value}')
44 if param.name == 'gains.p':
45 self.p_gain = param.value
46 elif param.name == 'gains.i':
47 self.i_gain = param.value
48 elif param.name == 'gains.d':
49 self.d_gain = param.value
50 else:
51 continue
52 return SetParametersResult(successful=True, reason='Parameter set')
53
54
55def main():
56 rclpy.init()
57 node = MyControlNode()
58 try:
59 rclpy.spin(node)
60 except KeyboardInterrupt:
61 pass
62
63
64if __name__ == '__main__':
65 main()
Try setting different values for the declared parameters with ros2 param set
as explained above.
Verify that the logs of the node should reflect the parameter changes.
Use Paramter Files
Parameter files can exist at arbitrary paths.
It is common practice to have the default parameters in a config
directory inside our package.
We create it for this tutorial package with
$ mkdir ~/fav/ros2/src/awesome_package/config
and create a controller_params.yaml
file to store the parameters.
The content of the file is
/**: # wildcard for the node name
ros__parameters:
gains:
p: 1.0
i: 0.0
d: 0.0
Make sure to tell the build system about your config directory.
We do this by making sure your CMakeLists.txt
of our package contains the following lines:
install(
DIRECTORY launch config
DESTINATION share/${PROJECT_NAME}
)
Note
Not sure what the launch
entry does?
Please read our guide on how to write a launch setup.
After we have done this, we need to rebuild our workspace to install the newly added config
directory and the corresponding config file.
$ build_ros
An example showing you how to load this parameter file during your launch setup is shown below. Lines corresponding to the loading of the parameters are highlighted.
1from ament_index_python.packages import get_package_share_path
2from launch_ros.actions import Node, PushRosNamespace
3
4from launch import LaunchDescription
5from launch.actions import (
6 DeclareLaunchArgument,
7 GroupAction,
8)
9from launch.substitutions import LaunchConfiguration
10
11
12def generate_launch_description() -> LaunchDescription:
13 launch_description = LaunchDescription()
14
15 # we ALWAYS use this argument as namespace
16 arg = DeclareLaunchArgument('vehicle_name')
17 launch_description.add_action(arg)
18
19 package_path = get_package_share_path('awesome_package')
20 param_file_path = str(package_path / 'config/controller_params.yaml')
21 # expose the parameter to the launch command line
22 param_file_arg = DeclareLaunchArgument('controller_config_file',
23 default_value=param_file_path)
24 launch_description.add_action(param_file_arg)
25
26 node = Node(executable='my_controller.py',
27 package='awesome_package',
28 parameters=[
29 LaunchConfiguration('controller_config_file'),
30 ])
31 group = GroupAction([
32 PushRosNamespace(LaunchConfiguration('vehicle_name')),
33 node,
34 ])
35 launch_description.add_action(group)
36
37 return launch_description
We have added a new file!
Hence, we rebuild our workspace with build_ros
🥳.
A default parameter file is chosen in line 20. If we run our setup with
$ ros2 launch awesome_package controller.launch.py vehicle_name:=bluerov00
we can tell by the logging output, that our parameters have been loaded from the parameter file:
[INFO] [my_controller.py-1]: process started with pid [11493]
[my_controller.py-1] [INFO] [bluerov00.my_controller]: gains.p=1.0
[my_controller.py-1] [INFO] [bluerov00.my_controller]: gains.i=0.0
[my_controller.py-1] [INFO] [bluerov00.my_controller]: gains.d=0.0
We can test several values for our gains, by editing the controller_params.yaml
file.
From now on no rebuilt of our workspace is required, since we do not add directories or files to our package.
The modification of our files does not require a rebuilt.
Use rqt to Change Parameters
Run
$ rqt
and open the dynamic_reconfigure
plugin as shown below
This way to change parameter values might be less tedious than doing it via the command line.