Troubleshoot

This commit is contained in:
firestar5683
2026-03-09 12:51:38 -05:00
parent b4cf18cdc2
commit 813e3183fc
7 changed files with 801 additions and 8 deletions
+1 -1
View File
@@ -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"),
@@ -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),
@@ -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" },
@@ -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;
}
}
@@ -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`
<div class="troubleshootItemRow">
<div class="troubleshootItemLabel">${item.label}</div>
<div class="troubleshootItemValue">${formatValue(item.value)}</div>
<div class="troubleshootItemDefault">${formatValue(item.defaultValue)}</div>
</div>
`)
}
export function Troubleshoot() {
initialize()
return html`
<div class="troubleshootPage">
<h2>Troubleshoot</h2>
${() => state.loading ? html`<div class="troubleshootCard">Loading troubleshoot data...</div>` : ""}
${() => !state.loading ? html`
<div class="troubleshootCard">
<p class="troubleshootIntro">
Quick diagnostics snapshot for weird behavior reports and copy-ready debug logs.
</p>
<div class="troubleshootActionRow">
<button class="troubleshootButton" ?disabled="${state.refreshing}" @click="${() => fetchTroubleshoot(true)}">
${state.refreshing ? "Refreshing..." : "Refresh"}
</button>
<button class="troubleshootButton" @click="${copyToClipboard}">
Copy to Clipboard
</button>
</div>
${() => state.error ? html`<p class="troubleshootError"><strong>Error:</strong> ${state.error}</p>` : ""}
<p class="troubleshootStatusLine"><strong>Onroad:</strong> ${state.isOnroad ? "Yes" : "No"}</p>
</div>
<div class="troubleshootCard">
<h3>Snapshot</h3>
<div class="troubleshootHeaderRow troubleshootHeaderRowSnapshot">
<span>Field</span>
<span>Value</span>
</div>
${() => state.snapshot.map((item) => html`
<div class="troubleshootSnapshotRow">
<div class="troubleshootItemLabel">${item.label}</div>
<div class="troubleshootItemValue">${formatValue(item.value)}</div>
</div>
`)}
</div>
${() => state.sections.map((section) => html`
<div class="troubleshootCard">
<div class="troubleshootSectionHeader">
<h3>${section.title}</h3>
${section.resettable ? html`
<button
class="troubleshootButton troubleshootDanger"
?disabled="${state.busySection === section.id}"
@click="${() => resetSection(section)}">
${state.busySection === section.id ? "Resetting..." : "Reset to Default"}
</button>
` : ""}
</div>
<div class="troubleshootHeaderRow">
<span>Setting</span>
<span>Current</span>
<span>Default</span>
</div>
${itemRows(section)}
</div>
`)}
` : ""}
</div>
`
}
@@ -32,6 +32,7 @@
<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/testing_ground.css">
<link rel="stylesheet" href="/assets/components/tools/troubleshoot.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/update_manager.css">
+464 -7
View File
@@ -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()