mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-08-05 16:26:06 +08:00
Mo' Mizu Mo' Problemz
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user