JavaScript Logic¶
web_src/script.js (~12,400 lines, ~247 top-level functions) is not really "UI code" — it is a streaming data ingestion pipeline with UI rendering bolted onto it. The core pattern:
raw SSE event → parse CSV into a data object → selectively push values into DOM spans →
→ separately push values into plot arrays →
→ separately update echos / toggles / status widgets
Whitelist arrays (criticalFields, otherFields, CSV*_FIELDS) decide what gets rendered. updateFields() is the core renderer, but the formatting rules are per-payload — there's no universal formatter. Get that right in your head before you go editing.
The Four SSE Streams¶
Plus one out-of-band stream for log output.
| Event name | Cadence | Length | Purpose |
|---|---|---|---|
CSVData |
every webgaugesinterval (100 ms default) |
CSV1_FIELDS.length (currently 34) |
Fast live values — the main dashboard and real-time plots |
CSVData2 |
5 s (500 ms during sysID) | CSV2_FIELDS.length (several hundred) |
State, settings, counters, max values, weather, lifetime, mode flags, IMU summary, function timing |
CSVData3 |
Event-driven on settingsDirty rising edge, 60 s heartbeat |
CSV3_FIELDS.length (several hundred) |
User-configurable settings echo + per-setting tuning constants |
TimestampData |
3 s | TS_FIELDS.length (~28) |
Per-sensor staleness ages (ms since last update). Drives the greying-out logic |
console (out of band) |
Up to 5 per 700 ms | one message at a time | Console-pane log line, queued by queueConsoleMessage*() on the firmware side |
Plus three event names for the field-bucketer:
EffMatrix— every 5 s, change-triggered — current bucket state for the matrix displayEffRed— every 5 s — live red-dot positionEffHistory— every 5 s — 30-session efficiency-history sparkline
The firmware-side priority gating (CSV1 → console → CSV2 → CSV3 → TS) means at most one telemetry channel arrives per loop() iteration. The dispatcher in the firmware is described in Network & Web System → Server-Sent Events.
Header validation¶
Every CSV payload starts with the field count as the first value:
const declaredCount = parseInt(values[0]);
if (values.length !== declaredCount) {
console.warn(`[CSV1] length mismatch: declared=${declaredCount}, actual=${values.length}`);
return;
}
if (declaredCount !== CSV1_FIELDS.length) {
console.warn(`[CSV1] schema mismatch: ESP32=${declaredCount}, UI=${CSV1_FIELDS.length}`);
return;
}
Two-tier check: declared-vs-actual catches firmware bugs (snprintf truncation); declared-vs-UI catches schema drift (firmware added a field but the JS index map wasn't updated). Both warnings go to the browser console. If either fires, the payload is dropped and the dashboard freezes for that channel — never silently render partial data.
CSV Index Maps¶
Top-of-file constants. These must be kept synchronized with the firmware's CSV payload field order — see Important Functions → CSV Payload Integrity.
const CSV1_FIELDS = [
"AlternatorTemperatureF", // 0
"dutyCycle", // 1
"BatteryV", // 2
"MeasuredAmps", // 3
"RPM", // 4
"Channel3V", // 5
"IBV", // 6
"Bcur", // 7
// …
];
CSV2_FIELDS and CSV3_FIELDS follow the same pattern with several hundred entries each. TS_FIELDS is for TimestampData.
The handlers use Object.fromEntries(CSV1_FIELDS.map((key, i) => [key, values[i]])) to turn values[] into a data object keyed by name. After that point, nothing downstream cares about the raw array index — everything is data.IBV, data.dutyCycle, etc.
CSVData Handler Path (the canonical example)¶
When CSVData arrives:
- Connection bookkeeping —
lastEventTime = Date.now(); the inline connection indicator flips to "connected." - Parse — split on commas,
parseFloat()everything. - Validate — two-tier length check above.
- Map —
data = Object.fromEntries(CSV1_FIELDS.map(...)). - Plot config (cadence-gated) —
updatePlotConfiguration(data)runs only every Nth packet, where N depends onwebgaugesinterval. Pulls Y-axis limits, time-window, and color settings fromdata. - Special status widgets (every packet) — field state text/class (
Field: ON/OFF/LIMP/WAITING_CLOUD), charge-stage visibility (BULK/ABS/FLOAT/IDLE/MAINTAIN/TARGET-V/MANUAL/ hidden). updateFields(criticalFields)— every packet — narrow whitelist of fields that must update at full rate (live amps, volts, RPM, the displayed setpoint, peak values relevant to the visible page).updateFields(otherFields)— every 4th packet — the rest. Saves DOM-write cost for less-time-sensitive numbers.- Push to plot arrays — appends
data.MeasuredAmps,data.BatteryV, etc. to the rolling uPlot data arrays. Shifts off old samples beyondplotTimeWindow. - Interpolation loop wakeup —
startInterpLoop()was already started; this handler just appends new samples.
processCSVDataOptimized(data) is the heart of step 9. It pre-allocates target arrays, does index math, and pushes new points without per-tick reallocation — exactly the pattern that lets the page render at 100 ms cadence without GC pauses.
Per-payload formatters¶
Each payload has its own formatter rules inside its local updateFields():
| Payload | Common transforms |
|---|---|
| CSV1 | /100 for V, A, °F, %, PID terms; /10 for iiout; /1000000 for MaximumLoopTime (µs → s); label lookups for currentMode and fieldActiveStatus |
| CSV2 | /100 for V, A, °F (consistent with CSV1); /1000 for some scaled ms; division for accumulated Wh display |
| CSV3 | /1000 for ms-to-seconds settings (absorptionCompleteTime, bulkVoltageHoldMs, etc.); /100 for voltage caps; /60000 for ms-to-minutes (AbsorptionTimeoutMs) — see the per-setting transform registration |
This per-payload formatter pattern is the single biggest source of confusion when adding a new field. The right rule is: whatever scale you used in SafeInt() in the firmware, divide by that here.
Echo System (CSV3)¶
When the user types a new value into a form input and submits, the firmware writes it to LittleFS, sets settingsDirty = true, and the next CSV3 dispatch echoes the saved value back. The dashboard updates a small <span id="...echo"> showing the saved value in parentheses next to the input.
Per-setting echo registration¶
In updateAllEchosOptimized(data), a single declarative array drives every echo update:
const ECHO_REGISTRATIONS = [
{ key: 'BulkVoltage', id: 'BulkVoltage_echo', transform: v => (v / 100).toFixed(2) },
{ key: 'absorptionCompleteTime', id: 'absorptionCompleteTime_echo', transform: v => Math.round(v / 1000) },
{ key: 'VoltageKp', id: 'VoltageKp_echo', transform: v => (v / 100).toFixed(3) },
// … one entry per echoable setting
];
transform: v => v means no conversion (raw integer). Echoes only update when the value changes — updateEchoIfChanged(elementId, newValue) checks textContent before writing.
Do not put echo formatting in the unit-adjustment block at the top of the renderer — that block is for raw telemetry display, not echo labels. Mixing the two layers is one of the long-standing tech-debt items on the roadmap.
Staleness System¶
TimestampData carries millis() ages in milliseconds. The handler:
const data = Object.fromEntries(TS_FIELDS.map((key, i) => [key, values[i]]));
window.sensorAges = data; // global; consulted by other code
window.sensorAges is then consulted by:
- The cell-greying logic — any DOM span representing a sensor value is wrapped to grey itself out when its age exceeds the threshold.
- The connection-status indicator — overall stream-stale check.
Thresholds (top of script.js):
const STALE_THRESHOLD_DEFAULT_MS = 6000; // 6 s — everything except temp
const STALE_THRESHOLD_TEMP_MS = 12000; // 12 s — DS18B20 reads every 5 s, allows one missed read
IDX_ALTERNATOR_TEMP and IDX_THERMISTOR_TEMP use the higher threshold; everything else uses the default.
Reconnect Logic — initializeEventSource()¶
The EventSource instance lives in source at module scope. Key behavior:
openevent — flip indicator green, resetsseReconnectAttempts = 0.errorevent — log state (CONNECTING / OPEN / CLOSED), schedule reconnect with exponential backoff if CLOSED.- Reconnect cap —
MAX_SSE_RECONNECTS = 10. If exceeded, stops retrying — user must callmanualReconnect()(mapped to a button) to resume. Prevents infinite reconnect storms on a permanently-down regulator. - Background guard —
document.visibilitychangelistener tracksisAppInBackgroundso reconnects don't fire when the browser tab is hidden (mobile battery hit). - Capacitor support —
IS_CAPACITOR = !!window.Capacitorswitches the base URL tohttp://alternator.local(otherwise relative paths work on the local server). See Capacitor iOS layout for the wrapped-mobile-app architecture.
Plot Management — uPlot¶
uPlot.iife.min.js and uPlot.min.css (third-party, do not modify). Several charts are instantiated:
| Chart | Data source | Time window |
|---|---|---|
| Volts plot | CSV1 BatteryV, IBV, target lines |
xTime (default 60 s) |
| Amps plot | CSV1 MeasuredAmps, Bcur, setpointLimited, Icv, uTargetAmps |
xTime |
| Duty plot | CSV1 dutyCycle, pidOutput |
xTime |
| PID terms | CSV1 P/I/D contributions | xTime |
| IMU plots | CSV1 heel/pitch/yaw/accel | xTime |
| Field-bucketer matrix | EffMatrix event |
snapshot |
queuePlotUpdate(plotName) debounces draw calls — multiple data appends within one animation frame coalesce into a single setData() call. updateUplotTheme(plot) runs on the global dark/light toggle.
reinitializePlotsWithNewTiming(data) re-creates plots whenever webgaugesinterval or plotTimeWindow changes (CSV1 carries these values; user changes them via form fields). uPlot doesn't support dynamic time-window resizing well, so destroying and rebuilding is the cleaner path.
Top-Level Functions, Grouped¶
There are 247 functions; the important ones break down as:
Connection / lifecycle¶
initializeEventSource(), manualReconnect(), cleanupResources(), setupDemoPasswordHandler(), enableDemoMode(), startDemoData(), sendFakeCSVData(), checkForDemoMode().
Rendering¶
updateFields() (local to each handler), updateAllEchosOptimized(), updateEchoIfChanged(), scheduleDOMUpdateOptimized(), updateWeatherAlerts(), updatePlotConfiguration(), processCSVDataOptimized(), formatSessionWindow() (used by all .session-window-label spans), updateAllTempUnitLabels() (cycles °F ↔ °C labels everywhere when the user toggles units).
Form / input¶
submitMessage(), handleAlternatorToggle(), togglePassword(), updatePasswordFields(), populateProfileForm(), handleProfileUpdate(), handleDeleteAllData(), convertTempFormIfNeeded().
Reset buttons¶
resetThermalPID(), resetInnerPID(), resetVoltageLoop(), resetTuningLog(), resetCVTuningLog(), resetThermalTuningLog(), restart actions.
Tuning logs¶
fetchTuningLog(), renderTuningLog(), commitTuningScore(), fetchCVTuningLog(), renderCVTuningLog(), commitCVTuningScore(), restartCVTest(), fetchThermalTuningLog(), renderThermalTuningLog().
Firmware / OTA¶
displayAvailableVersions(), confirmUpdate(), updateFirmwareVersion(), firmwareIntToString(), formatDeadline(), handleForcedUpdate(), disableAllInputs(), enableAllInputs(), triggerForcedUpdate().
Plot config¶
toggleTimeAxisMode(), reinitializeXAxisForNewMode(), queuePlotUpdate(), startInterpLoop(), lerp(), computeScaleRange(), reinitializePlotsWithNewTiming().
Diagnostics¶
setDiagnosticMode(), isDiagnosticMode(), diagLog(), diagWarn(), diagError(), devLog(), trackFrameTime(), profileOperation(), showPerformanceReport().
Utility¶
buildURL(), fetchWithTimeout(), setTrackedInterval(), setTrackedTimeout(), debounce(), toDisplayTemp(), tempUnitLabel(), setTempUnit().
Where to Modify When¶
| Goal | File / section to edit |
|---|---|
| Add a new live gauge | CSV1_FIELDS (insert at index N); add HTML span with id="<key>"; add to criticalFields or otherFields |
| Add a new echo for a setting | CSV3_FIELDS (insert at index N); add HTML span id="<key>_echo"; add entry to ECHO_REGISTRATIONS with transform |
| Add a new plot series | processCSVDataOptimized() (push to a new array); add uPlot series config; add legend entry; update reinitializePlotsWithNewTiming if axis changes |
| Add a new staleness threshold | TS_FIELDS + STALE_THRESHOLD_*_MS; wrap the corresponding DOM span in the greying-out logic |
| Add a new SSE event type | source.addEventListener('YourEvent', ...) block in initializeEventSource() |
| Add a new button action | <button onclick="myFunc()"> in HTML; function myFunc() in the appropriate section above |
| Tweak the dark / light theme | updateUplotTheme(plot) and the corresponding CSS variables (see CSS Styling) |
Cross-references¶
- CSV plumbing recipe (the firmware side of every field-add) → Important Functions
- SSE dispatcher cadence, priority gating → Network & Web System → Server-Sent Events
- HTML structure, form pattern → HTML Structure
- CSS theming, dark / light mode, mobile layouts → CSS Styling