Skip to content

Trackside: fix Max Cell T + realtime Cell Thermals grid#308

Open
cyberneel wants to merge 7 commits into
mainfrom
trackside-cell-thermals
Open

Trackside: fix Max Cell T + realtime Cell Thermals grid#308
cyberneel wants to merge 7 commits into
mainfrom
trackside-cell-thermals

Conversation

@cyberneel

Copy link
Copy Markdown
Collaborator

What

  • Fix "Max Cell T" stuck at 0. It read cell_top_temp (CAN 0x132), which HVC does not emit, so it sat at 0. Now uses the per-cell cells_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's max_cell_temp scalar, then the legacy fields; requires > 0 so genuine no-data reads -- instead of a misleading 0.
  • New "Cell Thermals" panel on the Live tab: a Grafana-style heatmap grid of every populated cell, fixed 20–60 °C blue→red scale, with min/avg/max in the header. Tooltip shows cell index + exact °C.

Why it was 0 (and the dash wasn't)

Two different sources: trackside read the unemitted 0x132 scalar; the dash computes max from the emitted 0x100 cells_temps[] array (zero-filtered). This aligns trackside to the dash's source.

Scope — client-only

The derived feed grafana_data_orion_derived already carries cells_temps (the bridge emits the array; the enricher passes lists through) plus a computed max_cell_temp. The only gaps were client-side: trackside read the wrong key, and flattenNumeric drops arrays. So this PR touches only the viewer_tool — no enricher/bridge/firmware changes.

Changes: LiveSample (+cellTemps, both type defs), normalizeLivePayload extracts the array, maxCellTempFor rewrite, CellThermalsPanel + CSS.

Verify

tsc --noEmit clean; next build green. Data path traced end-to-end (SSE JSON → event.sample.cellTemps → stored raw → smoothLiveSample preserves via spread → panel).

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.
@cyberneel cyberneel requested a review from a team as a code owner June 13, 2026 04:04
@cyberneel cyberneel requested review from alicedimauro and angelasrsh and removed request for a team June 13, 2026 04:04
@vercel

vercel Bot commented Jun 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lhre-2026 Ready Ready Preview, Comment Jun 18, 2026 4:14am

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +4541 to +4543
const vals = populated.map((c) => c.t);
const min = Math.min(...vals);
const max = Math.max(...vals);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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);

Comment on lines +6673 to +6674
const fromArray = sample.cellTemps?.filter((t) => t > 0) ?? [];
if (fromArray.length) return Math.max(...fromArray);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant