From 4861d50c21cb3cfcc7daa86dfe677597f704ae11 Mon Sep 17 00:00:00 2001 From: hkuehl <4090175+hkuehl@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:15:13 +0000 Subject: [PATCH] fix: restart from WebUI no longer needs the browser tab to stay open A restart requested from the WebUI (Restart button, firmware upload, firmware update from URL, settings or customization upload) is set to PENDING_RESTART and was only escalated to RESTART_REQUESTED when the WebUI's SystemMonitor polled /rest/systemStatus. If the tab was closed, nothing ever escalated it and the generic 5 minute status timeout reset it back to NORMAL, so the restart was dropped and an uploaded firmware was never booted. - System::loop() now escalates a pending restart itself if it hasn't been picked up within 10 seconds (the SystemMonitor polls every second) - the generic status reset timeout skips PENDING_RESTART, and its timer is cleared whenever the status is back to normal, so a start time left over from an earlier status can't reset a later one (e.g. a pending restart right after a firmware download) immediately --- CHANGELOG_LATEST.md | 1 + src/core/emsesp.cpp | 8 +++++--- src/core/system.cpp | 14 ++++++++++++++ src/core/system.h | 5 +++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index c27c7992c..889399c07 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -32,6 +32,7 @@ This release is based on the latest Espressif/Arduino core version 3. It brings ## Fixed +- restart from the WebUI (Restart button, firmware/settings upload) only happened while the browser tab stayed open. If the tab was closed the restart was silently dropped after 5 minutes, leaving a new firmware uninstalled. EMS-ESP now restarts on its own if the WebUI hasn't picked up the pending restart within 10 seconds - shunting yard show json - `system/sendmail` called from a Command or the Scheduler computed its value twice, which stripped the quotes from literal text and then failed on characters like `!`. Also shunting yard treated a `?` inside a quoted string as a ternary, so text like `"too hot?"` returned nothing - memory leak when using different timezones for ems-esp and thermostat[#3184](https://github.com/emsesp/EMS-ESP32/issues/3184) diff --git a/src/core/emsesp.cpp b/src/core/emsesp.cpp index 977b3c2b9..02a00fc8a 100644 --- a/src/core/emsesp.cpp +++ b/src/core/emsesp.cpp @@ -2110,9 +2110,11 @@ void EMSESP::loop() { } } - // reset status after a timeout - if (EMSESP::system_.systemStatus() != SYSTEM_STATUS::SYSTEM_STATUS_NORMAL) { - static uint32_t starttime = 0; + // reset status after a timeout. A pending restart is never reset, it's handled in System::loop() + static uint32_t starttime = 0; + if (EMSESP::system_.systemStatus() == SYSTEM_STATUS::SYSTEM_STATUS_NORMAL || EMSESP::system_.systemStatus() == SYSTEM_STATUS::SYSTEM_STATUS_PENDING_RESTART) { + starttime = 0; + } else { if (starttime == 0) { starttime = uuid::get_uptime_ms(); } else if (uuid::get_uptime_ms() - starttime > SYSTEM_STATUS_RESET_TIMEOUT) { diff --git a/src/core/system.cpp b/src/core/system.cpp index d880cc9ef..9e5c9d4e3 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -1025,6 +1025,20 @@ void System::uart_init() { // checks system health and handles LED flashing wizardry // returns true if the LED flash is active bool System::loop() { + // a restart requested from the WebUI is first set to pending, so the WebUI's SystemMonitor can pick it up + // and show the restart screen before the connection drops. Don't depend on the browser tab staying open though: + // if nothing has polled the status after a short grace period, go ahead and restart anyway + if (systemStatus() == SYSTEM_STATUS::SYSTEM_STATUS_PENDING_RESTART) { + if (pending_restart_start_ == 0) { + pending_restart_start_ = uuid::get_uptime_ms(); + } else if (uuid::get_uptime_ms() - pending_restart_start_ > PENDING_RESTART_TIMEOUT) { + LOG_INFO("No WebUI response to pending restart, restarting now"); + systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_RESTART_REQUESTED); + } + } else { + pending_restart_start_ = 0; + } + // check if we're supposed to do a reset/restart if (systemStatus() == SYSTEM_STATUS::SYSTEM_STATUS_RESTART_REQUESTED) { system_restart(); diff --git a/src/core/system.h b/src/core/system.h index af83e3324..8b434169a 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -391,6 +391,11 @@ class System { volatile uint8_t systemStatus_; // uses SYSTEM_STATUS enum - written from the AsyncTCP task (e.g. cancel) and read from the main loop during OTA + uint32_t pending_restart_start_ = 0; // uptime (ms) when a pending restart was first seen in the main loop + // restart anyway if a pending restart hasn't been picked up by the WebUI within this time (in ms) + // the SystemMonitor polls every second, so this only kicks in when the browser tab was closed or lost connection + static constexpr uint32_t PENDING_RESTART_TIMEOUT = 10000; + void set_partition_install_date(); volatile bool recovery_installed_pending_ = false; // set by the main loop, handled on the AsyncTCP task