Adding a Dashboard Control¶
This page assumes you have read Adding a User Setting for the firmware half.
Parts 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. Copy any existing one — 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 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=valuerequest. Thetarget="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. You add no credential fields yourself. At submit time the page adds to every/getform a hiddenuid(the regulator the page is showing, so a write cannot be applied by a second regulator responding at the same address) and, when the optional settings password is on, the unlock tokentok. The device accepts the write only while its settings window is unlocked (the Unlock Controls button). - The named input. Its
nameattribute must exactly match the parameter string the firmware checks withhasParam(...)in3_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 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 work¶
submitMessage()is not the submit mechanism. Most submit buttons carryonclick="submitMessage()", but if you read that function inweb_src/script.jsyou will find it only plays a brief visual pulse on the button. The request itself is the native HTML form GET into the hidden frame. (A few safety-critical battery fields route through a validation wrapper instead — searchscript.jsforvalidateAndSubmitBatteryNumber— which validates the number before allowing the same form submission. The Absorption Completion Time row is one of them, so drop itsonsubmitattribute and addonclick="submitMessage()"to the button when you copy it for an ordinary field.)- 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 cycle. The dashboard's echo registration list (searchscript.jsforechoUpdates) routes the value into your_echospan, applying itstransformfor 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 device stored the setting. 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 the flash key (step 2), the boot load (step 4), the settings-echo payload and its echo registration (steps 5 and 6), and the config-sharing manifest (step 8). Nothing is persisted, so there is nothing to load at boot, echo, or share. Steps 1, 3 and 7 still apply: the flag global, the /get handler, and the control itself in the page — which for an action can be a plain button that calls a small JavaScript helper (submitSimpleParam) instead of a full parameter row.
Style and UX rules¶
- Use the shared button classes from
web_src/styles.css—btn-primary(main affirmative action),btn-secondary,btn-danger/btn-danger-ghost(destructive actions), and thebtn-smsize modifier. Never inline-style a button; the shared classes carry the light/dark theming and press feedback for the whole app. - Design for mobile as a primary target. This exact UI also runs inside the iOS and Android apps (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 modified any of the telemetry or settings-echo payloads (adding an echoed setting always modifies CSV3), build the web bundle with compress_web.sh before flashing. It checks that each channel's firmware fields and JavaScript field array agree in count and order, regenerates CSV3_FIELDS from the firmware's CSV3_LIST, and refuses to build on a mismatch. A mismatch that reached a device would make the browser reject every frame on that channel, and every control on that channel, including yours, would stop updating. The rule is stated in Adding a Telemetry Value.