16 KiB
参数系统 (Parameter) 全解
目标: 彻底理解 ROS2 参数系统的设计原理、API、配置方式、回调机制,能独立设计参数化节点。
阅读时间: 60-90 分钟
前置知识: 已完成
py_pubsub(理解节点 + Topic),已阅读10-concepts.md。
目录
- 1. 是什么 (What)
- 2. 为什么需要参数 (Why)
- 3. 设计原理 (Design)
- 4. API 全解 (API)
- 5. 实战代码 (Code)
- 6. 测试策略 (Test)
- 7. 进阶玩法 (Advanced)
- 8. 故障排查 (Pitfalls)
- 9. 推荐阅读 (Further)
1. 是什么 (What)
参数 (Parameter) 是 ROS2 节点的运行时配置项,它有以下特点:
| 特性 | 说明 |
|---|---|
| 类型 | 7 种基本类型 + 数组 + 字节数组 |
| 生命周期 | 节点启动时声明,运行时可改,节点关闭时销毁 |
| 作用范围 | 节点级(每个节点独立) / 全局(全局参数服务) |
| 持久化 | 可选,持久化参数在节点重启后恢复 |
| 原子性 | set_parameters_atomically 保证多参数同时生效 |
7 种基本类型
ParameterType.PARAMETER_BOOL # bool
ParameterType.PARAMETER_INTEGER # int
ParameterType.PARAMETER_DOUBLE # float
ParameterType.PARAMETER_STRING # str
ParameterType.PARAMETER_BYTE_ARRAY # bytes
ParameterType.PARAMETER_BOOL_ARRAY # List[bool]
ParameterType.PARAMETER_INTEGER_ARRAY # List[int]
ParameterType.PARAMETER_DOUBLE_ARRAY # List[float]
ParameterType.PARAMETER_STRING_ARRAY # List[str]
与 Topic / Service 的区别
| 维度 | Parameter | Topic | Service |
|---|---|---|---|
| 用途 | 配置 | 流式数据 | 请求-响应 |
| 频率 | 低(偶尔改) | 高(传感器 ~100Hz) | 单次 |
| 持久化 | 可选 | 否 | 否 |
| 回调 | on_set_parameters | on_message | on_request |
2. 为什么需要参数 (Why)
2.1 没有参数会怎样
假设你写了一个相机驱动节点,分辨率硬编码为 640×480。换相机后想用 1920×1080,只能改源码重编译。
2.2 有参数的好处
class CameraDriver(Node):
def __init__(self):
super().__init__('camera_driver')
self.declare_parameter('width', 640)
self.declare_parameter('height', 480)
self.declare_parameter('frame_rate', 30)
# ...
换相机 / 调分辨率,不用改代码,只要改 launch 文件 / YAML / CLI:
ros2 param set /camera_driver width 1920
2.3 ROS2 参数的设计目标
"Parameters are intended to be a way to configure nodes at startup or during runtime, without changing code."
关键点:
- 无代码修改 — 配置与代码解耦
- 支持运行时修改 — 不重启节点也能改
- 类型安全 — 7 种类型 + 校验
- 可回调 — 节点能响应参数变化
- 可序列化 — YAML / 命令行
3. 设计原理 (Design)
3.1 架构图
┌──────────────────────────────────────┐
│ Parameter Server │ ← 全局服务 /set_parameters, /list_parameters, /describe_parameters, /get_parameters
│ (每个进程内置,无独立进程) │
└──────────────────────────────────────┘
▲ ▲
│ set_parameters │ get_parameters
│ │
┌──────┴──────┐ ┌──────┴──────┐
│ Node A │ │ Node B │
│ params: │ │ params: │
│ - rate=10 │ │ - topic= │
│ - topic=X │ │ - format= │
└─────────────┘ └─────────────┘
注意:Parameter Server 不是独立进程,它运行在每个节点进程内的 rcl 层(具体是 rcl_params)。每个节点都有自己的参数副本,通过 DDS 同步。
3.2 关键概念
a) Declare vs Use
# 第一步:声明(必须)
self.declare_parameter('rate', 1.0)
# 第二步:使用
rate = self.get_parameter('rate').value
不声明就 get_parameter → 抛 ParameterNotDeclaredException。
b) On-Set Callback
self.add_on_set_parameters_callback(self._on_change)
回调签名:Callable[[List[Parameter]], SetParametersResult]。
返回 SetParametersResult(successful=True) → 接受;返回 (False, reason) → 拒绝。
注意: 回调是同步阻塞的,执行慢的回调会卡住参数设置。
c) Parameter Override
启动顺序(优先级从高到低):
1. CLI: --params-file /path/to/file.yaml
2. CLI: -p param_name:=value
3. Launch: Node(parameters=[yaml_file])
4. YAML 文件: config/params.yaml
5. 代码: declare_parameter('name', default_value)
最右的默认值优先级最低,CLI / Launch 覆盖它。
d) 持久化参数 (YAML I/O)
# 保存当前参数
ros2 param dump /node_name > saved.yaml
# 恢复
ros2 param load /node_name saved.yaml
格式:
/node_name:
ros__parameters:
rate: 5.0
topic: "/chatter"
4. API 全解 (API)
4.1 rclpy API
声明
declare_parameter(
name: str,
value: Any = None, # 推断类型
descriptor: str = '',
ignore_override: bool = False,
) -> Parameter
读取
get_parameter(name: str) -> Parameter
# or
get_parameters(names: List[str]) -> List[Parameter]
Parameter 对象的属性:
.name: 参数名.value: 参数值(类型推断).type: ParameterType 枚举.descriptor: 描述符
设置
set_parameters(parameters: List[Parameter]) -> List[SetParametersResult]
或原子性:
set_parameters_atomically(parameters: List[Parameter]) -> SetParametersResult
回调
add_on_set_parameters_callback(
callback: Callable[[List[Parameter]], SetParametersResult],
prepend: bool = False,
) -> None
remove_on_set_parameters_callback(callback) -> None
列出 / 描述
list_parameters() -> List[str]
describe_parameters(names: List[str]) -> List[ParameterDescriptor]
4.2 CLI 工具
# 列出某节点所有参数
ros2 param list /node_name
# 读参数
ros2 param get /node_name param_name
# 设参数
ros2 param set /node_name param_name value
# 导出参数
ros2 param dump /node_name
# 加载参数
ros2 param load /node_name saved.yaml
4.3 Launch 文件传参
方式 A: 直接传值
Node(
package='my_pkg',
executable='my_node',
parameters=[{
'rate': 10.0,
'topic': '/chatter',
}],
)
方式 B: 加载 YAML
from launch.substitutions import PathJoinSubstitution
from launch_ros.substitutions import FindPackageShare
config = PathJoinSubstitution([
FindPackageShare('my_pkg'),
'config',
'params.yaml',
])
Node(
package='my_pkg',
executable='my_node',
parameters=[config],
)
方式 C: CLI 覆盖
ros2 run my_pkg my_node --ros-args -p rate:=10.0 -p topic:=/new_topic
方式 D: 全局参数
from launch_ros.actions import PushRosNamespace
# 启动时把所有参数推到 /my_ns
PushRosNamespace('my_ns')
5. 实战代码 (Code)
完整示例见 src/py_params/,这里讲关键设计:
5.1 主节点: param_node.py
class ParamNode(Node):
def __init__(self):
super().__init__('param_node')
# 1. 声明三个参数(类型自动推断)
self.declare_parameter('publish_rate', 1.0)
self.declare_parameter('topic_name', 'params_chatter')
self.declare_parameter('message_prefix', 'Params:')
# 2. 读取参数
topic_name = self.get_parameter('topic_name').value
# 3. 用参数构造发布者
self._pub = self.create_publisher(String, topic_name, 10)
# 4. 用参数构造定时器
rate = self.get_parameter('publish_rate').value
self._timer = self.create_timer(1.0 / rate, self._cb)
# 5. 注册回调
self.add_on_set_parameters_callback(self._on_change)
5.2 回调: 拒绝非法值
def _on_change(self, params):
for p in params:
if p.name == 'publish_rate' and p.value <= 0.0:
return SetParametersResult(
successful=False,
reason='publish_rate 必须 > 0'
)
return SetParametersResult(successful=True)
5.3 YAML 配置: config/params.yaml
param_node:
ros__parameters:
publish_rate: 2.0
topic_name: "params_chatter"
message_prefix: "Configured:"
5.4 Launch 文件
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.substitutions import PathJoinSubstitution
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
cfg = PathJoinSubstitution([
FindPackageShare('py_params'),
'config',
'params.yaml',
])
return LaunchDescription([
Node(
package='py_params',
executable='param_node',
parameters=[cfg],
output='screen',
),
])
6. 测试策略 (Test)
6.1 单元测试: 声明默认值
def test_param_declaration():
rclpy.init()
node = ParamNode()
assert node.get_parameter('publish_rate').value == 1.0
6.2 单元测试: 合法 set
new_param = Parameter(
name='publish_rate',
value=ParameterValue(type=ParameterType.PARAMETER_DOUBLE, double_value=5.0),
)
result = node.set_parameters([new_param])
assert result[0].successful is True
6.3 单元测试: 非法值被拒绝
bad = Parameter(
name='publish_rate',
value=ParameterValue(type=ParameterType.PARAMETER_DOUBLE, double_value=-1.0),
)
result = node.set_parameters([bad])
assert result[0].successful is False
assert '必须 > 0' in result[0].reason
6.4 YAML 集成测试
def test_yaml_loadable():
with open('config/params.yaml') as f:
cfg = yaml.safe_load(f)
assert cfg['param_node']['ros__parameters']['publish_rate'] == 2.0
7. 进阶玩法 (Advanced)
7.1 动态重配置 (Dynamic Reconfigure)
ROS1 时代的 dynamic_reconfigure,在 ROS2 里被参数 + 回调取代。
例: 实时调 PID 参数:
def _on_change(self, params):
for p in params:
if p.name == 'kp':
self._pid.set_kp(p.value)
elif p.name == 'kd':
self._pid.set_kd(p.value)
return SetParametersResult(successful=True)
外部通过 ros2 param set 实时调:
ros2 param set /controller kp 0.5
7.2 参数回调链 (Callback Chain)
多个回调按顺序执行,任何一个返回 False 整链失败:
node.add_on_set_parameters_callback(cb_validate) # 校验
node.add_on_set_parameters_callback(cb_propagate) # 传给内部子系统
7.3 全局参数 (Global Parameter)
通过 PushRosNamespace 把所有参数推到命名空间:
from launch_ros.actions import PushRosNamespace
LaunchDescription([
PushRosNamespace('robot1'),
Node(package='cam', executable='driver'),
])
# 启动后参数命名:/robot1/cam/driver/...
7.4 参数文件覆盖
启动顺序优先级(从高到低):
1. --params-file (CLI)
2. -p name:=value (CLI)
3. Launch(parameters=...)
4. YAML 文件
5. declare_parameter 默认值
多个 YAML 文件,后面的覆盖前面的(数组传参)。
8. 故障排查 (Pitfalls)
8.1 ParameterNotDeclaredException
症状: get_parameter 抛异常。
原因: 没 declare_parameter 就 get_parameter。
解决:
# 错误
def __init__(self):
rate = self.get_parameter('rate').value # 💥
# 正确
def __init__(self):
self.declare_parameter('rate', 1.0) # 先声明
rate = self.get_parameter('rate').value # 再 get
8.2 回调未触发
症状: ros2 param set 后回调没反应。
原因: 多个回调,前一个返回 False 短路了。
解决: 检查每个回调的返回值,所有都要 successful=True。
8.3 YAML 没加载
症状: 启动后参数是默认值,不是 YAML 里的值。
原因: YAML 路径错 / 节点名不匹配。
解决:
# 检查 YAML 是否被安装到 share/
ros2 pkg prefix py_params
# 查看 install/py_params/share/py_params/config/params.yaml
# 启动时打印实际加载的参数
ros2 param list /param_node # 看实际值
8.4 浮点精度
症状: ros2 param set rate 0.1 后实际值是 0.10000000149...。
原因: IEEE 754 浮点表示。
解决: 容忍误差,或在回调里 round(p.value, 3)。
8.5 数组参数类型不匹配
症状: get_parameter 抛 ParameterTypeMismatchException。
原因: YAML 写 'topic'(字符串)但代码期望 topic_name 是字符串数组。
解决: 检查 YAML 缩进和 [] / 引号:
# 字符串数组
names: ["a", "b", "c"]
# 或
names: ['a', 'b', 'c']
8.6 Callback 阻塞导致 hang
症状: ros2 param set 卡死。
原因: 回调里有阻塞 I/O(网络 / 大文件)。
解决: 把阻塞操作放后台线程,回调只做校验。
8.7 Atomically vs 普通 set
症状: 多参数同时设置,部分生效部分失败。
解决: 用 set_parameters_atomically,要么全成功要么全失败。
result = node.set_parameters_atomically([
Parameter(name='kp', value=...),
Parameter(name='kd', value=...),
])
if not result.successful:
self.get_logger().error(f'set failed: {result.reason}')
9. 推荐阅读 (Further)
官方文档
- ROS2 Parameter Design — 设计稿,必读
- ROS2 Humble Parameter Tutorial
- ROS2 CLI: ros2 param
- rclpy: Parameter class
- rcl_interfaces.msg — Parameter / ParameterValue / SetParametersResult 消息定义
相关 RFC / 设计稿
进阶话题
- 与 ros2_control 集成: hardware_interface 启动时从 YAML 加载控制器参数
- 与 MoveIt2 集成: PlanningScene 用参数配置避障
- 与 Nav2 集成: 行为树参数、Costmap 参数全部走 ROS2 参数
实战例子
一句话总结
ROS2 参数 = 节点的"配置项",从 launch / YAML / CLI 传入,运行时可改,回调里能拒绝非法值。设计目标是"配置与代码解耦"。
下一节: doc/16-custom-interfaces.md 学习自定义 .msg / .srv / .action。