news 2026/9/5 20:39:54

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
openpilot 数据加载实战:Route 与 LogReader 如何读取路线日志(rlog/qlog)

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

【免费下载链接】openpilotopenpilot is an operating system for robotics. Currently, it upgrades the driver assistance system on 300+ supported cars.项目地址: https://gitcode.com/GitHub_Trending/op/openpilot

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)定义了每个分段中可能出现的数据文件:

类型文件名含义
RLOGrlog.zst/rlog.bz2全量原始日志(rlog 保留完整 CAN、传感器等高频消息)
QLOGqlog.zst/qlog.bz2精简日志(qlog 只保留低频关键消息,体积更小)
FCAMERAfcamera.hevc前视道路相机视频
DCAMERAdcamera.hevc驾驶员监测相机视频
ECAMERAecamera.hevc事件相机视频
QCAMERAqcamera.ts时间戳化的相机流
BOOTLOGbootlog.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-19a2a0ccea32023010\|2023-07-27--13-01-19-1(整条路线)None
a2a0ccea32023010/2023-07-27--13-01-19--1同上1None
a2a0ccea32023010\|2023-07-27--13-01-19/2同上2None
/data/media/0/realdata/a2a0ccea32023010\|2023-07-27--13-01-19同上-1/data/media/0/realdata

注意SegmentName(route.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:") # 除第一个分段外全部

切片语义与 Pythonslice完全一致,由SegmentRange.seg_idxs(route.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.py 的ReadMode枚举与 helpers.py 正则中[qra]的 selector),完整支持四种模式:

选择器ReadMode行为
/rRLOG只读 rlog,缺失即失败
/qQLOG只读 qlog
/aAUTO优先 rlog,缺失分段自动回退 qlog(无提示)
(无选择器时的默认参数)RLOG/AUTO/AUTO_INTERACTIVELogReader(default_mode=...)指定
/iAUTO_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)做了三件事:

  1. 获取原始字节:通过 filereader.py 的FileReader——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.py 的save_log提供了对称的写路径:把消息列表拼接成字节后按目标扩展名选择bz2.compresszstd.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)的解析流程是:

  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_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,输出为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_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 校验脚本时的可靠参照。

【免费下载链接】openpilotopenpilot is an operating system for robotics. Currently, it upgrades the driver assistance system on 300+ supported cars.项目地址: https://gitcode.com/GitHub_Trending/op/openpilot

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 20:39:24

基于MADDPG与Gymnasium的多无人机协同围捕仿真实战

简介&#xff1a;本资源是一个面向人工智能与机器人方向研究者、高校师生及强化学习实践者的多无人机协同控制仿真项目&#xff0c;聚焦于利用多智能体深度强化学习解决动态环境下的目标围捕与协同决策问题。项目基于MADDPG算法&#xff0c;在自定义Gymnasium仿真环境中训练3架…

作者头像 李华
网站建设 2026/9/5 20:36:19

本地部署大模型实战:Ollama、Transformers与llama.cpp量化指南

最早决定在自己电脑上折腾大模型本地部署&#xff0c;是因为炼丹房里排队排到怀疑人生&#xff0c;而且有些内部数据实在不方便往外丢。后来发现这事儿的门槛并没有想象中那么高&#xff0c;关键是找对工具链。目前主流的三条路线——Ollama、transformers、llama.cpp——我前前…

作者头像 李华
网站建设 2026/9/5 20:32:38

军事体能考核Python评定系统设计与实战落地

简介&#xff1a;这是一套面向军队院校、基层部队体能训练管理人员及Python数据分析初学者的军事体能考核成绩自动化评定工具&#xff0c;依据2021年1月最新通用训练课目标准开发&#xff0c;解决人工计算仰卧起坐、蛇形跑、单杠引体向上、3000米跑及身高体重体脂体型、高原海拔…

作者头像 李华
网站建设 2026/9/5 20:25:44

巅峰对决BP与选手状态:拆解AG对KSG第七局的可复用复盘框架

第七局&#xff0c;巅峰对决&#xff0c;AG 在 BP 和临场执行上被 KSG 压制。赛后讨论里出现频率最高的词&#xff0c;一个是“完爆”&#xff0c;一个是“战犯”&#xff0c;钟意和一诺被不少人点名。作为一场高强度收官局&#xff0c;这个结果确实有很多可以拆的地方。但我不…

作者头像 李华