openpilot 数据加载实战:Route 与 LogReader 如何读取路线日志(rlog/qlog)
openpilot 的所有运行时数据——CAN 信号、传感器、控制量、事件——都以 Cap'n Proto 编码后压缩成 rlog/qlog 文件,按“路线(route)+ 分段(segment)”组织存储。本文以 tools/lib 库的 README 为骨架,结合 route.py 与 logreader.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.py 的 RouteName 类(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 |
注意 SegmentName(route.py)会自动剥离本地目录前缀:当字符串中 | 之前出现 / 时,前半部分被识别为 data_dir,后半部分才是路线名。
分段范围(Segment Range)
这是 README 的重点:可以在路线名后直接追加 selector 与 query 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_idxs(route.py)实现:
seg_idxs先按RE.SLICE(start: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.py 的 ReadMode 枚举与 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_mode(test_logreader.py)中得到验证:当 rlog 不可用且应答“y”时,回退后的消息条数与直接读 qlog 完全一致;回答“n”则抛出 LogsUnavailable。README 只展示了 /q 与 /r,而 /a 与交互式模式是源码中真实可用的补充能力。
LogReader:从字符串到消息流
构造与迭代
LogReader(logreader.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排序事件(_LogFileReaderL119-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)做了三件事:
- 获取原始字节:通过 filereader.py 的
FileReader——HTTP URL 走URLFile,本地路径走DiskFile;cd:/xxx前缀会解析到DATA_ENDPOINT环境变量指向的内部端点(默认http://data-raw.comma.internal/,L11)。 - 识别并解压:扩展名为
.bz2或文件头为BZh9时用bz2解压;扩展名为.zst或文件头为 zstd magic(\x28\xB5\x2F\xFD)时用zstandard流式解压(L104-L108)。注释明确指出这是为了兼容旧版未压缩 rlog 与新旧两代压缩格式(FileName中每类文件都列出.zst与.bz2两个名字)。 - 批量解码:
capnp_log.Event.read_multiple_bytes(dat)一次解出全部事件,包装为CachedEventReader(L52-L86)。该类缓存了which()的结果——因为 Cap'n Proto 的属性访问开销较大且which()在分析代码里经常被多次调用;它还提供__reduce__以支持快速 pickle(run_across_segments多进程分发时用到)。
另外 logreader.py 的 save_log 提供了对称的写路径:把消息列表拼接成字节后按目标扩展名选择 bz2.compress 或 zstd.compress(level=10) 写出,常用于裁剪/合并日志。
多进程处理长路线
对于分段很多的路线,run_across_segments(L275-L281)用 multiprocessing.Pool 把每个分段的处理函数并行执行,带 tqdm 进度条。CachedEventReader 的 pickle 支持正是为这一步服务的——每个子进程独立解码各自分段。测试 test_run_across_segments(test_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.py 的 Route 类,经 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_source(logreader.py)的解析流程是:
- 把标识符解析为
SegmentRange,算出需要的分段下标seg_idxs; - 按选择器决定尝试顺序:
/q只试 qlog;其余模式先试 rlog,AUTO/AUTO_INTERACTIVE模式再追加 qlog 回退; - 对每种文件名逐个来源查询,用
file_exists(HEAD 校验 URL、os.path.exists校验本地)取每个分段第一个存在的文件; - 所有分段都找到才返回;否则
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_index(vidindex.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,输出为rgb24的numpy数组(-1, h, w, 3)。
也就是说,日志(结构化消息)与视频(图像帧)分别由 LogReader 和 FrameReader 承担,二者共享同一套 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_call(test_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.py 与 test_route_library.py 中的用例,可以完整验证每一种取法的行为,这也是在编写自己的数据回放、离线挖掘或 CI 校验脚本时的可靠参照。
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 StartedRust0622
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