标签: 固件更新

  • 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

  • camerad 原生相机请求环:缓冲生命周期与 enabled 门

    结论:camerad 的原生相机链是一条带请求环的资源生命周期:分配缓冲 → 每帧取槽(request_id % ife_buf_depth)→ 建立 IFE/BPS 同步栅栏 → 调度 req_mgr → 先 poke 传感器再配置 ISP → 消费 SOF 事件、等待栅栏 → 销毁栅栏并补排同槽。它对 Qualcomm/AGNOS 特定 ABI 强耦合,不是通用 V4L2 接口。最需要记住的边界是:enabled 不是运行时发布闸门,且「配置关闭但仍探测」的失败路径不会在析构时执行显式释放。

    一、重要组件与函数

    组件 关键入口 静态行为
    平台主句柄 SpectraMaster::init() 打开 req_mgr、cam_sync、ISP、ICP,查询 ISP/ICP 非安全 IOMMU 句柄并订阅内核 SOF 事件;这是 Qualcomm/AGNOS 特定 ABI,非通用 V4L2。
    命令内存 MemoryManager::alloc_buf()、free() 按精确大小缓存 req_mgr 分配的命令缓冲,取出时 memset;free() 只返还缓存,析构时才 munmap、关闭关联 FD 并释放内核 handle。
    相机开路 camera_open() 探测/获取传感器,计算输出尺寸,配置 ISP/ICP/CSIPHY、链接设备,分配并映射 RAW/VisionIPC 缓冲,预排一环请求;ife_buf_depth 取 VIPC_BUFFER_COUNT 且断言小于 20。
    图像缓冲 CameraBuf::init()、camera_map_bufs() 座舱分配 RAW VisionBuf,三路按各自 stream 创建 NV12 VisionIPC 缓冲,再将帧 FD 映射到 ISP、必要时 ICP 的 IOMMU。
    每帧请求 enqueue_frame()、sensors_poke() 用 request_id % ife_buf_depth 选槽,创建 IFE/BPS sync 对象,向 req_mgr 调度,随后提交传感器 NOP 包,最后配置 IFE 与可选 BPS;源码明确要求 poke 在调度之后。
    事件消费 camerad_thread()、handle_camera_event() poll(POLLPRI) 获取事件,按 session 分给对应相机;通过请求号/帧号校验与栅栏等待后处理帧,销毁当前槽 sync 对象并补排 request_id + ife_buf_depth。
    失败重排 clear_req_queue()、clearAndRequeue() flush ICP 和 req_mgr,请求环所有槽销毁 sync 对象,从新 request ID 预排一环,并以 last_requeue_ts 跳过旧 SOF;重新入队不保证图像时间连续。
    关闭 camera_close()、析构函数 open 为真才调用关闭;关闭内仅在 enabled 为真时 flush、停 ISP/ICP/PHY、unlink 和释放帧映射。

    这些资源的所有权也要分清楚:命令内存的缓存复用只用于 req_mgr 分配的命令缓冲,不等于图像帧缓冲可以无限复用。座舱分配 RAW VisionBuf,三路按各自 stream 创建 NV12 VisionIPC 缓冲,再把帧 FD 映射到 ISP、必要时 ICP 的 IOMMU。而 camera_close() 仅在 open 为真时调用,关闭内又仅在 enabled 为真时 flush、停 ISP/ICP/PHY、unlink 并释放帧映射,因此两个布尔门并不等价于完整的资源所有权。

    二、请求时序与故障语义

    validateEvent() 对 requestId=0 累计次数,超过深度加 2 后重排;正常帧若帧号或请求号跳跃且 skip_expected 为假,也重排。waitForFrameReady() 对 IFE 最多等 100 ms,对存在的 BPS 栅栏再等 50 ms;失败时 handle_camera_event() 不发布该帧,而是清队列重排,以避免读取可能尚未写完的图像。即使 processFrame() 因初始跨相机同步返回 false,只要栅栏完成,当前槽仍会销毁并补排;同步等待失败则由全环重排负责销毁。这些路径的帧缺口和新鲜度应在消费者侧单独观测。

    触发条件 处理方式
    requestId=0 累计超过深度加 2 全环重排。
    正常帧帧号或请求号跳跃且 skip_expected 为假 重排。
    IFE 等待超过 100 ms 或 BPS 栅栏超过 50 ms 不发布该帧,清队列重排。
    processFrame() 初始跨相机同步返回 false 但栅栏完成 当前槽仍销毁并补排。

    三、传感器 NOP 失败后的状态

    sensors_poke() 为当前请求构造无命令缓冲的 CAM_SENSOR_PACKET_OPCODE_SENSOR_NOP 包,它不是新一组曝光寄存器写入。enqueue_frame() 先调度 req_mgr,再调用 poke,最后提交 IFE/BPS 配置。若 device_config() 失败,poke 只记录错误并把 enabled=false,void 返回使调用方无法据此停止后续配置;即便先前的 req_mgr 调度失败,代码也没有跳过 poke 或 ISP 提交。

    enabled 不是运行时发布闸门:handle_camera_event() 没有检查该标志,栅栏完成后仍会补排;camerad_thread() 只按 session 匹配并在返回真时调用 CameraState::sendState(),后者也未按 enabled 阻断图像/状态发布。曝光函数会因 enabled=false 直接返回,但若内核继续给出有效事件,主机代码仍可能发布帧;反过来,若请求不再完成,也可能等待/重排。静态源码不能判定实板最终行为。新硬件适配器应把 NOP/调度/IFE 提交失败作为独立错误状态上报,统一停止排队、发布和清理,而不只改变一个布尔值。

    四、显式释放与硬件移植风险

    1. 双状态门不等价于资源所有权:构造器以配置中的 enabled 初始化状态,但 camera_open() 总先调用 openSensor();后者成功时已经创建 session、获取传感器句柄并写初始化表,再遇 !enabled 才返回,且尚未设置 open=true。因此「配置关闭但仍探测」的路径没有在析构中执行显式 CAM_RELEASE_DEV/DESTROY_SESSION。源码中也有 I2C 失败将 enabled=false 的分支;若在 open=true 后触发,则 camera_close() 会跳过 ISP/ICP/PHY、link 和帧映射的显式释放。进程退出或 FD 关闭后内核是否自动回收,须按驱动和故障注入验证。
    2. 命令失败与后续使用:enqueue_frame() 记录 sync 创建或调度失败,却仍继续 sensor poke、IFE/BPS 配置;后续等待路径断言 IFE sync 对象非零。另 camera_map_bufs() 对 YUV FD 映射结果只记录日志,没有像 RAW 映射一样断言成功。需在台架注入分配、映射和调度失败,验证实际错误传播与重启策略。
    3. 插件边界:图像插件可以提供带 frameId、SOF/EOF、格式和缓冲生命周期契约的帧,但 req_mgr、IOMMU 句柄、sync 对象及内核 ioctl 应由受控平台适配器独占。支持热拔插前,应把句柄获取/释放写成与 enabled 无关的资源状态机,并验证消费者在重排或摄像头禁用时不会把旧帧当新帧。

    常见问题

    ife_buf_depth 有上限吗?

    有。camera_open() 中 ife_buf_depth 取 VIPC_BUFFER_COUNT 且断言小于 20;每帧用 request_id % ife_buf_depth 选槽,因此该值同时决定请求环的槽数与补排步长。

    栅栏等待失败后会发生什么?

    waitForFrameReady() 对 IFE 最多等 100 ms、对存在的 BPS 栅栏再等 50 ms;失败时 handle_camera_event() 不发布该帧,而是清队列重排,以避免读取可能尚未写完的图像。重新入队不保证图像时间连续。

    enabled=false 会阻止帧发布吗?

    不会自动阻止。enabled 不是运行时发布闸门:handle_camera_event() 不检查该标志,sendState() 也未按它阻断发布。曝光函数会因 enabled=false 直接返回,但若内核继续给出有效事件,主机代码仍可能发布帧。

    配置关闭但仍然探测的路径会释放资源吗?

    不会执行显式释放。camera_open() 总先调用 openSensor(),它成功时已创建 session、获取句柄并写初始化表,再遇 !enabled 才返回且尚未设置 open=true;因此析构中不会执行显式的设备释放。内核是否自动回收须按驱动和故障注入验证。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CAMERA_REQUEST_BUFFER_LIFECYCLE_DEEP_DIVE.md(取用了相机请求环组件清单、请求时序与故障语义、NOP 失败后的状态、显式释放与移植风险)。

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

    更新记录

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

  • tinygrad QCOM/KGSL:代际门与外部相机指针映射

    结论:QCOM/KGSL 后端在入口处设了一道明确的代际门——gpu_id[:2] >= (7, 3) 直接抛出不支持异常,而 QCOMCLRenderer 的编译器构造器只接受 a630,二者说明「运行时接受的 GPU ID」不等于「该编译器可用」。外部相机指针映射有一条容易被误读的回退:_gpu_map() 仅当 errno=14 时才把原指针当 GPU 地址,这不能被当作任何普通 CPU 指针的通用零拷贝保证。此外 signal() 的 7.3+ 分支尚未实现,与设备初始化的代际门一致。以下为固定源码快照 c8fb906 及固定 tinygrad 归档的静态追踪,不是 Qualcomm GPU、AGNOS 或车辆上的运行验证。

    入口与编译选择

    AGNOS 上 load_oob() 选择 DEV=QCOM;构建模型的 SConscript 还设置 IMAGE=1、FLOAT16=1 等标志,因此运行产物的设备名、图像布局和编译器必须匹配,改选新 SoC 不能只替换设备字符串。QCOMDevice.__init__() 打开 /dev/kgsl-3d0,创建带优先级/细粒度抢占等标志的 KGSL 上下文,设置功率约束并查询 chip_id;gpu_id[:2] >= (7, 3) 直接抛出不支持异常。

    随后选 QCOMCLRenderer 或 IR3Renderer,安装栈、cache-flush dummy 与 border-color 的缓冲替换规则。QCOMCLRenderer 使用 QCOMCompiler,其构造器只接受 a630;非 aarch64 主机尝试下载编译环境并经 QEMU/Docker 服务编译,不能推断任意被运行时接受的 GPU ID 都能走该编译器。IR3 是另一条编译路径,其可用性同样需要目标平台验证。

    内存与外部相机指针

    QCOMAllocator._alloc() 在 external_ptr 非零时走 _gpu_map(),否则走 _gpu_alloc()。所有权随来源变化:自分配对象走 GPUOBJ_FREE 和 munmap,映射的外部 CPU 指针走 SHAREDMEM_FREE,回退的外部 GPU 指针不调用 KGSL 释放。因此主机析构不能替相机驱动释放底层图像,外部相机缓冲复用必须晚于 GPU 完成。

    函数 源码行为 对车载硬件的契约
    QCOMAllocator._alloc() external_ptr 非零走 _gpu_map(),否则走 _gpu_alloc() 外部指针必须有明确长度、来源和所有者;空指针不进入外部映射分支。
    _gpu_alloc() 按 4 KiB 对齐向 KGSL 分配 GPU 对象,并 mmap 成 CPU 可见地址,可选 uncached/清零 要验证目标驱动的分配标志、CPU/GPU 一致性和峰值内存。
    _gpu_map() 页对齐外部地址与跨度,先调用 CPU dcache_flush(),再尝试 KGSL_MAP_USER_MEM;仅 errno=14 时退回把原指针当 GPU 地址 errno=14 回退不是任何普通 CPU 指针的通用零拷贝保证,须确认指针为 GPU 可用地址且租约有效。
    _free()、_gpu_free() 释放前同步;自分配走 GPUOBJ_FREE 和 munmap,映射的外部 CPU 指针走 SHAREDMEM_FREE,回退的外部 GPU 指针不调 KGSL 释放 所有权随来源变化,外部相机缓冲复用必须晚于 GPU 完成。
    _ensure_stack_size() 设备共享私有栈,仅在需要增长时同步并重分配 内核首次触发更大栈可能引入帧延迟尖峰,须预热与量测。

    QCOM has_copy_queue=False,通用 get_enqueue_devs() 还将 QCOM COPY 排除 HCQ 入队;exec_copy() 会根据缓冲能力选 host 可见复制或其他分支。不能把 Buffer.copy_from() 一律算成 QCOM DMA,也不能只测 kernel 耗时作为整帧推理时延。

    二进制、参数与命令包

    QCOMCompiler.compile() 在 aarch64 上通过专用 LLVM 接口编译/链接,再取设备二进制;checked() 在错误时重建编译实例并抛错,非 aarch64 走编译服务,存在额外环境与网络依赖。QCOMProgramData 依所选 renderer 解析 NIR 或专有库的程序镜像、寄存器占用、私有/共享内存、图像和参数布局;qcom_build_program() 以二进制内容和设备集合缓存程序占位。

    QCOMComputeQueue.kernargs() 把普通缓冲地址、标量、常量、sampler、texture 和 image buffer 编成参数表,NIR 分支还按编译器给出的映射重排图像槽位。exec() 检查资源/维度、取得程序镜像和私有栈,随后设置 Adreno A6xx 寄存器、装载常量与 shader,分别使用 CP_EXEC_CS 或 CP_RUN_OPENCL 启动。cmd()、reg() 将其包装为 Type7/Type4 命令,动态地址由 HCQ 链接期或运行期 patch 处理。

    源码中 exec() 的维度异常条件为「某全局乘局部维度超 65536 且 某局部维度超 1024」;单独超出前者不会由这个条件拒绝。它还单独检查 max_threads < prod(local_size)。这是静态可见的输入检查范围,不等于目标驱动一定接受其余组合;移植测试应分别覆盖每一维边界、组合边界及驱动错误回报。

    提交、缓存与失效

    QCOMComputeQueue.submit() 为命令缓冲构造 kgsl_command_object 和 kgsl_gpu_command,经 ioctl(KGSL_GPU_COMMAND) 提交。memory_barrier() 发缓存写回、失效和等待;signal()、wait() 用 GPU 事件/内存轮询配合 HCQ 时间线,而 signal() 的 7.3+ 分支尚未实现,这与设备初始化的代际门一致。

    QCOMDevice._wait_signal() 在共享信号未达目标值时先尝试 KGSL timestamp 等待,抑制该等待的 OSError/RuntimeError,再调用通用轮询检查;通用 Compiled.synchronize() 还等待 peer pending,并在运行时超时后调用 on_device_hang()。这不是车辆控制的故障降级策略;模型无输出、过期输出和 GPU 挂起应由上层状态/安全链处理,不得以成功提交命令代替有效推理结果。

    验证证据与待办

    验证项 归档测试覆盖 待补
    CPU dcache flush test/device/test_qcom.py 覆盖(仅 ARM64/Clang 条件) —
    KGSL 映射 未覆盖 缓冲租约与缓存一致性
    命令包/时间线/图像参数 未覆盖 帧级尾延迟、错误码与挂起超时
    模型数值 未覆盖 纯读输入与输出数值

    归档中的 test/device/test_qcom.py 仅有一个 ARM64/Clang 条件下的 CPU dcache flush 测试,不覆盖 KGSL 映射、图像参数、命令包、时间线或模型数值;本文没有运行 QCOM 硬件测试。硬件验收需要固定模型产物与目标驱动版本,依次验证缓冲租约/缓存一致性、纯读输入与输出数值、帧级尾延迟、错误码与挂起超时,再验证摄像头断流/重连和降级状态。任何上车步骤都应先在隔离台架执行,保持 panda 独立 safety 边界。

    常见问题

    QCOM 后端支持所有 Adreno GPU 吗?

    不是。QCOMDevice.__init__() 中 gpu_id[:2] >= (7, 3) 直接抛出不支持异常,这是一条明确的代际门;而 QCOMCompiler 构造器只接受 a630,运行时接受的 GPU ID 不等于该编译器可用。

    外部相机指针映射失败会怎样?

    _gpu_map() 先做 CPU dcache_flush() 再尝试 KGSL_MAP_USER_MEM,仅 errno=14 时退回把原指针当 GPU 地址。该回退不是任何普通 CPU 指针的通用零拷贝保证,必须确认指针为 GPU 可用地址且租约仍有效。

    exec() 一定拒绝超维度的启动吗?

    不完全是。维度异常条件为「某全局乘局部维度超 65536 且某局部维度超 1024」,单独超出前者不会被这个条件拒绝;它还单独检查 max_threads 小于 prod(local_size)。这只是静态可见的检查范围。

    7.3+ 代际的 GPU 时间线信号可用吗?

    源文档说明 signal() 的 7.3+ 分支尚未实现,这与设备初始化的代际门一致;GPU 挂起与超时应由上层状态/安全链处理,不能以命令成功提交代替有效推理结果。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_QCOM_KGSL_RUNTIME_DEEP_DIVE.md(代际门、外部指针映射、命令包与提交同步)。
    • 项目 02 —— openpilot-research/ 同目录模型运行时与 HCQ 专题(作为相邻边界参考)。

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

    更新记录

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

  • Toyota panda safety:LKA/LTA 与纵向模式矩阵

    结论:Toyota panda safety 是MCU 本地规则,不验证 SecOC CMAC、雷达静默的正响应或车辆 ECU 是否接受命令。同一套 safety 代码按「原车 / 自控纵向 × 非 SecOC / SecOC × LKA / LTA」组合出不同模式,各模式允许的 TX 帧、长度与字段门都不同,不能把两个标志当成可随意组合。SecOC 相关位只在 ALLOW_DEBUG 构建下生效,上层即便能构帧也不能绕开普通固件或发布版 dashcamOnly 的能力边界。固定快照基线为 c8fb906,没有目标固件、车辆密钥、ECU 或实车验证。

    关键函数与调用顺序

    CarInterface._get_params() 配置 Toyota safety 参数、LTA / 替代制动 / SecOC、原车或自控纵向——注意上层参数和目标 ECU 已接受控制不是同一事实。toyota_init() 从 safetyParam 低八位读取 EPS 扭矩比例,高位选择替代制动、原车纵向、LTA;SecOC 位只在 ALLOW_DEBUG 下生效。

    toyota_rx_hook() 更新 EPS / 驾驶员扭矩、测量角、巡航 / 油门 / 制动和轮速;toyota_tx_hook() 分别检查 ACC、LKA 扭矩、LTA 角度 / 请求位、SecOC LTA2、AEB 和诊断保活。更外层,safety_tx_hook() 与 safety_fwd_hook() 先做地址 / 总线 / 长度白名单,继电器状态与静态转发阻断是独立的最终门。

    模式矩阵

    维度 选择与实际影响
    原车纵向 STOCK_LONGITUDINAL 非 SecOC 的 TX 仅含基础 LKA / LTA / HUD 与 0x343 取消;0x343 必须置 CANCEL_REQ 且加速度为零。
    自控纵向,非 SecOC 在基础 TX 上增加 DSU bus 0/1、PCS_HUD、0x750、0x283;0x343 可携带受限加速度,并另以 check_relay=true 登记。
    SecOC + 原车纵向 0x2E4 长度由 5 改 8,增加 0x131;0x343 仍只准零加速度取消;没有 0x183。
    SecOC + 自控纵向 0x2E4 长度 8,增加 0x131 和 0x183;0x343 只许零加速度,实际加速度走 0x183;没有非 SecOC DSU / 雷达诊断扩展表。
    LTA 标志 0x191 是否可执行受限角度由 LTA 位决定,即使同时设置 SecOC 位也如此;0x2E4 必须零扭矩 / 无请求。LKA 模式相反,0x191 必须无角度执行。SecOC 的 0x131 LTA2 在当前钩子中始终不准执行。
    替代制动 仅非 SecOC、非 LTA 的 RX 分支选择 0x224;普通分支用 0x226。LTA 分支固定普通 RX 表,SecOC 固定 0x101。

    自控纵向表里 0x343 同时经基础宏以 check_relay=false、扩展项以 check_relay=true 列出;通用继电器和转发代码遍历整个表,所以后者仍会触发对应故障 / 静态阻断,不能只看第一条白名单记录。

    RX 信号与健康

    所有 safety RX 均在总线 0。0x0AA 轮速 83 Hz,忽略 checksum / counter,但四个轮速故障位必须为零;0x260 转向传感器 50 Hz 检查 Toyota 累加校验,非 SecOC 的 LTA RX 表还要求 STEER_ANGLE_INITIALIZING=0。SecOC 初始化优先选择 TOYOTA_SECOC_RX_CHECKS,即使 LTA 位同时设置,该质量位也不作为 RX 有效性门。忽略某一完整性字段不等于不要求报文存活。

    报文 频率与门
    0x0AA 轮速 83 Hz,忽略 checksum / counter,四路故障位必须为零
    0x260 转向传感器 50 Hz,检查累加校验;非 SecOC LTA 表要求转角初始化位为 0
    0x1D2 巡航 / 油门 普通车 33 Hz,检查校验
    0x226 / 0x224 制动 40 Hz,忽略校验 / 计数;替代制动改用 0x224
    SecOC 路径 使用 0x176 巡航、0x116 油门和 0x101 制动

    toyota_rx_hook() 从 0x260 读取 16 位 EPS 扭矩,乘 safetyParam 低八位比例后采样,并把窗口最小值减 1、最大值加 1 以覆盖整数取整;驾驶员扭矩和测量角分别取独立字段。测量角仅在初始化位关闭时更新,不能把上一样本当成当前有效角。0x0AA 四轮原始值各减 6767 后求和,和不为零即视为移动。

    TX 权限与未覆盖载荷

    1. 扭矩 LKA:非 LTA 时 0x2E4 检查最大 1500、每帧上升 15 / 下降 25、相对 EPS 实测误差 350、实时差 450 与请求位切断节拍;LTA 模式仅允许零扭矩且请求位为零。SecOC 的 0x2E4 长度为 8,普通车为 5。
    2. 角度 LTA:非 LTA 时 0x191 的两个请求位、角度、TORQUE_WIND_DOWN 都必须不执行;LTA 时角度受速度相关变化率、最大 1657 原始单位及测量角邻域约束,两个请求位必须一致,wind-down 只可为 0 / 100,且驾驶员 / EPS 扭矩过高时禁止 100。SecOC 的 0x131 只允许请求位和角度字段均为零——这不是车辆认证成功的证明。
    3. 纵向:0x343 或 SecOC 0x183 的有符号加速度活动范围 -3.5 至 2.0 m/s²,未激活码为 0;共享门还要求允许控制且未踩油门。
    4. AEB 与诊断:仅非 SecOC 自控纵向白名单包含 0x283,前六字节必须全零,避免主机执行 AEB;0x750 仅允许固定载荷 0F 02 3E 00 00 00 00 00,用于雷达 tester-present。

    自制硬件与插件化契约

    车型插件提交 DBC / 平台、LKA / LTA、SecOC、制动地址及 EPS 比例的证据;未知 / 矛盾组合保持 noOutput。受信任网关独占 SecOC 密钥与签名、UDS 雷达静默、DSU / HUD 模板及节拍;普通插件只提交高层转向 / 加速度 / 取消意图,MCU safety 保留独立限幅。验证分层覆盖模式参数组合、错误 CRC / 质量位、双请求位不一致、wind-down、油门覆盖、非零 AEB、诊断错误载荷、签名失败与失联降级;随后是无负载台架和受控场地。当前没有这些目标车证据。

    常见问题

    LKA 和 LTA 模式能随便组合吗?

    不能。0x191 是否可执行受限角度由 LTA 位决定,即使同时设置 SecOC 位也如此;LKA 模式相反,0x191 必须无角度执行。0x2E4 在 LTA 模式必须零扭矩 / 无请求。未知或矛盾组合应保持 noOutput。

    「四个轮速故障位为零」是不是意味着数据都可信?

    不等于。0x0AA 忽略 checksum / counter,只用四个故障位做门;0x260 才检查 Toyota 累加校验并要求转角初始化位为 0。忽略某一完整性字段不等于不要求报文存活,健康判定要按报文明细区分。

    AEB 帧在 safety 里怎么处理?

    仅非 SecOC 自控纵向白名单包含 0x283,且前六字节必须全零,以避免主机执行 AEB。其它 DSU、PCS_HUD、0x1D2 / 0x412 等白名单帧没有同等级的逐字段门。

    panda safety 通过就等于车辆 ECU 接受命令了吗?

    不等于。这些只是 MCU 本地规则,不验证 SecOC CMAC、雷达静默的正响应或车辆 ECU 是否接受命令。SecOC 相关位只在 ALLOW_DEBUG 构建下生效,也不能绕开 dashcamOnly 等能力边界。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TOYOTA_PANDA_SAFETY_MODE_DEEP_DIVE.md(取用了模式矩阵、RX 健康门、TX 权限与插件化契约分析)。

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

    更新记录

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

  • Ioniq 5 电池预热的 CAN 报文:两段最小可复用样本

    结论:这份档案记录的是现代 Ioniq 5 / 6 / EV6(E-GMP 第一代)电池预热(preconditioning)手动开关背后的最小可用 CAN 报文集合(MWE)。源文档把 2021–2024 年 E-GMP 车上的「触发预热」与「取消预热」压缩成两份可回放的报文集,其中主触发报文 ID 为 0x4ED,取消报文涉及 0x0C7。要点在于:预热可以只用少数几条报文、靠被动旁听加回放实现,不需要车机越狱。需要注意,随本快照归档的是作者过滤后的回放样例,不是原始全总线抓包,也没有配套 DBC。

    车辆与适用范围

    源文档是一个硬件改装的公开文档仓库,围绕「给 Ioniq 5/6/EV6 增加一个手动预热按钮」展开,CAN 报文逆向是其中一环。

    项 内容
    车型 Hyundai Ioniq 5 / Ioniq 6 / Kia EV6(E-GMP 第一代,带电池 PTC 加热器)
    年款 / 市场版本 源文档讨论 2021–2024 年 E-GMP 车;这两份 CSV 的采集车年款/配置/市场/固件未指定
    ECU / 改装状态 加装微控制器注入 CAN 报文(WiCAN 系硬件)+ 线束;也用 WiCAN 网页远程开关
    采集跨度 两份最小可用样本:MWE_preconditioning.csv(230 行)与 MWE_cancel_preconditioning.csv(5 行)
    适用范围 仅限带 PTC 加热器且已启用预热模式的 E-GMP 车;源文档称必要报文于 2026 年 3 月分离出来

    总线与寻址

    源文档的样本以 SavvyCAN 兼容文本保存(时间戳单位为微秒),列含 ID、扩展帧标志、方向、长度与数据字节。抓取工具为 WiCAN Pro(另一份清单说明还用了 SavvyCAN)。下表按这两份 CSV 可确认的内容填写,未给出的一律标注「源文档未给出」。

    项 内容
    物理层 源文档未给出(经 OBD 形态的 WiCAN 接入)
    链路层 / 比特率 源文档未给出
    寻址方式 样本中报文均为 11 位标准帧(Extended=false)
    CAN FD 样本未包含 CAN FD 帧;文档另提及第二代原型机才有一个 CAN FD 接口
    时间戳单位 微秒(SOURCE 说明)

    已解码信号

    下表登记的是这两份 CSV 里可核对到的报文与字段模式,逐条纹目均给出证据(文件名与行数)。源文档把整份材料定位为「MWE 回放样例」,因此没有更细的位域命名与缩放系数;相关的 DBC 由另一项目维护,不在本快照内。

    条目 位置(ID / 字节) 观测载荷(十六进制) 证据
    触发预热帧 0x4ED(8 字节) 00 00 00 00 00 FF FF 1F MWE_preconditioning.csv(230 行)
    触发帧的充电态变体 0x4ED D6–D8 00 00 00 00 00 09 90 00 同上
    取消预热帧 0x0C7(8 字节) 00 00 00 E0 07 00 00 00 MWE_cancel_preconditioning.csv(5 行)
    取消帧的状态字段 0x0C7 D4–D5 E0 07 同上
    触发日志中的另一变体 0x0C7 D4–D5 40 03 MWE_preconditioning.csv
    空闲/复位帧 0x0C7(全零) 00 00 00 00 00 00 00 00 两份 CSV 首帧
    采样工具 — SavvyCAN + WiCAN Pro README「Contents」
    样本规模 — 触发 230 行 / 取消 5 行 SOURCE 说明表

    读取方法与注意事项

    源文档给出一条很实用的方法论主张:一两份好日志比十份可疑日志更有价值。作者建议把日志采集当成一次科学实验——设计「测试条件」与「对照条件」,例如把导航设到附近的充电桩 vs. 设到附近的学校。若都没把握,就对大范围报文画时间曲线找线索。

    数据清理同样关键:作者提到清洗日志(例如剔除带缓冲数据造成的超范围时间戳)后才取得更好效果。文档还列出成套公开工具(含 SavvyCAN 社区分支、DBC 结构入门、基于 DBC 的绘图库等)。

    工程实现上,这套方案由五部分构成:CAN 报文、注入报文的微控制器、该微控制器的固件、把微控制器接到车的线束、以及触发预热的用户界面。触发方式从「用笔记本」到「用 panda 上的星形按钮」都有演示,当前用户界面支持映射到车上既有按钮,物理按钮仍在开发。作者也提醒:另一条「车机越狱」路线更偏技术、没有恢复出厂设置的兜底,且可能被后续 OTA 封堵。

    证据等级与边界

    本档案的证据层级需要分清。可核对的是文件与行数(230 行、5 行)、报文 ID 与字节(0x4ED、0x0C7 的观测载荷),以及工具链(SavvyCAN、WiCAN Pro)。而「这些报文在另一台车上就能触发预热」属于作者自述、未被本归档独立验证——来源说明明确写道:上游称这些报文能在带 PTC 的 Ioniq 5 上启动/取消预热,但本归档未独立复核解码、回放行为或兼容性。

    三处不能外推。第一,这是作者过滤后的回放样例,不是原始全总线抓包,也不是 DBC,因此不能据此推出整车信号全集。第二,采集车的具体年款/配置/市场/固件未指定,不能默认对所有 E-GMP 车成立。第三,文档提到后续 E-GMP 平台与软件版本可能变化,且越狱路线曾多次被封堵,说明结论有时效性。

    关于「安全访问」类问题:本文只整理公开文档登记的回放样例,未涉及安全访问的绕过;源文档未提供绕过方法,且不建议尝试。

    常见问题

    预热功能是主动问出来的还是被动听到的?

    这份材料记录的是可回放的最小报文集合:两段日志里反复出现的少数几条报文,被整理成可发送的样例。它不涉及对某个 ECU 的主动诊断轮询。

    0x4ED 和 0x0C7 分别是什么?

    按样本,0x4ED 出现在触发预热日志里(230 行,载荷在 FF FF 1F 与 09 90 00 之间切换),0x0C7 出现在取消日志里(5 行,含 E0 07)。本档案只登记观测到的载荷,不代填位域语义。

    能不能套用到其他 E-GMP 车?

    不能默认成立。来源说明指出,这两份 CSV 的采集车年款、配置、市场与固件均未指定,且本归档未独立复核解码与回放行为;要用于其他车辆须自行在实车上验证。

    这套方案需要车机越狱吗?

    不需要。源文档给出的主线是「微控制器 + 线束 + 用户界面」注入报文;文档另提到一条车机越狱路线,但指出它更偏技术、缺少恢复兜底,且可能被后续 OTA 封堵。

    延伸阅读

    参考资料

    • 项目 01 —— public-can-dbc-datasets/additional/tylerharvey-ioniq5-preconditioning/README.md(本项目车型档案主来源:项目背景、MWE 报文、方法论与工具清单)。
    • 项目 01 —— public-can-dbc-datasets/additional/tylerharvey-ioniq5-preconditioning/SOURCE.md(两份 CSV 的行数、校验值,以及「未独立验证」的边界声明)。
    • 项目 01 —— public-can-dbc-datasets/additional/tylerharvey-ioniq5-preconditioning/preconditioning_messages/MWE_preconditioning.csv(触发预热的最小可用样本)。

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

    更新记录

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

  • 启动辅助界面:有限 Markdown 方言、进度管道与退出码协议

    结论:启动辅助界面的三块内容各有明确的契约边界:发布说明走的是一个「有限 Markdown 方言」,只把 === 后的行包成 <h1>、* 行转成列表,末尾只替换 & 与双引号,不转义 < 与 >;进度窗口通过子进程标准输入按行传文本或百分比,空行中断本帧读取而 EOF 不会主动退出;错误窗口的宿主 wait_for_exit() 在成功创建子进程后只接受退出码 1,而 PC 正常关闭路径的退出码预期是 0,可能造成无限等待。以下为固定源码快照 c8fb906 与无 GUI 探针的静态结构分析,未启动构建、注册、更新、raylib 窗口或车载设备。

    发布说明的有限 Markdown 方言

    parse_markdown() 按换行拆分:后一行以 === 开头时把当前行包为 <h1>,以 * 开头的行转为 <ul>/<li>,并根据前导空格和 tab_length 粗略处理嵌套;其他非空行原样输出。它不是通用 Markdown 解析器,不支持标题等级、代码块、链接和转义规则的完整语义。输入以 ===anything 开头也满足标题分隔条件,非规范缩进可能与内部 list_level 不一致。

    末尾统一把 & 变为 &amp;、双引号变为 &quot;,不转义 < 与 >。无设备探针中,输入 <b>note</b> & x 得到 <b>note</b> &amp; x。下游 HtmlRenderer.parse_html_content() 是 raylib 文本渲染器,只识别有限标签,不是浏览器执行环境;但原始 <b> 等受支持标签仍能改变显示结构,不能把这函数当作对任意来源文本的 HTML 净化器。

    updated.parse_release_notes() 从 RELEASES.md 取第一个双换行前的最新说明,UTF-8 解码后调用上述函数;解析异常则回退原始字节加换行,文件缺失则返回空字节。结果写入更新参数后,由行车外告警页 ReleaseNotesDialog.refresh() 或软件设置项显示。现有 test_markdown.py 只遍历发布说明段并检查转换结果非空,没有验证标签转义、嵌套列表或异常输入。

    构建/注册进度窗口

    宿主 common.Spinner 通过 Popen(["./spinner.py"], stdin=PIPE, cwd=system/ui) 启动独立 UI,启动 OSError 只留下 None。update() 向标准输入写入 UTF-8 文本加换行再 flush,它只在 flush 周围捕获 BrokenPipeError,写入本身若抛错会向调用方传播。update_progress(cur,total) 计算并四舍五入百分比,宿主端不检查 total=0 或范围。

    契约点 源码行为 探针确认
    进度写管道 update() 写 UTF-8 文本加换行,只在 flush 周围捕获 BrokenPipeError 1/4 写为 25n;零分母抛错;flush 断管被吞、write 断管传播。
    数字解析 子进程 set_text() 对 str.isdigit() 为真的文本尝试 int() 并裁到 0..100,其余换行为文字 -1 是文字;某些 Unicode 数字如 ² 通过 isdigit() 却在 int() 抛 ValueError,中断当前 UI 主循环。
    帧读取 _read_stdin() 用零等待 select 读取就绪行并 strip() 两端空白,读到空行或 EOF 就结束本轮 空行之后尚存输入留待下一帧;main() 每帧只取本次读到的最后一行,EOF 本身不会主动关闭窗口。
    子进程回收 Spinner.close() 发 kill(),再 communicate(timeout=2) 回收 与 TextWindow 的终止策略不同。

    manager.build() 用它显示 scons 构建进度,失败时关闭 spinner 并打开错误窗口;registration.register() 在需要在线注册时显示阶段文字。二者的进度窗口只是呈现,不是构建/注册完成的证明;真正结果仍由子进程返回码或注册 API 决定。

    错误窗口的等待与退出码

    场景 子进程退出码 宿主 wait_for_exit() 行为
    创建失败 — text_proc is None,直接返回,不会无限循环
    PC 正常关闭 预期 0(源码无显式 sys.exit(1)) 只接受 1,状态 0 每 0.1 秒继续轮询,无 deadline
    设备 Reboot 路径 源文档未给出显式 sys.exit(1) 同上,可能无限等待(静态推论,未 GUI 实测)

    宿主 common.TextWindow 以错误文本为进程参数启动 system/ui/text.py,失败时将 text_proc 设为 None。get_status() 调用 poll() 返回退出码或 None;wait_for_exit() 在成功创建子进程后只接受退出码 1,状态 0 或其他值每 0.1 秒继续轮询,没有 deadline。创建失败后 text_proc is None,则直接返回,不会无限循环;close() 只发 terminate()、置空引用,没有 wait() 确认回收。

    UI 子进程 system/ui/text.py 在 PC 上显示 Exit,在设备上显示 Reboot;按钮先调用 gui_app.request_close(),设备上再调用 HARDWARE.reboot()。GuiApplication.render() 在关闭请求后自然结束循环,text.py 的 __main__ 没有显式 sys.exit(1)。因此在普通 PC 正常关闭、脚本正常返回的源码路径上,子进程预期退出码是 0,而宿主只接受 1,可能无限等待;这是跨进程静态推论,尚未用 GUI 实测。

    system/ui/text.py 自有的 wrap_text() 不调用通用 lib.wrap_text():它按段落拆开、保留中间/尾部空段,但丢掉前导空段;若首个词本身超宽,会先插入空字符串行,随后仍保留完整超宽词,不执行词内断行。探针确认 abcdefgh 在模拟宽度 4 下输出 ["", "abcdefgh"],a b 在宽度 1 下还可能保留行尾空格。错误追踪中的长路径或不可断词因此可能越过文本视口。

    插件化与验证要求

    源文档给出的是建议而非已实现能力:更新或插件提供的说明文本须明确来源和信任级别,如需富文本应用结构化、白名单标签生成,不能依赖 parse_markdown() 的 &/引号替换完成净化;进度 UI 的输入应是受限状态对象(阶段、0..100 进度、错误码),而不是让任意插件直接写入引导进程标准输入;启动/故障窗口应受进程监督,退出码、存活检测、最大等待时长、终止后的 wait() 与无 UI 回退日志都需有契约;故障窗口不得阻断安全关机或设备恢复。

    已用无设备探针覆盖宿主子进程正常 0/异常 1/无法启动及进度管道 write/flush 断开,另覆盖进度 UI 输入/帧选择和错误 UI 换行/按钮回调;卡住进程的真实资源回收、UI 渲染与设备 Reboot 动作仍需台架验证。

    常见问题

    parse_markdown() 会转义 HTML 吗?

    只替换 & 与双引号,不转义尖括号。探针中一个 b 标签加上 & 与 x 后,b 标签仍原样保留,& 则被编码。它不能当作对任意来源文本的 HTML 净化器。

    进度条输入一个负数会怎样?

    子进程 set_text() 对 str.isdigit() 为真的文本才尝试 int(),-1 因不是 isdigit() 而被当作文本;而某些 Unicode 数字如 ² 通过 isdigit() 却在 int() 抛 ValueError,会中断当前 UI 主循环。

    为什么错误窗口可能一直不退出?

    宿主 wait_for_exit() 在成功创建子进程后只接受退出码 1,而 PC 正常关闭路径的 text.py 退出码预期是 0,状态 0 会被每 0.1 秒继续轮询且没有 deadline,因此可能无限等待;这是静态推论,尚未用 GUI 实测。

    进度窗口显示完成就代表任务完成了吗?

    不代表。进度窗口只是呈现,真正结果仍由子进程返回码或注册 API 决定;进度消息也不是可靠队列或确认协议。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/BOOTSTRAP_UI_MARKDOWN_PROCESS_CONTRACT_DEEP_DIVE.md(Markdown 方言、进度管道、退出码协议与探针结论)。
    • 项目 02 —— openpilot-research/ 同目录富文本与更新流程专题(作为相邻边界参考)。

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

    更新记录

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

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

    结论:界面告警、驻车更新、云端连接与设备诊断这些进程大多不在控制闭环内,但它们决定驾驶员能否看见告警、设备能否更新、以及远程能否观测。源文档给出的一条清晰边界是: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

  • 车载硬件状态:300 秒驻车、11.8 V 与 45 W 的关机链

    结论:硬件状态的信任缺口集中在两处:缺测被静默当成 0(ThermalZone.read() 在温度文件不存在时返回 0,HardwareBase 多数方法是空操作或零值默认),以及慢速链没有采样时间戳(hw_state_thread() 消费端无采样时间,旧网络/USB 状态可能继续出现在 deviceState)。此外驻车容量是估算值而非车载 BMS 实测电量。以下基于固定源码快照 c8fb906 的静态分析与离线探针,不是实板验证。

    平台抽象与 hardwared 线程

    平台选择在导入时就固定:有 /AGNOS 文件则用 HardwareComma,否则用 HardwarePc,并非运行时可插拔发现。HardwareBase 除 get_device_type() 外多数方法有空操作或零值默认实现——移植时不能把「继承成功」等同于「能力可用」。

    组件 / 函数 输入与输出 实际职责
    HARDWARE 选择 /AGNOS 文件 → HardwareComma 或 HardwarePc 导入时固定平台实例,并非运行时可插拔发现
    ThermalZone.read()、ThermalConfig.get_msg() sysfs 温区 → *TempC 字段 按温区名称查找并读取;温度文件不存在返回 0,不是显式失效标志
    hw_state_thread() 网络、调制解调器、USB → 容量 1 的队列 每约 10 秒或 USB 拓扑变化时抓取慢速状态;队列满或采集失败会消耗本次触发,主线程沿用旧值
    hardware_thread() pandaStates、peripheralState、温度、Params → deviceState 点火边沿和约 2 Hz 周期更新,处理启动门、热状态、风扇目标、功耗与关机请求
    FanController.update() 温度、点火 → 目标风扇百分比 积分 PID 加前馈,点火变化重置
    PowerMonitoring.calculate() 车端电压、点火 → 累计耗电 / 剩余容量估计 驻车按设备功率积分,点火时按固定 45 W 充电率估计;约 10 秒保存 CarBatteryCapacity

    点火、热状态与行车资格

    pandaStates 中任一已知 panda 的点火线或 CAN 点火信号可建立点火;超过 5 秒未收到状态则清除点火,但线束连接状态 in_car 仅在收到 panda 状态时更新。行车资格分两层:已有行车状态要求点火、无 onroad cycle、未达到 critical;从驻车新启动还要求条款/培训、空间、设备已启动、温度低于 overheated、非卸载等启动条件。

    因此热状态迟滞允许「过热但未严重」的在途状态暂时保持,不能把「禁止进入行车」误写成「立即退出行车」。温度源分两组:驻车危险检查用内存、CPU、GPU 的最大值,行车热状态再加入 PMIC,两者都经 5 秒一阶滤波;mici 与其他设备采用不同热带阈值,驻车高温可直接升到 critical。风扇目标在温度告警时被强制为 100%,随后经 pandad 发给 panda;panda 回报的转速另由 selfdrived 检查。

    风扇 PID 与温度带

    风扇与温度构成一条闭环:温度决定目标百分比,panda 执行并回报转速,故障事件则由 selfdrive 侧产生。关键参数如下。

    项目 参数 / 行为
    风扇控制 积分 PID 加前馈,点火变化时重置
    行车风扇范围 30–100%
    驻车风扇范围 0–30%
    温度告警 风扇目标强制 100%
    温度滤波 5 秒一阶滤波
    温度源 驻车用内存/CPU/GPU 最大值;行车再加入 PMIC

    电源估算与关机条件

    普通关机不是单条件触发,而是「前置状态 + 多个触发之一」的组合;ForcePowerDown 能绕过普通条件,但仍受末尾约束。

    条件 阈值 / 机制
    普通关机前置 驻车、连接车辆、未禁用关机、驻车超过 300 秒
    触发之一 驻车超过 30 小时
    触发之二 低通电压低于 11.8 V
    触发之三 估计容量耗尽
    点火充电率 按固定 45 W 估计
    ForcePowerDown 绕过上述普通条件,但仍受末尾的 started_seen / 最短开机时间约束

    hardwared 只写 DoShutdown;真正调用关机的是 manager.main()。

    缺测的零值风险与插件契约

    两个具体风险值得单独记住:hw_state_thread() 消费端无采样时间戳,旧网络/USB 状态可能继续出现在 deviceState;ThermalZone.read() 把缺失温区当 0。这类慢速状态之所以能继续沿用旧值,正是因为消费端没有采样时刻可供判断新鲜度;新平台应给每条状态附上采样时间与失效哨兵。这些是移植设计风险,需要在目标平台上显式处理,而不是沿用零值默认。

    因此新平台要显式声明能力与缺测状态:标定 sysfs 温区名、单位、采样频率、失效哨兵;定义电压测点、功率方向与关机电气动作;点火、温度、关机和风扇属于可信任设备服务,需要时效性、故障降级和唯一发布者。硬件插件不得自行宣布 deviceState.started 或绕开独立 panda 安全门。

    外置计算模块与触摸记录

    同一条 hardwared 线程里还挂着外置计算模块与触摸记录两条支线。Chestnut.update() 在驻车时限次刷固件,并对外置 GPU 的丢电、链路、过热、模型失败分别生成不同告警;chestnut_state_thread() 读取供电与 PCIe 状态,并按有效位合并 GPU 状态。也就是说「外置模块可用」本身也是一组需要区分失效类型的判定位,而不是一个布尔值。

    触摸记录则只属诊断路径:touch_thread() 仅在 comma 硬件启动,按事件帧分组发布到原始 touch 主题,UI 交互另由 raylib 采集,两者不应混为一谈。此外,hardware_thread() 里的 ign_edge 是「已启动状态」与 onroad 条件的持续差异,不一定是单次点火边沿:启动条件受阻而 onroad 条件仍真时,它会持续绕过约 2 Hz 的周期门,device_booted 一旦为真也会在本进程锁存。

    常见问题

    温度读不到会怎样?

    ThermalZone.read() 在温度文件不存在时返回 0,这不是显式失效标志。缺测不能被静默当作 0 °C,新硬件平台必须明确失效处理。

    普通关机需要满足哪些条件?

    前置是驻车、连接车辆、未禁用关机且驻车超过 300 秒;再由超过 30 小时、低通电压低于 11.8 V、或估计容量耗尽三者之一触发。

    驻车容量是车载 BMS 实测电量吗?

    不是。它是估算值:无点火时按平台报告的设备功率积分,有点火时按固定 45 W 充电率估算,并非测得的真实电量。

    过热会立即退出行车吗?

    不一定。热状态迟滞允许「过热但未严重」的在途状态暂时保持;「禁止进入行车」与「立即退出行车」是两条不同的判定。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/HARDWARE_STATE_DEEP_DIVE.md(硬件状态、供电与热管理,含源码行号索引)
    • 项目 02 —— openpilot-research/probes/test_power_monitoring_contract.py(电源估算与关机条件的离线探针)
    • 项目 02 —— openpilot-research/probes/test_fan_controller_contract.py(风扇 PID 上下限与积分的离线探针)

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

    更新记录

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