4. 话题通信编程:让节点真正“聊”起来
好,我们继续往前走。前面几章我们把 ROS2 的环境搭好了,也学会了怎么创建功能包和节点。但说实话,单个节点就像一台没联网的电脑——能干点本地活,但没啥大用。这一章,我们来聊聊 ROS2 最核心的通信方式:话题(Topic)。
说白了,话题通信就是“发布-订阅”模式。一个节点发布消息,其他节点订阅这个消息。发布者不管谁在听,订阅者也不管谁在发。这种解耦设计,我在做多传感器融合项目时深有体会——你想想看,激光雷达和摄像头各自发布数据,融合节点只管订阅,互不干扰,多清爽。
4.1 编写发布者(Publisher)节点
我们先从发布者开始。我习惯先写 C++ 版本,因为性能要求高的场景下 C++ 更靠谱。但别担心,Python 版本我也会一并给出。
4.1.1 C++ 发布者实现
假设我们要发布一个“字符串消息”,内容就是“Hello, ROS2!”。在 C++ 里,核心逻辑就这几步:
// publisher_node.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>("topic_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 的第二个参数是队列深度。嗯,这里要注意:队列满了之后,最老的消息会被丢弃。如果你做的是控制指令,丢消息可能出大问题。
4.1.2 Python 发布者实现
Python 版本更简洁,适合快速原型验证。我在做算法验证时经常先用 Python 写一遍。
# publisher_node.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, 'topic_chatter', 10)
timer_period = 1.0 # seconds
self.timer = self.create_timer(timer_period, self.timer_callback)
self.i = 0
def timer_callback(self):
msg = String()
msg.data = 'Hello, ROS2! %d' % self.i
self.get_logger().info('Publishing: "%s"' % msg.data)
self.publisher_.publish(msg)
self.i += 1
def main(args=None):
rclpy.init(args=args)
talker = Talker()
rclpy.spin(talker)
talker.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
create_timer 时,回调函数里千万别做耗时操作。有一次我在回调里写了个大文件,结果定时器直接卡死,整个节点像睡着了一样。后来我改用多线程才解决。
4.2 编写订阅者(Subscriber)节点
有发就有收。订阅者节点的写法跟发布者对称,只是角色反过来。
4.2.1 C++ 订阅者实现
// subscriber_node.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>(
"topic_chatter", 10, topic_callback);
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
你看,订阅者不需要循环,rclcpp::spin 会一直等待消息到来。回调函数里处理数据就行。
4.2.2 Python 订阅者实现
# subscriber_node.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,
'topic_chatter',
self.listener_callback,
10)
self.subscription # prevent unused variable warning
def listener_callback(self, msg):
self.get_logger().info('I heard: "%s"' % msg.data)
def main(args=None):
rclpy.init(args=args)
listener = Listener()
rclpy.spin(listener)
listener.destroy_node()
rclpy.close()
topic_chatter 写成 Topic_Chatter,结果两个节点各说各话,排查了半天。
4.3 自定义消息类型
内置的 String、Int32 这些类型,说实话,在真实项目中根本不够用。比如你要发布一个“机器人状态”消息,包含位置、速度、电池电量——这时候就需要自定义消息了。
4.3.1 定义 .msg 文件
在功能包的 msg 目录下创建 RobotStatus.msg:
# RobotStatus.msg
float32 x
float32 y
float32 theta
float32 linear_velocity
float32 angular_velocity
float32 battery_percentage
string status_description
然后修改 CMakeLists.txt 和 package.xml,把消息类型注册进去。这一步容易忘,我刚开始学的时候经常漏掉 rosidl_generate_interfaces 那行。
4.3.2 在代码中使用自定义消息
C++ 里包含头文件:
#include "your_package/msg/robot_status.hpp"
Python 里导入:
from your_package.msg import RobotStatus
用法跟内置消息完全一样。我个人建议,消息字段尽量用基本类型,别嵌套太深,否则序列化性能会下降。
4.4 话题通信的核心逻辑
为了让你更直观地理解,我画了一张图:
这张图展示了话题通信的精髓:发布者只管往话题里扔数据,订阅者只管从话题里取数据。两者完全解耦,互不知道对方的存在。我在做机器人集群控制时,一个发布者可以同时被十几个订阅者监听,代码一行都不用改。
4.5 编译与运行
写完了代码,怎么跑起来?
- 编译:在功能包目录下执行
colcon build --packages-select your_package - source 环境:
source install/setup.bash - 启动发布者:
ros2 run your_package talker - 启动订阅者:另开终端,
ros2 run your_package listener
ros2 topic list 查看所有活跃话题,用 ros2 topic echo /topic_chatter 直接看消息内容。这两个命令我几乎每天用。
4.6 常见问题与避坑
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 订阅者收不到消息 | 话题名称拼写不一致 | 用 ros2 topic list 核对名称 |
| 编译时报找不到消息类型 | 忘记在 CMakeLists.txt 中添加依赖 | 检查 find_package 和 rosidl_generate_interfaces |
| Python 节点启动后立即退出 | 没有调用 rclpy.spin() |
确保主循环里有 spin 调用 |
| 消息频率不稳定 | 回调函数里做了耗时操作 | 用多线程或异步处理 |
uint8[] 数组,结果在 Python 里死活解析不出来。后来发现是字节序问题——ROS2 默认用 little-endian,而我的数据源是 big-endian。从那以后,我定消息类型时都会先确认字节序。
好了,这一章的内容就到这里。话题通信是 ROS2 的基石,你把它吃透了,后面服务、动作这些通信方式学起来会快很多。记住:发布者只管发,订阅者只管收,中间的话题就是你们的“邮局”。