3. ROS2进阶通信

聊完了Topic,咱们得聊聊ROS2里另外几个“狠角色”。

Topic是“发布-订阅”,说白了就是单向广播。你发你的,我收我的,谁也不等谁。但实际工程里,哪有这么简单?你想想看,你让底盘转个弯,总得知道它转没转好吧?你让相机拍张照,总得拿到照片吧?这时候,Service和Action就派上用场了。

3.1 Service与Client:一问一答的“请求-响应”模式

Service,我习惯叫它“服务”。它跟Topic最大的区别就是:有来有回。

Client发一个请求,Service处理完,再给一个响应。就像你打电话问客服:“我家网断了,帮我查查?”客服查完告诉你:“先生,您欠费了。”——这就是一个完整的Service调用。

核心区别一句话总结:

  • Topic:单向流,适合高频、持续的数据(比如激光雷达点云、摄像头图像)。
  • Service:双向交互,适合低频、一次性的任务(比如拍照、设置参数、查询状态)。

我在项目中遇到过一个问题:用Topic去请求一个传感器校准结果。结果发现,因为Topic是异步的,我发完请求就跑了,根本不知道校准啥时候结束。后来改成Service,发请求后直接阻塞等待响应,代码逻辑一下子就清晰了。

3.1.1 定义.srv文件

Service的接口定义在.srv文件里。格式很简单:请求部分和响应部分用---隔开。

# 例:拍照服务
# CaptureImage.srv
string camera_name   # 请求:相机名称
---
bool success         # 响应:是否成功
sensor_msgs/Image image  # 响应:图像数据

3.1.2 编写Service端(C++)

嗯,这里要注意。Service端的回调函数,必须返回一个bool值,表示是否处理成功。

// service_server.cpp
#include "rclcpp/rclcpp.hpp"
#include "your_package/srv/capture_image.hpp"

class CaptureService : public rclcpp::Node {
public:
  CaptureService() : Node("capture_service") {
    srv_ = this->create_service<your_package::srv::CaptureImage>(
      "capture_image",
      std::bind(&CaptureService::handle_capture, this, std::placeholders::_1, std::placeholders::_2));
  }

private:
  bool handle_capture(
    const std::shared_ptr<your_package::srv::CaptureImage::Request> req,
    std::shared_ptr<your_package::srv::CaptureImage::Response> res) {
    RCLCPP_INFO(this->get_logger(), "收到拍照请求,相机:%s", req->camera_name.c_str());
    // 模拟拍照
    res->success = true;
    // res->image = ... 填充图像数据
    return true;
  }
  rclcpp::Service<your_package::srv::CaptureImage>::SharedPtr srv_;
};

3.1.3 编写Client端(C++)

Client端调用Service时,我建议你加上超时判断。否则网络一卡,程序就卡死了。

// service_client.cpp
#include "rclcpp/rclcpp.hpp"
#include "your_package/srv/capture_image.hpp"

auto client = node->create_client<your_package::srv::CaptureImage>("capture_image");
auto request = std::make_shared<your_package::srv::CaptureImage::Request>();
request->camera_name = "front_camera";

// 等待服务上线
while (!client->wait_for_service(std::chrono::seconds(1))) {
  RCLCPP_WARN(node->get_logger(), "等待拍照服务上线...");
}

// 异步发送请求
auto future = client->async_send_request(request);
// 等待结果(带超时)
if (future.wait_for(std::chrono::seconds(5)) == std::future_status::ready) {
  auto response = future.get();
  if (response->success) {
    RCLCPP_INFO(node->get_logger(), "拍照成功!");
  }
} else {
  RCLCPP_ERROR(node->get_logger(), "拍照超时!");
}

避坑指南:我曾经在Client端忘了检查wait_for_service,结果Service还没启动,Client就发请求了,直接崩溃。记住:Service不是Topic,没有“晚加入”机制。Client必须确保Service在线,才能调用。

3.2 Action通信机制:带反馈的“长期任务”

Service虽然好用,但它有个硬伤:阻塞。你发一个请求,必须等响应回来才能干别的事。如果这个任务要执行10秒钟呢?你让整个程序卡10秒?

Action就是来解决这个问题的。它本质上是一个“带反馈的Service”。

  • Goal:你下达的目标(比如“导航到A点”)。
  • Feedback:任务执行过程中的状态反馈(比如“已行驶50%”)。
  • Result:最终结果(比如“到达A点”)。

你想想看,自动驾驶里的路径规划、泊车入库,哪个不是长期任务?Action简直就是为这些场景量身定做的。

3.2.1 定义.action文件

# 例:导航到目标点
# NavigateToPose.action
# Goal
geometry_msgs/PoseStamped target_pose
---
# Result
bool success
float32 duration_seconds
---
# Feedback
float32 progress_percentage  # 0.0 ~ 100.0

3.2.2 Action Server端(C++)

Server端需要实现三个回调:handle_goal、handle_cancel、handle_accepted。

// action_server.cpp
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "your_package/action/navigate_to_pose.hpp"

class NavigateServer : public rclcpp::Node {
public:
  using NavigateAction = your_package::action::NavigateToPose;
  using GoalHandle = rclcpp_action::ServerGoalHandle<NavigateAction>;

  NavigateServer() : Node("navigate_server") {
    action_server_ = rclcpp_action::create_server<NavigateAction>(
      this, "navigate_to_pose",
      std::bind(&NavigateServer::handle_goal, this, std::placeholders::_1, std::placeholders::_2),
      std::bind(&NavigateServer::handle_cancel, this, std::placeholders::_1),
      std::bind(&NavigateServer::handle_accepted, this, std::placeholders::_1));
  }

private:
  rclcpp_action::GoalResponse handle_goal(const rclcpp_action::GoalUUID &uuid,
                                          std::shared_ptr<const NavigateAction::Goal> goal) {
    RCLCPP_INFO(this->get_logger(), "收到导航目标");
    // 可以在这里做合法性检查
    return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
  }

  rclcpp_action::CancelResponse handle_cancel(std::shared_ptr<GoalHandle> goal_handle) {
    RCLCPP_INFO(this->get_logger(), "收到取消请求");
    return rclcpp_action::CancelResponse::ACCEPT;
  }

  void handle_accepted(std::shared_ptr<GoalHandle> goal_handle) {
    // 启动一个线程去执行任务,避免阻塞
    std::thread{std::bind(&NavigateServer::execute, this, goal_handle)}.detach();
  }

  void execute(std::shared_ptr<GoalHandle> goal_handle) {
    auto feedback = std::make_shared<NavigateAction::Feedback>();
    auto result = std::make_shared<NavigateAction::Result>();

    // 模拟导航过程
    for (int i = 0; i <= 100; i += 10) {
      if (goal_handle->is_canceling()) {
        result->success = false;
        goal_handle->canceled(result);
        return;
      }
      feedback->progress_percentage = i;
      goal_handle->publish_feedback(feedback);
      std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }

    result->success = true;
    result->duration_seconds = 5.0;
    goal_handle->succeed(result);
  }

  rclcpp_action::Server<NavigateAction>::SharedPtr action_server_;
};

注意:Action Server的execute函数里,一定要检查is_canceling()。我曾经写过一个导航Action,忘了检查取消标志,结果用户点了取消,机器人还在傻乎乎地往前开……差点撞墙。

3.3 参数服务器:节点的“配置文件”

参数服务器,说白了就是ROS2里一个全局的键值对存储。你可以在启动时加载参数,也可以在运行时动态修改。

我个人习惯把传感器参数、PID系数、算法开关都放在参数服务器里。这样改参数不用重新编译代码,重启节点就行。

3.3.1 声明与获取参数

// 声明参数(带默认值)
this->declare_parameter<double>("pid.kp", 1.0);
this->declare_parameter<double>("pid.ki", 0.1);
this->declare_parameter<double>("pid.kd", 0.05);

// 获取参数
double kp = this->get_parameter("pid.kp").as_double();

3.3.2 动态参数回调

如果你想在参数改变时立刻响应,可以注册一个回调。

// 参数改变回调
rclcpp::ParameterCallbackHandle::SharedPtr cb_handle_;
cb_handle_ = this->add_on_set_parameters_callback(
  [this](const std::vector<rclcpp::Parameter>& params) -> rcl_interfaces::msg::SetParametersResult {
    for (auto &param : params) {
      if (param.get_name() == "pid.kp") {
        RCLCPP_INFO(this->get_logger(), "Kp 更新为: %f", param.as_double());
        // 更新控制器
      }
    }
    rcl_interfaces::msg::SetParametersResult result;
    result.successful = true;
    return result;
  });

小技巧:我建议你把参数名用“.”分层,比如pid.kp、sensor.lidar.range。这样在YAML配置文件里结构清晰,也方便用ros2 param list查看。

3.4 生命周期节点:让节点“活”起来

这个功能,说实话,我第一次看到时觉得有点多余。直到我在实际项目中,需要控制多个传感器的启动顺序——先启动激光雷达,再启动相机,最后启动融合算法。如果不用生命周期节点,你得自己写一堆状态机代码。

生命周期节点,把节点的生命周期分成了几个明确的状态:

状态 说明
Unconfigured 未配置,节点刚创建
Inactive 已配置,但未激活(不处理数据)
Active 激活状态,正常运行
Finalized 销毁前清理

节点在这几个状态之间切换,需要调用对应的接口:configure()、activate()、deactivate()、cleanup()、shutdown()。

3.4.1 编写生命周期节点

#include "rclcpp_lifecycle/lifecycle_node.hpp"

class MyLifecycleNode : public rclcpp_lifecycle::LifecycleNode {
public:
  MyLifecycleNode() : LifecycleNode("my_lifecycle_node") {}

  // 配置阶段:分配资源、读取参数
  rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
  on_configure(const rclcpp_lifecycle::State &) override {
    RCLCPP_INFO(get_logger(), "配置中...");
    // 创建Publisher、Subscriber等
    return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
  }

  // 激活阶段:开始处理数据
  rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
  on_activate(const rclcpp_lifecycle::State &) override {
    RCLCPP_INFO(get_logger(), "激活中...");
    return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
  }

  // 清理阶段:释放资源
  rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
  on_cleanup(const rclcpp_lifecycle::State &) override {
    RCLCPP_INFO(get_logger(), "清理中...");
    return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
  }
};

核心价值:生命周期节点让系统具备了“可控性”。你可以通过ros2 lifecycle set /node_name configure这样的命令,精确控制每个节点的行为。在自动驾驶系统中,这简直是救命的功能——你可以先让所有传感器就绪,再统一激活感知算法,避免数据错乱。

3.5 本章知识体系总览

下面这张图,是我自己梳理的。它把本章四个核心通信机制的关系画清楚了。

ROS2进阶通信机制知识体系 ROS2通信核心 Service / Client Action 通信 参数服务器 生命周期节点 一问一答,阻塞式 带反馈的长期任务 全局键值对存储 状态机管理节点 拍照、查询、设置参数 导航、机械臂控制 PID系数、传感器配置 传感器启动顺序控制 核心原则:Topic用于高频数据流,Service用于短任务,Action用于长任务,参数服务器用于配置,生命周期节点用于状态管理

这张图里,我把Service、Action、参数服务器、生命周期节点并列展示。你仔细看,它们其实解决的是不同维度的问题:Service和Action解决“交互模式”,参数服务器解决“配置管理”,生命周期节点解决“节点状态控制”。

在实际的自动驾驶项目中,这四个机制经常混合使用。比如:用生命周期节点控制传感器启动顺序,用参数服务器加载算法参数,用Service触发拍照,用Action执行路径规划。嗯,这才是工程级的做法。

我的建议:初学者可以先从Service和参数服务器入手,这两个最常用。Action稍微复杂一点,但一旦你写过一次,就会发现它比Service优雅得多。生命周期节点嘛,等你需要做系统集成时,自然会爱上它。

好了,这一章的内容就到这儿。记住,通信机制是ROS2的骨架,把骨架搭好了,后面的感知和规划算法才能跑得稳。

可可精品资料库