4、ROS话题通信机制:话题模型详解、发布者/订阅者模式、消息类型定义、自定义消息

聊到ROS,绕不开的就是话题通信。我经常跟团队里的新人说,你把话题通信搞明白了,ROS就算入门一半了。它就像整个机器人系统的神经网络,把各个节点串在一起。

说白了,话题通信就是发布者-订阅者模式。一个节点发布数据,其他节点订阅它。发布者只管发,订阅者只管收,双方不需要知道对方是谁。这种解耦设计,在机器人这种多模块系统里,简直太实用了。

4.1 话题模型详解

先看看话题模型长什么样。我画个简单的图帮你理解:

+-----------+      发布话题A      +-----------+
| 发布者节点 |  ================>  | 订阅者节点 |
+-----------+                     +-----------+
       |                               |
       | 话题A (/topic_name)           |
       | 消息类型 (std_msgs/String)    |
       +-------------------------------+

每个话题都有一个唯一的名字,比如/cmd_vel/odom。这个名字就是通信的地址。发布者和订阅者通过这个名字匹配上。

我记得第一次做多机器人项目时,有个同事把话题名写错了,结果机器人死活不动。查了半天才发现是/cmd_vel写成了/cmd_vel/,多了一个斜杠。嗯,这种小坑,踩过一次就记住了。

话题模型有几个关键点:

  • 异步通信:发布者发完就走,不等待订阅者处理
  • 多对多:一个话题可以有多个发布者和多个订阅者
  • 无连接:发布者和订阅者之间没有直接连接,全靠ROS Master做中介

核心要点:话题通信是ROS中最常用的通信方式,适合周期性数据(传感器数据、控制指令等)。不适合请求-响应模式,那种场景用服务通信更合适。

4.2 发布者/订阅者模式

发布者和订阅者,说白了就是生产者-消费者的变体。我习惯把发布者想象成广播电台,订阅者就是收音机。电台只管播,收音机只管收,互不干扰。

4.2.1 发布者实现

写一个发布者节点,其实就三步:

  1. 初始化ROS节点
  2. 创建发布者对象,指定话题名和消息类型
  3. 循环发布消息

看个例子:

#include <ros/ros.h>
#include <std_msgs/String.h>

int main(int argc, char **argv)
{
    // 1. 初始化节点
    ros::init(argc, argv, "talker");
    ros::NodeHandle nh;

    // 2. 创建发布者
    // 参数:话题名,队列大小
    ros::Publisher pub = nh.advertise<std_msgs::String>("chatter", 10);

    // 3. 设置发布频率
    ros::Rate rate(10); // 10Hz

    while (ros::ok())
    {
        std_msgs::String msg;
        msg.data = "Hello ROS!";

        // 发布消息
        pub.publish(msg);

        // 休眠,控制频率
        rate.sleep();
    }

    return 0;
}

这里有个细节——队列大小。我建议设成10左右,别太大也别太小。设太大,内存占用高;设太小,消息容易丢。我在项目中遇到过,队列设成1,结果订阅者处理慢一点,消息全丢了,机器人控制直接卡顿。

4.2.2 订阅者实现

订阅者更简单,核心是回调函数

#include <ros/ros.h>
#include <std_msgs/String.h>

// 回调函数:收到消息时自动调用
void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
    ROS_INFO("I heard: [%s]", msg->data.c_str());
}

int main(int argc, char **argv)
{
    ros::init(argc, argv, "listener");
    ros::NodeHandle nh;

    // 创建订阅者
    // 参数:话题名,队列大小,回调函数
    ros::Subscriber sub = nh.subscribe("chatter", 10, chatterCallback);

    // 进入循环,等待回调
    ros::spin();

    return 0;
}

你想想看,回调函数是订阅者的灵魂。消息一来,它自动触发。你不用写循环去轮询,ROS框架帮你搞定了。

小技巧:如果订阅者处理不过来,可以适当增大队列大小。但别超过100,否则内存占用会飙升。我一般设20-50,够用。

4.3 消息类型定义

ROS的消息类型,说白了就是数据结构定义。它用.msg文件来描述。ROS内置了很多常用类型:

包名 常用消息类型 说明
std_msgs String, Int32, Float64, Bool 基础数据类型
geometry_msgs Point, Pose, Twist, Vector3 几何相关数据
sensor_msgs Image, LaserScan, Imu 传感器数据
nav_msgs Odometry, Path, MapMetaData 导航相关数据

这些内置类型覆盖了大部分场景。但有时候,你需要传递一些特殊数据。比如我在做多机器人协同项目时,需要传递机器人的任务状态和优先级。内置类型没有,那就得自己定义。

4.4 自定义消息

自定义消息,是ROS里非常实用的技能。我建议每个做ROS开发的人都掌握。

4.4.1 创建.msg文件

在功能包下创建msg文件夹,然后新建一个.msg文件。比如RobotStatus.msg

# 机器人状态消息
string robot_name
uint8  battery_level    # 0-100
bool   is_busy
float32 speed
uint8  task_priority    # 1-5,1最高

语法很简单:类型 字段名。支持的类型包括:

  • 基础类型:int8, uint8, int16, uint16, int32, uint32, float32, float64, string, bool
  • 数组类型:int32[], float64[]
  • 其他消息类型:geometry_msgs/Point, std_msgs/Header

4.4.2 配置编译

创建完.msg文件,还要告诉编译系统去处理它。修改CMakeLists.txt

# 添加消息生成依赖
find_package(catkin REQUIRED COMPONENTS
  roscpp
  std_msgs
  message_generation
)

# 声明要生成的消息
add_message_files(
  FILES
  RobotStatus.msg
)

# 生成消息
generate_messages(
  DEPENDENCIES
  std_msgs
)

再修改package.xml

<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>

4.4.3 使用自定义消息

编译成功后,就可以像使用内置消息一样使用它了:

#include <ros/ros.h>
#include <your_package/RobotStatus.h>

int main(int argc, char **argv)
{
    ros::init(argc, argv, "status_publisher");
    ros::NodeHandle nh;

    ros::Publisher pub = nh.advertise<your_package::RobotStatus>("robot_status", 10);

    ros::Rate rate(1); // 1Hz

    while (ros::ok())
    {
        your_package::RobotStatus msg;
        msg.robot_name = "robot_1";
        msg.battery_level = 85;
        msg.is_busy = false;
        msg.speed = 0.5;
        msg.task_priority = 3;

        pub.publish(msg);
        rate.sleep();
    }

    return 0;
}

注意:自定义消息的包名一定要写对。我曾经因为包名写错,编译通过了但运行时找不到消息类型,折腾了半小时。检查一下#include路径和advertise中的类型名。

4.5 避坑指南

做话题通信这么多年,踩过的坑不少。分享几个常见的:

  • 话题名拼写错误:发布者和订阅者的话题名必须完全一致,大小写敏感。我建议用rostopic list检查一下。
  • 消息类型不匹配:发布者和订阅者的消息类型必须一致。用rostopic info /topic_name查看。
  • 队列大小设置不当:太小丢消息,太大占内存。根据你的数据频率和处理速度来调。
  • 忘记ros::spin():订阅者必须调用ros::spin()ros::spinOnce(),否则回调函数不会执行。

我曾经在一个项目中,订阅者收不到数据,查了半天发现是ros::spin()写在了while循环外面。嗯,这种低级错误,犯过一次就长记性了。

话题通信是ROS的基石。你把它吃透了,后面的服务通信、动作通信学起来就轻松多了。下一章我们聊服务通信,那个更适合请求-响应的场景。