Trackside: fix Max Cell T + realtime Cell Thermals grid#308
Conversation
Max Cell T was stuck at 0 because maxCellTempFor read cell_top_temp (CAN 0x132), which HVC doesn't emit. Switch to the per-cell cells_temps[] array — the same source dashd uses on-car — taking the max over positive temps (zero-padded slots ignored); fall back to the enricher's max_cell_temp scalar, then the legacy fields, requiring >0 so no-data reads '--' not a fake 0. The derived feed (grafana_data_orion_derived) already carries cells_temps (bridge passes the array, enricher passes lists through) but flattenNumeric dropped it; normalizeLivePayload now extracts it into LiveSample.cellTemps. New 'Cell Thermals' panel renders the populated cells as a Grafana-style heatmap grid (fixed 20-60C blue->red scale) with min/avg/max. Client-only; enricher/bridge unchanged.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Code Review
This pull request introduces a real-time per-cell thermal grid (heatmap) to the trackside live viewer. It extracts per-cell temperatures (cellTemps) from the telemetry feed, defines the CellThermalsPanel component to display the thermal grid with min, max, and average statistics, and updates the maxCellTempFor calculation to prioritize this array. The review feedback highlights potential stack overflow risks (RangeError) when using the spread operator with Math.min and Math.max on unvalidated arrays, suggesting the use of Array.prototype.reduce as a safer alternative.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const vals = populated.map((c) => c.t); | ||
| const min = Math.min(...vals); | ||
| const max = Math.max(...vals); |
There was a problem hiding this comment.
Using the spread operator ... with Math.min and Math.max on an array of unvalidated size can lead to a stack overflow (RangeError: Maximum call stack size exceeded) if the array is unexpectedly large (e.g., due to corrupted telemetry data). It is safer and more defensive to use reduce to find the minimum and maximum values.
| const vals = populated.map((c) => c.t); | |
| const min = Math.min(...vals); | |
| const max = Math.max(...vals); | |
| const vals = populated.map((c) => c.t); | |
| const min = vals.reduce((m, t) => t < m ? t : m, Infinity); | |
| const max = vals.reduce((m, t) => t > m ? t : m, -Infinity); |
| const fromArray = sample.cellTemps?.filter((t) => t > 0) ?? []; | ||
| if (fromArray.length) return Math.max(...fromArray); |
There was a problem hiding this comment.
Using the spread operator ... with Math.max on an array of unvalidated size can lead to a stack overflow (RangeError: Maximum call stack size exceeded) if the array is unexpectedly large. It is safer and more defensive to use reduce to find the maximum value.
| const fromArray = sample.cellTemps?.filter((t) => t > 0) ?? []; | |
| if (fromArray.length) return Math.max(...fromArray); | |
| const fromArray = sample.cellTemps?.filter((t) => t > 0) ?? []; | |
| if (fromArray.length) return fromArray.reduce((max, t) => t > max ? t : max, -Infinity); |
The live view showed several energy numbers (drive/out, regen/in) alongside net, and the hero 'Energy' tile actually showed drive (out) energy mislabeled as Energy — confusing. Now net only, everywhere labeled 'Energy': - hero: Energy = totalEnergyWh (net); removed the 'Regen In' strip. - Energy Window chart: single net 'Energy' tile + net series; dropped Window Out / Regen In tiles and the out/in plot lines. - lap table: single 'Energy'/'Δ Energy' column; dropped per-lap Drive/Regen. - lap-mini panes (last lap / selected avg): Energy only. - Live Data + (hidden) Energy Strategy panels: Drive Power + Regen Power consolidated into one signed 'Power' (kW, negative = regen); stripped the hidden panel's Drive/Regen/Lap Drive/Lap Regen/Avg Regen rows. Underlying out/in math is kept (net = drive - regen is computed from it); CSV export columns left unchanged. Part of #308.
The completed-lap count (and hero 'Lap X/22') drove laps-remaining + the dynamic per-lap energy budget off the RAW lap total, so a double-counted trigger or a lap left running over a driver change offset the plan. Now lapsCompletedPlan = selected laps from the live list (the existing checkboxes, which auto-select on completion). Deselect a double-count or driver-change lap -> completed drops, laps-remaining rises, budget/auto-power recompute live. energyUsedWh stays the actual total over ALL laps (a deselected lap's energy was still consumed; a double-count splits one lap's energy across two records, total conserved) — only the COUNT follows the selection. Checkbox tooltip updated to reflect the dual role; Live Laps header shows 'N/M counted' when they differ. Dash lap-card count stays raw (deselecting a past lap can't un-fire its card). Part of #308.
The dash count followed the RAW forward-only lapTrigger, so deselecting a double-count / driver-change lap on the live list didn't reach the driver dash. Now the SELECTED count is the single source of truth, synced bidirectionally: - dashd: new lhre/dash/lapCount field adopts an absolute count in EITHER direction with no card / no integrator reset (lapTrigger stays forward-only, so the drain/republish safety is untouched). Acks on ack/lapCount. - dashSignals: publishLapCount(n) sets the count down (or up) without a card and keeps lapCounterRef aligned so the next forward sendLap numbers correctly. - TracksideApp: one effect drives the dash off selectedLaps.length — increases fire the card via lapTrigger (when auto-card on, or a manual Log Lap requested it via dashCardForceRef), decreases publishLapCount to pull the dash down. markLap now requests the card through the flag instead of sending raw; drain compares against the selected count. Deselect a lap -> dash lap count drops in lockstep with trackside. Part of #308. (Deploy waits: dashd change must reconcile with the car's #300 dashd on main.)
Investigating the 'lap number lags on deselect' issue: dashd already publishes
the live lap_count every WS frame as mqtt.lapTrigger, and last commit's lapCount
sync makes that correct up/down on a deselect. So a NEW field was redundant —
the live count is already bindable; the catalog labels just didn't make that
obvious. Relabel so the right choice is clear:
- mqtt.lapTrigger: 'Lap count' -> 'Laps completed (live)' (use for a persistent
laps-done readout; tracks trackside deselects in real time)
- lapCard.lapNumber: 'Lap number' -> 'Lap number (card only)' (the lap-card
snapshot; only updates on lap completion)
Fix for an existing custom layout: re-bind the driving 'LAP' widget from
'Lap number (card only)' to 'Laps completed (live)'. Part of #308.
…p trend) New 'Battery Thermal Projection' graph on the Live tab (battery only, per the ask). Implements computa's 80/20 spec from the #comp-2026 trackside thread: projectedTemp = currentTemp + max(0, slopeDegPerLap) * lapsRemaining slopeDegPerLap = max(median(last 3 lap deltas), last lap delta), clamped >=0 - per-lap PEAK battery temp captured at lap close (lapMaxCellTempC over the lap's samples) -> new LiveLap.maxCellTempC; uses SELECTED laps (deselects excluded). - requires >=2 completed laps else 'collecting'; biases hot (catches end-of-run ramp); one decimal; colored by margin to a configurable limit (default 60C), not by confidence. - graph: per-lap temp vs lap + dashed projection from now->finish + limit line; header reads 'XX.X now -> YY.Y @ finish'. Frontend-only (uses maxCellTempFor / the cellTemps array). Part of #308.
- Hero: dominant LAP X/Y counter + prominent LAST LAP (net Wh + time); dropped the running-total Energy tile (it's in Energy Plan). Net-only, per Matt. - 'Diagnostics & charts' collapse (collapsed by default) keeps the glance set scroll-free: hides Live Data, Energy Window, Temperature Window, and non-battery temps (battery is the thermal priority). One toggle, persisted. - Main column reordered: Battery Thermal Projection + Vitals on top; Driver Controls kept visible but lower (cool-to-see). Energy Plan / Pack Status / Cell Thermals stay primary in the left column. - Lap list: recent-at-top (in-progress row first, completed newest-first). Web-only, on #308. Layout iterable — say what to move.
6223dee to
3b9ddf0
Compare
What
cell_top_temp(CAN0x132), which HVC does not emit, so it sat at 0. Now uses the per-cellcells_temps[]array — the same source the driver park dash (dashd) uses on-car — taking the max over positive temps (the array is zero-padded for not-yet-reported cells). Falls back to the enricher'smax_cell_tempscalar, then the legacy fields; requires> 0so genuine no-data reads--instead of a misleading 0.Why it was 0 (and the dash wasn't)
Two different sources: trackside read the unemitted
0x132scalar; the dash computes max from the emitted0x100cells_temps[]array (zero-filtered). This aligns trackside to the dash's source.Scope — client-only
The derived feed
grafana_data_orion_derivedalready carriescells_temps(the bridge emits the array; the enricher passes lists through) plus a computedmax_cell_temp. The only gaps were client-side: trackside read the wrong key, andflattenNumericdrops arrays. So this PR touches only the viewer_tool — no enricher/bridge/firmware changes.Changes:
LiveSample(+cellTemps, both type defs),normalizeLivePayloadextracts the array,maxCellTempForrewrite,CellThermalsPanel+ CSS.Verify
tsc --noEmitclean;next buildgreen. Data path traced end-to-end (SSE JSON →event.sample.cellTemps→ stored raw →smoothLiveSamplepreserves via spread → panel).