【ROS2学习笔记】话题通信篇:话题通信再探
前言
本系列博文是本人的学习笔记,自用为主,不是教程,学习请移步其他大佬的相关教程。前几篇学习资源来自鱼香ROS大佬的详细教程,适合深入学习,但对本人这样的初学者不算友好,而且涉及python与C++混合编程内容,增加了学习成本,后续笔记将以@古月居的ROS2入门21讲为主,侵权即删。
一、基础概念
1. 话题通信核心概念
1.1 什么是话题?
话题是 ROS2 中节点(Node)之间传递数据的桥梁,用于实现节点间的单向数据传输。
- 场景举例:相机节点(A)获取图像数据,视频监控节点(B)需要显示图像,此时 A 通过话题将图像数据传给 B,B 才能完成可视化。
1.2 话题的通信模型:发布 / 订阅(Pub/Sub)
话题基于DDS(数据分发服务) 实现,核心是 “发布者 - 订阅者” 模式,类比生活中的 “微信公众号”:
ROS2 概念 | 微信公众号类比 | 作用说明 |
---|---|---|
话题名称 | 公众号名称(如 “古月居”) | 唯一标识,发布者和订阅者通过名称匹配 |
发布者(Publisher) | 公众号小编 | 发送数据的节点(主动推送数据) |
订阅者(Subscriber) | 公众号订阅用户 | 接收数据的节点(被动接收数据) |
消息类型 | 公众号文章格式(如图文) | 数据的固定格式(如字符串、图像),确保收发双方理解一致 |
1.3 话题的 2 个关键特性
(1)多对多通信
- 一个话题可以有多个发布者,也可以有多个订阅者:
- 例 1:2 个摇杆节点(发布者)同时向 “/cmd_vel” 话题发控制指令,1 个机器人节点(订阅者)接收。
- 例 2:1 个相机节点(发布者)向 “/image_raw” 话题发图像,2 个节点(订阅者:监控 + 识别)同时接收。
- ⚠️ 注意:多发布者时需设置优先级,避免机器人 “混乱”。
(2)异步通信
- 发布者发送数据后,不关心订阅者何时接收 / 处理(类似公众号发文章,小编不知道用户何时阅读)。
- 适用场景:周期发布的数据(如传感器数据、控制指令)。
- 不适用场景:需要即时响应的指令(如修改参数)。
1.4 消息接口(Msg):统一数据格式
- 问题:如果发布者发 “英文”,订阅者理解为 “中文”,数据会失效。
- 解决方案:消息(Msg) ——ROS2 定义的标准化数据格式,与编程语言无关。
- 标准消息:ROS2 已内置(如
std_msgs/msg/String
(字符串)、sensor_msgs/msg/Image
(图像))。 - 自定义消息:通过
.msg
文件定义(如自定义 “温度 + 湿度” 消息)。
- 标准消息:ROS2 已内置(如
- 例:
sensor_msgs/msg/Image
包含的信息:图像宽度、高度、像素格式(如 bgr8)、像素数据等。
二、项目实战(全流程)
1. 第一步:创建 ROS2 工作空间
工作空间是 ROS2 项目的 “容器”,用于存放功能包、代码、编译产物等,所有开发都需在工作空间中进行。标准 ROS2 工作空间结构如下:
工作空间名称/
├── build/ # 编译过程中生成的中间文件(自动创建)
├── install/ # 编译后的可执行文件、库、环境变量(自动创建)
├── log/ # 编译和运行日志(自动创建)
└── src/ # 核心目录:存放功能包(需手动创建)
1.1 创建工作空间目录
打开终端,执行以下命令,创建名为ros2_ws
的工作空间(名称可自定义,建议用_ws
后缀标识):
# 1. 进入用户主目录(也可选择其他路径,如~/code)
cd ~# 2. 创建工作空间根目录ros2_ws
mkdir -p ros2_ws/src # -p:确保父目录(ros2_ws)和子目录(src)都创建# 3. 进入工作空间根目录
cd ros2_ws
- 说明:
src
目录是必须手动创建的核心目录,后续所有功能包都放在src
下;build
、install
、log
会在编译时自动生成,无需手动创建。
1.2 初始化工作空间(可选,仅首次)
若工作空间是首次创建,可执行以下命令初始化(主要用于生成编译配置模板,对 Python 项目非强制,但建议执行以保证规范):
# 在ros2_ws目录下执行(确保当前路径是~/ros2_ws)
colcon build --symlink-install
colcon build
:ROS2 的编译工具,用于编译工作空间中的功能包;--symlink-install
:创建符号链接(而非复制文件),修改 Python 代码后无需重新编译,直接运行即可(对 Python 开发非常友好,建议每次编译都加此参数)。
执行后,会自动生成build
、install
、log
三个目录,工作空间初始化完成。
2. 第二步:创建功能包
功能包是 ROS2 代码的 “最小组织单元”,一个功能包可包含多个节点、话题、服务等。需在src
目录下创建功能包,且必须指定依赖项(如rclpy
(ROS2 Python 接口)、std_msgs
(标准消息)等)。
2.1 进入 src 目录并创建功能包
在ros2_ws
目录下,执行以下命令创建名为learning_topic
的功能包(名称对应之前的示例,便于衔接):
# 1. 进入src目录(当前路径:~/ros2_ws)
cd src# 2. 创建功能包:指定包名、依赖项
ros2 pkg create learning_topic \
--build-type ament_python \ # 编译类型:ament_python(Python项目),C++项目用ament_cmake
--dependencies rclpy std_msgs sensor_msgs cv_bridge opencv-python \ # 依赖项(空格分隔)
--node-name topic_helloworld_pub # 可选:创建包时自动生成一个节点文件(这里先指定发布者节点名)
- 依赖项说明(必须包含,否则后续代码会报错):
rclpy
:ROS2 Python 核心接口库(所有 Python 节点都需要);std_msgs
:ROS2 标准消息库(提供 String 等基础消息类型);sensor_msgs
:ROS2 传感器消息库(提供 Image 等图像消息类型,机器视觉案例需用);cv_bridge
:ROS2 与 OpenCV 的图像格式转换工具(机器视觉案例需用);opencv-python
:Python 版 OpenCV 库(机器视觉案例需用)。
2.2 查看功能包结构
创建完成后,src
目录下会生成learning_topic
文件夹,结构如下:
learning_topic/
├── learning_topic/ # 包的核心代码目录(与包名同名)
│ ├── __init__.py # Python包初始化文件(空文件即可)
│ └── topic_helloworld_pub.py # 自动生成的发布者节点文件(后续修改)
├── resource/ # 资源文件目录(如配置文件,暂不用)
│ └── learning_topic
├── test/ # 测试文件目录(暂不用)
├── package.xml # 功能包配置文件(指定依赖、作者等)
├── setup.cfg # Python包安装配置(自动生成,无需修改)
└── setup.py # 编译配置文件(指定入口点、依赖等,关键文件)
3. 第三步:编写代码(在功能包中)
接下来,在learning_topic/learning_topic/
目录下编写之前的 3 个核心节点代码(Hello World 发布者 / 订阅者、相机发布者 / 订阅者)。首先进入代码目录:
# 进入核心代码目录(当前路径:~/ros2_ws/src/learning_topic)
cd learning_topic
3.1 案例一:Hello World 话题通信(发布者 + 订阅者)
3.1.1 发布者代码(topic_helloworld_pub.py)
若之前创建包时已自动生成该文件,直接覆盖内容;若未生成,新建文件并写入以下代码:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2话题示例-发布“Hello World”话题
"""# 导入ROS2 Python核心接口库
import rclpy
# 导入ROS2节点类(所有自定义节点需继承此类)
from rclpy.node import Node
# 导入ROS2标准字符串消息类型
from std_msgs.msg import String"""
自定义发布者节点类,继承自Node
"""
class PublisherNode(Node):def __init__(self, name):# 调用父类Node的初始化方法,设置节点名称super().__init__(name)# 创建发布者对象:消息类型String、话题名"chatter"、队列长度10self.pub = self.create_publisher(String, "chatter", 10)# 创建定时器:周期0.5秒,触发回调函数timer_callbackself.timer = self.create_timer(0.5, self.timer_callback)# (新增)打印节点初始化日志,方便调试self.get_logger().info("Hello World发布者节点已启动!")# 定时器回调函数:定时发布消息def timer_callback(self):# 创建String消息对象msg = String()# 给消息赋值msg.data = "Hello World! 这是ROS2话题通信示例"# 发布消息self.pub.publish(msg)# 打印发布日志self.get_logger().info(f"已发布:{msg.data}")# 主入口函数
def main(args=None):# 初始化ROS2 Python接口rclpy.init(args=args)# 创建发布者节点对象node = PublisherNode("topic_helloworld_pub")# 循环运行节点(阻塞线程,确保回调函数执行)rclpy.spin(node)# 销毁节点(释放资源)node.destroy_node()# 关闭ROS2接口rclpy.shutdown()
3.1.2 订阅者代码(新建 topic_helloworld_sub.py)
在learning_topic/learning_topic/
目录下新建文件,命名为topic_helloworld_sub.py
,写入以下代码:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2话题示例-订阅“Hello World”话题
"""# 导入ROS2核心库
import rclpy
# 导入ROS2节点类
from rclpy.node import Node
# 导入标准字符串消息类型
from std_msgs.msg import String"""
自定义订阅者节点类,继承自Node
"""
class SubscriberNode(Node):def __init__(self, name):# 初始化节点,设置名称super().__init__(name)# 创建订阅者对象:消息类型String、话题名"chatter"、回调函数、队列长度10self.sub = self.create_subscription(String, "chatter", self.listener_callback, 10)# (新增)打印节点初始化日志self.get_logger().info("Hello World订阅者节点已启动!")# 订阅者回调函数:收到消息后执行def listener_callback(self, msg):# 打印收到的消息self.get_logger().info(f"已接收:{msg.data}")# 主入口函数
def main(args=None):# 初始化ROS2rclpy.init(args=args)# 创建订阅者节点node = SubscriberNode("topic_helloworld_sub")# 循环运行节点rclpy.spin(node)# 销毁节点node.destroy_node()# 关闭ROS2rclpy.shutdown()
3.2 案例二:机器视觉识别(相机发布者 + 订阅者)
3.2.1 相机发布者代码(新建 topic_webcam_pub.py)
在learning_topic/learning_topic/
目录下新建文件,命名为topic_webcam_pub.py
:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2话题示例-发布相机图像话题
"""import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image # 图像消息类型
from cv_bridge import CvBridge # ROS-OpenCV图像转换
import cv2 # OpenCV库class ImagePublisher(Node):def __init__(self, name):super().__init__(name)# 创建图像发布者:话题名"image_raw"(ROS2相机标准话题名)self.pub = self.create_publisher(Image, "image_raw", 10)# 定时器:0.1秒触发一次(10帧/秒)self.timer = self.create_timer(0.1, self.timer_callback)# 初始化相机(0表示默认摄像头,如笔记本内置相机)self.cap = cv2.VideoCapture(0)# 检查相机是否正常打开(新增:避免相机故障导致报错)if not self.cap.isOpened():self.get_logger().error("无法打开相机!请检查相机连接")return# 初始化图像转换对象self.cv_bridge = CvBridge()self.get_logger().info("相机发布者节点已启动!")def timer_callback(self):# 读取一帧图像:ret(是否成功)、frame(OpenCV格式图像)ret, frame = self.cap.read()if not ret:self.get_logger().warning("读取相机图像失败!")return# 将OpenCV图像(BGR)转换为ROS图像消息(格式bgr8)ros_image = self.cv_bridge.cv2_to_imgmsg(frame, "bgr8")# 发布图像消息self.pub.publish(ros_image)self.get_logger().info("正在发布相机图像...")def main(args=None):rclpy.init(args=args)node = ImagePublisher("topic_webcam_pub")rclpy.spin(node)# (新增)释放相机资源if node.cap.isOpened():node.cap.release()node.destroy_node()rclpy.shutdown()
3.2.2 红色物体识别订阅者代码(新建 topic_webcam_sub.py)
在learning_topic/learning_topic/
目录下新建文件,命名为topic_webcam_sub.py
:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2话题示例-订阅图像并识别红色物体
"""import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import cv2
import numpy as np # 数值计算库(处理颜色阈值)# 红色物体的HSV颜色范围(抗光照干扰,比RGB更适合颜色识别)
lower_red = np.array([0, 90, 128]) # 下限
upper_red = np.array([180, 255, 255])# 上限class ImageSubscriber(Node):def __init__(self, name):super().__init__(name)# 订阅图像话题"image_raw"self.sub = self.create_subscription(Image, "image_raw", self.listener_callback, 10)self.cv_bridge = CvBridge()self.get_logger().info("视觉识别订阅者节点已启动!")# 红色物体识别函数def detect_red_object(self, frame):# 将BGR图像转换为HSV图像hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)# 二值化:只保留红色区域(白色),其他区域黑色mask = cv2.inRange(hsv, lower_red, upper_red)# 检测红色区域的轮廓contours, _ = cv2.findContours(mask, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)# 过滤小轮廓(噪声)for cnt in contours:if len(cnt) < 150: # 轮廓像素数小于150,视为噪声continue# 绘制轮廓(绿色,线宽2)cv2.drawContours(frame, [cnt], -1, (0, 255, 0), 2)# 计算轮廓外接矩形,标记中心点x, y, w, h = cv2.boundingRect(cnt)center_x = x + w // 2center_y = y + h // 2# 绘制中心点(绿色,半径5,填充)cv2.circle(frame, (center_x, center_y), 5, (0, 255, 0), -1)# (新增)在图像上标注中心点坐标cv2.putText(frame, f"Center: ({center_x}, {center_y})", (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)# 显示处理后的图像cv2.imshow("Red Object Detection", frame)cv2.waitKey(10) # 等待10ms,避免图像窗口卡死# 订阅回调函数def listener_callback(self, msg):self.get_logger().info("正在接收相机图像...")# 将ROS图像消息转换为OpenCV图像frame = self.cv_bridge.imgmsg_to_cv2(msg, "bgr8")# 调用红色物体识别函数self.detect_red_object(frame)def main(args=None):rclpy.init(args=args)node = ImageSubscriber("topic_webcam_sub")rclpy.spin(node)# (新增)关闭OpenCV窗口,释放资源cv2.destroyAllWindows()node.destroy_node()rclpy.shutdown()
4. 第四步:配置编译文件(关键!让 ROS2 识别代码)
ROS2 需要通过setup.py
(Python 项目)和package.xml
(通用配置)识别节点和依赖,必须正确配置才能编译运行。
4.1 配置 setup.py(指定入口点,即终端命令)
打开~/ros2_ws/src/learning_topic/setup.py
文件,找到entry_points
字段,修改为以下内容(指定每个 Python 脚本对应的终端命令):
entry_points={'console_scripts': [# 格式:"终端命令名" = "包名.文件名:main函数"'topic_helloworld_pub = learning_topic.topic_helloworld_pub:main','topic_helloworld_sub = learning_topic.topic_helloworld_sub:main','topic_webcam_pub = learning_topic.topic_webcam_pub:main','topic_webcam_sub = learning_topic.topic_webcam_sub:main',],
},
- 作用:编译后,在终端输入
topic_helloworld_pub
,即可运行对应的 Python 脚本。
4.2 配置 package.xml(确认依赖项)
打开~/ros2_ws/src/learning_topic/package.xml
文件,确保<exec_depend>
标签包含所有依赖(创建包时已自动添加,若缺失需手动补充):
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3"><name>learning_topic</name><version>0.0.0</version><description>ROS2话题通信学习示例</description><maintainer email="your_email@example.com">your_name</maintainer><license>Apache-2.0</license><!-- 构建依赖(编译时需要) --><buildtool_depend>ament_cmake_python</buildtool_depend><depend>rclpy</depend><depend>std_msgs</depend><depend>sensor_msgs</depend><depend>cv_bridge</depend><!-- 运行依赖(运行时需要,关键!) --><exec_depend>opencv-python</exec_depend><test_depend>ament_lint_auto</test_depend><test_depend>ament_lint_common</test_depend><export><build_type>ament_python</build_type></export>
</package>
- 说明:
opencv-python
是 Python 库,需通过<exec_depend>
指定,否则运行时可能提示 “找不到 cv2 模块”。
4.3 赋予 Python 脚本可执行权限(关键!避免 “权限不足”)
ROS2 运行 Python 脚本需要可执行权限,在终端执行以下命令(进入learning_topic/learning_topic
目录):
# 进入代码目录(当前路径:~/ros2_ws/src/learning_topic/learning_topic)
cd ~/ros2_ws/src/learning_topic/learning_topic# 赋予所有Python脚本可执行权限
chmod +x *.py
- 验证:执行
ls -l
,若脚本名前有x
(如-rwxr-xr-x
),说明权限已添加。
5. 第五步:编译工作空间
回到工作空间根目录(~/ros2_ws
),执行编译命令,将代码编译为 ROS2 可识别的可执行文件:
# 进入工作空间根目录
cd ~/ros2_ws# 编译工作空间,添加--symlink-install(Python代码修改后无需重新编译)
colcon build --symlink-install --packages-select learning_topic
--packages-select learning_topic
:只编译learning_topic
功能包(避免编译所有功能包,节省时间);- 编译成功标志:终端最后显示 “[100%] Built target ...”,无红色错误信息。
6. 第六步:激活环境并运行节点
编译完成后,需要 “激活” 工作空间的环境变量,让 ROS2 找到编译后的功能包和节点。
6.1 激活环境
在~/ros2_ws
目录下执行以下命令:
# 激活当前工作空间的环境(每次打开新终端都需执行,或配置自动激活)
source install/setup.bash
- 自动激活配置(可选):若不想每次手动激活,可将命令添加到
~/.bashrc
文件:# 打开.bashrc文件 gedit ~/.bashrc # 在文件末尾添加一行: source ~/ros2_ws/install/setup.bash # 保存并关闭文件,执行以下命令使配置生效: source ~/.bashrc
6.2 运行 Hello World 案例
需要打开两个终端(均已激活环境):
终端 1:运行发布者
ros2 run learning_topic topic_helloworld_pub
- 预期输出:
[INFO] [topic_helloworld_pub] [1710000000.123456]:Hello World发布者节点已启动! [INFO] [topic_helloworld_pub] [1710000000.123456]:已发布:Hello World! 这是ROS2话题通信示例 [INFO] [topic_helloworld_pub] [1710000000.623456]:已发布:Hello World! 这是ROS2话题通信示例
终端 2:运行订阅者
ros2 run learning_topic topic_helloworld_sub
- 预期输出:
[INFO] [topic_helloworld_sub] [1710000001.123456]:Hello World订阅者节点已启动! [INFO] [topic_helloworld_sub] [1710000001.123456]:已接收:Hello World! 这是ROS2话题通信示例 [INFO] [topic_helloworld_sub] [1710000001.623456]:已接收:Hello World! 这是ROS2话题通信示例
6.3 运行机器视觉案例
同样需要两个终端,且确保电脑已连接相机(内置或 USB 外接):
终端 1:运行相机发布者
ros2 run learning_topic topic_webcam_pub
- 预期输出:
[INFO] [topic_webcam_pub] [1710000010.123456]:相机发布者节点已启动! [INFO] [topic_webcam_pub] [1710000010.223456]:正在发布相机图像... [INFO] [topic_webcam_pub] [1710000010.323456]:正在发布相机图像...
终端 2:运行视觉识别订阅者
ros2 run learning_topic topic_webcam_sub
- 预期效果:
- 弹出 “Red Object Detection” 窗口,显示相机实时画面;
- 将红色物体(如苹果、红色笔)对准相机,画面中会用绿色轮廓和中心点标记红色物体,并显示中心点坐标。
6.4 案例三:复用 ROS2 标准相机驱动(优化版)
若不想自己写相机发布者,可直接使用 ROS2 官方的usb_cam
驱动,步骤如下:
- 安装
usb_cam
驱动:sudo apt install ros-humble-usb-cam
- 终端 1:运行官方相机驱动(发布
/image_raw
话题):ros2 run usb_cam usb_cam_node_exe
- 终端 2:运行之前的视觉识别订阅者(无需修改代码):
topic_webcam_sub
- 效果与案例二完全一致,体现 ROS2 “模块化复用” 的优势。
7. 第七步:话题调试工具(验证通信是否正常)
若运行节点后无预期效果,可使用 ROS2 自带的话题工具排查问题,常用命令如下:
命令 | 功能 | 示例(针对 Hello World) |
---|---|---|
ros2 topic list | 查看当前所有活跃话题 | 输出:/chatter /image_raw /rosout |
ros2 topic info /chatter | 查看话题详情(发布者 / 订阅者数量、消息类型) | 输出:Type: std_msgs/msg/String ,Publishers: 1 |
ros2 topic echo /chatter | 实时打印话题数据 | 输出所有 “Hello World” 消息 |
ros2 topic hz /chatter | 查看话题发布频率 | 输出:average rate: 2.00 Hz (每 0.5 秒 1 次) |
8. 常见问题排查(初学者必看)
“命令未找到”(如 topic_helloworld_pub: command not found):
- 原因:未激活环境或编译失败;
- 解决:执行
source ~/ros2_ws/install/setup.bash
,或重新编译工作空间。
“找不到 cv2 模块”(ModuleNotFoundError: No module named 'cv2'):
- 原因:未安装 opencv-python;
- 解决:执行
pip install opencv-python
(若有多个 Python 版本,用pip3
)。
“相机无法打开”(无法打开相机!请检查相机连接):
- 原因:相机被占用(如其他软件打开了相机)或未连接;
- 解决:关闭其他使用相机的软件,重新插拔 USB 相机,或更换相机设备号(将
cv2.VideoCapture(0)
改为1
)。
“订阅者收不到消息”:
- 原因:话题名不匹配(如发布者用 “chatter”,订阅者用 “chat”)或消息类型不匹配;
- 解决:检查代码中话题名和消息类型是否完全一致,用
ros2 topic list
确认话题名。
9. 全流程总结
- 创建工作空间:
mkdir -p ~/ros2_ws/src
→cd ~/ros2_ws
; - 创建功能包:
cd src
→ros2 pkg create learning_topic --build-type ament_python --dependencies ...
; - 编写代码:在
learning_topic/learning_topic
目录下创建 Python 脚本; - 配置文件:修改
setup.py
(入口点)和package.xml
(依赖); - 赋予权限:
chmod +x *.py
; - 编译工作空间:
cd ~/ros2_ws
→colcon build --symlink-install --packages-select learning_topic
; - 激活环境:
source install/setup.bash
; - 运行节点:打开多个终端,分别运行发布者和订阅者;
- 调试:用
ros2 topic list/info/echo
排查问题。
按照以上步骤,即可从 “零” 开始完成 ROS2 话题通信的全流程开发,后续学习服务、动作等通信机制,也可复用此工作空间和功能包结构。