深入解析:【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"、队列长度10
self.pub = self.create_publisher(String, "chatter", 10)
# 创建定时器:周期0.5秒,触发回调函数timer_callback
self.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"、回调函数、队列长度10
self.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):
# 初始化ROS2
rclpy.init(args=args)
# 创建订阅者节点
node = SubscriberNode("topic_helloworld_sub")
# 循环运行节点
rclpy.spin(node)
# 销毁节点
node.destroy_node()
# 关闭ROS2
rclpy.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 // 2
center_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>标签包含所有依赖(创建包时已自动添加,若缺失需手动补充):
learning_topic
0.0.0
ROS2话题通信学习示例
your_name
Apache-2.0
ament_cmake_python
rclpy
std_msgs
sensor_msgs
cv_bridge
opencv-python
ament_lint_auto
ament_lint_common
ament_python
- 说明:
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 话题通信的全流程开发,后续学习服务、动作等通信机制,也可复用此工作空间和功能包结构。

浙公网安备 33010602011771号