fix: harden portal OTA release

This commit is contained in:
2026-09-20 19:44:05 +10:00
parent 7c5b50499e
commit 360b7cff86
20 changed files with 621 additions and 198 deletions
+8 -6
View File
@@ -25,12 +25,14 @@ the `WM_NMCLI_AUTH` options.
## A/B portal OTA fixture
The same fixture has dedicated `*_ota_a` and `*_ota_b` PlatformIO environments
for the physical portal HTTP OTA test harness. A and B differ only by a compiled
marker served from the fixture-only `/api/test/firmware-marker` endpoint and an
immutable serial boot marker. The test harness requires serial A, updater
start/completion, then serial B, so it proves a B boot without trusting saved
portal values, EEPROM, or a filename.
The physical portal HTTP OTA test harness uses one `*_ota` environment per
platform. It writes a harness-only A/B identity header to an ignored, owner-only
directory unique to that run before each build, so PlatformIO reuses dependency
objects while the fixture-only `/api/test/firmware-marker` endpoint still proves
the newly booted image. The test harness requires the real form's successful response, its automatic
restart, and two fresh B-marker responses. Passive serial capture is retained
for failure diagnosis, but a product log-message wording change cannot turn a
successful A-to-B update into a failed test.
```bash
./tools/portal-hardware ota \
+12 -22
View File
@@ -6,6 +6,9 @@ framework = arduino
lib_ldf_mode = deep+
lib_deps =
WiFiManager=symlink://../..
build_flags =
extra_scripts =
pre:tools/ota_fixture_input.py
[env:esp8266]
extends = common
@@ -27,34 +30,21 @@ build_flags =
-I${platformio.packages_dir}/framework-arduinoespressif32/libraries/Network/src
-DWM_LOG_LEVEL=4
; OTA test-harness images deliberately differ only by their compile-time marker.
; ESP8266 needs an OTA-capable linker layout; ESP32 needs a tracked two-slot
; partition table. The hardware runner always flashes A over serial then
; uploads B through the real portal form.
[env:esp8266_ota_a]
; The OTA test harness writes its A/B marker into an ignored header included
; only by the portal fixture. One environment per platform keeps the explicit
; update layout while allowing the A-to-B rebuild to reuse every library object.
[env:esp8266_ota]
extends = env:esp8266
board_build.ldscript = eagle.flash.4m1m.ld
build_flags =
${env:esp8266.build_flags}
-DWM_OTA_TEST_IMAGE=\"A\"
${common.build_flags}
-DWM_PORTAL_OTA_TEST=1
[env:esp8266_ota_b]
extends = env:esp8266
board_build.ldscript = eagle.flash.4m1m.ld
build_flags =
${env:esp8266.build_flags}
-DWM_OTA_TEST_IMAGE=\"B\"
[env:esp32_ota_a]
[env:esp32_ota]
extends = env:esp32
board_build.partitions = partitions/esp32_ota_4m_no_fs.csv
build_flags =
${env:esp32.build_flags}
-DWM_OTA_TEST_IMAGE=\"A\"
[env:esp32_ota_b]
extends = env:esp32
board_build.partitions = partitions/esp32_ota_4m_no_fs.csv
build_flags =
${env:esp32.build_flags}
-DWM_OTA_TEST_IMAGE=\"B\"
${common.build_flags}
-DWM_PORTAL_OTA_TEST=1
+11 -9
View File
@@ -3,19 +3,21 @@
namespace {
// These markers belong only to the portal hardware fixture. They are compiled
// into the image rather than stored in Wi-FiManager settings, so an A -> B
// assertion proves that the new firmware booted after the updater restarted
// the board. Normal portal test-harness builds retain a descriptive fixture
// value.
#if defined(WM_OTA_TEST_IMAGE)
// These markers belong only to the portal OTA fixture. The hardware runner
// writes the ignored header immediately before each A/B build, so the marker
// is compiled into firmware rather than saved in WiFiManager settings.
#if defined(WM_PORTAL_OTA_TEST)
#include <ota_fixture_identity.h>
#ifndef WM_OTA_FIXTURE_IMAGE
#error "Portal OTA fixture identity is missing."
#endif
// PlatformIO releases the serial port only after the upload-triggered reset.
// The physical OTA test harness then attaches passively, so give it the same
// explicit window as the DeviceFramework A/B fixtures before boot evidence or
// portal work begins. Normal portal test-harness builds keep the short delay.
constexpr unsigned long kSerialMonitorAttachDelayMs = 5000UL;
#else
#define WM_OTA_TEST_IMAGE "portal-harness"
#define WM_OTA_FIXTURE_IMAGE "portal-harness"
constexpr unsigned long kSerialMonitorAttachDelayMs = 300UL;
#endif
@@ -87,7 +89,7 @@ void registerOtaTestMarker() {
server->on("/api/test/firmware-marker", HTTP_GET,
[](AsyncWebServerRequest* request) {
String response = F("{\"marker\":\"");
response += WM_OTA_TEST_IMAGE;
response += WM_OTA_FIXTURE_IMAGE;
response += F("\",\"freeSketchSpace\":");
response += String(ESP.getFreeSketchSpace());
response += F("}");
@@ -105,7 +107,7 @@ void setup() {
// and after its browser upload. It cannot be faked by saved portal values
// or an HTTP response from a stale image.
Serial.print(F("WiFiManager portal OTA fixture image: "));
Serial.println(WM_OTA_TEST_IMAGE);
Serial.println(WM_OTA_FIXTURE_IMAGE);
// This fixture must be independent of whichever sketch was previously
// flashed to the board. Clear saved station credentials before starting
@@ -0,0 +1,23 @@
"""Add the run-owned A/B identity directory to this portal test-harness build."""
import os
from SCons.Script import Exit
Import("env")
if env["PIOENV"].endswith("_ota"):
identity_dir = os.environ.get("WIFIMANAGER_OTA_IDENTITY_DIR")
if not identity_dir:
print(
"WIFIMANAGER_OTA_IDENTITY_DIR is required; "
"run this fixture through its named test harness."
)
Exit(1)
identity_header = os.path.join(identity_dir, "ota_fixture_identity.h")
if not os.path.isfile(identity_header):
print(f"OTA fixture identity header is missing: {identity_header}")
Exit(1)
env.Append(CPPPATH=[identity_dir])