Files
ROS2_learn/doc/15-params.md
T

643 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 参数系统 (Parameter) 全解
> **目标**: 彻底理解 ROS2 参数系统的设计原理、API、配置方式、回调机制,能独立设计参数化节点。
>
> **阅读时间**: 60-90 分钟
>
> **前置知识**: 已完成 `py_pubsub`(理解节点 + Topic),已阅读 `10-concepts.md`。
---
## 目录
- [1. 是什么 (What)](#1-是什么-what)
- [2. 为什么需要参数 (Why)](#2-为什么需要参数-why)
- [3. 设计原理 (Design)](#3-设计原理-design)
- [4. API 全解 (API)](#4-api-全解-api)
- [5. 实战代码 (Code)](#5-实战代码-code)
- [6. 测试策略 (Test)](#6-测试策略-test)
- [7. 进阶玩法 (Advanced)](#7-进阶玩法-advanced)
- [8. 故障排查 (Pitfalls)](#8-故障排查-pitfalls)
- [9. 推荐阅读 (Further)](#9-推荐阅读-further)
---
## 1. 是什么 (What)
**参数 (Parameter)** 是 ROS2 节点的**运行时配置项**,它有以下特点:
| 特性 | 说明 |
|---|---|
| **类型** | 7 种基本类型 + 数组 + 字节数组 |
| **生命周期** | 节点启动时声明,运行时可改,节点关闭时销毁 |
| **作用范围** | 节点级(每个节点独立) / 全局(全局参数服务) |
| **持久化** | 可选,持久化参数在节点重启后恢复 |
| **原子性** | `set_parameters_atomically` 保证多参数同时生效 |
### 7 种基本类型
```python
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 有参数的好处
```python
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:
```bash
ros2 param set /camera_driver width 1920
```
### 2.3 ROS2 参数的设计目标
引用 [ROS2 Design: Parameter](https://design.ros2.org/articles/ros_parameters.html):
> "Parameters are intended to be a way to configure nodes at startup or during runtime, without changing code."
关键点:
1. **无代码修改** — 配置与代码解耦
2. **支持运行时修改** — 不重启节点也能改
3. **类型安全** — 7 种类型 + 校验
4. **可回调** — 节点能响应参数变化
5. **可序列化** — 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
```python
# 第一步:声明(必须)
self.declare_parameter('rate', 1.0)
# 第二步:使用
rate = self.get_parameter('rate').value
```
不声明就 `get_parameter` → 抛 `ParameterNotDeclaredException`
#### b) On-Set Callback
```python
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)
```bash
# 保存当前参数
ros2 param dump /node_name > saved.yaml
# 恢复
ros2 param load /node_name saved.yaml
```
格式:
```yaml
/node_name:
ros__parameters:
rate: 5.0
topic: "/chatter"
```
---
## 4. API 全解 (API)
### 4.1 rclpy API
#### 声明
```python
declare_parameter(
name: str,
value: Any = None, # 推断类型
descriptor: str = '',
ignore_override: bool = False,
) -> Parameter
```
#### 读取
```python
get_parameter(name: str) -> Parameter
# or
get_parameters(names: List[str]) -> List[Parameter]
```
`Parameter` 对象的属性:
- `.name`: 参数名
- `.value`: 参数值(类型推断)
- `.type`: ParameterType 枚举
- `.descriptor`: 描述符
#### 设置
```python
set_parameters(parameters: List[Parameter]) -> List[SetParametersResult]
```
或原子性:
```python
set_parameters_atomically(parameters: List[Parameter]) -> SetParametersResult
```
#### 回调
```python
add_on_set_parameters_callback(
callback: Callable[[List[Parameter]], SetParametersResult],
prepend: bool = False,
) -> None
remove_on_set_parameters_callback(callback) -> None
```
#### 列出 / 描述
```python
list_parameters() -> List[str]
describe_parameters(names: List[str]) -> List[ParameterDescriptor]
```
### 4.2 CLI 工具
```bash
# 列出某节点所有参数
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: 直接传值
```python
Node(
package='my_pkg',
executable='my_node',
parameters=[{
'rate': 10.0,
'topic': '/chatter',
}],
)
```
#### 方式 B: 加载 YAML
```python
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 覆盖
```bash
ros2 run my_pkg my_node --ros-args -p rate:=10.0 -p topic:=/new_topic
```
#### 方式 D: 全局参数
```python
from launch_ros.actions import PushRosNamespace
# 启动时把所有参数推到 /my_ns
PushRosNamespace('my_ns')
```
---
## 5. 实战代码 (Code)
完整示例见 `src/py_params/`,这里讲关键设计:
### 5.1 主节点: `param_node.py`
```python
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 回调: 拒绝非法值
```python
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`
```yaml
param_node:
ros__parameters:
publish_rate: 2.0
topic_name: "params_chatter"
message_prefix: "Configured:"
```
### 5.4 Launch 文件
```python
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 单元测试: 声明默认值
```python
def test_param_declaration():
rclpy.init()
node = ParamNode()
assert node.get_parameter('publish_rate').value == 1.0
```
### 6.2 单元测试: 合法 set
```python
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 单元测试: 非法值被拒绝
```python
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 集成测试
```python
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 参数:
```python
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` 实时调:
```bash
ros2 param set /controller kp 0.5
```
### 7.2 参数回调链 (Callback Chain)
多个回调按顺序执行,任何一个返回 False 整链失败:
```python
node.add_on_set_parameters_callback(cb_validate) # 校验
node.add_on_set_parameters_callback(cb_propagate) # 传给内部子系统
```
### 7.3 全局参数 (Global Parameter)
通过 `PushRosNamespace` 把所有参数推到命名空间:
```python
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`
**解决**:
```python
# 错误
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 路径错 / 节点名不匹配。
**解决**:
```bash
# 检查 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 缩进和 `[]` / 引号:
```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`,要么全成功要么全失败。
```python
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](https://design.ros2.org/articles/ros_parameters.html) — 设计稿,必读
- [ROS2 Humble Parameter Tutorial](https://docs.ros.org/en/humble/Tutorials/Beginner-CLI-Tools/Understanding-ROS2-Parameters/Understanding-ROS2-Parameters.html)
- [ROS2 CLI: ros2 param](https://docs.ros.org/en/humble/Tutorials/Beginner-CLI-Tools/Using-ROS2-CLI-Tools.html)
- [rclpy: Parameter class](https://docs.ros2.org/en/latest/rclpy_api/rclpy.parameter.html)
- [rcl_interfaces.msg](https://github.com/ros2/rcl_interfaces) — Parameter / ParameterValue / SetParametersResult 消息定义
### 相关 RFC / 设计稿
- [ROS2 Design: Parameter Validation](https://design.ros2.org/articles/ros_parameters.html#parameter-validation)
- [ROS2 Design: On-Set Parameters Callback](https://design.ros2.org/articles/ros_parameters.html#on-set-parameters-callback)
### 进阶话题
- **与 ros2_control 集成**: hardware_interface 启动时从 YAML 加载控制器参数
- **与 MoveIt2 集成**: PlanningScene 用参数配置避障
- **与 Nav2 集成**: 行为树参数、Costmap 参数全部走 ROS2 参数
### 实战例子
- [ros2/demos: topic_monitor](https://github.com/ros2/demos/blob/humble/demo_nodes_py/demo_nodes_py/topics/topic_monitor.py)
- [turtlebot3: 参数化](https://github.com/ROBOTIS-GIT/turtlebot3/blob/humble/turtlebot3_node/src/turtlebot3_node.cpp)
---
## 一句话总结
> **ROS2 参数 = 节点的"配置项",从 launch / YAML / CLI 传入,运行时可改,回调里能拒绝非法值。设计目标是"配置与代码解耦"。**
下一节: `doc/16-custom-interfaces.md` 学习自定义 .msg / .srv / .action。
---
---
## 📖 阅读路径导航
> 💡 这是仓库 `doc/` 下所有文档的推荐阅读顺序。[返回 README 总导航](../README.md#-23-篇文档怎么读)
>
> ⏱ **本文预计阅读时间**: 60 分钟
> 📍 **当前位置**: 第 6 / 24 篇
-**上一篇**: [Node / Topic / Service / Action / TF / Time](10-concepts.md)
-**下一篇**: [自定义 .msg/.srv/.action](16-custom-interfaces.md)