话题通信基础:话题模型(发布/订阅模式)、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”。然后写一个发布者,每秒发送一次;再写一个订阅者,打印接收到的数据。