diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index f0921b6723..198b7a92a1 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -180,6 +180,8 @@ inline static std::unordered_map keys = { {"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}}, {"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}}, {"RocketFuel", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"ScreenSaverEnabled", {PERSISTENT | BACKUP, BOOL, "1"}}, + {"ScreenSaverTimeout", {PERSISTENT | BACKUP, INT, "300"}}, {"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}}, {"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}}, diff --git a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/display.py b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/display.py index 8ba5663662..897d34085a 100644 --- a/openpilot/selfdrive/ui/sunnypilot/layouts/settings/display.py +++ b/openpilot/selfdrive/ui/sunnypilot/layouts/settings/display.py @@ -9,7 +9,7 @@ from enum import IntEnum from openpilot.system.ui.widgets import Widget from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets.scroller_tici import Scroller -from openpilot.system.ui.sunnypilot.widgets.list_view import option_item_sp +from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, option_item_sp from openpilot.sunnypilot.system.params_migration import ONROAD_BRIGHTNESS_TIMER_VALUES @@ -61,10 +61,26 @@ class DisplayLayout(Widget): f"{value} s" if value < 60 else f"{int(value/60)} m"), inline=True ) + self._screensaver_toggle = toggle_item_sp( + param="ScreenSaverEnabled", + title=lambda: tr("Screen Saver"), + description=lambda: tr("Show a screen saver when the device is offroad and idle, instead of turning the screen off."), + ) + self._screensaver_timeout = option_item_sp( + param="ScreenSaverTimeout", + title=lambda: tr("Screen Saver Duration"), + description=lambda: tr("How long the screen saver runs before the screen turns off."), + min_value=60, + max_value=600, + value_change_step=60, + label_callback=lambda value: f"{int(value/60)} m" + ) items = [ self._onroad_brightness, self._onroad_brightness_timer, self._interactivity_timeout, + self._screensaver_toggle, + self._screensaver_timeout, ] return items @@ -87,6 +103,8 @@ class DisplayLayout(Widget): brightness_val = self._onroad_brightness.action_item.current_value self._onroad_brightness_timer.action_item.set_enabled(brightness_val not in (OnroadBrightness.AUTO, OnroadBrightness.AUTO_DARK)) + self._screensaver_timeout.set_visible(self._screensaver_toggle.action_item.get_state()) + def _render(self, rect): self._scroller.render(rect) diff --git a/openpilot/selfdrive/ui/sunnypilot/ui_state.py b/openpilot/selfdrive/ui/sunnypilot/ui_state.py index 4828f37103..3f2889de9a 100644 --- a/openpilot/selfdrive/ui/sunnypilot/ui_state.py +++ b/openpilot/selfdrive/ui/sunnypilot/ui_state.py @@ -12,6 +12,7 @@ from openpilot.common.params import Params from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import OnroadBrightness from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.sunnypilot.widgets.screen_saver import ScreenSaverSP OpenpilotState = log.SelfdriveState.OpenpilotState MADSState = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState @@ -38,6 +39,9 @@ class UIStateSP: self.sunnylink_state = SunnylinkState() + self.screensaver = ScreenSaverSP(params=self.params) + self.screensaver_enabled: bool = False + self.active_bundle = None self.blindspot: bool = False self.chevron_metrics = None @@ -170,6 +174,7 @@ class UIStateSP: self.turn_signals = self.params.get_bool("ShowTurnSignals") self.boot_offroad_mode = self.params.get("DeviceBootMode", return_default=True) self.always_offroad = self.params.get_bool("OffroadMode") + self.screensaver_enabled = self.params.get_bool("ScreenSaverEnabled") if not self._sp_initialized: self._sp_initialized = True @@ -230,10 +235,26 @@ class UIStateSP: class DeviceSP: + def __init__(self): + self._blocked_by_screensaver: bool = False + def _set_awake(self, on: bool, _ui_state=None): + self._blocked_by_screensaver = False + if _ui_state.boot_offroad_mode == 1 and not on: _ui_state.params.put_bool("OffroadMode", True) + if not on and _ui_state.screensaver_enabled: + if _ui_state.screensaver.was_dismissed: + if gui_app.get_active_widget() == _ui_state.screensaver: + gui_app.pop_widget() + elif _ui_state.screensaver.is_active: + self._blocked_by_screensaver = True + else: + _ui_state.screensaver.initialize() + gui_app.push_widget(_ui_state.screensaver) + self._blocked_by_screensaver = True + @staticmethod def set_onroad_brightness(_ui_state, awake: bool, cur_brightness: float) -> float: if not awake or not _ui_state.started: diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index 59742edfed..67845bdc87 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -340,6 +340,8 @@ class Device(DeviceSP): def _set_awake(self, on: bool, _ui_state=None): if on != self._awake: super()._set_awake(on, _ui_state or ui_state) + if self._blocked_by_screensaver: + return self._awake = on cloudlog.debug(f"setting display power {int(on)}") HARDWARE.set_display_power(on) diff --git a/openpilot/sunnypilot/sunnylink/settings_ui.json b/openpilot/sunnypilot/sunnylink/settings_ui.json index 1e6422ac84..dd972c2957 100644 --- a/openpilot/sunnypilot/sunnylink/settings_ui.json +++ b/openpilot/sunnypilot/sunnylink/settings_ui.json @@ -1286,6 +1286,67 @@ "label": "2 m" } ] + }, + { + "key": "ScreenSaverEnabled", + "widget": "toggle", + "title": "Screen Saver", + "description": "Show a screen saver when the device is offroad and idle, instead of turning the screen off." + }, + { + "key": "ScreenSaverTimeout", + "widget": "multiple_button", + "title": "Screen Saver Duration", + "description": "How long the screen saver runs before the screen turns off.", + "options": [ + { + "value": 60, + "label": "1 m" + }, + { + "value": 120, + "label": "2 m" + }, + { + "value": 180, + "label": "3 m" + }, + { + "value": 240, + "label": "4 m" + }, + { + "value": 300, + "label": "5 m" + }, + { + "value": 360, + "label": "6 m" + }, + { + "value": 420, + "label": "7 m" + }, + { + "value": 480, + "label": "8 m" + }, + { + "value": 540, + "label": "9 m" + }, + { + "value": 600, + "label": "10 m" + } + ], + "enablement": [ + { + "type": "param", + "key": "ScreenSaverEnabled", + "equals": true + } + ] } ] } diff --git a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/display.yaml b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/display.yaml index 39a8cbaf80..3e3b16c374 100644 --- a/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/display.yaml +++ b/openpilot/sunnypilot/sunnylink/settings_ui_src/pages/display.yaml @@ -128,3 +128,36 @@ sections: label: 1 m - value: 120 label: 2 m + - key: ScreenSaverEnabled + widget: toggle + title: Screen Saver + description: Show a screen saver when the device is offroad and idle, instead of turning the screen off. + - key: ScreenSaverTimeout + widget: multiple_button + title: Screen Saver Duration + description: How long the screen saver runs before the screen turns off. + options: + - value: 60 + label: 1 m + - value: 120 + label: 2 m + - value: 180 + label: 3 m + - value: 240 + label: 4 m + - value: 300 + label: 5 m + - value: 360 + label: 6 m + - value: 420 + label: 7 m + - value: 480 + label: 8 m + - value: 540 + label: 9 m + - value: 600 + label: 10 m + enablement: + - type: param + key: ScreenSaverEnabled + equals: true diff --git a/openpilot/system/ui/sunnypilot/widgets/screen_saver.py b/openpilot/system/ui/sunnypilot/widgets/screen_saver.py new file mode 100644 index 0000000000..bf218306d8 --- /dev/null +++ b/openpilot/system/ui/sunnypilot/widgets/screen_saver.py @@ -0,0 +1,118 @@ +""" +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 os +import time + +import pyray as rl + +from openpilot.common.hardware import HARDWARE +from openpilot.common.params import Params +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.widgets import Widget + + +class ScreenSaverSP(Widget): + def __init__(self, params: Params | None = None): + super().__init__() + self.set_rect(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) + self._params = params or Params() + self._is_mici = HARDWARE.get_device_type() == 'mici' or (HARDWARE.get_device_type() == "pc" and os.getenv("BIG") != "1") + + self.x = 0.0 + self.y = 100.0 + self.vx = 120.0 if self._is_mici else 300.0 + self.vy = 70.0 if self._is_mici else 200.0 + self._hue = 150 + self.color = rl.color_from_hsv(self._hue, 1, 1) + + self.text = "sunnypilot" + self.font_size = 50 if self._is_mici else 200 + self._start_time = None + self._dismiss = False + self._screensaver_timeout = 300 + self._hit_last_frame = False + + @property + def is_active(self) -> bool: + return self._start_time is not None and not self._dismiss + + @property + def was_dismissed(self) -> bool: + return self._dismiss + + def initialize(self): + self._screensaver_timeout = self._params.get("ScreenSaverTimeout", return_default=True) + if self._start_time is None: + self._start_time = time.monotonic() + self._dismiss = False + + def hide_event(self): + super().hide_event() + self._dismiss = False + self._start_time = None + + def _handle_mouse_release(self, mouse_pos): + self._dismiss = True + self._start_time = None + gui_app.pop_widget() + return super()._handle_mouse_release(mouse_pos) + + def _update_state(self): + super()._update_state() + + self.font = gui_app.font(FontWeight.AUDIOWIDE) + text_size = measure_text_cached(self.font, self.text, self.font_size, 0) + self.logo_width = text_size.x + self.logo_height = text_size.y + + if self._start_time and time.monotonic() - self._start_time > self._screensaver_timeout: + self._dismiss = True + self._start_time = None + + dt = rl.get_frame_time() + + self.x += self.vx * dt + self.y += self.vy * dt + + hit_x = hit_y = False + if self.x + self.logo_width > self.rect.width: + self.vx *= -1 + self.x = self.rect.width - self.logo_width + hit_x = True + elif self.x < 0: + self.vx *= -1 + self.x = 0 + hit_x = True + + if self.y + self.logo_height > self.rect.height: + self.vy *= -1 + self.y = self.rect.height - self.logo_height + hit_y = True + elif self.y < 0: + self.vy *= -1 + self.y = 0 + hit_y = True + + hit = hit_x or hit_y + if hit and not self._hit_last_frame: + while self._hue_dist((new_hue := rl.get_random_value(0, 360)), self._hue) < 120: + pass + self._hue = new_hue + self.color = rl.color_from_hsv(self._hue, 1, 1) + self._hit_last_frame = hit + + @staticmethod + def _hue_dist(a, b): + d = abs(a - b) + return min(d, 360 - d) + + def _render(self, rect: rl.Rectangle): + self.set_rect(rect) + rl.clear_background(rl.BLACK) + rl.draw_text_ex(self.font, self.text, rl.Vector2(int(self.x), int(self.y)), self.font_size, 0, self.color) + return -1