mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-06 05:52:12 +08:00
Add Pond long maneuvers page and live status wiring
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user