mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-10 20:02:13 +08:00
Rays
This commit is contained in:
+111
-15
@@ -1,4 +1,6 @@
|
||||
import json
|
||||
import time
|
||||
import datetime
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from enum import IntEnum
|
||||
@@ -7,6 +9,8 @@ from openpilot.selfdrive.ui.widgets.offroad_alerts import UpdateAlert, OffroadAl
|
||||
from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButton
|
||||
from openpilot.selfdrive.ui.widgets.prime import PrimeWidget
|
||||
from openpilot.selfdrive.ui.widgets.setup import SetupWidget
|
||||
from openpilot.selfdrive.ui.widgets.drive_summary import DriveSummary, RANDOM_EVENTS
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr, trn
|
||||
@@ -58,6 +62,14 @@ class HomeLayout(Widget):
|
||||
self._prime_widget = PrimeWidget()
|
||||
self._setup_widget = SetupWidget()
|
||||
|
||||
self._drive_summary = DriveSummary(show_random_events=False)
|
||||
self._random_events_summary = DriveSummary(show_random_events=True)
|
||||
self._drive_summary_active = False
|
||||
self._random_events_active = False
|
||||
self._last_onroad_stats: dict = {}
|
||||
|
||||
ui_state.add_offroad_transition_callback(self._on_offroad_transition)
|
||||
|
||||
self._exp_mode_button = ExperimentalModeButton()
|
||||
self._setup_callbacks()
|
||||
|
||||
@@ -71,6 +83,34 @@ class HomeLayout(Widget):
|
||||
self.offroad_alert.set_dismiss_callback(lambda: self._set_state(HomeLayoutState.HOME))
|
||||
self._exp_mode_button.set_click_callback(lambda: self.settings_callback() if self.settings_callback else None)
|
||||
|
||||
def _on_offroad_transition(self):
|
||||
"""Called when transitioning from onroad to offroad."""
|
||||
raw = self.params.get("FrogPilotStats")
|
||||
if raw:
|
||||
try:
|
||||
current_stats = json.loads(raw)
|
||||
prev = self._last_onroad_stats or {}
|
||||
# Show drive summary if there's new drive data
|
||||
if current_stats.get("TrackedTime", 0) > prev.get("TrackedTime", 0):
|
||||
self._drive_summary.set_previous_stats(prev)
|
||||
self._drive_summary.show_event()
|
||||
self._drive_summary_active = True
|
||||
# Random events
|
||||
if ui_state.frogpilot_toggles.get("random_events", False):
|
||||
cur_ev = current_stats.get("RandomEvents", {})
|
||||
prev_ev = prev.get("RandomEvents", {})
|
||||
if any(cur_ev.get(k, 0) > prev_ev.get(k, 0) for k, _ in RANDOM_EVENTS):
|
||||
self._random_events_summary.set_previous_stats(prev)
|
||||
self._random_events_summary.show_event()
|
||||
self._random_events_active = True
|
||||
self._last_onroad_stats = dict(current_stats)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
|
||||
def _dismiss_drive_summary(self):
|
||||
self._drive_summary_active = False
|
||||
self._random_events_active = False
|
||||
|
||||
def set_settings_callback(self, callback: Callable):
|
||||
self.settings_callback = callback
|
||||
|
||||
@@ -104,24 +144,18 @@ class HomeLayout(Widget):
|
||||
self._render_alerts_view()
|
||||
|
||||
def _update_state(self):
|
||||
self.header_rect = rl.Rectangle(
|
||||
self._rect.x + CONTENT_MARGIN, self._rect.y + CONTENT_MARGIN, self._rect.width - 2 * CONTENT_MARGIN, HEADER_HEIGHT
|
||||
)
|
||||
self.header_rect = rl.Rectangle(self._rect.x + CONTENT_MARGIN, self._rect.y + CONTENT_MARGIN, self._rect.width - 2 * CONTENT_MARGIN, HEADER_HEIGHT)
|
||||
|
||||
content_y = self._rect.y + CONTENT_MARGIN + HEADER_HEIGHT + SPACING
|
||||
content_height = self._rect.height - CONTENT_MARGIN - HEADER_HEIGHT - SPACING - CONTENT_MARGIN
|
||||
|
||||
self.content_rect = rl.Rectangle(
|
||||
self._rect.x + CONTENT_MARGIN, content_y, self._rect.width - 2 * CONTENT_MARGIN, content_height
|
||||
)
|
||||
self.content_rect = rl.Rectangle(self._rect.x + CONTENT_MARGIN, content_y, self._rect.width - 2 * CONTENT_MARGIN, content_height)
|
||||
|
||||
left_width = self.content_rect.width - RIGHT_COLUMN_WIDTH - SPACING
|
||||
|
||||
self.left_column_rect = rl.Rectangle(self.content_rect.x, self.content_rect.y, left_width, self.content_rect.height)
|
||||
|
||||
self.right_column_rect = rl.Rectangle(
|
||||
self.content_rect.x + left_width + SPACING, self.content_rect.y, RIGHT_COLUMN_WIDTH, self.content_rect.height
|
||||
)
|
||||
self.right_column_rect = rl.Rectangle(self.content_rect.x + left_width + SPACING, self.content_rect.y, RIGHT_COLUMN_WIDTH, self.content_rect.height)
|
||||
|
||||
self.update_notif_rect.x = self.header_rect.x
|
||||
self.update_notif_rect.y = self.header_rect.y + (self.header_rect.height - 60) // 2
|
||||
@@ -133,6 +167,12 @@ class HomeLayout(Widget):
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
# Dismiss drive summary on click anywhere in content area
|
||||
if self._drive_summary_active:
|
||||
if rl.check_collision_point_rec(mouse_pos, self.left_column_rect) or rl.check_collision_point_rec(mouse_pos, self.right_column_rect):
|
||||
self._dismiss_drive_summary()
|
||||
return
|
||||
|
||||
if self.update_available and rl.check_collision_point_rec(mouse_pos, self.update_notif_rect):
|
||||
self._set_state(HomeLayoutState.UPDATE)
|
||||
elif self.alert_count > 0 and rl.check_collision_point_rec(mouse_pos, self.alert_notif_rect):
|
||||
@@ -143,6 +183,15 @@ class HomeLayout(Widget):
|
||||
|
||||
version_text_width = self.header_rect.width
|
||||
|
||||
# FrogPilot: date display (left-aligned)
|
||||
date_text = datetime.datetime.now().strftime("%A, %B %-d")
|
||||
date_font = gui_app.font(FontWeight.NORMAL)
|
||||
date_size = 32
|
||||
date_ts = measure_text_cached(date_font, date_text, date_size)
|
||||
date_x = self.header_rect.x
|
||||
date_y = self.header_rect.y + (self.header_rect.height - date_ts.y) // 2
|
||||
rl.draw_text_ex(date_font, date_text, rl.Vector2(int(date_x), int(date_y)), date_size, 0, rl.Color(255, 255, 255, 150))
|
||||
|
||||
# Update notification button
|
||||
if self.update_available:
|
||||
version_text_width -= self.update_notif_rect.width
|
||||
@@ -175,8 +224,9 @@ class HomeLayout(Widget):
|
||||
if self.update_available or self.alert_count > 0:
|
||||
version_text_width -= SPACING * 1.5
|
||||
|
||||
version_rect = rl.Rectangle(self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y,
|
||||
version_text_width, self.header_rect.height)
|
||||
version_rect = rl.Rectangle(
|
||||
self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y, version_text_width, self.header_rect.height
|
||||
)
|
||||
gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT)
|
||||
|
||||
def _render_home_content(self):
|
||||
@@ -190,13 +240,19 @@ class HomeLayout(Widget):
|
||||
self.offroad_alert.render(self.content_rect)
|
||||
|
||||
def _render_left_column(self):
|
||||
self._prime_widget.render(self.left_column_rect)
|
||||
if self._drive_summary_active:
|
||||
self._drive_summary.render(self.left_column_rect)
|
||||
else:
|
||||
self._prime_widget.render(self.left_column_rect)
|
||||
self._render_frogpilot_stats()
|
||||
|
||||
def _render_right_column(self):
|
||||
if self._random_events_active:
|
||||
self._random_events_summary.render(self.right_column_rect)
|
||||
return
|
||||
|
||||
exp_height = 125
|
||||
exp_rect = rl.Rectangle(
|
||||
self.right_column_rect.x, self.right_column_rect.y, self.right_column_rect.width, exp_height
|
||||
)
|
||||
exp_rect = rl.Rectangle(self.right_column_rect.x, self.right_column_rect.y, self.right_column_rect.width, exp_height)
|
||||
self._exp_mode_button.render(exp_rect)
|
||||
|
||||
setup_rect = rl.Rectangle(
|
||||
@@ -207,6 +263,46 @@ class HomeLayout(Widget):
|
||||
)
|
||||
self._setup_widget.render(setup_rect)
|
||||
|
||||
def _render_frogpilot_stats(self):
|
||||
"""Render FrogPilot lifetime stats (Drives/Distance/Hours) at bottom of left column."""
|
||||
raw = self.params.get("FrogPilotStats")
|
||||
if not raw:
|
||||
return
|
||||
try:
|
||||
stats = json.loads(raw)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return
|
||||
|
||||
drives = stats.get("FrogPilotDrives", 0)
|
||||
meters = stats.get("FrogPilotMeters", 0)
|
||||
hours = int(stats.get("FrogPilotSeconds", 0) / 3600)
|
||||
|
||||
if ui_state.is_metric:
|
||||
dist_str = f"{meters / 1000:.0f} km"
|
||||
else:
|
||||
dist_str = f"{meters * 0.000621371:.0f} mi"
|
||||
|
||||
font = gui_app.font(FontWeight.NORMAL)
|
||||
label_size = 24
|
||||
val_size = 36
|
||||
green = rl.Color(0x17, 0x86, 0x44, 255)
|
||||
|
||||
# Position at bottom of left column
|
||||
y = self.left_column_rect.y + self.left_column_rect.height - 100
|
||||
x = self.left_column_rect.x
|
||||
col_w = self.left_column_rect.width / 3
|
||||
|
||||
stats_data = [("Drives", str(drives)), ("Distance", dist_str), ("Hours", str(hours))]
|
||||
|
||||
for i, (label, value) in enumerate(stats_data):
|
||||
cx = x + col_w * i + col_w / 2
|
||||
# Value
|
||||
vs = measure_text_cached(font, value, val_size)
|
||||
rl.draw_text_ex(font, value, rl.Vector2(cx - vs.x / 2, y), val_size, 0, rl.WHITE)
|
||||
# Label
|
||||
ls = measure_text_cached(font, label, label_size)
|
||||
rl.draw_text_ex(font, label, rl.Vector2(cx - ls.x / 2, y + val_size + 5), label_size, 0, green)
|
||||
|
||||
def _refresh(self):
|
||||
self._version_text = self._get_version_text()
|
||||
update_available = self.update_alert.refresh()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import time
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
from openpilot.common.time_helpers import system_time_valid
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
@@ -76,15 +77,20 @@ class SoftwareLayout(Widget):
|
||||
self._branch_btn.action_item.set_value(ui_state.params.get("UpdaterTargetBranch") or "")
|
||||
self._branch_dialog: MultiOptionDialog | None = None
|
||||
|
||||
self._scroller = Scroller([
|
||||
self._onroad_label,
|
||||
self._version_item,
|
||||
self._auto_updates_toggle,
|
||||
self._download_btn,
|
||||
self._install_btn,
|
||||
self._branch_btn,
|
||||
button_item(lambda: tr("Uninstall"), lambda: tr("UNINSTALL"), callback=self._on_uninstall),
|
||||
], line_separator=True, spacing=0)
|
||||
self._scroller = Scroller(
|
||||
[
|
||||
self._onroad_label,
|
||||
self._version_item,
|
||||
self._auto_updates_toggle,
|
||||
self._download_btn,
|
||||
self._install_btn,
|
||||
self._branch_btn,
|
||||
button_item(lambda: tr("Uninstall"), lambda: tr("UNINSTALL"), callback=self._on_uninstall),
|
||||
button_item(lambda: tr("Error Log"), lambda: tr("VIEW"), callback=self._on_error_log),
|
||||
],
|
||||
line_separator=True,
|
||||
spacing=0,
|
||||
)
|
||||
|
||||
def show_event(self):
|
||||
self._scroller.show_event()
|
||||
@@ -170,18 +176,43 @@ class SoftwareLayout(Widget):
|
||||
# Start downloading
|
||||
self._waiting_for_updater = True
|
||||
self._waiting_start_ts = time.monotonic()
|
||||
ui_state.params_memory.put_bool("ManualUpdateInitiated", True)
|
||||
os.system("pkill -SIGHUP -f system.updated.updated")
|
||||
|
||||
def _on_auto_updates_toggle(self, enabled: bool):
|
||||
ui_state.params.put_bool("AutomaticUpdates", enabled)
|
||||
|
||||
def _on_uninstall(self):
|
||||
def handle_uninstall_confirmation(result):
|
||||
def handle_step1(result):
|
||||
if result == DialogResult.CONFIRM:
|
||||
ui_state.params.put_bool("DoUninstall", True)
|
||||
|
||||
def handle_step2(result2):
|
||||
if result2 == DialogResult.CONFIRM:
|
||||
|
||||
def handle_step3(result3):
|
||||
if result3 == DialogResult.CONFIRM:
|
||||
ui_state.params.clear_all()
|
||||
ui_state.params.put_bool("DoUninstall", True)
|
||||
|
||||
dialog = ConfirmDialog(tr("This is a complete factory reset and cannot be undone. Are you absolutely sure?"), tr("Reset"))
|
||||
gui_app.set_modal_overlay(dialog, callback=handle_step3)
|
||||
else:
|
||||
ui_state.params.put_bool("DoUninstall", True)
|
||||
|
||||
dialog = ConfirmDialog(
|
||||
tr("Do you want to perform a full factory reset? All saved assets and settings will be permanently deleted!"), tr("Factory Reset"), tr("Skip")
|
||||
)
|
||||
gui_app.set_modal_overlay(dialog, callback=handle_step2)
|
||||
|
||||
dialog = ConfirmDialog(tr("Are you sure you want to uninstall?"), tr("Uninstall"))
|
||||
gui_app.set_modal_overlay(dialog, callback=handle_uninstall_confirmation)
|
||||
gui_app.set_modal_overlay(dialog, callback=handle_step1)
|
||||
|
||||
def _on_error_log(self):
|
||||
try:
|
||||
txt = Path("/data/error_logs/error.txt").read_text(encoding='utf-8', errors='replace')
|
||||
except Exception:
|
||||
txt = tr("No error log found.")
|
||||
gui_app.set_modal_overlay(ConfirmDialog(txt, tr("OK"), on_close=lambda r: None, rich=True))
|
||||
|
||||
def _on_install_update(self):
|
||||
# Trigger reboot to install update
|
||||
|
||||
@@ -1,25 +1,233 @@
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import shutil
|
||||
import threading
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.widgets.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.system.ui.widgets.input_dialog import InputDialog
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
|
||||
|
||||
|
||||
class StarPilotDataLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
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("Toggle Backups"), "panel": "toggle_backups", "icon": "toggle_icons/icon_system.png", "color": "#FA6800"},
|
||||
{"title": tr_noop("Manage Storage"), "panel": "storage", "icon": "toggle_icons/icon_system.png", "color": "#FA6800"},
|
||||
{
|
||||
"title": tr_noop("Delete Driving Data"),
|
||||
"type": "hub",
|
||||
"on_click": self._on_delete_driving_data,
|
||||
"icon": "toggle_icons/icon_system.png",
|
||||
"color": "#FA6800",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Delete Error Logs"),
|
||||
"type": "hub",
|
||||
"on_click": self._on_delete_error_logs,
|
||||
"icon": "toggle_icons/icon_system.png",
|
||||
"color": "#FA6800",
|
||||
},
|
||||
]
|
||||
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
self._sub_panels = {
|
||||
"backups": StarPilotBackupsLayout(),
|
||||
"toggle_backups": StarPilotToggleBackupsLayout(),
|
||||
"storage": StarPilotStorageLayout(),
|
||||
}
|
||||
|
||||
for name, panel in self._sub_panels.items():
|
||||
if hasattr(panel, 'set_navigate_callback'):
|
||||
panel.set_navigate_callback(self._navigate_to)
|
||||
if hasattr(panel, 'set_back_callback'):
|
||||
panel.set_back_callback(self._go_back)
|
||||
|
||||
self._rebuild_grid()
|
||||
|
||||
def _on_delete_driving_data(self):
|
||||
def _do_delete(res):
|
||||
if res == DialogResult.CONFIRM:
|
||||
|
||||
def _task():
|
||||
drive_paths = ["/data/media/0/realdata/", "/data/media/0/realdata_HD/", "/data/media/0/realdata_konik/"]
|
||||
for path in drive_paths:
|
||||
p = Path(path)
|
||||
if p.exists():
|
||||
for entry in p.iterdir():
|
||||
if entry.is_dir():
|
||||
shutil.rmtree(entry, ignore_errors=True)
|
||||
|
||||
threading.Thread(target=_task, daemon=True).start()
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Driving data deletion started.")))
|
||||
|
||||
gui_app.set_modal_overlay(ConfirmDialog(tr("Delete all driving data and footage?"), tr("Delete"), on_close=_do_delete))
|
||||
|
||||
def _on_delete_error_logs(self):
|
||||
def _do_delete(res):
|
||||
if res == DialogResult.CONFIRM:
|
||||
shutil.rmtree("/data/error_logs", ignore_errors=True)
|
||||
os.makedirs("/data/error_logs", exist_ok=True)
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Error logs deleted.")))
|
||||
|
||||
gui_app.set_modal_overlay(ConfirmDialog(tr("Delete all error logs?"), tr("Delete"), on_close=_do_delete))
|
||||
|
||||
|
||||
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):
|
||||
def on_name(res, name):
|
||||
if res == DialogResult.CONFIRM:
|
||||
safe_name = name.replace(" ", "_") if name else f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||
backup_path = f"/data/backups/{safe_name}.tar.zst"
|
||||
if Path(backup_path).exists():
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("A backup with this name already exists.")))
|
||||
return
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Backup creation started.")))
|
||||
|
||||
def _task():
|
||||
os.makedirs("/data/backups", exist_ok=True)
|
||||
subprocess.run(["tar", "--use-compress-program=zstd", "-cf", backup_path, "/data/openpilot"])
|
||||
|
||||
threading.Thread(target=_task, daemon=True).start()
|
||||
|
||||
gui_app.set_modal_overlay(InputDialog(tr("Name your backup"), "", on_close=on_name))
|
||||
|
||||
def _on_restore_backup(self):
|
||||
backups = self._get_backups()
|
||||
if not backups:
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("No backups found.")))
|
||||
return
|
||||
|
||||
def _on_select(res, val):
|
||||
if res == DialogResult.CONFIRM:
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Restoring... device will reboot.")))
|
||||
|
||||
def _task():
|
||||
subprocess.run(["rm", "-rf", "/data/openpilot/*"])
|
||||
subprocess.run(["tar", "--use-compress-program=zstd", "-xf", f"/data/backups/{val}", "-C", "/"])
|
||||
os.system("reboot")
|
||||
|
||||
threading.Thread(target=_task, daemon=True).start()
|
||||
|
||||
gui_app.set_modal_overlay(SelectionDialog(tr("Select Backup"), backups, on_close=_on_select))
|
||||
|
||||
def _on_delete_backup(self):
|
||||
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 StarPilotToggleBackupsLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.CATEGORIES = [
|
||||
{"title": tr_noop("Create Toggle Backup"), "type": "hub", "on_click": self._on_create, "color": "#FA6800"},
|
||||
{"title": tr_noop("Restore Toggle Backup"), "type": "hub", "on_click": self._on_restore, "color": "#FA6800"},
|
||||
{"title": tr_noop("Delete Toggle Backup"), "type": "hub", "on_click": self._on_delete, "color": "#FA6800"},
|
||||
]
|
||||
self._rebuild_grid()
|
||||
|
||||
def _get_backups(self):
|
||||
b_dir = Path("/data/toggle_backups")
|
||||
if not b_dir.exists():
|
||||
return []
|
||||
return [d.name for d in b_dir.iterdir() if d.is_dir() and "in_progress" not in d.name]
|
||||
|
||||
def _on_create(self):
|
||||
def on_name(res, name):
|
||||
if res == DialogResult.CONFIRM:
|
||||
safe_name = name.replace(" ", "_") if name else f"toggle_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||
backup_path = Path(f"/data/toggle_backups/{safe_name}")
|
||||
if backup_path.exists():
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("A toggle backup with this name already exists.")))
|
||||
return
|
||||
os.makedirs(backup_path, exist_ok=True)
|
||||
shutil.copytree("/data/params/d", str(backup_path), dirs_exist_ok=True)
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Toggle backup created.")))
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(InputDialog(tr("Name your toggle backup"), "", on_close=on_name))
|
||||
|
||||
def _on_restore(self):
|
||||
backups = self._get_backups()
|
||||
if not backups:
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("No toggle backups found.")))
|
||||
return
|
||||
|
||||
def _on_select(res, val):
|
||||
if res == DialogResult.CONFIRM:
|
||||
|
||||
def on_confirm(r2):
|
||||
if r2 == DialogResult.CONFIRM:
|
||||
src = Path(f"/data/toggle_backups/{val}")
|
||||
shutil.copytree(str(src), "/data/params/d", dirs_exist_ok=True)
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Toggles restored.")))
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(ConfirmDialog(tr("This will overwrite your current toggles."), tr("Restore"), on_close=on_confirm))
|
||||
|
||||
gui_app.set_modal_overlay(SelectionDialog(tr("Select Toggle Backup"), backups, on_close=_on_select))
|
||||
|
||||
def _on_delete(self):
|
||||
backups = self._get_backups()
|
||||
if not backups:
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("No toggle backups found.")))
|
||||
return
|
||||
|
||||
def _on_select(res, val):
|
||||
if res == DialogResult.CONFIRM:
|
||||
shutil.rmtree(f"/data/toggle_backups/{val}", ignore_errors=True)
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(SelectionDialog(tr("Delete Toggle Backup"), backups, on_close=_on_select))
|
||||
|
||||
|
||||
class StarPilotStorageLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.CATEGORIES = [
|
||||
{"title": tr_noop("Driving Data"), "type": "value", "get_value": self._get_storage, "on_click": lambda: None, "color": "#FA6800"},
|
||||
]
|
||||
self._rebuild_grid()
|
||||
|
||||
def _get_storage(self):
|
||||
paths = ["/data/media/0/osm/offline", "/data/media/0/realdata", "/data/backups"]
|
||||
total = 0
|
||||
for p in paths:
|
||||
pp = Path(p)
|
||||
if pp.exists():
|
||||
total += sum(f.stat().st_size for f in pp.rglob('*') if f.is_file())
|
||||
mb = total / (1024 * 1024)
|
||||
if mb > 1024:
|
||||
return f"{(mb / 1024):.2f} GB"
|
||||
return f"{mb:.2f} MB"
|
||||
|
||||
@@ -0,0 +1,446 @@
|
||||
from __future__ import annotations
|
||||
from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.widgets import DialogResult
|
||||
from openpilot.system.ui.widgets.selection_dialog import SelectionDialog
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.metro import TileGrid, ToggleTile
|
||||
|
||||
|
||||
SIDEBAR_METRIC_OPTIONS = [
|
||||
"None",
|
||||
"Acceleration: Current",
|
||||
"Acceleration: Max",
|
||||
"Auto Tune: Actuator Delay",
|
||||
"Auto Tune: Friction",
|
||||
"Auto Tune: Lateral Acceleration",
|
||||
"Auto Tune: Steer Ratio",
|
||||
"Auto Tune: Stiffness Factor",
|
||||
"Engagement %: Lateral",
|
||||
"Engagement %: Longitudinal",
|
||||
"Lateral Control: Steering Angle",
|
||||
"Lateral Control: Torque % Used",
|
||||
"Longitudinal Control: Actuator Acceleration Output",
|
||||
"Longitudinal MPC: Danger Factor",
|
||||
"Longitudinal MPC Jerk: Acceleration",
|
||||
"Longitudinal MPC Jerk: Danger Zone",
|
||||
"Longitudinal MPC Jerk: Speed Control",
|
||||
]
|
||||
|
||||
# Exclusive groups for sidebar metrics (only one in each group can be active)
|
||||
_SIDEBAR_CPU_GPU = {"ShowCPU", "ShowGPU"}
|
||||
_SIDEBAR_STORAGE = {"ShowMemoryUsage", "ShowStorageLeft", "ShowStorageUsed"}
|
||||
|
||||
|
||||
class StarPilotDeveloperLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._sub_panels = {
|
||||
"metrics": StarPilotDevMetricsLayout(),
|
||||
"sidebar": StarPilotDevSidebarLayout(),
|
||||
"widgets": StarPilotDevWidgetsLayout(),
|
||||
}
|
||||
self.CATEGORIES = [
|
||||
{"title": tr_noop("Developer Metrics"), "panel": "metrics", "icon": "toggle_icons/icon_display.png", "color": "#364DEF"},
|
||||
{"title": tr_noop("Developer Sidebar"), "panel": "sidebar", "icon": "toggle_icons/icon_device.png", "color": "#364DEF"},
|
||||
{"title": tr_noop("Developer Widgets"), "panel": "widgets", "icon": "toggle_icons/icon_road.png", "color": "#364DEF"},
|
||||
]
|
||||
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 StarPilotDevMetricsLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.CATEGORIES = [
|
||||
{
|
||||
"title": tr_noop("Adjacent Lane Metrics"),
|
||||
"type": "toggle",
|
||||
"key": "AdjacentPathMetrics",
|
||||
"get_state": lambda: self._params.get_bool("AdjacentPathMetrics"),
|
||||
"set_state": lambda s: self._params.put_bool("AdjacentPathMetrics", s),
|
||||
"icon": "toggle_icons/icon_road.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Border Metrics"),
|
||||
"type": "toggle",
|
||||
"key": "BorderMetrics",
|
||||
"get_state": lambda: self._params.get_bool("BorderMetrics"),
|
||||
"set_state": lambda s: self._params.put_bool("BorderMetrics", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Blind Spot"),
|
||||
"type": "toggle",
|
||||
"key": "BlindSpotMetrics",
|
||||
"parent": "BorderMetrics",
|
||||
"get_state": lambda: self._params.get_bool("BlindSpotMetrics"),
|
||||
"set_state": lambda s: self._params.put_bool("BlindSpotMetrics", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Steering Torque"),
|
||||
"type": "toggle",
|
||||
"key": "ShowSteering",
|
||||
"parent": "BorderMetrics",
|
||||
"get_state": lambda: self._params.get_bool("ShowSteering"),
|
||||
"set_state": lambda s: self._params.put_bool("ShowSteering", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Turn Signal"),
|
||||
"type": "toggle",
|
||||
"key": "SignalMetrics",
|
||||
"parent": "BorderMetrics",
|
||||
"get_state": lambda: self._params.get_bool("SignalMetrics"),
|
||||
"set_state": lambda s: self._params.put_bool("SignalMetrics", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("FPS Display"),
|
||||
"type": "toggle",
|
||||
"key": "FPSCounter",
|
||||
"get_state": lambda: self._params.get_bool("FPSCounter"),
|
||||
"set_state": lambda s: self._params.put_bool("FPSCounter", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Lead Info"),
|
||||
"type": "toggle",
|
||||
"key": "LeadInfo",
|
||||
"get_state": lambda: self._params.get_bool("LeadInfo"),
|
||||
"set_state": lambda s: self._params.put_bool("LeadInfo", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Numerical Temp"),
|
||||
"type": "toggle",
|
||||
"key": "NumericalTemp",
|
||||
"get_state": lambda: self._params.get_bool("NumericalTemp"),
|
||||
"set_state": lambda s: self._params.put_bool("NumericalTemp", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Fahrenheit"),
|
||||
"type": "toggle",
|
||||
"key": "Fahrenheit",
|
||||
"parent": "NumericalTemp",
|
||||
"get_state": lambda: self._params.get_bool("Fahrenheit"),
|
||||
"set_state": lambda s: self._params.put_bool("Fahrenheit", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Sidebar Metrics"),
|
||||
"type": "toggle",
|
||||
"key": "SidebarMetrics",
|
||||
"get_state": lambda: self._params.get_bool("SidebarMetrics"),
|
||||
"set_state": lambda s: self._params.put_bool("SidebarMetrics", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("CPU"),
|
||||
"type": "toggle",
|
||||
"key": "ShowCPU",
|
||||
"parent": "SidebarMetrics",
|
||||
"get_state": lambda: self._params.get_bool("ShowCPU"),
|
||||
"set_state": lambda s: self._set_sidebar_exclusive("ShowCPU", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("GPU"),
|
||||
"type": "toggle",
|
||||
"key": "ShowGPU",
|
||||
"parent": "SidebarMetrics",
|
||||
"get_state": lambda: self._params.get_bool("ShowGPU"),
|
||||
"set_state": lambda s: self._set_sidebar_exclusive("ShowGPU", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("IP Address"),
|
||||
"type": "toggle",
|
||||
"key": "ShowIP",
|
||||
"parent": "SidebarMetrics",
|
||||
"get_state": lambda: self._params.get_bool("ShowIP"),
|
||||
"set_state": lambda s: self._params.put_bool("ShowIP", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("RAM Usage"),
|
||||
"type": "toggle",
|
||||
"key": "ShowMemoryUsage",
|
||||
"parent": "SidebarMetrics",
|
||||
"get_state": lambda: self._params.get_bool("ShowMemoryUsage"),
|
||||
"set_state": lambda s: self._set_sidebar_exclusive("ShowMemoryUsage", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("SSD Left"),
|
||||
"type": "toggle",
|
||||
"key": "ShowStorageLeft",
|
||||
"parent": "SidebarMetrics",
|
||||
"get_state": lambda: self._params.get_bool("ShowStorageLeft"),
|
||||
"set_state": lambda s: self._set_sidebar_exclusive("ShowStorageLeft", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("SSD Used"),
|
||||
"type": "toggle",
|
||||
"key": "ShowStorageUsed",
|
||||
"parent": "SidebarMetrics",
|
||||
"get_state": lambda: self._params.get_bool("ShowStorageUsed"),
|
||||
"set_state": lambda s: self._set_sidebar_exclusive("ShowStorageUsed", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Use SI Units"),
|
||||
"type": "toggle",
|
||||
"key": "UseSI",
|
||||
"get_state": lambda: self._params.get_bool("UseSI"),
|
||||
"set_state": lambda s: self._params.put_bool("UseSI", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
]
|
||||
self._rebuild_grid()
|
||||
|
||||
def _set_sidebar_exclusive(self, key: str, state: bool):
|
||||
self._params.put_bool(key, state)
|
||||
if state:
|
||||
if key in _SIDEBAR_CPU_GPU:
|
||||
for k in _SIDEBAR_CPU_GPU:
|
||||
if k != key:
|
||||
self._params.put_bool(k, False)
|
||||
elif key in _SIDEBAR_STORAGE:
|
||||
for k in _SIDEBAR_STORAGE:
|
||||
if k != key:
|
||||
self._params.put_bool(k, False)
|
||||
self._rebuild_grid()
|
||||
|
||||
def _rebuild_grid(self):
|
||||
if not self.CATEGORIES:
|
||||
return
|
||||
if self._tile_grid is None:
|
||||
self._tile_grid = TileGrid(columns=None, padding=20)
|
||||
self._tile_grid.clear()
|
||||
|
||||
border_on = self._params.get_bool("BorderMetrics")
|
||||
temp_on = self._params.get_bool("NumericalTemp")
|
||||
sidebar_on = self._params.get_bool("SidebarMetrics")
|
||||
|
||||
for cat in self.CATEGORIES:
|
||||
key = cat.get("key")
|
||||
parent = cat.get("parent")
|
||||
|
||||
# Conditional visibility
|
||||
if parent == "BorderMetrics" and not border_on:
|
||||
if key == "BlindSpotMetrics" and not starpilot_state.car_state.hasBSM:
|
||||
continue
|
||||
if not border_on:
|
||||
continue
|
||||
elif parent == "NumericalTemp" and not temp_on:
|
||||
continue
|
||||
elif parent == "SidebarMetrics" and not sidebar_on:
|
||||
continue
|
||||
|
||||
# BlindSpotMetrics extra gate
|
||||
if key == "BlindSpotMetrics" and not starpilot_state.car_state.hasBSM:
|
||||
continue
|
||||
|
||||
tile = ToggleTile(
|
||||
title=tr(cat["title"]),
|
||||
get_state=cat["get_state"],
|
||||
set_state=cat["set_state"],
|
||||
icon_path=cat.get("icon"),
|
||||
bg_color=cat.get("color"),
|
||||
)
|
||||
self._tile_grid.add_tile(tile)
|
||||
|
||||
|
||||
class StarPilotDevSidebarLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.CATEGORIES = [
|
||||
{
|
||||
"title": tr_noop("Metric #1"),
|
||||
"type": "value",
|
||||
"key": "DeveloperSidebarMetric1",
|
||||
"get_value": lambda: tr(self._get_metric_name("DeveloperSidebarMetric1")),
|
||||
"on_click": lambda: self._show_metric_selector("DeveloperSidebarMetric1"),
|
||||
"icon": "toggle_icons/icon_device.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Metric #2"),
|
||||
"type": "value",
|
||||
"key": "DeveloperSidebarMetric2",
|
||||
"get_value": lambda: tr(self._get_metric_name("DeveloperSidebarMetric2")),
|
||||
"on_click": lambda: self._show_metric_selector("DeveloperSidebarMetric2"),
|
||||
"icon": "toggle_icons/icon_device.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Metric #3"),
|
||||
"type": "value",
|
||||
"key": "DeveloperSidebarMetric3",
|
||||
"get_value": lambda: tr(self._get_metric_name("DeveloperSidebarMetric3")),
|
||||
"on_click": lambda: self._show_metric_selector("DeveloperSidebarMetric3"),
|
||||
"icon": "toggle_icons/icon_device.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Metric #4"),
|
||||
"type": "value",
|
||||
"key": "DeveloperSidebarMetric4",
|
||||
"get_value": lambda: tr(self._get_metric_name("DeveloperSidebarMetric4")),
|
||||
"on_click": lambda: self._show_metric_selector("DeveloperSidebarMetric4"),
|
||||
"icon": "toggle_icons/icon_device.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Metric #5"),
|
||||
"type": "value",
|
||||
"key": "DeveloperSidebarMetric5",
|
||||
"get_value": lambda: tr(self._get_metric_name("DeveloperSidebarMetric5")),
|
||||
"on_click": lambda: self._show_metric_selector("DeveloperSidebarMetric5"),
|
||||
"icon": "toggle_icons/icon_device.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Metric #6"),
|
||||
"type": "value",
|
||||
"key": "DeveloperSidebarMetric6",
|
||||
"get_value": lambda: tr(self._get_metric_name("DeveloperSidebarMetric6")),
|
||||
"on_click": lambda: self._show_metric_selector("DeveloperSidebarMetric6"),
|
||||
"icon": "toggle_icons/icon_device.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Metric #7"),
|
||||
"type": "value",
|
||||
"key": "DeveloperSidebarMetric7",
|
||||
"get_value": lambda: tr(self._get_metric_name("DeveloperSidebarMetric7")),
|
||||
"on_click": lambda: self._show_metric_selector("DeveloperSidebarMetric7"),
|
||||
"icon": "toggle_icons/icon_device.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
]
|
||||
self._rebuild_grid()
|
||||
|
||||
def _get_metric_name(self, key: str) -> str:
|
||||
idx = self._params.get_int(key)
|
||||
if 0 <= idx < len(SIDEBAR_METRIC_OPTIONS):
|
||||
return SIDEBAR_METRIC_OPTIONS[idx]
|
||||
return "None"
|
||||
|
||||
def _show_metric_selector(self, key: str):
|
||||
current_idx = self._params.get_int(key)
|
||||
current = SIDEBAR_METRIC_OPTIONS[current_idx] if 0 <= current_idx < len(SIDEBAR_METRIC_OPTIONS) else "None"
|
||||
|
||||
def on_close(res, val):
|
||||
if res == DialogResult.CONFIRM:
|
||||
try:
|
||||
selected_idx = SIDEBAR_METRIC_OPTIONS.index(val)
|
||||
self._params.put_int(key, selected_idx)
|
||||
self._rebuild_grid()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
gui_app.set_modal_overlay(SelectionDialog(tr("Select a Metric"), SIDEBAR_METRIC_OPTIONS, current, on_close=on_close))
|
||||
|
||||
|
||||
class StarPilotDevWidgetsLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.CATEGORIES = [
|
||||
{
|
||||
"title": tr_noop("Adjacent Leads Tracking"),
|
||||
"type": "toggle",
|
||||
"key": "AdjacentLeadsUI",
|
||||
"get_state": lambda: self._params.get_bool("AdjacentLeadsUI"),
|
||||
"set_state": lambda s: self._params.put_bool("AdjacentLeadsUI", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Model Stopping Point"),
|
||||
"type": "toggle",
|
||||
"key": "ShowStoppingPoint",
|
||||
"get_state": lambda: self._params.get_bool("ShowStoppingPoint"),
|
||||
"set_state": lambda s: self._params.put_bool("ShowStoppingPoint", s),
|
||||
"icon": "toggle_icons/icon_road.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Show Distance"),
|
||||
"type": "toggle",
|
||||
"key": "ShowStoppingPointMetrics",
|
||||
"parent": "ShowStoppingPoint",
|
||||
"get_state": lambda: self._params.get_bool("ShowStoppingPointMetrics"),
|
||||
"set_state": lambda s: self._params.put_bool("ShowStoppingPointMetrics", s),
|
||||
"icon": "toggle_icons/icon_road.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Radar Tracks"),
|
||||
"type": "toggle",
|
||||
"key": "RadarTracksUI",
|
||||
"get_state": lambda: self._params.get_bool("RadarTracksUI"),
|
||||
"set_state": lambda s: self._params.put_bool("RadarTracksUI", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#364DEF",
|
||||
},
|
||||
]
|
||||
self._rebuild_grid()
|
||||
|
||||
def _rebuild_grid(self):
|
||||
if not self.CATEGORIES:
|
||||
return
|
||||
if self._tile_grid is None:
|
||||
self._tile_grid = TileGrid(columns=None, padding=20)
|
||||
self._tile_grid.clear()
|
||||
|
||||
stopping_on = self._params.get_bool("ShowStoppingPoint")
|
||||
|
||||
for cat in self.CATEGORIES:
|
||||
key = cat.get("key")
|
||||
parent = cat.get("parent")
|
||||
|
||||
# Conditional visibility
|
||||
if key == "AdjacentLeadsUI" and not starpilot_state.car_state.hasRadar:
|
||||
continue
|
||||
elif key == "RadarTracksUI" and not starpilot_state.car_state.hasRadar:
|
||||
continue
|
||||
elif key == "ShowStoppingPoint" and not starpilot_state.car_state.hasOpenpilotLongitudinal:
|
||||
continue
|
||||
elif parent == "ShowStoppingPoint" and not stopping_on:
|
||||
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,25 +1,275 @@
|
||||
from __future__ import annotations
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.widgets.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
|
||||
|
||||
|
||||
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("Disable Onroad Uploads"),
|
||||
"type": "toggle",
|
||||
"param": "DisableOnroadUploads",
|
||||
"get_state": lambda: self._params.get_bool("DisableOnroadUploads"),
|
||||
"set_state": lambda s: self._params.put_bool("DisableOnroadUploads", s),
|
||||
"color": "#FA6800",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("High-Quality Recording"),
|
||||
"type": "toggle",
|
||||
"param": "HigherBitrate",
|
||||
"get_state": lambda: self._params.get_bool("HigherBitrate"),
|
||||
"set_state": lambda s: self._on_higher_bitrate_toggle(s),
|
||||
"color": "#FA6800",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Screen Recorder"),
|
||||
"type": "hub",
|
||||
"on_click": self._on_screen_recorder_clicked,
|
||||
"color": "#FA6800",
|
||||
},
|
||||
]
|
||||
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
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 _rebuild_grid(self):
|
||||
no_uploads = self._params.get_bool("NoUploads")
|
||||
disable_onroad = self._params.get_bool("DisableOnroadUploads")
|
||||
filtered = []
|
||||
for cat in self.CATEGORIES:
|
||||
param = cat.get("param")
|
||||
if param == "DisableOnroadUploads" and not no_uploads:
|
||||
continue
|
||||
if param == "HigherBitrate" and (not no_uploads or disable_onroad):
|
||||
continue
|
||||
filtered.append(cat)
|
||||
original = self.CATEGORIES
|
||||
self.CATEGORIES = filtered
|
||||
super()._rebuild_grid()
|
||||
self.CATEGORIES = original
|
||||
|
||||
def _on_higher_bitrate_toggle(self, state):
|
||||
self._params.put_bool("HigherBitrate", state)
|
||||
cache_path = Path("/cache/use_HD")
|
||||
if state:
|
||||
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
cache_path.touch()
|
||||
else:
|
||||
if cache_path.exists():
|
||||
cache_path.unlink()
|
||||
if ui_state.started:
|
||||
gui_app.set_modal_overlay(
|
||||
ConfirmDialog(
|
||||
tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None
|
||||
)
|
||||
)
|
||||
|
||||
def _get_shutdown_timer(self):
|
||||
v = self._params.get_int("DeviceShutdown")
|
||||
if v == 0:
|
||||
return tr("5 mins")
|
||||
if v <= 3:
|
||||
return f"{v * 15} mins"
|
||||
return f"{v - 3} " + (tr("hour") if v == 4 else tr("hours"))
|
||||
|
||||
def _show_shutdown_selector(self):
|
||||
def on_close(res, val):
|
||||
if res == DialogResult.CONFIRM:
|
||||
self._params.put_int("DeviceShutdown", int(val))
|
||||
self._rebuild_grid()
|
||||
|
||||
labels = {0: tr("5 mins")}
|
||||
for i in range(1, 4):
|
||||
labels[i] = f"{i * 15} mins"
|
||||
for i in range(4, 34):
|
||||
labels[i] = f"{i - 3} " + (tr("hour") if i == 4 else tr("hours"))
|
||||
|
||||
gui_app.set_modal_overlay(SliderDialog(tr("Device Shutdown"), 0, 33, 1, self._params.get_int("DeviceShutdown"), on_close, labels=labels, color="#FA6800"))
|
||||
|
||||
def _on_screen_recorder_clicked(self):
|
||||
is_recording = self._params_memory.get_bool("ScreenRecording")
|
||||
if is_recording:
|
||||
# Stop recording
|
||||
subprocess.run(["killall", "-INT", "screenrecord"], capture_output=True)
|
||||
self._params_memory.put_bool("ScreenRecording", False)
|
||||
else:
|
||||
# Start recording
|
||||
output = "/tmp/screen_recording.mp4"
|
||||
subprocess.Popen(["screenrecord", "--time-limit", "180", output], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
self._params_memory.put_bool("ScreenRecording", True)
|
||||
|
||||
|
||||
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._get_konik_state(),
|
||||
"set_state": lambda s: self._on_konik_toggle(s),
|
||||
"color": "#FA6800",
|
||||
},
|
||||
]
|
||||
self._rebuild_grid()
|
||||
|
||||
def _get_konik_state(self):
|
||||
if Path("/data/not_vetted").exists():
|
||||
return True
|
||||
return self._params.get_bool("UseKonikServer")
|
||||
|
||||
def _on_konik_toggle(self, state):
|
||||
self._params.put_bool("UseKonikServer", state)
|
||||
cache_path = Path("/cache/use_konik")
|
||||
if state:
|
||||
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
cache_path.touch()
|
||||
else:
|
||||
if cache_path.exists():
|
||||
cache_path.unlink()
|
||||
if ui_state.started:
|
||||
gui_app.set_modal_overlay(
|
||||
ConfirmDialog(
|
||||
tr("Reboot required. Reboot now?"), tr("Reboot"), tr("Cancel"), on_close=lambda res: HARDWARE.reboot() if res == DialogResult.CONFIRM else None
|
||||
)
|
||||
)
|
||||
|
||||
def _show_voltage_selector(self):
|
||||
def on_close(res, val):
|
||||
if res == DialogResult.CONFIRM:
|
||||
self._params.put_float("LowVoltageShutdown", float(val))
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(
|
||||
SliderDialog(tr("Low-Voltage Cutoff"), 11.8, 12.5, 0.1, self._params.get_float("LowVoltageShutdown"), on_close, unit="V", color="#FA6800")
|
||||
)
|
||||
|
||||
@@ -12,14 +12,13 @@ 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
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.metro import SliderDialog
|
||||
|
||||
|
||||
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 +34,35 @@ 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("Ratings"), "type": "hub", "icon": "toggle_icons/icon_system.png", "on_click": self._on_scores_clicked, "color": "#1BA1E2"},
|
||||
{
|
||||
"title": tr_noop("Recovery Power"),
|
||||
"type": "value",
|
||||
"icon": "toggle_icons/icon_longitudinal_tune.png",
|
||||
"on_click": self._on_recovery_power_clicked,
|
||||
"get_value": lambda: f"{self._params.get_float('RecoveryPower', default=1.0):.1f}",
|
||||
"visible": lambda: self._params.get_int("TuningLevel", default=1) >= 3,
|
||||
"color": "#1BA1E2",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Stop Distance"),
|
||||
"type": "value",
|
||||
"icon": "toggle_icons/icon_longitudinal_tune.png",
|
||||
"on_click": self._on_stop_distance_clicked,
|
||||
"get_value": lambda: f"{self._params.get_float('StopDistance', default=6.0):.1f}",
|
||||
"visible": lambda: self._params.get_int("TuningLevel", default=1) >= 3,
|
||||
"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 +72,13 @@ 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
|
||||
|
||||
has_thneed = (self._model_dir / f"{key}.thneed").exists()
|
||||
if has_thneed:
|
||||
return True
|
||||
|
||||
return has_policy_meta and has_policy_tg and has_vision_meta and has_vision_tg
|
||||
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')
|
||||
@@ -173,30 +104,27 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
|
||||
name = self._available_model_names[i].strip()
|
||||
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):
|
||||
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:
|
||||
@@ -204,9 +132,11 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
|
||||
return
|
||||
|
||||
if isinstance(options, list):
|
||||
|
||||
def _on_close_list(res, 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
|
||||
@@ -219,15 +149,14 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
|
||||
grouped[series] = []
|
||||
grouped[series].append(name)
|
||||
name_to_key[name] = key
|
||||
|
||||
|
||||
for series in grouped:
|
||||
grouped[series].sort()
|
||||
|
||||
sorted_series = sorted(grouped.keys())
|
||||
if "StarPilot" in sorted_series:
|
||||
sorted_series.remove("StarPilot")
|
||||
sorted_series.insert(0, "StarPilot")
|
||||
|
||||
|
||||
final_grouped = {s: grouped[s] for s in sorted_series}
|
||||
|
||||
def _on_close_grouped(res, val):
|
||||
@@ -247,15 +176,15 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
|
||||
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,
|
||||
model_released_dates=self._model_released_dates,
|
||||
model_file_to_name=self._model_file_to_name,
|
||||
user_favorites=user_favs,
|
||||
community_favorites=comm_favs,
|
||||
on_favorite_toggled=_on_favorite_toggled
|
||||
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,
|
||||
community_favorites=comm_favs,
|
||||
on_favorite_toggled=_on_favorite_toggled,
|
||||
)
|
||||
gui_app.set_modal_overlay(dialog)
|
||||
|
||||
@@ -268,20 +197,17 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
|
||||
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(
|
||||
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(reboot_dialog)
|
||||
|
||||
self._show_selection_dialog(tr("Select Driving Model"), installed_models, self._current_model_name, _on_confirm)
|
||||
|
||||
@@ -294,136 +220,109 @@ 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}
|
||||
|
||||
def _on_confirm(model_key):
|
||||
def _execute_delete(confirm_res):
|
||||
if confirm_res == DialogResult.CONFIRM:
|
||||
for file in self._model_dir.iterdir():
|
||||
if file.name.startswith(model_key):
|
||||
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)
|
||||
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(mk):
|
||||
def _execute_delete(res):
|
||||
if res == DialogResult.CONFIRM:
|
||||
for file in self._model_dir.iterdir():
|
||||
if file.name.startswith(mk):
|
||||
file.unlink()
|
||||
self._update_model_metadata()
|
||||
|
||||
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"):
|
||||
blacklistable = {k: v for k, v in self._model_file_to_name.items() if k not in blacklisted}
|
||||
self._show_selection_dialog(tr("Add to Blacklist"), blacklistable, "", lambda k: self._params.put("BlacklistedModels", ",".join(blacklisted + [k])))
|
||||
elif val == tr("REMOVE"):
|
||||
options = {k: self._model_file_to_name.get(k, k) for k in blacklisted}
|
||||
def _remove(k):
|
||||
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")
|
||||
if res == DialogResult.CONFIRM:
|
||||
if val == tr("ADD"):
|
||||
blacklistable = {k: v for k, v in self._model_file_to_name.items() if k not in blacklisted}
|
||||
self._show_selection_dialog(tr("Add to Blacklist"), blacklistable, "", lambda k: self._params.put("BlacklistedModels", ",".join(blacklisted + [k])))
|
||||
elif val == tr("REMOVE"):
|
||||
options = {k: self._model_file_to_name.get(k, k) for k in blacklisted}
|
||||
|
||||
dialog = SelectionDialog(
|
||||
tr("Manage Blacklist"),
|
||||
[tr("ADD"), tr("REMOVE"), tr("RESET ALL")],
|
||||
on_close=_on_action_selected
|
||||
)
|
||||
gui_app.set_modal_overlay(dialog)
|
||||
def _remove(k):
|
||||
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")
|
||||
|
||||
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)
|
||||
scores = json.loads(scores_raw)
|
||||
lines = [f"{k}: {v.get('Score', 0)}% ({v.get('Drives', 0)} drives)" for k, v in scores.items()]
|
||||
gui_app.set_modal_overlay(ConfirmDialog("\n".join(lines), tr("Close"), rich=True))
|
||||
except:
|
||||
pass
|
||||
pass
|
||||
|
||||
def _on_model_randomizer_toggled(self, state: bool):
|
||||
self._params.put_bool("ModelRandomizer", state)
|
||||
if state:
|
||||
not_installed = [k for k in self._model_file_to_name if not self._is_model_installed(k)]
|
||||
if not_installed:
|
||||
def _on_download_confirm(res):
|
||||
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)
|
||||
not_installed = [k for k in self._model_file_to_name if not self._is_model_installed(k)]
|
||||
if not_installed:
|
||||
|
||||
def _on_download_confirm(res):
|
||||
if res == DialogResult.CONFIRM:
|
||||
self._params_memory.put_bool("DownloadAllModels", True)
|
||||
self._params_memory.put("ModelDownloadProgress", "Downloading...")
|
||||
|
||||
gui_app.set_modal_overlay(ConfirmDialog(tr("Download all models for Randomizer?"), tr("Download All"), on_close=_on_download_confirm))
|
||||
|
||||
def _on_recovery_power_clicked(self):
|
||||
def on_close(res, val):
|
||||
if res == DialogResult.CONFIRM:
|
||||
self._params.put_float("RecoveryPower", val)
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(
|
||||
SliderDialog(tr("Recovery Power"), 0.5, 2.0, 0.1, self._params.get_float("RecoveryPower", default=1.0), on_close, color="#1BA1E2")
|
||||
)
|
||||
|
||||
def _on_stop_distance_clicked(self):
|
||||
def on_close(res, val):
|
||||
if res == DialogResult.CONFIRM:
|
||||
self._params.put_float("StopDistance", val)
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(SliderDialog(tr("Stop Distance"), 4.0, 10.0, 0.5, self._params.get_float("StopDistance", default=6.0), on_close, color="#1BA1E2"))
|
||||
|
||||
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
|
||||
|
||||
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!")
|
||||
def _download_task():
|
||||
try:
|
||||
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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -22,46 +21,56 @@ from openpilot.selfdrive.ui.layouts.settings.starpilot.visuals import StarPilotV
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.themes import StarPilotThemesLayout
|
||||
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.developer import StarPilotDeveloperLayout
|
||||
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.metro import TileGrid, HubTile, RadioTileGroup
|
||||
|
||||
STARPILOT_ICONS_DIR = "toggle_icons"
|
||||
|
||||
|
||||
class StarPilotLayout(Widget):
|
||||
CATEGORIES = [
|
||||
{
|
||||
"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>",
|
||||
"buttons": [("APPEARANCE", "VISUALS", 0), ("THEME", "THEMES", 0)],
|
||||
"desc": "Customize the look of the driving screen and interface, including themes!",
|
||||
"buttons": [("APPEARANCE", "VISUALS", 0), ("THEME", "THEMES", 0), ("DEVELOPER", "DEVELOPER", 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,18 +79,12 @@ 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._panel_stack: list[tuple[StarPilotPanelType, str]] = []
|
||||
self._sub_panel_callbacks: dict[str, Callable] = {}
|
||||
|
||||
self._panels = {
|
||||
StarPilotPanelType.MAIN: StarPilotPanelInfo("", None),
|
||||
@@ -98,78 +101,18 @@ class StarPilotLayout(Widget):
|
||||
StarPilotPanelType.THEMES: StarPilotPanelInfo(tr_noop("Themes"), StarPilotThemesLayout()),
|
||||
StarPilotPanelType.VEHICLE: StarPilotPanelInfo(tr_noop("Vehicle Settings"), StarPilotVehicleSettingsLayout()),
|
||||
StarPilotPanelType.WHEEL: StarPilotPanelInfo(tr_noop("Wheel Controls"), StarPilotWheelLayout()),
|
||||
StarPilotPanelType.DEVELOPER: StarPilotPanelInfo(tr_noop("Developer"), StarPilotDeveloperLayout()),
|
||||
}
|
||||
|
||||
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()
|
||||
self._setup_developer_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 +123,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 +178,24 @@ 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
|
||||
elif self._current_panel == StarPilotPanelType.DEVELOPER:
|
||||
developer = self._panels[StarPilotPanelType.DEVELOPER].instance
|
||||
if developer:
|
||||
current_sub = self._get_current_sub_panel()
|
||||
if hasattr(developer, '_navigate_to'):
|
||||
developer._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,36 +217,23 @@ 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")]
|
||||
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)
|
||||
|
||||
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_developer_sub_panels(self):
|
||||
developer = self._panels[StarPilotPanelType.DEVELOPER].instance
|
||||
if developer and hasattr(developer, 'set_navigate_callback'):
|
||||
developer.set_navigate_callback(self._push_sub_panel)
|
||||
|
||||
def _rebuild_grid(self):
|
||||
self._main_grid.clear()
|
||||
|
||||
panel_type_map = {
|
||||
"SOUNDS": StarPilotPanelType.SOUNDS,
|
||||
@@ -268,28 +249,57 @@ class StarPilotLayout(Widget):
|
||||
"THEMES": StarPilotPanelType.THEMES,
|
||||
"VEHICLE": StarPilotPanelType.VEHICLE,
|
||||
"WHEEL": StarPilotPanelType.WHEEL,
|
||||
"DEVELOPER": StarPilotPanelType.DEVELOPER,
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
if filtered_buttons:
|
||||
full_icon_path = f"{STARPILOT_ICONS_DIR}/{cat['icon']}"
|
||||
item = category_buttons_item(
|
||||
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)
|
||||
|
||||
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"]
|
||||
|
||||
self._main_scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
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)
|
||||
|
||||
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 +308,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 +323,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):
|
||||
|
||||
@@ -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,412 @@
|
||||
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 _pressed_color(self, base: rl.Color | None = None) -> rl.Color:
|
||||
"""Return the press-darkened version of base (defaults to self.bg_color)."""
|
||||
c = base if base is not None else self.bg_color
|
||||
if not self._is_pressed:
|
||||
return c
|
||||
return rl.Color(max(0, c.r - 20), max(0, c.g - 20), max(0, c.b - 20), c.a)
|
||||
|
||||
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)
|
||||
color = self._pressed_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
|
||||
color = self._pressed_color(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)
|
||||
color = self._pressed_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,134 @@
|
||||
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 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.system.ui.widgets.input_dialog import InputDialog
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
|
||||
|
||||
|
||||
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": "value",
|
||||
"get_value": lambda: self._params.get("HomeAddress", encoding='utf-8') or tr("Not set"),
|
||||
"on_click": self._on_home,
|
||||
"icon": "toggle_icons/icon_navigate.png",
|
||||
"color": "#8CBF26",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Work Address"),
|
||||
"type": "value",
|
||||
"get_value": lambda: self._params.get("WorkAddress", encoding='utf-8') or tr("Not set"),
|
||||
"on_click": self._on_work,
|
||||
"icon": "toggle_icons/icon_navigate.png",
|
||||
"color": "#8CBF26",
|
||||
},
|
||||
]
|
||||
for name, panel in self._sub_panels.items():
|
||||
if hasattr(panel, 'set_navigate_callback'):
|
||||
panel.set_navigate_callback(self._navigate_to)
|
||||
if hasattr(panel, 'set_back_callback'):
|
||||
panel.set_back_callback(self._go_back)
|
||||
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):
|
||||
def on_close(res, text):
|
||||
if res == DialogResult.CONFIRM and text:
|
||||
self._params.put("SearchAddress", text)
|
||||
|
||||
gui_app.set_modal_overlay(InputDialog(tr("Search Destination"), "", on_close=on_close))
|
||||
|
||||
def _on_home(self):
|
||||
current = self._params.get("HomeAddress", encoding='utf-8') or ""
|
||||
|
||||
def on_close(res, text):
|
||||
if res == DialogResult.CONFIRM:
|
||||
self._params.put("HomeAddress", text)
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(InputDialog(tr("Home Address"), current, on_close=on_close))
|
||||
|
||||
def _on_work(self):
|
||||
current = self._params.get("WorkAddress", encoding='utf-8') or ""
|
||||
|
||||
def on_close(res, text):
|
||||
if res == DialogResult.CONFIRM:
|
||||
self._params.put("WorkAddress", text)
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(InputDialog(tr("Work Address"), current, on_close=on_close))
|
||||
|
||||
|
||||
class StarPilotMapboxLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.CATEGORIES = [
|
||||
{
|
||||
"title": tr_noop("Public Mapbox Key"),
|
||||
"type": "value",
|
||||
"get_value": self._get_key_display,
|
||||
"on_click": lambda: self._on_key("MapboxPublicKey", "pk."),
|
||||
"color": "#8CBF26",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Secret Mapbox Key"),
|
||||
"type": "value",
|
||||
"get_value": self._get_secret_display,
|
||||
"on_click": lambda: self._on_key("MapboxSecretKey", "sk."),
|
||||
"color": "#8CBF26",
|
||||
},
|
||||
]
|
||||
self._rebuild_grid()
|
||||
|
||||
def _get_key_display(self):
|
||||
v = self._params.get("MapboxPublicKey", encoding='utf-8') or ""
|
||||
return f"{v[:8]}..." if v else tr("Not set")
|
||||
|
||||
def _get_secret_display(self):
|
||||
v = self._params.get("MapboxSecretKey", encoding='utf-8') or ""
|
||||
return "********" if v else tr("Not set")
|
||||
|
||||
def _on_key(self, key, prefix):
|
||||
current = self._params.get(key, encoding='utf-8') or ""
|
||||
if current:
|
||||
|
||||
def on_remove(res):
|
||||
if res == DialogResult.CONFIRM:
|
||||
self._params.remove(key)
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(ConfirmDialog(tr(f"Remove your {key.replace('Mapbox', '')} key?"), tr("Remove"), on_close=on_remove))
|
||||
else:
|
||||
|
||||
def on_close(res, text):
|
||||
if res == DialogResult.CONFIRM and text:
|
||||
if not text.startswith(prefix):
|
||||
text = prefix + text
|
||||
self._params.put(key, text)
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(InputDialog(tr(f"Enter {key.replace('Mapbox', 'Mapbox ')}"), "", on_close=on_close))
|
||||
|
||||
@@ -6,8 +6,10 @@ 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):
|
||||
MAIN = 0
|
||||
SOUNDS = 1
|
||||
@@ -23,23 +25,30 @@ class StarPilotPanelType(IntEnum):
|
||||
THEMES = 11
|
||||
VEHICLE = 12
|
||||
WHEEL = 13
|
||||
DEVELOPER = 14
|
||||
|
||||
|
||||
@dataclass
|
||||
class StarPilotPanelInfo:
|
||||
name: str
|
||||
instance: Widget
|
||||
|
||||
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.metro import TileGrid, HubTile, ToggleTile, ValueTile
|
||||
|
||||
|
||||
class StarPilotPanel(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
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 +56,42 @@ 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:
|
||||
visible_fn = cat.get("visible")
|
||||
if visible_fn is not None and not visible_fn():
|
||||
continue
|
||||
|
||||
tile_type = cat.get("type", "hub")
|
||||
if tile_type == "hub":
|
||||
on_click = cat.get("on_click")
|
||||
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 +106,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:
|
||||
|
||||
@@ -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,160 @@
|
||||
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__()
|
||||
|
||||
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._sub_panels = {
|
||||
"personalize": StarPilotPersonalizeLayout(),
|
||||
}
|
||||
|
||||
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",
|
||||
},
|
||||
{"title": tr_noop("Startup Alert"), "type": "hub", "on_click": self._on_startup_alert, "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()
|
||||
|
||||
def _on_startup_alert(self):
|
||||
options = ["Stock", "FrogPilot", "Clear"]
|
||||
current_top = self._params.get("StartupMessageTop", encoding='utf-8') or ""
|
||||
if current_top == "Be ready to take over at any time":
|
||||
current = "Stock"
|
||||
elif current_top == "Hop in and buckle up!":
|
||||
current = "FrogPilot"
|
||||
else:
|
||||
current = "Clear"
|
||||
|
||||
def on_select(res, val):
|
||||
if res == DialogResult.CONFIRM:
|
||||
if val == "Stock":
|
||||
self._params.put("StartupMessageTop", "Be ready to take over at any time")
|
||||
self._params.put("StartupMessageBottom", "Always keep hands on wheel and eyes on road")
|
||||
elif val == "FrogPilot":
|
||||
self._params.put("StartupMessageTop", "Hop in and buckle up!")
|
||||
self._params.put("StartupMessageBottom", "Human-tested, frog-approved")
|
||||
else:
|
||||
self._params.remove("StartupMessageTop")
|
||||
self._params.remove("StartupMessageBottom")
|
||||
|
||||
gui_app.set_modal_overlay(SelectionDialog(tr("Startup Alert"), options, current, on_close=on_select))
|
||||
|
||||
|
||||
class StarPilotPersonalizeLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.CATEGORIES = [
|
||||
{
|
||||
"title": tr_noop("Boot Logo"),
|
||||
"type": "value",
|
||||
"get_value": lambda: self._params.get("BootLogo", encoding='utf-8') or "Stock",
|
||||
"on_click": lambda: self._show_theme_selector("BootLogo"),
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Color Scheme"),
|
||||
"type": "value",
|
||||
"get_value": lambda: self._params.get("ColorScheme", encoding='utf-8') or "Stock",
|
||||
"on_click": lambda: self._show_theme_selector("ColorScheme"),
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Distance Icons"),
|
||||
"type": "value",
|
||||
"get_value": lambda: self._params.get("DistanceIconPack", encoding='utf-8') or "Stock",
|
||||
"on_click": lambda: self._show_theme_selector("DistanceIconPack"),
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Icon Pack"),
|
||||
"type": "value",
|
||||
"get_value": lambda: self._params.get("IconPack", encoding='utf-8') or "Stock",
|
||||
"on_click": lambda: self._show_theme_selector("IconPack"),
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Turn Signals"),
|
||||
"type": "value",
|
||||
"get_value": lambda: self._params.get("SignalAnimation", encoding='utf-8') or "Stock",
|
||||
"on_click": lambda: self._show_theme_selector("SignalAnimation"),
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Sound Pack"),
|
||||
"type": "value",
|
||||
"get_value": lambda: self._params.get("SoundPack", encoding='utf-8') or "Stock",
|
||||
"on_click": lambda: self._show_theme_selector("SoundPack"),
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Steering Wheel"),
|
||||
"type": "value",
|
||||
"get_value": lambda: self._params.get("WheelIcon", encoding='utf-8') or "Stock",
|
||||
"on_click": lambda: self._show_theme_selector("WheelIcon"),
|
||||
"color": "#A200FF",
|
||||
},
|
||||
]
|
||||
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,163 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.widgets.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.system.ui.widgets.input_dialog import InputDialog
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
|
||||
|
||||
EXCLUDED_KEYS = {
|
||||
"AvailableModels",
|
||||
"AvailableModelNames",
|
||||
"FrogPilotStats",
|
||||
"GithubSshKeys",
|
||||
"GithubUsername",
|
||||
"MapBoxRequests",
|
||||
"ModelDrivesAndScores",
|
||||
"OverpassRequests",
|
||||
"SpeedLimits",
|
||||
"SpeedLimitsFiltered",
|
||||
"UpdaterAvailableBranches",
|
||||
}
|
||||
|
||||
REPORT_CATEGORIES = [
|
||||
"Acceleration feels harsh or jerky",
|
||||
"An alert was unclear and I'm not sure what it meant",
|
||||
"Braking is too sudden or uncomfortable",
|
||||
"I'm not sure if this is normal or a bug:",
|
||||
"My steering wheel buttons aren't working",
|
||||
"openpilot disengages when I don't expect it",
|
||||
"openpilot feels sluggish or slow to respond",
|
||||
"Something else (please describe)",
|
||||
]
|
||||
|
||||
|
||||
class StarPilotUtilitiesLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
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": "value",
|
||||
"get_value": lambda: tr("Paired") if self._params.get_bool("PondPaired") else tr("Not paired"),
|
||||
"on_click": self._on_pond_clicked,
|
||||
"color": "#FA6800",
|
||||
},
|
||||
{"title": tr_noop("Report Issue"), "type": "hub", "on_click": self._on_report_issue, "color": "#FA6800"},
|
||||
{"title": tr_noop("Reset to Defaults"), "type": "hub", "on_click": self._on_reset_defaults, "color": "#FA6800"},
|
||||
{"title": tr_noop("Reset to Stock"), "type": "hub", "on_click": self._on_reset_stock, "color": "#FA6800"},
|
||||
]
|
||||
self._rebuild_grid()
|
||||
|
||||
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):
|
||||
paired = self._params.get_bool("PondPaired")
|
||||
if paired:
|
||||
|
||||
def on_unpair(res):
|
||||
if res == DialogResult.CONFIRM:
|
||||
self._params.put_bool("PondPaired", False)
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Unpaired from The Pond.")))
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(ConfirmDialog(tr("Unpair from The Pond?"), tr("Unpair"), on_close=on_unpair))
|
||||
else:
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Visit frogpilot.com/the_pond to pair your device.")))
|
||||
|
||||
def _on_report_issue(self):
|
||||
def on_category(res, val):
|
||||
if res != DialogResult.CONFIRM:
|
||||
return
|
||||
discord_user = self._params.get("DiscordUsername", encoding='utf-8') or ""
|
||||
|
||||
def on_discord(res2, username):
|
||||
if res2 == DialogResult.CONFIRM and username:
|
||||
self._params.put("DiscordUsername", username)
|
||||
report = json.dumps({"DiscordUser": username, "Issue": val})
|
||||
self._params_memory.put("IssueReported", report)
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Issue reported. Thank you!")))
|
||||
|
||||
gui_app.set_modal_overlay(InputDialog(tr("Discord Username"), discord_user or "", on_close=on_discord))
|
||||
|
||||
gui_app.set_modal_overlay(SelectionDialog(tr("Select Issue"), REPORT_CATEGORIES, on_close=on_category))
|
||||
|
||||
def _on_reset_defaults(self):
|
||||
def _do_reset(res):
|
||||
if res == DialogResult.CONFIRM:
|
||||
all_keys = self._params.all_keys()
|
||||
for k in all_keys:
|
||||
if k in EXCLUDED_KEYS:
|
||||
continue
|
||||
default = self._params.get_default_value(k)
|
||||
if default is not None:
|
||||
self._params.put(k, default)
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Toggles reset to defaults.")))
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(ConfirmDialog(tr("Reset all toggles to defaults?"), tr("Reset"), on_close=_do_reset))
|
||||
|
||||
def _on_reset_stock(self):
|
||||
def _do_reset(res):
|
||||
if res == DialogResult.CONFIRM:
|
||||
all_keys = self._params.all_keys()
|
||||
for k in all_keys:
|
||||
if k in EXCLUDED_KEYS:
|
||||
continue
|
||||
stock = self._params.get_stock_value(k)
|
||||
if stock is not None:
|
||||
self._params.put(k, stock)
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Toggles reset to stock openpilot.")))
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(ConfirmDialog(tr("Reset all toggles to stock openpilot?"), tr("Reset"), on_close=_do_reset))
|
||||
|
||||
@@ -1,20 +1,516 @@
|
||||
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, TileGrid, HubTile, ToggleTile, ValueTile
|
||||
from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state
|
||||
|
||||
MAKE_TO_FOLDER = {
|
||||
"acura": "honda",
|
||||
"audi": "volkswagen",
|
||||
"buick": "gm",
|
||||
"cadillac": "gm",
|
||||
"chevrolet": "gm",
|
||||
"chrysler": "chrysler",
|
||||
"cupra": "volkswagen",
|
||||
"dodge": "chrysler",
|
||||
"ford": "ford",
|
||||
"genesis": "hyundai",
|
||||
"gmc": "gm",
|
||||
"holden": "gm",
|
||||
"honda": "honda",
|
||||
"hyundai": "hyundai",
|
||||
"jeep": "chrysler",
|
||||
"kia": "hyundai",
|
||||
"lexus": "toyota",
|
||||
"lincoln": "ford",
|
||||
"man": "volkswagen",
|
||||
"mazda": "mazda",
|
||||
"nissan": "nissan",
|
||||
"peugeot": "psa",
|
||||
"ram": "chrysler",
|
||||
"rivian": "rivian",
|
||||
"seat": "volkswagen",
|
||||
"škoda": "volkswagen",
|
||||
"subaru": "subaru",
|
||||
"tesla": "tesla",
|
||||
"toyota": "toyota",
|
||||
"volkswagen": "volkswagen",
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _lock_doors_timer_labels():
|
||||
labels: dict[float, str] = {0.0: tr("Never")}
|
||||
for i in range(5, 305, 5):
|
||||
labels[float(i)] = f"{i}s"
|
||||
return labels
|
||||
|
||||
|
||||
class StarPilotVehicleSettingsLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._sub_panels = {
|
||||
"gm": StarPilotGMVehicleLayout(),
|
||||
"hkg": StarPilotHKGVehicleLayout(),
|
||||
"subaru": StarPilotSubaruVehicleLayout(),
|
||||
"toyota": StarPilotToyotaVehicleLayout(),
|
||||
"info": StarPilotVehicleInfoLayout(),
|
||||
}
|
||||
|
||||
items = [
|
||||
button_item(
|
||||
tr_noop("Vehicle Settings"),
|
||||
lambda: tr("MANAGE"),
|
||||
tr_noop("<b>Configure car-specific options</b> like model name and features."),
|
||||
),
|
||||
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", "key": "gm"},
|
||||
{"title": tr_noop("HKG Settings"), "panel": "hkg", "icon": "toggle_icons/icon_vehicle.png", "color": "#FFC40D", "key": "hkg"},
|
||||
{"title": tr_noop("Subaru Settings"), "panel": "subaru", "icon": "toggle_icons/icon_vehicle.png", "color": "#FFC40D", "key": "subaru"},
|
||||
{"title": tr_noop("Toyota Settings"), "panel": "toyota", "icon": "toggle_icons/icon_vehicle.png", "color": "#FFC40D", "key": "toyota"},
|
||||
{"title": tr_noop("Vehicle Info"), "panel": "info", "icon": "toggle_icons/icon_vehicle.png", "color": "#FFC40D"},
|
||||
]
|
||||
|
||||
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()
|
||||
|
||||
def _rebuild_grid(self):
|
||||
if not self.CATEGORIES:
|
||||
return
|
||||
if self._tile_grid is None:
|
||||
self._tile_grid = TileGrid(columns=None, padding=20)
|
||||
self._tile_grid.clear()
|
||||
|
||||
cs = starpilot_state.car_state
|
||||
for cat in self.CATEGORIES:
|
||||
key = cat.get("key")
|
||||
visible = True
|
||||
|
||||
if key == "gm":
|
||||
visible = cs.isGM
|
||||
elif key == "hkg":
|
||||
visible = cs.isHKG
|
||||
elif key == "subaru":
|
||||
visible = cs.isSubaru
|
||||
elif key == "toyota":
|
||||
visible = cs.isToyota
|
||||
|
||||
if not visible:
|
||||
continue
|
||||
|
||||
tile_type = cat.get("type", "hub")
|
||||
if tile_type == "hub":
|
||||
on_click = cat.get("on_click")
|
||||
if on_click is None:
|
||||
on_click = lambda c=cat: self._navigate_to(c["panel"])
|
||||
tile = HubTile(
|
||||
title=tr(cat["title"]),
|
||||
desc=tr(cat.get("desc", "")),
|
||||
icon_path=cat.get("icon"),
|
||||
on_click=on_click,
|
||||
starpilot_icon=cat.get("starpilot_icon", True),
|
||||
bg_color=cat.get("color"),
|
||||
)
|
||||
elif tile_type == "toggle":
|
||||
tile = ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=cat["set_state"], icon_path=cat.get("icon"), bg_color=cat.get("color"))
|
||||
elif tile_type == "value":
|
||||
tile = ValueTile(title=tr(cat["title"]), get_value=cat["get_value"], on_click=cat["on_click"], icon_path=cat.get("icon"), bg_color=cat.get("color"))
|
||||
else:
|
||||
continue
|
||||
|
||||
self._tile_grid.add_tile(tile)
|
||||
|
||||
def _on_select_make(self):
|
||||
makes = sorted(list(MAKE_TO_FOLDER.keys()))
|
||||
makes = [m.capitalize() for m in makes]
|
||||
|
||||
def on_select(res, val):
|
||||
if res == DialogResult.CONFIRM:
|
||||
self._params.put("CarMake", val)
|
||||
self._params.remove("CarModel")
|
||||
self._params.remove("CarModelName")
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(SelectionDialog(tr("Select Make"), makes, self._params.get("CarMake", encoding='utf-8') or "", on_close=on_select))
|
||||
|
||||
def _on_select_model(self):
|
||||
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",
|
||||
"key": "GMPedalLongitudinal",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Remote Start Panda"),
|
||||
"type": "toggle",
|
||||
"get_state": lambda: self._params.get_bool("RemoteStartBootsComma"),
|
||||
"set_state": lambda s: self._params.put_bool("RemoteStartBootsComma", s),
|
||||
"color": "#FFC40D",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Volt SNG Hack"),
|
||||
"type": "toggle",
|
||||
"get_state": lambda: self._params.get_bool("VoltSNG"),
|
||||
"set_state": lambda s: self._params.put_bool("VoltSNG", s),
|
||||
"color": "#FFC40D",
|
||||
"key": "VoltSNG",
|
||||
},
|
||||
]
|
||||
self._rebuild_grid()
|
||||
|
||||
def _rebuild_grid(self):
|
||||
if not self.CATEGORIES:
|
||||
return
|
||||
if self._tile_grid is None:
|
||||
self._tile_grid = TileGrid(columns=None, padding=20)
|
||||
self._tile_grid.clear()
|
||||
|
||||
cs = starpilot_state.car_state
|
||||
for cat in self.CATEGORIES:
|
||||
key = cat.get("key")
|
||||
visible = True
|
||||
|
||||
if key == "GMPedalLongitudinal":
|
||||
visible = cs.hasPedal
|
||||
elif key == "VoltSNG":
|
||||
visible = cs.isVolt and not cs.hasSNG
|
||||
|
||||
if not visible:
|
||||
continue
|
||||
|
||||
tile = ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=cat["set_state"], icon_path=cat.get("icon"), bg_color=cat.get("color"))
|
||||
self._tile_grid.add_tile(tile)
|
||||
|
||||
|
||||
class StarPilotHKGVehicleLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.CATEGORIES = [
|
||||
{
|
||||
"title": tr_noop("Taco Bell Torque Hack"),
|
||||
"type": "toggle",
|
||||
"get_state": lambda: self._params.get_bool("TacoTuneHacks"),
|
||||
"set_state": lambda s: self._params.put_bool("TacoTuneHacks", s),
|
||||
"color": "#FFC40D",
|
||||
"key": "TacoTuneHacks",
|
||||
},
|
||||
]
|
||||
self._rebuild_grid()
|
||||
|
||||
def _rebuild_grid(self):
|
||||
if not self.CATEGORIES:
|
||||
return
|
||||
if self._tile_grid is None:
|
||||
self._tile_grid = TileGrid(columns=None, padding=20)
|
||||
self._tile_grid.clear()
|
||||
|
||||
cs = starpilot_state.car_state
|
||||
for cat in self.CATEGORIES:
|
||||
key = cat.get("key")
|
||||
visible = True
|
||||
|
||||
if key == "TacoTuneHacks":
|
||||
visible = cs.isHKGCanFd
|
||||
|
||||
if not visible:
|
||||
continue
|
||||
|
||||
tile = ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=cat["set_state"], icon_path=cat.get("icon"), bg_color=cat.get("color"))
|
||||
self._tile_grid.add_tile(tile)
|
||||
|
||||
|
||||
class StarPilotSubaruVehicleLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.CATEGORIES = [
|
||||
{
|
||||
"title": tr_noop("Stop and Go"),
|
||||
"type": "toggle",
|
||||
"get_state": lambda: self._params.get_bool("SubaruSNG"),
|
||||
"set_state": lambda s: self._params.put_bool("SubaruSNG", s),
|
||||
"color": "#FFC40D",
|
||||
},
|
||||
]
|
||||
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("Lock Doors Timer"),
|
||||
"type": "value",
|
||||
"get_value": lambda: _lock_doors_timer_labels().get(self._params.get_int('LockDoorsTimer'), f"{self._params.get_int('LockDoorsTimer')}s"),
|
||||
"on_click": self._show_lock_timer_selector,
|
||||
"color": "#FFC40D",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Dashboard Speed Offset"),
|
||||
"type": "value",
|
||||
"get_value": lambda: f"{self._params.get_float('ClusterOffset'):.3f}x",
|
||||
"on_click": self._show_offset_selector,
|
||||
"color": "#FFC40D",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Stop-and-Go Hack"),
|
||||
"type": "toggle",
|
||||
"get_state": lambda: self._params.get_bool("SNGHack"),
|
||||
"set_state": lambda s: self._params.put_bool("SNGHack", s),
|
||||
"color": "#FFC40D",
|
||||
"key": "SNGHack",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("FrogsGoMoo Tweak"),
|
||||
"type": "toggle",
|
||||
"get_state": lambda: self._params.get_bool("FrogsGoMoosTweak"),
|
||||
"set_state": lambda s: self._params.put_bool("FrogsGoMoosTweak", s),
|
||||
"color": "#FFC40D",
|
||||
"key": "FrogsGoMoosTweak",
|
||||
},
|
||||
]
|
||||
self._rebuild_grid()
|
||||
|
||||
def _rebuild_grid(self):
|
||||
if not self.CATEGORIES:
|
||||
return
|
||||
if self._tile_grid is None:
|
||||
self._tile_grid = TileGrid(columns=None, padding=20)
|
||||
self._tile_grid.clear()
|
||||
|
||||
cs = starpilot_state.car_state
|
||||
for cat in self.CATEGORIES:
|
||||
key = cat.get("key")
|
||||
visible = True
|
||||
|
||||
if key == "SNGHack":
|
||||
visible = not cs.hasSNG
|
||||
elif key == "FrogsGoMoosTweak":
|
||||
visible = cs.hasOpenpilotLongitudinal
|
||||
|
||||
if not visible:
|
||||
continue
|
||||
|
||||
tile_type = cat.get("type", "hub")
|
||||
if tile_type == "toggle":
|
||||
tile = ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=cat["set_state"], icon_path=cat.get("icon"), bg_color=cat.get("color"))
|
||||
elif tile_type == "value":
|
||||
tile = ValueTile(title=tr(cat["title"]), get_value=cat["get_value"], on_click=cat["on_click"], icon_path=cat.get("icon"), bg_color=cat.get("color"))
|
||||
else:
|
||||
continue
|
||||
|
||||
self._tile_grid.add_tile(tile)
|
||||
|
||||
def _show_lock_timer_selector(self):
|
||||
def on_close(res, val):
|
||||
if res == DialogResult.CONFIRM:
|
||||
self._params.put_int("LockDoorsTimer", int(val))
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(
|
||||
SliderDialog(tr("Lock Doors Timer"), 0, 300, 5, self._params.get_int("LockDoorsTimer"), on_close, labels=_lock_doors_timer_labels(), color="#FFC40D")
|
||||
)
|
||||
|
||||
def _show_offset_selector(self):
|
||||
def on_close(res, val):
|
||||
if res == DialogResult.CONFIRM:
|
||||
self._params.put_float("ClusterOffset", float(val))
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(
|
||||
SliderDialog(tr("Dashboard Speed Offset"), 1.000, 1.050, 0.001, self._params.get_float("ClusterOffset"), on_close, unit="x", color="#FFC40D")
|
||||
)
|
||||
|
||||
|
||||
class StarPilotVehicleInfoLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.CATEGORIES = [
|
||||
{
|
||||
"title": tr_noop("Radar Support"),
|
||||
"type": "value",
|
||||
"get_value": lambda: tr("Yes") if starpilot_state.car_state.hasRadar else tr("No"),
|
||||
"on_click": lambda: None,
|
||||
"color": "#FFC40D",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Longitudinal Support"),
|
||||
"type": "value",
|
||||
"get_value": lambda: tr("Yes") if starpilot_state.car_state.hasOpenpilotLongitudinal else tr("No"),
|
||||
"on_click": lambda: None,
|
||||
"color": "#FFC40D",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Blind Spot Support"),
|
||||
"type": "value",
|
||||
"get_value": lambda: tr("Yes") if starpilot_state.car_state.hasBSM else tr("No"),
|
||||
"on_click": lambda: None,
|
||||
"color": "#FFC40D",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Hardware Detected"),
|
||||
"type": "value",
|
||||
"get_value": lambda: (
|
||||
", ".join(
|
||||
filter(
|
||||
None,
|
||||
[
|
||||
tr("Pedal") if starpilot_state.car_state.canUsePedal else "",
|
||||
tr("SDSU") if starpilot_state.car_state.canUseSDSU else "",
|
||||
tr("ZSS") if starpilot_state.car_state.hasZSS else "",
|
||||
],
|
||||
)
|
||||
)
|
||||
or tr("None")
|
||||
),
|
||||
"on_click": lambda: None,
|
||||
"color": "#FFC40D",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Pedal Support"),
|
||||
"type": "value",
|
||||
"get_value": lambda: tr("Yes") if starpilot_state.car_state.canUsePedal else tr("No"),
|
||||
"on_click": lambda: None,
|
||||
"color": "#FFC40D",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("SDSU Support"),
|
||||
"type": "value",
|
||||
"get_value": lambda: tr("Yes") if starpilot_state.car_state.canUseSDSU else tr("No"),
|
||||
"on_click": lambda: None,
|
||||
"color": "#FFC40D",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("SNG Support"),
|
||||
"type": "value",
|
||||
"get_value": lambda: tr("Yes") if starpilot_state.car_state.hasSNG else tr("No"),
|
||||
"on_click": lambda: None,
|
||||
"color": "#FFC40D",
|
||||
},
|
||||
]
|
||||
self._rebuild_grid()
|
||||
|
||||
@@ -1,40 +1,509 @@
|
||||
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
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.metro import TileGrid, ToggleTile, 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",
|
||||
"key": "HideSpeed",
|
||||
"get_state": lambda: self._params.get_bool("HideSpeed"),
|
||||
"set_state": lambda s: self._params.put_bool("HideSpeed", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Hide Lead Marker"),
|
||||
"type": "toggle",
|
||||
"key": "HideLeadMarker",
|
||||
"get_state": lambda: self._params.get_bool("HideLeadMarker"),
|
||||
"set_state": lambda s: self._params.put_bool("HideLeadMarker", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Hide Max Speed"),
|
||||
"type": "toggle",
|
||||
"key": "HideMaxSpeed",
|
||||
"get_state": lambda: self._params.get_bool("HideMaxSpeed"),
|
||||
"set_state": lambda s: self._params.put_bool("HideMaxSpeed", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Hide Alerts"),
|
||||
"type": "toggle",
|
||||
"key": "HideAlerts",
|
||||
"get_state": lambda: self._params.get_bool("HideAlerts"),
|
||||
"set_state": lambda s: self._params.put_bool("HideAlerts", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Hide Speed Limit"),
|
||||
"type": "toggle",
|
||||
"key": "HideSpeedLimit",
|
||||
"get_state": lambda: self._params.get_bool("HideSpeedLimit"),
|
||||
"set_state": lambda s: self._params.put_bool("HideSpeedLimit", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Wheel Speed"),
|
||||
"type": "toggle",
|
||||
"key": "WheelSpeed",
|
||||
"get_state": lambda: self._params.get_bool("WheelSpeed"),
|
||||
"set_state": lambda s: self._params.put_bool("WheelSpeed", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
]
|
||||
self._rebuild_grid()
|
||||
|
||||
def _rebuild_grid(self):
|
||||
if not self.CATEGORIES:
|
||||
return
|
||||
if self._tile_grid is None:
|
||||
self._tile_grid = TileGrid(columns=None, padding=20)
|
||||
self._tile_grid.clear()
|
||||
|
||||
for cat in self.CATEGORIES:
|
||||
key = cat.get("key")
|
||||
visible = True
|
||||
|
||||
if key == "HideLeadMarker":
|
||||
visible &= starpilot_state.car_state.hasOpenpilotLongitudinal
|
||||
|
||||
if not visible:
|
||||
continue
|
||||
|
||||
tile = ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=cat["set_state"], icon_path=cat.get("icon"), bg_color=cat.get("color"))
|
||||
self._tile_grid.add_tile(tile)
|
||||
|
||||
|
||||
class StarPilotVisualWidgetsLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.CATEGORIES = [
|
||||
{
|
||||
"title": tr_noop("Acceleration Path"),
|
||||
"type": "toggle",
|
||||
"key": "AccelerationPath",
|
||||
"get_state": lambda: self._params.get_bool("AccelerationPath"),
|
||||
"set_state": lambda s: self._params.put_bool("AccelerationPath", s),
|
||||
"icon": "toggle_icons/icon_road.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Adjacent Lanes"),
|
||||
"type": "toggle",
|
||||
"key": "AdjacentPath",
|
||||
"get_state": lambda: self._params.get_bool("AdjacentPath"),
|
||||
"set_state": lambda s: self._params.put_bool("AdjacentPath", s),
|
||||
"icon": "toggle_icons/icon_road.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Blind Spot Path"),
|
||||
"type": "toggle",
|
||||
"key": "BlindSpotPath",
|
||||
"get_state": lambda: self._params.get_bool("BlindSpotPath"),
|
||||
"set_state": lambda s: self._params.put_bool("BlindSpotPath", s),
|
||||
"icon": "toggle_icons/icon_road.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Compass"),
|
||||
"type": "toggle",
|
||||
"key": "Compass",
|
||||
"get_state": lambda: self._params.get_bool("Compass"),
|
||||
"set_state": lambda s: self._params.put_bool("Compass", s),
|
||||
"icon": "toggle_icons/icon_navigate.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Personality Button"),
|
||||
"type": "toggle",
|
||||
"key": "OnroadDistanceButton",
|
||||
"get_state": lambda: self._params.get_bool("OnroadDistanceButton"),
|
||||
"set_state": lambda s: self._params.put_bool("OnroadDistanceButton", s),
|
||||
"icon": "toggle_icons/icon_personality.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Pedal Indicators"),
|
||||
"type": "toggle",
|
||||
"key": "PedalsOnUI",
|
||||
"get_state": lambda: self._params.get_bool("PedalsOnUI"),
|
||||
"set_state": lambda s: self._params.put_bool("PedalsOnUI", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Dynamic Pedals"),
|
||||
"type": "toggle",
|
||||
"key": "DynamicPedalsOnUI",
|
||||
"get_state": lambda: self._params.get_bool("DynamicPedalsOnUI"),
|
||||
"set_state": lambda s: self._set_exclusive_pedal("DynamicPedalsOnUI", "StaticPedalsOnUI", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Static Pedals"),
|
||||
"type": "toggle",
|
||||
"key": "StaticPedalsOnUI",
|
||||
"get_state": lambda: self._params.get_bool("StaticPedalsOnUI"),
|
||||
"set_state": lambda s: self._set_exclusive_pedal("StaticPedalsOnUI", "DynamicPedalsOnUI", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Rotating Wheel"),
|
||||
"type": "toggle",
|
||||
"key": "RotatingWheel",
|
||||
"get_state": lambda: self._params.get_bool("RotatingWheel"),
|
||||
"set_state": lambda s: self._params.put_bool("RotatingWheel", s),
|
||||
"icon": "toggle_icons/icon_steering.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
]
|
||||
self._rebuild_grid()
|
||||
|
||||
def _set_exclusive_pedal(self, key, other_key, state):
|
||||
self._params.put_bool(key, state)
|
||||
if state:
|
||||
self._params.put_bool(other_key, False)
|
||||
self._rebuild_grid()
|
||||
|
||||
def _rebuild_grid(self):
|
||||
if not self.CATEGORIES:
|
||||
return
|
||||
if self._tile_grid is None:
|
||||
self._tile_grid = TileGrid(columns=None, padding=20)
|
||||
self._tile_grid.clear()
|
||||
|
||||
pedals_on_ui = self._params.get_bool("PedalsOnUI")
|
||||
|
||||
for cat in self.CATEGORIES:
|
||||
key = cat.get("key")
|
||||
visible = True
|
||||
|
||||
if key == "AccelerationPath":
|
||||
visible &= starpilot_state.car_state.hasOpenpilotLongitudinal
|
||||
elif key == "BlindSpotPath":
|
||||
visible &= starpilot_state.car_state.hasBSM
|
||||
elif key == "PedalsOnUI":
|
||||
visible &= starpilot_state.car_state.hasOpenpilotLongitudinal
|
||||
elif key == "DynamicPedalsOnUI":
|
||||
visible &= starpilot_state.car_state.hasOpenpilotLongitudinal and pedals_on_ui
|
||||
elif key == "StaticPedalsOnUI":
|
||||
visible &= starpilot_state.car_state.hasOpenpilotLongitudinal and pedals_on_ui
|
||||
|
||||
if not visible:
|
||||
continue
|
||||
|
||||
tile = ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=cat["set_state"], icon_path=cat.get("icon"), bg_color=cat.get("color"))
|
||||
self._tile_grid.add_tile(tile)
|
||||
|
||||
|
||||
class StarPilotModelUILayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.CATEGORIES = [
|
||||
{
|
||||
"title": tr_noop("Dynamic Path"),
|
||||
"type": "toggle",
|
||||
"key": "DynamicPathWidth",
|
||||
"get_state": lambda: self._params.get_bool("DynamicPathWidth"),
|
||||
"set_state": lambda s: self._params.put_bool("DynamicPathWidth", s),
|
||||
"icon": "toggle_icons/icon_road.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Lane Line Width"),
|
||||
"type": "value",
|
||||
"key": "LaneLinesWidth",
|
||||
"get_value": lambda: self._get_lane_lines_display(),
|
||||
"on_click": lambda: self._show_int_selector("LaneLinesWidth", 0, 24, self._get_lane_lines_unit()),
|
||||
"icon": "toggle_icons/icon_road.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Path Edge Width"),
|
||||
"type": "value",
|
||||
"key": "PathEdgeWidth",
|
||||
"get_value": lambda: f"{self._params.get_int('PathEdgeWidth')}%",
|
||||
"on_click": lambda: self._show_int_selector("PathEdgeWidth", 0, 100, "%"),
|
||||
"icon": "toggle_icons/icon_road.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Path Width"),
|
||||
"type": "value",
|
||||
"key": "PathWidth",
|
||||
"get_value": lambda: self._get_path_width_display(),
|
||||
"on_click": lambda: self._show_path_width_selector(),
|
||||
"icon": "toggle_icons/icon_road.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Road Edge Width"),
|
||||
"type": "value",
|
||||
"key": "RoadEdgesWidth",
|
||||
"get_value": lambda: self._get_road_edges_display(),
|
||||
"on_click": lambda: self._show_int_selector("RoadEdgesWidth", 0, 24, self._get_road_edges_unit()),
|
||||
"icon": "toggle_icons/icon_road.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
]
|
||||
self._rebuild_grid()
|
||||
|
||||
def _get_lane_lines_unit(self):
|
||||
return "cm" if self._params.get_bool("IsMetric") else "in"
|
||||
|
||||
def _get_lane_lines_display(self):
|
||||
val = self._params.get_int('LaneLinesWidth')
|
||||
if self._params.get_bool("IsMetric"):
|
||||
return f"{int(val * 2.54)}cm"
|
||||
return f"{val}in"
|
||||
|
||||
def _get_path_width_unit(self):
|
||||
return "m" if self._params.get_bool("IsMetric") else "ft"
|
||||
|
||||
def _get_path_width_display(self):
|
||||
val = self._params.get_float('PathWidth')
|
||||
if self._params.get_bool("IsMetric"):
|
||||
return f"{val / 3.28084:.1f}m"
|
||||
return f"{val:.1f}ft"
|
||||
|
||||
def _get_road_edges_unit(self):
|
||||
return "cm" if self._params.get_bool("IsMetric") else "in"
|
||||
|
||||
def _get_road_edges_display(self):
|
||||
val = self._params.get_int('RoadEdgesWidth')
|
||||
if self._params.get_bool("IsMetric"):
|
||||
return f"{int(val * 2.54)}cm"
|
||||
return f"{val}in"
|
||||
|
||||
def _show_path_width_selector(self):
|
||||
if self._params.get_bool("IsMetric"):
|
||||
self._show_float_selector("PathWidth", 0, 10, 0.1, "m", convert=lambda v: v / 3.28084, unconvert=lambda v: v * 3.28084)
|
||||
else:
|
||||
self._show_float_selector("PathWidth", 0, 10, 0.1, "ft")
|
||||
|
||||
def _show_int_selector(self, key, min_v, max_v, unit=""):
|
||||
def on_close(res, val):
|
||||
if res == DialogResult.CONFIRM:
|
||||
self._params.put_int(key, int(val))
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(SliderDialog(tr(key), min_v, max_v, 1, self._params.get_int(key), on_close, unit=unit, color="#A200FF"))
|
||||
|
||||
def _show_float_selector(self, key, min_v, max_v, step, unit="", convert=None, unconvert=None):
|
||||
current = self._params.get_float(key)
|
||||
if convert:
|
||||
current = convert(current)
|
||||
|
||||
def on_close(res, val):
|
||||
if res == DialogResult.CONFIRM:
|
||||
v = float(val)
|
||||
if unconvert:
|
||||
v = unconvert(v)
|
||||
self._params.put_float(key, v)
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(SliderDialog(tr(key), min_v, max_v, step, current, on_close, unit=unit, color="#A200FF"))
|
||||
|
||||
|
||||
class StarPilotNavigationVisualsLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.CATEGORIES = [
|
||||
{
|
||||
"title": tr_noop("Road Name"),
|
||||
"type": "toggle",
|
||||
"get_state": lambda: self._params.get_bool("RoadNameUI"),
|
||||
"set_state": lambda s: self._params.put_bool("RoadNameUI", s),
|
||||
"icon": "toggle_icons/icon_navigate.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Speed Limits"),
|
||||
"type": "toggle",
|
||||
"get_state": lambda: self._params.get_bool("ShowSpeedLimits"),
|
||||
"set_state": lambda s: self._params.put_bool("ShowSpeedLimits", s),
|
||||
"icon": "toggle_icons/icon_speed_limit.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Mapbox Limits"),
|
||||
"type": "toggle",
|
||||
"get_state": lambda: self._params.get_bool("SLCMapboxFiller"),
|
||||
"set_state": lambda s: self._params.put_bool("SLCMapboxFiller", s),
|
||||
"icon": "toggle_icons/icon_speed_limit.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Vienna Signs"),
|
||||
"type": "toggle",
|
||||
"get_state": lambda: self._params.get_bool("UseVienna"),
|
||||
"set_state": lambda s: self._params.put_bool("UseVienna", s),
|
||||
"icon": "toggle_icons/icon_speed_limit.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
]
|
||||
self._rebuild_grid()
|
||||
|
||||
|
||||
class StarPilotVisualQOLLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.CAMERA_VIEWS = ["Auto", "Driver", "Standard", "Wide"]
|
||||
self.CATEGORIES = [
|
||||
{
|
||||
"title": tr_noop("Camera View"),
|
||||
"type": "value",
|
||||
"key": "CameraView",
|
||||
"get_value": lambda: tr(self.CAMERA_VIEWS[self._params.get_int('CameraView')]),
|
||||
"on_click": lambda: self._show_camera_view_selector(),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Driver Camera"),
|
||||
"type": "toggle",
|
||||
"get_state": lambda: self._params.get_bool("DriverCamera"),
|
||||
"set_state": lambda s: self._params.put_bool("DriverCamera", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Stopped Timer"),
|
||||
"type": "toggle",
|
||||
"get_state": lambda: self._params.get_bool("StoppedTimer"),
|
||||
"set_state": lambda s: self._params.put_bool("StoppedTimer", s),
|
||||
"icon": "toggle_icons/icon_display.png",
|
||||
"color": "#A200FF",
|
||||
},
|
||||
]
|
||||
self._rebuild_grid()
|
||||
|
||||
def _show_camera_view_selector(self):
|
||||
current = self._params.get_int("CameraView")
|
||||
|
||||
def on_select(res, val):
|
||||
if res == DialogResult.CONFIRM:
|
||||
idx = self.CAMERA_VIEWS.index(val)
|
||||
self._params.put_int("CameraView", idx)
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(SelectionDialog(tr("Camera View"), self.CAMERA_VIEWS, self.CAMERA_VIEWS[current], on_close=on_select))
|
||||
|
||||
@@ -1,20 +1,107 @@
|
||||
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
|
||||
|
||||
ACTION_NAMES = ["No Action", "Change Personality", "Force Coast", "Pause Steering", "Pause Accel/Brake", "Toggle Experimental", "Toggle Traffic"]
|
||||
ACTION_IDS = {name: i for i, name in enumerate(ACTION_NAMES)}
|
||||
|
||||
|
||||
class StarPilotWheelLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
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),
|
||||
"color": "#FFC40D",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Distance Button"),
|
||||
"type": "value",
|
||||
"get_value": lambda: self._get_action_name("DistanceButtonControl"),
|
||||
"on_click": lambda: self._show_action_picker("DistanceButtonControl"),
|
||||
"color": "#FFC40D",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Distance (Long Press)"),
|
||||
"type": "value",
|
||||
"get_value": lambda: self._get_action_name("LongDistanceButtonControl"),
|
||||
"on_click": lambda: self._show_action_picker("LongDistanceButtonControl"),
|
||||
"color": "#FFC40D",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Distance (Very Long)"),
|
||||
"type": "value",
|
||||
"get_value": lambda: self._get_action_name("VeryLongDistanceButtonControl"),
|
||||
"on_click": lambda: self._show_action_picker("VeryLongDistanceButtonControl"),
|
||||
"color": "#FFC40D",
|
||||
},
|
||||
{
|
||||
"title": tr_noop("LKAS Button"),
|
||||
"type": "value",
|
||||
"get_value": lambda: self._get_action_name("LKASButtonControl"),
|
||||
"on_click": lambda: self._show_action_picker("LKASButtonControl"),
|
||||
"key": "LKASButtonControl",
|
||||
"color": "#FFC40D",
|
||||
},
|
||||
]
|
||||
self._rebuild_grid()
|
||||
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
def _get_action_name(self, key):
|
||||
idx = self._params.get_int(key)
|
||||
if 0 <= idx < len(ACTION_NAMES):
|
||||
return ACTION_NAMES[idx]
|
||||
return ACTION_NAMES[0]
|
||||
|
||||
def _get_available_actions(self):
|
||||
actions = list(ACTION_NAMES[:1]) # No Action
|
||||
cs = starpilot_state.car_state
|
||||
if cs.hasOpenpilotLongitudinal:
|
||||
actions.extend(ACTION_NAMES[1:])
|
||||
return actions
|
||||
|
||||
def _show_action_picker(self, key):
|
||||
actions = self._get_available_actions()
|
||||
current = self._get_action_name(key)
|
||||
if current not in actions:
|
||||
current = actions[0]
|
||||
|
||||
def on_select(res, val):
|
||||
if res == DialogResult.CONFIRM:
|
||||
self._params.put_int(key, ACTION_IDS.get(val, 0))
|
||||
self._rebuild_grid()
|
||||
|
||||
gui_app.set_modal_overlay(SelectionDialog(tr(key), actions, current, on_close=on_select))
|
||||
|
||||
def _rebuild_grid(self):
|
||||
if not self.CATEGORIES:
|
||||
return
|
||||
if self._tile_grid is None:
|
||||
self._tile_grid = __import__('openpilot.selfdrive.ui.layouts.settings.starpilot.metro', fromlist=['TileGrid']).TileGrid(columns=None, padding=20)
|
||||
self._tile_grid.clear()
|
||||
cs = starpilot_state.car_state
|
||||
for cat in self.CATEGORIES:
|
||||
key = cat.get("key")
|
||||
visible = True
|
||||
if key == "LKASButtonControl":
|
||||
visible &= not cs.isSubaru
|
||||
if not visible:
|
||||
continue
|
||||
tile_type = cat.get("type", "hub")
|
||||
if tile_type == "toggle":
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.metro import ToggleTile
|
||||
|
||||
tile = ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=cat["set_state"], bg_color=cat.get("color"))
|
||||
elif tile_type == "value":
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.metro import ValueTile
|
||||
|
||||
tile = ValueTile(title=tr(cat["title"]), get_value=cat["get_value"], on_click=cat["on_click"], bg_color=cat.get("color"))
|
||||
else:
|
||||
continue
|
||||
self._tile_grid.add_tile(tile)
|
||||
|
||||
+204
-14
@@ -1,9 +1,12 @@
|
||||
import pyray as rl
|
||||
import socket
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import Callable
|
||||
from cereal import log
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.onroad.gif_player import load_starpilot_asset
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, FONT_SCALE
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
@@ -18,6 +21,11 @@ FONT_SIZE = 35
|
||||
SETTINGS_BTN = rl.Rectangle(50, 35, 200, 117)
|
||||
HOME_BTN = rl.Rectangle(60, 860, 180, 180)
|
||||
|
||||
# FrogPilot click regions (for metric cycling)
|
||||
FP_TEMP_REGION = rl.Rectangle(30, 338, 240, 126)
|
||||
FP_COMPUTE_REGION = rl.Rectangle(30, 496, 240, 126)
|
||||
FP_STORAGE_REGION = rl.Rectangle(30, 654, 240, 126)
|
||||
|
||||
ThermalStatus = log.DeviceState.ThermalStatus
|
||||
NetworkType = log.DeviceState.NetworkType
|
||||
|
||||
@@ -73,11 +81,34 @@ class Sidebar(Widget):
|
||||
self._connect_status = MetricData(tr_noop("CONNECT"), tr_noop("OFFLINE"), Colors.WARNING)
|
||||
self._recording_audio = False
|
||||
|
||||
# FrogPilot metrics (replaces stock slots when toggles are on)
|
||||
self._fp_temp = MetricData(tr_noop("TEMP"), tr_noop("--"), Colors.GOOD)
|
||||
self._fp_compute = MetricData(tr_noop("CPU"), tr_noop("--"), Colors.GOOD)
|
||||
self._fp_storage = MetricData(tr_noop("MEM"), tr_noop("--"), Colors.GOOD)
|
||||
|
||||
# FrogPilot click-to-cycle state (0=stock, then 1..N for options)
|
||||
self._fp_temp_cycle = 0 # 0=temp, 1=numerical temp
|
||||
self._fp_compute_cycle = 0 # 0=stock, 1=CPU, 2=GPU
|
||||
self._fp_storage_cycle = 0 # 0=stock, 1=memory, 2=storage left, 3=storage used
|
||||
|
||||
self._toggles: dict = {}
|
||||
|
||||
# IP address (cached, refreshed rarely)
|
||||
self._ip_address: str = ""
|
||||
self._ip_refresh_time: float = 0.0
|
||||
|
||||
self._home_img = gui_app.texture("images/button_home.png", HOME_BTN.width, HOME_BTN.height)
|
||||
self._flag_img = gui_app.texture("images/button_flag.png", HOME_BTN.width, HOME_BTN.height)
|
||||
self._settings_img = gui_app.texture("images/button_settings.png", SETTINGS_BTN.width, SETTINGS_BTN.height)
|
||||
self._mic_img = gui_app.texture("icons/microphone.png", 30, 30)
|
||||
self._mic_indicator_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
|
||||
# FrogPilot: theme GIF buttons (lazy loaded)
|
||||
self._home_gif = None
|
||||
self._flag_gif = None
|
||||
self._settings_gif = None
|
||||
self._gifs_loaded = False
|
||||
|
||||
self._font_regular = gui_app.font(FontWeight.NORMAL)
|
||||
self._font_bold = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
|
||||
@@ -86,8 +117,7 @@ class Sidebar(Widget):
|
||||
self._on_flag_click: Callable | None = None
|
||||
self._open_settings_callback: Callable | None = None
|
||||
|
||||
def set_callbacks(self, on_settings: Callable | None = None, on_flag: Callable | None = None,
|
||||
open_settings: Callable | None = None):
|
||||
def set_callbacks(self, on_settings: Callable | None = None, on_flag: Callable | None = None, open_settings: Callable | None = None):
|
||||
self._on_settings_click = on_settings
|
||||
self._on_flag_click = on_flag
|
||||
self._open_settings_callback = open_settings
|
||||
@@ -102,6 +132,29 @@ class Sidebar(Widget):
|
||||
|
||||
def _update_state(self):
|
||||
sm = ui_state.sm
|
||||
self._toggles = ui_state.frogpilot_toggles
|
||||
|
||||
# FrogPilot: load theme GIFs on first update
|
||||
if not self._gifs_loaded:
|
||||
self._gifs_loaded = True
|
||||
try:
|
||||
self._home_gif = load_starpilot_asset("active_theme/icons/button_home.gif", int(HOME_BTN.width))
|
||||
except Exception:
|
||||
self._home_gif = None
|
||||
try:
|
||||
self._flag_gif = load_starpilot_asset("active_theme/icons/button_flag.gif", int(HOME_BTN.width))
|
||||
except Exception:
|
||||
self._flag_gif = None
|
||||
try:
|
||||
self._settings_gif = load_starpilot_asset("active_theme/icons/button_settings.gif", int(SETTINGS_BTN.width))
|
||||
except Exception:
|
||||
self._settings_gif = None
|
||||
|
||||
# Advance GIF animations
|
||||
for gif in (self._home_gif, self._flag_gif, self._settings_gif):
|
||||
if gif is not None:
|
||||
gif.play()
|
||||
|
||||
if not sm.updated['deviceState']:
|
||||
return
|
||||
|
||||
@@ -112,6 +165,19 @@ class Sidebar(Widget):
|
||||
self._update_temperature_status(device_state)
|
||||
self._update_connection_status(device_state)
|
||||
self._update_panda_status()
|
||||
self._update_frogpilot_metrics(device_state)
|
||||
|
||||
# Refresh IP address rarely
|
||||
now = time.monotonic()
|
||||
if now - self._ip_refresh_time > 30.0:
|
||||
self._ip_refresh_time = now
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.connect(("8.8.8.8", 80))
|
||||
self._ip_address = s.getsockname()[0]
|
||||
s.close()
|
||||
except Exception:
|
||||
self._ip_address = ""
|
||||
|
||||
def _update_network_status(self, device_state):
|
||||
self._net_type = NETWORK_TYPES.get(device_state.networkType.raw, tr_noop("Unknown"))
|
||||
@@ -143,6 +209,82 @@ class Sidebar(Widget):
|
||||
else:
|
||||
self._panda_status.update(tr_noop("VEHICLE"), tr_noop("ONLINE"), Colors.GOOD)
|
||||
|
||||
def _update_frogpilot_metrics(self, device_state):
|
||||
t = self._toggles
|
||||
is_metric = ui_state.is_metric
|
||||
|
||||
# --- Temperature slot: numerical temp or stock ---
|
||||
dev_ui = t.get("developer_ui", False)
|
||||
if dev_ui and t.get("numerical_temp", False):
|
||||
temps_c = list(device_state.cpuTempC) if device_state.cpuTempC else []
|
||||
temp_c = max(temps_c) if temps_c else 0.0
|
||||
if t.get("fahrenheit", False):
|
||||
val = f"{temp_c * 9 / 5 + 32:.0f}F"
|
||||
else:
|
||||
val = f"{temp_c:.0f}C"
|
||||
color = Colors.DANGER if temp_c >= 90 else Colors.WARNING if temp_c >= 75 else Colors.GOOD
|
||||
custom = t.get("sidebar_color1")
|
||||
if custom:
|
||||
color = self._parse_color(custom)
|
||||
self._fp_temp.update(tr_noop("TEMP"), val, color)
|
||||
else:
|
||||
self._fp_temp = self._temp_status
|
||||
|
||||
# --- Compute slot: CPU/GPU or stock ---
|
||||
show_cpu = t.get("show_cpu", False)
|
||||
show_gpu = t.get("show_gpu", False)
|
||||
if show_cpu or show_gpu:
|
||||
if show_gpu:
|
||||
gpu_pct = device_state.gpuUsagePercent if device_state.gpuUsagePercent else 0
|
||||
val = f"{gpu_pct}%"
|
||||
color = Colors.DANGER if gpu_pct >= 85 else Colors.WARNING if gpu_pct >= 70 else Colors.GOOD
|
||||
self._fp_compute.update(tr_noop("GPU"), val, color)
|
||||
else:
|
||||
cpu_loads = list(device_state.cpuUsagePercent) if device_state.cpuUsagePercent else []
|
||||
avg = sum(cpu_loads) / max(len(cpu_loads), 1) if cpu_loads else 0
|
||||
val = f"{int(avg)}%"
|
||||
color = Colors.DANGER if avg >= 85 else Colors.WARNING if avg >= 70 else Colors.GOOD
|
||||
self._fp_compute.update(tr_noop("CPU"), val, color)
|
||||
custom = t.get("sidebar_color2")
|
||||
if custom:
|
||||
self._fp_compute.color = self._parse_color(custom)
|
||||
else:
|
||||
self._fp_compute = self._panda_status
|
||||
|
||||
# --- Storage slot: memory/storage or stock ---
|
||||
show_mem = t.get("show_memory_usage", False)
|
||||
show_storage_left = t.get("show_storage_left", False)
|
||||
show_storage_used = t.get("show_storage_used", False)
|
||||
if show_mem:
|
||||
mem_pct = device_state.memoryUsagePercent if device_state.memoryUsagePercent else 0
|
||||
val = f"{mem_pct}%"
|
||||
color = Colors.DANGER if mem_pct >= 85 else Colors.WARNING if mem_pct >= 70 else Colors.GOOD
|
||||
self._fp_storage.update(tr_noop("MEM"), val, color)
|
||||
elif show_storage_left or show_storage_used:
|
||||
if sm_valid := ui_state.sm.valid.get("frogpilotDeviceState", False):
|
||||
fp_dev = ui_state.sm["frogpilotDeviceState"]
|
||||
gb = fp_dev.freeSpace if show_storage_left else fp_dev.usedSpace
|
||||
label = tr_noop("SSD LEFT") if show_storage_left else tr_noop("SSD USED")
|
||||
val = f"{gb:.0f} GB"
|
||||
color = Colors.DANGER if (show_storage_left and gb < 5) else Colors.WARNING if (show_storage_left and gb < 25) else Colors.GOOD
|
||||
self._fp_storage.update(label, val, color)
|
||||
else:
|
||||
self._fp_storage = self._connect_status
|
||||
else:
|
||||
self._fp_storage = self._connect_status
|
||||
custom = t.get("sidebar_color3")
|
||||
if custom:
|
||||
self._fp_storage.color = self._parse_color(custom)
|
||||
|
||||
@staticmethod
|
||||
def _parse_color(color_str: str) -> rl.Color:
|
||||
s = color_str.lstrip('#')
|
||||
if len(s) == 8:
|
||||
return rl.Color(int(s[2:4], 16), int(s[4:6], 16), int(s[6:8], 16), int(s[0:2], 16))
|
||||
if len(s) == 6:
|
||||
return rl.Color(int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16), 255)
|
||||
return rl.Color(255, 255, 255, 255)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN):
|
||||
if self._on_settings_click:
|
||||
@@ -153,22 +295,47 @@ class Sidebar(Widget):
|
||||
elif self._recording_audio and rl.check_collision_point_rec(mouse_pos, self._mic_indicator_rect):
|
||||
if self._open_settings_callback:
|
||||
self._open_settings_callback()
|
||||
# FrogPilot: click-to-cycle metrics when developer_ui is on
|
||||
elif self._toggles.get("developer_ui", False):
|
||||
if rl.check_collision_point_rec(mouse_pos, FP_TEMP_REGION):
|
||||
self._fp_temp_cycle = (self._fp_temp_cycle + 1) % 3
|
||||
p = Params()
|
||||
p.put_bool("Fahrenheit", self._fp_temp_cycle == 2)
|
||||
p.put_bool("NumericalTemp", self._fp_temp_cycle >= 1)
|
||||
elif rl.check_collision_point_rec(mouse_pos, FP_COMPUTE_REGION):
|
||||
self._fp_compute_cycle = (self._fp_compute_cycle + 1) % 3
|
||||
p = Params()
|
||||
p.put_bool("ShowCPU", self._fp_compute_cycle == 1)
|
||||
p.put_bool("ShowGPU", self._fp_compute_cycle == 2)
|
||||
elif rl.check_collision_point_rec(mouse_pos, FP_STORAGE_REGION):
|
||||
self._fp_storage_cycle = (self._fp_storage_cycle + 1) % 4
|
||||
p = Params()
|
||||
p.put_bool("ShowMemoryUsage", self._fp_storage_cycle == 1)
|
||||
p.put_bool("ShowStorageLeft", self._fp_storage_cycle == 2)
|
||||
p.put_bool("ShowStorageUsed", self._fp_storage_cycle == 3)
|
||||
|
||||
def _draw_buttons(self, rect: rl.Rectangle):
|
||||
mouse_pos = rl.get_mouse_position()
|
||||
mouse_down = self.is_pressed and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT)
|
||||
|
||||
# Settings button
|
||||
# Settings button (GIF theme or stock)
|
||||
settings_down = mouse_down and rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN)
|
||||
tint = Colors.BUTTON_PRESSED if settings_down else Colors.BUTTON_NORMAL
|
||||
rl.draw_texture(self._settings_img, int(SETTINGS_BTN.x), int(SETTINGS_BTN.y), tint)
|
||||
gif_tex = self._settings_gif.texture if self._settings_gif is not None else None
|
||||
settings_tex = gif_tex if gif_tex is not None else self._settings_img
|
||||
rl.draw_texture(settings_tex, int(SETTINGS_BTN.x), int(SETTINGS_BTN.y), tint)
|
||||
|
||||
# Home/Flag button
|
||||
# Home/Flag button (GIF theme or stock)
|
||||
flag_pressed = mouse_down and rl.check_collision_point_rec(mouse_pos, HOME_BTN)
|
||||
button_img = self._flag_img if ui_state.started else self._home_img
|
||||
if ui_state.started:
|
||||
gif_tex = self._flag_gif.texture if self._flag_gif is not None else None
|
||||
button_tex = gif_tex if gif_tex is not None else self._flag_img
|
||||
else:
|
||||
gif_tex = self._home_gif.texture if self._home_gif is not None else None
|
||||
button_tex = gif_tex if gif_tex is not None else self._home_img
|
||||
|
||||
tint = Colors.BUTTON_PRESSED if (ui_state.started and flag_pressed) else Colors.BUTTON_NORMAL
|
||||
rl.draw_texture(button_img, int(HOME_BTN.x), int(HOME_BTN.y), tint)
|
||||
rl.draw_texture(button_tex, int(HOME_BTN.x), int(HOME_BTN.y), tint)
|
||||
|
||||
# Microphone button
|
||||
if self._recording_audio:
|
||||
@@ -178,10 +345,23 @@ class Sidebar(Widget):
|
||||
bg_color = rl.Color(Colors.DANGER.r, Colors.DANGER.g, Colors.DANGER.b, int(255 * 0.65)) if mic_pressed else Colors.DANGER
|
||||
|
||||
rl.draw_rectangle_rounded(self._mic_indicator_rect, 1, 10, bg_color)
|
||||
rl.draw_texture(self._mic_img, int(self._mic_indicator_rect.x + (self._mic_indicator_rect.width - self._mic_img.width) / 2),
|
||||
int(self._mic_indicator_rect.y + (self._mic_indicator_rect.height - self._mic_img.height) / 2), Colors.WHITE)
|
||||
rl.draw_texture(
|
||||
self._mic_img,
|
||||
int(self._mic_indicator_rect.x + (self._mic_indicator_rect.width - self._mic_img.width) / 2),
|
||||
int(self._mic_indicator_rect.y + (self._mic_indicator_rect.height - self._mic_img.height) / 2),
|
||||
Colors.WHITE,
|
||||
)
|
||||
|
||||
def _draw_network_indicator(self, rect: rl.Rectangle):
|
||||
# FrogPilot: IP address replaces signal dots
|
||||
if self._toggles.get("ip_metrics", False) and self._ip_address:
|
||||
text_pos = rl.Vector2(rect.x + 58, rect.y + 200)
|
||||
rl.draw_text_ex(self._font_regular, self._ip_address, text_pos, 30, 0, Colors.WHITE)
|
||||
# Still show network type below
|
||||
text_pos2 = rl.Vector2(rect.x + 58, rect.y + 247)
|
||||
rl.draw_text_ex(self._font_regular, tr(self._net_type), text_pos2, FONT_SIZE, 0, Colors.WHITE)
|
||||
return
|
||||
|
||||
# Signal strength dots
|
||||
x_start = rect.x + 58
|
||||
y_pos = rect.y + 196
|
||||
@@ -200,7 +380,20 @@ class Sidebar(Widget):
|
||||
rl.draw_text_ex(self._font_regular, tr(self._net_type), text_pos, FONT_SIZE, 0, Colors.WHITE)
|
||||
|
||||
def _draw_metrics(self, rect: rl.Rectangle):
|
||||
metrics = [(self._temp_status, 338), (self._panda_status, 496), (self._connect_status, 654)]
|
||||
t = self._toggles
|
||||
use_fp = (
|
||||
t.get("numerical_temp", False)
|
||||
or t.get("show_cpu", False)
|
||||
or t.get("show_gpu", False)
|
||||
or t.get("show_memory_usage", False)
|
||||
or t.get("show_storage_left", False)
|
||||
or t.get("show_storage_used", False)
|
||||
)
|
||||
|
||||
if use_fp:
|
||||
metrics = [(self._fp_temp, 338), (self._fp_compute, 496), (self._fp_storage, 654)]
|
||||
else:
|
||||
metrics = [(self._temp_status, 338), (self._panda_status, 496), (self._connect_status, 654)]
|
||||
|
||||
for metric, y_offset in metrics:
|
||||
self._draw_metric(rect, metric, rect.y + y_offset)
|
||||
@@ -222,8 +415,5 @@ class Sidebar(Widget):
|
||||
for text in labels:
|
||||
text_size = measure_text_cached(self._font_bold, text, FONT_SIZE)
|
||||
text_y += text_size.y
|
||||
text_pos = rl.Vector2(
|
||||
metric_rect.x + 22 + (metric_rect.width - 22 - text_size.x) / 2,
|
||||
text_y
|
||||
)
|
||||
text_pos = rl.Vector2(metric_rect.x + 22 + (metric_rect.width - 22 - text_size.x) / 2, text_y)
|
||||
rl.draw_text_ex(self._font_bold, text, text_pos, FONT_SIZE, 0, Colors.WHITE)
|
||||
|
||||
@@ -32,9 +32,9 @@ SELFDRIVE_UNRESPONSIVE_TIMEOUT = 10 # Seconds
|
||||
|
||||
# Constants
|
||||
ALERT_COLORS = {
|
||||
AlertStatus.normal: rl.Color(0x15, 0x15, 0x15, 0xF1), # #151515 with alpha 0xF1
|
||||
AlertStatus.normal: rl.Color(0x15, 0x15, 0x15, 0xF1), # #151515 with alpha 0xF1
|
||||
AlertStatus.userPrompt: rl.Color(0xDA, 0x6F, 0x25, 0xF1), # #DA6F25 with alpha 0xF1
|
||||
AlertStatus.critical: rl.Color(0xC9, 0x22, 0x31, 0xF1), # #C92231 with alpha 0xF1
|
||||
AlertStatus.critical: rl.Color(0xC9, 0x22, 0x31, 0xF1), # #C92231 with alpha 0xF1
|
||||
}
|
||||
|
||||
|
||||
@@ -76,14 +76,21 @@ class AlertRenderer(Widget):
|
||||
self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD)
|
||||
|
||||
# font size is set dynamically
|
||||
self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||
self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP)
|
||||
self._full_text1_label = Label(
|
||||
"",
|
||||
font_size=0,
|
||||
font_weight=FontWeight.BOLD,
|
||||
text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP,
|
||||
)
|
||||
self._full_text2_label = Label(
|
||||
"", font_size=ALERT_FONT_BIG, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP
|
||||
)
|
||||
|
||||
def get_alert(self, sm: messaging.SubMaster) -> Alert | None:
|
||||
"""Generate the current alert based on selfdrive state."""
|
||||
ss = sm['selfdriveState']
|
||||
t = ui_state.frogpilot_toggles
|
||||
|
||||
# Check if selfdriveState messages have stopped arriving
|
||||
recv_frame = sm.recv_frame['selfdriveState']
|
||||
@@ -93,12 +100,16 @@ class AlertRenderer(Widget):
|
||||
# 1. Never received selfdriveState since going onroad
|
||||
waiting_for_startup = recv_frame < ui_state.started_frame
|
||||
if waiting_for_startup and time_since_onroad > 5:
|
||||
if t.get("force_onroad", False):
|
||||
return None
|
||||
return ALERT_STARTUP_PENDING
|
||||
|
||||
# 2. Lost communication with selfdriveState after receiving it
|
||||
if TICI and not waiting_for_startup:
|
||||
ss_missing = time.monotonic() - sm.recv_time['selfdriveState']
|
||||
if ss_missing > SELFDRIVE_STATE_TIMEOUT:
|
||||
if t.get("force_onroad", False):
|
||||
return None
|
||||
if ss.enabled and (ss_missing - SELFDRIVE_STATE_TIMEOUT) < SELFDRIVE_UNRESPONSIVE_TIMEOUT:
|
||||
return ALERT_CRITICAL_TIMEOUT
|
||||
return ALERT_CRITICAL_REBOOT
|
||||
@@ -107,12 +118,23 @@ class AlertRenderer(Widget):
|
||||
if ss.alertSize == 0:
|
||||
return None
|
||||
|
||||
# Hide non-critical alerts when FrogPilot toggle is on
|
||||
if t.get("hide_alerts", False) and ss.alertStatus == AlertStatus.normal:
|
||||
return None
|
||||
|
||||
# Don't get old alert
|
||||
if recv_frame < ui_state.started_frame:
|
||||
return None
|
||||
|
||||
# Return current alert
|
||||
return Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize.raw, status=ss.alertStatus.raw)
|
||||
alert_text1 = ss.alertText1
|
||||
alert_text2 = ss.alertText2
|
||||
|
||||
# FrogPilot: random events crash emoji
|
||||
if t.get("random_events", False) and "Crashed" in alert_text1:
|
||||
alert_text1 = "\U0001f4a9 " + alert_text1
|
||||
|
||||
return Alert(text1=alert_text1, text2=alert_text2, size=ss.alertSize.raw, status=ss.alertStatus.raw)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
alert = self.get_alert(ui_state.sm)
|
||||
@@ -123,10 +145,7 @@ class AlertRenderer(Widget):
|
||||
self._draw_background(alert_rect, alert)
|
||||
|
||||
text_rect = rl.Rectangle(
|
||||
alert_rect.x + ALERT_PADDING,
|
||||
alert_rect.y + ALERT_PADDING,
|
||||
alert_rect.width - 2 * ALERT_PADDING,
|
||||
alert_rect.height - 2 * ALERT_PADDING
|
||||
alert_rect.x + ALERT_PADDING, alert_rect.y + ALERT_PADDING, alert_rect.width - 2 * ALERT_PADDING, alert_rect.height - 2 * ALERT_PADDING
|
||||
)
|
||||
self._draw_text(text_rect, alert)
|
||||
|
||||
@@ -135,8 +154,7 @@ class AlertRenderer(Widget):
|
||||
return rect
|
||||
|
||||
h = ALERT_HEIGHTS.get(size, rect.height)
|
||||
return rl.Rectangle(rect.x + ALERT_MARGIN, rect.y + rect.height - h + ALERT_MARGIN,
|
||||
rect.width - ALERT_MARGIN * 2, h - ALERT_MARGIN * 2)
|
||||
return rl.Rectangle(rect.x + ALERT_MARGIN, rect.y + rect.height - h + ALERT_MARGIN, rect.width - ALERT_MARGIN * 2, h - ALERT_MARGIN * 2)
|
||||
|
||||
def _draw_background(self, rect: rl.Rectangle, alert: Alert) -> None:
|
||||
color = ALERT_COLORS.get(alert.status, ALERT_COLORS[AlertStatus.normal])
|
||||
|
||||
@@ -10,6 +10,8 @@ from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer
|
||||
from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer
|
||||
from openpilot.selfdrive.ui.onroad.model_renderer import ModelRenderer
|
||||
from openpilot.selfdrive.ui.onroad.cameraview import CameraView
|
||||
from openpilot.selfdrive.ui.onroad.frogpilot_overlay import FrogPilotOverlay
|
||||
from openpilot.selfdrive.ui.widgets.developer_sidebar import DeveloperSidebar
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCameraConfig, view_frame_from_device_frame
|
||||
from openpilot.common.transformations.orientation import rot_from_euler
|
||||
@@ -48,6 +50,8 @@ class AugmentedRoadView(CameraView):
|
||||
self._hud_renderer = HudRenderer()
|
||||
self.alert_renderer = AlertRenderer()
|
||||
self.driver_state_renderer = DriverStateRenderer()
|
||||
self._frogpilot_overlay = FrogPilotOverlay(self.model_renderer, self._hud_renderer, self.driver_state_renderer)
|
||||
self._developer_sidebar = DeveloperSidebar()
|
||||
|
||||
# debug
|
||||
self._pm = messaging.PubMaster(['uiDebug'])
|
||||
@@ -73,12 +77,7 @@ class AugmentedRoadView(CameraView):
|
||||
|
||||
# Enable scissor mode to clip all rendering within content rectangle boundaries
|
||||
# This creates a rendering viewport that prevents graphics from drawing outside the border
|
||||
rl.begin_scissor_mode(
|
||||
int(self._content_rect.x),
|
||||
int(self._content_rect.y),
|
||||
int(self._content_rect.width),
|
||||
int(self._content_rect.height)
|
||||
)
|
||||
rl.begin_scissor_mode(int(self._content_rect.x), int(self._content_rect.y), int(self._content_rect.width), int(self._content_rect.height))
|
||||
|
||||
# Render the base camera view
|
||||
super()._render(rect)
|
||||
@@ -89,8 +88,15 @@ class AugmentedRoadView(CameraView):
|
||||
self.alert_renderer.render(self._content_rect)
|
||||
self.driver_state_renderer.render(self._content_rect)
|
||||
|
||||
# Custom UI extension point - add custom overlays here
|
||||
# Use self._content_rect for positioning within camera bounds
|
||||
# StarPilot overlays (speed limits, compass, turn signals, etc.)
|
||||
self._frogpilot_overlay.render(self._content_rect)
|
||||
|
||||
# Developer sidebar (right-side metrics panel)
|
||||
t = ui_state.frogpilot_toggles
|
||||
if t.get("developer_sidebar", False):
|
||||
sb_w = 300
|
||||
sb_rect = rl.Rectangle(self._content_rect.x + self._content_rect.width - sb_w, self._content_rect.y, sb_w, self._content_rect.height)
|
||||
self._developer_sidebar.render(sb_rect)
|
||||
|
||||
# End clipping region
|
||||
rl.end_scissor_mode()
|
||||
@@ -109,14 +115,13 @@ class AugmentedRoadView(CameraView):
|
||||
|
||||
def _handle_mouse_release(self, _):
|
||||
# We only call click callback on press if not interacting with HUD
|
||||
pass
|
||||
self._frogpilot_overlay._handle_mouse_release(rl.get_mouse_position())
|
||||
|
||||
def _draw_border(self, rect: rl.Rectangle):
|
||||
rl.draw_rectangle_lines_ex(rect, UI_BORDER_SIZE, rl.BLACK)
|
||||
border_roundness = 0.12
|
||||
border_color = BORDER_COLORS.get(ui_state.status, BORDER_COLORS[UIStatus.DISENGAGED])
|
||||
border_rect = rl.Rectangle(rect.x + UI_BORDER_SIZE, rect.y + UI_BORDER_SIZE,
|
||||
rect.width - 2 * UI_BORDER_SIZE, rect.height - 2 * UI_BORDER_SIZE)
|
||||
border_rect = rl.Rectangle(rect.x + UI_BORDER_SIZE, rect.y + UI_BORDER_SIZE, rect.width - 2 * UI_BORDER_SIZE, rect.height - 2 * UI_BORDER_SIZE)
|
||||
rl.draw_rectangle_rounded_lines_ex(border_rect, border_roundness, 10, UI_BORDER_SIZE, border_color)
|
||||
|
||||
def _switch_stream_if_needed(self, sm):
|
||||
@@ -160,12 +165,7 @@ class AugmentedRoadView(CameraView):
|
||||
|
||||
def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray:
|
||||
# Check if we can use cached matrix
|
||||
cache_key = (
|
||||
ui_state.sm.recv_frame['liveCalibration'],
|
||||
self._content_rect.width,
|
||||
self._content_rect.height,
|
||||
self.stream_type
|
||||
)
|
||||
cache_key = (ui_state.sm.recv_frame['liveCalibration'], self._content_rect.width, self._content_rect.height, self.stream_type)
|
||||
if cache_key == self._matrix_cache_key and self._cached_matrix is not None:
|
||||
return self._cached_matrix
|
||||
|
||||
@@ -202,17 +202,9 @@ class AugmentedRoadView(CameraView):
|
||||
|
||||
# Cache the computed transformation matrix to avoid recalculations
|
||||
self._matrix_cache_key = cache_key
|
||||
self._cached_matrix = np.array([
|
||||
[zoom * 2 * cx / w, 0, -x_offset / w * 2],
|
||||
[0, zoom * 2 * cy / h, -y_offset / h * 2],
|
||||
[0, 0, 1.0]
|
||||
])
|
||||
self._cached_matrix = np.array([[zoom * 2 * cx / w, 0, -x_offset / w * 2], [0, zoom * 2 * cy / h, -y_offset / h * 2], [0, 0, 1.0]])
|
||||
|
||||
video_transform = np.array([
|
||||
[zoom, 0.0, (w / 2 + x - x_offset) - (cx * zoom)],
|
||||
[0.0, zoom, (h / 2 + y - y_offset) - (cy * zoom)],
|
||||
[0.0, 0.0, 1.0]
|
||||
])
|
||||
video_transform = np.array([[zoom, 0.0, (w / 2 + x - x_offset) - (cx * zoom)], [0.0, zoom, (h / 2 + y - y_offset) - (cy * zoom)], [0.0, 0.0, 1.0]])
|
||||
self.model_renderer.set_transform(video_transform @ calib_transform)
|
||||
|
||||
return self._cached_matrix
|
||||
|
||||
@@ -10,17 +10,44 @@ from openpilot.system.ui.widgets import Widget
|
||||
AlertSize = log.SelfdriveState.AlertSize
|
||||
|
||||
# Default 3D coordinates for face keypoints as a NumPy array
|
||||
DEFAULT_FACE_KPTS_3D = np.array([
|
||||
[-5.98, -51.20, 8.00], [-17.64, -49.14, 8.00], [-23.81, -46.40, 8.00], [-29.98, -40.91, 8.00],
|
||||
[-32.04, -37.49, 8.00], [-34.10, -32.00, 8.00], [-36.16, -21.03, 8.00], [-36.16, 6.40, 8.00],
|
||||
[-35.47, 10.51, 8.00], [-32.73, 19.43, 8.00], [-29.30, 26.29, 8.00], [-24.50, 33.83, 8.00],
|
||||
[-19.01, 41.37, 8.00], [-14.21, 46.17, 8.00], [-12.16, 47.54, 8.00], [-4.61, 49.60, 8.00],
|
||||
[4.99, 49.60, 8.00], [12.53, 47.54, 8.00], [14.59, 46.17, 8.00], [19.39, 41.37, 8.00],
|
||||
[24.87, 33.83, 8.00], [29.67, 26.29, 8.00], [33.10, 19.43, 8.00], [35.84, 10.51, 8.00],
|
||||
[36.53, 6.40, 8.00], [36.53, -21.03, 8.00], [34.47, -32.00, 8.00], [32.42, -37.49, 8.00],
|
||||
[30.36, -40.91, 8.00], [24.19, -46.40, 8.00], [18.02, -49.14, 8.00], [6.36, -51.20, 8.00],
|
||||
[-5.98, -51.20, 8.00],
|
||||
], dtype=np.float32)
|
||||
DEFAULT_FACE_KPTS_3D = np.array(
|
||||
[
|
||||
[-5.98, -51.20, 8.00],
|
||||
[-17.64, -49.14, 8.00],
|
||||
[-23.81, -46.40, 8.00],
|
||||
[-29.98, -40.91, 8.00],
|
||||
[-32.04, -37.49, 8.00],
|
||||
[-34.10, -32.00, 8.00],
|
||||
[-36.16, -21.03, 8.00],
|
||||
[-36.16, 6.40, 8.00],
|
||||
[-35.47, 10.51, 8.00],
|
||||
[-32.73, 19.43, 8.00],
|
||||
[-29.30, 26.29, 8.00],
|
||||
[-24.50, 33.83, 8.00],
|
||||
[-19.01, 41.37, 8.00],
|
||||
[-14.21, 46.17, 8.00],
|
||||
[-12.16, 47.54, 8.00],
|
||||
[-4.61, 49.60, 8.00],
|
||||
[4.99, 49.60, 8.00],
|
||||
[12.53, 47.54, 8.00],
|
||||
[14.59, 46.17, 8.00],
|
||||
[19.39, 41.37, 8.00],
|
||||
[24.87, 33.83, 8.00],
|
||||
[29.67, 26.29, 8.00],
|
||||
[33.10, 19.43, 8.00],
|
||||
[35.84, 10.51, 8.00],
|
||||
[36.53, 6.40, 8.00],
|
||||
[36.53, -21.03, 8.00],
|
||||
[34.47, -32.00, 8.00],
|
||||
[32.42, -37.49, 8.00],
|
||||
[30.36, -40.91, 8.00],
|
||||
[24.19, -46.40, 8.00],
|
||||
[18.02, -49.14, 8.00],
|
||||
[6.36, -51.20, 8.00],
|
||||
[-5.98, -51.20, 8.00],
|
||||
],
|
||||
dtype=np.float32,
|
||||
)
|
||||
|
||||
# UI constants
|
||||
BTN_SIZE = 192
|
||||
@@ -39,6 +66,7 @@ ARC_ANGLES = np.linspace(0.0, np.pi, ARC_POINT_COUNT, dtype=np.float32)
|
||||
@dataclass
|
||||
class ArcData:
|
||||
"""Data structure for arc rendering parameters."""
|
||||
|
||||
x: float
|
||||
y: float
|
||||
width: float
|
||||
@@ -78,8 +106,7 @@ class DriverStateRenderer(Widget):
|
||||
self.engaged_color = rl.Color(26, 242, 66, 255)
|
||||
self.disengaged_color = rl.Color(139, 139, 139, 255)
|
||||
|
||||
self.set_visible(lambda: (ui_state.sm["selfdriveState"].alertSize == AlertSize.none and
|
||||
ui_state.sm.recv_frame["driverStateV2"] > ui_state.started_frame))
|
||||
self.set_visible(lambda: ui_state.sm["selfdriveState"].alertSize == AlertSize.none and ui_state.sm.recv_frame["driverStateV2"] > ui_state.started_frame)
|
||||
|
||||
def _render(self, rect):
|
||||
# Set opacity based on active state
|
||||
@@ -106,6 +133,15 @@ class DriverStateRenderer(Widget):
|
||||
if self.v_arc_data:
|
||||
rl.draw_spline_linear(self.v_arc_lines, len(self.v_arc_lines), self.v_arc_data.thickness, self.arc_color)
|
||||
|
||||
@property
|
||||
def dm_icon_position(self) -> tuple[float, float]:
|
||||
"""Screen position of the driver monitoring icon center."""
|
||||
return (self.position_x, self.position_y)
|
||||
|
||||
@property
|
||||
def is_right_hand_drive(self) -> bool:
|
||||
return self.is_rhd
|
||||
|
||||
def _update_state(self):
|
||||
"""Update the driver monitoring state based on model data"""
|
||||
sm = ui_state.sm
|
||||
@@ -181,20 +217,16 @@ class DriverStateRenderer(Widget):
|
||||
# Horizontal arc
|
||||
h_width = abs(delta_x)
|
||||
self.h_arc_data = self._calculate_arc_data(
|
||||
delta_x, h_width, self.position_x, self.position_y - ARC_LENGTH / 2,
|
||||
self.driver_pose_sins[1], self.driver_pose_diff[1], is_horizontal=True
|
||||
delta_x, h_width, self.position_x, self.position_y - ARC_LENGTH / 2, self.driver_pose_sins[1], self.driver_pose_diff[1], is_horizontal=True
|
||||
)
|
||||
|
||||
# Vertical arc
|
||||
v_height = abs(delta_y)
|
||||
self.v_arc_data = self._calculate_arc_data(
|
||||
delta_y, v_height, self.position_x - ARC_LENGTH / 2, self.position_y,
|
||||
self.driver_pose_sins[0], self.driver_pose_diff[0], is_horizontal=False
|
||||
delta_y, v_height, self.position_x - ARC_LENGTH / 2, self.position_y, self.driver_pose_sins[0], self.driver_pose_diff[0], is_horizontal=False
|
||||
)
|
||||
|
||||
def _calculate_arc_data(
|
||||
self, delta: float, size: float, x: float, y: float, sin_val: float, diff_val: float, is_horizontal: bool
|
||||
):
|
||||
def _calculate_arc_data(self, delta: float, size: float, x: float, y: float, sin_val: float, diff_val: float, is_horizontal: bool):
|
||||
"""Calculate arc data and pre-compute arc points."""
|
||||
if size <= 0:
|
||||
return None
|
||||
|
||||
@@ -5,13 +5,20 @@ from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
# Status-aware background colors
|
||||
_BG_AOL = rl.Color(145, 155, 149, 204) # Always on lateral: grey-green
|
||||
_BG_CEM = rl.Color(0xDA, 0x6F, 0x25, 204) # Conditional experimental: orange
|
||||
_BG_TRAFFIC = rl.Color(0xC9, 0x22, 0x31, 204) # Traffic mode: red
|
||||
|
||||
|
||||
class ExpButton(Widget):
|
||||
def __init__(self, button_size: int, icon_size: int):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._params_memory = Params(memory=True)
|
||||
self._experimental_mode: bool = False
|
||||
self._engageable: bool = False
|
||||
self._icon_size = icon_size
|
||||
|
||||
# State hold mechanism
|
||||
self._hold_duration = 2.0 # seconds
|
||||
@@ -22,6 +29,11 @@ class ExpButton(Widget):
|
||||
self._black_bg: rl.Color = rl.Color(0, 0, 0, 166)
|
||||
self._txt_wheel: rl.Texture = gui_app.texture('icons/chffr_wheel.png', icon_size, icon_size)
|
||||
self._txt_exp: rl.Texture = gui_app.texture('icons/experimental.png', icon_size, icon_size)
|
||||
|
||||
# FrogPilot custom wheel (lazy loaded)
|
||||
self._custom_wheel: rl.Texture | None = None
|
||||
self._custom_wheel_loaded: bool = False
|
||||
|
||||
self._rect = rl.Rectangle(0, 0, button_size, button_size)
|
||||
|
||||
def set_rect(self, rect: rl.Rectangle) -> None:
|
||||
@@ -29,9 +41,22 @@ class ExpButton(Widget):
|
||||
|
||||
def _update_state(self) -> None:
|
||||
selfdrive_state = ui_state.sm["selfdriveState"]
|
||||
self._experimental_mode = selfdrive_state.experimentalMode
|
||||
self._toggles = ui_state.frogpilot_toggles
|
||||
|
||||
# Conditional experimental mode: use CEStatus instead of ExperimentalMode
|
||||
if self._toggles.get("conditional_experimental_mode", False):
|
||||
self._experimental_mode = self._params_memory.get_int("CEStatus", default=0) >= 2
|
||||
else:
|
||||
self._experimental_mode = selfdrive_state.experimentalMode
|
||||
|
||||
self._engageable = selfdrive_state.engageable or selfdrive_state.enabled
|
||||
|
||||
# FrogPilot: hot-reload custom wheel image
|
||||
if self._params_memory.get_bool("UpdateWheelImage"):
|
||||
self._custom_wheel = None
|
||||
self._custom_wheel_loaded = False
|
||||
self._params_memory.put_bool("UpdateWheelImage", False)
|
||||
|
||||
def _handle_mouse_release(self, _):
|
||||
super()._handle_mouse_release(_)
|
||||
if self._is_toggle_allowed():
|
||||
@@ -45,12 +70,57 @@ class ExpButton(Widget):
|
||||
def _render(self, rect: rl.Rectangle) -> None:
|
||||
center_x = int(self._rect.x + self._rect.width // 2)
|
||||
center_y = int(self._rect.y + self._rect.height // 2)
|
||||
t = self._toggles
|
||||
|
||||
self._white_color.a = 180 if self.is_pressed or not self._engageable else 255
|
||||
|
||||
texture = self._txt_exp if self._held_or_actual_mode() else self._txt_wheel
|
||||
rl.draw_circle(center_x, center_y, self._rect.width / 2, self._black_bg)
|
||||
rl.draw_texture(texture, center_x - texture.width // 2, center_y - texture.height // 2, self._white_color)
|
||||
# Status-aware background color
|
||||
bg = self._black_bg
|
||||
if ui_state.traffic_mode_enabled:
|
||||
bg = _BG_TRAFFIC
|
||||
elif ui_state.always_on_lateral_active:
|
||||
bg = _BG_AOL
|
||||
elif self._held_or_actual_mode():
|
||||
ce_status = self._params_memory.get_int("CEStatus", default=0) if t.get("conditional_experimental_mode", False) else 0
|
||||
if ce_status >= 2:
|
||||
bg = _BG_CEM
|
||||
|
||||
# Custom wheel image or stock
|
||||
if self._held_or_actual_mode():
|
||||
texture = self._txt_exp
|
||||
elif t.get("wheel_image", False):
|
||||
texture = self._get_custom_wheel()
|
||||
else:
|
||||
texture = self._txt_wheel
|
||||
|
||||
rl.draw_circle(center_x, center_y, self._rect.width / 2, bg)
|
||||
|
||||
# Rotating wheel support
|
||||
if t.get("rotating_wheel", False) and not self._held_or_actual_mode():
|
||||
self._draw_rotated_wheel(texture, center_x, center_y)
|
||||
else:
|
||||
rl.draw_texture(texture, center_x - texture.width // 2, center_y - texture.height // 2, self._white_color)
|
||||
|
||||
def _get_custom_wheel(self) -> rl.Texture:
|
||||
if not self._custom_wheel_loaded:
|
||||
self._custom_wheel_loaded = True
|
||||
try:
|
||||
self._custom_wheel = gui_app.starpilot_texture("active_theme/steering_wheel/wheel.png", self._icon_size, self._icon_size)
|
||||
except Exception:
|
||||
self._custom_wheel = None
|
||||
return self._custom_wheel if self._custom_wheel is not None else self._txt_wheel
|
||||
|
||||
def _draw_rotated_wheel(self, texture: rl.Texture, center_x: int, center_y: int) -> None:
|
||||
"""Draw the wheel texture rotated by steering angle."""
|
||||
car_state = ui_state.sm["carState"]
|
||||
angle = car_state.steeringAngleDeg
|
||||
# Clamp to reasonable range and normalize to texture rotation
|
||||
rotation = max(-90.0, min(90.0, angle))
|
||||
|
||||
source = rl.Rectangle(0, 0, texture.width, texture.height)
|
||||
dest = rl.Rectangle(center_x, center_y, texture.width, texture.height)
|
||||
origin = rl.Vector2(texture.width / 2, texture.height / 2)
|
||||
rl.draw_texture_pro(texture, source, dest, origin, rotation, self._white_color)
|
||||
|
||||
def _held_or_actual_mode(self):
|
||||
now = time.monotonic()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,170 @@
|
||||
import ctypes
|
||||
import time
|
||||
import pyray as rl
|
||||
from PIL import Image
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
from importlib.resources import files, as_file
|
||||
|
||||
|
||||
class GifPlayer:
|
||||
"""Plays a GIF animation as Raylib textures, one frame at a time."""
|
||||
|
||||
def __init__(self, gif_path: str, size: int = 0):
|
||||
self._frames: list[rl.Texture] = []
|
||||
self._frame_count: int = 0
|
||||
self._current_frame: int = 0
|
||||
self._frame_delay: float = 0.05 # seconds per frame
|
||||
self._last_tick: float = 0.0
|
||||
self._playing: bool = False
|
||||
self._loaded: bool = False
|
||||
self._gif_path = gif_path
|
||||
self._size = size
|
||||
|
||||
def _ensure_loaded(self):
|
||||
if self._loaded:
|
||||
return
|
||||
self._loaded = True
|
||||
|
||||
path = Path(self._gif_path)
|
||||
if not path.exists():
|
||||
return
|
||||
|
||||
try:
|
||||
img = Image.open(path)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
if getattr(img, "n_frames", 1) > 1:
|
||||
# GIF with multiple frames
|
||||
total_ms = 0
|
||||
for i in range(img.n_frames):
|
||||
img.seek(i)
|
||||
frame = img.convert("RGBA")
|
||||
if self._size > 0 and frame.size != (self._size, self._size):
|
||||
frame = frame.resize((self._size, self._size), Image.LANCZOS)
|
||||
self._frames.append(self._make_texture(frame))
|
||||
total_ms += img.info.get("duration", 50)
|
||||
self._frame_delay = max(0.02, total_ms / (img.n_frames * 1000.0))
|
||||
else:
|
||||
# Static image
|
||||
frame = img.convert("RGBA")
|
||||
if self._size > 0 and frame.size != (self._size, self._size):
|
||||
frame = frame.resize((self._size, self._size), Image.LANCZOS)
|
||||
self._frames.append(self._make_texture(frame))
|
||||
self._frame_delay = 0.05
|
||||
|
||||
self._frame_count = len(self._frames)
|
||||
|
||||
@staticmethod
|
||||
def _make_texture(pil_img: Image.Image) -> rl.Texture:
|
||||
raw = pil_img.tobytes("raw", "RGBA")
|
||||
data = (ctypes.c_uint8 * len(raw)).from_buffer_copy(raw)
|
||||
img = rl.Image(
|
||||
rl.ffi.from_buffer(data),
|
||||
pil_img.width,
|
||||
pil_img.height,
|
||||
1,
|
||||
rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
|
||||
)
|
||||
tex = rl.load_texture_from_image(img)
|
||||
rl.set_texture_filter(tex, rl.TextureFilter.TEXTURE_FILTER_BILINEAR)
|
||||
rl.set_texture_wrap(tex, rl.TextureWrap.TEXTURE_WRAP_CLAMP)
|
||||
return tex
|
||||
|
||||
def play(self):
|
||||
self._ensure_loaded()
|
||||
if self._frame_count > 1 and not self._playing:
|
||||
self._playing = True
|
||||
self._last_tick = time.monotonic()
|
||||
|
||||
def stop(self):
|
||||
self._playing = False
|
||||
self._current_frame = 0
|
||||
|
||||
def update(self):
|
||||
if not self._playing or self._frame_count <= 1:
|
||||
return
|
||||
now = time.monotonic()
|
||||
if now - self._last_tick >= self._frame_delay:
|
||||
self._current_frame = (self._current_frame + 1) % self._frame_count
|
||||
self._last_tick = now
|
||||
|
||||
def current_texture(self) -> Optional[rl.Texture]:
|
||||
self._ensure_loaded()
|
||||
if self._frame_count == 0:
|
||||
return None
|
||||
return self._frames[self._current_frame]
|
||||
|
||||
@property
|
||||
def texture(self) -> Optional[rl.Texture]:
|
||||
return self.current_texture()
|
||||
|
||||
@property
|
||||
def frame_count(self) -> int:
|
||||
self._ensure_loaded()
|
||||
return self._frame_count
|
||||
|
||||
def unload(self):
|
||||
for tex in self._frames:
|
||||
rl.unload_texture(tex)
|
||||
self._frames.clear()
|
||||
self._frame_count = 0
|
||||
|
||||
|
||||
class StaticTexture:
|
||||
"""Wraps a single PNG as a texture, matching GifPlayer interface."""
|
||||
|
||||
def __init__(self, path: str, size: int = 0):
|
||||
self._texture: Optional[rl.Texture] = None
|
||||
self._loaded = False
|
||||
self._path = path
|
||||
self._size = size
|
||||
|
||||
def _ensure_loaded(self):
|
||||
if self._loaded:
|
||||
return
|
||||
self._loaded = True
|
||||
path = Path(self._path)
|
||||
if not path.exists():
|
||||
return
|
||||
try:
|
||||
img = Image.open(path).convert("RGBA")
|
||||
if self._size > 0 and img.size != (self._size, self._size):
|
||||
img = img.resize((self._size, self._size), Image.LANCZOS)
|
||||
self._texture = GifPlayer._make_texture(img)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def current_texture(self) -> Optional[rl.Texture]:
|
||||
self._ensure_loaded()
|
||||
return self._texture
|
||||
|
||||
@property
|
||||
def texture(self) -> Optional[rl.Texture]:
|
||||
return self.current_texture()
|
||||
|
||||
def update(self):
|
||||
pass
|
||||
|
||||
def play(self):
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
def unload(self):
|
||||
if self._texture:
|
||||
rl.unload_texture(self._texture)
|
||||
self._texture = None
|
||||
|
||||
|
||||
def load_starpilot_asset(rel_path: str, size: int = 0) -> Union[GifPlayer, StaticTexture]:
|
||||
"""Load a GIF or PNG from frogpilot/assets/."""
|
||||
frogpilot_assets = files("openpilot.frogpilot").joinpath("assets")
|
||||
with as_file(frogpilot_assets.joinpath(rel_path)) as fspath:
|
||||
full_path = str(fspath)
|
||||
|
||||
if full_path.lower().endswith(".gif"):
|
||||
return GifPlayer(full_path, size)
|
||||
return StaticTexture(full_path, size)
|
||||
@@ -75,6 +75,8 @@ class HudRenderer(Widget):
|
||||
def _update_state(self) -> None:
|
||||
"""Update HUD state based on car state and controls state."""
|
||||
sm = ui_state.sm
|
||||
self._toggles = ui_state.frogpilot_toggles
|
||||
|
||||
if sm.recv_frame["carState"] < ui_state.started_frame:
|
||||
self.is_cruise_set = False
|
||||
self.set_speed = SET_SPEED_NA
|
||||
@@ -85,9 +87,7 @@ class HudRenderer(Widget):
|
||||
car_state = sm['carState']
|
||||
|
||||
v_cruise_cluster = car_state.vCruiseCluster
|
||||
self.set_speed = (
|
||||
controls_state.vCruiseDEPRECATED if v_cruise_cluster == 0.0 else v_cruise_cluster
|
||||
)
|
||||
self.set_speed = controls_state.vCruiseDEPRECATED if v_cruise_cluster == 0.0 else v_cruise_cluster
|
||||
self.is_cruise_set = 0 < self.set_speed < SET_SPEED_NA
|
||||
self.is_cruise_available = self.set_speed != -1
|
||||
|
||||
@@ -96,12 +96,17 @@ class HudRenderer(Widget):
|
||||
|
||||
v_ego_cluster = car_state.vEgoCluster
|
||||
self.v_ego_cluster_seen = self.v_ego_cluster_seen or v_ego_cluster != 0.0
|
||||
v_ego = v_ego_cluster if self.v_ego_cluster_seen else car_state.vEgo
|
||||
if self._toggles.get("use_wheel_speed", False):
|
||||
v_ego = car_state.vEgo
|
||||
else:
|
||||
v_ego = v_ego_cluster if self.v_ego_cluster_seen else car_state.vEgo
|
||||
speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
||||
self.speed = max(0.0, v_ego * speed_conversion)
|
||||
|
||||
def _render(self, rect: rl.Rectangle) -> None:
|
||||
"""Render HUD elements to the screen."""
|
||||
t = self._toggles
|
||||
|
||||
# Draw the header background
|
||||
rl.draw_rectangle_gradient_v(
|
||||
int(rect.x),
|
||||
@@ -112,10 +117,11 @@ class HudRenderer(Widget):
|
||||
COLORS.HEADER_GRADIENT_END,
|
||||
)
|
||||
|
||||
if self.is_cruise_available:
|
||||
if self.is_cruise_available and not t.get("hide_max_speed", False):
|
||||
self._draw_set_speed(rect)
|
||||
|
||||
self._draw_current_speed(rect)
|
||||
if not t.get("hide_speed", False):
|
||||
self._draw_current_speed(rect)
|
||||
|
||||
button_x = rect.x + rect.width - UI_CONFIG.border_size - UI_CONFIG.button_size
|
||||
button_y = rect.y + UI_CONFIG.border_size
|
||||
@@ -124,6 +130,20 @@ class HudRenderer(Widget):
|
||||
def user_interacting(self) -> bool:
|
||||
return self._exp_button.is_pressed
|
||||
|
||||
@property
|
||||
def set_speed_rect(self) -> rl.Rectangle:
|
||||
"""The rect of the MAX speed indicator box."""
|
||||
set_speed_width = UI_CONFIG.set_speed_width_metric if ui_state.is_metric else UI_CONFIG.set_speed_width_imperial
|
||||
x = 60 + (UI_CONFIG.set_speed_width_imperial - set_speed_width) // 2
|
||||
return rl.Rectangle(x, 45, set_speed_width, UI_CONFIG.set_speed_height)
|
||||
|
||||
@property
|
||||
def exp_button_rect(self) -> rl.Rectangle:
|
||||
"""The rect of the exp button (relative to content rect)."""
|
||||
btn_x = self._rect.width - UI_CONFIG.border_size - UI_CONFIG.button_size
|
||||
btn_y = UI_CONFIG.border_size
|
||||
return rl.Rectangle(btn_x, btn_y, UI_CONFIG.button_size, UI_CONFIG.button_size)
|
||||
|
||||
def _draw_set_speed(self, rect: rl.Rectangle) -> None:
|
||||
"""Draw the MAX speed indicator box."""
|
||||
set_speed_width = UI_CONFIG.set_speed_width_metric if ui_state.is_metric else UI_CONFIG.set_speed_width_imperial
|
||||
|
||||
@@ -16,15 +16,15 @@ MIN_DRAW_DISTANCE = 10.0
|
||||
MAX_DRAW_DISTANCE = 100.0
|
||||
|
||||
THROTTLE_COLORS = [
|
||||
rl.Color(13, 248, 122, 102), # HSLF(148/360, 0.94, 0.51, 0.4)
|
||||
rl.Color(114, 255, 92, 89), # HSLF(112/360, 1.0, 0.68, 0.35)
|
||||
rl.Color(114, 255, 92, 0), # HSLF(112/360, 1.0, 0.68, 0.0)
|
||||
rl.Color(13, 248, 122, 102), # HSLF(148/360, 0.94, 0.51, 0.4)
|
||||
rl.Color(114, 255, 92, 89), # HSLF(112/360, 1.0, 0.68, 0.35)
|
||||
rl.Color(114, 255, 92, 0), # HSLF(112/360, 1.0, 0.68, 0.0)
|
||||
]
|
||||
|
||||
NO_THROTTLE_COLORS = [
|
||||
rl.Color(242, 242, 242, 102), # HSLF(148/360, 0.0, 0.95, 0.4)
|
||||
rl.Color(242, 242, 242, 102), # HSLF(148/360, 0.0, 0.95, 0.4)
|
||||
rl.Color(242, 242, 242, 89), # HSLF(112/360, 0.0, 0.95, 0.35)
|
||||
rl.Color(242, 242, 242, 0), # HSLF(112/360, 0.0, 0.95, 0.0)
|
||||
rl.Color(242, 242, 242, 0), # HSLF(112/360, 0.0, 0.95, 0.0)
|
||||
]
|
||||
|
||||
|
||||
@@ -50,11 +50,15 @@ class ModelRenderer(Widget):
|
||||
self._prev_allow_throttle = True
|
||||
self._lane_line_probs = np.zeros(4, dtype=np.float32)
|
||||
self._road_edge_stds = np.zeros(2, dtype=np.float32)
|
||||
self._lead_vehicles = [LeadVehicle(), LeadVehicle()]
|
||||
self._lead_vehicles = [LeadVehicle(), LeadVehicle(), LeadVehicle(), LeadVehicle()]
|
||||
self._adjacent_lead_count = 0
|
||||
self._path_offset_z = HEIGHT_INIT[0]
|
||||
|
||||
# Initialize ModelPoints objects
|
||||
self._path = ModelPoints()
|
||||
self._path_edge = ModelPoints()
|
||||
self._adjacent_left = ModelPoints()
|
||||
self._adjacent_right = ModelPoints()
|
||||
self._lane_lines = [ModelPoints() for _ in range(4)]
|
||||
self._road_edges = [ModelPoints() for _ in range(2)]
|
||||
self._acceleration_x = np.empty((0,), dtype=np.float32)
|
||||
@@ -63,6 +67,7 @@ class ModelRenderer(Widget):
|
||||
self._car_space_transform = np.zeros((3, 3), dtype=np.float32)
|
||||
self._transform_dirty = True
|
||||
self._clip_region = None
|
||||
self._lead_data_raw = [None, None]
|
||||
|
||||
self._exp_gradient = Gradient(
|
||||
start=(0.0, 1.0), # Bottom of path
|
||||
@@ -76,25 +81,85 @@ class ModelRenderer(Widget):
|
||||
cp = messaging.log_from_bytes(car_params, car.CarParams)
|
||||
self._longitudinal_control = cp.openpilotLongitudinalControl
|
||||
|
||||
# FrogPilot state
|
||||
self._toggles: dict = {}
|
||||
self._model_ui: bool = False
|
||||
self._accel_path_active: bool = False
|
||||
|
||||
# Gradient caches (avoids per-frame object allocation)
|
||||
self._rainbow_cache: Gradient | None = None
|
||||
self._rainbow_speed_key: float = -1.0
|
||||
self._blend_gradient_cache: Gradient | None = None
|
||||
self._blend_factor_cache: float = -1.0
|
||||
|
||||
def set_transform(self, transform: np.ndarray):
|
||||
self._car_space_transform = transform.astype(np.float32)
|
||||
self._transform_dirty = True
|
||||
|
||||
@property
|
||||
def car_space_transform(self) -> np.ndarray:
|
||||
return self._car_space_transform
|
||||
|
||||
def project_point(self, x: float, y: float, z: float) -> tuple[float, float] | None:
|
||||
"""Project a car-space (x, y, z) point to screen coordinates."""
|
||||
return self._map_to_screen(x, y, z)
|
||||
|
||||
@property
|
||||
def path_points(self) -> np.ndarray:
|
||||
"""Projected screen-space path polygon points."""
|
||||
return self._path.projected_points
|
||||
|
||||
@property
|
||||
def path_raw_points(self) -> np.ndarray:
|
||||
"""Raw car-space path points (Nx3)."""
|
||||
return self._path.raw_points
|
||||
|
||||
@property
|
||||
def road_edge_points(self) -> list[np.ndarray]:
|
||||
"""Projected screen-space road edge polygons."""
|
||||
return [e.projected_points for e in self._road_edges]
|
||||
|
||||
@property
|
||||
def lane_line_points(self) -> list[np.ndarray]:
|
||||
"""Projected screen-space lane line polygons."""
|
||||
return [l.projected_points for l in self._lane_lines]
|
||||
|
||||
@property
|
||||
def adjacent_left_points(self) -> np.ndarray:
|
||||
"""Projected left adjacent lane polygon."""
|
||||
return self._adjacent_left.projected_points
|
||||
|
||||
@property
|
||||
def adjacent_right_points(self) -> np.ndarray:
|
||||
"""Projected right adjacent lane polygon."""
|
||||
return self._adjacent_right.projected_points
|
||||
|
||||
@property
|
||||
def lead_vehicles(self) -> list[LeadVehicle]:
|
||||
return self._lead_vehicles
|
||||
|
||||
@property
|
||||
def lead_data(self) -> list:
|
||||
"""Raw radarState lead data (from last render)."""
|
||||
return self._lead_data_raw
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
sm = ui_state.sm
|
||||
t = ui_state.frogpilot_toggles
|
||||
self._toggles = t
|
||||
self._model_ui = t.get("model_ui", False)
|
||||
|
||||
# Check if data is up-to-date
|
||||
if (sm.recv_frame["liveCalibration"] < ui_state.started_frame or
|
||||
sm.recv_frame["modelV2"] < ui_state.started_frame):
|
||||
if sm.recv_frame["liveCalibration"] < ui_state.started_frame or sm.recv_frame["modelV2"] < ui_state.started_frame:
|
||||
return
|
||||
|
||||
# Set up clipping region
|
||||
self._clip_region = rl.Rectangle(
|
||||
rect.x - CLIP_MARGIN, rect.y - CLIP_MARGIN, rect.width + 2 * CLIP_MARGIN, rect.height + 2 * CLIP_MARGIN
|
||||
)
|
||||
self._clip_region = rl.Rectangle(rect.x - CLIP_MARGIN, rect.y - CLIP_MARGIN, rect.width + 2 * CLIP_MARGIN, rect.height + 2 * CLIP_MARGIN)
|
||||
|
||||
# Update state
|
||||
self._experimental_mode = sm['selfdriveState'].experimentalMode
|
||||
# Acceleration path: allow acceleration coloring independent of exp mode
|
||||
self._accel_path_active = self._experimental_mode or t.get("acceleration_path", False)
|
||||
|
||||
live_calib = sm['liveCalibration']
|
||||
self._path_offset_z = live_calib.height[0] if live_calib.height else HEIGHT_INIT[0]
|
||||
@@ -107,6 +172,10 @@ class ModelRenderer(Widget):
|
||||
lead_one = radar_state.leadOne if radar_state else None
|
||||
render_lead_indicator = self._longitudinal_control and radar_state is not None
|
||||
|
||||
# Hide lead marker toggle
|
||||
if t.get("hide_lead_marker", False):
|
||||
render_lead_indicator = False
|
||||
|
||||
# Update model data when needed
|
||||
model_updated = sm.updated['modelV2']
|
||||
if model_updated or sm.updated['radarState'] or self._transform_dirty:
|
||||
@@ -137,30 +206,43 @@ class ModelRenderer(Widget):
|
||||
# the fixed number of lane/edge slots used by the UI.
|
||||
for lane_line in self._lane_lines:
|
||||
lane_line.raw_points = np.empty((0, 3), dtype=np.float32)
|
||||
for i, lane_line in enumerate(model.laneLines[:len(self._lane_lines)]):
|
||||
for i, lane_line in enumerate(model.laneLines[: len(self._lane_lines)]):
|
||||
self._lane_lines[i].raw_points = np.array([lane_line.x, lane_line.y, lane_line.z], dtype=np.float32).T
|
||||
|
||||
for road_edge in self._road_edges:
|
||||
road_edge.raw_points = np.empty((0, 3), dtype=np.float32)
|
||||
for i, road_edge in enumerate(model.roadEdges[:len(self._road_edges)]):
|
||||
for i, road_edge in enumerate(model.roadEdges[: len(self._road_edges)]):
|
||||
self._road_edges[i].raw_points = np.array([road_edge.x, road_edge.y, road_edge.z], dtype=np.float32).T
|
||||
|
||||
lane_line_probs = np.array(model.laneLineProbs, dtype=np.float32)
|
||||
self._lane_line_probs = np.zeros(len(self._lane_lines), dtype=np.float32)
|
||||
self._lane_line_probs[:min(len(self._lane_lines), len(lane_line_probs))] = lane_line_probs[:len(self._lane_lines)]
|
||||
self._lane_line_probs[: min(len(self._lane_lines), len(lane_line_probs))] = lane_line_probs[: len(self._lane_lines)]
|
||||
|
||||
road_edge_stds = np.array(model.roadEdgeStds, dtype=np.float32)
|
||||
self._road_edge_stds = np.ones(len(self._road_edges), dtype=np.float32)
|
||||
self._road_edge_stds[:min(len(self._road_edges), len(road_edge_stds))] = road_edge_stds[:len(self._road_edges)]
|
||||
self._road_edge_stds[: min(len(self._road_edges), len(road_edge_stds))] = road_edge_stds[: len(self._road_edges)]
|
||||
self._acceleration_x = np.array(model.acceleration.x, dtype=np.float32)
|
||||
|
||||
def _update_leads(self, radar_state, path_x_array):
|
||||
"""Update positions of lead vehicles"""
|
||||
self._lead_vehicles = [LeadVehicle(), LeadVehicle()]
|
||||
for lv in self._lead_vehicles:
|
||||
lv.glow = []
|
||||
lv.chevron = []
|
||||
lv.fill_alpha = 0
|
||||
self._adjacent_lead_count = 0
|
||||
self._lead_data_raw = [None, None]
|
||||
leads = [radar_state.leadOne, radar_state.leadTwo]
|
||||
|
||||
# FrogPilot: lead detection probability threshold
|
||||
t = self._toggles
|
||||
prob_threshold = t.get("lead_detection_probability", 0.0) if self._model_ui else 0.0
|
||||
|
||||
for i, lead_data in enumerate(leads):
|
||||
if lead_data and lead_data.status:
|
||||
# Apply lead detection probability threshold
|
||||
if prob_threshold > 0 and hasattr(lead_data, 'prob') and lead_data.prob < prob_threshold:
|
||||
continue
|
||||
self._lead_data_raw[i] = lead_data
|
||||
d_rel, y_rel, v_rel = lead_data.dRel, lead_data.yRel, lead_data.vRel
|
||||
idx = self._get_path_length_idx(path_x_array, d_rel)
|
||||
|
||||
@@ -170,20 +252,48 @@ class ModelRenderer(Widget):
|
||||
if point:
|
||||
self._lead_vehicles[i] = self._update_lead_vehicle(d_rel, v_rel, point, self._rect)
|
||||
|
||||
# FrogPilot: adjacent lead tracking from radar
|
||||
if t.get("adjacent_leads_ui", False) and ui_state.sm.valid.get("frogpilotRadarState", False):
|
||||
fp_radar = ui_state.sm["frogpilotRadarState"]
|
||||
adj_idx = 2 # slots 2 and 3 are for adjacent leads
|
||||
for adj_lead in [fp_radar.leadLeft, fp_radar.leadRight]:
|
||||
if adj_lead and adj_lead.status:
|
||||
d_rel, y_rel, v_rel = adj_lead.dRel, adj_lead.yRel, adj_lead.vRel
|
||||
idx = self._get_path_length_idx(path_x_array, d_rel)
|
||||
z = self._path.raw_points[idx, 2] if idx < len(self._path.raw_points) else 0.0
|
||||
point = self._map_to_screen(d_rel, -y_rel, z + self._path_offset_z)
|
||||
if point and adj_idx < len(self._lead_vehicles):
|
||||
self._lead_vehicles[adj_idx] = self._update_lead_vehicle(d_rel, v_rel, point, self._rect)
|
||||
self._adjacent_lead_count += 1
|
||||
adj_idx += 1
|
||||
|
||||
def _update_model(self, lead, path_x_array):
|
||||
"""Update model visualization data based on model message"""
|
||||
t = self._toggles
|
||||
model_ui = self._model_ui
|
||||
|
||||
# FrogPilot custom widths
|
||||
lane_width = t.get("lane_line_width", 0.025) if model_ui else 0.025
|
||||
edge_width = t.get("road_edge_width", 0.025) if model_ui else 0.025
|
||||
path_width = t.get("path_width", 0.9) if model_ui else 0.9
|
||||
|
||||
# Dynamic path width: scale by engagement
|
||||
if model_ui and t.get("dynamic_path_width", False):
|
||||
if ui_state.always_on_lateral_active:
|
||||
path_width *= 0.75
|
||||
elif not ui_state.sm["selfdriveState"].enabled:
|
||||
path_width *= 0.5
|
||||
|
||||
max_distance = np.clip(path_x_array[-1], MIN_DRAW_DISTANCE, MAX_DRAW_DISTANCE)
|
||||
max_idx = self._get_path_length_idx(self._lane_lines[0].raw_points[:, 0], max_distance)
|
||||
|
||||
# Update lane lines using raw points
|
||||
for i, lane_line in enumerate(self._lane_lines):
|
||||
lane_line.projected_points = self._map_line_to_polygon(
|
||||
lane_line.raw_points, 0.025 * self._lane_line_probs[i], 0.0, max_idx, max_distance
|
||||
)
|
||||
lane_line.projected_points = self._map_line_to_polygon(lane_line.raw_points, lane_width * self._lane_line_probs[i], 0.0, max_idx, max_distance)
|
||||
|
||||
# Update road edges using raw points
|
||||
for road_edge in self._road_edges:
|
||||
road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, 0.025, 0.0, max_idx, max_distance)
|
||||
road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, edge_width, 0.0, max_idx, max_distance)
|
||||
|
||||
# Update path using raw points
|
||||
if lead and lead.status:
|
||||
@@ -191,9 +301,32 @@ class ModelRenderer(Widget):
|
||||
max_distance = np.clip(lead_d - min(lead_d * 0.35, 10.0), 0.0, max_distance)
|
||||
|
||||
max_idx = self._get_path_length_idx(path_x_array, max_distance)
|
||||
self._path.projected_points = self._map_line_to_polygon(
|
||||
self._path.raw_points, 0.9, self._path_offset_z, max_idx, max_distance, allow_invert=False
|
||||
)
|
||||
self._path.projected_points = self._map_line_to_polygon(self._path.raw_points, path_width, self._path_offset_z, max_idx, max_distance, allow_invert=False)
|
||||
|
||||
# FrogPilot: path edge polygon (full-width outer polygon for edge rendering)
|
||||
if model_ui and t.get("path_edge_width", 0) > 0:
|
||||
self._path_edge.projected_points = self._map_line_to_polygon(self._path.raw_points, 0.9, self._path_offset_z, max_idx, max_distance, allow_invert=False)
|
||||
else:
|
||||
self._path_edge.projected_points = np.empty((0, 2), dtype=np.float32)
|
||||
|
||||
# FrogPilot: adjacent lane polygons (averaged from lane line pairs)
|
||||
if t.get("adjacent_paths", False) or t.get("adjacent_path_metrics", False) or t.get("blind_spot_path", False):
|
||||
fp_plan = ui_state.frogpilot_plan
|
||||
if fp_plan and len(self._lane_lines) >= 4:
|
||||
lane_w_left = fp_plan.laneWidthLeft / 2.0 if fp_plan.laneWidthLeft else 1.75
|
||||
lane_w_right = fp_plan.laneWidthRight / 2.0 if fp_plan.laneWidthRight else 1.75
|
||||
self._adjacent_left.projected_points = self._map_averaged_line_to_polygon(
|
||||
self._lane_lines[0].raw_points, self._lane_lines[1].raw_points, lane_w_left, 0.0, max_idx
|
||||
)
|
||||
self._adjacent_right.projected_points = self._map_averaged_line_to_polygon(
|
||||
self._lane_lines[2].raw_points, self._lane_lines[3].raw_points, lane_w_right, 0.0, max_idx
|
||||
)
|
||||
else:
|
||||
self._adjacent_left.projected_points = np.empty((0, 2), dtype=np.float32)
|
||||
self._adjacent_right.projected_points = np.empty((0, 2), dtype=np.float32)
|
||||
else:
|
||||
self._adjacent_left.projected_points = np.empty((0, 2), dtype=np.float32)
|
||||
self._adjacent_right.projected_points = np.empty((0, 2), dtype=np.float32)
|
||||
|
||||
self._update_experimental_gradient()
|
||||
|
||||
@@ -242,6 +375,30 @@ class ModelRenderer(Widget):
|
||||
stops=gradient_stops,
|
||||
)
|
||||
|
||||
def _build_rainbow_gradient(self) -> Gradient:
|
||||
"""Build a rainbow gradient for the path based on current speed (cached)."""
|
||||
speed = abs(ui_state.sm["carState"].vEgo)
|
||||
speed_key = round(speed, 1)
|
||||
if speed_key == self._rainbow_speed_key and self._rainbow_cache is not None:
|
||||
return self._rainbow_cache
|
||||
|
||||
self._rainbow_speed_key = speed_key
|
||||
t = min(speed / 35.0, 1.0) # ~80 mph max
|
||||
|
||||
n_stops = 5
|
||||
colors = []
|
||||
stops = []
|
||||
for i in range(n_stops):
|
||||
frac = i / (n_stops - 1)
|
||||
hue = (frac * 0.8 + t * 0.2) % 1.0
|
||||
r, g, b = colorsys.hls_to_rgb(hue, 0.5, 1.0)
|
||||
alpha = int(80 * (1.0 - frac * 0.5))
|
||||
colors.append(rl.Color(int(r * 255), int(g * 255), int(b * 255), alpha))
|
||||
stops.append(frac)
|
||||
|
||||
self._rainbow_cache = Gradient(start=(0.0, 1.0), end=(0.0, 0.0), colors=colors, stops=stops)
|
||||
return self._rainbow_cache
|
||||
|
||||
def _update_lead_vehicle(self, d_rel, v_rel, point, rect):
|
||||
speed_buff, lead_buff = 10.0, 40.0
|
||||
|
||||
@@ -268,12 +425,24 @@ class ModelRenderer(Widget):
|
||||
|
||||
def _draw_lane_lines(self):
|
||||
"""Draw lane lines and road edges"""
|
||||
t = self._toggles
|
||||
model_ui = self._model_ui
|
||||
custom_scheme = model_ui and t.get("color_scheme", "stock") != "stock"
|
||||
|
||||
# Custom lane line color
|
||||
lane_color_override = None
|
||||
if custom_scheme and t.get("lane_lines_color"):
|
||||
lane_color_override = self._parse_color(t["lane_lines_color"])
|
||||
|
||||
for i, lane_line in enumerate(self._lane_lines):
|
||||
if lane_line.projected_points.size == 0:
|
||||
continue
|
||||
|
||||
alpha = np.clip(self._lane_line_probs[i], 0.0, 0.7)
|
||||
color = rl.Color(255, 255, 255, int(alpha * 255))
|
||||
if lane_color_override:
|
||||
color = rl.Color(lane_color_override.r, lane_color_override.g, lane_color_override.b, int(alpha * 255))
|
||||
else:
|
||||
color = rl.Color(255, 255, 255, int(alpha * 255))
|
||||
draw_polygon(self._rect, lane_line.projected_points, color)
|
||||
|
||||
for i, road_edge in enumerate(self._road_edges):
|
||||
@@ -289,11 +458,45 @@ class ModelRenderer(Widget):
|
||||
if not self._path.projected_points.size:
|
||||
return
|
||||
|
||||
t = self._toggles
|
||||
model_ui = self._model_ui
|
||||
custom_scheme = model_ui and t.get("color_scheme", "stock") != "stock"
|
||||
|
||||
# Custom path color (applied when acceleration is low)
|
||||
path_color_override = None
|
||||
if custom_scheme and t.get("path_color"):
|
||||
path_color_override = self._parse_color(t["path_color"])
|
||||
|
||||
# FrogPilot: draw path edge polygon first (status-colored outer layer)
|
||||
edge_pts = self._path_edge.projected_points
|
||||
if edge_pts.size > 0:
|
||||
cs = ui_state.conditional_status
|
||||
# Alpha tuned for Raylib shader polygon renderer (C++ uses 241 via QPainter)
|
||||
_EDGE_ALPHA = 120
|
||||
if ui_state.always_on_lateral_active:
|
||||
edge_color = rl.Color(0x0A, 0xBA, 0xB5, _EDGE_ALPHA)
|
||||
elif cs >= 2:
|
||||
edge_color = rl.Color(0xDA, 0x6F, 0x25, _EDGE_ALPHA)
|
||||
elif cs >= 1:
|
||||
edge_color = rl.Color(0xFF, 0xFF, 0x00, _EDGE_ALPHA)
|
||||
elif ui_state.traffic_mode_enabled:
|
||||
edge_color = rl.Color(0xC9, 0x22, 0x31, _EDGE_ALPHA)
|
||||
else:
|
||||
edge_color = rl.Color(255, 255, 255, 30)
|
||||
if custom_scheme and t.get("path_edges_color"):
|
||||
edge_color = self._parse_color(t["path_edges_color"])
|
||||
draw_polygon(self._rect, edge_pts, edge_color)
|
||||
|
||||
allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control
|
||||
self._blend_filter.update(int(allow_throttle))
|
||||
|
||||
if self._experimental_mode:
|
||||
# Draw with acceleration coloring
|
||||
# FrogPilot: rainbow path (speed-based HSL gradient, integrated into path rendering)
|
||||
if t.get("rainbow_path", False) and not t.get("acceleration_path", False):
|
||||
rainbow = self._build_rainbow_gradient()
|
||||
draw_polygon(self._rect, self._path.projected_points, gradient=rainbow)
|
||||
|
||||
elif self._accel_path_active:
|
||||
# Draw with acceleration coloring (experimental or acceleration_path toggle)
|
||||
if len(self._exp_gradient.colors) > 1:
|
||||
draw_polygon(self._rect, self._path.projected_points, gradient=self._exp_gradient)
|
||||
else:
|
||||
@@ -301,23 +504,51 @@ class ModelRenderer(Widget):
|
||||
else:
|
||||
# Blend throttle/no throttle colors based on transition
|
||||
blend_factor = round(self._blend_filter.x * 100) / 100
|
||||
blended_colors = self._blend_colors(NO_THROTTLE_COLORS, THROTTLE_COLORS, blend_factor)
|
||||
gradient = Gradient(
|
||||
start=(0.0, 1.0), # Bottom of path
|
||||
end=(0.0, 0.0), # Top of path
|
||||
colors=blended_colors,
|
||||
stops=[0.0, 0.5, 1.0],
|
||||
)
|
||||
draw_polygon(self._rect, self._path.projected_points, gradient=gradient)
|
||||
|
||||
# Custom path color override: use when not accelerating
|
||||
if path_color_override and blend_factor < 0.5:
|
||||
c = path_color_override
|
||||
draw_polygon(self._rect, self._path.projected_points, rl.Color(c.r, c.g, c.b, 102))
|
||||
else:
|
||||
if blend_factor != self._blend_factor_cache:
|
||||
self._blend_factor_cache = blend_factor
|
||||
blended_colors = self._blend_colors(NO_THROTTLE_COLORS, THROTTLE_COLORS, blend_factor)
|
||||
self._blend_gradient_cache = Gradient(
|
||||
start=(0.0, 1.0),
|
||||
end=(0.0, 0.0),
|
||||
colors=blended_colors,
|
||||
stops=[0.0, 0.5, 1.0],
|
||||
)
|
||||
draw_polygon(self._rect, self._path.projected_points, gradient=self._blend_gradient_cache)
|
||||
|
||||
# FrogPilot: adjacent lane paths (green-to-red based on width)
|
||||
t = self._toggles
|
||||
if t.get("adjacent_paths", False):
|
||||
for pts in [self._adjacent_left.projected_points, self._adjacent_right.projected_points]:
|
||||
if pts.size > 0:
|
||||
draw_polygon(self._rect, pts, rl.Color(0, 255, 100, 50))
|
||||
if t.get("blind_spot_path", False):
|
||||
for pts in [self._adjacent_left.projected_points, self._adjacent_right.projected_points]:
|
||||
if pts.size > 0:
|
||||
draw_polygon(self._rect, pts, rl.Color(201, 34, 49, 80))
|
||||
|
||||
def _draw_lead_indicator(self):
|
||||
t = self._toggles
|
||||
# Custom lead marker color
|
||||
glow_color = rl.Color(218, 202, 37, 255)
|
||||
chevron_color_base = rl.Color(201, 34, 49, 255)
|
||||
if self._model_ui and t.get("lead_marker_color"):
|
||||
custom = self._parse_color(t["lead_marker_color"])
|
||||
glow_color = rl.Color(custom.r, custom.g, custom.b, 255)
|
||||
chevron_color_base = rl.Color(custom.r, custom.g, custom.b, 255)
|
||||
|
||||
# Draw lead vehicles if available
|
||||
for lead in self._lead_vehicles:
|
||||
if not lead.glow or not lead.chevron:
|
||||
continue
|
||||
|
||||
rl.draw_triangle_fan(lead.glow, len(lead.glow), rl.Color(218, 202, 37, 255))
|
||||
rl.draw_triangle_fan(lead.chevron, len(lead.chevron), rl.Color(201, 34, 49, lead.fill_alpha))
|
||||
rl.draw_triangle_fan(lead.glow, len(lead.glow), glow_color)
|
||||
rl.draw_triangle_fan(lead.chevron, len(lead.chevron), rl.Color(chevron_color_base.r, chevron_color_base.g, chevron_color_base.b, lead.fill_alpha))
|
||||
|
||||
@staticmethod
|
||||
def _get_path_length_idx(pos_x_array: np.ndarray, path_distance: float) -> int:
|
||||
@@ -329,6 +560,9 @@ class ModelRenderer(Widget):
|
||||
|
||||
def _map_to_screen(self, in_x, in_y, in_z):
|
||||
"""Project a point in car space to screen space"""
|
||||
if self._clip_region is None:
|
||||
return None
|
||||
|
||||
input_pt = np.array([in_x, in_y, in_z])
|
||||
pt = self._car_space_transform @ input_pt
|
||||
|
||||
@@ -349,7 +583,7 @@ class ModelRenderer(Widget):
|
||||
return np.empty((0, 2), dtype=np.float32)
|
||||
|
||||
# Slice points and filter non-negative x-coordinates
|
||||
points = line[:max_idx + 1]
|
||||
points = line[: max_idx + 1]
|
||||
|
||||
# Interpolate around max_idx so path end is smooth (max_distance is always >= p0.x)
|
||||
if 0 < max_idx < line.shape[0] - 1:
|
||||
@@ -392,14 +626,8 @@ class ModelRenderer(Widget):
|
||||
y_min, y_max = clip.y, clip.y + clip.height
|
||||
|
||||
# Filter points within clip region
|
||||
left_in_clip = (
|
||||
(left_screen[0] >= x_min) & (left_screen[0] <= x_max) &
|
||||
(left_screen[1] >= y_min) & (left_screen[1] <= y_max)
|
||||
)
|
||||
right_in_clip = (
|
||||
(right_screen[0] >= x_min) & (right_screen[0] <= x_max) &
|
||||
(right_screen[1] >= y_min) & (right_screen[1] <= y_max)
|
||||
)
|
||||
left_in_clip = (left_screen[0] >= x_min) & (left_screen[0] <= x_max) & (left_screen[1] >= y_min) & (left_screen[1] <= y_max)
|
||||
right_in_clip = (right_screen[0] >= x_min) & (right_screen[0] <= x_max) & (right_screen[1] >= y_min) & (right_screen[1] <= y_max)
|
||||
both_in_clip = left_in_clip & right_in_clip
|
||||
|
||||
if not np.any(both_in_clip):
|
||||
@@ -420,15 +648,92 @@ class ModelRenderer(Widget):
|
||||
|
||||
return np.vstack((left_screen.T, right_screen[:, ::-1].T)).astype(np.float32)
|
||||
|
||||
def _map_averaged_line_to_polygon(self, line1: np.ndarray, line2: np.ndarray, y_off: float, z_off: float, max_idx: int) -> np.ndarray:
|
||||
"""Convert two averaged 3D lane lines to a 2D polygon (center-of-lane path).
|
||||
|
||||
Averages the Y coordinates of line1 and line2, then creates left/right points
|
||||
at avg_y -/+ y_off. Uses X and Z from line1.
|
||||
"""
|
||||
if line1.shape[0] == 0 or line2.shape[0] == 0:
|
||||
return np.empty((0, 2), dtype=np.float32)
|
||||
|
||||
n = min(max_idx + 1, line1.shape[0], line2.shape[0])
|
||||
if n == 0:
|
||||
return np.empty((0, 2), dtype=np.float32)
|
||||
|
||||
# Build averaged points: X from line1, Y averaged, Z from line1
|
||||
points = np.zeros((n, 3), dtype=np.float32)
|
||||
points[:, 0] = line1[:n, 0] # X from line1
|
||||
points[:, 1] = (line1[:n, 1] + line2[:n, 1]) / 2.0 # Y averaged
|
||||
points[:, 2] = line1[:n, 2] # Z from line1
|
||||
|
||||
# Filter non-negative X
|
||||
points = points[points[:, 0] >= 0]
|
||||
if points.shape[0] < 2:
|
||||
return np.empty((0, 2), dtype=np.float32)
|
||||
|
||||
N = points.shape[0]
|
||||
offsets = np.array([[0, -y_off, z_off], [0, y_off, z_off]], dtype=np.float32)
|
||||
points_3d = points[None, :, :] + offsets[:, None, :]
|
||||
points_3d = points_3d.reshape(2 * N, 3)
|
||||
|
||||
proj = self._car_space_transform @ points_3d.T
|
||||
proj = proj.reshape(3, 2, N)
|
||||
left_proj = proj[:, 0, :]
|
||||
right_proj = proj[:, 1, :]
|
||||
|
||||
valid_proj = (np.abs(left_proj[2]) >= 1e-6) & (np.abs(right_proj[2]) >= 1e-6)
|
||||
if not np.any(valid_proj):
|
||||
return np.empty((0, 2), dtype=np.float32)
|
||||
|
||||
left_screen = left_proj[:2, valid_proj] / left_proj[2, valid_proj][None, :]
|
||||
right_screen = right_proj[:2, valid_proj] / right_proj[2, valid_proj][None, :]
|
||||
|
||||
clip = self._clip_region
|
||||
x_min, x_max = clip.x, clip.x + clip.width
|
||||
y_min, y_max = clip.y, clip.y + clip.height
|
||||
|
||||
left_in_clip = (left_screen[0] >= x_min) & (left_screen[0] <= x_max) & (left_screen[1] >= y_min) & (left_screen[1] <= y_max)
|
||||
right_in_clip = (right_screen[0] >= x_min) & (right_screen[0] <= x_max) & (right_screen[1] >= y_min) & (right_screen[1] <= y_max)
|
||||
both_in_clip = left_in_clip & right_in_clip
|
||||
|
||||
if not np.any(both_in_clip):
|
||||
return np.empty((0, 2), dtype=np.float32)
|
||||
|
||||
left_screen = left_screen[:, both_in_clip]
|
||||
right_screen = right_screen[:, both_in_clip]
|
||||
|
||||
result = np.vstack((left_screen.T, right_screen[:, ::-1].T)).astype(np.float32)
|
||||
|
||||
# Ground the path: extend bottom vertices to screen bottom (matches C++ mapAveragedLineToPolygon)
|
||||
if result.shape[0] >= 4:
|
||||
mid = result.shape[0] // 2
|
||||
screen_h = self._rect.y + self._rect.height
|
||||
|
||||
# Extend left-bottom pair to screen bottom
|
||||
for idx in [mid - 1, mid - 2]:
|
||||
if 0 <= idx < result.shape[0] - 1:
|
||||
dy = result[idx, 1] - result[idx + 1, 1]
|
||||
if abs(dy) > 0.1:
|
||||
slope = (result[idx, 0] - result[idx + 1, 0]) / dy
|
||||
result[idx, 0] += (screen_h - result[idx, 1]) * slope
|
||||
result[idx, 1] = screen_h
|
||||
|
||||
# Extend right-bottom pair to screen bottom
|
||||
for idx in [mid, mid + 1]:
|
||||
if 0 < idx < result.shape[0]:
|
||||
dy = result[idx, 1] - result[idx - 1, 1]
|
||||
if abs(dy) > 0.1:
|
||||
slope = (result[idx, 0] - result[idx - 1, 0]) / dy
|
||||
result[idx, 0] += (screen_h - result[idx, 1]) * slope
|
||||
result[idx, 1] = screen_h
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _hsla_to_color(h, s, l, a):
|
||||
rgb = colorsys.hls_to_rgb(h, l, s)
|
||||
return rl.Color(
|
||||
int(rgb[0] * 255),
|
||||
int(rgb[1] * 255),
|
||||
int(rgb[2] * 255),
|
||||
int(a * 255)
|
||||
)
|
||||
return rl.Color(int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255), int(a * 255))
|
||||
|
||||
@staticmethod
|
||||
def _blend_colors(begin_colors, end_colors, t):
|
||||
@@ -438,9 +743,17 @@ class ModelRenderer(Widget):
|
||||
return begin_colors
|
||||
|
||||
inv_t = 1.0 - t
|
||||
return [rl.Color(
|
||||
int(inv_t * start.r + t * end.r),
|
||||
int(inv_t * start.g + t * end.g),
|
||||
int(inv_t * start.b + t * end.b),
|
||||
int(inv_t * start.a + t * end.a)
|
||||
) for start, end in zip(begin_colors, end_colors, strict=True)]
|
||||
return [
|
||||
rl.Color(int(inv_t * start.r + t * end.r), int(inv_t * start.g + t * end.g), int(inv_t * start.b + t * end.b), int(inv_t * start.a + t * end.a))
|
||||
for start, end in zip(begin_colors, end_colors, strict=True)
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _parse_color(color_str: str) -> rl.Color:
|
||||
"""Parse a hex color string (e.g., '#FF0000' or 'FF0000') to rl.Color."""
|
||||
s = color_str.lstrip('#')
|
||||
if len(s) == 8: # ARGB
|
||||
return rl.Color(int(s[2:4], 16), int(s[4:6], 16), int(s[6:8], 16), int(s[0:2], 16))
|
||||
if len(s) == 6: # RGB
|
||||
return rl.Color(int(s[0:2], 16), int(s[2:4], 16), int(s[4:6], 16), 255)
|
||||
return rl.Color(255, 255, 255, 255)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import pyray as rl
|
||||
import numpy as np
|
||||
import time
|
||||
@@ -58,8 +59,19 @@ class UIState:
|
||||
"rawAudioData",
|
||||
"frogpilotCarState",
|
||||
]
|
||||
).extend(
|
||||
[
|
||||
"frogpilotPlan",
|
||||
"frogpilotSelfdriveState",
|
||||
"frogpilotRadarState",
|
||||
"frogpilotDeviceState",
|
||||
"mapdOut",
|
||||
"liveTracks",
|
||||
]
|
||||
)
|
||||
|
||||
self._frogpilot_toggles: dict = {}
|
||||
|
||||
self.prime_state = PrimeState()
|
||||
|
||||
# UI Status tracking
|
||||
@@ -150,8 +162,7 @@ class UIState:
|
||||
self.always_on_dm = self.params.get_bool("AlwaysOnDM")
|
||||
if self.sm.valid.get("frogpilotCarState", False):
|
||||
frogpilot_car_state = self.sm["frogpilotCarState"]
|
||||
self.always_on_lateral_active = (not self.sm["selfdriveState"].enabled and
|
||||
frogpilot_car_state.alwaysOnLateralEnabled)
|
||||
self.always_on_lateral_active = not self.sm["selfdriveState"].enabled and frogpilot_car_state.alwaysOnLateralEnabled
|
||||
self.traffic_mode_enabled = frogpilot_car_state.trafficModeEnabled
|
||||
else:
|
||||
self.always_on_lateral_active = False
|
||||
@@ -159,6 +170,20 @@ class UIState:
|
||||
|
||||
self.conditional_status = self.params_memory.get_int("CEStatus", default=0) if self.started else 0
|
||||
|
||||
if self.sm.valid.get("frogpilotPlan", False) and self.sm.updated["frogpilotPlan"]:
|
||||
try:
|
||||
self._frogpilot_toggles = json.loads(self.sm["frogpilotPlan"].frogpilotToggles)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
|
||||
@property
|
||||
def frogpilot_toggles(self) -> dict:
|
||||
return self._frogpilot_toggles
|
||||
|
||||
@property
|
||||
def frogpilot_plan(self):
|
||||
return self.sm["frogpilotPlan"] if self.sm.valid.get("frogpilotPlan", False) else None
|
||||
|
||||
def _update_status(self) -> None:
|
||||
if self.started and self.sm.updated["selfdriveState"]:
|
||||
ss = self.sm["selfdriveState"]
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import time
|
||||
import pyray as rl
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
METRIC_HEIGHT = 126
|
||||
METRIC_WIDTH = 275
|
||||
SIDEBAR_WIDTH = 300
|
||||
FONT_SIZE = 32
|
||||
|
||||
|
||||
def _fmt(v, decimals=2):
|
||||
return f"{v:.{decimals}f}" if isinstance(v, float) else str(v)
|
||||
|
||||
|
||||
class DeveloperSidebar(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._font_bold = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
self._font_normal = gui_app.font(FontWeight.NORMAL)
|
||||
|
||||
# Engagement tracking
|
||||
self._lat_time = 0.0
|
||||
self._long_time = 0.0
|
||||
self._total_time = 0.0
|
||||
self._last_update_time: float = time.monotonic()
|
||||
|
||||
# Peak tracking
|
||||
self._max_steer_angle = 0.0
|
||||
self._max_torque = 0.0
|
||||
|
||||
def _update_state(self):
|
||||
sm = ui_state.sm
|
||||
|
||||
now = time.monotonic()
|
||||
dt = now - self._last_update_time
|
||||
self._last_update_time = now
|
||||
|
||||
# Engagement time tracking (only when onroad and not standstill)
|
||||
if ui_state.started and sm.valid.get("carControl", False) and sm.valid.get("carState", False):
|
||||
cc = sm["carControl"]
|
||||
cs = sm["carState"]
|
||||
if not cs.standstill and cs.gearShifter != 3: # not reverse
|
||||
self._total_time += dt
|
||||
if cc.latActive:
|
||||
self._lat_time += dt
|
||||
if cc.longActive:
|
||||
self._long_time += dt
|
||||
|
||||
# Peak tracking
|
||||
if sm.valid.get("carControl", False):
|
||||
torque = abs(sm["carControl"].actuators.torque)
|
||||
if torque > self._max_torque:
|
||||
self._max_torque = torque
|
||||
if sm.valid.get("carState", False):
|
||||
angle = abs(sm["carState"].steeringAngleDeg)
|
||||
if angle > self._max_steer_angle:
|
||||
self._max_steer_angle = angle
|
||||
|
||||
def reset_engagement(self):
|
||||
self._lat_time = 0.0
|
||||
self._long_time = 0.0
|
||||
self._total_time = 0.0
|
||||
self._max_steer_angle = 0.0
|
||||
self._max_torque = 0.0
|
||||
|
||||
def _get_metric_value(self, metric_id: int) -> tuple[str, str]:
|
||||
"""Return (label, value_string) for a metric ID (1-16)."""
|
||||
sm = ui_state.sm
|
||||
is_metric = ui_state.is_metric
|
||||
conv = 1.0 if is_metric else 3.281 # m/s^2 to ft/s^2
|
||||
|
||||
if metric_id == 1: # ACCEL
|
||||
a = sm["carState"].aEgo * conv if sm.valid.get("carState", False) else 0
|
||||
return ("ACCEL", f"{a:.2f}")
|
||||
elif metric_id == 2: # MAX ACCEL
|
||||
return ("MAX ACCEL", "-")
|
||||
elif metric_id == 3: # STEER DELAY
|
||||
d = sm["liveDelay"].lateralDelay if sm.valid.get("liveDelay", False) else 0
|
||||
return ("STEER DELAY", f"{d:.5f}")
|
||||
elif metric_id == 4: # FRICTION
|
||||
f = sm["liveTorqueParameters"].frictionCoefficientFiltered if sm.valid.get("liveTorqueParameters", False) else 0
|
||||
return ("FRICTION", f"{f:.5f}")
|
||||
elif metric_id == 5: # LAT ACCEL
|
||||
l = sm["liveTorqueParameters"].latAccelFactorFiltered if sm.valid.get("liveTorqueParameters", False) else 0
|
||||
return ("LAT ACCEL", f"{l:.5f}")
|
||||
elif metric_id == 6: # STEER RATIO
|
||||
r = sm["liveParameters"].steerRatio if sm.valid.get("liveParameters", False) else 0
|
||||
return ("STEER RATIO", f"{r:.5f}")
|
||||
elif metric_id == 7: # STEER STIFF
|
||||
s = sm["liveParameters"].stiffnessFactor if sm.valid.get("liveParameters", False) else 0
|
||||
return ("STEER STIFF", f"{s:.5f}")
|
||||
elif metric_id == 8: # LATERAL %
|
||||
pct = self._lat_time * 100 / max(self._total_time, 1)
|
||||
return ("LATERAL %", f"{pct:.2f}%")
|
||||
elif metric_id == 9: # LONG %
|
||||
pct = self._long_time * 100 / max(self._total_time, 1)
|
||||
return ("LONG %", f"{pct:.2f}%")
|
||||
elif metric_id == 10: # STEER ANGLE
|
||||
if sm.valid.get("carState", False):
|
||||
angle = sm["carState"].steeringAngleDeg
|
||||
if abs(self._max_torque) >= 50:
|
||||
return ("STEER ANGLE", f"{angle:.0f} - ({self._max_steer_angle:.0f})")
|
||||
return ("STEER ANGLE", f"{angle:.0f}")
|
||||
return ("STEER ANGLE", "-")
|
||||
elif metric_id == 11: # TORQUE %
|
||||
if sm.valid.get("carControl", False):
|
||||
torque = sm["carControl"].actuators.torque
|
||||
if abs(torque) >= 50:
|
||||
return ("TORQUE %", f"{torque:.0f} - ({self._max_torque:.0f})")
|
||||
return ("TORQUE %", f"{torque:.0f}")
|
||||
return ("TORQUE %", "-")
|
||||
elif metric_id == 12: # ACT ACCEL
|
||||
a = sm["carControl"].actuators.accel * conv if sm.valid.get("carControl", False) else 0
|
||||
return ("ACT ACCEL", f"{a:.2f}")
|
||||
elif metric_id == 13: # DANGER %
|
||||
if sm.valid.get("frogpilotPlan", False):
|
||||
d = sm["frogpilotPlan"].dangerFactor * 100
|
||||
return ("DANGER %", f"{d:.2f}%")
|
||||
return ("DANGER %", "-")
|
||||
elif metric_id == 14: # ACCEL JERK
|
||||
j = sm["frogpilotPlan"].accelerationJerk if sm.valid.get("frogpilotPlan", False) else 0
|
||||
return ("ACCEL JERK", f"{j}")
|
||||
elif metric_id == 15: # DANGER JERK
|
||||
j = sm["frogpilotPlan"].dangerJerk if sm.valid.get("frogpilotPlan", False) else 0
|
||||
return ("DANGER JERK", f"{j}")
|
||||
elif metric_id == 16: # SPEED JERK
|
||||
j = sm["frogpilotPlan"].speedJerk if sm.valid.get("frogpilotPlan", False) else 0
|
||||
return ("SPEED JERK", f"{j}")
|
||||
return ("", "")
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
t = ui_state.frogpilot_toggles
|
||||
if not t.get("developer_sidebar", False):
|
||||
return
|
||||
|
||||
# Read slot assignments (1-7 map to metric IDs 1-16, 0 = empty)
|
||||
slots = []
|
||||
for i in range(1, 8):
|
||||
mid = t.get(f"developer_sidebar_metric{i}", 0)
|
||||
if mid and 1 <= mid <= 16:
|
||||
slots.append(mid)
|
||||
|
||||
if not slots:
|
||||
return
|
||||
|
||||
count = len(slots)
|
||||
spacing = (rect.height - count * METRIC_HEIGHT) / max(count + 1, 1)
|
||||
y = rect.y + spacing
|
||||
custom_color = t.get("sidebar_color1", None)
|
||||
|
||||
for mid in slots:
|
||||
label, value = self._get_metric_value(mid)
|
||||
if not label:
|
||||
continue
|
||||
self._draw_metric(rect.x + 12, y, METRIC_WIDTH, METRIC_HEIGHT, label, value, custom_color)
|
||||
y += METRIC_HEIGHT + spacing
|
||||
|
||||
def _draw_metric(self, x, y, w, h, label, value, custom_color=None):
|
||||
metric_rect = rl.Rectangle(x, y, w, h)
|
||||
|
||||
# Accent bar on right side (like QT developer sidebar)
|
||||
accent_color = rl.Color(0x17, 0x86, 0x44, 255) # Green default
|
||||
if custom_color:
|
||||
s = custom_color.lstrip('#')
|
||||
if len(s) >= 6:
|
||||
accent_color = rl.Color(int(s[-6:-4], 16), int(s[-4:-2], 16), int(s[-2:], 16), 255)
|
||||
|
||||
edge_rect = rl.Rectangle(x + w - 100, y + 4, 100, h - 8)
|
||||
rl.begin_scissor_mode(int(x + w - 18), int(y), 18, int(h))
|
||||
rl.draw_rectangle_rounded(edge_rect, 0.3, 10, accent_color)
|
||||
rl.end_scissor_mode()
|
||||
|
||||
# Border
|
||||
rl.draw_rectangle_rounded_lines_ex(metric_rect, 0.3, 10, 2, rl.Color(255, 255, 255, 85))
|
||||
|
||||
# Text
|
||||
text = f"{label}\n{value}"
|
||||
ts = measure_text_cached(self._font_bold, text, FONT_SIZE)
|
||||
text_x = x + 22 + (w - 22 - ts.x) / 2
|
||||
text_y = y + (h - ts.y) / 2
|
||||
rl.draw_text_ex(self._font_bold, text, rl.Vector2(text_x, text_y), FONT_SIZE, 0, rl.WHITE)
|
||||
@@ -0,0 +1,183 @@
|
||||
import json
|
||||
import pyray as rl
|
||||
from dataclasses import dataclass
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
METER_TO_MILE = 0.000621371
|
||||
MS_TO_KPH = 3.6
|
||||
MS_TO_MPH = 2.237
|
||||
|
||||
# Random events: (json_key, display_name)
|
||||
RANDOM_EVENTS = [
|
||||
("accel30", "UwUs"),
|
||||
("accel35", "Loch Ness Encounters"),
|
||||
("accel40", "Visits to 1955"),
|
||||
("dejaVuCurve", "Deja Vu Moments"),
|
||||
("firefoxSteerSaturated", "Internet Explorer Weeeeeeees"),
|
||||
("hal9000", "HAL 9000 Denials"),
|
||||
("openpilotCrashedRandomEvent", "openpilot Crashes"),
|
||||
("thisIsFineSteerSaturated", "This Is Fine Moments"),
|
||||
("toBeContinued", "To Be Continued Moments"),
|
||||
("vCruise69", "Noices"),
|
||||
("yourFrogTriedToKillMe", "Attempted Frog Murders"),
|
||||
("youveGotMail", "Total Mail Received"),
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class StatBox:
|
||||
label: str = ""
|
||||
value: str = "-"
|
||||
|
||||
|
||||
class DriveSummary(Widget):
|
||||
def __init__(self, show_random_events: bool = False):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._show_random_events = show_random_events
|
||||
self._previous_stats: dict = {}
|
||||
self._current_stats: dict = {}
|
||||
self._stat_boxes: list[StatBox] = []
|
||||
self._random_events: list[tuple[str, int]] = []
|
||||
self._sorted_events: list[tuple[str, int]] = []
|
||||
|
||||
if show_random_events:
|
||||
self._title = "Random Events Summary"
|
||||
else:
|
||||
self._title = "Drive Summary"
|
||||
|
||||
def show_event(self):
|
||||
"""Called when widget becomes visible. Snapshot current stats."""
|
||||
raw = self._params.get("FrogPilotStats")
|
||||
if raw:
|
||||
try:
|
||||
self._current_stats = json.loads(raw)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
self._current_stats = {}
|
||||
else:
|
||||
self._current_stats = {}
|
||||
|
||||
self._previous_stats = dict(self._current_stats)
|
||||
|
||||
# Pre-compute sorted random events (data is immutable while visible)
|
||||
if self._show_random_events:
|
||||
cur_events = self._current_stats.get("RandomEvents", {})
|
||||
prev_events = self._previous_stats.get("RandomEvents", {})
|
||||
self._sorted_events = []
|
||||
for key, name in RANDOM_EVENTS:
|
||||
diff = cur_events.get(key, 0) - prev_events.get(key, 0)
|
||||
if diff > 0:
|
||||
self._sorted_events.append((name, diff))
|
||||
self._sorted_events.sort(key=lambda x: (-x[1], x[0]))
|
||||
|
||||
def set_previous_stats(self, stats: dict):
|
||||
"""Set the baseline stats snapshot (taken when last went onroad)."""
|
||||
self._previous_stats = stats
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
font_bold = gui_app.font(FontWeight.BOLD)
|
||||
font_normal = gui_app.font(FontWeight.NORMAL)
|
||||
|
||||
ox, oy = rect.x, rect.y
|
||||
w = rect.width
|
||||
|
||||
# Title
|
||||
title_size = 48
|
||||
ts = measure_text_cached(font_bold, self._title, title_size)
|
||||
rl.draw_text_ex(font_bold, self._title, rl.Vector2(ox + (w - ts.x) / 2, oy), title_size, 0, rl.WHITE)
|
||||
|
||||
y = oy + ts.y + 30
|
||||
|
||||
if self._show_random_events:
|
||||
self._render_random_events(font_bold, font_normal, ox, y, w)
|
||||
else:
|
||||
self._render_drive_stats(font_bold, font_normal, ox, y, w)
|
||||
|
||||
def _render_drive_stats(self, font_bold, font_normal, ox, y, w):
|
||||
cur = self._current_stats
|
||||
prev = self._previous_stats
|
||||
|
||||
tracked_time = cur.get("TrackedTime", 0) - prev.get("TrackedTime", 0)
|
||||
engaged_time = (cur.get("AOLTime", 0) - prev.get("AOLTime", 0)) + (cur.get("LongitudinalTime", 0) - prev.get("LongitudinalTime", 0))
|
||||
exp_time = cur.get("ExperimentalModeTime", 0) - prev.get("ExperimentalModeTime", 0)
|
||||
meters = cur.get("FrogPilotMeters", 0) - prev.get("FrogPilotMeters", 0)
|
||||
|
||||
is_metric = ui_state.is_metric
|
||||
|
||||
# Engagement %
|
||||
eng_pct = f"{engaged_time * 100 / max(tracked_time, 1):.0f}%" if tracked_time > 0 else "-"
|
||||
|
||||
# Distance
|
||||
if is_metric:
|
||||
dist = f"{meters / 1000:.1f} km"
|
||||
else:
|
||||
dist = f"{meters * METER_TO_MILE:.1f} mi"
|
||||
|
||||
# Time
|
||||
mins = tracked_time / 60
|
||||
hours = int(mins // 60)
|
||||
rem_mins = int(mins % 60)
|
||||
if hours > 0:
|
||||
time_str = f"{hours}h {rem_mins}m"
|
||||
else:
|
||||
time_str = f"{rem_mins}m"
|
||||
|
||||
# Exp %
|
||||
exp_pct = f"{exp_time * 100 / max(tracked_time, 1):.0f}%" if tracked_time > 0 else "-"
|
||||
|
||||
stats = [
|
||||
("Engagement", eng_pct),
|
||||
("Distance", dist),
|
||||
("Drive Time", time_str),
|
||||
("Experimental", exp_pct),
|
||||
]
|
||||
|
||||
box_w = (w - 30) / 2
|
||||
box_h = 180
|
||||
|
||||
for i, (label, value) in enumerate(stats):
|
||||
col = i % 2
|
||||
row = i // 2
|
||||
bx = ox + col * (box_w + 10)
|
||||
by = y + row * (box_h + 10)
|
||||
self._draw_stat_box(font_bold, font_normal, bx, by, box_w, box_h, label, value)
|
||||
|
||||
def _render_random_events(self, font_bold, font_normal, ox, y, w):
|
||||
if not self._sorted_events:
|
||||
text = "No Random Events Played!"
|
||||
ts = measure_text_cached(font_normal, text, 36)
|
||||
rl.draw_text_ex(font_normal, text, rl.Vector2(ox + (w - ts.x) / 2, y + 100), 36, 0, rl.Color(255, 255, 255, 150))
|
||||
return
|
||||
|
||||
box_w = (w - 20) / 3
|
||||
box_h = 140
|
||||
|
||||
for i, (name, count) in enumerate(self._sorted_events[:12]):
|
||||
col = i % 3
|
||||
row = i // 3
|
||||
bx = ox + col * (box_w + 10)
|
||||
by = y + row * (box_h + 10)
|
||||
self._draw_stat_box(font_bold, font_normal, bx, by, box_w, box_h, name, str(count))
|
||||
|
||||
def _draw_stat_box(self, font_bold, font_normal, x, y, w, h, label, value):
|
||||
rect = rl.Rectangle(x, y, w, h)
|
||||
rl.draw_rectangle_rounded(rect, 0.15, 10, rl.Color(57, 57, 57, 255))
|
||||
rl.draw_rectangle_rounded_lines_ex(rect, 0.15, 10, 2, rl.Color(255, 255, 255, 50))
|
||||
|
||||
# Label (top)
|
||||
label_size = 28
|
||||
ls = measure_text_cached(font_normal, label, label_size)
|
||||
lx = x + (w - ls.x) / 2
|
||||
ly = y + h * 0.15
|
||||
rl.draw_text_ex(font_normal, label, rl.Vector2(lx, ly), label_size, 0, rl.Color(170, 170, 170, 255))
|
||||
|
||||
# Value (center)
|
||||
val_size = 56
|
||||
vs = measure_text_cached(font_bold, value, val_size)
|
||||
vx = x + (w - vs.x) / 2
|
||||
vy = y + h * 0.45
|
||||
rl.draw_text_ex(font_bold, value, rl.Vector2(vx, vy), val_size, 0, rl.WHITE)
|
||||
@@ -225,6 +225,7 @@ class GuiApplication:
|
||||
self._modal_overlay = ModalOverlay()
|
||||
self._modal_overlay_shown = False
|
||||
self._modal_overlay_tick: Callable[[], None] | None = None
|
||||
self._nav_stack: list = []
|
||||
|
||||
self._mouse = MouseState(self._scale)
|
||||
self._mouse_events: list[MouseEvent] = []
|
||||
@@ -369,6 +370,41 @@ class GuiApplication:
|
||||
def set_modal_overlay_tick(self, tick_function: Callable | None):
|
||||
self._modal_overlay_tick = tick_function
|
||||
|
||||
def push_widget(self, widget):
|
||||
if widget in self._nav_stack:
|
||||
return
|
||||
if self._nav_stack:
|
||||
prev = self._nav_stack[-1]
|
||||
if hasattr(prev, 'set_enabled'):
|
||||
prev.set_enabled(False)
|
||||
self._nav_stack.append(widget)
|
||||
if hasattr(widget, 'show_event'):
|
||||
widget.show_event()
|
||||
if hasattr(widget, 'set_enabled'):
|
||||
widget.set_enabled(True)
|
||||
|
||||
def pop_widget(self, idx: int | None = None):
|
||||
if len(self._nav_stack) < 2:
|
||||
return
|
||||
idx_to_pop = len(self._nav_stack) - 1 if idx is None else idx
|
||||
if idx_to_pop <= 0 or idx_to_pop >= len(self._nav_stack):
|
||||
return
|
||||
if idx_to_pop == len(self._nav_stack) - 1:
|
||||
prev = self._nav_stack[idx_to_pop - 1]
|
||||
if hasattr(prev, 'set_enabled'):
|
||||
prev.set_enabled(True)
|
||||
widget = self._nav_stack.pop(idx_to_pop)
|
||||
if hasattr(widget, 'hide_event'):
|
||||
widget.hide_event()
|
||||
|
||||
def _render_nav_stack(self) -> bool:
|
||||
if not self._nav_stack:
|
||||
return False
|
||||
widget = self._nav_stack[-1]
|
||||
if hasattr(widget, 'render'):
|
||||
widget.render(rl.Rectangle(0, 0, self.width, self.height))
|
||||
return True
|
||||
|
||||
def set_should_render(self, should_render: bool):
|
||||
self._should_render = should_render
|
||||
|
||||
@@ -523,7 +559,9 @@ class GuiApplication:
|
||||
rl.clear_background(rl.BLACK)
|
||||
|
||||
# Handle modal overlay rendering and input processing
|
||||
if self._handle_modal_overlay():
|
||||
if self._render_nav_stack():
|
||||
yield False
|
||||
elif self._handle_modal_overlay():
|
||||
# Allow a Widget to still run a function while overlay is shown
|
||||
if self._modal_overlay_tick is not None:
|
||||
self._modal_overlay_tick()
|
||||
|
||||
@@ -34,6 +34,7 @@ class Widget(abc.ABC):
|
||||
self._click_callback: Callable[[], None] | None = None
|
||||
self._multi_touch = False
|
||||
self.__was_awake = True
|
||||
self._children: list = []
|
||||
|
||||
@property
|
||||
def rect(self) -> rl.Rectangle:
|
||||
@@ -180,9 +181,25 @@ class Widget(abc.ABC):
|
||||
|
||||
def show_event(self):
|
||||
"""Optionally handle show event. Parent must manually call this"""
|
||||
for child in self._children:
|
||||
child.show_event()
|
||||
|
||||
def hide_event(self):
|
||||
"""Optionally handle hide event. Parent must manually call this"""
|
||||
for child in self._children:
|
||||
child.hide_event()
|
||||
|
||||
def _child(self, widget):
|
||||
"""Register a child widget for lifecycle propagation."""
|
||||
assert widget not in self._children, f"{type(widget).__name__} already a child of {type(self).__name__}"
|
||||
self._children.append(widget)
|
||||
return widget
|
||||
|
||||
def dismiss(self, callback: Callable[[], None] | None = None):
|
||||
"""Dismiss this widget from the nav stack."""
|
||||
gui_app.pop_widget()
|
||||
if callback:
|
||||
callback()
|
||||
|
||||
|
||||
SWIPE_AWAY_THRESHOLD = 80 # px to dismiss after releasing
|
||||
|
||||
@@ -1,118 +1,43 @@
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget, DialogResult
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
from openpilot.system.ui.widgets.label import Label, FontWeight
|
||||
from openpilot.system.ui.widgets.keyboard import Keyboard, KeyboardLayout
|
||||
from openpilot.system.ui.widgets.keyboard import Keyboard
|
||||
|
||||
MARGIN = 50
|
||||
BUTTON_HEIGHT = 160
|
||||
OUTER_MARGIN_X = 200
|
||||
OUTER_MARGIN_Y = 150
|
||||
BACKGROUND_COLOR = rl.Color(27, 27, 27, 255)
|
||||
|
||||
class InputDialog(Widget):
|
||||
def __init__(self, title: str, default_text: str = "", hint_text: str = "", on_close: Callable[[DialogResult, str], None] | None = None):
|
||||
super().__init__()
|
||||
self._title = title
|
||||
self._text = default_text
|
||||
self._hint = hint_text
|
||||
self._default_text = default_text
|
||||
self._on_close = on_close
|
||||
|
||||
self._dialog_result = DialogResult.NO_ACTION
|
||||
|
||||
self._title_label = Label(title, 70, FontWeight.BOLD, text_color=rl.Color(201, 201, 201, 255))
|
||||
self._cancel_button = Button("Cancel", self._cancel_button_callback)
|
||||
self._confirm_button = Button("Confirm", self._confirm_button_callback, button_style=ButtonStyle.PRIMARY)
|
||||
|
||||
self._keyboard = Keyboard(self._on_key_pressed, self._on_keyboard_done, layout=KeyboardLayout.QWERTY)
|
||||
|
||||
self._font = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
def _on_key_pressed(self, key: str):
|
||||
if key == "\b":
|
||||
self._text = self._text[:-1]
|
||||
else:
|
||||
self._text += key
|
||||
self._keyboard = Keyboard(callback=self._on_keyboard_result)
|
||||
self._keyboard.set_title(title)
|
||||
self._keyboard.set_text(default_text)
|
||||
|
||||
def _on_keyboard_done(self):
|
||||
self._confirm_button_callback()
|
||||
|
||||
def _cancel_button_callback(self):
|
||||
self._dialog_result = DialogResult.CANCEL
|
||||
def _on_keyboard_result(self, result: DialogResult):
|
||||
if self._dialog_result != DialogResult.NO_ACTION:
|
||||
return
|
||||
self._dialog_result = result
|
||||
if self._on_close:
|
||||
self._on_close(self._dialog_result, self._text)
|
||||
|
||||
def _confirm_button_callback(self):
|
||||
self._dialog_result = DialogResult.CONFIRM
|
||||
if self._on_close:
|
||||
self._on_close(self._dialog_result, self._text)
|
||||
self._on_close(result, self._keyboard.text)
|
||||
|
||||
@property
|
||||
def result(self) -> DialogResult:
|
||||
return self._dialog_result
|
||||
|
||||
|
||||
@property
|
||||
def text(self) -> str:
|
||||
return self._text
|
||||
return self._keyboard.text
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._dialog_result = DialogResult.NO_ACTION
|
||||
self._keyboard.show_event()
|
||||
self._keyboard.clear()
|
||||
if self._default_text:
|
||||
self._keyboard.set_text(self._default_text)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
# Dim background
|
||||
rl.draw_rectangle(0, 0, int(rect.width), int(rect.height), rl.Color(0, 0, 0, 200))
|
||||
|
||||
# Dialog Box
|
||||
dialog_rect = rl.Rectangle(
|
||||
rect.x + OUTER_MARGIN_X,
|
||||
rect.y + OUTER_MARGIN_Y,
|
||||
rect.width - 2 * OUTER_MARGIN_X,
|
||||
rect.height - 2 * OUTER_MARGIN_Y,
|
||||
)
|
||||
rl.draw_rectangle_rounded(dialog_rect, 0.05, 10, BACKGROUND_COLOR)
|
||||
|
||||
# Title
|
||||
title_rect = rl.Rectangle(dialog_rect.x + MARGIN, dialog_rect.y + MARGIN, dialog_rect.width - 2 * MARGIN, 100)
|
||||
self._title_label.render(title_rect)
|
||||
|
||||
# Text Input Field
|
||||
input_rect = rl.Rectangle(dialog_rect.x + MARGIN, title_rect.y + title_rect.height + 40, dialog_rect.width - 2 * MARGIN, 120)
|
||||
rl.draw_rectangle_rounded(input_rect, 0.1, 10, rl.Color(40, 40, 40, 255))
|
||||
|
||||
display_text = self._text
|
||||
text_color = rl.WHITE
|
||||
if not display_text:
|
||||
display_text = self._hint
|
||||
text_color = rl.Color(128, 128, 128, 255)
|
||||
|
||||
text_size = rl.measure_text_ex(self._font, display_text, 50, 0)
|
||||
text_pos = rl.Vector2(input_rect.x + 40, input_rect.y + (input_rect.height - text_size.y) / 2)
|
||||
rl.draw_text_ex(self._font, display_text, text_pos, 50, 0, text_color)
|
||||
|
||||
# Blinking cursor
|
||||
if (rl.get_time() % 1.0) < 0.5:
|
||||
cursor_x = text_pos.x + (text_size.x if self._text else 0) + 5
|
||||
rl.draw_rectangle(int(cursor_x), int(text_pos.y), 4, 50, rl.WHITE)
|
||||
|
||||
# Keyboard
|
||||
keyboard_rect = rl.Rectangle(
|
||||
dialog_rect.x + MARGIN,
|
||||
input_rect.y + input_rect.height + 40,
|
||||
dialog_rect.width - 2 * MARGIN,
|
||||
400
|
||||
)
|
||||
self._keyboard.render(keyboard_rect)
|
||||
|
||||
# Buttons
|
||||
btn_y = dialog_rect.y + dialog_rect.height - BUTTON_HEIGHT - MARGIN
|
||||
btn_width = (dialog_rect.width - 3 * MARGIN) / 2
|
||||
|
||||
cancel_rect = rl.Rectangle(dialog_rect.x + MARGIN, btn_y, btn_width, BUTTON_HEIGHT)
|
||||
confirm_rect = rl.Rectangle(dialog_rect.x + 2 * MARGIN + btn_width, btn_y, btn_width, BUTTON_HEIGHT)
|
||||
|
||||
self._cancel_button.render(cancel_rect)
|
||||
self._confirm_button.render(confirm_rect)
|
||||
def _render(self, rect):
|
||||
self._keyboard.render(rect)
|
||||
return self._dialog_result
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from functools import partial
|
||||
import time
|
||||
from typing import Literal
|
||||
from collections.abc import Callable
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets import Widget, DialogResult
|
||||
from openpilot.system.ui.widgets.button import ButtonStyle, Button
|
||||
from openpilot.system.ui.widgets.inputbox import InputBox
|
||||
from openpilot.system.ui.widgets.label import Label
|
||||
@@ -58,7 +59,14 @@ KEYBOARD_LAYOUTS = {
|
||||
|
||||
|
||||
class Keyboard(Widget):
|
||||
def __init__(self, max_text_size: int = 255, min_text_size: int = 0, password_mode: bool = False, show_password_toggle: bool = False):
|
||||
def __init__(
|
||||
self,
|
||||
max_text_size: int = 255,
|
||||
min_text_size: int = 0,
|
||||
password_mode: bool = False,
|
||||
show_password_toggle: bool = False,
|
||||
callback: Callable[[DialogResult], None] | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
self._layout_name: Literal["lowercase", "uppercase", "numbers", "specials"] = "lowercase"
|
||||
self._caps_lock = False
|
||||
@@ -71,6 +79,7 @@ class Keyboard(Widget):
|
||||
self._input_box = InputBox(max_text_size)
|
||||
self._password_mode = password_mode
|
||||
self._show_password_toggle = show_password_toggle
|
||||
self._callback = callback
|
||||
|
||||
# Backspace key repeat tracking
|
||||
self._backspace_pressed: bool = False
|
||||
@@ -78,6 +87,8 @@ class Keyboard(Widget):
|
||||
self._backspace_last_repeat: float = 0.0
|
||||
|
||||
self._render_return_status = -1
|
||||
self._first_render = False
|
||||
self._skip_input = False
|
||||
self._cancel_button = Button(lambda: tr("Cancel"), self._cancel_button_callback)
|
||||
|
||||
self._eye_button = Button("", self._eye_button_callback, button_style=ButtonStyle.TRANSPARENT)
|
||||
@@ -98,12 +109,18 @@ class Keyboard(Widget):
|
||||
for _, key in enumerate(keys):
|
||||
if key in self._key_icons:
|
||||
texture = self._key_icons[key]
|
||||
self._all_keys[key] = Button("", partial(self._key_callback, key), icon=texture,
|
||||
button_style=ButtonStyle.PRIMARY if key == ENTER_KEY else ButtonStyle.KEYBOARD, multi_touch=True)
|
||||
self._all_keys[key] = Button(
|
||||
"",
|
||||
partial(self._key_callback, key),
|
||||
icon=texture,
|
||||
button_style=ButtonStyle.PRIMARY if key == ENTER_KEY else ButtonStyle.KEYBOARD,
|
||||
multi_touch=True,
|
||||
)
|
||||
else:
|
||||
self._all_keys[key] = Button(key, partial(self._key_callback, key), button_style=ButtonStyle.KEYBOARD, font_size=85, multi_touch=True)
|
||||
self._all_keys[CAPS_LOCK_KEY] = Button("", partial(self._key_callback, CAPS_LOCK_KEY), icon=self._key_icons[CAPS_LOCK_KEY],
|
||||
button_style=ButtonStyle.KEYBOARD, multi_touch=True)
|
||||
self._all_keys[CAPS_LOCK_KEY] = Button(
|
||||
"", partial(self._key_callback, CAPS_LOCK_KEY), icon=self._key_icons[CAPS_LOCK_KEY], button_style=ButtonStyle.KEYBOARD, multi_touch=True
|
||||
)
|
||||
|
||||
def set_text(self, text: str):
|
||||
self._input_box.text = text
|
||||
@@ -122,20 +139,42 @@ class Keyboard(Widget):
|
||||
self._title.set_text(title)
|
||||
self._sub_title.set_text(sub_title)
|
||||
|
||||
def _eye_button_callback(self):
|
||||
self._password_mode = not self._password_mode
|
||||
def set_callback(self, callback: Callable[[DialogResult], None] | None):
|
||||
self._callback = callback
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._skip_input = True
|
||||
|
||||
def _process_mouse_events(self):
|
||||
if not self._skip_input:
|
||||
super()._process_mouse_events()
|
||||
|
||||
def _cancel_button_callback(self):
|
||||
self.clear()
|
||||
self._render_return_status = 0
|
||||
if self in gui_app._nav_stack:
|
||||
gui_app.pop_widget()
|
||||
else:
|
||||
self._render_return_status = 0
|
||||
if self._callback:
|
||||
self._callback(DialogResult.CANCEL)
|
||||
|
||||
def _eye_button_callback(self):
|
||||
self._password_mode = not self._password_mode
|
||||
|
||||
def _key_callback(self, k):
|
||||
if k == ENTER_KEY:
|
||||
self._render_return_status = 1
|
||||
if self in gui_app._nav_stack:
|
||||
gui_app.pop_widget()
|
||||
else:
|
||||
self._render_return_status = 1
|
||||
if self._callback:
|
||||
self._callback(DialogResult.CONFIRM)
|
||||
else:
|
||||
self.handle_key_press(k)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self._skip_input = False
|
||||
rect = rl.Rectangle(rect.x + CONTENT_MARGIN, rect.y + CONTENT_MARGIN, rect.width - 2 * CONTENT_MARGIN, rect.height - 2 * CONTENT_MARGIN)
|
||||
self._title.render(rl.Rectangle(rect.x, rect.y, rect.width, 95))
|
||||
self._sub_title.render(rl.Rectangle(rect.x, rect.y + 95, rect.width, 60))
|
||||
|
||||
Reference in New Issue
Block a user