话题通信基础:话题模型(发布/订阅模式)、Publisher与Subscriber的API、自定义消息类型

各位同学,欢迎来到第三章。今天咱们聊聊ROS里最核心、最常用的通信方式——话题通信。说白了,话题就是ROS世界里的一条“消息高速公路”。

我刚开始接触ROS时,最困惑的就是:节点之间到底怎么说话?后来搞明白了,其实就两种模式:一种是“你问我答”(服务通信),另一种就是今天要讲的“你发我看”(话题通信)。

3.1 话题模型:发布/订阅模式

话题模型,本质上是一个发布/订阅模式。想象一下,你订阅了一个公众号。公众号作者(发布者)每次发文章,你(订阅者)就能收到。作者不知道谁订阅了他,你也不知道作者还有多少其他粉丝。这就是解耦。

在ROS里,一个节点可以发布话题,另一个节点可以订阅这个话题。发布者只管发,订阅者只管收。它们之间不需要建立直接连接。嗯,这里要注意:一个话题可以有多个发布者和多个订阅者

我曾在项目中遇到过一个问题:两个传感器节点同时发布同一个话题,结果订阅者收到的数据乱套了。后来发现,ROS默认不会对多个发布者的消息做同步处理。所以,如果你有多个发布者,一定要在订阅者端做好时间戳校验。

核心要点:

  • 发布者(Publisher):负责发送消息
  • 订阅者(Subscriber):负责接收消息
  • 话题(Topic):消息的“通道名称”
  • 通信是异步的、匿名的、多对多的

3.2 Publisher与Subscriber的API

讲完模型,咱们直接上代码。ROS里创建发布者和订阅者,其实就几个API调用。我个人习惯用C++,因为性能好,但Python写起来更直观。这里我两种都给你展示一下。

3.2.1 创建Publisher(C++)

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

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

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

    ros::Rate loop_rate(10); // 10Hz
    while (ros::ok())
    {
        std_msgs::String msg;
        msg.data = "Hello ROS!";

        pub.publish(msg); // 发布消息
        ros::spinOnce();
        loop_rate.sleep();
    }
    return 0;
}

你看,nh.advertise<std_msgs::String>("chatter", 1000) 这行就是创建发布者。模板参数是消息类型,第一个参数是话题名,第二个参数是队列大小。队列大小是什么意思?说白了,就是如果订阅者处理不过来,ROS会帮你缓存多少条消息。

我的经验:队列大小别设太大。我曾经设过10000,结果内存爆了。一般100到1000就够用。高频话题(比如激光雷达)可以设小一点,低频话题(比如地图)可以设大一点。

3.2.2 创建Subscriber(C++)

#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;

    // 创建订阅者,订阅"chatter"话题,回调函数为chatterCallback
    ros::Subscriber sub = nh.subscribe("chatter", 1000, chatterCallback);

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

订阅者更简单。你只需要提供一个回调函数。每当有消息到达,ROS就会自动调用这个函数。注意ros::spin(),它会让程序一直运行,等待消息。如果你有多个回调,也可以用ros::spinOnce()手动控制。

3.2.3 Python版本

Python的API更简洁,我直接贴代码:

#!/usr/bin/env python
import rospy
from std_msgs.msg import String

def talker():
    pub = rospy.Publisher('chatter', String, queue_size=10)
    rospy.init_node('talker', anonymous=True)
    rate = rospy.Rate(10) # 10hz
    while not rospy.is_shutdown():
        msg = "hello world %s" % rospy.get_time()
        pub.publish(msg)
        rate.sleep()

if __name__ == '__main__':
    try:
        talker()
    except rospy.ROSInterruptException:
        pass

订阅者:

#!/usr/bin/env python
import rospy
from std_msgs.msg import String

def callback(data):
    rospy.loginfo("I heard %s", data.data)

def listener():
    rospy.init_node('listener', anonymous=True)
    rospy.Subscriber("chatter", String, callback)
    rospy.spin()

if __name__ == '__main__':
    listener()

你想想看,Python版本是不是更清爽?但C++性能更好,适合高频场景。我个人在写底层驱动时用C++,在写上层逻辑时用Python。

3.3 自定义消息类型

ROS内置的消息类型(比如String、Int32、Pose)够用吗?说实话,大部分时候不够。你想想,如果你要发送一个包含“温度、湿度、气压”的数据包,用三个独立的话题?太麻烦了。这时候就需要自定义消息类型。

3.3.1 创建.msg文件

在功能包下创建一个msg文件夹,然后新建一个SensorData.msg文件:

float32 temperature
float32 humidity
float32 pressure
string sensor_id
time timestamp

就这么简单。每一行是一个字段,格式是“类型 名称”。支持的类型包括:int8、int16、int32、float32、float64、string、time、duration,以及其他的msg类型。

3.3.2 修改CMakeLists.txt和package.xml

创建完.msg文件,还需要告诉编译系统。在CMakeLists.txt中:

find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  std_msgs
  message_generation
)

add_message_files(
  FILES
  SensorData.msg
)

generate_messages(
  DEPENDENCIES
  std_msgs
)

package.xml中:

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

避坑指南:我曾经忘记加message_generation依赖,结果编译报错“找不到头文件”。还有一次,我改了.msg文件但忘了重新编译,结果运行时报错“字段不存在”。记住:每次修改.msg文件,都要catkin_make一下。

3.3.3 使用自定义消息

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

#include <your_package/SensorData.h>

// 发布
your_package::SensorData msg;
msg.temperature = 25.5;
msg.humidity = 60.0;
msg.pressure = 1013.25;
msg.sensor_id = "sensor_01";
msg.timestamp = ros::Time::now();
pub.publish(msg);

// 订阅回调
void callback(const your_package::SensorData::ConstPtr& msg)
{
    ROS_INFO("Temp: %.2f, Hum: %.2f", msg->temperature, msg->humidity);
}

Python版本类似:

from your_package.msg import SensorData

msg = SensorData()
msg.temperature = 25.5
msg.humidity = 60.0
pub.publish(msg)

3.4 总结与建议

好了,这一章的内容就这些。咱们回顾一下:

概念 说明
话题模型 发布/订阅模式,异步、匿名、多对多
Publisher API advertise() 创建,publish() 发送
Subscriber API subscribe() 创建,回调函数处理
自定义消息 .msg文件 + CMakeLists.txt + package.xml

我个人建议:刚开始学的时候,先用内置消息类型跑通流程。等熟悉了,再尝试自定义消息。别一上来就搞复杂的数据结构,容易把自己绕晕。

下一章,咱们会讲服务通信。那是另一种模式——“你问我答”。到时候你会发现,话题和服务就像ROS的两条腿,缺一不可。

小练习:试着创建一个自定义消息,包含一个int32的“id”和一个float32的“value”。然后写一个发布者,每秒发送一次;再写一个订阅者,打印接收到的数据。