【ROS 2 全栈入门指南三】:Action、参数与Launch文件全链路指南

ROS 2 Jazzy 进阶实战:Action、参数与Launch文件全链路指南

在机器人开发的世界里,只会写节点和用话题、服务通信,就像只会开手动挡汽车------能跑,但遇到复杂路况会手忙脚乱。当你需要控制机器人导航、机械臂抓取这类耗时任务,需要动态调整节点参数,或者一键启动十几个节点组成的完整系统时,就必须掌握 ROS 2 的三个进阶利器:Action(动作)Parameters(参数)Launch Files(启动文件)

本文将带你从零开始,通过实战案例掌握这三个核心技能,让你的 ROS 2 项目从"能跑的 demo"升级为"可维护的工程系统"。

一、Action:当 Service "不够用"时的终极解决方案

话题适合持续的数据流,服务适合快速的请求-响应,但如果一次请求要执行几秒、几十秒甚至更久,服务就显得力不从心了。这就是 Action 存在的意义。

1.1 为什么我们需要 Action?

先看一个所有机器人开发者都会遇到的经典场景:让移动机器人从 A 点走到 B 点

如果用 Service 实现,流程是这样的:

  1. 客户端发送 /move_robot 请求,附带目标坐标
  2. 服务端收到请求,开始控制轮子运动
  3. 到达目标后返回成功/失败

看起来很合理,但实际运行时你会遇到三个致命问题:

  • 客户端"盲等":机器人走到一半,你完全不知道它走到哪了,有没有偏离路线
  • 无法中途取消 :如果突然检测到障碍物,你只能眼睁睁看着它撞上去,或者写一个额外的 /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 中使用参数只有两步:

  1. 声明(Declare):告诉节点"我有这个参数",并指定默认值和类型
  2. 获取(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 文件,你需要:

  1. 开 9 个终端
  2. 每个终端敲一条长长的 ros2 run 命令,带对参数和参数文件
  3. 确保启动顺序正确(比如导航节点必须在传感器节点之后启动)
  4. 调试时重启一次,就要重复上面所有步骤

最致命的是,节点越多,你犯错的概率越高------名字写错、参数漏了、重映射忘了,这些低级错误会浪费你大量的时间。

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 坐标变换和导航栈,进一步提升你的机器人开发能力。

相关推荐
xian_wwq2 小时前
【学习笔记】「大模型安全:攻击面演化史」第 02 篇-越狱攻防战
笔记·学习
Agilex松灵机器人2 小时前
ROS 机械臂开发效率低?用 Agent + 具身智能重构开发流程
重构·机器人·自动驾驶
Kobebryant-Manba2 小时前
学习automl
学习
Inhand陈工2 小时前
映翰通IG502实战:通过RS232采集交通信号灯数据,实现自动短信告警
网络·嵌入式硬件·物联网·网络安全·边缘计算·信息与通信·信号处理
不脱发的程序猿2 小时前
我把360里真正用得到的功能拆出来,做个轻量工具
stm32·单片机·嵌入式硬件
J.Kuchiki2 小时前
【PostgreSQL 内核学习:平衡 K 路归并(Balanced k-way Merge)】
数据库·学习·postgresql
EriccoShaanxi2 小时前
高性能MEMS IMU:为精准导航与传感注入强大动力
机器人·无人机
XGeFei2 小时前
【Fastapi学习笔记(7)】—— Fastapi 中间件、前端跨域请求
笔记·学习·fastapi