This commit is contained in:
firestar5683
2026-03-05 16:26:12 -06:00
parent e8009ad9b9
commit cd7cb862fd
6 changed files with 862 additions and 0 deletions
@@ -14,6 +14,7 @@ import { ScreenRecordings } from "/assets/components/recordings/screen_recording
import { Sidebar } from "/assets/components/sidebar.js"
import { SpeedLimits } from "/assets/components/tools/speed_limits.js"
import { ModelManager } from "/assets/components/tools/model_manager.js?v=20260303t"
import { LivePlots } from "/assets/components/tools/plots.js"
import { ThemeMaker } from "/assets/components/tools/theme_maker.js"
import { TmuxLog } from "/assets/components/tools/tmux.js"
import { ToggleControl } from "/assets/components/tools/toggles.js"
@@ -43,6 +44,7 @@ function Root() {
createRoute("settings", "/settings/:section/:subsection?", SettingsView),
createRoute("speed_limits", "/download_speed_limits", SpeedLimits),
createRoute("model_manager", "/manage_models", ModelManager),
createRoute("plots", "/plots", LivePlots),
createRoute("thememaker", "/theme_maker", ThemeMaker),
createRoute("tmux", "/manage_tmux", TmuxLog),
createRoute("toggles", "/manage_toggles", ToggleControl),
@@ -20,6 +20,7 @@ const MenuItems = {
{ name: "Error Logs", link: "/manage_error_logs", icon: "bi-exclamation-triangle" },
{ name: "Galaxy", link: "/galaxy", icon: "bi-globe2" },
{ name: "Model Manager", link: "/manage_models", icon: "bi-cpu" },
{ name: "Plots", link: "/plots", icon: "bi-graph-up-arrow" },
{ name: "Theme Maker", link: "/theme_maker", icon: "bi-palette-fill" },
{ name: "Tmux Log", link: "/manage_tmux", icon: "bi-terminal" },
{ name: "Toggles", link: "/manage_toggles", icon: "bi-toggle-on" },
@@ -0,0 +1,212 @@
.plotsPage {
max-width: var(--width-xxxxl);
}
.plotCard {
background-color: var(--secondary-bg);
border-radius: var(--border-radius-md);
color: var(--text-color);
max-width: 95%;
padding: var(--border-radius-xl);
}
.plotStatusCard {
margin-bottom: var(--margin-base);
}
.plotDescription {
margin: 0;
}
.plotStatusGrid {
display: grid;
gap: 0.5em 1em;
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-top: var(--margin-base);
}
.plotStatusGrid p,
.plotRangeRow span,
.plotLegendItem {
font-size: var(--font-size-sm);
margin: 0;
}
.plotActions {
display: flex;
gap: var(--gap-sm);
margin-top: var(--margin-base);
}
.plotButton {
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);
}
.plotButton:hover {
transform: var(--hover-scale-sm);
transition: transform var(--transition-fast), background-color var(--transition-fast);
}
.plotCharts {
display: grid;
gap: var(--gap-sm);
grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
max-width: 95%;
}
.plotAdvancedRow {
margin: var(--margin-base) 0;
}
.plotChartCard {
max-width: 100%;
}
.plotCardHeader {
align-items: baseline;
display: flex;
gap: var(--gap-sm);
justify-content: space-between;
}
.plotSource {
color: var(--text-muted);
font-size: var(--font-size-sm);
}
.plotLegend {
display: flex;
gap: var(--gap-sm);
margin-bottom: var(--margin-sm);
}
.plotLegendItem {
align-items: center;
display: inline-flex;
gap: var(--gap-xs);
}
.plotLegendLine {
border-radius: var(--border-radius-sm);
display: inline-block;
height: 3px;
width: 22px;
}
.plotLegendLine.desired,
.plotLine.desired {
background-color: var(--main-fg);
stroke: var(--main-fg);
}
.plotLegendLine.actual,
.plotLine.actual {
background-color: var(--success-fg);
stroke: var(--success-fg);
}
.plotLegendLine.p,
.plotLine.p {
background-color: #63b3ff;
stroke: #63b3ff;
}
.plotLegendLine.i,
.plotLine.i {
background-color: #63d79d;
stroke: #63d79d;
}
.plotLegendLine.d,
.plotLine.d {
background-color: #f0b35e;
stroke: #f0b35e;
}
.plotLegendLine.f,
.plotLine.f {
background-color: #d08bff;
stroke: #d08bff;
}
.plotSvgWrap {
background: #0b0b18;
border: 1px solid var(--track-color);
border-radius: var(--border-radius-sm);
min-height: 260px;
overflow: hidden;
}
.plotSvg {
display: block;
height: 260px;
width: 100%;
}
.plotLine {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 3;
}
.plotGridLine {
stroke: rgba(255, 255, 255, 0.08);
stroke-width: 1;
}
.plotZeroLine {
stroke: rgba(255, 255, 255, 0.35);
stroke-dasharray: 6 6;
stroke-width: 1;
}
.plotRangeRow {
display: flex;
justify-content: space-between;
margin-top: var(--margin-xs);
}
.plotEmpty {
align-items: center;
color: var(--text-muted);
display: flex;
font-size: var(--font-size-sm);
height: 260px;
justify-content: center;
}
.plotError {
color: var(--danger-fg);
margin-top: var(--margin-sm);
}
@media only screen and (max-width: 768px) and (orientation: portrait) {
.plotCard,
.plotCharts {
max-width: 100%;
}
.plotCharts {
grid-template-columns: 1fr;
}
.plotStatusGrid {
grid-template-columns: 1fr;
}
.plotActions {
flex-direction: column;
}
.plotLegend {
flex-direction: column;
gap: var(--gap-xs);
}
}
@@ -0,0 +1,447 @@
import { html, reactive } from "https://esm.sh/@arrow-js/core"
const state = reactive({
loading: true,
error: "",
paused: false,
showAdvancedTerms: false,
live: null,
samples: [],
})
let initialized = false
let pollHandle = null
let lastTimestamp = 0
const MAX_POINTS = 240
const POLL_INTERVAL_MS = 500
const SVG_WIDTH = 1000
const SVG_HEIGHT = 260
const ADVANCED_TERMS_KEY = "plotsShowAdvancedTerms"
function isPlotsRouteActive() {
return window.location.pathname === "/plots"
}
function toNumber(value, fallback = 0) {
const n = Number(value)
return Number.isFinite(n) ? n : fallback
}
function formatValue(value, digits = 2) {
return toNumber(value).toFixed(digits)
}
function formatAge(seconds) {
const value = Math.max(0, toNumber(seconds))
if (value < 1) return `${Math.round(value * 1000)} ms`
return `${value.toFixed(1)} s`
}
function formatSourceLabel(kind, source) {
const normalizedKind = String(kind || "").toLowerCase()
const normalizedSource = String(source || "").toLowerCase()
if (normalizedKind === "lateral") {
if (normalizedSource === "torquestate") return "Steering controller output"
if (normalizedSource === "curvature") return "Path model estimate"
}
if (normalizedKind === "longitudinal") {
if (normalizedSource.includes("livelocationkalman")) return "Control output + calibrated acceleration"
if (normalizedSource === "controlsstate") return "Planner target output"
}
if (normalizedKind === "lateralterms") {
if (normalizedSource === "torquestate") return "Steering torque controller"
if (normalizedSource === "pidstate") return "Steering angle PID controller"
}
if (normalizedKind === "longitudinalterms") {
if (normalizedSource === "controlsstate") return "Longitudinal controller terms"
}
return "Live control signal"
}
function stopPolling() {
if (!pollHandle) return
clearTimeout(pollHandle)
pollHandle = null
}
function pushSample(payload) {
const timestamp = toNumber(payload.timestamp, 0)
if (!timestamp || timestamp <= 0 || timestamp === lastTimestamp) return
lastTimestamp = timestamp
state.samples.push({
timestamp,
desiredLateralAccel: toNumber(payload.desiredLateralAccel),
actualLateralAccel: toNumber(payload.actualLateralAccel),
desiredLongitudinalAccel: toNumber(payload.desiredLongitudinalAccel),
actualLongitudinalAccel: toNumber(payload.actualLongitudinalAccel),
lateralP: toNumber(payload.lateralP),
lateralI: toNumber(payload.lateralI),
lateralD: toNumber(payload.lateralD),
lateralF: toNumber(payload.lateralF),
longitudinalUpAccelCmd: toNumber(payload.longitudinalUpAccelCmd),
longitudinalUiAccelCmd: toNumber(payload.longitudinalUiAccelCmd),
longitudinalUfAccelCmd: toNumber(payload.longitudinalUfAccelCmd),
})
if (state.samples.length > MAX_POINTS) {
state.samples.splice(0, state.samples.length - MAX_POINTS)
}
}
async function fetchLiveData() {
const response = await fetch("/api/plots/live")
const payload = await response.json()
if (!response.ok) {
throw new Error(payload.error || response.statusText || "Failed to load live plot data")
}
state.live = payload
state.error = ""
state.loading = false
if (!payload.stale) {
pushSample(payload)
}
}
function ensurePolling() {
if (pollHandle) return
const poll = async () => {
if (!isPlotsRouteActive()) {
pollHandle = null
return
}
if (!state.paused) {
try {
await fetchLiveData()
} catch (error) {
state.error = error?.message || String(error)
state.loading = false
}
}
pollHandle = setTimeout(poll, POLL_INTERVAL_MS)
}
pollHandle = setTimeout(poll, POLL_INTERVAL_MS)
}
function clearHistory() {
state.samples = []
lastTimestamp = 0
}
function togglePaused() {
state.paused = !state.paused
if (state.paused) {
stopPolling()
return
}
if (!isPlotsRouteActive()) return
fetchLiveData().catch((error) => {
state.error = error?.message || String(error)
})
ensurePolling()
}
function toggleAdvancedTerms() {
state.showAdvancedTerms = !state.showAdvancedTerms
try {
localStorage.setItem(ADVANCED_TERMS_KEY, state.showAdvancedTerms ? "1" : "0")
} catch (error) {
console.warn("Failed to persist plots advanced terms preference", error)
}
}
function yForValue(value, min, max) {
const safeMin = toNumber(min, -1)
const safeMax = toNumber(max, 1)
const clamped = Math.max(safeMin, Math.min(safeMax, toNumber(value)))
const span = Math.max(1e-6, safeMax - safeMin)
return ((safeMax - clamped) / span) * SVG_HEIGHT
}
function computeRange(samples, desiredKey, actualKey) {
let maxAbs = 0
for (const sample of samples) {
maxAbs = Math.max(
maxAbs,
Math.abs(toNumber(sample?.[desiredKey])),
Math.abs(toNumber(sample?.[actualKey])),
)
}
const halfSpan = Math.max(1.5, Math.ceil((maxAbs * 1.25) * 10) / 10)
return { min: -halfSpan, max: halfSpan }
}
function buildPolyline(samples, key, min, max) {
if (!samples.length) return ""
const lastIndex = Math.max(1, samples.length - 1)
return samples.map((sample, index) => {
const x = (index / lastIndex) * SVG_WIDTH
const y = yForValue(sample?.[key], min, max)
return `${x.toFixed(2)},${y.toFixed(2)}`
}).join(" ")
}
function computeRangeForKeys(samples, keys) {
let maxAbs = 0
for (const sample of samples) {
for (const key of keys) {
maxAbs = Math.max(maxAbs, Math.abs(toNumber(sample?.[key])))
}
}
const halfSpan = Math.max(0.15, Math.ceil((maxAbs * 1.35) * 1000) / 1000)
return { min: -halfSpan, max: halfSpan }
}
function latestSampleValue(key) {
const latest = state.samples[state.samples.length - 1]
if (latest) return latest[key]
return toNumber(state.live?.[key])
}
function PlotCard(title, desiredKey, actualKey, sourceKind, sourceLabel, desiredLabel = "Target", actualLabel = "Measured") {
const hasData = state.samples.length > 1
const range = computeRange(state.samples, desiredKey, actualKey)
const desiredPolyline = buildPolyline(state.samples, desiredKey, range.min, range.max)
const actualPolyline = buildPolyline(state.samples, actualKey, range.min, range.max)
const zeroY = yForValue(0, range.min, range.max)
const topQuarterY = yForValue(range.max * 0.5, range.min, range.max)
const bottomQuarterY = yForValue(range.min * 0.5, range.min, range.max)
return html`
<section class="plotCard plotChartCard">
<div class="plotCardHeader">
<h2>${title}</h2>
<span class="plotSource">Source: ${formatSourceLabel(sourceKind, sourceLabel)}</span>
</div>
<div class="plotLegend">
<span class="plotLegendItem"><i class="plotLegendLine desired"></i>${desiredLabel}: ${formatValue(latestSampleValue(desiredKey))}</span>
<span class="plotLegendItem"><i class="plotLegendLine actual"></i>${actualLabel}: ${formatValue(latestSampleValue(actualKey))}</span>
</div>
<div class="plotSvgWrap">
${hasData ? html`
<svg class="plotSvg" viewBox="0 0 ${SVG_WIDTH} ${SVG_HEIGHT}" preserveAspectRatio="none" role="img" aria-label="${title} live plot">
<line class="plotGridLine" x1="0" y1="${topQuarterY}" x2="${SVG_WIDTH}" y2="${topQuarterY}"></line>
<line class="plotZeroLine" x1="0" y1="${zeroY}" x2="${SVG_WIDTH}" y2="${zeroY}"></line>
<line class="plotGridLine" x1="0" y1="${bottomQuarterY}" x2="${SVG_WIDTH}" y2="${bottomQuarterY}"></line>
<polyline class="plotLine desired" points="${desiredPolyline}"></polyline>
<polyline class="plotLine actual" points="${actualPolyline}"></polyline>
</svg>
` : html`
<div class="plotEmpty">Waiting for enough live samples...</div>
`}
</div>
<div class="plotRangeRow">
<span>${formatValue(range.max, 1)} m/s²</span>
<span>0.0 m/s²</span>
<span>${formatValue(range.min, 1)} m/s²</span>
</div>
</section>
`
}
function LateralTermsPlotCard(sourceLabel) {
const hasData = state.samples.length > 1
const keys = ["lateralP", "lateralI", "lateralD", "lateralF"]
const range = computeRangeForKeys(state.samples, keys)
const zeroY = yForValue(0, range.min, range.max)
const topQuarterY = yForValue(range.max * 0.5, range.min, range.max)
const bottomQuarterY = yForValue(range.min * 0.5, range.min, range.max)
return html`
<section class="plotCard plotChartCard">
<div class="plotCardHeader">
<h2>Lateral Controller Terms</h2>
<span class="plotSource">Source: ${formatSourceLabel("lateralTerms", sourceLabel)}</span>
</div>
<div class="plotLegend">
<span class="plotLegendItem"><i class="plotLegendLine p"></i>P: ${formatValue(latestSampleValue("lateralP"), 3)}</span>
<span class="plotLegendItem"><i class="plotLegendLine i"></i>I: ${formatValue(latestSampleValue("lateralI"), 3)}</span>
<span class="plotLegendItem"><i class="plotLegendLine d"></i>D: ${formatValue(latestSampleValue("lateralD"), 3)}</span>
<span class="plotLegendItem"><i class="plotLegendLine f"></i>F: ${formatValue(latestSampleValue("lateralF"), 3)}</span>
</div>
<div class="plotSvgWrap">
${hasData ? html`
<svg class="plotSvg" viewBox="0 0 ${SVG_WIDTH} ${SVG_HEIGHT}" preserveAspectRatio="none" role="img" aria-label="Lateral controller terms live plot">
<line class="plotGridLine" x1="0" y1="${topQuarterY}" x2="${SVG_WIDTH}" y2="${topQuarterY}"></line>
<line class="plotZeroLine" x1="0" y1="${zeroY}" x2="${SVG_WIDTH}" y2="${zeroY}"></line>
<line class="plotGridLine" x1="0" y1="${bottomQuarterY}" x2="${SVG_WIDTH}" y2="${bottomQuarterY}"></line>
<polyline class="plotLine p" points="${buildPolyline(state.samples, "lateralP", range.min, range.max)}"></polyline>
<polyline class="plotLine i" points="${buildPolyline(state.samples, "lateralI", range.min, range.max)}"></polyline>
<polyline class="plotLine d" points="${buildPolyline(state.samples, "lateralD", range.min, range.max)}"></polyline>
<polyline class="plotLine f" points="${buildPolyline(state.samples, "lateralF", range.min, range.max)}"></polyline>
</svg>
` : html`
<div class="plotEmpty">Waiting for enough live samples...</div>
`}
</div>
<div class="plotRangeRow">
<span>${formatValue(range.max, 3)}</span>
<span>0.000</span>
<span>${formatValue(range.min, 3)}</span>
</div>
</section>
`
}
function LongitudinalTermsPlotCard(sourceLabel) {
const hasData = state.samples.length > 1
const keys = ["longitudinalUpAccelCmd", "longitudinalUiAccelCmd", "longitudinalUfAccelCmd"]
const range = computeRangeForKeys(state.samples, keys)
const zeroY = yForValue(0, range.min, range.max)
const topQuarterY = yForValue(range.max * 0.5, range.min, range.max)
const bottomQuarterY = yForValue(range.min * 0.5, range.min, range.max)
return html`
<section class="plotCard plotChartCard">
<div class="plotCardHeader">
<h2>Longitudinal Accel Cmd Terms</h2>
<span class="plotSource">Source: ${formatSourceLabel("longitudinalTerms", sourceLabel)}</span>
</div>
<div class="plotLegend">
<span class="plotLegendItem"><i class="plotLegendLine p"></i>Up: ${formatValue(latestSampleValue("longitudinalUpAccelCmd"), 3)}</span>
<span class="plotLegendItem"><i class="plotLegendLine i"></i>Ui: ${formatValue(latestSampleValue("longitudinalUiAccelCmd"), 3)}</span>
<span class="plotLegendItem"><i class="plotLegendLine f"></i>Uf: ${formatValue(latestSampleValue("longitudinalUfAccelCmd"), 3)}</span>
</div>
<div class="plotSvgWrap">
${hasData ? html`
<svg class="plotSvg" viewBox="0 0 ${SVG_WIDTH} ${SVG_HEIGHT}" preserveAspectRatio="none" role="img" aria-label="Longitudinal accel cmd terms live plot">
<line class="plotGridLine" x1="0" y1="${topQuarterY}" x2="${SVG_WIDTH}" y2="${topQuarterY}"></line>
<line class="plotZeroLine" x1="0" y1="${zeroY}" x2="${SVG_WIDTH}" y2="${zeroY}"></line>
<line class="plotGridLine" x1="0" y1="${bottomQuarterY}" x2="${SVG_WIDTH}" y2="${bottomQuarterY}"></line>
<polyline class="plotLine p" points="${buildPolyline(state.samples, "longitudinalUpAccelCmd", range.min, range.max)}"></polyline>
<polyline class="plotLine i" points="${buildPolyline(state.samples, "longitudinalUiAccelCmd", range.min, range.max)}"></polyline>
<polyline class="plotLine f" points="${buildPolyline(state.samples, "longitudinalUfAccelCmd", range.min, range.max)}"></polyline>
</svg>
` : html`
<div class="plotEmpty">Waiting for enough live samples...</div>
`}
</div>
<div class="plotRangeRow">
<span>${formatValue(range.max, 3)}</span>
<span>0.000</span>
<span>${formatValue(range.min, 3)}</span>
</div>
</section>
`
}
async function initialize() {
try {
state.showAdvancedTerms = localStorage.getItem(ADVANCED_TERMS_KEY) === "1"
} catch (error) {
state.showAdvancedTerms = false
}
try {
await fetchLiveData()
} catch (error) {
state.error = error?.message || String(error)
state.loading = false
} finally {
ensurePolling()
}
}
export function LivePlots() {
if (!initialized) {
initialized = true
initialize()
}
if (!state.paused) {
ensurePolling()
}
return html`
<div class="plotsPage">
<h1>Plots</h1>
<section class="plotCard plotStatusCard">
<p class="plotDescription">
Live comparison view for tuning diagnostics. If desired tracks actual closely, tuning is likely good. Large divergence often points to model mismatch or limits.
</p>
<div class="plotStatusGrid">
<p><strong>Onroad:</strong> ${state.live?.isOnroad ? "Yes" : "No"}</p>
<p><strong>Sample Age:</strong> ${formatAge(state.live?.sampleAgeSeconds)}</p>
<p><strong>Vehicle Speed:</strong> ${formatValue(state.live?.speed)} m/s</p>
<p><strong>Samples:</strong> ${state.samples.length}</p>
</div>
<div class="plotActions">
<button class="plotButton" @click="${togglePaused}">
${state.paused ? "Resume Live" : "Pause Live"}
</button>
<button class="plotButton" @click="${clearHistory}">
Clear History
</button>
</div>
${state.error ? html`<p class="plotError"><strong>Error:</strong> ${state.error}</p>` : ""}
${state.live?.lastError ? html`<p class="plotError"><strong>Source Error:</strong> ${state.live.lastError}</p>` : ""}
</section>
<div class="plotCharts">
${PlotCard(
"Lateral Response (m/s²)",
"desiredLateralAccel",
"actualLateralAccel",
"lateral",
state.live?.lateralSource,
"Target",
"Measured",
)}
${PlotCard(
"Longitudinal Response (m/s²)",
"desiredLongitudinalAccel",
"actualLongitudinalAccel",
"longitudinal",
state.live?.longitudinalSource,
"Target",
"Measured",
)}
</div>
<div class="plotAdvancedRow">
<button class="plotButton" @click="${toggleAdvancedTerms}">
${state.showAdvancedTerms ? "Hide Advanced Controller Terms" : "Show Advanced Controller Terms"}
</button>
</div>
${state.showAdvancedTerms ? html`
<div class="plotCharts">
${LateralTermsPlotCard(state.live?.lateralTermsSource)}
${LongitudinalTermsPlotCard(state.live?.longitudinalTermsSource)}
</div>
` : ""}
${state.loading ? html`<p>Loading live data...</p>` : ""}
</div>
`
}
@@ -28,6 +28,7 @@
<link rel="stylesheet" href="/assets/components/tools/doors.css">
<link rel="stylesheet" href="/assets/components/tools/error_logs.css">
<link rel="stylesheet" href="/assets/components/tools/model_manager.css">
<link rel="stylesheet" href="/assets/components/tools/plots.css">
<link rel="stylesheet" href="/assets/components/tools/speed_limits.css">
<link rel="stylesheet" href="/assets/components/tools/theme_maker.css">
<link rel="stylesheet" href="/assets/components/tools/tmux.css">
+199
View File
@@ -162,9 +162,192 @@ _fast_update_state = {
"progressDetail": "",
}
_PLOTS_POLL_INTERVAL_S = 0.5
_PLOTS_CLIENT_IDLE_TIMEOUT_S = 15.0
_PLOTS_SAMPLE_STALE_AFTER_S = 1.5
_plots_lock = threading.Lock()
_plots_worker_thread = None
_plots_last_client_request_ts = 0.0
_plots_state = {
"timestamp": 0.0,
"desiredLateralAccel": 0.0,
"actualLateralAccel": 0.0,
"desiredLongitudinalAccel": 0.0,
"actualLongitudinalAccel": 0.0,
"lateralP": 0.0,
"lateralI": 0.0,
"lateralD": 0.0,
"lateralF": 0.0,
"longitudinalUpAccelCmd": 0.0,
"longitudinalUiAccelCmd": 0.0,
"longitudinalUfAccelCmd": 0.0,
"speed": 0.0,
"lateralSource": "curvature",
"longitudinalSource": "controlsState + liveLocationKalman",
"lateralTermsSource": "unknown",
"longitudinalTermsSource": "controlsState",
"sampleIndex": 0,
"lastError": "",
}
def _normalize_fingerprint_make_key(make_value):
return str(make_value or "").strip().lower()
def _safe_float(value, default=0.0):
try:
return float(value)
except Exception:
return float(default)
def _extract_lateral_accel_values(controls_state, speed_mps):
v_ego = max(0.0, _safe_float(speed_mps))
speed_sq = v_ego * v_ego
try:
lateral_state = controls_state.lateralControlState
if lateral_state.which() == "torqueState":
torque_state = lateral_state.torqueState
desired = _safe_float(getattr(torque_state, "desiredLateralAccel", 0.0))
actual = _safe_float(getattr(torque_state, "actualLateralAccel", 0.0))
if abs(desired) > 1e-3 or abs(actual) > 1e-3:
return desired, actual, "torqueState"
except Exception:
pass
desired_curvature = _safe_float(getattr(controls_state, "desiredCurvature", 0.0))
actual_curvature = _safe_float(getattr(controls_state, "curvature", 0.0))
return desired_curvature * speed_sq, actual_curvature * speed_sq, "curvature"
def _extract_longitudinal_accel_values(controls_state, live_location_kalman):
desired = _safe_float(getattr(controls_state, "upAccelCmd", 0.0)) + \
_safe_float(getattr(controls_state, "uiAccelCmd", 0.0)) + \
_safe_float(getattr(controls_state, "ufAccelCmd", 0.0))
actual = 0.0
source = "controlsState + liveLocationKalman"
try:
accel_calibrated = getattr(live_location_kalman, "accelerationCalibrated", None)
if accel_calibrated and getattr(accel_calibrated, "valid", False):
accel_values = list(getattr(accel_calibrated, "value", []))
if len(accel_values) > 0:
actual = _safe_float(accel_values[0], 0.0)
except Exception:
source = "controlsState"
return desired, actual, source
def _extract_lateral_controller_terms(controls_state):
terms = {
"lateralP": 0.0,
"lateralI": 0.0,
"lateralD": 0.0,
"lateralF": 0.0,
}
source = "unknown"
try:
lateral_state = controls_state.lateralControlState
which = lateral_state.which()
if which == "torqueState":
torque_state = lateral_state.torqueState
terms["lateralP"] = _safe_float(getattr(torque_state, "p", 0.0))
terms["lateralI"] = _safe_float(getattr(torque_state, "i", 0.0))
terms["lateralD"] = _safe_float(getattr(torque_state, "d", 0.0))
terms["lateralF"] = _safe_float(getattr(torque_state, "f", 0.0))
source = "torqueState"
elif which == "pidState":
pid_state = lateral_state.pidState
terms["lateralP"] = _safe_float(getattr(pid_state, "p", 0.0))
terms["lateralI"] = _safe_float(getattr(pid_state, "i", 0.0))
terms["lateralF"] = _safe_float(getattr(pid_state, "f", 0.0))
source = "pidState"
elif which:
source = which
except Exception:
pass
return terms, source
def _extract_longitudinal_controller_terms(controls_state):
terms = {
"longitudinalUpAccelCmd": _safe_float(getattr(controls_state, "upAccelCmd", 0.0)),
"longitudinalUiAccelCmd": _safe_float(getattr(controls_state, "uiAccelCmd", 0.0)),
"longitudinalUfAccelCmd": _safe_float(getattr(controls_state, "ufAccelCmd", 0.0)),
}
return terms, "controlsState"
def _plots_worker():
global _plots_worker_thread
try:
sm = messaging.SubMaster(["controlsState", "liveLocationKalman"], poll="controlsState")
except Exception as exception:
with _plots_lock:
_plots_state["lastError"] = str(exception)
_plots_worker_thread = None
return
while True:
with _plots_lock:
idle_for = time.monotonic() - _plots_last_client_request_ts
if idle_for >= _PLOTS_CLIENT_IDLE_TIMEOUT_S:
break
try:
sm.update(0)
controls_state = sm["controlsState"]
live_location_kalman = sm["liveLocationKalman"]
speed = _safe_float(getattr(controls_state, "vPid", 0.0))
desired_lateral, actual_lateral, lateral_source = _extract_lateral_accel_values(controls_state, speed)
desired_longitudinal, actual_longitudinal, longitudinal_source = _extract_longitudinal_accel_values(controls_state, live_location_kalman)
lateral_terms, lateral_terms_source = _extract_lateral_controller_terms(controls_state)
longitudinal_terms, longitudinal_terms_source = _extract_longitudinal_controller_terms(controls_state)
with _plots_lock:
_plots_state.update({
"timestamp": time.time(),
"desiredLateralAccel": round(desired_lateral, 4),
"actualLateralAccel": round(actual_lateral, 4),
"desiredLongitudinalAccel": round(desired_longitudinal, 4),
"actualLongitudinalAccel": round(actual_longitudinal, 4),
"lateralP": round(lateral_terms["lateralP"], 4),
"lateralI": round(lateral_terms["lateralI"], 4),
"lateralD": round(lateral_terms["lateralD"], 4),
"lateralF": round(lateral_terms["lateralF"], 4),
"longitudinalUpAccelCmd": round(longitudinal_terms["longitudinalUpAccelCmd"], 4),
"longitudinalUiAccelCmd": round(longitudinal_terms["longitudinalUiAccelCmd"], 4),
"longitudinalUfAccelCmd": round(longitudinal_terms["longitudinalUfAccelCmd"], 4),
"speed": round(speed, 4),
"lateralSource": lateral_source,
"longitudinalSource": longitudinal_source,
"lateralTermsSource": lateral_terms_source,
"longitudinalTermsSource": longitudinal_terms_source,
"sampleIndex": int(_plots_state.get("sampleIndex", 0)) + 1,
"lastError": "",
})
except Exception as exception:
with _plots_lock:
_plots_state["lastError"] = str(exception)
time.sleep(_PLOTS_POLL_INTERVAL_S)
with _plots_lock:
_plots_worker_thread = None
def _ensure_plots_worker():
global _plots_worker_thread, _plots_last_client_request_ts
with _plots_lock:
_plots_last_client_request_ts = time.monotonic()
if _plots_worker_thread and _plots_worker_thread.is_alive():
return
_plots_worker_thread = threading.Thread(target=_plots_worker, daemon=True)
_plots_worker_thread.start()
def _set_fast_update_state(**kwargs):
with _fast_update_lock:
_fast_update_state.update(kwargs)
@@ -1936,6 +2119,22 @@ def setup(app):
},
}
@app.route("/api/plots/live", methods=["GET"])
def get_live_plots():
_ensure_plots_worker()
with _plots_lock:
payload = dict(_plots_state)
timestamp = _safe_float(payload.get("timestamp", 0.0), 0.0)
age_seconds = max(0.0, time.time() - timestamp) if timestamp else 999.0
return jsonify({
**payload,
"isOnroad": params.get_bool("IsOnroad"),
"sampleAgeSeconds": round(age_seconds, 3),
"stale": age_seconds > _PLOTS_SAMPLE_STALE_AFTER_S,
}), 200
@app.route("/api/update/fast/status", methods=["GET"])
def get_fast_update_status():
state_data = _get_fast_update_state()