3、话题通信实战:发布者与订阅者模型,自定义消息接口

好,咱们今天来聊聊 ROS2 里最核心的通信方式——话题通信。

说白了,话题通信就是节点之间的一种“发布-订阅”模式。一个节点发布数据,其他节点订阅这些数据。发布者只管发,订阅者只管收,双方不需要知道对方是谁。这种解耦设计,在机器人系统里特别实用。

3.1 发布者与订阅者模型

我刚开始接触 ROS 时,总觉得这玩意儿有点抽象。后来自己动手写了个小例子,一下子就明白了。

想象一下,你有一个传感器节点,它不断发布温度数据。另一个节点订阅这个数据,用来做决策。这就是典型的话题通信。

核心概念:

  • 发布者(Publisher):负责发送消息的节点
  • 订阅者(Subscriber):负责接收消息的节点
  • 话题(Topic):消息传递的通道,有唯一的名称
  • 消息(Message):具体的数据结构,比如 int32、string 或自定义类型

嗯,这里要注意:发布者和订阅者可以是一对多、多对一,甚至多对多的关系。ROS2 的 DDS 中间件帮我们处理了这些复杂的网络通信细节。

3.2 自定义消息接口

ROS2 内置了很多消息类型,比如 std_msgs/String、geometry_msgs/Twist。但实际项目中,我们经常需要定义自己的消息结构。

我在做导航项目时,就遇到过需要传递“路径点+速度+时间戳”的组合数据。内置消息搞不定,只能自己定义。

3.2.1 创建自定义消息文件

首先,在你的功能包下创建一个 msg 文件夹。然后新建一个 .msg 文件,比如 NavPoint.msg。

# NavPoint.msg
int32 id
float64 x
float64 y
float64 yaw
float64 speed
string description
builtin_interfaces/Time timestamp

你看,这里我用了基本类型,也引用了 builtin_interfaces/Time 这个内置消息。ROS2 支持消息嵌套,非常灵活。

3.2.2 修改 CMakeLists.txt 和 package.xml

定义好消息文件后,需要告诉编译系统去生成对应的代码。

package.xml 中添加依赖:

<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>

CMakeLists.txt 中添加:

find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/NavPoint.msg"
)

ament_export_dependencies(rosidl_default_runtime)

我曾经在这里踩过坑——忘记添加 member_of_group 那行,结果编译死活不生成消息代码。折腾了半天才发现。

3.3 编写发布者节点

好,消息接口定义好了,我们来写一个发布者节点。

// publisher_node.cpp
#include "rclcpp/rclcpp.hpp"
#include "your_package/NavPoint.hpp"

using namespace std::chrono_literals;

class NavPointPublisher : public rclcpp::Node
{
public:
  NavPointPublisher() : Node("nav_point_publisher")
  {
    publisher_ = this->create_publisher<your_package::msg::NavPoint>("nav_point_topic", 10);
    timer_ = this->create_wall_timer(500ms, std::bind(&NavPointPublisher::timer_callback, this));
  }

private:
  void timer_callback()
  {
    auto msg = your_package::msg::NavPoint();
    msg.id = 1;
    msg.x = 1.5;
    msg.y = 2.3;
    msg.yaw = 0.78;
    msg.speed = 0.5;
    msg.description = "Waypoint 1";
    msg.timestamp = this->now();

    publisher_->publish(msg);
    RCLCPP_INFO(this->get_logger(), "Published NavPoint: id=%d", msg.id);
  }

  rclcpp::Publisher<your_package::msg::NavPoint>::SharedPtr publisher_;
  rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char *argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<NavPointPublisher>());
  rclcpp::shutdown();
  return 0;
}

你想想看,这个节点每 500 毫秒发布一次导航点数据。实际项目中,你可能需要根据传感器数据动态调整发布频率。

3.4 编写订阅者节点

有发布就有订阅。下面这个节点会接收我们发布的导航点数据。

// subscriber_node.cpp
#include "rclcpp/rclcpp.hpp"
#include "your_package/NavPoint.hpp"

class NavPointSubscriber : public rclcpp::Node
{
public:
  NavPointSubscriber() : Node("nav_point_subscriber")
  {
    subscription_ = this->create_subscription<your_package::msg::NavPoint>(
      "nav_point_topic", 10, std::bind(&NavPointSubscriber::topic_callback, this, std::placeholders::_1));
  }

private:
  void topic_callback(const your_package::msg::NavPoint::SharedPtr msg)
  {
    RCLCPP_INFO(this->get_logger(), "Received: id=%d, x=%.2f, y=%.2f, desc=%s",
                msg->id, msg->x, msg->y, msg->description.c_str());
  }

  rclcpp::Subscription<your_package::msg::NavPoint>::SharedPtr subscription_;
};

int main(int argc, char *argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<NavPointSubscriber>());
  rclcpp::shutdown();
  return 0;
}

我个人习惯在回调函数里加个时间戳检查,防止处理过期的消息。尤其是在导航场景下,旧数据可能导致机器人做出错误决策。

3.5 编译与运行

写完后,编译整个工作空间:

colcon build --packages-select your_package
source install/setup.bash

然后分别运行两个节点:

ros2 run your_package publisher_node
ros2 run your_package subscriber_node

你会在终端看到订阅者不断打印接收到的导航点信息。

调试小技巧:

  • 使用 ros2 topic list 查看当前所有活跃话题
  • 使用 ros2 topic echo /nav_point_topic 直接查看话题数据
  • 使用 ros2 topic info /nav_point_topic 查看发布者和订阅者数量

3.6 核心逻辑流程图

为了让你更直观地理解整个通信流程,我画了一张图:

发布者节点 NavPointPublisher 订阅者节点 NavPointSubscriber /nav_point_topic NavPoint 消息结构 id: int32 x, y, yaw: float64 speed: float64 description: string timestamp: builtin_interfaces/Time 话题通信核心流程 发布者 -> 话题通道 -> 订阅者

这张图清晰地展示了:发布者将数据推送到话题通道,订阅者从通道拉取数据。双方完全解耦,各自独立运行。

3.7 避坑指南

我曾经踩过的坑:

  • 消息类型不匹配:发布者和订阅者必须使用完全相同的消息类型,包括包名。否则订阅者收不到任何数据。
  • QoS 设置不一致:发布者和订阅者的 QoS 配置需要兼容。比如一个用“可靠传输”,另一个用“尽力而为”,可能导致通信失败。
  • 忘记 source:每次编译后记得 source install/setup.bash,否则节点找不到自定义消息。
  • 话题名称拼写错误:发布者和订阅者的话题名称必须完全一致,大小写敏感。

我个人建议,在调试阶段先用 ros2 topic echo 验证发布者是否正常工作,再启动订阅者。这样能快速定位问题出在哪一端。

3.8 小结

话题通信是 ROS2 的基石。掌握了发布者-订阅者模型和自定义消息接口,你就能搭建出灵活的机器人通信系统。

实际项目中,我经常用自定义消息来传递传感器融合数据、控制指令、状态信息等。它让代码更清晰,也让团队协作更高效。

嗯,今天就聊到这里。动手试试看,写一个你自己的话题通信例子吧。

可可精品资料库