标签: openpilot

  • process_replay 的配置、触发器与同步:14 个进程怎么被回放

    结论:process_replay 的 ProcessConfig 里 pubs 是注入给进程的主题,subs 是采集进程输出的主题——与被测进程自己的发布 / 订阅视角相反。回放的「首帧」「双摄齐备」「输出主题周期」都是测试调度逻辑,给出的是功能回归证据,不等于实时车载时序:processing_time 只用于给输出合成 logMonoTime,不是测量到的运行时延。固定快照基线为 c8fb906,本文对原版三个回调做了 AST 隔离探针,不启动任何受管进程。

    配置字段与调用顺序

    ignore 和 tolerance 由测试比较器消费;processing_time 仅给输出合成 logMonoTime;timeout 控制单个 run_step() 周期。vision_pubs 把相机状态与额外 VisionIPC 帧发送关联起来。注意 ignore_alive_pubs 在两个模型配置中赋值,但该字段在本回放模块没有读路径,不能把它当作已实现的存活校验豁免。

    get_process_config() 从表中深拷贝配置;ProcessContainer.__init__() 再深拷贝配置及受管进程,故有状态触发器通常每次回放独立。start() 依次建立隔离前缀、Params / 环境、config_callback、fake 事件、Pub / Sub、可选 VisionIPC、进程,再执行 init_callback。ProcessConfig.__post_init__() 对普通 MessageBasedRcvCallback 自动把触发主题设为 main_pub,模型配置则显式指定 VisionIPC 端点——后者的事件同步点是图像流端点,不能简单等同相机状态消息自身。

    14 个进程配置速查

    下表是回放触发逻辑,不是车型支持或测试实际覆盖矩阵;默认测试会排除两个模型进程。各行 pubs / subs 的完整清单以 CONFIGS 定义为准。

    进程 回放触发 / 同步入口 主要输出
    selfdrived carState,并设置 first_frame=True selfdriveState、onroadEvents
    controlsd selfdriveState carControl、controlsState
    card can 输出频率与 sendcan 启动门,main_pub_drained=True sendcan、carState、carParams、carOutput、radarTracks
    radard modelV2 radarState
    plannerd modelV2 longitudinalPlan、driverAssistance
    calibrationd cameraOdometry,first_frame=True extrinsicsCalibration
    dmonitoringd driverStateV2 driverMonitoringState
    locationd cameraOdometry deviceMotion(合成处理时间 10 ms)
    paramsd deviceMotion vehicleParameters
    lagd deviceMotion lateralDelay
    ubloxd 没有回调:每条被注入消息都可结束一轮 ubloxGnss、gpsLocationExternal
    torqued deviceMotion,first_frame=True lateralTorqueParameters
    modeld 双路道路相机状态标志;主入口为窄路 VisionIPC 端点 modelV2、drivingModelData、cameraOdometry
    dmonitoringmodeld cabinCameraState;主入口为座舱 VisionIPC 端点 driverStateV2

    除 locationd 外,显式 processing_time 是 selfdrived / card / paramsd 的 4 ms 和两个模型的 20 ms,其余默认 1 ms。这些值用于回放中不同进程输出的排序,不能拿来证明对应进程在硬件上达到该延迟。

    ignore / tolerance 由测试比较器消费,属于输出比对边界,而不是进程行为的一部分;调整它们改变的是「通过」的定义,而非被测逻辑本身。ubloxd 没有回调,每条被注入消息都可结束一轮——这类「无触发门」的进程在回放里最容易产生与真实节拍不同的批次划分。模型配置的 fake 事件主入口是图像流端点(窄路或座舱 VisionIPC),而不是相机状态消息本身。

    触发器逐函数行为

    MessageBasedRcvCallback.__call__() 返回「当前主题是触发主题」或「first_frame=True 且 frame==1」。这里的 frame 实际是 run_step() 完成周期后递增的 self.cnt。对非触发主题做隔离探针,计数为 0、1、2 时结果依次为 False, True, False;因此「首帧」并不意味着第一个到达的输入无条件结束周期。

    触发器 探针结果与含义
    ModeldCameraSyncRcvCallback 双摄时两路出现标志齐备才返回真并复位;单摄时窄路出现即可。输入 wide, wide, narrow 结果为 False, False, True——只追踪出现标志,不匹配 frameId,也不验证宽 / 窄帧同一时刻。
    card_rcv_callback 只对 can 计算输出主题周期 frame % int(can频率 / 输出主题频率),且 sendcan 在 (frame - 1) < 2000 时从候选主题删掉。频率相同且仅 sendcan 为输出时,周期计数 0、1、2000、2001 返回 False, False, False, True。
    selfdrived_config_callback 把 cfg.pubs 由列表改为集合差值,删除 GPS 二选一的一侧。
    card_fingerprint_callback 初始化时喂入最多 300 条 CAN,等待 CarParams 出现;get_car_params_callback() 优先按指定指纹构造参数,否则用 CAN、固件查询跳过标志和缓存去识别车型。

    某一路相机缺失时消息会在 msg_queue 中继续累积,不能把「已等齐两路」解释为视觉同步已验证。card_rcv_callback 是测试初始化阶段的输出采集门,不是车端允许发送 CAN 的 safety 决策;配置的频率比还需保持可除且 int 后非零,源码没有在此函数显式校验非法配置。

    对插件化与自制硬件的含义

    新增插件不能只注册一个进程名:应同时声明输入 / 输出主题、schema、触发帧或时间门、初始 Params、相机帧关联键、输出完整性和故障策略。回放配置要能区分「收到任意消息」「收到触发主题」「同一 frameId 的双摄齐备」和真实 deadline;当前回放的标志同步与合成时间戳只给出功能回归证据。自制相机 / 计算板还须独立验证时间戳域、丢帧 / 重复帧、IPC 缓冲寿命、端到端帧年龄、超时回退,以及 panda / opendbc 的发送限制。本文未运行目标硬件或完整回放。

    常见问题

    pubs 和 subs 到底哪个是给被测进程的?

    pubs 是注入给进程的主题,subs 是采集进程输出的主题,与被测进程自己的发布 / 订阅视角相反。混淆两者会读错回放配置。

    回放的 processing_time 是真实运行时延吗?

    不是。processing_time 只给输出合成 logMonoTime,用于回放中不同进程输出的排序。显式值有 selfdrived / card / paramsd 的 4 ms 与两个模型的 20 ms,其余默认 1 ms;不能拿来证明硬件达到该延迟。

    双摄对齐在回放里有校验吗?

    没有校验同一时刻。ModeldCameraSyncRcvCallback 只追踪宽 / 窄两路「出现」标志,标志齐备即返回真,不匹配 frameId。探针输入 wide, wide, narrow 得到 False, False, True。

    「已等齐两路」能说明视觉同步已验证吗?

    不能。回放只检查出现标志;某一路缺失时消息会在 msg_queue 中继续累积。「首帧」也不意味着第一个到达的输入无条件结束周期——它依赖周期计数在 1 时被其他条件允许。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/PROCESS_REPLAY_CONFIG_TRIGGER_SYNC_CONTRACT_DEEP_DIVE.md(取用了 ProcessConfig 字段、14 个 CONFIGS、触发器探针行为与插件化含义分析)。

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

    更新记录

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

  • 车辆消息字段契约:请求、软件回报与硬件执行的三层之分

    结论:车端消息契约里最容易混淆的是「请求、软件回报、硬件执行」三层。CarOutput.actuatorsOutput 只是软件计算后的回报,不表示 panda TX 钩子接受、ECU 已执行或车辆真实响应;CarState.valid 直接取 CS.canValid,不自动合并全部逐字段新鲜度;SafetyConfig 虽是列表,但当前所读 setSafetyMode() 只取 safetyConfigs[0]。schema 注释是意图,运行时许可必须以代码及 panda 固件为准。本文为固定源码快照 c8fb906 的静态阅读,未运行回放或测试。

    生产和消费链

    card.state_update() 从 CAN 批次构造 CarState,card.state_publish() 发 carState、carOutput 和周期性 carParams;controlsd 用 CarState、selfdriveState 与计划形成 CarControl,再由车型控制器转成候选 CAN 帧。CarParams 还在启动时写入 Params,pandad 需等待 FirmwareQueryDone 和 ControlsReady 后读取其安全配置。

    消息 生产者 主要消费者
    CarState card.state_update() 状态机、控制器
    CarControl controlsd 车型控制器 apply()
    CarOutput card(取上次保存值) 控制器饱和逻辑参考
    CarParams 启动时写入 Params pandad 读取安全配置

    把这条链当成单向管道同样不准确。同一轮里 controlsd 既消费 CarState,又把自己的输出回报成 CarOutput 供下一轮饱和判断使用;CarParams 虽在启动时写入,却是 pandad、控制器与规划层共同读取的配置来源。理解这种「请求—回报—配置」的多向关系,是判断某个字段能否作为执行依据的前提。

    CarState:归一化车况

    字段簇 schema 语义 运行时注意点
    CAN 健康 canValid、canTimeout、canErrorCounter 前两者由车型接口汇总解析器健康;canErrorCounter 累计的是空 CAN 接收周期,不是校验和错误计数;顶层 carState.valid 直接取 CS.canValid
    速度与巡航 vEgo 估计、vEgoRaw 原始轮速、vEgoCluster 仪表速度 单位/估计来源有别,不能把仪表显示值直接反馈给动力学控制;CruiseState 区分可用、已启用和静止
    驾驶员与转向 踏板为用户输入;驾驶员扭矩和 EPS 扭矩使用原生 CAN 单位,另有 steeringPressed、steeringDisengage 与暂时/永久故障 跨车型不得把原生扭矩直接比较;原车 AEB/LKAS/FCW 故障与功能状态是不同字段
    交互与车身 档位、按键边沿、转向灯、门、安全带、盲区 ButtonEvent 应在变化时发送;布尔默认假值不代表传感器已观测到安全无障碍

    车型基类先更新所有 CANParser,再调用品牌 CarState.update(),最后计算 canValid/canTimeout、仪表速度迟滞及 buttonEnable。需要明确:canValid 并不是所有源的数字签名。

    逐字段看,vEgo、vEgoRaw、vEgoCluster 三者单位相同但含义不同:一个是估计值,一个是原始轮速,一个是经迟滞处理的仪表速度。把它们当作可互换的「车速」会导致控制层用到被截零或迟滞过的值。同理,驾驶员扭矩与 EPS 扭矩使用原生 CAN 单位,跨车型直接比较会得到错误结论。

    CarControl 与 CarOutput:请求和回报

    CarControl.enabled 是总使能,latActive/longActive 分别表达横纵向活动。controlsd.state_control() 从 selfdriveState、车况故障、低速门、纵向覆写事件及 CarParams.openpilotLongitudinalControl 计算它们,三者不应合成一个开关。Actuators 中 torque、steeringAngleDeg、curvature 是按控制类型选用的横向请求,accel 是纵向 m/s² 请求;另有 gas、brake、torqueOutputCan、speed 用于记录车型控制器实际计算的下层值。

    字段 含义 不能推断的结论
    CruiseControl 取消/恢复/覆写意图 不能等同具体 CAN 帧,需品牌控制器映射
    HUDControl 发给原车仪表的提示数据 同样是请求,不代表仪表已响应
    CarOutput.actuatorsOutput 车型控制器计算值,注释称反映发送前的车型限幅/特性 不是 panda TX hook 接受、ECU 已执行或车辆真实响应的确认
    carOutput.valid 取 carControl 的 all_checks 不能升级成物理回执

    controlsd 会比较 CarOutput 与本轮 CarControl.actuators,估计转向受限以影响控制器饱和逻辑。controlsd 发布 carControl 时把外层 Event.valid 设为 CS.canValid;card.controls_update() 的本地门控是 carControl 存活后调用品牌 apply(),并用 CS.canValid 标记 sendcan 的外层有效位。它不在这一函数再次以 carControl.valid 为硬许可条件;完整许可仍依赖上游状态机、车型控制器及 panda 安全层。

    这里存在一个容易忽略的非对称:carControl 的外层 Event.valid 取 CS.canValid,而 card.controls_update() 在调用品牌 apply() 时并不再以 carControl.valid 为硬条件,只要求 carControl 存活。也就是说,消息层有效性、上游状态机、车型控制器与 panda 安全层各自负责一段,任何单一有效位都不能代表整条链已获授权。

    CarParams:跨进程配置

    字段簇 作用及信任界限
    身份/证据 brand、carFingerprint、VIN、carFw、fingerprintSource、fuzzyFingerprint 记录识别结果;来源标记不是身份认证或执行许可
    动力学/调参 质量、轴距、重心、转向比、惯量、轮胎刚度、PID/扭矩调参、执行器延迟进入规划控制;标称值需按目标车辆验证
    执行配置 pcmCruise、最低速度、横纵向控制能力、steerControlType、safetyConfigs、alternativeExperience、passive、dashcamOnly 决定不同层行为;任意单字段都不是最终执行许可
    认证状态 secOcRequired、secOcKeyAvailable 记录车型需求和密钥可用状态;不能把「存在密钥」理解为报文已通过对端认证

    SafetyConfig 是 (safetyModel, safetyParam) 列表,但当前所读 PandaSafety::setSafetyMode() 对其连接的 panda 只取 safetyConfigs[0] 并设置 alternativeExperience;多 panda 的分配须看上层设备编排。card 在被动模式会把列表替换成 noOutput。

    身份/证据类字段尤其需要小心:fingerprintSource 与 fuzzyFingerprint 只是来源标记,说明识别结果是如何得到的,而不是身份认证;secOcKeyAvailable 说明密钥可用,也不表示对端报文已通过认证。两者都不能升级为执行许可。动力学与调参字段同理,它们是标称值,进入规划控制前必须按目标车辆验证。

    RadarData 与扩展边界

    RadarData.errors 包含 CAN 错误、雷达故障、错误配置及临时不可用;点数据包含轨迹 ID、相对距离/横移/速度。schema 注释称这些点字段必需,但 Cap’n Proto 默认值仍可能存在,消费者应结合生产者健康和点轨迹年龄;card 只在收到 RD 时发 radarTracks,外层 valid 由错误位汇总。对自制硬件和插件化,建议将字段单位、坐标系、采样频率、时间戳、最大年龄、缺失值与信号来源做成版本化契约,并把外层 valid、字段默认值、软件输出回报和硬件执行确认分别建模。

    常见问题

    CarOutput 能证明车辆真实执行了吗?

    不能。它来自车型控制器 apply() 的计算值,注释称反映发送前的车型限幅/特性,是软件计算后的回报;不是 panda TX 钩子接受、ECU 已执行或车辆真实响应的确认。

    carState.valid 为真代表所有字段都新鲜吗?

    不代表。顶层 carState.valid 直接取 CS.canValid,并不自动合并全部逐字段新鲜度;消费关键字段时仍需结合解析器健康与字段年龄判断。

    SafetyConfig 列表里的每个配置都会生效吗?

    不能直接推断。当前所读 setSafetyMode() 只取 safetyConfigs[0] 并设置 alternativeExperience;多 panda 的分配须看上层设备编排。

    CarControl 里的计划字段等于具体 CAN 帧吗?

    不等于。CruiseControl、HUDControl 等是意图与提示数据,需要品牌控制器映射为具体帧;schema 字段不能直接等同 CAN 报文。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/CAR_MESSAGES_FIELD_CONTRACT_DEEP_DIVE.md(取用了生产消费链、CarState/CarControl/CarOutput/CarParams 字段簇、RadarData 与扩展边界)。
    • 项目 02 —— openpilot-research/ECU_DIAGNOSTIC_TRANSPORT_DEEP_DIVE.md(仅用于确认指纹与固件证据进入 CarParams 的位置)。

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

    更新记录

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

  • 桌面片段渲染:50 ms 日志分桶、帧号映射与 Params 前缀

    结论:桌面片段渲染器 tools/clip/run.py 与车端 encoderd --clip 是两套不同实现。它把日志按 50 ms 分桶、按 20 FPS/60 秒映射帧号、再用原车载 UI 渲染并用 FFmpeg 录成 MP4。三个最容易踩的边界是:分桶起点用每段首条日志的单调时间而非视频首帧 SOF;patch_submaster() 会把 valid 等标志全置真、不保留原始 Event.valid;GuiApplication 录屏等待编码子进程却不检查非零退出码。

    一、入口和分层调用

    阶段 关键函数 数据契约
    参数与录制配置 parse_args()、setup_env() 解析路段、秒数、输出、目标 MB、倍速和镜头;设置 RECORD/RECORD_OUTPUT/RECORD_SPEED/RECORD_BITRATE,这些值在随后导入 GUI 模块时成为录制配置。
    文件来源 Route.__init__()、Route.log_paths() 本地目录或 API 远端索引按段号组装列表,缺段位置为 None;默认 rlog,不从 qlog 抽样复原。
    日志分桶 load_logs_parallel()、_parse_and_chunk_segment() 线程并行读取完整段字节,进程池解码并迁移,按首消息时间起的 50 ms 窗口保存每主题最近一条 Event。
    UI 数据注入 patch_submaster()、UIState.update() 把 UI 的 SubMaster.update 替换为逐帧写入消息快照,再运行普通 UI 状态更新和增强道路视图。
    相机取帧 iter_segment_frames()、FrameQueue 全局帧号按 20 FPS / 60 秒映射到段内帧;后台线程预取 NV12,主线程推送本地 VisionIPC。
    合成与文件 clip()、render_overlays()、GuiApplication.init_window() 用原车载 UI 渲染画面,叠加可选路段/时间/标题,GuiApplication 经 FFmpeg 录制输出 MP4。

    二、时间、图像与消息一致性

    _parse_and_chunk_segment() 以每段首条日志的单调时间为起点,而不是该段视频首帧 SOF。一个 50 ms 桶只保留同主题最后一条;时间戳跨越多个桶时只追加一次旧桶并把 next_time 跳到新消息附近,不会补齐中间空桶。clip() 随后用 frame_start 和 duration * 20 直接切这个桶列表;若日志起点偏离视频起点、消息缺口或某段不足 60 秒,桶序号和图像帧号可能错位或片段短于请求。程序只检查切片非空,不验证切片长度等于请求帧数。

    时间基准 来源 与真实相机时间的关系
    日志分桶起点 每段首条日志的单调时间 与视频首帧 SOF 可能不一致,是桶序号与帧号错位的隐患来源。
    帧号映射 20 FPS / 60 秒 按段内帧计数,不校验切片长度是否等于请求帧数。
    VisionIPC SOF/EOF frame_idx * 50 ms 起点为零且二者相同,并非原车相机时间。

    patch_submaster() 对存在于 UI 服务表中的消息把 seen、updated、alive、valid 全置真,并把原始 logMonoTime 与当前本机接收时间混合登记;它没有保留原 Event.valid,未出现的新主题也不会在此函数中被显式标成失活。因此导出画面可显示出原始日志里曾无效或已失联的数据,不宜作为告警新鲜度的权威回放。VisionIPC 发送则用 frame_idx * 50 ms 为 SOF/EOF,起点为零且二者相同,并非原车相机时间。

    --qcam 通过 FFmpeg 将整段 qcamera 解码为 NV12 并保留在内存数组;普通 fcamera 走 FrameReader.get() 随机访问。FrameQueue 只从首个非空相机路径探测宽高,后续段没有统一尺寸校验;缺目标段时主动抛错。宽路镜头只要所选区间中存在任意 ecamera 路径就整体启用,但其中某段缺文件时仍会按每一帧取宽路,导致该段失败。

    三、Params、资源和失败边界

    clip() 在进入 OpenpilotPrefix 之前导入全局 ui_state。UIState._initialize() 当场构造 Params(),而 Params::Params() 在构造时按当前 OPENPILOT_PREFIX 固定路径。随后 load_route_metadata() 在新 Prefix 中把路段 initData.params 写入另一个 Params 命名空间,但已存在的 ui_state.params 仍指向旧路径,因此 UI 的参数刷新不保证读到该路段快照。另外 load_route_metadata() 假定第一个可用日志含 initData,否则访问会抛错;缺头的路段即使视频/其他日志可用也可能提前失败。

    FrameQueue._worker() 异常后在 finally 中投递终止哨兵,队列满而消费停止时该 put() 也可能阻塞;stop() 最多等线程 2 秒。clip() 只在正常渲染循环后显式停队列、关 GUI,没有包住整个运行链的 finally;解码、VIPC 或 UI 异常时清理可能不完整。GUI 的 close_ffmpeg() 会等待编码子进程,但不检查非零退出码,因此 clip() 之后打印「saved」并不等于文件可解码。RECORD_OUTPUT 被强制改成 .mp4 后,成功日志却打印原始 -o 路径,非 .mp4 后缀输入会造成日志路径与实际文件名不一致。

    parse_args() 只要求结束时间大于起始时间,没有对负起点、零/负倍速做完整校验;默认正的目标体积下 --speed 0 会在码率计算中除零。若选中的 rlog 路径列表含 None,并发读取阶段就会失败;这与「部分路段可预览」的语义不同。

    四、面向硬件和插件化的约束

    重构建议是把路段来源、时间轴对齐、相机解码、消息状态复原、UI 渲染和文件写入划为独立插件接口,但输入必须带不可变路段 manifest(段号、内容哈希、首末单调时刻、帧 ID、相机 SOF/EOF、schema 版本)。回放宿主应独占隔离 Params/msgq/VisionIPC 命名空间,并在导入 UI 单例之前建立命名空间;不能让路段 Params 落到生产配置,也不能把过期/无效 Event 提升为有效。文件输出需校验实际路径、FFmpeg 退出码和可解码帧数,异常路径统一回收线程、IPC、GPU 和临时文件。此桌面渲染链不能替代车端硬编/硬解或原车时序的台架验收。

    常见问题

    导出画面里的数据一定是有效的吗?

    不一定。patch_submaster() 把 seen、updated、alive、valid 全置真且不保留原 Event.valid,未出现的新主题也不会被显式标成失活。因此导出画面可显示出原始日志里曾无效或已失联的数据,不宜作为告警新鲜度的权威回放。

    为什么导出的片段长度可能与请求不符?

    因为分桶以每段首条日志的单调时间为起点而非视频首帧 SOF,一个 50 ms 桶只保留同主题最后一条且不补齐中间空桶。clip() 用 frame_start 和 duration * 20 直接切桶列表,日志起点偏离、消息缺口或某段不足 60 秒都可能造成错位或片段过短;程序只检查切片非空。

    导出打印 saved 就说明文件可解码吗?

    不能说明。GuiApplication 的 close_ffmpeg() 等待编码子进程但不检查非零退出码,因此打印「saved」不等于文件可解码;而且 RECORD_OUTPUT 被强制改成 .mp4,成功日志仍打印原始 -o 路径,非 .mp4 后缀会造成路径不一致。

    –speed 0 会怎样?

    parse_args() 只要求结束时间大于起始时间,没有对负起点、零/负倍速做完整校验;在默认正的目标体积下,--speed 0 会在码率计算中除零。这是源码可见的输入校验缺口,不是可控的「静音播放」。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/DESKTOP_CLIP_RENDER_LOG_CAMERA_UI_CONTRACT_DEEP_DIVE.md(取用了分层调用、时间/图像/消息一致性、Params 与前缀顺序、资源与失败边界、插件化约束)。

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

    更新记录

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

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

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

    两条主链的入口与时序

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

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

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

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

    文件选择与共享辅助

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

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

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

    门禁层次与元数据

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

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

    可见风险与插件化约束

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

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

    验证范围

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

    常见问题

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

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

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

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

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

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

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

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

    延伸阅读

    参考资料

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

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

    更新记录

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

  • Hyundai 车辆输入:经典 CAN、CAN FD 与按钮意图的边界

    结论:Hyundai 的车辆输入不能按「同品牌即同契约」理解:经典 CAN 把 Bus.pt 映射到物理 0、Bus.cam 映射到 2,而 CAN FD 的动力/相机总线取决于 CanBus(CP).ECAN/CAM,LKA 转向平台还会交换 A-CAN 与 E-CAN,所以不能把「E-CAN 总是 0」写死到线束或接口。另一个易误解点是按钮意图:recent_button_interaction() 检查的是最近八个报文样本而非固定八帧或 80 ms 窗口,无新样本时旧交互可能跨多个调用继续被视为「最近」。本文为固定源码快照 c8fb906 的静态阅读,未运行回放、未连接 CAN FD 硬件。

    组件与关键函数

    CarState.__init__() 根据车型 flags 选择档位、踏板、按钮报文及 DBC 枚举,建立八样本按钮队列、仪表速度采样器和控制参数;CarState.update() 做经典 CAN 车辆状态归一化,CAN FD 车型转给 update_canfd()。总线与解析器由 get_can_parsers()/get_can_parsers_canfd() 分别建立,CarInterfaceBase.update() 先更新解析器再归一化,并汇总 canValid、canTimeout。

    入口 职责 边界
    CanBus.__init__() 依据 LKA 转向架构交换 A-CAN/E-CAN,并按安全配置数计算端口偏移 插件不能把「E-CAN 总是 0」写死
    recent_button_interaction() 判断八个最近巡航/主开关样本是否出现驾驶员启用意图 是样本数而非固定帧数或固定时间窗
    create_button_events() 由当前/上次状态合成按下和释放事件 中间报文过渡不会自动逐个转成事件
    CarEvents.update() 在 PCM 巡航启用上升沿,结合 blockPcmEnable 决定是否生成启用事件 仅上升沿且未被阻断时才生成

    总线与解析健康

    车型接口先用相机/动力 fingerprint 推导 LKA、备用按钮、档位及 SCC flags,状态解码依赖这些参数已经正确生成。经典 CAN 的两个解析器起初都不预注册报文;CAN FD 非备用按钮路径仅将 CRUISE_BUTTONS 以 1 Hz 预注册,源码注释说明它名义 50 Hz、但 ECU 可停止约半秒;备用按钮路径则不预注册该报文。其余读取 cp.vl[...] 会动态注册 DBC 报文,而 vl_all 自身不具有动态注册能力。

    概念 确切含义 常见误读
    动态注册时机 发生在本轮 CANParser.update() 之后 首轮此前到达的新报文不在缓存中,零初值不代表物理零测量
    vl_all 本轮样本 每轮清空,只加入成功解码的帧;最近值 vl 保留到下次成功帧 不能用 vl 推断某值采自当前周期
    canValid 所有已注册报文的聚合活性/计数器判据 不是逐字段新鲜度证明
    canTimeout 总线空窗判据 不能说明某个 vl 值恰好来自当前周期

    1 Hz 预注册只是调整该按钮报文的超时判据,不表示发送频率真的 1 Hz。

    经典 CAN 状态路径

    CLU11 决定 kph/mph,四轮 WHL_SPD11 通过基类 parse_wheel_speeds() 进入 vEgoRaw 和 KF1D;但 standstill 只比较左前与右后两轮,阈值为 12 * 0.03125,不是四轮均值。CLU15 仪表车速初始在第一次更新被采样,之后计数器从 0 增至大于 20 才再采一次,即正常每 21 次 CarState.update() 重采;非 Sorento 的英制仪表分支另作向下取整换算,再由基类接口对 vEgoCluster 做迟滞/低速截零,这不是轮速 KF1D 的原始速度。

    SAS11 给方向盘角与角速度,MDPS12 给驾驶员/电机扭矩和临时转向故障。驾驶员施力调用基类计数去抖(阈值 5),方向灯则按灯状态延续 50 次调用,二者与 Toyota 的直接扭矩阈值不同。巡航来源由 CAMERA_SCC 决定:原车纵向读动力或相机 SCC11/SCC12,自控纵向则从动力 TCS13 得到 available/enabled,并明确这些字段不用于实际启停按钮判决。刹车 DriverOverride==2 包含混动/电动车驾驶员再生制动;油门和档位再按 ICE/EV/HYBRID/FCEV flags 分报文。USE_FCA 决定原车 AEB/FCW 来源,HAS_BSM 才读 LCA11。相机 LKAS11 和动力 CLU11 被浅拷贝作为控制器构帧模板,它们是解析器最新缓存,状态函数没有单独验证每个字段的年龄。

    CAN FD 状态路径

    CAN FD 的档位消息在 ACCELERATOR、GEAR_ALT、GEAR_ALT_2、GEAR_SHIFTER 中选;踏板消息也分 EV/HYBRID/其他。CRUISE_BUTTONS_ALT.DISTANCE_UNIT 决定仪表单位,轮速四轮全低于阈值才视为停止;转向/扭矩/故障分别来自 STEERING_SENSORS 与 MDPS。TCS.ACCEnable 决定巡航可用,自动纵向用 TCS.ACC_REQ 作启用状态;原车纵向则按 CANFD_CAMERA_SCC 从相机或动力 SCC_CONTROL 读取启用、停止和设定速度。EV 平台另以 MANUAL_SPEED_LIMIT_ASSIST.MSLA_ENABLED 标记 nonAdaptive,与自适应 ACC 启用不是同一个概念。LKA 转向车还缓存相机 CAM_0x2a4/0x362 供后续 LFA 抑制帧保留原字段,同样没有逐字段新鲜度检查。

    按钮历史、边沿和下游

    经典 CAN 从 CLU11、CAN FD 从选定 CRUISE_BUTTONS 报文的 vl_all 取得本轮可能多条按钮样本,追加到最长 8 条的队列。recent_button_interaction() 检查最近八个报文样本是否有 RES/SET/CANCEL 或任意主开关非零;无新样本时队列不前进,旧交互可能跨多个调用继续被视为「最近」。另一方面,buttonEvents 只比较队列本轮前的末尾与追加后的末尾,一轮内多个中间跳变不会逐个变成事件。blockPcmEnable 取该意图判据的反值,CarEvents 仅在 PCM 巡航由未启用变启用且未被阻断时生成 pcmEnable。CAN FD 还保留按钮 COUNTER,控制器构造原车巡航取消/恢复按钮候选时以 CS.buttons_counter + 1 作为输入;缓存字段是最近成功解析值,能构帧不等于 ECU 已接受。

    新硬件与插件契约

    车型输入插件应明确声明 DBC、物理总线与线束拓扑、每个报文的原始样本时间、校验/计数器结果和有效期。驾驶员按钮需要同时提供逐条边沿序列与当前状态;blockPcmEnable 的意图窗口要用样本年龄或实际时间约束,不能让无新帧的旧按钮无限代表当前意图。原车 AEB/FCW、BSM、刹车与转向故障必须保留各自来源和缺测状态,无法验证的关键输入不能默认为安全。非上车验收应按经典 CAN/CAN FD、LKA/LFA 线束、相机/动力 SCC、四类动力踏板、备用按钮及档位组合建立参数矩阵,并注入首轮报文、按钮突发/断流、单轮速度异常、SCC 丢失、相机缓存过期与总线映射错误。

    常见问题

    Hyundai 同品牌车型的 CAN 总线映射是一样的吗?

    不一样。经典 CAN 与 CAN FD 的物理总线不同,LKA 转向平台还会交换 A-CAN 与 E-CAN 并叠加安全配置偏移,所以不能把「E-CAN 总是 0」写死,需按线束与 DBC 核对。

    recent_button_interaction() 是固定八帧窗口吗?

    不是。它检查最近八个报文样本,既不是固定八帧也不是固定 80 ms;无新样本时队列不前进,旧交互可能跨多个调用仍被视为「最近」。

    1 Hz 预注册 CRUISE_BUTTONS 代表该报文真的 1 Hz 吗?

    不代表。预注册只是调整该按钮报文的超时判据;源码注释说明它名义 50 Hz,但 ECU 可停止约半秒。

    能构出取消/恢复巡航帧就代表 ECU 已接受吗?

    不代表。控制器用的是缓存字段(如 CS.buttons_counter + 1)与最近成功解析值,能构帧不等于 ECU 已接受,也不是硬件执行确认。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/HYUNDAI_CARSTATE_CLASSIC_CANFD_INPUT_DEEP_DIVE.md(取用了组件索引、总线与解析健康、经典 CAN/CAN FD 状态路径、按钮边沿与下游、插件契约)。
    • 项目 02 —— openpilot-research/CAR_MESSAGES_FIELD_CONTRACT_DEEP_DIVE.md(仅用于确认归一化结果进入的消息结构位置)。

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

    更新记录

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

  • 驾驶员监测与在线估计:多进程反馈环与有效位分层

    结论:驾驶员监测、相机标定、设备姿态与在线参数学习构成一个带反馈的多进程图,而不是单向初始化序列:modeld 的相机里程计帮助标定与姿态估计,而标定与在线延迟估计又反过来改变模型输入变换与动作时间。两个必须分清的有效位层次是:dmonitoringd 在订阅输入无效时不更新策略,却仍发布有效位为假的监测状态;deviceMotion 顶层有效位取决于滤波器初始化与滤波时间,inputsOK、sensorsOK、posenetOK 是额外健康指标而非同一件事。本文为固定源码快照 c8fb906 的静态调用链分析,模型文件仍是 LFS 指针,未运行设备传感器、驾驶员模型或实车参数学习。

    带反馈的多进程图

    座舱视频流进入座舱模型进程生成 driverStateV2,监测策略进程以它为轮询主题生成 driverMonitoringState,供行车状态进程与控制进程消费。另一条链上,道路模型进程产出相机里程计,分别驱动标定进程与定位进程,IMU 也送入定位;标定结果又回送给模型与定位进程。定位发布的 deviceMotion 同时驱动在线参数、扭矩与延迟三个估计进程,它们的结果再回流到控制进程,延迟估计还回流到模型进程。这种闭环意味着插件化时必须防止循环依赖中的版本不一致、过期消息,以及把历史估计当成实时观测。

    座舱模型与监测策略

    组件 关键入口 读入与输出
    座舱模型 ModelState.run() 从座舱视频缓冲建立输入,做鱼眼 warp 与模型推理
    预测解码 parse_model_output() 解码左右驾驶位人脸姿态、眼睛与眨眼、墨镜、手机、睡眠和方向盘位置概率
    消息填充 get_driverstate_packet() 发布 driverStateV2 的帧 ID、模型耗时和左右驾驶位特征
    策略循环 dmonitoringd_thread() 以 driverStateV2 为轮询主题;输入检查通过才调用策略
    注意力策略 DriverMonitoring.run_step() 合并人脸预测、车速、档位、方向盘与油门交互、辅助驾驶使能及标定
    告警与锁定 _update_events() 注意力递减与恢复;达阈值后升级一至三级告警,累计无响应与锁定

    dmonitoringd_thread() 在订阅输入无效时不更新策略,但仍发布有效位为假的监测状态。controlsd 消费无响应强制减速字段形成控制状态里的强制减速标记,行车状态进程还根据监测事件处理使能。调试或驾驶员视角模式在源码中有单独的示例分支,不能用它的默认速度和使能值代表实际行车行为。

    相机标定与设备姿态

    标定器只采用满足车速、直行、角速度和不确定度条件的相机里程计,更新安装俯仰与偏航、相机间角度和高度的分块统计;状态区分未标定、重标定、已标定与无效,安装姿态分散度过大时平滑重标定,发布的状态里含进度、安装角、分散度和高度,非汽车模式有专用默认值。姿态滤波器按时间顺序处理加速度计、陀螺仪、车速、标定和相机里程计,对时间戳、来源、幅值与方差做检查后更新滤波器,发布 deviceMotion 的姿态、速度、角速度、加速度以及三项健康指标。定位循环以相机里程计驱动发布,把传感器与消息输入按单调时间排序后送入滤波器,并追踪坏输入计数与恢复衰减。

    这里的关键边界是:deviceMotion 顶层有效位取决于滤波器初始化和滤波时间,inputsOK、sensorsOK、posenetOK 是额外健康指标,并不自动等同于顶层有效位。替换传感器时要保留坐标系、时间源、方差和数据来源检查;只给姿态角数值而没有这些元数据,不足以替代现有传感器输入。

    车辆在线参数、扭矩与延迟

    进程 关键入口 输出与消费关系
    paramsd VehicleParamsLearner.handle_log()、get_msg() 估计转向比、轮胎刚度、侧倾及方向盘零偏,发布车辆参数供控制进程构建车辆模型与转向补偿
    torqued TorqueEstimator.handle_log()、get_msg() 估计横向加速度因子、偏置和摩擦并发布;扭矩控制器在消息检查通过且参数启用时采用
    lagd LateralLagEstimator.handle_log()、update_estimate() 比较期望与实际横向响应估计执行器延迟,发布给模型与控制进程

    三个进程都以 deviceMotion 为主要节拍读取消息:paramsd 在新 deviceMotion 上发布车辆参数,torqued 与 lagd 约每五帧发布一次(源码注释称 4 Hz)。paramsd 对非有限滤波状态重置并把细分估计有效性写进消息;lagd 当块数不足或方差过大时把状态标成未估计或无效,延迟回退初始值;torqued 对不足或无效样本不会宣称新估计有效,并对异常估计做重置。三者都周期性写参数供重启后恢复,但持久缓存不是实时消息的替代品。

    对硬件与插件化的约束

    第一,座舱摄像头插件需要交付可追溯的帧 ID、时间戳、图像几何和标定输入,驾驶员监测策略及状态机必须继续能判断数据是否新鲜有效。第二,IMU 与相机插件要声明轴向、坐标系、采样频率、时间基准和噪声模型,姿态滤波器当前有明确的来源、时间和物理量合理性检查。第三,在线参数学习插件不能直接把无效估计写成车辆执行命令,应保留有效位与细分状态、回退值和参数版本。第四,回放验证至少覆盖丢帧、时间戳倒序、相机标定漂移、传感器失效、驾驶员无响应和在线估计不收敛;实车控制最终仍经过车型接口与 panda 安全钩子。

    常见问题

    deviceMotion 的顶层有效位等于所有健康指标吗?

    不等于。顶层有效位在源码中取决于滤波器初始化和滤波时间,而 inputsOK、sensorsOK、posenetOK 是额外健康指标,两者是分层关系。因此拿到姿态角数值时,还要同时看来源、时间源与这三项指标,才能判断输入是否可用。

    驾驶员监测输入无效时会怎样?

    dmonitoringd_thread() 在订阅输入无效时不更新策略,但仍发布有效位为假的监测状态。也就是说监测状态存在不代表策略刚被刷新;下游消费无响应强制减速字段时,必须结合有效位与状态机判断,而不是只看字段是否存在。

    paramsd、torqued、lagd 的发布节拍一样吗?

    不完全一样。三者都以 deviceMotion 为主节拍,paramsd 在新 deviceMotion 上发布车辆参数,而 torqued 与 lagd 约每五帧发布一次(源码注释称 4 Hz)。因此不同估计值的时间新鲜度需要分别判断。

    为什么把在线估计当实时观测会出问题?

    因为这些进程是带反馈的闭环:标定与在线延迟估计会反过来改变模型输入变换和动作时间,消息可能来自不同采样时刻甚至历史估计。插件化时必须防止循环依赖里的版本不一致与过期消息,并保留有效位、回退值与参数版本,不能把持久缓存当实时观测。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/MONITORING_LOCALIZATION_DEEP_DIVE.md(取用了多进程反馈图、座舱模型与监测策略、标定与姿态、在线参数三进程与插件约束)。
    • 项目 02 —— openpilot-research/CALIBRATIOND_STATE_PERSISTENCE_DEEP_DIVE.md(仅用于确认标定状态与持久化的分工位置)。

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

    更新记录

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

  • 行车界面 GPU 渲染链:NV12 缓冲、投影裁剪与告警的受信任边界

    结论:行车画面是一条纯显示链:camerad → VisionIPC → CameraView → EGL 外部纹理或 PC Y/UV 纹理 → AugmentedRoadView → ModelRenderer/HUD/Alert → GuiApplication.render → 屏幕;模型叠加另走 modelV2 + extrinsicsCalibration + radarState → ModelRenderer,并非从当前显示的视频帧反推。这条链不是车辆控制或功能安全的执行链,主循环在顶层 Widget 绘制后才更新 UIState,因此订阅的新消息通常在下一帧绘制,不能仅凭帧率推断相机到显示或故障到告警的端到端时延。实机 GPU、摄像头时序、屏幕亮度和触摸延迟均未测试。

    一帧如何到达屏幕

    GuiApplication.init_window() 创建窗口和可选的离屏 render texture,render() 每帧收集触摸、绘制栈顶 Widget、结束绘制;关屏时仍 yield False,让外层更新状态。AugmentedRoadView._render() 做行车门控、选宽/窄相机、更新标定、scissor 限定内容区,并按相机、模型、HUD、告警、驾驶员状态的顺序绘制。CameraView._render() 用 recv(timeout_ms=0) 非阻塞取最新帧:无新帧沿用旧画面,连接丢失清帧并显示占位,座舱相机水平翻转。断连时 _ensure_connection() 按 0.2 秒间隔重试,_handle_switch() 在目标相机流有帧后才切换并重建纹理。

    组件 / 函数 静态职责
    GuiApplication.init_window() / render() 创建窗口与可选离屏纹理;每帧收集触摸、绘制栈顶 Widget、结束绘制;关屏仍 yield False
    AugmentedRoadView._render() 行车门控、选宽窄相机、更新标定、scissor 限定内容区,按固定顺序绘制
    CameraView._render() 非阻塞取最新帧;无新帧沿用旧画面,断连清帧显示占位;座舱相机水平翻转
    CameraView._ensure_connection() / _handle_switch() 断连按 0.2 秒重试;目标流有帧后切换并重建纹理
    AugmentedRoadView._update_calibration() / _calc_frame_matrix() 接受有效且 calibrated 的外参,按内参/宽窄镜头/消失点算视频变换,并设车体到屏幕矩阵
    ModelRenderer._render() modelV2 与外参需本次行车后至少到达一次;按脏位重算图形
    AlertRenderer.get_alert() 正常显示 selfdriveState 告警;首启等待与状态失联有单独降级提示

    图像缓冲与 GPU 资源

    comma 分支由 CameraView.__init__() 初始化 EGL 和占位纹理。每个 VisionIPC 缓冲的 idx 首次出现时,create_egl_image() 复制 fd,并用 NV12 两平面、stride、UV 偏移属性导入 DMA-BUF;_render_egl() 缓存 idx -> EGLImage,绑定 GL_TEXTURE_EXTERNAL_OES 后由 shader 绘制。切流或关闭时 _clear_textures() 销毁 EGL image,destroy_egl_image() 关闭复制的 fd。这里的「零拷贝」只表示 UI 不执行 CPU 端 YUV 上传,不保证摄像头至显示全链路没有拷贝。

    非 comma 分支由 _initialize_textures() 创建 Y 和 UV 纹理,_render_textures() 仅在帧变化时上传数据,fragment shader 把 YUV 转成 RGB。两个分支都依赖 producer 的 NV12 缓冲尺寸、stride 与生命周期;更换摄像头/ISP、显示驱动或 GPU 时应先验证缓冲复用、纹理方向、色彩和断流恢复。idx 缓存以 _initialize_textures() 为释放边界,非切流的异常重连是否会复用相同 idx 但更换底层 fd,需要实机注入断连测试,不能仅凭源码保证。

    投影、裁剪和告警

    ModelRenderer._update_raw_points() 提取路径、车道线、路缘和加速度,_update_model() 依据前车距离缩短路径,_map_line_to_polygon() 把车体坐标两侧采样点投影成屏幕上的带状多边形,并过滤投影奇点和区域外点。渲染层外侧还有 CLIP_MARGIN=500 的几何筛选与 AugmentedRoadView 的 scissor 裁剪,两者用途不同。shader_polygon.draw_polygon() 把两条边交织成 triangle strip 并懒加载 shader;Gradient 限制最多 20 个色标,但显式 stops 不校验与颜色等长。普通路径颜色跟 longitudinalPlan.allowThrottle 混合,实验模式路径颜色与加速度有关。

    需要明确的是:这些门控只保证某些消息在本次行车后到达过一次,并不构成「摄像头帧与模型预测严格同时间戳」的保证,也没有在 CameraView 每帧旧画面保留时自动隐藏模型。告警应作为独立于装饰性图层的受信任通道验证:摄像头占位、UI 消息断流、绘制遮挡、驾驶员接管提示和语音提示须分别测量。

    大屏与 mici 的实现差异

    mici 的 AugmentedRoadView._render() 为侧栏留宽、添加渐隐纹理、置信球与书签,告警先经 will_render() 决定 HUD 顶部图标和方向盘提示,再绘制告警/HUD;大屏则使用彩色行车边框,叠加顺序不同。mici 的 ModelRenderer._render() 在 disengaged 时隐藏车道/路径,大屏没有这一条件——两者不能视为同一 UI 的简单缩放。

    对比项 大屏 mici
    布局与边框 彩色行车边框 为侧栏留宽、渐隐纹理、置信球与书签
    告警协调 按固定叠加顺序绘制 先经 will_render() 决定 HUD 顶部图标与方向盘提示
    车道/路径显示 无 disengaged 隐藏条件 disengaged 时隐藏车道与路径
    离路切换 重新 onroad 且曾有帧时重建客户端 排空现有连接的队列并清帧
    着色器 基础 YUV 转换 另有 engaged 去饱和/对比度与座舱增强

    过渡逻辑也不同:mici 的 CameraView._offroad_transition() 排空现有连接的队列并清帧,大屏的 _offroad_transition() 在重新 onroad 且曾有帧时重建客户端;迁移时要为两种屏幕分别验证切换时不会闪出上一行程图像。mici 的着色器另有 engaged 去饱和/对比度和座舱增强。

    自制硬件与插件化结论

    硬件原型至少需要相机同步/时间戳、可稳定复用的 NV12 缓冲和 DMA-BUF/EGL 支持或经验证的纹理上传替代、可见且可测的显示路径、可靠触摸/背光/屏幕唤醒,以及独立于 UI 的车辆安全网关。台架验收项可列为:相机断连/重启/切宽窄流、GPU 上下文丢失、屏幕休眠唤醒、模型/状态消息失联、告警抢占、长时间热降频,并测量真实最坏时延。

    「万物即插件」适合导航、可视化指标和非关键叠加;摄像头底图生命周期、标定/投影契约、接管告警与失联降级、驾驶员状态显示应由受信任宿主掌控。插件只读受版本约束的消息快照,通过宿主分配的低优先级绘图区域输出,不能遮挡或替代核心告警,更不能发布 selfdriveState 或触发车辆控制。

    常见问题

    「零拷贝」是否意味着摄像头到屏幕全链路都没有拷贝?

    不是。源文档明确这里的「零拷贝」只表示 UI 不执行 CPU 端 YUV 上传,不保证摄像头至显示全链路没有拷贝。comma 分支用 DMA-BUF 导入 EGLImage,非 comma 分支则在帧变化时上传 Y/UV 纹理,两条路径的拷贝点不同。

    模型叠加会不会因为显示旧画面而与视频帧错位?

    存在这种可能。CameraView._render() 无新帧时沿用旧画面,而源码没有在每帧旧画面保留时自动隐藏模型;门控只保证 modelV2 与外参在本次行车后至少到达一次,并不构成摄像头帧与模型预测严格同时间戳的保证。

    大屏和 mici 两套界面能不能当作同一 UI 的缩放?

    不能。mici 为侧栏留宽、加渐隐纹理与置信球,告警先经 will_render() 协调 HUD,且 ModelRenderer 在 disengaged 时隐藏车道/路径;大屏使用彩色行车边框、叠加顺序不同,也没有 disengaged 隐藏条件。

    第三方插件能否自行绘制告警或相机底图?

    不建议,也不符合源文档的边界。摄像头底图生命周期、标定/投影契约、接管告警与失联降级、驾驶员状态显示应由受信任宿主掌控;插件只读受版本约束的消息快照,在宿主分配的低优先级区域输出,不能遮挡核心告警或发布 selfdriveState。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/ONROAD_GPU_RENDER_DEEP_DIVE.md(取用了帧循环、图像缓冲与 GPU 资源、投影裁剪与告警、大屏与 mici 差异及硬件插件边界)。
    • 项目 02 —— openpilot-research/UI_SHADER_POLYGON_GRADIENT_CONTRACT_DEEP_DIVE.md(仅用于确认 shader_polygon 与 Gradient 的边界)。

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

    更新记录

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

  • common.utils 契约:atomic_write、retry 与内存峰值

    结论:openpilot.common.utils 里的几个名字听起来很稳妥的辅助函数,其保证范围都比名字窄:atomic_write() 的「原子」只是同一文件系统内已完成写入的可见性切换,它不执行 fsync,因此不能承诺断电持久化,overwrite=False 也只在建临时文件前检查一次;get_upload_stream() 注释称实时压缩,实现却把压缩结果完整写入 BytesIO 再上传,峰值内存随压缩后大小增长;retry() 每次失败都睡 delay,最后一次失败后也睡,且抛出的是新异常、原始错误既不是抛出类型也不在 __context__ 中。以下为固定源码快照 c8fb906 并隔离运行 atomic_write() 的静态分析,未执行设备写入、上传、联网或外部进程。

    函数与调用方索引

    函数/类 关键行为 主要调用方
    Timer time.monotonic() 记录起点和 lap,fmt() 以给定媒体时长计算倍速 导出性能统计
    MovingAverage 长度为 window_size 的环形缓冲,部分窗口按当前 count 求均值;未采样返回 NaN Ratekeeper 循环间隔、FrequencyTracker 消息频率
    atomic_write() 在目标目录建立临时文件;上下文正常退出后 os.replace;异常时删除临时文件 令牌持久化、URL 文件、manager 配置
    get_upload_stream() 原文件返回文件流及字节数;压缩时把 zstd 完整结果写入 BytesIO 后返回流和压缩长度 uploader.do_upload()、athenad._do_upload()
    CallbackReader 委托底层对象,read() 后累计字节并回调 athenad 上传进度
    retry() 捕获 Exception,每次失败后睡眠并重试;耗尽后抛新异常或按设置返回 None qcomgpsd.at_cmd()、micd、soundd
    managed_proc() 上下文退出时 terminate()、最多等 5 秒、超时 kill() 当前仅见定义,未见调用
    sudo_write()、sudo_read() 设备文件读写;写失败时尝试提权命令,读错误返回空串 硬件 IRQ/CPU/GPU 配置、sensord IRQ 配置

    准确的失败与资源语义

    atomic_write() 的「原子」范围是同一文件系统中已完成写入的可见性切换:关闭临时文件之后才 os.replace,读者不会看到半写的目标文件。它没有对文件和目录执行 fsync,因此不能单凭这个接口承诺断电持久化。overwrite=False 只在建临时文件前用 os.path.exists 检查一次;并发创建目标文件时,后续 os.replace 仍可能覆盖它。上游 test_atomic_write() 只覆盖正常写入后目标文件可读;三项隔离探针进一步确认写入期间目标不可见、异常保留旧值并清理临时文件,以及默认 overwrite=False 在进入上下文后仍会覆盖并发创建的目标。

    get_upload_stream() 实现把压缩结果完整写入 io.BytesIO 然后才发起网络上传,对大日志峰值内存随压缩后大小增长;读取源文件失败或压缩失败会在上传前发生。调用方在 finally 关闭返回流;CallbackReader.read() 报告的是请求库从流中读取的字节数,并非远端已确认接收的字节数。athenad 使用返回长度构造 Content-Length,而 uploader 当前不使用这个长度。

    retry() 每次失败都睡 delay,最后一次失败后也睡;调用方最终看到通用 Exception。新异常在 except 块之外抛出,原始错误既不是抛出类型,也不在 __context__ 中。attempts=0 时循环不执行,行为是立即抛新异常或返回 None;ignore_failure=True 也会返回 None。这些语义对硬件初始化和 AT 命令尤其重要:重试五次、每次内部串口超时五秒,并不是「五秒的总 deadline」。

    接口 真正的保证 容易误解的地方
    atomic_write() 同文件系统内可见性切换 不 fsync,不承诺断电持久化;overwrite=False 只检查一次。
    get_upload_stream() 返回文件流或整块压缩结果流 压缩结果先进 BytesIO,峰值内存随压缩后大小增长。
    CallbackReader.read() 请求库已读字节数 不是远端已确认接收的字节数。
    retry() 按次数重试并睡眠 最后一次也睡;原始异常既非抛出类型也不在 __context__。
    MovingAverage 固定容量环形均值 构造不校验零窗口,第一次 add_value() 就失败。
    tabulate(html) 拼接单元格字符串 没有 HTML 转义,不能当安全 HTML 展示。

    嵌套重试与副作用放大

    嵌套使用会放大副作用:try_setup_logs() 最多执行十次,外层 setup_quectel() 最多执行五轮。两项假 DIAG 组合探针确认持续失败时共调用 50 次日志配置、睡眠 55 次,并重复五轮前置 NV 写/读请求;这是软件调用次数,不证明 NV 状态改变,也不包含真实 I/O 耗时。

    MovingAverage 要求正的窗口大小,构造时不校验,零窗口在第一次 add_value() 访问空缓冲时就失败。tabulate(tablefmt="html") 直接拼接单元格字符串,没有 HTML 转义,因此不能把不可信插件文本直接当作安全 HTML 展示。sudo_write() 的提权回退把值和路径拼接进 shell 命令,当前调用主要是平台常量和已发现的 IRQ 编号,但该函数不应成为接受插件任意路径/值的通用能力;sudo_read() 吞掉所有异常并返回空串,调用方应区分「空内容」和「读失败」。

    硬件与插件化落点

    源文档给出的是一组建议而非已实现能力:将特权文件操作封装为受信任平台服务,明确允许的设备节点、参数范围、审计记录和失败返回,第三方插件不直接调用 sudo_write();对配置文件分别规定可见性、并发覆盖、权限、断电耐久性与密钥保密要求,atomic_write() 只解决其中的可见性问题;上传插件按最大文件和压缩输出设内存预算,使用可测的流式压缩/限额方案,回调进度与服务端确认分开表示;重试契约显式声明可重试异常、退避、总 deadline、取消信号与最终原始故障,硬件命令还需考虑幂等性。

    本文没有运行硬件、网络或断电试验。atomic_write() 的上述文件状态已用隔离探针验证,其余故障推论来自当前实现;调用方及平台行为仍需继续审计。

    常见问题

    atomic_write() 能保证断电后不丢数据吗?

    不能。它只在同文件系统内切换可见性,关闭临时文件后才 os.replace,没有对文件和目录执行 fsync,因此不能单凭这个接口承诺断电持久化。

    overwrite=False 能防止并发覆盖吗?

    不能。它只在建临时文件前用 os.path.exists 检查一次;并发创建目标文件时,后续 os.replace 仍可能覆盖它。探针已确认进入上下文后仍会覆盖并发创建的目标。

    retry() 五次重试总耗时是固定的吗?

    不是。每次失败都睡 delay,最后一次失败后也睡;若每次内部串口超时五秒,五次重试并不是「五秒的总 deadline」,而是显著更长。

    上传进度回调代表服务端已收到吗?

    不代表。CallbackReader.read() 报告的是请求库从流中读取的字节数,并非远端已确认接收的字节数;进度与服务端确认应分开表示。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/COMMON_UTILS_IO_RETRY_RUNTIME_DEEP_DIVE.md(函数索引、atomic_write 探针、retry 语义与嵌套重试)。
    • 项目 02 —— openpilot-research/ 同目录上传队列与硬件配置专题(作为相邻边界参考)。

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

    更新记录

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

  • 车载 UI 控件:可点击不等于授权,滑动确认不重检车况

    结论:界面控件「可点击、可见、已滑动确认」都不是车辆控制授权,也不构成功能安全保证。Toggle 先翻转内存状态再调用回调,set_state() 甚至不触发业务回调;MultiOptionDialog 的「显示层选择」与「持久化」是两步;滑动确认只减少偶发触碰,并不自动重检当前车辆状态。本文为固定源码快照的静态阅读,失败模式是源码边界推断,不是实车复现。

    基础输入与弹窗

    Button 复用基础 Widget 的点击回调和命中规则,_update_state() 根据按压与禁用选择背景/文字颜色,_render() 绘制按钮及文字。ButtonRadio 在释放后切换自身选中状态;IconButton 用纹理和透明度滤波表示状态。视觉上的 DANGER 风格只改变样式,不增加确认门槛。

    控件 关键行为 边界
    Toggle _handle_mouse_release() 先翻转内存状态、再调用可选回调 set_state() 不调用业务回调,也不代表 Params 已更新
    ConfirmDialog 确认或取消时先 pop_widget(),再以 DialogResult 调业务回调 接受 Enter/Escape 物理键;若当作不可绕过安全门需另作状态约束
    单按钮 alert_dialog() 隐藏取消按钮 底层仍支持 Escape 取消
    MultiOptionDialog 先在列表中暂存 selection,仅当当前选项与原值不同才启用 Select _set_result() 出栈后由调用方决定是否写参数;显示与持久化是两步

    两种屏幕的设置写入路径

    大屏与 mici 不是仅换了皮肤的同一套业务状态机,新增设置时必须逐屏追踪写入和刷新路径。

    路径/关键函数 参数与门控
    TogglesLayout._update_toggles() 大屏按 CarParams 的纵向控制能力显示/禁用实验模式和驾驶风格;从 Params 重新同步开关;需重启的开关在 engaged 时禁用
    TogglesLayout._handle_experimental_mode_toggle() 首次开启实验模式弹出富文本确认;确认后写 ExperimentalMode 与 ExperimentalModeConfirmed,取消时回滚 UI 开关
    TogglesLayout._toggle_callback() 非实验模式开关同步写布尔参数;需重启的选项追加 OnroadCycleRequested
    TogglesLayoutMici._on_experimental_mode() 首次开启跳到确认页,取消时不写开启参数;完成滑动确认后才写入确认与模式参数
    BigParamControl._handle_mouse_release() 先执行 BigToggle 的状态翻转/可选回调,再写对应 Params;restart_needed_callback() 只请求行车周期重启

    mici 多选项控件的回调与参数顺序

    BigMultiToggle._handle_mouse_release() 先调用 BigToggle 父类:最底层 Widget._handle_mouse_release() 执行可选通用点击回调,然后 BigToggle 翻转 _checked 并执行 toggle_callback(布尔值);返回后才用 options.index(value) 循环到下一选项,执行 select_callback(新字符串)。BigMultiParamToggle._handle_mouse_release() 再把新选项索引写入 Params。因此若调用方同时注册这些回调,观察顺序是点击 → 布尔翻转 → 选项变化 → 参数写入,前面的回调不能假定持久参数已更新。父类 BigToggle 的 _checked 在 BigMultiToggle._draw_content() 中不参与选项 pill 绘制,布尔回调值与屏幕当前选项语义无关。

    BigMultiParamToggle._load_value() 用 Params.get(param) or 0 直接索引选项列表。LongitudinalPersonality 在参数 schema 中是整数,log.capnp 枚举的 aggressive/standard/relaxed 顺序与当前 UI 选项一致。缺参回到 0;越界正值会在构造时抛 IndexError,负值按 Python 负索引显示非预期选项。本类不校验外部修改后的参数范围,也没有在写入失败时回滚已轮转的画面值。

    插件化时多选设置应显式建模为一个枚举状态和一次提交结果,不把内部隐藏布尔态暴露为第二个业务状态;宿主应在显示与提交两处检查选项范围、车辆状态和参数写入回执。硬件台架要测物理/触摸重复释放、滚动撤销、外部方向盘按钮并发改 personality、畸形持久值及写入失败后的 UI 一致性。

    滑动确认如何生效

    SliderBase._handle_mouse_event() 只在圆形手柄区域开始拖动,并根据释放时的位置设置已确认时间;_update_state() 等手柄动画到终点且至少经过 0.2 秒后仅调用一次确认回调。LargerSlider 用于 mici 安装流程,BigSlider/RedBigSlider 用于 BigConfirmationDialog,后者可在确认后先动画退出,再调用业务动作。滑动减少偶发触碰,但并不自动重检当前车辆状态。

    还需注意释放路径的守卫条件:基础 Widget._process_mouse_events() 在收到 release 时先调用子类 _handle_mouse_event(),再检查此前是否按下并在命中区内;而 SliderBase._handle_mouse_event() 的确认判断只看手柄位置,未显式要求本次仍在拖动手柄。若 UI 帧停顿、手势取消或其他释放事件与残留位置交错,需验证不会误确认——这是静态推断的手势竞争测试点,不是实测误触结论。

    硬件与插件化边界

    硬件验收应覆盖触摸抖动、按下后移出、长按、快速滑动、休眠首触、物理键 Enter/Escape、页面弹出时触点未释放,以及确认期间 engaged/车型能力变化。控制类写入应在最终业务回调再次核对当前状态,不能只靠按钮 enabled、警示颜色或滑块门槛。插件可提供非关键 UI 控件和设置说明,但宿主须掌握关键参数允许列表、写入权限、确认流程及行车冻结规则;涉及 OpenpilotEnabledToggle、实验模式或硬件/网络状态的写入不得由任意插件直接提交。

    常见问题

    把开关拨到「开」就代表参数已写入吗?

    不一定。Toggle._handle_mouse_release() 先翻转内存状态再调用可选回调,set_state() 甚至不调用业务回调;显示层的翻转与 Params 持久化是不同的两步。

    滑动确认能替代对车辆状态的检查吗?

    不能。滑动只在手柄到达终点且至少经过 0.2 秒后调用一次确认回调,用于减少偶发触碰,但不会自动重检当前车辆状态;控制类写入仍需在最终业务回调再次核对。

    mici 多选项控件的回调顺序是什么?

    若调用方同时注册回调,观察顺序是点击 → 布尔翻转 → 选项变化 → 参数写入,前面的回调不能假定持久参数已更新,因为参数写入发生在最后一步。

    为什么负值的持久化参数会显示意外选项?

    BigMultiParamToggle._load_value() 用 Params.get(param) or 0 直接索引选项列表,负值会按 Python 负索引取到非预期选项,越界正值则在构造时抛 IndexError,本类不校验外部修改后的参数范围。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/UI_CONTROLS_CONFIRMATION_DEEP_DIVE.md(取用了基础输入与弹窗、两屏设置写入路径、mici 多选项控件、滑动确认与硬件/插件边界)。
    • 项目 02 —— openpilot-research/DYNAMIC_ALERT_CALLBACKS_DEEP_DIVE.md(仅用于确认「可见状态不等于授权」的相邻表述)。

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

    更新记录

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

  • WebRTC 直播码率与关键帧:0.5/1.5/5 Mbps 三档

    结论:直播码率自适应是三档电平控制:默认 0.5/1.5/5 Mbps,每 0.2 秒采样 RTCP 接收报告,丢包率 ≥10% 立即降一档、≥5% 累积五次降档,零丢包累积五次升档且降档后升档等待次数翻倍。关键帧请求与码率都是全局 Params 电平,不含相机 ID、请求 ID 或逐路确认,多路会话共享同一请求位。时间维度上,WebRTC PTS 取的是发送时的 monotonic_ns 换算为 90 kHz,并不等于相机采样的 SOF/EOF。以下为固定源码快照 c8fb906 及固定版本 teleoprtc 归档、并对原码率类与视频轨道做隔离探针的静态分析,未运行真实相机、编码器或网络丢包测试。

    关键函数与跨进程调用

    入口 源码行为 边界
    LivestreamBitrateController.__init__()、run() 默认三档 0.5/1.5/5 Mbps(最高档可由 STREAM_BITRATE 覆盖);初始化就把最高档写 LivestreamEncoderBitrate。每 0.2 秒按接收报告调档。 只采样 get_stats() 字典的第一份报告,并非多轨道最坏/加权网络状态;未观察或报告完全不变则保持当前控制状态。
    _sample()、set_quality() fraction_lost / 256 转换丢包比例;手动 high/med/low 直接写 Params 并停止自动控制,auto 恢复自动。 手动模式不更新内部 level、counter、up_samples;回到 auto 后首次调档仍以先前自动状态为基准。代码未核验编码器是否实际应用码率。
    StreamSession.run()、message_handler() 会话开始置 LivestreamRequestKeyframe=True;数据通道可切相机、调质量、暂停/恢复视频、启用计时 SEI。 livestreamCameraSwitch 直接更换唯一轨道的订阅源,没有重置轨道 _seen_keyframe 或显式新关键帧请求。
    LiveStreamVideoStreamTrack.recv()、switch_camera() 订阅主题采用 conflate=True;首次看到关键帧标记 _seen_keyframe=True 并清全局请求,但收到非空消息都会返回视频包。PTS 用发送时 monotonic_ns 换算为 90 kHz。 _seen_keyframe 是请求清除门,不是发帧门;入流可能先送非关键帧。多轨道共用一个关键帧 Params 位。
    _build_frame_data() 普通模式拼接 EncodeData.header + data;可选计时 SEI 额外放入 SOF→EOF、EOF→日志、日志→本机当前时间三段毫秒延迟及 wall-clock 毫秒。 SEI 里同时存在单调时间差与墙钟绝对时间;跨时钟域比较必须说明校时、漂移和跳变。

    码率状态机与探针结论

    run() 只有丢包率 ≥5% 或 ≤0% 两类调档分支;0% 到 5% 间的正丢包率不清零 counter。因此「连续五次」更准确地说是相关分支累计五次采样,不保证中间完全没有低丢包样本。_sample() 只把整个报告元组相同视为没有新样本,不能证明报告对应新的媒体时间窗口。

    五项原 LivestreamBitrateController 类隔离探针核对了:10% 报告立即降档并把升档窗口从 5 翻倍到 10;5% 报告中穿插 1% 样本仍累计到五次降档;降档后需十个零丢包样本才升档;手动 low 切回 auto 时内部仍在 high,后续零丢包不回写 high,Params 继续留在手动 low;完全相同的报告元组不会重复采样。这些探针用人工 RTCP 报告和 Params 记录器执行原类方法,不验证真实 WebRTC 统计刷新、编码器应用或网络体验。

    关键帧请求与轨道行为

    RTCP PLI 调轨道 request_keyframe(),写全局 Params;每个直播编码器每帧读取码率/关键帧参数再编码。Params 是全局电平触发控制,不含相机 ID、请求 ID、过期时刻或逐路 ACK;软件编码后端只记录不支持设置码率/请求关键帧。多路会话共享 LivestreamRequestKeyframe 和 LivestreamEncoderBitrate,而各轨道各自处理媒体,一个轨道清除全局关键帧请求可能使另一个仍未见关键帧的轨道失去持续请求。

    LiveStreamVideoStreamTrack.enable(False) 重置 _seen_keyframe,但 switch_camera() 不重置它。即使 _seen_keyframe=False,recv() 也不会阻挡非关键帧,只等待有消息;消费者能否解码取决于编码器/封装及新关键帧到达,不能由该标志保证。五项轨道与时间探针确认:订阅采用 conflate=True、切相机仅替换 socket、首次关键帧前的非关键帧仍返回、PTS 由本机取帧时单调时钟换算为 90 kHz、首次关键帧清全局请求。探针使用假基类、假帧与假时钟,不证明真实编码包可解码、相机切换连续或跨设备时钟已对齐。

    帧时间与 SEI 字段

    PTS 使用取帧时间计算,不使用 EncodeData.idx.timestampSof/Eof;网络等待、conflate 丢旧包或暂停/恢复都可能让媒体时间轴与相机采样时间轴不同。计时 SEI 中三个毫秒差依次为 SOF→EOF、EOF→日志、日志→读取时刻,第四项为墙钟毫秒。插件层需同时保留采样、编码、传输与显示时间。现有 WebRTC 会话测试检查单轨道 recv() 的 PTS 单调趋势与空包字节,并测试消息代理/非 JSON 请求;未见码率状态机、多轨道关键帧确认、切相机或 SEI 时间字段的原套件专门回归。

    时间字段 来源 不能代替什么
    WebRTC PTS 发送时 monotonic_ns 换算为 90 kHz 相机采样 SOF/EOF;反映的是取帧/发送节奏。
    SEI SOF→EOF 编码内部两段单调时间差 完整采样时间消息契约。
    SEI 日志→本机当前时间 单调时间差 跨设备已校时;仍需说明漂移与跳变。
    SEI 第四项 wall-clock 绝对毫秒 可直接与其他单调时钟字段比较。

    新硬件与插件接口验收

    编码插件至少应声明 codec/profile、每路输入时钟、最大分辨率/码率、关键帧间隔、可变码率是否受支持及实际应用后的回执;关键帧请求用 camera_id + request_id + deadline 逐路确认已输出可解码关键帧。码率控制器应以每条流的实际 RTCP 统计及编码器应用回执为依据,手动/自动切换时同步内部档位并明确计数器重置规则,无报告、报告停滞与网络失联要分开。回放/台架应覆盖多路同时启动、一条流无关键帧、相机切换、暂停恢复、PLI 连发、无接收报告、0/中/高丢包、软件编码后端不支持动态码率,以及 wall clock 跳变时 SEI 的解释。

    常见问题

    WebRTC 直播码率有哪几档,怎么切换?

    默认三档 0.5/1.5/5 Mbps,最高档可由 STREAM_BITRATE 覆盖。控制器每 0.2 秒按 RTCP 接收报告调档:丢包率 ≥10% 立即降一档、≥5% 累积五次降档,零丢包累积五次升档且降档后升档等待次数翻倍。

    关键帧请求是逐路确认的吗?

    不是。关键帧请求与码率都是全局 Params 电平,不含相机 ID、请求 ID 或逐路 ACK;多路会话共享同一个请求位,一个轨道清除全局请求可能使另一个仍未见关键帧的轨道失去持续请求。

    切相机会自动请求新关键帧吗?

    不会。switch_camera() 只更换唯一轨道的订阅源,不重置 _seen_keyframe,也没有显式新关键帧请求;而且即使 _seen_keyframe=False,recv() 也不会阻挡非关键帧。成功切换后的解码连续性需实测。

    WebRTC 的 PTS 就是相机采样时间吗?

    不是。PTS 用发送时的 monotonic_ns 换算为 90 kHz,不使用 EncodeData.idx.timestampSof/Eof;网络等待、conflate 丢旧包或暂停恢复都可能让媒体时间轴与采样时间轴不同。

    延伸阅读

    参考资料

    • 项目 02 —— openpilot-research/WEBRTC_BITRATE_KEYFRAME_TIMING_DEEP_DIVE.md(码率状态机、关键帧请求、PTS/SEI 时间字段与探针结论)。
    • 项目 02 —— openpilot-research/ 同目录远程 RPC 与视频编码专题(会话边界与编码路径作为相邻边界参考)。

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

    更新记录

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