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.
This commit is contained in:
Justin Mitchell
2026-06-24 16:13:43 -04:00
parent 9ab0b0bfb7
commit 6305777b22
9 changed files with 63 additions and 94 deletions
-6
View File
@@ -296,15 +296,9 @@ STR_BT_PAIRED_DEVICES: "Paired Devices"
STR_BT_NO_PAIRED: "No paired devices" STR_BT_NO_PAIRED: "No paired devices"
STR_BT_MAP_BUTTONS: "Map Remote Buttons" STR_BT_MAP_BUTTONS: "Map Remote Buttons"
STR_BT_PRESS_REMOTE: "Press a button on your remote" 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_FORWARD: "Page Forward"
STR_BT_PAGE_BACK: "Page Back" STR_BT_PAGE_BACK: "Page Back"
STR_BT_FORGET_PROMPT: "Hold Confirm to forget" 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_TO_PERCENT: "Go to %"
STR_GO_HOME_BUTTON: "Go Home" STR_GO_HOME_BUTTON: "Go Home"
STR_SYNC_PROGRESS: "Sync Progress" STR_SYNC_PROGRESS: "Sync Progress"
+14 -21
View File
@@ -1,7 +1,6 @@
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <Logging.h>
#include "BleInput.h" #include "BleInput.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
@@ -128,51 +127,37 @@ bool MappedInputManager::takeCapturedBleKey(uint8_t& kind, uint8_t& value) {
void MappedInputManager::pollBle() { void MappedInputManager::pollBle() {
bleActivityThisFrame = false; bleActivityThisFrame = false;
// Age last frame's press edges into this frame's release edges (the FreeInk host // 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++) { for (uint8_t i = 0; i < kButtonCount; i++) {
bleReleaseEdge[i] = blePressEdge[i]; bleReleaseEdge[i] = blePressEdge[i];
blePressEdge[i] = false; blePressEdge[i] = false;
if (bleReleaseEdge[i]) bleActivityThisFrame = true;
} }
freeink::KeyEvent ev; freeink::KeyEvent ev;
while (BleHid.popKey(ev)) { while (BleHid.popKey(ev)) {
uint8_t kind = 0xFF; uint8_t kind = 0xFF;
uint8_t value = 0; uint8_t value = 0;
const bool encoded = bleinput::encodeKey(ev, kind, value); if (!bleinput::encodeKey(ev, kind, value)) continue;
// 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 (bleCaptureMode) { if (bleCaptureMode) {
bleCapturedKind = kind; bleCapturedKind = kind;
bleCapturedValue = value; bleCapturedValue = value;
bleHasCaptured = true; bleHasCaptured = true;
LOG_DBG("BLE", "captured (capture mode) kind=%u val=0x%02X", kind, value);
continue; continue;
} }
// Resolve the key identity against the persisted mapping table. // Resolve the key identity against the persisted mapping table.
bool matched = false;
for (const auto& e : SETTINGS.bleKeyMap) { for (const auto& e : SETTINGS.bleKeyMap) {
if (e.button == 0xFF || e.keyKind != kind || e.keyValue != value) continue; if (e.button == 0xFF || e.keyKind != kind || e.keyValue != value) continue;
if (e.button < kButtonCount) { if (e.button < kButtonCount) {
blePressEdge[e.button] = true; blePressEdge[e.button] = true;
bleActivityThisFrame = true; bleActivityThisFrame = true;
matched = true;
LOG_DBG("BLE", "matched -> logical button %u", e.button);
} }
break; 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(); } 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, MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous,
const char* next) const { const char* next) const {
+19 -2
View File
@@ -17,6 +17,7 @@
#include <iterator> #include <iterator>
#include <limits> #include <limits>
#include "BleInput.h"
#include "BookmarkEntry.h" #include "BookmarkEntry.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "CrossPointState.h" #include "CrossPointState.h"
@@ -856,10 +857,26 @@ void EpubReaderActivity::render(RenderLock&& lock) {
const auto popupFn = [this]() { GUI.drawPopup(renderer, tr(STR_INDEXING)); }; const auto popupFn = [this]() { GUI.drawPopup(renderer, tr(STR_INDEXING)); };
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), auto buildSection = [&]() {
return section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth,
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled, popupFn)) { 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"); LOG_ERR("ERS", "Failed to persist page data to SD");
section.reset(); section.reset();
showPendingSyncSaveError(); showPendingSyncSaveError();
@@ -21,14 +21,18 @@ const BleButtonMapActivity::Fn BleButtonMapActivity::kFunctions[] = {
{MappedInputManager::Button::Left, StrId::STR_DIR_LEFT}, {MappedInputManager::Button::Left, StrId::STR_DIR_LEFT},
{MappedInputManager::Button::Right, StrId::STR_DIR_RIGHT}, {MappedInputManager::Button::Right, StrId::STR_DIR_RIGHT},
}; };
const uint8_t BleButtonMapActivity::kFunctionCount = const uint8_t BleButtonMapActivity::kFunctionCount = static_cast<uint8_t>(sizeof(kFunctions) / sizeof(kFunctions[0]));
static_cast<uint8_t>(sizeof(kFunctions) / sizeof(kFunctions[0]));
void BleButtonMapActivity::onEnter() { void BleButtonMapActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
step = Step::WaitForKey; step = Step::WaitForKey;
capturedKind = 0xFF; capturedKind = 0xFF;
functionIndex = 0; 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); mappedInput.setBleCaptureMode(true);
requestUpdate(); requestUpdate();
} }
@@ -40,6 +44,13 @@ void BleButtonMapActivity::onExit() {
bool BleButtonMapActivity::assignCapturedKey(MappedInputManager::Button button) { bool BleButtonMapActivity::assignCapturedKey(MappedInputManager::Button button) {
const uint8_t btn = static_cast<uint8_t>(button); const uint8_t btn = static_cast<uint8_t>(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. // Update an existing binding for this key, if present.
for (auto& e : SETTINGS.bleKeyMap) { for (auto& e : SETTINGS.bleKeyMap) {
if (e.button != 0xFF && e.keyKind == capturedKind && e.keyValue == capturedValue) { if (e.button != 0xFF && e.keyKind == capturedKind && e.keyValue == capturedValue) {
@@ -92,11 +103,8 @@ void BleButtonMapActivity::loop() {
}); });
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (!assignCapturedKey(kFunctions[functionIndex].button)) { assignCapturedKey(kFunctions[functionIndex].button);
// Table full: surface it instead of silently dropping the binding. // Back to capturing so the user can map (or re-map) the next remote button.
errorUntil = millis() + 2500;
}
// Back to capturing so the user can map the next remote button.
step = Step::WaitForKey; step = Step::WaitForKey;
capturedKind = 0xFF; capturedKind = 0xFF;
requestUpdate(); requestUpdate();
@@ -146,10 +154,6 @@ void BleButtonMapActivity::render(RenderLock&&) {
[this](int i) { return std::string(I18N.get(kFunctions[i].label)); }, nullptr, nullptr, nullptr, false); [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 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)); 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); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
@@ -39,9 +39,6 @@ class BleButtonMapActivity final : public Activity {
uint8_t capturedValue = 0; uint8_t capturedValue = 0;
int functionIndex = 0; int functionIndex = 0;
// Transient "mapping table full" banner.
unsigned long errorUntil = 0;
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
// Bind the captured key to the chosen logical button in SETTINGS.bleKeyMap and // Bind the captured key to the chosen logical button in SETTINGS.bleKeyMap and
@@ -45,30 +45,10 @@ void BluetoothSettingsActivity::rebuildMenuRows() {
if (BleHid.isConnected()) menuRows.push_back({Action::Disconnect, StrId::STR_BT_DISCONNECT}); 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::PairedDevices, StrId::STR_BT_PAIRED_DEVICES});
menuRows.push_back({Action::MapButtons, StrId::STR_BT_MAP_BUTTONS}); 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<int>(menuRows.size())) menuIndex = 0; if (menuIndex >= static_cast<int>(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<uint8_t>(key);
SETTINGS.bleKeyMap[slot].button = static_cast<uint8_t>(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() { void BluetoothSettingsActivity::startScanView() {
view = View::Scan; view = View::Scan;
scanIndex = 0; scanIndex = 0;
@@ -113,22 +93,6 @@ void BluetoothSettingsActivity::handleMenuConfirm() {
requestUpdate(); requestUpdate();
}); });
break; 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;
} }
} }
@@ -268,7 +232,8 @@ void BluetoothSettingsActivity::render(RenderLock&&) {
renderer, listRect, static_cast<int>(menuRows.size()), menuIndex, renderer, listRect, static_cast<int>(menuRows.size()), menuIndex,
[this](int i) { return std::string(I18N.get(menuRows[i].label)); }, nullptr, nullptr, [this](int i) { return std::string(I18N.get(menuRows[i].label)); }, nullptr, nullptr,
[this](int i) -> std::string { [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 ""; return "";
}, },
true); true);
@@ -28,7 +28,7 @@ class BluetoothSettingsActivity final : public Activity {
enum class View { Menu, Scan, Paired }; enum class View { Menu, Scan, Paired };
// Menu row actions. // Menu row actions.
enum class Action { ToggleBt, Scan, Disconnect, MapButtons, PairedDevices, PresetFree2, PresetFree3, ClearMap }; enum class Action { ToggleBt, Scan, Disconnect, MapButtons, PairedDevices };
struct MenuRow { struct MenuRow {
Action action; Action action;
StrId label; StrId label;
@@ -55,7 +55,6 @@ class BluetoothSettingsActivity final : public Activity {
void rebuildMenuRows(); void rebuildMenuRows();
void handleMenuConfirm(); void handleMenuConfirm();
void startScanView(); void startScanView();
void applyPreset(bool free3);
void setBanner(const char* text); void setBanner(const char* text);
std::string deviceLabel(int index) const; // scan list row text std::string deviceLabel(int index) const; // scan list row text