From 6305777b22717b0fa6990d53fa3d87917d76b3cc Mon Sep 17 00:00:00 2001 From: Justin Mitchell Date: Wed, 24 Jun 2026 16:13:43 -0400 Subject: [PATCH] Retry EPUB section build after freeing BLE stack When building an EPUB section fails with Bluetooth enabled, temporarily stop the BLE stack to free up memory (~16KB), retry the build, then restart BLE. This works around memory fragmentation caused by the NimBLE stack that prevents allocating the large contiguous buffer needed for inflate/deflate operations. The recovery only runs once per uncached chapter since chapters are cached afterwards. --- lib/I18n/translations/english.yaml | 6 --- src/CrossPointSettings.h | 6 +-- src/MappedInputManager.cpp | 35 ++++++--------- src/activities/reader/EpubReaderActivity.cpp | 25 +++++++++-- .../settings/BleButtonMapActivity.cpp | 26 ++++++----- .../settings/BleButtonMapActivity.h | 3 -- .../settings/BluetoothSettingsActivity.cpp | 45 +++---------------- .../settings/BluetoothSettingsActivity.h | 7 ++- src/main.cpp | 4 +- 9 files changed, 63 insertions(+), 94 deletions(-) diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 3a59e618..7d552ead 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -296,15 +296,9 @@ STR_BT_PAIRED_DEVICES: "Paired Devices" 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_ASSIGN_PROMPT: "Select what this button does" -STR_BT_PRESET_FREE2: "Apply Free2 Preset" -STR_BT_PRESET_FREE3: "Apply Free3 Preset" -STR_BT_CLEAR_MAP: "Clear Button Mappings" -STR_BT_PAIRING_PASSKEY: "Passkey: %u" STR_BT_PAGE_FORWARD: "Page Forward" STR_BT_PAGE_BACK: "Page Back" STR_BT_FORGET_PROMPT: "Hold Confirm to forget" -STR_BT_MAP_FULL: "Mapping table full - clear mappings first" STR_GO_TO_PERCENT: "Go to %" STR_GO_HOME_BUTTON: "Go Home" STR_SYNC_PROGRESS: "Sync Progress" diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 73410c83..d87b7dd0 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -227,9 +227,9 @@ class CrossPointSettings { // (some buttons emit more than one code). Each entry is 3 bytes. static constexpr uint8_t BLE_MAP_CAPACITY = 10; struct BleKeyMapEntry { - uint8_t keyKind = 0xFF; // 0 = SpecialKey, 1 = HID usage code; 0xFF = empty slot - uint8_t keyValue = 0; // (uint8_t)freeink::SpecialKey, or the raw HID usage id - uint8_t button = 0xFF; // (uint8_t)MappedInputManager::Button; 0xFF = unassigned + uint8_t keyKind = 0xFF; // 0 = SpecialKey, 1 = HID usage code; 0xFF = empty slot + uint8_t keyValue = 0; // (uint8_t)freeink::SpecialKey, or the raw HID usage id + uint8_t button = 0xFF; // (uint8_t)MappedInputManager::Button; 0xFF = unassigned }; BleKeyMapEntry bleKeyMap[BLE_MAP_CAPACITY] = {}; // Reader font settings diff --git a/src/MappedInputManager.cpp b/src/MappedInputManager.cpp index 13a49068..b9470edc 100644 --- a/src/MappedInputManager.cpp +++ b/src/MappedInputManager.cpp @@ -1,7 +1,6 @@ #include "MappedInputManager.h" #include -#include #include "BleInput.h" #include "CrossPointSettings.h" @@ -128,51 +127,37 @@ bool MappedInputManager::takeCapturedBleKey(uint8_t& kind, uint8_t& value) { void MappedInputManager::pollBle() { bleActivityThisFrame = false; // Age last frame's press edges into this frame's release edges (the FreeInk host - // surfaces presses + synthetic repeats but never releases), then clear presses. + // surfaces presses + synthetic repeats but never releases), then clear presses. A + // pending release also counts as BLE activity this frame so getHeldTime() reports + // zero on the release frame too (page-turn handlers often fire on release). for (uint8_t i = 0; i < kButtonCount; i++) { bleReleaseEdge[i] = blePressEdge[i]; blePressEdge[i] = false; + if (bleReleaseEdge[i]) bleActivityThisFrame = true; } freeink::KeyEvent ev; while (BleHid.popKey(ev)) { uint8_t kind = 0xFF; uint8_t value = 0; - const bool encoded = bleinput::encodeKey(ev, kind, value); - // TEMP page-turner bring-up: log every decoded BLE key so the actual keycodes - // a remote sends are visible on serial. Remove once mapping is verified. - LOG_DBG("BLE", "key ch=%d code=0x%02X special=%u -> kind=%u val=0x%02X enc=%d", ev.ch, ev.keycode, - (unsigned)ev.special, kind, value, encoded); - if (!encoded) continue; + if (!bleinput::encodeKey(ev, kind, value)) continue; if (bleCaptureMode) { bleCapturedKind = kind; bleCapturedValue = value; bleHasCaptured = true; - LOG_DBG("BLE", "captured (capture mode) kind=%u val=0x%02X", kind, value); continue; } // Resolve the key identity against the persisted mapping table. - bool matched = false; for (const auto& e : SETTINGS.bleKeyMap) { if (e.button == 0xFF || e.keyKind != kind || e.keyValue != value) continue; if (e.button < kButtonCount) { blePressEdge[e.button] = true; bleActivityThisFrame = true; - matched = true; - LOG_DBG("BLE", "matched -> logical button %u", e.button); } break; } - if (!matched) { - LOG_DBG("BLE", "NO MATCH for kind=%u val=0x%02X; current map:", kind, value); - for (uint8_t i = 0; i < CrossPointSettings::BLE_MAP_CAPACITY; i++) { - const auto& e = SETTINGS.bleKeyMap[i]; - if (e.button == 0xFF) continue; - LOG_DBG("BLE", " slot %u: kind=%u val=0x%02X -> button %u", i, e.keyKind, e.keyValue, e.button); - } - } } } @@ -180,7 +165,15 @@ bool MappedInputManager::wasAnyPressed() const { return gpio.wasAnyPressed(); } bool MappedInputManager::wasAnyReleased() const { return gpio.wasAnyReleased(); } -unsigned long MappedInputManager::getHeldTime() const { return gpio.getHeldTime(); } +unsigned long MappedInputManager::getHeldTime() const { + // A BLE-mapped key is a momentary tap with no physical hold (we don't model BLE + // press-and-hold). gpio.getHeldTime() returns the *last physical* button's hold + // duration, which is stale — if a BLE edge drove input this frame, reporting that + // stale value makes a tap look like a long-press (e.g. page tap -> chapter skip). + // Report zero in that case so BLE taps are always treated as short presses. + if (bleActivityThisFrame) return 0; + return gpio.getHeldTime(); +} MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous, const char* next) const { diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 3d2df14e..8672f812 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -17,6 +17,7 @@ #include #include +#include "BleInput.h" #include "BookmarkEntry.h" #include "CrossPointSettings.h" #include "CrossPointState.h" @@ -856,10 +857,26 @@ void EpubReaderActivity::render(RenderLock&& lock) { const auto popupFn = [this]() { GUI.drawPopup(renderer, tr(STR_INDEXING)); }; - if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), - SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, - viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, - SETTINGS.imageRendering, SETTINGS.focusReadingEnabled, popupFn)) { + auto buildSection = [&]() { + return section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), + SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, + viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, + SETTINGS.imageRendering, SETTINGS.focusReadingEnabled, popupFn); + }; + + bool built = buildSection(); + if (!built && SETTINGS.bluetoothEnabled) { + // Building a section needs a large contiguous inflate (deflate) window that the + // resident NimBLE stack fragments out of existence (~16 KB max block with BT on). + // 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::stop(); + built = buildSection(); + const bool bleOk = bleinput::ensureStarted(); + LOG_INF("ERS", "BLE restart after build: begin=%d (auto-reconnect follows)", bleOk); + } + if (!built) { LOG_ERR("ERS", "Failed to persist page data to SD"); section.reset(); showPendingSyncSaveError(); diff --git a/src/activities/settings/BleButtonMapActivity.cpp b/src/activities/settings/BleButtonMapActivity.cpp index 154a4480..e094c05c 100644 --- a/src/activities/settings/BleButtonMapActivity.cpp +++ b/src/activities/settings/BleButtonMapActivity.cpp @@ -21,14 +21,18 @@ const BleButtonMapActivity::Fn BleButtonMapActivity::kFunctions[] = { {MappedInputManager::Button::Left, StrId::STR_DIR_LEFT}, {MappedInputManager::Button::Right, StrId::STR_DIR_RIGHT}, }; -const uint8_t BleButtonMapActivity::kFunctionCount = - static_cast(sizeof(kFunctions) / sizeof(kFunctions[0])); +const uint8_t BleButtonMapActivity::kFunctionCount = static_cast(sizeof(kFunctions) / sizeof(kFunctions[0])); void BleButtonMapActivity::onEnter() { Activity::onEnter(); step = Step::WaitForKey; capturedKind = 0xFF; functionIndex = 0; + // Start every mapping session from a clean slate: the user re-maps each remote + // button once, so a button can't be left bound to a stale action and there's no + // separate "clear mappings" step to remember. + for (auto& e : SETTINGS.bleKeyMap) e = CrossPointSettings::BleKeyMapEntry{}; + SETTINGS.saveToFile(); mappedInput.setBleCaptureMode(true); requestUpdate(); } @@ -40,6 +44,13 @@ void BleButtonMapActivity::onExit() { bool BleButtonMapActivity::assignCapturedKey(MappedInputManager::Button button) { const uint8_t btn = static_cast(button); + // One key per action: drop any other key currently bound to this action so the same + // action can't be triggered by two different remote buttons. + for (auto& e : SETTINGS.bleKeyMap) { + if (e.button == btn && !(e.keyKind == capturedKind && e.keyValue == capturedValue)) { + e = CrossPointSettings::BleKeyMapEntry{}; + } + } // Update an existing binding for this key, if present. for (auto& e : SETTINGS.bleKeyMap) { if (e.button != 0xFF && e.keyKind == capturedKind && e.keyValue == capturedValue) { @@ -92,11 +103,8 @@ void BleButtonMapActivity::loop() { }); if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { - if (!assignCapturedKey(kFunctions[functionIndex].button)) { - // Table full: surface it instead of silently dropping the binding. - errorUntil = millis() + 2500; - } - // Back to capturing so the user can map the next remote button. + assignCapturedKey(kFunctions[functionIndex].button); + // Back to capturing so the user can map (or re-map) the next remote button. step = Step::WaitForKey; capturedKind = 0xFF; requestUpdate(); @@ -146,10 +154,6 @@ void BleButtonMapActivity::render(RenderLock&&) { [this](int i) { return std::string(I18N.get(kFunctions[i].label)); }, nullptr, nullptr, nullptr, false); } - if (errorUntil > millis()) { - GUI.drawHelpText(renderer, Rect{0, pageHeight - metrics.buttonHintsHeight - 22, pageWidth, 20}, tr(STR_BT_MAP_FULL)); - } - const char* confirm = step == Step::WaitForKey ? "" : tr(STR_SELECT); 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/BleButtonMapActivity.h b/src/activities/settings/BleButtonMapActivity.h index 25673ce7..7b069d5c 100644 --- a/src/activities/settings/BleButtonMapActivity.h +++ b/src/activities/settings/BleButtonMapActivity.h @@ -39,9 +39,6 @@ class BleButtonMapActivity final : public Activity { uint8_t capturedValue = 0; int functionIndex = 0; - // Transient "mapping table full" banner. - unsigned long errorUntil = 0; - ButtonNavigator buttonNavigator; // Bind the captured key to the chosen logical button in SETTINGS.bleKeyMap and diff --git a/src/activities/settings/BluetoothSettingsActivity.cpp b/src/activities/settings/BluetoothSettingsActivity.cpp index 06204f47..c7e8e727 100644 --- a/src/activities/settings/BluetoothSettingsActivity.cpp +++ b/src/activities/settings/BluetoothSettingsActivity.cpp @@ -45,30 +45,10 @@ void BluetoothSettingsActivity::rebuildMenuRows() { if (BleHid.isConnected()) menuRows.push_back({Action::Disconnect, StrId::STR_BT_DISCONNECT}); menuRows.push_back({Action::PairedDevices, StrId::STR_BT_PAIRED_DEVICES}); menuRows.push_back({Action::MapButtons, StrId::STR_BT_MAP_BUTTONS}); - menuRows.push_back({Action::PresetFree2, StrId::STR_BT_PRESET_FREE2}); - menuRows.push_back({Action::PresetFree3, StrId::STR_BT_PRESET_FREE3}); - menuRows.push_back({Action::ClearMap, StrId::STR_BT_CLEAR_MAP}); } if (menuIndex >= static_cast(menuRows.size())) menuIndex = 0; } -void BluetoothSettingsActivity::applyPreset(bool free3) { - // Starter presets. Page-turner remotes commonly emit PageUp/PageDown (and a - // center key on the 3-button Free3); the user can re-map via "Map Remote - // Buttons" if their device sends different codes. - using Btn = MappedInputManager::Button; - for (auto& e : SETTINGS.bleKeyMap) e = CrossPointSettings::BleKeyMapEntry{}; - auto set = [&](int slot, freeink::SpecialKey key, Btn button) { - SETTINGS.bleKeyMap[slot].keyKind = 0; // SpecialKey - SETTINGS.bleKeyMap[slot].keyValue = static_cast(key); - SETTINGS.bleKeyMap[slot].button = static_cast(button); - }; - set(0, freeink::SpecialKey::PageDown, Btn::PageForward); - set(1, freeink::SpecialKey::PageUp, Btn::PageBack); - if (free3) set(2, freeink::SpecialKey::Enter, Btn::Confirm); - SETTINGS.saveToFile(); -} - void BluetoothSettingsActivity::startScanView() { view = View::Scan; scanIndex = 0; @@ -113,22 +93,6 @@ void BluetoothSettingsActivity::handleMenuConfirm() { requestUpdate(); }); break; - case Action::PresetFree2: - applyPreset(false); - setBanner(tr(STR_BT_PRESET_FREE2)); - requestUpdate(); - break; - case Action::PresetFree3: - applyPreset(true); - setBanner(tr(STR_BT_PRESET_FREE3)); - requestUpdate(); - break; - case Action::ClearMap: - for (auto& e : SETTINGS.bleKeyMap) e = CrossPointSettings::BleKeyMapEntry{}; - SETTINGS.saveToFile(); - setBanner(tr(STR_BT_CLEAR_MAP)); - requestUpdate(); - break; } } @@ -173,9 +137,9 @@ void BluetoothSettingsActivity::loop() { } // Navigation within the active list. - const int count = view == View::Menu ? static_cast(menuRows.size()) - : view == View::Scan ? BleHid.deviceCount() - : BleHid.pairedCount(); + const int count = view == View::Menu ? static_cast(menuRows.size()) + : view == View::Scan ? BleHid.deviceCount() + : BleHid.pairedCount(); int* idx = view == View::Menu ? &menuIndex : view == View::Scan ? &scanIndex : &pairedIndex; buttonNavigator.onNext([this, count, idx] { if (count > 0) *idx = ButtonNavigator::nextIndex(*idx, count); @@ -268,7 +232,8 @@ void BluetoothSettingsActivity::render(RenderLock&&) { renderer, listRect, static_cast(menuRows.size()), menuIndex, [this](int i) { return std::string(I18N.get(menuRows[i].label)); }, nullptr, nullptr, [this](int i) -> std::string { - if (menuRows[i].action == Action::ToggleBt) return SETTINGS.bluetoothEnabled ? tr(STR_STATE_ON) : tr(STR_STATE_OFF); + if (menuRows[i].action == Action::ToggleBt) + return SETTINGS.bluetoothEnabled ? tr(STR_STATE_ON) : tr(STR_STATE_OFF); return ""; }, true); diff --git a/src/activities/settings/BluetoothSettingsActivity.h b/src/activities/settings/BluetoothSettingsActivity.h index af9abb51..c8e142e2 100644 --- a/src/activities/settings/BluetoothSettingsActivity.h +++ b/src/activities/settings/BluetoothSettingsActivity.h @@ -28,7 +28,7 @@ class BluetoothSettingsActivity final : public Activity { enum class View { Menu, Scan, Paired }; // Menu row actions. - enum class Action { ToggleBt, Scan, Disconnect, MapButtons, PairedDevices, PresetFree2, PresetFree3, ClearMap }; + enum class Action { ToggleBt, Scan, Disconnect, MapButtons, PairedDevices }; struct MenuRow { Action action; StrId label; @@ -55,9 +55,8 @@ class BluetoothSettingsActivity final : public Activity { void rebuildMenuRows(); void handleMenuConfirm(); void startScanView(); - void applyPreset(bool free3); void setBanner(const char* text); - std::string deviceLabel(int index) const; // scan list row text - std::string pairedLabel(int index) const; // paired list row text + std::string deviceLabel(int index) const; // scan list row text + std::string pairedLabel(int index) const; // paired list row text }; diff --git a/src/main.cpp b/src/main.cpp index 9016d9b2..8b4e4314 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -500,8 +500,8 @@ void loop() { static unsigned long lastMemPrint = 0; gpio.update(); - BleHid.poll(); // drive BLE auto-reconnect + key auto-repeat (no-op when BT off) - mappedInputManager.pollBle(); // drain BLE keys -> logical-button overlay for this frame + BleHid.poll(); // drive BLE auto-reconnect + key auto-repeat (no-op when BT off) + mappedInputManager.pollBle(); // drain BLE keys -> logical-button overlay for this frame halTiltSensor.update(SETTINGS.tiltPageTurn, SETTINGS.orientation, activityManager.isReaderActivity()); renderer.setFadingFix(SETTINGS.fadingFix);