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)