mirror of
https://github.com/MoreTore/openpilot.git
synced 2026-08-05 08:16:06 +08:00
UI Replay
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -1 +1 @@
|
||||
DEV-541c4d47-DEBUG
|
||||
DEV-66f064d1-DEBUG
|
||||
@@ -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
|
||||
|
||||
@@ -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 "$@"
|
||||
|
||||
@@ -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}" "$@"
|
||||
|
||||
@@ -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 "$@"
|
||||
|
||||
@@ -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"
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Binary file not shown.
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -65,16 +65,25 @@ void loadGif(const QString &gifPath, QSharedPointer<QMovie> &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<QMovie> &movie, const QSize &size, QWidget *parent) {
|
||||
if (!parent || basePath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
static QHash<QString, QPixmap> 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<QMovie>
|
||||
|
||||
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<QMovie>
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user