From 0175cebcd80eb28b106cd597a0d1b685748bccae Mon Sep 17 00:00:00 2001 From: Pixel Perfect Date: Mon, 3 Aug 2026 21:29:48 -0700 Subject: [PATCH] fix(tpager): enable Wi-Fi without reboot --- src/MyMesh.cpp | 25 ++- src/helpers/esp32/WifiRuntimeStore.cpp | 9 +- src/helpers/esp32/WifiRuntimeStore.h | 3 +- src/main.cpp | 132 ++++++++++----- src/ui-touch/UITask.cpp | 214 +++++++++++++++++-------- 5 files changed, 270 insertions(+), 113 deletions(-) diff --git a/src/MyMesh.cpp b/src/MyMesh.cpp index 8348efe..e2d6cf4 100644 --- a/src/MyMesh.cpp +++ b/src/MyMesh.cpp @@ -725,7 +725,7 @@ bool MyMesh::handleMeshcomodCommand(const char* text, int text_len) { } else if (wifiConfigPagerWifiBlocksBle()) { pushMeshcomodReply("OK\nble: pending (Wi-Fi handoff)"); } else if (wifiConfigGetBleEnabled()) { - pushMeshcomodReply("ble: pending (restart required)"); + pushMeshcomodReply("ble: pending (retry from Settings)"); #endif } else { pushMeshcomodReply("error: Bluetooth could not start"); @@ -932,14 +932,35 @@ bool MyMesh::handleMeshcomodCommand(const char* text, int text_len) { pushMeshcomodReply("error: wifi radio off — send wifi on first"); return true; } + if (wifiScanIsActive()) { + pushMeshcomodReply("wifi: scan already in progress"); + return true; + } // Ensure STA is fully up before scan; disconnected STA needs a bit more settle time. wifi_mode_t mode = WiFi.getMode(); +#if defined(TLORA_PAGER) + // Never cold-initialize Wi-Fi from a command handler while NimBLE is + // resident. The main loop performs the ordered live handoff; a subsequent + // command can scan once that short transition has completed. + if ((mode & WIFI_MODE_STA) == 0) { + wifiConfigRequestApply(); + pushMeshcomodReply("wifi: starting; retry scan shortly"); + return true; + } + if (wifiConfigPagerWifiBlocksBle()) { + pushMeshcomodReply("wifi: association in progress; retry scan shortly"); + return true; + } + delay(40); +#else if ((mode & WIFI_MODE_STA) == 0) { WiFi.mode(WIFI_STA); delay(180); } else { delay(40); } +#endif + wifiScanSetActive(true); s_meshcomod_scan_count = 0; // Watchdog-safe: one bounded, yielding async pass (see wifiScanWatchdogSafe). // This handler runs on the main/loop task, so the old twin synchronous scans @@ -948,6 +969,7 @@ bool MyMesh::handleMeshcomodCommand(const char* text, int text_len) { if (found <= 0) { pushMeshcomodReply("scan: no networks (2.4GHz only)"); WiFi.scanDelete(); + wifiScanSetActive(false); return true; } String scanMsg = "scan results:"; @@ -992,6 +1014,7 @@ bool MyMesh::handleMeshcomodCommand(const char* text, int text_len) { pushMeshcomodReply(scanMsg.c_str()); } WiFi.scanDelete(); + wifiScanSetActive(false); return true; } if (strncasecmp(p, "use", 3) == 0 && (p[3] == '\0' || p[3] == ' ' || p[3] == '\t')) { diff --git a/src/helpers/esp32/WifiRuntimeStore.cpp b/src/helpers/esp32/WifiRuntimeStore.cpp index 4943f2d..67180d4 100644 --- a/src/helpers/esp32/WifiRuntimeStore.cpp +++ b/src/helpers/esp32/WifiRuntimeStore.cpp @@ -207,11 +207,10 @@ void wifiScanSetActive(bool active) { s_wifi_scan_active = active; } bool wifiScanIsActive() { return s_wifi_scan_active; } void wifiConfigApply() { -#if defined(TLORA_PAGER) && defined(MULTI_TRANSPORT_COMPANION) - // The Pager must release a resident NimBLE controller before starting WPA. - // Funnel every legacy/CLI apply through main.cpp, which owns that ordering, - // the bounded handoff deadline, and BLE restoration. Calling WiFi.begin() - // directly here would bypass the coexistence state machine. +#if defined(MULTI_TRANSPORT_COMPANION) && !defined(HAS_TANMATSU) + // Multi-transport targets serialize apply and worker scans in main.cpp. The + // Pager additionally releases/recreates NimBLE around WPA. Calling + // disconnect()/begin() directly here bypasses both ownership contracts. wifiConfigRequestApply(); return; #endif diff --git a/src/helpers/esp32/WifiRuntimeStore.h b/src/helpers/esp32/WifiRuntimeStore.h index 2e80afc..2cf3462 100644 --- a/src/helpers/esp32/WifiRuntimeStore.h +++ b/src/helpers/esp32/WifiRuntimeStore.h @@ -30,7 +30,8 @@ void wifiConfigSetBleEnabled(bool enabled); #if defined(TLORA_PAGER) /* Pager coexistence ownership. Wi-Fi intent alone cannot answer whether a * cold NimBLE start is safe: touch builds keep the STA scannable even with no - * SSID. Only an active WPA association owns the ordering boundary. */ + * SSID. Cold STA allocation and active WPA association own the short ordering + * boundary; an initialized idle STA can coexist with NimBLE. */ enum class PagerWifiBlePhase : uint8_t { Idle, Associating, diff --git a/src/main.cpp b/src/main.cpp index 6de6f72..2905c34 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -105,16 +105,21 @@ static uint32_t _atoi(const char* sp) { (unsigned)heap_caps_get_free_size(MALLOC_CAP_DMA), (unsigned)heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); } - // Claim Wi-Fi's coexistence resources first. A real WPA association owns a - // bounded ordering window; idle/scannable Wi-Fi and a failed association - // both allow BLE to run. + // Claim Wi-Fi's coexistence resources first. A cold STA allocation or real + // WPA association owns a bounded ordering window; idle/scannable Wi-Fi and + // a failed association both allow BLE to run. static bool s_pager_ble_after_wifi = false; static uint32_t s_pager_wifi_ble_deadline_ms = 0; static const uint32_t PAGER_WIFI_BLE_HANDOFF_MS = 6000; static void pagerWifiEnterBleFallback(const char* reason) { WiFi.setAutoReconnect(false); - WiFi.mode(WIFI_OFF); + // Stop WPA without deinitializing esp_wifi. Keeping the STA allocation + // resident lets BLE come back immediately and avoids repeating the + // order-sensitive cold allocation for a later scan or explicit retry. + if ((WiFi.getMode() & WIFI_MODE_STA) != 0) { + WiFi.disconnect(false, false); + } wifiConfigSetPagerWifiBlePhase(PagerWifiBlePhase::BleFallback); s_pager_wifi_ble_deadline_ms = 0; Serial.printf("[wifi] BLE fallback: %s\n", reason ? reason : "association stopped"); @@ -724,8 +729,11 @@ void setup() { * contiguous DMA block, so let it claim memory before BLE. (Association * happens later in loop(); this just inits the stack.) */ if (want_wifi) { - WiFi.mode(WIFI_STA); + const bool wifi_mode_ready = WiFi.mode(WIFI_STA); #if defined(TLORA_PAGER) + if (!wifi_mode_ready) { + pagerWifiEnterBleFallback("boot STA initialization failed"); + } // Pager reconnects are owned by the loop below. Background WPA retries are // invisible to PagerWifiBlePhase and could overlap a cold NimBLE start. WiFi.setAutoReconnect(false); @@ -744,7 +752,7 @@ void setup() { * first and wait on the state transition (not a fixed delay), while * internal heap is still plentiful; BLE starts below only after the link * is established. T-Deck does not reproduce this ordering constraint. */ - if (pager_has_wifi_credentials) { + if (wifi_mode_ready && pager_has_wifi_credentials) { char ssid[WIFI_CONFIG_SSID_MAX]; char pwd[WIFI_CONFIG_PWD_MAX]; wifiConfigGetSsid(ssid, sizeof(ssid)); @@ -772,7 +780,7 @@ void setup() { (unsigned long)(millis() - assoc_start_ms)); pagerLogInternalHeap("after Wi-Fi association"); } - } else { + } else if (wifi_mode_ready) { // Keep STA available for the setup wizard's scan, but with no credentials // there is no association to order ahead of BLE. WiFi.setAutoReconnect(false); @@ -939,9 +947,6 @@ void loop() { static const uint32_t WIFI_RETRY_INTERVAL_MS = 10000; static bool wifi_radio_prev = true; static bool wifi_radio_inited = false; -#if defined(TLORA_PAGER) && defined(BLE_PIN_CODE) - static bool pager_wifi_was_connected = false; -#endif /* Run the saved Wi-Fi state machine whenever its radio preference is on. * Pager setup separately guarantees that Wi-Fi claims its coexistence * resources before a cold BLE start. */ @@ -949,8 +954,10 @@ void loop() { #if defined(TLORA_PAGER) && defined(BLE_PIN_CODE) // BLE fallback pauses automatic association so a missing AP cannot make the // advertised GATT service disappear every retry interval. Turning Bluetooth - // off releases that preference: Wi-Fi may resume its ordinary retry loop. - if (wifiConfigPagerBleFallbackActive() && !wifiConfigGetBleEnabled()) { + // off resumes retries only when the STA allocation itself is healthy; an + // allocation failure stays latched until an explicit Apply/toggle. + if (wifiConfigPagerBleFallbackActive() && !wifiConfigGetBleEnabled() && + (WiFi.getMode() & WIFI_MODE_STA) != 0) { wifiConfigSetPagerWifiBlePhase(PagerWifiBlePhase::Idle); } // An association may have started while Bluetooth was disabled. If the user @@ -973,7 +980,6 @@ void loop() { delay(50); WiFi.mode(WIFI_OFF); #if defined(TLORA_PAGER) && defined(BLE_PIN_CODE) - pager_wifi_was_connected = false; s_pager_wifi_ble_deadline_ms = 0; wifiConfigSetPagerWifiBlePhase(PagerWifiBlePhase::Idle); const bool ble_waiting = s_pager_ble_after_wifi || @@ -986,9 +992,8 @@ void loop() { if (serial_interface.isBleEnabled()) { Serial.println("[boot] deferred BLE enabled after T-Pager Wi-Fi was disabled"); } else { - Serial.println("[boot] restarting to allocate deferred BLE before the UI"); - ui_task.rebootDevice(); - return; + Serial.println("[wifi] deferred BLE unavailable after Wi-Fi was disabled"); + ui_task.showAlert(TR("Bluetooth unavailable; retry from Settings"), 2600); } } } @@ -1000,21 +1005,34 @@ void loop() { * by forcing wifi_started=false; on next iter the block below will WiFi.begin * with the freshly-saved credentials. Also handles toggling radio_en off * from the UI (the transition above already covered the on case). */ - if (wifiConfigConsumeApplyRequest()) { + bool wifi_apply_ready = true; +#if defined(ESP32) + // Keep a queued credential/radio re-apply pending until the worker releases + // its scan. Consuming it here would disconnect or reconfigure the driver + // underneath esp_wifi_scan_start on the other core. + wifi_apply_ready = !wifiScanIsActive(); +#endif + if (wifi_apply_ready && wifiConfigConsumeApplyRequest()) { #if defined(TLORA_PAGER) && defined(BLE_PIN_CODE) - // Credential changes need the same Wi-Fi-before-BLE order as boot. Release - // the resident controller without changing the saved BLE intent, apply the - // credentials live, then recreate BLE after the new link reaches GOT_IP. - if (wifi_radio_en && wifiConfigHasRuntime()) { - s_pager_ble_after_wifi = wifiConfigGetBleEnabled(); - s_pager_wifi_ble_deadline_ms = s_pager_ble_after_wifi - ? millis() + PAGER_WIFI_BLE_HANDOFF_MS : 0; - wifiConfigSetPagerWifiBlePhase(PagerWifiBlePhase::Associating); - if (serial_interface.isBleStackBegun()) { + // Cold Wi-Fi allocation and credential changes use the same live handoff: + // preserve BLE intent, release NimBLE, let the main task own esp_wifi_init + // and WPA, then recreate NimBLE once Wi-Fi is stable (or has timed out). + if (wifi_radio_en) { + const bool cold_sta = (WiFi.getMode() & WIFI_MODE_STA) == 0; + const bool ordered_handoff = cold_sta || wifiConfigHasRuntime(); + s_pager_ble_after_wifi = wifiConfigGetBleEnabled() && + (ordered_handoff || !serial_interface.isBleStackBegun()); + s_pager_wifi_ble_deadline_ms = + (wifiConfigHasRuntime() && s_pager_ble_after_wifi) + ? millis() + PAGER_WIFI_BLE_HANDOFF_MS : 0; + wifiConfigSetPagerWifiBlePhase(ordered_handoff + ? PagerWifiBlePhase::Associating : PagerWifiBlePhase::Idle); + if (ordered_handoff && serial_interface.isBleStackBegun()) { serial_interface.suspendBleForWifiReconnect(); - Serial.println("[wifi] BLE released for ordered T-Pager association"); + Serial.println("[wifi] BLE released for ordered T-Pager Wi-Fi start"); } } else { + s_pager_ble_after_wifi = false; s_pager_wifi_ble_deadline_ms = 0; wifiConfigSetPagerWifiBlePhase(PagerWifiBlePhase::Idle); } @@ -1046,9 +1064,33 @@ void loop() { #endif if (wifi_state_machine_active) { if (!wifi_started) { - wifi_started = true; - WiFi.mode(WIFI_STA); - if (wifiConfigHasRuntime()) { + const bool wifi_mode_ready = WiFi.mode(WIFI_STA); + if (!wifi_mode_ready) { +#if defined(TLORA_PAGER) && defined(BLE_PIN_CODE) + const bool ble_waiting = s_pager_ble_after_wifi || + (wifiConfigGetBleEnabled() && + !serial_interface.isBleStackBegun()); + s_pager_ble_after_wifi = false; + last_wifi_retry_ms = millis(); + pagerWifiEnterBleFallback("STA initialization failed; Bluetooth restored"); + if (ble_waiting && wifiConfigGetBleEnabled() && + !serial_interface.isBleEnabled()) { + serial_interface.enableBle(); + } + if (wifiConfigGetBleEnabled() && serial_interface.isBleEnabled()) { + ui_task.showAlert(TR("Wi-Fi unavailable; Bluetooth restored"), 2200); + } else if (wifiConfigGetBleEnabled()) { + ui_task.showAlert(TR("Wi-Fi and Bluetooth unavailable; retry from Settings"), 2800); + } else { + ui_task.showAlert(TR("Wi-Fi unavailable; retry from Settings"), 2200); + } +#else + last_wifi_retry_ms = millis(); +#endif + } else { + wifi_started = true; + } + if (wifi_mode_ready && wifiConfigHasRuntime() && !wifiScanIsActive()) { char ssid[WIFI_CONFIG_SSID_MAX]; char pwd[WIFI_CONFIG_PWD_MAX]; wifiConfigGetSsid(ssid, sizeof(ssid)); @@ -1061,8 +1103,23 @@ void loop() { } last_wifi_retry_ms = millis(); #if defined(TLORA_PAGER) && defined(BLE_PIN_CODE) - } else { + } else if (wifi_mode_ready) { wifiConfigSetPagerWifiBlePhase(PagerWifiBlePhase::Idle); + s_pager_wifi_ble_deadline_ms = 0; + const bool ble_waiting = s_pager_ble_after_wifi || + (wifiConfigGetBleEnabled() && + !serial_interface.isBleStackBegun()); + s_pager_ble_after_wifi = false; + if (ble_waiting && wifiConfigGetBleEnabled() && + !serial_interface.isBleEnabled()) { + serial_interface.enableBle(); + if (serial_interface.isBleEnabled()) { + Serial.println("[wifi] BLE restored after idle STA initialization"); + } else { + Serial.println("[wifi] BLE unavailable after idle STA initialization"); + ui_task.showAlert(TR("Bluetooth unavailable; retry from Settings"), 2600); + } + } #endif } } @@ -1109,12 +1166,11 @@ void loop() { #if defined(TLORA_PAGER) && defined(BLE_PIN_CODE) wifiConfigSetPagerWifiBlePhase(PagerWifiBlePhase::Connected); s_pager_wifi_ble_deadline_ms = 0; - const bool became_connected = !pager_wifi_was_connected; - pager_wifi_was_connected = true; - const bool ble_waiting = s_pager_ble_after_wifi || - (wifiConfigGetBleEnabled() && - !serial_interface.isBleStackBegun()); - if (became_connected && ble_waiting) { + // Consume only the explicit handoff token. If the cold NimBLE recreate + // is refused, do not hammer the allocator and alert on every loop while + // Wi-Fi remains connected; Settings is the deliberate retry surface. + const bool ble_waiting = s_pager_ble_after_wifi; + if (ble_waiting) { s_pager_ble_after_wifi = false; if (wifiConfigGetBleEnabled() && !serial_interface.isBleEnabled()) { // Wi-Fi is now stable, so a cold BLE allocation cannot enter WPA in @@ -1164,7 +1220,6 @@ void loop() { } } else { #if defined(TLORA_PAGER) && defined(BLE_PIN_CODE) - pager_wifi_was_connected = false; if (wifiConfigGetPagerWifiBlePhase() == PagerWifiBlePhase::Connected) wifiConfigSetPagerWifiBlePhase(PagerWifiBlePhase::Idle); #endif @@ -1178,7 +1233,6 @@ void loop() { if (s_pager_ble_after_wifi && wifiConfigGetBleEnabled() && WiFi.status() != WL_CONNECTED) { s_pager_ble_after_wifi = false; - pager_wifi_was_connected = false; wifi_started = false; pagerWifiEnterBleFallback("association timed out; Bluetooth restored"); serial_interface.enableBle(); diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index 45a0767..3de1025 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -4964,12 +4964,17 @@ static char s_wifiscan_ssids[kWifiScanMax][WIFI_CONFIG_SSID_MAX]; static volatile int s_wifiscan_count = 0; static volatile bool s_wifiscan_request = false; // UI -> worker: scan now static volatile bool s_wifiscan_done = false; // worker -> UI: results ready +#if defined(TLORA_PAGER) +static bool s_wifiscan_wait_for_radio = false; // main task must cold-start STA first +static uint32_t s_wifiscan_pager_guard_ms = 0; // recover if worker never services request +#endif // Scan-while-connected handshake (S3): the radio can't sweep reliably while // associated, so the MAIN task briefly drops the link before the worker scans and // rejoins after. All WiFi state changes happen on the main task (never the worker). static volatile bool s_wifiscan_drop_req = false; // UI -> main: drop link, then scan static bool s_wifiscan_reconnect_after = false; // main: rejoin once results land static uint32_t s_wifiscan_drop_ms = 0; // main: disassoc-settle timer +static uint32_t s_wifiscan_guard_ms = 0; // main: cancel only an unclaimed scan // DEBUG: last wifiScanWatchdogSafe outcome (so the main-task list rebuild can log it). static volatile int16_t s_swd_kick = -9; static volatile int16_t s_swd_st = -9; @@ -13429,6 +13434,38 @@ static void openWifiScanPopup() { // results instantly (if any) while the rescan lands. Shared by the Settings // Wi-Fi page, the setup wizard's Scan button, and the wizard's auto-scan on // arrival. Caller must have already confirmed wifiConfigWantsWifi() (radio up). +static void wifiQueueScanWhenReady() { + if (wifiScanIsActive() || + __atomic_load_n(&s_wifiscan_request, __ATOMIC_ACQUIRE) || + s_wifiscan_drop_req || s_wifiscan_drop_ms != 0 +#if defined(TLORA_PAGER) + || s_wifiscan_wait_for_radio +#endif + ) return; + ensureTileFetchTaskRunning(); // the worker idles out after 5 s — respawn it +#if defined(TLORA_PAGER) + wifiScanSetActive(true); // suppress main-loop reconnects for the whole sweep + s_wifiscan_pager_guard_ms = millis(); + s_wifiscan_reconnect_after = false; + if ((WiFi.getMode() & WIFI_MODE_STA) == 0 || wifiConfigPagerWifiBlocksBle()) { + // The worker must never call esp_wifi_init. Ask the main-loop handoff to + // initialize STA, and release the scan only after the ordering phase ends. + s_wifiscan_wait_for_radio = true; + if ((WiFi.getMode() & WIFI_MODE_STA) == 0) { + s_wifiscan_reconnect_after = wifiConfigHasRuntime(); + wifiConfigRequestApply(); + } + return; + } +#elif defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION) && !defined(HAS_TANMATSU) + wifiScanSetActive(true); + s_wifiscan_guard_ms = millis(); + s_wifiscan_reconnect_after = + wifiConfigHasRuntime() && WiFi.status() != WL_CONNECTED; +#endif + __atomic_store_n(&s_wifiscan_request, true, __ATOMIC_RELEASE); +} + static void wifiScanOpenAndKick() { hideKb(); // unbind the keyboard mirror so it can't later revert the picked SSID openWifiScanPopup(); @@ -13440,16 +13477,14 @@ static void wifiScanOpenAndKick() { lv_obj_set_style_text_color(l, lv_color_hex(COLOR_SUB), LV_PART_MAIN); lv_obj_set_style_text_font(l, &g_font_14, LV_PART_MAIN); } - ensureTileFetchTaskRunning(); // the worker idles out after 5 s — respawn it - s_wifiscan_request = true; + wifiQueueScanWhenReady(); } // Mirror of the BLE-enable guard: bringing esp_wifi up needs ~50 KB free internal // heap, and with BLE already holding its share on a tight board the init fails -// DEEP in the Wi-Fi state machine (main.cpp) where nothing reports it. Keep the -// decision pure so every caller can distinguish a Pager's ordered restart from -// a real low-memory refusal. -enum class WifiEnableGate : uint8_t { Ready, RestartRequired, LowMemory }; +// DEEP in the Wi-Fi state machine (main.cpp) where nothing reports it. Pager +// uses the live main-loop BLE handoff; the preflight remains for other boards. +enum class WifiEnableGate : uint8_t { Ready, LowMemory }; static WifiEnableGate wifiEnableGate() { #if defined(ESP32) @@ -13457,12 +13492,9 @@ static WifiEnableGate wifiEnableGate() { // one-time DMA allocation. Apply the reserve only to a cold start. if (WiFi.getMode() != WIFI_MODE_NULL) return WifiEnableGate::Ready; #if defined(TLORA_PAGER) - // Pager coexistence is reliable only when setup claims Wi-Fi before NimBLE - // and before LVGL. Always route a genuinely cold Wi-Fi start through that - // ordering, even if the current heap happens to look large enough. No heap - // check here on purpose: the restart is unconditional, so there is no - // LowMemory outcome to fall through to on this board. - return WifiEnableGate::RestartRequired; + // The main loop releases NimBLE, allocates Wi-Fi, then recreates NimBLE live. + // Let that path make the authoritative allocation attempt and report failure. + return WifiEnableGate::Ready; #else const uint32_t internal_caps = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT; const size_t freeh = heap_caps_get_free_size(internal_caps); @@ -13486,39 +13518,19 @@ static bool wifiPrepareEnable(lv_obj_t* switch_to_revert = nullptr) { g_lv.task->showAlert(TR("Not enough free memory for Wi-Fi. Turn Bluetooth off first."), 2200); return false; } - - // Persist the requested state, paint an honest status, then reboot through - // the normal flush path so setup can claim Wi-Fi before NimBLE and LVGL. - wifiConfigSetRadioEnabled(true); - if (g_lv.task) { - g_lv.task->showAlert( -#if defined(TLORA_PAGER) - wifiConfigGetBleEnabled() - ? TR("Restarting for Wi-Fi; Bluetooth resumes after it connects") - : TR("Restarting to enable Wi-Fi"), -#else - TR("Restarting to enable Wi-Fi"), -#endif - 1400); - lv_refr_now(NULL); - g_lv.task->rebootDevice(); - } else { - touchPrefsFlush(); - ESP.restart(); - } - return false; + return true; } // "Scan" button -> open the popup + queue a scan on the core-0 worker. If Wi-Fi -// is off, prepare the radio first (a cold Pager needs the ordered restart above; -// other live stacks can continue immediately), then let the worker list networks. +// is off, prepare the radio first, then let the main loop initialize it before +// the worker lists networks. static void wifiScanStartCb(lv_event_t* e) { if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; #if defined(MULTI_TRANSPORT_COMPANION) if (!wifiConfigWantsWifi()) { if (!wifiPrepareEnable()) return; wifiConfigSetRadioEnabled(true); // wantsWifi() now true -> the scan worker can bring STA up - wifiConfigRequestApply(); // main loop applies live or takes Pager's ordered restart + wifiConfigRequestApply(); // main loop applies the live Pager handoff if (g_lv.task) g_lv.task->showAlert(TR("Wi-Fi on, scanning\xE2\x80\xA6"), 1200); } wifiScanOpenAndKick(); @@ -13535,14 +13547,17 @@ static void wifiRebuildNetworkList(); // fwd: reworked Wi-Fi page list (define // best-effort while associated and never turns a page-open into a reboot. static void wifiKickScan() { #if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION) + if (wifiScanIsActive() || + __atomic_load_n(&s_wifiscan_request, __ATOMIC_ACQUIRE) || + s_wifiscan_drop_req || s_wifiscan_drop_ms != 0) return; ensureTileFetchTaskRunning(); #if defined(HAS_TANMATSU) - s_wifiscan_request = true; // esp-hosted C6 sweeps fine while associated + __atomic_store_n(&s_wifiscan_request, true, __ATOMIC_RELEASE); // esp-hosted C6 sweeps fine while associated #elif defined(TLORA_PAGER) - s_wifiscan_request = true; // never force WPA re-auth under resident NimBLE + wifiQueueScanWhenReady(); // main task owns any cold STA allocation #else if (WiFi.status() == WL_CONNECTED) s_wifiscan_drop_req = true; - else s_wifiscan_request = true; + else wifiQueueScanWhenReady(); #endif #endif } @@ -13552,9 +13567,6 @@ static void wifiKickScan() { // here (sequential with main.cpp's wifi loop on the same task — no worker race, and // never eraseap, which wedges this radio). Drop the link, let the disassoc settle, // then kick the worker; wifiScanService rejoins once results land. -// Wall-clock deadline for the scan gate below (0 = not armed). See the recovery -// branch in wifiScanMainService for why this exists. -static uint32_t s_wifiscan_guard_ms = 0; static void wifiScanMainService() { if (s_wifiscan_drop_req) { s_wifiscan_drop_req = false; @@ -13566,11 +13578,11 @@ static void wifiScanMainService() { s_wifiscan_guard_ms = millis(); // arm the stuck-gate deadline (recovery below) s_wifiscan_reconnect_after = true; } else { - s_wifiscan_request = true; // already disconnected — scan now + wifiQueueScanWhenReady(); // already disconnected — establish scan ownership now } } else if (s_wifiscan_drop_ms && (uint32_t)(millis() - s_wifiscan_drop_ms) >= 400) { s_wifiscan_drop_ms = 0; - s_wifiscan_request = true; // disassoc settled -> run the sweep + __atomic_store_n(&s_wifiscan_request, true, __ATOMIC_RELEASE); // disassoc settled -> run the sweep } // RECOVERY: opening the Wi-Fi page drops the link and raises the scan gate, which is // normally lowered in wifiScanService() once the worker reports s_wifiscan_done. But @@ -13579,20 +13591,29 @@ static void wifiScanMainService() { // long tile/LOS/OTA job. If it never reports, the gate LATCHES: autoreconnect is off // and main.cpp's 10 s reconnect retry stays suppressed (main.cpp gates it on // !wifiScanIsActive()), so Wi-Fi never comes back until a reboot — a plausible source - // of "connects on a fresh flash, never reconnects after a reboot" (issue #171). The - // deadline is deliberately far longer than any legitimate sweep (the worker's own scan - // path allows several seconds plus retries) so this can only fire when the normal - // completion path is genuinely lost, never mid-scan. + // of "connects on a fresh flash, never reconnects after a reboot" (issue #171). + // Only cancel a request the worker has not claimed. Once the worker atomically + // consumes the request, its bounded scan owns the gate until normal completion; + // clearing it on a wall-clock guess would race esp_wifi_scan_start. if (s_wifiscan_guard_ms && wifiScanIsActive() && - (uint32_t)(millis() - s_wifiscan_guard_ms) >= 30000UL) { + (uint32_t)(millis() - s_wifiscan_guard_ms) >= 60000UL) { s_wifiscan_guard_ms = 0; - s_wifiscan_reconnect_after = false; - s_wifiscan_drop_ms = 0; - wifiScanSetActive(false); // un-gate main.cpp's reconnect retry + bool cancelled = false; + if (s_wifiscan_drop_ms != 0) { + s_wifiscan_drop_ms = 0; + cancelled = true; + } + if (__atomic_exchange_n(&s_wifiscan_request, false, __ATOMIC_ACQ_REL)) + cancelled = true; + if (cancelled) { + const bool reconnect = s_wifiscan_reconnect_after; + s_wifiscan_reconnect_after = false; + wifiScanSetActive(false); // un-gate main.cpp's reconnect retry #if !defined(TLORA_PAGER) - WiFi.setAutoReconnect(true); + WiFi.setAutoReconnect(true); #endif - wifiConfigRequestApply(); // re-begin with the stored cred (main task) + if (reconnect) wifiConfigRequestApply(); + } } } #endif @@ -13601,6 +13622,36 @@ static void wifiScanMainService() { static void wifiScanService() { #if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION) && !defined(HAS_TANMATSU) wifiScanMainService(); // drive the drop-link-then-scan handshake (main task) +#endif +#if defined(TLORA_PAGER) && defined(MULTI_TRANSPORT_COMPANION) + if (s_wifiscan_wait_for_radio) { + const bool sta_ready = (WiFi.getMode() & WIFI_MODE_STA) != 0; + if (!wifiConfigWantsWifi()) { + s_wifiscan_wait_for_radio = false; + s_wifiscan_reconnect_after = false; + s_wifiscan_done = true; + } else if (sta_ready && !wifiConfigPagerWifiBlocksBle()) { + s_wifiscan_wait_for_radio = false; + __atomic_store_n(&s_wifiscan_request, true, __ATOMIC_RELEASE); + } else if (!sta_ready && wifiConfigPagerBleFallbackActive()) { + // main.cpp already reported the failed allocation and restored BLE. + s_wifiscan_wait_for_radio = false; + s_wifiscan_reconnect_after = false; + s_wifiscan_done = true; + } + } + // Cancel only a wait or queue entry the worker has not claimed. If the worker + // already atomically consumed the request, normal completion alone releases + // the scan gate; a timeout must never race its esp_wifi_scan_start call. + if (s_wifiscan_pager_guard_ms && wifiScanIsActive() && + (uint32_t)(millis() - s_wifiscan_pager_guard_ms) >= 60000UL) { + s_wifiscan_pager_guard_ms = 0; + bool cancelled = s_wifiscan_wait_for_radio; + s_wifiscan_wait_for_radio = false; + if (__atomic_exchange_n(&s_wifiscan_request, false, __ATOMIC_ACQ_REL)) + cancelled = true; + if (cancelled) s_wifiscan_done = true; + } #endif if (!s_wifiscan_done) return; s_wifiscan_done = false; @@ -13608,22 +13659,32 @@ static void wifiScanService() { #if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION) wifiRebuildNetworkList(); // refresh the reworked Wi-Fi page (guarded: no-op unless it's open) #if !defined(HAS_TANMATSU) - if (s_wifiscan_reconnect_after) { // we dropped the link for this sweep — rejoin now +#if defined(TLORA_PAGER) + const bool reconnect_after_scan = s_wifiscan_reconnect_after; + s_wifiscan_reconnect_after = false; + s_wifiscan_pager_guard_ms = 0; + wifiScanSetActive(false); + if (reconnect_after_scan) wifiConfigRequestApply(); +#else + if (wifiScanIsActive()) { + const bool reconnect_after_scan = s_wifiscan_reconnect_after; s_wifiscan_reconnect_after = false; s_wifiscan_guard_ms = 0; // normal completion — disarm the stuck-gate deadline wifiScanSetActive(false); #if !defined(TLORA_PAGER) WiFi.setAutoReconnect(true); #endif - wifiConfigRequestApply(); // main.cpp re-begins with the stored cred (main task) + if (reconnect_after_scan) + wifiConfigRequestApply(); // main.cpp re-begins with the stored cred (main task) } #endif #endif +#endif } // Wi-Fi radio toggle on the Wi-Fi settings page (mirrors the control-center -// toggle). Existing stacks apply live; a cold Pager routes through the ordered -// restart above. No separate Save action is needed. +// toggle). Existing stacks apply live; a cold Pager uses the main-loop BLE +// handoff above. No separate Save action is needed. static void wifiRadioToggleCb(lv_event_t* e) { if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return; #if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION) @@ -13889,7 +13950,12 @@ static void wifiHiddenRowCb(lv_event_t* e) { static void wifiRefreshRowCb(lv_event_t* e) { if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; if (!wifiConfigWantsWifi()) { if (g_lv.task) g_lv.task->showAlert(TR("Turn Wi-Fi on first"), 1200); return; } - if (s_wifiscan_request || s_wifiscan_drop_req || s_wifiscan_drop_ms) return; // a scan is already queued/running + if (wifiScanIsActive() || s_wifiscan_request || + s_wifiscan_drop_req || s_wifiscan_drop_ms +#if defined(TLORA_PAGER) + || s_wifiscan_wait_for_radio +#endif + ) return; // a scan is already queued/running wifiKickScan(); // connected -> main task drops link first wifiRebuildNetworkList(); // re-render so the row shows "Scanning…" } @@ -13945,7 +14011,12 @@ static void wifiRebuildNetworkList() { const bool connected = (WiFi.status() == WL_CONNECTED); if (connected) strlcpy(cur, WiFi.SSID().c_str(), sizeof cur); // "Scanning…" spans the whole cycle: the main-task drop window AND the worker sweep. - const bool scanning_now = (s_wifiscan_request || s_wifiscan_drop_req || s_wifiscan_drop_ms != 0); + const bool scanning_now = (wifiScanIsActive() || s_wifiscan_request || + s_wifiscan_drop_req || s_wifiscan_drop_ms != 0 +#if defined(TLORA_PAGER) + || s_wifiscan_wait_for_radio +#endif + ); // Saved networks, most-recent first. TouchWifiNet nets[TOUCH_WIFI_NET_COUNT]; @@ -25108,16 +25179,26 @@ static void tileFetchTaskFn(void* arg) { #endif // Wi-Fi scan (user-initiated from the Network tab / setup wizard). Blocking // here on the Wi-Fi core, not the UI thread. Mirrors the CLI `wifi scan`. - if (s_wifiscan_request) { - s_wifiscan_request = false; + if (__atomic_exchange_n(&s_wifiscan_request, false, __ATOMIC_ACQ_REL)) { s_wifiscan_count = 0; // Never bring the Wi-Fi driver up from here when BLE owns the radio (fresh device // on BLE => Bluedroid holds the internal heap). WiFi.mode(STA)/esp_wifi_init would // then OOM-panic (BLE-vs-Wi-Fi mutex, see main.cpp). wantsWifi() mirrors the boot // transport choice: true only when Wi-Fi is the active transport. if (!wifiConfigWantsWifi()) { s_wifiscan_done = true; continue; } +#if defined(TLORA_PAGER) + // Cold STA allocation is order-sensitive with NimBLE on the Pager and is + // owned exclusively by main.cpp. If the driver disappeared after the UI + // released this request, fail the sweep instead of initializing it here. + if ((WiFi.getMode() & WIFI_MODE_STA) == 0 || wifiConfigPagerWifiBlocksBle()) { + s_wifiscan_done = true; + continue; + } + vTaskDelay(pdMS_TO_TICKS(40)); +#else if ((WiFi.getMode() & WIFI_MODE_STA) == 0) { WiFi.mode(WIFI_STA); vTaskDelay(pdMS_TO_TICKS(180)); } else { vTaskDelay(pdMS_TO_TICKS(40)); } +#endif // One bounded, yielding async pass — the stable beta_19 scan. NEVER touch WiFi // state (disconnect/begin/eraseap) from this worker: it races main.cpp's wifi // state machine and wedges the radio (a hard lesson). Scanning while associated @@ -36215,14 +36296,13 @@ static void openControlCenter(); // fwd — toggle cbs rebuild the panel // (no touch), so a long-press of the green ○ Home key triggers this (see navPump). static void toggleControlCenter() { if (s_cc_root) closeControlCenter(); else openControlCenter(); } -// Wi-Fi and Bluetooth coexist once allocated. Resident stacks toggle live; -// Pager cold starts may use the ordered reboot path; re-authentication releases -// and recreates BLE live. Each requested state persists independently. +// Wi-Fi and Bluetooth coexist once allocated. Pager cold starts and +// re-authentication release and recreate BLE live; no reboot is required. static void ccWifiCb(lv_event_t* e) { if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; #if defined(ESP32) - // Existing stacks apply live in the main loop; a cold Pager takes the ordered - // restart path so Wi-Fi claims its allocations before NimBLE and LVGL. + // Existing stacks apply live in the main loop; a cold Pager temporarily + // releases NimBLE so Wi-Fi can claim its allocation first. const bool on = wifiConfigGetRadioEnabled(); if (!on && !wifiPrepareEnable()) { openControlCenter(); return; } wifiConfigSetRadioEnabled(!on);