openpilot 的 juggle.py 详解:用 PlotJuggler 可视化行驶日志、解析 CAN 与实时流数据
openpilot 通过 tools/plotjuggler/ 目录下的辅助脚本 juggle.py,把 PlotJuggler(一个通用的时间序列可视化工具)与 openpilot 的 cereal 日志格式打通:自动下载 PlotJuggler 及其 openpilot 专用插件、解析路由/段日志、推断车型 DBC 以解码 CAN 信号,并支持从 comma 设备实时串流数据。读完本文,你将掌握 juggle.py 的全部命令行参数、14 个预置布局(Layout)的用途与结构,以及从 LogReader 到 .rlog 再到 PlotJuggler 插件的完整源码调用链,能够独立完成从"回放一次行驶记录"到"从车机实时观察控制量"的完整调试工作流。
PlotJuggler 在 openpilot 中的定位
openpilot 的日志是 Cap'n Proto 序列化、经 zstd 压缩的 cereal 消息流(rlog/qlog),直接读取并不直观。juggle.py 解决的核心问题是:把这些日志转换成 PlotJuggler 能加载的时间序列视图。comma 团队为 PlotJuggler 编写了两个关键插件(随 --install 一起安装到 openpilot/tools/plotjuggler/bin/ 插件目录):
- DataLoad Rlog:加载并解析 openpilot 的 rlog 文件;
- Cereal Subscriber:以 ZeroMQ 订阅 cereal 消息总线,实现实时流式绘图。
这两个插件 ID 直接写死在仓库自带的每个布局文件中,例如 tuning.xml 末尾的:
<Plugins>
<plugin ID="DataLoad Rlog"/>
<plugin ID="Cereal Subscriber"/>
</Plugins>
因此布局文件与本地安装的插件是配套关系——这也是 --install 需要同时安装 PlotJuggler 和插件的原因。
安装:下载 PlotJuggler 与插件
在 配置好 openpilot 开发环境 之后,执行:
cd openpilot/tools/plotjuggler && ./juggle.py --install
从 juggle.py 的 install() 实现(L43-L61)可以看到安装细节:
- 以
platform.system() + platform.machine()组合出平台标识,仅支持Linux-x86_64、Linux-aarch64、Darwin-arm64三种平台,其他平台直接抛异常; - 删除并重建
openpilot/tools/plotjuggler/bin/安装目录; - 从 commaai/PlotJuggler 的
releases/download/latest/<平台>.tar.gz流式下载(1MB 分块),解包到bin/目录。
脚本还有两条"自愈"逻辑,保证日常无需手动维护版本:
- 若
bin/plotjuggler不存在,自动提示并安装(L158-L160); - 若已安装版本低于
MINIMUM_PLOTJUGGLER_VERSION = (3, 5, 2),通过执行plotjuggler -v解析版本号后自动更新(L162-L164、L64-L67)。
命令行参数全解
./juggle.py -h 的输出(README 中的快照)与实际 argparse 定义一致,此外当前源码中还有一个 README 帮助文本未列出的 --no-migration 选项。以 juggle.py 的参数定义 为准,完整参数如下:
| 参数 | 类型 | 说明 |
|---|---|---|
route_or_segment_name |
位置参数(可选) | 要绘制的路由或段名称,接受 cabana 分享 URL;不提供且不指定 --demo 时无法绘图 |
--demo |
开关 | 使用内置演示路由 5beb9b58bd12b691/0000010a--a51155e496(即源码常量 DEMO_ROUTE,L23) |
--can |
开关 | 解析 CAN 数据(默认过滤掉 can/sendcan 消息) |
--stream |
开关 | 以流式模式启动 PlotJuggler(不加载历史数据文件) |
--layout [LAYOUT] |
可选值 | 使用预定义布局文件启动,如 --layout layouts/tuning.xml |
--install |
开关 | 安装或更新 PlotJuggler + 插件(执行完即退出) |
--dbc DBC |
值 | 指定解析 CAN 数据所用的 DBC 文件名;不指定时从日志自动推断 |
--no-migration |
开关 | 跳过旧版日志字段迁移(源码 L138,README 帮助文本未列出) |
另外注意:不带任何参数直接运行 ./juggle.py 时,会先打印一段紫色横幅(指向后继工具 JotPluggler,见文末),再打印帮助并退出(L144-L148)。
路由与段的命名约定
README 给出了三类典型用法(5beb9b58bd12b691 为设备名,0000010a--a51155e496 为路由 ID,两者均沿用演示路由):
# 整个路由(跨所有段合并绘制)
./juggle.py "5beb9b58bd12b691/0000010a--a51155e496"
# 单个段
./juggle.py "5beb9b58bd12b691/0000010a--a51155e496/1"
# 单个段,使用 qlog(调试日志)而非 rlog
./juggle.py "5beb9b58bd12b691/0000010a--a51155e496/1/q"
# 段范围
./juggle.py "5beb9b58bd12b691/0000010a--a51155e496/0:1"
源码流程:从路由名到绘图窗口
juggle_route()(juggle.py L108-L128)串起了完整流程,值得逐步拆解:
- 加载日志:
LogReader(route_or_segment_name, default_mode=ReadMode.AUTO_INTERACTIVE)。AUTO_INTERACTIVE模式定义在 logreader.py 中,语义是"优先读取 rlog,缺失时与用户交互确认后回退到 qlog",所以传入/1/q这样的后缀即可显式指定 qlog。 - 多进程聚合:
lr.run_across_segments(24, partial(process, can))用最多 24 个进程跨段并行处理。process()(L104-L105)的过滤规则是:不带--can时丢弃can、sendcan以及所有customReserved*开头的消息——CAN 原始报文体积大且默认无意义,故被排除;带--can时全部保留。 - 日志迁移:除非指定
--no-migration,调用 migration.py 中的migrate_all()把旧版字段名重映射为当前版本,保证旧路由也能用新版布局打开。 - DBC 自动推断(见下节)。
- 落盘临时 rlog:
save_log(tmp.name, all_data, compress=False)把聚合后的消息写入openpilot/tools/plotjuggler/下以.rlog为后缀的临时文件(解压状态),供 DataLoad Rlog 插件加载;随后start_juggler()启动图形界面。
start_juggler()(L70-L101)在启动前做了一件容易忽略但很关键的事:在临时目录中拼出 PlotJuggler 插件所需的 Cap'n Proto 模式文件——复制 log.capnp、deprecated.capnp、custom.capnp 并重写其中的 import 路径(/include/c++.capnp → ./include/c++.capnp,/car.capnp → car.capnp),再符号链接 cereal 的 include/ 目录与 opendbc_repo/opendbc,最后把该临时目录设为插件的 BASEDIR 环境变量。这使插件能在不依赖完整构建产物的情况下正确反序列化消息。最终执行的命令形如:
bin/plotjuggler --buffer_size 1000 --plugin_folders <bin> -d <临时rlog> -l <布局文件> --window_title "<路由名> (<车型平台>)"
其中 --buffer_size 1000 来自常量 MAX_STREAMING_BUFFER_SIZE,--window_title 会附带推断出的车型平台名,便于多窗口调试时区分。
解析 CAN 数据:--can 与 DBC
加上 --can 后,CAN 原始报文会进入绘图数据源,但要把报文解码成有意义的信号,还需要 DBC 文件。juggle_route() 中的推断逻辑(L116-L123)为:
- 若未显式
--dbc,取日志中第一条carParams消息,用CP.carFingerprint经MIGRATION表(来自 opendbc 子模块的opendbc.car.fingerprints)映射出车型平台; - 再通过
openpilot.tools.cabana.dbc.generate_dbc_json中的generate_dbc_dict()查该平台对应的 DBC 名,并以环境变量DBC_NAME传给 PlotJuggler(L73-L76),由 CAN 解析插件据此解码。
因此通常无需手动指定 DBC;当路由中缺少 carParams(如纯 CAN 录制)或推断失败时(此时会打印 "Failed to get DBC name from logs!"),才需要用 --dbc 显式给出 DBC 文件名或路径(本地路径会被转换为绝对路径)。
实时流式数据:--stream
--stream 分支跳过一切日志加载,直接 start_juggler(layout=args.layout) 启动 PlotJuggler,在 Streaming 下拉菜单中选择 Cereal Subscriber 插件并点击 Start,即可订阅本机 cereal 总线实时绘图(L166-L167)。数据源有两种:
从 comma 设备串流到笔记本
- 在 comma 设备上开启 Wi-Fi 热点(tethering);
- SSH 进入设备后执行
cd /data/openpilot && ./openpilot/cereal/messaging/bridge——该 bridge 二进制由 bridge.cc 构建,作用是把设备本地的共享内存消息总线桥接为 ZeroMQ 对外发布; - 笔记本连接该热点;
- 执行
ZMQ=1 ./juggle.py --stream,找到Cereal Subscriber插件并点击Start。ZMQ=1环境变量告诉消息层走 ZeroMQ 传输。
从本地回放串流
如果在 PC 上用 replay 工具回放路由,直接运行 ./juggle.py --stream 并启动 cereal subscriber 即可——replay 进程发布的本地消息同样会被订阅到。
快速演示:--demo
完成安装后,最快看到效果的方式是:
./juggle.py --demo --layout=layouts/tuning.xml
这会下载/复用内置演示路由 DEMO_ROUTE,加载调参布局打开 PlotJuggler。仓库的自动化测试 test_plotjuggler.py 也以 "{DEMO_ROUTE}/:2"(前 3 个段)为输入验证整条链路:在无显示环境下以 QT_QPA_PLATFORM=offscreen 启动 juggle.py,等待 stderr 出现插件打印的 Done reading Rlog data(180 秒超时),再确认进程没有崩溃、输出中不含 Raw file read failed。该测试依赖 Qt(qmake),未安装时自动跳过。
Layouts:14 个预置布局及其结构
openpilot/tools/plotjuggler/layouts/ 目录内置了 14 个开箱即用的布局,README 鼓励社区把自己的有用布局上游化。各布局的适用场景可从文件名与内容对应:
| 布局文件 | 典型用途 |
|---|---|
| tuning.xml | 横向/纵向整体调参,生成调参 PR 所需图表 |
| longitudinal.xml | 纵向控制:aEgo、MPC 加速度、执行器输出、油门状态 |
| torque-controller.xml | 转向扭矩控制器深入调试(仓库中最大的布局,249 行) |
| max-torque-debug.xml | 最大扭矩/限幅问题排查 |
| CAN-bus-debug.xml | 三条 CAN 总线的 RX/TX/错误计数(对累计值做 Derivative 变换得到速率) |
| can-states.xml | CAN 总线状态 |
| controls_mismatch_debug.xml | controls 输出不一致排查 |
| locationd_debug.xml | 定位模块(locationd)调试 |
| gps.xml / gps_vs_llk.xml / ublox-debug.xml | GNSS 信号、GPS 与里程计对比、u-blox 调试 |
| camera-timings.xml | 相机帧时序 |
| system_lag_debug.xml | 系统时延/卡顿排查 |
| thermal_debug.xml | 热状态排查 |
以 tuning.xml 为例拆解布局文件
以 --layout layouts/tuning.xml 为例,布局文件是一个 PlotJuggler 的 XML 工程,包含三大块:
tabbed_widget:主窗口标签页结构。tuning 布局含Lateral、Longitudinal、Lateral Debug三个 Tab,每个 Tab 内用DockSplitter把若干DockArea均分,每个DockArea是一个TimeSeries折线图,<curve>直接引用 cereal 字段路径,例如/carState/vEgo、/carControl/orientationNED/0(横滚角)、/controlsState/lateralControlState/pidState/saturated(横向 PID 饱和标志)等。customMathEquations:自定义数学公式区。tuning 布局中的每个公式片段(snippet)都实现了统一的"engage_delay = 5 秒"逻辑:当驾驶员干预(steeringPressed)或系统未接管(enabled == 0)时记录last_bad_time,只有距最后一次干预超过 5 秒的时段才返回真实曲率/加速度,否则返回 0。这样调参图表只反映"纯 openpilot 控制"的片段,排除人为干扰。例如engaged curvature plan取/modelV2/action/desiredCurvature,engaged_accel_plan取/longitudinalPlan/accels/0并在brakePressed/gasPressed非零时置零。Plugins:声明该布局依赖DataLoad Rlog与Cereal Subscriber两个插件。
测试代码 test_layouts 还规定了一个上游约束:布局文件中不允许残留 fileInfo 或 previouslyLoaded_Datafiles 字段——因为 PlotJuggler 在加载引用了先前数据文件的布局时会告警,这些内容必须在提交前剔除。
与 JotPluggler 的过渡关系
需要说明的一点是:当前仓库的 juggle.py 每次运行时都会打印横幅,提示 "JotPluggler is the future" 与 "PlotJuggler will be deleted soon"(juggle.py L31-L40),并给出等价命令 ./openpilot/tools/jotpluggler/jotpluggler --demo --layout tuning。从源码结构看,openpilot/tools/jotpluggler/ 是一套 C++ 实现的新版可视化工具,支持相同的 demo 与 layout 概念;本文所述的 PlotJuggler 工作流在过渡期内仍然完整可用,且其日志解析(LogReader、迁移、DBC 推断)逻辑是理解 openpilot 日志生态的良好切入点。
速查小结
- 安装/更新:
cd openpilot/tools/plotjuggler && ./juggle.py --install(支持 Linux x86_64/aarch64 与 macOS arm64,自动校验最低版本 3.5.2); - 回放绘图:
./juggle.py "<设备名>/<路由ID>",支持/段号、/段号/q、/起:止后缀,接受 cabana 分享 URL; - CAN 调试:
--can [--dbc <名>],DBC 默认由首条carParams自动推断; - 实时流:设备侧跑
cereal/messaging/bridge+ 笔记本ZMQ=1 ./juggle.py --stream,或本地 replay +./juggle.py --stream,均通过Cereal Subscriber插件订阅; - 快速体验:
./juggle.py --demo --layout=layouts/tuning.xml; - 核心源码入口:juggle.py、布局目录 layouts/、回归测试 test_plotjuggler.py、日志读取层 tools/lib/logreader.py。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00