diff --git a/openpilot/selfdrive/ui/layouts/settings/firehose.py b/openpilot/selfdrive/ui/layouts/settings/firehose.py index b06840e4d..b7709973e 100644 --- a/openpilot/selfdrive/ui/layouts/settings/firehose.py +++ b/openpilot/selfdrive/ui/layouts/settings/firehose.py @@ -5,7 +5,7 @@ from openpilot.system.ui.lib.multilang import tr, tr_noop from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.wrap_text import wrap_text -from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayoutBase +from openpilot.selfdrive.ui.layouts.settings.firehose_base import FirehoseLayoutBase TITLE = tr_noop("Firehose Mode") DESCRIPTION = tr_noop( diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/firehose.py b/openpilot/selfdrive/ui/layouts/settings/firehose_base.py similarity index 98% rename from openpilot/selfdrive/ui/mici/layouts/settings/firehose.py rename to openpilot/selfdrive/ui/layouts/settings/firehose_base.py index 5e3b6da2c..d7d6f8ae7 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/firehose.py +++ b/openpilot/selfdrive/ui/layouts/settings/firehose_base.py @@ -15,7 +15,6 @@ from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.system.ui.lib.multilang import tr, trn, tr_noop from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.scroller import NavRawScrollPanel TITLE = tr_noop("Firehose Mode") DESCRIPTION = tr_noop( @@ -218,7 +217,3 @@ class FirehoseLayoutBase(Widget): if not ui_state.started and device._awake: self._fetch_firehose_stats() time.sleep(self.UPDATE_INTERVAL) - - -class FirehoseLayout(NavRawScrollPanel, FirehoseLayoutBase): - pass diff --git a/openpilot/selfdrive/ui/mici/layouts/home.py b/openpilot/selfdrive/ui/mici/layouts/home.py deleted file mode 100644 index 3e90a7d1a..000000000 --- a/openpilot/selfdrive/ui/mici/layouts/home.py +++ /dev/null @@ -1,245 +0,0 @@ -from __future__ import annotations - -import datetime -import math - -from openpilot.cereal import log -import pyray as rl -from collections.abc import Callable -from openpilot.system.ui.widgets import Widget -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, TextAlignment, TextAlignmentVertical -from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState -from openpilot.common.version import RELEASE_BRANCHES - -HEAD_BUTTON_FONT_SIZE = 40 -HOME_PADDING = 8 -ALERTS_ZONE_WIDTH = 180 - -NetworkType = log.DeviceState.NetworkType - -NETWORK_TYPES = { - NetworkType.none: "Offline", - NetworkType.wifi: "WiFi", - NetworkType.cell2G: "2G", - NetworkType.cell3G: "3G", - NetworkType.cell4G: "LTE", - NetworkType.cell5G: "5G", - NetworkType.ethernet: "Ethernet", -} - - -class AlertsPill(Widget): - ICON_OFFSET = 12 - COUNT_OFFSET = 40 - - def __init__(self): - super().__init__() - self.set_rect(rl.Rectangle(0, 0, 104, 52)) - - self._pill_bg_txt = gui_app.texture("icons_mici/alerts_pill.png", 104, 52) - self._alert_count_callback: Callable[[], int] | None = None - self._alert_icon_callback: Callable[[], rl.Texture | None] | None = None - - def set_alert_count_callback(self, callback: Callable[[], int] | None, - icon_callback: Callable[[], rl.Texture | None] | None = None): - self._alert_count_callback = callback - self._alert_icon_callback = icon_callback - - def _render(self, _): - alert_count = self._alert_count_callback() if self._alert_count_callback else 0 - if alert_count > 0: - pill_w, pill_h = self._pill_bg_txt.width, self._pill_bg_txt.height - rl.draw_texture_ex(self._pill_bg_txt, rl.Vector2(self.rect.x, self.rect.y), 0.0, 1.0, rl.WHITE) - - warning_txt = self._alert_icon_callback() if self._alert_icon_callback else None - if warning_txt is not None: - scale = 36 / max(warning_txt.width, warning_txt.height) - warn_x = self.rect.x + self.ICON_OFFSET - warn_y = self.rect.y + (pill_h - warning_txt.height * scale) / 2 - rl.draw_texture_ex(warning_txt, rl.Vector2(warn_x, warn_y), 0.0, scale, rl.WHITE) - - count_rect = rl.Rectangle(self.rect.x + self.COUNT_OFFSET, self.rect.y, pill_w - self.COUNT_OFFSET, pill_h) - gui_label(count_rect, str(alert_count), font_size=36, - alignment=TextAlignment.CENTER, - alignment_vertical=TextAlignmentVertical.MIDDLE) - - -class NetworkIcon(Widget): - def __init__(self): - super().__init__() - self.set_rect(rl.Rectangle(0, 0, 60, 47)) # max size of all icons - self._net_type = NetworkType.none - self._net_strength = 0 - - self._wifi_slash_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_slash.png", 54, 47) - self._wifi_none_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_none.png", 54, 40) - self._wifi_low_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_low.png", 54, 40) - self._wifi_medium_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_medium.png", 54, 40) - self._wifi_full_txt = gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 54, 40) - - self._cell_none_txt = gui_app.texture("icons_mici/settings/network/cell_strength_none.png", 60, 40) - self._cell_low_txt = gui_app.texture("icons_mici/settings/network/cell_strength_low.png", 60, 40) - self._cell_medium_txt = gui_app.texture("icons_mici/settings/network/cell_strength_medium.png", 60, 40) - self._cell_high_txt = gui_app.texture("icons_mici/settings/network/cell_strength_high.png", 60, 40) - self._cell_full_txt = gui_app.texture("icons_mici/settings/network/cell_strength_full.png", 60, 40) - - def _update_state(self): - device_state = ui_state.sm['deviceState'] - self._net_type = device_state.networkType - strength = device_state.networkStrength - self._net_strength = max(0, min(5, strength.raw + 1)) if strength.raw > 0 else 0 - - def _render(self, _): - if self._net_type == NetworkType.wifi: - # There is no 1 - draw_net_txt = {0: self._wifi_none_txt, - 2: self._wifi_low_txt, - 3: self._wifi_medium_txt, - 4: self._wifi_full_txt, - 5: self._wifi_full_txt}.get(self._net_strength, self._wifi_low_txt) - elif self._net_type in (NetworkType.cell2G, NetworkType.cell3G, NetworkType.cell4G, NetworkType.cell5G): - draw_net_txt = {0: self._cell_none_txt, - 2: self._cell_low_txt, - 3: self._cell_medium_txt, - 4: self._cell_high_txt, - 5: self._cell_full_txt}.get(self._net_strength, self._cell_none_txt) - else: - draw_net_txt = self._wifi_slash_txt - - draw_x = self._rect.x + (self._rect.width - draw_net_txt.width) / 2 - draw_y = self._rect.y + (self._rect.height - draw_net_txt.height) / 2 - - if draw_net_txt == self._wifi_slash_txt: - # Offset by difference in height between slashless and slash icons to make center align match - draw_y -= (self._wifi_slash_txt.height - self._wifi_none_txt.height) / 2 - - rl.draw_texture_ex(draw_net_txt, rl.Vector2(draw_x, draw_y), 0.0, 1.0, rl.Color(255, 255, 255, int(255 * 0.9))) - - -class MiciHomeLayout(Widget): - def __init__(self): - super().__init__() - self._on_settings_click: Callable | None = None - self._on_alerts_click: Callable | None = None - self._alert_count_callback: Callable[[], int] | None = None - - self._version_text = self._get_version_text() - - self._usb_icon = IconWidget("icons_mici/usb.png", (62, 40)) - self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (54, 40)) - self._chestnut_loading_icon = IconWidget("icons_mici/chestnut.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)) - - self._alerts_pill = AlertsPill() - - self._status_bar_layout = HBoxLayout([ - IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9), - NetworkIcon(), - self._usb_icon, - self._chestnut_icon, - self._chestnut_loading_icon, - self._chestnut_failed_icon, - self._body_icon, - self._mic_icon, - ], spacing=18) - - self._openpilot_label = UnifiedLabel("openpilot", font_size=96, font_weight=FontWeight.DISPLAY, max_width=480, wrap_text=False) - self._version_label = UnifiedLabel("", font_size=36, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False) - self._large_version_label = UnifiedLabel("", font_size=64, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False) - self._date_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False) - self._branch_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, scroll=True) - self._version_commit_label = UnifiedLabel("", font_size=36, text_color=rl.GRAY, font_weight=FontWeight.ROMAN, max_width=480, wrap_text=False) - - def set_callbacks(self, on_settings: Callable | None = None, on_alerts: Callable | None = None, - alert_count_callback: Callable[[], int] | None = None, - alert_icon_callback: Callable[[], rl.Texture | None] | None = None): - self._on_settings_click = on_settings - self._on_alerts_click = on_alerts - self._alert_count_callback = alert_count_callback - self._alerts_pill.set_alert_count_callback(alert_count_callback, alert_icon_callback) - - def _handle_mouse_release(self, mouse_pos: MousePos): - relative_x = mouse_pos.x - self.rect.x - has_alerts = self._alert_count_callback and self._alert_count_callback() > 0 - if has_alerts and relative_x > self.rect.width - ALERTS_ZONE_WIDTH: - if self._on_alerts_click: - self._on_alerts_click() - elif self._on_settings_click: - self._on_settings_click() - - def _get_version_text(self) -> tuple[str, str, str, str] | None: - version = ui_state.params.get("Version") - branch = ui_state.params.get("GitBranch") - commit = ui_state.params.get("GitCommit") - - if not all((version, branch, commit)): - return None - - commit_date_raw = ui_state.params.get("GitCommitDate") - try: - # GitCommitDate format from get_commit_date(): '%ct %ci' e.g. "'1708012345 2024-02-15 ...'" - unix_ts = int(commit_date_raw.strip("'").split()[0]) - date_str = datetime.datetime.fromtimestamp(unix_ts).strftime("%b %d") - except (ValueError, IndexError, TypeError, AttributeError): - date_str = "" - - return version, branch, commit[:7], date_str - - def _render(self, _): - # TODO: why is there extra space here to get it to be flush? - text_pos = rl.Vector2(self.rect.x - 2 + HOME_PADDING, self.rect.y - 16) - self._openpilot_label.set_position(text_pos.x, text_pos.y) - self._openpilot_label.render() - - if self._version_text is not None: - # release branch - release_branch = self._version_text[1] in RELEASE_BRANCHES - version_pos = rl.Rectangle(text_pos.x, text_pos.y + self._openpilot_label.font_size + 16, 100, 44) - self._version_label.set_text(self._version_text[0]) - self._version_label.set_position(version_pos.x, version_pos.y) - self._version_label.render() - - self._date_label.set_text(" " + self._version_text[3]) - self._date_label.set_position(version_pos.x + self._version_label.text_width + 10, version_pos.y) - self._date_label.render() - - self._branch_label.set_max_width(gui_app.width - self._version_label.text_width - self._date_label.text_width - 32) - self._branch_label.set_text(" " + ("release" if release_branch else self._version_text[1])) - self._branch_label.set_position(version_pos.x + self._version_label.text_width + self._date_label.text_width + 20, version_pos.y) - self._branch_label.render() - - if not release_branch: - # 2nd line - self._version_commit_label.set_text(self._version_text[2]) - self._version_commit_label.set_position(version_pos.x, version_pos.y + self._date_label.font_size + 7) - self._version_commit_label.render() - - # ***** Center-aligned bottom section icons ***** - usb_connected = ui_state.usb_connected - usb_unknown = ui_state.usb_unknown - chestnut_state = ui_state.chestnut_state - if gui_app.sunnypilot_ui(): - self._set_chestnut_visibility() - else: - self._usb_icon.set_visible(usb_connected and usb_unknown) - self._chestnut_icon.set_visible(not usb_unknown and chestnut_state not in - (ChestnutState.LOADING, ChestnutState.UNCOMPILED, ChestnutState.FAILED) and - (usb_connected or chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE))) - self._chestnut_loading_icon.set_visible(not usb_unknown and chestnut_state == ChestnutState.LOADING) - self._chestnut_loading_icon.set_opacity(0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))) - self._chestnut_failed_icon.set_visible(not usb_unknown and 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)) - - footer_rect = rl.Rectangle(self.rect.x + HOME_PADDING, self.rect.y + self.rect.height - 48, self.rect.width - HOME_PADDING, 48) - self._status_bar_layout.render(footer_rect) - - # TODO: add alignment to hboxlayout and add to there - self._alerts_pill.set_position(self.rect.x + self.rect.width - self._alerts_pill.rect.width - HOME_PADDING, - self.rect.y + self.rect.height - self._alerts_pill.rect.height) - self._alerts_pill.render() diff --git a/openpilot/selfdrive/ui/mici/layouts/main.py b/openpilot/selfdrive/ui/mici/layouts/main.py deleted file mode 100644 index dc294f318..000000000 --- a/openpilot/selfdrive/ui/mici/layouts/main.py +++ /dev/null @@ -1,155 +0,0 @@ -import pyray as rl -import openpilot.cereal.messaging as messaging -from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout -from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsLayout -from openpilot.selfdrive.ui.mici.layouts.offroad_alerts import MiciOffroadAlerts -from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView -from openpilot.selfdrive.ui.ui_state import device, ui_state -from openpilot.selfdrive.ui.mici.layouts.onboarding import OnboardingWindow -from openpilot.selfdrive.ui.body.layouts.onroad import BodyLayout -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.scroller import Scroller -from openpilot.system.ui.lib.application import gui_app - -if gui_app.sunnypilot_ui(): - from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout - from openpilot.selfdrive.ui.sunnypilot.mici.layouts.home import MiciHomeLayoutSP as MiciHomeLayout - -ONROAD_DELAY = 2.5 # seconds - - -class MiciMainLayout(Scroller): - def __init__(self): - super().__init__(snap_items=True, spacing=0, pad=0, scroll_indicator=False, edge_shadows=False) - - self._pm = messaging.PubMaster(['bookmarkButton', 'userBookmark']) - - self._prev_onroad = False - self._prev_standstill = False - self._onroad_time_delay: float | None = None - self._setup = False - - # Initialize widgets - self._home_layout = MiciHomeLayout() - self._alerts_layout = MiciOffroadAlerts() - self._settings_layout = SettingsLayout() - self._car_onroad_layout = AugmentedRoadView(bookmark_callback=self._on_bookmark_clicked) - self._body_onroad_layout = BodyLayout() - - # Initialize widget rects - for widget in (self._home_layout, self._alerts_layout, self._settings_layout, - self._car_onroad_layout, self._body_onroad_layout): - # TODO: set parent rect and use it if never passed rect from render (like in Scroller) - widget.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - - self._scroller.add_widgets([ - self._alerts_layout, - self._home_layout, - self._car_onroad_layout, - self._body_onroad_layout, - ]) - self._scroller.set_reset_scroll_at_show(False) - - # Disable scrolling when onroad is interacting with bookmark - self._scroller.set_scrolling_enabled(lambda: not self._car_onroad_layout.is_swiping_left()) - - # Set callbacks - self._setup_callbacks() - - gui_app.add_nav_stack_tick(self._handle_transitions) - gui_app.push_widget(self) - - # Start onboarding if terms or training not completed, make sure to push after self - self._onboarding_window = OnboardingWindow(lambda: gui_app.pop_widgets_to(self)) - if not self._onboarding_window.completed: - gui_app.push_widget(self._onboarding_window) - - # initialize correct onroad layout - self._on_body_changed() - - @property - def _onroad_layout(self) -> Widget: - # For scroll_to - return self._body_onroad_layout if ui_state.is_body else self._car_onroad_layout - - def _setup_callbacks(self): - self._home_layout.set_callbacks( - on_settings=lambda: gui_app.push_widget(self._settings_layout), - on_alerts=lambda: self._scroll_to(self._alerts_layout), - alert_count_callback=self._alerts_layout.active_alerts, - alert_icon_callback=self._alerts_layout.highest_severity_icon, - ) - for layout in (self._car_onroad_layout, self._body_onroad_layout): - layout.set_click_callback(lambda: self._scroll_to(self._home_layout)) - - device.add_interactive_timeout_callback(self._on_interactive_timeout) - ui_state.add_on_body_changed_callbacks(self._on_body_changed) - - def _scroll_to(self, layout: Widget): - layout_x = int(layout.rect.x) - self._scroller.scroll_to(layout_x, smooth=True) - - def _update_state(self): - super()._update_state() - # TODO: Hack to run alert updates while not in view. Add a nav stack tick? - self._alerts_layout._update_state() - - def _render(self, _): - if not self._setup: - if self._alerts_layout.active_alerts() > 0: - self._scroller.scroll_to(self._alerts_layout.rect.x) - else: - self._scroller.scroll_to(self._rect.width) - self._setup = True - - # Render - super()._render(self._rect) - - def _handle_transitions(self): - # Don't pop if onboarding - if gui_app.widget_in_stack(self._onboarding_window): - return - - if ui_state.started != self._prev_onroad: - self._prev_onroad = ui_state.started - - # onroad: after delay, pop nav stack and scroll to onroad - # offroad: immediately scroll to home, but don't pop nav stack (can stay in settings) - if ui_state.started: - self._onroad_time_delay = rl.get_time() - else: - self._scroll_to(self._home_layout) - - # FIXME: these two pops can interrupt user interacting in the settings - if self._onroad_time_delay is not None and rl.get_time() - self._onroad_time_delay >= ONROAD_DELAY: - gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout)) - self._onroad_time_delay = None - - # When car leaves standstill, pop nav stack and scroll to onroad - CS = ui_state.sm["carState"] - if not CS.standstill and self._prev_standstill: - gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout)) - self._prev_standstill = CS.standstill - - def _on_interactive_timeout(self): - # Don't pop if onboarding - if gui_app.widget_in_stack(self._onboarding_window): - return - - if ui_state.started: - # Don't pop if at standstill - if not ui_state.sm["carState"].standstill: - gui_app.pop_widgets_to(self, lambda: self._scroll_to(self._onroad_layout)) - else: - # Screen turns off on timeout offroad, so pop immediately without animation - gui_app.pop_widgets_to(self, instant=True) - self._scroll_to(self._home_layout) - - def _on_bookmark_clicked(self): - for service in ('bookmarkButton', 'userBookmark'): - msg = messaging.new_message(service, valid=True) - self._pm.send(service, msg) - - def _on_body_changed(self): - self._car_onroad_layout.set_visible(not ui_state.is_body) - self._body_onroad_layout.set_visible(bool(ui_state.is_body)) diff --git a/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py b/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py deleted file mode 100644 index 9c9018146..000000000 --- a/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py +++ /dev/null @@ -1,336 +0,0 @@ -from __future__ import annotations - -import pyray as rl -import re -import threading -import time -from dataclasses import dataclass -from enum import IntEnum -from openpilot.common.params import Params -from openpilot.common.realtime import drop_realtime -from openpilot.selfdrive.selfdrived.alertmanager import OFFROAD_ALERTS -from openpilot.common.hardware import HARDWARE -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.system.ui.widgets.scroller import Scroller -from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical -from openpilot.system.ui.lib.multilang import tr - -REFRESH_INTERVAL = 5.0 # seconds - - -class AlertSize(IntEnum): - SMALL = 0 - MEDIUM = 1 - BIG = 2 - - -@dataclass -class AlertData: - key: str - text: str - severity: int - visible: bool = False - icon: str | None = None - - -class AlertItem(Widget): - # TODO: click should always go somewhere: home or specific settings pane - """Individual alert item widget with background image and text.""" - ALERT_WIDTH = 520 - ALERT_HEIGHT_SMALL = 212 - ALERT_HEIGHT_MED = 240 - ALERT_HEIGHT_BIG = 324 - ALERT_PADDING = 28 - ICON_SIZE = 64 - ICON_MARGIN = 12 - TEXT_COLOR = rl.Color(255, 255, 255, int(255 * 0.9)) - TITLE_BODY_SPACING = 24 - - def __init__(self, alert_data: AlertData): - super().__init__() - self.alert_data = alert_data - - # Load background textures - self._bg_small = gui_app.texture("icons_mici/offroad_alerts/small_alert.png", self.ALERT_WIDTH, self.ALERT_HEIGHT_SMALL) - self._bg_small_pressed = gui_app.texture("icons_mici/offroad_alerts/small_alert_pressed.png", self.ALERT_WIDTH, self.ALERT_HEIGHT_SMALL) - self._bg_medium = gui_app.texture("icons_mici/offroad_alerts/medium_alert.png", self.ALERT_WIDTH, self.ALERT_HEIGHT_MED) - self._bg_medium_pressed = gui_app.texture("icons_mici/offroad_alerts/medium_alert_pressed.png", self.ALERT_WIDTH, self.ALERT_HEIGHT_MED) - self._bg_big = gui_app.texture("icons_mici/offroad_alerts/big_alert.png", self.ALERT_WIDTH, self.ALERT_HEIGHT_BIG) - self._bg_big_pressed = gui_app.texture("icons_mici/offroad_alerts/big_alert_pressed.png", self.ALERT_WIDTH, self.ALERT_HEIGHT_BIG) - - # Load alert icons - self._icon_orange = gui_app.texture("icons_mici/offroad_alerts/orange_warning.png", self.ICON_SIZE, self.ICON_SIZE) - self._icon_red = gui_app.texture("icons_mici/offroad_alerts/red_warning.png", self.ICON_SIZE, self.ICON_SIZE) - self._icon_green = gui_app.texture("icons_mici/offroad_alerts/green_wheel.png", self.ICON_SIZE, self.ICON_SIZE) - self._custom_icon = gui_app.texture(alert_data.icon, self.ICON_SIZE, self.ICON_SIZE) if alert_data.icon else None - - if self._custom_icon is not None: - self._icon = self._custom_icon - elif alert_data.severity == -1: - self._icon = self._icon_green - elif alert_data.severity > 0: - self._icon = self._icon_red - else: - self._icon = self._icon_orange - - self._title_label = UnifiedLabel(text="", font_size=32, font_weight=FontWeight.SEMI_BOLD, text_color=self.TEXT_COLOR, - alignment=TextAlignment.LEFT, - alignment_vertical=TextAlignmentVertical.TOP, line_height=0.95) - - self._body_label = UnifiedLabel(text="", font_size=28, font_weight=FontWeight.ROMAN, text_color=self.TEXT_COLOR, - alignment=TextAlignment.LEFT, - alignment_vertical=TextAlignmentVertical.BOTTOM, line_height=0.95) - - self._title_text = "" - self._body_text = "" - self._alert_size = AlertSize.SMALL - - self._update_content() - - @property - def icon(self) -> rl.Texture: - return self._icon - - def _split_text(self, text: str) -> tuple[str, str]: - """Split text into title (first sentence) and body (remaining text).""" - # Find the end of the first sentence (period, exclamation, or question mark followed by space or end) - match = re.search(r'[.!?](?:\s+|$)', text) - if match: - # Found a sentence boundary - split at the end of the sentence - title = text[:match.start()].strip() - body = text[match.end():].strip() - return title, body - else: - # No sentence boundary found, return full text as title - return "", text - - def _update_content(self): - """Update text and calculate height.""" - if not self.alert_data.visible or not self.alert_data.text: - self.set_visible(False) - return - - self.set_visible(True) - - # Split text into title and body - self._title_text, self._body_text = self._split_text(self.alert_data.text) - - # Calculate text width (alert width minus padding and icon space on right) - title_width = self.ALERT_WIDTH - (self.ALERT_PADDING * 2) - self.ICON_SIZE - self.ICON_MARGIN - body_width = self.ALERT_WIDTH - (self.ALERT_PADDING * 2) - - # Update labels - self._title_label.set_text(self._title_text) - self._body_label.set_text(self._body_text) - - # Calculate content height - title_height = self._title_label.get_content_height(title_width) if self._title_text else 0 - body_height = self._body_label.get_content_height(body_width) if self._body_text else 0 - spacing = self.TITLE_BODY_SPACING if (self._title_text and self._body_text) else 0 - total_text_height = title_height + spacing + body_height - - # Determine which background size to use based on content height - min_height_with_padding = total_text_height + (self.ALERT_PADDING * 2) - if min_height_with_padding > self.ALERT_HEIGHT_MED: - self._alert_size = AlertSize.BIG - height = self.ALERT_HEIGHT_BIG - elif min_height_with_padding > self.ALERT_HEIGHT_SMALL: - self._alert_size = AlertSize.MEDIUM - height = self.ALERT_HEIGHT_MED - else: - self._alert_size = AlertSize.SMALL - height = self.ALERT_HEIGHT_SMALL - - # Set rect size - self.set_rect(rl.Rectangle(0, 0, self.ALERT_WIDTH, height)) - - def update_alert_data(self, alert_data: AlertData): - """Update alert data and refresh display.""" - self.alert_data = alert_data - self._update_content() - - def _render(self, _): - if not self.alert_data.visible or not self.alert_data.text: - return - - # Choose background based on size - if self._alert_size == AlertSize.BIG: - bg_texture = self._bg_big_pressed if self.is_pressed else self._bg_big - elif self._alert_size == AlertSize.MEDIUM: - bg_texture = self._bg_medium_pressed if self.is_pressed else self._bg_medium - else: # AlertSize.SMALL - bg_texture = self._bg_small_pressed if self.is_pressed else self._bg_small - - # Draw background - rl.draw_texture_ex(bg_texture, rl.Vector2(self._rect.x, self._rect.y), 0.0, 1.0, rl.WHITE) - - # Calculate text area (left side, avoiding icon on right) - title_width = self.ALERT_WIDTH - (self.ALERT_PADDING * 2) - self.ICON_SIZE - self.ICON_MARGIN - body_width = self.ALERT_WIDTH - (self.ALERT_PADDING * 2) - text_x = self._rect.x + self.ALERT_PADDING - text_y = self._rect.y + self.ALERT_PADDING - - # Draw title label - if self._title_text: - title_rect = rl.Rectangle( - text_x, - text_y, - title_width, - self._title_label.get_content_height(title_width), - ) - self._title_label.render(title_rect) - text_y += title_rect.height + self.TITLE_BODY_SPACING - - # Draw body label - if self._body_text: - body_rect = rl.Rectangle( - text_x, - text_y, - body_width, - self._rect.height - text_y + self._rect.y - self.ALERT_PADDING, - ) - self._body_label.render(body_rect) - - # Draw warning icon on the right side - icon_x = self._rect.x + self.ALERT_WIDTH - self.ALERT_PADDING - self.ICON_SIZE - icon_y = self._rect.y + self.ALERT_PADDING - rl.draw_texture_ex(self._icon, rl.Vector2(icon_x, icon_y), 0.0, 1.0, rl.WHITE) - - -class MiciOffroadAlerts(Scroller): - """Offroad alerts layout with vertical scrolling.""" - - def __init__(self): - # Create vertical scroller - super().__init__(horizontal=False, spacing=12, pad=0) - self.params = Params() - self.sorted_alerts: list[AlertData] = [] - self.alert_items: list[AlertItem] = [] - - # Create empty state label - self._empty_label = UnifiedLabel(tr("no alerts"), 65, FontWeight.DISPLAY, rl.WHITE, - alignment=TextAlignment.CENTER, - alignment_vertical=TextAlignmentVertical.MIDDLE) - - # Build initial alert list - self._build_alerts() - - # Start param thread - self._pending_params: dict | None = None - self._params_thread = threading.Thread(target=self._params_worker, daemon=True) - self._params_thread.start() - - def active_alerts(self) -> int: - return sum(alert.visible for alert in self.sorted_alerts) - - def highest_severity_icon(self) -> rl.Texture | None: - item = max((item for item in self.alert_items if item.alert_data.visible), - key=lambda item: (item.alert_data.severity, bool(item.alert_data.icon)), default=None) - return item.icon if item is not None else None - - def scrolling(self): - return self._scroller.scroll_panel.is_touch_valid() - - def _build_alerts(self): - """Build sorted list of alerts from OFFROAD_ALERTS.""" - self.sorted_alerts = [] - - # Add UpdateAvailable alert at the top (severity = -1 to indicate special handling) - update_alert_data = AlertData(key="UpdateAvailable", text="", severity=-1) - self.sorted_alerts.append(update_alert_data) - update_alert_item = AlertItem(update_alert_data) - update_alert_item.set_click_callback(lambda: HARDWARE.reboot()) - self.alert_items.append(update_alert_item) - self._scroller.add_widget(update_alert_item) - - # Add regular alerts sorted by severity - for key, config in sorted(OFFROAD_ALERTS.items(), key=lambda x: x[1].get("severity", 0), reverse=True): - severity = config.get("severity", 0) - alert_data = AlertData(key=key, text="", severity=severity, icon=config.get("icon")) - self.sorted_alerts.append(alert_data) - - # Create alert item widget - alert_item = AlertItem(alert_data) - self.alert_items.append(alert_item) - self._scroller.add_widget(alert_item) - - def _params_worker(self): - drop_realtime() - while True: - self._pending_params = ({"UpdaterNewDescription": self.params.get("UpdaterNewDescription")} | - {alert_data.key: self.params.get(alert_data.key) for alert_data in self.sorted_alerts}) - time.sleep(REFRESH_INTERVAL) - - def _refresh(self, pending_params: dict) -> int: - """Refresh alerts from params and return active count.""" - active_count = 0 - - # Handle UpdateAvailable alert specially - update_available = pending_params["UpdateAvailable"] - update_alert_data = next((alert_data for alert_data in self.sorted_alerts if alert_data.key == "UpdateAvailable"), None) - - if update_alert_data: - if update_available: - version_string = "" - - # Get new version description and parse version and date - new_desc = pending_params["UpdaterNewDescription"] or "" - if new_desc: - # format: "version / branch / commit / date" - parts = new_desc.split(" / ") - if len(parts) > 3: - version, date = parts[0], parts[3] - version_string = f"\nsunnypilot {version}, {date}\n" - - update_alert_data.text = f"Update available {version_string}. Click to update. Read the release notes at blog.comma.ai." - update_alert_data.visible = True - active_count += 1 - else: - update_alert_data.text = "" - update_alert_data.visible = False - - # Handle regular alerts - for alert_data in self.sorted_alerts: - if alert_data.key == "UpdateAvailable": - continue # Skip, already handled above - - text = "" - alert_json = pending_params[alert_data.key] - - if alert_json: - text = alert_json.get("text", "").replace("%1", alert_json.get("extra", "")) - - if text and not alert_data.visible: - # Bump newly visible alerts to the top, severity sort keeps it at the top of its category - widget = next(w for w in self._scroller.items if w.alert_data is alert_data) - self._scroller.items.remove(widget) - self._scroller.items.insert(0, widget) - alert_data.text = text - alert_data.visible = bool(text) - - if alert_data.visible: - active_count += 1 - - # Update alert items (they reference the same alert_data objects) - for alert_item in self.alert_items: - alert_item.update_alert_data(alert_item.alert_data) - - self._scroller.items.sort(key=lambda w: -w.alert_data.severity) - - return active_count - - def _update_state(self): - """Periodically refresh alerts.""" - # Refresh alerts when thread updates params - pending_params = self._pending_params - if pending_params is not None: - self._refresh(pending_params) - self._pending_params = None - - def _render(self, rect: rl.Rectangle): - """Render the alerts scroller or empty state.""" - if self.active_alerts() == 0: - self._empty_label.render(rect) - else: - self._scroller.render(rect) diff --git a/openpilot/selfdrive/ui/mici/layouts/onboarding.py b/openpilot/selfdrive/ui/mici/layouts/onboarding.py deleted file mode 100644 index d138172da..000000000 --- a/openpilot/selfdrive/ui/mici/layouts/onboarding.py +++ /dev/null @@ -1,419 +0,0 @@ -import math -import numpy as np -import pyray as rl -from collections.abc import Callable -from openpilot.common.filter_simple import FirstOrderFilter -from openpilot.common.qrcode import make_texture -from openpilot.system.ui.lib.application import FontWeight, gui_app, TextAlignment -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.button import SmallCircleIconButton -from openpilot.system.ui.widgets.scroller import NavScroller, Scroller -from openpilot.system.ui.widgets.nav_widget import NavWidget -from openpilot.system.ui.mici_setup import GreyBigButton, BigPillButton -from openpilot.system.ui.widgets.label import gui_label -from openpilot.system.ui.lib.multilang import tr -from openpilot.common.version import terms_version, training_version, terms_version_sp -from openpilot.common.version import sunnylink_consent_version, sunnylink_consent_declined -from openpilot.selfdrive.ui.ui_state import ui_state, device -from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationCircleButton -from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer -from openpilot.selfdrive.ui.mici.onroad.cabin_camera_dialog import BaseCabinCameraDialog -from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkConsentPage - - -class DriverCameraSetupDialog(BaseCabinCameraDialog): - def __init__(self): - super().__init__() - self.driver_state_renderer = DriverStateRenderer(inset=True) - self.driver_state_renderer.set_rect(rl.Rectangle(0, 0, 120, 120)) - self.driver_state_renderer.load_icons() - self.driver_state_renderer.set_force_active(True) - - def _render(self, rect): - rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) - self._camera_view._render(rect) - - if not self._camera_view.frame: - gui_label(rect, tr("camera starting"), font_size=64, font_weight=FontWeight.BOLD, - alignment=TextAlignment.CENTER) - rl.end_scissor_mode() - return - - # Position dmoji on opposite side from driver - is_rhd = self.driver_state_renderer.is_rhd - self.driver_state_renderer.set_position( - rect.x + 8 if is_rhd else rect.x + rect.width - self.driver_state_renderer.rect.width - 8, - rect.y + 8, - ) - self.driver_state_renderer.render() - - self._draw_face_detection(rect) - - rl.end_scissor_mode() - - -class TrainingGuidePreDMTutorial(NavScroller): - def __init__(self, continue_callback: Callable[[], None]): - super().__init__() - - continue_button = BigPillButton("next") - continue_button.set_click_callback(continue_callback) - - self._scroller.add_widgets([ - GreyBigButton("driver monitoring\ncheck", "scroll to continue", - gui_app.texture("icons_mici/setup/green_dm.png", 64, 64)), - GreyBigButton("", "Next, we'll check if comma four can detect the driver properly."), - GreyBigButton("", "sunnypilot uses the cabin camera to check if the driver is distracted."), - GreyBigButton("", "If it does not have a clear view of the driver, unplug and remount before continuing."), - continue_button, - ]) - - def show_event(self): - super().show_event() - # Get driver monitoring model ready for next step - ui_state.params.put_bool("IsDriverViewEnabled", True) - - -class DMBadFaceDetected(NavScroller): - def __init__(self): - super().__init__() - - back_button = BigPillButton("back") - back_button.set_click_callback(self.dismiss) - - self._scroller.add_widgets([ - GreyBigButton("looking for driver", "make sure comma\nfour can see your face", - gui_app.texture("icons_mici/setup/orange_dm.png", 64, 64)), - GreyBigButton("", "Remount if your face is blocked, or driver monitoring has difficulty tracking your face."), - back_button, - ]) - - -class TrainingGuideDMTutorial(NavWidget): - PROGRESS_DURATION = 4 - LOOKING_THRESHOLD_DEG = 30.0 - - def __init__(self, continue_callback: Callable[[], None]): - super().__init__() - - self._back_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_question.png", 28, 48)) - self._back_button.set_click_callback(lambda: gui_app.push_widget(self._bad_face_page)) - self._back_button.set_touch_valid_callback(lambda: self.enabled and not self.is_dismissing) # for nav stack - self._good_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 42, 42)) - self._good_button.set_touch_valid_callback(lambda: self.enabled and not self.is_dismissing) # for nav stack - - self._good_button.set_click_callback(continue_callback) - self._good_button.set_enabled(False) - - self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps) - self._dialog = DriverCameraSetupDialog() - self._bad_face_page = DMBadFaceDetected() - - # Disable driver monitoring model when device times out for inactivity - def inactivity_callback(): - ui_state.params.put_bool("IsDriverViewEnabled", False) - - device.add_interactive_timeout_callback(inactivity_callback) - - def show_event(self): - super().show_event() - self._dialog.show_event() - self._progress.x = 0.0 - - def _update_state(self): - super()._update_state() - if device.awake and not ui_state.params.get_bool("IsDriverViewEnabled"): - ui_state.params.put_bool("IsDriverViewEnabled", True) - - sm = ui_state.sm - if sm.recv_frame.get("driverMonitoringState", 0) == 0: - return - - dm_state = sm["driverMonitoringState"] - driver_data = self._dialog.driver_state_renderer.get_driver_data() - - if len(driver_data.faceOrientation) == 3: - pitch, yaw, _ = driver_data.faceOrientation - looking_center = abs(math.degrees(pitch)) < self.LOOKING_THRESHOLD_DEG and abs(math.degrees(yaw)) < self.LOOKING_THRESHOLD_DEG - else: - looking_center = False - - # stay at 100% once reached - in_bad_face = gui_app.get_active_widget() == self._bad_face_page - if ((dm_state.visionPolicyState.faceDetected and looking_center) or self._progress.x > 0.99) and not in_bad_face: - slow = self._progress.x < 0.25 - duration = self.PROGRESS_DURATION * 2 if slow else self.PROGRESS_DURATION - self._progress.x += 1.0 / (duration * gui_app.target_fps) - self._progress.x = min(1.0, self._progress.x) - else: - self._progress.update(0.0) - - self._good_button.set_enabled(self._progress.x >= 0.999) - - def _render(self, _): - self._dialog.render(self._rect) - - gradient_y = int(self._rect.y + self._rect.height - 80) - gradient_h = int(self._rect.y) + int(self._rect.height) - gradient_y - rl.draw_rectangle_gradient_v(int(self._rect.x), gradient_y, - int(self._rect.width), gradient_h, rl.BLANK, rl.BLACK) - - # draw white ring around dm icon to indicate progress - ring_thickness = 8 - - # DM icon is 120x120, positioned on opposite side from driver - dm_size = 120 - is_rhd = self._dialog.driver_state_renderer._is_rhd - dm_center_x = (self._rect.x + dm_size / 2 + 8) if is_rhd else (self._rect.x + self._rect.width - dm_size / 2 - 8) - dm_center_y = self._rect.y + dm_size / 2 + 8 - icon_edge_radius = dm_size / 2 - outer_radius = icon_edge_radius + 1 # 2px outward from icon edge - inner_radius = outer_radius - ring_thickness # Inset by ring_thickness - start_angle = 90.0 # Start from bottom - end_angle = start_angle + self._progress.x * 360.0 # Clockwise - - # Fade in alpha - current_angle = end_angle - start_angle - alpha = int(np.interp(current_angle, [0.0, 45.0], [0, 255])) - - # White to green - color_t = np.clip(np.interp(current_angle, [45.0, 360.0], [0.0, 1.0]), 0.0, 1.0) - r = int(np.interp(color_t, [0.0, 1.0], [255, 0])) - g = int(np.interp(color_t, [0.0, 1.0], [255, 255])) - b = int(np.interp(color_t, [0.0, 1.0], [255, 64])) - ring_color = rl.Color(r, g, b, alpha) - - rl.draw_ring( - rl.Vector2(dm_center_x, dm_center_y), - inner_radius, - outer_radius, - start_angle, - end_angle, - 36, - ring_color, - ) - - if self._dialog._camera_view.frame: - self._back_button.render(rl.Rectangle( - self._rect.x + 8, - self._rect.y + self._rect.height - self._back_button.rect.height, - self._back_button.rect.width, - self._back_button.rect.height, - )) - - self._good_button.render(rl.Rectangle( - self._rect.x + self._rect.width - self._good_button.rect.width - 8, - self._rect.y + self._rect.height - self._good_button.rect.height, - self._good_button.rect.width, - self._good_button.rect.height, - )) - - # rounded border - rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height)) - rl.draw_rectangle_rounded_lines_ex(self._rect, 0.2 * 1.02, 10, 50, rl.BLACK) - rl.end_scissor_mode() - - -class TrainingGuideRecordFront(NavScroller): - def __init__(self, continue_callback: Callable[[], None]): - super().__init__() - - def on_accept(): - ui_state.params.put_bool("RecordFront", True) - continue_callback() - - def on_decline(): - ui_state.params.put_bool("RecordFront", False) - continue_callback() - - self._accept_button = BigConfirmationCircleButton("allow data uploading", gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 64, 64), - on_accept, exit_on_confirm=False) - - self._decline_button = BigConfirmationCircleButton("no, don't upload", gui_app.texture("icons_mici/setup/cancel.png", 64, 64), on_decline, - exit_on_confirm=False) - - self._scroller.add_widgets([ - GreyBigButton("driver camera data", "do you want to share video data for training?", - gui_app.texture("icons_mici/setup/green_dm.png", 64, 64)), - GreyBigButton("", "Sharing your data with comma helps improve openpilot and sunnypilot for everyone."), - self._accept_button, - self._decline_button, - ]) - - -class TrainingGuideAttentionNotice(Scroller): - def __init__(self, continue_callback: Callable[[], None]): - super().__init__() - - continue_button = BigPillButton("next") - continue_button.set_click_callback(continue_callback) - - self._scroller.add_widgets([ - GreyBigButton("what is sunnypilot?", "scroll to continue", - gui_app.texture("icons_mici/setup/green_info.png", 64, 64)), - GreyBigButton("", "1. sunnypilot is a driver assistance system."), - GreyBigButton("", "2. You must pay attention at all times."), - GreyBigButton("", "3. You must be ready to take over at any time."), - GreyBigButton("", "4. You are fully responsible for driving the car."), - continue_button, - ]) - - -class TrainingGuide(NavWidget): - def __init__(self, completed_callback: Callable[[], None]): - super().__init__() - - self._steps = [ - TrainingGuideAttentionNotice(continue_callback=lambda: gui_app.push_widget(self._steps[1])), - TrainingGuidePreDMTutorial(continue_callback=lambda: gui_app.push_widget(self._steps[2])), - TrainingGuideDMTutorial(continue_callback=lambda: gui_app.push_widget(self._steps[3])), - TrainingGuideRecordFront(continue_callback=completed_callback), - ] - - self._child(self._steps[0]) - self._steps[0].set_enabled(lambda: self.enabled and not self.is_dismissing) # for nav stack - - def _render(self, _): - self._steps[0].render(self._rect) - - -class QRCodeWidget(Widget): - def __init__(self, url: str, size: int = 170): - super().__init__() - self.set_rect(rl.Rectangle(0, 0, size, size)) - self._size = size - self._qr_texture = make_texture(url, inverted=True) - - def _render(self, _): - if self._qr_texture: - scale = self._size / self._qr_texture.height - rl.draw_texture_ex(self._qr_texture, rl.Vector2(round(self._rect.x), round(self._rect.y)), 0.0, scale, rl.WHITE) - - def __del__(self): - if self._qr_texture and self._qr_texture.id != 0: - rl.unload_texture(self._qr_texture) - - -class TermsPage(Scroller): - def __init__(self, on_accept, on_decline): - super().__init__() - - self._accept_button = BigConfirmationCircleButton("accept\nterms", gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 64, 64), on_accept) - self._decline_button = BigConfirmationCircleButton("decline &\nuninstall", gui_app.texture("icons_mici/setup/cancel.png", 64, 64), on_decline, - red=True, exit_on_confirm=False) - - self._terms_header = GreyBigButton("terms of\nservice", "scroll to continue", - gui_app.texture("icons_mici/setup/green_info.png", 64, 64)) - self._must_accept_card = GreyBigButton("", "You must accept the Terms of Service to use sunnypilot.") - - self._scroller.add_widgets([ - self._terms_header, - GreyBigButton("swipe for QR code", "or go to https://sunnypilot.ai/terms", - gui_app.texture("icons_mici/setup/small_slider/slider_arrow.png", 64, 56, flip_x=True)), - QRCodeWidget("https://sunnypilot.ai/terms"), - self._must_accept_card, - self._accept_button, - self._decline_button, - ]) - - def _render(self, _): - rl.draw_rectangle_rec(self._rect, rl.BLACK) - super()._render(_) - - -class OnboardingWindow(Widget): - def __init__(self, completed_callback: Callable[[], None]): - super().__init__() - self._completed_callback = completed_callback - self._accepted_terms: bool = (ui_state.params.get("HasAcceptedTerms") == terms_version and - ui_state.params.get("HasAcceptedTermsSP") == terms_version_sp) - self._training_done: bool = ui_state.params.get("CompletedTrainingVersion") == training_version - self._sunnylink_consent_done: bool = ui_state.params.get("CompletedSunnylinkConsentVersion") in { - sunnylink_consent_version, sunnylink_consent_declined - } - - self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - - # Windows — all pushed onto nav stack, _terms is always rendered as base layer - self._terms = TermsPage(on_accept=self._on_terms_accepted, on_decline=self._on_uninstall) - self._terms.set_enabled(lambda: self.enabled) # for nav stack - - self._sunnylink_consent = SunnylinkConsentPage( - on_accept=self._on_sunnylink_accepted, - on_decline=self._on_sunnylink_declined, - ) - - self._training_guide = TrainingGuide(completed_callback=self._on_completed_training) - self._training_guide.set_enabled(lambda: self.enabled) # for nav stack - - self._needs_initial_push = False - - def _on_uninstall(self): - ui_state.params.put_bool("DoUninstall", True, block=True) - - def show_event(self): - super().show_event() - device.set_override_interactive_timeout(300) - device.set_offroad_brightness(100) - self._needs_initial_push = True - - def hide_event(self): - super().hide_event() - # FIXME: when nav stack sends hide event to widget 2 below on push, this needs to be moved - device.set_override_interactive_timeout(None) - device.set_offroad_brightness(None) - - @property - def completed(self) -> bool: - return self._accepted_terms and self._sunnylink_consent_done and self._training_done - - def close(self): - ui_state.params.put_bool("IsDriverViewEnabled", False) - self._completed_callback() - - def _on_terms_accepted(self): - ui_state.params.put("HasAcceptedTerms", terms_version, block=True) - ui_state.params.put("HasAcceptedTermsSP", terms_version_sp, block=True) - self._accepted_terms = True - if not self._sunnylink_consent_done: - gui_app.push_widget(self._sunnylink_consent) - elif not self._training_done: - gui_app.push_widget(self._training_guide) - else: - self.close() - - def _on_sunnylink_accepted(self): - ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version, block=True) - ui_state.params.put_bool("SunnylinkEnabled", True) - self._sunnylink_consent_done = True - if not self._training_done: - gui_app.push_widget(self._training_guide) - else: - self.close() - - def _on_sunnylink_declined(self): - ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_declined, block=True) - ui_state.params.put_bool("SunnylinkEnabled", False) - self._sunnylink_consent_done = True - if not self._training_done: - gui_app.push_widget(self._training_guide) - else: - self.close() - - def _on_completed_training(self): - ui_state.params.put("CompletedTrainingVersion", training_version, block=True) - self._training_done = True - self.close() - - def _render(self, _): - rl.draw_rectangle_rec(self._rect, rl.BLACK) - - # Deferred from show_event to avoid nested push_widget re-enable bug - if self._needs_initial_push: - self._needs_initial_push = False - if self._accepted_terms and not self._sunnylink_consent_done: - gui_app.push_widget(self._sunnylink_consent) - elif self._accepted_terms and self._sunnylink_consent_done and not self._training_done: - gui_app.push_widget(self._training_guide) - - self._terms.render(self._rect) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/developer.py b/openpilot/selfdrive/ui/mici/layouts/settings/developer.py deleted file mode 100644 index 3c1121953..000000000 --- a/openpilot/selfdrive/ui/mici/layouts/settings/developer.py +++ /dev/null @@ -1,204 +0,0 @@ -from collections.abc import Callable -from openpilot.common.time_helpers import system_time_valid -from openpilot.system.ui.widgets.scroller import NavScroller -from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle, BigParamControl, BigCircleParamControl, GreyBigButton -from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigInputDialog, BigConfirmationCircleButton -from openpilot.system.ui.lib.application import gui_app -from openpilot.selfdrive.ui.layouts.settings.common import restart_needed_callback -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.selfdrive.ui.widgets.ssh_key import SshKeyFetcher - - -class AlphaLongConfirmPage(NavScroller): - def __init__(self, on_confirm: Callable[[], None]): - super().__init__() - - accept = BigConfirmationCircleButton("enable alpha\nlongitudinal", - gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 64, 64), - lambda: self.dismiss(on_confirm)) - - self._scroller.add_widgets([ - GreyBigButton("enabling alpha longitudinal", "scroll to continue", - gui_app.texture("icons_mici/setup/warning.png", 64, 64)), - GreyBigButton("", "WARNING: alpha longitudinal control may disable Automatic Emergency Braking (AEB)"), - GreyBigButton("", "On this car, openpilot defaults to the stock system's built-in ACC."), - GreyBigButton("", "Enabling this will switch to openpilot longitudinal control."), - accept, - ]) - - -class DeveloperLayoutMici(NavScroller): - def __init__(self): - super().__init__() - self._ssh_fetcher = SshKeyFetcher(ui_state.params) - - def github_username_callback(username: str): - if username: - self._ssh_keys_btn.set_value("Loading...") - self._ssh_keys_btn.set_enabled(False) - - def on_response(error): - self._ssh_keys_btn.set_enabled(True) - if error is None: - self._ssh_keys_btn.set_value(username) - else: - self._ssh_keys_btn.set_value("Not set") - gui_app.push_widget(BigDialog("", error)) - - self._ssh_fetcher.fetch(username, on_response) - else: - self._ssh_fetcher.clear() - self._ssh_keys_btn.set_value("Not set") - - def ssh_keys_callback(): - github_username = ui_state.params.get("GithubUsername") or "" - dlg = BigInputDialog("enter GitHub username...", github_username, minimum_length=0, confirm_callback=github_username_callback) - if not system_time_valid(): - dlg = BigDialog("", "Please connect to Wi-Fi to fetch your key.") - gui_app.push_widget(dlg) - return - gui_app.push_widget(dlg) - - txt_ssh = gui_app.texture("icons_mici/settings/developer/ssh.png", 56, 64) - github_username = ui_state.params.get("GithubUsername") or "" - self._ssh_keys_btn = BigButton("SSH keys", "Not set" if not github_username else github_username, icon=txt_ssh, - description="Grant SSH access to all public keys in your GitHub settings. Only enter your own username.") - self._ssh_keys_btn.set_click_callback(ssh_keys_callback) - - # adb, ssh, ssh keys, debug mode, joystick debug mode, longitudinal maneuver mode, ip address - # ******** Main Scroller ******** - self._adb_toggle = BigCircleParamControl(gui_app.texture("icons_mici/adb_short.png", 82, 82), "AdbEnabled", icon_offset=(0, 12), - description="Use Android Debug Bridge (ADB) over USB or the network.", title="enable ADB") - self._ssh_toggle = BigCircleParamControl(gui_app.texture("icons_mici/ssh_short.png", 82, 82), "SshEnabled", icon_offset=(0, 12), - description="Access the device remotely using your SSH keys.", title="enable SSH") - self._joystick_toggle = BigToggle("joystick debug\nmode", initial_state=ui_state.params.get_bool("JoystickDebugMode"), - toggle_callback=self._on_joystick_debug_mode, description="Control the car with a joystick for debugging.") - self._long_maneuver_toggle = BigToggle("longitudinal maneuver mode", initial_state=ui_state.params.get_bool("LongitudinalManeuverMode"), - toggle_callback=self._on_long_maneuver_mode, - description="Run longitudinal maneuvers for testing gas and brake control.") - self._lat_maneuver_toggle = BigToggle("lateral maneuver mode", initial_state=ui_state.params.get_bool("LateralManeuverMode"), - toggle_callback=self._on_lat_maneuver_mode, - description="Run lateral maneuvers for testing steering control.") - self._alpha_long_toggle = BigToggle("alpha longitudinal", initial_state=ui_state.params.get_bool("AlphaLongitudinalEnabled"), - toggle_callback=self._on_alpha_long_enabled, - description="Use alpha openpilot longitudinal control instead of stock ACC. This may disable Automatic Emergency " + - "Braking (AEB).") - self._navigation_toggle = BigParamControl("navigation (alpha)", "NavigationEnabled", - description="Turn-by-turn navigation with a route map. Set a destination from comma connect.") - self._debug_mode_toggle = BigParamControl("ui debug mode", "ShowDebugInfo", - toggle_callback=lambda checked: (gui_app.set_show_touches(checked), gui_app.set_show_fps(checked)), - description="Show touch locations and the UI frame rate.") - - self._scroller.add_widgets([ - self._adb_toggle, - self._ssh_toggle, - self._ssh_keys_btn, - self._joystick_toggle, - self._long_maneuver_toggle, - self._lat_maneuver_toggle, - self._alpha_long_toggle, - self._debug_mode_toggle, - self._navigation_toggle, - ]) - - # Toggle lists - self._refresh_toggles = ( - ("AdbEnabled", self._adb_toggle), - ("SshEnabled", self._ssh_toggle), - ("JoystickDebugMode", self._joystick_toggle), - ("LongitudinalManeuverMode", self._long_maneuver_toggle), - ("LateralManeuverMode", self._lat_maneuver_toggle), - ("AlphaLongitudinalEnabled", self._alpha_long_toggle), - ("ShowDebugInfo", self._debug_mode_toggle), - ("NavigationEnabled", self._navigation_toggle), - ) - onroad_blocked_toggles = (self._adb_toggle, self._joystick_toggle) - release_blocked_toggles = (self._joystick_toggle, self._long_maneuver_toggle, self._lat_maneuver_toggle, self._alpha_long_toggle, self._navigation_toggle) - engaged_blocked_toggles = (self._long_maneuver_toggle, self._lat_maneuver_toggle, self._alpha_long_toggle) - - # Hide non-release toggles on release builds - for item in release_blocked_toggles: - item.set_visible(not ui_state.is_release) - - # Disable toggles that require offroad - for item in onroad_blocked_toggles: - item.set_enabled(lambda: ui_state.is_offroad()) - - # Disable toggles that require not engaged - for item in engaged_blocked_toggles: - item.set_enabled(lambda: not ui_state.engaged) - - # Set initial state - if ui_state.params.get_bool("ShowDebugInfo"): - gui_app.set_show_touches(True) - gui_app.set_show_fps(True) - - ui_state.add_offroad_transition_callback(self._update_toggles) - - def _update_state(self): - super()._update_state() - self._ssh_fetcher.update() - - def show_event(self): - super().show_event() - self._update_toggles() - - def _update_toggles(self): - ui_state.update_params() - - # CP gating - if ui_state.CP is not None: - alpha_avail = ui_state.CP.alphaLongitudinalAvailable - if not alpha_avail or ui_state.is_release: - self._alpha_long_toggle.set_visible(False) - ui_state.params.remove("AlphaLongitudinalEnabled") - else: - self._alpha_long_toggle.set_visible(True) - - long_man_enabled = ui_state.has_longitudinal_control and ui_state.is_offroad() - self._long_maneuver_toggle.set_enabled(long_man_enabled) - self._lat_maneuver_toggle.set_enabled(ui_state.is_offroad()) - else: - self._long_maneuver_toggle.set_enabled(False) - self._lat_maneuver_toggle.set_enabled(False) - self._alpha_long_toggle.set_visible(False) - - # Refresh toggles from params to mirror external changes - for key, item in self._refresh_toggles: - item.set_checked(ui_state.params.get_bool(key)) - - def _on_joystick_debug_mode(self, state: bool): - ui_state.params.put_bool("JoystickDebugMode", state, block=True) - ui_state.params.put_bool("LongitudinalManeuverMode", False, block=True) - self._long_maneuver_toggle.set_checked(False) - ui_state.params.put_bool("LateralManeuverMode", False, block=True) - self._lat_maneuver_toggle.set_checked(False) - - def _on_long_maneuver_mode(self, state: bool): - ui_state.params.put_bool("LongitudinalManeuverMode", state, block=True) - ui_state.params.put_bool("JoystickDebugMode", False, block=True) - self._joystick_toggle.set_checked(False) - ui_state.params.put_bool("LateralManeuverMode", False, block=True) - self._lat_maneuver_toggle.set_checked(False) - restart_needed_callback() - - def _on_lat_maneuver_mode(self, state: bool): - ui_state.params.put_bool("LateralManeuverMode", state, block=True) - ui_state.params.put_bool("JoystickDebugMode", False, block=True) - self._joystick_toggle.set_checked(False) - ui_state.params.put_bool("LongitudinalManeuverMode", False, block=True) - self._long_maneuver_toggle.set_checked(False) - restart_needed_callback() - - def _on_alpha_long_enabled(self, state: bool): - def do_toggle(_state: bool): - ui_state.params.put_bool("AlphaLongitudinalEnabled", _state, block=True) - restart_needed_callback() - self._update_toggles() - - if state: - # Don't show enabled state until confirm - self._alpha_long_toggle.set_checked(False) - gui_app.push_widget(AlphaLongConfirmPage(lambda: do_toggle(True))) - else: - do_toggle(False) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/device.py b/openpilot/selfdrive/ui/mici/layouts/settings/device.py deleted file mode 100644 index c038f5fdd..000000000 --- a/openpilot/selfdrive/ui/mici/layouts/settings/device.py +++ /dev/null @@ -1,223 +0,0 @@ -import os -import pyray as rl -from collections.abc import Callable -from typing import Union - -from openpilot.common.basedir import BASEDIR -from openpilot.common.params import Params -from openpilot.common.time_helpers import system_time_valid -from openpilot.system.ui.widgets.scroller import NavRawScrollPanel, NavScroller -from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigCircleButton -from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog -from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog -from openpilot.selfdrive.ui.mici.onroad.cabin_camera_dialog import CabinCameraDialog -from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide, TermsPage -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos -from openpilot.system.ui.lib.multilang import tr -from openpilot.system.ui.widgets import Widget -from openpilot.selfdrive.ui.ui_state import device, ui_state -from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.system.ui.widgets.html_render import HtmlRenderer -from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID - - -class ReviewTermsPage(TermsPage, NavScroller): - """TermsPage with NavWidget swipe-to-dismiss for reviewing in device settings.""" - def __init__(self): - super().__init__(on_accept=self.dismiss, on_decline=self.dismiss) - self._terms_header.set_visible(False) - self._must_accept_card.set_visible(False) - self._accept_button.set_visible(False) - self._decline_button.set_visible(False) - - -class ReviewTrainingGuide(TrainingGuide): - def show_event(self): - super().show_event() - device.set_override_interactive_timeout(300) - - def hide_event(self): - super().hide_event() - device.set_override_interactive_timeout(None) - ui_state.params.put_bool("IsDriverViewEnabled", False) - - -class MiciFccModal(NavRawScrollPanel): - def __init__(self, file_path: str | None = None, text: str | None = None): - super().__init__() - self._content = HtmlRenderer(file_path=file_path, text=text) - self._fcc_logo = gui_app.texture("icons_mici/settings/device/fcc_logo.png", 76, 64) - - def _render(self, rect: rl.Rectangle): - content_height = self._content.get_total_height(int(rect.width)) - content_height += self._fcc_logo.height + 20 - - scroll_content_rect = rl.Rectangle(rect.x, rect.y, rect.width, content_height) - scroll_offset = round(self._scroll_panel.update(rect, scroll_content_rect.height)) - - fcc_pos = rl.Vector2(rect.x + 20, rect.y + 20 + scroll_offset) - - scroll_content_rect.y += scroll_offset + self._fcc_logo.height + 20 - self._content.render(scroll_content_rect) - - rl.draw_texture_ex(self._fcc_logo, fcc_pos, 0.0, 1.0, rl.WHITE) - - -def _engaged_confirmation_click(callback: Callable, action_text: str, icon: rl.Texture, exit_on_confirm: bool = True, red: bool = False): - if not ui_state.engaged: - def confirm_callback(): - # Check engaged again in case it changed while the dialog was open - # TODO: if true, we stay on the dialog if not exit_on_confirm until normal onroad timeout - if not ui_state.engaged: - callback() - - gui_app.push_widget(BigConfirmationDialog(f"slide to\n{action_text.lower()}", icon, confirm_callback, exit_on_confirm=exit_on_confirm, red=red)) - else: - gui_app.push_widget(BigDialog("", f"Disengage to {action_text}")) - - -class EngagedConfirmationCircleButton(BigCircleButton): - def __init__(self, title: str, icon: rl.Texture, callback: Callable[[], None], exit_on_confirm: bool = True, - red: bool = False, icon_offset: tuple[int, int] = (0, 0), *, description: str = ""): - super().__init__(icon, red, icon_offset, description=description, title=title) - self.set_click_callback(lambda: _engaged_confirmation_click(callback, title, icon, exit_on_confirm=exit_on_confirm, red=red)) - - -class EngagedConfirmationButton(BigButton): - def __init__(self, text: str, action_text: str, icon: rl.Texture, callback: Callable[[], None], - exit_on_confirm: bool = True, red: bool = False, *, description: str = "", - description_icon: Union[rl.Texture, None] = None): - super().__init__(text, "", icon, description=description, description_icon=description_icon) - self.set_click_callback(lambda: _engaged_confirmation_click(callback, action_text, icon, exit_on_confirm=exit_on_confirm, red=red)) - - -class DeviceInfoLayoutMici(Widget): - def __init__(self): - super().__init__() - - self.set_rect(rl.Rectangle(0, 0, 360, 180)) - - params = Params() - subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)) - max_width = int(self._rect.width - 20) - self._dongle_id_label = UnifiedLabel("device ID", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) - self._dongle_id_text_label = UnifiedLabel(params.get("DongleId") or 'N/A', 32, max_width=max_width, text_color=subheader_color, - font_weight=FontWeight.ROMAN, wrap_text=False) - - self._serial_number_label = UnifiedLabel("serial", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) - self._serial_number_text_label = UnifiedLabel(params.get("HardwareSerial") or 'N/A', 32, max_width=max_width, text_color=subheader_color, - font_weight=FontWeight.ROMAN, wrap_text=False) - - def _render(self, _): - self._dongle_id_label.set_position(self._rect.x + 20, self._rect.y - 10) - self._dongle_id_label.render() - - self._dongle_id_text_label.set_position(self._rect.x + 20, self._rect.y + 68 - 25) - self._dongle_id_text_label.render() - - self._serial_number_label.set_position(self._rect.x + 20, self._rect.y + 114 - 30) - self._serial_number_label.render() - - self._serial_number_text_label.set_position(self._rect.x + 20, self._rect.y + 161 - 25) - self._serial_number_text_label.render() - - -class PairBigButton(BigButton): - def __init__(self): - super().__init__("pair", "connect.comma.ai", gui_app.texture("icons_mici/settings/comma_icon.png", 33, 60)) - - def _get_label_font_size(self): - return 64 - - def _update_state(self): - super()._update_state() - - if ui_state.prime_state.is_paired(): - self.set_text("paired") - if ui_state.prime_state.is_prime(): - self.set_value("subscribed") - else: - self.set_value("upgrade to prime") - else: - self.set_text("pair") - self.set_value("connect.comma.ai") - - def _handle_mouse_release(self, mouse_pos: MousePos): - super()._handle_mouse_release(mouse_pos) - - # TODO: show ad dialog when clicked if not prime - if ui_state.prime_state.is_paired(): - return - dlg: BigDialog | PairingDialog - if not system_time_valid(): - dlg = BigDialog("", tr("Please connect to Wi-Fi to complete initial pairing.")) - elif UNREGISTERED_DONGLE_ID == (ui_state.params.get("DongleId") or UNREGISTERED_DONGLE_ID): - dlg = BigDialog("", tr("Device must be registered with the comma.ai backend to pair.")) - else: - dlg = PairingDialog() - gui_app.push_widget(dlg) - - -class DeviceLayoutMici(NavScroller): - def __init__(self): - super().__init__() - - self._fcc_dialog: MiciFccModal | None = None - - def power_off_callback(): - ui_state.params.put_bool("DoShutdown", True, block=True) - - def reboot_callback(): - ui_state.params.put_bool("DoReboot", True, block=True) - - def reset_calibration_callback(): - params = ui_state.params - params.remove("CalibrationParams") - params.remove("LiveTorqueParameters") - params.remove("LiveParametersV2") - params.remove("LiveDelay") - params.put_bool("OnroadCycleRequested", True, block=True) - - reset_calibration_btn = EngagedConfirmationButton("reset calibration", "reset", gui_app.texture("icons_mici/settings/device/lkas.png", 122, 64), - reset_calibration_callback, - description="Mount the device within 4° left or right and 5° up or 9° down. openpilot calibrates " + - "continuously; resetting is rarely needed. Resetting clears learned calibration.") - - reboot_btn = EngagedConfirmationCircleButton("reboot", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70), - reboot_callback, exit_on_confirm=False) - - self._power_off_btn = EngagedConfirmationCircleButton("power off", gui_app.texture("icons_mici/settings/device/power.png", 64, 66), - power_off_callback, exit_on_confirm=False, red=True) - self._power_off_btn.set_visible(lambda: not ui_state.ignition) - - regulatory_btn = BigButton("regulatory info", "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64)) - regulatory_btn.set_click_callback(self._on_regulatory) - - cabin_cam_btn = BigButton("driver\ncamera preview", "", gui_app.texture("icons_mici/settings/device/cameras.png", 64, 64), - description="Preview the cabin camera to check driver monitoring visibility. The vehicle must be off.") - cabin_cam_btn.set_click_callback(lambda: gui_app.push_widget(CabinCameraDialog())) - cabin_cam_btn.set_enabled(lambda: ui_state.is_offroad()) - - review_training_guide_btn = BigButton("review\ntraining guide", "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64)) - review_training_guide_btn.set_click_callback(lambda: gui_app.push_widget(ReviewTrainingGuide(completed_callback=lambda: gui_app.pop_widgets_to(self)))) - review_training_guide_btn.set_enabled(lambda: ui_state.is_offroad()) - - terms_btn = BigButton("terms &\nconditions", "", gui_app.texture("icons_mici/settings/device/info.png", 64, 64)) - terms_btn.set_click_callback(lambda: gui_app.push_widget(ReviewTermsPage())) - - self._scroller.add_widgets([ - DeviceInfoLayoutMici(), - PairBigButton(), - review_training_guide_btn, - cabin_cam_btn, - terms_btn, - regulatory_btn, - reset_calibration_btn, - reboot_btn, - self._power_off_btn, - ]) - - def _on_regulatory(self): - if not self._fcc_dialog: - self._fcc_dialog = MiciFccModal(os.path.join(BASEDIR, "openpilot/selfdrive/assets/offroad/mici_fcc.html")) - gui_app.push_widget(self._fcc_dialog) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/device/__init__.py b/openpilot/selfdrive/ui/mici/layouts/settings/device/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/device/prime.py b/openpilot/selfdrive/ui/mici/layouts/settings/device/prime.py deleted file mode 100644 index 9a914dff4..000000000 --- a/openpilot/selfdrive/ui/mici/layouts/settings/device/prime.py +++ /dev/null @@ -1,171 +0,0 @@ -import pyray as rl -import time - -from openpilot.common.api import Api -from openpilot.common.swaglog import cloudlog -from openpilot.common.params import Params -from openpilot.selfdrive.ui.mici.widgets.button import BigButton, GreyBigButton -from openpilot.selfdrive.ui.mici.widgets.info import InfoLayoutMici -from openpilot.selfdrive.ui.mici.widgets.qr import QR -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.widgets.scroller import NavScroller - - -class PairingInfoLayout(InfoLayoutMici): - def __init__(self): - super().__init__("account", "", "status", "") - self._commacare_badge = gui_app.texture("icons_mici/settings/device/commacare.png", 27, 32) - self._provider_icons = {provider: gui_app.texture(f"icons_mici/settings/device/{provider}.png", 32, 32) - for provider in ("github", "google", "apple")} - - def _update_state(self): - super()._update_state() - self.subtext1.set_text(ui_state.prime_state.get_pairing_account()) - self.subtext2.set_text(self._get_prime_status()) - self._provider_icon = self._provider_icons.get(ui_state.prime_state.get_pairing_provider()) - self._show_commacare = ui_state.prime_state.has_commacare() - - @staticmethod - def _get_prime_status() -> str: - if ui_state.prime_state.is_prime(): - return "prime" if ui_state.prime_state.is_full_prime() else "prime lite" - return "not subscribed" - - def _layout(self): - super()._layout() - provider_offset = self._provider_icon.width + 14 if self._provider_icon else 0 - commacare_offset = self._commacare_badge.width + 14 if self._show_commacare else 0 - self.subtext1.set_position(self.subtext1.rect.x + provider_offset, self.subtext1.rect.y) - self.subtext1.set_max_width(int(self._rect.width - provider_offset - (40 if self._provider_icon else 20))) - self.subtext2.set_position(self.subtext2.rect.x + commacare_offset, self.subtext2.rect.y) - self.subtext2.set_max_width(int(self._rect.width - commacare_offset - (40 if self._show_commacare else 20))) - - def _render(self, rect: rl.Rectangle): - super()._render(rect) - - if self._provider_icon: - icon_pos = rl.Vector2(self._rect.x + 20, self.subtext1.rect.y + (self.subtext1.rect.height - self._provider_icon.height) / 2) - rl.draw_texture_v(self._provider_icon, icon_pos, self._subheader_color) - if self._show_commacare: - icon_pos = rl.Vector2(self._rect.x + 20, self.subtext2.rect.y + (self.subtext2.rect.height - self._commacare_badge.height) / 2 + 4) - rl.draw_texture_v(self._commacare_badge, icon_pos, rl.WHITE) - - -class PrimeManagementScroller(NavScroller): - """Prime subscription management and upgrade information.""" - - def __init__(self): - super().__init__() - self._params = Params() - - can_claim_trial = ui_state.prime_state.can_claim_prime_trial() - - self._prime_icon = gui_app.texture("icons_mici/settings/device/green_cell.png", 64, 64) - self._phone_icon = gui_app.texture("icons_mici/settings/device/phone.png", 85, 64) - self._prime_adverts = [ - QR(self._get_prime_url()), - GreyBigButton( - "try prime for\n30 days" if can_claim_trial else "upgrade to prime", - "scan to claim trial" if can_claim_trial else "scan to manage\nprime status", - self._prime_icon - ), - GreyBigButton("", "prime adds 24/7 LTE to device and 1 year of cloud storage in connect.",), - GreyBigButton("", "prime lets you view live video and GPS location remotely in connect.",), - GreyBigButton("", "prime also includes commacare extended device warranty.",), - ] - self._prime_management = [ - QR(self._get_prime_url()), - GreyBigButton( - "manage prime", - "scan to open\ndevice prime settings", - self._phone_icon - ), - ] - self._scroller.add_widgets(self._prime_management if ui_state.prime_state.is_prime() else self._prime_adverts) - - def _get_prime_url(self) -> str: - if dongle_id := self._params.get("DongleId"): - return f"https://connect.comma.ai/{dongle_id}/prime" - return "https://connect.comma.ai" - - def _update_state(self): - super()._update_state() - if not ui_state.prime_state.is_paired(): - self.dismiss() - - -class PrimeScroller(NavScroller): - """Pairing flow or paired account information with access to prime management.""" - - QR_REFRESH_INTERVAL = 300 # 5 minutes in seconds - - def __init__(self): - super().__init__() - self._params = Params() - self.initial_is_paired = ui_state.prime_state.is_paired() - self._last_pairing_qr_generation = float("-inf") - - self._manage_icon = gui_app.texture("icons_mici/settings/comma_icon.png", 33, 60) - if not ui_state.prime_state.is_prime(): - title = "claim\nprime trial" if ui_state.prime_state.can_claim_prime_trial() else "upgrade\nto prime" - self._manage_prime = BigButton(title, icon=self._manage_icon) - else: - self._manage_prime = BigButton("manage prime", icon=self._manage_icon) - self._manage_prime.set_click_callback(lambda: gui_app.push_widget(PrimeManagementScroller())) - - if self.initial_is_paired: - self._scroller.add_widgets([ - PairingInfoLayout(), - self._manage_prime, - ]) - else: - self._qr = QR(self._get_pairing_url()) - self._scroller.scroll_indicator_start_after = self._qr - self._scroller.add_widgets([ - self._qr, - GreyBigButton("finish setup", "scan QR code or visit connect.comma.ai", - gui_app.texture("icons_mici/settings/device/green_settings.png", 64, 64)), - GreyBigButton("", "connect lets you review recent driving footage and bookmark events."), - ]) - - def _get_pairing_url(self) -> str: - try: - dongle_id = self._params.get("DongleId") or "" - token = Api(dongle_id).get_token({'pair': True}) - except Exception as e: - cloudlog.warning(f"Failed to get pairing token: {e}") - token = "" - return f"https://connect.comma.ai/?pair={token}" - - def _update_layout_rects(self): - super()._update_layout_rects() - if not self.initial_is_paired: - self._qr.set_rect(rl.Rectangle(self._qr.rect.x, self._qr.rect.y, self._rect.height, self._rect.height)) - - def _render(self, rect: rl.Rectangle): - if not self.initial_is_paired: - current_time = time.monotonic() - if current_time - self._last_pairing_qr_generation >= self.QR_REFRESH_INTERVAL: - self._qr._url = self._get_pairing_url() - if self._qr._texture and self._qr._texture.id != 0: - rl.unload_texture(self._qr._texture) - self._qr._texture = self._qr._generate_qr_code() - self._last_pairing_qr_generation = current_time - super()._render(rect) - - def _update_state(self): - super()._update_state() - if self.initial_is_paired != ui_state.prime_state.is_paired() and gui_app.get_active_widget() is self: - self.dismiss() - - -if __name__ == "__main__": - gui_app.init_window("pairing device") - pairing = PrimeScroller() - gui_app.push_widget(pairing) - try: - for _ in gui_app.render(): - pass - finally: - del pairing diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/initial_cruise_speed.py b/openpilot/selfdrive/ui/mici/layouts/settings/initial_cruise_speed.py deleted file mode 100644 index e36673889..000000000 --- a/openpilot/selfdrive/ui/mici/layouts/settings/initial_cruise_speed.py +++ /dev/null @@ -1,160 +0,0 @@ -import math -import pyray as rl -from collections.abc import Callable - -from openpilot.selfdrive.ui.layouts.settings.initial_cruise_speed import get_speed_options, set_initial_cruise_speed -from openpilot.selfdrive.ui.mici.widgets.button import BigButton, LABEL_COLOR -from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialogBase -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, FONT_SCALE -from openpilot.system.ui.lib.multilang import tr -from openpilot.system.ui.lib.scroll_panel2 import AUTO_SCROLL_TC, GuiScrollPanel2, ScrollState -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 UnifiedLabel - -ITEM_DRAG_DISTANCE = 120 # px of drag per value -ITEM_GAP = 20 # px between adjacent values -VALUE_CENTER_Y = 102 -VISIBLE_VALUES = 3 # on each side of center value - -# value styles -CENTER_FONT_SIZE = 120 -CENTER_LETTER_SPACING = -0.03 -SIDE_FONT_SIZE = 96 -SIDE_LETTER_SPACING = -0.01 - - -def lerp(start: float, end: float, t: float) -> float: - return start + (end - start) * t - - -class SpeedScroller(Widget): - def __init__(self, change_callback: Callable[[], None]): - super().__init__() - self._change_callback = change_callback - - self._center_font = gui_app.font(FontWeight.BOLD) - self._side_font = gui_app.font(FontWeight.ROMAN) - - self._scroll_panel = GuiScrollPanel2(horizontal=True) - self._scroll_panel.set_enabled(lambda: self.enabled) - self.set_touch_valid_callback(self._scroll_panel.is_touch_valid) - self.set_click_callback(self._on_tap) - - self._options: list[int] = [] - self._center_widths: list[float] = [] - self._side_widths: list[float] = [] - self._locked_idx: int | None = None # value the scroller is easing onto, None while the user drags - - def show_event(self): - super().show_event() - self._options, self._locked_idx = get_speed_options() - self._center_widths = [measure_text_cached(self._center_font, str(option), CENTER_FONT_SIZE, - CENTER_LETTER_SPACING * CENTER_FONT_SIZE * FONT_SCALE).x for option in self._options] - self._side_widths = [measure_text_cached(self._side_font, str(option), SIDE_FONT_SIZE, - SIDE_LETTER_SPACING * SIDE_FONT_SIZE * FONT_SCALE).x for option in self._options] - - self._scroll_panel.set_offset(-self._locked_idx * ITEM_DRAG_DISTANCE) - - def _clamp_index(self, idx: int) -> int: - return max(0, min(len(self._options) - 1, idx)) - - def _lock(self, idx: int): - self._locked_idx = self._clamp_index(idx) - set_initial_cruise_speed(self._options[self._locked_idx]) - self._change_callback() - - def _update_state(self): - super()._update_state() - - # when the finger releases, lock the value where the scroll will stop - if self._scroll_panel.state == ScrollState.MANUAL_SCROLL: - self._locked_idx = None - elif self._locked_idx is None and self._scroll_panel.state != ScrollState.PRESSED: - projected_offset = self._scroll_panel.get_offset() + self._scroll_panel.velocity * AUTO_SCROLL_TC - self._lock(round(-projected_offset / ITEM_DRAG_DISTANCE)) - - snap_target = -self._locked_idx * ITEM_DRAG_DISTANCE if self._locked_idx is not None else None - content_size = (len(self._options) - 1) * ITEM_DRAG_DISTANCE + self._rect.width - self._scroll_panel.update(self._rect, content_size, snap_target=snap_target) - - def _on_tap(self): - if self._locked_idx is None: - return - - # a tap on the left or the right moves one value - tap_x = gui_app.last_mouse_event.pos.x - center_x = self._rect.x + self._rect.width / 2 - tap_zone = self._center_widths[self._locked_idx] / 2 + ITEM_GAP / 2 - if tap_x > center_x + tap_zone: - self._lock(self._locked_idx + 1) - elif tap_x < center_x - tap_zone: - self._lock(self._locked_idx - 1) - - def _render(self, _): - center_x = self._rect.x + self._rect.width / 2 - current = -self._scroll_panel.get_offset() / ITEM_DRAG_DISTANCE - - # the style of each value changes from the center style to the side style over a distance of one value - blends = [min(abs(idx - current), 1.0) for idx in range(len(self._options))] - widths = [lerp(center, side, t) for center, side, t in zip(self._center_widths, self._side_widths, blends, strict=True)] - - # lay values out with a constant gap, then shift so the scroll position sits at the center - centers = [0.0] - for idx in range(1, len(widths)): - centers.append(centers[-1] + widths[idx - 1] / 2 + ITEM_GAP + widths[idx] / 2) - left_idx = max(0, min(len(widths) - 2, math.floor(current))) - shift = center_x - lerp(centers[left_idx], centers[left_idx + 1], current - left_idx) - - # only the values near the center can be on the screen. the nearest value uses the bold font - closest_idx = self._clamp_index(round(current)) - for idx in range(max(0, closest_idx - VISIBLE_VALUES), min(len(self._options), closest_idx + VISIBLE_VALUES + 1)): - t = blends[idx] - font_size = lerp(CENTER_FONT_SIZE, SIDE_FONT_SIZE, t) - spacing = lerp(CENTER_LETTER_SPACING, SIDE_LETTER_SPACING, t) * font_size * FONT_SCALE - color = rl.Color(255, 255, 255, int(255 * lerp(0.9, 0.45, t))) - # use the width of the font that draws the value. this keeps the value at its center when the font changes - if idx == closest_idx: - font, width = self._center_font, self._center_widths[idx] * font_size / CENTER_FONT_SIZE - else: - font, width = self._side_font, self._side_widths[idx] * font_size / SIDE_FONT_SIZE - position = rl.Vector2(centers[idx] + shift - width / 2, self._rect.y + VALUE_CENTER_Y - font_size * FONT_SCALE / 2) - rl.draw_text_ex(font, str(self._options[idx]), position, font_size, spacing, color) - - # gradient on left and right - rl.draw_rectangle_gradient_h(int(self._rect.x), int(self._rect.y), 100, int(self._rect.height), rl.BLACK, rl.BLANK) - rl.draw_rectangle_gradient_h(int(self._rect.x + self._rect.width - 100), int(self._rect.y), 100, int(self._rect.height), rl.BLANK, rl.BLACK) - - -class InitialCruiseSpeedPicker(BigDialogBase): - def __init__(self, change_callback: Callable[[], None]): - super().__init__() - self._unit_label = self._child(UnifiedLabel("", 36, FontWeight.BOLD, LABEL_COLOR, alignment=TextAlignment.CENTER, letter_spacing=-0.03)) - self._speed_scroller = self._child(SpeedScroller(change_callback)) - self._speed_scroller.set_enabled(lambda: self.enabled and not self.is_dismissing) - - def show_event(self): - super().show_event() - self._unit_label.set_text(tr("KPH") if ui_state.is_metric else tr("MPH")) - - def _render(self, _): - self._speed_scroller.render(self._rect) - self._unit_label.render(rl.Rectangle(self._rect.x, self._rect.y + 166, self._rect.width, self._rect.height - 166)) - - -class InitialCruiseSpeedButton(BigButton): - def __init__(self): - super().__init__("initial max speed", " ", description="Max speed when engaging openpilot.\n" + - "If you are already driving faster, your current speed is used.\n" + - "Resume restores the previous max speed.") - self._picker = InitialCruiseSpeedPicker(self.refresh) - self.set_click_callback(lambda: gui_app.push_widget(self._picker)) - self.refresh() - - def _get_label_font_size(self): - return 42 - - def refresh(self): - options, idx = get_speed_options() - self.set_value(f"{options[idx]} " + (tr("kph") if ui_state.is_metric else tr("mph"))) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/settings.py b/openpilot/selfdrive/ui/mici/layouts/settings/settings.py deleted file mode 100644 index 477bd7893..000000000 --- a/openpilot/selfdrive/ui/mici/layouts/settings/settings.py +++ /dev/null @@ -1,56 +0,0 @@ -from openpilot.common.params import Params -from openpilot.system.ui.widgets.scroller import NavScroller -from openpilot.selfdrive.ui.mici.widgets.button import BigButton -from openpilot.selfdrive.ui.mici.layouts.settings.toggles import TogglesLayoutMici -from openpilot.selfdrive.ui.mici.layouts.settings.network.network_layout import NetworkLayoutMici -from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici -from openpilot.selfdrive.ui.mici.layouts.settings.developer import DeveloperLayoutMici -from openpilot.selfdrive.ui.mici.layouts.settings.software import SoftwareLayoutMici -from openpilot.selfdrive.ui.mici.layouts.settings.firehose import FirehoseLayout -from openpilot.system.ui.lib.application import gui_app, FontWeight - - -class SettingsBigButton(BigButton): - def _get_label_font_size(self): - return 64 - - -class SettingsLayout(NavScroller): - def __init__(self): - super().__init__() - self._params = Params() - - toggles_panel = TogglesLayoutMici() - toggles_btn = SettingsBigButton("toggles", "", gui_app.texture("icons_mici/settings.png", 64, 64)) - toggles_btn.set_click_callback(lambda: gui_app.push_widget(toggles_panel)) - - network_panel = NetworkLayoutMici() - network_btn = SettingsBigButton("network", "", gui_app.texture("icons_mici/settings/network/wifi_strength_full.png", 76, 56)) - network_btn.set_click_callback(lambda: gui_app.push_widget(network_panel)) - - device_panel = DeviceLayoutMici() - device_btn = SettingsBigButton("device", "", gui_app.texture("icons_mici/settings/device_icon.png", 72, 58)) - device_btn.set_click_callback(lambda: gui_app.push_widget(device_panel)) - - software_panel = SoftwareLayoutMici() - software_btn = SettingsBigButton("software", "", gui_app.texture("icons_mici/settings/software.png", 64, 75)) - software_btn.set_click_callback(lambda: gui_app.push_widget(software_panel)) - - developer_panel = DeveloperLayoutMici() - developer_btn = SettingsBigButton("developer", "", gui_app.texture("icons_mici/settings/developer_icon.png", 64, 60)) - developer_btn.set_click_callback(lambda: gui_app.push_widget(developer_panel)) - - firehose_panel = FirehoseLayout() - firehose_btn = SettingsBigButton("firehose", "", gui_app.texture("icons_mici/settings/firehose.png", 52, 62)) - firehose_btn.set_click_callback(lambda: gui_app.push_widget(firehose_panel)) - - self._scroller.add_widgets([ - toggles_btn, - network_btn, - device_btn, - software_btn, - firehose_btn, - developer_btn, - ]) - - self._font_medium = gui_app.font(FontWeight.MEDIUM) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/software.py b/openpilot/selfdrive/ui/mici/layouts/settings/software.py deleted file mode 100644 index 15be56d36..000000000 --- a/openpilot/selfdrive/ui/mici/layouts/settings/software.py +++ /dev/null @@ -1,291 +0,0 @@ -import subprocess -import threading -import pyray as rl -from enum import IntEnum -from collections.abc import Callable - -from openpilot.common.time_helpers import system_time_valid -from openpilot.selfdrive.ui.mici.layouts.settings.device import EngagedConfirmationButton -from openpilot.selfdrive.ui.mici.widgets.button import BigButton -from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos -from openpilot.system.ui.lib.multilang import tr -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.system.ui.widgets.scroller import NavScroller - -UPDATER_TIMEOUT = 10.0 # seconds to wait for updater to respond - - -def _split_description(desc: str) -> tuple[str, str, str, str] | None: - # UpdaterCurrentDescription/UpdaterNewDescription format: "version / branch / commit / date" - parts = [p.strip() for p in desc.split(" / ")] - if len(parts) != 4: - return None - version, branch, commit, date = parts - return version, branch, commit, date - - -class UpdaterState(IntEnum): - IDLE = 0 - WAITING_FOR_UPDATER = 1 - UPDATER_RESPONDING = 2 - - -class SoftwareInfoLayoutMici(Widget): - def __init__(self): - super().__init__() - - self.set_rect(rl.Rectangle(0, 0, 360, 180)) - - subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)) - max_width = int(self._rect.width - 20) - self._version_label = UnifiedLabel("version", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) - self._version_text_label = UnifiedLabel("", 32, max_width=max_width, text_color=subheader_color, - font_weight=FontWeight.ROMAN, wrap_text=False) - - self._branch_label = UnifiedLabel("branch", 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) - self._branch_text_label = UnifiedLabel("", 32, max_width=max_width, text_color=subheader_color, - font_weight=FontWeight.ROMAN, wrap_text=False, scroll=True) - - def _update_state(self): - desc = _split_description(ui_state.params.get("UpdaterCurrentDescription") or "") - if desc is not None: - version, branch, commit, date = desc - self._version_text_label.set_text(f"{version} ({date})") - self._branch_text_label.set_text(f"{branch} ({commit})") - else: - self._version_text_label.set_text(ui_state.params.get("Version") or "N/A") - self._branch_text_label.set_text(ui_state.params.get("GitBranch") or "N/A") - - def _render(self, _): - self._version_label.set_position(self._rect.x + 20, self._rect.y - 10) - self._version_label.render() - - self._version_text_label.set_position(self._rect.x + 20, self._rect.y + 68 - 25) - self._version_text_label.render() - - self._branch_label.set_position(self._rect.x + 20, self._rect.y + 114 - 30) - self._branch_label.render() - - self._branch_text_label.set_position(self._rect.x + 20, self._rect.y + 161 - 25) - self._branch_text_label.render() - - -class CheckUpdateButton(BigButton): - UPDATER_PROC = "openpilot.system.updated.updated" - CHECK_FOR_UPDATE = "SIGUSR1" - DOWNLOAD_UPDATE = "SIGHUP" - - def __init__(self): - self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75) - self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64) - super().__init__("check for update", "", self._txt_update_icon) - - self._waiting_for_updater_t: float | None = None - self._hide_value_t: float | None = None - self._state: UpdaterState = UpdaterState.IDLE - - ui_state.add_offroad_transition_callback(self.offroad_transition) - - def offroad_transition(self): - if ui_state.is_offroad(): - self.set_enabled(True) - - def _handle_mouse_release(self, mouse_pos: MousePos): - super()._handle_mouse_release(mouse_pos) - - if not system_time_valid(): - dlg = BigDialog("", tr("Please connect to Wi-Fi to update.")) - gui_app.push_widget(dlg) - return - - self._signal_updater(self.DOWNLOAD_UPDATE if self.get_value() == "download update" else self.CHECK_FOR_UPDATE) - - def check_for_update(self): - self._signal_updater(self.CHECK_FOR_UPDATE) - - def _signal_updater(self, sig: str): - self.set_enabled(False) - self._state = UpdaterState.WAITING_FOR_UPDATER - self._hide_value_t = None - self.set_value("") - self.set_icon(self._txt_update_icon) - - def run(): - subprocess.run(f"pkill -{sig} -f {self.UPDATER_PROC}", shell=True) - - threading.Thread(target=run, daemon=True).start() - - def set_value(self, value: str): - super().set_value(value) - if value: - self.set_text("") - else: - self.set_text("check for update") - - def _update_state(self): - super()._update_state() - - if ui_state.started: - self.set_enabled(False) - return - - updater_state = ui_state.params.get("UpdaterState") or "" - failed_count = ui_state.params.get("UpdateFailedCount") or 0 - failed = int(failed_count) > 0 - - if self._state == UpdaterState.WAITING_FOR_UPDATER: - self.set_rotate_icon(True) - if updater_state != "idle": - self._state = UpdaterState.UPDATER_RESPONDING - - # Recover from updater not responding (time invalid shortly after boot) - if self._waiting_for_updater_t is None: - self._waiting_for_updater_t = rl.get_time() - - if self._waiting_for_updater_t is not None and rl.get_time() - self._waiting_for_updater_t > UPDATER_TIMEOUT: - self.set_rotate_icon(False) - self.set_value("updater failed\nto respond") - self._state = UpdaterState.IDLE - self._hide_value_t = rl.get_time() - - elif self._state == UpdaterState.UPDATER_RESPONDING: - if updater_state == "idle": - self.set_rotate_icon(False) - self._state = UpdaterState.IDLE - self._hide_value_t = rl.get_time() - else: - if self.get_value() != updater_state: - self.set_value(updater_state) - - elif self._state == UpdaterState.IDLE: - self.set_rotate_icon(False) - if failed: - self.set_enabled(True) # allow retry when failure came from updater param - if self.get_value() != "failed to update": - self.set_value("failed to update") - - elif ui_state.params.get_bool("UpdaterFetchAvailable"): - self.set_enabled(True) - if self.get_value() != "download update": - self.set_value("download update") - - elif self._hide_value_t is not None: - self.set_enabled(True) - if self.get_value() == "checking...": - self.set_value("up to date") - self.set_icon(self._txt_up_to_date_icon) - - # Hide previous text after short amount of time (up to date or failed) - if rl.get_time() - self._hide_value_t > 3.0: - self._hide_value_t = None - self.set_value("") - self.set_icon(self._txt_update_icon) - else: - if self.get_value() != "": - self.set_value("") - - if self._state != UpdaterState.WAITING_FOR_UPDATER: - self._waiting_for_updater_t = None - - -class InstallUpdateButton(BigButton): - def __init__(self): - super().__init__("install now", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70)) - self.set_visible(lambda: ui_state.is_offroad() and ui_state.params.get_bool("UpdateAvailable")) - - def _update_state(self): - super()._update_state() - - desc = _split_description(ui_state.params.get("UpdaterNewDescription") or "") - value = f"{desc[0]} ({desc[1]})" if desc is not None else "" - if self.get_value() != value: - self.set_value(value) - - def _handle_mouse_release(self, mouse_pos: MousePos): - super()._handle_mouse_release(mouse_pos) - - self.set_enabled(False) - - def run(): - ui_state.params.put_bool("DoReboot", True, block=True) - - threading.Thread(target=run, daemon=True).start() - - -class BranchSelectPage(NavScroller): - def __init__(self, on_select: Callable[[str], None]): - super().__init__() - - params = ui_state.params - current_git_branch = params.get("GitBranch") or "" - branches_str = params.get("UpdaterAvailableBranches") or "" - branches = [b for b in branches_str.split(",") if b] - - for b in [current_git_branch, "devel-staging", "devel", "nightly", "nightly-dev", "master"]: - if b in branches: - branches.remove(b) - branches.insert(0, b) - - current_target = params.get("UpdaterTargetBranch") or "" - check_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64) - - buttons = [] - for branch in branches: - btn = BigButton(branch, "", check_icon if branch == current_target else None, scroll=True) - btn.set_click_callback(lambda b=branch: self.dismiss(lambda: on_select(b))) - buttons.append(btn) - self._scroller.add_widgets(buttons) - - -class TargetBranchButton(BigButton): - def __init__(self, check_update_btn: CheckUpdateButton): - super().__init__("target branch", ui_state.params.get("UpdaterTargetBranch") or "", - description="Select the software branch to download on the next update check.") - self._check_update_btn = check_update_btn - self.set_click_callback(self._on_click) - self.set_visible(not ui_state.params.get_bool("IsTestedBranch")) - self.set_enabled(lambda: ui_state.is_offroad()) - - def _update_state(self): - super()._update_state() - - target = ui_state.params.get("UpdaterTargetBranch") or "" - if self.get_value() != target: - self.set_value(target) - - def _on_click(self): - if not ui_state.params.get("UpdaterAvailableBranches"): - gui_app.push_widget(BigDialog("", tr("Failed to get available branches. Ensure you're connected to the internet and try again."))) - return - gui_app.push_widget(BranchSelectPage(self._on_select)) - - def _on_select(self, branch: str): - ui_state.params.put("UpdaterTargetBranch", branch, block=True) - self.set_value(branch) - self._check_update_btn.check_for_update() - - -class SoftwareLayoutMici(NavScroller): - def __init__(self): - super().__init__() - - def uninstall_openpilot_callback(): - ui_state.params.put_bool("DoUninstall", True, block=True) - - uninstall_openpilot_btn = EngagedConfirmationButton("uninstall sunnypilot", "uninstall", - gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64), - uninstall_openpilot_callback, exit_on_confirm=False, - description="Remove openpilot from this device.", - description_icon=gui_app.texture("icons_mici/setup/factory_reset.png", 64, 64)) - - check_update_btn = CheckUpdateButton() - self._scroller.add_widgets([ - SoftwareInfoLayoutMici(), - check_update_btn, - InstallUpdateButton(), - TargetBranchButton(check_update_btn), - uninstall_openpilot_btn, - ]) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py b/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py deleted file mode 100644 index 406e0a77d..000000000 --- a/openpilot/selfdrive/ui/mici/layouts/settings/toggles.py +++ /dev/null @@ -1,98 +0,0 @@ -from openpilot.cereal import log - -from openpilot.system.ui.widgets.scroller import NavScroller -from openpilot.selfdrive.ui.mici.widgets.button import BigToggle, BigParamControl, BigMultiParamToggle -from openpilot.system.ui.lib.application import gui_app -from openpilot.selfdrive.ui.layouts.settings.common import restart_needed_callback -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.selfdrive.ui.mici.layouts.settings.initial_cruise_speed import InitialCruiseSpeedButton - -PERSONALITY_TO_INT = log.LongitudinalPersonality.schema.enumerants - - -class TogglesLayoutMici(NavScroller): - def __init__(self): - super().__init__() - - self._personality_toggle = BigMultiParamToggle("following distance", "LongitudinalPersonality", ["aggressive", "standard", "relaxed"], - description="Standard is recommended.\n" + - "Aggressive follows closer, with firmer gas and braking.\n" + - "Relaxed leaves more space.\n" + - "Use the steering wheel distance button on supported cars.") - self._initial_cruise_speed = InitialCruiseSpeedButton() - self._initial_cruise_speed.set_visible(lambda: ui_state.CP is not None and ui_state.has_longitudinal_control and not ui_state.CP.pcmCruise) - is_metric_toggle = BigToggle("use metric units", initial_state=ui_state.params.get_bool("IsMetric"), toggle_callback=self._on_metric_toggle) - ldw_toggle = BigParamControl("lane departure warnings", "IsLdwEnabled", - description="Warn when you drift across a detected lane line.\n" + - "Only above 31 mph (50 km/h), with no turn signal.") - always_on_dm_toggle = BigParamControl("always-on driver monitor", "AlwaysOnDM", description="Monitor the driver even when sunnypilot is not engaged.") - record_front = BigParamControl("record & upload cabin camera", "RecordFront", - description_icon=gui_app.texture("icons_mici/settings/device/cameras.png", 64, 64), - toggle_callback=restart_needed_callback, description="Upload cabin camera data to help improve driver monitoring.") - record_mic = BigParamControl("record & upload mic audio", "RecordAudio", description_icon=gui_app.texture("icons_mici/microphone.png", 64, 64), - toggle_callback=restart_needed_callback, - description="Record microphone audio while driving.\n" + - "Audio is included in dashcam videos in sunnylink.") - enable_openpilot = BigParamControl("enable sunnypilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback, - description="Enable to use sunnypilot driver assistance.\n" + - "Disable to use your car's stock driver assistance.") - - - self._scroller.add_widgets([ - self._personality_toggle, - self._initial_cruise_speed, - is_metric_toggle, - ldw_toggle, - always_on_dm_toggle, - record_front, - record_mic, - enable_openpilot, - ]) - - # Toggle lists - self._refresh_toggles = ( - ("IsMetric", is_metric_toggle), - ("IsLdwEnabled", ldw_toggle), - ("AlwaysOnDM", always_on_dm_toggle), - ("RecordFront", record_front), - ("RecordAudio", record_mic), - ("OpenpilotEnabledToggle", enable_openpilot), - ) - - enable_openpilot.set_enabled(lambda: not ui_state.engaged) - record_front.set_enabled(False if ui_state.params.get_bool("RecordFrontLock") else (lambda: not ui_state.engaged)) - record_mic.set_enabled(lambda: not ui_state.engaged) - - if ui_state.params.get_bool("ShowDebugInfo"): - gui_app.set_show_touches(True) - gui_app.set_show_fps(True) - - ui_state.add_engaged_transition_callback(self._update_toggles) - - def _update_state(self): - super()._update_state() - - if ui_state.sm.updated["selfdriveState"]: - personality = PERSONALITY_TO_INT[ui_state.sm["selfdriveState"].personality] - if personality != ui_state.personality and ui_state.started: - self._personality_toggle.set_value(self._personality_toggle._options[personality]) - ui_state.personality = personality - - def show_event(self): - super().show_event() - self._update_toggles() - - def _on_metric_toggle(self, state: bool): - ui_state.params.put_bool("IsMetric", state, block=True) - self._update_toggles() - - def _update_toggles(self): - ui_state.update_params() - self._initial_cruise_speed.refresh() - - if ui_state.CP is not None: - self._personality_toggle.set_visible(ui_state.has_longitudinal_control) - - # Refresh toggles from params to mirror external changes - for key, item in self._refresh_toggles: - item.set_checked(ui_state.params.get_bool(key)) diff --git a/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py b/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py deleted file mode 100644 index 9665db4d1..000000000 --- a/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py +++ /dev/null @@ -1,386 +0,0 @@ -import time -from enum import StrEnum -from typing import NamedTuple -import pyray as rl -import random -import string -from dataclasses import dataclass -from openpilot.cereal import messaging, log -from opendbc.car.structs import car -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter -from openpilot.common.hardware import COMMA_HARDWARE -from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.label import UnifiedLabel - -from openpilot.selfdrive.ui.sunnypilot.onroad.speed_limit import SpeedLimitAlertRenderer - -AlertSize = log.SelfdriveState.AlertSize -AlertStatus = log.SelfdriveState.AlertStatus - -ALERT_MARGIN = 18 - -ALERT_FONT_SMALL = 66 - 50 -ALERT_FONT_BIG = 88 - 40 - -SELFDRIVE_STATE_TIMEOUT = 5 # Seconds -SELFDRIVE_UNRESPONSIVE_TIMEOUT = 10 # Seconds - -# Constants -ALERT_COLORS = { - AlertStatus.normal: rl.Color(0, 0, 0, 255), - AlertStatus.userPrompt: rl.Color(255, 115, 0, 255), - AlertStatus.critical: rl.Color(255, 0, 21, 255), -} - -TURN_SIGNAL_BLINK_PERIOD = 1 / (80 / 60) # Mazda heartbeat turn signal BPM - -DEBUG = False - - -class IconSide(StrEnum): - left = 'left' - right = 'right' - - -class IconLayout(NamedTuple): - texture: rl.Texture - side: IconSide - margin_x: int - margin_y: int - alpha: float = 255.0 - - -class AlertLayout(NamedTuple): - text_rect: rl.Rectangle - icon: IconLayout | None - - -@dataclass -class Alert: - text1: str = "" - text2: str = "" - size: int = 0 - status: int = 0 - visual_alert: int = car.CarControl.HUDControl.VisualAlert.none - alert_type: str = "" - - -# Pre-defined alert instances -ALERT_STARTUP_PENDING = Alert( - text1="sunnypilot Unavailable", - text2="Waiting to start", - size=AlertSize.mid, - status=AlertStatus.normal, -) - -ALERT_CRITICAL_TIMEOUT = Alert( - text1="TAKE CONTROL IMMEDIATELY", - text2="System Unresponsive", - size=AlertSize.full, - status=AlertStatus.critical, -) - -ALERT_CRITICAL_REBOOT = Alert( - text1="System Unresponsive", - text2="Reboot Device", - size=AlertSize.full, - status=AlertStatus.critical, -) - - -class AlertRenderer(Widget, SpeedLimitAlertRenderer): - def __init__(self): - Widget.__init__(self) - SpeedLimitAlertRenderer.__init__(self) - - self._alert_text1_label = UnifiedLabel(text="", font_size=ALERT_FONT_BIG, font_weight=FontWeight.DISPLAY, line_height=0.86, - letter_spacing=-0.02) - self._alert_text2_label = UnifiedLabel(text="", font_size=ALERT_FONT_SMALL, font_weight=FontWeight.ROMAN, line_height=0.86, - letter_spacing=0.025) - - self._prev_alert: Alert | None = None - self._text_gen_time = 0 - self._alert_text2_gen = '' - - # animation filters - # TODO: use 0.1 but with proper alert height calculation - self._alert_y_filter = BounceFilter(0, 0.1, 1 / gui_app.target_fps) - self._alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) - - self._turn_signal_timer = 0.0 - self._turn_signal_alpha_filter = FirstOrderFilter(0.0, 0.3, 1 / gui_app.target_fps) - self._last_icon_side: IconSide | None = None - - self._load_icons() - - def _load_icons(self): - self._txt_turn_signal_left = gui_app.texture('icons_mici/onroad/turn_signal_left.png', 104, 96) - self._txt_turn_signal_right = gui_app.texture('icons_mici/onroad/turn_signal_left.png', 104, 96, flip_x=True) - self._txt_blind_spot_left = gui_app.texture('icons_mici/onroad/blind_spot_left.png', 134, 150) - self._txt_blind_spot_right = gui_app.texture('icons_mici/onroad/blind_spot_left.png', 134, 150, flip_x=True) - - 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 - 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 > 10: - return ALERT_STARTUP_PENDING - - # 2. Lost communication with selfdriveState after receiving it - if COMMA_HARDWARE and not waiting_for_startup: - ss_missing = time.monotonic() - sm.recv_time['selfdriveState'] - if ss_missing > SELFDRIVE_STATE_TIMEOUT: - if ss.enabled and (ss_missing - SELFDRIVE_STATE_TIMEOUT) < SELFDRIVE_UNRESPONSIVE_TIMEOUT: - return ALERT_CRITICAL_TIMEOUT - return ALERT_CRITICAL_REBOOT - - # No alert if size is none - if ss.alertSize == 0: - return None - - # Return current alert - ret = Alert(text1=self._tr_alert_text(ss.alertText1), text2=self._tr_alert_text(ss.alertText2), size=ss.alertSize.raw, status=ss.alertStatus.raw, - visual_alert=ss.alertHudVisual, alert_type=ss.alertType) - self._prev_alert = ret - return ret - - def will_render(self) -> tuple[Alert | None, bool]: - alert = self.get_alert(ui_state.sm) - return alert or self._prev_alert, alert is None - - def _icon_helper(self, alert: Alert) -> AlertLayout: - icon_side = None - txt_icon = None - icon_alpha = 255.0 - icon_margin_x = 20 - icon_margin_y = 18 - - # alert_type format is "EventName/eventType" (e.g., "preLaneChangeLeft/warning") - event_name = alert.alert_type.split('/')[0] if alert.alert_type else '' - - if event_name == 'preLaneChangeLeft': - icon_side = IconSide.left - txt_icon = self._txt_turn_signal_left - icon_margin_x = 2 - icon_margin_y = 5 - - elif event_name == 'preLaneChangeRight': - icon_side = IconSide.right - txt_icon = self._txt_turn_signal_right - icon_margin_x = 2 - icon_margin_y = 5 - - elif event_name == 'laneChange': - icon_side = self._last_icon_side - txt_icon = self._txt_turn_signal_left if self._last_icon_side == 'left' else self._txt_turn_signal_right - icon_margin_x = 2 - icon_margin_y = 5 - - elif event_name == 'laneChangeBlocked': - CS = ui_state.sm['carState'] - if CS.leftBlinker: - icon_side = IconSide.left - elif CS.rightBlinker: - icon_side = IconSide.right - else: - icon_side = self._last_icon_side - txt_icon = self._txt_blind_spot_left if icon_side == 'left' else self._txt_blind_spot_right - icon_margin_x = 8 - icon_margin_y = 0 - - elif event_name == 'speedLimitPreActive': - icon_side, txt_icon, icon_alpha, icon_margin_x, icon_margin_y = SpeedLimitAlertRenderer.speed_limit_pre_active_icon_helper(self) - - else: - self._turn_signal_timer = 0.0 - - self._last_icon_side = icon_side - - # create text rect based on icon presence - text_x = self._rect.x + ALERT_MARGIN - text_width = self._rect.width - ALERT_MARGIN - if icon_side == 'left': - text_x = self._rect.x + self._txt_turn_signal_right.width - text_width = self._rect.width - ALERT_MARGIN - self._txt_turn_signal_right.width - elif icon_side == 'right': - text_x = self._rect.x + ALERT_MARGIN - text_width = self._rect.width - ALERT_MARGIN - self._txt_turn_signal_right.width - - text_rect = rl.Rectangle( - text_x, - self._alert_y_filter.x, - text_width, - self._rect.height, - ) - icon_layout = IconLayout(txt_icon, icon_side, icon_margin_x, icon_margin_y, icon_alpha) if txt_icon is not None and icon_side is not None else None - return AlertLayout(text_rect, icon_layout) - - def _tr_alert_text(self, text: str) -> str: - t = tr(text) - if t != text: - return t - if ': ' in text: - head, _, tail = text.partition(': ') - ht = tr(head) - if ht != head: - return f"{ht}: {tail}" - parts = text.split(' ', 2) - if len(parts) >= 3: - head = parts[0] + ' ' + parts[1] - ht = tr(head) - if ht != head: - return f"{ht} {' '.join(parts[2:])}" - return text - - def _render(self, rect: rl.Rectangle) -> bool: - alert = self.get_alert(ui_state.sm) - - # Animate fade and slide in/out - self._alert_y_filter.update(self._rect.y - 50 if alert is None else self._rect.y) - self._alpha_filter.update(0 if alert is None else 1) - - if gui_app.sunnypilot_ui(): - ui_state.onroad_brightness_handle_alerts(ui_state, alert) - - if alert is None: - # If still animating out, keep the previous alert - 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) - - # update speed limit UI states - SpeedLimitAlertRenderer.update(self) - - alert_layout = self._icon_helper(alert) - self._draw_text(alert, alert_layout) - self._draw_icons(alert_layout) - - return True - - def _draw_icons(self, alert_layout: AlertLayout) -> None: - if alert_layout.icon is None: - return - - if time.monotonic() - self._turn_signal_timer > TURN_SIGNAL_BLINK_PERIOD: - self._turn_signal_timer = time.monotonic() - self._turn_signal_alpha_filter.x = 255 * 2 - else: - self._turn_signal_alpha_filter.update(255 * 0.2) - - if alert_layout.icon.side == 'left': - pos_x = int(self._rect.x + alert_layout.icon.margin_x) - else: - pos_x = int(self._rect.x + self._rect.width - alert_layout.icon.margin_x - alert_layout.icon.texture.width) - - if alert_layout.icon.texture not in (self._txt_turn_signal_left, self._txt_turn_signal_right): - icon_alpha = alert_layout.icon.alpha - else: - icon_alpha = int(min(self._turn_signal_alpha_filter.x, 255)) - - rl.draw_texture_ex(alert_layout.icon.texture, rl.Vector2(pos_x, self._rect.y + alert_layout.icon.margin_y), 0.0, 1.0, - rl.Color(255, 255, 255, int(icon_alpha * self._alpha_filter.x))) - - def _draw_background(self, alert: Alert) -> None: - # draw top gradient for alert text at top - 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)) - - small_alert_height = round(self._rect.height * 0.583) # 140px at mici height - medium_alert_height = round(self._rect.height * 0.833) # 200px at mici height - - # alert_type format is "EventName/eventType" (e.g., "preLaneChangeLeft/warning") - event_name = alert.alert_type.split('/')[0] if alert.alert_type else '' - - if event_name == 'preLaneChangeLeft': - bg_height = small_alert_height - elif event_name == 'preLaneChangeRight': - bg_height = small_alert_height - elif event_name == 'laneChange': - bg_height = small_alert_height - elif event_name == 'laneChangeBlocked': - bg_height = medium_alert_height - else: - 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) - - def _draw_text(self, alert: Alert, alert_layout: AlertLayout) -> None: - icon_side = alert_layout.icon.side if alert_layout.icon is not None else None - - # TODO: hack - alert_text1 = alert.text1.lower().replace('calibrating: ', 'calibrating:\n') - # TODO: there should be a common way to determine font size based on text length to maximize rect - if len(alert_text1) <= 12: - font_size = 92 - 10 - elif len(alert_text1) <= 16: - font_size = 70 - else: - font_size = 64 - 10 - - if icon_side is not None: - font_size -= 10 - - color = rl.Color(255, 255, 255, int(255 * 0.9 * self._alpha_filter.x)) - - text1_y_offset = 11 if font_size >= 70 else 4 - text_rect1 = rl.Rectangle( - alert_layout.text_rect.x, - alert_layout.text_rect.y - text1_y_offset, - alert_layout.text_rect.width, - alert_layout.text_rect.height, - ) - self._alert_text1_label.set_text(alert_text1) - self._alert_text1_label.set_text_color(color) - self._alert_text1_label.set_font_size(font_size) - self._alert_text1_label.set_alignment(TextAlignment.LEFT if icon_side != 'left' else TextAlignment.RIGHT) - self._alert_text1_label.render(text_rect1) - - alert_text2 = alert.text2.lower() - - # randomize chars and length for testing - if DEBUG: - if time.monotonic() - self._text_gen_time > 0.5: - self._alert_text2_gen = ''.join(random.choices(string.ascii_lowercase + ' ', k=random.randint(0, 40))) - self._text_gen_time = time.monotonic() - alert_text2 = self._alert_text2_gen or alert_text2 - - if alert_text2: - last_line_h = self._alert_text1_label.rect.y + self._alert_text1_label.get_content_height(int(alert_layout.text_rect.width)) - last_line_h -= 4 - if len(alert_text2) > 24: - small_font_size = 32 - elif len(alert_text2) > 18: - small_font_size = 36 - else: - small_font_size = 40 - text_rect2 = rl.Rectangle( - alert_layout.text_rect.x, - last_line_h, - alert_layout.text_rect.width, - alert_layout.text_rect.height - last_line_h - ) - color = rl.Color(255, 255, 255, int(255 * 0.65 * self._alpha_filter.x)) - - self._alert_text2_label.set_text(alert_text2) - self._alert_text2_label.set_text_color(color) - self._alert_text2_label.set_font_size(small_font_size) - self._alert_text2_label.set_alignment(TextAlignment.LEFT if icon_side != 'left' else TextAlignment.RIGHT) - self._alert_text2_label.render(text_rect2) diff --git a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py deleted file mode 100644 index b155ddce8..000000000 --- a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py +++ /dev/null @@ -1,414 +0,0 @@ -import numpy as np -import pyray as rl -from openpilot.cereal import log -from opendbc.car.structs import car -from openpilot.cereal.visionipc import VisionStreamType -from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus -from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH -from openpilot.selfdrive.ui.mici.onroad.alert_renderer import AlertRenderer -from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer -from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer -from openpilot.selfdrive.ui.mici.onroad.long_indicator import LongIndicator -from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer -from openpilot.selfdrive.ui.onroad.navigation import NavigationPanel, PANEL_WIDTH -from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall -from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView -from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent, TextAlignment, TextAlignmentVertical -from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.system.ui.widgets import Widget -from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter -from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCameraConfig, view_frame_from_device_frame -from openpilot.common.transformations.orientation import rot_from_euler -from enum import IntEnum - -if gui_app.sunnypilot_ui(): - from openpilot.selfdrive.ui.sunnypilot.mici.onroad.hud_renderer import HudRendererSP as HudRenderer - from openpilot.selfdrive.ui.sunnypilot.ui_state import OnroadTimerStatus - -OpState = log.SelfdriveState.OpenpilotState -CALIBRATED = log.ExtrinsicsCalibration.Status.calibrated -NARROW_ROAD_CAM = VisionStreamType.VISION_STREAM_NARROW_ROAD -WIDE_CAM = VisionStreamType.VISION_STREAM_WIDE_ROAD -DEFAULT_DEVICE_CAMERA = DEVICE_CAMERAS["tici", "ar0231"] - - -class BookmarkState(IntEnum): - HIDDEN = 0 - DRAGGING = 1 - TRIGGERED = 2 - -WIDE_CAM_MAX_SPEED = 5.0 # m/s (11 mph) -ROAD_CAM_MIN_SPEED = 10.0 # m/s (22 mph) - -CAM_Y_OFFSET = 20 - - -class BookmarkIcon(Widget): - PEEK_THRESHOLD = 150 # If icon peeks out this much, snap it fully visible - FULL_VISIBLE_OFFSET = 200 # How far onscreen when fully visible - HIDDEN_OFFSET = -50 # How far offscreen when hidden - - def __init__(self, bookmark_callback): - super().__init__() - self._bookmark_callback = bookmark_callback - self._icon = gui_app.texture("icons_mici/onroad/bookmark.png", 180, 180) - self._filled_icon = gui_app.texture("icons_mici/onroad/bookmark_fill.png", 180, 180) - self._offset_filter = BounceFilter(0.0, 0.1, 1 / gui_app.target_fps) - self._active_alpha = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) - - # State - self._interacting = False - self._state = BookmarkState.HIDDEN - self._swipe_start_x = 0.0 - self._swipe_current_x = 0.0 - self._is_swiping = False - self._is_swiping_left: bool = False - self._triggered_time: float = 0.0 - self._triggered_duration = 1.5 - self._swipe_expired = False - - def is_swiping_left(self) -> bool: - """Check if currently swiping left (for scroller to disable).""" - return self._is_swiping_left - - def interacting(self): - interacting, self._interacting = self._interacting, False - return interacting - - def _update_state(self): - if self._state == BookmarkState.TRIGGERED and rl.get_time() - self._triggered_time >= self._triggered_duration: - self._state = BookmarkState.HIDDEN - self._swipe_expired = self._is_swiping - - swipe_offset = self._swipe_start_x - self._swipe_current_x - armed = self._state == BookmarkState.DRAGGING and swipe_offset > self.PEEK_THRESHOLD - if self._state == BookmarkState.DRAGGING: - # Snap to the released position when armed, while allowing further dragging or cancellation. - if armed: - swipe_offset += self.FULL_VISIBLE_OFFSET - self.PEEK_THRESHOLD - swipe_offset = min(swipe_offset, self.FULL_VISIBLE_OFFSET + 50) - self._offset_filter.update(swipe_offset) - - elif self._state == BookmarkState.TRIGGERED: - # Let another left swipe move the same bookmark. - offset = self.FULL_VISIBLE_OFFSET - if self._is_swiping and self._is_swiping_left: - offset += min(swipe_offset, 50) - self._offset_filter.update(offset) - - elif self._state == BookmarkState.HIDDEN: - self._offset_filter.update(self.HIDDEN_OFFSET) - - if self._offset_filter.x < 1e-3: - self._interacting = False - - self._active_alpha.update(float(armed or self._state == BookmarkState.TRIGGERED)) - - def _handle_mouse_event(self, mouse_event: MouseEvent): - if not ui_state.started: - return - - if mouse_event.left_pressed: - if self._state == BookmarkState.TRIGGERED: - self._triggered_time = rl.get_time() - self._triggered_duration = 0.5 - - # Store relative position within widget - self._swipe_start_x = mouse_event.pos.x - self._swipe_current_x = mouse_event.pos.x - self._is_swiping = True - self._is_swiping_left = False - self._swipe_expired = False - - elif mouse_event.left_down and self._is_swiping: - self._swipe_current_x = mouse_event.pos.x - swipe_offset = self._swipe_start_x - self._swipe_current_x - self._is_swiping_left = swipe_offset > 0 - if self._is_swiping_left: - self._interacting = True - if self._state == BookmarkState.HIDDEN and not self._swipe_expired: - self._state = BookmarkState.DRAGGING - - elif mouse_event.left_released: - if self._is_swiping and self._state == BookmarkState.DRAGGING: - swipe_distance = self._swipe_start_x - self._swipe_current_x - - # If peeking past threshold, transition to animating to fully visible and bookmark - if swipe_distance > self.PEEK_THRESHOLD: - self._state = BookmarkState.TRIGGERED - self._triggered_time = rl.get_time() - self._triggered_duration = 1.5 - self._bookmark_callback() - else: - # Otherwise, transition back to hidden - self._state = BookmarkState.HIDDEN - - # Reset swipe state - self._is_swiping = False - self._is_swiping_left = False - - def _render(self, _): - """Render the bookmark icon.""" - if self._offset_filter.x > 0: - icon_x = self.rect.x + self.rect.width - round(self._offset_filter.x) - icon_y = self.rect.y + (self.rect.height - self._icon.height) / 2 # Vertically centered - for icon, alpha in ((self._icon, 1.0 - self._active_alpha.x), (self._filled_icon, self._active_alpha.x)): - rl.draw_texture_ex(icon, rl.Vector2(icon_x, icon_y), 0.0, 1.0, rl.Color(255, 255, 255, round(255 * alpha))) - - -class AugmentedRoadView(CameraView): - def __init__(self, bookmark_callback=None, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_NARROW_ROAD): - super().__init__("camerad", stream_type) - self._bookmark_callback = bookmark_callback - self._set_placeholder_color(rl.BLACK) - - self.device_camera: DeviceCameraConfig | None = None - self.view_from_calib = view_frame_from_device_frame.copy() - self.view_from_wide_calib = view_frame_from_device_frame.copy() - - self._matrix_cache_key: tuple | None = None - self._cached_matrix: np.ndarray | None = None - self._content_rect = rl.Rectangle() - self._last_click_time = 0.0 - - # Bookmark icon with swipe gesture - self._bookmark_icon = BookmarkIcon(bookmark_callback) - - self._navigation = NavigationPanel() - self._model_renderer = ModelRenderer() - self._hud_renderer = HudRenderer() - self._long_indicator = LongIndicator() - self._alert_renderer = AlertRenderer() - self._driver_state_renderer = DriverStateRenderer() - self._confidence_ball = ConfidenceBall() - self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY, - text_color=rl.Color(255, 255, 255, int(255 * 0.9)), - alignment=TextAlignment.CENTER, - alignment_vertical=TextAlignmentVertical.MIDDLE) - - self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png") - - def is_swiping_left(self) -> bool: - """Check if currently swiping left (for scroller to disable).""" - return self._bookmark_icon.is_swiping_left() - - def _update_state(self): - super()._update_state() - - # update offroad label - if ui_state.panda_type == log.PandaState.PandaType.unknown: - self._offroad_label.set_text("system booting") - elif ui_state.ignition and not ui_state.started: - self._offroad_label.set_text("openpilot can't start\ncheck alerts") - else: - self._offroad_label.set_text("start the car to\nuse sunnypilot") - - def _handle_mouse_release(self, mouse_pos: MousePos): - # Don't trigger click callback if bookmark was triggered - if not self._bookmark_icon.interacting(): - super()._handle_mouse_release(mouse_pos) - - def _render(self, _): - # Draw text if not onroad - if not ui_state.started: - rl.draw_rectangle_rec(self.rect, rl.BLACK) - self._offroad_label.render(self._rect) - return - - self._switch_stream_if_needed(ui_state.sm) - - # Update calibration before rendering - self._update_calibration() - - alert_to_render, not_animating_out = self._alert_renderer.will_render() - show_navigation = ui_state.navigation_enabled and ui_state.nav_destination_set and alert_to_render is None - - # Create inner content area with border padding - self._content_rect = rl.Rectangle( - self.rect.x, - self.rect.y, - self.rect.width - (PANEL_WIDTH if show_navigation else SIDE_PANEL_WIDTH), - self.rect.height, - ) - - # Enable scissor mode to clip all rendering within content rectangle boundaries - # This creates a rendering viewport that prevents graphics from drawing outside the border - rl.begin_scissor_mode( - int(self._content_rect.x), - int(self._content_rect.y), - int(self._content_rect.width), - int(self._content_rect.height) - ) - - # Render the base camera view - super()._render(self._content_rect) - - # Draw all UI overlays - self._model_renderer.render(self._content_rect) - - # Fade out bottom of overlays for looks - rl.draw_texture_ex(self._fade_texture, rl.Vector2(self._content_rect.x, self._content_rect.y), 0.0, 1.0, rl.WHITE) - - # Hide DMoji when disengaged unless AlwaysOnDM is enabled - should_draw_dmoji = (not self._hud_renderer.drawing_top_icons() and - (ui_state.status != UIStatus.DISENGAGED or ui_state.always_on_dm)) - self._driver_state_renderer.set_should_draw(should_draw_dmoji) - self._driver_state_renderer.set_position(self._rect.x + 16, self._rect.y + 10) - self._driver_state_renderer.render() - self._long_indicator.set_should_draw(not self._hud_renderer.drawing_top_icons()) - self._long_indicator.render(self._content_rect) - - self._hud_renderer.set_can_draw_top_icons(alert_to_render is None) - self._hud_renderer.set_wheel_critical_icon(alert_to_render is not None and not not_animating_out and - alert_to_render.visual_alert == car.CarControl.HUDControl.VisualAlert.steerRequired) - self._alert_renderer.render(self._content_rect) - self._hud_renderer.render(self._content_rect) - - # Draw fake rounded border - rl.draw_rectangle_rounded_lines_ex(self._content_rect, 0.2 * 1.02, 10, 50, rl.BLACK) - - # End clipping region - rl.end_scissor_mode() - - # Custom UI extension point - add custom overlays here - # Use self._content_rect for positioning within camera bounds - if show_navigation: - self._navigation.render(rl.Rectangle(self._content_rect.x + self._content_rect.width, self.rect.y, PANEL_WIDTH, self.rect.height)) - else: - self._confidence_ball.render(self.rect) - - self._bookmark_icon.render(self.rect) - - def close(self): - self._navigation.close() - super().close() - - def _switch_stream_if_needed(self, sm): - if sm['carParams'].openpilotLongitudinalControl and WIDE_CAM in self.available_streams: - v_ego = sm['carState'].vEgo - if v_ego < WIDE_CAM_MAX_SPEED: - target = WIDE_CAM - elif v_ego > ROAD_CAM_MIN_SPEED: - target = NARROW_ROAD_CAM - else: - # Hysteresis zone - keep current stream - target = self.stream_type - else: - target = NARROW_ROAD_CAM - - if self.stream_type != target: - self.switch_stream(target) - - def _update_calibration(self): - # Update device camera if not already set - sm = ui_state.sm - if not self.device_camera and sm.seen['narrowRoadCameraState'] and sm.seen['deviceState']: - self.device_camera = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['narrowRoadCameraState'].sensor))] - - # Check if camera calibration data is available and valid - if not (sm.updated["extrinsicsCalibration"] and sm.valid['extrinsicsCalibration']): - return - - calib = sm['extrinsicsCalibration'] - if len(calib.rpyCalib) != 3 or calib.calStatus != CALIBRATED: - return - - # Update view_from_calib matrix - device_from_calib = rot_from_euler(calib.rpyCalib) - self.view_from_calib = view_frame_from_device_frame @ device_from_calib - - # Update wide calibration if available - if hasattr(calib, 'wideFromDeviceEuler') and len(calib.wideFromDeviceEuler) == 3: - wide_from_device = rot_from_euler(calib.wideFromDeviceEuler) - self.view_from_wide_calib = view_frame_from_device_frame @ wide_from_device @ device_from_calib - - def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray: - cache_key = ( - ui_state.sm.recv_frame['extrinsicsCalibration'], - int(self._content_rect.width), - int(self._content_rect.height), - self.stream_type, - round(ui_state.sm['carState'].vEgo, 1), - ) - - if cache_key == self._matrix_cache_key and self._cached_matrix is not None: - return self._cached_matrix - - # Get camera configuration - device_camera = self.device_camera or DEFAULT_DEVICE_CAMERA - is_wide_camera = self.stream_type == WIDE_CAM - intrinsic = device_camera.wide_road.intrinsics if is_wide_camera else device_camera.narrow_road.intrinsics - calibration = self.view_from_wide_calib if is_wide_camera else self.view_from_calib - if is_wide_camera: - zoom = 0.7 * 1.5 - else: - zoom = np.interp(ui_state.sm['carState'].vEgo, [10, 30], [0.8, 1.0]) - - # Calculate transforms for vanishing point - inf_point = np.array([1000.0, 0.0, 0.0]) - calib_transform = intrinsic @ calibration - kep = calib_transform @ inf_point - - # Calculate center points and dimensions - w, h = self._content_rect.width, self._content_rect.height - cx, cy = intrinsic[0, 2], intrinsic[1, 2] - - # Ensure zoom views the whole area - zoom = max(zoom, w / (2 * cx), h / (2 * cy)) - - # Calculate max allowed offsets with margins - margin = 5 - max_x_offset = max(0.0, cx * zoom - w / 2 - margin) - max_y_offset = max(0.0, cy * zoom - h / 2 - margin) - - # Calculate and clamp offsets to prevent out-of-bounds issues - try: - if abs(kep[2]) > 1e-6: - x_offset = np.clip((kep[0] / kep[2] - cx) * zoom, -max_x_offset, max_x_offset) - y_offset = np.clip((kep[1] / kep[2] - cy) * zoom + CAM_Y_OFFSET, -max_y_offset, max_y_offset) - else: - x_offset, y_offset = 0, 0 - except (ZeroDivisionError, OverflowError): - x_offset, y_offset = 0, 0 - - # Cache the computed transformation matrix to avoid recalculations - self._matrix_cache_key = cache_key - self._cached_matrix = np.array([ - [zoom * 2 * cx / w, 0, -x_offset / w * 2], - [0, zoom * 2 * cy / h, -y_offset / h * 2], - [0, 0, 1.0] - ]) - - # built without rect.x/y so cache stays hot during scroll. ModelRenderer adds offset at draw time - video_transform = np.array([ - [zoom, 0.0, (w / 2 - x_offset) - (cx * zoom)], - [0.0, zoom, (h / 2 - y_offset) - (cy * zoom)], - [0.0, 0.0, 1.0] - ]) - self._model_renderer.set_transform(video_transform @ calib_transform) - - return self._cached_matrix - - def show_event(self): - if gui_app.sunnypilot_ui(): - ui_state.reset_onroad_sleep_timer(OnroadTimerStatus.RESUME) - - def hide_event(self): - if gui_app.sunnypilot_ui(): - ui_state.reset_onroad_sleep_timer(OnroadTimerStatus.PAUSE) - - -if __name__ == "__main__": - gui_app.init_window("OnRoad Camera View") - road_camera_view = AugmentedRoadView(lambda: None, stream_type=NARROW_ROAD_CAM) - print("***press space to switch camera view***") - try: - for _ in gui_app.render(): - ui_state.update() - if rl.is_key_released(rl.KeyboardKey.KEY_SPACE): - if WIDE_CAM in road_camera_view.available_streams: - stream = NARROW_ROAD_CAM if road_camera_view.stream_type == WIDE_CAM else WIDE_CAM - road_camera_view.switch_stream(stream) - road_camera_view.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - finally: - road_camera_view.close() diff --git a/openpilot/selfdrive/ui/mici/onroad/confidence_ball.py b/openpilot/selfdrive/ui/mici/onroad/confidence_ball.py deleted file mode 100644 index 54699eab5..000000000 --- a/openpilot/selfdrive/ui/mici/onroad/confidence_ball.py +++ /dev/null @@ -1,86 +0,0 @@ -import math -import pyray as rl -from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH -from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.lib.application import gui_app -from openpilot.common.filter_simple import FirstOrderFilter - -from openpilot.selfdrive.ui.sunnypilot.mici.onroad.confidence_ball import ConfidenceBallSP - - -def draw_circle_gradient(center_x: float, center_y: float, radius: int, - top: rl.Color, bottom: rl.Color) -> None: - # Draw a square with the gradient - rl.draw_rectangle_gradient_v(int(center_x - radius), int(center_y - radius), - radius * 2, radius * 2, - top, bottom) - - # Paint over square with a ring - outer_radius = math.ceil(radius * math.sqrt(2)) + 1 - rl.draw_ring(rl.Vector2(int(center_x), int(center_y)), radius, outer_radius, - 0.0, 360.0, - 20, rl.BLACK) - - -class ConfidenceBall(Widget, ConfidenceBallSP): - def __init__(self, demo: bool = False): - Widget.__init__(self) - ConfidenceBallSP.__init__(self) - self._demo = demo - self._confidence_filter = FirstOrderFilter(-0.5, 0.5, 1 / gui_app.target_fps) - - def update_filter(self, value: float): - self._confidence_filter.update(value) - - def _update_state(self): - if self._demo: - return - - # animate status dot in from bottom - if ui_state.status == UIStatus.DISENGAGED: - self._confidence_filter.update(-0.5) - elif ui_state.status in (UIStatus.LAT_ONLY, UIStatus.LONG_ONLY): - self._confidence_filter.update(1 - max(self.get_animate_status_probs() or [1])) - else: - self._confidence_filter.update((1 - max(ui_state.sm['modelV2'].meta.disengagePredictions.brakeDisengageProbs or [1])) * - (1 - max(ui_state.sm['modelV2'].meta.disengagePredictions.steerOverrideProbs or [1]))) - - def _render(self, _): - content_rect = rl.Rectangle( - self.rect.x + self.rect.width - SIDE_PANEL_WIDTH, - self.rect.y, - SIDE_PANEL_WIDTH, - self.rect.height, - ) - - status_dot_radius = 24 - dot_height = (1 - self._confidence_filter.x) * (content_rect.height - 2 * status_dot_radius) + status_dot_radius - dot_height = self._rect.y + dot_height - - # confidence zones - if ui_state.status == UIStatus.ENGAGED or self._demo: - if self._confidence_filter.x > 0.5: - top_dot_color = rl.Color(0, 255, 204, 255) - bottom_dot_color = rl.Color(0, 255, 38, 255) - elif self._confidence_filter.x > 0.2: - top_dot_color = rl.Color(255, 200, 0, 255) - bottom_dot_color = rl.Color(255, 115, 0, 255) - else: - top_dot_color = rl.Color(255, 0, 21, 255) - bottom_dot_color = rl.Color(255, 0, 89, 255) - - elif ui_state.status in (UIStatus.LAT_ONLY, UIStatus.LONG_ONLY): - top_dot_color = bottom_dot_color = self.get_lat_long_dot_color() - - elif ui_state.status == UIStatus.OVERRIDE: - top_dot_color = rl.Color(255, 255, 255, 255) - bottom_dot_color = rl.Color(82, 82, 82, 255) - - else: - top_dot_color = rl.Color(50, 50, 50, 255) - bottom_dot_color = rl.Color(13, 13, 13, 255) - - draw_circle_gradient(content_rect.x + content_rect.width - status_dot_radius, - dot_height, status_dot_radius, - top_dot_color, bottom_dot_color) diff --git a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py deleted file mode 100644 index 05abae82e..000000000 --- a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py +++ /dev/null @@ -1,317 +0,0 @@ -import math -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, 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 -from openpilot.system.ui.widgets import Widget -from openpilot.common.filter_simple import FirstOrderFilter -from openpilot.cereal import log - -EventName = log.OnroadEvent.EventName - -# Constants -SET_SPEED_NA = 255 -KM_TO_MILE = 0.621371 -CRUISE_DISABLED_CHAR = '–' - -SET_SPEED_PERSISTENCE = 2.5 # seconds - - -@dataclass(frozen=True) -class FontSizes: - current_speed: int = 176 - speed_unit: int = 66 - max_speed: int = 36 - set_speed: int = 112 - - -@dataclass(frozen=True) -class Colors: - WHITE = rl.WHITE - WHITE_TRANSLUCENT = rl.Color(255, 255, 255, 200) - - -FONT_SIZES = FontSizes() -COLORS = Colors() - - -class TurnIntent(Widget): - FADE_IN_ANGLE = 30 # degrees - - def __init__(self): - super().__init__() - self._pre = False - self._turn_intent_direction: int = 0 - - self._turn_intent_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) - self._turn_intent_rotation_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) - - self._txt_turn_intent_left: rl.Texture = gui_app.texture('icons_mici/turn_intent_left.png', 50, 20) - self._txt_turn_intent_right: rl.Texture = gui_app.texture('icons_mici/turn_intent_left.png', 50, 20, flip_x=True) - - def _render(self, _): - if self._turn_intent_alpha_filter.x > 1e-2: - turn_intent_texture = self._txt_turn_intent_right if self._turn_intent_direction == 1 else self._txt_turn_intent_left - src_rect = rl.Rectangle(0, 0, turn_intent_texture.width, turn_intent_texture.height) - dest_rect = rl.Rectangle(self._rect.x + self._rect.width / 2, self._rect.y + self._rect.height / 2, - turn_intent_texture.width, turn_intent_texture.height) - - origin = (turn_intent_texture.width / 2, self._rect.height / 2) - color = rl.Color(255, 255, 255, int(255 * self._turn_intent_alpha_filter.x)) - rl.draw_texture_pro(turn_intent_texture, src_rect, dest_rect, origin, self._turn_intent_rotation_filter.x, color) - - def _update_state(self) -> None: - sm = ui_state.sm - - left = any(e.name == EventName.preLaneChangeLeft for e in sm['onroadEvents']) - right = any(e.name == EventName.preLaneChangeRight for e in sm['onroadEvents']) - if left or right: - # pre lane change - if not self._pre: - self._turn_intent_rotation_filter.x = self.FADE_IN_ANGLE if left else -self.FADE_IN_ANGLE - - self._pre = True - self._turn_intent_direction = -1 if left else 1 - self._turn_intent_alpha_filter.update(1) - self._turn_intent_rotation_filter.update(0) - elif any(e.name == EventName.laneChange for e in sm['onroadEvents']): - # fade out and rotate away - self._pre = False - self._turn_intent_alpha_filter.update(0) - - if self._turn_intent_direction == 0: - # unknown. missed pre frame? - self._turn_intent_rotation_filter.update(0) - else: - self._turn_intent_rotation_filter.update(self._turn_intent_direction * self.FADE_IN_ANGLE) - else: - # didn't complete lane change, just hide - self._pre = False - self._turn_intent_direction = 0 - self._turn_intent_alpha_filter.update(0) - self._turn_intent_rotation_filter.update(0) - - -class HudRenderer(Widget): - def __init__(self): - super().__init__() - """Initialize the HUD renderer.""" - self.is_cruise_set: bool = False - self.is_cruise_available: bool = True - self.set_speed: float = SET_SPEED_NA - self._set_speed_changed_time: float = 0 - self.speed: float = 0.0 - self.v_ego_cluster_seen: bool = False - self._engaged: bool = False - self._chestnut_fade_time: float = 0 - - self._can_draw_top_icons = True - self._show_wheel_critical = False - - self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD) - self._font_medium: rl.Font = gui_app.font(FontWeight.MEDIUM) - self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD) - self._font_display: rl.Font = gui_app.font(FontWeight.DISPLAY) - - self._turn_intent = TurnIntent() - self._torque_bar = TorqueBar() - - self._txt_wheel: rl.Texture = gui_app.texture('icons_mici/wheel.png', 50, 50) - self._txt_wheel_critical: rl.Texture = gui_app.texture('icons_mici/wheel_critical.png', 50, 50) - self._txt_exclamation_point: rl.Texture = gui_app.texture('icons_mici/exclamation_point.png', 9, 44) - 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._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) - - self._set_speed_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) - self._chestnut_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) - - def set_wheel_critical_icon(self, critical: bool): - """Set the wheel icon to critical or normal state.""" - self._show_wheel_critical = critical - - def set_can_draw_top_icons(self, can_draw_top_icons: bool): - """Set whether to draw the top part of the HUD.""" - self._can_draw_top_icons = can_draw_top_icons - - def drawing_top_icons(self) -> bool: - # whether we're drawing any top icons currently - return bool(self._set_speed_alpha_filter.x > 1e-2) - - def _update_state(self) -> None: - """Update HUD state based on car state and controls state.""" - sm = ui_state.sm - if sm.recv_frame["carState"] < ui_state.started_frame: - self.is_cruise_set = False - self.set_speed = SET_SPEED_NA - self.speed = 0.0 - return - - controls_state = sm['controlsState'] - car_state = sm['carState'] - - v_cruise_cluster = car_state.vCruiseCluster - set_speed = ( - controls_state.deprecated.vCruise if v_cruise_cluster == 0.0 else v_cruise_cluster - ) - engaged = sm['selfdriveState'].enabled - 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 - self.is_cruise_available = self.set_speed != -1 - - v_ego_cluster = car_state.vEgoCluster - self.v_ego_cluster_seen = self.v_ego_cluster_seen or v_ego_cluster != 0.0 - v_ego = v_ego_cluster if self.v_ego_cluster_seen else car_state.vEgo - speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH - self.speed = max(0.0, v_ego * speed_conversion) - - def _render(self, rect: rl.Rectangle) -> None: - """Render HUD elements to the screen.""" - - self._torque_bar.render(rect) - - if self.is_cruise_set: - self._draw_set_speed(rect) - - self._draw_model_source(rect) - - self._draw_steering_wheel(rect) - - def _draw_model_source(self, rect: rl.Rectangle) -> None: - if ui_state.sm.recv_frame['selfdriveState'] < ui_state.started_frame: - return - - loading = ui_state.chestnut_state == ChestnutState.LOADING - if loading: - icon = self._txt_chestnut - 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 - 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 - 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 - - pos = rl.Vector2(rect.x + rect.width - 10 - icon.width, - rect.y + rect.height - 14 - (self._txt_wheel.height + icon.height) / 2) - rl.draw_texture_ex(icon, pos, 0.0, 1.0, rl.Color(255, 255, 255, int(255 * opacity * alpha))) - - def _draw_steering_wheel(self, rect: rl.Rectangle) -> None: - wheel_txt = self._txt_wheel_critical if self._show_wheel_critical else self._txt_wheel - - bsm_detected = self._has_blind_spot_detected() if gui_app.sunnypilot_ui() else False - - if self._show_wheel_critical: - self._wheel_alpha_filter.update(255) - self._wheel_y_filter.update(0) - else: - if ui_state.status == UIStatus.DISENGAGED or bsm_detected: - self._wheel_alpha_filter.update(0) - self._wheel_y_filter.update(wheel_txt.height / 2) - else: - self._wheel_alpha_filter.update(255 * 0.9) - self._wheel_y_filter.update(0) - - # pos - pos_x = int(rect.x + 21 + wheel_txt.width / 2) - pos_y = int(rect.y + rect.height - 14 - wheel_txt.height / 2 + self._wheel_y_filter.x) - rotation = -ui_state.sm['carState'].steeringAngleDeg - - turn_intent_margin = 25 - self._turn_intent.render(rl.Rectangle( - pos_x - wheel_txt.width / 2 - turn_intent_margin, - pos_y - wheel_txt.height / 2 - turn_intent_margin, - wheel_txt.width + turn_intent_margin * 2, - wheel_txt.height + turn_intent_margin * 2, - )) - - src_rect = rl.Rectangle(0, 0, wheel_txt.width, wheel_txt.height) - dest_rect = rl.Rectangle(pos_x, pos_y, wheel_txt.width, wheel_txt.height) - origin = (wheel_txt.width / 2, wheel_txt.height / 2) - - # color and draw - color = rl.Color(255, 255, 255, int(self._wheel_alpha_filter.x)) - rl.draw_texture_pro(wheel_txt, src_rect, dest_rect, origin, rotation, color) - - if self._show_wheel_critical: - # Draw exclamation point icon - EXCLAMATION_POINT_SPACING = 10 - exclamation_pos_x = pos_x - self._txt_exclamation_point.width / 2 + wheel_txt.width / 2 + EXCLAMATION_POINT_SPACING - exclamation_pos_y = pos_y - self._txt_exclamation_point.height / 2 - rl.draw_texture_ex(self._txt_exclamation_point, rl.Vector2(exclamation_pos_x, exclamation_pos_y), 0.0, 1.0, rl.WHITE) - - def _draw_set_speed(self, rect: rl.Rectangle) -> None: - """Draw the MAX speed indicator box.""" - alpha = self._set_speed_alpha_filter.update(0 < rl.get_time() - self._set_speed_changed_time < SET_SPEED_PERSISTENCE and - self._can_draw_top_icons and self._engaged) - if alpha < 1e-2: - return - - x = rect.x - y = rect.y - - # draw drop shadow - circle_radius = 162 // 2 - rl.draw_circle_gradient(rl.Vector2(x + circle_radius, y + circle_radius), circle_radius, - rl.Color(0, 0, 0, int(255 / 2 * alpha)), rl.BLANK) - - set_speed_color = rl.Color(255, 255, 255, int(255 * 0.9 * alpha)) - max_color = rl.Color(255, 255, 255, int(255 * 0.9 * alpha)) - - set_speed = self.set_speed - if self.is_cruise_set and not ui_state.is_metric: - set_speed *= KM_TO_MILE - - set_speed_text = CRUISE_DISABLED_CHAR if not self.is_cruise_set else str(round(set_speed)) - rl.draw_text_ex( - self._font_display, - set_speed_text, - rl.Vector2(x + 13 + 4, y + 3 - 8 - 3 + 4), - FONT_SIZES.set_speed, - 0, - set_speed_color, - ) - - max_text = tr("MAX") - rl.draw_text_ex( - self._font_semi_bold, - max_text, - rl.Vector2(x + 25, y + FONT_SIZES.set_speed - 7 + 4), - FONT_SIZES.max_speed, - 0, - max_color, - ) - - def _draw_current_speed(self, rect: rl.Rectangle) -> None: - """Draw the current vehicle speed and unit.""" - speed_text = str(round(self.speed)) - speed_text_size = measure_text_cached(self._font_bold, speed_text, FONT_SIZES.current_speed) - speed_pos = rl.Vector2(rect.x + rect.width / 2 - speed_text_size.x / 2, 180 - speed_text_size.y / 2) - rl.draw_text_ex(self._font_bold, speed_text, speed_pos, FONT_SIZES.current_speed, 0, COLORS.WHITE) - - unit_text = tr("km/h") if ui_state.is_metric else tr("mph") - unit_text_size = measure_text_cached(self._font_medium, unit_text, FONT_SIZES.speed_unit) - unit_pos = rl.Vector2(rect.x + rect.width / 2 - unit_text_size.x / 2, 290 - unit_text_size.y / 2) - rl.draw_text_ex(self._font_medium, unit_text, unit_pos, FONT_SIZES.speed_unit, 0, COLORS.WHITE_TRANSLUCENT) diff --git a/openpilot/selfdrive/ui/mici/onroad/long_indicator.py b/openpilot/selfdrive/ui/mici/onroad/long_indicator.py deleted file mode 100644 index 1ff83082a..000000000 --- a/openpilot/selfdrive/ui/mici/onroad/long_indicator.py +++ /dev/null @@ -1,89 +0,0 @@ -import pyray as rl -from openpilot.cereal import log -from openpilot.common.filter_simple import FirstOrderFilter -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.widgets import Widget - -HIGHLIGHT_TIME = 2.5 # seconds - - -class LongIndicator(Widget): - def __init__(self): - super().__init__() - self._txt_lead_car = (self._texture('car', 35, 27), self._texture('car_green', 50, 42)) - self._txt_distance = [(self._texture('distance_1', 32, 7), self._texture('distance_1_green', 60, 35)), - (self._texture('distance_2', 40, 9), self._texture('distance_2_green', 68, 37)), - (self._texture('distance_3', 48, 11), self._texture('distance_3_green', 76, 39))] - self._alpha_filter = FirstOrderFilter(0.0, 0.05, 1 / gui_app.target_fps) - # crossfade between states, a white and a green filter per icon - self._lead_car_filters = (FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps), FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)) - self._distance_filters = [(FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps), FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)) - for _ in range(3)] - self._personality: int | None = None - self._personality_changed_time = -HIGHLIGHT_TIME - self._should_draw = False - - @staticmethod - def _texture(name: str, width: int, height: int) -> rl.Texture: - return gui_app.texture(f'icons_mici/longitudinal/{name}.png', width, height, keep_aspect_ratio=False) - - def set_should_draw(self, should_draw: bool): - self._should_draw = should_draw - - def _render(self, rect: rl.Rectangle) -> None: - sm = ui_state.sm - if sm.recv_frame['selfdriveState'] < ui_state.started_frame or not sm['selfdriveState'].enabled or not ui_state.has_longitudinal_control: - self._personality = None - self._personality_changed_time = -HIGHLIGHT_TIME - self._alpha_filter.x = 0.0 - return - - # hidden under alerts and set speed - visible = self._should_draw and sm['selfdriveState'].alertSize == log.SelfdriveState.AlertSize.none - alpha = self._alpha_filter.update(visible) - self._draw_lead_car(rect, alpha) - self._draw_distance_bars(rect, alpha, visible) - - def _draw_lead_car(self, rect: rl.Rectangle, alpha: float) -> None: - sm = ui_state.sm - plan = sm['longitudinalPlan'] - has_lead = sm.alive['longitudinalPlan'] and plan.hasLead - - e2e = has_lead and plan.longitudinalPlanSource == log.LongitudinalPlan.LongitudinalPlanSource.e2e - white_f, green_f = self._lead_car_filters - white_alpha = white_f.update(0.0 if e2e else 0.9 if has_lead else 0.35) - green_alpha = green_f.update(float(e2e)) - - white, green = self._txt_lead_car - self._draw_centered(white, rect, 100, white_alpha * alpha) - self._draw_centered(green, rect, 100, green_alpha * alpha) - - def _draw_distance_bars(self, rect: rl.Rectangle, alpha: float, visible: bool) -> None: - sm = ui_state.sm - now = rl.get_time() - personality = sm['selfdriveState'].personality.raw - if self._personality is not None and personality != self._personality: - self._personality_changed_time = now - self._personality = personality - # the personality alert covers the bars, hold the highlight until they show - if not visible and now - self._personality_changed_time < HIGHLIGHT_TIME: - self._personality_changed_time = now - highlight = now - self._personality_changed_time < HIGHLIGHT_TIME - - # blink at double the turn signal rate (2.67 Hz) while overriding the gas - overriding = any(e.name == log.OnroadEvent.EventName.gasPressedOverride for e in sm['onroadEvents']) - blink = 0.35 / 0.9 if overriding and now % 0.375 > 0.1875 else 1.0 - - count = personality + 1 - for i, ((white, green), y, (active_f, green_f)) in enumerate(zip(self._txt_distance, (122, 136, 152), self._distance_filters, strict=True)): - active = active_f.update(float(i < count)) - green_alpha = green_f.update(float(highlight and i == count - 1)) - # only lit bars blink on override - self._draw_centered(white, rect, y, (0.35 * (1 - active) + 0.9 * (active - green_alpha) * blink) * alpha) - self._draw_centered(green, rect, y, green_alpha * blink * alpha) - - @staticmethod - def _draw_centered(texture: rl.Texture, rect: rl.Rectangle, y: float, alpha: float) -> None: - pos = rl.Vector2(rect.x + 46 - texture.width / 2, rect.y + y - texture.height / 2) - rl.draw_texture_ex(texture, pos, 0.0, 1.0, rl.Color(255, 255, 255, round(255 * alpha))) diff --git a/openpilot/selfdrive/ui/mici/onroad/model_renderer.py b/openpilot/selfdrive/ui/mici/onroad/model_renderer.py deleted file mode 100644 index 68d748847..000000000 --- a/openpilot/selfdrive/ui/mici/onroad/model_renderer.py +++ /dev/null @@ -1,424 +0,0 @@ -import colorsys -import numpy as np -import pyray as rl -from openpilot.cereal import log, messaging -from opendbc.car.structs import car -from dataclasses import dataclass, field -from openpilot.common.params import Params -from openpilot.common.filter_simple import FirstOrderFilter -from openpilot.selfdrive.controls.radard import RADAR_TO_CAMERA -from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT -from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus -from openpilot.selfdrive.ui.mici.onroad import blend_colors -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient -from openpilot.system.ui.widgets import Widget - -from openpilot.selfdrive.ui.sunnypilot.mici.onroad.model_renderer import LANE_LINE_COLORS_SP, ModelRendererSP - -CLIP_MARGIN = 500 -MIN_DRAW_DISTANCE = 10.0 -MAX_DRAW_DISTANCE = 100.0 -LEAD_BAR_LENGTH = 12.0 # px -LEAD_BAR_WIDTH = 1.8 # m - -STOCK_PATH_GRADIENT = Gradient( - start=(0.0, 1.0), - end=(0.0, 0.0), - colors=[rl.Color(13, 248, 122, 102), rl.Color(114, 255, 92, 89), rl.Color(114, 255, 92, 0)], - stops=[0.0, 0.5, 1.0], -) - -LANE_LINE_COLORS = { - UIStatus.DISENGAGED: rl.Color(200, 200, 200, 255), - UIStatus.OVERRIDE: rl.Color(255, 255, 255, 255), - UIStatus.ENGAGED: rl.Color(0, 255, 64, 255), - **LANE_LINE_COLORS_SP, -} - - -@dataclass -class ModelPoints: - raw_points: np.ndarray = field(default_factory=lambda: np.empty((0, 3), dtype=np.float32)) - projected_points: np.ndarray = field(default_factory=lambda: np.empty((0, 2), dtype=np.float32)) - - -class LeadVehicle: - def __init__(self): - self.bar = np.empty((0, 2), dtype=np.float32) - self.d_filter = FirstOrderFilter(0.0, 0.2, 1 / gui_app.target_fps, initialized=False) - self.y_filter = FirstOrderFilter(0.0, 0.2, 1 / gui_app.target_fps, initialized=False) - self.fade_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) - - -class ModelRenderer(Widget, ModelRendererSP): - def __init__(self): - Widget.__init__(self) - ModelRendererSP.__init__(self) - self._longitudinal_control = False - self._lane_line_probs = np.zeros(4, dtype=np.float32) - self._road_edge_stds = np.zeros(2, dtype=np.float32) - self._lead_vehicles = [LeadVehicle(), LeadVehicle()] - self._path_offset_z = HEIGHT_INIT[0] - - # Initialize ModelPoints objects - self._path = ModelPoints() - self._lane_lines = [ModelPoints() for _ in range(4)] - self._road_edges = [ModelPoints() for _ in range(2)] - self._acceleration_x = np.empty((0,), dtype=np.float32) - - self._torque_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) - - # 3x3 car space -> rect-origin space (draw methods add rect.x/y) - self._car_space_transform = np.zeros((3, 3), dtype=np.float32) - self._transform_dirty = True - self._clip_region = None - - self._counter = -1 - self._camera_offset = ui_state.params.get("CameraOffset", return_default=True) if ui_state.active_bundle else 0.0 - - self._path_gradient = Gradient( - start=(0.0, 1.0), # Bottom of path - end=(0.0, 0.0), # Top of path - colors=[], - stops=[], - ) - - # Get longitudinal control setting from car parameters - if car_params := Params().get("CarParams"): - cp = messaging.log_from_bytes(car_params, car.CarParams) - self._longitudinal_control = cp.openpilotLongitudinalControl - - def set_transform(self, transform: np.ndarray): - self._car_space_transform = transform.astype(np.float32) - self._transform_dirty = True - - def _render(self, rect: rl.Rectangle): - sm = ui_state.sm - - if self._counter % 180 == 0: # This runs at 60fps, so we query every 3 seconds - self._camera_offset = ui_state.params.get("CameraOffset", return_default=True) if ui_state.active_bundle else 0.0 - self._counter += 1 - - self._torque_filter.update(-ui_state.sm['carOutput'].actuatorsOutput.torque) - - # Check if data is up-to-date - if (sm.recv_frame["extrinsicsCalibration"] < ui_state.started_frame or - sm.recv_frame["modelV2"] < ui_state.started_frame): - return - - # Set up clipping region - self._clip_region = rl.Rectangle( - rect.x - CLIP_MARGIN, rect.y - CLIP_MARGIN, rect.width + 2 * CLIP_MARGIN, rect.height + 2 * CLIP_MARGIN - ) - - # Update state - extrinsics_calibration = sm['extrinsicsCalibration'] - self._path_offset_z = extrinsics_calibration.height[0] if extrinsics_calibration.height else HEIGHT_INIT[0] - - if sm.updated['carParams']: - self._longitudinal_control = sm['carParams'].openpilotLongitudinalControl - - model = sm['modelV2'] - radar_state = sm['radarState'] if sm.valid['radarState'] else None - lead_one = radar_state.leadOne if radar_state else None - render_lead_indicator = self._longitudinal_control and radar_state is not None - - # Update model data when needed - model_updated = sm.updated['modelV2'] - if model_updated or sm.updated['radarState'] or self._transform_dirty: - if model_updated: - self._update_raw_points(model) - - path_x_array = self._path.raw_points[:, 0] - if path_x_array.size == 0: - return - - self._update_model(lead_one, path_x_array) - self._transform_dirty = False - - # Draw elements (hide when disengaged) - if ui_state.status != UIStatus.DISENGAGED: - self._draw_lane_lines() - self._draw_path() - - if render_lead_indicator: - self._update_leads(sm) - self._draw_lead_indicator() - else: - self._lead_vehicles = [LeadVehicle(), LeadVehicle()] - - def _update_raw_points(self, model): - """Update raw 3D points from model data""" - self._path.raw_points = np.array([model.position.x, np.array(model.position.y) + self._camera_offset, model.position.z], dtype=np.float32).T - - for i, lane_line in enumerate(model.laneLines): - self._lane_lines[i].raw_points = np.array([lane_line.x, np.array(lane_line.y) + self._camera_offset, lane_line.z], dtype=np.float32).T - - for i, road_edge in enumerate(model.roadEdges): - self._road_edges[i].raw_points = np.array([road_edge.x, np.array(road_edge.y) + self._camera_offset, road_edge.z], dtype=np.float32).T - - self._lane_line_probs = np.array(model.laneLineProbs, dtype=np.float32) - self._road_edge_stds = np.array(model.roadEdgeStds, dtype=np.float32) - self._acceleration_x = np.array(model.acceleration.x, dtype=np.float32) - - def _update_leads(self, sm): - plan = sm['longitudinalPlan'] - if plan.longitudinalPlanSource == log.LongitudinalPlan.LongitudinalPlanSource.e2e and len(sm['modelV2'].leadsV3) > 1: - leads = [(lead.prob > 0.5, lead.x[0], -lead.y[0]) for lead in list(sm['modelV2'].leadsV3)[:2]] - else: - radar = sm['radarState'] - leads = [(lead.present, lead.dRel + RADAR_TO_CAMERA, lead.yRel) for lead in (radar.leadOne, radar.leadTwo)] - - # both leads can be the same vehicle - if leads[0][0] and abs(leads[1][1] - leads[0][1]) < 3.0: - leads[1] = (False, 0.0, 0.0) - - ss, cs = sm['selfdriveState'], sm['carState'] - # braking disengages without making openpilot unavailable - available = ss.enabled or ss.engageable or cs.brakePressed - lane = self._path.raw_points - opacity = 0.4 if ui_state.status == UIStatus.DISENGAGED else 0.8 - for lead, (present, d_rel, y_rel) in zip(self._lead_vehicles, leads, strict=True): - visible = available and present and d_rel < MAX_DRAW_DISTANCE and len(lane) > 0 - # snap to a new vehicle instead of sliding over - if not visible or abs(y_rel - lead.y_filter.x) > 3.0: - lead.d_filter.initialized = lead.y_filter.initialized = False - lead.fade_filter.update(opacity if visible else 0.0) - if visible: - lead.bar = self._get_lead_bar(lane, lead.d_filter.update(d_rel), lead.y_filter.update(y_rel)) - - def _get_lead_bar(self, lane, d_rel, y_rel): - # bar on the road behind the lead, following the lane - x = np.array([d_rel, d_rel - min(6.0, 0.25 * d_rel)]) - y = np.interp(x, lane[:, 0], lane[:, 1]) - np.interp(d_rel, lane[:, 0], lane[:, 1]) - y_rel + self._camera_offset - z = np.interp(x, self._path.raw_points[:, 0], self._path.raw_points[:, 2]) + self._path_offset_z - corners = np.vstack((np.column_stack((x, y + LEAD_BAR_WIDTH / 2, z)), np.column_stack((x, y - LEAD_BAR_WIDTH / 2, z))[::-1])) - pts = self._car_space_transform @ corners.T - bar = (pts[:2] / pts[2]).T - - far, near = bar[[0, 3]], bar[[1, 2]] - length = np.linalg.norm(near.mean(axis=0) - far.mean(axis=0)) - bar[[1, 2]] = far + (near - far) * np.clip(length, 3.0, LEAD_BAR_LENGTH) / length - return bar.astype(np.float32) - - def _update_model(self, lead, path_x_array): - """Update model visualization data based on model message""" - max_distance = np.clip(path_x_array[-1], MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE) - max_idx = self._get_path_length_idx(self._lane_lines[0].raw_points[:, 0], max_distance) - - # Update lane lines using raw points - line_width_factor = 0.12 - for i, lane_line in enumerate(self._lane_lines): - if i in (1, 2): - line_width_factor = 0.16 - lane_line.projected_points = self._map_line_to_polygon( - lane_line.raw_points, line_width_factor * self._lane_line_probs[i], 0.0, max_idx - ) - - # Update road edges using raw points - for road_edge in self._road_edges: - road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, line_width_factor, 0.0, max_idx) - - # Update path using raw points - if lead and lead.present: - lead_d = lead.dRel * 2.0 - max_distance = np.clip(lead_d - min(lead_d * 0.35, 10.0), 0.0, max_distance) - - max_idx = self._get_path_length_idx(path_x_array, max_distance) - self._path.projected_points = self._map_line_to_polygon( - self._path.raw_points, 0.9, self._path_offset_z, max_idx, allow_invert=False - ) - - self._update_path_gradient() - - def _update_path_gradient(self): - if not self._longitudinal_control: - return - - path_pts = self._path.projected_points + np.array([self._rect.x, self._rect.y], dtype=np.float32) - max_len = min(len(path_pts) // 2, len(self._acceleration_x)) - - segment_colors = [] - gradient_stops = [] - - i = 0 - while i < max_len: - # Some points (screen space) are out of frame (rect space) - track_y = path_pts[i][1] - if track_y < self._rect.y or track_y > (self._rect.y + self._rect.height): - i += 1 - continue - - # Calculate color based on acceleration (0 is bottom, 1 is top) - lin_grad_point = 1 - (track_y - self._rect.y) / self._rect.height - - # speed up: 120, slow down: 0 - path_hue = np.clip(60 + self._acceleration_x[i] * 35, 0, 120) - - saturation = min(abs(self._acceleration_x[i] * 1.5), 1) - lightness = np.interp(saturation, [0.0, 1.0], [0.95, 0.62]) - alpha = np.interp(lin_grad_point, [0.75 / 2.0, 0.75], [0.4, 0.0]) - - # Use HSL to RGB conversion - color = self._hsla_to_color(path_hue / 360.0, saturation, lightness, alpha) - - gradient_stops.append(lin_grad_point) - segment_colors.append(color) - - # Skip a point, unless next is last - i += 1 + (1 if (i + 2) < max_len else 0) - - # Store the gradient in the path object - self._path_gradient.colors = segment_colors - self._path_gradient.stops = gradient_stops - - def _get_ll_color(self, prob: float, adjacent: bool, left: bool): - alpha = np.clip(prob, 0.0, 0.7) - if adjacent: - _base_color = LANE_LINE_COLORS.get(ui_state.status, LANE_LINE_COLORS[UIStatus.DISENGAGED]) - color = rl.Color(_base_color.r, _base_color.g, _base_color.b, int(alpha * 255)) - - # turn adjacent lls orange if torque is high - torque = self._torque_filter.x - high_torque = abs(torque) > 0.6 - if high_torque and (left == (torque > 0)): - color = blend_colors( - color, - rl.Color(255, 115, 0, int(alpha * 255)), # orange - np.interp(abs(torque), [0.6, 0.8], [0.0, 1.0]) - ) - else: - color = rl.Color(255, 255, 255, int(alpha * 255)) - - if ui_state.status == UIStatus.DISENGAGED: - color = rl.Color(0, 0, 0, int(alpha * 255)) - - return color - - def _draw_lane_lines(self): - """Draw lane lines and road edges. Two closest lines should be green (lane line or road edges).""" - offset = np.array([self._rect.x, self._rect.y], dtype=np.float32) - - for i, lane_line in enumerate(self._lane_lines): - if lane_line.projected_points.size == 0: - continue - - color = self._get_ll_color(float(self._lane_line_probs[i]), i in (1, 2), i in (0, 1)) - draw_polygon(self._rect, lane_line.projected_points + offset, color) - - for i, road_edge in enumerate(self._road_edges): - if road_edge.projected_points.size == 0: - continue - - # if closest lane lines are not confident, make road edges green - color = self._get_ll_color(float(1.0 - self._road_edge_stds[i]), float(self._lane_line_probs[i + 1]) < 0.25, i == 0) - draw_polygon(self._rect, road_edge.projected_points + offset, color) - - def _draw_path(self): - if not self._path.projected_points.size: - return - - if ui_state.rainbow_path: - self.rainbow_path.draw_rainbow_path(self._rect, self._path) - return - - path_pts = self._path.projected_points + np.array([self._rect.x, self._rect.y], dtype=np.float32) - - if ui_state.status == UIStatus.DISENGAGED: - draw_polygon(self._rect, path_pts, rl.Color(0, 0, 0, 90)) - elif not self._longitudinal_control: - draw_polygon(self._rect, path_pts, gradient=STOCK_PATH_GRADIENT) - elif len(self._path_gradient.colors) > 1: - draw_polygon(self._rect, path_pts, gradient=self._path_gradient) - else: - draw_polygon(self._rect, path_pts, rl.Color(255, 255, 255, 30)) - - def _draw_lead_indicator(self): - offset = np.array([self._rect.x, self._rect.y], dtype=np.float32) - for lead in self._lead_vehicles: - draw_polygon(self._rect, lead.bar + offset, rl.Color(255, 255, 255, int(255 * lead.fade_filter.x))) - - @staticmethod - def _get_path_length_idx(pos_x_array: np.ndarray, path_height: float) -> int: - """Get the index corresponding to the given path height""" - if len(pos_x_array) == 0: - return 0 - indices = np.where(pos_x_array <= path_height)[0] - return indices[-1] if indices.size > 0 else 0 - - def _map_line_to_polygon(self, line: np.ndarray, y_off: float, z_off: float, max_idx: int, allow_invert: bool = True) -> np.ndarray: - """Convert 3D line to 2D polygon for rendering.""" - if line.shape[0] == 0: - return np.empty((0, 2), dtype=np.float32) - - # Slice points and filter non-negative x-coordinates - points = line[:max_idx + 1] - points = points[points[:, 0] >= 0] - if points.shape[0] == 0: - return np.empty((0, 2), dtype=np.float32) - - N = points.shape[0] - # Generate left and right 3D points in one array using broadcasting - offsets = np.array([[0, -y_off, z_off], [0, y_off, z_off]], dtype=np.float32) - points_3d = points[None, :, :] + offsets[:, None, :] # Shape: 2xNx3 - points_3d = points_3d.reshape(2 * N, 3) # Shape: (2*N)x3 - - # Transform all points to projected space in one operation - proj = self._car_space_transform @ points_3d.T # Shape: 3x(2*N) - proj = proj.reshape(3, 2, N) - left_proj = proj[:, 0, :] - right_proj = proj[:, 1, :] - - # Filter points where z is sufficiently large - valid_proj = (np.abs(left_proj[2]) >= 1e-6) & (np.abs(right_proj[2]) >= 1e-6) - if not np.any(valid_proj): - return np.empty((0, 2), dtype=np.float32) - - # Compute screen coordinates - left_screen = left_proj[:2, valid_proj] / left_proj[2, valid_proj][None, :] - right_screen = right_proj[:2, valid_proj] / right_proj[2, valid_proj][None, :] - - # Define clip region bounds - clip = self._clip_region - x_min, x_max = clip.x, clip.x + clip.width - y_min, y_max = clip.y, clip.y + clip.height - - # Filter points within clip region - left_in_clip = ( - (left_screen[0] >= x_min) & (left_screen[0] <= x_max) & - (left_screen[1] >= y_min) & (left_screen[1] <= y_max) - ) - right_in_clip = ( - (right_screen[0] >= x_min) & (right_screen[0] <= x_max) & - (right_screen[1] >= y_min) & (right_screen[1] <= y_max) - ) - both_in_clip = left_in_clip & right_in_clip - - if not np.any(both_in_clip): - return np.empty((0, 2), dtype=np.float32) - - # Select valid and clipped points - left_screen = left_screen[:, both_in_clip] - right_screen = right_screen[:, both_in_clip] - - # Handle Y-coordinate inversion on hills - if not allow_invert and left_screen.shape[1] > 1: - y = left_screen[1, :] # y-coordinates - keep = y == np.minimum.accumulate(y) - if not np.any(keep): - return np.empty((0, 2), dtype=np.float32) - left_screen = left_screen[:, keep] - right_screen = right_screen[:, keep] - - return np.vstack((left_screen.T, right_screen[:, ::-1].T)).astype(np.float32) - - @staticmethod - def _hsla_to_color(h, s, l, a): - rgb = colorsys.hls_to_rgb(h, l, s) - return rl.Color( - int(rgb[0] * 255), - int(rgb[1] * 255), - int(rgb[2] * 255), - int(a * 255) - ) diff --git a/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py b/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py deleted file mode 100755 index c0f1ac151..000000000 --- a/openpilot/selfdrive/ui/mici/tests/test_widget_leaks.py +++ /dev/null @@ -1,122 +0,0 @@ -import gc -import weakref -import unittest - -# FIXME: known small leaks not worth worrying about at the moment -KNOWN_LEAKS = { - "openpilot.selfdrive.ui.mici.onroad.cabin_camera_dialog.CabinCameraView", - "openpilot.selfdrive.ui.mici.layouts.onboarding.TermsPage", - "openpilot.selfdrive.ui.mici.layouts.onboarding.TrainingGuide", - "openpilot.selfdrive.ui.mici.layouts.onboarding.DeclinePage", - "openpilot.selfdrive.ui.mici.layouts.onboarding.OnboardingWindow", - "openpilot.selfdrive.ui.onroad.driver_state.DriverStateRenderer", - "openpilot.selfdrive.ui.onroad.cabin_camera_dialog.CabinCameraDialog", - "openpilot.selfdrive.ui.layouts.onboarding.TermsPage", - "openpilot.selfdrive.ui.layouts.onboarding.DeclinePage", - "openpilot.selfdrive.ui.layouts.onboarding.OnboardingWindow", - "openpilot.system.ui.widgets.confirm_dialog.ConfirmDialog", - "openpilot.system.ui.widgets.label.Label", - "openpilot.system.ui.widgets.button.Button", - "openpilot.system.ui.widgets.html_render.HtmlRenderer", - "openpilot.system.ui.widgets.nav_widget.NavBar", - "openpilot.selfdrive.ui.mici.layouts.settings.device.MiciFccModal", - "openpilot.system.ui.widgets.inputbox.InputBox", - "openpilot.system.ui.widgets.scroller_tici.Scroller", - "openpilot.system.ui.widgets.label.UnifiedLabel", - "openpilot.system.ui.widgets.mici_keyboard.MiciKeyboard", - "openpilot.selfdrive.ui.mici.widgets.dialog.BigConfirmationDialog", - "openpilot.system.ui.widgets.keyboard.Keyboard", - "openpilot.system.ui.widgets.slider.BigSlider", - "openpilot.selfdrive.ui.mici.widgets.dialog.BigInputDialog", - "openpilot.system.ui.widgets.option_dialog.MultiOptionDialog", -} - - -def get_child_widgets(widget) -> list: - from openpilot.system.ui.widgets import Widget - children = [] - for val in widget.__dict__.values(): - items = val if isinstance(val, (list, tuple)) else (val,) - children.extend(w for w in items if isinstance(w, Widget)) - return children - - -from openpilot.common.test import OpenpilotTestCase -class TestWidgetLeaks(OpenpilotTestCase): - @unittest.skip("segfaults") - def test_dialogs_do_not_leak(self): - import pyray as rl - rl.set_config_flags(rl.ConfigFlags.FLAG_WINDOW_HIDDEN) - from openpilot.system.ui.lib.application import gui_app - - # mici dialogs - from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide as MiciTrainingGuide, OnboardingWindow as MiciOnboardingWindow - from openpilot.selfdrive.ui.mici.onroad.cabin_camera_dialog import CabinCameraDialog as MiciCabinCameraDialog - from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog as MiciPairingDialog - from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog, BigInputDialog - from openpilot.selfdrive.ui.mici.layouts.settings.device import MiciFccModal - - # tici dialogs - from openpilot.selfdrive.ui.onroad.cabin_camera_dialog import CabinCameraDialog as TiciCabinCameraDialog - from openpilot.selfdrive.ui.layouts.onboarding import OnboardingWindow as TiciOnboardingWindow - from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog as TiciPairingDialog - from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog - from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog - from openpilot.system.ui.widgets.html_render import HtmlModal - from openpilot.system.ui.widgets.keyboard import Keyboard - - gui_app.init_window("ref-test") - - leaked_widgets = set() - - for ctor in ( - # mici - MiciCabinCameraDialog, MiciPairingDialog, - lambda: MiciTrainingGuide(lambda: None), - lambda: MiciOnboardingWindow(lambda: None), - lambda: BigDialog("test", "test"), - lambda: BigConfirmationDialog("test", gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64), lambda: None), - lambda: BigInputDialog("test"), - lambda: MiciFccModal(text="test"), - # tici - TiciCabinCameraDialog, TiciOnboardingWindow, TiciPairingDialog, Keyboard, - lambda: ConfirmDialog("test", "ok"), - lambda: MultiOptionDialog("test", ["a", "b"]), - lambda: HtmlModal(text="test"), - ): - widget = ctor() - all_refs = [weakref.ref(w) for w in get_child_widgets(widget) + [widget]] - - del widget - - for ref in all_refs: - if ref() is not None: - obj = ref() - name = f"{type(obj).__module__}.{type(obj).__qualname__}" - leaked_widgets.add(name) - - print(f"\n=== Widget {name} alive after del") - print(" Referrers:") - for r in gc.get_referrers(obj): - if r is obj: - continue - - if hasattr(r, '__self__') and r.__self__ is not obj: - print(f" bound method: {type(r.__self__).__qualname__}.{r.__name__}") - elif hasattr(r, '__func__'): - print(f" method: {r.__name__}") - else: - print(f" {type(r).__module__}.{type(r).__qualname__}") - del obj - - gui_app.close() - - unexpected = leaked_widgets - KNOWN_LEAKS - assert not unexpected, f"New leaked widgets: {unexpected}" - - fixed = KNOWN_LEAKS - leaked_widgets - assert not fixed, f"These leaks are fixed, remove from KNOWN_LEAKS: {fixed}" - - -if __name__ == "__main__": - TestWidgetLeaks().test_dialogs_do_not_leak() diff --git a/openpilot/selfdrive/ui/mici/widgets/info.py b/openpilot/selfdrive/ui/mici/widgets/info.py deleted file mode 100644 index 2d765cd6f..000000000 --- a/openpilot/selfdrive/ui/mici/widgets/info.py +++ /dev/null @@ -1,31 +0,0 @@ -import pyray as rl - -from openpilot.system.ui.lib.application import FontWeight -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.label import UnifiedLabel - - -class InfoLayoutMici(Widget): - def __init__(self, title1: str, subtext1: str, title2: str, subtext2: str, *, width: int = 360): - super().__init__() - self.set_rect(rl.Rectangle(0, 0, width, 180)) - - max_width = int(self._rect.width - 20) - self._subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)) - - self.title1 = UnifiedLabel(title1, 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) - self.subtext1 = UnifiedLabel(subtext1, 36, max_width=max_width, text_color=self._subheader_color, - font_weight=FontWeight.ROMAN, wrap_text=False, scroll=True) - self.title2 = UnifiedLabel(title2, 48, max_width=max_width, font_weight=FontWeight.DISPLAY, wrap_text=False) - self.subtext2 = UnifiedLabel(subtext2, 36, max_width=max_width, text_color=self._subheader_color, - font_weight=FontWeight.ROMAN, wrap_text=False, scroll=True) - - def _layout(self): - self.title1.set_position(self._rect.x + 20, self._rect.y - 10) - self.subtext1.set_position(self._rect.x + 20, self._rect.y + 68 - 25) - self.title2.set_position(self._rect.x + 20, self._rect.y + 114 - 30) - self.subtext2.set_position(self._rect.x + 20, self._rect.y + 161 - 25) - - def _render(self, rect: rl.Rectangle): - for widget in [self.title1, self.subtext1, self.title2, self.subtext2]: - widget.render() diff --git a/openpilot/selfdrive/ui/mici/widgets/pairing_dialog.py b/openpilot/selfdrive/ui/mici/widgets/pairing_dialog.py deleted file mode 100644 index 7e05bc2fa..000000000 --- a/openpilot/selfdrive/ui/mici/widgets/pairing_dialog.py +++ /dev/null @@ -1,95 +0,0 @@ -import pyray as rl -import time - -from openpilot.common.api import Api -from openpilot.common.qrcode import make_texture -from openpilot.common.swaglog import cloudlog -from openpilot.common.params import Params -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.ui.widgets.nav_widget import NavWidget -from openpilot.system.ui.lib.application import FontWeight, gui_app -from openpilot.system.ui.widgets.label import UnifiedLabel - - -class PairingDialog(NavWidget): - """Dialog for device pairing with QR code.""" - - QR_REFRESH_INTERVAL = 300 # 5 minutes in seconds - - def __init__(self): - super().__init__() - self._params = Params() - self._qr_texture: rl.Texture | None = None - self._last_qr_generation = float("-inf") - - self._txt_pair = gui_app.texture("icons_mici/settings/device/pair.png", 33, 60) - self._pair_label = UnifiedLabel("pair with comma connect", font_size=48, font_weight=FontWeight.BOLD, line_height=0.8) - - def _get_pairing_url(self) -> str: - try: - dongle_id = self._params.get("DongleId") or "" - token = Api(dongle_id).get_token({'pair': True}) - except Exception as e: - cloudlog.warning(f"Failed to get pairing token: {e}") - token = "" - return f"https://connect.comma.ai/?pair={token}" - - def _generate_qr_code(self) -> None: - try: - if self._qr_texture and self._qr_texture.id != 0: - rl.unload_texture(self._qr_texture) - self._qr_texture = make_texture(self._get_pairing_url(), inverted=True) - except Exception as e: - cloudlog.warning(f"QR code generation failed: {e}") - self._qr_texture = None - - def _check_qr_refresh(self) -> None: - current_time = time.monotonic() - if current_time - self._last_qr_generation >= self.QR_REFRESH_INTERVAL: - self._generate_qr_code() - self._last_qr_generation = current_time - - def _update_state(self): - super()._update_state() - if ui_state.prime_state.is_paired() and not self.is_dismissing: - self.dismiss() - - def _render(self, rect: rl.Rectangle): - self._check_qr_refresh() - - self._render_qr_code() - - label_x = self._rect.x + 8 + self._rect.height + 24 - self._pair_label.set_max_width(int(self._rect.width - label_x)) - self._pair_label.set_position(label_x, self._rect.y + 16) - self._pair_label.render() - - rl.draw_texture_ex(self._txt_pair, rl.Vector2(label_x, self._rect.y + self._rect.height - self._txt_pair.height - 16), - 0.0, 1.0, rl.Color(255, 255, 255, int(255 * 0.35))) - - def _render_qr_code(self) -> None: - if not self._qr_texture: - error_font = gui_app.font(FontWeight.BOLD) - rl.draw_text_ex( - error_font, "QR Code Error", rl.Vector2(self._rect.x + 20, self._rect.y + self._rect.height // 2 - 15), 30, 0.0, rl.RED - ) - return - - scale = self._rect.height / self._qr_texture.height - pos = rl.Vector2(round(self._rect.x + 8), round(self._rect.y)) - rl.draw_texture_ex(self._qr_texture, pos, 0.0, scale, rl.WHITE) - - def __del__(self): - if self._qr_texture and self._qr_texture.id != 0: - rl.unload_texture(self._qr_texture) - - -if __name__ == "__main__": - gui_app.init_window("pairing device") - pairing = PairingDialog() - gui_app.push_widget(pairing) - try: - for _ in gui_app.render(): - pass - finally: - del pairing diff --git a/openpilot/selfdrive/ui/mici/widgets/qr.py b/openpilot/selfdrive/ui/mici/widgets/qr.py deleted file mode 100644 index f0ff0d0ed..000000000 --- a/openpilot/selfdrive/ui/mici/widgets/qr.py +++ /dev/null @@ -1,37 +0,0 @@ -import pyray as rl - -from openpilot.common.swaglog import cloudlog -from openpilot.system.ui.lib.application import FontWeight -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.label import Label -from openpilot.common.qrcode import make_texture - - -class QR(Widget): - def __init__(self, url: str, width: int = 170): - super().__init__() - self._url = url - - self._texture = self._generate_qr_code() - - self.set_rect(rl.Rectangle(0, 0, width, width)) - self._error = Label("QR Code Error", font_size=30, font_weight=FontWeight.BOLD, text_color=rl.RED) - - def _generate_qr_code(self): - try: - return make_texture(self._url, inverted=True) - except Exception as e: - cloudlog.warning(f"QR code generation failed: {e}") - return None - - def _render(self, rect: rl.Rectangle): - if not self._texture: - self._error.render(rect) - return - scale = rect.height / self._texture.height - pos = rl.Vector2(round(rect.x), round(rect.y)) - rl.draw_texture_ex(self._texture, pos, 0.0, scale, rl.WHITE) - - def __del__(self): - if self._texture and self._texture.id != 0: - rl.unload_texture(self._texture) diff --git a/openpilot/selfdrive/ui/mici/onroad/torque_bar.py b/openpilot/selfdrive/ui/onroad/torque_bar.py similarity index 97% rename from openpilot/selfdrive/ui/mici/onroad/torque_bar.py rename to openpilot/selfdrive/ui/onroad/torque_bar.py index c4ccafa0a..d320758bf 100644 --- a/openpilot/selfdrive/ui/mici/onroad/torque_bar.py +++ b/openpilot/selfdrive/ui/onroad/torque_bar.py @@ -6,7 +6,6 @@ from collections import OrderedDict import numpy as np import pyray as rl from opendbc.car import ACCELERATION_DUE_TO_GRAVITY -from openpilot.selfdrive.ui.mici.onroad import blend_colors from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient @@ -19,6 +18,13 @@ TORQUE_ANGLE_SPAN = 12.7 DEBUG = False +def blend_colors(a: rl.Color, b: rl.Color, f: float) -> rl.Color: + h0, s0, v0 = (hsv0 := rl.color_to_hsv(a)).x, hsv0.y, hsv0.z + h1, s1, v1 = (hsv1 := rl.color_to_hsv(b)).x, hsv1.y, hsv1.z + dh = ((h1 - h0 + 180) % 360) - 180 + return rl.color_from_hsv((h0 + f * dh) % 360, s0 + f * (s1 - s0), v0 + f * (v1 - v0)) + + def quantized_lru_cache(maxsize=128): def decorator(func): cache = OrderedDict() diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/__init__.py b/openpilot/selfdrive/ui/sunnypilot/mici/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/__init__.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py deleted file mode 100644 index 5d83f7fef..000000000 --- a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/home.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - -This file is part of sunnypilot and is licensed under the MIT License. -See the LICENSE.md file in the root directory for more details. -""" -import math - -import pyray as rl - -from openpilot.selfdrive.ui.mici.layouts.home import MiciHomeLayout -from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState -from openpilot.system.ui.lib.application import FontWeight -from openpilot.system.ui.widgets.label import UnifiedLabel - - -class MiciHomeLayoutSP(MiciHomeLayout): - def __init__(self): - super().__init__() - self._openpilot_label = UnifiedLabel("", font_size=88, font_weight=FontWeight.AUDIOWIDE, max_width=480, wrap_text=False) - - def _set_chestnut_visibility(self): - usb_connected = ui_state.usb_connected - usb_unknown = ui_state.usb_unknown - chestnut_state = ui_state.chestnut_state - loading = chestnut_state == ChestnutState.LOADING - - self._usb_icon.set_visible(usb_connected and usb_unknown) - self._chestnut_loading_icon.set_opacity(0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))) - self._chestnut_loading_icon.set_visible(not usb_unknown and loading) - self._chestnut_icon.set_visible(not usb_unknown and not loading and - chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE)) - self._chestnut_failed_icon.set_visible(not usb_unknown and chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED)) diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py deleted file mode 100644 index 404ff2e30..000000000 --- a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/models.py +++ /dev/null @@ -1,283 +0,0 @@ -""" -Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - -This file is part of sunnypilot and is licensed under the MIT License. -See the LICENSE.md file in the root directory for more details. -""" -import time - -import pyray as rl - -from openpilot.cereal import custom -from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, BigDialog -from openpilot.sunnypilot.models.helpers import ACTIVE_BUNDLE_KEYS, get_selected_bundle -from openpilot.selfdrive.ui.mici.widgets.button import BigButton -from openpilot.selfdrive.ui.ui_state import ui_state, device -from openpilot.selfdrive.ui.sunnypilot.model_info import (active_source, big_model_state, bundles_for_source, carrying_model, - default_model_name, model_cache_size_mb, model_info, queued_name, - refresh_in_progress, refresh_model_list) -from openpilot.system.ui.lib.application import FontWeight, gui_app -from openpilot.system.ui.lib.multilang import tr -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.system.ui.widgets.scroller import NavScroller - -def _model_info() -> tuple[str, str, str]: - """(active model, info header, info text) for the panel. Runner-matched: the - active line names what actually drives, and a notable big-model state takes - the info pair.""" - source, active_name, other_name = model_info() - state = big_model_state() - _, _, carry_display = carrying_model() - if carry_display is None: - big = get_selected_bundle(ui_state.params, "chestnut") - carry_display = big.displayName if big else default_model_name("chestnut") - active_text = (carry_display or active_name).lower() - if state == 'failed': - return active_text, tr("big model"), tr("unavailable") - if state == 'loading': - return active_text, tr("big model"), tr("getting ready") - header = tr("small model") if source == "chestnut" else tr("big model") - return active_text, header, other_name.lower() - - -class CurrentModelInfo(Widget): - def __init__(self): - super().__init__() - - self.set_rect(rl.Rectangle(0, 0, 360, 180)) - - header_color = rl.Color(255, 255, 255, int(255 * 0.9)) - subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)) - max_width = int(self._rect.width - 20) - active_text, info_header, info_text = _model_info() - self.current_model_header = UnifiedLabel(tr("active model"), 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY) - self.current_model_text = UnifiedLabel(active_text, 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, scroll=True) - - self.info_header = UnifiedLabel(info_header, 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY) - self.info_text = UnifiedLabel(info_text, 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, scroll=True) - - def _render(self, _): - self.current_model_header.set_position(self._rect.x + 20, self._rect.y - 10) - self.current_model_header.render() - - self.current_model_text.set_position(self._rect.x + 20, self._rect.y + 68 - 25) - self.current_model_text.render() - - self.info_header.set_position(self._rect.x + 20, self._rect.y + 114 - 30) - self.info_header.render() - - self.info_text.set_position(self._rect.x + 20, self._rect.y + 161 - 25) - self.info_text.render() - -class ModelsLayoutMici(NavScroller): - def __init__(self): - super().__init__() - self.focused_widget = None - - self.current_model_info = CurrentModelInfo() - self._download_progress = "." - self._download_frame = 0 - self._was_downloading = False - self._selection_source: str | None = None - - self.select_model_btn = BigButton(tr("select model")) - self.select_model_btn.set_click_callback(self._show_folders) - - self.refresh_btn = BigButton(tr("refresh models")) - self.refresh_btn.set_click_callback(self._refresh_models) - self._refresh_start: float | None = None - - self.cancel_download_btn = BigButton(tr("cancel download")) - self.cancel_download_btn.set_click_callback(lambda: ui_state.params.remove("ModelManager_DownloadRef")) - - self.clear_cache_btn = BigButton(tr("clear cache"), value=f"{model_cache_size_mb():.1f} MB") - self.clear_cache_btn.set_click_callback(self._confirm_clear_cache) - self._cache_size_time = 0.0 - - self.main_items = [self.current_model_info, self.select_model_btn, self.cancel_download_btn, self.refresh_btn, self.clear_cache_btn] - self._scroller.add_widgets(self.main_items) - - @property - def model_manager(self): - return ui_state.sm["modelManagerSP"] - - def _get_grouped_bundles(self, bundles, favorites = None): - folders = {} - for bundle in bundles: - folder = next((override.value for override in bundle.overrides if override.key == "folder"), "") - folders.setdefault(folder, []).append(bundle) - - if favorites: - for fav_bundle in [bundle for bundle in bundles if bundle.ref in favorites]: - folders.setdefault("favorites", []).append(fav_bundle) - - return folders - - def _push_selection_view(self, items): - scroller = NavScroller() - scroller._scroller.add_widgets(items) - gui_app.push_widget(scroller) - - def _show_folders(self): - self.focused_widget = self.select_model_btn - - hardware_btns = [] - active = active_source() - for source, label in (("qcom", tr("small models")), ("chestnut", tr("big models"))): - bundle = get_selected_bundle(ui_state.params, source) - value = (bundle.internalName if bundle else default_model_name(source)).lower() - if source == active: - value += f" ({tr('active')})" - btn = BigButton(label.lower(), value=value) - btn.set_click_callback(lambda s=source: self._select_hardware(s)) - hardware_btns.append(btn) - self._push_selection_view(hardware_btns) - - def _select_hardware(self, source): - self._selection_source = source - - favs = ui_state.params.get("ModelManager_Favs") - favorites = set(favs.split(';')) if favs else set() - - bundles = bundles_for_source(source) - if not bundles: - gui_app.push_widget(BigDialog(title=tr("No models available"), - description=tr("No models are available for this hardware yet. Connect to the internet and refresh the model list."))) - return - folders = self._get_grouped_bundles(bundles, favorites) - - folder_buttons = [] - default_btn = BigButton(default_model_name(source).lower()) - default_btn.set_click_callback(lambda s=source: self._select_default(s)) - folder_buttons.append(default_btn) - - for folder in sorted(folders.keys(), key=lambda f: max((bundle.index for bundle in folders[f]), default=-1), reverse=True): - btn = BigButton(folder.lower()) - btn.set_click_callback(lambda f=folder: self._select_folder(f)) - if folder.lower() == "favorites": - folder_buttons.insert(0, btn) - else: - folder_buttons.append(btn) - self._push_selection_view(folder_buttons) - - def _pop_to_main(self): - gui_app.pop_widgets_to(self) - self._scroller.scroll_panel.set_offset(0.0) - - def _select_model(self, bundle): - ui_state.params.put("ModelManager_DownloadRef", bundle.ref) - self._pop_to_main() - - def _select_default(self, source): - ui_state.params.remove(ACTIVE_BUNDLE_KEYS[source]) - self._pop_to_main() - - def _confirm_clear_cache(self): - icon = gui_app.texture("icons_mici/settings/network/new/trash.png", 54, 64) - gui_app.push_widget(BigConfirmationDialog(f"{tr('slide to')}\n{tr('clear cache')}", icon, - lambda: ui_state.params.put_bool("ModelManager_ClearCache", True), red=True)) - - def _refresh_models(self): - refresh_model_list() - self._refresh_start = time.monotonic() - - def _select_folder(self, folder_name): - source = self._selection_source - if source is None: # folders are only reachable after picking a hardware - return - favs = ui_state.params.get("ModelManager_Favs") - favorites = set(favs.split(';')) if favs else set() - - folders = self._get_grouped_bundles(bundles_for_source(source), favorites) - bundles = sorted(folders.get(folder_name, []), key=lambda b: b.index, reverse=True) - - btns = [] - for bundle in bundles: - btn = BigButton(bundle.displayName.lower()) - btn.set_click_callback(lambda b=bundle: self._select_model(b)) - btns.append(btn) - self._push_selection_view(btns) - - def hide_event(self): - super().hide_event() - if self._was_downloading: - device.set_override_interactive_timeout(None) - self._was_downloading = False - - def _update_state(self): - super()._update_state() - - self.select_model_btn.set_enabled(ui_state.is_offroad()) - self.cancel_download_btn.set_visible(False) - self.current_model_info.current_model_header._shimmer = False - self.current_model_info.info_header._shimmer = False - - manager = self.model_manager - self._download_frame += 1 - should_update = self._download_frame % (gui_app.target_fps / 2) == 0 - if should_update: - self._download_progress = self._download_progress + "." if len(self._download_progress) < 3 else "" - - is_downloading = (manager.selectedBundle - and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading) - if self._was_downloading and not is_downloading: - device.set_override_interactive_timeout(None) - self._was_downloading = is_downloading - - # manager is offroad-only, so an onroad clear would never be serviced - clearing = ui_state.params.get_bool("ModelManager_ClearCache") - self.clear_cache_btn.set_enabled(ui_state.is_offroad() and not is_downloading and not clearing) - if clearing: - self.clear_cache_btn.set_value(tr("clearing...")) - self._cache_size_time = 0.0 # refresh the size as soon as clearing finishes - elif (now := time.monotonic()) - self._cache_size_time > 0.5: - self._cache_size_time = now - self.clear_cache_btn.set_value(f"{model_cache_size_mb():.1f} MB") - - # manager is offroad-only, so a refresh queued onroad would never be serviced - refreshing = refresh_in_progress(self._refresh_start) - self.refresh_btn.set_enabled(ui_state.is_offroad() and not is_downloading and not refreshing) - self.refresh_btn.set_value(tr("fetching...") if refreshing else "") - - self.current_model_info.current_model_header.set_text(tr("active model")) - active_text, info_header, info_text = _model_info() - self.current_model_info.current_model_text.set_text(active_text) - self.current_model_info.info_header.set_text(info_header) - self.current_model_info.info_text.set_text(info_text) - - if manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.failed: - self.current_model_info.info_header.set_text(tr("error") + self._download_progress) - self.current_model_info.info_text.set_text(tr("download failed")) - - elif manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading: - self.cancel_download_btn.set_visible(True) - device.set_override_interactive_timeout(5) - progress = 0.0 - count = 0 - verifying = False - for model in manager.selectedBundle.models: - count += 1 - p = model.artifact.downloadProgress - if p.status in (custom.ModelManagerSP.DownloadStatus.downloading, - custom.ModelManagerSP.DownloadStatus.verifying): - progress += p.progress - verifying = verifying or p.status == custom.ModelManagerSP.DownloadStatus.verifying - elif p.status in (custom.ModelManagerSP.DownloadStatus.downloaded, - custom.ModelManagerSP.DownloadStatus.cached): - progress += 100.0 - - self.current_model_info.current_model_header.set_text(tr("verifying") if verifying else tr("downloading")) - self.cancel_download_btn.set_text(tr("cancel verification") if verifying else tr("cancel download")) - self.current_model_info.current_model_header._shimmer = True - name_text = manager.selectedBundle.internalName.lower() - if queued := queued_name(manager.selectedBundle.ref): - name_text += f" | {queued.lower()} {tr('queued')}" - self.current_model_info.current_model_text.set_text(name_text) - self.current_model_info.info_header.set_text(tr("progress") + self._download_progress) - self.current_model_info.info_header._shimmer = True - self.current_model_info.info_text.set_text(f"{progress/count:.2f}%") - - elif manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloaded: - self.current_model_info.info_header.set_text(tr("downloaded")) - self.current_model_info.info_text.set_text(tr("downloaded")) diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/onboarding.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/onboarding.py deleted file mode 100644 index 5aed8fac3..000000000 --- a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/onboarding.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - -This file is part of sunnypilot and is licensed under the MIT License. -See the LICENSE.md file in the root directory for more details. -""" -from collections.abc import Callable - -from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationCircleButton -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.mici_setup import GreyBigButton -from openpilot.system.ui.widgets.scroller import NavScroller - - -class SunnylinkConsentPage(NavScroller): - def __init__(self, on_accept: Callable | None = None, on_decline: Callable | None = None): - super().__init__() - - assert on_accept is not None and callable(on_accept) - assert on_decline is not None and callable(on_decline) - - self._accept_button = BigConfirmationCircleButton("enable\nsunnylink", gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 64, 64), - on_accept, exit_on_confirm=False) - - self._decline_button = BigConfirmationCircleButton("disable\nsunnylink", gui_app.texture("icons_mici/setup/cancel.png", 64, 64), - on_decline, red=True, exit_on_confirm=False) - - self._scroller.add_widgets([ - GreyBigButton("sunnylink", "scroll to continue", - gui_app.texture("../../sunnypilot/selfdrive/assets/logo.png", 64, 64)), - GreyBigButton("", "sunnylink enables secured remote access to your comma device from anywhere."), - self._accept_button, - self._decline_button, - ]) diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/settings.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/settings.py deleted file mode 100644 index f14efe51a..000000000 --- a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/settings.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - -This file is part of sunnypilot and is licensed under the MIT License. -See the LICENSE.md file in the root directory for more details. -""" -from openpilot.selfdrive.ui.mici.layouts.settings import settings as OP -from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsBigButton -from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici -from openpilot.selfdrive.ui.mici.widgets.button import BigCircleButton -from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, BigDialog -from openpilot.selfdrive.ui.sunnypilot.mici.layouts.sunnylink import SunnylinkLayoutMici -from openpilot.selfdrive.ui.sunnypilot.mici.layouts.models import ModelsLayoutMici -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.multilang import tr - -ICON_SIZE = 70 -BIG_ICON_SIZE = 110 - - -class SettingsLayoutSP(OP.SettingsLayout): - def __init__(self): - OP.SettingsLayout.__init__(self) - - device_panel = DeviceLayoutMici() - self._scroller._items[2].set_click_callback(lambda: gui_app.push_widget(device_panel)) - - self.icon_offroad_enable = gui_app.texture("../../sunnypilot/selfdrive/assets/icons_mici/always_offroad.png", BIG_ICON_SIZE, - BIG_ICON_SIZE) - self.icon_offroad_disable = gui_app.texture("../../sunnypilot/selfdrive/assets/icons_mici/disable_offroad.png", BIG_ICON_SIZE, - BIG_ICON_SIZE) - self.icon_offroad_slider = gui_app.texture("icons_mici/settings/device/lkas.png", BIG_ICON_SIZE, BIG_ICON_SIZE) - - sunnylink_panel = SunnylinkLayoutMici() - sunnylink_btn = SettingsBigButton(tr("sunnylink"), "", gui_app.texture("icons_mici/settings/developer/ssh.png", 55, 55)) - sunnylink_btn.set_click_callback(lambda: gui_app.push_widget(sunnylink_panel)) - - models_panel = ModelsLayoutMici() - models_btn = SettingsBigButton(tr("models"), "", gui_app.texture("../../sunnypilot/selfdrive/assets/offroad/icon_models.png", ICON_SIZE, ICON_SIZE)) - models_btn.set_click_callback(lambda: gui_app.push_widget(models_panel)) - - # onroad: enable button sits at the front (left of toggles) - self._enable_offroad_btn_onroad = BigCircleButton(self.icon_offroad_enable, red=True) - self._enable_offroad_btn_onroad.set_click_callback(lambda: self._handle_always_offroad(True)) - self._enable_offroad_btn_onroad.set_visible(lambda: ui_state.started and not ui_state.always_offroad) - - # offroad: enable button sits at the end (right of developer) - self._enable_offroad_btn_offroad = BigCircleButton(self.icon_offroad_enable, red=True) - self._enable_offroad_btn_offroad.set_click_callback(lambda: self._handle_always_offroad(True)) - self._enable_offroad_btn_offroad.set_visible(lambda: not ui_state.started and not ui_state.always_offroad) - - self._disable_offroad_btn = BigCircleButton(self.icon_offroad_disable, red=False) - self._disable_offroad_btn.set_click_callback(lambda: self._handle_always_offroad(False)) - self._disable_offroad_btn.set_visible(lambda: ui_state.always_offroad) - - items = self._scroller._items.copy() - - items.insert(1, sunnylink_btn) - items.insert(2, models_btn) - - # front slots (only one ever visible at a time): exit-always-offroad, then enable-onroad - items.insert(0, self._enable_offroad_btn_onroad) - items.insert(0, self._disable_offroad_btn) - # end slot: enable-offroad (right of developer) - items.append(self._enable_offroad_btn_offroad) - - self._scroller._items.clear() - for item in items: - self._scroller.add_widget(item) - - def _update_state(self): - super()._update_state() - - def _handle_always_offroad(self, enable: bool): - - def _set_offroad_status(status: bool): - if not ui_state.engaged: - ui_state.params.put_bool("OffroadMode", status) - ui_state.always_offroad = status - - if not enable: - dlg = BigConfirmationDialog(tr("slide to exit always offroad"), self.icon_offroad_slider, red=False, - confirm_callback=lambda: _set_offroad_status(False)) - else: - if ui_state.engaged: - gui_app.push_widget(BigDialog(tr("disengage to enable always offroad"), "", )) - return - - dlg = BigConfirmationDialog(tr("slide to force offroad"), self.icon_offroad_slider, red=True, - confirm_callback=lambda: _set_offroad_status(True)) - gui_app.push_widget(dlg) diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py b/openpilot/selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py deleted file mode 100644 index 7c42f99f8..000000000 --- a/openpilot/selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py +++ /dev/null @@ -1,254 +0,0 @@ -""" -Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - -This file is part of sunnypilot and is licensed under the MIT License. -See the LICENSE.md file in the root directory for more details. -""" -import pyray as rl - - -from openpilot.cereal import custom -from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle -from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialog -from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkConsentPage -from openpilot.selfdrive.ui.sunnypilot.mici.widgets.sunnylink_pairing_dialog import SunnylinkPairingDialog -from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID -from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight -from openpilot.system.ui.lib.multilang import tr -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.system.ui.widgets.scroller import NavScroller -from openpilot.common.version import sunnylink_consent_version, sunnylink_consent_declined - -class SunnylinkInfo(Widget): - def __init__(self): - super().__init__() - - self.set_rect(rl.Rectangle(0, 0, 360, 180)) - - header_color = rl.Color(255, 255, 255, int(255 * 0.9)) - subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)) - max_width = int(self._rect.width - 20) - self.device_id_header = UnifiedLabel(tr("device id"), 48, max_width=max_width, text_color=header_color, - font_weight=FontWeight.DISPLAY, shimmer=True) - self.device_id_text = UnifiedLabel(UNREGISTERED_SUNNYLINK_DONGLE_ID, 32, max_width=max_width, text_color=subheader_color, - font_weight=FontWeight.ROMAN, scroll=True) - - self.sponsor_header = UnifiedLabel(tr("sponsor tier"), 48, max_width=max_width, text_color=header_color, - font_weight=FontWeight.DISPLAY, shimmer=True) - self.sponsor_text = UnifiedLabel("N/A", 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN) - - def _render(self, _): - self.device_id_header.set_position(self._rect.x + 20, self._rect.y - 10) - self.device_id_header.render() - - self.device_id_text.set_position(self._rect.x + 20, self._rect.y + 68 - 25) - self.device_id_text.render() - - self.sponsor_header.set_position(self._rect.x + 20, self._rect.y + 114 - 30) - self.sponsor_header.render() - - self.sponsor_text.set_position(self._rect.x + 20, self._rect.y + 161 - 25) - self.sponsor_text.render() - -class SunnylinkLayoutMici(NavScroller): - def __init__(self): - super().__init__() - self._restore_in_progress = False - self._backup_in_progress = False - self._sunnylink_enabled = ui_state.params.get("SunnylinkEnabled") - - self._sunnylink_info = SunnylinkInfo() - - self._sunnylink_toggle = BigToggle(text=tr("enable sunnylink"), - initial_state=self._sunnylink_enabled, - toggle_callback=self._sunnylink_toggle_callback) - self._sunnylink_sponsor_button = SunnylinkPairBigButton(sponsor_pairing=False) - self._sunnylink_pair_button = SunnylinkPairBigButton(sponsor_pairing=True) - self._backup_btn = BigButton(tr("backup settings"), "") - self._backup_btn.set_click_callback(lambda: self._handle_backup_restore_btn(restore=False)) - self._restore_btn = BigButton(tr("restore settings"), "") - self._restore_btn.set_click_callback(lambda: self._handle_backup_restore_btn(restore=True)) - self._sunnylink_uploader_toggle = BigToggle(text=tr("sunnylink uploader"), initial_state=False, - toggle_callback=self._sunnylink_uploader_callback) - - self._scroller.add_widgets([ - self._sunnylink_info, - self._sunnylink_toggle, - self._sunnylink_sponsor_button, - self._sunnylink_pair_button, - self._backup_btn, - self._restore_btn, - self._sunnylink_uploader_toggle - ]) - - def _update_state(self): - super()._update_state() - self._sunnylink_enabled = ui_state.params.get("SunnylinkEnabled") - self._sunnylink_toggle.set_checked(self._sunnylink_enabled) - self._sunnylink_pair_button.set_visible(self._sunnylink_enabled) - self._sunnylink_sponsor_button.set_visible(self._sunnylink_enabled) - self._backup_btn.set_visible(self._sunnylink_enabled) - self._restore_btn.set_visible(self._sunnylink_enabled) - self._sunnylink_uploader_toggle.set_visible(self._sunnylink_enabled) - self.handle_backup_restore_progress() - - self._sunnylink_info.device_id_text.set_text(ui_state.params.get("SunnylinkDongleId") or UNREGISTERED_SUNNYLINK_DONGLE_ID) - self._sunnylink_info.sponsor_text.set_text(ui_state.sunnylink_state.get_sponsor_tier().name.lower() or "N/A") - self._sunnylink_info.set_visible(self._sunnylink_enabled) - - if ui_state.sunnylink_state.is_sponsor(): - self._sunnylink_sponsor_button.set_text(tr("thanks")) - self._sunnylink_sponsor_button.set_value(ui_state.sunnylink_state.get_sponsor_tier().name.lower()) - self._sunnylink_sponsor_button.set_enabled(False) - else: - self._sunnylink_sponsor_button.set_text(tr("sponsor")) - self._sunnylink_sponsor_button.set_value("") - - if ui_state.sunnylink_state.is_paired(): - self._sunnylink_pair_button.set_text(tr("paired")) - else: - self._sunnylink_pair_button.set_text(tr("pair")) - - def show_event(self): - super().show_event() - ui_state.update_params() - ui_state.sunnylink_state.set_settings_open(True) - - def hide_event(self): - super().hide_event() - ui_state.sunnylink_state.set_settings_open(False) - - @staticmethod - def _sunnylink_toggle_callback(state: bool): - sl_consent: bool = ui_state.params.get("CompletedSunnylinkConsentVersion") == sunnylink_consent_version - sl_enabled: bool = ui_state.params.get("SunnylinkEnabled") - - def sl_terms_accepted(): - ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version) - ui_state.params.put_bool("SunnylinkEnabled", True) - gui_app.pop_widget() - - def sl_terms_declined(): - ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_declined) - ui_state.params.put_bool("SunnylinkEnabled", False) - gui_app.pop_widget() - - if state and not sl_consent and not sl_enabled: - sl_terms_dlg = SunnylinkConsentPage(on_accept=sl_terms_accepted, on_decline=sl_terms_declined) - gui_app.push_widget(sl_terms_dlg) - else: - ui_state.params.put_bool("SunnylinkEnabled", state) - - ui_state.update_params() - - @staticmethod - def _sunnylink_uploader_callback(state: bool): - ui_state.params.put_bool("EnableSunnylinkUploader", state) - - def _handle_backup_restore_btn(self, restore: bool = False): - lbl = tr("slide to restore") if restore else tr("slide to backup") - icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 64) - dlg = BigConfirmationDialog(lbl, icon, confirm_callback=self._restore_handler if restore else self._backup_handler) - gui_app.push_widget(dlg) - - def _backup_handler(self): - self._backup_in_progress = True - self._backup_btn.set_enabled(False) - ui_state.params.put_bool("BackupManager_CreateBackup", True) - - def _restore_handler(self): - self._restore_in_progress = True - self._restore_btn.set_enabled(False) - ui_state.params.put("BackupManager_RestoreVersion", "latest") - - def handle_backup_restore_progress(self): - sunnylink_backup_manager = ui_state.sm["backupManagerSP"] - - backup_status = sunnylink_backup_manager.backupStatus - restore_status = sunnylink_backup_manager.restoreStatus - backup_progress = sunnylink_backup_manager.backupProgress - restore_progress = sunnylink_backup_manager.restoreProgress - - if self._backup_in_progress: - self._restore_btn.set_enabled(False) - self._backup_btn.set_enabled(False) - - if backup_status == custom.BackupManagerSP.Status.inProgress: - self._backup_in_progress = True - self._backup_btn.set_text(tr("backing up")) - text = tr(f"{backup_progress}%") - self._backup_btn.set_value(text) - - elif backup_status == custom.BackupManagerSP.Status.failed: - self._backup_in_progress = False - self._backup_btn.set_enabled(not ui_state.is_onroad()) - self._backup_btn.set_text(tr("backup")) - self._backup_btn.set_value(tr("failed")) - - elif (backup_status == custom.BackupManagerSP.Status.completed or - (backup_status == custom.BackupManagerSP.Status.idle and backup_progress == 100.0)): - self._backup_in_progress = False - gui_app.push_widget(BigDialog(title=tr("settings backed up"), description="")) - self._backup_btn.set_enabled(not ui_state.is_onroad()) - - elif self._restore_in_progress: - self._restore_btn.set_enabled(False) - self._backup_btn.set_enabled(False) - - if restore_status == custom.BackupManagerSP.Status.inProgress: - self._restore_in_progress = True - self._restore_btn.set_text(tr("restoring")) - text = tr(f"{restore_progress}%") - self._restore_btn.set_value(text) - - elif restore_status == custom.BackupManagerSP.Status.failed: - self._restore_in_progress = False - self._restore_btn.set_enabled(not ui_state.is_onroad()) - self._restore_btn.set_text(tr("restore")) - self._restore_btn.set_value(tr("failed")) - gui_app.push_widget(BigDialog(title=tr("unable to restore"), description="try again later.")) - - elif (restore_status == custom.BackupManagerSP.Status.completed or - (restore_status == custom.BackupManagerSP.Status.idle and restore_progress == 100.0)): - self._restore_in_progress = False - gui_app.push_widget(BigConfirmationDialog( - title="slide to restart", icon=gui_app.texture("icons_mici/settings/device/reboot.png", 64, 64), - confirm_callback=lambda: gui_app.request_close())) - - else: - can_enable = self._sunnylink_enabled and not ui_state.is_onroad() - self._backup_btn.set_enabled(can_enable) - self._backup_btn.set_text(tr("backup settings")) - self._backup_btn.set_value("") - self._restore_btn.set_enabled(can_enable) - self._restore_btn.set_text(tr("restore settings")) - self._restore_btn.set_value("") - - -class SunnylinkPairBigButton(BigButton): - def __init__(self, sponsor_pairing: bool = False): - self.sponsor_pairing = sponsor_pairing - super().__init__("") - - def _update_state(self): - super()._update_state() - - def _handle_mouse_release(self, mouse_pos: MousePos): - super()._handle_mouse_release(mouse_pos) - - network_type = ui_state.sm["deviceState"].networkType - - dlg: BigDialog | SunnylinkPairingDialog | None = None - - if network_type == 0: - dlg = BigDialog(tr("no internet"), tr("please connect to WiFi & try again")) - elif UNREGISTERED_SUNNYLINK_DONGLE_ID == (ui_state.params.get("SunnylinkDongleId") or UNREGISTERED_SUNNYLINK_DONGLE_ID): - dlg = BigDialog(tr("sunnylink dongle id not found"), tr("please reboot & try again")) - elif self.sponsor_pairing: - dlg = SunnylinkPairingDialog(sponsor_pairing=True) - elif not self.sponsor_pairing: - dlg = SunnylinkPairingDialog(sponsor_pairing=False) - if dlg: - gui_app.push_widget(dlg) diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/onroad/__init__.py b/openpilot/selfdrive/ui/sunnypilot/mici/onroad/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/onroad/confidence_ball.py b/openpilot/selfdrive/ui/sunnypilot/mici/onroad/confidence_ball.py deleted file mode 100644 index 4a1aa9224..000000000 --- a/openpilot/selfdrive/ui/sunnypilot/mici/onroad/confidence_ball.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - -This file is part of sunnypilot and is licensed under the MIT License. -See the LICENSE.md file in the root directory for more details. -""" -from openpilot.selfdrive.ui.onroad.augmented_road_view import BORDER_COLORS -from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus - - -class ConfidenceBallSP: - @staticmethod - def get_animate_status_probs(): - if ui_state.status == UIStatus.LAT_ONLY: - return ui_state.sm['modelV2'].meta.disengagePredictions.steerOverrideProbs - - # UIStatus.LONG_ONLY - return ui_state.sm['modelV2'].meta.disengagePredictions.brakeDisengageProbs - - @staticmethod - def get_lat_long_dot_color(): - if ui_state.status == UIStatus.LAT_ONLY: - return BORDER_COLORS[UIStatus.LAT_ONLY] - - # UIStatus.LONG_ONLY - return BORDER_COLORS[UIStatus.LONG_ONLY] diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/onroad/hud_renderer.py b/openpilot/selfdrive/ui/sunnypilot/mici/onroad/hud_renderer.py deleted file mode 100644 index 83b619cc9..000000000 --- a/openpilot/selfdrive/ui/sunnypilot/mici/onroad/hud_renderer.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - -This file is part of sunnypilot and is licensed under the MIT License. -See the LICENSE.md file in the root directory for more details. -""" -import pyray as rl - -from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer -from openpilot.selfdrive.ui.sunnypilot.onroad.blind_spot_indicators import BlindSpotIndicators -from openpilot.selfdrive.ui.ui_state import ui_state - - -class HudRendererSP(HudRenderer): - def __init__(self): - super().__init__() - self.blind_spot_indicators = BlindSpotIndicators() - self._sp_engaged: bool = False - - def _update_state(self) -> None: - super()._update_state() - self.blind_spot_indicators.update() - - sp_engaged = ui_state.engaged - if sp_engaged != self._sp_engaged: - self._chestnut_fade_time = rl.get_time() if sp_engaged else 0 - self._sp_engaged = sp_engaged - - def _render(self, rect: rl.Rectangle) -> None: - super()._render(rect) - self.blind_spot_indicators.render(rect) - - def _has_blind_spot_detected(self) -> bool: - - return self.blind_spot_indicators.detected diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/onroad/model_renderer.py b/openpilot/selfdrive/ui/sunnypilot/mici/onroad/model_renderer.py deleted file mode 100644 index e3de9401c..000000000 --- a/openpilot/selfdrive/ui/sunnypilot/mici/onroad/model_renderer.py +++ /dev/null @@ -1,19 +0,0 @@ -""" -Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - -This file is part of sunnypilot and is licensed under the MIT License. -See the LICENSE.md file in the root directory for more details. -""" -import pyray as rl -from openpilot.selfdrive.ui.ui_state import UIStatus -from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath - -LANE_LINE_COLORS_SP = { - UIStatus.LAT_ONLY: rl.Color(0, 255, 64, 255), - UIStatus.LONG_ONLY: rl.Color(0, 255, 64, 255), -} - - -class ModelRendererSP: - def __init__(self): - self.rainbow_path = RainbowPath() diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/widgets/__init__.py b/openpilot/selfdrive/ui/sunnypilot/mici/widgets/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/openpilot/selfdrive/ui/sunnypilot/mici/widgets/sunnylink_pairing_dialog.py b/openpilot/selfdrive/ui/sunnypilot/mici/widgets/sunnylink_pairing_dialog.py deleted file mode 100644 index c727e4fc1..000000000 --- a/openpilot/selfdrive/ui/sunnypilot/mici/widgets/sunnylink_pairing_dialog.py +++ /dev/null @@ -1,57 +0,0 @@ -""" -Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - -This file is part of sunnypilot and is licensed under the MIT License. -See the LICENSE.md file in the root directory for more details. -""" -import base64 - -import pyray as rl -from openpilot.common.swaglog import cloudlog -from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog -from openpilot.sunnypilot.sunnylink.api import SunnylinkApi, UNREGISTERED_SUNNYLINK_DONGLE_ID, API_HOST -from openpilot.system.ui.lib.application import FontWeight, gui_app -from openpilot.system.ui.lib.multilang import tr -from openpilot.system.ui.widgets.nav_widget import NavWidget -from openpilot.system.ui.widgets.label import UnifiedLabel - - -class SunnylinkPairingDialog(PairingDialog): - """Dialog for device pairing with QR code.""" - - def __init__(self, sponsor_pairing: bool = False): - PairingDialog.__init__(self) - self._sponsor_pairing = sponsor_pairing - label_text = tr("pair with sunnylink") if sponsor_pairing else tr("become a sunnypilot sponsor") - self._pair_label = UnifiedLabel(label_text, font_size=48, font_weight=FontWeight.BOLD, - text_color=rl.Color(255, 255, 255, int(255 * 0.9)), line_height=0.8) - - def _get_pairing_url(self) -> str: - qr_string = "https://github.com/sponsors/sunnyhaibin" - - if self._sponsor_pairing: - try: - sl_dongle_id = self._params.get("SunnylinkDongleId") or UNREGISTERED_SUNNYLINK_DONGLE_ID - token = SunnylinkApi(sl_dongle_id).get_token() - inner_string = f"1|{sl_dongle_id}|{token}" - payload_bytes = base64.b64encode(inner_string.encode('utf-8')).decode('utf-8') - qr_string = f"{API_HOST}/sso?state={payload_bytes}" - except Exception: - cloudlog.exception("Failed to get pairing token") - - return qr_string - - def _update_state(self): - NavWidget._update_state(self) - - -if __name__ == "__main__": - gui_app.init_window("pairing device") - pairing = SunnylinkPairingDialog(sponsor_pairing=True) - try: - for _ in gui_app.render(): - result = pairing.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) - if result != -1: - break - finally: - del pairing diff --git a/openpilot/selfdrive/ui/sunnypilot/onroad/hud_renderer.py b/openpilot/selfdrive/ui/sunnypilot/onroad/hud_renderer.py index 6e49c996d..0e2d96184 100644 --- a/openpilot/selfdrive/ui/sunnypilot/onroad/hud_renderer.py +++ b/openpilot/selfdrive/ui/sunnypilot/onroad/hud_renderer.py @@ -7,7 +7,7 @@ See the LICENSE.md file in the root directory for more details. import pyray as rl from openpilot.common.constants import CV -from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar +from openpilot.selfdrive.ui.onroad.torque_bar import TorqueBar from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRenderer, DeveloperUiState, get_bottom_dev_ui_offset from openpilot.selfdrive.ui.sunnypilot.onroad.road_name import RoadNameRenderer from openpilot.selfdrive.ui.sunnypilot.onroad.rocket_fuel import RocketFuel diff --git a/openpilot/selfdrive/ui/sunnypilot/onroad/turn_signal.py b/openpilot/selfdrive/ui/sunnypilot/onroad/turn_signal.py index fc6f7eb91..6c2c0ecdd 100644 --- a/openpilot/selfdrive/ui/sunnypilot/onroad/turn_signal.py +++ b/openpilot/selfdrive/ui/sunnypilot/onroad/turn_signal.py @@ -7,14 +7,22 @@ See the LICENSE.md file in the root directory for more details. import pyray as rl import time from dataclasses import dataclass +from enum import StrEnum from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.selfdrive.ui.mici.onroad.alert_renderer import IconSide, TURN_SIGNAL_BLINK_PERIOD from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import Widget from openpilot.common.filter_simple import FirstOrderFilter +TURN_SIGNAL_BLINK_PERIOD = 1 / (80 / 60) # Mazda heartbeat turn signal BPM + + +class IconSide(StrEnum): + left = 'left' + right = 'right' + + @dataclass(frozen=True) class TurnSignalConfig: left_x: int = 80 diff --git a/openpilot/selfdrive/ui/tests/diff/replay.py b/openpilot/selfdrive/ui/tests/diff/replay.py index e37807685..e18a710bf 100755 --- a/openpilot/selfdrive/ui/tests/diff/replay.py +++ b/openpilot/selfdrive/ui/tests/diff/replay.py @@ -16,7 +16,7 @@ from openpilot.selfdrive.ui.tests.diff.diff import DIFF_OUT_DIR from openpilot.system.updated.updated import parse_release_notes from openpilot.common.version import terms_version, training_version, terms_version_sp, sunnylink_consent_version -LayoutVariant = Literal["mici", "tizi"] +LayoutVariant = Literal["tizi"] FPS = 60 HEADLESS = os.getenv("WINDOWED", "0") != "1" @@ -56,11 +56,7 @@ def run_replay(variant: LayoutVariant) -> None: gui_app.init_window("ui diff test", fps=FPS) - # Dynamically import main layout based on variant - if variant == "mici": - from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout as MainLayout - else: - from openpilot.selfdrive.ui.layouts.main import MainLayout + from openpilot.selfdrive.ui.layouts.main import MainLayout main_layout = MainLayout() # Disable interactive timeout — replay clicks use left_down=False so they never reset the timer, @@ -115,27 +111,23 @@ def run_replay(variant: LayoutVariant) -> None: def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--big', action='store_true', help='Use big UI layout (tizi/tici) instead of mici layout') - args = parser.parse_args() + argparse.ArgumentParser().parse_args() - variant: LayoutVariant = 'tizi' if args.big else 'mici' - - if args.big: - os.environ["BIG"] = "1" + variant: LayoutVariant = 'tizi' + os.environ["BIG"] = "1" os.environ["RECORD"] = "1" os.environ["RECORD_QUALITY"] = "0" # Use CRF 0 ("lossless" encode) for deterministic output across different machines os.environ["RECORD_OUTPUT"] = os.path.join(DIFF_OUT_DIR, os.environ.get("RECORD_OUTPUT", f"{variant}_ui_replay.mp4")) print(f"Running {variant} UI replay...") with OpenpilotPrefix(): - sources = ["openpilot.system.ui"] - if variant == "mici": - sources.append("openpilot.selfdrive.ui.mici") - omit = ["**/*tizi*", "**/*tici*"] # exclude files containing "tizi" or "tici" - else: - sources.extend(["openpilot.selfdrive.ui.layouts", "openpilot.selfdrive.ui.onroad", "openpilot.selfdrive.ui.widgets"]) - omit = ["**/*mici*"] # exclude files containing "mici" + sources = [ + "openpilot.system.ui", + "openpilot.selfdrive.ui.layouts", + "openpilot.selfdrive.ui.onroad", + "openpilot.selfdrive.ui.widgets", + ] + omit = ["**/*mici*"] cov = coverage.Coverage(source=sources, omit=omit) with cov.collect(): run_replay(variant) diff --git a/openpilot/selfdrive/ui/tests/diff/replay_script.py b/openpilot/selfdrive/ui/tests/diff/replay_script.py index fb654c5e8..b9b7cbbe1 100644 --- a/openpilot/selfdrive/ui/tests/diff/replay_script.py +++ b/openpilot/selfdrive/ui/tests/diff/replay_script.py @@ -218,173 +218,6 @@ def test_onroad_alerts(script: Script, pm: PubMaster) -> None: # --- Script builders --- -def build_mici_script(pm: PubMaster, main_layout, script: Script) -> None: - """Build the replay script for the mici layout.""" - from openpilot.system.ui.lib.application import gui_app - - width, height = gui_app.width, gui_app.height - center = (width // 2, height // 2) - right = (width * 4 // 5, height // 2) - left = (width // 5, height // 2) - top = (width // 2, height // 10) - bottom = (width // 2, height * 9 // 10) - - DURATION = 5 - SWIPE_WAIT = FPS * 3 // 4 - - def click(times: int = 1, wait_after: int = WAIT_SHORT) -> None: - """Click at the center of the screen the given number of times with optional delay after.""" - for _ in range(times): - script.click(*center, wait_after=wait_after) - - def press(x: int, y: int, duration_frames: int = DURATION, wait_after: int = WAIT_SHORT) -> None: - """Perform a drag with no movement to simulate a left_down mouse event at the given position for the specified duration and delay after.""" - script.drag(x, y, (0, 0), 0, duration_frames, wait_after=wait_after) - - def swipe_left(distance: int = right[0] - left[0], duration_frames: int = DURATION, wait_after: int = SWIPE_WAIT) -> None: - """Drag from right edge to left (scroll right / slide confirmation).""" - script.drag(*right, DIR_LEFT, distance, duration_frames, wait_after) - - def swipe_right(distance: int = right[0] - left[0], duration_frames: int = DURATION, wait_after: int = SWIPE_WAIT) -> None: - """Drag from left edge to right (scroll left).""" - script.drag(*left, DIR_RIGHT, distance, duration_frames, wait_after) - - def swipe_down(distance: int = bottom[1] - top[1], duration_frames: int = DURATION, wait_after: int = SWIPE_WAIT) -> None: - """Drag from top edge to bottom (scroll up / go back).""" - script.drag(*top, DIR_DOWN, distance, duration_frames, wait_after) - - def swipe_up(distance: int = bottom[1] - top[1], duration_frames: int = DURATION, wait_after: int = SWIPE_WAIT) -> None: - """Drag from bottom edge to top (scroll down).""" - script.drag(*bottom, DIR_UP, distance, duration_frames, wait_after) - - ActionFn = Callable[[], None] | None - Cases = list[ActionFn] - - def run_actions(*actions: ActionFn, after_each: ActionFn = None) -> None: - """Helper function to run a sequence of actions in order for interaction tests, calling after_each callback after each action if provided.""" - for action in actions: - if action is not None: - action() - if after_each is not None: - after_each() - - def explore_setting(*actions: ActionFn) -> None: - """Helper function to open a settings item, run the given actions, and go back.""" - run_actions(click, *actions, swipe_down) # open, interact, go back - - def scroll_through_cases(cases: Cases) -> None: - """Helper function to explore a panel by calling the interaction callbacks for each item/page before swiping to the next one.""" - run_actions(*cases, after_each=lambda: swipe_left(210, 10)) # swipe to roughly the center of the next toggle after each case - - def interact_keyboard() -> None: - """Interact with the keyboard in various ways to test different actions and states. - Assumes it's a password keyboard with 8 characters required. Closes by pressing confirm at the end.""" - KEY = (250, 160) # key in the middle of the keyboard ('G') - SHIFT = (50, 210) - NUMBERS = (480, 210) - SPACE = (500, 160) - BACKSPACE = (490, 30) - CONFIRM = (50, 30) - # Begin interactions - press(*CONFIRM, wait_after=FAST_CLICK) # confirm while disabled should do nothing - swipe_left(duration_frames=FPS // 2) # swipe to type - swipe_up(duration_frames=FPS // 2) # swipe out of keyboard (nothing typed) - # press various keys to test different states: - for key in [ - SHIFT, KEY, KEY, SHIFT, SHIFT, KEY, KEY, # test casing (upper, lower, caps lock) - SPACE, SPACE, BACKSPACE, BACKSPACE, # test multiple space and backspace - NUMBERS, KEY, center, SHIFT, KEY # test numbers and symbols - ]: - press(*key, wait_after=FAST_CLICK) - # press confirm to close - script.wait(WAIT_SHORT) # wait for confirm to enable - press(*CONFIRM) - - toggle_cases: Cases = [ - lambda: click(times=3, wait_after=FAST_CLICK), # first toggle is personality, which has 3 states - None, None, None, None, None, None, # skip other toggles to save time - lambda: click(times=2, wait_after=FAST_CLICK), # test final toggle (enable openpilot) - ] - - network_cases: Cases = [ - explore_setting, # select wifi (just open and close) - None, None, - lambda: run_actions(click, interact_keyboard), # tether password keyboard - ] - - device_cases: Cases = [ - None, - click, # update - explore_setting, # pairing (just open and close) - lambda: explore_setting( - # training guide - lambda: swipe_left(width * 2), click, # first page, click next - lambda: swipe_left(width * 2), swipe_down # second page, go back (TODO: make driver cam preview work) - ), - None, # TODO: preview cabin camera; enabling this causes MultiplePublishersError later in onroad alert tests - lambda: explore_setting(swipe_left), # terms & conditions (swipe to view QR code) - lambda: explore_setting(lambda: swipe_up(height * 3), lambda: swipe_down(height * 3)), # regulatory info - lambda: run_actions(click, lambda: swipe_left(width)), # reset calibration confirm (goes back automatically) - lambda: explore_setting(lambda: swipe_left(width)), # uninstall - lambda: run_actions( - lambda: explore_setting(lambda: swipe_left(width)), # reboot - lambda: script.click(430, 120), lambda: swipe_left(width), swipe_down, # shutdown - ), - ] - - developer_cases: Cases = [ - lambda: click(times=2, wait_after=FAST_CLICK), # toggle ssh mode - explore_setting, # SSH keys keyboard (just open and close) - None, # joystick mode - lambda: click(wait_after=FAST_CLICK), # longitudinal maneuver mode (disabled; should do nothing) - lambda: click(times=2, wait_after=FAST_CLICK), # toggle UI debug mode - ] - - settings_cases: Cases = [ - lambda: scroll_through_cases(toggle_cases), - None, # sunnylink (just open and close) - None, # models (just open and close) - lambda: scroll_through_cases(network_cases), - lambda: scroll_through_cases(device_cases), - lambda: script.wait(WAIT_SHORT), # software - lambda: script.wait(WAIT_SHORT), # pairing - lambda: run_actions(lambda: swipe_up(height * 3), lambda: swipe_down(height * 3)), # firehose (scroll down and back up) - lambda: scroll_through_cases(developer_cases), - ] - - # === Homescreen === # - script.wait(WAIT_SHORT) - swipe_left(width, wait_after=WAIT_SHORT) # onroad screen - swipe_right(width, wait_after=WAIT_SHORT) # back to home - - # === Offroad Alerts === - def setup_offroad_alerts_and_refresh() -> None: - """Setup function to trigger offroad alerts and force a refresh on the alerts layout.""" - setup_offroad_alerts() - params = Params() - main_layout._alerts_layout._pending_params = ({"UpdaterNewDescription": params.get("UpdaterNewDescription")} | - {alert_data.key: params.get(alert_data.key) for alert_data in main_layout._alerts_layout.sorted_alerts}) - main_layout._alerts_layout._update_state() - - swipe_right(width, wait_after=WAIT_SHORT) # open alerts - script.setup(setup_offroad_alerts_and_refresh) # show alerts - swipe_up(height) # scroll alerts - swipe_left(width, wait_after=WAIT_SHORT) # close alerts - - # === Settings === # - click() # open settings - scroll_through_cases([lambda case=case: explore_setting(case) for case in settings_cases]) # explore settings - swipe_down() # back to home - - # === Onroad === - script.set_send(lambda: send_onroad(pm)) - swipe_left(width, wait_after=WAIT_SHORT) # onroad screen - test_onroad_alerts(script, pm) - swipe_right(width) # back to home - - script.end() - - def build_tizi_script(pm: PubMaster, main_layout, script: Script) -> None: """Build the replay script for the tizi layout.""" @@ -528,8 +361,7 @@ def build_script(pm: PubMaster, main_layout, variant: LayoutVariant) -> list[Scr print(f"Building {variant} replay script...") script = Script(FPS) - builder = build_tizi_script if variant == 'tizi' else build_mici_script - builder(pm, main_layout, script) + build_tizi_script(pm, main_layout, script) print(f"Built replay script with {len(script.entries)} events and {script.frame} frames ({script.get_frame_time():.2f} seconds)") diff --git a/openpilot/selfdrive/ui/tests/profile_onroad.py b/openpilot/selfdrive/ui/tests/profile_onroad.py index 98816f9dd..f5c60642a 100755 --- a/openpilot/selfdrive/ui/tests/profile_onroad.py +++ b/openpilot/selfdrive/ui/tests/profile_onroad.py @@ -8,7 +8,7 @@ import numpy as np from openpilot.cereal.visionipc import VisionStreamType from msgq.visionipc import VisionIpcServer from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout +from openpilot.selfdrive.ui.layouts.main import MainLayout from openpilot.system.ui.lib.application import gui_app from openpilot.tools.lib.logreader import LogReader @@ -83,7 +83,7 @@ if __name__ == "__main__": os.environ['SDL_VIDEODRIVER'] = 'dummy' gui_app.init_window("UI Profiling", fps=600) - main_layout = MiciMainLayout() + main_layout = MainLayout() print("Running...") patch_submaster(message_chunks) diff --git a/openpilot/selfdrive/ui/translations/update_translations.py b/openpilot/selfdrive/ui/translations/update_translations.py index 0b5ecc26c..103f16f89 100755 --- a/openpilot/selfdrive/ui/translations/update_translations.py +++ b/openpilot/selfdrive/ui/translations/update_translations.py @@ -16,7 +16,6 @@ def update_translations(): os.walk(os.path.join(str(UI_DIR), "layouts")), os.walk(os.path.join(str(UI_DIR), "onroad")), os.walk(os.path.join(str(UI_DIR), "sunnypilot")), - os.walk(os.path.join(str(UI_DIR), "mici")), os.walk(os.path.join(str(UI_DIR), "..", "selfdrived"))): for filename in filenames: if filename.endswith(".py"): diff --git a/openpilot/selfdrive/ui/ui.py b/openpilot/selfdrive/ui/ui.py index e8863f9ab..de5caef4c 100755 --- a/openpilot/selfdrive/ui/ui.py +++ b/openpilot/selfdrive/ui/ui.py @@ -9,20 +9,13 @@ from openpilot.system.ui.lib.application import gui_app from openpilot.selfdrive.ui.layouts.main import MainLayout from openpilot.selfdrive.ui.ui_state import ui_state -BIG_UI = gui_app.big_ui() - - def main(): cores = {5, } # above plannerd and radard config_realtime_process(0, Priority.CTRL_HIGH) gui_app.init_window("UI") - if BIG_UI: - MainLayout() - else: - from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout - MiciMainLayout() + MainLayout() pm = messaging.PubMaster(['uiDebug']) for should_render, frame_time, cpu_time in gui_app.render(): diff --git a/openpilot/tools/clip/run.py b/openpilot/tools/clip/run.py index 81145e225..5aeab2966 100755 --- a/openpilot/tools/clip/run.py +++ b/openpilot/tools/clip/run.py @@ -287,10 +287,7 @@ def clip(route: Route, output: str, start: int, end: int, headless: bool = True, timer, duration = Timer(), end - start import pyray as rl - if big: - from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView - else: - from openpilot.selfdrive.ui.mici.onroad.augmented_road_view import AugmentedRoadView + from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app, FontWeight timer.lap("import") diff --git a/openpilot/tools/replay/ui.py b/openpilot/tools/replay/ui.py index 7f34f1303..57d48adf5 100755 --- a/openpilot/tools/replay/ui.py +++ b/openpilot/tools/replay/ui.py @@ -22,7 +22,7 @@ from openpilot.tools.replay.lib.ui_helpers import ( plot_model, ) from openpilot.cereal.visionipc import VisionStreamType -from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView +from openpilot.selfdrive.ui.onroad.cameraview import CameraView os.environ['BASEDIR'] = BASEDIR