September 27th, 2025 Update

This commit is contained in:
James
2025-09-27 12:00:00 -07:00
parent 73821d9482
commit a759abb082
1166 changed files with 228204 additions and 267090 deletions
Binary file not shown.
+18 -30
View File
@@ -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))
+18 -26
View File
@@ -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("Couldnt 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">
+708 -10
View File
@@ -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)
+390 -23
View File
@@ -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"