diff --git a/starpilot/system/the_galaxy/assets/components/tools/device_settings.js b/starpilot/system/the_galaxy/assets/components/tools/device_settings.js index 10f57e4d0..1f4f95b2e 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/device_settings.js +++ b/starpilot/system/the_galaxy/assets/components/tools/device_settings.js @@ -36,6 +36,7 @@ const state = reactive({ fetched: false, activeSectionSlug: "", numericUpdating: {}, + actionUpdating: {}, favoriteLoading: false, favoriteSaving: false, favoriteOptions: [], @@ -860,6 +861,35 @@ async function resetNumericParam(param) { }) } +async function runSettingAction(param) { + const key = String(param?.key || "") + const endpoint = String(param?.action_endpoint || "") + if (!key || !endpoint || state.actionUpdating[key]) return + + const confirmation = String(param?.confirm_message || `Run ${param?.label || key}?`) + if (!window.confirm(confirmation)) return + + state.actionUpdating = { ...state.actionUpdating, [key]: true } + try { + const response = await fetch(endpoint, { method: "POST" }) + const payload = await response.json() + if (!response.ok) { + throw new Error(payload.error || response.statusText || "Action failed") + } + + const updated = payload.updated && typeof payload.updated === "object" ? payload.updated : {} + state.values = { ...state.values, ...updated } + showParamSnackbar(payload.message || `${param?.label || key} completed.`) + scheduleSyncInputs() + } catch (error) { + showParamSnackbar(error?.message || `${param?.label || key} failed.`, "error") + } finally { + const next = { ...state.actionUpdating } + delete next[key] + state.actionUpdating = next + } +} + async function updateParam(key, elType) { if (String(key).toLowerCase() === "starpilotfavoriteslots") { await saveFavoriteSlots(state.favoriteSlots) @@ -1230,6 +1260,7 @@ function renderSettingRow(p) { const isNumeric = p.ui_type === "numeric" const isColor = p.ui_type === "color" + const isAction = p.ui_type === "action" const isGroup = isGroupParam(p) const isChild = p.parent_key ? "ds-child-modifier" : "" const lockReason = () => getSettingLockReason(p) @@ -1238,7 +1269,16 @@ function renderSettingRow(p) { const flmTrialSummary = p.key === "AdvancedLateralTune" ? getFlmTrialSummary() : null let rowControl = "" - if (isNumeric) { + if (isAction) { + rowControl = html` + + ` + } else if (isNumeric) { rowControl = html`
${(() => { diff --git a/starpilot/system/the_galaxy/assets/components/tools/device_settings_layout.json b/starpilot/system/the_galaxy/assets/components/tools/device_settings_layout.json index 97ecf7d0c..d9f90b338 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/device_settings_layout.json +++ b/starpilot/system/the_galaxy/assets/components/tools/device_settings_layout.json @@ -626,6 +626,16 @@ "ui_type": "toggle", "parent_key": "CurveSpeedController" }, + { + "key": "ResetCurveData", + "label": "Reset Curve Data", + "description": "Clear learned Curve Speed Controller data and begin training again from the default lateral acceleration.", + "ui_type": "action", + "action_label": "Reset", + "action_endpoint": "/api/curve_speed_controller/reset", + "confirm_message": "Reset all learned Curve Speed Controller data? Training will restart from the default value.", + "parent_key": "CurveSpeedController" + }, { "key": "CustomPersonalities", "label": "Driving Personalities", diff --git a/starpilot/system/the_galaxy/tests/test_navigation_params.py b/starpilot/system/the_galaxy/tests/test_navigation_params.py index 9574041c7..50a8ada34 100644 --- a/starpilot/system/the_galaxy/tests/test_navigation_params.py +++ b/starpilot/system/the_galaxy/tests/test_navigation_params.py @@ -47,6 +47,7 @@ class WritableFakeParams: def __init__(self, values=None): self.values = dict(values or {}) self.writes = [] + self.removals = [] def get(self, key, encoding=None, default=None, block=False): del encoding, block @@ -66,6 +67,10 @@ class WritableFakeParams: self.writes.append((key, bool(value))) self.values[key] = bool(value) + def remove(self, key): + self.removals.append(key) + self.values.pop(key, None) + def _params_client(monkeypatch, values, device_type): fake_params = WritableFakeParams(values) @@ -236,3 +241,40 @@ def test_legacy_try_raylib_ui_payload_updates_use_old_ui(monkeypatch): assert fake_params.values["UseOldUI"] is False assert fake_params.values["TryRaylibUI"] is True assert fake_params.writes == [("UseOldUI", False), ("TryRaylibUI", True)] + + +def test_curve_speed_controller_reset_clears_learned_data_offroad(monkeypatch): + client, fake_params = _params_client(monkeypatch, { + "IsOnroad": False, + "CalibratedLateralAcceleration": 2.73, + "CalibrationProgress": 48.0, + "CurvatureData": {"0.01": {"average": 2.73, "count": 12}}, + }, "tici") + + response = client.post("/api/curve_speed_controller/reset") + + assert response.status_code == 200 + assert response.get_json()["updated"] == { + "CalibratedLateralAcceleration": 2.0, + "CalibrationProgress": 0.0, + } + assert fake_params.values["CalibratedLateralAcceleration"] == 2.0 + assert "CalibrationProgress" not in fake_params.values + assert "CurvatureData" not in fake_params.values + assert fake_params.removals == ["CalibrationProgress", "CurvatureData"] + + +def test_curve_speed_controller_reset_rejected_onroad(monkeypatch): + client, fake_params = _params_client(monkeypatch, { + "IsOnroad": True, + "CalibratedLateralAcceleration": 2.73, + "CalibrationProgress": 48.0, + "CurvatureData": {"0.01": {"average": 2.73, "count": 12}}, + }, "tici") + + response = client.post("/api/curve_speed_controller/reset") + + assert response.status_code == 403 + assert response.get_json()["error"] == "Curve Speed Controller data can only be reset while parked." + assert fake_params.writes == [] + assert fake_params.removals == [] diff --git a/starpilot/system/the_galaxy/the_galaxy.py b/starpilot/system/the_galaxy/the_galaxy.py index 02b76d2fb..aeeabb926 100644 --- a/starpilot/system/the_galaxy/the_galaxy.py +++ b/starpilot/system/the_galaxy/the_galaxy.py @@ -4539,6 +4539,23 @@ def setup(app): return canonical_model_key(str(value).strip()), 200 return value, 200 + @app.route("/api/curve_speed_controller/reset", methods=["POST"]) + def reset_curve_speed_controller_data(): + if params.get_bool("IsOnroad"): + return jsonify({"error": "Curve Speed Controller data can only be reset while parked."}), 403 + + params.put("CalibratedLateralAcceleration", 2.0) + params.remove("CalibrationProgress") + params.remove("CurvatureData") + + return jsonify({ + "message": "Curve Speed Controller data reset. Training will restart on the next drive.", + "updated": { + "CalibratedLateralAcceleration": 2.0, + "CalibrationProgress": 0.0, + }, + }), 200 + @app.route("/api/params/all", methods=["GET"]) def get_all_params(): migrate_cancel_button_controls(params)