Skip to content

Adding a Telemetry Value

This page shows how to send a new live number from the regulator firmware to the web dashboard. Telemetry travels over four one-way browser-push channels (Server-Sent Events, or SSE), and the single most important decision — the one contributors get wrong most often — is picking the right channel before writing any code.

Step zero: classify your value

Ask one question first: is this a number the firmware computes, or a value the user types in?

  • Fast-moving live value (changes many times per second, the dashboard should plot or display it in real time — battery voltage, field duty, control-loop outputs) → the high-rate channel (CSVData, called CSV1, sent roughly 10 times per second).
  • Slow-moving status, diagnostic, or counter (a once-per-5-seconds update is plenty — peak temperatures, error counters, runtime state flags, timing statistics) → the diagnostics channel (CSVData2, called CSV2, sent every 5 seconds).
  • User-configurable setting (it has a form input, persists across reboot) → that is not telemetry. It belongs on the settings-echo channel (CSV3) with persistence — follow Adding a User Setting instead. Putting a setting in CSV2 makes it re-send every 5 seconds for no reason and breaks the "echo on change" behaviour the settings UI depends on.

A fourth channel (TimestampData) carries staleness watchdogs — the last-update time of each external sensor source so the UI can grey out dead inputs. You only touch it when adding a whole new sensor source, not for ordinary values.

The recipe (CSV1 and CSV2 are the same shape)

Both channels are a comma-separated frame of scaled integers. The firmware builds the frame with one big formatted-print call (snprintf), and the browser splits it and maps each position to a name using a JavaScript array. Four stops, all of which must stay in sync:

  1. Declare the global variable in the firmware (typically in Xregulator.ino) and update it wherever your computation lives. Floats are fine — they get scaled to integers at send time.

  2. Append the value to the channel's payload builder in 3_functions.ino. Search for payload1Len = snprintf (CSV1) or payload2Len = snprintf (CSV2). Two edits in the same statement: add one more conversion specifier (usually %d) to the end of the format string, and add the matching argument to the end of the argument list. Values go through the SafeInt() helper, which multiplies by a scale factor and turns not-a-number values into -1. Also add your field's name to the matching position-index list (the Csv1Index / Csv2Index enum near the top of 3_functions.ino) — the new entry goes immediately above the count sentinel (CSV1_FIELD_COUNT / CSV2_FIELD_COUNT).

    Illustrative only — your real code should follow the current pattern in the source; search 3_functions.ino for the named example:

    SafeInt(setpointLimited, 100),   // sent as value x100, two decimal places preserved
    
  3. Append the matching name to the channel's JavaScript field array in web_src/script.js. Search for CSV1_FIELDS or CSV2_FIELDS. The name goes at the end of the array, in the same position your argument occupies in the firmware payload. Position is everything — these frames have no labels on the wire.

  4. Consume it in the UI. The dispatcher in script.js turns each frame into named values; wherever you display yours, divide by the same scale factor you used in SafeInt() on the firmware side (a value sent as SafeInt(x, 100) is read back as value / 100).

The three-way sync rule

Every channel carries its own declared field count as the first value of each frame, taken from the count sentinel at the bottom of the channel's position-index list (CSV1_FIELD_COUNT, CSV2_FIELD_COUNT, ...). Three things must always agree:

  1. the count sentinel in the firmware enum,
  2. the number of conversion specifiers in the format string (which is count + 1, because the count itself is the first field), and
  3. the length of the JavaScript field array (CSV1_FIELDS / CSV2_FIELDS).

If the format string has fewer specifiers than arguments, the trailing fields are silently dropped — the browser sees a frame whose actual length doesn't match the declared count and rejects the entire frame, taking every other value on that channel down with your one mistake. This failure mode has recurred repeatedly in this project. Re-count all three after every change.

Worked examples to search for

  • CSV1 (high-rate): setpointLimited — the voltage-controller's rate-limited target. Search for CSV1_setpointLimited in 3_functions.ino, SafeInt(setpointLimited, 100) in the CSV1 payload builder, and "setpointLimited" in CSV1_FIELDS in web_src/script.js. Note the UI divides by 100 wherever it reads this value.
  • CSV2 (diagnostics): MaxAlternatorTemperatureF — a per-session peak. Search for CSV2_MaxAlternatorTemperatureF in 3_functions.ino, SafeInt(MaxAlternatorTemperatureF) in the CSV2 payload builder, and "MaxAlternatorTemperatureF" in CSV2_FIELDS in web_src/script.js.

One practical note on cost: only one channel transmits per pass through the firmware's main loop, so adding a field does not add per-tick CPU load — it only makes that channel's frame slightly longer. The real cost of a misplaced field is wasted repetition (a never-changing setting re-sent every 5 seconds) and a broken echo workflow, which is why classification comes first.