From 2ad3b134f2ffff1a91227fcde3bc9ebe6d2edb5f Mon Sep 17 00:00:00 2001 From: Jared Dohrman Date: Tue, 21 Apr 2026 18:19:33 +1000 Subject: [PATCH] fix: suspend web panel for OTA --- docs/custom-cli.md | 3 ++- docs/web-panel.md | 3 ++- examples/simple_repeater/MyMesh.cpp | 6 ++++++ examples/simple_repeater/MyMesh.h | 1 + src/helpers/CommonCLI.cpp | 1 + src/helpers/CommonCLI.h | 3 +++ src/helpers/web/WebService.cpp | 21 ++++++++++++++++++--- src/helpers/web/WebService.h | 2 ++ 8 files changed, 35 insertions(+), 5 deletions(-) diff --git a/docs/custom-cli.md b/docs/custom-cli.md index 0077cedf..1cac81bb 100644 --- a/docs/custom-cli.md +++ b/docs/custom-cli.md @@ -68,7 +68,7 @@ Legacy dotted aliases are also accepted: ### Web Panel Controls - `get web` -- `get web.status`: shows whether the local HTTPS panel is available. +- `get web.status`: shows whether the local HTTPS panel is available. After `start ota`, this reports `web:suspended ota` until the repeater reboots. - `get web.stats.status`: shows whether the dedicated stats page and history subsystem are enabled, whether recent history is active, whether PSRAM-backed history is available, and whether the SD-backed archive is mounted. When enabled, the history capture now covers supported environment telemetry too, not just the original battery/radio series. - `set web on|off` - `set.web on|off`: enables or disables the local HTTPS panel. @@ -123,6 +123,7 @@ Notes: - the panel still uses the repeater admin password for access - commands run with the same care as if you typed them into the repeater CLI directly - this is intended for local admin use on a trusted network +- `start ota` suspends the local repeater web panel until reboot so the OTA HTTP listener can take over port `80` ## Companion WiFi Rescue Commands diff --git a/docs/web-panel.md b/docs/web-panel.md index 53f9aa6c..2e59cd58 100644 --- a/docs/web-panel.md +++ b/docs/web-panel.md @@ -277,7 +277,8 @@ On mobile: 1. Press `Start OTA`. 2. Confirm the action. -3. Continue with your normal OTA workflow. +3. The local repeater web panel is suspended until reboot so OTA can take over HTTP on port `80`. +4. Continue with your normal OTA workflow. ### Use Historical Stats diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 06051279..90e1b6ac 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1973,6 +1973,12 @@ void MyMesh::clearStats() { ((SimpleMeshTables *)getTables())->resetStats(); } +void MyMesh::prepareForOTAStart() { +#if defined(ESP_PLATFORM) && WITH_WEB_PANEL + web.suspendForOTA(); +#endif +} + void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply) { if (region_load_active) { if (StrHelper::isBlank(command)) { // empty/blank line, signal to terminate 'load' operation diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index ad4d28d0..18912e95 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -255,6 +255,7 @@ public: void formatRadioStatsReply(char *reply, size_t reply_size) override; void formatPacketStatsReply(char *reply, size_t reply_size) override; void formatMemoryReply(char *reply, size_t reply_size) override; + void prepareForOTAStart() override; void startRegionsLoad() override; bool saveRegions() override; void onDefaultRegionChanged(const RegionEntry* r) override; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 7197a78e..9e2306e1 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -252,6 +252,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re strcpy(reply, "ERR: clock cannot go backwards"); } } else if (memcmp(command, "start ota", 9) == 0) { + _callbacks->prepareForOTAStart(); if (!_board->startOTAUpdate(_prefs->node_name, reply)) { strcpy(reply, "Error"); } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index d5123dc6..a3aaf541 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -92,6 +92,9 @@ public: virtual void saveIdentity(const mesh::LocalIdentity& new_id) = 0; virtual void clearStats() = 0; virtual void applyTempRadioParams(float freq, float bw, uint8_t sf, uint8_t cr, int timeout_mins) = 0; + virtual void prepareForOTAStart() { + // no op by default + } virtual void startRegionsLoad() { // no op by default diff --git a/src/helpers/web/WebService.cpp b/src/helpers/web/WebService.cpp index afc5ef5c..1420526d 100644 --- a/src/helpers/web/WebService.cpp +++ b/src/helpers/web/WebService.cpp @@ -4,7 +4,7 @@ #include #endif -WebService::WebService() : _fs(nullptr), _prefs{}, _runner(nullptr), _network(nullptr) { +WebService::WebService() : _fs(nullptr), _prefs{}, _runner(nullptr), _network(nullptr), _suspended_for_ota(false) { WebPrefsStore::setDefaults(_prefs); } @@ -19,6 +19,13 @@ void WebService::end() { #endif } +void WebService::suspendForOTA() { +#if defined(ESP_PLATFORM) && WITH_WEB_PANEL + _suspended_for_ota = true; + _panel.stop(); +#endif +} + void WebService::loop() { #if defined(ESP_PLATFORM) && WITH_WEB_PANEL ensureWebServer(); @@ -39,9 +46,12 @@ bool WebService::savePrefs() { bool WebService::setWebEnabled(bool enabled) { _prefs.web_enabled = enabled ? 1 : 0; + if (!enabled) { + _suspended_for_ota = false; + } bool ok = savePrefs(); #if defined(ESP_PLATFORM) && WITH_WEB_PANEL - if (_prefs.web_enabled != 0) { + if (_prefs.web_enabled != 0 && !_suspended_for_ota) { ensureWebServer(); } else { _panel.stop(); @@ -62,6 +72,11 @@ void WebService::formatWebStatusReply(char* reply, size_t reply_size) const { return; } + if (_suspended_for_ota) { + snprintf(reply, reply_size, "> web:suspended ota"); + return; + } + if (!_panel.isRunning() || _network == nullptr || !_network->isWifiConnected()) { snprintf(reply, reply_size, "> web:down"); return; @@ -76,7 +91,7 @@ void WebService::formatWebStatusReply(char* reply, size_t reply_size) const { #if defined(ESP_PLATFORM) && WITH_WEB_PANEL void WebService::ensureWebServer() { - if (_runner == nullptr || _prefs.web_enabled == 0 || _network == nullptr || !_network->isWifiConnected()) { + if (_suspended_for_ota || _runner == nullptr || _prefs.web_enabled == 0 || _network == nullptr || !_network->isWifiConnected()) { _panel.stop(); return; } diff --git a/src/helpers/web/WebService.h b/src/helpers/web/WebService.h index 0cdaebf1..4275b890 100644 --- a/src/helpers/web/WebService.h +++ b/src/helpers/web/WebService.h @@ -14,6 +14,7 @@ public: void begin(FILESYSTEM* fs); void end(); void loop(); + void suspendForOTA(); void setCommandRunner(WebPanelCommandRunner* runner); void setNetworkStateProvider(NetworkStateProvider* network) { _network = network; } @@ -38,4 +39,5 @@ private: WebPanelCommandRunner* _runner; NetworkStateProvider* _network; WebPanelServer _panel; + bool _suspended_for_ota; };