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 核心逻辑流程图
为了让你更直观地理解整个通信流程,我画了一张图:
这张图清晰地展示了:发布者将数据推送到话题通道,订阅者从通道拉取数据。双方完全解耦,各自独立运行。
3.7 避坑指南
我曾经踩过的坑:
- 消息类型不匹配:发布者和订阅者必须使用完全相同的消息类型,包括包名。否则订阅者收不到任何数据。
- QoS 设置不一致:发布者和订阅者的 QoS 配置需要兼容。比如一个用“可靠传输”,另一个用“尽力而为”,可能导致通信失败。
- 忘记 source:每次编译后记得
source install/setup.bash,否则节点找不到自定义消息。 - 话题名称拼写错误:发布者和订阅者的话题名称必须完全一致,大小写敏感。
我个人建议,在调试阶段先用 ros2 topic echo 验证发布者是否正常工作,再启动订阅者。这样能快速定位问题出在哪一端。
3.8 小结
话题通信是 ROS2 的基石。掌握了发布者-订阅者模型和自定义消息接口,你就能搭建出灵活的机器人通信系统。
实际项目中,我经常用自定义消息来传递传感器融合数据、控制指令、状态信息等。它让代码更清晰,也让团队协作更高效。
嗯,今天就聊到这里。动手试试看,写一个你自己的话题通信例子吧。