From 4a13639cfd122ccb9113a4d6ce225dcbd8e61914 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Wed, 26 Aug 2026 19:12:49 -0700 Subject: [PATCH] reduce chestnut states (#38705) ui: unify chestnut status presentation --- .../assets/icons_mici/chestnut_crossed.png | 3 -- .../assets/icons_mici/chestnut_gray.png | 3 -- openpilot/selfdrive/ui/mici/layouts/home.py | 12 +++--- .../selfdrive/ui/mici/onroad/hud_renderer.py | 37 ++++++------------ openpilot/selfdrive/ui/ui_state.py | 38 +++++++++++++++++-- 5 files changed, 53 insertions(+), 40 deletions(-) delete mode 100644 openpilot/selfdrive/assets/icons_mici/chestnut_crossed.png delete mode 100644 openpilot/selfdrive/assets/icons_mici/chestnut_gray.png diff --git a/openpilot/selfdrive/assets/icons_mici/chestnut_crossed.png b/openpilot/selfdrive/assets/icons_mici/chestnut_crossed.png deleted file mode 100644 index 4fc5decb5..000000000 --- a/openpilot/selfdrive/assets/icons_mici/chestnut_crossed.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8a8c5fece2a1c7587feb41cbe04c6aee08e768ecd9b5d00da6af9832a4ccc842 -size 2034 diff --git a/openpilot/selfdrive/assets/icons_mici/chestnut_gray.png b/openpilot/selfdrive/assets/icons_mici/chestnut_gray.png deleted file mode 100644 index a6aeb8468..000000000 --- a/openpilot/selfdrive/assets/icons_mici/chestnut_gray.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7409c53d7c72681c24982fd83b56ce70f80797c9c0f936d9296a5c18557ac472 -size 7279 diff --git a/openpilot/selfdrive/ui/mici/layouts/home.py b/openpilot/selfdrive/ui/mici/layouts/home.py index 50dc95901..519580925 100644 --- a/openpilot/selfdrive/ui/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/mici/layouts/home.py @@ -9,7 +9,7 @@ from openpilot.system.ui.widgets.layouts import HBoxLayout from openpilot.system.ui.widgets.icon_widget import IconWidget from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos -from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState from openpilot.common.version import RELEASE_BRANCHES HEAD_BUTTON_FONT_SIZE = 40 @@ -139,8 +139,8 @@ class MiciHomeLayout(Widget): self._version_text = self._get_version_text() self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48)) - self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (50, 37)) - self._chestnut_icon_gray = IconWidget("icons_mici/chestnut_gray.png", (50, 37)) + self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (68, 40)) + self._chestnut_failed_icon = IconWidget("icons_mici/chestnut_orange.png", (68, 40)) self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46)) self._body_icon = IconWidget("icons_mici/body.png", (54, 37)) @@ -151,7 +151,7 @@ class MiciHomeLayout(Widget): NetworkIcon(), self._experimental_icon, self._chestnut_icon, - self._chestnut_icon_gray, + self._chestnut_failed_icon, self._body_icon, self._mic_icon, ], spacing=18) @@ -248,8 +248,8 @@ class MiciHomeLayout(Widget): # ***** Center-aligned bottom section icons ***** self._experimental_icon.set_visible(ui_state.experimental_mode) - self._chestnut_icon.set_visible(ui_state.sm["deviceState"].chestnutPresent and ui_state.chestnut_compiled) - self._chestnut_icon_gray.set_visible(ui_state.sm["deviceState"].chestnutPresent and not ui_state.chestnut_compiled) + self._chestnut_icon.set_visible(ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.LOADING, ChestnutState.ACTIVE)) + self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED)) self._mic_icon.set_visible(ui_state.recording_audio) self._body_icon.set_visible(bool(ui_state.is_body)) diff --git a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py index e844f5e9e..3bb2f70b8 100644 --- a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py @@ -3,7 +3,7 @@ import pyray as rl from dataclasses import dataclass from openpilot.common.constants import CV from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar -from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus +from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus, ChestnutState from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached @@ -107,7 +107,6 @@ class HudRenderer(Widget): self.speed: float = 0.0 self.v_ego_cluster_seen: bool = False self._engaged: bool = False - self._small_model_engaged: bool = False self._chestnut_fade_time: float = 0 self._can_draw_top_icons = True @@ -127,9 +126,7 @@ class HudRenderer(Widget): self._txt_chestnut: rl.Texture = gui_app.texture('icons_mici/chestnut.png', 60, 44) self._txt_chestnut_green: rl.Texture = gui_app.texture('icons_mici/chestnut_green.png', 60, 44) self._txt_chestnut_orange: rl.Texture = gui_app.texture('icons_mici/chestnut_orange.png', 75, 44) - self._txt_chestnut_crossed: rl.Texture = gui_app.texture('icons_mici/chestnut_crossed.png', 60, 52) self._chestnut_icon: rl.Texture | None = None - self._wheel_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) self._wheel_y_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) @@ -165,13 +162,10 @@ class HudRenderer(Widget): controls_state.deprecated.vCruise if v_cruise_cluster == 0.0 else v_cruise_cluster ) engaged = sm['selfdriveState'].enabled - if (engaged and not self._engaged and not ui_state.chestnut_loading and ui_state.chestnut_active is not True and - ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame): - self._small_model_engaged = True - if engaged != self._engaged: - self._chestnut_fade_time = rl.get_time() if engaged else 0 if (set_speed != self.set_speed and engaged) or (engaged and not self._engaged): self._set_speed_changed_time = rl.get_time() + if engaged != self._engaged: + self._chestnut_fade_time = rl.get_time() if engaged else 0 self._engaged = engaged self.set_speed = set_speed self.is_cruise_set = 0 < self.set_speed < SET_SPEED_NA @@ -191,8 +185,7 @@ class HudRenderer(Widget): if self.is_cruise_set: self._draw_set_speed(rect) - if ui_state.chestnut and ui_state.chestnut_compiled: - self._draw_model_source(rect) + self._draw_model_source(rect) self._draw_steering_wheel(rect) @@ -200,30 +193,24 @@ class HudRenderer(Widget): if ui_state.sm.recv_frame['selfdriveState'] < ui_state.started_frame: return - big_failed = (ui_state.chestnut_active is False or not ui_state.sm['deviceState'].chestnutPresent or - (ui_state.chestnut_active is True and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame and - not ui_state.sm.alive['modelV2']) or - (ui_state.chestnut_active is None and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame)) - self._small_model_engaged &= big_failed - loading = ui_state.chestnut_loading or (ui_state.chestnut_active is None and not big_failed) + loading = ui_state.chestnut_state == ChestnutState.LOADING if loading: - pulse = 0.5 - 0.5 * math.cos(rl.get_time() * 6.0) icon = self._txt_chestnut - opacity = 0.35 + 0.65 * pulse - elif self._small_model_engaged: - icon = self._txt_chestnut_crossed - opacity = 0.65 - elif big_failed: + opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)) + elif ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED): icon = self._txt_chestnut_orange opacity = 1.0 - else: + elif ui_state.chestnut_state == ChestnutState.ACTIVE: icon = self._txt_chestnut_green opacity = 1.0 + else: + return if icon is not self._chestnut_icon: self._chestnut_fade_time = rl.get_time() self._chestnut_icon = icon - alpha = self._chestnut_alpha_filter.update(loading or 0 < rl.get_time() - self._chestnut_fade_time < SET_SPEED_PERSISTENCE) + visible = loading or rl.get_time() - self._chestnut_fade_time < SET_SPEED_PERSISTENCE + alpha = self._chestnut_alpha_filter.update(visible) if alpha < 1e-2: return diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index 63a71758f..c169cbeaf 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -24,6 +24,15 @@ class UIStatus(Enum): OVERRIDE = "override" +class ChestnutState(Enum): + DISCONNECTED = "disconnected" + UNCOMPILED = "uncompiled" + READY = "ready" + LOADING = "loading" + ACTIVE = "active" + FAILED = "failed" + + class UIState: _instance: 'UIState | None' = None @@ -77,10 +86,11 @@ class UIState: self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM") self.experimental_mode: bool = self.params.get_bool("ExperimentalMode") self.experimental_mode_confirmed: bool = self.params.get_bool("ExperimentalModeConfirmed") - self.chestnut: bool = False + self.chestnut_present: bool = False self.chestnut_compiled: bool = chestnut_compiled() self.chestnut_active: bool | None = None self.chestnut_loading: bool = False + self.chestnut_state = ChestnutState.DISCONNECTED self.started: bool = False self.ignition: bool = False self.recording_audio: bool = False @@ -126,6 +136,7 @@ class UIState: self.sm.update(0) self._update_state() self._update_status() + self._update_chestnut_state() device.update() def _params_refresh_worker(self): @@ -186,12 +197,35 @@ class UIState: self.status = UIStatus.DISENGAGED self.started_frame = self.sm.frame self.started_time = time.monotonic() + self.chestnut_present = self.sm["deviceState"].chestnutPresent for callback in self._offroad_transition_callbacks: callback() self._started_prev = self.started + def _update_chestnut_state(self) -> None: + detected = self.sm["deviceState"].chestnutPresent + if not self.started: + self.chestnut_present = detected + self.chestnut_state = (ChestnutState.READY if detected and self.chestnut_compiled else + ChestnutState.UNCOMPILED if detected else ChestnutState.DISCONNECTED) + return + + model_seen = self.sm.recv_frame["modelV2"] > self.started_frame + if not self.chestnut_present: + self.chestnut_state = ChestnutState.DISCONNECTED + elif not self.chestnut_compiled: + self.chestnut_state = ChestnutState.UNCOMPILED + elif self.chestnut_state == ChestnutState.FAILED or not detected or (model_seen and (not self.sm.alive["modelV2"] or not self.sm["modelV2"].big)): + self.chestnut_state = ChestnutState.FAILED + elif self.chestnut_loading or not model_seen: + self.chestnut_state = ChestnutState.LOADING + elif self.chestnut_active is False: + self.chestnut_state = ChestnutState.FAILED + else: + self.chestnut_state = ChestnutState.ACTIVE + def update_params(self) -> None: # For slower operations # Update longitudinal control state @@ -208,8 +242,6 @@ class UIState: self.always_on_dm = self.params.get_bool("AlwaysOnDM") self.experimental_mode = self.params.get_bool("ExperimentalMode") self.experimental_mode_confirmed = self.params.get_bool("ExperimentalModeConfirmed") - # keep chestnut UI active until offroad transition when gpu disappears - self.chestnut = self.sm["deviceState"].chestnutPresent or (self.chestnut and self.started) if not self.chestnut_compiled: self.chestnut_compiled = chestnut_compiled() self.chestnut_active = self.params.get("ChestnutActive")