Mo' Mizu Mo' Problemz

This commit is contained in:
firestar5683
2026-04-12 15:11:28 -05:00
parent c1420b24bd
commit b9657a688a
7 changed files with 343 additions and 46 deletions
@@ -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
@@ -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
@@ -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;
@@ -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"}
</button>
${() => state.factoryResetStatus ? html`
<div class="toggle-control-status ${state.factoryResetStatus.stage === "error" ? "error" : ""}">
<div class="toggle-control-status-title">Factory Reset Status</div>
<p class="toggle-control-status-line">
<strong>Step:</strong>
${state.factoryResetStatus.progressStep}/${state.factoryResetStatus.progressTotalSteps}
${state.factoryResetStatus.progressLabel ? `- ${state.factoryResetStatus.progressLabel}` : ""}
</p>
<div class="toggle-control-status-track ${state.factoryResetStatus.stage === "error" ? "error" : ""}">
<div class="toggle-control-status-fill ${state.factoryResetStatus.stage === "error" ? "error" : ""}" style="width: ${state.factoryResetStatus.progressPercent}%;"></div>
</div>
${() => state.factoryResetStatus.message ? html`<p class="toggle-control-status-line">${state.factoryResetStatus.message}</p>` : ""}
${() => state.factoryResetStatus.progressDetail ? html`<p class="toggle-control-status-line ${state.factoryResetStatus.stage === "error" ? "error" : ""}">${state.factoryResetStatus.progressDetail}</p>` : ""}
${() => state.factoryResetStatus.lastError ? html`<p class="toggle-control-status-line error"><strong>Last Error:</strong> ${state.factoryResetStatus.lastError}</p>` : ""}
</div>
` : ""}
</div>
</section>
@@ -110,6 +110,10 @@ function shouldShowPrimaryUpdateAction() {
return !!state.checkedForUpdates && !!state.status?.updateAvailable
}
function isFactoryResetStatusActive() {
return String(state.status?.lastMode || "").trim() === "factory-reset"
}
function shouldContinuePolling() {
return !!state.status?.running || state.status?.stage === "rebooting" || reconnectPending
}
@@ -503,19 +507,21 @@ export function UpdateManager() {
<p><strong>Onroad:</strong> ${state.status?.isOnroad ? "Yes" : "No"}</p>
</div>
<div class="updateProgressCard">
<div class="updateProgressHeader">
<span>
Step ${state.status?.progressStep || 0}/${state.status?.progressTotalSteps || 5}:
${state.status?.progressLabel || "Idle"}
</span>
<span>${Math.round(toPercent(state.status?.progressPercent))}%</span>
${() => !isFactoryResetStatusActive() ? html`
<div class="updateProgressCard">
<div class="updateProgressHeader">
<span>
Step ${state.status?.progressStep || 0}/${state.status?.progressTotalSteps || 5}:
${state.status?.progressLabel || "Idle"}
</span>
<span>${Math.round(toPercent(state.status?.progressPercent))}%</span>
</div>
<div class="updateProgressTrack ${state.status?.stage === "error" ? "error" : ""}">
<div class="updateProgressFill ${state.status?.stage === "error" ? "error" : ""}" style="width: ${toPercent(state.status?.progressPercent)}%;"></div>
</div>
${() => state.status?.progressDetail ? html`<p class="updateProgressDetail ${state.status?.stage === "error" ? "error" : ""}">${state.status.progressDetail}</p>` : ""}
</div>
<div class="updateProgressTrack ${state.status?.stage === "error" ? "error" : ""}">
<div class="updateProgressFill ${state.status?.stage === "error" ? "error" : ""}" style="width: ${toPercent(state.status?.progressPercent)}%;"></div>
</div>
${() => state.status?.progressDetail ? html`<p class="updateProgressDetail ${state.status?.stage === "error" ? "error" : ""}">${state.status.progressDetail}</p>` : ""}
</div>
` : html`<p class="updateHint">Factory reset status and errors are shown on Backup/Restore.</p>`}
${() => state.status?.isOnroad ? html`<p class="updateWarning"><strong>Onroad: actions disabled</strong></p>` : ""}
@@ -573,9 +579,9 @@ export function UpdateManager() {
</div>
` : ""}
${() => state.status?.message && state.status?.stage !== "rebooting" ? html`<p class="updateMessage">${state.status.message}</p>` : ""}
${() => !isFactoryResetStatusActive() && state.status?.message && state.status?.stage !== "rebooting" ? html`<p class="updateMessage">${state.status.message}</p>` : ""}
${() => state.status?.remoteError ? html`<p class="updateError"><strong>Remote Check:</strong> ${state.status.remoteError}</p>` : ""}
${() => state.status?.lastError ? html`<p class="updateError"><strong>Last Error:</strong> ${state.status.lastError}</p>` : ""}
${() => !isFactoryResetStatusActive() && state.status?.lastError ? html`<p class="updateError"><strong>Last Error:</strong> ${state.status.lastError}</p>` : ""}
${() => state.error ? html`<p class="updateError"><strong>Error:</strong> ${state.error}</p>` : ""}
<div class="updateActions">
+32 -4
View File
@@ -37,6 +37,7 @@ from openpilot.common.realtime import set_core_affinity
from openpilot.system.hardware import HARDWARE, PC
from openpilot.system.loggerd.xattr_cache import getxattr, setxattr
from openpilot.common.swaglog import cloudlog
from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID
from openpilot.system.version import get_build_metadata
from openpilot.system.hardware.hw import Paths
@@ -812,6 +813,20 @@ def backoff(retries: int) -> int:
return random.randrange(0, min(128, int(2 ** retries)))
def get_athena_dongle_id(params: Params) -> str | None:
dongle_id = params.get("DongleId", encoding="utf-8")
if dongle_id in (None, "", UNREGISTERED_DONGLE_ID):
return None
return dongle_id
def wait_for_exit(exit_event: threading.Event | None, timeout: float) -> bool:
if exit_event is None:
time.sleep(timeout)
return False
return exit_event.wait(timeout)
def main(exit_event: threading.Event = None):
try:
set_core_affinity([0, 1, 2, 3])
@@ -819,15 +834,28 @@ def main(exit_event: threading.Event = None):
cloudlog.exception("failed to set core affinity")
params = Params()
dongle_id = params.get("DongleId")
UploadQueueCache.initialize(upload_queue)
ws_uri = ATHENA_HOST + "/ws/v2/" + dongle_id
api = Api(dongle_id)
conn_start = None
conn_retries = 0
waiting_for_dongle_id = False
while exit_event is None or not exit_event.is_set():
dongle_id = get_athena_dongle_id(params)
if dongle_id is None:
if not waiting_for_dongle_id:
cloudlog.warning("athenad.main.missing_dongle_id")
waiting_for_dongle_id = True
conn_start = None
conn_retries = 0
params.remove("LastAthenaPingTime")
if wait_for_exit(exit_event, 5):
break
continue
waiting_for_dongle_id = False
ws_uri = ATHENA_HOST + "/ws/v2/" + dongle_id
api = Api(dongle_id)
try:
if conn_start is None:
conn_start = time.monotonic()
+35
View File
@@ -0,0 +1,35 @@
import threading
import time
from openpilot.common.params import Params
from openpilot.system.athena import athenad
class TestAthenadMain:
def setup_method(self):
self.default_params = {
"DongleId": "0000000000000000",
"AthenadUploadQueue": [],
}
self.params = Params()
for k, v in self.default_params.items():
self.params.put(k, v)
def test_main_waits_for_dongle_id(self, mocker):
self.params.remove("DongleId")
mock_create_connection = mocker.patch("openpilot.system.athena.athenad.create_connection")
exit_event = threading.Event()
thread = threading.Thread(target=athenad.main, args=(exit_event,))
thread.start()
try:
time.sleep(0.2)
mock_create_connection.assert_not_called()
assert thread.is_alive()
finally:
exit_event.set()
thread.join(5)
assert not thread.is_alive()