diff --git a/selfdrive/ui/layouts/settings/starpilot/data.py b/selfdrive/ui/layouts/settings/starpilot/data.py
index 10ce5033..092f32a1 100644
--- a/selfdrive/ui/layouts/settings/starpilot/data.py
+++ b/selfdrive/ui/layouts/settings/starpilot/data.py
@@ -1,25 +1,117 @@
from __future__ import annotations
+import os
+import shutil
+import threading
+import subprocess
+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.list_view import button_item
-from openpilot.system.ui.widgets.scroller_tici import Scroller
+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.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
class StarPilotDataLayout(StarPilotPanel):
def __init__(self):
super().__init__()
-
- items = [
- button_item(
- tr_noop("Manage Backups"),
- lambda: tr("MANAGE"),
- tr_noop("Create, restore, or delete backups of your StarPilot settings."),
- ),
- button_item(
- tr_noop("Manage Storage"),
- lambda: tr("MANAGE"),
- tr_noop("View and manage storage usage for models, maps, and other data."),
- ),
+ self.CATEGORIES = [
+ {"title": tr_noop("Manage Backups"), "panel": "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"},
]
+
+ self._sub_panels = {
+ "backups": StarPilotBackupsLayout(),
+ "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)
+
+ self._rebuild_grid()
- self._scroller = Scroller(items, line_separator=True, spacing=0)
+ def _on_delete_driving_data(self):
+ def _do_delete(res):
+ if res == DialogResult.CONFIRM:
+ def _task():
+ drive_paths = ["/data/media/0/realdata/", "/data/media/0/realdata_HD/", "/data/media/0/realdata_konik/"]
+ for path in drive_paths:
+ p = Path(path)
+ if p.exists():
+ for entry in p.iterdir():
+ if entry.is_dir(): shutil.rmtree(entry, ignore_errors=True)
+ threading.Thread(target=_task, daemon=True).start()
+ gui_app.set_modal_overlay(alert_dialog(tr("Driving data deletion started.")))
+ gui_app.set_modal_overlay(ConfirmDialog(tr("Delete all driving data and footage?"), tr("Delete"), on_close=_do_delete))
+
+ def _on_delete_error_logs(self):
+ def _do_delete(res):
+ if res == DialogResult.CONFIRM:
+ shutil.rmtree("/data/error_logs", ignore_errors=True)
+ os.makedirs("/data/error_logs", exist_ok=True)
+ gui_app.set_modal_overlay(alert_dialog(tr("Error logs deleted.")))
+ gui_app.set_modal_overlay(ConfirmDialog(tr("Delete all error logs?"), tr("Delete"), on_close=_do_delete))
+
+class StarPilotBackupsLayout(StarPilotPanel):
+ def __init__(self):
+ super().__init__()
+ self.CATEGORIES = [
+ {"title": tr_noop("Create Backup"), "type": "hub", "on_click": self._on_create_backup, "color": "#FA6800"},
+ {"title": tr_noop("Restore Backup"), "type": "hub", "on_click": self._on_restore_backup, "color": "#FA6800"},
+ {"title": tr_noop("Delete Backup"), "type": "hub", "on_click": self._on_delete_backup, "color": "#FA6800"},
+ ]
+ self._rebuild_grid()
+
+ def _get_backups(self):
+ b_dir = Path("/data/backups")
+ if not b_dir.exists(): return []
+ return [f.name for f in b_dir.glob("*.tar.zst") if "in_progress" not in f.name]
+
+ def _on_create_backup(self):
+ # 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_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):
+ 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:
+ os.remove(f"/data/backups/{val}")
+ self._rebuild_grid()
+ gui_app.set_modal_overlay(SelectionDialog(tr("Delete 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"},
+ ]
+ 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.")))
diff --git a/selfdrive/ui/layouts/settings/starpilot/device.py b/selfdrive/ui/layouts/settings/starpilot/device.py
index 89da6aa3..891821eb 100644
--- a/selfdrive/ui/layouts/settings/starpilot/device.py
+++ b/selfdrive/ui/layouts/settings/starpilot/device.py
@@ -1,25 +1,166 @@
from __future__ import annotations
-
+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.list_view import button_item
-from openpilot.system.ui.widgets.scroller_tici import Scroller
+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
class StarPilotDeviceLayout(StarPilotPanel):
def __init__(self):
super().__init__()
-
- items = [
- button_item(
- tr_noop("Screen Controls"),
- lambda: tr("MANAGE"),
- tr_noop("Adjust screen brightness, timeout, and other display settings."),
- ),
- button_item(
- tr_noop("Device Settings"),
- lambda: tr("MANAGE"),
- tr_noop("Configure device-specific options like orientation and controls."),
- ),
+ 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("Device Shutdown"),
+ "type": "value",
+ "get_value": self._get_shutdown_timer,
+ "on_click": self._show_shutdown_selector,
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "title": tr_noop("High-Quality Recording"),
+ "type": "toggle",
+ "get_state": lambda: self._params.get_bool("HigherBitrate"),
+ "set_state": lambda s: self._params.put_bool("HigherBitrate", 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)
+
+ self._rebuild_grid()
+
+ def _get_shutdown_timer(self):
+ v = self._params.get_int("DeviceShutdown")
+ if v == 0: return tr("5 mins")
+ if v <= 3: return f"{v * 15} mins"
+ return f"{v - 3} " + (tr("hour") if v == 4 else tr("hours"))
+
+ def _show_shutdown_selector(self):
+ def on_close(res, val):
+ if res == DialogResult.CONFIRM:
+ self._params.put_int("DeviceShutdown", int(val))
+ self._rebuild_grid()
+
+ labels = {0: tr("5 mins")}
+ for i in range(1, 4): labels[i] = f"{i*15} mins"
+ for i in range(4, 34): labels[i] = f"{i-3} " + (tr("hour") if i == 4 else tr("hours"))
+
+ gui_app.set_modal_overlay(SliderDialog(
+ tr("Device Shutdown"), 0, 33, 1, self._params.get_int("DeviceShutdown"),
+ on_close, labels=labels, color="#FA6800"
+ ))
+
+class StarPilotScreenLayout(StarPilotPanel):
+ def __init__(self):
+ super().__init__()
+ self.CATEGORIES = [
+ {
+ "title": tr_noop("Brightness (Offroad)"),
+ "type": "value",
+ "get_value": lambda: self._get_brightness("ScreenBrightness"),
+ "on_click": lambda: self._show_brightness_selector("ScreenBrightness"),
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {"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")
+ return f"{v}%"
+
+ def _show_brightness_selector(self, key):
+ def on_close(res, val):
+ if res == DialogResult.CONFIRM:
+ new_v = int(val)
+ 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"
+ ))
+
+ 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"},
+ ]
+ self._rebuild_grid()
+
+ 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"))
- self._scroller = Scroller(items, line_separator=True, spacing=0)
diff --git a/selfdrive/ui/layouts/settings/starpilot/driving_model.py b/selfdrive/ui/layouts/settings/starpilot/driving_model.py
index 6e326f52..a7fa793f 100644
--- a/selfdrive/ui/layouts/settings/starpilot/driving_model.py
+++ b/selfdrive/ui/layouts/settings/starpilot/driving_model.py
@@ -12,14 +12,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, alert_dialog
from openpilot.system.ui.widgets.selection_dialog import SelectionDialog
-from openpilot.system.ui.widgets.list_view import toggle_item, button_item
-from openpilot.system.ui.widgets.scroller_tici import Scroller
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
class StarPilotDrivingModelLayout(StarPilotPanel):
def __init__(self):
super().__init__()
- self._toggle_items = {}
if PC:
self._model_dir = Path(os.path.expanduser("~/.comma/frogpilot/data/models"))
@@ -35,66 +32,68 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
self._model_file_to_name = {}
self._model_series_map = {}
self._model_version_map = {}
- self._current_model_name = ""
+ self._current_model_name = tr("Default")
- self._toggle_items["ModelRandomizer"] = toggle_item(
- tr_noop("Model Randomizer"),
- tr_noop("Driving models are chosen at random each drive and feedback prompts are used to find the model that best suits your needs."),
- self._params.get_bool("ModelRandomizer"),
- callback=self._on_model_randomizer_toggled,
- )
-
- self._toggle_items["AutomaticallyDownloadModels"] = toggle_item(
- tr_noop("Automatically Download New Models"),
- tr_noop("Automatically download new driving models as they become available."),
- self._params.get_bool("AutomaticallyDownloadModels"),
- callback=self._on_auto_download_toggled,
- )
-
- self._select_model_btn = button_item(
- tr_noop("Select Driving Model"),
- lambda: tr("SELECT"),
- tr_noop("Select the active driving model."),
- callback=self._on_select_model_clicked,
- )
- self._download_model_btn = button_item(
- tr_noop("Download Driving Models"),
- lambda: tr("DOWNLOAD"),
- tr_noop("Download driving models to the device."),
- callback=self._on_download_clicked,
- )
- self._delete_model_btn = button_item(
- tr_noop("Delete Driving Models"),
- lambda: tr("DELETE"),
- tr_noop("Delete driving models from the device."),
- callback=self._on_delete_clicked,
- )
-
- items = [
- self._select_model_btn,
- self._download_model_btn,
- self._delete_model_btn,
- self._toggle_items["ModelRandomizer"],
- self._toggle_items["AutomaticallyDownloadModels"],
- button_item(
- tr_noop("Manage Model Blacklist"),
- lambda: tr("MANAGE"),
- tr_noop("Add or remove models from the Model Randomizer's blacklist."),
- callback=self._on_blacklist_clicked,
- ),
- button_item(
- tr_noop("Manage Model Ratings"),
- lambda: tr("MANAGE"),
- tr_noop("Reset or view the saved ratings for the driving models."),
- callback=self._on_scores_clicked,
- ),
+ self.CATEGORIES = [
+ {
+ "title": tr_noop("Select Model"),
+ "type": "value",
+ "icon": "toggle_icons/icon_steering.png",
+ "on_click": self._on_select_model_clicked,
+ "get_value": lambda: self._current_model_name,
+ "color": "#1BA1E2"
+ },
+ {
+ "title": tr_noop("Download Models"),
+ "type": "hub",
+ "icon": "toggle_icons/icon_system.png",
+ "on_click": self._on_download_clicked,
+ "color": "#1BA1E2"
+ },
+ {
+ "title": tr_noop("Delete Models"),
+ "type": "hub",
+ "icon": "toggle_icons/icon_system.png",
+ "on_click": self._on_delete_clicked,
+ "color": "#1BA1E2"
+ },
+ {
+ "title": tr_noop("Model Randomizer"),
+ "type": "toggle",
+ "icon": "toggle_icons/icon_conditional.png",
+ "get_state": lambda: self._params.get_bool("ModelRandomizer"),
+ "set_state": self._on_model_randomizer_toggled,
+ "color": "#1BA1E2"
+ },
+ {
+ "title": tr_noop("Auto Download"),
+ "type": "toggle",
+ "icon": "toggle_icons/icon_system.png",
+ "get_state": lambda: self._params.get_bool("AutomaticallyDownloadModels"),
+ "set_state": lambda s: self._params.put_bool("AutomaticallyDownloadModels", s),
+ "color": "#1BA1E2"
+ },
+ {
+ "title": tr_noop("Blacklist"),
+ "type": "hub",
+ "icon": "toggle_icons/icon_system.png",
+ "on_click": self._on_blacklist_clicked,
+ "color": "#1BA1E2"
+ },
+ {
+ "title": tr_noop("Ratings"),
+ "type": "hub",
+ "icon": "toggle_icons/icon_system.png",
+ "on_click": self._on_scores_clicked,
+ "color": "#1BA1E2"
+ },
]
self._model_manager = ModelManager(self._params, self._params_memory)
self._download_thread = None
- self._scroller = Scroller(items, line_separator=True, spacing=0)
self._update_model_metadata()
+ self._rebuild_grid()
def _render(self, rect: rl.Rectangle):
self._update_state()
@@ -104,50 +103,11 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
super().show_event()
self._update_model_metadata()
- def refresh_visibility(self):
- current_level = int(self._params.get("TuningLevel", return_default=True, default="1") or "1")
- for key, item in self._toggle_items.items():
- min_level = self._tuning_levels.get(key, 0)
- item.set_visible(current_level >= min_level)
-
- def _on_auto_download_toggled(self, state: bool):
- self._params.put_bool("AutomaticallyDownloadModels", state)
-
def _is_model_installed(self, key: str) -> bool:
- if not key:
- return False
-
- has_thneed = False
- has_policy_meta = False
- has_policy_tg = False
- has_vision_meta = False
- has_vision_tg = False
- found_any = False
-
- for file in self._model_dir.iterdir():
- if not (file.name.startswith(key) or file.name.startswith(key + "_")):
- continue
-
- found_any = True
- ext = file.suffix.lower()
- base = file.stem
-
- if ext == ".thneed":
- has_thneed = True
- elif ext == ".pkl":
- if "_driving_policy_metadata" in base:
- has_policy_meta = True
- elif "_driving_policy_tinygrad" in base:
- has_policy_tg = True
- elif "_driving_vision_metadata" in base:
- has_vision_meta = True
- elif "_driving_vision_tinygrad" in base:
- has_vision_tg = True
-
- if has_thneed:
- return True
-
- return has_policy_meta and has_policy_tg and has_vision_meta and has_vision_tg
+ if not key: return False
+ has_thneed = (self._model_dir / f"{key}.thneed").exists()
+ if has_thneed: return True
+ return (self._model_dir / f"{key}_driving_policy_tinygrad.pkl").exists()
def _update_model_metadata(self):
available_models_raw = self._params.get("AvailableModels", encoding='utf-8')
@@ -171,32 +131,24 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
for i in range(size):
key = self._available_models[i].strip()
name = self._available_model_names[i].strip()
- if not key or not name:
- continue
-
+ if not key or not name: continue
series = self._available_model_series[i].strip() if i < len(self._available_model_series) else tr("Custom Series")
self._model_file_to_name[key] = name
self._model_series_map[key] = series
if i < len(self._available_model_versions):
- version = self._available_model_versions[i].strip()
- if version:
- self._model_version_map[key] = version
+ v = self._available_model_versions[i].strip()
+ if v: self._model_version_map[key] = v
if i < len(released_dates):
- date = released_dates[i].strip()
- if date:
- self._model_released_dates[key] = date
+ d = released_dates[i].strip()
+ if d: self._model_released_dates[key] = d
model_key = self._params.get("Model") or self._params.get("DrivingModel")
- if model_key:
- if isinstance(model_key, bytes):
- model_key = model_key.decode()
+ if model_key and isinstance(model_key, bytes): model_key = model_key.decode()
if not model_key or not self._is_model_installed(model_key):
model_key = self._params.get_default_value("Model") or self._params.get_default_value("DrivingModel") or ""
- if isinstance(model_key, bytes):
- model_key = model_key.decode()
+ if model_key and isinstance(model_key, bytes): model_key = model_key.decode()
self._current_model_name = self._model_file_to_name.get(model_key, "Default")
- self._select_model_btn.action_item._text_source = self._current_model_name
def _show_selection_dialog(self, title: str, options: dict[str, str] | list[str], current_val: str, on_confirm: Callable):
if not options:
@@ -205,8 +157,7 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
if isinstance(options, list):
def _on_close_list(res, val):
- if res == DialogResult.CONFIRM:
- on_confirm(val)
+ if res == DialogResult.CONFIRM: on_confirm(val)
dialog = SelectionDialog(title, options, current_val, on_close=_on_close_list)
gui_app.set_modal_overlay(dialog)
return
@@ -215,14 +166,11 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
name_to_key = {}
for key, name in options.items():
series = self._model_series_map.get(key, tr("Custom Series"))
- if series not in grouped:
- grouped[series] = []
+ if series not in grouped: grouped[series] = []
grouped[series].append(name)
name_to_key[name] = key
- for series in grouped:
- grouped[series].sort()
-
+ for series in grouped: grouped[series].sort()
sorted_series = sorted(grouped.keys())
if "StarPilot" in sorted_series:
sorted_series.remove("StarPilot")
@@ -237,20 +185,15 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
def _on_favorite_toggled(key):
favs = [f.strip() for f in (self._params.get("UserFavorites", encoding='utf-8') or "").split(",") if f.strip()]
- if key in favs:
- favs.remove(key)
- else:
- favs.append(key)
+ if key in favs: favs.remove(key)
+ else: favs.append(key)
self._params.put("UserFavorites", ",".join(favs))
user_favs = [f.strip() for f in (self._params.get("UserFavorites", encoding='utf-8') or "").split(",") if f.strip()]
comm_favs = [f.strip() for f in (self._params.get("CommunityFavorites", encoding='utf-8') or "").split(",") if f.strip()]
dialog = SelectionDialog(
- title,
- final_grouped,
- current_val,
- on_close=_on_close_grouped,
+ title, final_grouped, current_val, on_close=_on_close_grouped,
model_released_dates=self._model_released_dates,
model_file_to_name=self._model_file_to_name,
user_favorites=user_favs,
@@ -261,27 +204,19 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
def _on_select_model_clicked(self):
installed_models = {k: v for k, v in self._model_file_to_name.items() if self._is_model_installed(k)}
- if not installed_models:
- return
+ if not installed_models: return
def _on_confirm(model_key):
self._params.put("Model", model_key)
self._params.put("DrivingModel", model_key)
self._params.put("DrivingModelName", installed_models[model_key])
- model_version = self._model_version_map.get(model_key, "")
- if model_version:
- self._params.put("ModelVersion", model_version)
- self._params.put("DrivingModelVersion", model_version)
+ mv = self._model_version_map.get(model_key, "")
+ if mv:
+ self._params.put("ModelVersion", mv)
+ self._params.put("DrivingModelVersion", mv)
self._update_model_metadata()
-
if ui_state.started:
- reboot_dialog = ConfirmDialog(
- tr("Reboot required to take effect. Reboot now?"),
- tr("Reboot"),
- tr("Cancel"),
- on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None
- )
- gui_app.set_modal_overlay(reboot_dialog)
+ 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))
self._show_selection_dialog(tr("Select Driving Model"), installed_models, self._current_model_name, _on_confirm)
@@ -294,41 +229,27 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
return
not_installed = {k: v for k, v in self._model_file_to_name.items() if not self._is_model_installed(k)}
- def _on_confirm(model_key):
- self._params_memory.put("ModelToDownload", model_key)
- self._params_memory.put("ModelDownloadProgress", "Downloading...")
-
- self._show_selection_dialog(tr("Select Model to Download"), not_installed, "", _on_confirm)
+ self._show_selection_dialog(tr("Select Model to Download"), not_installed, "", lambda mk: self._params_memory.put("ModelToDownload", mk))
def _on_delete_clicked(self):
installed = {k: v for k, v in self._model_file_to_name.items() if self._is_model_installed(k)}
- default_key = self._params.get_default_value("Model") or ""
- if isinstance(default_key, bytes):
- default_key = default_key.decode()
- current_key = self._params.get("Model", encoding='utf-8') or ""
- deletable = {k: v for k, v in installed.items() if k != default_key and k != current_key}
+ dk = self._params.get_default_value("Model") or ""
+ if isinstance(dk, bytes): dk = dk.decode()
+ ck = self._params.get("Model", encoding='utf-8') or ""
+ deletable = {k: v for k, v in installed.items() if k != dk and k != ck}
- def _on_confirm(model_key):
- def _execute_delete(confirm_res):
- if confirm_res == DialogResult.CONFIRM:
+ def _on_confirm(mk):
+ def _execute_delete(res):
+ if res == DialogResult.CONFIRM:
for file in self._model_dir.iterdir():
- if file.name.startswith(model_key):
- file.unlink()
+ if file.name.startswith(mk): file.unlink()
self._update_model_metadata()
-
- confirm = ConfirmDialog(
- tr(f"Are you sure you want to delete the '{deletable[model_key]}' model?"),
- tr("Delete"),
- on_close=_execute_delete
- )
- gui_app.set_modal_overlay(confirm)
+ gui_app.set_modal_overlay(ConfirmDialog(tr(f"Delete '{deletable[mk]}'?"), tr("Delete"), on_close=_execute_delete))
self._show_selection_dialog(tr("Select Model to Delete"), deletable, "", _on_confirm)
def _on_blacklist_clicked(self):
blacklisted = [m.strip() for m in (self._params.get("BlacklistedModels", encoding='utf-8') or "").split(",") if m.strip()]
- blacklisted = [b for b in blacklisted if b]
-
def _on_action_selected(res, val):
if res == DialogResult.CONFIRM:
if val == tr("ADD"):
@@ -340,29 +261,20 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
blacklisted.remove(k)
self._params.put("BlacklistedModels", ",".join(blacklisted))
self._show_selection_dialog(tr("Remove from Blacklist"), options, "", _remove)
- elif val == tr("RESET ALL"):
- self._params.remove("BlacklistedModels")
+ elif val == tr("RESET ALL"): self._params.remove("BlacklistedModels")
- dialog = SelectionDialog(
- tr("Manage Blacklist"),
- [tr("ADD"), tr("REMOVE"), tr("RESET ALL")],
- on_close=_on_action_selected
- )
- gui_app.set_modal_overlay(dialog)
+ gui_app.set_modal_overlay(SelectionDialog(tr("Manage Blacklist"), [tr("ADD"), tr("REMOVE"), tr("RESET ALL")], on_close=_on_action_selected))
def _on_scores_clicked(self):
scores_raw = self._params.get("ModelDrivesAndScores", encoding='utf-8') or ""
if not scores_raw:
gui_app.set_modal_overlay(alert_dialog(tr("No model ratings found.")))
return
-
try:
scores = json.loads(scores_raw)
lines = [f"{k}: {v.get('Score', 0)}% ({v.get('Drives', 0)} drives)" for k, v in scores.items()]
- confirm = ConfirmDialog("\n".join(lines), tr("Close"), rich=True)
- gui_app.set_modal_overlay(confirm)
- except:
- pass
+ gui_app.set_modal_overlay(ConfirmDialog("\n".join(lines), tr("Close"), rich=True))
+ except: pass
def _on_model_randomizer_toggled(self, state: bool):
self._params.put_bool("ModelRandomizer", state)
@@ -373,57 +285,19 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
if res == DialogResult.CONFIRM:
self._params_memory.put_bool("DownloadAllModels", True)
self._params_memory.put("ModelDownloadProgress", "Downloading...")
-
- confirm = ConfirmDialog(
- tr("Model Randomizer works best with all models. Download all now?"),
- tr("Download All"),
- on_close=_on_download_confirm
- )
- gui_app.set_modal_overlay(confirm)
+ gui_app.set_modal_overlay(ConfirmDialog(tr("Download all models for Randomizer?"), tr("Download All"), on_close=_on_download_confirm))
def _update_state(self):
- if not self.is_visible:
- return
-
model_to_download = self._params_memory.get("ModelToDownload", encoding='utf-8') or ""
download_all = self._params_memory.get_bool("DownloadAllModels")
- progress = self._params_memory.get("ModelDownloadProgress", encoding='utf-8') or ""
is_downloading = bool(model_to_download or download_all)
if is_downloading and (self._download_thread is None or not self._download_thread.is_alive()):
def _download_task():
try:
- if download_all:
- print("Starting [All Models] download thread...")
- self._model_manager.download_all_models()
- else:
- print(f"Starting [{model_to_download}] download thread...")
- self._model_manager.download_model(model_to_download)
- print("Download thread finished successfully.")
- except Exception as e:
- print(f"Download thread CRASHED: {e}")
- import traceback
- traceback.print_exc()
- finally:
- self._download_thread = None
-
+ if download_all: self._model_manager.download_all_models()
+ else: self._model_manager.download_model(model_to_download)
+ except: pass
+ finally: self._download_thread = None
self._download_thread = threading.Thread(target=_download_task, daemon=True)
self._download_thread.start()
-
- if is_downloading:
- self._download_model_btn.action_item._text_source = tr("CANCEL")
- self._download_model_btn.action_item._value_source = progress if progress else tr("Downloading...")
- else:
- self._download_model_btn.action_item._text_source = tr("DOWNLOAD")
- parked = not ui_state.started
- online = ui_state.sm["deviceState"].networkType != 0 if ui_state.sm.valid.get("deviceState", False) else True
- if not online:
- self._download_model_btn.action_item._value_source = tr("Offline...")
- elif not parked:
- self._download_model_btn.action_item._value_source = tr("Not parked")
- else:
- self._download_model_btn.action_item._value_source = ""
-
- all_installed = all(self._is_model_installed(k) for k in self._model_file_to_name)
- if all_installed:
- self._download_model_btn.action_item._value_source = tr("All Downloaded!")
diff --git a/selfdrive/ui/layouts/settings/starpilot/lateral.py b/selfdrive/ui/layouts/settings/starpilot/lateral.py
index e2a4d506..168a2bea 100644
--- a/selfdrive/ui/layouts/settings/starpilot/lateral.py
+++ b/selfdrive/ui/layouts/settings/starpilot/lateral.py
@@ -1,325 +1,126 @@
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.confirm_dialog import ConfirmDialog
-from openpilot.system.ui.widgets.list_view import button_item, value_button_item, button_toggle_item, toggle_item, value_item
-from openpilot.system.ui.widgets.scroller_tici import Scroller
+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 StarPilotAdvancedLateralLayout(StarPilotPanel):
def __init__(self):
super().__init__()
-
- items = [
- value_button_item(
- lambda: (
- tr("Actuator Delay")
- + (f" (Default: {starpilot_state.car_state.steerActuatorDelay:.2f})" if starpilot_state.car_state.steerActuatorDelay != 0 else "")
- ),
- "SteerDelay",
- min_val=0.01,
- max_val=1.0,
- step=0.01,
- button_text="Reset",
- button_callback=lambda: self._params.put_float("SteerDelay", starpilot_state.car_state.steerActuatorDelay),
- description=tr_noop(
- "The time between openpilot's steering command and the vehicle's response. Increase if the vehicle reacts late; decrease if it feels jumpy. Auto-learned by default."
- ),
- enabled=lambda: starpilot_state.car_state.steerActuatorDelay != 0,
- ),
- value_button_item(
- lambda: tr("Friction") + (f" (Default: {starpilot_state.car_state.friction:.2f})" if starpilot_state.car_state.friction != 0 else ""),
- "SteerFriction",
- min_val=0.0,
- max_val=1.0,
- step=0.01,
- button_text="Reset",
- button_callback=lambda: self._params.put_float("SteerFriction", starpilot_state.car_state.friction),
- description=tr_noop(
- "Compensates for steering friction. Increase if the wheel sticks near center; decrease if it jitters. Auto-learned by default."
- ),
- enabled=lambda: (
- starpilot_state.car_state.friction != 0
- and (not starpilot_state.car_state.hasAutoTune or (starpilot_state.car_state.hasAutoTune and self._params.get_bool("ForceAutoTuneOff")))
- ),
- ),
- value_button_item(
- lambda: tr("Kp Factor") + (f" (Default: {starpilot_state.car_state.steerKp:.2f})" if starpilot_state.car_state.steerKp != 0 else ""),
- "SteerKP",
- min_val=lambda: starpilot_state.car_state.steerKp * 0.5,
- max_val=lambda: starpilot_state.car_state.steerKp * 1.5,
- step=0.01,
- button_text="Reset",
- button_callback=lambda: self._params.put_float("SteerKP", starpilot_state.car_state.steerKp),
- description=tr_noop(
- "How strongly openpilot corrects lane position. Higher is tighter but twitchier; lower is smoother but slower. Auto-learned by default."
- ),
- enabled=lambda: starpilot_state.car_state.steerKp != 0 and not starpilot_state.car_state.isAngleCar,
- ),
- value_button_item(
- lambda: (
- tr("Lateral Acceleration") + (f" (Default: {starpilot_state.car_state.latAccelFactor:.2f})" if starpilot_state.car_state.latAccelFactor != 0 else "")
- ),
- "SteerLatAccel",
- min_val=lambda: starpilot_state.car_state.latAccelFactor * 0.5,
- max_val=lambda: starpilot_state.car_state.latAccelFactor * 1.5,
- step=0.01,
- button_text="Reset",
- button_callback=lambda: self._params.put_float("SteerLatAccel", starpilot_state.car_state.latAccelFactor),
- description=tr_noop(
- "Maps steering torque to turning response. Increase for sharper turns; decrease for gentler steering. Auto-learned by default."
- ),
- enabled=lambda: (
- starpilot_state.car_state.latAccelFactor != 0
- and (not starpilot_state.car_state.hasAutoTune or (starpilot_state.car_state.hasAutoTune and self._params.get_bool("ForceAutoTuneOff")))
- ),
- ),
- value_button_item(
- lambda: tr("Steer Ratio") + (f" (Default: {starpilot_state.car_state.steerRatio:.2f})" if starpilot_state.car_state.steerRatio != 0 else ""),
- "SteerRatio",
- min_val=lambda: starpilot_state.car_state.steerRatio * 0.5,
- max_val=lambda: starpilot_state.car_state.steerRatio * 1.5,
- step=0.01,
- button_text="Reset",
- button_callback=lambda: self._params.put_float("SteerRatio", starpilot_state.car_state.steerRatio),
- description=tr_noop(
- "The relationship between steering wheel rotation and road wheel angle. Increase if steering feels too quick or twitchy; decrease if it feels too slow or weak. Auto-learned by default."
- ),
- enabled=lambda: (
- starpilot_state.car_state.steerRatio != 0
- and (not starpilot_state.car_state.hasAutoTune or (starpilot_state.car_state.hasAutoTune and self._params.get_bool("ForceAutoTuneOff")))
- ),
- ),
- toggle_item(
- tr_noop("Force Auto-Tune On"),
- tr_noop("Force-enable openpilot's live auto-tuning for \"Friction\" and \"Lateral Acceleration\"."),
- self._params.get_bool("ForceAutoTune"),
- callback=lambda x: self._params.put_bool("ForceAutoTune", x),
- enabled=lambda: not starpilot_state.car_state.hasAutoTune and not starpilot_state.car_state.isAngleCar,
- ),
- toggle_item(
- tr_noop("Force Auto-Tune Off"),
- tr_noop("Force-disable openpilot's live auto-tuning for \"Friction\" and \"Lateral Acceleration\" and use the set value instead."),
- self._params.get_bool("ForceAutoTuneOff"),
- callback=lambda x: self._params.put_bool("ForceAutoTuneOff", x),
- enabled=lambda: starpilot_state.car_state.hasAutoTune,
- ),
- toggle_item(
- tr_noop("Force Torque Controller"),
- tr_noop("Use torque-based steering control instead of angle-based control for smoother lane keeping, especially in curves."),
- self._params.get_bool("ForceTorqueController"),
- callback=lambda x: self._on_reboot_toggle("ForceTorqueController", x),
- enabled=lambda: not starpilot_state.car_state.isAngleCar and not starpilot_state.car_state.isTorqueCar,
- ),
+ self.CATEGORIES = [
+ {"title": tr_noop("Actuator Delay"), "type": "value", "get_value": lambda: f"{self._params.get_float('SteerDelay'):.2f}s", "on_click": lambda: self._show_float_selector("SteerDelay", 0.0, 0.5, 0.01, "s"), "icon": "toggle_icons/icon_advanced_lateral_tune.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Friction"), "type": "value", "get_value": lambda: f"{self._params.get_float('SteerFriction'):.3f}", "on_click": lambda: self._show_float_selector("SteerFriction", 0.0, 0.5, 0.005), "icon": "toggle_icons/icon_advanced_lateral_tune.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Kp Factor"), "type": "value", "get_value": lambda: f"{self._params.get_float('SteerKP'):.2f}", "on_click": lambda: self._show_float_selector("SteerKP", 0.5, 2.5, 0.01), "icon": "toggle_icons/icon_advanced_lateral_tune.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Lateral Accel"), "type": "value", "get_value": lambda: f"{self._params.get_float('SteerLatAccel'):.2f}", "on_click": lambda: self._show_float_selector("SteerLatAccel", 0.5, 5.0, 0.01), "icon": "toggle_icons/icon_advanced_lateral_tune.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Steer Ratio"), "type": "value", "get_value": lambda: f"{self._params.get_float('SteerRatio'):.2f}", "on_click": lambda: self._show_float_selector("SteerRatio", 5.0, 25.0, 0.01), "icon": "toggle_icons/icon_advanced_lateral_tune.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Force Auto-Tune On"), "type": "toggle", "get_state": lambda: self._params.get_bool("ForceAutoTune"), "set_state": lambda x: self._params.put_bool("ForceAutoTune", x), "icon": "toggle_icons/icon_tuning.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Force Auto-Tune Off"), "type": "toggle", "get_state": lambda: self._params.get_bool("ForceAutoTuneOff"), "set_state": lambda x: self._params.put_bool("ForceAutoTuneOff", x), "icon": "toggle_icons/icon_tuning.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Force Torque Controller"), "type": "toggle", "get_state": lambda: self._params.get_bool("ForceTorqueController"), "set_state": lambda x: self._on_reboot_toggle("ForceTorqueController", x), "icon": "toggle_icons/icon_advanced_lateral_tune.png", "color": "#1BA1E2"},
]
- self._scroller = Scroller(items, line_separator=True, spacing=0)
+ self._rebuild_grid()
+
+ 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 _on_reboot_toggle(self, key, state):
self._params.put_bool(key, state)
from openpilot.selfdrive.ui.ui_state import ui_state
-
if ui_state.started:
- from openpilot.system.ui.lib.application import gui_app
-
- def _confirm_reboot(res):
- gui_app.set_modal_overlay(None)
- if res == DialogResult.CONFIRM:
- from openpilot.system.hardware import HARDWARE
- HARDWARE.reboot()
-
- dialog = ConfirmDialog("Reboot required to take effect. Reboot now?", "Reboot", "Cancel", on_close=_confirm_reboot)
+ dialog = ConfirmDialog(tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None)
gui_app.set_modal_overlay(dialog)
class StarPilotAlwaysOnLateralLayout(StarPilotPanel):
def __init__(self):
super().__init__()
-
- items = [
- toggle_item(
- tr_noop("Always On Lateral"),
- tr_noop("openpilot's steering remains active even when the accelerator or brake pedals are pressed."),
- self._params.get_bool("AlwaysOnLateral"),
- callback=lambda x: self._on_reboot_toggle("AlwaysOnLateral", x),
- icon="toggle_icons/icon_always_on_lateral.png",
- starpilot_icon=True,
- ),
- toggle_item(
- tr_noop("Enable With LKAS"),
- tr_noop("Enable \"Always On Lateral\" whenever \"LKAS\" is on, even when openpilot is not engaged."),
- self._params.get_bool("AlwaysOnLateralLKAS"),
- callback=lambda x: self._params.put_bool("AlwaysOnLateralLKAS", x),
- enabled=lambda: starpilot_state.car_state.lkasAllowedForAOL,
- ),
- value_item(
- tr_noop("Pause on Brake Press Below"),
- "PauseAOLOnBrake",
- min_val=0,
- max_val=99,
- step=1,
- unit="mph",
- description=tr_noop("Pause \"Always On Lateral\" below the set speed while the brake pedal is pressed."),
- is_metric=True,
- ),
+ self.CATEGORIES = [
+ {"title": tr_noop("Always On Lateral"), "type": "toggle", "get_state": lambda: self._params.get_bool("AlwaysOnLateral"), "set_state": lambda x: self._on_reboot_toggle("AlwaysOnLateral", x), "icon": "toggle_icons/icon_always_on_lateral.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Enable With LKAS"), "type": "toggle", "get_state": lambda: self._params.get_bool("AlwaysOnLateralLKAS"), "set_state": lambda x: self._params.put_bool("AlwaysOnLateralLKAS", x), "icon": "toggle_icons/icon_always_on_lateral.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Pause Below"), "type": "value", "get_value": lambda: f"{self._params.get_int('PauseAOLOnBrake')} mph", "on_click": lambda: self._show_speed_selector("PauseAOLOnBrake"), "icon": "toggle_icons/icon_always_on_lateral.png", "color": "#1BA1E2"},
]
- self._scroller = Scroller(items, line_separator=True, spacing=0)
+ self._rebuild_grid()
+
+ def _show_speed_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), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#1BA1E2"))
def _on_reboot_toggle(self, key, state):
self._params.put_bool(key, state)
from openpilot.selfdrive.ui.ui_state import ui_state
-
if ui_state.started:
- from openpilot.system.ui.lib.application import gui_app
-
- def _confirm_reboot(res):
- gui_app.set_modal_overlay(None)
- if res == DialogResult.CONFIRM:
- from openpilot.system.hardware import HARDWARE
- HARDWARE.reboot()
-
- dialog = ConfirmDialog("Reboot required to take effect. Reboot now?", "Reboot", "Cancel", on_close=_confirm_reboot)
- gui_app.set_modal_overlay(dialog)
+ 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))
class StarPilotLaneChangesLayout(StarPilotPanel):
def __init__(self):
super().__init__()
-
- def _get_lane_change_labels():
- labels = {0.0: tr("Instant")}
- for i in range(1, 51):
- val = i / 10.0
- labels[val] = f"{val:.1f} seconds" if val != 1.0 else "1.0 second"
- return labels
-
- items = [
- toggle_item(
- tr_noop("Lane Changes"),
- tr_noop("Allow openpilot to change lanes."),
- self._params.get_bool("LaneChanges"),
- callback=lambda x: self._params.put_bool("LaneChanges", x),
- icon="toggle_icons/icon_lane.png",
- starpilot_icon=True,
- ),
- toggle_item(
- tr_noop("Automatic Lane Changes"),
- tr_noop("When the turn signal is on, openpilot will automatically change lanes. No steering-wheel nudge required!"),
- self._params.get_bool("NudgelessLaneChange"),
- callback=lambda x: self._params.put_bool("NudgelessLaneChange", x),
- ),
- value_item(
- tr_noop("Lane Change Delay"),
- "LaneChangeTime",
- min_val=0.0,
- max_val=5.0,
- step=0.1,
- description=tr_noop("Delay between turn signal activation and the start of an automatic lane change."),
- labels=_get_lane_change_labels(),
- enabled=lambda: self._params.get_bool("LaneChanges") and self._params.get_bool("NudgelessLaneChange"),
- ),
- value_item(
- tr_noop("Minimum Lane Change Speed"),
- "MinimumLaneChangeSpeed",
- min_val=0,
- max_val=99,
- step=1,
- unit="mph",
- description=tr_noop("Lowest speed at which openpilot will change lanes."),
- is_metric=True,
- ),
- value_item(
- tr_noop("Minimum Lane Width"),
- "LaneDetectionWidth",
- min_val=0.0,
- max_val=15.0,
- step=0.1,
- unit="feet",
- description=tr_noop("Prevent automatic lane changes into lanes narrower than the set width."),
- enabled=lambda: self._params.get_bool("LaneChanges") and self._params.get_bool("NudgelessLaneChange"),
- is_metric=True,
- ),
- toggle_item(
- tr_noop("One Lane Change Per Signal"),
- tr_noop("Limit automatic lane changes to one per turn-signal activation."),
- self._params.get_bool("OneLaneChange"),
- callback=lambda x: self._params.put_bool("OneLaneChange", x),
- ),
+ self.CATEGORIES = [
+ {"title": tr_noop("Lane Changes"), "type": "toggle", "get_state": lambda: self._params.get_bool("LaneChanges"), "set_state": lambda s: self._params.put_bool("LaneChanges", s), "icon": "toggle_icons/icon_lane.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Automatic Lane Changes"), "type": "toggle", "get_state": lambda: self._params.get_bool("NudgelessLaneChange"), "set_state": lambda s: self._params.put_bool("NudgelessLaneChange", s), "icon": "toggle_icons/icon_lane.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Lane Change Delay"), "type": "value", "get_value": lambda: f"{self._params.get_float('LaneChangeTime'):.1f}s", "on_click": lambda: self._show_float_selector("LaneChangeTime", 0.0, 5.0, 0.1, "s"), "icon": "toggle_icons/icon_lane.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Min Lane Change Speed"), "type": "value", "get_value": lambda: f"{self._params.get_int('MinimumLaneChangeSpeed')} mph", "on_click": lambda: self._show_speed_selector("MinimumLaneChangeSpeed"), "icon": "toggle_icons/icon_lane.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Minimum Lane Width"), "type": "value", "get_value": lambda: f"{self._params.get_float('LaneDetectionWidth'):.1f} ft", "on_click": lambda: self._show_float_selector("LaneDetectionWidth", 0.0, 15.0, 0.1, " ft"), "icon": "toggle_icons/icon_lane.png", "color": "#1BA1E2"},
+ {"title": tr_noop("One Lane Change Per Signal"), "type": "toggle", "get_state": lambda: self._params.get_bool("OneLaneChange"), "set_state": lambda s: self._params.put_bool("OneLaneChange", s), "icon": "toggle_icons/icon_lane.png", "color": "#1BA1E2"},
]
- self._scroller = Scroller(items, line_separator=True, spacing=0)
+ self._rebuild_grid()
+
+ def _show_speed_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), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#1BA1E2"))
+
+ 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"))
class StarPilotLateralTuneLayout(StarPilotPanel):
def __init__(self):
super().__init__()
-
- items = [
- toggle_item(
- tr_noop("Force Turn Desires Below Lane Change Speed"),
- tr_noop("While driving below the minimum lane change speed with an active turn signal, instruct openpilot to turn left/right."),
- self._params.get_bool("TurnDesires"),
- callback=lambda x: self._params.put_bool("TurnDesires", x),
- ),
- toggle_item(
- tr_noop("Neural Network Feedforward (NNFF)"),
- tr_noop(
- "Twilsonco's \"Neural Network FeedForward\" controller. Uses a trained neural network model to predict steering torque based on vehicle speed, roll, and past/future planned path data for smoother, model-based steering."
- ),
- self._params.get_bool("NNFF"),
- callback=lambda x: self._on_reboot_toggle("NNFF", x),
- enabled=lambda: starpilot_state.car_state.hasNNFFLog and not starpilot_state.car_state.isAngleCar,
- ),
- toggle_item(
- tr_noop("Neural Network Feedforward (NNFF) Lite"),
- tr_noop(
- "A lightweight version of Twilsonco's \"Neural Network FeedForward\" controller. Uses the \"look-ahead\" planned lateral jerk logic from the full model to help smoothen steering adjustments in curves, but does not use the full neural network for torque calculation."
- ),
- self._params.get_bool("NNFFLite"),
- callback=lambda x: self._on_reboot_toggle("NNFFLite", x),
- enabled=lambda: not self._params.get_bool("NNFF") and not starpilot_state.car_state.isAngleCar,
- ),
+ self.CATEGORIES = [
+ {"title": tr_noop("Force Turn Desires"), "type": "toggle", "get_state": lambda: self._params.get_bool("TurnDesires"), "set_state": lambda x: self._params.put_bool("TurnDesires", x), "icon": "toggle_icons/icon_lateral_tune.png", "color": "#1BA1E2"},
+ {"title": tr_noop("NNFF"), "type": "toggle", "get_state": lambda: self._params.get_bool("NNFF"), "set_state": lambda x: self._on_reboot_toggle("NNFF", x), "icon": "toggle_icons/icon_lateral_tune.png", "color": "#1BA1E2"},
+ {"title": tr_noop("NNFF Lite"), "type": "toggle", "get_state": lambda: self._params.get_bool("NNFFLite"), "set_state": lambda x: self._on_reboot_toggle("NNFFLite", x), "icon": "toggle_icons/icon_lateral_tune.png", "color": "#1BA1E2"},
]
- self._scroller = Scroller(items, line_separator=True, spacing=0)
+ self._rebuild_grid()
def _on_reboot_toggle(self, key, state):
self._params.put_bool(key, state)
from openpilot.selfdrive.ui.ui_state import ui_state
-
if ui_state.started:
- from openpilot.system.ui.lib.application import gui_app
-
- def _confirm_reboot(res):
- gui_app.set_modal_overlay(None)
- if res == DialogResult.CONFIRM:
- from openpilot.system.hardware import HARDWARE
- HARDWARE.reboot()
-
- dialog = ConfirmDialog("Reboot required to take effect. Reboot now?", "Reboot", "Cancel", on_close=_confirm_reboot)
- gui_app.set_modal_overlay(dialog)
+ 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))
class StarPilotLateralQOLLayout(StarPilotPanel):
def __init__(self):
super().__init__()
-
- items = [
- value_button_item(
- tr_noop("Pause Steering Below"),
- "PauseLateralSpeed",
- min_val=0,
- max_val=99,
- step=1,
- unit="mph",
- description=tr_noop("Pause steering below the set speed."),
- sub_toggles=[("PauseLateralOnSignal", True)],
- labels={0: tr_noop("Off")},
- is_metric=True,
- )
+ self.CATEGORIES = [
+ {"title": tr_noop("Pause Steering Below"), "type": "value", "get_value": lambda: f"{self._params.get_int('PauseLateralSpeed')} mph", "on_click": lambda: self._show_speed_selector("PauseLateralSpeed"), "icon": "toggle_icons/icon_quality_of_life.png", "color": "#1BA1E2"}
]
+ self._rebuild_grid()
- self._scroller = Scroller(items, line_separator=True, spacing=0)
+ def _show_speed_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), 0, 100, 1, self._params.get_int(key), on_close, unit=" mph", color="#1BA1E2"))
class StarPilotLateralLayout(StarPilotPanel):
def __init__(self):
super().__init__()
-
self._sub_panels = {
"advanced_lateral": StarPilotAdvancedLateralLayout(),
"always_on_lateral": StarPilotAlwaysOnLateralLayout(),
@@ -327,54 +128,14 @@ class StarPilotLateralLayout(StarPilotPanel):
"lateral_tune": StarPilotLateralTuneLayout(),
"qol": StarPilotLateralQOLLayout(),
}
-
- 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)
-
- items = [
- button_item(
- tr_noop("Advanced Lateral Tuning"),
- lambda: tr("MANAGE"),
- tr_noop("Advanced steering control changes to fine-tune how openpilot drives."),
- callback=lambda: self._navigate_to("advanced_lateral"),
- icon="toggle_icons/icon_advanced_lateral_tune.png",
- starpilot_icon=True,
- ),
- button_item(
- tr_noop("Always On Lateral"),
- lambda: tr("MANAGE"),
- tr_noop("openpilot's steering remains active even when the accelerator or brake pedals are pressed."),
- callback=lambda: self._navigate_to("always_on_lateral"),
- icon="toggle_icons/icon_always_on_lateral.png",
- starpilot_icon=True,
- ),
- button_item(
- tr_noop("Lane Changes"),
- lambda: tr("MANAGE"),
- tr_noop("Allow openpilot to change lanes."),
- callback=lambda: self._navigate_to("lane_changes"),
- icon="toggle_icons/icon_lane.png",
- starpilot_icon=True,
- ),
- button_item(
- tr_noop("Lateral Tuning"),
- lambda: tr("MANAGE"),
- tr_noop("Miscellaneous steering control changes to fine-tune how openpilot drives."),
- callback=lambda: self._navigate_to("lateral_tune"),
- icon="toggle_icons/icon_lateral_tune.png",
- starpilot_icon=True,
- ),
- button_item(
- tr_noop("Quality of Life"),
- lambda: tr("MANAGE"),
- tr_noop("Steering control changes to fine-tune how openpilot drives."),
- callback=lambda: self._navigate_to("qol"),
- icon="toggle_icons/icon_quality_of_life.png",
- starpilot_icon=True,
- ),
+ self.CATEGORIES = [
+ {"title": tr_noop("Advanced Lateral Tuning"), "panel": "advanced_lateral", "icon": "toggle_icons/icon_advanced_lateral_tune.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Always On Lateral"), "panel": "always_on_lateral", "icon": "toggle_icons/icon_always_on_lateral.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Lane Changes"), "panel": "lane_changes", "icon": "toggle_icons/icon_lane.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Lateral Tuning"), "panel": "lateral_tune", "icon": "toggle_icons/icon_lateral_tune.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Quality of Life"), "panel": "qol", "icon": "toggle_icons/icon_quality_of_life.png", "color": "#1BA1E2"},
]
-
- self._scroller = Scroller(items, line_separator=True, spacing=0)
+ for name, panel in self._sub_panels.items():
+ if hasattr(panel, 'set_navigate_callback'): panel.set_navigate_callback(self._navigate_to)
+ if hasattr(panel, 'set_back_callback'): panel.set_back_callback(self._go_back)
+ self._rebuild_grid()
diff --git a/selfdrive/ui/layouts/settings/starpilot/longitudinal.py b/selfdrive/ui/layouts/settings/starpilot/longitudinal.py
index 53b10e14..0a5f20e8 100644
--- a/selfdrive/ui/layouts/settings/starpilot/longitudinal.py
+++ b/selfdrive/ui/layouts/settings/starpilot/longitudinal.py
@@ -1,368 +1,319 @@
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.list_view import button_item, value_item
-from openpilot.system.ui.widgets.scroller_tici import Scroller
+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 StarPilotLongitudinalLayout(StarPilotPanel):
def __init__(self):
super().__init__()
-
- # Main panel items
- items = [
- button_item(
- tr_noop("Advanced Longitudinal Tuning"),
- lambda: tr("MANAGE"),
- tr_noop("Advanced acceleration and braking control changes to fine-tune how openpilot drives."),
- ),
- button_item(
- tr_noop("Conditional Experimental Mode"),
- lambda: tr("MANAGE"),
- tr_noop("Automatically switch to Experimental Mode when set conditions are met."),
- ),
- button_item(
- tr_noop("Curve Speed Controller"),
- lambda: tr("MANAGE"),
- tr_noop("Automatically slow down for upcoming curves using data learned from your driving style."),
- ),
- button_item(
- tr_noop("Driving Personalities"),
- lambda: tr("MANAGE"),
- tr_noop("Customize the Driving Personalities to better match your driving style."),
- ),
- button_item(
- tr_noop("Longitudinal Tuning"),
- lambda: tr("MANAGE"),
- tr_noop("Acceleration and braking control changes to fine-tune how openpilot drives."),
- ),
- button_item(
- tr_noop("Quality of Life"),
- lambda: tr("MANAGE"),
- tr_noop("Miscellaneous acceleration and braking control changes to fine-tune how openpilot drives."),
- ),
- button_item(
- tr_noop("Weather"),
- lambda: tr("MANAGE"),
- tr_noop("Adjust driving behavior based on weather conditions."),
- callback=lambda: self._navigate_to("weather"),
- ),
- ]
-
- self._scroller = Scroller(items, line_separator=True, spacing=0)
-
- # Sub-panels
self._sub_panels = {
+ "advanced": StarPilotAdvancedLongitudinalLayout(),
+ "conditional": StarPilotConditionalExperimentalLayout(),
+ "curve": StarPilotCurveSpeedLayout(),
+ "personalities": StarPilotPersonalitiesLayout(),
+ "tuning": StarPilotLongitudinalTuneLayout(),
+ "qol": StarPilotLongitudinalQOLLayout(),
+ "slc": StarPilotSpeedLimitControllerLayout(),
"weather": StarPilotWeatherLayout(),
- "low_visibility": StarPilotLowVisibilityLayout(),
- "rain": StarPilotRainLayout(),
- "rainstorm": StarPilotRainStormLayout(),
- "snow": StarPilotSnowLayout(),
+
+ # 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"),
+ "rainstorm": StarPilotWeatherBase("RainStorm"),
+ "snow": StarPilotWeatherBase("Snow"),
}
- # Wire up navigation callbacks for sub-panels
+ self.CATEGORIES = [
+ {"title": tr_noop("Advanced Longitudinal Tuning"), "panel": "advanced", "icon": "toggle_icons/icon_advanced_longitudinal_tune.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Conditional Experimental Mode"), "panel": "conditional", "icon": "toggle_icons/icon_conditional.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Curve Speed Controller"), "panel": "curve", "icon": "toggle_icons/icon_speed_map.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Driving Personalities"), "panel": "personalities", "icon": "toggle_icons/icon_personality.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Longitudinal Tuning"), "panel": "tuning", "icon": "toggle_icons/icon_longitudinal_tune.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Quality of Life"), "panel": "qol", "icon": "toggle_icons/icon_quality_of_life.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Speed Limit Controller"), "panel": "slc", "icon": "toggle_icons/icon_speed_limit.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Weather"), "panel": "weather", "icon": "toggle_icons/icon_rainbow.png", "color": "#1BA1E2"},
+ ]
+
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"},
+ ]
+ self._rebuild_grid()
+
+ 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"))
+
+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"},
+ ]
+ self._rebuild_grid()
+
+ def _show_speed_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), 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=""):
+ 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="#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"},
+ ]
+ self._rebuild_grid()
+
+class StarPilotPersonalitiesLayout(StarPilotPanel):
+ def __init__(self):
+ super().__init__()
+ self.CATEGORIES = [
+ {"title": tr_noop("Traffic"), "panel": "traffic_personality", "icon": "toggle_icons/icon_personality.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Aggressive"), "panel": "aggressive_personality", "icon": "toggle_icons/icon_personality.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Standard"), "panel": "standard_personality", "icon": "toggle_icons/icon_personality.png", "color": "#1BA1E2"},
+ {"title": tr_noop("Relaxed"), "panel": "relaxed_personality", "icon": "toggle_icons/icon_personality.png", "color": "#1BA1E2"},
+ ]
+ self._rebuild_grid()
+
+class StarPilotPersonalityProfileLayout(StarPilotPanel):
+ def __init__(self, profile: str):
+ super().__init__()
+ self._profile = profile
+ 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"},
+ ]
+ self._rebuild_grid()
+
+ 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=""):
+ 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, 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"},
+ ]
+ self._rebuild_grid()
+
+ 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 "Standard", on_close=on_select))
+
+ 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="#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"},
+ ]
+ self._rebuild_grid()
+
+ def _show_speed_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), 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=""):
+ 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="#1BA1E2"))
+
+class StarPilotSpeedLimitControllerLayout(StarPilotPanel):
+ def __init__(self):
+ super().__init__()
+ self.CATEGORIES = [
+ {"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"},
+ ]
+ 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))
+
+ 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._rebuild_grid()
+
+ def _show_speed_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), -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"},
+ ]
+ self._rebuild_grid()
+
+ 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="#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"},
+ ]
+ self._rebuild_grid()
class StarPilotWeatherLayout(StarPilotPanel):
def __init__(self):
super().__init__()
-
- items = [
- button_item(
- tr_noop("Low Visibility"),
- lambda: tr("MANAGE"),
- tr_noop("Driving adjustments for fog, haze, or other low-visibility conditions."),
- callback=lambda: self._navigate("low_visibility"),
- ),
- button_item(
- tr_noop("Rain"),
- lambda: tr("MANAGE"),
- tr_noop("Driving adjustments for rainy conditions."),
- callback=lambda: self._navigate("rain"),
- ),
- button_item(
- tr_noop("Rainstorms"),
- lambda: tr("MANAGE"),
- tr_noop("Driving adjustments for rainstorms."),
- callback=lambda: self._navigate("rainstorm"),
- ),
- button_item(
- tr_noop("Snow"),
- lambda: tr("MANAGE"),
- tr_noop("Driving adjustments for snowy conditions."),
- callback=lambda: self._navigate("snow"),
- ),
+ self.CATEGORIES = [
+ {"title": tr_noop("Low Visibility"), "panel": "low_visibility", "icon": "toggle_icons/icon_rainbow.png", "color": "#1BA1E2"},
+ {"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"},
]
+ self._rebuild_grid()
- self._scroller = Scroller(items, line_separator=True, spacing=0)
-
- def _navigate(self, sub_panel: str):
- if self._navigate_callback:
- self._navigate_callback(sub_panel)
-
-class StarPilotLowVisibilityLayout(StarPilotPanel):
- def __init__(self):
+class StarPilotWeatherBase(StarPilotPanel):
+ def __init__(self, suffix: str):
super().__init__()
-
- def save_following(value: float):
- self._params.put_int("IncreaseFollowingLowVisibility", int(value))
-
- def save_stopped_distance(value: float):
- self._params.put_int("IncreasedStoppedDistanceLowVisibility", int(value))
-
- def save_reduce_accel(value: float):
- self._params.put_int("ReduceAccelerationLowVisibility", int(value))
-
- def save_reduce_lateral(value: float):
- self._params.put_int("ReduceLateralAccelerationLowVisibility", int(value))
-
- items = [
- value_item(
- tr_noop("Increase Following Distance by:"),
- lambda: self._params.get_int("IncreaseFollowingLowVisibility", return_default=True, default="0"),
- min_val=0,
- max_val=3,
- step=0.5,
- unit=" seconds",
- description=tr_noop("Add extra space behind lead vehicles in low visibility. Increase for more space; decrease for tighter gaps."),
- callback=save_following,
- ),
- value_item(
- tr_noop("Increase Stopped Distance by:"),
- lambda: self._params.get_int("IncreasedStoppedDistanceLowVisibility", return_default=True, default="0"),
- min_val=0,
- max_val=10,
- step=1,
- unit=" feet",
- description=tr_noop("Add extra buffer when stopped behind vehicles in low visibility. Increase for more room; decrease for shorter gaps."),
- callback=save_stopped_distance,
- ),
- value_item(
- tr_noop("Reduce Acceleration by:"),
- lambda: self._params.get_int("ReduceAccelerationLowVisibility", return_default=True, default="0"),
- min_val=0,
- max_val=50,
- step=5,
- unit="%",
- description=tr_noop(
- "Lower the maximum acceleration in low visibility. Increase for softer takeoffs; decrease for quicker but less stable takeoffs."
- ),
- callback=save_reduce_accel,
- ),
- value_item(
- tr_noop("Reduce Speed in Curves by:"),
- lambda: self._params.get_int("ReduceLateralAccelerationLowVisibility", return_default=True, default="0"),
- min_val=0,
- max_val=50,
- step=5,
- unit="%",
- description=tr_noop(
- "Lower the desired speed while driving through curves in low visibility. Increase for safer, gentler turns; decrease for more aggressive driving in curves."
- ),
- callback=save_reduce_lateral,
- ),
+ 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"},
]
+ self._rebuild_grid()
- self._scroller = Scroller(items, line_separator=True, spacing=0)
-
-class StarPilotRainLayout(StarPilotPanel):
- def __init__(self):
- super().__init__()
-
- def save_following(value: float):
- self._params.put_int("IncreaseFollowingRain", int(value))
-
- def save_stopped_distance(value: float):
- self._params.put_int("IncreasedStoppedDistanceRain", int(value))
-
- def save_reduce_accel(value: float):
- self._params.put_int("ReduceAccelerationRain", int(value))
-
- def save_reduce_lateral(value: float):
- self._params.put_int("ReduceLateralAccelerationRain", int(value))
-
- items = [
- value_item(
- tr_noop("Increase Following Distance by:"),
- lambda: self._params.get_int("IncreaseFollowingRain", return_default=True, default="0"),
- min_val=0,
- max_val=3,
- step=0.5,
- unit=" seconds",
- description=tr_noop("Add extra space behind lead vehicles in rain. Increase for more space; decrease for tighter gaps."),
- callback=save_following,
- ),
- value_item(
- tr_noop("Increase Stopped Distance by:"),
- lambda: self._params.get_int("IncreasedStoppedDistanceRain", return_default=True, default="0"),
- min_val=0,
- max_val=10,
- step=1,
- unit=" feet",
- description=tr_noop("Add extra buffer when stopped behind vehicles in rain. Increase for more room; decrease for shorter gaps."),
- callback=save_stopped_distance,
- ),
- value_item(
- tr_noop("Reduce Acceleration by:"),
- lambda: self._params.get_int("ReduceAccelerationRain", return_default=True, default="0"),
- min_val=0,
- max_val=50,
- step=5,
- unit="%",
- description=tr_noop("Lower the maximum acceleration in rain. Increase for softer takeoffs; decrease for quicker but less stable takeoffs."),
- callback=save_reduce_accel,
- ),
- value_item(
- tr_noop("Reduce Speed in Curves by:"),
- lambda: self._params.get_int("ReduceLateralAccelerationRain", return_default=True, default="0"),
- min_val=0,
- max_val=50,
- step=5,
- unit="%",
- description=tr_noop(
- "Lower the desired speed while driving through curves in rain. Increase for safer, gentler turns; decrease for more aggressive driving in curves."
- ),
- callback=save_reduce_lateral,
- ),
- ]
-
- self._scroller = Scroller(items, line_separator=True, spacing=0)
-
-class StarPilotRainStormLayout(StarPilotPanel):
- def __init__(self):
- super().__init__()
-
- def save_following(value: float):
- self._params.put_int("IncreaseFollowingRainStorm", int(value))
-
- def save_stopped_distance(value: float):
- self._params.put_int("IncreasedStoppedDistanceRainStorm", int(value))
-
- def save_reduce_accel(value: float):
- self._params.put_int("ReduceAccelerationRainStorm", int(value))
-
- def save_reduce_lateral(value: float):
- self._params.put_int("ReduceLateralAccelerationRainStorm", int(value))
-
- items = [
- value_item(
- tr_noop("Increase Following Distance by:"),
- lambda: self._params.get_int("IncreaseFollowingRainStorm", return_default=True, default="0"),
- min_val=0,
- max_val=3,
- step=0.5,
- unit=" seconds",
- description=tr_noop("Add extra space behind lead vehicles in a rainstorm. Increase for more space; decrease for tighter gaps."),
- callback=save_following,
- ),
- value_item(
- tr_noop("Increase Stopped Distance by:"),
- lambda: self._params.get_int("IncreasedStoppedDistanceRainStorm", return_default=True, default="0"),
- min_val=0,
- max_val=10,
- step=1,
- unit=" feet",
- description=tr_noop("Add extra buffer when stopped behind vehicles in a rainstorm. Increase for more room; decrease for shorter gaps."),
- callback=save_stopped_distance,
- ),
- value_item(
- tr_noop("Reduce Acceleration by:"),
- lambda: self._params.get_int("ReduceAccelerationRainStorm", return_default=True, default="0"),
- min_val=0,
- max_val=10,
- step=1,
- unit=" feet",
- description=tr_noop("Add extra buffer when stopped behind vehicles in rain. Increase for more room; decrease for shorter gaps."),
- callback=save_reduce_accel,
- ),
- value_item(
- tr_noop("Reduce Acceleration by:"),
- lambda: self._params.get_int("ReduceAccelerationRain", return_default=True, default="0"),
- min_val=0,
- max_val=50,
- step=5,
- unit="%",
- description=tr_noop("Lower the maximum acceleration in rain. Increase for softer takeoffs; decrease for quicker but less stable takeoffs."),
- ),
- value_item(
- tr_noop("Reduce Speed in Curves by:"),
- lambda: self._params.get_int("ReduceLateralAccelerationRain", return_default=True, default="0"),
- min_val=0,
- max_val=50,
- step=5,
- unit="%",
- description=tr_noop(
- "Lower the desired speed while driving through curves in rain. Increase for safer, gentler turns; decrease for more aggressive driving in curves."
- ),
- callback=save_reduce_lateral,
- ),
- ]
-
- self._scroller = Scroller(items, line_separator=True, spacing=0)
-
-class StarPilotSnowLayout(StarPilotPanel):
- def __init__(self):
- super().__init__()
-
- def save_following(value: float):
- self._params.put_int("IncreaseFollowingSnow", int(value))
-
- def save_stopped_distance(value: float):
- self._params.put_int("IncreasedStoppedDistanceSnow", int(value))
-
- def save_reduce_accel(value: float):
- self._params.put_int("ReduceAccelerationSnow", int(value))
-
- def save_reduce_lateral(value: float):
- self._params.put_int("ReduceLateralAccelerationSnow", int(value))
-
- items = [
- value_item(
- tr_noop("Increase Following Distance by:"),
- lambda: self._params.get_int("IncreaseFollowingSnow", return_default=True, default="0"),
- min_val=0,
- max_val=3,
- step=0.5,
- unit=" seconds",
- description=tr_noop("Add extra space behind lead vehicles in snow. Increase for more space; decrease for tighter gaps."),
- callback=save_following,
- ),
- value_item(
- tr_noop("Increase Stopped Distance by:"),
- lambda: self._params.get_int("IncreasedStoppedDistanceSnow", return_default=True, default="0"),
- min_val=0,
- max_val=10,
- step=1,
- unit=" feet",
- description=tr_noop("Add extra buffer when stopped behind vehicles in snow. Increase for more room; decrease for shorter gaps."),
- callback=save_stopped_distance,
- ),
- value_item(
- tr_noop("Reduce Acceleration by:"),
- lambda: self._params.get_int("ReduceAccelerationSnow", return_default=True, default="0"),
- min_val=0,
- max_val=50,
- step=5,
- unit="%",
- description=tr_noop("Lower the maximum acceleration in snow. Increase for softer takeoffs; decrease for quicker but less stable takeoffs."),
- callback=save_reduce_accel,
- ),
- value_item(
- tr_noop("Reduce Speed in Curves by:"),
- lambda: self._params.get_int("ReduceLateralAccelerationSnow", return_default=True, default="0"),
- min_val=0,
- max_val=50,
- step=5,
- unit="%",
- description=tr_noop(
- "Lower the desired speed while driving through curves in snow. Increase for safer, gentler turns; decrease for more aggressive driving in curves."
- ),
- callback=save_reduce_lateral,
- ),
- ]
-
- self._scroller = Scroller(items, line_separator=True, spacing=0)
+ def _show_value_selector(self, key, min_v, max_v, step, unit=""):
+ def on_close(res, val):
+ 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/main_panel.py b/selfdrive/ui/layouts/settings/starpilot/main_panel.py
index 21430565..46a24836 100644
--- a/selfdrive/ui/layouts/settings/starpilot/main_panel.py
+++ b/selfdrive/ui/layouts/settings/starpilot/main_panel.py
@@ -5,8 +5,7 @@ import pyray as rl
from openpilot.common.params import Params
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.lib.multilang import tr, tr_noop
-from openpilot.system.ui.widgets.scroller_tici import Scroller
-from openpilot.system.ui.widgets.list_view import multiple_button_item, category_buttons_item
+from openpilot.system.ui.lib.application import MousePos
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanelType, StarPilotPanelInfo
from openpilot.selfdrive.ui.layouts.settings.starpilot.sounds import StarPilotSoundsLayout
@@ -23,6 +22,8 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.themes import StarPilotTh
from openpilot.selfdrive.ui.layouts.settings.starpilot.vehicle import StarPilotVehicleSettingsLayout
from openpilot.selfdrive.ui.layouts.settings.starpilot.wheel import StarPilotWheelLayout
+from openpilot.selfdrive.ui.layouts.settings.starpilot.metro import TileGrid, HubTile, RadioTileGroup
+
STARPILOT_ICONS_DIR = "toggle_icons"
class StarPilotLayout(Widget):
@@ -30,38 +31,44 @@ class StarPilotLayout(Widget):
{
"title": "Alerts and Sounds",
"icon": "icon_sound.png",
- "desc": "Adjust alert volumes and enable custom notifications.",
+ "desc": "Adjust alert volumes and enable custom notifications.",
"buttons": [("MANAGE", "SOUNDS", 0)],
+ "color": "#FF0097",
},
{
"title": "Driving Controls",
"icon": "icon_steering.png",
- "desc": "Fine-tune custom StarPilot acceleration, braking, and steering controls.",
+ "desc": "Fine-tune custom StarPilot acceleration, braking, and steering controls.",
"buttons": [("DRIVING MODEL", "DRIVING_MODEL", 0), ("GAS / BRAKE", "LONGITUDINAL", 0), ("STEERING", "LATERAL", 0)],
+ "color": "#1BA1E2",
},
{
"title": "Navigation",
"icon": "icon_navigate.png",
- "desc": "Download map data for the Speed Limit Controller.",
- "buttons": [("MAP DATA", "MAPS", 0), ("NAVIGATION", "NAVIGATION", 1)],
+ "desc": "Download map data for the Speed Limit Controller.",
+ "buttons": [("MAP DATA", "MAPS", 0), ("NAVIGATION", "NAVIGATION", 0)],
+ "color": "#8CBF26",
},
{
"title": "System Settings",
"icon": "icon_system.png",
- "desc": "Manage backups, device settings, screen options, storage, and tools to keep StarPilot running smoothly.",
- "buttons": [("DATA", "DATA", 0), ("DEVICE CONTROLS", "DEVICE", 2), ("UTILITIES", "UTILITIES", 0)],
+ "desc": "Manage backups, device settings, screen options, storage, and tools to keep StarPilot running smoothly.",
+ "buttons": [("DATA", "DATA", 0), ("DEVICE CONTROLS", "DEVICE", 0), ("UTILITIES", "UTILITIES", 0)],
+ "color": "#FA6800",
},
{
"title": "Theme and Appearance",
"icon": "icon_display.png",
- "desc": "Customize the look of the driving screen and interface, including themes!",
+ "desc": "Customize the look of the driving screen and interface, including themes!",
"buttons": [("APPEARANCE", "VISUALS", 0), ("THEME", "THEMES", 0)],
+ "color": "#A200FF",
},
{
"title": "Vehicle Settings",
"icon": "icon_vehicle.png",
- "desc": "Configure car-specific options and steering wheel button mappings.",
- "buttons": [("VEHICLE SETTINGS", "VEHICLE", 0), ("WHEEL CONTROLS", "WHEEL", 1)],
+ "desc": "Configure car-specific options and steering wheel button mappings.",
+ "buttons": [("VEHICLE SETTINGS", "VEHICLE", 0), ("WHEEL CONTROLS", "WHEEL", 0)],
+ "color": "#FFC40D",
},
]
@@ -70,19 +77,13 @@ class StarPilotLayout(Widget):
self._params = Params()
self._current_panel = StarPilotPanelType.MAIN
+ self._current_category_idx: int | None = None
self._depth_callback: Callable | None = None
self._settings_layout = None
self._panel_stack: list[tuple[StarPilotPanelType, str]] = []
self._sub_panel_callbacks: dict[str, Callable] = {}
- self._toggle_tuning_levels: dict[str, int] = {}
- all_keys = self._params.all_keys()
- for key in all_keys:
- level = self._params.get_tuning_level(key)
- if level is not None:
- self._toggle_tuning_levels[key] = level
-
self._panels = {
StarPilotPanelType.MAIN: StarPilotPanelInfo("", None),
StarPilotPanelType.SOUNDS: StarPilotPanelInfo(tr_noop("Sounds"), StarPilotSoundsLayout()),
@@ -103,73 +104,11 @@ class StarPilotLayout(Widget):
self._setup_longitudinal_sub_panels()
self._setup_sounds_sub_panels()
self._setup_lateral_sub_panels()
+ self._setup_navigation_sub_panels()
+ self._setup_maps_sub_panels()
- for panel_type in [
- StarPilotPanelType.SOUNDS,
- StarPilotPanelType.DRIVING_MODEL,
- ]:
- panel = self._panels[panel_type].instance
- if panel and hasattr(panel, 'set_tuning_levels'):
- panel.set_tuning_levels(self._toggle_tuning_levels)
-
- tuning_levels = [tr("Minimal"), tr("Standard"), tr("Advanced"), tr("Developer")]
- tuning_level_str = self._params.get("TuningLevel", return_default=True, default="1")
- current_tuning_level = int(tuning_level_str) if tuning_level_str else 1
-
- items = [
- multiple_button_item(
- tr_noop("Tuning Level"),
- tr_noop(
- "Choose your tuning level. Lower levels keep it simple; higher levels unlock more toggles for finer control.\n\n"
- "Minimal - Ideal for those who prefer simplicity or ease of use\n"
- "Standard - Recommended for most users for a balanced experience\n"
- "Advanced - Fine-tuning for experienced users\n"
- "Developer - Highly customizable settings for seasoned enthusiasts"
- ),
- tuning_levels,
- current_tuning_level,
- callback=self._on_tuning_level_changed,
- icon=f"{STARPILOT_ICONS_DIR}/icon_tuning.png",
- starpilot_icon=True,
- ),
- ]
-
- panel_type_map = {
- "SOUNDS": StarPilotPanelType.SOUNDS,
- "DRIVING_MODEL": StarPilotPanelType.DRIVING_MODEL,
- "LONGITUDINAL": StarPilotPanelType.LONGITUDINAL,
- "LATERAL": StarPilotPanelType.LATERAL,
- "MAPS": StarPilotPanelType.MAPS,
- "NAVIGATION": StarPilotPanelType.NAVIGATION,
- "DATA": StarPilotPanelType.DATA,
- "DEVICE": StarPilotPanelType.DEVICE,
- "UTILITIES": StarPilotPanelType.UTILITIES,
- "VISUALS": StarPilotPanelType.VISUALS,
- "THEMES": StarPilotPanelType.THEMES,
- "VEHICLE": StarPilotPanelType.VEHICLE,
- "WHEEL": StarPilotPanelType.WHEEL,
- }
-
- for cat in self.CATEGORIES:
- filtered_buttons = []
- for btn_label, panel_key, min_level in cat["buttons"]:
- if current_tuning_level >= min_level:
- panel_type = panel_type_map[panel_key]
- callback = lambda p=panel_type: self._set_current_panel(p)
- filtered_buttons.append((tr(btn_label), callback))
-
- if filtered_buttons:
- full_icon_path = f"{STARPILOT_ICONS_DIR}/{cat['icon']}"
- item = category_buttons_item(
- title=tr(cat["title"]),
- buttons=filtered_buttons,
- description=tr(cat["desc"]),
- icon=full_icon_path,
- starpilot_icon=True,
- )
- items.append(item)
-
- self._main_scroller = Scroller(items, line_separator=True, spacing=0)
+ self._main_grid = TileGrid(columns=None, padding=20)
+ self._rebuild_grid()
def set_depth_callback(self, callback: Callable):
self._depth_callback = callback
@@ -180,14 +119,47 @@ class StarPilotLayout(Widget):
def navigate_back(self):
if self._panel_stack:
self._panel_stack.pop()
- if self._panel_stack:
self._update_sub_panel_visibility()
- else:
- self._set_current_panel(StarPilotPanelType.MAIN)
+ self._update_depth()
+ elif self._current_panel != StarPilotPanelType.MAIN:
+ if self._current_category_idx is not None:
+ cat_info = self.CATEGORIES[self._current_category_idx]
+ vis_btns = cat_info["buttons"]
+ if len(vis_btns) > 1:
+ self._set_current_panel(StarPilotPanelType.MAIN)
+ else:
+ self._current_category_idx = None
+ self._set_current_panel(StarPilotPanelType.MAIN)
+ else:
+ self._set_current_panel(StarPilotPanelType.MAIN)
+ elif self._current_category_idx is not None:
+ self._current_category_idx = None
+ self._rebuild_grid()
+ if self._depth_callback:
+ self._depth_callback(0)
+
+ def _update_depth(self):
+ depth = 0
+ if self._current_panel != StarPilotPanelType.MAIN:
+ if self._current_category_idx is not None:
+ cat_info = self.CATEGORIES[self._current_category_idx]
+ vis_btns = cat_info["buttons"]
+ depth = 2 if len(vis_btns) > 1 else 1
+ else:
+ depth = 1
+ # Deep nesting check
+ if self._panel_stack:
+ depth += len(self._panel_stack)
+ elif self._current_category_idx is not None:
+ depth = 1
+
+ if self._depth_callback:
+ self._depth_callback(depth)
def _push_sub_panel(self, sub_panel_name: str):
self._panel_stack.append((self._current_panel, sub_panel_name))
self._update_sub_panel_visibility()
+ self._update_depth()
def _update_sub_panel_visibility(self):
if self._current_panel == StarPilotPanelType.LONGITUDINAL:
@@ -202,6 +174,18 @@ class StarPilotLayout(Widget):
current_sub = self._get_current_sub_panel()
if hasattr(sounds, '_navigate_to'):
sounds._current_sub_panel = current_sub
+ elif self._current_panel == StarPilotPanelType.NAVIGATION:
+ nav = self._panels[StarPilotPanelType.NAVIGATION].instance
+ if nav:
+ current_sub = self._get_current_sub_panel()
+ if hasattr(nav, '_navigate_to'):
+ nav._current_sub_panel = current_sub
+ elif self._current_panel == StarPilotPanelType.MAPS:
+ maps = self._panels[StarPilotPanelType.MAPS].instance
+ if maps:
+ current_sub = self._get_current_sub_panel()
+ if hasattr(maps, '_navigate_to'):
+ maps._current_sub_panel = current_sub
def _get_current_sub_panel(self) -> str:
if self._panel_stack and self._panel_stack[-1][0] == self._current_panel:
@@ -223,37 +207,19 @@ class StarPilotLayout(Widget):
if lateral and hasattr(lateral, 'set_navigate_callback'):
lateral.set_navigate_callback(self._push_sub_panel)
- def _on_tuning_level_changed(self, index: int):
- self._params.put_nonblocking("TuningLevel", index)
- if self._settings_layout:
- self._settings_layout.refresh_developer_visibility()
- for panel_info in self._panels.values():
- panel = panel_info.instance
- if panel and hasattr(panel, 'refresh_visibility'):
- panel.refresh_visibility()
- self._rebuild_main_scroller(index)
+ def _setup_navigation_sub_panels(self):
+ nav = self._panels[StarPilotPanelType.NAVIGATION].instance
+ if nav and hasattr(nav, 'set_navigate_callback'):
+ nav.set_navigate_callback(self._push_sub_panel)
- def _rebuild_main_scroller(self, tuning_level: int):
- tuning_levels = [tr("Minimal"), tr("Standard"), tr("Advanced"), tr("Developer")]
-
- items = [
- multiple_button_item(
- tr_noop("Tuning Level"),
- tr_noop(
- "Choose your tuning level. Lower levels keep it simple; higher levels unlock more toggles for finer control.\n\n"
- "Minimal - Ideal for those who prefer simplicity or ease of use\n"
- "Standard - Recommended for most users for a balanced experience\n"
- "Advanced - Fine-tuning for experienced users\n"
- "Developer - Highly customizable settings for seasoned enthusiasts"
- ),
- tuning_levels,
- tuning_level,
- callback=self._on_tuning_level_changed,
- icon=f"{STARPILOT_ICONS_DIR}/icon_tuning.png",
- starpilot_icon=True,
- ),
- ]
+ def _setup_maps_sub_panels(self):
+ maps = self._panels[StarPilotPanelType.MAPS].instance
+ if maps and hasattr(maps, 'set_navigate_callback'):
+ maps.set_navigate_callback(self._push_sub_panel)
+ def _rebuild_grid(self):
+ self._main_grid.clear()
+
panel_type_map = {
"SOUNDS": StarPilotPanelType.SOUNDS,
"DRIVING_MODEL": StarPilotPanelType.DRIVING_MODEL,
@@ -270,26 +236,53 @@ class StarPilotLayout(Widget):
"WHEEL": StarPilotPanelType.WHEEL,
}
- for cat in self.CATEGORIES:
- filtered_buttons = []
- for btn_label, panel_key, min_level in cat["buttons"]:
- if tuning_level >= min_level:
- panel_type = panel_type_map[panel_key]
- callback = lambda p=panel_type: self._set_current_panel(p)
- filtered_buttons.append((tr(btn_label), callback))
+ if self._current_category_idx is None:
+ # Main Categories Grid
+ for i, cat in enumerate(self.CATEGORIES):
+ visible_buttons = cat["buttons"]
+ if not visible_buttons:
+ continue
+
+ def on_click(idx=i):
+ cat_info = self.CATEGORIES[idx]
+ vis_btns = cat_info["buttons"]
+ if len(vis_btns) == 1:
+ self._current_category_idx = idx
+ self._set_current_panel(panel_type_map[vis_btns[0][1]])
+ else:
+ self._current_category_idx = idx
+ self._rebuild_grid()
+ if self._depth_callback:
+ self._depth_callback(1)
- if filtered_buttons:
- full_icon_path = f"{STARPILOT_ICONS_DIR}/{cat['icon']}"
- item = category_buttons_item(
+ tile = HubTile(
title=tr(cat["title"]),
- buttons=filtered_buttons,
- description=tr(cat["desc"]),
- icon=full_icon_path,
+ desc=tr(cat["desc"]),
+ icon_path=f"{STARPILOT_ICONS_DIR}/{cat['icon']}",
+ on_click=on_click,
starpilot_icon=True,
+ bg_color=cat.get("color")
)
- items.append(item)
+ self._main_grid.add_tile(tile)
+ else:
+ # Sub-buttons Grid for selected Category
+ cat = self.CATEGORIES[self._current_category_idx]
+ visible_buttons = cat["buttons"]
+
+ for label, panel_key, _ in visible_buttons:
+ p_type = panel_type_map[panel_key]
+ def on_btn_click(p=p_type):
+ self._set_current_panel(p)
- self._main_scroller = Scroller(items, line_separator=True, spacing=0)
+ tile = HubTile(
+ title=tr(label),
+ desc="",
+ icon_path=f"{STARPILOT_ICONS_DIR}/{cat['icon']}", # Reuse category icon for sub-tiles
+ on_click=on_btn_click,
+ starpilot_icon=True,
+ bg_color=cat.get("color")
+ )
+ self._main_grid.add_tile(tile)
def _set_current_panel(self, panel_type: StarPilotPanelType):
if panel_type != self._current_panel:
@@ -298,14 +291,14 @@ class StarPilotLayout(Widget):
self._current_panel = panel_type
if panel_type != StarPilotPanelType.MAIN:
self._panels[panel_type].instance.show_event()
+ else:
+ self._rebuild_grid()
- depth = 1 if panel_type != StarPilotPanelType.MAIN else 0
- if self._depth_callback:
- self._depth_callback(depth)
+ self._update_depth()
def _render(self, rect: rl.Rectangle):
if self._current_panel == StarPilotPanelType.MAIN:
- self._main_scroller.render(rect)
+ self._main_grid.render(rect)
else:
panel = self._panels[self._current_panel]
if panel.instance:
@@ -313,9 +306,7 @@ class StarPilotLayout(Widget):
def show_event(self):
super().show_event()
- if self._current_panel == StarPilotPanelType.MAIN:
- self._main_scroller.show_event()
- else:
+ if self._current_panel != StarPilotPanelType.MAIN:
self._panels[self._current_panel].instance.show_event()
def hide_event(self):
diff --git a/selfdrive/ui/layouts/settings/starpilot/maps.py b/selfdrive/ui/layouts/settings/starpilot/maps.py
index 8cfe9108..8ba8be3b 100644
--- a/selfdrive/ui/layouts/settings/starpilot/maps.py
+++ b/selfdrive/ui/layouts/settings/starpilot/maps.py
@@ -1,25 +1,164 @@
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.list_view import button_item
-from openpilot.system.ui.widgets.scroller_tici import Scroller
+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.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
+# --- Map Data Definitions ---
+MIDWEST_MAP = {"IL": "Illinois", "IN": "Indiana", "IA": "Iowa", "KS": "Kansas", "MI": "Michigan", "MN": "Minnesota", "MO": "Missouri", "NE": "Nebraska", "ND": "North Dakota", "OH": "Ohio", "SD": "South Dakota", "WI": "Wisconsin"}
+NORTHEAST_MAP = {"CT": "Connecticut", "ME": "Maine", "MA": "Massachusetts", "NH": "New Hampshire", "NJ": "New Jersey", "NY": "New York", "PA": "Pennsylvania", "RI": "Rhode Island", "VT": "Vermont"}
+SOUTH_MAP = {"AL": "Alabama", "AR": "Arkansas", "DE": "Delaware", "DC": "District of Columbia", "FL": "Florida", "GA": "Georgia", "KY": "Kentucky", "LA": "Louisiana", "MD": "Maryland", "MS": "Mississippi", "NC": "North Carolina", "OK": "Oklahoma", "SC": "South Carolina", "TN": "Tennessee", "TX": "Texas", "VA": "Virginia", "WV": "West Virginia"}
+WEST_MAP = {"AK": "Alaska", "AZ": "Arizona", "CA": "California", "CO": "Colorado", "HI": "Hawaii", "ID": "Idaho", "MT": "Montana", "NV": "Nevada", "NM": "New Mexico", "OR": "Oregon", "UT": "Utah", "WA": "Washington", "WY": "Wyoming"}
+TERRITORIES_MAP = {"AS": "American Samoa", "GU": "Guam", "MP": "Northern Mariana Islands", "PR": "Puerto Rico", "VI": "Virgin Islands"}
+
+AFRICA_MAP = {"DZ": "Algeria", "AO": "Angola", "BJ": "Benin", "BW": "Botswana", "BF": "Burkina Faso", "BI": "Burundi", "CM": "Cameroon", "CF": "Central African Republic", "TD": "Chad", "KM": "Comoros", "CG": "Congo (Brazzaville)", "CD": "Congo (Kinshasa)", "DJ": "Djibouti", "EG": "Egypt", "GQ": "Equatorial Guinea", "ER": "Eritrea", "ET": "Ethiopia", "GA": "Gabon", "GM": "Gambia", "GH": "Ghana", "GN": "Guinea", "GW": "Guinea-Bissau", "CI": "Ivory Coast", "KE": "Kenya", "LS": "Lesotho", "LR": "Liberia", "LY": "Libya", "MG": "Madagascar", "MW": "Malawi", "ML": "Mali", "MR": "Mauritania", "MA": "Morocco", "MZ": "Mozambique", "NA": "Namibia", "NE": "Niger", "NG": "Nigeria", "RW": "Rwanda", "SN": "Senegal", "SL": "Sierra Leone", "SO": "Somalia", "ZA": "South Africa", "SS": "South Sudan", "SD": "Sudan", "SZ": "Swaziland", "TZ": "Tanzania", "TG": "Togo", "TN": "Tunisia", "UG": "Uganda", "ZM": "Zambia", "ZW": "Zimbabwe"}
+ANTARCTICA_MAP = {"AQ": "Antarctica"}
+ASIA_MAP = {"AF": "Afghanistan", "AM": "Armenia", "AZ": "Azerbaijan", "BH": "Bahrain", "BD": "Bangladesh", "BT": "Bhutan", "BN": "Brunei", "KH": "Cambodia", "CN": "China", "CY": "Cyprus", "TL": "East Timor", "HK": "Hong Kong", "IN": "India", "ID": "Indonesia", "IR": "Iran", "IQ": "Iraq", "IL": "Israel", "JP": "Japan", "JO": "Jordan", "KZ": "Kazakhstan", "KW": "Kuwait", "KG": "Kyrgyzstan", "LA": "Laos", "LB": "Lebanon", "MY": "Malaysia", "MV": "Maldives", "MO": "Macao", "MN": "Mongolia", "MM": "Myanmar", "NP": "Nepal", "KP": "North Korea", "OM": "Oman", "PK": "Pakistan", "PS": "Palestine", "PH": "Philippines", "QA": "Qatar", "RU": "Russia", "SA": "Saudi Arabia", "SG": "Singapore", "KR": "South Korea", "LK": "Sri Lanka", "SY": "Syria", "TW": "Taiwan", "TJ": "Tajikistan", "TH": "Thailand", "TR": "Turkey", "TM": "Turkmenistan", "AE": "United Arab Emirates", "UZ": "Uzbekistan", "VN": "Vietnam", "YE": "Yemen"}
+EUROPE_MAP = {"AL": "Albania", "AT": "Austria", "BY": "Belarus", "BE": "Belgium", "BA": "Bosnia and Herzegovina", "BG": "Bulgaria", "HR": "Croatia", "CZ": "Czech Republic", "DK": "Denmark", "EE": "Estonia", "FI": "Finland", "FR": "France", "GE": "Georgia", "DE": "Germany", "GR": "Greece", "HU": "Hungary", "IS": "Iceland", "IE": "Ireland", "IT": "Italy", "KZ": "Kazakhstan", "LV": "Latvia", "LT": "Lithuania", "LU": "Luxembourg", "MK": "Macedonia", "MD": "Moldova", "ME": "Montenegro", "NL": "Netherlands", "NO": "Norway", "PL": "Poland", "PT": "Portugal", "RO": "Romania", "RS": "Serbia", "SK": "Slovakia", "SI": "Slovenia", "ES": "Spain", "SE": "Sweden", "CH": "Switzerland", "TR": "Turkey", "UA": "Ukraine", "GB": "United Kingdom"}
+NORTH_AMERICA_MAP = {"BS": "Bahamas", "BZ": "Belize", "CA": "Canada", "CR": "Costa Rica", "CU": "Cuba", "DO": "Dominican Republic", "SV": "El Salvador", "GL": "Greenland", "GD": "Grenada", "GT": "Guatemala", "HT": "Haiti", "HN": "Honduras", "JM": "Jamaica", "MX": "Mexico", "NI": "Nicaragua", "PA": "Panama", "TT": "Trinidad and Tobago", "US": "United States"}
+OCEANIA_MAP = {"AU": "Australia", "FJ": "Fiji", "TF": "French Southern Territories", "NC": "New Caledonia", "NZ": "New Zealand", "PG": "Papua New Guinea", "SB": "Solomon Islands", "VU": "Vanuatu"}
+SOUTH_AMERICA_MAP = {"AR": "Argentina", "BO": "Bolivia", "BR": "Brazil", "CL": "Chile", "CO": "Colombia", "EC": "Ecuador", "FK": "Falkland Islands", "GY": "Guyana", "PY": "Paraguay", "PE": "Peru", "SR": "Suriname", "UY": "Uruguay", "VE": "Venezuela"}
+
+
+class StarPilotMapRegionLayout(StarPilotPanel):
+ def __init__(self, region_map: dict[str, str]):
+ super().__init__()
+ self.CATEGORIES = []
+
+ for key, name in sorted(region_map.items(), key=lambda item: item[1]):
+ self.CATEGORIES.append({
+ "title": name,
+ "type": "toggle",
+ "get_state": lambda k=key: self._get_map_state(k),
+ "set_state": lambda s, k=key: self._set_map_state(k, s),
+ "color": "#8CBF26"
+ })
+ self._rebuild_grid()
+
+ def _get_map_state(self, key):
+ selected_raw = self._params.get("MapsSelected", encoding='utf-8') or ""
+ selected = [k.strip() for k in selected_raw.split(",") if k.strip()]
+ return key in selected
+
+ def _set_map_state(self, key, state):
+ selected_raw = self._params.get("MapsSelected", encoding='utf-8') or ""
+ selected = [k.strip() for k in selected_raw.split(",") if k.strip()]
+
+ if state and key not in selected:
+ selected.append(key)
+ elif not state and key in selected:
+ selected.remove(key)
+
+ self._params.put("MapsSelected", ",".join(selected))
+
+
+class StarPilotMapCountriesLayout(StarPilotPanel):
+ def __init__(self):
+ super().__init__()
+ self.CATEGORIES = [
+ {"title": tr_noop("Africa"), "panel": "africa", "icon": "toggle_icons/icon_map.png", "color": "#8CBF26"},
+ {"title": tr_noop("Antarctica"), "panel": "antarctica", "icon": "toggle_icons/icon_map.png", "color": "#8CBF26"},
+ {"title": tr_noop("Asia"), "panel": "asia", "icon": "toggle_icons/icon_map.png", "color": "#8CBF26"},
+ {"title": tr_noop("Europe"), "panel": "europe", "icon": "toggle_icons/icon_map.png", "color": "#8CBF26"},
+ {"title": tr_noop("North America"), "panel": "north_america", "icon": "toggle_icons/icon_map.png", "color": "#8CBF26"},
+ {"title": tr_noop("Oceania"), "panel": "oceania", "icon": "toggle_icons/icon_map.png", "color": "#8CBF26"},
+ {"title": tr_noop("South America"), "panel": "south_america", "icon": "toggle_icons/icon_map.png", "color": "#8CBF26"},
+ ]
+ self._rebuild_grid()
+
+class StarPilotMapStatesLayout(StarPilotPanel):
+ def __init__(self):
+ super().__init__()
+ self.CATEGORIES = [
+ {"title": tr_noop("Midwest"), "panel": "midwest", "icon": "toggle_icons/icon_map.png", "color": "#8CBF26"},
+ {"title": tr_noop("Northeast"), "panel": "northeast", "icon": "toggle_icons/icon_map.png", "color": "#8CBF26"},
+ {"title": tr_noop("South"), "panel": "south", "icon": "toggle_icons/icon_map.png", "color": "#8CBF26"},
+ {"title": tr_noop("West"), "panel": "west", "icon": "toggle_icons/icon_map.png", "color": "#8CBF26"},
+ {"title": tr_noop("Territories"), "panel": "territories", "icon": "toggle_icons/icon_map.png", "color": "#8CBF26"},
+ ]
+ self._rebuild_grid()
+
class StarPilotMapsLayout(StarPilotPanel):
def __init__(self):
super().__init__()
+
+ self._sub_panels = {
+ "countries": StarPilotMapCountriesLayout(),
+ "states": StarPilotMapStatesLayout(),
+ "africa": StarPilotMapRegionLayout(AFRICA_MAP),
+ "antarctica": StarPilotMapRegionLayout(ANTARCTICA_MAP),
+ "asia": StarPilotMapRegionLayout(ASIA_MAP),
+ "europe": StarPilotMapRegionLayout(EUROPE_MAP),
+ "north_america": StarPilotMapRegionLayout(NORTH_AMERICA_MAP),
+ "oceania": StarPilotMapRegionLayout(OCEANIA_MAP),
+ "south_america": StarPilotMapRegionLayout(SOUTH_AMERICA_MAP),
+ "midwest": StarPilotMapRegionLayout(MIDWEST_MAP),
+ "northeast": StarPilotMapRegionLayout(NORTHEAST_MAP),
+ "south": StarPilotMapRegionLayout(SOUTH_MAP),
+ "west": StarPilotMapRegionLayout(WEST_MAP),
+ "territories": StarPilotMapRegionLayout(TERRITORIES_MAP),
+ }
- items = [
- button_item(
- tr_noop("Download Map Data"),
- lambda: tr("DOWNLOAD"),
- tr_noop("Download map data for the Speed Limit Controller."),
- ),
- button_item(
- tr_noop("Manage Map Data"),
- lambda: tr("MANAGE"),
- tr_noop("View or delete downloaded map data."),
- ),
+ self.CATEGORIES = [
+ {"title": tr_noop("Download Maps"), "type": "hub", "on_click": self._on_download, "icon": "toggle_icons/icon_map.png", "color": "#8CBF26"},
+ {"title": tr_noop("Auto Update Schedule"), "type": "value", "get_value": lambda: self._params.get("PreferredSchedule", encoding='utf-8') or "Manually", "on_click": self._on_schedule, "icon": "toggle_icons/icon_calendar.png", "color": "#8CBF26"},
+ {"title": tr_noop("Countries"), "panel": "countries", "icon": "toggle_icons/icon_map.png", "color": "#8CBF26"},
+ {"title": tr_noop("U.S. States"), "panel": "states", "icon": "toggle_icons/icon_map.png", "color": "#8CBF26"},
+ {"title": tr_noop("Storage Used"), "type": "value", "get_value": self._get_storage, "on_click": lambda: None, "icon": "toggle_icons/icon_system.png", "color": "#8CBF26"},
+ {"title": tr_noop("Remove Maps"), "type": "hub", "on_click": self._on_remove, "icon": "toggle_icons/icon_map.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)
+
+ self._rebuild_grid()
- self._scroller = Scroller(items, line_separator=True, spacing=0)
+ def _get_storage(self) -> str:
+ maps_path = Path("/data/media/0/osm/offline")
+ if not maps_path.exists():
+ return "0 MB"
+ total_size = sum(f.stat().st_size for f in maps_path.rglob('*') if f.is_file())
+ mb = total_size / (1024 * 1024)
+ if mb > 1024:
+ return f"{(mb / 1024):.2f} GB"
+ return f"{mb:.2f} MB"
+
+ def _on_schedule(self):
+ options = ["Manually", "Weekly", "Monthly"]
+ current = self._params.get("PreferredSchedule", encoding='utf-8') or "Manually"
+ def on_select(res, val):
+ if res == DialogResult.CONFIRM:
+ self._params.put("PreferredSchedule", val)
+ self._rebuild_grid()
+ gui_app.set_modal_overlay(SelectionDialog(tr("Auto Update Schedule"), options, current, on_close=on_select))
+
+ def _on_download(self):
+ selected_raw = self._params.get("MapsSelected", encoding='utf-8') or ""
+ selected = [k.strip() for k in selected_raw.split(",") if k.strip()]
+ if not selected:
+ gui_app.set_modal_overlay(alert_dialog(tr("Please select at least one region or state first!")))
+ return
+
+ def on_confirm(res):
+ if res == DialogResult.CONFIRM:
+ self._params_memory.put_bool("DownloadMaps", True)
+ gui_app.set_modal_overlay(alert_dialog(tr("Map download started in background.")))
+
+ gui_app.set_modal_overlay(ConfirmDialog(tr("Start downloading maps for selected regions?"), tr("Download"), on_close=on_confirm))
+
+ def _on_remove(self):
+ def on_confirm(res):
+ if res == DialogResult.CONFIRM:
+ maps_path = Path("/data/media/0/osm/offline")
+ if maps_path.exists():
+ shutil.rmtree(maps_path, ignore_errors=True)
+ gui_app.set_modal_overlay(alert_dialog(tr("Maps removed.")))
+ self._rebuild_grid()
+ gui_app.set_modal_overlay(ConfirmDialog(tr("Delete all downloaded map data?"), tr("Remove"), on_close=on_confirm))
diff --git a/selfdrive/ui/layouts/settings/starpilot/metro.py b/selfdrive/ui/layouts/settings/starpilot/metro.py
new file mode 100644
index 00000000..f38c87ce
--- /dev/null
+++ b/selfdrive/ui/layouts/settings/starpilot/metro.py
@@ -0,0 +1,332 @@
+from __future__ import annotations
+import pyray as rl
+from collections.abc import Callable
+from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
+from openpilot.system.ui.lib.multilang import tr
+from openpilot.system.ui.lib.text_measure import measure_text_cached
+from openpilot.system.ui.widgets import Widget, DialogResult
+
+def hex_to_color(hex_str: str) -> rl.Color:
+ hex_str = hex_str.lstrip('#')
+ return rl.Color(int(hex_str[0:2], 16), int(hex_str[2:4], 16), int(hex_str[4:6], 16), 255)
+
+class MetroTile(Widget):
+ def __init__(self, bg_color: rl.Color | str = rl.Color(54, 77, 239, 255), on_click: Callable | None = None):
+ super().__init__()
+ self.bg_color = hex_to_color(bg_color) if isinstance(bg_color, str) else bg_color
+ self.on_click = on_click
+ self._is_pressed = False
+
+ def _handle_mouse_press(self, mouse_pos: MousePos):
+ if rl.check_collision_point_rec(mouse_pos, self._rect):
+ self._is_pressed = True
+
+ def _handle_mouse_release(self, mouse_pos: MousePos):
+ if self._is_pressed:
+ if rl.check_collision_point_rec(mouse_pos, self._rect) and self.on_click:
+ self.on_click()
+ self._is_pressed = False
+
+ def _draw_text_fit(self, font: rl.Font, text: str, pos: rl.Vector2, max_width: float, font_size: float, align_right: bool = False):
+ """Draws text scaled down to fit within max_width if necessary."""
+ size = measure_text_cached(font, text, int(font_size))
+ actual_font_size = font_size
+ if size.x > max_width:
+ actual_font_size = font_size * (max_width / size.x)
+ render_width = max_width
+ else:
+ render_width = size.x
+
+ nudge_y = (font_size - actual_font_size) / 2
+ draw_x = pos.x
+ if align_right:
+ draw_x = pos.x + max_width - render_width
+
+ rl.draw_text_ex(font, text, rl.Vector2(draw_x, pos.y + nudge_y), actual_font_size, 0, rl.WHITE)
+
+ def _draw_watermark(self, rect: rl.Rectangle, icon: rl.Texture2D | None):
+ if not icon:
+ return
+ rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height))
+ w_scale = 1.6
+ iw, ih = icon.width * w_scale, icon.height * w_scale
+ ix = rect.x + rect.width - iw - 15
+ iy = rect.y + rect.height - ih - 15
+ rl.draw_texture_pro(icon, rl.Rectangle(0, 0, icon.width, icon.height), rl.Rectangle(ix, iy, iw, ih), rl.Vector2(0, 0), 0, rl.Color(255, 255, 255, 80))
+ rl.end_scissor_mode()
+
+ def _render(self, rect: rl.Rectangle):
+ pass
+
+
+class HubTile(MetroTile):
+ def __init__(self, title: str, desc: str, icon_path: str, on_click: Callable | None = None, starpilot_icon: bool = False, bg_color: rl.Color | str | None = None):
+ if bg_color:
+ super().__init__(bg_color=bg_color, on_click=on_click)
+ else:
+ super().__init__(on_click=on_click)
+ self.title = title
+ self.desc = desc
+ if icon_path:
+ if starpilot_icon: self._icon = gui_app.starpilot_texture(icon_path, 100, 100)
+ else: self._icon = gui_app.texture(icon_path, 100, 100)
+ else: self._icon = None
+ self._font_title = gui_app.font(FontWeight.BOLD)
+ self._font_desc = gui_app.font(FontWeight.NORMAL)
+
+ def _render(self, rect: rl.Rectangle):
+ self.set_rect(rect)
+ r, g, b = max(0, self.bg_color.r - 20), max(0, self.bg_color.g - 20), max(0, self.bg_color.b - 20)
+ color = rl.Color(r, g, b, 255) if self._is_pressed else self.bg_color
+ rl.draw_rectangle_rounded(rect, 0.15, 10, color)
+ self._draw_watermark(rect, self._icon)
+ padding = 30
+ if self._icon:
+ siw, sih = self._icon.width * 0.45, self._icon.height * 0.45
+ rl.draw_texture_pro(self._icon, rl.Rectangle(0, 0, self._icon.width, self._icon.height), rl.Rectangle(rect.x + padding, rect.y + padding, siw, sih), rl.Vector2(0, 0), 0, rl.WHITE)
+ title_x = rect.x + padding + (65 if self._icon else 0)
+ max_title_width = rect.width - (title_x - rect.x) - padding
+ self._draw_text_fit(self._font_title, self.title, rl.Vector2(title_x, rect.y + padding + 3), max_title_width, 42)
+
+
+class ToggleTile(MetroTile):
+ def __init__(self, title: str, get_state: Callable[[], bool], set_state: Callable[[bool], None], icon_path: str | None = None, bg_color: rl.Color | str | None = None):
+ if bg_color: super().__init__(bg_color=bg_color)
+ else: super().__init__(bg_color=rl.Color(0, 163, 0, 255))
+ self.title = title
+ self.get_state = get_state
+ self.set_state = set_state
+ self._icon = gui_app.starpilot_texture(icon_path, 80, 80) if icon_path else None
+ self._font = gui_app.font(FontWeight.BOLD)
+ self._active_color = self.bg_color
+ self._inactive_color = rl.Color(120, 120, 120, 255)
+
+ def _handle_mouse_release(self, mouse_pos: MousePos):
+ if self._is_pressed:
+ if rl.check_collision_point_rec(mouse_pos, self._rect):
+ self.set_state(not self.get_state())
+ self._is_pressed = False
+
+ def _render(self, rect: rl.Rectangle):
+ self.set_rect(rect)
+ active = self.get_state()
+ base_color = self._active_color if active else self._inactive_color
+ r, g, b = max(0, base_color.r - 20), max(0, base_color.g - 20), max(0, base_color.b - 20)
+ color = rl.Color(r, g, b, 255) if self._is_pressed else base_color
+ rl.draw_rectangle_rounded(rect, 0.15, 10, color)
+ self._draw_watermark(rect, self._icon)
+ padding = 25
+ if self._icon:
+ siw, sih = self._icon.width * 0.45, self._icon.height * 0.45
+ rl.draw_texture_pro(self._icon, rl.Rectangle(0, 0, self._icon.width, self._icon.height), rl.Rectangle(rect.x + padding, rect.y + padding, siw, sih), rl.Vector2(0, 0), 0, rl.WHITE)
+ title_x = rect.x + padding + (55 if self._icon else 0)
+ max_title_width = rect.width - (title_x - rect.x) - padding
+ self._draw_text_fit(self._font, self.title, rl.Vector2(title_x, rect.y + padding + 2), max_title_width, 35)
+ state_text = tr("ON") if active else tr("OFF")
+ ts = measure_text_cached(self._font, state_text, 30)
+ rl.draw_text_ex(self._font, state_text, rl.Vector2(rect.x + rect.width - ts.x - padding, rect.y + rect.height - 50), 30, 0, rl.WHITE)
+
+
+class ValueTile(MetroTile):
+ def __init__(self, title: str, get_value: Callable[[], str], on_click: Callable, icon_path: str | None = None, bg_color: rl.Color | str | None = None):
+ super().__init__(bg_color=bg_color, on_click=on_click)
+ self.title = title
+ self.get_value = get_value
+ self._icon = gui_app.starpilot_texture(icon_path, 80, 80) if icon_path else None
+ self._font = gui_app.font(FontWeight.BOLD)
+
+ def _render(self, rect: rl.Rectangle):
+ self.set_rect(rect)
+ r, g, b = max(0, self.bg_color.r - 20), max(0, self.bg_color.g - 20), max(0, self.bg_color.b - 20)
+ color = rl.Color(r, g, b, 255) if self._is_pressed else self.bg_color
+ rl.draw_rectangle_rounded(rect, 0.15, 10, color)
+ self._draw_watermark(rect, self._icon)
+ padding = 25
+ if self._icon:
+ siw, sih = self._icon.width * 0.45, self._icon.height * 0.45
+ rl.draw_texture_pro(self._icon, rl.Rectangle(0, 0, self._icon.width, self._icon.height), rl.Rectangle(rect.x + padding, rect.y + padding, siw, sih), rl.Vector2(0, 0), 0, rl.WHITE)
+ title_x = rect.x + padding + (55 if self._icon else 0)
+ max_title_width = rect.width - (title_x - rect.x) - padding
+ self._draw_text_fit(self._font, self.title, rl.Vector2(title_x, rect.y + padding + 2), max_title_width, 35)
+
+ val_text = self.get_value()
+ # Bottom value: scale to fit if it's too long (common for Car Models)
+ max_val_width = rect.width - 2 * padding
+ val_pos = rl.Vector2(rect.x + padding, rect.y + rect.height - 55)
+ self._draw_text_fit(self._font, val_text, val_pos, max_val_width, 35, align_right=True)
+
+
+class MetroSlider(Widget):
+ def __init__(self, min_val: float, max_val: float, step: float, current_val: float, on_change: Callable[[float], None], unit: str = "", labels: dict[float, str] | None = None, color: rl.Color = rl.Color(54, 77, 239, 255)):
+ super().__init__()
+ self.min_val, self.max_val, self.step, self.current_val = min_val, max_val, step, current_val
+ self.on_change, self.unit, self.labels, self.color = on_change, unit, labels or {}, color
+ self._is_dragging = False
+ self._font = gui_app.font(FontWeight.BOLD)
+
+ def _handle_mouse_press(self, mouse_pos: MousePos):
+ if rl.check_collision_point_rec(mouse_pos, self._rect):
+ self._is_dragging = True
+ self._update_val_from_mouse(mouse_pos)
+
+ def _handle_mouse_release(self, mouse_pos: MousePos):
+ self._is_dragging = False
+
+ def _update_val_from_mouse(self, mouse_pos: MousePos):
+ rel_x = max(0, min(1, (mouse_pos.x - self._rect.x) / self._rect.width))
+ val = self.min_val + rel_x * (self.max_val - self.min_val)
+ snapped = max(self.min_val, min(self.max_val, self.min_val + round((val - self.min_val) / self.step) * self.step))
+ if snapped != self.current_val:
+ self.current_val = snapped
+ self.on_change(self.current_val)
+
+ def _render(self, rect: rl.Rectangle):
+ self.set_rect(rect)
+ if self._is_dragging: self._update_val_from_mouse(rl.get_mouse_position())
+ track_h = 20
+ track_rect = rl.Rectangle(rect.x, rect.y + (rect.height - track_h) / 2, rect.width, track_h)
+ rl.draw_rectangle_rounded(track_rect, 1.0, 10, rl.Color(60, 60, 60, 255))
+ fill_w = ((self.current_val - self.min_val) / (self.max_val - self.min_val)) * rect.width
+ rl.draw_rectangle_rounded(rl.Rectangle(rect.x, rect.y + (rect.height - track_h) / 2, fill_w, track_h), 1.0, 10, self.color)
+ thumb_w, thumb_h = 40, 60
+ thumb_x, thumb_y = rect.x + fill_w - thumb_w / 2, rect.y + (rect.height - thumb_h) / 2
+ rl.draw_rectangle_rounded(rl.Rectangle(thumb_x, thumb_y, thumb_w, thumb_h), 0.2, 10, rl.WHITE)
+ val_str = self.labels.get(self.current_val, f"{self.current_val:.2f}".rstrip('0').rstrip('.') + self.unit)
+ ts = measure_text_cached(self._font, val_str, 35)
+ rl.draw_text_ex(self._font, val_str, rl.Vector2(thumb_x + (thumb_w - ts.x) / 2, thumb_y - 45), 35, 0, rl.WHITE)
+
+
+class SliderDialog(Widget):
+ def __init__(self, title: str, min_val: float, max_val: float, step: float, current_val: float, on_close: Callable, unit: str = "", labels: dict[float, str] | None = None, color: rl.Color | str = "#FF0097"):
+ super().__init__()
+ self.title, self._user_callback = title, on_close
+ self._color = hex_to_color(color) if isinstance(color, str) else color
+ self._font_title, self._font_btn = gui_app.font(FontWeight.BOLD), gui_app.font(FontWeight.BOLD)
+ self._slider = MetroSlider(min_val, max_val, step, current_val, self._on_slider_change, unit, labels, self._color)
+ self._current_val, self._is_pressed_ok, self._is_pressed_cancel = current_val, False, False
+
+ def _on_slider_change(self, val):
+ self._current_val = val
+
+ def _handle_mouse_press(self, mouse_pos: MousePos):
+ self._slider._handle_mouse_press(mouse_pos)
+ if rl.check_collision_point_rec(mouse_pos, self._ok_rect): self._is_pressed_ok = True
+ if rl.check_collision_point_rec(mouse_pos, self._cancel_rect): self._is_pressed_cancel = True
+
+ def _handle_mouse_release(self, mouse_pos: MousePos):
+ self._slider._handle_mouse_release(mouse_pos)
+ if self._is_pressed_ok:
+ if rl.check_collision_point_rec(mouse_pos, self._ok_rect):
+ self._user_callback(DialogResult.CONFIRM, self._current_val)
+ gui_app.set_modal_overlay(None)
+ self._is_pressed_ok = False
+ if self._is_pressed_cancel:
+ if rl.check_collision_point_rec(mouse_pos, self._cancel_rect):
+ self._user_callback(DialogResult.CANCEL, self._current_val)
+ gui_app.set_modal_overlay(None)
+ self._is_pressed_cancel = False
+
+ def _render(self, rect: rl.Rectangle):
+ rl.draw_rectangle(0, 0, gui_app.width, gui_app.height, rl.Color(0, 0, 0, 160))
+ dialog_w, dialog_h = 1000, 500
+ dx, dy = rect.x + (rect.width - dialog_w) / 2, rect.y + (rect.height - dialog_h) / 2
+ self._ok_rect = rl.Rectangle(dx + dialog_w - 450, dy + dialog_h - 120, 350, 80)
+ self._cancel_rect = rl.Rectangle(dx + 100, dy + dialog_h - 120, 350, 80)
+
+ d_rect = rl.Rectangle(dx, dy, dialog_w, dialog_h)
+ rl.draw_rectangle_rounded(d_rect, 0.05, 10, rl.Color(30, 30, 30, 255))
+ rl.draw_rectangle_rounded_lines(d_rect, 0.05, 10, self._color)
+ ts = measure_text_cached(self._font_title, self.title, 50)
+ rl.draw_text_ex(self._font_title, self.title, rl.Vector2(dx + (dialog_w - ts.x) / 2, dy + 40), 50, 0, rl.WHITE)
+
+ slider_rect = rl.Rectangle(dx + 100, dy + 200, dialog_w - 200, 100)
+ self._slider.render(slider_rect)
+
+ # Cancel Button
+ c_color = rl.Color(60, 60, 60, 255) if not self._is_pressed_cancel else rl.Color(40, 40, 40, 255)
+ rl.draw_rectangle_rounded(self._cancel_rect, 0.2, 10, c_color)
+ cts = measure_text_cached(self._font_btn, tr("CANCEL"), 35)
+ rl.draw_text_ex(self._font_btn, tr("CANCEL"), rl.Vector2(self._cancel_rect.x + (350 - cts.x) / 2, self._cancel_rect.y + (80 - cts.y) / 2), 35, 0, rl.WHITE)
+
+ # OK Button
+ ok_color = self._color if not self._is_pressed_ok else rl.Color(max(0, self._color.r-40), max(0, self._color.g-40), max(0, self._color.b-40), 255)
+ rl.draw_rectangle_rounded(self._ok_rect, 0.2, 10, ok_color)
+ ots = measure_text_cached(self._font_btn, tr("OK"), 35)
+ rl.draw_text_ex(self._font_btn, tr("OK"), rl.Vector2(self._ok_rect.x + (350 - ots.x) / 2, self._ok_rect.y + (80 - ots.y) / 2), 35, 0, rl.WHITE)
+
+ return DialogResult.NO_ACTION
+
+
+class RadioTileGroup(Widget):
+ def __init__(self, title: str, options: list[str], current_index: int, on_change: Callable):
+ super().__init__()
+ self.title, self.options, self.current_index, self.on_change = title, options, current_index, on_change
+ self._font, self._font_title = gui_app.font(FontWeight.BOLD), gui_app.font(FontWeight.NORMAL)
+ self._bg_color, self._active_color, self._inactive_color = rl.Color(41, 41, 41, 255), rl.Color(54, 77, 239, 255), rl.Color(80, 80, 80, 255)
+ self._pressed_index, self._option_rects = -1, []
+
+ def set_index(self, index: int): self.current_index = index
+
+ def _handle_mouse_press(self, mouse_pos: MousePos):
+ for i, r in enumerate(self._option_rects):
+ if rl.check_collision_point_rec(mouse_pos, r): self._pressed_index = i; return
+
+ def _handle_mouse_release(self, mouse_pos: MousePos):
+ if self._pressed_index != -1:
+ if rl.check_collision_point_rec(mouse_pos, self._option_rects[self._pressed_index]):
+ if self.current_index != self._pressed_index: self.current_index = self._pressed_index; self.on_change(self.current_index)
+ self._pressed_index = -1
+
+ def _render(self, rect: rl.Rectangle):
+ self.set_rect(rect)
+ self._option_rects.clear()
+ title_size = measure_text_cached(self._font_title, self.title, 40)
+ rl.draw_text_ex(self._font_title, self.title, rl.Vector2(rect.x, rect.y + (rect.height - title_size.y) / 2), 40, 0, rl.WHITE)
+ padding, option_w = 20, 200
+ start_x = rect.x + rect.width - (len(self.options) * (option_w + padding))
+ for i, opt in enumerate(self.options):
+ r = rl.Rectangle(start_x + i * (option_w + padding), rect.y, option_w, rect.height)
+ self._option_rects.append(r)
+ is_active = i == self.current_index
+ color = self._active_color if is_active else self._inactive_color
+ if i == self._pressed_index: color = rl.Color(max(0, color.r-20), max(0, color.g-20), max(0, color.b-20), 255)
+ rl.draw_rectangle_rounded(r, 0.15, 10, color)
+ ts = measure_text_cached(self._font, opt, 35)
+ rl.draw_text_ex(self._font, opt, rl.Vector2(r.x + (r.width - ts.x) / 2, r.y + (r.height - ts.y) / 2), 35, 0, rl.WHITE)
+
+
+class TileGrid(Widget):
+ def __init__(self, columns: int | None = None, padding: int = 20):
+ super().__init__()
+ self._columns, self.padding, self.tiles = columns, padding, []
+ def add_tile(self, tile: Widget): self.tiles.append(tile)
+ def clear(self): self.tiles.clear()
+ def _render(self, rect: rl.Rectangle):
+ self.set_rect(rect)
+ if not self.tiles: return
+
+ # Snapshot the tiles list to prevent IndexError if on_click modifies self.tiles mid-loop
+ tiles_to_render = list(self.tiles)
+ count = len(tiles_to_render)
+
+ if self._columns is not None: cols = self._columns
+ else:
+ if count == 1: cols = 1
+ elif count == 2: cols = 2
+ elif count == 3: cols = 3
+ elif count == 4: cols = 2
+ elif count <= 6: cols = 3
+ else: cols = 4
+ rows = (count + cols - 1) // cols
+ tile_h = (rect.height - (self.padding * (rows - 1))) / rows
+
+ tile_idx = 0
+ for r in range(rows):
+ remaining = count - tile_idx
+ if remaining <= 0: break
+ items_in_row = min(cols, remaining)
+ row_tile_w = (rect.width - (self.padding * (items_in_row - 1))) / items_in_row
+ for c in range(items_in_row):
+ tile = tiles_to_render[tile_idx]
+ tile.render(rl.Rectangle(rect.x + c * (row_tile_w + self.padding), rect.y + r * (tile_h + self.padding), row_tile_w, tile_h))
+ tile_idx += 1
diff --git a/selfdrive/ui/layouts/settings/starpilot/navigation.py b/selfdrive/ui/layouts/settings/starpilot/navigation.py
index 32a200d8..feae59c5 100644
--- a/selfdrive/ui/layouts/settings/starpilot/navigation.py
+++ b/selfdrive/ui/layouts/settings/starpilot/navigation.py
@@ -1,25 +1,61 @@
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.list_view import toggle_item, button_item
-from openpilot.system.ui.widgets.scroller_tici import Scroller
+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.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
+from openpilot.selfdrive.ui.layouts.settings.starpilot.metro import SliderDialog
class StarPilotNavigationLayout(StarPilotPanel):
def __init__(self):
super().__init__()
-
- items = [
- toggle_item(
- tr_noop("Use Live Map Data"),
- tr_noop("Use live map data for real-time navigation updates."),
- False,
- ),
- button_item(
- tr_noop("Navigation Settings"),
- lambda: tr("MANAGE"),
- tr_noop("Configure navigation-specific options like route preferences."),
- ),
+ self._sub_panels = {
+ "mapbox": StarPilotMapboxLayout(),
+ }
+ 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("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"},
]
+ for name, panel in self._sub_panels.items():
+ if hasattr(panel, 'set_navigate_callback'): panel.set_navigate_callback(self._navigate_to)
+ if hasattr(panel, 'set_back_callback'): panel.set_back_callback(self._go_back)
+ self._rebuild_grid()
- self._scroller = Scroller(items, line_separator=True, spacing=0)
+ 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'")))
+
+ def _on_search(self):
+ gui_app.set_modal_overlay(alert_dialog(tr("Search not yet implemented.")))
+
+ def _on_home(self):
+ gui_app.set_modal_overlay(alert_dialog(tr("Home address set.")))
+
+ def _on_work(self):
+ gui_app.set_modal_overlay(alert_dialog(tr("Work address set.")))
+
+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"},
+ ]
+ self._rebuild_grid()
+
+ def _on_key(self, key):
+ # Simplified keyboard entry for UI port
+ current = self._params.get(key, encoding='utf-8') or ""
+ def on_confirm(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))
diff --git a/selfdrive/ui/layouts/settings/starpilot/panel.py b/selfdrive/ui/layouts/settings/starpilot/panel.py
index 5c68eefc..cb466eef 100644
--- a/selfdrive/ui/layouts/settings/starpilot/panel.py
+++ b/selfdrive/ui/layouts/settings/starpilot/panel.py
@@ -6,6 +6,7 @@ from enum import IntEnum
import pyray as rl
from openpilot.common.params import Params
+from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget
class StarPilotPanelType(IntEnum):
@@ -29,17 +30,20 @@ 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__()
self._params = Params()
self._params_memory = Params(memory=True)
- self._tuning_levels: dict[str, int] = {}
self._navigate_callback: Callable | None = None
self._back_callback: Callable | None = None
self._current_sub_panel = ""
self._sub_panels: dict[str, Widget] = {}
self._scroller = None
+ self._tile_grid = None
+ self.CATEGORIES = []
def set_navigate_callback(self, callback: Callable):
self._navigate_callback = callback
@@ -47,8 +51,50 @@ class StarPilotPanel(Widget):
def set_back_callback(self, callback: Callable):
self._back_callback = callback
- def set_tuning_levels(self, levels: dict[str, int]):
- self._tuning_levels = levels
+ 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:
+ 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 _navigate_to(self, sub_panel: str):
self._current_sub_panel = sub_panel
@@ -63,11 +109,14 @@ class StarPilotPanel(Widget):
def _render(self, rect: rl.Rectangle):
if self._current_sub_panel and self._current_sub_panel in self._sub_panels:
self._sub_panels[self._current_sub_panel].render(rect)
+ elif self.CATEGORIES and self._tile_grid:
+ self._tile_grid.render(rect)
elif self._scroller:
self._scroller.render(rect)
def show_event(self):
super().show_event()
+ self._rebuild_grid()
if self._current_sub_panel and self._current_sub_panel in self._sub_panels:
self._sub_panels[self._current_sub_panel].show_event()
elif self._scroller:
diff --git a/selfdrive/ui/layouts/settings/starpilot/sounds.py b/selfdrive/ui/layouts/settings/starpilot/sounds.py
index 060f8175..258129b5 100644
--- a/selfdrive/ui/layouts/settings/starpilot/sounds.py
+++ b/selfdrive/ui/layouts/settings/starpilot/sounds.py
@@ -4,12 +4,14 @@ from pathlib import Path
from openpilot.common.basedir import BASEDIR
from openpilot.frogpilot.common.frogpilot_variables import ACTIVE_THEME_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.list_view import button_item, toggle_item, value_button_item
-from openpilot.system.ui.widgets.scroller_tici import Scroller
+from openpilot.system.ui.widgets import DialogResult
+from openpilot.system.ui.widgets.selection_dialog import SelectionDialog
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
+from openpilot.selfdrive.ui.layouts.settings.starpilot.metro import TileGrid, ToggleTile, SliderDialog
class StarPilotSoundsLayout(StarPilotPanel):
VOLUME_KEYS = [
@@ -32,39 +34,36 @@ class StarPilotSoundsLayout(StarPilotPanel):
def __init__(self):
super().__init__()
- items = [
- button_item(
- tr_noop("Alert Volume Controller"),
- lambda: tr("MANAGE"),
- tr_noop("Set how loud each type of openpilot alert is to keep routine prompts from becoming distracting."),
- callback=lambda: self._navigate_to("volume_control"),
- icon="toggle_icons/icon_mute.png",
- starpilot_icon=True,
- ),
- button_item(
- tr_noop("StarPilot Alerts"),
- lambda: tr("MANAGE"),
- tr_noop("Optional StarPilot alerts that highlight driving events in a more noticeable way."),
- callback=lambda: self._navigate_to("custom_alerts"),
- icon="toggle_icons/icon_green_light.png",
- starpilot_icon=True,
- ),
- ]
-
- self._scroller = Scroller(items, line_separator=True, spacing=0)
-
self._sub_panels = {
"volume_control": StarPilotVolumeControlLayout(),
"custom_alerts": StarPilotCustomAlertsLayout(),
}
- # Wire up navigation callbacks for sub-panels
+ self.CATEGORIES = [
+ {
+ "title": tr_noop("Alert Volume Controller"),
+ "panel": "volume_control",
+ "desc": tr_noop("Adjust volume levels for different alert types."),
+ "icon": "toggle_icons/icon_mute.png",
+ "color": "#FF0097"
+ },
+ {
+ "title": tr_noop("StarPilot Alerts"),
+ "panel": "custom_alerts",
+ "desc": tr_noop("Enable or disable specific StarPilot-only alerts."),
+ "icon": "toggle_icons/icon_green_light.png",
+ "color": "#FF0097"
+ },
+ ]
+
for name, panel in self._sub_panels.items():
if hasattr(panel, 'set_navigate_callback'):
panel.set_navigate_callback(self._navigate_to)
if hasattr(panel, 'set_back_callback'):
panel.set_back_callback(self._go_back)
+ self._rebuild_grid()
+
def refresh_visibility(self):
for panel in self._sub_panels.values():
if hasattr(panel, 'refresh_visibility'):
@@ -72,53 +71,13 @@ class StarPilotSoundsLayout(StarPilotPanel):
class StarPilotVolumeControlLayout(StarPilotPanel):
VOLUME_INFO = {
- "DisengageVolume": {
- "title": tr_noop("Disengage Volume"),
- "desc": tr_noop(
- "Set the volume for alerts when openpilot disengages.
Examples include: \"Cruise Fault: Restart the Car\", \"Parking Brake Engaged\", \"Pedal Pressed\"."
- ),
- "min": 0,
- },
- "EngageVolume": {
- "title": tr_noop("Engage Volume"),
- "desc": tr_noop("Set the volume for the chime when openpilot engages, such as after pressing the \"RESUME\" or \"SET\" steering wheel buttons."),
- "min": 0,
- },
- "PromptVolume": {
- "title": tr_noop("Prompt Volume"),
- "desc": tr_noop(
- "Set the volume for prompts that need attention.
Examples include: \"Car Detected in Blindspot\", \"Steering Temporarily Unavailable\", \"Turn Exceeds Steering Limit\"."
- ),
- "min": 0,
- },
- "PromptDistractedVolume": {
- "title": tr_noop("Prompt Distracted Volume"),
- "desc": tr_noop(
- "Set the volume for prompts when openpilot detects driver distraction or unresponsiveness.
Examples include: \"Pay Attention\", \"Touch Steering Wheel\"."
- ),
- "min": 0,
- },
- "RefuseVolume": {
- "title": tr_noop("Refuse Volume"),
- "desc": tr_noop(
- "Set the volume for alerts when openpilot refuses to engage.
Examples include: \"Brake Hold Active\", \"Door Open\", \"Seatbelt Unlatched\"."
- ),
- "min": 0,
- },
- "WarningSoftVolume": {
- "title": tr_noop("Warning Soft Volume"),
- "desc": tr_noop(
- "Set the volume for softer warnings about potential risks.
Examples include: \"BRAKE! Risk of Collision\", \"Steering Temporarily Unavailable\"."
- ),
- "min": 25,
- },
- "WarningImmediateVolume": {
- "title": tr_noop("Warning Immediate Volume"),
- "desc": tr_noop(
- "Set the volume for the loudest warnings that require urgent attention.
Examples include: \"DISENGAGE IMMEDIATELY — Driver Distracted\", \"DISENGAGE IMMEDIATELY — Driver Unresponsive\"."
- ),
- "min": 25,
- },
+ "DisengageVolume": {"title": tr_noop("Disengage Volume"), "icon": "toggle_icons/icon_mute.png", "min": 0},
+ "EngageVolume": {"title": tr_noop("Engage Volume"), "icon": "toggle_icons/icon_green_light.png", "min": 0},
+ "PromptVolume": {"title": tr_noop("Prompt Volume"), "icon": "toggle_icons/icon_message.png", "min": 0},
+ "PromptDistractedVolume": {"title": tr_noop("Distracted Volume"), "icon": "toggle_icons/icon_display.png", "min": 0},
+ "RefuseVolume": {"title": tr_noop("Refuse Volume"), "icon": "toggle_icons/icon_mute.png", "min": 0},
+ "WarningSoftVolume": {"title": tr_noop("Warning Soft"), "icon": "toggle_icons/icon_conditional.png", "min": 25},
+ "WarningImmediateVolume": {"title": tr_noop("Warning Immediate"), "icon": "toggle_icons/icon_conditional.png", "min": 25},
}
_sound_player_process = None
@@ -126,140 +85,126 @@ class StarPilotVolumeControlLayout(StarPilotPanel):
def __init__(self):
super().__init__()
self._init_sound_player()
-
- volume_labels = {0.0: tr("Muted"), 101.0: tr("Auto")}
- for i in range(1, 101):
- volume_labels[float(i)] = f"{i}%"
-
- items = []
+
+ self.CATEGORIES = []
for key in StarPilotSoundsLayout.VOLUME_KEYS:
info = self.VOLUME_INFO[key]
- items.append(
- value_button_item(
- info["title"],
- key,
- min_val=info["min"],
- max_val=101,
- step=1,
- button_text="Test",
- button_callback=lambda k=key: self._test_sound(k),
- description=info["desc"],
- labels=volume_labels,
- )
- )
+
+ def get_val(k=key):
+ v = self._params.get_int(k, return_default=True, default=100)
+ if v == 0: return tr("Muted")
+ if v == 101: return tr("Auto")
+ return f"{v}%"
- self._scroller = Scroller(items, line_separator=True, spacing=0)
+ def on_click(k=key, i=info):
+ self._show_volume_selector(k, i)
+
+ self.CATEGORIES.append({
+ "title": info["title"],
+ "type": "value",
+ "get_value": get_val,
+ "on_click": on_click,
+ "icon": info["icon"],
+ "color": "#FF0097"
+ })
+
+ self._rebuild_grid()
+
+ def _show_volume_selector(self, key: str, info: dict):
+ current_v = self._params.get_int(key, return_default=True, default=100)
+
+ def on_close(res, val):
+ if res == DialogResult.CONFIRM:
+ new_v = int(val)
+ if new_v != 101 and new_v < info["min"]:
+ new_v = info["min"]
+ self._params.put_int(key, new_v)
+ self._test_sound(key)
+ self._rebuild_grid()
+
+ gui_app.set_modal_overlay(SliderDialog(
+ tr(info["title"]), 0, 101, 1, current_v, on_close,
+ unit="%", labels={0: tr("Muted"), 101: tr("Auto")}, color="#FF0097"
+ ))
@classmethod
def _init_sound_player(cls):
- if cls._sound_player_process is not None:
- return
-
+ if cls._sound_player_process is not None: return
program = """
import numpy as np
import sounddevice as sd
import sys
import wave
-
while True:
try:
line = sys.stdin.readline()
- if not line:
- break
+ if not line: break
path, volume = line.strip().split('|')
-
sound_file = wave.open(path, 'rb')
audio = np.frombuffer(sound_file.readframes(sound_file.getnframes()), dtype=np.int16).astype(np.float32) / 32768.0
-
sd.play(audio * float(volume), sound_file.getframerate())
sd.wait()
- except Exception:
- pass
+ except: pass
"""
+ cls._sound_player_process = subprocess.Popen(["python3", "-u", "-c", program], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
- cls._sound_player_process = subprocess.Popen(
- ["python3", "-u", "-c", program],
- stdin=subprocess.PIPE,
- stdout=subprocess.DEVNULL,
- stderr=subprocess.DEVNULL,
- )
+ def _test_sound(self, key: str):
+ base_name = key.replace("Volume", "")
+ if ui_state.started:
+ self._params_memory.put("TestAlert", base_name[0].lower() + base_name[1:])
+ else:
+ self._play_sound_offroad(key)
def _play_sound_offroad(self, key: str):
base_name = key.replace("Volume", "")
snake_case = "".join(["_" + c.lower() if c.isupper() else c for c in base_name]).lstrip("_")
stock_path = Path(BASEDIR) / "selfdrive" / "assets" / "sounds" / f"{snake_case}.wav"
theme_path = ACTIVE_THEME_PATH / "sounds" / f"{snake_case}.wav"
-
sound_path = theme_path if theme_path.exists() else stock_path
- if not sound_path.exists():
- return
-
+ if not sound_path.exists(): return
volume = self._params.get_int(key, return_default=True, default=100) / 100.0
-
try:
self._sound_player_process.stdin.write(f"{sound_path}|{volume}\n".encode())
self._sound_player_process.stdin.flush()
- except Exception:
- pass
-
- def _test_sound(self, key: str):
- base_name = key.replace("Volume", "")
- if ui_state.started:
- camel_case = base_name[0].lower() + base_name[1:]
- self._params_memory.put("TestAlert", camel_case)
- else:
- self._play_sound_offroad(key)
+ except: pass
class StarPilotCustomAlertsLayout(StarPilotPanel):
ALERT_INFO = {
- "GoatScream": {
- "title": tr_noop("Goat Scream"),
- "desc": tr_noop(
- "Play the infamous \"Goat Scream\" when the steering controller reaches its limit. Based on the \"Turn Exceeds Steering Limit\" event."
- ),
- },
- "GreenLightAlert": {
- "title": tr_noop("Green Light Alert"),
- "desc": tr_noop(
- "Play an alert when the model predicts a red light has turned green.
Disclaimer: openpilot does not explicitly detect traffic lights. This alert is based on end-to-end model predictions from camera input and may trigger even when the light has not changed."
- ),
- },
- "LeadDepartingAlert": {
- "title": tr_noop("Lead Departing Alert"),
- "desc": tr_noop("Play an alert when the lead vehicle departs from a stop."),
- },
- "LoudBlindspotAlert": {
- "title": tr_noop("Loud \"Car Detected in Blindspot\" Alert"),
- "desc": tr_noop(
- "Play a louder alert if a vehicle is in the blind spot when attempting to change lanes. Based on the \"Car Detected in Blindspot\" event."
- ),
- },
- "SpeedLimitChangedAlert": {
- "title": tr_noop("Speed Limit Changed Alert"),
- "desc": tr_noop("Play an alert when the posted speed limit changes."),
- },
+ "GoatScream": {"title": tr_noop("Goat Scream"), "icon": "toggle_icons/icon_sound.png"},
+ "GreenLightAlert": {"title": tr_noop("Green Light"), "icon": "toggle_icons/icon_green_light.png"},
+ "LeadDepartingAlert": {"title": tr_noop("Lead Departure"), "icon": "toggle_icons/icon_steering.png"},
+ "LoudBlindspotAlert": {"title": tr_noop("Loud Blindspot"), "icon": "toggle_icons/icon_display.png"},
+ "SpeedLimitChangedAlert": {"title": tr_noop("Speed Limit"), "icon": "toggle_icons/icon_speed_limit.png"},
}
def __init__(self):
super().__init__()
- self._toggle_items = {}
-
+ self.CATEGORIES = []
for key in StarPilotSoundsLayout.CUSTOM_ALERTS_KEYS:
info = self.ALERT_INFO[key]
- self._toggle_items[key] = toggle_item(
- info["title"],
- info["desc"],
- self._params.get_bool(key),
- callback=lambda s, k=key: self._params.put_bool(k, s),
- )
-
- self._scroller = Scroller(list(self._toggle_items.values()), line_separator=True, spacing=0)
+ self.CATEGORIES.append({
+ "title": info["title"],
+ "type": "toggle",
+ "get_state": lambda k=key: self._params.get_bool(k),
+ "set_state": lambda s, k=key: self._params.put_bool(k, s),
+ "icon": info["icon"],
+ "color": "#FF0097",
+ "key": key # Store for visibility check
+ })
+ self._rebuild_grid()
def refresh_visibility(self):
- current_level = int(self._params.get("TuningLevel", return_default=True, default="1") or "1")
- for key, item in self._toggle_items.items():
- min_level = self._tuning_levels.get(key, 0)
- visible = current_level >= min_level
+ self._rebuild_grid()
+
+ def _rebuild_grid(self):
+ # Override to add custom BSM/SLC visibility logic
+ 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 == "LoudBlindspotAlert":
visible &= starpilot_state.car_state.hasBSM
@@ -268,4 +213,12 @@ class StarPilotCustomAlertsLayout(StarPilotPanel):
starpilot_state.car_state.hasOpenpilotLongitudinal and self._params.get_bool("SpeedLimitController")
)
- item.set_visible(visible)
+ 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)
diff --git a/selfdrive/ui/layouts/settings/starpilot/themes.py b/selfdrive/ui/layouts/settings/starpilot/themes.py
index 17301b5d..9f7a023e 100644
--- a/selfdrive/ui/layouts/settings/starpilot/themes.py
+++ b/selfdrive/ui/layouts/settings/starpilot/themes.py
@@ -1,30 +1,89 @@
from __future__ import annotations
-
+from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr, tr_noop
-from openpilot.system.ui.widgets.list_view import button_item
-from openpilot.system.ui.widgets.scroller_tici import Scroller
+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(),
+ }
- items = [
- button_item(
- tr_noop("Select Theme"),
- lambda: tr("SELECT"),
- tr_noop("Select a theme for the StarPilot interface."),
- ),
- button_item(
- tr_noop("Holiday Themes"),
- lambda: tr("MANAGE"),
- tr_noop("Enable or disable holiday-themed visuals."),
- ),
- button_item(
- tr_noop("Custom Theme"),
- lambda: tr("MANAGE"),
- tr_noop("Create or import a custom theme."),
- ),
+ self.CATEGORIES = [
+ {
+ "title": tr_noop("Personalize openpilot"),
+ "panel": "personalize",
+ "icon": "toggle_icons/icon_frog.png",
+ "color": "#A200FF",
+ "desc": tr_noop("Customize the overall look and feel.")
+ },
+ {
+ "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"
+ },
]
- self._scroller = Scroller(items, line_separator=True, spacing=0)
+ for name, panel in self._sub_panels.items():
+ if hasattr(panel, 'set_navigate_callback'): panel.set_navigate_callback(self._navigate_to)
+ if hasattr(panel, 'set_back_callback'): panel.set_back_callback(self._go_back)
+
+ self._rebuild_grid()
+
+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"},
+ ]
+ self._rebuild_grid()
+
+ def _show_theme_selector(self, key):
+ # Ported logic for theme selection. In a real environment we'd scan directories.
+ # 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 0c96fe9b..c37cffb7 100644
--- a/selfdrive/ui/layouts/settings/starpilot/utilities.py
+++ b/selfdrive/ui/layouts/settings/starpilot/utilities.py
@@ -1,30 +1,68 @@
from __future__ import annotations
-
+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.list_view import button_item
-from openpilot.system.ui.widgets.scroller_tici import Scroller
+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.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
class StarPilotUtilitiesLayout(StarPilotPanel):
def __init__(self):
super().__init__()
-
- items = [
- button_item(
- tr_noop("Update StarPilot"),
- lambda: tr("CHECK"),
- tr_noop("Check for updates and update StarPilot to the latest version."),
- ),
- button_item(
- tr_noop("Reset Settings"),
- lambda: tr("RESET"),
- tr_noop("Reset all StarPilot settings to their default values."),
- ),
- button_item(
- tr_noop("View Logs"),
- lambda: tr("VIEW"),
- tr_noop("View StarPilot logs for debugging."),
- ),
+ 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("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("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"},
]
+ self._rebuild_grid()
- self._scroller = Scroller(items, line_separator=True, spacing=0)
+ def _get_force_drive_state(self):
+ if self._params.get_bool("ForceOnroad"): return tr("Onroad")
+ if self._params.get_bool("ForceOffroad"): return tr("Offroad")
+ return tr("Default")
+
+ def _on_flash_panda(self):
+ def _do_flash(res):
+ if res == DialogResult.CONFIRM:
+ self._params_memory.put_bool("FlashPanda", True)
+ gui_app.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"):
+ self._params.put_bool("ForceOffroad", True)
+ self._params.put_bool("ForceOnroad", False)
+ elif val == tr("Onroad"):
+ self._params.put_bool("ForceOnroad", True)
+ self._params.put_bool("ForceOffroad", False)
+ else:
+ self._params.put_bool("ForceOffroad", False)
+ self._params.put_bool("ForceOnroad", False)
+ self._rebuild_grid()
+ 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.")))
+
+ def _on_report_issue(self):
+ gui_app.set_modal_overlay(alert_dialog(tr("Issue reporting not yet implemented in Python.")))
+
+ def _on_reset_toggles(self):
+ def _do_reset(res):
+ if res == DialogResult.CONFIRM:
+ # Simplified reset logic
+ all_keys = self._params.all_keys()
+ for k in all_keys:
+ 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.")))
+ self._rebuild_grid()
+ gui_app.set_modal_overlay(ConfirmDialog(tr("Reset all toggles to default?"), 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 01cc2d0d..1f2545d7 100644
--- a/selfdrive/ui/layouts/settings/starpilot/vehicle.py
+++ b/selfdrive/ui/layouts/settings/starpilot/vehicle.py
@@ -1,20 +1,192 @@
from __future__ import annotations
+import os
+import re
+from pathlib import Path
+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.list_view import button_item
-from openpilot.system.ui.widgets.scroller_tici import Scroller
+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
+
+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"
+}
+
+def get_car_names(car_make: str):
+ folder = MAKE_TO_FOLDER.get(car_make.lower())
+ 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)
+ for match in matches:
+ name = match.group(1)
+ if name.lower().startswith(car_make.lower()):
+ # 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
class StarPilotVehicleSettingsLayout(StarPilotPanel):
def __init__(self):
super().__init__()
-
- items = [
- button_item(
- tr_noop("Vehicle Settings"),
- lambda: tr("MANAGE"),
- tr_noop("Configure car-specific options like model name and features."),
- ),
+ self._sub_panels = {
+ "gm": StarPilotGMVehicleLayout(),
+ "hkg": StarPilotHKGVehicleLayout(),
+ "subaru": StarPilotSubaruVehicleLayout(),
+ "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"
+ },
+ {
+ "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"
+ },
+ {"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("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)
+
+ self._rebuild_grid()
- self._scroller = Scroller(items, line_separator=True, spacing=0)
+ 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):
+ make = self._params.get("CarMake", encoding='utf-8')
+ 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))
+ return
+
+ def on_select(res, val):
+ if res == DialogResult.CONFIRM:
+ 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()
+ 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"},
+ ]
+ self._rebuild_grid()
+
+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"},
+ ]
+ self._rebuild_grid()
+
+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"},
+ ]
+ 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"},
+ ]
+ self._rebuild_grid()
+
+ 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"))
+
+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"},
+ ]
+ self._rebuild_grid()
diff --git a/selfdrive/ui/layouts/settings/starpilot/visuals.py b/selfdrive/ui/layouts/settings/starpilot/visuals.py
index 84efe873..5958efe6 100644
--- a/selfdrive/ui/layouts/settings/starpilot/visuals.py
+++ b/selfdrive/ui/layouts/settings/starpilot/visuals.py
@@ -1,40 +1,144 @@
from __future__ import annotations
-
+from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.lib.multilang import tr, tr_noop
-from openpilot.system.ui.widgets.list_view import button_item
-from openpilot.system.ui.widgets.scroller_tici import Scroller
+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
+
+class StarPilotThemesLayout(StarPilotPanel):
+ def __init__(self):
+ super().__init__()
+ self._sub_panels = {
+ "personalize": StarPilotPersonalizeLayout(),
+ }
+ 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"},
+ ]
+ for name, panel in self._sub_panels.items():
+ if hasattr(panel, 'set_navigate_callback'): panel.set_navigate_callback(self._navigate_to)
+ if hasattr(panel, 'set_back_callback'): panel.set_back_callback(self._go_back)
+ self._rebuild_grid()
+
+class StarPilotPersonalizeLayout(StarPilotPanel):
+ def __init__(self):
+ super().__init__()
+ self.CATEGORIES = [
+ {"title": tr_noop("Boot Logo"), "type": "hub", "on_click": lambda: self._show_theme_selector("BootLogo"), "color": "#A200FF"},
+ {"title": tr_noop("Color Scheme"), "type": "hub", "on_click": lambda: self._show_theme_selector("ColorScheme"), "color": "#A200FF"},
+ {"title": tr_noop("Distance Icons"), "type": "hub", "on_click": lambda: self._show_theme_selector("DistanceIconPack"), "color": "#A200FF"},
+ {"title": tr_noop("Icon Pack"), "type": "hub", "on_click": lambda: self._show_theme_selector("IconPack"), "color": "#A200FF"},
+ {"title": tr_noop("Turn Signals"), "type": "hub", "on_click": lambda: self._show_theme_selector("SignalAnimation"), "color": "#A200FF"},
+ {"title": tr_noop("Sound Pack"), "type": "hub", "on_click": lambda: self._show_theme_selector("SoundPack"), "color": "#A200FF"},
+ {"title": tr_noop("Steering Wheel"), "type": "hub", "on_click": lambda: self._show_theme_selector("WheelIcon"), "color": "#A200FF"},
+ ]
+ self._rebuild_grid()
+
+ 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__()
-
- items = [
- button_item(
- tr_noop("Advanced UI Controls"),
- lambda: tr("MANAGE"),
- tr_noop("Advanced visual changes to fine-tune how the driving screen looks."),
- ),
- button_item(
- tr_noop("Driving Screen Widgets"),
- lambda: tr("MANAGE"),
- tr_noop("Custom StarPilot widgets for the driving screen."),
- ),
- button_item(
- tr_noop("Model UI"),
- lambda: tr("MANAGE"),
- tr_noop("Model visualizations for the driving path, lane lines, path edges, and road edges."),
- ),
- button_item(
- tr_noop("Navigation Widgets"),
- lambda: tr("MANAGE"),
- tr_noop("Speed limits, and other navigation widgets."),
- ),
- button_item(
- tr_noop("Quality of Life"),
- lambda: tr("MANAGE"),
- tr_noop("Miscellaneous visual changes to fine-tune how the driving screen looks."),
- ),
+ self._sub_panels = {
+ "advanced": StarPilotAdvancedVisualsLayout(),
+ "widgets": StarPilotVisualWidgetsLayout(),
+ "model": StarPilotModelUILayout(),
+ "navigation": StarPilotNavigationVisualsLayout(),
+ "qol": StarPilotVisualQOLLayout(),
+ }
+ self.CATEGORIES = [
+ {"title": tr_noop("Advanced UI Controls"), "panel": "advanced", "icon": "toggle_icons/icon_advanced_device.png", "color": "#A200FF"},
+ {"title": tr_noop("Driving Screen Widgets"), "panel": "widgets", "icon": "toggle_icons/icon_display.png", "color": "#A200FF"},
+ {"title": tr_noop("Model UI"), "panel": "model", "icon": "toggle_icons/icon_road.png", "color": "#A200FF"},
+ {"title": tr_noop("Navigation Widgets"), "panel": "navigation", "icon": "toggle_icons/icon_map.png", "color": "#A200FF"},
+ {"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)
+ self._rebuild_grid()
- self._scroller = Scroller(items, line_separator=True, spacing=0)
+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"},
+ ]
+ self._rebuild_grid()
+
+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"},
+ ]
+ self._rebuild_grid()
+
+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"},
+ ]
+ self._rebuild_grid()
+
+ 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 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="#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"},
+ ]
+ self._rebuild_grid()
+
+class StarPilotVisualQOLLayout(StarPilotPanel):
+ def __init__(self):
+ super().__init__()
+ 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"},
+ ]
+ self._rebuild_grid()
diff --git a/selfdrive/ui/layouts/settings/starpilot/wheel.py b/selfdrive/ui/layouts/settings/starpilot/wheel.py
index 78827082..f144c4d0 100644
--- a/selfdrive/ui/layouts/settings/starpilot/wheel.py
+++ b/selfdrive/ui/layouts/settings/starpilot/wheel.py
@@ -1,20 +1,22 @@
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.list_view import button_item
-from openpilot.system.ui.widgets.scroller_tici import Scroller
+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 StarPilotWheelLayout(StarPilotPanel):
def __init__(self):
super().__init__()
-
- items = [
- button_item(
- tr_noop("Wheel Controls"),
- lambda: tr("MANAGE"),
- tr_noop("Configure steering wheel button mappings for custom controls."),
- ),
+ self.CATEGORIES = [
+ {
+ "title": tr_noop("Remap Cancel Button"),
+ "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"
+ },
]
-
- self._scroller = Scroller(items, line_separator=True, spacing=0)
+ self._rebuild_grid()