diff --git a/selfdrive/ui/layouts/settings/software.py b/selfdrive/ui/layouts/settings/software.py index b3aad97e..4251f2f5 100644 --- a/selfdrive/ui/layouts/settings/software.py +++ b/selfdrive/ui/layouts/settings/software.py @@ -1,6 +1,7 @@ import os import time import datetime +from pathlib import Path from openpilot.common.time_helpers import system_time_valid from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app @@ -76,15 +77,20 @@ class SoftwareLayout(Widget): self._branch_btn.action_item.set_value(ui_state.params.get("UpdaterTargetBranch") or "") self._branch_dialog: MultiOptionDialog | None = None - self._scroller = Scroller([ - self._onroad_label, - self._version_item, - self._auto_updates_toggle, - self._download_btn, - self._install_btn, - self._branch_btn, - button_item(lambda: tr("Uninstall"), lambda: tr("UNINSTALL"), callback=self._on_uninstall), - ], line_separator=True, spacing=0) + self._scroller = Scroller( + [ + self._onroad_label, + self._version_item, + self._auto_updates_toggle, + self._download_btn, + self._install_btn, + self._branch_btn, + button_item(lambda: tr("Uninstall"), lambda: tr("UNINSTALL"), callback=self._on_uninstall), + button_item(lambda: tr("Error Log"), lambda: tr("VIEW"), callback=self._on_error_log), + ], + line_separator=True, + spacing=0, + ) def show_event(self): self._scroller.show_event() @@ -170,18 +176,43 @@ class SoftwareLayout(Widget): # Start downloading self._waiting_for_updater = True self._waiting_start_ts = time.monotonic() + ui_state.params_memory.put_bool("ManualUpdateInitiated", True) os.system("pkill -SIGHUP -f system.updated.updated") def _on_auto_updates_toggle(self, enabled: bool): ui_state.params.put_bool("AutomaticUpdates", enabled) def _on_uninstall(self): - def handle_uninstall_confirmation(result): + def handle_step1(result): if result == DialogResult.CONFIRM: - ui_state.params.put_bool("DoUninstall", True) + + def handle_step2(result2): + if result2 == DialogResult.CONFIRM: + + def handle_step3(result3): + if result3 == DialogResult.CONFIRM: + ui_state.params.clear_all() + ui_state.params.put_bool("DoUninstall", True) + + dialog = ConfirmDialog(tr("This is a complete factory reset and cannot be undone. Are you absolutely sure?"), tr("Reset")) + gui_app.set_modal_overlay(dialog, callback=handle_step3) + else: + ui_state.params.put_bool("DoUninstall", True) + + dialog = ConfirmDialog( + tr("Do you want to perform a full factory reset? All saved assets and settings will be permanently deleted!"), tr("Factory Reset"), tr("Skip") + ) + gui_app.set_modal_overlay(dialog, callback=handle_step2) dialog = ConfirmDialog(tr("Are you sure you want to uninstall?"), tr("Uninstall")) - gui_app.set_modal_overlay(dialog, callback=handle_uninstall_confirmation) + gui_app.set_modal_overlay(dialog, callback=handle_step1) + + def _on_error_log(self): + try: + txt = Path("/data/error_logs/error.txt").read_text(encoding='utf-8', errors='replace') + except Exception: + txt = tr("No error log found.") + gui_app.set_modal_overlay(ConfirmDialog(txt, tr("OK"), on_close=lambda r: None, rich=True)) def _on_install_update(self): # Trigger reboot to install update diff --git a/selfdrive/ui/layouts/settings/starpilot/data.py b/selfdrive/ui/layouts/settings/starpilot/data.py index 092f32a1..965bcd46 100644 --- a/selfdrive/ui/layouts/settings/starpilot/data.py +++ b/selfdrive/ui/layouts/settings/starpilot/data.py @@ -3,6 +3,7 @@ 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 @@ -10,41 +11,63 @@ 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.selection_dialog import SelectionDialog +from openpilot.system.ui.widgets.input_dialog import InputDialog from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel + class StarPilotDataLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ {"title": tr_noop("Manage Backups"), "panel": "backups", "icon": "toggle_icons/icon_system.png", "color": "#FA6800"}, + {"title": tr_noop("Toggle Backups"), "panel": "toggle_backups", "icon": "toggle_icons/icon_system.png", "color": "#FA6800"}, {"title": tr_noop("Manage Storage"), "panel": "storage", "icon": "toggle_icons/icon_system.png", "color": "#FA6800"}, - {"title": tr_noop("Delete Driving Data"), "type": "hub", "on_click": self._on_delete_driving_data, "icon": "toggle_icons/icon_system.png", "color": "#FA6800"}, - {"title": tr_noop("Delete Error Logs"), "type": "hub", "on_click": self._on_delete_error_logs, "icon": "toggle_icons/icon_system.png", "color": "#FA6800"}, + { + "title": tr_noop("Delete Driving Data"), + "type": "hub", + "on_click": self._on_delete_driving_data, + "icon": "toggle_icons/icon_system.png", + "color": "#FA6800", + }, + { + "title": tr_noop("Delete Error Logs"), + "type": "hub", + "on_click": self._on_delete_error_logs, + "icon": "toggle_icons/icon_system.png", + "color": "#FA6800", + }, ] - + self._sub_panels = { "backups": StarPilotBackupsLayout(), + "toggle_backups": StarPilotToggleBackupsLayout(), "storage": StarPilotStorageLayout(), } - + 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) - + 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) + 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): @@ -53,8 +76,10 @@ class StarPilotDataLayout(StarPilotPanel): 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)) + class StarPilotBackupsLayout(StarPilotPanel): def __init__(self): super().__init__() @@ -67,30 +92,45 @@ class StarPilotBackupsLayout(StarPilotPanel): def _get_backups(self): b_dir = Path("/data/backups") - if not b_dir.exists(): return [] + 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): - # Simplified backup logic - gui_app.set_modal_overlay(alert_dialog(tr("Backup creation started in background."))) - def _task(): - os.makedirs("/data/backups", exist_ok=True) - subprocess.run(["tar", "--use-compress-program=zstd", "-cf", "/data/backups/manual_backup.tar.zst", "/data/openpilot"]) - threading.Thread(target=_task, daemon=True).start() + 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() + + gui_app.set_modal_overlay(InputDialog(tr("Name your backup"), "", on_close=on_name)) def _on_restore_backup(self): backups = self._get_backups() if not backups: gui_app.set_modal_overlay(alert_dialog(tr("No backups found."))) return + def _on_select(res, val): if res == DialogResult.CONFIRM: 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/{val}", "-C", "/"]) os.system("reboot") + threading.Thread(target=_task, daemon=True).start() + gui_app.set_modal_overlay(SelectionDialog(tr("Select Backup"), backups, on_close=_on_select)) def _on_delete_backup(self): @@ -98,20 +138,96 @@ class StarPilotBackupsLayout(StarPilotPanel): if not backups: gui_app.set_modal_overlay(alert_dialog(tr("No backups found."))) return + def _on_select(res, val): if res == DialogResult.CONFIRM: os.remove(f"/data/backups/{val}") self._rebuild_grid() + gui_app.set_modal_overlay(SelectionDialog(tr("Delete Backup"), backups, on_close=_on_select)) + +class StarPilotToggleBackupsLayout(StarPilotPanel): + def __init__(self): + super().__init__() + self.CATEGORIES = [ + {"title": tr_noop("Create Toggle Backup"), "type": "hub", "on_click": self._on_create, "color": "#FA6800"}, + {"title": tr_noop("Restore Toggle Backup"), "type": "hub", "on_click": self._on_restore, "color": "#FA6800"}, + {"title": tr_noop("Delete Toggle Backup"), "type": "hub", "on_click": self._on_delete, "color": "#FA6800"}, + ] + 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() + + gui_app.set_modal_overlay(InputDialog(tr("Name your toggle backup"), "", on_close=on_name)) + + def _on_restore(self): + backups = self._get_backups() + if not backups: + gui_app.set_modal_overlay(alert_dialog(tr("No toggle backups found."))) + return + + def _on_select(res, val): + if res == DialogResult.CONFIRM: + + def on_confirm(r2): + if r2 == DialogResult.CONFIRM: + src = Path(f"/data/toggle_backups/{val}") + shutil.copytree(str(src), "/data/params/d", dirs_exist_ok=True) + 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(SelectionDialog(tr("Select Toggle Backup"), backups, on_close=_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 + + def _on_select(res, val): + if res == DialogResult.CONFIRM: + shutil.rmtree(f"/data/toggle_backups/{val}", ignore_errors=True) + self._rebuild_grid() + + gui_app.set_modal_overlay(SelectionDialog(tr("Delete Toggle Backup"), backups, on_close=_on_select)) + + class StarPilotStorageLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - {"title": tr_noop("Driving Data"), "type": "hub", "on_click": self._show_stats, "color": "#FA6800"}, + {"title": tr_noop("Driving Data"), "type": "value", "get_value": self._get_storage, "on_click": lambda: None, "color": "#FA6800"}, ] self._rebuild_grid() - def _show_stats(self): - # In a real environment we'd calculate du -sh /data - gui_app.set_modal_overlay(alert_dialog(tr("Storage management not yet fully ported to Python."))) + 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" diff --git a/selfdrive/ui/layouts/settings/starpilot/device.py b/selfdrive/ui/layouts/settings/starpilot/device.py index 891821eb..98a49924 100644 --- a/selfdrive/ui/layouts/settings/starpilot/device.py +++ b/selfdrive/ui/layouts/settings/starpilot/device.py @@ -1,73 +1,113 @@ 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.system.ui.widgets.selection_dialog import SelectionDialog from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel from openpilot.selfdrive.ui.layouts.settings.starpilot.metro import SliderDialog + class StarPilotDeviceLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - { - "title": tr_noop("Screen Settings"), - "panel": "screen", - "icon": "toggle_icons/icon_light.png", - "color": "#FA6800" - }, - { - "title": tr_noop("Device Settings"), - "panel": "device_settings", - "icon": "toggle_icons/icon_device.png", - "color": "#FA6800" - }, + {"title": tr_noop("Screen Settings"), "panel": "screen", "icon": "toggle_icons/icon_light.png", "color": "#FA6800"}, + {"title": tr_noop("Device Settings"), "panel": "device_settings", "icon": "toggle_icons/icon_device.png", "color": "#FA6800"}, { "title": tr_noop("Device Shutdown"), "type": "value", "get_value": self._get_shutdown_timer, "on_click": self._show_shutdown_selector, - "color": "#FA6800" + "color": "#FA6800", }, { "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": "#FA6800" + "color": "#FA6800", }, { "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": "#FA6800" + "color": "#FA6800", + }, + { + "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": "#FA6800", }, { "title": tr_noop("High-Quality Recording"), "type": "toggle", + "param": "HigherBitrate", "get_state": lambda: self._params.get_bool("HigherBitrate"), - "set_state": lambda s: self._params.put_bool("HigherBitrate", s), - "color": "#FA6800" + "set_state": lambda s: self._on_higher_bitrate_toggle(s), + "color": "#FA6800", }, ] - + self._sub_panels = { "screen": StarPilotScreenLayout(), "device_settings": StarPilotDeviceManagementLayout(), } - + 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) - + 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" + 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): @@ -75,15 +115,15 @@ class StarPilotDeviceLayout(StarPilotPanel): 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(SliderDialog( - tr("Device Shutdown"), 0, 33, 1, self._params.get_int("DeviceShutdown"), - on_close, labels=labels, color="#FA6800" - )) + 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(SliderDialog(tr("Device Shutdown"), 0, 33, 1, self._params.get_int("DeviceShutdown"), on_close, labels=labels, color="#FA6800")) + class StarPilotScreenLayout(StarPilotPanel): def __init__(self): @@ -94,37 +134,45 @@ class StarPilotScreenLayout(StarPilotPanel): "type": "value", "get_value": lambda: self._get_brightness("ScreenBrightness"), "on_click": lambda: self._show_brightness_selector("ScreenBrightness"), - "color": "#FA6800" + "color": "#FA6800", }, { "title": tr_noop("Brightness (Onroad)"), "type": "value", "get_value": lambda: self._get_brightness("ScreenBrightnessOnroad"), "on_click": lambda: self._show_brightness_selector("ScreenBrightnessOnroad"), - "color": "#FA6800" + "color": "#FA6800", }, { "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": "#FA6800" + "color": "#FA6800", }, { "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": "#FA6800" + "color": "#FA6800", + }, + { + "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": "#FA6800", }, - {"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": "#FA6800"}, ] self._rebuild_grid() def _get_brightness(self, key): v = self._params.get_int(key) - if v == 0: return tr("Off") - if v == 101: return tr("Auto") + if v == 0: + return tr("Off") + if v == 101: + return tr("Auto") return f"{v}%" def _show_brightness_selector(self, key): @@ -134,33 +182,75 @@ class StarPilotScreenLayout(StarPilotPanel): self._params.put_int(key, new_v) HARDWARE.set_brightness(new_v) self._rebuild_grid() - - gui_app.set_modal_overlay(SliderDialog( - tr(key), 0, 101, 1, self._params.get_int(key), - on_close, unit="%", labels={0: tr("Off"), 101: tr("Auto")}, color="#FA6800" - )) + + gui_app.set_modal_overlay( + SliderDialog(tr(key), 0, 101, 1, self._params.get_int(key), on_close, unit="%", labels={0: tr("Off"), 101: tr("Auto")}, color="#FA6800") + ) 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(SliderDialog(tr(key), 5, 60, 5, self._params.get_int(key), on_close, unit="s", color="#FA6800")) + class StarPilotDeviceManagementLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - {"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": "#FA6800"}, - {"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": "#FA6800"}, - {"title": tr_noop("Use Konik Server"), "type": "toggle", "get_state": lambda: self._params.get_bool("UseKonikServer"), "set_state": lambda s: self._params.put_bool("UseKonikServer", s), "color": "#FA6800"}, + { + "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": "#FA6800", + }, + { + "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": "#FA6800", + }, + { + "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": "#FA6800", + }, ] 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(SliderDialog(tr("Low-Voltage Cutoff"), 11.8, 12.5, 0.1, self._params.get_float("LowVoltageShutdown"), on_close, unit="V", color="#FA6800")) + gui_app.set_modal_overlay( + SliderDialog(tr("Low-Voltage Cutoff"), 11.8, 12.5, 0.1, self._params.get_float("LowVoltageShutdown"), on_close, unit="V", color="#FA6800") + ) diff --git a/selfdrive/ui/layouts/settings/starpilot/longitudinal.py b/selfdrive/ui/layouts/settings/starpilot/longitudinal.py index 0a5f20e8..bf471c9f 100644 --- a/selfdrive/ui/layouts/settings/starpilot/longitudinal.py +++ b/selfdrive/ui/layouts/settings/starpilot/longitudinal.py @@ -5,9 +5,11 @@ 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.system.ui.widgets.selection_dialog import SelectionDialog +from openpilot.system.ui.widgets.input_dialog import InputDialog from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel from openpilot.selfdrive.ui.layouts.settings.starpilot.metro import SliderDialog + class StarPilotLongitudinalLayout(StarPilotPanel): def __init__(self): super().__init__() @@ -20,18 +22,15 @@ class StarPilotLongitudinalLayout(StarPilotPanel): "qol": StarPilotLongitudinalQOLLayout(), "slc": StarPilotSpeedLimitControllerLayout(), "weather": StarPilotWeatherLayout(), - # Personality Sub-panels "traffic_personality": StarPilotPersonalityProfileLayout("Traffic"), "aggressive_personality": StarPilotPersonalityProfileLayout("Aggressive"), "standard_personality": StarPilotPersonalityProfileLayout("Standard"), "relaxed_personality": StarPilotPersonalityProfileLayout("Relaxed"), - # SLC Sub-panels "slc_offsets": StarPilotSLCOffsetsLayout(), "slc_qol": StarPilotSLCQOLLayout(), "slc_visuals": StarPilotSLCVisualsLayout(), - # Weather Sub-panels "low_visibility": StarPilotWeatherBase("LowVisibility"), "rain": StarPilotWeatherBase("Rain"), @@ -51,21 +50,80 @@ class StarPilotLongitudinalLayout(StarPilotPanel): ] 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) + 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() + class StarPilotAdvancedLongitudinalLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - {"title": tr_noop("EV Tuning"), "type": "toggle", "get_state": lambda: self._params.get_bool("EVTuning"), "set_state": lambda s: self._params.put_bool("EVTuning", s), "color": "#1BA1E2"}, - {"title": tr_noop("Truck Tuning"), "type": "toggle", "get_state": lambda: self._params.get_bool("TruckTuning"), "set_state": lambda s: self._params.put_bool("TruckTuning", s), "color": "#1BA1E2"}, - {"title": tr_noop("Actuator Delay"), "type": "value", "get_value": lambda: f"{self._params.get_float('LongitudinalActuatorDelay'):.2f}s", "on_click": lambda: self._show_float_selector("LongitudinalActuatorDelay", 0.0, 1.0, 0.01, "s"), "color": "#1BA1E2"}, - {"title": tr_noop("Max Acceleration"), "type": "value", "get_value": lambda: f"{self._params.get_float('MaxDesiredAcceleration'):.1f}m/s²", "on_click": lambda: self._show_float_selector("MaxDesiredAcceleration", 0.1, 4.0, 0.1, "m/s²"), "color": "#1BA1E2"}, - {"title": tr_noop("Start Accel"), "type": "value", "get_value": lambda: f"{self._params.get_float('StartAccel'):.2f}m/s²", "on_click": lambda: self._show_float_selector("StartAccel", 0.0, 4.0, 0.01, "m/s²"), "color": "#1BA1E2"}, - {"title": tr_noop("Stop Accel"), "type": "value", "get_value": lambda: f"{self._params.get_float('StopAccel'):.2f}m/s²", "on_click": lambda: self._show_float_selector("StopAccel", -4.0, 0.0, 0.01, "m/s²"), "color": "#1BA1E2"}, - {"title": tr_noop("Stopping Rate"), "type": "value", "get_value": lambda: f"{self._params.get_float('StoppingDecelRate'):.3f}m/s²", "on_click": lambda: self._show_float_selector("StoppingDecelRate", 0.001, 1.0, 0.001, "m/s²"), "color": "#1BA1E2"}, + { + "title": tr_noop("EV Tuning"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("EVTuning"), + "set_state": lambda s: self._params.put_bool("EVTuning", s), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Truck Tuning"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("TruckTuning"), + "set_state": lambda s: self._params.put_bool("TruckTuning", s), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Actuator Delay"), + "type": "value", + "get_value": lambda: f"{self._params.get_float('LongitudinalActuatorDelay'):.2f}s", + "on_click": lambda: self._show_float_selector("LongitudinalActuatorDelay", 0.0, 1.0, 0.01, "s"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Max Acceleration"), + "type": "value", + "get_value": lambda: f"{self._params.get_float('MaxDesiredAcceleration'):.1f}m/s²", + "on_click": lambda: self._show_float_selector("MaxDesiredAcceleration", 0.1, 4.0, 0.1, "m/s²"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Start Accel"), + "type": "value", + "get_value": lambda: f"{self._params.get_float('StartAccel'):.2f}m/s²", + "on_click": lambda: self._show_float_selector("StartAccel", 0.0, 4.0, 0.01, "m/s²"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Stop Accel"), + "type": "value", + "get_value": lambda: f"{self._params.get_float('StopAccel'):.2f}m/s²", + "on_click": lambda: self._show_float_selector("StopAccel", -4.0, 0.0, 0.01, "m/s²"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Stopping Rate"), + "type": "value", + "get_value": lambda: f"{self._params.get_float('StoppingDecelRate'):.3f}m/s²", + "on_click": lambda: self._show_float_selector("StoppingDecelRate", 0.001, 1.0, 0.001, "m/s²"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("VEgo Starting"), + "type": "value", + "get_value": lambda: f"{self._params.get_float('VEgoStarting'):.2f}m/s", + "on_click": lambda: self._show_float_selector("VEgoStarting", 0.01, 1.0, 0.01, "m/s"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("VEgo Stopping"), + "type": "value", + "get_value": lambda: f"{self._params.get_float('VEgoStopping'):.2f}m/s", + "on_click": lambda: self._show_float_selector("VEgoStopping", 0.01, 1.0, 0.01, "m/s"), + "color": "#1BA1E2", + }, ] self._rebuild_grid() @@ -74,22 +132,108 @@ class StarPilotAdvancedLongitudinalLayout(StarPilotPanel): if res == DialogResult.CONFIRM: self._params.put_float(key, float(val)) self._rebuild_grid() + gui_app.set_modal_overlay(SliderDialog(tr(key), min_v, max_v, step, self._params.get_float(key), on_close, unit=unit, color="#1BA1E2")) + class StarPilotConditionalExperimentalLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - {"title": tr_noop("Conditional Experimental"), "type": "toggle", "get_state": lambda: self._params.get_bool("ConditionalExperimental"), "set_state": lambda s: self._params.put_bool("ConditionalExperimental", s), "icon": "toggle_icons/icon_conditional.png", "color": "#1BA1E2"}, - {"title": tr_noop("Below Speed"), "type": "value", "get_value": lambda: f"{self._params.get_int('CESpeed')} mph", "on_click": lambda: self._show_speed_selector("CESpeed"), "color": "#1BA1E2"}, - {"title": tr_noop("Curves"), "type": "toggle", "get_state": lambda: self._params.get_bool("CECurves"), "set_state": lambda s: self._params.put_bool("CECurves", s), "color": "#1BA1E2"}, - {"title": tr_noop("Stop Lights"), "type": "toggle", "get_state": lambda: self._params.get_bool("CEStopLights"), "set_state": lambda s: self._params.put_bool("CEStopLights", s), "color": "#1BA1E2"}, - {"title": tr_noop("Lead Detected"), "type": "toggle", "get_state": lambda: self._params.get_bool("CELead"), "set_state": lambda s: self._params.put_bool("CELead", s), "color": "#1BA1E2"}, - {"title": tr_noop("Slower Lead"), "type": "toggle", "get_state": lambda: self._params.get_bool("CESlowerLead"), "set_state": lambda s: self._params.put_bool("CESlowerLead", s), "color": "#1BA1E2"}, - {"title": tr_noop("Stopped Lead"), "type": "toggle", "get_state": lambda: self._params.get_bool("CEStoppedLead"), "set_state": lambda s: self._params.put_bool("CEStoppedLead", s), "color": "#1BA1E2"}, - {"title": tr_noop("Predicted Stop"), "type": "value", "get_value": lambda: f"{self._params.get_int('CEModelStopTime')}s", "on_click": lambda: self._show_int_selector("CEModelStopTime", 0, 10, "s"), "color": "#1BA1E2"}, - {"title": tr_noop("Signal Below"), "type": "value", "get_value": lambda: f"{self._params.get_int('CESignalSpeed')} mph", "on_click": lambda: self._show_speed_selector("CESignalSpeed"), "color": "#1BA1E2"}, - {"title": tr_noop("Status Widget"), "type": "toggle", "get_state": lambda: self._params.get_bool("ShowCEMStatus"), "set_state": lambda s: self._params.put_bool("ShowCEMStatus", s), "color": "#1BA1E2"}, + { + "title": tr_noop("Conditional Experimental"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("ConditionalExperimental"), + "set_state": lambda s: self._params.put_bool("ConditionalExperimental", s), + "icon": "toggle_icons/icon_conditional.png", + "color": "#1BA1E2", + }, + { + "title": tr_noop("Below Speed"), + "type": "value", + "get_value": lambda: f"{self._params.get_int('CESpeed')} mph", + "on_click": lambda: self._show_speed_selector("CESpeed"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Curves"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("CECurves"), + "set_state": lambda s: self._params.put_bool("CECurves", s), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Curves Lead"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("CECurvesLead"), + "set_state": lambda s: self._params.put_bool("CECurvesLead", s), + "visible": lambda: self._params.get_bool("CECurves"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Stop Lights"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("CEStopLights"), + "set_state": lambda s: self._params.put_bool("CEStopLights", s), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Lead Detected"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("CELead"), + "set_state": lambda s: self._params.put_bool("CELead", s), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Slower Lead"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("CESlowerLead"), + "set_state": lambda s: self._params.put_bool("CESlowerLead", s), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Stopped Lead"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("CEStoppedLead"), + "set_state": lambda s: self._params.put_bool("CEStoppedLead", s), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Predicted Stop"), + "type": "value", + "get_value": lambda: f"{self._params.get_int('CEModelStopTime')}s", + "on_click": lambda: self._show_int_selector("CEModelStopTime", 0, 10, "s"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Signal Below"), + "type": "value", + "get_value": lambda: f"{self._params.get_int('CESignalSpeed')} mph", + "on_click": lambda: self._show_speed_selector("CESignalSpeed"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Speed Lead"), + "type": "value", + "get_value": lambda: f"{self._params.get_int('CESpeedLead')} mph", + "on_click": lambda: self._show_speed_selector("CESpeedLead"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Signal Lane Detection"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("CESignalLaneDetection"), + "set_state": lambda s: self._params.put_bool("CESignalLaneDetection", s), + "visible": lambda: self._params.get_int("CESignalSpeed") > 0, + "color": "#1BA1E2", + }, + { + "title": tr_noop("Status Widget"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("ShowCEMStatus"), + "set_state": lambda s: self._params.put_bool("ShowCEMStatus", s), + "color": "#1BA1E2", + }, ] self._rebuild_grid() @@ -98,6 +242,7 @@ class StarPilotConditionalExperimentalLayout(StarPilotPanel): if res == DialogResult.CONFIRM: self._params.put_int(key, int(val)) self._rebuild_grid() + gui_app.set_modal_overlay(SliderDialog(tr(key), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#1BA1E2")) def _show_int_selector(self, key, min_v, max_v, unit=""): @@ -105,17 +250,62 @@ class StarPilotConditionalExperimentalLayout(StarPilotPanel): if res == DialogResult.CONFIRM: self._params.put_int(key, int(val)) self._rebuild_grid() + gui_app.set_modal_overlay(SliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#1BA1E2")) + class StarPilotCurveSpeedLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - {"title": tr_noop("Curve Speed Controller"), "type": "toggle", "get_state": lambda: self._params.get_bool("CurveSpeedController"), "set_state": lambda s: self._params.put_bool("CurveSpeedController", s), "icon": "toggle_icons/icon_speed_map.png", "color": "#1BA1E2"}, - {"title": tr_noop("Status Widget"), "type": "toggle", "get_state": lambda: self._params.get_bool("ShowCSCStatus"), "set_state": lambda s: self._params.put_bool("ShowCSCStatus", s), "color": "#1BA1E2"}, + { + "title": tr_noop("Curve Speed Controller"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("CurveSpeedController"), + "set_state": lambda s: self._params.put_bool("CurveSpeedController", s), + "icon": "toggle_icons/icon_speed_map.png", + "color": "#1BA1E2", + }, + { + "title": tr_noop("Status Widget"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("ShowCSCStatus"), + "set_state": lambda s: self._params.put_bool("ShowCSCStatus", s), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Calibrated Lateral Accel"), + "type": "value", + "get_value": lambda: f"{self._params_memory.get_float('CalibratedLateralAcceleration'):.2f} m/s²", + "on_click": lambda: None, + "color": "#1BA1E2", + }, + { + "title": tr_noop("Calibration Progress"), + "type": "value", + "get_value": lambda: f"{self._params_memory.get_float('CalibrationProgress'):.2f}%", + "on_click": lambda: None, + "color": "#1BA1E2", + }, + { + "title": tr_noop("Reset Curve Data"), + "type": "hub", + "on_click": lambda: self._reset_curve_data(), + "color": "#1BA1E2", + }, ] self._rebuild_grid() + def _reset_curve_data(self): + def on_close(res): + if res == DialogResult.CONFIRM: + self._params.remove("CalibratedLateralAcceleration") + self._params.remove("CalibrationProgress") + self._rebuild_grid() + + gui_app.set_modal_overlay(ConfirmDialog(tr("Reset Curve Data?"), tr("Confirm"), on_close=on_close)) + + class StarPilotPersonalitiesLayout(StarPilotPanel): def __init__(self): super().__init__() @@ -127,25 +317,88 @@ class StarPilotPersonalitiesLayout(StarPilotPanel): ] self._rebuild_grid() + class StarPilotPersonalityProfileLayout(StarPilotPanel): def __init__(self, profile: str): super().__init__() self._profile = profile + follow_min = 1.0 if profile == "Traffic" else 0.5 + follow_max = 2.5 if profile == "Traffic" else 3.0 self.CATEGORIES = [ - {"title": tr_noop("Follow Distance"), "type": "value", "get_value": lambda: f"{self._params.get_float(self._profile + 'Follow'):.2f}s", "on_click": lambda: self._show_float_selector(self._profile + "Follow", 0.5, 3.0, 0.05, "s"), "color": "#1BA1E2"}, - {"title": tr_noop("Accel Smoothness"), "type": "value", "get_value": lambda: f"{self._params.get_int(self._profile + 'JerkAcceleration')}%", "on_click": lambda: self._show_int_selector(self._profile + "JerkAcceleration", 25, 200, "%"), "color": "#1BA1E2"}, - {"title": tr_noop("Brake Smoothness"), "type": "value", "get_value": lambda: f"{self._params.get_int(self._profile + 'JerkDeceleration')}%", "on_click": lambda: self._show_int_selector(self._profile + "JerkDeceleration", 25, 200, "%"), "color": "#1BA1E2"}, - {"title": tr_noop("Safety Gap Bias"), "type": "value", "get_value": lambda: f"{self._params.get_int(self._profile + 'JerkDanger')}%", "on_click": lambda: self._show_int_selector(self._profile + "JerkDanger", 25, 200, "%"), "color": "#1BA1E2"}, - {"title": tr_noop("Slowdown Response"), "type": "value", "get_value": lambda: f"{self._params.get_int(self._profile + 'JerkSpeedDecrease')}%", "on_click": lambda: self._show_int_selector(self._profile + "JerkSpeedDecrease", 25, 200, "%"), "color": "#1BA1E2"}, - {"title": tr_noop("Speed-Up Response"), "type": "value", "get_value": lambda: f"{self._params.get_int(self._profile + 'JerkSpeed')}%", "on_click": lambda: self._show_int_selector(self._profile + "JerkSpeed", 25, 200, "%"), "color": "#1BA1E2"}, + { + "title": tr_noop("Follow Distance"), + "type": "value", + "get_value": lambda: f"{self._params.get_float(self._profile + 'Follow'):.2f}s", + "on_click": lambda: self._show_float_selector(self._profile + "Follow", follow_min, follow_max, 0.05, "s"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Follow High"), + "type": "value", + "get_value": lambda: f"{self._params.get_float(self._profile + 'FollowHigh'):.2f}s", + "on_click": lambda: self._show_float_selector(self._profile + "FollowHigh", 1.0, 3.0, 0.05, "s"), + "visible": lambda: self._profile != "Traffic", + "color": "#1BA1E2", + }, + { + "title": tr_noop("Accel Smoothness"), + "type": "value", + "get_value": lambda: f"{self._params.get_int(self._profile + 'JerkAcceleration')}%", + "on_click": lambda: self._show_int_selector(self._profile + "JerkAcceleration", 25, 200, "%"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Brake Smoothness"), + "type": "value", + "get_value": lambda: f"{self._params.get_int(self._profile + 'JerkDeceleration')}%", + "on_click": lambda: self._show_int_selector(self._profile + "JerkDeceleration", 25, 200, "%"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Safety Gap Bias"), + "type": "value", + "get_value": lambda: f"{self._params.get_int(self._profile + 'JerkDanger')}%", + "on_click": lambda: self._show_int_selector(self._profile + "JerkDanger", 25, 200, "%"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Slowdown Response"), + "type": "value", + "get_value": lambda: f"{self._params.get_int(self._profile + 'JerkSpeedDecrease')}%", + "on_click": lambda: self._show_int_selector(self._profile + "JerkSpeedDecrease", 25, 200, "%"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Speed-Up Response"), + "type": "value", + "get_value": lambda: f"{self._params.get_int(self._profile + 'JerkSpeed')}%", + "on_click": lambda: self._show_int_selector(self._profile + "JerkSpeed", 25, 200, "%"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Reset to Defaults"), + "type": "hub", + "on_click": lambda: self._reset_profile(), + "color": "#1BA1E2", + }, ] self._rebuild_grid() + def _reset_profile(self): + def on_close(res): + if res == DialogResult.CONFIRM: + for key in ["Follow", "FollowHigh", "JerkAcceleration", "JerkDeceleration", "JerkDanger", "JerkSpeedDecrease", "JerkSpeed"]: + 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)) + 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(SliderDialog(tr(key), min_v, max_v, step, self._params.get_float(key), on_close, unit=unit, color="#1BA1E2")) def _show_int_selector(self, key, min_v, max_v, unit=""): @@ -153,19 +406,63 @@ class StarPilotPersonalityProfileLayout(StarPilotPanel): if res == DialogResult.CONFIRM: self._params.put_int(key, int(val)) self._rebuild_grid() + gui_app.set_modal_overlay(SliderDialog(tr(key), min_v, max_v, 5, self._params.get_int(key), on_close, unit=unit, color="#1BA1E2")) + class StarPilotLongitudinalTuneLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - {"title": tr_noop("Acceleration Profile"), "type": "value", "get_value": lambda: self._params.get("AccelerationProfile", encoding='utf-8') or "Standard", "on_click": lambda: self._show_selection("AccelerationProfile", ["Standard", "Eco", "Sport", "Sport+"]), "color": "#1BA1E2"}, - {"title": tr_noop("Deceleration Profile"), "type": "value", "get_value": lambda: self._params.get("DecelerationProfile", encoding='utf-8') or "Standard", "on_click": lambda: self._show_selection("DecelerationProfile", ["Standard", "Eco", "Sport"]), "color": "#1BA1E2"}, - {"title": tr_noop("Human Acceleration"), "type": "toggle", "get_state": lambda: self._params.get_bool("HumanAcceleration"), "set_state": lambda s: self._params.put_bool("HumanAcceleration", s), "color": "#1BA1E2"}, - {"title": tr_noop("Human Following"), "type": "toggle", "get_state": lambda: self._params.get_bool("HumanFollowing"), "set_state": lambda s: self._params.put_bool("HumanFollowing", s), "color": "#1BA1E2"}, - {"title": tr_noop("Human Lane Changes"), "type": "toggle", "get_state": lambda: self._params.get_bool("HumanLaneChanges"), "set_state": lambda s: self._params.put_bool("HumanLaneChanges", s), "color": "#1BA1E2"}, - {"title": tr_noop("Lead Detection"), "type": "value", "get_value": lambda: f"{self._params.get_int('LeadDetectionThreshold')}%", "on_click": lambda: self._show_int_selector("LeadDetectionThreshold", 25, 50, "%"), "color": "#1BA1E2"}, - {"title": tr_noop("Taco Tune"), "type": "toggle", "get_state": lambda: self._params.get_bool("TacoTune"), "set_state": lambda s: self._params.put_bool("TacoTune", s), "color": "#1BA1E2"}, + { + "title": tr_noop("Acceleration Profile"), + "type": "value", + "get_value": lambda: self._params.get("AccelerationProfile", encoding='utf-8') or "Standard", + "on_click": lambda: self._show_selection("AccelerationProfile", ["Standard", "Eco", "Sport", "Sport+"]), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Deceleration Profile"), + "type": "value", + "get_value": lambda: self._params.get("DecelerationProfile", encoding='utf-8') or "Standard", + "on_click": lambda: self._show_selection("DecelerationProfile", ["Standard", "Eco", "Sport"]), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Human Acceleration"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("HumanAcceleration"), + "set_state": lambda s: self._params.put_bool("HumanAcceleration", s), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Human Following"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("HumanFollowing"), + "set_state": lambda s: self._params.put_bool("HumanFollowing", s), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Human Lane Changes"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("HumanLaneChanges"), + "set_state": lambda s: self._params.put_bool("HumanLaneChanges", s), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Lead Detection"), + "type": "value", + "get_value": lambda: f"{self._params.get_int('LeadDetectionThreshold')}%", + "on_click": lambda: self._show_int_selector("LeadDetectionThreshold", 25, 50, "%"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Taco Tune"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("TacoTune"), + "set_state": lambda s: self._params.put_bool("TacoTune", s), + "color": "#1BA1E2", + }, ] self._rebuild_grid() @@ -174,6 +471,7 @@ class StarPilotLongitudinalTuneLayout(StarPilotPanel): if res == DialogResult.CONFIRM: self._params.put(key, val) self._rebuild_grid() + gui_app.set_modal_overlay(SelectionDialog(tr(key), options, self._params.get(key, encoding='utf-8') or "Standard", on_close=on_select)) def _show_int_selector(self, key, min_v, max_v, unit=""): @@ -181,17 +479,79 @@ class StarPilotLongitudinalTuneLayout(StarPilotPanel): if res == DialogResult.CONFIRM: self._params.put_int(key, int(val)) self._rebuild_grid() + gui_app.set_modal_overlay(SliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#1BA1E2")) + class StarPilotLongitudinalQOLLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - {"title": tr_noop("Cruise Interval"), "type": "value", "get_value": lambda: f"{self._params.get_int('CustomCruise')} mph", "on_click": lambda: self._show_speed_selector("CustomCruise"), "color": "#1BA1E2"}, - {"title": tr_noop("Reverse Cruise"), "type": "toggle", "get_state": lambda: self._params.get_bool("ReverseCruise"), "set_state": lambda s: self._params.put_bool("ReverseCruise", s), "color": "#1BA1E2"}, - {"title": tr_noop("Force Stops"), "type": "toggle", "get_state": lambda: self._params.get_bool("ForceStops"), "set_state": lambda s: self._params.put_bool("ForceStops", s), "color": "#1BA1E2"}, - {"title": tr_noop("Stopped Distance"), "type": "value", "get_value": lambda: f"{self._params.get_int('IncreasedStoppedDistance')} ft", "on_click": lambda: self._show_int_selector("IncreasedStoppedDistance", 0, 10, " ft"), "color": "#1BA1E2"}, - {"title": tr_noop("Set Speed Offset"), "type": "value", "get_value": lambda: f"+{self._params.get_int('SetSpeedOffset')} mph", "on_click": lambda: self._show_int_selector("SetSpeedOffset", 0, 99, " mph"), "color": "#1BA1E2"}, + { + "title": tr_noop("Cruise Interval"), + "type": "value", + "get_value": lambda: f"{self._params.get_int('CustomCruise')} mph", + "on_click": lambda: self._show_speed_selector("CustomCruise"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Cruise Long"), + "type": "value", + "get_value": lambda: f"{self._params.get_int('CustomCruiseLong')} mph", + "on_click": lambda: self._show_speed_selector("CustomCruiseLong"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Reverse Cruise"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("ReverseCruise"), + "set_state": lambda s: self._params.put_bool("ReverseCruise", s), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Force Stops"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("ForceStops"), + "set_state": lambda s: self._params.put_bool("ForceStops", s), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Stopped Distance"), + "type": "value", + "get_value": lambda: f"{self._params.get_int('IncreasedStoppedDistance')} ft", + "on_click": lambda: self._show_int_selector("IncreasedStoppedDistance", 0, 10, " ft"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Set Speed Offset"), + "type": "value", + "get_value": lambda: f"+{self._params.get_int('SetSpeedOffset')} mph", + "on_click": lambda: self._show_int_selector("SetSpeedOffset", 0, 99, " mph"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Map Gears"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("MapGears"), + "set_state": lambda s: self._params.put_bool("MapGears", s), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Map Acceleration"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("MapAcceleration"), + "set_state": lambda s: self._params.put_bool("MapAcceleration", s), + "visible": lambda: self._params.get_bool("MapGears"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Map Deceleration"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("MapDeceleration"), + "set_state": lambda s: self._params.put_bool("MapDeceleration", s), + "visible": lambda: self._params.get_bool("MapGears"), + "color": "#1BA1E2", + }, ] self._rebuild_grid() @@ -200,6 +560,7 @@ class StarPilotLongitudinalQOLLayout(StarPilotPanel): if res == DialogResult.CONFIRM: self._params.put_int(key, int(val)) self._rebuild_grid() + gui_app.set_modal_overlay(SliderDialog(tr(key), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#1BA1E2")) def _show_int_selector(self, key, min_v, max_v, unit=""): @@ -207,8 +568,10 @@ class StarPilotLongitudinalQOLLayout(StarPilotPanel): if res == DialogResult.CONFIRM: self._params.put_int(key, int(val)) self._rebuild_grid() + gui_app.set_modal_overlay(SliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#1BA1E2")) + class StarPilotSpeedLimitControllerLayout(StarPilotPanel): def __init__(self): super().__init__() @@ -216,40 +579,66 @@ class StarPilotSpeedLimitControllerLayout(StarPilotPanel): {"title": tr_noop("SLC Offsets"), "panel": "slc_offsets", "icon": "toggle_icons/icon_speed_limit.png", "color": "#1BA1E2"}, {"title": tr_noop("SLC Quality of Life"), "panel": "slc_qol", "icon": "toggle_icons/icon_speed_limit.png", "color": "#1BA1E2"}, {"title": tr_noop("SLC Visuals"), "panel": "slc_visuals", "icon": "toggle_icons/icon_speed_limit.png", "color": "#1BA1E2"}, - {"title": tr_noop("Fallback Speed"), "type": "value", "get_value": lambda: self._params.get("SLCFallback", encoding='utf-8') or "Set Speed", "on_click": lambda: self._show_selection("SLCFallback", ["Set Speed", "Experimental Mode", "Previous Limit"]), "color": "#1BA1E2"}, - {"title": tr_noop("Override Speed"), "type": "value", "get_value": lambda: self._params.get("SLCOverride", encoding='utf-8') or "None", "on_click": lambda: self._show_selection("SLCOverride", ["None", "Set With Gas Pedal", "Max Set Speed"]), "color": "#1BA1E2"}, - {"title": tr_noop("Source Priority"), "type": "value", "get_value": lambda: self._params.get("SLCPriority1", encoding='utf-8') or "Dashboard", "on_click": self._on_priority_clicked, "color": "#1BA1E2"}, + { + "title": tr_noop("Fallback Speed"), + "type": "value", + "get_value": lambda: self._params.get("SLCFallback", encoding='utf-8') or "Set Speed", + "on_click": lambda: self._show_selection("SLCFallback", ["Set Speed", "Experimental Mode", "Previous Limit"]), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Override Speed"), + "type": "value", + "get_value": lambda: self._params.get("SLCOverride", encoding='utf-8') or "None", + "on_click": lambda: self._show_selection("SLCOverride", ["None", "Set With Gas Pedal", "Max Set Speed"]), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Source Priority"), + "type": "value", + "get_value": lambda: self._params.get("SLCPriority1", encoding='utf-8') or "Dashboard", + "on_click": self._on_priority_clicked, + "color": "#1BA1E2", + }, ] self._rebuild_grid() def _on_priority_clicked(self): options = ["Dashboard", "Map Data", "Highest", "Lowest"] + def on_select(res, val): if res == DialogResult.CONFIRM: self._params.put("SLCPriority1", val) self._rebuild_grid() - gui_app.set_modal_overlay(SelectionDialog(tr("SLC Priority"), options, self._params.get("SLCPriority1", encoding='utf-8') or "Dashboard", on_close=on_select)) + + gui_app.set_modal_overlay( + SelectionDialog(tr("SLC Priority"), options, self._params.get("SLCPriority1", encoding='utf-8') or "Dashboard", on_close=on_select) + ) def _show_selection(self, key, options): def on_select(res, val): if res == DialogResult.CONFIRM: self._params.put(key, val) self._rebuild_grid() + gui_app.set_modal_overlay(SelectionDialog(tr(key), options, self._params.get(key, encoding='utf-8') or "None", on_close=on_select)) + class StarPilotSLCOffsetsLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [] for i in range(1, 8): key = f"Offset{i}" - self.CATEGORIES.append({ - "title": tr_noop(f"Offset {i}"), - "type": "value", - "get_value": lambda k=key: f"{self._params.get_int(k)} mph", - "on_click": lambda k=key: self._show_speed_selector(k), - "color": "#1BA1E2" - }) + self.CATEGORIES.append( + { + "title": tr_noop(f"Offset {i}"), + "type": "value", + "get_value": lambda k=key: f"{self._params.get_int(k)} mph", + "on_click": lambda k=key: self._show_speed_selector(k), + "color": "#1BA1E2", + } + ) self._rebuild_grid() def _show_speed_selector(self, key): @@ -257,17 +646,65 @@ class StarPilotSLCOffsetsLayout(StarPilotPanel): if res == DialogResult.CONFIRM: self._params.put_int(key, int(val)) self._rebuild_grid() + gui_app.set_modal_overlay(SliderDialog(tr(key), -99, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#1BA1E2")) + class StarPilotSLCQOLLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - {"title": tr_noop("Match Speed on Engage"), "type": "toggle", "get_state": lambda: self._params.get_bool("SetSpeedLimit"), "set_state": lambda s: self._params.put_bool("SetSpeedLimit", s), "color": "#1BA1E2"}, - {"title": tr_noop("Confirm New Limits"), "type": "toggle", "get_state": lambda: self._params.get_bool("SLCConfirmation"), "set_state": lambda s: self._params.put_bool("SLCConfirmation", s), "color": "#1BA1E2"}, - {"title": tr_noop("Higher Lookahead"), "type": "value", "get_value": lambda: f"{self._params.get_int('SLCLookaheadHigher')}s", "on_click": lambda: self._show_int_selector("SLCLookaheadHigher", 0, 30, "s"), "color": "#1BA1E2"}, - {"title": tr_noop("Lower Lookahead"), "type": "value", "get_value": lambda: f"{self._params.get_int('SLCLookaheadLower')}s", "on_click": lambda: self._show_int_selector("SLCLookaheadLower", 0, 30, "s"), "color": "#1BA1E2"}, - {"title": tr_noop("Mapbox Fallback"), "type": "toggle", "get_state": lambda: self._params.get_bool("SLCMapboxFiller"), "set_state": lambda s: self._params.put_bool("SLCMapboxFiller", s), "color": "#1BA1E2"}, + { + "title": tr_noop("Match Speed on Engage"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("SetSpeedLimit"), + "set_state": lambda s: self._params.put_bool("SetSpeedLimit", s), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Confirm New Limits"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("SLCConfirmation"), + "set_state": lambda s: self._params.put_bool("SLCConfirmation", s), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Confirm Lower"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("SLCConfirmationLower"), + "set_state": lambda s: self._params.put_bool("SLCConfirmationLower", s), + "visible": lambda: self._params.get_bool("SLCConfirmation"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Confirm Higher"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("SLCConfirmationHigher"), + "set_state": lambda s: self._params.put_bool("SLCConfirmationHigher", s), + "visible": lambda: self._params.get_bool("SLCConfirmation"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Higher Lookahead"), + "type": "value", + "get_value": lambda: f"{self._params.get_int('SLCLookaheadHigher')}s", + "on_click": lambda: self._show_int_selector("SLCLookaheadHigher", 0, 30, "s"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Lower Lookahead"), + "type": "value", + "get_value": lambda: f"{self._params.get_int('SLCLookaheadLower')}s", + "on_click": lambda: self._show_int_selector("SLCLookaheadLower", 0, 30, "s"), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Mapbox Fallback"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("SLCMapboxFiller"), + "set_state": lambda s: self._params.put_bool("SLCMapboxFiller", s), + "color": "#1BA1E2", + }, ] self._rebuild_grid() @@ -276,17 +713,32 @@ class StarPilotSLCQOLLayout(StarPilotPanel): if res == DialogResult.CONFIRM: self._params.put_int(key, int(val)) self._rebuild_grid() + gui_app.set_modal_overlay(SliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#1BA1E2")) + class StarPilotSLCVisualsLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - {"title": tr_noop("Show SLC Offset"), "type": "toggle", "get_state": lambda: self._params.get_bool("ShowSLCOffset"), "set_state": lambda s: self._params.put_bool("ShowSLCOffset", s), "color": "#1BA1E2"}, - {"title": tr_noop("Show Sources"), "type": "toggle", "get_state": lambda: self._params.get_bool("SpeedLimitSources"), "set_state": lambda s: self._params.put_bool("SpeedLimitSources", s), "color": "#1BA1E2"}, + { + "title": tr_noop("Show SLC Offset"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("ShowSLCOffset"), + "set_state": lambda s: self._params.put_bool("ShowSLCOffset", s), + "color": "#1BA1E2", + }, + { + "title": tr_noop("Show Sources"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("SpeedLimitSources"), + "set_state": lambda s: self._params.put_bool("SpeedLimitSources", s), + "color": "#1BA1E2", + }, ] self._rebuild_grid() + class StarPilotWeatherLayout(StarPilotPanel): def __init__(self): super().__init__() @@ -295,18 +747,77 @@ class StarPilotWeatherLayout(StarPilotPanel): {"title": tr_noop("Rain"), "panel": "rain", "icon": "toggle_icons/icon_rainbow.png", "color": "#1BA1E2"}, {"title": tr_noop("Rainstorms"), "panel": "rainstorm", "icon": "toggle_icons/icon_rainbow.png", "color": "#1BA1E2"}, {"title": tr_noop("Snow"), "panel": "snow", "icon": "toggle_icons/icon_rainbow.png", "color": "#1BA1E2"}, + { + "title": tr_noop("Set Weather Key"), + "type": "hub", + "on_click": lambda: self._set_weather_key(), + "color": "#1BA1E2", + }, ] self._rebuild_grid() + def _set_weather_key(self): + options = ["ADD", "REMOVE"] + + def on_select(res, val): + if res == DialogResult.CONFIRM: + if val == "ADD": + + def on_key(res, text): + if res == DialogResult.CONFIRM: + self._params.put("WeatherAPIKey", text) + self._rebuild_grid() + + gui_app.set_modal_overlay(InputDialog(tr("Weather API Key"), on_close=on_key)) + elif val == "REMOVE": + + def on_confirm(res): + if res == DialogResult.CONFIRM: + 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.set_modal_overlay(SelectionDialog(tr("Weather API Key"), options, "ADD", on_close=on_select)) + + class StarPilotWeatherBase(StarPilotPanel): def __init__(self, suffix: str): super().__init__() self._suffix = suffix self.CATEGORIES = [ - {"title": tr_noop("Following Distance"), "type": "value", "get_value": lambda: f"+{self._params.get_int('IncreaseFollowing' + self._suffix)}s", "on_click": lambda: self._show_value_selector("IncreaseFollowing" + self._suffix, 0, 3, 0.5, "s"), "icon": "toggle_icons/icon_longitudinal_tune.png", "color": "#1BA1E2"}, - {"title": tr_noop("Stopped Distance"), "type": "value", "get_value": lambda: f"+{self._params.get_int('IncreasedStoppedDistance' + self._suffix)} ft", "on_click": lambda: self._show_value_selector("IncreasedStoppedDistance" + self._suffix, 0, 10, 1, " ft"), "icon": "toggle_icons/icon_longitudinal_tune.png", "color": "#1BA1E2"}, - {"title": tr_noop("Reduce Accel"), "type": "value", "get_value": lambda: f"{self._params.get_int('ReduceAcceleration' + self._suffix)}%", "on_click": lambda: self._show_value_selector("ReduceAcceleration" + self._suffix, 0, 50, 5, "%"), "icon": "toggle_icons/icon_longitudinal_tune.png", "color": "#1BA1E2"}, - {"title": tr_noop("Reduce Curve Speed"), "type": "value", "get_value": lambda: f"{self._params.get_int('ReduceLateralAcceleration' + self._suffix)}%", "on_click": lambda: self._show_value_selector("ReduceLateralAcceleration" + self._suffix, 0, 50, 5, "%"), "icon": "toggle_icons/icon_longitudinal_tune.png", "color": "#1BA1E2"}, + { + "title": tr_noop("Following Distance"), + "type": "value", + "get_value": lambda: f"+{self._params.get_int('IncreaseFollowing' + self._suffix)}s", + "on_click": lambda: self._show_value_selector("IncreaseFollowing" + self._suffix, 0, 3, 0.5, "s"), + "icon": "toggle_icons/icon_longitudinal_tune.png", + "color": "#1BA1E2", + }, + { + "title": tr_noop("Stopped Distance"), + "type": "value", + "get_value": lambda: f"+{self._params.get_int('IncreasedStoppedDistance' + self._suffix)} ft", + "on_click": lambda: self._show_value_selector("IncreasedStoppedDistance" + self._suffix, 0, 10, 1, " ft"), + "icon": "toggle_icons/icon_longitudinal_tune.png", + "color": "#1BA1E2", + }, + { + "title": tr_noop("Reduce Accel"), + "type": "value", + "get_value": lambda: f"{self._params.get_int('ReduceAcceleration' + self._suffix)}%", + "on_click": lambda: self._show_value_selector("ReduceAcceleration" + self._suffix, 0, 99, 1, "%"), + "icon": "toggle_icons/icon_longitudinal_tune.png", + "color": "#1BA1E2", + }, + { + "title": tr_noop("Reduce Curve Speed"), + "type": "value", + "get_value": lambda: f"{self._params.get_int('ReduceLateralAcceleration' + self._suffix)}%", + "on_click": lambda: self._show_value_selector("ReduceLateralAcceleration" + self._suffix, 0, 99, 1, "%"), + "icon": "toggle_icons/icon_longitudinal_tune.png", + "color": "#1BA1E2", + }, ] self._rebuild_grid() @@ -315,5 +826,6 @@ class StarPilotWeatherBase(StarPilotPanel): if res == DialogResult.CONFIRM: self._params.put_int(key, int(float(val))) self._rebuild_grid() + curr = self._params.get_int(key) gui_app.set_modal_overlay(SliderDialog(tr(key), min_v, max_v, step, curr, on_close, unit=unit, color="#1BA1E2")) diff --git a/selfdrive/ui/layouts/settings/starpilot/navigation.py b/selfdrive/ui/layouts/settings/starpilot/navigation.py index feae59c5..b8decb75 100644 --- a/selfdrive/ui/layouts/settings/starpilot/navigation.py +++ b/selfdrive/ui/layouts/settings/starpilot/navigation.py @@ -1,15 +1,12 @@ from __future__ import annotations -import os -import shutil -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.selection_dialog import SelectionDialog from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog, alert_dialog +from openpilot.system.ui.widgets.input_dialog import InputDialog from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel -from openpilot.selfdrive.ui.layouts.settings.starpilot.metro import SliderDialog + class StarPilotNavigationLayout(StarPilotPanel): def __init__(self): @@ -20,42 +17,118 @@ class StarPilotNavigationLayout(StarPilotPanel): self.CATEGORIES = [ {"title": tr_noop("Mapbox Credentials"), "panel": "mapbox", "icon": "toggle_icons/icon_navigate.png", "color": "#8CBF26"}, {"title": tr_noop("Setup Instructions"), "type": "hub", "on_click": self._on_setup, "icon": "toggle_icons/icon_navigate.png", "color": "#8CBF26"}, - {"title": tr_noop("Speed Limit Filler"), "type": "toggle", "get_state": lambda: self._params.get_bool("SpeedLimitFiller"), "set_state": lambda s: self._params.put_bool("SpeedLimitFiller", s), "icon": "toggle_icons/icon_speed_limit.png", "color": "#8CBF26"}, + { + "title": tr_noop("Speed Limit Filler"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("SpeedLimitFiller"), + "set_state": lambda s: self._params.put_bool("SpeedLimitFiller", s), + "icon": "toggle_icons/icon_speed_limit.png", + "color": "#8CBF26", + }, {"title": tr_noop("Search Destination"), "type": "hub", "on_click": self._on_search, "icon": "toggle_icons/icon_navigate.png", "color": "#8CBF26"}, - {"title": tr_noop("Home Address"), "type": "hub", "on_click": self._on_home, "icon": "toggle_icons/icon_navigate.png", "color": "#8CBF26"}, - {"title": tr_noop("Work Address"), "type": "hub", "on_click": self._on_work, "icon": "toggle_icons/icon_navigate.png", "color": "#8CBF26"}, + { + "title": tr_noop("Home Address"), + "type": "value", + "get_value": lambda: self._params.get("HomeAddress", encoding='utf-8') or tr("Not set"), + "on_click": self._on_home, + "icon": "toggle_icons/icon_navigate.png", + "color": "#8CBF26", + }, + { + "title": tr_noop("Work Address"), + "type": "value", + "get_value": lambda: self._params.get("WorkAddress", encoding='utf-8') or tr("Not set"), + "on_click": self._on_work, + "icon": "toggle_icons/icon_navigate.png", + "color": "#8CBF26", + }, ] 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) + 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_setup(self): - gui_app.set_modal_overlay(alert_dialog(tr("Mapbox Setup:\n1. Create account at mapbox.com\n2. Generate Public/Secret keys\n3. Add keys in 'Mapbox Credentials'"))) + gui_app.set_modal_overlay( + alert_dialog(tr("Mapbox Setup:\n1. Create account at mapbox.com\n2. Generate Public/Secret keys\n3. Add keys in 'Mapbox Credentials'")) + ) def _on_search(self): - gui_app.set_modal_overlay(alert_dialog(tr("Search not yet implemented."))) + def on_close(res, text): + if res == DialogResult.CONFIRM and text: + self._params.put("SearchAddress", text) + + gui_app.set_modal_overlay(InputDialog(tr("Search Destination"), "", on_close=on_close)) def _on_home(self): - gui_app.set_modal_overlay(alert_dialog(tr("Home address set."))) + current = self._params.get("HomeAddress", encoding='utf-8') or "" + + def on_close(res, text): + if res == DialogResult.CONFIRM: + self._params.put("HomeAddress", text) + self._rebuild_grid() + + gui_app.set_modal_overlay(InputDialog(tr("Home Address"), current, on_close=on_close)) def _on_work(self): - gui_app.set_modal_overlay(alert_dialog(tr("Work address set."))) + current = self._params.get("WorkAddress", encoding='utf-8') or "" + + def on_close(res, text): + if res == DialogResult.CONFIRM: + self._params.put("WorkAddress", text) + self._rebuild_grid() + + gui_app.set_modal_overlay(InputDialog(tr("Work Address"), current, on_close=on_close)) + class StarPilotMapboxLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - {"title": tr_noop("Public Mapbox Key"), "type": "hub", "on_click": lambda: self._on_key("MapboxPublicKey"), "color": "#8CBF26"}, - {"title": tr_noop("Secret Mapbox Key"), "type": "hub", "on_click": lambda: self._on_key("MapboxSecretKey"), "color": "#8CBF26"}, + { + "title": tr_noop("Public Mapbox Key"), + "type": "value", + "get_value": self._get_key_display, + "on_click": lambda: self._on_key("MapboxPublicKey", "pk."), + "color": "#8CBF26", + }, + { + "title": tr_noop("Secret Mapbox Key"), + "type": "value", + "get_value": self._get_secret_display, + "on_click": lambda: self._on_key("MapboxSecretKey", "sk."), + "color": "#8CBF26", + }, ] self._rebuild_grid() - def _on_key(self, key): - # Simplified keyboard entry for UI port + def _get_key_display(self): + v = self._params.get("MapboxPublicKey", encoding='utf-8') or "" + return f"{v[:8]}..." if v else tr("Not set") + + def _get_secret_display(self): + v = self._params.get("MapboxSecretKey", encoding='utf-8') or "" + return "********" if v else tr("Not set") + + def _on_key(self, key, prefix): current = self._params.get(key, encoding='utf-8') or "" - def on_confirm(res): + if current: + + def on_remove(res): if res == DialogResult.CONFIRM: - # In a real build, we'd trigger a keyboard overlay - pass - gui_app.set_modal_overlay(ConfirmDialog(tr(f"Current Key:\n{current[:20]}..."), tr("Change"), on_close=on_confirm)) + 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)) + else: + + def on_close(res, text): + if res == DialogResult.CONFIRM and text: + if not text.startswith(prefix): + text = prefix + text + self._params.put(key, text) + self._rebuild_grid() + + gui_app.set_modal_overlay(InputDialog(tr(f"Enter {key.replace('Mapbox', 'Mapbox ')}"), "", on_close=on_close)) diff --git a/selfdrive/ui/layouts/settings/starpilot/panel.py b/selfdrive/ui/layouts/settings/starpilot/panel.py index cb466eef..d683262e 100644 --- a/selfdrive/ui/layouts/settings/starpilot/panel.py +++ b/selfdrive/ui/layouts/settings/starpilot/panel.py @@ -9,6 +9,7 @@ from openpilot.common.params import Params from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import Widget + class StarPilotPanelType(IntEnum): MAIN = 0 SOUNDS = 1 @@ -25,13 +26,16 @@ class StarPilotPanelType(IntEnum): VEHICLE = 12 WHEEL = 13 + @dataclass class StarPilotPanelInfo: name: str instance: Widget + from openpilot.selfdrive.ui.layouts.settings.starpilot.metro import TileGrid, HubTile, ToggleTile, ValueTile + class StarPilotPanel(Widget): def __init__(self): super().__init__() @@ -54,13 +58,17 @@ class StarPilotPanel(Widget): def _rebuild_grid(self): if not self.CATEGORIES: return - + if self._tile_grid is None: self._tile_grid = TileGrid(columns=None, padding=20) - + self._tile_grid.clear() - + for cat in self.CATEGORIES: + visible_fn = cat.get("visible") + if visible_fn is not None and not visible_fn(): + continue + tile_type = cat.get("type", "hub") if tile_type == "hub": on_click = cat.get("on_click") @@ -73,24 +81,12 @@ class StarPilotPanel(Widget): icon_path=cat.get("icon"), on_click=on_click, starpilot_icon=cat.get("starpilot_icon", True), - bg_color=cat.get("color") + bg_color=cat.get("color"), ) elif tile_type == "toggle": - 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") - ) + 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")) elif tile_type == "value": - tile = ValueTile( - title=tr(cat["title"]), - get_value=cat["get_value"], - on_click=cat["on_click"], - icon_path=cat.get("icon"), - bg_color=cat.get("color") - ) + tile = ValueTile(title=tr(cat["title"]), get_value=cat["get_value"], on_click=cat["on_click"], icon_path=cat.get("icon"), bg_color=cat.get("color")) else: continue diff --git a/selfdrive/ui/layouts/settings/starpilot/themes.py b/selfdrive/ui/layouts/settings/starpilot/themes.py index 9f7a023e..352a18c6 100644 --- a/selfdrive/ui/layouts/settings/starpilot/themes.py +++ b/selfdrive/ui/layouts/settings/starpilot/themes.py @@ -5,10 +5,11 @@ from openpilot.system.ui.widgets import DialogResult from openpilot.system.ui.widgets.selection_dialog import SelectionDialog from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel + class StarPilotThemesLayout(StarPilotPanel): def __init__(self): super().__init__() - + self._sub_panels = { "personalize": StarPilotPersonalizeLayout(), } @@ -19,7 +20,7 @@ class StarPilotThemesLayout(StarPilotPanel): "panel": "personalize", "icon": "toggle_icons/icon_frog.png", "color": "#A200FF", - "desc": tr_noop("Customize the overall look and feel.") + "desc": tr_noop("Customize the overall look and feel."), }, { "title": tr_noop("Holiday Themes"), @@ -27,7 +28,7 @@ class StarPilotThemesLayout(StarPilotPanel): "get_state": lambda: self._params.get_bool("HolidayThemes"), "set_state": lambda s: self._params.put_bool("HolidayThemes", s), "icon": "toggle_icons/icon_calendar.png", - "color": "#A200FF" + "color": "#A200FF", }, { "title": tr_noop("Rainbow Path"), @@ -35,7 +36,7 @@ class StarPilotThemesLayout(StarPilotPanel): "get_state": lambda: self._params.get_bool("RainbowPath"), "set_state": lambda s: self._params.put_bool("RainbowPath", s), "icon": "toggle_icons/icon_rainbow.png", - "color": "#A200FF" + "color": "#A200FF", }, { "title": tr_noop("Random Events"), @@ -43,7 +44,7 @@ class StarPilotThemesLayout(StarPilotPanel): "get_state": lambda: self._params.get_bool("RandomEvents"), "set_state": lambda s: self._params.put_bool("RandomEvents", s), "icon": "toggle_icons/icon_random.png", - "color": "#A200FF" + "color": "#A200FF", }, { "title": tr_noop("Random Themes"), @@ -51,27 +52,97 @@ class StarPilotThemesLayout(StarPilotPanel): "get_state": lambda: self._params.get_bool("RandomThemes"), "set_state": lambda s: self._params.put_bool("RandomThemes", s), "icon": "toggle_icons/icon_random_themes.png", - "color": "#A200FF" + "color": "#A200FF", }, + {"title": tr_noop("Startup Alert"), "type": "hub", "on_click": self._on_startup_alert, "color": "#A200FF"}, ] 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) + 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_startup_alert(self): + options = ["Stock", "FrogPilot", "Clear"] + current_top = self._params.get("StartupMessageTop", encoding='utf-8') or "" + if current_top == "Be ready to take over at any time": + current = "Stock" + elif current_top == "Hop in and buckle up!": + current = "FrogPilot" + else: + current = "Clear" + + def on_select(res, val): + if res == DialogResult.CONFIRM: + if val == "Stock": + self._params.put("StartupMessageTop", "Be ready to take over at any time") + self._params.put("StartupMessageBottom", "Always keep hands on wheel and eyes on road") + elif val == "FrogPilot": + self._params.put("StartupMessageTop", "Hop in and buckle up!") + self._params.put("StartupMessageBottom", "Human-tested, frog-approved") + else: + self._params.remove("StartupMessageTop") + self._params.remove("StartupMessageBottom") + + gui_app.set_modal_overlay(SelectionDialog(tr("Startup Alert"), options, current, on_close=on_select)) + + class StarPilotPersonalizeLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - {"title": tr_noop("Boot Logo"), "type": "value", "get_value": lambda: self._params.get("BootLogo", encoding='utf-8') or "Stock", "on_click": lambda: self._show_theme_selector("BootLogo"), "color": "#A200FF"}, - {"title": tr_noop("Color Scheme"), "type": "value", "get_value": lambda: self._params.get("ColorScheme", encoding='utf-8') or "Stock", "on_click": lambda: self._show_theme_selector("ColorScheme"), "color": "#A200FF"}, - {"title": tr_noop("Distance Icons"), "type": "value", "get_value": lambda: self._params.get("DistanceIconPack", encoding='utf-8') or "Stock", "on_click": lambda: self._show_theme_selector("DistanceIconPack"), "color": "#A200FF"}, - {"title": tr_noop("Icon Pack"), "type": "value", "get_value": lambda: self._params.get("IconPack", encoding='utf-8') or "Stock", "on_click": lambda: self._show_theme_selector("IconPack"), "color": "#A200FF"}, - {"title": tr_noop("Turn Signals"), "type": "value", "get_value": lambda: self._params.get("SignalAnimation", encoding='utf-8') or "Stock", "on_click": lambda: self._show_theme_selector("SignalAnimation"), "color": "#A200FF"}, - {"title": tr_noop("Sound Pack"), "type": "value", "get_value": lambda: self._params.get("SoundPack", encoding='utf-8') or "Stock", "on_click": lambda: self._show_theme_selector("SoundPack"), "color": "#A200FF"}, - {"title": tr_noop("Steering Wheel"), "type": "value", "get_value": lambda: self._params.get("WheelIcon", encoding='utf-8') or "Stock", "on_click": lambda: self._show_theme_selector("WheelIcon"), "color": "#A200FF"}, + { + "title": tr_noop("Boot Logo"), + "type": "value", + "get_value": lambda: self._params.get("BootLogo", encoding='utf-8') or "Stock", + "on_click": lambda: self._show_theme_selector("BootLogo"), + "color": "#A200FF", + }, + { + "title": tr_noop("Color Scheme"), + "type": "value", + "get_value": lambda: self._params.get("ColorScheme", encoding='utf-8') or "Stock", + "on_click": lambda: self._show_theme_selector("ColorScheme"), + "color": "#A200FF", + }, + { + "title": tr_noop("Distance Icons"), + "type": "value", + "get_value": lambda: self._params.get("DistanceIconPack", encoding='utf-8') or "Stock", + "on_click": lambda: self._show_theme_selector("DistanceIconPack"), + "color": "#A200FF", + }, + { + "title": tr_noop("Icon Pack"), + "type": "value", + "get_value": lambda: self._params.get("IconPack", encoding='utf-8') or "Stock", + "on_click": lambda: self._show_theme_selector("IconPack"), + "color": "#A200FF", + }, + { + "title": tr_noop("Turn Signals"), + "type": "value", + "get_value": lambda: self._params.get("SignalAnimation", encoding='utf-8') or "Stock", + "on_click": lambda: self._show_theme_selector("SignalAnimation"), + "color": "#A200FF", + }, + { + "title": tr_noop("Sound Pack"), + "type": "value", + "get_value": lambda: self._params.get("SoundPack", encoding='utf-8') or "Stock", + "on_click": lambda: self._show_theme_selector("SoundPack"), + "color": "#A200FF", + }, + { + "title": tr_noop("Steering Wheel"), + "type": "value", + "get_value": lambda: self._params.get("WheelIcon", encoding='utf-8') or "Stock", + "on_click": lambda: self._show_theme_selector("WheelIcon"), + "color": "#A200FF", + }, ] self._rebuild_grid() @@ -80,10 +151,10 @@ class StarPilotPersonalizeLayout(StarPilotPanel): # For now, we'll provide a simplified selection based on current param. themes = ["Stock", "Frog", "Cyberpunk", "Minimal"] current = self._params.get(key, encoding='utf-8') or "Stock" - + def on_select(res, val): if res == DialogResult.CONFIRM: self._params.put(key, val) self._rebuild_grid() - + gui_app.set_modal_overlay(SelectionDialog(tr(key), themes, current, on_close=on_select)) diff --git a/selfdrive/ui/layouts/settings/starpilot/utilities.py b/selfdrive/ui/layouts/settings/starpilot/utilities.py index c37cffb7..22b5a64a 100644 --- a/selfdrive/ui/layouts/settings/starpilot/utilities.py +++ b/selfdrive/ui/layouts/settings/starpilot/utilities.py @@ -1,28 +1,77 @@ 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.selection_dialog import SelectionDialog +from openpilot.system.ui.widgets.input_dialog import InputDialog from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel +EXCLUDED_KEYS = { + "AvailableModels", + "AvailableModelNames", + "FrogPilotStats", + "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.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": "#FA6800"}, + { + "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": "#FA6800", + }, {"title": tr_noop("Flash Panda"), "type": "hub", "on_click": self._on_flash_panda, "color": "#FA6800"}, - {"title": tr_noop("Force Drive State"), "type": "value", "get_value": self._get_force_drive_state, "on_click": self._on_force_drive_state, "color": "#FA6800"}, - {"title": tr_noop("The Pond"), "type": "hub", "on_click": self._on_pond_clicked, "color": "#FA6800"}, + { + "title": tr_noop("Force Drive State"), + "type": "value", + "get_value": self._get_force_drive_state, + "on_click": self._on_force_drive_state, + "color": "#FA6800", + }, + { + "title": tr_noop("The Pond"), + "type": "value", + "get_value": lambda: tr("Paired") if self._params.get_bool("PondPaired") else tr("Not paired"), + "on_click": self._on_pond_clicked, + "color": "#FA6800", + }, {"title": tr_noop("Report Issue"), "type": "hub", "on_click": self._on_report_issue, "color": "#FA6800"}, - {"title": tr_noop("Reset Toggles"), "type": "hub", "on_click": self._on_reset_toggles, "color": "#FA6800"}, + {"title": tr_noop("Reset to Defaults"), "type": "hub", "on_click": self._on_reset_defaults, "color": "#FA6800"}, + {"title": tr_noop("Reset to Stock"), "type": "hub", "on_click": self._on_reset_stock, "color": "#FA6800"}, ] 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") + 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): @@ -30,10 +79,12 @@ class StarPilotUtilitiesLayout(StarPilotPanel): if res == DialogResult.CONFIRM: self._params_memory.put_bool("FlashPanda", True) gui_app.set_modal_overlay(alert_dialog(tr("Panda flashing started. Device will reboot when finished."))) + gui_app.set_modal_overlay(ConfirmDialog(tr("Flash Panda firmware?"), tr("Flash"), on_close=_do_flash)) def _on_force_drive_state(self): options = [tr("Offroad"), tr("Onroad"), tr("Default")] + def on_select(res, val): if res == DialogResult.CONFIRM: if val == tr("Offroad"): @@ -46,23 +97,67 @@ class StarPilotUtilitiesLayout(StarPilotPanel): self._params.put_bool("ForceOffroad", False) self._params.put_bool("ForceOnroad", False) self._rebuild_grid() + current = self._get_force_drive_state() gui_app.set_modal_overlay(SelectionDialog(tr("Force Drive State"), options, current, on_close=on_select)) def _on_pond_clicked(self): - gui_app.set_modal_overlay(alert_dialog(tr("The Pond pairing not yet implemented in Python."))) + paired = self._params.get_bool("PondPaired") + if paired: + + def on_unpair(res): + if res == DialogResult.CONFIRM: + self._params.put_bool("PondPaired", False) + gui_app.set_modal_overlay(alert_dialog(tr("Unpaired from The Pond."))) + self._rebuild_grid() + + gui_app.set_modal_overlay(ConfirmDialog(tr("Unpair from The Pond?"), tr("Unpair"), on_close=on_unpair)) + else: + gui_app.set_modal_overlay(alert_dialog(tr("Visit frogpilot.com/the_pond to pair your device."))) def _on_report_issue(self): - gui_app.set_modal_overlay(alert_dialog(tr("Issue reporting not yet implemented in Python."))) + def on_category(res, val): + if res != DialogResult.CONFIRM: + return + discord_user = self._params.get("DiscordUsername", encoding='utf-8') or "" - def _on_reset_toggles(self): + def on_discord(res2, username): + if res2 == DialogResult.CONFIRM and username: + self._params.put("DiscordUsername", username) + report = json.dumps({"DiscordUser": username, "Issue": val}) + self._params_memory.put("IssueReported", report) + gui_app.set_modal_overlay(alert_dialog(tr("Issue reported. Thank you!"))) + + gui_app.set_modal_overlay(InputDialog(tr("Discord Username"), discord_user or "", on_close=on_discord)) + + gui_app.set_modal_overlay(SelectionDialog(tr("Select Issue"), REPORT_CATEGORIES, on_close=on_category)) + + def _on_reset_defaults(self): def _do_reset(res): if res == DialogResult.CONFIRM: - # Simplified reset logic 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: self._params.put(k, default) - gui_app.set_modal_overlay(alert_dialog(tr("Toggles reset to default."))) + if default is not None: + self._params.put(k, default) + gui_app.set_modal_overlay(alert_dialog(tr("Toggles reset to defaults."))) self._rebuild_grid() - gui_app.set_modal_overlay(ConfirmDialog(tr("Reset all toggles to default?"), tr("Reset"), on_close=_do_reset)) + + gui_app.set_modal_overlay(ConfirmDialog(tr("Reset all toggles to defaults?"), tr("Reset"), on_close=_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.set_modal_overlay(alert_dialog(tr("Toggles reset to stock openpilot."))) + self._rebuild_grid() + + gui_app.set_modal_overlay(ConfirmDialog(tr("Reset all toggles to stock openpilot?"), tr("Reset"), on_close=_do_reset)) diff --git a/selfdrive/ui/layouts/settings/starpilot/vehicle.py b/selfdrive/ui/layouts/settings/starpilot/vehicle.py index 1f2545d7..1a4d5987 100644 --- a/selfdrive/ui/layouts/settings/starpilot/vehicle.py +++ b/selfdrive/ui/layouts/settings/starpilot/vehicle.py @@ -10,38 +10,64 @@ from openpilot.system.ui.widgets import DialogResult from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets.selection_dialog import SelectionDialog from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel -from openpilot.selfdrive.ui.layouts.settings.starpilot.metro import SliderDialog +from openpilot.selfdrive.ui.layouts.settings.starpilot.metro import SliderDialog, TileGrid, HubTile, ToggleTile, ValueTile +from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state MAKE_TO_FOLDER = { - "acura": "honda", "audi": "volkswagen", "buick": "gm", "cadillac": "gm", "chevrolet": "gm", - "chrysler": "chrysler", "cupra": "volkswagen", "dodge": "chrysler", "ford": "ford", - "genesis": "hyundai", "gmc": "gm", "holden": "gm", "honda": "honda", "hyundai": "hyundai", - "jeep": "chrysler", "kia": "hyundai", "lexus": "toyota", "lincoln": "ford", "man": "volkswagen", - "mazda": "mazda", "nissan": "nissan", "peugeot": "psa", "ram": "chrysler", "rivian": "rivian", - "seat": "volkswagen", "škoda": "volkswagen", "subaru": "subaru", "tesla": "tesla", - "toyota": "toyota", "volkswagen": "volkswagen" + "acura": "honda", + "audi": "volkswagen", + "buick": "gm", + "cadillac": "gm", + "chevrolet": "gm", + "chrysler": "chrysler", + "cupra": "volkswagen", + "dodge": "chrysler", + "ford": "ford", + "genesis": "hyundai", + "gmc": "gm", + "holden": "gm", + "honda": "honda", + "hyundai": "hyundai", + "jeep": "chrysler", + "kia": "hyundai", + "lexus": "toyota", + "lincoln": "ford", + "man": "volkswagen", + "mazda": "mazda", + "nissan": "nissan", + "peugeot": "psa", + "ram": "chrysler", + "rivian": "rivian", + "seat": "volkswagen", + "škoda": "volkswagen", + "subaru": "subaru", + "tesla": "tesla", + "toyota": "toyota", + "volkswagen": "volkswagen", } + def get_car_names(car_make: str): folder = MAKE_TO_FOLDER.get(car_make.lower()) - if not folder: return [], {} - + if not folder: + return [], {} + # Path to values.py in opendbc values_path = Path(__file__).parents[4] / "opendbc" / "car" / folder / "values.py" if not values_path.exists(): return [], {} - + with open(values_path, "r") as f: content = f.read() - + # Clean comments content = re.sub(r'#.*', '', content) - + # Find platforms and car names platforms = re.findall(r'(\w+)\s*=\s*\w+\s*\(', content) car_models = {} car_names = [] - + # This is a simplified version of the C++ regex logic # In values.py, CarDocs often appears as CarDocs("Name", ...) matches = re.finditer(r'CarDocs\w*\s*\(\s*"([^"]+)"', content) @@ -51,9 +77,17 @@ def get_car_names(car_make: str): # Find the platform name by looking backwards for the nearest platform assignment # For now, we'll just store the name car_names.append(name) - + return sorted(list(set(car_names))), car_models + +def _lock_doors_timer_labels(): + labels: dict[float, str] = {0.0: tr("Never")} + for i in range(5, 305, 5): + labels[float(i)] = f"{i}s" + return labels + + class StarPilotVehicleSettingsLayout(StarPilotPanel): def __init__(self): super().__init__() @@ -64,46 +98,108 @@ class StarPilotVehicleSettingsLayout(StarPilotPanel): "toyota": StarPilotToyotaVehicleLayout(), "info": StarPilotVehicleInfoLayout(), } - + self.CATEGORIES = [ { "title": tr_noop("Car Make"), "type": "value", "get_value": lambda: self._params.get("CarMake", encoding='utf-8') or tr("None"), "on_click": self._on_select_make, - "color": "#FFC40D" + "color": "#FFC40D", }, { "title": tr_noop("Car Model"), "type": "value", "get_value": lambda: self._params.get("CarModelName", encoding='utf-8') or tr("None"), "on_click": self._on_select_model, - "color": "#FFC40D" + "color": "#FFC40D", }, - {"title": tr_noop("Disable Fingerprinting"), "type": "toggle", "get_state": lambda: self._params.get_bool("ForceFingerprint"), "set_state": lambda s: self._params.put_bool("ForceFingerprint", s), "color": "#FFC40D"}, - {"title": tr_noop("Disable openpilot Long"), "type": "toggle", "get_state": lambda: self._params.get_bool("DisableOpenpilotLongitudinal"), "set_state": self._on_disable_long, "color": "#FFC40D"}, - {"title": tr_noop("GM Settings"), "panel": "gm", "icon": "toggle_icons/icon_vehicle.png", "color": "#FFC40D"}, - {"title": tr_noop("HKG Settings"), "panel": "hkg", "icon": "toggle_icons/icon_vehicle.png", "color": "#FFC40D"}, - {"title": tr_noop("Subaru Settings"), "panel": "subaru", "icon": "toggle_icons/icon_vehicle.png", "color": "#FFC40D"}, - {"title": tr_noop("Toyota Settings"), "panel": "toyota", "icon": "toggle_icons/icon_vehicle.png", "color": "#FFC40D"}, + { + "title": tr_noop("Disable Fingerprinting"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("ForceFingerprint"), + "set_state": lambda s: self._params.put_bool("ForceFingerprint", s), + "color": "#FFC40D", + }, + { + "title": tr_noop("Disable openpilot Long"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("DisableOpenpilotLongitudinal"), + "set_state": self._on_disable_long, + "color": "#FFC40D", + }, + {"title": tr_noop("GM Settings"), "panel": "gm", "icon": "toggle_icons/icon_vehicle.png", "color": "#FFC40D", "key": "gm"}, + {"title": tr_noop("HKG Settings"), "panel": "hkg", "icon": "toggle_icons/icon_vehicle.png", "color": "#FFC40D", "key": "hkg"}, + {"title": tr_noop("Subaru Settings"), "panel": "subaru", "icon": "toggle_icons/icon_vehicle.png", "color": "#FFC40D", "key": "subaru"}, + {"title": tr_noop("Toyota Settings"), "panel": "toyota", "icon": "toggle_icons/icon_vehicle.png", "color": "#FFC40D", "key": "toyota"}, {"title": tr_noop("Vehicle Info"), "panel": "info", "icon": "toggle_icons/icon_vehicle.png", "color": "#FFC40D"}, ] - + 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) - + 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): + if not self.CATEGORIES: + return + if self._tile_grid is None: + self._tile_grid = TileGrid(columns=None, padding=20) + self._tile_grid.clear() + + cs = starpilot_state.car_state + for cat in self.CATEGORIES: + key = cat.get("key") + visible = True + + if key == "gm": + visible = cs.isGM + elif key == "hkg": + visible = cs.isHKG + elif key == "subaru": + visible = cs.isSubaru + elif key == "toyota": + visible = cs.isToyota + + if not visible: + continue + + tile_type = cat.get("type", "hub") + if tile_type == "hub": + on_click = cat.get("on_click") + if on_click is None: + on_click = lambda c=cat: self._navigate_to(c["panel"]) + tile = HubTile( + title=tr(cat["title"]), + desc=tr(cat.get("desc", "")), + icon_path=cat.get("icon"), + on_click=on_click, + starpilot_icon=cat.get("starpilot_icon", True), + bg_color=cat.get("color"), + ) + elif tile_type == "toggle": + 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")) + elif tile_type == "value": + tile = ValueTile(title=tr(cat["title"]), get_value=cat["get_value"], on_click=cat["on_click"], icon_path=cat.get("icon"), bg_color=cat.get("color")) + else: + continue + + self._tile_grid.add_tile(tile) + def _on_select_make(self): makes = sorted(list(MAKE_TO_FOLDER.keys())) makes = [m.capitalize() for m in makes] + def on_select(res, val): if res == DialogResult.CONFIRM: self._params.put("CarMake", val) self._params.remove("CarModel") self._params.remove("CarModelName") self._rebuild_grid() + gui_app.set_modal_overlay(SelectionDialog(tr("Select Make"), makes, self._params.get("CarMake", encoding='utf-8') or "", on_close=on_select)) def _on_select_model(self): @@ -111,7 +207,7 @@ class StarPilotVehicleSettingsLayout(StarPilotPanel): if not make: gui_app.set_modal_overlay(ConfirmDialog(tr("Please select a Car Make first!"), tr("OK"), on_close=lambda r: None)) return - + models, _ = get_car_names(make) if not models: gui_app.set_modal_overlay(ConfirmDialog(tr("No models found for this make."), tr("OK"), on_close=lambda r: None)) @@ -122,71 +218,299 @@ class StarPilotVehicleSettingsLayout(StarPilotPanel): self._params.put("CarModelName", val) # In a real build we'd map name to platform code here self._rebuild_grid() + gui_app.set_modal_overlay(SelectionDialog(tr("Select Model"), models, self._params.get("CarModelName", encoding='utf-8') or "", on_close=on_select)) def _on_disable_long(self, state): if state: + def on_confirm(res): if res == DialogResult.CONFIRM: self._params.put_bool("DisableOpenpilotLongitudinal", True) from openpilot.selfdrive.ui.ui_state import ui_state - if ui_state.started: HARDWARE.reboot() + + if ui_state.started: + HARDWARE.reboot() self._rebuild_grid() + gui_app.set_modal_overlay(ConfirmDialog(tr("Disable openpilot longitudinal control?"), tr("Disable"), on_close=on_confirm)) else: self._params.put_bool("DisableOpenpilotLongitudinal", False) self._rebuild_grid() + class StarPilotGMVehicleLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - {"title": tr_noop("Pedal for Long"), "type": "toggle", "get_state": lambda: self._params.get_bool("GMPedalLongitudinal"), "set_state": lambda s: self._params.put_bool("GMPedalLongitudinal", s), "color": "#FFC40D"}, - {"title": tr_noop("Remote Start Panda"), "type": "toggle", "get_state": lambda: self._params.get_bool("RemoteStartBootsComma"), "set_state": lambda s: self._params.put_bool("RemoteStartBootsComma", s), "color": "#FFC40D"}, - {"title": tr_noop("Volt SNG Hack"), "type": "toggle", "get_state": lambda: self._params.get_bool("VoltSNG"), "set_state": lambda s: self._params.put_bool("VoltSNG", s), "color": "#FFC40D"}, + { + "title": tr_noop("Pedal for Long"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("GMPedalLongitudinal"), + "set_state": lambda s: self._params.put_bool("GMPedalLongitudinal", s), + "color": "#FFC40D", + "key": "GMPedalLongitudinal", + }, + { + "title": tr_noop("Remote Start Panda"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("RemoteStartBootsComma"), + "set_state": lambda s: self._params.put_bool("RemoteStartBootsComma", s), + "color": "#FFC40D", + }, + { + "title": tr_noop("Volt SNG Hack"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("VoltSNG"), + "set_state": lambda s: self._params.put_bool("VoltSNG", s), + "color": "#FFC40D", + "key": "VoltSNG", + }, ] self._rebuild_grid() + def _rebuild_grid(self): + if not self.CATEGORIES: + return + if self._tile_grid is None: + self._tile_grid = TileGrid(columns=None, padding=20) + self._tile_grid.clear() + + cs = starpilot_state.car_state + for cat in self.CATEGORIES: + key = cat.get("key") + visible = True + + if key == "GMPedalLongitudinal": + visible = cs.hasPedal or cs.canUsePedal + elif key == "VoltSNG": + visible = cs.isVolt and not cs.hasSNG + + if not visible: + continue + + 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")) + self._tile_grid.add_tile(tile) + + class StarPilotHKGVehicleLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - {"title": tr_noop("Taco Bell Torque Hack"), "type": "toggle", "get_state": lambda: self._params.get_bool("TacoTuneHacks"), "set_state": lambda s: self._params.put_bool("TacoTuneHacks", s), "color": "#FFC40D"}, + { + "title": tr_noop("Taco Bell Torque Hack"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("TacoTuneHacks"), + "set_state": lambda s: self._params.put_bool("TacoTuneHacks", s), + "color": "#FFC40D", + "key": "TacoTuneHacks", + }, ] self._rebuild_grid() + def _rebuild_grid(self): + if not self.CATEGORIES: + return + if self._tile_grid is None: + self._tile_grid = TileGrid(columns=None, padding=20) + self._tile_grid.clear() + + cs = starpilot_state.car_state + for cat in self.CATEGORIES: + key = cat.get("key") + visible = True + + if key == "TacoTuneHacks": + visible = cs.isHKGCanFd + + if not visible: + continue + + 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")) + self._tile_grid.add_tile(tile) + + class StarPilotSubaruVehicleLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - {"title": tr_noop("Stop and Go"), "type": "toggle", "get_state": lambda: self._params.get_bool("SubaruSNG"), "set_state": lambda s: self._params.put_bool("SubaruSNG", s), "color": "#FFC40D"}, + { + "title": tr_noop("Stop and Go"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("SubaruSNG"), + "set_state": lambda s: self._params.put_bool("SubaruSNG", s), + "color": "#FFC40D", + }, ] self._rebuild_grid() + class StarPilotToyotaVehicleLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - {"title": tr_noop("Auto Lock Doors"), "type": "toggle", "get_state": lambda: self._params.get_bool("LockDoors"), "set_state": lambda s: self._params.put_bool("LockDoors", s), "color": "#FFC40D"}, - {"title": tr_noop("Auto Unlock Doors"), "type": "toggle", "get_state": lambda: self._params.get_bool("UnlockDoors"), "set_state": lambda s: self._params.put_bool("UnlockDoors", s), "color": "#FFC40D"}, - {"title": tr_noop("Dashboard Speed Offset"), "type": "value", "get_value": lambda: f"{self._params.get_float('ClusterOffset'):.3f}x", "on_click": self._show_offset_selector, "color": "#FFC40D"}, - {"title": tr_noop("Stop-and-Go Hack"), "type": "toggle", "get_state": lambda: self._params.get_bool("SNGHack"), "set_state": lambda s: self._params.put_bool("SNGHack", s), "color": "#FFC40D"}, + { + "title": tr_noop("Auto Lock Doors"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("LockDoors"), + "set_state": lambda s: self._params.put_bool("LockDoors", s), + "color": "#FFC40D", + }, + { + "title": tr_noop("Auto Unlock Doors"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("UnlockDoors"), + "set_state": lambda s: self._params.put_bool("UnlockDoors", s), + "color": "#FFC40D", + }, + { + "title": tr_noop("Lock Doors Timer"), + "type": "value", + "get_value": lambda: _lock_doors_timer_labels().get(self._params.get_int('LockDoorsTimer'), f"{self._params.get_int('LockDoorsTimer')}s"), + "on_click": self._show_lock_timer_selector, + "color": "#FFC40D", + }, + { + "title": tr_noop("Dashboard Speed Offset"), + "type": "value", + "get_value": lambda: f"{self._params.get_float('ClusterOffset'):.3f}x", + "on_click": self._show_offset_selector, + "color": "#FFC40D", + }, + { + "title": tr_noop("Stop-and-Go Hack"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("SNGHack"), + "set_state": lambda s: self._params.put_bool("SNGHack", s), + "color": "#FFC40D", + "key": "SNGHack", + }, + { + "title": tr_noop("FrogsGoMoo Tweak"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("FrogsGoMoosTweak"), + "set_state": lambda s: self._params.put_bool("FrogsGoMoosTweak", s), + "color": "#FFC40D", + "key": "FrogsGoMoosTweak", + }, ] self._rebuild_grid() + def _rebuild_grid(self): + if not self.CATEGORIES: + return + if self._tile_grid is None: + self._tile_grid = TileGrid(columns=None, padding=20) + self._tile_grid.clear() + + cs = starpilot_state.car_state + for cat in self.CATEGORIES: + key = cat.get("key") + visible = True + + if key == "SNGHack": + visible = not cs.hasSNG + elif key == "FrogsGoMoosTweak": + visible = cs.hasOpenpilotLongitudinal + + if not visible: + continue + + tile_type = cat.get("type", "hub") + if tile_type == "toggle": + 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")) + elif tile_type == "value": + tile = ValueTile(title=tr(cat["title"]), get_value=cat["get_value"], on_click=cat["on_click"], icon_path=cat.get("icon"), bg_color=cat.get("color")) + else: + continue + + self._tile_grid.add_tile(tile) + + def _show_lock_timer_selector(self): + def on_close(res, val): + if res == DialogResult.CONFIRM: + self._params.put_int("LockDoorsTimer", int(val)) + self._rebuild_grid() + + gui_app.set_modal_overlay( + SliderDialog(tr("Lock Doors Timer"), 0, 300, 5, self._params.get_int("LockDoorsTimer"), on_close, labels=_lock_doors_timer_labels(), color="#FFC40D") + ) + def _show_offset_selector(self): def on_close(res, val): if res == DialogResult.CONFIRM: self._params.put_float("ClusterOffset", float(val)) self._rebuild_grid() - gui_app.set_modal_overlay(SliderDialog(tr("Dashboard Speed Offset"), 1.000, 1.050, 0.001, self._params.get_float("ClusterOffset"), on_close, unit="x", color="#FFC40D")) + + gui_app.set_modal_overlay( + SliderDialog(tr("Dashboard Speed Offset"), 1.000, 1.050, 0.001, self._params.get_float("ClusterOffset"), on_close, unit="x", color="#FFC40D") + ) + class StarPilotVehicleInfoLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - {"title": tr_noop("Radar Support"), "type": "value", "get_value": lambda: tr("Yes") if starpilot_state.car_state.hasRadar else tr("No"), "on_click": lambda: None, "color": "#FFC40D"}, - {"title": tr_noop("Longitudinal Support"), "type": "value", "get_value": lambda: tr("Yes") if starpilot_state.car_state.hasOpenpilotLongitudinal else tr("No"), "on_click": lambda: None, "color": "#FFC40D"}, - {"title": tr_noop("Blind Spot Support"), "type": "value", "get_value": lambda: tr("Yes") if starpilot_state.car_state.hasBSM else tr("No"), "on_click": lambda: None, "color": "#FFC40D"}, + { + "title": tr_noop("Radar Support"), + "type": "value", + "get_value": lambda: tr("Yes") if starpilot_state.car_state.hasRadar else tr("No"), + "on_click": lambda: None, + "color": "#FFC40D", + }, + { + "title": tr_noop("Longitudinal Support"), + "type": "value", + "get_value": lambda: tr("Yes") if starpilot_state.car_state.hasOpenpilotLongitudinal else tr("No"), + "on_click": lambda: None, + "color": "#FFC40D", + }, + { + "title": tr_noop("Blind Spot Support"), + "type": "value", + "get_value": lambda: tr("Yes") if starpilot_state.car_state.hasBSM else tr("No"), + "on_click": lambda: None, + "color": "#FFC40D", + }, + { + "title": tr_noop("Hardware Detected"), + "type": "value", + "get_value": lambda: ( + ", ".join( + filter( + None, + [ + tr("Pedal") if starpilot_state.car_state.canUsePedal else "", + tr("SDSU") if starpilot_state.car_state.canUseSDSU else "", + tr("ZSS") if starpilot_state.car_state.hasZSS else "", + ], + ) + ) + or tr("None") + ), + "on_click": lambda: None, + "color": "#FFC40D", + }, + { + "title": tr_noop("Pedal Support"), + "type": "value", + "get_value": lambda: tr("Yes") if starpilot_state.car_state.canUsePedal else tr("No"), + "on_click": lambda: None, + "color": "#FFC40D", + }, + { + "title": tr_noop("SDSU Support"), + "type": "value", + "get_value": lambda: tr("Yes") if starpilot_state.car_state.canUseSDSU else tr("No"), + "on_click": lambda: None, + "color": "#FFC40D", + }, + { + "title": tr_noop("SNG Support"), + "type": "value", + "get_value": lambda: tr("Yes") if starpilot_state.car_state.hasSNG else tr("No"), + "on_click": lambda: None, + "color": "#FFC40D", + }, ] self._rebuild_grid() diff --git a/selfdrive/ui/layouts/settings/starpilot/visuals.py b/selfdrive/ui/layouts/settings/starpilot/visuals.py index 5958efe6..077d8940 100644 --- a/selfdrive/ui/layouts/settings/starpilot/visuals.py +++ b/selfdrive/ui/layouts/settings/starpilot/visuals.py @@ -1,10 +1,12 @@ from __future__ import annotations +from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state 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.selection_dialog import SelectionDialog from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel -from openpilot.selfdrive.ui.layouts.settings.starpilot.metro import SliderDialog +from openpilot.selfdrive.ui.layouts.settings.starpilot.metro import TileGrid, ToggleTile, SliderDialog + class StarPilotThemesLayout(StarPilotPanel): def __init__(self): @@ -14,16 +16,47 @@ class StarPilotThemesLayout(StarPilotPanel): } self.CATEGORIES = [ {"title": tr_noop("Personalize openpilot"), "panel": "personalize", "icon": "toggle_icons/icon_frog.png", "color": "#A200FF"}, - {"title": tr_noop("Holiday Themes"), "type": "toggle", "get_state": lambda: self._params.get_bool("HolidayThemes"), "set_state": lambda s: self._params.put_bool("HolidayThemes", s), "icon": "toggle_icons/icon_calendar.png", "color": "#A200FF"}, - {"title": tr_noop("Rainbow Path"), "type": "toggle", "get_state": lambda: self._params.get_bool("RainbowPath"), "set_state": lambda s: self._params.put_bool("RainbowPath", s), "icon": "toggle_icons/icon_rainbow.png", "color": "#A200FF"}, - {"title": tr_noop("Random Events"), "type": "toggle", "get_state": lambda: self._params.get_bool("RandomEvents"), "set_state": lambda s: self._params.put_bool("RandomEvents", s), "icon": "toggle_icons/icon_random.png", "color": "#A200FF"}, - {"title": tr_noop("Random Themes"), "type": "toggle", "get_state": lambda: self._params.get_bool("RandomThemes"), "set_state": lambda s: self._params.put_bool("RandomThemes", s), "icon": "toggle_icons/icon_random_themes.png", "color": "#A200FF"}, + { + "title": tr_noop("Holiday Themes"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("HolidayThemes"), + "set_state": lambda s: self._params.put_bool("HolidayThemes", s), + "icon": "toggle_icons/icon_calendar.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Rainbow Path"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("RainbowPath"), + "set_state": lambda s: self._params.put_bool("RainbowPath", s), + "icon": "toggle_icons/icon_rainbow.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Random Events"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("RandomEvents"), + "set_state": lambda s: self._params.put_bool("RandomEvents", s), + "icon": "toggle_icons/icon_random.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Random Themes"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("RandomThemes"), + "set_state": lambda s: self._params.put_bool("RandomThemes", s), + "icon": "toggle_icons/icon_random_themes.png", + "color": "#A200FF", + }, ] 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) + 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() + class StarPilotPersonalizeLayout(StarPilotPanel): def __init__(self): super().__init__() @@ -41,12 +74,15 @@ class StarPilotPersonalizeLayout(StarPilotPanel): def _show_theme_selector(self, key): themes = ["Stock", "Frog", "Cyberpunk", "Minimal"] current = self._params.get(key, encoding='utf-8') or "Stock" + def on_select(res, val): if res == DialogResult.CONFIRM: self._params.put(key, val) self._rebuild_grid() + gui_app.set_modal_overlay(SelectionDialog(tr(key), themes, current, on_close=on_select)) + class StarPilotVisualsLayout(StarPilotPanel): def __init__(self): super().__init__() @@ -65,80 +101,409 @@ class StarPilotVisualsLayout(StarPilotPanel): {"title": tr_noop("Quality of Life"), "panel": "qol", "icon": "toggle_icons/icon_quality_of_life.png", "color": "#A200FF"}, ] 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) + 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() + class StarPilotAdvancedVisualsLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - {"title": tr_noop("Hide Speed"), "type": "toggle", "get_state": lambda: self._params.get_bool("HideSpeed"), "set_state": lambda s: self._params.put_bool("HideSpeed", s), "icon": "toggle_icons/icon_display.png", "color": "#A200FF"}, - {"title": tr_noop("Hide Lead Marker"), "type": "toggle", "get_state": lambda: self._params.get_bool("HideLeadMarker"), "set_state": lambda s: self._params.put_bool("HideLeadMarker", s), "icon": "toggle_icons/icon_display.png", "color": "#A200FF"}, - {"title": tr_noop("Hide Max Speed"), "type": "toggle", "get_state": lambda: self._params.get_bool("HideMaxSpeed"), "set_state": lambda s: self._params.put_bool("HideMaxSpeed", s), "icon": "toggle_icons/icon_display.png", "color": "#A200FF"}, - {"title": tr_noop("Hide Alerts"), "type": "toggle", "get_state": lambda: self._params.get_bool("HideAlerts"), "set_state": lambda s: self._params.put_bool("HideAlerts", s), "icon": "toggle_icons/icon_display.png", "color": "#A200FF"}, - {"title": tr_noop("Hide Speed Limit"), "type": "toggle", "get_state": lambda: self._params.get_bool("HideSpeedLimit"), "set_state": lambda s: self._params.put_bool("HideSpeedLimit", s), "icon": "toggle_icons/icon_display.png", "color": "#A200FF"}, - {"title": tr_noop("Wheel Speed"), "type": "toggle", "get_state": lambda: self._params.get_bool("WheelSpeed"), "set_state": lambda s: self._params.put_bool("WheelSpeed", s), "icon": "toggle_icons/icon_display.png", "color": "#A200FF"}, + { + "title": tr_noop("Hide Speed"), + "type": "toggle", + "key": "HideSpeed", + "get_state": lambda: self._params.get_bool("HideSpeed"), + "set_state": lambda s: self._params.put_bool("HideSpeed", s), + "icon": "toggle_icons/icon_display.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Hide Lead Marker"), + "type": "toggle", + "key": "HideLeadMarker", + "get_state": lambda: self._params.get_bool("HideLeadMarker"), + "set_state": lambda s: self._params.put_bool("HideLeadMarker", s), + "icon": "toggle_icons/icon_display.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Hide Max Speed"), + "type": "toggle", + "key": "HideMaxSpeed", + "get_state": lambda: self._params.get_bool("HideMaxSpeed"), + "set_state": lambda s: self._params.put_bool("HideMaxSpeed", s), + "icon": "toggle_icons/icon_display.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Hide Alerts"), + "type": "toggle", + "key": "HideAlerts", + "get_state": lambda: self._params.get_bool("HideAlerts"), + "set_state": lambda s: self._params.put_bool("HideAlerts", s), + "icon": "toggle_icons/icon_display.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Hide Speed Limit"), + "type": "toggle", + "key": "HideSpeedLimit", + "get_state": lambda: self._params.get_bool("HideSpeedLimit"), + "set_state": lambda s: self._params.put_bool("HideSpeedLimit", s), + "icon": "toggle_icons/icon_display.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Wheel Speed"), + "type": "toggle", + "key": "WheelSpeed", + "get_state": lambda: self._params.get_bool("WheelSpeed"), + "set_state": lambda s: self._params.put_bool("WheelSpeed", s), + "icon": "toggle_icons/icon_display.png", + "color": "#A200FF", + }, ] self._rebuild_grid() + def _rebuild_grid(self): + if not self.CATEGORIES: + return + if self._tile_grid is None: + self._tile_grid = TileGrid(columns=None, padding=20) + self._tile_grid.clear() + + for cat in self.CATEGORIES: + key = cat.get("key") + visible = True + + if key == "HideLeadMarker": + visible &= starpilot_state.car_state.hasOpenpilotLongitudinal + + if not visible: + continue + + 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")) + self._tile_grid.add_tile(tile) + + class StarPilotVisualWidgetsLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - {"title": tr_noop("Acceleration Path"), "type": "toggle", "get_state": lambda: self._params.get_bool("AccelerationPath"), "set_state": lambda s: self._params.put_bool("AccelerationPath", s), "icon": "toggle_icons/icon_road.png", "color": "#A200FF"}, - {"title": tr_noop("Adjacent Lanes"), "type": "toggle", "get_state": lambda: self._params.get_bool("AdjacentPath"), "set_state": lambda s: self._params.put_bool("AdjacentPath", s), "icon": "toggle_icons/icon_road.png", "color": "#A200FF"}, - {"title": tr_noop("Blind Spot Path"), "type": "toggle", "get_state": lambda: self._params.get_bool("BlindSpotPath"), "set_state": lambda s: self._params.put_bool("BlindSpotPath", s), "icon": "toggle_icons/icon_road.png", "color": "#A200FF"}, - {"title": tr_noop("Compass"), "type": "toggle", "get_state": lambda: self._params.get_bool("Compass"), "set_state": lambda s: self._params.put_bool("Compass", s), "icon": "toggle_icons/icon_navigate.png", "color": "#A200FF"}, - {"title": tr_noop("Personality Button"), "type": "toggle", "get_state": lambda: self._params.get_bool("OnroadDistanceButton"), "set_state": lambda s: self._params.put_bool("OnroadDistanceButton", s), "icon": "toggle_icons/icon_personality.png", "color": "#A200FF"}, - {"title": tr_noop("Pedal Indicators"), "type": "toggle", "get_state": lambda: self._params.get_bool("PedalsOnUI"), "set_state": lambda s: self._params.put_bool("PedalsOnUI", s), "icon": "toggle_icons/icon_display.png", "color": "#A200FF"}, - {"title": tr_noop("Rotating Wheel"), "type": "toggle", "get_state": lambda: self._params.get_bool("RotatingWheel"), "set_state": lambda s: self._params.put_bool("RotatingWheel", s), "icon": "toggle_icons/icon_steering.png", "color": "#A200FF"}, + { + "title": tr_noop("Acceleration Path"), + "type": "toggle", + "key": "AccelerationPath", + "get_state": lambda: self._params.get_bool("AccelerationPath"), + "set_state": lambda s: self._params.put_bool("AccelerationPath", s), + "icon": "toggle_icons/icon_road.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Adjacent Lanes"), + "type": "toggle", + "key": "AdjacentPath", + "get_state": lambda: self._params.get_bool("AdjacentPath"), + "set_state": lambda s: self._params.put_bool("AdjacentPath", s), + "icon": "toggle_icons/icon_road.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Blind Spot Path"), + "type": "toggle", + "key": "BlindSpotPath", + "get_state": lambda: self._params.get_bool("BlindSpotPath"), + "set_state": lambda s: self._params.put_bool("BlindSpotPath", s), + "icon": "toggle_icons/icon_road.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Compass"), + "type": "toggle", + "key": "Compass", + "get_state": lambda: self._params.get_bool("Compass"), + "set_state": lambda s: self._params.put_bool("Compass", s), + "icon": "toggle_icons/icon_navigate.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Personality Button"), + "type": "toggle", + "key": "OnroadDistanceButton", + "get_state": lambda: self._params.get_bool("OnroadDistanceButton"), + "set_state": lambda s: self._params.put_bool("OnroadDistanceButton", s), + "icon": "toggle_icons/icon_personality.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Pedal Indicators"), + "type": "toggle", + "key": "PedalsOnUI", + "get_state": lambda: self._params.get_bool("PedalsOnUI"), + "set_state": lambda s: self._params.put_bool("PedalsOnUI", s), + "icon": "toggle_icons/icon_display.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Dynamic Pedals"), + "type": "toggle", + "key": "DynamicPedalsOnUI", + "get_state": lambda: self._params.get_bool("DynamicPedalsOnUI"), + "set_state": lambda s: self._set_exclusive_pedal("DynamicPedalsOnUI", "StaticPedalsOnUI", s), + "icon": "toggle_icons/icon_display.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Static Pedals"), + "type": "toggle", + "key": "StaticPedalsOnUI", + "get_state": lambda: self._params.get_bool("StaticPedalsOnUI"), + "set_state": lambda s: self._set_exclusive_pedal("StaticPedalsOnUI", "DynamicPedalsOnUI", s), + "icon": "toggle_icons/icon_display.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Rotating Wheel"), + "type": "toggle", + "key": "RotatingWheel", + "get_state": lambda: self._params.get_bool("RotatingWheel"), + "set_state": lambda s: self._params.put_bool("RotatingWheel", s), + "icon": "toggle_icons/icon_steering.png", + "color": "#A200FF", + }, ] self._rebuild_grid() + def _set_exclusive_pedal(self, key, other_key, state): + self._params.put_bool(key, state) + if state: + self._params.put_bool(other_key, False) + self._rebuild_grid() + + def _rebuild_grid(self): + if not self.CATEGORIES: + return + if self._tile_grid is None: + self._tile_grid = TileGrid(columns=None, padding=20) + self._tile_grid.clear() + + pedals_on_ui = self._params.get_bool("PedalsOnUI") + + for cat in self.CATEGORIES: + key = cat.get("key") + visible = True + + if key == "AccelerationPath": + visible &= starpilot_state.car_state.hasOpenpilotLongitudinal + elif key == "BlindSpotPath": + visible &= starpilot_state.car_state.hasBSM + elif key == "PedalsOnUI": + visible &= starpilot_state.car_state.hasOpenpilotLongitudinal + elif key == "DynamicPedalsOnUI": + visible &= starpilot_state.car_state.hasOpenpilotLongitudinal and pedals_on_ui + elif key == "StaticPedalsOnUI": + visible &= starpilot_state.car_state.hasOpenpilotLongitudinal and pedals_on_ui + + if not visible: + continue + + 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")) + self._tile_grid.add_tile(tile) + + class StarPilotModelUILayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - {"title": tr_noop("Dynamic Path"), "type": "toggle", "get_state": lambda: self._params.get_bool("DynamicPathWidth"), "set_state": lambda s: self._params.put_bool("DynamicPathWidth", s), "icon": "toggle_icons/icon_road.png", "color": "#A200FF"}, - {"title": tr_noop("Lane Line Width"), "type": "value", "get_value": lambda: f"{self._params.get_int('LaneLinesWidth')}in", "on_click": lambda: self._show_int_selector("LaneLinesWidth", 0, 24, "in"), "icon": "toggle_icons/icon_road.png", "color": "#A200FF"}, - {"title": tr_noop("Path Edge Width"), "type": "value", "get_value": lambda: f"{self._params.get_int('PathEdgeWidth')}%", "on_click": lambda: self._show_int_selector("PathEdgeWidth", 0, 100, "%"), "icon": "toggle_icons/icon_road.png", "color": "#A200FF"}, - {"title": tr_noop("Path Width"), "type": "value", "get_value": lambda: f"{self._params.get_float('PathWidth'):.1f}ft", "on_click": lambda: self._show_float_selector("PathWidth", 0, 10, 0.1, "ft"), "icon": "toggle_icons/icon_road.png", "color": "#A200FF"}, - {"title": tr_noop("Road Edge Width"), "type": "value", "get_value": lambda: f"{self._params.get_int('RoadEdgesWidth')}in", "on_click": lambda: self._show_int_selector("RoadEdgesWidth", 0, 24, "in"), "icon": "toggle_icons/icon_road.png", "color": "#A200FF"}, + { + "title": tr_noop("Dynamic Path"), + "type": "toggle", + "key": "DynamicPathWidth", + "get_state": lambda: self._params.get_bool("DynamicPathWidth"), + "set_state": lambda s: self._params.put_bool("DynamicPathWidth", s), + "icon": "toggle_icons/icon_road.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Lane Line Width"), + "type": "value", + "key": "LaneLinesWidth", + "get_value": lambda: self._get_lane_lines_display(), + "on_click": lambda: self._show_int_selector("LaneLinesWidth", 0, 24, self._get_lane_lines_unit()), + "icon": "toggle_icons/icon_road.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Path Edge Width"), + "type": "value", + "key": "PathEdgeWidth", + "get_value": lambda: f"{self._params.get_int('PathEdgeWidth')}%", + "on_click": lambda: self._show_int_selector("PathEdgeWidth", 0, 100, "%"), + "icon": "toggle_icons/icon_road.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Path Width"), + "type": "value", + "key": "PathWidth", + "get_value": lambda: self._get_path_width_display(), + "on_click": lambda: self._show_path_width_selector(), + "icon": "toggle_icons/icon_road.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Road Edge Width"), + "type": "value", + "key": "RoadEdgesWidth", + "get_value": lambda: self._get_road_edges_display(), + "on_click": lambda: self._show_int_selector("RoadEdgesWidth", 0, 24, self._get_road_edges_unit()), + "icon": "toggle_icons/icon_road.png", + "color": "#A200FF", + }, ] self._rebuild_grid() + def _get_lane_lines_unit(self): + return "cm" if self._params.get_bool("IsMetric") else "in" + + def _get_lane_lines_display(self): + val = self._params.get_int('LaneLinesWidth') + if self._params.get_bool("IsMetric"): + return f"{int(val * 2.54)}cm" + return f"{val}in" + + def _get_path_width_unit(self): + return "m" if self._params.get_bool("IsMetric") else "ft" + + def _get_path_width_display(self): + val = self._params.get_float('PathWidth') + if self._params.get_bool("IsMetric"): + return f"{val / 3.28084:.1f}m" + return f"{val:.1f}ft" + + def _get_road_edges_unit(self): + return "cm" if self._params.get_bool("IsMetric") else "in" + + def _get_road_edges_display(self): + val = self._params.get_int('RoadEdgesWidth') + if self._params.get_bool("IsMetric"): + return f"{int(val * 2.54)}cm" + return f"{val}in" + + def _show_path_width_selector(self): + if self._params.get_bool("IsMetric"): + self._show_float_selector("PathWidth", 0, 10, 0.1, "m", convert=lambda v: v / 3.28084, unconvert=lambda v: v * 3.28084) + else: + self._show_float_selector("PathWidth", 0, 10, 0.1, "ft") + def _show_int_selector(self, key, min_v, max_v, unit=""): def on_close(res, val): if res == DialogResult.CONFIRM: self._params.put_int(key, int(val)) self._rebuild_grid() + gui_app.set_modal_overlay(SliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#A200FF")) - def _show_float_selector(self, key, min_v, max_v, step, unit=""): + def _show_float_selector(self, key, min_v, max_v, step, unit="", convert=None, unconvert=None): + current = self._params.get_float(key) + if convert: + current = convert(current) + def on_close(res, val): if res == DialogResult.CONFIRM: - self._params.put_float(key, float(val)) + v = float(val) + if unconvert: + v = unconvert(v) + self._params.put_float(key, v) self._rebuild_grid() - gui_app.set_modal_overlay(SliderDialog(tr(key), min_v, max_v, step, self._params.get_float(key), on_close, unit=unit, color="#A200FF")) + + gui_app.set_modal_overlay(SliderDialog(tr(key), min_v, max_v, step, current, on_close, unit=unit, color="#A200FF")) + class StarPilotNavigationVisualsLayout(StarPilotPanel): def __init__(self): super().__init__() self.CATEGORIES = [ - {"title": tr_noop("Road Name"), "type": "toggle", "get_state": lambda: self._params.get_bool("RoadNameUI"), "set_state": lambda s: self._params.put_bool("RoadNameUI", s), "icon": "toggle_icons/icon_navigate.png", "color": "#A200FF"}, - {"title": tr_noop("Speed Limits"), "type": "toggle", "get_state": lambda: self._params.get_bool("ShowSpeedLimits"), "set_state": lambda s: self._params.put_bool("ShowSpeedLimits", s), "icon": "toggle_icons/icon_speed_limit.png", "color": "#A200FF"}, - {"title": tr_noop("Mapbox Limits"), "type": "toggle", "get_state": lambda: self._params.get_bool("SLCMapboxFiller"), "set_state": lambda s: self._params.put_bool("SLCMapboxFiller", s), "icon": "toggle_icons/icon_speed_limit.png", "color": "#A200FF"}, - {"title": tr_noop("Vienna Signs"), "type": "toggle", "get_state": lambda: self._params.get_bool("UseVienna"), "set_state": lambda s: self._params.put_bool("UseVienna", s), "icon": "toggle_icons/icon_speed_limit.png", "color": "#A200FF"}, + { + "title": tr_noop("Road Name"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("RoadNameUI"), + "set_state": lambda s: self._params.put_bool("RoadNameUI", s), + "icon": "toggle_icons/icon_navigate.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Speed Limits"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("ShowSpeedLimits"), + "set_state": lambda s: self._params.put_bool("ShowSpeedLimits", s), + "icon": "toggle_icons/icon_speed_limit.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Mapbox Limits"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("SLCMapboxFiller"), + "set_state": lambda s: self._params.put_bool("SLCMapboxFiller", s), + "icon": "toggle_icons/icon_speed_limit.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Vienna Signs"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("UseVienna"), + "set_state": lambda s: self._params.put_bool("UseVienna", s), + "icon": "toggle_icons/icon_speed_limit.png", + "color": "#A200FF", + }, ] self._rebuild_grid() + class StarPilotVisualQOLLayout(StarPilotPanel): def __init__(self): super().__init__() + self.CAMERA_VIEWS = ["Auto", "Driver", "Standard", "Wide"] self.CATEGORIES = [ - {"title": tr_noop("Camera View"), "type": "toggle", "get_state": lambda: self._params.get_bool("CameraView"), "set_state": lambda s: self._params.put_bool("CameraView", s), "icon": "toggle_icons/icon_display.png", "color": "#A200FF"}, - {"title": tr_noop("Driver Camera"), "type": "toggle", "get_state": lambda: self._params.get_bool("DriverCamera"), "set_state": lambda s: self._params.put_bool("DriverCamera", s), "icon": "toggle_icons/icon_display.png", "color": "#A200FF"}, - {"title": tr_noop("Stopped Timer"), "type": "toggle", "get_state": lambda: self._params.get_bool("StoppedTimer"), "set_state": lambda s: self._params.put_bool("StoppedTimer", s), "icon": "toggle_icons/icon_display.png", "color": "#A200FF"}, + { + "title": tr_noop("Camera View"), + "type": "value", + "key": "CameraView", + "get_value": lambda: tr(self.CAMERA_VIEWS[self._params.get_int('CameraView')]), + "on_click": lambda: self._show_camera_view_selector(), + "icon": "toggle_icons/icon_display.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Driver Camera"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("DriverCamera"), + "set_state": lambda s: self._params.put_bool("DriverCamera", s), + "icon": "toggle_icons/icon_display.png", + "color": "#A200FF", + }, + { + "title": tr_noop("Stopped Timer"), + "type": "toggle", + "get_state": lambda: self._params.get_bool("StoppedTimer"), + "set_state": lambda s: self._params.put_bool("StoppedTimer", s), + "icon": "toggle_icons/icon_display.png", + "color": "#A200FF", + }, ] self._rebuild_grid() + + def _show_camera_view_selector(self): + current = self._params.get_int("CameraView") + + def on_select(res, val): + if res == DialogResult.CONFIRM: + idx = self.CAMERA_VIEWS.index(val) + self._params.put_int("CameraView", idx) + self._rebuild_grid() + + gui_app.set_modal_overlay(SelectionDialog(tr("Camera View"), self.CAMERA_VIEWS, self.CAMERA_VIEWS[current], on_close=on_select)) diff --git a/selfdrive/ui/layouts/settings/starpilot/wheel.py b/selfdrive/ui/layouts/settings/starpilot/wheel.py index f144c4d0..102c4024 100644 --- a/selfdrive/ui/layouts/settings/starpilot/wheel.py +++ b/selfdrive/ui/layouts/settings/starpilot/wheel.py @@ -6,6 +6,10 @@ from openpilot.system.ui.widgets import DialogResult from openpilot.system.ui.widgets.selection_dialog import SelectionDialog from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel +ACTION_NAMES = ["No Action", "Change Personality", "Force Coast", "Pause Steering", "Pause Accel/Brake", "Toggle Experimental", "Toggle Traffic"] +ACTION_IDS = {name: i for i, name in enumerate(ACTION_NAMES)} + + class StarPilotWheelLayout(StarPilotPanel): def __init__(self): super().__init__() @@ -15,8 +19,89 @@ class StarPilotWheelLayout(StarPilotPanel): "type": "toggle", "get_state": lambda: self._params.get_bool("RemapCancelToDistance"), "set_state": lambda s: self._params.put_bool("RemapCancelToDistance", s), - "icon": "toggle_icons/icon_steering.png", - "color": "#FFC40D" + "color": "#FFC40D", + }, + { + "title": tr_noop("Distance Button"), + "type": "value", + "get_value": lambda: self._get_action_name("DistanceButtonControl"), + "on_click": lambda: self._show_action_picker("DistanceButtonControl"), + "color": "#FFC40D", + }, + { + "title": tr_noop("Distance (Long Press)"), + "type": "value", + "get_value": lambda: self._get_action_name("LongDistanceButtonControl"), + "on_click": lambda: self._show_action_picker("LongDistanceButtonControl"), + "color": "#FFC40D", + }, + { + "title": tr_noop("Distance (Very Long)"), + "type": "value", + "get_value": lambda: self._get_action_name("VeryLongDistanceButtonControl"), + "on_click": lambda: self._show_action_picker("VeryLongDistanceButtonControl"), + "color": "#FFC40D", + }, + { + "title": tr_noop("LKAS Button"), + "type": "value", + "get_value": lambda: self._get_action_name("LKASButtonControl"), + "on_click": lambda: self._show_action_picker("LKASButtonControl"), + "key": "LKASButtonControl", + "color": "#FFC40D", }, ] self._rebuild_grid() + + def _get_action_name(self, key): + idx = self._params.get_int(key) + if 0 <= idx < len(ACTION_NAMES): + return ACTION_NAMES[idx] + return ACTION_NAMES[0] + + def _get_available_actions(self): + actions = list(ACTION_NAMES[:1]) # No Action + cs = starpilot_state.car_state + if cs.hasOpenpilotLongitudinal: + actions.extend(ACTION_NAMES[1:]) + return actions + + def _show_action_picker(self, key): + actions = self._get_available_actions() + current = self._get_action_name(key) + if current not in actions: + current = actions[0] + + def on_select(res, val): + if res == DialogResult.CONFIRM: + self._params.put_int(key, ACTION_IDS.get(val, 0)) + self._rebuild_grid() + + gui_app.set_modal_overlay(SelectionDialog(tr(key), actions, current, on_close=on_select)) + + def _rebuild_grid(self): + if not self.CATEGORIES: + return + if self._tile_grid is None: + self._tile_grid = __import__('openpilot.selfdrive.ui.layouts.settings.starpilot.metro', fromlist=['TileGrid']).TileGrid(columns=None, padding=20) + self._tile_grid.clear() + cs = starpilot_state.car_state + for cat in self.CATEGORIES: + key = cat.get("key") + visible = True + if key == "LKASButtonControl": + visible &= not cs.isSubaru + if not visible: + continue + tile_type = cat.get("type", "hub") + if tile_type == "toggle": + from openpilot.selfdrive.ui.layouts.settings.starpilot.metro import ToggleTile + + tile = ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=cat["set_state"], bg_color=cat.get("color")) + elif tile_type == "value": + from openpilot.selfdrive.ui.layouts.settings.starpilot.metro import ValueTile + + tile = ValueTile(title=tr(cat["title"]), get_value=cat["get_value"], on_click=cat["on_click"], bg_color=cat.get("color")) + else: + continue + self._tile_grid.add_tile(tile) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 4f657198..06f10e0d 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -225,6 +225,7 @@ class GuiApplication: self._modal_overlay = ModalOverlay() self._modal_overlay_shown = False self._modal_overlay_tick: Callable[[], None] | None = None + self._nav_stack: list = [] self._mouse = MouseState(self._scale) self._mouse_events: list[MouseEvent] = [] @@ -369,6 +370,41 @@ class GuiApplication: def set_modal_overlay_tick(self, tick_function: Callable | None): self._modal_overlay_tick = tick_function + def push_widget(self, widget): + if widget in self._nav_stack: + return + if self._nav_stack: + prev = self._nav_stack[-1] + if hasattr(prev, 'set_enabled'): + prev.set_enabled(False) + self._nav_stack.append(widget) + if hasattr(widget, 'show_event'): + widget.show_event() + if hasattr(widget, 'set_enabled'): + widget.set_enabled(True) + + def pop_widget(self, idx: int | None = None): + if len(self._nav_stack) < 2: + return + idx_to_pop = len(self._nav_stack) - 1 if idx is None else idx + if idx_to_pop <= 0 or idx_to_pop >= len(self._nav_stack): + return + if idx_to_pop == len(self._nav_stack) - 1: + prev = self._nav_stack[idx_to_pop - 1] + if hasattr(prev, 'set_enabled'): + prev.set_enabled(True) + widget = self._nav_stack.pop(idx_to_pop) + if hasattr(widget, 'hide_event'): + widget.hide_event() + + def _render_nav_stack(self) -> bool: + if not self._nav_stack: + return False + widget = self._nav_stack[-1] + if hasattr(widget, 'render'): + widget.render(rl.Rectangle(0, 0, self.width, self.height)) + return True + def set_should_render(self, should_render: bool): self._should_render = should_render @@ -523,7 +559,9 @@ class GuiApplication: rl.clear_background(rl.BLACK) # Handle modal overlay rendering and input processing - if self._handle_modal_overlay(): + if self._render_nav_stack(): + yield False + elif self._handle_modal_overlay(): # Allow a Widget to still run a function while overlay is shown if self._modal_overlay_tick is not None: self._modal_overlay_tick() diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 39e18f8f..9bc8c16d 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -34,6 +34,7 @@ class Widget(abc.ABC): self._click_callback: Callable[[], None] | None = None self._multi_touch = False self.__was_awake = True + self._children: list = [] @property def rect(self) -> rl.Rectangle: @@ -180,9 +181,25 @@ class Widget(abc.ABC): def show_event(self): """Optionally handle show event. Parent must manually call this""" + for child in self._children: + child.show_event() def hide_event(self): """Optionally handle hide event. Parent must manually call this""" + for child in self._children: + child.hide_event() + + def _child(self, widget): + """Register a child widget for lifecycle propagation.""" + assert widget not in self._children, f"{type(widget).__name__} already a child of {type(self).__name__}" + self._children.append(widget) + return widget + + def dismiss(self, callback: Callable[[], None] | None = None): + """Dismiss this widget from the nav stack.""" + gui_app.pop_widget() + if callback: + callback() SWIPE_AWAY_THRESHOLD = 80 # px to dismiss after releasing diff --git a/system/ui/widgets/input_dialog.py b/system/ui/widgets/input_dialog.py index ff9d475b..b71b0df3 100644 --- a/system/ui/widgets/input_dialog.py +++ b/system/ui/widgets/input_dialog.py @@ -1,118 +1,43 @@ -import pyray as rl from collections.abc import Callable -from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import Widget, DialogResult -from openpilot.system.ui.widgets.button import Button, ButtonStyle -from openpilot.system.ui.widgets.label import Label, FontWeight -from openpilot.system.ui.widgets.keyboard import Keyboard, KeyboardLayout +from openpilot.system.ui.widgets.keyboard import Keyboard -MARGIN = 50 -BUTTON_HEIGHT = 160 -OUTER_MARGIN_X = 200 -OUTER_MARGIN_Y = 150 -BACKGROUND_COLOR = rl.Color(27, 27, 27, 255) class InputDialog(Widget): def __init__(self, title: str, default_text: str = "", hint_text: str = "", on_close: Callable[[DialogResult, str], None] | None = None): super().__init__() - self._title = title - self._text = default_text - self._hint = hint_text + self._default_text = default_text self._on_close = on_close - self._dialog_result = DialogResult.NO_ACTION - - self._title_label = Label(title, 70, FontWeight.BOLD, text_color=rl.Color(201, 201, 201, 255)) - self._cancel_button = Button("Cancel", self._cancel_button_callback) - self._confirm_button = Button("Confirm", self._confirm_button_callback, button_style=ButtonStyle.PRIMARY) - - self._keyboard = Keyboard(self._on_key_pressed, self._on_keyboard_done, layout=KeyboardLayout.QWERTY) - - self._font = gui_app.font(FontWeight.MEDIUM) - def _on_key_pressed(self, key: str): - if key == "\b": - self._text = self._text[:-1] - else: - self._text += key + self._keyboard = Keyboard(callback=self._on_keyboard_result) + self._keyboard.set_title(title) + self._keyboard.set_text(default_text) - def _on_keyboard_done(self): - self._confirm_button_callback() - - def _cancel_button_callback(self): - self._dialog_result = DialogResult.CANCEL + def _on_keyboard_result(self, result: DialogResult): + if self._dialog_result != DialogResult.NO_ACTION: + return + self._dialog_result = result if self._on_close: - self._on_close(self._dialog_result, self._text) - - def _confirm_button_callback(self): - self._dialog_result = DialogResult.CONFIRM - if self._on_close: - self._on_close(self._dialog_result, self._text) + self._on_close(result, self._keyboard.text) @property def result(self) -> DialogResult: return self._dialog_result - + @property def text(self) -> str: - return self._text + return self._keyboard.text def show_event(self): super().show_event() self._dialog_result = DialogResult.NO_ACTION + self._keyboard.show_event() + self._keyboard.clear() + if self._default_text: + self._keyboard.set_text(self._default_text) - def _render(self, rect: rl.Rectangle): - # Dim background - rl.draw_rectangle(0, 0, int(rect.width), int(rect.height), rl.Color(0, 0, 0, 200)) - - # Dialog Box - dialog_rect = rl.Rectangle( - rect.x + OUTER_MARGIN_X, - rect.y + OUTER_MARGIN_Y, - rect.width - 2 * OUTER_MARGIN_X, - rect.height - 2 * OUTER_MARGIN_Y, - ) - rl.draw_rectangle_rounded(dialog_rect, 0.05, 10, BACKGROUND_COLOR) - - # Title - title_rect = rl.Rectangle(dialog_rect.x + MARGIN, dialog_rect.y + MARGIN, dialog_rect.width - 2 * MARGIN, 100) - self._title_label.render(title_rect) - - # Text Input Field - input_rect = rl.Rectangle(dialog_rect.x + MARGIN, title_rect.y + title_rect.height + 40, dialog_rect.width - 2 * MARGIN, 120) - rl.draw_rectangle_rounded(input_rect, 0.1, 10, rl.Color(40, 40, 40, 255)) - - display_text = self._text - text_color = rl.WHITE - if not display_text: - display_text = self._hint - text_color = rl.Color(128, 128, 128, 255) - - text_size = rl.measure_text_ex(self._font, display_text, 50, 0) - text_pos = rl.Vector2(input_rect.x + 40, input_rect.y + (input_rect.height - text_size.y) / 2) - rl.draw_text_ex(self._font, display_text, text_pos, 50, 0, text_color) - - # Blinking cursor - if (rl.get_time() % 1.0) < 0.5: - cursor_x = text_pos.x + (text_size.x if self._text else 0) + 5 - rl.draw_rectangle(int(cursor_x), int(text_pos.y), 4, 50, rl.WHITE) - - # Keyboard - keyboard_rect = rl.Rectangle( - dialog_rect.x + MARGIN, - input_rect.y + input_rect.height + 40, - dialog_rect.width - 2 * MARGIN, - 400 - ) - self._keyboard.render(keyboard_rect) - - # Buttons - btn_y = dialog_rect.y + dialog_rect.height - BUTTON_HEIGHT - MARGIN - btn_width = (dialog_rect.width - 3 * MARGIN) / 2 - - cancel_rect = rl.Rectangle(dialog_rect.x + MARGIN, btn_y, btn_width, BUTTON_HEIGHT) - confirm_rect = rl.Rectangle(dialog_rect.x + 2 * MARGIN + btn_width, btn_y, btn_width, BUTTON_HEIGHT) - - self._cancel_button.render(cancel_rect) - self._confirm_button.render(confirm_rect) + def _render(self, rect): + self._keyboard.render(rect) + return self._dialog_result diff --git a/system/ui/widgets/keyboard.py b/system/ui/widgets/keyboard.py index 4ec92f50..0eec4061 100644 --- a/system/ui/widgets/keyboard.py +++ b/system/ui/widgets/keyboard.py @@ -1,12 +1,13 @@ from functools import partial import time from typing import Literal +from collections.abc import Callable import pyray as rl from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr -from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets.button import ButtonStyle, Button from openpilot.system.ui.widgets.inputbox import InputBox from openpilot.system.ui.widgets.label import Label @@ -58,7 +59,14 @@ KEYBOARD_LAYOUTS = { class Keyboard(Widget): - def __init__(self, max_text_size: int = 255, min_text_size: int = 0, password_mode: bool = False, show_password_toggle: bool = False): + def __init__( + self, + max_text_size: int = 255, + min_text_size: int = 0, + password_mode: bool = False, + show_password_toggle: bool = False, + callback: Callable[[DialogResult], None] | None = None, + ): super().__init__() self._layout_name: Literal["lowercase", "uppercase", "numbers", "specials"] = "lowercase" self._caps_lock = False @@ -71,6 +79,7 @@ class Keyboard(Widget): self._input_box = InputBox(max_text_size) self._password_mode = password_mode self._show_password_toggle = show_password_toggle + self._callback = callback # Backspace key repeat tracking self._backspace_pressed: bool = False @@ -78,6 +87,8 @@ class Keyboard(Widget): self._backspace_last_repeat: float = 0.0 self._render_return_status = -1 + self._first_render = False + self._skip_input = False self._cancel_button = Button(lambda: tr("Cancel"), self._cancel_button_callback) self._eye_button = Button("", self._eye_button_callback, button_style=ButtonStyle.TRANSPARENT) @@ -98,12 +109,18 @@ class Keyboard(Widget): for _, key in enumerate(keys): if key in self._key_icons: texture = self._key_icons[key] - self._all_keys[key] = Button("", partial(self._key_callback, key), icon=texture, - button_style=ButtonStyle.PRIMARY if key == ENTER_KEY else ButtonStyle.KEYBOARD, multi_touch=True) + self._all_keys[key] = Button( + "", + partial(self._key_callback, key), + icon=texture, + button_style=ButtonStyle.PRIMARY if key == ENTER_KEY else ButtonStyle.KEYBOARD, + multi_touch=True, + ) else: self._all_keys[key] = Button(key, partial(self._key_callback, key), button_style=ButtonStyle.KEYBOARD, font_size=85, multi_touch=True) - self._all_keys[CAPS_LOCK_KEY] = Button("", partial(self._key_callback, CAPS_LOCK_KEY), icon=self._key_icons[CAPS_LOCK_KEY], - button_style=ButtonStyle.KEYBOARD, multi_touch=True) + self._all_keys[CAPS_LOCK_KEY] = Button( + "", partial(self._key_callback, CAPS_LOCK_KEY), icon=self._key_icons[CAPS_LOCK_KEY], button_style=ButtonStyle.KEYBOARD, multi_touch=True + ) def set_text(self, text: str): self._input_box.text = text @@ -122,20 +139,42 @@ class Keyboard(Widget): self._title.set_text(title) self._sub_title.set_text(sub_title) - def _eye_button_callback(self): - self._password_mode = not self._password_mode + def set_callback(self, callback: Callable[[DialogResult], None] | None): + self._callback = callback + + def show_event(self): + super().show_event() + self._skip_input = True + + def _process_mouse_events(self): + if not self._skip_input: + super()._process_mouse_events() def _cancel_button_callback(self): self.clear() - self._render_return_status = 0 + if self in gui_app._nav_stack: + gui_app.pop_widget() + else: + self._render_return_status = 0 + if self._callback: + self._callback(DialogResult.CANCEL) + + def _eye_button_callback(self): + self._password_mode = not self._password_mode def _key_callback(self, k): if k == ENTER_KEY: - self._render_return_status = 1 + if self in gui_app._nav_stack: + gui_app.pop_widget() + else: + self._render_return_status = 1 + if self._callback: + self._callback(DialogResult.CONFIRM) else: self.handle_key_press(k) def _render(self, rect: rl.Rectangle): + self._skip_input = False rect = rl.Rectangle(rect.x + CONTENT_MARGIN, rect.y + CONTENT_MARGIN, rect.width - 2 * CONTENT_MARGIN, rect.height - 2 * CONTENT_MARGIN) self._title.render(rl.Rectangle(rect.x, rect.y, rect.width, 95)) self._sub_title.render(rl.Rectangle(rect.x, rect.y + 95, rect.width, 60))