From 9c5bf2ce0af3860749ebac3a5c039a6bd874eae1 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 1 Mar 2026 13:50:44 -0500 Subject: [PATCH] ui: `AlertFadeAnimator` for longitudinal-related statuses (#1748) --- .../sunnypilot/onroad/smart_cruise_control.py | 53 +++++++++---------- selfdrive/ui/sunnypilot/onroad/speed_limit.py | 19 ++----- system/ui/sunnypilot/lib/utils.py | 24 +++++++++ 3 files changed, 53 insertions(+), 43 deletions(-) diff --git a/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py b/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py index b4848141e..c89bd914b 100644 --- a/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py +++ b/selfdrive/ui/sunnypilot/onroad/smart_cruise_control.py @@ -10,6 +10,7 @@ from openpilot.selfdrive.ui.onroad.hud_renderer import COLORS from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.sunnypilot.lib.utils import AlertFadeAnimator from openpilot.system.ui.widgets import Widget @@ -18,12 +19,13 @@ class SmartCruiseControlRenderer(Widget): super().__init__() self.vision_enabled = False self.vision_active = False - self.vision_frame = 0 self.map_enabled = False self.map_active = False - self.map_frame = 0 self.long_override = False + self._vision_fade = AlertFadeAnimator(gui_app.target_fps) + self._map_fade = AlertFadeAnimator(gui_app.target_fps) + self.font = gui_app.font(FontWeight.BOLD) def update(self): @@ -41,21 +43,10 @@ class SmartCruiseControlRenderer(Widget): if sm.updated["carControl"]: self.long_override = sm["carControl"].cruiseControl.override - if self.vision_active: - self.vision_frame += 1 - else: - self.vision_frame = 0 + self._vision_fade.update(self.vision_active) + self._map_fade.update(self.map_active) - if self.map_active: - self.map_frame += 1 - else: - self.map_frame = 0 - - @staticmethod - def _pulse_element(frame): - return not (frame % gui_app.target_fps < (gui_app.target_fps / 2.5)) - - def _draw_icon(self, rect_center_x, rect_height, x_offset, y_offset, name): + def _draw_icon(self, rect_center_x, rect_height, x_offset, y_offset, name, alpha=1.0): text = name font_size = 36 padding_v = 5 @@ -65,9 +56,12 @@ class SmartCruiseControlRenderer(Widget): box_height = int(sz.y + padding_v * 2) if self.long_override: - box_color = COLORS.OVERRIDE + color = COLORS.OVERRIDE + box_color = rl.Color(color.r, color.g, color.b, int(alpha * 255)) else: - box_color = rl.Color(0, 255, 0, 255) + box_color = rl.Color(0, 255, 0, int(alpha * 255)) + + text_color = rl.Color(0, 0, 0, int(alpha * 255)) screen_y = rect_height / 4 + y_offset @@ -75,13 +69,14 @@ class SmartCruiseControlRenderer(Widget): box_y = screen_y - box_height / 2 # Draw rounded background box - rl.draw_rectangle_rounded(rl.Rectangle(box_x, box_y, box_width, box_height), 0.2, 10, box_color) + if alpha > 0.01: + rl.draw_rectangle_rounded(rl.Rectangle(box_x, box_y, box_width, box_height), 0.2, 10, box_color) - # Draw text centered in the box (black color for contrast against bright green/grey) - text_pos_x = box_x + (box_width - sz.x) / 2 - text_pos_y = box_y + (box_height - sz.y) / 2 + # Draw text centered in the box (black color for contrast against bright green/grey) + text_pos_x = box_x + (box_width - sz.x) / 2 + text_pos_y = box_y + (box_height - sz.y) / 2 - rl.draw_text_ex(self.font, text, rl.Vector2(text_pos_x, text_pos_y), font_size, 0, rl.BLACK) + rl.draw_text_ex(self.font, text, rl.Vector2(text_pos_x, text_pos_y), font_size, 0, text_color) def _render(self, rect: rl.Rectangle): x_offset = -260 @@ -101,10 +96,10 @@ class SmartCruiseControlRenderer(Widget): y_scc_m = orders[idx] idx += 1 - scc_vision_pulse = self._pulse_element(self.vision_frame) - if (self.vision_enabled and not self.vision_active) or (self.vision_active and scc_vision_pulse): - self._draw_icon(rect.x + rect.width / 2, rect.height, x_offset, y_scc_v, "SCC-V") + if self.vision_enabled: + alpha = self._vision_fade.alpha if self.vision_active else 1.0 + self._draw_icon(rect.x + rect.width / 2, rect.height, x_offset, y_scc_v, "SCC-V", alpha) - scc_map_pulse = self._pulse_element(self.map_frame) - if (self.map_enabled and not self.map_active) or (self.map_active and scc_map_pulse): - self._draw_icon(rect.x + rect.width / 2, rect.height, x_offset, y_scc_m, "SCC-M") + if self.map_enabled: + alpha = self._map_fade.alpha if self.map_active else 1.0 + self._draw_icon(rect.x + rect.width / 2, rect.height, x_offset, y_scc_m, "SCC-M", alpha) diff --git a/selfdrive/ui/sunnypilot/onroad/speed_limit.py b/selfdrive/ui/sunnypilot/onroad/speed_limit.py index 02df292cd..1b03e9a20 100644 --- a/selfdrive/ui/sunnypilot/onroad/speed_limit.py +++ b/selfdrive/ui/sunnypilot/onroad/speed_limit.py @@ -11,7 +11,6 @@ import pyray as rl from cereal import custom from openpilot.common.constants import CV -from openpilot.common.filter_simple import FirstOrderFilter from openpilot.selfdrive.ui.onroad.hud_renderer import UI_CONFIG from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode as SpeedLimitMode @@ -19,6 +18,7 @@ from openpilot.system.hardware import HARDWARE 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 +from openpilot.system.ui.sunnypilot.lib.utils import AlertFadeAnimator from openpilot.system.ui.widgets import Widget METER_TO_FOOT = 3.28084 @@ -58,23 +58,14 @@ class SpeedLimitAlertRenderer: self.arrow_blank = rl.load_texture_from_image(blank_image) rl.unload_image(blank_image) - self._pre_active_alpha_filter = FirstOrderFilter(1.0, 0.05, 1 / gui_app.target_fps) - self._pre_active_alert_frame = 0 + self._pre_active_fade = AlertFadeAnimator(gui_app.target_fps, duration_on=0.75, rc=0.05) def update(self): assist_state = ui_state.sm['longitudinalPlanSP'].speedLimit.assist.state - if assist_state == AssistState.preActive: - self._pre_active_alert_frame += 1 - if (self._pre_active_alert_frame % gui_app.target_fps) < (gui_app.target_fps * 0.75): - self._pre_active_alpha_filter.x = 1.0 - else: - self._pre_active_alpha_filter.update(0.0) - else: - self._pre_active_alert_frame = 0 - self._pre_active_alpha_filter.update(1.0) + self._pre_active_fade.update(assist_state == AssistState.preActive) def speed_limit_pre_active_icon_helper(self): - icon_alpha = max(0.0, min(self._pre_active_alpha_filter.x * 255.0, 255.0)) + icon_alpha = max(0.0, min(self._pre_active_fade.alpha * 255.0, 255.0)) txt_icon = self.arrow_blank icon_margin_x = 10 icon_margin_y = 18 @@ -197,7 +188,7 @@ class SpeedLimitRenderer(Widget, SpeedLimitAlertRenderer): sign_rect = rl.Rectangle(x, y, width, UI_CONFIG.set_speed_height + 6 * 2) - alpha = self._pre_active_alpha_filter.x + alpha = self._pre_active_fade.alpha if ui_state.speed_limit_mode != SpeedLimitMode.off: self._draw_sign_main(sign_rect, alpha) diff --git a/system/ui/sunnypilot/lib/utils.py b/system/ui/sunnypilot/lib/utils.py index 09230da14..ecaba86f4 100644 --- a/system/ui/sunnypilot/lib/utils.py +++ b/system/ui/sunnypilot/lib/utils.py @@ -10,3 +10,27 @@ from openpilot.system.ui.sunnypilot.widgets.list_view import ButtonActionSP class NoElideButtonAction(ButtonActionSP): def get_width_hint(self): return super().get_width_hint() + 1 + + +class AlertFadeAnimator: + def __init__(self, target_fps: int, duration_on: float = 0.75, rc: float = 0.05): + from openpilot.common.filter_simple import FirstOrderFilter + self._filter = FirstOrderFilter(1.0, rc, 1 / target_fps) + self._frame = 0 + self._target_fps = target_fps + self._duration_on = duration_on + + def update(self, active: bool): + if active: + self._frame += 1 + if (self._frame % self._target_fps) < (self._target_fps * self._duration_on): + self._filter.x = 1.0 + else: + self._filter.update(0.0) + else: + self._frame = 0 + self._filter.update(1.0) + + @property + def alpha(self) -> float: + return self._filter.x