ROS 2 Jazzy 进阶实战:Action、参数与Launch文件全链路指南
在机器人开发的世界里,只会写节点和用话题、服务通信,就像只会开手动挡汽车------能跑,但遇到复杂路况会手忙脚乱。当你需要控制机器人导航、机械臂抓取这类耗时任务,需要动态调整节点参数,或者一键启动十几个节点组成的完整系统时,就必须掌握 ROS 2 的三个进阶利器:Action(动作) 、Parameters(参数) 和 Launch Files(启动文件)。
本文将带你从零开始,通过实战案例掌握这三个核心技能,让你的 ROS 2 项目从"能跑的 demo"升级为"可维护的工程系统"。
一、Action:当 Service "不够用"时的终极解决方案
话题适合持续的数据流,服务适合快速的请求-响应,但如果一次请求要执行几秒、几十秒甚至更久,服务就显得力不从心了。这就是 Action 存在的意义。

1.1 为什么我们需要 Action?
先看一个所有机器人开发者都会遇到的经典场景:让移动机器人从 A 点走到 B 点。
如果用 Service 实现,流程是这样的:
- 客户端发送
/move_robot请求,附带目标坐标 - 服务端收到请求,开始控制轮子运动
- 到达目标后返回成功/失败
看起来很合理,但实际运行时你会遇到三个致命问题:
- 客户端"盲等":机器人走到一半,你完全不知道它走到哪了,有没有偏离路线
- 无法中途取消 :如果突然检测到障碍物,你只能眼睁睁看着它撞上去,或者写一个额外的
/stop_robot服务 - 多请求处理混乱:如果同时有两个客户端发送导航请求,服务端是排队执行?还是拒绝?还是覆盖?Service 没有标准的处理机制
你当然可以"补丁式"地解决这些问题:加一个话题发进度,加一个服务做取消,再写一个状态机管理任务。但 ROS 2 已经把这些通用能力打包成了标准组件------这就是 Action。
通俗解释:
- 话题:广播电台,单向持续发送,谁都能听
- 服务:打电话,一问一答,说完就挂
- Action:快递配送,你下单(发目标)→ 快递员接单(接受)→ 实时更新物流信息(反馈)→ 送货上门(结果)→ 中途可以取消订单
1.2 Action 的工作原理与底层实现
Action 本质上是封装好的 Topics + Services,它自动帮你生成了管理任务所需的所有通信接口。一个完整的 Action 流程包含以下几个部分:
- Goal(目标):客户端向服务端发送的任务请求
- Accept/Reject(接受/拒绝):服务端收到目标后立刻表态
- Feedback(反馈):执行过程中服务端持续向客户端发送进度信息
- Result(结果):任务执行完成后返回的最终结果
- Cancel(取消):客户端可以请求取消正在执行的任务
- Status(状态):任务的内部状态流转(等待中/执行中/已完成/已取消/失败)
关键底层事实:Action 不是新的通信协议,它在底层会自动生成 3 个话题和 3 个服务:
/action_name/_action/goal:发送目标的服务/action_name/_action/cancel:取消任务的服务/action_name/_action/result:获取结果的服务/action_name/_action/feedback:反馈进度的话题/action_name/_action/status:状态更新的话题/action_name/_action/feedback:反馈进度的话题
你不需要自己创建这些接口,只需要定义一个 Action 接口文件,ROS 2 会自动帮你生成所有代码。
1.3 自定义 Action 接口:CountUntil 计数器
我们来实现一个简单但完整的 Action 示例:CountUntil(计数到指定数字)。
- Goal :目标数字
target_number+ 每次计数间隔delay(秒) - Feedback :当前计数
current_number - Result :最终到达的数字
reached_number(如果被取消就返回最后计数到的数字)
步骤1:创建 Action 接口文件
在你的接口包 my_robot_interfaces 中新建 action 目录:
bash
cd ~/ros2_ws/src/my_robot_interfaces
mkdir action
touch action/CountUntil.action
写入接口定义(注意必须用两条 --- 分隔,顺序固定为 Goal/Result/Feedback):
# Goal
int64 target_number
float64 delay
---
# Result
int64 reached_number
---
# Feedback
int64 current_number
步骤2:修改 CMakeLists.txt
在 rosidl_generate_interfaces 函数中添加 Action 文件:
cmake
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/HardwareStatus.msg"
"srv/ResetCounter.srv"
"action/CountUntil.action"
)
步骤3:构建并验证
bash
cd ~/ros2_ws
colcon build --packages-select my_robot_interfaces
source ~/.bashrc
# 验证接口是否生成成功
ros2 interface show my_robot_interfaces/action/CountUntil
1.4 编写 Action Server:处理任务并返回结果
Action Server 的核心是两个回调函数:
goal_callback:收到目标时调用,决定接受还是拒绝execute_callback:目标被接受后调用,执行实际任务
python
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from rclpy.action import ActionServer
from rclpy.executors import MultiThreadedExecutor
from rclpy.callback_groups import ReentrantCallbackGroup
from my_robot_interfaces.action import CountUntil
import time
class CountUntilServerNode(Node):
def __init__(self):
super().__init__("count_until_server")
# 创建 Action Server,必须使用 ReentrantCallbackGroup 支持取消
self.callback_group = ReentrantCallbackGroup()
self.action_server = ActionServer(
self,
CountUntil,
"count_until",
goal_callback=self.goal_callback,
execute_callback=self.execute_callback,
cancel_callback=self.cancel_callback,
callback_group=self.callback_group
)
self.get_logger().info("Count Until Action Server has been started.")
def goal_callback(self, goal_request):
# 校验目标参数:目标数字必须大于 0
if goal_request.target_number <= 0:
self.get_logger().warn(f"Rejected goal: target_number={goal_request.target_number} must be > 0")
return rclpy.action.GoalResponse.REJECT
self.get_logger().info(f"Accepted goal: count to {goal_request.target_number} with delay {goal_request.delay}s")
return rclpy.action.GoalResponse.ACCEPT
def cancel_callback(self, goal_handle):
self.get_logger().info("Received cancel request")
return rclpy.action.CancelResponse.ACCEPT
def execute_callback(self, goal_handle):
target = goal_handle.request.target_number
delay = goal_handle.request.delay
counter = 0
self.get_logger().info(f"Starting execution: counting to {target}")
while counter < target:
# 检查是否有取消请求
if goal_handle.is_cancel_requested:
self.get_logger().info("Goal canceled")
goal_handle.canceled()
result = CountUntil.Result()
result.reached_number = counter
return result
counter += 1
self.get_logger().info(f"Count: {counter}")
# 发布反馈
feedback = CountUntil.Feedback()
feedback.current_number = counter
goal_handle.publish_feedback(feedback)
time.sleep(delay)
# 任务成功完成
self.get_logger().info(f"Goal succeeded: reached {counter}")
goal_handle.succeed()
result = CountUntil.Result()
result.reached_number = counter
return result
def main(args=None):
rclpy.init(args=args)
node = CountUntilServerNode()
# 必须使用 MultiThreadedExecutor,否则 cancel 回调无法执行
executor = MultiThreadedExecutor()
rclpy.spin(node, executor)
rclpy.shutdown()
if __name__ == "__main__":
main()
⚠️ 最容易踩的坑 :
默认的
SingleThreadedExecutor是单线程的,如果execute_callback长时间占用线程,cancel_callback根本没有机会运行。也就是说:你点了取消,但服务器"听不见"。解决方法:使用
MultiThreadedExecutor+ReentrantCallbackGroup,允许并发执行回调。
1.5 编写 Action Client:发送目标并处理结果
Action Client 发送目标后不会阻塞,而是通过回调函数处理结果和反馈:
python
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from rclpy.action import ActionClient
from my_robot_interfaces.action import CountUntil
class CountUntilClientNode(Node):
def __init__(self):
super().__init__("count_until_client")
self.action_client = ActionClient(self, CountUntil, "count_until")
self.get_logger().info("Count Until Action Client has been started.")
def send_goal(self, target_number, delay):
# 等待服务端上线
while not self.action_client.wait_for_service(1.0):
self.get_logger().warn("Waiting for Count Until Action Server...")
# 创建目标
goal = CountUntil.Goal()
goal.target_number = target_number
goal.delay = delay
# 异步发送目标,注册回调
self.get_logger().info(f"Sending goal: count to {target_number} with delay {delay}s")
future = self.action_client.send_goal_async(
goal,
feedback_callback=self.feedback_callback
)
future.add_done_callback(self.goal_response_callback)
def feedback_callback(self, feedback_msg):
# 处理反馈
current = feedback_msg.feedback.current_number
self.get_logger().info(f"Feedback: {current}")
def goal_response_callback(self, future):
goal_handle = future.result()
if not goal_handle.accepted:
self.get_logger().error("Goal rejected by server")
return
self.get_logger().info("Goal accepted by server")
# 等待结果
result_future = goal_handle.get_result_async()
result_future.add_done_callback(self.goal_result_callback)
def goal_result_callback(self, future):
result = future.result().result
status = future.result().status
if status == 4: # STATUS_SUCCEEDED
self.get_logger().info(f"Goal succeeded! Reached number: {result.reached_number}")
elif status == 6: # STATUS_CANCELED
self.get_logger().info(f"Goal canceled! Last reached number: {result.reached_number}")
else:
self.get_logger().error(f"Goal failed with status: {status}")
# 退出节点
rclpy.shutdown()
def main(args=None):
rclpy.init(args=args)
node = CountUntilClientNode()
# 发送目标:计数到 10,间隔 0.5 秒
node.send_goal(10, 0.5)
rclpy.spin(node)
if __name__ == "__main__":
main()
1.6 命令行调试 Action
ROS 2 提供了完整的命令行工具来调试 Action:
bash
# 列出所有 Action
ros2 action list
# 查看 Action 信息
ros2 action info /count_until
# 从终端直接发送 Goal 并查看反馈
ros2 action send_goal /count_until my_robot_interfaces/action/CountUntil "{target_number: 5, delay: 0.4}" --feedback
# 查看 Action 底层生成的隐藏话题和服务
ros2 topic list --include-hidden-topics
ros2 service list --include-hidden-services
1.7 三种通信方式对比与选择
| 特性 | 话题(Topic) | 服务(Service) | 动作(Action) |
|---|---|---|---|
| 通信模型 | 发布-订阅 | 请求-响应 | 目标-反馈-结果 |
| 数据流向 | 单向 | 双向 | 双向(带进度) |
| 适用场景 | 持续高频数据流(传感器、速度指令) | 快速一次性操作(开关、复位) | 耗时任务(导航、机械臂运动) |
| 实时性 | 高 | 中等 | 低 |
| 支持取消 | 不支持 | 不支持 | 支持 |
| 带进度反馈 | 不支持 | 不支持 | 支持 |
| 多客户端 | 支持 | 支持 | 支持 |
一句话选择原则:
- 持续流用话题
- 快速调用用服务
- 耗时任务用 Action

二、参数:让节点从"写死的程序"变成"可配置的组件"
你有没有遇到过这种情况:写了一个完美的相机驱动节点,结果换了个 USB 口就要改代码、重新编译;想把帧率从 30 改成 60,又要重复一遍流程。这就是硬编码的痛点,而 ROS 2 的参数系统就是为了解决这个问题。
2.1 为什么需要参数?
先看一个真实工程中的例子:USB 相机驱动节点。
一个相机驱动节点通常需要以下配置:
- USB 设备名:
/dev/ttyUSB0还是/dev/ttyUSB1 - 帧率:30 FPS 还是 60 FPS
- 分辨率:640x480 还是 1280x720
- 仿真模式:
true还是false
如果把这些值都硬编码在代码里,会发生什么?
- 换硬件就要改代码、重新编译
- 同时跑两个相机就要复制两份代码
- 调试时修改参数非常麻烦
参数的出现,就是把这些"配置项"从代码里剥离出来,让你可以在启动时甚至运行时修改它们,而不需要重新编译代码。
通俗解释 :
参数就像你手机 App 的"设置"界面。你不需要修改 App 的源代码,只要在设置里改一下,App 的行为就会跟着变。ROS 2 的参数就是节点的"设置项"。
2.2 参数的基本使用:声明 + 获取
在 ROS 2 中使用参数只有两步:
- 声明(Declare):告诉节点"我有这个参数",并指定默认值和类型
- 获取(Get):把参数的值读出来,用到业务逻辑中
⚠️ 重要规则 :必须先声明参数,才能获取它。否则节点启动时会抛出
ParameterNotDeclaredException异常。
Python 版本
python
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from example_interfaces.msg import Int64
class NumberPublisherNode(Node):
def __init__(self):
super().__init__("number_publisher")
# 1. 声明参数,指定默认值(类型自动推断)
self.declare_parameter("number", 2)
self.declare_parameter("publish_period", 1.0)
# 2. 获取参数值
self.number_ = self.get_parameter("number").value
self.timer_period_ = self.get_parameter("publish_period").value
# 定时器周期公式:T = 1 / f
# 其中:
# T: 定时器周期(秒)
# f: 定时器频率(赫兹)
self.timer_ = self.create_timer(self.timer_period_, self.publish_number)
self.publisher_ = self.create_publisher(Int64, "number", 10)
self.get_logger().info(f"Number publisher started: number={self.number_}, period={self.timer_period_}s")
def publish_number(self):
msg = Int64()
msg.data = self.number_
self.publisher_.publish(msg)
def main(args=None):
rclpy.init(args=args)
node = NumberPublisherNode()
rclpy.spin(node)
rclpy.shutdown()
if __name__ == "__main__":
main()
C++ 版本
cpp
#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/msg/int64.hpp"
class NumberPublisherNode : public rclcpp::Node
{
public:
NumberPublisherNode() : Node("number_publisher")
{
// 1. 声明参数
this->declare_parameter("number", 2);
this->declare_parameter("publish_period", 1.0);
// 2. 获取参数值
number_ = this->get_parameter("number").as_int();
double timer_period = this->get_parameter("publish_period").as_double();
timer_ = this->create_wall_timer(
std::chrono::duration<double>(timer_period),
std::bind(&NumberPublisherNode::publish_number, this)
);
publisher_ = this->create_publisher<example_interfaces::msg::Int64>("number", 10);
RCLCPP_INFO(this->get_logger(), "Number publisher started: number=%d, period=%.2fs", number_, timer_period);
}
private:
void publish_number()
{
auto msg = example_interfaces::msg::Int64();
msg.data = number_;
publisher_->publish(msg);
}
int number_;
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<example_interfaces::msg::Int64>::SharedPtr publisher_;
};
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<NumberPublisherNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
2.3 启动时传参:命令行方式
启动节点时,你可以通过 --ros-args -p 参数覆盖默认值:
bash
# 发布数字 3,周期 0.5 秒(频率 2Hz)
ros2 run my_py_pkg number_publisher --ros-args -p number:=3 -p publish_period:=0.5
同一份代码跑多个节点实例 :
你可以给同一个节点不同的名字和参数,同时运行多个实例:
bash
# 第一个节点:名字 num_pub1,发布数字 3,周期 0.5 秒
ros2 run my_py_pkg number_publisher --ros-args -r __node:=num_pub1 -p number:=3 -p publish_period:=0.5
# 第二个节点:名字 num_pub2,发布数字 4,周期 1.0 秒
ros2 run my_py_pkg number_publisher --ros-args -r __node:=num_pub2 -p number:=4 -p publish_period:=1.0
这样你就有两个完全独立的发布者节点,用同一份代码,不同的配置。
2.4 批量管理参数:YAML 参数文件
当参数数量超过 5 个时,命令行传参就变得非常麻烦。这时我们可以用 YAML 文件来批量管理参数。
创建 config/number_params.yaml:
yaml
/num_pub1:
ros__parameters:
number: 3
publish_period: 0.5
/num_pub2:
ros__parameters:
number: 4
publish_period: 1.0
启动时加载参数文件:
bash
ros2 run my_py_pkg number_publisher --ros-args --params-file ~/ros2_ws/src/my_robot_bringup/config/number_params.yaml
一个 YAML 文件可以包含多个节点的参数,每个节点只会读取属于自己的那一部分配置。
2.5 ros2 param 命令行工具
ROS 2 提供了强大的 ros2 param 命令来管理参数:
bash
# 列出所有节点的所有参数
ros2 param list
# 获取某个参数的值
ros2 param get /num_pub1 number
# 设置某个参数的值(运行时修改)
ros2 param set /num_pub1 number 7
# 导出某个节点的所有参数到 YAML
ros2 param dump /num_pub1 > number_params.yaml
# 从 YAML 文件加载参数(运行时批量修改)
ros2 param load /num_pub1 number_params.yaml
2.6 参数回调:让运行时修改真正生效
这里有一个非常容易踩的坑:你用 ros2 param set 修改了参数,但代码里的变量并没有跟着变。
因为 get_parameter() 只是在节点启动时读了一次值,之后参数变了,变量不会自动更新。要解决这个问题,你需要注册参数回调函数。
python
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from example_interfaces.msg import Int64
from rcl_interfaces.msg import SetParametersResult
class NumberPublisherNode(Node):
def __init__(self):
super().__init__("number_publisher")
self.declare_parameter("number", 2)
self.declare_parameter("publish_period", 1.0)
self.number_ = self.get_parameter("number").value
self.timer_period_ = self.get_parameter("publish_period").value
self.timer_ = self.create_timer(self.timer_period_, self.publish_number)
self.publisher_ = self.create_publisher(Int64, "number", 10)
# 注册参数回调
self.add_post_set_parameters_callback(self.parameters_callback)
self.get_logger().info(f"Number publisher started: number={self.number_}, period={self.timer_period_}s")
def parameters_callback(self, params):
# 遍历所有被修改的参数
for param in params:
if param.name == "number":
self.number_ = param.value
self.get_logger().info(f"Updated number to {self.number_}")
elif param.name == "publish_period":
self.timer_period_ = param.value
# 重新创建定时器
self.timer_.cancel()
self.timer_ = self.create_timer(self.timer_period_, self.publish_number)
self.get_logger().info(f"Updated publish period to {self.timer_period_}s")
# 返回成功
return SetParametersResult(successful=True)
def publish_number(self):
msg = Int64()
msg.data = self.number_
self.publisher_.publish(msg)
def main(args=None):
rclpy.init(args=args)
node = NumberPublisherNode()
rclpy.spin(node)
rclpy.shutdown()
if __name__ == "__main__":
main()
现在当你运行 ros2 param set /number_publisher number 7 时,发布的数字会立刻变成 7,不需要重启节点。
注意:并不是所有参数都适合运行时修改。比如相机的分辨率,通常需要重启相机才能生效。是否需要参数回调,取决于你的业务逻辑。
三、Launch 文件:一键启动 ROS 2 全家桶
当你的项目只有 2-3 个节点时,手动开终端一个个启动还能接受。但当节点数量增加到 10 个、20 个,每个节点还有十几个参数时,手动启动就变成了一场灾难。Launch 文件就是为了解决这个问题。
3.1 为什么必须要用 Launch 文件?
想象一个最简单的移动机器人系统,它包含以下节点:
- 3 个相机节点(不同端口、不同分辨率)
- 2 个激光雷达节点
- 1 个 IMU 节点
- 1 个电机控制节点
- 1 个导航节点
- 1 个 UI 节点
这已经是 9 个节点了。如果不用 Launch 文件,你需要:
- 开 9 个终端
- 每个终端敲一条长长的
ros2 run命令,带对参数和参数文件 - 确保启动顺序正确(比如导航节点必须在传感器节点之后启动)
- 调试时重启一次,就要重复上面所有步骤
最致命的是,节点越多,你犯错的概率越高------名字写错、参数漏了、重映射忘了,这些低级错误会浪费你大量的时间。
Launch 文件的意义就是:把启动系统这件事,从一堆容易出错的终端命令,变成一个可版本管理、可复用、可一键启动的配置文件。
通俗解释 :
Launch 文件就像电脑上的"一键启动"快捷方式。你不用一个个打开应用程序,只要点一下快捷方式,所有需要的程序都会按顺序自动启动。
3.2 工程最佳实践:单独建 bringup 包
一个非常容易踩的坑是:把 Launch 文件随便丢进某个业务包里面。一开始看起来省事,后面会形成严重的依赖混乱。
正确的做法 :专门创建一个"启动入口包",名字通常以 _bringup 结尾。比如你的机器人叫 my_robot,就建一个 my_robot_bringup 包。
这个包通常不写任何业务代码,只放两类东西:
launch/:所有的 Launch 文件config/:所有的 YAML 参数文件
这样做的好处:
- 启动配置与业务代码完全解耦
- 依赖清晰:bringup 包只依赖其他业务包
- 迁移、复用、交付都非常干净
创建 bringup 包:
bash
cd ~/ros2_ws/src
ros2 pkg create my_robot_bringup
cd my_robot_bringup
rm -r src/ include/
mkdir launch config
3.3 最小可用 Launch 文件:XML vs Python
ROS 2 支持三种格式的 Launch 文件:XML、Python 和 YAML。工程中最常用的是 XML 和 Python。
XML 版本:简洁直观,适合大多数场景
创建 launch/number_app.launch.xml:
xml
<launch>
<!-- 启动 number_publisher 节点 -->
<node pkg="my_py_pkg" exec="number_publisher" name="num_pub1">
<param name="number" value="3" />
<param name="publish_period" value="0.5" />
</node>
<!-- 启动 number_counter 节点 -->
<node pkg="my_cpp_pkg" exec="number_counter" name="num_counter" />
</launch>
启动:
bash
ros2 launch my_robot_bringup number_app.launch.xml
Python 版本:功能强大,适合需要逻辑的场景
创建 launch/number_app.launch.py:
python
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package="my_py_pkg",
executable="number_publisher",
name="num_pub1",
parameters=[
{"number": 3},
{"publish_period": 0.5}
]
),
Node(
package="my_cpp_pkg",
executable="number_counter",
name="num_counter"
)
])
选择原则:
- 默认用 XML:简单、短、结构清晰
- 只有当你需要条件判断、循环、动态路径拼接等逻辑时,才用 Python
3.4 Launch 文件的核心配置
你在命令行 ros2 run 里能做的所有事情,Launch 文件都能做。最常用的有三类配置:
1. 重命名节点
避免同名节点冲突:
xml
<node pkg="my_py_pkg" exec="number_publisher" name="num_pub1" />
<node pkg="my_py_pkg" exec="number_publisher" name="num_pub2" />
2. Remap 通信名
把代码里写死的话题/服务/Action 名映射到另一个名字:
xml
<node pkg="my_cpp_pkg" exec="number_counter">
<!-- 把代码里的 "/number" 话题映射到 "/my_number" -->
<remap from="/number" to="/my_number" />
</node>
3. 加载参数
少量参数可以直接写在 Launch 文件里,大量参数建议用 YAML 文件:
xml
<node pkg="my_py_pkg" exec="number_publisher">
<!-- 直接写参数 -->
<param name="number" value="3" />
<param name="publish_period" value="0.5" />
<!-- 从 YAML 文件加载参数 -->
<param from="$(find-pkg-share my_robot_bringup)/config/number_params.yaml" />
</node>
3.5 Namespace:一招隔离多套系统
Namespace(命名空间)是 ROS 2 中非常强大的功能,尤其适合以下场景:
- 同时启动两台相同的机器人
- 同时运行两套相同的节点组(比如两个 turtlesim 模拟器)
不用 Namespace 的话,你需要手动修改所有节点名、话题名、服务名,很快就会乱成一团。用 Namespace 的话,只要给每套系统加一个统一的前缀即可。
xml
<launch>
<!-- 第一套系统:命名空间 /robot1 -->
<group ns="robot1">
<node pkg="turtlesim" exec="turtlesim_node" name="turtle" />
<node pkg="my_py_pkg" exec="turtle_controller" name="controller" />
</group>
<!-- 第二套系统:命名空间 /robot2 -->
<group ns="robot2">
<node pkg="turtlesim" exec="turtlesim_node" name="turtle" />
<node pkg="my_py_pkg" exec="turtle_controller" name="controller" />
</group>
</launch>
启动后,你会看到两个完全独立的 turtlesim 窗口,它们的话题名分别是 /robot1/turtle1/cmd_vel 和 /robot2/turtle1/cmd_vel,完全不会互相干扰。
⚠️ 最容易踩的坑 :
Namespace 能否作用到话题名,取决于代码里话题名的写法:
- 写
number(无前导/)→ 会随 Namespace 变成/robot1/number- 写
/number(有前导/)→ 这是全局名字,Namespace 不会影响它所以工程最佳实践是:代码里永远不要写带前导
/的话题名。
总结
本文完整介绍了 ROS 2 三个最重要的进阶概念:Action、参数和 Launch 文件。
- Action 解决了耗时任务的管理问题,提供了标准化的目标、反馈、取消和结果机制
- 参数 把配置从代码中剥离出来,让节点变得可配置、可复用
- Launch 文件 解决了多节点系统的规模化启动问题,让你可以一键启动整套系统
掌握了这三个技能,你就从"会写 ROS 节点"升级到了"会搭 ROS 系统"。接下来,你可以继续学习 ROS 2 的 QoS 配置、TF 坐标变换和导航栈,进一步提升你的机器人开发能力。