Files
ROS2_learn/doc/15-params.md
T
2026-08-05 18:17:25 +08:00

17 KiB
Raw Blame History

参数系统 (Parameter) 全解

目标: 彻底理解 ROS2 参数系统的设计原理、API、配置方式、回调机制,能独立设计参数化节点。

阅读时间: 60-90 分钟

前置知识: 已完成 py_pubsub(理解节点 + Topic),已阅读 10-concepts.md


目录


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 参数的设计目标

引用 ROS2 Design: Parameter:

"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

# 第一步:声明(必须)
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 主节点: py_params/param_node.py

class ParamsTalker(Node):
    DEFAULT_RATE_HZ = 1.0
    DEFAULT_TOPIC = 'params_chatter'
    DEFAULT_PREFIX = 'Params:'

    def __init__(self, *, node_name='params_talker'):
        super().__init__(node_name)

        # 1. 声明三个参数(类型自动推断)
        self.declare_parameter('publish_rate_hz', self.DEFAULT_RATE_HZ)
        self.declare_parameter('topic_name', self.DEFAULT_TOPIC)
        self.declare_parameter('message_prefix', self.DEFAULT_PREFIX)

        # 2. 读取参数 + 构造组件
        publish_rate_hz = self.get_parameter('publish_rate_hz').value
        topic_name = self.get_parameter('topic_name').value

        self.publisher_ = self.create_publisher(String, topic_name, 10)

        period = 1.0 / publish_rate_hz if publish_rate_hz > 0 else 1.0
        self.timer_ = self.create_timer(period, self._on_timer)

        # 3. 缓存可变参数 + 注册回调
        self._prefix = self.get_parameter('message_prefix').value
        self.add_on_set_parameters_callback(self._validate_parameter_change)

5.2 回调: 拒绝非法值

def _validate_parameter_change(self, params):
    for param in params:
        if param.name == 'publish_rate_hz':
            if not isinstance(param.value, (int, float)):
                return SetParametersResult(successful=False,
                    reason=f'publish_rate_hz 必须是数字')
            if param.value <= 0.0:
                return SetParametersResult(successful=False,
                    reason=f'publish_rate_hz 必须 > 0')
        elif param.name == 'message_prefix':
            if not isinstance(param.value, str):
                return SetParametersResult(successful=False,
                    reason='message_prefix 必须是字符串')
            self._prefix = param.value
    return SetParametersResult(successful=True)

5.3 YAML 配置: config/params.yaml

params_talker:
  ros__parameters:
    publish_rate_hz: 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='params_talker',
            parameters=[cfg],
            output='screen',
        ),
    ])

注:本仓库 launch 文件名是 params_launch.py,命令:ros2 launch py_params params_launch.py


6. 测试策略 (Test)

6.1 单元测试: 声明默认值

def test_param_declaration():
    rclpy.init()
    node = ParamsTalker()
    assert node.get_parameter('publish_rate_hz').value == 1.0
    assert node.get_parameter('topic_name').value == 'params_chatter'
    assert node.get_parameter('message_prefix').value == 'Params:'

6.2 单元测试: 合法 set

new_param = Parameter(
    name='publish_rate_hz',
    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_hz',
    value=ParameterValue(type=ParameterType.PARAMETER_DOUBLE, double_value=-1.0),
)
result = node.set_parameters([bad])
assert result[0].successful is False
assert 'publish_rate_hz 必须 > 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['params_talker']['ros__parameters']['publish_rate_hz'] == 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_parameterget_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 /params_talker  # 看实际值

8.4 浮点精度

症状: ros2 param set rate 0.1 后实际值是 0.10000000149...

原因: IEEE 754 浮点表示。

解决: 容忍误差,或在回调里 round(p.value, 3)

8.5 数组参数类型不匹配

症状: get_parameterParameterTypeMismatchException

原因: 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)

官方文档

相关 RFC / 设计稿

进阶话题

  • 与 ros2_control 集成: hardware_interface 启动时从 YAML 加载控制器参数
  • 与 MoveIt2 集成: PlanningScene 用参数配置避障
  • 与 Nav2 集成: 行为树参数、Costmap 参数全部走 ROS2 参数

实战例子


一句话总结

ROS2 参数 = 节点的"配置项",从 launch / YAML / CLI 传入,运行时可改,回调里能拒绝非法值。设计目标是"配置与代码解耦"。

下一节: doc/16-custom-interfaces.md 学习自定义 .msg / .srv / .action。



📖 阅读路径导航

💡 这是仓库 doc/ 下所有文档的推荐阅读顺序。返回 README 总导航

本文预计阅读时间: 60 分钟 📍 当前位置: 第 6 / 24 篇