标签: openpilot

  • Cabana CAN 分析:五种输入源、MessageId 严格解析与保存陷阱

    结论:Cabana 是一个明确的只读采集与分析工具:它把路段回放、本地/设备 msgq、ZMQ 桥接、USB Panda、Linux SocketCAN 五种输入汇入同一套 CAN 事件与消息状态模型,并在这之上做 DBC 编辑与 CSV 导出。MessageId 严格解析为「总线:十六进制地址」,不能只用地址当唯一键。要特别记住一个保存陷阱:saveFile() 没有检查 DBCFile::save() 的布尔返回值就把撤销栈标记为 clean 并显示「File saved」,因此磁盘写入失败时 UI 状态不能当作保存成功证据。此外,界面能解码不等于允许发送帧——DBC 文件不等于固件安全策略。

    五种输入源汇入同一事件层

    main() 解析 CLI 后按 --msgq、--zmq、--panda、Linux --socketcan、路段回放的顺序选择一个输入源;路段加载被延后到窗口出现后执行,避免网络文件列表阻塞首屏。各源最终进入 AbstractStream 的 CAN 事件和消息状态模型。下表是各输入组件的关键行为与限制。

    输入组件 关键函数 源码行为与限制
    路段回放 ReplayStream::loadRoute()、mergeSegments() 用 Replay 加载 can、视频索引与 carParams;回放线程解析段内 CAN,主线程插入排序索引。鉴权、网络、文件缺失有不同错误文案。
    本地 / 设备消息 DeviceStream::start()、streamThread() 本地从 can msgq 订阅;指定 ZMQ 地址时先 fork/exec 桥进程并用 can 过滤,失败经 self-pipe 报错,析构时终止桥进程。
    Panda PandaStream::connect()、streamThread() 连接后设置 NO_OUTPUT 安全模式,配置三个总线速率及适用设备的 CAN-FD 数据速率;循环接收 CAN、发 heartbeat(false),断线尝试重连。
    SocketCAN SocketCanStream::connect()、streamThread() Linux RAW CAN socket 绑定指定接口、尝试启用 CAN-FD,读取帧后映射到 src=0 的 cereal CAN。当前此流只读。
    实时共用层 LiveStream::handleEvent()、updateLastMessages() 可按一分钟目录写本地 rlog;非主线程收帧,主线程合并、节流刷新并支持暂停/回看。是否落盘取决于设置,不应默认视为无数据留存。

    需要提醒两点:Panda 的连接与速率设置仍是硬件操作,不能当作纯软件只读模拟;SocketCAN 的 src=0 映射也不适合直接表达多总线车辆拓扑。

    MessageId 是「总线:地址」,不能用地址当唯一键

    AbstractStream::newEvent() 从 cereal CanData 复制时间、地址、来源和字节;mergeEvents() 按 (source, address) 建立每消息索引,并维护总时间序列。MessageId 严格解析「总线:十六进制地址」,不能只用地址当唯一键。updateLastMessages() 把后台新数据合并到可供 UI 消费的最后消息状态,seek 则按时间重建消息快照。

    这里还有一个容易被当成结论的提示:CanData::compute() 的颜色与翻转频率只是观察和提示信号,不构成 DBC 正确性证明。它能帮你发现可疑的字节变化,但不能替代逐车型的 DBC 校验。

    边界 源码事实 正确理解
    消息键 MessageId 严格解析「总线:地址」 地址可跨总线重复,单用地址会串帧
    字节频率 CanData::compute() 给颜色与翻转频率 是观察提示,不是 DBC 正确性证明
    SocketCAN 来源 读取帧后映射到 src=0 不适合直接表达多总线车辆拓扑
    实时落盘 可按一分钟目录写本地 rlog 是否落盘取决于设置,不应默认无留存

    DBC 编辑:UI 状态不是保存成功证据

    DBCFile::parse() 解析 BO_ 消息、SG_ 信号、VAL_ 枚举和 CM_ 注释,保留首个识别语句之前的头部;遇到重复地址或重复信号会报错,异常附带行号。DBCManager::open() 把文件关联到一个或多个来源总线,同一个 DBC 可共享给多个总线;没有专属文件时 findDBCFile() 回退全来源 DBC。因此源总线与文件关联影响解码结果,不能只按车型名称自动推定。

    编辑路径同样要注意状态与磁盘的分离:DBCManager::addSignal() 等编辑接口发出变化通知;updateMasks() 在「抑制已定义信号」开启时按 DBC 掩码清理高亮位计数;UndoStack::push() 立即执行命令并维护撤销/重做,只有 saveFiles() 才尝试写文件。最关键的失败模式是 saveFile() 不检查 DBCFile::save() 的布尔返回值就把撤销栈标记 clean 并显示「File saved」——磁盘写入失败时,界面提示不能作为保存成功的证据。

    导出与安全边界:只读采集不是控制许可

    utils::exportToCSV() 导出时间、地址、总线与十六进制原始数据;exportSignalsToCSV() 按当前 DBC 对选中消息解码信号值。导出的是观察数据和当前解释版本,时间戳被格式化为秒的小数三位;用于控制验证时应保留原始日志、DBC 版本与更高精度时间源。

    面向自制硬件与插件化,源文档给出三条要求:第一,把 Cabana 当成只读采集与分析插件,接入自制硬件时分别记录物理总线、接口速率、时间戳来源、丢帧与 CAN-FD 能力,保留原始帧以供回放;第二,DBC 编辑器产物必须经过版本管理、逐车型测试和 opendbc/panda 独立安全策略审查,「界面能解码」不等于允许发送帧;第三,直播桥与本地 rlog 涉及车内数据外传/留存,应有明确的用户授权、加密、存储期限和网络访问边界,开发工具不应共享车辆控制进程的写权限。

    常见问题

    Cabana 能直接当成车辆控制工具吗?

    不能。源文档把它定位为协议分析工具,明确「不是车型安全许可或控制执行器」。Panda 输入路径虽设置 NO_OUTPUT 安全模式,SocketCAN 流也只读,但连接与速率设置仍是硬件操作;「界面能解码」不等于允许发送帧。

    为什么 MessageId 不能只用地址?

    因为 MessageId 严格解析「总线:十六进制地址」,mergeEvents() 按 (source, address) 建立每消息索引。同一个地址可能出现在不同总线上,只用地址当唯一键会导致串帧,也无法正确表达多总线拓扑。

    为什么点了「File saved」仍可能没保存成功?

    因为 saveFile() 没有检查 DBCFile::save() 的布尔返回值,就把撤销栈标记为 clean 并显示「File saved」。源文档明确指出磁盘写入失败时,UI 状态不能当作保存成功证据,需要另行核对磁盘文件内容。

    字节变化热图能否证明 DBC 解析正确?

    不能。CanData::compute() 的颜色和翻转频率是观察与提示信号,源文档明确它不构成 DBC 正确性证明。发现可疑字节后仍需按车型核对 DBC 版本与解码结果。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CABANA_CAN_ANALYSIS_DEEP_DIVE.md(取用了五种输入源、MessageId 严格解析、DBC 编辑与保存失败模式、CSV 导出与只读安全边界)。

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

    更新记录

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

  • MetaDrive 世界进程:20 Hz 物理步进与 100 Hz 状态转发

    结论:MetaDrive 世界进程每圈(100 Hz 节拍)都从 env.vehicle 读状态并发给父进程,但只有 rk.frame % 5 == 0 才调用 env.step(),所以名义物理/图像步进约 20 Hz,状态管道最多在同一物理状态上重复发约五次,并不代表 100 Hz 独立物理观测;帧号和物理时戳没有随管道消息一起发送。父进程控制归零的判定 time.monotonic() - reset_time <= 2 也不是「重置后两秒窗口」,因为 reset_time 初始化为 0 且源码中没有后续赋值。以下为固定快照 c8fb906 的静态分析与假依赖探针,未安装 MetaDrive/Panda3D,未做物理仿真测试。

    进程与物理时间结构

    spawn_world() 为路面相机和可选广角相机配置同一 W×H 画面,关闭车内置渲染、交通密度和多数终止条件,并设置 physics_world_step_size=TICKS_PER_FRAME/100=0.05 秒、decision_repeat=1。MetaDriveWorld.__init__() 创建共享 RGB 数组、控制/仿真状态/车辆状态三条 Pipe 与事件,再启动子进程;父进程随后无超时地等待首条车辆状态,才把状态队列从 starting 推到 started。子进程初始化失败或卡在首次发送前,启动缺少明确 deadline。

    子进程 apply_metadrive_patches() 在进程内替换 EngineCore.add_image_sensor、ImageObservation.observe 和可选的到达目的地判断。它们是模块/类级 monkeypatch,没有在函数末尾恢复;这适合隔离的一次性仿真子进程,不宜嵌入可同时运行多实例的插件宿主。

    量 取值 说明
    主循环节拍 Ratekeeper(100, None) 每圈读车辆状态并发送
    物理 / 图像步进 每 5 圈一次,约 20 Hz 仅 rk.frame % 5 == 0 时 env.step() 并写 RGB
    physics_world_step_size 0.05 秒 由 TICKS_PER_FRAME/100 得到
    decision_repeat 1 —
    状态管道重复 同一物理状态最多约 5 次 帧号与物理时戳未随消息发送

    控制输入、重置与状态归一化

    父进程 apply_controls() 仅在 time.monotonic() - reset_time <= 2 时把控制置零;但 reset_time 初始化为 0,源码中没有后续赋值,reset() 只将 should_reset 置真。因此这不是重置后两秒的归零窗口:单调时钟已超过 2 秒时,重置后下一条命令仍携带当下转角/油门,同时只附带一次重置标志。两项原函数假时钟探针确认该边界和严格 >2 判定。

    子进程 controls_recv.poll()/recv() 在每圈 drain 到最后一条命令,以 steer_angle/(MAX_STEERING*8) 限幅到 [-1,1],再作为环境动作。控制输入是「最近值」,而不是逐条执行或有序回放。探针给同一轮预置 [转角, 油门, True] 后紧接 [..., False],确认只执行初始化时的第一次 env.reset(),没有执行请求的第二次重置;反转布尔顺序时则执行第二次重置。父类还存有 self.steer_ratio=15,但当前控制映射使用子进程局部值 8,不能把它当成已生效的同一参数。

    父进程 read_state() drain 状态管道,收到 done 才设退出事件;read_sensors() drain 车辆管道,把最新速度/角度/位置赋给 SimulatorState,用简化平面投影算 GPS,并在第一次观察到已使能时通知子进程开始计时。三项假管道/时钟探针确认收到首条车辆消息后 state.valid=True,随后即使假管道停发且时间推进到 100 秒,函数也不清除此位或首次使能时刻。

    终止判定与故障路径

    env.step() 分支每五圈检查环境 terminated;仅 test_run 才把越出当前车道或到达 test_duration 转成 done。子进程发送终止状态后未立即 break,实际退出依赖父进程读到 done、设置 exit_event 并令外层桥关闭。

    检查项 条件与边界
    终止检测 每五圈检查;仅 test_run 才把越出车道/到达 test_duration 转 done
    未移动检查 使能五秒后、每约 29 秒检查一次;distance_moved 是相对上次检查基线的位移累计,不是逐帧里程积分
    位移累计探针 连续两次相同 (2, 0) 位置、基线 (0, 0),累计量从 0 变成 4
    关闭 close() 设退出事件后无超时 join();SimulatorBridge.close() 先设相机线程退出标志再调用它
    阻塞风险 子进程若阻塞于 send()、环境调用或图像获取,单独设置事件不能保证有限时间退出;未见对 MetaDriveEnv 的显式 close()/finally

    这些是进程生命周期审计点,不是已复现的泄漏或死锁。闭环 test_driving() 被跳过,MetaDrive 适配测试还可因依赖缺失跳过。

    硬件与插件化要求

    世界进程适合做可替换的仿真后端插件,但其控制、状态、图像应有同一帧 ID、采样时戳和时钟域;对父子进程握手、心跳、队列/管道背压、关闭/强制终止设置 deadline。生产车辆适配器不能复用「最近控制值」或 state.valid 常真语义,而应由受信任核心按消息年龄、授权状态和硬件回执仲裁。仿真中的 20 Hz 物理步进与 100 Hz 状态转发,也必须在测试报告中分开标注。本专题没有证明 MetaDrive 动力学精度、相机图像一致性或实际闭环性能。

    常见问题

    状态按 100 Hz 发,就等于 100 Hz 物理观测吗?

    不等于。主循环每 5 圈才调用一次 env.step(),名义物理/图像步进约 20 Hz,状态管道最多在同一物理状态上重复发约五次;且帧号与物理时戳未随消息发送,父进程无法仅凭消息内容重建精确曝光时刻。

    重置后两秒内控制会归零吗?

    不是这样。apply_controls() 的 <= 2 判定使用 reset_time,而它初始化为 0 且源码无后续赋值,所以这不是「重置后两秒窗口」;单调时钟超过 2 秒时,重置后下一条命令仍携带当下转角/油门。

    排队的重置命令会被逐条执行吗?

    不会。controls_recv.poll()/recv() 每圈 drain 到最后一条,控制是「最近值」;探针显示预置 [转角, 油门, True] 后紧接 [..., False] 只执行初始化时的第一次 env.reset()。

    车辆状态会因失联而自动失效吗?

    不会。探针确认收到首条车辆消息后 state.valid=True,此后即使假管道停发、时间推进 100 秒也不清除该位;新硬件插件应显式设置消息年龄门。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/METADRIVE_WORLD_PROCESS_LIFECYCLE_DEEP_DIVE.md(世界进程的物理步进、管道状态、重置边界与退出路径)

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

    更新记录

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

  • 首次引导启动门:条款版本、培训版本与 DMS 进度三个独立契约

    结论:首次引导的启动资格由三个互相独立的契约拼成:条款版本、培训版本与设备启动条件。大屏与 mici 两套 UI 都只在窗口构造时读取一次布尔快照,训练 UI 完成不等于驾驶员监测或摄像头的实车验证;hardwared 真正看的是持久键里的版本值,而不是 UI 是否关闭。

    版本契约:terms_version 与 training_version

    源码中 terms_version="2" 与 training_version="0.2.0" 分别与 HasAcceptedTerms、CompletedTrainingVersion 比较;两键在参数定义中标记为持久化,条款键另有一段。两套 UI 均在窗口构造时读取一次布尔快照,未完成才由主布局推入导航栈。

    hardwared 把匹配条款和培训版本分别列入 accepted_terms、completed_training 启动条件,同时要求非 DoUninstall。只有 started_ts is None 时,启动条件才合入 should_start;已上路后版本变化不是这段代码的即时退场门。UI 是否关闭也不是启动条件,持久键的版本值才是跨进程依据。开发/回放夹具可直接写入这两键,因此不能把其存在视为用户真实完成培训的证明。

    这里有一个容易被忽略的顺序问题:两套 UI 的 completed 属性都是构造时快照,点击后只写 Params,并不重新赋这两个布尔值。因此「本次会话里 UI 已消失」与「hardwared 下次启动会读到版本匹配」是两件事——跨进程依据只能是持久键里的版本值,而不是界面状态。

    大屏函数链:图片热区与第九步

    函数/组件 行为 边界
    OnboardingWindow.__init__()、completed 构造时读条款/培训版本,决定先显示条款还是培训。 _accepted_terms、_training_done 是构造快照;点击后写 Params,但本实例不重新赋这两个布尔值。
    _on_terms_accepted()、_on_completed_training() 同步写对应版本值;如果培训早已完成,接受条款后直接退场。 这是软件版本确认,不验证摄像头、CAN 或 DMS 当前可用。
    TrainingGuide._load_image_paths()、_preload_thread() 从 assets/training/stepN.png 数字排序,首图立即加载,其余在线程解码并在 UI 线程上传纹理。 点击热区来自固定 STEP_RECTS;图片数/顺序变化必须同步热区与录音选择步号。
    TrainingGuide._handle_mouse_release() 命中本步热区才前进;第 9 步按额外矩形选择 RecordFront,末页可重看或完成并 pop_widget()。 这些是图片坐标热区,不是语义化控件;不同分辨率/素材改版需实屏验收。
    DeclinePage._on_uninstall_clicked() 写 DoUninstall=True 并请求关闭 UI。 真正卸载由 manager 观察参数、清理进程后调用硬件层卸载。

    mici 函数链:DMS 进度与缓存状态风险

    函数/组件 行为 边界
    OnboardingWindow.__init__()、show_event() 读取版本快照,创建条款/培训页面;显示时延长交互超时并提高驻车亮度。 hide_event() 恢复覆盖设置,但子页导航也可触发隐藏回调,生命周期需实屏确认。
    _on_terms_accepted() 写条款版本后无条件推入培训页。 与大屏不同:若旧培训版本已匹配而只需重签条款,mici 仍要求再次走培训 UI。
    TrainingGuidePreDMTutorial.show_event() 设置 IsDriverViewEnabled=True,使驻车时可运行座舱采集/监测链。 此参数是预览进程门,不是模型就绪回执;close() 会写回 false。
    TrainingGuideDMTutorial._update_state() 曾收到 driverMonitoringState 后,用检测到人脸且朝向中心的条件累积约 4 秒进度,达到阈值才启用继续按钮。 只检 recv_frame != 0,没有同时要求两条 DMS 消息本帧更新、存活、有效或年龄上限;一条曾合格的缓存状态在后续 UI 帧仍可能推动进度。
    TrainingGuideRecordFront 接受/拒绝各写 RecordFront,随后进入完成回调。 RecordFront 影响座舱录制配置,不能与 DMS 必需的短时预览混为同一授权。
    _on_completed_training()、close() 写培训版本、关 IsDriverViewEnabled、回调主布局退场。 退场与 Params 写入分步完成;窗口实例的 completed 属性仍是构造时快照。

    小屏的 DMS 教程读取左右驾驶位模型结果,并从 driverMonitoringState 读取策略与脸部状态;预览中的绿色图标还可被 set_force_active(True) 强制呈现,不能拿图标颜色证明教程输入有效。

    DMS 进度的静态推论

    TrainingGuideDMTutorial._update_state() 的进度累积条件值得单独强调:它只检查 recv_frame != 0,没有同时要求两条 DMS 消息本帧更新、存活、有效或年龄上限。因此一条曾合格的缓存状态在后续 UI 帧仍可能推动进度。这属于静态推论,源文档未做断流复现。

    换句话说,「培训完成」这一步记录的是用户在 UI 上走完了编号图片与约 4 秒的进度条,而不是 DMS 在真实车辆环境下的可用性结论。把前者当作后者,是引导流程最常被误读的地方。

    硬件与插件化要求

    第一,把「用户确认的条款版本」「培训完成记录」「摄像头/DMS 当前健康」「行车启动许可」分别存储和审计,不要让 UI 插件直接伪造版本键或持有设备卸载权限。第二,小屏 DMS 进度应绑定新鲜的成对消息、相机帧 ID、有效位和单调时间,丢帧/失联时冻结或回退,并在模型/摄像头/安装支架换版后重新验收。第三,RecordFront 是独立的隐私选择,硬件设计应分别证明短时座舱预览、可选录制和远端上传的权限及启停回执;对大屏图片热区和 mici 导航栈做不同尺寸、快速连点、掉电和中途退出测试。

    这三条要求背后是同一个判断:引导流程能证明的只是「用户在界面上完成了对应步骤」。它不能证明摄像头当前成像正常、DMS 模型在真实座舱光照下可用,也不能证明持久键没有被回放夹具写入。因此条款、培训、设备健康与行车许可必须各自留痕、各自可审计。

    常见问题

    条款和培训版本在哪里决定是否启动?

    hardwared 把匹配条款和培训版本分别列入 accepted_terms、completed_training 启动条件,并要求非 DoUninstall;只有 started_ts is None 时才合入 should_start。UI 是否关闭不是启动条件。

    大屏和 mici 在重签条款时的行为一致吗?

    不一致。mici 的 _on_terms_accepted() 写条款版本后无条件推入培训页;即使旧培训版本已经匹配而只需重签条款,mici 仍要求再次走培训 UI。大屏则在培训早已完成时于接受条款后直接退场。

    小屏 DMS 教程的进度条一定能代表监测可用吗?

    不能。TrainingGuideDMTutorial._update_state() 只检 recv_frame != 0,没有同时要求两条 DMS 消息本帧更新、存活、有效或年龄上限;一条曾合格的缓存状态在后续 UI 帧仍可能推动进度,源文档也未做断流复现。

    RecordFront 与 DMS 预览是同一个授权吗?

    不是。RecordFront 影响座舱录制配置,是独立的隐私选择,不能与 DMS 必需的短时预览混为同一授权;两者应有各自的权限与启停回执。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/ONBOARDING_TERMS_TRAINING_DMS_STARTUP_GATE_DEEP_DIVE.md(取用了版本契约、启动门、大屏与 mici 函数链及 DMS 进度边界)。
    • 项目 02 —— openpilot-research/DRIVER_MONITORING_POLICY_DEEP_DIVE.md(仅用于确认 DMS 状态与策略的消费边界)。

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

    更新记录

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

  • tinygrad 执行引擎:JIT 到四类后端的 Buffer 与同步边界

    结论:openpilot 的 modeld 走的是「捕获图一次链接、逐帧重放」的执行链,而不是每帧重新生成模型:lower_and_compile() 按编译键去重并把 SINK/PROGRAM 编译为程序,CapturedJit.linear 首次访问时 link_linear(..., allow_cache=False) 并把链接结果缓存。要替换推理硬件,不只是实现一个算子 API,还必须同时保持 Buffer 映射、命令提交、同步与编译产物契约。源文档列出四类已用后端(CPU:LLVM、METAL、QCOM、USB+AMD:LLVM),并明确后端类存在不等于当前机器能打开设备。

    从 JIT 到执行器:捕获图如何被补齐

    modeld.ModelState.__init__() 装入模型/warp 产物,对捕获图调用 lower_and_compile()。后者识别尚未完成的 SINK/PROGRAM,按编译键去重,编译为程序并替换进调用图。它不等同于每帧重新生成模型。

    _TinyJit.__call__() 在重放时比较输入名称与输入视图、变量、dtype、设备等签名;CapturedJit.linear 首次访问时调用 link_linear(..., allow_cache=False),把链接结果缓存在该捕获对象中。随后 CapturedJit.__call__() 替换需写入的输入,调用 run_linear(..., jit=True),返回捕获时的返回对象。

    run_linear() 的 jit=True 路径直接执行已链接的 LINEAR;普通路径先 compile_linear() 和 link_linear()。pm_exec 将 COPY、PROGRAM、graph、校验等调用分发到不同执行器;带 HCQInfo 的调用经 exec_hcq() 处理地址表、队列执行及必要同步。

    在这条 HCQ 路径里,hcq_compile() 对调用图进行缓冲参数替换、复制 staging、分批、命令编码和程序编译,hcq_link() 处理链接期缓冲与地址。源文档据此总结:推理硬件替换不仅是实现一个算子 API,还要保持 Buffer 映射、命令提交、同步和编译产物契约。

    内存与传输的四条边界

    边界 已核对实现 对新硬件的含义
    缓冲别名 Buffer.view() 保留 base/offset;allocate() 为视图取得底层存储并追踪已分配视图 输入切片与跨帧状态不能被当成独立存储;重置和释放顺序要保持别名关系
    输入复制 Buffer.copy_from() 构造 COPY 的 LINEAR 并调用 run_linear();exec_copy() 依设备能力选择传输 一次 copy_from() 是逻辑上传,不保证任何后端都只做一次物理 DMA 或没有同步
    外部图像指针 Tensor.from_blob() 用 external_ptr 分配但不接管原内存;QCOM 分配器进入用户内存映射 座舱图像指针必须在 GPU 使用完毕前有效;相机 ringbuffer 重用和重连需要台架验证
    回读 Buffer.as_memoryview() 对 host 可见存储先同步,否则经 PYTHON 缓冲复制 输出 .numpy() 是同步/传输边界,不能仅用 GPU kernel 耗时估计帧级延迟

    源文档把「外部图像指针」与「回读」特别分开:Tensor.from_blob() 用 external_ptr 分配但不接管原内存,QCOM 分配器进入用户内存映射,因此座舱图像指针必须在 GPU 使用完毕前保有效;相机 ringbuffer 重用与重连需要台架验证。

    四类设备后端与各自待验证项

    路径 关键入口及源码行为 尚需验证
    PC CPU:LLVM CPUDevice 选 HostAllocator、LLVM renderer 和 CPUProgram;程序可通过本机可执行内存调用 目标 CPU 指令集、编译器/LLVM 版本、数值和最坏时延
    macOS METAL MetalDevice 创建设备与命令队列;程序绑定缓冲并提交命令,wait 时检查错误 macOS/Metal 版本、虚拟化环境 graph 限制、异步错误和回读时延
    AGNOS QCOM QCOMDevice 打开 KGSL、创建上下文并校验 GPU 代际;_gpu_map() 处理外部 CPU 指针 SoC/KGSL 兼容性、相机内存映射、缓存一致性及异常后的输出门控
    Chestnut USB+AMD:LLVM AMDDevice 选接口与 renderer;USB 接口发现特定设备,经控制窗口和 VRAM 管理提交 USB 断连/复连、传输峰值、显存故障、超时回退及大小模型切换后的状态一致性

    Device.__getitem__() 懒加载设备,Compiled._select_renderer() 按 DEV 目标挑选 renderer;源文档明确后端类存在不等于当前机器能打开设备。Compiled.synchronize() 等待本设备 timeline 与跨设备 pending,并在超时/故障时进入 on_device_hang();这只是运行时故障检测,不是车辆安全退出策略。

    四类后端都通过 DEV 目标选择 renderer,但设备打开方式、命令提交与同步机制各不相同:CPU 走本机可执行内存,METAL 走命令缓冲,QCOM 走 KGSL 与外部映射,USB+AMD 走控制窗口与 VRAM 管理。源文档把外部指针失效、USB 断连、GPU 挂起与配置不匹配列为需要故障注入的对象,并明确这些结论来自对 modeld 实际调用路径的审阅,不代表 tinygrad 全部算子、调度优化或设备驱动均已审计。

    modeld 设置 AMD 等待超时并保留小模型回退。车辆控制仍必须由 selfdrived/panda 安全链约束,推理后端的替换不能改变这条约束。

    验证缺口与顺序

    现有测试入口有 JIT 单元测试、JIT 后端测试和 HCQ 测试,但本次未运行,不能替代 openpilot 实际模型及设备验证。源文档给出的顺序是:先补齐 LFS 与编译产物,在目标平台运行固定日志的道路/座舱模型数值对比;随后测量上传、warp、模型、回读和异常切换的分项与最坏时延;最后做外部指针失效、USB 断连、GPU 挂起和配置不匹配的故障注入。测试通过前不应把该后端视作可上车的插件。

    源文档还列出 modeld 对 AMD 的等待超时与小模型回退属于待验证项,其大小模型切换后的状态一致性要与 USB 断连/复连一起测试。

    常见问题

    每帧推理都会重新编译模型吗?

    不会。lower_and_compile() 识别尚未完成的 SINK/PROGRAM 并按编译键去重,编译一次后替换进调用图;CapturedJit.linear 首次访问时链接并把结果缓存到该捕获对象,重放时按签名校验后直接执行。

    一次 copy_from() 是否等于一次物理 DMA?

    不等同。源文档指出它是一次逻辑上传:Buffer.copy_from() 构造 COPY 的 LINEAR,exec_copy() 依设备能力选择传输、磁盘复制、共享 host 映射或 CPU 拷贝,不保证只做一次物理 DMA 或没有同步。

    输出 .numpy() 能用来估算帧级延迟吗?

    不能直接用来估算。Buffer.as_memoryview() 对 host 可见存储先同步,否则经 PYTHON 缓冲复制,源文档明确它是同步/传输边界,只能配合分项时延测量使用。

    后端类存在是否说明当前设备可用?

    不说明。Device.__getitem__() 懒加载设备、Compiled._select_renderer() 按目标挑 renderer,源文档明确后端类存在不等于当前机器能打开设备。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_ENGINE_BACKENDS_DEEP_DIVE.md(取用了 JIT 到执行器的调用链、内存与传输边界表、四类后端待验证项清单)。

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

    更新记录

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

  • V4LDecoder 重配:解码面所有权与资源生命周期

    结论:Qualcomm msm_vidc 解码路径(V4LDecoder)的核心风险不在解码本身,而在解码面的所有权与重配代际。它在固定节点上建立 8 个 OUTPUT 压缩包槽与 16 个 CAPTURE 图像面槽,把面交出去后依赖下游归还;一旦发生分辨率重配,源码没有外借面计数、epoch 或锁——若编码器仍持有旧面,旧指针仍落在同一数组槽位,可能对新一代面执行 QBUF。这是源码可推导的并发风险,实际可达性与驱动时序待设备测试。

    初始化与两套缓冲队列

    基线是本地 source/ 快照 c8fb906,仅据源码静态分析 Qualcomm 解码路径;没有在设备上运行 HEVC、UBWC、动态分辨率或故障注入。V4LDecoder::init() 打开设备、订阅事件、设压缩格式和 20 FPS,建立 OUTPUT/CAPTURE 队列;direct 用于 clip 并配置 UBWC 捕获,普通模式用于 Replay 的 NV12 拷贝。多数 ioctl 故障通过 safe_ioctl 异常退出,而非返回 false。

    函数/组件 作用 关键约束
    init() 打开设备、订阅事件、设压缩格式与 20 FPS、建立两队列。 多数 ioctl 故障经 safe_ioctl 异常退出,不是返回 false。
    setPlaneFormat() 按协商 sizeimage 分配 8 个输入包缓冲;为 16 个输出面按 sizeimage 与 Y/UV 尺寸较大值分配。 请求 REQBUFS 后没有检查驱动返回的实际缓冲数;输出面用协商 stride 初始化。
    queuePacket()、sendPacket() 找空闲 OUTPUT 槽,拷贝压缩包,带微秒 token QBUF。 没有空槽时返回 false;尺寸约束在 sendPacket() 为断言,调用方必须先做长度校验。
    pump() poll 后先耗尽端口事件、再耗尽已归还 OUTPUT,最后只取一个 CAPTURE 结果。 rc==0 表示本次无结果但返回 true,不代表已解出一帧。

    输入 OUTPUT 缓冲每个是独立 VisionBuf,out_buf_flag 在 QBUF 后置真、OUTPUT DQBUF 后清零。queuePacket() 仅扫描本地标志找空槽,不阻塞。

    数据循环与消费链

    handleCapture() 与 releaseFrame() 是所有权交接点:非空输出返还 VisionBuf* 与时间戳 token,消费者用完后重新 QBUF。直接模式的面交给编码器后,由其 OUTPUT 回收回调归还,所有权可能跨线程。

    驱动把 CAPTURE 时间戳按微秒还原为 token,clip 用它反推出源帧序号;这依赖驱动复制该 token,不能作为跨芯片默认保证。CAPTURE 队列中的面在返回 frame.buf 后不立刻重新提交:clip 选择跳过时立即 releaseFrame();送给 V4LEncoder 后由编码器 OUTPUT DQBUF 的 input_done_callback 调用 decoder.releaseFrame(),避免在编码器仍引用面时覆盖。

    Replay 的 V4LVideoDecoder 调用 decodeFrame(),复制后立即归还;其 open() 没检查 init() 的布尔返回。decodeFrame() 的 copyBuffer() 只按源面的 stride/height 拷贝,并不核对目标缓冲尺寸,需由调用方保证布局兼容。

    重配、故障与清理边界

    当驱动发 PORT_SETTINGS_CHANGED_INSUFFICIENT,handleEvent() 更新宽高、发 FLUSH_CAPTURE、设置 reconfigure_pending;此后 CAPTURE 包不作为新帧交出。收到 FLUSH_DONE 后 restartCapture() 对旧队列做 STREAMOFF/REQBUFS(0),逐个 free() 并在原位重建 VisionBuf,再设格式、分配和 QBUF。

    头文件允许 releaseFrame() 在消费线程调用,但这里没有外借面计数、epoch 或锁。若编码器仍持有旧面,在重配后才调用归还,旧指针仍落在同一数组槽位,可能对新一代面执行 QBUF,且与 pump() 的重建交错。

    另有几处资源边界:request_buffers() 不校验实际 count,而 handleCapture()/handleOutput() 直接按驱动返回 index 索引固定数组,若设备返回不同槽数或异常 index,没有显式范围保护;pump() 将 poll 超时视为正常空结果,clip 尾部仅在连续五次 1 秒空轮询后失败,但输入槽不足时使用 pump(-1),设备永不返回消息则无本地截止期;sendEOS() 不等待 EOS 确认。更关键的是 ~V4LDecoder() 只关闭 fd,VisionBuf 没有析构器,真正的 munmap/close 只在 VisionBuf::free() 中实现——本解码器对当前 OUTPUT 缓冲和最终 CAPTURE 缓冲没有显式 free(),按源码会保留这些映射和 FD 直至进程退出,重配时只释放旧 CAPTURE 面。

    新硬件插件验收

    解码插件应返回实际协商格式、stride、缓冲数量和帧尺寸,统一用 surface_id + generation 跟踪外借面;重配前等待归还或取消旧租约,禁止新旧代面复用时发生误归还。提交包、产出帧、EOS 和取消分别设置 deadline,并提供实际释放映射/FD 的可观测结果。

    验收维度 必须可观测
    格式协商 实际协商格式、stride、缓冲数量、帧尺寸
    面所有权 surface_id + generation、重配前归还/取消状态
    时限 提交包 / 产出帧 / EOS / 取消各自 deadline
    资源 实际释放的映射与 FD 数量

    台架回放至少覆盖多包一帧、帧重排、包长越界、动态分辨率、编码器延迟归还、设备无响应、失败重试与重复工作器的 FD/内存曲线;clip 的 20 FPS 名义时长不等同于每个输入段的真实帧数。现有测试主要覆盖 Athena 命令构造与硬编录制整链,并没有对解码器重配和资源所有权单独断言;本轮未运行任何设备测试。

    常见问题

    V4LDecoder 的分辨率重配为什么危险?

    因为 restartCapture() 在收到 FLUSH_DONE 后会原位重建 VisionBuf,但源码没有外借面计数、epoch 或锁。若编码器仍持有旧面、在重配后才归还,旧指针仍落在同一数组槽位,可能对新一代面执行 QBUF,并与 pump() 的重建交错。这是源码可推导的并发风险,实际驱动时序待设备测试。

    解码出的面什么时候可以重新提交?

    面在返回 frame.buf 后不立刻重新提交:clip 跳过时立即 releaseFrame();交给编码器后由编码器 OUTPUT DQBUF 的 input_done_callback 调用 decoder.releaseFrame(),避免在编码器仍引用面时覆盖。归还的触发点在消费方,所有权可能跨线程。

    pump() 返回 true 是否代表解出了一帧?

    不代表。pump() 的 rc==0 表示本次无结果但仍返回 true,源码明确这不代表已解出一帧;它 poll 后先耗尽端口事件、再耗尽已归还 OUTPUT,最后只取一个 CAPTURE 结果,需要一个完整循环才会推进一帧。

    为什么说解码器会保留映射和 FD?

    因为 ~V4LDecoder() 只关闭 fd,而 VisionBuf 没有析构器,真正的 munmap/close 只在 VisionBuf::free() 中实现。此解码器对当前 OUTPUT 缓冲和最终 CAPTURE 缓冲没有显式 free(),按源码会保留这些映射和 FD 直至进程退出;重配时只释放旧 CAPTURE 面。泄漏规模本轮未测。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/V4L_DECODER_RECONFIG_SURFACE_LIFECYCLE_DEEP_DIVE.md(取用了 V4LDecoder 函数索引、输入包/输出面消费链、重配与清理边界、插件验收清单)。
    • 项目 02 —— openpilot-research/V4L_ENCODER_BUFFER_EOS_OWNERSHIP_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

  • Tesla panda 安全模式:Autopark 优先与选择性转发矩阵

    结论:Tesla panda 安全模式的优先级不是「谁能发」而是「谁优先」:启动时先假定 Autopark/Summon 活跃并拒绝所有主机 Tesla 候选帧;原车 LKAS 锁存时把 0x488 从主机 TX 切回原车透传;原车 AEB 活跃时恢复 0x2B9 透传并拒绝主机取消帧。因此「白名单里有某地址」不等于该地址的每个字段都经过逐信号安全校验。

    RX 完整性:9 类监视帧与校验

    tesla_model3_y_rx_checks 含 9 类帧:相机总线 2 的 0x2B9/0x488,主总线 0 的 DI 速度、ESP 第二车速、EPAS 状态、油门、刹车、DI 巡航/Autopark 和 UI 警告。各自配置地址、长度、名义频率及计数上限;仅 ESP 轮速和刹车未忽略质量位。UI 警告进入通用 RX 健康监控,但 tesla_rx_hook() 并无对应的状态提取分支。

    共享 safety_rx_hook() 先验证校验/计数/质量位,再对命中且有效的帧运行车型钩子;坏 RX 帧不可当作有效的原车 AEB/LKAS 状态更新。校验按地址选择校验字节,计算地址两字节与其它载荷字节的 8 位和。

    函数 责任
    tesla_get_counter() 按地址从不同字节/位域取得 3 或 4 位计数器。
    tesla_get_checksum_byte()、tesla_get_checksum()、tesla_compute_checksum() 按地址选择校验字节,计算地址两字节与其它载荷字节的 8 位和。
    tesla_get_quality_flag_valid() 0x155 ESP 轮速质量位必须为 1;0x145 制动状态不能为未初始化或故障编码。
    tesla_rx_hook() 更新角度/接管、DI/ESP 双车速、踏板、巡航、Autopark、原车 AEB/LKAS 锁存。
    tesla_init() 选择默认或调试纵向 TX 表,重置原车辅助状态,并在启动时先假定 Autopark/Summon 活跃。

    原车状态锁存:Autopark、LKAS 与 AEB 的上升沿

    tesla_rx_hook() 的 DI 主车速与 0x155 ESP 第二车速按不同原始编码解码;后者经 speed_mismatch_check() 比较,差超过 2 m/s 时撤销控制许可。0x370 EPAS 角度进入采样窗口;高等级手握或特定高角速度故障置 steering_disengage,由共享接管逻辑消费。两速度帧及 EPAS 帧异步到达,静态代码不保证共时。

    0x286 的 Autopark 活跃编码包括 3、4、9;只有在原车巡航未接合的上升沿才锁存 tesla_autopark,状态退出时清除。随后的原车巡航状态须再与 !tesla_autopark 合取后送 pcm_cruise_check()。0x488 相机报文仅在自身 LKAS 状态上升沿且 controls_allowed=false 时锁存原车 LKAS;状态消失时清除。0x2B9 相机报文的 AEB 活动位则直接更新 tesla_stock_aeb。插件必须保留三种原车状态的来源、有效性和时间信息,不可只发布合并后的「辅助驾驶占用」布尔量。

    TX 与转发优先级矩阵

    条件 主机 TX 相机总线 2 到主总线 0
    初始化后尚未收到解除 Autopark 的 0x286,或锁存的 Autopark 活跃 tesla_tx_hook() 拒绝所有 Tesla 候选帧 车型动态钩子不阻断 0x488/0x27D/0x2B9,允许原车路径继续。
    普通非 Autopark、原车 LKAS 未锁存 0x488 可在角度/控制类型许可下发送 0x488 与 0x27D 被 tesla_fwd_hook() 阻断。
    原车 LKAS 已锁存 0x488 被车型 TX 钩子拒绝 0x488 透传给主总线。
    默认原车纵向 0x2B9 仅允许无执行量的 ACC_CANCEL_GENERIC_SILENT,且不得宣称 AEB 事件 0x2B9 一直透传;原车 AEB 活跃时主机取消帧也被拒绝。
    ALLOW_DEBUG 且纵向参数位置位 0x2B9 可按加速度限值发送,仍禁止主机 AEB 事件 无原车 AEB 时阻断原车 0x2B9;原车 AEB 活跃时恢复透传并拒绝主机 0x2B9。

    tesla_tx_hook() 对 0x488 只接受控制类型 NONE 或 ANGLE_CONTROL,并用车辆模型参数限制 0.1 度转角及横向动力学;原车 LKAS 锁存时即使数值合规仍拒绝。0x2B9 禁止主机设置任何 AEB 事件;默认模式仅可送加速度上下限均为中性 375 的取消编码。调试纵向模式的原始加速度范围为 288–425,中性为 375;两端都低于中性时额外拒绝,以防静止后倒车风险。

    白名单不等于逐信号校验:0x27D 的边界

    tesla_init() 的两个 TX 表都含 0x488、0x2B9、0x27D/总线 0。0x488 与 0x27D 保留 check_relay=true,但设 disable_static_blocking=true,因此共享 safety_fwd_hook() 不会先把它们恒定阻断,留给 Tesla 的动态钩子选择透传。默认模式的 0x2B9 check_relay=false;纵向模式则为 true 但仍关闭静态阻断。

    关键在于 0x27D 没有 Tesla TX 载荷分支:非 Autopark 时,它只经过通用地址/总线/长度、继电器状态和车型总阻断门,不能据此认定各字段经过逐信号安全校验。

    证据与新硬件边界

    test_tesla.py 包含校验/计数、速度失配、制动质量位、驾驶员接管、Autopark、原车 LKAS/AEB 透传、转角和倒车限制用例;其测试库默认加 ALLOW_DEBUG。新增的本机转发矩阵探针在严格编译和 UBSan 下分别运行 release/debug 变体,两者均通过:核对启动 Autopark 阻断、普通/原车 LKAS 转向透传,以及调试纵向和原车 AEB 的 0x2B9 转发切换。但它只设置隔离状态并检查通用 safety 函数,没有覆盖真实 RX 报文时序、目标编译器、板级继电器或 ECU 接受。

    自制硬件与插件化重构应把固件构建类型、Tesla 平台/转向控制字段代际、CAN 来源、车辆模型参数和安全参数版本绑定。感知/控制插件只能提供候选角度或加速度;Autopark、原车 LKAS/AEB 优先权、双车速、质量位与故障降级仍须由独立受信任安全层执行。

    常见问题

    为什么启动时会拒绝所有主机 Tesla 候选帧?

    因为 tesla_init() 在启动时先假定 Autopark/Summon 活跃,在收到解除 Autopark 的 0x286 之前,tesla_tx_hook() 会拒绝所有 Tesla 候选帧;Autopark 锁存活跃时同样如此。

    原车 LKAS 锁存后,0x488 还能由主机发送吗?

    不能。原车 LKAS 已锁存时 0x488 被车型 TX 钩子拒绝,主总线方向则改为把 0x488 透传给主总线。普通非 Autopark 且原车 LKAS 未锁存时,0x488 才可在角度/控制类型许可下发送。

    0x27D 在白名单里是否代表它经过逐信号校验?

    不代表。0x27D 没有 Tesla TX 载荷分支;非 Autopark 时它只经过通用地址/总线/长度、继电器状态和车型总阻断门,不能据此认定各字段经过逐信号安全校验。

    原车 AEB 活跃时 0x2B9 会怎样处理?

    原车 AEB 活跃时恢复 0x2B9 透传并拒绝主机 0x2B9;默认原车纵向模式下,主机侧 0x2B9 仅允许无执行量的 ACC_CANCEL_GENERIC_SILENT,且不得宣称 AEB 事件。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TESLA_PANDA_SAFETY_MODE_DEEP_DIVE.md(取用了 RX 监视帧、原车状态锁存、TX 与转发优先级矩阵及证据边界)。
    • 项目 02 —— openpilot-research/FORD_PANDA_SAFETY_MODE_DEEP_DIVE.md(仅用于对照品牌安全模式的通用分发结构)。

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

    更新记录

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

  • Chrysler 固件身份:0x22 F132 双偏移与转向能力前缀启发式

    结论:Chrysler 平台的身份链里,0x22 F132 这一个 DID 走的是两条响应路由:仪表/SRS/ABS/雷达/EPS 用 rx_offset=-0x280,ABS/混动/发动机/变速箱用默认 +0x8,发动机与变速箱还额外查 supplier 软件号。匹配分精确与通用 fuzzy 两级,fuzzy 明确排除 EPS/前视雷达并要求两个 ECU 地址上的独有版本指向同一车型,且 Chrysler 没有品牌自定义 fuzzy。转向能力不是规范化零件号解析,而是对 EPS 固件版本前 4 字节的前缀启发式:非 Ram 平台 >= b"6841" 抬高最低转向速度,Ram 1500 落在 b"68" < prefix <= b"6831" 时才可设为 0。识别、BSM、雷达解析与可发送控制是四种不同能力,必须分别声明与验证。

    十个平台是入口索引,不是适用性声明

    CAR 与 ChryslerCarSpecs 声明 10 个平台:Pacifica 4、Jeep 3、Ram 2、Durango 1,并给出 PT/雷达 DBC、质量、轴距与基础最低转向速度。Cherokee 使用 CUSW PT DBC,Ram 使用独立 DBC。FW_VERSIONS 对这 10 个平台逐一列出 ECU 地址与版本原始字节:发动机版本可含尾部空格,Ram HD 列了 engine 0x7e0 却没有 transmission 条目,该文件也没有旧式 FINGERPRINTS。

    这些声明不是物理线束、EPS 固件或执行权限的证明,也不代表已完成所有年款的报文与安全审计。版本字节必须按字节保存,不应剥除尾部空格或把车型名称当成版本匹配规则。

    FW_QUERY_CONFIG:一个 DID,两条响应路由

    查询配置在 bus 0 上组织了三组请求,最容易踩坑的是「同一个 ABS 类型出现在两组」——因此不能只按 ECU 类型推导响应地址,必须按请求组分别对应偏移。

    请求组 目标 ECU 偏移 / 结果
    0x22 F132 仪表 / SRS / ABS / 雷达 / EPS rx_offset=-0x280;如 EPS 0x75a 预期响应 0x4da
    0x22 F132 ABS / 混动 / 发动机 / 变速箱 默认 +0x8;如发动机 0x7e0 默认响应 0x7e8
    0x22 supplier 发动机 / 变速箱 额外查询 supplier ECU 软件号
    extra_ecus 混动 ABS 替代地址 0x7e4 加入查询集合,get_fw_versions() 标为 logging,build_fw_dict() 排除

    CHRYSLER_VERSION_REQUEST 为 0x22 F132,supplier 软件号也走 0x22,每组正响应以 0x62 加相同 DID 为前缀。这里的版本正则 [A-Z0-9_]{10} ? 只用于离线固件表的全匹配测试,不是运行时未知版本归一化器。混动 ABS 的替代地址应视为额外采集,不应成为自动选型证据。

    精确与模糊匹配:独有 ECU 数与冲突拒绝

    match_fw_to_car_exact() 对每个候选的已知 ECU/版本逐项检查:未见 essential ECU 或已见不匹配都可淘汰,非 essential 缺席可略过。要记住它返回的是集合,不保证唯一,没有实测固件时不能据此宣称车型已识别。

    通用 fuzzy(match_fw_to_car_fuzzy())排除 EPS、前视雷达等共享 ECU,要求两个不同 ECU 地址上的独有版本都指向同一车型。Chrysler 未配置品牌自定义 fuzzy,因此未知 EPS 版本不能单独作为 fuzzy 身份;两个 ECU 冲突时会拒绝候选,缺帧或共享件则可能导致无结果。

    能力 判定依据 边界
    精确匹配 候选的已知 ECU/版本逐项核对,essential 缺失或不匹配可淘汰 返回集合,不保证唯一
    通用 fuzzy 两个不同 ECU 地址上的独有版本指向同一车型 排除 EPS/前视雷达;Chrysler 无品牌自定义 fuzzy
    BSM 依据 bus 0 地址探测,而非固件型号 与车型识别是两件事
    雷达解析 统一 radarUnavailable=True 默认禁用,需另行启用

    转向能力:前缀启发式,不是零件号解析

    CarInterface._get_params() 用固件版本前 4 字节做前缀判断。非 Ram 平台:新平台或任意 EPS 版本前缀按字节序 >= b"6841" 时,置高最低转向速度标志;Ram 1500 的 EPS 前缀落在 b"68" < prefix <= b"6831" 时可设 minSteerSpeed=0;最后的高阈值标志把最低速度设为 17.5 m/s。

    这些判断是代码里的能力启发式,不是规范化零件号解析;CarParams 需要保留固件来源和未知状态。CarController.update() 用 minSteerSpeed 决定 LKAS 使能,平台分支决定关断迟滞并包含重新使能等待,扭矩在未激活或使能位关闭时归零。参数阈值会进入实时执行链,不能仅凭插件清单声明「支持低速转向」。

    四种能力必须分开声明

    Ram HD 与 CUSW 被标记 dashcamOnly;CUSW 选独立 safety model,Ram DT/HD 使用安全参数位;雷达统一 radarUnavailable=True;BSM 依据 bus 0 地址而非固件型号。源文档把这一点总结为一句要求:车型识别、BSM、雷达解析和可发送控制是四种不同能力,必须分别声明、验证和降级。对缺失或相互矛盾的证据应返回未知,高阈值与零速转向不应从车型说明或单一未验证响应推断。

    常见问题

    为什么 ABS 的响应地址不能只按 ECU 类型推导?

    因为同一个 DID 0x22 F132 在 bus 0 上有三组请求,同一个 ABS 类型会同时出现在 rx_offset=-0x280 组和默认 +0x8 组。源文档明确不能按 ECU 类型单独推导响应地址,必须按请求组对应偏移。

    EPS 版本能不能单独决定车型身份?

    不能。通用 fuzzy 明确排除 EPS、前视雷达等共享 ECU,要求两个不同 ECU 地址上的独有版本都指向同一车型,且 Chrysler 未配置品牌自定义 fuzzy。因此未知 EPS 版本不能单独作 fuzzy 身份。

    Ram 1500 的零速转向是怎么判定的?

    由 EPS 固件版本前 4 字节的前缀启发式判定:当 b"68" < prefix <= b"6831" 时可设 minSteerSpeed=0。这是代码中的能力启发式,不是规范化零件号解析,CarParams 需保留固件来源和未知状态。

    车型被识别出来,是否就能发送转向控制?

    不能等同。源文档强调车型识别、BSM、雷达解析和可发送控制是四种不同能力。Ram HD/CUSW 标记 dashcamOnly,雷达统一 radarUnavailable=True,而 minSteerSpeed 等阈值会进入实时执行链,不能仅凭插件清单声明支持低速转向。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CHRYSLER_FIRMWARE_QUERY_STEERING_CAPABILITY_IDENTITY_DEEP_DIVE.md(取用了 10 个平台固件库、0x22 F132 双偏移查询、精确与 fuzzy 匹配边界、转向能力前缀启发式及四类能力分层)。

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

    更新记录

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

  • Chestnut 告警锁存:告警消失不等于故障已清

    结论:Chestnut 的故障告警大多锁存到下一个行车周期边沿,不是按当前测量实时刷新:usb_failed、model_failed、power_lost、pcie_failed 都在 offroad→onroad 边沿才复位。因此「告警消失了」不等于「故障已清」,而 state is None(遥测缺失)这一轮既不更新供电/PCIe/温度,也不会自动清除原故障——没有新有效样本既不证明健康,也不必然生成新告警。

    本文基于固定源码快照追踪 ChestnutStatus.update() 的状态转换及其上下游,并以原类方法离线探针核对部分迁移。探针使用假 USB 快照、时钟和告警回调,未接真实桥接层、USB 或设备,不代表实板故障已复现。USB/GPU 原始遥测的有效位属于相邻专题。

    状态迁移与复位

    ChestnutStatus.update() 每次由 USB、模型和遥测输入更新锁存状态,再计算七类 offroad 告警。hardwared.hardware_thread() 把慢速 USB 快照、固件失败、Params 和存活且有效的遥测传入状态机;set_offroad_alert_if_changed() 以 (show_alert, extra_text) 去重,只在变化时写 Params。

    状态 置位/更新条件 清除条件与后果
    usb_seen 仅在 offroad→onroad 边沿取当时的 firmware_ok 下次行车边沿重取;若出发时未识别成功,之后出现再消失不会触发本轮 usb_failed
    usb_failed 行车中曾在边沿看见 USB,后来 firmware_ok 为假 下次行车边沿清除;置位后即使设备恢复也维持缺失告警,并压下 PCIe/供电锁存
    model_loading_seen / model_attempted 本次行车边沿后曾见加载为真,随后加载为假且 model_active 非 None 下次行车边沿清除;驻车阶段曾见加载会在切入行车时清空
    model_failed 行车中、已尝试且 model_active is False 下次行车边沿清除;显示时再受 missing 或 pcie_failed 抑制
    power_seen / power_lost 行车中有效遥测电压不低于 5000 mV;已尝试后供电故障或低于阈值 下次行车边沿清除;power_lost 锁存,power_restored 记录后续恢复用于改告警文本
    link_failures / pcie_failed 已尝试且有效遥测中 LTSSM 连续两次不为 0x78,或供电丢失 计数在下次 0x78 样本清零,故障位仍锁存至下次行车边沿;USB 失败会清故障位
    overheated 有效遥测 GPU 不低于 100°C 或显存不低于 95°C 已过热时降至各阈值减 5°C 以下才清除;行车边沿不重置该位

    usb_failed 优先级最高:源码会把若干供电/PCIe 锁存清掉,并用 missing 抑制模型错误显示;pcie_failed 也抑制模型错误,以免同一根因重复报错。锁存用于保留行车周期内已发生的事件,其恢复语义要与实时测量分开。

    缺测不等于正常:state is None 的语义

    hardwared 只在 chestnutState 同时 alive 且 valid 时传具体 state,否则传 None。state is None 时本轮不更新供电、PCIe 和温度状态,不会把原故障自动清除。离线探针确认:一次异常 LTSSM、一次 state=None、再一次异常 LTSSM 会累计到两次并锁存 pcie_failed;之后正常样本把计数清零,却不会在当前行车周期清除故障位。

    观测 源码行为 不能推出的结论
    本轮 state is None 不更新供电/PCIe/温度,也不清原故障 不能判定「故障已恢复」
    两次异常 LTSSM 后正常样本 计数清零,但故障位锁存至下次行车边沿 不能判定本周期内 PCIe 已健康
    USB 快照重复消费 快照来自较慢线程,可能被数次调用重复消费 不能按调用次数换算固定毫秒数

    两次异常不能简单换算成固定毫秒数:link_failures 按有效融合消息的消费次数计,不按 PCIe 原始采样时间计;USB 快照也来自较慢的线程,状态机可能在数次调用中重复消费同一枚举结果。

    告警条件与其边界

    Offroad_ChestnutNotDetected 有两条路径:本轮行车周期内 usb_failed 锁存,或处于 release 分支且驻车、进程启动超过 10 秒、运行态加 ROM 态候选数不等于一。这里的 10 秒是 ChestnutStatus 创建后的时间,不是每次进入驻车后重新计时。

    其余告警各有窄边界:Offroad_ChestnutUsbSlow 仅在驻车且恰好一个运行态设备时检查速度低于 5000 Mbps,并不校验产品字符串,sysfs 缺测读成零速时也符合该比较;Offroad_ChestnutBranch 是非 release 分支且恰好一个运行态设备的提示;Offroad_ChestnutUpdateFailed 只在驻车展示固件更新失败。

    Offroad_ChestnutPcieUnavailable 的文本按电源失去、恢复或纯链路失败选择:首次失电前是否曾达到 5 V 决定「连接断开」还是「运行中丢电」文案;恢复后仍要求重新点火,而不是在本周期自动解除锁存。温度告警可在驻车和行车更新,其附加文字只展示 GPU 温度,即使触发源实际上是显存温度。以上均为代码显示语义,不等于硬件电气诊断结论。

    自研硬件与插件化约束

    硬件诊断插件应输出独立的 observation 与 latched_fault:每条观测携带设备身份、采样时间、有效位和新鲜度;故障事件携带首次发生时刻、恢复时刻及重置原因。告警展示由宿主统一仲裁,避免插件自己清除更高优先级的安全故障。

    1. 车辆控制安全链不应依据这些展示用 offroad 告警直接授权执行;外置计算丢失时仍须经模型有效位、控制状态机和独立网关处理。
    2. 台架需分别注入:启动边沿 USB 缺失后再恢复/断开、运行中 USB 消失后恢复、连续与间隔的 LTSSM 错误、供电跌落后恢复、模型加载中/失败/回退。
    3. 还须覆盖温度在 5°C 迟滞带附近抖动、遥测失效和慢速 USB 快照;比较故障锁存、告警可见性与实际控制退出。
    4. 不得把告警消失当作故障已清。

    此仓库没有 ChestnutStatus 的专门单测;五项原类方法离线探针核对驻车加载历史在行车边沿清空、USB 消失优先级、遥测缺失时的 PCIe 计数、温度迟滞和 release 缺失计时。

    常见问题

    告警消失是不是说明故障已经清除?

    不一定。usb_failed、model_failed、power_lost、pcie_failed 都锁存到下一个 offroad→onroad 边沿才复位,用于保留行车周期内已发生的事件;其恢复语义与实时测量是两回事,不能把告警消失当作故障已清。

    state 为 None 会清掉原故障吗?

    不会。hardwared 只在 chestnutState 同时 alive 且 valid 时传具体 state,否则传 None;本轮不更新供电、PCIe 和温度状态,也不会自动清除原故障。没有新有效样本既不证明健康,也不必然生成新告警。

    两次 LTSSM 异常等于固定毫秒数吗?

    不等于。源码把 link_failures 按有效融合消息的消费次数计,不按 PCIe 原始采样时间计;USB 快照来自较慢线程,可能被数次调用重复消费同一枚举结果,所以两次异常不能简单换算成固定毫秒数。

    温度告警文字为什么只显示 GPU 温度?

    因为显示语义就是这样:温度告警的附加文字只展示 GPU 温度,即使触发源实际上是显存温度。过热位在有效遥测 GPU 不低于 100°C 或显存不低于 95°C 时置位,已过热时降至各阈值减 5°C 以下才清除,行车边沿不重置。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CHESTNUT_STATUS_ALERT_LATCH_LIFECYCLE_DEEP_DIVE.md(取用了 ChestnutStatus.update() 状态迁移表、缺测语义、告警条件与五项离线探针结论)。
    • 项目 02 —— openpilot-research/CHESTNUT_TELEMETRY_VALIDITY_CONTRACT_DEEP_DIVE.md(仅用于确认遥测有效位边界,未取用其未公开内容)。

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

    更新记录

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

  • 大众 MLB/PQ 构帧器:返回类型不匹配与固件白名单

    结论:大众 MLB 与 PQ 共用同一个 CarController 和一套「统一 CCS 插件接口」,但两者的纵向实现并不等价。静态源码里有两处硬边界:PQ 纵向助手返回帧列表(与 can_sends.extend() 一致),而 MLB 助手返回 CANPacker.make_can_msg() 的 (address, bytes, bus) 单帧三元组,extend() 会把三个成员分别加入列表——这是源码可见的返回类型不匹配;同时 MLB 固件安全配置只允许 HCA_01、LDW_02、两总线的 LS_01,没有 ACC_05/ACC_02。统一接口不能掩盖这些差异。

    共享控制器与两套构帧器

    基线是 openpilot 固定快照 c8fb906,覆盖 mlbcan.py 与 pqcan.py 的全部实现函数,并追踪共享车型控制器、接口配置、DBC packer 与 panda safety。仅做静态分析:未发 CAN、未改 ECU、未跑实车。共享控制器 CarController.update() 先限扭矩、做重复值缓解,再以非零扭矩作为 HCA 使能值。

    功能 MLB 实现 PQ 实现
    转向 create_steering_control() 输出 HCA_01,状态 7/3 与 lkas_enabled 对应,扭矩拆绝对值和符号。 create_steering_control() 输出 HCA_1,仅在 enabled 且扭矩非零时状态 7,否则 3。
    LDW / HUD create_lka_hud_control() 直接委托 MQB 构帧。 构造 LDW_Status,可复制五个原车警示字段,再覆盖车道线/告警/转向提示。
    巡航按钮 复制 LS_01 四个配置字段,原车 COUNTER+1 mod 16,覆写 cancel/resume。 对 GRA_Neu 作同类处理。
    纵向状态 acc_control_value() 与 acc_hud_status_value() 均固定返回零。 acc_control_value() 按活动/主开关返回 1/2/0;acc_hud_status_value() 另将故障置 6。

    按钮帧由 pcmCruise、原车 COUNTER 变化与 cancel/resume 请求共同决定发送,总线由 CAN.ext 选择;LDW/HUD 按平台 LDW_STEP 发送,输入来自 CarState 的原车缓存。纵向状态分支只在 openpilotLongitudinalControl 为真时走自控纵向及其 HUD 分支。

    需要说明的是,「什么时候发」和「发什么内容」由不同东西决定:LDW/HUD 依平台的 LDW_STEP 节拍发送,纵向 HUD 依 ACC_HUD_STEP 把单个返回值 append 到发送列表,而内容取自 CarState 缓存的原车配置与计数。节拍常量与原车缓存只保证本机按序产出候选帧,并不等于车辆已接受该帧。

    纵向路径与返回类型不匹配

    加速度与纵向 HUD 构帧是差异最集中的地方。MLB 的 create_acc_accel_control() 用空字段构造单个 ACC_05 三元组,create_acc_hud_control() 用空字段构造 ACC_02;PQ 的 create_acc_accel_control() 构造 ACC_System 并返回单元素列表(关闭时加速度字段用 3.01),create_acc_hud_control() 构造 ACC_GRA_Anzeige,包含状态、目标车速和距离。

    控制器统一调用 can_sends.extend(...),但两种助手的返回类型并不一致:PQ 返回 list[frame],与 extend() 语义一致;MLB 返回 CANPacker.make_can_msg() 的 (address, bytes, bus) 单帧三元组,extend() 会把三个成员分别加入 can_sends,不再是帧列表。这是源码可见的返回类型不匹配,MLB 自控纵向不能据此视为可工作。

    这类差异不会在简单调用里立刻显形:PQ 返回单元素列表,可直接被 extend() 展开为帧;MLB 返回的是 make_can_msg() 的三元组,被 extend() 展开后会把地址、字节和总线各自当成一个元素,破坏原本的帧列表结构。只有真正进入发送链路,这种结构错位才会以异常或错误帧的形式暴露。

    校验与固件白名单

    MLB 的 volkswagen_mlb_checksum() 对十个指定地址使用不同 XOR 初值,其他地址回退 MQB/MEB CRC;PQ 文件没有独立校验函数,由其 DBC/packer 负责已声明的计数和校验字段。CANPacker.pack() 按 DBC signal 类型选回调并填入校验,但它不替代 panda RX/TX 安全检查。

    换句话说,校验只回答「这段字节是否按 DBC 约定自洽」,不回答「这条帧是否应该被允许发出」。后者由固件白名单与安全模式独立裁定,两者不能互相代替。

    可达性边界 源码可见事实
    接口能力 接口对 PQ/MLB 都可在 alpha_long 置位时设置 openpilotLongitudinalControl,但两者纵向实现不等价。
    返回类型 MLB 纵向助手返回单帧三元组,被 extend() 拆成三个成员;PQ 返回帧列表。
    固件白名单 MLB 仅允许 HCA_01、LDW_02 与两总线的 LS_01,无 ACC_05/ACC_02。
    构建门 MLB/PQ 两个 safety 模式都仅在 ALLOW_DEBUG 下注册,发布构建不能因识别到平台便假定可启用。
    数据质量 按钮门的当前实现只比较 COUNTER 是否变化,未同时校验该帧接收时间或原车数据质量。

    PQ 的 ACC_System 虽然存在明确的活动/非活动加速度编码、固件也有单独的纵向配置与加速度限幅,但这仅说明源码中存在对应路径,不等于车辆已验证。按钮帧引用 CarState 缓存的原车配置及计数,接线/插件层需保留 bus、时间戳与报文健康,不能只传一个 cancel 布尔量。

    新硬件的验收要点

    新硬件的验收应分别列出 MLB/PQ 车型身份、网络位置、线束、DBC、固件 safety 配置、计数/校验、失联数据新鲜度和拒帧行为。尤其不能让「统一 CCS 插件接口」掩盖 MLB 的占位纵向实现或返回类型差异。

    验收项 MLB 关注点 PQ 关注点
    纵向可发性 返回类型须先修正,且固件白名单不含 ACC 帧。 返回列表与 extend 一致,但仍需实车验证。
    安全模式 仅在 ALLOW_DEBUG 注册。 仅在 ALLOW_DEBUG 注册。
    计数/校验 十地址 XOR 初值 + MQB/MEB 回退。 由 DBC/packer 声明字段负责。
    数据新鲜度 按钮门不校验接收时间。 按钮门不校验接收时间。

    本机隔离探针未完成:系统 Python 缺 numpy,工作区 Python 缺 capnp;本篇没有运行 DBC 字节回放、目标固件或实车试验。归档源码未编辑。

    常见问题

    为什么大众 MLB 的自控纵向「看起来有实现」却不能用?

    因为源码里 MLB 的纵向助手返回的是 CANPacker.make_can_msg() 的 (address, bytes, bus) 单帧三元组,而控制器用 can_sends.extend() 追加,会把三个成员拆开,不再是帧列表;同时 MLB 固件安全配置只允许 HCA_01、LDW_02 与两总线的 LS_01,没有 ACC_05/ACC_02。返回类型不匹配与白名单缺口是两处独立证据。

    PQ 和 MLB 的转向构帧有什么差别?

    MLB 的 create_steering_control() 输出 HCA_01,状态 7/3 与 lkas_enabled 对应,扭矩拆成绝对值和符号;PQ 输出 HCA_1,仅在 enabled 且扭矩非零时状态取 7,否则取 3。两者都经共享控制器先限扭矩、做重复值缓解。

    两个平台都设了 openpilotLongitudinalControl 就等价吗?

    不等价。接口对 PQ/MLB 都可在 alpha_long 置位时设置 openpilotLongitudinalControl,但两者的纵向实现(返回类型、固件白名单、状态编码)都不同;且 MLB/PQ 两个 safety 模式都仅在 ALLOW_DEBUG 下注册,发布构建不能因识别到平台就假定可启用。

    按钮帧只比较 COUNTER 变化够吗?

    源码中该门只比较 COUNTER 是否变化,没有在本地控制器同时校验该帧的接收时间或原车数据质量。接线/插件层因此需保留 bus、时间戳与报文健康,不能只传一个 cancel 布尔量。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/VW_MLB_PQ_CAN_BUILDER_CONTRACT_DEEP_DIVE.md(取用了 MLB/PQ 构帧函数矩阵、纵向返回类型不匹配、MLB 校验与固件白名单、验收要点)。
    • 项目 02 —— openpilot-research/VOLKSWAGEN_MLB_PANDA_SAFETY_DEEP_DIVE.md(仅用于确认 MLB safety 语境的接续关系,未取用其未公开内容)。

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

    更新记录

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