4. ROS2话题通信实战:发布者与订阅者模型、自定义消息接口、实现车辆速度话题发布与订阅
好,咱们今天来聊点实在的。话题通信,说白了就是ROS2里最核心的“传话”机制。一个节点负责喊话(发布者),另一个节点负责听(订阅者),中间通过一个叫“话题”的通道来传递消息。我刚开始接触ROS2时,总觉得这玩意儿跟ROS1差不多,但实际用起来,细节上差别还真不小。
4.1 发布者与订阅者模型:谁在说话,谁在听?
先看个最简单的模型。假设你有一辆车,车上有个速度传感器,它每隔0.1秒就往外喊一声“当前速度是30km/h”。另一边,仪表盘上的显示器一直在听,听到后就刷新显示。这就是发布者-订阅者模型。
发布者只管发,不管有没有人听。订阅者只管收,不管是谁发的。这种解耦设计,我在实际项目中深有体会——有一次我们要给自动驾驶系统加一个紧急制动模块,只需要订阅已有的速度话题就行,完全不用改传感器那边的代码。
核心要点:
- 发布者(Publisher):负责发送消息,可以有多个
- 订阅者(Subscriber):负责接收消息,也可以有多个
- 话题(Topic):消息传递的通道,名称唯一
- 消息(Message):具体的数据结构,比如速度值、时间戳
这里有个坑,我踩过。ROS2的话题通信是异步的,发布者发完消息就继续干别的事,不会等订阅者处理完。如果你的订阅者处理速度跟不上发布者的频率,消息就会堆积。嗯,这个问题后面我们会聊到怎么处理。
4.2 自定义消息接口:别用别人的鞋,不合脚
ROS2自带了很多标准消息类型,比如std_msgs/Float64、geometry_msgs/Twist。但实际项目中,这些往往不够用。我记得有一次做多传感器融合,需要同时传递速度、加速度、时间戳和置信度,标准消息根本装不下。
这时候就需要自定义消息接口。说白了,就是自己定义一个结构体,想装什么就装什么。
4.2.1 创建自定义消息文件
在ROS2工作空间里,消息定义文件放在.msg文件中。比如我们定义一个车辆速度消息:
# VehicleSpeed.msg
float32 speed # 速度值,单位 m/s
float32 acceleration # 加速度,单位 m/s²
uint64 timestamp # 时间戳,微秒
string source # 数据来源,比如 "wheel_encoder" 或 "gps"
float32 confidence # 置信度,0.0 ~ 1.0
你看,这样就把所有需要的信息打包在一起了。我习惯把消息定义放在单独的msg包里,方便其他包引用。
4.2.2 配置CMakeLists.txt和package.xml
定义好消息文件后,还需要告诉编译系统怎么处理它。在CMakeLists.txt里加上:
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/VehicleSpeed.msg"
)
在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>
这里有个小技巧:消息字段的类型一定要选对。比如时间戳用uint64而不是float64,因为微秒级的时间戳用整数更精确,不会出现浮点误差。
4.3 实现车辆速度话题发布与订阅
好,理论说完了,咱们直接上代码。我会用C++实现一个发布者节点和一个订阅者节点。
4.3.1 发布者节点:模拟速度传感器
// speed_publisher.cpp
#include "rclcpp/rclcpp.hpp"
#include "vehicle_msgs/msg/vehicle_speed.hpp"
class SpeedPublisher : public rclcpp::Node
{
public:
SpeedPublisher() : Node("speed_publisher")
{
publisher_ = this->create_publisher<vehicle_msgs::msg::VehicleSpeed>("vehicle_speed", 10);
timer_ = this->create_wall_timer(
std::chrono::milliseconds(100),
std::bind(&SpeedPublisher::timer_callback, this));
RCLCPP_INFO(this->get_logger(), "速度发布者已启动");
}
private:
void timer_callback()
{
auto msg = vehicle_msgs::msg::VehicleSpeed();
msg.speed = 10.0 + (rand() % 50) / 10.0; // 模拟速度变化
msg.acceleration = 0.5;
msg.timestamp = this->now().nanoseconds() / 1000; // 微秒
msg.source = "wheel_encoder";
msg.confidence = 0.95;
publisher_->publish(msg);
RCLCPP_INFO(this->get_logger(), "发布速度: %.2f m/s", msg.speed);
}
rclcpp::Publisher<vehicle_msgs::msg::VehicleSpeed>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr timer_;
};
int main(int argc, char *argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<SpeedPublisher>());
rclcpp::shutdown();
return 0;
}
这段代码里,我用了定时器每100ms发布一次消息。实际项目中,你可能需要从硬件读取真实数据,但模拟数据用来测试逻辑完全够用。
个人经验:发布者的队列大小(上面代码中的10)很关键。如果订阅者处理慢,消息会积压。我一般设成10~50,太大容易爆内存,太小容易丢消息。
4.3.2 订阅者节点:模拟仪表盘
// speed_subscriber.cpp
#include "rclcpp/rclcpp.hpp"
#include "vehicle_msgs/msg/vehicle_speed.hpp"
class SpeedSubscriber : public rclcpp::Node
{
public:
SpeedSubscriber() : Node("speed_subscriber")
{
subscription_ = this->create_subscription<vehicle_msgs::msg::VehicleSpeed>(
"vehicle_speed", 10,
std::bind(&SpeedSubscriber::topic_callback, this, std::placeholders::_1));
RCLCPP_INFO(this->get_logger(), "速度订阅者已启动");
}
private:
void topic_callback(const vehicle_msgs::msg::VehicleSpeed::SharedPtr msg)
{
RCLCPP_INFO(this->get_logger(), "收到速度: %.2f m/s, 来源: %s, 置信度: %.2f",
msg->speed, msg->source.c_str(), msg->confidence);
// 这里可以做速度校验
if (msg->confidence < 0.5) {
RCLCPP_WARN(this->get_logger(), "速度数据置信度偏低,请检查传感器");
}
}
rclcpp::Subscription<vehicle_msgs::msg::VehicleSpeed>::SharedPtr subscription_;
};
int main(int argc, char *argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<SpeedSubscriber>());
rclcpp::shutdown();
return 0;
}
订阅者的回调函数里,我加了一个置信度检查。这是我在实际项目中养成的习惯——永远不要相信传感器数据。曾经有一次,轮速编码器被泥巴糊住了,速度数据直接飘到200km/h,要不是有置信度校验,系统差点就失控了。
4.3.3 运行测试
编译好之后,开两个终端:
# 终端1:启动发布者
ros2 run vehicle_system speed_publisher
# 终端2:启动订阅者
ros2 run vehicle_system speed_subscriber
你会看到订阅者终端不断打印收到的速度信息。如果发布者停了,订阅者会一直等,不会报错——这就是话题通信的优雅之处。
避坑指南:我曾经遇到过一个问题——发布者和订阅者都启动了,但订阅者就是收不到消息。排查了半天,发现是话题名称写错了,一个叫"vehicle_speed",另一个叫"vehicle_speed_data"。ROS2的话题名称是大小写敏感的,而且不能有拼写错误。所以,命名一定要统一,最好在配置文件里定义好。
4.4 本章知识体系
下面这张图展示了本章的核心逻辑,我画成了流程图,方便你理解整个通信链路:
从这张图可以看得很清楚:发布者把消息扔到话题通道里,订阅者从通道里取。两者不需要知道对方的存在,这就是ROS2话题通信的精髓。
4.5 总结与避坑
好了,这一章的内容就这些。我最后再唠叨几句:
- 消息类型要选对:能用整数别用浮点,能定长别变长,性能差很多
- 话题名称要统一:最好在常量或配置文件中定义,别手写字符串
- 队列大小要合理:根据你的发布频率和订阅处理速度来调
- 别忘了加日志:调试时能救命,我吃过这个亏
下一章我们会聊更高级的话题——服务通信,那是另一种“一问一答”的模式。不过今天先到这里,把话题通信练熟了再说。
课后练习:试着修改发布者代码,让速度值按照正弦曲线变化,模拟车辆加速减速的过程。然后在订阅者里计算平均速度,每10秒打印一次。