FirmwareUpdateModule
Source:
FirmwareUpdateModule.h
FirmwareUpdateModule¶
src/core/FirmwareUpdateModule.h:99Inherits:
MoonModule
A thin status surface for OTA flashing — surfaces flash progress as live read-only controls plus the per-module status banner.
Not user-configurable: ensureInfraModules() recreates it on every boot if absent (same safety net as NetworkModule). The actual flash is driven by POST /api/firmware/url in HttpServerModule, which hands the URL to platform::http_fetch_to_ota() — a task that downloads via esp_https_ota and writes the next OTA partition, communicating through the file-scope globals above (g_otaStatus, g_otaBytesRead, g_otaBytesTotal). This module polls them in tick1s() and copies into its bound control buffers so the WebSocket state push picks up the change at 1 Hz. The shared-buffer + 1 Hz poll pattern is the simplest way to bridge a FreeRTOS task and a MoonModule on the scheduler thread without locks; no synchronisation, since torn reads of display-only fields are acceptable.
Controls.version — pure semver (MM_VERSION): a stable release is a clean X.Y.Z, a moving latest build is a monotonic prerelease <core>-dev.<N> (semver.org §9/§11), so the release channel is derivable from the version rather than mixed into it, keeping the string a clean machine-comparable semver the UI's update check compares against the newest GitHub release. build — build date/time. firmware — the build-time variant key (esp32, esp32-eth, esp32s3-n16r8, … / desktop-*) that identifies which release asset matches the device (a legacy esp32-eth-wifi key OTA-maps to esp32); the physical hardware is SystemModule's deviceModel. firmwarePartition — running app image size / app-partition size (named distinctly from firmware so a find(name === "firmware") caller resolves the string). update_pct — live byte counters rendered "X KB / Y KB" (total is 0 until esp_https_ota_get_image_size reports it just after the TLS handshake; the name is historical, the wire shape is bytes).
Flash phase is not a control — it surfaces through the module's shared status slot (setStatus()): idle clears the banner, an error: prefix maps to Severity::Error, everything else (starting/downloading/flashing/rebooting) is neutral. On desktop (platform::hasOta == false) the controls still exist for UI uniformity but the route returns 501 and status stays "idle" forever.
Flash lifecycle. A POST /api/firmware/url (HttpServerModule, body {"url": …}) writes starting and spawns the platform OTA task, which walks: downloading → esp_https_ota_begin opens the TLS connection and follows redirects (a GitHub release URL 302s to release-assets.githubusercontent.com) → flashing, esp_https_ota_perform advancing update_pct 0→100 → esp_https_ota_finish commits the image to the next OTA partition and flips the boot pointer → rebooting, a ~600 ms delay so the HTTP response reaches the browser first, then esp_restart(). The device boots the new image and the UI re-reads version / firmware.
Error taxonomy (all via the status slot, error: prefix, staying until the next POST): error: ota begin <IDF name> (DNS / TLS / no OTA partition), error: ota perform <IDF name> (network drop mid-download), error: incomplete download (size mismatch), error: ota finish <IDF name> (commit / boot-flip), error: task create failed (xTaskCreate OOM — no retry). A wrong-firmware binary fails at esp_https_ota_begin (chip-family mismatch) or boot (partition mismatch) — recoverable by a USB re-flash, not a brick.
Compatibility is the caller's responsibility (the install-picker's isCompatible()): strip -eth* from both firmware keys, equal identities are compatible — so esp32 and esp32-eth are mutually OTA-compatible (same chip, different feature flags), the legacy esp32-eth-wifi key strips to esp32, and esp32s3-n16r8 is only itself.
Prior art:esp_https_ota is the standard ESP-IDF OTA-from-HTTP component used by every ESP32 OTA flow since IDF v4.x; the install-picker UI is the new layer on top.

Public Methods¶
virtual inline bool respectsEnabled() const override
: Diagnostics keep ticking regardless of the user toggle; matches SystemModule + NetworkModule.
virtual inline void setup() override
: Default lifecycle propagates to children.
virtual inline void defineControls() override
: defineControls MUST be idempotent and pure: only controls_.clear() + controls_.addX().
virtual inline void tick1s() override
inline void publishStatus()
: Point the shared status slot at our owned buffer, choosing the severity from the status text: the platform OTA task prefixes every failure with "error: " (see platform_esp32_ota.cpp), so that prefix is the Error gate.