Over-the-Air Updates¶
The regulator updates its own firmware and dashboard over WiFi: the user picks a version, the device reboots into a dedicated recovery installer, and the installer downloads, cryptographically verifies, and writes the new software. No cable, no computer, and a failed update leaves the previous software in place.
What the user sees¶
The dashboard's software-update panel fetches the list of released versions, with release notes, from the project's update server. The current version is shown alongside; clicking a version starts the update. The device disables the alternator, reboots, installs, and comes back up on the new version — typically within a minute or two.
There is also a forced update path: the cloud can flag a critical release for a device, in which case the dashboard shows an "update now" prompt. It never auto-installs — a person always clicks the button. (Firmware-side: the flag only raises the prompt; the same install trigger below does the work.)
How an update actually runs¶
The install trigger is a single request, /get?UpdateToVersion=<version>, handled first in the settings endpoint — before the password gate — so an update can always be started even if the rest of the interface is in a bad state.
The handler:
- Turns the alternator field off for safety.
- Records the requested version in non-volatile storage (the
update_reqrecord, written by theUpdateToVersionblock in the/gethandler), along with a wake flag so the device keeps WiFi up after the reboot. - Sets the next boot to the recovery installer and restarts.
As a decision tree (the rare first branch applies when the device is already running the installer, e.g. after a recovery boot):
Update requested (/get?UpdateToVersion=<version>)
├─ Already running the factory installer?
│ ├─ YES → install immediately:
│ │ download → SHA-256 + RSA verify → write updated slot (ota_0)
│ │ → reboot → boot logic finds a valid ota_0 → runs new firmware
│ └─ NO (running normal firmware from ota_0)
│ → write update request to NVS (target version + update flag + wake flag)
│ → set next boot to the factory installer → reboot
│ → installer finds the request → download → verify → write ota_0
│ → reboot → boot logic finds a valid ota_0 → runs new firmware
The recovery installer is the factory application — a complete, known-good copy of the firmware that ships in its own flash slot and is never overwritten by updates. It serves as a "golden installer": because it never changes, a bug in a new release cannot break the machinery that installs the next one.
On boot, the installer finds the pending request (checkForPendingUpdateNonBlocking()) and runs the install (performOTAUpdateToVersion() → performStreamingOTAUpdate()):
- The release is a single archive containing both the firmware image and the compressed dashboard web files, downloaded from the project's update server and unpacked as it streams (
StreamingExtractor) — nothing is buffered whole, so a multi-megabyte release installs within the device's modest RAM. - A SHA-256 digest is computed incrementally over the stream, and an RSA signature is verified on-device against a public key compiled into the firmware (
OTA_PUBLIC_KEY). Firmware goes to the spare application slot, web files to the production web area — and the new slot is only selected for boot after the signature checks out. - On success the device reboots into the new firmware. On any failure — bad signature, dropped connection, write error — the install is abandoned and the previously working software remains in place.
Trust model¶
Transport is HTTPS, but the trust anchor is the on-device signature check, not the connection. Even if the download path were redirected or tampered with, an image that does not verify against the embedded public key is refused. This is why the download client does not pin the server's certificate chain: integrity comes from the signature, and the server can be re-homed without touching devices in the field.
Released version numbers only move forward (monotonic) — the update flow assumes a device is never asked to "update" to something older than a release it already cleared.
Boot selection and recovery¶
Every boot, ensurePreferredBootPartition() decides which application runs:
- Normally it prefers the updated slot, if a valid image is present there.
- If the updated slot is invalid or corrupt, it falls back to the factory installer automatically.
- Holding the recovery pin (GPIO41) low at boot forces the factory application regardless — a hardware override if an update misbehaves. It also clears any pending update request, so a bad request cannot cause a boot loop.
As a matrix:
| GPIO41 (recovery pin) | Updated slot (ota_0) valid? |
Application that runs | Dashboard files |
|---|---|---|---|
| Held low at boot | any | Factory installer | Factory copies (factory_fs) |
| Normal (high) | yes | Updated firmware (ota_0) |
Production set (prod_fs) |
| Normal (high) | no | Factory installer | Factory copies (factory_fs) |
The dashboard files have the same redundancy: the production web files are validated at startup (validateWebFilesystem()), and the device falls back to the immutable factory copies if they fail, so there is always a working interface.
Lifecycle, end to end¶
- As shipped — only the factory slot is programmed; the updated slot is empty, so the factory firmware runs (with the factory web files).
- First update — the factory app installs directly into the updated slot; from then on the updated slot boots (with the production web files).
- Every later update — the running firmware hands off to the factory installer via the NVS update request, the installer writes the updated slot, and the device boots back into it.
- Recovery — GPIO41 (or a corrupt updated slot) lands you on the factory firmware until the next successful update.
Invariants¶
- Boot preference never changes — every boot tries the updated slot first, with the factory installer as the fallback.
- Updates only ever write the updated slot. The factory installer is never overwritten over the air — it is the permanent, known-good recovery path.
- The trip through the factory installer exists only to get the running app out of the way. A flash slot cannot be rewritten while it is executing, so the installer — running from its own slot — does all download, verify, and write work.
- The new image only becomes bootable after its signature verifies. Any failure — bad signature, dropped connection, write error — leaves the previous software selected and running.
A note for contributors flashing over USB¶
After a device has taken even one over-the-air update, the boot logic prefers the updated slot — and the Arduino IDE's USB upload writes the factory slot. The practical symptom: you flash your modified code over USB, the upload reports success, and the device behaves exactly as before, because your code is in the factory slot while the previously installed update keeps booting. The build is intact; it is not the one being selected. To run factory-slot code, hold the recovery pin (GPIO41) low at boot, which forces the factory application. Conversely, anything you change in the download-and-verify machinery itself only takes effect on real updates after the factory slot is re-flashed, since the factory app is the one that performs installs.