From fe7bc1c40f1a5dc46cb4fdfae3324f1dffbeed8e Mon Sep 17 00:00:00 2001 From: proddy Date: Tue, 20 Oct 2026 13:06:26 +0200 Subject: [PATCH] fix SoftAP fallback to keep retrying WiFi STA after cold boot --- CHANGELOG_LATEST.md | 2 ++ interface/src/i18n/cs/index.ts | 2 +- interface/src/i18n/de/index.ts | 2 +- interface/src/i18n/en/index.ts | 2 +- interface/src/i18n/fr/index.ts | 2 +- interface/src/i18n/it/index.ts | 2 +- interface/src/i18n/nl/index.ts | 2 +- interface/src/i18n/no/index.ts | 2 +- interface/src/i18n/pl/index.ts | 2 +- interface/src/i18n/sk/index.ts | 2 +- interface/src/i18n/sv/index.ts | 2 +- interface/src/i18n/tr/index.ts | 2 +- src/core/network.cpp | 22 ++++++++++++++++++---- 13 files changed, 31 insertions(+), 15 deletions(-) diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index e067f33be..579593296 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -68,11 +68,13 @@ This release is based on the latest Espressif/Arduino core version 3. It brings - a read or write aimed at EMS-ESP's own bus id was left unanswered, so the sender waited for a timeout. EMS-ESP now sends an empty reply to those reads and acknowledges those writes [#3094](https://github.com/emsesp/EMS-ESP32/issues/3094) - a second boiler monitor `0xE4` carrying `0xFFFF` cleared a valid `pc0Flow`. That empty value is now ignored [#3224](https://github.com/emsesp/EMS-ESP32/issues/3224) - writing a negative custom entity value was always 1 too high because float-to-int rounding always added +0.5. Negative values now round the same way as positive ones [#3297](https://github.com/emsesp/EMS-ESP32/issues/3297) +- after a cold boot, SoftAP fallback no longer abandons STA forever. If a home WiFi SSID is configured, EMS-ESP keeps retrying STA while the captive portal is up so a late-booting router (e.g. after a power outage) can still be joined; SoftAP stops once STA gets an IP ## Changed - various memory optimizations [#3083](https://github.com/emsesp/EMS-ESP32/issues/3083) - Scheduler name is now mandatory +- Access Point provision mode label renamed from "When network connection is lost" to "When network connection not found" (all languages) - network fallback to AP only after start [#3090](https://github.com/emsesp/EMS-ESP32/issues/3090) - replaced Web async-validator with custom validator and toast with native snackbar to reduce bundle size - Gateway and Connect devices are shown in the Devices page, but disabled [3126](https://github.com/emsesp/EMS-ESP32/discussions/3126) diff --git a/interface/src/i18n/cs/index.ts b/interface/src/i18n/cs/index.ts index 4d0759776..cf6587738 100644 --- a/interface/src/i18n/cs/index.ts +++ b/interface/src/i18n/cs/index.ts @@ -238,7 +238,7 @@ const cs: Translation = { TIME_ZONE: 'Časová zóna', ACCESS_POINT: 'Přístupový bod', AP_PROVIDE: 'Povolit přístupový bod', - AP_PROVIDE_TEXT_2: 'Když je síťové připojení stratené', + AP_PROVIDE_TEXT_2: 'Když není síťové připojení nalezeno', AP_PROVIDE_TEXT_3: 'Nikdy', AP_PREFERRED_CHANNEL: 'Preferovaný kanál', AP_HIDE_SSID: 'Skrýt SSID', diff --git a/interface/src/i18n/de/index.ts b/interface/src/i18n/de/index.ts index debe19b16..ded292e53 100644 --- a/interface/src/i18n/de/index.ts +++ b/interface/src/i18n/de/index.ts @@ -238,7 +238,7 @@ const de: Translation = { TIME_ZONE: 'Zeitzone', ACCESS_POINT: 'Zugangspunkt', AP_PROVIDE: 'Aktiviere Zugangspunkt', - AP_PROVIDE_TEXT_2: 'Wenn Netzwerkverbindung verloren geht', + AP_PROVIDE_TEXT_2: 'Wenn keine Netzwerkverbindung gefunden wird', AP_PROVIDE_TEXT_3: 'Niemals', AP_PREFERRED_CHANNEL: 'Bevorzugter Kanal', AP_HIDE_SSID: 'Verstecke SSID', diff --git a/interface/src/i18n/en/index.ts b/interface/src/i18n/en/index.ts index a93db6062..221ee7198 100644 --- a/interface/src/i18n/en/index.ts +++ b/interface/src/i18n/en/index.ts @@ -238,7 +238,7 @@ const en: Translation = { TIME_ZONE: 'Time Zone', ACCESS_POINT: 'Access Point', AP_PROVIDE: 'Enable Access Point', - AP_PROVIDE_TEXT_2: 'When network connection is lost', + AP_PROVIDE_TEXT_2: 'When network connection not found', AP_PROVIDE_TEXT_3: 'Never', AP_PREFERRED_CHANNEL: 'Preferred Channel', AP_HIDE_SSID: 'Hide SSID', diff --git a/interface/src/i18n/fr/index.ts b/interface/src/i18n/fr/index.ts index 90326795d..4c2e03a78 100644 --- a/interface/src/i18n/fr/index.ts +++ b/interface/src/i18n/fr/index.ts @@ -238,7 +238,7 @@ const fr: Translation = { TIME_ZONE: 'Fuseau horaire', ACCESS_POINT: "Point d'accès", AP_PROVIDE: "Activer le Point d'Accès", - AP_PROVIDE_TEXT_2: 'quand la connexion réseau est perdue', + AP_PROVIDE_TEXT_2: 'quand la connexion réseau n\'est pas trouvée', AP_PROVIDE_TEXT_3: 'jamais', AP_PREFERRED_CHANNEL: 'Canal préféré', AP_HIDE_SSID: 'Cacher le SSID', diff --git a/interface/src/i18n/it/index.ts b/interface/src/i18n/it/index.ts index 8a0c994ca..3f9038ac4 100644 --- a/interface/src/i18n/it/index.ts +++ b/interface/src/i18n/it/index.ts @@ -238,7 +238,7 @@ const it: Translation = { TIME_ZONE: 'Fuso orario', ACCESS_POINT: 'Access Point', AP_PROVIDE: 'Abilita Access Point', - AP_PROVIDE_TEXT_2: 'quando la connessione di rete è persa', + AP_PROVIDE_TEXT_2: 'quando la connessione di rete non è trovata', AP_PROVIDE_TEXT_3: 'mai', AP_PREFERRED_CHANNEL: 'Canale preferito', AP_HIDE_SSID: 'Nascondi SSID', diff --git a/interface/src/i18n/nl/index.ts b/interface/src/i18n/nl/index.ts index 8ac4e9fd6..7dfbace3b 100644 --- a/interface/src/i18n/nl/index.ts +++ b/interface/src/i18n/nl/index.ts @@ -238,7 +238,7 @@ const nl: Translation = { TIME_ZONE: 'Tijdzone', ACCESS_POINT: 'Access Point', AP_PROVIDE: 'Activeer Access Point', - AP_PROVIDE_TEXT_2: 'als netwerk verbinding verloren gaat', + AP_PROVIDE_TEXT_2: 'als netwerkverbinding niet gevonden wordt', AP_PROVIDE_TEXT_3: 'nooit', AP_PREFERRED_CHANNEL: 'Voorkeurskanaal', AP_HIDE_SSID: 'SSID verbergen', diff --git a/interface/src/i18n/no/index.ts b/interface/src/i18n/no/index.ts index 2d9b8549e..2e4a4bacc 100644 --- a/interface/src/i18n/no/index.ts +++ b/interface/src/i18n/no/index.ts @@ -238,7 +238,7 @@ const no: Translation = { TIME_ZONE: 'Tidssone', ACCESS_POINT: 'Aksesspunkt', AP_PROVIDE: 'Aktiver Aksesspunkt', - AP_PROVIDE_TEXT_2: 'når nettverksforbindelsen er utilgjengelig', + AP_PROVIDE_TEXT_2: 'når nettverksforbindelsen ikke finnes', AP_PROVIDE_TEXT_3: 'aldri', AP_PREFERRED_CHANNEL: 'Foretrukket kanal', AP_HIDE_SSID: 'Skjul SSID', diff --git a/interface/src/i18n/pl/index.ts b/interface/src/i18n/pl/index.ts index 7730bc0f1..9b49912a4 100644 --- a/interface/src/i18n/pl/index.ts +++ b/interface/src/i18n/pl/index.ts @@ -238,7 +238,7 @@ const pl: BaseTranslation = { TIME_ZONE: 'Strefa czasowa', ACCESS_POINT: '{{Punkt|punktu|}} {{dostępowy|dostępowego|}}', AP_PROVIDE: 'Punkt dostępowy', - AP_PROVIDE_TEXT_2: 'aktywny jeśli brak połączenia z siecią', + AP_PROVIDE_TEXT_2: 'gdy nie znaleziono połączenia z siecią', AP_PROVIDE_TEXT_3: 'nieaktywny', AP_PREFERRED_CHANNEL: 'Preferowany kanał', AP_HIDE_SSID: 'Ukryj SSID', diff --git a/interface/src/i18n/sk/index.ts b/interface/src/i18n/sk/index.ts index aee30c490..33269fb21 100644 --- a/interface/src/i18n/sk/index.ts +++ b/interface/src/i18n/sk/index.ts @@ -238,7 +238,7 @@ const sk: Translation = { TIME_ZONE: 'Časová zóna', ACCESS_POINT: 'Prístupový bod', AP_PROVIDE: 'Povoliť prístupový bod', - AP_PROVIDE_TEXT_2: 'keď je sieťové pripojenie stratené', + AP_PROVIDE_TEXT_2: 'keď sieťové pripojenie nie je nájdené', AP_PROVIDE_TEXT_3: 'nikdy', AP_PREFERRED_CHANNEL: 'Preferovaný kanál', AP_HIDE_SSID: 'Skryť SSID', diff --git a/interface/src/i18n/sv/index.ts b/interface/src/i18n/sv/index.ts index ef0b0444b..46e50a506 100644 --- a/interface/src/i18n/sv/index.ts +++ b/interface/src/i18n/sv/index.ts @@ -238,7 +238,7 @@ const sv: Translation = { TIME_ZONE: 'Tidszon', ACCESS_POINT: 'Accesspunkt', AP_PROVIDE: 'Aktivera accesspunkt', - AP_PROVIDE_TEXT_2: 'när nätverksanslutningen är bortkopplad', + AP_PROVIDE_TEXT_2: 'när nätverksanslutningen inte hittas', AP_PROVIDE_TEXT_3: 'aldrig', AP_PREFERRED_CHANNEL: 'Kanal', AP_HIDE_SSID: 'Göm SSID', diff --git a/interface/src/i18n/tr/index.ts b/interface/src/i18n/tr/index.ts index 308523439..a04154046 100644 --- a/interface/src/i18n/tr/index.ts +++ b/interface/src/i18n/tr/index.ts @@ -238,7 +238,7 @@ const tr: Translation = { TIME_ZONE: 'Saat dilimi', ACCESS_POINT: 'Erişim Noktası', AP_PROVIDE: 'Erişim noktasını çalıştır', - AP_PROVIDE_TEXT_2: 'Ağ bağlantısı kesildiğinde', + AP_PROVIDE_TEXT_2: 'Ağ bağlantısı bulunamadığında', AP_PROVIDE_TEXT_3: 'asla', AP_PREFERRED_CHANNEL: 'Tercih edilen kanal', AP_HIDE_SSID: 'SSID yi gizle', diff --git a/src/core/network.cpp b/src/core/network.cpp index eb58ee7f6..d5babaa1b 100644 --- a/src/core/network.cpp +++ b/src/core/network.cpp @@ -125,6 +125,10 @@ void Network::begin() { wifi_connect_pending_ = false; if (wifi_ever_connected_) { LOG_WARNING("WiFi disconnected (reason: %s)", disconnectReason(last_disconnect_reason_)); + } else if (phase_ == NetPhase::AP) { + // SoftAP fallback is up and we're periodically retrying STA (slow router after + // power restore) — don't flood the log with WARNING on every failed attempt + LOG_DEBUG("WiFi STA retry while AP active failed (reason: %s)", disconnectReason(last_disconnect_reason_)); } else { LOG_WARNING("WiFi initial connect attempt failed (reason: %s)", disconnectReason(last_disconnect_reason_)); } @@ -663,13 +667,17 @@ void Network::stopWiFiRadio() { // WiFi management void Network::startWIFI() { #ifndef EMSESP_STANDALONE - // only run during the WIFI phase; ETHERNET phase keeps WiFi quiet, AP phase has its own bring-up - if (phase_ != NetPhase::WIFI) { + // WIFI phase: normal STA connect. AP phase with a configured SSID: keep retrying STA while + // SoftAP is up so a late-booting router (e.g. after a power outage) can still be joined — + // findNetworks() stops SoftAP once STA gets an IP. ETHERNET phase keeps WiFi quiet. + const bool ap_fallback_retry = (phase_ == NetPhase::AP && !ssid_.isEmpty()); + if (phase_ != NetPhase::WIFI && !ap_fallback_retry) { return; } - // the radio was powered down while Ethernet was up, so bring it back before trying to associate - if (wifi_radio_off_) { + // the radio was powered down while Ethernet was up, so bring it back before trying to associate. + // Don't call this during SoftAP fallback — startWiFiRadio() forces WIFI_STA and would kill the AP. + if (wifi_radio_off_ && !ap_fallback_retry) { startWiFiRadio(); } @@ -678,6 +686,11 @@ void Network::startWIFI() { return; } + // SoftAP is serving the captive portal — keep it and enable STA alongside + if (ap_fallback_retry && (WiFi.getMode() & WIFI_AP)) { + WiFi.mode(WIFI_AP_STA); + } + wifi_connect_pending_ = true; // attempt to connect to the wifi network @@ -969,6 +982,7 @@ void Network::findNetworks() { : "AP"); // give up on this interface and try the next one. ETHERNET -> WIFI (or straight to AP if no SSID configured); WIFI -> AP. + // Once in AP phase with a configured SSID, startWIFI() keeps retrying STA (WIFI_AP_STA) so a late router can still be joined. // AP_MODE_NEVER: never promote to the AP phase — keep waiting on the current interface. if (connect_retry_ >= MAX_NETWORK_RECONNECTION_ATTEMPTS && phase_ != NetPhase::AP) { if (phase_ == NetPhase::ETHERNET) {