fix(tpager): stabilize Wi-Fi and BLE coexistence

Signed-off-by: Pixel Perfect <me@pixp.cc>
This commit is contained in:
Pixel Perfect
2026-08-05 17:56:10 -07:00
parent ad6446b312
commit cadfa35cb5
6 changed files with 279 additions and 92 deletions
+14 -4
View File
@@ -40,9 +40,19 @@ extern "C" void* lvglPsramRealloc(void* ptr, size_t size) {
if (!ptr) {
return lvglPsramAlloc(size);
}
// Plain realloc preserves whatever heap the original block was on (PSRAM
// stays in PSRAM, DRAM stays in DRAM). Migrating between heaps on realloc
// would need to know the original size to memcpy safely; not worth the
// complexity for LVGL's usage pattern.
#if defined(ESP32)
// ESP-IDF documents plain realloc(ptr, size) as
// heap_caps_realloc(ptr, size, MALLOC_CAP_8BIT). That generic capability can
// move a PSRAM-backed LVGL block into the higher-priority internal heap. The
// UI performs many small text/style reallocations while building its tree,
// so the old implementation silently migrated roughly 60 KB back into the
// DRAM needed by Wi-Fi/BLE connection and security work. Preserve the PSRAM
// requirement explicitly. heap_caps_realloc handles copying even when ptr
// came from the internal fallback in lvglPsramAlloc().
void* p = heap_caps_realloc(ptr, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (p) return p;
#endif
// PSRAM absent or exhausted: retain the original fallback semantics. A failed
// heap_caps_realloc leaves ptr valid, so the ordinary realloc remains safe.
return realloc(ptr, size);
}
@@ -2,6 +2,7 @@
#include <helpers/RepeaterTcpOtaEmit.h>
#include "WifiRuntimeStore.h" // persist BLE on/off (ble_en) across reboots
#include "WebMirror.h" // web UI mirror bridge (served over the WS server)
#include <esp_heap_caps.h>
#include <string.h>
// Companion push code for the per-packet RX log (matches MyMesh.cpp). It is kept OFF
@@ -14,7 +15,7 @@ bool MultiTransportCompanionInterface::s_ble_rxlog_once = false;
MultiTransportCompanionInterface::MultiTransportCompanionInterface()
: _tcp_port(0), _ws_port(0), _tcp_started(false), _ws_started(false), _tcp_enabled(true), _isEnabled(false), _broadcast(false), _last_reply_target(REPLY_TARGET_USB), _ota_tcp_suspended(false), _ota_ws_suspended(false), _ota_ws_listen_paused(false)
#ifdef BLE_PIN_CODE
, _ble_begun(false), _ble_enabled(false), _ota_ble_released(false), _ble_pin_code(0)
, _ble_begun(false), _ble_enabled(false), _ota_ble_released(false), _ota_ble_was_enabled(false), _ble_pin_code(0)
#endif
{
for (size_t i = 0; i < sizeof(_client_ids) / sizeof(_client_ids[0]); i++)
@@ -183,13 +184,15 @@ void MultiTransportCompanionInterface::prepareBle(const char* prefix, char* name
_ble_pin_code = pin_code;
}
void MultiTransportCompanionInterface::beginBle(const char* prefix, char* name, uint32_t pin_code) {
void MultiTransportCompanionInterface::beginBle(const char* prefix, char* name, uint32_t pin_code,
bool create_enabled) {
prepareBle(prefix, name, pin_code);
_ble.begin(prefix, name, pin_code);
_ble_begun = true;
_ble_enabled = true;
_ble_enabled = create_enabled;
_ota_ble_released = false;
_ble.enable();
_ota_ble_was_enabled = false;
if (create_enabled) _ble.enable();
}
void MultiTransportCompanionInterface::enableBle() {
@@ -197,6 +200,31 @@ void MultiTransportCompanionInterface::enableBle() {
// Deferred at boot (heap guard) or toggled on from off: bring the stack up
// now, live, from the params stashed by prepareBle()/beginBle().
if (_ble_prefix[0] == '\0' && _ble_name[0] == '\0') return; // no params known
#if defined(TLORA_PAGER)
// The Pager must claim Wi-Fi before NimBLE. A cold BLE start after Wi-Fi
// exists can both violate that ordering and consume the last contiguous
// internal block after LVGL is built. Refuse here; UITask persists the
// request and reboots through setup's proven Wi-Fi -> BLE -> UI sequence.
if (WiFi.getMode() != WIFI_MODE_NULL) {
Serial.println("[ble] cold start deferred to ordered T-Pager reboot");
return;
}
#endif
// Only a cold NimBLE start needs the coexistence reserve. Re-enabling an
// already-created stack below this threshold is allocation-free and must
// not be rejected (UITask used to gate both cases identically, trapping the
// user with BLE resident-but-off and neither radio enableable).
const size_t BLE_COEXIST_MIN_FREE = 50u * 1024u;
const size_t BLE_COEXIST_MIN_BLOCK = 20u * 1024u;
const uint32_t internal_caps = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
const size_t internal_free = heap_caps_get_free_size(internal_caps);
const size_t internal_max = heap_caps_get_largest_free_block(internal_caps);
if (internal_free < BLE_COEXIST_MIN_FREE ||
internal_max < BLE_COEXIST_MIN_BLOCK) {
Serial.printf("[ble] cold start refused: free=%u maxblk=%u\n",
(unsigned)internal_free, (unsigned)internal_max);
return;
}
char name[sizeof(_ble_name)];
strncpy(name, _ble_name, sizeof(name) - 1);
name[sizeof(name) - 1] = '\0';
@@ -211,11 +239,18 @@ void MultiTransportCompanionInterface::enableBle() {
void MultiTransportCompanionInterface::disableBle() {
_ble_enabled = false;
wifiConfigSetBleEnabled(false); // persist so BT stays off across reboot
_ble.disable(); // stop advertising + drop any connection
// NOTE: we deliberately do NOT NimBLEDevice::deinit() here. Tearing the BT
// controller down while Wi-Fi+BLE coexistence is active crashes — the esp_coex
// layer still holds a reference to the controller — so "off" stops advertising
// but keeps the NimBLE host resident. Its RAM is only fully reclaimed on reboot.
// deinit(true) below deletes NimBLE's server, but SerialBLEInterface keeps
// its cached server pointer. Never call disable() again after that teardown;
// a later begin() replaces the cached pointers with a fresh GATT server.
if (_ble_begun) _ble.disable(); // stop advertising + drop any connection
// If Wi-Fi is genuinely absent, fully release NimBLE so a later Wi-Fi enable
// has the contiguous internal heap esp_wifi_init needs. While Wi-Fi exists we
// keep the controller resident: tearing it out from under an active coex path
// is unsafe, and re-enabling the already-created stack needs no allocation.
if (_ble_begun && WiFi.getMode() == WIFI_MODE_NULL) {
NimBLEDevice::deinit(true);
_ble_begun = false;
}
}
bool MultiTransportCompanionInterface::getBlePeerAddress(char* buf, size_t len) const {
@@ -241,7 +276,9 @@ void MultiTransportCompanionInterface::disable() {
_isEnabled = false;
_usb.disable();
#ifdef BLE_PIN_CODE
_ble.disable();
// A prior disableBle() may have fully deinitialised NimBLE and left the
// wrapped SerialBLEInterface's cached server pointer dangling.
if (_ble_begun) _ble.disable();
#endif
}
@@ -288,8 +325,9 @@ void MultiTransportCompanionInterface::prepareForHttpOta() {
}
#ifdef BLE_PIN_CODE
if (_ble_begun && _ble_enabled) {
_ble.disable();
if (_ble_begun) {
_ota_ble_was_enabled = _ble_enabled;
if (_ble_enabled) _ble.disable();
NimBLEDevice::deinit(true);
_ble_begun = false;
_ble_enabled = false;
@@ -320,9 +358,10 @@ void MultiTransportCompanionInterface::restoreAfterHttpOta() {
ble_name[sizeof(ble_name) - 1] = '\0';
_ble.begin(_ble_prefix, ble_name, _ble_pin_code);
_ble_begun = true;
_ble_enabled = true;
_ble.enable();
_ble_enabled = _ota_ble_was_enabled;
if (_ble_enabled) _ble.enable();
_ota_ble_released = false;
_ota_ble_was_enabled = false;
meshcoreRepeaterTcpOtaEmitLine("OTA: restored BLE stack");
}
#endif
@@ -31,7 +31,11 @@ public:
#ifdef BLE_PIN_CODE
// Call after begin() and the_mesh is ready (e.g. after startInterface). Enables BLE by default.
void beginBle(const char* prefix, char* name, uint32_t pin_code);
// create_enabled=false pre-creates the NimBLE host/GATT table but leaves
// advertising off and the user-visible/persisted state disabled. T-Pager
// uses this after Wi-Fi claims its heap so a later live toggle allocates
// nothing after the UI working set has filled internal DRAM.
void beginBle(const char* prefix, char* name, uint32_t pin_code, bool create_enabled = true);
// Store the BLE name/pin WITHOUT bringing the stack up. Used at boot when the
// heap guard defers co-initialising BLE alongside Wi-Fi: the params are kept so
// a later enableBle() can lazily bring BLE up live (no reboot).
@@ -135,6 +139,7 @@ private:
bool _ble_begun; // beginBle() was called
bool _ble_enabled; // user has BLE on (toggle via UI)
bool _ota_ble_released;
bool _ota_ble_was_enabled;
char _ble_prefix[24];
char _ble_name[48];
uint32_t _ble_pin_code;
+110 -18
View File
@@ -65,7 +65,11 @@ static uint32_t _atoi(const char* sp) {
#ifdef ESP32
#ifdef MULTI_TRANSPORT_COMPANION
#include <helpers/esp32/MultiTransportCompanionInterface.h>
// This class is extended locally (web mirror/P4 routing/BLE state). Include
// the matching project header explicitly: the MeshCore dependency ships an
// older class layout, and mixing that header with our local .cpp makes the
// placement allocation undersized and shifts every member after _ws_started.
#include "helpers/esp32/MultiTransportCompanionInterface.h"
#include "helpers/esp32/MqttBridge.h"
#include <esp_heap_caps.h>
#include <new>
@@ -88,6 +92,23 @@ static uint32_t _atoi(const char* sp) {
#ifndef WS_PORT
#define WS_PORT 8765
#endif
#if defined(TLORA_PAGER)
static void pagerLogInternalHeap(const char* phase) {
const uint32_t caps = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
Serial.printf("[heap] %s: internal free=%u largest=%u low=%u dma=%u psram=%u\n",
phase,
(unsigned)heap_caps_get_free_size(caps),
(unsigned)heap_caps_get_largest_free_block(caps),
(unsigned)heap_caps_get_minimum_free_size(caps),
(unsigned)heap_caps_get_free_size(MALLOC_CAP_DMA),
(unsigned)heap_caps_get_free_size(MALLOC_CAP_SPIRAM));
}
// On a fresh profile there may be no saved credentials yet.
// Claim Wi-Fi's coexistence resources first, then start the prepared BLE
// stack as soon as that first association succeeds.
static bool s_pager_ble_after_wifi = false;
#endif
#elif defined(WIFI_SSID)
#include <helpers/esp32/SerialWifiInterface.h>
SerialWifiInterface serial_interface;
@@ -677,16 +698,13 @@ void setup() {
serial_interface.begin(Serial, TCP_PORT, WS_PORT);
Serial.println("[BOOT] serial_interface ok");
serial_interface.setBroadcastResponses(true); // RX log, channel messages, etc. go to all clients (USB + TCP + WS [+ BLE]), not only last sender
/* Pick BLE vs WiFi at boot. The ESP32-S3 doesn't have enough internal heap
* (esp_wifi_init needs ~50KB for DMA buffers) to run Bluedroid BLE +
* LVGL/TFT + WiFi all at once — esp_wifi_init silently returns ESP_ERR_NO_MEM,
* leaving WiFi.getMode() at WIFI_MODE_NULL. So we mutex them: if the user
* has saved WiFi credentials AND the radio is enabled, skip BLE init and
* use WiFi exclusively. Otherwise init BLE. Toggle by saving/clearing creds
* + reboot (saveWifiCb auto-restarts). On the touch build the user can also
* pick Wi-Fi with no creds yet (to scan/configure on-device) — wantsWifi()
* returns true for that case so the radio comes up scannable. */
/* Wi-Fi and NimBLE coexist, but their first allocations are order-sensitive
* on the Pager. Claim Wi-Fi's DMA/coexistence resources before starting BLE;
* T-Deck does not reproduce this sequencing constraint. */
bool want_wifi = wifiConfigWantsWifi();
#if defined(TLORA_PAGER)
bool pager_wifi_ready_for_ble = false;
#endif
/* Wi-Fi + BLE now COEXIST (NimBLE host is light enough — the old Bluedroid
* heap clash is gone). Bring Wi-Fi up FIRST: esp_wifi_init grabs a big
* contiguous DMA block, so let it claim memory before BLE. (Association
@@ -701,6 +719,32 @@ void setup() {
// setup wizard, no creds yet) DTIM modem-sleep naps the radio through the
// scan dwell, so WiFi.scanNetworks() comes back empty ("no networks found").
// It's enabled once we actually associate — see the GOT_IP handler below.
#if defined(TLORA_PAGER)
/* Arduino-ESP32 2.0.17 can watchdog in WPA3 SAE on the Pager when NimBLE
* already owns the coexistence path. Start the saved Wi-Fi association
* 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 (wifiConfigHasRuntime()) {
char ssid[WIFI_CONFIG_SSID_MAX];
char pwd[WIFI_CONFIG_PWD_MAX];
wifiConfigGetSsid(ssid, sizeof(ssid));
wifiConfigGetPwd(pwd, sizeof(pwd));
if (ssid[0]) {
const uint32_t assoc_start_ms = millis();
WiFi.begin(ssid, pwd[0] ? pwd : nullptr);
while (WiFi.status() != WL_CONNECTED &&
(uint32_t)(millis() - assoc_start_ms) < 12000UL) {
delay(20);
}
pager_wifi_ready_for_ble = WiFi.status() == WL_CONNECTED;
Serial.printf("[boot] Pager Wi-Fi pre-BLE association: %s (%lums)\n",
pager_wifi_ready_for_ble ? "ready" : "deferred",
(unsigned long)(millis() - assoc_start_ms));
pagerLogInternalHeap("after Wi-Fi association");
}
}
#endif
}
#if defined(BLE_PIN_CODE)
/* Always stash the BLE params so the toggle can bring BLE up live later, even
@@ -715,6 +759,35 @@ void setup() {
{ NodePrefs* _np = the_mesh.getNodePrefs();
_np->node_name[sizeof(_np->node_name) - 1] = '\0'; }
serial_interface.prepareBle(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
#if defined(TLORA_PAGER)
{
const bool want_ble = wifiConfigGetBleEnabled();
/* Serialise the Pager's first association before BLE init. Once associated,
* the two radios coexist normally. When Wi-Fi is enabled, pre-create the
* stack even if the saved BLE state is off: late allocation after LVGL has
* built the home UI can no longer find a large enough internal block, while
* a pre-created disabled stack can start advertising allocation-free. */
if (!want_wifi || pager_wifi_ready_for_ble || !wifiConfigHasRuntime()) {
if (want_ble || want_wifi) {
serial_interface.beginBle(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name,
the_mesh.getBLEPin(), want_ble);
Serial.printf("[boot] BLE stack ready (enabled=%d wifi=%d)\n",
(int)want_ble, (int)want_wifi);
pagerLogInternalHeap("after BLE init");
}
} else {
// Wi-Fi owns its required heap now, but a saved association did not
// complete. Still allocate the NimBLE/GATT objects before LVGL consumes
// and fragments the remaining internal RAM; leave advertising disabled
// until Wi-Fi connects (or the user explicitly enables BLE live).
serial_interface.beginBle(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name,
the_mesh.getBLEPin(), false);
s_pager_ble_after_wifi = want_ble;
Serial.printf("[boot] BLE stack ready disabled; deferred enable=%d\n", (int)want_ble);
pagerLogInternalHeap("after deferred BLE init");
}
}
#else
if (wifiConfigGetBleEnabled()) {
const size_t BLE_COEXIST_MIN_FREE = 50 * 1024; // free heap after Wi-Fi to also start BLE
const size_t BLE_COEXIST_MIN_BLOCK = 20 * 1024; // largest contiguous block (NimBLE controller/host)
@@ -728,6 +801,7 @@ void setup() {
}
}
#endif
#endif
#elif defined(WIFI_SSID)
board.setInhibitSleep(true); // prevent sleep when WiFi is active
WiFi.setAutoReconnect(true);
@@ -798,6 +872,9 @@ void setup() {
#ifdef DISPLAY_CLASS
ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved
Serial.println("[BOOT] ui ready");
#if defined(TLORA_PAGER) && defined(MULTI_TRANSPORT_COMPANION)
pagerLogInternalHeap("after UI init");
#endif
#endif
board.onBootComplete();
@@ -826,12 +903,9 @@ void loop() {
static const uint32_t WIFI_RETRY_INTERVAL_MS = 10000;
static bool wifi_radio_prev = true;
static bool wifi_radio_inited = false;
/* BLE-vs-WiFi mutex (chosen at setup based on saved creds + radio_en pref):
* if BLE was initialized, do NOT attempt to bring WiFi up here — esp_wifi_init
* would fail with ESP_ERR_NO_MEM after Bluedroid grabbed the internal heap,
* and the resulting OOM cascade freezes LVGL. Only run the WiFi state
* machine if creds are saved AND the radio pref is on, mirroring `want_wifi`
* in setup(). (Touch may also want Wi-Fi up with no creds, to scan.) */
/* 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. */
bool wifi_radio_en = wifiConfigWantsWifi();
if (!wifi_radio_inited) {
wifi_radio_inited = true;
@@ -842,6 +916,15 @@ void loop() {
WiFi.disconnect(true);
delay(50);
WiFi.mode(WIFI_OFF);
#if defined(TLORA_PAGER) && defined(BLE_PIN_CODE)
if (s_pager_ble_after_wifi) {
s_pager_ble_after_wifi = false;
if (wifiConfigGetBleEnabled() && !serial_interface.isBleEnabled()) {
serial_interface.enableBle();
Serial.println("[boot] deferred BLE enabled after T-Pager Wi-Fi was disabled");
}
}
#endif
}
wifi_started = false;
}
@@ -877,10 +960,10 @@ void loop() {
char pwd[WIFI_CONFIG_PWD_MAX];
wifiConfigGetSsid(ssid, sizeof(ssid));
wifiConfigGetPwd(pwd, sizeof(pwd));
if (strlen(ssid) > 0) {
if (strlen(ssid) > 0 && WiFi.status() != WL_CONNECTED) {
WiFi.begin(ssid, pwd[0] ? pwd : nullptr);
last_wifi_retry_ms = millis();
}
last_wifi_retry_ms = millis();
}
}
// Automatic WiFi recovery for TCP mode: retry connection periodically if link drops.
@@ -909,6 +992,15 @@ void loop() {
static bool sntp_pushed = false;
static uint32_t sntp_kick_ms = 0;
if (WiFi.status() == WL_CONNECTED) {
#if defined(TLORA_PAGER) && defined(BLE_PIN_CODE)
if (s_pager_ble_after_wifi) {
s_pager_ble_after_wifi = false;
if (wifiConfigGetBleEnabled() && !serial_interface.isBleEnabled()) {
serial_interface.enableBle();
Serial.println("[boot] deferred BLE enabled after T-Pager Wi-Fi association");
}
}
#endif
// Now that we're associated, enable DTIM modem-sleep (saves power + gives
// BLE coexistence airtime). Deferred to here on purpose: enabling it on the
// unassociated STA naps the radio through a scan dwell and breaks the setup
+93 -50
View File
@@ -1993,11 +1993,11 @@ static bool discoveredSweepHops() {
static bool g_cap_touch_hw_started = false;
// ---- LVGL draw buffer ----
// 240x24 RGB565 = 11,520 bytes. Allocated in PSRAM at UITask::begin() so the
// internal DRAM stays free for WiFi DMA buffers (esp_wifi_init needs ~50 KB
// of DRAM; with the buffer + LVGL widgets in DRAM the device was OOM'ing as
// soon as WiFi came up). PSRAM is slower than DRAM but the Adafruit ST7789
// SPI driver reads the buffer linearly which the cache handles well.
// 240x24 RGB565 = 11,520 bytes. The T-Pager keeps this in PSRAM so Wi-Fi +
// NimBLE connection/security allocations retain enough contiguous internal
// DRAM. Its ST7796 flush is synchronous TFT_eSPI::pushColors (no DMA), so the
// external-RAM source is valid. Other boards retain the faster internal-DMA
// allocation below unless it fails.
//
// 1.5x the original 240x16 size — modest bump to reduce setAddrWindow
// round-trips without giving LVGL more headroom to over-invalidate.
@@ -12987,15 +12987,15 @@ static void showConfirm(const char* msg, const char* ok_label, SimpleCb on_confi
}
// ----- Bluetooth settings page -----
// Wi-Fi + BLE coexist now (NimBLE's host is light enough to share the ESP32-S3
// internal heap with esp_wifi + LVGL), so this is a plain LIVE toggle of the BLE
// radio — no reboot, and Wi-Fi is left untouched. State is persisted (ble_en).
// Wi-Fi + BLE coexist once both stacks are allocated. Re-enabling a resident
// stack is live; a cold Pager allocation restarts through setup's Wi-Fi-first
// ordering. State is persisted (ble_en), and Wi-Fi is left enabled.
// The pairing code is editable here (persisted to _prefs.ble_pin; applied at the
// next boot, since the passkey is baked into serial_interface.begin()).
static lv_obj_t* s_ble_pin_ta = nullptr; // editable 6-digit pairing-code field on the BLE page
#if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION)
// Bluetooth enable switch: instant toggle (BLE on/off is live enableBle() lazily brings
// NimBLE up; no Save button, no reboot).
// Bluetooth enable switch: resident BLE toggles live; enableBle() handles the
// Pager's ordered-restart fallback for a cold stack. No Save button is needed.
static void bleEnableSwitchCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED || !g_lv.task) return;
if (!g_lv.task->hasBleCapability()) { g_lv.task->showAlert(TR("No Bluetooth on this device"), 1400); return; }
@@ -13400,28 +13400,66 @@ static void wifiScanOpenAndKick() {
// 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 the UI
// toasted "Wi-Fi on" while the radio never came up. Refuse at the toggle instead,
// symmetrically with UITask::enableBle(). Thresholds match the boot co-init guard.
static bool wifiEnableGuardOk() {
// 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 };
static WifiEnableGate wifiEnableGate() {
#if defined(ESP32)
return ESP.getFreeHeap() >= 50u * 1024u && ESP.getMaxAllocHeap() >= 20u * 1024u;
// Once esp_wifi is initialized, a live off/on toggle does not need its large
// 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.
return WifiEnableGate::RestartRequired;
#else
return true;
const uint32_t internal_caps = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
const size_t freeh = heap_caps_get_free_size(internal_caps);
const size_t maxblk = heap_caps_get_largest_free_block(internal_caps);
if (freeh >= 50u * 1024u && maxblk >= 20u * 1024u) return WifiEnableGate::Ready;
Serial.printf("[wifi] cold start refused: free=%u maxblk=%u\n",
(unsigned)freeh, (unsigned)maxblk);
#endif
return WifiEnableGate::LowMemory;
#else
return WifiEnableGate::Ready;
#endif
}
static bool wifiPrepareEnable(lv_obj_t* switch_to_revert = nullptr) {
const WifiEnableGate gate = wifiEnableGate();
if (gate == WifiEnableGate::Ready) return true;
if (gate == WifiEnableGate::LowMemory) {
if (switch_to_revert) lv_obj_clear_state(switch_to_revert, LV_STATE_CHECKED);
if (g_lv.task)
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(TR("Restarting to enable Wi-Fi"), 800);
lv_refr_now(NULL);
g_lv.task->rebootDevice();
} else {
ESP.restart();
}
return false;
}
// "Scan" button -> open the popup + queue a scan on the core-0 worker. If Wi-Fi
// is off (BLE active), bring the radio up LIVE first — it coexists with NimBLE,
// no reboot — then scan; the worker brings STA up and lists networks.
// 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.
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 (!wifiEnableGuardOk()) {
if (g_lv.task) g_lv.task->showAlert(TR("Not enough free memory for Wi-Fi. Turn Bluetooth off first."), 2200);
return;
}
if (!wifiPrepareEnable()) return;
wifiConfigSetRadioEnabled(true); // wantsWifi() now true -> the scan worker can bring STA up
wifiConfigRequestApply(); // main loop brings esp_wifi up live (no reboot)
if (g_lv.task) g_lv.task->showAlert(TR("Wi-Fi on, scanning\xE2\x80\xA6"), 1200);
@@ -13518,18 +13556,14 @@ static void wifiScanService() {
#endif
}
// Live Wi-Fi radio toggle on the Wi-Fi settings page (mirrors the control-center
// toggle). wifiConfigSetRadioEnabled persists the pref + flags an apply, so the
// main loop brings esp_wifi up / down on the spot — no Save needed, no reboot.
// 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.
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)
const bool on = lv_obj_has_state(lv_event_get_target(e), LV_STATE_CHECKED);
if (on && !wifiEnableGuardOk()) {
lv_obj_clear_state(lv_event_get_target(e), LV_STATE_CHECKED); // revert the switch
if (g_lv.task) g_lv.task->showAlert(TR("Not enough free memory for Wi-Fi. Turn Bluetooth off first."), 2200);
return;
}
if (on && !wifiPrepareEnable(lv_event_get_target(e))) return;
wifiConfigSetRadioEnabled(on);
if (g_lv.task) g_lv.task->showAlert(on ? TR("Wi-Fi on") : TR("Wi-Fi off"), 800);
refreshStatusLabels();
@@ -36122,14 +36156,10 @@ static void toggleControlCenter() { if (s_cc_root) closeControlCenter(); else op
static void ccWifiCb(lv_event_t* e) {
if (lv_event_get_code(e) != LV_EVENT_CLICKED) return;
#if defined(ESP32)
// Live: the main loop brings esp_wifi up (WiFi.mode/begin) or down (WIFI_OFF)
// in response to this pref — no reboot.
// 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.
const bool on = wifiConfigGetRadioEnabled();
if (!on && !wifiEnableGuardOk()) {
if (g_lv.task) g_lv.task->showAlert(TR("Not enough free memory for Wi-Fi. Turn Bluetooth off first."), 2200);
openControlCenter();
return;
}
if (!on && !wifiPrepareEnable()) return;
wifiConfigSetRadioEnabled(!on);
if (g_lv.task) g_lv.task->showAlert(on ? TR("Wi-Fi off") : TR("Wi-Fi on"), 800);
openControlCenter();
@@ -46148,17 +46178,25 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
const int draw_band_w = 240;
#endif
const size_t buf_bytes = sizeof(lv_color_t) * draw_band_w * LV_DRAW_BUF_LINES;
// Internal DMA-capable DRAM — this is the hot loop's read source
// during SPI flush. PSRAM (~80 MHz QSPI) is ~3× slower than
// internal SRAM. INTERNAL|DMA also makes it eligible for SPI DMA
// transfers if the display driver ever grows them. Fall back to
// PSRAM if internal DRAM is too fragmented at boot.
// Internal DMA-capable DRAM — this is the hot loop's read source during
// SPI flush. The T-Pager is deliberately the exception: ST7796LCDDisplay
// uses synchronous pushColors (not DMA), while BLE needs this contiguous
// internal block later when a client connects and negotiates security.
#if defined(TLORA_PAGER)
g_draw_buffer = (lv_color_t*)heap_caps_malloc(
buf_bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (!g_draw_buffer) {
g_draw_buffer = (lv_color_t*)heap_caps_malloc(
buf_bytes, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
}
#else
g_draw_buffer = (lv_color_t*)heap_caps_malloc(
buf_bytes, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT);
if (!g_draw_buffer) {
g_draw_buffer = (lv_color_t*)heap_caps_malloc(
buf_bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
}
#endif
if (!g_draw_buffer) g_draw_buffer = (lv_color_t*)malloc(buf_bytes);
// Last-ditch under severe DRAM pressure — e.g. a unit whose PSRAM didn't
// init (some T-Deck clones are QSPI, not the expected OPI), so even SPIRAM
@@ -47549,15 +47587,20 @@ static bool uiHistWaitWorkerIdle() {
bool UITask::enableBle() {
if (!_serial) return false;
#if defined(ESP32)
// Same thresholds as the boot-time co-init guard (main.cpp) — keep in sync.
const size_t BLE_COEXIST_MIN_FREE = 50u * 1024u;
const size_t BLE_COEXIST_MIN_BLOCK = 20u * 1024u;
if (ESP.getFreeHeap() < BLE_COEXIST_MIN_FREE || ESP.getMaxAllocHeap() < BLE_COEXIST_MIN_BLOCK)
return false;
#endif
_serial->enableBle();
return true;
// The concrete transport applies the heap guard only when it must cold-start
// NimBLE. A resident disabled stack can always be re-enabled allocation-free.
if (_serial->isBleEnabled()) return true;
#if defined(TLORA_PAGER)
// Same cold-start contract as Wi-Fi above. This is not an OOM failure the
// user can repair by toggling the other radio: remember the requested state
// and allocate NimBLE during the next ordered boot, before LVGL fragments
// internal DRAM. rebootDevice() flushes pending history before ESP.restart().
wifiConfigSetBleEnabled(true);
showAlert("Restarting to enable Bluetooth", 800);
rebootDevice();
#endif
return false;
}
void UITask::persistHistoryNow() {
+3 -5
View File
@@ -491,11 +491,9 @@ public:
void disableTcp() { if (_serial) _serial->disableTcp(); }
bool hasBleCapability() const { return _serial && _serial->hasBleCapability(); }
bool isBleEnabled() const { return _serial && _serial->isBleEnabled(); }
// Live BLE enable, guarded like the boot co-init in main.cpp: NimBLE needs
// ~50 KB free internal heap + a 20 KB contiguous block, and starting it below
// that does not fail cleanly — it panics mid-init (the "reboots when I turn
// BLE on with Wi-Fi running" report). Returns false when refused; the caller
// shows the reason and reverts its switch. Defined in UITask.cpp.
// Live BLE enable. The concrete transport applies its heap guard only for a
// cold NimBLE allocation; re-enabling a pre-created stack is allocation-free.
// Returns false when a required cold start cannot be made safely.
bool enableBle();
void disableBle() { if (_serial) _serial->disableBle(); }
int getWsConnectedCount() const { return _serial ? _serial->getWsConnectedCount() : 0; }