From 814af739d0c3b580b727fb0eaeebcc10f451276a Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Tue, 15 Sep 2026 12:53:17 -0500 Subject: [PATCH] Keep screen settings in Galaxy Leave the existing UI-state consumer in place so Galaxy values still control the display, but remove the native settings pages and tests. Hide the new brightness and wake-choice controls behind Galaxy Developer Mode while preserving current defaults. Co-authored-by: AngusBell97 <124716116+AngusBell97@users.noreply.github.com> --- .../settings/starpilot/screen_controls.py | 157 -------- .../settings/starpilot/system_settings.py | 108 ++---- selfdrive/ui/lib/screen_settings.py | 14 - selfdrive/ui/mici/layouts/settings/device.py | 4 - selfdrive/ui/mici/layouts/settings/screen.py | 344 ------------------ .../ui/tests/test_native_screen_controls.py | 209 ----------- .../tests/test_native_screen_save_errors.py | 302 --------------- .../common/assets/device_settings_layout.json | 18 +- starpilot/common/screen_settings.py | 7 +- .../common/tests/test_screen_settings.py | 12 +- starpilot/docs/screen_settings.md | 23 +- .../tests/test_device_settings_layout.py | 19 + .../tests/test_screen_settings_dom.cjs | 6 +- 13 files changed, 85 insertions(+), 1138 deletions(-) delete mode 100644 selfdrive/ui/layouts/settings/starpilot/screen_controls.py delete mode 100644 selfdrive/ui/lib/screen_settings.py delete mode 100644 selfdrive/ui/mici/layouts/settings/screen.py delete mode 100644 selfdrive/ui/tests/test_native_screen_controls.py delete mode 100644 selfdrive/ui/tests/test_native_screen_save_errors.py diff --git a/selfdrive/ui/layouts/settings/starpilot/screen_controls.py b/selfdrive/ui/layouts/settings/starpilot/screen_controls.py deleted file mode 100644 index e536cde135..0000000000 --- a/selfdrive/ui/layouts/settings/starpilot/screen_controls.py +++ /dev/null @@ -1,157 +0,0 @@ -from __future__ import annotations - -import pyray as rl - -from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( - AetherAdjustorRow, - AetherInlineRangeControl, - AetherSegmentedControl, - DEFAULT_PANEL_STYLE, - RowToggleTile, - draw_rounded_fill, - draw_rounded_stroke, - draw_text_fit_common, - wrap_text, -) -from openpilot.starpilot.common.screen_settings import brightness_preferences, set_brightness_mode, write_screen_setting -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 gui_label -from openpilot.system.ui.widgets.confirm_dialog import alert_dialog -from openpilot.selfdrive.ui.lib.screen_settings import try_screen_setting - - -def show_screen_save_error(): - gui_app.push_widget(alert_dialog(tr("Unable to save screen settings. Please try again."))) - - -class BrightnessAdjustorRow(AetherAdjustorRow): - def __init__(self, *args, params, brightness_key: str, **kwargs): - self._params = params - self._brightness_key = brightness_key - super().__init__(*args, **kwargs) - - def formatted_value(self) -> str: - prefs = brightness_preferences(self._params, self._brightness_key) - if prefs["mode"] == "auto": - return tr("Auto") + f" {prefs['offset']:+d}%" - return tr("Manual") + f" {prefs['manual']}%" - - -class WakeToggleRow(RowToggleTile): - """Show the wake description; the standard compact toggle row only shows its state.""" - - def _render(self, rect: rl.Rectangle): - active = self.get_state() - self._animate_plate(rl.get_frame_time()) - face, accent = self._render_hud_background(rect, self._active_color, self._glow) - x, width = face.x + 24, face.width - 48 - draw_text_fit_common(self._font, self.title, rl.Vector2(x, face.y + 14), width - 150, 32, color=rl.WHITE) - state = tr("Enabled") if active else tr("Disabled") - gui_label( - rl.Rectangle(x + width - 135, face.y + 12, 135, 38), - state, - 24, - accent if active else rl.Color(170, 175, 185, 255), - FontWeight.MEDIUM, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, - ) - lines = wrap_text(self._font_desc, self.desc, width, 22, max_lines=3) - for index, line in enumerate(lines): - draw_text_fit_common(self._font_desc, line, rl.Vector2(x, face.y + 54 + index * 24), width, 22, color=rl.Color(190, 195, 205, 255)) - - -class AetherBrightnessDialog(Widget): - """Separate persisted mode and value controls; all brightness output stays in Device.""" - - def __init__(self, params, key: str, title: str): - super().__init__() - self._params, self._key, self._title = params, key, title - self._done_rect = rl.Rectangle(0, 0, 0, 0) - self._done_pressed = False - self._mode_control = self._child(AetherSegmentedControl([tr("Auto"), tr("Manual")], self._mode_index, self._set_mode, style=DEFAULT_PANEL_STYLE)) - prefs = brightness_preferences(params, key) - self._offset = self._child( - AetherInlineRangeControl( - -30, - 30, - 1, - prefs["offset"], - lambda value: None, - on_commit=lambda value: self._save_value(self._key + "Offset", int(value)), - unit="%", - labels={value: f"{value:+d}%" for value in range(-30, 31)}, - ) - ) - self._manual = self._child( - AetherInlineRangeControl(0, 100, 1, prefs["manual"], lambda value: None, on_commit=lambda value: self._save_value(self._key, int(value)), unit="%") - ) - self._offset.set_visible(lambda: self._mode_index() == 0) - self._manual.set_visible(lambda: self._mode_index() == 1) - - def _mode_index(self) -> int: - return 0 if brightness_preferences(self._params, self._key)["mode"] == "auto" else 1 - - def _set_mode(self, index: int): - saved = try_screen_setting(lambda: set_brightness_mode(self._params, self._key, "auto" if index == 0 else "manual"), show_screen_save_error) - if not saved: - self._refresh_controls() - return saved - - def _save_value(self, key: str, value: int): - saved = try_screen_setting(lambda: write_screen_setting(self._params, key, value), show_screen_save_error) - if not saved: - self._refresh_controls() - return saved - - def _refresh_controls(self): - prefs = brightness_preferences(self._params, self._key) - for control, value in ((self._offset, prefs["offset"]), (self._manual, prefs["manual"])): - control.reset_interaction() - control.set_value(value) - - def _handle_mouse_press(self, mouse_pos): - self._done_pressed = rl.check_collision_point_rec(mouse_pos, self._done_rect) - - def _handle_mouse_release(self, mouse_pos): - if self._done_pressed and rl.check_collision_point_rec(mouse_pos, self._done_rect): - gui_app.pop_widget() - self._done_pressed = False - - def _render(self, rect: rl.Rectangle): - rl.draw_rectangle(0, 0, gui_app.width, gui_app.height, rl.Color(0, 0, 0, 180)) - card = rl.Rectangle(rect.x + 60, rect.y + 60, rect.width - 120, rect.height - 120) - draw_rounded_fill(card, rl.Color(10, 12, 16, 255), radius_px=35) - draw_rounded_stroke(card, rl.Color(255, 255, 255, 35), radius_px=35) - inset, width = card.x + 80, card.width - 160 - gui_label(rl.Rectangle(inset, card.y + 50, width - 250, 80), self._title, 60, rl.WHITE, FontWeight.BOLD) - self._done_rect = rl.Rectangle(card.x + card.width - 290, card.y + 35, 230, 110) - draw_rounded_fill(self._done_rect, DEFAULT_PANEL_STYLE.accent, radius_px=20) - gui_label(self._done_rect, tr("Done"), 46, rl.WHITE, FontWeight.BOLD, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) - self._mode_control.render(rl.Rectangle(inset, card.y + 175, width, 125)) - - prefs = brightness_preferences(self._params, self._key) - auto = prefs["mode"] == "auto" - control = self._offset if auto else self._manual - if not control.is_interacting: - control.set_value(prefs["offset"] if auto else prefs["manual"]) - value = int(control.current_val) - title = tr("Auto Brightness Offset") if auto else tr("Manual Brightness") - description = ( - tr("Adjust automatic brightness from -30% to +30%. 0% keeps the normal automatic level.") if auto else tr("Set a fixed brightness from 0 to 100%.") - ) - gui_label(rl.Rectangle(inset, card.y + 355, width, 70), title, 46, rl.WHITE, FontWeight.MEDIUM) - gui_label(rl.Rectangle(inset, card.y + 435, width, 75), f"{value:+d}%" if auto else f"{value}%", 66, DEFAULT_PANEL_STYLE.accent, FontWeight.BOLD) - control.render(rl.Rectangle(inset, card.y + card.height - 245, width, 100)) - endpoint_y = card.y + card.height - 153 - gui_label(rl.Rectangle(inset + 84, endpoint_y, 300, 30), "-30%" if auto else "0%", 26, rl.Color(160, 165, 175, 255), FontWeight.NORMAL) - gui_label( - rl.Rectangle(inset + width - 384, endpoint_y, 300, 30), - "+30%" if auto else "100%", - 26, - rl.Color(160, 165, 175, 255), - FontWeight.NORMAL, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, - ) - gui_label(rl.Rectangle(inset, card.y + card.height - 115, width, 70), description, 28, rl.Color(180, 185, 195, 255), FontWeight.NORMAL) diff --git a/selfdrive/ui/layouts/settings/starpilot/system_settings.py b/selfdrive/ui/layouts/settings/starpilot/system_settings.py index 7a8b648920..05f3605f92 100644 --- a/selfdrive/ui/layouts/settings/starpilot/system_settings.py +++ b/selfdrive/ui/layouts/settings/starpilot/system_settings.py @@ -57,13 +57,6 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import ( TOGGLE_ROW_HEIGHT, ) from openpilot.starpilot.common import param_profiles -from openpilot.starpilot.common.screen_settings import ( - BRIGHTNESS_KEYS, SCREEN_WAKE_DESCRIPTIONS, SCREEN_WAKE_OPTIONS, brightness_preferences, write_screen_setting, -) -from openpilot.selfdrive.ui.layouts.settings.starpilot.screen_controls import ( - AetherBrightnessDialog, BrightnessAdjustorRow, WakeToggleRow, show_screen_save_error, -) -from openpilot.selfdrive.ui.lib.screen_settings import try_screen_setting from openpilot.starpilot.common.connect_server import prepare_konik_server_switch from openpilot.starpilot.common.starpilot_variables import EXCLUDED_KEYS as STARPILOT_EXCLUDED_KEYS, TOGGLE_BACKUPS, update_starpilot_toggles @@ -142,7 +135,7 @@ class SystemSettingsManagerView(PanelManagerView): hours: f"{hours} " + (tr("hour") if hours == 1 else tr("hours")) for hours in range(1, 31) } - brightness_labels = {0: tr("Off")} + brightness_labels = {101: tr("Auto"), 0: tr("Off")} self._slider_specs: dict[str, dict[str, Any]] = { "ScreenBrightness": { @@ -151,11 +144,11 @@ class SystemSettingsManagerView(PanelManagerView): "unit": "%", "labels": brightness_labels, "min": 0, - "max": 100, + "max": 101, "step": 1, "live": True, - "presets": [0, 25, 50, 75, 100], - "get": lambda: float(brightness_preferences(self._controller._params, "ScreenBrightness")["manual"]), + "presets": [0, 25, 50, 75, 101], + "get": lambda: float(self._controller._params.get_int("ScreenBrightness")), "set": lambda v: self._controller._set_brightness("ScreenBrightness", v), }, "ScreenBrightnessOnroad": { @@ -164,17 +157,17 @@ class SystemSettingsManagerView(PanelManagerView): "unit": "%", "labels": brightness_labels, "min": 0, - "max": 100, + "max": 101, "step": 1, "live": True, - "presets": [0, 25, 50, 75, 100], - "get": lambda: float(brightness_preferences(self._controller._params, "ScreenBrightnessOnroad")["manual"]), + "presets": [0, 35, 60, 80, 101], + "get": lambda: float(self._controller._params.get_int("ScreenBrightnessOnroad")), "set": lambda v: self._controller._set_brightness("ScreenBrightnessOnroad", int(v)), }, "ScreenTimeout": { - "title": tr("Offroad Timeout"), - "subtitle": tr("Screen sleeps while parked."), - "unit": " seconds", + "title": tr("Offroad Screen Timeout"), + "subtitle": "", + "unit": "s", "labels": {}, "min": 5, "max": 60, @@ -185,9 +178,9 @@ class SystemSettingsManagerView(PanelManagerView): "set": lambda v: self._set_timeout("ScreenTimeout", v), }, "ScreenTimeoutOnroad": { - "title": tr("Onroad Timeout"), - "subtitle": tr("Standby sleeps the screen after this many seconds."), - "unit": " seconds", + "title": tr("Onroad Screen Timeout"), + "subtitle": "", + "unit": "s", "labels": {}, "min": 5, "max": 60, @@ -229,10 +222,8 @@ class SystemSettingsManagerView(PanelManagerView): return lambda active: self._show_system_slider(k) if active else None for key, spec in self._slider_specs.items(): - row_type = BrightnessAdjustorRow if key in BRIGHTNESS_KEYS else AetherAdjustorRow - row_kwargs = {"params": self._controller._params, "brightness_key": key} if key in BRIGHTNESS_KEYS else {} adjustor = self._child( - row_type( + AetherAdjustorRow( spec["title"], spec["subtitle"], spec["min"], @@ -248,7 +239,6 @@ class SystemSettingsManagerView(PanelManagerView): set_active=make_set_active(key), style=PANEL_STYLE, color=PANEL_STYLE.accent, - **row_kwargs, ) ) adjustor.set_touch_valid_callback(lambda: self._scroll_panel.is_touch_valid()) @@ -257,7 +247,7 @@ class SystemSettingsManagerView(PanelManagerView): self._toggle_defs = [ { "title": tr("Standby Mode"), - "subtitle": tr("Touch and ignition changes always wake the screen. Choose additional wake events below."), + "subtitle": "", "get_state": lambda: self._controller._params.get_bool("StandbyMode"), "set_state": lambda v: self._controller._params.put_bool("StandbyMode", v), }, @@ -309,17 +299,6 @@ class SystemSettingsManagerView(PanelManagerView): }, ] - self._wake_toggle_defs = [ - { - "title": tr("Wake: {}").format(tr(label)), - "subtitle": tr(SCREEN_WAKE_DESCRIPTIONS[key]), - "wake": True, - "get_state": lambda k=key, d=default: self._controller._params.get_bool(k, default=d), - "set_state": lambda value, k=key: self._save_wake_setting(k, value), - } - for key, label, default in SCREEN_WAKE_OPTIONS - ] - self._standby_visible = self._controller._params.get_bool("StandbyMode") self._basics_tile_grid_h = 0.0 if self.PANEL_STYLE.toggle_row_mode: @@ -330,7 +309,8 @@ class SystemSettingsManagerView(PanelManagerView): tile = self._make_toggle_tile(toggle_def) self._connectivity_tile_grid.add_tile(tile) self.register_page_grid(self._connectivity_tile_grid) - self._refresh_toggle_pages() + page_size = self._compute_page_size(TOGGLE_ROW_HEIGHT) + self._set_toggle_pages([self._toggle_defs[i:i+page_size] for i in range(0, len(self._toggle_defs), page_size)]) self._drive_mode_control = self._child( AetherSegmentedControl( @@ -342,29 +322,6 @@ class SystemSettingsManagerView(PanelManagerView): ) ) - def _save_wake_setting(self, key: str, value: bool): - return try_screen_setting(lambda: write_screen_setting(self._controller._params, key, value), show_screen_save_error) - - def _make_toggle_tile(self, definition: dict): - if definition.get("wake"): - return WakeToggleRow(title=definition["title"], desc=definition["subtitle"], - get_state=definition["get_state"], set_state=definition["set_state"], bg_color=self.PANEL_STYLE.accent) - return super()._make_toggle_tile(definition) - - def _display_keys(self) -> list[str]: - standby = self._controller._params.get_bool("StandbyMode") - return [key for key in self._display_slider_keys if key != "ScreenTimeoutOnroad" or standby] - - def _visible_toggle_defs(self) -> list[dict]: - if self._controller._params.get_bool("StandbyMode"): - return self._toggle_defs[:1] + self._wake_toggle_defs + self._toggle_defs[1:] - return self._toggle_defs - - def _refresh_toggle_pages(self): - definitions = self._visible_toggle_defs() - page_size = self._compute_page_size(TOGGLE_ROW_HEIGHT) - self._set_toggle_pages([definitions[i:i + page_size] for i in range(0, len(definitions), page_size)]) - def _tab_subtitle(self, tab_id: str) -> str: if tab_id == "basics": return tr("{} controls + {} toggles").format( @@ -385,9 +342,6 @@ class SystemSettingsManagerView(PanelManagerView): return f"{int(current_val)}{spec['unit']}" def _show_system_slider(self, key: str): - if key in BRIGHTNESS_KEYS: - gui_app.push_widget(AetherBrightnessDialog(self._controller._params, key, self._slider_specs[key]["title"])) - return spec = self._slider_specs[key] original_val = spec["get"]() @@ -535,21 +489,17 @@ class SystemSettingsManagerView(PanelManagerView): draw_custom_icon("first_aid", icon_x, icon_y, s, icon_color) def _measure_content_height(self, width: float) -> float: - standby = self._controller._params.get_bool("StandbyMode") - if standby != self._standby_visible: - self._standby_visible = standby - self._refresh_toggle_pages() - display_h = self._slider_section_height(self._display_keys(), width) + GROUP_TOP_INSET + GROUP_HEADER_TOTAL_HEIGHT + display_h = self._slider_section_height(self._display_slider_keys, width) + GROUP_TOP_INSET + GROUP_HEADER_TOTAL_HEIGHT power_h = self._slider_section_height(self._power_slider_keys, width) + GROUP_TOP_INSET + GROUP_HEADER_TOTAL_HEIGHT if self._uses_two_columns(width): column_w = self._column_width(width) # Reset custom heights to calculate natural measurements first - for key in self._display_keys() + self._power_slider_keys: + for key in self._display_slider_keys + self._power_slider_keys: self._adjustor_rows[key].custom_row_height = None - display_container_h = self._slider_section_height(self._display_keys(), column_w) + display_container_h = self._slider_section_height(self._display_slider_keys, column_w) power_container_h = self._slider_section_height(self._power_slider_keys, column_w) left_overhead = GROUP_TOP_INSET + 2 * GROUP_HEADER_TOTAL_HEIGHT + SECTION_GAP @@ -572,7 +522,7 @@ class SystemSettingsManagerView(PanelManagerView): if max_container_h < max_natural_h: scale_f = max_container_h / left_natural_content_h row_h = max(80.0, float(AETHER_LIST_METRICS.adjustor_row_height) * scale_f) - for key in self._display_keys() + self._power_slider_keys: + for key in self._display_slider_keys + self._power_slider_keys: self._adjustor_rows[key].custom_row_height = row_h self._system_max_container_h = max_container_h @@ -580,7 +530,7 @@ class SystemSettingsManagerView(PanelManagerView): return self._compute_two_column_height(max_container_h) else: # Ensure defaults are restored in single column mode - for key in self._display_keys() + self._power_slider_keys: + for key in self._display_slider_keys + self._power_slider_keys: self._adjustor_rows[key].custom_row_height = None tiles_content_h = self.measure_page_grid_height(self._connectivity_tile_grid, width - 24) return self._stacked_section_height([display_h, power_h, tiles_content_h + 24]) @@ -605,8 +555,8 @@ class SystemSettingsManagerView(PanelManagerView): current_y = y + GROUP_TOP_INSET current_y = draw_group_header(x + 24, current_y, column_w - 48, tr("Display")) - for index, key in enumerate(self._display_keys()): - current_y = self._draw_slider_row(rl.Rectangle(x, current_y, column_w, 0), key, is_last=index == len(self._display_keys()) - 1) + for index, key in enumerate(self._display_slider_keys): + current_y = self._draw_slider_row(rl.Rectangle(x, current_y, column_w, 0), key, is_last=index == len(self._display_slider_keys) - 1) current_y += SECTION_GAP @@ -620,7 +570,7 @@ class SystemSettingsManagerView(PanelManagerView): self._system_max_container_h, columns=tg_cols) return - y = self._draw_slider_section(y, x, width, tr("Display"), self._display_keys()) + y = self._draw_slider_section(y, x, width, tr("Display"), self._display_slider_keys) y += SECTION_GAP y = self._draw_slider_section(y, x, width, tr("Power"), self._power_slider_keys) y += SECTION_GAP @@ -1028,7 +978,13 @@ class StarPilotSystemLayout(_SettingsPage): self._on_reset_stock() def _set_brightness(self, key, val): - return try_screen_setting(lambda: write_screen_setting(self._params, key, int(val)), show_screen_save_error) + self._params.put_int(key, int(val)) + if not ui_state.started and key == "ScreenBrightness": + if hasattr(HARDWARE, 'set_screen_brightness'): + HARDWARE.set_screen_brightness(int(val)) + elif ui_state.started and key == "ScreenBrightnessOnroad": + if hasattr(HARDWARE, 'set_screen_brightness'): + HARDWARE.set_screen_brightness(int(val)) def _get_konik_state(self): if Path("/data/not_vetted").exists(): diff --git a/selfdrive/ui/lib/screen_settings.py b/selfdrive/ui/lib/screen_settings.py deleted file mode 100644 index 930bc1aa6d..0000000000 --- a/selfdrive/ui/lib/screen_settings.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Keep native screen controls usable after a rejected or failed settings write.""" - -from collections.abc import Callable - -from openpilot.common.params import UnknownKeyName - - -def try_screen_setting(write: Callable[[], object], on_error: Callable[[], None]) -> bool: - try: - write() - except (OSError, ValueError, UnknownKeyName): - on_error() - return False - return True diff --git a/selfdrive/ui/mici/layouts/settings/device.py b/selfdrive/ui/mici/layouts/settings/device.py index 4e855fac6d..c761be204e 100644 --- a/selfdrive/ui/mici/layouts/settings/device.py +++ b/selfdrive/ui/mici/layouts/settings/device.py @@ -14,7 +14,6 @@ from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmatio from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog, get_pairing_backend_name, get_pairing_host from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog from openpilot.selfdrive.ui.mici.layouts.onboarding import TrainingGuide, TermsPage -from openpilot.selfdrive.ui.mici.layouts.settings.screen import ScreenSettingsLayoutMici 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 @@ -272,8 +271,6 @@ class DeviceLayoutMici(NavScroller): self._connect_server_btn = ConnectServerBigButton() self._simple_mode_btn = BigParamControl("simple mode", "SimpleMode") - screen_settings_btn = BigButton("screen settings") - screen_settings_btn.set_click_callback(lambda: gui_app.push_widget(ScreenSettingsLayoutMici())) driver_cam_btn = BigButton("driver\ncamera preview", "", gui_app.texture("icons_mici/settings/device/cameras.png", 64, 64)) driver_cam_btn.set_click_callback(lambda: gui_app.push_widget(DriverCameraDialog())) @@ -291,7 +288,6 @@ class DeviceLayoutMici(NavScroller): self._connect_server_btn, PairBigButton(), self._simple_mode_btn, - screen_settings_btn, review_training_guide_btn, driver_cam_btn, reset_driver_monitoring_btn, diff --git a/selfdrive/ui/mici/layouts/settings/screen.py b/selfdrive/ui/mici/layouts/settings/screen.py deleted file mode 100644 index d95c3acfb2..0000000000 --- a/selfdrive/ui/mici/layouts/settings/screen.py +++ /dev/null @@ -1,344 +0,0 @@ -from __future__ import annotations - -from collections.abc import Callable -import pyray as rl - -from openpilot.common.params import Params -from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle, GreyBigButton -from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigMultiOptionDialog -from openpilot.starpilot.common.screen_settings import ( - BRIGHTNESS_KEYS, - SCREEN_INT_KEYS, - SCREEN_WAKE_DESCRIPTIONS, - SCREEN_WAKE_KEYS, - SCREEN_WAKE_OPTIONS, - brightness_preferences, - set_brightness_mode, - write_screen_setting, -) -from openpilot.system.ui.lib.application import FontWeight, gui_app -from openpilot.system.ui.widgets.label import gui_label -from openpilot.system.ui.widgets.nav_widget import NavWidget -from openpilot.system.ui.widgets.scroller import NavScroller -from openpilot.system.ui.lib.multilang import tr -from openpilot.selfdrive.ui.lib.screen_settings import try_screen_setting - - -def show_screen_save_error(): - gui_app.push_widget(BigDialog("", tr("Unable to save screen settings. Please try again."))) - - -class ScreenModeDialogMici(BigMultiOptionDialog): - def __init__(self, *args, current_option: Callable[[], str], **kwargs): - super().__init__(*args, **kwargs) - self._current_option = current_option - - def _confirm_selection(self): - if self._right_btn_callback is None or self._right_btn_callback(): - self.dismiss() - else: - self._selected_option = self._default_option = self._current_option() - self._on_option_selected(self._selected_option) - - -class ScreenSliderMici(NavWidget): - """A numeric slider sized for the comma4 display, with swipe-down back navigation.""" - - BACK_TOUCH_AREA_PERCENTAGE = 0.30 - - def __init__( - self, - title: str, - minimum: int, - maximum: int, - step: int, - value: int, - unit: str, - on_change: Callable[[int], bool | None], - read_value: Callable[[], int] | None = None, - ): - super().__init__() - self._title, self._minimum, self._maximum, self._step = title, minimum, maximum, step - self._value, self._unit, self._on_change = value, unit, on_change - self._committed_value = value - self._read_value = read_value - self._track = rl.Rectangle(0, 0, 1, 1) - self._minus = rl.Rectangle(0, 0, 0, 0) - self._plus = rl.Rectangle(0, 0, 0, 0) - self._pressed = None - self._dragging = False - - def _value_from_x(self, x: float) -> int: - fraction = max(0.0, min(1.0, (x - self._track.x) / max(1.0, self._track.width))) - value = self._minimum + round(fraction * (self._maximum - self._minimum) / self._step) * self._step - return max(self._minimum, min(self._maximum, int(value))) - - def _handle_mouse_press(self, mouse_pos): - if self.is_dismissing: - return - self._pressed = None - if rl.check_collision_point_rec(mouse_pos, self._minus): - self._pressed = "minus" - elif rl.check_collision_point_rec(mouse_pos, self._plus): - self._pressed = "plus" - elif rl.check_collision_point_rec(mouse_pos, rl.Rectangle(self._track.x, self._track.y - 35, self._track.width, 80)): - self._dragging = True - self._value = self._value_from_x(mouse_pos.x) - - def _handle_mouse_event(self, mouse_event): - super()._handle_mouse_event(mouse_event) - if self._dragging and mouse_event.left_down: - self._value = self._value_from_x(mouse_event.pos.x) - - def _handle_mouse_release(self, mouse_pos): - changed = self._dragging - if self._pressed == "minus" and rl.check_collision_point_rec(mouse_pos, self._minus): - self._value = max(self._minimum, self._value - self._step) - changed = True - elif self._pressed == "plus" and rl.check_collision_point_rec(mouse_pos, self._plus): - self._value = min(self._maximum, self._value + self._step) - changed = True - self._dragging, self._pressed = False, None - if changed and not self.is_dismissing: - if self._on_change(self._value) is False: - self._value = self._read_value() if self._read_value is not None else self._committed_value - self._committed_value = self._value - else: - self._committed_value = self._value - - def _render(self, rect: rl.Rectangle): - color = rl.Color(175, 150, 255, 255) - gui_label(rl.Rectangle(rect.x + 25, rect.y + 22, rect.width - 50, 45), self._title, 34, rl.WHITE, FontWeight.BOLD) - value = f"{self._value:+d}{self._unit}" if self._minimum < 0 else f"{self._value}{self._unit}" - gui_label(rl.Rectangle(rect.x + 25, rect.y + 72, rect.width - 50, 48), value, 42, color, FontWeight.BOLD, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) - y = rect.y + rect.height - 75 - self._minus = rl.Rectangle(rect.x + 20, y - 20, 60, 65) - self._plus = rl.Rectangle(rect.x + rect.width - 80, y - 20, 60, 65) - self._track = rl.Rectangle(rect.x + 105, y + 10, max(1, rect.width - 210), 8) - for button, label in ((self._minus, "-"), (self._plus, "+")): - rl.draw_rectangle_rounded(button, 0.3, 6, rl.Color(50, 50, 55, 255)) - gui_label(button, label, 42, rl.WHITE, FontWeight.BOLD, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) - rl.draw_rectangle_rounded(self._track, 1.0, 8, rl.Color(65, 65, 70, 255)) - fraction = (self._value - self._minimum) / max(1, self._maximum - self._minimum) - fill = rl.Rectangle(self._track.x, self._track.y, self._track.width * fraction, self._track.height) - if fill.width > 0: - rl.draw_rectangle_rounded(fill, 1.0, 8, color) - rl.draw_circle_v(rl.Vector2(self._track.x + fill.width, self._track.y + 4), 18, color) - for x, endpoint, alignment in ( - (self._track.x, self._minimum, rl.GuiTextAlignment.TEXT_ALIGN_LEFT), - (self._track.x + self._track.width - 140, self._maximum, rl.GuiTextAlignment.TEXT_ALIGN_RIGHT), - ): - endpoint_text = f"{endpoint:+d}{self._unit}" if self._minimum < 0 else f"{endpoint}{self._unit}" - gui_label(rl.Rectangle(x, y + 38, 140, 26), endpoint_text, 20, rl.Color(160, 160, 170, 255), FontWeight.NORMAL, alignment=alignment) - - -class ScreenValueButton(BigButton): - def __init__(self, title: str, params, get_spec: Callable[[], dict]): - super().__init__(title) - self._params, self._get_spec = params, get_spec - self.set_click_callback(self._show_slider) - self.refresh() - - def refresh(self): - spec = self._get_spec() - value = int(spec["value"]) - self.set_text(spec.get("label", self.text)) - self.set_value(f"{value:+d}{spec['unit']}" if spec["min"] < 0 else f"{value}{spec['unit']}") - - def _show_slider(self): - spec = self._get_spec() - gui_app.push_widget( - ScreenSliderMici( - spec["title"], - spec["min"], - spec["max"], - spec.get("step", 1), - spec["value"], - spec["unit"], - lambda value: self._save(spec["key"], value), - read_value=lambda: self._read_saved_value(spec["key"]), - ) - ) - - def _read_saved_value(self, key: str) -> int: - if key in BRIGHTNESS_KEYS: - return brightness_preferences(self._params, key)["manual"] - if key.endswith("Offset"): - return brightness_preferences(self._params, key.removesuffix("Offset"))["offset"] - return self._params.get_int(key, return_default=True) - - def _save(self, key: str, value: int): - def write(): - if key in SCREEN_INT_KEYS: - write_screen_setting(self._params, key, value) - else: - self._params.put_int(key, value) - if self._params.get_int(key) != value: - raise OSError("Screen timeout write did not persist") - - saved = try_screen_setting(write, show_screen_save_error) - self.refresh() - return saved - - -class BrightnessLayoutMici(NavScroller): - def __init__(self, key: str, title: str): - super().__init__() - self._params, self._key, self._title = Params(), key, title - self._mode = BigButton(title, "") - self._mode.set_click_callback(self._show_mode_selector) - self._value = ScreenValueButton("", self._params, self._slider_spec) - self._explanation = GreyBigButton("", "") - self._scroller.add_widgets([self._mode, self._value, self._explanation]) - self._refresh() - - def _slider_spec(self) -> dict: - prefs = brightness_preferences(self._params, self._key) - if prefs["mode"] == "auto": - return { - "key": self._key + "Offset", - "title": "auto brightness offset", - "label": "auto offset", - "min": -30, - "max": 30, - "value": prefs["offset"], - "unit": "%", - } - return {"key": self._key, "title": "manual brightness", "label": "manual brightness", "min": 0, "max": 100, "value": prefs["manual"], "unit": "%"} - - def _show_mode_selector(self): - prefs = brightness_preferences(self._params, self._key) - holder = {} - - def confirm(): - saved = try_screen_setting(lambda: set_brightness_mode(self._params, self._key, holder["dialog"].get_selected_option().lower()), show_screen_save_error) - self._refresh() - return saved - - dialog = ScreenModeDialogMici( - options=["Auto", "Manual"], - default=prefs["mode"].title(), - right_btn_callback=confirm, - current_option=lambda: brightness_preferences(self._params, self._key)["mode"].title(), - ) - holder["dialog"] = dialog - gui_app.push_widget(dialog) - - def _refresh(self): - prefs = brightness_preferences(self._params, self._key) - self._mode.set_value(prefs["mode"].title()) - self._value.refresh() - self._explanation.set_value( - "Auto offset: -30% to +30%. 0% keeps normal brightness." if prefs["mode"] == "auto" else "Manual sets a fixed brightness from 0 to 100%." - ) - - def show_event(self): - super().show_event() - self._refresh() - - def _update_state(self): - super()._update_state() - self._refresh() - - -class ScreenToggleMici(BigToggle): - def __init__(self, title: str, params, key: str, default: bool = False): - self._params, self._key, self._default = params, key, default - super().__init__(title, toggle_callback=self._save) - self.refresh() - - def _save(self, value: bool): - def write(): - if self._key in SCREEN_WAKE_KEYS: - write_screen_setting(self._params, self._key, value) - else: - self._params.put_bool(self._key, value) - if self._params.get_bool(self._key) != value: - raise OSError("Screen toggle write did not persist") - - saved = try_screen_setting(write, show_screen_save_error) - self.refresh() - return saved - - def refresh(self): - self.set_checked(self._params.get_bool(self._key, default=self._default)) - - -class ScreenWakeToggleMici(ScreenToggleMici): - """Keep each wake condition and its full explanation on one small-screen card.""" - - LABEL_HORIZONTAL_PADDING = 22 - LABEL_VERTICAL_PADDING = 14 - - def __init__(self, title: str, params, key: str, default: bool): - super().__init__(title, params, key, default) - self.set_value(SCREEN_WAKE_DESCRIPTIONS[key]) - self._sub_label.set_line_height(1.0) - - def _get_label_font_size(self): - return 26 - - def _width_hint(self) -> int: - return int(self._rect.width - self.LABEL_HORIZONTAL_PADDING * 2 - self._txt_enabled_toggle.width) - - def _draw_content(self, btn_y: float): - width = self._width_hint() - available = self._rect.height - self.LABEL_VERTICAL_PADDING * 2 - self._label.get_content_height(width) - for size in range(22, 17, -1): - self._sub_label.set_font_size(size) - if self._sub_label.get_content_height(width) <= available: - break - super()._draw_content(btn_y) - - -class ScreenSettingsLayoutMici(NavScroller): - def __init__(self): - super().__init__() - self._params = Params() - management = ScreenToggleMici("screen controls", self._params, "ScreenManagement", True) - offroad = BigButton("offroad brightness") - offroad.set_click_callback(lambda: gui_app.push_widget(BrightnessLayoutMici("ScreenBrightness", "offroad brightness"))) - onroad = BigButton("onroad brightness") - onroad.set_click_callback(lambda: gui_app.push_widget(BrightnessLayoutMici("ScreenBrightnessOnroad", "onroad brightness"))) - offroad_timeout = self._timeout_button("ScreenTimeout", "offroad timeout") - self._onroad_timeout = self._timeout_button("ScreenTimeoutOnroad", "onroad timeout") - standby = ScreenToggleMici("standby mode", self._params, "StandbyMode") - self._wake_controls = [ScreenWakeToggleMici("wake: " + label.lower(), self._params, key, default) for key, label, default in SCREEN_WAKE_OPTIONS] - explanation = GreyBigButton("", "Standby sleeps the screen onroad. Touch and ignition changes always wake it.") - self._refresh_controls = [management, offroad_timeout, self._onroad_timeout, standby, *self._wake_controls] - for control in [offroad, onroad, offroad_timeout, self._onroad_timeout, standby, *self._wake_controls]: - control.set_enabled(lambda: self._params.get_bool("ScreenManagement", default=True)) - self._scroller.add_widgets([management, offroad, onroad, offroad_timeout, standby, self._onroad_timeout, *self._wake_controls, explanation]) - self._refresh() - - def _timeout_button(self, key: str, title: str) -> ScreenValueButton: - return ScreenValueButton( - title, - self._params, - lambda: { - "key": key, - "title": title, - "label": title, - "min": 5, - "max": 60, - "step": 5, - "value": self._params.get_int(key, return_default=True), - "unit": " seconds", - }, - ) - - def _refresh(self): - for control in self._refresh_controls: - control.refresh() - standby = self._params.get_bool("StandbyMode") - self._onroad_timeout.set_visible(standby) - for control in self._wake_controls: - control.set_visible(standby) - - def show_event(self): - super().show_event() - self._refresh() - - def _update_state(self): - super()._update_state() - self._refresh() diff --git a/selfdrive/ui/tests/test_native_screen_controls.py b/selfdrive/ui/tests/test_native_screen_controls.py deleted file mode 100644 index fba90e7461..0000000000 --- a/selfdrive/ui/tests/test_native_screen_controls.py +++ /dev/null @@ -1,209 +0,0 @@ -"""Native UI behavior tests without loading device-only raylib/msgq libraries.""" - -import ast -import importlib.util -from pathlib import Path -from types import SimpleNamespace -import pytest -from openpilot.selfdrive.ui.lib.screen_settings import try_screen_setting - -ROOT = Path(__file__).resolve().parents[3] - - -def method(path, cls, name, **namespace): - assert (ROOT / path).exists(), f'Missing native screen implementation {path}' - tree = ast.parse((ROOT / path).read_text()) - class_node = next((node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == cls), None) - assert class_node is not None, f'Missing native screen control {cls}' - node = next((node for node in class_node.body if isinstance(node, ast.FunctionDef) and node.name == name), None) - assert node is not None, f'Missing native screen behavior {cls}.{name}' - module = ast.Module(body=[ast.ImportFrom(module='__future__', names=[ast.alias(name='annotations')], level=0), node], type_ignores=[]) - namespace.setdefault('try_screen_setting', try_screen_setting) - namespace.setdefault('show_screen_save_error', lambda: pytest.fail('Unexpected native save error')) - exec(compile(ast.fix_missing_locations(module), str(ROOT / path), 'exec'), namespace) - return namespace[name] - - -SYSTEM = 'selfdrive/ui/layouts/settings/starpilot/system_settings.py' -MICi = 'selfdrive/ui/mici/layouts/settings/screen.py' - - -def real_params(directory): - spec = importlib.util.spec_from_file_location('native_test_params', ROOT / 'common/params.py') - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module.Params(directory) - - -class TestNativeScreenControls: - @pytest.fixture(autouse=True) - def params_directory(self, tmp_path): - self._params_directory = tmp_path - - def _real_params(self): - return real_params(str(self._params_directory)) - - def test_mici_toggle_refresh_uses_real_boolean_getter_signature(self): - refresh = method(MICi, 'ScreenToggleMici', 'refresh') - params = self._real_params() - values = [] - control = SimpleNamespace(_params=params, _key='StandbyWakeTurnSignal', _default=False, set_checked=values.append) - refresh(control) - params.put_bool('StandbyWakeTurnSignal', True) - refresh(control) - assert values == [False, True] - - def test_mici_screen_controls_enabled_uses_real_boolean_getter(self): - tree = ast.parse((ROOT / MICi).read_text()) - screen_class = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == 'ScreenSettingsLayoutMici') - enabled_call = next( - node for node in ast.walk(screen_class) if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and (node.func.attr == 'set_enabled') - ) - params = self._real_params() - enabled = eval(compile(ast.Expression(enabled_call.args[0]), 'native-enable-callback', 'eval'), {'self': SimpleNamespace(_params=params)}) - assert enabled() - params.put_bool('ScreenManagement', False) - assert not enabled() - - def test_big_ui_wake_callbacks_use_real_boolean_getter_and_keep_choices_independent(self): - tree = ast.parse((ROOT / SYSTEM).read_text()) - system_class = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == 'SystemSettingsManagerView') - assignment = next( - node - for node in ast.walk(system_class) - if isinstance(node, ast.Assign) and any(isinstance(target, ast.Attribute) and target.attr == '_wake_toggle_defs' for target in node.targets) - ) - params = self._real_params() - view = SimpleNamespace(_controller=SimpleNamespace(_params=params)) - namespace = { - 'self': view, - 'tr': lambda text: text, - 'SCREEN_WAKE_DESCRIPTIONS': {'StandbyWakeEngage': 'Wake when engaged.', 'StandbyWakeTurnSignal': 'Wake when indicating.'}, - 'SCREEN_WAKE_OPTIONS': [('StandbyWakeEngage', 'Engagement', True), ('StandbyWakeTurnSignal', 'Turn signals', False)], - } - exec(compile(ast.Module(body=[assignment], type_ignores=[]), 'native-wake-callbacks', 'exec'), namespace) - assert [option['get_state']() for option in view._wake_toggle_defs] == [True, False] - assert [option['subtitle'] for option in view._wake_toggle_defs] == ['Wake when engaged.', 'Wake when indicating.'] - params.put_bool('StandbyWakeTurnSignal', True) - params.put_bool('StandbyWakeEngage', False) - assert [option['get_state']() for option in view._wake_toggle_defs] == [False, True] - - def test_mici_screen_toggles_save_existing_and_new_settings(self): - helper_spec = importlib.util.spec_from_file_location('screen_settings', ROOT / 'starpilot/common/screen_settings.py') - helper = importlib.util.module_from_spec(helper_spec) - helper_spec.loader.exec_module(helper) - save = method(MICi, 'ScreenToggleMici', '_save', SCREEN_WAKE_KEYS=helper.SCREEN_WAKE_KEYS, write_screen_setting=helper.write_screen_setting) - params = self._real_params() - for key in ('ScreenManagement', 'StandbyMode', 'StandbyWakeTurnSignal'): - save(SimpleNamespace(_params=params, _key=key, refresh=lambda: None), True) - assert [params.get_bool(key) for key in ('ScreenManagement', 'StandbyMode', 'StandbyWakeTurnSignal')] == [True, True, True] - - def test_mici_value_save_remembers_brightness_and_accepts_existing_timeout(self): - helper_spec = importlib.util.spec_from_file_location('screen_settings', ROOT / 'starpilot/common/screen_settings.py') - helper = importlib.util.module_from_spec(helper_spec) - helper_spec.loader.exec_module(helper) - save = method(MICi, 'ScreenValueButton', '_save', SCREEN_INT_KEYS=helper.SCREEN_INT_KEYS, write_screen_setting=helper.write_screen_setting) - params = self._real_params() - params.put_int('ScreenBrightness', 101) - view = SimpleNamespace(_params=params, refresh=lambda: None) - save(view, 'ScreenBrightness', 38) - save(view, 'ScreenTimeout', 25) - assert [params.get_int(key) for key in ('ScreenBrightness', 'ScreenBrightnessManual', 'ScreenTimeout')] == [38, 38, 25] - - def test_big_ui_offroad_timeout_remains_visible_when_standby_is_off(self): - get_keys = method(SYSTEM, 'SystemSettingsManagerView', '_display_keys') - params = SimpleNamespace(get_bool=lambda key: False) - view = SimpleNamespace( - _controller=SimpleNamespace(_params=params), _display_slider_keys=['ScreenBrightness', 'ScreenBrightnessOnroad', 'ScreenTimeout', 'ScreenTimeoutOnroad'] - ) - assert get_keys(view) == ['ScreenBrightness', 'ScreenBrightnessOnroad', 'ScreenTimeout'] - params.get_bool = lambda key: True - assert get_keys(view) == view._display_slider_keys - - def test_big_ui_standby_reveals_wake_choices_without_losing_other_toggles(self): - get_defs = method(SYSTEM, 'SystemSettingsManagerView', '_visible_toggle_defs') - params = SimpleNamespace(get_bool=lambda key: False) - view = SimpleNamespace( - _controller=SimpleNamespace(_params=params), - _toggle_defs=[{'title': 'Standby'}, {'title': 'Uploads'}], - _wake_toggle_defs=[{'title': 'Engagement'}, {'title': 'Turn signals'}], - ) - assert get_defs(view) == [{'title': 'Standby'}, {'title': 'Uploads'}] - params.get_bool = lambda key: True - assert get_defs(view) == [{'title': 'Standby'}, {'title': 'Engagement'}, {'title': 'Turn signals'}, {'title': 'Uploads'}] - - def test_mici_brightness_slider_selects_offset_or_manual_with_correct_bounds(self): - for mode, want in [('auto', ('ScreenBrightnessOnroadOffset', -30, 30, -25, '%')), ('manual', ('ScreenBrightnessOnroad', 0, 100, 73, '%'))]: - get_spec = method( - MICi, 'BrightnessLayoutMici', '_slider_spec', brightness_preferences=lambda params, key, mode=mode: {'mode': mode, 'manual': 73, 'offset': -25} - ) - view = SimpleNamespace(_params=object(), _key='ScreenBrightnessOnroad') - spec = get_spec(view) - assert (spec['key'], spec['min'], spec['max'], spec['value'], spec['unit']) == want - - def test_big_ui_auto_summary_uses_signed_percent_without_affecting_manual(self): - for mode, offset, want in [('auto', 25, 'Auto +25%'), ('auto', -25, 'Auto -25%'), ('manual', 0, 'Manual 73%')]: - formatted_value = method( - 'selfdrive/ui/layouts/settings/starpilot/screen_controls.py', - 'BrightnessAdjustorRow', - 'formatted_value', - brightness_preferences=lambda params, key, mode=mode, offset=offset: {'mode': mode, 'manual': 73, 'offset': offset}, - tr=lambda text: text, - ) - assert formatted_value(SimpleNamespace(_params=object(), _brightness_key='ScreenBrightness')) == want - - def test_mici_value_card_signs_offset_only_when_both_modes_use_percent(self): - refresh = method(MICi, 'ScreenValueButton', 'refresh') - for minimum, value, want in [(-30, 25, '+25%'), (-30, -25, '-25%'), (-30, 0, '+0%'), (0, 73, '73%')]: - values = [] - spec = {'min': minimum, 'value': value, 'unit': '%'} - button = SimpleNamespace(_get_spec=lambda spec=spec: spec, text='brightness', set_text=lambda text: None, set_value=values.append) - refresh(button) - assert values == [want] - - def test_big_ui_manual_selection_uses_new_default_and_preserves_remembered_value(self): - helper_spec = importlib.util.spec_from_file_location('screen_settings', ROOT / 'starpilot/common/screen_settings.py') - helper = importlib.util.module_from_spec(helper_spec) - helper_spec.loader.exec_module(helper) - choose_mode = method( - 'selfdrive/ui/layouts/settings/starpilot/screen_controls.py', 'AetherBrightnessDialog', '_set_mode', set_brightness_mode=helper.set_brightness_mode - ) - params = self._real_params() - view = SimpleNamespace(_params=params, _key='ScreenBrightnessOnroad') - choose_mode(view, 1) - assert params.get_int('ScreenBrightnessOnroad') == 100 - params.put_int('ScreenBrightnessOnroad', 41) - choose_mode(view, 0) - choose_mode(view, 1) - assert params.get_int('ScreenBrightnessOnroad') == 41 - - def test_mici_standby_controls_visibility_without_changing_saved_values(self): - refresh = method(MICi, 'ScreenSettingsLayoutMici', '_refresh') - - class Control: - visible = None - - def refresh(self): - pass - - def set_visible(self, value): - self.visible = value - - params = SimpleNamespace(get_bool=lambda key: False) - timeout = Control() - triggers = [Control(), Control()] - view = SimpleNamespace(_params=params, _refresh_controls=[], _onroad_timeout=timeout, _wake_controls=triggers) - refresh(view) - assert not timeout.visible - assert all(control.visible is False for control in triggers) - params.get_bool = lambda key: True - refresh(view) - assert timeout.visible - assert all(control.visible is True for control in triggers) - - def test_mici_numeric_slider_clamps_and_snaps_to_allowed_steps(self): - value_from_x = method(MICi, 'ScreenSliderMici', '_value_from_x') - slider = SimpleNamespace(_track=SimpleNamespace(x=50, width=400), _minimum=-30, _maximum=30, _step=1) - assert [value_from_x(slider, x) for x in (0, 50, 150, 250, 449, 500)] == [-30, -30, -15, 0, 30, 30] - slider._minimum, slider._maximum, slider._step = (5, 60, 5) - assert [value_from_x(slider, x) for x in (0, 122, 250, 500)] == [5, 15, 35, 60] diff --git a/selfdrive/ui/tests/test_native_screen_save_errors.py b/selfdrive/ui/tests/test_native_screen_save_errors.py deleted file mode 100644 index 904a6e44a5..0000000000 --- a/selfdrive/ui/tests/test_native_screen_save_errors.py +++ /dev/null @@ -1,302 +0,0 @@ -"""Exercise registered native save callbacks when the shared writer rejects a save.""" - -import os -from contextlib import nullcontext -from types import SimpleNamespace -import pytest -from unittest.mock import patch # noqa: TID251 - standard-library mocking inside pytest tests - -os.environ.setdefault('SP_HEADLESS_TEST', '1') -from openpilot.common.params import Params, UnknownKeyName -from openpilot.selfdrive.ui.layouts.settings.starpilot import screen_controls as big -from openpilot.selfdrive.ui.mici.layouts.settings import screen as mici - - -class Range: - def __init__(self, *args, **kwargs): - self.current_val = args[3] if len(args) > 3 else 0 - self.commit = kwargs.get('on_commit') - self.is_interacting = False - - def set_visible(self, value): - pass - - def set_value(self, value): - self.current_val = value - - def reset_interaction(self): - self.is_interacting = False - - -class TestNativeScreenSaveErrors: - @pytest.fixture(autouse=True) - def setup(self, tmp_path): - self.params = Params(str(tmp_path)) - self.params.put_int('ScreenBrightness', 101) - self.params.put_int('ScreenBrightnessManual', 37) - self.params.put_int('ScreenBrightnessOffset', -12) - self.params.put_bool('StandbyWakeTurnSignal', False) - self.errors = [] - with ( - patch.object(big, 'show_screen_save_error', lambda: self.errors.append('save failed')), - patch.object(mici, 'show_screen_save_error', lambda: self.errors.append('save failed')), - ): - yield - - def assert_recoverable(self, action): - try: - result = action() - except (OSError, ValueError) as error: - pytest.fail(f'Save error escaped the native callback: {error}') - assert self.errors == ['save failed'] - return result - - def big_dialog(self): - with patch.object(big, 'AetherInlineRangeControl', Range), patch.object(big, 'AetherSegmentedControl', Range): - return big.AetherBrightnessDialog(self.params, 'ScreenBrightness', 'Offroad brightness') - - def test_c3_failed_mode_save_preserves_auto_and_manual_memory(self): - dialog = self.big_dialog() - with patch.object(big, 'set_brightness_mode', side_effect=OSError('Screen settings busy')): - self.assert_recoverable(lambda: dialog._set_mode(1)) - assert dialog._mode_index() == 0 - assert self.params.get_int('ScreenBrightnessManual') == 37 - - def test_c3_failed_auto_selection_preserves_manual_brightness(self): - self.params.put_int('ScreenBrightness', 37) - dialog = self.big_dialog() - with patch.object(big, 'set_brightness_mode', side_effect=ValueError('Invalid setting')): - self.assert_recoverable(lambda: dialog._set_mode(0)) - assert dialog._mode_index() == 1 - assert self.params.get_int('ScreenBrightness') == 37 - - def test_c3_registered_slider_callbacks_restore_saved_values(self): - for control_name, saved in (('_offset', -12), ('_manual', 37)): - self.errors.clear() - self.params.put_int('ScreenBrightness', 37 if control_name == '_manual' else 101) - dialog = self.big_dialog() - control = getattr(dialog, control_name) - control.current_val = 25 - with patch.object(big, 'write_screen_setting', side_effect=OSError('disk full')): - self.assert_recoverable(lambda control=control: control.commit(25)) - assert control.current_val == saved - assert self.params.get_int('ScreenBrightnessManual') == 37 - assert self.params.get_int('ScreenBrightnessOffset') == -12 - - def test_c3_registered_wake_toggle_recovers_from_failed_save(self): - import ast - from types import MethodType, SimpleNamespace - from openpilot.selfdrive.ui.tests.test_native_screen_controls import ROOT, SYSTEM, method - from openpilot.starpilot.common.screen_settings import SCREEN_WAKE_DESCRIPTIONS, SCREEN_WAKE_OPTIONS - - def fail_write(params, key, value): - raise OSError("Screen settings busy") - - namespace = {"write_screen_setting": fail_write, "show_screen_save_error": lambda: self.errors.append("save failed")} - save = method(SYSTEM, "SystemSettingsManagerView", "_save_wake_setting", **namespace) - view = SimpleNamespace(_controller=SimpleNamespace(_params=self.params)) - view._save_wake_setting = MethodType(save, view) - tree = ast.parse((ROOT / SYSTEM).read_text()) - cls = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "SystemSettingsManagerView") - definition = next( - node.value - for node in ast.walk(cls) - if isinstance(node, ast.Assign) and any(isinstance(target, ast.Attribute) and target.attr == "_wake_toggle_defs" for target in node.targets) - ) - definitions = eval( - compile(ast.Expression(definition), "wake-controls", "eval"), - dict(namespace, self=view, tr=lambda value: value, SCREEN_WAKE_OPTIONS=SCREEN_WAKE_OPTIONS, SCREEN_WAKE_DESCRIPTIONS=SCREEN_WAKE_DESCRIPTIONS), - ) - turn_signal = next(control for control in definitions if "Turn signals" in control["title"]) - self.assert_recoverable(lambda: turn_signal["set_state"](True)) - assert turn_signal["get_state"]() is False - assert self.params.get_bool("StandbyWakeTurnSignal") is False - - @pytest.mark.parametrize("error_type", [OSError, ValueError, UnknownKeyName]) - def test_c4_failed_wake_toggle_restores_checked_state(self, error_type): - control = mici.ScreenToggleMici.__new__(mici.ScreenToggleMici) - control._params, control._key, control._default = (self.params, 'StandbyWakeTurnSignal', False) - control._checked = True - with patch.object(mici, 'write_screen_setting', side_effect=error_type('Screen settings busy')): - self.assert_recoverable(lambda: control._save(True)) - assert not control._checked - assert not self.params.get_bool('StandbyWakeTurnSignal') - - @pytest.mark.parametrize('key,minimum,saved', [('ScreenBrightnessOffset', -30, -12), ('ScreenBrightness', 0, 37)]) - def test_c4_slider_failure_restores_visible_value_and_keeps_slider_open(self, key, minimum, saved): - self.params.put_int('ScreenBrightness', 37 if key == 'ScreenBrightness' else 101) - button = mici.ScreenValueButton.__new__(mici.ScreenValueButton) - button._params = self.params - button.refresh = lambda: None - slider = mici.ScreenSliderMici("brightness", minimum, 30 if minimum < 0 else 100, 1, saved, "%", lambda value: button._save(key, value)) - slider._value, slider._dragging = 25, True - with patch.object(mici, 'write_screen_setting', side_effect=OSError('Screen settings busy')): - self.assert_recoverable(lambda: slider._handle_mouse_release(None)) - assert slider._value == saved - assert not slider.is_dismissing - assert self.params.get_int(key) == saved - - @pytest.mark.parametrize('initial,selected,saved_mode', [(101, 'Manual', 'Auto'), (37, 'Auto', 'Manual')]) - @pytest.mark.parametrize('fail_save', [True, False]) - def test_c4_mode_confirmation_dismisses_only_after_successful_save(self, initial, selected, saved_mode, fail_save): - self.params.put_int('ScreenBrightness', initial) - layout = mici.BrightnessLayoutMici.__new__(mici.BrightnessLayoutMici) - layout._params, layout._key = (self.params, 'ScreenBrightness') - shown_modes = [] - layout._mode = SimpleNamespace(set_value=shown_modes.append) - layout._value = SimpleNamespace(refresh=lambda: None) - layout._explanation = SimpleNamespace(set_value=lambda value: None) - dialogs = [] - base = getattr(mici, 'ScreenModeDialogMici', mici.BigMultiOptionDialog) - - class Dialog: - def __init__(self, options, default, right_btn_callback, **kwargs): - self._right_btn_callback = right_btn_callback - self._selected_option = selected - self._default_option = default - self._current_option = kwargs.get('current_option', lambda: default) - self.dismissed = False - - def get_selected_option(self): - return self._selected_option - - def dismiss(self, callback=None): - self.dismissed = True - if callback: - callback() - - def _on_option_selected(self, option): - self._selected_option = option - - with ( - patch.object(mici, 'BigMultiOptionDialog', Dialog), - patch.object(mici, 'ScreenModeDialogMici', Dialog, create=True), - patch.object(mici.gui_app, 'push_widget', dialogs.append), - ): - layout._show_mode_selector() - dialog = dialogs[0] - with patch.object(mici, 'set_brightness_mode', side_effect=OSError('Screen settings busy')) if fail_save else nullcontext(): - if fail_save: - self.assert_recoverable(lambda: base._confirm_selection(dialog)) - else: - base._confirm_selection(dialog) - assert self.errors == [] - assert dialog.dismissed is not fail_save - assert dialog.get_selected_option() == (saved_mode if fail_save else selected) - assert shown_modes[-1] == (saved_mode if fail_save else selected) - assert self.params.get_int('ScreenBrightness') == (initial if fail_save else 37 if selected == 'Manual' else 101) - assert self.params.get_int('ScreenBrightnessManual') == 37 - - @pytest.mark.parametrize("interface", ["c3", "c4"]) - def test_native_controls_recover_when_another_save_holds_the_lock(self, interface): - from openpilot.starpilot.common.screen_settings import _screen_write_transaction - - if interface == "c3": - control = self.big_dialog() - - def action(): - return control._set_mode(1) - else: - control = mici.ScreenToggleMici.__new__(mici.ScreenToggleMici) - control._params, control._key, control._default = self.params, "StandbyWakeTurnSignal", False - control._checked = True - - def action(): - return control._save(True) - - with _screen_write_transaction(self.params): - self.assert_recoverable(action) - assert self.params.get_int("ScreenBrightness") == 101 - assert self.params.get_int("ScreenBrightnessManual") == 37 - assert self.params.get_bool("StandbyWakeTurnSignal") is False - if interface == "c3": - assert control._mode_index() == 0 - else: - assert control._checked is False - - @pytest.mark.parametrize("key,initial,external", [("ScreenBrightnessOffset", -12, 19), ("ScreenBrightness", 37, 73)]) - def test_c4_rejected_save_reads_latest_persisted_slider_value_before_retry(self, key, initial, external): - if key == "ScreenBrightness": - mici.write_screen_setting(self.params, key, initial) - button = mici.ScreenValueButton.__new__(mici.ScreenValueButton) - button._params = self.params - button.refresh = lambda: None - button._get_spec = lambda: { - "key": key, - "title": "brightness", - "value": initial, - "min": -30 if key.endswith("Offset") else 0, - "max": 30 if key.endswith("Offset") else 100, - "unit": "%", - } - dialogs = [] - with patch.object(mici.gui_app, "push_widget", dialogs.append): - button._show_slider() - slider = dialogs[0] - # A second client changes this value while the native slider is open. - mici.write_screen_setting(self.params, key, external) - if key == "ScreenBrightness": - mici.set_brightness_mode(self.params, key, "auto") - slider._value, slider._dragging = 25, True - with patch.object(mici, "write_screen_setting", side_effect=OSError("Screen settings busy")): - self.assert_recoverable(lambda: slider._handle_mouse_release(None)) - assert slider._value == external - assert slider._committed_value == external - assert not slider.is_dismissing - slider._value, slider._dragging = external + 1, True - slider._handle_mouse_release(None) - assert self.params.get_int(key) == external + 1 - assert slider._committed_value == external + 1 - - - @pytest.mark.parametrize("key", ["ScreenTimeout", "ScreenTimeoutOnroad"]) - def test_c4_silent_timeout_failure_restores_slider_and_allows_retry(self, key): - self.params.put_int(key, 30) - - class DroppedWrites: - def put_int(self, key, value): - pass - - def __getattr__(_self, name): - return getattr(self.params, name) - - button = mici.ScreenValueButton.__new__(mici.ScreenValueButton) - button._params = DroppedWrites() - button.refresh = lambda: None - slider = mici.ScreenSliderMici( - "timeout", 5, 60, 5, 30, " seconds", lambda value: button._save(key, value), - read_value=lambda: button._read_saved_value(key), - ) - slider._value, slider._dragging = 60, True - self.assert_recoverable(lambda: slider._handle_mouse_release(None)) - assert slider._value == slider._committed_value == self.params.get_int(key) == 30 - assert not slider.is_dismissing - button._params = self.params - self.errors.clear() - slider._value, slider._dragging = 60, True - slider._handle_mouse_release(None) - assert slider._value == slider._committed_value == self.params.get_int(key) == 60 - assert self.errors == [] - - @pytest.mark.parametrize("key", ["ScreenManagement", "StandbyMode"]) - def test_c4_silent_toggle_failure_restores_state_and_reports_error(self, key): - self.params.put_bool(key, False) - - class DroppedWrites: - def put_bool(self, key, value): - pass - - def __getattr__(_self, name): - return getattr(self.params, name) - - control = mici.ScreenToggleMici.__new__(mici.ScreenToggleMici) - control._params, control._key, control._default = DroppedWrites(), key, False - control._checked = True - assert self.assert_recoverable(lambda: control._save(True)) is False - assert control._checked is False - assert self.params.get_bool(key) is False - control._params = self.params - self.errors.clear() - assert control._save(True) is True - assert control._checked is True and self.params.get_bool(key) is True - assert self.errors == [] diff --git a/starpilot/common/assets/device_settings_layout.json b/starpilot/common/assets/device_settings_layout.json index b463c08c9b..c9983a6ee1 100644 --- a/starpilot/common/assets/device_settings_layout.json +++ b/starpilot/common/assets/device_settings_layout.json @@ -4689,7 +4689,7 @@ "galaxy_ui_type": "brightness", "step": 1, "parent_key": "ScreenManagement", - "settings_tier": "simple", + "settings_tier": "advanced", "default": 101, "min": 0, "max": 101 @@ -4703,7 +4703,7 @@ "galaxy_ui_type": "brightness", "step": 1, "parent_key": "ScreenManagement", - "settings_tier": "simple", + "settings_tier": "advanced", "default": 101, "min": 0, "max": 101 @@ -4771,7 +4771,7 @@ "ScreenManagement", "StandbyMode" ], - "settings_tier": "simple" + "settings_tier": "advanced" }, { "key": "StandbyWakeDisengage", @@ -4785,7 +4785,7 @@ "ScreenManagement", "StandbyMode" ], - "settings_tier": "simple" + "settings_tier": "advanced" }, { "key": "StandbyWakeInfoAlert", @@ -4799,7 +4799,7 @@ "ScreenManagement", "StandbyMode" ], - "settings_tier": "simple" + "settings_tier": "advanced" }, { "key": "StandbyWakeWarningAlert", @@ -4813,7 +4813,7 @@ "ScreenManagement", "StandbyMode" ], - "settings_tier": "simple" + "settings_tier": "advanced" }, { "key": "StandbyWakeCriticalAlert", @@ -4827,7 +4827,7 @@ "ScreenManagement", "StandbyMode" ], - "settings_tier": "simple" + "settings_tier": "advanced" }, { "key": "StandbyWakeTurnSignal", @@ -4841,7 +4841,7 @@ "ScreenManagement", "StandbyMode" ], - "settings_tier": "simple" + "settings_tier": "advanced" }, { "key": "StandbyWakeButton", @@ -4855,7 +4855,7 @@ "ScreenManagement", "StandbyMode" ], - "settings_tier": "simple" + "settings_tier": "advanced" } ] }, diff --git a/starpilot/common/screen_settings.py b/starpilot/common/screen_settings.py index 75368d1009..edbc8dd2b6 100644 --- a/starpilot/common/screen_settings.py +++ b/starpilot/common/screen_settings.py @@ -1,4 +1,4 @@ -"""Shared screen preferences and wake policy for native UI and Galaxy. +"""Shared screen preferences and wake policy for Galaxy and services. The existing brightness value 101 continues to encode Auto for older clients. Manual values and offsets are independent for the driving and parked screens. @@ -43,7 +43,7 @@ def _raw(params, key): try: return params.get(key) except (UnknownKeyName, KeyError, TypeError, ValueError): - # Older native registries may not yet know new settings during an update. + # Older registries may not know new settings during an update. return None @@ -84,8 +84,7 @@ def enabled_wake_keys(params): @contextmanager def _screen_write_transaction(params): - # Keep the lock outside the key directory, where Params clearing cannot remove - # it. Include the prefix so isolated Params stores do not block each other. + # Keep the lock outside the key directory, where Params clearing cannot remove it. directory = Path(params.get_param_path()) lock_path = directory.parent / f'.screen_settings.{directory.name}.lock' with _WRITE_LOCK: diff --git a/starpilot/common/tests/test_screen_settings.py b/starpilot/common/tests/test_screen_settings.py index 97402f1ec0..9b0583f5f6 100644 --- a/starpilot/common/tests/test_screen_settings.py +++ b/starpilot/common/tests/test_screen_settings.py @@ -1,6 +1,7 @@ import importlib from pathlib import Path -from tempfile import TemporaryDirectory +import shutil +from tempfile import mkdtemp from types import SimpleNamespace import pytest @@ -14,10 +15,15 @@ class Params: def __init__(self, values=None): self.values = dict(values or {}) self.fail_key = None - self.directory = TemporaryDirectory(prefix='screen-settings-test-') + self.directory = Path(mkdtemp(prefix='screen-settings-test-')) def get_param_path(self): - return str(Path(self.directory.name) / 'd') + return str(self.directory / 'd') + + def __del__(self): + directory = getattr(self, 'directory', None) + if directory is not None: + shutil.rmtree(directory, ignore_errors=True) def get(self, key): return self.values.get(key) diff --git a/starpilot/docs/screen_settings.md b/starpilot/docs/screen_settings.md index 0b5fb68101..6a7a10f761 100644 --- a/starpilot/docs/screen_settings.md +++ b/starpilot/docs/screen_settings.md @@ -1,6 +1,6 @@ # Screen brightness and Standby -Screen Management provides independent driving and parked brightness preferences in New Galaxy and the native comma 3/3X and comma 4 interfaces. +Screen Management provides independent driving and parked brightness preferences in Galaxy. ## Brightness @@ -8,9 +8,7 @@ Auto remains the default. It uses the existing automatic brightness calculation The existing onroad calculation follows camera exposure and filters changes. Parked Auto retains the existing base level: 50% on comma 3/3X and 65% on comma 4, with existing screen overrides still applied. This feature does not add an offroad ambient-light sensor. -Manual allows 0–100% and starts at 100% when there is no previous manual choice. Each context remembers its own manual value when switched to Auto. Existing manual selections remain selected. Touch and ignition changes temporarily make manual 0% visible at 5%; recognised button presses do so when their wake toggle is enabled. As in Dom, manual onroad 0% suppresses automatic engagement and alert wakes; the same rule applies to the optional turn-signal wake. - -New Galaxy shows a mode selector and the slider for that mode. Standby uses the normal Galaxy toggle styling and an enabled-only Manage/Close submenu containing the onroad timeout and wake choices. Native settings provide the same preferences using their existing screen sizes and navigation patterns. +Galaxy shows a mode selector and the slider for that mode. Standby uses the normal Galaxy toggle styling and an enabled-only Manage/Close submenu containing the onroad timeout and wake choices. ## Timeouts and wake choices @@ -28,19 +26,15 @@ Touch and ignition changes always wake Standby, as in Dom. The **Bluetooth or st | Turn signals | Off | Signal activation or direction change | | Bluetooth or steering wheel button | Off | Recognised button press, including unassigned buttons | -Engagement and alert detection use Dom's existing status and alert predicates. Entering override alone does not wake. A selected alert keeps resetting the timer while it remains reported. Primary alerts take precedence over the secondary StarPilot alert state, as in Dom; this code does not duplicate renderer-generated alerts or add a separate freshness policy. Wake preferences only affect the display, not the alert or its sound. +Engagement and alert detection use Dom's existing status and alert predicates. Entering override alone does not wake. A selected alert keeps resetting the timer while it remains reported; wake preferences only affect the display, not the alert or its sound. -Dom's ignition transitions, screen-setting changes and page timeout handling are retained. There are no gear, brake-pedal or accelerator wake triggers. Standby powers the display down after the timeout using Dom's existing display-power path. - -Vehicle buttons use the car interface's existing decoded `carState.buttonEvents`. The existing UI subscriber drains every message so short presses survive between UI refreshes, while UI state and frequency tracking receive only the latest frame. No additional vehicle-message reader is opened. Controller buttons use the existing input-device reader. When button wake is enabled, fresh presses wake once; releases, key repeat and held buttons do not keep extending the timer. Mapped actions retain their separate enable setting and continue to work normally. Button coverage depends on what the existing vehicle interface and supported input devices expose; this PR introduces no manufacturer-specific CAN decoding. +Vehicle buttons use the car interface's existing decoded `carState.buttonEvents`. The existing UI subscriber drains every message so short presses survive between UI refreshes, while UI state and frequency tracking receive only the latest frame. Controller buttons use the existing input-device reader. When button wake is enabled, fresh presses wake once; releases, key repeat and held buttons do not keep extending the timer. Mapped actions retain their separate enable setting and continue to work normally. This feature introduces no manufacturer-specific CAN decoding. ## Persistence and compatibility The existing brightness keys retain 101 as Auto and 0–100 as Manual. Four additional persistent integers store manual memory and relative offsets. Seven persistent booleans store wake selections. StandbyButtonPressTime carries fresh external-controller button timestamps in RAM, clears on manager start, and is excluded from logging. -Native UI and Galaxy writes use one shared validator and an advisory nonblocking file lock outside the Params key directory. Snapshot, write, readback and rollback run within that transaction; UI caches invalidate inside and after it. A busy or failed save is reported and can be retried. Other direct Params writers must use the shared helper to participate in this transaction contract. - -The Params registry source must be included in the normal device build before installation. Source changes alone do not update an existing compiled native registry. +Galaxy and UI-state writes use one shared validator and an advisory nonblocking file lock outside the Params key directory. Snapshot, write, readback and rollback run within that transaction; caches invalidate inside and after it. A busy or failed save is reported and can be retried. Other direct Params writers should use the shared helper to participate in this transaction contract. ## Focused verification @@ -49,8 +43,7 @@ From a configured Linux checkout with the project Python dependencies: ```sh PYTHONPATH=. python -m pytest -q -c /dev/null --confcutdir=starpilot/common/tests \ starpilot/common/tests/test_screen_*.py \ - selfdrive/ui/tests/test_native_screen_controls.py \ - selfdrive/ui/tests/test_native_screen_save_errors.py \ + selfdrive/ui/tests/test_device_screen_settings.py \ starpilot/system/wheel_controls/tests PYTHONPATH=. python -m pytest -q -c /dev/null --confcutdir=starpilot/system/the_galaxy/tests \ @@ -62,6 +55,6 @@ PYTHONPATH=. python -m pytest -q -c /dev/null --confcutdir=starpilot/system/the_ node starpilot/system/the_galaxy/tests/test_screen_settings_dom.cjs ``` -The DOM test requires Playwright and Chromium. PLAYWRIGHT_MODULE and CHROMIUM_EXECUTABLE can point to an existing installation; GALAXY_DOM_SCREENSHOT optionally saves previews. It loads the real Vue components with synthetic API responses and no device writes. The Python commands bypass unrelated manager-wide fixtures and explicitly include starpilot tests, which are outside the repository's default testpaths. A fully built environment can additionally run selfdrive/ui/tests/test_device_screen_settings.py through the normal pytest configuration. +The DOM test requires Playwright and Chromium. `PLAYWRIGHT_MODULE` and `CHROMIUM_EXECUTABLE` can point at an existing installation; `GALAXY_DOM_SCREENSHOT` optionally saves previews. It loads the real Vue components with synthetic API responses and no device writes. The Python commands bypass unrelated manager-wide fixtures and explicitly include StarPilot tests, which are outside the repository's default testpaths. -Automated tests cover every wake choice enabled and disabled, generic-button freshness, held inputs, Dom status and alert behavior, minimum brightness, write failures, cross-process saves, native controls, browser interactions and existing controller actions. Physical screen readability and actual car input coverage still require checks on the relevant hardware. +Automated tests cover wake choices, generic-button freshness, held inputs, Dom status and alert behavior, minimum brightness, write failures, cross-process saves, browser interactions and existing controller actions. Physical screen readability and actual car input coverage still require checks on relevant hardware. diff --git a/starpilot/system/the_galaxy/tests/test_device_settings_layout.py b/starpilot/system/the_galaxy/tests/test_device_settings_layout.py index e5753b877d..0351bda2ef 100644 --- a/starpilot/system/the_galaxy/tests/test_device_settings_layout.py +++ b/starpilot/system/the_galaxy/tests/test_device_settings_layout.py @@ -293,6 +293,15 @@ def test_requested_simple_and_advanced_settings_tiers(): if not param["key"].startswith("PIPPreview") and param["key"] != "DisableWideRoad" ] + if section_name == "Device & Data": + params = [ + param for param in params + if param["key"] not in { + "ScreenBrightness", "ScreenBrightnessOnroad", "StandbyWakeEngage", + "StandbyWakeDisengage", "StandbyWakeInfoAlert", "StandbyWakeWarningAlert", + "StandbyWakeCriticalAlert", "StandbyWakeTurnSignal", "StandbyWakeButton", + } + ] assert {param["settings_tier"] for param in params} == {"simple"} for key in ("AlwaysOnLateral", "LaneChanges", "QOLLateral"): @@ -338,6 +347,16 @@ def test_requested_simple_and_advanced_settings_tiers(): assert developer["RedneckCruise"]["settings_tier"] == "advanced" assert sections["Visual (Display & UI)"]["DisableWideRoad"]["settings_tier"] == "advanced" + device = sections["Device & Data"] + assert device["ScreenBrightness"]["settings_tier"] == "advanced" + assert device["ScreenBrightnessOnroad"]["settings_tier"] == "advanced" + for key in ( + "StandbyWakeEngage", "StandbyWakeDisengage", "StandbyWakeInfoAlert", + "StandbyWakeWarningAlert", "StandbyWakeCriticalAlert", "StandbyWakeTurnSignal", + "StandbyWakeButton", + ): + assert device[key]["settings_tier"] == "advanced" + def test_turn_steering_limit_mute_speed_is_galaxy_developer_only(): sections = _params_by_section(_layout()) diff --git a/starpilot/system/the_galaxy/tests/test_screen_settings_dom.cjs b/starpilot/system/the_galaxy/tests/test_screen_settings_dom.cjs index 714eb5699e..a6ba920c58 100644 --- a/starpilot/system/the_galaxy/tests/test_screen_settings_dom.cjs +++ b/starpilot/system/the_galaxy/tests/test_screen_settings_dom.cjs @@ -21,7 +21,7 @@ import {api} from '/assets/mobile/js/api.js'; const layout = ${JSON.stringify(layout)}; const values = reactive({ScreenManagement:true, ScreenBrightness:101, ScreenBrightnessManual:67, ScreenBrightnessOffset:0, ScreenBrightnessOnroad:101, ScreenBrightnessOnroadOffset:0, ScreenTimeout:30, ScreenTimeoutOnroad:15, StandbyMode:false, - ...${JSON.stringify(wakeDefaults)}}); + GalaxyDeveloperMode:false, ...${JSON.stringify(wakeDefaults)}}); window.values=values; window.writes=[]; window.holdWrite=false; window.failWrite=false; window.fetch=async (input,init={}) => { const url=new URL(input,location.href); @@ -64,6 +64,10 @@ createApp({components:{SettingTree},setup:()=>({values}), const onMode=page.locator('#gx-ScreenBrightnessOnroad-mode') const offSlider=page.locator('#gx-ScreenBrightness-slider') const onSlider=page.locator('#gx-ScreenBrightnessOnroad-slider') + assert.equal(section.params.find(p => p.key === 'ScreenBrightness').settings_tier, 'advanced') + assert.equal(section.params.find(p => p.key === 'StandbyWakeButton').settings_tier, 'advanced') + assert.equal(await offMode.count(), 0, 'screen controls stay hidden until Galaxy Developer Mode is enabled') + await page.evaluate(()=>{window.values.GalaxyDeveloperMode=true}) await offMode.waitFor() assert.equal(await offMode.inputValue(),'auto') assert.equal(await onMode.inputValue(),'auto')