From 793685e76b0df75aa2759f9c5a56fa353e3fb9fd Mon Sep 17 00:00:00 2001 From: Nick <2506116+k5njm@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:09:28 -0500 Subject: [PATCH] feat: user path to resume BT from low-memory pause via reader menu toggle Field session: after a recovery teardown the heap settles at ~72 KB -- below the 100 KB start floor -- and never recovers on its own (the defrag silent-restart only fires when a build FAILS, and builds succeed now). BT stayed paused forever, the menu toggle claimed ON, and toggling off/on changed nothing. - Menu label tells the truth: ON / PAUSED (enabled but stack down) / OFF (new STR_STATE_PAUSED string) - Toggling BT on below the heap floor silent-restarts into the current book: the fresh boot's ~118 KB passes the gate and BT auto-starts on resume. Explicit user intent is the right trigger for the defrag. - Hoist the floor to bleinput::kStartMinFreeHeap, shared by the lifecycle gate and the toggle --- lib/I18n/translations/english.yaml | 2 ++ src/BleInput.h | 6 ++++++ .../reader/EpubReaderMenuActivity.cpp | 20 +++++++++++++++++-- src/main.cpp | 10 +++++----- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 38ad2652..a5f4949c 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -299,6 +299,8 @@ STR_BT_NO_PAIRED: "No paired devices" STR_BT_MAP_BUTTONS: "Map Remote Buttons" STR_BT_PRESS_REMOTE: "Press a button on your remote" STR_BT_CONNECTING_POPUP: "BT Connecting..." +STR_BT_PAUSED_LOW_MEM_POPUP: "BT paused (low memory)" +STR_STATE_PAUSED: "PAUSED" STR_BT_PAGE_FORWARD: "Page Forward" STR_BT_PAGE_BACK: "Page Back" STR_BT_FORGET_PROMPT: "Hold Confirm to forget" diff --git a/src/BleInput.h b/src/BleInput.h index 535822b9..85ec7858 100644 --- a/src/BleInput.h +++ b/src/BleInput.h @@ -25,6 +25,12 @@ namespace bleinput { // Advertised central name shown to peripherals during pairing. inline constexpr const char* kHostName = "CrossPoint"; +// Heap floor for starting the NimBLE stack (~57 KB) while leaving the reader's +// section-build pre-flight (40 KB) satisfiable afterwards. Shared by the main-loop +// lifecycle gate and the reader menu's toggle (which offers a defrag restart when a +// user turns BT on below the floor). +inline constexpr size_t kStartMinFreeHeap = 100 * 1024; + // Start the BLE HID host (idempotent). Returns false if BLE is compiled out or // NimBLE init failed. Safe to call repeatedly. bool ensureStarted(); diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index dac06714..4d24b3e4 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -2,9 +2,12 @@ #include #include +#include +#include "BleInput.h" #include "CrossPointSettings.h" #include "MappedInputManager.h" +#include "SilentRestart.h" #include "components/UITheme.h" #include "fontIds.h" @@ -84,6 +87,15 @@ void EpubReaderMenuActivity::loop() { // so start/stop has a single owner. SETTINGS.bluetoothEnabled = SETTINGS.bluetoothEnabled ? 0 : 1; SETTINGS.saveToFile(); + // Turning BT on below the lifecycle's heap floor would otherwise sit in PAUSED + // until the heap happens to recover -- which a long session's fragmentation never + // gives back. The user asked for BT *now*: silent-restart into this book to + // defrag (fresh boot is ~118 KB free, comfortably above the floor), and BT + // auto-starts on the way back in. + if (SETTINGS.bluetoothEnabled && !BleHid.isRunning() && ESP.getFreeHeap() < bleinput::kStartMinFreeHeap) { + LOG_INF("ERM", "BT enabled below heap floor (%u); silent restart to defrag", ESP.getFreeHeap()); + silentRestartToReader(); + } requestUpdate(); return; } @@ -138,8 +150,12 @@ void EpubReaderMenuActivity::render(RenderLock&&) { // Render current page turn value on the right edge of the content area. return pageTurnLabels[selectedPageTurnOption]; } else if (value == MenuAction::TOGGLE_BLUETOOTH) { - // Render current Bluetooth on/off state on the right edge. - return SETTINGS.bluetoothEnabled ? tr(STR_STATE_ON) : tr(STR_STATE_OFF); + // Render current Bluetooth state on the right edge. "Enabled but the stack + // isn't running" is the low-memory deferral -- show PAUSED, not a lie. + if (SETTINGS.bluetoothEnabled) { + return BleHid.isRunning() ? tr(STR_STATE_ON) : tr(STR_STATE_PAUSED); + } + return tr(STR_STATE_OFF); } else { return ""; } diff --git a/src/main.cpp b/src/main.cpp index d071de37..66ca4c61 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -514,18 +514,18 @@ void updateBluetoothLifecycle() { static constexpr uint32_t BLE_RESTART_COOLDOWN_MS = 30 * 1000; const bool inCooldown = recoveryTeardownMs != 0 && millis() - recoveryTeardownMs < BLE_RESTART_COOLDOWN_MS; // Heap gate: NimBLE needs ~57 KB, and the section-build pre-flight needs 40 KB free - // AFTER the stack is up -- so anything below ~100 KB just re-enters build recovery + // AFTER the stack is up -- so anything below the floor just re-enters build recovery // and tears BLE straight back down. (A first cut used 70 KB for restarts; 70-57=13 KB // left for builds, guaranteeing the oscillation above.) Defer and retry next loop; - // the build path's silent-restart defrag yields ~118 KB and passes this gate. - static constexpr size_t BLE_START_MIN_FREE_HEAP = 100 * 1024; + // the reader menu's toggle offers a defrag restart, and the build path's silent + // restart also yields ~118 KB and passes this gate. static bool deferralAnnounced = false; - if (wanted && !BleHid.isRunning() && (inCooldown || ESP.getFreeHeap() < BLE_START_MIN_FREE_HEAP)) { + if (wanted && !BleHid.isRunning() && (inCooldown || ESP.getFreeHeap() < bleinput::kStartMinFreeHeap)) { static uint32_t lastGateLogMs = 0; if (millis() - lastGateLogMs > 10000) { lastGateLogMs = millis(); LOG_INF("BLELC", "start deferred: heap %u floor %u cooldown=%d", ESP.getFreeHeap(), - (unsigned)BLE_START_MIN_FREE_HEAP, inCooldown ? 1 : 0); + (unsigned)bleinput::kStartMinFreeHeap, inCooldown ? 1 : 0); } // Tell the reader once per deferral episode that the remote is paused -- otherwise // the only symptom is a remote that silently stopped working. Draws into the