diff --git a/freeink-sdk b/freeink-sdk index a913bb30..12f9af14 160000 --- a/freeink-sdk +++ b/freeink-sdk @@ -1 +1 @@ -Subproject commit a913bb300362df2986a897c4fe5f444def3e7e49 +Subproject commit 12f9af14bbd2e7fd245556111fbceb90cd38675f diff --git a/platformio.ini b/platformio.ini index a01838ae..de8e2317 100644 --- a/platformio.ini +++ b/platformio.ini @@ -94,6 +94,7 @@ build_flags = -DENABLE_SERIAL_LOG -DLOG_LEVEL=2 ; Set log level to debug for development builds -DFREEINK_CAP_BLE_HID_HOST=1 ; BLE HID page-turner host (NimBLE) + -DFREEINK_BLE_HID_SCAN_DEBUG=1 ; verbose BLE scan lifecycle/advertisement logs for bring-up [env:gh_release] diff --git a/src/BleInput.cpp b/src/BleInput.cpp index eaa1e5a9..7e61bed3 100644 --- a/src/BleInput.cpp +++ b/src/BleInput.cpp @@ -12,6 +12,10 @@ namespace bleinput { +namespace { +volatile bool g_lifecyclePaused = false; +} + // NimBLE controller init/deinit hang (interrupt WDT) if run at the 10 MHz low-power // frequency, so force normal CPU speed around both. Centralized here so every caller // (boot restore, settings toggle, reader toggle, sleep) is covered automatically. @@ -28,6 +32,10 @@ void stop() { BleHid.end(); } +void setLifecyclePaused(bool paused) { g_lifecyclePaused = paused; } + +bool lifecyclePaused() { return g_lifecyclePaused; } + bool encodeKey(const freeink::KeyEvent& ev, uint8_t& kind, uint8_t& value) { if (ev.special != freeink::SpecialKey::None) { kind = 0; diff --git a/src/BleInput.h b/src/BleInput.h index a46b1e56..535822b9 100644 --- a/src/BleInput.h +++ b/src/BleInput.h @@ -32,6 +32,11 @@ bool ensureStarted(); // Drop the active link (e.g. before deep sleep or when the user disables BT). void stop(); +// Temporarily block the main-loop BLE lifecycle from auto-starting the stack. +// Used while a render path deliberately frees BLE RAM for a large allocation. +void setLifecyclePaused(bool paused); +bool lifecyclePaused(); + // Encode a decoded key event into the stable (kind, value) identity used by the // settings map. kind: 0 = SpecialKey, 1 = HID usage. Returns false when the event // carries no usable identity (no special key and no usage code). diff --git a/src/activities/ActivityManager.cpp b/src/activities/ActivityManager.cpp index c4c92225..a42f8539 100644 --- a/src/activities/ActivityManager.cpp +++ b/src/activities/ActivityManager.cpp @@ -254,6 +254,10 @@ bool ActivityManager::isReaderActivity() const { (currentActivity && currentActivity->isReaderActivity()); } +bool ActivityManager::currentKeepsBluetoothAlive() const { + return currentActivity && currentActivity->keepsBluetoothAlive(); +} + void ActivityManager::requestGhostCleanup() { if (currentActivity) currentActivity->requestGhostCleanup(); } diff --git a/src/activities/ActivityManager.h b/src/activities/ActivityManager.h index ea1fa373..e1b5dcc0 100644 --- a/src/activities/ActivityManager.h +++ b/src/activities/ActivityManager.h @@ -101,6 +101,7 @@ class ActivityManager { bool preventAutoSleep() const; bool isReaderActivity() const; + bool currentKeepsBluetoothAlive() const; // True if BLE should be resident for the current context: any reader (page-turner // input) or the Bluetooth settings screen (pairing) is on the stack. bool bluetoothShouldBeActive() const; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 2de5e825..3ae5365b 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -871,9 +871,11 @@ void EpubReaderActivity::render(RenderLock&& lock) { // Free the BLE stack, build, then restore it. The chapter is cached afterwards, so // this recovery runs at most once per uncached chapter; BT reconnects in a few s. LOG_INF("ERS", "Section build failed with Bluetooth on; freeing BLE RAM and retrying"); + bleinput::setLifecyclePaused(true); bleinput::stop(); built = buildSection(); const bool bleOk = bleinput::ensureStarted(); + bleinput::setLifecyclePaused(false); LOG_INF("ERS", "BLE restart after build: begin=%d", bleOk); // Hold the "BT Connecting..." popup until the remote re-links, then force the // page render below onto the ghost-cleanup (HALF) path so the popup clears diff --git a/src/activities/settings/BluetoothSettingsActivity.cpp b/src/activities/settings/BluetoothSettingsActivity.cpp index 576c9855..144de3e7 100644 --- a/src/activities/settings/BluetoothSettingsActivity.cpp +++ b/src/activities/settings/BluetoothSettingsActivity.cpp @@ -2,6 +2,7 @@ #include #include +#include #include @@ -49,10 +50,15 @@ void BluetoothSettingsActivity::rebuildMenuRows() { } void BluetoothSettingsActivity::startScanView() { + LOG_INF("BLEUI", "scan view: begin running=%d scanning=%d devices=%u paired=%u", BleHid.isRunning(), + BleHid.isScanning(), BleHid.deviceCount(), BleHid.pairedCount()); view = View::Scan; scanIndex = 0; awaitingConnect = false; + lastLoggedScanState = false; + lastLoggedDeviceCount = 0xFF; BleHid.startScan(kScanMs); + LOG_INF("BLEUI", "scan view: startScan requested scanning=%d devices=%u", BleHid.isScanning(), BleHid.deviceCount()); requestUpdate(); } @@ -176,9 +182,18 @@ void BluetoothSettingsActivity::loop() { if (view == View::Menu) { handleMenuConfirm(); } else if (view == View::Scan) { - if (!awaitingConnect && scanIndex < BleHid.deviceCount()) { + const int count = BleHid.deviceCount(); + if (!awaitingConnect && count == 0 && !BleHid.isScanning()) { + LOG_INF("BLEUI", "scan view: restart scan requested"); + BleHid.startScan(kScanMs); + LOG_INF("BLEUI", "scan view: restart scan state scanning=%d devices=%u", BleHid.isScanning(), + BleHid.deviceCount()); + requestUpdate(); + } else if (!awaitingConnect && scanIndex < count) { if (BleHid.isScanning()) BleHid.stopScan(); const auto& d = BleHid.device(static_cast(scanIndex)); + LOG_INF("BLEUI", "scan view: connect addr=%s name='%s' rssi=%d type=%u hid=%d conn=%d", d.addr, d.name, + d.rssi, d.addrType, d.hid, d.connectable); awaitingConnect = true; setBanner(tr(STR_CONNECTING)); BleHid.connect(d.addr); @@ -189,7 +204,16 @@ void BluetoothSettingsActivity::loop() { } // The scan list changes as devices are discovered — keep repainting while active. - if (view == View::Scan && BleHid.isScanning()) requestUpdate(); + if (view == View::Scan) { + const bool scanning = BleHid.isScanning(); + const uint8_t deviceCount = BleHid.deviceCount(); + if (scanning != lastLoggedScanState || deviceCount != lastLoggedDeviceCount) { + LOG_INF("BLEUI", "scan view: state scanning=%d devices=%u", scanning, deviceCount); + lastLoggedScanState = scanning; + lastLoggedDeviceCount = deviceCount; + } + if (scanning) requestUpdate(); + } } std::string BluetoothSettingsActivity::deviceLabel(int index) const { @@ -270,7 +294,8 @@ void BluetoothSettingsActivity::render(RenderLock&&) { } // Button hints differ by view (Menu selects; Scan and Paired both connect). - const char* confirm = view == View::Menu ? tr(STR_SELECT) : tr(STR_CONNECT); + const bool scanCanRestart = view == View::Scan && BleHid.deviceCount() == 0 && !BleHid.isScanning(); + const char* confirm = view == View::Menu ? tr(STR_SELECT) : scanCanRestart ? "Scan" : tr(STR_CONNECT); const auto labels = mappedInput.mapLabels(tr(STR_BACK), confirm, tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); diff --git a/src/activities/settings/BluetoothSettingsActivity.h b/src/activities/settings/BluetoothSettingsActivity.h index 648ca9c0..7ce3797b 100644 --- a/src/activities/settings/BluetoothSettingsActivity.h +++ b/src/activities/settings/BluetoothSettingsActivity.h @@ -52,6 +52,8 @@ class BluetoothSettingsActivity final : public Activity { // Guards the Paired view's hold-to-forget so it fires once per hold and suppresses // the tap-to-connect on the same press. bool pairedActionTaken = false; + bool lastLoggedScanState = false; + uint8_t lastLoggedDeviceCount = 0xFF; void rebuildMenuRows(); void handleMenuConfirm(); diff --git a/src/main.cpp b/src/main.cpp index c9e6156a..3622413c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -496,16 +496,34 @@ void setup() { void updateBluetoothLifecycle() { const bool wanted = SETTINGS.bluetoothEnabled && activityManager.bluetoothShouldBeActive() && WiFi.getMode() == WIFI_MODE_NULL; + if (wanted && !BleHid.isRunning() && bleinput::lifecyclePaused()) return; if (wanted && !BleHid.isRunning()) { - bleinput::ensureStarted(); - // Single place BLE starts for normal use (reader entry, BT toggled on, etc.), so the - // "BT Connecting..." popup shows uniformly. Then clear it with a ghost-cleanup (HALF) - // refresh so a grayscale reader page doesn't ghost over the popup. - bleinput::showConnectingUntilLinked(renderer, mappedInputManager); - activityManager.requestGhostCleanup(); - activityManager.requestUpdate(); + LOG_INF("BLELC", "start requested enabled=%u reader=%d settings=%d wifi=%d paired=%u heap=%u maxAlloc=%u", + SETTINGS.bluetoothEnabled, activityManager.isReaderActivity(), activityManager.currentKeepsBluetoothAlive(), + WiFi.getMode(), BleHid.pairedCount(), ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + RenderLock renderLock; + if (!bleinput::ensureStarted()) { + LOG_ERR("BLELC", "start failed heap=%u maxAlloc=%u", ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + return; + } + LOG_INF("BLELC", "started paired=%u heap=%u maxAlloc=%u", BleHid.pairedCount(), ESP.getFreeHeap(), + ESP.getMaxAllocHeap()); + HalPowerManager::Lock powerLock; + const bool showReconnectPopup = + activityManager.isReaderActivity() && !activityManager.currentKeepsBluetoothAlive() && BleHid.pairedCount() > 0; + if (showReconnectPopup) { + // Single place BLE starts for reader reconnect. Then clear it with a + // ghost-cleanup (HALF) refresh so a grayscale page doesn't ghost over the + // popup. + bleinput::showConnectingUntilLinked(renderer, mappedInputManager); + activityManager.requestGhostCleanup(); + activityManager.requestUpdate(); + } } else if (!wanted && BleHid.isRunning()) { + LOG_INF("BLELC", "stop requested enabled=%u active=%d wifi=%d heap=%u maxAlloc=%u", SETTINGS.bluetoothEnabled, + activityManager.bluetoothShouldBeActive(), WiFi.getMode(), ESP.getFreeHeap(), ESP.getMaxAllocHeap()); bleinput::stop(); + LOG_INF("BLELC", "stopped heap=%u maxAlloc=%u", ESP.getFreeHeap(), ESP.getMaxAllocHeap()); } }