标签: panda

  • Toyota SecOC 边界缺口:密钥在内存、同步不匹配只记日志、计数先于确认

    结论:Toyota 的 SecOC 链路在源码里是三段彼此独立的机制:密钥装载、同步计数器校验与 panda 安全钩子。它们各自都有明确的边界缺口——密钥只以 16 字节形式留在内存中,Params 的 DONT_LOG 标记并不等于加密;同步 MAC 不匹配时源码只写一条 carlog.error(),不停止组帧;三个消息计数在调用 add_mac() 后、返回候选帧前就递增,不等待 sendcan、SPI、panda TX 或 ECU 的接受回执。发布版对这类车型设 dashcamOnly,MCU 侧也不持有车辆认证密钥。以下为固定快照的静态阅读,未获取车辆密钥、未连接 ECU、未发送 CAN。

    能力门与密钥生命周期

    Toyota SecOC 平台把认证要求写进 CarParams,并把安全参数传给 panda。ToyotaSecOCPlatformConfig.init() 设置 TSS2/NO_DSU/SECOC 标志和专用 DBC;CarInterface._get_params() 置 secOcRequired 与 Toyota safety 参数,发布版把这类车型设成 dashcamOnly。Car.__init__() 从缓存文件或 Params 读取键,做长度与十六进制解析后交给 CarState/CarController;其中的 secOcKeyAvailable 仅是本地装载标志。

    具体地,card 可以从设备本地缓存文件中的 SecOCKey 导入 32 个字符到 Params,再用 bytes.fromhex() 解成 16 字节。正确长度只能证明数据格式,secOcKeyAvailable=True 并不证明它匹配车辆;若 Params 里是非十六进制文本,bytes.fromhex() 在该分支没有局部异常处理,初始化可能直接失败。密钥在 Params 里注册为 DONT_LOG,这只约束常规日志取值,不是存储加密、进程隔离或密钥来源认证。

    层 载体 能证明什么 / 不能证明什么
    能力门 CarParams.secOcRequired + dashcamOnly 声明该车型需要 SecOC;发布版因此进入被动模式,安全配置被改为 NO_OUTPUT。
    密钥装载 设备本地缓存文件中的 SecOCKey → Params → 16 字节内存 长度与格式可校验;不证明匹配车辆,也不是加密存储。
    日志标记 SecOCKey 注册为 DONT_LOG 只约束常规日志取值,不等于进程隔离或来源认证。

    同步与认证:MAC 不匹配只记日志

    SECOC_SYNCHRONIZATION 在 DBC 中是 8 字节消息,含 Trip/Reset 计数和 Authenticator。CarState 每轮复制解析器缓存的当前值,未在此处对同步 MAC 或样本年龄作校验。控制器看到 Reset 与上次不同时,把 LKA、LTA2、ACC2 三个计数清零,再计算期望的同步 MAC。

    关键边界在于:不匹配时只有 carlog.error(),没有在该分支停止组帧或撤销控制资格。另有一个初始化细节——首次本地 Reset 初值为零,若同步消息也为零,本轮不会触发这次校验。解析器总线健康属于另一层,控制器在这里没有逐同步帧的年龄门。

    签名输出:计数先于链路确认递增

    add_mac() 用地址、控制载荷前四字节以及 Trip/Reset/消息计数构造 CMAC 输入,截断后与两组低位标记一起放进 8 字节 CAN 数据;不同命令流各有递增计数。CarController.update() 在 Reset 变化时归零三种本地计数,并分别签 LKA、LTA2、ACC2。

    递增时机是核心问题:计数在调用 add_mac() 之后、返回候选 CAN 帧之前增加,不等待 sendcan、SPI、panda TX hook 或 ECU 的接受回执。因此应用层的计数是「本地尝试序号」,不是「车辆已执行序号」;断流、重启、Reset 切换及报文丢失时的同步策略需要另行验证。

    环节 源码可见做法 缺口
    同步帧解析 CarState.update() 复制解析器缓存 未在此处校验 MAC 或样本年龄。
    同步 MAC 校验 Reset 变化时清零三计数并算期望值 不匹配只 carlog.error(),不停组帧。
    Reset 初值 首次本地 Reset 为 0 同步消息也为 0 时不触发本轮校验。
    命令计数 add_mac() 后立即递增 不等待 sendcan/SPI/panda/ECU 回执。

    panda safety 与覆盖范围

    Toyota safety 的 SecOC TX 白名单包含相应的 LKA/LTA2/ACC2 报文,继续约束消息地址、总线、长度、力矩/角度及纵向输出;例如 SecOC LTA2 的非零执行字段在当前钩子中被禁止。toyota_secoc 参数只有在 ALLOW_DEBUG 编译时才从安全参数开启,不能假定所有固件构建都允许相同模式。

    更重要的是责任边界:MCU 侧没有车辆 SecOC 密钥,也不在此代码中校验 AES-CMAC;认证通过与否由车辆 ECU 决定,安全钩子只负责本地发送约束。相邻测试里,通用车型 TX 回放会跳过 SecOC(需要真实键),panda safety 单测会检查 SecOC 地址与限幅,但不验证 ECU 的密码学接受——白名单与限幅通过并不证明车辆会接受签名帧。

    自制硬件与插件重构

    把车辆认证能力放在受信任、受限的签名服务中,而不是通用车型插件或日志进程:签名服务只对已授权的车型、命令与当前有效同步状态处理候选帧,记录键版本而不泄露键值;CAN 执行仍需独立的 ECU 安全限幅网关。输入契约至少应包含同步帧来源、解析器有效位、接收时间、Trip/Reset 代号和认证结果;MAC 失败、缺帧、过期或密钥不匹配都应进入明确的不可执行状态,不能只写日志。

    离线验收要覆盖:未配置键、非法十六进制键、错误键、同步 MAC 不匹配、Reset 为零或跳变、丢帧、重复或过期同步,以及三条命令流计数的重启行为;再在无车台架验证安全钩子与总线发送结果。在没有车辆密钥或 ECU 反馈时,只能确认本地编码与限幅,不应宣称 SecOC 车型已受支持或已可安全上路。

    常见问题

    SecOCKey 注册为 DONT_LOG 就等于密钥被加密了吗?

    不是。DONT_LOG 只约束常规日志取值,它不代表存储加密、进程隔离或密钥来源认证。密钥最终以 16 字节形式留在 CarState/CarController 内存中,正确长度也只证明数据格式,不证明匹配车辆。

    同步 MAC 不匹配会阻止发送控制帧吗?

    不会。源码在不匹配分支里只有一条 carlog.error(),没有停止组帧或撤销控制资格。此外首次本地 Reset 初值为零,若同步消息也为零,本轮根本不会触发这次校验。

    LKA/LTA2/ACC2 的计数是「车辆已执行」的序号吗?

    不是。计数在调用 add_mac() 后、返回候选 CAN 帧前就递增,不等待 sendcan、SPI、panda TX hook 或 ECU 回执,因此它只是本地尝试序号,不是车辆已执行的序号。

    panda safety 通过就能说明 SecOC 车型可用吗?

    不能。panda 只约束消息地址、总线、长度与力矩/角度/纵向限幅;MCU 不持有车辆密钥、也不校验 AES-CMAC。车型回放测试因无密钥会跳过 SecOC TX 用例,白名单与限幅通过并不证明车辆会接受签名帧。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TOYOTA_SECOC_KEY_FRESHNESS_SAFETY_BOUNDARY_DEEP_DIVE.md(取用了能力门与密钥生命周期、同步与认证边界、add_mac() 计数时机、panda safety 覆盖范围与插件重构建议)。

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

    更新记录

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

  • Cabana 消息详情:标签同步、编辑提交与告警的三条边界

    结论:Cabana 的「消息详情」页是 DBC 分析/编辑界面,不提供车端 CAN 发送或安全模式授权。它把消息选择、位图/信号视图、历史页与告警收敛在一个 DetailWidget 里,通过 MessageId(source/address)建立标签并刷新状态。需要记住三条边界:告警不是协议证明(不查校验、计数器与新鲜度);编辑只是内存 DBC 变更(磁盘保存是另一条链);关闭最后一个标签后详情与序列化的活动消息 ID 可能仍指向旧项。

    入口与状态传播

    主窗口把消息列表选中事件接到 CenterWidget::setMessage(),CenterWidget 在首次选择时惰性创建详情页。DetailWidget::setMessage() 用 source/address 组成的 MessageId 查找或新建标签,选择标签后同步 SignalView、BinaryView 和 LogsWidget,最后刷新标题/告警。TabBar::setCurrentIndex() 会同步发 currentChanged,回调再次进入 setMessage() 时由相同 ID 的早退门避免重复更新。

    函数 主要职责 边界
    findOrAddTab() 以 MessageId::toString() 作为标签文本和查找键,新增时填充消息名 tooltip。 tooltip 只在新增时设置;DBC 改名后 refresh() 更新标题,不同步重写旧标签 tooltip。
    restoreTabs() 从会话字符串恢复当前 DBC 中存在的消息标签,再尝试恢复活动项。 fromString() 解析失败退回 0:0;只查 DBC 存在性,坏字符串可能别名到 0:0;未知 DBC 消息不会恢复。
    refresh() 重算标题、删除按钮可用性和告警。 它不是所有子组件状态的唯一刷新器;信号视图/历史页各自响应 DBC、撤销事件。
    updateState() 若到达集合包含当前 ID,更新当前显示的位图或历史页,再重算告警。 未显示页不在该调用中增量更新;切换到 Logs 页时另调用 onShown()。

    标签关闭请求由 DetailWidget 构造连接直接交给 TabBar::removeTab()。若关闭最后一个标签,TabBar 发 currentChanged(-1);详情页的回调忽略负索引,也未清空 msg_id_ 或销毁 CenterWidget。因此最后一个标签关闭后,详情内容及序列化的活动消息 ID 可能仍指向旧项——这是源码可见的状态边界,实际 GUI 表现待验证。初始 msg_id_ 为默认 0:0,setMessage() 的相同 ID 早退也使首次选择真实 0:0 时不能建立标签。

    告警不是协议证明

    updateWarnings() 仅在当前 ID 能找到 DBC 消息时检查三类问题:没有收到该 source/address 的帧、DBC 长度与最新帧长度不同、以及 BinaryView::getOverlappingSignals() 返回的位重叠。没有 DBC 消息时不生成这些告警。重叠集合受当前可见复用分支和 Normal 类型筛选影响,不能当作完整 DBC 冲突审计。

    告警会检查 告警不会检查
    该 source/address 是否收到帧 checksum 是否正确
    DBC 长度与最新帧长度是否一致 计数器连续性与回绕
    当前可见信号是否位重叠 时间戳新鲜度
    —— 车辆拓扑与 panda safety

    热力图菜单有 Hidden、Live 和当前范围/All 三态;timeRangeChanged 回调在共享范围变化时更新文字,并让无范围的直播流进入 live 统计模式。需注意 Hidden 是布局可见性,不等于清掉已有统计。热力图菜单也只影响显示与统计模式,不会修改原始 CAN 帧。

    消息编辑与撤销栈

    editMsg() 以已有 DBC 长度或最新帧长度初始化非阻塞 EditMessageDialog。弹窗的 Name、Size、Node、Comment 字段分别使用字符过滤/数值输入/多行文本;只有用户改动 Size 输入时才把数值夹到 1..CAN_MAX_DATA_BYTES。若初始长度为 0 而用户不碰该输入,静态代码未再次夹取;是否可经完整 GUI 流程提交 0 需验证。

    validateName() 大小写不敏感地禁止保留名 untitled,非空且修改过时检查当前 source 的 DBC 重名;空字符串没有被此函数判为无效,nameValidator 只过滤新输入字符、允许删空字段。result() 会 trim 名称/节点/注释,DetailWidget::draw() 在弹窗返回「已接受」后才压入 EditMsgCommand。

    更细的一条边界:EditMsgCommand::undo() 以 old_name.empty() 判断原消息是否不存在。若先把已有消息改为空名称,再编辑一次,后一条命令的撤销会按「新建消息」路径移除消息。删除按钮另走 RemoveMsgCommand。DBCFile::updateMsg() 直接保存传入名称和长度到内存对象,没有补充非空名称验证。这些都是内存 DBC 编辑,磁盘保存是另一条链。

    插件与硬件边界

    若把详情页拆成插件,消息选择、可见统计、DBC 编辑和安全诊断应分成不同能力。编辑插件输出版本化 DBC 草稿和校验结果,不能把「当前无告警」解释为硬件接线或发送规则安全——告警本就不检查 checksum、计数器、新鲜度与 panda safety。插件宿主应统一校验名称非空、CAN 长度范围、source/address 身份和提交事务,再让只读分析视图消费已确认的版本;硬件采集插件保留原始帧及时间戳,不依赖 UI 当前选项。

    常见问题

    Cabana 详情页没有告警,是否说明报文没问题?

    不是。updateWarnings() 只检查「是否收到帧」「DBC 长度与最新帧长度是否一致」「当前可见信号是否位重叠」三类;它不检验 checksum、计数器、时间戳新鲜度、车辆拓扑或 panda safety,且重叠集合受可见复用分支和 Normal 类型筛选影响,不能当作完整 DBC 冲突审计。

    为什么关闭最后一个标签后详情还可能指向旧消息?

    因为关闭请求直达 TabBar::removeTab(),关闭最后一个标签时 TabBar 发 currentChanged(-1),而详情页回调忽略负索引,也未清空 msg_id_ 或销毁 CenterWidget。于是详情内容及序列化的活动消息 ID 可能仍指向旧项。这是源码可见边界,实际 GUI 表现待验证。

    编辑消息时把名称删空会发生什么?

    validateName() 不把空字符串判为无效,nameValidator 也允许删空字段,DBCFile::updateMsg() 更不会补非空校验。更麻烦的是 EditMsgCommand::undo() 以 old_name.empty() 判断原消息不存在:先改空名再编辑一次,后一条命令的撤销会按「新建消息」路径移除消息。

    Cabana 能直接向车上发 CAN 或授权安全模式吗?

    不能。这页是 DBC 分析/编辑界面,源码明确它不提供车端 CAN 发送或安全模式授权。它做的是内存 DBC 编辑与显示统计,磁盘保存是另一条链;插件化时也应把编辑、统计与安全诊断拆成不同能力。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CABANA_DETAIL_MESSAGE_TABS_EDIT_WARNING_STATE_DEEP_DIVE.md(取用了详情页入口与标签同步、告警三类检查、编辑弹窗与 EditMsgCommand 撤销语义、插件与硬件边界)。
    • 项目 02 —— openpilot-research/CABANA_DBC_UNDO_MULTIFILE_DIRTY_CONTRACT_DEEP_DIVE.md(仅用于确认命令栈专题的接续关系,未取用其未公开内容)。

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

    更新记录

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

  • pandad 原生 CAN 编解码与健康回执:短读、下溢与 512 字节缓冲

    结论:原生 pandad 的 Panda C++ 封装把「CAN 编解码」「健康回执」「命令确认」拆成三层,但三层的校验并不对称——发送路径用 assert 而非显式长度校验,接收路径存在短读下溢,健康结构接受零字节/短读。因此「sendcan 收到」「SPI 交易成功」「MCU 已入队」「安全钩子放行」是四个不同观测点,任何一个都不能替代另一个。

    位置与调用链:从 sendcan 到 6 字节帧头

    Panda 是 pandad.cc 与 PandaSpiHandle 之间的 C++ 设备封装,而 can_header 把总线、DLC、拒绝/回环位、29 位地址和 XOR 校验压成 6 字节。构造函数创建 SPI 句柄、读取板型,再用 0xc0 清通信状态。

    发送路径从 sendcan 取消息并做一秒年龄门,再调用 can_send();接收路径从 can_receive() 得到帧,包装为 can 事件,并把 bool 结果写入事件 valid。send_panda_states() 消费健康结构,按 ignition/onroad 调整安全模式和省电,心跳由后续的 process_panda_state() 发送。封装本身不是独立的 MCU 安全策略,它只是主机侧的一段设备协议。

    函数/组件 作用与边界
    Panda::Panda()、get_hw_type() 构造 SPI 句柄、查板型、复位通信;板型读未核对返回长度。
    get_state()、get_can_state() EP0 读取 health_t/can_health_t,负值才返回空值。
    get_firmware_version()、up_to_date() 两次 64 字节读拼签名,与本地签名文件尾部比较;这不是运行中固件完整性远程证明。
    len_to_dlc()、pack_can_buffer() 过滤非本 Panda 总线,编码 DLC/头/XOR;批次达到 256 字节后调用写回调。
    can_send() 把批次写向 bulk 端点 3,未向调用方返回实际写入量或错误。
    can_receive()、unpack_can_buffer() 从端点 0x81 读最多 16 KiB,保留未完整帧;XOR 失败清接收缓存并请求 MCU 复位通信。

    CAN 字节流边界:512 字节缓冲与 256 字节软阈值

    发送缓冲为 2 * USB_TX_SOFT_LIMIT(512 字节),每个合法 CAN FD 帧至多 6 + 64 字节,写后超过 256 字节就清零位置。因此在长度被限制为 64 的前提下,单次追加不超过缓冲。

    但当前的长度检查只有 assert:代码先把 can_data.size() 传给接受 uint8_t 参数的 len_to_dlc(),再断言 <=64 及长度与 DLC 表相符。在关闭断言的构建中,超长或不能由 CAN FD DLC 表表示的负载不被拒绝,随后的 memcpy 仍按原始 can_data.size() 写进 512 字节栈缓冲。即使保留断言,这也是进程终止而非可恢复的输入错误,必须先做显式长度/DLC 校验再写缓冲。

    另一点常被误读:src 在 cereal CanData 中已经是 UInt8,因此不能把 C++ 局部 uint8_t 赋值本身当作独立的源总线截断漏洞;bus < 4 才是此封装对可发送总线的实际门。

    assert 不是输入校验:短读下溢与半包

    接收循环的条件是 pos <= size - sizeof(can_header),其中 size 为 uint32_t 而 sizeof 为无符号 size_t。当本次读取只得到 1–5 字节且之前没有半包时,减法下溢,循环仍可能进入并从不足 6 字节的数据中拷贝头,之后按头部 DLC/长度判断和移动缓冲,属于条件性越界/错误解析风险。SPI bulk 读允许返回实际收到的字节数,不能假设总是帧头以上。改法是先判断 size - pos >= sizeof(can_header),且只在 pos <= size 不变量成立时做减法。

    头中的 DLC 为 4 位,查 16 项表本身有边界;完整帧的 XOR 校验失败会清缓存,但已在同一轮压入 out_vec 的先前帧不会回滚,上层会发布 valid=false 的事件。接收缓存大小为 RECV_SIZE + sizeof(can_header) + 64,can_receive() 只用 assert 检查剩余容量再读 16 KiB;正常解析最多保留一个未完成帧,但不应依靠断言证明这个不变量。

    健康回执与命令确认的分离

    get_state()、get_can_state() 把结构体零初始化,但只以 err >= 0 接受控制读。SPI 层返回实际字节数,因此零字节或短读也可能被包装成「存在的健康结构」;send_panda_states() 只检查 optional 是否存在,随后据这些字段判断点火、模式及省电。get_serial() 同样接受非负短读;get_firmware_version() 则要求两次恰好 64 字节;风扇 RPM 与板型读没有长度门。这里属于主机读取契约风险,不能推断正常 MCU 必然会产生短响应。

    安全模式、心跳、波特率、IR、电源等控制写方法为 void,未检查 SPI 交易返回值;can_send() 也丢弃 bulk_write 的字节数/错误。这正是必须区分四类观测点的原因:

    观测点 它到底证明了什么
    sendcan 消息收到 主机侧已提交发送请求,仅此而已。
    SPI 交易完成 字节写到了端点;返回值可能被调用方丢弃。
    MCU 入队 需固件侧回执,封装层不提供确认。
    安全钩子放行 由 panda 固件独立许可,主机侧无从得知。

    自制硬件与插件重构的最低契约

    设备插件可以输出带设备身份、采样时间、实际字节数及逐字段有效位的只读健康数据;可替换的 CAN 观测解码器须显式声明帧头、DLC、校验、总线编号和分块恢复契约。发送则由单一受信任网关持有能力:插件只提交候选帧,网关在序列化前校验载荷长度/总线/地址,实施年龄门、资源上限和安全策略,并分别报告「接收请求、SPI 完成、MCU 入队、被安全拒绝」等可观测结果。安全模式、心跳和复位绝不开放给普通插件直接调用。

    离线单测应优先覆盖 0–5 字节短头、分块恰在头中间结束、CAN FD 合法长度与非法 9/65/超大长度、非本 Panda 总线过滤、坏 XOR 前已有好帧、健康结构短读,以及 bulk 部分写/负返回。随后在无车台架以 ASan/UBSan、故障注入和 MCU 回执核验;真实车辆执行控制还需车型安全钩子与接管测试。

    常见问题

    为什么「sendcan 收到」不能当作 MCU 已发送?

    因为 can_send() 丢弃 bulk_write 的字节数与错误,控制写方法为 void;sendcan 只是主机侧入队请求。SPI 交易成功、MCU 入队、安全钩子放行是另外三个独立观测点。

    发送超过 64 字节的 CAN FD 负载会被拒绝吗?

    当前长度检查只有 assert。在关闭断言的构建中不被拒绝,memcpy 仍按原始长度写进 512 字节栈缓冲;即使保留断言,这也是进程终止而非可恢复的输入错误。

    为什么 1–5 字节的 SPI 读取会导致帧头解析错误?

    因为接收循环用无符号的 size 减去 sizeof(can_header),短读时减法下溢,循环仍可能进入并从不足 6 字节的数据中拷贝头。改法是先判断 size - pos >= sizeof(can_header)。

    健康结构读到零字节意味着什么?

    get_state()、get_can_state() 只以 err >= 0 接受控制读,零字节或短读也可能被包装成「存在的健康结构」,send_panda_states() 只检查 optional 是否存在,因此不能把结构存在当作字段有效。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PANDAD_NATIVE_PANDA_CAN_CODEC_HEALTH_CONTRACT_DEEP_DIVE.md(取用了 CAN 字节流边界、短读下溢、健康回执与命令确认分离的静态结论)。
    • 项目 02 —— openpilot-research/PANDAD_NATIVE_SPI_RESPONSE_BOUNDS_RETRY_CONTRACT_DEEP_DIVE.md(仅用于确认底层 SPI 响应长度与重试契约的边界)。

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

    更新记录

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

  • opendbc 车型校验算法族:同名 CRC8 不可替换,RX/TX 共用函数

    结论:opendbc 的车型校验不是「一种 CRC8 走天下」,而是按品牌/平台硬绑定的一族函数:Honda 用半字节补码、Toyota 把帧长本身算进和、Chrysler 用 CRC8 J1850(初值与末异或均为 0xFF)、Hyundai CAN FD 用 CRC16 XMODEM、VW MQB/MEB 用 CRC8 H2F 再追加地址常量。同名「CRC8」只代表多项式查找表相同,地址、帧长、字节顺序与最终 XOR 各不相同,不构成车型间可替换的插件接口。更关键的是,RX 校验与 TX 生成共用同一个函数,任何「只改发送端」的假设都必须重新核对。

    每个校验函数都写死了覆盖字节与校验位位置

    在固定源码快照里,get_checksum_state() 按 DBC 名称与信号名把每帧绑定到一个具体函数。所谓「算法」其实由四件事共同决定:哪些字节进入计算、校验字段在哪个字节或半字节、初始值与末异或是什么、帧长是否参与。源文档明确指出,帧长度和字段位置本身就是协议的一部分,不能当作可省略的装载细节。

    下面的函数目录来自源文档的逐函数静态核对。注意「参与计算的内容」一列——同一种「CRC8」在 Chrysler、VW、body 三处覆盖的字节范围完全不同。

    家族 / 函数 实际参与计算的内容 重要前提
    honda_checksum() 地址各半字节、每个载荷字节的高低半字节;末字节仅取高半字节,结果为 8 - sum,扩展 ID 再加 3,截 4 位。 假定校验位在最后字节低半字节;扩展地址判据是 address > 0x7FF。
    toyota_checksum() len(d)、地址各字节和除最后一字节外的载荷字节,相加截 8 位。 假定校验在末字节,帧长本身参与计算。
    subaru_checksum() 地址各字节与除第一字节外的载荷,相加截 8 位。 Global 版本校验位默认在首字节;和 pre-global 手动辅助函数不是同一算法。
    chrysler_checksum() 对末字节以外数据查 CRC8 J1850 表,初值和末异或均为 0xFF。 不把地址计入此函数;checksum 按末字节处理。
    fca_giorgio_checksum() 同表、初值 0,末异或按地址 0xDE/0x106/0x122/其他 分别为 0x10/0xF6/0xF1/0x0A。 未识别地址落入通用 0x0A 分支;不能把四组样本扩大为所有 FCA 报文。
    hkg_can_fd_checksum() 从第 2 字节到末尾做 CRC16 XMODEM,再加入地址低、高字节;仅 8/16/24/32 字节追加特定末异或常量。 前两字节视作校验字段;其它长度不加表中常量,但函数自身不会拒绝。
    volkswagen_mqb_meb_checksum() 初值 0xFF,对第 1 字节起载荷做 CRC8 H2F;已知地址再按第二字节低半字节选 16 项常量追加一步,最终异或 0xFF。 首字节为校验位;未在常量表中的地址跳过附加常量步骤。
    tesla_checksum() 地址低、高字节加上除 checksum 字节外的所有载荷,截 8 位。 校验位置由 sig.start_bit 决定,不限固定末字节。
    body_checksum() 从倒数第 2 字节向首字节反向迭代做 CRC8 BODY,初值 0xFF。 仅最后一字节不进入计算,函数不检查其字段含义。
    byd_checksum() 前七字节之和按位取反再截 8 位。 固定覆盖前七字节;不是通用「跳过任意校验字段」的算法。
    psa_checksum() 地址选择 4 位初值,先在传入 bytearray 中清除校验半字节,再把全帧高低半字节求和,从初值中减去并截 4 位。 函数就地修改传入缓冲;RX 解析器传副本,TX packer 传当前构帧缓冲。

    CRC8H2F、CRC8J1850、CRC8BODY、CRC16_XMODEM 只是按多项式生成的查找表。查找表可以共享,但函数外层的地址、帧长、字节顺序和最终 XOR 无法共享,这正是「同名不同义」的根源。

    RX 校验与 TX 生成共用同一个函数

    源文档反复强调一个契约:RX 验证和 TX 生成走的是同一份实现,而不是「接收一套、发送另一套」。这带来两个直接后果。第一,校验位的位置往往由信号元数据决定,例如 tesla_checksum() 的校验位置由 sig.start_bit 决定,并非固定在末字节。第二,某些函数带有副作用,例如 psa_checksum() 会就地清除并回写传入缓冲里的校验半字节。

    副作用的差别决定了移植风险:接收解析器传入的是 bytearray(dat) 副本,因此就地修改不影响原始帧;而发送打包器传入的是当前工作缓冲。把这类函数当成无副作用纯函数重用,会直接改变构帧结果。同时,VW MLB 的 XOR 变体以 sig.start_bit // 8 定位整字节,说明「校验在哪个字节」不是全局常量,而是逐信号给定的。

    按「接收帧已有校验字节」选择变体,而不是按算法优劣

    VW MEB 提供了一个容易被误读的变体函数 volkswagen_meb_alt_crc_checksum():它对备用表内六个地址先按指定长度算另一组 CRC,只有候选值等于输入 d[0] 时才返回,否则退回标准 MQB/MEB CRC。这个语义是按接收帧里已经带着的校验字节来「认领」变体,并不代表发送端会主动优先采用备用值。

    源文档用三个隔离探针把边界固定下来,结果如下表。它们都使用原版函数与真实查找表,但只在纯函数层面运行,未加载完整车型 DBC、未发 CAN、未测 ECU。

    探针 输入 观察结果 结论边界
    MEB 双 CRC 选择 地址 0x0DB、64 字节零缓冲 备用长度 42,备用结果 0xEB、标准结果 0xED;d[0]=0 时选 0xED,改成 0xEB 时才选 0xEB CANPacker.pack() 默认从零缓冲开始,故不能凭接收端可识别备用 CRC 就推断发送端会按备用算法生成
    Hyundai 长度分支 长度 8/12/16/24/32 的全零缓冲 各长度均给出结果;12 字节路径不会被拒绝,只是不使用四种已列长度常量 仅验证函数分支,不代表 12 字节是该平台合法帧长
    PSA 就地修改 0x452、start_bit=0、输入 AB CD 返回校验半字节 1,同时把首字节原地改为 A0 移植时不能把该函数当无副作用纯函数

    把这几条合起来看:一个「接收端能认」的校验并不自动意味着「发送端会这么写」。当前实际发送函数是否涉及 MEB 那六个地址,需要按车型另做核对,本篇探针不说明已有车型发生过错误发送。

    「跳过校验字节」不是通用能力

    Subaru 有两个函数常被混为一谈:subaru_checksum() 是 Global 版本,校验位默认在首字节;subaru_preglobal_checksum() 则先用 packer 生成字节,再对除 checksum_byte 指定字节(默认第 7 字节)外的字节求和。后者由 pre-global 构帧函数显式填写 Checksum,不由 DBC.get_checksum_state() 自动绑定。同理,byd_checksum() 固定覆盖前七字节,也是一种特定布局,而不是「跳过任意校验字段」的通用规则。

    这些差异说明:校验算法必须和 DBC 名称、报文地址、DLC、校验字段位位置、覆盖字节范围、counter 位置一起版本化。只声明「用了 CRC8」在车型之间没有任何可迁移性。

    给协议插件的验收底线

    新车型的协议插件应把下列字段作为一个整体来管理和验收,缺一项都可能造成接收能过、发送写错,或反之。

    必须版本化的字段 为什么不能省
    DBC 名称、报文地址、DLC 决定帧长与字段位置,帧长本身参与部分算法(如 Toyota)。
    校验字段位位置、覆盖字节范围 Tesla 由 sig.start_bit 决定、Honda 在低半字节,不能固定假定末字节。
    counter 位置 与 checksum 同级参与健康判定,不能只看数值。
    CRC 多项式 / 初值 / 末异或 Chrysler 与 FCA Giorgio 同表不同初值与末异或。0x10/0xF6/0xF1/0x0A
    长度变体与 RX/TX 选择规则 MEB 备用 CRC 只在匹配接收字节时启用;Hyundai 只在 8/16/24/32 追加常量。

    测试层面,应在参考载荷上直接比对计算值与输出字节,并确认解析器确实接受本帧;还要覆盖坏 checksum、截短/超长帧、counter 回绕与未知地址。需要提醒的是,checksum 只检测某类传输错误,不提供发送身份认证,也不构成执行器安全约束——这些仍由可信的车型逻辑、panda safety 和物理台架验收承担。

    常见问题

    为什么同为「CRC8」,不同车型的校验函数不能互相替换?

    因为查找表相同不代表算法相同。Chrysler 用 CRC8 J1850 且初值与末异或均为 0xFF,VW MQB/MEB 用 CRC8 H2F、初值 0xFF 并追加地址常量,body 则从倒数第二字节反向迭代。地址、帧长、字节顺序和最终 XOR 各不相同,源文档明确指出相同「CRC8」名称不构成车型间可替换的插件接口。

    接收端能校验通过的备用 CRC,发送端也会照此生成吗?

    不会自动如此。MEB 备用函数只有候选值等于输入 d[0] 时才返回,否则退回标准 CRC,这是「按接收帧已有校验字节选择变体」的语义;而 CANPacker.pack() 默认从零缓冲开始,不会凭接收可识别就优先采用备用值。当前发送函数是否涉及那六个地址需按车型另核。

    PSA 校验函数可以直接当纯函数复用吗?

    不可以。源文档探针显示,psa_checksum() 对 0x452、start_bit=0、输入 AB CD 返回校验半字节 1 的同时,把首字节原地改成了 A0。接收解析器传的是副本,发送打包器传的是原始工作缓冲,移植时必须区分。

    checksum 通过是否等于报文安全可信?

    不等于。checksum 只检测某类传输错误,不提供发送身份认证或执行器安全约束。源文档明确这些仍由可信的车型逻辑、panda safety 和物理台架验收承担,并且本专题没有实板证据。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/OPENDBC_CHECKSUM_ALGORITHM_FAMILY_RX_TX_CONTRACT_DEEP_DIVE.md(取用了校验函数目录、RX/TX 共用函数的语义、三个隔离探针结果与插件验收字段清单)。

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

    更新记录

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

  • tinygrad C 风格渲染器:指针重解释、门条件与调用可信边界

    结论:tinygrad 的 C 风格渲染层(CStyleLanguage)不是「读任意计算图再生成代码」的入口,而是一道下游文本生成器:它只消费已完成范围展开、类型与算子降解、门搬移和线性化的 UOp 图,把模式表映射成 C 风格控制流、指针与类型声明。两个前提必须同时成立——门条件不能在这一层丢失,CALL 与 CUSTOM 必须当成可信编译输入边界。渲染成功既不证明指针可安全解引用,也不授予 CAN 发送权。

    渲染调用链:这一层消费的是「已完成」的图

    入口是 do_render():它对已线性化的 UOp 调用 renderer 的 render()。CStyleLanguage.render() 先经 _render() 生成内核体、名字与参数表,再由 render_kernel() 拼出源码;随后 do_compile() 把源码交给 compiler.compile_cached()。这是一条单向链路:上游做结构变换,这一层只做文本落地。

    关键在于它不能直接接收任意前端 UOp。范围展开、类型/算子降解、门搬移和线性化都必须在进入这一层之前完成;如果未匹配的模式漏了进来,_render() 会断言失败,而不是静默忽略。也就是说,本层对「输入是否已合法」是有断言的,但对「生成的指针是否真的可访问」没有保证。

    Clang、OpenCL、Metal、CUDA、HIP 与 QCOM 等子类都基于这一通用基类提供目标语法或能力。基类的 code_for_workitem 和 barrier 是空配置,正是留给具体后端填空的位置。

    模式表与关键函数

    下表列出这一层的核心组件及其行为前提。未匹配会在 _render() 断言失败,这是它与「静默降级」实现最本质的区别。

    函数或组件 行为与前提
    base_rewrite 用模式表把 RANGE/END/IF/ENDIF 渲染为 C 风格控制流,BARRIER 渲染为子类提供的 barrier 文本;CONST、CAST/BITCAST、INDEX/SHRINK、LOAD/STORE、ALU、WMMA、CALL 与 CUSTOM 也在此映射。
    CStyleLanguage 声明 kernel_typedef、地址空间前缀、code_for_workitem、code_for_op、向量语法与图像常量等可覆盖项。
    render_kernel() 用 SPECIAL 的局部维度上界计算 launch_bounds 并构造函数签名;有图像形状参数时附加固定的未归一化、边界钳制与最近邻采样器声明。只输出源码,不验证真实设备工作组资源。
    render_buffer() 为 BUFFER 生成局部/寄存器数组声明,LOCAL 可加对齐或共享内存前缀;数组长度来自 max_numel(),设备端资源上限仍需实际编译与启动验证。
    _render() 先统计子节点使用次数与被 STORE 指向的 PARAM;按 UOp 线性顺序命名临时变量,单消费者表达式多数直接内联,多消费者或控制节点发出语句并维护缩进。AFTER 仅取其值源名称。

    还有一组负责非原生浮点的函数:create_non_native_float_pats() 对不原生支持的浮点 dtype 在 ALU 前提交 weak 常量,通过 float 中间类型运算/比较;pm_manual_bf16_cast 与 pm_bf16_ushort_const 处理 BF16 位型转换及以 ushort 存储的常量文本。uops_to_dtypes() 与 wmma_args() 收集向量 dtype/宽度和 WMMA 形状,供子类生成 typedef、矩阵内建函数或额外声明。

    指针重解释、门条件与向量 ABI

    指针与访问类型的生成集中在 render_index()、_render_dtype()、render_type()、render_ptr() 和 render_access()。ALU 地址空间的索引按 C 数组或向量 lane 访问,其他地址空间返回缓冲指针加偏移;常量向量 lane 在小向量时可能变为 .xyzwabcd,阈值由 gep_arr_threshold 决定。

    环节 生成的文本行为 它不保证什么
    访问类型生成 按 lane 数选择向量访问类型。 不保证访问宽度与硬件对齐一致。
    向量指针重解释 向量访问宽于标量指针或访问 dtype 与缓冲 dtype 不同时,先重解释为目标指针再解引用。 不证明对齐、缓冲大小、别名或可访问性。
    图像类型选择 依据是否被写入选择 write_only/read_only image2d_t。 不校验真实采样规则与图像尺寸上限。
    门条件 LOAD base_rewrite 对有 gate 的 LOAD 生成条件表达式,避免 gate 假时实际求值访存。 普通 STORE 只接受无独立 gate 的形式,安全性同时依赖上游门搬移。

    这里有一条容易被忽略的两阶段依赖:更早的 pm_linearize_cleanups 把 gated STORE 拆为 IF-STORE-ENDIF,因此门的安全性同时依赖上游线性化和本层文本渲染。code_for_op 中的 CDIV/CMOD 是 C 的截断语义,FLOORDIV/FLOORMOD 的负数补偿已由前序降解处理,不能在后端直接合并为不等价指令。

    CALL 与 CUSTOM 是可信编译输入边界

    CALL 通过函数指针强制转换生成调用文本,CUSTOM/CUSTOMI 用 UOp 参数字符串格式化源码。源码文档明确把它们定义为可信编译输入边界,不应开放给未经审核的热插拔插件或远程数据。新后端应限制可用模板、绑定函数 ABI,并保留生成源码及二进制工件供审计。

    同样地,render_ptr() 的强制类型转换要求调用方保证对齐与长度;NV12/warp 输入缓冲不能仅凭生成的指针表达式就认定可安全读取。若后端仍保留未处理的 Invalid、门条件或不支持的 ALU,编译失败或运行时越界都必须使模型不可用,而不是继续向控制链输出「有效」观测。

    自制 C 风格后端的验收清单

    综合上述,自制 C 风格后端至少须定义线程/工作组坐标、真实屏障、GLOBAL/LOCAL 指针与向量 ABI、图像读写与采样规则、支持的 dtype/算子,以及 compiler。验收时可对照下表逐项确认边界来源。

    验收项 必须显式声明 来源边界
    坐标与工作组 线程/工作组坐标、launch_bounds 依据 基类默认空配置,须目标语言补齐。
    同步 真实屏障文本 基类 barrier 为空。
    指针与向量 GLOBAL/LOCAL 前缀、向量 ABI、对齐要求 render_ptr() 只做文本转换。
    图像 读写方向、采样规则、尺寸上限 render_kernel() 只发声明。
    可审计性 生成源码与二进制工件留存 CALL/CUSTOM 为可信输入边界。

    渲染成功不授予 CAN 发送权——车型控制与 panda 固件 safety 仍是独立边界。验证层面,源码作者用 PYTHONPATH=. DEV=PYTHON 的最小 UOp 编译探针确认:CStyleLanguage(Target()) 对普通 float LOAD/ADD/STORE 输出参数指针和赋值源码;以输入值比较作为门的 STORE 输出 if (...) { ... }。首次尝试用裸 UOp.variable() 构造 gate 被 spec_program 拒绝,改用真实 LOAD 比较后才通过——失败样例不能计为有效通过。

    常见问题

    能直接用 tinygrad 的 C 风格渲染器写自己的 GPU 后端吗?

    可以作为起点,但基类只提供模式表与可覆盖项,code_for_workitem 和 barrier 默认是空配置。自制后端必须补齐线程/工作组坐标、真实屏障、GLOBAL/LOCAL 指针与向量 ABI、图像读写与采样规则、支持的 dtype/算子,以及 compiler,否则生成的源码不具备可执行前提。

    为什么渲染成功不代表能发出 CAN?

    因为渲染层只输出源码文本,render_kernel() 也只发声明、不验证真实设备工作组资源。源码文档明确:渲染成功不授予 CAN 发送权,车型控制与 panda 固件 safety 是独立边界。编译通过只是计算路径的前一步,不影响车辆控制与安全许可。

    指针强制类型转换后读内存安全吗?

    不安全。render_ptr() 的向量指针重解释只是文本转换,源码文档说明它不证明对齐、缓冲大小、别名或可访问性,并要求调用方保证对齐与长度;NV12/warp 输入缓冲不能仅凭生成的指针表达式认定可安全读取。

    CALL 与 CUSTOM 能不能给热插拔插件或远程数据使用?

    不建议。源码把 CALL(函数指针强制转换)与 CUSTOM/CUSTOMI(按 UOp 参数字符串格式化源码)定义为可信编译输入边界,不应开放给未经审核的热插拔插件或远程数据;新后端应限制模板、绑定函数 ABI,并保留生成源码与二进制工件供审计。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_CSTYLE_RENDERER_POINTER_GATE_CALL_TRUST_CONTRACT_DEEP_DIVE.md(取用了 base_rewrite 模式表、指针/图像类型生成、门条件两阶段依赖、CALL/CUSTOM 可信边界与最小 UOp 编译探针结论)。
    • 项目 02 —— openpilot-research/TINYGRAD_RENDERER_CAPABILITY_ESTIMATES_LAUNCH_METADATA_DEEP_DIVE.md(仅用于确认渲染能力专题的接续关系,未取用其未公开内容)。

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

    更新记录

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

  • 仿真相机与传感器:图像缓冲、采样节拍及有效位

    结论:仿真感知链适合状态机和流程回放,但不能替代传感器校准、时钟同步、panda 安全钩子或物理总线测试。发布时戳不是物理曝光时戳——仿真把同一个 logMonoTime 同时当作 VisionIPC 的 SOF 与 EOF;重复样本也不等于传感器频率——主循环每圈发 5 条加速度、5 条陀螺仪与 10 条 GPS。所有频率推导均以 SimulatorBridge 主循环达到标称 100 Hz 为前提,实际频率未测。固定快照基线为 c8fb906。

    图像从 MetaDrive 到 VisionIPC

    MetaDriveBridge.spawn_world() 为窄路相机及可选广角相机配置 RGB 图像源,分辨率取 W=1928, H=1208。MetaDriveWorld 用 multiprocessing.Array 暴露两块 RGB 内存;metadrive_process() 把渲染图像写入共享数组后对 image_lock.release() 计数;桥接侧另有目标 20 Hz 的相机线程,在 send_camera_images() 开头 acquire() 一次,再把当前数组转为 NV12 并发布。

    这里的 image_lock 实际是初值 0 的 multiprocessing.Semaphore,用于等待至少一帧,而非读写互斥:生产者写图前不获取它,消费者转换 RGB 时也不阻止下一次写入。因此静态上存在读取一幅图像过程中被生产者覆盖的窗口;若生产速度超过消费者,semaphore 计数积压,消费者可能对同一块「最新图像」连续取样。是否出现撕裂或重复帧需要像素哈希 / 帧号探针验证。

    环节 实现事实
    色彩转换 rgb_to_nv12() 用整数 BT.601 近似系数生成 Y,2×2 box filter 后交错 U / V;要求偶数宽高;调用方固定断言 1928×1208×3 且 uint8。
    缓冲 Camerad.__init__() 为每路建立 5 个 VisionIPC 缓冲;窄路总建,广角按 dual_camera 决定。
    发布 _send_yuv() 先构造外层 Event.valid=True 的 *CameraState,把同一个 dat.logMonoTime 同时当作 SOF / EOF,再发布独立递增的仿真帧号和单位变换矩阵。
    有效位 底层 VisionIpcServer.send() 另把 extra.valid 固定为 False;CameraState 外层有效位与 VisionIPC extra 有效位不同。

    三项原类探针确认单路只分配窄路五个缓冲、双路帧号分别从零递增、图像发送先于状态消息发布,且每帧 SOF = EOF。它是仿真 RGB 走 CPU 拷贝的路径,不证明真实 ISP 的色彩、曝光或 DMA 缓存行为。探针还确认:若假 VisionIPC 已接受图像而假 PubMaster.send() 抛错,cam_send_yuv_road() 尚未递增帧号;调用方若捕获异常并再次调用,会用同一帧号发送另一份图像、但产生新时间戳——这是函数边界的部分提交状态,不证明现有桥接线程会自动重试。

    IMU、GPS、驾驶员与外设占位

    SimulatorBridge._run() 以目标 100 Hz 的主循环调用 SimulatedSensors.update(),另起目标 20 Hz 相机线程。每次 update() 中,send_imu_message() 连续发布 5 条加速度和 5 条陀螺仪消息,数据都取同一份 SimulatorState.imu,各条的内层 timestamp 使用各自消息的 logMonoTime,并非模拟器采样时刻。

    消息 仿真节拍与标称值
    IMU(加速度 / 陀螺仪) 每圈各 5 条;100 Hz 主循环下约 500 条 / 秒 / 主题;服务表标称 104 Hz 不是实际硬件节拍。
    GPS 仅在 simulator_state.valid 时,每圈发布 10 条 gpsLocationExternal,约 1000 条 / 秒;服务表标称 10 Hz。
    驾驶员状态 update() 以 DT_DMON/2 检查驾驶员消息,以 0.25 秒检查外设状态,并非严格 20 / 4 Hz 定时器。

    send_gps_message() 把当前系统墙钟填成 unixTimestampMillis、固定 flags=1、精度常量和 source=ublox;这些重复包不能用于证明 GNSS 热启动、时钟校准、观测协方差或实际链路吞吐。send_fake_driver_monitoring() 把两侧脸概率置 1、分心置假、注意力置满;driverMonitoringState 外层显式有效,而 driverStateV2 使用 new_message() 默认外层 valid=False。不可把这些「健康」字段当作自研传感器或驾驶员监测的物理验证。

    假时钟探针还确认两个节拍门都使用严格 >:恰好到 0.25 秒时不发外设状态,到 0.251 秒才发;只有发送后才更新对应的上次发布时间。无效仿真状态并不阻止驾驶员 / 外设占位消息,且 driverStateV2.valid=False 与 driverMonitoringState.valid=True 会同时出现。

    退出、测试和插件化边界

    仿真启动脚本 launch_openpilot.sh 设置 NOBOARD=1、SIMULATION=1,屏蔽真实 camerad、encoderd、loggerd 等进程。主循环、相机线程和 MetaDrive 子进程的生命周期分散在桥接层;相机线程的 Semaphore.acquire() 没有超时,模拟器不再产帧时单靠线程 exit_event 不能立即唤醒它。现有 test_driving() 被 unittest.skip 标记,不能据此声称当前闭环已通过。

    自研硬件插件应分别验证图像采样帧号、读出时戳、消息外层 / 内层有效位、stride 与缓冲持有期,以及 IMU / GNSS 的独立采样时刻和真实频率。建议先在隔离命名空间记录每路源帧号、共享内存哈希、发布时戳和消费时戳,再注入掉帧 / 停发 / 速率偏差。

    常见问题

    仿真里 SOF 和 EOF 为什么不等于曝光时间?

    因为 _send_yuv() 把同一个 dat.logMonoTime 同时当作 VisionIPC 的 SOF 与 EOF,表示「在软件发布时打的同一时刻」,不是传感器曝光 / 读出测量。它是仿真 RGB 走 CPU 拷贝的路径,不证明真实 ISP 行为。

    「CameraState 有效」和「图像有效」是一回事吗?

    不是。仿真里 *CameraState 外层 Event.valid=True,而底层 VisionIpcServer.send() 把 extra.valid 固定为 False。两者是不同层级的有效位,必须分别检查。

    仿真里 IMU 和 GPS 的频率是多少?

    取决于主循环实际频率。每次 update() 发 5 条加速度、5 条陀螺仪与 10 条 GPS;若主循环确为 100 Hz,则约 500 条 / 秒 / IMU 主题、约 1000 条 / 秒 GPS,而服务表标称 IMU 104 Hz、GPS 10 Hz。重复样本不等于传感器频率。

    仿真通过能代替传感器标定吗?

    不能。仿真链适合状态机和流程回放,但不能替代传感器校准、时钟同步、panda 安全钩子或物理总线测试。若要验证自研硬件,应分别记录源帧号、共享内存哈希、发布 / 消费时戳,并注入掉帧、停发与速率偏差。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/SIM_CAMERA_SENSOR_TIMING_DEEP_DIVE.md(取用了图像缓冲与 semaphore 行为、IMU / GPS 仿真节拍、有效位与插件化边界分析)。

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

    更新记录

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

  • panda 健康包 ABI:版本哈希、字段映射与无损镜像的边界

    结论:panda 的健康包 ABI 里,「签名匹配」「ABI 兼容」「传输成功」「本轮采样完整」是四件不同的事,不能合并成一个布尔位。固件 0xd2 返回完整的 health_t(按 packed 字段相加为 48 字节),0xc2 对合法总线号返回 64 字节的 can_health_t;版本哈希只是头文件原文的 SHA-256 前 4 字节,不是抗恶意替换的签名。此外 pandaStates 并不是原始 MCU health 的无损镜像——temperature_pkt 在 C++ 映射中未进入 PandaState,total_tx_checksum_error_cnt 在 cereal 中没有对应字段。本文为固定源码快照 c8fb906 的静态阅读与离线探针,未连接实板、未做坏包注入。

    组件与函数索引

    入口 作用 边界
    health_t / can_health_t packed 二进制结构,按字段宽度当前分别为 48、64 字节 CAN health 正好占满 64 字节控制包上限
    get_health_pkt() / comms_control_handler() MCU 填充整机状态;三路 CAN 状态按请求总线号选择,响应结构体原始字节 0xc2 非法总线号不填充响应
    MCU 0xdd 与构建哈希 返回 health.h 和 opendbc/safety/can.h 的 SHA-256 前 4 字节(小端整数) 注释变化也可能改哈希
    Panda.connect() / get_packets_versions() 连接时读版本;短于 8 字节则记为 (0,0) 空包或短包返回零值,需上层甄别
    ensure_version() / health() / can_health() 被装饰的读/收发入口按本地与设备哈希拒绝不匹配;CAN health 用固定 struct 格式解码 不是所有 Python 方法在连接瞬间统一失败
    Panda::get_state() / get_can_state() C++ 直接按本地 sizeof 请求,返回值只检查非负,不要求读满 短读可能被解读为后部字段为零的有效快照

    ABI 与版本门的实际差异

    固件 0xd2 返回当前 health_t 的完整原始结构,0xc2 对合法总线号返回 can_health_t。编译期断言两者不超过 64 字节,但只约束固件本地缓冲容量,不说明不同主机版本的字段布局相同。版本哈希由头文件原文字节计算,仅忽略 CR,它检测构建所用头文件是否一致,注释变化也可能改哈希,且 32 位截断值不是抗恶意替换的签名。Python panda 在连接时读两个版本,再由 ensure_health_packet_version/ensure_can_packet_version 包装相关 API。

    主机路径 版本门行为 不保证的内容
    Python panda 连接读两个版本,包装相关 API 拒绝用不匹配结构解释健康包或发送 CAN 不是所有方法在连接瞬间统一失败
    原生 C++ pandad 不调用 0xdd 校验哈希;通常由 Python 包装器刷写/核验签名,connect() 再检查签名是否匹配本地文件 属镜像身份门,不是每次读健康包前的显式 ABI 协商;调试环境变量可跳过
    CAN_HEALTH_STRUCT 手写 64 字节格式 头文件变更不会自动改变 Python 布局,字段顺序仍需随固件同步维护

    因此,若自制板只实现同名控制端点却使用不同结构布局,或部署时镜像/头文件不配套,仅凭端点返回非负值不足以证明安全解释。

    Python 解析与失败边界

    _parse_c_struct() 从头文件寻找精确的 struct __attribute__((packed)) health_t { 文本,只接受单行的 uint8_t、uint16_t、uint32_t、float 标量声明,并用小端无填充格式生成 Struct,当前 health_t 得到 48 字节;声明行尾注释、数组或新类型会在导入 Panda 类时抛 ValueError,缺失精确结构头还会在字符串切分处失败。

    函数 行为 失败表现
    get_packets_versions() 用 0xdd 请求 8 字节,恰好收到 8 字节才解为两个小端 uint32 空包或短包返回 (0, 0)
    health() / can_health() 版本不符时在控制 I/O 之前抛 RuntimeError;随后 Struct.unpack() 要求长度恰好为 48/64 短读或超长响应抛 struct.error,没有「部分有效」字典
    health() 缩放 interrupt_load_pkt 除以 255,temperature_pkt 减去 40,标志位拆为布尔值 插件不能把缩放值与原始包字节混用
    can_health() LEC 四个 LEC 字段直接按 0 至 7 的字典查找 未知值抛 KeyError 而非返回未知状态
    总线号 Python 入口没有先限定总线号,把 int(can_number) 原样传给 0xc2 的 param1 固件只在总线号小于 3 时填包,正常空响应会使解包失败

    需要强调:Python 路径的失败表现与 C++ pandad 的短读零填充不同,不能用其中一条主机路径的成功条件推断另一条路径安全。C++ get_state() 与 get_can_state() 先清零结构体,随后只用 err >= 0 接受,因此短读可能被解读为「后部字段为零的有效快照」。这些是源码条件风险,并非正常固件必然短读。

    字段映射与信息损失

    fill_panda_state() 把点火、控制许可、安全模式、故障、供电、SPI 错误等字段转换到 PandaState;fill_panda_can_state() 处理 bus-off、LEC、错误计数、速率与 CAN FD 参数。当前 MCU health_t.temperature_pkt 在 C++ 映射中未进入 PandaState,而 Python health() 会转换为摄氏度;can_health_t.total_tx_checksum_error_cnt 在 cereal PandaCanState 中没有对应字段,也未发布。故 pandaStates 并不是原始 MCU health 的无损镜像,诊断插件不能据此假定所有板级指标可用。当 get_state() 或任一路 get_can_state() 返回负值时,send_panda_states() 不发布本轮消息;但短读由于上述接受策略仍可能发布。事件 valid 主要由句柄 comms_healthy 决定,不能替代每一字段的完整性/新鲜度证明。

    自制硬件与插件契约

    新网关应先交换独立的协议版本、结构布局/字段集 ID、设备 UID 与固件镜像身份;每个响应须核对实际长度恰好等于协商长度,再做解析。不要把「签名匹配」「ABI 兼容」「传输成功」「本轮采样完整」合并为一个布尔位。版本升级应有逐字段单位、缩放、缺测语义和向后兼容规则;若插件需要 MCU 温度或 TX checksum 错误计数,应明确新增消息字段或只读扩展通道,并保留原始值与时间戳。离线验收应覆盖 47/48/49 字节整机响应、63/64/65 字节 CAN 响应、同长度字段重排、版本不匹配、非法总线号、返回长度超过请求上限和字段缺测;上车前还需在无车台架用固件/主机版本矩阵验证拒绝策略、读回和安全默认态。

    常见问题

    版本哈希匹配能证明固件未被替换吗?

    不能。版本哈希由头文件原文字节计算并截断为 32 位,注释变化也可能改哈希,它是构建一致性的检查而非抗恶意替换的签名。

    C++ pandad 会像 Python 那样做健康包版本协商吗?

    不会按同样方式。原生 pandad 不调用 0xdd 校验哈希,connect() 检查的是签名是否匹配本地文件,属镜像身份门,不是每次读健康包前的显式 ABI 协商,且调试环境变量可跳过。

    短读的健康包会不会被当成有效快照?

    可能会。C++ get_state() 与 get_can_state() 先清零结构体、只检查返回值非负,因此短读可能被解读为「后部字段为零的有效快照」;这是源码条件风险,并非正常固件必然短读。

    pandaStates 是原始 MCU health 的完整镜像吗?

    不是。temperature_pkt 在 C++ 映射中未进入 PandaState,total_tx_checksum_error_cnt 在 PandaCanState 中没有对应字段,因此部分板级指标在 pandaStates 里并不可用。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PANDA_HEALTH_ABI_VERSION_HOST_MAPPING_DEEP_DIVE.md(取用了 ABI 与版本门差异、Python 解析与失败边界、字段映射信息损失、自制硬件契约与离线探针结论)。
    • 项目 02 —— openpilot-research/CAR_MESSAGES_FIELD_CONTRACT_DEEP_DIVE.md(仅用于确认健康字段进入上层消息有效位的位置)。

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

    更新记录

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

  • 温区采样与热状态机:阈值迟滞、驻车捷径与启动退场门

    结论:热状态不是「一过热就立刻停机」,而是分层的:新启动要求热状态低于 overheated,而已在行车只要求低于 critical,因此允许在 overheated 暂时滞留,直到达到 critical 才撤销在途资格。状态机按当前档位的严格小于/大于判断,每轮最多移动一档,精确等于阈值时保持原档;另有驻车捷径——设备未在行车且满足「本次未曾进入行车、没有驻车起点、驻车超过 5 分钟」之一时,不含 PMIC 的驻车复合温度超过 mici 的 85 °C 或其他设备的 75 °C 就直接设为 critical。本文为固定源码快照 c8fb906 的静态追踪与临时探针,未运行真实设备温升、风扇或热故障注入。

    关键函数与配置

    组件 实现行为
    ThermalZone.read() 首次按温区 type 名称发现编号,之后缓存编号;读取 temp 并按默认 1000 缩放;temp 不存在时返回 0
    ThermalConfig.get_msg() 遍历平台配置的单温区或列表形成各温度字段;各项顺序读取,没有共同时戳
    HardwareComma.get_thermal_config() 提供 8 个 CPU、2 个 GPU、DSP、DDR、2 个 PMIC 温区;mici 额外配置进气、排气、GNSS、底部 SoC
    FirstOrderFilter.update() 两条滤波器用 0.5 秒采样步长与 5 秒时间常数,首个样本直接成为状态,随后固定系数推进
    THERMAL_BANDS mici 与其余设备使用不同的升降温阈值
    hardware_thread() 热状态分支 计算两类复合温度,先更新风扇目标,再执行驻车危险覆盖或逐级状态转换
    阈值 mici 其他设备
    正常升级到 overheated 大于 100 °C 大于 96 °C
    overheated 升级到 critical 大于 107 °C 大于 107 °C
    降级阈值 92 °C 与 98 °C 88 °C 与 94 °C
    驻车危险捷径 大于 85 °C 大于 75 °C

    采样与聚合边界

    ThermalZone 只缓存找到的编号,不跟踪该编号以后是否换了类型;五项临时系统探针确认同一编号从一种温区类型改为另一种后,旧对象仍读取原编号的温度。找不到名称时编号仍为负一,随后读取会得到文件不存在错误并返回 0;稍后出现该名称时,由于尚未缓存编号,又可重新发现。单个 temp 文件消失同样变成 0 °C 而不是缺测,但 temp 内容不是整数会抛值错误,发现阶段的类型文件缺失会抛文件不存在错。名称扫描与非「文件不存在」类异常不在读取函数的捕获范围内,因此不能概括成「所有传感器错误都回零」。

    复合温度上,驻车复合温度取内存、CPU 最大与 GPU 最大中的最大值,行车复合温度另加入 PMIC 最大值;DSP、进气/排气、GNSS 和底部 SoC 虽进入设备状态,却不参与这两条复合温度。两条滤波器互相独立,初始化为未就绪,第一次更新不从 0 慢慢爬升;之后每次用固定系数推进,若调用周期因消息或调度变化而不等于 0.5 秒,物理时间常数会偏离标称的 5 秒。缺测温区的 0 不会作为「未知」传给聚合器,若关键热源缺失,最大值可能被其他热源掩盖,也可能整体偏低。

    热带状态与行为门

    正常状态机按当前档位的下限与上限做严格小于/大于判断,每轮最多向相邻档移动一步。六项热带分支探针确认从正常遇到极高温先到 overheated,下一轮高温持续才到 critical,精确等于阈值时保持原档。降温时 critical 和 overheated 有不同下限,形成迟滞。驻车危险捷径是独立分支:设备未在行车且满足前述三条件之一时,不含 PMIC 的驻车复合温度超过对应阈值就直接设为 critical;已启动的设备即使驻车复合温度很高也不走该捷径,首次驻车可立即直升,已启动过的设备则要严格超过五分钟。

    资格门 条件 后果
    新启动 热状态低于 overheated overheated 与 critical 都阻止进入
    已在途 热状态低于 critical 可在 overheated 暂留,达 critical 才撤销在途
    告警与风扇 点火为真且温度阻止启动或在途 请求高温离车告警并把风扇目标覆盖为 100%

    告警还要求点火为真。热状态阻止启动或在途时,它会请求高温离车告警,并在目标计算后强制风扇目标为 100%;告警清除或点火消失时,这一覆盖停止,后续目标回到风扇控制器独立的积分与前馈状态。对外报告的热状态是档位,既不证明哪个温区有效,也不证明风扇已执行。

    测试边界与硬件插件验收

    相邻的风扇单测只验证控制器数值上下限,本目录未找到对温区缺失、档位迟滞、驻车捷径与在途退场组合的专门单测。五项系统探针执行原温区读取类,六项热带分支探针执行原阈值表、状态转换与两项资格门;它们不是完整设备进程测试,实车热惯量、传感器布局、目标板系统接口格式和恢复时序均待验证。自制硬件的温区插件应提供稳定传感器身份、单位、量程、采样时间、有效位和故障原因,聚合器不能把失效当成 0 °C;可信热安全层要独立规定多传感器不一致、全部缺测和超温时的降级策略,并把「禁止新启动、在途退出、风扇全速」作为分别可验证的动作。

    常见问题

    设备过热就直接停机吗?

    不是。热状态分两档门槛:新启动要求低于 overheated,已在途只要求低于 critical,所以在 overheated 允许暂时滞留,直到达到 critical 才撤销在途资格。这两个门不能合并成一条「过热就立刻停机」的规则。

    传感器读不到温度会怎样?

    单个 temp 文件消失或编号为负一时读取返回 0 °C,而不是缺测;但 temp 内容不是整数会抛值错误,发现阶段的类型文件缺失会抛文件不存在错,名称扫描等异常也不在捕获范围内。因此不能概括为「所有传感器错误都回零」,聚合器也不能把 0 当作未知。

    为什么驻车时也判 critical?

    有一条独立捷径:设备未在行车且满足「本次未曾进入行车、没有驻车起点、驻车超过 5 分钟」之一时,不含 PMIC 的驻车复合温度超过 mici 的 85 °C 或其他设备的 75 °C 就直升 critical。已启动的设备即使驻车复合温度很高也不走这条捷径。

    温度报的档位能代表风扇已执行吗?

    不能。对外报告的是档位,既不证明哪个温区有效,也不证明风扇已执行;风扇目标的 100% 覆盖只在点火为真且温度阻止启动或在途时生效,告警清除或点火消失后即停止覆盖,回到控制器独立的积分与前馈状态。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/THERMAL_ZONE_BAND_STARTUP_EXIT_CONTRACT_DEEP_DIVE.md(取用了温区采样、聚合边界、热带状态机、驻车捷径与两项资格门)。
    • 项目 02 —— openpilot-research/FAN_HOST_PANDA_FEEDBACK_CONTRACT_DEEP_DIVE.md(仅用于确认风扇指令与转速告警的分工位置)。

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

    更新记录

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

  • panda safety 测试链:libsafety、回放与变异的证据边界

    结论:panda safety 的测试链要分四层证据看——本机逻辑库、离线回放、板级验证、车辆验证,它们不能互相替代。源文档反复强调:默认启用的 ALLOW_DEBUG 测试库、test.sh 要求的 100% 行覆盖率、以及回放返回的布尔通过,都不等于目标固件、线束或实车安全认证。make_CANPacket() 得到的只是 DBC 构帧后的 CFFI CANPacket_t,不是 panda USB/SPI、FDCAN 或 ECU 真实发送回执。

    一、本机库与通用断言

    入口 作用与限制
    _build_libsafety()、__getattr__() 首次取 libsafety 时将测试 C 翻译单元编译为临时共享库;默认启用 ALLOW_DEBUG,release=True 才不加该宏。正常模式加 UBSan,覆盖率模式改用 gcov flags。
    make_CANPacket()、CANPackerSafety.make_can_msg_safety() DBC 构帧后转成 CFFI CANPacket_t,保留地址、bus、长度码与数据;不是 panda USB/SPI、FDCAN 或 ECU 真实发送回执。
    init_tests() 测试库可人工改时钟、使能和测量窗口,并设置 safety_mode_cnt=2 等测试专用状态,并不模拟真实 MCU 启动。
    test_spam_can_buses()、test_fwd_hook() 对已列扫描地址、总线及测试用长度检查 TX/转发;不是 29 位地址和 0~64 字节载荷的全穷举。
    test_safety_tick()、test_relay_malfunction() 分别验证缺失关键 RX 时退出与继电器故障否决;仍是本机状态模拟。

    库编译使用 -Wall -Wextra -Werror -nostdlib -fno-builtin;common.py 用继承式 mixin 组合地址扫描、踏板/巡航、转向扭矩、角度、曲率、纵向和转发断言。SCANNED_ADDRS 扫全 11 位地址,但仅取少量 29 位诊断区间,且基础 spam 测试固定构造 8 字节零载荷。test_tx_hook_on_wrong_safety_mode() 读取其他测试类声明的 TX 列表并有多组车型/地址重叠例外,它是跨模式回归检查,不是形式化证明。另一个关键点:独立 test_release_build.py 只确认两种测试库能编译,没有对 release 版本逐车型跑相同 TX/RX 行为矩阵。

    还要注意测试库与真实固件的差异:libsafety/safety.c 内嵌仓库的 can.h、safety.h、ignition.h,以全局 timer_cnt 替代 MCU 微秒计时器,并暴露测试专用 getter/setter。模式切换仍走真实 safety 头文件,但总线驱动、线束继电器、电源/心跳和芯片时钟不在测试库内;init_tests() 在 set_safety_hooks() 之后另改测试状态,例如跳过初期继电器忽略窗口,故它是逻辑钩子验证而不是板级启动验证。

    二、覆盖率与变异测试的解释边界

    机制 给出的结论 不能被读成什么
    test.sh + UBSan 先跑 UBSan 单测,再在 SAFETY_COVERAGE=1 下重跑。 行覆盖率(排除 libsafety 包装器后 100%)不等于分支、状态序列、真实总线或发布固件覆盖。
    MISRA(test_misra.sh) 用 cppcheck 执行静态检查。 不能与动态 safety 断言合并为同一种证据。
    enumerate_sites()、build_priority_tests()、eval_mutant() 对预处理后的 safety C 生成变异点,按车型文件或共享核心测试选用例,统计杀死、存活或基础设施错误。 modes/ 下默认只取同名 test_<mode>.py;共享模式头文件若无同名测试,所选列表可能为空,不能据「该文件变异全部存活」直接断言无测试能覆盖它。

    变异统计里还有两个陷阱:worker 异常或进程池崩溃在结果汇总中被记为 killed,而另一路 Python 异常记为 infra_error——查看变异得分时必须把崩溃原因从有效测试断言失败中分离;脚本还有三条已知存活变异例外,不能把返回码零解释为零存活点。

    三、回放的证据边界

    replay_drive() 从日志 carParams.safetyConfigs[-1] 默认取最后一个模式,可覆盖参数;package_can_msg() 将 src % 4 映射为本地 bus。它按 logMonoTime 排序,给安全库设置软件时钟;在离片段首尾各超过 1 秒的区间,对每条 CAN 消息调用 safety_tick(),而板端该函数按 1 Hz 调用。回放成功条件是「控制允许时无 TX 被拒、无 RX 无效、没有区间内的 RX 健康失败」;控制未允许时被拒的 TX 仍计数但不使返回值失败。没有 can/sendcan 消息时会在首尾索引处失败,不是空路段的安全通过证明。

    init_segment() 用首个识别到的转向 sendcan 帧人工设 controls_allowed、上轮目标及测量值,然后断言该帧 TX 可过。这样可降低片段开头状态缺失导致的误报,但也意味着回放不完整验证实际巡航使能沿和最初几帧的真实状态。测试前应记录路段是否从设备启动/控制开始,不能只看最终布尔结果。

    四、自制硬件的验收证据链

    1. 本机逻辑:按目标安全模式分别运行 development/release 构建和关键 RX/TX 单测,保存精确源提交、编译宏、测试选择及失败列表。
    2. 离线时序:用目标车型路段回放,记录首帧预置、源 bus 映射、RX 健康与「控制未允许时拒帧」;回放不能替代无车故障注入。
    3. 板级验证:确认 MCU 固件签名/模式参数读回、实际 FDCAN 发送回执、继电器方向、心跳失联和异常报文抑制,不用 CFFI 测试库结果替代。
    4. 车辆验证:在合法合规的受控条件下单独确认 ECU 接受、原车 AEB/制动保留、驾驶员覆盖与恢复;插件系统不得把测试报告布尔值当作实时控制许可。

    本轮未运行完整安全测试,也没有目标硬件/车辆证据;上述内容是测试框架能力与缺口的源码索引。

    常见问题

    默认测试通过能否当作发布固件权限证明?

    不能。测试库默认启用 ALLOW_DEBUG,日常单测可覆盖仅调试注册的模式和纵向参数;test_release_build.py 只确认两种测试库能编译,没有对 release 版本逐车型跑相同 TX/RX 行为矩阵。

    100% 行覆盖率说明什么?

    只说明在 SAFETY_COVERAGE=1、排除 libsafety 包装器后行覆盖达到 100%,不等于分支、状态序列、真实总线或发布固件覆盖。MISRA 的 cppcheck 检查是另一种证据,不能与之合并。

    回放成功是否等于通过了安全验证?

    不等于。回放要求「控制允许时无 TX 被拒、无 RX 无效、无区间 RX 健康失败」,但控制未允许时被拒的 TX 仍计数而不使返回值失败;init_segment() 还人工预置首个转向帧状态,故不完整验证实际使能沿与最初几帧。

    变异评分零存活就代表没有缺口吗?

    不代表。共享模式头文件若无同名测试文件,所选用例列表可能为空;worker 异常或进程池崩溃会被汇总记为 killed,脚本还有三条已知存活变异例外,因此不能把返回码零解释为零存活点。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/SAFETY_TEST_HARNESS_REPLAY_EVIDENCE_DEEP_DIVE.md(取用了本机库与通用断言、覆盖率与变异边界、回放证据边界、硬件验收证据链)。

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

    更新记录

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

  • openpilot 发布构建链:文件选择、LFS、测试与推送

    结论:openpilot 的发布构建有两条 Git 主链:build_stripped.sh 生成精简源分支,build_release.sh 在固定设备工作树内编译并产出设备预编译分支。文件选择由 release_files.py 按 git ls-files 的受跟踪路径清单过滤,实际复制的是工作树文件,因此本地未提交修改可能进入产物。路径存在、blob 不超限、LFS 已安装,都不证明模型内容与哈希正确。本文为固定源码快照 c8fb906 的静态阅读,未执行任何发布命令。

    两条主链的入口与时序

    build_stripped.sh 的默认目标目录由 mktemp 生成后又被删除重建,显式指定的 TARGET_DIR 同样会被删除;它复制源 .git、新建 orphan tmp、移除旧文件及子模块目录,写入源提交元数据后提交,再检查子模块、LFS 与尺寸,只在有 BRANCH 时强推。它不运行 SCons,也不检查运行时模型是否可用。

    build_release.sh 则在固定的设备工作树目录中操作:先强制移除 worktree,失败则整体删除;随后添加共享引用的 worktree、删除本地 staging 引用、把当前 HEAD 指向它并清空 index。它运行 scons,再按 PANDA_DEBUG_BUILD 选择 release 证书构建或 debug panda;INCLUDE_BIG_MODEL 时仅用 test -f 检查三种模型路径。最后删除中间产物与发布工具、生成 prebuilt、提交、做尺寸检查、运行 test_onroad.py,成功后对一个或多个分支强推。

    两条链路共同的数据流是:git ls-files -z --recurse-submodules 取清单,正则过滤,xargs -0 cp -pR --parents 复制,再在目标目录提交。

    阶段 精简源分支 设备预编译分支
    调用 build_stripped.sh,可由 BRANCH 决定是否推送 build_release.sh,必须给 RELEASE_BRANCH
    目标目录 临时目录或 TARGET_DIR,都会被删除重建 固定的设备工作树目录,先移除 worktree
    构建与模型 不运行 SCons,不检查运行时模型 scons,INCLUDE_BIG_MODEL 时仅 test -f 检查模型路径
    提交前后 写源提交元数据、提交、检查子模块/LFS/尺寸 提交、尺寸检查、test_onroad.py、强推

    文件选择与共享辅助

    release_files.py 排除 .git/、.venv/、CI 工作流、.gitmodules、.gitattributes 与默认 .lfsconfig 等;未设置 INCLUDE_BIG_MODEL 时还按 big_ 前缀排除大模型。选择依据是跟踪路径清单,实际复制的是工作树文件,所以已跟踪但已从工作树删除的路径可能让复制失败。check-dirty.sh 是单独的辅助门,并不在两个构建脚本内被调用。

    共享辅助各管一段:setup_lfs.sh 在 INCLUDE_BIG_MODEL 时重建匹配大模型的 .gitattributes、设置目标 Git 的 hooksPath 并运行 git lfs install --local --force,但它不负责下载模型实体;identity.sh 固定发布提交的作者与提交者,覆盖调用者身份环境变量。

    组件/函数 作用与边界
    release_files.py 按受跟踪路径清单过滤;实际复制工作树文件,未在脚本内强制检查 clean
    setup_lfs.sh 重建大模型 .gitattributes 与 hooks;不下载模型实体
    check_file_sizes.sh 拒绝当前 HEAD 中超过 95 MiB 的 blob;在提交之后运行
    check-submodules.sh 检查子模块固定提交可达;未设 set -e,且不是两个构建脚本的内联门
    check-dirty.sh 检查工作树 git status --porcelain;Jenkins 常规 onroad 阶段调用,发布脚本不调用
    pack.py 按扩展名白名单 zipapp 打包;不覆盖发布主链的测试与尺寸门

    门禁层次与元数据

    check_file_sizes.sh 的尺寸检查发生在提交之后,不能证明外部 LFS 对象已就绪。SConstruct 以 .gitmodules 是否存在推断 release,这与文件选择器主动排除 .gitmodules 相配,但它不是签名或测试证明。version.py 把 prebuilt 文件当作预构建标记,而构建元数据另按 channel 判断 tested 与 release,两者是不同维度,不能因为有 prebuilt 就认定来自受测分支。

    在流水线侧,Jenkins 的设备阶段调用 build_release.sh,目标包括 staging 设备;nightly 分别构建普通与 PANDA_DEBUG_BUILD=1 版本;Chestnut nightly 另设 INCLUDE_BIG_MODEL=1 并以对应车端测试作为参数。车端测试位于提交和尺寸检查之后、强推之前,而精简脚本本身不运行该测试。

    可见风险与插件化约束

    第一类是破坏性路径与引用:目标目录、固定设备路径、本地 staging 引用和目标远端分支都可能被覆盖,这些入口只应由受控 CI 身份调用,并对目标路径与分支做独立白名单校验。第二类是来源一致性:目前按 Git 跟踪名单拷贝工作树,而非直接从已审计提交导出,插件构建应绑定主仓与子模块提交、干净状态、依赖锁和产物哈希,避免「提交 ID 一样、拷贝内容不同」。

    第三类是 LFS 与大模型:.lfsconfig、.gitattributes 和模型实体是不同对象,路径存在、blob 不超限、LFS 已安装都不证明内容正确。第四类是安全固件模式:PANDA_DEBUG_BUILD 使 panda 构建走非 release 证书分支,nightly-dev 与普通 nightly 不能互作安全权限等价物,插件框架不能允许普通软件插件选择或绕过固件安全模式。

    验证范围

    上述结论来自固定归档的脚本与直接调用点;未运行 Git worktree、LFS、SCons、panda 构建、设备测试、强推或安装。README 里的发布步骤是人工清单,不能当作流水线已执行证据。后续还需逐项审查 Jenkins 设备锁与凭据,以及构建产物的实际内容,并在隔离 Git 仓库中验证工作树复制与 LFS 规则。强推前应保留独立审核与可回滚来源,pack.py 只是 Python 分发辅助,不覆盖 CAN 安全、模型运行或车端依赖。

    常见问题

    发布脚本复制的文件来自提交还是工作树?

    来自工作树。选择依据是 git ls-files 的受跟踪路径清单,实际复制的是工作树文件,所以本地未提交修改可能进入产物,而已跟踪但已从工作树删除的路径可能让复制失败;check-dirty.sh 并不在两个构建脚本内被调用。

    95 MiB 尺寸门能证明 LFS 模型已就绪吗?

    不能。check_file_sizes.sh 只拒绝当前 HEAD 中超过 95 MiB 的 blob,且检查发生在提交之后;它不能证明外部 LFS 对象已就绪,路径存在、blob 不超限、LFS 已安装都不证明模型内容与哈希正确。

    PANDA_DEBUG_BUILD 构建能当发布固件用吗?

    不能。该开关使 panda 构建走非 release 证书分支,nightly-dev 与普通 nightly 不能互作安全权限等价物。插件框架也不应允许普通软件插件选择或绕过固件安全模式。

    pack.py 打出的产物是整车可部署包吗?

    不是。pack.py 只按扩展名白名单用 zipapp 打包,没有拷贝独立子模块,也没有运行发布主链的测试与尺寸门,因此只是 Python 分发辅助,不能视作整车可部署产物。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/RELEASE_BUILD_FILE_SELECTION_LFS_PUSH_CONTRACT_DEEP_DIVE.md(取用了两条主链时序、文件选择器、共享辅助门、可见风险与验证范围)。
    • 项目 02 —— openpilot-research/BUILD_METADATA_VERSION_PROVENANCE_DEEP_DIVE.md(关联的版本与构建元数据专题,用于区分 prebuilt 标记与分支分类,本文未展开其探针细节)。

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

    更新记录

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