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:
@@ -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"
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user