标签: 数据管线

  • Polaris Sportsman 的 J1939 档案:12 个 PGN 与 250 kbit/s

    结论:Polaris Sportsman 的这份 J1939 档案给出 250 kbit/s、29 位扩展 ID 下 12 个 PGN 的信号定义,但它们是 2022 款 Sportsman 570 的逆向结果,工具面向的 2026 车型仍需逐条复验。另外两处硬边界必须知道:仓库未找到许可证文件,且工具导出的 DBC 因多信号 PGN 会重复占用同一 CAN ID,未被作为可用文件归档。

    12 个 PGN 分成两类看

    整车走 J1939,位速率 250 kbps、29 位扩展 ID,硬件链路是 Polaris 8 针诊断口 → GT150 → DB9 → PCAN DSUB9,配合 PEAK PCAN-USB FD 适配器与 PCAN-Basic 驱动,Python 侧通过 python-can 的 pcan 接口调用 PCANBasic.dll。配置集中在 polaris_can/config.py:interface 为 pcan,channel 为 PCAN_USBBUS1,bitrate 为 250000。

    把 12 个 PGN 按性质分类,比按地址排序有用得多:一类是状态枚举(取值是离散码),另一类是数值量(需要字节序与缩放)。

    PGN(含字节位) 信号 取值 / 换算 类别
    0x18F00500[5] 挡位 0x4C = L、0x48 = H、0x4E = N、0x52 = R、0x50 = P 状态枚举
    0x18FEF100[0] 手刹 0x37 = 已拉起、0x33 = 已释放 状态枚举
    0x18FEF1FE[3] 脚刹 0xDF = 踩下、0xCF = 释放 状态枚举
    0x18FF6CFE[1] 安全带 0xF7 = 系好、0xF3 = 解开 状态枚举
    0x18F006FE[1] 差速锁 0xDF = 锁止、0xCF = 打开 状态枚举
    0x18FDDFFE[0] 四驱模式 0xFC = 2WD、0xFD = 4WD 状态枚举
    0x18FEF200[6] 油门 0x19–0xFA 映射为 0–100% 数值量,有量程端点
    0x18FEF100[1:2] 车速 小端 uint16 除以 500,单位 mph 数值量,需字节序
    0x18FF6600[0:1] 发动机转速 小端 uint16 数值量,未给缩放
    0x18FEEEFE[0] 冷却液温度 字节值 + 40°C 偏移 数值量,偏移方向需复核
    0x18FEFC00[1] 油量 0–254 数值量,未给百分比换算
    0x18FEE500[0:1] 发动机小时计 小端 uint16 数值量,未给缩放

    这张表里最值得注意的是最后一列。状态枚举只要码值对得上就能用;而转速、油量、小时计三项只给了类型与字节序,没有给缩放因子,水温那条甚至写成「字节 + 40°C 偏移」——与常见的减 40 相反。这四项在使用前都必须用已知工况反推。

    为什么导出的 DBC 不能直接用

    工具提供了 polaris-can export-dbc,输出 cantools 兼容格式,可导入 SavvyCAN、Cabana、Vector CANoe。但归档说明明确指出:导出函数会为多信号 PGN 创建多个使用同一 CAN ID 的独立报文,因此该输出未被作为经过验证的 DBC 归档。

    这个坑在 J1939 场景下很典型。像 0x18FEF100 这样同时承载手刹与车速的 PGN,在 DBC 里应当是同一条报文下的两个信号;若导出成两条同 ID 报文,解析器要么只认第一条,要么按 ID 覆盖,结果就是丢信号。因此正确做法是把这份 PGN 表当作原始事实,手工建 DBC,而不是依赖自动导出。

    五种模式:哪一种是真正的逆向工具

    工具提供 decode、log、discover、replay、export-dbc 五种模式,其中 discover 才是为新车型定位信号的核心手段。

    模式 命令 输出 适用场景
    实时解码 polaris-can decode 终端仪表盘,10 Hz 刷新,含帧率、总线负载百分比、唯一 ID 计数 已知信号的日常查看,可加 --output 落盘
    记录 polaris-can log session1.log 微秒级时间戳的 candump 兼容日志 外场采集,后续导入 SavvyCAN 等工具
    发现 polaris-can discover --warmup 30 基线冻结后报告新 ID 与变化字节 新车型定位信号:怠速 30 秒建基线,再逐个操作开关
    回放 polaris-can replay session1.log --dashboard 离线回放,可实时或加速 无硬件时开发与验证
    导出 polaris-can export-dbc polaris.dbc DBC 文件 仅作起点,多信号 PGN 需手工修正

    discover 的工作流是:预热阶段(默认 30 秒,可用 --warmup 60 延长)记录基线流量并冻结,随后进入发现阶段,监控新出现的 ID 与发生变化的字节。文档给出的示例输出显示基线里冻结了 47 个 CAN ID,随后报告新 ID 与具体字节的旧值到新值变化。操作要领是预热期间车辆真正处于怠速,之后一次只动一个开关或踏板。

    解码时匹配成功的信号会打印 [KNOWN],不匹配的被标为需要重新映射的候选——2026 与 2022 的差异正是靠这个标记暴露出来的。

    常见问题

    2022 年的 PGN 表能直接用在 2026 车型上吗?

    不能直接套用。README 明确说明工具面向 2026 款 Sportsman 570 EPS,但内置的 12 个 PGN 定义来自 2022 款 Sportsman 570,属于基线事实,需要在目标车型上逐条验证。若发现不匹配,应用 discover 模式重建:先怠速建立基线,再逐个操作开关与踏板,观察新 ID 与字节变化。

    为什么导出的 DBC 没有被当作可用文件归档?

    因为导出函数对多信号 PGN 会生成多条使用同一 CAN ID 的独立报文。例如 0x18FEF100 同时承载手刹与车速,正确形态应是同一条报文下的两个信号;拆成两条同 ID 报文会导致解析器丢信号。因此该输出未通过验证,未被归档,建议以 PGN 表为事实源手工建库。

    车速 PGN 0x18FEF100 的换算单位是什么?

    原文给出的是小端 uint16 除以 500,单位为 mph。也就是说原始值需要先按小端字节序拼成 16 位无符号整数,再除以 500 得到英里每小时;若系统内部使用 km/h 或 m/s,还要再做一级单位换算。

    没有 PCAN 硬件能不能用这套工具?

    可以。replay 模式支持离线回放日志文件,可实时或加速播放并接入仪表盘,仓库也提供了 tests/sample.log,在硬件未连接时用示例数据即可跑通,单元测试同样不依赖硬件。真正需要硬件的只有 decode、log 与 discover 三种模式。

    延伸阅读

    参考资料

    • 项目 01 —— public-can-dbc-datasets/additional/marvbull-polaris-sportsman-can/README.md(250 kbit/s J1939 配置、12 个 PGN 的信号定义、五种工具模式与故障排查)
    • 项目 01 —— public-can-dbc-datasets/additional/marvbull-polaris-sportsman-can/SOURCE.md(快照提交、证据边界、PGN_MAP 的 12 个标识符与导出 DBC 的缺陷说明、权利状态)

    免责声明

    本文所述 PGN、码值与换算均来自公开开源仓库的自述与工具代码,属社区逆向工程产物,不代表车辆制造商的官方数据。信号定义来自 2022 款车型,未经独立抓包验证,不同年款、市场与 ECU 版本可能存在差异。仓库未找到许可证文件,相关材料应作为私有离线研究副本,在权利确认前不得再分发或集成。

    更新记录

    • 2026-10-14 初稿

  • feberkeley-sn4 数据集:99 报文、1305 信号与 11 段帧 ID 分区

    结论:feberkeley-sn4 数据集的核心是一份由 Python 定义生成的 DBC,含 99 条 BO_ 报文与 1,305 条 SG_ 信号。它的可复用之处在于一套清晰的帧 ID 分区策略:11 段地址区间按板卡/子系统划分并预留扩展空间。但有两个必须记住的边界——README 自称 SN4 库却描述为 SN5 车辆,且快照中未发现仓库许可证。

    数据集的构成:从 Python 定义到 DBC 与 C 代码

    这个库的工作方式不是手写 DBC,而是用 Python 的 cantools 对象描述报文,再由脚本生成 DBC 与 C 编解码代码。生成脚本 generate_can.sh 会自动创建虚拟环境、安装指定版本 cantools(39.4.13)、校验定义、生成 DBC、再由 DBC 生成 C 源文件,并归一化时间戳以保证 diff 稳定。

    产物 路径 作用
    DBC 数据库 gen/FEB_CAN.dbc 99 条 BO_ 报文、1,305 条 SG_ 信号
    C 头文件 gen/feb_can_db.h 编解码函数与最新值数据库
    C 实现 gen/feb_can_db.c 上述两者的实现

    external_dbc/ 目录里放供应商 DBC,其中的 inverter.dbc(Cascadia PM100 逆变器)与 elcon.dbc(Elcon/HK 充电机)会被自动合并进生成结果。这意味着 99 条报文里包含第三方设备的定义,使用时需要知道哪些信号来自自有板卡、哪些来自外部设备。

    帧 ID 分区:这套数据集最值得抄的部分

    帧 ID 按板卡划段,每段都预留了扩展余量。这种分配方式让「新增一条报文」变成一个可判定的问题——先看所属板卡区间还有没有空位,而不是全局找一个没用过的 ID。

    区间 归属 当前报文数 用途
    0x00-0x08 BMS 5 电池管理
    0x09-0x0F PCU 2 安全关键(制动、BSPD)
    0x10-0x15 DASH 1 仪表盘 I/O
    0x16-0x1D LVPDB 3 配电
    0x1E-0x2C Sensors 8 胎温、轮速、线性电位计
    0x2D-0x33 DART 2 风扇控制
    0x34-0x3F TPS 4 电源监控芯片
    0xC0-0xCF RMS 2 逆变器通信
    0xD0-0xDF Heartbeats 6 板卡健康监测
    0xE0-0xEF Debug 4 测试与调试报文
    0xFF PCU 1 原始 ADC 值

    注意地址空间并非连续填满:0x40 到 0xBF 之间是一大片未分配区域,Heartbeats 与 Debug 被放到了高位。这种「功能段上移、控制段下沉」的排布,让低地址区留给数量可能增长的子系统。

    新增一条报文要改三处

    流程是固定的:先在对应板卡的 *_messages.py 里写一个返回 cantools.db.Message 的函数,再在 generate.py 的 MESSAGE_REGISTRY 字典里用可用帧 ID 注册,最后重新运行 generate_can.sh 并用 --list 与 --dump 核验。校验阶段会拦两类错误:同一函数被注册到多个帧 ID(报 Duplicate function),以及帧 ID 超出标准 CAN 的 0x000-0x7FF 范围(报 Frame ID out of range)。

    信号定义的统一约定

    整个库统一使用小端字节序,这一点在排障提示里被单独强调:解包后数值不对,第一件事就是检查字节序与有无符号。位位置按小端排列,8 字节报文的位编号从 0 到 63,与字节序一一对应。

    信号类型 典型长度 有无符号 用途
    16 位电压/电流 16 无符号 以 mV、mA 为单位传输
    16 位温度 16 有符号 摄氏度
    单比特故障标志 1 无符号 故障位
    8 位状态枚举 8 无符号 状态机取值
    32 位计数器 32 无符号 计数与心跳

    心跳报文有一套标准模式:64 个独立的错误标志位,实现见 lvpdb_messages.py 中的 get_lvpdb_heartbeat()。把板卡健康状态压成 64 个比特放进周期性心跳,是这套库里最通用的设计模式之一。

    CI 里的 check 模式

    GitHub Actions 工作流用 ./generate_can.sh --check 验证生成文件是否最新,确保 Python 定义与提交进仓库的 C 代码一致。这条检查把「改了定义忘了重新生成」变成一次 CI 失败,而不是运行期的错位解码。本地若遇到该检查失败,重新生成并提交 gen/ 目录即可。

    使用边界:车款归属与权利状态

    数据集有两处明确的边界,引用前必须确认。

    边界项 实际情况 影响
    车辆归属 仓库名为 SN4,README 却描述为 FEB SN5 方程式电动车 确切车款与赛季未定,复用需自行确认
    许可证 快照中未发现仓库许可证 仅可作为私有离线研究副本
    第三方定义 合并了 Cascadia PM100 与 Elcon/HK 的供应商 DBC 供应商定义的权利未单独声明,需另行确认
    归档范围 仅归档生成的 DBC 与 README 两个供应商源文件未单独拷贝,但定义可能已在生成的 DBC 内

    综合来看,这份数据集适合作为「如何组织一个多板卡 CAN 库」的参考实现——分区策略、生成流水线、心跳模式、CI 校验都可直接借鉴;但作为某辆具体赛车的权威信号定义来用则证据不足。

    常见问题

    这份 DBC 有多少报文和信号?

    生成的 DBC 有 99 条 BO_ 报文与 1,305 条 SG_ 信号行。其中包含自动合并进来的第三方设备定义:external_dbc/ 目录下的 Cascadia PM100 逆变器 DBC 与 Elcon/HK 充电机 DBC 会被自动并入生成结果。

    新增一条 CAN 报文的正确步骤是什么?

    三步:先在对应板卡的 *_messages.py 里定义返回 cantools.db.Message 的函数;再在 generate.py 的 MESSAGE_REGISTRY 里用一个可用帧 ID 注册;最后重新运行 generate_can.sh,并用 –list 与 –dump 核验。校验会拦截同函数重复注册与帧 ID 超出 0x000-0x7FF 两类错误。

    怎么知道哪些帧 ID 还没被占用?

    运行 ./generate_can.sh –ids 查看帧 ID 分配图。帧 ID 按板卡分区并预留扩展空间,例如 BMS 为 0x00-0x08(已用 5 条)、Sensors 为 0x1E-0x2C(已用 8 条)、Heartbeats 为 0xD0-0xDF(已用 6 条)。

    解包出来的数值不对,先查什么?

    先查字节序与有无符号。这个库统一使用小端字节序,若定义与实际不符,解包结果就会错。其次确认 C 侧的 pack 函数名——报文名会被规范化成 C 标识符,实际函数名需在 gen/feb_can_db.h 中确认(小写、下划线)。

    这份数据集能用于我自己的赛车吗?

    作为组织方式的参考可以,作为权威信号定义则证据不足。仓库名为 SN4 但 README 描述为 FEB SN5 方程式电动车,确切车款与赛季未定;快照中未发现仓库许可证;合并的供应商定义权利也未单独声明。建议仅作私有离线研究副本。

    延伸阅读

    参考资料

    • 项目 07 —— public-can-dbc-datasets/additional/feberkeley-sn4/SOURCE.md(固定提交、99 报文 1,305 信号规模、车款归属疑问与权利状态)
    • 项目 07 —— public-can-dbc-datasets/additional/feberkeley-sn4/README.md(生成流水线、帧 ID 分区表、信号定义模式与 CI 校验)

    免责声明

    本文内容来自公开仓库的说明文件与生成产物统计,未对信号定义做逐条验证。数据集的车款归属与赛季在来源中存在不一致,且未发现仓库许可证,供应商定义的权利亦未单独声明。请仅作为私有离线研究副本使用,实际车辆应用前请以原始来源的权利确认与自有验证为准。

    更新记录

    • 2026-10-14 初稿

  • JotPluggler 视频与缩略图:8 帧缓存与解码器共享

    结论:JotPluggler 的相机与缩略图路径是离线路段媒体查看——实时模式会排除四类编码索引/编码数据、直播编码索引及 thumbnail,因此不能把它描述成实时行车画面输入。build_camera_index() 按数组最短长度合成 (timestamp, segment, decode_index, frame_id) 并按时间排序,但不逐项核对多条字段曲线时间戳一致性、也不在建索引时验证 decode_index 在媒体帧数内,无效索引要到取帧时才失败。解码器按 (CameraType, width, height) 复用,锁只覆盖获取/建立过程,若两路映射到相同组合并发解码,存在共享可变解码状态竞争风险(跨文件静态推断,未复现)。以下为固定快照 c8fb906 的静态阅读,未下载视频、未解码损坏媒体、未运行 GUI、未测内存/帧时延。

    路段中的图像索引

    load_route_data() 在路段数据归一化后,调用 build_camera_index() 分别建立窄路、座舱、广角路和 qcamera 四类索引。它先收集有文件路径的片段,再从对应 *EncodeIdx 曲线读取 segmentNum、segmentId(缺失时 segmentIdEncode)及可选 frameId;按数组最短长度和同一索引组合成 (timestamp, segment, decode_index, frame_id),最后按时间排序。缺少索引曲线时虽然有文件路径,也不会形成可按时间选帧的条目。

    frame_id 会保留在结构中,但本工具取帧请求使用的是 decode_index。build_camera_index() 没有逐项核对多条字段曲线时间戳一致性,也没有在建索引时验证 decode_index 在媒体文件帧数内;实际无效索引要到 FrameReader::get() 才失败。这与 GPS 字段按索引拼接的风险类似:对丢字段、乱序、跨段切换需要带真实日志的对齐测试。

    extract_segment_thumbnails() 另从 THUMBNAIL 事件提取 JPEG,时间优先用 timestampEof,为零时退回事件 logMonoTime;build_route_data() 排序并与其他数据共用时间原点。

    相机请求、解码与纹理

    四个 CameraFeedView 各自有需求线程,围绕「选帧 → 解码 → 上传纹理」运转。下表列出实现行为与边界。

    函数 / 组件 实现行为 边界
    CameraFeedView::Impl::setCameraIndex() 替换相机索引、清空待处理请求和 8 帧结果缓存、递增 generation,按首个片段组织预加载 切换路段会重置纹理与请求;旧下载/解码通过 generation/serial 检查避免结果误入新路段
    request_for_time() / update() 根据 tracker 时间选最近索引帧;优先当前帧或缓存,允许先显示同片段附近帧,再排入最新请求 附近帧只是临时可视回退,不应作为精确时间证据;失败帧记入 failed_request,相同请求不立即重试
    demand_worker_loop() 加载片段的 FrameReader,按 decode_index 解码进 VisionBuf,NV12 转 RGBA;完成后向 UI 交付并向前预取两帧 RGBA 按 width * height * 4 分配;媒体尺寸异常时需端到端限制和压力测试
    preload_worker_loop() / ensure_reader_loaded() 两个预加载线程按与当前片段距离预开其他视频,需求线程可等待同片段的在途加载 readers 是按片段保存的 map,本路径未见固定数量/字节上限
    upload_pending_result() / drawSized() UI 线程上传 RGBA 到 GL 纹理;Fit 模式裁切填满 pane,另一模式保持原比例完整显示 8 帧 RGBA 缓存之外还有纹理、解码缓冲及预加载 reader
    FrameReader::load() / DecoderManager::acquire() HTTP(S) 视频先走本地缓存再用 FFmpeg 探测/建帧索引;按相机类型和尺寸复用解码器 锁只覆盖获取/建立过程;FFmpegVideoDecoder::decode() 使用实例上下文与帧缓冲,未见同实例解码互斥

    这套链路也有资源上界缺口:readers 是按片段保存的 map,本路径未见固定数量或字节上限,长路段可能积累解析器与索引;四个 CameraFeedView 各自有需求线程,若两路映射到相同 (CameraType, width, height) 并发解码,存在共享可变解码状态竞争风险(跨文件静态推断,需并发用例确认)。

    缩略图与实时限制

    ThumbnailView::Impl::update() 用二分查找选离 tracker 最近的缩略图;decode_jpeg() 使用 FFmpeg MJPEG 解码,仅接受 YUV420/422/444 类格式,手工转换成 RGBA,随后上传单个 GL 纹理。成功图像按比例居中并叠加时间、片段、序号;失败索引在当前缩略图集合内不反复解码。rgba.resize(width * height * 4) 前只检查解码尺寸大于零,未见更小的像素/内存预算,应以异常 JPEG 测试资源约束。

    环节 行为 边界
    缩略图选图 二分查找选离 tracker 最近的缩略图,时间优先 timestampEof、为零退回 logMonoTime 失败索引在当前集合内不反复解码
    JPEG 解码 FFmpeg MJPEG,仅接受 YUV420/422/444 类格式,手工转 RGBA 后上传单个 GL 纹理 rgba.resize(width * height * 4) 前只检查解码尺寸大于零,无更小像素/内存预算
    实时订阅排除 实时模式排除四类编码索引/编码数据、直播编码索引及 thumbnail 相机与缩略图路径是离线路段媒体查看,不是实时行车画面
    隐私 座舱视频与缩略图可能涉及人脸、乘员与道路位置 导出、远程下载、缓存清理及访问控制应分开设计

    关键限制来自 should_subscribe_stream_service():它在实时模式排除四类编码索引/编码数据、直播编码索引及 thumbnail,因此上述相机和缩略图路径是离线路段媒体查看,不能把它描述成实时行车画面输入。工具读到的座舱视频和缩略图可能涉及人脸、乘员与道路位置;导出、远程下载、缓存清理及访问控制应分开设计。

    对「万物即插件」的约束

    建议拆成 VideoIndexProvider、FrameDecoder、ThumbnailDecoder 和只读 MediaPane。索引契约必须声明时间基准、片段编号、解码帧序号及缺帧语义;解码器实例要么独占线程,要么明确同步。宿主统一限制像素数、缓存字节、打开的 reader 数及并发下载;出错时向用户标出「正在显示附近帧/上一帧」,避免将近似画面误当当前时刻。媒体插件不得获取 CAN TX、固件更新或安全配置权限。

    同目录测试仅检查帮助输出;本文未运行损坏 HEVC/JPEG、并发多路、远程下载、GPU 纹理或内存压力测试。

    常见问题

    这是实时行车画面吗?

    不是。实时模式会排除四类编码索引/编码数据、直播编码索引及 thumbnail,因此相机与缩略图路径是离线路段媒体查看,不能描述成实时行车画面输入。

    8 帧缓存就是内存上界吗?

    不是。8 帧 RGBA 缓存之外还有纹理、解码缓冲及预加载 reader,不能用「8 帧」估算总内存上界。

    索引里的 decode_index 一定有效吗?

    不一定。build_camera_index() 不在建索引时验证 decode_index 在媒体文件帧数内,实际无效要到 FrameReader::get() 才失败。

    两路相机同时解码安全吗?

    不能假定安全。解码器按 (CameraType, width, height) 复用,锁只覆盖获取/建立过程;若两路映射到相同组合并发解码,存在共享可变解码状态竞争风险。这是跨文件静态推断,需并发视频用例或 ThreadSanitizer 确认,不能视为已复现故障。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/JOTPLUGGLER_VIDEO_THUMBNAIL_DEEP_DIVE.md(相机索引、请求/解码/纹理链与缩略图边界)
    • 项目 02 —— openpilot-research/JOTPLUGGLER_DATA_PIPELINES_DEEP_DIVE.md(图像链所在的离线/实时数据管线,作为本文的补充阅读)

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 编码配置表:录制、音频与直播的进程快照分层

    结论:loggerd 的相机/编码表是进程静态对象,不是运行时可重建的配置:encoderd 与 loggerd 各自加载同一头文件并构造本进程的静态表,运行中改参数不会重建配置表。RecordFront=false 在这条路径只代表「不写本地媒体文件」,座舱流仍会发布 cabinEncodeData 并可能写派生索引;RecordAudio 至少参与两个独立时刻的读取,二者不在同一事务。直播是独立的 stream_encoderd 进程与主题,录制与直播的保留/远程访问策略必须独立设计。

    静态描述表与进程边界

    EncoderSettings 给出编码参数:主流按输入宽度选 5/10 Mbps、GOP 20/30;qcamera 为 H.264、256 kbps、GOP 15;直播默认 H.264、5 Mbps、GOP 5,默认 B 帧数为 0。EncoderInfo/LogCameraInfo 由 topic、文件名、录制/音频/直播标志、输出尺寸、FPS 和 Cap’n Proto union 读写函数指针组成。

    编码目标 编码与码率 GOP 默认 B 帧
    主流(按输入宽度) 5/10 Mbps 20/30 0
    qcamera H.264、256 kbps 15 0
    直播默认 H.264、5 Mbps 5 0

    cameras_logged 含窄路面主流与 qcamera 两个编码器(共享同一路 VisionIPC 帧),广角与座舱各一个;stream_cameras_logged 是三路独立直播主题,record=false,没有媒体文件名也不配置缩略图。encoderd main() 无参数使用录制表,--stream 使用直播表,两种是同一可执行文件的不同进程模式。

    录制策略不是编码生产开关

    RecordFront 在座舱编码信息的静态初始化期间读一次,用作 record 字段。encoderd 的编码线程没用该字段过滤编码器:座舱流一旦被发现,仍会发布 cabinEncodeData。loggerd 也仍订阅非直播编码主题;record=false 时不创建 dcamera.hevc writer,但通过关键帧门后仍写 cabinEncodeIdx。

    因此源文档的结论是:「关闭座舱录制」在这条路径代表不写本地媒体文件,不是停止座舱图像采集/编码,也不是证明下游没有索引。直播座舱流使用独立 topic 和进程,不能由 RecordFront 的本地文件策略自动推导其权限。

    RecordAudio 的两个读取时刻

    RecordAudio 至少参与两个独立时刻:qcamera 编码信息的 include_audio 在各进程静态初始化时读取;loggerd_thread() 建订阅时再次读取,只有当时为真才订阅原始音频。micd 的 callback() 本身持续发布 rawAudioData,并不以 RecordAudio 阻止采集。

    在正常一致的启动快照下,loggerd 收音频后把它同时作为 rlog 普通事件和 qcamera 音轨输入。但两次 Params 读取不在同一事务,运行中切换也不会刷新已有订阅或配置表:若启动窗口内值变化,静态 include_audio 与实际订阅可能不一致。源文档指出,例如等待音频的 qcamera writer 未得到音频初始化包,会走有界包数的等待/丢包路径,是否在常规 manager 时序下可达需故障注入验证。

    平台格式、直播与时间边界

    项 静态行为
    MAIN_ENCODE_TYPE 依 Hardware::PC() 选择 PC 的 BIG_BOX_LOSSLESS 或设备的 FULL_H_E_V_C;固定 fcamera.hevc 文件名不能单独证明实际编码为 HEVC。
    直播输出尺寸 livestream_width()/height() 对 TIZI 为 1152×720、MICI 为 1280×720,其他设备返回 -1;VideoEncoder 构造器对非正输出尺寸退回输入尺寸。
    STREAM_BITRATE 用 atoi 读取且无配置层范围校验;运行时 LivestreamEncoderBitrate 由上层逐帧读 Params,PC 后端不支持动态调整。
    SEGMENT_LENGTH 通常固定 60 秒;LOGGERD_TEST 存在时改用 atoi(LOGGERD_SEGMENT_LENGTH),是测试配置入口,不应作为生产插件 API。

    直播三个 EncoderInfo 都没有文件名,且 loggerd 订阅时明确排除以 livestream 开头的 EncodeData,WebRTC 另行消费这些主题。录制与直播可以同用相机源,但数据保留/远程访问策略必须独立设计。两个进程各自加载同一头文件并构造本进程的静态对象,因此不能把它们的 Params 快照视为原子一致。

    验证与重构要求

    已有测试检查多路段媒体与索引、座舱文件有无、qcamera 音轨和 rlog 原始音频;源文档指出它们未直接断言 RecordFront 运行中变化时生产端是否继续编码、跨进程 RecordAudio 快照竞争、直播/录制权限隔离或静态表重建。本轮未运行这些测试。

    插件宿主应将「采集许可、编码生产、本地存储、音频复用、上传、远程直播」拆为不同能力,并在一个版本化配置事务中确认生效时刻。隐私开关变更需要可观察的撤销/排空及下游回执,而不是只写 Params;录制与直播要按 topic、相机和车辆状态独立授权。台架验收应覆盖启动竞态、开关热变、进程重启不一致、qcamera 等音频、座舱关闭录制但仍发布 EncodeData、直播单独启停及文件/索引残留。

    常见问题

    关闭座舱录制(RecordFront=false)会停止座舱图像采集吗?

    不会。源文档明确 encoderd 的编码线程没有用该字段过滤编码器,座舱流一旦被发现仍会发布 cabinEncodeData;record=false 只是不创建 dcamera.hevc writer,但通过关键帧门后仍写 cabinEncodeIdx。

    RecordAudio 关掉后麦克风就停止采集了吗?

    不会。源文档指出 micd 的 callback() 本身持续发布 rawAudioData,并不以 RecordAudio 阻止采集;RecordAudio 只影响 qcamera 的 include_audio 与 loggerd 是否订阅原始音频。

    固定文件名 fcamera.hevc 能否说明实际编码是 HEVC?

    不能。MAIN_ENCODE_TYPE 依 Hardware::PC() 选择 PC 的 BIG_BOX_LOSSLESS 或设备的 FULL_H_E_V_C,源文档明确固定文件名不能单独证明实际编码为 HEVC。

    运行中修改录制/音频参数会立即生效吗?

    不会。源文档指出配置在进程静态初始化时读取,运行中切换不会刷新已有订阅或配置表;两次 RecordAudio 读取也不在同一事务,启动窗口内值变化可能造成 include_audio 与实际订阅不一致。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/ENCODER_CONFIG_RECORDING_LIVESTREAM_SNAPSHOT_DEEP_DIVE.md(取用了静态编码配置表、录制/音频/直播的进程快照边界、平台格式与时间边界、重构验证要求)。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 四个开发工具的信任边界:回放导出与 CarParams 注入

    结论:四个开发工具的信任等级并不相同:print_flags.py 只读枚举,test_fw_query_on_routes.py 只读已记录固件,run_process_on_route.py 会把新旧消息合并导出,而 set_car_params.py 会写入当前参数库。前两者可分析,后两者带副作用,其中最危险的是把 CarParams 写入下放给任意插件。

    工具清单与副作用等级

    工具 源码链路 静态结论
    test_fw_query_on_routes.py 参数可为单一路段或文件路径;文件分支取每行再交 SegmentRange,未指定段则拼 /0,随后按 QLOG 读 carParams 并分别做精确/模糊固件匹配。 原主入口探针确认列表行尾换行未剥离,正则在内部 try 前抛断言并中断整批;读取器构造失败也在 try 前传播。脚本只读已记录固件,不向 ECU 发查询。
    批量统计与不匹配输出 以 dongle_id 去重,在读路段前就把它加入集合;对不匹配固件按车型、地址去重汇总,最终打印精确/未识别/模糊计数。 假消息探针确认首个路段没有 carParams 时,同设备第二路段仍被跳过;不支持的 panda 类型会提前结束,随后又报告无 CarParams。输出含 VIN 与固件字节,需视为敏感诊断数据。
    run_process_on_route.py 默认从 CONFIGS 选全部进程,再减去被排除的两个模型进程;读完整路段调用回放,只按实际产生的消息类型删除旧输入,再将新旧消息按时间合并并保存。 探针确认零回放输出会保留并重排全部旧消息,未知白名单会选出空配置但仍读取和导出原日志。文件名进程部分来自集合迭代顺序,不是稳定标识。导出不调用车型日志净化器,副本可能携带原始 CAN/VIN。
    set_car_params.py 有路段时只读第一条 qlog 中第一条 carParams;无参数时新建空白消息,只写两个纵向标志;随后为每个键新建 Params(),按三个键顺序阻塞写入。 探针确认缺少 carParams 时在任何写入前抛 IndexError;第二键抛异常时第一键已调用成功、第三键未调用。原 Python Params.put() 会丢弃底层负错误码,故底层写盘失败可能不抛异常。本层无回滚或读回,不校验平台、VIN、safety 或当前车辆身份。
    print_flags.py 遍历 BRANDS 与各平台配置标志,按标志列出车型名称。 Platforms 与归档的 CAR 枚举均未定义脚本调用的 with_flags();存在非零标志配置,非零假枚举时原主体抛 AttributeError,零标志枚举则无输出。

    run_process_on_route:替换与导出的边界

    该脚本默认从 CONFIGS 选全部进程,再减去被排除的两个模型进程,读完整路段后调用 replay_process(),只按本轮实际产生的消息类型删除旧输入,再把新旧消息按 logMonoTime 合并并 save_log()。

    假依赖主入口探针确认:零回放输出会保留并重排全部旧消息;未知白名单会选出空配置但仍读取和导出原日志。另外,文件名中进程部分来自集合迭代顺序,不是稳定标识;save_log() 不调用车型日志净化器,副本可能携带原始 CAN/VIN。因此不能以脚本名称替代对进程副作用的审查。

    回放导出还有一个隐含前提:所选进程必须在当前代码版本下可复现,否则混合出的日志既不等于原始输入,也不等于一次真实回放;这正是需要把代码版本与被替换主题一并写入导出元数据的原因。

    test_fw_query_on_routes:批量审计的失败面

    该脚本的参数可为单一路段或文件路径;文件分支用 list(open(...)) 直接取每行,再交 SegmentRange;未指定段则拼 /0,随后按 QLOG 读取 carParams 并分别做精确/模糊固件匹配。探针确认列表行尾换行未剥离,正则在内部 try 前抛断言并中断整批;读取器构造失败同样在 try 前传播。

    统计侧以 dongle_id 去重,且在读路段之前就把该键加入集合;对不匹配固件按车型、地址去重汇总,最终打印精确、未识别与模糊匹配计数。需要提醒的是:首个路段没有 carParams 时,同设备第二路段仍被跳过;不支持的 panda 类型会提前结束,随后又报告无 CarParams。这些输出包含 VIN 与固件字节,应视为敏感诊断数据。

    set_car_params:三键写入无补偿

    有路段时,脚本只读取第一条 qlog 中第一条 carParams;无参数时新建空白消息,只把 openpilotLongitudinalControl=True、alphaLongitudinalAvailable=False 写入。随后为每个键新建 Params(),按 CarParams、CarParamsCache、CarParamsPersistent 的顺序阻塞写入。

    假 Params 探针确认:缺少 carParams 时在任何写入前抛 IndexError;第二键抛异常时第一键已调用成功、第三键未调用。但原 Python Params.put() 会丢弃底层负错误码,故底层写盘失败可能不抛异常,脚本仍继续写第三键。本层没有回滚或读回,也不校验平台、VIN、safety 或当前车辆身份。另据键属性,前两者会被 manager 启动清理,CarParamsPersistent 持久化。

    键 写入时机 生命周期
    CarParams 三键顺序写入的第一键 被 manager 启动清理。
    CarParamsCache 第二键 被 manager 启动清理。
    CarParamsPersistent 第三键 持久化。

    插件化:写入权归谁

    print_flags.py 的只读枚举也提示了一件事:即便修复了缺少 with_flags() 的问题,它仍只枚举本地声明,不证明硬件或 safety 已启用。实际车型能力仍须从车型接口、DBC 与 panda safety 交叉验证。这也说明开发工具的结论天然是局部的:它们验证的是脚本自身的控制流,而不是目标车辆的参数正确性。

    探针 覆盖的契约
    标志枚举探针 核对 Platforms 与各品牌方法清单,确认缺少 with_flags() 的异常与零输出分支。
    参数注入主入口探针 验证默认两个标志、三键写入顺序、只取首个 carParams,以及第二键异常后不触发第三键。
    固件审计主入口探针 验证行尾换行、读取器构造异常、同设备去重及不支持 panda 后的状态报告。
    回放导出主入口探针 确认剔除被排除的模型进程、只替换实际产生的类型、零输出仍导出全部旧消息。

    批量固件审计应先把路段列表解析、去重与错误计数做成显式结果;精确/模糊候选只能用于平台审核,不自动修改固件库。回放导出的日志需携带输入路段、所选进程、代码版本、被替换主题、生成主题和隐私标记,避免把混合旧/新消息误当统一来源。CarParams 的写入应归受控车辆身份服务所有,要求平台/线束/DBC/safety 绑定、三键一致性门和读回验证,不能下放给任意插件。

    常见问题

    为什么 set_car_params.py 不能在车端随意试跑?

    因为它会写入当前参数库:按 CarParams、CarParamsCache、CarParamsPersistent 三键顺序阻塞写入,且本层没有回滚或读回、不校验平台/VIN/safety/车辆身份。底层写盘失败还可能因负错误码被丢弃而不抛异常。

    回放导出会混入旧消息吗?

    会。探针确认零回放输出时,全部旧消息仍会进入导出并按时间排序;脚本只按本轮实际产生的消息类型删除旧输入。导出还不调用车型日志净化器,副本可能携带原始 CAN/VIN。

    为什么固件审计脚本可能整批中断?

    因为文件分支取每行时行尾换行未剥离,SegmentRange 的正则在内部 try 之前就抛断言并中断整批;读取器构造失败也在 try 前传播,同样会中断。

    print_flags.py 能证明某车型能力已启用吗?

    不能。它只遍历本地品牌与平台配置来列出车型名称,且归档中并未定义它调用的 with_flags();即使修复,它仍只枚举本地声明,不证明硬件或 safety 已启用。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/DEV_FW_REPLAY_CARPARAMS_TOOLS_TRUST_BOUNDARY_DEEP_DIVE.md(取用了四个开发工具的入口、数据流与失败边界及探针结论)。
    • 项目 02 —— openpilot-research/PROCESS_REPLAY_VALIDATION_DEEP_DIVE.md(仅用于确认回放进程与验证的边界)。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • URLFile 缓存完整性:查询串不入键与接受 200 全量响应

    结论:hash_url() 只对去掉 ? 之后的 URL 计算 MD5 作为缓存键,因此签名查询串变化仍共用同一缓存,可能读到另一版本甚至另一授权上下文的字节。read() 默认把请求范围扩到 1,000,000 字节对齐块,但即使某块实际响应短于要求,也不核对块大小、Content-Range 或最终输出长度,返回后位置仍设为目标终点;get_multi_range() 接受 HTTP 200/206 且不核对单段偏移。以下为固定快照 c8fb906 的静态阅读与隔离探针,未访问远端、未运行会删除本机缓存的上游测试、未读取真实路段。

    函数链

    url_file.py 是离线日志/视频的远端字节入口,其文件接口与常规文件类似,但每一层都缺少内容校验。下表列出关键函数。

    请求层与文件层的语义也要分开看:pool_manager()/_request() 复用 urllib3 池并配置特定重试与超时,但超时是请求级配置,不提供整个多块读取操作的总 deadline;seek() 只更新内部位置,SEEK_END 依赖已知长度,既不限制负偏移也不限制越过 EOF,异常要等后续读取才暴露。

    函数 实现 数据契约与风险
    hash_url() 去掉 ? 后的 URL 计算 MD5,作为块与长度缓存键 同一路径但查询参数不同(如带签名 URL)会共享缓存;MD5 这里只是文件名,不是内容完整性校验
    pool_manager() / _request() 复用 urllib3 池,配置特定 409/429/503/504 重试及 connect/read 超时;fork 后重置池引用 超时是请求级配置,不提供整个多块读取操作的总 deadline
    get_length_online() / get_length() HEAD 的 2xx Content-Length 转整数;缓存长度单独落盘 无 Content-Length 按 0 处理,非 2xx 返回 -1;本地长度缓存不随远端 ETag/内容版本自动失效
    read() 默认把范围扩到 1,000,000 字节对齐块;不存在时从远端读并原子写入缓存,命中时直接读文件,再裁切请求片段 即使某块实际响应短于要求,也未核对块大小、Content-Range 或最终输出长度;请求零长度也会进入块处理循环
    read_aux() 以当前位置构造单段 Range,调用 get_multi_range(),按返回字节数推进位置 仅依赖收到的字节,不校验其与请求区间的对应关系
    get_multi_range() 构造含端点的 Range 头;接受 HTTP 200/206;非 multipart 直接返回单个字节串,multipart 按 boundary 切分 不核对单段 Content-Range、起止偏移或长度;multipart 用 .rstrip(b"rn") 可能删掉合法尾部字节
    prune_cache() manifest.txt 存每个新块的写入时刻,按条目数乘块大小估计容量,再删最旧条目 缓存命中不刷新时间,实际是近似创建时间淘汰,不是访问级 LRU;真实块大小与并发写入未纳入容量/事务保证
    seek() 更新内部位置;SEEK_END 依赖已知长度 不限制负偏移或越过 EOF,后续读取才可能暴露异常

    Range 响应隔离验证

    四项原方法探针从归档提取 get_multi_range()/read_aux(),注入固定 HTTP 响应,不加载 urllib3 或写入缓存。

    假响应与请求 实际结果 合约缺口
    请求 bytes=5-7,返回 200 和完整 10 字节文件 原样返回 10 字节;read_aux(ll=3) 从位置 5 前进到 15 200 全文件没有按 Range 裁切,也不校验 Content-Range 或请求长度
    请求 5 字节,multipart 单段载荷为 abcrn 结果为 abc .rstrip(b"rn") 把合法载荷尾部也删掉
    请求两个 Range,206 非 multipart 只返回 AB 返回 [b"AB"] 非 multipart 分支没有检查响应段数与请求数
    请求一个 Range,返回 404 抛 URLFileException 状态拒绝正常,但不证明其他状态的字节正确

    这些输入在原函数分支下可复现,但不是对真实存储服务会如此响应的断言。生产修复需按请求逐段核对状态、Content-Range、实际长度和顺序,不能只依赖状态码或 multipart 段数。

    缓存读取隔离验证

    三项原方法探针用临时目录运行 read()、read_aux()、hash_url() 和原子写;为降低开销,只把全局 CHUNK_SIZE 从生产的 1,000,000 改为 4 字节,远端响应仍由内存假方法提供。

    受控输入 实际结果 边界
    请求 4 字节,假来源仅给 ab 返回 2 字节,缓存也保存 ab,位置却推进到请求终点 4 缓存存在性代替内容长度/范围校验,短块可成为持续错误输入
    read(ll=0) 返回空字节,但仍取一次 4 字节块并写缓存 零长度请求没有在 I/O 前早退
    相同 URL 路径,仅签名查询串变化 新请求读到前一查询串的 old! 缓存,没有调用假远端 缓存键不含查询串或远端内容版本

    这些结果验证原函数在缩尺块长下的控制流,不能证明真实 1 MB 块的性能、并发缓存写入、远端授权差异或实际模型回放输出。

    对模型与硬件回归的影响

    FileReader() 将 HTTP(S) 交给 URLFile;LogReader 与视频 FfmpegDecoder.get_iterator() 因此可能消费这些缓存块。若服务端忽略 Range 返回 200 全文件,非零偏移读可取得错误片段;若旧缓存与新 URL 查询串共享键,回放可能混合版本。因此 HEAD 成功、缓存命中、Cap’n Proto 可解析或 FFmpeg 能输出帧,都不能单独证明输入字节与固定参考数据一致。

    现有 test_caching.py 检查默认池配置、正常缓存与非缓存读的一致性、缺失文件恢复和淘汰;其中部分测试访问公网,还有测试会删除下载缓存根目录,故本次没有运行该上游套件。隔离探针现已覆盖忽略 Range 的 200 响应、multipart 尾部 CRLF、短缓存块、零长度读取和查询串共用键;并发清单覆盖、真实云端版本和内容哈希仍未动态验证。

    分块缓存的键、长度与时序都缺少内容身份:hash_url() 用 URL 路径的 MD5 作键,长度缓存单独落盘且不随远端 ETag/内容版本失效,prune_cache() 的容量估计只按条目数乘块大小,不含真实块大小、_length 文件与并发写入。因此「缓存命中」只说明本地存在文件,不说明该文件与当前远端内容一致。

    面向插件化重构,远端数据源应携带固定内容摘要/大小、ETag 或版本 ID,并将身份和版本纳入缓存键;每次 Range 响应检查状态、Content-Range、长度及顺序,完整文件下载后校验 SHA-256。缓存清单应可并发更新且按实际字节计量,明确过期/撤销与隔离域。自制硬件的模型回归必须保存完整输入清单,而不是仅保存可变 URL。

    常见问题

    带签名的 URL 会共用缓存吗?

    会。hash_url() 只对去掉 ? 之后的 URL 取 MD5,查询串不入键;若响应内容由查询参数决定,可能读到另一版本甚至另一授权上下文的字节。这里的 MD5 只是文件名,不是内容完整性校验。

    缓存命中的块长度可信吗?

    不可信。缩尺探针里请求 4 字节、假来源只给 2 字节时,read() 返回 2 字节、缓存也保存 ab,但位置推进到请求终点 4;再次读取会直接复用这个短块。

    服务端忽略 Range、返回 200 全文件会怎样?

    请求 bytes=5-7 却拿到完整 10 字节时,原函数原样返回 10 字节,read_aux(ll=3) 从位置 5 前进到 15,既不按 Range 裁切也不校验 Content-Range。

    怎样防止回放混合版本?

    让远端数据源携带固定内容摘要/大小、ETag 或版本 ID,并把身份与版本纳入缓存键;每次 Range 响应检查状态、Content-Range、长度及顺序,完整下载后校验 SHA-256。模型回归应保存完整输入清单,而不是仅保存可变 URL。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/URLFILE_RANGE_CACHE_INTEGRITY_DEEP_DIVE.md(HTTP Range、分块缓存与回放完整性)
    • 项目 02 —— openpilot-research/LOGREADER_DECODE_ITERATION_LIFECYCLE_DEEP_DIVE.md(消费缓存块的日志解码器,作为本文的补充阅读)

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 常规 uploader:候选选择、HTTP 结果与退避循环

    结论:常规 uploader(system/loggerd/uploader.py)每轮只上传一个对象,且它的「成功」是本地终态,不是远端交付回执。源码用同一个 upload_success 事件与 user.upload=1 标记覆盖 200/201 与 401/403,把「零字节/超限文件跳过」也一并记为成功;因此不能据事件或 xattr 推断服务端已存完整对象。它也不是 Athena 的指定 URL 队列,两套逻辑要分开看。

    函数级调用链

    基线是本地 source/ 快照 c8fb906,结合静态阅读及四项原 upload() 假依赖探针;没有发网络请求、清锁、写真实扩展属性或运行设备测试。

    函数 具体行为
    get_directory_sort() / listdir_by_creation() 路段名按末个 -- 拆成两段、右对齐并在左侧补零后排序;这是目录名排序,不是读取文件创建时间。只选日志根下一层目录。
    clear_locks() 进程启动时遍历根目录各子项,删除名称以 .lock 结尾的文件;未逐项确认对应写入者已退出。
    list_upload_files() 跳过含任意 .lock 文件的目录及 user.upload=1 文件;计费网络下限制 qcamera.ts 的路段前缀。枚举不等于会被下游选中。
    next_file_to_upload() 先物化全部候选到列表,第一轮按路径包含 crash/ 或 boot/ 选;第二轮只选 qlog、qlog.zst、qcamera.ts。没有候选则返回 None。
    do_upload() / upload() 用设备令牌获取上传 URL/headers;按 key 与原文件扩展名决定是否压缩,然后 requests.put(..., timeout=10);压缩源完整进入内存缓冲。

    排序与清锁两处都带假设。目录排序只是把路段名按末个 -- 拆开、右对齐补零后比较字符串,并非读取文件创建时间;清锁则在启动时直接删除所有以 .lock 结尾的文件,没有逐项确认对应写入者已退出。两者都只保证「本地看起来就绪」,不等于段已经写完或写入者已经停止。

    选择与标记的精确语义

    list_upload_files() 的产出是候选流,next_file_to_upload() 才决定本轮只上传一个对象。其第一优先级按完整路径中的 crash/、boot/ 子串匹配,第二优先级仅包含 immediate_priority 的三个文件名;普通路段 rlog、前后/座舱原始视频即使在候选流中,也不会被常规循环直接选中。还需注意 MAX_UPLOAD_SIZES 只含 qlog 和 qcam 文件名,不能把它当作对 qlog.zst 或 qcamera.ts 的通用大小上限。

    传输实现还有一处容易被误解的地方:get_upload_stream() 对未压缩文件返回文件句柄,对压缩文件则先完整压缩到 BytesIO 再上传,所以「边上传边压缩」并不是本路径的实现;do_upload() 还忽略返回的压缩后长度,本层没有独立校验上传字节数与对象摘要。FAKEUPLOAD 会令非 412 的 URL 响应直接返回假成功,只适用于隔离测试,不是生产交付回执。

    结果分支 源码处理 能/不能推断什么
    零字节 / 命中上限且超限 不调用上传 API,但仍置 success=True 并写 user.upload=1。 只说明本地跳过,不说明远端收到。
    412 URL API 或 PUT 的 412 记为 upload_ignored。 表示被忽略,不等于失败或成功。
    200/201 与 401/403 都走名为 upload_success 的事件和本地标记。 不能据该事件或 xattr 推断服务端已存完整对象。
    setxattr() 失败 通常只记事件,函数仍返回 True。 文件可能下轮被再次选择。

    还有一个条件性错误路径:last_exc 只在尝试 do_upload() 的分支初始化;零字节或超限文件直接进入成功标记分支。如果此时 setxattr() 抛 OSError,其异常处理日志引用未定义的 last_exc,会引发 UnboundLocalError 并退出当前调用,外层 main() 没有包住 step()。假 xattr 探针分别复现了零字节与超限文件的原函数行为,但并未执行真实文件系统故障。另外,do_upload() 的 debug 日志还会记录预签名 URL 和 headers,这些值可能含短期访问凭据,生产日志策略需单独审计。

    网络调度与测试边界

    main() 从 deviceState 读 networkType/networkMetered,SubMaster.update(0) 后没有在这里核验消息新鲜度或外层有效位。FORCEWIFI 仅把用于「是否无网」的局部 network_type 设成 Wi-Fi,传给 step() 的仍是原始 deviceState 类型及计费位。

    情形 睡眠/退避
    无网 睡 60 秒(驻车)或 5 秒(行车)。
    无候选 以同样基数加随机抖动等待。
    成功后 基数 0.1 秒。
    失败时 基数先翻倍、最大 120 秒,再睡基数到两倍之间。

    UPLOADER_SLEEP=0 会跳过睡眠,适合测试而非目标硬件常态。test_uploader.py 用 mock URL API 与 FAKEUPLOAD 覆盖正常/412、目录顺序、锁文件和 xattr;它没有验证真实 HTTP PUT、401/403、预签名 URL 解析失败、零字节且 xattr 失败、计费网络状态切换或掉电恢复。测试中的 clear_locks 确认启动时会删除锁,但不证明活动 writer 不受影响。本轮未运行原仓库测试;四项隔离探针只覆盖 upload() 局部控制流。

    自制硬件的上传插件契约

    自制硬件的上传插件应把「未尝试/待传/传输中/被服务端接受/拒绝/本地跳过/确认存储」分别持久化,并以文件摘要或对象版本核对交付结果。上传器与 loggerd 应协商段关闭与锁所有权,不能在不确定写入者状态时全局清锁;对网络费用、RAM 中整文件压缩、带宽、eMMC 写放大和重试设预算。台架验收需要覆盖以上结果分支、网络状态失联、API/PUT 各类 HTTP 码、断电与 xattr 不可用,再验证实板资源峰值。

    常见问题

    uploader 报了 upload_success,文件一定上传成功了吗?

    不一定。源码让 200/201 与 401/403 都走名为 upload_success 的事件和本地标记,零字节或超限文件也会置 success=True 并写 user.upload=1 但根本不调用上传 API。这些都是本地终态,不能据事件或 xattr 推断服务端已存完整对象。

    为什么普通路段的 rlog 和视频不会被上传?

    因为 next_file_to_upload() 第一轮只按路径包含 crash/ 或 boot/ 选,第二轮只选 qlog、qlog.zst、qcamera.ts 这三个 immediate_priority 文件名。普通路段的 rlog 与前后/座舱原始视频即使在候选流里,也不会被常规循环直接选中。

    FORCEWIFI 会改变计费网络判断吗?

    不会完全改变。源码中 FORCEWIFI 仅把用于「是否无网」的局部 network_type 设成 Wi-Fi,而传给 step() 的仍是原始 deviceState 类型及计费位。因此计费相关的限制仍按原始状态生效。

    清锁会不会影响正在写入的日志?

    clear_locks() 在进程启动时遍历根目录各子项,删除名称以 .lock 结尾的文件,但源码未逐项确认对应写入者已退出;测试里的 clear_locks 也只确认启动时会删除锁,不证明活动 writer 不受影响。因此自制硬件应让上传器与 loggerd 协商锁所有权,而不是在不确定写入者状态时全局清锁。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/UPLOADER_SELECTION_RESULT_BACKOFF_DEEP_DIVE.md(取用了候选选择与标记语义、结果分支、退避参数、条件性未定义变量路径与上传插件契约)。
    • 项目 02 —— openpilot-research/ATHENA_RECENT_ROUTES_METERED_UPLOAD_CONTRACT_DEEP_DIVE.md(仅用于确认计费网络最近路段门的接续关系,未取用其未公开内容)。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • loggerd 测试:合成 VisionIPC、qlog 抽样与隐私门

    结论:loggerd 的测试用合成输入覆盖记录器主链,而不是真实相机链路:它自建名为 camerad 的 VisionIPC server,三路各 40 个 320×240 NV12 缓冲,以 20 Hz 等价循环发送,并随机发布 cereal 服务消息。这决定了它的证据边界——它能校验 rlog/qlog 数量与顺序、段文件集合与书签保留,却不能证明真实相机时序、AV 同步、失帧恢复或编码器 buffer 所有权。另外两处易被高估的断言是:test_rlog 没有在循环后断言发送列表已空,test_record_audio 的 ffprobe 未检查退出码。

    输入夹具:合成 VisionIPC 与随机 cereal 服务

    CEREAL_SERVICES 取 Event schema 中同时位于服务表、允许记录且名称不含 encode 的主题。_publish_random_messages() 启动 loggerd,对每个服务等读者;每轮给所有服务发新 Event,随机发 200~1000 轮,每 100 轮及结束时等读者更新,随后停止进程。源文档提醒,这个等待反映消息队列读者进度,不等同于磁盘同步完成。

    _publish_camera_and_audio_messages() 自建 VisionIPC server,三路各 40 个 320×240 NV12 缓冲;设置 LOGGERD_TEST 与段长,启动 loggerd、encoderd 后以 20 Hz 等价循环编号发送三路缓冲/CameraState、每轮 800 个 int16 零音频样本。它不启动真实 camerad,不测传感器寄存器、曝光、DMA 或时钟同步。源文档也说明该测试文件既有可在 PC 上运行的逻辑,也会调用本机编译产物、FFmpeg、VisionIPC 与日志文件。

    源文档特别指出:视频载荷来自 np.empty,未显式初始化为有意义像素;VisionIpcServer.send() 的时间戳参数是 uint64_t,夹具传入 n/fps 浮点秒值,因此不能把测试输出当真实纳秒级相机时间契约。

    夹具/检查 实际内容
    _get_latest_log_dir() 按目录 mtime 取最新日志段;依赖当前测试 Prefix/LOG_ROOT 下的目录列表。
    _gen_bootlog() 调本地 ./bootlog 二进制并在 5 秒 Timeout 内从输出解析文件路径;要求构建产物。
    _check_init_data() 只检查第一条消息类型为 initData;没有检查所有字段。
    _check_sentinel() 用第二条及末条消息核对 route/segment 起止标记。

    各测试的证据范围

    测试 断言与限制
    test_init_data_values() 写入构建身份等 Params,检查 initData 映射、版本、内核字段;AccessToken 虽在参数列表中但记录值必须为空;只覆盖该组参数,不证明其他敏感字段已净化。
    test_bootlog() 写 /tmp/launch_log,要求 bootlog 含两条消息、wall time 约 5 秒内、launchLog 相等;两次 bootlog 的 UID 不同且 BootCount 从 0 到 1;写入的 /tmp/launch_log 未在测试中删除。
    test_qlog() 从有/无 decimation 的服务各随机挑子集,只核对 qlog 中每主题的数量是否符合公式;不核对选中消息具体载荷或跨段抽样相位。
    test_rlog() 随机 5~10 种服务,逐条从相应服务的发送列表头部弹出并比较序列化字节;没有在循环后断言各发送列表已空。
    test_rotation() 随机 2~3 段、4~5 秒/段,要求每段文件名集合恰为 rlog、qlog、qcamera、三路 HEVC;不读取这六个文件的码流或每段内容。
    书签保留两组用例 对有/无 userBookmark 的段核对 xattr;不验证跨进程 xattr 负缓存、上传/删除器的后续保留决策。
    test_record_front() 真/假两种启动前 Params 值,仅比较最新段 dcamera.hevc 是否存在;不核对动态切换、内容、索引或隐私擦除。
    test_record_audio() 真/假两种启动前 Params 值,检查 qcamera 是否有音轨及 rlog 是否出现原始音频;ffprobe 子进程未检查退出码。

    test_qlog/test_rlog 使用随机服务及消息数量且未固定 seed,所以一次通过不能覆盖所有主题/抽样组合。源文档也澄清:test_rlog 的逐条比较确实检查了已读取消息的载荷,不应把缺少尾部断言误写成完全没有内容校验。

    test_rotation() 只要求每段文件名集合恰为六个文件,不读取码流内容,因此它验证的是段切分与命名,而不是每段编码的正确性。

    隐私与录音门的边界

    test_init_data_values() 只覆盖该组参数,源文档明确不证明其他敏感字段已净化。test_record_front() 只比较最新段 dcamera.hevc 是否存在。test_record_audio() 存在一个易漏点:subprocess.run(ffprobe..., capture_output=True) 未检查退出码,命令失败而 stdout 为空时,关闭录音用例的「无音轨」条件仍可能通过。源文档据此要求在插件化时区分「明确无音轨」和「探针失败」。

    书签保留的两组用例只核对 xattr,源文档指出它们不验证跨进程 xattr 负缓存、上传/删除器的后续保留决策;test_bootlog() 写入的 /tmp/launch_log 也未在测试中删除。这些残留与边界提醒说明,测试覆盖的是「可见输入 + 源码断言」,不是端到端的保留与清理保证。

    插件化验证要求

    将记录功能拆成插件时,应保留独立契约:服务注册/抽样、rlog 顺序和完整性、媒体包/索引对应、音频/座舱录制许可、书签后的保留与上传、异常关机后的文件可读性。对每个测试输入记录种子,断言发送与接收双向数量相等;FFprobe 等外部探针须检查退出码。

    真实相机插件还需带纳秒时间戳的帧级对齐、NV12 形状/stride/有效位、坏帧与断流注入。源文档最后限定:全部结论限定于源码断言与可见输入,未运行构建、FFmpeg、进程或媒体生成,因此无法确认各测试在本归档环境的实际通过情况,本 suite 也不能替代实机时序和隐私审计。

    常见问题

    loggerd 测试用的是真实相机吗?

    不是。源文档明确夹具自建名为 camerad 的 VisionIPC server,三路各 40 个 320×240 NV12 缓冲,不启动真实 camerad,也不测传感器寄存器、曝光、DMA 或时钟同步。

    为什么测试里的相机时间戳不能当真实契约?

    VisionIpcServer.send() 的时间戳参数是 uint64_t,而夹具传入 n/fps 浮点秒值,源文档明确不能把测试输出当真实纳秒级相机时间契约,具体转换效果需用构建后的绑定验证。

    test_rlog 通过是否说明日志完整?

    不足以保证完整。它逐条比较从各服务发送列表头部弹出的序列化字节,但没有在循环后断言发送列表已空,因此若日志只保留每主题的正确前缀,尾部缺失不会由这条断言本身发现。

    test_record_audio 判定「无音轨」可信吗?

    需要警惕。源文档指出 subprocess.run(ffprobe..., capture_output=True) 未检查退出码,命令失败而 stdout 为空时,关闭录音用例的「无音轨」条件仍可能通过,插件化时应区分「明确无音轨」和「探针失败」。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/LOGGERD_TEST_SYNTHETIC_VIPC_QLOG_RLOG_PRIVACY_GATES_DEEP_DIVE.md(取用了合成 VisionIPC 与随机 cereal 夹具、各测试证据范围表、隐私与录音门边界、插件化验证要求)。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • 开发日志工具的三个权限等级:转储、音频导出与状态刺激

    结论:四个开发脚本的权限差异很大,不应统称「开发插件」。dump.py 与 filter_log_message.py 主要读取消息/日志,extract_audio.py 会导出含乘员信息的音频,而 cycle_alerts.py 会主动发布核心状态主题(selfdriveState、deviceState、pandaStates)。至少两个反直觉的事实:--json 对 Cap’n Proto 原始字节调用 json.loads 通常无法解析,真正转 JSON 的是 --dump-json;cycle_alerts.py 先声明九个告警却随即重赋值为只剩两项,实际只循环 cameraMalfunction 与 cameraFrameRate。远端 --addr 不能当作权限、脱敏或租户隔离策略。

    四个脚本,三种权限等级

    把数据流和副作用放在一起看,权限差异一目了然:前三个工具的输出主要是「读出来的内容」,第四个则会争用发布入口。

    工具 数据流 实际输出或副作用
    dump.py raw_live_logreader() 订阅指定地址主题,默认为 SERVICE_LIST 全部服务;每条原始字节先按 log.Event 解码 可打印 Event、十六进制或字段;--addr 非环回时底层切换 ZMQ 并重置消息上下文
    filter_log_message.py 有路段参数时用排序后的 LogReader 查 logMessage、errorLogMessage、operatingSystemLog;无路段时实时订阅前者与 OS 日志 打印 JSON 结构化日志或 OS 日志;实时模式未订阅 errorLogMessage
    extract_audio.py ReadMode.AUTO_INTERACTIVE 选路段来源,LogReader.filter("rawAudioData") 得载荷,拼成 int16 样本 经 write_wav_file() 导出单声道 WAV,或经 play_audio() 本机播放
    cycle_alerts.py 构造 Honda 参数与随机化 SubMaster 状态,用 Events.create_alerts()、AlertManager 生成告警 每 200 个控制周期发布 selfdriveState、deviceState、pandaStates,循环不停

    dump.py 与 filter_log_message.py 的失效点

    dump.py 即使指定 --no-print 仍逐条解析 Event;--pipe 用 str(msg) 写标准输出,得到的是 Python 字节表示文本而不是原始二进制流;--json 对原始字节调用 json.loads 通常失败。--values 只检查路径第一段存在,后续 getattr 缺失可直接抛错。顶层 codecs.register_error("strict", ...) 还会影响进程内使用该错误处理器的解码行为,插件封装时不应默许这种全局修改。

    filter_log_message.py 的坑集中在时间与字段两处。等级过滤只作用于结构化日志,不影响 OS 日志;离线非 --absolute 的时间从第一条读取到的消息起算,而不是第一条匹配日志,且多个路段共享该时间原点、段间不重排序;实时模式则用原始 logMonoTime 纳秒换算秒数,两者语义不同。print_logmessage() 只捕获 JSON 解析异常,若 JSON 缺 levelnum、filename 或 msg 会抛键错误。

    工具 易踩边界 原因
    dump.py --json 通常解析失败 对 Cap’n Proto 原始字节调用 json.loads;转换应使用 --dump-json
    dump.py 解码行为被全局改写 顶层注册了 "strict" 错误处理器
    filter_log_message.py 相对时间原点不是首条匹配日志 离线时间从第一条读取到的消息起算,多路段共享
    filter_log_message.py 实时模式拿不到错误日志 实时订阅未包含 errorLogMessage

    两个工具都会打印原始事件或日志内容,可能包含标识符、位置、进程路径及异常栈,因此远端 --addr 不能作为生产环境的权限、脱敏或租户隔离策略。

    extract_audio.py:WAV 连续性与隐私

    extract_audio.py 把所有 rawAudioData.data 按 NumPy 原生端序的 int16 视为连续样本,再原样写成 WAV,未显式转换字节序。它只取第一条的 sampleRate,不检查之后的采样率变化、通道数、消息时间戳或缺包间隔,因此导出文件不承诺与原车时间轴严格连续。它先把所有消息和样本拼成内存数组,长路段可能占用较大内存。

    隐私侧还有一个关键前提:rawAudioData 缺省 should_log=False,loggerd 只有在启动时读取 RecordAudio 为真才订阅该主题。所以没有音频消息不等于麦克风硬件故障;而一旦录制,导出的语音可能含高度敏感的乘员信息。使用上,输出文件已存在时会交互确认,--play 会使用本机音频设备;只传 --play 或 -o 而不传路段名,主程序仍执行 args.route_or_segment_name.strip(),会报 AttributeError。

    cycle_alerts.py:发布核心状态的边界

    cycle_alerts.py 先声明九个可听告警,但随后把 alerts 重赋值为仅 cameraMalfunction 与 cameraFrameRate 两项,本快照实际只循环这两项。它随机改写 SubMaster 内的设备状态、模型速度、进程列表和主题 alive/valid/freq_ok,没有对这些订阅数据调用 sm.update(),由此产生的告警文本是随机夹具结果,不是车辆健康检查。

    发送消息由 new_message() 构造,默认 Event valid=False,脚本没有覆写。即便如此,在实际设备上争用 selfdriveState、deviceState、pandaStates 的发布入口仍会干扰 UI 和状态消费者,不能作为行车调试插件。脚本没有发送 sendcan 或直接控制 ECU,但这不等于发布核心状态没有风险。

    硬件与插件化约束

    建议把原始消息/日志检索、音频导出、状态刺激拆成三个权限等级,而不是统称「开发插件」:观测插件仅获指定主题、时间范围及字段脱敏后的只读视图;音频导出需要单独授权、留审计并限定本地文件生命周期;状态刺激仅允许仿真进程或隔离测试总线,不能发布到生产 deviceState/pandaStates。源文档明确这些是重构要求,不是当前工具已有的隔离保证。

    常见问题

    dump.py 的 –json 为什么常常解析失败?

    因为它对 Cap’n Proto 原始字节直接调用 json.loads(msg),通常无法作为 JSON 解析。源文档指出真正把 Event 转为 JSON 文本的是 --dump-json;--pipe 得到的也只是 Python 字节表示文本,不是原始二进制流。

    导出的音频能当作和原车同步的时间轴证据吗?

    不能。extract_audio.py 只取第一条的 sampleRate,不检查之后的采样率变化、通道数、消息时间戳或缺包间隔,而且是按 NumPy 原生端序把 int16 视为连续样本原样写 WAV,因此导出文件不承诺与原车时间轴严格连续。

    cycle_alerts.py 会循环几个告警?

    本快照实际只循环两个:cameraMalfunction 与 cameraFrameRate。脚本先声明九个可听告警,随后把 alerts 重赋值为这两项,且告警文本来自随机夹具结果,不是车辆健康检查。

    这些开发工具能当作「只读」吗?

    不能一概而论。前三个工具主要读取数据,但远端 --addr 不是权限或脱敏策略;cycle_alerts.py 则主动发布三个核心状态主题,会干扰 UI 与状态消费者。音频导出还涉及高度敏感的乘员信息,应单独授权与审计。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/DEV_LOG_AUDIO_ALERT_TOOL_BOUNDARIES_DEEP_DIVE.md(取用了四个开发脚本的数据流与副作用、转储与日志过滤失效点、音频导出连续性与隐私门、告警刺激的真实范围)。

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11

  • LogReader 生命周期:reset 不清缓存与跨片段不归并

    结论:LogReader.reset() 只重算路径清单、并不清 __lrs;原类探针确认先读 first、把 identifier 改成 second 后调用 reset(),迭代仍返回 first 的缓存而未读取第二文件。sort_by_time=True 只在该文件内部排序,多个文件按 logreader_identifiers 列表顺序拼接、不做跨片段归并:假事件探针在两个文件内分别给出 30,10 与 20,启用排序后整体输出 10,30,20。以下为固定快照 c8fb906 的静态阅读与隔离探针,未读取真实路段、未执行进程回放。

    关键函数与数据契约

    解码围绕「单文件一次读入 + 惰性片段加载 + 事件包装」展开。下表列出关键函数、源码事实与插件化含义。

    函数 源码事实 风险与插件化含义
    save_log() 把每个事件重新序列化并 b"".join() 成完整缓冲;按输出后缀可选 bz2/zstd 压缩,再直接 open(..., "wb") 写目标 序列化异常发生在打开目标前;写入异常可能留下被截断的旧目标;无隐私净化、哈希核验、原子替换或断电持久化
    decompress_stream() / _LogFileReader.__init__() FileReader 一次 read() 整个文件;先按 .bz2/.zst 扩展名,否则按 BZh9/zstd 魔数决定解压 后缀优先于魔数;错后缀会进入对应解压器并失败;压缩输入、解压输出与事件列表可能同时占内存
    损坏事件分支 遍历 Cap’n Proto 事件时若抛 capnp.KjException,发出 RuntimeWarning,保留异常前已加入的事件 消费者仍可得到合法前缀,但没有返回「日志已截断」的结构化状态
    CachedEventReader which() 首次计算后缓存 union 名称,普通字段访问交给原事件;__reduce__ 用于跨进程 只是读取加速包装,不验证事件 schema、有效位或字段语义
    _LogFileReader.__iter__() 默认逐个返回包装事件;only_union_types=True 时调用 which(),判别失败则跳过 「仅 union 类型」会静默丢掉无法判别的事件,不是修复坏日志;sort_by_time 仅文件内排序
    LogReader._get_lr() / __iter__() 按索引首次构造 _LogFileReader 后保存在 __lrs;多文件按标识列表顺序拼接 已加载事件列表长期留存;无跨片段归并
    LogReader.reset() 重新计算 logreader_identifiers,没有清 __lrs 改标识后迭代仍返回旧缓存;源码内仅构造函数调用它
    run_across_segments() / from_bytes() 前者用进程池 imap 按片段调用并聚合;后者以字节缓冲构建单文件读取器 存在序列化/内存成本;from_bytes(b'') 会触发空缓冲回退,不是可用空日志

    解码与迭代的边界

    「能解析」并不等于「完整」。坏 union 过滤会静默丢事件;dat=b'' 因 if not dat 被当作未提供而尝试打开路径。下表的受控输入在原函数分支下可复现。

    输入 / 操作 实际结果 边界
    压缩后缀与实际格式不一致 后缀优先,错后缀进入对应解压器并失败 compress=False 即使后缀是 .zst 也写原始字节
    dat=b'' 被当作未提供,尝试打开路径 不是可用的空日志表示
    坏 union + only_union_types=True 判别失败的事件被跳过 静默丢失,不是修复
    两个文件分别给 30,10 与 20 启用排序后整体输出 10,30,20 确认没有跨片段时间归并
    reset() 后同索引 返回旧缓存,未读取新文件 路径清单已变但事件缓存未失效

    损坏事件分支保留异常前已加入的事件并发 RuntimeWarning,但没有返回「日志已截断」的结构化状态;消费者仍可得到合法前缀,因此完整性检查必须比较预期段、时间跨度、消息数与内容摘要。同理,only_union_types=True 会静默丢掉无法判别的事件,这不是修复坏日志。

    故障传播与现有测试

    六项导出隔离探针用原 save_log()、假消息和真实 bz2/zstandard 库验证:未知后缀直写拼接字节;bz2/zstd 后缀能解压回原字节;compress=False 可产出后缀与实际格式不一致的文件;序列化失败时现有目标保持原状;模拟写入失败时现有目标被 wb 截断并仅留下两字节。它未测试真实 Cap’n Proto 消息、断电持久性或大文件内存峰值。

    该导出函数的调用方包括 run_process_on_route.py、进程回放测试与 PlotJuggler 桥;PlotJuggler 明确用 compress=False 写临时 .rlog。已有 test_logreader.py 验证重复迭代只构造一次 _LogFileReader,并覆盖文件内排序与坏 union 过滤。

    并行与容错侧还有两点:run_across_segments() 用进程池 imap 按片段调用函数并聚合结果,from_bytes() 以字节缓冲构建单文件读取器;并行处理函数与事件包装存在序列化/内存成本,from_bytes(b'') 还会触发空缓冲回退。CachedEventReader 与 which() 缓存只是读取加速,不验证事件 schema、有效位或字段语义。日志的输入与输出都不是内容摘要证明——「可解析」既不代表完整,也不代表与固定参考一致。

    LogReader.first() 与 filter() 都会触发迭代,第一次调用可能下载/解压所需文件;重复调用使用缓存,但仍遍历事件。若读取的是远端来源,HTTP HEAD 探测成功与实际 FileReader.read() 完成也在不同阶段;任何解压/解析失败需要被单独归档,而不能被上层概括成「该路线不存在」。

    硬件与插件化验收

    回放输入插件应交付固定有序片段清单、内容摘要、压缩格式、schema 版本、消息条数和时间覆盖;解析器输出「完整/截断/坏 union/解压失败」的显式状态。单段读入应设内存与解压比上限,必要时改流式处理;在跨片段时间归并前校验时间域与单调性。

    reset/热重载必须同时使路径清单与已载入事件缓存失效,或把不可变快照做成新实例,不能仅更新标识列表。日志导出应由受信任边界执行字段级隐私审查、目标路径约束、临时文件与原子发布,再记录文件摘要;读取方应按魔数或受信任元数据核对真实压缩格式,不只看扩展名。

    常见问题

    reset 之后会读到新的文件吗?

    不会自动生效。reset() 只重算 logreader_identifiers,不清 __lrs;原类探针确认先读 first、改 identifier 为 second 后调用 reset(),实际迭代仍返回 first 的缓存。

    sort_by_time=True 会跨文件排序吗?

    不会。它只在该文件内部按 logMonoTime 排序;多个文件按标识列表顺序拼接,探针给出的整体输出 10,30,20 即说明没有跨片段归并。

    only_union_types 能修好坏日志吗?

    不能。它在迭代时调用 which(),凡 union 判别失败的事件都被跳过,是静默丢弃而不是修复;完整性检查必须比较预期段、时间跨度、消息数与内容摘要。

    save_log 是原子写吗?

    不是。序列化异常发生在打开目标之前;写入异常则可能留下被截断的旧目标。该路径没有隐私净化、哈希核验、原子替换或断电持久化。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/LOGREADER_DECODE_ITERATION_LIFECYCLE_DEEP_DIVE.md(单文件解码、惰性片段加载与迭代契约)
    • 项目 02 —— openpilot-research/LOG_SOURCE_AVAILABILITY_CACHE_DEEP_DIVE.md(日志来源发现与可用性,作为本文的补充阅读)
    • 项目 02 —— openpilot-research/URLFILE_RANGE_CACHE_INTEGRITY_DEEP_DIVE.md(远端字节与缓存完整性,支撑「HEAD 成功不等于内容一致」)

    说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

    更新记录

    首次发布 2026-10-11 最近核对:2026-10-11