mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-08-21 08:14:00 +08:00
September 27th, 2025 Update
This commit is contained in:
Binary file not shown.
@@ -2,18 +2,21 @@ import json
|
||||
import os
|
||||
import random
|
||||
import requests
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "third_party"))
|
||||
|
||||
from collections import Counter
|
||||
from datetime import datetime, timezone
|
||||
from influxdb_client import InfluxDBClient, Point
|
||||
from influxdb_client.client.write_api import SYNCHRONOUS
|
||||
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.version import get_build_metadata
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import run_cmd
|
||||
from openpilot.frogpilot.common.frogpilot_variables import get_frogpilot_toggles, params, params_tracking
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import clean_model_name, run_cmd
|
||||
from openpilot.frogpilot.common.frogpilot_variables import get_frogpilot_toggles, params
|
||||
|
||||
BASE_URL = "https://nominatim.openstreetmap.org"
|
||||
MINIMUM_POPULATION = 100_000
|
||||
@@ -94,23 +97,11 @@ def get_city_center(latitude, longitude):
|
||||
print(f"Falling back to (0, 0) for {latitude}, {longitude}")
|
||||
return float(0.0), float(0.0), "N/A", "N/A", "N/A"
|
||||
|
||||
def install_influxdb_client():
|
||||
try:
|
||||
import influxdb_client
|
||||
import influxdb_client.client.write_api
|
||||
except ModuleNotFoundError:
|
||||
print("influxdb-client not found. Attempting installation...")
|
||||
stock_mount_options = subprocess.run(["findmnt", "-no", "OPTIONS", "/"], capture_output=True, text=True, check=True).stdout.strip()
|
||||
|
||||
run_cmd(["sudo", "mount", "-o", "remount,rw", "/"], "Successfully remounted / as read-write", "Failed to remount / as read-write", report=False)
|
||||
run_cmd(["sudo", sys.executable, "-m", "pip", "install", "influxdb-client"], "Successfully installed influxdb-client", "Failed to install influxdb-client", report=False)
|
||||
run_cmd(["sudo", "mount", "-o", f"remount,{stock_mount_options}", "/"], "Successfully restored stock mount options", "Failed to restore stock mount options", report=False)
|
||||
|
||||
def is_up_to_date(build_metadata):
|
||||
remote_commit = subprocess.check_output(["git", "ls-remote", "origin", build_metadata.channel], text=True, stderr=subprocess.DEVNULL).strip()
|
||||
remote_commit = run_cmd(["git", "ls-remote", "origin", build_metadata.channel], f"Fetched remote commit", "Failed to fetch remote commit", report=False)
|
||||
|
||||
if remote_commit:
|
||||
return build_metadata.openpilot.git_commit == remote_commit.split()[0]
|
||||
return build_metadata.openpilot.git_commit == remote_commit.strip().split()[0]
|
||||
|
||||
return True
|
||||
|
||||
@@ -125,11 +116,6 @@ def send_stats():
|
||||
if frogpilot_toggles.car_make == "mock":
|
||||
return
|
||||
|
||||
install_influxdb_client()
|
||||
|
||||
from influxdb_client import InfluxDBClient, Point
|
||||
from influxdb_client.client.write_api import SYNCHRONOUS
|
||||
|
||||
bucket = os.environ.get("STATS_BUCKET", "")
|
||||
org_ID = os.environ.get("STATS_ORG_ID", "")
|
||||
token = os.environ.get("STATS_TOKEN", "")
|
||||
@@ -159,16 +145,18 @@ def send_stats():
|
||||
selected_theme = random.choice([item for item, count in most_common if count == max_count]).replace("-user_created", "").replace("_", " ")
|
||||
|
||||
point = (Point("user_stats")
|
||||
.field("blocked_user", frogpilot_toggles.block_user)
|
||||
.field("car_make", "GM" if frogpilot_toggles.car_make == "gm" else frogpilot_toggles.car_make.title())
|
||||
.field("car_model", frogpilot_toggles.car_model)
|
||||
.field("city", city)
|
||||
.field("country", country)
|
||||
.field("current_months_kilometers", int(frogpilot_stats.get("CurrentMonthsKilometers", 0)))
|
||||
.field("device", HARDWARE.get_device_type())
|
||||
.field("driving_model", frogpilot_toggles.model_name.replace("🗺️", "").replace("📡", "").replace("👀", "").replace("(Default)", "").strip())
|
||||
.field("driving_model", clean_model_name(frogpilot_toggles.model_name))
|
||||
.field("event", 1)
|
||||
.field("frogpilot_drives", params_tracking.get_int("FrogPilotDrives"))
|
||||
.field("frogpilot_hours", params_tracking.get_int("FrogPilotMinutes") / 60)
|
||||
.field("frogpilot_miles", params_tracking.get_int("FrogPilotKilometers") * CV.KPH_TO_MPH)
|
||||
.field("frogpilot_drives", int(frogpilot_stats.get("FrogPilotDrives", 0)))
|
||||
.field("frogpilot_hours", float(frogpilot_stats.get("FrogPilotSeconds", 0)) / (60 * 60))
|
||||
.field("frogpilot_miles", float(frogpilot_stats.get("FrogPilotMeters", 0)) * CV.METER_TO_MILE)
|
||||
.field("goat_scream", frogpilot_toggles.goat_scream_alert)
|
||||
.field("has_cc_long", frogpilot_toggles.has_cc_long)
|
||||
.field("has_openpilot_longitudinal", frogpilot_toggles.openpilot_longitudinal)
|
||||
@@ -181,10 +169,10 @@ def send_stats():
|
||||
.field("random_events", frogpilot_toggles.random_events)
|
||||
.field("state", state)
|
||||
.field("theme", selected_theme.title())
|
||||
.field("total_aol_seconds", float(frogpilot_stats.get("TotalAOLTime", 0)))
|
||||
.field("total_lateral_seconds", float(frogpilot_stats.get("TotalLateralTime", 0)))
|
||||
.field("total_longitudinal_seconds", float(frogpilot_stats.get("TotalLongitudinalTime", 0)))
|
||||
.field("total_tracked_seconds", float(frogpilot_stats.get("TotalTrackedTime", 0)))
|
||||
.field("total_aol_seconds", float(frogpilot_stats.get("AOLTime", 0)))
|
||||
.field("total_lateral_seconds", float(frogpilot_stats.get("LateralTime", 0)))
|
||||
.field("total_longitudinal_seconds", float(frogpilot_stats.get("LongitudinalTime", 0)))
|
||||
.field("total_tracked_seconds", float(frogpilot_stats.get("TrackedTime", 0)))
|
||||
.field("tuning_level", params.get_int("TuningLevel") + 1 if params.get_bool("TuningLevelConfirmed") else 0)
|
||||
.field("up_to_date", is_up_to_date(build_metadata))
|
||||
.field("using_stock_acc", not (frogpilot_toggles.has_cc_long or frogpilot_toggles.openpilot_longitudinal))
|
||||
|
||||
@@ -7,8 +7,6 @@ import time
|
||||
from collections import OrderedDict, deque
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import openpilot.system.sentry as sentry
|
||||
|
||||
from cereal import log, messaging
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import calculate_distance_to_point, calculate_lane_width, is_url_pingable
|
||||
@@ -126,7 +124,7 @@ class MapSpeedLogger:
|
||||
if self.should_stop_processing:
|
||||
return False
|
||||
|
||||
time.sleep(10)
|
||||
time.sleep(5)
|
||||
return True
|
||||
|
||||
def fetch_from_overpass(self, latitude, longitude):
|
||||
@@ -379,35 +377,29 @@ def main():
|
||||
previously_started = False
|
||||
|
||||
while True:
|
||||
try:
|
||||
logger.sm.update()
|
||||
logger.sm.update()
|
||||
|
||||
if logger.sm["deviceState"].started:
|
||||
logger.log_speed_limit()
|
||||
if logger.sm["deviceState"].started:
|
||||
logger.log_speed_limit()
|
||||
|
||||
previously_started = True
|
||||
elif previously_started:
|
||||
existing_dataset = json.loads(params.get("SpeedLimits") or "[]")
|
||||
existing_dataset.extend(logger.dataset_additions)
|
||||
previously_started = True
|
||||
elif previously_started:
|
||||
existing_dataset = json.loads(params.get("SpeedLimits") or "[]")
|
||||
existing_dataset.extend(logger.dataset_additions)
|
||||
|
||||
new_dataset = logger.cleanup_dataset(existing_dataset)
|
||||
params.put("SpeedLimits", json.dumps(list(new_dataset)))
|
||||
new_dataset = logger.cleanup_dataset(existing_dataset)
|
||||
params.put("SpeedLimits", json.dumps(list(new_dataset)))
|
||||
|
||||
if logger.sm["deviceState"].networkType in (NetworkType.ethernet, NetworkType.wifi):
|
||||
params_memory.put_bool("UpdateSpeedLimits", True)
|
||||
if logger.sm["deviceState"].networkType in (NetworkType.ethernet, NetworkType.wifi):
|
||||
params_memory.put_bool("UpdateSpeedLimits", True)
|
||||
|
||||
logger.dataset_additions.clear()
|
||||
logger.dataset_additions.clear()
|
||||
|
||||
previously_started = False
|
||||
elif params_memory.get_bool("UpdateSpeedLimits"):
|
||||
logger.process_speed_limits()
|
||||
else:
|
||||
time.sleep(5)
|
||||
|
||||
except Exception as exception:
|
||||
print(f"Error in speed_limit_filler: {exception}")
|
||||
sentry.capture_exception(exception)
|
||||
time.sleep(1)
|
||||
previously_started = False
|
||||
elif params_memory.get_bool("UpdateSpeedLimits"):
|
||||
logger.process_speed_limits()
|
||||
else:
|
||||
time.sleep(5)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -782,7 +782,7 @@ function NavigationDestination({
|
||||
if (fav) {
|
||||
removeFavorite(fav);
|
||||
} else {
|
||||
showSnackbar("Couldn’t find favorite entry…");
|
||||
showSnackbar("Couldn't find favorite entry…");
|
||||
}
|
||||
} else {
|
||||
await favoriteDestination();
|
||||
|
||||
@@ -227,7 +227,7 @@ async function openOverlay(route) {
|
||||
|
||||
downloadButton.onclick = () => {
|
||||
const link = document.createElement("a");
|
||||
const videoPath = `/video/${route.name}--${current}?camera=${selectedCamera}`;
|
||||
const videoPath = `/video/${route.name}/combined?camera=${selectedCamera}`;
|
||||
link.href = videoPath;
|
||||
link.download = `${route.timestamp}-${selectedCamera}.mp4`;
|
||||
document.body.appendChild(link);
|
||||
@@ -251,8 +251,7 @@ async function openOverlay(route) {
|
||||
vid.load();
|
||||
vid.play();
|
||||
} catch (error) {
|
||||
showSnackbar("Error: Could not load all route segments.", "error");
|
||||
segments = [`/video/${route.name}--0`];
|
||||
showSnackbar("Error: Could not load combined route video.", "error");
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -271,8 +270,7 @@ async function openOverlay(route) {
|
||||
overlay.querySelectorAll(".camera-button").forEach(btn => btn.classList.remove("active"));
|
||||
e.target.classList.add("active");
|
||||
selectedCamera = e.target.dataset.camera;
|
||||
const videoPath = segments[current].includes("?") ? `${segments[current]}&camera=${selectedCamera}` : `${segments[current]}?camera=${selectedCamera}`
|
||||
vid.src = videoPath;
|
||||
vid.src = segments[current].includes("?") ? `${segments[current]}&camera=${selectedCamera}` : `${segments[current]}?camera=${selectedCamera}`;
|
||||
vid.load();
|
||||
vid.play();
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ScreenRecordings } from "/assets/components/recordings/screen_recording
|
||||
import { Sidebar } from "/assets/components/sidebar.js"
|
||||
import { SpeedLimits } from "/assets/components/tools/speed_limits.js"
|
||||
import { TailscaleControl } from "/assets/components/tailscale/tailscale.js"
|
||||
import { ThemeMaker } from "/assets/components/tools/theme_maker.js"
|
||||
import { TmuxLog } from "/assets/components/tools/tmux.js"
|
||||
import { ToggleControl } from "/assets/components/tools/toggles.js"
|
||||
import { TSKManager } from "/assets/components/tools/tsk_manager.js"
|
||||
@@ -39,6 +40,7 @@ function Root() {
|
||||
createRoute("settings", "/settings/:section/:subsection?", SettingsView),
|
||||
createRoute("speed_limits", "/download_speed_limits", SpeedLimits),
|
||||
createRoute("tailscale", "/manage_tailscale", TailscaleControl),
|
||||
createRoute("thememaker", "/theme_maker", ThemeMaker),
|
||||
createRoute("tmux", "/manage_tmux", TmuxLog),
|
||||
createRoute("toggles", "/manage_toggles", ToggleControl),
|
||||
createRoute("tsk_manager", "/tsk_manager", TSKManager),
|
||||
|
||||
@@ -21,6 +21,7 @@ const MenuItems = {
|
||||
{ name: "Download Speed Limits", link: "/download_speed_limits", icon: "bi-download" },
|
||||
{ name: "Error Logs", link: "/manage_error_logs", icon: "bi-exclamation-triangle" },
|
||||
{ name: "Lock/Unlock Doors", link: "/lock_or_unlock_doors", icon: "bi-door-closed" },
|
||||
{ name: "Theme Maker", link: "/theme_maker", icon: "bi-palette-fill" },
|
||||
{ name: "Tmux Log", link: "/manage_tmux", icon: "bi-terminal" },
|
||||
{ name: "Toggles", link: "/manage_toggles", icon: "bi-toggle-on" },
|
||||
{ name: "Toyota Security Keys", link: "/tsk_manager", icon: "bi-key-fill" },
|
||||
|
||||
@@ -0,0 +1,720 @@
|
||||
.apply-button {
|
||||
background-color: var(--accent-bg);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-lg);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
overflow: hidden;
|
||||
padding: 0.75rem 1.5rem;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.apply-button:hover {
|
||||
background-color: var(--accent-hover-bg);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.checklist-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-sm);
|
||||
margin-top: var(--margin-base);
|
||||
}
|
||||
|
||||
.checklist-item {
|
||||
align-items: center;
|
||||
background-color: var(--input-bg);
|
||||
border-radius: var(--border-radius-md);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
padding: var(--padding-sm);
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.checklist-item .custom-checkbox {
|
||||
background-color: var(--color-gray-600);
|
||||
border-radius: 13px;
|
||||
flex-shrink: 0;
|
||||
height: 26px;
|
||||
position: relative;
|
||||
transition: background-color var(--transition-fast);
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.checklist-item .custom-checkbox::before {
|
||||
background-color: var(--color-white);
|
||||
border-radius: 50%;
|
||||
content: "";
|
||||
height: 20px;
|
||||
left: 4px;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
transition: transform var(--transition-fast);
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.checklist-item .label-text {
|
||||
color: var(--text-color);
|
||||
flex-grow: 1;
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
padding-left: var(--padding-sm);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.checklist-item:hover {
|
||||
background-color: var(--main-fg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.checklist-item input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.checklist-item input[type="checkbox"]:checked ~ .custom-checkbox {
|
||||
background-color: var(--success-bg);
|
||||
}
|
||||
|
||||
.checklist-item input[type="checkbox"]:checked ~ .custom-checkbox::before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
.color-label input[type="color"] {
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
transition: box-shadow var(--transition-fast);
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.color-label input[type="color"]::-moz-color-swatch {
|
||||
border: var(--border-width-base) solid var(--text-color);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.color-label input[type="color"]::-webkit-color-swatch {
|
||||
border: var(--border-width-base) solid var(--text-color);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.color-label:hover input[type="color"] {
|
||||
box-shadow: 0 0 8px var(--thumb-color);
|
||||
}
|
||||
|
||||
.color-section,
|
||||
.upload-section {
|
||||
display: grid;
|
||||
gap: var(--gap-md);
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.delete-theme-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--danger-fg);
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
padding: 0 0.5rem;
|
||||
transition: color var(--transition-fast), transform 0.2s;
|
||||
}
|
||||
|
||||
.delete-theme-button:hover {
|
||||
color: var(--danger-hover-bg);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.discord-username-input {
|
||||
background-color: var(--input-bg);
|
||||
border: var(--border-style-input);
|
||||
border-radius: var(--border-radius-base);
|
||||
box-sizing: border-box;
|
||||
color: var(--text-color);
|
||||
margin-top: var(--border-radius-lg);
|
||||
padding: var(--padding-sm);
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.download-theme-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-confirm);
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
padding: 0 0.5rem;
|
||||
transition: color var(--transition-fast), transform 0.2s;
|
||||
}
|
||||
|
||||
.download-theme-button:hover {
|
||||
color: var(--color-confirm-hover);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.file-clear-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--danger-fg);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
padding: 0 0.4rem;
|
||||
flex-shrink: 0;
|
||||
transition: color var(--transition-fast), transform 0.2s;
|
||||
}
|
||||
.file-clear-button:hover {
|
||||
color: var(--danger-hover-bg);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.file-name-display {
|
||||
color: var(--text-muted);
|
||||
flex-grow: 1;
|
||||
font-size: 0.9em;
|
||||
font-style: italic;
|
||||
margin-right: var(--margin-sm);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
label.file-upload-button {
|
||||
background-color: var(--success-bg);
|
||||
border-radius: var(--border-radius-sm);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
font-size: 0.9em;
|
||||
padding: 0.3rem 0.6rem;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-upload-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-upload-label {
|
||||
align-items: center;
|
||||
background-color: var(--secondary-bg);
|
||||
border-radius: var(--border-radius-md);
|
||||
display: flex;
|
||||
min-height: 2.5rem;
|
||||
overflow: hidden;
|
||||
padding: var(--padding-sm);
|
||||
transition: background-color 0.2s, box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.file-upload-label:hover {
|
||||
background-color: var(--main-fg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.file-upload-label:hover .file-upload-button {
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.file-upload-text {
|
||||
color: var(--text-color);
|
||||
flex-shrink: 0;
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
margin-right: var(--margin-sm);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.help-icon {
|
||||
background-color: var(--main-fg);
|
||||
border-radius: 50%;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
height: 1.2em;
|
||||
line-height: 1.2em;
|
||||
margin-left: 0.5em;
|
||||
text-align: center;
|
||||
transition: background-color var(--transition-fast), transform var(--transition-fast);
|
||||
width: 1.2em;
|
||||
}
|
||||
|
||||
.help-icon:hover {
|
||||
background-color: var(--success-hover-bg);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.manage-themes-button {
|
||||
background-color: var(--color-confirm);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-lg);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
overflow: hidden;
|
||||
padding: 0.75rem 1.5rem;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.manage-themes-button:hover {
|
||||
background-color: var(--color-confirm-hover);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.manage-themes-modal {
|
||||
animation: modalFadeIn 0.3s ease;
|
||||
max-width: var(--width-xl);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.manage-themes-tabs {
|
||||
border: 1px solid var(--sidebar-border-color);
|
||||
border-radius: var(--border-radius-md);
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
background-color: var(--success-bg);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-lg);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
overflow: hidden;
|
||||
padding: 0.75rem 1.5rem;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.save-button:hover {
|
||||
background-color: var(--success-hover-bg);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.save-button-wrapper {
|
||||
display: flex;
|
||||
gap: var(--gap-md);
|
||||
justify-content: center;
|
||||
margin-top: var(--margin-lg);
|
||||
}
|
||||
|
||||
.signal-type-toggle {
|
||||
display: flex;
|
||||
gap: var(--gap-sm);
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
background-color: var(--accent-bg);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-lg);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
overflow: hidden;
|
||||
padding: 0.75rem 1.5rem;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
background-color: var(--accent-hover-bg);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
background-color: var(--input-bg);
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
padding: 0.75rem;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), color var(--transition-fast), transform var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
background-color: var(--main-fg);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tab-button:not(:last-child) {
|
||||
border-right: 1px solid var(--sidebar-border-color);
|
||||
}
|
||||
|
||||
.tab-button:not(.active):hover {
|
||||
background-color: var(--main-fg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.theme-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
flex-grow: 1;
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
text-align: left;
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
.theme-button:hover {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.theme-item {
|
||||
align-items: center;
|
||||
background-color: var(--input-bg);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.theme-item:not(:last-child) {
|
||||
border-bottom: 1px solid var(--sidebar-border-color);
|
||||
}
|
||||
|
||||
.theme-item:hover {
|
||||
background-color: var(--main-fg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.theme-maker-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.theme-maker-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-md);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.theme-maker-form .color-label {
|
||||
align-items: center;
|
||||
background-color: var(--secondary-bg);
|
||||
border-radius: var(--border-radius-md);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
justify-content: space-between;
|
||||
padding: var(--padding-sm);
|
||||
transition: box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.theme-maker-form .color-label:hover {
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.theme-maker-main-title {
|
||||
background-color: var(--input-bg);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
box-sizing: border-box;
|
||||
color: var(--text-color);
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: var(--margin-base);
|
||||
padding: var(--padding-sm);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.theme-maker-main-widget {
|
||||
background-color: var(--secondary-bg);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-md);
|
||||
color: var(--main-fg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: var(--padding-xl);
|
||||
max-width: var(--width-xxxxl);
|
||||
padding: var(--padding-lg);
|
||||
transition: box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.theme-maker-main-widget:hover {
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.theme-maker-sub-widgets {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--gap-lg);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.theme-maker-title {
|
||||
background-color: var(--main-fg);
|
||||
border-radius: var(--border-radius-md);
|
||||
color: var(--text-color);
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin: 0 auto var(--margin-base);
|
||||
padding: var(--padding-sm) var(--padding-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.theme-maker-widget {
|
||||
background-color: var(--input-bg);
|
||||
border-radius: var(--border-radius-md);
|
||||
flex: 1 1 var(--width-md);
|
||||
overflow: visible;
|
||||
padding: var(--padding-base);
|
||||
transition: box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.theme-maker-widget:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.theme-name-label {
|
||||
color: var(--text-color);
|
||||
display: block;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: var(--margin-xs);
|
||||
}
|
||||
|
||||
.theme-name-section {
|
||||
box-sizing: border-box;
|
||||
margin-bottom: var(--margin-lg);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.themes-list {
|
||||
border: 1px solid var(--sidebar-border-color);
|
||||
border-radius: var(--border-radius-md);
|
||||
max-height: 300px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
background-color: var(--secondary-bg);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-md);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-normal);
|
||||
padding: var(--padding-sm);
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), color var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.toggle-button.active {
|
||||
background-color: var(--main-fg);
|
||||
color: var(--text-color);
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
}
|
||||
|
||||
.toggle-button:not(.active):hover {
|
||||
background-color: var(--main-fg);
|
||||
box-shadow: var(--shadow-md);
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.turn-signal-help-text {
|
||||
background-color: var(--secondary-bg);
|
||||
border-radius: var(--border-radius-md);
|
||||
margin-top: 0.5em;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.turn-signal-help-text p {
|
||||
font-weight: normal;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.turn-signal-input {
|
||||
-moz-appearance: textfield;
|
||||
background-color: var(--secondary-bg);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-md);
|
||||
box-sizing: border-box;
|
||||
color: var(--text-color);
|
||||
font-size: var(--font-size-base);
|
||||
padding: var(--padding-sm);
|
||||
transition: box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.turn-signal-input::-webkit-inner-spin-button,
|
||||
.turn-signal-input::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.turn-signal-label {
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
}
|
||||
|
||||
.turn-signal-length-section,
|
||||
.turn-signal-style-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-xs);
|
||||
}
|
||||
|
||||
#themeName,
|
||||
#submitThemeName {
|
||||
background-color: var(--input-bg);
|
||||
border: var(--border-style-input);
|
||||
border-radius: var(--border-radius-sm);
|
||||
box-sizing: border-box;
|
||||
color: var(--text-color);
|
||||
font-size: var(--font-size-sm);
|
||||
padding: var(--padding-sm);
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#themeName:focus,
|
||||
#themeName:hover,
|
||||
#submitThemeName:focus,
|
||||
#submitThemeName:hover,
|
||||
.discord-username-input:focus,
|
||||
.discord-username-input:hover,
|
||||
.turn-signal-input:focus,
|
||||
.turn-signal-input:hover {
|
||||
border-color: var(--thumb-color);
|
||||
box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
|
||||
outline: none;
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.sequence-order-button {
|
||||
background-color: var(--main-fg);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-md);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
margin-top: var(--margin-sm);
|
||||
padding: var(--padding-sm) var(--padding-base);
|
||||
text-align: center;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sequence-order-button:hover {
|
||||
background-color: var(--main-fg);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.draggable-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.draggable-item {
|
||||
align-items: center;
|
||||
background-color: var(--input-bg);
|
||||
border: 1px solid var(--sidebar-border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
cursor: move;
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
padding: 10px;
|
||||
transition: background-color 0.2s, opacity 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.draggable-item.dragging {
|
||||
opacity: 0.5;
|
||||
background-color: var(--main-fg);
|
||||
}
|
||||
|
||||
.draggable-item:hover {
|
||||
background-color: var(--main-fg);
|
||||
}
|
||||
|
||||
.draggable-item.drop-before::before,
|
||||
.draggable-item.drop-after::after {
|
||||
bottom: -2px;
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background-color: var(--success-bg);
|
||||
}
|
||||
|
||||
.draggable-item.drop-before::before {
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
.sequential-image-preview {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
object-fit: contain;
|
||||
margin-right: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@keyframes modalFadeIn {
|
||||
from { opacity: 0; transform: scale(0.95); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) and (orientation: portrait) {
|
||||
.manage-themes-tabs {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.manage-themes-tabs .tab-button {
|
||||
border-bottom: 1px solid var(--sidebar-border-color);
|
||||
border-right: 1px solid var(--sidebar-border-color);
|
||||
flex: 1 0 33.333%;
|
||||
}
|
||||
|
||||
.manage-themes-tabs .tab-button:nth-child(3n) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.save-button-wrapper {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.save-button-wrapper > button {
|
||||
flex-basis: calc(50% - 1rem);
|
||||
margin-bottom: var(--gap-sm);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,7 @@
|
||||
<link rel="stylesheet" href="/assets/components/tools/doors.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/error_logs.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/speed_limits.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/theme_maker.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/tmux.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/toggles.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/tsk_manager.css">
|
||||
|
||||
@@ -6,6 +6,7 @@ from io import BytesIO
|
||||
from pathlib import Path
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
import base64
|
||||
import errno
|
||||
import hashlib
|
||||
import json
|
||||
@@ -13,6 +14,7 @@ import os
|
||||
import re
|
||||
import requests
|
||||
import secrets
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
@@ -28,11 +30,18 @@ from openpilot.system.loggerd.deleter import PRESERVE_ATTR_NAME, PRESERVE_ATTR_V
|
||||
from openpilot.system.version import get_build_metadata
|
||||
from panda import Panda
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import delete_file, get_lock_status, run_cmd
|
||||
from openpilot.frogpilot.common.frogpilot_variables import ERROR_LOGS_PATH, EXCLUDED_KEYS, SCREEN_RECORDINGS_PATH,\
|
||||
frogpilot_default_params, params, update_frogpilot_toggles
|
||||
from openpilot.frogpilot.assets.theme_manager import HOLIDAY_THEME_PATH, THEME_COMPONENT_PARAMS
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import delete_file, get_lock_status, run_cmd, extract_tar
|
||||
from openpilot.frogpilot.common.frogpilot_variables import ACTIVE_THEME_PATH, ERROR_LOGS_PATH, EXCLUDED_KEYS, RESOURCES_REPO, SCREEN_RECORDINGS_PATH, THEME_SAVE_PATH,\
|
||||
frogpilot_default_params, params, params_memory, update_frogpilot_toggles
|
||||
from openpilot.frogpilot.system.the_pond import utilities
|
||||
|
||||
DISCORD_WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL")
|
||||
|
||||
GITLAB_API = "https://gitlab.com/api/v4"
|
||||
GITLAB_SUBMISSIONS_PROJECT_ID = "71992109"
|
||||
GITLAB_TOKEN = os.environ.get("GITLAB_TOKEN", "")
|
||||
|
||||
FOOTAGE_PATHS = [
|
||||
Paths.log_root(HD=True, raw=True),
|
||||
Paths.log_root(konik=True, raw=True),
|
||||
@@ -71,7 +80,8 @@ def setup(app):
|
||||
|
||||
while True:
|
||||
with Panda(disable_checks=True) as panda:
|
||||
panda.set_safety_mode(panda.SAFETY_TOYOTA)
|
||||
if not params.get_bool("IsOnroad"):
|
||||
panda.set_safety_mode(panda.SAFETY_TOYOTA)
|
||||
panda.can_send(0x750, LOCK_CMD, 0)
|
||||
|
||||
time.sleep(1)
|
||||
@@ -89,7 +99,8 @@ def setup(app):
|
||||
|
||||
while True:
|
||||
with Panda(disable_checks=True) as panda:
|
||||
panda.set_safety_mode(panda.SAFETY_TOYOTA)
|
||||
if not params.get_bool("IsOnroad"):
|
||||
panda.set_safety_mode(panda.SAFETY_TOYOTA)
|
||||
panda.can_send(0x750, UNLOCK_CMD, 0)
|
||||
|
||||
time.sleep(1)
|
||||
@@ -293,6 +304,10 @@ def setup(app):
|
||||
def get_param():
|
||||
return params.get(request.args.get("key")) or "", 200
|
||||
|
||||
@app.route("/api/params_memory", methods=["GET"])
|
||||
def get_param_memory():
|
||||
return params_memory.get(request.args.get("key")) or "", 200
|
||||
|
||||
@app.route("/api/routes", methods=["GET"])
|
||||
def list_routes():
|
||||
def generate():
|
||||
@@ -306,6 +321,27 @@ def setup(app):
|
||||
try:
|
||||
result = future.result()
|
||||
yield f"data: {json.dumps({'routes': [result]})}\n\n"
|
||||
|
||||
path, name = futures[future]
|
||||
segments = utilities.get_segments_in_route(name, path)
|
||||
if segments:
|
||||
for camera, cam_file in {
|
||||
"forward": "fcamera.hevc",
|
||||
"wide": "ecamera.hevc",
|
||||
"driver": "dcamera.hevc"
|
||||
}.items():
|
||||
input_files = [
|
||||
os.path.join(path, seg, cam_file)
|
||||
for seg in segments
|
||||
if os.path.exists(os.path.join(path, seg, cam_file))
|
||||
]
|
||||
if input_files:
|
||||
executor.submit(
|
||||
utilities.ffmpeg_concat_segments_to_mp4,
|
||||
input_files,
|
||||
f"{name}-{camera}"
|
||||
)
|
||||
|
||||
except Exception as exception:
|
||||
print(f"Error processing route: {exception}")
|
||||
yield f"data: {json.dumps({'progress': processed, 'total': total})}\n\n"
|
||||
@@ -370,6 +406,32 @@ def setup(app):
|
||||
return {"message": "Route unpreserved!"}, 200
|
||||
return {"error": "Route not found"}, 404
|
||||
|
||||
@app.route("/video/<name>/combined", methods=["GET"])
|
||||
def get_combined_route_video(name):
|
||||
camera = request.args.get("camera", "forward")
|
||||
for footage_path in FOOTAGE_PATHS:
|
||||
segments = utilities.get_segments_in_route(name, footage_path)
|
||||
if segments:
|
||||
cam_file = {
|
||||
"forward": "fcamera.hevc",
|
||||
"wide": "ecamera.hevc",
|
||||
"driver": "dcamera.hevc",
|
||||
}.get(camera, "fcamera.hevc")
|
||||
|
||||
input_files = [
|
||||
os.path.join(footage_path, seg, cam_file)
|
||||
for seg in segments
|
||||
if os.path.exists(os.path.join(footage_path, seg, cam_file))
|
||||
]
|
||||
|
||||
if not input_files:
|
||||
return {"error": "No video files found"}, 404
|
||||
|
||||
mp4_file = utilities.ffmpeg_concat_segments_to_mp4(input_files, cache_key=f"{name}-{camera}")
|
||||
return send_file(mp4_file, mimetype="video/mp4")
|
||||
|
||||
return {"error": "Route not found"}, 404
|
||||
|
||||
@app.route("/api/routes/<name>", methods=["GET"])
|
||||
def get_route(name):
|
||||
for footage_path in FOOTAGE_PATHS:
|
||||
@@ -652,7 +714,9 @@ def setup(app):
|
||||
os.makedirs(state, exist_ok=True)
|
||||
|
||||
run_cmd(["curl", "-fsSL", tgz_url, "-o", tgz_path], "Downloaded Tailscale archive.", "Failed to download Tailscale archive.")
|
||||
run_cmd(["tar", "xzf", tgz_path, "-C", base], "Extracted Tailscale archive.", "Failed to extract Tailscale archive.")
|
||||
|
||||
extract_tar(tgz_path, base)
|
||||
|
||||
run_cmd(["cp", f"{bin_dir}/tailscale", f"{base}/tailscale"], "Copied tailscale binary.", "Failed to copy tailscale binary.")
|
||||
run_cmd(["cp", f"{bin_dir}/tailscaled", f"{base}/tailscaled"], "Copied tailscaled binary.", "Failed to copy tailscaled binary.")
|
||||
run_cmd(["chmod", "+x", f"{base}/tailscale", f"{base}/tailscaled"], "Made binaries executable.", "Failed to chmod binaries.")
|
||||
@@ -740,6 +804,591 @@ def setup(app):
|
||||
|
||||
return jsonify({"message": "Tailscale uninstalled!"}), 200
|
||||
|
||||
@app.route("/api/themes", methods=["POST"])
|
||||
def save_theme_route():
|
||||
theme_path, error = utilities.create_theme(request.form, request.files)
|
||||
if error:
|
||||
return jsonify({"message": error}), 400
|
||||
return jsonify({"message": f'Theme "{request.form.get("themeName")}" saved!'}), 200
|
||||
|
||||
@app.route("/api/themes/download_asset", methods=["POST"])
|
||||
def start_download_asset():
|
||||
data = request.get_json() or {}
|
||||
raw_component = (data.get("component") or "").strip()
|
||||
display_name = (data.get("name") or "").strip()
|
||||
if not raw_component or not display_name:
|
||||
return jsonify({"error": "Missing component or name"}), 400
|
||||
|
||||
component = "steering_wheels" if raw_component == "steering_wheel" else ("signals" if raw_component == "turn_signals" else raw_component)
|
||||
mem_key = THEME_COMPONENT_PARAMS.get(component)
|
||||
if not mem_key:
|
||||
return jsonify({"error": "Unknown component"}), 400
|
||||
|
||||
slug = display_name.lower().replace("(", "").replace(")", "").replace(" ", "_")
|
||||
|
||||
params_memory.put(mem_key, slug)
|
||||
params_memory.put("ThemeDownloadProgress", "Downloading...")
|
||||
|
||||
return jsonify({"message": "Download started", "component": component, "param": mem_key, "slug": slug}), 200
|
||||
|
||||
@app.route("/api/themes/apply", methods=["POST"])
|
||||
def apply_theme():
|
||||
form_data = request.form.to_dict(flat=True)
|
||||
files = request.files
|
||||
|
||||
if not form_data.get("themeName"):
|
||||
form_data["themeName"] = f"tmp_{secrets.token_hex(8)}"
|
||||
|
||||
temp_path, error = utilities.create_theme(form_data, files, temporary=True)
|
||||
if error:
|
||||
return {"error": error}, 400
|
||||
|
||||
save_checklist = json.loads(form_data.get("saveChecklist", "{}"))
|
||||
|
||||
if save_checklist.get("colors"):
|
||||
asset_location = temp_path / "colors"
|
||||
save_location = ACTIVE_THEME_PATH / "colors"
|
||||
if save_location.exists() or save_location.is_symlink():
|
||||
delete_file(save_location)
|
||||
if asset_location.exists():
|
||||
save_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_location.symlink_to(asset_location, target_is_directory=True)
|
||||
|
||||
if save_checklist.get("distance_icons"):
|
||||
asset_location = temp_path / "distance_icons"
|
||||
save_location = ACTIVE_THEME_PATH / "distance_icons"
|
||||
if save_location.exists() or save_location.is_symlink():
|
||||
delete_file(save_location)
|
||||
if asset_location.exists():
|
||||
save_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_location.symlink_to(asset_location, target_is_directory=True)
|
||||
|
||||
if save_checklist.get("icons"):
|
||||
asset_location = temp_path / "icons"
|
||||
save_location = ACTIVE_THEME_PATH / "icons"
|
||||
if save_location.exists() or save_location.is_symlink():
|
||||
delete_file(save_location)
|
||||
if asset_location.exists():
|
||||
save_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_location.symlink_to(asset_location, target_is_directory=True)
|
||||
|
||||
if save_checklist.get("sounds"):
|
||||
asset_location = temp_path / "sounds"
|
||||
save_location = ACTIVE_THEME_PATH / "sounds"
|
||||
if save_location.exists() or save_location.is_symlink():
|
||||
delete_file(save_location)
|
||||
if asset_location.exists():
|
||||
save_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_location.symlink_to(asset_location, target_is_directory=True)
|
||||
|
||||
if save_checklist.get("turn_signals"):
|
||||
asset_location = temp_path / "signals"
|
||||
save_location = ACTIVE_THEME_PATH / "signals"
|
||||
if save_location.exists() or save_location.is_symlink():
|
||||
delete_file(save_location)
|
||||
if asset_location.exists():
|
||||
save_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_location.symlink_to(asset_location, target_is_directory=True)
|
||||
|
||||
wheel_location = temp_path / "WheelIcon"
|
||||
wheel_save_location = ACTIVE_THEME_PATH / "steering_wheel"
|
||||
if wheel_location.exists():
|
||||
if wheel_save_location.exists():
|
||||
delete_file(wheel_save_location)
|
||||
|
||||
wheel_save_location.mkdir(parents=True, exist_ok=True)
|
||||
for file in wheel_location.iterdir():
|
||||
destination_file = wheel_save_location / file.name
|
||||
delete_file(destination_file)
|
||||
destination_file.symlink_to(file)
|
||||
|
||||
params.put_bool("PersonalizeOpenpilot", True)
|
||||
params_memory.put_bool("UseActiveTheme", True)
|
||||
|
||||
update_frogpilot_toggles()
|
||||
return {"message": "Theme applied successfully!"}, 200
|
||||
|
||||
@app.route("/api/themes/asset/<path:theme>/<path:asset_path>")
|
||||
def get_theme_asset(theme, asset_path):
|
||||
theme_type = request.args.get("type", "")
|
||||
|
||||
if theme_type == "active" or theme == "__active__":
|
||||
file_path = ACTIVE_THEME_PATH / asset_path
|
||||
elif asset_path.startswith("steering_wheels/"):
|
||||
file_path = THEME_SAVE_PATH / asset_path
|
||||
elif asset_path.startswith("steering_wheel/") and "holiday" in theme_type:
|
||||
file_path = HOLIDAY_THEME_PATH / theme / asset_path
|
||||
else:
|
||||
base_dir = HOLIDAY_THEME_PATH / theme if "holiday" in theme_type else THEME_SAVE_PATH / "theme_packs" / theme
|
||||
file_path = base_dir / asset_path
|
||||
|
||||
if not file_path.exists():
|
||||
return "File not found", 404
|
||||
|
||||
return send_file(file_path, as_attachment=False)
|
||||
|
||||
@app.route("/api/themes/delete/<path:theme_path_str>", methods=["DELETE"])
|
||||
def delete_theme(theme_path_str):
|
||||
theme_type = request.args.get("type", "user")
|
||||
component = (request.args.get("component") or "").strip()
|
||||
|
||||
if theme_type == "holiday":
|
||||
return jsonify({"message": "Cannot delete holiday themes."}), 403
|
||||
|
||||
if theme_type == "steering_wheel":
|
||||
wheel_path = THEME_SAVE_PATH / "steering_wheels" / theme_path_str
|
||||
if wheel_path.exists():
|
||||
delete_file(wheel_path)
|
||||
return jsonify({"message": f'Steering wheel "{utilities.normalize_theme_name(wheel_path.stem)}" deleted!'}), 200
|
||||
return jsonify({"message": "Steering wheel not found..."}), 404
|
||||
|
||||
theme_path = THEME_SAVE_PATH / "theme_packs" / theme_path_str
|
||||
if not theme_path.is_dir():
|
||||
return jsonify({"message": "Theme not found..."}), 404
|
||||
|
||||
if component:
|
||||
allowed = {"colors", "distance_icons", "icons", "sounds", "signals"}
|
||||
if component not in allowed:
|
||||
return jsonify({"message": "Unknown component..."}), 400
|
||||
|
||||
target = theme_path / component
|
||||
if not target.exists():
|
||||
return jsonify({"message": f'Component "{component}" not found in theme...'}), 404
|
||||
|
||||
delete_file(target)
|
||||
|
||||
return jsonify({"message": f'Removed {component.replace("_", " ")} from "{utilities.normalize_theme_name(theme_path.name)}"!'}), 200
|
||||
|
||||
delete_file(theme_path)
|
||||
return jsonify({"message": f'Theme "{utilities.normalize_theme_name(theme_path.name)}" deleted!'}), 200
|
||||
|
||||
@app.route("/api/themes/default", methods=["GET"])
|
||||
def get_default_theme():
|
||||
theme_data = {
|
||||
"colors": {},
|
||||
"images": {},
|
||||
"sounds": {},
|
||||
"turnSignalLength": 100,
|
||||
"turnSignalType": "Single Image",
|
||||
"sequentialImages": [],
|
||||
"theme_names": {}
|
||||
}
|
||||
|
||||
if not params.get_bool("PersonalizeOpenpilot"):
|
||||
theme_data["theme_names"] = {
|
||||
"colors": "Stock",
|
||||
"distanceIcons": "Stock",
|
||||
"icons": "Stock",
|
||||
"sounds": "Stock",
|
||||
"turnSignals": "Stock",
|
||||
"steeringWheel": "Stock"
|
||||
}
|
||||
else:
|
||||
theme_param_map = {
|
||||
"CustomColors": "colors",
|
||||
"CustomDistanceIcons": "distanceIcons",
|
||||
"CustomIcons": "icons",
|
||||
"CustomSounds": "sounds",
|
||||
"CustomSignals": "turnSignals",
|
||||
"WheelIcon": "steeringWheel"
|
||||
}
|
||||
for param, theme_key in theme_param_map.items():
|
||||
param_value = params.get(param, encoding="utf-8")
|
||||
if param_value:
|
||||
theme_data["theme_names"][theme_key] = utilities.normalize_theme_name(param_value)
|
||||
|
||||
colors_path = ACTIVE_THEME_PATH / "colors" / "colors.json"
|
||||
if colors_path.exists():
|
||||
with open(colors_path, "r") as f:
|
||||
theme_data["colors"] = json.load(f)
|
||||
|
||||
signals_dir = ACTIVE_THEME_PATH / "signals"
|
||||
if signals_dir.exists():
|
||||
sequential_files = sorted([f.name for f in signals_dir.glob("turn_signal_*.png") if "blindspot" not in f.name.lower()])
|
||||
if sequential_files:
|
||||
theme_data["sequentialImages"] = sequential_files
|
||||
theme_data["turnSignalType"] = "Sequential"
|
||||
|
||||
theme_data["turnSignalStyle"] = "Traditional"
|
||||
theme_data["turnSignalLength"] = 100
|
||||
|
||||
for file in os.listdir(signals_dir):
|
||||
if not any(file.endswith(ext) for ext in [".png", ".gif", ".jpg", ".jpeg"]):
|
||||
parts = file.split("_")
|
||||
if len(parts) == 2:
|
||||
theme_data["turnSignalStyle"] = parts[0].capitalize()
|
||||
try:
|
||||
theme_data["turnSignalLength"] = int(parts[1])
|
||||
except ValueError:
|
||||
pass
|
||||
break
|
||||
|
||||
exts = [".png", ".gif", ".jpg", ".jpeg"]
|
||||
for ext in exts:
|
||||
p = signals_dir / f"turn_signal{ext}"
|
||||
if p.exists():
|
||||
theme_data["images"]["turnSignal"] = f"turn_signal{ext}"
|
||||
break
|
||||
for ext in exts:
|
||||
p = signals_dir / f"turn_signal_blindspot{ext}"
|
||||
if p.exists():
|
||||
theme_data["images"]["turnSignalBlindspot"] = f"turn_signal_blindspot{ext}"
|
||||
break
|
||||
|
||||
icons_path = ACTIVE_THEME_PATH / "icons"
|
||||
if icons_path.exists() and icons_path.is_dir():
|
||||
for file in os.listdir(icons_path):
|
||||
if Path(file).stem == "button_settings":
|
||||
theme_data["images"]["settingsButton"] = file
|
||||
elif Path(file).stem == "button_home":
|
||||
theme_data["images"]["homeButton"] = file
|
||||
|
||||
wheel_path = ACTIVE_THEME_PATH / "steering_wheel"
|
||||
if wheel_path.exists() and wheel_path.is_dir():
|
||||
wheel_files = list(wheel_path.glob("wheel.*"))
|
||||
if wheel_files:
|
||||
theme_data["images"]["steeringWheel"] = wheel_files[0].name
|
||||
|
||||
distance_icons_path = ACTIVE_THEME_PATH / "distance_icons"
|
||||
if distance_icons_path.exists() and distance_icons_path.is_dir():
|
||||
theme_data["images"]["distanceIcons"] = {}
|
||||
for file in os.listdir(distance_icons_path):
|
||||
key = Path(file).stem
|
||||
if key in ["traffic", "aggressive", "standard", "relaxed"]:
|
||||
theme_data["images"]["distanceIcons"][key] = file
|
||||
|
||||
sounds_path = ACTIVE_THEME_PATH / "sounds"
|
||||
if sounds_path.exists() and sounds_path.is_dir():
|
||||
valid_sound_keys = ["engage", "disengage", "prompt", "startup"]
|
||||
for file in os.listdir(sounds_path):
|
||||
stem = Path(file).stem
|
||||
if stem in valid_sound_keys:
|
||||
theme_data["sounds"][stem] = file
|
||||
|
||||
return jsonify(theme_data)
|
||||
|
||||
@app.route("/api/themes/download", methods=["POST"])
|
||||
def download_theme_route():
|
||||
theme_path, error = utilities.create_theme(request.form, request.files, temporary=True)
|
||||
if error:
|
||||
return jsonify({"message": error}), 400
|
||||
|
||||
sane_theme_name = utilities.normalize_theme_name(request.form.get("themeName"), for_path=True)
|
||||
|
||||
archive_path = shutil.make_archive(str(theme_path.parent / sane_theme_name), "zip", theme_path.parent, sane_theme_name)
|
||||
|
||||
memory_file = BytesIO()
|
||||
with open(archive_path, "rb") as f:
|
||||
memory_file.write(f.read())
|
||||
memory_file.seek(0)
|
||||
|
||||
delete_file(theme_path.parent)
|
||||
|
||||
return send_file(memory_file, download_name=f'{sane_theme_name}.zip', as_attachment=True)
|
||||
|
||||
@app.route("/api/themes/list", methods=["GET"])
|
||||
def list_themes():
|
||||
all_themes = []
|
||||
themes_path = THEME_SAVE_PATH / "theme_packs"
|
||||
|
||||
if themes_path.exists():
|
||||
for theme_dir in themes_path.iterdir():
|
||||
if theme_dir.is_dir():
|
||||
is_user_created = "-user_created" in theme_dir.name
|
||||
components = utilities.check_theme_components(theme_dir)
|
||||
all_themes.append({
|
||||
"name": utilities.normalize_theme_name(theme_dir.name),
|
||||
"path": theme_dir.name,
|
||||
"type": "user" if is_user_created else "standard",
|
||||
"is_user_created": is_user_created,
|
||||
**components
|
||||
})
|
||||
|
||||
if HOLIDAY_THEME_PATH.exists():
|
||||
for theme_dir in HOLIDAY_THEME_PATH.iterdir():
|
||||
if theme_dir.is_dir():
|
||||
components = utilities.check_theme_components(theme_dir)
|
||||
all_themes.append({
|
||||
"name": utilities.normalize_theme_name(theme_dir.name),
|
||||
"path": theme_dir.name,
|
||||
"type": "holiday",
|
||||
"is_user_created": False,
|
||||
**components
|
||||
})
|
||||
|
||||
wheels_path = THEME_SAVE_PATH / "steering_wheels"
|
||||
if wheels_path.exists():
|
||||
for wheel_file in wheels_path.iterdir():
|
||||
all_themes.append({
|
||||
"name": utilities.normalize_theme_name(wheel_file.stem),
|
||||
"path": wheel_file.name,
|
||||
"type": "steering_wheel",
|
||||
"is_user_created": "-user_created" in wheel_file.name,
|
||||
"hasSteeringWheel": True,
|
||||
})
|
||||
|
||||
return jsonify({"themes": sorted(all_themes, key=lambda x: x['name'])})
|
||||
|
||||
@app.route("/api/themes/load/<path:theme_path>")
|
||||
def load_theme(theme_path):
|
||||
theme_type = request.args.get("type", "")
|
||||
theme_dir = HOLIDAY_THEME_PATH / theme_path if "holiday" in theme_type else THEME_SAVE_PATH / "theme_packs" / theme_path
|
||||
|
||||
response_data = {
|
||||
"colors": None,
|
||||
"images": {},
|
||||
"sounds": {},
|
||||
"sequentialImages": [],
|
||||
"turnSignalType": "Single Image",
|
||||
"turnSignalStyle": "Static",
|
||||
"turnSignalLength": 100
|
||||
}
|
||||
|
||||
colors_file = theme_dir / "colors" / "colors.json"
|
||||
if colors_file.exists():
|
||||
with open(colors_file) as f:
|
||||
response_data["colors"] = json.load(f)
|
||||
|
||||
icons_dir = theme_dir / "icons"
|
||||
if icons_dir.exists():
|
||||
if (icons_dir / "button_home.gif").exists():
|
||||
response_data["images"]["homeButton"] = {
|
||||
"filename": "button_home.gif",
|
||||
"path": "icons/button_home.gif"
|
||||
}
|
||||
if (icons_dir / "button_settings.png").exists():
|
||||
response_data["images"]["settingsButton"] = {
|
||||
"filename": "button_settings.png",
|
||||
"path": "icons/button_settings.png"
|
||||
}
|
||||
|
||||
distance_dir = theme_dir / "distance_icons"
|
||||
if distance_dir.exists():
|
||||
response_data["images"]["distanceIcons"] = {}
|
||||
exts = [".png", ".gif", ".jpg", ".jpeg"]
|
||||
for name in ["aggressive", "relaxed", "standard", "traffic"]:
|
||||
for ext in exts:
|
||||
p = distance_dir / f"{name}{ext}"
|
||||
if p.exists():
|
||||
response_data["images"]["distanceIcons"][name] = {
|
||||
"filename": f"{name}{ext}",
|
||||
"path": f"distance_icons/{name}{ext}"
|
||||
}
|
||||
break
|
||||
|
||||
signals_dir = theme_dir / "signals"
|
||||
if signals_dir.exists():
|
||||
sequential_files = sorted([f.name for f in signals_dir.glob("turn_signal_*.png") if "blindspot" not in f.name.lower()])
|
||||
if sequential_files:
|
||||
response_data["sequentialImages"] = sequential_files
|
||||
response_data["turnSignalType"] = "Sequential"
|
||||
|
||||
response_data["turnSignalStyle"] = "Traditional"
|
||||
response_data["turnSignalLength"] = 100
|
||||
|
||||
for file in os.listdir(signals_dir):
|
||||
if not any(file.endswith(ext) for ext in [".png", ".gif", ".jpg", ".jpeg"]):
|
||||
parts = file.split("_")
|
||||
if len(parts) == 2:
|
||||
response_data["turnSignalStyle"] = parts[0].capitalize()
|
||||
try:
|
||||
response_data["turnSignalLength"] = int(parts[1])
|
||||
except ValueError:
|
||||
pass
|
||||
break
|
||||
|
||||
exts = [".png", ".gif", ".jpg", ".jpeg"]
|
||||
for ext in exts:
|
||||
p = signals_dir / f"turn_signal{ext}"
|
||||
if p.exists():
|
||||
response_data["images"]["turnSignal"] = {
|
||||
"filename": f"turn_signal{ext}",
|
||||
"path": f"signals/turn_signal{ext}",
|
||||
}
|
||||
break
|
||||
for ext in exts:
|
||||
p = signals_dir / f"turn_signal_blindspot{ext}"
|
||||
if p.exists():
|
||||
response_data["images"]["turnSignalBlindspot"] = {
|
||||
"filename": f"turn_signal_blindspot{ext}",
|
||||
"path": f"signals/turn_signal_blindspot{ext}",
|
||||
}
|
||||
break
|
||||
|
||||
sounds_dir = theme_dir / "sounds"
|
||||
if sounds_dir.exists():
|
||||
for name in ["engage", "disengage", "startup", "prompt"]:
|
||||
file_path = sounds_dir / f"{name}.wav"
|
||||
if file_path.exists():
|
||||
response_data["sounds"][name] = {
|
||||
"filename": f"{name}.wav",
|
||||
"path": f"sounds/{name}.wav"
|
||||
}
|
||||
|
||||
steering_wheel_path = None
|
||||
if "holiday" in theme_type:
|
||||
steering_dir = theme_dir / "steering_wheel"
|
||||
if steering_dir.exists() and steering_dir.is_dir():
|
||||
for file in steering_dir.iterdir():
|
||||
if file.is_file() and file.suffix.lower() in [".png", ".jpg", ".jpeg", ".gif"]:
|
||||
steering_wheel_path = f"steering_wheel/{file.name}"
|
||||
break
|
||||
else:
|
||||
steering_wheels_dir = THEME_SAVE_PATH / "steering_wheels"
|
||||
if steering_wheels_dir.exists():
|
||||
for file in steering_wheels_dir.iterdir():
|
||||
if file.is_file() and file.stem.lower() == theme_path.lower() and file.suffix.lower() in [".png", ".jpg", ".jpeg", ".gif"]:
|
||||
steering_wheel_path = f"steering_wheels/{file.name}"
|
||||
break
|
||||
|
||||
if steering_wheel_path:
|
||||
response_data["images"]["steeringWheel"] = {
|
||||
"filename": steering_wheel_path.split("/")[-1],
|
||||
"path": steering_wheel_path
|
||||
}
|
||||
|
||||
return jsonify(response_data)
|
||||
|
||||
@app.route("/api/themes/submit", methods=["POST"])
|
||||
def submit_theme():
|
||||
if not GITLAB_TOKEN:
|
||||
return jsonify({"error": "Missing GitLab token"}), 500
|
||||
|
||||
try:
|
||||
theme_name = request.form.get("themeName")
|
||||
if not theme_name:
|
||||
return jsonify({"error": "Missing theme name"}), 400
|
||||
|
||||
discord_username = request.form.get("discordUsername") or "Unknown"
|
||||
|
||||
theme_path, error = utilities.create_theme(request.form, request.files, temporary=True)
|
||||
if error:
|
||||
return jsonify({"message": error}), 400
|
||||
|
||||
safe_theme_name = utilities.normalize_theme_name(theme_name, for_path=True)
|
||||
combined_name = f"{safe_theme_name}~{discord_username}"
|
||||
timestamp = int(time.time())
|
||||
|
||||
def gitlab_post(project_id, endpoint, payload):
|
||||
url = f"{GITLAB_API}/projects/{project_id}/{endpoint}"
|
||||
resp = requests.post(url, headers={"PRIVATE-TOKEN": GITLAB_TOKEN}, json=payload)
|
||||
if resp.status_code not in (200, 201):
|
||||
raise RuntimeError(f"GitLab API error {resp.status_code}: {resp.text}")
|
||||
return resp.json()
|
||||
|
||||
def encode_file_base64(path):
|
||||
with open(path, "rb") as f:
|
||||
return base64.b64encode(f.read()).decode("utf-8")
|
||||
|
||||
def send_discord_notification(username, theme_name, asset_types):
|
||||
if not DISCORD_WEBHOOK_URL:
|
||||
return
|
||||
|
||||
message = (
|
||||
f"🎨 **New Theme Submission**\n"
|
||||
f"User: `{username}`\n"
|
||||
f"Theme: `{theme_name}`\n"
|
||||
f"Assets: {', '.join(asset_types)}\n"
|
||||
f"[View Submissions Repo](https://gitlab.com/{RESOURCES_REPO}-Submissions)\n"
|
||||
f"<@263565721336807424>"
|
||||
)
|
||||
payload = {"content": message}
|
||||
try:
|
||||
resp = requests.post(DISCORD_WEBHOOK_URL, json=payload)
|
||||
if resp.status_code not in (200, 204):
|
||||
print(f"Discord notification failed: {resp.status_code} {resp.text}")
|
||||
except Exception as exception:
|
||||
print(f"Error sending Discord message: {exception}")
|
||||
|
||||
asset_types = []
|
||||
submission_urls = {}
|
||||
|
||||
distance_icons_path = theme_path / "distance_icons"
|
||||
if distance_icons_path.exists() and any(distance_icons_path.iterdir()):
|
||||
zip_path = shutil.make_archive(str(distance_icons_path), "zip", distance_icons_path)
|
||||
encoded = encode_file_base64(zip_path)
|
||||
file_name = f"{combined_name}.zip"
|
||||
actions = [
|
||||
{
|
||||
"action": "create",
|
||||
"file_path": file_name,
|
||||
"content": encoded,
|
||||
"encoding": "base64"
|
||||
}
|
||||
]
|
||||
commit_payload = {
|
||||
"branch": "Distance-Icons",
|
||||
"commit_message": f"Added Distance Icons: {combined_name}",
|
||||
"actions": actions
|
||||
}
|
||||
gitlab_post(GITLAB_SUBMISSIONS_PROJECT_ID, "repository/commits", commit_payload)
|
||||
asset_types.append("Distance Icons")
|
||||
submission_urls["distance_icons"] = f"https://gitlab.com/{RESOURCES_REPO}-Submissions/-/tree/Distance-Icons"
|
||||
|
||||
theme_actions = []
|
||||
for folder in ["colors", "icons", "signals", "sounds"]:
|
||||
folder_path = theme_path / folder
|
||||
if folder_path.exists() and any(folder_path.iterdir()):
|
||||
zip_path = shutil.make_archive(str(folder_path), "zip", folder_path)
|
||||
encoded = encode_file_base64(zip_path)
|
||||
file_path = f"{combined_name}/{folder}.zip"
|
||||
theme_actions.append({
|
||||
"action": "create",
|
||||
"file_path": file_path,
|
||||
"content": encoded,
|
||||
"encoding": "base64"
|
||||
})
|
||||
|
||||
if theme_actions:
|
||||
commit_payload = {
|
||||
"branch": "Themes",
|
||||
"commit_message": f"Added Theme: {combined_name}",
|
||||
"actions": theme_actions
|
||||
}
|
||||
gitlab_post(GITLAB_SUBMISSIONS_PROJECT_ID, "repository/commits", commit_payload)
|
||||
asset_types.append("Theme")
|
||||
submission_urls["theme"] = f"https://gitlab.com/{RESOURCES_REPO}-Submissions/-/tree/Themes"
|
||||
|
||||
wheel_file = request.files.get("steeringWheel")
|
||||
if wheel_file and wheel_file.filename:
|
||||
suffix = Path(wheel_file.filename).suffix
|
||||
file_name = f"{combined_name}{suffix}"
|
||||
wheel_file.seek(0)
|
||||
encoded_wheel = base64.b64encode(wheel_file.read()).decode("utf-8")
|
||||
actions = [
|
||||
{
|
||||
"action": "create",
|
||||
"file_path": file_name,
|
||||
"content": encoded_wheel,
|
||||
"encoding": "base64"
|
||||
}
|
||||
]
|
||||
commit_payload = {
|
||||
"branch": "Steering-Wheels",
|
||||
"commit_message": f"Added Steering Wheel: {combined_name}",
|
||||
"actions": actions
|
||||
}
|
||||
gitlab_post(GITLAB_SUBMISSIONS_PROJECT_ID, "repository/commits", commit_payload)
|
||||
asset_types.append("Steering Wheel")
|
||||
submission_urls["steering_wheel"] = f"https://gitlab.com/{RESOURCES_REPO}-Submissions/-/tree/Steering-Wheels"
|
||||
|
||||
if not submission_urls:
|
||||
return jsonify({"error": "No valid theme data or steering wheel file provided"}), 400
|
||||
|
||||
send_discord_notification(discord_username, theme_name, asset_types)
|
||||
|
||||
return jsonify({
|
||||
"message": "Submission successful!",
|
||||
"branches": submission_urls
|
||||
}), 200
|
||||
|
||||
except Exception as exception:
|
||||
return jsonify({"error": str(exception)}), 500
|
||||
|
||||
finally:
|
||||
if "theme_path" in locals() and theme_path.parent.exists():
|
||||
delete_file(theme_path.parent)
|
||||
|
||||
@app.route("/api/tmux_log/capture", methods=["POST"])
|
||||
def capture_tmux_log_route():
|
||||
TMUX_LOGS_PATH.mkdir(parents=True, exist_ok=True)
|
||||
@@ -924,16 +1573,65 @@ def setup(app):
|
||||
def get_video(path):
|
||||
camera = request.args.get("camera")
|
||||
filename = {"driver": "dcamera.hevc", "wide": "ecamera.hevc"}.get(camera, "fcamera.hevc")
|
||||
|
||||
for footage_path in FOOTAGE_PATHS:
|
||||
filepath = f"{footage_path}{path}/{filename}"
|
||||
if os.path.exists(filepath):
|
||||
process = utilities.ffmpeg_mp4_wrap_process_builder(filepath)
|
||||
return Response(process.stdout.read(), status=200, mimetype="video/mp4")
|
||||
file_handle = utilities.ffmpeg_mp4_wrap_process_builder(filepath)
|
||||
|
||||
file_handle.seek(0, 2)
|
||||
file_size = file_handle.tell()
|
||||
file_handle.seek(0)
|
||||
|
||||
range_header = request.headers.get('Range', None)
|
||||
if range_header:
|
||||
byte_start = 0
|
||||
byte_end = file_size - 1
|
||||
|
||||
if range_header.startswith('bytes='):
|
||||
range_spec = range_header[6:]
|
||||
if '-' in range_spec:
|
||||
start, end = range_spec.split('-', 1)
|
||||
if start:
|
||||
byte_start = max(0, int(start))
|
||||
if end:
|
||||
byte_end = min(file_size - 1, int(end))
|
||||
|
||||
if byte_start >= file_size:
|
||||
file_handle.close()
|
||||
return Response("Requested Range Not Satisfiable", 416)
|
||||
|
||||
byte_end = max(byte_start, byte_end)
|
||||
|
||||
file_handle.seek(byte_start)
|
||||
read_length = byte_end - byte_start + 1
|
||||
data = file_handle.read(read_length)
|
||||
|
||||
response = Response(
|
||||
data,
|
||||
206,
|
||||
headers={
|
||||
'Content-Range': f'bytes {byte_start}-{byte_end}/{file_size}',
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': str(len(data)),
|
||||
'Content-Type': 'video/mp4'
|
||||
}
|
||||
)
|
||||
else:
|
||||
data = file_handle.read()
|
||||
response = Response(
|
||||
data,
|
||||
200,
|
||||
headers={
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': str(file_size),
|
||||
'Content-Type': 'video/mp4'
|
||||
}
|
||||
)
|
||||
|
||||
file_handle.close()
|
||||
return response
|
||||
return {"error": "Video not found"}, 404
|
||||
|
||||
|
||||
def main():
|
||||
app = Flask(__name__, static_folder="assets", static_url_path="/assets")
|
||||
setup(app)
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
from pydub import AudioSegment
|
||||
from typing import List
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.system.loggerd.config import get_available_bytes, get_used_bytes
|
||||
@@ -17,7 +23,8 @@ from openpilot.system.loggerd.deleter import PRESERVE_ATTR_NAME, PRESERVE_ATTR_V
|
||||
from openpilot.system.loggerd.uploader import listdir_by_creation
|
||||
from openpilot.tools.lib.route import SegmentName
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import params, params_tracking
|
||||
from openpilot.frogpilot.common.frogpilot_variables import THEME_SAVE_PATH, VIDEO_CACHE_PATH, params
|
||||
from openpilot.frogpilot.assets.theme_manager import HOLIDAY_THEME_PATH
|
||||
|
||||
LOG_CANDIDATES = [
|
||||
"qlog",
|
||||
@@ -29,8 +36,293 @@ LOG_CANDIDATES = [
|
||||
|
||||
SEGMENT_RE = re.compile(r"^[0-9a-fA-F]{8}--[0-9a-fA-F]{10}--\d+$")
|
||||
|
||||
TARGET_LOUDNESS = -15.0
|
||||
|
||||
XOR_KEY = "s8#pL3*Xj!aZ@dWq"
|
||||
|
||||
MAX_FILE_SIZE = 5 * 1024 * 1024
|
||||
|
||||
def check_theme_components(theme_path):
|
||||
components = {
|
||||
"hasColors": False,
|
||||
"hasIcons": False,
|
||||
"hasSounds": False,
|
||||
"hasTurnSignals": False,
|
||||
"hasDistanceIcons": False,
|
||||
"hasSteeringWheel": False
|
||||
}
|
||||
|
||||
colors_path = theme_path / "colors" / "colors.json"
|
||||
if colors_path.exists():
|
||||
components["hasColors"] = True
|
||||
|
||||
icons_path = theme_path / "icons"
|
||||
if icons_path.exists() and any(icons_path.iterdir()):
|
||||
components["hasIcons"] = True
|
||||
|
||||
sounds_path = theme_path / "sounds"
|
||||
if sounds_path.exists() and any(sounds_path.iterdir()):
|
||||
components["hasSounds"] = True
|
||||
|
||||
signals_path = theme_path / "signals"
|
||||
if signals_path.exists() and any(signals_path.iterdir()):
|
||||
components["hasTurnSignals"] = True
|
||||
|
||||
distance_icons_path = theme_path / "distance_icons"
|
||||
if distance_icons_path.exists() and any(distance_icons_path.iterdir()):
|
||||
components["hasDistanceIcons"] = True
|
||||
|
||||
is_holiday_theme = str(HOLIDAY_THEME_PATH) in str(theme_path)
|
||||
|
||||
if is_holiday_theme:
|
||||
wheel_path = theme_path / "steering_wheel"
|
||||
if wheel_path.exists() and any(f.name.startswith("wheel.") for f in wheel_path.iterdir()):
|
||||
components["hasSteeringWheel"] = True
|
||||
else:
|
||||
wheel_path = THEME_SAVE_PATH / "steering_wheels"
|
||||
if wheel_path.exists():
|
||||
theme_name = theme_path.name.replace('-user_created', '')
|
||||
if any(wheel_path.glob(f"{theme_name}-user_created.*")):
|
||||
components["hasSteeringWheel"] = True
|
||||
|
||||
return components
|
||||
|
||||
def covert_audio(input_file):
|
||||
sound = AudioSegment.from_file(input_file)
|
||||
sound = sound.set_frame_rate(48000)
|
||||
sound = sound.set_channels(1)
|
||||
|
||||
output_filename = os.path.splitext(input_file)[0] + ".wav"
|
||||
sound.export(output_filename, format="wav", parameters=["-acodec", "pcm_s16le"])
|
||||
|
||||
if input_file != output_filename:
|
||||
os.remove(input_file)
|
||||
|
||||
def create_theme(form_data, files, temporary=False):
|
||||
theme_name = form_data.get("themeName")
|
||||
if not theme_name:
|
||||
return None, "Theme name is required."
|
||||
|
||||
sane_theme_name = secure_filename(theme_name.replace(" ", "_"))
|
||||
|
||||
save_checklist_str = form_data.get("saveChecklist", "{}")
|
||||
save_checklist = json.loads(save_checklist_str)
|
||||
|
||||
needs_theme_pack = any([
|
||||
save_checklist.get("colors"),
|
||||
save_checklist.get("icons"),
|
||||
save_checklist.get("sounds"),
|
||||
save_checklist.get("turn_signals"),
|
||||
save_checklist.get("distance_icons"),
|
||||
])
|
||||
|
||||
if temporary:
|
||||
base_path = Path(f"/tmp/{sane_theme_name}_{secrets.token_hex(8)}")
|
||||
else:
|
||||
base_path = THEME_SAVE_PATH / "theme_packs" if needs_theme_pack else None
|
||||
|
||||
theme_path = (base_path / f"{sane_theme_name}-user_created") if base_path else None
|
||||
if theme_path:
|
||||
theme_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if save_checklist.get("colors"):
|
||||
(theme_path / "colors").mkdir(exist_ok=True)
|
||||
colors_str = form_data.get("colors")
|
||||
if colors_str:
|
||||
color_data = json.loads(colors_str)
|
||||
for key, values in color_data.items():
|
||||
if "alpha" in values:
|
||||
values["alpha"] = values.pop("alpha")
|
||||
colors_file = theme_path / "colors" / "colors.json"
|
||||
with open(colors_file, "w") as f:
|
||||
json.dump(color_data, f, indent=2)
|
||||
|
||||
if save_checklist.get("turn_signals"):
|
||||
signals_path = theme_path / "signals"
|
||||
signals_path.mkdir(exist_ok=True)
|
||||
|
||||
if turn_signal_length := form_data.get("turnSignalLength"):
|
||||
style = form_data.get("turnSignalStyle", "Traditional").lower()
|
||||
(signals_path / f"{style}_{turn_signal_length}").touch()
|
||||
|
||||
turn_signal_type = form_data.get("turnSignalType", "Single Image").lower()
|
||||
|
||||
if turn_signal_type == "single image":
|
||||
for f in signals_path.glob("turn_signal.*"):
|
||||
f.unlink()
|
||||
for f in signals_path.glob("turn_signal_blindspot.*"):
|
||||
f.unlink()
|
||||
|
||||
file = files.get("turnSignal")
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
ext = Path(file.filename).suffix
|
||||
file.save(signals_path / f"turn_signal{ext}")
|
||||
|
||||
file = files.get("turnSignalBlindspot")
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
ext = Path(file.filename).suffix
|
||||
file.save(signals_path / f"turn_signal_blindspot{ext}")
|
||||
|
||||
elif turn_signal_type == "sequential":
|
||||
for f in signals_path.glob("turn_signal_*"):
|
||||
f.unlink()
|
||||
|
||||
signal_map = {
|
||||
"turnSignal": "turn_signal",
|
||||
"turnSignalBlindspot": "turn_signal_blindspot",
|
||||
}
|
||||
for field, base_name in signal_map.items():
|
||||
file = files.get(field)
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
for f in signals_path.glob(f"{base_name}.*"):
|
||||
f.unlink()
|
||||
ext = Path(file.filename).suffix.lower()
|
||||
file.save(signals_path / f"{base_name}{ext}")
|
||||
|
||||
for f in signals_path.glob("turn_signal.*"):
|
||||
f.unlink()
|
||||
for f in signals_path.glob("turn_signal_blindspot.*"):
|
||||
f.unlink()
|
||||
|
||||
sequential_keys = sorted(
|
||||
[k for k in files if k.startswith("turn_signal_")],
|
||||
key=lambda name: int(name.split("_")[-1])
|
||||
)
|
||||
|
||||
for key in sequential_keys:
|
||||
file = files.get(key)
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
idx = key.split("_")[-1]
|
||||
ext = Path(file.filename).suffix
|
||||
file.save(signals_path / f"turn_signal_{idx}{ext}")
|
||||
|
||||
if save_checklist.get("icons"):
|
||||
(theme_path / "icons").mkdir(exist_ok=True)
|
||||
|
||||
icon_map = {
|
||||
"settingsButton": (theme_path / "icons", "button_settings", (169, 104)),
|
||||
"homeButton": (theme_path / "icons", "button_home", (250, 250)),
|
||||
}
|
||||
|
||||
for field, (dest_path, base_name, resize_dims) in icon_map.items():
|
||||
file = files.get(field)
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
|
||||
for f in dest_path.glob(f"{base_name}.*"):
|
||||
f.unlink()
|
||||
|
||||
ext = Path(file.filename).suffix.lower()
|
||||
save_path = dest_path / f"{base_name}{ext}"
|
||||
file.save(save_path)
|
||||
|
||||
if resize_dims:
|
||||
if ext == ".gif":
|
||||
width, height = resize_dims
|
||||
palette_path = save_path.with_suffix(".palette.png")
|
||||
temp_output_path = save_path.with_suffix(".resized.gif")
|
||||
subprocess.run(["ffmpeg", "-i", str(save_path), "-vf", "palettegen", "-y", str(palette_path)], check=True)
|
||||
subprocess.run(["ffmpeg", "-i", str(save_path), "-i", str(palette_path), "-lavfi", f"fps=20,scale={width}:{height}:flags=lanczos[x];[x][1:v]paletteuse", "-y", str(temp_output_path)], check=True)
|
||||
palette_path.unlink()
|
||||
temp_output_path.rename(save_path)
|
||||
else:
|
||||
img = Image.open(save_path).resize(resize_dims, Image.Resampling.LANCZOS)
|
||||
if ext != ".png":
|
||||
save_path.unlink()
|
||||
save_path = save_path.with_suffix(".png")
|
||||
img.save(save_path, "PNG")
|
||||
|
||||
if save_checklist.get("steering_wheel"):
|
||||
wheels_dir = THEME_SAVE_PATH / "steering_wheels"
|
||||
wheels_dir.mkdir(parents=True, exist_ok=True)
|
||||
file = files.get("steeringWheel")
|
||||
saved_wheel_path = None
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
for f in wheels_dir.glob(f"{sane_theme_name}-user_created.*"):
|
||||
f.unlink()
|
||||
ext = Path(file.filename).suffix.lower()
|
||||
saved_wheel_path = wheels_dir / f"{sane_theme_name}-user_created{ext}"
|
||||
file.save(saved_wheel_path)
|
||||
if ext == ".gif":
|
||||
width, height = (250, 250)
|
||||
palette_path = saved_wheel_path.with_suffix(".palette.png")
|
||||
temp_output_path = saved_wheel_path.with_suffix(".resized.gif")
|
||||
subprocess.run(["ffmpeg", "-i", str(saved_wheel_path), "-vf", "palettegen", "-y", str(palette_path)], check=True)
|
||||
subprocess.run(["ffmpeg", "-i", str(saved_wheel_path), "-i", str(palette_path), "-lavfi", f"fps=20,scale={width}:{height}:flags=lanczos[x];[x][1:v]paletteuse", "-y", str(temp_output_path)], check=True)
|
||||
palette_path.unlink()
|
||||
temp_output_path.rename(saved_wheel_path)
|
||||
else:
|
||||
img = Image.open(saved_wheel_path).resize((250, 250), Image.Resampling.LANCZOS)
|
||||
if ext != ".png":
|
||||
saved_wheel_path.unlink()
|
||||
saved_wheel_path = saved_wheel_path.with_suffix(".png")
|
||||
img.save(saved_wheel_path, "PNG")
|
||||
if temporary and (theme_path is not None):
|
||||
existing = saved_wheel_path if saved_wheel_path is not None else next(wheels_dir.glob(f"{sane_theme_name}-user_created.*"), None)
|
||||
if existing:
|
||||
wheel_icon_dir = theme_path / "WheelIcon"
|
||||
wheel_icon_dir.mkdir(parents=True, exist_ok=True)
|
||||
dest = wheel_icon_dir / f"wheel{existing.suffix.lower()}"
|
||||
if dest.exists():
|
||||
dest.unlink()
|
||||
dest.symlink_to(existing)
|
||||
|
||||
if save_checklist.get("distance_icons"):
|
||||
dist_path = theme_path / "distance_icons"
|
||||
dist_path.mkdir(exist_ok=True)
|
||||
for name in ["traffic", "aggressive", "standard", "relaxed"]:
|
||||
file = files.get(f"distanceIcons_{name}")
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
|
||||
for f in dist_path.glob(f"{name}.*"):
|
||||
f.unlink()
|
||||
|
||||
ext = Path(file.filename).suffix.lower()
|
||||
save_path = dist_path / f"{name}{ext}"
|
||||
file.save(save_path)
|
||||
if ext == ".gif":
|
||||
width, height = (250, 250)
|
||||
palette_path = save_path.with_suffix(".palette.png")
|
||||
temp_output_path = save_path.with_suffix(".resized.gif")
|
||||
subprocess.run(["ffmpeg", "-i", str(save_path), "-vf", "palettegen", "-y", str(palette_path)], check=True)
|
||||
subprocess.run(["ffmpeg", "-i", str(save_path), "-i", str(palette_path), "-lavfi", f"fps=20,scale={width}:{height}:flags=lanczos[x];[x][1:v]paletteuse", "-y", str(temp_output_path)], check=True)
|
||||
palette_path.unlink()
|
||||
temp_output_path.rename(save_path)
|
||||
else:
|
||||
img = Image.open(save_path).resize((250, 250), Image.Resampling.LANCZOS)
|
||||
if ext != ".png":
|
||||
save_path.unlink()
|
||||
save_path = save_path.with_suffix(".png")
|
||||
img.save(save_path, "PNG")
|
||||
|
||||
if save_checklist.get("sounds"):
|
||||
sounds_path = theme_path / "sounds"
|
||||
sounds_path.mkdir(exist_ok=True)
|
||||
for name in ["engage", "disengage", "prompt", "startup"]:
|
||||
file = files.get(name)
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
|
||||
save_path = sounds_path / f"{name}{Path(file.filename).suffix}"
|
||||
file.save(save_path)
|
||||
covert_audio(str(save_path))
|
||||
|
||||
return theme_path, None
|
||||
|
||||
def decode_parameters(encoded_string):
|
||||
obfuscated_data = base64.b64decode(encoded_string.encode("utf-8")).decode("utf-8")
|
||||
decrypted_data = xor_encrypt_decrypt(obfuscated_data, XOR_KEY)
|
||||
@@ -42,26 +334,89 @@ def encode_parameters(params_dict):
|
||||
encoded_data = base64.b64encode(obfuscated_data.encode("utf-8")).decode("utf-8")
|
||||
return encoded_data
|
||||
|
||||
def ffmpeg_concat_segments_to_mp4(input_files, cache_key=None):
|
||||
if not input_files:
|
||||
raise ValueError("No input files provided for concatenation")
|
||||
|
||||
VIDEO_CACHE_PATH.mkdir(exist_ok=True)
|
||||
|
||||
key_str = "|".join(str(p) for p in input_files)
|
||||
if cache_key:
|
||||
key_str = f"{cache_key}|{key_str}"
|
||||
file_hash = hashlib.md5(key_str.encode()).hexdigest()
|
||||
cache_path = VIDEO_CACHE_PATH / f"{file_hash}.mp4"
|
||||
|
||||
if cache_path.exists() and all(cache_path.stat().st_mtime > Path(f).stat().st_mtime for f in input_files):
|
||||
return open(cache_path, "rb")
|
||||
|
||||
list_file = VIDEO_CACHE_PATH / f"{file_hash}.txt"
|
||||
with open(list_file, "w") as f:
|
||||
for seg in input_files:
|
||||
f.write(f"file '{Path(seg)}'\n")
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
["ffmpeg", "-hide_banner", "-loglevel", "error", "-f", "concat", "-safe", "0",
|
||||
"-i", str(list_file), "-c", "copy", "-movflags", "faststart", "-y", str(cache_path)],
|
||||
check=True
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
try:
|
||||
subprocess.run(
|
||||
["ffmpeg", "-hide_banner", "-loglevel", "error", "-f", "concat", "-safe", "0",
|
||||
"-i", str(list_file), "-c:v", "libx264", "-movflags", "faststart", "-y", str(cache_path)],
|
||||
check=True
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
if cache_path.exists():
|
||||
cache_path.unlink()
|
||||
raise ValueError(f"Cannot process concatenated video segments: {input_files}")
|
||||
finally:
|
||||
if list_file.exists():
|
||||
list_file.unlink()
|
||||
|
||||
return open(cache_path, "rb")
|
||||
|
||||
def ffmpeg_mp4_wrap_process_builder(filename):
|
||||
is_raw_hevc = filename.rsplit(".", 1)[-1] == "hevc"
|
||||
input_path = Path(filename)
|
||||
|
||||
command = [
|
||||
"ffmpeg",
|
||||
"-hide_banner",
|
||||
"-loglevel", "error",
|
||||
"-probesize", "1M",
|
||||
"-analyzeduration", "1M",
|
||||
*(["-f", "hevc"] if is_raw_hevc else []),
|
||||
"-i", filename,
|
||||
"-c", "copy",
|
||||
"-map", "0",
|
||||
*(["-vtag", "hvc1"] if is_raw_hevc else []),
|
||||
"-f", "mp4",
|
||||
"-movflags", "empty_moov",
|
||||
"-"
|
||||
]
|
||||
if not input_path.exists():
|
||||
raise FileNotFoundError(f"Input file does not exist: {input_path}")
|
||||
|
||||
return subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if input_path.stat().st_size == 0:
|
||||
raise ValueError(f"Input file is empty: {input_path}")
|
||||
|
||||
lock_file = input_path.parent / "rlog.lock"
|
||||
if lock_file.exists():
|
||||
raise ValueError(f"File is still being recorded: {input_path}")
|
||||
|
||||
VIDEO_CACHE_PATH.mkdir(exist_ok=True)
|
||||
|
||||
total, used, free = shutil.disk_usage(VIDEO_CACHE_PATH)
|
||||
if free < 500 * 1024 * 1024:
|
||||
for cache_file in VIDEO_CACHE_PATH.glob("*.mp4"):
|
||||
try:
|
||||
cache_file.unlink()
|
||||
except:
|
||||
pass
|
||||
|
||||
file_hash = hashlib.md5(str(input_path).encode()).hexdigest()
|
||||
cache_path = VIDEO_CACHE_PATH / f"{file_hash}.mp4"
|
||||
|
||||
if cache_path.exists() and cache_path.stat().st_mtime > input_path.stat().st_mtime:
|
||||
return open(cache_path, "rb")
|
||||
|
||||
try:
|
||||
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-i", str(input_path), "-c", "copy", "-movflags", "faststart", "-y", str(cache_path)], check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
try:
|
||||
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-i", str(input_path), "-c:v", "libx264", "-movflags", "faststart", "-y", str(cache_path)], check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
if cache_path.exists():
|
||||
cache_path.unlink()
|
||||
raise ValueError(f"Cannot process video file: {input_path}")
|
||||
|
||||
return open(cache_path, "rb")
|
||||
|
||||
def format_git_date(raw_date: str):
|
||||
date_object = datetime.strptime(raw_date.split()[1], "%Y-%m-%d")
|
||||
@@ -109,15 +464,15 @@ def get_disk_usage():
|
||||
|
||||
def get_drive_stats():
|
||||
stats = json.loads(params.get("ApiCache_DriveStats", encoding="utf-8") or "{}")
|
||||
frogpilot_stats = json.loads(params.get("FrogPilotStats") or "{}")
|
||||
|
||||
is_metric = params.get_bool("IsMetric")
|
||||
conversion = 1 if is_metric else CV.KPH_TO_MPH
|
||||
unit = "kilometers" if is_metric else "miles"
|
||||
|
||||
def process(timeframe):
|
||||
data = stats.get(timeframe, {})
|
||||
return {
|
||||
"distance": data.get("distance", 0) * conversion,
|
||||
"distance": data.get("distance", 0) * (1 if is_metric else CV.KPH_TO_MPH),
|
||||
"drives": data.get("routes", 0),
|
||||
"hours": data.get("minutes", 0) / 60,
|
||||
"unit": unit
|
||||
@@ -126,9 +481,9 @@ def get_drive_stats():
|
||||
stats["all"] = process("all")
|
||||
stats["week"] = process("week")
|
||||
stats["frogpilot"] = {
|
||||
"distance": params_tracking.get_int("FrogPilotKilometers") * conversion,
|
||||
"hours": params_tracking.get_int("FrogPilotMinutes") / 60,
|
||||
"drives": params_tracking.get_int("FrogPilotDrives"),
|
||||
"distance": frogpilot_stats.get("FrogPilotMeters", 0) * (0.001 if is_metric else CV.METER_TO_MILE),
|
||||
"hours": frogpilot_stats.get("FrogPilotSeconds", 0) / (60 * 60),
|
||||
"drives": frogpilot_stats.get("FrogPilotDrives", 0),
|
||||
"unit": unit
|
||||
}
|
||||
|
||||
@@ -173,6 +528,18 @@ def has_preserve_attr(path: str):
|
||||
def list_file(path):
|
||||
return sorted(os.listdir(path), reverse=True)
|
||||
|
||||
def normalize_theme_name(name, for_path=False):
|
||||
name = name.replace("-user_created", "")
|
||||
if for_path:
|
||||
return name.lower().replace(" (", "-").replace(")", "").replace(" ", "-").replace("'", "").replace(".", "")
|
||||
|
||||
parts = re.split(r'[-_]', name)
|
||||
normalized_parts = [part.capitalize() for part in parts]
|
||||
|
||||
if '-' in name and len(normalized_parts) > 1:
|
||||
return f"{normalized_parts[0]} ({' '.join(normalized_parts[1:])})".replace(" Week", "")
|
||||
return ' '.join(normalized_parts).replace(" Week", "")
|
||||
|
||||
def process_route(footage_path, route_name):
|
||||
segment_path = f"{footage_path}{route_name}--0"
|
||||
qcamera_path = f"{segment_path}/qcamera.ts"
|
||||
|
||||
Reference in New Issue
Block a user