ROS 2 参数(Parameters)— Python 教程
ROS 2 参数(Parameters)— Python 教程
ROS 2 中的 参数(Parameters) 是节点级的配置项,用于在不修改代码的情况下调整节点的行为,例如速度、频率、阈值、颜色等。本文带你理解参数的原理,并用 Python 从零写一个可声明、可读取、可动态修改参数的节点。
一、什么是 ROS 参数?
1.1 基本概念
- 参数(Parameter):附着在某个节点上的一个 键值对(key-value),例如节点
turtle上有参数background_r = 255。 - 每个参数都属于某个节点,通过
/节点名/参数名来定位,例如/turtle/background_r。 - 参数由节点自己管理,可以设置默认值;运行中可以通过命令行、launch 文件或其他节点动态修改。
flowchart LR
A[ros2 param set<br/>命令行] -->|"设置 /param_node/my_int"| B[节点 param_node]
C[launch 文件<br/>parameters=...] -->|"启动时注入"| B
D[其他节点<br/>set_parameters] -->|"运行时修改"| B
B -->|"get_parameter<br/>读取"| E[节点逻辑<br/>使用参数值]
1.2 参数 vs 话题(Topic)
| 对比项 | 参数(Parameter) | 话题(Topic) |
|---|---|---|
| 归属 | 属于某个节点 | 不属于任何节点,全局广播 |
| 内容 | 单个键值对(配置项) | 结构化的消息流 |
| 方向 | 无方向,可读可写 | 单向:发布者 → 订阅者 |
| 用途 | 配置 / 调参 | 数据传输 / 通信 |
| 数据量 | 小,偶发改变 | 可高频、大量 |
| 修改方式 | 一次性设置即可 | 持续发布 |
打个比方:参数像汽车仪表盘上的旋钮(转速、亮度、音量……),调一次管用;话题像电台广播(数据流),一直在播。调参用参数,通信用话题,二者是 ROS 2 中相辅相成的两种机制。
1.3 参数的数据类型
| 类型 | 说明 | 示例 |
|---|---|---|
integer |
整数 | 42 |
double |
浮点数 | 3.14 |
string |
字符串 | "hello" |
bool |
布尔值 | true |
integer_array |
整型数组 | [1, 2, 3] |
double_array |
浮点数组 | [1.5, 2.5] |
string_array |
字符串数组 | ["a", "b"] |
bool_array |
布尔数组 | [true, false] |
还有
byte[](字节数组)等,但上面这 8 种是最常用的。一个参数在任意时刻只能取一种类型。
二、准备工作
本文基于 ROS 2 Jazzy + Python 3,假设你的环境已经配置好:
# 检查 ROS 2 是否可用
printenv ROS_DISTRO # 应输出 jazzy
# 每次打开终端都要 source 环境(也可写入 ~/.bashrc)
source /opt/ros/jazzy/setup.bash
source ~/ros_ws/install/setup.bash
三、最小代码样例
下面是一个使用参数的完整节点:它声明了 6 个参数,每 0.5 秒读取并打印一次。
3.1 节点 param_node.py
import rclpy
from rclpy.node import Node
class ParamNode(Node):
"""演示 ROS 2 参数的节点:声明参数 → 定时读取并打印。"""
def __init__(self):
super().__init__('param_node')
# ---- 1. 声明参数(名字 + 默认值)----
# 声明之后才能被 ros2 param list 看到、被命令行/launch 修改
self.declare_parameter('my_str', 'world') # 字符串参数,默认 'world'
self.declare_parameter('my_int', 42) # 整数参数,默认 42
self.declare_parameter('my_double', 3.14) # 浮点参数,默认 3.14
self.declare_parameter('my_bool', True) # 布尔参数,默认 True
self.declare_parameter('my_array', [1, 2, 3]) # 整型数组参数,默认 [1,2,3]
self.declare_parameter('my_enum', 'A') # 模拟枚举:只允许 A/B/C
# ---- 2. 定时器:每 0.5 秒读取并打印一次参数 ----
self.timer = self.create_timer(0.5, self.timer_callback)
def timer_callback(self):
# get_parameter() 返回 Parameter 对象,用 .value 取实际值
s = self.get_parameter('my_str').value
i = self.get_parameter('my_int').value
d = self.get_parameter('my_double').value
b = self.get_parameter('my_bool').value
arr = self.get_parameter('my_array').value
e = self.get_parameter('my_enum').value
self.get_logger().info(
f'my_str={s} my_int={i} my_double={d} '
f'my_bool={b} my_array={arr} my_enum={e}')
def main(args=None):
rclpy.init(args=args) # 1. 初始化 rclpy
node = ParamNode() # 2. 实例化节点(此时会声明参数)
try:
rclpy.spin(node) # 3. 阻塞运行,持续处理回调
except KeyboardInterrupt:
pass # 按 Ctrl+C 静默退出
finally:
node.destroy_node() # 4. 清理
rclpy.shutdown()
if __name__ == '__main__':
main()
3.2 代码要点解读
| 代码 | 作用 |
|---|---|
declare_parameter(名字, 默认值) |
声明一个参数并给定默认值。未声明的参数在读取时会有警告 |
get_parameter(名字) |
读取参数,返回一个 Parameter 对象 |
.value |
取出 Parameter 对象里的实际值(int / float / str / bool / list) |
create_timer(秒, 回调) |
定时触发回调,这里用来周期性打印当前参数值 |
为什么要
declare_parameter? 声明参数有两个好处:一是参数有了默认值(不传也能跑);二是参数会出现在ros2 param list中,并且能被ros2 param set、launch 文件合法地修改。不声明直接get_parameter会得到”未声明”的警告。
四、进阶:动态修改参数 + 合法性校验
默认情况下,ros2 param set 可以随意改参数值。如果想在修改时拦截并校验(比如枚举值只能取 A/B/C),可以用 add_on_set_parameters_callback 注册回调。
4.1 动态参数节点 param_node_dynamic.py
import rclpy
from rclpy.node import Node
from rclpy.parameter import Parameter
from rclpy.parameters import SetParametersResult
class DynamicParamNode(Node):
"""演示动态参数修改:允许 set 的同时做合法性校验。"""
def __init__(self):
super().__init__('param_node_dynamic')
self.declare_parameter('my_enum', 'A') # 只允许 A/B/C
self.declare_parameter('my_int', 42) # 只允许 0~100
# 注册"参数被修改时"的回调
self.add_on_set_parameters_callback(self.param_callback)
self.timer = self.create_timer(1.0, self.timer_callback)
def param_callback(self, params):
"""每次参数被 set 时调用。返回 SetParametersResult 表示接受/拒绝。"""
for p in params: # params 是一批 Parameter 对象
if p.name == 'my_enum' and p.value not in ('A', 'B', 'C'):
return SetParametersResult(
successful=False, reason='my_enum 只能是 A/B/C')
if p.name == 'my_int' and not (0 <= p.value <= 100):
return SetParametersResult(
successful=False, reason='my_int 必须在 0~100 之间')
return SetParametersResult(successful=True)
def timer_callback(self):
self.get_logger().info(
f'my_enum={self.get_parameter("my_enum").value} '
f'my_int={self.get_parameter("my_int").value}')
def main(args=None):
rclpy.init(args=args)
node = DynamicParamNode()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
4.2 校验回调的返回类型
| 返回 | 含义 |
|---|---|
SetParametersResult(successful=True) |
接受本次修改 |
SetParametersResult(successful=False, reason='...') |
拒绝本次修改,reason 会反馈给调用方 |
注意:校验回调的参数
params是一个列表(一次可能同时改多个参数),所以用for p in params逐个检查。只要有一个不合法,就返回successful=False,整批修改都会被拒绝。
五、完整过程(从包到运行)
第 1 步:创建功能包
在 src/ 下用官方命令创建 Python 包(这里包名用 py_param 演示,也可换成你自己的名字):
cd ~/ros_ws/src
ros2 pkg create py_param --build-type ament_python --node-name param_node
--node-name param_node 会自动生成 py_param/param_node.py 并在 setup.py 中注册入口。
第 2 步:放置代码文件
把上面的代码保存为:
src/py_param/py_param/param_node.py # 基础版:声明 + 读取
src/py_param/py_param/param_node_dynamic.py # 进阶版:动态修改 + 校验
第 3 步:配置 setup.py
编辑 src/py_param/setup.py,在 console_scripts 中注册两个可执行入口:
entry_points={
'console_scripts': [
'param_node = py_param.param_node:main',
'param_node_dynamic = py_param.param_node_dynamic:main',
],
},
第 4 步:确认 package.xml 依赖
确保 package.xml 里声明了 rclpy(ros2 pkg create 默认已带):
<exec_depend>rclpy</exec_depend>
第 5 步:构建
回到工作区根目录,构建这个包(每次改代码后都要重新构建):
cd ~/ros_ws
colcon build --packages-select py_param
source install/setup.bash
第 6 步:运行与命令行调参
# 终端 1:启动节点
source /opt/ros/jazzy/setup.bash
source ~/ros_ws/install/setup.bash
ros2 run py_param param_node
# 终端 2:查看 / 修改参数
source /opt/ros/jazzy/setup.bash
source ~/ros_ws/install/setup.bash
ros2 param list /param_node # 列出该节点的所有参数
ros2 param get /param_node my_str # 获取单个参数值
ros2 param set /param_node my_int 100 # 动态修改参数(终端 1 会立即打印新值)
第 7 步:验证结果
- 终端 1 会周期性打印:
my_str=world my_int=42 ... - 在终端 2 执行
ros2 param set /param_node my_int 100后,终端 1 立刻变为my_int=100 - 试试动态版本校验:
ros2 run py_param param_node_dynamic
# 另一个终端:
ros2 param set /param_node_dynamic my_enum X # 会被拒绝:my_enum 只能是 A/B/C
ros2 param set /param_node_dynamic my_enum B # 成功
ros2 param set /param_node_dynamic my_int 999 # 会被拒绝:必须在 0~100 之间
预期输出示例:
$ ros2 param set /param_node_dynamic my_enum X
Setting parameter failed: my_enum 只能是 A/B/C
$ ros2 param set /param_node_dynamic my_enum B
Set parameter successful
六、在 launch 文件中使用参数
参数也可以在启动时通过 launch 文件注入,这样”改参数不用动代码”。
6.1 直接传参
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='py_param',
executable='param_node',
# 启动时把参数注入到节点,等价于逐个 --ros-args -p
parameters=[{
'my_str': 'hello launch',
'my_int': 99,
'my_enum': 'B',
}],
)
])
6.2 从 YAML 文件加载(配合 ros2 param dump)
先导出节点当前参数到 YAML:
ros2 param dump /param_node # 生成 param_node.yaml
ros2 param dump /param_node --output-dir ~/ros_ws/src/py_param/config/
然后在 launch 中加载该文件:
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import PathJoinSubstitution, LaunchConfiguration
from launch_ros.actions import Node
from launch_ros.parameter_descriptions import ParameterFile
def generate_launch_description():
# 假设 config 目录在功能包内,路径会被正确解析
param_file = PathJoinSubstitution([
LaunchConfiguration('param_file'),
])
return LaunchDescription([
DeclareLaunchArgument(
'param_file',
default_value='src/py_param/config/param_node.yaml',
description='参数文件路径'),
Node(
package='py_param',
executable='param_node',
parameters=[ParameterFile(param_file, allow_substs=True)],
),
])
ros2 param dump生成的 YAML 会包含/**前缀(表示”任意命名空间下的该节点”),launch 加载时会自动套用到本节点的命名空间。
七、常用命令行速查
| 命令 | 作用 |
|---|---|
ros2 param list /节点名 |
列出节点的所有参数 |
ros2 param get /节点名 参数名 |
获取某个参数值 |
ros2 param set /节点名 参数名 值 |
设置某个参数值(可触发校验回调) |
ros2 param describe /节点名 参数名 |
查看参数类型、默认值、描述 |
ros2 param dump /节点名 |
把节点参数导出为 YAML 文件 |
ros2 param load /节点名 文件.yaml |
从 YAML 文件批量加载参数 |
ros2 run 包 可执行 --ros-args -p 名:=值 |
启动时直接传参 |
八、常见问题排查
| 现象 | 原因 / 解决 |
|---|---|
ros2 param list 看不到自己声明的参数 |
忘记 declare_parameter,只声明了却没调用;或节点没在运行 |
get_parameter 有 “undeclared parameter” 警告 |
参数未声明就读取,先用 declare_parameter 声明 |
ros2 param set 提示找不到节点 |
参数是属于节点的,先确认节点在运行:ros2 node list |
ros2 param set 修改无效 |
节点用 add_on_set_parameters_callback 拒绝了,或节点没读取该参数 |
| launch 里传的参数不生效 | 参数名写错,或 parameters 字典键名与 declare_parameter 的名字不一致 |
| 改了代码但行为没变 | 忘记重新构建:colcon build --packages-select py_param |
九、小结
- 参数是节点级的键值对配置,用于在不改代码的情况下调整节点行为。
- 用 Python 只需掌握四个关键点:
declare_parameter(声明)、get_parameter(...).value(读取)、add_on_set_parameters_callback(修改时校验)、ros2 param set(命令行调参)。 - 参数可以从三个入口设置:代码默认值 → launch 文件 → 命令行/其他节点,后设置的会覆盖先前的。
- 完整流程:建包 → 声明参数 → 注册入口 → 构建 → 运行 → 调参。
掌握了参数,你就有了给节点”拧旋钮”的能力。结合之前学的话题(Topic)和后面的服务(Service),就能搭建出灵活、可配置的机器人系统。