标签: 数据管线

  • 回放三入口:可视化订阅与 CAN 硬件回放的边界完全不同

    结论:openpilot 的「回放」有三个入口,用途完全不同:C++ replay 可执行文件发布历史消息与 VisionIPC 视频并驱动终端 UI;Python ui.py 只订阅数据做调试可视化;而 can_replay.py 会直接向 Panda Jungle 发 CAN 帧,是唯一对硬件有直接副作用的入口。三者中最容易被误用的就是把 --allow 当成 CAN TX 防火墙,或把可视化订阅当成只读安全——can_replay.py 不是只读分析工具。

    本文基于固定源码快照,静态梳理命令行、终端 UI、Python 调试画面与独立的 CAN 硬件回放脚本。仅做静态阅读,未运行任一回放命令、下载数据、连接/重置/刷写硬件或发送 CAN。CAN 报文解析的健康语义见相邻专题。

    三类入口:能力边界必须先分清

    C++ Replay 是开发进程,启动时会把日志中的 CarParams 写入本地 Params,并可切换 ZMQ=1 让其他 UI/PlotJuggler 订阅。--prefix 为实例指定前缀,但仅靠服务过滤或视觉界面并不能隔离同机所有消息、参数和设备资源,必须在独立开发环境核对具体命名空间。

    入口 实现 能力 关键边界
    C++ replay 可执行文件 main.cc 发布历史消息与视频、终端 UI、benchmark 启动改写本地 Params;--cache 最终仍被钳制为至少五段
    Python 可视化 ui.py raylib 窗口、相机画面、模型轨迹、告警曲线 只订阅数据;曲线是调试可视化,不是严格多主题时间对齐采样
    CAN 硬件回放 can_replay.py 直接向 Jungle 发 CAN 帧 对板卡状态有直接副作用,绝非只读分析工具

    parseArgs() 解析 --allow/--block、段缓存数、起点、倍速、--auto、--data_dir、--prefix、视频与 benchmark 标志,参数转换用 atoi/atof;缓存和倍速在 main 中按正数过滤,起点由 Replay::start() 处理。未知路段、无可用片段仍在后续加载失败。

    main() 随后可选创建 OpenpilotPrefix,实例化 Replay,设置缓存与倍速并加载路段:普通模式启动 ConsoleUI,benchmark 模式直接等待并打印时间线。两个边界要记牢:--cache 最终仍由 setSegmentCacheLimit() 钳制为至少五段;benchmark 只是发布吞吐统计,不是控制算法正确性验证。

    终端 UI 与 Python 调试画面

    ConsoleUI::exec() 约 20 Hz 刷新终端:空格暂停、s/m 前后跳、e/d/t/i/w/c 按时间轴跳、+/- 变速、回车输入绝对秒数。这里的 seek 输入会先暂停再恢复,但未检查 scanw 返回值;错误输入会保持初始 choice=0 并尝试 seek。这是 UI 层行为,不等同于引擎 seek 的参数校验。

    函数 功能 边界
    handleKey() 空格暂停、+/- 变速、回车输入绝对秒数 未检查 scanw 返回值,错误输入仍会 seek
    updateStatus() 从 carState/vehicleParameters 展示车速与转向参数 数据来自回放主题,不是实时车辆状态
    updateTimeline() 从 qlog 生成的 Timeline 绘制使能与告警 缺 qlog 时时间轴可不完整

    Python 侧 ui_thread() 创建 raylib 窗口,用 CameraView("camerad", NARROW_ROAD) 读取 VisionIPC,相机画面与消息图层分开获取;SubMaster 订阅车况、规划、控制、雷达、标定、模型、告警等主题。一个明确的窄边界:get_arg_parser() 接受 ip_address 和 --frame-address,但主入口只用 ip_address 设置 ZMQ 并传给 SubMaster,--frame-address 在本文件中没有被传入 CameraView。远端消息可连接不代表远端帧可显示,该参数在当前实现中无效。

    CAN 硬件回放:独立且危险的边界

    can_replay.py 的四个阶段各有副作用。load_route() 读取 carParams 和所有 can 消息转为逐帧 CAN 列表,无入参时使用默认公开路段;脚本没有依据目标车型、总线拓扑或插入设备自动校验安全适配。connect() 持续枚举 Jungle,每个序列号启动独立发送线程——新连接设备会自动进入发送流程。

    阶段 源码行为 安全含义
    载入 load_route() 读 carParams 与全部 can 消息 来源与内容必须固定、审计;不自动校验安全适配
    发现 connect() 枚举 Jungle 并起发送线程 新设备自动进入发送流程,不是只读工具
    设备配置 send_thread() 在 FLASH 存在时调用 flash(),随后 reset()、清缓冲、设 500 kbps、开点火与供电 对板卡状态有直接副作用;绝不能连实车总线运行
    发报 以 DT_CTRL 节拍按记录帧组循环,仅保留 bus 不高于 2 仅过滤总线号不构成安全认证;依赖物理隔离台架

    FLASH 是「存在即触发」,不是解析布尔值。发送循环仅过滤总线号,空日志会在取模位置失败,USB 超时被吞掉,不能把循环存活视作实际发送成功。

    插件化落点与验证顺序

    把三类入口分开:ReplayControl 只管理路线、时间、速率和服务白名单;ReplayVisualization 只订阅数据且不得取得 CAN 发送能力;HardwareCanStimulus 必须是独立、显式启用的台架插件,含设备白名单、物理隔离确认、报文/总线过滤、发送上限、急停和全量审计。

    1. 不能让通用插件注册机制自动发现并启用硬件刺激插件。
    2. 不能把 --allow 当作 CAN TX 防火墙。
    3. 验证先从固定日志与 mock sink 开始,再查 Params/ZMQ/VisionIPC 隔离、CLI 异常输入、UI 缺主题/缺帧、消息时序。
    4. CAN 输出仅在独立 HIL 台架上用目标硬件与外部监测完成。

    当前目录的 test_route.cc 只覆盖路线范围解析;本文没有运行 GUI、benchmark 或 HIL 测试,也未发现针对上述入口分支的同目录测试。

    常见问题

    can_replay.py 是只读分析工具吗?

    不是。connect() 会持续枚举 Jungle,每个序列号启动独立发送线程;send_thread() 在 FLASH 存在时调用 flash(),随后 reset()、清 CAN 缓冲、设 500 kbps、打开点火与供电。它对板卡状态有直接副作用,绝不能在连接实车总线时运行。

    replay 的 –allow 能当 CAN 发送防火墙吗?

    不能。--allow/--block 只是服务过滤;can_replay.py 的发送循环仅按记录帧组循环发报、只保留 bus 不高于 2,仅过滤总线号不构成安全认证,脚本依赖物理隔离的 Jungle 台架。

    为什么 ui.py 的 –frame-address 不生效?

    源码里 get_arg_parser() 接受 ip_address 和 --frame-address,但主入口只用 ip_address 设置 ZMQ 并传给 SubMaster,--frame-address 在本文件中没有被传入 CameraView,所以远端消息可连接不代表远端帧可显示。

    replay 曲线是严格时间对齐采样吗?

    不是。曲线历史仅在 carState 更新时前移,其余主题值写入最后一行,因此它是调试可视化,不是严格的多主题时间对齐采样;终端时间轴缺 qlog 时也会不完整。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/REPLAY_FRONTENDS_CAN_DEEP_DIVE.md(取用了 C++/Python/CAN 三类入口的功能矩阵、终端 UI 边界与硬件回放四阶段副作用)。
    • 项目 02 —— openpilot-research/REPLAY_ENGINE_DEEP_DIVE.md(仅用于确认回放引擎的消息发布与加载边界,未取用其未公开内容)。

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

    更新记录

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

  • deleter 空间回收:书签只延后删除与五名额占用

    结论:user.preserve 书签只把目录往后排、并不使其成为不可删除状态;get_preserved_segments() 在解析段号之前先取「倒序前五个带标记目录」,若带标记的 boot/crash 或非段目录占了名额,较早的真实书签段就不会进入延后集合。容量阈值用严格小于:可用字节 < 5 GiB 或可用比例 < 10%;两次 statvfs 读取失败时各自返回高于阈值的默认值,因此缺测本身不触发删除。以下为固定快照 c8fb906 的静态阅读加假目录/临时文件系统探针,未删除真实路段、未填盘、未做车端测试。

    函数与数据依赖

    回收逻辑集中在 system/loggerd/deleter.py,它与容量配置、目录枚举和上传器存在共享依赖。下表的语义以源码事实为准。

    函数 源码语义
    has_preserve_xattr() 查询某目录 user.preserve == b'1';走进程内 xattr_cache,跨进程变更可能不可见
    get_preserved_segments() 从名称排序列表尾部向前找最多五个带标记目录,解析末个 -- 后的段号,将该段及前两段加入延后集合
    get_available_bytes() / get_available_percent() 各自独立调用 statvfs();OSError 时按调用方默认值返回,两个读数不是原子快照
    listdir_by_creation() 只列根目录下被判定为目录的项目,再按路段名称排序;函数名不表示按 inode 创建时间
    deleter_step() 空间低于任一阈值后枚举、分组排序、检查 .lock,尝试 shutil.rmtree();成功即返回,单次最多删一个目录
    deleter_thread() / main() 空间不足时每 0.1 秒重试,正常每 30 秒;主入口不另行处理步骤异常

    优先级与保留语义

    deleter_step() 使用 sorted(dirs, key=lambda d: (d in DELETE_LAST, d in preserved_dirs))。由于 False < True 且排序稳定,实际分组为「普通未保留、普通保留、boot/crash 未保留、boot/crash 保留」,同组内维持 listdir_by_creation() 的名称顺序。

    关键边界是:user.preserve 只延后删除。当普通路段删完而空间仍不足时,书签段、boot 与 crash 仍可被删。这里也不检查 user.upload 或远端对象存储回执,因此「待上传」与「已书签」都不是不可删除状态。

    另一个易被忽略的点:get_preserved_segments() 的 enumerate(filter(has_preserve_xattr, reversed(dirs))) 在解析路段格式之前就计算五个名额。假目录探针用两个非段目录占位,确认较早的真实书签段不进入集合——这不是实测删除事故。

    每个候选先用 os.listdir(delete_path) 检查任意 .lock,有锁即跳过。但 uploader.main() 启动时又会清理日志根下所有锁文件,因此锁的存在并非跨进程可靠 lease。若 loggerd 仍在写而锁被误清,静态看 deleter 可能把该目录视为可删;实际触发取决于进程调度和空间状态。shutil.rmtree() 仅在无锁分支执行,成功后返回 (True, path),不能证明释放字节数达到目标,也没有按本轮删除内容重新测量容量。

    缺测、异常与循环

    阈值分别使用严格小于:可用字节 < 5 GiB,或可用比例 < 10%。两个 statvfs 读取失败时各自默认高于阈值,所以缺测本身不触发删除;若仅一次失败,另一次仍可能触发。由 f_blocks == 0 引起的除零没有被 config.py 的 OSError 捕获。

    场景 实际行为与边界
    空间不足但无目录可删 返回 (True, None),外层每 0.1 秒再尝试,不转入较长间隔
    rmtree() 抛 OSError 在循环内捕获并转向下一目录;但书签 xattr 过滤与每项 os.listdir(delete_path) 位于该 try 之外
    listdir() 抛 FileNotFoundError 直接穿透 deleter_step();枚举后目录被并发删除或 xattr 读取异常时,错误可穿透 deleter_thread()
    目录符号链接 listdir_by_creation() 会把它列为候选,但 shutil.rmtree() 拒绝直接删除该链接,异常被记录后继续删普通段;链接及目标内容未动

    本机临时目录探针只证明函数级行为,未复现真实跨进程调度、Pipe 或断电。目标车端的 Python、文件系统与并发行为仍需另测,不能仅据路径字符串认定候选为受信任普通目录。

    现有测试与插件化验收

    test_deleter.py 用替换的 statvfs 和临时路段覆盖阈值、普通/书签/boot/crash 删除顺序及锁跳过;未覆盖缺测、f_blocks=0、异常目录、跨进程 xattr 陈旧、被其他进程并发删除、活动锁被清理或删除失败后的持续重试。

    四项原函数假目录探针可用 python3 -m unittest probes.test_deleter_preserve_failure_contract -q 复现书签名额、锁跳过、删除失败续试与列目录异常;两项临时文件系统探针执行原 listdir_by_creation()、书签分组与 deleter_step(),使用真实 os.listdir()/shutil.rmtree() 仅操作自动清理的临时目录:书签段及 boot 确实排在普通段之后,目录符号链接本机删不掉但不会阻断后续普通段删除。容量与 xattr 来源仍是假依赖,不覆盖运行中的 loggerd、真实日志根、断电或目标板文件系统。

    面向插件化,应由存储服务发放段级写入 lease,并把「段已关闭、远端已确认、书签保留期限、空间紧急覆盖」做成明确状态;删除前重新验证目录身份与 lease,异常统一记录并维持监督健康。容量插件须报告挂载身份、可用字节/比例、采样时间与缺测,不把默认值伪装成真实容量。

    常见问题

    带 user.preserve 的书签段会被删除吗?

    会。user.preserve 只延后删除;当普通路段删完、空间仍不足时,书签段、boot 与 crash 仍可被删。系统也不检查 user.upload 或远端回执。

    为什么较早的书签段可能没被保护?

    因为 get_preserved_segments() 在解析段号之前先取倒序前五个带标记目录;若 boot、crash 或其他非段目录占用了名额,它们随后因无合法段号被跳过,而较早的真实书签段不再进入延后集合。

    删除阈值是多少,缺测会怎样?

    阈值是严格小于:可用字节 < 5 GiB 或可用比例 < 10%。两次 statvfs 失败时各自返回高于阈值的默认值,缺测本身不触发删除;仅一次失败时另一次仍可能触发。

    锁能防止删到正在写入的段吗?

    不能保证。deleter 只检查目录内是否出现 .lock,而 uploader.main() 启动会清理日志根下所有锁文件,因此锁不是跨进程可靠 lease。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/DELETER_PRESERVE_ORDER_FAILURE_CONTRACT_DEEP_DIVE.md(目录级回收的优先级、书签名额占用与失败边界)
    • 项目 02 —— openpilot-research/LOG_STORAGE_CAPACITY_FAILOPEN_CONTRACT_DEEP_DIVE.md(容量测量与缺测默认值,作为本文的补充阅读)

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

    更新记录

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

  • 崩溃采集的三道门:100 MB 过滤、0o100640 模式与 12 小时暂缓

    结论:崩溃采集不是「一律上报」,而是一条跨进程链路,且至少被三道门约束:报告门(构建来源为官方 origin、本地已注册、非 PC 三者同时成立)、扫描过滤门(只接受精确 mode 0o100640 的 .crash,超过 100 MB 直接丢弃)、上传门(计费网络暂缓最近 12 小时的 crash/、HTTP 401/403/412 不再重试)。要特别记住两点:100 MB 阈值不是隐私过滤,完整 crash 副本可能含路径、进程环境与内存相关信息;而「已进入本地处理」也不等于云端已保存。

    入口:报告门只在进程启动时采样一次

    在固定源码快照里,tombstoned 被配置成非 PC 的常驻进程。它的 main() 启动时计算一次 should_report,要求构建元数据的官方 origin、is_registered_device() 与「非 PC」三项都成立。这里的「已注册」只依据本地 DongleId 非空且不为 UnregisteredDevice,不是每次崩溃都向后端重新鉴权。

    两个时序后果必须分开看。第一,should_report 不在循环中刷新,运行期间身份或来源发生变化不会立即改变本实例的处理门。第二,启动时 clear_apport_folder() 会遍历 /var/crash/* 尝试删除,之后才建立初始快照——这意味着重启前尚未处理的 crash 文件可能直接丢失,源文档给出的动机是避免重复报告。

    扫描与本地处理:三道门各自不管什么

    采集循环每五秒用 get_tombstones() 扫描至多 1000 个目录项,用「路径 + ctime」的集合差异找出新条目。下面这张表把各道门的条件和边界摊开——每一道的「不管什么」比它「管什么」更容易被误读。

    门 / 环节 条件 边界与后果
    报告门 官方 origin 且 is_registered_device() 且非 PC,仅在启动时计算一次 运行中改变返回值不会改变本实例的删除决策
    文件模式门 .crash 必须是精确 mode 0o100640 名字以 tombstone 开头的条目不受该 mode 门约束,但报告门打开时只记录「unknown crash type」,不进入解析与复制
    大小门 超过 100 MB 记错后返回 该分支不复制也不删除原文件;大小阈值不是隐私过滤
    目录常量 TOMBSTONE_DIR="/data/tombstones/" 已定义 该常量未被 get_tombstones() 使用,实际扫描的是 /var/crash/;不能据常量名推断 Android tombstone 也被处理

    解析与落盘阶段的关键行为见下表。摘要与完整副本走的是两条路径,清洗只作用于摘要,不改写完整文件。

    阶段 源码行为
    摘要 提取 ExecutablePath、Signal,经 apport-retrace -s 获取栈,优先找 openpilot/selfdrive 栈帧;去掉形似指针的参数并剥除圆括号内容后写 cloudlog.error。外部命令设 30 秒超时
    完整报告 用日期、提交哈希与清洗后路径组成文件名,复制完整原始 crash 文件到 Paths.log_root()/crash/,随后尝试删除 /var/crash 原件
    未启用报告 对新检测到的条目尝试删除;不会保留「本地可查但不上传」的独立模式
    处理异常 异常写 cloudlog,下一轮仍把当前集合设为基线;未变化的同一文件通常不会因本次失败自动重试

    需要强调的是:摘要去掉指针、剥掉圆括号内容,只是让日志更短,并不等于对 dump 做了脱敏审计。完整 crash 可能包含路径、进程环境和内存相关信息,不能因为 cloudlog 摘要删了指针就把副本视为匿名数据。

    上游处理与下游上传是两段独立判断

    Uploader 把 crash/ 放进 immediate_folders,next_file_to_upload() 会优先选该目录的未标记文件。但「优先」仍受网络与计费策略约束:在计费连接上,最近 12 小时的 crash/ 与 boot/ 文件会暂缓;无网络时主循环不上传。因此 tombstoned 的「reporting」日志只表示进入本地处理,不代表云端已收到文件。

    Uploader.upload() 把 HTTP 200/201/401/403/412 都列为停止重试的状态,并尝试写 user.upload xattr。其中 401/403 或 412 的情形要单独记忆:处理完成不等于服务端保存成功。回收侧 deleter_step() 在空间不足时优先删普通目录,把 boot/crash 列为最后删除,但这不是永久保留——存储紧张时崩溃副本最终仍可能被删掉。

    对自制硬件与插件平台的要求

    源文档给出的重构要求可以归纳为一组「必须分开授权」的清单:

    1. 崩溃诊断插件默认只读、驻车或后台运行;原始 dump 的收集、读取和外传分别授权,并提供本地留存但禁止上传的隐私模式,避免把「是否官方 origin」当成唯一隐私开关。
    2. 在受信任核心记录采集决策与数据流:设备身份状态、用户/车队同意、文件分类、脱敏结果、上传结果与删除原因。不要把文件名清洗或摘要去指针等同于 dump 脱敏。
    3. 重构时把「启动清空、扫描上限、失败后不自动重试、上报门固定不变」列为可复现的行为差异,并用模拟 crash、权限错误、断电、磁盘满和计费网络来测试新策略。
    4. 崩溃报告管线不应向 carControl、sendcan 或 panda 安全模式写入;诊断过载或上传阻塞不得影响实时控制与驾驶员告警。

    此外要承认覆盖局限:system/tombstoned.py 没有就近的专门单元测试,工作区六项隔离探针只覆盖扫描主循环与本地文件处理。1000 条目录项上限、启动残留清理与实际文件竞争仍只有静态证据,具体 crash 内容与服务端保留策略仍需在目标硬件和服务协议上验证。

    常见问题

    100 MB 的文件大小上限是隐私过滤吗?

    不是。源文档明确指出超过 100 MB 的文件只是记错后返回,该分支不复制也不删除原文件,并特别标注「大小阈值不是隐私过滤」。完整 crash 副本可能包含路径、进程环境或内存相关信息,不能因为摘要清洗了指针就把副本当匿名数据。

    为什么「本地已上报」不等于云端收到了文件?

    因为本地与远端是两段独立判断。tombstoned 的 reporting 日志只表示进入本地处理;上传侧还受网络与计费策略约束,计费连接上最近 12 小时的 crash 文件会暂缓、无网络时不上传,而 401/403 或 412 属于停止重试状态,处理完成不等于服务端保存成功。

    重启后没处理的崩溃文件会保留吗?

    可能不会。启动时 clear_apport_folder() 会遍历 /var/crash/* 尝试删除,之后才建立初始快照,源文档的动机是避免重复报告。这意味着重启前尚未处理的 crash 文件可能直接丢失。

    崩溃报告会影响实时控制或驾驶员告警吗?

    按源文档的重构要求不应影响。管线不应向 carControl、sendcan 或 panda 安全模式写入,诊断过载或上传阻塞也不得干扰实时控制与驾驶员告警。但这些是重构要求,不是当前实现已验证的隔离保证。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CRASH_COLLECTION_PRIVACY_DEEP_DIVE.md(取用了崩溃采集的报告门、扫描与大小过滤、完整副本复制、上传与回收语义及插件化要求)。

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

    更新记录

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

  • 网络状态到费用策略:路由 metric、RSSI 分档与计量位的回退边界

    结论:网络状态是一条「慢速快照 → 分类 → 费用策略」的链路:hardwared 约每 10 秒采集一次网络/调制解调器/USB 快照,分类函数从 /proc/net/route、wpa_supplicant 与 modem 状态文件 推导类型、强度与计费位,再由 uploader、Athena 与 updated 消费。关键边界有三处:网络接口切换本身不是立即刷新触发器;蜂窝缺 signal_quality 会落入 poor 而非 unknown;未知 Wi-Fi 计费可能被当成免费网络,不能作为上传费用授权的保守依据。本文未连接 Wi-Fi、蜂窝网络或运营商账单。

    来源与关键函数

    类型判断从 get_default_route_iface() 开始:它解析 /proc/net/route,只取目的地为 00000000 且带网关标志的条目,以 (metric, 接口名) 元组最小者为接口,不单独要求 UP 标志。默认路由名匹配 wlan* 即认作 Wi-Fi、eth* 即认作以太网,否则读取 modem 的 connected 与制式,未知返回 none。SSID 与 RSSI 来自 wpa_supplicant_cmd():它用抽象 UNIX datagram socket 发命令,跳过以尖括号开头的事件消息,收到 FAIL 前缀即返回空结果,超时异常向调用方传播。

    入口 实现语义
    wpa_supplicant_cmd() 抽象 UNIX datagram socket;跳过事件消息,FAIL 返回空字典,超时异常传播
    get_default_route_iface() 取默认目的地且带网关标志的条目,按 (metric, 接口名) 最小者;不要求 UP 标志
    get_network_type() wlan*/eth* 优先;否则读 modem connected 与制式,未知返回 none
    get_modem_state() 文件缺失或 JSON 语法错误返回 {};权限异常透传;合法 JSON 非对象不检查形状
    parse_strength() / get_network_strength() 四档阈值分类;Wi-Fi 用 RSSI,蜂窝用 signal_quality,以太网直接 great
    get_network_metered() 蜂窝取 GsmMetered;Wi-Fi 按 SSID 匹配 NetworkManager keyfile 的 metered,无明确值回退基类
    hw_state_thread() 约每 10 秒或 USB 拓扑变化构造快照;容量 1,队列满丢弃新状态
    hardware_thread() 复用最后成功快照发布 deviceState.network*,并把计费位写入持久 NetworkMetered

    路由、信号与计费的精确回退

    网络类型先按 /proc/net/route 的最低 metric 默认路由接口名判断;同 metric 时按接口名字典序选取,路由行只要求默认目的地和网关标志,不检查 UP 位。格式错误的行会让解析函数抛异常,get_network_type() 则吞掉它并尝试 modem,不会跳过坏行继续比较其余路由。若 modem 虽连接但制式不在 nr/lte/utran/umts/gsm 表内,仍返回 none。

    强度方面,get_network_strength() 对以太网固定给 great,并非链路质量实测。Wi-Fi 通过 wpa_supplicant 的 SIGNAL_POLL 读 RSSI,只有大于 -100 且小于等于 0 的 dBm 才映射为 120 + clamp(dBm, -100, -20),再按四档阈值分类;恰好 -100 dBm、缺 RSSI、非法值或 socket 异常保持 unknown。蜂窝缺 signal_quality 时默认数值 0,会进入 poor,不是 unknown;读取异常则因宽泛异常捕获返回 unknown。信号质量读取与 modem 网络分类仍不是一次原子快照。

    场景 结果 边界说明
    以太网强度 great 固定值,非链路质量实测
    Wi-Fi RSSI 有效 120 + clamp(dBm, -100, -20) 后四档 仅当 dBm 大于 -100 且小于等于 0
    恰好 -100 dBm / 缺 RSSI / 非法值 unknown socket 异常同样保持 unknown
    蜂窝缺 signal_quality poor 默认数值 0,不是 unknown
    蜂窝读取异常 unknown 由强度函数的宽泛异常捕获返回

    get_modem_state() 只把文件不存在及 JSON 语法错误转为 {},权限错误会向调用方传播;它不验证 JSON 顶层必须为对象——合法数组会原样返回,get_network_type() 在需要 modem 回退时访问 .get() 可抛 AttributeError,而 get_network_strength() 会捕获异常并返回 unknown。这是控制流差异,不能据此断言真实 modem 写入了这种文件。

    计费方面,蜂窝读取布尔参数 GsmMetered(源码默认真)。Wi-Fi 先读 wpa 的 STATUS SSID,再尝试把其转义形式转换为 NetworkManager keyfile 的文本或 byte;byte; 编码,在运行态与持久目录搜索配置。首个匹配 SSID 的配置若有 metered=1/2 分别返回真/假;若为默认或未知值,会跳出搜索并回退基类,后续同名配置不再检查。SSID 缺失、读取或解析失败也回退基类,而基类对 Wi-Fi、以太网和 none 返回非计费。因此「未知 Wi-Fi 计费」在此路径可能被当成免费网络,不能作为上传费用授权的保守依据。

    从慢速快照到上传决策

    hw_state_thread() 把类型、网络信息、强度、计费位分次调用取值,采集异常只记录、不发布该次快照。约 10 秒的采样周期可被 USB 拓扑变化提前触发,但网络接口切换本身不是立即刷新触发器;队列满时新快照会被丢弃。主线程沿用最后一次快照并以较高频率重复发布 deviceState,其消息 logMonoTime 不能代替这些网络字段的实际采样年龄。

    uploader.main() 消费 deviceState.networkType/networkMetered,Athena.upload_handler() 为禁止蜂窝的任务检查计费位,updated 则读 hardwared 写入的持久 NetworkMetered 决定自动拉取节奏。三者消费的是分类/配置状态,不是计费账单、当前传输 socket 的实际路由或全链路一致快照,状态切换窗口须靠传输层重新核验。

    自研硬件与插件接口

    网络插件至少输出 link_type、default_route、connectivity、metering 四个独立字段,并携带接口 ID、来源、采样单调时间、有效位和原因。metering=unknown 应由宿主费用策略保守处理,不应默认为免费;上传器还需在每次开始与传输过程中按实际出口接口和账单策略复核。蜂窝 eSIM/SIM 身份属于敏感信息,遥测与日志插件不应默认共享 ICCID/IMEI。

    验证层面,探针只覆盖隔离控制流;台架或隔离网络测试仍应覆盖双默认路由 metric 切换、Wi-Fi RSSI 缺失、SSID 转义、真实配置读取、蜂窝 GsmMetered 切换、慢快线程交错和队列满,并同时观察 deviceState、NetworkMetered、uploader/Athena/updated 决策与真实出站接口。

    常见问题

    蜂窝信号缺 signal_quality 时为什么不是 unknown?

    因为源文档说明默认数值为 0,会进入 poor,不是 unknown;只有读取异常才因强度函数的宽泛异常捕获返回 unknown。这一差异在费用与上传策略里很关键,因为 poor 与 unknown 的保守程度不同。

    未知 Wi-Fi 的计费状态会被当成什么?

    可能被当成免费网络。源文档指出:首个匹配 SSID 的配置若为默认或未知值,会跳出搜索并回退基类,而基类对 Wi-Fi、以太网和 none 返回非计费。因此 metering=unknown 应由宿主费用策略保守处理,不能作为上传费用授权的保守依据。

    网络接口切换会不会立刻刷新状态?

    不会。约 10 秒的采样周期可被 USB 拓扑变化提前触发,但源文档明确网络接口切换本身不是立即刷新触发器,且队列满时新快照会被丢弃;主线程会沿用最后一次快照重复发布,其消息时间不能代表网络字段的实际采样年龄。

    deviceState 的 logMonoTime 能当网络状态的采样时间吗?

    不能。主线程沿用最后一次快照并以较高频率重复发布 deviceState,其消息 logMonoTime 不能代替这些网络字段的实际采样年龄。网络插件应显式携带采样单调时间与有效位。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/COMMA_NETWORK_ROUTE_SIGNAL_METERING_SNAPSHOT_DEEP_DIVE.md(取用了网络分类、强度与计费回退、慢速快照与上传决策边界及插件接口要求)。
    • 项目 02 —— openpilot-research/ATHENA_DEVICE_NETWORK_METADATA_RPC_DEEP_DIVE.md(仅用于确认设备网络元数据接口的上下文)。

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

    更新记录

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

  • 桌面片段渲染:50 ms 日志分桶、帧号映射与 Params 前缀

    结论:桌面片段渲染器 tools/clip/run.py 与车端 encoderd --clip 是两套不同实现。它把日志按 50 ms 分桶、按 20 FPS/60 秒映射帧号、再用原车载 UI 渲染并用 FFmpeg 录成 MP4。三个最容易踩的边界是:分桶起点用每段首条日志的单调时间而非视频首帧 SOF;patch_submaster() 会把 valid 等标志全置真、不保留原始 Event.valid;GuiApplication 录屏等待编码子进程却不检查非零退出码。

    一、入口和分层调用

    阶段 关键函数 数据契约
    参数与录制配置 parse_args()、setup_env() 解析路段、秒数、输出、目标 MB、倍速和镜头;设置 RECORD/RECORD_OUTPUT/RECORD_SPEED/RECORD_BITRATE,这些值在随后导入 GUI 模块时成为录制配置。
    文件来源 Route.__init__()、Route.log_paths() 本地目录或 API 远端索引按段号组装列表,缺段位置为 None;默认 rlog,不从 qlog 抽样复原。
    日志分桶 load_logs_parallel()、_parse_and_chunk_segment() 线程并行读取完整段字节,进程池解码并迁移,按首消息时间起的 50 ms 窗口保存每主题最近一条 Event。
    UI 数据注入 patch_submaster()、UIState.update() 把 UI 的 SubMaster.update 替换为逐帧写入消息快照,再运行普通 UI 状态更新和增强道路视图。
    相机取帧 iter_segment_frames()、FrameQueue 全局帧号按 20 FPS / 60 秒映射到段内帧;后台线程预取 NV12,主线程推送本地 VisionIPC。
    合成与文件 clip()、render_overlays()、GuiApplication.init_window() 用原车载 UI 渲染画面,叠加可选路段/时间/标题,GuiApplication 经 FFmpeg 录制输出 MP4。

    二、时间、图像与消息一致性

    _parse_and_chunk_segment() 以每段首条日志的单调时间为起点,而不是该段视频首帧 SOF。一个 50 ms 桶只保留同主题最后一条;时间戳跨越多个桶时只追加一次旧桶并把 next_time 跳到新消息附近,不会补齐中间空桶。clip() 随后用 frame_start 和 duration * 20 直接切这个桶列表;若日志起点偏离视频起点、消息缺口或某段不足 60 秒,桶序号和图像帧号可能错位或片段短于请求。程序只检查切片非空,不验证切片长度等于请求帧数。

    时间基准 来源 与真实相机时间的关系
    日志分桶起点 每段首条日志的单调时间 与视频首帧 SOF 可能不一致,是桶序号与帧号错位的隐患来源。
    帧号映射 20 FPS / 60 秒 按段内帧计数,不校验切片长度是否等于请求帧数。
    VisionIPC SOF/EOF frame_idx * 50 ms 起点为零且二者相同,并非原车相机时间。

    patch_submaster() 对存在于 UI 服务表中的消息把 seen、updated、alive、valid 全置真,并把原始 logMonoTime 与当前本机接收时间混合登记;它没有保留原 Event.valid,未出现的新主题也不会在此函数中被显式标成失活。因此导出画面可显示出原始日志里曾无效或已失联的数据,不宜作为告警新鲜度的权威回放。VisionIPC 发送则用 frame_idx * 50 ms 为 SOF/EOF,起点为零且二者相同,并非原车相机时间。

    --qcam 通过 FFmpeg 将整段 qcamera 解码为 NV12 并保留在内存数组;普通 fcamera 走 FrameReader.get() 随机访问。FrameQueue 只从首个非空相机路径探测宽高,后续段没有统一尺寸校验;缺目标段时主动抛错。宽路镜头只要所选区间中存在任意 ecamera 路径就整体启用,但其中某段缺文件时仍会按每一帧取宽路,导致该段失败。

    三、Params、资源和失败边界

    clip() 在进入 OpenpilotPrefix 之前导入全局 ui_state。UIState._initialize() 当场构造 Params(),而 Params::Params() 在构造时按当前 OPENPILOT_PREFIX 固定路径。随后 load_route_metadata() 在新 Prefix 中把路段 initData.params 写入另一个 Params 命名空间,但已存在的 ui_state.params 仍指向旧路径,因此 UI 的参数刷新不保证读到该路段快照。另外 load_route_metadata() 假定第一个可用日志含 initData,否则访问会抛错;缺头的路段即使视频/其他日志可用也可能提前失败。

    FrameQueue._worker() 异常后在 finally 中投递终止哨兵,队列满而消费停止时该 put() 也可能阻塞;stop() 最多等线程 2 秒。clip() 只在正常渲染循环后显式停队列、关 GUI,没有包住整个运行链的 finally;解码、VIPC 或 UI 异常时清理可能不完整。GUI 的 close_ffmpeg() 会等待编码子进程,但不检查非零退出码,因此 clip() 之后打印「saved」并不等于文件可解码。RECORD_OUTPUT 被强制改成 .mp4 后,成功日志却打印原始 -o 路径,非 .mp4 后缀输入会造成日志路径与实际文件名不一致。

    parse_args() 只要求结束时间大于起始时间,没有对负起点、零/负倍速做完整校验;默认正的目标体积下 --speed 0 会在码率计算中除零。若选中的 rlog 路径列表含 None,并发读取阶段就会失败;这与「部分路段可预览」的语义不同。

    四、面向硬件和插件化的约束

    重构建议是把路段来源、时间轴对齐、相机解码、消息状态复原、UI 渲染和文件写入划为独立插件接口,但输入必须带不可变路段 manifest(段号、内容哈希、首末单调时刻、帧 ID、相机 SOF/EOF、schema 版本)。回放宿主应独占隔离 Params/msgq/VisionIPC 命名空间,并在导入 UI 单例之前建立命名空间;不能让路段 Params 落到生产配置,也不能把过期/无效 Event 提升为有效。文件输出需校验实际路径、FFmpeg 退出码和可解码帧数,异常路径统一回收线程、IPC、GPU 和临时文件。此桌面渲染链不能替代车端硬编/硬解或原车时序的台架验收。

    常见问题

    导出画面里的数据一定是有效的吗?

    不一定。patch_submaster() 把 seen、updated、alive、valid 全置真且不保留原 Event.valid,未出现的新主题也不会被显式标成失活。因此导出画面可显示出原始日志里曾无效或已失联的数据,不宜作为告警新鲜度的权威回放。

    为什么导出的片段长度可能与请求不符?

    因为分桶以每段首条日志的单调时间为起点而非视频首帧 SOF,一个 50 ms 桶只保留同主题最后一条且不补齐中间空桶。clip() 用 frame_start 和 duration * 20 直接切桶列表,日志起点偏离、消息缺口或某段不足 60 秒都可能造成错位或片段过短;程序只检查切片非空。

    导出打印 saved 就说明文件可解码吗?

    不能说明。GuiApplication 的 close_ffmpeg() 等待编码子进程但不检查非零退出码,因此打印「saved」不等于文件可解码;而且 RECORD_OUTPUT 被强制改成 .mp4,成功日志仍打印原始 -o 路径,非 .mp4 后缀会造成路径不一致。

    –speed 0 会怎样?

    parse_args() 只要求结束时间大于起始时间,没有对负起点、零/负倍速做完整校验;在默认正的目标体积下,--speed 0 会在码率计算中除零。这是源码可见的输入校验缺口,不是可控的「静音播放」。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/DESKTOP_CLIP_RENDER_LOG_CAMERA_UI_CONTRACT_DEEP_DIVE.md(取用了分层调用、时间/图像/消息一致性、Params 与前缀顺序、资源与失败边界、插件化约束)。

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

    更新记录

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

  • common.utils 契约:atomic_write、retry 与内存峰值

    结论:openpilot.common.utils 里的几个名字听起来很稳妥的辅助函数,其保证范围都比名字窄:atomic_write() 的「原子」只是同一文件系统内已完成写入的可见性切换,它不执行 fsync,因此不能承诺断电持久化,overwrite=False 也只在建临时文件前检查一次;get_upload_stream() 注释称实时压缩,实现却把压缩结果完整写入 BytesIO 再上传,峰值内存随压缩后大小增长;retry() 每次失败都睡 delay,最后一次失败后也睡,且抛出的是新异常、原始错误既不是抛出类型也不在 __context__ 中。以下为固定源码快照 c8fb906 并隔离运行 atomic_write() 的静态分析,未执行设备写入、上传、联网或外部进程。

    函数与调用方索引

    函数/类 关键行为 主要调用方
    Timer time.monotonic() 记录起点和 lap,fmt() 以给定媒体时长计算倍速 导出性能统计
    MovingAverage 长度为 window_size 的环形缓冲,部分窗口按当前 count 求均值;未采样返回 NaN Ratekeeper 循环间隔、FrequencyTracker 消息频率
    atomic_write() 在目标目录建立临时文件;上下文正常退出后 os.replace;异常时删除临时文件 令牌持久化、URL 文件、manager 配置
    get_upload_stream() 原文件返回文件流及字节数;压缩时把 zstd 完整结果写入 BytesIO 后返回流和压缩长度 uploader.do_upload()、athenad._do_upload()
    CallbackReader 委托底层对象,read() 后累计字节并回调 athenad 上传进度
    retry() 捕获 Exception,每次失败后睡眠并重试;耗尽后抛新异常或按设置返回 None qcomgpsd.at_cmd()、micd、soundd
    managed_proc() 上下文退出时 terminate()、最多等 5 秒、超时 kill() 当前仅见定义,未见调用
    sudo_write()、sudo_read() 设备文件读写;写失败时尝试提权命令,读错误返回空串 硬件 IRQ/CPU/GPU 配置、sensord IRQ 配置

    准确的失败与资源语义

    atomic_write() 的「原子」范围是同一文件系统中已完成写入的可见性切换:关闭临时文件之后才 os.replace,读者不会看到半写的目标文件。它没有对文件和目录执行 fsync,因此不能单凭这个接口承诺断电持久化。overwrite=False 只在建临时文件前用 os.path.exists 检查一次;并发创建目标文件时,后续 os.replace 仍可能覆盖它。上游 test_atomic_write() 只覆盖正常写入后目标文件可读;三项隔离探针进一步确认写入期间目标不可见、异常保留旧值并清理临时文件,以及默认 overwrite=False 在进入上下文后仍会覆盖并发创建的目标。

    get_upload_stream() 实现把压缩结果完整写入 io.BytesIO 然后才发起网络上传,对大日志峰值内存随压缩后大小增长;读取源文件失败或压缩失败会在上传前发生。调用方在 finally 关闭返回流;CallbackReader.read() 报告的是请求库从流中读取的字节数,并非远端已确认接收的字节数。athenad 使用返回长度构造 Content-Length,而 uploader 当前不使用这个长度。

    retry() 每次失败都睡 delay,最后一次失败后也睡;调用方最终看到通用 Exception。新异常在 except 块之外抛出,原始错误既不是抛出类型,也不在 __context__ 中。attempts=0 时循环不执行,行为是立即抛新异常或返回 None;ignore_failure=True 也会返回 None。这些语义对硬件初始化和 AT 命令尤其重要:重试五次、每次内部串口超时五秒,并不是「五秒的总 deadline」。

    接口 真正的保证 容易误解的地方
    atomic_write() 同文件系统内可见性切换 不 fsync,不承诺断电持久化;overwrite=False 只检查一次。
    get_upload_stream() 返回文件流或整块压缩结果流 压缩结果先进 BytesIO,峰值内存随压缩后大小增长。
    CallbackReader.read() 请求库已读字节数 不是远端已确认接收的字节数。
    retry() 按次数重试并睡眠 最后一次也睡;原始异常既非抛出类型也不在 __context__。
    MovingAverage 固定容量环形均值 构造不校验零窗口,第一次 add_value() 就失败。
    tabulate(html) 拼接单元格字符串 没有 HTML 转义,不能当安全 HTML 展示。

    嵌套重试与副作用放大

    嵌套使用会放大副作用:try_setup_logs() 最多执行十次,外层 setup_quectel() 最多执行五轮。两项假 DIAG 组合探针确认持续失败时共调用 50 次日志配置、睡眠 55 次,并重复五轮前置 NV 写/读请求;这是软件调用次数,不证明 NV 状态改变,也不包含真实 I/O 耗时。

    MovingAverage 要求正的窗口大小,构造时不校验,零窗口在第一次 add_value() 访问空缓冲时就失败。tabulate(tablefmt="html") 直接拼接单元格字符串,没有 HTML 转义,因此不能把不可信插件文本直接当作安全 HTML 展示。sudo_write() 的提权回退把值和路径拼接进 shell 命令,当前调用主要是平台常量和已发现的 IRQ 编号,但该函数不应成为接受插件任意路径/值的通用能力;sudo_read() 吞掉所有异常并返回空串,调用方应区分「空内容」和「读失败」。

    硬件与插件化落点

    源文档给出的是一组建议而非已实现能力:将特权文件操作封装为受信任平台服务,明确允许的设备节点、参数范围、审计记录和失败返回,第三方插件不直接调用 sudo_write();对配置文件分别规定可见性、并发覆盖、权限、断电耐久性与密钥保密要求,atomic_write() 只解决其中的可见性问题;上传插件按最大文件和压缩输出设内存预算,使用可测的流式压缩/限额方案,回调进度与服务端确认分开表示;重试契约显式声明可重试异常、退避、总 deadline、取消信号与最终原始故障,硬件命令还需考虑幂等性。

    本文没有运行硬件、网络或断电试验。atomic_write() 的上述文件状态已用隔离探针验证,其余故障推论来自当前实现;调用方及平台行为仍需继续审计。

    常见问题

    atomic_write() 能保证断电后不丢数据吗?

    不能。它只在同文件系统内切换可见性,关闭临时文件后才 os.replace,没有对文件和目录执行 fsync,因此不能单凭这个接口承诺断电持久化。

    overwrite=False 能防止并发覆盖吗?

    不能。它只在建临时文件前用 os.path.exists 检查一次;并发创建目标文件时,后续 os.replace 仍可能覆盖它。探针已确认进入上下文后仍会覆盖并发创建的目标。

    retry() 五次重试总耗时是固定的吗?

    不是。每次失败都睡 delay,最后一次失败后也睡;若每次内部串口超时五秒,五次重试并不是「五秒的总 deadline」,而是显著更长。

    上传进度回调代表服务端已收到吗?

    不代表。CallbackReader.read() 报告的是请求库从流中读取的字节数,并非远端已确认接收的字节数;进度与服务端确认应分开表示。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/COMMON_UTILS_IO_RETRY_RUNTIME_DEEP_DIVE.md(函数索引、atomic_write 探针、retry 语义与嵌套重试)。
    • 项目 02 —— openpilot-research/ 同目录上传队列与硬件配置专题(作为相邻边界参考)。

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

    更新记录

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

  • cereal 消息契约:schema、服务表、valid 与落盘各管什么

    结论:cereal 的消息契约必须拆成四层看:schema 定义、services.py 服务表配置、运行消息有效性、loggerd 落盘结果。union 分支存在不表示进程正在发布,也不表示 loggerd 会记录;logMonoTime 是消息封装时间,valid 也只表示运行有效位,不承诺车辆总线帧通过车型解析或 panda 安全检查。把「字段存在」当作「数据已发布或已记录」是这份契约最常见的误读。

    四层契约各管一段

    schema 层,Event 有 logMonoTime、valid 与 union,carState、carControl、carParams 指向 opendbc 的 car.capnp,customReserved* 保留固定 field ID。服务表层,SERVICE_LIST 声明服务名、期望频率、should_log、qlog decimation 和队列容量,build_header() 生成对应的 C++ 表——频率是配置与健康检查依据,不等于真实发布速率。

    消息构建层,new_message() 默认 valid=False,把 logMonoTime 设为单调时钟纳秒并初始化主题,发布者通常再赋有效位。日志订阅层,loggerd_thread() 订阅 should_log 服务、非直播 EncodeData 和可选原始音频;普通消息按配置写 rlog,按计数抽样写 qlog。因此收到消息、valid=True、内容可信和时戳新鲜是四个不同判断。

    层 源码与含义 不能推出的结论
    schema Event 有 logMonoTime/valid/union;车型消息指向 car.capnp union 分支存在不表示进程正在发布或 loggerd 会记录
    服务表 SERVICE_LIST 声明频率、should_log、decimation、队列容量 频率是配置依据,不等于真实发布速率;should_log=False 也可能有专门编码路径
    消息构建 new_message() 默认 valid=False,设置单调时间 收到、有效、可信、时戳新鲜是不同判断
    日志订阅 loggerd_thread() 按 should_log 与计数抽样写盘 qlog 不是每个主题的完整回放数据

    关键主题与字段语义

    CAN 接入侧,Event 的 can 是从车接收帧,sendcan 是主机候选发送帧;CarState.canValid/canTimeout/canErrorCounter 表示车型解析健康,不能只取上次解码值。services.py 为 can 设大队列与 qlog 高抽样率,因此完整 CAN 分析应使用 rlog。执行决策侧,enabled、latActive、longActive 不等价,Actuators 的扭矩/转角/曲率属互斥横向命令语义,纵向 accel 为 m/s²。

    模型到规划侧,modelV2 的 qlog 抽样未配置,不应拿 qlog 复原所有推理帧;LateralManeuverPlan 是调试覆盖曲率,不是一般模型输出。使能与监督侧,enabled 与 active 分开,警报文本/声音、人机监督策略与注意力状态在独立消息中。摄像头与编码侧,帧标识、曝光时间戳、编码索引/时间戳和视频字节是不同层,图像主数据经 VisionIPC,EncodeData 由 loggerd 分离写媒体文件。

    链路 载荷定义 核心字段与边界
    CAN 接入 Event.can/sendcan、CarState canValid/canTimeout/canErrorCounter 表示解析健康
    执行决策 CarControl、CarParams enabled/latActive/longActive 不等价;safetyConfigs/dashcamOnly/passive 决定执行边界
    模型到规划 ModelDataV2、LongitudinalPlan、LateralManeuverPlan 模型含帧 ID、EOF 时间、耗时、轨迹;modelV2 qlog 抽样未配置
    使能与监督 SelfdriveState、DriverMonitoringState enabled 与 active 分开;监督策略独立消息
    panda 安全状态 PandaState controlsAllowed、模式/参数、RX 无效、TX 阻断、心跳丢失分列
    摄像头与编码 FrameData、EncodeIndex、EncodeData 帧标识、时间戳与视频字节分层;EncodeData 分离写媒体文件

    需要单独记住:Event 的 logMonoTime 是消息封装时间;相机 timestampEof、模型 timestampEof、GNSS Unix 时间和 IMU 自有时间字段并不自动相等。传感器/定位插件必须显式声明时钟域、单位、采样时点和迟到处理。

    Route 与 Segment 文件如何落盘

    元数据由 logger_build_init_data() 记录版本、设备、内核、Git 信息、参数和部分硬件日志,参数值只有不带 DONT_LOG 标志才写入。命名/开段阶段,路由名由递增计数与随机后缀组成,段目录为 <route>--<part>;开段创建 rlog.lock、rlog.zst、qlog.zst,写 initData 与 START sentinel,结束段/路由写 END sentinel 并移除锁。

    压缩阶段,rlog.zst、qlog.zst 是 zstd 压缩的连续 Cap’n Proto Event 字节流,qlog 只在抽样命中时附写。切段时,rotate_if_needed() 与 handle_encoder_msg() 等已出现的编码器进入新段后切段,相机/编码器异常时按时间回退切段;新段编码数据须等关键帧,先于关键帧的帧被丢弃。媒体文件按相机表写 fcamera.hevc、ecamera.hevc、可选 dcamera.hevc 与 qcamera.ts;RecordAudio 打开时 rawAudioData 还会写入 rlog(decimation=None,不进 qlog),这涉及额外的数据保留与隐私审查。

    抽样率是理解落盘差异的关键:can 的 qlog 抽样率为 2053、sendcan 为 139、carState 为 10、modelV2 为 None。编码索引虽通常 should_log=False,但在 write_encode_data() 中专门转写且始终进 qlog。不要用 qlog 作为控制/安全故障分析的唯一依据。

    对插件化与硬件的最低要求

    1. 插件 schema 需要版本、字段号与兼容策略;Cap’n Proto union 的固定编号不能任意复用,customReserved* 约束也不能未经全链路兼容评估就改控制主契约。
    2. 发布权限应按主题分级:观测只读、规划候选、carControl 和 sendcan 分开授权;valid、新鲜度、总线校验与硬件许可各自独立。
    3. 存储插件需保留元数据、段边界、单调时钟、媒体索引、丢帧及部分文件状态;为取证保留 rlog,不可用 qlog 抽样代替。

    常见问题

    消息字段存在,是否说明该数据一定被记录?

    不代表。四层契约是分开的:schema 里 union 分支存在,不表示受管进程正在发布,也不表示 loggerd 会记录;服务表里的频率是配置与健康检查依据,不等于真实发布速率;只有 loggerd_thread() 按 should_log 与抽样配置实际写盘的内容才在日志里。

    为什么不能用 qlog 复原所有推理帧?

    因为 modelV2 的 qlog 抽样配置为 None,carState 抽样率为 10,can 为 2053,各主题抽样并不一致,qlog 只在抽样命中时附写。源文档明确 qlog 不是每个主题的完整回放数据,控制/安全故障分析不要以它作为唯一依据。

    Event 的 logMonoTime 就是传感器采样时间吗?

    不是。logMonoTime 是消息封装时间;相机 timestampEof、模型 timestampEof、GNSS Unix 时间和 IMU 自有时间字段并不自动相等。传感器/定位插件必须显式声明时钟域、单位、采样时点和迟到处理。

    valid=True 是否表示这一帧数据可信?

    不等于。valid 只是运行有效位,源文档明确它「不承诺车辆总线帧通过车型解析或 panda 安全检查」。车端数据是否可信还要看 CarState 的解析健康字段、时间戳新鲜度以及 PandaState 中的许可与故障分列。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/MESSAGE_LOG_CONTRACTS_DEEP_DIVE.md(取用了四层契约、关键主题字段语义、Route/Segment 落盘流程与 qlog 抽样率)。
    • 项目 02 —— openpilot-research/LOGGERD_SERVICE_SAMPLING_POLL_CONTRACT_DEEP_DIVE.md(仅用于确认 loggerd 逐服务订阅与计数抽样相位的边界)。

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

    更新记录

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

  • openpilot 系统底座:manager、cereal 与 msgq 分工

    结论:openpilot 的系统底座由四类互不等价的服务组成:manager 是进程生命周期的控制平面,cereal/msgq 是实时结构化消息数据平面,VisionIPC 单独传图像缓冲,Params 保存跨重启配置。不能把它们合并理解为一个「事件总线」,因为四者对可靠性、持久性和时序的承诺完全不同:msgq 是可被新写入覆盖的实时队列,而 Params 才承担持久化。本文为固定源码快照 c8fb906 的静态阅读,未在真实设备上启动完整系统、未验证断电与共享内存溢出的实际时序。

    控制平面与数据平面:四类服务的分工

    把 manager、cereal、VisionIPC 和 Params 当成同一类基础设施,会导致对可靠性和时序的错误假设。它们的职责边界在源码里是清晰分离的:manager 负责进程启停,cereal/msgq 负责实时消息,VisionIPC 负责图像缓冲的文件描述符传递,Params 负责跨重启的键值配置。

    层 代表入口 语义边界
    manager 控制平面 manager_init()、manager_thread()、ensure_running() 订阅 deviceState、carParams、pandaStates,按 started 与点火边沿管理进程;状态消息是报告,不是安全固件许可
    cereal/msgq 数据平面 new_message()、PubMaster.send()、SubMaster.update() 实时结构化消息;新插件若依赖每帧必达,需自建可靠传输,不能把该队列当事务消息系统
    VisionIPC VisionIpcServer::listener()、VisionIpcClient::recv() 只传图像缓冲索引与元数据,图像内容不复制进普通 Event
    Params Params::put()、Params::get()、Params::clearAll() 持久配置与少量控制状态,按生命周期标志清理;不适合替换高频 cereal 消息

    启动与进程管理

    manager_init() 清理按生命周期标志定义的参数,填默认值和版本/设备信息,建立共享内存路径并注册设备;注册失败会抛异常,因此不能假定 manager_thread() 已经开始。manager_thread() 订阅 deviceState、carParams、pandaStates,按 started 与点火边沿清理参数、更新 IsOffroad、调用 ensure_running() 并发布 managerState。NOBOARD、BLOCK 等配置会排除对应进程。

    入口 已核对行为 关键边界
    ensure_running() 用 enabled、排除列表及 should_run(started, params, CP) 判定每个进程的目标状态,先停后启 条件按每次循环重算,不能假定 44 个进程同时运行
    ManagerProcess.stop() 先发 SIGINT 或指定信号,等待后必要时 SIGKILL block=False 只发起停止,下次启动会等待停止完成
    PythonProcess.start() / NativeProcess.start() 分别用多进程包装 Python 模块与原生可执行文件 插件进程若纳入管理,需保留进程状态与退出语义
    DaemonProcess.start() 用持久 PID 与 /proc 命令行子串判断是否复用守护进程 不设置普通 proc 对象,stop() 为空

    进程的入口和运行条件由 process_config.py 的静态表决定,因此插件化不能只实现动态 import,还需定义能力声明、依赖、健康状态及受管退出协议。收到卸载、关机、重启参数会退出循环,manager_cleanup() 清理受管进程后由 main() 调硬件关机或重启。此外,manager_thread() 仅在 deviceState 的存活、平均频率和有效位检查通过时更新电源监控看门狗文件,但进程启停仍读取缓存的 started,不受该健康门直接阻断。

    消息契约与底层队列

    log.capnp 与 services.py 分别定义 Event 的结构字段与主题清单、标称频率、日志选项和队列容量,改 schema 与服务表要同步消费者。new_message() 新建消息默认 valid=False 并写单调时钟 logMonoTime,生产者需要显式设置有效性;SubMaster.update() 轮询指定主题,更新帧号、消息时间与有效位;SubMaster.all_checks() 合并存活、平均频率和消息有效位,模拟模式会放宽部分时序检查。频率追踪以长期和近期平均频率判断接收速率,不是硬实时截止时间证明。

    底层 msgq_new_queue() 在 /dev/shm(macOS 为 /tmp)建立 mmap 队列,msgq_msg_send() 检查发布者 UID 后向环形缓冲写入长度与负载并唤醒读者,msgq_msg_recv() 在读者被逐出或读指针失效时重置到当前写指针,旧消息不保证可补读。SubMaster 的 conflate=True 订阅意味着主要消费最新状态。因此 services.py 里的频率只是契约/健康检查参考,并不表示所有消息在该频率上严格同步。图像则单独走 VisionIPC:VisionIpcServer::send() 只向 msgq 发送缓冲索引、服务器 ID 和帧附加元数据,VisionIpcClient::recv() 用服务器 ID 与索引确认缓冲仍属于当前服务端。

    Params 与生命周期配置

    Params 是 C++ 实现的 Python 绑定。底层 Params::put() 先写临时文件、fsync、加锁、原子 rename 并同步目录;Params::get() 可阻塞等待非空值;Params::clearAll() 依键标志清除指定生命周期数据,并清理未登记键。它适合持久配置和少量控制状态,不适合替换高频 cereal 消息。插件应声明键名、类型、默认值、清理标志与迁移版本,避免私自写未登记键后被 clearAll() 删除。

    设备启动门、记录与空间回收

    hardware_thread() 订阅 panda、外设、定位与辅助驾驶状态,生成 deviceState;点火/连接状态形成行车条件,热状态、启动训练、条款、空间、系统启动完成、更新状态及设备注册等构成启动门。进入行车后仍持续检查点火与严重过热,panda 超时会清除点火条件。需要强调:deviceState.started 是 manager 启停 onroad 进程的信号,不是 selfdriveState.enabled 或 panda 的 controls_allowed。硬件移植必须实现 HardwareBase 的平台能力并处理相机、IMU、温度、供电及看门狗;当前硬件选择仅按 /AGNOS 在设备与 PC 两个实现间切换。

    组件 关键函数 边界
    loggerd loggerd_thread()、rotate_if_needed() 记录服务消息并按编码器就绪或超时旋转分段,无相机包时有兜底旋转
    encoderd encoder_thread() 图像流编码与分段配合;日志索引和视频负载是不同文件/主题
    uploader Uploader.list_upload_files()、upload() 跳过锁定或已上传文件;计费网络限制视频上传
    deleter deleter_step() 空间不足时优先删除可回收旧分段,避开锁定目录

    日志、上传与删除是旁路服务,不应成为转向/制动控制的同步前置条件。插件若新增高频主题,还需决定是否记录、抽样、队列容量与磁盘配额——仅在 services.py 注册主题并不足以保证长期可运维。

    常见问题

    msgq 队列能保证每条消息都被消费到吗?

    不能。msgq 是可被新写入覆盖的实时队列,读者被逐出或读指针失效时会重置到当前写指针,旧消息不保证可补读。依赖每帧必达的插件应自行定义可靠传输或落盘机制。

    deviceState.started 等于车辆已被使能吗?

    不等于。started 是 manager 启停 onroad 进程的信号,与 selfdriveState.enabled 和 panda 的 controls_allowed 是不同层概念,不能相互替代。

    为什么 Params 不适合传高频数据?

    Params 每次写入都要写临时文件、fsync、加锁并原子 rename 并同步目录,面向持久配置而非高频流。高频结构化消息应走 cereal/msgq。

    插件的参数键可以随便写吗?

    不建议。Params::clearAll() 会依生命周期标志清理未登记键,插件应声明键名、类型、默认值、清理标志与迁移版本,否则可能被意外删除。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/SYSTEM_FOUNDATION_DEEP_DIVE.md(取用了控制平面与数据平面分工、启动与进程管理、消息契约与 msgq 队列、Params 生命周期,以及设备启动门与旁路服务)。
    • 项目 02 —— openpilot-research/HARDWARE_BACKEND_USB_SNAPSHOT_CONTRACT_DEEP_DIVE.md(仅用于确认硬件选择与 deviceState 的衔接位置)。

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

    更新记录

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

  • C++ Replay 引擎:五段窗口缓存与消息时序发布

    结论:C++ Replay 引擎不是「把日志快一点放一遍」,而是一条把历史路段重新发布到消息总线、SubMaster 与 VisionIPC 的离线回放链:Route 选段 → SegmentManager 五段窗口缓存 → LogReader 解析 Cap’n Proto → Replay 按单调时间节拍发布 → CameraServer 解码并推送帧。源文档将其定义为交互式开发工具,其中 startStream() 会把日志里的 CarParams 写入本机 CarParams 与 CarParamsPersistent,属于有持久副作用的路径,不应在连接真实车辆控制栈的环境中直接启动。本文为静态阅读,未下载路段、未运行 GUI、未编译 C++。

    一、五个组件各管一段

    Replay::load() 调用 SegmentManager::load();Route::loadSegments() 选择本地目录、服务端或 auto_source,只保留带 rlog/qlog 的段。路段号用有序 map 管理,但文件元数据本身并不是数据完整性证明。

    LogReader 解析 Cap’n Proto,按 union 过滤,把完整 HEVC EncodeIndex 复制成带 segmentNum 的帧事件,并按单调时间排序;FrameReader 用 FFmpeg 建立包索引,随机帧从最近关键帧重解码。Replay 则负责 setupServices()、seekTo()、pause()、publishEvents(),消息 allow/block 与加载过滤联动。

    Timeline 独立读取每段 qlog,按 selfdriveState/书签构建使能、告警与书签区间,seekToFlag() 在目标时间前 2 秒跳转。qlog 缺失时该段时间轴可为空,即使 rlog 正常;这两条输入链不能互相替代。

    二、五段窗口缓存与失败重试

    Replay::seekTo() 将目标秒数映射到 60 秒段并通知缓存线程;manageSegmentCache() 默认维护五段窗口,先向前加载,再补前段,失败对象最多三次尝试并退避。Segment::Segment() 并行启动日志、可选相机加载,任一文件失败会使该段失败。

    组件 核心入口 责任与行为边界
    Route parseRoute()、loadFromLocal()、loadFromServer() 解析路段范围、枚举或查询文件 URL;rlog 优先,缺失才回退 qlog;窄路相机可回退 qcamera。
    SegmentManager manageSegmentCache()、mergeSegments() 异步装载与驱逐窗口、失败重试、按时间合并事件,并原子交换只读快照;isSegmentLoaded() 不代表媒体质量或日志完整性。
    LogReader load()、migrateOldEvents() 调用 Python 下载/解压;旧 controlsState 可补生成 selfdriveState;解析异常会保留已读事件,非空即返回成功。
    FrameReader loadFromFile()、get() FFmpeg 建包索引,随机帧从最近关键帧重解码;未找到帧时记录错误,不合成替代帧。
    Timeline buildTimeline()、find() 独立读取每段 qlog;seekToFlag() 在目标时间前 2 秒跳转。

    合并阶段 mergeSegments() 会跳过各段首部的 INIT_DATA,通过共享 EventData 快照保留事件与对应段对象的生命周期。这也意味着段边界处的初始化数据不会作为普通事件重复发布。

    三、时序发布、seek 与失败边界

    streamThread() 用 upper_bound 从当前 (mono_time, which) 之后继续发布,因此相同时间且相同消息类型的多条事件在 seek/中断续播边界可能一起被跳过——源文档标注这是静态推导,尚无构造样本实测。publishEvents() 按回放速度计算睡眠,落后超过一秒、跨段大跳变或变速时重置节拍。

    入口 行为与边界
    seekTo() 只接受路段 map 内存在的目标段;onSeekedTo 由段加载完成触发,不表示目标帧已解码、订阅者已收到或算法已收敛。
    startStream() 设置时间原点、写入回放 CarParams、启动可选 VisionIPC 和时间轴;缺失 CarParams 时只警告并继续。
    publishMessage() 检测多发布者错误后禁用该服务 socket,避免继续冲突;不提供自动恢复。
    CameraServer::pushFrame() 分辨率变化时重建 VisionIPC 缓冲,相关订阅端需能处理重连/尺寸变化。
    waitForSent() 等待队列发送完毕,但不证明下游消费完成。

    因此 seekTo() 的成功、onSeekedTo 的触发、以及 waitForSent() 的返回,三者语义各不相同,不能互相代替。

    四、对硬件与插件重构的含义

    另一个易被忽略的点是帧事件与普通事件走两条发布路径:普通事件由 publishMessage() 发往 PubMaster 或注入 SubMaster,帧事件由 publishFrame() 送往 CameraServer::pushFrame(),再由 cameraThread() 处理。帧用 segmentId 取视频内帧、用 frameId 标注 VisionIPC 缓冲,并携带 SOF/EOF 时间戳;未找到帧时记录错误,不合成替代帧。因此「帧未发布」与「消息未发布」是两类不同的失败,日志回放时不能只看消息侧是否完整。

    Replay 可当作离线验证适配器,不能当作实车控制安全门。建议插件边界分为 RouteSource(路段元数据/文件定位)、EventTransform(纯函数消息转换)、FrameSource(解码/时间戳)和 ReplaySink(隔离的消息/VisionIPC 输出);每个插件需声明 schema、时间基准、输入完整性及失败策略。车端正式控制路径仍由 panda/opendbc 安全钩子与单一控制发布者约束,不允许回放插件绕过。

    回归实验必须先记录每段实际输入文件、哈希、消息数、首末时间戳、视频包数及帧 ID 对照,才能比较两次结果。源码现有 test_route_ranges() 仅覆盖路段范围解析;本次未运行测试,也未发现覆盖上述发布时序和视频失败路径的同目录专门测试,本结论只针对当前归档及该目录。

    常见问题

    Replay 引擎会把日志里的车辆参数写到本机吗?

    会。源文档指出 startStream() 把日志中的 CarParams 写入本机 CarParams 和 CarParamsPersistent,因此它是有持久副作用的开发工具,不应在连接真实车辆控制栈的环境中直接启动,应隔离 Params、消息命名空间与 VisionIPC。

    为什么回放时「相同时间戳的事件」可能被跳过?

    因为事件比较先按单调时间、再按消息 union 编号排序,streamThread() 的 upper_bound 从严格大于上次键的位置继续。相同时间且相同消息类型的多条事件在 seek/中断续播边界可能一起被跳过;源文档标注这是静态推导,尚无构造样本实测。

    段文件存在就等于日志完整吗?

    不等于。isSegmentLoaded() 只表示快照中有段对象;LogReader::load() 对损坏日志可保留前缀事件,非空即返回成功,故不能以 load 成功代表日志完整。结合 rlog/qlog 与 qcamera 回退,必须记录每段实际输入文件与哈希才能比较两次结果。

    Replay 能当作实车控制的安全门吗?

    不能。源文档把 Replay 定位为离线验证适配器,车端正式控制路径仍由 panda/opendbc 安全钩子与单一控制发布者约束,不允许回放插件绕过。建议先用确定性日志/视频样本验证 seek、断段、损坏文件与多发布者冲突,再进行封闭台架测试。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/REPLAY_ENGINE_DEEP_DIVE.md(取用了端到端调用链、五段窗口缓存、时序与失败边界,以及插件化重构含义)。

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

    更新记录

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

  • 开发诊断脚本:事件计数、日志体积归因与 8 核 CPU 采样边界

    结论:项目 02 的五个开发诊断脚本(count_events、qlog_size、mem_usage、watch_timings、live_cpu_and_temp)只产出启发式观测,不是系统健康证明:事件计数是消息出现次数而非独立故障次数,日志体积饼图是给定压缩顺序下的边际贡献而非可相加的存储配额,8 核 CPU 采样对超过 7 的核号会数组越界、对零累计时间差会除零。任何监测结果只能形成告警或实验准入证据,不能绕过 panda 安全钩子直接获得车辆控制权。本文为固定源码快照 c8fb906 的静态阅读,未使用真实路段、未启动订阅、未连接车端。

    五个脚本各自算什么

    这五个脚本被放在同一目录下,但输入域分成两类:三个作用于离线路段日志,两个订阅实时消息。count_events.py 用 LogReader(route, ReadMode.QLOG) 读取路段,经 migrate_all() 兼容迁移后统计 onroadEvents 各名称出现次数,并记录去掉三类踏板/覆盖事件后的集合变化、selfdriveState.alertType 变化和各相机状态条数。qlog_size.py 的 make_pie() 先压缩全体序列,再逐主题移除并重新压缩,把「总压缩长度减去移除后长度」作为该主题体积。mem_usage.py 从 LogReader 收集 procLog 与 deviceState,交 print_report() 汇总内存。

    组件 输入与计算 结果及边界
    count_events.py LogReader 读路段,migrate_all() 后按名称统计事件 是消息出现次数,不是独立故障次数
    qlog_size.make_pie() 全体压缩后逐主题移除再压缩,取差额 边际贡献,不可相加;分别打印 real total 与 breakdown total
    mem_usage.py 收集 procLog/deviceState,调用 print_report() 末帧系统内存与各进程 RSS/PSS 均值峰值;非泄漏或 GPU 用量直接测量
    watch_timings.ServiceTiming.add() 保留 logMonoTime 列表、最近 100 个间隔与 valid 间隔超过 lag_threshold / frequency 记滞后
    live_cpu_and_temp.py SubMaster 订阅 deviceState/procLog 按每核累计 busy/total 差算 CPU;只有更新了 procLog 才打印

    把「脚本跑出数字」等同于「系统健康」是最常见的误用。count_events.py 在每条消息上先用旧的 start_time 计算相对时间再更新起点,若第一条消息恰为要输出的事件或告警,时间可以是负无穷;它遇到第一次点火关闭的 pandaStates 就停止遍历,此后的消息均不参与持续时间与频率统计。相机期望帧数为 int(frequency * duration / decimation),短路段若得到 0,输出百分比时会除零。再加上 migrate_all() 会把整个输入转成列表、按 logMonoTime 排序且相机状态迁移缺省关闭,因此「缺少相机状态」不能只凭该脚本的计数判定相机物理故障。

    日志体积归因为什么不可当配额

    qlog_size.py 默认由 LogReader 读取 rlog,并打印标题为 qlog 的重压缩结果;没有 --as-qlog 时,这个标题并不代表输入或输出是实际 qlog。开关启用后按当前 SERVICE_LIST 的 decimation 过滤,initData/sentinel 总保留、未注册主题丢弃——这是用现在的配置模拟抽样,不是重建历史录制时的 qlog。loggerd 实际在每个已订阅服务上连续递增计数器并按模数选取,日志中还有编码器等特殊写入路径。

    make_pie() 将所有消息和每个主题排除后的其余消息重复序列化、拼接、压缩,时间与内存开销随主题数增长;压缩上下文使各差额不具可加性,空输入或总大小为零也没有有效饼图的保护。因此不能把饼图比例当作日志服务的精确配额,更不能据此直接关闭高频安全证据主题。

    内存与 CPU 采样:缺测、越界与残差标签

    print_report() 没有 procLog 时只打印提示;有数据时仅用最后一帧是否存在任一非零 PSS 来选全报告指标,不同帧/进程的缺测可能混入零值。进程名来自命令行首项,可能合并同名进程;均值只在出现的样本上取,不能当作连续驻留时间加权平均。kernel/ION/GPU 一行是从已用内存扣除缓存和进程估算的残差标签,不是 GPU 计数器。

    live_cpu_and_temp.py 固定为 8 核数组,cpuNum 超过 7 会越界,累计时间差为零会除零;首份 procLog 相对全零基线得到的是累计历史占比,不是当前瞬时负载。进程 CPU 以 PID 找上份采样却未校验 PID 复用,--mem 按名称写字典时同名进程会覆盖而非求和;温度与 RAM 由最近一次 deviceState 更新保留,打印时没有新鲜度提示。

    实时消息间隔:能看见的与看不见的

    monitor_services() 关闭 conflate 并逐轮排空队列,ServiceTiming 将全部时间戳和滞后事件长期保留,长时间运行内存会增长。valid 展示的是最近最多 100 条 Event 的全部有效性,即使 Ctrl-C 的频率统计使用全程数据也是如此。它只观察收到的相邻消息时间戳,既没有第一条消息前或最后一条消息后的静默时长,也无法从一个大间隔区分发布端迟到、订阅端丢包、队列覆盖或时钟问题。若订阅了已注册但频率为 0 Hz 的服务,收到消息后 1.0 / frequency 会触发除零异常,而不是跳过滞后判断。

    观测插件化时的最小契约

    这些建议不是当前工具的已实现能力。观测插件应声明数据源(路段或实时、rlog 或 qlog、服务版本、时间域)、样本区间和缺测状态;CPU 与温度要随每份 deviceState 标注年龄,日志体积归因要保留「估算」而非「账单」语义。对新硬件,核数、PSS 可用性、传感器温度字段和相机期望帧率应由平台能力描述,不写死在诊断界面。原始路段、车辆标识与进程命令行可能含敏感信息,插件仅应获得最小只读数据范围。

    观测项 必须声明的元数据 不能据此推断
    事件计数 路段来源、迁移版本、遍历终止条件 独立故障次数、相机物理健康
    体积归因 压缩顺序、压缩级别、是否 qlog 模拟 可相加的存储配额
    CPU/温度 核数、样本年龄、PID 复用策略 瞬时负载、GPU 用量、驻留均值
    消息间隔 时间域、队列策略、有效位窗口 丢包与迟到的成因归属

    常见问题

    事件计数能当作故障次数吗?

    不能。count_events.py 统计的是 onroadEvents 各名称在消息里出现的次数,不是独立故障次数;脚本遇到第一次点火关闭的 pandaStates 就停止遍历,此后的消息都不参与统计,所以它不是一份完整的故障台账。

    日志体积饼图能直接用来做存储配额吗?

    不能。make_pie() 在给定压缩顺序与压缩级别下求每个主题的边际贡献,压缩上下文使各差额不具可加性,脚本本身也分别打印 real total 与 breakdown total。它适合定位「谁更占地方」,不适合当作精确配额,更不能据此关闭高频安全证据主题。

    为什么 8 核 CPU 采样会异常退出?

    live_cpu_and_temp.py 固定为 8 核数组,cpuNum 超过 7 会越界,累计时间差为零会除零。另外首份 procLog 相对全零基线得到的是累计历史占比而非瞬时负载,参数缺失或核数与平台不符时应先改由平台能力描述。

    消息时间间隔变大就一定是丢包吗?

    不一定。watch_timings 只观察收到的相邻消息时间戳,无法从一个大间隔区分发布端迟到、订阅端丢包、队列覆盖或时钟问题,也看不到第一条消息前与最后一条消息后的静默时长。判定成因需要结合队列策略与其他进程日志。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/DEV_METRICS_LOG_SIZE_RESOURCE_TIMING_TOOLS_DEEP_DIVE.md(取用了五个脚本的输入输出、事件与体积失效边界、内存/CPU 越界除零,以及观测插件化建议)。
    • 项目 02 —— openpilot-research/MESSAGE_LOG_CONTRACTS_DEEP_DIVE.md(仅用于确认 loggerd 写入相位与 decimation 的抽样位置)。

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

    更新记录

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