2、ROS2核心通信机制(上):话题通信模型、发布者与订阅者编程(C++/Python)、自定义消息接口、rqt_graph可视化工具
好,咱们正式开始聊ROS2最核心的东西——通信机制。
我个人一直觉得,学ROS2如果没搞懂通信,那就像学开车只会踩油门不会打方向盘。你想想看,机器人身上那么多传感器、控制器、执行器,它们之间怎么说话?靠的就是这套通信机制。
今天咱们先讲上半部分:话题通信。这是ROS2里最常用、最基础的一种通信方式。
2.1 话题通信模型:发布者与订阅者
话题通信,说白了就是发布-订阅模式。
我举个例子你就明白了。想象一下你在做一个天气预报系统。有一个传感器负责采集温度数据,它每隔一秒就把温度值发出来。另一边,你的手机App想显示当前温度,它只需要接收这个数据就行。
在这个场景里:
- 发布者:就是那个温度传感器。它只管发数据,不管谁在收。
- 订阅者:就是你的手机App。它只管收数据,不管谁在发。
- 话题:就是"温度数据"这个通道。发布者和订阅者通过话题名字来匹配。
我在项目中遇到过一个问题:两个节点明明都写对了,但就是收不到数据。查了半天,发现是话题名字拼写不一致。一个叫/temperature,另一个叫/temp。嗯,这种低级错误我犯过一次就再也没犯过了。
核心要点:ROS2的话题通信是异步的、一对多的。一个发布者可以对应多个订阅者,反之亦然。发布者和订阅者之间完全解耦,它们不需要知道对方的存在。
2.2 发布者编程(C++版本)
咱们先看C++怎么写一个发布者。我个人习惯用C++写性能敏感的部分,比如控制循环。
// publisher_demo.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
int main(int argc, char *argv[])
{
rclcpp::init(argc, argv);
auto node = std::make_shared<rclcpp::Node>("talker");
auto publisher = node->create_publisher<std_msgs::msg::String>("chatter", 10);
rclcpp::WallRate rate(1); // 1 Hz
while (rclcpp::ok())
{
auto msg = std_msgs::msg::String();
msg.data = "Hello ROS2!";
RCLCPP_INFO(node->get_logger(), "Publishing: '%s'", msg.data.c_str());
publisher->publish(msg);
rclcpp::spin_some(node);
rate.sleep();
}
rclcpp::shutdown();
return 0;
}
这里有几个地方要注意:
create_publisher的第二个参数是队列大小。我一般设10,如果消息积压超过10条,老的消息会被丢弃。rclcpp::spin_some(node)用来处理回调。虽然这个例子里没有订阅回调,但养成习惯总是好的。- 消息类型
std_msgs::msg::String是ROS2内置的,直接拿来用。
小技巧:如果你想让发布者一直运行,记得用rclcpp::spin(node)代替spin_some。前者会阻塞,后者是非阻塞的。我一般在循环里用spin_some,在纯回调驱动的节点里用spin。
2.3 发布者编程(Python版本)
Python版本就简洁多了。我经常用Python做原型验证,写起来快。
# publisher_demo.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class Talker(Node):
def __init__(self):
super().__init__('talker')
self.publisher_ = self.create_publisher(String, 'chatter', 10)
self.timer = self.create_timer(1.0, self.timer_callback)
def timer_callback(self):
msg = String()
msg.data = 'Hello ROS2!'
self.get_logger().info(f'Publishing: "{msg.data}"')
self.publisher_.publish(msg)
def main(args=None):
rclpy.init(args=args)
node = Talker()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Python版用了定时器回调,而不是手动循环。这样代码更干净,也符合ROS2的事件驱动模型。
我曾经在Python版里忘记调用rclpy.spin(node),结果节点启动后啥也不干。嗯,这种坑踩过一次就记住了。
2.4 订阅者编程(C++版本)
有发就有收。咱们看看订阅者怎么写。
// subscriber_demo.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
void topic_callback(const std_msgs::msg::String::SharedPtr msg)
{
RCLCPP_INFO(rclcpp::get_logger("listener"), "I heard: '%s'", msg->data.c_str());
}
int main(int argc, char *argv[])
{
rclcpp::init(argc, argv);
auto node = std::make_shared<rclcpp::Node>("listener");
auto subscription = node->create_subscription<std_msgs::msg::String>(
"chatter", 10, topic_callback);
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
注意看,订阅者的回调函数是在spin里被调用的。你不需要手动去轮询,ROS2框架会帮你处理。
避坑指南:回调函数里不要做耗时操作。我曾经在回调里写了个复杂的图像处理算法,结果导致消息堆积,系统越来越卡。正确的做法是:回调里只做轻量级处理,或者把数据放到队列里,另起线程去处理。
2.5 订阅者编程(Python版本)
# subscriber_demo.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class Listener(Node):
def __init__(self):
super().__init__('listener')
self.subscription = self.create_subscription(
String, 'chatter', self.listener_callback, 10)
def listener_callback(self, msg):
self.get_logger().info(f'I heard: "{msg.data}"')
def main(args=None):
rclpy.init(args=args)
node = Listener()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Python版订阅者跟C++版逻辑完全一样。只是语法更简洁。我个人觉得Python版更适合教学和快速验证。
2.6 自定义消息接口
内置的消息类型(比如String、Int32)够用吗?说实话,在真实项目中基本不够用。
你想想看,如果你要发布一个机器人的位姿信息,包含x、y、z坐标和四元数,用内置类型拼起来多麻烦。这时候就需要自定义消息。
自定义消息的步骤很简单:
- 在功能包下创建
msg文件夹 - 编写
.msg文件,定义字段 - 修改
CMakeLists.txt或setup.py,添加依赖 - 编译生成代码
举个例子,创建一个Pose2D.msg:
float64 x
float64 y
float64 theta
然后在C++里这样用:
#include "your_package/msg/pose2_d.hpp"
// ...
auto msg = your_package::msg::Pose2D();
msg.x = 1.0;
msg.y = 2.0;
msg.theta = 0.5;
Python里也一样:
from your_package.msg import Pose2D
msg = Pose2D()
msg.x = 1.0
msg.y = 2.0
msg.theta = 0.5
重要提醒:自定义消息的包名和消息名不要用下划线开头,也不要包含特殊字符。我见过有人把消息命名为_my_msg.msg,结果编译报错,查了半天才发现是命名问题。
2.7 rqt_graph可视化工具
代码写完了,怎么验证节点之间的通信是否正确?
我最常用的工具就是rqt_graph。它能实时显示当前ROS2系统中的节点和话题关系图。
使用方法:
# 先启动你的节点
ros2 run your_package talker
ros2 run your_package listener
# 再打开另一个终端
rqt_graph
你会看到一个漂亮的拓扑图:
- 椭圆框代表节点
- 矩形框代表话题
- 箭头表示数据流向
我记得有一次调试一个多机器人系统,有十几个节点在同时通信。用rqt_graph一看,发现有个话题被三个节点同时发布,数据乱成一锅粥。嗯,这种问题光看代码很难发现,但可视化工具一眼就能看出来。
小技巧:在rqt_graph界面里,你可以勾选"Hide debug"来隐藏系统内部节点,只显示你自己的节点。这样图会更清晰。
2.8 本章小结
今天咱们把ROS2话题通信的核心内容过了一遍:
- 发布-订阅模型:发布者只管发,订阅者只管收
- C++和Python两种语言的发布者/订阅者编程
- 自定义消息接口:让通信数据结构更贴合你的需求
- rqt_graph:可视化调试利器
下一章咱们会讲ROS2的另一种通信方式——服务通信。它跟话题通信有什么不同?什么时候该用服务而不是话题?到时候咱们细聊。
好,今天就到这里。如果你在写代码时遇到问题,记得先检查话题名字拼写,再检查消息类型是否匹配。这两个坑我替你们踩过了,不用再踩一遍。