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);
  • 控制主窗口的布局、菜单栏、工具栏;
  • 实现消息过滤、时间范围裁剪、话题订阅逻辑。

它只做三件事,且仅此三件:

  1. 声明能力 :通过 plugins.xml 告诉 rqt_bag :“我能处理哪些消息类型?”( get_message_types );
  2. 提供视图 :当用户在时间轴上右键某个话题时,提供一个可嵌入的 Qt 面板( get_view_class 返回 TopicMessageView 子类);
  3. 提供渲染 :当用户开启“缩略图”模式时,在时间轴下方绘制自定义图形( 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'] ,而非通配符 ['*'] ,这是一个深思熟虑的工程决策。表面上看, ['*'] 更“方便”,能让插件对所有话题生效。但实际项目中,这会带来三个严重问题:

  1. 性能灾难 rqt_bag 在加载 bag 文件时,会对每个话题检查所有已注册插件的 get_message_types() 。如果插件返回 ['*'] rqt_bag 就必须为每一个话题(哪怕只是 /tf /clock )都尝试初始化你的插件类。而你的插件可能依赖 diagnostic_msgs ,当遇到不相关的消息类型时, deserialize_message 可能抛出异常,导致整个插件系统崩溃。

  2. 语义污染 :想象一个 bag 文件里同时有 /diagnostics (诊断)和 /sensor/imu (IMU 数据)。如果你的插件对所有话题生效,那么当用户右键 /sensor/imu 时,“Awesome Diagnostic”这个名称就完全失去了意义,反而造成认知混乱。

  3. 维护陷阱 :未来当你需要为 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% 精确。它由三部分组成:

    1. rqt_bag_diagnostics_demo :包名( package.xml <name> 的值);
    2. the_plugin :Python 模块名(即 the_plugin.py 文件名,不含 .py );
    3. 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 是一个完美范例。它的工作流程是:

  1. 在插件初始化时( __init__ ),启动一个 QThread 后台线程;
  2. 后台线程遍历 bag 文件的所有消息,按固定时间间隔(如 0.1s )采样,对每个采样点计算出一个 QPixmap (例如,将一张图片缩略图绘制到指定尺寸);
  3. 将所有 QPixmap 按时间戳索引,存入一个 QMap dict 缓存;
  4. 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/ 目录。接下来,按顺序修改文件:

  1. 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>
  1. 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']), # <-- 新增这一行
],
  1. 创建 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 包名,与外层目录同名),并在其中创建两个文件:

  1. src/rqt_bag_diagnostics_demo/rqt_bag_diagnostics_demo/__init__.py :留空,仅创建文件。

  2. 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 分钟)

这是最关键的一步。请严格按照以下顺序操作,避免环境变量污染:

  1. 确保工作空间已 source
cd /path/to/your/workspace
source install/setup.bash
  1. 启动 rqt_bag 并加载 bag 文件
rqt_bag /tmp/diag_bag
  1. rqt_bag GUI 中操作
    • 在左侧话题列表中,找到 /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() 中的比较改为数值比较
Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐