Mastering ROS 2 Parameters with Python
ROS 2 parameters are a fundamental concept for building flexible and adaptable robotic applications. They provide a mechanism for nodes to expose configuration values that can be queried and modified at runtime, without needing to recompile or even restart the node. This capability is essential for developing robust and versatile robot systems that can easily adapt to changing environments, tasks, or operational requirements. Understanding how to effectively use ROS 2 parameters with Python is a cornerstone for any serious ROS 2 developer.
This comprehensive guide will explore the intricacies of ROS 2 parameters, focusing specifically on their implementation and usage within Python. We will cover everything from declaring parameters and setting their default values to dynamically retrieving and updating them during node execution. Furthermore, we will delve into advanced topics such as parameter descriptors, validation, and how to save and load parameter configurations for persistent settings. By the end of this article, you will possess a deep understanding of how to leverage ROS 2 parameters to create highly configurable and intelligent robotic systems.
What are ROS 2 Parameters and Why Are They Important?
ROS 2 parameters serve as dynamic configuration variables for individual nodes or an entire ROS 2 system. Think of them as tunable settings that influence a node’s behavior. Instead of hardcoding values directly into your source code, parameters allow you to externalize these settings, making your nodes more generic and reusable. For instance, a navigation node might have parameters for maximum speed, obstacle avoidance distance, or path planning algorithm selection.
The importance of parameters in ROS 2 cannot be overstated. They enable critical functionalities such as adapting robot behavior to different scenarios, debugging and tuning systems without recompilation, and facilitating the creation of modular and extensible software architectures. Without parameters, every change to a configuration value would necessitate a code modification, recompilation, and redeployment, which is impractical in complex robotic systems. Parameters promote agility and significantly reduce development and testing cycles. They are a core component of the ROS 2 ecosystem, providing a standardized way for nodes to interact with their configuration.
Setting Up Your ROS 2 Workspace
Before we delve into the code, ensure you have a properly configured ROS 2 workspace. If you don’t already have one, follow these steps to create and source it. A workspace provides an organized environment for your ROS 2 packages, allowing you to develop and build your applications efficiently. This setup is crucial for managing dependencies and ensuring your nodes can be found and executed by the ROS 2 runtime.
First, create a new directory for your workspace and a src subdirectory within it:
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
Next, create a new Python package for our examples. We’ll call it param_tutorial. The --build-type ament_python flag specifies that this will be a Python package, which is essential for our Python-based parameter examples.
ros2 pkg create --build-type ament_python param_tutorial
Navigate back to the root of your workspace and build it. This step compiles any C++ code and processes Python packages, making them discoverable by ROS 2.
cd ~/ros2_ws
colcon build
Finally, source your workspace. This command adds your workspace’s install space to your environment variables, allowing you to run your new ROS 2 packages and nodes. You’ll need to do this in every new terminal session where you want to work with your ROS 2 packages.
source install/setup.bash
With your workspace set up, you are now ready to create and run ROS 2 nodes that utilize parameters. This foundational step ensures that all subsequent code examples will execute correctly within your ROS 2 environment.
Declaring ROS 2 Parameters in Python Nodes
Declaring parameters is the first step in making them available to your ROS 2 node. In Python, this is typically done within the __init__ method of your Node class. The self.declare_parameter() method is used for this purpose, allowing you to specify the parameter’s name, its default value, and optionally, a ParameterDescriptor for more detailed configuration.
Let’s create a simple Python node that declares a few parameters. Open ~/ros2_ws/src/param_tutorial/param_tutorial/simple_param_node.py and add the following code:
import rclpy
from rclpy.node import Node
from rcl_interfaces.msg import ParameterDescriptor, ParameterType
class SimpleParamNode(Node):
def __init__(self):
super().__init__('simple_param_node')
self.get_logger().info('SimpleParamNode has been started.')
# Declare a simple string parameter
self.declare_parameter('robot_name', 'Clawbot')
# Declare an integer parameter with a default value
self.declare_parameter('max_speed_rpm', 100)
# Declare a boolean parameter for a toggle feature
self.declare_parameter('enable_safety_mode', True)
# Declare a double (float) parameter
self.declare_parameter('sensor_offset_meters', 0.15)
# Declare a parameter with a descriptor for more details
# The descriptor can specify type, description, read-only status, etc.
max_acceleration_descriptor = ParameterDescriptor(
type=ParameterType.PARAMETER_DOUBLE,
description='Maximum acceleration in m/s^2. Must be positive.',
read_only=False
)
self.declare_parameter('max_acceleration_mps2', 0.5, max_acceleration_descriptor)
# Declare a parameter with constraints using a descriptor
min_temperature_descriptor = ParameterDescriptor(
type=ParameterType.PARAMETER_INTEGER,
description='Minimum operating temperature in Celsius.',
integer_range=[
rcl_interfaces.msg.IntegerRange(from_value=-20, to_value=50, step=1)
]
)
self.declare_parameter('min_operating_temperature_c', -5, min_temperature_descriptor)
self.get_logger().info('Parameters declared successfully.')
def main(args=None):
rclpy.init(args=args)
node = SimpleParamNode()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
In this example, we declare several parameters of different types: robot_name (string), max_speed_rpm (integer), enable_safety_mode (boolean), and sensor_offset_meters (double/float). We also demonstrate the use of ParameterDescriptor for max_acceleration_mps2 and min_operating_temperature_c. The descriptor allows you to provide a description, specify the parameter type explicitly (even if it can be inferred from the default value), and add constraints like integer_range. This makes your parameters more robust and self-documenting.
After saving the file, you need to add an entry point in your setup.py file within the param_tutorial package so that ros2 run can find your node. Open ~/ros2_ws/src/param_tutorial/setup.py and modify the entry_points section:
from setuptools import find_packages, setup
package_name = 'param_tutorial'
setup(
name=package_name,
version='0.0.0',
packages=find_packages(exclude=['test']),
data_files=[
('share/' + package_name, ['package.xml']),
('share/' + package_name + '/resource', ['param_tutorial']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='your_name',
maintainer_email='your_email@example.com',
description='TODO: Package description',
license='TODO: License declaration',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'simple_param_node = param_tutorial.simple_param_node:main',
],
},
)
Remember to rebuild your workspace after modifying setup.py:
cd ~/ros2_ws
colcon build
source install/setup.bash
Now you can run your node:
ros2 run param_tutorial simple_param_node
You should see the log messages indicating the node has started and parameters have been declared. This node will now be running and exposing its parameters to the ROS 2 system.
Retrieving Parameter Values
Once parameters are declared, a node often needs to retrieve their values to configure its internal state or behavior. ROS 2 provides straightforward methods for accessing these values, both during initialization and at runtime. This allows your node to start with sensible defaults and then dynamically adapt if those parameters are changed externally.
To retrieve a parameter’s value, you use the self.get_parameter() method, which returns a Parameter object. From this object, you can access the actual value using the .value attribute. For convenience, you can also directly get the value by chaining .get_parameter().value.
Let’s modify our SimpleParamNode to retrieve and log the declared parameter values. This will demonstrate how to access these settings immediately after declaration.
import rclpy
from rclpy.node import Node
from rcl_interfaces.msg import ParameterDescriptor, ParameterType, IntegerRange
class SimpleParamNode(Node):
def __init__(self):
super().__init__('simple_param_node')
self.get_logger().info('SimpleParamNode has been started.')
# Declare a simple string parameter
self.declare_parameter('robot_name', 'Clawbot')
# Declare an integer parameter with a default value
self.declare_parameter('max_speed_rpm', 100)
# Declare a boolean parameter for a toggle feature
self.declare_parameter('enable_safety_mode', True)
# Declare a double (float) parameter
self.declare_parameter('sensor_offset_meters', 0.15)
# Declare a parameter with a descriptor for more details
max_acceleration_descriptor = ParameterDescriptor(
type=ParameterType.PARAMETER_DOUBLE,
description='Maximum acceleration in m/s^2. Must be positive.',
read_only=False
)
self.declare_parameter('max_acceleration_mps2', 0.5, max_acceleration_descriptor)
# Declare a parameter with constraints using a descriptor
min_temperature_descriptor = ParameterDescriptor(
type=ParameterType.PARAMETER_INTEGER,
description='Minimum operating temperature in Celsius.',
integer_range=[
IntegerRange(from_value=-20, to_value=50, step=1)
]
)
self.declare_parameter('min_operating_temperature_c', -5, min_temperature_descriptor)
self.get_logger().info('Parameters declared successfully.')
# Now, retrieve and log the parameter values
robot_name = self.get_parameter('robot_name').value
max_speed = self.get_parameter('max_speed_rpm').value
safety_mode = self.get_parameter('enable_safety_mode').value
sensor_offset = self.get_parameter('sensor_offset_meters').value
max_accel = self.get_parameter('max_acceleration_mps2').value
min_temp = self.get_parameter('min_operating_temperature_c').value
self.get_logger().info(f'Current robot_name: {robot_name}')
self.get_logger().info(f'Current max_speed_rpm: {max_speed}')
self.get_logger().info(f'Current enable_safety_mode: {safety_mode}')
self.get_logger().info(f'Current sensor_offset_meters: {sensor_offset}')
self.get_logger().info(f'Current max_acceleration_mps2: {max_accel}')
self.get_logger().info(f'Current min_operating_temperature_c: {min_temp}')
def main(args=None):
rclpy.init(args=args)
node = SimpleParamNode()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
After updating the file and rebuilding (if you changed setup.py, otherwise just run), execute the node again:
ros2 run param_tutorial simple_param_node
You will now see the log messages displaying the default values of all declared parameters. This demonstrates the initial state of your node’s configuration based on the declared defaults. Retrieving parameters is a common operation, especially during node initialization, to ensure the node starts with the correct settings.
Updating Parameters at Runtime
One of the most powerful features of ROS 2 parameters is the ability to modify them at runtime without stopping and restarting the node. This dynamic adjustment is crucial for adaptive systems, allowing operators or other nodes to fine-tune behavior on the fly. Parameters can be updated programmatically from within another node, or manually using the ros2 param command-line tool.
Updating from the Command Line
The ros2 param set command is invaluable for testing and debugging. It allows you to change a parameter’s value on a running node directly from your terminal.
First, ensure your simple_param_node is running in one terminal:
ros2 run param_tutorial simple_param_node
In a second terminal, you can list the parameters exposed by the node:
ros2 param list /simple_param_node
This will output a list like:
/simple_param_node:
enable_safety_mode
max_acceleration_mps2
max_speed_rpm
min_operating_temperature_c
robot_name
sensor_offset_meters
use_sim_time
Now, let’s change a parameter. For example, to change max_speed_rpm:
ros2 param set /simple_param_node max_speed_rpm 150
You can verify the change:
ros2 param get /simple_param_node max_speed_rpm
The output will show:
Integer value is: 150
Notice that the simple_param_node itself doesn’t automatically log the change when it happens. To make the node react to parameter changes, we need to implement a parameter event handler.
Implementing a Parameter Event Callback
Nodes can register a callback function that is executed whenever one of their declared parameters changes. This is achieved using self.add_on_set_parameters_callback(). This callback receives a list of Parameter objects that have changed and should return a SetParametersResult message indicating whether the changes were successful.
Let’s enhance our SimpleParamNode to include a callback for parameter changes. This will allow the node to react to updates and log the new values.
import rclpy
from rclpy.node import Node
from rcl_interfaces.msg import ParameterDescriptor, ParameterType, IntegerRange, SetParametersResult
import rcl_interfaces.msg
class SimpleParamNode(Node):
def __init__(self):
super().__init__('simple_param_node')
self.get_logger().info('SimpleParamNode has been started.')
# Declare parameters
self.declare_parameter('robot_name', 'Clawbot')
self.declare_parameter('max_speed_rpm', 100)
self.declare_parameter('enable_safety_mode', True)
self.declare_parameter('sensor_offset_meters', 0.15)
max_acceleration_descriptor = ParameterDescriptor(
type=ParameterType.PARAMETER_DOUBLE,
description='Maximum acceleration in m/s^2. Must be positive.',
read_only=False
)
self.declare_parameter('max_acceleration_mps2', 0.5, max_acceleration_descriptor)
min_temperature_descriptor = ParameterDescriptor(
type=ParameterType.PARAMETER_INTEGER,
description='Minimum operating temperature in Celsius.',
integer_range=[
IntegerRange(from_value=-20, to_value=50, step=1)
]
)
self.declare_parameter('min_operating_temperature_c', -5, min_temperature_descriptor)
self.get_logger().info('Parameters declared successfully.')
# Register the parameter change callback
self.add_on_set_parameters_callback(self.parameter_callback)
# Log initial values
self.log_current_parameters()
def log_current_parameters(self):
robot_name = self.get_parameter('robot_name').value
max_speed = self.get_parameter('max_speed_rpm').value
safety_mode = self.get_parameter('enable_safety_mode').value
sensor_offset = self.get_parameter('sensor_offset_meters').value
max_accel = self.get_parameter('max_acceleration_mps2').value
min_temp = self.get_parameter('min_operating_temperature_c').value
self.get_logger().info(f'--- Current Parameter Values ---')
self.get_logger().info(f' robot_name: {robot_name}')
self.get_logger().info(f' max_speed_rpm: {max_speed}')
self.get_logger().info(f' enable_safety_mode: {safety_mode}')
self.get_logger().info(f' sensor_offset_meters: {sensor_offset}')
self.get_logger().info(f' max_acceleration_mps2: {max_accel}')
self.get_logger().info(f' min_operating_temperature_c: {min_temp}')
self.get_logger().info(f'--------------------------------')
def parameter_callback(self, parameters):
result = SetParametersResult()
result.successful = True
for param in parameters:
self.get_logger().info(f'Received parameter update for: {param.name} = {param.value}')
if param.name == 'max_speed_rpm':
if param.type_ == ParameterType.PARAMETER_INTEGER and param.value >= 0:
self.get_logger().info(f'Max speed updated to: {param.value}')
else:
self.get_logger().warn(f'Invalid type or value for max_speed_rpm: {param.value}. Must be a non-negative integer.')
result.successful = False
result.reason = "Invalid max_speed_rpm value"
elif param.name == 'max_acceleration_mps2':
if param.type_ == ParameterType.PARAMETER_DOUBLE and param.value > 0.0:
self.get_logger().info(f'Max acceleration updated to: {param.value}')
else:
self.get_logger().warn(f'Invalid type or value for max_acceleration_mps2: {param.value}. Must be a positive double.')
result.successful = False
result.reason = "Invalid max_acceleration_mps2 value"
elif param.name == 'min_operating_temperature_c':
# The descriptor handles the range validation automatically for integer_range
# We can still add custom logic if needed, but for simple range, ROS 2 handles it.
self.get_logger().info(f'Min operating temperature updated to: {param.value}')
# You can add more specific logic for other parameters here
# After processing all changes, log the new state
if result.successful:
self.log_current_parameters()
return result
def main(args=None):
rclpy.init(args=args)
node = SimpleParamNode()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Rebuild your workspace if you made changes to setup.py (otherwise just run), and then run the node:
ros2 run param_tutorial simple_param_node
Now, from a second terminal, try setting parameters again:
ros2 param set /simple_param_node max_speed_rpm 150
ros2 param set /simple_param_node enable_safety_mode false
ros2 param set /simple_param_node max_acceleration_mps2 0.75
ros2 param set /simple_param_node min_operating_temperature_c 10
You will observe that the simple_param_node now logs messages each time a parameter is updated, along with the new value. The parameter_callback function provides a hook for your node to react to these changes, allowing you to reconfigure internal states or trigger specific actions based on the new parameter values. It also demonstrates how to perform custom validation beyond what descriptors offer, returning successful = False if a parameter is set to an invalid value.
Updating Parameters Programmatically
Sometimes, one ROS 2 node might need to change parameters of another node (or even its own parameters). This is achieved using the set_parameters service client. This method is particularly useful for orchestrating complex behaviors where different parts of the system need to dynamically adjust each other’s configurations.
Let’s create a new node, param_setter_node, which will programmatically set parameters on our simple_param_node. Create ~/ros2_ws/src/param_tutorial/param_tutorial/param_setter_node.py:
import rclpy
from rclpy.node import Node
from rcl_interfaces.msg import Parameter, ParameterType
from rcl_interfaces.srv import SetParameters, SetParametersResult
from rclpy.parameter import Parameter as ROS2Parameter # Alias to avoid conflict with msg.Parameter
class ParamSetterNode(Node):
def __init__(self):
super().__init__('param_setter_node')
self.get_logger().info('ParamSetterNode has been started.')
# Create a client for the set_parameters service of the target node
self.set_parameters_client = self.create_client(SetParameters, '/simple_param_node/set_parameters')
# Wait for the service to be available
while not self.set_parameters_client.wait_for_service(timeout_sec=1.0):
self.get_logger().info('set_parameters service not available, waiting...')
self.get_logger().info('set_parameters service available.')
# Create a timer to periodically set parameters
self.timer = self.create_timer(5.0, self.set_parameters_periodically)
self.counter = 0
def set_parameters_periodically(self):
self.counter += 1
self.get_logger().info(f'Attempting to set parameters (Iteration {self.counter})...')
request = SetParameters.Request()
# Example 1: Change robot_name
new_robot_name = f'Clawbot_V{self.counter}'
request.parameters.append(ROS2Parameter('robot_name', ROS2Parameter.Type.STRING, new_robot_name).to_parameter_msg())
# Example 2: Toggle safety mode
new_safety_mode = self.counter % 2 == 0 # True, False, True, False...
request.parameters.append(ROS2Parameter('enable_safety_mode', ROS2Parameter.Type.BOOL, new_safety_mode).to_parameter_msg())
# Example 3: Change max_speed_rpm
new_max_speed = 100 + (self.counter * 10)
request.parameters.append(ROS2Parameter('max_speed_rpm', ROS2Parameter.Type.INTEGER, new_max_speed).to_parameter_msg())
# Example 4: Attempt to set an invalid value for max_acceleration_mps2 (should be caught by callback)
if self.counter % 3 == 0:
self.get_logger().warn('Attempting to set an invalid max_acceleration_mps2 (negative value).')
request.parameters.append(ROS2Parameter('max_acceleration_mps2', ROS2Parameter.Type.DOUBLE, -0.1).to_parameter_msg())
else:
request.parameters.append(ROS2Parameter('max_acceleration_mps2', ROS2Parameter.Type.DOUBLE, 0.5 + (self.counter * 0.1)).to_parameter_msg())
# Call the service asynchronously
self.future = self.set_parameters_client.call_async(request)
self.future.add_done_callback(self.set_parameters_response_callback)
def set_parameters_response_callback(self, future):
try:
response = future.result()
if response.results:
for result in response.results:
if result.successful:
self.get_logger().info(f'Parameter set successfully: {result.reason}')
else:
self.get_logger().error(f'Failed to set parameter: {result.reason}')
else:
self.get_logger().warn('Set parameters service returned no results.')
except Exception as e:
self.get_logger().error(f'Service call failed: {e}')
def main(args=None):
rclpy.init(args=args)
node = ParamSetterNode()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Now, update setup.py in your param_tutorial package to include the new param_setter_node:
from setuptools import find_packages, setup
package_name = 'param_tutorial'
setup(
name=package_name,
version='0.0.0',
packages=find_packages(exclude=['test']),
data_files=[
('share/' + package_name, ['package.xml']),
('share/' + package_name + '/resource', ['param_tutorial']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='your_name',
maintainer_email='your_email@example.com',
description='TODO: Package description',
license='TODO: License declaration',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'simple_param_node = param_tutorial.simple_param_node:main',
'param_setter_node = param_tutorial.param_setter_node:main',
],
},
)
Rebuild your workspace:
cd ~/ros2_ws
colcon build
source install/setup.bash
First, run simple_param_node in one terminal. Then, in a second terminal, run param_setter_node:
ros2 run param_tutorial param_setter_node
Observe the output in both terminals. The param_setter_node will periodically send requests to change parameters, and the simple_param_node will log these changes through its parameter_callback. This demonstrates a powerful way for different parts of your ROS 2 system to communicate and reconfigure each other dynamically. Notice how invalid values are rejected by the simple_param_node’s callback, preventing erroneous configurations.
Understanding Parameter Descriptors and Validation
Parameter descriptors are powerful tools that allow you to define metadata and constraints for your parameters beyond just their name and default value. They enhance the robustness and self-documentation of your ROS 2 nodes by ensuring that parameters are used correctly and within expected ranges. Descriptors can specify the parameter’s type, provide a human-readable description, mark it as read-only, and even define integer or floating-point ranges.
We briefly touched upon ParameterDescriptor when declaring max_acceleration_mps2 and min_operating_temperature_c. Let’s explore the capabilities of ParameterDescriptor more deeply.
The ParameterDescriptor message (from rcl_interfaces.msg) offers several fields:
type: Explicitly defines the parameter’s data type (e.g.,ParameterType.PARAMETER_INTEGER,ParameterType.PARAMETER_DOUBLE,ParameterType.PARAMETER_STRING,ParameterType.PARAMETER_BOOL). While ROS 2 can often infer the type from the default value, explicitly setting it improves clarity and can prevent unintended type conversions.description: A string providing a human-readable explanation of the parameter’s purpose. This is invaluable for documentation and for users interacting with your node via command-line tools or GUI.read_only: A boolean flag. IfTrue, the parameter cannot be changed after declaration. Attempts to set a read-only parameter will fail.dynamic_typing: A boolean flag. IfTrue, the parameter’s type can change at runtime. This is rarely used and can lead to complex behavior, so it’s generally best left asFalse.additional_constraints: A string for any additional constraints not covered by other fields. This is purely informational and not enforced by ROS 2 itself.integer_range: A list ofIntegerRangemessages. EachIntegerRangedefines a valid minimum (from_value), maximum (to_value), and step size (step) for integer parameters.floating_point_range: Similar tointeger_range, but for double/float parameters, usingFloatingPointRangemessages.
Example with Detailed Descriptors
Let’s modify our SimpleParamNode to use more detailed descriptors and observe their effects. We’ll add a read-only parameter and a parameter with a floating-point range.
import rclpy
from rclpy.node import Node
from rcl_interfaces.msg import ParameterDescriptor, ParameterType, IntegerRange, FloatingPointRange, SetParametersResult
import rcl_interfaces.msg
class SimpleParamNode(Node):
def __init__(self):
super().__init__('simple_param_node')
self.get_logger().info('SimpleParamNode has been started.')
# Declare a simple string parameter
self.declare_parameter('robot_name', 'Clawbot')
# Declare an integer parameter with a default value
self.declare_parameter('max_speed_rpm', 100)
# Declare a boolean parameter for a toggle feature
self.declare_parameter('enable_safety_mode', True)
# Declare a double (float) parameter
self.declare_parameter('sensor_offset_meters', 0.15)
# Declare a parameter with a descriptor for more details
max_acceleration_descriptor = ParameterDescriptor(
type=ParameterType.PARAMETER_DOUBLE,
description='Maximum acceleration in m/s^2. Must be positive.',
read_only=False,
floating_point_range=[ # Adding a floating point range
FloatingPointRange(from_value=0.01, to_value=2.0, step=0.0) # step 0.0 means arbitrary step
]
)
self.declare_parameter('max_acceleration_mps2', 0.5, max_acceleration_descriptor)
# Declare a parameter with integer range constraints
min_temperature_descriptor = ParameterDescriptor(
type=ParameterType.PARAMETER_INTEGER,
description='Minimum operating temperature in Celsius.',
integer_range=[
IntegerRange(from_value=-20, to_value=50, step=1)
]
)
self.declare_parameter('min_operating_temperature_c', -5, min_temperature_descriptor)
# Declare a READ-ONLY parameter
robot_serial_descriptor = ParameterDescriptor(
type=ParameterType.PARAMETER_STRING,
description='Unique serial number of the robot. Cannot be changed at runtime.',
read_only=True
)
self.declare_parameter('robot_serial_number', 'SN-CLAW-001', robot_serial_descriptor)
self.get_logger().info('Parameters declared successfully.')
# Register the parameter change callback
self.add_on_set_parameters_callback(self.parameter_callback)
# Log initial values
self.log_current_parameters()
def log_current_parameters(self):
# ... (same as before) ...
robot_name = self.get_parameter('robot_name').value
max_speed = self.get_parameter('max_speed_rpm').value
safety_mode = self.get_parameter('enable_safety_mode').value
sensor_offset = self.get_parameter('sensor_offset_meters').value
max_accel = self.get_parameter('max_acceleration_mps2').value
min_temp = self.get_parameter('min_operating_temperature_c').value
robot_serial = self.get_parameter('robot_serial_number').value
self.get_logger().info(f'--- Current Parameter Values ---')
self.get_logger().info(f' robot_name: {robot_name}')
self.get_logger().info(f' max_speed_rpm: {max_speed}')
self.get_logger().info(f' enable_safety_mode: {safety_mode}')
self.get_logger().info(f' sensor_offset_meters: {sensor_offset}')
self.get_logger().info(f' max_acceleration_mps2: {max_accel}')
self.get_logger().info(f' min_operating_temperature_c: {min_temp}')
self.get_logger().info(f' robot_serial_number (READ-ONLY): {robot_serial}')
self.get_logger().info(f'--------------------------------')
def parameter_callback(self, parameters):
result = SetParametersResult()
result.successful = True
for param in parameters:
self.get_logger().info(f'Received parameter update for: {param.name} = {param.value}')
if param.name == 'max_speed_rpm':
if param.type_ == ParameterType.PARAMETER_INTEGER and param.value >= 0:
self.get_logger().info(f'Max speed updated to: {param.value}')
else:
self.get_logger().warn(f'Invalid type or value for max_speed_rpm: {param.value}. Must be a non-negative integer.')
result.successful = False
result.reason = "Invalid max_speed_rpm value"
elif param.name == 'max_acceleration_mps2':
# The descriptor's floating_point_range handles validation here.
# If we tried to set it outside [0.01, 2.0], ROS 2 would reject it before this callback.
self.get_logger().info(f'Max acceleration updated to: {param.value}')
elif param.name == 'robot_serial_number':
self.get_logger().warn(f'Attempted to change read-only parameter robot_serial_number. This change will be rejected by ROS 2.')
# ROS 2 will automatically set result.successful to False for read-only parameters
# We can still log here, but the actual rejection happens before this callback returns.
else:
self.get_logger().info(f'Parameter {param.name} updated to: {param.value}')
# After processing all changes, log the new state
if result.successful:
self.log_current_parameters()
return result
def main(args=None):
rclpy.init(args=args)
node = SimpleParamNode()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
After updating and rebuilding, run simple_param_node. Then, try these commands in another terminal:
# This should succeed and be logged by the node
ros2 param set /simple_param_node max_acceleration_mps2 1.5
# This should fail due to the floating_point_range [0.01, 2.0]
ros2 param set /simple_param_node max_acceleration_mps2 5.0
# The response will likely indicate failure due to out of range.
# This should fail because robot_serial_number is read-only
ros2 param set /simple_param_node robot_serial_number "SN-CLAW-002"
# The response will indicate failure because it's read-only.
You’ll observe that attempts to set max_acceleration_mps2 outside its defined range or to change robot_serial_number will be rejected by the ROS 2 parameter system itself, often before your custom parameter_callback even processes them as successful. This automatic validation by descriptors simplifies your code and makes your nodes more robust.
Saving and Loading Parameters with YAML Files
For persistent configurations, ROS 2 allows you to save the current state of a node’s parameters to a YAML file and then load them when launching the node. This is incredibly useful for deploying robots with specific operating configurations, switching between different modes, or ensuring consistent behavior across multiple runs without manually setting parameters each time.
Saving Parameters
To save the parameters of a running node, use the ros2 param dump command. This command takes the node’s name as an argument and outputs its current parameters to standard output, which you can then redirect to a YAML file.
First, ensure your simple_param_node is running:
ros2 run param_tutorial simple_param_node
Now, in a separate terminal, set some parameters to non-default values:
ros2 param set /simple_param_node robot_name "AlphaClaw"
ros2 param set /simple_param_node max_speed_rpm 180
ros2 param set /simple_param_node enable_safety_mode false
Next, dump these parameters to a file:
ros2 param dump /simple_param_node > ~/ros2_ws/src/param_tutorial/config/clawbot_config.yaml
You might need to create the config directory first: mkdir -p ~/ros2_ws/src/param_tutorial/config.
Open the clawbot_config.yaml file. It will look something like this:
/**:
ros__parameters:
enable_safety_mode: false
max_acceleration_mps2: 0.5
max_speed_rpm: 180
min_operating_temperature_c: -5
robot_name: AlphaClaw
robot_serial_number: SN-CLAW-001
sensor_offset_meters: 0.15
use_sim_time: false
Notice that the robot_serial_number is included even though it’s read-only. ros2 param dump saves the current values of all parameters.
Loading Parameters with a Launch File
Loading parameters is typically done via a ROS 2 launch file. Launch files are Python scripts that allow you to start multiple nodes, set their parameters, remap topics, and generally orchestrate your ROS 2 system.
Create a launch file named simple_param_launch.py in ~/ros2_ws/src/param_tutorial/launch/:
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
# Get the path to the parameter file
param_file_path = os.path.join(
get_package_share_directory('param_tutorial'),
'config',
'clawbot_config.yaml'
)
return LaunchDescription([
Node(
package='param_tutorial',
executable='simple_param_node',
name='simple_param_node',
output='screen',
parameters=[param_file_path] # Load parameters from the YAML file
)
])
Now, stop your running simple_param_node (Ctrl+C). Then, run it using the launch file:
ros2 launch param_tutorial simple_param_launch.py
Observe the output of the simple_param_node. You will see that the initial parameter values (e.g., robot_name: AlphaClaw, max_speed_rpm: 180) are now loaded from the clawbot_config.yaml file, overriding the default values specified in the declare_parameter calls within the Python code. This demonstrates how you can effectively manage and deploy different configurations for your robots.
Loading Parameters Directly (Overrides)
You can also specify individual parameter overrides directly in the launch file, which take precedence over values in a loaded YAML file. This is useful for making small adjustments without creating a new YAML file.
Modify simple_param_launch.py:
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
# Get the path to the parameter file
param_file_path = os.path.join(
get_package_share_directory('param_tutorial'),
'config',
'clawbot_config.yaml'
)
return LaunchDescription([
Node(
package='param_tutorial',
executable='simple_param_node',
name='simple_param_node',
output='screen',
parameters=[
param_file_path, # Load from file first
{'max_speed_rpm': 200, # Then override individual parameters
'enable_safety_mode': True,
'new_parameter_from_launch': 'launch_value'} # Can also add new ones
]
)
])
In this revised launch file, max_speed_rpm will be 200, and enable_safety_mode will be True, even if clawbot_config.yaml specified different values. A new parameter new_parameter_from_launch is also added. Note that if new_parameter_from_launch is not declared in the node, it will still be available as a parameter, but the node won’t have a descriptor for it by default. It’s generally good practice to declare all parameters within the node for type safety and proper validation.
Launch the node again:
ros2 launch param_tutorial simple_param_launch.py
You will see the max_speed_rpm and enable_safety_mode reflect the values from the launch file overrides. This layered approach to parameter loading provides immense flexibility for managing robot configurations.
Best Practices for ROS 2 Parameters
Effective use of ROS 2 parameters goes beyond just knowing the syntax; it involves adopting best practices that lead to more maintainable, robust, and user-friendly robotic applications.
1. Declare All Parameters with Meaningful Defaults
Always declare every parameter your node intends to use within its __init__ method. Provide sensible default values. This ensures that your node can run out-of-the-box without requiring an external parameter file, making it easier to test and deploy. Meaningful defaults act as a form of self-documentation and provide a baseline for operation.
2. Use Parameter Descriptors for Validation and Documentation
Leverage ParameterDescriptor to its fullest.
- Specify
typeexplicitly: Even if the type can be inferred, explicit declaration enhances clarity and helps catch errors early. - Provide clear
description: These descriptions are visible to users viaros2 param describeand are crucial for understanding the parameter’s purpose. - Define
integer_rangeandfloating_point_range: This offloads basic validation to the ROS 2 system, preventing invalid values from being set and simplifying your node’sparameter_callback. - Use
read_onlyfor static configuration: For parameters that should never change after node startup (e.g., hardware serial numbers, fundamental operating modes), mark them asread_only.
3. Implement Parameter Callbacks for Dynamic Reconfiguration
For parameters that can change at runtime, always implement an add_on_set_parameters_callback. This callback is your node’s opportunity to react to changes, update internal state, and perform any necessary re-initialization. Within the callback, perform any custom validation that isn’t covered by descriptors and return a SetParametersResult indicating success or failure. This prevents your node from entering an invalid state due to bad parameter values.
4. Group Parameters Logically
If your node has many parameters, consider grouping them logically using prefixes. For example, controller.kp, controller.ki, controller.kd or sensor.camera.resolution, sensor.lidar.range. While ROS 2 doesn’t enforce strict hierarchical grouping in the same way some older systems did, using dot notation in parameter names is a common convention that improves readability and organization, especially when dumping or loading parameters.
5. Document Parameter Usage
Beyond the description in the descriptor, provide comprehensive documentation for your node’s parameters in your package’s README or a dedicated parameters file. Explain what each parameter does, its units, valid ranges, and how it affects the node’s behavior. This is vital for users and other developers integrating your node into their systems.
6. Use Launch Files for Deployment and Configuration Management
Always use ROS 2 launch files to start your nodes and load their parameters for deployment. This provides a centralized and reproducible way to configure your entire system. Store parameter YAML files in a config directory within your package and refer to them from your launch files using get_package_share_directory. This ensures portability and easy management of different configurations (e.g., robot_sim_config.yaml, robot_physical_config.yaml).
7. Avoid Over-parameterization
While parameters offer flexibility, avoid turning every single internal variable into a parameter. Parameters introduce overhead and can make a node’s configuration overly complex. Parameterize only those values that are genuinely expected to change between deployments, tasks, or during runtime tuning. Internal, static constants should remain as constants in your code.
By adhering to these best practices, you can create ROS 2 applications that are not only powerful and flexible but also easy to understand, configure, and maintain.
Comparing ROS 1 and ROS 2 Parameters
While both ROS 1 and ROS 2 provide mechanisms for dynamic configuration, there are significant differences in their design and implementation. Understanding these distinctions is crucial for developers transitioning between the two versions or for those who need to integrate systems from both.
Here’s a comparison table highlighting the key differences:
| Feature/Aspect | ROS 1 Parameters | ROS 2 Parameters |
|---|---|---|
| Underlying Mechanism | Parameter Server (Centralized) | Parameter Service (Decentralized, per-node) |
| Storage | Global key-value store, typically backed by YAML | Each node manages its own parameters, exposed via services |
| Access API | rospy.get_param(), rospy.set_param() | Node.declare_parameter(), Node.get_parameter(), Node.set_parameters() |
| Dynamic Updates | Requires dynamic_reconfigure package for callbacks | Built-in add_on_set_parameters_callback() method |
| Type Safety | Weakly typed, type inference at runtime | Strongly typed (specified during declaration or inferred), with ParameterDescriptor |
| Validation | Primarily through dynamic_reconfigure config files | Built-in via ParameterDescriptor (ranges, read-only) and custom callbacks |
| Descriptors/Metadata | Limited, mainly through dynamic_reconfigure | Rich ParameterDescriptor for description, type, constraints |
| Persistence | Load from YAML in launch files, rosparam dump/load | Load from YAML in launch files, ros2 param dump/load |
| Default Values | Often set in launch files or rospy.get_param(..., default_value) | Explicitly set in declare_parameter() with fallback |
| Overhead | Centralized server can be a bottleneck for many accessors | Distributed, per-node services reduce central bottlenecks |
| Read-Only Parameters | Not directly supported, relies on convention or dynamic_reconfigure | Explicit read_only flag in ParameterDescriptor |
| Namespaces | Hierarchical with / delimiters | Hierarchical with . delimiters within a node, / for node names |
Key Takeaways from the Comparison:
- Decentralization: The most significant shift is from ROS 1’s centralized Parameter Server to ROS 2’s decentralized parameter services. In ROS 2, each node is responsible for managing its own parameters, leading to improved scalability and fault tolerance. If one node crashes, it doesn’t bring down the entire parameter system.
- Stronger Typing and Validation: ROS 2 parameters are much more robust due to explicit type declarations and the powerful
ParameterDescriptor. This reduces common errors and makes configurations more reliable. ROS 1 relied heavily ondynamic_reconfigurefor these features, which was an add-on package. - Built-in Dynamic Reconfiguration: The
add_on_set_parameters_callbackin ROS 2 is a core feature, making it easier to build nodes that react to runtime configuration changes without needing an external package. - Simplified API: While the underlying mechanisms are more complex, the Python API for declaring, getting, and setting parameters in ROS 2 is arguably cleaner and more integrated into the
rclpy.node.Nodeclass.
For developers migrating from ROS 1, the transition requires understanding this paradigm shift from a global parameter store to node-specific parameter management. Embracing the ParameterDescriptor and callback mechanisms in ROS 2 will lead to more resilient and maintainable code.
Advanced Parameter Concepts and Future Directions
Beyond the core functionalities, ROS 2 parameters offer several advanced capabilities and continue to evolve. Exploring these aspects can further enhance your understanding and application of dynamic configuration.
Parameter Events (Notifications)
While add_on_set_parameters_callback allows a node to react to changes to its own parameters, what if one node needs to be notified when another node’s parameters change? This is possible through the ParameterEvent topic. When a node’s parameters are changed successfully, it publishes a ParameterEvent message on the /parameter_events topic. This message contains information about which parameters were set, unset, or changed.
A node can subscribe to this /parameter_events topic to monitor parameter changes across the entire system. This is particularly useful for system-level configuration managers or debugging tools that need a holistic view of parameter states.
# Example of subscribing to parameter events (not a full node)
import rclpy
from rclpy.node import Node
from rcl_interfaces.msg import ParameterEvent
class ParamEventMonitor(Node):
def __init__(self):
super().__init__('param_event_monitor')
self.param_event_sub = self.create_subscription(
ParameterEvent,
'/parameter_events',
self.param_event_callback,
10
)
self.get_logger().info('Parameter event monitor started.')
def param_event_callback(self, msg: ParameterEvent):
# Filter out events from self if needed, or process all
self.get_logger().info(f'Parameter event from node: {msg.node}')
for new_param in msg.new_parameters:
self.get_logger().info(f' New: {new_param.name} = {new_param.value.string_value or new_param.value.integer_value or new_param.value.double_value or new_param.value.bool_value}')
for changed_param in msg.changed_parameters:
self.get_logger().info(f' Changed: {changed_param.name} = {changed_param.value.string_value or changed_param.value.integer_value or changed_param.value.double_value or changed_param.value.bool_value}')
for deleted_param in msg.deleted_parameters:
self.get_logger().info(f' Deleted: {deleted_param.name}')
# To run this, you would integrate it into a main function similar to our other nodes.
Parameter Service for Getting All Parameters
While ros2 param list and ros2 param get are useful command-line tools, nodes can also programmatically query all parameters of another node using the get_parameters service. This service allows you to request the values of multiple parameters by name, or even all parameters if no specific names are provided.
# Example of getting multiple parameters from another node (not a full node)
import rclpy
from rclpy.node import Node
from rcl_interfaces.srv import GetParameters
class ParamQueryNode(Node):
def __init__(self):
super().__init__('param_query_node')
self.get_logger().info('ParamQueryNode started.')
self.get_params_client = self.create_client(GetParameters, '/simple_param_node/get_parameters')
while not self.get_params_client.wait_for_service(timeout_sec=1.0):
self.get_logger().info('get_parameters service not available, waiting...')
self.get_logger().info('get_parameters service available.')
self.request_parameters()
def request_parameters(self):
req = GetParameters.Request()
req.names = ['robot_name', 'max_speed_rpm', 'non_existent_param'] # Request specific parameters
# For all parameters, leave req.names empty
self.future = self.get_params_client.call_async(req)
self.future.add_done_callback(self.response_callback)
def response_callback(self, future):
try:
response = future.result()
for param_value in response.values:
self.get_logger().info(f'Queried parameter: {param_value.name} = {param_value.string_value or param_value.integer_value or param_value.double_value or param_value.bool_value}')
except Exception as e:
self.get_logger().error(f'Service call failed: {e}')
# To run this, you would integrate it into a main function.
Integration with ros2_control and Hardware Interfaces
In more complex robotic systems, especially those using ros2_control, parameters play a vital role in configuring hardware interfaces, controllers, and their gains. Controllers often expose parameters for PID gains (kp, ki, kd), setpoints, or operational limits. Understanding how to manage these parameters is essential for tuning robot performance and stability.
ros2_control often uses a hierarchical YAML structure for its configurations, which can then be loaded into the controller_manager and individual controllers as parameters. This is a common pattern for defining the entire control stack’s behavior from external configuration files.
Future Enhancements
The ROS 2 parameter system is under continuous development. Potential future enhancements might include:
- More advanced validation types: Beyond simple ranges, perhaps regular expressions for strings or more complex logical constraints.
- Parameter aliases: Allowing multiple names to refer to the same underlying parameter for backward compatibility or thematic grouping.
- Improved introspection tools: Even more powerful command-line and GUI tools for visualizing, comparing, and managing parameter sets across large systems.
By keeping an eye on these advanced concepts and the ongoing development of ROS 2, you can ensure your robotic applications remain at the forefront of robust and flexible design.
Conclusion
Mastering ROS 2 parameters with Python is an indispensable skill for any robotics developer aiming to build adaptable and maintainable systems. We’ve journeyed from the foundational concepts of parameter declaration and retrieval to the dynamic world of runtime updates, validation with descriptors, and persistent configurations using YAML files. We also compared the ROS 2 parameter system with its ROS 1 predecessor, highlighting the significant improvements in decentralization, type safety, and built-in dynamic reconfiguration.
The ability to externalize configuration values and modify them at runtime empowers you to create robots that can seamlessly transition between tasks, operate in diverse environments, and be debugged and tuned with unprecedented flexibility. By adhering to best practices—declaring with meaningful defaults, utilizing robust descriptors, implementing reactive callbacks, and managing configurations with launch files—you ensure your ROS 2 applications are not only functional but also scalable, understandable, and resilient.
As you continue your journey in ROS 2 development, remember that parameters are more than just variables; they are a critical interface for interacting with and controlling the intelligent behavior of your robotic systems. Embrace their power, and your robots will be more capable and easier to manage.