mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-08-06 00:36:25 +08:00
Outsourcing the work
This commit is contained in:
Binary file not shown.
@@ -331,6 +331,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"ForceTorqueController", {PERSISTENT, BOOL, "0", "0", 3}},
|
||||
{"FLMActiveOverrides", {PERSISTENT, JSON, "{}", "{}", 2}},
|
||||
{"FLMActiveProfileId", {PERSISTENT, STRING, "", "", 2}},
|
||||
{"FLMSubmittedTune", {CLEAR_ON_MANAGER_START, JSON, "{}", "{}"}},
|
||||
{"FLMTrialBaseline", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}},
|
||||
{"FLMTrialApplied", {PERSISTENT, BOOL, "0", "0", 2}},
|
||||
{"FPSCounter", {PERSISTENT, BOOL, "0", "0", 3}},
|
||||
|
||||
Binary file not shown.
@@ -13,7 +13,7 @@ from openpilot.common.gps import get_gps_location_service
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import DT_MDL, Priority, Ratekeeper, config_realtime_process
|
||||
from openpilot.common.time_helpers import system_time_valid
|
||||
from openpilot.system.sentry import capture_report
|
||||
from openpilot.system.sentry import capture_flm_tune_submission, capture_report
|
||||
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
|
||||
@@ -116,6 +116,11 @@ def check_assets(now, model_manager, theme_manager, thread_manager, params, para
|
||||
capture_report(report_data["DiscordUser"], report_data["Issue"], vars(starpilot_toggles))
|
||||
params_memory.remove("IssueReported")
|
||||
|
||||
flm_submission = params_memory.get("FLMSubmittedTune")
|
||||
if flm_submission:
|
||||
capture_flm_tune_submission(flm_submission)
|
||||
params_memory.remove("FLMSubmittedTune")
|
||||
|
||||
if params_memory.get_bool("DownloadMaps"):
|
||||
thread_manager.run_with_lock(update_maps, (now, params, params_memory, True))
|
||||
|
||||
|
||||
@@ -512,6 +512,35 @@ async function deleteSavedTune(tune) {
|
||||
}
|
||||
}
|
||||
|
||||
async function submitSavedTune(tune) {
|
||||
if (!tune?.tuneId || state.runningAction) return
|
||||
const approved = window.confirm(
|
||||
"Think this FLM tune is genuinely good and worth sharing? Send it to Firestar for review and possible inclusion in future tuning. Only the tune values, car identity, and your Discord username are sent; routes and driving logs are not included."
|
||||
)
|
||||
if (!approved) return
|
||||
|
||||
const discordUsername = window.prompt("Enter your Discord username so Firestar can credit you.", "")
|
||||
if (discordUsername === null || !discordUsername.trim()) return
|
||||
|
||||
state.runningAction = true
|
||||
try {
|
||||
const response = await fetch(`/api/flm/saved-tunes/${encodeURIComponent(tune.tuneId)}/submit`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ discordUsername: discordUsername.trim() }),
|
||||
})
|
||||
const payload = await response.json()
|
||||
if (!response.ok) throw new Error(payload.error || "Failed to submit saved tune.")
|
||||
state.error = ""
|
||||
showSnackbar(payload.message || "Tune submitted to Firestar for review.")
|
||||
} catch (error) {
|
||||
state.error = error?.message || "Failed to submit saved tune."
|
||||
showSnackbar(state.error, "error")
|
||||
} finally {
|
||||
state.runningAction = false
|
||||
}
|
||||
}
|
||||
|
||||
async function selectPath(pathKey) {
|
||||
if (!state.report?.reportId || !pathKey || state.runningAction) return
|
||||
if (pathKey === (state.report.selectedPathKey || state.report.primaryPathKey)) return
|
||||
@@ -1263,6 +1292,9 @@ export function Tuning() {
|
||||
<p class="longManeuverMuted">
|
||||
Save a working FLM trial, switch between vehicle or trailer setups, then use Revert Trial to return to the exact manual settings from before FLM.
|
||||
</p>
|
||||
<p class="longManeuverMuted">
|
||||
Think a tune is genuinely excellent? Send it to Firestar for review and possible community sharing. Submission includes only tune values, car identity, and your Discord username, not routes or driving logs.
|
||||
</p>
|
||||
<div class="flmWorkspaceList">
|
||||
${() => (state.workspace?.savedTunes || []).length
|
||||
? state.workspace.savedTunes.map((tune) => html`
|
||||
@@ -1294,6 +1326,12 @@ export function Tuning() {
|
||||
@click="${() => deleteSavedTune(tune)}">
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
class="longManeuverButton"
|
||||
disabled="${() => state.runningAction}"
|
||||
@click="${() => submitSavedTune(tune)}">
|
||||
Send to Firestar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`)
|
||||
|
||||
@@ -2731,15 +2731,16 @@ def _active_trial_display_state(paths: dict[str, Path], snapshot: Any) -> dict[s
|
||||
def _current_car_identity(params: Params) -> dict[str, str]:
|
||||
cp_bytes = params.get("CarParamsPersistent")
|
||||
if not cp_bytes:
|
||||
return {"carFingerprint": "", "brand": ""}
|
||||
return {"carFingerprint": "", "brand": "", "carName": ""}
|
||||
try:
|
||||
with car.CarParams.from_bytes(cp_bytes) as car_params:
|
||||
return {
|
||||
"carFingerprint": str(getattr(car_params, "carFingerprint", "") or "").strip(),
|
||||
"brand": str(getattr(car_params, "brand", "") or "").strip(),
|
||||
"carName": str(getattr(car_params, "carName", "") or "").strip(),
|
||||
}
|
||||
except Exception:
|
||||
return {"carFingerprint": "", "brand": ""}
|
||||
return {"carFingerprint": "", "brand": "", "carName": ""}
|
||||
|
||||
|
||||
def _normalize_saved_tune_name(name: str) -> str:
|
||||
@@ -2751,6 +2752,22 @@ def _normalize_saved_tune_name(name: str) -> str:
|
||||
return normalized
|
||||
|
||||
|
||||
def _normalize_discord_username(username: str) -> str:
|
||||
normalized = " ".join(str(username or "").split())
|
||||
if not normalized:
|
||||
raise ValueError("A Discord username is required to submit a tune.")
|
||||
if len(normalized) > 64:
|
||||
raise ValueError("Discord usernames must be 64 characters or fewer.")
|
||||
if any(ord(character) < 32 for character in normalized):
|
||||
raise ValueError("Discord username contains an invalid control character.")
|
||||
return normalized
|
||||
|
||||
|
||||
def _saved_tune_car_name(tune: dict[str, Any]) -> str:
|
||||
raw_name = str(tune.get("carFingerprint", "") or tune.get("carName", "") or tune.get("brand", "") or "Unknown car")
|
||||
return " ".join(raw_name.replace("_", " ").split()).title()
|
||||
|
||||
|
||||
def _load_saved_tune(tune_id: str, paths: dict[str, Path] | None = None) -> dict[str, Any]:
|
||||
paths = paths or ensure_flm_workspace()
|
||||
tune = _read_json(paths["savedTunes"] / f"{tune_id}.json", {})
|
||||
@@ -3153,6 +3170,7 @@ def save_active_trial_as_tune(name: str) -> dict[str, Any]:
|
||||
current_car = _current_car_identity(params)
|
||||
car_fingerprint = current_car["carFingerprint"] or str(report_car.get("carFingerprint", "") or "")
|
||||
brand = current_car["brand"] or str(report_car.get("brand", "") or "")
|
||||
car_name = current_car.get("carName", "") or str(report_car.get("carName", "") or "")
|
||||
now = time.time()
|
||||
tune_id = f"tune-{time.time_ns()}"
|
||||
tune = {
|
||||
@@ -3163,6 +3181,7 @@ def save_active_trial_as_tune(name: str) -> dict[str, Any]:
|
||||
"updatedAt": now,
|
||||
"carFingerprint": car_fingerprint,
|
||||
"brand": brand,
|
||||
"carName": car_name,
|
||||
"sourceReportId": report_id,
|
||||
"sourceProfileId": str(display_state.get("profileId", "") or ""),
|
||||
"pathKey": str(display_state.get("pathKey", "") or ""),
|
||||
@@ -3188,6 +3207,44 @@ def save_active_trial_as_tune(name: str) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def submit_saved_tune(tune_id: str, discord_username: str) -> dict[str, Any]:
|
||||
_require_flm_offroad()
|
||||
paths = ensure_flm_workspace()
|
||||
tune = _load_saved_tune(tune_id, paths)
|
||||
discord_username = _normalize_discord_username(discord_username)
|
||||
car_name = _saved_tune_car_name(tune)
|
||||
|
||||
# Keep this payload deliberately separate from reports: tune review needs the
|
||||
# applied values, not route names, log files, camera footage, or device state.
|
||||
submitted_tune = {
|
||||
"schemaVersion": tune.get("schemaVersion", 1),
|
||||
"tuneId": str(tune.get("tuneId", tune_id) or tune_id),
|
||||
"name": str(tune.get("name", "Saved Tune") or "Saved Tune"),
|
||||
"carName": car_name,
|
||||
"carFingerprint": str(tune.get("carFingerprint", "") or ""),
|
||||
"brand": str(tune.get("brand", "") or ""),
|
||||
"baselineParams": {
|
||||
key: value for key, value in (tune.get("baselineParams", {}) or {}).items()
|
||||
if key in TRIAL_PARAM_SPECS
|
||||
},
|
||||
"genericParams": {
|
||||
key: value for key, value in (tune.get("genericParams", {}) or {}).items()
|
||||
if key in FLM_ADVANCED_LATERAL_PARAM_KEYS
|
||||
},
|
||||
"flmOverrides": normalize_flm_overrides(tune.get("flmOverrides", {})),
|
||||
}
|
||||
Params(memory=True).put("FLMSubmittedTune", {
|
||||
"discordUsername": discord_username,
|
||||
"carName": car_name,
|
||||
"tune": submitted_tune,
|
||||
})
|
||||
return {
|
||||
"message": f"Submitted {tune.get('name', 'Saved Tune')} to Firestar for review.",
|
||||
"tuneId": tune_id,
|
||||
"carName": car_name,
|
||||
}
|
||||
|
||||
|
||||
def apply_saved_tune(tune_id: str) -> dict[str, Any]:
|
||||
paths = ensure_flm_workspace()
|
||||
tune = _load_saved_tune(tune_id, paths)
|
||||
|
||||
@@ -1230,6 +1230,48 @@ def test_saved_tune_rename_delete_and_vehicle_guard(tmp_path, monkeypatch):
|
||||
assert not tune_path.exists()
|
||||
|
||||
|
||||
def test_submit_saved_tune_queues_credit_and_tune_only(tmp_path):
|
||||
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
|
||||
workspace = module.ensure_flm_workspace()
|
||||
tune_id = "tune-submit"
|
||||
(workspace["savedTunes"] / f"{tune_id}.json").write_text(json.dumps({
|
||||
"schemaVersion": 1,
|
||||
"tuneId": tune_id,
|
||||
"name": "Good Curve Tune",
|
||||
"createdAt": 1.0,
|
||||
"updatedAt": 1.0,
|
||||
"carFingerprint": "HYUNDAI_IONIQ_6",
|
||||
"brand": "hyundai",
|
||||
"sourceReportId": "report-private",
|
||||
"pathLabel": "Cleanup Pass",
|
||||
"baselineParams": {"SteerLatAccel": 2.1},
|
||||
"genericParams": {"SteerLatAccel": 2.3},
|
||||
"flmOverrides": {"vehicleKnobs": {"turn_in_boost": 0.1}},
|
||||
"routeNames": ["must-not-be-submitted"],
|
||||
}), encoding="utf-8")
|
||||
fake_params_cls._store = {"IsOnroad": False}
|
||||
fake_params_cls._memory_store = {}
|
||||
|
||||
result = module.submit_saved_tune(tune_id, "@tuner")
|
||||
submission = fake_params_cls._memory_store["FLMSubmittedTune"]
|
||||
|
||||
assert result["carName"] == "Hyundai Ioniq 6"
|
||||
assert submission["discordUsername"] == "@tuner"
|
||||
assert submission["carName"] == "Hyundai Ioniq 6"
|
||||
assert submission["tune"]["genericParams"] == {"SteerLatAccel": 2.3}
|
||||
assert "routeNames" not in submission["tune"]
|
||||
assert "routes" not in submission["tune"]
|
||||
assert "sourceReportId" not in submission["tune"]
|
||||
assert "pathLabel" not in submission["tune"]
|
||||
|
||||
with pytest.raises(ValueError, match="Discord username"):
|
||||
module.submit_saved_tune(tune_id, "")
|
||||
|
||||
fake_params_cls._store["IsOnroad"] = True
|
||||
with pytest.raises(module.FLMAnalysisCancelled, match="went onroad"):
|
||||
module.submit_saved_tune(tune_id, "@tuner")
|
||||
|
||||
|
||||
def test_saved_tune_car_switch_uses_the_destination_car_baseline(tmp_path, monkeypatch):
|
||||
module, fake_params_cls = _load_flm_workspace_module(tmp_path)
|
||||
workspace = module.ensure_flm_workspace()
|
||||
|
||||
@@ -6059,6 +6059,19 @@ def setup(app):
|
||||
except RuntimeError as error:
|
||||
return jsonify({"error": str(error)}), 409
|
||||
|
||||
@app.route(f"{LEGACY_LATERAL_METHOD_API_PREFIX}/saved-tunes/<tune_id>/submit", methods=["POST"])
|
||||
@app.route("/api/flm/saved-tunes/<tune_id>/submit", methods=["POST"])
|
||||
def submit_flm_saved_tune(tune_id):
|
||||
data = request.get_json(silent=True) or {}
|
||||
try:
|
||||
return jsonify(flm_workspace.submit_saved_tune(tune_id, str(data.get("discordUsername") or ""))), 200
|
||||
except FileNotFoundError:
|
||||
return jsonify({"error": "Saved FLM tune not found."}), 404
|
||||
except ValueError as error:
|
||||
return jsonify({"error": str(error)}), 400
|
||||
except RuntimeError as error:
|
||||
return jsonify({"error": str(error)}), 409
|
||||
|
||||
@app.route(f"{LEGACY_LATERAL_METHOD_API_PREFIX}/saved-tunes/<tune_id>", methods=["PATCH"])
|
||||
@app.route("/api/flm/saved-tunes/<tune_id>", methods=["PATCH"])
|
||||
def rename_flm_saved_tune(tune_id):
|
||||
|
||||
@@ -107,6 +107,22 @@ def capture_report(discord_user, report, starpilot_toggles):
|
||||
sentry_sdk.flush()
|
||||
|
||||
|
||||
def capture_flm_tune_submission(submission: dict) -> None:
|
||||
"""Send an explicitly submitted FLM tune without attaching drive artifacts."""
|
||||
discord_user = str(submission.get("discordUsername", "Unknown") or "Unknown")
|
||||
car_name = str(submission.get("carName", "Unknown car") or "Unknown car")
|
||||
tune = submission.get("tune", {})
|
||||
if not isinstance(tune, dict):
|
||||
tune = {}
|
||||
|
||||
capture_message(
|
||||
f"{car_name} Tune submitted by {discord_user}",
|
||||
level="info",
|
||||
tags={"report_type": "flm_tune_submission", "car_name": car_name},
|
||||
extras={"flm_tune": tune},
|
||||
)
|
||||
|
||||
|
||||
def set_tag(key: str, value: str) -> None:
|
||||
sentry_sdk.set_tag(key, value)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user