BigUI WIP: Refinement

BigUI WIP: Sound Panel Touch Sliders
BigUI WIP: Sound Elements -> Aethergrid Extraction
BigUI WIP: More push_widget
This commit is contained in:
firestarsdog
2026-04-20 23:03:05 -04:00
parent 91f587f061
commit 5e3ceafa5b
16 changed files with 1084 additions and 3962 deletions
-2972
View File
File diff suppressed because it is too large Load Diff
@@ -4,7 +4,7 @@ import math
import time
import pyray as rl
from collections.abc import Callable
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, MouseEvent
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget, DialogResult
@@ -1352,3 +1352,117 @@ class TileGrid(Widget):
tile = tiles_to_render[tile_idx]
tile.render(rl.Rectangle(row_x + c * (row_tile_w + self._gap), rect.y + r * (tile_h + self._gap), row_tile_w, tile_h))
tile_idx += 1
class AetherContinuousSlider(Widget):
def __init__(self, min_val: float, max_val: float, step: float, current_val: float, on_change, title: str = "", unit: str = "", labels: dict | None = None, color: rl.Color | None = None):
super().__init__()
self.min_val = min_val
self.max_val = max_val
self.base_step = step
self.current_val = current_val
self.on_change = on_change
self.title = title
self.unit = unit
self.labels = labels or {}
self.color = color or rl.Color(54, 77, 239, 255)
self._is_dragging = False
self._last_mouse_x = 0.0
self._smooth_value = current_val
self._font = gui_app.font(FontWeight.BOLD)
def _handle_mouse_press(self, mouse_pos: MousePos):
if rl.check_collision_point_rec(mouse_pos, self._rect):
self._is_dragging = True
self._last_mouse_x = mouse_pos.x
self._update_val_from_absolute(mouse_pos.x, self.base_step)
def _handle_mouse_release(self, mouse_pos: MousePos):
if self._is_dragging:
self._is_dragging = False
def _handle_mouse_event(self, mouse_event: MouseEvent):
if self._is_dragging:
dt = rl.get_frame_time()
dx = mouse_event.pos.x - self._last_mouse_x
self._last_mouse_x = mouse_event.pos.x
velocity = abs(dx / max(dt, 0.001))
if velocity > 1500:
step = self.base_step * 10
elif velocity > 500:
step = self.base_step * 5
else:
step = self.base_step
self._update_val_from_absolute(mouse_event.pos.x, step)
def _update_val_from_absolute(self, mouse_x: float, step: float):
track_w = self._rect.width
if track_w <= 0: return
rel_x = max(0.0, min(1.0, (mouse_x - self._rect.x) / track_w))
val = self.min_val + rel_x * (self.max_val - self.min_val)
self._set_snapped_val(val, step)
def _set_snapped_val(self, val: float, step: float):
snapped = round((val - self.min_val) / step) * step + self.min_val
snapped = max(self.min_val, min(self.max_val, snapped))
if snapped != self.current_val:
self.current_val = snapped
self.on_change(self.current_val)
def _render(self, rect: rl.Rectangle):
self.set_rect(rect)
dt = rl.get_frame_time()
self._smooth_value += (self.current_val - self._smooth_value) * (1 - math.exp(-dt / 0.060))
rl.draw_rectangle_rounded(rect, 0.3, 16, rl.Color(35, 35, 40, 255))
frac = max(0.0, min(1.0, (self._smooth_value - self.min_val) / (self.max_val - self.min_val)))
fill_w = frac * rect.width
if fill_w > 0:
fill_rect = rl.Rectangle(rect.x, rect.y, fill_w, rect.height)
rl.draw_rectangle_rounded(fill_rect, 0.3, 16, self.color)
if fill_w > 16:
rl.draw_rectangle_rounded(rl.Rectangle(fill_rect.x, fill_rect.y, fill_rect.width - 2, fill_rect.height - 2), 0.3, 16, rl.Color(255, 255, 255, 30))
title_y = rect.y + (rect.height - 24) / 2
rl.draw_text_ex(self._font, self.title, rl.Vector2(round(rect.x + 24), round(title_y)), 24, 0, rl.WHITE)
val_str = self.labels.get(self.current_val, f"{int(self.current_val)}{self.unit}")
ts = measure_text_cached(self._font, val_str, 24)
text_color = rl.WHITE if frac < 0.85 else rl.Color(0, 0, 0, 180)
text_x = rect.x + rect.width - ts.x - 24
text_y = rect.y + (rect.height - ts.y) / 2
rl.draw_text_ex(self._font, val_str, rl.Vector2(round(text_x), round(text_y)), 24, 0, text_color)
def draw_toggle_pill(rect: rl.Rectangle, is_on: bool, is_enabled: bool, title: str, status_str: str, hovered: bool, pressed: bool):
if not is_enabled:
bg_color = rl.Color(35, 35, 40, 150)
elif is_on:
bg_color = AetherListColors.PRIMARY
else:
bg_color = rl.Color(35, 35, 40, 255)
rl.draw_rectangle_rounded(rect, 0.3, 16, bg_color)
if (hovered or pressed) and is_enabled:
overlay = rl.Color(255, 255, 255, 14 if pressed else 8)
rl.draw_rectangle_rounded(rect, 0.3, 16, overlay)
if is_on and is_enabled:
rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y, rect.width - 2, rect.height - 2), 0.3, 16, rl.Color(255, 255, 255, 30))
font = gui_app.font(FontWeight.BOLD)
title_y = rect.y + (rect.height - 24) / 2
text_color = rl.WHITE if is_enabled else AetherListColors.MUTED
rl.draw_text_ex(font, title, rl.Vector2(round(rect.x + 24), round(title_y)), 24, 0, text_color)
ts = measure_text_cached(font, status_str, 24)
status_x = rect.x + rect.width - ts.x - 24
rl.draw_text_ex(font, status_str, rl.Vector2(round(status_x), round(title_y)), 24, 0, text_color)
@@ -1,258 +0,0 @@
from __future__ import annotations
import os
import shutil
import threading
import subprocess
from datetime import datetime
from pathlib import Path
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog
from openpilot.system.ui.widgets.keyboard import Keyboard
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid
LEGACY_STARPILOT_PARAM_RENAMES = {
"FrogPilotApiToken": "StarPilotApiToken",
"FrogPilotCarParams": "StarPilotCarParams",
"FrogPilotCarParamsPersistent": "StarPilotCarParamsPersistent",
"FrogPilotDongleId": "StarPilotDongleId",
"FrogPilotStats": "StarPilotStats",
}
class StarPilotDataLayout(StarPilotPanel):
def __init__(self):
super().__init__()
self._keyboard = Keyboard(min_text_size=0)
self.CATEGORIES = [
{"title": tr_noop("Manage Backups"), "panel": "backups", "icon": "toggle_icons/icon_system.png", "color": "#D43D8A"},
{"title": tr_noop("Toggle Backups"), "panel": "toggle_backups", "icon": "toggle_icons/icon_system.png", "color": "#D43D8A"},
{"title": tr_noop("Driving Data Storage"), "type": "value", "get_value": self._get_storage, "on_click": lambda: None, "icon": "toggle_icons/icon_system.png", "color": "#D43D8A", "is_enabled": lambda: False},
{
"title": tr_noop("Delete Driving Data"),
"type": "hub",
"on_click": self._on_delete_driving_data,
"icon": "toggle_icons/icon_system.png",
"color": "#D43D8A",
},
{
"title": tr_noop("Delete Error Logs"),
"type": "hub",
"on_click": self._on_delete_error_logs,
"icon": "toggle_icons/icon_system.png",
"color": "#D43D8A",
},
]
self._sub_panels = {
"backups": StarPilotBackupsLayout(),
"toggle_backups": StarPilotToggleBackupsLayout(),
}
self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True)
for name, panel in self._sub_panels.items():
if hasattr(panel, 'set_navigate_callback'):
panel.set_navigate_callback(self._navigate_to)
if hasattr(panel, 'set_back_callback'):
panel.set_back_callback(self._go_back)
self._rebuild_grid()
def _on_delete_driving_data(self):
def _do_delete(res):
if res == DialogResult.CONFIRM:
def _task():
drive_paths = ["/data/media/0/realdata/", "/data/media/0/realdata_HD/", "/data/media/0/realdata_konik/"]
for path in drive_paths:
p = Path(path)
if p.exists():
for entry in p.iterdir():
if entry.is_dir():
shutil.rmtree(entry, ignore_errors=True)
threading.Thread(target=_task, daemon=True).start()
gui_app.set_modal_overlay(alert_dialog(tr("Driving data deletion started.")))
gui_app.set_modal_overlay(ConfirmDialog(tr("Delete all driving data and footage?"), tr("Delete"), on_close=_do_delete))
def _on_delete_error_logs(self):
def _do_delete(res):
if res == DialogResult.CONFIRM:
shutil.rmtree("/data/error_logs", ignore_errors=True)
os.makedirs("/data/error_logs", exist_ok=True)
gui_app.set_modal_overlay(alert_dialog(tr("Error logs deleted.")))
gui_app.set_modal_overlay(ConfirmDialog(tr("Delete all error logs?"), tr("Delete"), on_close=_do_delete))
def _get_storage(self):
paths = ["/data/media/0/osm/offline", "/data/media/0/realdata", "/data/backups"]
total = 0
for p in paths:
pp = Path(p)
if pp.exists():
total += sum(f.stat().st_size for f in pp.rglob('*') if f.is_file())
mb = total / (1024 * 1024)
if mb > 1024:
return f"{(mb / 1024):.2f} GB"
return f"{mb:.2f} MB"
class StarPilotBackupsLayout(StarPilotPanel):
def __init__(self):
super().__init__()
self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True)
self.CATEGORIES = [
{"title": tr_noop("Create Backup"), "type": "hub", "on_click": self._on_create_backup, "color": "#D43D8A"},
{"title": tr_noop("Restore Backup"), "type": "hub", "on_click": self._on_restore_backup, "color": "#D43D8A"},
{"title": tr_noop("Delete Backup"), "type": "hub", "on_click": self._on_delete_backup, "color": "#D43D8A"},
]
self._rebuild_grid()
def _get_backups(self):
b_dir = Path("/data/backups")
if not b_dir.exists():
return []
return [f.name for f in b_dir.glob("*.tar.zst") if "in_progress" not in f.name]
def _on_create_backup(self):
def on_name(res, name):
if res == DialogResult.CONFIRM:
safe_name = name.replace(" ", "_") if name else f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
backup_path = f"/data/backups/{safe_name}.tar.zst"
if Path(backup_path).exists():
gui_app.set_modal_overlay(alert_dialog(tr("A backup with this name already exists.")))
return
gui_app.set_modal_overlay(alert_dialog(tr("Backup creation started.")))
def _task():
os.makedirs("/data/backups", exist_ok=True)
subprocess.run(["tar", "--use-compress-program=zstd", "-cf", backup_path, "/data/openpilot"])
threading.Thread(target=_task, daemon=True).start()
self._keyboard.reset(min_text_size=0)
self._keyboard.set_title(tr("Name your backup"), "")
self._keyboard.set_text("")
self._keyboard.set_callback(lambda result: on_name(result, self._keyboard.text))
gui_app.push_widget(self._keyboard)
def _on_restore_backup(self):
backups = self._get_backups()
if not backups:
gui_app.set_modal_overlay(alert_dialog(tr("No backups found.")))
return
dialog = MultiOptionDialog(tr("Select Backup"), backups)
def _on_select(res):
if res == DialogResult.CONFIRM and dialog.selection:
gui_app.set_modal_overlay(alert_dialog(tr("Restoring... device will reboot.")))
def _task():
subprocess.run(["rm", "-rf", "/data/openpilot/*"])
subprocess.run(["tar", "--use-compress-program=zstd", "-xf", f"/data/backups/{dialog.selection}", "-C", "/"])
os.system("reboot")
threading.Thread(target=_task, daemon=True).start()
gui_app.set_modal_overlay(dialog, callback=_on_select)
def _on_delete_backup(self):
backups = self._get_backups()
if not backups:
gui_app.set_modal_overlay(alert_dialog(tr("No backups found.")))
return
dialog = MultiOptionDialog(tr("Delete Backup"), backups)
def _on_select(res):
if res == DialogResult.CONFIRM and dialog.selection:
os.remove(f"/data/backups/{dialog.selection}")
self._rebuild_grid()
gui_app.set_modal_overlay(dialog, callback=_on_select)
class StarPilotToggleBackupsLayout(StarPilotPanel):
def __init__(self):
super().__init__()
self._keyboard = Keyboard(min_text_size=0)
self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True)
self.CATEGORIES = [
{"title": tr_noop("Create Toggle Backup"), "type": "hub", "on_click": self._on_create, "color": "#D43D8A"},
{"title": tr_noop("Restore Toggle Backup"), "type": "hub", "on_click": self._on_restore, "color": "#D43D8A"},
{"title": tr_noop("Delete Toggle Backup"), "type": "hub", "on_click": self._on_delete, "color": "#D43D8A"},
]
self._rebuild_grid()
def _get_backups(self):
b_dir = Path("/data/toggle_backups")
if not b_dir.exists():
return []
return [d.name for d in b_dir.iterdir() if d.is_dir() and "in_progress" not in d.name]
def _on_create(self):
def on_name(res, name):
if res == DialogResult.CONFIRM:
safe_name = name.replace(" ", "_") if name else f"toggle_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
backup_path = Path(f"/data/toggle_backups/{safe_name}")
if backup_path.exists():
gui_app.set_modal_overlay(alert_dialog(tr("A toggle backup with this name already exists.")))
return
os.makedirs(backup_path, exist_ok=True)
shutil.copytree("/data/params/d", str(backup_path), dirs_exist_ok=True)
gui_app.set_modal_overlay(alert_dialog(tr("Toggle backup created.")))
self._rebuild_grid()
self._keyboard.reset(min_text_size=0)
self._keyboard.set_title(tr("Name your toggle backup"), "")
self._keyboard.set_text("")
self._keyboard.set_callback(lambda result: on_name(result, self._keyboard.text))
gui_app.push_widget(self._keyboard)
def _on_restore(self):
backups = self._get_backups()
if not backups:
gui_app.set_modal_overlay(alert_dialog(tr("No toggle backups found.")))
return
dialog = MultiOptionDialog(tr("Select Toggle Backup"), backups)
def _on_select(res):
if res == DialogResult.CONFIRM and dialog.selection:
def on_confirm(r2):
if r2 == DialogResult.CONFIRM:
src = Path(f"/data/toggle_backups/{dialog.selection}")
params_dir = Path("/data/params/d")
for old_key, new_key in LEGACY_STARPILOT_PARAM_RENAMES.items():
if (src / old_key).exists():
(params_dir / new_key).unlink(missing_ok=True)
shutil.copytree(str(src), "/data/params/d", dirs_exist_ok=True)
for old_key, new_key in LEGACY_STARPILOT_PARAM_RENAMES.items():
old_path = params_dir / old_key
new_path = params_dir / new_key
if old_path.exists():
old_path.replace(new_path)
gui_app.set_modal_overlay(alert_dialog(tr("Toggles restored.")))
self._rebuild_grid()
gui_app.set_modal_overlay(ConfirmDialog(tr("This will overwrite your current toggles."), tr("Restore"), on_close=on_confirm))
gui_app.set_modal_overlay(dialog, callback=_on_select)
def _on_delete(self):
backups = self._get_backups()
if not backups:
gui_app.set_modal_overlay(alert_dialog(tr("No toggle backups found.")))
return
dialog = MultiOptionDialog(tr("Delete Toggle Backup"), backups)
def _on_select(res):
if res == DialogResult.CONFIRM and dialog.selection:
shutil.rmtree(f"/data/toggle_backups/{dialog.selection}", ignore_errors=True)
self._rebuild_grid()
gui_app.set_modal_overlay(dialog, callback=_on_select)
@@ -1,292 +0,0 @@
from __future__ import annotations
from pathlib import Path
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import AetherSliderDialog, TileGrid
class StarPilotDeviceLayout(StarPilotPanel):
def __init__(self):
super().__init__()
self.CATEGORIES = [
{"title": tr_noop("Screen Settings"), "panel": "screen", "icon": "toggle_icons/icon_light.png", "color": "#D43D8A"},
{"title": tr_noop("Device Settings"), "panel": "device_settings", "icon": "toggle_icons/icon_device.png", "color": "#D43D8A"},
{
"title": tr_noop("Device Shutdown"),
"type": "value",
"get_value": self._get_shutdown_timer,
"on_click": self._show_shutdown_selector,
"color": "#D43D8A",
},
{
"title": tr_noop("Disable Logging"),
"type": "toggle",
"get_state": lambda: self._params.get_bool("NoLogging"),
"set_state": lambda s: self._params.put_bool("NoLogging", s),
"color": "#D43D8A",
},
{
"title": tr_noop("Disable Uploads"),
"type": "toggle",
"get_state": lambda: self._params.get_bool("NoUploads"),
"set_state": lambda s: self._params.put_bool("NoUploads", s),
"color": "#D43D8A",
},
{
"title": tr_noop("Disable Onroad Uploads"),
"type": "toggle",
"param": "DisableOnroadUploads",
"get_state": lambda: self._params.get_bool("DisableOnroadUploads"),
"set_state": lambda s: self._params.put_bool("DisableOnroadUploads", s),
"color": "#D43D8A",
},
{
"title": tr_noop("High-Quality Recording"),
"type": "toggle",
"param": "HigherBitrate",
"get_state": lambda: self._params.get_bool("HigherBitrate"),
"set_state": lambda s: self._on_higher_bitrate_toggle(s),
"color": "#D43D8A",
},
]
self._sub_panels = {
"screen": StarPilotScreenLayout(),
"device_settings": StarPilotDeviceManagementLayout(),
}
self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True)
for name, panel in self._sub_panels.items():
if hasattr(panel, 'set_navigate_callback'):
panel.set_navigate_callback(self._navigate_to)
if hasattr(panel, 'set_back_callback'):
panel.set_back_callback(self._go_back)
self._rebuild_grid()
def _rebuild_grid(self):
no_uploads = self._params.get_bool("NoUploads")
disable_onroad = self._params.get_bool("DisableOnroadUploads")
filtered = []
for cat in self.CATEGORIES:
param = cat.get("param")
if param == "DisableOnroadUploads" and not no_uploads:
continue
if param == "HigherBitrate" and (not no_uploads or disable_onroad):
continue
filtered.append(cat)
original = self.CATEGORIES
self.CATEGORIES = filtered
super()._rebuild_grid()
self.CATEGORIES = original
def _on_higher_bitrate_toggle(self, state):
self._params.put_bool("HigherBitrate", state)
cache_path = Path("/cache/use_HD")
if state:
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.touch()
else:
if cache_path.exists():
cache_path.unlink()
if ui_state.started:
gui_app.set_modal_overlay(
ConfirmDialog(
tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None
)
)
def _get_shutdown_timer(self):
v = self._params.get_int("DeviceShutdown")
if v == 0:
return tr("5 mins")
if v <= 3:
return f"{v * 15} mins"
return f"{v - 3} " + (tr("hour") if v == 4 else tr("hours"))
def _show_shutdown_selector(self):
def on_close(res, val):
if res == DialogResult.CONFIRM:
self._params.put_int("DeviceShutdown", int(val))
self._rebuild_grid()
labels = {0: tr("5 mins")}
for i in range(1, 4):
labels[i] = f"{i * 15} mins"
for i in range(4, 34):
labels[i] = f"{i - 3} " + (tr("hour") if i == 4 else tr("hours"))
gui_app.set_modal_overlay(AetherSliderDialog(tr("Device Shutdown"), 0, 33, 1, self._params.get_int("DeviceShutdown"), on_close, labels=labels, color="#D43D8A"))
class StarPilotScreenLayout(StarPilotPanel):
def __init__(self):
super().__init__()
self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True)
self.CATEGORIES = [
{
"title": tr_noop("Screen Settings"),
"type": "toggle",
"get_state": lambda: self._params.get_bool("ScreenManagement"),
"set_state": lambda s: self._params.put_bool("ScreenManagement", s),
"icon": "toggle_icons/icon_light.png",
"color": "#D43D8A",
},
{
"title": tr_noop("Brightness (Offroad)"),
"type": "value",
"get_value": lambda: self._get_brightness("ScreenBrightness"),
"on_click": lambda: self._show_brightness_selector("ScreenBrightness"),
"color": "#D43D8A",
"visible": lambda: self._params.get_bool("ScreenManagement"),
},
{
"title": tr_noop("Brightness (Onroad)"),
"type": "value",
"get_value": lambda: self._get_brightness("ScreenBrightnessOnroad"),
"on_click": lambda: self._show_brightness_selector("ScreenBrightnessOnroad"),
"color": "#D43D8A",
"visible": lambda: self._params.get_bool("ScreenManagement"),
},
{
"title": tr_noop("Timeout (Offroad)"),
"type": "value",
"get_value": lambda: f"{self._params.get_int('ScreenTimeout')}s",
"on_click": lambda: self._show_timeout_selector("ScreenTimeout"),
"color": "#D43D8A",
"visible": lambda: self._params.get_bool("ScreenManagement"),
},
{
"title": tr_noop("Timeout (Onroad)"),
"type": "value",
"get_value": lambda: f"{self._params.get_int('ScreenTimeoutOnroad')}s",
"on_click": lambda: self._show_timeout_selector("ScreenTimeoutOnroad"),
"color": "#D43D8A",
"visible": lambda: self._params.get_bool("ScreenManagement"),
},
{
"title": tr_noop("Standby Mode"),
"type": "toggle",
"get_state": lambda: self._params.get_bool("StandbyMode"),
"set_state": lambda s: self._params.put_bool("StandbyMode", s),
"color": "#D43D8A",
"visible": lambda: self._params.get_bool("ScreenManagement"),
},
]
self._rebuild_grid()
def _get_brightness(self, key):
v = self._params.get_int(key)
if key == "ScreenBrightnessOnroad" and v == 0:
v = 1
if v == 0:
return tr("Off")
if v == 101:
return tr("Auto")
return f"{v}%"
def _show_brightness_selector(self, key):
def on_close(res, val):
if res == DialogResult.CONFIRM:
new_v = int(val)
if key == "ScreenBrightnessOnroad":
new_v = max(new_v, 1)
self._params.put_int(key, new_v)
HARDWARE.set_brightness(new_v)
self._rebuild_grid()
min_value = 1 if key == "ScreenBrightnessOnroad" else 0
current_value = max(self._params.get_int(key), min_value)
labels = {101: tr("Auto")}
if min_value == 0:
labels[0] = tr("Off")
gui_app.set_modal_overlay(
AetherSliderDialog(tr(key), min_value, 101, 1, current_value, on_close, unit="%", labels=labels, color="#D43D8A")
)
def _show_timeout_selector(self, key):
def on_close(res, val):
if res == DialogResult.CONFIRM:
self._params.put_int(key, int(val))
self._rebuild_grid()
gui_app.set_modal_overlay(AetherSliderDialog(tr(key), 5, 60, 5, self._params.get_int(key), on_close, unit="s", color="#D43D8A"))
class StarPilotDeviceManagementLayout(StarPilotPanel):
def __init__(self):
super().__init__()
self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True)
self.CATEGORIES = [
{
"title": tr_noop("Device Settings"),
"type": "toggle",
"get_state": lambda: self._params.get_bool("DeviceManagement"),
"set_state": lambda s: self._params.put_bool("DeviceManagement", s),
"icon": "toggle_icons/icon_device.png",
"color": "#D43D8A",
},
{
"title": tr_noop("Low-Voltage Cutoff"),
"type": "value",
"get_value": lambda: f"{self._params.get_float('LowVoltageShutdown'):.1f}V",
"on_click": self._show_voltage_selector,
"color": "#D43D8A",
"visible": lambda: self._params.get_bool("DeviceManagement"),
},
{
"title": tr_noop("Raise Temp Limits"),
"type": "toggle",
"get_state": lambda: self._params.get_bool("IncreaseThermalLimits"),
"set_state": lambda s: self._params.put_bool("IncreaseThermalLimits", s),
"color": "#D43D8A",
"visible": lambda: self._params.get_bool("DeviceManagement"),
},
{
"title": tr_noop("Use Konik Server"),
"type": "toggle",
"get_state": lambda: self._get_konik_state(),
"set_state": lambda s: self._on_konik_toggle(s),
"color": "#D43D8A",
"visible": lambda: self._params.get_bool("DeviceManagement"),
},
]
self._rebuild_grid()
def _get_konik_state(self):
if Path("/data/not_vetted").exists():
return True
return self._params.get_bool("UseKonikServer")
def _on_konik_toggle(self, state):
self._params.put_bool("UseKonikServer", state)
cache_path = Path("/cache/use_konik")
if state:
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.touch()
else:
if cache_path.exists():
cache_path.unlink()
if ui_state.started:
gui_app.set_modal_overlay(
ConfirmDialog(
tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None
)
)
def _show_voltage_selector(self):
def on_close(res, val):
if res == DialogResult.CONFIRM:
self._params.put_float("LowVoltageShutdown", float(val))
self._rebuild_grid()
gui_app.set_modal_overlay(
AetherSliderDialog(tr("Low-Voltage Cutoff"), 11.8, 12.5, 0.1, self._params.get_float("LowVoltageShutdown"), on_close, unit="V", color="#D43D8A")
)
@@ -105,14 +105,14 @@ class StarPilotAdvancedLateralLayout(StarPilotPanel):
if res == DialogResult.CONFIRM:
self._params.put_float(key, float(val))
self._rebuild_grid()
gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, step, self._params.get_float(key), on_close, unit=unit, color="#597497"))
gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, step, self._params.get_float(key), on_close, unit=unit, color="#597497"))
def _on_reboot_toggle(self, key, state):
self._params.put_bool(key, state)
from openpilot.selfdrive.ui.ui_state import ui_state
if ui_state.started:
dialog = ConfirmDialog(tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None)
gui_app.set_modal_overlay(dialog)
gui_app.push_widget(dialog)
class StarPilotAlwaysOnLateralLayout(StarPilotPanel):
def __init__(self):
@@ -129,13 +129,13 @@ class StarPilotAlwaysOnLateralLayout(StarPilotPanel):
if res == DialogResult.CONFIRM:
self._params.put_int(key, int(val))
self._rebuild_grid()
gui_app.set_modal_overlay(AetherSliderDialog(tr(key), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#597497"))
gui_app.push_widget(AetherSliderDialog(tr(key), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#597497"))
def _on_reboot_toggle(self, key, state):
self._params.put_bool(key, state)
from openpilot.selfdrive.ui.ui_state import ui_state
if ui_state.started:
gui_app.set_modal_overlay(ConfirmDialog(tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None))
gui_app.push_widget(ConfirmDialog(tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None))
class StarPilotLaneChangesLayout(StarPilotPanel):
def __init__(self):
@@ -155,14 +155,14 @@ class StarPilotLaneChangesLayout(StarPilotPanel):
if res == DialogResult.CONFIRM:
self._params.put_int(key, int(val))
self._rebuild_grid()
gui_app.set_modal_overlay(AetherSliderDialog(tr(key), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#597497"))
gui_app.push_widget(AetherSliderDialog(tr(key), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#597497"))
def _show_float_selector(self, key, min_v, max_v, step, unit=""):
def on_close(res, val):
if res == DialogResult.CONFIRM:
self._params.put_float(key, float(val))
self._rebuild_grid()
gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, step, self._params.get_float(key), on_close, unit=unit, color="#597497"))
gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, step, self._params.get_float(key), on_close, unit=unit, color="#597497"))
class StarPilotLateralTuneLayout(StarPilotPanel):
def __init__(self):
@@ -192,7 +192,7 @@ class StarPilotLateralTuneLayout(StarPilotPanel):
self._params.put_bool(key, state)
from openpilot.selfdrive.ui.ui_state import ui_state
if ui_state.started:
gui_app.set_modal_overlay(ConfirmDialog(tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None))
gui_app.push_widget(ConfirmDialog(tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None))
class StarPilotLateralQOLLayout(StarPilotPanel):
def __init__(self):
@@ -208,7 +208,7 @@ class StarPilotLateralQOLLayout(StarPilotPanel):
if res == DialogResult.CONFIRM:
self._params.put_int(key, int(val))
self._rebuild_grid()
gui_app.set_modal_overlay(AetherSliderDialog(tr(key), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#597497"))
gui_app.push_widget(AetherSliderDialog(tr(key), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#597497"))
class StarPilotLateralLayout(StarPilotPanel):
def __init__(self):
@@ -240,7 +240,7 @@ class StarPilotAdvancedLongitudinalLayout(StarPilotPanel):
self._params.put_float(key, float(val))
self._rebuild_grid()
gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, step, self._params.get_float(key), on_close, unit=unit, color="#597497"))
gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, step, self._params.get_float(key), on_close, unit=unit, color="#597497"))
class StarPilotConditionalExperimentalLayout(StarPilotPanel):
@@ -372,7 +372,7 @@ class StarPilotConditionalExperimentalLayout(StarPilotPanel):
self._params.put_int(key, int(val))
self._rebuild_grid()
gui_app.set_modal_overlay(AetherSliderDialog(tr(key), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#597497"))
gui_app.push_widget(AetherSliderDialog(tr(key), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#597497"))
def _show_int_selector(self, key, min_v, max_v, unit=""):
def on_close(res, val):
@@ -380,7 +380,7 @@ class StarPilotConditionalExperimentalLayout(StarPilotPanel):
self._params.put_int(key, int(val))
self._rebuild_grid()
gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#597497"))
gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#597497"))
class StarPilotCurveSpeedLayout(StarPilotPanel):
@@ -441,7 +441,7 @@ class StarPilotCurveSpeedLayout(StarPilotPanel):
self._params.remove("CurvatureData")
self._rebuild_grid()
gui_app.set_modal_overlay(ConfirmDialog(tr("Reset Curve Data?"), tr("Confirm"), on_close=on_close))
gui_app.push_widget(ConfirmDialog(tr("Reset Curve Data?"), tr("Confirm"), on_close=on_close))
class StarPilotPersonalitiesLayout(StarPilotPanel):
@@ -537,7 +537,7 @@ class StarPilotPersonalityProfileLayout(StarPilotPanel):
self._params.remove(self._profile + key)
self._rebuild_grid()
gui_app.set_modal_overlay(ConfirmDialog(tr("Reset to Defaults?"), tr("Confirm"), on_close=on_close))
gui_app.push_widget(ConfirmDialog(tr("Reset to Defaults?"), tr("Confirm"), on_close=on_close))
def _show_float_selector(self, key, min_v, max_v, step, unit=""):
def on_close(res, val):
@@ -545,7 +545,7 @@ class StarPilotPersonalityProfileLayout(StarPilotPanel):
self._params.put_float(key, float(val))
self._rebuild_grid()
gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, step, self._params.get_float(key), on_close, unit=unit, color="#597497"))
gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, step, self._params.get_float(key), on_close, unit=unit, color="#597497"))
def _show_int_selector(self, key, min_v, max_v, unit=""):
def on_close(res, val):
@@ -553,7 +553,7 @@ class StarPilotPersonalityProfileLayout(StarPilotPanel):
self._params.put_int(key, int(val))
self._rebuild_grid()
gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, 5, self._params.get_int(key), on_close, unit=unit, color="#597497"))
gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, 5, self._params.get_int(key), on_close, unit=unit, color="#597497"))
class StarPilotLongitudinalTuneLayout(StarPilotPanel):
@@ -669,7 +669,7 @@ class StarPilotLongitudinalTuneLayout(StarPilotPanel):
self._params.put_int(key, label_to_value[dialog.selection])
self._rebuild_grid()
gui_app.set_modal_overlay(dialog, callback=on_select)
gui_app.push_widget(dialog, callback=on_select)
def _show_int_selector(self, key, min_v, max_v, unit=""):
def on_close(res, val):
@@ -677,7 +677,7 @@ class StarPilotLongitudinalTuneLayout(StarPilotPanel):
self._params.put_int(key, int(val))
self._rebuild_grid()
gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#597497"))
gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#597497"))
class StarPilotLongitudinalQOLLayout(StarPilotPanel):
@@ -782,7 +782,7 @@ class StarPilotLongitudinalQOLLayout(StarPilotPanel):
self._rebuild_grid()
current = max(1, self._params.get_int(key))
gui_app.set_modal_overlay(AetherSliderDialog(tr(key), 1, 100, 1, current, on_close, unit=" mph", color="#597497"))
gui_app.push_widget(AetherSliderDialog(tr(key), 1, 100, 1, current, on_close, unit=" mph", color="#597497"))
def _show_int_selector(self, key, min_v, max_v, unit=""):
def on_close(res, val):
@@ -790,7 +790,7 @@ class StarPilotLongitudinalQOLLayout(StarPilotPanel):
self._params.put_int(key, int(val))
self._rebuild_grid()
gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#597497"))
gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#597497"))
class StarPilotSpeedLimitControllerLayout(StarPilotPanel):
@@ -867,7 +867,7 @@ class StarPilotSpeedLimitControllerLayout(StarPilotPanel):
secondary_options = ["None"] + [option for option in ("Dashboard", "Map Data", "Vision") if option != primary]
selected_secondary = current_secondary if current_secondary in secondary_options else "None"
secondary_dialog = MultiOptionDialog(tr("SLC Secondary Priority"), secondary_options, selected_secondary)
gui_app.set_modal_overlay(secondary_dialog, callback=lambda res: on_secondary_select(primary, secondary_dialog, res))
gui_app.push_widget(secondary_dialog, callback=lambda res: on_secondary_select(primary, secondary_dialog, res))
primary_dialog = MultiOptionDialog(tr("SLC Primary Priority"), primary_options, current_primary)
@@ -881,7 +881,7 @@ class StarPilotSpeedLimitControllerLayout(StarPilotPanel):
return
show_secondary_dialog(primary_dialog.selection)
gui_app.set_modal_overlay(primary_dialog, callback=on_primary_select)
gui_app.push_widget(primary_dialog, callback=on_primary_select)
def _show_selection(self, key, options):
current = self._params.get(key, encoding='utf-8') or "None"
@@ -892,7 +892,7 @@ class StarPilotSpeedLimitControllerLayout(StarPilotPanel):
self._params.put(key, dialog.selection)
self._rebuild_grid()
gui_app.set_modal_overlay(dialog, callback=on_select)
gui_app.push_widget(dialog, callback=on_select)
class StarPilotSLCOffsetsLayout(StarPilotPanel):
@@ -928,7 +928,7 @@ class StarPilotSLCOffsetsLayout(StarPilotPanel):
self._rebuild_grid()
min_value, max_value = self._speed_range()
gui_app.set_modal_overlay(
gui_app.push_widget(
AetherSliderDialog(tr(key), min_value, max_value, 1, self._params.get_int(key), on_close, unit=self._speed_unit(), color="#597497")
)
@@ -997,7 +997,7 @@ class StarPilotSLCQOLLayout(StarPilotPanel):
self._params.put_int(key, int(val))
self._rebuild_grid()
gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#597497"))
gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#597497"))
class StarPilotSLCVisualsLayout(StarPilotPanel):
@@ -1065,9 +1065,9 @@ class StarPilotWeatherLayout(StarPilotPanel):
self._params.remove("WeatherAPIKey")
self._rebuild_grid()
gui_app.set_modal_overlay(ConfirmDialog(tr("Remove API Key?"), tr("Confirm"), on_close=on_confirm))
gui_app.push_widget(ConfirmDialog(tr("Remove API Key?"), tr("Confirm"), on_close=on_confirm))
gui_app.set_modal_overlay(dialog, callback=on_select)
gui_app.push_widget(dialog, callback=on_select)
class StarPilotWeatherBase(StarPilotPanel):
@@ -1117,4 +1117,4 @@ class StarPilotWeatherBase(StarPilotPanel):
self._rebuild_grid()
curr = self._params.get_int(key)
gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, step, curr, on_close, unit=unit, color="#597497"))
gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, step, curr, on_close, unit=unit, color="#597497"))
@@ -14,10 +14,7 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.longitudinal import StarP
from openpilot.selfdrive.ui.layouts.settings.starpilot.lateral import StarPilotLateralLayout
from openpilot.selfdrive.ui.layouts.settings.starpilot.maps import StarPilotMapsLayout
from openpilot.selfdrive.ui.layouts.settings.starpilot.navigation import StarPilotNavigationLayout
from openpilot.selfdrive.ui.layouts.settings.starpilot.data import StarPilotDataLayout
from openpilot.selfdrive.ui.layouts.settings.starpilot.device import StarPilotDeviceLayout
from openpilot.selfdrive.ui.layouts.settings.starpilot.system_settings import StarPilotSystemLayout
from openpilot.selfdrive.ui.layouts.settings.starpilot.utilities import StarPilotUtilitiesLayout
from openpilot.selfdrive.ui.layouts.settings.starpilot.visuals import StarPilotVisualsLayout
from openpilot.selfdrive.ui.layouts.settings.starpilot.themes import StarPilotThemesLayout
from openpilot.selfdrive.ui.layouts.settings.starpilot.vehicle import StarPilotVehicleSettingsLayout
@@ -94,9 +91,6 @@ class StarPilotLayout(Widget):
StarPilotPanelType.LATERAL: StarPilotPanelInfo(tr_noop("Steering"), StarPilotLateralLayout()),
StarPilotPanelType.MAPS: StarPilotPanelInfo(tr_noop("Map Data"), StarPilotMapsLayout()),
StarPilotPanelType.NAVIGATION: StarPilotPanelInfo(tr_noop("Navigation"), StarPilotNavigationLayout()),
StarPilotPanelType.DATA: StarPilotPanelInfo(tr_noop("Data Management"), StarPilotDataLayout()),
StarPilotPanelType.DEVICE: StarPilotPanelInfo(tr_noop("Device Controls"), StarPilotDeviceLayout()),
StarPilotPanelType.UTILITIES: StarPilotPanelInfo(tr_noop("Utilities"), StarPilotUtilitiesLayout()),
StarPilotPanelType.VISUALS: StarPilotPanelInfo(tr_noop("Appearance"), StarPilotVisualsLayout()),
StarPilotPanelType.THEMES: StarPilotPanelInfo(tr_noop("Themes"), StarPilotThemesLayout()),
StarPilotPanelType.VEHICLE: StarPilotPanelInfo(tr_noop("Vehicle Settings"), StarPilotVehicleSettingsLayout()),
@@ -201,9 +195,6 @@ class StarPilotLayout(Widget):
"LATERAL": StarPilotPanelType.LATERAL,
"MAPS": StarPilotPanelType.MAPS,
"NAVIGATION": StarPilotPanelType.NAVIGATION,
"DATA": StarPilotPanelType.DATA,
"DEVICE": StarPilotPanelType.DEVICE,
"UTILITIES": StarPilotPanelType.UTILITIES,
"VISUALS": StarPilotPanelType.VISUALS,
"THEMES": StarPilotPanelType.THEMES,
"VEHICLE": StarPilotPanelType.VEHICLE,
@@ -124,7 +124,7 @@ class StarPilotMapsLayout(StarPilotPanel):
self._params.put("PreferredSchedule", schedule_param_value(dialog.selection))
self._rebuild_grid()
gui_app.set_modal_overlay(dialog, callback=on_select)
gui_app.push_widget(dialog, callback=on_select)
def _on_download(self):
current_selected = self._params.get("MapsSelected", encoding="utf-8") or ""
@@ -133,15 +133,15 @@ class StarPilotMapsLayout(StarPilotPanel):
self._params.put("MapsSelected", selected_raw)
selected = [k.strip() for k in selected_raw.split(",") if k.strip()]
if not selected:
gui_app.set_modal_overlay(alert_dialog(tr("Please select at least one region or state first!")))
gui_app.push_widget(alert_dialog(tr("Please select at least one region or state first!")))
return
def on_confirm(res):
if res == DialogResult.CONFIRM:
self._params_memory.put_bool("DownloadMaps", True)
gui_app.set_modal_overlay(alert_dialog(tr("Map download started in background.")))
gui_app.push_widget(alert_dialog(tr("Map download started in background.")))
gui_app.set_modal_overlay(ConfirmDialog(tr("Start downloading maps for selected regions?"), tr("Download"), on_close=on_confirm))
gui_app.push_widget(ConfirmDialog(tr("Start downloading maps for selected regions?"), tr("Download"), on_close=on_confirm))
def _on_remove(self):
def on_confirm(res):
@@ -149,7 +149,7 @@ class StarPilotMapsLayout(StarPilotPanel):
maps_path = Path("/data/media/0/osm/offline")
if maps_path.exists():
shutil.rmtree(maps_path, ignore_errors=True)
gui_app.set_modal_overlay(alert_dialog(tr("Maps removed.")))
gui_app.push_widget(alert_dialog(tr("Maps removed.")))
self._rebuild_grid()
gui_app.set_modal_overlay(ConfirmDialog(tr("Delete all downloaded map data?"), tr("Remove"), on_close=on_confirm))
gui_app.push_widget(ConfirmDialog(tr("Delete all downloaded map data?"), tr("Remove"), on_close=on_confirm))
@@ -51,7 +51,7 @@ class StarPilotNavigationLayout(StarPilotPanel):
self._rebuild_grid()
def _on_setup(self):
gui_app.set_modal_overlay(
gui_app.push_widget(
alert_dialog(tr("Mapbox Setup:\n1. Create account at mapbox.com\n2. Generate Public/Secret keys\n3. Add keys in 'Mapbox Credentials'"))
)
@@ -134,7 +134,7 @@ class StarPilotMapboxLayout(StarPilotPanel):
self._params.remove(key)
self._rebuild_grid()
gui_app.set_modal_overlay(ConfirmDialog(tr(f"Remove your {key.replace('Mapbox', '')} key?"), tr("Remove"), on_close=on_remove))
gui_app.push_widget(ConfirmDialog(tr(f"Remove your {key.replace('Mapbox', '')} key?"), tr("Remove"), on_close=on_remove))
else:
def on_close(res, text):
+237 -156
View File
@@ -1,4 +1,5 @@
from __future__ import annotations
import math
import subprocess
import time
from pathlib import Path
@@ -7,14 +8,213 @@ import pyray as rl
from openpilot.common.basedir import BASEDIR
from openpilot.starpilot.common.starpilot_variables import ACTIVE_THEME_PATH
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.application import gui_app, FontWeight, MouseEvent, MousePos
from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.lib.text_measure import measure_text_cached
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.label import gui_label
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
from openpilot.selfdrive.ui.layouts.settings.starpilot.tabbed_panel import TabSectionSpec, TabbedSectionHost
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, ToggleTile, SPACING
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import (
AETHER_LIST_METRICS,
AetherListColors,
build_list_panel_frame,
draw_list_panel_shell,
AetherContinuousSlider,
draw_toggle_pill,
)
MODEL_PANEL_BG = AetherListColors.PANEL_BG
MODEL_HEADER_TEXT = AetherListColors.HEADER
MODEL_SUBTEXT = AetherListColors.SUBTEXT
MODEL_MUTED = AetherListColors.MUTED
SECTION_GAP = AETHER_LIST_METRICS.section_gap
class SoundsManagerView(Widget):
def __init__(self, controller: "StarPilotSoundsLayout"):
super().__init__()
self._controller = controller
self._pressed_target: str | None = None
self._sliders: dict[str, AetherContinuousSlider] = {}
self._slider_was_dragging: dict[str, bool] = {}
self._toggle_rects: dict[str, rl.Rectangle] = {}
self._font = gui_app.font(FontWeight.BOLD)
self._init_sliders()
def _init_sliders(self):
for key in self._controller.VOLUME_KEYS:
val = self._controller._params.get_int(key, return_default=True, default=100)
def on_change(v, k=key):
new_v = int(v)
if new_v != 101 and new_v < self._controller.VOLUME_INFO[k]["min"]:
new_v = self._controller.VOLUME_INFO[k]["min"]
self._controller._params.put_int(k, new_v)
slider = AetherContinuousSlider(
min_val=0.0,
max_val=101.0,
step=1.0,
current_val=float(val),
on_change=on_change,
title=tr(self._controller.VOLUME_INFO[key]["title"]),
unit="%",
labels={0.0: tr("Muted"), 101.0: tr("Auto")},
color=AetherListColors.PRIMARY
)
self._sliders[key] = slider
self._slider_was_dragging[key] = False
cd_val = self._controller._params.get_int(self._controller.COOLDOWN_KEY, return_default=True, default=0)
def on_cd_change(v):
self._controller._params.put_int(self._controller.COOLDOWN_KEY, int(v))
cd_slider = AetherContinuousSlider(
min_val=0.0,
max_val=float(self._controller.COOLDOWN_INFO["max"]),
step=1.0,
current_val=float(cd_val),
on_change=on_cd_change,
title=tr(self._controller.COOLDOWN_INFO["title"]),
unit=" " + tr("min"),
labels={0.0: tr("Off"), 1.0: tr("1 minute")},
color=AetherListColors.PRIMARY
)
self._sliders[self._controller.COOLDOWN_KEY] = cd_slider
self._slider_was_dragging[self._controller.COOLDOWN_KEY] = False
def _handle_mouse_press(self, mouse_pos: MousePos):
self._pressed_target = self._target_at(mouse_pos)
for slider in self._sliders.values():
slider._handle_mouse_press(mouse_pos)
def _handle_mouse_release(self, mouse_pos: MousePos):
for slider in self._sliders.values():
slider._handle_mouse_release(mouse_pos)
target = self._target_at(mouse_pos)
if self._pressed_target is not None and self._pressed_target == target:
self._activate_target(target)
self._pressed_target = None
def _handle_mouse_event(self, mouse_event: MouseEvent):
for slider in self._sliders.values():
slider._handle_mouse_event(mouse_event)
def _target_at(self, mouse_pos: MousePos) -> str | None:
for key, rect in self._toggle_rects.items():
if rl.check_collision_point_rec(mouse_pos, rect):
return f"toggle:{key}"
return None
def _activate_target(self, target: str):
if target.startswith("toggle:"):
key = target.split(":", 1)[1]
info = self._controller.ALERT_INFO.get(key)
if info and info.get("is_enabled", lambda: True)():
current = self._controller._params.get_bool(key)
self._controller._params.put_bool(key, not current)
def _render(self, rect: rl.Rectangle):
self.set_rect(rect)
self._toggle_rects.clear()
frame = build_list_panel_frame(rect)
draw_list_panel_shell(frame)
header_rect = frame.header
self._draw_header(header_rect)
# Reclaim the dead space! The global header allocates 210px, but our text only uses ~100px.
metrics = AETHER_LIST_METRICS
actual_header_height = 100
content_y = header_rect.y + actual_header_height
content_h = (frame.shell.y + frame.shell.height) - content_y - metrics.panel_padding_bottom
content_rect = rl.Rectangle(
frame.scroll.x,
content_y,
frame.scroll.width,
content_h
)
col_width = (content_rect.width - SECTION_GAP) / 2
left_col = rl.Rectangle(content_rect.x, content_rect.y, col_width, content_rect.height)
right_col = rl.Rectangle(content_rect.x + col_width + SECTION_GAP, content_rect.y, col_width, content_rect.height)
self._draw_volume_section(left_col)
self._draw_utility_section(right_col)
for key, slider in self._sliders.items():
is_dragging = slider._is_dragging
if self._slider_was_dragging[key] and not is_dragging:
if key in self._controller.VOLUME_KEYS:
self._controller._test_sound(key)
self._slider_was_dragging[key] = is_dragging
def _draw_header(self, rect: rl.Rectangle):
title_rect = rl.Rectangle(rect.x, rect.y + 4, rect.width * 0.55, 40)
gui_label(title_rect, tr("Sounds & Alerts"), 40, MODEL_HEADER_TEXT, FontWeight.SEMI_BOLD)
subtitle_rect = rl.Rectangle(rect.x, rect.y + 48, rect.width * 0.58, 36)
gui_label(subtitle_rect, tr("Manage system volumes and custom alert toggles."), 24, MODEL_SUBTEXT, FontWeight.NORMAL)
def _draw_volume_section(self, rect: rl.Rectangle):
num_volumes = len(self._controller.VOLUME_KEYS)
vol_row_h = rect.height / num_volumes
for index, key in enumerate(self._controller.VOLUME_KEYS):
row_rect = rl.Rectangle(rect.x, rect.y + index * vol_row_h, rect.width, vol_row_h)
self._draw_slider_row(row_rect, key, self._controller.VOLUME_INFO[key])
def _draw_utility_section(self, rect: rl.Rectangle):
total_elements = 7 # 1 cooldown + 6 alerts
row_h = rect.height / total_elements
# Cooldown Slider (Index 0)
cd_row_rect = rl.Rectangle(rect.x, rect.y, rect.width, row_h)
self._draw_slider_row(cd_row_rect, self._controller.COOLDOWN_KEY, self._controller.COOLDOWN_INFO)
# Custom Alert Toggle Pills (Indices 1 to 6)
for index, key in enumerate(self._controller.CUSTOM_ALERTS_KEYS):
row_rect = rl.Rectangle(rect.x, rect.y + (index + 1) * row_h, rect.width, row_h)
self._draw_toggle_row(row_rect, key, self._controller.ALERT_INFO[key])
def _draw_slider_row(self, rect: rl.Rectangle, key: str, info: dict):
slider = self._sliders[key]
padded_rect = rl.Rectangle(rect.x, rect.y + 4, rect.width - 12, rect.height - 8)
if not slider._is_dragging:
current_param = self._controller._params.get_int(key, return_default=True, default=100 if key != self._controller.COOLDOWN_KEY else 0)
if slider.current_val != current_param:
slider.current_val = float(current_param)
slider.render(padded_rect)
def _draw_toggle_row(self, rect: rl.Rectangle, key: str, info: dict):
padded_rect = rl.Rectangle(rect.x, rect.y + 4, rect.width - 12, rect.height - 8)
current_val = self._controller._params.get_bool(key)
is_enabled = info.get("is_enabled", lambda: True)()
mouse_pos = gui_app.last_mouse_event.pos
hovered = rl.check_collision_point_rec(mouse_pos, padded_rect)
pressed = self._pressed_target == f"toggle:{key}"
status_str = tr("ON") if current_val else tr("OFF")
if not is_enabled: status_str = tr(info.get("disabled_label", "UNAVAILABLE"))
draw_toggle_pill(padded_rect, current_val, is_enabled, tr(info["title"]), status_str, hovered, pressed)
self._toggle_rects[key] = padded_rect
class StarPilotSoundsLayout(StarPilotPanel):
COOLDOWN_KEY = "SwitchbackModeCooldown"
@@ -37,42 +237,16 @@ class StarPilotSoundsLayout(StarPilotPanel):
"SpeedLimitChangedAlert",
]
def __init__(self):
super().__init__()
self._section_tabs = TabbedSectionHost([
TabSectionSpec("volume_control", "Volumes", StarPilotVolumeControlLayout()),
TabSectionSpec("custom_alerts", "Alerts", StarPilotCustomAlertsLayout()),
])
def set_navigate_callback(self, callback):
self._section_tabs.set_navigate_callback(callback)
def set_back_callback(self, callback):
self._section_tabs.set_back_callback(callback)
def _render(self, rect):
self._section_tabs.render(rect)
def set_current_sub_panel(self, sub_panel: str):
self._section_tabs.set_current_sub_panel(sub_panel)
def show_event(self):
self._section_tabs.show_event()
def hide_event(self):
self._section_tabs.hide_event()
class StarPilotVolumeControlLayout(StarPilotPanel):
COOLDOWN_INFO = {"title": tr_noop("Switchback Mode Cooldown"), "icon": "toggle_icons/icon_mute.png", "min": 0, "max": 30}
COOLDOWN_INFO = {"title": tr_noop("Switchback Mode Cooldown"), "min": 0, "max": 30}
VOLUME_INFO = {
"BelowSteerSpeedVolume": {"title": tr_noop("Min Steer Speed Alert"), "icon": "toggle_icons/icon_mute.png", "min": 0},
"DisengageVolume": {"title": tr_noop("Disengage Volume"), "icon": "toggle_icons/icon_mute.png", "min": 0},
"EngageVolume": {"title": tr_noop("Engage Volume"), "icon": "toggle_icons/icon_green_light.png", "min": 0},
"PromptVolume": {"title": tr_noop("Prompt Volume"), "icon": "toggle_icons/icon_message.png", "min": 0},
"PromptDistractedVolume": {"title": tr_noop("Distracted Volume"), "icon": "toggle_icons/icon_display.png", "min": 0},
"RefuseVolume": {"title": tr_noop("Refuse Volume"), "icon": "toggle_icons/icon_mute.png", "min": 0},
"WarningSoftVolume": {"title": tr_noop("Warning Soft"), "icon": "toggle_icons/icon_conditional.png", "min": 25},
"WarningImmediateVolume": {"title": tr_noop("Warning Immediate"), "icon": "toggle_icons/icon_conditional.png", "min": 25},
"BelowSteerSpeedVolume": {"title": tr_noop("Min Steer Speed Alert"), "min": 0},
"DisengageVolume": {"title": tr_noop("Disengage Volume"), "min": 0},
"EngageVolume": {"title": tr_noop("Engage Volume"), "min": 0},
"PromptVolume": {"title": tr_noop("Prompt Volume"), "min": 0},
"PromptDistractedVolume": {"title": tr_noop("Distracted Volume"), "min": 0},
"RefuseVolume": {"title": tr_noop("Refuse Volume"), "min": 0},
"WarningSoftVolume": {"title": tr_noop("Warning Soft"), "min": 25},
"WarningImmediateVolume": {"title": tr_noop("Warning Immediate"), "min": 25},
}
_sound_player_process = None
@@ -81,71 +255,35 @@ class StarPilotVolumeControlLayout(StarPilotPanel):
super().__init__()
self._init_sound_player()
self.SECTIONS = [
{
"title": tr_noop("Volume Levels"),
"categories": self._build_volume_categories(),
self.ALERT_INFO = {
"GoatScream": {"title": tr_noop("Goat Scream")},
"GoatScreamCriticalAlerts": {"title": tr_noop("Goat Critical")},
"GreenLightAlert": {"title": tr_noop("Green Light")},
"LeadDepartingAlert": {"title": tr_noop("Lead Departure")},
"LoudBlindspotAlert": {
"title": tr_noop("Loud Blindspot"),
"is_enabled": lambda: starpilot_state.car_state.hasBSM,
"disabled_label": tr_noop("Needs BSM")
},
{
"title": tr_noop("Safety & Cooldown"),
"categories": self._build_safety_categories(),
}
]
self._rebuild_grid()
"SpeedLimitChangedAlert": {
"title": tr_noop("Speed Limit"),
"is_enabled": lambda: self._params.get_bool("ShowSpeedLimits") or (
starpilot_state.car_state.hasOpenpilotLongitudinal and self._params.get_bool("SpeedLimitController")
),
"disabled_label": tr_noop("Needs Speed Limits")
},
}
def _build_volume_categories(self):
cats = []
for key in StarPilotSoundsLayout.VOLUME_KEYS:
info = self.VOLUME_INFO[key]
self._manager_view = SoundsManagerView(self)
def get_val(k=key):
return float(self._params.get_int(k, return_default=True, default=100))
def _render(self, rect: rl.Rectangle):
self._manager_view.render(rect)
def set_val(val, k=key):
new_v = int(val)
if new_v != 101 and new_v < self.VOLUME_INFO[k]["min"]:
new_v = self.VOLUME_INFO[k]["min"]
self._params.put_int(k, new_v)
def show_event(self):
super().show_event()
def test_cb(k=key):
self._test_sound(k)
cats.append({
"title": info["title"],
"type": "slider",
"get_value": get_val,
"set_value": set_val,
"on_test": test_cb,
"min_val": 0,
"max_val": 101,
"step": 1,
"unit": "%",
"labels": {0: tr("Muted"), 101: tr("Auto")},
"icon": info["icon"],
"color": "#3B82F6",
})
return cats
def _build_safety_categories(self):
def get_cooldown_val():
return float(self._params.get_int(StarPilotSoundsLayout.COOLDOWN_KEY, return_default=True, default=0))
def set_cooldown_val(val):
self._params.put_int(StarPilotSoundsLayout.COOLDOWN_KEY, int(val))
return [{
"title": self.COOLDOWN_INFO["title"],
"type": "slider",
"get_value": get_cooldown_val,
"set_value": set_cooldown_val,
"min_val": 0,
"max_val": float(self.COOLDOWN_INFO["max"]),
"step": 1,
"unit": " " + tr("min"),
"labels": {0: tr("Off"), 1: tr("1 minute")},
"icon": self.COOLDOWN_INFO["icon"],
"color": "#3B82F6",
}]
def hide_event(self):
super().hide_event()
@classmethod
def _init_sound_player(cls):
@@ -194,60 +332,3 @@ while True:
self._sound_player_process.stdin.write(f"{sound_path}|{volume}\n".encode())
self._sound_player_process.stdin.flush()
except: pass
class StarPilotCustomAlertsLayout(StarPilotPanel):
ALERT_INFO = {
"GoatScream": {"title": tr_noop("Goat Scream"), "icon": "toggle_icons/icon_sound.png"},
"GoatScreamCriticalAlerts": {"title": tr_noop("Goat Critical"), "icon": "toggle_icons/icon_sound.png"},
"GreenLightAlert": {"title": tr_noop("Green Light"), "icon": "toggle_icons/icon_green_light.png"},
"LeadDepartingAlert": {"title": tr_noop("Lead Departure"), "icon": "toggle_icons/icon_steering.png"},
"LoudBlindspotAlert": {"title": tr_noop("Loud Blindspot"), "icon": "toggle_icons/icon_display.png"},
"SpeedLimitChangedAlert": {"title": tr_noop("Speed Limit"), "icon": "toggle_icons/icon_speed_limit.png"},
}
def __init__(self):
super().__init__()
self._tile_grid = TileGrid(columns=2, padding=SPACING.tile_gap, uniform_width=True)
self.CATEGORIES = []
for key in StarPilotSoundsLayout.CUSTOM_ALERTS_KEYS:
info = self.ALERT_INFO[key]
self.CATEGORIES.append({
"title": info["title"],
"type": "toggle",
"get_state": lambda k=key: self._params.get_bool(k),
"set_state": lambda s, k=key: self._params.put_bool(k, s),
"icon": info["icon"],
"color": "#3B82F6",
"key": key # Store for visibility check
})
self._rebuild_grid()
def _rebuild_grid(self):
if not self.CATEGORIES:
return
self._tile_grid.clear()
for cat in self.CATEGORIES:
key = cat.get("key")
is_enabled = lambda: True
disabled_label = ""
if key == "LoudBlindspotAlert":
is_enabled = lambda: starpilot_state.car_state.hasBSM
disabled_label = tr_noop("Needs BSM")
elif key == "SpeedLimitChangedAlert":
is_enabled = lambda: self._params.get_bool("ShowSpeedLimits") or (
starpilot_state.car_state.hasOpenpilotLongitudinal and self._params.get_bool("SpeedLimitController")
)
disabled_label = tr_noop("Needs Speed Limits")
tile = ToggleTile(
title=tr(cat["title"]),
get_state=cat["get_state"],
set_state=cat["set_state"],
icon_path=cat.get("icon"),
bg_color=cat.get("color"),
is_enabled=is_enabled,
disabled_label=tr(disabled_label) if disabled_label else "",
)
self._tile_grid.add_tile(tile)
@@ -1,92 +1,700 @@
from __future__ import annotations
import json
import os
import shutil
import subprocess
import threading
import time
from datetime import datetime
from pathlib import Path
from dataclasses import replace
import pyray as rl
from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.application import gui_app, FontWeight, MouseEvent, MousePos
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2
from openpilot.system.ui.widgets import DialogResult, Widget
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog
from openpilot.system.ui.widgets.keyboard import Keyboard
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from openpilot.system.ui.widgets.label import gui_label
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import RadioTileGroup
from openpilot.selfdrive.ui.layouts.settings.starpilot.data import StarPilotDataLayout
from openpilot.selfdrive.ui.layouts.settings.starpilot.device import StarPilotDeviceLayout
from openpilot.selfdrive.ui.layouts.settings.starpilot.utilities import StarPilotUtilitiesLayout
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import (
AETHER_LIST_METRICS,
AetherListColors,
AetherScrollbar,
AetherContinuousSlider,
build_list_panel_frame,
draw_list_panel_shell,
draw_list_scroll_fades,
draw_toggle_pill,
)
LEGACY_STARPILOT_PARAM_RENAMES = {
"FrogPilotApiToken": "StarPilotApiToken",
"FrogPilotCarParams": "StarPilotCarParams",
"FrogPilotCarParamsPersistent": "StarPilotCarParamsPersistent",
"FrogPilotDongleId": "StarPilotDongleId",
"FrogPilotStats": "StarPilotStats",
}
EXCLUDED_KEYS = {
"AvailableModels",
"AvailableModelNames",
"StarPilotStats",
"GithubSshKeys",
"GithubUsername",
"MapBoxRequests",
"ModelDrivesAndScores",
"OverpassRequests",
"SpeedLimits",
"SpeedLimitsFiltered",
"UpdaterAvailableBranches",
}
REPORT_CATEGORIES = [
"Acceleration feels harsh or jerky",
"An alert was unclear and I'm not sure what it meant",
"Braking is too sudden or uncomfortable",
"I'm not sure if this is normal or a bug:",
"My steering wheel buttons aren't working",
"openpilot disengages when I don't expect it",
"openpilot feels sluggish or slow to respond",
"Something else (please describe)",
]
class SystemSettingsManagerView(Widget):
def __init__(self, controller: "StarPilotSystemLayout"):
super().__init__()
self._controller = controller
self._scroll_panel = GuiScrollPanel2(horizontal=False)
self._scrollbar = AetherScrollbar()
self._content_height = 0.0
self._scroll_offset = 0.0
self._pressed_target: str | None = None
self._can_click = True
self._action_rects: dict[str, rl.Rectangle] = {}
self._toggle_rects: dict[str, rl.Rectangle] = {}
self._shell_rect = rl.Rectangle(0, 0, 0, 0)
self._scroll_rect = rl.Rectangle(0, 0, 0, 0)
shutdown_labels = {0: tr("5 mins")}
for i in range(1, 4): shutdown_labels[i] = f"{i * 15} mins"
for i in range(4, 34): shutdown_labels[i] = f"{i - 3} " + (tr("hour") if i == 4 else tr("hours"))
brightness_labels = {101: tr("Auto"), 0: tr("Off")}
self._sliders = {
"ScreenBrightness": AetherContinuousSlider(0, 101, 1, self._controller._params.get_int("ScreenBrightness"), lambda v: self._controller._set_brightness("ScreenBrightness", v), title=tr("Brightness (Offroad)"), unit="%", labels=brightness_labels, color=AetherListColors.PRIMARY),
"ScreenBrightnessOnroad": AetherContinuousSlider(1, 101, 1, max(1, self._controller._params.get_int("ScreenBrightnessOnroad")), lambda v: self._controller._set_brightness("ScreenBrightnessOnroad", max(1, int(v))), title=tr("Brightness (Onroad)"), unit="%", labels=brightness_labels, color=AetherListColors.PRIMARY),
"ScreenTimeout": AetherContinuousSlider(5, 60, 5, self._controller._params.get_int("ScreenTimeout"), lambda v: self._controller._params.put_int("ScreenTimeout", int(v)), title=tr("Timeout (Offroad)"), unit="s", color=AetherListColors.PRIMARY),
"ScreenTimeoutOnroad": AetherContinuousSlider(5, 60, 5, self._controller._params.get_int("ScreenTimeoutOnroad"), lambda v: self._controller._params.put_int("ScreenTimeoutOnroad", int(v)), title=tr("Timeout (Onroad)"), unit="s", color=AetherListColors.PRIMARY),
"DeviceShutdown": AetherContinuousSlider(0, 33, 1, self._controller._params.get_int("DeviceShutdown"), lambda v: self._controller._params.put_int("DeviceShutdown", int(v)), title=tr("Device Shutdown"), labels=shutdown_labels, color=AetherListColors.PRIMARY),
"LowVoltageShutdown": AetherContinuousSlider(11.8, 12.5, 0.1, self._controller._params.get_float("LowVoltageShutdown"), lambda v: self._controller._params.put_float("LowVoltageShutdown", float(v)), title=tr("Low-Voltage Cutoff"), unit="V", color=AetherListColors.PRIMARY),
}
def _clear_ephemeral_state(self):
self._pressed_target = None
self._can_click = True
def show_event(self):
super().show_event()
self._clear_ephemeral_state()
def hide_event(self):
super().hide_event()
self._clear_ephemeral_state()
def _handle_mouse_press(self, mouse_pos: MousePos):
self._pressed_target = None
self._can_click = True
for action_id, rect in self._action_rects.items():
visible_rect = rl.get_collision_rec(rect, self._scroll_rect)
if visible_rect.width > 0 and visible_rect.height > 0 and rl.check_collision_point_rec(mouse_pos, visible_rect):
self._pressed_target = f"action:{action_id}"
return
for toggle_id, rect in self._toggle_rects.items():
visible_rect = rl.get_collision_rec(rect, self._scroll_rect)
if visible_rect.width > 0 and visible_rect.height > 0 and rl.check_collision_point_rec(mouse_pos, visible_rect):
self._pressed_target = f"toggle:{toggle_id}"
return
for slider in self._sliders.values():
slider._handle_mouse_press(mouse_pos)
def _handle_mouse_event(self, mouse_event: MouseEvent):
if not self._scroll_panel.is_touch_valid():
self._can_click = False
for slider in self._sliders.values():
slider._handle_mouse_event(mouse_event)
def _handle_mouse_release(self, mouse_pos: MousePos):
target = self._pressed_target
self._pressed_target = None
if target and self._can_click:
if target.startswith("action:"):
action_id = target.split(":", 1)[1]
rect = self._action_rects.get(action_id)
elif target.startswith("toggle:"):
toggle_id = target.split(":", 1)[1]
rect = self._toggle_rects.get(toggle_id)
if rect:
visible_rect = rl.get_collision_rec(rect, self._scroll_rect)
if visible_rect.width > 0 and visible_rect.height > 0 and rl.check_collision_point_rec(mouse_pos, visible_rect):
self._activate_target(target)
for slider in self._sliders.values():
slider._handle_mouse_release(mouse_pos)
def _activate_target(self, target: str):
action_id = target.split(":", 1)[1]
self._controller.handle_action(action_id)
def _render(self, rect: rl.Rectangle):
self.set_rect(rect)
self._action_rects.clear()
self._toggle_rects.clear()
metrics = replace(AETHER_LIST_METRICS, header_height=110)
frame = build_list_panel_frame(rect, metrics)
self._shell_rect = frame.shell
draw_list_panel_shell(frame)
header_rect = frame.header
self._draw_header(header_rect)
scroll_rect = frame.scroll
self._scroll_rect = scroll_rect
content_width = scroll_rect.width - AETHER_LIST_METRICS.content_right_gutter
self._content_height = self._measure_content_height()
self._scroll_offset = self._scroll_panel.update(scroll_rect, max(self._content_height, scroll_rect.height))
rl.begin_scissor_mode(int(scroll_rect.x), int(scroll_rect.y), int(scroll_rect.width), int(scroll_rect.height))
self._draw_scroll_content(scroll_rect, content_width)
rl.end_scissor_mode()
if self._content_height > scroll_rect.height:
self._draw_scrollbar(scroll_rect)
draw_list_scroll_fades(scroll_rect, self._content_height, self._scroll_offset, AetherListColors.PANEL_BG, fade_height=AETHER_LIST_METRICS.fade_height)
def _draw_header(self, rect: rl.Rectangle):
title_rect = rl.Rectangle(rect.x, rect.y + 4, rect.width * 0.55, 40)
gui_label(title_rect, tr("System Settings"), 40, AetherListColors.HEADER, FontWeight.SEMI_BOLD)
subtitle_rect = rl.Rectangle(rect.x, rect.y + 48, rect.width * 0.58, 36)
gui_label(subtitle_rect, tr("Manage device behavior, power, and storage seamlessly."), 24, AetherListColors.SUBTEXT, FontWeight.NORMAL)
def _measure_column_height(self, sections: list[dict]) -> float:
total_height = 0
for section in sections:
total_height += AETHER_LIST_METRICS.section_header_height + AETHER_LIST_METRICS.section_header_gap
for row in section["rows"]:
if row["type"] == "slider":
total_height += 100 + 16
elif row["type"] in ["toggle", "toggle_row"]:
total_height += 90 + 16
elif row["type"] == "action_group":
total_height += 110 + 16
total_height += AETHER_LIST_METRICS.section_gap
return max(total_height - AETHER_LIST_METRICS.section_gap, 0.0)
def _measure_content_height(self) -> float:
cols = self._controller.utility_columns()
return max(self._measure_column_height(cols["left"]), self._measure_column_height(cols["right"]), 0.0)
def _draw_scroll_content(self, rect: rl.Rectangle, width: float):
cols = self._controller.utility_columns()
col_w = (width - AETHER_LIST_METRICS.section_gap) / 2
left_x = rect.x
right_x = rect.x + col_w + AETHER_LIST_METRICS.section_gap
self._draw_column(rl.Rectangle(left_x, rect.y + self._scroll_offset, col_w, rect.height), cols["left"])
self._draw_column(rl.Rectangle(right_x, rect.y + self._scroll_offset, col_w, rect.height), cols["right"])
def _draw_column(self, rect: rl.Rectangle, sections: list[dict]):
y = rect.y
mouse_pos = gui_app.last_mouse_event.pos
for section in sections:
title_rect = rl.Rectangle(rect.x, y, rect.width, AETHER_LIST_METRICS.section_header_height)
gui_label(title_rect, section["title"], 26, AetherListColors.SUBTEXT, FontWeight.MEDIUM)
y += AETHER_LIST_METRICS.section_header_height + AETHER_LIST_METRICS.section_header_gap
for row in section["rows"]:
if row["type"] == "slider":
slider = self._sliders[row["id"]]
slider.render(rl.Rectangle(rect.x, y, rect.width, 100))
y += 100 + 16
elif row["type"] == "toggle_row":
items = row["items"]
item_w = (rect.width - 16 * (len(items) - 1)) / len(items)
for i, item in enumerate(items):
enabled = item.get("enabled", True)
toggle_rect = rl.Rectangle(rect.x + i * (item_w + 16), y, item_w, 90)
if enabled:
self._toggle_rects[item["id"]] = toggle_rect
hovered = rl.check_collision_point_rec(mouse_pos, toggle_rect)
pressed = self._pressed_target == f"toggle:{item['id']}"
draw_toggle_pill(toggle_rect, item["value"], enabled, item["title"], tr("ON") if item["value"] else tr("OFF"), hovered, pressed)
y += 90 + 16
elif row["type"] == "toggle":
enabled = row.get("enabled", True)
toggle_rect = rl.Rectangle(rect.x, y, rect.width, 90)
if enabled:
self._toggle_rects[row["id"]] = toggle_rect
hovered = rl.check_collision_point_rec(mouse_pos, toggle_rect)
pressed = self._pressed_target == f"toggle:{row['id']}"
draw_toggle_pill(toggle_rect, row["value"], enabled, row["title"], tr("ON") if row["value"] else tr("OFF"), hovered, pressed)
y += 90 + 16
elif row["type"] == "action_group":
group_rect = rl.Rectangle(rect.x, y, rect.width, 110)
self._draw_action_group(group_rect, row, mouse_pos)
y += 110 + 16
y += AETHER_LIST_METRICS.section_gap
def _draw_action_group(self, rect: rl.Rectangle, row: dict, mouse_pos: MousePos):
rl.draw_rectangle_rounded(rect, 0.3, 16, rl.Color(35, 35, 40, 255))
title_y = rect.y + (rect.height - 24) / 2
gui_label(rl.Rectangle(rect.x + 24, title_y, rect.width * 0.4, 24), row["title"], 24, rl.WHITE, FontWeight.BOLD)
actions = row["actions"]
btn_gap = 12
available_w = rect.width * 0.6 - 40
btn_w = (available_w - (len(actions) - 1) * btn_gap) / len(actions)
start_x = rect.x + rect.width - available_w - 16
for i, action in enumerate(actions):
btn_rect = rl.Rectangle(start_x + i * (btn_w + btn_gap), rect.y + 12, btn_w, rect.height - 24)
self._action_rects[action["id"]] = btn_rect
hovered = rl.check_collision_point_rec(mouse_pos, btn_rect)
pressed = self._pressed_target == f"action:{action['id']}"
active = action.get("active", False)
color = AetherListColors.PRIMARY if active else rl.Color(60, 60, 65, 255)
if action.get("danger"):
color = AetherListColors.DANGER
if hovered: color = rl.Color(min(color.r + 20, 255), min(color.g + 20, 255), min(color.b + 20, 255), 255)
if pressed: color = rl.Color(max(color.r - 20, 0), max(color.g - 20, 0), max(color.b - 20, 0), 255)
rl.draw_rectangle_rounded(btn_rect, 0.4, 16, color)
gui_label(btn_rect, action["label"], 24, rl.WHITE, FontWeight.BOLD, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER)
def _draw_scrollbar(self, rect: rl.Rectangle):
self._scrollbar.render(rect, self._content_height, self._scroll_offset)
class StarPilotSystemLayout(StarPilotPanel):
def __init__(self):
super().__init__()
self._section_names = ["device", "data_and_backups", "utilities"]
self._active_section = self._section_names[0]
self._sub_panels = {
"device": StarPilotDeviceLayout(),
"data_and_backups": StarPilotDataLayout(),
"utilities": StarPilotUtilitiesLayout(),
}
self._section_tabs = RadioTileGroup(
"",
[tr("Device"), tr("Data"), tr("Utilities")],
0,
self._on_section_change,
)
for name, panel in self._sub_panels.items():
if hasattr(panel, 'set_navigate_callback'):
panel.set_navigate_callback(lambda sub_panel, section_name=name: self._navigate_to_child(section_name, sub_panel))
if hasattr(panel, 'set_back_callback'):
panel.set_back_callback(self._go_back)
def _on_section_change(self, index: int):
if 0 <= index < len(self._section_names):
previous_panel = self._sub_panels[self._active_section]
if hasattr(previous_panel, 'set_current_sub_panel'):
previous_panel.set_current_sub_panel("")
self._current_sub_panel = ""
self._set_active_section(self._section_names[index], "")
if self._navigate_callback:
self._navigate_callback("")
def _set_active_section(self, section_name: str, child_panel: str = ""):
if section_name not in self._sub_panels:
return
if section_name != self._active_section:
self._sub_panels[self._active_section].hide_event()
self._active_section = section_name
self._sub_panels[self._active_section].show_event()
self._section_tabs.set_index(self._section_names.index(section_name))
panel = self._sub_panels[section_name]
if hasattr(panel, 'set_current_sub_panel'):
panel.set_current_sub_panel(child_panel)
def _navigate_to_child(self, section_name: str, child_panel: str):
self._set_active_section(section_name, child_panel)
if self._navigate_callback:
self._navigate_callback(f"{section_name}:{child_panel}")
def set_current_sub_panel(self, sub_panel: str):
super().set_current_sub_panel(sub_panel)
if not sub_panel:
self._set_active_section(self._active_section, "")
return
if ":" in sub_panel:
section_name, child_panel = sub_panel.split(":", 1)
self._set_active_section(section_name, child_panel)
elif sub_panel in self._section_names:
self._set_active_section(sub_panel)
def _render(self, rect):
tab_rect = rl.Rectangle(rect.x, rect.y, rect.width, 110)
panel_rect = rl.Rectangle(rect.x, rect.y + 140, rect.width, rect.height - 140)
self._section_tabs.render(tab_rect)
self._sub_panels[self._active_section].render(panel_rect)
self._keyboard = Keyboard(min_text_size=0)
self._manager_view = SystemSettingsManagerView(self)
def show_event(self):
super().show_event()
self._sub_panels[self._active_section].show_event()
self._manager_view.show_event()
def hide_event(self):
super().hide_event()
self._sub_panels[self._active_section].hide_event()
self._manager_view.hide_event()
def _render(self, rect: rl.Rectangle):
self._manager_view.render(rect)
def utility_columns(self) -> dict[str, list[dict]]:
state = self._get_force_drive_state()
no_uploads = self._params.get_bool("NoUploads")
disable_onroad = self._params.get_bool("DisableOnroadUploads")
screen_management = self._params.get_bool("ScreenManagement")
screen_rows = [
{"id": "ScreenManagement", "type": "toggle", "title": tr("Screen Management"), "value": screen_management},
{"id": "StandbyMode", "type": "toggle", "title": tr("Standby Mode"), "value": self._params.get_bool("StandbyMode"), "enabled": screen_management},
]
if screen_management:
screen_rows.extend([
{"id": "ScreenBrightness", "type": "slider"},
{"id": "ScreenBrightnessOnroad", "type": "slider"},
{"id": "ScreenTimeout", "type": "slider"},
{"id": "ScreenTimeoutOnroad", "type": "slider"},
])
device_management = self._params.get_bool("DeviceManagement")
device_rows = [
{"id": "DeviceManagement", "type": "toggle", "title": tr("Device Management"), "value": device_management},
{"id": "IncreaseThermalLimits", "type": "toggle", "title": tr("Raise Thermal Limits"), "value": self._params.get_bool("IncreaseThermalLimits"), "enabled": device_management},
]
if device_management:
device_rows.extend([
{"id": "DeviceShutdown", "type": "slider"},
{"id": "LowVoltageShutdown", "type": "slider"},
])
network_rows = [
{"type": "toggle_row", "items": [
{"id": "NoUploads", "title": tr("Disable Uploads"), "value": no_uploads},
{"id": "UseKonikServer", "title": tr("Use Konik Server"), "value": self._get_konik_state()}
]},
{"type": "toggle_row", "items": [
{"id": "DisableOnroadUploads", "title": tr("Disable Onroad Uploads"), "value": disable_onroad, "enabled": not no_uploads},
{"id": "NoLogging", "title": tr("Disable Logging"), "value": self._params.get_bool("NoLogging")}
]},
{"id": "HigherBitrate", "type": "toggle", "title": tr("High-Quality Recording"), "value": self._params.get_bool("HigherBitrate"), "enabled": not disable_onroad and not no_uploads}
]
data_rows = [
{"id": "Storage", "type": "action_group", "title": tr("Storage & Logs"), "actions": [
{"id": "Storage", "label": f"{tr('Clear Data')} ({self._get_storage()})", "danger": True},
{"id": "ErrorLogs", "label": tr("Clear Logs"), "danger": True}
]},
{"id": "SystemBackups", "type": "action_group", "title": tr("System Backups"), "actions": [
{"id": "CreateBackup", "label": tr("Create")},
{"id": "RestoreBackup", "label": tr("Restore")},
{"id": "DeleteBackup", "label": tr("Delete"), "danger": True}
]},
{"id": "ToggleBackups", "type": "action_group", "title": tr("Toggle Backups"), "actions": [
{"id": "CreateToggleBackup", "label": tr("Create")},
{"id": "RestoreToggleBackup", "label": tr("Restore")},
{"id": "DeleteToggleBackup", "label": tr("Delete"), "danger": True}
]},
]
util_rows = [
{"type": "toggle_row", "items": [{"id": "DebugMode", "type": "toggle", "title": tr("Debug Mode"), "value": self._params.get_bool("DebugMode")}]},
{"id": "ForceDriveState", "type": "action_group", "title": tr("Force Drive State"), "actions": [
{"id": "DriveDefault", "label": tr("Auto"), "active": state == tr("Default")},
{"id": "DriveOnroad", "label": tr("Onroad"), "active": state == tr("Onroad")},
{"id": "DriveOffroad", "label": tr("Offroad"), "active": state == tr("Offroad")}
]},
{"id": "QuickActions", "type": "action_group", "title": tr("Quick Actions"), "actions": [
{"id": "FlashPanda", "label": tr("Flash Panda")},
{"id": "ReportIssue", "label": tr("Report Issue")}
]},
{"id": "FactoryReset", "type": "action_group", "title": tr("Factory Reset"), "actions": [
{"id": "ResetDefaults", "label": tr("Toggles"), "danger": True},
{"id": "ResetStock", "label": tr("Stock OP"), "danger": True}
]},
]
return {
"left": [
{"title": tr("Display Configuration"), "rows": screen_rows},
{"title": tr("Developer & Maintenance"), "rows": util_rows},
],
"right": [
{"title": tr("Power & Thermals"), "rows": device_rows},
{"title": tr("Networking & Data"), "rows": network_rows},
{"title": tr("Data & Backups"), "rows": data_rows},
]
}
def handle_action(self, action_id: str):
if action_id == "ScreenManagement":
self._params.put_bool("ScreenManagement", not self._params.get_bool("ScreenManagement"))
elif action_id == "StandbyMode":
self._params.put_bool("StandbyMode", not self._params.get_bool("StandbyMode"))
elif action_id == "DeviceManagement":
self._params.put_bool("DeviceManagement", not self._params.get_bool("DeviceManagement"))
elif action_id == "IncreaseThermalLimits":
self._params.put_bool("IncreaseThermalLimits", not self._params.get_bool("IncreaseThermalLimits"))
elif action_id == "UseKonikServer":
self._on_konik_toggle(not self._get_konik_state())
elif action_id == "NoLogging":
self._params.put_bool("NoLogging", not self._params.get_bool("NoLogging"))
elif action_id == "NoUploads":
self._params.put_bool("NoUploads", not self._params.get_bool("NoUploads"))
elif action_id == "DisableOnroadUploads":
self._params.put_bool("DisableOnroadUploads", not self._params.get_bool("DisableOnroadUploads"))
elif action_id == "HigherBitrate":
self._on_higher_bitrate_toggle(not self._params.get_bool("HigherBitrate"))
elif action_id == "Storage":
self._on_delete_driving_data()
elif action_id == "ErrorLogs":
self._on_delete_error_logs()
elif action_id == "CreateBackup":
self._on_create_backup()
elif action_id == "RestoreBackup":
self._on_restore_backup()
elif action_id == "DeleteBackup":
self._on_delete_backup()
elif action_id == "CreateToggleBackup":
self._on_create_toggle_backup()
elif action_id == "RestoreToggleBackup":
self._on_restore_toggle_backup()
elif action_id == "DeleteToggleBackup":
self._on_delete_toggle_backup()
elif action_id == "DebugMode":
self._params.put_bool("DebugMode", not self._params.get_bool("DebugMode"))
elif action_id == "DriveDefault":
self._params.put_bool("ForceOffroad", False)
self._params.put_bool("ForceOnroad", False)
elif action_id == "DriveOnroad":
self._params.put_bool("ForceOnroad", True)
self._params.put_bool("ForceOffroad", False)
elif action_id == "DriveOffroad":
self._params.put_bool("ForceOffroad", True)
self._params.put_bool("ForceOnroad", False)
elif action_id == "FlashPanda":
self._on_flash_panda()
elif action_id == "ReportIssue":
self._on_report_issue()
elif action_id == "ResetDefaults":
self._on_reset_defaults()
elif action_id == "ResetStock":
self._on_reset_stock()
def _set_brightness(self, key, val):
self._params.put_int(key, int(val))
if key == "ScreenBrightnessOnroad" or key == "ScreenBrightness":
HARDWARE.set_brightness(int(val))
def _get_konik_state(self):
if Path("/data/not_vetted").exists():
return True
return self._params.get_bool("UseKonikServer")
def _on_konik_toggle(self, state):
self._params.put_bool("UseKonikServer", state)
cache_path = Path("/cache/use_konik")
if state:
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.touch()
else:
if cache_path.exists():
cache_path.unlink()
if ui_state.started:
gui_app.push_widget(
ConfirmDialog(
tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None
)
)
def _on_higher_bitrate_toggle(self, state):
self._params.put_bool("HigherBitrate", state)
cache_path = Path("/cache/use_HD")
if state:
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.touch()
else:
if cache_path.exists():
cache_path.unlink()
if ui_state.started:
gui_app.push_widget(
ConfirmDialog(
tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None
)
)
def _get_storage(self):
paths = ["/data/media/0/osm/offline", "/data/media/0/realdata", "/data/backups"]
total = 0
for p in paths:
pp = Path(p)
if pp.exists():
total += sum(f.stat().st_size for f in pp.rglob('*') if f.is_file())
mb = total / (1024 * 1024)
if mb > 1024:
return f"{(mb / 1024):.2f} GB"
return f"{mb:.2f} MB"
def _on_delete_driving_data(self):
def _do_delete(res):
if res == DialogResult.CONFIRM:
def _task():
drive_paths = ["/data/media/0/realdata/", "/data/media/0/realdata_HD/", "/data/media/0/realdata_konik/"]
for path in drive_paths:
p = Path(path)
if p.exists():
for entry in p.iterdir():
if entry.is_dir():
shutil.rmtree(entry, ignore_errors=True)
threading.Thread(target=_task, daemon=True).start()
gui_app.push_widget(alert_dialog(tr("Driving data deletion started.")))
gui_app.push_widget(ConfirmDialog(tr("Delete all driving data and footage?"), tr("Delete"), on_close=_do_delete))
def _on_delete_error_logs(self):
def _do_delete(res):
if res == DialogResult.CONFIRM:
shutil.rmtree("/data/error_logs", ignore_errors=True)
os.makedirs("/data/error_logs", exist_ok=True)
gui_app.push_widget(alert_dialog(tr("Error logs deleted.")))
gui_app.push_widget(ConfirmDialog(tr("Delete all error logs?"), tr("Delete"), on_close=_do_delete))
def _get_backups(self, folder="backups"):
b_dir = Path(f"/data/{folder}")
if not b_dir.exists():
return []
if folder == "backups":
return [f.name for f in b_dir.glob("*.tar.zst") if "in_progress" not in f.name]
return [d.name for d in b_dir.iterdir() if d.is_dir() and "in_progress" not in d.name]
def _on_create_backup(self):
def on_name(res, name):
if res == DialogResult.CONFIRM:
safe_name = name.replace(" ", "_") if name else f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
backup_path = f"/data/backups/{safe_name}.tar.zst"
if Path(backup_path).exists():
gui_app.push_widget(alert_dialog(tr("A backup with this name already exists.")))
return
gui_app.push_widget(alert_dialog(tr("Backup creation started.")))
def _task():
os.makedirs("/data/backups", exist_ok=True)
subprocess.run(["tar", "--use-compress-program=zstd", "-cf", backup_path, "/data/openpilot"])
threading.Thread(target=_task, daemon=True).start()
self._keyboard.reset(min_text_size=0)
self._keyboard.set_title(tr("Name your backup"), "")
self._keyboard.set_text("")
self._keyboard.set_callback(lambda result: on_name(result, self._keyboard.text))
gui_app.push_widget(self._keyboard)
def _on_restore_backup(self):
backups = self._get_backups("backups")
if not backups:
gui_app.push_widget(alert_dialog(tr("No backups found.")))
return
dialog = MultiOptionDialog(tr("Select Backup"), backups)
def _on_select(res):
if res == DialogResult.CONFIRM and dialog.selection:
gui_app.push_widget(alert_dialog(tr("Restoring... device will reboot.")))
def _task():
subprocess.run(["rm", "-rf", "/data/openpilot/*"])
subprocess.run(["tar", "--use-compress-program=zstd", "-xf", f"/data/backups/{dialog.selection}", "-C", "/"])
os.system("reboot")
threading.Thread(target=_task, daemon=True).start()
gui_app.push_widget(dialog, callback=_on_select)
def _on_delete_backup(self):
backups = self._get_backups("backups")
if not backups:
gui_app.push_widget(alert_dialog(tr("No backups found.")))
return
dialog = MultiOptionDialog(tr("Delete Backup"), backups)
def _on_select(res):
if res == DialogResult.CONFIRM and dialog.selection:
os.remove(f"/data/backups/{dialog.selection}")
gui_app.push_widget(dialog, callback=_on_select)
def _on_create_toggle_backup(self):
def on_name(res, name):
if res == DialogResult.CONFIRM:
safe_name = name.replace(" ", "_") if name else f"toggle_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
backup_path = Path(f"/data/toggle_backups/{safe_name}")
if backup_path.exists():
gui_app.push_widget(alert_dialog(tr("A toggle backup with this name already exists.")))
return
os.makedirs(backup_path, exist_ok=True)
shutil.copytree("/data/params/d", str(backup_path), dirs_exist_ok=True)
gui_app.push_widget(alert_dialog(tr("Toggle backup created.")))
self._keyboard.reset(min_text_size=0)
self._keyboard.set_title(tr("Name your toggle backup"), "")
self._keyboard.set_text("")
self._keyboard.set_callback(lambda result: on_name(result, self._keyboard.text))
gui_app.push_widget(self._keyboard)
def _on_restore_toggle_backup(self):
backups = self._get_backups("toggle_backups")
if not backups:
gui_app.push_widget(alert_dialog(tr("No toggle backups found.")))
return
dialog = MultiOptionDialog(tr("Select Toggle Backup"), backups)
def _on_select(res):
if res == DialogResult.CONFIRM and dialog.selection:
def on_confirm(r2):
if r2 == DialogResult.CONFIRM:
src = Path(f"/data/toggle_backups/{dialog.selection}")
params_dir = Path("/data/params/d")
for old_key, new_key in LEGACY_STARPILOT_PARAM_RENAMES.items():
if (src / old_key).exists():
(params_dir / new_key).unlink(missing_ok=True)
shutil.copytree(str(src), "/data/params/d", dirs_exist_ok=True)
for old_key, new_key in LEGACY_STARPILOT_PARAM_RENAMES.items():
old_path = params_dir / old_key
new_path = params_dir / new_key
if old_path.exists():
old_path.replace(new_path)
gui_app.push_widget(alert_dialog(tr("Toggles restored.")))
gui_app.push_widget(ConfirmDialog(tr("This will overwrite your current toggles."), tr("Restore"), on_close=on_confirm))
gui_app.push_widget(dialog, callback=_on_select)
def _on_delete_toggle_backup(self):
backups = self._get_backups("toggle_backups")
if not backups:
gui_app.push_widget(alert_dialog(tr("No toggle backups found.")))
return
dialog = MultiOptionDialog(tr("Delete Toggle Backup"), backups)
def _on_select(res):
if res == DialogResult.CONFIRM and dialog.selection:
shutil.rmtree(f"/data/toggle_backups/{dialog.selection}", ignore_errors=True)
gui_app.push_widget(dialog, callback=_on_select)
def _get_force_drive_state(self):
if self._params.get_bool("ForceOnroad"):
return tr("Onroad")
if self._params.get_bool("ForceOffroad"):
return tr("Offroad")
return tr("Default")
def _on_flash_panda(self):
def _do_flash(res):
if res == DialogResult.CONFIRM:
self._params_memory.put_bool("FlashPanda", True)
gui_app.push_widget(alert_dialog(tr("Panda flashing started. Device will reboot when finished.")))
gui_app.push_widget(ConfirmDialog(tr("Flash Panda firmware?"), tr("Flash"), callback=_do_flash))
def _on_report_issue(self):
def on_category(res):
if res != DialogResult.CONFIRM or not dialog.selection:
return
discord_user = self._params.get("DiscordUsername", encoding='utf-8') or ""
def on_discord(res2, username):
if res2 == DialogResult.CONFIRM and username:
self._params.put("DiscordUsername", username)
report = json.dumps({"DiscordUser": username, "Issue": dialog.selection})
self._params_memory.put("IssueReported", report)
gui_app.push_widget(alert_dialog(tr("Issue reported. Thank you!")))
self._keyboard.reset(min_text_size=1)
self._keyboard.set_title(tr("Discord Username"), "")
self._keyboard.set_text(discord_user or "")
self._keyboard.set_callback(lambda result: on_discord(result, self._keyboard.text))
gui_app.push_widget(self._keyboard)
dialog = MultiOptionDialog(tr("Select Issue"), REPORT_CATEGORIES, callback=on_category)
gui_app.push_widget(dialog)
def _on_reset_defaults(self):
def _do_reset(res):
if res == DialogResult.CONFIRM:
all_keys = self._params.all_keys()
for k in all_keys:
if k in EXCLUDED_KEYS:
continue
default = self._params.get_default_value(k)
if default is not None:
self._params.put(k, default)
gui_app.push_widget(alert_dialog(tr("Toggles reset to defaults.")))
gui_app.push_widget(ConfirmDialog(tr("Reset all toggles to defaults?"), tr("Reset"), callback=_do_reset))
def _on_reset_stock(self):
def _do_reset(res):
if res == DialogResult.CONFIRM:
all_keys = self._params.all_keys()
for k in all_keys:
if k in EXCLUDED_KEYS:
continue
stock = self._params.get_stock_value(k)
if stock is not None:
self._params.put(k, stock)
gui_app.push_widget(alert_dialog(tr("Toggles reset to stock openpilot.")))
gui_app.push_widget(ConfirmDialog(tr("Reset all toggles to stock openpilot?"), tr("Reset"), callback=_do_reset))
@@ -175,7 +175,7 @@ class StarPilotThemesLayout(StarPilotPanel):
self._params.remove("StartupMessageTop")
self._params.remove("StartupMessageBottom")
gui_app.set_modal_overlay(dialog, callback=on_select)
gui_app.push_widget(dialog, callback=on_select)
class StarPilotPersonalizeLayout(StarPilotPanel):
@@ -329,4 +329,4 @@ class StarPilotPersonalizeLayout(StarPilotPanel):
self._params.put(key, selected_slug)
self._rebuild_grid()
gui_app.set_modal_overlay(dialog, callback=on_select)
gui_app.push_widget(dialog, callback=on_select)
@@ -1,150 +0,0 @@
from __future__ import annotations
import json
from openpilot.system.hardware import HARDWARE
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr, tr_noop
from openpilot.system.ui.widgets import DialogResult
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog
from openpilot.system.ui.widgets.keyboard import Keyboard
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid
EXCLUDED_KEYS = {
"AvailableModels",
"AvailableModelNames",
"StarPilotStats",
"GithubSshKeys",
"GithubUsername",
"MapBoxRequests",
"ModelDrivesAndScores",
"OverpassRequests",
"SpeedLimits",
"SpeedLimitsFiltered",
"UpdaterAvailableBranches",
}
REPORT_CATEGORIES = [
"Acceleration feels harsh or jerky",
"An alert was unclear and I'm not sure what it meant",
"Braking is too sudden or uncomfortable",
"I'm not sure if this is normal or a bug:",
"My steering wheel buttons aren't working",
"openpilot disengages when I don't expect it",
"openpilot feels sluggish or slow to respond",
"Something else (please describe)",
]
class StarPilotUtilitiesLayout(StarPilotPanel):
def __init__(self):
super().__init__()
self._keyboard = Keyboard(min_text_size=1)
self._tile_grid = TileGrid(columns=2, padding=20, uniform_width=True)
self.CATEGORIES = [
{
"title": tr_noop("Debug Mode"),
"type": "toggle",
"get_state": lambda: self._params.get_bool("DebugMode"),
"set_state": lambda s: self._params.put_bool("DebugMode", s),
"color": "#D43D8A",
},
{"title": tr_noop("Flash Panda"), "type": "hub", "on_click": self._on_flash_panda, "color": "#D43D8A"},
{
"title": tr_noop("Force Drive State"),
"type": "value",
"get_value": self._get_force_drive_state,
"on_click": self._on_force_drive_state,
"color": "#D43D8A",
},
{"title": tr_noop("Report Issue"), "type": "hub", "on_click": self._on_report_issue, "color": "#D43D8A"},
{"title": tr_noop("Reset to Defaults"), "type": "hub", "on_click": self._on_reset_defaults, "color": "#D43D8A"},
{"title": tr_noop("Reset to Stock"), "type": "hub", "on_click": self._on_reset_stock, "color": "#D43D8A"},
]
self._rebuild_grid()
def _get_force_drive_state(self):
if self._params.get_bool("ForceOnroad"):
return tr("Onroad")
if self._params.get_bool("ForceOffroad"):
return tr("Offroad")
return tr("Default")
def _on_flash_panda(self):
def _do_flash(res):
if res == DialogResult.CONFIRM:
self._params_memory.put_bool("FlashPanda", True)
gui_app.push_widget(alert_dialog(tr("Panda flashing started. Device will reboot when finished.")))
gui_app.push_widget(ConfirmDialog(tr("Flash Panda firmware?"), tr("Flash"), callback=_do_flash))
def _on_force_drive_state(self):
options = [tr("Offroad"), tr("Onroad"), tr("Default")]
current = self._get_force_drive_state()
def on_select(res):
if res == DialogResult.CONFIRM and dialog.selection:
if dialog.selection == tr("Offroad"):
self._params.put_bool("ForceOffroad", True)
self._params.put_bool("ForceOnroad", False)
elif dialog.selection == tr("Onroad"):
self._params.put_bool("ForceOnroad", True)
self._params.put_bool("ForceOffroad", False)
else:
self._params.put_bool("ForceOffroad", False)
self._params.put_bool("ForceOnroad", False)
self._rebuild_grid()
dialog = MultiOptionDialog(tr("Force Drive State"), options, current, callback=on_select)
gui_app.push_widget(dialog)
def _on_report_issue(self):
def on_category(res):
if res != DialogResult.CONFIRM or not dialog.selection:
return
discord_user = self._params.get("DiscordUsername", encoding='utf-8') or ""
def on_discord(res2, username):
if res2 == DialogResult.CONFIRM and username:
self._params.put("DiscordUsername", username)
report = json.dumps({"DiscordUser": username, "Issue": dialog.selection})
self._params_memory.put("IssueReported", report)
gui_app.push_widget(alert_dialog(tr("Issue reported. Thank you!")))
self._keyboard.reset(min_text_size=1)
self._keyboard.set_title(tr("Discord Username"), "")
self._keyboard.set_text(discord_user or "")
self._keyboard.set_callback(lambda result: on_discord(result, self._keyboard.text))
gui_app.push_widget(self._keyboard)
dialog = MultiOptionDialog(tr("Select Issue"), REPORT_CATEGORIES, callback=on_category)
gui_app.push_widget(dialog)
def _on_reset_defaults(self):
def _do_reset(res):
if res == DialogResult.CONFIRM:
all_keys = self._params.all_keys()
for k in all_keys:
if k in EXCLUDED_KEYS:
continue
default = self._params.get_default_value(k)
if default is not None:
self._params.put(k, default)
gui_app.push_widget(alert_dialog(tr("Toggles reset to defaults.")))
self._rebuild_grid()
gui_app.push_widget(ConfirmDialog(tr("Reset all toggles to defaults?"), tr("Reset"), callback=_do_reset))
def _on_reset_stock(self):
def _do_reset(res):
if res == DialogResult.CONFIRM:
all_keys = self._params.all_keys()
for k in all_keys:
if k in EXCLUDED_KEYS:
continue
stock = self._params.get_stock_value(k)
if stock is not None:
self._params.put(k, stock)
gui_app.push_widget(alert_dialog(tr("Toggles reset to stock openpilot.")))
self._rebuild_grid()
gui_app.push_widget(ConfirmDialog(tr("Reset all toggles to stock openpilot?"), tr("Reset"), callback=_do_reset))
@@ -167,7 +167,7 @@ class StarPilotVehicleSettingsLayout(StarPilotPanel):
def _on_select_make(self):
makes = list(self._make_options)
if not makes:
gui_app.set_modal_overlay(ConfirmDialog(tr("No fingerprint list available."), tr("OK"), on_close=lambda r: None))
gui_app.push_widget(ConfirmDialog(tr("No fingerprint list available."), tr("OK"), on_close=lambda r: None))
return
current_make = self._params.get("CarMake", encoding='utf-8') or ""
@@ -185,17 +185,17 @@ class StarPilotVehicleSettingsLayout(StarPilotPanel):
self._params.remove("CarModelName")
self._rebuild_grid()
gui_app.set_modal_overlay(dialog, callback=on_select)
gui_app.push_widget(dialog, callback=on_select)
def _on_select_model(self):
make = self._params.get("CarMake", encoding='utf-8') or ""
if not make:
gui_app.set_modal_overlay(ConfirmDialog(tr("Please select a Car Make first!"), tr("OK"), on_close=lambda r: None))
gui_app.push_widget(ConfirmDialog(tr("Please select a Car Make first!"), tr("OK"), on_close=lambda r: None))
return
model_options = self._models_by_make.get(make, ())
if not model_options:
gui_app.set_modal_overlay(ConfirmDialog(tr("No models available for this make."), tr("OK"), on_close=lambda r: None))
gui_app.push_widget(ConfirmDialog(tr("No models available for this make."), tr("OK"), on_close=lambda r: None))
return
option_labels = [option.option_label for option in model_options]
@@ -216,7 +216,7 @@ class StarPilotVehicleSettingsLayout(StarPilotPanel):
self._params.put("CarMake", make)
self._rebuild_grid()
gui_app.set_modal_overlay(dialog, callback=on_select)
gui_app.push_widget(dialog, callback=on_select)
def _on_disable_long(self, state):
if state:
@@ -230,7 +230,7 @@ class StarPilotVehicleSettingsLayout(StarPilotPanel):
HARDWARE.reboot()
self._rebuild_grid()
gui_app.set_modal_overlay(ConfirmDialog(tr("Disable openpilot longitudinal control?"), tr("Disable"), on_close=on_confirm))
gui_app.push_widget(ConfirmDialog(tr("Disable openpilot longitudinal control?"), tr("Disable"), on_close=on_confirm))
else:
self._params.put_bool("DisableOpenpilotLongitudinal", False)
self._rebuild_grid()
@@ -447,7 +447,7 @@ class StarPilotToyotaVehicleLayout(StarPilotPanel):
self._params.put_int("LockDoorsTimer", int(val))
self._rebuild_grid()
gui_app.set_modal_overlay(
gui_app.push_widget(
AetherSliderDialog(tr("Lock Doors Timer"), 0, 300, 5, self._params.get_int("LockDoorsTimer"), on_close, labels=_lock_doors_timer_labels(), color="#64748B")
)
@@ -457,7 +457,7 @@ class StarPilotToyotaVehicleLayout(StarPilotPanel):
self._params.put_float("ClusterOffset", float(val))
self._rebuild_grid()
gui_app.set_modal_overlay(
gui_app.push_widget(
AetherSliderDialog(tr("Dashboard Speed Offset"), 1.000, 1.050, 0.001, self._params.get_float("ClusterOffset"), on_close, unit="x", color="#64748B")
)
@@ -439,7 +439,7 @@ class StarPilotModelUILayout(StarPilotPanel):
self._params.put_int(key, int(val))
self._rebuild_grid()
gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#8B5CF6"))
gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#8B5CF6"))
def _show_float_selector(self, key, min_v, max_v, step, unit="", convert=None, unconvert=None):
current = self._params.get_float(key)
@@ -454,7 +454,7 @@ class StarPilotModelUILayout(StarPilotPanel):
self._params.put_float(key, v)
self._rebuild_grid()
gui_app.set_modal_overlay(AetherSliderDialog(tr(key), min_v, max_v, step, current, on_close, unit=unit, color="#8B5CF6"))
gui_app.push_widget(AetherSliderDialog(tr(key), min_v, max_v, step, current, on_close, unit=unit, color="#8B5CF6"))
def _get_color_display(self, key):
val = self._params.get(key, encoding='utf-8') or ""
@@ -476,7 +476,7 @@ class StarPilotModelUILayout(StarPilotPanel):
self._params.put(key, dialog.selection)
self._rebuild_grid()
gui_app.set_modal_overlay(dialog, callback=on_select)
gui_app.push_widget(dialog, callback=on_select)
class StarPilotNavigationVisualsLayout(StarPilotPanel):
@@ -577,4 +577,4 @@ class StarPilotVisualQOLLayout(StarPilotPanel):
self._params.put_int("CameraView", idx)
self._rebuild_grid()
gui_app.set_modal_overlay(dialog, callback=on_select)
gui_app.push_widget(dialog, callback=on_select)
@@ -115,7 +115,7 @@ class StarPilotWheelLayout(StarPilotPanel):
self._params_memory.put_bool("StarPilotTogglesUpdated", True)
self._rebuild_grid()
gui_app.set_modal_overlay(dialog, callback=on_select)
gui_app.push_widget(dialog, callback=on_select)
def _rebuild_grid(self):
if not self.CATEGORIES: