标签: openpilot

  • 摄像头采集与 IMU 采样:SOF/EOF、18 个缓冲与轴系契约

    结论:车载视觉与惯性输入必须把图像时间、帧同步、曝光、IMU 时间和故障降级当作独立契约,不能只满足「能输出 NV12 / XYZ」。原生 camerad 与 sensord 是两个独立时钟源的生产者:logMonoTime 是发布封装时刻,不等于 IMU 中断时间或图像 SOF;源码中的 EOF 是由内核 SOF 时间加传感器读出时间推算的,不是直接测得的硬件 EOF。固定快照基线为 c8fb906,本文为静态代码阅读,未连接相机模组、I2C / GPIO 实板或路测数据。

    原生摄像头:启动到发布

    进程选择由 USE_WEBCAM 决定:置位时原生 camerad 不启用;原生程序设置 CPU 亲和性后进入 camerad_thread()。该 C++ 目标在非 Darwin 平台构建,PC / macOS 不能仅因目录存在便声称原生采集可运行。

    板级配置里广角、窄角、座舱固定 camera_num 0 / 1 / 2,各有 VisionIPC stream、焦距、发布主题和 PHY;窄 / 广角同步 SOF,座舱期望半周期错相。即使禁用某路图像,源码仍会尝试探测 / 配置传感器,以避免 strobe GPIO 电气副作用。设备初始化打开 Qualcomm req_mgr / cam_sync / ISP / ICP,订阅 SOF 事件;逐路先探测 OS04C10、再探测 OX03C10,探测失败会禁用该路。

    阶段 实现事实
    缓冲 为处理模式分配原始缓冲,并为各流创建 18 个 YUV VisionIPC 缓冲;发布时附带 frameId、SOF、EOF。
    逐帧 poll 读取 req_mgr SOF,按 session_handle 路由,验证请求 / 帧序号,等 IFE / BPS sync,处理帧后送 VisionIPC 与对应 *CameraState,再按亮度更新曝光寄存器。
    状态消息 记录帧号、requestId、SOF / EOF、曝光 / 增益及处理耗时;图像主数据走 VisionIPC,FrameData 是另一条状态消息。

    图像主数据走 VisionIPC,而 FrameData 是另一条状态消息;FD 握手、缓冲复用与通知一致性需与图像数据契约分开验证。

    同步与故障边界

    validateEvent() 对 requestId=0、帧号 / 请求号跳变重排队;waitForFrameReady() 等 IFE 最多 100 ms、BPS 最多 50 ms,失败清队列并重排,避免使用可能撕裂的帧——该恢复动作不等于图像连续性已保住。

    syncFirstFrame() 要求非错相路面相机约 0.2 ms 内、错相座舱相机在 25 ms 偏移的约 0.2 ms 内对齐。若 IFE 帧号超过 40 仍未同步,源码记录超时并将 first_frame_synced 设真,后续可继续发布;因此「有图像」不证明同步质量合格。processFrame() 用内核 SOF 时间加传感器 readout_time_ns 推算 EOF;set_camera_exposure() 根据抽样 Y 平面亮度、目标灰度和前 3 帧曝光状态调整积分时间、模拟增益及高转换增益,测光区域依赖焦距 / 输出尺寸。替换镜头 / 传感器必须重新标定焦距、ROI、曝光延迟和增益表。

    webcam:开发替代,不等价于车载模组

    Camerad.__init__() 从环境变量选择窄路面、可选广路面和驾驶员摄像头,创建 VisionIPC 缓冲;Camera.read_frames() 经 OpenCV 读取、旋转 180 度并用 PyAV 转 NV12;camera_runner() 以 20 Hz 发布。_send_yuv() 的 SOF / EOF 值用 frame_id * 0.05 s 合成,FrameData 只填帧号与单位变换;没有原生传感器 SOF / EOF、跨相机硬件同步、曝光 / 增益状态。它是开发输入替代,不足以验证车载相机时间契约。

    sensord:采样、坐标与时间

    I2C bus 1 的 LSM6DS3 加速度、陀螺仪和温度共享设备;先 reset / init,再启动 IRQ 和温度轮询线程。GPIO 边沿时间戳来自内核,用 time.time_ns - time.monotonic_ns 折算到单调时钟;时钟偏移突变超过 10 ms 时跳过当次事件并更新偏移。

    层 实现事实
    加速度 读取 data-ready 与三轴寄存器,按 ±2g / 16-bit 换算 m/s²,输出轴顺序为 [y, -x, z];没有 data-ready 时不发布。
    角速度 读取 data-ready 与三轴寄存器,按量程换算 rad/s,同样输出 [y, -x, z];与加速度共享 IRQ,但逐传感器检查可读状态。
    温度 按 2 Hz 轮询,使用单调时间并按芯片变体选温度比例。
    启动有效性 从首次调用有效性检查起,前 0.5 s 不发布有效数据;自测仅在对应环境变量触发。

    传感器插件如果改轴定义而不更新定位观测模型,会产生系统性估计误差。定位 / 模型消费端应依据观测时间戳和有效性判断新鲜度,而不是依赖发布时刻的 logMonoTime。

    验证状态与硬件任务

    test_camerad.py 标记 COMMA_HARDWARE_TEST,覆盖曝光收敛、帧连续、路面同步 / 座舱错相及故障注入;test_sensord.py 同样要求硬件,检查频率、时间戳、数值范围和中断停机。本次仅确认测试存在,未运行。自制硬件的最小验收应记录:各路图像 SOF / EOF / 帧号、读出时间、丢帧及重排次数、测光 / 曝光响应;IMU IRQ 时间、轴向 / 量程、启动稳定、掉线和时钟跳变。再以回放、台架和实板测试逐项比较,不以 webcam 或仿真通过替代。

    常见问题

    「有图像」为什么不能说明相机同步合格?

    因为 syncFirstFrame() 在 IFE 帧号超过 40 仍未同步时,源码记录超时并直接把 first_frame_synced 设真,后续可继续发布。允许发布不等于对齐质量达标,需记录实际对齐误差与重排次数。

    图像 EOF 是硬件测得的吗?

    不是。processFrame() 用内核 SOF 时间加传感器 readout_time_ns 推算 EOF,文中 EOF 并非直接测得的硬件 EOF。新相机插件必须提供时间域、读出时间、请求序列及同步质量,不能把采集完成时刻冒充 SOF。

    IMU 的 logMonoTime 就是采样时间吗?

    不是。logMonoTime 是发布封装时刻,不等于 IMU 中断时间;GPIO 边沿时间戳来自内核再折算到单调时钟。定位 / 模型消费端应依据观测时间戳和有效性判断新鲜度。

    IMU 轴顺序为什么重要?

    因为 LSM6DS3_Accel 与 LSM6DS3_Gyro 都输出 [y, -x, z]。传感器插件若改轴定义而不更新定位观测模型,会产生系统性估计误差;新硬件必须显式定义启动稳定时间、量程、轴系和异常状态。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CAMERA_IMU_DEEP_DIVE.md(取用了摄像头启动到发布、同步 / 故障边界、webcam 替代路径与 sensord 采样轴系分析)。

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

    更新记录

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

  • cereal 消息契约:schema、服务表、valid 与落盘各管什么

    结论:cereal 的消息契约必须拆成四层看:schema 定义、services.py 服务表配置、运行消息有效性、loggerd 落盘结果。union 分支存在不表示进程正在发布,也不表示 loggerd 会记录;logMonoTime 是消息封装时间,valid 也只表示运行有效位,不承诺车辆总线帧通过车型解析或 panda 安全检查。把「字段存在」当作「数据已发布或已记录」是这份契约最常见的误读。

    四层契约各管一段

    schema 层,Event 有 logMonoTime、valid 与 union,carState、carControl、carParams 指向 opendbc 的 car.capnp,customReserved* 保留固定 field ID。服务表层,SERVICE_LIST 声明服务名、期望频率、should_log、qlog decimation 和队列容量,build_header() 生成对应的 C++ 表——频率是配置与健康检查依据,不等于真实发布速率。

    消息构建层,new_message() 默认 valid=False,把 logMonoTime 设为单调时钟纳秒并初始化主题,发布者通常再赋有效位。日志订阅层,loggerd_thread() 订阅 should_log 服务、非直播 EncodeData 和可选原始音频;普通消息按配置写 rlog,按计数抽样写 qlog。因此收到消息、valid=True、内容可信和时戳新鲜是四个不同判断。

    层 源码与含义 不能推出的结论
    schema Event 有 logMonoTime/valid/union;车型消息指向 car.capnp union 分支存在不表示进程正在发布或 loggerd 会记录
    服务表 SERVICE_LIST 声明频率、should_log、decimation、队列容量 频率是配置依据,不等于真实发布速率;should_log=False 也可能有专门编码路径
    消息构建 new_message() 默认 valid=False,设置单调时间 收到、有效、可信、时戳新鲜是不同判断
    日志订阅 loggerd_thread() 按 should_log 与计数抽样写盘 qlog 不是每个主题的完整回放数据

    关键主题与字段语义

    CAN 接入侧,Event 的 can 是从车接收帧,sendcan 是主机候选发送帧;CarState.canValid/canTimeout/canErrorCounter 表示车型解析健康,不能只取上次解码值。services.py 为 can 设大队列与 qlog 高抽样率,因此完整 CAN 分析应使用 rlog。执行决策侧,enabled、latActive、longActive 不等价,Actuators 的扭矩/转角/曲率属互斥横向命令语义,纵向 accel 为 m/s²。

    模型到规划侧,modelV2 的 qlog 抽样未配置,不应拿 qlog 复原所有推理帧;LateralManeuverPlan 是调试覆盖曲率,不是一般模型输出。使能与监督侧,enabled 与 active 分开,警报文本/声音、人机监督策略与注意力状态在独立消息中。摄像头与编码侧,帧标识、曝光时间戳、编码索引/时间戳和视频字节是不同层,图像主数据经 VisionIPC,EncodeData 由 loggerd 分离写媒体文件。

    链路 载荷定义 核心字段与边界
    CAN 接入 Event.can/sendcan、CarState canValid/canTimeout/canErrorCounter 表示解析健康
    执行决策 CarControl、CarParams enabled/latActive/longActive 不等价;safetyConfigs/dashcamOnly/passive 决定执行边界
    模型到规划 ModelDataV2、LongitudinalPlan、LateralManeuverPlan 模型含帧 ID、EOF 时间、耗时、轨迹;modelV2 qlog 抽样未配置
    使能与监督 SelfdriveState、DriverMonitoringState enabled 与 active 分开;监督策略独立消息
    panda 安全状态 PandaState controlsAllowed、模式/参数、RX 无效、TX 阻断、心跳丢失分列
    摄像头与编码 FrameData、EncodeIndex、EncodeData 帧标识、时间戳与视频字节分层;EncodeData 分离写媒体文件

    需要单独记住:Event 的 logMonoTime 是消息封装时间;相机 timestampEof、模型 timestampEof、GNSS Unix 时间和 IMU 自有时间字段并不自动相等。传感器/定位插件必须显式声明时钟域、单位、采样时点和迟到处理。

    Route 与 Segment 文件如何落盘

    元数据由 logger_build_init_data() 记录版本、设备、内核、Git 信息、参数和部分硬件日志,参数值只有不带 DONT_LOG 标志才写入。命名/开段阶段,路由名由递增计数与随机后缀组成,段目录为 <route>--<part>;开段创建 rlog.lock、rlog.zst、qlog.zst,写 initData 与 START sentinel,结束段/路由写 END sentinel 并移除锁。

    压缩阶段,rlog.zst、qlog.zst 是 zstd 压缩的连续 Cap’n Proto Event 字节流,qlog 只在抽样命中时附写。切段时,rotate_if_needed() 与 handle_encoder_msg() 等已出现的编码器进入新段后切段,相机/编码器异常时按时间回退切段;新段编码数据须等关键帧,先于关键帧的帧被丢弃。媒体文件按相机表写 fcamera.hevc、ecamera.hevc、可选 dcamera.hevc 与 qcamera.ts;RecordAudio 打开时 rawAudioData 还会写入 rlog(decimation=None,不进 qlog),这涉及额外的数据保留与隐私审查。

    抽样率是理解落盘差异的关键:can 的 qlog 抽样率为 2053、sendcan 为 139、carState 为 10、modelV2 为 None。编码索引虽通常 should_log=False,但在 write_encode_data() 中专门转写且始终进 qlog。不要用 qlog 作为控制/安全故障分析的唯一依据。

    对插件化与硬件的最低要求

    1. 插件 schema 需要版本、字段号与兼容策略;Cap’n Proto union 的固定编号不能任意复用,customReserved* 约束也不能未经全链路兼容评估就改控制主契约。
    2. 发布权限应按主题分级:观测只读、规划候选、carControl 和 sendcan 分开授权;valid、新鲜度、总线校验与硬件许可各自独立。
    3. 存储插件需保留元数据、段边界、单调时钟、媒体索引、丢帧及部分文件状态;为取证保留 rlog,不可用 qlog 抽样代替。

    常见问题

    消息字段存在,是否说明该数据一定被记录?

    不代表。四层契约是分开的:schema 里 union 分支存在,不表示受管进程正在发布,也不表示 loggerd 会记录;服务表里的频率是配置与健康检查依据,不等于真实发布速率;只有 loggerd_thread() 按 should_log 与抽样配置实际写盘的内容才在日志里。

    为什么不能用 qlog 复原所有推理帧?

    因为 modelV2 的 qlog 抽样配置为 None,carState 抽样率为 10,can 为 2053,各主题抽样并不一致,qlog 只在抽样命中时附写。源文档明确 qlog 不是每个主题的完整回放数据,控制/安全故障分析不要以它作为唯一依据。

    Event 的 logMonoTime 就是传感器采样时间吗?

    不是。logMonoTime 是消息封装时间;相机 timestampEof、模型 timestampEof、GNSS Unix 时间和 IMU 自有时间字段并不自动相等。传感器/定位插件必须显式声明时钟域、单位、采样时点和迟到处理。

    valid=True 是否表示这一帧数据可信?

    不等于。valid 只是运行有效位,源文档明确它「不承诺车辆总线帧通过车型解析或 panda 安全检查」。车端数据是否可信还要看 CarState 的解析健康字段、时间戳新鲜度以及 PandaState 中的许可与故障分列。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/MESSAGE_LOG_CONTRACTS_DEEP_DIVE.md(取用了四层契约、关键主题字段语义、Route/Segment 落盘流程与 qlog 抽样率)。
    • 项目 02 —— openpilot-research/LOGGERD_SERVICE_SAMPLING_POLL_CONTRACT_DEEP_DIVE.md(仅用于确认 loggerd 逐服务订阅与计数抽样相位的边界)。

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

    更新记录

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

  • 原生 Git 安装器:分支迁移、缓存复制与断电非原子替换窗口

    结论:这个原生 Git 安装器把 git clone/fetch、git checkout/reset 和复制、删除、移动交给 shell,只用断言判断返回码,且在关键窗口先删缓存与旧安装目录、再把新目录移入,三步不是一次原子替换——断电或命令失败可能留下旧目录已删、新目录未就位的状态。它也没有内建的下载总超时、安装树签名/摘要验证或固定 commit,远端仓库与分支即安装内容的信任根。已有的继续启动脚本会让程序直接进入完成画面而不核对安装目录,因此进度 100% 或 60 秒完成屏幕都不是安装成功回执。本文为固定快照静态分析,未运行下载、Git 命令、文件替换或设备启动。

    构建、入口与板型分支

    构建脚本仅在 extras 开启且架构为 comma_arm64 时产出四个变体:openpilot、openpilot_test、openpilot_nightly、openpilot_internal,分支由 BRANCH 编译宏写入,internal 变体另定义 INTERNAL。继续脚本和字体通过 ld -r -b binary 嵌入 ELF。安装器全局的 tici_device 调 C++ 硬件类型判断,选择 2160×1080 或 536×240 窗口;新板型进入该路径前必须先满足跨语言硬件身份契约。

    函数/组件 实现链与限制
    get_str()、GIT_URL/BRANCH_STR 从带分隔与填充空格的编译字符串截取有效值;没有运行时 URL/分支白名单
    branchMigration() 按 TICI/TIZI/MICI 对 release3、staging、master 等分支作迁移;未知板型保留原分支
    doInstall() 无限等待系统时间有效,删除临时安装目录;只有旧安装目录与缓存目录同时存在才走缓存复制
    freshClone()/cachedFetch() 新安装用浅克隆与递归子模块;缓存分支先整目录复制、改 remote/branch,再 fetch
    executeGitCommand() popen() 读 Git 输出,以英文进度前缀与百分号更新界面;命令没有整体期限
    cloneFinished() 断言 Git 返回 0,checkout/reset/submodule update,删旧标记与安装目录,移动新目录
    main() 初始化界面与字体,迁移分支;已有继续启动脚本则直接显示完成画面,否则安装后收尾

    拉取路径上,doInstall() 没有内建超时或离线失败界面:代码不验证旧目录或缓存标记内容的真实性。executeGitCommand() 用 popen() 读 Git 输出并以英文进度前缀与百分号更新界面,std::stoi() 解析进度没有局部异常处理,命令也没有整体 deadline。

    落盘次序与非事务替换

    安装器把 git clone/fetch 的 shell 命令、git checkout/reset 和复制、删除、移动、改权限都交给系统调用;run() 只用 assert(err == 0) 报失败。在启用断言的构建中会中止;如果断言被编译掉,命令非零返回仍可能继续后续步骤。cloneFinished() 对 pclose 和 chdir 也采用断言,没有结构化错误恢复。不能把最终 100% 进度或 60 秒完成屏幕当作安装成功回执。

    关键窗口在 cloneFinished():先删缓存目录,再删旧安装目录,然后移动新目录;这些操作不是单个原子替换,断电或命令失败可能留下旧目录已删除而新目录未就位的状态。继续脚本先写新文件、再改权限与重命名;脚本内容仅进入安装目录并执行启动脚本。源码中未见安装树签名或摘要验证、Git commit 固定 pin,也没有对继续脚本执行成功的确认,因此远端仓库与分支就是安装内容的信任根。INTERNAL 分支还会写入 SSH 开关、前向摄像头锁和公钥参数,不能把该构建变体当作普通公开设备的等价安装包。

    步骤 动作 风险
    1 删除缓存目录 后续失败时缓存已丢失
    2 删除旧安装目录 旧版本在此刻起不可用
    3 移动新目录就位 断电可致新目录未就位
    4 写并重命名继续脚本 仅在收尾阶段执行

    继续启动链与两层信任

    已有继续启动脚本时,本程序不再核对安装目录是否存在、是否匹配标记,也不执行更新,只进入完成画面;旧安装树与标记的组合可能来自外部安装或恢复流程,本归档无法证明其完整性。若外部启动器执行该脚本,它会先进入安装目录,再执行启动脚本,而启动脚本立即转交另一个脚本。后者先加载环境脚本中的平台版本与 staging 根,再检查已定稿的 overlay 更新,并可能替换整个安装目录、重新执行启动脚本;之后建立子模块 import 链接,执行平台初始化与版本升级门,最后按 prebuilt 标志决定是否调用构建,再运行进程管理器。这进一步说明继续脚本不是「安装完成」的最终回执,后面仍可能发生 overlay 切换、系统镜像更新和构建失败。

    还需要区分两层:Python 下载界面只检查 ELF 魔数,这个原生 Git 安装器再拉取源码并准备启动脚本;中间的分发、执行与重启交接仍需在目标镜像与服务端验证,不能从这两份源码推导端到端可信链。平台初始化在版本比对和进程管理器启动之前把当前槽位标记为成功,它只表示该标记的时点,不证明服务已通过健康检查。

    自制硬件与插件化验收

    安装器应作为受信任维护服务,而不是任意插件执行 shell 的入口。新硬件方案需定义板型与目标分支的签名清单、固定 commit 或内容摘要、下载总超时和空间预算;先在新目录完整验证,再以可回滚的目录或槽位协议切换。宿主应记录安装事务 ID、来源、验证结果、旧版本与新版本,并在启动后回执确认;界面插件只显示状态。台架至少覆盖时钟长期无效、缓存损坏、Git 进度格式变化、fetch 与子模块失败、每一步断电、继续脚本已存在但安装目录缺失,以及不同板型窗口与字体。

    常见问题

    安装进度到 100% 就说明安装完成了吗?

    不能。进度来自解析 Git 的英文输出,cloneFinished() 之后还有 checkout、reset、子模块更新和目录替换,且这些操作不是原子的。已有继续脚本时程序还会跳过安装直接显示完成画面,因此 100% 与 60 秒完成屏幕都不是安装成功回执。

    为什么断电可能让设备处于中间状态?

    因为关键窗口先删缓存目录、再删旧安装目录、然后移动新目录,这三步不是一次原子替换。cloneFinished() 只用断言判断返回码,如果断电发生在移动之前,就可能出现旧目录已删除而新目录尚未就位的状态,后续还依赖外部启动器恢复。

    安装内容的信任根是什么?

    是远端仓库与分支。源码中未见安装树签名或摘要验证,也没有固定 commit pin;get_str() 只从编译进去的字符串截取 URL 与分支,没有运行时白名单。因此产物或分发端若改写这些字节,不能只靠界面文案判断目标值。

    缓存复制路径更安全吗?

    不必然。doInstall() 只在旧安装目录与缓存目录同时存在时才走缓存复制,cachedFetch() 会先整目录复制,再改 remote/branch 并 fetch;代码不验证旧目录或缓存标记内容的真实性,因此缓存路径的提升主要是速度,不是可信度。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/NATIVE_GIT_INSTALLER_BRANCH_CACHE_ACTIVATION_DEEP_DIVE.md(取用了构建变体、分支迁移、缓存复制、落盘次序、非事务替换窗口与继续启动链)。
    • 项目 02 —— openpilot-research/INITIAL_INSTALLER_DOWNLOAD_TRUST_BOUNDARY_DEEP_DIVE.md(仅用于确认下载侧只检查 ELF 魔数的信任边界)。

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

    更新记录

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

  • selfdrived 动态告警:49 个槽位与软退出的真实边界

    结论:在 EVENTS 主映射里共有 49 个回调槽位、22 种回调形式:两个工厂覆盖 27 槽位,20 个直接函数覆盖 22 槽位。回调在 creation_delay 延迟过滤之前执行,即使告警最终未显示也会读取输入、构造文本,且这里没有逐回调异常隔离。更关键的是,回调返回的 ImmediateDisableAlert 只改变文案、声音、视觉标记、优先级与持续时间,是否立刻退出仍由事件类型决定,而不是由告警子类名里有 Immediate 决定。本文为固定源码快照 c8fb906 的静态阅读,未实测墙上时间与声音输出延迟。

    调用链:从输入到仲裁

    SelfdriveD.update_alerts() 提供六个实参:CP、CS、sm、is_metric、soft_disable_timer 和驾驶风格枚举字符串。Events.create_alerts() 只处理当前状态允许的事件类型:值若是 Alert 实例则直接使用,否则调用回调创建新实例。随后按连续帧计数判断 creation_delay,写入 EventName/类型 键和事件类型。

    环节 行为 边界
    update_alerts() 传入 CP、CS、sm、公英制、软退出计时与驾驶风格 输入字段来自当前 SubMaster 消息,跨主题时序需回放确认
    create_alerts() 只处理当前状态允许的事件类型;静态 Alert 直接用,否则调用回调 回调在 creation_delay 过滤前执行,无逐回调异常隔离
    AlertManager.add_many() 以 EventName/类型 键保存告警及结束帧 「事件已出现」与「UI 当前展示」可能不同步
    process_alerts() 按优先级、起始帧选当前项 最终仲裁仍在受控核心,不由插件决定

    这里有一个容易被忽略的顺序细节:create_alerts() 在判断 creation_delay 之前就已经执行了回调,因此告警文案与动态数值是在过滤之前构造的。若某回调依赖尚未更新的输入,它可能读到上一帧的字段;反过来,一个最终因延迟未显示的告警,其回调副作用——例如读取 sm、构造字符串——仍已发生。由于没有逐回调异常隔离,单个回调抛错会影响同一容器内的处理顺序,这一点在插件化时必须由受控核心兜底。

    两类软退出工厂:27 槽位

    软退出相关的槽位由两个工厂覆盖,共 27 个。soft_disable_alert() 覆盖 24 个事件,包括 bigModelFailed、cameraMalfunction、cameraFrameRate、locationdTemporaryError、paramsdTemporaryError、steerTempUnavailable、steerTimeLimit、sensorDataInvalid、excessiveActuation、overheat、calibrationInvalid、calibrationIncomplete、calibrationRecalibrating、espDisabled、commIssue、commIssueAvgFreq、selfdrivedLagging、processNotRunning、radarFault、radarTempUnavailable、modeldLagging、posenetInvalid、lowMemory、espActive;user_soft_disable_alert() 覆盖 3 个:wrongGear、doorOpen、seatbeltNotLatched。

    回调形式 槽位数与事件 运行结果
    soft_disable_alert() 24 个(含 overheat、radarFault、modeldLagging、lowMemory 等) 软退出计数低于 int(0.5/DT_CTRL) 时构造 ImmediateDisableAlert 外观,否则构造 SoftDisableAlert
    user_soft_disable_alert() 3 个:wrongGear、doorOpen、seatbeltNotLatched 同样在剩余约 0.5 秒改成紧急外观,之前使用文案较缓和的 UserSoftDisableAlert

    关键区别在于:回调返回的 ImmediateDisableAlert 只改变告警的文案、声音、视觉标记、优先级与持续时间,Events.create_alerts() 仍将 event_type 标为映射键 SOFT_DISABLE。StateMachine.update() 是否立刻退出由事件类型决定,不会因为 Alert 子类名含 Immediate 就改走 IMMEDIATE_DISABLE。计数按 DT_CTRL=0.01 的名义周期换算,墙上时间和声音输出延迟尚未实测。

    20 个直接回调:22 槽位

    回调与主映射事件 消费字段和动态内容
    joystick_alert():joystickDebug.WARNING 从 carControl.actuators.accel/torque 计算调试文案;不是实车执行反馈
    longitudinal_maneuver_alert():longitudinalManeuver.WARNING、lateralManeuver.WARNING 两个事件都读 alertDebug 的两行文案;横向事件由 lateralManeuverPlan 触发但回调仍读 alertDebug
    invalid_lkas_setting_alert():invalidLkasSetting.PERMANENT 根据 Tesla/Mazda/Nissan 品牌调整原车 LKAS 说明,其余用通用文案
    paramsd_invalid_alert():paramsdTemporaryError.NO_ENTRY 按 angleOffsetValid、steerRatioValid、stiffnessFactorValid 顺序挑第一个错误并附数值
    camera_malfunction_alert():cameraMalfunction.PERMANENT 对三路摄像头逐个调用 sm.all_checks([service]) 拼出失败名称
    overheat_alert():overheat.PERMANENT 取 CPU、GPU、内存温度的最大值显示;空列表用 0 兜底
    comm_issue_alert():commIssue.NO_ENTRY 对 sm.data 各主题重新执行 all_checks,只列前四个失败主题
    personality_changed_alert():personalityChanged.WARNING 显示当前驾驶风格字符串,持续时间 1.5 秒

    其余直接回调包括 startup_master_alert()、below_steer_speed_alert()、wrong_car_mode_alert()、out_of_space_alert()、below_engage_speed_alert()、too_distracted_alert()、calibration_invalid_alert()、calibration_incomplete_alert()、process_not_running_alert()、modeld_lagging_alert()、posenet_invalid_alert()、low_memory_alert()。需要强调「槽位」是 EVENTS 中事件名与事件类型的组合,不等于 22 个独立事件,也不等于 49 条屏幕上同时可见的告警。此外 NoEntryAlert 在 mici 上会调换两行文字,EVENTS.update() 还覆写 9 个条目中的告警定义,实际设备呈现必须按设备类型重核。

    从这些实现里还能看出一个共性:动态文案普遍消费当前订阅到的消息字段,例如 overheat_alert() 取 CPU/GPU/内存温度最大值、modeld_lagging_alert() 读 modelV2.frameDropPerc、posenet_invalid_alert() 计算 CS.vEgo - modelV2.velocity.x[0]。这些字段来自不同进程、不同采样时刻,回调本身不做新鲜度校验,因此「文案里显示的数值」不宜直接当作同一时刻的联合状态。

    仲裁、测试与插件边界

    AlertManager.add_many() 以 EventName/类型 键保存告警及结束帧,process_alerts() 按优先级、起始帧选当前项。update_alerts() 在不活动时清理 WARNING,已使能时清理 NO_ENTRY,这可能让「事件已出现」和「UI 当前展示」不同步。selfdriveState 再发布当前文案、声音与 HUD 标记。

    测试覆盖存在缺口:test_alerts.py 的静态告警属性检查明确跳过回调值,test_alertmanager.py 主要验证持续帧逻辑,不能证明 20 个直接回调的所有输入边界正确。隔离探针确认回调在 creation_delay 过滤前执行、异常不在容器中隔离。重构成插件时应把输入观测和告警策略分层:插件提供可信字段与新鲜度,受控核心保留事件类型、软退出计时、回调异常处理和最终仲裁;不得让插件仅靠改文案或改告警子类来更改安全状态。

    常见问题

    回调返回 ImmediateDisableAlert 会让车辆立刻退出吗?

    不会。create_alerts() 仍把 event_type 标为 SOFT_DISABLE,是否立刻退出由 StateMachine.update() 按事件类型决定,与告警子类名是否含 Immediate 无关。

    回调只在告警最终显示时才执行吗?

    不是。回调在 creation_delay 延迟过滤之前执行,因此即使最终未显示也会读取输入并构造文本,且这里没有逐回调异常隔离。

    49 个槽位等于 49 条同时可见的告警吗?

    不等于。槽位是 EVENTS 中事件名与事件类型的组合,既不是 22 个独立事件,也不是屏幕上同时可见的告警条数;mici 覆写还会改变实际呈现。

    「事件已出现」和「UI 当前展示」总是同步吗?

    不一定。update_alerts() 在不活动时清理 WARNING、已使能时清理 NO_ENTRY,因此事件状态与 selfdriveState 发布给 UI 的展示可能不同步。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/DYNAMIC_ALERT_CALLBACKS_DEEP_DIVE.md(取用了回调槽位统计、两类软退出工厂、20 个直接回调、仲裁与测试缺口、插件边界及隔离探针结论)。
    • 项目 02 —— openpilot-research/CAR_MESSAGES_FIELD_CONTRACT_DEEP_DIVE.md(仅用于确认回调消费字段的消息来源)。

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

    更新记录

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

  • Cabana UI 会话:流生命周期、DBC 编辑与落盘边界

    结论:Cabana 的 UI 是一条以 MainWindow 为中心的生命周期:run() 初始化 GLFW 与 ImGui 后构造主窗口,把流、消息列表、消息详情、视频与图表组成 dock;startStream() 先启动源再加载 DBC 并创建组件,releaseStream() 则先销毁对话框与消费组件再销毁流。它的 DBC 编辑缺少可靠落盘证明:MainWindow::saveFile() 未检查 DBCFile::save() 的失败返回值,所以「File saved」与 clean 状态不能当作文件已可靠写盘的证据。本文为固定源码快照 c8fb906 的静态阅读,未启动 GUI 或连接设备。

    窗口与流的生命周期

    run() 初始化 GLFW、ImGui、主题与布局,然后构造 MainWindow 并进入渲染循环。startStream() 先启动源,再加载 DBC 和创建组件,等待第一批事件合并后关闭等待提示。

    切换流时,releaseStream() 先销毁工具对话框和消费组件,再销毁流,以避免回调持有已释放的全局 can 指针。这个顺序说明「流」与「消费组件」是两类生命周期,不能只按打开顺序反着关。

    主窗口还负责窗口布局的持久化:退出时 saveSessionState() 记录当前 DBC、消息标签和图表 ID,restoreSessionState() 仅在当前加载文件名与记录的 DBC 一致时恢复,且消息标签只恢复仍能在 DBC 找到的项目。保存 UI 会话不包含原始 CAN 帧,流日志是否留存另由输入和设置决定。

    流选择与输入配置

    StreamSelector 提供路段、Panda、Linux SocketCAN 和本地/远端 msgq 入口,并允许同时选择 DBC。每次打开都会重建选择控件,但它只是输入配置,不代表已经连接。Panda 配置允许选择序列号、三路 CAN 速率及适用型号的 CAN-FD 数据速率;选择器构造时即会短暂打开 Panda 判定型号,Panda 构造还会发 USB 通信重置,真正连接时设置 NO_OUTPUT,仍须遵守台架总线接线和速率约束。

    远端路段流程先取设备列表,再按时间范围取路段;401 转登录界面,网络错误会提示。其异步回调带存活弱引用与请求序号,避免旧响应覆盖新列表。登录调用认证并异步取设备与路段,但该账户授权只用于路段可见性,不等于车端网关写权限。

    组件 关键函数 作用与边界
    流选择 StreamSelector::open() / draw() 提供多输入入口并允许选 DBC;仅是配置,不代表已连接
    路段选择 OpenReplayWidget::open() / fetchRoutes() 远端先取设备再按时间取路段;异步回调带弱引用与序号
    Panda 配置 buildConfigForm() / PandaStream::connect() 构造时短暂打开判定型号;连接设 NO_OUTPUT,仍受台架约束
    登录 signIn() / fetchDevices() 账户授权只用于路段可见性,不等于网关写权限
    设置 SettingsDialog::save() 更新主题、缓存分钟、图表高度等;开启直播记录会在本地留存原始数据

    消息详情与 DBC 编辑

    DetailWidget::setMessage() 为 (bus, address) 开标签,联动信号树、位图与历史帧;其 updateWarnings() 对未收到消息、DBC 长度与收到长度不符、信号位重叠给出提示。这些是编辑器的观察性检查,不验证车辆总线发送策略。

    BinaryView 把位图拖拽选择转为新增或调整信号,并展示字节/位翻转热图;binaryViewSignals() 只把当前多路复用分支拥有的 payload 信号送入位图,避免不同分支的位重叠被误画为同时生效。热图表示样本变化,不证明信号语义或大小端定义正确。SignalModel::setData() 从属性表编辑名称、长度、大小端、比例、偏移、单位与取值描述,saveSignal() 检查重名并通过 EditSignalCommand 入撤销栈。

    关键在于:所有编辑都先修改内存 DBC,磁盘写入是单独步骤。由于主窗口的写盘失败返回值未传播,确认保存后仍可能以 clean 状态继续关闭;自制硬件的 DBC 制作流程应以重新打开、哈希校验和版本提交为准。

    文件、会话与错误状态

    FileDialog::accept() 在覆盖已有文件时二次确认,MessageBox::draw() 顺序消费确认与警告队列。remindSaveChanges() 在关闭或切换文件前提示保存或丢弃,但同样受上述返回值未传播的影响。

    设置保存与 DBC 保存不是一回事:Settings::save() 先加文件锁,再写临时 JSON、fsync 文件、rename 并 fsync 父目录,损坏的旧设置会尝试移到 .corrupt。这说明 UI 会话与用户设置本身具备更完整的原子写入路径,而 DBC 保存没有。

    对象 写入/存储方式 边界
    DBC 文件 先改内存,写盘为单独步骤 失败返回值未传播,clean 标志不足以作发布门
    用户设置 cabana.json 锁 + 临时文件 + fsync + rename + 父目录 fsync 损坏旧设置尝试移到 .corrupt
    UI 会话 记录当前 DBC、消息标签、图表 ID 不包含原始 CAN 帧,且需文件名一致才恢复

    对自制硬件与插件平台的约束

    流选择、图表和 DBC 编辑器属于离线或只读工具能力,不应和受信任的车端发送网关共享写权限;Panda 连接即使使用 NO_OUTPUT,也需台架审查波特率与接线。协议文件、用户 UI 设置、路段授权和原始日志应分别管理,DBC 保存必须验证返回值、做原子写入与重新读取,并记录所用源码与车型版本。

    远端路段登录、直播日志与设置目录可能处理敏感车内数据,插件清单应声明读写文件、网络与留存权限,并提供可审计的禁用与清理路径。需要提醒的是,当前 test_cabana_ui.py 只启动 cabana -h 并检查帮助输出,不能据此称菜单、弹窗、持久化或图形交互已通过 UI 自动化。

    常见问题

    Cabana 的「File saved」能证明 DBC 已可靠落盘吗?

    不能。MainWindow::saveFile() 未检查 DBCFile::save() 的失败返回值,因此「File saved」提示与 clean 状态不能作为文件已可靠写盘的证明。自制硬件应以重新打开、哈希校验和版本提交为准。

    打开流选择器会连上 Panda 吗?

    选择器构造时会短暂打开 Panda 判定型号,Panda 构造还会发 USB 通信重置;但真正连接时才设置 NO_OUTPUT。这只是输入配置流程,仍须遵守台架总线接线与速率约束。

    远端路段登录会给车端写权限吗?

    不会。登录调用认证并异步取设备与路段,但该账户授权只用于路段可见性,不等于车端网关写权限。

    保存 UI 会话会把原始 CAN 帧一起存下来吗?

    不会。saveSessionState() 只记录当前 DBC、消息标签和图表 ID;保存 UI 会话不包含原始 CAN 帧,流日志是否留存由输入和设置另行决定。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CABANA_UI_SESSION_DEEP_DIVE.md(取用了主窗口与流生命周期、流选择与设置组件、DBC 编辑与落盘边界、会话持久化)。
    • 项目 02 —— openpilot-research/CABANA_VISUAL_ANALYSIS_DEEP_DIVE.md(关联的可视分析专题,用于区分 UI 会话与图表视频能力,本文未展开其实现)。

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

    更新记录

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

  • tinygrad HCQ 执行链:队列依赖、链接与提交生命周期

    结论:tinygrad 的 HCQ 执行链把 LINEAR 调用编成 GPU 队列批次,再用主机提交程序驱动:hcq_compile() 负责分批、依赖、命令编码与编译,hcq_link() 在链接期把占位缓冲变成真实 Buffer,exec_hcq() 在运行时填地址表并执行提交。一次逻辑 copy_from() 不保证只做一次物理 DMA,多队列完成也不代表某个 kernel 已结束。本文为固定源码快照 c8fb906 的静态阅读,未在 GPU 上运行。

    入口、分批和复制

    在普通非 JIT 执行链中,compile_linear() 完成内核编译后调用 hcq_compile()。get_enqueue_devs() 只考虑带缓冲参数的 PROGRAM 或 COPY 调用;候选设备类型受 HCQ_DEVS 限制,AMD 只有开启 HCQ2 才进入该集合,QCOM 的 COPY 被排除,留给 host 与统一内存路径。因此不能把所有 tinygrad 调用都理解成 GPU 队列提交。

    hcq_compile() 先在需要时把已有 BUFFER 参数化,再用 unwrap_call() 拆多设备 lane,用 pm_insert_copy_staging 处理复制。跨不同 peer group 且可用 RDMA NIC 时,split_rdma() 产生两段调用;不能映射源与目标时,stage_copy() 借用 host staging 缓冲并按两个 slot 分块;设备没有 copy queue 时,复制会被转成可编译的内核。它随后按连续可入队片段与 peer group 调用 sched_batches(),并把编码后的主机提交 SINK 交给 lower_and_compile()。

    这些分支决定真实的传输次数与等待点,所以不能从一个逻辑 copy_from() 直接推算物理 DMA 数量。

    分支条件 处理方式 含义
    跨 peer group 且有 RDMA NIC split_rdma() 产生两段调用 真实传输次数与网络路径相关
    源/目标无法映射 stage_copy() 借用 host staging 并按两个 slot 分块 多出主机中转与等待点
    设备无 copy queue 把复制转为可编译内核 复制变成 kernel 提交
    QCOM 的 COPY 被排除,留给 host/统一内存路径 不进入 HCQ 入队集合

    队列依赖和时间线

    BatchCtx 收集每个设备的计算/复制队列、上一调用、peer 访问、需要 signal 的提交及 slot 占位。HCQDepsTracker._key() 以 (base, lane, byte offset/end) 标识访问范围,能在同一底层缓冲中区分不重叠视图;缓冲映射的基址、lane 与跨度一旦出错,会直接污染依赖判断。_wait_ins() 由读写冲突选出每个来源队列最近的依赖:同队列依靠 FIFO,跨队列生成 wait,并把被等待的提交标记为 signal。

    _start_ins() 与 _build_queues() 让队列首次使用时先等待本设备与 peer 的既有时间线,再插入依赖 wait、可选 timestamp、调用与 signal;收尾队列等其他队列后推进设备时间线。异步 COPY 与 compute 是否能正确重叠,取决于这套规则和设备驱动实现。

    函数或结构 具体职责 硬件适配边界
    BatchCtx 收集队列、上一调用、peer 访问、signal 与 slot 占位 新后端要提供队列、时间线与提交顺序能力
    HCQDepsTracker._key() 以基址、lane、字节跨度标识访问范围 映射基址或跨度错误会污染依赖判断
    _wait_ins() 读写冲突选最近依赖,跨队列生成 wait 异步重叠是否正确取决于驱动实现
    hcq_fence() 等上次调度完成以避免 slot 冲突 循环留有 timeout TODO,最坏等待须故障注入
    HWQueue.q() / encode_submit() 常量写命令字节,动态字段留 patch 新后端要实现命令编码、地址 patch 与错误回报

    编译缓存、链接所有权和运行时地址

    hcq_compile() 的模板缓存只在 cache=True 且提供输入 UOp 时启用,缓存键包含准备后的 LINEAR、profile 开关与 ALL2ALL>=1。较短调度(HCQ_CACHE_THRESH 默认 64)用运行期地址 patch,较长调度可带 lt_input 链接期输入,随后不能复用同一已烘焙链接。该缓存与内核编译缓存不是同一层。

    lower_call() 经设备 pm_encode/pm_lower 编码队列,把动态输入地址改写为地址表槽位,合并同类临时缓冲并以 128 字节对齐;HCQInfo 记录地址表位置、slot、被写缓冲、host 依赖与 profile 元数据。链接时 hcq_link() 把占位缓冲变成实际 Buffer,解析 GETADDR 并保持引用。allow_cache=True 且没有 lt_input 的可缓存链接会分配自有缓冲并缓存结果;含 lt_input 的一次性链接才借用 runtime ring 槽;allow_cache=False 的 JIT 保留链接分配的自有缓冲。设备专属 pm_bufferize 仍可能接管具体缓冲分配,因此不能仅凭 eager 或 JIT 名称推断所有权。

    每次 exec_hcq() 在有动态输入地址时先填地址表,再执行主机提交程序;它向内存所有者登记访问设备的 pending timeline,并仅在等待或性能采样条件下同步或读回时间戳。Compiled.synchronize() 等待本设备及 peer pending,超时路径调用 on_device_hang(),但这不自动证明提交程序内所有无超时等待都可恢复。

    硬件适配边界

    上车硬件适配须分别量化编译、链接、提交、COPY、GPU 完成与输出读回时间,并验证掉线或超时能传递为模型不可用状态,不能把主机提交返回当成推理成功。新后端不仅要能运行 kernel,还要实现命令编码、地址 patch 与错误回报。

    本机一项不接触 GPU 的 HCQDepsTracker 探针显示:在同一 UOp 缓冲写入 [0,4) 后,读取 [8,12) 没有依赖,读取 [2,6) 则返回原写依赖。这验证了区间冲突的一个样例,但不覆盖队列 signal、实际设备缓存一致性或 peer 内存。

    常见问题

    是不是所有 tinygrad 调用都会走 GPU 队列提交?

    不是。get_enqueue_devs() 只考虑带缓冲参数的 PROGRAM 或 COPY,且候选设备受 HCQ_DEVS 限制;AMD 需开启 HCQ2 才进入,QCOM 的 COPY 被留给 host 与统一内存路径。

    一次逻辑 copy_from() 就等于一次物理 DMA 吗?

    不等于。跨 peer group 且有 RDMA NIC 时会产生两段调用,源/目标无法映射时会借用 host staging 分块,设备没有 copy queue 时复制还会被转成内核。传输次数与等待点由这些分支决定。

    多队列完成就代表某个 kernel 已经结束了吗?

    不代表。源码指出多队列完成不等于某一 kernel 已结束,必须以最终 timeline 或回执为准。Compiled.synchronize() 等待本设备及 peer pending,超时路径调用 on_device_hang()。

    主机提交返回是不是就代表推理成功?

    不是。exec_hcq() 向内存所有者登记 pending timeline,并仅在等待或性能采样条件下同步。synchronize() 的超时恢复也不自动证明所有无超时等待可恢复,应将超时映射为模型不可用状态。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_HCQ_QUEUE_LINK_LIFECYCLE_DEEP_DIVE.md(取用了分批与复制分支、队列依赖与时间线、链接所有权与运行时地址、证据缺口)。
    • 项目 02 —— openpilot-research/TINYGRAD_MODEL_RUNTIME_DEEP_DIVE.md(关联的模型运行时专题,用于区分 HCQ 提交与模型集成链,本文未展开其实现)。

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

    更新记录

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

  • Params 生命周期:注册键、清理时机与未上抛的写失败

    结论:Params 是低频配置与状态的持久层,不是高频消息总线,也不是独立的车辆安全授权。每个合法键都有注册类型、位标志和可选默认值;同名相近的键(CarParamsCache / CarParamsPersistent / CarParamsPrevRoute)用途与新鲜度完全不同。写入路径存在一个明确的缺口:文件系统返回的负错误码没有在 Python put() 中被检查,所以关键安全配置不能仅凭 put(..., block=True) 没抛异常就宣称持久化成功。固定快照基线为 c8fb906,本地归档缺编译后的 libparams_c,未运行原生参数测试或断电故障注入。

    注册、读写与类型

    params_keys.h 与 ParamKeyAttributes 规定每个合法键有类型、位标志、可选默认值。例如 CarParams 是 bytes 且在 manager 启动 / 上路时清除,CarParamsPersistent 则持久保存。动态插件不能只在自己进程里声明任意新键;必须设计命名空间、类型、迁移与生命周期,并纳入可信配置注册机制。

    Params.check_key() 让 Python 拒绝未注册键及含 NUL 的键;put() 仅接受与注册类型匹配的 Python 类型,get() 按声明反序列化。Python ParamKeyType 支持 string、bool、int、float、time、JSON、bytes;C++ Params 是字节存储,并不自行验证每个键的业务内容。get_bool() 只把 "1" 视为真,其余值都返回 false,因此它不能用来判断某键是否被明确写入;get() 与 get_bool() 在缺省处理上也不完全相同。

    组件 / 函数 已核对契约 对插件的影响
    params_keys.h / ParamKeyAttributes 每个合法键有类型、位标志、可选默认值;CarParams 启动 / 上路清除,CarParamsPersistent 持久。 插件须设计命名空间、类型、迁移与生命周期。
    check_key() 拒绝未注册键与含 NUL 的键;put() 只接受匹配类型。 不能靠自由字符串文件模拟已登记参数。
    get() / _cpp2python() 非阻塞空字节不请求默认时返回 None,请求默认时用注册表默认值;block=True 得到空字节时抛 KeyboardInterrupt;损坏 JSON 警告一次并尝试默认。 默认值回退可能掩盖原始损坏;空字符串不能区分「缺失」与「已写空值」。
    put() / params_put() 默认走 C++ 异步队列;block=True 直接调用文件写入;负返回码未在 Python 侧检查。 关键配置不能只凭无异常宣称持久化成功。

    存储层:原子替换与异步写队列

    Params::put() 走临时文件写入、fsync、目录锁、原子 rename、目录 fsync;异步写线程按队列逐项执行,析构时等待。单键替换具备原子写入路径,但多键配置不是原子事务;异步写也不是跨进程的严格提交确认。

    参数目录通过参数前缀软链接暴露,OpenpilotPrefix 能让测试进程隔离到单独前缀;C ABI 层负责异常翻译与返回缓冲复制。这两层保证的是「路径与接口」的正确性,并不校验业务内容是否符合调用方预期。

    Params::clearAll() 按位标志删除匹配键,同时删除注册表之外的文件;OpenpilotPrefix 能把测试进程隔离到单独前缀。这意味着未登记的插件键可能在清理时消失,验证环境也必须记录前缀与参数快照。

    manager 的清理时机与实际键例

    参数的新鲜度由 manager 在几个时点统一管理:

    时点 源码动作 示例影响
    manager 初始化 清除 CLEAR_ON_MANAGER_START、上路、下路、点火标志;发布渠道还清 DEVELOPMENT_ONLY,随后给缺失的有默认值键写默认。 CarParams、ControlsReady、FirmwareQueryDone 被清除;OpenpilotEnabledToggle 等持久键保留 / 补默认。
    上路边沿 检测 deviceState.started 从 false 到 true,清 CLEAR_ON_ONROAD_TRANSITION。 车型参数需 card 在新行车周期重新提供,插件不能缓存旧控制授权。
    下路边沿 清 CLEAR_ON_OFFROAD_TRANSITION,并更新 IsOffroad。 调试模式、Chestnut 活动状态不应跨周期视为有效。
    点火边沿 基于 pandaStates 点火值清 CLEAR_ON_IGNITION_ON。 DriverTooDistracted、ChestnutActive 等按点火周期复位。

    安全敏感的 SecOCKey 标为 PERSISTENT | DONT_LOG,而 DONT_LOG 的含义由 logger 实现为保留键名但不写值。它不是加密、访问控制或对所有外部插件日志的自动脱敏。

    失败路径与验证

    params_c.cc 只将抛出的 C++ 异常转成 last_error;params_put() 原样返回 Params::put() 的整数结果,而 Params.put() 丢弃该结果。原方法探针确认:若假 C 函数返回负值而 last_error 为空,_bind() 会把负值交还给 Python,两个写入方法都返回 None 且不抛异常;若 last_error 有值,_bind() 抛 RuntimeError。mkstemp、write、fsync 或 rename 的真实磁盘故障尚未复现。

    Params::clearAll() 对删除操作不检查每个 unlink 的返回值;FileLock 锁打开或加锁失败时记录错误,但构造并未抛异常。配置冻结与清理的失败检测不能仅依赖调用返回。现有参数测试覆盖同步 / 异步读写、未知键、默认值与按标志清理,日志测试检查 DONT_LOG 值被遮蔽;本次未运行。新插件参数还应测试断电、磁盘满、跨进程竞争、旧版本迁移、上路 / 下路与点火边沿,以及记录中是否泄露密钥。插件注册器应在 offroad 冻结配置版本、哈希及权限;行车进程读到的配置与 panda 固件安全模式必须可互相校验,失败时保持被动模式。

    常见问题

    put(block=True) 没报错,是否说明参数已经落盘?

    不能这样推断。文件系统调用返回的负错误码没有在 Python put() 中检查;探针确认假 C 函数返回负值且 last_error 为空时,写入方法仍返回 None 且不抛异常。关键安全配置需读回、版本校验及故障注入。

    CarParams 和 CarParamsPersistent 有什么区别?

    用途与新鲜度不同。CarParams 是 bytes 且在 manager 启动 / 上路时被清除,需要 card 在新行车周期重新提供;CarParamsPersistent 与 CarParamsPrevRoute 是持久键。名称相似但不能互相替代。

    DONT_LOG 会自动脱敏日志吗?

    不会。DONT_LOG 由 logger 实现为保留键名但不写值;它不是加密、访问控制,也不是对所有外部插件日志的自动脱敏。安全敏感的 SecOCKey 只是被标为 PERSISTENT | DONT_LOG。

    插件能自己声明新的参数键吗?

    不能只在自己进程里声明。合法键必须在注册表里有类型、位标志与默认值;check_key() 会拒绝未注册键。clearAll() 还会删除注册表之外的文件,所以未登记的插件键可能在清理时消失。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PARAMS_LIFECYCLE_DEEP_DIVE.md(取用了键注册、读写类型、存储原子性、manager 清理时机与写失败路径分析)。

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

    更新记录

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

  • tinygrad AMD AM 驱动:七个 IP 启动顺序与部分重启生命周期

    结论:AMDev 的启动不是一条固定路径,而是先读 IP discovery 再分流:scratch7 等于 AMDev.Version 且未请求 AM_RESET 时才初步允许部分启动,scratch6 非零或 GC 保护故障寄存器非零通常退回完整启动,GC 9.5.0 的既有 AM 状态反而再次强制部分启动以避免完整启动损伤互连。完整启动按 SOC→GMC→IH→PSP→SMU 初始化,两条路径最终都重建 GFX 与 SDMA,VF 会跳过 PF 专属的 PSP 与 SMU。启动标记只表示 AM 自身的阶段完成,不代表模型已编译、GPU 队列已成功执行或已获车辆控制权。本文为固定归档 amdev.py 与 ip.py 的静态阅读,未打开 PCI/USB、未刷写固件、未碰 GPU/车辆/CAN。

    启动顺序与实际分支

    AMDev.__init__() 先处理 PCIe ASPM,映射 VRAM、doorbell 与 MMIO,判断 VF 并请求访问窗口;随后 _run_discovery() 从 VRAM 末端读取 IP discovery、harvested 实例与版本/基址,再由 _build_regs() 生成寄存器对象及可访问的 AID 集合。_run_discovery() 仅检查两类 signature,仍直接按表内偏移与数量做 ctypes 地址遍历,固件或设备损坏场景下需要台架注入验证长度边界,不能把它视为不可信数据解析沙箱。

    init_sw() 建内存管理器、固件目录和七个 IP 实例,按 SOC、GMC、IH、PSP、SMU、GFX、SDMA 顺序对每个组件调用 init_sw()。scratch7 匹配且未请求 AM_RESET 时,才初步允许部分启动;若 scratch6 非零或 GC 保护故障寄存器非零,通常退回完整启动。完整启动如检测到存活的 PSP/SMU,可能先做 mode1 reset;hive 多卡状态要求另行复位。之后完整启动按 SOC→GMC→IH→PSP→SMU 初始化,VF 跳过 PF 专属的 PSP/SMU;部分启动沿用底层状态,非 VF 会重新初始化 TMR;两路最终都初始化 GFX→SDMA。

    判定条件 走向 说明
    scratch7 == AMDev.Version 且未请求 AM_RESET 初步允许部分启动 沿用底层状态,非 VF 重新初始化 TMR
    scratch6 非零或 GC 保护故障寄存器非零 退回完整启动 可能先做 mode1 reset
    GC 9.5.0 的既有 AM 状态 强制部分启动 避免完整启动损伤互连
    VF 模式 跳过 PF 专属 PSP/SMU 经访问窗口获得并释放租约
    两路共同收尾 GFX→SDMA 配置功耗/时钟与 clock gating,写启动标记

    启动尾部对非 VF 配置功耗/时钟与 clock gating,再写 scratch5(TMR 大小)、scratch7(AM 版本)和 scratch6=1。reset_mode=True 的 hive 异常分支可提早返回,因此调用者不能仅以构造函数返回推断所有初始化阶段完成。

    七个 IP 的入口与职责

    子组件 主要入口 本层职责与风险边界
    AM_SOC init_sw()、init_hw()、doorbell_enable() 选 SoC/中断源元数据、设置 NBIO/doorbell;门铃可达不等于队列完成
    AM_GMC init_hub()、enable_vm_addressing()、flush_tlb() 计算 FB/MC/XGMI/VA 范围,初始化页表与 TLB;错误映射可能影响所有 GPU 内存访问
    AM_IH init_hw()、interrupt_handler()、drain() 启动 IH ring,解析 SQ/VM fault/RAS,设置 is_err_state;日志打印不是自动恢复
    AM_PSP init_hw()、_ring_submit()、_load_ip_fw_cmd() 启动 sOS、创建命令 ring、设置 TMR 并加载各 IP 固件;固件来源、哈希与响应状态是独立验收门
    AM_SMU _send_msg()、mode1_reset()、set_clocks() 电源/时钟/重置消息,按 IP 版本选协议;超时与电源策略不能由模型插件自行决定
    AM_GFX init_hw()、reset_mec()、setup_ring()、fini_hw() 初始化 GC hub/MEC,配置 MQD/HQD 与计算 ring;部分启动只重置 MEC
    AM_SDMA init_hw()、setup_ring()、fini_hw() 按 SDMA 版本配置 watchdog、doorbell、copy ring 与关闭路径;传输完成与计算完成不是同一事件

    内存侧还有一条必须保持的先后关系:AMMemoryManager.on_range_mapped() 在页表变更后依次请求 GC 和 MM TLB flush,AM_GMC.flush_tlb() 会先做 HDP flush,未初始化 hub 则返回,VF 与常规路径的 invalidation/ACK 不同。插件若要抽象 GPU 内存,必须保持映射、TLB、队列提交的先后关系,不能只替换分配器。

    停机、恢复与未验证项

    AMDev.fini() 先在 VF 需要时请求访问窗口,只对 SDMA/GFX 调用 fini_hw(),再调低时钟、处理 IH、写 scratch6 并释放 VF 租约;它不是逐一销毁七个 IP 或解除所有 BAR 与固件状态。recover() 只在错误态或 force=True 时处理 IH 并重置 MEC,不能等同于 PCI 热复位、全卡重新发现或固件重验。本专题没有运行 AMD/AM 单元测试或真机驱动,初始化寄存器表、PSP 固件加载、SMU 响应、IH 故障与内存一致性需要单独台架验证;AMDev.__init__() 的多阶段初始化没有包裹通用清理块,若中间异常,已映射资源、VF 租约和部分配置如何回退应通过故障注入确认。此外,VF 与 PF 模式、hive 多卡状态以及 GC 9.5.0 的强制部分启动分支,都不能用一块普通单卡的测试结果替代——它们各自有独立的复位路径与访问租约,验收时必须分别覆盖。

    自制车载硬件的插件边界

    应把 AMDev/IP 层限定为受信任的计算设备驱动,而不是任意动态插件。硬件适配需声明 PCI/USB 身份、BAR 布局、IP 发现表版本、固件哈希、PF/VF 模式、AID/XCC/harvest 信息、内存与 TLB 语义、队列和中断超时。验证顺序为只读发现与固件校验、独立台架冷/暖启动和异常回滚、GPU 队列与内存数值回归、车端只读观察;每一步单独上报设备在线、固件准备、队列可提交、任务完成与模型输出有效,任何状态都不得绕过车型 CAN 编码或 panda 安全钩子。

    常见问题

    AM 启动后 scratch6=1 代表什么?

    它只是 AM 自身的启动标记之一,与 scratch5(TMR 大小)、scratch7(AM 版本)一起写入。它不表示任何模型已编译、GPU 队列已成功执行或系统已获车辆控制权限,因此不能把标记写入当作端到端就绪。

    什么情况下会走部分启动,什么情况下退回完整启动?

    scratch7 等于 AMDev.Version 且未请求 AM_RESET 时才初步允许部分启动;scratch6 非零或 GC 保护故障寄存器非零通常退回完整启动。GC 9.5.0 的既有 AM 状态是例外,会再次强制部分启动以避免完整启动损伤互连。

    recover() 等于把 GPU 重新初始化一遍吗?

    不等于。recover() 只在错误态或 force=True 时处理 IH 并重置 MEC,它不能等同于 PCI 热复位、全卡重新发现或固件重验;AMDev.fini() 也只对 SDMA/GFX 调用 fini_hw(),并非逐组件销毁。

    为什么说队列可达不等于任务完成?

    因为各层完成点是分开的:AM_SOC 的 doorbell 可达、AM_SDMA 的传输完成与 GPU 计算完成都不是同一事件,IH 打印故障日志也不等于自动恢复。上报时应把设备在线、固件准备、队列可提交、任务完成与模型输出有效分开记录。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/TINYGRAD_AM_DEVICE_IP_BOOT_PARTIAL_RESET_LIFECYCLE_DEEP_DIVE.md(取用了启动顺序、完整与部分启动分支、七个 IP 入口、停机恢复与硬件插件边界)。
    • 项目 02 —— openpilot-research/TINYGRAD_AM_GMC_VM_PTE_TLB_COHERENCY_DEEP_DIVE.md(仅用于确认 TLB flush 与页表变更的先后关系)。

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

    更新记录

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

  • 开发工具链的四种权限:摇杆、机动、消息桥与仿真各能做什么

    结论:这套开发工具链包含四种不同权限等级的入口:摇杆调试控制(替代 controlsd)、纵向机动(替代 plannerd)、横向机动(覆盖期望曲率)、以及 msgq/ZMQ 消息桥。它们都能影响 carControl 或 sendcan 路径,但配置层的「实验开关互斥」不是安全门。此外「bridge」在本项目有两个不同含义:受管进程 bridge 是 msgq/ZMQ 主题桥,tools/sim/bridge 是物理仿真与 openpilot 的适配器,二者不能混为一个控制入口。

    四类调试入口与运行条件

    摇杆输入由 joystick_control.main() 提供,车载由 process_config.py 管理;脚本要求驻车启动或在 ZMQ 模式下运行,并设置 JoystickDebugMode——但这不是安全固件许可。摇杆控制器 joystickd_thread() 在 JoystickDebugMode 或 notCar 时运行,模式启用时 controlsd 不运行;testJoystick 超过 0.2 秒未收到会把轴归零。

    纵向机动由 maneuversd.main() 在 LongitudinalManeuverMode 时运行,与 plannerd 互斥,按目标速度及纵向激活连续满足约三秒后执行预置加速度曲线——它不是一般规划器的插件实现。横向机动由 lateral_maneuversd.main() 在 LateralManeuverMode 时运行,只在目标速度、直路、低侧倾且横向激活时起动,转向/油门输入或速度偏差会重置动作。

    组件 入口与条件 输入 → 输出 关键限制
    摇杆输入 joystick_control.main() 键盘/游戏手柄 → testJoystick 要求驻车启动或 ZMQ 模式;设置调试模式不等于固件许可
    摇杆控制器 joystickd_thread() testJoystick/carState 等 → carControl 启用时 controlsd 不运行;0.2 秒超时轴归零
    纵向机动 maneuversd.main() 模型时钟/车速/carControl → longitudinalPlan 与 plannerd 互斥;约三秒条件后执行预置曲线
    横向机动 lateral_maneuversd.main() 车速/曲率/驾驶员输入 → lateralManeuverPlan 仅在目标速度、直路、低侧倾且横向激活时起动
    msgq/ZMQ bridge bridge.main() msgq 服务 ↔ ZMQ 网络主题 只传输消息,不校验驾驶控制语义
    崩溃收集 tombstoned.main() apport crash → cloudlog 摘要/日志副本 仅处理 .crash,限制 100 MB;不在实时闭环内

    开发者界面把三个实验参数设置为互斥,但这只是配置层保障;插件架构仍应在进程启动层做独立冲突检测。selfdrived 对收到的机动主题添加专用事件,如果外部插件冒充这些主题,就会影响使能和告警路径。

    关键执行路径

    Keyboard.update() 与 Joystick.update() 把人工输入变成双轴,send_thread() 以 100 Hz 发布;游戏手柄使用动态记录的轴范围、中心死区和三次曲线,重插拔异常时轴归零,而键盘输入是逐键累积、不是弹簧归中。joystickd_thread() 同样以 100 Hz 发布 carControl:纵轴经系数 4 转加速度,横轴经车辆模型转换曲率及转角,latActive 依赖 selfdriveState.active 与转向故障,longActive 还依赖纵向控制能力和覆盖事件。软件层后续仍走 card 和 panda 安全钩子。

    纵向 Maneuver.get_accel() 等待初始车速、纵向激活与驻车状态,再由 _step() 插值并推进动作/重复次数;main() 发布目标加速度、停止条件及调试文本。横向 Maneuver.get_accel() 等待约两秒稳定条件、记下基准曲率,main() 把横向加速度换算成 baseline + a/v²;Controls.state_control() 仅在该消息有效时替代模型期望曲率,随后调用 clip_curvature()。这里的「加速度」是横向试验扰动,不是纵向油门命令。

    入口 节拍/条件 语义要点
    Keyboard.update() / Joystick.update() 输入事件 键盘逐键累积;手柄动态轴范围、死区与三次曲线
    send_thread() 100 Hz 发布 testJoystick
    joystickd_thread() 100 Hz 纵轴系数 4 转加速度;横轴经车辆模型转曲率/转角
    横向 main() 稳定约两秒后 baseline + a/v² 覆盖期望曲率;未发消息则不替代

    网络消息桥与崩溃诊断

    get_services() 决定转发主题,zmq_to_msgq() 建立网络订阅到本地发布。白名单使用子串查找,不是严格集合匹配;get_port() 按主题名散列端口,源码注明可能冲突。新增跨设备插件必须显式定义主题允许集、身份鉴别和冲突处理,不能把此调试桥当生产控制 API。

    崩溃收集侧,get_tombstones() 只扫描至多 1000 个目录项,report_tombstone_apport() 提取可执行路径、信号和首个相关栈帧,清洗文件名后复制崩溃文件,main() 每五秒比较增量。报告路径适合诊断插件订阅,不能影响实时安全控制。

    仿真闭环与其证据边界

    run_bridge.create_bridge() 启动 MetaDrive 进程,SimulatorBridge._run() 创建车、传感器与各自周期线程,读取人工输入及 carControl,按使能状态决定应用人工还是 openpilot 输出。SimulatedCar.send_can_messages() 生成 Honda Civic 2022 风格 CAN,send_panda_state() 发布虚拟 pandaStates,SimulatedSensors.update() 发布 IMU、GPS、驾驶员状态和周边设备状态,send_camera_images() 送 VisionIPC 图像;launch_openpilot.sh 使用 NOBOARD=1、SIMULATION=1 和强制 Honda 指纹。

    但这条路径没有证明真实 panda 安全钩子、线束、电气故障、实际相机时序或目标车 DBC 行为。test_sim_bridge.py 的驾驶闭环测试被 unittest.skip 明确跳过,test_metadrive_bridge.py 继承该测试且还可能因未安装 MetaDrive 跳过。因此测试文件存在也不能声称当前仿真闭环通过。调试控制、纵向规划替代、横向曲率覆盖和网络消息桥分别是不同权限等级;测试插件不能将 UI 的互斥开关视为唯一安全门,仿真适配器也应保持独立于生产硬件抽象。

    常见问题

    开启了 JoystickDebugMode 是否就等于可以实车控制?

    不等于。源文档明确脚本设置 JoystickDebugMode「不是安全固件许可」,且脚本要求驻车启动或在 ZMQ 模式下运行。使能、故障和纵向覆盖状态仍参与计算,软件层后续仍走 card 和 panda 安全钩子。

    UI 里三个实验参数互斥,是否已经足够安全?

    不足。开发者界面的互斥只是配置层保障,源文档建议插件架构仍应在进程启动层做独立冲突检测。另外若外部插件冒充机动主题,会影响使能和告警路径,因此需要进程级仲裁与回放基线。

    仿真测试文件存在,是否说明仿真闭环已经通过?

    不能这样推断。test_sim_bridge.py 的驾驶闭环测试被 unittest.skip 明确跳过,test_metadrive_bridge.py 继承该测试且可能因未安装 MetaDrive 跳过。而且仿真路径没有证明真实 panda 安全钩子、线束、电气故障、实际相机时序或目标车 DBC 行为。

    msgq/ZMQ bridge 能不能当作生产控制接口?

    不能。源文档明确它「传输消息,不校验驾驶控制语义」,白名单使用子串查找而非严格集合匹配,端口按主题名散列且源码注明可能冲突。新增跨设备插件必须显式定义主题允许集、身份鉴别和冲突处理。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/DEVELOPMENT_TOOLS_DEEP_DIVE.md(取用了四类调试入口的运行条件、关键执行路径、消息桥与崩溃诊断边界、仿真闭环的证据边界)。
    • 项目 02 —— openpilot-research/PLUGIN_REFACTOR.md(仅用于确认调试工具在插件化中的权限分级约束)。

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

    更新记录

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