标签: openpilot

  • CAN 与 Cap’n Proto 适配层:有效位为何在跨语言边界丢失

    结论:在这套 CAN 与 Cap’n Proto 适配层里,valid 只在 Event 外层,帧本身没有独立有效位:编码器只写外层 logMonoTime 与 valid,解码器只返回时间戳与帧三元组,不返回外层有效位。更关键的是发送侧——C++ 发送线程只读 logMonoTime 和 sendcan,用一秒年龄门与 fake_send 条件决定是否调用打包发送,没有检查 event.getValid(),因此 CS.canValid=False 只是日志与下游可见的标注,并不单独阻止该线程尝试发送。是否实际发上车总线仍受后续编码、传输与 MCU 安全钩子控制。本文为固定源码快照 c8fb906 的静态阅读与假 Event 原函数探针,未运行真实 Cap’n Proto、总线进程或连接车辆。

    组件与关键函数

    CAN 帧在 schema 里有 32 位地址、数据载荷和 8 位来源;Event 有 logMonoTime、默认恒真的 valid,且 can 与 sendcan 是不同 union 成员。编码器首次遇到非空帧时缓存字段对象,后续按相同 schema 访问。

    函数/组件 行为
    CanData/Event 帧有 32 位地址、数据载荷与 8 位来源;Event 有 logMonoTime 与默认真的 valid
    _get_reader_fields()/_get_writer_fields() 首次非空帧时缓存字段对象,后续按相同 CanData schema 访问
    can_list_to_can_capnp() 新建 Event,填入 can 或 sendcan 帧列表并序列化;valid 与时间戳只写在外层
    can_capnp_to_list() 逐条解析 bytes,返回时间戳与帧三元组,不返回 Event valid
    controls_update() 控制侧用 CS.canValid 标注 sendcan Event
    can_send_thread()/can_recv() 发送侧按时间戳与一秒年龄门筛选;接收侧把接收布尔值设为 Event valid

    从帧到 Event 的数据流

    编码器接受三元组列表,按调用方 msgtype 在 Event 上初始化 union 列表,外层 logMonoTime 来自单调时钟。首次遇到非空列表才缓存 CanData 的三个 schema 字段对象;空列表仍可产生合法的空 can 或 sendcan Event。函数自身不检查地址范围、总线号、CAN 或 CAN FD 合法长度与消息数量,依赖 schema 字段类型以及后续边界处理——因此不能把 schema 能序列化当作总线发送合法性证明。

    控制更新时,若控制消息存活则生成 sendcan,以状态里的 canValid 填外层 valid;而车型识别回调使用默认 valid=True。发送线程只读 logMonoTime 和 sendcan,用一秒年龄门与 fake_send 条件决定是否调用打包发送,没有检查 event.getValid()。所以状态标注与是否尝试发送是两件事,日志里看到的无效标注不能推断「危险动作已经发生」或「一定不会发送」。

    接收侧与解码资源边界

    接收侧即使底层接收返回假,仍构造一个有效位为假的 can Event,并放入当前收集到的帧。Python 解码只保留时间戳与帧、不传递外层有效位;状态更新先把这些帧送给车型状态与雷达接口,再以原始消息列表是否非空判断本轮是否收到 CAN。这样「收到一个无效 Event」仍算消息到达,其中已有的帧仍可能进入车型解析——当然车型解析器本身还有 CAN 检查,这里的结论仅限适配层与状态更新的直接门控。

    解码函数用接近无穷的遍历上限解析输入,相当于不在此调用处施加实用的遍历预算,输入消息大小和队列限制需沿上游单独核对。它直接按 msgtype 取 union 字段,没有先核对 union 实际类型,调用方传入错误类型或坏 bytes 时可能抛异常,而非返回结构化的坏消息结果。字段对象全局缓存假设后续仍是同一个 CanData schema,当前正常路径成立;缓存不保存消息内容或设备状态,也不等于逐帧验证。

    离线原函数探针与证据范围

    四项隔离测试从归档文件抽取四个原函数运行,仅替换 Event、帧字段对象与序列化存储。

    输入 原函数观察结果 证据边界
    空 can 列表 可生成并解出空 Event;字段缓存仍为空 假 Event 只实现最小接口,不证明真实序列化字节
    非空 sendcan,有效位为假 帧与时间戳往返;外层有效位不出现在解码结果;首次帧填充缓存 只证明本适配函数的字段路径与返回形状
    错误 union 与坏载荷 假 Event 抛出的属性或解析异常直接向上传播 真实解析库的错误类型与 union 语义仍未验证
    缺字段三元组、非法编码类型 原函数没有预检查;前者索引失败,后者委托初始化失败 假 Event 只模拟非法 union 拒绝,不能外推 schema 约束

    回放与仿真也复用这套适配函数:回放会把日志中的 can Event 转 bytes 后经该解码器导出帧列表,仿真则复用编码器发布合成 can。它们既服务实车运行也服务回放,但信任级别与时间域并不相同。

    自研硬件与插件契约

    消息插件契约应明确区分载荷有效位、总线链路健康、schema 可解析、时间戳年龄和 MCU 安全放行。只读观察插件可拿到带来源与有效位的 CAN 快照;控制候选插件不得直接发布 sendcan 到受信任网关。网关需逐 Event 核对 union 类型、有效位、时间域与年龄、帧数、地址、总线、长度和车型授权,再把送达传输、MCU 接受与安全拒绝分开记录。诊断与回放输入应有明确大小与遍历预算,异常帧应隔离而非让主车控线程意外退出。离线验收至少覆盖空列表、错误 union、有效位为假但含帧、CAN 事件无效且仍有先前好帧、坏 bytes、极大列表与载荷、未来或跨时钟域时间戳。

    常见问题

    CS.canValid=False 会阻止发送吗?

    不会单独阻止。控制更新会把 CS.canValid 写进 sendcan Event 的外层有效位,但 C++ 发送线程只读 logMonoTime 与 sendcan,用一秒年龄门和 fake_send 条件筛帧,没有检查 event.getValid()。是否真正发上车仍受后续编码、传输与 MCU 安全钩子控制。

    为什么解码后拿不到 valid?

    因为有效位只在 Event 外层。编码器把 valid 与时间戳写在外层,解码函数只返回时间戳与帧三元组列表,不返回外层有效位;因此跨语言往返后,帧层面看不到原始的无效标注,需要在上层另行传递。

    收到一个无效 CAN 事件算收到消息吗?

    在这层算到达。接收侧即使底层接收返回假仍会构造有效位为假的 can Event 并放入已收集帧;状态更新以原始消息列表是否非空判断本轮是否收到 CAN,所以无效事件里的帧仍可能进入车型解析,导致「丢帧」判断被低估。

    探针证明了真实 Cap’n Proto 行为吗?

    没有。四项探针替换的是 Event、帧字段对象与序列化存储,属于局部回调与编码器证据;真实解析库的错误类型、union 访问语义、遍历预算下的内存与时间行为都未验证,也不是诊断帧取得板级发送授权的证明。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PANDAD_PYTHON_CAN_CAPNP_ADAPTER_VALIDITY_DEEP_DIVE.md(取用了编码与解码函数、发送侧不读有效位、接收侧无效事件构造与四项原函数探针)。
    • 项目 02 —— openpilot-research/CAN_PARSER_HEALTH_DEEP_DIVE.md(仅用于确认车型解析器自身的 CAN 检查位置)。

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

    更新记录

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

  • tinygrad NV 原生后端:RM/UVM、PCI 与映射所有权

    结论:tinygrad 的 NV 原生后端把设备发现、内存管理与队列提交分成三层接口:NVKIface 走内核驱动与 RM/UVM,PCIIface 走用户态 PCI 与 GSP,MOCKIface 用于模拟。块内存由 NVAllocator 经所选接口分配,NVKIface.alloc() 依次创建 VA、RM 物理句柄、CPU 映射、UVM range 与 DMA/GPU 映射。它的 VA 分配使用不回退的 BumpAllocator,且中途失败未见配对的逐阶段回滚,因此长时间运行、模型热替换或多插件加载必须专门设计可回收地址分配。本文为固定源码快照的静态阅读,openpilot 的 load_oob() 默认并不使用 NV,本文不代表车端已部署行为。

    接口选择与设备启动

    NVKIface.__init__() 先打开控制与 UVM 节点,按驱动主版本切换常量表,初始化 UVM 并按可见设备规则筛选 GPU,再打开指定设备描述符并读取 GPU instance 与 minor。root、全局描述符和 VA 分配器都是类级状态,因此同一进程内多次初始化会共享这些资源。

    setup_usermode()、setup_vm() 与 setup_gpfifo_vm() 查询可用硬件 class、映射门铃 MMIO,并注册 GPU 与虚拟地址空间及队列,已打开的 NV 设备之间还会尝试启用 P2P。类支持情况与同主机上的初始化顺序会影响启动结果。NVDevice.__init__() 再选择接口、创建设备树、vaspace 与 channel group、查询 SM 能力,并懒建计算与复制相关的 GPFIFO 对象。

    两条用户态路径互斥:PCIIface.__init__() 启动时会拒绝已初始化 NVKIface 的进程,原因是固定地址 mmap 可能覆盖 UVM 分配。因此不能在同一进程里随意从一个接口切到另一个。

    接口 底层路径 关键边界
    NVKIface 内核驱动 + RM/UVM 类级 root/描述符/VA 分配器;与 PCI 路径互斥
    PCIIface 用户态 PCI/GSP 拒绝已初始化 NVKIface 的进程;不能套用 UVM 生命周期
    MOCKIface 模拟器 用于无设备环境,不代表真实驱动行为

    RM/UVM 内存与跨设备映射

    底层 nv_iowr()、rm_alloc()、rm_control() 与 uvm() 只检查系统调用返回,RM 与 UVM 的包内状态分别再检查,显存不足会被映射为 MemoryError;因此不能以 ioctl 成功替代对象创建成功。NVKIface.alloc() 按 host、uncached 与大小选择页粒度,分配 VA 与 RM 物理对象,再创建 UVM external range、DMA 地址映射与 GPU 可见映射,cpu_access 会把内存映射到 CPU 地址。

    NVKIface.map() 与 _alloc_gpu_vaddr() 要求 CPU/PYTHON/NPY 缓冲 4 KiB 对齐;首次创建 host range,已存在 NV 映射时复用物理对象,NV 到 NV 则在既有 VA 上添加 GPU 映射。低、高地址池都是 wrap=False 的 BumpAllocator。

    位置 作用与边界
    nv_iowr() / rm_alloc() / uvm() ioctl 只检查系统调用返回,RM/UVM 包内状态另行检查
    NVKIface.alloc() / _gpu_uvm_map() 分配 VA、RM 物理对象、UVM range、DMA 与 GPU 映射
    NVKIface.free() / unmap() 按句柄条件 RM_FREE,再 UVM_FREE,必要时 munmap
    NVKIface.map() / _alloc_gpu_vaddr() 要求 4 KiB 对齐,复用物理对象或在既有 VA 上加映射

    所有权矩阵

    NVAllocator 把通用 BufferSpec 的 host、cpu_access、zero 转给选定接口,释放前先 synchronize(),跨设备映射与注销委托接口;external_ptr 由通用 Allocator.do_free() 跳过底层释放。Compiled.finalize() 在进程结束时先尝试同步,再在有 device_fini() 时调用它,它不是每次 Buffer.free() 的通用设备重启或销毁 API。

    对象 创建/拥有者 释放或完成门 待验证点
    普通 NV BufferStorage NVAllocator 经 iface 分配 先同步,再由 NVKIface.free() 释放 RM/UVM/CPU 映射 异常顺序下 RM 句柄与 UVM range 能否全部回滚
    BufferSpec.external_ptr 提供地址的上层 do_free() 不调用底层释放 提供方是否保持地址有效到最后一次 GPU 访问
    CPU 缓冲的 NV 映射 原缓冲持有宿主地址 宿主缓冲释放时触发 _unmap() 多卡重复映射与 owns_range=False 的引用回收
    VA 地址池 NVKIface 类级 BumpAllocator UVM_FREE 释放映射,但 bump 指针不后退 大量创建释放后 VA 是否耗尽
    设备树与 GPFIFO NVDevice 与 iface 进程结束时同步并调用可选 device_fini() NVKIface 分支没有逐对象显式销毁路径

    静态风险与边界

    NVKIface.alloc() 依次分配 VA、RM 物理句柄、CPU mmap、UVM range、DMA 映射与 GPU 映射;若中途失败,本函数未看到配对的逐阶段 try/finally 回滚,仅靠 Buffer 成功返回后的析构无法处理此前的半成品状态。NVKIface.free() 的 RM_FREE 条件按句柄与主机对象枚举比较,主机对象与显存对象释放走不同路径,因此不能把 UVM_FREE 等同为每一层资源都已归还。

    更结构性的问题是 VA 池:高低地址池使用 wrap=False 且没有对应 free,即使单个缓冲的 UVM 映射已释放,进程内地址预留仍单调增长。对长时间运行、模型热替换或多插件加载,必须设计可回收的地址分配或代际隔离,并测最坏地址消耗;这是代码结构上的上界问题,不等于当前可达负载已触发。

    需要区分的是,PCI/GSP 路径使用不同的基类,不能直接套用 NVKIface 的 UVM 生命周期推论。本机没有 NVIDIA 目标设备,也没有执行设备 ioctl 或映射操作,上述影响须在隔离设备上做故障注入与句柄、映射计数验证。

    硬件插件要求

    车载硬件插件必须显式声明控制面(驱动与 PCI)、地址空间、分配与映射与释放的权责、P2P 资格和故障回滚;推理插件不得拥有 CAN 发送或更改 panda 安全门的权限。若借鉴该后端,应把设备发现、内存所有权、队列提交与资源释放拆成彼此独立的契约,再逐项验收。

    常见问题

    ioctl 成功是不是就代表 RM/UVM 对象创建成功?

    不代表。nv_iowr() 等只检查系统调用返回,RM 与 UVM 的包内状态会分别再检查,显存不足被映射为 MemoryError。不能以 ioctl 成功替代对象创建成功。

    NVKIface 和 PCIIface 能同时用吗?

    不能随意混用。PCIIface.__init__() 会拒绝已初始化 NVKIface 的进程,原因是固定地址 mmap 可能覆盖 UVM 分配。两条路径使用不同的基类与生命周期。

    VA 地址池会随创建释放一起回收吗?

    不会。低、高地址池使用 wrap=False 的 BumpAllocator,没有对应的 free;即使单个缓冲的 UVM 映射已释放,进程内地址预留仍单调增长。长时间运行或多插件加载需要额外的回收或隔离设计。

    NV 后端是 openpilot 车端的默认推理路径吗?

    不是。openpilot 的 load_oob() 默认不使用 NV,因此本专题描述的是可用路径而非已部署行为,车端实际推理路径另有选择。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_NV_RM_UVM_PCI_MEMORY_OWNERSHIP_DEEP_DIVE.md(取用了接口互斥、RM/UVM 分配与映射、所有权矩阵、VA 池与失败回滚边界)。
    • 项目 02 —— openpilot-research/TINYGRAD_NV_QMD_GPFIFO_PROGRAM_CACHE_CONTRACT_DEEP_DIVE.md(关联的 NV 队列与程序镜像专题,用于区分内存所有权与队列提交,本文未展开其实现)。

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

    更新记录

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

  • onroad 验收测试:25 秒日志、相机 SOF/EOF 与模型耗时各断言了什么

    结论:test_onroad.py 是需要 comma 硬件的设备测试,不是实车驾驶测试:它启动 manager、跑 25 秒、读首段 rlog.zst,再对服务频率、日志大小、相机时序、模型耗时和资源占用做断言。它的边界很明确——只分析短时日志、不测驾驶动态与安全限幅,且部分断言因算式问题无法触发、部分后备分支不可达。本文基于固定源码快照的静态分析,没有运行 manager、相机、panda、模型或车辆 I/O。

    启动链与副作用

    TestOnroad 设置 COMMA_HARDWARE_TEST=True;继承的 OpenpilotTestCase.setUpClass() 在非 comma 硬件跳过,然后调用小写的 setup_class()。普通路径先删 CurrentRoute、开启 RecordFront、写启用/条款/标定参数并设置 REPLAY、MSGQ_PREALLOC、TESTING_CLOSET,随后 Paths.log_root() 指向的目录若存在就被递归删除。在 AGNOS 设备上该路径位于设备本地介质目录,不随 per-test OPENPILOT_PREFIX 隔离。

    类级准备通过 subprocess.Popen() 启动 manager,最多等 30 秒收到 carState,要求 CurrentRoute 存在且日志目录中恰有一个匹配路段,然后睡眠 25 秒;终止 manager 后读取首段 rlog.zst。TestOnroad.setUp() 直接调用 unittest.TestCase.setUp(),没有调用每测试硬件初始化的那层,但继承的 run() 仍对各测试包裹环境快照、Prefix 和 manager 清理——类级准备发生在 per-test Prefix 之前,不能把它的 Params 和日志删除视为测试命名空间内的动作。

    并行运行时有隐患:通用 test_runner.make_batches() 只按类自身的大写 fixture 合批,而该类只定义小写 setup_class。若用多 worker 运行该文件,类级 manager 启动与日志目录删除可能在多个批次重复甚至并发;发布脚本和 Jenkins 直接调用本文件的 unittest.main(),不走这条并行分组路径。另外 finally 中判断 proc.wait(60) is None,而 Popen.wait() 正常返回退出码、超时抛 TimeoutExpired、不会返回 None,故这条 proc.kill() 后备分支不可达。

    主要验收断言与实际覆盖

    测试函数 有效检查 覆盖边界
    test_service_frequencies() 对已在日志中出现的非 GPS/init/sentinel 主题要求至少约 frequency * 20 * 0.8 条 未出现的主题不进循环;不是全服务存在性证明
    test_manager_starting_time() manager 启动到首条 managerState 小于 15 秒 只约束首消息,不证明所有进程就绪
    test_cloudlog_size() logMessage 总字节数小于 350 kB spam 判据 n / sum > 30 左侧至多 1,额外断言无法触发
    test_log_sizes() 文件落在 60 秒量按 25 秒缩放后的 0.5~1.5 倍 未知文件名访问会报错;不验证内容可解码
    test_ui_timings() 跳过约前 8 秒;最大绘制 CPU <250 ms、均值 <20 ms、>40 ms 帧少于 5 用 uiDebug.cpuTimeMillis,不是屏幕端到端延迟
    test_cpu_usage() 每进程上限 max(expected*1.8, expected+5) 总预算只求静态表之和;缺样本时上限可能未赋值
    test_memory_usage() 跳过前 8 秒,检查 deviceState 内存均值与增量 25 秒窗口不是长期泄漏证明

    相机与模型相关断言如下,它们共同的特点是只检验录入日志的元数据,不解码视频像素、不判断预测准确率:

    测试函数 有效检查 覆盖边界
    test_camera_frame_timings() 三路相机相邻 SOF 与 50 ms 的最大偏差 <5 ms 只检验日志帧时间戳
    test_camera_sync() 帧号连续、EOF 在 SOF 后 50 ms 内;道路/宽角同帧差 <2 ms、座舱错开 20~30 ms、编码器起始帧一致 相机状态只要求最早帧号 <100;仅对三路共同帧号比较
    test_camera_encoder_matches() 编码索引帧号对应相机状态的 SOF/EOF 完全相等 只核对元数据,不解码视频像素
    test_model_execution_timings() 8 秒后道路模型最大/均值 <60/40 ms,驾驶员模型 <300/50 ms 比较执行时间,非模型输出数值或最坏闭环时延
    test_timings() 预设服务消息间隔均值在目标频率 3% 内 独立于实际执行器安全约束
    test_startup()/test_engagable() RELEASE 存在时比较启动告警;8 秒后 engageable 全为真 release_only 只检查变量存在;可接管不等于真正 engage

    DEBUG 分支仅初始化 lr/ts,未设置 manager_st、msgs、log_sizes,只能支持使用到这些字段的定向用例,不能当作完整 suite 的等价运行模式。

    资源类断言也各有算式边界。test_cpu_usage() 用 procLog 首末 CPU 时间差与日志时长计算各进程使用率,每进程上限为 max(expected*1.8, expected+5),而总预算断言只求静态 PROCS 表的和,不约束实测使用率总和;缺样本时 max_allowed 可能未赋值或沿用上一进程值。test_memory_usage() 跳过前 8 秒,检查 deviceState 内存占用均值与相邻增量并打印 procLog 报告,但 25 秒窗口与百分比变化不是长期泄漏证明,共享内存区用量也只打印。test_log_sizes() 要求文件落在预设 60 秒量按 25 秒缩放后的 0.5~1.5 倍,未知文件名访问 LOGS_SIZE[f.name] 会报错,且它不验证文件内容可解码或跨段完整性。

    Chestnut 独立测试

    TestChestnutOnroad 还要求设备类型为 mici,并检查 Chestnut 硬件/编译产物。它写入演示 CarParams,用 mock deviceMotion 和合成 deviceState 驱动 camerad、calibrationd、modeld、dmonitoringmodeld,等到模型有效再收集 25 秒消息。断言包括:五种服务数量在目标频率约 5% 内、Event valid、帧号单调上升、至少 90% 道路模型帧与窄路相机交集、全部 modelV2.big 为真、以及 position.x 有限。

    这能证明短时大模型路径没有回落到小模型的可见标志,但不证明预测准确率、整帧像素对齐、车辆控制或长期热稳定性。发布脚本在强推前直接运行此文件的指定 unittest 入口,Jenkins 另把 Chestnut 测试与模型回放放在设备阶段;两者属于不同调用场景,且有分支/环境/硬件门,不能用任意一次绿色结果宣称全部平台通过。

    验证范围与插件化含义

    已静态核对完整测试文件、相邻 OpenpilotTestCase/Paths/helper 和 CI 入口,但未执行任何测试,未验证真实日志格式、采样分布、模型权重或目标设备。文中不可达/无效断言是直接源码控制流与算式结论,非已在设备上复现的故障。硬件插件验收应补固定日志的数值基准、CAN safety 测试、故障注入与独立台架时序,再进入实车驾驶员监督验证。

    常见问题

    test_onroad.py 是用来验证实车驾驶表现的吗?

    不是。它是需要 comma 硬件的设备测试,只启动 manager、跑 25 秒、读首段 rlog.zst,对服务频率、日志大小、相机时序、模型耗时和资源占用做断言。源文档明确它既不测驾驶动态与安全限幅,也不验证真实驾驶表现;本文也未运行 manager、相机、panda、模型或车辆 I/O。

    为什么说 proc.kill() 后备分支不可达?

    因为源码判断的是 proc.wait(60) is None:Popen.wait() 正常返回退出码,超时抛 TimeoutExpired,不会返回 None,所以基于该条件的后备分支逻辑上不可达。这是直接源码控制流结论,不是设备上复现的故障。

    相机同步测试通过,是否说明视频像素也对齐?

    不代表。test_camera_sync() 只比较录入日志的帧号与 SOF/EOF 时间戳,test_camera_encoder_matches() 只核对编码索引帧号与相机状态元数据,都不解码视频像素;且仅对三路共同帧号比较,不证明未记录帧的同步。

    Chestnut 测试通过能说明什么?

    它能证明短时大模型路径没有回落到小模型的可见标志(如 modelV2.big 全为真、帧号单调上升、服务数量符合目标频率)。但它不证明预测准确率、整帧像素对齐、车辆控制或长期热稳定性,也不能用绿测结果宣称全部平台通过。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/ONROAD_TEST_ACCEPTANCE_LOG_TIMING_CAMERA_MODEL_CONTRACT_DEEP_DIVE.md(取用了启动链与副作用、验收断言覆盖边界、Chestnut 专项与验证范围)。
    • 项目 02 —— openpilot-research/TEST_RUNNER_PARALLEL_FIXTURE_CONTRACT_DEEP_DIVE.md(仅用于确认测试合批与 fixture 命名的边界)。

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

    更新记录

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

  • 公共运行时原语:DT 常量、Ratekeeper 与 I2C 契约

    结论:common/ 里的时间常量、滤波器、PID、系统统计与 I2C 只是被复用的基础函数,它们不是独立进程,也不能单独证明端到端实时性。DT_CTRL 等定义的是名义时间步长,不是操作系统保障的周期;monitor_time() 只记录进度与超期,keep_time() 才在剩余时间为正时睡眠。因此「没有 lag 日志」并不能理解为满足控制时限。

    一、时间常量、优先级与调度

    原语 具体行为 调用边界
    时间常量 DT_CTRL 等定义 100/20/2/20 Hz 的名义时间步长。 控制器、模型、hardwared 和驾驶员监测用作状态更新或滤波参数;不是操作系统保障的周期。
    Priority 定义控制进程两级 FIFO 数值。 controlsd、card 等选择 CPU/优先级;数值只对当前平台布局有意义。
    config_realtime_process() 禁用 GC;非 PC Linux 调用 SCHED_FIFO 并设 CPU 亲和性。 modeld、controlsd、selfdrived、card 等入口;PC 上仍禁用 GC,但跳过内核调度设置。
    drop_realtime() / set_core_affinity() 恢复 SCHED_OTHER;单独改 CPU 亲和性。 UI 部分操作、Athena、上传器等避免抢占高优先级控制任务。
    Ratekeeper.monitor_time() / keep_time() 前者用单调时间累积下一帧截止点、记录移动平均和超期;后者仅在剩余时间为正时睡眠。 card、controlsd、selfdrived 调用 monitor_time(),不调用 keep_time();轮询传感器调用 keep_time()(sensord.py)。

    MovingAverage.add_value()/get_average() 保留 100 个间隔的移动平均;Ratekeeper.lagging 在均值超过名义间隔约 11.1% 时为真——该布尔值不等于最坏延迟或丢消息率。

    二、滤波与 PID

    原语 关键公式 / 行为 使用注意
    FirstOrderFilter.update() 使用 alpha=dt/(rc+dt);initialized=False 时直接接受首样本。 调用方传固定 dt,无自动适配实际帧间隔;hardwared 温度链采用 initialized=False。
    BounceFilter.update() 在一阶位置滤波上加速度/阻尼状态,并按 60 FPS 基准缩放。 主要用于按钮、滚动、键盘等界面动画;不应误用作车辆动态滤波。
    PIDController.update() 按速度插值 P/I/D 增益,积分项按 rate 固定步长推进并做抗积分饱和,最后限幅。 横向/纵向控制和风扇;实际调用周期偏离配置速率时,积分动态会变化。
    LinuxSystemStats cpu_usage_percent()/memory_usage_percent() 读 /proc/stat 与 /proc/meminfo。 CPU 新出现或计数回退时报告 0;缺字段会抛异常;它是遥测输入,不能代替控制循环的 99.9 分位时延。

    三、运行时约束与故障边界

    原生线程侧还有一组 C++ 入口:set_realtime_priority() 与 set_core_affinity() 对当前 Linux TID 设置 FIFO/CPU,非 Linux 返回 -1。源文档给出两种态度:pandad 在非 PC 上要求设置成功(使用断言),而 camerad 只设亲和性、驻车时允许失败。这也说明「能否设置实时优先级」是平台能力问题,普通插件不应默认拥有该权限。

    Ratekeeper 的 _next_frame_time 按固定间隔递增,循环落后时不会跳帧重设截止点;连续超期可能积累负的 remaining。当 print_delay_threshold=None 时,它不会打印超期,也不会让 monitor_time() 返回 True,但仍计算 remaining 和窗口均值——所以不能把「无 lag 日志」理解为满足控制时限。config_realtime_process() 的内核调用失败会抛异常;C++ 侧对 pandad 使用断言要求成功,而 camerad 只设亲和性、驻车时允许失败。

    源文档的六项原函数/假时钟与调度探针给出一个直观例子:10 Hz 下首次正余量约 0.1 秒,后续某轮 0.25 秒的迟到仍让下一截止点只加 0.1 秒,连续轮次继续报迟到;keep_time() 只为正余量调用睡眠。config_realtime_process() 先禁用 GC,再尝试 FIFO,最后设置 CPU 亲和性——若 FIFO 调用抛错,GC 已禁用而亲和性尚未设置。探针不使用真实内核优先级或时钟,因此不证明车端尾延迟。

    FirstOrderFilter 默认把 x0 当有效初值,固定 dt 意味着跳帧/超时后仍按一次名义更新,不会自动按真实经过时间补偿。PIDController 的积分步长也在构造时固定为 1/rate,所以硬件移植和插件执行线程必须记录实际间隔,再用回放/故障注入验证误差与饱和恢复。SMBus.read_i2c_block_data() 在发 ioctl 前限制请求为 0–32 字节,回读长度上限为请求长度;底层没有统一重试、设备健康位或锁,共享总线上的驱动冲突与电气隔离由上层/平台负责。

    四、插件化落点与验证

    新增可运行插件应在 manifest 中声明输入频率、最大处理时长、消息最大年龄、CPU/内存上限和故障处理;受信任调度器保留唯一控制权。SCHED_FIFO/亲和性仅供经过基线测试的核心进程申请,普通插件默认低优先级、进程隔离。传感器插件必须明确总线地址、单位、时间基准、错误/重试语义,并在断连时发布无效/超时状态,不能把 0 遥测值当作健康证据。

    现有 common/tests/ 未找到专门覆盖这几个调度/滤波/I2C 原语的测试文件;本篇已对 Python 调度分支做隔离探针,未测真实内核调度权限、总线时序或车辆级截止时间。建议先在台架采集控制和传感器消息年龄、循环延迟分布、CPU 热限流与故障恢复,再考虑调整优先级或将插件接入热路径。若确实要把这类原语接入热路径,应记录实际调用间隔并据此校准滤波与 PID 的 dt,而不是沿用名义值。

    常见问题

    标称 100 Hz 是不是操作系统保障的周期?

    不是。DT_CTRL 等只是名义时间步长。控制类循环调用 monitor_time() 而非 keep_time(),受输入/工作负载驱动;源文档明确 100 Hz 是期望节拍,而非主动节流,更不是操作系统保障。

    没有 lag 日志是否说明满足控制时限?

    不能说明。当 print_delay_threshold=None 时既不打印超期,也不让 monitor_time() 返回 True,但仍计算 remaining 和窗口均值。因此「无 lag 日志」不能理解为满足控制时限。

    FirstOrderFilter 会在跳帧后按真实时间补偿吗?

    不会。调用方传固定 dt,alpha=dt/(rc+dt),跳帧/超时后仍按一次名义更新,不会自动按真实经过时间补偿。同理 PIDController 的积分步长在构造时固定为 1/rate。

    I2C 一次最多能读多少字节?有统一重试吗?

    SMBus.read_i2c_block_data() 在发 ioctl 前限制请求为 0–32 字节,回读长度上限为请求长度;SMBus 打开 /dev/i2c-N 后经 ioctl 读写,force=True 选 I2C_SLAVE_FORCE。底层没有统一重试、设备健康位或锁,错误透传给调用方。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/COMMON_RUNTIME_PRIMITIVES_DEEP_DIVE.md(取用了时间常量与调度、滤波与 PID、系统统计与 I2C 的函数级契约及插件化要求)。

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

    更新记录

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

  • 大众 MQB panda safety:双总线按钮、冗余制动与纵向编译门

    结论:Volkswagen MQB 的 panda safety 用同一张 RX 表区分两种模式,差异几乎全在 TX 白名单:Stock ACC 模式允许在 bus 0 与 bus 2 发 GRA_ACC_01,自控纵向模式则完全不发该帧,改为允许 ACC_02/06/07。纵向配置依赖 FLAG_VOLKSWAGEN_LONG_CONTROL,且只有 ALLOW_DEBUG 构建才读取它,所以上层 CarParams 设置了纵向能力,并不能单独证明发布固件允许发送 ACC。本文为固定源码快照 c8fb906 的静态阅读,bus 号是单个 safety 实例的本地编号。

    模式与总线矩阵

    volkswagen_mqb_init() 初始化共享状态与同一张 RX 表;仅在 ALLOW_DEBUG 构建中读取纵向参数位,从而选择 Stock 或 Long 的 TX 白名单。两种模式都没有专属转发钩子,通用转发只在 bus 0 与 bus 2 之间,check_relay 地址在目标总线上被静态阻断。

    一个容易误读的点是方向:LH_EPS_03 的 RX 在 bus 0,候选 TX 在 bus 2,不能因为 ID 相同就把两个方向混为一个。

    模式 RX 关键 TX 使能来源
    Stock ACC 七组均在 bus 0 HCA_01、LDW_02 在 bus 0;LH_EPS_03 在 bus 2;GRA_ACC_01 在 bus 0 及 bus 2 TSK_06 原车巡航接合沿;主开关关闭或 GRA Cancel 退出
    自控纵向 同一七组均在 bus 0 HCA_01、LDW_02、ACC_02/06/07 在 bus 0;LH_EPS_03 在 bus 2;无 GRA_ACC_01 TX TSK_06 主开关可用时,GRA Set/Resume 释放沿进入;Cancel 退出

    RX 完整性与状态来源

    volkswagen_mqb_rx_hook() 更新移动状态、驾驶员扭矩、巡航许可、按钮、油门和两种制动观测。制动状态是 Motor_14 踏板开关或 ESP_05 压力检测的组合,之后由共享安全层处理制动退出。ESP_19 的四轮原始值求和大于零即判移动,它不形成第二车速估计。

    需要注意,忽略 checksum 与计数器的两帧仍留在 RX 存活表里,因此不能写成「完全不检查」。频率是检查配置,并非对每帧的实时 deadline 保证。

    RX 帧 检查配置 钩子消费
    ESP_19 0x0B2 8 字节、100 Hz;忽略 checksum/计数 四轮原始值求和大于 0 判移动
    LH_EPS_03 0x09F 8 字节、100 Hz;CRC + 4 位计数 带符号驾驶员扭矩采样
    ESP_05 0x106 8 字节、50 Hz;CRC + 4 位计数 制动压力超过门限的布尔值
    TSK_06 0x120 8 字节、50 Hz;CRC + 4 位计数 ACC 状态 3/4/5 视为接合,2 视为主开关可用
    Motor_20 0x121 8 字节、50 Hz;CRC + 4 位计数 原始油门字段非零
    Motor_14 0x3BE 8 字节、10 Hz;忽略 checksum/计数 踏板开关位
    GRA_ACC_01 0x12B 8 字节、33 Hz;CRC + 4 位计数 纵向 Set/Resume 释放沿,任意模式 Cancel 退出

    TX 字段门与空白

    volkswagen_mqb_tx_hook() 检查 HCA_01 的扭矩与请求位、ACC_06/07 的加速度,以及在未允许控制时 GRA 的设置/恢复位。HCA_01 由共享扭矩钩子检查最大 300、每帧上升 4 与下降 10、实时差 75、驾驶员扭矩余量 80 和请求位;LH_EPS_03 bus 2 虽在 TX 表中用于 Emergency Assist 兼容,但本模式 TX 钩子没有该帧的专属扭矩或载荷检查,其准入只受白名单和继电器故障约束。

    GRA_ACC_01 在 Stock 模式允许 bus 0/2 与 8 字节;未允许控制时只禁止字节 2 的 Set/Resume 对应位,钩子并不要求 Cancel 必须置位,也不全面约束其余按钮位。已允许控制时,这一专属按钮门不再限制这些位。ACC_06/07 只在纵向 TX 表中,两帧主加速度都用 -3.5 至 2.0 m/s² 活动范围与 3.01 m/s² 未激活编码,ACC_07 的另一加速度字段固定要求 3.02 m/s²。

    TX 帧 专属检查 空白与边界
    HCA_01 最大 300、每帧上升 4/下降 10、实时差 75、驾驶员扭矩余量 80、请求位 共享扭矩钩子生效
    LH_EPS_03(bus 2) 仅白名单与继电器故障约束 本模式无专属扭矩/载荷检查
    GRA_ACC_01 Stock 模式;未允许时禁字节 2 Set/Resume 位 不要求 Cancel 置位,不全面约束其余按钮位
    ACC_06/07 仅纵向表;主加速度 -3.5 至 2.0,未激活 3.01 ACC_07 另一字段固定要求 3.02
    LDW_02 / ACC_02 无本模式专属载荷分支 白名单不证明仪表语义或 ECU 接受

    面向自制硬件的插件契约

    车型插件应提交 MQB 平台、DBC、网关或相机线束、固件构建及纵向授权证据,未知组合保持只读。总线插件要分别标注 LH_EPS_03 的 bus 0 RX 与 bus 2 TX,以及 Stock 模式 GRA_ACC_01 的 bus 0/2 TX,不能按单一 ID 推断方向。

    转向与纵向插件只提供意图;可信任网关负责编码、节拍、HUD 与 LH_EPS_03 模板及 ECU 静默回执,最终 TX 仍由独立 MCU safety 决定。故障注入须覆盖两路制动输入失效、主开关或按钮沿错序、CRC 与计数故障、拒帧、错总线,以及纵向固件不支持时的降级。

    证据与缺口

    三项模式离线探针直接运行编译版 safety:调试构建的 Stock 模式允许 bus 2 的 GRA_ACC_01,纵向模式拒绝它;用 DBC 打包的未激活加速度 ACC_06=3.01 m/s² 仅在调试纵向模式通过。以同一 LONG 参数但无 ALLOW_DEBUG 编译后,bus 2 的 GRA_ACC_01 再次可通过、该合法未激活 ACC_06 被拒,说明该测试构建实际退回 Stock TX 表。

    这里说的「无调试宏」只表示本机 C 测试库的预处理条件,不等于目标 MCU 固件已验证。现有 test_volkswagen_mqb.py 覆盖扭矩、冗余制动、Stock 按钮、纵向加速度与按钮使能;本机运行该模块 65 项,9 项跳过,其余通过,但仍没有目标板或实车数据,不能宣称车载可用。

    常见问题

    Stock ACC 与自控纵向模式的 TX 白名单差在哪?

    Stock 模式在 bus 0 与 bus 2 发 GRA_ACC_01;自控纵向模式完全不发该帧,改为在 bus 0 发 ACC_02/06/07。两种模式的 RX 都是同一七组、都在 bus 0。

    为什么 LH_EPS_03 的 RX 在 bus 0、TX 却在 bus 2?

    这是该实例的本地总线角色分工:RX 取自 bus 0,候选 TX 在 bus 2,用于 Emergency Assist 兼容。不能因为 ID 相同就把两个方向混为一个。

    忽略 checksum 与计数器的两帧是不是等于完全没检查?

    不等于。ESP_19 与 Motor_14 虽然忽略 checksum 与计数,但仍留在 RX 存活表中,仍受地址、总线、长度与存活检查约束。

    上层 CarParams 开了纵向能力就代表固件允许发 ACC 吗?

    不代表。纵向配置依赖 FLAG_VOLKSWAGEN_LONG_CONTROL,且只有 ALLOW_DEBUG 构建才读取它;上层能力位不能单独证明发布固件允许发送 ACC。探针显示无调试宏编译时会退回 Stock TX 表。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/VOLKSWAGEN_MQB_PANDA_SAFETY_DEEP_DIVE.md(取用了双模式总线矩阵、RX 完整性表、TX 字段门与空白、模式离线探针结论)。
    • 项目 02 —— openpilot-research/VOLKSWAGEN_MEB_PANDA_SAFETY_DEEP_DIVE.md(同品牌关联专题,用于对照 MQB 与 MEB 的差异,本文未展开其 Gen2 CRC 细节)。

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

    更新记录

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

  • process_replay 容器:初始 Params、消息合流与合成时钟

    结论:回放器的关键是分清「测试初始条件」与「真实运行」,以及分清「合成时钟」与「真实时延」。replay_process()` 先用 migrate_all() 迁移历史消息,再交给多进程调度;return_all_logs=True 时一旦某主题产生至少一条新输出,该主题全部旧消息被替换。「完整日志」不是原始日志加增量。同时 get_output_msgs() 把输出时间改成 start_time + processing_time,这是调度模型的合成值,不是进程真实运行时间、相机曝光时间或 CAN 发送时间。

    一、入口和数据所有权

    入口/对象 行为 重要边界
    replay_process_with_name() 名称经 get_process_config() 转成独立配置,再委托 replay_process()。 名称必须存在于 CONFIGS,不是 manager 的 44 个进程均可回放。
    replay_process() migrate_all() 先做历史 schema/manager/panda/相机状态准备,再调用多进程调度;return_all_logs=True 时删除迁移日志中所有与新输出同主题的旧消息,合并后按 logMonoTime 排序。 迁移会改变输入和时间顺序;同主题旧消息被整体替换,而非增量叠加。
    ProcessContainer.__init__() 创建随机 OpenpilotPrefix,深拷贝回放配置和受管进程对象,维护输入队列/计数/最近输入时间。 前缀隔离 Params、msgq、部分日志路径,不隔离 CPU、设备或权限;同一解释器内环境变量仍是进程级共享状态。
    ProcessContainer.start() 显式创建前缀目录,写 Params/环境,运行配置回调,开启 fake 事件,创建 Pub/Sub 和可选 VisionIPC,然后启动进程并运行初始化回调。 先启动后回调意味着初始化回调必须按 fake 事件握手;启动中途异常可能留下部分资源。
    ProcessContainer.run_step() 先用触发器决定周期是否结束,当前消息始终入 msg_queue;周期结束才收上周期输出、依次发布队列消息与相机帧、解锁 fake 事件。 输出采集发生在本周期输入发送之前,不能按单次调用直接对齐输入输出。
    ProcessContainer.stop() 对进程发 SIGKILL、关闭 fake 事件、删除前缀目录及回放设置的环境变量。 不恢复回放前相同变量的旧值;失败清理可能再抛异常并掩盖最初故障。

    二、初始 Params、指纹与环境

    生成函数 关键写入 语义
    generate_params_config() 默认开启 OpenpilotEnabledToggle/DisengageOnAccelerator,默认不禁用日志;含 ubloxGnss 写 UbloxAvailable,含 driverMonitoringState 写 IsRhdDetected;固件来源保存 CarParamsCache,纵向权限与 notCar 决定 AlphaLongitudinalEnabled、JoystickDebugMode。 这些是测试初始条件,不是重新判断车辆安全许可。
    generate_environ_config() 设置 PARAMS_ROOT 和 REPLAY=1,可选 LOG_ROOT;强制指纹时设 SKIP_FW_QUERY=1。 原函数的 AST 隔离探针分别确认三种字典输出,未创建任何目录或执行指纹查询。
    _setup_env() / _clean_env() 对空字符串执行「从 os.environ 删除该键」,非空值覆盖;设置 PROC_NAME;SIMULATION=1。 _clean_env() 不会恢复之前的值,故回放应运行于独立子进程或自行保留环境快照。

    环境隔离还有一层容易被忽略:_setup_env() 对配置中的空字符串执行「从 os.environ 删除该键」,非空值直接覆盖,并设置 PROC_NAME;_clean_env() 删除配置中所有环境键及 PROC_NAME/SIMULATION,不会恢复之前的值。而 OpenpilotPrefix 自己仅保存和恢复 OPENPILOT_PREFIX。因此在已有环境变量的测试宿主中,回放应运行于独立子进程或自行保留环境快照,不能把容器当成可嵌套的完整环境事务。

    OpenpilotPrefix 自己仅保存和恢复 OPENPILOT_PREFIX;因此在已有环境变量的测试宿主中,容器不是可嵌套的完整环境事务。

    三、消息合流和时钟

    _replay_multi_process() 先创建所有容器,再计算 all_pubs(注入主题)与 all_subs(输出主题)。仅把原路段中属于 all_pubs - all_subs 的消息放入外部队列;与某个受测进程输出同主题的原日志不会进入外部队列,必须依靠新的内部输出驱动下游。每次被测进程产出的消息都加入最终 log_msgs;若其主题又属于 all_pubs,则加入内部队列和按 logMonoTime 排序的小根堆。外部和内部候选比较时,时间相等优先内部消息。

    时间上,get_output_msgs() 等进程下一次 recv_called,排空输出 socket,并把所有采到的输出时间改成 start_time + processing_time;start_time 是此前收到的输入最大 logMonoTime。run_step() 内的 Timeout 只覆盖已结束的周期;没有触发周期的积压输入,以及尾部 get_output_msgs() 冲刷,不共享这个超时上下文。

    两个收尾条件值得单独验证:主循环在外部队列耗尽、所有容器的 msg_queue 都为空时会停止,即使内部堆仍有已生成消息——这些消息可能已进入最终输出,却未再次送给下游容器;结束后逐容器冲刷输出也只追加到最终结果,不重新进入内部堆。这是源码控制流可见的覆盖边界,尚无实际多进程路段复现,不能把多进程回放等同完整闭环执行。

    四、故障与硬件插件验收

    失效路径也需注意:_replay_multi_process() 先将容器加入 containers 再调用 start(),若在 self.rc 或 capture 初始化之前抛错,finally 中的 stop() 及捕获读取可能因部分初始化而再次异常。尾部冲刷的 ReplayContext.wait_for_recv_called() 最终使用默认无限等待,需另设总 deadline 才能证明崩溃/失联路径有界。以上为静态失效路径,未执行故障注入。

    要把新的传感器、计算或车辆接口做成插件,需给回放器明确的输入/输出主题、触发条件、初始 Params/环境、时钟域、消息新鲜度、最大积压和异常清理契约。验收应分开记录功能输出、端到端时延、处理器资源、断流/进程退出和 panda 安全测试;processing_time 或对齐后的参考日志不构成实车时限证据。当前缺 capnp、参考路段和目标硬件,本专题没有运行完整回放。此外迁移本身会改变输入与时间顺序,因此比较两次回放前必须固定同一迁移版本与路段快照,否则结果不可比。

    常见问题

    所有 manager 进程都能被回放吗?

    不是。replay_process_with_name() 要求名称存在于 CONFIGS,源文档明确不是 manager 的 44 个进程均可回放。名称只是转成独立配置的入口,不代表该进程具备完整的回放输入/输出契约。

    return_all_logs=True 时日志是原日志加增量吗?

    不是。一旦某主题产生至少一条新输出,该主题全部旧消息被替换,而不是叠加。源文档强调「完整日志」不是原始日志加增量,且迁移会改变输入和时间顺序。

    processing_time 是进程真实运行时间吗?

    不是。get_output_msgs() 把输出时间改成 start_time + processing_time,start_time 是此前输入最大 logMonoTime,因此它是调度模型的合成值,不是进程真实运行时间、相机曝光时间或 CAN 发送时间。

    多进程回放等于完整闭环吗?

    不等于。主循环在外部队列耗尽且所有容器 msg_queue 为空时会停止,即使内部堆仍有已生成消息;尾部冲刷也不重新进入内部堆。源文档称这是控制流可见的覆盖边界,尚无实际多进程路段复现。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PROCESS_REPLAY_CONTAINER_SCHEDULER_PARAM_LIFECYCLE_DEEP_DIVE.md(取用了容器入口与数据所有权、初始 Params/环境生成、消息合流与合成时钟、故障与插件验收)。

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

    更新记录

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

  • Toyota 接口:能力参数怎么算,雷达静默事务又证明了什么

    结论:Toyota 车型接口把能力参数构建与雷达静默事务分成两件不同的事:前者在 _get_params() 里派生安全参数、转向模式与纵向能力,后者由 init() 通过 UDS 0x28 通信控制向总线 0 地址 0x750 发请求。源文档指出 disable_ecu() 的返回值只反映「本函数的请求路径判定」,而 Toyota.init() 丢弃该返回值、card.controls_update() 在 CI.init() 返回后直接设置 ControlsReady——这是一个确认状态缺口,但不等于任何具体车辆 ECU 实际未静默。本文为静态追踪,没有车辆、台架、UDS 抓包或回放结果。

    参数生成的先后次序

    get_car() 在指纹和固件识别后,把 alpha_long_allowed、is_release 传给车型 get_params(),再实例化接口。基类 CarInterfaceBase.get_params() 先装配平台质量、轴距、转向比和静态 flags,再调用 Toyota _get_params(),随后增加标准载荷并计算惯量和轮胎刚度。末尾的质量、惯量和轮胎刚度计算依赖 Toyota 已设置的 centerToFront 等值,所以单独调用 _get_params() 或只复制 values.py 的标志,不能重建最终 CarParams。

    Toyota safety model 固定为 toyota,safetyParam 先装载该车型 EPS_SCALE,再按动力 DBC、SecOC、角度控制和原车纵向模式叠加标志;ToyotaSafetyFlags 从第 9 位起定义,低字节留给 EPS 比例。换 DBC 或线束地址时,必须同步核对 MCU hook 的解析/白名单,不能把 CarParams 能力位看成安全策略的唯一实现。

    参数分支 取值 边界
    角度车型 steerControlType=angle、启用 LTA 标志、延迟 0.18 秒、steerLimitTimer=0.8 秒 与扭矩车型的标定不同
    其他车型 扭矩横向标定、默认延迟 0.12 秒、0.4 秒限制 按车型逐项核对
    旧 Prius 特例 非指定 EPS 版本时 0.25 秒延迟、0.2 度死区;另一不支持版本标 dashcamOnly 固件列表为空或未命中时不能推断该特例已验证
    混动标志 固件列表中存在 Ecu.hybrid;TSS2 混动纵向执行器延迟 0.05 秒 按固件列表判定
    BSM 能力 TSS2 且动力 fingerprint 中存在 0x3F6 不由所有静态配置直接推断
    radarUnavailable 仅测试该车型 DBC 映射是否包含 Bus.radar 不是运行时雷达在线/健康检测

    注意 TSS2 且 RADAR_ACC 的平台配置还会移除雷达 DBC,因此「未提供雷达解析器」与「已经成功静默雷达 ECU」是两个不同状态。get_pid_accel_limits() 按 CarControllerParams 给出加速度最小/最大值,函数签名中的当前/巡航车速并没有用于动态调整。

    纵向能力决策

    纵向能力由源码布尔表达式决定,不是对全部平台的实车适用性声明:

    输入条件 参数结果 运行含义
    TSS2 且非 RADAR_ACC 默认 openpilotLongitudinalControl=True 相机侧 ACC 路径,源码不调用 Toyota 雷达静默请求
    RADAR_ACC 且 alpha_long=True 加 DISABLE_RADAR,允许自控纵向;alphaLongitudinalAvailable=True 需走 ECU 通信控制
    RADAR_ACC 且 alpha_long=False 不加 DISABLE_RADAR,原车纵向;safety 加 STOCK_LONGITUDINAL 不应发自控加速候选
    非 TSS2 / 其他组合 由 DISABLE_RADAR 是否设置决定 逐平台核对

    autoResumeSng 在 Toyota 分支直接设为 openpilotLongitudinalControl。最低启用速度由 stop-and-go 能力选择 -1 或 19 mph 换算值;TSS2 加 RAISED_ACCEL_LIMIT 时,控制参数上限从 1.5 提升到 2.0 m/s²。发布版 SecOC 车型 dashcamOnly=is_release;card 若判为被动,会把 safety 配置改为 noOutput——这是上线许可门,不是 SecOC 密钥校验。

    雷达静默事务:证明了什么、没证明什么

    Toyota init() 仅在 DISABLE_RADAR 时调用 disable_ecu(),目标为总线 0、地址 0x750、子地址 0xF;默认请求是 UDS 0x28 的「使能接收/禁用发送」,作用于正常消息。disable_ecu() 最多尝试 10 次,每次先要求扩展会话 0x10 0x03 的 0x50 0x03 前缀响应,再发送通信控制请求。该函数自身注释明确警告此操作会禁用 AEB,应把原车主动安全能力变化纳入车型准入,而非当作普通功能开关。

    关键缺口在于:通信控制阶段把期望响应前缀设置为空字节串,调用 query.get_data(0),却丢弃该调用的返回字典;只要前置扩展会话查询产生条目且通信控制调用没有抛异常,disable_ecu() 就记录「ecu disabled」并返回真。get_data() 对非空响应使用 startswith(expected_response),空前缀无法鉴别 UDS 正响应内容。加之 Toyota.init() 丢弃 disable_ecu() 返回值,card.controls_update() 在 CI.init() 返回后直接设置 ControlsReady,即便诊断静默请求返回假,也没有从这条返回链阻断就绪。

    控制器在 DISABLE_RADAR 时按 20 帧节拍发送 tester-present 候选,但该帧生成并不证明 ECU 进入目标会话,也不证明会话保持或 AEB 真实状态。Toyota.deinit() 定义了相反的「使能接收/使能发送」请求,复用同一路径;但在本快照的运行链检索中未找到它的调用,因此不能把函数存在视为正常退出一定恢复 ECU。

    硬件与插件重构边界

    车型能力插件应产出期望配置而非宣称已经完成 ECU 改写,至少包含车型/固件/DBC 身份、纵向权限、AEB 影响、safety 模式及版本摘要。受信任诊断服务独占 UDS 写权限,把「请求接受」「正响应校验」「目标 ECU 读回/总线行为确认」「tester-present 存活」「恢复结果」设为独立状态;不满足确认条件时不得授予需要静默雷达的纵向执行能力。这一建议比当前源码多了安全门,属于拟议重构,不是现有实现描述。

    验证路径上,可先在无车环境用 alpha_long、RADAR_ACC、SecOC 发布版、不同 Prius EPS 固件和 0x3F6 指纹构造参数矩阵,再用模拟 ISO-TP 正/负响应、无响应及超时检查请求与回执,最后在隔离台架核对 ECU 静默、AEB/FCW、重启与恢复时序。

    常见问题

    disable_ecu() 返回真,是否说明雷达 ECU 已被静默?

    不代表。源文档指出该返回值只反映「本函数的请求路径判定」:通信控制阶段把期望响应前缀设为空字节串并丢弃返回字典,只要前置扩展会话查询产生条目且调用没有抛异常就返回真;get_data() 的 startswith 对空前缀无法鉴别正响应内容。因此这是确认状态缺口,不能断言具体车辆 ECU 的实际状态。

    radarUnavailable 是不是在检测雷达是否在线?

    不是。radarUnavailable 仅测试该车型 DBC 映射是否包含 Bus.radar,不是运行时雷达在线或健康检测;而 DISABLE_RADAR 反映的是参数层是否要发起诊断静默请求。源文档明确「未提供雷达解析器」与「已经成功静默雷达 ECU」是两个不同状态。

    为什么单独调用 _get_params() 复现不了最终 CarParams?

    因为基类 get_params() 先装配平台质量、轴距、转向比和静态 flags,再调用 Toyota _get_params(),末尾的质量、惯量和轮胎刚度计算依赖 Toyota 已设置的 centerToFront 等值。所以只复制 values.py 的标志或单独调用 _get_params(),都不能重建最终结果。

    两种 EPS 固件的 Prius 为什么处理不同?

    因为源码把旧 Prius 由 EPS 固件字节作例外:非指定版本时设 0.25 秒延迟和 0.2 度死区;明确识别到另一不支持的 EPS 版本时标记 dashcamOnly。源文档同时说明,固件列表为空或版本未命中时,不能据此推断该特例得到验证。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TOYOTA_INTERFACE_CAPABILITY_RADAR_DISABLE_DEEP_DIVE.md(取用了参数生成次序、纵向能力决策表、雷达静默事务的确认状态缺口与插件重构边界)。
    • 项目 02 —— openpilot-research/CAR_PROTOCOL_IMPLEMENTATIONS.md(仅用于确认 Toyota 品牌目录与启动识别门的总体位置)。

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

    更新记录

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

  • Honda panda safety:RX/TX 白名单与 AEB 转发

    结论:Honda panda safety 的规则分四段:RX 检查表(地址/总线/长度/频率 + nibble checksum 与二位计数器)、使能状态(主开关、PCM/SET-RESUME 使能沿)、TX 白名单与内容钩子、以及转发钩子。honda_tx_hook() 的 Honda 内容钩子在 controls_allowed=False 时拒绝非零转向,但源码并未在该钩子内对使能时的转向幅度或变化率作额外限制,那部分由主机 CarController 承担。拒帧还可能产生状态副作用:本地 C 探针显示,一帧被拒的主机 0x1FA 仍会先写入全局 honda_brake,从而影响下一次原车 AEB 转发决策。以下为固定源码快照 c8fb906 与本地 C 探针的静态分析,不证明目标板固件编译选项、实际 CAN 总线或整车安全性。

    关键函数与组件

    入口 源码 作用
    honda_nidec_init() / honda_bosch_init() honda.h 按参数选择 RX 期望、TX 地址/总线/长度白名单和平台状态。
    honda_get_checksum() / honda_compute_checksum() / honda_get_counter() honda.h 解码 Honda checksum/计数器,供通用接收检查使用。
    safety_rx_hook() / honda_rx_hook() safety.h、honda.h 通用验证通过且属于 RX 检查表后,才更新 Honda 使能、踏板与 AEB 状态。
    generic_rx_checks() / safety_tick() safety.h 踩刹车等通用退出,以及每秒检查关键 RX 消息是否超时/失效。
    safety_tx_hook() / honda_tx_hook() safety.h、honda.h 通用白名单加 Honda 内容检查;最终还需无继电器故障。
    safety_fwd_hook() / honda_nidec_fwd_hook() safety.h、honda.h 通用阻断冲突帧后,Nidec 对相机原车制动执行动态转发决策。

    RX 与使能状态

    Honda RX 检查表把 SCM_BUTTONS 两种地址、ENGINE_DATA、POWERTRAIN_DATA 纳入核心检查;多数模式还检查 SCM_FEEDBACK,备用刹车模式加 BRAKE_MODULE,Nidec 还检查相机总线 BRAKE_COMMAND。每项声明地址、总线、长度和频率,部分声明最大计数器。Honda 钩子提供 nibble checksum 和二位计数器解码;通用 safety_rx_hook() 仅在消息匹配、校验有效且属于 RX 表时才调用 honda_rx_hook()。safety_tick() 每秒检查迟到/无效关键消息并清除 controls_allowed,不能把 25/100 Hz 配置频率误读为毫秒级 fail-safe deadline。

    honda_rx_hook() 从 ENGINE_DATA 更新是否行驶,从 SCM 更新 ACC 主开关,主开关关闭直接禁止控制。Bosch 原车巡航和 Nidec 使用 PCM 巡航上升沿允许控制;Bosch 巡航退出会撤销许可,但 Nidec 允许在 PCM 退出后继续制动至零。Bosch 自控纵向改用 SET/RESUME 的释放沿使能,MAIN/CANCEL 则退出。用户制动有普通 POWERTRAIN_DATA 的连续开关样本判据或备用 BRAKE_MODULE 来源;随后通用 generic_rx_checks() 再按制动上升沿/车辆运动状态撤销控制。主机侧 CarState 字段不替代 MCU 的这一路独立采样。

    TX 白名单与内容规则

    通用 safety_tx_hook() 先核对当前模式 TX 表中的地址、总线和长度,再调用 Honda 内容钩子,最终还受 relay_malfunction 阻断。Nidec 允许指定转向、BRAKE_COMMAND、ACC_HUD 和 LKAS_HUD;Bosch 普通、纵向、radarless、CAN FD 各有不同表。Bosch 纵向 safetyParam 只在 ALLOW_DEBUG 编译条件下转成纵向模式能力,不能由 Python CarParams 位推断实板支持。

    内容钩子在 controls_allowed=False 时拒绝非零转向;源码未在该钩子内对使能时的转向扭矩幅度、变化率或驾驶员反向扭矩作额外限制。主机 CarController 自身会做变化率和车型 STEER_MAX 处理,但这是与 MCU TX 规则不同的层。当前 Honda 安全测试的转向专项只断言未使能零值可发、非零值被拒,不能把通用车型的转向扭矩安全测试结果自动套给 Honda。Nidec 对 ACC_HUD 的 PCM 速度/gas 和 BRAKE_COMMAND 的制动量做纵向许可检查,并阻止主机设置 AEB 执行位;Bosch 有雷达的 ACC_CONTROL 检查加速度、gas 和 AEB 位,radarless 的加速报文走另一地址与编码。所有这些规则只约束通过当前 TX 表的候选,不能保证 ECU 实际接受。

    Nidec AEB 转发及状态顺序

    相机总线 0x1FA 通过 RX 检查后,如果未设置「禁用原车 AEB」的替代体验位,Honda 钩子比较原车制动力与最近记录的主机制动力:原车 AEB 不活动时清掉转发锁存;AEB 活动且原车制动力不小于主机记录值时置位;较小时保持之前状态。通用转发层先按继电器故障和静态冲突表阻断,Nidec 专属钩子再决定是否允许相机 0x1FA 走向另一总线。

    honda_tx_hook() 读取 Nidec 主机 0x1FA 时,先写全局 honda_brake,后检查制动上限、是否正在转发原车 AEB 和 AEB 位。本地拒帧探针直接编译仓库的 libsafety/safety.c:在 Nidec 模式下,相机 AEB 0x1FA 请求制动 100,基线转发;重新初始化后,先送主机 0x1FA 请求制动 200 且带 AEB 执行位,该帧被 TX 钩子拒绝,再送相同的相机帧,RX 有效但转发被阻断。严格编译和 UBSan 运行输出:

    场景 相机帧 RX 转发 说明
    基线 有效(rx=1) 允许(forward=1) 无先前主机帧干扰。
    先送被拒主机帧 — 候选未被接受(accepted=0) 主机制动 200 且带 AEB 位,被 TX 钩子拒绝。
    随后相机帧 有效(rx=1) 被阻断(forward=0) RX 有效但转发被阻断,证明拒帧有状态副作用。

    这证明当前测试库实现中拒帧可影响下一次 AEB 转发决策,尚不证明目标实板的编译配置、真实消息时序或车辆制动后果。

    自制硬件与插件验收

    车型插件只能提交受限候选,MCU 必须独立持有模式、RX 有效性、TX 白名单、超时与转发规则。Honda 的不同模式要分别验证 PT/camera 总线、备用刹车、Nidec 原车 AEB、Bosch ALLOW_DEBUG 门、诊断固定载荷和失联默认状态。探针应扩展为正式回归测试:对越界、带 AEB 位、因原车 AEB 锁存而被拒的主机 0x1FA,都断言下一帧的转发状态只依据已准许发送的主机制动记录。还需检查使能时转向幅度由哪些独立物理/软件约束承担,不能把 Python 限幅写成 MCU 的保证。本文未运行完整 safety 测试或实板故障注入。

    常见问题

    Honda 的转向扭矩在 MCU 侧会被限幅吗?

    Honda 内容钩子只在 controls_allowed=False 时拒绝非零转向,源码未在该钩子内对使能时的幅度、变化率或驾驶员反向扭矩作额外限制。变化率与车型 STEER_MAX 由主机 CarController 处理,属于不同层次。

    Nidec 用哪种沿使能,Bosch 自控纵向呢?

    Nidec 与 Bosch 原车巡航用 PCM 巡航上升沿允许控制;Bosch 自控纵向改用 SET/RESUME 的释放沿使能,MAIN/CANCEL 退出。Nidec 还允许在 PCM 退出后继续制动至零。

    一帧被拒的主机 0x1FA 会影响后续转发吗?

    会。主机 0x1FA 被 TX 钩子读取时先写全局 honda_brake 再检查,探针显示被拒帧仍能改变下一次相机 AEB 0x1FA 的转发决策,输出为 baseline rx=1 forward=1; rejected candidate accepted=0; stock rx=1 forward=0。

    safety_tick() 是毫秒级 fail-safe 吗?

    不是。它每秒才检查迟到/无效关键消息并清除 controls_allowed,不能把 25/100 Hz 配置频率误读为毫秒级安全时限。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/HONDA_PANDA_SAFETY_RX_TX_FORWARD_DEEP_DIVE.md(RX/TX 白名单、使能状态、Nidec AEB 转发与拒帧探针)。
    • 项目 02 —— openpilot-research/ 同目录 Honda 接口与输出专题(模式参数与控制构帧作为相邻边界参考)。

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

    更新记录

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

  • 横纵向控制算法:四种横向单位与纵向「最小加速度」边界

    结论:横向的四种控制器不共享相同单位——角度、曲率与归一化扭矩只能在各自的 CarParams 契约下解释,不能互相套用阈值;纵向的「最小加速度」合成是偏保守的候选合成,shouldStop 与所选加速度甚至不一定来自同一候选。本文还特别指出:非零求解状态会记录告警并 reset(),但清零后的轨迹仍会被规划器继续处理。以下数值均为源码设定,不是新硬件或新车型的合格阈值;本地缺少 acados 生成代码与模型实体,未构建求解器、未在车辆上验证。

    控制器选择与横向实现

    Controls.__init__() 按 CarParams.steerControlType 优先选择角度或曲率接口,否则按 lateralTuning 选择 PID 或扭矩控制器。Controls.state_control() 根据使能、转向故障、静止条件计算 latActive,由 clip_curvature() 限制曲率变化率、横向加速度和绝对曲率后再调用控制器;这一路仍不是最终 CAN 发送许可。

    实现 关键函数与算法 状态或故障边界
    公共基类 LatControl._check_saturation() 对控制器饱和或曲率受限按 dt 积分;速度、驾驶员施力及安全限幅可抑制计时 reset() 仅清 sat_time,不会自动清子类 PID、延迟缓冲或滤波器
    转角接口 LatControlAngle.update() 用车辆模型把目标曲率转为方向盘角并加学习角偏置 返回的是目标角;执行器变化率仍由车型控制器与 panda 策略约束;角偏置与车辆模型必须随车型校准
    曲率接口 LatControlCurvature.update() 从当前转角估计曲率;配置 PID 时做误差闭环,未配置或驾驶员施力时用前馈 输出限到 MAX_CURVATURE 的 PID 范围,不能把曲率值当作扭矩
    转角 PID LatControlPID.update() 以目标/当前转角误差闭环,转角前馈不含角偏置 安全限幅、驾驶员施力或低速时冻结积分;停用期间不调用 PID update(),原积分未由此清零
    扭矩控制 LatControlTorque.update() 把目标曲率换算成横向加速度,在该域做 PID,并补偿 roll、学习偏置、摩擦和前瞻 jerk 使用 1 秒目标加速度环形缓冲及 jerk 滤波;停用时仍更新缓冲/滤波但不调用 PID,继承的 reset() 不清积分

    PIDController.update() 按速度插值增益,组合 P/I/D 与前馈,对输出限幅并在已限幅方向抑制积分增长;其积分候选也直接以输出上下限裁剪,因此正下限或动态收紧限幅时的内部状态不能按普通「剩余裕度」积分推断。还应注意扭矩控制器的延迟 deque 在停用时仍追加目标,短停再启用可能先读到旧设定值;原测试只覆盖停用满一秒后的缓冲清零。四种横向实现中,扭矩控制的返回值还有符号翻转。

    还有两个跨层边界需要记住:其一,基类 reset() 只清 sat_time,不会清子类 PID、延迟缓冲或滤波器,因此「调用过 reset()」不等于内部状态全部归零;其二,selfdrived 还有独立的欠跟踪、转弯与驾驶员施力门,它们与控制器自身的饱和计时不是同一套判据。插件替换任一横向实现时,若不保留这些内部状态与门控语义,就可能出现停用再启用后的状态残留。

    纵向 MPC、候选合成与执行

    纵向链的关键在于层层限幅与状态门:MPC 生成轨迹,规划器在候选里选最小加速度并做车辆范围限幅,执行层再按车型加速度范围裁剪。任何一层都可能把上游的意图收窄,因此「规划器输出」与「最终发给车型控制器的目标」并不等价。此外,无前车时使用虚拟高速前车是为了维持同一求解模式,而不是真的存在一个前车目标。

    层次 关键函数与数据流 失效语义
    求解器构建 gen_long_model() 设位置/速度/加速度状态和 jerk 控制量;gen_long_ocp() 配目标距离、速度/加速度/障碍约束和软约束成本;SConscript 生成/链接 acados Cython 扩展 生成代码本地缺失;仅阅读 Python 建模不足以证明求解器数值、收敛或耗时
    前车目标 LongitudinalMpc.process_lead() 处理雷达前车;无前车时用虚拟高速前车维持同一求解模式 crash_cnt 用 5 秒内预测距离和前车模型概率累积,规划层再阈值化 FCW;不是独立碰撞检测认证
    求解失败 LongitudinalMpc.run() 设置每节点参数与初始约束、取解及耗时 非零求解状态按条件记录警告并 reset();解轨迹被清零,规划器仍会处理清零轨迹
    候选计划 LongitudinalPlanner.update() 用 MPC 轨迹、巡航加速度及实验模式 e2e 加速度组成候选,选最小加速度并做车辆范围限幅 所选加速度与 shouldStop 不一定来自同一候选;这是保守合成,不是可绕开车型限制的插件接口
    纵向执行 long_control_state_trans() 在 off、stopping、pid 间切换;LongControl.update() 积分/前馈控制并在车型加速度范围裁剪 输出仍需车型编码及硬件安全门,不能当成直接制动扭矩

    get_accel_from_plan() 与 get_curvature_from_plan() 将模型离散轨迹插值到动作时刻;get_cruise_accel() 综合巡航误差、弯道横向加速度、滑行/油门许可与 jerk 限制。插件替换规划器必须保留时间轴、单位、滞后补偿和输出有效位语义。

    变道与警告状态

    DesireHelper.update() 要求横向控制活动、单侧转向灯新沿和最低车速进入准备状态;驾驶员同向施力且无盲区报警后进入变道开始,再结合模型概率、时间与灯状态回到准备或关闭。它输出模型 desire,不直接授权转向。LaneDepartureWarning.update() 在足够车速、近期无转向灯且横向控制未活动时,按车道置信度、距离和变道意图置警告,输出同样不是执行器命令。

    测试与插件边界

    现有纵向状态测试、横向饱和测试与扭矩缓冲测试只覆盖指定车型和局部状态,本轮运行了纵向隔离探针但未运行上述原仓库测试。重构时应分别记录四种横向控制器的内部 PID/缓冲状态、规划器 MPC 状态、车型学习参数和最终 carOutput,并用进程回放、SIL/HIL 及故障注入验证停用、重启和切换。需要明确:selfdrived 的使能/退出与 panda 固件安全限制不能由普通控制插件覆盖。

    常见问题

    横向控制器的曲率值可以直接当扭矩用吗?

    不能。四种横向实现不共享单位:角度输出的是方向盘目标角,曲率输出被限到 MAX_CURVATURE 的 PID 范围,扭矩控制输出的是归一化扭矩,只能各自在对应 CarParams 契约下解释。

    纵向求解失败后会发生什么?

    run() 遇到非零求解状态会按条件记录警告并调用 reset(),将解轨迹与告警时间戳清零;探针确认连续失败可逐次记录,规划器仍会处理清零后的轨迹,实机故障频率未验证。

    「最小加速度」合成等于安全保证吗?

    不等于。它是偏保守的候选合成,所选加速度与 shouldStop 甚至可能来自不同候选;求解器失败、输入无效、车型加速度能力和实际制动执行仍分别由其他层处理。

    停用再启用横向控制会保留旧的积分或缓冲吗?

    可能。继承的 reset() 不清子类 PID 积分、延迟缓冲或滤波器;扭矩控制器的延迟 deque 停用时仍追加目标,短停再启用可能先读到旧设定值,再使能状态需实测。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CONTROL_ALGORITHMS_DEEP_DIVE.md(取用了四种横向控制器、纵向 MPC 与候选合成、变道与警告状态、测试覆盖与插件边界)。
    • 项目 02 —— openpilot-research/PERCEPTION_CONTROL_DEEP_DIVE.md(仅用于确认感知到控制主链路与控制器位置)。

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

    更新记录

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

  • WebRTC 数据桥:bridge_services_in 是请求不是授权

    结论:WebRTC cereal 数据桥的核心边界是——bridge_services_in 只能是「请求」,不是授权结果;/schema 只是结构提示,不是权限清单。出站代理按请求里的主题列表订阅 SubMaster,仅对 updated 的主题发送 {type, logMonoTime, valid, data} JSON;入站代理只取 JSON 的 type/data,用本地 monotonic() 生成新时间、valid 默认 False。因此 sendcan、carControl 与固件配置主题都不应作为通用桥写入目标。

    一、出站:消息内容、时间与编码

    入口 源码行为 边界
    CerealOutgoingMessageProxy.__init__()、update() 按请求主题列表建立 SubMaster;每轮 sm.update(0),仅对 updated 的主题发送 JSON 到所有开放通道。 sm.update(0) 在 asyncio 任务中同步执行;多个服务同轮发布时逐一编码/发送,没有本地有界队列或每主题独立带宽限额。
    to_json()、run() struct 用 to_dict(),list 元素递归处理,顶层 bytes 用默认 UTF-8 decode();异常在任务外层记录后下一轮继续。 任意二进制 Data 不保证是 UTF-8;解码异常会使当前轮 update() 中断。

    出站 logMonoTime 与 valid 来自本地 SubMaster,属于原 cereal 事件的元信息;这两个字段会作为 JSON 呈现给对端,但它们不是入站代理会原样恢复的字段。仅用 updated 判断发送,也不代表服务消息通过了有效性或新鲜度门。

    二、入站:允许列表与重新发布

    入口 源码行为 边界
    StreamSession.message_handler()、run() 内建控制命令单独处理;普通 type 仅在会话的 bridge_services_in 列表里才转给入站代理,并在共享 DynamicPubMaster 创建发布 socket。 允许列表来自本地 /stream 请求体;本机服务没有按主题风险等级的固定全局许可表。
    CerealIncomingMessageProxy.send()、messaging.new_message() JSON 的 type、data 决定新事件类型与负载;非 dict 的 data 用 len(data) 作为 list/Data 大小;新事件 logMonoTime 取本机 monotonic(),valid 默认 False。 入站 JSON 里的 logMonoTime、valid 不被复制;调用点无消息体字节上限、数值范围白名单或单主题频率限制。
    DynamicPubMaster.add_services_if_needed() asyncio.Lock 保护动态加 socket,同一进程的 shared_pub_master 跨会话复用已注册主题。 锁保护的是同进程 socket 注册,不是来源认证或 msgq 主题授权;旧会话注册的 socket 仍可能留在共享实例。

    一个反直觉但重要的边界:valid=False 不等于所有消费端都会丢弃消息。源文档举例 joystickd_thread() 对 testJoystick 只检查接收帧年龄是否超过 0.2 秒,然后取 axes 形成 carControl,该路径没有检查 sm.valid['testJoystick']。这不是「普通远端请求能控制实车」的证明——还受进程模式、selfdriveState、车型参数、card 与 panda 安全门约束——但足以说明 valid 不能替代主题权限判定。

    入站侧还有两处与「成功」语义有关的边界。其一,动态发布器探针确认:同一次和跨次重复主题只建一个 socket,注册序列中后项失败时前项仍留在 socket 里、没有回滚,未知名称会直接下传给 messaging.pub_sock(),实际服务名能否注册由底层决定。其二,handle_post_notify() 向当前会话逐个发送原 JSON,某通道异常被跳过;没有会话或部分发送失败时仍返回 HTTP 200 OK。二者都说明「HTTP 成功」不等于「通知已被远端接收」,也不证明任意名称能成为真实 msgq 主题。

    三、/schema 是结构提示,不是权限清单

    handle_get_schema() 按逗号解析请求服务名,只检查它们是 log.Event 字段且不以 DEPRECATED 结尾,再调用 generate_field()。其中 generate_type() 把 struct 展开为字段映射,list 表示为单元素模板,enum 只写 "text",基础类型写 Cap’n Proto 类型名;generate_struct() 过滤废弃字段。它不包含字段编号、单位、频率、枚举成员、最大长度、schema 版本或读写权限。非法服务触发 assert,由 HTTP handler 的通用异常路径返回 500,而不是专门的 4xx 校验错误。

    源文档的五项原 schema 函数/假类型树探针确认:基础类型保留类型名,枚举压成 text,列表压成单元素模板;struct 和 group 都跳过名为 deprecated 或以 DEPRECATED 结尾的字段。这些输入是合成 Cap’n Proto 类型树,不能证明所有真实 schema 分支都能导出,或其结果能作为授权清单。

    四、硬件与插件重构验收

    普通诊断插件只应授予明确的只读主题 + 字段;写主题必须由宿主按调用方身份、会话、车型状态和安全等级批准。桥接协议应保留原始事件的采样时戳与传输时戳之区别,显式定义 valid 的重建策略、二进制编码方式、schema 版本、最大消息长度、速率和背压;下游不能以 valid=False 自动假设无执行副作用。

    回归覆盖应包含非 UTF-8 Data、嵌套字节字段、未知字段/服务、错误 list 长度、极大负载、重复主题注册、桥接重连,以及 testJoystick 无效位但新鲜消息的隔离行为。现有 WebRTC 会话测试只用 ASCII Data、一组结构/list 输入和单次代理发送,没有 schema 版本、任意二进制、外层有效位/时间戳保留或风险主题授权回归;本篇未运行上游测试,所有写入仍须经过车端独立安全链。

    常见问题

    valid=false 是不是等于消息一定被丢弃?

    不等于。入站代理生成的新事件 valid 默认 False,但消费端未必检查它:joystickd_thread() 对 testJoystick 只检查接收帧年龄是否超过 0.2 秒,不检查有效位。所以不能用 valid 替代主题权限判定,也不应假定无执行副作用。

    bridge_services_in 是授权结果吗?

    不是。它来自本地 /stream 请求体,只是「请求」,本机服务没有按主题风险等级的固定全局许可表。写主题必须由宿主按调用方身份、会话、车型状态和安全等级批准,sendcan、carControl 和固件配置主题不作为通用桥写入目标。

    /schema 能当权限清单用吗?

    不能。它只检查服务名是 log.Event 字段且不以 DEPRECATED 结尾,输出 struct 字段映射、list 单元素模板和 enum 的 "text",不含字段编号、单位、频率、枚举成员、最大长度、schema 版本或读写权限;非法服务还会触发 assert 返回 500。

    桥接协议该保留哪些时间字段?

    应显式区分原始事件的采样时戳与传输时戳。出站的 logMonoTime 来自本地 SubMaster,入站却改用本机 monotonic() 且不复制传入值;若不加区分,下游无法判断数据新鲜度,也无法重建 valid 语义。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/WEBRTC_CEREAL_SCHEMA_BRIDGE_CONTRACT_DEEP_DIVE.md(取用了出站/入站桥接语义、/schema 表达能力边界与插件验收要求)。

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

    更新记录

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