首页
/ openpilot 数据加载实战:Route 与 LogReader 如何读取路线日志(rlog/qlog)

openpilot 数据加载实战:Route 与 LogReader 如何读取路线日志(rlog/qlog)

2026-09-04 14:32:27作者:翟江哲Frasier

openpilot 的所有运行时数据——CAN 信号、传感器、控制量、事件——都以 Cap'n Proto 编码后压缩成 rlog/qlog 文件,按“路线(route)+ 分段(segment)”组织存储。本文以 tools/lib 库的 README 为骨架,结合 route.pylogreader.py 的源码实现,讲清楚如何用最少的代码定位并读取一段路线的日志、如何表达分段范围(segment range)与日志类型选择器(r/q),以及底层的多来源文件发现与解压机制,帮助你在回放、分析、挖掘 openpilot 数据时快速上手。

Route:路线命名规范与文件路径

命名格式

openpilot 中一条路线由 dongle id时间戳 唯一标识,二者以 | 分隔;若再加分段号,则以 /(或 --)衔接:

344c5c15b34f2d8a   /   2024-01-03--09-37-12   /     2:6    /       q
[   dongle id     ] [       timestamp        ] [ selector ]  [ query type]

route.pyRouteName 类(L206-L232)可以看到严格的长度约束:dongle_id 必须是 16 位十六进制字符,time_str 必须是 20 位(YYYY-MM-DD--HH-MM-SS),否则构造时直接断言失败。helpers.py 中的正则(helpers.py)定义了同一套格式:

  • DONGLE_ID:16 位小写十六进制 [a-f0-9]{16}
  • TIMESTAMP[0-9]{4}-[0-9]{2}-[0-9]{2}--[0-9]{2}-[0-9]{2}-[0-9]{2}
  • SEGMENT_RANGE:路线名后接可选的 --/ 分段切片,再接可选的 /q/r/a 选择器

分段类型与文件名

route.py 顶部的 FileName 常量(L14-L21)定义了每个分段中可能出现的数据文件:

类型 文件名 含义
RLOG rlog.zst / rlog.bz2 全量原始日志(rlog 保留完整 CAN、传感器等高频消息)
QLOG qlog.zst / qlog.bz2 精简日志(qlog 只保留低频关键消息,体积更小)
FCAMERA fcamera.hevc 前视道路相机视频
DCAMERA dcamera.hevc 驾驶员监测相机视频
ECAMERA ecamera.hevc 事件相机视频
QCAMERA qcamera.ts 时间戳化的相机流
BOOTLOG bootlog.zst / bootlog.bz2 启动日志

Route 类基本用法

README 给出的核心示例如下(可直接运行):

from openpilot.tools.lib.route import Route
from openpilot.tools.lib.logreader import LogReader

r = Route("a2a0ccea32023010|2023-07-27--13-01-19")

# 获取该路线所有 rlog 文件的路径列表(按分段编号对齐,缺失分段为 None)
print(r.log_paths())

# 获取道路相机(fcamera.hevc)文件路径
print(r.camera_paths())

# 用 LogReader 读取该路线第一个分段的 rlog
lr = LogReader(r.log_paths()[0])

# 打印日志中的全部消息
import codecs
codecs.register_error("strict", codecs.backslashreplace_errors)
for msg in lr:
  print(msg)

# 读取路线第二个分段的 qlog
lr = LogReader(r.log_paths()[1])

# 打印所有转向角值
for msg in lr:
  if msg.which() == "carState":
    print(msg.carState.steeringAngleDeg)

对照 route.py 的实现,Route 的行为是:

  • 构造时(__init__,L25-L32)二选一:
    • 远程模式:不带 data_dir 时,通过 CommaApi 请求 v1/route/<name>/files_get_segments_remote,L67-L97),把返回的文件 URL 按分段聚合为 Segment 列表。这依赖 auth_config.py 提供的 API token;
    • 本地模式:传入 data_dir 时,_get_segments_local(L99-L164)扫描目录,识别三种布局——explorer 下载的单文件(dongle|time--seg/rlog.bz2 这类文件名,由 RE.EXPLORER_FILE 匹配)、openpilot 分段目录、以及以 canonical_name 为名的目录(内含 0/1/ 数字子目录)。找不到任何分段时抛出 ValueError
  • log_paths() / qlog_paths() / camera_paths() / dcamera_paths() / ecamera_paths() / qcamera_paths() 均返回按分段编号对齐的列表{分段号: 路径} 映射后按 0..max_seg_number 展开,缺失分段的槽位为 None(L42-L64)。例如 log_paths()[0] 就是 README 示例中“第一个分段的 rlog”。
  • 每个 Segment(L167-L203)还带 events 属性:懒加载 {segment_url}/events.json,可用于查询该分段内的事件(如接管、警报)。

名称解析测试用例

test_route_library.py 用参数化用例固化了名称解析规则,值得作为格式参考:

输入 解析出的路线名 分段号 data_dir
a2a0ccea32023010|2023-07-27--13-01-19 a2a0ccea32023010|2023-07-27--13-01-19 -1(整条路线) None
a2a0ccea32023010/2023-07-27--13-01-19--1 同上 1 None
a2a0ccea32023010|2023-07-27--13-01-19/2 同上 2 None
/data/media/0/realdata/a2a0ccea32023010|2023-07-27--13-01-19 同上 -1 /data/media/0/realdata

注意 SegmentNameroute.py)会自动剥离本地目录前缀:当字符串中 | 之前出现 / 时,前半部分被识别为 data_dir,后半部分才是路线名。

分段范围(Segment Range)

这是 README 的重点:可以在路线名后直接追加 selectorquery type,表达“只读哪些分段、读哪种日志”。

分段切片

lr = LogReader("a2a0ccea32023010|2023-07-27--13-01-19/4")   # 第 4 个分段
lr = LogReader("a2a0ccea32023010|2023-07-27--13-01-19/4:6")  # 第 4、5 个分段
lr = LogReader("a2a0ccea32023010|2023-07-27--13-01-19/-1")  # 最后一个分段
lr = LogReader("a2a0ccea32023010|2023-07-27--13-01-19/:5")  # 前 5 个分段
lr = LogReader("a2a0ccea32023010|2023-07-27--13-01-19/1:")  # 除第一个分段外全部

切片语义与 Python slice 完全一致,由 SegmentRange.seg_idxsroute.py)实现:

  • seg_idxs 先按 RE.SLICEstart:end:step 三段式,均可为空)解析;
  • 只给了单个分段号(无 :)时返回 [start],负数会通过 get_max_seg_number_cached 查询路线最大分段号后换算(start += max_seg + 1);
  • 只有当切片用到相对索引(end 为空或为负、start 为负)时才需要向 API 请求 maxqlog 字段;纯正数闭区间如 /0:10 完全离线可算。

test_logreader.py 中的参数化用例覆盖了完整行为,包括步长切片:/2:-1 → 倒数第二个之前、/:10:2 → 前 10 个里隔一个取一个、/5::2 → 从第 5 个开始隔一个取一个。同一个测试文件还固化了非法输入(test_bad_ranges,L156-L170):///---/-4:--2/j/0:1:2:3 等都会触发断言错误,说明格式校验是构造期 fail-fast 的。

日志类型选择器

lr = LogReader("a2a0ccea32023010|2023-07-27--13-01-19/4/q") # 只读 qlog
lr = LogReader("a2a0ccea32023010|2023-07-27--13-01-19/4/r") # 只读 rlog(默认)

从源码看(logreader.pyReadMode 枚举与 helpers.py 正则中 [qra] 的 selector),完整支持四种模式:

选择器 ReadMode 行为
/r RLOG 只读 rlog,缺失即失败
/q QLOG 只读 qlog
/a AUTO 优先 rlog,缺失分段自动回退 qlog(无提示)
(无选择器时的默认参数) RLOG / AUTO / AUTO_INTERACTIVE LogReader(default_mode=...) 指定
/i AUTO_INTERACTIVE 优先 rlog,回退 qlog 前交互询问(y/N)

ReadMode.AUTO 的语义在测试 test_auto_modetest_logreader.py)中得到验证:当 rlog 不可用且应答“y”时,回退后的消息条数与直接读 qlog 完全一致;回答“n”则抛出 LogsUnavailable。README 只展示了 /q/r,而 /a 与交互式模式是源码中真实可用的补充能力。

LogReader:从字符串到消息流

构造与迭代

LogReaderlogreader.py)构造函数签名为:

LogReader(identifier: str | list[str],
          default_mode: ReadMode = ReadMode.RLOG,
          sources: list[Source] | None = None,
          sort_by_time=False,
          only_union_types=False)
  • identifier 可以是:本地文件路径、HTTP(S) URL、cd:/... 内部端点前缀、useradmin.comma.ai / connect.comma.ai 的分享链接,或上面任意形式的 segment range 字符串,也支持传入字符串列表一次加载多段;
  • sort_by_time=True 时按 logMonoTime 排序事件(_LogFileReader L119-L120)——这对回放多个分段拼接场景很有用;
  • only_union_types=True 时迭代器会跳过无法通过 which() 识别的损坏消息(L122-L131),测试 test_only_union_types(L261-L287)验证了它能把被写入非法 discriminant 的消息过滤掉。

迭代返回的是 Cap'n Proto 事件消息,常用辅助方法:

lr.filter("carState")   # 生成器:所有 carState 消息
lr.first("carParams")  # 第一条 carParams 消息
lr.time_series         # 消息转时间序列(log_time_series.msgs_to_time_series)

解压与解析细节

底层 _LogFileReader(L89-L131)做了三件事:

  1. 获取原始字节:通过 filereader.pyFileReader——HTTP URL 走 URLFile,本地路径走 DiskFilecd:/xxx 前缀会解析到 DATA_ENDPOINT 环境变量指向的内部端点(默认 http://data-raw.comma.internal/,L11)。
  2. 识别并解压:扩展名为 .bz2 或文件头为 BZh9 时用 bz2 解压;扩展名为 .zst 或文件头为 zstd magic(\x28\xB5\x2F\xFD)时用 zstandard 流式解压(L104-L108)。注释明确指出这是为了兼容旧版未压缩 rlog 与新旧两代压缩格式(FileName 中每类文件都列出 .zst.bz2 两个名字)。
  3. 批量解码capnp_log.Event.read_multiple_bytes(dat) 一次解出全部事件,包装为 CachedEventReader(L52-L86)。该类缓存了 which() 的结果——因为 Cap'n Proto 的属性访问开销较大且 which() 在分析代码里经常被多次调用;它还提供 __reduce__ 以支持快速 pickle(run_across_segments 多进程分发时用到)。

另外 logreader.pysave_log 提供了对称的写路径:把消息列表拼接成字节后按目标扩展名选择 bz2.compresszstd.compress(level=10) 写出,常用于裁剪/合并日志。

多进程处理长路线

对于分段很多的路线,run_across_segments(L275-L281)用 multiprocessing.Pool 把每个分段的处理函数并行执行,带 tqdm 进度条。CachedEventReader 的 pickle 支持正是为这一步服务的——每个子进程独立解码各自分段。测试 test_run_across_segmentstest_logreader.py)验证了并行处理后的消息总数与串行一致。

文件从哪来:多来源发现链

LogReader 真正“聪明”的地方在于把“字符串 → 具体文件 URL”的发现过程抽象成可插拔的来源(Source)。默认来源列表在构造函数中写死(logreader.py):

sources = [internal_source, comma_api_source, openpilotci_source, comma_car_segments_source]

对应 file_sources.py 中的四个实现:

来源 说明
internal_source 内部数据端点(DATA_ENDPOINT),构造 {dongle_id}/{log_id}/{seg}/{file} 形式的 URL;端点不可达时抛 InternalUnavailableException
comma_api_source route.pyRoute 类,经 comma API 的 v1/route/<name>/files 拿已校验存在的文件路径
openpilotci_source 从 openpilot CI 的 blob 存储(openpilotci.py)拼 URL
comma_car_segments_source 从 comma 车队公开 segment 数据集(comma_car_segments.py)拼 URL

auto_sourcelogreader.py)的解析流程是:

  1. 把标识符解析为 SegmentRange,算出需要的分段下标 seg_idxs
  2. 按选择器决定尝试顺序:/q 只试 qlog;其余模式先试 rlog,AUTO/AUTO_INTERACTIVE 模式再追加 qlog 回退;
  3. 对每种文件名逐个来源查询,用 file_exists(HEAD 校验 URL、os.path.exists 校验本地)取每个分段第一个存在的文件;
  4. 所有分段都找到才返回;否则 AUTO 模式打印 cloudlog.warning 回退、交互式模式询问用户,最终失败则抛出 LogsUnavailable,并在异常信息中附上每个来源的失败原因,方便排查。

分享链接的识别由 parse_indirect(L203-L224)处理:useradmin.comma.ai/?onebox=... 直接取 onebox 查询参数;connect.comma.ai 链接若带秒数,会自动把秒换算成分段区间(start//60 : end//60+1),因此可以直接把分享页 URL 喂给 LogReader。测试用例(test_logreader.py)甚至验证了 | 被 URL 编码成 %7C 的 onebox 链接也能正确解析。

视频读取:FrameReader 简介

README 提到与 LogReader 对应的 FrameReader 用于读取视频。framereader.py 的实现要点:

  • 仅支持 HEVC(fcamera.hevc 等),通过 hevc_indexvidindex.py)扫描帧头建立“帧类型 + 字节偏移”索引,再 ffprobe 取分辨率;
  • FfmpegDecoder(L102-L143)按 GOP 边界读取并解码:定位到目标帧所属 GOP 的起始 I 帧(get_gop_start),从该处 seek 后解码整段,丢弃目标帧之前的帧,支持 start_fidx / end_fidx / frame_skip 稀疏取帧;
  • FrameReader.get(fidx)(L151-L173)内置 LRU 帧缓存(默认容量 30),跨 GOP 跳帧时自动重置解码迭代器;
  • 解码走 ffmpeg 管道,hwaccel 默认 auto,输出为 rgb24numpy 数组(-1, h, w, 3)。

也就是说,日志(结构化消息)与视频(图像帧)分别由 LogReaderFrameReader 承担,二者共享同一套 Route 路径定位与 FileReader 远程读取基础设施。

使用前提与限制

基于当前仓库源码,使用这套工具链时需要注意以下事实:

  • 路线名格式严格:dongle id 16 位、时间戳 20 位(RouteName 的断言,route.py);|/ 可作为 dongle 与时间戳之间的分隔符(SegmentName 同时兼容两种),但 dongle id 本身必须是十六进制。
  • 远程访问需要 API 权限Route 远程模式、get_max_seg_number_cached(L308-L316)都要经 CommaApi,token 来自 auth_config.py;权限不足时异常信息会提示“确保你有该路线的访问权限或该路线是公开的”。
  • 负数/开区间切片会触发 API 查询test_slicing_api_calltest_logreader.py)验证了 /0/:2 不需要 API 而 /0:/-1、整条路线需要 API 获取 maxqlog,在离线环境下应优先使用正数闭区间。
  • 日志文件压缩格式:当前为 .zst(zstandard),兼容历史 .bz2_LogFileReader 遇到未知扩展名(且非旧版未压缩日志)会抛 ValueError
  • qlog 与 rlog 的信息量不同:qlog 是精简子集,测试 test_modes(L185-L189)断言同一分段 rlog 消息数 > 6 × qlog 消息数;需要完整 CAN/高频传感器数据时必须用 rlog。

小结

围绕 tools/lib/README.md 的核心内容可以归纳为三点:Route 负责把“dongle id + 时间戳”映射到每个分段的 rlog/qlog/相机文件路径;LogReader 负责把“分段范围字符串”解析成具体文件并解码为可迭代的消息流;segment range(/4/4:6/-1/1: 乃至步长切片)加 /q/r/a 选择器构成了路线数据的“查询语言”。配合 test_logreader.pytest_route_library.py 中的用例,可以完整验证每一种取法的行为,这也是在编写自己的数据回放、离线挖掘或 CI 校验脚本时的可靠参照。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
981
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384