From b9657a688a75425424ec50eb39c6b8268fe62be4 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:11:28 -0500 Subject: [PATCH] Mo' Mizu Mo' Problemz --- .../test_conditional_experimental_mode.py | 41 ++++ .../lib/conditional_experimental_mode.py | 7 +- .../assets/components/tools/toggles.css | 51 +++++ .../assets/components/tools/toggles.js | 185 +++++++++++++++--- .../assets/components/tools/update_manager.js | 34 ++-- system/athena/athenad.py | 36 +++- system/athena/tests/test_athenad_main.py | 35 ++++ 7 files changed, 343 insertions(+), 46 deletions(-) create mode 100644 selfdrive/controls/tests/test_conditional_experimental_mode.py create mode 100644 system/athena/tests/test_athenad_main.py diff --git a/selfdrive/controls/tests/test_conditional_experimental_mode.py b/selfdrive/controls/tests/test_conditional_experimental_mode.py new file mode 100644 index 000000000..34c93cce2 --- /dev/null +++ b/selfdrive/controls/tests/test_conditional_experimental_mode.py @@ -0,0 +1,41 @@ +from types import SimpleNamespace + +from openpilot.common.constants import CV +from openpilot.starpilot.controls.lib.conditional_experimental_mode import ConditionalExperimentalMode + + +def make_cem(*, model_length: float, model_stopped: bool = False, tracking_lead: bool = False): + planner = SimpleNamespace( + params=None, + params_memory=None, + model_length=model_length, + model_stopped=model_stopped, + tracking_lead=tracking_lead, + ) + return ConditionalExperimentalMode(planner) + + +def make_sm(traffic_mode_enabled: bool = False): + return { + "starpilotCarState": SimpleNamespace(trafficModeEnabled=traffic_mode_enabled), + } + + +def test_low_speed_cruise_does_not_trigger_stop_light_from_model_stopped(): + v_ego = 10 * CV.MPH_TO_MS + model_length = v_ego * 10.0 + + cem = make_cem(model_length=model_length, model_stopped=True) + cem.stop_sign_and_light(v_ego, make_sm(), model_time=7.0) + + assert not cem.stop_light_detected + + +def test_predicted_stop_within_threshold_triggers_stop_light(): + v_ego = 30 * CV.MPH_TO_MS + model_length = v_ego * 4.0 + + cem = make_cem(model_length=model_length) + cem.stop_sign_and_light(v_ego, make_sm(), model_time=7.0) + + assert cem.stop_light_detected diff --git a/starpilot/controls/lib/conditional_experimental_mode.py b/starpilot/controls/lib/conditional_experimental_mode.py index ff58833f5..78d51e32a 100644 --- a/starpilot/controls/lib/conditional_experimental_mode.py +++ b/starpilot/controls/lib/conditional_experimental_mode.py @@ -199,7 +199,12 @@ class ConditionalExperimentalMode: model_stopping = self.starpilot_planner.model_length < v_ego * adjusted_model_time - self.stop_light_filter.update(self.starpilot_planner.model_stopped or model_stopping) + # `model_stopped` is a coarse horizon-length check (< 50 m with current constants) + # used elsewhere for force-stop/green-light behavior. Reusing it here causes + # ordinary low-speed cruising to look like a stop prediction and can latch the + # STOP_LIGHT CEM trigger. For the CEM detector, key strictly off the configured + # "predicted stop within N seconds" threshold. + self.stop_light_filter.update(model_stopping) self.stop_light_detected = bool(self.stop_light_filter.x >= THRESHOLD**2 and not self.starpilot_planner.tracking_lead) else: self.stop_light_filter.x = 0 diff --git a/starpilot/system/the_pond/assets/components/tools/toggles.css b/starpilot/system/the_pond/assets/components/tools/toggles.css index 8218cb497..dbe9c6335 100644 --- a/starpilot/system/the_pond/assets/components/tools/toggles.css +++ b/starpilot/system/the_pond/assets/components/tools/toggles.css @@ -109,6 +109,57 @@ width: 100%; } +.toggle-control-status { + background-color: rgba(10, 12, 18, 0.35); + border: 1px solid rgba(94, 200, 200, 0.2); + border-radius: var(--border-radius-lg); + margin-top: var(--padding-base); + padding: var(--padding-base); +} + +.toggle-control-status.error { + border-color: rgba(224, 85, 119, 0.4); +} + +.toggle-control-status-title { + color: var(--text-color); + font-size: var(--font-size-base); + font-weight: var(--font-weight-bold); +} + +.toggle-control-status-line { + color: var(--text-color); + font-size: var(--font-size-sm); + margin: var(--margin-sm) 0 0; +} + +.toggle-control-status-line.error { + color: var(--danger-fg); +} + +.toggle-control-status-track { + background: var(--track-color); + border-radius: var(--border-radius-sm); + height: 10px; + margin-top: var(--margin-sm); + overflow: hidden; +} + +.toggle-control-status-track.error { + border: 1px solid var(--danger-bg); +} + +.toggle-control-status-fill { + background: linear-gradient(90deg, #5ec8c8 0%, #8b6cc5 100%); + border-radius: var(--border-radius-sm); + height: 100%; + transition: width var(--transition-base); +} + +.toggle-control-status-fill.error { + background: linear-gradient(90deg, #b43a3a 0%, #de5656 100%); +} + @media only screen and (max-width: 768px) and (orientation: portrait) { .toggle-control-wrapper { flex-direction: column; diff --git a/starpilot/system/the_pond/assets/components/tools/toggles.js b/starpilot/system/the_pond/assets/components/tools/toggles.js index 4f2097a56..79d075fe5 100644 --- a/starpilot/system/the_pond/assets/components/tools/toggles.js +++ b/starpilot/system/the_pond/assets/components/tools/toggles.js @@ -2,20 +2,141 @@ import { html, reactive } from "/assets/vendor/arrow-core.js" import { Modal } from "/assets/components/modal.js" import { TailscaleControl } from "/assets/components/tailscale/tailscale.js" -export function ToggleControl() { - const state = reactive({ - showResetDefaultModal: false, - showResetStockModal: false, - showSaveMeModal: false, - factoryResetBusy: false, - }); +const FACTORY_RESET_STATUS_POLL_INTERVAL_MS = 1000 - const fileInput = document.createElement("input") +const state = reactive({ + showResetDefaultModal: false, + showResetStockModal: false, + showSaveMeModal: false, + factoryResetBusy: false, + factoryResetStatus: null, +}) + +let initialized = false +let fileInput = null +let factoryResetPollHandle = null + +function isToggleRouteActive() { + return window.location.pathname === "/manage_toggles" +} + +function isFactoryResetStatusRelevant(payload) { + return String(payload?.lastMode || "").trim() === "factory-reset" +} + +function toPercent(value) { + const n = Number(value) + if (!Number.isFinite(n)) return 0 + return Math.max(0, Math.min(100, n)) +} + +function shouldContinueFactoryResetPolling() { + return !!state.factoryResetStatus && ( + !!state.factoryResetStatus.running + || state.factoryResetStatus.stage === "factory-resetting" + || state.factoryResetStatus.stage === "rebooting" + || state.factoryResetStatus.stage === "starting" + ) +} + +function stopFactoryResetPolling() { + if (!factoryResetPollHandle) return + clearTimeout(factoryResetPollHandle) + factoryResetPollHandle = null +} + +function ensureFactoryResetPolling() { + if (factoryResetPollHandle) return + + const poll = async () => { + if (!isToggleRouteActive()) { + factoryResetPollHandle = null + return + } + + await fetchFactoryResetStatus() + if (shouldContinueFactoryResetPolling()) { + factoryResetPollHandle = setTimeout(poll, FACTORY_RESET_STATUS_POLL_INTERVAL_MS) + } else { + factoryResetPollHandle = null + } + } + + factoryResetPollHandle = setTimeout(poll, FACTORY_RESET_STATUS_POLL_INTERVAL_MS) +} + +async function fetchFactoryResetStatus() { + try { + const response = await fetch("/api/update/fast/status") + const payload = await response.json() + if (!response.ok) { + throw new Error(payload.error || response.statusText || "Failed to load factory reset status") + } + + state.factoryResetStatus = isFactoryResetStatusRelevant(payload) ? { + running: !!payload.running, + stage: String(payload.stage || "idle"), + message: String(payload.message || ""), + lastError: String(payload.lastError || ""), + progressLabel: String(payload.progressLabel || ""), + progressDetail: String(payload.progressDetail || ""), + progressPercent: toPercent(payload.progressPercent), + progressStep: Number(payload.progressStep || 0), + progressTotalSteps: Number(payload.progressTotalSteps || 5), + } : null + } catch (error) { + if (!shouldContinueFactoryResetPolling()) { + state.factoryResetStatus = null + } + } finally { + if (shouldContinueFactoryResetPolling()) { + ensureFactoryResetPolling() + } else { + stopFactoryResetPolling() + } + } +} + +async function restoreToggles(event) { + const uploadedFile = event.target.files[0] + if (uploadedFile) { + const fileContents = await uploadedFile.text() + const toggleData = JSON.parse(fileContents) + + const response = await fetch("/api/toggles/restore", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(toggleData), + }) + + const result = await response.json() + showSnackbar(result.message || "Toggles restored!") + + event.target.value = "" + } +} + +function initializeFileInput() { + if (fileInput) return + + fileInput = document.createElement("input") fileInput.type = "file" fileInput.accept = ".json" fileInput.style.display = "none" fileInput.addEventListener("change", restoreToggles) document.body.appendChild(fileInput) +} + +function initialize() { + if (initialized) return + initialized = true + + initializeFileInput() +} + +export function ToggleControl() { + initialize() + fetchFactoryResetStatus() async function backupToggles() { const response = await fetch("/api/toggles/backup", { method: "POST" }) @@ -29,25 +150,6 @@ export function ToggleControl() { URL.revokeObjectURL(downloadUrl) } - async function restoreToggles(event) { - const uploadedFile = event.target.files[0] - if (uploadedFile) { - const fileContents = await uploadedFile.text() - const toggleData = JSON.parse(fileContents) - - const response = await fetch("/api/toggles/restore", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(toggleData) - }) - - const result = await response.json() - showSnackbar(result.message || "Toggles restored!") - - event.target.value = "" - } - } - function confirmResetDefault() { state.showResetDefaultModal = true; } @@ -94,7 +196,20 @@ export function ToggleControl() { throw new Error(payload.error || response.statusText || "Failed to start factory reset") } + state.factoryResetStatus = { + running: true, + stage: "starting", + message: payload.message || "Factory reset started. Device will reboot when complete.", + lastError: "", + progressLabel: "Preparing factory reset", + progressDetail: "Initializing factory reset...", + progressPercent: 0, + progressStep: 1, + progressTotalSteps: 5, + } + ensureFactoryResetPolling() showSnackbar(payload.message || "Factory reset started.") + fetchFactoryResetStatus() } catch (error) { showSnackbar(error?.message || "Failed to start factory reset", "error") } finally { @@ -135,6 +250,22 @@ export function ToggleControl() { disabled="${() => state.factoryResetBusy}"> ${() => state.factoryResetBusy ? "Starting..." : "SAVE ME"} + ${() => state.factoryResetStatus ? html` +
+ Step: + ${state.factoryResetStatus.progressStep}/${state.factoryResetStatus.progressTotalSteps} + ${state.factoryResetStatus.progressLabel ? `- ${state.factoryResetStatus.progressLabel}` : ""} +
+${state.factoryResetStatus.message}
` : ""} + ${() => state.factoryResetStatus.progressDetail ? html`${state.factoryResetStatus.progressDetail}
` : ""} + ${() => state.factoryResetStatus.lastError ? html`Last Error: ${state.factoryResetStatus.lastError}
` : ""} +Onroad: ${state.status?.isOnroad ? "Yes" : "No"}
-${state.status.progressDetail}
` : ""}${state.status.progressDetail}
` : ""} -Factory reset status and errors are shown on Backup/Restore.
`} ${() => state.status?.isOnroad ? html`Onroad: actions disabled
` : ""} @@ -573,9 +579,9 @@ export function UpdateManager() {${state.status.message}
` : ""} + ${() => !isFactoryResetStatusActive() && state.status?.message && state.status?.stage !== "rebooting" ? html`${state.status.message}
` : ""} ${() => state.status?.remoteError ? html`Remote Check: ${state.status.remoteError}
` : ""} - ${() => state.status?.lastError ? html`Last Error: ${state.status.lastError}
` : ""} + ${() => !isFactoryResetStatusActive() && state.status?.lastError ? html`Last Error: ${state.status.lastError}
` : ""} ${() => state.error ? html`Error: ${state.error}
` : ""}