Networking and Web Server¶
The regulator serves its own dashboard: a web application hosted on the device, streamed live data over server-sent events, and a single settings endpoint that everything funnels through. This page covers the WiFi modes, how the dashboard is served, how settings and commands reach the firmware, and the background task that handles outbound internet work.
WiFi modes¶
setupWiFi() picks one of three modes at boot. Two recovery pins are sampled first, then saved state decides.
| Mode | How you get there | What runs |
|---|---|---|
Configuration portal (MODE_CONFIG) |
GPIO45 held low at boot, first boot, or no saved network credentials | A minimal access point with default credentials and a captive setup page. The alternator is disabled in this mode — it exists to enter WiFi credentials and recover lost WiFi passwords. |
Operational access point (MODE_AP) |
GPIO46 held low at boot | The device runs its own WiFi network (user-customizable credentials) with the full dashboard and full alternator function — no ship's network or internet needed. Cloud features are off. |
Station (MODE_CLIENT) |
The default once credentials are saved | Joins the ship's WiFi. The device also registers itself via mDNS so the dashboard is reachable at alternator.local. |
The distinction between the two button modes is deliberate: GPIO45 is "I need to reconfigure and the alternator must stay off"; GPIO46 is "emergency access with full charging, no credentials required."
In station mode, checkWiFiConnection() owns reconnection — the library's auto-reconnect is disabled, and the function tracks connection-state edges with backoff, counting disconnects and reconnects for the diagnostics panel.
Configuration portal¶
setupWiFiConfigServer() serves a single setup form and intercepts the captive-portal probe URLs that phones and laptops fire when they join a network (the iOS, Android, and Windows connectivity-check paths all redirect to the form). Submitting the form posts to /wifi; the handler stores the credentials with settingWrite() — settings live in the device's non-volatile key-value store (NVS), not as files — marks first-time setup complete, and restarts into station mode.
In the operational access point mode, by contrast, the connectivity probes are answered with a plain "success" so no captive sheet pops up — the user opens the dashboard deliberately.
The web server¶
The dashboard is served by an asynchronous web server (ESPAsyncWebServer) on port 80. The interface files — HTML, JavaScript, CSS, and the plotting library — are stored gzip-compressed in on-device flash (a LittleFS web partition), copied into RAM at boot (cacheGzFiles()), and served with a Content-Encoding: gzip header (serveCachedGz()). Browsers decompress transparently, so the page references plain filenames like script.js while the wire carries the compressed bytes.
There are two copies of the web bundle: the production set, replaced by software updates, and an immutable factory set. At boot validateWebFilesystem() checks the production files and falls back to the factory copies if they are corrupt or missing, so a failed update cannot leave the device without a dashboard.
The settings endpoint: /get¶
Every user-tunable parameter and most one-shot commands go through a single endpoint, GET /get?<name>=<value>. The handler checks each known parameter name; a match parses the value, updates the live variable, and persists it with settingWrite() into the device's non-volatile key-value store (NVS) — user settings are not files. The write is compare-first, so re-submitting an unchanged value costs no flash wear.
Applying any setting also raises a "settings changed" flag (settingsDirty), which causes the settings-echo channel (see below) to re-broadcast current values — that is how the dashboard's echo labels confirm a change landed.
Two things are deliberately handled before the password gate:
- Field off is always allowed. A request to turn the alternator off is honored without a password — safety takes precedence over access control.
- The software-update trigger is processed first so an update can be started even if the rest of the interface is misbehaving. See Over-the-Air Updates.
Everything else requires the interface password.
The interface password¶
Settings changes and destructive actions require a user-set interface password (default admin). The device stores a one-way fingerprint of it (a SHA-256 hash — see sha256() and validatePassword()), and the dashboard's unlock flow verifies a candidate password against that hash before enabling the settings forms. Requests arriving without a valid password are refused.
The interface password is distinct from the two WiFi passwords, and its recovery story is different. The configuration portal (GPIO45) recovers WiFi credentials only: ship-network credentials can be re-entered and the hotspot password reverts to its default, but the portal form has no interface-password field, and the full factory reset (/factoryReset → performDeepFactoryReset()) itself requires the interface password. A lost interface password therefore has no self-service reset on the device today.
Live data: server-sent events¶
The dashboard receives all live data over one server-sent-events stream (/events, an AsyncEventSource). The firmware-side dispatcher, SendWifiData(), runs every loop pass and sends at most one event per pass, in strict priority order:
- Fast telemetry (
CSVData) — the high-rate channel: voltages, currents, RPM, field duty, control-loop state. Sent on a user-adjustable cadence of roughly ten times per second. - Console messages — firmware log lines, throttled.
- Slow telemetry (
CSVData2) — diagnostics, counters, worst-case timings, status flags, every few seconds. - Settings echo (
CSVData3) — the current value of every user setting. Event-driven: it fires when a setting changes, with a slow heartbeat as a fallback, so the dashboard always reflects what the firmware actually holds. - Freshness timestamps (
TimestampData) — per-sensor "last updated" ages so stale sources can be greyed out.
Payloads are compact comma-separated integers: each value is scaled and rounded by SafeInt() on the firmware side and un-scaled by a matching index map in script.js. The first field of every payload is the declared field count, and the browser rejects the whole payload on a mismatch — a deliberate tripwire, because a format-string/argument mismatch in the firmware would otherwise silently truncate the stream.
When a new browser connects, the connect hook nudges the settings-echo channel so the client sees current settings immediately instead of waiting for the heartbeat. Payload building is skipped entirely when no client is connected.
Background internet work¶
All outbound HTTPS — cloud telemetry history, configuration backup, weather forecast fetches, and software-update checks (all opt-in, user-visible features) — runs in a dedicated task (httpsTask) on the second core, fed by a small queue. This keeps TLS handshakes, which are slow and memory-hungry, entirely off the control core.
Two coordination rules matter:
- A
core0Busyflag is set while a request is in flight; other subsystems consult it before doing anything that would compete for resources (for example, WiFi power-down transitions wait for it to clear). - Cloud uploads are gated on the alternator field having been off and settled for a while (
fieldOffSettled()), so a TLS handshake never lands on top of a charging transient. A dashboard "upload now" button can bypass the schedule, but work still drains through the same queue.
Failure handling is conservative: repeated upload failures temporarily suspend uploads, while client-side rejections (malformed payloads) are dropped rather than retried forever.