From 9dbf5db9ecea0eb2621db412b29d9b5483bc65df Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Sat, 11 Jul 2026 12:19:35 -0500 Subject: [PATCH] FTMv2 --- .../the_galaxy/assets/components/router.js | 2 +- .../assets/components/tools/tuning.js | 48 ++++++++- starpilot/system/the_galaxy/ftm_workspace.py | 97 ++++++++++++++++++- .../the_galaxy/tests/test_ftm_workspace.py | 74 ++++++++++++++ starpilot/system/the_galaxy/the_galaxy.py | 14 +++ 5 files changed, 224 insertions(+), 11 deletions(-) diff --git a/starpilot/system/the_galaxy/assets/components/router.js b/starpilot/system/the_galaxy/assets/components/router.js index 978a2c976..33e7f8a54 100644 --- a/starpilot/system/the_galaxy/assets/components/router.js +++ b/starpilot/system/the_galaxy/assets/components/router.js @@ -19,7 +19,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 { Tuning } from "/assets/components/tools/tuning.js?v=ftm-workspace-4" +import { Tuning } from "/assets/components/tools/tuning.js?v=ftm-workspace-5" import { Troubleshoot } from "/assets/components/tools/troubleshoot.js" import { TmuxLog } from "/assets/components/tools/tmux.js" import { ToggleControl } from "/assets/components/tools/toggles.js" diff --git a/starpilot/system/the_galaxy/assets/components/tools/tuning.js b/starpilot/system/the_galaxy/assets/components/tools/tuning.js index 745f15652..433920327 100644 --- a/starpilot/system/the_galaxy/assets/components/tools/tuning.js +++ b/starpilot/system/the_galaxy/assets/components/tools/tuning.js @@ -378,6 +378,30 @@ async function applyProfile(profileId) { } } +async function selectPath(pathKey) { + if (!state.report?.reportId || !pathKey || state.runningAction) return + if (pathKey === (state.report.selectedPathKey || state.report.primaryPathKey)) return + + state.runningAction = true + try { + const response = await fetch(`/api/ftm/report/${encodeURIComponent(state.report.reportId)}/path`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ pathKey }), + }) + const payload = await response.json() + if (!response.ok) throw new Error(payload.error || "Failed to select tuning path.") + state.report = payload.report + syncFeedbackState(state.report) + showSnackbar(payload.message || "Tuning path selected.") + } catch (error) { + state.error = error?.message || "Failed to select tuning path." + showSnackbar(state.error, "error") + } finally { + state.runningAction = false + } +} + async function revertProfile() { if (state.runningAction) return state.runningAction = true @@ -422,7 +446,7 @@ async function saveFeedback() { }) const payload = await response.json() if (!response.ok) throw new Error(payload.error || "Failed to save tuning feedback.") - state.report = { + state.report = payload.report || { ...state.report, feedback: payload.feedback, profiles: payload.profiles || state.report.profiles, @@ -480,7 +504,8 @@ function reportPaths() { function primaryPath() { const paths = reportPaths() - return paths.find((path) => path.isPrimary) || paths[0] || null + const selectedPathKey = state.report?.selectedPathKey || state.report?.primaryPathKey + return paths.find((path) => path.key === selectedPathKey) || paths.find((path) => path.isPrimary) || paths[0] || null } function renderProfile(profile) { @@ -586,13 +611,22 @@ function renderSuggestion(suggestion) { } function renderPathSummary(path) { + const selected = path.key === (state.report?.selectedPathKey || state.report?.primaryPathKey) return html`
${path.isPrimary ? "Recommended path" : "Alternate path"}
++ ${path.isPrimary ? "Analyzer recommended" : "Alternate path"}${selected ? " / Active" : ""} +
${path.description || ""}
Why this path: ${path.whySelected || "No path note available."}
@@ -758,7 +792,11 @@ export function Tuning() {Car: ${state.report.car?.carFingerprint || "Unknown"}
Control Path: ${state.report.car?.controlPath || "unknown"}
Friction Family: ${state.report.capabilities?.frictionFamily || "standard"}
-Recommended Path: ${primaryPath()?.title || "Recommendations"}
+Analyzer Recommended: ${reportPaths().find((path) => path.isPrimary)?.title || "Recommendations"}
+Active Path: ${primaryPath()?.title || "Recommendations"}
+Path Choice: ${state.report.pathSelectionSource === "manual" ? "Manual override" : "Automatic"}
+Nonlinear Torque Map: ${state.report.capabilities?.nonlinearTorqueMap?.asymmetric ? "Asymmetric left/right siglin" : (state.report.capabilities?.nonlinearTorqueMap ? "Symmetric siglin" : "Not detected")}
+Live Learner Refits Map: ${state.report.capabilities?.nonlinearTorqueMap ? "No" : "Not applicable"}
Processed Segments: ${safeCount(state.report.summary?.processedSegments)}
qlog Fallback: ${state.report.summary?.usedQlogFallback ? "Yes" : "No"}
Samples: ${safeCount(state.report.summary?.sampleCount)}
@@ -790,7 +828,7 @@ export function Tuning() {${primaryPath()?.whySelected || "Mark the dimensions that match what the driver felt."}
diff --git a/starpilot/system/the_galaxy/ftm_workspace.py b/starpilot/system/the_galaxy/ftm_workspace.py index 2dc20351a..7e9a5c2d5 100644 --- a/starpilot/system/the_galaxy/ftm_workspace.py +++ b/starpilot/system/the_galaxy/ftm_workspace.py @@ -581,6 +581,38 @@ def _current_param_state(CP, params: Params) -> dict[str, Any]: } +def _nonlinear_torque_map(CP) -> dict[str, Any]: + if str(getattr(CP, "brand", "") or "") != "gm": + return {} + + try: + from opendbc.car.gm.interface import NON_LINEAR_TORQUE_PARAMS + except (ImportError, AttributeError): + return {} + + raw_params = NON_LINEAR_TORQUE_PARAMS.get(CP.carFingerprint) + if raw_params is None: + return {} + + if isinstance(raw_params, dict): + left = [float(value) for value in raw_params.get("left", [])] + right = [float(value) for value in raw_params.get("right", [])] + else: + left = [float(value) for value in raw_params] + right = list(left) + + if len(left) != 4 or len(right) != 4: + return {} + + return { + "type": "siglin", + "left": left, + "right": right, + "asymmetric": any(not math.isclose(left[idx], right[idx], abs_tol=1e-9) for idx in range(4)), + "learnedByLiveTorque": False, + } + + def _baseline_family_curve(family: str) -> list[float]: getter = { "gm": get_gm_base_friction_threshold, @@ -855,6 +887,9 @@ def _primary_delta_from_summary(summary: dict[str, Any], capabilities: dict[str, supports_curvy_speed_max = _rich_profile_supports_knob(capabilities, "curvy_speed_max") supports_curvy_unwind_extra = _rich_profile_supports_knob(capabilities, f"curvy_unwind_extra_reduction_{side}") supports_curvy_unwind_floor = _rich_profile_supports_knob(capabilities, f"curvy_unwind_floor_relief_{side}") + supports_ff_gain = _rich_profile_supports_knob(capabilities, f"ff_gain_{side}") + nonlinear_map = capabilities.get("nonlinearTorqueMap", {}) + asymmetric_nonlinear_map = bool(isinstance(nonlinear_map, dict) and nonlinear_map.get("asymmetric")) if bucket == "model_limited": return None @@ -889,12 +924,20 @@ def _primary_delta_from_summary(summary: dict[str, Any], capabilities: dict[str, } if bucket in ("understeer", "late_turn_in", "saturation_limited"): + if asymmetric_nonlinear_map and direction in ("left", "right") and supports_ff_gain: + adjustment = _vehicle_knob_adjustment(f"{rich_profile}.ff_gain_{side}", 0.025 * severity, current) + if adjustment is not None: + return adjustment current_value = float(current["SteerLatAccel"]) scale = 0.04 if bucket == "saturation_limited" else 0.03 suggested_value = round(_clamp(current_value + max(scale, current_value * scale * severity), 0.5, 5.0), 4) return {"type": "generic_param", "paramKey": "SteerLatAccel", "current": current_value, "suggested": suggested_value, "delta": round(suggested_value - current_value, 4)} if bucket in ("oversteer", "early_turn_in"): + if asymmetric_nonlinear_map and direction in ("left", "right") and supports_ff_gain: + adjustment = _vehicle_knob_adjustment(f"{rich_profile}.ff_gain_{side}", -0.025 * severity, current) + if adjustment is not None: + return adjustment current_value = float(current["SteerLatAccel"]) suggested_value = round(_clamp(current_value - max(0.03, current_value * 0.03 * severity), 0.5, 5.0), 4) return {"type": "generic_param", "paramKey": "SteerLatAccel", "current": current_value, "suggested": suggested_value, "delta": round(suggested_value - current_value, 4)} @@ -1039,6 +1082,8 @@ def _likely_interpretation(summary: dict[str, Any], adjustment: dict[str, Any]) return "This looks more like a friction-threshold problem than a whole-tune problem; the controller is busy around center and needs a calmer deadzone slope." if adjustment["type"] == "vehicle_knob": symbol = adjustment["symbol"] + if "ff_gain_" in symbol: + return "This car has a directional nonlinear torque map, and the mismatch is concentrated on one side. Correct that side's feedforward layer before moving global authority." if "low_speed_angle_assist_max_torque" in symbol: return "The main torque path is waking up too late below about 8 mph, so the low-speed assist layer needs a little more authority." if "crawl_turn_in_ff_boost" in symbol: @@ -1067,6 +1112,8 @@ def _why_this_knob(adjustment: dict[str, Any]) -> str: return "This changes the threshold that maps small lateral-accel error into friction compensation without pretending the whole torque slope is wrong." if adjustment["type"] == "vehicle_knob": symbol = adjustment["symbol"] + if "ff_gain_" in symbol: + return "This compensates the affected side without flattening the car's separate left/right nonlinear torque response into one global value." if "low_speed_angle_assist_max_torque" in symbol: return "This directly raises the crawl-speed assist ceiling that fills the gap before the normal torque path wakes up." if "crawl_turn_in_ff_boost" in symbol: @@ -1474,7 +1521,10 @@ def build_recommendation_paths(report_id: str, summaries: list[dict[str, Any]], def _render_report_html(report: dict[str, Any]) -> str: report_paths = [path for path in report.get("paths", []) if isinstance(path, dict)] - primary_path = next((path for path in report_paths if path.get("isPrimary")), report_paths[0] if report_paths else {}) + selected_path_key = str(report.get("selectedPathKey") or report.get("primaryPathKey") or "") + primary_path = next((path for path in report_paths if path.get("key") == selected_path_key), None) + if primary_path is None: + primary_path = next((path for path in report_paths if path.get("isPrimary")), report_paths[0] if report_paths else {}) findings_html = [] for suggestion in report.get("suggestions", []): evidence = suggestion.get("evidence", {}) @@ -1508,7 +1558,12 @@ def _render_report_html(report: dict[str, Any]) -> str: path_html = [] for path in report_paths: - badge = "Recommended" if path.get("isPrimary") else "Alternate" + badges = [] + if path.get("isPrimary"): + badges.append("Analyzer recommended") + if path.get("key") == selected_path_key: + badges.append("Active") + badge = " / ".join(badges) or "Alternate" path_html.append( "{report['car']['carFingerprint']} | {report['car'].get('gitBranch', '')} {report['car'].get('gitCommit', '')}
" f"{', '.join(report.get('routeNames', []))}
{report['car'].get('controlPath', 'unknown')}
{report['capabilities'].get('frictionFamily', 'standard')}
{report['capabilities'].get('frictionFamily', 'standard')}
{'Asymmetric left/right siglin' if report['capabilities'].get('nonlinearTorqueMap', {}).get('asymmetric') else ('Symmetric siglin' if report['capabilities'].get('nonlinearTorqueMap') else 'Not detected')}