diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 2f26a95f..fc71b296 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -969,17 +969,15 @@ void EpubReaderActivity::render(RenderLock&& lock) { return; } - // Shed the BLE stack before rendering into a starved heap. Everything below — - // page deserialization, glyph caching, catch-up build steps — allocates through - // throwing paths that abort() on OOM under -fno-exceptions. Field data: with no - // shed, a session ground to <2.2 KB free (per-glyph SD fallbacks, no AA, 4.7 s - // pages) and then aborted on a tiny vector growth. Freeing BLE returns ~52 KB and - // restores a large contiguous block; the lifecycle restarts it behind its heap - // gate once the pressure passes. + FrameBufferBuildLoan buildLoan(renderer); + + // If BLE leaves the reader below the render floor, lend the framebuffer before + // deserializing/loading the page instead of tearing BLE down immediately. The + // restore path still frees BLE if the framebuffer cannot be reallocated. if (BleHid.isRunning() && ESP.getFreeHeap() < RENDER_MIN_FREE_HEAP) { - LOG_ERR("ERS", "Render heap %u below floor %u; freeing BLE RAM", (unsigned)ESP.getFreeHeap(), + LOG_INF("ERS", "Render heap %u below floor %u; lending framebuffer", (unsigned)ESP.getFreeHeap(), (unsigned)RENDER_MIN_FREE_HEAP); - bleinput::stop(); + buildLoan.release(); } const auto showPendingSyncSaveError = [this]() { @@ -988,8 +986,6 @@ void EpubReaderActivity::render(RenderLock&& lock) { GUI.drawPopup(renderer, tr(STR_SAVE_PROGRESS_FAILED)); }; - FrameBufferBuildLoan buildLoan(renderer); - // A section build failure (e.g. an invalid/corrupt EPUB that fails XML parsing) leaves the // "Indexing" popup on screen with no way forward. Surface an explicit error instead of hanging. // clearScreen first so the error popup doesn't overlay the stale "Indexing" popup. @@ -1639,7 +1635,7 @@ void EpubReaderActivity::renderStatusBar() const { } GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset, true, currentPageBookmarked, - section->isBuilding()); + section->isBuilding(), BleHid.isConnected()); } void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) { diff --git a/src/components/icons/bluetooth.h b/src/components/icons/bluetooth.h new file mode 100644 index 00000000..fb667a15 --- /dev/null +++ b/src/components/icons/bluetooth.h @@ -0,0 +1,7 @@ +#pragma once +#include + +// size: 16x16, generated from Lucide bluetooth.svg with FreeInk's icon generator. +static const uint8_t BluetoothStatusIcon[] = {0xFF, 0xFF, 0xFE, 0x7F, 0xFE, 0x3F, 0xFE, 0x1F, 0xF6, 0x4F, 0xFA, + 0x5F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0xFA, 0x5F, + 0xF6, 0x4F, 0xFE, 0x1F, 0xFE, 0x3F, 0xFE, 0x7F, 0xFF, 0xFF}; diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 5fe6e545..eb22266e 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -12,6 +12,7 @@ #include "I18n.h" #include "RecentBooksStore.h" +#include "components/icons/bluetooth.h" #include "components/UITheme.h" #include "components/icons/bookmark.h" #include "fontIds.h" @@ -23,8 +24,10 @@ constexpr int homeMarginTop = 30; constexpr int subtitleY = 738; constexpr int bookmarkStatusIconWidth = 16; constexpr int bookmarkStatusIconHeight = 14; -constexpr int bookmarkStatusIconGap = 4; constexpr int bookmarkStatusIconTopCrop = 2; +constexpr int bluetoothStatusIconWidth = 16; +constexpr int bluetoothStatusIconHeight = 16; +constexpr int statusIconGap = 4; bool statusBarTextLaneVisible() { return SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage || @@ -43,6 +46,17 @@ void drawBookmarkStatusIcon(const GfxRenderer& renderer, const int x, const int } } +void drawBluetoothStatusIcon(const GfxRenderer& renderer, const int x, const int y) { + constexpr int bytesPerRow = bluetoothStatusIconWidth / 8; + for (int row = 0; row < bluetoothStatusIconHeight; ++row) { + for (int col = 0; col < bluetoothStatusIconWidth; ++col) { + const uint8_t byte = BluetoothStatusIcon[row * bytesPerRow + col / 8]; + const uint8_t mask = 1U << (7 - (col % 8)); + renderer.drawPixel(x + col, y + row, (byte & mask) == 0); + } + } +} + } // namespace void BaseTheme::drawBatteryOutline(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight) { @@ -749,7 +763,8 @@ void BaseTheme::fillPopupProgress(const GfxRenderer& renderer, const Rect& layou void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage, const int pageCount, std::string title, const int paddingBottom, const int textYOffset, - const bool fillMargin, const bool isPageBookmarked, const bool pageCountEstimated) const { + const bool fillMargin, const bool isPageBookmarked, const bool pageCountEstimated, + const bool bluetoothConnected) const { auto metrics = UITheme::getInstance().getMetrics(); int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft; renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom, @@ -845,9 +860,17 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c } } - // Draw Bookmark + // Draw status icons + if (showStatusBarTextLane && bluetoothConnected) { + const int bluetoothGap = leftClusterWidth > 0 ? statusIconGap : 0; + const int bluetoothX = leftClusterX + leftClusterWidth + bluetoothGap; + const int bluetoothY = textY + 3; + drawBluetoothStatusIcon(renderer, bluetoothX, bluetoothY); + leftClusterWidth += bluetoothStatusIconWidth + bluetoothGap; + } + if (showStatusBarTextLane && isPageBookmarked) { - const int bookmarkGap = leftClusterWidth > 0 ? bookmarkStatusIconGap : 0; + const int bookmarkGap = leftClusterWidth > 0 ? statusIconGap : 0; const int bookmarkX = leftClusterX + leftClusterWidth + bookmarkGap; const int bookmarkY = textY + 5; drawBookmarkStatusIcon(renderer, bookmarkX, bookmarkY); diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index b50bfc6d..47215505 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -238,7 +238,7 @@ class BaseTheme { void drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage, const int pageCount, std::string title, const int paddingBottom = 0, const int textYOffset = 0, const bool fillMargin = true, const bool isPageBookmarked = false, - const bool pageCountEstimated = false) const; + const bool pageCountEstimated = false, const bool bluetoothConnected = false) const; void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const; virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode = false, int contentStartX = 0, int contentWidth = 0) const; diff --git a/src/main.cpp b/src/main.cpp index ba056951..5a035381 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -571,7 +571,22 @@ void loop() { gpio.update(); updateBluetoothLifecycle(); // bring BLE up/down for the current activity context + static bool lastBleConnected = false; BleHid.poll(); // drive BLE auto-reconnect + key auto-repeat (no-op when BT off) + const bool bleConnected = BleHid.isConnected(); + if (bleConnected && !lastBleConnected) { + LOG_INF("BLELC", "connected name=%s heap=%u maxAlloc=%u", BleHid.connectedName(), ESP.getFreeHeap(), + ESP.getMaxAllocHeap()); + if (activityManager.isReaderActivity() && !activityManager.currentKeepsBluetoothAlive()) { + activityManager.requestUpdate(); + } + } else if (!bleConnected && lastBleConnected) { + LOG_INF("BLELC", "disconnected heap=%u maxAlloc=%u", ESP.getFreeHeap(), ESP.getMaxAllocHeap()); + if (activityManager.isReaderActivity() && !activityManager.currentKeepsBluetoothAlive()) { + activityManager.requestUpdate(); + } + } + lastBleConnected = bleConnected; mappedInputManager.pollBle(); // drain BLE keys -> logical-button overlay for this frame halTiltSensor.update(SETTINGS.tiltPageTurn, SETTINGS.orientation, activityManager.isReaderActivity());