Device Settings
@@ -266,114 +431,92 @@ export function DeviceSettings() {
class="ds-search"
type="text"
placeholder="Search settings..."
- @input="${(e) => { state.filter = e.target.value }}"
- />
+ @input="${(e) => {
+ state.filter = e.target.value
+ scheduleSyncInputs()
+ }}" />
${() => {
if (state.loadingLayout || state.loadingValues) {
return html`
Loading configuration...
`
}
- const loadedKeys = state.allKeys.length
+ const sections = getSectionsWithSlug()
+ if (sections.length === 0) {
+ return html`
No settings available.
`
+ }
- // Sync DOM inputs after reactive render
- requestAnimationFrame(syncInputs)
+ // Sync DOM inputs after ArrowJS renders (safe: syncScheduled is non-reactive)
+ scheduleSyncInputs()
- return html`
-
- ${loadedKeys} settings mapped dynamically
-
+ // Search active β show matching results from ALL sections
+ if (state.filter) {
+ const MAX_PER_SECTION = 25
+ const searchResults = sections
+ .map(s => ({ ...s, matches: s.params.filter(p => matchesFilter(p)) }))
+ .filter(s => s.matches.length > 0)
- ${state.layout.map(section => {
- const visibleParams = section.params.filter(p => matchesFilter(p))
- if (visibleParams.length === 0) return ""
-
- const isCollapsed = state.collapsed[section.name]
+ const totalMatches = searchResults.reduce((n, s) => n + s.matches.length, 0)
return html`
-
-
-
- ${() => visibleParams.map(p => {
- if (p.parent_key) {
- if (!state.values[p.parent_key]) return ""
- if (!state.expanded[p.parent_key]) return ""
- }
+
+ ${totalMatches} result${totalMatches !== 1 ? "s" : ""} across ${searchResults.length} section${searchResults.length !== 1 ? "s" : ""}
+ ${state.allKeys.length} total mapped
+
- const isNumeric = p.ui_type === "numeric"
- const isChild = p.parent_key ? "ds-child-modifier" : ""
-
- return html`
-
-
-
-
${p.label}
- ${p.description ? html`
${p.description}
` : ""}
-
- ${() => p.is_parent_toggle && state.values[p.key] ? html`
-
toggleManage(p.key)}">
- ${state.expanded[p.key] ? 'Close' : 'Manage'}
-
- ` : ''}
-
- ${isNumeric ? html`
${state.values[p.key] !== undefined ? formatSliderValue(state.values[p.key], p.step !== undefined ? String(p.step) : undefined, p.precision, p.key) : '..'}` : ""}
-
-
- ${isNumeric ? html`
-
- ${(() => {
- const bounds = numericBounds(p)
- return html`
- handleSliderInput(e, p.key)}"
- @change="${() => updateParam(p.key, 'numeric')}"
- />
- `
- })()}
-
- ` : p.ui_type === "dropdown" ? html`
-
- ` : html`
-
updateParam(p.key, 'checkbox')}"
- />
- `}
-
- `
- })}
-
+ ${searchResults.map(section => html`
+
+
- `
- })}
+
+ ${section.matches.slice(0, MAX_PER_SECTION).map(p => renderSettingRow(p))}
+ ${section.matches.length > MAX_PER_SECTION ? html`
+${section.matches.length - MAX_PER_SECTION} more β refine your search
` : ""}
+
+
+ `)}
- ${() => {
- const totalVisible = state.layout.reduce((acc, s) =>
- acc + s.params.filter(p => matchesFilter(p)).length, 0)
- if (totalVisible === 0) {
- return html`
No settings match your search.
`
+ ${totalMatches === 0 ? html`
No settings match your search.
` : ""}
+ `
+ }
+
+ // No search β normal tab-based single-section view
+ const activeSection = sections.find(s => s.slug === state.activeSectionSlug) || sections[0]
+ const visibleParams = activeSection.params.filter(p => matchesFilter(p))
+
+ return html`
+
+ ${sections.map(section => html`
+
+ `)}
+
+
+
+ ${activeSection.params.length} settings in ${activeSection.name}
+ ${state.allKeys.length} total mapped
+
+
+
+
+
+ ${visibleParams.map(p => renderSettingRow(p))}
+
+
+
+ ${visibleParams.length === 0 ? html`
No settings match your search.
` : ""}
`
}}
diff --git a/frogpilot/system/the_pond/assets/components/tools/device_settings_layout.json b/frogpilot/system/the_pond/assets/components/tools/device_settings_layout.json
index f9101d5c4..b1f0a3d67 100644
--- a/frogpilot/system/the_pond/assets/components/tools/device_settings_layout.json
+++ b/frogpilot/system/the_pond/assets/components/tools/device_settings_layout.json
@@ -2178,6 +2178,29 @@
"name": "Vehicle",
"icon": "bi-car-front",
"params": [
+ {
+ "key": "CarMake",
+ "label": "Car Make",
+ "description": "Select your car make.",
+ "data_type": "str",
+ "ui_type": "dropdown",
+ "options_endpoint": "/api/fingerprints/makes"
+ },
+ {
+ "key": "CarModel",
+ "label": "Car Model (Fingerprint)",
+ "description": "Choose the fingerprint platform to use when automatic detection is disabled.",
+ "data_type": "str",
+ "ui_type": "dropdown",
+ "options_endpoint": "/api/fingerprints/models?make={CarMake}"
+ },
+ {
+ "key": "ForceFingerprint",
+ "label": "Disable Automatic Fingerprint Detection",
+ "description": "Force the selected fingerprint and prevent it from changing automatically.",
+ "data_type": "bool",
+ "ui_type": "toggle"
+ },
{
"key": "GMPedalLongitudinal",
"label": "Use Pedal for Longitudinal Control",
@@ -2425,4 +2448,4 @@
}
]
}
-]
\ No newline at end of file
+]
diff --git a/frogpilot/system/the_pond/assets/components/tools/model_manager.js b/frogpilot/system/the_pond/assets/components/tools/model_manager.js
index 35f81a263..47d12dea9 100644
--- a/frogpilot/system/the_pond/assets/components/tools/model_manager.js
+++ b/frogpilot/system/the_pond/assets/components/tools/model_manager.js
@@ -260,12 +260,15 @@ function ensurePolling() {
if (pollingHandle) return;
const poll = async () => {
+ if (!isModelRouteActive()) {
+ pollingHandle = null;
+ return;
+ }
+
let nextDelay = IDLE_POLL_INTERVAL_MS;
try {
- if (isModelRouteActive()) {
- await fetchStatus();
- nextDelay = state.status.downloading ? ACTIVE_POLL_INTERVAL_MS : IDLE_POLL_INTERVAL_MS;
- }
+ await fetchStatus();
+ nextDelay = state.status.downloading ? ACTIVE_POLL_INTERVAL_MS : IDLE_POLL_INTERVAL_MS;
} finally {
pollingHandle = setTimeout(poll, nextDelay);
}
@@ -487,8 +490,8 @@ export function ModelManager() {
state.refreshing = false;
logDebug("Initial refresh failed", state.error);
});
- ensurePolling();
}
+ ensurePolling();
return html`
diff --git a/frogpilot/system/the_pond/assets/components/tools/theme_maker.js b/frogpilot/system/the_pond/assets/components/tools/theme_maker.js
index 86a474f5e..a6b120432 100644
--- a/frogpilot/system/the_pond/assets/components/tools/theme_maker.js
+++ b/frogpilot/system/the_pond/assets/components/tools/theme_maker.js
@@ -97,7 +97,8 @@ const state = reactive({
showDeleteConfirmModal: false,
themeToDelete: null,
themes: [],
- activeTab: "colors"
+ activeTab: "colors",
+ fetched: false,
});
let draggedIndex = -1;
@@ -405,17 +406,19 @@ const fetchDownloadables = async () => {
};
-(async () => {
- try {
- const response = await fetch("/api/params?key=DiscordUsername");
- state.discordUsername = await response.text();
- await loadDefaultTheme();
- await fetchDownloadables();
-
- } catch {}
-})();
-
export function ThemeMaker() {
+ if (!state.fetched) {
+ state.fetched = true;
+ (async () => {
+ try {
+ const response = await fetch("/api/params?key=DiscordUsername");
+ state.discordUsername = await response.text();
+ await loadDefaultTheme();
+ await fetchDownloadables();
+ } catch {}
+ })();
+ }
+
const normalize = (str) => (str || "")
.toString()
.trim()
diff --git a/frogpilot/system/the_pond/assets/components/tools/vehicle_features.js b/frogpilot/system/the_pond/assets/components/tools/vehicle_features.js
new file mode 100644
index 000000000..dbe330401
--- /dev/null
+++ b/frogpilot/system/the_pond/assets/components/tools/vehicle_features.js
@@ -0,0 +1,162 @@
+import { html, reactive } from "https://esm.sh/@arrow-js/core"
+import { DoorControl } from "/assets/components/tools/doors.js"
+import { TSKManager } from "/assets/components/tools/tsk_manager.js"
+
+const state = reactive({
+ activeTool: null,
+ toolStatus: {
+ doors: "untested", // untested, allowed, denied
+ tsk: "untested"
+ },
+ loading: false
+})
+
+async function checkToolAvailability(toolName) {
+ if (state.toolStatus[toolName] !== "untested") {
+ state.activeTool = toolName;
+ return;
+ }
+
+ state.loading = true;
+ state.activeTool = toolName;
+
+ try {
+ const response = await fetch(`/api/car_features_check?tool=${toolName}`);
+ const data = await response.json();
+ state.toolStatus[toolName] = data.result ? "allowed" : "denied";
+ } catch (error) {
+ console.error("Failed to check feature availability:", error);
+ state.toolStatus[toolName] = "denied";
+ } finally {
+ state.loading = false;
+ }
+}
+
+export function VehicleFeatures() {
+ return html`
+
+
+
+ ${() => {
+ if (!state.activeTool) {
+ return html`
+
Vehicle Specific Features
+
+ Select a feature below to access it. These features verify vehicle compatibility when launched.
+
+
+
checkToolAvailability('doors')}">
+
+
Lock/Unlock Doors
+
Send lock or unlock commands remotely to your vehicle.
+
+
checkToolAvailability('tsk')}">
+
+
Toyota Security Keys
+
Manage and apply security keys for secOC protected devices.
+
+
+ `;
+ }
+
+ return html`
+
+
+ ${() => {
+ if (state.loading) {
+ return html`
Verifying vehicle compatibility...
`;
+ }
+
+ if (state.toolStatus[state.activeTool] === "denied") {
+ const toolNames = { doors: "Lock/Unlock Doors", tsk: "Toyota Security Keys" };
+ return html`
+
+
+ ${toolNames[state.activeTool]} is not supported for your current vehicle.
+
+ `;
+ }
+
+ if (state.activeTool === "doors") return DoorControl();
+ if (state.activeTool === "tsk") return TSKManager();
+ return "";
+ }}
+ `;
+ }}
+
+ `;
+}
diff --git a/frogpilot/system/the_pond/templates/index.html b/frogpilot/system/the_pond/templates/index.html
index dbb30f792..5c5e5c4cb 100644
--- a/frogpilot/system/the_pond/templates/index.html
+++ b/frogpilot/system/the_pond/templates/index.html
@@ -14,7 +14,6 @@
-
@@ -40,8 +39,6 @@
-
-
", methods=["DELETE"])
@@ -1066,9 +1305,6 @@ def setup(app):
yield f"data: {json.dumps({'progress': processed, 'total': total})}\n\n"
- for recording in recordings:
- utilities.process_screen_recording_gif(recording)
-
return Response(generate(), mimetype="text/event-stream")
@app.route("/screen_recordings/
", methods=["GET"])
@@ -1132,17 +1368,9 @@ def setup(app):
else:
env = short_branch
- try:
- response = requests.get(f"https://api.comma.ai/v1/devices/{params.get('DongleId', encoding='utf8')}/firehose_stats", timeout=10)
- response.raise_for_status()
- firehose_stats = response.json().get("firehose", 0)
- except (requests.RequestException, ValueError) as e:
- firehose_stats = 0
-
return {
"diskUsage": utilities.get_disk_usage(),
"driveStats": utilities.get_drive_stats(),
- "firehoseStats": {"segments": firehose_stats},
"softwareInfo": {
"branchName": build_metadata.channel,
"buildEnvironment": env,
@@ -1985,12 +2213,6 @@ def setup(app):
return jsonify({"message": f"Renamed {old} to {new_safe}!"}), 200
- @app.route("/api/tsk_available", methods=["GET"])
- def tsk_available():
- with car.CarParams.from_bytes(params.get("CarParamsPersistent")) as cp_reader:
- CP = cp_reader.as_builder()
-
- return jsonify({"result": CP.secOcRequired})
@app.route("/api/tsk_keys", methods=["DELETE"])
def delete_secoc_key():
diff --git a/frogpilot/system/the_pond/utilities.py b/frogpilot/system/the_pond/utilities.py
index 056056c94..42b16b7b8 100644
--- a/frogpilot/system/the_pond/utilities.py
+++ b/frogpilot/system/the_pond/utilities.py
@@ -8,7 +8,6 @@ import secrets
import shutil
import subprocess
import time
-import uuid
from datetime import datetime
from pathlib import Path
@@ -563,20 +562,11 @@ def process_route(footage_path, route_name):
return {
"name": route_name,
- "gif": f"/thumbnails/{route_name}--0/preview.gif",
"png": f"/thumbnails/{route_name}--0/preview.png",
"timestamp": route_timestamp_str,
"is_preserved": has_preserve_attr(segment_path)
}
-def process_route_gif(footage_path, route_name):
- segment_path = f"{footage_path}{route_name}--0"
- qcamera_path = f"{segment_path}/qcamera.ts"
- gif_output_path = os.path.join(segment_path, "preview.gif")
-
- if not os.path.exists(gif_output_path):
- video_to_gif(qcamera_path, gif_output_path)
-
def process_screen_recording(mp4):
stem = mp4.with_suffix("")
png_path = stem.with_suffix(".png")
@@ -592,44 +582,29 @@ def process_screen_recording(mp4):
return {
"filename": mp4.name,
- "gif": f"/screen_recordings/{stem.with_suffix('.gif').name}",
"png": f"/screen_recordings/{png_path.name}",
"timestamp": datetime.fromtimestamp(mp4.stat().st_mtime).isoformat(),
"is_custom_name": is_custom_name
}
-def process_screen_recording_gif(mp4):
- stem = mp4.with_suffix("")
- gif_path = stem.with_suffix(".gif")
- if not gif_path.exists():
- video_to_gif(mp4, gif_path)
-
-def run_ffmpeg(args):
- process = subprocess.Popen(["ffmpeg", "-hide_banner", "-loglevel", "error"] + args, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
- stdout, stderr = process.communicate()
- return stdout
-
def segment_to_segment_name(data_dir, segment):
full_path = os.path.join(data_dir, f"FakeDongleID1337|{segment}")
return SegmentName(full_path)
-def video_to_gif(input_path, output_path):
- output_path = Path(output_path)
- sped_up_path = output_path.with_suffix(f".{uuid.uuid4()}.spedup.mp4")
-
- run_ffmpeg(["-i", str(input_path), "-an", "-vf", "setpts=PTS/35", str(sped_up_path)])
- run_ffmpeg(["-i", str(sped_up_path), "-loop", "0", str(output_path)])
-
- if os.path.exists(sped_up_path):
- os.remove(sped_up_path)
-
def video_to_png(input_path, output_path):
- run_ffmpeg([
- "-ss", str(get_video_duration(input_path) / 2),
- "-i", str(input_path),
- "-frames:v", "1",
- str(output_path)
- ])
+ try:
+ subprocess.run([
+ "ffmpeg", "-hide_banner", "-loglevel", "error",
+ "-ss", "1",
+ "-i", str(input_path),
+ "-frames:v", "1",
+ "-y",
+ str(output_path)
+ ], capture_output=True, check=True, text=True)
+ except subprocess.CalledProcessError as e:
+ print(f"Failed to generate PNG for {input_path}")
+ if e.stderr:
+ print(e.stderr)
def xor_encrypt_decrypt(data, key):
return "".join(chr(ord(c) ^ ord(key[i % len(key)])) for i, c in enumerate(data))
diff --git a/system/manager/process.py b/system/manager/process.py
index 4fad44eca..0668ebd9a 100644
--- a/system/manager/process.py
+++ b/system/manager/process.py
@@ -21,8 +21,11 @@ WATCHDOG_FN = "/dev/shm/wd_"
ENABLE_WATCHDOG = os.getenv("NO_WATCHDOG") is None
-def launcher(proc: str, name: str) -> None:
+def launcher(proc: str, name: str, nice: int | None = None) -> None:
try:
+ if nice is not None:
+ os.nice(nice)
+
# import the process
mod = importlib.import_module(proc)
@@ -47,9 +50,12 @@ def launcher(proc: str, name: str) -> None:
raise
-def nativelauncher(pargs: list[str], cwd: str, name: str) -> None:
+def nativelauncher(pargs: list[str], cwd: str, name: str, nice: int | None = None) -> None:
os.environ['MANAGER_DAEMON'] = name
+ if nice is not None:
+ os.nice(nice)
+
# exec the process
os.chdir(cwd)
os.execvp(pargs[0], pargs)
@@ -168,7 +174,7 @@ class ManagerProcess(ABC):
class NativeProcess(ManagerProcess):
- def __init__(self, name, cwd, cmdline, should_run, enabled=True, sigkill=False, watchdog_max_dt=None):
+ def __init__(self, name, cwd, cmdline, should_run, enabled=True, sigkill=False, watchdog_max_dt=None, nice=None):
self.name = name
self.cwd = cwd
self.cmdline = cmdline
@@ -176,6 +182,7 @@ class NativeProcess(ManagerProcess):
self.enabled = enabled
self.sigkill = sigkill
self.watchdog_max_dt = watchdog_max_dt
+ self.nice = nice
self.launcher = nativelauncher
def prepare(self) -> None:
@@ -191,20 +198,21 @@ class NativeProcess(ManagerProcess):
cwd = os.path.join(BASEDIR, self.cwd)
cloudlog.info(f"starting process {self.name}")
- self.proc = Process(name=self.name, target=self.launcher, args=(self.cmdline, cwd, self.name))
+ self.proc = Process(name=self.name, target=self.launcher, args=(self.cmdline, cwd, self.name, self.nice))
self.proc.start()
self.watchdog_seen = False
self.shutting_down = False
class PythonProcess(ManagerProcess):
- def __init__(self, name, module, should_run, enabled=True, sigkill=False, watchdog_max_dt=None):
+ def __init__(self, name, module, should_run, enabled=True, sigkill=False, watchdog_max_dt=None, nice=None):
self.name = name
self.module = module
self.should_run = should_run
self.enabled = enabled
self.sigkill = sigkill
self.watchdog_max_dt = watchdog_max_dt
+ self.nice = nice
self.launcher = launcher
def prepare(self) -> None:
@@ -221,7 +229,7 @@ class PythonProcess(ManagerProcess):
return
cloudlog.info(f"starting python {self.module}")
- self.proc = Process(name=self.name, target=self.launcher, args=(self.module, self.name))
+ self.proc = Process(name=self.name, target=self.launcher, args=(self.module, self.name, self.nice))
self.proc.start()
self.watchdog_seen = False
self.shutting_down = False
diff --git a/system/manager/process_config.py b/system/manager/process_config.py
index fe31471d1..66060cb80 100644
--- a/system/manager/process_config.py
+++ b/system/manager/process_config.py
@@ -114,7 +114,8 @@ procs = [
PythonProcess("frogpilot_process", "frogpilot.frogpilot_process", always_run),
PythonProcess("mapd", "frogpilot.navigation.mapd", always_run),
PythonProcess("speed_limit_filler", "frogpilot.system.speed_limit_filler", run_speed_limit_filler),
- PythonProcess("the_pond", "frogpilot.system.the_pond.the_pond", always_run),
+ # Lower priority so onroad processes win CPU time if The Pond is busy.
+ PythonProcess("the_pond", "frogpilot.system.the_pond.the_pond", always_run, nice=15),
PythonProcess("galaxy", "frogpilot.system.galaxy.galaxy", always_run),
PythonProcess("tinygrad_modeld", "frogpilot.tinygrad_modeld.tinygrad_modeld", run_tinygrad_modeld),
]