mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-14 13:52:12 +08:00
ftm
This commit is contained in:
@@ -321,6 +321,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"ForceTorqueController", {PERSISTENT, BOOL, "0", "0", 3}},
|
||||
{"FTMActiveOverrides", {PERSISTENT, JSON, "{}", "{}", 2}},
|
||||
{"FTMActiveProfileId", {PERSISTENT, STRING, "", "", 2}},
|
||||
{"FTMTrialBaseline", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}},
|
||||
{"FTMTrialApplied", {PERSISTENT, BOOL, "0", "0", 2}},
|
||||
{"FPSCounter", {PERSISTENT, BOOL, "1", "0", 3}},
|
||||
{"GalaxyDashboardStats", {PERSISTENT | DONT_LOG, JSON, "{}", "{}"}},
|
||||
|
||||
@@ -305,7 +305,53 @@ def classifier_reject_row(row: dict[str, str], split: str) -> dict[str, object]:
|
||||
}
|
||||
|
||||
|
||||
def positive_classifier_row(row: dict[str, str], split: str) -> dict[str, object]:
|
||||
def corrected_classifier_crop(
|
||||
row: dict[str, str],
|
||||
output_dir: Path,
|
||||
overwrite: bool,
|
||||
) -> tuple[str, str, bool]:
|
||||
original_crop = Path(row.get("crop_path", "")).expanduser()
|
||||
original_bbox = parse_bbox(row.get("bbox", ""))
|
||||
review_bbox = parse_bbox(row.get("review_bbox", ""))
|
||||
if review_bbox is None or review_bbox == original_bbox:
|
||||
return str(original_crop), row.get("crop_bbox", ""), False
|
||||
|
||||
frame_path = Path(row.get("frame_path", "")).expanduser()
|
||||
frame = cv2.imread(str(frame_path))
|
||||
if frame is None:
|
||||
raise RuntimeError(f"Cannot regenerate corrected crop for {row['record_key']}: unreadable frame {frame_path}")
|
||||
|
||||
image_h, image_w = frame.shape[:2]
|
||||
x1, y1, x2, y2 = review_bbox
|
||||
pad_x = max(round((x2 - x1) * 0.10), 2)
|
||||
pad_y = max(round((y2 - y1) * 0.10), 2)
|
||||
crop_bbox = (
|
||||
max(x1 - pad_x, 0),
|
||||
max(y1 - pad_y, 0),
|
||||
min(x2 + pad_x, image_w),
|
||||
min(y2 + pad_y, image_h),
|
||||
)
|
||||
crop_x1, crop_y1, crop_x2, crop_y2 = crop_bbox
|
||||
crop = frame[crop_y1:crop_y2, crop_x1:crop_x2]
|
||||
if crop.size == 0:
|
||||
raise RuntimeError(f"Cannot regenerate corrected crop for {row['record_key']}: empty review bbox {review_bbox}")
|
||||
|
||||
corrected_dir = output_dir / "corrected_classifier_crops"
|
||||
corrected_path = corrected_dir / f"{safe_stem(row['record_key'])}_crop.jpg"
|
||||
if overwrite or not corrected_path.is_file():
|
||||
ensure_dir(corrected_dir)
|
||||
if not cv2.imwrite(str(corrected_path), crop, [cv2.IMWRITE_JPEG_QUALITY, 94]):
|
||||
raise RuntimeError(f"Cannot write corrected crop for {row['record_key']}: {corrected_path}")
|
||||
crop_bbox_text = ",".join(str(value) for value in crop_bbox)
|
||||
return str(corrected_path), crop_bbox_text, True
|
||||
|
||||
|
||||
def positive_classifier_row(
|
||||
row: dict[str, str],
|
||||
split: str,
|
||||
crop_path: str | None = None,
|
||||
crop_bbox: str | None = None,
|
||||
) -> dict[str, object]:
|
||||
speed = parse_speed(row.get("review_speed_limit_mph", ""))
|
||||
return {
|
||||
"record_key": row["record_key"],
|
||||
@@ -315,10 +361,10 @@ def positive_classifier_row(row: dict[str, str], split: str) -> dict[str, object
|
||||
"split": split,
|
||||
"speed_limit_mph": speed,
|
||||
"review_sign_type": effective_sign_type(row),
|
||||
"crop_path": row.get("crop_path", ""),
|
||||
"crop_path": crop_path if crop_path is not None else row.get("crop_path", ""),
|
||||
"frame_path": row.get("frame_path", ""),
|
||||
"bbox": row.get("bbox", ""),
|
||||
"crop_bbox": row.get("crop_bbox", ""),
|
||||
"bbox": row.get("review_bbox") or row.get("bbox", ""),
|
||||
"crop_bbox": crop_bbox if crop_bbox is not None else row.get("crop_bbox", ""),
|
||||
"review_status": row.get("review_status", ""),
|
||||
"candidate_speed_limit_mph": row.get("candidate_speed_limit_mph", ""),
|
||||
"candidate_confidence": row.get("candidate_confidence", ""),
|
||||
@@ -436,10 +482,13 @@ def main() -> int:
|
||||
runtime_rows: list[dict[str, object]] = []
|
||||
detector_rows: list[dict[str, object]] = []
|
||||
reject_rows: list[dict[str, object]] = []
|
||||
corrected_classifier_crops = 0
|
||||
|
||||
for row in positive_rows:
|
||||
split = split_for_key(split_group_key(row), args.val_modulo, args.val_remainder)
|
||||
classifier_rows.append(positive_classifier_row(row, split))
|
||||
classifier_crop_path, classifier_crop_bbox, corrected = corrected_classifier_crop(row, output_dir, args.overwrite)
|
||||
classifier_rows.append(positive_classifier_row(row, split, classifier_crop_path, classifier_crop_bbox))
|
||||
corrected_classifier_crops += int(corrected)
|
||||
sample_type = "advisory_negative" if is_advisory_positive(row) else "positive"
|
||||
runtime_rows.append(runtime_row(row, split, sample_type))
|
||||
detector_row = import_detector_example(workspace, row, split, args.source_name, "positive", args.mode, args.overwrite)
|
||||
@@ -475,6 +524,7 @@ def main() -> int:
|
||||
"uncertain_positive_rows": len(uncertain_positive_rows),
|
||||
"true_negative_rows": len(true_negative_rows),
|
||||
"classifier_reject_rows": len(reject_rows),
|
||||
"corrected_classifier_crops": corrected_classifier_crops,
|
||||
"classifier_manifest": str(classifier_manifest),
|
||||
"runtime_manifest": str(runtime_manifest),
|
||||
"detector_manifest": str(detector_manifest),
|
||||
|
||||
@@ -114,8 +114,8 @@ HTML = r"""<!doctype html>
|
||||
<button data-status="uncertain">Uncertain (u)</button>
|
||||
<button data-status="needs_later">Needs Later</button>
|
||||
</div>
|
||||
<label>Box</label>
|
||||
<input id="bboxInput" placeholder="x1,y1,x2,y2 - drag on frame to set">
|
||||
<label>Box (optional - redraw only when the current box is wrong)</label>
|
||||
<input id="bboxInput" placeholder="Drag around the complete sign only when correction is needed">
|
||||
<div class="buttons">
|
||||
<button id="clearBBoxBtn">Clear Box (b)</button>
|
||||
</div>
|
||||
|
||||
@@ -191,3 +191,31 @@ def test_rescore_row_preserves_before_values_and_marks_gained_read(tmp_path):
|
||||
assert rescored["before_speed_limit_mph"] == ""
|
||||
assert rescored["candidate_speed_limit_mph"] == "20"
|
||||
assert rescored["comparison_change"] == "gained_read"
|
||||
|
||||
|
||||
def test_corrected_bbox_regenerates_classifier_crop(tmp_path):
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
frame_path = tmp_path / "frame.jpg"
|
||||
original_crop_path = tmp_path / "original_crop.jpg"
|
||||
frame = np.zeros((100, 200, 3), dtype=np.uint8)
|
||||
frame[20:80, 60:140] = 255
|
||||
cv2.imwrite(str(frame_path), frame)
|
||||
cv2.imwrite(str(original_crop_path), np.zeros((20, 20, 3), dtype=np.uint8))
|
||||
row = {
|
||||
"record_key": "corrected-box",
|
||||
"frame_path": str(frame_path),
|
||||
"crop_path": str(original_crop_path),
|
||||
"bbox": "0,0,20,20",
|
||||
"crop_bbox": "0,0,24,24",
|
||||
"review_bbox": "60,20,140,80",
|
||||
}
|
||||
|
||||
crop_path, crop_bbox, corrected = import_queue.corrected_classifier_crop(row, tmp_path, overwrite=False)
|
||||
crop = cv2.imread(crop_path)
|
||||
|
||||
assert corrected
|
||||
assert crop is not None and crop.shape[:2] == (72, 96)
|
||||
assert crop.mean() > 150
|
||||
assert crop_bbox == "52,14,148,86"
|
||||
|
||||
@@ -234,6 +234,7 @@ EXCLUDED_KEYS = {
|
||||
"ExperimentalLongitudinalEnabled",
|
||||
"FTMActiveOverrides",
|
||||
"FTMActiveProfileId",
|
||||
"FTMTrialBaseline",
|
||||
"FTMTrialApplied",
|
||||
"InstallDate",
|
||||
"StarPilotCarParamsPersistent",
|
||||
|
||||
@@ -368,6 +368,7 @@ async function applyProfile(profileId) {
|
||||
})
|
||||
const payload = await response.json()
|
||||
if (!response.ok) throw new Error(payload.error || "Failed to apply trial profile.")
|
||||
state.error = ""
|
||||
await fetchWorkspace()
|
||||
showSnackbar(payload.message || "Trial profile applied.")
|
||||
} catch (error) {
|
||||
@@ -409,6 +410,7 @@ async function revertProfile() {
|
||||
const response = await fetch("/api/ftm/trials/revert", { method: "POST" })
|
||||
const payload = await response.json()
|
||||
if (!response.ok) throw new Error(payload.error || "Failed to revert trial profile.")
|
||||
state.error = ""
|
||||
await fetchWorkspace()
|
||||
showSnackbar(payload.message || "Trial profile reverted.")
|
||||
} catch (error) {
|
||||
@@ -419,6 +421,26 @@ async function revertProfile() {
|
||||
}
|
||||
}
|
||||
|
||||
async function acceptCurrentAsBaseline() {
|
||||
if (state.runningAction || !state.workspace?.activeTrial) return
|
||||
if (!window.confirm("Keep the currently applied tuning values and end this trial? This does not restore the previous tune.")) return
|
||||
|
||||
state.runningAction = true
|
||||
try {
|
||||
const response = await fetch("/api/ftm/trials/accept", { method: "POST" })
|
||||
const payload = await response.json()
|
||||
if (!response.ok) throw new Error(payload.error || "Failed to keep the current tune.")
|
||||
state.error = ""
|
||||
state.workspace = payload.workspace || state.workspace
|
||||
showSnackbar(payload.message || "Current tune kept as the new baseline.")
|
||||
} catch (error) {
|
||||
state.error = error?.message || "Failed to keep the current tune."
|
||||
showSnackbar(state.error, "error")
|
||||
} finally {
|
||||
state.runningAction = false
|
||||
}
|
||||
}
|
||||
|
||||
function setDimensionFeedback(dimensionId, mode) {
|
||||
const accepted = new Set(state.feedbackAccepted)
|
||||
const ignored = new Set(state.feedbackIgnored)
|
||||
@@ -785,7 +807,7 @@ function renderProfile(profile) {
|
||||
</div>
|
||||
<button
|
||||
class="longManeuverButton"
|
||||
disabled="${() => state.runningAction || false}"
|
||||
disabled="${() => state.runningAction || state.workspace?.activeTrial?.rollbackAvailable === false}"
|
||||
@click="${() => applyProfile(profile.id)}">
|
||||
Apply Trial
|
||||
</button>
|
||||
@@ -926,10 +948,18 @@ export function Tuning() {
|
||||
</button>
|
||||
<button
|
||||
class="longManeuverButton"
|
||||
disabled="${() => state.runningAction || !state.workspace?.activeTrial}"
|
||||
disabled="${() => state.runningAction || !state.workspace?.activeTrial || state.workspace.activeTrial.rollbackAvailable === false}"
|
||||
@click="${revertProfile}">
|
||||
Revert Trial
|
||||
</button>
|
||||
${() => state.workspace?.activeTrial?.rollbackAvailable === false ? html`
|
||||
<button
|
||||
class="longManeuverButton"
|
||||
disabled="${() => state.runningAction}"
|
||||
@click="${acceptCurrentAsBaseline}">
|
||||
Keep Current as Baseline
|
||||
</button>
|
||||
` : ""}
|
||||
<button
|
||||
class="longManeuverButton"
|
||||
disabled="${() => state.runningAction}"
|
||||
@@ -963,6 +993,12 @@ export function Tuning() {
|
||||
<p class="longManeuverError">FTM analysis is offroad-only. Stop the car and go offroad before starting a run.</p>
|
||||
` : ""}
|
||||
|
||||
${() => state.workspace?.activeTrial?.rollbackAvailable === false ? html`
|
||||
<p class="longManeuverError">
|
||||
The original rollback data is unavailable. Keep the current tune as the new baseline before applying another trial.
|
||||
</p>
|
||||
` : ""}
|
||||
|
||||
${() => state.status?.currentSegment ? html`
|
||||
<div class="longManeuverCurrent">
|
||||
<p><strong>Current Segment:</strong> ${state.status.currentSegment}</p>
|
||||
|
||||
@@ -70,6 +70,7 @@ FTM_ADVANCED_LATERAL_PARAM_KEYS = {
|
||||
"SteerLatAccel",
|
||||
"SteerRatio",
|
||||
}
|
||||
FTM_TRIAL_BASELINE_PARAM = "FTMTrialBaseline"
|
||||
|
||||
GENERIC_PARAM_METADATA = {
|
||||
"SteerDelay": {"min": 0.01, "max": 1.0, "precision": 0.001, "deltaType": "absolute", "safeLiveTrial": True},
|
||||
@@ -2052,13 +2053,23 @@ def list_workspace() -> dict[str, Any]:
|
||||
params = Params(return_defaults=True)
|
||||
current_profile_id = params.get("FTMActiveProfileId", encoding="utf-8") or ""
|
||||
raw_active_snapshot = _read_json(paths["snapshots"] / "active.json", {})
|
||||
if not raw_active_snapshot and params.get_bool("FTMTrialApplied"):
|
||||
raw_active_snapshot = _find_revert_snapshot(paths, {}, current_profile_id) or {}
|
||||
if raw_active_snapshot:
|
||||
if params.get_bool("FTMTrialApplied"):
|
||||
active_payload = raw_active_snapshot if isinstance(raw_active_snapshot, dict) else {}
|
||||
baseline_snapshot = _find_revert_snapshot(paths, raw_active_snapshot, current_profile_id, params)
|
||||
if baseline_snapshot is not None:
|
||||
raw_active_snapshot = {
|
||||
**raw_active_snapshot,
|
||||
"profileId": current_profile_id or raw_active_snapshot.get("profileId", ""),
|
||||
**active_payload,
|
||||
"params": baseline_snapshot["params"],
|
||||
"profileId": current_profile_id or active_payload.get("profileId", ""),
|
||||
"recoveryNeeded": baseline_snapshot is not raw_active_snapshot,
|
||||
"rollbackAvailable": True,
|
||||
}
|
||||
else:
|
||||
raw_active_snapshot = {
|
||||
**active_payload,
|
||||
"profileId": current_profile_id or active_payload.get("profileId", ""),
|
||||
"recoveryNeeded": True,
|
||||
"rollbackAvailable": False,
|
||||
}
|
||||
active_snapshot = _active_trial_display_state(paths, raw_active_snapshot)
|
||||
return {
|
||||
@@ -2071,6 +2082,9 @@ def list_workspace() -> dict[str, Any]:
|
||||
|
||||
def delete_report(report_id: str) -> dict[str, Any]:
|
||||
paths = ensure_ftm_workspace()
|
||||
params = Params(return_defaults=True)
|
||||
if params.get_bool("FTMTrialApplied"):
|
||||
raise RuntimeError("Revert or keep the active FTM trial before deleting tuning reports.")
|
||||
active_snapshot = _read_json(paths["snapshots"] / "active.json", {})
|
||||
if isinstance(active_snapshot, dict) and active_snapshot.get("reportId") == report_id:
|
||||
raise RuntimeError("Revert the active FTM trial before deleting its source report.")
|
||||
@@ -2114,7 +2128,7 @@ def clear_workspace() -> dict[str, Any]:
|
||||
params = Params(return_defaults=True)
|
||||
active_snapshot = _read_json(paths["snapshots"] / "active.json", {})
|
||||
if params.get_bool("FTMTrialApplied") or (isinstance(active_snapshot, dict) and active_snapshot.get("params")):
|
||||
raise RuntimeError("Revert the active FTM trial before clearing the workspace.")
|
||||
raise RuntimeError("Revert or keep the active FTM trial before clearing the workspace.")
|
||||
|
||||
removed = []
|
||||
for key in ("reports", "profiles", "feedback", "snapshots"):
|
||||
@@ -2123,6 +2137,7 @@ def clear_workspace() -> dict[str, Any]:
|
||||
path.unlink()
|
||||
removed.append(str(path))
|
||||
|
||||
_clear_persistent_trial_baseline(params)
|
||||
_clear_ftm_status()
|
||||
|
||||
return {
|
||||
@@ -2146,6 +2161,73 @@ def _snapshot_current_trial_state(params: Params) -> dict[str, Any]:
|
||||
return snapshot
|
||||
|
||||
|
||||
def _read_persistent_trial_baseline(params: Params) -> dict[str, Any] | None:
|
||||
raw = params.get(FTM_TRIAL_BASELINE_PARAM, encoding="utf-8") or {}
|
||||
if isinstance(raw, str):
|
||||
try:
|
||||
raw = json.loads(raw)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if not isinstance(raw, dict) or not isinstance(raw.get("params"), dict):
|
||||
return None
|
||||
if raw["params"].get("FTMTrialApplied", False):
|
||||
return None
|
||||
return raw
|
||||
|
||||
|
||||
def _persist_trial_baseline(params: Params, snapshot: dict[str, Any]) -> None:
|
||||
if isinstance(snapshot.get("params"), dict) and not snapshot["params"].get("FTMTrialApplied", False):
|
||||
params.put(FTM_TRIAL_BASELINE_PARAM, snapshot)
|
||||
|
||||
|
||||
def _clear_persistent_trial_baseline(params: Params) -> None:
|
||||
params.remove(FTM_TRIAL_BASELINE_PARAM)
|
||||
|
||||
|
||||
def _profile_report_id(profile_id: str) -> str:
|
||||
return str(profile_id or "").split(":", 1)[0]
|
||||
|
||||
|
||||
def _recover_report_baseline(paths: dict[str, Path], profile_id: str,
|
||||
visited_profiles: set[str] | None = None) -> dict[str, Any] | None:
|
||||
profile_id = str(profile_id or "")
|
||||
if not profile_id:
|
||||
return None
|
||||
|
||||
visited_profiles = set(visited_profiles or set())
|
||||
if profile_id in visited_profiles:
|
||||
return None
|
||||
visited_profiles.add(profile_id)
|
||||
|
||||
report_id = _profile_report_id(profile_id)
|
||||
report = _read_json(paths["reports"] / f"{report_id}.json", {})
|
||||
report_params = report.get("currentParams") if isinstance(report, dict) else None
|
||||
if not isinstance(report_params, dict) or not report_params:
|
||||
return None
|
||||
|
||||
baseline_params = {
|
||||
key: value for key, value in report_params.items()
|
||||
if key in TRIAL_PARAM_SPECS
|
||||
}
|
||||
if not baseline_params:
|
||||
return None
|
||||
if not baseline_params.get("FTMTrialApplied", False):
|
||||
baseline_params["FTMTrialApplied"] = False
|
||||
baseline_params.setdefault("FTMActiveProfileId", "")
|
||||
return {
|
||||
"reportId": report_id,
|
||||
"profileId": profile_id,
|
||||
"capturedAt": float(report.get("createdAt", 0.0) or 0.0),
|
||||
"params": baseline_params,
|
||||
"recoverySource": "report",
|
||||
}
|
||||
|
||||
previous_profile_id = str(baseline_params.get("FTMActiveProfileId", "") or "")
|
||||
if previous_profile_id and previous_profile_id != profile_id:
|
||||
return _recover_report_baseline(paths, previous_profile_id, visited_profiles)
|
||||
return None
|
||||
|
||||
|
||||
def _apply_param_bundle(params: Params, bundle: dict[str, Any]) -> None:
|
||||
for key, value in bundle.items():
|
||||
kind = TRIAL_PARAM_SPECS.get(key)
|
||||
@@ -2177,11 +2259,16 @@ def _merge_ftm_override_state(base: dict[str, Any], delta: dict[str, Any]) -> di
|
||||
|
||||
|
||||
def _find_revert_snapshot(paths: dict[str, Path], active_snapshot: dict[str, Any],
|
||||
current_profile_id: str = "") -> dict[str, Any] | None:
|
||||
current_profile_id: str = "", params: Params | None = None) -> dict[str, Any] | None:
|
||||
if isinstance(active_snapshot, dict) and isinstance(active_snapshot.get("params"), dict):
|
||||
if not active_snapshot["params"].get("FTMTrialApplied", False):
|
||||
return active_snapshot
|
||||
|
||||
if params is not None:
|
||||
persistent_baseline = _read_persistent_trial_baseline(params)
|
||||
if persistent_baseline is not None:
|
||||
return persistent_baseline
|
||||
|
||||
cutoff = float(active_snapshot.get("capturedAt", math.inf) or math.inf) if isinstance(active_snapshot, dict) else math.inf
|
||||
candidates = []
|
||||
for path in paths["snapshots"].glob("*.json"):
|
||||
@@ -2196,11 +2283,12 @@ def _find_revert_snapshot(paths: dict[str, Path], active_snapshot: dict[str, Any
|
||||
continue
|
||||
candidates.append(candidate)
|
||||
|
||||
if not candidates:
|
||||
return None
|
||||
matching = [candidate for candidate in candidates if current_profile_id and candidate.get("profileId") == current_profile_id]
|
||||
pool = matching or candidates
|
||||
return max(pool, key=lambda candidate: float(candidate.get("capturedAt", 0.0) or 0.0))
|
||||
if candidates:
|
||||
matching = [candidate for candidate in candidates if current_profile_id and candidate.get("profileId") == current_profile_id]
|
||||
pool = matching or candidates
|
||||
return max(pool, key=lambda candidate: float(candidate.get("capturedAt", 0.0) or 0.0))
|
||||
|
||||
return _recover_report_baseline(paths, current_profile_id)
|
||||
|
||||
|
||||
def apply_trial_profile(report_id: str, profile_id: str) -> dict[str, Any]:
|
||||
@@ -2225,9 +2313,10 @@ def apply_trial_profile(report_id: str, profile_id: str) -> dict[str, Any]:
|
||||
paths,
|
||||
raw_active_snapshot,
|
||||
str(current_state.get("FTMActiveProfileId", "") or ""),
|
||||
params,
|
||||
)
|
||||
if baseline_snapshot is None:
|
||||
raise RuntimeError("The active FTM trial is missing its original rollback snapshot. Revert or reset the existing trial before applying another profile.")
|
||||
raise RuntimeError("The active FTM trial has no recoverable rollback baseline. Keep the current tune as the new baseline before applying another profile.")
|
||||
baseline_params = baseline_snapshot["params"]
|
||||
session_started_at = float(baseline_snapshot.get("sessionStartedAt", baseline_snapshot.get("capturedAt", time.time())) or time.time())
|
||||
else:
|
||||
@@ -2283,6 +2372,7 @@ def apply_trial_profile(report_id: str, profile_id: str) -> dict[str, Any]:
|
||||
}
|
||||
_write_json(paths["snapshots"] / "active.json", snapshot)
|
||||
_write_json(paths["snapshots"] / f"{report_id}-{profile_id.replace(':', '_')}.json", snapshot)
|
||||
_persist_trial_baseline(params, snapshot)
|
||||
|
||||
bundle = generic_params
|
||||
bundle["FTMActiveProfileId"] = profile_id
|
||||
@@ -2304,10 +2394,11 @@ def revert_trial_profile() -> dict[str, Any]:
|
||||
snapshot = _read_json(snapshot_path, {})
|
||||
params = Params(return_defaults=True)
|
||||
current_profile_id = params.get("FTMActiveProfileId", encoding="utf-8") or ""
|
||||
revert_snapshot = _find_revert_snapshot(paths, snapshot if isinstance(snapshot, dict) else {}, current_profile_id)
|
||||
revert_snapshot = _find_revert_snapshot(paths, snapshot if isinstance(snapshot, dict) else {}, current_profile_id, params)
|
||||
if revert_snapshot is None:
|
||||
raise FileNotFoundError("active trial snapshot")
|
||||
_apply_param_bundle(params, revert_snapshot["params"])
|
||||
_clear_persistent_trial_baseline(params)
|
||||
try:
|
||||
snapshot_path.unlink()
|
||||
except FileNotFoundError:
|
||||
@@ -2322,6 +2413,25 @@ def revert_trial_profile() -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def accept_trial_as_baseline() -> dict[str, Any]:
|
||||
paths = ensure_ftm_workspace()
|
||||
params = Params(return_defaults=True)
|
||||
active_snapshot = _read_json(paths["snapshots"] / "active.json", {})
|
||||
if not params.get_bool("FTMTrialApplied") and not (isinstance(active_snapshot, dict) and active_snapshot):
|
||||
raise FileNotFoundError("active trial")
|
||||
|
||||
params.put_bool("FTMTrialApplied", False)
|
||||
params.put("FTMActiveProfileId", "")
|
||||
_clear_persistent_trial_baseline(params)
|
||||
for path in paths["snapshots"].glob("*.json"):
|
||||
path.unlink()
|
||||
|
||||
return {
|
||||
"message": "Kept the current tuning values and made them the new FTM baseline.",
|
||||
"workspace": list_workspace(),
|
||||
}
|
||||
|
||||
|
||||
def record_feedback(report_id: str, feedback: dict[str, Any]) -> dict[str, Any]:
|
||||
paths = ensure_ftm_workspace()
|
||||
normalized = {
|
||||
|
||||
@@ -56,6 +56,9 @@ def _install_ftm_import_stubs(tmp_path):
|
||||
def put_float(self, key, value):
|
||||
self._store[key] = float(value)
|
||||
|
||||
def remove(self, key):
|
||||
self._store.pop(key, None)
|
||||
|
||||
FakeParams._store = {}
|
||||
|
||||
class FakeHyundaiFlags:
|
||||
@@ -642,6 +645,7 @@ def test_apply_and_revert_trial_profile_round_trip(tmp_path):
|
||||
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.9)
|
||||
assert fake_params_cls._store["FTMActiveProfileId"] == profile_id
|
||||
assert fake_params_cls._store["FTMTrialApplied"] is True
|
||||
assert fake_params_cls._store["FTMTrialBaseline"]["params"]["SteerLatAccel"] == pytest.approx(1.5)
|
||||
assert fake_params_cls._store["FTMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.turn_in_boost_left"] == pytest.approx(0.08)
|
||||
assert fake_params_cls._store["FTMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.unwind_taper_left"] == pytest.approx(0.55)
|
||||
|
||||
@@ -650,6 +654,7 @@ def test_apply_and_revert_trial_profile_round_trip(tmp_path):
|
||||
assert fake_params_cls._store["AdvancedLateralTune"] is False
|
||||
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.5)
|
||||
assert fake_params_cls._store["FTMTrialApplied"] is False
|
||||
assert "FTMTrialBaseline" not in fake_params_cls._store
|
||||
assert fake_params_cls._store["FTMActiveOverrides"]["vehicleKnobs"]["hyundai_ioniq_6.unwind_taper_left"] == pytest.approx(0.55)
|
||||
|
||||
|
||||
@@ -747,6 +752,94 @@ def test_orphaned_previous_revision_can_recover_its_baseline(tmp_path):
|
||||
assert fake_params_cls._store["FTMTrialApplied"] is False
|
||||
|
||||
|
||||
def test_persistent_baseline_recovers_when_snapshot_files_are_missing(tmp_path):
|
||||
module, fake_params_cls = _load_ftm_workspace_module(tmp_path)
|
||||
workspace = module.ensure_ftm_workspace()
|
||||
report_id = "report-persistent-recovery"
|
||||
profile_id = f"{report_id}:cleanup_pass:recommended"
|
||||
profile = {
|
||||
"id": profile_id,
|
||||
"label": "Recommended",
|
||||
"genericParams": {"AdvancedLateralTune": True, "SteerLatAccel": 1.8},
|
||||
"ftmOverrides": {},
|
||||
}
|
||||
(workspace["profiles"] / f"{report_id}.json").write_text(json.dumps([profile]), encoding="utf-8")
|
||||
fake_params_cls._store = {
|
||||
"AdvancedLateralTune": False,
|
||||
"SteerLatAccel": 1.5,
|
||||
"FTMActiveProfileId": "",
|
||||
"FTMActiveOverrides": {},
|
||||
"FTMTrialApplied": False,
|
||||
}
|
||||
|
||||
module.apply_trial_profile(report_id, profile_id)
|
||||
for path in workspace["snapshots"].glob("*.json"):
|
||||
path.unlink()
|
||||
|
||||
active_trial = module.list_workspace()["activeTrial"]
|
||||
assert active_trial["rollbackAvailable"] is True
|
||||
assert active_trial["recoveryNeeded"] is True
|
||||
|
||||
module.revert_trial_profile()
|
||||
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.5)
|
||||
assert fake_params_cls._store["FTMTrialApplied"] is False
|
||||
|
||||
|
||||
def test_legacy_orphan_recovers_baseline_from_source_report(tmp_path):
|
||||
module, fake_params_cls = _load_ftm_workspace_module(tmp_path)
|
||||
workspace = module.ensure_ftm_workspace()
|
||||
report_id = "report-source-recovery"
|
||||
profile_id = f"{report_id}:baseline_fix:assertive"
|
||||
(workspace["reports"] / f"{report_id}.json").write_text(json.dumps({
|
||||
"reportId": report_id,
|
||||
"createdAt": 123.0,
|
||||
"currentParams": {
|
||||
"AdvancedLateralTune": False,
|
||||
"SteerLatAccel": 1.5,
|
||||
"FTMActiveProfileId": "",
|
||||
"FTMActiveOverrides": {},
|
||||
"FTMTrialApplied": False,
|
||||
},
|
||||
}), encoding="utf-8")
|
||||
fake_params_cls._store = {
|
||||
"AdvancedLateralTune": True,
|
||||
"SteerLatAccel": 1.9,
|
||||
"FTMActiveProfileId": profile_id,
|
||||
"FTMActiveOverrides": {},
|
||||
"FTMTrialApplied": True,
|
||||
}
|
||||
|
||||
active_trial = module.list_workspace()["activeTrial"]
|
||||
assert active_trial["rollbackAvailable"] is True
|
||||
assert active_trial["recoveryNeeded"] is True
|
||||
|
||||
module.revert_trial_profile()
|
||||
assert fake_params_cls._store["AdvancedLateralTune"] is False
|
||||
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.5)
|
||||
assert fake_params_cls._store["FTMTrialApplied"] is False
|
||||
|
||||
|
||||
def test_irrecoverable_trial_can_keep_current_values_as_new_baseline(tmp_path):
|
||||
module, fake_params_cls = _load_ftm_workspace_module(tmp_path)
|
||||
module.ensure_ftm_workspace()
|
||||
fake_params_cls._store = {
|
||||
"AdvancedLateralTune": True,
|
||||
"SteerLatAccel": 1.9,
|
||||
"FTMActiveProfileId": "missing-report:baseline_fix:assertive",
|
||||
"FTMActiveOverrides": {"vehicleKnobs": {"generic.turn_in_boost_left": 0.1}},
|
||||
"FTMTrialApplied": True,
|
||||
}
|
||||
|
||||
active_trial = module.list_workspace()["activeTrial"]
|
||||
assert active_trial["rollbackAvailable"] is False
|
||||
|
||||
module.accept_trial_as_baseline()
|
||||
assert fake_params_cls._store["SteerLatAccel"] == pytest.approx(1.9)
|
||||
assert fake_params_cls._store["FTMActiveOverrides"]["vehicleKnobs"]["generic.turn_in_boost_left"] == pytest.approx(0.1)
|
||||
assert fake_params_cls._store["FTMActiveProfileId"] == ""
|
||||
assert fake_params_cls._store["FTMTrialApplied"] is False
|
||||
|
||||
|
||||
def test_workspace_hydrates_display_metadata_for_existing_active_trial(tmp_path):
|
||||
module, _ = _load_ftm_workspace_module(tmp_path)
|
||||
workspace = module.ensure_ftm_workspace()
|
||||
@@ -800,6 +893,19 @@ def test_delete_report_removes_saved_artifacts(tmp_path):
|
||||
assert not (workspace["snapshots"] / f"{report_id}-recommended.json").exists()
|
||||
|
||||
|
||||
def test_delete_report_is_blocked_while_trial_is_active(tmp_path):
|
||||
module, fake_params_cls = _load_ftm_workspace_module(tmp_path)
|
||||
workspace = module.ensure_ftm_workspace()
|
||||
report_id = "report-active-delete"
|
||||
report_path = workspace["reports"] / f"{report_id}.json"
|
||||
report_path.write_text("{}", encoding="utf-8")
|
||||
fake_params_cls._store = {"FTMTrialApplied": True}
|
||||
|
||||
with pytest.raises(RuntimeError, match="Revert or keep"):
|
||||
module.delete_report(report_id)
|
||||
assert report_path.exists()
|
||||
|
||||
|
||||
def test_select_report_path_persists_manual_override(tmp_path):
|
||||
module, _ = _load_ftm_workspace_module(tmp_path)
|
||||
workspace = module.ensure_ftm_workspace()
|
||||
|
||||
@@ -5790,6 +5790,15 @@ def setup(app):
|
||||
|
||||
return jsonify(result), 200
|
||||
|
||||
@app.route("/api/ftm/trials/accept", methods=["POST"])
|
||||
def accept_ftm_trial():
|
||||
try:
|
||||
result = ftm_workspace.accept_trial_as_baseline()
|
||||
except FileNotFoundError:
|
||||
return jsonify({"error": "No active FTM trial was found."}), 404
|
||||
|
||||
return jsonify(result), 200
|
||||
|
||||
@app.route("/api/ftm/feedback", methods=["POST"])
|
||||
def save_ftm_feedback():
|
||||
data = request.get_json(silent=True) or {}
|
||||
|
||||
@@ -32,6 +32,7 @@ KNOWN_READ_ONLY = {
|
||||
"ClusterOffset", "Compass", "DeveloperSidebarMetric1", "DeveloperSidebarMetric2",
|
||||
"DeveloperSidebarMetric3", "DeveloperSidebarMetric4", "DeveloperSidebarMetric5",
|
||||
"DeveloperSidebarMetric6", "DeveloperSidebarMetric7", "DongleId",
|
||||
"FTMActiveOverrides", "FTMActiveProfileId", "FTMTrialApplied", "FTMTrialBaseline",
|
||||
"StarPilotCarParamsPersistent", "StarPilotDrives", "StarPilotKilometers",
|
||||
"StarPilotMinutes", "GitBranch", "GitCommit", "GitCommitDate", "GitDiff",
|
||||
"GitRemote", "GithubSshKeys", "GithubUsername", "HardwareSerial", "IMEI",
|
||||
|
||||
Reference in New Issue
Block a user