From cdc2b7a3ed5e12d5c5346d9972f25cbbeed8c358 Mon Sep 17 00:00:00 2001 From: firestarsdog <229254897+firestarsdog@users.noreply.github.com> Date: Sat, 4 Jul 2026 18:29:56 -0400 Subject: [PATCH] BigUI WIP: Mici the Ticis & Tizis --- selfdrive/ui/onroad/alert_renderer.py | 239 ++++++++++++------ selfdrive/ui/onroad/augmented_road_view.py | 2 +- .../onroad/starpilot/starpilot_onroad_view.py | 12 +- 3 files changed, 165 insertions(+), 88 deletions(-) diff --git a/selfdrive/ui/onroad/alert_renderer.py b/selfdrive/ui/onroad/alert_renderer.py index 564990b34..77a37a684 100644 --- a/selfdrive/ui/onroad/alert_renderer.py +++ b/selfdrive/ui/onroad/alert_renderer.py @@ -1,14 +1,14 @@ import time import pyray as rl from dataclasses import dataclass -from cereal import custom, messaging, log +from cereal import messaging, log, custom from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter from openpilot.system.hardware import TICI 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.widgets import Widget -from openpilot.system.ui.widgets.label import Label +from openpilot.system.ui.widgets.label import UnifiedLabel AlertSize = log.SelfdriveState.AlertSize AlertStatus = log.SelfdriveState.AlertStatus @@ -16,27 +16,31 @@ StarPilotAlertStatus = custom.StarPilotSelfdriveState.AlertStatus ALERT_MARGIN = 40 ALERT_PADDING = 60 -ALERT_LINE_SPACING = 45 ALERT_BORDER_RADIUS = 30 -ALERT_FONT_SMALL = 66 -ALERT_FONT_MEDIUM = 74 -ALERT_FONT_BIG = 88 - ALERT_HEIGHTS = { AlertSize.small: 271, AlertSize.mid: 420, } -SELFDRIVE_STATE_TIMEOUT = 5 # Seconds -SELFDRIVE_UNRESPONSIVE_TIMEOUT = 10 # Seconds +ALERT_SLIDE_DISTANCES = { + AlertSize.small: 40, + AlertSize.mid: 70, + AlertSize.full: 100, +} + +SMALL_FONT_SIZE = 60 +MID_FONT_SIZE_1 = 88 +MID_FONT_SIZE_2 = 56 + +SELFDRIVE_STATE_TIMEOUT = 5 +SELFDRIVE_UNRESPONSIVE_TIMEOUT = 10 -# Constants ALERT_COLORS = { - AlertStatus.normal: rl.Color(0x15, 0x15, 0x15, 0xF1), # #151515 with alpha 0xF1 - AlertStatus.userPrompt: rl.Color(0xDA, 0x6F, 0x25, 0xF1), # #DA6F25 with alpha 0xF1 - AlertStatus.critical: rl.Color(0xC9, 0x22, 0x31, 0xF1), # #C92231 with alpha 0xF1 - StarPilotAlertStatus.starpilot: rl.Color(0x17, 0x86, 0x44, 0xF1), + AlertStatus.normal: rl.Color(0, 0, 0, 255), + AlertStatus.userPrompt: rl.Color(255, 115, 0, 255), + AlertStatus.critical: rl.Color(255, 0, 21, 255), + StarPilotAlertStatus.starpilot: rl.Color(23, 134, 68, 255), } @@ -46,9 +50,9 @@ class Alert: text2: str = "" size: int = 0 status: int = 0 + alert_type: str = "" -# Pre-defined alert instances ALERT_STARTUP_PENDING = Alert( text1=tr("openpilot Unavailable"), text2=tr("Waiting to start"), @@ -74,30 +78,28 @@ ALERT_CRITICAL_REBOOT = Alert( class AlertRenderer(Widget): def __init__(self): super().__init__() - self.font_regular: rl.Font = gui_app.font(FontWeight.NORMAL) - self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD) - # font size is set dynamically - self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP) - self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP) + self._alert_text1_label = UnifiedLabel(text="", font_size=48, font_weight=FontWeight.BOLD, + line_height=0.86, letter_spacing=-0.02) + self._alert_text2_label = UnifiedLabel(text="", font_size=32, font_weight=FontWeight.NORMAL, + line_height=0.86, letter_spacing=0.025) + + self._prev_alert: Alert | None = None + + self._alert_y_filter = BounceFilter(0, 0.1, 1 / gui_app.target_fps, initialized=False) + self._alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) def get_alert(self, sm: messaging.SubMaster) -> Alert | None: - """Generate the current alert based on selfdrive state.""" ss = sm['selfdriveState'] - # Check if selfdriveState messages have stopped arriving - recv_frame = sm.recv_frame['selfdriveState'] if not sm.updated['selfdriveState']: + recv_frame = sm.recv_frame['selfdriveState'] time_since_onroad = time.monotonic() - ui_state.started_time - # 1. Never received selfdriveState since going onroad waiting_for_startup = recv_frame < ui_state.started_frame if waiting_for_startup and time_since_onroad > 5: return ALERT_STARTUP_PENDING - # 2. Lost communication with selfdriveState after receiving it if TICI and not waiting_for_startup: ss_missing = time.monotonic() - sm.recv_time['selfdriveState'] if ss_missing > SELFDRIVE_STATE_TIMEOUT: @@ -106,82 +108,159 @@ class AlertRenderer(Widget): return ALERT_CRITICAL_REBOOT if ss.alertSize != AlertSize.none: - alert = Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize.raw, status=ss.alertStatus.raw) + ret = Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize.raw, status=ss.alertStatus.raw, + alert_type=ss.alertType) else: starpilot_ss = sm["starpilotSelfdriveState"] if starpilot_ss.alertSize == custom.StarPilotSelfdriveState.AlertSize.none: return None - alert = Alert(text1=starpilot_ss.alertText1, text2=starpilot_ss.alertText2, - size=starpilot_ss.alertSize.raw, status=starpilot_ss.alertStatus.raw) + ret = Alert(text1=starpilot_ss.alertText1, text2=starpilot_ss.alertText2, + size=starpilot_ss.alertSize.raw, status=starpilot_ss.alertStatus.raw, + alert_type=starpilot_ss.alertType) - if alert.status == AlertStatus.normal and ui_state.starpilot_toggles.get("hide_alerts", False): + if ret.status == AlertStatus.normal and ui_state.starpilot_toggles.get("hide_alerts", False): return None - # Don't get old alert - if recv_frame < ui_state.started_frame: + if sm.recv_frame['selfdriveState'] < ui_state.started_frame: return None - return alert + self._prev_alert = ret + return ret - def _render(self, rect: rl.Rectangle): + def will_render(self) -> tuple[Alert | None, bool]: alert = self.get_alert(ui_state.sm) - if not alert: - return - - alert_rect = self._get_alert_rect(rect, alert.size) - self._draw_background(alert_rect, alert) - - text_rect = rl.Rectangle( - alert_rect.x + ALERT_PADDING, - alert_rect.y + ALERT_PADDING, - alert_rect.width - 2 * ALERT_PADDING, - alert_rect.height - 2 * ALERT_PADDING - ) - self._draw_text(text_rect, alert) + return alert or self._prev_alert, alert is None def _get_alert_rect(self, rect: rl.Rectangle, size: int) -> rl.Rectangle: if size == AlertSize.full: return rect - h = ALERT_HEIGHTS.get(size, rect.height) - return rl.Rectangle(rect.x + ALERT_MARGIN, rect.y + rect.height - h + ALERT_MARGIN, - rect.width - ALERT_MARGIN * 2, h - ALERT_MARGIN * 2) + return rl.Rectangle(rect.x, rect.y + rect.height - h, rect.width, h) - def _draw_background(self, rect: rl.Rectangle, alert: Alert) -> None: - color = ALERT_COLORS.get(alert.status, ALERT_COLORS[AlertStatus.normal]) + def _render(self, rect: rl.Rectangle) -> bool: + alert = self.get_alert(ui_state.sm) - if alert.size != AlertSize.full: - roundness = ALERT_BORDER_RADIUS / (min(rect.width, rect.height) / 2) - rl.draw_rectangle_rounded(rect, roundness, 10, color) + self._alpha_filter.update(0 if alert is None else 1) + + if alert is not None: + self._rect = self._get_alert_rect(rect, alert.size) + self._alert_y_filter.update(self._rect.y) + elif self._prev_alert is not None: + self._rect = self._get_alert_rect(rect, self._prev_alert.size) + slide = ALERT_SLIDE_DISTANCES.get(self._prev_alert.size, 100) + self._alert_y_filter.update(self._rect.y - slide) else: - rl.draw_rectangle_rec(rect, color) + return False + + if alert is None: + if self._alpha_filter.x > 0.01 and self._prev_alert is not None: + alert = self._prev_alert + else: + self._prev_alert = None + return False + + self._draw_background(alert) + self._draw_text(alert) + + return True + + def _draw_background(self, alert: Alert) -> None: + color = ALERT_COLORS.get(alert.status, ALERT_COLORS[AlertStatus.normal]) + color = rl.Color(color.r, color.g, color.b, int(255 * 0.90 * self._alpha_filter.x)) + translucent_color = rl.Color(color.r, color.g, color.b, int(0 * self._alpha_filter.x)) + + if alert.size == AlertSize.full: + bg_height = int(self._rect.height) + solid_height = round(bg_height * 0.2) + rl.draw_rectangle(int(self._rect.x), int(self._rect.y), int(self._rect.width), solid_height, color) + rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + solid_height), int(self._rect.width), + int(bg_height - solid_height), color, translucent_color) + else: + bg_height = int(self._rect.height) + solid_height = round(bg_height * 0.2) + roundness = ALERT_BORDER_RADIUS / (min(self._rect.width, self._rect.height) / 2) + rl.draw_rectangle_rounded(rl.Rectangle(self._rect.x, self._rect.y, self._rect.width, solid_height), + roundness, 10, color) + rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + solid_height), + int(self._rect.width), int(bg_height - solid_height), + color, translucent_color) + + def _draw_text(self, alert: Alert) -> None: + text_color = rl.Color(255, 255, 255, int(255 * 0.9 * self._alpha_filter.x)) + text2_color = rl.Color(255, 255, 255, int(255 * 0.65 * self._alpha_filter.x)) + + text_rect = rl.Rectangle( + self._rect.x + ALERT_PADDING, + self._alert_y_filter.x, + self._rect.width - ALERT_PADDING * 2, + self._rect.height, + ) - def _draw_text(self, rect: rl.Rectangle, alert: Alert) -> None: if alert.size == AlertSize.small: - self._draw_centered(alert.text1, rect, self.font_bold, ALERT_FONT_MEDIUM) + self._alert_text1_label.set_text(alert.text1) + self._alert_text1_label.set_text_color(text_color) + self._alert_text1_label.set_font_size(SMALL_FONT_SIZE) + self._alert_text1_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + self._alert_text1_label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + self._alert_text1_label.render(text_rect) elif alert.size == AlertSize.mid: - self._draw_centered(alert.text1, rect, self.font_bold, ALERT_FONT_BIG, center_y=False) - rect.y += ALERT_FONT_BIG + ALERT_LINE_SPACING - self._draw_centered(alert.text2, rect, self.font_regular, ALERT_FONT_SMALL, center_y=False) + self._alert_text1_label.set_text(alert.text1) + self._alert_text1_label.set_text_color(text_color) + self._alert_text1_label.set_font_size(MID_FONT_SIZE_1) + self._alert_text1_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + self._alert_text1_label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + self._alert_text1_label.render(text_rect) + + if alert.text2: + content_h = self._alert_text1_label.get_content_height(int(text_rect.width)) + center_y = self._alert_y_filter.x + (self._rect.height - content_h) / 2 + text2_rect = rl.Rectangle( + text_rect.x, + center_y + content_h, + text_rect.width, + self._rect.height - (center_y + content_h - self._alert_y_filter.x), + ) + self._alert_text2_label.set_text(alert.text2) + self._alert_text2_label.set_text_color(text2_color) + self._alert_text2_label.set_font_size(MID_FONT_SIZE_2) + self._alert_text2_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + self._alert_text2_label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP) + self._alert_text2_label.render(text2_rect) else: - is_long = len(alert.text1) > 15 - font_size1 = 132 if is_long else 177 + if len(alert.text1) <= 12: + font_size = 120 + elif len(alert.text1) <= 16: + font_size = 110 + else: + font_size = 100 - top_offset = 200 if is_long or '\n' in alert.text1 else 270 - title_rect = rl.Rectangle(rect.x, rect.y + top_offset, rect.width, 600) - self._full_text1_label.set_font_size(font_size1) - self._full_text1_label.set_text(alert.text1) - self._full_text1_label.render(title_rect) + self._alert_text1_label.set_text(alert.text1) + self._alert_text1_label.set_text_color(text_color) + self._alert_text1_label.set_font_size(font_size) + self._alert_text1_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + self._alert_text1_label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + self._alert_text1_label.render(text_rect) - bottom_offset = 361 if is_long else 420 - subtitle_rect = rl.Rectangle(rect.x, rect.y + rect.height - bottom_offset, rect.width, 300) - self._full_text2_label.set_text(alert.text2) - self._full_text2_label.render(subtitle_rect) - - def _draw_centered(self, text, rect, font, font_size, center_y=True, color=rl.WHITE) -> None: - text_size = measure_text_cached(font, text, font_size) - x = rect.x + (rect.width - text_size.x) / 2 - y = rect.y + ((rect.height - text_size.y) / 2 if center_y else 0) - rl.draw_text_ex(font, text, rl.Vector2(x, y), font_size, 0, color) + if alert.text2: + if len(alert.text2) > 24: + t2_size = 48 + elif len(alert.text2) > 18: + t2_size = 52 + else: + t2_size = 56 + content_h = self._alert_text1_label.get_content_height(int(text_rect.width)) + center_y = self._alert_y_filter.x + (self._rect.height - content_h) / 2 + text2_rect = rl.Rectangle( + text_rect.x, + center_y + content_h, + text_rect.width, + self._rect.height - (center_y + content_h - self._alert_y_filter.x), + ) + self._alert_text2_label.set_text(alert.text2) + self._alert_text2_label.set_text_color(text2_color) + self._alert_text2_label.set_font_size(t2_size) + self._alert_text2_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + self._alert_text2_label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP) + self._alert_text2_label.render(text2_rect) diff --git a/selfdrive/ui/onroad/augmented_road_view.py b/selfdrive/ui/onroad/augmented_road_view.py index 67ca707b7..cda7ed64b 100644 --- a/selfdrive/ui/onroad/augmented_road_view.py +++ b/selfdrive/ui/onroad/augmented_road_view.py @@ -189,7 +189,7 @@ class AugmentedRoadView(CameraView): super()._render(self._content_rect) # Draw all UI overlays - current_alert = self.alert_renderer.get_alert(ui_state.sm) + alert_to_render, _not_animating_out = self.alert_renderer.will_render() self.model_renderer.render(self._content_rect) self._hud_renderer.render(self._content_rect) self.alert_renderer.render(self._content_rect) diff --git a/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py b/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py index 134181449..2e174f8cc 100644 --- a/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py +++ b/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py @@ -289,10 +289,9 @@ class StarPilotOnroadView(AugmentedRoadView): def _render_bottom_row_widgets(self): - # Hide if alerts are active - from cereal import log - AlertSize = log.SelfdriveState.AlertSize - if ui_state.sm["selfdriveState"].alertSize != AlertSize.none: + # Hide if any alert (stock or StarPilot) is active + alert_showing, _ = self.alert_renderer.will_render() + if alert_showing is not None: return dm = self.driver_state_renderer @@ -377,9 +376,8 @@ class StarPilotOnroadView(AugmentedRoadView): render_weather_icon(weather_rect) def _render_pedals(self): - from cereal import log - AlertSize = log.SelfdriveState.AlertSize - if ui_state.sm["selfdriveState"].alertSize != AlertSize.none: + alert_showing, _ = self.alert_renderer.will_render() + if alert_showing is not None: return dm = self.driver_state_renderer