ImprovProvisioningModule
Source:
ImprovProvisioningModule.h
ImprovProvisioningModule¶
src/core/ImprovProvisioningModule.h:95Inherits:
MoonModule
Browser-driven WiFi provisioning over USB-serial, using the Improv-WiFi protocol (https://www.improv-wifi.com/, from Nabu Casa).
Improv is an open standard for handing a device its WiFi credentials over a local link — USB serial here — at the moment it has no network yet, solving the bootstrap chicken-and-egg: a freshly-flashed ESP32 isn't on your WiFi, so you can't reach it over the network to give it the password; Improv carries that first handoff over the cable the browser is already connected to from flashing. projectMM extends this past credentials with the APPLY_OP vendor RPC ("Improv = REST over serial") to push the whole device config (including the deviceModel identity, which is just one of the config controls) over the same already-there link.
This module is the status surface: one read-only provision_status control that reports listening / received credentials / connecting / connected: <ssid> / error: reason / not supported on this platform (desktop). The actual protocol parsing + UART task live in the platform layer (mm::platform::improvProvisioningInit, platform.h). [tick1s()] polls a ready flag the platform task sets when credentials arrive, then calls NetworkModule::setWifiCredentials, which writes through to the same buffers the AP-fallback UI flow uses. A code-wired child of NetworkModule (markWiredByCode()) so persistence-apply preserves it across reboots even on devices whose Network.json predates the addition. On desktop (platform::hasImprov == false) the module exists for UI uniformity; the listener-install is skipped.
Transports. The listener serves both UART0 (external USB-to-UART bridges) and the S3's native USB-Serial-JTAG port, so a native-USB-only board provisions over that port directly. If neither serial path is available, the AP-mode flow remains (the device boots a SoftAP at 4.3.2.1, join from a phone, enter credentials). Both transports speak the same Improv-WiFi serial protocol — frames of IMPROV + version byte + type + length + payload + checksum (full spec: https://www.improv-wifi.com/serial/; the constants are authored in [ImprovFrame.h]).
RPCs. Four standard commands plus two vendor extensions: GET_CURRENT_STATE (authorized / provisioned), GET_DEVICE_INFO ([firmware, version, chipFamily, deviceName]), GET_WIFI_NETWORKS (synchronous scan, up to 10 SSIDs — rejected while STA is connected), WIFI_SETTINGS (writes SSID + password, polls wifiStaConnected() up to 30 s, replies with http://ip/ or ERROR_UNABLE_TO_CONNECT). Vendor SET_TX_POWER (0xFD) persists + applies a pre-association TX-power cap before any association attempt — the escape hatch for boards whose LDO browns out at full TX power, since the cap must land before the first association or the board fails auth at 20 dBm before it is ever online. Payload [1][dBm] (0–21; 0 lifts the cap); an out-of-range value replies with error 0x81. Vendor APPLY_OP (0xFC) carries ONE REST op as JSON, routed to HttpServerModule's apply-core — the exact same code the HTTP handlers call — so a REST call over the network and an APPLY_OP over serial execute identically. The web installer pushes a device-model's whole catalog config this way during provisioning (the deviceModel identity is just one set System.deviceModel op), so the defaults apply over the serial port the installer already owns during the flash — which is what lets the HTTPS installer page configure an http:// device a browser fetch can't reach (mixed-content). Ops are idempotent; a long value chunks across frames into a reassembly buffer, applied on the device's main loop when last=1; single-buffered (a new op errors with 0x82 while the previous is unconsumed).
Frame payload shape.[0xFC][seq][last][chunk]. The installer paces ops open-loop (a fixed delay sized to the worst-case consume window) rather than reading the ack back, so a lost op is improbable rather than impossible; each op is idempotent, so a re-flash re-applies cleanly.
Parent-key caveat. The serial add op names the parent parent, while HTTP POST /api/modules names it parent_id; both feed applyAddModule() but parse different keys, so an HTTP payload is NOT a drop-in APPLY_OP — rename parent_id → parent. The serial key stays terse because every byte counts against the frame.
clearChildren pre-pass. The installer does a clearChildren pre-pass for every add-parent (not only replaceChildren containers) so "apply device defaults" lands the same with or without an erase: the device-side add is idempotent on module id, so a persisted same-name child would otherwise survive and the re-add be skipped.
Eth-only builds. The serial listener runs on every ESP32 target, including Ethernet-only builds: they compile in the vendor RPCs plus GET_CURRENT_STATE / GET_DEVICE_INFO, so the installer pushes config over serial to an eth device exactly as to a WiFi one. The WiFi-provisioning RPCs (WIFI_SETTINGS, GET_WIFI_NETWORKS) build only on WiFi targets; on eth, GET_CURRENT_STATE reports "provisioned" + the URL from the Ethernet link. WIFI_SETTINGS and GET_WIFI_NETWORKS are both rejected while platform::wifiStaConnected() — the scan gate protects large installs, since esp_wifi_scan_start puts the radio into scan mode for 2-5 s, dropping inbound ArtNet packets (a visible glitch on a 16K-LED rig).
Prior art: Improv-WiFi is the standard ESPHome / Home Assistant use for cross-firmware WiFi provisioning (spec: https://www.improv-wifi.com/serial/; reference C++: https://github.com/improv-wifi/sdk-cpp). projectMM-v1's deploy/wifi.py + deploy/flashfs.py --wifi covered the same rack-provisioning use case by baking credentials into a LittleFS partition image and re-flashing; Improv replaces that with live serial provisioning (devices stay running, no flash mode required).

Public Methods¶
inline void setSystemModule(SystemModule * s)
inline void setNetworkModule(NetworkModule * n)
inline void setHttpServerModule(HttpServerModule * h)
: For the APPLY_OP vendor RPC — the module routes a pushed REST op to the HttpServerModule's apply-core (the same code /api/modules + /api/control use).
virtual inline bool respectsEnabled() const override
: Diagnostics keep ticking; matches FirmwareUpdateModule / SystemModule.
virtual inline bool userEditable() const override
: Apparatus, not swappable content — provisioning is a fixed device service.
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
: Poll the SET_TX_POWER cap then the WiFi credentials the Improv task published (an acquire-load pairs each atomic flag with the platform task's release-store so the buffer writes are visible before we read them).
virtual inline void tick() override
: Apply a pending APPLY_OP through HttpServerModule::applyOp.