标签: openpilot

  • Chestnut USB/AMD 推理后端:三层地址空间与双缓冲拷贝协议

    结论:Chestnut(USB+AMD:LLVM)推理后端不是一条本地 GPU 数据路径,而是「USB 桥控制内存 + VRAM 映射 + PCIe BAR」三层地址空间叠加双缓冲拷贝协议的组合。主机侧分配即使请求 host=True 也仍落在 VRAM,CPU 访问必须经 USB 桥映射;一次逻辑 COPY 会被拆成多次 USB 传输与 GPU 端复制。因此「USB 接口能打开」既不等于 GPU 就绪,也不等于模型输出有效。以下为固定源码快照 c8fb906 及固定 tinygrad 归档的静态结构分析,缺少 LFS 模型实体与 Chestnut 实板,所有时序与故障判断均为源码推导。

    从模型入口到设备的初始化链

    load_oob() 在 Chestnut 路径选择 USB+AMD:LLVM;modeld.main() 先按模型产物、设备或线缆判断是否尝试大模型,并把 AMDDevice.wait_timeout_ms 设为 3000 ms,加载或运行失败的回退属于上层逻辑,不是 USB 后端自身的保证。AMDDevice.__init__() 负责选接口、校验 AMD 目标架构、初始化设备属性;USB 分支采用 USBAllocator,设置 4 MiB 运行时分配区,并安装 pm_usb_batch、pm_usb_lower 与 pm_usb_bufferize。一般的 PCI/KFD 路径不走这套 USB 改写。

    USBIface.__init__() 从两个 USB VID/PID 集合中选设备,构造 USBPCIDevice 和 AMDev。后者先取得跨进程锁,创建 USB3,随后由 CustomASM24Controller 检查 LTSSM:未到 0x78 时尝试上电并复查,再配置 PCIe BAR。USB3 会读取产品字符串、必要时解绑内核驱动并重置设备、设置 configuration 并 claim interface。

    这里必须区分三层资格:VID/PID 匹配、PCIe 链路建立(LTSSM 到 0x78)、AMD 架构校验。任何单一的 chestnutPresent 位都不代表 GPU 已准备好执行推理。

    三种地址空间与缓冲所有权

    USBIface.ctrl 把系统页、计算队列页和 512 KiB SRAM 映射到 GPU 虚拟区,主机持有桥的 dma_view;初始化时清空 fence 与 SRAM,防止沿用旧哨兵。USBIface.alloc() 与 USBAllocator.map() 的语义容易被误读:即使请求 host=True 也落在 VRAM,CPU 侧访问需通过 USB 桥映射,并非本机普通 DRAM。

    层次 关键函数 源码契约
    USB 桥控制内存 USBIface.ctrl 系统页、计算队列页与 512 KiB SRAM 映射到 GPU 虚拟区;初始化清空 fence 与 SRAM。
    VRAM/CPU 可见视图 USBIface.alloc()、USBAllocator.map() 即使 host=True 也落在 VRAM;CPU 访问须经 USB 桥映射。
    PCIe BAR、XDATA、SRAM USBMMIOInterface BAR 读写需 4 字节对齐;桥 XDATA 用 0xE4/0xE5,SRAM 用 0xF2 加 bulk 端点。
    主机 staging 与传输结构 _host_block()、_xfer() 常驻 CPU 缓冲保存 libusb handle/context 与双半区 staging;提交后仍需保留源缓冲直至完成。

    USB3.list_devices() 带 functools.cache,结果中的设备指针用 libusb_ref_device 保留。该实现未在此函数内定义重枚举失效,因此断开后同一进程再次选设备是否拿到最新列表,须通过模拟或实板验证,不应假设缓存会自动更新。

    双缓冲复制协议与双半区

    常量 HALF/CHUNK/SLOT 分别是 0x40000、0x40000-512、0x4000 字节:每个 SRAM 半区末尾预留哨兵块。usb_wire() 为载荷加 512 字节并向 slot 对齐;usb_chunks() 对输入用一个 CHUNK、对输出用两个 CHUNK 作为窗口,因此逻辑 COPY 可能拆成多次 USB 传输和 GPU 端复制。

    usb_copy_rewriter() 只针对 host 与远端设备跨界的 COPY,按复制方向形成连续 run 并给每块编号;usb_copy_slicer() 在 GPU 提交图中插入等待、SRAM 复制、清哨兵与 usb_fence 更新。主机侧先用 usb_drained() 等待上轮完成,再通过 0xE5 设置 fence。

    方向 主机侧 GPU 侧
    host → GPU usb_copyin() 轮换两个半区;usb_chunk() 等前次 transfer 与 fence、填 staging 哨兵、发 0xF2,再异步 bulk OUT。 等对应 SRAM 哨兵,复制载荷到目标 VRAM,清哨兵并更新 fence。
    GPU → host usb_copyout() 先发 0xF2 arm read、写 usb_go,然后 bulk IN,再将两半区载荷复制回目标 host 地址。 等 usb_go,将 VRAM 复制到 SRAM 两半区,清 completion queue 并更新 fence。

    usb_reap() 和 usb_drained() 都是条件轮询,本身没有显式墙钟超时。libusb 传输有各自的 timeout,USB3.bulk_wait() 会抛异步错误,但不能据此推断任一桥接 fence 永远有界完成。对新硬件必须分别故障注入「传输成功、GPU 不更新 fence」与「GPU 更新、USB 回执丢失」两类状态。

    USB 访存降级与提交开销

    pm_usb_lower 将远端参数缓冲的标量 load/store 改成 USB 读/写;usb_copy() 对可识别的连续 copy/fill 循环合并为 stream,不能识别时退为逐元素写。64 位 kernargs 写入由 usb_store() 拆为两个 32 位 0xF0 写;usb_load() 则通过桥的 PCIe read stream 读取。这些路径决定命令提交开销与异常时可能的部分写入状态,不是普通进程内内存赋值。

    底层 USB3 使用控制传输与同步/异步 bulk;异步完成回调把状态锁存到 _async_err,等待方通过 bulk_wait() 消费事件并检查失败。桥的 pcie_request() 对读请求/配置写回执重试,对部分写操作走不等回执的快速路径。

    故障层次 含义 不能推出
    USB 传输成功 bulk/控制事务完成 PCIe TLP 已被 GPU 接受。
    PCIe TLP 被接受 桥层读写回执正常 GPU 队列已完成该命令。
    GPU 队列完成 fence 被更新 模型输出数值有效。
    模型输出有效 上层得到可用结果 安全放行条件成立。

    测试证据与硬件验收

    仓库存在 MockUSB3 和 mock GPU 环境,可作为后续隔离测试入口,但本文未运行 USB/AMD 模拟测试,也没有实板。建议先固定模型与目标固件版本、建立逐帧输入/输出数值基线;再量测分块、bulk、SRAM、VRAM、GPU 与回读的延迟分布;最后故障注入断连、重枚举、短传输、哨兵未清、fence 停止与 GPU 挂起。

    模型回退、ChestnutActive 与 selfdrived 的控制退出需要联合验证,不能把 USB 接口可打开当作安全放行条件。

    常见问题

    Chestnut 路径下 host=True 的分配真的在主机内存吗?

    不是。源文档说明 USBIface.alloc() 与 USBAllocator.map() 即使请求 host=True 也落在 VRAM,CPU 侧访问必须通过 USB 桥映射,并不是本机普通 DRAM。

    USB 传输成功就说明 GPU 已经算完了吗?

    不等价。源文档把故障归因分为四层:USB 传输成功、PCIe TLP 被接受、GPU 队列完成、模型输出有效。它们逐层递进,任一层成功都不能推出下一层成立。

    一次 COPY 会变成几次 USB 传输?

    不固定。逻辑 COPY 可能被拆成多次 USB 传输和 GPU 端复制:usb_chunks() 对输入用一个 CHUNK、对输出用两个 CHUNK 作为窗口,usb_copy_rewriter() 还会按方向形成连续 run。

    拔插设备后同一进程会重新枚举吗?

    源文档未给出重枚举失效机制。USB3.list_devices() 带 functools.cache,设备指针用 libusb_ref_device 保留,但该函数没有定义缓存失效,须通过模拟或实板验证,不能假设会自动更新。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_CHESTNUT_USB_AMD_BRIDGE_DEEP_DIVE.md(USB/AMD 初始化链、三种地址空间、双缓冲复制协议与故障分层)。
    • 项目 02 —— openpilot-research/ 同目录 Chestnut 与 HCQ 专题(固件刷写、通用队列链接作为相邻边界参考)。

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

    更新记录

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

  • maneuversd 纵向动作状态机:为什么第 61 次循环才激活

    结论:maneuversd 是 openpilot 中一个实验性的纵向动作发布进程,它用 DT_MDL=0.05 秒按主循环次数推进动作,而不是按真实秒数。它的就绪门要求车速偏离初速小于 0.3 m/s 且纵向处于活动状态,连续超过 60 次合格循环后第 61 次才置为 _active;进入活动后若 longActive 变假,函数仍会继续推进当前动作帧。它替换的是 plannerd 的计划发布者身份,不替换 controlsd、card 与 panda 的执行门。本文为固定源码快照 c8fb906 的静态阅读,未连接车辆与硬件。

    进程身份:发布者切换不等于安全许可

    在进程配置里,plannerd 与 maneuversd 通过条件互斥:plannerd 使用 not_long_maneuver,maneuversd 使用 long_maneuver,后者要求 started 为真且 LongitudinalManeuverMode 处于开启状态。这是 manager 正常管理路径上的发布者切换,并不是一份独立的安全许可。

    main() 先等待 CarParams 存在,再轮询 modelV2,依次取用内置的七个 Maneuver。内置计划固定 allowBrake=True、allowThrottle=True、hasLead=True、speeds=[0.2],并按 should_stop(v_ego, accel) 设置停止位;这些是实验消息的构造字段,不能代表正常纵向规划器的前车识别、速度轨迹或 FCW 能力。

    入口 源码事实 边界
    process_config.py 进程配置 plannerd 用 not_long_maneuver,maneuversd 用 long_maneuver 是 manager 正常路径的发布者切换,不是独立安全许可
    Action.__post_init__() 只断言加速度与时间节点数量一致 未校验节点非空、单调、有限、加速度范围或持续时间
    main() 等 CarParams,轮询 modelV2,每轮发三路主题 未在状态推进前用 all_checks() 做门控

    就绪门与「第 61 次循环」的由来

    Maneuver.get_accel() 在车速偏离初速小于 0.3 m/s、longActive 为真且巡航不处于 standstill 时累加就绪计数;当初速接近零时,还额外要求 carState.standstill 为真。它把状态机推进建立在「连续满足条件」的计数上,而不是单次采样。

    只有 _ready_cnt > 3/DT_MDL 才置 _active=True。由于 DT_MDL=0.05,3/DT_MDL 等于 60,而这里用的是严格大于,所以至少需要 61 次合格循环,而不是精确的 3.00 秒墙钟。未进入活动状态时,函数返回一个限幅到 ±2 m/s² 的初速追赶目标。

    条件 阈值 说明
    车速偏离初速 小于 0.3 m/s 偏离越大越不计入就绪计数
    纵向激活 longActive 为真 初速接近零时还要 carState.standstill
    就绪计数门 _ready_cnt > 3/DT_MDL 严格大于,至少 61 次合格循环
    未活动追赶目标 ±2 m/s² 限幅后的初速追赶,不是最终执行命令

    动作插值、重复与 reset 边界

    Maneuver._step() 按 _action_frames * DT_MDL 对当前 Action 插值;帧计数加一后,只有超过末节点时间才切换到下一个动作、重做或结束,因此最后一个时间节点需要再推进一帧才会切换。以主循环次数近似时间,意味着卡顿或模型节拍异常都会改变墙钟持续时长。

    repeat=2 使内置动作总共执行三轮。reset() 只清 _active、_action_frames、_action_index,不清 _ready_cnt、_repeated、_finished;它与横向子类的 reset() 不同,不能当作完整的状态重置。若 repeat 之后就绪条件继续成立,下一循环可以立即再次激活。

    三路发布与有效位语义

    每轮 main() 依次发出 alertDebug、longitudinalPlan 和 driverAssistance。计划的 valid=sm.all_checks(),另两路直接为真;其中 driverAssistance 是空默认字段,不提供常规 LDW 结果。动作列表耗尽后,仍会按同样顺序持续发送 aTarget=0 的计划和完成提示。

    源码没有在进入 get_accel() 之前用 sm.all_checks() 阻止状态推进,因此无效计划也会先完成计算并填入目标,只是把外层有效位标为假。

    主题 有效位 内容 边界
    longitudinalPlan sm.all_checks() 按动作插值填入 aTarget、停止位等 有效位不冻结状态机
    alertDebug 恒为真 纵向动作模式告警 由 selfdrived 消费,不做执行门
    driverAssistance 恒为真 空默认字段 不提供常规 LDW 结果

    失效路径与插件化要求

    SubMaster.update() 的默认 100 ms 超时也会让循环继续,所以「循环次数 × DT_MDL」并不等于真实经过的时间。sm.all_checks() 只设置发布事件的有效位,不冻结状态机;本进程本身没有「数据失联即重置动作」的独立分支。若模型消息或其他输入失联,外层消息健康和下游自驾状态机可能触发退出,但那是另一层。

    若要把这类实验规划做成插件,至少应:用单调时钟或样本时间推进,而不是用消息循环数冒充秒数;建立显式的 idle → armed → active → abort/complete 转移,纵向失活、速度越界、数据失联与进程重启都要撤销动作;把实验规划插件与量产规划插件分开注册、隔离主题与权限,即使替换计划发布者,也不能成为 carControl 或 sendcan 的直接所有者。

    常见问题

    为什么是「第 61 次循环」而不是精确 3 秒?

    就绪门写的是 _ready_cnt > 3/DT_MDL。DT_MDL=0.05 秒,3/DT_MDL 等于 60,而比较是严格大于,所以至少需要 61 次合格循环才置为活动。动作推进也按主循环次数乘 DT_MDL 计时,轮询超时同样会让循环继续,因此它不是精确的 3.00 秒墙钟。

    活动后 longActive 变假,动作会立刻停止吗?

    函数本身不会在这一点重置 _active。活动后若 longActive 变假或速度越界,get_accel() 仍会进入 _step() 继续推进动作帧;这不等于执行器仍有输出,controlsd 有独立的 CC.longActive 门。

    longitudinalPlan.valid 为假时 controlsd 会丢弃目标值吗?

    不能这样断言。controlsd 的 publish() 处没有按 self.sm.valid['longitudinalPlan'] 包住目标读取;selfdrived 会对该主题做通信健康检查并可能产生 commIssue,但它与 controlsd 的字段消费是不同层。计划外层 valid=False 不必然让控制器直接丢弃数值。

    reset() 是完整的状态重置吗?

    不是。reset() 只清 _active、_action_frames、_action_index,不清 _ready_cnt、_repeated、_finished。若 repeat 之后就绪条件继续成立,下一个循环可能立即再次激活,因此不能把它当作完整状态重置。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/LONGITUDINAL_MANEUVER_STATE_MACHINE_DEEP_DIVE.md(取用了进程互斥、就绪门与第 61 次循环阈值、三路发布有效位与失效路径)。
    • 项目 02 —— openpilot-research/LONGITUDINAL_MANEUVER_TEST_MATRIX_DEEP_DIVE.md(关联的回归测试矩阵专题,用于区分发布控制流与测试覆盖,本文未展开其用例)。

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

    更新记录

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

  • openpilot 感知到控制:四条数据流与 100 Hz 闭环

    结论:openpilot 的感知到执行不是一个同步函数调用栈,而是 modeld、radard、plannerd、controlsd 四条进程经消息松耦合的数据流:modeld 随图像帧运行,radard 与 plannerd 以 modelV2 为轮询节拍,controlsd 按 100 Hz 的 Ratekeeper 循环并最多等待 15 ms。同一轮控制里各输入可能来自不同采样时刻,因此不能按顺序函数调用推断一致性。本文为固定源码快照 c8fb906 的静态阅读,未加载模型、未连接车辆。

    闭环与进程时序:四条数据流而不是一条调用栈

    把整条链路理解成「摄像头 → 模型 → 规划 → 控制」的一次顺序调用,是最容易出错的心智模型。源码里 modeld 随图像帧运行,radard 和 plannerd 的 SubMaster 以 modelV2 为轮询主题,controlsd 则按 100 Hz 的 Ratekeeper 循环,以 selfdriveState 为轮询主题并有最多 15 ms 的更新等待。三条速率彼此不同,服务频率的定义见 services.py。

    进程 驱动节拍 主要输入 主要输出
    modeld 随图像帧 VisionIPC 相机流、CarParams、标定、车辆状态、驾驶员监测 modelV2、drivingModelData、cameraOdometry
    radard 轮询 modelV2 carState、radarTracks、modelV2.leadsV3 radarState
    plannerd 轮询 modelV2 modelV2、radarState、carState、CarParams longitudinalPlan、driverAssistance
    controlsd 100 Hz carState、selfdriveState、modelV2、计划 controlsState、carControl

    四者之间没有共享的同步函数栈,消息来自不同采样时刻是常态而非异常。这一点是后面所有边界讨论的前提:讨论「某一帧模型输出是否导致本轮转向」时,必须先确定这些消息是否属于同一次采样。

    感知:modeld 从图像到结构化预测

    main() 等待并连接 VisionIPC 相机流,读取 CarParams、标定、车辆状态与驾驶员监测信息,同步主相机与广角帧,计算图像变换和动作延迟,最后发布 modelV2、drivingModelData、cameraOdometry,并跟踪丢帧比例与相机时间戳。真正的模型装载在 ModelState.__init__():加载 tinygrad 序列化模型与 warp JIT,建立输入输出缓冲和递归状态对;模型文件、设备或编译任一步失败,初始化便无法继续。

    函数或类 读入与处理 输出与边界
    ModelState.pack_inputs() 将变换矩阵、动作条件、图像帧和状态输入装入连续缓冲 为外接 GPU 路径减少上传次数;缓冲布局本身是模型契约的一部分
    ModelState.run() 复制 NV12 帧,按上升沿形成 desire 脉冲,上传输入,执行 warp 与模型,按切片解析输出 大模型路径显式拒绝非有限输出;可选保留原始预测
    Parser.parse_outputs() 对轨迹、车道线、道路边缘、前车、意图和元信息执行 MDN / sigmoid / softmax 解码 结果供消息填充,不是最终车辆执行命令
    get_action_from_model() 从 action 头或 plan 回退提取目标曲率/加速度,按速度与延迟平滑 生成 ModelDataV2.Action:desiredCurvature、desiredAcceleration、shouldStop

    大模型选择逻辑在 main():检测到 Chestnut 后异步加载大模型,等待最多 60 秒,失败或超时改用小模型;大模型运行时异常会记录并切回小模型,小模型异常则继续抛出,交由进程管理层处理。这个回退是运行时选择,不能解释为对任意模型插件的兼容承诺。此外 extrinsics_calibration_seen 随标定数据更新并随模型消息发布,接入新相机时必须保留时间戳、图像几何和有效性语义。

    前车融合与纵向规划:radard 与 plannerd

    radard 不是「只转发雷达」的组件。即使没有雷达匹配,它也可能根据视觉预测产生 radar=False 的前车;反过来,低速场景可能用较近的雷达轨迹覆盖第一前车。Track.update() 按雷达 track ID 更新相对距离、横向位置、相对速度和滤波后的前车速度/加速度;match_vision_to_track() 用距离、横向位置与速度的概率分数选择轨迹;get_lead() 优先视觉与雷达匹配,无匹配但视觉概率足够时使用视觉前车。RadarD.update() 对齐自车速度与雷达延迟,结合 modelV2.leadsV3 形成 leadOne / leadTwo,RadarD.publish() 发布 radarState,有效位取自输入主题检查结果。

    plannerd.main() 等待 CarParams,以 modelV2 为节拍更新纵向计划与车道偏离警告,分别发布 longitudinalPlan 与 driverAssistance。LongitudinalMpc.update() 依据两辆前车和驾驶风格设定跟车时距、障碍距离及约束,运行 acados MPC;run() 调求解器保存速度、加速度与 jerk 轨迹,非零求解状态会记录告警并 reset(),此时该轨迹不能当成成功解。LongitudinalPlanner.update() 从 MPC、巡航及实验模式的端到端动作候选中选择最小加速度作为目标,shouldStop 对各候选取逻辑或。

    值得注意的是,所选加速度与 shouldStop 不一定来自同一候选,这种「最小加速度」合成是偏保守的候选合成,但不等于独立安全证明;求解器失败、输入无效、车型加速度能力和实际制动执行仍分别由其他层处理。车道偏离警告由 LaneDepartureWarning.update() 在高车速、近 5 秒未打转向灯且横向控制不活动时,结合车道线置信度、距离与变道意图触发,它不是转向控制命令。

    controlsd:从计划到 carControl

    Controls.__init__() 等待 CarParams,实例化车型接口、LongControl 与车辆动力学模型,并依转向控制类型或车型调参选择横向控制器;Controls.run() 以 100 Hz 调用 update()、state_control()、publish()。state_control() 计算使能、横向与纵向活动条件,执行纵向 PID 与横向控制,对目标曲率限幅,并将非有限执行器字段清零。

    横向实现 算法要点 边界
    角度接口 LatControlAngle.update() 用车辆模型把目标曲率转为目标转角,加入学习角偏置 执行器变化率仍由车型控制器与 panda 策略约束
    曲率接口 LatControlCurvature.update() 从当前转角估计曲率,配置 PID 时做误差闭环,否则前馈 输出限到 MAX_CURVATURE 的 PID 范围,不能当扭矩
    转角 PID LatControlPID.update() 以目标/当前转角误差闭环 安全限幅、驾驶员施力或低速时冻结积分
    扭矩接口 LatControlTorque.update() 在横向加速度域闭环,补偿摩擦、坡度与响应延迟 使用 1 秒目标加速度环形缓冲与 jerk 滤波

    state_control() 在 lateralManeuverPlan 有效时优先取其目标曲率,否则用 modelV2.action;停止横向活动时把目标重置为当前曲率以避免使能瞬间跳变。纵向 LongControl.update() 有 off / stopping / pid 三种状态,根据制动、巡航静止与 shouldStop 切换。需要特别注意的是,carControl 与 controlsState 的有效位当前仅赋值为 CS.canValid,源码注释指出若改为 sm.all_checks() 会形成循环依赖——因此消息有效位不能被误读为所有模型、计划与姿态输入都有效,下游状态机和 panda 安全门仍必须独立检查。

    插件化时的接口边界

    如果要按插件方式替换上述任一层,至少要保留该层的输入契约与有效性语义,而不是只替换一个数值输出。

    可替换层 至少保留的契约 不应开放的能力
    感知模型 VisionIPC 帧时间戳、相机标定、输出 schema、有效性/丢帧/延迟、启动预热与回退 直接发送车辆 CAN
    雷达融合 radarTracks 与 modelV2 对齐、leadOne/leadTwo 来源标志、错误与时延 绕过状态机强制使能
    规划器 longitudinalPlan 的轨迹、aTarget、停止/油门标志、求解状态和超时 把非成功解当成正常计划
    横纵向控制器 CarParams 能力、活动条件、饱和反馈、非有限数处理、控制频率 修改 panda 安全模式或白名单

    可迁移的路径是先建立消息契约与进程回放基线,再逐层替换算法实现。实车输出仍经 card 的车型接口和独立 panda 固件安全钩子,因此插件层的正确性并不等于执行链的完整性。

    常见问题

    为什么不能把 modelV2 到 carControl 当成一次函数调用?

    因为各进程速率不同:modeld 随图像帧运行,radard 与 plannerd 以 modelV2 为轮询主题,controlsd 按 100 Hz 循环。消息可能来自不同采样时刻,不存在保证它们同属一次采样的同步栈。

    radard 是雷达数据的转发器吗?

    不是。即使没有雷达匹配,也可能根据视觉预测产生 radar=False 的前车;低速场景还可能用较近的雷达轨迹覆盖第一前车。插件化时必须保留 track ID、延迟、误差和 present/radar 语义。

    carControl.valid 为真就代表所有输入都有效吗?

    不代表。当前 carControl 与 controlsState 的有效位仅取 CS.canValid;源码注释指出改成 sm.all_checks() 会形成循环依赖。因此它不能替代对模型、计划与姿态输入的独立检查。

    大模型加载失败会怎样?

    检测到 Chestnut 后异步加载大模型并等待最多 60 秒,失败或超时改用小模型;大模型运行时异常会记录并切回小模型,小模型异常则继续抛出,由进程管理层处理。这是运行时选择,不是对任意模型插件的兼容承诺。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PERCEPTION_CONTROL_DEEP_DIVE.md(取用了四条进程数据流、modeld 感知链、radard 融合、纵向 MPC 与 controlsd 输出,以及插件化接口边界)。
    • 项目 02 —— openpilot-research/CONTROL_ALGORITHMS_DEEP_DIVE.md(仅用于确认四种横向控制器与纵向执行状态的分层位置)。

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

    更新记录

    首次发布 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

  • Hyundai 控制输出:经典 CAN 与 CAN FD 两条构帧路径与独立安全门

    结论:Hyundai 的控制输出不是一条「转向命令」链路,而是同一份车型控制器代码按经典 CAN / CAN FD、原车纵向 / 自控纵向、LKA / LFA 架构分支,形成不同候选帧,再交给 panda 固件的 hyundai_tx_hook 或 hyundai_canfd_tx_hook 独立复核。主机侧 CANPacker 编码成功、CarOutput.accel 回报正确、tester-present 发出,都不等于 ECU 接受。下述节拍以 card 目标 100 Hz 且每轮调用一次控制器为前提,是代码名义节拍,不是实测总线频率。

    公共控制周期:力矩、加速度与请求位

    每一控制轮,CarController.update() 先把 actuators.torque 乘以车型 STEER_MAX,再以最近一次软件输出和 CS.out.steeringTorque 调用驾驶员力矩/变化率限制。横向未活动时力矩清零;经典 CAN 分支在横向活动但请求位被切断时还会设置 torque_fault,CAN FD 分支没有这个额外字段。这是两条分支在同一控制器里的第一处实现差异。

    common_fault_avoidance() 在转角绝对值达到 85 度时管理请求位,目的在于避免 EPS 长时间大角度带扭矩故障。加速度只做 [-3.5, 2.0] m/s² 夹限,再传给具体纵向报文;CarOutput 回报的 accel 就是这一软件夹限值,不是 CAN FD 内部 aReqValue、MCU 发送回执或车辆实测加速度。

    诊断保活方面,若启用 openpilot 纵向且非 CANFD_CAMERA_SCC,每 100 帧向经典 CAN 雷达 0x7d0、CAN FD E-CAN 雷达 0x7d0 或 LKA 架构 ADAS ECU 0x730 形成一条 tester-present 候选。它的目的是维持此前请求的 ECU 静默,不能证明 ECU 已静默;CarInterface.init() 使用通用 disable_ecu(),其成功/失败返回值没有被 card.ControlsReady 闭环消费。取消请求持续时 cancel_counter 逐轮加一,解除请求即归零——源码注释指出部分车辆的取消键也可能是暂停/恢复键,因此原车纵向的按钮取消被延迟。

    经典 CAN 输出:LKAS11 与 SCC 家族

    LKAS11 每控制轮形成候选:继承 CS.lkas11 中一批原车相机字段,覆盖转向力矩、请求位、HUD 警告、车道线和计数,再按车型选择 CRC8、前 6 字节和、或前 6+末字节和三种校验之一。它不是泛用的「转向命令」结构,不同车型的显示字段也有例外,process_hud_alert() 对部分 Genesis 警告采用不同编码。原车纵向下,持续取消超过 10 帧后发送一条 CLU11 取消候选;恢复请求则在距上次按钮帧大于 0.1 秒时一次追加 25 个相同恢复候选,而 last_button_frame 只在间隔至少 0.15 秒时更新。

    自控纵向每偶数帧形成 SCC11、SCC12、SCC14;USE_FCA 且非 CAMERA_SCC 时还生成 FCA11。SCC12 的 aReqRaw / aReqValue 都取软件加速度,计数器及 nibble checksum 由构帧函数处理;SCC14 传入 jerk 上限和 HUD 前车状态。非 FCA 路径在 SCC12 设置 AEB disabled 字段,FCA 路径在 FCA11 设置相应禁用状态。

    经典 CAN 报文 触发条件与节拍 关键边界
    LKAS11 每控制轮 覆盖力矩/请求位/HUD;校验方式按车型三分支
    CLU11 取消 原车纵向且持续取消超过 10 帧 「最多 10 Hz」注释不等于严格上限
    恢复候选 ×25 距上次按钮帧 > 0.1 秒 不构成 25 条 ECU 接受证明
    SCC11 / SCC12 / SCC14 自控纵向每偶数帧 SCC12 双加速度字段;SCC14 传 jerk 与前车状态
    FCA11 USE_FCA 且非 CAMERA_SCC 设置制动执行禁用状态
    SCC13 / FCA12 5 Hz 选项帧 额外选项帧,非原车 AEB 保留保证
    FRT_RADAR11 2 Hz 选项帧 同上,单独验证
    LFA 图标 SEND_LFA 平台每 5 帧 仅图标候选

    经典 panda hook 对 LKAS11 限扭矩/请求位,对 SCC12 检查两路加速度并禁止 AEB 执行字段,对 FCA11 阻止制动执行位;诊断地址只允许固定 tester-present 载荷。不同 hyundai、hyundaiLegacy、相机 SCC 和纵向模式选择不同 RX 检查与 TX 白名单,软件字段编码正确仍可能被 MCU 拒绝。

    CAN FD 输出:LKA/LFA 路由与 SCC_CONTROL

    CanBus 按 LKA/LFA 转向架构决定 A-CAN 或 E-CAN。LKA 平台生成 LKAS / LKAS_ALT,仅在自控纵向时同时补 LFA,其他平台直接生成 LFA。LKA 平台每 5 帧还用最近缓存的 CAM_0x2a4 / CAM_0x362 字段构造「无车道线」报文来抑制原车 LFA——这属于 ADAS 能力修改,应单独验证缓存年龄与原车功能影响。图标候选按 LKA/纵向组合每 5 帧输出;若 flag 开启,LKA 平台可生成 SPAS1 / SPAS2 转向灯候选。

    自控纵向每偶数帧生成一条 SCC_CONTROL:aReqRaw 用夹限后的目标,aReqValue 用与上次记录 accel_last 相差不超过 5/50 的值;accel_last 随后记录当前原始目标,而不是编码出的 aReqValue。所以这个运算不能简单称为持续的输出速率限制,阶跃目标须回放核对两个字段和 ECU 响应。LKA 架构另发 ADRV 系列报文以配合禁用 ADAS ECU,LFA 架构按偶数帧生成 AEB disabled 图标报文。

    CAN FD 路径 生成条件 需单独验证的点
    LKAS / LKAS_ALT LKA 架构每轮 总线归属(A-CAN/E-CAN)
    LFA 自控纵向补发,或非 LKA 平台直接生成 架构判定是否正确
    无车道线抑制报文 LKA 平台每 5 帧 缓存 CAM 报文年龄、原车功能影响
    SCC_CONTROL 自控纵向每偶数帧 aReqRaw 与 aReqValue 的 5/50 差值语义
    ADRV 系列 LKA 架构 与禁用 ADAS ECU 的配合
    AEB disabled 图标 LFA 架构每偶数帧 仅图标,不等于 AEB 状态
    SPAS1 / SPAS2 flag 开启且 LKA 平台 本快照未见设置该 flag 的车型

    原车纵向 CAN FD 用按钮或 SCC_CONTROL 取消:备用按钮平台直接复用最近 CS.cruise_info 的计数/校验及状态字段,把 ACCMode 改为 4、加速度置零;标准按钮平台须取消持续超过 10 帧、距上次按钮超过 0.25 秒,才一次追加 20 个相同的取消按钮候选。标准按钮的恢复也可一次追加 20 个候选;备用按钮平台的恢复分支只是 pass,本快照未实现。MCU safety 仍要求原车纵向 SCC_CONTROL 为取消模式且加速度为零。

    插件化与硬件验收边界

    把转向、纵向、巡航按钮、HUD、ADAS 抑制和诊断保活声明为不同能力,各自绑定平台、DBC、线束、节拍及安全模式,是这套实现的自然结论。车型控制插件只提交结构化候选;唯一受信任网关验证 CAN 地址、物理总线、长度、年龄、限幅和 ECU 静默状态,并由独立 MCU 保留不可绕过的 TX safety。禁止把 CANPacker 成功、CarOutput 回报或 tester-present 发送等同于 ECU 接受。

    无车验证应覆盖经典 CAN/CAN FD、LKA/LFA、原车/自控纵向、相机 SCC、备用按钮、AEB 图标、缓存断流、大转角请求位、加速度阶跃和诊断失效,再逐帧与 DBC、panda hook 对照。CANFD_ENABLE_BLINKERS 还存在对 0x7b1 的保活及 SPAS 构帧代码,但本快照静态平台表未见设置该 flag 的车型,不能把代码分支当成已启用的实车能力。

    常见问题

    为什么 CAN FD 里的 aReqValue 不能当作真实加速度限幅?

    因为 SCC_CONTROL 的 aReqValue 取的是与上次记录 accel_last 相差不超过 5/50 的值,而 accel_last 记录的是当前原始目标而非编码结果。这个运算在阶跃目标下不是持续速率限制,须回放核对两字段与 ECU 响应;CarOutput 回报的 accel 也只是软件夹限值,不是车辆实测加速度。

    tester-present 发出去,是否说明雷达 ECU 已被静默?

    不代表。每 100 帧形成的 tester-present 候选只用于维持此前请求的 ECU 静默状态,本身不能证明 ECU 已进入静默;CarInterface.init() 使用的 disable_ecu() 返回值也没有被 card.ControlsReady 闭环消费,因此不能由「帧已构造」推断「ECU 已接受」。

    经典 CAN 与 CAN FD 的转向输出差在哪?

    经典 CAN 每控制轮形成 LKAS11,并在横向活动但请求位被切断时设置 torque_fault;CAN FD 按 LKA/LFA 架构生成 LKAS/LKAS_ALT 或 LFA,没有这个额外字段。此外 CAN FD 的 LKA 平台还会每 5 帧构造「无车道线」报文抑制原车 LFA,属于 ADAS 能力修改。

    备用按钮平台的恢复功能是否已实现?

    未实现。源文档明确:原车纵向 CAN FD 中,标准按钮平台的恢复可一次追加 20 个候选,而备用按钮平台的恢复分支只是 pass,本快照未实现。取消分支则复用最近 CS.cruise_info 的字段并把 ACCMode 改为 4、加速度置零。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/HYUNDAI_CONTROLLER_CAN_OUTPUT_DEEP_DIVE.md(取用了公共控制周期、经典 CAN 与 CAN FD 构帧节拍、panda hook 复核点与插件化边界)。
    • 项目 02 —— openpilot-research/CAR_PROTOCOL_IMPLEMENTATIONS.md(仅用于确认 Hyundai 品牌目录与 CAN/CAN FD 链路的总体位置)。

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

    更新记录

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

  • openpilot legacy 支持矩阵:可读车型与低速边界登记表

    结论:这份「车型 CAN 数据档案」记录的不是某一辆车的逐条报文,而是一套多车型支持矩阵:源文档以「原车纵向 / 自控纵向、出厂 ACC 是否被替换、低于多少速度不可用」为列,逐平台登记可读能力,并说明车辆数据由 opendbc 目录下的文件解释。换言之,它回答的是「这台车能否被读取、由谁供给纵向控制、低速边界在哪」,而不是「0x1XX 的第几个字节是什么」。下述结论仅限源文档登记的那份 legacy 支持清单,不能外推到清单之外的车型、年款或市场版本;源文档本身也未提供任何逐信号 DBC 定义。

    车辆与适用范围

    源文档是一份开源驾驶辅助系统的总览说明,覆盖其官方支持与社区维护两级车辆名单。数据来自该项目的公开文档快照,不是对某台实车的采集记录,因此这里不存在「采样了几小时、几万帧」的采集跨度,只有一份能力登记表。

    项 内容
    车型 多品牌多车型的平台集合(Acura、Honda、Hyundai、Lexus、Toyota、Subaru、Volkswagen 等)
    年款 / 市场版本 以「美国市场参照(US Market Reference)」列登记,年款随平台不同;源文档未确认其他市场版本的对应关系
    ECU / 改装状态 分「官方支持」与「社区维护」两级;部分平台需断开驾驶员支持单元(DSU)或加装 ASCM 线束才能由本系统接管纵向
    采集跨度 源文档未提供日志数或事务数,仅提供能力矩阵与目录结构
    适用范围 仅覆盖源文档列出的平台与年款区间;社区维护平台未被上游安全模型验证

    总线与寻址

    源文档不描述物理层细节,只在目录结构中说明 opendbc「展示如何解释来自车辆的数据」,并由 selfdrive/car 承载各车型的状态读取与执行器控制。也就是说,寻址方式、比特率与帧格式都在被引用的 DBC 与车型代码里,本快照未包含这些定义本身。

    项 内容
    物理层 源文档未给出(由各车型 DBC / 适配器决定)
    链路层 / 比特率 源文档未给出
    寻址方式 源文档未给出;仅提及 opendbc 存放「如何解释数据」的文件
    CAN FD 源文档未提供该版本是否包含 CAN FD 车型
    承载硬件 EON DevKit 或 comma two,配合车型线束;实验场景可用带外置摄像头的 Ubuntu 电脑

    已解码信号

    本项目的源文档是一张能力矩阵,而非逐信号解码表。为保持「只抄源文档里有证据的条目」这一原则,下表登记的是源文档逐平台明确给出的可读字段(支持包、ACC 供给方、低速边界),并逐条注明证据页。任何涉及具体字节、DID 或缩放系数的内容,源文档均未提供。

    平台(美国市场参照) 支持包 可读字段(ACC 供给 / 低速边界) 证据
    Acura ILX 2016-19 AcuraWatch Plus ACC 由本系统供给;低于 25mph 不可加速;低于 25mph 无 ALC Supported Cars 表
    Acura RDX 2019-21 All ACC 保留出厂;低于 0mph 不可加速;低于 3mph 无 ALC Supported Cars 表
    Honda Accord 2018-20 All ACC 保留出厂;低于 0mph 不可加速;低于 3mph 无 ALC Supported Cars 表
    Honda Civic Sedan/Coupe 2016-18 Honda Sensing ACC 由本系统供给;低于 0mph 不可加速;低于 12mph 无 ALC Supported Cars 表
    Honda CR-V 2017-20 Honda Sensing ACC 保留出厂;低于 0mph 不可加速;低于 12mph 无 ALC Supported Cars 表
    Hyundai Palisade 2020-21 All ACC 保留出厂;低于 0mph 不可加速;低于 0mph 无 ALC Supported Cars 表
    Lexus ES 2019-21 All ACC 由本系统供给;低于 0mph 不可加速;低于 0mph 无 ALC Supported Cars 表
    Toyota Camry 2018-20 All ACC 保留出厂;低于 0mph 不可加速;低于 0mph 无 ALC Supported Cars 表
    Toyota Corolla 2020-21 All ACC 由本系统供给;低于 0mph 不可加速;低于 0mph 无 ALC Supported Cars 表
    Toyota Rav4 2019-21 All ACC 由本系统供给;低于 0mph 不可加速;低于 0mph 无 ALC Supported Cars 表
    Toyota Prius 2016-20 TSS-P ACC 保留出厂;低于 0mph 不可加速;低于 0mph 无 ALC Supported Cars 表
    Subaru Forester 2019-21 EyeSight ACC 保留出厂;低于 0mph 不可加速;低于 0mph 无 ALC Supported Cars 表
    Volkswagen Golf 2015-19 Driver Assistance ACC 保留出厂;低于 0mph 不可加速;低于 0mph 无 ALC Community Maintained 表

    读取方法与注意事项

    源文档给出的操作条件集中在安装与接入层面:安装时在设备安装向导中输入指定的安装地址,随后按视频说明把设备固定到风挡;设备自带位姿自动标定,轻微的俯仰、偏航安装误差不影响性能。上车前需先确认当地法规是否禁止在风挡上放置物品。

    数据层面,源文档说明系统会记录面向道路的摄像头、CAN、GPS、IMU、磁力计、温度传感器、碰撞与系统日志;面向驾驶员的摄像头仅在设置中显式开启时记录,麦克风不记录。因此从 CAN 侧能拿到什么,取决于各车型的 opendbc 定义与本机车型代码,源文档并未展开。

    能力边界方面,官方支持的车型将被纳入上游安全模型验证;社区维护车型未被验证,需要先在 Settings->Developer->Enable Community Features 中打开开关才能使用,且使用时须格外谨慎。若要把社区平台提升为官方支持,需满足:brand 下至少拥有一辆车、对应线束在售、panda 与本系统均具备完整 ISO 26262 支持、存在纵向控制的推进路径、AEB 仍保持启用、且支持 fingerprinting 2.0。

    证据等级与边界

    本文全部内容属于「文档登记」,既不是实车观测,也不是外部旁证。能力矩阵中的每一行都来自源文档的表格,可核对;但表格只回答「该平台被登记为支持/社区支持」,不保证具体年款、市场版本、配置包与软件版本都能匹配。源文档用「美国市场参照」列明确提示了这一点。

    三处不能外推。第一,低速边界(No ACC accel below、No ALC below)是按平台登记的名义值,不是对某一台实车测得的曲线,同型号不同配置可能不同——例如源文档用脚注说明 2019 Honda Civic 1.6L Diesel Sedan 在 12mph 以下无 ALC,Camry 的 4CYL 部分配置边界为 28mph。第二,断开 DSU 或加装 ASCM 线束会让本系统接管 ACC,但同时关闭 AEB,这是功能取舍而非免费升级。第三,车速相关字段(如 ALC 边界)来自源文档表格,源文档未提供任何原始报文或 DBC 条目作为间接支持。

    关于「安全访问」类问题,源文档未提供任何绕过方法,且不建议尝试;本项目讨论的是公开文档登记的可读能力,不涉及对车辆安全机制的规避。

    常见问题

    这份档案为什么没有逐条 CAN 信号?

    因为主素材是一份多车型支持矩阵与安装说明,只登记「平台 / 支持包 / ACC 供给方 / 低速边界」四类字段,逐信号定义放在被引用但未包含在本快照内的 opendbc 文件中。这是素材本身的边界,不是解析遗漏。

    「ACC 由本系统供给」与「ACC 保留出厂」有什么区别?

    前者的纵向控制由本系统承担,后者仍由原车 ACC 执行、本系统只做横向。源文档同时提示:在部分平台上,需断开驾驶员支持单元(DSU)或加装社区 ASCM 线束才能切换,而这两种做法都会关闭原车 AEB。

    社区维护车型能当官方支持用吗?

    不能。源文档明确社区维护车型未经上游安全模型验证,须在开发者设置中手动开启社区功能,并提示使用时格外谨慎;升级为官方支持还需同时满足拥有车辆、线束在售、完整 ISO 26262、纵向路径、AEB 保持启用、支持 fingerprinting 2.0 等条件。

    低速边界能不能套用到同型号其他车?

    不建议。源文档的边界是按平台以「美国市场参照」登记的名义值,并以脚注给出个别配置的例外(如某柴油版 Civic 与部分 Camry 配置)。把它直接外推到其他年款、市场版本或配置,超出了源文档的证据范围。

    延伸阅读

    参考资料

    • 项目 01 —— public-can-dbc-datasets/additional/openpilotusers-opkr084-legacy-subset/README.md(本项目车型档案主来源:支持车型与社区维护车型矩阵、安装说明、目录结构)。

    说明:本文整理自第三方开源项目的公开文档,仅代表该文档所记录的单个车辆样本,不代表同型号全系车型。CAN 与诊断操作存在风险,任何实车操作前请自行复核并遵守当地法规。

    更新记录

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

  • 座舱预览的双屏入口、面框绘制与声音主题边界

    结论:座舱摄像头预览是一条展示 / 测试路径,源码里没有任何一处把「预览能画出面框、能发声」当成驾驶员监测(DMS)已就绪的证明。两个入口(大屏设备页与 mici 设备页)都只在 ui_state.is_offroad 为真时允许点击;预览把 driverStateV2 与 driverMonitoringState 拼在一起绘制,但这两条消息之间没有同帧对齐或新鲜度门;mici 预览发布的 selfdriveState 用的是 new_message() 默认的 valid=False,而声音服务只按「是否 updated」读取 alertSound,不检查有效位。固定快照基线为 c8fb906。

    双屏入口:点击门与构造时写入的参数

    预览按钮存在两个入口:大屏设备页(device.py 的 L60 附近)与 mici 设备页(device_layout.py 的 L183 附近)。两者都用 ui_state.is_offroad 在行车时禁用预览按钮——这是一道点击入口门,不是对话框运行期间的持续安全检查。

    更值得注意的是参数写入的时机。大屏 CabinCameraDialog.__init__() 在构造时就同步写 IsDriverViewEnabled=True,并追加一个交互超时 pop_widget 回调;而不是等到 show_event()。反复构造会累积回调。mici 的 show_event() 才写 demo 参数、把交互超时延长到 300 秒、清 DriverTooDistracted 并建立 selfdriveState 发布端;hide_event() 关 demo 参数、恢复超时。

    IsDriverViewEnabled 由 dmonitoringd_thread() 每 40 个 driverStateV2.frameId 读取一次。demo 下只要有有效驾驶员模型消息,就运行 DriverMonitoring.run_step(..., demo=True),使用固定的模拟速度、使能和标定输入。因此 UI 写参数不是立刻切换策略,预览中的警示等级也不是实车速度 / 档位条件下的策略结果。

    入口 关键行为 契约边界
    大屏设备页 / mici 设备页 以 is_offroad 禁用行车预览按钮 点击入口门,非运行期持续检查
    大屏 CabinCameraDialog.__init__() 构造时即写 IsDriverViewEnabled=True,追加超时回调 反复构造累积回调
    mici show_event() / hide_event() 写 / 清 demo 参数,超时 300 秒,建发布端 首次 show_event() 在发布端创建前发 _publish_alert_sound(None),不发送
    mici 培训 CabinCameraSetupDialog 复用基类,_render() 不调用 _render_dm_alerts() 培训绘制路径与设备页声音路径不可混同

    面框绘制:两条消息没有对齐门

    大屏 _render() / _draw_face_detection() 在无帧时显示启动文案,有帧时按 driverStateV2 的左右驾驶位概率、faceProb>0.7 与姿态标准差绘制面框。绘制侧没有检查这条模型消息的年龄或 valid,面框坐标是近似公式。

    mici 侧更复杂:driverMonitoringState 决定面部检测门,而面框与眼镜概率使用 driverStateV2 字段。两个消息没有同帧对齐,也没有新鲜度门。无帧时 mici 仍会发布一次无声消息。这意味着「画出了一个面框」既不能证明模型消息是新鲜的,也不能证明它与当前画面属于同一帧。对自研相机 / 计算板的接口而言,画面帧、driverStateV2 与 driverMonitoringState 应分别显示采样年龄与有效性;模型面框必须与具体相机标定 / 帧时间关联,不能只按静态坐标公式判定安装合格。

    声音总线:同主题发布与非阻塞有效位

    mici 设备页在画面可见时每次绘制都执行 _render_dm_alerts(),从而发布 selfdriveState。_publish_alert_sound() 把 alertLevel 映射为预警 / 提示 / 紧急声音,用 messaging.new_message('selfdriveState') 发布一条只填了 alertSound 的消息。

    问题在于有效位。new_message() 默认 valid=False,此处没有覆写;其他 selfdriveState 字段仍是默认值。而 Soundd.get_audible_alert() 只检查 sm.updated['selfdriveState'],读取 alertSound 而不检查 valid。因此预览可以驱动声音;声卡是否实际播放仍取决于进程运行条件与设备。

    相同主题也被控制、规划、UI 等模块读取,不能假定它只是私有的测试音轨。驻车入口虽然降低了同时行车的概率,但源码未提供预览发布者的独立主题,也没有持续的驻车发布门。建议把预览音效改为独立测试音命令 / 主题,或由声音服务显式接收带来源和驻车门的请求,正式 selfdriveState 保持单一权威发布者。

    风险点 源码事实 影响
    超时回调只增不减 add_interactive_timeout_callback() 只 append,注册的是无参数 gui_app.pop_widget() 多次打开预览后,下次超时可连续弹出多个当前导航页
    offroad 回调未注销 两套 CameraView 构造时注册绑实例回调;close() 释放纹理 / 客户端但不注销 已关闭实例仍可能被列表引用
    泄漏测试被跳过 test_widget_leaks.py 被 skip("segfaults") 不能视为通过的生命周期证据
    mici hide_event() 只关子视图 清参数与超时覆盖,仅 close() 关闭子视图 再显示时底层状态需按各路径分别验证

    生命周期:只追加的回调与未注销的订阅

    大屏和 mici 对话框在每次构造时都调用 Device.add_interactive_timeout_callback(),该函数只 append,而 Device._update_wakefulness() 在超时边沿遍历全部历史回调。它们注册的是无参数的 gui_app.pop_widget(),不是「仅关闭此预览实例」。先后打开多个预览后,下一次超时可连续弹出多个当前导航页——这在源码层面可推导,具体导航栈 / 事件调度需 GUI 故障注入。

    两套底层 CameraView 均在构造时向 UIState 注册绑定实例的 offroad 回调;close() 释放纹理 / 客户端,但未注销该回调。UIState.add_offroad_transition_callback() 也只追加,因此已关闭实例仍可能被列表引用,长期反复打开可能保留对象并在状态切换时调用已关闭视图。

    把预览的「请求驾驶员模型 demo」定义为有所有者、可撤销、带超时的租约,是避免裸布尔 Params 在多个 UI 页面之间互相覆盖的方向;回调订阅、VisionIPC 客户端、纹理与超时覆盖都需要可重入的 show / hide / close 生命周期测试,至少覆盖连续打开、页面切换、超时、摄像头断流、离线转行车与 GUI 重启。

    常见问题

    座舱预览能画出面框,是否说明驾驶员监测已就绪?

    不能。预览把 driverStateV2 与 driverMonitoringState 拼在一起绘制,但两者没有同帧对齐或新鲜度门,绘制侧也不检查模型消息的年龄或 valid;demo 模式还使用固定的模拟速度、使能和标定。预览是展示 / 测试路径,不构成 DMS 安全就绪的证明。

    预览为什么会影响声音?

    mici 预览用 new_message('selfdriveState') 只填 alertSound 发布消息,默认 valid=False;而 Soundd.get_audible_alert() 只检查是否 updated,读取 alertSound 而不看 valid。这就让预览能驱动同一条声音主题,该主题同时被控制、规划、UI 读取。

    反复打开预览会有什么副作用?

    两处回调都只追加不注销:交互超时回调追加无参数 pop_widget(),offroad 回调在 close() 后仍保留引用。源码层面可推导下次超时可能连续弹出多个导航页、已关闭视图仍可能被调用;具体调度与资源增长需故障注入验证。

    预览按钮的驻车限制够不够?

    不够。两个入口只用 is_offroad 在点击时禁用按钮,这是入口门而不是对话框运行期间的持续检查;源码未提供预览发布者的独立主题或持续的驻车发布门。降低同时行车概率不等于运行期安全约束。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CABIN_PREVIEW_DM_AUDIO_LIFECYCLE_DEEP_DIVE.md(取用了双屏入口、DM 面框绘制、声音主题发布与回调生命周期分析)。

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

    更新记录

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

  • 19 条迁移规则:migrate_all() 的合成字段不等于原始测量

    结论:process_replay 的历史日志迁移是离线回放输入的兼容层,不是车辆实时消息改写路径。migrate_all() 默认列出 15 条规则,manager_states 加 1 条、panda_states 加 2 条、camera_states 加 1 条,共最多 19 条。核心判定有两条:迁移产出的是合成字段,不等于原始测量;而 migrate() 让所有规则读取同一份原始列表,前一条的替换/新增不会成为后一条的输入——这是兼容性批处理,不是依次变换的管道。本文仅做静态源码与 Python AST 检查,本机缺 capnp,未运行日志迁移、整段回放、模型或实车。

    调度契约

    replay_process() 始终打开 manager 迁移,并按进程发布主题开启 panda 与相机迁移。migrate() 先把输入全量转列表并按原始 which() 分组:每条规则由 @migration 标注输入主题和可选 product,原始日志中只要出现一次该 product,整条规则跳过。结束时先替换、再倒序删除、再追加,最后按 logMonoTime 稳定排序。

    开关 规则数变化 触发条件
    默认 15 条 migrate_all() 的基础规则集
    manager_states +1 条 replay_process() 始终打开 manager 迁移
    panda_states +2 条 按进程发布主题开启
    camera_states +1 条 按进程发布主题开启

    由于每条规则都独立读取同一份原始列表,规则之间的书写顺序在语义上无关紧要;真正决定结果的是「哪些 product 出现在原始日志里」以及各规则自身的触发条件。也正因为如此,同一份日志在不同开关组合下会得到不同的迁移结果,比较前必须先固定开关集合。

    此外,规则跳过是「整条跳过」而非「逐条补齐」:新主题一旦在原始日志中出现,对应旧消息在整个路段都不会被迁移生成。

    逐函数规则表

    下表是按规则入口整理的全量清单。它们的共同特征是:多数规则只在「原始日志缺少对应新主题」时才生成消息,一旦新主题已存在就整条跳过;生成的字段往往直接置 valid=True 或沿用旧时间戳,因此回放里看到的「有效」并不等于原始采集时该值真实有效。

    规则入口 输入到输出或修改 证据/注意点
    migrate_sensorEvents() 旧聚合 sensorEventsDEPRECATED 拆为加速度、陀螺仪、光、磁力、温度消息,删旧消息 新消息无条件置 valid=True,即使旧 Event 无效
    migrate_carParams() 用 opendbc MIGRATION 重写车型指纹,并把每项 carFw.brand 设为当前品牌 为回放统一身份,不是 ECU 固件真实性校验
    migrate_gpsLocation() 当 flags==1 且无 fix 时补 hasFix=True,覆盖内部/外部 GPS 两主题 只做布尔兼容,不重新评估 GNSS 质量
    migrate_deviceState() 取首条 initData.deviceType 写到所有 deviceState 缺任一主题时不操作
    migrate_carOutput() 从旧 carControl.deprecated.actuatorsOutput 合成 carOutput 原始日志只要已有一条 carOutput 就跳过全部缺失时点
    migrate_controlsState() 从旧 controlsState.deprecated 合成 selfdriveState 的使能、告警等字段 合成状态沿用旧 valid/时间
    migrate_carState() 以先前 controlsState.deprecated.vCruise 修正 carState.vCruise/cluster 只在差值大于 0.1 时改;依赖原始遍历顺序
    migrate_liveLocationKalman() 旧姿态、速度、加速度、角速度及健康位转 deviceMotion 缺失向量改为 NaN,时间戳取 Event 的 logMonoTime
    migrate_deviceMotion() 若所有已有 deviceMotion.timestamp 均为 0,逐条补 logMonoTime 混合零/非零时整条规则不操作
    migrate_liveTracks() 旧 liveTracksDEPRECATED 换为 radarTracks.points 仅保留 trackId/dRel/yRel/vRel 等子集
    migrate_driverAssistance() 每条 longitudinalPlan 旁合成空 driverAssistance 存在并不证明旧算法输出了该辅助结果
    migrate_drivingModelData() 从 modelV2 提取帧、动作、车道线与轨迹多项式 meta 列表重复 laneChangeState,遗漏 laneChangeDirection
    migrate_onroadEvents() 旧事件列表换为新 onroadEvents,过滤 DEPRECATED 与无法映射枚举 空成员 RuntimeError 会打印并跳过
    migrate_driverMonitoringState() 旧 DMS 模式、注意力、事件映射为 vision/wheeltouch 策略、三级告警及 lockout 百分比与标定值由旧字段估算,不能当原生轨迹
    migrate_longitudinalPlan() 所有 aTarget==0 且存在 carParams 时,用 speeds/accels 估算 aTarget 与 shouldStop 若任一计划已有非零 aTarget,整条不操作
    migrate_managerState() 用当前 managed_processes 名单重建每条进程列表,统一置 running=True 合成结果不是当时真实进程健康快照
    migrate_pandaStates() 从旧单 panda 转列表,或修改现有列表最后一项的 safetyParam 与 alternativeExperience 需要 carParams,不能拿回放值当 MCU 实际配置读回
    migrate_peripheralState() 按原始 pandaState(s) 的时间与 valid 合成空 peripheralState 不能据此证明原设备供电/风扇等外围指标真实存在
    migrate_cameraStates() 从三路 EncodeIdx 的 frameId 找 segmentId,重建 CameraState 的 frameId/encodeId,缺 SOF 时估算 已有索引但某帧缺匹配则删除该相机状态

    从变换方向看,这些规则大致分三类:拆分与改名(如传感器事件、雷达轨迹、onroad 事件)、估算与合成(如纵向计划、驾驶员监测、外设状态与 carOutput)、统一身份与默认填充(如车型参数、manager 状态、deviceMotion 时间戳)。三类对回放结论的可信度不同,若把合成值当原始值,回归比较会得出错误结论。

    精确发现与证据级别

    1. migrate_drivingModelData() 的循环 for meta_field in ["laneChangeState", "laneChangeState"] 复制同一字段两次;源与目标均有该方向字段,故迁移方向保持 schema 默认值(源码静态确认)。
    2. product 判断只看原始日志主题是否出现,不逐时间戳补齐;新主题存在即阻止该规则为旧消息生成对应时点。
    3. migrate_pandaStates() 启用时无条件要求 carParams;回放需提供完整车辆身份输入。
    4. migrate_cameraStates() 用已见最小 frameId 作无索引回退,既非全段预扫描最小值,也非视频帧实体校验。

    AST 无依赖检查确认了 19 个装饰器规则及其输入/product 声明;本机 import capnp 失败,故未执行 Cap’n Proto 迁移单测。本文不能证明 19 条规则的运行结果、性能或跨版本 schema 兼容。

    对硬件与插件化重构的意义

    历史日志迁移应属于离线验证数据适配插件,与车端实时消息源严格隔离。插件清单要声明输入 schema、产出 schema、字段级变换、合成/丢弃策略与版本,并把「原始观测」「迁移估算」「默认填充」分别标注。车型、panda safety、相机时间与模型输出的迁移数据可以帮助回归比较,但不能给新硬件或车辆授予控制资格;新增硬件版本须保留原始日志、迁移后的日志和差异报告,尤其复核 valid、logMonoTime、方向枚举与安全参数。

    具体到回放实验,建议保留原始日志、迁移后的日志与差异报告三份产物;在比较前先核对被迁移主题的数量、顺序与时间是否单调,再判断差异来自算法改动还是迁移策略本身。缺少这三份产物时,一次「通过」既无法定位回归,也无法解释合成字段的来源。

    常见问题

    迁移后的日志能当原始测量吗?

    不能。迁移产出的是合成字段:如 migrate_sensorEvents() 无条件置 valid=True、migrate_controlsState() 沿用旧 valid/时间、migrate_peripheralState() 合成空结构,因此须把「原始观测」「迁移估算」「默认填充」分别标注。

    一条规则会用到前一条规则生成的消息吗?

    不会。migrate() 让所有规则读取同一份原始列表,前一条的替换/新增不会成为后一条的输入,结束时才统一替换、倒序删除、追加并按 logMonoTime 排序——这是兼容性批处理,不是管道。

    laneChangeDirection 迁移后为什么是默认值?

    migrate_drivingModelData() 的循环 for meta_field in ["laneChangeState", "laneChangeState"] 复制同一字段两次,未处理 laneChangeDirection,因此方向保持 schema 默认值。这是源码静态确认,尚无动态回放证据。

    迁移规则一共是多少条?

    最多 19 条:migrate_all() 默认 15 条,manager_states 加 1 条,panda_states 加 2 条,camera_states 加 1 条;replay_process() 始终打开 manager 迁移,panda 与相机迁移按进程发布主题开启。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PROCESS_REPLAY_SCHEMA_MIGRATION_RULES_OUTPUT_VALIDITY_DEEP_DIVE.md(19 条迁移规则、调度契约与证据级别)
    • 项目 02 —— openpilot-research/PROCESS_REPLAY_VALIDATION_DEEP_DIVE.md(迁移规则所处的回放主链路)

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

    更新记录

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

  • 二维码编码只到版本 20、纠错固定 L:openpilot 编解码两端并不对称

    结论:openpilot 的二维码模块编码端与解码端能力并不对称:编码只覆盖版本 1 到 20、只构造 byte mode,并固定纠错等级 L 与 mask 0;解码端则支持版本 1 到 40 与四个纠错等级,还会尝试相邻版本、alignment 与转置等候选。更重要的产品边界是:二维码只是载体不是授权——配对令牌取不到时会退化成空令牌 URL,而这个 URL 仍能成功编码成图形,所以「屏幕上有二维码」不证明令牌有效;eSIM 扫码的前置检查也只校验字符串形状。以下为固定源码快照 c8fb906 的静态分析与隔离测试结论。

    编码与屏幕呈现

    make_texture() 把输入字符串编码为 UTF-8,按 _capacity() 从版本 1 到 20 选择首个可容纳的码型,超过上限抛 ValueError。_data_codewords() 只构造 QR 的 byte mode,写入 8/16 位长度、终止位与交替填充字节;_codewords() 按 _EC 表分块、计算 GF(256) Reed-Solomon 余数并交织。当前编码固定纠错等级 L 和 mask 0,并不搜索视觉质量最佳的 mask,编码版本范围也比解码范围窄。

    _Qr 绘制 finder/timing/alignment、格式位与版本位,再以两列之字形填入数据。普通模式外加 4 模块 quiet zone,反色模式不加边框;两者都把每个模块放大为 10×10 RGBA 像素后通过 raylib 上传纹理。make_texture() 的注释认为上传会复制像素,因此中间 NumPy 数组无需保持存活,但实际 GPU 驱动资源生命周期仍由 UI 调用方负责。

    项 编码端 解码端
    版本范围 1 到 20 1 到 40
    纠错等级 固定 L 四个等级均支持
    字符模式 仅 byte mode(UTF-8) 数字、字母数字、byte、Kanji 与受支持 ECI
    mask 固定 mask 0,不搜索 从格式位恢复级别/mask

    配对 QR 的展示边界

    大屏 PairingDialog._generate_qr_code() 先释放旧纹理,再把 DongleId 与配对 JWT 拼入 URL 制作新纹理,每 300 秒刷新,失败时记录日志并清空纹理。二维码中可能有配对令牌,视觉截图、投屏与诊断日志应视为敏感路径;生成纹理只负责表示 URL,不能证明 URL 仍有效或账户已授权。

    mici 的 QR 控件用反色纹理,析构时卸载;__init__() 立即生成纹理,失败时 _texture=None,_render() 改画错误标签。显示缩放只按矩形高度计算,不按宽度取较小比例:若插件给非方形且宽度更窄的矩形,二维码可能横向溢出(当前控件默认建为正方形,配对页还会把它设为屏幕高度的正方形,因此这是组件复用边界而非已实测故障)。

    未配对时 PrimeScroller.__init__() 先为 QR 调用一次 _get_pairing_url() 并生成纹理,但 _last_pairing_qr_generation 初始化为负无穷,首次 _render() 会再次请求令牌、释放初始纹理并生成新纹理,之后按 300 秒周期刷新;这意味着首次展示有两份请求/纹理工作,并可能在两个请求之间取到不同令牌。刷新的时间戳即使 _generate_qr_code() 失败也会更新,错误标签将持续到下一次刷新或重建。若令牌 API 抛异常,_get_pairing_url() 返回 https://connect.comma.ai/?pair=;该字符串仍可成功编码为二维码,故有 QR 图形并不证明配对令牌有效。

    图像到文本的解码链

    顶层 decode() 先定位 finder,再尝试估计版本及相邻两个版本、是否使用 alignment、矩阵是否转置;候选均失败返回 None。它仅捕获 QRError,并非对任意形状、任意 dtype 或超大数组都提供「失败返回 None」的保证。二值图与游程/透视运算会分配与输入像素数相关的内存,外部插件传入图像前必须限制形状、大小与处理预算。

    阶段 关键函数与限制
    灰度二值化 _binarize() 分块取样、算局部对比度并参考邻块/全局阈值;边长小于 21 像素抛 QRError,假定二维灰度数组。
    finder 定位 _find_patterns() 以 1:1:3:1:1 横向候选、纵向复查去重,至少约 2 像素/模块;_pick_finders() 从前十个候选中选几何最接近直角等边的三点。
    透视采样 _perspective() 解单应矩阵;_sample() 可用 alignment pattern 修正右下角,采样越出图像抛 QRError。
    矩阵恢复 decode_matrix() 只接受 21..177 且边长满足 4v+17 的方阵;从两份格式位选汉明距离不超过 3 的级别/mask,再反掩码、去交织。
    纠错与文本 _rs_correct() 做 syndrome、Berlekamp-Massey、错误定位/幅值求解并复验;无 ECI 的 byte 段先试 UTF-8,失败退回 Latin-1。

    eSIM 扫码调用与信任边界

    QRScannerDialog 使用座舱 CabinCameraView:显示时打开驾驶员视角并关闭红外,隐藏时反向恢复;它最多约每 0.25 秒启动一次扫描,上一扫描线程仍活跃时不再启动新线程。取帧时只复制 NV12 的 Y 平面有效宽度,避免 VisionIPC 缓冲回收后后台线程读到复用数据,随后在后台调用 qrcode.decode(gray)。扫码 UI 的轮询与相机/线程生命周期不等于二维码算法内置超时。

    识别到文本后,UI 先用 parse_lpa_activation_code() 检查 LPA: 前缀与三个非空 $ 分段,失败展示短时错误;通过则让用户输入昵称,再交给蜂窝管理器下载 profile。这个前置检查只是字符串形状校验,不认证 SM-DP+ 服务器、激活码归属或 eUICC 安装授权。插件化时应把「二维码像素识别」「业务数据格式校验」「服务端授权」分为三层:配对展示插件只接收短时令牌的受控呈现能力,扫码插件只输出有长度上限的候选字符串,不直接触发下载或持有 eUICC/网络权限。

    已有测试与待验证项

    test_qrcode.py 使用独立生成、打包的 .npz 矩阵 fixture,覆盖 1 到 40 版本、四种纠错级别、多种文本模式/ECI、噪声、旋转、镜像、透视、边缘贴码与无二维码图像,还检查自有编码器版本 1 到 20 的矩阵往返。通过隔离脚本执行原测试文件的 13 个测试方法全部通过,其中版本/纠错等级循环含 160 组矩阵与渲染图检查,纹理编码往返含 20 个版本;隔离器仅提供 pyray.Texture 的导入期类型桩。

    但该测试不覆盖 make_texture() 的 raylib 上传,也不验证真实车载相机曝光/畸变、GPU 纹理生命周期、恶意超大图像或 eSIM 服务端授权。新显示硬件需在重复开闭、令牌失败、图形上下文销毁/重建与非方形布局下检查 GPU 纹理数量与 QR 可扫性。

    常见问题

    屏幕上有配对二维码就说明令牌有效吗?

    不能。令牌 API 抛异常时 _get_pairing_url() 返回 https://connect.comma.ai/?pair=,这个空令牌 URL 仍可成功编码成二维码。生成纹理只负责表示 URL,不证明令牌有效或账户已授权。

    为什么首次进入配对页会出现两次令牌请求?

    因为 PrimeScroller.__init__() 已为 QR 调用过一次 _get_pairing_url() 并生成纹理,而 _last_pairing_qr_generation 初始化为负无穷,首次 _render() 会再次请求令牌、释放初始纹理并生成新纹理;两个请求之间可能取到不同令牌。

    扫码识别到 LPA 激活码就等于能装成功吗?

    不等于。parse_lpa_activation_code() 只检查 LPA: 前缀与三个非空 $ 分段,是字符串形状校验,不认证 SM-DP+ 服务器、激活码归属或 eUICC 安装授权。

    编码端和解码端的版本范围一样吗?

    不一样。编码端只覆盖版本 1 到 20、固定纠错等级 L 与 mask 0;解码端支持版本 1 到 40 与四个纠错等级,并会尝试相邻版本、alignment 与转置等候选。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/QR_CODE_PAIRING_ESIM_PIPELINE_DEEP_DIVE.md(编码/解码两端能力、配对 QR 展示边界与 eSIM 扫码信任边界)。
    • 项目 02 —— openpilot-research/ 同目录探针记录:原二维码测试文件的 13 个隔离测试方法与 160 组矩阵检查结论。

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

    更新记录

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