This commit is contained in:
firestarsdog
2026-03-22 19:39:26 -04:00
committed by firestar5683
parent 8258b6db84
commit 4d5d48dbe8
16 changed files with 2042 additions and 1348 deletions
+107 -15
View File
@@ -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("<b>Create, restore, or delete backups</b> of your StarPilot settings."),
),
button_item(
tr_noop("Manage Storage"),
lambda: tr("MANAGE"),
tr_noop("<b>View and manage storage usage</b> 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.")))
+157 -16
View File
@@ -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("<b>Adjust screen brightness, timeout,</b> and other display settings."),
),
button_item(
tr_noop("Device Settings"),
lambda: tr("MANAGE"),
tr_noop("<b>Configure device-specific options</b> 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)
@@ -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("<b>Driving models are chosen at random each drive</b> 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("<b>Automatically download new driving models</b> 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("<b>Select the active driving model.</b>"),
callback=self._on_select_model_clicked,
)
self._download_model_btn = button_item(
tr_noop("Download Driving Models"),
lambda: tr("DOWNLOAD"),
tr_noop("<b>Download driving models to the device.</b>"),
callback=self._on_download_clicked,
)
self._delete_model_btn = button_item(
tr_noop("Delete Driving Models"),
lambda: tr("DELETE"),
tr_noop("<b>Delete driving models from the device.</b>"),
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("<b>Add or remove models</b> from the Model Randomizer's blacklist."),
callback=self._on_blacklist_clicked,
),
button_item(
tr_noop("Manage Model Ratings"),
lambda: tr("MANAGE"),
tr_noop("<b>Reset or view the saved ratings</b> 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!")
@@ -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(
"<b>The time between openpilot's steering command and the vehicle's response.</b> 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(
"<b>Compensates for steering friction.</b> 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(
"<b>How strongly openpilot corrects lane position.</b> 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(
"<b>Maps steering torque to turning response.</b> 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(
"<b>The relationship between steering wheel rotation and road wheel angle.</b> 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("<b>Force-enable openpilot's live auto-tuning for \"Friction\" and \"Lateral Acceleration\".</b>"),
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("<b>Force-disable openpilot's live auto-tuning for \"Friction\" and \"Lateral Acceleration\" and use the set value instead.</b>"),
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("<b>Use torque-based steering control instead of angle-based control for smoother lane keeping, especially in curves.</b>"),
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("<b>openpilot's steering remains active even when the accelerator or brake pedals are pressed.</b>"),
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("<b>Enable \"Always On Lateral\" whenever \"LKAS\" is on, even when openpilot is not engaged.</b>"),
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("<b>Pause \"Always On Lateral\" below the set speed while the brake pedal is pressed.</b>"),
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("<b>Allow openpilot to change lanes.</b>"),
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("<b>When the turn signal is on, openpilot will automatically change lanes.</b> 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("<b>Delay between turn signal activation and the start of an automatic lane change.</b>"),
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("<b>Lowest speed at which openpilot will change lanes.</b>"),
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("<b>Prevent automatic lane changes into lanes narrower than the set width.</b>"),
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("<b>Limit automatic lane changes to one per turn-signal activation.</b>"),
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("<b>While driving below the minimum lane change speed with an active turn signal, instruct openpilot to turn left/right.</b>"),
self._params.get_bool("TurnDesires"),
callback=lambda x: self._params.put_bool("TurnDesires", x),
),
toggle_item(
tr_noop("Neural Network Feedforward (NNFF)"),
tr_noop(
"<b>Twilsonco's \"Neural Network FeedForward\" controller.</b> 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(
"<b>A lightweight version of Twilsonco's \"Neural Network FeedForward\" controller.</b> 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("<b>Pause steering below the set speed.</b>"),
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("<b>Advanced steering control changes to fine-tune how openpilot drives.</b>"),
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("<b>openpilot's steering remains active even when the accelerator or brake pedals are pressed.</b>"),
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("<b>Allow openpilot to change lanes.</b>"),
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("<b>Miscellaneous steering control changes</b> 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("<b>Steering control changes to fine-tune how openpilot drives.</b>"),
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()
@@ -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("<b>Advanced acceleration and braking control changes</b> to fine-tune how openpilot drives."),
),
button_item(
tr_noop("Conditional Experimental Mode"),
lambda: tr("MANAGE"),
tr_noop("<b>Automatically switch to Experimental Mode</b> when set conditions are met."),
),
button_item(
tr_noop("Curve Speed Controller"),
lambda: tr("MANAGE"),
tr_noop("<b>Automatically slow down for upcoming curves</b> using data learned from your driving style."),
),
button_item(
tr_noop("Driving Personalities"),
lambda: tr("MANAGE"),
tr_noop("<b>Customize the Driving Personalities</b> to better match your driving style."),
),
button_item(
tr_noop("Longitudinal Tuning"),
lambda: tr("MANAGE"),
tr_noop("<b>Acceleration and braking control changes</b> to fine-tune how openpilot drives."),
),
button_item(
tr_noop("Quality of Life"),
lambda: tr("MANAGE"),
tr_noop("<b>Miscellaneous acceleration and braking control changes</b> to fine-tune how openpilot drives."),
),
button_item(
tr_noop("Weather"),
lambda: tr("MANAGE"),
tr_noop("<b>Adjust driving behavior</b> 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("<b>Driving adjustments for fog, haze, or other low-visibility conditions.</b>"),
callback=lambda: self._navigate("low_visibility"),
),
button_item(
tr_noop("Rain"),
lambda: tr("MANAGE"),
tr_noop("<b>Driving adjustments for rainy conditions.</b>"),
callback=lambda: self._navigate("rain"),
),
button_item(
tr_noop("Rainstorms"),
lambda: tr("MANAGE"),
tr_noop("<b>Driving adjustments for rainstorms.</b>"),
callback=lambda: self._navigate("rainstorm"),
),
button_item(
tr_noop("Snow"),
lambda: tr("MANAGE"),
tr_noop("<b>Driving adjustments for snowy conditions.</b>"),
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("<b>Add extra space behind lead vehicles in low visibility.</b> 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("<b>Add extra buffer when stopped behind vehicles in low visibility.</b> 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(
"<b>Lower the maximum acceleration in low visibility.</b> 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(
"<b>Lower the desired speed while driving through curves in low visibility.</b> 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("<b>Add extra space behind lead vehicles in rain.</b> 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("<b>Add extra buffer when stopped behind vehicles in rain.</b> 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("<b>Lower the maximum acceleration in rain.</b> 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(
"<b>Lower the desired speed while driving through curves in rain.</b> 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("<b>Add extra space behind lead vehicles in a rainstorm.</b> 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("<b>Add extra buffer when stopped behind vehicles in a rainstorm.</b> 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("<b>Add extra buffer when stopped behind vehicles in rain.</b> 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("<b>Lower the maximum acceleration in rain.</b> 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(
"<b>Lower the desired speed while driving through curves in rain.</b> 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("<b>Add extra space behind lead vehicles in snow.</b> 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("<b>Add extra buffer when stopped behind vehicles in snow.</b> 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("<b>Lower the maximum acceleration in snow.</b> 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(
"<b>Lower the desired speed while driving through curves in snow.</b> 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"))
@@ -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": "<b>Adjust alert volumes and enable custom notifications.</b>",
"desc": "Adjust alert volumes and enable custom notifications.",
"buttons": [("MANAGE", "SOUNDS", 0)],
"color": "#FF0097",
},
{
"title": "Driving Controls",
"icon": "icon_steering.png",
"desc": "<b>Fine-tune custom StarPilot acceleration, braking, and steering controls.</b>",
"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": "<b>Download map data for the Speed Limit Controller.</b>",
"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": "<b>Manage backups, device settings, screen options, storage, and tools to keep StarPilot running smoothly.</b>",
"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": "<b>Customize the look of the driving screen and interface, including themes!</b>",
"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": "<b>Configure car-specific options and steering wheel button mappings.</b>",
"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):
+153 -14
View File
@@ -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("<b>Download map data</b> for the Speed Limit Controller."),
),
button_item(
tr_noop("Manage Map Data"),
lambda: tr("MANAGE"),
tr_noop("<b>View or delete downloaded map data.</b>"),
),
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))
@@ -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
@@ -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("<b>Use live map data</b> for real-time navigation updates."),
False,
),
button_item(
tr_noop("Navigation Settings"),
lambda: tr("MANAGE"),
tr_noop("<b>Configure navigation-specific options</b> 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))
@@ -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:
+116 -163
View File
@@ -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("<b>Set how loud each type of openpilot alert is</b> 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("<b>Optional StarPilot alerts</b> 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(
"<b>Set the volume for alerts when openpilot disengages.</b><br><br>Examples include: \"Cruise Fault: Restart the Car\", \"Parking Brake Engaged\", \"Pedal Pressed\"."
),
"min": 0,
},
"EngageVolume": {
"title": tr_noop("Engage Volume"),
"desc": tr_noop("<b>Set the volume for the chime when openpilot engages</b>, such as after pressing the \"RESUME\" or \"SET\" steering wheel buttons."),
"min": 0,
},
"PromptVolume": {
"title": tr_noop("Prompt Volume"),
"desc": tr_noop(
"<b>Set the volume for prompts that need attention.</b><br><br>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(
"<b>Set the volume for prompts when openpilot detects driver distraction or unresponsiveness.</b><br><br>Examples include: \"Pay Attention\", \"Touch Steering Wheel\"."
),
"min": 0,
},
"RefuseVolume": {
"title": tr_noop("Refuse Volume"),
"desc": tr_noop(
"<b>Set the volume for alerts when openpilot refuses to engage.</b><br><br>Examples include: \"Brake Hold Active\", \"Door Open\", \"Seatbelt Unlatched\"."
),
"min": 0,
},
"WarningSoftVolume": {
"title": tr_noop("Warning Soft Volume"),
"desc": tr_noop(
"<b>Set the volume for softer warnings about potential risks.</b><br><br>Examples include: \"BRAKE! Risk of Collision\", \"Steering Temporarily Unavailable\"."
),
"min": 25,
},
"WarningImmediateVolume": {
"title": tr_noop("Warning Immediate Volume"),
"desc": tr_noop(
"<b>Set the volume for the loudest warnings that require urgent attention.</b><br><br>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(
"<b>Play the infamous \"Goat Scream\" when the steering controller reaches its limit.</b> Based on the \"Turn Exceeds Steering Limit\" event."
),
},
"GreenLightAlert": {
"title": tr_noop("Green Light Alert"),
"desc": tr_noop(
"<b>Play an alert when the model predicts a red light has turned green.</b><br><br><i><b>Disclaimer</b>: 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.</i>"
),
},
"LeadDepartingAlert": {
"title": tr_noop("Lead Departing Alert"),
"desc": tr_noop("<b>Play an alert when the lead vehicle departs from a stop.</b>"),
},
"LoudBlindspotAlert": {
"title": tr_noop("Loud \"Car Detected in Blindspot\" Alert"),
"desc": tr_noop(
"<b>Play a louder alert if a vehicle is in the blind spot when attempting to change lanes.</b> Based on the \"Car Detected in Blindspot\" event."
),
},
"SpeedLimitChangedAlert": {
"title": tr_noop("Speed Limit Changed Alert"),
"desc": tr_noop("<b>Play an alert when the posted speed limit changes.</b>"),
},
"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)
@@ -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("<b>Select a theme</b> for the StarPilot interface."),
),
button_item(
tr_noop("Holiday Themes"),
lambda: tr("MANAGE"),
tr_noop("<b>Enable or disable holiday-themed visuals.</b>"),
),
button_item(
tr_noop("Custom Theme"),
lambda: tr("MANAGE"),
tr_noop("<b>Create or import a custom theme.</b>"),
),
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))
@@ -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("<b>Check for updates</b> and update StarPilot to the latest version."),
),
button_item(
tr_noop("Reset Settings"),
lambda: tr("RESET"),
tr_noop("<b>Reset all StarPilot settings</b> to their default values."),
),
button_item(
tr_noop("View Logs"),
lambda: tr("VIEW"),
tr_noop("<b>View StarPilot logs</b> 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))
@@ -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("<b>Configure car-specific options</b> 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()
@@ -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("<b>Advanced visual changes</b> to fine-tune how the driving screen looks."),
),
button_item(
tr_noop("Driving Screen Widgets"),
lambda: tr("MANAGE"),
tr_noop("<b>Custom StarPilot widgets</b> for the driving screen."),
),
button_item(
tr_noop("Model UI"),
lambda: tr("MANAGE"),
tr_noop("<b>Model visualizations</b> for the driving path, lane lines, path edges, and road edges."),
),
button_item(
tr_noop("Navigation Widgets"),
lambda: tr("MANAGE"),
tr_noop("<b>Speed limits, and other navigation widgets.</b>"),
),
button_item(
tr_noop("Quality of Life"),
lambda: tr("MANAGE"),
tr_noop("<b>Miscellaneous visual changes</b> 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()
@@ -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("<b>Configure steering wheel button mappings</b> 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()