The Problem Nobody Talks About
You hardcoded a value. It works. You push to production. The customer’s hardware is slightly different. You hardcode a different value. Now you have 15 hardcoded configs for 15 different robots.
This is what parameters are supposed to prevent. But the ROS 2 parameter system is not intuitive. It’s powerful, but the documentation assumes you already understand it.
This guide is for the person who just spent 3 hours debugging why a parameter change didn’t take effect.
The Fundamental Truth About ROS 2 Parameters
# This is NOT how ROS 2 parameters work:
self.get_parameter('my_param') # It doesn't magically appear
# This IS how they work:
# 1. Parameter server (rclcpp) holds values
# 2. Your node requests them
# 3. They're set BEFORE node initialization (usually)
# 4. You can change them dynamically IF the node subscribes to updatesReality check: Parameters are stored in the parameter server, not in your node. Your node reads them. If you don’t listen for changes, modifying a parameter does nothing.
Part 1: Static Parameters (The Simple Case)
1.1 Declaring Parameters in Your Node
import rclpy
from rclpy.node import Node
class MyRobot(Node):
def __init__(self):
super().__init__('my_robot')
# Declare a parameter with default value
self.declare_parameter('wheel_radius', 0.05)
# Get the value
wheel_radius = self.get_parameter('wheel_radius').value
self.get_logger().info(f'Wheel radius: {wheel_radius}')
def main():
rclpy.init()
node = MyRobot()
rclpy.spin(node)
if __name__ == '__main__':
main()Key insight: declare_parameter() registers the parameter. get_parameter() reads it.
1.2 Setting Parameters from Launch File
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
my_node = Node(
package='my_pkg',
executable='my_node',
name='robot',
parameters=[
{
'wheel_radius': 0.033,
'wheel_separation': 0.16,
'max_velocity': 1.5,
}
]
)
return LaunchDescription([my_node])1.3 Setting Parameters from YAML
# config/robot_params.yaml
my_robot:
ros__parameters:
wheel_radius: 0.033
wheel_separation: 0.16
max_velocity: 1.5
pid_gains:
p: 1.0
i: 0.1
d: 0.05Important: The node name (my_robot) must match the namespace in your YAML, or parameters won’t be found.
# In launch file
from launch.substitutions import FindPackageShare, PathJoinSubstitution
pkg_share = FindPackageShare('my_robot_bringup')
params_file = PathJoinSubstitution([pkg_share, 'config', 'robot_params.yaml'])
node = Node(
package='my_pkg',
executable='my_node',
name='my_robot', # This must match YAML namespace!
parameters=[params_file]
)Part 2: Dynamic Parameters (The Tricky Part)
Problem: You change a parameter with ros2 param set /my_robot wheel_radius 0.04 and nothing happens.
Why: Your node isn’t listening for changes.
Solution: Use add_on_set_parameters_callback().
2.1 Listening for Parameter Changes
import rclpy
from rclpy.node import Node
from rcl_interfaces.msg import SetParametersResult
class DynamicRobot(Node):
def __init__(self):
super().__init__('robot')
# Declare parameters
self.declare_parameter('wheel_radius', 0.05)
self.declare_parameter('max_velocity', 1.5)
# Register callback for parameter changes
self.add_on_set_parameters_callback(self.parameters_callback)
# Store current values
self.wheel_radius = self.get_parameter('wheel_radius').value
self.max_velocity = self.get_parameter('max_velocity').value
def parameters_callback(self, params):
"""Called whenever ANY parameter changes"""
for param in params:
if param.name == 'wheel_radius':
self.wheel_radius = param.value
self.get_logger().info(f'Updated wheel_radius to {param.value}')
elif param.name == 'max_velocity':
self.max_velocity = param.value
self.get_logger().info(f'Updated max_velocity to {param.value}')
# Return SetParametersResult(successful=True) to accept
# Return SetParametersResult(successful=False) to reject
return SetParametersResult(successful=True)Now you can dynamically reconfigure:
# Change a parameter at runtime
ros2 param set /robot wheel_radius 0.04
# Your callback fires and the node updates
# Output: "Updated wheel_radius to 0.04"2.2 Validating Parameters Before Accepting
def parameters_callback(self, params):
"""Validate parameters before accepting"""
for param in params:
if param.name == 'wheel_radius':
if param.value <= 0:
self.get_logger().warn(f'Wheel radius must be positive!')
return SetParametersResult(successful=False) # Reject!
if param.value > 0.5:
self.get_logger().warn(f'Wheel radius suspiciously large!')
return SetParametersResult(successful=False)
self.wheel_radius = param.value
return SetParametersResult(successful=True)Now invalid parameters are rejected:
ros2 param set /robot wheel_radius -0.5
# Error: Parameter change rejectedPart 3: The Gotchas That Will Break Your Code
Gotcha 1: Parameter Type Mismatches
# YAML file says:
# max_velocity: "1.5" # Oops, it's a STRING
# Your code does:
velocity = self.get_parameter('max_velocity').value # string "1.5"
accel = velocity * 2 # ❌ TypeError: can't multiply str by intSolution: Always check the type or convert explicitly.
param = self.get_parameter('max_velocity')
velocity = float(param.value) # Convert to float
accel = velocity * 2 # ✅ WorksGotcha 2: Declaring Parameters Multiple Times
def __init__(self):
self.declare_parameter('param', 1.0)
self.declare_parameter('param', 2.0) # ❌ Error! Already declaredSolution: Check if already declared.
if not self.has_parameter('param'):
self.declare_parameter('param', 1.0)Gotcha 3: Namespace Hell
# config/params.yaml
my_namespace:
ros__parameters:
param1: 100# Launch file
node = Node(
package='pkg',
executable='exe',
name='my_node',
namespace='my_namespace',
parameters=[params_file] # ✅ This works
)
# But if you forget namespace in launch:
node = Node(
package='pkg',
executable='exe',
name='my_node',
parameters=[params_file] # ❌ Parameters not found!
)Pro tip: Make sure launch file namespace matches YAML namespace.
Gotcha 4: Parameters Aren’t Inherited
# Node 1 in namespace /robot
self.declare_parameter('shared_param', 10)
# Node 2 in namespace /robot (different process)
value = self.get_parameter('shared_param').value # ❌ Parameter doesn't existWhy: Each node has its own parameter space. There’s no global inheritance.
Solution: Set parameters globally in launch file, or use ROS services for inter-node communication.
Part 4: Advanced Patterns
4.1 Parameter Namespacing (the right way)
# config/params.yaml
motor_controller:
ros__parameters:
max_velocity: 1.5
acceleration: 2.0
pid:
kp: 1.0
ki: 0.1
kd: 0.05
lidar_driver:
ros__parameters:
frame_id: "laser"
range_max: 30.0class MotorController(Node):
def __init__(self):
super().__init__('motor_controller')
# These are automatically namespaced under 'motor_controller'
self.max_velocity = self.get_parameter('max_velocity').value
# Nested parameters
pid_gains = {
'kp': self.get_parameter('pid.kp').value,
'ki': self.get_parameter('pid.ki').value,
'kd': self.get_parameter('pid.kd').value,
}4.2 Reading All Parameters at Once
def __init__(self):
super().__init__('my_node')
# Get all parameters in this node's namespace
all_params = self.list_parameters(
prefixes=[''] # Empty prefix = all
).names
self.get_logger().info(f'Available parameters: {all_params}')4.3 Parameter Descriptor (Advanced Type Info)
from rcl_interfaces.msg import ParameterDescriptor, IntegerRange
descriptor = ParameterDescriptor(
description='Maximum velocity of the robot',
integer_range=[IntegerRange(
from_value=0,
to_value=5,
step=1
)]
)
self.declare_parameter(
'max_velocity',
2, # default
descriptor=descriptor
)This helps ROS tools validate parameters automatically.
Part 5: Real-World Pattern - Configuration with Validation
import rclpy
from rclpy.node import Node
from rcl_interfaces.msg import SetParametersResult
from dataclasses import dataclass
@dataclass
class RobotConfig:
"""Strongly-typed configuration"""
wheel_radius: float
wheel_separation: float
max_velocity: float
def validate(self) -> bool:
"""Validate before applying"""
if self.wheel_radius <= 0:
return False
if self.wheel_separation <= 0:
return False
if self.max_velocity <= 0:
return False
return True
class ConfigurableRobot(Node):
def __init__(self):
super().__init__('robot')
# Declare all parameters
self.declare_parameter('wheel_radius', 0.05)
self.declare_parameter('wheel_separation', 0.16)
self.declare_parameter('max_velocity', 1.5)
# Load initial config
self.config = self._load_config()
# Listen for changes
self.add_on_set_parameters_callback(self.on_parameters_changed)
def _load_config(self) -> RobotConfig:
"""Load configuration from parameters"""
return RobotConfig(
wheel_radius=self.get_parameter('wheel_radius').value,
wheel_separation=self.get_parameter('wheel_separation').value,
max_velocity=self.get_parameter('max_velocity').value,
)
def on_parameters_changed(self, params):
"""Update configuration when parameters change"""
# Reload config
new_config = self._load_config()
# Validate
if not new_config.validate():
self.get_logger().error('Invalid configuration!')
return SetParametersResult(successful=False)
# Apply
self.config = new_config
self.get_logger().info(f'Configuration updated: {self.config}')
return SetParametersResult(successful=True)Debugging Parameters
# List all parameters on a node
ros2 param list /robot
# Get a parameter value
ros2 param get /robot wheel_radius
# Set a parameter
ros2 param set /robot wheel_radius 0.04
# Dump all parameters to a file
ros2 param dump /robot > /tmp/robot_params.yaml
# Load parameters from a file
ros2 param load /robot /tmp/robot_params.yamlKey Takeaways
- Static parameters are set at startup via launch files or YAML
- Dynamic parameters require
add_on_set_parameters_callback() - Namespace carefully - YAML namespace must match launch file namespace
- Validate always - Users will pass invalid values
- Use typed parameters - Don’t assume string == number
- Test parameter changes - Many bugs hide in the parameter system
Quick Reference
# Declare
self.declare_parameter('param_name', default_value)
# Read
value = self.get_parameter('param_name').value
# Listen for changes
self.add_on_set_parameters_callback(self.on_params_changed)
# In callback
def on_params_changed(self, params):
for param in params:
if param.name == 'target_param':
# Do something
pass
return SetParametersResult(successful=True)
# Check if exists
self.has_parameter('param_name')
# Get parameter type
param_type = self.get_parameter('param_name').type_Next time your parameter change doesn’t work, remember: it’s probably because your node isn’t listening for changes.