结论:同一帧里可以同时成立多个故障事件,但最终屏幕文字、声音与 HUD 图像只会呈现一条告警。selfdrived 用「事件容器(有序但不去重)→ 状态机(按类型优先级)→ AlertManager(按 alert_type 每帧留一条)」这三段流水线完成仲裁,任何一段的边界都会让结果和直觉不同。本文为固定源码快照 c8fb906 的静态分析,不等于独立硬件安全保证。
从车辆状态到消息输出
SelfdriveD.__init__() 等待或接收 CarParams,创建车辆事件适配、消息订阅、告警管理器和状态机。carState 用独立 socket 接收,以它作为步进输入;data_sample() 再非阻塞更新其余主题。初始化须满足 CS.canValid && sm.all_checks(),或约 6 秒超时进入初始化完成路径;这不是「超时后所有输入都变有效」,后续仍会创建故障事件。
step() 每次先采样、聚合事件;只有 CP.passive=False 且完成初始化才调用状态机;随后更新告警并发布消息。params_thread() 约每 100 毫秒刷新单位、车道偏离、油门踏板退出、实验模式和驾驶风格。异步参数刷新与控制步进并行,不能假设 UI 写完 Params 后下一条 selfdriveState 已同步使用全部设置。
publish_selfdriveState() 将 enabled、active、状态、engageable、告警文案/声音/HUD 图像写入 selfdriveState;engageable 的条件是当前没有 NO_ENTRY 事件,并不等于当前已使能。onroadEvents 每约 1 秒或事件名单变化时发布。controlsd 用 enabled/active 再结合车辆故障、静止门和纵向覆盖决定 latActive/longActive;pandad 还要求 selfdriveState 消息 alive/valid 才把 engaged 告知 panda。软件状态机、控制器活动门和 panda 固件安全门是三个不同层次。
事件容器为什么不去重
Events 保存当前帧有序事件、静态事件以及每个 EventName 的连续出现计数。clear() 先更新计数再仅保留静态事件,随后 SelfdriveD.update_events() 按来源追加动态事件。contains(ET) 查询事件的类型映射;create_alerts() 只为本状态允许的类型创建 Alert,并用 creation_delay 抑制短暂出现的告警。
add() / add_from_msg() 用 bisect.insort 排序,但不去重;重复事件会产生重复的 create_alerts() 结果,计数器仍按「该事件名是否出现」每帧只加一次。static=True 会同时加入静态列表和当前列表,连续 clear() 会保留静态项并继续累计它的计数。创建延迟按 DT_CTRL * (之前连续帧数 + 1) 判断当前帧,等号也允许产生告警;回调在延迟判断前调用,异常直接向调用方传播。
生产路径并非完全没有重复机会:bigModelFailed 可由失败边沿与活动模型故障段同帧添加,canError 可由雷达错误与车辆 CAN 无效同帧添加;这四项分支由隔离探针确认,但不证明实车同帧共现。同一 EventName 还可以有多个类型,例如 controlsMismatch 同时映射 IMMEDIATE_DISABLE 与 NO_ENTRY,cameraMalfunction 有 PERMANENT/SOFT_DISABLE/NO_ENTRY;哪个 Alert 真正参与显示取决于状态机当前的 current_alert_types。
| 事件类型 | 状态意义 | 典型例子 |
|---|---|---|
ENABLE / NO_ENTRY |
启用请求 / 禁止进入 | 按键或 PCM 启用;系统初始化、车型能力、故障。 |
PRE_ENABLE |
进入预使能等待 | 需要条件继续满足才进入 enabled。 |
OVERRIDE_LATERAL / OVERRIDE_LONGITUDINAL |
驾驶员覆盖某执行维度 | overriding 状态。 |
SOFT_DISABLE |
给驾驶员接管窗口 | 摄像头故障、进程失联、通信问题。 |
IMMEDIATE_DISABLE / USER_DISABLE |
立即退出 / 用户主动退出 | 安全模式不匹配、CAN 总线丢失、踏板/取消。 |
WARNING / PERMANENT |
活动状态展示 / 始终候选 | 转向临时不可用、FCW/LDW、启动与硬件提示。 |
状态机的优先级
StateMachine.update() 每步先递减软退出计时器,并始终加入 PERMANENT 告警类型。非 disabled 状态下,IMMEDIATE_DISABLE 优先于 USER_DISABLE,两者优先于软退出或覆盖。disabled 只有在出现 ENABLE 且没有 NO_ENTRY 时才能进入 preEnabled、overriding 或 enabled。ENABLED_STATES 包含 preEnabled、enabled、softDisabling、overriding;ACTIVE_STATES 不含 preEnabled,所以「enabled」与「执行器正在活动」并非同义词。
| 当前状态 | 条件 | 结果 |
|---|---|---|
enabled |
SOFT_DISABLE |
进入 softDisabling,计时约 3 秒。 |
enabled |
横/纵覆盖 | 进入 overriding,继续 enabled=True, active=True。 |
softDisabling |
故障消失 | 回 enabled;若持续至计时结束则转 disabled。 |
preEnabled |
PRE_ENABLE 消失 |
转 enabled;否则停留预使能。 |
overriding |
覆盖消失 | 回 enabled;若出现软退出则转 softDisabling。 |
| 任意非 disabled | IMMEDIATE_DISABLE 或 USER_DISABLE |
直接变成 disabled。 |
SOFT_DISABLE_TIME 是按 DT_CTRL 换算的步数,取决于循环是否按预期执行;最坏墙上时间和驾驶员实际听到提醒的延迟需要实测。源码还有两个易误判的边界:处于 disabled 且同帧有 ENABLE + IMMEDIATE_DISABLE、却没有 NO_ENTRY 时,代码只走 disabled 分支、结果进入 enabled;处于 preEnabled 时若 PRE_ENABLE 消失但 SOFT_DISABLE 仍在,会先进入 enabled 且返回 active=True,下一次调用才进入 softDisabling。
告警仲裁:一帧只留一条
AlertEntry.active() / just_added() 用包含端点的 frame <= end_frame 判定有效;只有当前帧紧挨上次加入帧且原条目仍有效才保留旧 start_frame。AlertManager.add_many() 以 alert_type 为键保留告警:每次覆盖该槽的负载和 added_frame,结束帧取 max(frame + 1, start_frame + duration),duration 单位是控制帧而非墙上秒。间隔一帧以上的重加即使旧告警还有效,也会重置 start_frame。
process_alerts() 先把匹配 clear_event_types 的条目 end_frame 设为 -1,再从仍有效的条目按 (priority, start_frame) 最大值选择。完全同分时使用严格 >,字典中先插入的 alert_type 胜出;同一键的不同负载则由最后一次 add_many() 覆盖,并不产生第二个候选。SelfdriveD.update_alerts() 在不活动时清理 WARNING,已使能时清理 NO_ENTRY。因此同一帧可有多个事件,但只有一个当前显示/声音告警。set_offroad_alert() 另把驻车提示写入 Params,属于另一呈现通道,不参加本次 onroad AlertManager 排序。
常见问题
同一帧有多个故障,屏幕上为什么只显示一条告警?
因为 process_alerts() 只从有效条目里按 (priority, start_frame) 最大值选一条;完全同分时用严格 >,字典中先插入的 alert_type 胜出。事件列表可以有多条,最终呈现的告警只有一条。
enabled=True 是不是就代表执行器已经动了?
不是。ENABLED_STATES 含 preEnabled,而 ACTIVE_STATES 不含它;enabled 只表示状态机处于使能族。controlsd 还要结合车辆故障、静止门和纵向覆盖决定 latActive/longActive,pandad 另要求 selfdriveState 消息 alive/valid。
重复添加的事件会被自动去重吗?
不会。add() / add_from_msg() 用 bisect.insort 排序但不去重,重复事件会产生重复的 create_alerts() 结果,计数仍按「该事件名是否出现」每帧只加一次。插件接入不应依赖容器自动去重。
软退出(soft disable)给的 3 秒是按墙钟算的吗?
不是。SOFT_DISABLE_TIME 是按 DT_CTRL 换算的步数,取决于循环是否按预期执行;最坏墙上时间和驾驶员实际听到提醒的延迟需要实测。
延伸阅读
- 从使能到车轮:执行链上的八道门,以及它们各自不保证什么 —— 事件状态机之后还有哪些门。
- selfdrived 的事件生产者:53 个直接条件怎么落成一条告警 —— 事件是从哪里被加进容器的。
- 告警声音是怎么发出来的:从 Alert 到扬声器的链路 —— 仲裁结果如何变成可听见的提醒。
- 车载信号为什么会说谎:TTL、源时间与 500 ms 新鲜度阈值 —— 事件数据新鲜度为什么不能只看当前值。
参考资料
- 项目 02 ——
openpilot-research/SELFDRIVE_EVENTS_STATE_ALERTS_DEEP_DIVE.md(事件容器、状态机与 AlertManager 的三段仲裁机制与故障边界)。 - 项目 02 —— 同目录探针记录:五项 Events 容器、五项状态机转移、五项 AlertManager 仲裁隔离探针结论。
说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。
更新记录
首次发布 2026-10-11 最近核对:2026-10-11