结论:界面控件「可点击、可见、已滑动确认」都不是车辆控制授权,也不构成功能安全保证。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,本类不校验外部修改后的参数范围。
延伸阅读
- 一套令牌覆盖 5 种分辨率:车载 HUD 从 456×280 到 1280×720 的适配工程 —— 控件与布局在不同分辨率下的适配视角。
- 52 张预览自动校验:五屏 HUD 怎么把「文字出界」变成一条断言 —— 把 UI 边界做成可自动验证的断言。
- 赛道仪表的信息架构:从圈速到告警的设计顺序 —— 控件与信息层级的整体设计顺序。
- selfdrived 动态告警:49 个槽位与软退出的真实边界 —— 另一处「可见状态不等于安全状态」的例子。
- 进程生命周期、通信与设备服务 —— 说明设置写入依赖的
Params生命周期与清理规则。
参考资料
- 项目 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
发表回复