Merge branch 'StarPilot' of https://github.com/firestar5683/StarPilot into StarPilot
This commit is contained in:
@@ -96,6 +96,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"]
|
||||
|
||||
"CADILLAC_CT6_CC" = "CHEVROLET_VOLT"
|
||||
"CADILLAC_XT5_CC" = "GMC_ACADIA"
|
||||
"BUICK_BABYENCLAVE" = "GMC_ACADIA"
|
||||
"CHEVROLET_EQUINOX_CC" = "CHEVROLET_EQUINOX"
|
||||
"GMC_YUKON_CC" = "CHEVROLET_SILVERADO"
|
||||
"CHEVROLET_TRAILBLAZER_CC" = "CHEVROLET_TRAILBLAZER"
|
||||
|
||||
@@ -49,7 +49,7 @@ def get_long_tune(CP, params):
|
||||
k_f = 1.0
|
||||
|
||||
if CP.carFingerprint == CAR.TOYOTA_PRIUS:
|
||||
k_f = 0.4
|
||||
k_f = 0.7
|
||||
elif CP.carFingerprint not in TSS2_CAR:
|
||||
kiBP = [0., 5., 35.]
|
||||
kiV = [3.6, 2.4, 1.5]
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
extern const uint8_t gitversion[19];
|
||||
const uint8_t gitversion[19] = "DEV-e0cf4fd9-DEBUG";
|
||||
const uint8_t gitversion[19] = "DEV-7ceb47eb-DEBUG";
|
||||
|
||||
@@ -1 +1 @@
|
||||
DEV-e0cf4fd9-DEBUG
|
||||
DEV-7ceb47eb-DEBUG
|
||||
@@ -9,7 +9,7 @@ from cereal import car, custom, log
|
||||
from msgq.visionipc import VisionIpcClient, VisionStreamType
|
||||
|
||||
|
||||
from opendbc.car.gm.values import CC_ONLY_CAR
|
||||
from opendbc.car.gm.values import GMFlags
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper, DT_CTRL
|
||||
@@ -165,7 +165,7 @@ class SelfdriveD:
|
||||
|
||||
self.starpilot_events_prev = []
|
||||
|
||||
self.has_menu = self.CP.brand == "gm" and self.CP.carFingerprint not in CC_ONLY_CAR
|
||||
self.has_menu = self.CP.brand == "gm" and not (self.CP.flags & GMFlags.NO_CAMERA.value)
|
||||
|
||||
self.FPCP = messaging.log_from_bytes(self.params.get("StarPilotCarParams", block=True), custom.StarPilotCarParams)
|
||||
|
||||
|
||||
@@ -198,11 +198,12 @@ class AetherTile(Widget):
|
||||
|
||||
|
||||
class HubTile(AetherTile):
|
||||
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):
|
||||
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, get_status: Callable[[], str] | None = None):
|
||||
if bg_color:
|
||||
super().__init__(surface_color=bg_color, on_click=on_click)
|
||||
else:
|
||||
super().__init__(on_click=on_click)
|
||||
self.get_status = get_status
|
||||
self.title = title
|
||||
self.desc = desc
|
||||
if icon_path:
|
||||
@@ -214,6 +215,17 @@ class HubTile(AetherTile):
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
face = self._render_layers(rect)
|
||||
|
||||
status_text = self.get_status() if self.get_status else ""
|
||||
if status_text:
|
||||
import re
|
||||
m = re.search(r'(\d+)%$', status_text)
|
||||
if m:
|
||||
ratio = min(1.0, max(0.0, float(m.group(1)) / 100.0))
|
||||
if ratio > 0.05:
|
||||
fill_rect = rl.Rectangle(face.x, face.y, face.width * ratio, face.height)
|
||||
rl.draw_rectangle_rounded(fill_rect, TILE_RADIUS, 10, rl.Color(255, 255, 255, 40))
|
||||
|
||||
content_pad = SPACING.tile_content
|
||||
max_w = face.width - (content_pad * 2)
|
||||
lines = self._wrap_text(self._font_title, self.title, max_w, 30)
|
||||
@@ -224,8 +236,9 @@ class HubTile(AetherTile):
|
||||
for i, line in enumerate(lines):
|
||||
self._draw_text_fit(self._font_title, line, rl.Vector2(face.x + content_pad, ty + i * (line_h + line_spacing)), max_w, line_h, align_center=True)
|
||||
|
||||
if self.desc:
|
||||
desc_lines = self._wrap_text(self._font_desc, self.desc, max_w, 18, max_lines=3)
|
||||
desc_to_render = status_text if status_text else self.desc
|
||||
if desc_to_render:
|
||||
desc_lines = self._wrap_text(self._font_desc, desc_to_render, max_w, 18, max_lines=3)
|
||||
desc_y = ty + len(lines) * (line_h + line_spacing) + SPACING.lg
|
||||
for i, line in enumerate(desc_lines):
|
||||
self._draw_text_fit(self._font_desc, line, rl.Vector2(face.x + content_pad, desc_y + i * 20), max_w, 18, align_center=True)
|
||||
@@ -492,14 +505,14 @@ class AetherSliderDialog(Widget):
|
||||
if self._is_pressed_ok:
|
||||
self._ok_target = 0.0
|
||||
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._user_callback(DialogResult.CONFIRM, self._current_val)
|
||||
self._is_pressed_ok = False
|
||||
if self._is_pressed_cancel:
|
||||
self._cancel_target = 0.0
|
||||
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._user_callback(DialogResult.CANCEL, self._current_val)
|
||||
self._is_pressed_cancel = False
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
@@ -635,26 +648,46 @@ class TileGrid(Widget):
|
||||
self.tiles = []
|
||||
self._uniform_width = uniform_width
|
||||
|
||||
@property
|
||||
def gap(self) -> int:
|
||||
return self._gap
|
||||
|
||||
def add_tile(self, tile: Widget): self.tiles.append(tile)
|
||||
|
||||
def clear(self): self.tiles.clear()
|
||||
|
||||
def get_column_count(self, tile_count: int | None = None) -> int:
|
||||
count = len(self.tiles) if tile_count is None else tile_count
|
||||
if count <= 0:
|
||||
return self._columns or 1
|
||||
if self._columns is not None:
|
||||
return self._columns
|
||||
if count == 1: return 1
|
||||
if count == 2: return 2
|
||||
if count == 3: return 3
|
||||
if count == 4: return 2
|
||||
if count <= 6: return 3
|
||||
return 4
|
||||
|
||||
def get_row_count(self, tile_count: int | None = None) -> int:
|
||||
count = len(self.tiles) if tile_count is None else tile_count
|
||||
if count <= 0:
|
||||
return 0
|
||||
cols = self.get_column_count(count)
|
||||
return (count + cols - 1) // cols
|
||||
|
||||
def get_internal_gap_height(self, tile_count: int | None = None) -> float:
|
||||
rows = self.get_row_count(tile_count)
|
||||
return self._gap * max(0, rows - 1)
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self.set_rect(rect)
|
||||
if not self.tiles:
|
||||
return
|
||||
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
|
||||
cols = self.get_column_count(count)
|
||||
rows = self.get_row_count(count)
|
||||
tile_h = (rect.height - (self._gap * (rows - 1))) / rows
|
||||
uniform_tile_w = (rect.width - (self._gap * (cols - 1))) / cols if self._uniform_width else 0
|
||||
tile_idx = 0
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import Callable
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.starpilot.assets.model_manager import ModelManager
|
||||
from openpilot.system.hardware import HARDWARE, PC
|
||||
from openpilot.starpilot.assets.model_manager import ModelManager, TINYGRAD_VERSIONS, canonical_model_key, is_builtin_model_key, model_key_aliases
|
||||
from openpilot.starpilot.common.starpilot_variables import MODELS_PATH, update_starpilot_toggles
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.widgets import DialogResult
|
||||
@@ -15,105 +17,158 @@ 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.aethergrid import AetherSliderDialog
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelCatalogEntry:
|
||||
key: str
|
||||
name: str
|
||||
series: str
|
||||
version: str
|
||||
released: str
|
||||
builtin: bool
|
||||
installed: bool
|
||||
partial: bool
|
||||
community_favorite: bool
|
||||
user_favorite: bool
|
||||
|
||||
|
||||
def _clean_model_name(name: str) -> str:
|
||||
return str(name or "").replace("_default", "").replace("(Default)", "").strip()
|
||||
|
||||
class StarPilotDrivingModelLayout(StarPilotPanel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
if PC:
|
||||
self._model_dir = Path(os.path.expanduser("~/.comma/starpilot/data/models"))
|
||||
else:
|
||||
self._model_dir = Path("/data/starpilot/models")
|
||||
self._model_dir = MODELS_PATH
|
||||
self._model_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self._available_models = []
|
||||
self._available_model_names = []
|
||||
self._available_model_series = []
|
||||
self._available_model_versions = []
|
||||
self._model_released_dates = {}
|
||||
self._model_file_to_name = {}
|
||||
self._model_series_map = {}
|
||||
self._model_version_map = {}
|
||||
self._current_model_name = tr("Default")
|
||||
self._catalog_entries: dict[str, ModelCatalogEntry] = {}
|
||||
self._model_file_to_name: dict[str, str] = {}
|
||||
self._model_file_to_name_processed: dict[str, str] = {}
|
||||
self._model_series_map: dict[str, str] = {}
|
||||
self._model_released_dates: dict[str, str] = {}
|
||||
self._model_version_map: dict[str, str] = {}
|
||||
self._community_favorites: set[str] = set()
|
||||
self._user_favorites: set[str] = set()
|
||||
self._current_model_key = self._default_model_key()
|
||||
self._current_model_name = self._default_model_name()
|
||||
|
||||
self.CATEGORIES = [
|
||||
self.SECTIONS = [
|
||||
{
|
||||
"title": tr_noop("Select Model"),
|
||||
"type": "value",
|
||||
"icon": "toggle_icons/icon_steering.png",
|
||||
"on_click": self._on_select_model_clicked,
|
||||
"get_value": lambda: self._current_model_name,
|
||||
"visible": lambda: not self._params.get_bool("ModelRandomizer"),
|
||||
"color": "#597497"
|
||||
"title": tr_noop("Model Selection"),
|
||||
"columns": 1,
|
||||
"uniform_width": True,
|
||||
"categories": [
|
||||
{
|
||||
"title": tr_noop("Select Model"),
|
||||
"type": "value",
|
||||
"icon": "toggle_icons/icon_steering.png",
|
||||
"on_click": self._on_select_model_clicked,
|
||||
"get_value": lambda: self._current_model_name,
|
||||
"visible": lambda: not self._params.get_bool("ModelRandomizer"),
|
||||
"color": "#597497"
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Download Models"),
|
||||
"type": "hub",
|
||||
"icon": "toggle_icons/icon_system.png",
|
||||
"on_click": self._on_download_clicked,
|
||||
"color": "#597497"
|
||||
"title": tr_noop("Model Actions"),
|
||||
"columns": 2,
|
||||
"uniform_width": True,
|
||||
"categories": [
|
||||
{
|
||||
"title": tr_noop("Download Models"),
|
||||
"type": "hub",
|
||||
"icon": "toggle_icons/icon_system.png",
|
||||
"on_click": self._on_download_clicked,
|
||||
"color": "#597497",
|
||||
"get_status": lambda: self._params_memory.get("ModelDownloadProgress", encoding="utf-8") if self._is_download_active() else ""
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Delete Models"),
|
||||
"type": "hub",
|
||||
"icon": "toggle_icons/icon_system.png",
|
||||
"on_click": self._on_delete_clicked,
|
||||
"color": "#597497"
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Delete Models"),
|
||||
"type": "hub",
|
||||
"icon": "toggle_icons/icon_system.png",
|
||||
"on_click": self._on_delete_clicked,
|
||||
"color": "#597497"
|
||||
"title": tr_noop("Automation"),
|
||||
"columns": 2,
|
||||
"uniform_width": True,
|
||||
"categories": [
|
||||
{
|
||||
"title": tr_noop("Model Randomizer"),
|
||||
"type": "toggle",
|
||||
"icon": "toggle_icons/icon_conditional.png",
|
||||
"get_state": lambda: self._params.get_bool("ModelRandomizer"),
|
||||
"set_state": self._on_model_randomizer_toggled,
|
||||
"color": "#597497"
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Auto Download"),
|
||||
"type": "toggle",
|
||||
"icon": "toggle_icons/icon_system.png",
|
||||
"get_state": lambda: self._params.get_bool("AutomaticallyDownloadModels"),
|
||||
"set_state": lambda s: self._params.put_bool("AutomaticallyDownloadModels", s),
|
||||
"color": "#597497"
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Model Randomizer"),
|
||||
"type": "toggle",
|
||||
"icon": "toggle_icons/icon_conditional.png",
|
||||
"get_state": lambda: self._params.get_bool("ModelRandomizer"),
|
||||
"set_state": self._on_model_randomizer_toggled,
|
||||
"color": "#597497"
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Recovery Power"),
|
||||
"type": "value",
|
||||
"icon": "toggle_icons/icon_road.png",
|
||||
"get_value": lambda: f"{self._params.get_float('RecoveryPower'):.1f}",
|
||||
"on_click": self._on_recovery_power_clicked,
|
||||
"visible": lambda: self._params.get_int("TuningLevel") == 3,
|
||||
"color": "#597497"
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Stop Distance"),
|
||||
"type": "value",
|
||||
"icon": "toggle_icons/icon_road.png",
|
||||
"get_value": lambda: f"{self._params.get_float('StopDistance'):.1f}m",
|
||||
"on_click": self._on_stop_distance_clicked,
|
||||
"visible": lambda: self._params.get_int("TuningLevel") == 3,
|
||||
"color": "#597497"
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Auto Download"),
|
||||
"type": "toggle",
|
||||
"icon": "toggle_icons/icon_system.png",
|
||||
"get_state": lambda: self._params.get_bool("AutomaticallyDownloadModels"),
|
||||
"set_state": lambda s: self._params.put_bool("AutomaticallyDownloadModels", s),
|
||||
"color": "#597497"
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Blacklist"),
|
||||
"type": "hub",
|
||||
"icon": "toggle_icons/icon_system.png",
|
||||
"on_click": self._on_blacklist_clicked,
|
||||
"title": tr_noop("Randomizer Details"),
|
||||
"columns": 2,
|
||||
"uniform_width": True,
|
||||
"visible": lambda: self._params.get_bool("ModelRandomizer"),
|
||||
"color": "#597497"
|
||||
"categories": [
|
||||
{
|
||||
"title": tr_noop("Blacklist"),
|
||||
"type": "hub",
|
||||
"icon": "toggle_icons/icon_system.png",
|
||||
"on_click": self._on_blacklist_clicked,
|
||||
"color": "#597497"
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Ratings"),
|
||||
"type": "hub",
|
||||
"icon": "toggle_icons/icon_system.png",
|
||||
"on_click": self._on_scores_clicked,
|
||||
"color": "#597497"
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Ratings"),
|
||||
"type": "hub",
|
||||
"icon": "toggle_icons/icon_system.png",
|
||||
"on_click": self._on_scores_clicked,
|
||||
"visible": lambda: self._params.get_bool("ModelRandomizer"),
|
||||
"color": "#597497"
|
||||
"title": tr_noop("Advanced Tuning"),
|
||||
"columns": 2,
|
||||
"uniform_width": True,
|
||||
"visible": lambda: self._params.get_int("TuningLevel") == 3,
|
||||
"categories": [
|
||||
{
|
||||
"title": tr_noop("Recovery Power"),
|
||||
"type": "value",
|
||||
"icon": "toggle_icons/icon_road.png",
|
||||
"get_value": lambda: f"{self._params.get_float('RecoveryPower'):.1f}",
|
||||
"on_click": self._on_recovery_power_clicked,
|
||||
"color": "#597497"
|
||||
},
|
||||
{
|
||||
"title": tr_noop("Stop Distance"),
|
||||
"type": "value",
|
||||
"icon": "toggle_icons/icon_road.png",
|
||||
"get_value": lambda: f"{self._params.get_float('StopDistance'):.1f}m",
|
||||
"on_click": self._on_stop_distance_clicked,
|
||||
"color": "#597497"
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
self._model_manager = ModelManager(self._params, self._params_memory)
|
||||
self._download_thread = None
|
||||
self._manifest_fetch_thread = None
|
||||
self._manifest_fetched = False
|
||||
|
||||
self._fetch_manifest_async()
|
||||
self._update_model_metadata()
|
||||
self._rebuild_grid()
|
||||
|
||||
@@ -123,56 +178,187 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._fetch_manifest_async()
|
||||
self._update_model_metadata()
|
||||
|
||||
def _is_model_installed(self, key: str) -> bool:
|
||||
if not key: return False
|
||||
has_thneed = (self._model_dir / f"{key}.thneed").exists()
|
||||
if has_thneed: return True
|
||||
return (self._model_dir / f"{key}_driving_policy_tinygrad.pkl").exists()
|
||||
def _fetch_manifest_async(self):
|
||||
if self._manifest_fetch_thread is not None and self._manifest_fetch_thread.is_alive():
|
||||
return
|
||||
|
||||
def _task():
|
||||
self._model_manager.update_models()
|
||||
self._manifest_fetched = True
|
||||
|
||||
self._manifest_fetch_thread = threading.Thread(target=_task, daemon=True)
|
||||
self._manifest_fetch_thread.start()
|
||||
|
||||
def _default_model_key(self) -> str:
|
||||
default_key = self._params.get_default_value("Model") or self._params.get_default_value("DrivingModel")
|
||||
if isinstance(default_key, bytes):
|
||||
default_key = default_key.decode("utf-8", errors="ignore")
|
||||
return canonical_model_key(str(default_key or "").strip()) or "sc2"
|
||||
|
||||
def _default_model_name(self) -> str:
|
||||
default_name = self._params.get_default_value("DrivingModelName")
|
||||
if isinstance(default_name, bytes):
|
||||
default_name = default_name.decode("utf-8", errors="ignore")
|
||||
return _clean_model_name(default_name or "") or "South Carolina"
|
||||
|
||||
def _default_model_version(self) -> str:
|
||||
default_version = self._params.get_default_value("ModelVersion") or self._params.get_default_value("DrivingModelVersion")
|
||||
if isinstance(default_version, bytes):
|
||||
default_version = default_version.decode("utf-8", errors="ignore")
|
||||
return str(default_version or "").strip() or "v11"
|
||||
|
||||
def _current_selected_key(self) -> str:
|
||||
current_key = self._params.get("Model", encoding="utf-8") or self._params.get("DrivingModel", encoding="utf-8") or ""
|
||||
return canonical_model_key(str(current_key).strip()) or self._default_model_key()
|
||||
|
||||
def _load_on_disk_files(self) -> set[str]:
|
||||
try:
|
||||
return {entry.name for entry in self._model_dir.iterdir()}
|
||||
except Exception:
|
||||
return set()
|
||||
|
||||
def _is_model_installed(self, key: str, version: str = "", on_disk_files: set[str] | None = None) -> bool:
|
||||
model_key = canonical_model_key(str(key or "").strip())
|
||||
if not model_key:
|
||||
return False
|
||||
|
||||
if is_builtin_model_key(model_key):
|
||||
return True
|
||||
|
||||
files = on_disk_files if on_disk_files is not None else self._load_on_disk_files()
|
||||
if f"{model_key}.thneed" in files:
|
||||
return True
|
||||
|
||||
if version in TINYGRAD_VERSIONS:
|
||||
required_files = set(self._required_files_for_version(model_key, version))
|
||||
return required_files.issubset(files)
|
||||
|
||||
if version == "v7":
|
||||
return f"{model_key}.pkl" in files
|
||||
|
||||
return any(file.startswith(f"{model_key}.") or file.startswith(f"{model_key}_") for file in files)
|
||||
|
||||
def _required_files_for_version(self, key: str, version: str) -> list[str]:
|
||||
files = [
|
||||
f"{key}_driving_policy_tinygrad.pkl",
|
||||
f"{key}_driving_vision_tinygrad.pkl",
|
||||
f"{key}_driving_policy_metadata.pkl",
|
||||
f"{key}_driving_vision_metadata.pkl",
|
||||
]
|
||||
|
||||
if version == "v12":
|
||||
files.extend([
|
||||
f"{key}_driving_off_policy_tinygrad.pkl",
|
||||
f"{key}_driving_off_policy_metadata.pkl",
|
||||
])
|
||||
|
||||
return files
|
||||
|
||||
def _ensure_default_model_visible(self):
|
||||
default_key = self._default_model_key()
|
||||
default_name = self._default_model_name()
|
||||
default_series = tr("Custom Series")
|
||||
default_released = ""
|
||||
|
||||
for alias in model_key_aliases(default_key):
|
||||
alias = canonical_model_key(alias)
|
||||
if alias not in self._model_file_to_name:
|
||||
continue
|
||||
|
||||
default_name = self._model_file_to_name.get(alias, default_name)
|
||||
default_series = self._model_series_map.get(alias, default_series)
|
||||
default_released = self._model_released_dates.get(alias, default_released)
|
||||
|
||||
if alias != default_key:
|
||||
self._model_file_to_name.pop(alias, None)
|
||||
self._model_file_to_name_processed.pop(alias, None)
|
||||
self._model_series_map.pop(alias, None)
|
||||
self._model_released_dates.pop(alias, None)
|
||||
self._model_version_map.pop(alias, None)
|
||||
self._catalog_entries.pop(alias, None)
|
||||
|
||||
version = self._model_version_map.get(default_key, self._default_model_version())
|
||||
self._model_file_to_name[default_key] = default_name
|
||||
self._model_file_to_name_processed[default_key] = _clean_model_name(default_name)
|
||||
self._model_series_map[default_key] = default_series
|
||||
if default_released:
|
||||
self._model_released_dates[default_key] = default_released
|
||||
self._model_version_map.setdefault(default_key, version)
|
||||
|
||||
def _build_catalog_entries(self, on_disk_files: set[str]):
|
||||
self._catalog_entries.clear()
|
||||
self._model_file_to_name.clear()
|
||||
self._model_file_to_name_processed.clear()
|
||||
self._model_series_map.clear()
|
||||
self._model_released_dates.clear()
|
||||
self._model_version_map.clear()
|
||||
|
||||
available_models = [entry.strip() for entry in (self._params.get("AvailableModels", encoding="utf-8") or "").split(",")]
|
||||
available_names = [entry.strip() for entry in (self._params.get("AvailableModelNames", encoding="utf-8") or "").split(",")]
|
||||
available_series = [entry.strip() for entry in (self._params.get("AvailableModelSeries", encoding="utf-8") or "").split(",")]
|
||||
available_versions = [entry.strip() for entry in (self._params.get("ModelVersions", encoding="utf-8") or "").split(",")]
|
||||
released_dates = [entry.strip() for entry in (self._params.get("ModelReleasedDates", encoding="utf-8") or "").split(",")]
|
||||
|
||||
self._community_favorites = {canonical_model_key(entry.strip()) for entry in (self._params.get("CommunityFavorites", encoding="utf-8") or "").split(",") if entry.strip()}
|
||||
self._user_favorites = {canonical_model_key(entry.strip()) for entry in (self._params.get("UserFavorites", encoding="utf-8") or "").split(",") if entry.strip()}
|
||||
|
||||
size = min(len(available_models), len(available_names))
|
||||
for i in range(size):
|
||||
canonical_key = canonical_model_key(available_models[i])
|
||||
name = available_names[i].strip()
|
||||
if not canonical_key or not name:
|
||||
continue
|
||||
|
||||
series = available_series[i].strip() if i < len(available_series) and available_series[i].strip() else tr("Custom Series")
|
||||
version = available_versions[i].strip() if i < len(available_versions) else ""
|
||||
released = released_dates[i].strip() if i < len(released_dates) else ""
|
||||
|
||||
self._model_file_to_name.setdefault(canonical_key, name)
|
||||
self._model_file_to_name_processed.setdefault(canonical_key, _clean_model_name(name))
|
||||
self._model_series_map.setdefault(canonical_key, series)
|
||||
if released:
|
||||
self._model_released_dates.setdefault(canonical_key, released)
|
||||
if version:
|
||||
self._model_version_map.setdefault(canonical_key, version)
|
||||
|
||||
self._ensure_default_model_visible()
|
||||
|
||||
for key, name in self._model_file_to_name.items():
|
||||
version = self._model_version_map.get(key, self._default_model_version() if is_builtin_model_key(key) else "")
|
||||
installed = self._is_model_installed(key, version, on_disk_files)
|
||||
partial = (not is_builtin_model_key(key)) and (not installed) and any(file.startswith(f"{key}.") or file.startswith(f"{key}_") for file in on_disk_files)
|
||||
self._catalog_entries[key] = ModelCatalogEntry(
|
||||
key=key,
|
||||
name=name,
|
||||
series=self._model_series_map.get(key, tr("Custom Series")),
|
||||
version=version,
|
||||
released=self._model_released_dates.get(key, ""),
|
||||
builtin=is_builtin_model_key(key),
|
||||
installed=installed,
|
||||
partial=partial,
|
||||
community_favorite=(key in self._community_favorites),
|
||||
user_favorite=(key in self._user_favorites),
|
||||
)
|
||||
|
||||
def _update_model_metadata(self):
|
||||
available_models_raw = self._params.get("AvailableModels", encoding='utf-8')
|
||||
if not available_models_raw:
|
||||
manager = ModelManager(self._params, self._params_memory)
|
||||
manager.update_models()
|
||||
available_models_raw = self._params.get("AvailableModels", encoding='utf-8')
|
||||
on_disk_files = self._load_on_disk_files()
|
||||
self._build_catalog_entries(on_disk_files)
|
||||
|
||||
self._available_models = [m.strip() for m in (available_models_raw or "").split(",") if m.strip()]
|
||||
self._available_model_names = [m.strip() for m in (self._params.get("AvailableModelNames", encoding='utf-8') or "").split(",") if m.strip()]
|
||||
self._available_model_series = [m.strip() for m in (self._params.get("AvailableModelSeries", encoding='utf-8') or "").split(",") if m.strip()]
|
||||
self._available_model_versions = [m.strip() for m in (self._params.get("ModelVersions", encoding='utf-8') or "").split(",")]
|
||||
released_dates = (self._params.get("ModelReleasedDates", encoding='utf-8') or "").split(",")
|
||||
self._current_model_key = self._current_selected_key()
|
||||
current_entry = self._catalog_entries.get(self._current_model_key)
|
||||
if current_entry is None or not current_entry.installed:
|
||||
self._current_model_key = self._default_model_key()
|
||||
current_entry = self._catalog_entries.get(self._current_model_key)
|
||||
|
||||
self._model_file_to_name.clear()
|
||||
self._model_series_map.clear()
|
||||
self._model_version_map.clear()
|
||||
self._model_released_dates.clear()
|
||||
if current_entry is not None:
|
||||
self._current_model_name = current_entry.name
|
||||
else:
|
||||
self._current_model_name = self._default_model_name()
|
||||
|
||||
size = min(len(self._available_models), len(self._available_model_names))
|
||||
for i in range(size):
|
||||
key = self._available_models[i].strip()
|
||||
name = self._available_model_names[i].strip()
|
||||
if not key or not name: continue
|
||||
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):
|
||||
v = self._available_model_versions[i].strip()
|
||||
if v: self._model_version_map[key] = v
|
||||
if i < len(released_dates):
|
||||
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 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 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")
|
||||
|
||||
def _show_selection_dialog(self, title: str, options: dict[str, str] | list[str], current_val: str, on_confirm: Callable):
|
||||
def _show_selection_dialog(self, title: str, options: dict[str, str] | list[str], current_val: str, on_confirm: Callable, current_key: str = ""):
|
||||
if not options:
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("No options available.")))
|
||||
return
|
||||
@@ -186,11 +372,16 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
|
||||
|
||||
grouped = {}
|
||||
name_to_key = {}
|
||||
key_to_display = {}
|
||||
name_counts = {}
|
||||
for key, name in options.items():
|
||||
series = self._model_series_map.get(key, tr("Custom Series"))
|
||||
if series not in grouped: grouped[series] = []
|
||||
grouped[series].append(name)
|
||||
name_to_key[name] = key
|
||||
name_counts[name] = name_counts.get(name, 0) + 1
|
||||
display_name = name if name_counts[name] == 1 else f"{name} [{key}]"
|
||||
grouped[series].append(display_name)
|
||||
name_to_key[display_name] = key
|
||||
key_to_display[key] = display_name
|
||||
|
||||
for series in grouped: grouped[series].sort()
|
||||
sorted_series = sorted(grouped.keys())
|
||||
@@ -214,8 +405,10 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
|
||||
user_favs = [f.strip() for f in (self._params.get("UserFavorites", encoding='utf-8') or "").split(",") if f.strip()]
|
||||
comm_favs = [f.strip() for f in (self._params.get("CommunityFavorites", encoding='utf-8') or "").split(",") if f.strip()]
|
||||
|
||||
current_display = key_to_display.get(current_key, current_val)
|
||||
|
||||
dialog = SelectionDialog(
|
||||
title, final_grouped, current_val, on_close=_on_close_grouped,
|
||||
title, final_grouped, current_display, 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,
|
||||
@@ -224,23 +417,75 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
|
||||
)
|
||||
gui_app.set_modal_overlay(dialog)
|
||||
|
||||
def _is_download_active(self) -> bool:
|
||||
return bool(self._params_memory.get("ModelToDownload", encoding="utf-8") or self._params_memory.get_bool("DownloadAllModels"))
|
||||
|
||||
def _selected_model_version(self, model_key: str) -> str:
|
||||
version = self._model_version_map.get(model_key, "")
|
||||
if version:
|
||||
return version
|
||||
|
||||
try:
|
||||
versions_file = self._model_dir / ".model_versions.json"
|
||||
if versions_file.is_file():
|
||||
payload = json.loads(versions_file.read_text())
|
||||
if isinstance(payload, dict):
|
||||
for alias in model_key_aliases(model_key):
|
||||
resolved = str(payload.get(alias, "")).strip()
|
||||
if resolved:
|
||||
return resolved
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if is_builtin_model_key(model_key):
|
||||
return self._default_model_version()
|
||||
return ""
|
||||
|
||||
def _build_selectable_models(self) -> dict[str, str]:
|
||||
models: dict[str, str] = {}
|
||||
for key, entry in self._catalog_entries.items():
|
||||
if entry.installed:
|
||||
models[key] = entry.name
|
||||
|
||||
return models
|
||||
|
||||
def _build_deletable_models(self) -> dict[str, str]:
|
||||
installed = self._build_selectable_models()
|
||||
default_key = self._default_model_key()
|
||||
current_name = _clean_model_name(self._current_model_name)
|
||||
default_name = _clean_model_name(installed.get(default_key, self._default_model_name()))
|
||||
|
||||
deletable: dict[str, str] = {}
|
||||
for key, display_name in installed.items():
|
||||
processed_name = _clean_model_name(display_name)
|
||||
if processed_name == current_name or processed_name == default_name:
|
||||
continue
|
||||
deletable[key] = display_name
|
||||
return deletable
|
||||
|
||||
def _on_select_model_clicked(self):
|
||||
installed_models = {k: v for k, v in self._model_file_to_name.items() if self._is_model_installed(k)}
|
||||
if not installed_models: return
|
||||
self._update_model_metadata()
|
||||
installed_models = self._build_selectable_models()
|
||||
if not installed_models:
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("No downloaded models found.")))
|
||||
return
|
||||
|
||||
def _on_confirm(model_key):
|
||||
self._params.put("Model", model_key)
|
||||
self._params.put("DrivingModel", model_key)
|
||||
selected_model = canonical_model_key(model_key)
|
||||
self._params.put("Model", selected_model)
|
||||
self._params.put("DrivingModel", selected_model)
|
||||
self._params.put("DrivingModelName", installed_models[model_key])
|
||||
mv = self._model_version_map.get(model_key, "")
|
||||
if mv:
|
||||
self._params.put("ModelVersion", mv)
|
||||
self._params.put("DrivingModelVersion", mv)
|
||||
resolved_version = self._selected_model_version(selected_model)
|
||||
if resolved_version:
|
||||
self._params.put("ModelVersion", resolved_version)
|
||||
self._params.put("DrivingModelVersion", resolved_version)
|
||||
update_starpilot_toggles()
|
||||
self._update_model_metadata()
|
||||
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))
|
||||
self._params.put_bool("OnroadCycleRequested", True)
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Drive-cycle requested for immediate apply.")))
|
||||
|
||||
self._show_selection_dialog(tr("Select Driving Model"), installed_models, self._current_model_name, _on_confirm)
|
||||
self._show_selection_dialog(tr("Select Driving Model"), installed_models, self._current_model_name, _on_confirm, current_key=self._current_model_key)
|
||||
|
||||
def _on_recovery_power_clicked(self):
|
||||
def on_close(res, val):
|
||||
@@ -259,29 +504,48 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
|
||||
gui_app.set_modal_overlay(AetherSliderDialog(tr("Stop Distance"), 4.0, 10.0, 0.1, self._params.get_float("StopDistance"), on_close, unit="m", color="#597497"))
|
||||
|
||||
def _on_download_clicked(self):
|
||||
is_downloading = self._params_memory.get("ModelToDownload") or self._params_memory.get_bool("DownloadAllModels")
|
||||
if is_downloading:
|
||||
self._params_memory.remove("ModelToDownload")
|
||||
self._params_memory.put_bool("DownloadAllModels", False)
|
||||
self._params_memory.remove("ModelDownloadProgress")
|
||||
self._update_model_metadata()
|
||||
if ui_state.started:
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Cannot download models while driving.")))
|
||||
return
|
||||
|
||||
is_downloading = self._is_download_active()
|
||||
if is_downloading:
|
||||
self._params_memory.put_bool("CancelModelDownload", True)
|
||||
return
|
||||
|
||||
not_installed = {key: entry.name for key, entry in self._catalog_entries.items() if not entry.installed}
|
||||
if not not_installed:
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("All models are already installed.")))
|
||||
return
|
||||
|
||||
not_installed = {k: v for k, v in self._model_file_to_name.items() if not self._is_model_installed(k)}
|
||||
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)}
|
||||
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}
|
||||
self._update_model_metadata()
|
||||
if ui_state.started:
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Cannot delete model files while driving.")))
|
||||
return
|
||||
if self._is_download_active():
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Cannot delete model files while a download is in progress.")))
|
||||
return
|
||||
|
||||
deletable = self._build_deletable_models()
|
||||
|
||||
if not deletable:
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("No deletable models found.")))
|
||||
return
|
||||
|
||||
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()
|
||||
if res == DialogResult.CONFIRM:
|
||||
for file in self._model_dir.iterdir():
|
||||
if not (file.name == f"{mk}.thneed" or file.name == f"{mk}.pkl" or file.name.startswith(f"{mk}_")):
|
||||
continue
|
||||
if file.is_file():
|
||||
file.unlink(missing_ok=True)
|
||||
self._update_model_metadata()
|
||||
self._rebuild_grid()
|
||||
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)
|
||||
@@ -317,7 +581,7 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
|
||||
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)]
|
||||
not_installed = [key for key, entry in self._catalog_entries.items() if not entry.installed]
|
||||
if not_installed:
|
||||
def _on_download_confirm(res):
|
||||
if res == DialogResult.CONFIRM:
|
||||
@@ -326,16 +590,32 @@ class StarPilotDrivingModelLayout(StarPilotPanel):
|
||||
gui_app.set_modal_overlay(ConfirmDialog(tr("Download all models for Randomizer?"), tr("Download All"), on_close=_on_download_confirm))
|
||||
|
||||
def _update_state(self):
|
||||
if getattr(self, "_manifest_fetched", False):
|
||||
self._manifest_fetched = False
|
||||
self._update_model_metadata()
|
||||
self._rebuild_grid()
|
||||
|
||||
model_to_download = self._params_memory.get("ModelToDownload", encoding='utf-8') or ""
|
||||
download_all = self._params_memory.get_bool("DownloadAllModels")
|
||||
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: 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()
|
||||
def _download_task():
|
||||
try:
|
||||
if download_all:
|
||||
self._model_manager.download_all_models()
|
||||
else:
|
||||
self._model_manager.download_model(model_to_download)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self._params_memory.remove("CancelModelDownload")
|
||||
self._params_memory.remove("ModelToDownload")
|
||||
self._params_memory.put_bool("DownloadAllModels", False)
|
||||
self._params_memory.remove("ModelDownloadProgress")
|
||||
self._download_thread = None
|
||||
self._update_model_metadata()
|
||||
self._rebuild_grid()
|
||||
|
||||
self._download_thread = threading.Thread(target=_download_task, daemon=True)
|
||||
self._download_thread.start()
|
||||
|
||||
@@ -9,6 +9,7 @@ from openpilot.common.params import Params
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, HubTile, ToggleTile, ValueTile, SPACING
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.sectioned_panel import SectionedTileLayout, TileSection
|
||||
|
||||
|
||||
class StarPilotPanelType(IntEnum):
|
||||
@@ -46,7 +47,9 @@ class StarPilotPanel(Widget):
|
||||
self._sub_panels: dict[str, Widget] = {}
|
||||
self._scroller = None
|
||||
self._tile_grid = None
|
||||
self._sectioned_grid = None
|
||||
self.CATEGORIES = []
|
||||
self.SECTIONS = []
|
||||
|
||||
def set_navigate_callback(self, callback: Callable):
|
||||
self._navigate_callback = callback
|
||||
@@ -57,7 +60,74 @@ class StarPilotPanel(Widget):
|
||||
def set_current_sub_panel(self, sub_panel: str):
|
||||
self._current_sub_panel = sub_panel
|
||||
|
||||
def _is_category_visible(self, cat: dict) -> bool:
|
||||
visible_fn = cat.get("visible")
|
||||
return visible_fn is None or visible_fn()
|
||||
|
||||
def _build_tile(self, cat: dict) -> Widget | None:
|
||||
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"])
|
||||
|
||||
return 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"),
|
||||
get_status=cat.get("get_status"),
|
||||
)
|
||||
|
||||
if tile_type == "toggle":
|
||||
raw_set_state = cat["set_state"]
|
||||
|
||||
def on_toggle(state: bool, setter=raw_set_state):
|
||||
setter(state)
|
||||
self._rebuild_grid()
|
||||
|
||||
return ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=on_toggle, icon_path=cat.get("icon"), bg_color=cat.get("color"), desc=tr(cat.get("desc", "")), is_enabled=cat.get("is_enabled"), disabled_label=cat.get("disabled_label", ""))
|
||||
|
||||
if tile_type == "value":
|
||||
return 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"), is_enabled=cat.get("is_enabled"), desc=tr(cat.get("desc", "")))
|
||||
|
||||
return None
|
||||
|
||||
def _build_tile_grid(self, categories: list[dict], columns: int | None = None, padding: int | None = None, uniform_width: bool = False) -> TileGrid:
|
||||
grid = TileGrid(columns=columns, padding=padding, uniform_width=uniform_width)
|
||||
for cat in categories:
|
||||
if not self._is_category_visible(cat):
|
||||
continue
|
||||
tile = self._build_tile(cat)
|
||||
if tile is not None:
|
||||
grid.add_tile(tile)
|
||||
return grid
|
||||
|
||||
def _rebuild_grid(self):
|
||||
if self.SECTIONS:
|
||||
if self._sectioned_grid is None:
|
||||
self._sectioned_grid = SectionedTileLayout()
|
||||
|
||||
sections: list[TileSection] = []
|
||||
for section in self.SECTIONS:
|
||||
visible_fn = section.get("visible")
|
||||
if visible_fn is not None and not visible_fn():
|
||||
continue
|
||||
|
||||
grid = self._build_tile_grid(
|
||||
section.get("categories", []),
|
||||
columns=section.get("columns", 2),
|
||||
padding=section.get("padding", SPACING.tile_gap),
|
||||
uniform_width=section.get("uniform_width", True),
|
||||
)
|
||||
if grid.tiles:
|
||||
sections.append(TileSection(tr(section["title"]), grid))
|
||||
|
||||
self._sectioned_grid.set_sections(sections)
|
||||
return
|
||||
|
||||
if not self.CATEGORIES:
|
||||
return
|
||||
|
||||
@@ -67,38 +137,11 @@ class StarPilotPanel(Widget):
|
||||
self._tile_grid.clear()
|
||||
|
||||
for cat in self.CATEGORIES:
|
||||
visible_fn = cat.get("visible")
|
||||
if visible_fn is not None and not visible_fn():
|
||||
if not self._is_category_visible(cat):
|
||||
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":
|
||||
raw_set_state = cat["set_state"]
|
||||
|
||||
def on_toggle(state: bool, setter=raw_set_state):
|
||||
setter(state)
|
||||
self._rebuild_grid()
|
||||
|
||||
tile = ToggleTile(title=tr(cat["title"]), get_state=cat["get_state"], set_state=on_toggle, icon_path=cat.get("icon"), bg_color=cat.get("color"), desc=tr(cat.get("desc", "")), is_enabled=cat.get("is_enabled"), disabled_label=cat.get("disabled_label", ""))
|
||||
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"), is_enabled=cat.get("is_enabled"), desc=tr(cat.get("desc", "")))
|
||||
else:
|
||||
continue
|
||||
|
||||
self._tile_grid.add_tile(tile)
|
||||
tile = self._build_tile(cat)
|
||||
if tile is not None:
|
||||
self._tile_grid.add_tile(tile)
|
||||
|
||||
def _navigate_to(self, sub_panel: str):
|
||||
self._current_sub_panel = sub_panel
|
||||
@@ -113,6 +156,8 @@ 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.SECTIONS and self._sectioned_grid:
|
||||
self._sectioned_grid.render(rect)
|
||||
elif self.CATEGORIES and self._tile_grid:
|
||||
self._tile_grid.render(rect)
|
||||
elif self._scroller:
|
||||
@@ -123,9 +168,20 @@ class StarPilotPanel(Widget):
|
||||
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.SECTIONS and self._sectioned_grid:
|
||||
self._sectioned_grid.show_event()
|
||||
elif self._scroller:
|
||||
self._scroller.show_event()
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
if self._current_sub_panel and self._current_sub_panel in self._sub_panels:
|
||||
self._sub_panels[self._current_sub_panel].hide_event()
|
||||
elif self.SECTIONS and self._sectioned_grid:
|
||||
self._sectioned_grid.hide_event()
|
||||
elif self._scroller:
|
||||
self._scroller.hide_event()
|
||||
|
||||
|
||||
def create_tile_panel(categories: list[dict], sub_panels: dict[str, Widget] | None = None) -> StarPilotPanel:
|
||||
panel = StarPilotPanel()
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import SPACING, TileGrid
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TileSection:
|
||||
title: str
|
||||
grid: TileGrid
|
||||
|
||||
|
||||
class SectionedTileLayout(Widget):
|
||||
def __init__(self, section_gap: int = SPACING.section_gap, title_height: int = 32, title_gap: int = SPACING.sm,
|
||||
min_row_height: int = 150, max_row_height: int = 280, top_padding: int = 0,
|
||||
horizontal_padding: int = SPACING.xl, max_content_width: int | None = 1440):
|
||||
super().__init__()
|
||||
self._sections: list[TileSection] = []
|
||||
self._section_gap = section_gap
|
||||
self._title_height = title_height
|
||||
self._title_gap = title_gap
|
||||
self._min_row_height = min_row_height
|
||||
self._max_row_height = max_row_height
|
||||
self._top_padding = top_padding
|
||||
self._horizontal_padding = horizontal_padding
|
||||
self._max_content_width = max_content_width
|
||||
self._title_font_size = 26
|
||||
self._font_title = gui_app.font(FontWeight.BOLD)
|
||||
self._is_active = False
|
||||
|
||||
def set_sections(self, sections: list[TileSection]):
|
||||
if self._is_active:
|
||||
for section in self._sections:
|
||||
section.grid.hide_event()
|
||||
self._sections = list(sections)
|
||||
if self._is_active:
|
||||
for section in self._sections:
|
||||
section.grid.show_event()
|
||||
|
||||
def clear(self):
|
||||
self._sections.clear()
|
||||
|
||||
def show_event(self):
|
||||
self._is_active = True
|
||||
super().show_event()
|
||||
for section in self._sections:
|
||||
section.grid.show_event()
|
||||
|
||||
def hide_event(self):
|
||||
self._is_active = False
|
||||
super().hide_event()
|
||||
for section in self._sections:
|
||||
section.grid.hide_event()
|
||||
|
||||
def _title_block_height(self, section: TileSection) -> int:
|
||||
return (self._title_height + self._title_gap) if section.title else 0
|
||||
|
||||
def _section_band_height(self, section: TileSection, row_height: float) -> float:
|
||||
rows = section.grid.get_row_count()
|
||||
if rows <= 0:
|
||||
return 0.0
|
||||
return (rows * row_height) + (section.grid.gap * max(0, rows - 1))
|
||||
|
||||
def _content_rect(self, rect: rl.Rectangle) -> rl.Rectangle:
|
||||
content_x = rect.x + self._horizontal_padding
|
||||
content_w = max(0.0, rect.width - (self._horizontal_padding * 2))
|
||||
if self._max_content_width is not None and content_w > self._max_content_width:
|
||||
content_w = float(self._max_content_width)
|
||||
content_x = rect.x + (rect.width - content_w) / 2
|
||||
return rl.Rectangle(content_x, rect.y, content_w, rect.height)
|
||||
|
||||
def _compute_row_height(self, rect: rl.Rectangle, sections: list[TileSection]) -> float:
|
||||
total_rows = sum(section.grid.get_row_count() for section in sections)
|
||||
if total_rows <= 0:
|
||||
return 0.0
|
||||
|
||||
total_title_height = sum(self._title_block_height(section) for section in sections)
|
||||
total_section_gaps = self._section_gap * max(0, len(sections) - 1)
|
||||
total_internal_gaps = sum(section.grid.get_internal_gap_height() for section in sections)
|
||||
# Clamp oversized sections so tiles keep a touch-friendly shape instead of stretching to fill the full panel.
|
||||
fit_row_height = max(0.0, (rect.height - self._top_padding - total_title_height - total_section_gaps - total_internal_gaps) / total_rows)
|
||||
if fit_row_height >= self._min_row_height:
|
||||
return min(fit_row_height, self._max_row_height)
|
||||
return fit_row_height
|
||||
|
||||
def _draw_section_title(self, rect: rl.Rectangle, title: str):
|
||||
title_text = title.upper()
|
||||
spacing = round(self._title_font_size * 0.08)
|
||||
size = measure_text_cached(self._font_title, title_text, self._title_font_size, spacing=spacing)
|
||||
text_y = rect.y + (rect.height - size.y) / 2
|
||||
text_pos = rl.Vector2(round(rect.x), round(text_y))
|
||||
rl.draw_text_ex(self._font_title, title_text, rl.Vector2(text_pos.x + 1, text_pos.y + 1), self._title_font_size, spacing, rl.Color(0, 0, 0, 90))
|
||||
rl.draw_text_ex(self._font_title, title_text, text_pos, self._title_font_size, spacing, rl.Color(255, 255, 255, 215))
|
||||
|
||||
line_x = rect.x + size.x + SPACING.lg
|
||||
line_w = rect.width - (line_x - rect.x)
|
||||
if line_w <= 0:
|
||||
return
|
||||
line_y = int(rect.y + rect.height / 2)
|
||||
rl.draw_rectangle(int(line_x), line_y, int(line_w), 2, rl.Color(255, 255, 255, 36))
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self.set_rect(rect)
|
||||
sections = [section for section in self._sections if section.grid.tiles]
|
||||
if not sections:
|
||||
return
|
||||
|
||||
content_rect = self._content_rect(rect)
|
||||
row_height = self._compute_row_height(content_rect, sections)
|
||||
if row_height <= 0:
|
||||
return
|
||||
|
||||
y = content_rect.y + self._top_padding
|
||||
for index, section in enumerate(sections):
|
||||
if section.title:
|
||||
title_rect = rl.Rectangle(content_rect.x, y, content_rect.width, self._title_height)
|
||||
self._draw_section_title(title_rect, section.title)
|
||||
y += self._title_height + self._title_gap
|
||||
|
||||
active_grid_height = (section.grid.get_row_count() * row_height) + section.grid.get_internal_gap_height()
|
||||
section.grid.render(rl.Rectangle(content_rect.x, y, content_rect.width, active_grid_height))
|
||||
y += self._section_band_height(section, row_height)
|
||||
|
||||
if index < len(sections) - 1:
|
||||
y += self._section_gap
|
||||
@@ -8,7 +8,7 @@ from openpilot.system.hardware import TICI
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
|
||||
CONNECTION_RETRY_INTERVAL = 0.2 # seconds between connection attempts
|
||||
|
||||
@@ -37,52 +37,70 @@ void main() {
|
||||
}
|
||||
"""
|
||||
|
||||
FRAME_FRAGMENT_SHADER_EGL = """
|
||||
#version 300 es
|
||||
#extension GL_OES_EGL_image_external_essl3 : enable
|
||||
precision mediump float;
|
||||
in vec2 fragTexCoord;
|
||||
uniform samplerExternalOES texture0;
|
||||
out vec4 fragColor;
|
||||
uniform int enhance_driver;
|
||||
# Choose fragment shader based on platform capabilities
|
||||
if TICI:
|
||||
FRAME_FRAGMENT_SHADER = """
|
||||
#version 300 es
|
||||
#extension GL_OES_EGL_image_external_essl3 : enable
|
||||
precision mediump float;
|
||||
in vec2 fragTexCoord;
|
||||
uniform samplerExternalOES texture0;
|
||||
out vec4 fragColor;
|
||||
uniform int engaged;
|
||||
uniform int enhance_driver;
|
||||
|
||||
void main() {
|
||||
vec4 color = texture(texture0, fragTexCoord);
|
||||
color.rgb = pow(color.rgb, vec3(1.0/1.28));
|
||||
if (enhance_driver == 1) {
|
||||
float brightness = 1.1;
|
||||
color.rgb = color.rgb + 0.15;
|
||||
color.rgb = clamp((color.rgb - 0.5) * (brightness * 0.8) + 0.5, 0.0, 1.0);
|
||||
color.rgb = color.rgb * color.rgb * (3.0 - 2.0 * color.rgb);
|
||||
color.rgb = pow(color.rgb, vec3(0.8));
|
||||
void main() {
|
||||
vec4 color = texture(texture0, fragTexCoord);
|
||||
if (engaged == 1) {
|
||||
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114)); // Luma
|
||||
color.rgb = mix(vec3(gray), color.rgb, 0.2); // 20% saturation
|
||||
color.rgb = clamp((color.rgb - 0.5) * 1.2 + 0.5, 0.0, 1.0); // +20% contrast
|
||||
color.rgb = pow(color.rgb, vec3(1.0/1.28));
|
||||
} else {
|
||||
color.rgb *= 0.85; // 85% opacity
|
||||
}
|
||||
if (enhance_driver == 1) {
|
||||
float brightness = 1.1;
|
||||
color.rgb = color.rgb + 0.15;
|
||||
color.rgb = clamp((color.rgb - 0.5) * (brightness * 0.8) + 0.5, 0.0, 1.0);
|
||||
color.rgb = color.rgb * color.rgb * (3.0 - 2.0 * color.rgb);
|
||||
color.rgb = pow(color.rgb, vec3(0.8));
|
||||
}
|
||||
fragColor = vec4(color.rgb, color.a);
|
||||
}
|
||||
fragColor = vec4(color.rgb, color.a);
|
||||
}
|
||||
"""
|
||||
"""
|
||||
else:
|
||||
FRAME_FRAGMENT_SHADER = VERSION + """
|
||||
in vec2 fragTexCoord;
|
||||
uniform sampler2D texture0;
|
||||
uniform sampler2D texture1;
|
||||
out vec4 fragColor;
|
||||
uniform int engaged;
|
||||
uniform int enhance_driver;
|
||||
|
||||
FRAME_FRAGMENT_SHADER_TEXTURES = VERSION + """
|
||||
in vec2 fragTexCoord;
|
||||
uniform sampler2D texture0;
|
||||
uniform sampler2D texture1;
|
||||
out vec4 fragColor;
|
||||
uniform int enhance_driver;
|
||||
|
||||
void main() {
|
||||
float y = texture(texture0, fragTexCoord).r;
|
||||
vec2 uv = texture(texture1, fragTexCoord).ra - 0.5;
|
||||
vec3 rgb = vec3(y + 1.402*uv.y, y - 0.344*uv.x - 0.714*uv.y, y + 1.772*uv.x);
|
||||
// TODO: the images out of camerad need some more correction and
|
||||
// the ui should apply a gamma curve for the device display
|
||||
if (enhance_driver == 1) {
|
||||
float brightness = 1.1;
|
||||
rgb = rgb + 0.15;
|
||||
rgb = clamp((rgb - 0.5) * (brightness * 0.8) + 0.5, 0.0, 1.0);
|
||||
rgb = rgb * rgb * (3.0 - 2.0 * rgb);
|
||||
rgb = pow(rgb, vec3(0.8));
|
||||
void main() {
|
||||
float y = texture(texture0, fragTexCoord).r;
|
||||
vec2 uv = texture(texture1, fragTexCoord).ra - 0.5;
|
||||
vec3 rgb = vec3(y + 1.402*uv.y, y - 0.344*uv.x - 0.714*uv.y, y + 1.772*uv.x);
|
||||
if (engaged == 1) {
|
||||
float gray = dot(rgb, vec3(0.299, 0.587, 0.114));
|
||||
rgb = mix(vec3(gray), rgb, 0.2); // 20% saturation
|
||||
rgb = clamp((rgb - 0.5) * 1.2 + 0.5, 0.0, 1.0); // +20% contrast
|
||||
} else {
|
||||
rgb *= 0.85; // 85% opacity
|
||||
}
|
||||
// TODO: the images out of camerad need some more correction and
|
||||
// the ui should apply a gamma curve for the device display
|
||||
if (enhance_driver == 1) {
|
||||
float brightness = 1.1;
|
||||
rgb = rgb + 0.15;
|
||||
rgb = clamp((rgb - 0.5) * (brightness * 0.8) + 0.5, 0.0, 1.0);
|
||||
rgb = rgb * rgb * (3.0 - 2.0 * rgb);
|
||||
rgb = pow(rgb, vec3(0.8));
|
||||
}
|
||||
fragColor = vec4(rgb, 1.0);
|
||||
}
|
||||
fragColor = vec4(rgb, 1.0);
|
||||
}
|
||||
"""
|
||||
"""
|
||||
|
||||
|
||||
class CameraView(Widget):
|
||||
@@ -101,6 +119,12 @@ class CameraView(Widget):
|
||||
|
||||
self._texture_needs_update = True
|
||||
self.last_connection_attempt: float = 0.0
|
||||
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAME_FRAGMENT_SHADER)
|
||||
self._texture1_loc: int = rl.get_shader_location(self.shader, "texture1") if not TICI else -1
|
||||
self._engaged_loc = rl.get_shader_location(self.shader, "engaged")
|
||||
self._engaged_val = rl.ffi.new("int[1]", [1])
|
||||
self._enhance_driver_loc = rl.get_shader_location(self.shader, "enhance_driver")
|
||||
self._enhance_driver_val = rl.ffi.new("int[1]", [1 if stream_type == VisionStreamType.VISION_STREAM_DRIVER else 0])
|
||||
|
||||
self.frame: VisionBuf | None = None
|
||||
self.texture_y: rl.Texture | None = None
|
||||
@@ -111,18 +135,12 @@ class CameraView(Widget):
|
||||
self.egl_texture: rl.Texture | None = None
|
||||
|
||||
self._placeholder_color: rl.Color | None = None
|
||||
self._use_egl = TICI and init_egl()
|
||||
|
||||
fragment_shader = FRAME_FRAGMENT_SHADER_EGL if self._use_egl else FRAME_FRAGMENT_SHADER_TEXTURES
|
||||
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, fragment_shader)
|
||||
self._texture1_loc: int = rl.get_shader_location(self.shader, "texture1") if not self._use_egl else -1
|
||||
self._enhance_driver_loc = rl.get_shader_location(self.shader, "enhance_driver")
|
||||
self._enhance_driver_val = rl.ffi.new("int[1]", [1 if stream_type == VisionStreamType.VISION_STREAM_DRIVER else 0])
|
||||
# Initialize EGL for zero-copy rendering on TICI
|
||||
if TICI:
|
||||
if not init_egl():
|
||||
raise RuntimeError("Failed to initialize EGL")
|
||||
|
||||
# Keep the UI alive if EGL cannot be used on device startup.
|
||||
if TICI and not self._use_egl:
|
||||
cloudlog.warning(f"Failed to initialize EGL for {self._name}; falling back to texture rendering")
|
||||
elif self._use_egl:
|
||||
# Create a 1x1 pixel placeholder texture for EGL image binding
|
||||
temp_image = rl.gen_image_color(1, 1, rl.BLACK)
|
||||
self.egl_texture = rl.load_texture_from_image(temp_image)
|
||||
@@ -136,11 +154,11 @@ class CameraView(Widget):
|
||||
# Prevent old frames from showing when going onroad. Qt has a separate thread
|
||||
# which drains the VisionIpcClient SubSocket for us. Re-connecting is not enough
|
||||
# and only clears internal buffers, not the message queue.
|
||||
self.frame = None
|
||||
self.available_streams.clear()
|
||||
if self.client:
|
||||
del self.client
|
||||
self.client = VisionIpcClient(self._name, self._stream_type, conflate=True)
|
||||
self.frame = None
|
||||
|
||||
def _set_placeholder_color(self, color: rl.Color):
|
||||
"""Set a placeholder color to be drawn when no frame is available."""
|
||||
@@ -170,7 +188,7 @@ class CameraView(Widget):
|
||||
self._clear_textures()
|
||||
|
||||
# Clean up EGL texture
|
||||
if self.egl_texture:
|
||||
if TICI and self.egl_texture:
|
||||
rl.unload_texture(self.egl_texture)
|
||||
self.egl_texture = None
|
||||
|
||||
@@ -245,7 +263,7 @@ class CameraView(Widget):
|
||||
dst_rect = rl.Rectangle(x_offset, y_offset, scale_x, scale_y)
|
||||
|
||||
# Render with appropriate method
|
||||
if self._use_egl:
|
||||
if TICI:
|
||||
self._render_egl(src_rect, dst_rect)
|
||||
else:
|
||||
self._render_textures(src_rect, dst_rect)
|
||||
@@ -279,7 +297,7 @@ class CameraView(Widget):
|
||||
|
||||
# Render with shader
|
||||
rl.begin_shader_mode(self.shader)
|
||||
self._update_shader_uniforms()
|
||||
self._update_texture_color_filtering()
|
||||
rl.draw_texture_pro(self.egl_texture, src_rect, dst_rect, rl.Vector2(0, 0), 0.0, rl.WHITE)
|
||||
rl.end_shader_mode()
|
||||
|
||||
@@ -299,12 +317,14 @@ class CameraView(Widget):
|
||||
|
||||
# Render with shader
|
||||
rl.begin_shader_mode(self.shader)
|
||||
self._update_shader_uniforms()
|
||||
self._update_texture_color_filtering()
|
||||
rl.set_shader_value_texture(self.shader, self._texture1_loc, self.texture_uv)
|
||||
rl.draw_texture_pro(self.texture_y, src_rect, dst_rect, rl.Vector2(0, 0), 0.0, rl.WHITE)
|
||||
rl.end_shader_mode()
|
||||
|
||||
def _update_shader_uniforms(self):
|
||||
def _update_texture_color_filtering(self):
|
||||
self._engaged_val[0] = 1 if ui_state.status != UIStatus.DISENGAGED else 0
|
||||
rl.set_shader_value(self.shader, self._engaged_loc, self._engaged_val, rl.ShaderUniformDataType.SHADER_UNIFORM_INT)
|
||||
rl.set_shader_value(self.shader, self._enhance_driver_loc, self._enhance_driver_val, rl.ShaderUniformDataType.SHADER_UNIFORM_INT)
|
||||
|
||||
def _ensure_connection(self) -> bool:
|
||||
@@ -356,7 +376,6 @@ class CameraView(Widget):
|
||||
self.client = self._target_client
|
||||
self._stream_type = self._target_stream_type
|
||||
self._texture_needs_update = True
|
||||
self._enhance_driver_val[0] = 1 if self._stream_type == VisionStreamType.VISION_STREAM_DRIVER else 0
|
||||
|
||||
# Reset state
|
||||
self._target_client = None
|
||||
@@ -368,7 +387,7 @@ class CameraView(Widget):
|
||||
|
||||
def _initialize_textures(self):
|
||||
self._clear_textures()
|
||||
if not self._use_egl:
|
||||
if not TICI:
|
||||
self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride),
|
||||
int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE))
|
||||
self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2),
|
||||
@@ -384,7 +403,7 @@ class CameraView(Widget):
|
||||
self.texture_uv = None
|
||||
|
||||
# Clean up EGL resources
|
||||
if self._use_egl:
|
||||
if TICI:
|
||||
for data in self.egl_images.values():
|
||||
destroy_egl_image(data)
|
||||
self.egl_images = {}
|
||||
|
||||
@@ -37,30 +37,32 @@ void main() {
|
||||
}
|
||||
"""
|
||||
|
||||
FRAME_FRAGMENT_SHADER_EGL = """
|
||||
#version 300 es
|
||||
#extension GL_OES_EGL_image_external_essl3 : enable
|
||||
precision mediump float;
|
||||
in vec2 fragTexCoord;
|
||||
uniform samplerExternalOES texture0;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec4 color = texture(texture0, fragTexCoord);
|
||||
fragColor = vec4(pow(color.rgb, vec3(1.0/1.28)), color.a);
|
||||
}
|
||||
"""
|
||||
|
||||
FRAME_FRAGMENT_SHADER_TEXTURES = VERSION + """
|
||||
in vec2 fragTexCoord;
|
||||
uniform sampler2D texture0;
|
||||
uniform sampler2D texture1;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
float y = texture(texture0, fragTexCoord).r;
|
||||
vec2 uv = texture(texture1, fragTexCoord).ra - 0.5;
|
||||
fragColor = vec4(y + 1.402*uv.y, y - 0.344*uv.x - 0.714*uv.y, y + 1.772*uv.x, 1.0);
|
||||
}
|
||||
"""
|
||||
# Choose fragment shader based on platform capabilities
|
||||
if TICI:
|
||||
FRAME_FRAGMENT_SHADER = """
|
||||
#version 300 es
|
||||
#extension GL_OES_EGL_image_external_essl3 : enable
|
||||
precision mediump float;
|
||||
in vec2 fragTexCoord;
|
||||
uniform samplerExternalOES texture0;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
vec4 color = texture(texture0, fragTexCoord);
|
||||
fragColor = vec4(pow(color.rgb, vec3(1.0/1.28)), color.a);
|
||||
}
|
||||
"""
|
||||
else:
|
||||
FRAME_FRAGMENT_SHADER = VERSION + """
|
||||
in vec2 fragTexCoord;
|
||||
uniform sampler2D texture0;
|
||||
uniform sampler2D texture1;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
float y = texture(texture0, fragTexCoord).r;
|
||||
vec2 uv = texture(texture1, fragTexCoord).ra - 0.5;
|
||||
fragColor = vec4(y + 1.402*uv.y, y - 0.344*uv.x - 0.714*uv.y, y + 1.772*uv.x, 1.0);
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class CameraView(Widget):
|
||||
@@ -79,6 +81,8 @@ class CameraView(Widget):
|
||||
|
||||
self._texture_needs_update = True
|
||||
self.last_connection_attempt: float = 0.0
|
||||
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, FRAME_FRAGMENT_SHADER)
|
||||
self._texture1_loc: int = rl.get_shader_location(self.shader, "texture1") if not TICI else -1
|
||||
|
||||
self.frame: VisionBuf | None = None
|
||||
self.texture_y: rl.Texture | None = None
|
||||
@@ -89,16 +93,12 @@ class CameraView(Widget):
|
||||
self.egl_texture: rl.Texture | None = None
|
||||
|
||||
self._placeholder_color: rl.Color | None = None
|
||||
self._use_egl = TICI and init_egl()
|
||||
|
||||
fragment_shader = FRAME_FRAGMENT_SHADER_EGL if self._use_egl else FRAME_FRAGMENT_SHADER_TEXTURES
|
||||
self.shader = rl.load_shader_from_memory(VERTEX_SHADER, fragment_shader)
|
||||
self._texture1_loc: int = rl.get_shader_location(self.shader, "texture1") if not self._use_egl else -1
|
||||
# Initialize EGL for zero-copy rendering on TICI
|
||||
if TICI:
|
||||
if not init_egl():
|
||||
raise RuntimeError("Failed to initialize EGL")
|
||||
|
||||
# Keep the UI alive if EGL cannot be used on device startup.
|
||||
if TICI and not self._use_egl:
|
||||
cloudlog.warning(f"Failed to initialize EGL for {self._name}; falling back to texture rendering")
|
||||
elif self._use_egl:
|
||||
# Create a 1x1 pixel placeholder texture for EGL image binding
|
||||
temp_image = rl.gen_image_color(1, 1, rl.BLACK)
|
||||
self.egl_texture = rl.load_texture_from_image(temp_image)
|
||||
@@ -146,7 +146,7 @@ class CameraView(Widget):
|
||||
self._clear_textures()
|
||||
|
||||
# Clean up EGL texture
|
||||
if self.egl_texture:
|
||||
if TICI and self.egl_texture:
|
||||
rl.unload_texture(self.egl_texture)
|
||||
self.egl_texture = None
|
||||
|
||||
@@ -220,7 +220,7 @@ class CameraView(Widget):
|
||||
dst_rect = rl.Rectangle(x_offset, y_offset, scale_x, scale_y)
|
||||
|
||||
# Render with appropriate method
|
||||
if self._use_egl:
|
||||
if TICI:
|
||||
self._render_egl(src_rect, dst_rect)
|
||||
else:
|
||||
self._render_textures(src_rect, dst_rect)
|
||||
@@ -337,7 +337,7 @@ class CameraView(Widget):
|
||||
|
||||
def _initialize_textures(self):
|
||||
self._clear_textures()
|
||||
if not self._use_egl:
|
||||
if not TICI:
|
||||
self.texture_y = rl.load_texture_from_image(rl.Image(None, int(self.client.stride),
|
||||
int(self.client.height), 1, rl.PixelFormat.PIXELFORMAT_UNCOMPRESSED_GRAYSCALE))
|
||||
self.texture_uv = rl.load_texture_from_image(rl.Image(None, int(self.client.stride // 2),
|
||||
@@ -353,7 +353,7 @@ class CameraView(Widget):
|
||||
self.texture_uv = None
|
||||
|
||||
# Clean up EGL resources
|
||||
if self._use_egl:
|
||||
if TICI:
|
||||
for data in self.egl_images.values():
|
||||
destroy_egl_image(data)
|
||||
self.egl_images = {}
|
||||
|
||||
+55
-24
@@ -249,6 +249,25 @@ class Soundd:
|
||||
sd._initialize()
|
||||
return sd.OutputStream(channels=1, samplerate=SAMPLE_RATE, callback=self.callback, blocksize=SAMPLE_BUFFER)
|
||||
|
||||
def start_stream(self, sd):
|
||||
stream = self.get_stream(sd)
|
||||
stream.start()
|
||||
cloudlog.info(f"soundd stream started: {stream.samplerate=} {stream.channels=} {stream.dtype=} {stream.device=}, {stream.blocksize=}")
|
||||
return stream
|
||||
|
||||
def describe_stream(self, stream) -> str:
|
||||
attrs = {
|
||||
"active": getattr(stream, "active", None),
|
||||
"stopped": getattr(stream, "stopped", None),
|
||||
"closed": getattr(stream, "closed", None),
|
||||
"samplerate": getattr(stream, "samplerate", None),
|
||||
"channels": getattr(stream, "channels", None),
|
||||
"dtype": getattr(stream, "dtype", None),
|
||||
"device": getattr(stream, "device", None),
|
||||
"blocksize": getattr(stream, "blocksize", None),
|
||||
}
|
||||
return " ".join(f"{k}={v!r}" for k, v in attrs.items())
|
||||
|
||||
def soundd_thread(self):
|
||||
# sounddevice must be imported after forking processes
|
||||
import sounddevice as sd
|
||||
@@ -257,39 +276,51 @@ class Soundd:
|
||||
|
||||
sm = sm.extend(['starpilotSelfdriveState', 'starpilotPlan'])
|
||||
|
||||
with self.get_stream(sd) as stream:
|
||||
rk = Ratekeeper(20)
|
||||
while True:
|
||||
stream = None
|
||||
try:
|
||||
stream = self.start_stream(sd)
|
||||
rk = Ratekeeper(20)
|
||||
|
||||
cloudlog.info(f"soundd stream started: {stream.samplerate=} {stream.channels=} {stream.dtype=} {stream.device=}, {stream.blocksize=}")
|
||||
while True:
|
||||
sm.update(0)
|
||||
while True:
|
||||
sm.update(0)
|
||||
|
||||
if sm.updated['soundPressure'] and self.current_alert == AudibleAlert.none: # only update volume filter when not playing alert
|
||||
self.spl_filter_weighted.update(sm["soundPressure"].soundPressureWeightedDb)
|
||||
self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x))
|
||||
if sm.updated['soundPressure'] and self.current_alert == AudibleAlert.none: # only update volume filter when not playing alert
|
||||
self.spl_filter_weighted.update(sm["soundPressure"].soundPressureWeightedDb)
|
||||
self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x))
|
||||
|
||||
if self.starpilot_toggles.alert_volume_controller:
|
||||
self.auto_volume = self.current_volume
|
||||
self.current_volume = 0.0
|
||||
if self.starpilot_toggles.alert_volume_controller:
|
||||
self.auto_volume = self.current_volume
|
||||
self.current_volume = 0.0
|
||||
|
||||
elif self.current_alert != AudibleAlert.none and self.starpilot_toggles.alert_volume_controller:
|
||||
self.current_volume = self.get_volume_override()
|
||||
if self.current_volume == 1.01:
|
||||
self.current_volume = self.auto_volume
|
||||
elif self.current_alert != AudibleAlert.none and self.starpilot_toggles.alert_volume_controller:
|
||||
self.current_volume = self.get_volume_override()
|
||||
if self.current_volume == 1.01:
|
||||
self.current_volume = self.auto_volume
|
||||
|
||||
self.get_audible_alert(sm)
|
||||
self.get_audible_alert(sm)
|
||||
|
||||
rk.keep_time()
|
||||
rk.keep_time()
|
||||
|
||||
assert stream.active
|
||||
if not stream.active:
|
||||
raise RuntimeError(f"soundd stream inactive: {self.describe_stream(stream)}")
|
||||
|
||||
starpilot_toggles = get_starpilot_toggles(sm)
|
||||
if starpilot_toggles != self.starpilot_toggles:
|
||||
self.starpilot_toggles = starpilot_toggles
|
||||
starpilot_toggles = get_starpilot_toggles(sm)
|
||||
if starpilot_toggles != self.starpilot_toggles:
|
||||
self.starpilot_toggles = starpilot_toggles
|
||||
|
||||
stream = self.update_starpilot_sounds(sd, stream)
|
||||
elif rk.frame % 5 == 0:
|
||||
stream = self.update_starpilot_sounds(sd, stream)
|
||||
stream = self.update_starpilot_sounds(sd, stream)
|
||||
elif rk.frame % 5 == 0:
|
||||
stream = self.update_starpilot_sounds(sd, stream)
|
||||
except Exception:
|
||||
cloudlog.exception("soundd: stream failed, restarting")
|
||||
time.sleep(1)
|
||||
finally:
|
||||
if stream is not None:
|
||||
try:
|
||||
stream.close()
|
||||
except Exception:
|
||||
cloudlog.exception("soundd: failed to close stream")
|
||||
|
||||
def update_starpilot_sounds(self, sd=None, stream=None):
|
||||
self.volume_map = {
|
||||
|
||||
Binary file not shown.
+22
-26
@@ -5,13 +5,10 @@
|
||||
#include <cerrno>
|
||||
#include <cmath>
|
||||
#include <chrono>
|
||||
#include <csignal>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <execinfo.h>
|
||||
#include <fcntl.h>
|
||||
#include <mutex>
|
||||
#include <pthread.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/types.h>
|
||||
#include <thread>
|
||||
@@ -48,8 +45,7 @@ std::atomic<uint64_t> ui_stall_frame{0};
|
||||
std::atomic<bool> ui_stall_reported{false};
|
||||
std::atomic<uint64_t> ui_stall_reported_ns{0};
|
||||
std::atomic<int> ui_stall_reported_phase{static_cast<int>(UIStallPhase::INIT)};
|
||||
std::atomic<int> ui_stall_dump_fd{-1};
|
||||
pthread_t ui_main_thread{};
|
||||
std::atomic<pid_t> ui_main_tid{0};
|
||||
|
||||
double read_env_double(const char *name, double default_value) {
|
||||
const char *value = std::getenv(name);
|
||||
@@ -84,25 +80,29 @@ std::string ui_stall_dump_dir() {
|
||||
return access("/data/log", W_OK) == 0 ? "/data/log" : "/tmp";
|
||||
}
|
||||
|
||||
void ui_stall_signal_handler(int sig) {
|
||||
const int fd = ui_stall_dump_fd.load(std::memory_order_relaxed);
|
||||
if (fd < 0) {
|
||||
void write_stall_dump_section(int fd, const std::string &title, const std::string &body) {
|
||||
std::string output = "== " + title + " ==\n";
|
||||
output += body.empty() ? "<unavailable>\n" : body;
|
||||
if (!output.empty() && output.back() != '\n') {
|
||||
output += '\n';
|
||||
}
|
||||
HANDLE_EINTR(write(fd, output.data(), output.size()));
|
||||
}
|
||||
|
||||
void write_ui_thread_snapshot(int fd) {
|
||||
const pid_t tid = ui_main_tid.load(std::memory_order_relaxed);
|
||||
if (tid <= 0) {
|
||||
write_stall_dump_section(fd, "main_thread", "<unavailable>");
|
||||
return;
|
||||
}
|
||||
|
||||
char header[256];
|
||||
const pid_t tid = static_cast<pid_t>(syscall(SYS_gettid));
|
||||
const int header_len = std::snprintf(header, sizeof(header),
|
||||
"=== UI stall backtrace (signal=%d pid=%d tid=%d) ===\n",
|
||||
sig, getpid(), tid);
|
||||
if (header_len > 0) {
|
||||
write(fd, header, header_len);
|
||||
}
|
||||
write_stall_dump_section(fd, "main_thread", util::string_format("pid=%d tid=%d", getpid(), tid));
|
||||
|
||||
void *frames[128];
|
||||
const int frame_count = backtrace(frames, 128);
|
||||
backtrace_symbols_fd(frames, frame_count, fd);
|
||||
write(fd, "\n", 1);
|
||||
const std::string task_dir = "/proc/self/task/" + std::to_string(tid);
|
||||
write_stall_dump_section(fd, "main_thread status", util::read_file(task_dir + "/status"));
|
||||
write_stall_dump_section(fd, "main_thread wchan", util::read_file(task_dir + "/wchan"));
|
||||
write_stall_dump_section(fd, "main_thread syscall", util::read_file(task_dir + "/syscall"));
|
||||
write_stall_dump_section(fd, "main_thread kernel_stack", util::read_file(task_dir + "/stack"));
|
||||
}
|
||||
|
||||
void ui_stall_progress(UIStallPhase phase, uint64_t frame = 0) {
|
||||
@@ -126,8 +126,7 @@ void ui_stall_progress(UIStallPhase phase, uint64_t frame = 0) {
|
||||
void start_ui_stall_monitor() {
|
||||
static std::once_flag once;
|
||||
std::call_once(once, [] {
|
||||
ui_main_thread = pthread_self();
|
||||
std::signal(SIGUSR1, ui_stall_signal_handler);
|
||||
ui_main_tid.store(static_cast<pid_t>(syscall(SYS_gettid)), std::memory_order_relaxed);
|
||||
ui_stall_progress(UIStallPhase::INIT, 0);
|
||||
|
||||
const double stall_probe_dt = read_env_double("UI_STALL_PROBE_MAX_DT", 5.0);
|
||||
@@ -176,10 +175,7 @@ void start_ui_stall_monitor() {
|
||||
write(fd, header, header_len);
|
||||
}
|
||||
|
||||
ui_stall_dump_fd.store(fd, std::memory_order_relaxed);
|
||||
pthread_kill(ui_main_thread, SIGUSR1);
|
||||
std::this_thread::sleep_for(50ms);
|
||||
ui_stall_dump_fd.store(-1, std::memory_order_relaxed);
|
||||
write_ui_thread_snapshot(fd);
|
||||
close(fd);
|
||||
LOGE("UI main thread stalled for %.1fs (phase=%s frame=%llu dump=%s)",
|
||||
stalled_for_s,
|
||||
|
||||
@@ -101,7 +101,14 @@ def terminate_child(proc: subprocess.Popen[str]) -> None:
|
||||
|
||||
|
||||
def run_mapd_once() -> int:
|
||||
OFFLINE_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
OFFLINE_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
except PermissionError:
|
||||
cloudlog.exception(f"mapd_wrapper cannot create offline directory: {OFFLINE_ROOT}")
|
||||
return 2
|
||||
except OSError:
|
||||
cloudlog.exception(f"mapd_wrapper failed to prepare offline directory: {OFFLINE_ROOT}")
|
||||
return 2
|
||||
|
||||
proc = subprocess.Popen(
|
||||
[MAPD_BIN.as_posix()],
|
||||
|
||||
@@ -64,17 +64,14 @@ class SelectionHeader(Widget):
|
||||
self._pressed = False
|
||||
|
||||
class SelectionItem(Widget):
|
||||
def __init__(self, text: str, is_selected: bool, is_favorite: bool, callback: Callable[[str], None], fav_callback: Callable[[str], None] = None):
|
||||
def __init__(self, text: str, is_selected: bool, callback: Callable[[str], None]):
|
||||
super().__init__()
|
||||
self._text = text
|
||||
self._is_selected = is_selected
|
||||
self._is_favorite = is_favorite
|
||||
self._callback = callback
|
||||
self._fav_callback = fav_callback
|
||||
self._font = gui_app.font(FontWeight.MEDIUM)
|
||||
self._font_size = 48
|
||||
self._pressed = False
|
||||
self._fav_pressed = False
|
||||
self.set_rect(rl.Rectangle(0, 0, 0, 110))
|
||||
|
||||
def set_parent_rect(self, parent_rect: rl.Rectangle) -> None:
|
||||
@@ -94,14 +91,9 @@ class SelectionItem(Widget):
|
||||
if self._is_selected:
|
||||
rl.draw_rectangle_rounded_lines_ex(rect, 0.1, 10, 3, rl.WHITE)
|
||||
|
||||
# Favorite Star - Left side
|
||||
star = "♥" if self._is_favorite else "♡"
|
||||
star_pos = rl.Vector2(rect.x + 25, rect.y + (rect.height - self._font_size) / 2)
|
||||
rl.draw_text_ex(self._font, star, star_pos, self._font_size + 10, 0, rl.WHITE)
|
||||
|
||||
# Text
|
||||
text_size = rl.measure_text_ex(self._font, self._text, self._font_size, 0)
|
||||
text_pos = rl.Vector2(rect.x + 90, rect.y + (rect.height - text_size.y) / 2)
|
||||
text_pos = rl.Vector2(rect.x + 40, rect.y + (rect.height - text_size.y) / 2)
|
||||
rl.draw_text_ex(self._font, self._text, text_pos, self._font_size, 0, rl.WHITE)
|
||||
|
||||
# Indicator (Dot for selection instead of radio)
|
||||
@@ -109,25 +101,15 @@ class SelectionItem(Widget):
|
||||
circle_center = rl.Vector2(rect.x + rect.width - 50, rect.y + rect.height / 2)
|
||||
rl.draw_circle_v(circle_center, 12, rl.WHITE)
|
||||
|
||||
@property
|
||||
def _fav_rect(self) -> rl.Rectangle:
|
||||
return rl.Rectangle(self._rect.x, self._rect.y, 80, self._rect.height)
|
||||
|
||||
def _handle_mouse_press(self, mouse_pos):
|
||||
if rl.check_collision_point_rec(mouse_pos, self._fav_rect):
|
||||
self._fav_pressed = True
|
||||
elif rl.check_collision_point_rec(mouse_pos, self._hit_rect):
|
||||
if rl.check_collision_point_rec(mouse_pos, self._hit_rect):
|
||||
self._pressed = True
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos):
|
||||
if self._fav_pressed and rl.check_collision_point_rec(mouse_pos, self._fav_rect):
|
||||
if self._fav_callback:
|
||||
self._fav_callback(self._text)
|
||||
elif self._pressed and rl.check_collision_point_rec(mouse_pos, self._hit_rect):
|
||||
if self._pressed and rl.check_collision_point_rec(mouse_pos, self._hit_rect):
|
||||
if self._callback:
|
||||
self._callback(self._text)
|
||||
self._pressed = False
|
||||
self._fav_pressed = False
|
||||
|
||||
class SelectionDialog(Widget):
|
||||
def __init__(self, title: str, options, current_selection: str = "",
|
||||
@@ -214,20 +196,16 @@ class SelectionDialog(Widget):
|
||||
for model in sorted_models:
|
||||
key = self._name_to_file.get(model, model)
|
||||
is_selected = (model == self._selected_value or key == self._selected_value)
|
||||
is_fav = key in self._user_favorites or key in self._community_favorites
|
||||
items.append(SelectionItem(
|
||||
text=model,
|
||||
is_selected=is_selected,
|
||||
is_favorite=is_fav,
|
||||
callback=self._on_item_selected,
|
||||
fav_callback=self._toggle_favorite if self._favorites_editable else None
|
||||
callback=self._on_item_selected
|
||||
))
|
||||
else:
|
||||
for option in self._options_raw:
|
||||
items.append(SelectionItem(
|
||||
text=option,
|
||||
is_selected=(option == self._selected_value),
|
||||
is_favorite=False,
|
||||
callback=self._on_item_selected
|
||||
))
|
||||
|
||||
@@ -257,14 +235,14 @@ class SelectionDialog(Widget):
|
||||
item._is_selected = (item._text == val)
|
||||
|
||||
def _cancel_button_callback(self):
|
||||
gui_app.set_modal_overlay(None)
|
||||
if self._on_close:
|
||||
self._on_close(DialogResult.CANCEL, "")
|
||||
gui_app.set_modal_overlay(None)
|
||||
|
||||
def _confirm_button_callback(self):
|
||||
gui_app.set_modal_overlay(None)
|
||||
if self._on_close:
|
||||
self._on_close(DialogResult.CONFIRM, self._selected_value)
|
||||
gui_app.set_modal_overlay(None)
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
|
||||
Reference in New Issue
Block a user