Add Pond long maneuvers page and live status wiring

This commit is contained in:
firestar5683
2026-03-08 16:30:12 -05:00
parent 77f314a170
commit 404922e80d
6 changed files with 422 additions and 0 deletions
@@ -6,6 +6,7 @@ import { ErrorLogs } from "/assets/components/tools/error_logs.js"
import { VehicleFeatures } from "/assets/components/tools/vehicle_features.js"
import { GalaxyPairing } from "/assets/components/tools/galaxy.js"
import { Home } from "/assets/components/home/home.js"
import { LongitudinalManeuvers } from "/assets/components/tools/longitudinal_maneuvers.js"
import { NavDestination } from "/assets/components/navigation/navigation_destination.js"
import { NavKeys } from "/assets/components/navigation/navigation_keys.js"
import { RouteRecordings } from "/assets/components/recordings/dashcam_routes.js"
@@ -45,6 +46,7 @@ function Root() {
createRoute("settings", "/settings/:section/:subsection?", SettingsView),
createRoute("speed_limits", "/download_speed_limits", SpeedLimits),
createRoute("model_manager", "/manage_models", ModelManager),
createRoute("longitudinal_maneuvers", "/longitudinal_maneuvers", LongitudinalManeuvers),
createRoute("plots", "/plots", LivePlots),
createRoute("thememaker", "/theme_maker", ThemeMaker),
createRoute("testing_ground", "/testing_ground", TestingGround),
@@ -19,6 +19,7 @@ const MenuItems = {
{ name: "Download Speed Limits", link: "/download_speed_limits", icon: "bi-download" },
{ name: "Error Logs", link: "/manage_error_logs", icon: "bi-exclamation-triangle" },
{ name: "Galaxy", link: "/galaxy", icon: "bi-globe2" },
{ name: "Long Maneuvers", link: "/longitudinal_maneuvers", icon: "bi-signpost-split" },
{ name: "Model Manager", link: "/manage_models", icon: "bi-cpu" },
{ name: "Plots", link: "/plots", icon: "bi-graph-up-arrow" },
{ name: "Testing Ground", link: "/testing_ground", icon: "bi-bezier2" },
@@ -0,0 +1,117 @@
.longManeuverPage {
max-width: var(--width-xxl);
}
.longManeuverCard {
background-color: var(--secondary-bg);
border-radius: var(--border-radius-md);
color: var(--text-color);
max-width: 90%;
padding: var(--border-radius-xl);
}
.longManeuverIntro {
margin-top: 0;
}
.longManeuverActions {
display: flex;
gap: var(--gap-sm);
margin-top: var(--margin-base);
}
.longManeuverButton {
background: var(--main-fg);
border: none;
border-radius: var(--border-radius-sm);
color: var(--text-color);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-demi-bold);
padding: var(--padding-sm) var(--padding-base);
}
.longManeuverButton:hover {
transform: var(--hover-scale-sm);
transition: transform var(--transition-fast), background-color var(--transition-fast);
}
.longManeuverButton:disabled {
cursor: not-allowed;
opacity: 0.55;
transform: none;
}
.longManeuverButton.danger {
background: var(--danger-bg);
}
.longManeuverButton.danger:hover {
background: var(--danger-hover-bg);
}
.longManeuverError {
color: var(--danger-fg);
margin-top: var(--margin-base);
}
.longManeuverMuted {
color: var(--text-muted);
}
.longManeuverStatusGrid {
display: grid;
gap: 0.5em 1em;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin-top: var(--margin-base);
}
.longManeuverStatusGrid p {
font-size: var(--font-size-sm);
margin: 0;
}
.longManeuverCurrent {
margin-top: var(--margin-base);
}
.longManeuverCurrent p {
font-size: var(--font-size-sm);
margin: 0 0 var(--margin-xs);
}
.longManeuverInstructions,
.longManeuverHistory {
margin-top: var(--margin-base);
}
.longManeuverInstructions h3,
.longManeuverHistory h3 {
margin-bottom: var(--margin-xs);
}
.longManeuverInstructions ol,
.longManeuverHistory ol {
margin: var(--margin-xs) 0 0;
padding-left: 1.4rem;
}
.longManeuverInstructions li,
.longManeuverHistory li {
font-size: var(--font-size-sm);
margin-bottom: 0.25rem;
}
@media only screen and (max-width: 768px) and (orientation: portrait) {
.longManeuverCard {
max-width: 100%;
}
.longManeuverActions {
flex-direction: column;
}
.longManeuverStatusGrid {
grid-template-columns: 1fr;
}
}
@@ -0,0 +1,170 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
const state = reactive({
loading: true,
busy: false,
error: "",
data: null,
})
let initialized = false
let pollHandle = null
const POLL_INTERVAL_MS = 1000
function safeNumber(value, fallback = 0) {
const n = Number(value)
return Number.isFinite(n) ? n : fallback
}
function formatAgeSeconds(value) {
const sec = safeNumber(value, -1)
if (sec < 0) return "unknown"
if (sec < 1) return "just now"
if (sec < 60) return `${Math.round(sec)}s ago`
const min = sec / 60
if (min < 60) return `${Math.round(min)}m ago`
const hr = min / 60
return `${Math.round(hr)}h ago`
}
async function fetchStatus() {
try {
const response = await fetch("/api/longitudinal_maneuvers/status")
const payload = await response.json()
if (!response.ok) {
throw new Error(payload.error || response.statusText || "Failed to load maneuver status")
}
state.data = payload
state.error = ""
} catch (error) {
state.error = error?.message || "Failed to load maneuver status"
} finally {
state.loading = false
}
}
function stopPolling() {
if (!pollHandle) return
clearTimeout(pollHandle)
pollHandle = null
}
function ensurePolling() {
if (pollHandle) return
const poll = async () => {
await fetchStatus()
pollHandle = setTimeout(poll, POLL_INTERVAL_MS)
}
pollHandle = setTimeout(poll, POLL_INTERVAL_MS)
}
async function runAction(action) {
if (state.busy) return
state.busy = true
try {
const response = await fetch(`/api/longitudinal_maneuvers/${action}`, { method: "POST" })
const payload = await response.json()
if (!response.ok) {
throw new Error(payload.error || response.statusText || `Failed to ${action} maneuvers`)
}
state.data = payload
state.error = ""
showSnackbar(payload.message || "Action complete.")
} catch (error) {
const message = error?.message || `Failed to ${action} maneuvers`
state.error = message
showSnackbar(message, "error")
} finally {
state.busy = false
}
}
function initialize() {
if (initialized) return
initialized = true
fetchStatus()
ensurePolling()
}
function statusLine(label, value) {
return html`<p><strong>${label}:</strong> ${value}</p>`
}
export function LongitudinalManeuvers() {
initialize()
return html`
<div class="longManeuverPage">
<h2>Long Maneuvers</h2>
<div class="longManeuverCard">
<p class="longManeuverIntro">
Run the longitudinal maneuver suite from your phone and monitor progress live.
</p>
<div class="longManeuverActions">
<button
class="longManeuverButton"
?disabled="${state.busy}"
@click="${() => runAction("start")}">
Start / Arm
</button>
<button
class="longManeuverButton danger"
?disabled="${state.busy}"
@click="${() => runAction("stop")}">
Stop
</button>
</div>
${() => state.loading ? html`<p class="longManeuverMuted">Loading status...</p>` : ""}
${() => state.error ? html`<p class="longManeuverError">${state.error}</p>` : ""}
${() => state.data ? html`
<div class="longManeuverStatusGrid">
${statusLine("Mode Enabled", state.data.modeEnabled ? "Yes" : "No")}
${statusLine("State", state.data.state || "idle")}
${statusLine("Onroad", state.data.isOnroad ? "Yes" : "No")}
${statusLine("Engaged", state.data.isEngaged ? "Yes" : "No")}
${statusLine("Phase", state.data.phase || "n/a")}
${statusLine("Paddle Mode", state.data.paddleMode || "auto")}
${statusLine("Step", `${safeNumber(state.data.stepIndex, 0)}/${safeNumber(state.data.stepTotal, 0)}`)}
${statusLine("Run", `${safeNumber(state.data.runIndex, 0)}/${safeNumber(state.data.runTotal, 0)}`)}
${statusLine("Updated", formatAgeSeconds(state.data.updatedAgeSec))}
</div>
<div class="longManeuverCurrent">
<p><strong>Current Maneuver:</strong> ${state.data.maneuver || "n/a"}</p>
<p><strong>Popup Text:</strong> ${state.data.uiText1 || ""} ${state.data.uiText2 ? `| ${state.data.uiText2}` : ""}</p>
</div>
<div class="longManeuverInstructions">
<h3>Quick Guide</h3>
<ol>
<li>Find a large, empty, straight road or lot with no traffic.</li>
<li>Press <strong>Start / Arm</strong> here, then engage openpilot with SET.</li>
<li>Keep full supervision and be ready to disengage at all times.</li>
<li>For GM pedal-long cars, the suite runs both pedal-only and pedal+paddle phases automatically.</li>
<li>When the status says complete, collect logs and generate your HTML report.</li>
</ol>
</div>
<div class="longManeuverHistory">
<h3>Progress Chain</h3>
${() => (state.data.history || []).length ? html`
<ol>
${(state.data.history || []).slice().reverse().map((line) => html`<li>${line}</li>`)}
</ol>
` : html`<p class="longManeuverMuted">No steps logged yet.</p>`}
</div>
` : ""}
</div>
</div>
`
}
@@ -37,6 +37,7 @@
<link rel="stylesheet" href="/assets/components/tools/update_manager.css">
<link rel="stylesheet" href="/assets/components/tools/device_settings.css">
<link rel="stylesheet" href="/assets/components/tools/galaxy.css">
<link rel="stylesheet" href="/assets/components/tools/longitudinal_maneuvers.css">
<link rel="stylesheet" href="/assets/components/tools/tsk_manager.css">
<script type="module" src="/assets/components/router.js"></script>
+131
View File
@@ -1314,6 +1314,116 @@ def _set_testing_ground_selection(slot_id, variant):
_write_testing_grounds_state_unlocked(state)
return state
def _default_longitudinal_maneuver_status():
return {
"state": "idle",
"phase": "",
"paddleMode": "auto",
"maneuver": "",
"runIndex": 0,
"runTotal": 0,
"stepIndex": 0,
"stepTotal": 0,
"phaseStepIndex": 0,
"phaseStepTotal": 0,
"uiShow": False,
"uiSize": "small",
"uiText1": "Long Maneuvers",
"uiText2": "",
"updatedAtSec": 0.0,
"history": [],
}
def _load_longitudinal_maneuver_status():
status = _default_longitudinal_maneuver_status()
raw = params.get("LongitudinalManeuverStatus", encoding="utf-8") or ""
if raw:
try:
payload = json.loads(raw)
if isinstance(payload, dict):
status.update(payload)
except Exception:
pass
history = status.get("history")
if not isinstance(history, list):
history = []
status["history"] = [str(line) for line in history if str(line).strip()][-120:]
try:
status["updatedAtSec"] = float(status.get("updatedAtSec") or 0.0)
except Exception:
status["updatedAtSec"] = 0.0
return status
def _save_longitudinal_maneuver_status(status):
status_copy = dict(status)
history = status_copy.get("history")
if not isinstance(history, list):
history = []
status_copy["history"] = [str(line) for line in history if str(line).strip()][-120:]
status_copy["updatedAtSec"] = float(status_copy.get("updatedAtSec") or time.time())
params.put("LongitudinalManeuverStatus", json.dumps(status_copy, separators=(",", ":")))
return status_copy
def _append_longitudinal_maneuver_history(status, line):
if not line:
return status
history = list(status.get("history", []))
history.append(str(line))
status["history"] = history[-120:]
return status
def _serialize_longitudinal_maneuver_status(status):
updated_at = _safe_float(status.get("updatedAtSec"), 0.0)
age_seconds = max(0.0, time.time() - updated_at) if updated_at > 0 else None
mode_enabled = params.get_bool("LongitudinalManeuverMode")
paddle_mode = params.get("LongitudinalManeuverPaddleMode", encoding="utf-8") or str(status.get("paddleMode") or "auto")
return {
**status,
"modeEnabled": mode_enabled,
"paddleMode": paddle_mode,
"isOnroad": params.get_bool("IsOnroad"),
"isEngaged": params.get_bool("IsEngaged"),
"updatedAgeSec": age_seconds,
}
def _set_longitudinal_maneuver_mode(enabled):
status = _load_longitudinal_maneuver_status()
if enabled:
params.put_bool("LongitudinalManeuverMode", True)
params.put("LongitudinalManeuverPaddleMode", "auto")
status.update({
"state": "armed",
"phase": "",
"maneuver": "",
"runIndex": 0,
"runTotal": 0,
"stepIndex": 0,
"phaseStepIndex": 0,
"uiShow": True,
"uiSize": "small",
"uiText1": "Long Maneuvers Armed",
"uiText2": "Engage with SET to start the test suite.",
"updatedAtSec": time.time(),
})
_append_longitudinal_maneuver_history(status, "Armed from The Pond. Engage with SET to start.")
else:
params.put_bool("LongitudinalManeuverMode", False)
params.put("LongitudinalManeuverPaddleMode", "auto")
status.update({
"state": "stopped",
"uiShow": True,
"uiSize": "small",
"uiText1": "Long Maneuvers Stopped",
"uiText2": "Test mode disabled.",
"updatedAtSec": time.time(),
})
_append_longitudinal_maneuver_history(status, "Stopped from The Pond.")
return _save_longitudinal_maneuver_status(status)
def setup(app):
model_status_debug = {
"last_signature": None,
@@ -2474,6 +2584,27 @@ def setup(app):
"isOnroad": params.get_bool("IsOnroad"),
}), 200
@app.route("/api/longitudinal_maneuvers/status", methods=["GET"])
def get_longitudinal_maneuvers_status():
status = _load_longitudinal_maneuver_status()
return jsonify(_serialize_longitudinal_maneuver_status(status)), 200
@app.route("/api/longitudinal_maneuvers/start", methods=["POST"])
def start_longitudinal_maneuvers():
status = _set_longitudinal_maneuver_mode(True)
return jsonify({
"message": "Longitudinal maneuver mode armed. Engage with SET to start.",
**_serialize_longitudinal_maneuver_status(status),
}), 200
@app.route("/api/longitudinal_maneuvers/stop", methods=["POST"])
def stop_longitudinal_maneuvers():
status = _set_longitudinal_maneuver_mode(False)
return jsonify({
"message": "Longitudinal maneuver mode disabled.",
**_serialize_longitudinal_maneuver_status(status),
}), 200
@app.route("/api/update/fast/status", methods=["GET"])
def get_fast_update_status():
state_data = _get_fast_update_state()