# 参数系统 (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。