4、ROS2通信机制(一):话题通信——发布者与订阅者的实现,自定义消息接口
各位同学,欢迎来到第四章。从这一章开始,我们正式进入ROS2的核心——通信机制。
我个人一直觉得,ROS2最迷人的地方就是它的通信方式。它让机器人里的各个模块,比如传感器、控制器、执行器,能够像一群配合默契的队友一样协同工作。而话题通信,就是这群队友之间最常用、最基础的对讲机。
4.1 话题通信的本质:发布者与订阅者
先别急着敲代码,我们得先搞明白一个概念:什么是话题通信?
说白了,就是一个节点(Node)把数据扔到一个叫“话题”的通道里,另一个节点从这个通道里把数据取走。扔数据的叫发布者(Publisher),取数据的叫订阅者(Subscriber)。
你想想看,这就像电台广播。电台(发布者)不管有没有人在听,它只管播。听众(订阅者)只要调对频率(话题名),就能收到节目。而且,一个话题可以有多个发布者,也可以有多个订阅者,互不干扰。
- 异步通信:发布者和订阅者不需要同时在线,也不需要互相等待。
- 一对多/多对多:一个话题可以被多个节点订阅,也可以被多个节点发布。
- 匿名性:发布者不知道谁在订阅,订阅者也不知道谁在发布。它们只关心话题名和数据类型。
我在做第一个机器人项目时,就犯过一个低级错误。当时我让激光雷达的数据发布到 /scan 话题,但导航模块订阅的是 /laser_scan。结果机器人愣在原地不动,我还以为是硬件坏了。排查了半天才发现,就是话题名没对上。嗯,这里要注意,话题名必须完全一致,包括大小写和斜杠。
4.2 自定义消息接口:为什么需要它?
ROS2内置了很多标准消息类型,比如 std_msgs/String、geometry_msgs/Twist。但实际项目中,你总会遇到一些特殊的数据结构。比如,我要发布一个“机器人状态”消息,里面包含电池电量、当前速度、温度三个字段。标准消息里没有现成的,怎么办?
这时候就需要自定义消息接口了。
自定义消息,说白了就是你自己定义的一个数据结构。你可以把任意多个基础类型组合在一起,形成一个全新的消息类型。ROS2会帮你自动生成对应的C++或Python代码,你直接拿来用就行。
4.3 实战:创建自定义消息接口
好,理论讲完了,我们动手干。我会带你一步步创建一个自定义消息,然后实现发布者和订阅者。
4.3.1 创建消息定义文件
首先,在你的功能包目录下,创建一个 msg 文件夹。然后新建一个 RobotStatus.msg 文件。
# RobotStatus.msg
# 定义机器人状态消息
float32 battery_level # 电池电量,范围0.0~1.0
float32 current_speed # 当前速度,单位 m/s
float32 temperature # 核心温度,单位 摄氏度
string robot_name # 机器人名字
bool is_moving # 是否在移动
注意,每一行格式是:类型 字段名。ROS2支持的基础类型包括 int32、float64、string、bool 等,也支持数组和嵌套其他消息类型。
4.3.2 修改 CMakeLists.txt 和 package.xml
定义好 .msg 文件后,需要告诉编译系统去处理它。
修改 CMakeLists.txt:
# 找到消息生成依赖
find_package(rosidl_default_generators REQUIRED)
# 添加消息文件
set(msg_files
"msg/RobotStatus.msg"
)
# 生成消息代码
rosidl_generate_interfaces(${PROJECT_NAME}
${msg_files}
DEPENDENCIES std_msgs
)
# 确保依赖被导出
ament_export_dependencies(rosidl_default_runtime)
修改 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>
我曾经在这里卡了整整一个下午。编译总是报错找不到消息类型,后来发现是 package.xml 里忘了加 <member_of_group> 这一行。ROS2的依赖管理非常严格,少一行都不行。
4.3.3 编译并验证
回到工作空间根目录,执行编译:
colcon build --packages-select your_package_name
编译成功后,可以用 ros2 interface 命令查看消息定义:
ros2 interface show your_package_name/msg/RobotStatus
如果能看到你定义的字段,说明消息接口创建成功了。
4.4 实现发布者节点
消息接口有了,接下来我们写一个发布者节点,定时发布机器人状态。
# robot_status_publisher.py
import rclpy
from rclpy.node import Node
from your_package_name.msg import RobotStatus
class RobotStatusPublisher(Node):
def __init__(self):
super().__init__('robot_status_publisher')
# 创建发布者,话题名为 'robot_status',队列深度为 10
self.publisher_ = self.create_publisher(RobotStatus, 'robot_status', 10)
# 定时器,每 1 秒发布一次
self.timer = self.create_timer(1.0, self.timer_callback)
self.get_logger().info('机器人状态发布者已启动')
def timer_callback(self):
msg = RobotStatus()
msg.battery_level = 0.85
msg.current_speed = 0.5
msg.temperature = 36.5
msg.robot_name = 'TurtleBot3'
msg.is_moving = True
self.publisher_.publish(msg)
self.get_logger().info(f'发布状态: 电量={msg.battery_level}, 速度={msg.current_speed}')
def main(args=None):
rclpy.init(args=args)
node = RobotStatusPublisher()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
4.5 实现订阅者节点
有发布就有订阅。我们写一个订阅者节点,接收并打印机器人状态。
# robot_status_subscriber.py
import rclpy
from rclpy.node import Node
from your_package_name.msg import RobotStatus
class RobotStatusSubscriber(Node):
def __init__(self):
super().__init__('robot_status_subscriber')
# 创建订阅者,订阅 'robot_status' 话题
self.subscription = self.create_subscription(
RobotStatus,
'robot_status',
self.listener_callback,
10 # 队列深度
)
self.subscription # 防止被垃圾回收
self.get_logger().info('机器人状态订阅者已启动')
def listener_callback(self, msg):
self.get_logger().info(f'收到状态: 机器人={msg.robot_name}, '
f'电量={msg.battery_level:.2f}, '
f'速度={msg.current_speed:.2f}, '
f'温度={msg.temperature:.1f}, '
f'移动中={msg.is_moving}')
def main(args=None):
rclpy.init(args=args)
node = RobotStatusSubscriber()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
4.6 运行与测试
打开两个终端,分别运行发布者和订阅者:
# 终端1
ros2 run your_package_name robot_status_publisher
# 终端2
ros2 run your_package_name robot_status_subscriber
如果一切正常,订阅者终端会每秒打印一次机器人状态信息。
你也可以用命令行工具来验证:
# 查看话题列表
ros2 topic list
# 查看话题信息
ros2 topic info /robot_status
# 直接监听话题数据
ros2 topic echo /robot_status
4.7 本章知识体系
为了让你更直观地理解本章的知识结构,我画了一张图:
4.8 避坑指南与经验总结
最后,我把自己这些年踩过的坑总结一下,希望能帮你少走弯路:
- 话题名拼写错误:这是最常见的错误。ROS2不会报错,只是订阅者收不到数据。建议用
ros2 topic list确认话题名。 - 消息类型不匹配:发布者和订阅者必须使用完全相同的消息类型。我曾经把
std_msgs/String和std_msgs/string搞混过,大小写敏感。 - 忘记 source 环境:每次编译完新消息接口后,记得
source install/setup.bash,否则找不到自定义消息。 - 队列深度设置不当:如果订阅者处理速度慢,队列会溢出。可以用
rqt_graph工具观察节点间的通信状况。
ros2 topic echo 确认话题上有数据在流动,再用 ros2 topic hz 查看发布频率。如果数据流正常但订阅者没反应,那问题大概率在订阅者代码里。
好了,这一章的内容就到这里。话题通信是ROS2的基石,自定义消息接口则是你构建复杂系统的第一步。把这些基础打牢,后面的服务通信、动作通信学起来就会轻松很多。