标签: openpilot

  • ECU 探测与 ISO-TP:在场、固件候选与可执行是三件事

    结论:诊断链上的三个结论必须严格分开——「探测到响应」「解析到固件」「车型可执行」是三件不同的事。tester present 收到单帧响应只说明该地址在场,返回固件候选只说明查询有结果,二者都不构成车型可执行的许可。此外 get_ecu_addrs() 的 timeout 是循环条件而非硬墙钟上界,0x78 的 pending 判断只看第三个字节、是宽松的超时延长条件而非已认证的 UDS pending 状态。本文为固定源码快照 c8fb906 的静态阅读,未连接 CAN 设备、未发送任何诊断请求。

    发现链路:从在场探测到品牌排序

    make_tester_present_msg() 生成 0x3e 0x00 服务请求,诊断 RX 地址由 get_rx_addr_for_tx_addr() 按 11-bit 偏移或 29-bit 地址字节交换计算。get_ecu_addrs() 先排空接收,再批量发送 tester present,在时间窗口内收集允许的响应地址与总线。

    函数/组件 输入与实际行为 输出边界
    get_present_ecus() 从各品牌 FwQueryConfig 取 ECU 地址,按 OBD 复用模式分组;无子地址并行、有子地址逐个查询 返回已响应的 (rx_addr, subaddr, bus) 集合;未响应不等于 ECU 不存在
    get_ecu_addrs() 排空接收、批量发 tester present,在时间窗口内收集允许的响应地址/总线 异常被记录后返回已收集的局部集合,不是全有或全无事务
    _is_tester_present_response() 验证短单帧格式;接受 SID 0x7e 正响应或 0x7f 0x3e 负响应 「ECU 在场」不表示该 ECU 接受诊断会话或会返回固件
    get_brand_ecu_matches() 将探测地址与各品牌预期 RX 地址比较 只作查询顺序启发;实现明确未比较 bus
    get_fw_versions_ordered() 按已命中 ECU 数与比例排序,仅查询有命中的品牌,单一固件候选即停止 不是全品牌全 ECU 的完整清单;早停可由 fuzzy 单一候选触发

    这里有两个容易被忽略的边界。其一,get_ecu_addrs() 的 timeout 是循环条件,而 can_recv(wait_for_one=True) 可阻塞,因此该值不是整个扫描过程的硬墙钟上界;经 card 的回调使用时订阅端设置了 timeout=20。其二,OBD 复用切换由参数写入及 pandad 确认构成,回调内没有独立握手超时。

    独立开发工具 get_all_ecu_addrs() 枚举 11-bit 与 29-bit 发送地址,但直接把 queries 复用为 responses:11-bit 枚举覆盖整个 0x700..0x7ff 范围,29-bit 通常需要交换后的 RX 地址。因此按代码推导,该工具的 29-bit 响应多数不会通过白名单,并非可靠的完整 ECU 清单;生产用的 get_present_ecus() 是单独计算预期 RX 地址的另一条路径。

    ISO-TP 查询状态机

    IsoTpParallelQuery 把 TX 地址映射成预期 RX 地址,按总线与地址过滤,子地址再逐条筛选;get_data() 用每地址计数器校验多阶段请求的响应前缀,最终只返回成功完成的 (tx_addr, subaddr) -> payload。VIN 的功能寻址先发广播请求,随后由物理寻址的 ISO-TP 对象接收/发流控;固件查询通常用物理地址。

    层 职责 边界
    CanClient 按 bus/RX 地址过滤,处理子地址和发送节拍 只对匹配地址的帧做后续处理
    IsoTpMessage 单帧、多帧、连续帧序号及流控;固件查询设为非阻塞接收、10 ms separation time 发送与接收错误可被上层记录并使对应地址结束
    get_data() 每地址计数器校验多阶段响应前缀 只返回成功完成的结果,部分成功不返回
    pending 分支 响应第三字节为 0x78 时延长该地址等待 只看第三字节,不先要求完整负响应前缀,属宽松条件

    正常多帧进展会刷新单地址截止时间;响应的第三字节为 0x78 时走 pending 分支并延长等待。关键在于 pending 判断仅按第三字节,不先要求完整负响应前缀 0x7f ... 0x78,所以它是宽松的超时延长条件,而非已认证的 UDS pending 状态。另外 total_timeout=60 也在循环尾检查,不能约束本次阻塞式 can_recv() 的耗时。

    固件数据契约

    get_fw_versions() 为每个成功响应创建 CarFw,记录 ECU 类型、固件字节、TX/RX 地址、请求、品牌、bus、OBD 复用模式及 logging。请求配置允许白名单、特殊 RX offset、子地址,以及需记录但不参与指纹匹配的 ECU。个别查询异常被捕获并继续,因此返回值可以是部分固件清单。build_fw_dict() 丢弃 logging 响应;匹配函数用地址/子地址与固件版本来判定,而不是把 CarFw 中的 bus 和响应地址都作为唯一身份校验条件。

    自制硬件与插件边界

    以下是改造建议,不是现有运行时的保证。应将 ECU 探测器、VIN 解码器、固件查询器拆成可替换的证据提供者,但对外只暴露受限的诊断代理;插件不得直接持有原始 sendcan 发布权限。诊断代理应对 bus、地址、服务 ID、速率、总时长、同时请求数和点火/行车状态实施硬限制,物理网关/MCU 独立保留 CAN 输出安全门。状态建模上,应把正/负响应、部分超时、未查询、OBD 模式切换失败分成不同状态,不以空集合或空固件列表直接推断「该车型不受支持」;每条证据应附采集时间、硬件拓扑和本次启动标识,缓存或早停结果不得自动获得执行许可。

    常见问题

    收到 tester present 响应就说明 ECU 支持诊断吗?

    不说明。_is_tester_present_response() 只验证短单帧格式并接受 0x7e 正响应或 0x7f 0x3e 负响应。「ECU 在场」不表示它接受诊断会话或会返回固件。

    0x78 的 pending 判断是标准 UDS pending 吗?

    不是。pending 分支仅按第三个字节是否为 0x78 判断,不先要求完整的负响应前缀,因此它是宽松的超时延长条件,而非已认证的 UDS pending 状态。

    get_fw_versions_ordered() 会返回全部 ECU 固件吗?

    不会。它按已命中 ECU 数与比例排序,只查询有命中的品牌,并在拿到单一固件候选时即停止,因此返回的是候选子集而非全品牌全 ECU 的完整清单。

    为什么 get_all_ecu_addrs() 的 29-bit 结果不可靠?

    因为它把 queries 直接复用为 responses,而 29-bit 通常需要交换后的 RX 地址,多数响应不会通过白名单。可靠路径是单独计算预期 RX 地址的 get_present_ecus()。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/ECU_DIAGNOSTIC_TRANSPORT_DEEP_DIVE.md(取用了 ECU 发现链、ISO-TP 查询状态机、固件数据契约、插件边界与离线探针结论)。
    • 项目 02 —— openpilot-research/CAR_MESSAGES_FIELD_CONTRACT_DEEP_DIVE.md(仅用于确认诊断结果进入车型识别与安全配置的衔接位置)。

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

    更新记录

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

  • UI 翻译构建链:目录漏扫、msgid 冲突与 199 个唯一键

    结论:这套 UI 翻译构建链(update_translations → extract_strings → POT/PO → Multilang)目前只扫描四组 Python 目录,而 mici 不在其中:扫描范围为 70/125 个 Python 文件,脚本现行范围与 app.pot 都含 199 个唯一 msgid,全 UI 范围是 217 个,新增的 18 个唯一 msgid 全部来自未扫描的 mici,12 份现有语言文件对这 18 个均为 0/18。缺 msgid 不会崩溃,但非英语模式会静默显示英文。本文为固定源码快照的构建时静态分析,未运行会改写归档的更新脚本,未做实屏语言测试。

    扫描范围与 AST 提取

    update_translations() 遍历 SYSTEM_UI_DIR 以及 UI_DIR/widgets、layouts、onroad,收集 .py 相对路径,提取字符串后改写 app.pot,再逐语言 merge 或 init。mici、body 和 UI_DIR 根目录不在扫描集合内。extract_strings() 用 ast.walk 找名称或属性名为 tr、trn、tr_noop 的调用,只读取前一或两个位置参数为字符串常量的调用,按 msgid 去重并记录 file:line,解析失败的文件直接跳过。

    函数/组件 输入、输出与关键行为
    update_translations() 遍历四组目录,改写 app.pot,逐语言 merge 或 init;mici/body 不在扫描集合内
    extract_strings() ast.walk 找 tr/trn/tr_noop 调用,仅取位置参数字符串常量,按 msgid 去重
    generate_pot() 为提取结果写最小 POT 头部;本身不检查源文件覆盖率或占位符
    parse_po() 解析头部、单复数条目、续行、注释、引用与 flags;跳过 #~ 过时条目
    write_po() 引用从 file:line 折叠到文件名;不输出 flags,故 python-format 不会保留
    merge_po() 按 msgid 匹配旧译文,更新引用与注释,删除 POT 不再存在的条目,按 msgid 排序写回
    Multilang.trn() 运行时另行解析 PO;单数缺译回退原文,复数缺槽或空译文回退英文单复数

    generate_pot() 只写最小 POT 头部,本身不检查覆盖率或占位符;parse_po() 也不是完整的 gettext 元数据解析器,它会跳过 #~ 过时条目。构建器与运行时共享文件格式,却不是同一个解析器:load_translations() 与 Multilang.trn() 在运行时另行解析 PO。

    已核对的五条构建边界

    第一,目录漏扫会导致静默英文回退。mici 的离车告警布局使用了 tr("no alerts"),但 update_translations() 未扫描 mici,当前 app.pot 及中文语言文件均无该 msgid;某些其他 mici 字符串因大屏目录使用同一原文而碰巧出现在 POT 中,不能据此认为 mici 已被完整覆盖。

    第二,单复数同键冲突依赖遍历顺序。extract_strings() 只以单数 msgid 为键:若 tr("same") 先于 trn("same", "many", n),生成项仍是单数,反过来则为复数;属性调用仅按属性名匹配,并不确认调用目标就是翻译函数。第三,合并不做形态迁移:原 PO 的单数 msgstr 若遇到新 POT 同 msgid 的复数项,merge_po() 会更新 msgid_plural 却不生成复数槽,运行时 trn() 因缺译回退英文。

    第四,新条目与已有语言的复数槽不按语言初始化:extract_strings() 给 trn 固定生成索引 0/1 两槽,init_po() 只在新建语言文件时按 nplurals 调整,已存在 PO 经 merge_po() 接收新复数条目时直接接收两槽,乌克兰语第三槽不会自动补齐。第五,msgid 是无上下文的全局 key,没有 msgctxt、插件命名空间或相同英文不同语义的分离机制,write_po() 又不保留 flags。

    边界 触发条件 可见后果
    目录漏扫 字符串位于未扫描目录(如 mici) 非英语模式静默显示英文
    同键形态冲突 单数与复数使用同一 msgid 结果取决于遍历顺序
    合并不做形态迁移 旧 PO 单数遇到新 POT 复数项 复数槽为空,运行时回退英文
    复数槽不按语言初始化 已有 PO 接收新复数条目 多复数语言第三槽缺失
    无上下文 key 同一英文在不同语义下复用 无法分离语义,flags 不保留

    覆盖率实测

    用仓库的 extract_strings() 对 update_translations() 指定的四组目录,以及 selfdrive/ui 加 system/ui 的全部 Python 文件分别提取,并与当前 app.pot 的 parse_po() 结果按 msgid 比较:扫描范围是 70/125 个 Python 文件,脚本现行范围与 POT 都有 199 个唯一 msgid,全 UI 范围为 217 个。这证明的是当前提取器能识别的常量覆盖差值,不是所有动态文案、上下文语义或屏幕翻译率。

    页面组件 缺失 key 数 可核查的例子
    离车告警页 1 空状态 no alerts
    设备配对页 2 Wi-Fi 连接与设备注册提示
    Firehose 页 12 FAQ 标题与问答、ACTIVE/INACTIVE 状态和贡献片段复数文案
    eSIM 页 1 网络错误提示,同一 msgid 在两处使用
    软件更新页 2 Wi-Fi 要求与分支列表获取失败提示

    TestTranslations 会检查语言文件存在、非空译文占位符、引用不含行号和疑似实体拼写,但不检查 AST 覆盖率、POT/PO 同步或单复数形态迁移,因此它无法捕捉漏提取的字符串或错误的调用归属。

    插件资源边界与迁移顺序

    显示硬件固件不应依赖插件侧任意 PO 直接覆盖宿主告警。更合适的边界是宿主登记 plugin_id + key + source + plural_forms + placeholders,构建期从已登记插件源码目录提取并做覆盖率/重复键/占位符/复数槽检查,再由宿主决定语言回退和安全告警优先级;第三方资源只扩展其命名空间,不能修改接管、失联和制动类文案。迁移顺序上,先补扫描清单与漏提取测试,再定义翻译资源清单和命名空间;之后让运行时按插件 ID 查找,最后逐步迁移现有 tr() 调用。旧 msgid 资源可作为过渡映射,但要报告歧义,不能静默覆盖。

    常见问题

    新增一个 tr() 字符串为什么没进翻译文件?

    先确认所在目录是否被扫描。update_translations() 只遍历 SYSTEM_UI_DIR 与 UI_DIR 下的 widgets、layouts、onroad,mici、body 和 UI_DIR 根目录不在集合内;此外 extract_strings() 只接受位置参数为字符串常量的调用,属性调用也仅按属性名匹配。

    为什么非英语界面会显示英文?

    Multilang.tr() 与 trn() 在缺项时使用英文源文案,因此缺 msgid 或译文为空时页面不会崩溃,只是回退英文。实测 12 份现有语言文件对 mici 新增的 18 个唯一 msgid 均为 0/18,这类页面正是静默回退的典型。

    同一个英文在单数和复数里会怎样?

    extract_strings() 只以单数 msgid 为键,若 tr("same") 先于 trn("same", "many", n) 出现,生成项仍是单数,反之则为复数,结果依赖遍历顺序。并且 merge_po() 遇到旧单数变新复数时只更新 msgid_plural 而不生成复数槽,运行时仍回退英文。

    插件能直接用自己的 PO 覆盖界面文案吗?

    不应如此。更合适的边界是宿主登记 plugin_id、key、来源、复数形态与占位符,构建期从已登记插件源码目录提取并做覆盖率/重复键/占位符/复数槽检查,由宿主决定语言回退与安全告警优先级,第三方资源只扩展自身命名空间。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/UI_TRANSLATION_BUILD_PIPELINE_DEEP_DIVE.md(取用了扫描范围、AST 提取、POT/PO 生成合并、五条构建边界与覆盖率实测)。
    • 项目 02 —— openpilot-research/UI_LANGUAGE_TEXT_LAYOUT_DEEP_DIVE.md(仅用于确认运行时语言切换与字体排版的分工位置)。

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

    更新记录

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

  • tinygrad dtype:weak 提交、64 位整数拆字与低精度浮点模拟

    结论:tinygrad 的 dtype 语义分两段:前端 Tensor 只构造 cast/bitcast 与 weak 常量,真正定宽发生在编译期——由 pm_commit_weak 推导输入宽度、pm_lower_weak 消除弱类型计算节点、末端 pm_cast_const 提交裸露常量;当某类型不在 renderer 支持集或列入 EMULATED_DTYPES 时,才由 pm_dtype_decomps 用两个 32 位字或位型重写来模拟。EMULATED_DTYPES 是运行上下文配置,不等于物理设备确实支持或不支持该 dtype。

    从 API 到 UOp:三个阶段

    前端阶段,DTypeMixin.commit_dtype() 利用 UOp 值域确定 weakint 的具体整数类型;cast() 产生数值转换,bitcast() 拒绝 weak dtype 并产生位重解释。两者都先构造 UOp,并非当场完成设备计算。弱类型提交阶段由 commit_weak_srcs()、cast_weak_srcs()、lower_weak_node() 根据消费者、显式 CAST 和表达式边界定宽,最终 pm_cast_const 处理仍裸露的常量;弱类型图不是可直接交给任意设备的 ABI。

    降解插入点固定在 full_rewrite_to_sink():代码生成先整理 weak 类型和符号,再进行早期算子降解,随后将 pm_dtype_decomps 与弱常量提交一起执行,最后才进入后期降解和渲染。编译次序上,sym + indexing_simplify + pm_commit_weak 先达到定点;接着 pm_lower_weak + indexing_simplify 固定计算 dtype,特意不再组合 symbolic,以免 Invalid 门重新生成 weak 节点形成循环;dtype 模拟之后的最终重写再次提交新产生的 weak 常量,末端 pm_cast_const 处理剩余边。

    阶段 函数/组件 行为和边界
    前端 API commit_dtype() / cast() / bitcast() 确定 weakint 具体类型;产生数值转换或位重解释;均先构造 UOp
    弱类型提交 commit_weak_srcs() / cast_weak_srcs() / lower_weak_node() 按消费者、显式 CAST、表达式边界定宽;pm_cast_const 末端提交裸常量
    降解插入点 full_rewrite_to_sink() 先整理 weak 类型,再早期降解,随后执行 dtype 模拟与弱常量提交
    选择模拟 do_dtype_decomps() / pm_dtype_decomps 仅在类型不受支持或在 EMULATED_DTYPES 时重写;uint64 归并到 long 降解族

    64 位整数:两个 32 位字

    宽整数降解由 l2i()、split_l2i() 和 pm_long_decomp 实现:用两个 32 位字完成转换、加减乘、比较、移位和逐位除法,内存定义扩大为两个字,索引/存储拆分。ALU 地址空间的 Variable 明确不支持此降解——这是宽整数模拟最容易踩的边界。

    低精度浮点则由 f2f()、f2f_clamp()、f2f_load()、f2f_store() 通过位型重写 FP8/BF16/FP16 的载入、计算/转换与存回;模拟路径显式把非规格化数冲零,FP8 转换还涉及饱和及 FNUZ 非有限值约定。这些数值语义差异必须在逐算子数值测试中单独覆盖,不能只验证 API 层能否调用。

    目标类型 降解入口 关键语义与限制
    int64 / uint64 l2i() / split_l2i() / pm_long_decomp 两字实现转换、算术、比较、移位、逐位除法;ALU Variable 不支持
    FP8 f2f() 家族 位型重写;涉及饱和与 FNUZ 非有限值约定
    BF16 / FP16 f2f() 家族 位型重写载入/转换/存回;模拟路径非规格化数冲零
    内存定义 f2f_load() / f2f_store() LOAD/STORE 存储格式重写;长整型缓冲扩为两个字

    选择逻辑上,pm_dtype_decomps 仅收集 FP8、BF16、FP16、int64/uint64 等候选;当类型不在 renderer 支持集,或列入 EMULATED_DTYPES 时才重写。Renderer.supported_dtypes() 给出基础能力接口,具体 renderer 可以覆盖;do_dtype_decomps() 在处理完收集集后清空上下文,使后续编译遍历不把先前发现的类型当作当前结果。

    weak 类型规则的逐函数前提

    weak 类型是这套降解里最容易被误读的部分。commit_weak_consts() 与 derived_dtypes() 对已有具体 dtype 的广播算子求操作数共同类型及结果类型,再提交裸常量——但两者任一仍为 weak 时返回 None,不会强行选一个设备宽度。cast_weak_srcs() 与 pm_commit_weak 为显式 CAST 的 weak 表达式提供计算宽度下界,因此 float32 表达式再 cast 到 float16 不意味着内部计算可降到 float16,需保留范围允许的更宽 dtype。

    absorb_weak_src() 与 lower_weak_node() 吸收可重推导的 weak CAST,并给无法再推导的算子/常量定宽;这里从浮点经 weakint CAST 是数值转换,不能当纯注释删掉,且 WHERE 条件不参与分支宽度统一。pm_lower_weak 处理小缓冲的 gated long index、连续 weak 类型转换和 ALU 变量参数,索引缩窄只在缓冲最大元素数落入 int32 时适用。pm_uncast_const 在消费者类型推导完全不变时去掉常量上的强类型 CAST;pm_cast_const 最后提交仍裸露的常量边,其中 Invalid 不按普通常量提交。

    与 openpilot 集成的边界

    ModelState.pack_inputs() 的帧与辅助输入分别使用 uint8 和 float32 缓冲视图,这里必须先保持 dtype、shape、偏移和设备拷贝一致;后端内部低精度降解发生在编译阶段,不能拿它替代输入缓冲 ABI 检查。构建期 compile_model() 捕获并编译模型/warp 的 TinyJit 产物,实际节点是否包含某个被模拟 dtype,需要检查相应 LFS 模型图及编译产物。

    源码证据层面,本机补跑 test_dtype_weak.py 得 46 项 OK,含 1 项预期失败 test_changed_rows——预期失败明确表明部分弱标量提升矩阵尚未达到测试期待,不能把整套弱类型语义视为已完全收敛。后端小样例中,int64 两元素加法在普通及 EMULATED_DTYPES="long" 上均得到 [1099511627788, 18],强制模拟 BF16 后转回浮点得到 [1.5, -2.0]。自制硬件后端若声明支持新 dtype,应有逐算子数值、字节布局、端序、非有限值、饱和、非规格化数、跨设备复制与尾延迟证据。

    常见问题

    EMULATED_DTYPES 设了,是否说明设备真的不支持该类型?

    不等价。源文档明确 EMULATED_DTYPES 是运行上下文配置,不等于物理设备确实支持或不支持某 dtype。是否重写还取决于类型是否在 renderer 支持集内:pm_dtype_decomps 只有当类型不在支持集或列入 EMULATED_DTYPES 时才重写。

    float32 表达式再 cast 到 float16,内部计算会降到 float16 吗?

    不一定。源文档指出 cast_weak_srcs()/pm_commit_weak 只是为显式 CAST 提供计算宽度下界,float32 表达式再 cast 到 float16 不意味着内部计算可降到 float16,需保留范围允许的更宽 dtype,否则会丢失中间结果精度。

    64 位整数模拟在所有场景都能用吗?

    不是。宽整数降解用两个 32 位字实现,但源文档明确 ALU 地址空间的 Variable 不支持此降解;索引缩窄也只在缓冲最大元素数落入 int32 时才适用。超出这些边界时需要另行处理。

    测试通过是否代表 dtype 语义已经收敛?

    不代表。test_dtype_weak.py 的 46 项 OK 中含 1 项预期失败 test_changed_rows,源文档据此说明部分弱标量提升矩阵尚未达到测试期待。此外 test_dtype_alu.py 的定向运行因环境缺少 hypothesis 在导入阶段即停止,64 位除法/移位、FP8 特殊值和具体 GPU renderer 未经本轮动态验证。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_DTYPE_UOP_LOWERING_EMULATION_DEEP_DIVE.md(取用了前端 API、weak 类型规则、宽整数与低精度浮点降解、编译次序及与 openpilot 集成的边界)。
    • 项目 02 —— openpilot-research/TINYGRAD_DTYPE_PROMOTION_STORAGE_CONTRACT_DEEP_DIVE.md(仅用于确认基础 dtype 提升与存储契约的上下文)。

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

    更新记录

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

  • TinyJit 捕获与重放:三阶段调用、输入签名与输出缓冲复用

    结论:TinyJit 的调用是三阶段:cnt=0 直接执行原 Python 函数、cnt=1 捕获 LINEAR、cnt>=2 校验输入签名并重放。重放只执行已记录的设备调用,Python 条件分支、闭包常量与普通副作用在捕获时就被冻结;而输入签名并不等于完整 shape 校验——本机探针用裸一维 BUFFER 长度 4 捕获后以长度 8 重放,两者签名相同且都未抛 JitError,返回仍是长度 4 的 Tensor。以下为固定 tinygrad 归档与本机最小探针的静态结构分析,未运行依赖 NumPy 的完整测试,也不代表模型数值或车载时延已验证。

    三阶段调用与关键函数

    _prepare_jit_inputs() 收集顶层位置/关键字 Tensor 以及容器中一层 Tensor,拒绝虚拟或重复输入缓冲,并记录视图、符号变量、dtype 与设备;它按底层缓冲检查重复,非 Tensor 的 Python 标量不会自动成为运行时参数。_TinyJit.__call__() 按计数分派:cnt=0 执行原函数,cnt=1 捕获,cnt>=2 检查签名并重放;JIT=0 走原函数,但仍会在函数末尾递增 cnt。

    函数 本版本实际行为 需要保留的契约
    _prepare_jit_inputs() 收集顶层与容器一层的 Tensor,拒绝虚拟或重复缓冲,记录视图、符号变量、dtype、设备。 视图可参与签名,但裸 BUFFER 长度未必被记录;非 Tensor 标量不自动成为运行时参数。
    _TinyJit.__call__() cnt=0 执行原函数;cnt=1 捕获;cnt>=2 检查签名并重放。 JIT=0 仍递增 cnt;切换配置前应理解计数状态,有原函数时可用 reset() 重新捕获。
    prune_linear() prune=True 时保留触及输入缓冲及其后续相关缓冲的调用,其余作为一次性列表在捕获时执行。 输入无关的初始化可被固定为捕获期结果,不能假定每帧重新运行。
    jit_lower() 把输入缓冲参数化,对非持有缓冲做内存规划,调用 compile_linear(),JIT<2 时尝试组成设备图。 须固定输入、临时缓冲、编译配置及图后端的完成/错误语义。
    graph_split_rewrite()、GraphRunner.supports_uop() 只有后端声明支持时才分组;默认单设备只支持同设备的 PROGRAM。 「JIT 已捕获」不等于所有调用都在单个 GPU graph 内,也不等于没有主机/设备同步。

    输入签名、返回值与分支冻结

    捕获后重放会比较名称列表及由输入视图/变量/dtype/设备组成的 expected_input_info;同一函数从位置参数改关键字参数或换视图偏移可能被拒绝,但不能把此签名当作完整 shape 校验。输入内容本身可以变,却不能让 Python 布尔开关或闭包常量每帧重新执行。

    本机最小探针在默认 METAL 后端用裸一维 BUFFER 长度 4 完成捕获,再以长度 8 的同 dtype/设备 BUFFER 重放:两者的 expected_input_info 都是相同的 NOOP 视图、空变量、float 和 METAL,没有抛 JitError,返回的仍是捕获时长度 4 的 Tensor。这只证明该输入形态下签名未拦截长度变化,不证明所有 reshape/视图都绕过检查;仓库已有使用不同输入构造的 shape 变化用例,仍需运行和区分。对模型插件应在 JIT 之外显式检查形状、步长、字节长度和布局。

    第二次调用才在 capturing 非空期间收集 LINEAR;嵌套 TinyJit 被拒绝,未捕获任何调用或返回含非 Tensor 的对象也会报错。_check_no_non_tensor_return() 允许 None、Tensor 以及递归包含它们的 tuple/list/dict。需要强调的是 CapturedJit 保存的是捕获时的返回对象,不是每次调用重新构造的 Tensor;仓库的 test_output_buffer_reuse() 明确断言后续重放会覆盖先前捕获结果,需要保留某帧输出时须另行复制或在下游完成消费后再复用。

    Python 条件分支、闭包常量和普通副作用只在首轮直接执行及捕获轮执行,重放只执行已记录的设备调用。仓库已有把这种差异固定为回归用例的 test_python_constants_frozen() 与 test_conditional_branches_frozen()。对驾驶模型的驾驶位策略、道路模型的大/小模型选择或相机格式,不能只改变 Python 变量而继续复用同一捕获图,必须确认模型实例与捕获输入契约是否重新建立。

    与 openpilot 模型的实际连接

    load_pickle() 反序列化已捕获的模型产物,并用分块复制恢复权重缓冲。modeld.ModelState.__init__() 对道路模型及 warp 的 captured._linear 做 lower_and_compile();ModelState.run() 先上传打包输入,再跑 warp 与模型,最后读回输出。由于 CapturedJit.linear 是 cached_property,这条替换路径依赖首次 linear 访问发生在替换之后;当前构造函数顺序符合该前提,但未来插件若提前读取 captured.linear,随后仅修改 _linear 不会自动刷新已缓存链接结果。

    dmonitoringmodeld.ModelState.run() 则缓存指向相机缓冲的 from_blob Tensor,运行座舱 warp 和模型。两条模型路径都需要保证图捕获输入地址、相机缓冲生命周期和输出消费顺序;但座舱路径不能直接套用道路模型构造函数里的显式预编译步骤。

    验证边界

    调用轮次 cnt 行为 返回
    第一次 0 直接执行原 Python 函数 实现的 Tensor
    第二次 1 捕获 LINEAR 捕获结果
    第三次及以后 >=2 校验签名并重放 捕获时保存的返回对象
    长度 4→8 重放 >=2 签名相同、未抛 JitError 仍是长度 4 的 Tensor

    本机还运行了不依赖模型文件的 JIT 探针:第一次调用后 cnt=1 且未捕获,第二次后 cnt=2 且已捕获,第三次后 cnt=3;捕获轮与重放轮返回同一 Tensor 对象。优先继续验证相机 ringbuffer 复用、输出复制时点、设备图异常及重连后的重新捕获。最小探针不能替代实际道路/座舱模型的逐帧数值对比、时延上界及安全退出验证。

    常见问题

    TinyJit 第几次调用才是捕获?

    第二次。源码按计数分派:第一次(cnt=0)直接执行原 Python 函数,第二次(cnt=1)捕获 LINEAR,第三次及以后(cnt>=2)校验签名并重放。

    重放返回的 Tensor 是每帧新建的吗?

    不是。CapturedJit 保存的是捕获时的返回对象,仓库 test_output_buffer_reuse() 明确断言后续重放会覆盖先前捕获结果,所以需要保留某帧输出必须另行复制。

    改了 Python 里的开关,重放会跟着变吗?

    不会。Python 条件分支、闭包常量与副作用只在执行轮与捕获轮运行,重放只执行已记录的设备调用。仓库的 test_conditional_branches_frozen() 与 test_python_constants_frozen() 把这一差异作为回归用例。

    输入签名能保证形状正确吗?

    不能当作完整 shape 校验。本机探针用长度 4 的裸 BUFFER 捕获后以长度 8 重放,两者签名相同且未抛 JitError,返回仍是长度 4 的 Tensor。插件应在 JIT 之外显式检查形状、步长、字节长度与布局。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_JIT_CAPTURE_REPLAY_DEEP_DIVE.md(三阶段调用、输入签名探针、输出缓冲复用与分支冻结)。
    • 项目 02 —— openpilot-research/ 同目录张量调度与执行引擎专题(jit_lower 与图后端作为相邻边界参考)。

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

    更新记录

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

  • 硬件后端与 USB 快照:平台选择、热区与设备发现契约

    结论:openpilot 的硬件后端在模块导入时就以 /AGNOS 是否为文件确定平台,并实例化一个 HardwareComma 或 HardwarePc 单例;这不是按 USB、车型或插件清单动态完成的,运行中增删 /AGNOS 也不会切换已导入的单例。更关键的是,热区缺测会被解析为 0 摄氏度、USB 线缆 connected 只表示 Type-C 朝向非零,二者都不能当作传感器健康证据。本文为固定源码快照 c8fb906 的静态阅读与离线探针,未接触车端设备或真实热管理 sysfs。

    平台抽象与路径:能力不是「能实例化」就具备

    common.hardware.__init__ 在模块导入时以 /AGNOS 是否为文件确定 AGNOS、COMMA_HARDWARE、PC 三个标记,并实例化 HARDWARE:comma 设备用 HardwareComma,其他环境用 HardwarePc。cast(HardwareBase, ...) 只是类型提示,不提供运行时能力校验。HardwareBase 只把 get_device_type() 声明为抽象方法,其余多数接口有默认值、空操作或打印行为。

    接口 基类默认行为 含义
    电流/功率 默认 0 没有实现时返回 0,不等于实测为 0
    网络类型 默认 none HardwarePc 覆盖为 wifi
    shutdown() 仅打印 「能实例化」不等于具备车载关机能力
    get_thermal_config() PC 基类返回空配置 无热区配置时不应假定温度可用

    Paths 按已选平台给出日志、配置、持久化、共享内存和下载缓存路径。PC 的 comma_home() 带 OPENPILOT_PREFIX,设备侧 log_root() 固定在 /data/media/0/realdata/ 但可被 LOG_ROOT 覆盖,下载缓存可用 COMMA_CACHE 覆盖。这些路径不是同一个全局「沙箱根」,OPENPILOT_PREFIX 不具备权限隔离语义。

    热区读取的健康语义

    ThermalZone.read() 第一次在 /sys/devices/virtual/thermal 中按 type 查找编号并缓存,之后读取 temp 并按 scale 转为摄氏度。临时 sysfs 探针确认:未找到匹配项时编号仍为 -1,对应路径触发 FileNotFoundError 后返回 0;temp 文件消失同样返回 0,后续名称出现又可重新发现。但 os.listdir() 和 type 文件读取位于 try 之外,缺失 type 会传播异常,temp 非整数也会传播 ValueError。更需注意的是 zone_number 一经缓存不再重新按名称查找,探针确认热区编号对应的 type 改变后仍读原编号。

    ThermalConfig.get_msg() 遍历配置字段并把单项或列表输出为 cpuTempC、gpuTempC 等消息字段,未配置的类别被省略;HardwareComma.get_thermal_config() 给出对应平台的热区名称。结论很直接:自研主机不能用「0 °C」表示传感器失联,必须单独上报有效位或错误,否则后续热安全逻辑可能把缺测当低温。

    USB 发现到消息的逐层判据

    阶段 实现与确切含义
    拓扑变化 get_usb_topology() 返回 /sys/bus/usb/devices 中的名称集合;探针确认产品字符串改变但名称不变时集合不变。os.listdir() 抛 OSError 时返回空集合。
    设备枚举 usb_devices() 取有 idVendor 的 sysfs 节点并按名称排序;get_usb_state() 逐个读取 VID/PID、bus/dev 编号、速率、厂商和产品字符串。
    控制器与方向 controller() 从真实路径祖先中寻找 .ssusb 控制器;只有主控制器才把 Type-C 朝向 1/2 映射为 a/b。linkErrorCount 从 portli 读取并截 16 位,失败也变 0。
    线缆状态 cable_connected() 仅判断 Type-C 朝向整数是否非零;这是线缆/朝向指示,不是设备枚举成功、PCIe 就绪或 USB 传输健康的证明。
    消息发布 set_usb_state() 把传入列表写到 deviceState.usbState.devices;chestnutPresent 只根据运行态 VID/PID 判断,不核验产品字符串、数量或模型可用性。

    hw_state_thread() 约每 10 秒或名称集合变化时采集昂贵的硬件信息,并尝试投递到容量为 1 的队列;队列满则丢弃新快照。原函数探针确认它在采集和入队之前更新 prev_usb_topology:如果队列满、采集异常,而拓扑随后保持不变,下一轮不会仅因刚才未送达而重试,只能等周期触发或下一次拓扑变化。队列满时 prev_hw_state 仍更新,后续调制解调器温度为空时甚至可能继承一份从未交给主线程的温度。

    hardware_thread() 从初始空快照开始,拿不到新快照时沿用旧数据,随后调用 set_usb_state() 并驱动 Chestnut 维护和告警。因此 devices 与调用时重新读取的 connected 可能来自不同采样时刻,突然断线、重枚举或读取失败不能被简单解释为同一个原子 USB 状态。速率字段直接调用 read_int():探针确认 1.5 这类非整数文本会回退为 0 Mbps 而非保留小数或标记「未知」;同一测试中缺失制造商字段变空串、portli=0x10002 截为 2、主控制器朝向 1 映射为 lane a。所以 speedMbps == 0 不能直接解释为物理链速率为零。

    下游各自使用更严格或不同的判据:ChestnutStatus.update() 检查设备数量、精确产品字符串和速度;modeld.chestnut_present() 也要求产品字符串匹配;selfdrived 使用 chestnutPresent 与模型/Params 状态产生失败事件;UIState 还把线缆和编译产物纳入展示状态。不能把任意一个布尔值当作完整的外置计算就绪证明。

    面向自研硬件和插件的约束

    1. 平台后端应声明供电、热区、关机、屏幕、蜂窝、USB/PCIe 等能力及「不可用」状态;禁止用基类静默默认值伪装传感器健康。
    2. 设备发现事件与状态快照应带单调时钟采样时间、错误原因和新鲜度。VID/PID、固件版本、传输速率、线缆朝向、模型就绪必须分层校验。
    3. 数据总线可发布只读设备观测插件;固件刷写、特权 sysfs 写入、设备重置和关机属于受信任平台服务,只能在驻车维护流程中授权。
    4. 替换 /AGNOS 二选一选择前,先让原有 comma 与 PC 行为保持可回归,再对新平台测量热区缺测、USB 同名重枚举、队列积压和设备掉线后的退出时序。

    常见问题

    热区传感器缺失时读到 0 °C 意味着什么?

    意味着失联被当成低温。源码中未匹配到 type 或 temp 文件消失都会返回 0。自研主机必须另设有效位或错误上报,否则热安全逻辑可能误判。

    usbState.connected 为真代表设备已枚举成功吗?

    不代表。cable_connected() 仅判断 Type-C 朝向整数是否非零;它是线缆/朝向指示,不是枚举成功、PCIe 就绪或传输健康的证明,且发布时是另读的当前朝向,不必与设备快照同龄。

    为什么快照队列满之后可能不再重试?

    因为 hw_state_thread() 在采集和入队之前就更新了 prev_usb_topology。若队列满或采集异常而拓扑保持不变的,下一轮不会仅因未送达而重试,只能等周期触发或下一次拓扑变化。

    为什么 speedMbps == 0 不等于链路速率为零?

    因为速率字段直接调用 read_int(),1.5 这类非整数文本会回退为 0。读取或解析失败都归一为 0,因此不能把 0 解释为物理链速率为零。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/HARDWARE_BACKEND_USB_SNAPSHOT_CONTRACT_DEEP_DIVE.md(取用了平台抽象与路径、热区健康语义、USB 逐层判据、自研硬件约束与离线探针结论)。
    • 项目 02 —— openpilot-research/SYSTEM_FOUNDATION_DEEP_DIVE.md(仅用于确认 deviceState 与 hardware 线程的衔接位置)。

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

    更新记录

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

  • 巡航设定速度:VCruiseHelper 的 PCM 与非 PCM 两条路径

    结论:巡航设定速度(vCruise)是规划与显示的输入,不是车端已执行车速,也不代表系统已使能。源文档把它的产生拆成两条互斥路径:CP.pcmCruise=True 时直接读原车 cruiseState.speed/speedCluster 转 km/h,按键调速由原车系统负责;CP.pcmCruise=False 时由 VCruiseHelper 自己处理按键并步进。两条路径的单位(km/h)、哨兵(255/0/-1)与边界(8–145 km/h)都必须逐车型核对,不能按品牌名称猜测。

    一、数据流与单位

    Car.state_update() 在车型接口解析车况后,先用当前 sm['carControl'].enabled 调用 update_v_cruise(),在 carControl.enabled 从假变真时再用 CS_prev 调用 initialize_v_cruise()。源文档注释明确这是为了使用 selfdrived 使能那一步的按钮状态,避免按键边沿与 card 的下一次车况更新错位。

    随后发布的 CS.vCruise、CS.vCruiseCluster 单位都是 km/h;原始 cruiseState.speed 是 m/s,在 PCM 路径中乘 MS_TO_KPH。LongitudinalPlanner.update() 对 carState.vCruise 上限裁剪到 V_CRUISE_MAX 再转 m/s,并区分 unset;Controls.state_control() 将 CS.vCruise 转 m/s 传给车型 PID 加速度限幅,而 HUD 的设定速度取 vCruiseCluster。两种速度值可能不同,不能把仪表显示值当作规划器原始目标。

    二、PCM 与非 PCM 两条路径

    条件 update_v_cruise() 行为 影响
    原车巡航不可用 设定值及仪表值均为 255;非 PCM 按键计时器不在该分支清零 规划器将其识别为未初始化;不代表车辆实际时速 255 km/h。恢复时旧计时器可能仍在。
    CP.pcmCruise=True 直接使用 CS.cruiseState.speed/speedCluster 转 km/h 按键调速由原车系统负责,helper 不自行步进;speed==0 改为 unset,speed==-1 保留 -1 特殊值。
    CP.pcmCruise=False 且原车巡航可用 从本地状态处理按钮,然后令仪表设定值等于内部设定值 自管设定速度;只有 enabled=True 时按键会改目标。

    PCM 读取路径没有在本 helper 内对正常速度统一裁剪到 145 km/h;下游纵向规划器有自己的上限处理。量产适配需要分别核对原车报文中的 speed 和 speedCluster 含义、无效哨兵和单位,不能把任意 0 或 -1 当作普通速度。

    三、非 PCM 按键算法与阈值

    _update_v_cruise_non_pcm() 先找增/减速按钮的松开边沿;若找不到,再检查按住计时是否达到 50 帧倍数,将其视作长按重复。执行变更前有两个防重入门:增速键用于从静止巡航恢复时不调高设定值;若按钮按下时尚未 enabled,而松开前才完成使能,也不把同一次按键再次解释为调速。update_button_timers() 在当帧调速判断之后更新计时和按下时的 standstill/enabled 快照,这是边沿语义的一部分。

    参数 / 规则 取值 说明
    V_CRUISE_UNSET 255 未设定哨兵。
    V_CRUISE_MIN 8 km/h 结果下限。
    V_CRUISE_MAX 145 km/h 结果上限;上限注释指出模型训练分布外的速度预测可能不可靠。
    短按步长 公制 1 km/h;英制 IMPERIAL_INCREMENT=1.6 km/h 英制由 1 mph 转换并取一位小数。
    长按倍率 ×5 长按时若当前值不在 5 倍步长网格,增速先向上取整、减速先向下取整,再继续步进。
    长按判定 按住 50 帧倍数 最终结果取一位小数并限制在 8–145 km/h。

    油门覆盖时按减速键,会把新目标至少抬到当前 vEgo,随后仍受 145 km/h 上限约束。源码条件还写有 setCruise,但该函数的按键选择集合仅含增速/减速,因此当前路径不会选择到 setCruise;不能据该条件宣称 SET 键在此处有同样效果。

    四、使能初始化、上游门与测试

    initialize_v_cruise() 对 PCM 直接返回;非 PCM 首次使能时默认从当前车速与初始下限中取较大者,普通模式初始下限 40 km/h,实验模式 105 km/h,再限制到 145 km/h。若是增速/恢复按键触发且已有旧设定值,则恢复 v_cruise_kph_last,不是重新用当前车速初始化。这里的 105 km/h 是设定速度的初始下限/上界提示语义,实验模式仍由模型和下游规划裁决实际加速,不能理解为车辆会立即开到该速度。

    非 PCM 的按键使能来自 CarState.buttonEnable(基础接口由增/减速键松开边沿产生,车型可覆写),经 CarEvents 转成 buttonEnable 事件,再由状态机决定使能。VCruiseHelper 只管设定值,不负责判断驾驶员监测、车辆故障或 panda 的 controlsAllowed;取消键、刹车与安全事件仍可让系统退出。

    源文档的输入清单里还列出 IsMetric 与 ExperimentalMode:前者参与决定短按步长取公制 1 km/h 还是英制 1.6 km/h,后者决定首次使能的初始下限取 40 还是 105 km/h。二者都只影响设定值的产生,不影响车端是否已执行,也不替代使能与安全门。

    测试方面,test_cruise_speed.py 覆盖非 PCM 短按松开、使能中按键、静止 resume、油门覆盖和初始化,文件 TODO 明确 PCM 路径尚无对应测试;本工作区的八项原类隔离探针使用假 NumPy 裁剪和假车况,不依赖实际 capnp、CAN 或 UI,也不等于实车按键与执行器测试。

    常见问题

    设定速度和实际车速是一回事吗?

    不是。设定速度是规划/显示输入,不是车端已执行车速,也不代表系统已使能。源文档还强调 vCruise 与 HUD 取的 vCruiseCluster 可能不同,不能把仪表显示值当作规划器原始目标。

    PCM 车型的长按调速逻辑由谁负责?

    由原车系统负责。CP.pcmCruise=True 时 helper 直接使用 CS.cruiseState.speed/speedCluster 转 km/h,本 helper 不自行步进;因此 50 帧长按、5 倍步进这类算法只存在于非 PCM 路径。

    实验模式初始下限 105 km/h,是不是车会立刻加速到 105?

    不是。源文档明确 105 km/h 是设定速度的初始下限/上界提示语义,实验模式仍由模型和下游规划裁决实际加速,不能理解为车辆会立即开到该速度。

    按增速键从静止恢复会调高设定值吗?

    不会。源文档给出的防重入门之一就是:增速键用于从静止巡航恢复时不调高设定值;另一个是若按钮按下时尚未 enabled,而松开前才完成使能,也不把同一次按键再次解释为调速。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CRUISE_SET_SPEED_DEEP_DIVE.md(取用了数据流与单位、PCM 与非 PCM 路径、非 PCM 按键算法、阈值常量与测试边界)。

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

    更新记录

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

  • tinygrad 内核生成链:优化、降级与两级编译缓存

    结论:tinygrad 的内核生成是一条「优化 → 降级 → 线性化 → 渲染 → 编译」的多阶段重写链,而不是一次简单的源码翻译。apply_opts() 决定弱轴与访存形态,full_rewrite_to_sink() 负责展开、降级、屏障与控制流,最后才由 do_render()、do_assemble() 与后端编译器产出二进制。编译成功不等于数值一致,缓存命中也不等于本次重新做了重写诊断。本文为固定源码快照 c8fb906 的静态阅读,未运行目标设备推理。

    入口与阶段顺序

    道路模型的 ModelState.__init__() 加载已捕获图,分别对 _linear 调用 lower_and_compile()。该函数只收集仍为 SINK 或尚无二进制的 PROGRAM 调用,按 to_program_key() 去重;未缓存项再调用 to_program(),当存在多个任务且没有 Beam 搜索时可交给 worker pool。编译后的 PROGRAM 会被替换回调用图,后续执行与 HCQ 链仍由运行时处理。

    座舱模型的 ModelState.__init__() 只装入 JIT 和 warp,并不在该构造函数里显式调用 lower_and_compile(),所以不能把道路模型的预编译时序直接推广到座舱模型。

    do_to_program() 要求 SINK 携带 KernelInfo,先调用 full_rewrite_to_sink();若 renderer 走 ISA 路径,还会执行预指令选择与指令选择;最后用 pm_to_program 反复重写到带 SINK、线性指令、源码和二进制的 PROGRAM。若输入本身已是 PROGRAM,会跳过从 SINK 开始的完整优化,但仍进入后续程序重写链,所以它不是「任何输入都从头编译」。

    阶段 关键函数 源码责任 对硬件移植的意义
    入口 lower_and_compile() / to_program_key() 收集未完成的调用并去重,编译后替换回图 只编译仍缺二进制的调用,不是逐帧重建模型
    优化 apply_opts() / Scheduler.apply_opt() 弱轴转全局轴,显式配置优先,再 Beam 或启发式 同一图在不同配置或搜索下会生成不同 launch 形态
    降级 full_rewrite_to_sink() 展开、向量化、dtype 与运算分解、屏障、控制流依赖 后端不能只实现 render()
    输出 do_linearize() / do_render() / do_compile() 线性化后输出源码或汇编,再交给后端编译器 CPU、Metal、QCOM、AMD 产物形态不同,不是共同 ABI

    优化层:弱轴、tensor core 与启发式

    apply_opts() 与 Scheduler.apply_opt() 把弱轴转为全局轴;显式传入的 opts_to_apply 优先,之后才可能进行 Beam 搜索,否则按条件调用手写启发式,并记录已应用的优化。hand_coded_optimizations() 会尝试 tensor core、图像向量化、matvec、归约分组和 upcast 等规则。

    关键边界是:同一张图在配置、renderer 能力或搜索结果变化后,可以生成不同的 launch 与访存形态,因此不能只比较模型名称来判断两次编译是否等价。另外,tensor core 的支持声明与 dtype、维度门必须与目标设备一致,启发式本身不是硬件性能保证。

    优化规则 作用 边界
    显式 opts_to_apply 优先于自动搜索 决定最终 launch 形态,需随产物一起记录
    Beam 搜索 可实际打开设备测候选 外层 ALLOW_DEVICE_USAGE=0 不构成「永不触碰硬件」的保证
    hand_coded_optimizations() tensor core、图像向量化、matvec、归约分组、upcast 声明与维度门须与目标设备一致

    降级与代码输出

    expand_reduce() 展开归约与轴,add_gpudims() 按 renderer 最大维度把逻辑 range 映射到全局与局部索引;目标的线程维度、最大工作组和同步语义因此成为编译接口的一部分。memory_coalescing() 按操作、缓冲、基础索引、有效位和参数分组连续访存,跳过 volatile,并按 dtype 与 renderer 能力选择向量长度。

    输出顺序不只取决于 AST 遍历。linearize() 与 CFGContext 对依赖图做带优先级的拓扑排序,给嵌套或相依的 range 增加执行顺序约束,调度或降级错误都可能改变载入、存储与循环语义。do_render() 输出源代码,ISA renderer 则经寄存器分配与汇编;do_compile() 与 Compiler.compile_cached() 再经后端编译器转成二进制。

    函数 具体职责 硬件适配边界
    expand_reduce() / add_gpudims() 展开归约与轴,映射逻辑 range 到全局/局部索引 线程维度、工作组上限、同步语义是编译接口的一部分
    memory_coalescing() 分组连续访存,按能力选向量长度 对齐、掩码、越界语义需要数值与故障样例验证
    linearize() / CFGContext 带优先级拓扑排序,约束嵌套 range 顺序 输出顺序不是 AST 遍历顺序
    do_render() / do_assemble() / do_compile() 源码或汇编输出,再经后端编译器生成二进制 不应把一个后端的产物形态当作共同 ABI

    两级缓存与配置边界

    to_program_key() 包含 AST key、renderer 类型、目标以及 NOOPT、dtype 模拟、tensor core、IMAGE 等代码生成配置;进程内的 to_program_cache 是一级缓存。由于 SPEC、DEBUG 不在该 key 中,对已命中的内核切换这些诊断开关,不会重新执行 full_rewrite_to_sink() 的验证与调试打印。第二级是 Compiler.compile_cached():在后端编译器 cache key 非空时以源代码查询磁盘缓存。

    两级缓存不是同一机制,新硬件验收应分别测试冷编译、内存命中、磁盘命中和配置切换。此外 full_rewrite_to_sink(optimize=False) 会跳过前段的 load、range 与调度优化,但仍执行展开、降级、分解、屏障与线性化准备,因此不能解释为「完全不编译」;do_to_program() 用 ast.tag is None 来决定该参数。

    缓存/开关 键或范围 行为边界
    进程内 to_program_cache AST key、renderer、目标、NOOPT、dtype 模拟、tensor core、IMAGE SPEC、DEBUG 不在键中,命中后不重跑重写诊断
    磁盘 Compiler.compile_cached() 后端编译器 cache key 与源代码 与进程内缓存不是同一层
    optimize=False 跳过前段优化 仍执行展开、降级、分解、屏障与线性化准备

    车载硬件与插件验收

    把内核生成链搬到新后端时,至少应固定模型产物哈希、tinygrad 版本、renderer 与编译器、目标架构和全部代码生成配置,并保存冷、热编译耗时及产物身份;再用同一输入张量在 CPU 基准与目标后端之间比较每层或可观测输出,覆盖掩码边界、非对齐形状、NaN/Inf、设备重连与缓存失效。内核编译或加载失败、执行超时、输出非有限值都应转换为明确的模型不可用状态,并验证 selfdrived 的控制退出。代码生成插件不应自行获得 sendcan 权限。

    常见问题

    tinygrad 编译成功是不是就等于数值正确?

    不等于。编译只说明从 SINK 到 PROGRAM 的重写链与后端编译器走通。源码明确指出编译成功不等于数值一致,型号数据与设备时延也待实测;因此需要用同一输入在基准与目标后端之间做数值对比。

    为什么改了 SPEC 或 DEBUG 开关也可能不重新编译?

    因为这两个开关不在 to_program_key() 中,进程内 to_program_cache 命中后不会重新执行 full_rewrite_to_sink() 的验证与调试打印。需要区分「缓存命中」与「本次重新编译」。

    optimize=False 是不是就等于完全不编译?

    不是。它会跳过前段的 load、range 与调度优化,但仍执行展开、降级、分解、屏障与线性化准备。因此不能把它解释为「完全不编译」。

    道路模型的预编译时序能不能直接套到座舱模型?

    不能。道路模型的 ModelState.__init__() 会对捕获图调用 lower_and_compile();座舱模型的 ModelState.__init__() 只装入 JIT 和 warp,不在该构造函数里显式调用它。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_KERNEL_CODEGEN_DEEP_DIVE.md(取用了入口阶段顺序、优化与降级函数、两级缓存与配置边界、验收要求)。
    • 项目 02 —— openpilot-research/TINYGRAD_ENGINE_BACKENDS_DEEP_DIVE.md(关联的执行引擎与后端专题,用于区分内核生成与设备执行,本文未展开其实现)。

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

    更新记录

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

  • Hyundai 经典 CAN panda safety:三模式权限矩阵

    结论:Hyundai 经典 CAN panda safety 把「标准雷达 SCC / 标准相机 SCC / Legacy」三种模式做成一张权限矩阵:同一辆车的按钮 RX 固定在 bus 0,而 TX 可能落在 bus 0 或 bus 2,纵向能力只在调试构建生效。Legacy 也不是全面关闭 RX 校验,放宽的只有 0x386 与 0x394,0x260、0x4F1、0x421 仍保留各自的校验或计数门。以下为固定源码快照 c8fb906 的函数级静态分析,不是目标固件或实车验证;bus 号指单个 safety 实例的本地总线编号,车型、线束与 ECU 版本需另行确认。

    初始化与模式选择

    CarInterface._get_params() 在 hyundai 与 hyundaiLegacy 之间选择,并设置相机 SCC 与纵向能力。需要注意的是 openpilotLongitudinalControl 只是上层请求,不是固件准入证明。hyundai_init() 再依据纵向、相机 SCC、FCEV 标志选择 RX 检查表与 TX 白名单;hyundai_legacy_init() 使用 Legacy RX 表,并强制关闭纵向和相机 SCC,仅保留基础 TX 表。

    共享的 hyundai_common_init() 在非调试构建中强制 hyundai_longitudinal=false。也就是说纵向链在源码层面就带着 ALLOW_DEBUG 编译门,不能由 Python 侧 CarParams 位推定量产固件可发。

    模式 SCC12 RX CLU11 TX 纵向/附属 TX 关键差异
    标准雷达 SCC、非纵向 bus 0 bus 0,4 字节 无 SCC TX 巡航接合沿使能;按钮只许受门控的 Resume/Cancel。
    标准相机 SCC、非纵向 bus 2 bus 2,4 字节 无 SCC TX 按钮 RX 仍是 bus 0,不能把 TX 所在总线误当按钮输入来源。
    标准雷达 SCC、纵向 不列为必需 RX bus 0,4 字节 SCC11/12/13/14、FRT_RADAR11、FCA11/12、0x7D0 在 bus 0 LONG 仅 ALLOW_DEBUG 有效;不经原车 SCC 接合沿使能。
    标准相机 SCC、纵向 不列为必需 RX bus 2,4 字节 SCC11/12/13/14、FRT_RADAR11 在 bus 0 相机 SCC 纵向不使用雷达静默诊断白名单。
    Legacy bus 0 bus 0,4 字节 无 SCC TX 强制非纵向、非相机 SCC;仅部分 RX 的完整性检查放宽。

    所有模式均可发送 bus 0 的 0x340 LKAS11 与 0x485 LFAHDA_MFC。纵向 TX 表中的 SCC11/12/13/14 对 bus 0 标记 check_relay=true;若原 ECU 在该总线继续发送同地址帧,共享继电器故障逻辑可阻断 TX。safety_fwd_hook() 只提供通用 bus 0↔2 转发,并静态阻断将 check_relay 地址送往本机 TX 总线,这不是 ECU 已被成功静默的证明。

    RX 完整性与状态消费

    各地址使用不同的计数位与半字节校验:0x386 使用位计数/XOR,其余适用帧使用半字节和。RX 完整性检查先于车型钩子执行,通用 safety_rx_hook() 只在消息匹配、校验有效且属于 RX 表时才调用 hyundai_rx_hook()。

    帧 检查表 实际消费
    0x260 EMS16 或 0x371 E_EMS11 同一备选组;0x260 检查 2 位计数和校验,0x371 忽略二者 ICE 从 0x260 读油门,EV/HEV 从 0x371 读油门。
    0x386 WHL_SPD11 标准模式校验加 4 位计数;Legacy 忽略二者 只用左前、右后轮速判断移动,阈值原始值 12。
    0x394 TCS13 标准模式校验加 3 位计数;Legacy 忽略二者 字节 5 的 DriverOverride 等于 2 判制动。
    0x251 MDPS12 忽略校验/计数,但监视存活 11 位驾驶员扭矩进入共享采样窗口。
    0x4F1 CLU11 忽略校验、检查 4 位计数 bus 0 上解析按钮和主开关,更新近期交互或纵向按钮边沿。
    0x421 SCC12 只在非纵向为必需;校验加 4 位计数 按相机 SCC 标志从 bus 2 或 bus 0 读取 ACC 模式。
    0x91 FCEV 油门 仅特定标准雷达 SCC 或纵向分支附加;忽略校验/计数 FCEV 标志下读取字节 6;标准相机 SCC 非纵向表没有该附加检查。

    FCEV 分支中 0x91 更新油门,但 0x260 的 ICE 回退条件仅排除 EV/HEV;若车辆也出现 0x260,后续该帧仍可能改写油门状态,须用目标车报文核对,不能把单帧观测当成最终状态。

    TX 字段边界与使能条件

    转向 0x340 从 11 位字段还原扭矩与请求位:普通/ALT_LIMITS/ALT_LIMITS_2 的最大扭矩分别为 384/270/170,每帧上升分别为 3/2/2、下降为 7/3/3,均使用实时差 112、驾驶员扭矩余量 50 与请求位节拍容忍;两种替代标志同时存在时 ALT_LIMITS_2 优先。非纵向的 0x4F1 只允许 controls_allowed 时 Resume,或 cruise_engaged_prev 时 Cancel;纵向模式跳过整个按钮 TX 分支,白名单内的 0x4F1 不受该专属按钮码限制,这是插件权限设计必须额外收紧的差异。

    纵向加速度 0x421 同时核对 aReqRaw 与 aReqValue,单位是 0.01 m/s²,活动范围为 -350 到 200,并禁止 AEB 减速度命令、AEB 请求和停止请求。共享加速度检查允许未激活编码,不能仅用数值落在范围内推断准入。雷达 SCC 纵向表包含 0x38D FCA11,专属钩子禁止其减速度/AEB 激活字段;0x483 FCA12 仅有地址/总线/长度白名单;0x7D0 仅雷达 SCC 纵向可发送,载荷必须是 02 3E 80 00 00 00 00 00。SCC11/13/14、FRT_RADAR11 和 HUD 没有该模式的逐字段校验,白名单准入不代表 ECU 接受,也不代表原车 AEB、FCW 或仪表状态保持不变。

    面向自制硬件的插件契约

    源文档给出的是一组建议设计而非已实现能力:车型身份插件应输出带证据的标准/Legacy、SCC 来源、动力类型与转向限制,未知或冲突组合不授予 TX;总线插件同时报告 RX/TX 的物理接口与本地 bus,尤其记录相机 SCC 模式下按钮 RX bus 0、TX bus 2 的差异。

    纵向插件只能提交控制意图;可信任网关需核对构建类型、MCU mode/param 读回、ECU 静默回执、原车 AEB 功能与失联降级,再交由独立 MCU safety 门裁决。对纵向 CLU11、FCA12、HUD 与 SCC 附属帧应另设最小载荷模板和速率配额,不能因原模式白名单存在就向普通插件开放原始 CAN 写入。现有 test_hyundai.py 覆盖标准/Legacy、替代转向限值、相机 SCC、FCEV 与纵向 AEB/诊断路径,但本文未运行这些单测,模型实体、目标固件、台架及实车行为仍未验证。

    常见问题

    Hyundai Legacy 模式是不做 RX 校验吗?

    不是。Legacy 只放宽 0x386 和 0x394 的校验与计数检查;0x260、0x4F1、0x421 仍保留各自的校验或计数门。同时它强制关闭纵向和相机 SCC。

    相机 SCC 模式下按钮输入在哪个总线?

    按钮 RX 仍是 bus 0,只是 CLU11 的 TX 落在 bus 2。源码明确说明不能把 TX 所在总线误当成按钮输入来源。

    Hyundai 纵向控制是不是打开参数就能用?

    不是。LONG 仅在 ALLOW_DEBUG 编译条件下有效,共享 hyundai_common_init() 在非调试构建中强制关闭纵向。Python 侧 CarParams 位不能作为量产固件可发的证明。

    0x7D0 的载荷能自由填吗?

    不能。源文档说明 0x7D0 仅雷达 SCC 纵向可发送,载荷必须是固定的 02 3E 80 00 00 00 00 00。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/HYUNDAI_CLASSIC_PANDA_SAFETY_DEEP_DIVE.md(模式权限矩阵、RX 完整性表、TX 字段边界与插件契约)。
    • 项目 02 —— openpilot-research/ 同目录 Hyundai/Honda 品牌总览(SCC 与纵向能力作为相邻边界参考)。

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

    更新记录

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

  • 界面告警、驻车更新与远程服务:不在控制闭环内的进程边界

    结论:界面告警、驻车更新、云端连接与设备诊断这些进程大多不在控制闭环内,但它们决定驾驶员能否看见告警、设备能否更新、以及远程能否观测。源文档给出的一条清晰边界是:ui 只做呈现、soundd 只把上游状态转成音频、updated 只在驻车条件下由 manager 启动、webrtcd 默认只监听 127.0.0.1:5001 且 HTTP handler 本身没有展示独立认证检查。因此「替换显示或音频硬件」「开放本地端口」都不只是 UI 问题,而是部署安全边界。

    一、谁在控制闭环之外

    源文档用一张关系图概括了这些服务的输入输出:selfdriveState / onroadEvents 同时喂给 ui + UIState 与 soundd;deviceState / pandaStates 喂给 ui 和 updated;athenad 的云端连接与 webrtcd 的本地流媒体相连;camerad / encoderd 向 webrtcd 供流;proclogd / journald / tombstoned 汇入日志与上传;GNSS 侧 qcomgpsd 或 pigeond + ubloxd 共同供 timed。

    这些进程的启停条件写在 process_config.py:updated 使用 only_offroad,webrtcd 使用直播或 notCar 条件,soundd 使用驾驶员视角条件。具体启停仍由 manager 动态判定,配置声明不等于恒久运行的保证。

    二、UI 与声音告警

    组件 关键入口 作用与失效边界
    渲染进程 ui.main() 选择大屏 MainLayout 或小屏 MiciMainLayout,每轮刷新 ui_state 并发布 uiDebug。
    UI 数据聚合 UIState.update() 非阻塞收取模型、车况、标定、设备与自驾状态;刷新设备插拔与屏幕管理。
    行车/驻车切换 _update_state()、_update_status() deviceState.started 与点火共同决定 UI 的 started;selfdriveState 决定已使能/覆盖显示。
    声音输入 Mic.callback()、Mic.update() 采集原始音频、估计近似 A 加权声压并发布 rawAudioData;主循环发布 soundPressure。
    声音输出 Soundd.soundd_thread() 按 selfdriveState.alertSound 播放告警,关键警报逐步升音量。
    失联警报 check_selfdrive_timeout_alert() 此前处于使能状态而 selfdriveState 短时失联时发出立即警告。

    三层职责必须分清:ui 是呈现层,不是使能状态的裁决者;soundd 不负责决定警告事件,只把上游状态转成音频;micd 发布的是未校准的近似声压,用于自适应音量,不应当作精密声级计。替换显示或音频硬件时,应保留视觉/声音告警时延、失联提醒和看门狗行为,不能因插件崩溃而静默隐藏关键警告。

    三、驻车更新路径

    入口 源码行为
    updated.main() DisableUpdates 时退出;加排他锁,建立 staging overlay,循环检查/拉取,失败记录并退避。
    WaitTimeHelper SIGHUP 触发立即拉取,SIGUSR1 只做检查;定时等待避免高频网络请求。
    Updater.check_for_update() 查询远端分支,比较目标分支与当前提交;目标分支还受设备类型映射影响。
    Updater.fetch_update() 将 staging overlay 标成不一致,fetch 目标分支并更新子模块,必要时更新 AGNOS 后 finalize。
    set_consistent_flag() 用 .overlay_consistent 表示已完成可切换镜像;拉取前清除,完成后由 finalize 路径设置。

    需要特别记住:更新代码里的 checkout --force、reset --hard、clean -xdff 是应用到 staging overlay 的安装逻辑,不应拿来操作本地研究目录。updated 只在驻车条件下由 manager 启动;按流量计费的网络会推迟普通拉取,但长时间未更新或用户请求仍可进入拉取路径。安全相关插件包应同样采用可回退、版本一致、签名与发布管控的更新路径,不能在行车中热替换控制算法。

    四、云端连接与 WebRTC

    athenad.main() 用设备 token 建立 WebSocket,失败后指数退避重连,守护进程跨 manager 重启;jsonrpc_handler() 从接收队列取请求,按 dispatcher 执行 RPC。startStream() 在驻车时设置 IsLiveStreaming 唤醒相关进程,再向本地 webrtcd 提交 SDP,默认回传车况和设备状态。

    webrtcd 默认绑定 127.0.0.1:5001,以 HTTP 提供 stream/schema/notify;handle_get_stream() 中新会话替换旧会话,已有连接时预热请求返回 busy。StreamSession.message_handler() 只接受本会话声明的传入服务类型,再转成 cereal 主题;notCar 直播路径可声明 testJoystick。

    这里必须写明风险边界:webrtcd 默认仅监听本机,但 HTTP handler 本身没有展示独立的认证检查,而 /stream 的请求体含可声明的传入桥接服务。因此本地访问权限、端口暴露和上游 RPC 授权是部署边界,不能把此类桥接列表视为通用车辆控制插件 API;车载产品必须限制网络暴露与可桥接主题。此处是基于静态路径的风险提示,并非端到端安全审计结论。

    五、时间、GNSS、调制解调器与诊断

    timed 发布 clocks,仅在 GNSS 消息新鲜、有定位且时间落入合理范围时调整系统时间,启动阶段无 GNSS 的行为需另行核对。pigeond 负责 u-blox 串口设备的供电、初始化与原始报文接收,硬件 GPIO 和波特率强耦合;ubloxd 做 UBX 帧同步与校验和解析;qcomgpsd 是另一条 GNSS/蜂窝模组诊断路径,与 ubloxd 的选择由设备节点决定。

    modem 管理蜂窝数据连接及状态机,路由、DNS 和失败重试需逐状态分析。proclogd 收集 CPU/内存/进程统计并发布 procLog;journald 将系统 journal 的 JSON 行映射为 operatingSystemLog;tombstoned 收集崩溃报告/栈信息,需另核对隐私与上传边界。这些是当前源码级功能入口,不代表已完成 GNSS 协议、蜂窝状态机或系统日志隐私审计。

    常见问题

    UI 或声音进程崩溃会静默隐藏关键告警吗?

    这正是需要防护的场景。源文档强调 ui 只是呈现层、不是使能裁决者,soundd 只把上游状态转成音频;替换显示或音频硬件时应保留视觉/声音告警时延、失联提醒和看门狗行为,不能因插件崩溃而静默隐藏关键警告。

    更新代码里的 reset –hard 会不会动到本地研究目录?

    不会,前提是它们只作用于 staging overlay。源文档特别说明 checkout --force、reset --hard、clean -xdff 是应用到 staging overlay 的安装逻辑,不应拿来操作本地研究目录;updated 也只在驻车条件下由 manager 启动。

    webrtcd 的桥接列表是通用车辆控制 API 吗?

    不是。StreamSession.message_handler() 只接受本会话声明的传入服务类型,但 HTTP handler 本身没有展示独立认证检查,本地端口暴露与上游 RPC 授权才是部署边界。车载产品必须限制网络暴露与可桥接主题,确保远程消息无法直达 sendcan 或修改 panda 安全模式。

    没有 GNSS 时 timed 会怎样调整系统时间?

    源文档只说明 timed 仅在 GNSS 消息新鲜、有定位且时间落入合理范围时才调整系统时间,并明确「需核对启动阶段无 GNSS 的行为」。因此无 GNSS 时的具体回退行为尚未在源文档给出结论,不能想当然地推断。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/OPERATIONS_SERVICES_DEEP_DIVE.md(取用了服务边界、UI 与声音告警、驻车更新、云端连接与 WebRTC、GNSS 与诊断的服务清单)。

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

    更新记录

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