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_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"
+3 -3
View File
@@ -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
+14 -21
View File
@@ -1,7 +1,6 @@
#include "MappedInputManager.h"
#include <GfxRenderer.h>
#include <Logging.h>
#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 {
+21 -4
View File
@@ -17,6 +17,7 @@
#include <iterator>
#include <limits>
#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();
@@ -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<uint8_t>(sizeof(kFunctions) / sizeof(kFunctions[0]));
const uint8_t BleButtonMapActivity::kFunctionCount = static_cast<uint8_t>(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<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.
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);
@@ -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
@@ -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<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() {
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<int>(menuRows.size())
: view == View::Scan ? BleHid.deviceCount()
: BleHid.pairedCount();
const int count = view == View::Menu ? static_cast<int>(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<int>(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);
@@ -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
};
+2 -2
View File
@@ -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);