Skip to content

Adding a Dashboard Control

This page covers the browser side of putting a new control on the dashboard: what a settings row is made of, how a click actually reaches the device, how the device confirms the change back to the page, and the difference between a persistent setting and a one-shot action button. It assumes you have read Adding a User Setting for the firmware half.

Anatomy of a settings control

Every user-adjustable parameter on the dashboard is one self-contained block (a div with class form-row) in web_src/index.html. Pick any existing one as a donor — the "Absorption Completion Time" row is a fully worked example (search index.html for absorptionCompleteTime_echo). Each row contains:

  • A label with an echo span. The label text includes a small live readback — <span id="absorptionCompleteTime_echo">?</span> — showing the value the device is actually using. It starts as ? and is filled in as soon as the settings-echo channel delivers data.
  • A tooltip explaining the parameter in plain English.
  • A tiny ordinary web form (<form action="/get" method="GET" target="hidden-form">). Submission is native HTML form behavior: the browser builds a /get?yourParameter=value&password=... request. The target="hidden-form" points the response at an invisible inline frame at the top of the page (<iframe name="hidden-form">), so submitting never navigates away from the dashboard.
  • A hidden password input (<input type="hidden" name="password" class="password_field">). The dashboard fills these automatically once the user has unlocked the interface; the device rejects settings changes without it.
  • The named input. Its name attribute must exactly match the parameter string the firmware checks with hasParam(...) in 3_functions.ino.
  • The submit button, styled with the shared button classes (below).

Rows are separated by <hr /> elements — keep one between every parameter, it is a deliberate convention.

Illustrative skeleton only — copy a real row from index.html rather than typing this from scratch:

<div class="form-row">
  <div class="form-label">
    <span>My Parameter (sec) (<span id="MyParameter_echo">?</span>):</span>
  </div>
  <div class="form-input">
    <form action="/get" method="GET" target="hidden-form">
      <input type="hidden" name="password" class="password_field">
      <input name="MyParameter" type="number" step="1" min="1" max="999" />
      <input onclick="submitMessage()" type="submit" value="Set" class="btn-primary" />
    </form>
  </div>
</div>
<hr />

How submission and confirmation actually work

Two non-obvious details:

  1. submitMessage() is not the submit mechanism. Most submit buttons carry onclick="submitMessage()", but if you read that function in web_src/script.js you will find it only plays a brief visual pulse on the button. The real work is the native HTML form GET into the hidden frame. (A few safety-critical battery fields route through a validation wrapper instead — search script.js for validateAndSubmitBatteryNumber — which sanity-checks the number before allowing the same form submission.)
  2. Confirmation is the echo, not the HTTP response. The page never parses a reply from /get. Instead, the firmware handler marks settings as changed (settingsDirty), which makes the settings-echo channel (CSV3) send the full current settings on its next turn. The dashboard's echo registration list (search script.js for echoUpdates) routes the value into your _echo span, applying its transform for display units. The user sees the label change from the old value to the new one — that round trip through the device is the proof the setting took. If the echo never updates, the device never accepted the change.

Buttons that are actions, not settings

Some controls command a momentary action — nothing to persist, nothing to echo. These still arrive as /get parameters, but the firmware handler only sets a flag or performs the action, with no settingWrite() call. Verified example: the alarm-latch reset. Search 3_functions.ino for hasParam("ResetAlarmLatch") — the handler sets a one-shot flag and explicitly does not persist anything, because the action is momentary. (ZeroIMU, the tilt-sensor zeroing command, is another in the same block.)

When adding an action button: send a parameter, handle it in the /get block, perform the action, optionally push a confirmation line to the dashboard console (the handlers use queueConsoleMessage(...) for this) — and skip steps 4-7 of the settings recipe entirely.

Style and UX rules

  • Use the shared button classes from web_src/styles.cssbtn-primary (main affirmative action), btn-secondary, btn-danger / btn-danger-ghost (destructive actions), and the btn-sm size modifier. Never inline-style a button; the shared classes carry the light/dark theming and press feedback for the whole app.
  • Design for mobile first-class. This exact UI also runs inside the iOS app (a Capacitor wrapper around the same files), so every control must work at phone width and with touch targets — no hover-only affordances.
  • No emojis anywhere in UI text, and lead user-facing copy with plain English (put the code term in parentheses if it must appear at all).
  • Keep the <hr /> separators between parameter rows.

Final verification

If your change touched any of the telemetry or settings-echo payloads (adding an echoed setting always touches CSV3), re-verify the three-way sync before flashing: the channel's count sentinel in the firmware enum, the number of conversion specifiers in its format string, and the length of its JavaScript field array must all agree. A mismatch makes the browser reject every frame on that channel — your control and everyone else's go dark together. The rule is spelled out in Adding a Telemetry Value.