ui: screensaver (#1551)

* param to control stock vs sp ui

* init styles

* SP Toggles

* Lint

* optimizations

* Panels. With Icons. And Scroller.

* patience, grasshopper

* more patience, grasshopper

* sp raylib preview

* fix callback

* fix ui preview

* add ui previews

* Option Control

* Need this

* introducing ui_state_sp for py

* param to control stock vs sp ui

* better

* add ui_update callback

* better padding

* this

* listitem -> listitemsp

* Revert "add ui_update callback"

This reverts commit 4da32cc009.

* add show_description method

* remove padding from line separator.
like, WHY? 😩😩

* simplify

* I. SAID. SIMPLIFY.

* AAARGGGGGG.....

* init

* option control value fix

* add all controls

* hide all controls

* lint

* scroller -> scroller_tici

* scroller -> scroller_tici

* ui: `GuiApplicationExt`

* add to readme

* use gui_app.sunnypilot_ui()

* use gui_app.sunnypilot_ui()

* use gui_app.sunnypilot_ui()

* optimizations

* Removed hide for now

* why? because!

* oops

* better

* add param, timeout

* add toggle

* not doing this

* we doing this now @devtekve? FINE!!

* it wasn't me

* really @devtekve? REALLY??

* refresh controls

* ugh

* changes

* fix

* inline everything again

* handle default

* use ui_state param

* better

* fix

* lint

* better

* solve for lint

* default on pls

* screen saver hue distance and increase minimum color shift

* debounce screen saver color changes

* fix dismiss flow

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
Co-authored-by: DevTekVE <devtekve@gmail.com>
This commit is contained in:
Nayan
2026-08-10 04:27:30 -04:00
committed by GitHub
parent c6595fd99b
commit 2a16b0fbba
7 changed files with 256 additions and 1 deletions
+2
View File
@@ -180,6 +180,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> 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"}},
@@ -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)
@@ -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:
+2
View File
@@ -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)
@@ -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
}
]
}
]
}
@@ -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
@@ -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