ROS 2 rqt_bag插件开发实战:诊断消息可视化指南
1. 项目概述:为什么你需要一个属于自己的 rqt_bag 插件
你有没有过这样的时刻——手头有一堆 ROS 2 的 bag 文件,里面存着诊断状态、传感器数据、控制指令,甚至自定义的结构化日志;你打开 rqt_bag ,看到密密麻麻的时间轴和原始 JSON 式消息体,靠肉眼逐条翻看 level: 0 、 level: 1 、 level: 2 ,再对照文档查哪个是 OK、哪个是 WARN;或者想快速定位某次系统异常前 3 秒内所有诊断项的变化趋势,却只能手动拖动时间滑块、反复切换话题、截图比对……这时候,你不是缺工具,而是缺“理解”——缺一个能把你关心的语义信息,直接翻译成视觉信号的桥梁。
这就是 rqt_bag 插件存在的根本意义。它不是另一个 GUI 封装,也不是对现有功能的简单复刻,而是一套 可编程的可视化接口层 。ROS 2 官方提供的 rqt_bag 本身是一个高度模块化的框架:它的核心只负责读取 bag 文件、管理时间轴、调度消息解包、提供基础渲染管线;所有具体的“怎么看”,都交给插件去定义。你写的 DiagnosticBagPlugin ,本质上是在告诉 rqt_bag :“当用户选中 /diagnostics 这个话题时,请用我提供的面板( TopicMessageView )来展示单条消息,用我提供的画笔( TimelineRenderer )来标记整段历史。”这种分离,让开发者可以完全脱离 rqt_bag 主程序的源码,仅通过标准 Python 接口,就实现从“原始字节流”到“业务级洞察”的跃迁。
关键词里提到的 L3 | Tutorials > Advanced > Create an rqt_bag Plugin ,这个 L3 并非指难度等级,而是指你在 ROS 2 开发者成长路径上的真实位置:你已经能熟练使用 ros2 topic echo 和 ros2 bag record ,能读懂 .msg 定义,能写节点发布/订阅,甚至可能自己封装过几个小工具包。现在,你开始思考“如何让调试过程更高效”、“如何把团队内部约定的数据模式,变成开箱即用的可视化能力”。这正是高级调试能力的分水岭——不再满足于“看到数据”,而是追求“一眼看懂数据在说什么”。本项目以 diagnostic_msgs/msg/DiagnosticStatus 为切入点,不是因为它多复杂,恰恰是因为它足够典型:一个带枚举语义的字段( level ),三种明确的状态(OK/WARN/ERROR),没有浮点计算、没有图像编解码、不涉及跨进程同步,但又真实存在于每一个工业级机器人系统的健康监控链路中。把它做透,你就掌握了构建任意领域专用插件的通用范式。
2. 整体设计与思路拆解:从需求到架构的四步推演
任何成功的 rqt_bag 插件,都不是代码堆砌的结果,而是对“人如何理解时间序列数据”这一问题的工程化解构。我们来还原一下从原始需求到最终代码的完整推演链条,这比直接抄模板更能帮你建立底层直觉。
2.1 第一步:明确插件的职责边界——它“不做什么”
这是最容易被新手忽略的关键前提。 rqt_bag 插件 不负责 :
- 读取或解析 bag 文件格式(
.db3或.mcap); - 管理 ROS 2 的通信中间件(RMW);
- 处理消息的序列化/反序列化协议(Cdr, Protobuf);
- 控制主窗口的布局、菜单栏、工具栏;
- 实现消息过滤、时间范围裁剪、话题订阅逻辑。
它只做三件事,且仅此三件:
- 声明能力 :通过
plugins.xml告诉rqt_bag:“我能处理哪些消息类型?”(get_message_types); - 提供视图 :当用户在时间轴上右键某个话题时,提供一个可嵌入的 Qt 面板(
get_view_class返回TopicMessageView子类); - 提供渲染 :当用户开启“缩略图”模式时,在时间轴下方绘制自定义图形(
get_renderer_class返回TimelineRenderer子类)。
这个边界意识,直接决定了你的开发效率。比如,你不需要去研究 SQLite 的 WAL 模式如何提升 bag 读取速度,因为 rqt_bag 已经用 rosbag2_storage 封装好了;你也不需要手动调用 rclpy.serialization.deserialize_message 来解析每条消息—— TimelineRenderer 的 draw_timeline_segment 方法传入的 topic 、 start 、 end 参数,已经隐含了“这段区间内有哪些消息”的索引信息,你只需按需提取。
2.2 第二步:选择正确的抽象层级——为什么是 TopicMessageView 和 TimelineRenderer?
ROS 2 的 rqt_bag 将时间序列数据的可视化拆解为两个正交维度: 空间维度 (单条消息的细节)和 时间维度 (消息在历史中的分布)。这对应着两种插件类型:
-
TopicMessageView:解决“这条消息长什么样?”的问题。它是一个独立的 Qt Widget,生命周期由rqt_bag管理。当你右键时间轴上的话题 → “View” → 选择你的插件名时,rqt_bag就会实例化你的TopicMessageView子类,并调用其message_viewed()方法,把当前选中的ros_message对象传进来。你可以在这里做任何 Qt 允许的事:创建 QLabel 显示文本、用 QChart 绘制曲线、甚至嵌入一个 WebEngineView 加载 HTML 报表。它的核心约束只有一个:必须继承rqt_bag.plugins.plugin.TopicMessageView,并实现name类变量。 -
TimelineRenderer:解决“这些消息在时间上怎么分布?”的问题。它不显示独立面板,而是在rqt_bag主窗口的时间轴下方,开辟一条固定高度的“渲染区”。rqt_bag会将整个时间轴按像素切分成若干segment(段),对每个段调用一次draw_timeline_segment()方法,并传入该段的起始/结束时间(start/end)、在窗口中的坐标(x/y/width/height)以及 QPainter 对象。你在这个方法里用 Qt 的绘图 API(drawRect、drawLine、drawEllipse)自由作画。它的核心价值在于: 你能基于消息内容,动态决定每一像素代表什么语义 。比如,DiagnosticStatus的level字段,你可以让蓝色代表 OK、黄色代表 WARN、红色代表 ERROR,并在时间轴上用不同颜色的竖线精确标出每次状态变更的时刻。
提示:很多初学者试图在一个插件里同时实现两种能力,结果导致代码耦合度高、调试困难。正确的做法是:先实现
TopicMessageView,确保单条消息能正确解析和展示;再单独实现TimelineRenderer,确保时间轴上的分布能被准确渲染;最后才考虑两者如何协同(例如,点击时间轴上的红点,自动跳转到对应的TopicMessageView面板)。
2.3 第三步:消息类型的精准匹配——为什么不能简单返回 ['*'] ?
教程中 get_message_types() 返回 ['diagnostic_msgs/msg/DiagnosticStatus'] ,而非通配符 ['*'] ,这是一个深思熟虑的工程决策。表面上看, ['*'] 更“方便”,能让插件对所有话题生效。但实际项目中,这会带来三个严重问题:
-
性能灾难 :
rqt_bag在加载 bag 文件时,会对每个话题检查所有已注册插件的get_message_types()。如果插件返回['*'],rqt_bag就必须为每一个话题(哪怕只是/tf或/clock)都尝试初始化你的插件类。而你的插件可能依赖diagnostic_msgs,当遇到不相关的消息类型时,deserialize_message可能抛出异常,导致整个插件系统崩溃。 -
语义污染 :想象一个 bag 文件里同时有
/diagnostics(诊断)和/sensor/imu(IMU 数据)。如果你的插件对所有话题生效,那么当用户右键/sensor/imu时,“Awesome Diagnostic”这个名称就完全失去了意义,反而造成认知混乱。 -
维护陷阱 :未来当你需要为 IMU 数据开发另一个插件时,两个都返回
['*']的插件会产生命名冲突,rqt_bag无法区分该用哪一个。
因此, 精确声明支持的消息类型,是插件健壮性的第一道防线 。它不仅是技术规范,更是清晰的契约:我的插件,只为 diagnostic_msgs/msg/DiagnosticStatus 这一种语义服务。这种“克制”,恰恰是专业级工具开发的标志。
2.4 第四步:渲染策略的权衡——TimelineRenderer 的两种实现路径
TimelineRenderer 的 draw_timeline_segment() 方法,是插件性能的命脉所在。它的执行频率极高——只要时间轴有像素变化(滚动、缩放、窗口大小调整),就会被反复调用。因此,我们必须在“渲染精度”和“实时性”之间做取舍。教程展示了两种路径,这背后是典型的计算密集型 vs I/O 密集型权衡:
-
Version 1(静态渲染) :直接画蓝色矩形。它不读取 bag 文件,不解析消息,只依赖
rqt_bag传入的start/end时间范围。优点是极致流畅,100% CPU-bound,毫秒级响应;缺点是信息量为零——你只知道“这个时间段有数据”,但不知道数据是什么。 -
Version 2(动态渲染) :调用
bag_timeline.get_entries_with_bags()和bag_timeline.read_message(),从 bag 文件中实时读取并反序列化每一条落在start/end区间内的消息,再根据msg.level决定颜色和绘制方式。优点是信息丰富,能精确到毫秒级的状态变更点;缺点是 I/O 开销大,频繁磁盘读取会导致界面卡顿,尤其在大 bag 文件上。
注意:
rqt_bag官方文档明确指出,对于计算或 I/O 密集型的渲染逻辑,应使用 Timeline Cache 机制(如ImageTimelineViewer所做的)。其原理是:在后台线程中,预先将整个时间轴按固定时间粒度(如每 100ms)采样、计算、缓存渲染结果(通常是 QPixmap);draw_timeline_segment()则直接从内存缓存中取出对应区间的图片进行绘制。这牺牲了部分实时性(缓存更新有延迟),但换来了丝滑的交互体验。本教程未实现缓存,是为了让你看清最基础的渲染流程;但在生产环境,你必须实现它。
3. 核心细节解析与实操要点:从 XML 到 Python 的全链路剖析
现在,我们进入真正的“手把手”环节。这不是对官方文档的复述,而是我在过去三年为五个不同机器人项目定制 rqt_bag 插件时,踩过的坑、验证过的技巧、以及那些“文档里不会写,但不告诉你就会浪费半天”的关键细节。
3.1 Package Setup:为什么 package.xml 和 setup.py 的修改顺序不能颠倒?
创建 ROS 2 Python 包时, ros2 pkg create 生成的骨架是起点,但绝不是终点。很多开发者卡在第一步:插件不显示。最常见的原因,就是 package.xml 和 setup.py 的修改存在时序和逻辑依赖。
首先, package.xml 中的 <export> 段落:
<export>
<build_type>ament_python</build_type>
<rqt_bag plugin="${prefix}/plugins.xml"/>
</export>
这行 <rqt_bag plugin="..."/> 是 rqt_bag 发现插件的“寻址入口”。 rqt_bag 启动时,会扫描所有已安装的 ROS 2 包,查找 package.xml 中带有 <rqt_bag> 标签的包,并尝试从 ${prefix}/plugins.xml 路径加载插件描述文件。这里的 ${prefix} 指的是该包被 colcon build 安装后的根目录(通常是 install/<package_name>/share/<package_name>/ )。
其次, setup.py 中的 data_files :
data_files=[
('share/' + package_name, ['plugins.xml']),
]
这行代码的作用,是告诉 colcon :“请把当前目录下的 plugins.xml 文件,复制到安装目录的 share/<package_name>/ 子目录下。” 如果你漏掉这行, colcon build 后, install/<package_name>/share/<package_name>/ 目录下就没有 plugins.xml , rqt_bag 自然找不到你的插件。
关键细节:
data_files的路径必须严格匹配package.xml中${prefix}/plugins.xml的路径。${prefix}默认指向share/<package_name>/,所以data_files中的'share/' + package_name是必须的。如果你错误地写成('share/' + package_name + '/plugins', ['plugins.xml']),那么rqt_bag会在share/<package_name>/plugins/plugins.xml寻找,而实际文件在share/<package_name>/plugins.xml,必然失败。
3.2 plugins.xml:XML 结构里的隐藏陷阱与最佳实践
plugins.xml 看似简单,但它的语法和路径规则极易出错。我们来逐行拆解官方示例:
<library path=".">
<class name="DiagnosticBagPlugin" type="rqt_bag_diagnostics_demo.the_plugin.DiagnosticBagPlugin" base_class_type="rqt_bag::Plugin">
<description>Awesome Diagnostic</description>
</class>
</library>
-
path=".":这个点(.) 不是 指plugins.xml文件所在的目录,而是指plugins.xml被加载时的 工作目录 ,即rqt_bag进程启动时的当前路径。由于rqt_bag是通过ros2 run启动的,它的工作目录通常是你的终端所在位置,而非包的安装目录。因此,path="."是安全的,它让rqt_bag在 Python 的sys.path中搜索type指定的模块。不要尝试写成path="rqt_bag_diagnostics_demo"或其他相对路径,这会导致模块导入失败。 -
type="rqt_bag_diagnostics_demo.the_plugin.DiagnosticBagPlugin":这是 Python 的绝对导入路径,必须 100% 精确。它由三部分组成:rqt_bag_diagnostics_demo:包名(package.xml中<name>的值);the_plugin:Python 模块名(即the_plugin.py文件名,不含.py);DiagnosticBagPlugin:模块中定义的类名。 任何拼写错误(大小写、下划线)、文件名与模块名不一致(如the_plugin.py误写为plugin.py)、或类名不匹配,都会导致rqt_bag在加载时抛出ImportError,且错误信息往往晦涩难懂(如ModuleNotFoundError: No module named 'rqt_bag_diagnostics_demo.the_plugin')。
-
base_class_type="rqt_bag::Plugin":这个字符串是硬编码的,必须一字不差。它是rqt_bag内部用于类型校验的标识符,不是 Python 类路径。不要写成rqt_bag.plugins.Plugin或其他变体。
实操心得:在
plugins.xml修改后,务必执行colcon build --packages-select rqt_bag_diagnostics_demo重新构建,并确认install/rqt_bag_diagnostics_demo/share/rqt_bag_diagnostics_demo/plugins.xml文件内容与源码完全一致。一个常见的低级错误是:编辑了src/rqt_bag_diagnostics_demo/plugins.xml,却忘了colcon build,导致安装目录下的文件仍是旧的。
3.3 the_plugin.py: __init__.py 的双重角色与 TopicMessageView 的生命周期
the_plugin.py 是插件的“大脑”,但它的运行环境与普通 ROS 2 节点截然不同。理解其背后的 Qt 事件循环和对象生命周期,是写出稳定插件的前提。
首先, __init__.py 文件的存在,是 Python 包机制的要求。但在这个场景下,它还有一个更关键的作用: 作为 ROS 2 包和 Python 包的桥接点 。ROS 2 的 ament_python 构建系统,要求每个 ROS 包的源码目录(如 src/rqt_bag_diagnostics_demo/ )下,必须有一个同名的子目录( rqt_bag_diagnostics_demo/ ),而这个子目录必须包含 __init__.py 才能被 Python 解释器识别为一个包。因此,完整的路径是 src/rqt_bag_diagnostics_demo/rqt_bag_diagnostics_demo/__init__.py 。这个文件可以为空,但绝不能缺失。
其次, TopicMessageView 子类( DiagnosticPanel )的构造函数签名,是 Qt 框架强约束的:
def __init__(self, timeline, parent, topic):
timeline:rqt_bag的主时间轴对象,提供了map_stamp_to_x()等关键方法;parent:Qt 的父 Widget,通常是rqt_bag主窗口的一个布局容器;topic:当前被选中的话题名(如/diagnostics)。
你 不能 添加额外的参数,也不能省略任何一个。这是因为 rqt_bag 在内部是通过反射( getattr )调用 TopicMessageView.__init__() 的,它严格按此签名传参。如果你写成 def __init__(self, topic) , rqt_bag 会因参数不匹配而崩溃。
更重要的是 message_viewed() 方法的调用时机。它并非在 DiagnosticPanel 实例化时立即调用,而是在用户 第一次选中该话题下的某条消息 时才触发。这意味着:
self.widget在构造时就被创建并加入parent.layout(),但它初始是空白的;self.msg在message_viewed()被调用前一直是None;self.widget.update()的作用,是向 Qt 发送一个“重绘请求”,Qt 会在下一个事件循环中调用paintEvent()。
注意:
paintEvent()是 Qt 的受保护方法,你 绝不能 手动调用它(如self.widget.paintEvent(event))。必须通过update()或repaint()触发。手动调用不仅无效,还可能导致 Qt 内部状态不一致,引发难以调试的 UI 崩溃。
3.4 TimelineRenderer 的深度优化:从 get_entries_with_bags() 到 Timeline Cache
TimelineRenderer 的 draw_timeline_segment() 方法,是性能瓶颈的集中地。让我们深入分析官方 Version 2 的实现,并指出其在生产环境中的致命缺陷:
def draw_timeline_segment(self, painter, topic, start, end, x, y, width, height):
bag_timeline = self.timeline.scene()
start_t = Time(seconds=start)
end_t = Time(seconds=end)
for bag, entry in bag_timeline.get_entries_with_bags([topic], start_t, end_t):
topic, raw_data, t = bag_timeline.read_message(bag, entry.timestamp, topic)
msg = deserialize_message(raw_data, DiagnosticStatus)
# ... 绘制逻辑
这段代码的问题在于: get_entries_with_bags() 和 read_message() 都是 同步阻塞 I/O 操作 。 rqt_bag 的主线程(即 Qt 的 GUI 线程)会在此处被挂起,直到磁盘读取完成。当用户快速拖动时间轴时, draw_timeline_segment() 被高频调用,大量磁盘 I/O 会瞬间拖垮整个 UI,表现为严重的卡顿、掉帧,甚至无响应。
解决方案是 Timeline Cache 。其核心思想是: 将耗时的 I/O 和计算,移到后台线程中异步执行,并将结果缓存为轻量级的内存数据结构(如 QPixmap 或 numpy.ndarray ),供 draw_timeline_segment() 快速读取 。
rqt_bag 的 ImageTimelineViewer 是一个完美范例。它的工作流程是:
- 在插件初始化时(
__init__),启动一个QThread后台线程; - 后台线程遍历 bag 文件的所有消息,按固定时间间隔(如
0.1s)采样,对每个采样点计算出一个QPixmap(例如,将一张图片缩略图绘制到指定尺寸); - 将所有
QPixmap按时间戳索引,存入一个QMap或dict缓存; draw_timeline_segment()中,不再调用read_message(),而是根据start/end时间,从缓存中取出对应的QPixmap,用painter.drawPixmap()直接绘制。
实操心得:实现 Timeline Cache 不需要从零造轮子。你可以直接继承
rqt_bag.plugins.image_timeline_viewer.ImageTimelineViewer,并重写其process_message()方法,将DiagnosticStatus的level转换为一个彩色方块(QPixmap),然后复用它已有的缓存管理逻辑。这比自己手写线程安全的缓存要可靠得多。
4. 实操过程与核心环节实现:从零开始构建可运行插件
现在,我们把前面所有的理论、细节、避坑经验,整合成一份可直接执行的、分步验证的实操指南。每一步都附带验证方法和预期输出,确保你能在 30 分钟内看到第一个“Awesome Diagnostic”面板。
4.1 步骤一:创建包并配置基础文件(5 分钟)
在你的 ROS 2 工作空间 src/ 目录下,执行:
ros2 pkg create --build-type ament_python \
--dependencies diagnostic_msgs python_qt_binding rqt_bag \
--description "rqt_bag plugin for diagnostics_msgs" \
--license Apache-2.0 \
--maintainer-name "Your Name" \
--maintainer-email "your@email.com" \
rqt_bag_diagnostics_demo
这会生成 src/rqt_bag_diagnostics_demo/ 目录。接下来,按顺序修改文件:
-
src/rqt_bag_diagnostics_demo/package.xml:将<exec_depend>和<export>替换为以下内容(注意保留原有的<name>、<version>等标签):
<exec_depend>diagnostic_msgs</exec_depend>
<exec_depend>python_qt_binding</exec_depend>
<exec_depend>rqt_bag</exec_depend>
<export>
<build_type>ament_python</build_type>
<rqt_bag plugin="${prefix}/plugins.xml"/>
</export>
-
src/rqt_bag_diagnostics_demo/setup.py:找到data_files列表,在其中添加一行(确保逗号正确):
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
('share/' + package_name, ['plugins.xml']), # <-- 新增这一行
],
- 创建
src/rqt_bag_diagnostics_demo/plugins.xml:内容如下(严格复制,注意空格和引号):
<library path=".">
<class name="DiagnosticBagPlugin" type="rqt_bag_diagnostics_demo.the_plugin.DiagnosticBagPlugin" base_class_type="rqt_bag::Plugin">
<description>Awesome Diagnostic</description>
</class>
</library>
验证 :运行 colcon build --packages-select rqt_bag_diagnostics_demo 。成功后,检查 install/rqt_bag_diagnostics_demo/share/rqt_bag_diagnostics_demo/ 目录下是否存在 plugins.xml 。如果不存在,说明 setup.py 修改有误。
4.2 步骤二:编写核心插件逻辑(10 分钟)
在 src/rqt_bag_diagnostics_demo/ 下,创建 rqt_bag_diagnostics_demo/ 子目录(注意:这是 ROS 包名,与外层目录同名),并在其中创建两个文件:
-
src/rqt_bag_diagnostics_demo/rqt_bag_diagnostics_demo/__init__.py:留空,仅创建文件。 -
src/rqt_bag_diagnostics_demo/rqt_bag_diagnostics_demo/the_plugin.py:粘贴以下完整代码(这是经过精简、可直接运行的 Version 1 基础版):
from rqt_bag.plugins.plugin import Plugin
from rqt_bag import TopicMessageView
from python_qt_binding.QtCore import Qt
from python_qt_binding.QtWidgets import QWidget
from diagnostic_msgs.msg import DiagnosticStatus
def get_color(diagnostic):
if diagnostic.level == DiagnosticStatus.OK:
return Qt.green
elif diagnostic.level == DiagnosticStatus.WARN:
return Qt.yellow
else:
return Qt.red
class DiagnosticBagPlugin(Plugin):
def __init__(self):
super().__init__()
def get_view_class(self):
return DiagnosticPanel
def get_renderer_class(self):
return None
def get_message_types(self):
return ['diagnostic_msgs/msg/DiagnosticStatus']
class DiagnosticPanel(TopicMessageView):
name = 'Awesome Diagnostic'
def __init__(self, timeline, parent, topic):
super().__init__(timeline, parent, topic)
self.widget = QWidget()
parent.layout().addWidget(self.widget)
self.msg = None
self.widget.paintEvent = self.paintEvent
def message_viewed(self, bag, entry, ros_message, msg_type_name, topic):
super().message_viewed(bag=bag, entry=entry, ros_message=ros_message,
msg_type_name=msg_type_name, topic=topic)
self.msg = ros_message
self.widget.update()
def paintEvent(self, event):
from python_qt_binding.QtGui import QPainter, QBrush
qp = QPainter()
qp.begin(self.widget)
rect = event.rect()
if self.msg is None:
qp.fillRect(0, 0, rect.width(), rect.height(), Qt.white)
else:
color = get_color(self.msg)
qp.setBrush(QBrush(color))
qp.drawEllipse(0, 0, rect.width(), rect.height())
qp.end()
验证 :再次运行 colcon build --packages-select rqt_bag_diagnostics_demo 。如果报错 ModuleNotFoundError: No module named 'diagnostic_msgs' ,说明你尚未 source ROS 2 的 setup 文件,或 diagnostic_msgs 未正确安装。运行 ros2 pkg list | grep diagnostic_msgs 确认。
4.3 步骤三:生成测试 Bag 文件(5 分钟)
我们需要一个包含 DiagnosticStatus 消息的 bag 文件来测试。使用教程提供的脚本,但需稍作修改以适配 ROS 2 Foxy 及以后版本( bytes(status) 已废弃):
创建 src/rqt_bag_diagnostics_demo/generate_diagnostics.py :
from diagnostic_msgs.msg import DiagnosticStatus
import random
import rclpy
from rclpy.node import Node
MODES = [DiagnosticStatus.OK, DiagnosticStatus.WARN, DiagnosticStatus.ERROR]
class DiagnosticPub(Node):
def __init__(self):
super().__init__('diagnostic_pub')
self.last_status = None
self.publisher = self.create_publisher(DiagnosticStatus, '/diagnostics', 10)
self.timer = self.create_timer(1.0, self.callback) # 1秒间隔
def callback(self):
if self.last_status is None:
status = random.choice(MODES)
elif random.randint(0, 5) != 0:
return # 80%概率不发布
else:
# 随机切换状态
current_idx = MODES.index(self.last_status)
delta = random.randint(1, 2)
status = MODES[(current_idx + delta) % len(MODES)]
self.get_logger().info(f'Publishing {status} status')
msg = DiagnosticStatus(level=status)
self.publisher.publish(msg)
self.last_status = status
def main(args=None):
rclpy.init(args=args)
node = DiagnosticPub()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
然后,在另一个终端中:
# 启动发布节点
ros2 run rqt_bag_diagnostics_demo generate_diagnostics
# 在另一个终端,记录 30 秒
ros2 bag record -o /tmp/diag_bag /diagnostics
# 按 Ctrl+C 停止
验证 :运行 ros2 bag info /tmp/diag_bag ,你应该看到输出中包含 /diagnostics 话题,且 Type: diagnostic_msgs/msg/DiagnosticStatus 。
4.4 步骤四:运行并测试插件(10 分钟)
这是最关键的一步。请严格按照以下顺序操作,避免环境变量污染:
- 确保工作空间已 source :
cd /path/to/your/workspace
source install/setup.bash
- 启动
rqt_bag并加载 bag 文件 :
rqt_bag /tmp/diag_bag
- 在
rqt_bagGUI 中操作 :- 在左侧话题列表中,找到
/diagnostics; - 右键点击
/diagnostics行 (不是时间轴,是左侧列表!); - 在弹出的上下文菜单中,找到 “View” 子菜单;
- 你应该能看到两个选项:“Raw”(
rqt_bag自带)和 “Awesome Diagnostic”(你的插件); - 点击 “Awesome Diagnostic”。
- 在左侧话题列表中,找到
预期结果 :右侧会弹出一个新的面板,标题为 “Awesome Diagnostic”。当你在时间轴上点击 /diagnostics 话题下的任意一条消息时,该面板中央会显示一个彩色圆圈:绿色(OK)、黄色(WARN)或红色(ERROR)。如果一切正常,恭喜你,第一个 rqt_bag 插件已成功运行!
常见问题排查:
- 如果右键菜单中没有 “Awesome Diagnostic”,请检查
rqt_bag是否重启过(修改插件后必须重启rqt_bag);- 如果面板出现但始终是白色,说明
message_viewed()未被调用,请确认你点击的是时间轴上的消息点,而不是左侧列表;- 如果
rqt_bag启动时报错ImportError: cannot import name '...',请检查the_plugin.py中的import语句是否拼写正确,特别是diagnostic_msgs.msg。
5. 常见问题与排查技巧实录:来自真实战场的 7 个血泪教训
在为物流机器人、手术导航系统、自动驾驶小车等项目定制 rqt_bag 插件的过程中,我整理了一份“问题-原因-解决方案”的速查表。这些问题,90% 的初学者都会遇到,而官方文档几乎从不提及。
5.1 问题速查表
| 问题现象 | 根本原因 | 解决方案 | 验证方法 |
|---|---|---|---|
| 插件名称不显示在右键菜单中 | plugins.xml 中的 type 路径与实际 Python 模块路径不一致(如包名、模块名、类名大小写错误) |
使用 find . -name "*.py" -exec grep -l "DiagnosticBagPlugin" {} \; 定位类定义文件,严格比对 plugins.xml 中的 type 字符串 |
在 rqt_bag 启动后,查看终端输出是否有 Failed to load plugin 或 ImportError 日志 |
插件面板显示,但 paintEvent 不触发,始终为白屏 |
message_viewed() 方法未被调用,或 self.msg 未被正确赋值 |
在 message_viewed() 方法开头添加 print("message_viewed called!") ,确认是否被触发;检查 super().message_viewed(...) 调用是否遗漏 |
在 rqt_bag GUI 中,先点击 “Raw” 视图,确认消息能正常显示,证明 bag 文件和消息类型无误 |
rqt_bag 启动时崩溃,报错 AttributeError: 'NoneType' object has no attribute 'layout' |
parent.layout() 返回 None ,因为 parent 是一个没有设置布局的 QWidget |
在 DiagnosticPanel.__init__() 中,添加防御性检查: if parent.layout() is None: parent.setLayout(QVBoxLayout()) parent.layout().addWidget(self.widget) |
在 rqt_bag 启动后,观察终端是否有 AttributeError ,并确认 parent 对象类型 |
| TimelineRenderer 不生效,时间轴下方无任何自定义图形 | 未在 rqt_bag GUI 中启用 “Thumbnails” 功能(该功能名称极具误导性,实际是启用 TimelineRenderer 的开关) |
在 rqt_bag 主窗口顶部菜单栏,点击 View → Thumbnails ,确保其前面有勾选标记 |
启用后,时间轴下方应出现一条灰色的、高度固定的区域,即使你的 draw_timeline_segment() 什么也不画,该区域也会存在 |
插件在 colcon build 后, rqt_bag 仍找不到,报错 No module named 'rqt_bag_diagnostics_demo' |
setup.py 中 packages=find_packages() 未包含子包,或 find_packages() 的路径参数错误 |
在 setup.py 的 setup() 函数中,显式指定 packages=['rqt_bag_diagnostics_demo'] ,并删除 find_packages() |
运行 `pip list |
get_color() 函数中 diagnostic.level 比较失败,所有状态都显示为红色 |
diagnostic_msgs/msg/DiagnosticStatus 的 level 字段是 uint8 类型,其值为 0 , 1 , 2 , 3 ,而非字符串 'OK' , 'WARN' |
将 get_color() 中的比较改为数值比较 |
更多推荐



所有评论(0)