diff --git a/panda/board/obj/gitversion.h b/panda/board/obj/gitversion.h index fe719e47f..0efaedafc 100644 --- a/panda/board/obj/gitversion.h +++ b/panda/board/obj/gitversion.h @@ -1,2 +1,2 @@ extern const uint8_t gitversion[19]; -const uint8_t gitversion[19] = "DEV-541c4d47-DEBUG"; +const uint8_t gitversion[19] = "DEV-66f064d1-DEBUG"; diff --git a/panda/board/obj/version b/panda/board/obj/version index e953009ed..ac8a0db10 100644 --- a/panda/board/obj/version +++ b/panda/board/obj/version @@ -1 +1 @@ -DEV-541c4d47-DEBUG \ No newline at end of file +DEV-66f064d1-DEBUG \ No newline at end of file diff --git a/scripts/launch_onroad_desktop.sh b/scripts/launch_onroad_desktop.sh index 8cbf3f9ed..55b45b3f9 100755 --- a/scripts/launch_onroad_desktop.sh +++ b/scripts/launch_onroad_desktop.sh @@ -265,6 +265,14 @@ params.put_bool("ForceOffroad", False) PY } +seed_starpilot_theme() { + "${ROOT_DIR}/.venv/bin/python3" - <<'PY' +from openpilot.starpilot.common.starpilot_functions import seed_desktop_theme_assets + +seed_desktop_theme_assets() +PY +} + build_replay() { SP_DISABLE_AUTO_DEVICE_SCONS=1 "${ROOT_DIR}/.venv/bin/scons" --extras -j"${jobs}" tools/replay/replay } @@ -364,6 +372,7 @@ case " ${UI_TARGETS[*]-} " in esac seed_params +seed_starpilot_theme echo "Starting replay: ${REPLAY_ARGS[*]}" launch_replay diff --git a/scripts/launch_ui_c4_desktop.sh b/scripts/launch_ui_c4_desktop.sh index 6479fd48a..e9ff7ea45 100755 --- a/scripts/launch_ui_c4_desktop.sh +++ b/scripts/launch_ui_c4_desktop.sh @@ -266,6 +266,14 @@ start_fake_wifi() { FAKE_WIFI_PID=$! } +seed_starpilot_theme() { + "${PY_BIN}" - <<'PY' +from openpilot.starpilot.common.starpilot_functions import seed_desktop_theme_assets + +seed_desktop_theme_assets() +PY +} + if ! python_ui_runtime_ok >/dev/null 2>&1; then echo "Preparing host Python UI runtime extensions..." sync_deps @@ -329,6 +337,7 @@ params.put_bool("OpenpilotEnabledToggle", True) params.put_bool("IsDriverViewEnabled", False) PY +seed_starpilot_theme kill_stale_c4_ui start_fake_wifi "${PY_BIN}" selfdrive/ui/ui.py "$@" diff --git a/scripts/launch_ui_desktop.sh b/scripts/launch_ui_desktop.sh index 8676d6b4b..8ec935e09 100755 --- a/scripts/launch_ui_desktop.sh +++ b/scripts/launch_ui_desktop.sh @@ -120,6 +120,14 @@ start_fake_wifi() { FAKE_WIFI_PID=$! } +seed_starpilot_theme() { + "${ROOT_DIR}/.venv/bin/python3" - <<'PY' +from openpilot.starpilot.common.starpilot_functions import seed_desktop_theme_assets + +seed_desktop_theme_assets() +PY +} + stop_fake_wifi() { if [[ -n "${FAKE_WIFI_PID}" ]]; then kill "${FAKE_WIFI_PID}" >/dev/null 2>&1 || true @@ -225,6 +233,7 @@ if [[ "${SP_C3_COMPILE_ONLY:-0}" == "1" ]]; then exit 0 fi +seed_starpilot_theme start_fake_wifi trap stop_fake_wifi EXIT "${HOST_UI}" "$@" diff --git a/scripts/launch_ui_raybig_desktop.sh b/scripts/launch_ui_raybig_desktop.sh index f2d1aec4c..885bd2dab 100755 --- a/scripts/launch_ui_raybig_desktop.sh +++ b/scripts/launch_ui_raybig_desktop.sh @@ -266,6 +266,14 @@ start_fake_wifi() { FAKE_WIFI_PID=$! } +seed_starpilot_theme() { + "${PY_BIN}" - <<'PY' +from openpilot.starpilot.common.starpilot_functions import seed_desktop_theme_assets + +seed_desktop_theme_assets() +PY +} + if ! python_ui_runtime_ok >/dev/null 2>&1; then echo "Preparing host Python UI runtime extensions..." sync_deps @@ -329,6 +337,7 @@ params.put_bool("OpenpilotEnabledToggle", True) params.put_bool("IsDriverViewEnabled", False) PY +seed_starpilot_theme kill_stale_raybig_ui start_fake_wifi "${PY_BIN}" selfdrive/ui/ui.py "$@" diff --git a/selfdrive/ui/lib/starpilot_status.py b/selfdrive/ui/lib/starpilot_status.py new file mode 100644 index 000000000..092d79ea6 --- /dev/null +++ b/selfdrive/ui/lib/starpilot_status.py @@ -0,0 +1,76 @@ +import pyray as rl + +from openpilot.selfdrive.ui.ui_state import UIStatus, UIState + +CEM_DISABLED_OVERRIDE_STATUSES = {1} +CEM_MANUAL_OVERRIDE_STATUSES = {1, 2} +CEM_ACTIVE_STATUSES = {3, 4, 5, 6, 7, 8} + +DISENGAGED_COLOR = rl.Color(18, 40, 57, 255) +AOL_COLOR = rl.Color(10, 186, 181, 255) +ENGAGED_COLOR = rl.Color(22, 127, 64, 255) +OVERRIDE_COLOR = rl.Color(137, 146, 141, 255) +EXPERIMENTAL_COLOR = rl.Color(218, 111, 37, 255) +CEM_OVERRIDE_COLOR = rl.Color(255, 214, 0, 255) +SWITCHBACK_COLOR = rl.Color(139, 108, 197, 255) +TRAFFIC_COLOR = rl.Color(201, 34, 49, 255) + + +def get_border_color(state: UIState): + enabled = state.sm["selfdriveState"].enabled + lateral_active = enabled or state.always_on_lateral_active + if state.status == UIStatus.OVERRIDE: + return OVERRIDE_COLOR + if state.switchback_mode_enabled and lateral_active: + return SWITCHBACK_COLOR + if state.traffic_mode_enabled and enabled: + return TRAFFIC_COLOR + if state.always_on_lateral_active: + return AOL_COLOR + # Only color the border for CEM/experimental while actually enabled. + if enabled and state.conditional_status in CEM_DISABLED_OVERRIDE_STATUSES: + return CEM_OVERRIDE_COLOR + if enabled and state.sm["selfdriveState"].experimentalMode: + return EXPERIMENTAL_COLOR + if state.status == UIStatus.ENGAGED: + return ENGAGED_COLOR + return DISENGAGED_COLOR + + +def get_path_edge_color(state: UIState): + if state.conditional_status in CEM_ACTIVE_STATUSES: + return EXPERIMENTAL_COLOR + return get_border_color(state) + + +def get_screen_edge_color(state: UIState): + enabled = state.sm["selfdriveState"].enabled + lateral_active = enabled or state.always_on_lateral_active + if state.status == UIStatus.OVERRIDE: + return OVERRIDE_COLOR + if state.switchback_mode_enabled and lateral_active: + return SWITCHBACK_COLOR + if state.always_on_lateral_active: + return AOL_COLOR + if state.conditional_status in CEM_DISABLED_OVERRIDE_STATUSES: + return CEM_OVERRIDE_COLOR + if state.sm["selfdriveState"].experimentalMode: + return EXPERIMENTAL_COLOR + if state.traffic_mode_enabled and enabled: + return TRAFFIC_COLOR + return get_border_color(state) + + +def get_experimental_mode_banner_text(state: UIState): + conditional_enabled = state.params.get_bool("ConditionalExperimental") + + # With CEM enabled, only surface banner text for explicit manual override states. + # Automatic CEM transitions should only be reflected by path/border coloring. + if conditional_enabled: + if state.conditional_status in CEM_MANUAL_OVERRIDE_STATUSES: + return "OVERRIDDEN" + return None + + if state.sm["selfdriveState"].experimentalMode: + return "EXPERIMENTAL" + return "CHILL" diff --git a/selfdrive/ui/mici/onroad/starpilot_status.py b/selfdrive/ui/mici/onroad/starpilot_status.py index 20e1744c1..d9c6912e7 100644 --- a/selfdrive/ui/mici/onroad/starpilot_status.py +++ b/selfdrive/ui/mici/onroad/starpilot_status.py @@ -1,57 +1,35 @@ -import pyray as rl -from openpilot.selfdrive.ui.ui_state import UIStatus, UIState +from openpilot.selfdrive.ui.lib.starpilot_status import ( + AOL_COLOR, + CEM_ACTIVE_STATUSES, + CEM_DISABLED_OVERRIDE_STATUSES, + CEM_MANUAL_OVERRIDE_STATUSES, + CEM_OVERRIDE_COLOR, + DISENGAGED_COLOR, + ENGAGED_COLOR, + EXPERIMENTAL_COLOR, + OVERRIDE_COLOR, + SWITCHBACK_COLOR, + TRAFFIC_COLOR, + get_border_color, + get_experimental_mode_banner_text, + get_path_edge_color, + get_screen_edge_color, +) -CEM_DISABLED_OVERRIDE_STATUSES = {1} -CEM_MANUAL_OVERRIDE_STATUSES = {1, 2} -CEM_ACTIVE_STATUSES = {3, 4, 5, 6, 7, 8} - -DISENGAGED_COLOR = rl.Color(18, 40, 57, 255) -AOL_COLOR = rl.Color(10, 186, 181, 255) -ENGAGED_COLOR = rl.Color(22, 127, 64, 255) -OVERRIDE_COLOR = rl.Color(137, 146, 141, 255) -EXPERIMENTAL_COLOR = rl.Color(218, 111, 37, 255) -CEM_OVERRIDE_COLOR = rl.Color(255, 214, 0, 255) -SWITCHBACK_COLOR = rl.Color(139, 108, 197, 255) -TRAFFIC_COLOR = rl.Color(201, 34, 49, 255) - - -def get_border_color(state: UIState): - enabled = state.sm["selfdriveState"].enabled - lateral_active = enabled or state.always_on_lateral_active - if state.status == UIStatus.OVERRIDE: - return OVERRIDE_COLOR - if state.switchback_mode_enabled and lateral_active: - return SWITCHBACK_COLOR - if state.traffic_mode_enabled and enabled: - return TRAFFIC_COLOR - if state.always_on_lateral_active: - return AOL_COLOR - # Only color the border for CEM/experimental while actually enabled. - if enabled and state.conditional_status in CEM_DISABLED_OVERRIDE_STATUSES: - return CEM_OVERRIDE_COLOR - if enabled and state.sm["selfdriveState"].experimentalMode: - return EXPERIMENTAL_COLOR - if state.status == UIStatus.ENGAGED: - return ENGAGED_COLOR - return DISENGAGED_COLOR - - -def get_path_edge_color(state: UIState): - if state.conditional_status in CEM_ACTIVE_STATUSES: - return EXPERIMENTAL_COLOR - return get_border_color(state) - - -def get_experimental_mode_banner_text(state: UIState): - conditional_enabled = state.params.get_bool("ConditionalExperimental") - - # With CEM enabled, only surface banner text for explicit manual override states. - # Automatic CEM transitions should only be reflected by path/border coloring. - if conditional_enabled: - if state.conditional_status in CEM_MANUAL_OVERRIDE_STATUSES: - return "OVERRIDDEN" - return None - - if state.sm["selfdriveState"].experimentalMode: - return "EXPERIMENTAL" - return "CHILL" +__all__ = [ + "AOL_COLOR", + "CEM_ACTIVE_STATUSES", + "CEM_DISABLED_OVERRIDE_STATUSES", + "CEM_MANUAL_OVERRIDE_STATUSES", + "CEM_OVERRIDE_COLOR", + "DISENGAGED_COLOR", + "ENGAGED_COLOR", + "EXPERIMENTAL_COLOR", + "OVERRIDE_COLOR", + "SWITCHBACK_COLOR", + "TRAFFIC_COLOR", + "get_border_color", + "get_experimental_mode_banner_text", + "get_path_edge_color", + "get_screen_edge_color", +] diff --git a/selfdrive/ui/onroad/augmented_road_view.py b/selfdrive/ui/onroad/augmented_road_view.py index 913ca3c98..674a37a2d 100644 --- a/selfdrive/ui/onroad/augmented_road_view.py +++ b/selfdrive/ui/onroad/augmented_road_view.py @@ -11,6 +11,7 @@ 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.lib.starpilot_status import get_screen_edge_color from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCameraConfig, view_frame_from_device_frame from openpilot.common.transformations.orientation import rot_from_euler @@ -215,7 +216,7 @@ class AugmentedRoadView(CameraView): 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_color = get_screen_edge_color(ui_state) 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) diff --git a/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py b/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py index 6381f9986..e3a05e866 100644 --- a/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py +++ b/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py @@ -3,7 +3,7 @@ import time from msgq.visionipc import VisionStreamType from openpilot.common.params import Params from openpilot.selfdrive.ui import UI_BORDER_SIZE -from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView, BORDER_COLORS +from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView from openpilot.selfdrive.ui.onroad.starpilot.curve_speed_border import render_glow, render_filament from openpilot.selfdrive.ui.onroad.starpilot.path import render_adjacent_paths, render_blind_spot_path, render_path_edges from openpilot.selfdrive.ui.onroad.starpilot.personality_button import PersonalityButton, BTN_SIZE @@ -11,11 +11,10 @@ from openpilot.selfdrive.ui.onroad.starpilot.slc_speed_limit import ( render_speed_limit, handle_slc_click, SET_SPEED_X_OFFSET, SET_SPEED_Y_OFFSET, SET_SPEED_WIDTH_IMP, SET_SPEED_WIDTH_MET, SET_SPEED_HEIGHT, SIGN_MARGIN, ) -from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.lib.starpilot_status import get_screen_edge_color from openpilot.system.ui.lib.application import MousePos, gui_app, FontWeight -AOL_COLOR = rl.Color(10, 186, 181, 255) - class StarPilotOnroadView(AugmentedRoadView): def __init__(self, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_ROAD): @@ -181,7 +180,7 @@ class StarPilotOnroadView(AugmentedRoadView): render_glow(border_rect) # Layer 4: Standard border - border_color = AOL_COLOR if ui_state.always_on_lateral_active else BORDER_COLORS.get(ui_state.status, BORDER_COLORS[UIStatus.DISENGAGED]) + border_color = get_screen_edge_color(ui_state) rl.draw_rectangle_rounded_lines_ex(border_rect, 0.12, 10, UI_BORDER_SIZE, border_color) # Layer 5: Amber filament (on top of standard border) diff --git a/selfdrive/ui/qt/onroad/buttons.cc b/selfdrive/ui/qt/onroad/buttons.cc index a9dd36f30..efae2f690 100644 --- a/selfdrive/ui/qt/onroad/buttons.cc +++ b/selfdrive/ui/qt/onroad/buttons.cc @@ -20,7 +20,7 @@ void drawIcon(QPainter &p, const QPoint ¢er, const QPixmap &img, const QBrus } // ExperimentalButton -ExperimentalButton::ExperimentalButton(QWidget *parent) : experimental_mode(false), engageable(false), QPushButton(parent) { +ExperimentalButton::ExperimentalButton(QWidget *parent) : QPushButton(parent), experimental_mode(false), engageable(false), steering_angle_deg(0) { setFixedSize(btn_size, btn_size); engage_img = loadPixmap("../assets/icons/chffr_wheel.png", {img_size, img_size}); @@ -86,6 +86,9 @@ void ExperimentalButton::paintEvent(QPaintEvent *event) { drawIcon(p, QPoint(btn_size / 2, btn_size / 2), wheel_gif->currentPixmap(), background_color, (isDown() || !engageable) ? 0.6 : 1.0, steering_angle_deg); } else if (!wheel_img.isNull()) { drawIcon(p, QPoint(btn_size / 2, btn_size / 2), wheel_img, background_color, (isDown() || !engageable) ? 0.6 : 1.0, steering_angle_deg); + } else { + QPixmap img = experimental_mode ? experimental_img : engage_img; + drawIcon(p, QPoint(btn_size / 2, btn_size / 2), img, background_color, (isDown() || !engageable) ? 0.6 : 1.0, steering_angle_deg); } } @@ -115,4 +118,7 @@ void ExperimentalButton::updateBackgroundColor() { void ExperimentalButton::updateTheme() { loadImage("../../starpilot/assets/active_theme/steering_wheel/wheel", wheel_img, wheel_gif, QSize(img_size, img_size), this); + if (!wheel_gif && wheel_img.isNull()) { + loadImage("../../starpilot/assets/stock_theme/steering_wheel/wheel", wheel_img, wheel_gif, QSize(img_size, img_size), this); + } } diff --git a/selfdrive/ui/qt/onroad/onroad_home.cc b/selfdrive/ui/qt/onroad/onroad_home.cc index f62823434..17e8b0322 100644 --- a/selfdrive/ui/qt/onroad/onroad_home.cc +++ b/selfdrive/ui/qt/onroad/onroad_home.cc @@ -58,14 +58,27 @@ void OnroadWindow::updateState(const UIState &s, const StarPilotUIState &fs) { alerts->updateState(s, fs); nvg->updateState(s, fs); + const StarPilotUIScene &starpilot_scene = fs.starpilot_scene; + const auto selfdriveState = (*s.sm)["selfdriveState"].getSelfdriveState(); QColor bgColor = bg_colors[s.status]; + if (starpilot_scene.switchback_mode_enabled && (selfdriveState.getEnabled() || starpilot_scene.always_on_lateral_active)) { + bgColor = bg_colors[STATUS_SWITCHBACK_MODE_ENABLED]; + } else if (starpilot_scene.always_on_lateral_active) { + bgColor = bg_colors[STATUS_ALWAYS_ON_LATERAL_ACTIVE]; + } else if (starpilot_scene.conditional_status == 1) { + bgColor = bg_colors[STATUS_CEM_DISABLED]; + } else if (selfdriveState.getExperimentalMode()) { + bgColor = bg_colors[STATUS_EXPERIMENTAL_MODE_ENABLED]; + } else if (starpilot_scene.traffic_mode_enabled && selfdriveState.getEnabled()) { + bgColor = bg_colors[STATUS_TRAFFIC_MODE_ENABLED]; + } + if (bg != bgColor) { // repaint border bg = bgColor; update(); } - const StarPilotUIScene &starpilot_scene = fs.starpilot_scene; const QJsonObject &starpilot_toggles = starpilot_scene.starpilot_toggles; starpilot_nvg->alertHeight = alerts->alertHeight; diff --git a/selfdrive/ui/ui b/selfdrive/ui/ui index 98c46a226..76a5bb84e 100755 Binary files a/selfdrive/ui/ui and b/selfdrive/ui/ui differ diff --git a/starpilot/assets/theme_manager.py b/starpilot/assets/theme_manager.py index 06c4a4e5e..6f2687a1a 100644 --- a/starpilot/assets/theme_manager.py +++ b/starpilot/assets/theme_manager.py @@ -1,9 +1,13 @@ #!/usr/bin/env python3 import glob +import io import json +import os import random import requests import shutil +import subprocess +import zipfile from datetime import date, timedelta from dateutil import easter @@ -19,6 +23,7 @@ DOWNLOAD_PROGRESS_PARAM = "ThemeDownloadProgress" HOLIDAY_THEME_PATH = Path(__file__).parent / "holiday_themes" STOCKOP_THEME_PATH = Path(__file__).parent / "stock_theme" +LOCAL_RESOURCES_PATH = Path(os.getenv("STARPILOT_LOCAL_RESOURCES_PATH", "~/StarPilot-Resources")).expanduser() HOLIDAY_SLUGS = { "new_years": "New Year's", @@ -64,6 +69,7 @@ class ThemeManager: (THEME_SAVE_PATH / "bootlogos").mkdir(parents=True, exist_ok=True) (THEME_SAVE_PATH / "theme_packs").mkdir(parents=True, exist_ok=True) (THEME_SAVE_PATH / "steering_wheels").mkdir(parents=True, exist_ok=True) + self.sync_local_resources() self.theme_sizes = load_json_file(self.theme_sizes_path) @@ -77,6 +83,96 @@ class ThemeManager: if boot_run: self.copy_default_theme() + @staticmethod + def _local_resources_available(): + return LOCAL_RESOURCES_PATH.is_dir() and (LOCAL_RESOURCES_PATH / ".git").exists() + + @staticmethod + def _git_list_tree(ref): + result = subprocess.run( + ["git", "-C", str(LOCAL_RESOURCES_PATH), "ls-tree", "-r", "--name-only", ref], + capture_output=True, + text=True, + check=True, + ) + return [line for line in result.stdout.splitlines() if line] + + @staticmethod + def _git_show_bytes(ref, path): + result = subprocess.run( + ["git", "-C", str(LOCAL_RESOURCES_PATH), "show", f"{ref}:{path}"], + capture_output=True, + check=True, + ) + return result.stdout + + @staticmethod + def _directory_has_files(path): + return path.is_dir() and any(path.iterdir()) + + @staticmethod + def _write_file_if_missing(destination, data): + if destination.exists() and destination.stat().st_size > 0: + return False + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_bytes(data) + return True + + @staticmethod + def _extract_zip_if_missing(zip_bytes, destination): + if ThemeManager._directory_has_files(destination): + return False + destination.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(io.BytesIO(zip_bytes)) as archive: + archive.extractall(destination) + return True + + def sync_local_resources(self): + if not self._local_resources_available(): + return + + try: + imported_assets = 0 + + for path in self._git_list_tree("Steering-Wheels"): + suffix = Path(path).suffix.lower() + if suffix not in {".gif", ".png", ".webp", ".jpg", ".jpeg"}: + continue + destination = THEME_SAVE_PATH / "steering_wheels" / Path(path).name + imported_assets += int(self._write_file_if_missing(destination, self._git_show_bytes("Steering-Wheels", path))) + + for path in self._git_list_tree("Themes"): + path_obj = Path(path) + suffix = path_obj.suffix.lower() + + if path_obj.parts[:1] == ("bootlogo",) and suffix in {".png", ".jpg", ".jpeg"}: + destination = THEME_SAVE_PATH / "bootlogos" / path_obj.name + imported_assets += int(self._write_file_if_missing(destination, self._git_show_bytes("Themes", path))) + continue + + if suffix != ".zip" or len(path_obj.parts) != 2: + continue + + theme_name, archive_name = path_obj.parts + component = Path(archive_name).stem.lower() + if component not in {"colors", "distance_icons", "icons", "signals", "sounds"}: + continue + + destination = THEME_SAVE_PATH / "theme_packs" / theme_name / component + imported_assets += int(self._extract_zip_if_missing(self._git_show_bytes("Themes", path), destination)) + + for path in self._git_list_tree("Distance-Icons"): + path_obj = Path(path) + if path_obj.suffix.lower() != ".zip": + continue + destination = THEME_SAVE_PATH / "theme_packs" / path_obj.stem / "distance_icons" + imported_assets += int(self._extract_zip_if_missing(self._git_show_bytes("Distance-Icons", path), destination)) + + if imported_assets: + print(f"Imported {imported_assets} local theme assets from {LOCAL_RESOURCES_PATH}") + except (FileNotFoundError, subprocess.CalledProcessError, zipfile.BadZipFile, OSError) as error: + print(f"Failed to sync local theme resources from {LOCAL_RESOURCES_PATH}: {error}") + @staticmethod def calculate_thanksgiving(year): november_first = date(year, 11, 1) @@ -645,9 +741,12 @@ class ThemeManager: if self.downloading_theme: return + self.sync_local_resources() + repo_url = get_repository_url(self.session) if repo_url is None: print("GitHub and GitLab are offline...") + self.update_theme_params([], [], [], [], [], [], []) return assets = self.fetch_assets(repo_url, starpilot_toggles) @@ -734,11 +833,20 @@ class ThemeManager: image_name = image.replace(" ", "_").lower() matching_files = [images for images in wheel_location.iterdir() if images.stem.lower() in {image_name, "wheel"}] - if matching_files: - source_file = matching_files[0] - destination_file = wheel_save_location / f"wheel{source_file.suffix}" - destination_file.symlink_to(source_file) - print(f"Linked {destination_file} to {source_file}") + if not matching_files: + stock_location = STOCKOP_THEME_PATH / "steering_wheel" + matching_files = [images for images in stock_location.iterdir() if images.stem.lower() == "wheel"] + if matching_files: + print(f"Steering wheel '{image}' not found, using the stock steering wheel instead") + + if not matching_files: + print(f"No steering wheel asset found for '{image}'") + return + + source_file = matching_files[0] + destination_file = wheel_save_location / f"wheel{source_file.suffix}" + destination_file.symlink_to(source_file) + print(f"Linked {destination_file} to {source_file}") def validate_themes(self, downloadable_boot_logos, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels, starpilot_toggles): downloaded_data = self.params.get("ThemesDownloaded") diff --git a/starpilot/common/starpilot_functions.py b/starpilot/common/starpilot_functions.py index f7be8f4b5..16354dd75 100644 --- a/starpilot/common/starpilot_functions.py +++ b/starpilot/common/starpilot_functions.py @@ -6,6 +6,7 @@ import threading import time from pathlib import Path +from types import SimpleNamespace from cereal import messaging from openpilot.common.basedir import BASEDIR @@ -25,6 +26,37 @@ from openpilot.starpilot.common.starpilot_variables import ( ) +def seed_desktop_theme_assets(): + params = Params() + params_memory = Params(memory=True) + params_defaults = Params(return_defaults=True) + theme_manager = ThemeManager(params, params_memory, boot_run=True) + + custom_themes = params_defaults.get_bool("CustomThemes") + random_themes = custom_themes and params_defaults.get_bool("RandomThemes") + + starpilot_toggles = SimpleNamespace( + boot_logo=params_defaults.get("BootLogo", encoding="utf-8", default="starpilot"), + holiday_themes=params_defaults.get_bool("HolidayThemes"), + random_themes=random_themes, + random_themes_holidays=random_themes and params_defaults.get_bool("RandomThemesHolidays"), + color_scheme=params_defaults.get("ColorScheme", encoding="utf-8", default="stock") if custom_themes else "stock", + distance_icons=params_defaults.get("DistanceIconPack", encoding="utf-8", default="stock") if custom_themes else "stock", + icon_pack=params_defaults.get("IconPack", encoding="utf-8", default="stock") if custom_themes else "stock", + sound_pack=params_defaults.get("SoundPack", encoding="utf-8", default="stock") if custom_themes else "stock", + signal_icons=params_defaults.get("SignalAnimation", encoding="utf-8", default="stock") if custom_themes else "stock", + wheel_image=params_defaults.get("WheelIcon", encoding="utf-8", default="stock") if custom_themes else "stock", + ) + + theme_manager.update_active_theme( + time_validated=system_time_valid(), + starpilot_toggles=starpilot_toggles, + boot_run=True, + ) + theme_manager.update_theme_asset("distance_icons", starpilot_toggles.distance_icons, boot_run=True) + theme_manager.update_wheel_image(starpilot_toggles.wheel_image, boot_run=True) + + def starpilot_boot_functions(build_metadata, params): params_memory = Params(memory=True) diff --git a/starpilot/ui/qt/offroad/theme_settings.cc b/starpilot/ui/qt/offroad/theme_settings.cc index bb0554429..7fe0a394e 100644 --- a/starpilot/ui/qt/offroad/theme_settings.cc +++ b/starpilot/ui/qt/offroad/theme_settings.cc @@ -1,4 +1,5 @@ -#include "starpilot/ui/qt/offroad/theme_settings.h" +#include "starpilot/ui/qt/offroad/theme_settings.h" +#include "system/hardware/hw.h" bool isUserCreatedTheme(const QString &themeName) { return themeName.endsWith("-user_created"); @@ -207,10 +208,21 @@ QString storeThemeName(const QString &input, const std::string ¶mKey, Params return getThemeName(paramKey, params); } -StarPilotThemesPanel::StarPilotThemesPanel(StarPilotSettingsWindow *parent, bool forceOpen) : StarPilotListWidget(parent), parent(parent) { - forceOpenDescriptions = forceOpen; - - QStackedLayout *themesLayout = new QStackedLayout(); +StarPilotThemesPanel::StarPilotThemesPanel(StarPilotSettingsWindow *parent, bool forceOpen) : StarPilotListWidget(parent), parent(parent) { + forceOpenDescriptions = forceOpen; + + if (Hardware::PC()) { + const QString themesRoot = QString::fromStdString(Path::comma_home() + "/starpilot/data/themes/"); + bootLogosDirectory.setPath(themesRoot + "bootlogos/"); + themePacksDirectory.setPath(themesRoot + "theme_packs/"); + wheelsDirectory.setPath(themesRoot + "steering_wheels/"); + } + + QDir().mkpath(bootLogosDirectory.path()); + QDir().mkpath(themePacksDirectory.path()); + QDir().mkpath(wheelsDirectory.path()); + + QStackedLayout *themesLayout = new QStackedLayout(); addItem(themesLayout); StarPilotListWidget *themesList = new StarPilotListWidget(this); diff --git a/starpilot/ui/qt/widgets/starpilot_controls.cc b/starpilot/ui/qt/widgets/starpilot_controls.cc index c02e645d7..21ff31b13 100644 --- a/starpilot/ui/qt/widgets/starpilot_controls.cc +++ b/starpilot/ui/qt/widgets/starpilot_controls.cc @@ -65,16 +65,25 @@ void loadGif(const QString &gifPath, QSharedPointer &movie, const QSize movie->start(); } +static QString resolveImagePath(const QString &basePath, const QStringList &extensions) { + for (const QString &extension : extensions) { + const QString candidate = basePath + "." + extension; + if (QFileInfo::exists(candidate)) { + return candidate; + } + } + + return {}; +} + void loadImage(const QString &basePath, QPixmap &pixmap, QSharedPointer &movie, const QSize &size, QWidget *parent) { if (!parent || basePath.isEmpty()) { return; } static QHash pixmapCache; - QString cacheKey = basePath + QString("_%1x%2").arg(size.width()).arg(size.height()); - - QString gifPath = basePath + ".gif"; - if (QFileInfo::exists(gifPath)) { + const QString gifPath = resolveImagePath(basePath, {"gif"}); + if (!gifPath.isEmpty()) { loadGif(gifPath, movie, size, parent); if (!pixmap.isNull()) { pixmap = QPixmap(); @@ -84,6 +93,18 @@ void loadImage(const QString &basePath, QPixmap &pixmap, QSharedPointer clearMovie(movie, parent); + const QString imagePath = resolveImagePath(basePath, {"png", "webp", "jpg", "jpeg"}); + if (imagePath.isEmpty()) { + if (!pixmap.isNull()) { + pixmap = QPixmap(); + parent->update(); + } + return; + } + + const QFileInfo imageInfo(imagePath); + const QString cacheKey = imagePath + QString("_%1x%2_%3").arg(size.width()).arg(size.height()).arg(imageInfo.lastModified().toMSecsSinceEpoch()); + if (pixmapCache.contains(cacheKey)) { QPixmap &cached = pixmapCache[cacheKey]; if (pixmap.cacheKey() != cached.cacheKey()) { @@ -93,16 +114,7 @@ void loadImage(const QString &basePath, QPixmap &pixmap, QSharedPointer return; } - QString pngPath = basePath + ".png"; - if (!QFileInfo::exists(pngPath)) { - if (!pixmap.isNull()) { - pixmap = QPixmap(); - parent->update(); - } - return; - } - - QPixmap loadedPixmap(pngPath); + QPixmap loadedPixmap(imagePath); if (!loadedPixmap.isNull()) { pixmap = loadedPixmap.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); pixmapCache.insert(cacheKey, pixmap);