Merge branch 'pr-2527' into feat-bluetooth
# Conflicts: # src/activities/reader/EpubReaderActivity.cpp
This commit is contained in:
@@ -302,6 +302,8 @@ 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_CONNECTING_POPUP: "BT Connecting..."
|
||||
STR_BT_PAUSED_LOW_MEM_POPUP: "BT paused (low memory)"
|
||||
STR_STATE_PAUSED: "PAUSED"
|
||||
STR_BT_PAGE_FORWARD: "Page Forward"
|
||||
STR_BT_PAGE_BACK: "Page Back"
|
||||
STR_BT_FORGET_PROMPT: "Hold Confirm to forget"
|
||||
|
||||
@@ -97,6 +97,7 @@ build_flags =
|
||||
-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
|
||||
-DFREEINK_BLE_HID_REPORT_DEBUG=1 ; raw HID report hex dumps + report-map hints (bring-up)
|
||||
|
||||
|
||||
[env:gh_release]
|
||||
|
||||
@@ -25,6 +25,12 @@ namespace bleinput {
|
||||
// Advertised central name shown to peripherals during pairing.
|
||||
inline constexpr const char* kHostName = "CrossPoint";
|
||||
|
||||
// Heap floor for starting the NimBLE stack (~57 KB) while leaving the reader's
|
||||
// section-build pre-flight (40 KB) satisfiable afterwards. Shared by the main-loop
|
||||
// lifecycle gate and the reader menu's toggle (which offers a defrag restart when a
|
||||
// user turns BT on below the floor).
|
||||
inline constexpr size_t kStartMinFreeHeap = 100 * 1024;
|
||||
|
||||
// Start the BLE HID host (idempotent). Returns false if BLE is compiled out or
|
||||
// NimBLE init failed. Safe to call repeatedly.
|
||||
bool ensureStarted();
|
||||
|
||||
@@ -6,3 +6,7 @@
|
||||
|
||||
void silentRestart(); // home screen
|
||||
void silentRestartToReader(); // currently-open EPUB (APP_STATE.openEpubPath)
|
||||
// True when this boot itself came from a silent restart. Callers that restart
|
||||
// as a last-resort defrag must check this so a failure that survives the
|
||||
// restart degrades to an error instead of a reboot loop.
|
||||
bool bootWasSilentRestart();
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "QrDisplayActivity.h"
|
||||
#include "ReaderUtils.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "SilentRestart.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "util/BookmarkUtil.h"
|
||||
@@ -888,6 +889,15 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The build-recovery path below frees the BLE stack and pauses the main-loop lifecycle
|
||||
// so it can't restart NimBLE while this render is still allocating (glyph prewarm, scan
|
||||
// strings -- an inline restart re-starved exactly that and abort()ed). Unpause only
|
||||
// when the render fully completes, on every exit path; the main-loop lifecycle then
|
||||
// brings BLE back and shows its reconnect popup.
|
||||
struct LifecycleUnpause {
|
||||
~LifecycleUnpause() { bleinput::setLifecyclePaused(false); }
|
||||
} lifecycleUnpause;
|
||||
|
||||
const auto showPendingSyncSaveError = [this]() {
|
||||
if (!pendingSyncSaveError) return;
|
||||
pendingSyncSaveError = false;
|
||||
@@ -967,25 +977,45 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
LOG_DBG("ERS", "Cache not found, building...");
|
||||
}
|
||||
|
||||
// The layout code (line-break DP arrays, CSS lookups, glyph buffers) allocates freely
|
||||
// and abort()s on OOM under -fno-exceptions, so a starved heap must be handled BEFORE
|
||||
// the build: pre-flight the floor and take the recovery path up front instead of
|
||||
// crashing mid-parse. Field data: builds succeed at ~46 KB free with BLE resident;
|
||||
// abort() observed at ~11 KB free.
|
||||
const bool heapTooLow = ESP.getFreeHeap() < BUILD_MIN_FREE_HEAP;
|
||||
if (heapTooLow) {
|
||||
LOG_ERR("ERS", "Pre-build heap %u below floor %u; entering build recovery", (unsigned)ESP.getFreeHeap(),
|
||||
(unsigned)BUILD_MIN_FREE_HEAP);
|
||||
}
|
||||
|
||||
// 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).
|
||||
// On build failure with BT enabled: free the BLE stack, retry the build, then
|
||||
// restore it. The inflated HTML is cached after a successful build, so this
|
||||
// recovery runs at most once per uncached chapter; BT reconnects in a few s.
|
||||
// On build failure (or a pre-flight floor miss) with BT enabled: free the BLE stack
|
||||
// and retry. The chapter is cached afterwards, so this recovery runs at most once
|
||||
// per uncached chapter.
|
||||
// Deliberately do NOT restart BLE inline: this render still has its own allocations
|
||||
// to make (glyph prewarm, scan strings), and an inline NimBLE restart re-starves
|
||||
// it -- field crash: std::string::reserve(2048) abort()ed at ~8 KB free right
|
||||
// after an inline restart. The lifecycle stays paused until this render fully
|
||||
// completes (unpause guard at the top of render()); the main-loop lifecycle then
|
||||
// restarts BLE behind its own heap gate and shows the reconnect popup.
|
||||
const auto retryWithBleFreed = [&](auto&& buildFn) {
|
||||
LOG_INF("ERS", "Section build failed with Bluetooth on; freeing BLE RAM and retrying");
|
||||
LOG_INF("ERS", "Section build needs heap; freeing BLE RAM and retrying");
|
||||
bleinput::setLifecyclePaused(true);
|
||||
bleinput::stop();
|
||||
const bool built = buildFn();
|
||||
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
|
||||
// without ghosting the grayscale page.
|
||||
bleinput::showConnectingUntilLinked(renderer, mappedInput);
|
||||
requestGhostCleanup();
|
||||
return built;
|
||||
return buildFn();
|
||||
};
|
||||
|
||||
// Even with BLE freed, the build can fail when this session's parse churn has
|
||||
// fragmented the heap beyond in-place recovery. A silent restart is the only
|
||||
// real defrag on this heap (no compaction); it resumes into this book and
|
||||
// rebuilds the section on a fresh heap. Guarded by bootWasSilentRestart() so a
|
||||
// build that fails again after the restart degrades to the error popup below
|
||||
// instead of reboot-looping.
|
||||
const auto silentRestartDefrag = [&]() {
|
||||
if (bootWasSilentRestart()) return;
|
||||
LOG_ERR("ERS", "Section build failed after BLE recovery; silent restart to defrag heap");
|
||||
silentRestartToReader();
|
||||
};
|
||||
|
||||
// Jumps that need the final pagination or the anchor map -- explicit page jumps,
|
||||
@@ -1013,11 +1043,12 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled, popupFn);
|
||||
};
|
||||
bool built = buildSection();
|
||||
bool built = !heapTooLow && buildSection();
|
||||
if (!built && SETTINGS.bluetoothEnabled) {
|
||||
built = retryWithBleFreed(buildSection);
|
||||
}
|
||||
if (!built) {
|
||||
silentRestartDefrag();
|
||||
LOG_ERR("ERS", "Failed to persist page data to SD");
|
||||
section.reset();
|
||||
showPendingSyncSaveError();
|
||||
@@ -1065,11 +1096,12 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
||||
viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle,
|
||||
SETTINGS.imageRendering, SETTINGS.focusReadingEnabled);
|
||||
};
|
||||
bool started = beginBuild();
|
||||
bool started = !heapTooLow && beginBuild();
|
||||
if (!started && SETTINGS.bluetoothEnabled) {
|
||||
started = retryWithBleFreed(beginBuild);
|
||||
}
|
||||
if (!started) {
|
||||
silentRestartDefrag();
|
||||
LOG_ERR("ERS", "Failed to start section build");
|
||||
section.reset();
|
||||
showPendingSyncSaveError();
|
||||
|
||||
@@ -59,6 +59,14 @@ class EpubReaderActivity final : public Activity {
|
||||
SavedPosition savedPositions[MAX_FOOTNOTE_DEPTH] = {};
|
||||
int footnoteDepth = 0;
|
||||
|
||||
// Heap floor for entering a section build. The layout code allocates freely (line-break DP
|
||||
// arrays sized by word count, CSS rule lookups, glyph buffers) and under -fno-exceptions an
|
||||
// OOM there abort()s the firmware instead of failing cleanly -- so a starved heap must be
|
||||
// handled *before* the build, not after. Field data: builds succeed at ~46 KB free with BLE
|
||||
// resident; abort() observed at ~11 KB free. CSS styling already degrades below 48 KB
|
||||
// (MIN_FREE_HEAP_FOR_CSS), so 40 KB trades a few early BLE teardowns for not crashing.
|
||||
static constexpr size_t BUILD_MIN_FREE_HEAP = 40 * 1024;
|
||||
|
||||
void renderContents(std::unique_ptr<Page> page, int orientedMarginTop, int orientedMarginRight,
|
||||
int orientedMarginBottom, int orientedMarginLeft);
|
||||
void renderStatusBar() const;
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <I18n.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include "BleInput.h"
|
||||
#include "CrossPointSettings.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "SilentRestart.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
@@ -93,6 +96,15 @@ void EpubReaderMenuActivity::loop() {
|
||||
// so start/stop has a single owner.
|
||||
SETTINGS.bluetoothEnabled = SETTINGS.bluetoothEnabled ? 0 : 1;
|
||||
SETTINGS.saveToFile();
|
||||
// Turning BT on below the lifecycle's heap floor would otherwise sit in PAUSED
|
||||
// until the heap happens to recover -- which a long session's fragmentation never
|
||||
// gives back. The user asked for BT *now*: silent-restart into this book to
|
||||
// defrag (fresh boot is ~118 KB free, comfortably above the floor), and BT
|
||||
// auto-starts on the way back in.
|
||||
if (SETTINGS.bluetoothEnabled && !BleHid.isRunning() && ESP.getFreeHeap() < bleinput::kStartMinFreeHeap) {
|
||||
LOG_INF("ERM", "BT enabled below heap floor (%u); silent restart to defrag", ESP.getFreeHeap());
|
||||
silentRestartToReader();
|
||||
}
|
||||
requestUpdate();
|
||||
return;
|
||||
}
|
||||
@@ -149,8 +161,12 @@ void EpubReaderMenuActivity::render(RenderLock&&) {
|
||||
// Render current page turn value on the right edge of the content area.
|
||||
return pageTurnLabels[selectedPageTurnOption];
|
||||
} else if (value == MenuAction::TOGGLE_BLUETOOTH) {
|
||||
// Render current Bluetooth on/off state on the right edge.
|
||||
return SETTINGS.bluetoothEnabled ? tr(STR_STATE_ON) : tr(STR_STATE_OFF);
|
||||
// Render current Bluetooth state on the right edge. "Enabled but the stack
|
||||
// isn't running" is the low-memory deferral -- show PAUSED, not a lie.
|
||||
if (SETTINGS.bluetoothEnabled) {
|
||||
return BleHid.isRunning() ? tr(STR_STATE_ON) : tr(STR_STATE_PAUSED);
|
||||
}
|
||||
return tr(STR_STATE_OFF);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -128,6 +128,12 @@ enum class BootResume : uint8_t {
|
||||
QuickResume, // wake from a quick-resume deep sleep (SD flag; survives power loss)
|
||||
};
|
||||
|
||||
// Latched in setup() from the read-and-clear of the RTC flag, so the reboot-loop
|
||||
// guard in bootWasSilentRestart() has the answer for the whole session.
|
||||
static bool bootWasSilentRestartFlag = false;
|
||||
|
||||
bool bootWasSilentRestart() { return bootWasSilentRestartFlag; }
|
||||
|
||||
// Latched true once enterDeepSleep() commits to sleeping, before it tears down
|
||||
// the current activity. WiFi activities call silentRestart() in onExit() to
|
||||
// clear heap fragmentation on the way out, but deep sleep is a full chip reset
|
||||
@@ -330,6 +336,7 @@ void setup() {
|
||||
(isSilentReboot && silentRebootTarget <= SILENT_REBOOT_TARGET_READER) ? silentRebootTarget : 0;
|
||||
silentRebootMagic = 0;
|
||||
silentRebootTarget = 0;
|
||||
bootWasSilentRestartFlag = isSilentReboot;
|
||||
|
||||
gpio.begin();
|
||||
powerManager.begin();
|
||||
@@ -496,7 +503,47 @@ void setup() {
|
||||
void updateBluetoothLifecycle() {
|
||||
const bool wanted =
|
||||
SETTINGS.bluetoothEnabled && activityManager.bluetoothShouldBeActive() && WiFi.getMode() == WIFI_MODE_NULL;
|
||||
// Track recovery teardowns: while the reader's build recovery holds the lifecycle
|
||||
// paused it has taken BLE down deliberately. After that, hold a cool-down before any
|
||||
// restart -- an immediate restart re-enters the exact heap state that just failed and
|
||||
// the lifecycle becomes an oscillator (restart -> connect popup -> build fails ->
|
||||
// teardown -> restart...), observed in the field as a "BT Connecting..." loop.
|
||||
static uint32_t recoveryTeardownMs = 0;
|
||||
if (bleinput::lifecyclePaused()) recoveryTeardownMs = millis();
|
||||
if (wanted && !BleHid.isRunning() && bleinput::lifecyclePaused()) return;
|
||||
static constexpr uint32_t BLE_RESTART_COOLDOWN_MS = 30 * 1000;
|
||||
const bool inCooldown = recoveryTeardownMs != 0 && millis() - recoveryTeardownMs < BLE_RESTART_COOLDOWN_MS;
|
||||
// Heap gate: NimBLE needs ~57 KB, and the section-build pre-flight needs 40 KB free
|
||||
// AFTER the stack is up -- so anything below the floor just re-enters build recovery
|
||||
// and tears BLE straight back down. (A first cut used 70 KB for restarts; 70-57=13 KB
|
||||
// left for builds, guaranteeing the oscillation above.) Defer and retry next loop;
|
||||
// the reader menu's toggle offers a defrag restart, and the build path's silent
|
||||
// restart also yields ~118 KB and passes this gate.
|
||||
static bool deferralAnnounced = false;
|
||||
if (wanted && !BleHid.isRunning() && (inCooldown || ESP.getFreeHeap() < bleinput::kStartMinFreeHeap)) {
|
||||
static uint32_t lastGateLogMs = 0;
|
||||
if (millis() - lastGateLogMs > 10000) {
|
||||
lastGateLogMs = millis();
|
||||
LOG_INF("BLELC", "start deferred: heap %u floor %u cooldown=%d", ESP.getFreeHeap(),
|
||||
(unsigned)bleinput::kStartMinFreeHeap, inCooldown ? 1 : 0);
|
||||
}
|
||||
// Tell the reader once per deferral episode that the remote is paused -- otherwise
|
||||
// the only symptom is a remote that silently stopped working. Draws into the
|
||||
// existing framebuffer (no heap); the next page render clears it via ghost cleanup.
|
||||
if (!deferralAnnounced && activityManager.isReaderActivity() && BleHid.pairedCount() > 0) {
|
||||
deferralAnnounced = true;
|
||||
{
|
||||
RenderLock renderLock;
|
||||
GUI.drawPopup(renderer, tr(STR_BT_PAUSED_LOW_MEM_POPUP));
|
||||
activityManager.requestGhostCleanup();
|
||||
}
|
||||
// Toast semantics: request a redraw so the popup clears after the (~2 s) page
|
||||
// re-render instead of lingering until the next page turn. E-ink has no free
|
||||
// timers -- clearing costs one refresh whenever it happens, so do it now.
|
||||
activityManager.requestUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (wanted && !BleHid.isRunning()) {
|
||||
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(),
|
||||
@@ -508,6 +555,8 @@ void updateBluetoothLifecycle() {
|
||||
}
|
||||
LOG_INF("BLELC", "started paired=%u heap=%u maxAlloc=%u", BleHid.pairedCount(), ESP.getFreeHeap(),
|
||||
ESP.getMaxAllocHeap());
|
||||
recoveryTeardownMs = 0; // stack is back; clear the cool-down
|
||||
deferralAnnounced = false; // re-announce if a later episode defers again
|
||||
HalPowerManager::Lock powerLock;
|
||||
const bool showReconnectPopup =
|
||||
activityManager.isReaderActivity() && !activityManager.currentKeepsBluetoothAlive() && BleHid.pairedCount() > 0;
|
||||
|
||||
Reference in New Issue
Block a user