diff --git a/frogpilot/common/frogpilot_variables.py b/frogpilot/common/frogpilot_variables.py
index a09af2e12..58ae1ff9c 100644
--- a/frogpilot/common/frogpilot_variables.py
+++ b/frogpilot/common/frogpilot_variables.py
@@ -174,7 +174,7 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
("CECurves", "0", 1, "0"),
("CECurvesLead", "0", 1, "0"),
("CELead", "0", 1, "0"),
- ("CEModelStopTime", str(PLANNER_TIME - 2), 2, "0"),
+ ("CEModelStopTime", "7", 2, "0"),
("CENavigation", "0", 2, "0"),
("CENavigationIntersections", "1", 2, "0"),
("CENavigationLead", "1", 2, "0"),
diff --git a/frogpilot/system/the_pond/assets/components/router.js b/frogpilot/system/the_pond/assets/components/router.js
index 21827d144..a0ff96bc6 100644
--- a/frogpilot/system/the_pond/assets/components/router.js
+++ b/frogpilot/system/the_pond/assets/components/router.js
@@ -18,6 +18,7 @@ import { ModelManager } from "/assets/components/tools/model_manager.js?v=202603
import { LivePlots } from "/assets/components/tools/plots.js"
import { ThemeMaker } from "/assets/components/tools/theme_maker.js"
import { TestingGround } from "/assets/components/tools/testing_ground.js"
+import { Troubleshoot } from "/assets/components/tools/troubleshoot.js"
import { TmuxLog } from "/assets/components/tools/tmux.js"
import { ToggleControl } from "/assets/components/tools/toggles.js"
import { UpdateManager } from "/assets/components/tools/update_manager.js"
@@ -50,6 +51,7 @@ function Root() {
createRoute("plots", "/plots", LivePlots),
createRoute("thememaker", "/theme_maker", ThemeMaker),
createRoute("testing_ground", "/testing_ground", TestingGround),
+ createRoute("troubleshoot", "/troubleshoot", Troubleshoot),
createRoute("tmux", "/manage_tmux", TmuxLog),
createRoute("toggles", "/manage_toggles", ToggleControl),
createRoute("updates", "/manage_updates", UpdateManager),
diff --git a/frogpilot/system/the_pond/assets/components/sidebar.js b/frogpilot/system/the_pond/assets/components/sidebar.js
index f083ac548..13bf1cc70 100644
--- a/frogpilot/system/the_pond/assets/components/sidebar.js
+++ b/frogpilot/system/the_pond/assets/components/sidebar.js
@@ -23,6 +23,7 @@ const MenuItems = {
{ name: "Model Manager", link: "/manage_models", icon: "bi-cpu" },
{ name: "Plots", link: "/plots", icon: "bi-graph-up-arrow" },
{ name: "Testing Ground", link: "/testing_ground", icon: "bi-bezier2" },
+ { name: "Troubleshoot", link: "/troubleshoot", icon: "bi-tools" },
{ name: "Theme Maker", link: "/theme_maker", icon: "bi-palette-fill" },
{ name: "Tmux Log", link: "/manage_tmux", icon: "bi-terminal" },
{ name: "Backup and Restore", link: "/manage_toggles", icon: "bi-arrow-repeat" },
diff --git a/frogpilot/system/the_pond/assets/components/tools/troubleshoot.css b/frogpilot/system/the_pond/assets/components/tools/troubleshoot.css
new file mode 100644
index 000000000..bb3a7e621
--- /dev/null
+++ b/frogpilot/system/the_pond/assets/components/tools/troubleshoot.css
@@ -0,0 +1,124 @@
+.troubleshootPage {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.troubleshootCard {
+ background: var(--sidebar-bg);
+ border: 1px solid var(--sidebar-border-color);
+ border-radius: var(--border-radius-lg);
+ padding: 16px;
+}
+
+.troubleshootIntro {
+ margin: 0 0 12px;
+ opacity: 0.9;
+}
+
+.troubleshootActionRow {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ margin-bottom: 10px;
+}
+
+.troubleshootButton {
+ background: linear-gradient(135deg, #7a62b8, #8b6cc5);
+ color: #fff;
+ border: none;
+ border-radius: var(--border-radius-md);
+ padding: 10px 14px;
+ font-size: 0.95rem;
+ font-weight: 700;
+ cursor: pointer;
+}
+
+.troubleshootButton:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.troubleshootDanger {
+ background: linear-gradient(135deg, #b14a6b, #d95a7b);
+}
+
+.troubleshootError {
+ color: #ff6b6b;
+}
+
+.troubleshootStatusLine {
+ margin: 6px 0 0;
+}
+
+.troubleshootSectionHeader {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 8px;
+}
+
+.troubleshootSectionHeader h3,
+.troubleshootCard h3 {
+ margin: 0;
+}
+
+.troubleshootHeaderRow,
+.troubleshootItemRow,
+.troubleshootSnapshotRow {
+ display: grid;
+ gap: 10px;
+ align-items: center;
+ border-top: 1px solid var(--sidebar-border-color);
+ padding: 8px 0;
+}
+
+.troubleshootHeaderRow {
+ font-weight: 700;
+ opacity: 0.9;
+}
+
+.troubleshootHeaderRow {
+ grid-template-columns: 2fr 1fr 1fr;
+}
+
+.troubleshootHeaderRowSnapshot {
+ grid-template-columns: 2fr 2fr;
+}
+
+.troubleshootSnapshotRow {
+ grid-template-columns: 2fr 2fr;
+}
+
+.troubleshootItemRow {
+ grid-template-columns: 2fr 1fr 1fr;
+}
+
+.troubleshootItemLabel {
+ font-weight: 600;
+}
+
+.troubleshootItemValue,
+.troubleshootItemDefault {
+ font-family: "Open Sans", sans-serif;
+ word-break: break-word;
+}
+
+@media (max-width: 720px) {
+ .troubleshootHeaderRow,
+ .troubleshootItemRow {
+ grid-template-columns: 1.3fr 1fr 1fr;
+ font-size: 0.9rem;
+ }
+
+ .troubleshootSnapshotRow {
+ grid-template-columns: 1fr;
+ gap: 4px;
+ }
+
+ .troubleshootSectionHeader {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+}
diff --git a/frogpilot/system/the_pond/assets/components/tools/troubleshoot.js b/frogpilot/system/the_pond/assets/components/tools/troubleshoot.js
new file mode 100644
index 000000000..2bddaa543
--- /dev/null
+++ b/frogpilot/system/the_pond/assets/components/tools/troubleshoot.js
@@ -0,0 +1,208 @@
+import { html, reactive } from "https://esm.sh/@arrow-js/core"
+
+const state = reactive({
+ loading: true,
+ error: "",
+ snapshot: [],
+ sections: [],
+ isOnroad: false,
+ refreshing: false,
+ busySection: "",
+})
+
+let initialized = false
+
+function formatValue(value) {
+ if (typeof value === "boolean") return value ? "On" : "Off"
+ if (typeof value === "number") {
+ if (Number.isInteger(value)) return String(value)
+ return String(Number(value.toFixed(4)))
+ }
+ if (value === null || value === undefined) return "n/a"
+ const text = String(value).trim()
+ return text || "(empty)"
+}
+
+function buildReportText() {
+ const lines = []
+ lines.push("StarPilot Troubleshoot Report")
+ lines.push(`Generated: ${new Date().toISOString()}`)
+ lines.push(`Onroad: ${state.isOnroad ? "Yes" : "No"}`)
+ lines.push("")
+ lines.push("Snapshot")
+ for (const item of state.snapshot) {
+ lines.push(`- ${item.label}: ${formatValue(item.value)}`)
+ }
+
+ for (const section of state.sections) {
+ lines.push("")
+ lines.push(section.title)
+ for (const item of section.items || []) {
+ lines.push(`- ${item.label}: ${formatValue(item.value)} (default: ${formatValue(item.defaultValue)})`)
+ }
+ }
+
+ return lines.join("\n")
+}
+
+async function copyToClipboard() {
+ const text = buildReportText()
+ try {
+ if (navigator?.clipboard?.writeText) {
+ await navigator.clipboard.writeText(text)
+ } else {
+ const textArea = document.createElement("textarea")
+ textArea.value = text
+ textArea.style.position = "fixed"
+ textArea.style.left = "-9999px"
+ document.body.appendChild(textArea)
+ textArea.focus()
+ textArea.select()
+ document.execCommand("copy")
+ document.body.removeChild(textArea)
+ }
+ showSnackbar("Troubleshoot report copied to clipboard.", "", 2500, { key: "troubleshoot-copy" })
+ } catch (error) {
+ showSnackbar(error?.message || "Failed to copy report.", "error", 3000, { key: "troubleshoot-copy" })
+ }
+}
+
+async function fetchTroubleshoot(showToast = false) {
+ state.refreshing = true
+ try {
+ const response = await fetch("/api/troubleshoot")
+ const payload = await response.json()
+ if (!response.ok) {
+ throw new Error(payload.error || response.statusText || "Failed to load troubleshoot data")
+ }
+
+ state.snapshot = Array.isArray(payload.snapshot) ? payload.snapshot : []
+ state.sections = Array.isArray(payload.sections) ? payload.sections : []
+ state.isOnroad = !!payload.isOnroad
+ state.error = ""
+
+ if (showToast) {
+ showSnackbar("Troubleshoot data refreshed.", "", 1800, { key: "troubleshoot-refresh" })
+ }
+ } catch (error) {
+ const message = error?.message || "Failed to load troubleshoot data"
+ state.error = message
+ if (showToast) {
+ showSnackbar(message, "error", 3000, { key: "troubleshoot-refresh" })
+ }
+ } finally {
+ state.loading = false
+ state.refreshing = false
+ }
+}
+
+async function resetSection(section) {
+ const sectionId = String(section?.id || "")
+ if (!sectionId || state.busySection) return
+
+ const sectionTitle = String(section?.title || "this section")
+ if (!confirm(`Reset ${sectionTitle} to defaults?`)) return
+
+ state.busySection = sectionId
+ try {
+ const response = await fetch("/api/troubleshoot/reset", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ sectionId }),
+ })
+ const payload = await response.json()
+ if (!response.ok) {
+ throw new Error(payload.error || response.statusText || "Failed to reset section")
+ }
+
+ showSnackbar(payload.message || `${sectionTitle} reset.`, "", 2500, { key: "troubleshoot-reset" })
+ await fetchTroubleshoot(false)
+ } catch (error) {
+ showSnackbar(error?.message || "Failed to reset section.", "error", 3000, { key: "troubleshoot-reset" })
+ } finally {
+ state.busySection = ""
+ }
+}
+
+function initialize() {
+ if (initialized) return
+ initialized = true
+ fetchTroubleshoot(false)
+}
+
+function itemRows(section) {
+ const items = Array.isArray(section?.items) ? section.items : []
+ return items.map((item) => html`
+
+
${item.label}
+
${formatValue(item.value)}
+
${formatValue(item.defaultValue)}
+
+ `)
+}
+
+export function Troubleshoot() {
+ initialize()
+
+ return html`
+
+
Troubleshoot
+
+ ${() => state.loading ? html`
Loading troubleshoot data...
` : ""}
+
+ ${() => !state.loading ? html`
+
+
+ Quick diagnostics snapshot for weird behavior reports and copy-ready debug logs.
+
+
+
+
+
+ ${() => state.error ? html`
Error: ${state.error}
` : ""}
+
Onroad: ${state.isOnroad ? "Yes" : "No"}
+
+
+
+
Snapshot
+
+ ${() => state.snapshot.map((item) => html`
+
+
${item.label}
+
${formatValue(item.value)}
+
+ `)}
+
+
+ ${() => state.sections.map((section) => html`
+
+
+
+ ${itemRows(section)}
+
+ `)}
+ ` : ""}
+
+ `
+}
diff --git a/frogpilot/system/the_pond/templates/index.html b/frogpilot/system/the_pond/templates/index.html
index 26a15f2a1..96d5bbcc6 100644
--- a/frogpilot/system/the_pond/templates/index.html
+++ b/frogpilot/system/the_pond/templates/index.html
@@ -32,6 +32,7 @@
+
diff --git a/frogpilot/system/the_pond/the_pond.py b/frogpilot/system/the_pond/the_pond.py
index 472b18424..6a2792be0 100644
--- a/frogpilot/system/the_pond/the_pond.py
+++ b/frogpilot/system/the_pond/the_pond.py
@@ -213,6 +213,121 @@ _plots_state = {
"lastError": "",
}
+_TROUBLESHOOT_PERSONALITY_KEYS = [
+ "CustomPersonalities",
+ "TrafficPersonalityProfile",
+ "TrafficFollow",
+ "TrafficJerkAcceleration",
+ "TrafficJerkDeceleration",
+ "TrafficJerkDanger",
+ "TrafficJerkSpeedDecrease",
+ "TrafficJerkSpeed",
+ "AggressivePersonalityProfile",
+ "AggressiveFollow",
+ "AggressiveFollowHigh",
+ "AggressiveJerkAcceleration",
+ "AggressiveJerkDeceleration",
+ "AggressiveJerkDanger",
+ "AggressiveJerkSpeedDecrease",
+ "AggressiveJerkSpeed",
+ "StandardPersonalityProfile",
+ "StandardFollow",
+ "StandardFollowHigh",
+ "StandardJerkAcceleration",
+ "StandardJerkDeceleration",
+ "StandardJerkDanger",
+ "StandardJerkSpeedDecrease",
+ "StandardJerkSpeed",
+ "RelaxedPersonalityProfile",
+ "RelaxedFollow",
+ "RelaxedFollowHigh",
+ "RelaxedJerkAcceleration",
+ "RelaxedJerkDeceleration",
+ "RelaxedJerkDanger",
+ "RelaxedJerkSpeedDecrease",
+ "RelaxedJerkSpeed",
+]
+
+_TROUBLESHOOT_CEM_KEYS = [
+ "ConditionalExperimental",
+ "CESpeed",
+ "CESpeedLead",
+ "CECurves",
+ "CELead",
+ "CESlowerLead",
+ "CEStoppedLead",
+ "CENavigation",
+ "CEModelStopTime",
+ "CESignalSpeed",
+ "ShowCEMStatus",
+]
+
+_TROUBLESHOOT_ADVANCED_LATERAL_KEYS = [
+ "AdvancedLateralTune",
+ "SteerDelay",
+ "SteerFriction",
+ "SteerOffset",
+ "SteerKP",
+ "SteerLatAccel",
+ "SteerRatio",
+ "ForceAutoTune",
+ "ForceAutoTuneOff",
+ "ForceTorqueController",
+]
+
+_TROUBLESHOOT_ADVANCED_LONGITUDINAL_KEYS = [
+ "AdvancedLongitudinalTune",
+ "EVTuning",
+ "TruckTuning",
+ "LongitudinalActuatorDelay",
+ "StartAccel",
+ "VEgoStarting",
+ "StopAccel",
+ "StoppingDecelRate",
+ "VEgoStopping",
+]
+
+_TROUBLESHOOT_SECTION_DEFINITIONS = [
+ {
+ "id": "personality_settings",
+ "title": "Personality Profile Settings",
+ "keys": _TROUBLESHOOT_PERSONALITY_KEYS,
+ },
+ {
+ "id": "model_stop_distance",
+ "title": "Model Stop Distance",
+ "keys": ["StopDistance"],
+ },
+ {
+ "id": "cem_settings",
+ "title": "CEM Settings",
+ "keys": _TROUBLESHOOT_CEM_KEYS,
+ },
+ {
+ "id": "advanced_lateral_tuning",
+ "title": "Advanced Lateral Tuning",
+ "keys": _TROUBLESHOOT_ADVANCED_LATERAL_KEYS,
+ },
+ {
+ "id": "advanced_longitudinal_tuning",
+ "title": "Advanced Longitudinal Tuning",
+ "keys": _TROUBLESHOOT_ADVANCED_LONGITUDINAL_KEYS,
+ },
+]
+
+_TROUBLESHOOT_SECTION_BY_ID = {
+ section["id"]: section
+ for section in _TROUBLESHOOT_SECTION_DEFINITIONS
+}
+
+_TROUBLESHOOT_NON_RESETTABLE_SECTION_KEYS = {
+ "CustomPersonalities",
+ "TrafficPersonalityProfile",
+ "AggressivePersonalityProfile",
+ "StandardPersonalityProfile",
+ "RelaxedPersonalityProfile",
+}
+
def _normalize_fingerprint_make_key(make_value):
return str(make_value or "").strip().lower()
@@ -1035,26 +1150,39 @@ def write_legacy_param_file(key, value):
os.replace(tmp_path, value_path)
_layout_type_overrides = None
+_layout_param_metadata = None
-def _get_layout_type_overrides():
- global _layout_type_overrides
- if _layout_type_overrides is None:
+def _get_layout_param_metadata():
+ global _layout_param_metadata
+ if _layout_param_metadata is None:
try:
layout_path = os.path.join(os.path.dirname(__file__), "assets", "components", "tools", "device_settings_layout.json")
with open(layout_path) as f:
layout_data = json.load(f)
- _layout_type_overrides = {
- p["key"]: p["data_type"]
+ _layout_param_metadata = {
+ p["key"]: p
for section in layout_data
for p in section.get("params", [])
- if "key" in p and "data_type" in p
+ if "key" in p
}
except Exception:
- _layout_type_overrides = {}
+ _layout_param_metadata = {}
+ return _layout_param_metadata
+
+def _get_layout_type_overrides():
+ global _layout_type_overrides
+ if _layout_type_overrides is None:
+ layout_param_metadata = _get_layout_param_metadata()
+ _layout_type_overrides = {
+ key: param_data.get("data_type")
+ for key, param_data in layout_param_metadata.items()
+ if param_data.get("data_type")
+ }
return _layout_type_overrides
_cached_allowed_keys = None
_cached_param_types = None
+_cached_default_values = None
def _get_param_type_info():
global _cached_allowed_keys, _cached_param_types
@@ -1080,6 +1208,307 @@ def _get_param_type_info():
_cached_param_types = types
return _cached_allowed_keys, _cached_param_types
+def _get_default_param_values():
+ global _cached_default_values
+ if _cached_default_values is None:
+ _cached_default_values = {
+ key: default_val
+ for key, default_val, _, _ in frogpilot_default_params
+ if key not in EXCLUDED_KEYS
+ }
+ return _cached_default_values
+
+def _coerce_param_value(raw_value, value_type):
+ safe_type = value_type or str
+
+ if safe_type == bool:
+ if isinstance(raw_value, bool):
+ return raw_value
+ if isinstance(raw_value, bytes):
+ raw_value = raw_value.decode("utf-8", errors="replace")
+ return str(raw_value or "").strip() in ("1", "true", "True")
+
+ if safe_type == float:
+ if raw_value in (None, "", b""):
+ return 0.0
+ try:
+ if isinstance(raw_value, bytes):
+ raw_value = raw_value.decode("utf-8", errors="replace")
+ return float(str(raw_value).strip())
+ except Exception:
+ return 0.0
+
+ if safe_type == int:
+ if raw_value in (None, "", b""):
+ return 0
+ try:
+ if isinstance(raw_value, bytes):
+ raw_value = raw_value.decode("utf-8", errors="replace")
+ return int(float(str(raw_value).strip()))
+ except Exception:
+ return 0
+
+ if isinstance(raw_value, bytes):
+ return raw_value.decode("utf-8", errors="replace")
+ return str(raw_value or "")
+
+def _safe_params_get(key, encoding=None, default=None):
+ try:
+ if encoding is not None:
+ return params.get(key, encoding=encoding)
+ return params.get(key)
+ except Exception:
+ return default
+
+def _safe_params_get_bool(key, default=False):
+ try:
+ return params.get_bool(key)
+ except Exception:
+ return bool(default)
+
+def _is_blank_param_raw(raw_value):
+ if raw_value is None:
+ return True
+ if isinstance(raw_value, bytes):
+ return len(raw_value.strip()) == 0
+ if isinstance(raw_value, str):
+ return len(raw_value.strip()) == 0
+ return False
+
+def _get_current_param_value(key, value_type):
+ safe_type = value_type or str
+ if safe_type == bool:
+ return params.get_bool(key)
+ return _coerce_param_value(params.get(key), safe_type)
+
+def _serialize_param_write_value(raw_value):
+ if isinstance(raw_value, bool):
+ return "1" if raw_value else "0"
+ if isinstance(raw_value, bytes):
+ return raw_value.decode("utf-8", errors="replace")
+ return str(raw_value or "")
+
+def _format_longitudinal_personality(value):
+ mapping = {
+ "0": "Aggressive",
+ "1": "Standard",
+ "2": "Relaxed",
+ }
+ text = str(value or "").strip()
+ if text in mapping:
+ return mapping[text]
+ return f"Unknown ({text})" if text else "Unknown"
+
+def _resolve_troubleshoot_current_value(key, value_type, default_values):
+ safe_type = value_type or str
+
+ if safe_type == bool:
+ return _safe_params_get_bool(key)
+
+ raw_value = _safe_params_get(key)
+ if not _is_blank_param_raw(raw_value):
+ return _coerce_param_value(raw_value, safe_type)
+
+ stock_key = f"{key}Stock"
+ if stock_key in default_values:
+ stock_raw_value = _safe_params_get(stock_key)
+ if not _is_blank_param_raw(stock_raw_value):
+ return _coerce_param_value(stock_raw_value, safe_type)
+
+ default_raw_value = default_values.get(key)
+ if not _is_blank_param_raw(default_raw_value):
+ return _coerce_param_value(default_raw_value, safe_type)
+
+ return _coerce_param_value(raw_value, safe_type)
+
+def _resolve_troubleshoot_default_value(key, value_type, default_values):
+ safe_type = value_type or str
+ default_raw_value = default_values.get(key)
+ if not _is_blank_param_raw(default_raw_value):
+ return _coerce_param_value(default_raw_value, safe_type)
+
+ stock_key = f"{key}Stock"
+ if stock_key in default_values:
+ stock_current_raw = _safe_params_get(stock_key)
+ if not _is_blank_param_raw(stock_current_raw):
+ return _coerce_param_value(stock_current_raw, safe_type)
+
+ stock_default_raw = default_values.get(stock_key)
+ if not _is_blank_param_raw(stock_default_raw):
+ return _coerce_param_value(stock_default_raw, safe_type)
+
+ return _coerce_param_value(default_raw_value, safe_type)
+
+def _get_safety_snapshot_text():
+ cp_bytes = params.get("CarParamsPersistent")
+ if not cp_bytes:
+ return "Unavailable"
+
+ try:
+ with car.CarParams.from_bytes(cp_bytes) as cp:
+ safety_configs = list(getattr(cp, "safetyConfigs", []))
+ if not safety_configs:
+ return "Unavailable"
+
+ entries = []
+ for config in safety_configs:
+ model = str(getattr(config, "safetyModel", "unknown"))
+ safety_param = int(getattr(config, "safetyParam", 0))
+ entries.append(f"{model} ({safety_param} / 0x{safety_param:X})")
+
+ return ", ".join(entries) if entries else "Unavailable"
+ except Exception:
+ return "Unavailable"
+
+def _get_fingerprint_snapshot_text():
+ cp_bytes = params.get("CarParamsPersistent")
+ cp_fingerprint = ""
+ try:
+ if cp_bytes:
+ with car.CarParams.from_bytes(cp_bytes) as cp:
+ cp_fingerprint = str(getattr(cp, "carFingerprint", "") or "").strip()
+ except Exception:
+ cp_fingerprint = ""
+
+ model_name = str(params.get("CarModelName", encoding="utf-8") or "").strip()
+ model_value = str(params.get("CarModel", encoding="utf-8") or "").strip()
+
+ if model_name and model_value:
+ return f"{model_name} ({model_value})"
+ if model_name:
+ return model_name
+ if model_value:
+ return model_value
+ if cp_fingerprint:
+ return cp_fingerprint
+ return "Unknown"
+
+def _build_troubleshoot_section_payload(section_definition, value_types, default_values, layout_metadata):
+ section_keys = [str(key).strip() for key in section_definition.get("keys", []) if str(key).strip()]
+ items = []
+
+ for key in section_keys:
+ param_metadata = layout_metadata.get(key, {}) if isinstance(layout_metadata.get(key, {}), dict) else {}
+ value_type = value_types.get(key, str)
+ data_type = str(param_metadata.get("data_type") or "").strip().lower()
+ if data_type == "float":
+ value_type = float
+ elif data_type == "int":
+ value_type = int
+ elif data_type == "bool":
+ value_type = bool
+
+ label = str(param_metadata.get("label") or key)
+ try:
+ current_value = _resolve_troubleshoot_current_value(key, value_type, default_values)
+ default_value = _resolve_troubleshoot_default_value(key, value_type, default_values)
+ except Exception:
+ current_value = "Unavailable"
+ default_value = "n/a"
+
+ items.append({
+ "key": key,
+ "label": label,
+ "value": current_value,
+ "defaultValue": default_value,
+ })
+
+ return {
+ "id": section_definition["id"],
+ "title": section_definition["title"],
+ "resettable": True,
+ "items": items,
+ }
+
+def _build_troubleshoot_payload():
+ _, value_types = _get_param_type_info()
+ default_values = _get_default_param_values()
+ layout_metadata = _get_layout_param_metadata()
+
+ longitudinal_personality_raw = _safe_params_get("LongitudinalPersonality", encoding="utf-8", default="") or ""
+ snapshot_items = [
+ {
+ "id": "safety_param",
+ "label": "Safety Param",
+ "value": _get_safety_snapshot_text(),
+ "resettable": False,
+ },
+ {
+ "id": "fingerprint",
+ "label": "Fingerprint",
+ "value": _get_fingerprint_snapshot_text(),
+ "resettable": False,
+ },
+ {
+ "id": "driving_model",
+ "label": "Current Driving Model",
+ "value": str(_safe_params_get("Model", encoding="utf-8", default="") or "Unknown"),
+ "resettable": False,
+ },
+ {
+ "id": "selected_personality_profile",
+ "label": "Selected Personality Profile",
+ "value": _format_longitudinal_personality(longitudinal_personality_raw),
+ "resettable": False,
+ },
+ ]
+
+ sections = [
+ _build_troubleshoot_section_payload(section_definition, value_types, default_values, layout_metadata)
+ for section_definition in _TROUBLESHOOT_SECTION_DEFINITIONS
+ ]
+
+ return {
+ "snapshot": snapshot_items,
+ "sections": sections,
+ "isOnroad": params.get_bool("IsOnroad"),
+ }
+
+def _reset_troubleshoot_section(section_id):
+ section_definition = _TROUBLESHOOT_SECTION_BY_ID.get(str(section_id or "").strip())
+ if section_definition is None:
+ raise ValueError("Unknown troubleshoot section.")
+
+ allowed_keys, _ = _get_param_type_info()
+ default_values = _get_default_param_values()
+ is_onroad = params.get_bool("IsOnroad")
+ blocked_onroad_keys = {"Model", "AlwaysOnLateral", "ForceTorqueController", "NNFF", "NNFFLite"}
+
+ updated_keys = []
+ skipped_keys = []
+
+ for key in section_definition.get("keys", []):
+ if key in _TROUBLESHOOT_NON_RESETTABLE_SECTION_KEYS:
+ skipped_keys.append({"key": key, "reason": "preserved by design"})
+ continue
+
+ if key not in allowed_keys:
+ skipped_keys.append({"key": key, "reason": "not editable"})
+ continue
+
+ if is_onroad and key in blocked_onroad_keys:
+ skipped_keys.append({"key": key, "reason": "blocked while onroad"})
+ continue
+
+ if key not in default_values:
+ skipped_keys.append({"key": key, "reason": "default unavailable"})
+ continue
+
+ params.put(key, _serialize_param_write_value(default_values[key]))
+ updated_keys.append(key)
+
+ if updated_keys:
+ update_frogpilot_toggles()
+
+ return {
+ "sectionId": section_definition["id"],
+ "sectionTitle": section_definition["title"],
+ "updatedKeys": updated_keys,
+ "skippedKeys": skipped_keys,
+ "updatedCount": len(updated_keys),
+ "skippedCount": len(skipped_keys),
+ }
+
def _extract_testing_ground_variant_labels(slot_data, include_default=True):
labels = {}
if not isinstance(slot_data, dict):
@@ -1895,6 +2324,34 @@ def setup(app):
return jsonify(result), 200
+ @app.route("/api/troubleshoot", methods=["GET"])
+ def get_troubleshoot_data():
+ try:
+ return jsonify(_build_troubleshoot_payload()), 200
+ except Exception as exception:
+ return jsonify({"error": str(exception)}), 500
+
+ @app.route("/api/troubleshoot/reset", methods=["POST"])
+ def reset_troubleshoot_section():
+ request_data = request.get_json() or {}
+ section_id = str(request_data.get("sectionId") or "").strip()
+ if not section_id:
+ return jsonify({"error": "Missing 'sectionId' in request body."}), 400
+
+ try:
+ result = _reset_troubleshoot_section(section_id)
+ message = f"{result['sectionTitle']} reset to defaults."
+ if result["skippedCount"] > 0:
+ message += f" Updated {result['updatedCount']} setting(s), skipped {result['skippedCount']}."
+ return jsonify({
+ "message": message,
+ **result,
+ }), 200
+ except ValueError as exception:
+ return jsonify({"error": str(exception)}), 400
+ except Exception as exception:
+ return jsonify({"error": str(exception)}), 500
+
@app.route("/api/models/installed", methods=["GET"])
def get_installed_models():
catalog = get_model_catalog()