Compare commits

..
Author SHA1 Message Date
Justin Mitchell 98118d6e24 Improve heap management in EPUB rendering
Fix CPU pinning by tracking buildHeapPaused state when background build is blocked. Re-check heap gate after acquiring lock to handle race conditions where rendering expands glyph buffers. Refine plane buffer allocation to check both free heap and max contiguous block, preventing fragmentation issues that could cause OOM during page renders. Skip async refresh for image pages since they use blocking double-FAST path.
2026-07-17 12:16:32 -04:00
Justin Mitchell 3cc4069faa fix: suppress cppcheck missingInclude on fresh checkouts
On a fresh CI checkout cppcheck has no resolved include paths, so it
reports every project header as missing (~400 information-level lines)
and --fail-on-defect low fails the job. Suppress it alongside
missingIncludeSystem, matching the fix already proven on the BLE branch.
2026-07-14 21:37:11 -04:00
Justin Mitchell a3b8897977 fix: address review findings on prewarm gating and gitignore
Revalidate the section under RenderLock before the idle prewarm scan,
add a largest-block floor to its heap gate, rebuild the differential
baseline when the grayscale scratch allocation fails after an async
refresh, and anchor generated-file gitignore patterns to the repo root.
2026-07-14 16:29:33 -04:00
Justin Mitchell f7f1a6f8bf Merge remote-tracking branch 'origin/develop' into feat-deferred-refresh 2026-07-14 15:49:04 -04:00
Justin Mitchell 7cad3cfb0e Fix heap fragmentation issues
- Keep-if-fits font buffer reuse (SdCardFont), the page-turn fragmentation fix
- Background-build heap floors + buildTickHeapGate() (the ParsedText::addWord abort fix), with the BLE-shed branch removed
- KOSync TLS gate split (free ≥ 50K, largest block ≥ 20K)
- wolfSSL SP ECC flags + FP_MAX_BITS 8192 in the patch script
- custom_sdkconfig: timer-stack trims (~7KB) and, per your answer, the WiFi IRAM opts (~25-30KB); plus the cloud-component removal the hybrid build requires. All verified present in the generated sdkconfig after the build.
- Web server watchdog registration fix (this branch's handlers already call esp_task_wdt_reset without it)
2026-07-14 15:45:44 -04:00
Uri Tauber f180069643 fix: ignore open-x4-sdk and fs_ (#2609) 2026-07-14 21:34:28 +03:00
Husam Younis a0ec5812b1 feat: Arabic translation YAML - PR 3/3 (#2599) 2026-07-14 20:33:42 +03:00
Phạm Bình An e84840b473 fix(css-parser): don't save unusable rules to RAM (#2604) 2026-07-14 17:00:13 +03:00
Tom-Inge Larsen 94ae750e89 feat(i18n): add Norwegian Bokmål translation (#2113) 2026-07-14 16:54:43 +03:00
metoli86andmetoli 8c5cd67f38 feat: complete Turkish translation (390/390) (#2592)
Co-authored-by: metoli <metoli@metoli-Mac-mini.local>
2026-07-14 16:50:03 +03:00
Justin Mitchell e66de575a1 Add async display refresh support to HalDisplay
Introduces non-blocking refresh capability with displayBufferAsync() that starts panel waveform and returns immediately, plus waitRefreshComplete() to block until refresh finishes. Panels without async support fall back to blocking refresh. Requires framebuffer to remain untouched during refresh and differential baseline rebuild before next update.
2026-07-13 16:20:20 -04:00
Justin Mitchell bae29fc6ba Update freeink-sdk
1. X3 wb_gc passive byte (the one-byte 0x54 to 0x00, matching stock's passive WB table in the gc nudge bank) Every X3 grayscale render, reader AA, covers, and the sleep-cover nudge, gets it. 2. X4 blotchy grays on sleep covers: the displayGrayscaleBase(HALF_REFRESH) revert (the gray nudge LUT being calibrated against the HALF pixel state, not FULL's)
2026-07-13 02:23:59 -04:00
Justin Mitchell 379f49d165 Use HALF_REFRESH for sleep screens instead of FULL
Changes all sleep screen display refreshes from FULL_REFRESH to HALF_REFRESH to match OEM firmware behavior. The stock firmware uses a single-pass 0xD7 waveform for sleep screens, not the multi-flash GC waveform (0xF7) that FULL_REFRESH triggers. For grayscale mode, HALF_REFRESH is required because the gray nudge LUT is calibrated against the pixel charge state left by the single-pass waveform; using FULL_REFRESH causes blotchy noise in gray areas.
2026-07-13 01:08:05 -04:00
54 changed files with 1025 additions and 1875 deletions
+6 -4
View File
@@ -2,6 +2,8 @@
.idea .idea
.DS_Store .DS_Store
.vscode .vscode
open-x4-sdk
fs_
lib/EpdFont/fontsrc lib/EpdFont/fontsrc
lib/I18n/I18nKeys.h lib/I18n/I18nKeys.h
lib/I18n/I18nStrings.h lib/I18n/I18nStrings.h
@@ -25,7 +27,7 @@ lib/EpdFont/scripts/output/
!.claude/skills/ !.claude/skills/
/managed_components /managed_components
/.dummy /.dummy
dependencies.lock /CMakeLists.txt
sdkconfig.default /dependencies.lock
sdkconfig.defaults /sdkconfig.default
CMakeLists.txt /sdkconfig.defaults
-5
View File
@@ -369,11 +369,6 @@ int FontDecompressor::prewarmCache(const EpdFontData* fontData, const char* utf8
} }
stats.pageBufferBytes += totalBytes; stats.pageBufferBytes += totalBytes;
stats.pageGlyphsBytes += glyphCount * sizeof(PageGlyphEntry); stats.pageGlyphsBytes += glyphCount * sizeof(PageGlyphEntry);
// MEMFIX-PORT: page-slot address landmark for the heap map; portable
// Landmark for the heap block map: page slots are the largest flash-font
// allocations and otherwise show up as anonymous ~4-20 KB used blocks.
LOG_DBG("FDC", "page slot buffer=%p bytes=%u glyphs=%u", static_cast<void*>(slot.buffer), (unsigned)totalBytes,
(unsigned)glyphCount);
slot.fontData = fontData; slot.fontData = fontData;
slot.glyphCount = glyphCount; slot.glyphCount = glyphCount;
-27
View File
@@ -1406,33 +1406,6 @@ const EpdGlyph* SdCardFont::onGlyphMiss(void* ctx, uint32_t codepoint) {
return &self->overflow_[slot].glyph; return &self->overflow_[slot].glyph;
} }
size_t SdCardFont::reportMemory() const {
size_t total = 0;
for (uint8_t si = 0; si < MAX_STYLES; ++si) {
const auto& s = styles_[si];
if (!s.present) continue;
size_t fixed = 0; // loaded once per family: interval/kern/lig tables
if (s.fullIntervals) fixed += s.header.intervalCount * sizeof(EpdUnicodeInterval);
if (s.bmpIntervals) fixed += s.header.intervalCount * sizeof(PerStyle::BmpInterval16);
if (s.kernLeftClasses) fixed += s.header.kernLeftEntryCount * sizeof(EpdKernClassEntry);
if (s.kernRightClasses) fixed += s.header.kernRightEntryCount * sizeof(EpdKernClassEntry);
if (s.ligaturePairs) fixed += s.header.ligaturePairCount * sizeof(EpdLigaturePair);
// kept-if-fits mini arenas: capacity (not count) is what stays resident
size_t mini = s.miniIntervalCapacity * sizeof(EpdUnicodeInterval) + s.miniGlyphCapacity * sizeof(EpdGlyph) +
s.miniBitmapCapacity + s.miniKernLeftCapacity * sizeof(EpdKernClassEntry) +
s.miniKernRightCapacity * sizeof(EpdKernClassEntry) + s.miniKernMatrixCapacity;
const size_t adv = advanceTableSize_[si] * sizeof(AdvanceEntry);
LOG_DBG("SDCF", "mem style%u: fixed=%u mini=%u adv=%u", si, (unsigned)fixed, (unsigned)mini, (unsigned)adv);
total += fixed + mini + adv;
}
size_t overflowBytes = 0;
for (uint32_t i = 0; i < overflowCount_; ++i) {
if (overflow_[i].bitmap) overflowBytes += overflow_[i].glyph.dataLength;
}
total += overflowBytes + overflowCount_ * sizeof(OverflowEntry);
return total;
}
bool SdCardFont::isOverflowGlyph(const EpdGlyph* glyph) const { bool SdCardFont::isOverflowGlyph(const EpdGlyph* glyph) const {
for (uint32_t i = 0; i < overflowCount_; i++) { for (uint32_t i = 0; i < overflowCount_; i++) {
if (&overflow_[i].glyph == glyph) return true; if (&overflow_[i].glyph == glyph) return true;
-5
View File
@@ -104,11 +104,6 @@ class SdCardFont {
uint32_t uniqueGlyphs = 0; uint32_t uniqueGlyphs = 0;
uint32_t bitmapBytes = 0; uint32_t bitmapBytes = 0;
}; };
// MEMFIX-PORT: SD font resident-bytes audit; portable
// Log per-style resident heap (full tables + kept-if-fits mini arenas +
// advance tables + overflow bitmaps) and return the total in bytes. Pure
// accounting — no allocation, no state change.
size_t reportMemory() const;
void logStats(const char* label = "SDCF"); void logStats(const char* label = "SDCF");
void resetStats(); void resetStats();
const Stats& getStats() const { return stats_; } const Stats& getStats() const { return stats_; }
-8
View File
@@ -88,14 +88,6 @@ void SdCardFontManager::unloadAll(GfxRenderer& renderer) {
loadedPointSize_ = 0; loadedPointSize_ = 0;
} }
size_t SdCardFontManager::reportMemory() const {
size_t total = 0;
for (const auto& lf : loaded_) {
if (lf.font) total += lf.font->reportMemory();
}
return total;
}
int SdCardFontManager::getFontId(const std::string& familyName) const { int SdCardFontManager::getFontId(const std::string& familyName) const {
if (familyName != loadedFamilyName_ || loaded_.empty()) return 0; if (familyName != loadedFamilyName_ || loaded_.empty()) return 0;
return loaded_.front().fontId; return loaded_.front().fontId;
-4
View File
@@ -32,10 +32,6 @@ class SdCardFontManager {
// Get name of currently loaded family (empty if none). // Get name of currently loaded family (empty if none).
const std::string& currentFamilyName() const { return loadedFamilyName_; }; const std::string& currentFamilyName() const { return loadedFamilyName_; };
// MEMFIX-PORT: font manager audit passthrough; portable
// Sum of loaded fonts' resident heap (see SdCardFont::reportMemory).
size_t reportMemory() const;
// Point size that was actually loaded. // Point size that was actually loaded.
// 0 if nothing loaded. // 0 if nothing loaded.
uint8_t currentPointSize() const { return loadedPointSize_; }; uint8_t currentPointSize() const { return loadedPointSize_; };
-12
View File
@@ -44,18 +44,6 @@ class Epub {
} }
~Epub() = default; ~Epub() = default;
std::string& getBasePath() { return contentBasePath; } std::string& getBasePath() { return contentBasePath; }
// MEMFIX-PORT: epub resident-bytes audit accessor; portable
// Approximate resident heap of the open book (audit): path strings, the CSS
// file list, and the parsed stylesheet. BookMetadataCache is file-backed
// (counts + HalFile handles) and contributes little.
size_t residentBytes() const {
size_t total = sizeof(Epub) + tocNcxItem.capacity() + tocNavItem.capacity() + filepath.capacity() +
contentBasePath.capacity() + cachePath.capacity();
for (const auto& f : cssFiles) total += sizeof(f) + (f.capacity() > 15 ? f.capacity() : 0);
if (cssParser) total += cssParser->residentBytes();
return total;
}
size_t cssRuleCount() const { return cssParser ? cssParser->ruleCount() : 0; }
bool load(bool buildIfMissing = true, bool skipLoadingCss = false); bool load(bool buildIfMissing = true, bool skipLoadingCss = false);
bool clearCache() const; bool clearCache() const;
void setupCacheDir() const; void setupCacheDir() const;
-4
View File
@@ -173,10 +173,6 @@ std::unique_ptr<Page> Page::deserialize(HalFile& file) {
uint16_t count; uint16_t count;
serialization::readPod(file, count); serialization::readPod(file, count);
// Reserve up front: growth-by-doubling needs old + new capacity live at once and
// reallocates repeatedly — a field crash (bad_alloc -> abort under -fno-exceptions)
// hit exactly this append path on a heavily fragmented heap.
page->elements.reserve(count);
for (uint16_t i = 0; i < count; i++) { for (uint16_t i = 0; i < count; i++) {
uint8_t tag; uint8_t tag;
-15
View File
@@ -72,7 +72,6 @@ class Section {
// Builds write here and are swapped over filePath only on commit, so a prior // Builds write here and are swapped over filePath only on commit, so a prior
// partial/finalized file stays readable while a rebuild is in progress. // partial/finalized file stays readable while a rebuild is in progress.
std::string binTmpPath() const { return filePath + ".part"; } std::string binTmpPath() const { return filePath + ".part"; }
std::unique_ptr<Page> loadPageAt(int page) const; std::unique_ptr<Page> loadPageAt(int page) const;
// Read a page already laid out by the in-progress build (page < build LUT size), from // Read a page already laid out by the in-progress build (page < build LUT size), from
// the partially-written tmp .bin without disturbing the build's write cursor. // the partially-written tmp .bin without disturbing the build's write cursor.
@@ -132,20 +131,6 @@ class Section {
// (covers finalized sections and partials from a previous session). // (covers finalized sections and partials from a previous session).
std::optional<uint16_t> findAnchor(const std::string& anchor) const; std::optional<uint16_t> findAnchor(const std::string& anchor) const;
// MEMFIX-PORT: section resident-bytes audit accessor; portable
// Approximate resident heap for the audit log. Steady state (no build) a
// Section holds little beyond itself; during a build the page LUT and path
// strings dominate (the parser's internal footprint is not walked here).
size_t residentBytes() const {
size_t total = sizeof(Section) + filePath.capacity();
if (build_) {
total += sizeof(BuildContext) + build_->lut.capacity() * sizeof(PageLutEntry) +
build_->parsePath.capacity() + build_->contentBase.capacity() + build_->imageBasePath.capacity() +
build_->htmlPath.capacity() + build_->tmpHtmlPath.capacity();
}
return total;
}
// True if this spine's unzipped HTML is already cached, so a build won't pay the (multi-second on a // True if this spine's unzipped HTML is already cached, so a build won't pay the (multi-second on a
// giant spine) zip inflation. Lets the reader skip the indexing popup on a fast reopen/rebuild. // giant spine) zip inflation. Lets the reader skip the indexing popup on a fast reopen/rebuild.
bool hasHtmlCache() const; bool hasHtmlCache() const;
+5
View File
@@ -431,6 +431,11 @@ CssStyle CssParser::parseDeclarations(std::string_view declBlock) {
// Rule processing // Rule processing
void CssParser::processRuleBlockWithStyle(std::string_view selectorGroup, const CssStyle& style) { void CssParser::processRuleBlockWithStyle(std::string_view selectorGroup, const CssStyle& style) {
// Skip rules that don't define any supported properties to save RAM.
if (!style.defined.anySet()) {
return;
}
// Check if we've reached the rule limit before processing // Check if we've reached the rule limit before processing
if (rulesBySelector_.size() >= MAX_RULES) { if (rulesBySelector_.size() >= MAX_RULES) {
LOG_DBG("CSS", "Reached max rules limit (%zu), stopping CSS parsing", MAX_RULES); LOG_DBG("CSS", "Reached max rules limit (%zu), stopping CSS parsing", MAX_RULES);
+1 -15
View File
@@ -33,7 +33,7 @@
class CssParser { class CssParser {
public: public:
// Bump when CSS cache format or rules change; section caches are invalidated when this changes // Bump when CSS cache format or rules change; section caches are invalidated when this changes
static constexpr uint8_t CSS_CACHE_VERSION = 7; static constexpr uint8_t CSS_CACHE_VERSION = 8;
explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {} explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {}
~CssParser() = default; ~CssParser() = default;
@@ -67,20 +67,6 @@ class CssParser {
*/ */
[[nodiscard]] static CssStyle parseInlineStyle(std::string_view styleValue); [[nodiscard]] static CssStyle parseInlineStyle(std::string_view styleValue);
// MEMFIX-PORT: stylesheet resident-bytes audit accessor; portable
// Approximate resident heap of the parsed stylesheet, for the audit log.
// unordered_map cost model: bucket array + one node per rule (libstdc++ node
// overhead ~= 2 pointers + hash) + key string capacity when it exceeds SSO.
size_t residentBytes() const {
size_t total = rulesBySelector_.bucket_count() * sizeof(void*);
for (const auto& kv : rulesBySelector_) {
total += sizeof(void*) * 2 + sizeof(size_t); // node overhead
total += sizeof(kv);
if (kv.first.capacity() > 15) total += kv.first.capacity(); // beyond SSO
}
return total;
}
/** /**
* Check if any rules have been loaded * Check if any rules have been loaded
*/ */
+8 -14
View File
@@ -62,14 +62,7 @@ void FontCacheManager::resetStats() {
bool FontCacheManager::isScanning() const { return scanMode_ == ScanMode::Scanning; } bool FontCacheManager::isScanning() const { return scanMode_ == ScanMode::Scanning; }
void FontCacheManager::recordText(const char* text, int fontId, EpdFontFamily::Style style) { void FontCacheManager::recordText(const char* text, int fontId, EpdFontFamily::Style style) {
if (!text) return; scanText_ += text;
const size_t remaining = (scanTextLen_ < SCAN_TEXT_CAPACITY - 1) ? (SCAN_TEXT_CAPACITY - 1 - scanTextLen_) : 0;
if (remaining > 0) {
const size_t textLen = strnlen(text, remaining);
memcpy(scanText_ + scanTextLen_, text, textLen);
scanTextLen_ += textLen;
scanText_[scanTextLen_] = '\0';
}
if (scanFontId_ < 0) scanFontId_ = fontId; if (scanFontId_ < 0) scanFontId_ = fontId;
const uint8_t baseStyle = static_cast<uint8_t>(style) & 0x03; const uint8_t baseStyle = static_cast<uint8_t>(style) & 0x03;
const unsigned char* p = reinterpret_cast<const unsigned char*>(text); const unsigned char* p = reinterpret_cast<const unsigned char*>(text);
@@ -87,15 +80,15 @@ FontCacheManager::PrewarmScope::PrewarmScope(FontCacheManager& manager) : manage
manager_->scanMode_ = ScanMode::Scanning; manager_->scanMode_ = ScanMode::Scanning;
manager_->clearCache(); manager_->clearCache();
manager_->resetStats(); manager_->resetStats();
manager_->scanTextLen_ = 0; manager_->scanText_.clear();
manager_->scanText_[0] = '\0'; manager_->scanText_.reserve(2048); // Pre-allocate to avoid heap fragmentation from repeated concat
memset(manager_->scanStyleCounts_, 0, sizeof(manager_->scanStyleCounts_)); memset(manager_->scanStyleCounts_, 0, sizeof(manager_->scanStyleCounts_));
manager_->scanFontId_ = -1; manager_->scanFontId_ = -1;
} }
void FontCacheManager::PrewarmScope::endScanAndPrewarm() { void FontCacheManager::PrewarmScope::endScanAndPrewarm() {
manager_->scanMode_ = ScanMode::None; manager_->scanMode_ = ScanMode::None;
if (manager_->scanTextLen_ == 0) return; if (manager_->scanText_.empty()) return;
// Build style bitmask from all styles that appeared during the scan // Build style bitmask from all styles that appeared during the scan
uint8_t styleMask = 0; uint8_t styleMask = 0;
@@ -104,10 +97,11 @@ void FontCacheManager::PrewarmScope::endScanAndPrewarm() {
} }
if (styleMask == 0) styleMask = 1; // default to regular if (styleMask == 0) styleMask = 1; // default to regular
manager_->prewarmCache(manager_->scanFontId_, manager_->scanText_, styleMask); manager_->prewarmCache(manager_->scanFontId_, manager_->scanText_.c_str(), styleMask);
manager_->scanTextLen_ = 0; // Free scan string memory
manager_->scanText_[0] = '\0'; manager_->scanText_.clear();
manager_->scanText_.shrink_to_fit();
} }
FontCacheManager::PrewarmScope::~PrewarmScope() { FontCacheManager::PrewarmScope::~PrewarmScope() {
+2 -4
View File
@@ -2,9 +2,9 @@
#include <EpdFontFamily.h> #include <EpdFontFamily.h>
#include <cstddef>
#include <cstdint> #include <cstdint>
#include <map> #include <map>
#include <string>
class FontDecompressor; class FontDecompressor;
class SdCardFont; class SdCardFont;
@@ -51,9 +51,7 @@ class FontCacheManager {
enum class ScanMode : uint8_t { None, Scanning }; enum class ScanMode : uint8_t { None, Scanning };
ScanMode scanMode_ = ScanMode::None; ScanMode scanMode_ = ScanMode::None;
static constexpr size_t SCAN_TEXT_CAPACITY = 2048; std::string scanText_;
char scanText_[SCAN_TEXT_CAPACITY] = {};
size_t scanTextLen_ = 0;
uint32_t scanStyleCounts_[4] = {}; uint32_t scanStyleCounts_[4] = {};
int scanFontId_ = -1; int scanFontId_ = -1;
}; };
+394
View File
@@ -0,0 +1,394 @@
_language_name: "العربية"
_language_code: "AR"
_order: "28"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "جاري بدء التشغيل"
STR_SLEEPING: "وضع السكون"
STR_ENTERING_SLEEP: "جاري الدخول في وضع السكون"
STR_BROWSE_FILES: "تصفح الملفات"
STR_FILE_TRANSFER: "نقل الملفات"
STR_SETTINGS_TITLE: "الإعدادات"
STR_CONTINUE_READING: "متابعة القراءة"
STR_NO_OPEN_BOOK: "لا يوجد كتاب مفتوح"
STR_START_READING: "ابدأ القراءة أدناه"
STR_NO_FILES_FOUND: "لم يتم العثور على ملفات"
STR_SELECT_CHAPTER: "اختر الفصل"
STR_NO_CHAPTERS: "لا توجد فصول"
STR_END_OF_BOOK: "نهاية الكتاب"
STR_EMPTY_CHAPTER: "فصل فارغ"
STR_INDEXING: "جاري الفهرسة"
STR_INDEX_FAILED: "فشلت الفهرسة - كتاب غير صالح"
STR_MEMORY_ERROR: "خطأ في الذاكرة"
STR_PAGE_LOAD_ERROR: "خطأ في تحميل الصفحة"
STR_EMPTY_FILE: "ملف فارغ"
STR_OUT_OF_BOUNDS: "خارج النطاق"
STR_LOADING: "جاري التحميل..."
STR_LOADING_POPUP: "جاري التحميل"
STR_WIFI_NETWORKS: "شبكات Wi-Fi"
STR_NO_NETWORKS: "لم يتم العثور على شبكات"
STR_NETWORKS_FOUND: "تم العثور على %zu شبكة"
STR_SCANNING: "جاري البحث..."
STR_FINDING_SAVED_WIFI: "جاري البحث عن شبكة Wi-Fi محفوظة..."
STR_CONNECTING: "جاري الاتصال..."
STR_CONNECTING_SAVED_WIFI: "جاري الاتصال بشبكة Wi-Fi محفوظة..."
STR_SHOW_NETWORKS: "عرض"
STR_CONNECTED: "تم الاتصال!"
STR_CONNECTION_FAILED: "فشل الاتصال"
STR_FORGET_NETWORK: "نسيان هذه الشبكة؟"
STR_SAVE_PASSWORD: "حفظ كلمة المرور للمرة القادمة؟"
STR_PRESS_OK_SCAN: "اضغط موافق لإعادة البحث"
STR_JOIN_NETWORK: "الانضمام إلى شبكة"
STR_CREATE_HOTSPOT: "إنشاء نقطة اتصال"
STR_JOIN_DESC: "الاتصال بشبكة Wi-Fi موجودة"
STR_HOTSPOT_DESC: "إنشاء شبكة Wi-Fi يمكن للآخرين الانضمام إليها"
STR_STARTING_HOTSPOT: "جاري تشغيل نقطة الاتصال..."
STR_HOTSPOT_MODE: "وضع نقطة الاتصال"
STR_CONNECT_WIFI_HINT: "قم بتوصيل جهازك بشبكة Wi-Fi هذه"
STR_OPEN_URL_HINT: "افتح هذا العنوان في المتصفح"
STR_OR_HTTP_PREFIX: "أو http://"
STR_SCAN_QR_HINT: "أو امسح رمز QR بهاتفك:"
STR_CALIBRE_WIRELESS: "اتصال Calibre اللاسلكي"
STR_NETWORK_LEGEND: "* = مشفرة | + = محفوظة"
STR_MAC_ADDRESS: "عنوان MAC:"
STR_CHECKING_WIFI: "جاري فحص Wi-Fi..."
STR_ENTER_WIFI_PASSWORD: "أدخل كلمة مرور الشبكة"
STR_ADD_HIDDEN_NETWORK: "إضافة شبكة مخفية..."
STR_ENTER_WIFI_SSID: "أدخل اسم الشبكة (SSID)"
STR_TO_PREFIX: "إلى "
STR_CALIBRE_RECEIVING: "جاري الاستلام: "
STR_CALIBRE_RECEIVED: "تم الاستلام: "
STR_CALIBRE_INSTRUCTION_1: "1) قم بتثبيت إضافة CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) اتصل بنفس شبكة Wi-Fi"
STR_CALIBRE_INSTRUCTION_3: "3) في Calibre: اختر \"إرسال إلى الجهاز\""
STR_CALIBRE_INSTRUCTION_4: "اترك هذه الشاشة مفتوحة أثناء الإرسال"
STR_CAT_DISPLAY: "الشاشة"
STR_CAT_READER: "القراءة"
STR_CAT_CONTROLS: "الأزرار"
STR_CAT_SYSTEM: "النظام"
STR_SLEEP_SCREEN: "شاشة السكون"
STR_QUICK_RESUME_TIMEOUT: "استئناف سريع بعد المهلة"
STR_SLEEP_COVER_MODE: "عرض الغلاف في وضع السكون"
STR_HIDE_BATTERY: "إخفاء نسبة البطارية"
STR_EXTRA_SPACING: "تباعد إضافي بين الفقرات"
STR_TEXT_AA: "تنعيم حواف النص"
STR_IMAGES: "الصور"
STR_IMAGES_DISPLAY: "عرض"
STR_IMAGES_PLACEHOLDER: "عنصر نائب"
STR_IMAGES_SUPPRESS: "إخفاء"
STR_EOB_HOME: "الرئيسية"
STR_EOB_CONTINUE_WITH: "المتابعة إلى"
STR_SHORT_PWR_BTN: "ضغطة قصيرة على زر التشغيل"
STR_ORIENTATION: "اتجاه القراءة"
STR_SIDE_BTN_LAYOUT: "تخطيط الأزرار الجانبية (أثناء القراءة)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "تدوير الأزرار الأمامية مع الشاشة"
STR_LONG_PRESS_BEHAVIOR: "سلوك الضغطة الطويلة"
STR_LONG_PRESS_BEHAVIOR_OFF: "إيقاف"
STR_LONG_PRESS_BEHAVIOR_SKIP: "تخطي الفصل"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "تغيير الاتجاه"
STR_LONG_PRESS_MENU: "قائمة الضغطة الطويلة"
STR_FONT_PREVIEW_TEXT: "نص حكيم له سر قاطع وذو شأن عظيم مكتوب على ثوب أخضر ومغلف بجلد أزرق"
STR_FONT_FAMILY: "خط القراءة"
STR_FONT_SIZE: "حجم الخط"
STR_LINE_SPACING: "تباعد الأسطر"
STR_SCREEN_MARGIN: "هوامش شاشة القراءة"
STR_PARA_ALIGNMENT: "محاذاة الفقرات"
STR_HYPHENATION: "تقسيم الكلمات"
STR_TIME_TO_SLEEP: "مهلة الدخول في السكون"
STR_SHOW_HIDDEN_FILES: "عرض الملفات المخفية"
STR_REMOVE_READ_FROM_RECENTS: "إزالة الكتب المقروءة من قائمة الكتب الأخيرة"
STR_MOVE_FINISHED_TO_READ: "نقل الكتب المنتهية إلى مجلد المقروءة"
STR_REFRESH_FREQ: "معدل تحديث الشاشة"
STR_KOREADER_SYNC: "مزامنة KOReader"
STR_CHECK_UPDATES: "التحقق من التحديثات"
STR_LANGUAGE: "اللغة"
STR_CLEAR_READING_CACHE: "مسح ذاكرة القراءة المؤقتة"
STR_USERNAME: "اسم المستخدم"
STR_PASSWORD: "كلمة المرور"
STR_SYNC_SERVER_URL: "عنوان خادم المزامنة"
STR_DOCUMENT_MATCHING: "مطابقة المستندات"
STR_SEND_METADATA: "إرسال البيانات الوصفية للمستند"
STR_AUTHENTICATE: "تسجيل الدخول"
STR_KOREADER_USERNAME: "اسم مستخدم KOReader"
STR_KOREADER_PASSWORD: "كلمة مرور KOReader"
STR_FILENAME: "اسم الملف"
STR_BINARY: "ثنائي"
STR_SET_CREDENTIALS_FIRST: "أدخل بيانات الاعتماد أولا"
STR_WIFI_CONN_FAILED: "فشل الاتصال بالشبكة"
STR_AUTHENTICATING: "جاري التحقق..."
STR_AUTH_SUCCESS: "تم تسجيل الدخول بنجاح!"
STR_KOREADER_AUTH: "تسجيل الدخول إلى KOReader"
STR_SYNC_READY: "مزامنة KOReader جاهزة للاستخدام"
STR_AUTH_FAILED: "فشل تسجيل الدخول"
STR_DONE: "تم"
STR_CLEAR_CACHE_WARNING_1: "سيؤدي هذا إلى مسح جميع بيانات الكتب المخزنة."
STR_CLEAR_CACHE_WARNING_2: "سيتم فقدان كل تقدم القراءة!"
STR_CLEAR_CACHE_WARNING_3: "ستحتاج الكتب إلى إعادة فهرسة"
STR_CLEAR_CACHE_WARNING_4: "عند فتحها مرة أخرى."
STR_CLEARING_CACHE: "جاري مسح الذاكرة المؤقتة..."
STR_CACHE_CLEARED: "تم مسح الذاكرة المؤقتة"
STR_ITEMS_REMOVED: "عناصر تمت إزالتها"
STR_FAILED_LOWER: "فشل"
STR_CLEAR_CACHE_FAILED: "فشل مسح الذاكرة المؤقتة"
STR_CHECK_SERIAL_OUTPUT: "راجع مخرجات المنفذ التسلسلي للتفاصيل"
STR_DARK: "داكن"
STR_LIGHT: "فاتح"
STR_CUSTOM: "مخصص"
STR_COVER: "الغلاف"
STR_NONE_OPT: "بدون"
STR_FIT: "ملاءمة"
STR_CROP: "قص"
STR_NEVER: "أبدا"
STR_IN_READER: "أثناء القراءة"
STR_ALWAYS: "دائما"
STR_IGNORE: "تجاهل"
STR_SLEEP: "سكون"
STR_PAGE_TURN: "تقليب الصفحة"
STR_FORCE_REFRESH: "تحديث الشاشة"
STR_PORTRAIT: "عمودي"
STR_LANDSCAPE_CW: "أفقي (يمين)"
STR_INVERTED: "عكس الألوان"
STR_ORIENTATION_INVERTED: "عمودي 180°"
STR_LANDSCAPE_CCW: "أفقي (يسار)"
STR_PREV_NEXT: "السابق/التالي"
STR_NEXT_PREV: "التالي/السابق"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "إشارة مرجعية"
STR_DISABLED: "معطل"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "صغير"
STR_MEDIUM: "متوسط"
STR_LARGE: "كبير"
STR_X_LARGE: "كبير جدا"
STR_TIGHT: "ضيق"
STR_NORMAL: "عادي"
STR_WIDE: "واسع"
STR_JUSTIFY: "ضبط الطرفين"
STR_ALIGN_LEFT: "يسار"
STR_CENTER: "وسط"
STR_ALIGN_RIGHT: "يمين"
STR_PAGES_1: "صفحة واحدة"
STR_PAGES_5: "5 صفحات"
STR_PAGES_10: "10 صفحات"
STR_PAGES_15: "15 صفحة"
STR_PAGES_30: "30 صفحة"
STR_UPDATE: "تحديث"
STR_CHECKING_UPDATE: "جاري التحقق من التحديثات..."
STR_NEW_UPDATE: "يتوفر تحديث جديد!"
STR_CURRENT_VERSION: "الإصدار الحالي: "
STR_NEW_VERSION: "الإصدار الجديد: "
STR_UPDATING: "جاري التحديث..."
STR_NO_UPDATE: "لا يتوفر تحديث"
STR_UPDATE_FAILED: "فشل التحديث"
STR_UPDATE_COMPLETE: "اكتمل التحديث"
STR_POWER_ON_HINT: "اضغط مطولا على زر التشغيل لتشغيل الجهاز من جديد"
STR_RESTARTING_HINT: "جاري إعادة التشغيل... إذا لم يعمل الجهاز، اضغط مطولا على زر التشغيل لعدة ثوان."
STR_NO_ENTRIES: "لم يتم العثور على عناصر"
STR_DOWNLOADING: "جاري التنزيل..."
STR_DOWNLOAD_FAILED: "فشل التنزيل"
STR_ERROR_MSG: "خطأ:"
STR_UNNAMED: "بدون اسم"
STR_HOLD_OPEN_TO_DELETE: "اضغط مطولا على فتح للحذف"
STR_NO_SERVER_URL: "لم يتم تحديد عنوان الخادم"
STR_FETCH_FEED_FAILED: "فشل جلب القائمة"
STR_PARSE_FEED_FAILED: "فشل تحليل القائمة"
STR_NEXT_PAGE: "الصفحة التالية"
STR_PREV_PAGE: "الصفحة السابقة"
STR_NETWORK_PREFIX: "الشبكة: "
STR_IP_ADDRESS_PREFIX: "عنوان IP: "
STR_ERROR_GENERAL_FAILURE: "خطأ: فشل عام"
STR_ERROR_NETWORK_NOT_FOUND: "خطأ: الشبكة غير موجودة"
STR_ERROR_CONNECTION_TIMEOUT: "خطأ: انتهت مهلة الاتصال"
STR_SD_CARD: "بطاقة SD"
STR_BACK: "رجوع »"
STR_EXIT: "خروج »"
STR_HOME: "الرئيسية »"
STR_SELECT: "اختيار"
STR_SELECTED: "محدد"
STR_TOGGLE: "تبديل"
STR_TOGGLE_BOOKMARK: "إضافة/إزالة إشارة مرجعية"
STR_CONFIRM: "تأكيد"
STR_CANCEL: "إلغاء"
STR_CONNECT: "اتصال"
STR_OPEN: "فتح"
STR_DOWNLOAD: "تنزيل"
STR_RETRY: "إعادة المحاولة"
STR_YES: "نعم"
STR_NO: "لا"
STR_SHOW: "عرض"
STR_HIDE: "إخفاء"
STR_STATE_ON: "تشغيل"
STR_STATE_OFF: "إيقاف"
STR_NOT_SET: "غير محدد"
STR_DIR_LEFT: "يسار"
STR_DIR_RIGHT: "يمين"
STR_DIR_UP: "أعلى"
STR_DIR_DOWN: "أسفل"
STR_OK_BUTTON: "موافق"
STR_SLEEP_COVER_FILTER: "مرشح غلاف شاشة السكون"
STR_FILTER_CONTRAST: "التباين"
STR_CUSTOMISE_STATUS_BAR: "تخصيص شريط الحالة"
STR_CHAPTER_PAGE_COUNT: "عدد صفحات الفصل"
STR_BOOK_PROGRESS_PERCENTAGE: "نسبة التقدم في الكتاب"
STR_PROGRESS_BAR: "شريط التقدم"
STR_PROGRESS_BAR_THICKNESS: "سمك شريط التقدم"
STR_PROGRESS_BAR_THIN: "رفيع"
STR_PROGRESS_BAR_MEDIUM: "متوسط"
STR_PROGRESS_BAR_THICK: "سميك"
STR_BOOK: "الكتاب"
STR_CHAPTER: "الفصل"
STR_EXAMPLE_CHAPTER: "الفصل 21"
STR_EXAMPLE_BOOK: "عنوان الكتاب"
STR_PREVIEW: "معاينة"
STR_TITLE: "العنوان"
STR_BATTERY: "البطارية"
STR_XTC_STATUS_BAR: "شريط حالة XTC"
STR_BOTTOM: "أسفل"
STR_TOP: "أعلى"
STR_CLOCK: "الساعة"
STR_CLOCK_UTC_OFFSET: "فرق التوقيت عن UTC"
STR_CLOCK_FORMAT: "تنسيق الساعة"
STR_CLOCK_FORMAT_24H: "24 ساعة"
STR_CLOCK_FORMAT_12H: "12 ساعة"
STR_CURRENT_TIME: "الوقت الحالي:"
STR_NEXT_FIELD: "التالي"
STR_CLOCK_SYNC: "مزامنة الساعة"
STR_CLOCK_SYNC_NOW: "مزامنة الساعة الآن"
STR_CLOCK_SYNCING: "جاري المزامنة عبر NTP..."
STR_CLOCK_SYNC_OK: "تمت مزامنة الساعة"
STR_CLOCK_SYNC_FAIL: "فشلت المزامنة"
STR_CLOCK_SYNC_NO_WIFI: "لا يوجد اتصال Wi-Fi"
STR_CLOCK_SYNC_NO_WIFI_HINT: "اتصل بشبكة Wi-Fi أولا، ثم حاول مرة أخرى."
STR_CLOCK_SYNCED: "تمت مزامنة الساعة"
STR_UI_THEME: "مظهر الواجهة"
STR_THEME_CLASSIC: "كلاسيكي"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra موسع"
STR_SUNLIGHT_FADING_FIX: "إصلاح بهتان الشاشة في الشمس"
STR_REMAP_FRONT_BUTTONS: "تغيير وظائف الأزرار الأمامية"
STR_BOOKMARKS: "الإشارات المرجعية"
STR_BOOKMARK_ADDED: "تمت إضافة الإشارة المرجعية."
STR_BOOKMARK_REMOVED: "تمت إزالة الإشارة المرجعية."
STR_OPDS_BROWSER: "متصفح OPDS"
STR_SEARCH: "بحث"
STR_COVER_CUSTOM: "الغلاف + مخصص"
STR_QUICK_RESUME: "استئناف سريع"
STR_MENU_RECENT_BOOKS: "الكتب الأخيرة"
STR_REMOVE_FROM_RECENTS: "إزالة من الكتب الأخيرة؟"
STR_NO_RECENT_BOOKS: "لا توجد كتب أخيرة"
STR_CALIBRE_DESC: "استخدم النقل اللاسلكي من Calibre"
STR_FORGET_AND_REMOVE: "نسيان الشبكة وحذف كلمة المرور المحفوظة؟"
STR_FORGET_BUTTON: "نسيان"
STR_CALIBRE_STARTING: "جاري تشغيل Calibre..."
STR_CALIBRE_SETUP: "الإعداد"
STR_CALIBRE_STATUS: "الحالة"
STR_CLEAR_BUTTON: "مسح"
STR_DEFAULT_VALUE: "افتراضي"
STR_REMAP_PROMPT: "اضغط زرا أماميا لكل وظيفة"
STR_UNASSIGNED: "غير معين"
STR_ALREADY_ASSIGNED: "معين مسبقا"
STR_REMAP_RESET_HINT: "الزر الجانبي العلوي: استعادة التخطيط الافتراضي"
STR_REMAP_CANCEL_HINT: "الزر الجانبي السفلي: إلغاء التغيير"
STR_HW_BACK_LABEL: "رجوع (الزر 1)"
STR_HW_CONFIRM_LABEL: "تأكيد (الزر 2)"
STR_HW_LEFT_LABEL: "يسار (الزر 3)"
STR_HW_RIGHT_LABEL: "يمين (الزر 4)"
STR_GO_TO_PERCENT: "الانتقال إلى نسبة %"
STR_GO_HOME_BUTTON: "العودة إلى الرئيسية"
STR_SYNC_PROGRESS: "مزامنة التقدم"
STR_DELETE_CACHE: "حذف ذاكرة الكتاب المؤقتة"
STR_DELETE: "حذف"
STR_CONFIRM_DELETE_BOOKMARK: "حذف هذه الإشارة المرجعية؟"
STR_DISPLAY_QR: "عرض الصفحة كرمز QR"
STR_CHAPTER_PREFIX: "الفصل: "
STR_PAGES_SEPARATOR: " صفحات | "
STR_BOOK_PREFIX: "الكتاب: "
STR_CALIBRE_URL_HINT: "في Calibre، أضف /opds إلى العنوان"
STR_SYNCING_TIME: "جاري مزامنة الوقت..."
STR_CALC_HASH: "جاري حساب بصمة المستند..."
STR_HASH_FAILED: "فشل حساب بصمة المستند"
STR_FETCH_PROGRESS: "جاري جلب التقدم من الخادم..."
STR_UPLOAD_PROGRESS: "جاري رفع التقدم..."
STR_NO_CREDENTIALS_MSG: "لم يتم إعداد بيانات الاعتماد"
STR_KOREADER_SETUP_HINT: "قم بإعداد حساب KOReader في الإعدادات"
STR_PROGRESS_FOUND: "تم العثور على تقدم سابق!"
STR_REMOTE_LABEL: "الخادم:"
STR_LOCAL_LABEL: "الجهاز:"
STR_PAGE_OVERALL_FORMAT: "صفحة %d، %.2f%% إجمالا"
STR_PAGE_TOTAL_OVERALL_FORMAT: "صفحة %d من %d، %.2f%% إجمالا"
STR_DEVICE_FROM_FORMAT: " من: %s"
STR_APPLY_REMOTE: "استخدام تقدم الخادم"
STR_UPLOAD_LOCAL: "رفع التقدم المحلي"
STR_NO_REMOTE_MSG: "لا يوجد تقدم على الخادم"
STR_UPLOAD_PROMPT: "رفع الموضع الحالي؟"
STR_UPLOAD_SUCCESS: "تم رفع التقدم!"
STR_SYNC_FAILED_MSG: "فشلت المزامنة"
STR_SAVE_PROGRESS_FAILED: "تعذر حفظ التقدم"
STR_SECTION_PREFIX: "القسم "
STR_UPLOAD: "رفع"
STR_BOOK_S_STYLE: "تنسيق الكتاب الأصلي"
STR_EMBEDDED_STYLE: "التنسيق المضمن"
STR_FOCUS_READING: "قراءة مركزة"
STR_OPDS_SERVER_URL: "عنوان خادم OPDS"
STR_PWR_BTN_FOOTNOTE_BACK: "عودة سريعة من الحواشي"
STR_SET_SLEEP_COVER: "تعيين الغلاف"
STR_FOOTNOTES: "الحواشي"
STR_NO_FOOTNOTES: "لا توجد حواش في هذه الصفحة"
STR_LINK: "[رابط]"
STR_SCREENSHOT_BUTTON: "التقاط لقطة شاشة"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u دقيقة"
STR_SLEEP_NEVER: "أبدا"
STR_STEP_HINT_FRONT: "الأزرار الأمامية:"
STR_STEP_HINT_SIDE: "الأزرار الجانبية:"
STR_ADD_SERVER: "إضافة خادم"
STR_SERVER_NAME: "اسم الخادم"
STR_NO_SERVERS: "لم يتم إعداد خوادم OPDS"
STR_DELETE_SERVER: "حذف الخادم"
STR_OPDS_SERVERS: "خوادم OPDS"
STR_AUTO_TURN_ENABLED: "التقليب التلقائي مفعل: "
STR_AUTO_TURN_PAGES_PER_MIN: "التقليب التلقائي (صفحات في الدقيقة)"
STR_MANAGE_FONTS: "إدارة الخطوط"
STR_FONT_BROWSER: "متصفح الخطوط"
STR_LOADING_FONT_LIST: "جاري تحميل قائمة الخطوط..."
STR_NO_FONTS_AVAILABLE: "لا تتوفر خطوط"
STR_FONT_INSTALLED: "تم تثبيت الخط!"
STR_FONT_INSTALL_FAILED: "فشل تثبيت الخط"
STR_INSTALLED: "مثبت"
STR_DOWNLOAD_ALL: "تنزيل الكل"
STR_UPDATE_ALL: "تحديث الكل"
STR_UPDATE_AVAILABLE: "تحديث"
STR_CRASH_TITLE: "تعطل النظام"
STR_CRASH_DESCRIPTION: "تم حفظ تقرير مفصل في الملف crash_report.txt. يرجى إرفاق هذا الملف عند الإبلاغ عن المشكلة."
STR_CRASH_REASON: "سبب التعطل:"
STR_CRASH_NO_REASON: "(لم يتم تسجيل سبب)"
STR_TILT_PAGE_TURN: "تقليب الصفحة بالإمالة"
STR_KB_HINT_MOVE_CURSOR: "اضغط زر اليمين أو اليسار لتحريك المؤشر"
STR_KB_HINT_RETURN_CURSOR: "اضغط زر اليسار للعودة إلى موضع المؤشر"
STR_KB_HINT_HIDE_PASSWORD: "اضغط مطولا على زر اليمين ثم اضغط [***] لإخفاء كلمة المرور"
STR_KB_HINT_SHOW_PASSWORD: "اضغط مطولا على زر اليمين ثم اضغط [abc] لإظهار كلمة المرور"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "اضغط [***] لإخفاء كلمة المرور"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "اضغط [abc] لإظهار كلمة المرور"
STR_KB_HINT_EDIT_ENTRY: "اضغط مطولا على زر الأعلى للتعديل"
STR_KB_TIPS: "نصائح:"
STR_KB_HINT_RETURN_KEYBOARD: "اضغط زر الأسفل للعودة إلى لوحة المفاتيح"
STR_KB_HINT_EXIT_URL_MODE: "اضغط ABC للخروج من وضع URL"
STR_KB_HINT_CLEAR_TEXT: "اضغط مطولا على DEL لمسح كل النص"
STR_KB_HINT_SECONDARY_CHAR: "اضغط مطولا على زر الاختيار للحرف الثانوي"
STR_KB_HINT_UPPER_SECONDARY: "اضغط مطولا على زر الاختيار للحرف الكبير أو الثانوي"
STR_KB_HINT_LOWER_SECONDARY: "اضغط مطولا على زر الاختيار للحرف الصغير أو الثانوي"
STR_KB_HINT_URL_SNIPPETS: "اضغط URL للاختصارات"
STR_SD_FIRMWARE_UPDATE: "تحديث البرنامج الثابت من بطاقة SD"
STR_SELECT_FIRMWARE_FILE: "اختر ملف البرنامج الثابت (.bin)"
STR_NO_BIN_FILES: "لم يتم العثور على ملفات .bin"
STR_VALIDATING_FIRMWARE: "جاري التحقق من البرنامج الثابت..."
STR_INVALID_FIRMWARE: "ملف برنامج ثابت غير صالح"
STR_FIRMWARE_TOO_LARGE: "البرنامج الثابت أكبر من القسم المخصص له"
STR_FIRMWARE_TOO_SMALL: "ملف البرنامج الثابت صغير جدا"
STR_FIRMWARE_UPDATE_PROMPT: "تحديث البرنامج الثابت؟"
STR_FIRMWARE_FILE_OPEN_FAILED: "تعذر فتح الملف"
STR_FIRMWARE_WRITE_FAILED: "فشلت كتابة البرنامج الثابت"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "لا تطفئ الجهاز!"
STR_RECOVERY_MODE: "وضع الاستعادة"
STR_RECOVERY_MODE_HINT: "ضع الملف firmware.bin في المجلد الرئيسي لبطاقة SD ثم اختره"
-19
View File
@@ -295,25 +295,6 @@ STR_HW_BACK_LABEL: "Back (1st button)"
STR_HW_CONFIRM_LABEL: "Confirm (2nd button)" STR_HW_CONFIRM_LABEL: "Confirm (2nd button)"
STR_HW_LEFT_LABEL: "Left (3rd button)" STR_HW_LEFT_LABEL: "Left (3rd button)"
STR_HW_RIGHT_LABEL: "Right (4th button)" STR_HW_RIGHT_LABEL: "Right (4th button)"
STR_BLUETOOTH: "Bluetooth"
STR_TOGGLE_BLUETOOTH: "Toggle Bluetooth"
STR_BT_SCAN_PAIR: "Scan & Pair"
STR_BT_NO_DEVICES: "No devices found"
STR_BT_FREE_HINT1: "Set Free2/3 to Reader Mode and"
STR_BT_FREE_HINT2: "Volume Function to pair"
STR_BT_DISCONNECT: "Disconnect"
STR_BT_CONNECTED_TO: "Connected: %s"
STR_BT_NOT_CONNECTED: "Not connected"
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_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"
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"
+385
View File
@@ -0,0 +1,385 @@
_language_name: "Norsk bokmål"
_language_code: "NB"
_order: "26"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "STARTER"
STR_SLEEPING: "HVILER"
STR_ENTERING_SLEEP: "Går i hvilemodus"
STR_BROWSE_FILES: "Bla i filer"
STR_FILE_TRANSFER: "Filoverføring"
STR_SETTINGS_TITLE: "Innstillinger"
STR_CONTINUE_READING: "Fortsett å lese"
STR_NO_OPEN_BOOK: "Ingen åpen bok"
STR_START_READING: "Start å lese nedenfor"
STR_NO_FILES_FOUND: "Ingen filer funnet"
STR_SELECT_CHAPTER: "Velg kapittel"
STR_NO_CHAPTERS: "Ingen kapitler"
STR_END_OF_BOOK: "Slutten av boken"
STR_EMPTY_CHAPTER: "Tomt kapittel"
STR_INDEXING: "Indekserer"
STR_MEMORY_ERROR: "Minnefeil"
STR_PAGE_LOAD_ERROR: "Feil ved sidelasting"
STR_EMPTY_FILE: "Tom fil"
STR_OUT_OF_BOUNDS: "Utenfor område"
STR_LOADING: "Laster..."
STR_LOADING_POPUP: "Laster"
STR_WIFI_NETWORKS: "WiFi-nettverk"
STR_NO_NETWORKS: "Ingen nettverk funnet"
STR_NETWORKS_FOUND: "%zu nettverk funnet"
STR_SCANNING: "Skanner..."
STR_CONNECTING: "Kobler til..."
STR_CONNECTED: "Tilkoblet!"
STR_CONNECTION_FAILED: "Tilkobling mislyktes"
STR_FORGET_NETWORK: "Glem nettverk?"
STR_SAVE_PASSWORD: "Lagre passord til neste gang?"
STR_PRESS_OK_SCAN: "Trykk OK for å skanne på nytt"
STR_JOIN_NETWORK: "Koble til et nettverk"
STR_CREATE_HOTSPOT: "Opprett hotspot"
STR_JOIN_DESC: "Koble til et eksisterende WiFi-nettverk"
STR_HOTSPOT_DESC: "Opprett et WiFi-nettverk andre kan koble seg til"
STR_STARTING_HOTSPOT: "Starter hotspot..."
STR_HOTSPOT_MODE: "Hotspot-modus"
STR_CONNECT_WIFI_HINT: "Koble enheten din til dette WiFi-nettverket"
STR_OPEN_URL_HINT: "Åpne denne URL-en i nettleseren din"
STR_OR_HTTP_PREFIX: "eller http://"
STR_SCAN_QR_HINT: "eller skann QR-kode med telefonen din:"
STR_CALIBRE_WIRELESS: "Calibre Wireless"
STR_NETWORK_LEGEND: "* = Kryptert | + = Lagret"
STR_MAC_ADDRESS: "MAC-adresse:"
STR_CHECKING_WIFI: "Sjekker WiFi..."
STR_ENTER_WIFI_PASSWORD: "Skriv inn WiFi-passord"
STR_TO_PREFIX: "til "
STR_CALIBRE_RECEIVING: "Mottar: "
STR_CALIBRE_RECEIVED: "Mottatt: "
STR_CALIBRE_INSTRUCTION_1: "1) Installer CrossPoint Reader-plugin"
STR_CALIBRE_INSTRUCTION_2: "2) Vær på samme WiFi-nettverk"
STR_CALIBRE_INSTRUCTION_3: "3) I Calibre: \"Send til enhet\""
STR_CALIBRE_INSTRUCTION_4: "\"Hold denne skjermen åpen mens du sender\""
STR_CAT_DISPLAY: "Skjerm"
STR_CAT_READER: "Leser"
STR_CAT_CONTROLS: "Betjening"
STR_CAT_SYSTEM: "System"
STR_SLEEP_SCREEN: "Hvileskjerm"
STR_QUICK_RESUME_TIMEOUT: "Hurtig gjenopptak ved tidsavbrudd"
STR_SLEEP_COVER_MODE: "Omslagsmodus for hvileskjerm"
STR_HIDE_BATTERY: "Skjul batteri %"
STR_EXTRA_SPACING: "Ekstra avsnittsavstand"
STR_TEXT_AA: "Tekstutjevning"
STR_IMAGES: "Bilder"
STR_IMAGES_DISPLAY: "Vis"
STR_IMAGES_PLACEHOLDER: "Plassholder"
STR_IMAGES_SUPPRESS: "Skjul"
STR_SHORT_PWR_BTN: "Kort trykk på av/på-knapp"
STR_ORIENTATION: "Leseretning"
STR_SIDE_BTN_LAYOUT: "Sideknapp-oppsett (leser)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orienter frontknapper"
STR_LONG_PRESS_BEHAVIOR: "Atferd ved langt trykk"
STR_LONG_PRESS_BEHAVIOR_OFF: "AV"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Kapittelhopp"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Endre orientering"
STR_LONG_PRESS_MENU: "Langt trykk på Meny"
STR_FONT_PREVIEW_TEXT: "Høvdingens kjære squaw får litt pizza i Mexico by"
STR_FONT_FAMILY: "Skrifttype i leser"
STR_FONT_SIZE: "Skriftstørrelse i leser"
STR_LINE_SPACING: "Linjeavstand i leser"
STR_SCREEN_MARGIN: "Skjermmarg i leser"
STR_PARA_ALIGNMENT: "Avsnittsjustering i leser"
STR_HYPHENATION: "Orddeling"
STR_TIME_TO_SLEEP: "Tid før hvile"
STR_SHOW_HIDDEN_FILES: "Vis skjulte filer"
STR_REMOVE_READ_FROM_RECENTS: "Fjern leste bøker fra Nylig-listen"
STR_MOVE_FINISHED_TO_READ: "Flytt fullførte bøker til Read-mappen"
STR_REFRESH_FREQ: "Oppdateringsfrekvens"
STR_KOREADER_SYNC: "KOReader Sync"
STR_CHECK_UPDATES: "Se etter oppdateringer"
STR_LANGUAGE: "Språk"
STR_CLEAR_READING_CACHE: "Tøm lesebuffer"
STR_USERNAME: "Brukernavn"
STR_PASSWORD: "Passord"
STR_SYNC_SERVER_URL: "URL til synk-server"
STR_DOCUMENT_MATCHING: "Dokumentgjenkjenning"
STR_AUTHENTICATE: "Godkjenn"
STR_KOREADER_USERNAME: "KOReader-brukernavn"
STR_KOREADER_PASSWORD: "KOReader-passord"
STR_FILENAME: "Filnavn"
STR_BINARY: "Binær"
STR_SET_CREDENTIALS_FIRST: "Angi påloggingsinfo først"
STR_WIFI_CONN_FAILED: "WiFi-tilkobling mislyktes"
STR_AUTHENTICATING: "Godkjenner..."
STR_AUTH_SUCCESS: "Godkjenning vellykket!"
STR_KOREADER_AUTH: "KOReader-godkjenning"
STR_SYNC_READY: "KOReader-synk er klar til bruk"
STR_AUTH_FAILED: "Godkjenning mislyktes"
STR_DONE: "Ferdig"
STR_CLEAR_CACHE_WARNING_1: "Dette tømmer alle bufrede bokdata."
STR_CLEAR_CACHE_WARNING_2: "All lesefremdrift går tapt!"
STR_CLEAR_CACHE_WARNING_3: "Bøker må indekseres på nytt"
STR_CLEAR_CACHE_WARNING_4: "når de åpnes igjen."
STR_CLEARING_CACHE: "Tømmer buffer..."
STR_CACHE_CLEARED: "Buffer tømt"
STR_ITEMS_REMOVED: "elementer fjernet"
STR_FAILED_LOWER: "mislyktes"
STR_CLEAR_CACHE_FAILED: "Kunne ikke tømme buffer"
STR_CHECK_SERIAL_OUTPUT: "Sjekk seriell utdata for detaljer"
STR_DARK: "Mørk"
STR_LIGHT: "Lys"
STR_CUSTOM: "Egendefinert"
STR_COVER: "Omslag"
STR_NONE_OPT: "Ingen"
STR_FIT: "Tilpass"
STR_CROP: "Beskjær"
STR_NEVER: "Aldri"
STR_IN_READER: "I leseren"
STR_ALWAYS: "Alltid"
STR_IGNORE: "Ignorer"
STR_SLEEP: "Hvile"
STR_PAGE_TURN: "Bla om"
STR_FORCE_REFRESH: "Oppdater skjerm"
STR_PORTRAIT: "Stående"
STR_LANDSCAPE_CW: "Liggende med klokken"
STR_INVERTED: "Opp ned"
STR_ORIENTATION_INVERTED: "Portrett 180°"
STR_LANDSCAPE_CCW: "Liggende mot klokken"
STR_PREV_NEXT: "Forrige/Neste"
STR_NEXT_PREV: "Neste/Forrige"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Bokmerke"
STR_DISABLED: "Deaktivert"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "Liten"
STR_MEDIUM: "Middels"
STR_LARGE: "Stor"
STR_X_LARGE: "Ekstra stor"
STR_TIGHT: "Tett"
STR_NORMAL: "Normal"
STR_WIDE: "Bred"
STR_JUSTIFY: "Blokkjustert"
STR_ALIGN_LEFT: "Venstre"
STR_CENTER: "Midtstilt"
STR_ALIGN_RIGHT: "Høyre"
STR_PAGES_1: "1 side"
STR_PAGES_5: "5 sider"
STR_PAGES_10: "10 sider"
STR_PAGES_15: "15 sider"
STR_PAGES_30: "30 sider"
STR_UPDATE: "Oppdater"
STR_CHECKING_UPDATE: "Ser etter oppdatering..."
STR_NEW_UPDATE: "Ny oppdatering tilgjengelig!"
STR_CURRENT_VERSION: "Nåværende versjon: "
STR_NEW_VERSION: "Ny versjon: "
STR_UPDATING: "Oppdaterer..."
STR_NO_UPDATE: "Ingen oppdatering tilgjengelig"
STR_UPDATE_FAILED: "Oppdatering mislyktes"
STR_UPDATE_COMPLETE: "Oppdatering fullført"
STR_POWER_ON_HINT: "Trykk og hold av/på-knappen for å slå på igjen"
STR_RESTARTING_HINT: "Starter på nytt... Hvis enheten ikke starter, hold av/på-knappen i noen sekunder."
STR_NO_ENTRIES: "Ingen oppføringer funnet"
STR_DOWNLOADING: "Laster ned..."
STR_DOWNLOAD_FAILED: "Nedlasting mislyktes"
STR_ERROR_MSG: "Feil:"
STR_UNNAMED: "Uten navn"
STR_HOLD_OPEN_TO_DELETE: "Hold Åpne for å slette"
STR_NO_SERVER_URL: "Ingen server-URL konfigurert"
STR_FETCH_FEED_FAILED: "Kunne ikke hente feed"
STR_PARSE_FEED_FAILED: "Kunne ikke tolke feed"
STR_NEXT_PAGE: "Neste side »"
STR_PREV_PAGE: "« Forrige side"
STR_NETWORK_PREFIX: "Nettverk: "
STR_IP_ADDRESS_PREFIX: "IP-adresse: "
STR_ERROR_GENERAL_FAILURE: "Feil: Generell feil"
STR_ERROR_NETWORK_NOT_FOUND: "Feil: Nettverk ikke funnet"
STR_ERROR_CONNECTION_TIMEOUT: "Feil: Tidsavbrudd for tilkobling"
STR_SD_CARD: "SD-kort"
STR_BACK: "« Tilbake"
STR_EXIT: "« Avslutt"
STR_HOME: "« Hjem"
STR_SELECT: "Velg"
STR_SELECTED: "Valgt"
STR_TOGGLE: "Veksle"
STR_TOGGLE_BOOKMARK: "Veksle bokmerke"
STR_CONFIRM: "Bekreft"
STR_CANCEL: "Avbryt"
STR_CONNECT: "Koble til"
STR_OPEN: "Åpne"
STR_DOWNLOAD: "Last ned"
STR_RETRY: "Prøv igjen"
STR_YES: "Ja"
STR_NO: "Nei"
STR_SHOW: "Vis"
STR_HIDE: "Skjul"
STR_STATE_ON: "PÅ"
STR_STATE_OFF: "AV"
STR_NOT_SET: "Ikke angitt"
STR_DIR_LEFT: "Venstre"
STR_DIR_RIGHT: "Høyre"
STR_DIR_UP: "Opp"
STR_DIR_DOWN: "Ned"
STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Omslagsfilter for hvileskjerm"
STR_FILTER_CONTRAST: "Kontrast"
STR_CUSTOMISE_STATUS_BAR: "Tilpass statuslinje"
STR_CHAPTER_PAGE_COUNT: "Sideantall i kapittel"
STR_BOOK_PROGRESS_PERCENTAGE: "Bokfremdrift i prosent"
STR_PROGRESS_BAR: "Fremdriftslinje"
STR_PROGRESS_BAR_THICKNESS: "Tykkelse på fremdriftslinje"
STR_PROGRESS_BAR_THIN: "Tynn"
STR_PROGRESS_BAR_MEDIUM: "Middels"
STR_PROGRESS_BAR_THICK: "Tykk"
STR_BOOK: "Bok"
STR_CHAPTER: "Kapittel"
STR_EXAMPLE_CHAPTER: "Kapittel 21"
STR_EXAMPLE_BOOK: "Boktittel"
STR_PREVIEW: "Forhåndsvisning"
STR_TITLE: "Tittel"
STR_BATTERY: "Batteri"
STR_XTC_STATUS_BAR: "XTC-statuslinje"
STR_BOTTOM: "Nederst"
STR_TOP: "Øverst"
STR_CLOCK: "Klokke"
STR_CLOCK_UTC_OFFSET: "UTC-forskyvning for klokke"
STR_CLOCK_FORMAT: "Klokkeformat"
STR_CLOCK_FORMAT_24H: "24-timers"
STR_CLOCK_FORMAT_12H: "12-timers"
STR_CURRENT_TIME: "Nåværende tid:"
STR_NEXT_FIELD: "Neste"
STR_CLOCK_SYNC: "Synkroniser klokke"
STR_CLOCK_SYNC_NOW: "Synkroniser klokke nå"
STR_CLOCK_SYNCING: "Synkroniserer fra NTP..."
STR_CLOCK_SYNC_OK: "Klokke synkronisert"
STR_CLOCK_SYNC_FAIL: "Synkronisering mislyktes"
STR_CLOCK_SYNC_NO_WIFI: "WiFi ikke tilkoblet"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Koble til WiFi først, og prøv igjen."
STR_CLOCK_SYNCED: "Klokke synkronisert"
STR_UI_THEME: "Grensesnittstema"
STR_THEME_CLASSIC: "Klassisk"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Retting for soluttoning"
STR_REMAP_FRONT_BUTTONS: "Tilordne frontknapper på nytt"
STR_BOOKMARKS: "Bokmerker"
STR_BOOKMARK_ADDED: "Bokmerke lagt til."
STR_BOOKMARK_REMOVED: "Bokmerke fjernet."
STR_OPDS_BROWSER: "OPDS-leser"
STR_SEARCH: "Søk"
STR_COVER_CUSTOM: "Omslag + Egendefinert"
STR_QUICK_RESUME: "Hurtig gjenopptak"
STR_MENU_RECENT_BOOKS: "Nylige bøker"
STR_REMOVE_FROM_RECENTS: "Fjern fra Nylige bøker?"
STR_NO_RECENT_BOOKS: "Ingen nylige bøker"
STR_CALIBRE_DESC: "Bruk trådløs enhetsoverføring med Calibre"
STR_FORGET_AND_REMOVE: "Glem nettverk og fjern lagret passord?"
STR_FORGET_BUTTON: "Glem"
STR_CALIBRE_STARTING: "Starter Calibre..."
STR_CALIBRE_SETUP: "Oppsett"
STR_CALIBRE_STATUS: "Status"
STR_CLEAR_BUTTON: "Tøm"
STR_DEFAULT_VALUE: "Standard"
STR_REMAP_PROMPT: "Trykk en frontknapp for hver rolle"
STR_UNASSIGNED: "Ikke tilordnet"
STR_ALREADY_ASSIGNED: "Allerede tilordnet"
STR_REMAP_RESET_HINT: "Sideknapp opp: Nullstill til standardoppsett"
STR_REMAP_CANCEL_HINT: "Sideknapp ned: Avbryt omtilordning"
STR_HW_BACK_LABEL: "Tilbake (1. knapp)"
STR_HW_CONFIRM_LABEL: "Bekreft (2. knapp)"
STR_HW_LEFT_LABEL: "Venstre (3. knapp)"
STR_HW_RIGHT_LABEL: "Høyre (4. knapp)"
STR_GO_TO_PERCENT: "Gå til %"
STR_GO_HOME_BUTTON: "Hjem"
STR_SYNC_PROGRESS: "Synkroniser fremdrift"
STR_DELETE_CACHE: "Slett bokbuffer"
STR_DELETE: "Slett"
STR_CONFIRM_DELETE_BOOKMARK: "Slette dette bokmerket?"
STR_DISPLAY_QR: "Vis side som QR"
STR_CHAPTER_PREFIX: "Kapittel: "
STR_PAGES_SEPARATOR: " sider | "
STR_BOOK_PREFIX: "Bok: "
STR_CALIBRE_URL_HINT: "For Calibre, legg til /opds i URL-en din"
STR_PERCENT_STEP_HINT: "Venstre/Høyre: 1% Opp/Ned: 10%"
STR_SYNCING_TIME: "Synkroniserer tid..."
STR_CALC_HASH: "Beregner dokument-hash..."
STR_HASH_FAILED: "Kunne ikke beregne dokument-hash"
STR_FETCH_PROGRESS: "Henter ekstern fremdrift..."
STR_UPLOAD_PROGRESS: "Laster opp fremdrift..."
STR_NO_CREDENTIALS_MSG: "Ingen påloggingsinfo angitt"
STR_KOREADER_SETUP_HINT: "Sett opp KOReader-konto i Innstillinger"
STR_PROGRESS_FOUND: "Fremdrift funnet!"
STR_REMOTE_LABEL: "Ekstern:"
STR_LOCAL_LABEL: "Lokal:"
STR_PAGE_OVERALL_FORMAT: "Side %d, %.2f%% totalt"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Side %d/%d, %.2f%% totalt"
STR_DEVICE_FROM_FORMAT: " Fra: %s"
STR_APPLY_REMOTE: "Bruk ekstern fremdrift"
STR_UPLOAD_LOCAL: "Last opp lokal fremdrift"
STR_NO_REMOTE_MSG: "Ingen ekstern fremdrift funnet"
STR_UPLOAD_PROMPT: "Last opp nåværende posisjon?"
STR_UPLOAD_SUCCESS: "Fremdrift lastet opp!"
STR_SYNC_FAILED_MSG: "Synkronisering mislyktes"
STR_SAVE_PROGRESS_FAILED: "Kunne ikke lagre fremdrift"
STR_SECTION_PREFIX: "Del "
STR_UPLOAD: "Last opp"
STR_BOOK_S_STYLE: "Bokens stil"
STR_EMBEDDED_STYLE: "Innebygd stil"
STR_FOCUS_READING: "Fokuslesing"
STR_OPDS_SERVER_URL: "OPDS-server-URL"
STR_PWR_BTN_FOOTNOTE_BACK: "Hurtig retur fra fotnoter"
STR_SET_SLEEP_COVER: "Velg omslag"
STR_FOOTNOTES: "Fotnoter"
STR_NO_FOOTNOTES: "Ingen fotnoter på denne siden"
STR_LINK: "[lenke]"
STR_SCREENSHOT_BUTTON: "Ta skjermbilde"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Aldri"
STR_SLEEP_TIMER_STEP_HINT: "Venstre/Høyre: 1 min Opp/Ned: 5 min"
STR_ADD_SERVER: "Legg til server"
STR_SERVER_NAME: "Servernavn"
STR_NO_SERVERS: "Ingen OPDS-servere konfigurert"
STR_DELETE_SERVER: "Slett server"
STR_OPDS_SERVERS: "OPDS-servere"
STR_AUTO_TURN_ENABLED: "Automatisk bla aktivert: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk bla (sider per minutt)"
STR_MANAGE_FONTS: "Administrer skrifttyper"
STR_FONT_BROWSER: "Bla i skrifttyper"
STR_LOADING_FONT_LIST: "Laster skrifttypeliste..."
STR_NO_FONTS_AVAILABLE: "Ingen skrifttyper tilgjengelig"
STR_FONT_INSTALLED: "Skrifttype installert!"
STR_FONT_INSTALL_FAILED: "Installasjon av skrifttype mislyktes"
STR_INSTALLED: "Installert"
STR_DOWNLOAD_ALL: "Last ned alle"
STR_UPDATE_ALL: "Oppdater alle"
STR_UPDATE_AVAILABLE: "Oppdater"
STR_CRASH_TITLE: "Systemkrasj"
STR_CRASH_DESCRIPTION: "En detaljert rapport ble lagret i crash_report.txt. Legg ved denne filen i feilrapporten din."
STR_CRASH_REASON: "Krasjårsak:"
STR_CRASH_NO_REASON: "(Ingen årsak ble registrert)"
STR_TILT_PAGE_TURN: "Vipp for å bla"
STR_KB_HINT_MOVE_CURSOR: "Trykk VENSTRE eller HØYRE for å flytte markøren"
STR_KB_HINT_RETURN_CURSOR: "Trykk VENSTRE for å gå tilbake til markørposisjonen"
STR_KB_HINT_HIDE_PASSWORD: "Hold HØYRE og trykk [***] for å skjule passord"
STR_KB_HINT_SHOW_PASSWORD: "Hold HØYRE og trykk [abc] for å vise passord"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Trykk [***] for å skjule passord"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Trykk [abc] for å vise passord"
STR_KB_HINT_EDIT_ENTRY: "Hold OPP for å redigere oppføring"
STR_KB_TIPS: "Tips:"
STR_KB_HINT_RETURN_KEYBOARD: "Trykk NED for å gå tilbake til tastaturet"
STR_KB_HINT_EXIT_URL_MODE: "Trykk ABC for å avslutte URL-modus"
STR_KB_HINT_CLEAR_TEXT: "Hold DEL for å slette all tekst"
STR_KB_HINT_SECONDARY_CHAR: "Hold VELG for sekundærtegn"
STR_KB_HINT_UPPER_SECONDARY: "Hold VELG for STORE BOKSTAVER eller sekundærtegn"
STR_KB_HINT_LOWER_SECONDARY: "Hold VELG for små bokstaver eller sekundærtegn"
STR_KB_HINT_URL_SNIPPETS: "Trykk URL for tekstbiter"
STR_SD_FIRMWARE_UPDATE: "Fastvareoppdatering fra SD-kort"
STR_SELECT_FIRMWARE_FILE: "Velg fastvarefil (.bin)"
STR_NO_BIN_FILES: "Ingen .bin-filer funnet"
STR_VALIDATING_FIRMWARE: "Validerer fastvare..."
STR_INVALID_FIRMWARE: "Ugyldig fastvarefil"
STR_FIRMWARE_TOO_LARGE: "Fastvaren er for stor for partisjonen"
STR_FIRMWARE_TOO_SMALL: "Fastvarefilen er for liten"
STR_FIRMWARE_UPDATE_PROMPT: "Oppdatere fastvare?"
STR_FIRMWARE_FILE_OPEN_FAILED: "Kan ikke åpne filen"
STR_FIRMWARE_WRITE_FAILED: "Skriving av fastvare mislyktes"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Ikke slå av!"
STR_RECOVERY_MODE: "Gjenopprettingsmodus"
STR_RECOVERY_MODE_HINT: "Legg firmware.bin i roten av SD-kortet og velg den"
+90 -4
View File
@@ -68,10 +68,10 @@ STR_SHORT_PWR_BTN: "Kısa Güç Tuşu Tıklaması"
STR_ORIENTATION: "Okuma Yönü" STR_ORIENTATION: "Okuma Yönü"
STR_SIDE_BTN_LAYOUT: "Yan Tuş Dizilimi (okuyucu)" STR_SIDE_BTN_LAYOUT: "Yan Tuş Dizilimi (okuyucu)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Ön düğmeleri yönlendir" STR_FRONT_BTN_FOLLOW_ORIENTATION: "Ön düğmeleri yönlendir"
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior" STR_LONG_PRESS_BEHAVIOR: "Uzun basma tuş davranışı"
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF" STR_LONG_PRESS_BEHAVIOR_OFF: "KAPALI"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip" STR_LONG_PRESS_BEHAVIOR_SKIP: "Bölüm atlama"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change" STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Yön değiştirme"
STR_FONT_PREVIEW_TEXT: "Pijamalı hasta yağız şoföre çabucak güvendi" STR_FONT_PREVIEW_TEXT: "Pijamalı hasta yağız şoföre çabucak güvendi"
STR_FONT_FAMILY: "Okuyucu Yazı Tipi Ailesi" STR_FONT_FAMILY: "Okuyucu Yazı Tipi Ailesi"
STR_FONT_SIZE: "Arayüz Yazı Boyutu" STR_FONT_SIZE: "Arayüz Yazı Boyutu"
@@ -306,3 +306,89 @@ STR_TITLE: "Başlık"
STR_TILT_PAGE_TURN: "Eğerek sayfa çevirme" STR_TILT_PAGE_TURN: "Eğerek sayfa çevirme"
STR_ADD_HIDDEN_NETWORK: "Gizli ağ ekle..." STR_ADD_HIDDEN_NETWORK: "Gizli ağ ekle..."
STR_ENTER_WIFI_SSID: "Ağ adını girin (SSID)" STR_ENTER_WIFI_SSID: "Ağ adını girin (SSID)"
STR_ADD_SERVER: "Sunucu Ekle"
STR_BOOKMARKS: "Yer İmleri"
STR_BOOKMARK_ADDED: "Yer imi eklendi."
STR_BOOKMARK_OPTION: "Yer İmi"
STR_BOTTOM: "Alt"
STR_CLOCK: "Saat"
STR_CLOCK_FORMAT: "Saat Biçimi"
STR_CLOCK_FORMAT_12H: "12 saat"
STR_CLOCK_FORMAT_24H: "24 saat"
STR_CLOCK_SYNC: "Saati Eşitle"
STR_CLOCK_SYNCED: "Saat Eşitlendi"
STR_CLOCK_SYNCING: "NTP'den eşitleniyor..."
STR_CLOCK_SYNC_FAIL: "Eşitleme başarısız"
STR_CLOCK_SYNC_NOW: "Saati şimdi eşitle"
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi bağlı değil"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Önce Wi-Fi'ye bağlanın, sonra tekrar deneyin."
STR_CLOCK_SYNC_OK: "Saat eşitlendi"
STR_CLOCK_UTC_OFFSET: "Saat UTC Farkı"
STR_CONFIRM_DELETE_BOOKMARK: "Bu yer imi silinsin mi?"
STR_CONNECTING_SAVED_WIFI: "Kayıtlı Wi-Fi'ye bağlanılıyor..."
STR_CRASH_DESCRIPTION: "Ayrıntılı rapor crash_report.txt dosyasına kaydedildi. Lütfen hata bildiriminize bu dosyayı ekleyin."
STR_CRASH_NO_REASON: "(Neden kaydedilmedi)"
STR_CRASH_REASON: "Çökme nedeni:"
STR_CRASH_TITLE: "Sistem Çökmesi"
STR_CURRENT_TIME: "Şu anki saat:"
STR_DELETE_SERVER: "Sunucuyu Sil"
STR_DISABLED: "Devre dışı"
STR_DOWNLOAD_ALL: "Tümünü İndir"
STR_EOB_CONTINUE_WITH: "Şununla devam et"
STR_EOB_HOME: "Ana Ekran"
STR_FINDING_SAVED_WIFI: "Kayıtlı Wi-Fi aranıyor..."
STR_FIRMWARE_FILE_OPEN_FAILED: "Dosya açılamıyor"
STR_FIRMWARE_TOO_LARGE: "Firmware, bölümlemeye sığmıyor"
STR_FIRMWARE_TOO_SMALL: "Firmware dosyası çok küçük"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Cihazı kapatmayın!"
STR_FIRMWARE_UPDATE_PROMPT: "Firmware güncellensin mi?"
STR_FIRMWARE_WRITE_FAILED: "Firmware yazılamadı"
STR_FONT_BROWSER: "Yazı Tipi Tarayıcısı"
STR_FONT_INSTALLED: "Yazı tipi yüklendi!"
STR_FONT_INSTALL_FAILED: "Yazı tipi yüklenemedi"
STR_FORCE_REFRESH: "Ekranı Yenile"
STR_INDEX_FAILED: "Endeksleme başarısız - geçersiz kitap"
STR_INSTALLED: "Yüklü"
STR_INVALID_FIRMWARE: "Geçersiz firmware dosyası"
STR_KB_HINT_CLEAR_TEXT: "Tüm metni silmek için DEL tuşunu basılı tutun"
STR_KB_HINT_EDIT_ENTRY: "Girdiyi düzenlemek için UP tuşunu basılı tutun"
STR_KB_HINT_EXIT_URL_MODE: "URL modundan çıkmak için ABC'ye basın"
STR_KB_HINT_HIDE_PASSWORD: "Şifreyi gizlemek için RIGHT'ı basılı tutup [***] tuşuna basın"
STR_KB_HINT_LOWER_SECONDARY: "Küçük harf veya ikincil karakter için SELECT'i basılı tutun"
STR_KB_HINT_MOVE_CURSOR: "İmleci taşımak için LEFT veya RIGHT'a basın"
STR_KB_HINT_RETURN_CURSOR: "İmleç konumuna dönmek için LEFT'e basın"
STR_KB_HINT_RETURN_KEYBOARD: "Klavyeye dönmek için DOWN'a basın"
STR_KB_HINT_SECONDARY_CHAR: "İkincil karakter için SELECT'i basılı tutun"
STR_KB_HINT_SHOW_PASSWORD: "Şifreyi göstermek için RIGHT'ı basılı tutup [abc] tuşuna basın"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Şifreyi gizlemek için [***] tuşuna basın"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Şifreyi göstermek için [abc] tuşuna basın"
STR_KB_HINT_UPPER_SECONDARY: "BÜYÜK harf veya ikincil karakter için SELECT'i basılı tutun"
STR_KB_HINT_URL_SNIPPETS: "Hazır kalıplar için URL'ye basın"
STR_KB_TIPS: "İpuçları:"
STR_KOSYNC: "KOSync"
STR_LOADING_FONT_LIST: "Yazı tipi listesi yükleniyor..."
STR_LONG_PRESS_MENU: "Uzun Basma Menüsü"
STR_MANAGE_FONTS: "Yazı Tiplerini Yönet"
STR_NEXT_FIELD: "Sonraki"
STR_NEXT_PAGE: "Sonraki Sayfa »"
STR_NO_BIN_FILES: ".bin dosyası bulunamadı"
STR_NO_FONTS_AVAILABLE: "Kullanılabilir yazı tipi yok"
STR_NO_SERVERS: "Yapılandırılmış OPDS sunucusu yok"
STR_OPDS_SERVERS: "OPDS Sunucuları"
STR_PREV_PAGE: "« Önceki Sayfa"
STR_PWR_BTN_FOOTNOTE_BACK: "Dipnottan hızlı dönüş"
STR_RECOVERY_MODE: "Kurtarma Modu"
STR_RECOVERY_MODE_HINT: "firmware.bin dosyasını SD kartın köküne koyup seçin"
STR_RESTARTING_HINT: "Yeniden başlatılıyor... Cihaz yeniden başlamazsa güç düğmesini birkaç saniye basılı tutun."
STR_SD_FIRMWARE_UPDATE: "SD Karttan Firmware Güncelleme"
STR_SEARCH: "Ara"
STR_SELECT_FIRMWARE_FILE: "Firmware dosyası seçin (.bin)"
STR_SERVER_NAME: "Sunucu Adı"
STR_SET_SLEEP_COVER: "Kapak Ayarla"
STR_SHOW_NETWORKS: "Göster"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_TOP: "Üst"
STR_UPDATE_ALL: "Tümünü Güncelle"
STR_UPDATE_AVAILABLE: "Güncelle"
STR_VALIDATING_FIRMWARE: "Firmware doğrulanıyor..."
STR_XTC_STATUS_BAR: "XTC Durum Çubuğu"
+10 -46
View File
@@ -15,7 +15,7 @@ upload_speed = 921600
check_tool = cppcheck check_tool = cppcheck
; missingInclude (project headers) is suppressed alongside missingIncludeSystem: on a ; missingInclude (project headers) is suppressed alongside missingIncludeSystem: on a
; fresh CI checkout cppcheck has no resolved include paths, so it reports every ; fresh CI checkout cppcheck has no resolved include paths, so it reports every
; project header as missing (~470 information-level lines) and fails the job. ; project header as missing (~400 information-level lines) and fails the job.
check_flags = --enable=all --suppress=missingIncludeSystem --suppress=missingInclude --suppress=unusedFunction --suppress=unmatchedSuppression --suppress=*:*/.pio/* --inline-suppr check_flags = --enable=all --suppress=missingIncludeSystem --suppress=missingInclude --suppress=unusedFunction --suppress=unmatchedSuppression --suppress=*:*/.pio/* --inline-suppr
check_skip_packages = yes check_skip_packages = yes
@@ -63,16 +63,6 @@ build_flags =
-Wno-bidi-chars -Wno-bidi-chars
-Wl,--wrap=panic_print_backtrace,--wrap=panic_abort,--wrap=bootloader_common_check_efuse_blk_validity -Wl,--wrap=panic_print_backtrace,--wrap=panic_abort,--wrap=bootloader_common_check_efuse_blk_validity
-fno-exceptions -fno-exceptions
# FreeInk panel profiles: compile both X3 (792x528/UC8253) and X4 (800x480/SSD1677);
# the firmware picks the active one at runtime via HalDisplay setDisplayX3().
-DFREEINK_DEVICE_X3=1
-DFREEINK_DEVICE_X4=1
# BLE HID page-turner host (BleKeyboardHost). NimBLE role/bond config is baked into
# the prebuilt arduino-esp32 framework's sdkconfig.h, so we don't redefine it here
# (doing so only warns and has no effect). The host only compiles when
# FREEINK_CAP_BLE_HID_HOST is set on the env (below); with the capability off,
# BleKeyboardHost links stubs and pulls in zero NimBLE code.
-DFREEINK_BLE_HID_SHOW_UNNAMED_DEVICES=0
build_unflags = build_unflags =
-std=gnu++11 -std=gnu++11
@@ -83,34 +73,18 @@ board_build.flash_mode = dio
board_build.flash_size = 16MB board_build.flash_size = 16MB
board_build.partitions = partitions.csv board_build.partitions = partitions.csv
; Shrink the NimBLE footprint for a 1-connection HID host moving 3-6 byte reports. ; MEMFIX-PORT: custom_sdkconfig heap reclamation (~32-37 KB). Rebuilds the
; Field-measured: begin() costs ~52 KB with these trims vs ~68 KB with the prebuilt ; Arduino core libs on first build (slower once, cached after; needs the CMake
; framework defaults — and that 15 KB is the difference between the stack landing ; pin in platformio.local.ini on macOS).
; above the reader's render shed floor (stable coexistence) and below it (a ;
; guaranteed shed/restart flap). Rebuilds the Arduino core libs on first build ; If an interrupted rebuild fails with "multiple definition of 'app_main'"
; (slower once, cached after; needs the CMake pin in platformio.local.ini on macOS). ; (stale generated scaffold), clean it up with:
; rm -rf .dummy CMakeLists.txt sdkconfig.default sdkconfig.defaults .pio/build/default
; Do NOT use `git clean -fdX` — it deletes platformio.local.ini.
custom_sdkconfig = custom_sdkconfig =
CONFIG_BT_NIMBLE_ROLE_PERIPHERAL=n
CONFIG_BT_NIMBLE_ROLE_BROADCASTER=n
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
CONFIG_BT_NIMBLE_MAX_CCCDS=2
CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=23
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=6
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=6 ; was 24 x 320 B
CONFIG_BT_NIMBLE_ACL_BUF_COUNT=6 ; was 24 x 255 B
CONFIG_BT_NIMBLE_HCI_EVT_HI_BUF_COUNT=12 ; was 30 x 70 B; only scan bursts need many
; IDF 5.5 sizes the HCI transport pools under TRANSPORT_* names; pin both spellings.
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=6
CONFIG_BT_NIMBLE_TRANSPORT_EVT_COUNT=12
CONFIG_BT_NIMBLE_ATT_MAX_PREP_ENTRIES=4 ; was 64; a HID host never does prepared writes
CONFIG_BT_CTRL_BLE_MAX_ACT=3 ; was 6; need conn + scan + initiate only
CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_NUM=50 ; was 100; pairing-time scan only
CONFIG_BT_CTRL_ADV_DUP_FILT_MAX=10 ; was 30
; MEMFIX-PORT: task stack right-sizing (~7 KB); NOTE develop has no
; custom_sdkconfig block — port via sdkconfig.defaults or equivalent.
; Task stack right-sizing from measured high-water marks (heap block map + ; Task stack right-sizing from measured high-water marks (heap block map +
; per-task stack audit, July 2026): esp_timer used ~0.8 KB of 8 KB across ; per-task stack audit, July 2026): esp_timer used ~0.8 KB of 8 KB across
; every capture incl. BLE sessions; the FreeRTOS timer service used ~0.5 KB ; every capture; the FreeRTOS timer service used ~0.5 KB
; of 4 KB. Neither runs TLS or app code. ~7 KB back to the heap. ; of 4 KB. Neither runs TLS or app code. ~7 KB back to the heap.
CONFIG_ESP_TIMER_TASK_STACK_SIZE=4096 CONFIG_ESP_TIMER_TASK_STACK_SIZE=4096
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2560 CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2560
@@ -154,16 +128,11 @@ lib_deps =
InputManager=symlink://freeink-sdk/libs/hardware/InputManager InputManager=symlink://freeink-sdk/libs/hardware/InputManager
EInkDisplay=symlink://freeink-sdk/libs/display/FreeInkDisplay EInkDisplay=symlink://freeink-sdk/libs/display/FreeInkDisplay
SDCardManager=symlink://freeink-sdk/libs/hardware/SDCardManager SDCardManager=symlink://freeink-sdk/libs/hardware/SDCardManager
; FreeInk HAL support libs the above depend on (BoardConfig pin maps, etc.).
BoardConfig=symlink://freeink-sdk/libs/hardware/BoardConfig BoardConfig=symlink://freeink-sdk/libs/hardware/BoardConfig
PowerManager=symlink://freeink-sdk/libs/hardware/PowerManager PowerManager=symlink://freeink-sdk/libs/hardware/PowerManager
Rtc=symlink://freeink-sdk/libs/hardware/Rtc
Imu=symlink://freeink-sdk/libs/hardware/Imu
SecureNet=symlink://freeink-sdk/libs/network/SecureNet SecureNet=symlink://freeink-sdk/libs/network/SecureNet
FreeInkUI=symlink://freeink-sdk/libs/ui/FreeInkUI FreeInkUI=symlink://freeink-sdk/libs/ui/FreeInkUI
Icons=symlink://freeink-sdk/libs/assets/Icons Icons=symlink://freeink-sdk/libs/assets/Icons
BleKeyboardHost=symlink://freeink-sdk/libs/network/BleKeyboardHost
h2zero/NimBLE-Arduino @ ^2.3.8
bblanchon/ArduinoJson @ 7.4.2 bblanchon/ArduinoJson @ 7.4.2
ricmoo/QRCode @ 0.0.1 ricmoo/QRCode @ 0.0.1
bitbank2/PNGdec @ 1.1.6 bitbank2/PNGdec @ 1.1.6
@@ -181,9 +150,6 @@ build_flags =
; CROSSPOINT_VERSION is set by scripts/git_branch.py (includes branch + short SHA) ; CROSSPOINT_VERSION is set by scripts/git_branch.py (includes branch + short SHA)
-DENABLE_SERIAL_LOG -DENABLE_SERIAL_LOG
-DLOG_LEVEL=2 ; Set log level to debug for development builds -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] [env:gh_release]
@@ -193,7 +159,6 @@ build_flags =
-DCROSSPOINT_VERSION=\"${crosspoint.version}\" -DCROSSPOINT_VERSION=\"${crosspoint.version}\"
-DENABLE_SERIAL_LOG -DENABLE_SERIAL_LOG
-DLOG_LEVEL=1 ; Set log level to info for release builds -DLOG_LEVEL=1 ; Set log level to info for release builds
-DFREEINK_CAP_BLE_HID_HOST=1 ; BLE HID page-turner host (NimBLE)
[env:gh_release_rc] [env:gh_release_rc]
extends = base extends = base
@@ -202,7 +167,6 @@ build_flags =
-DCROSSPOINT_VERSION=\"${crosspoint.version}-rc+${sysenv.CROSSPOINT_RC_HASH}\" -DCROSSPOINT_VERSION=\"${crosspoint.version}-rc+${sysenv.CROSSPOINT_RC_HASH}\"
-DENABLE_SERIAL_LOG -DENABLE_SERIAL_LOG
-DLOG_LEVEL=1 ; Set log level to info for release candidate builds -DLOG_LEVEL=1 ; Set log level to info for release candidate builds
-DFREEINK_CAP_BLE_HID_HOST=1 ; BLE HID page-turner host (NimBLE)
[env:slim] [env:slim]
extends = base extends = base
-121
View File
@@ -1,121 +0,0 @@
#include "BleInput.h"
#include <GfxRenderer.h>
#include <HalPowerManager.h>
#include <I18n.h>
#include <cstdio>
#include <cstring>
#include "MappedInputManager.h"
#include "components/UITheme.h"
namespace bleinput {
namespace {
volatile bool g_startInProgress = false;
}
// NimBLE controller init/deinit hang (interrupt WDT) if run at the 10 MHz low-power
// frequency, so force normal CPU speed around both. Centralized here so every caller
// (boot restore, settings toggle, reader toggle, sleep) is covered automatically.
bool ensureStarted() {
g_startInProgress = true;
HalPowerManager::Lock powerLock;
const bool ok = BleHid.begin(kHostName);
g_startInProgress = false;
return ok;
}
bool startInProgress() { return g_startInProgress; }
// Full teardown (NimBLE deinit), not just a link drop, so the BLE stack's RAM is
// returned to the heap — otherwise memory-hungry work like EPUB inflate can't
// allocate even after the user turns Bluetooth off.
void stop() {
HalPowerManager::Lock powerLock;
BleHid.end();
}
bool encodeKey(const freeink::KeyEvent& ev, uint8_t& kind, uint8_t& value) {
if (ev.special != freeink::SpecialKey::None) {
kind = 0;
value = static_cast<uint8_t>(ev.special);
return true;
}
if (ev.keycode != 0) {
kind = 1;
value = ev.keycode;
return true;
}
return false;
}
namespace {
const char* specialName(uint8_t value) {
switch (static_cast<freeink::SpecialKey>(value)) {
case freeink::SpecialKey::Enter:
return "Enter";
case freeink::SpecialKey::Backspace:
return "Backspace";
case freeink::SpecialKey::Tab:
return "Tab";
case freeink::SpecialKey::Escape:
return "Escape";
case freeink::SpecialKey::Delete:
return "Delete";
case freeink::SpecialKey::Left:
return "Left";
case freeink::SpecialKey::Right:
return "Right";
case freeink::SpecialKey::Up:
return "Up";
case freeink::SpecialKey::Down:
return "Down";
case freeink::SpecialKey::Home:
return "Home";
case freeink::SpecialKey::End:
return "End";
case freeink::SpecialKey::PageUp:
return "Page Up";
case freeink::SpecialKey::PageDown:
return "Page Down";
default:
return nullptr;
}
}
} // namespace
void showConnectingUntilLinked(const GfxRenderer& renderer, const MappedInputManager& input) {
if (!BleHid.isRunning() || BleHid.isConnected()) return;
// drawPopup refreshes the panel itself, so draw once and let e-ink hold it while we
// pump the host. Holds until the remote links, the user presses a button to bail, or
// a generous timeout (a remote that slept after a disconnect needs a button to wake).
GUI.drawPopup(renderer, tr(STR_BT_CONNECTING_POPUP));
const unsigned long deadline = millis() + 10000;
while (!BleHid.isConnected() && millis() < deadline) {
BleHid.poll();
input.update();
if (input.wasAnyPressed()) break;
delay(50);
}
// Note: the caller must redraw to clear the popup. For grayscale reader pages the
// caller should also request a ghost-cleanup (HALF) refresh first — a plain fast/
// partial refresh ghosts badly over the BW popup (see Activity::requestGhostCleanup).
}
void describeKey(uint8_t kind, uint8_t value, char* out, size_t outLen) {
if (!out || outLen == 0) return;
if (kind == 0) {
const char* name = specialName(value);
if (name) {
strncpy(out, name, outLen - 1);
out[outLen - 1] = '\0';
return;
}
}
// Printable ASCII usage handled as a generic key code; show the raw value.
snprintf(out, outLen, "Key 0x%02X", static_cast<unsigned>(value));
}
} // namespace bleinput
-62
View File
@@ -1,62 +0,0 @@
#pragma once
// CrossPoint <-> FreeInk BLE HID host glue.
//
// Thin, capability-safe helpers around freeink::BleKeyboardHost (the `BleHid`
// singleton). When FREEINK_CAP_BLE_HID_HOST is compiled out the SDK links stubs,
// so every call here is still valid and simply no-ops / returns false — callers
// need no #ifdefs.
//
// The (kind, value) pair produced by encodeKey() is the stable identity stored in
// CrossPointSettings::bleKeyMap. Page-turner remotes emit "special" keys
// (PageUp/PageDown/arrows); plain keyboards emit usage codes. We deliberately
// ignore modifiers and the printable char for matching (page turners don't use
// modifiers), keeping the persisted entry a trivial two-byte comparison.
#include <BleKeyboardHost.h>
#include <cstdint>
class GfxRenderer;
class MappedInputManager;
namespace bleinput {
// Advertised central name shown to peripherals during pairing.
inline constexpr const char* kHostName = "CrossPoint";
// Heap floor for starting the NimBLE stack (measured begin() cost: ~52-57 KB).
// The reader now lends the framebuffer to section builds, so BLE startup no
// longer needs to reserve the old full build headroom. Keep a modest margin and
// let the render/build shed paths handle genuinely tight moments.
inline constexpr size_t kStartMinFreeHeap = 56 * 1024;
// Lower floor for the Bluetooth settings screen, where the user has explicitly asked
// for BLE right now (scanning/pairing is dead without the stack). No page renders or
// section builds run there, so the reader-sized reserve above doesn't apply — only
// NimBLE's own ~57 KB plus working margin.
inline constexpr size_t kStartMinFreeHeapExplicit = 70 * 1024;
// Start the BLE HID host (idempotent). Returns false if BLE is compiled out or
// NimBLE init failed. Safe to call repeatedly.
bool ensureStarted();
bool startInProgress();
// Drop the active link (e.g. before deep sleep or when the user disables BT).
void stop();
// Encode a decoded key event into the stable (kind, value) identity used by the
// settings map. kind: 0 = SpecialKey, 1 = HID usage. Returns false when the event
// carries no usable identity (no special key and no usage code).
bool encodeKey(const freeink::KeyEvent& ev, uint8_t& kind, uint8_t& value);
// Human-readable name for a stored (kind, value) identity, for the mapping UI.
// Writes a null-terminated string into out (e.g. "Page Down", "Key 0x4B").
void describeKey(uint8_t kind, uint8_t value, char* out, size_t outLen);
// Draw a "BT Connecting..." popup and pump the BLE host until the bonded remote
// links, the user presses a button to dismiss, or a timeout. No-op if BLE isn't
// running or is already connected. The caller must redraw afterward to clear it.
void showConnectingUntilLinked(const GfxRenderer& renderer, const MappedInputManager& input);
} // namespace bleinput
-16
View File
@@ -225,22 +225,6 @@ class CrossPointSettings {
uint8_t frontButtonConfirm = FRONT_HW_CONFIRM; uint8_t frontButtonConfirm = FRONT_HW_CONFIRM;
uint8_t frontButtonLeft = FRONT_HW_LEFT; uint8_t frontButtonLeft = FRONT_HW_LEFT;
uint8_t frontButtonRight = FRONT_HW_RIGHT; uint8_t frontButtonRight = FRONT_HW_RIGHT;
// --- Bluetooth (BLE HID page-turner) ---
// Master on/off for the BLE HID host. Persisted; auto-restored on boot/wake.
// Managed by BluetoothSettingsActivity and the in-reader "Toggle Bluetooth" menu item.
uint8_t bluetoothEnabled = 0;
// Remote-button mapping table: each slot binds a decoded BLE key identity to a
// logical MappedInputManager::Button. Fixed-capacity POD (no heap), persisted
// manually in JsonSettingsIO (like the front-button remap). 0xFF = empty/unassigned.
// Headroom for several buttons plus optional presets and rolling-code remotes
// (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
};
BleKeyMapEntry bleKeyMap[BLE_MAP_CAPACITY] = {};
// Reader font settings // Reader font settings
uint8_t fontFamily = NOTOSERIF; uint8_t fontFamily = NOTOSERIF;
uint8_t fontSize = MEDIUM; uint8_t fontSize = MEDIUM;
-85
View File
@@ -1,85 +0,0 @@
#include "HeapMap.h"
#include <Arduino.h>
#include <Logging.h>
#include <Memory.h>
#include <esp_heap_caps.h>
#include <cstdio>
#include "rom/ets_sys.h"
// heap_caps_dump walks each heap inside a critical section (interrupts
// masked), so its output can neither go through the UART-0 ROM path we can't
// see nor be flow-controlled toward the CDC (the CDC ring drains in an
// interrupt handler — waiting deadlocks into the interrupt WDT,
// field-verified). Instead the ROM putc parses each line into a compact
// record; the captured table is logged after the dump with interrupts live.
namespace heapmap {
namespace {
struct BlockRec {
uint32_t addr;
uint32_t size;
bool free;
};
constexpr uint16_t kMaxRecs = 1400;
BlockRec* g_recs = nullptr; // borrowed buffer, valid only during capture
uint16_t g_recCount = 0;
bool g_overflowed = false;
char g_line[96];
uint8_t g_lineLen = 0;
void captInterpolatePutc(char c) {
if (c != '\n') {
if (g_lineLen < sizeof(g_line) - 1) g_line[g_lineLen++] = c;
return;
}
g_line[g_lineLen] = '\0';
g_lineLen = 0;
// e.g. "Block 0x3fcc69bc data, size: 89424 bytes, Free: Yes"
unsigned addr = 0, size = 0;
char freeWord[4] = {0};
if (sscanf(g_line, "Block 0x%x data, size: %u bytes, Free: %3s", &addr, &size, freeWord) == 3) {
if (g_recs && g_recCount < kMaxRecs) {
g_recs[g_recCount++] = {addr, size, freeWord[0] == 'Y'};
} else {
g_overflowed = true;
}
}
}
} // namespace
void dump() {
auto recBuf = makeUniqueNoThrow<BlockRec[]>(kMaxRecs);
if (!recBuf) {
LOG_ERR("MEM", "heap map skipped: no room for capture buffer");
return;
}
g_recs = recBuf.get();
g_recCount = 0;
g_overflowed = false;
g_lineLen = 0;
// Capture (interrupts masked inside the dump): parse into records, never
// wait. Log the table afterward with the system live. NOTE: the capture
// buffer itself appears in the map as a used block of ~17.4 KB — it frees
// on return (observer effect, do not chase it as a leak/splitter).
ets_install_putc1(&captInterpolatePutc);
heap_caps_dump(MALLOC_CAP_8BIT);
ets_install_uart_printf();
g_recs = nullptr;
LOG_DBG("MEM", "---- heap block map: %u blocks%s ----", g_recCount, g_overflowed ? " (TRUNCATED)" : "");
uint32_t dustCount = 0, dustBytes = 0;
for (uint16_t i = 0; i < g_recCount; ++i) {
const auto& r = recBuf[i];
if (r.free || r.size >= 256) {
LOG_DBG("MEM", "%s 0x%08x %u", r.free ? "FREE" : "used", r.addr, r.size);
} else {
dustCount++;
dustBytes += r.size;
}
}
LOG_DBG("MEM", "dust: %u used blocks < 256B totaling %u bytes", dustCount, dustBytes);
LOG_DBG("MEM", "---- end heap block map ----");
}
} // namespace heapmap
-10
View File
@@ -1,10 +0,0 @@
#pragma once
// MEMFIX-PORT: heap block map (on-demand via CMD:MEMMAP + reader one-shot); portable, no BLE dependency
namespace heapmap {
// Capture-and-log the DRAM heap block map (address/size/free per block,
// sub-256B used blocks rolled up as "dust"). Safe to call from the main loop;
// ~60-100 LOG_DBG lines. See HeapMap.cpp for why capture-then-log is the only
// shape that works (heap_caps_dump runs with interrupts masked).
void dump();
} // namespace heapmap
-29
View File
@@ -7,13 +7,11 @@
#include <algorithm> #include <algorithm>
#include <cstring> #include <cstring>
#include <iterator>
#include <string> #include <string>
#include "BookmarkEntry.h" #include "BookmarkEntry.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "CrossPointState.h" #include "CrossPointState.h"
#include "MappedInputManager.h"
#include "OpdsServerStore.h" #include "OpdsServerStore.h"
#include "RecentBooksStore.h" #include "RecentBooksStore.h"
#include "SettingsList.h" #include "SettingsList.h"
@@ -145,16 +143,6 @@ bool JsonSettingsIO::saveSettings(const CrossPointSettings& s, const char* path)
doc["frontButtonConfirm"] = s.frontButtonConfirm; doc["frontButtonConfirm"] = s.frontButtonConfirm;
doc["frontButtonLeft"] = s.frontButtonLeft; doc["frontButtonLeft"] = s.frontButtonLeft;
doc["frontButtonRight"] = s.frontButtonRight; doc["frontButtonRight"] = s.frontButtonRight;
// Bluetooth — managed by BluetoothSettingsActivity, not in SettingsList.
doc["bluetoothEnabled"] = s.bluetoothEnabled;
JsonArray bleMap = doc["bleKeyMap"].to<JsonArray>();
for (const auto& e : s.bleKeyMap) {
if (e.keyKind == 0xFF || e.button == 0xFF) continue; // skip empty/unassigned slots
JsonObject o = bleMap.add<JsonObject>();
o["k"] = e.keyKind;
o["v"] = e.keyValue;
o["b"] = e.button;
}
// Font family — uses dynamic getter/setter in SettingsList so the generic loop skips it. // Font family — uses dynamic getter/setter in SettingsList so the generic loop skips it.
doc["fontFamily"] = s.fontFamily; doc["fontFamily"] = s.fontFamily;
// SD card font family name — not in SettingsList, save manually // SD card font family name — not in SettingsList, save manually
@@ -252,23 +240,6 @@ bool JsonSettingsIO::loadSettings(CrossPointSettings& s, const char* json, bool*
clamp(doc["frontButtonRight"] | (uint8_t)S::FRONT_HW_RIGHT, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_RIGHT); clamp(doc["frontButtonRight"] | (uint8_t)S::FRONT_HW_RIGHT, S::FRONT_BUTTON_HARDWARE_COUNT, S::FRONT_HW_RIGHT);
CrossPointSettings::validateFrontButtonMapping(s); CrossPointSettings::validateFrontButtonMapping(s);
// Bluetooth — managed by BluetoothSettingsActivity, not in SettingsList.
s.bluetoothEnabled = clamp(doc["bluetoothEnabled"] | (uint8_t)0, 2, 0);
std::fill(std::begin(s.bleKeyMap), std::end(s.bleKeyMap), CrossPointSettings::BleKeyMapEntry{}); // reset to empty
JsonArrayConst bleMap = doc["bleKeyMap"];
if (!bleMap.isNull()) {
uint8_t slot = 0;
for (JsonObjectConst o : bleMap) {
if (slot >= CrossPointSettings::BLE_MAP_CAPACITY) break;
const uint8_t button = o["b"] | (uint8_t)0xFF;
if (button >= MappedInputManager::kButtonCount) continue; // drop invalid mappings
s.bleKeyMap[slot].keyKind = o["k"] | (uint8_t)0xFF;
s.bleKeyMap[slot].keyValue = o["v"] | (uint8_t)0;
s.bleKeyMap[slot].button = button;
slot++;
}
}
// Font family — uses dynamic getter/setter in SettingsList so the generic loop skips it. // Font family — uses dynamic getter/setter in SettingsList so the generic loop skips it.
const uint8_t storedFontFamily = doc["fontFamily"] | (uint8_t)0; const uint8_t storedFontFamily = doc["fontFamily"] | (uint8_t)0;
s.fontFamily = clamp(storedFontFamily, CrossPointSettings::BUILTIN_FONT_COUNT, 0); s.fontFamily = clamp(storedFontFamily, CrossPointSettings::BUILTIN_FONT_COUNT, 0);
+4 -93
View File
@@ -2,7 +2,6 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include "BleInput.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
bool MappedInputManager::isNavDirectionSwapped() const { bool MappedInputManager::isNavDirectionSwapped() const {
@@ -75,105 +74,17 @@ bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint
return false; return false;
} }
bool MappedInputManager::bleEdge(const bool* arr, const Button button) const { bool MappedInputManager::wasPressed(const Button button) const { return mapButton(button, &HalGPIO::wasPressed); }
// Mirror mapButton()'s composite navigation handling so a BLE key bound to a
// physical direction also satisfies the derived NavNext / NavPrevious logical
// buttons (used by list navigation), respecting the orientation axis flip.
switch (button) {
case Button::NavNext:
return isNavDirectionSwapped() ? (arr[(int)Button::Up] || arr[(int)Button::Left])
: (arr[(int)Button::Down] || arr[(int)Button::Right]);
case Button::NavPrevious:
return isNavDirectionSwapped() ? (arr[(int)Button::Down] || arr[(int)Button::Right])
: (arr[(int)Button::Up] || arr[(int)Button::Left]);
default:
return arr[(int)button];
}
}
bool MappedInputManager::wasPressed(const Button button) const { bool MappedInputManager::wasReleased(const Button button) const { return mapButton(button, &HalGPIO::wasReleased); }
return mapButton(button, &HalGPIO::wasPressed) || bleEdge(blePressEdge, button);
}
bool MappedInputManager::wasReleased(const Button button) const { bool MappedInputManager::isPressed(const Button button) const { return mapButton(button, &HalGPIO::isPressed); }
return mapButton(button, &HalGPIO::wasReleased) || bleEdge(bleReleaseEdge, button);
}
bool MappedInputManager::isPressed(const Button button) const {
// A BLE tap is momentary: report "pressed" only on the press-edge frame.
return mapButton(button, &HalGPIO::isPressed) || bleEdge(blePressEdge, button);
}
void MappedInputManager::setBleCaptureMode(const bool on) {
bleCaptureMode = on;
bleHasCaptured = false;
if (on) {
// Clear any stale overlay so a held remote key doesn't leak into the UI.
for (uint8_t i = 0; i < kButtonCount; i++) {
blePressEdge[i] = false;
bleReleaseEdge[i] = false;
}
}
}
bool MappedInputManager::takeCapturedBleKey(uint8_t& kind, uint8_t& value) {
if (!bleHasCaptured) return false;
kind = bleCapturedKind;
value = bleCapturedValue;
bleHasCaptured = false;
return true;
}
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. 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;
if (!bleinput::encodeKey(ev, kind, value)) continue;
if (bleCaptureMode) {
bleCapturedKind = kind;
bleCapturedValue = value;
bleHasCaptured = true;
continue;
}
// Resolve the key identity against the persisted mapping table.
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;
}
break;
}
}
}
bool MappedInputManager::wasAnyPressed() const { return gpio.wasAnyPressed(); } 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 { unsigned long MappedInputManager::getHeldTime() const { return gpio.getHeldTime(); }
// 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 {
-33
View File
@@ -7,9 +7,6 @@ class GfxRenderer;
class MappedInputManager { class MappedInputManager {
public: public:
enum class Button { Back, Confirm, Left, Right, Up, Down, Power, PageBack, PageForward, NavNext, NavPrevious }; enum class Button { Back, Confirm, Left, Right, Up, Down, Power, PageBack, PageForward, NavNext, NavPrevious };
// Number of values in Button (Back..NavPrevious). Used to size the BLE overlay and
// to clamp persisted BLE mappings. Keep in sync with the enum above.
static constexpr uint8_t kButtonCount = 11;
struct Labels { struct Labels {
const char* btn1; const char* btn1;
@@ -31,23 +28,6 @@ class MappedInputManager {
// Returns the raw front button index that was pressed this frame (or -1 if none). // Returns the raw front button index that was pressed this frame (or -1 if none).
int getPressedFrontButton() const; int getPressedFrontButton() const;
// --- BLE page-turner overlay -------------------------------------------------
// Drain decoded key events from the FreeInk BLE HID host and translate the ones
// bound in SETTINGS.bleKeyMap into per-frame logical-button edges that OR into
// wasPressed()/isPressed()/wasReleased(). Call once per main-loop iteration,
// right after gpio.update() and BleHid.poll(). No-ops when BLE is compiled out.
void pollBle();
// True when a mapped BLE key produced an edge this frame — keeps the inactivity
// / auto-sleep timer alive while a remote is the only input device in use.
bool bleHadActivityThisFrame() const { return bleActivityThisFrame; }
// Capture mode: while on, pollBle() stops mapping events and instead stashes the
// raw decoded key identity so the button-mapping UI can read it without racing the
// live mapping over the single popKey() queue.
void setBleCaptureMode(bool on);
// Pop a captured (kind, value) key identity grabbed while in capture mode.
// Returns false when nothing has been captured since the last call.
bool takeCapturedBleKey(uint8_t& kind, uint8_t& value);
// True when the control axis is flipped relative to the physical buttons: the user opted into // True when the control axis is flipped relative to the physical buttons: the user opted into
// orientation-following front buttons AND the screen is *currently rendered* rotated (INVERTED / // orientation-following front buttons AND the screen is *currently rendered* rotated (INVERTED /
// LANDSCAPE_CCW). Keyed on the live renderer orientation rather than the persisted reader setting, // LANDSCAPE_CCW). Keyed on the live renderer orientation rather than the persisted reader setting,
@@ -64,17 +44,4 @@ class MappedInputManager {
const GfxRenderer& renderer; const GfxRenderer& renderer;
bool mapButton(Button button, bool (HalGPIO::*fn)(uint8_t) const) const; bool mapButton(Button button, bool (HalGPIO::*fn)(uint8_t) const) const;
// OR-in the BLE overlay for a logical button, mirroring mapButton()'s composite
// handling of NavNext/NavPrevious so a remote key bound to Up/Down/Left/Right also
// drives list navigation.
bool bleEdge(const bool* arr, Button button) const;
// Per-frame BLE overlay, indexed by (uint8_t)Button.
bool blePressEdge[kButtonCount] = {}; // press edge this frame -> wasPressed / isPressed
bool bleReleaseEdge[kButtonCount] = {}; // release edge this frame -> wasReleased
bool bleActivityThisFrame = false;
bool bleCaptureMode = false;
bool bleHasCaptured = false;
uint8_t bleCapturedKind = 0xFF;
uint8_t bleCapturedValue = 0;
}; };
-4
View File
@@ -32,10 +32,6 @@ class SdCardFontSystem {
/// Non-const access to the registry (for FontInstaller). /// Non-const access to the registry (for FontInstaller).
SdCardFontRegistry& registry() { return registry_; } SdCardFontRegistry& registry() { return registry_; }
// MEMFIX-PORT: font system audit passthrough; portable
/// Resident heap held by loaded SD fonts (audit; see SdCardFont::reportMemory).
size_t reportFontMemory() const { return manager_.reportMemory(); }
/// Mark the registry as needing re-discovery. /// Mark the registry as needing re-discovery.
/// Thread-safe: can be called from the web server task. /// Thread-safe: can be called from the web server task.
void markRegistryDirty() { registryDirty_.store(true, std::memory_order_release); } void markRegistryDirty() { registryDirty_.store(true, std::memory_order_release); }
-4
View File
@@ -6,7 +6,3 @@
void silentRestart(); // home screen void silentRestart(); // home screen
void silentRestartToReader(); // currently-open EPUB (APP_STATE.openEpubPath) 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();
-11
View File
@@ -44,17 +44,6 @@ class Activity {
virtual bool skipLoopDelay() { return false; } virtual bool skipLoopDelay() { return false; }
virtual bool preventAutoSleep() { return false; } virtual bool preventAutoSleep() { return false; }
virtual bool isReaderActivity() const { return false; } virtual bool isReaderActivity() const { return false; }
// True if this activity needs the BLE stack resident (beyond the readers, which are
// covered by isReaderActivity()). The Bluetooth settings screen overrides this so
// pairing/scanning works there. Everywhere else BLE is torn down to free heap.
virtual bool keepsBluetoothAlive() const { return false; }
// True while the current activity is doing heap-heavy work that must finish
// before the BLE stack (~52 KB) may start.
virtual bool deferBluetoothStart() const { return false; }
// Ask the activity to make its next render a full ghost-cleanup (HALF) refresh rather
// than a fast/partial one. Used after drawing a transient popup over grayscale content
// (e.g. the "BT Connecting..." popup over a reader page) so it clears without ghosting.
virtual void requestGhostCleanup() {}
virtual ScreenshotInfo getScreenshotInfo() const { return {}; } virtual ScreenshotInfo getScreenshotInfo() const { return {}; }
// Start a new activity without destroying the current one // Start a new activity without destroying the current one
-19
View File
@@ -256,25 +256,6 @@ bool ActivityManager::isReaderActivity() const {
(currentActivity && currentActivity->isReaderActivity()); (currentActivity && currentActivity->isReaderActivity());
} }
bool ActivityManager::currentKeepsBluetoothAlive() const {
return currentActivity && currentActivity->keepsBluetoothAlive();
}
void ActivityManager::requestGhostCleanup() {
if (currentActivity) currentActivity->requestGhostCleanup();
}
bool ActivityManager::bluetoothShouldBeActive() const {
const auto wants = [](const auto& activity) {
return activity && (activity->isReaderActivity() || activity->keepsBluetoothAlive());
};
return std::any_of(stackActivities.begin(), stackActivities.end(), wants) || wants(currentActivity);
}
bool ActivityManager::bluetoothStartDeferred() const {
return currentActivity && currentActivity->deferBluetoothStart();
}
bool ActivityManager::skipLoopDelay() const { return currentActivity && currentActivity->skipLoopDelay(); } bool ActivityManager::skipLoopDelay() const { return currentActivity && currentActivity->skipLoopDelay(); }
ScreenshotInfo ActivityManager::getScreenshotInfo() const { ScreenshotInfo ActivityManager::getScreenshotInfo() const {
-13
View File
@@ -102,16 +102,6 @@ class ActivityManager {
bool preventAutoSleep() const; bool preventAutoSleep() const;
bool isReaderActivity() const; bool isReaderActivity() const;
bool currentKeepsBluetoothAlive() const;
// True if BLE should be resident for the current context: any reader (page-turner
// input) or the Bluetooth settings screen (pairing) is on the stack.
bool bluetoothShouldBeActive() const;
// True while the CURRENT activity is mid heap-heavy work that must complete before
// NimBLE may start (see Activity::deferBluetoothStart). Current only, not the
// stack: a reader stacked under a menu has its loop() paused, so its build never
// advances — a stack-wide check would hold BLE off for as long as the menu stays
// open.
bool bluetoothStartDeferred() const;
bool skipLoopDelay() const; bool skipLoopDelay() const;
ScreenshotInfo getScreenshotInfo() const; ScreenshotInfo getScreenshotInfo() const;
@@ -119,9 +109,6 @@ class ActivityManager {
// Otherwise, it will be deferred until the end of the current loop iteration. // Otherwise, it will be deferred until the end of the current loop iteration.
void requestUpdate(bool immediate = false); void requestUpdate(bool immediate = false);
// Ask the current activity to make its next render a ghost-cleanup (HALF) refresh.
void requestGhostCleanup();
// Trigger a render and block until it completes. // Trigger a render and block until it completes.
// Must NOT be called from the render task or while holding a RenderLock. // Must NOT be called from the render task or while holding a RenderLock.
void requestUpdateAndWait(); void requestUpdateAndWait();
@@ -18,8 +18,6 @@
#include "fontIds.h" #include "fontIds.h"
#include "util/QrUtils.h" #include "util/QrUtils.h"
#include "BleInput.h"
namespace { namespace {
// AP Mode configuration // AP Mode configuration
constexpr const char* AP_SSID = "CrossPoint-Reader"; constexpr const char* AP_SSID = "CrossPoint-Reader";
@@ -143,12 +141,6 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode)
if (mode == NetworkMode::JOIN_NETWORK) { if (mode == NetworkMode::JOIN_NETWORK) {
// STA mode - launch WiFi selection // STA mode - launch WiFi selection
LOG_DBG("WEBACT", "Turning on WiFi (STA mode)..."); LOG_DBG("WEBACT", "Turning on WiFi (STA mode)...");
// Free the BLE stack BEFORE bringing WiFi up (matches WifiSelectionActivity):
// the C3 shares one radio and heap between the stacks, and the WiFi driver
// sizes its RX/TX buffer pools at init — initializing it with NimBLE's ~50 KB
// still resident leaves WiFi permanently starved even after the lifecycle
// stops BLE a loop later. No-op when BLE is already off.
bleinput::stop();
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
state = WebServerActivityState::WIFI_SELECTION; state = WebServerActivityState::WIFI_SELECTION;
@@ -201,13 +193,6 @@ void CrossPointWebServerActivity::startAccessPoint() {
LOG_DBG("WEBACT", "Starting Access Point mode..."); LOG_DBG("WEBACT", "Starting Access Point mode...");
LOG_DBG("WEBACT", "Free heap before AP start: %d bytes", ESP.getFreeHeap()); LOG_DBG("WEBACT", "Free heap before AP start: %d bytes", ESP.getFreeHeap());
// Free the BLE stack BEFORE bringing WiFi up (matches WifiSelectionActivity):
// the C3 shares one radio and heap between the stacks, and the WiFi driver
// sizes its RX/TX buffer pools at init — initializing it with NimBLE's ~50 KB
// still resident leaves WiFi permanently starved even after the lifecycle
// stops BLE a loop later. No-op when BLE is already off.
bleinput::stop();
// Configure and start the AP // Configure and start the AP
WiFi.mode(WIFI_AP); WiFi.mode(WIFI_AP);
delay(100); delay(100);
@@ -8,7 +8,6 @@
#include <algorithm> #include <algorithm>
#include "BleInput.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "WifiCredentialStore.h" #include "WifiCredentialStore.h"
@@ -98,11 +97,6 @@ void WifiSelectionActivity::startWifiScan(const bool autoScan) {
networks.clear(); networks.clear();
requestUpdate(); requestUpdate();
// Free the BLE stack before bringing WiFi up: the C3 has one radio and the two
// stacks can't both fit in heap. Reachable from the reader via KOReader sync, where
// BLE is still resident; a no-op when BLE is already off (launched from Settings).
bleinput::stop();
// Set WiFi mode to station // Set WiFi mode to station
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
WiFi.disconnect(); WiFi.disconnect();
@@ -355,7 +349,6 @@ void WifiSelectionActivity::attemptConnection() {
connectionError.clear(); connectionError.clear();
requestUpdate(); requestUpdate();
bleinput::stop(); // free the BLE stack before WiFi (shared C3 radio, tight heap)
WiFi.persistent(false); // Credentials are managed by WifiCredentialStore; suppress SDK NVS auto-connect WiFi.persistent(false); // Credentials are managed by WifiCredentialStore; suppress SDK NVS auto-connect
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
WiFi.disconnect(true, true); // Abort any in-progress SDK auto-connect and clear NVS-saved SSID WiFi.disconnect(true, true); // Abort any in-progress SDK auto-connect and clear NVS-saved SSID
+64 -255
View File
@@ -17,7 +17,6 @@
#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"
@@ -33,15 +32,10 @@
#include "QrDisplayActivity.h" #include "QrDisplayActivity.h"
#include "ReaderUtils.h" #include "ReaderUtils.h"
#include "RecentBooksStore.h" #include "RecentBooksStore.h"
#include "SilentRestart.h"
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
#include "util/BookmarkUtil.h" #include "util/BookmarkUtil.h"
#include "util/ScreenshotUtil.h" #include "util/ScreenshotUtil.h"
#include "SdCardFontSystem.h"
#include <esp_heap_caps.h>
#include "rom/ets_sys.h"
#include "HeapMap.h"
namespace { namespace {
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency() // pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
@@ -71,55 +65,6 @@ bool isInReadFolder(const std::string& path) {
return path.size() > n && path.compare(0, n, READ_FOLDER) == 0 && path[n] == '/'; return path.size() > n && path.compare(0, n, READ_FOLDER) == 0 && path[n] == '/';
} }
class FrameBufferBuildLoan {
public:
explicit FrameBufferBuildLoan(GfxRenderer& renderer) : renderer_(renderer) {}
~FrameBufferBuildLoan() {
if (active_ && !restore()) {
ESP.restart();
}
}
void release() {
if (active_ || !renderer_.hasFrameBuffer()) return;
if (bleinput::startInProgress()) {
LOG_INF("ERS", "Framebuffer loan waiting for BLE start to settle");
const uint32_t deadline = millis() + 1000;
while (bleinput::startInProgress() && millis() < deadline) {
delay(5);
}
}
renderer_.releaseFrameBufferForBuild();
active_ = true;
LOG_DBG("ERS", "Framebuffer lent for section build (ble=%u heap=%u maxAlloc=%u)", BleHid.isRunning() ? 1 : 0,
(unsigned)ESP.getFreeHeap(), (unsigned)ESP.getMaxAllocHeap());
}
bool restore() {
if (!active_) return true;
active_ = false;
if (renderer_.restoreFrameBufferAfterBuild()) {
LOG_DBG("ERS", "Framebuffer restored after section build");
return true;
}
if (BleHid.isRunning()) {
LOG_INF("ERS", "Framebuffer restore needs heap; freeing BLE and retrying (heap=%u maxAlloc=%u)",
(unsigned)ESP.getFreeHeap(), (unsigned)ESP.getMaxAllocHeap());
bleinput::stop();
if (renderer_.restoreFrameBufferAfterBuild()) {
LOG_DBG("ERS", "Framebuffer restored after freeing BLE");
return true;
}
}
LOG_ERR("ERS", "Framebuffer restore failed after section build");
return false;
}
private:
GfxRenderer& renderer_;
bool active_ = false;
};
struct ProgressRange { struct ProgressRange {
float start; float start;
float end; float end;
@@ -212,8 +157,6 @@ void EpubReaderActivity::onEnter() {
ImageBlock::clearSessionRenderFailures(); ImageBlock::clearSessionRenderFailures();
statusBarBleConnected = BleHid.isConnected();
// Configure screen orientation based on settings // Configure screen orientation based on settings
// NOTE: This affects layout math and must be applied before any render calls. // NOTE: This affects layout math and must be applied before any render calls.
ReaderUtils::applyOrientation(renderer, SETTINGS.orientation); ReaderUtils::applyOrientation(renderer, SETTINGS.orientation);
@@ -317,27 +260,13 @@ void EpubReaderActivity::openReaderMenu() {
bool EpubReaderActivity::buildTickHeapGate() { bool EpubReaderActivity::buildTickHeapGate() {
const size_t freeHeap = ESP.getFreeHeap(); const size_t freeHeap = ESP.getFreeHeap();
const size_t maxBlock = ESP.getMaxAllocHeap(); const size_t maxBlock = ESP.getMaxAllocHeap();
if (freeHeap >= BACKGROUND_BUILD_MIN_FREE_HEAP && maxBlock >= BACKGROUND_BUILD_MIN_MAX_ALLOC) { // Below the floors: just wait. The tick is deferrable — page-turn transients
return true; // free up between turns and the tick retries every loop pass. Track the
} // paused state so skipLoopDelay() stops pinning the CPU at full speed while
const size_t lendableFrameBuffer = renderer.hasFrameBuffer() ? renderer.getBufferSize() : 0; // no build work is actually happening (the gate can stay closed for a long
if (lendableFrameBuffer > 0 && freeHeap + lendableFrameBuffer >= BACKGROUND_BUILD_MIN_FREE_HEAP && // stretch if the retained build context itself holds the heap down).
maxBlock + lendableFrameBuffer >= BACKGROUND_BUILD_MIN_MAX_ALLOC) { buildHeapPaused = freeHeap < BACKGROUND_BUILD_MIN_FREE_HEAP || maxBlock < BACKGROUND_BUILD_MIN_MAX_ALLOC;
return true; return !buildHeapPaused;
}
// Below the floors. If the BLE stack is what's squeezing the heap, shed it — the
// established policy on this branch is that builds and resident BLE don't coexist,
// and this was the one build path without that protection (field crash: a tick's
// parse allocation aborted at maxAlloc ~11 KB with BLE resident). The lifecycle's
// build-pending deferral keeps BLE down until the window is caught up, then
// restarts it behind the start floor. Without BLE resident, just wait: page-turn
// transients free up between turns and the tick retries every loop pass.
if (BleHid.isRunning()) {
LOG_INF("ERS", "Background build needs heap (free=%u maxAlloc=%u); freeing BLE RAM", (unsigned)freeHeap,
(unsigned)maxBlock);
bleinput::stop();
}
return false;
} }
void EpubReaderActivity::loop() { void EpubReaderActivity::loop() {
@@ -347,19 +276,6 @@ void EpubReaderActivity::loop() {
return; return;
} }
// Swap the "BT connecting" status bar placeholder back to the chapter/book
// title the moment the connection completes (and back on disconnect). Skips
// while a render is in flight or the framebuffer is lent to a build; the
// next page render shows the right state anyway.
if (SETTINGS.bluetoothEnabled && section && renderer.hasFrameBuffer() && !RenderLock::peek()) {
const bool connected = BleHid.isConnected();
if (connected != statusBarBleConnected) {
statusBarBleConnected = connected;
renderStatusBar();
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
}
}
// Idle glyph prewarm for the likely next page (currentPage + 1). The scan // Idle glyph prewarm for the likely next page (currentPage + 1). The scan
// pass draws nothing (FCM scan mode suppresses pixels), so the displayed // pass draws nothing (FCM scan mode suppresses pixels), so the displayed
// framebuffer is untouched; endScanAndPrewarm loads only glyphs not already // framebuffer is untouched; endScanAndPrewarm loads only glyphs not already
@@ -370,13 +286,17 @@ void EpubReaderActivity::loop() {
constexpr unsigned long IDLE_PREWARM_DEBOUNCE_MS = 400; constexpr unsigned long IDLE_PREWARM_DEBOUNCE_MS = 400;
if (section && !section->isBuilding() && !RenderLock::peek() && renderer.hasFrameBuffer() && if (section && !section->isBuilding() && !RenderLock::peek() && renderer.hasFrameBuffer() &&
lastRenderCompleteMs != 0 && millis() - lastRenderCompleteMs > IDLE_PREWARM_DEBOUNCE_MS && lastRenderCompleteMs != 0 && millis() - lastRenderCompleteMs > IDLE_PREWARM_DEBOUNCE_MS &&
ESP.getFreeHeap() > RENDER_MIN_FREE_HEAP && ESP.getFreeHeap() > RENDER_MIN_FREE_HEAP && ESP.getMaxAllocHeap() > BACKGROUND_BUILD_MIN_MAX_ALLOC &&
(idlePrewarmSpine != currentSpineIndex || idlePrewarmPage != section->currentPage)) {
RenderLock lock; // the page table must not change under the scan
// Re-check under the lock: peek() and acquisition are not atomic, so the render
// task may have reset/replaced the section or moved the page in between.
if (section && !section->isBuilding() &&
(idlePrewarmSpine != currentSpineIndex || idlePrewarmPage != section->currentPage)) { (idlePrewarmSpine != currentSpineIndex || idlePrewarmPage != section->currentPage)) {
idlePrewarmSpine = currentSpineIndex; idlePrewarmSpine = currentSpineIndex;
idlePrewarmPage = section->currentPage; idlePrewarmPage = section->currentPage;
const int nextPage = section->currentPage + 1; const int nextPage = section->currentPage + 1;
if (nextPage < static_cast<int>(section->pageCount)) { if (nextPage < static_cast<int>(section->pageCount)) {
RenderLock lock; // the page table must not change under the scan
if (const auto p = section->loadPage(nextPage)) { if (const auto p = section->loadPage(nextPage)) {
if (auto* fcm = renderer.getFontCacheManager()) { if (auto* fcm = renderer.getFontCacheManager()) {
const auto t0 = millis(); const auto t0 = millis();
@@ -388,6 +308,7 @@ void EpubReaderActivity::loop() {
} }
} }
} }
}
// Lazily resume a partial's extension build once the reader nears its watermark. Far from // Lazily resume a partial's extension build once the reader nears its watermark. Far from
// it the rebuild is all cost (whole-chapter re-layout from page 0) and no benefit this // it the rebuild is all cost (whole-chapter re-layout from page 0) and no benefit this
@@ -429,12 +350,12 @@ void EpubReaderActivity::loop() {
RenderLock lock; RenderLock lock;
// Re-check under the lock: render() (which also holds the RenderLock) may have finalized the // Re-check under the lock: render() (which also holds the RenderLock) may have finalized the
// build between the outer isBuilding() check and acquiring the lock here, in which case // build between the outer isBuilding() check and acquiring the lock here, in which case
// buildSomeMore() would fail and wrongly reset the section. cppcheck can't see the cross-task // buildSomeMore() would fail and wrongly reset the section. The heap gate must be re-read
// mutation, so it flags this as always true. // too: a render that won the lock race can expand retained glyph buffers, invalidating the
// pre-lock heap reading. cppcheck can't see the cross-task mutation, so it flags this as
// always true.
// cppcheck-suppress knownConditionTrueFalse // cppcheck-suppress knownConditionTrueFalse
if (section->isBuilding()) { if (section->isBuilding() && buildTickHeapGate()) {
FrameBufferBuildLoan buildLoan(renderer);
buildLoan.release();
if (!section->buildSomeMore(BACKGROUND_BUILD_PAGES_PER_TICK)) { if (!section->buildSomeMore(BACKGROUND_BUILD_PAGES_PER_TICK)) {
LOG_ERR("ERS", "Background section build failed"); LOG_ERR("ERS", "Background section build failed");
section.reset(); section.reset();
@@ -444,9 +365,6 @@ void EpubReaderActivity::loop() {
// real page count, so re-render at the remapped page. No-op for an unchanged resume. // real page count, so re-render at the remapped page. No-op for an unchanged resume.
requestUpdate(); requestUpdate();
} }
if (!buildLoan.restore()) {
ESP.restart();
}
} }
} }
@@ -1048,17 +966,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
return; return;
} }
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_INF("ERS", "Render heap %u below floor %u; lending framebuffer", (unsigned)ESP.getFreeHeap(),
(unsigned)RENDER_MIN_FREE_HEAP);
buildLoan.release();
}
const auto showPendingSyncSaveError = [this]() { const auto showPendingSyncSaveError = [this]() {
if (!pendingSyncSaveError) return; if (!pendingSyncSaveError) return;
pendingSyncSaveError = false; pendingSyncSaveError = false;
@@ -1068,11 +975,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
// A section build failure (e.g. an invalid/corrupt EPUB that fails XML parsing) leaves the // 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. // "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. // clearScreen first so the error popup doesn't overlay the stale "Indexing" popup.
const auto showBuildError = [this, &buildLoan]() { const auto showBuildError = [this]() {
if (!buildLoan.restore()) {
ESP.restart();
return;
}
renderer.clearScreen(); renderer.clearScreen();
GUI.drawPopup(renderer, tr(STR_INDEX_FAILED)); GUI.drawPopup(renderer, tr(STR_INDEX_FAILED));
automaticPageTurnActive = false; automaticPageTurnActive = false;
@@ -1157,44 +1060,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
LOG_DBG("ERS", "Cache not found, building..."); 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 size_t lendableFrameBuffer = renderer.hasFrameBuffer() ? renderer.getBufferSize() : 0;
const bool heapTooLow = ESP.getFreeHeap() + lendableFrameBuffer < BUILD_MIN_FREE_HEAP;
if (heapTooLow) {
LOG_ERR("ERS", "Pre-build heap %u (+fb %u) below floor %u; entering build recovery",
(unsigned)ESP.getFreeHeap(), (unsigned)lendableFrameBuffer, (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 (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. The main-loop lifecycle restarts BLE later, behind its activity,
// render-lock, framebuffer, and heap gates.
const auto retryWithBleFreed = [&](auto&& buildFn) {
LOG_INF("ERS", "Section build needs heap; freeing BLE RAM and retrying");
bleinput::stop();
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, // Jumps that need the final pagination or the anchor map -- explicit page jumps,
// fragment anchors, percent jumps, and cross-setting progress repositioning -- can't // fragment anchors, percent jumps, and cross-setting progress repositioning -- can't
// resolve their landing page until the whole chapter is laid out, so they take the full // resolve their landing page until the whole chapter is laid out, so they take the full
@@ -1221,18 +1086,10 @@ void EpubReaderActivity::render(RenderLock&& lock) {
// Lend the framebuffer's 48 KB to the blocking full build; restored // Lend the framebuffer's 48 KB to the blocking full build; restored
// (white) at scope exit, and the page render below redraws everything. // (white) at scope exit, and the page render below redraws everything.
GfxRenderer::FrameBufferLoan loan(renderer); GfxRenderer::FrameBufferLoan loan(renderer);
const auto buildSection = [&]() { if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
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 = !heapTooLow && buildSection();
if (!built && SETTINGS.bluetoothEnabled) {
built = retryWithBleFreed(buildSection);
}
if (!built) {
silentRestartDefrag();
LOG_ERR("ERS", "Failed to persist page data to SD"); LOG_ERR("ERS", "Failed to persist page data to SD");
section.reset(); section.reset();
loan.end(); // restore before anything draws loan.end(); // restore before anything draws
@@ -1290,21 +1147,10 @@ void EpubReaderActivity::render(RenderLock&& lock) {
// background buildSomeMore chunks in loop() do NOT get the loan: they // background buildSomeMore chunks in loop() do NOT get the loan: they
// deliberately interleave with page renders. Restored before render. // deliberately interleave with page renders. Restored before render.
GfxRenderer::FrameBufferLoan loan(renderer); GfxRenderer::FrameBufferLoan loan(renderer);
// startBuild does the zip inflate (the big contiguous allocation), so it gets if (!section->startBuild(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
// the BLE free-and-retry fallback too; it cleans up fully on failure, making a
// retry safe.
const auto beginBuild = [&]() {
return section->startBuild(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); SETTINGS.imageRendering, SETTINGS.focusReadingEnabled)) {
};
bool started = !heapTooLow && beginBuild();
if (!started && SETTINGS.bluetoothEnabled) {
started = retryWithBleFreed(beginBuild);
}
if (!started) {
silentRestartDefrag();
LOG_ERR("ERS", "Failed to start section build"); LOG_ERR("ERS", "Failed to start section build");
section.reset(); section.reset();
loan.end(); // restore before anything draws (showBuildError renders a popup) loan.end(); // restore before anything draws (showBuildError renders a popup)
@@ -1379,7 +1225,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
} }
while (section->isPartial() && section->currentPage >= static_cast<int>(section->pageCount)) { while (section->isPartial() && section->currentPage >= static_cast<int>(section->pageCount)) {
// Start a build to extend a partial toward the requested page. // Start a build to extend a partial toward the requested page.
buildLoan.release();
if (!section->isBuilding() && if (!section->isBuilding() &&
!section->startBuild(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), !section->startBuild(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(),
SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight,
@@ -1402,7 +1247,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
} }
// For an in-progress incremental build, make sure the page we're about to show has been laid out. // For an in-progress incremental build, make sure the page we're about to show has been laid out.
if (section->isBuilding()) { if (section->isBuilding()) {
buildLoan.release();
while (!section->isBuildComplete() && section->currentPage >= static_cast<int>(section->pageCount)) { while (!section->isBuildComplete() && section->currentPage >= static_cast<int>(section->pageCount)) {
if (!section->buildSomeMore(BUILD_PAGES_PER_CHUNK)) { if (!section->buildSomeMore(BUILD_PAGES_PER_CHUNK)) {
LOG_ERR("ERS", "Failed during incremental section build"); LOG_ERR("ERS", "Failed during incremental section build");
@@ -1413,14 +1257,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
} }
} }
const auto restoreFramebufferForDraw = [&buildLoan]() {
if (!buildLoan.restore()) {
ESP.restart();
return false;
}
return true;
};
// The requested page is now as built as it will get. If it still lands past the end, // The requested page is now as built as it will get. If it still lands past the end,
// clamp to the last real page: the UINT16_MAX "last page" sentinel from backward chapter // clamp to the last real page: the UINT16_MAX "last page" sentinel from backward chapter
// navigation, an explicit jump beyond a finished chapter, or a stale saved position. // navigation, an explicit jump beyond a finished chapter, or a stale saved position.
@@ -1435,10 +1271,10 @@ void EpubReaderActivity::render(RenderLock&& lock) {
// a plain resume / unchanged pagination). If still building, this defers to loop() on completion. // a plain resume / unchanged pagination). If still building, this defers to loop() on completion.
applyDeferredReposition(); applyDeferredReposition();
renderer.clearScreen();
if (section->pageCount == 0) { if (section->pageCount == 0) {
LOG_DBG("ERS", "No pages to render"); LOG_DBG("ERS", "No pages to render");
if (!restoreFramebufferForDraw()) return;
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_EMPTY_CHAPTER), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_EMPTY_CHAPTER), true, EpdFontFamily::BOLD);
renderStatusBar(); renderStatusBar();
renderer.displayBuffer(); renderer.displayBuffer();
@@ -1449,8 +1285,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
if (section->currentPage < 0 || section->currentPage >= section->pageCount) { if (section->currentPage < 0 || section->currentPage >= section->pageCount) {
LOG_DBG("ERS", "Page out of bounds: %d (max %d)", section->currentPage, section->pageCount); LOG_DBG("ERS", "Page out of bounds: %d (max %d)", section->currentPage, section->pageCount);
if (!restoreFramebufferForDraw()) return;
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_OUT_OF_BOUNDS), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_OUT_OF_BOUNDS), true, EpdFontFamily::BOLD);
renderStatusBar(); renderStatusBar();
renderer.displayBuffer(); renderer.displayBuffer();
@@ -1479,7 +1313,6 @@ void EpubReaderActivity::render(RenderLock&& lock) {
if (giveUp) { if (giveUp) {
LOG_ERR("ERS", "Page load retry limit reached, aborting"); LOG_ERR("ERS", "Page load retry limit reached, aborting");
pageLoadRetryCount = 0; // Reset so a later user-initiated navigation can try afresh pageLoadRetryCount = 0; // Reset so a later user-initiated navigation can try afresh
if (!restoreFramebufferForDraw()) return;
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_PAGE_LOAD_ERROR), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_PAGE_LOAD_ERROR), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
@@ -1495,48 +1328,10 @@ void EpubReaderActivity::render(RenderLock&& lock) {
// Collect footnotes from the loaded page // Collect footnotes from the loaded page
currentPageFootnotes = std::move(p->footnotes); currentPageFootnotes = std::move(p->footnotes);
if (!restoreFramebufferForDraw()) return;
renderer.clearScreen();
const auto start = millis(); const auto start = millis();
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
LOG_DBG("ERS", "Rendered page in %dms", millis() - start); LOG_DBG("ERS", "Rendered page in %dms", millis() - start);
lastRenderCompleteMs = millis(); lastRenderCompleteMs = millis();
// Fragmentation tracker: free vs largest block after every page. A falling
// maxAlloc/free ratio across pages points at whichever allocation pattern the
// preceding lines show (mini rebuilds, kern reloads, BLE churn).
LOG_DBG("MEM", "post-render: free=%u maxAlloc=%u", (unsigned)ESP.getFreeHeap(), (unsigned)ESP.getMaxAllocHeap());
{
// MEMFIX-PORT: per-render heap owner audit + one-shot block map; portable
// Heap audit: attribute resident heap to its owners so margin work is
// measurement-driven. `other` = IDF/Arduino baseline + SdFat + epub
// metadata + BLE (when on) + anything not yet instrumented.
const size_t fontBytes = sdFontSystem.reportFontMemory();
const size_t sectionBytes = section ? section->residentBytes() : 0;
const size_t epubBytes = epub ? epub->residentBytes() : 0;
const size_t fbBytes = renderer.hasFrameBuffer() ? renderer.getBufferSize() : 0;
const uint32_t heapTotal = ESP.getHeapSize();
const uint32_t heapFree = ESP.getFreeHeap();
LOG_DBG("MEM",
"audit: total=%u free=%u maxAlloc=%u fb=%u fonts=%u section=%u epub=%u cssRules=%u building=%d ble=%d "
"other=%d",
heapTotal, heapFree, (unsigned)ESP.getMaxAllocHeap(), (unsigned)fbBytes, (unsigned)fontBytes,
(unsigned)sectionBytes, (unsigned)epubBytes, (unsigned)(epub ? epub->cssRuleCount() : 0),
section && section->isBuilding() ? 1 : 0, BleHid.isRunning() ? 1 : 0,
(int)(heapTotal - heapFree - fbBytes - fontBytes - sectionBytes - epubBytes));
// One-shot heap block map for fragmentation analysis: dumps every block
// (address/size/free) once per boot, at the first settled (non-building)
// audit, so mid-heap long-lived allocations identify themselves. ~2-4 s
// of serial output; debug builds only.
static bool heapMapDumped = false;
if (!heapMapDumped && !(section && section->isBuilding())) {
heapMapDumped = true;
// Landmarks: known owners' addresses, so map blocks self-identify.
LOG_DBG("MEM", "landmark framebuffer=%p section=%p epub=%p activity=%p", renderer.getFrameBuffer(),
static_cast<void*>(section.get()), static_cast<void*>(epub.get()), static_cast<void*>(this));
heapmap::dump();
}
}
} }
// Only persist when the position actually changed. render() also runs on menu, // Only persist when the position actually changed. render() also runs on menu,
// bookmark and screenshot re-renders, and writeAtomic is several FAT ops for 6 bytes. // bookmark and screenshot re-renders, and writeAtomic is several FAT ops for 6 bytes.
@@ -1608,9 +1403,11 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
const bool needsAnyGrayscale = needsTextGrayscale || pageHasImages; const bool needsAnyGrayscale = needsTextGrayscale || pageHasImages;
const bool tiledGrayscale = needsAnyGrayscale && renderer.supportsStripGrayscale(); const bool tiledGrayscale = needsAnyGrayscale && renderer.supportsStripGrayscale();
// Whole-plane buffering only pays when the BW refresh genuinely runs async // Whole-plane buffering only pays when the BW refresh genuinely runs async
// underneath it; on blocking panels it would just spend ~50 KB for the // underneath it; on blocking panels (X3) it would just spend ~50 KB for the
// identical serial timing. // identical serial timing. Image pages take the blocking double-FAST path
const bool overlapRefresh = tiledGrayscale && renderer.supportsAsyncRefresh(); // below (no async refresh is ever started), so they'd spend the buffers with
// nothing in flight to overlap.
const bool overlapRefresh = tiledGrayscale && renderer.supportsAsyncRefresh() && !pageHasImages;
auto renderGrayscalePass = [&]() { auto renderGrayscalePass = [&]() {
if (needsTextGrayscale) { if (needsTextGrayscale) {
page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop); page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop);
@@ -1656,9 +1453,9 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
// regardless of residue. // regardless of residue.
pagesUntilFullRefresh = 1; pagesUntilFullRefresh = 1;
} else { } else {
// Deferred when a tiled grayscale pass follows: the plane rendering below // Async form: start the waveform and return so the grayscale plane rendering
// then overlaps the panel's refresh time instead of following it. // below overlaps the panel's refresh time instead of following it.
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh, /*async=*/overlapRefresh); ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh, overlapRefresh);
} }
const auto tDisplay = millis(); const auto tDisplay = millis();
@@ -1693,13 +1490,25 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
// Tiered on heap pressure: two plane buffers hide both plane renders // Tiered on heap pressure: two plane buffers hide both plane renders
// inside the refresh wait; one hides the LSB render (its buffer is reused // inside the refresh wait; one hides the LSB render (its buffer is reused
// for MSB after streaming); none falls back to the strip-scratch flow with // for MSB after streaming); none falls back to the strip-scratch flow with
// no overlap. The MSB buffer is only attempted when it leaves ~60 KB free // no overlap. Each buffer is only attempted when it leaves ~60 KB free so
// so the pass never starves concurrent allocations (BLE especially). // the pass never starves concurrent allocations: the next page re-render
// Blocking panels skip the buffers entirely (nothing to overlap). // allocates through throwing std::string paths that abort() on OOM under
auto lsbPlaneBuf = overlapRefresh ? makeUniqueNoThrow<uint8_t[]>(planeBytes) : nullptr; // -fno-exceptions, so a plane buffer that "fits" but eats the render
auto msbPlaneBuf = (lsbPlaneBuf && ESP.getFreeHeap() >= planeBytes + 60000) // headroom is worse than the strip fallback. Blocking panels skip the
? makeUniqueNoThrow<uint8_t[]>(planeBytes) // buffers entirely (nothing to overlap).
: nullptr; constexpr size_t PLANE_BUF_HEADROOM = 60000;
// Free-heap alone ignores fragmentation: taking the largest block for a
// plane can leave only slivers behind even when total headroom looks fine.
// Require the block to fit the plane with 16 KB contiguous to spare, which
// also keeps the advance-table batch scratch viable mid-render (same
// rationale as BACKGROUND_BUILD_MIN_MAX_ALLOC).
constexpr size_t PLANE_BUF_MAX_ALLOC_RESERVE = 16 * 1024;
const auto planeBufFits = [planeBytes] {
return ESP.getFreeHeap() >= planeBytes + PLANE_BUF_HEADROOM &&
ESP.getMaxAllocHeap() >= planeBytes + PLANE_BUF_MAX_ALLOC_RESERVE;
};
auto lsbPlaneBuf = (overlapRefresh && planeBufFits()) ? makeUniqueNoThrow<uint8_t[]>(planeBytes) : nullptr;
auto msbPlaneBuf = (lsbPlaneBuf && planeBufFits()) ? makeUniqueNoThrow<uint8_t[]>(planeBytes) : nullptr;
if (lsbPlaneBuf) { if (lsbPlaneBuf) {
renderPlaneToBuffer(true, lsbPlaneBuf.get()); renderPlaneToBuffer(true, lsbPlaneBuf.get());
@@ -1733,13 +1542,20 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tGrayRender - tDisplay, tWait - tGrayRender, tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tGrayRender - tDisplay, tWait - tGrayRender,
tGrayWrite - tWait, tGrayDisplay - tGrayWrite, tEnd - tGrayDisplay, tEnd - t0, msbPlaneBuf ? 2 : 1); tGrayWrite - tWait, tGrayDisplay - tGrayWrite, tEnd - tGrayDisplay, tEnd - t0, msbPlaneBuf ? 2 : 1);
} else { } else {
// Per-strip scratch tier: blocking panels and the OOM fallback. The // Per-strip scratch tier: blocking panels (X3) and the OOM fallback.
// strip writes below need the panel idle, so wait out any pending async // The strip writes below need the panel idle, so wait out any pending
// refresh first (no-op on blocking panels). // async refresh first (no-op on blocking panels).
auto scratch = makeUniqueNoThrow<uint8_t[]>(static_cast<size_t>(gwBytes) * STRIP_ROWS); auto scratch = makeUniqueNoThrow<uint8_t[]>(static_cast<size_t>(gwBytes) * STRIP_ROWS);
renderer.waitRefreshComplete(); renderer.waitRefreshComplete();
if (!scratch) { if (!scratch) {
LOG_ERR("ERS", "OOM: grayscale strip scratch (%d bytes); skipping AA this page", gwBytes * STRIP_ROWS); LOG_ERR("ERS", "OOM: grayscale strip scratch (%d bytes); skipping AA this page", gwBytes * STRIP_ROWS);
if (overlapRefresh) {
// The BW refresh ran the shadow-free async path, so controller RAM's
// differential baseline was never rebuilt. Even with AA skipped it must
// be re-synced from the intact BW framebuffer, or the next differential
// update diffs against stale contents.
renderer.cleanupGrayscaleWithFrameBuffer();
}
} else { } else {
// Bands may be streamed in any order: X4 windows each via setRamArea, // Bands may be streamed in any order: X4 windows each via setRamArea,
// X3 via PTL. // X3 via PTL.
@@ -1869,15 +1685,8 @@ void EpubReaderActivity::renderStatusBar() const {
title = epub->getTitle(); title = epub->getTitle();
} }
if (SETTINGS.bluetoothEnabled && !BleHid.isConnected()) {
// Take over the title slot entirely while connecting; the watcher in
// loop() redraws the bar on the connect/disconnect flip, restoring the
// chapter/book title.
title = tr(STR_BT_CONNECTING_POPUP);
}
GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset, true, currentPageBookmarked, GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset, true, currentPageBookmarked,
section->isBuilding(), BleHid.isConnected()); section->isBuilding());
} }
void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) { void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) {
+23 -37
View File
@@ -41,11 +41,6 @@ class EpubReaderActivity final : public Activity {
bool showBookmarkMessage = false; bool showBookmarkMessage = false;
bool ignoreNextConfirmRelease = false; bool ignoreNextConfirmRelease = false;
bool currentPageBookmarked = false; bool currentPageBookmarked = false;
// BLE connection state the status bar last rendered with; loop() watches for
// a flip and redraws the bar so the "BT connecting" placeholder swaps back
// to the chapter/book title the moment the connection completes (and
// returns on disconnect) instead of waiting for the next page turn.
bool statusBarBleConnected = false;
// Idle-time glyph prewarm: after a page settles, scan the LIKELY next page // Idle-time glyph prewarm: after a page settles, scan the LIKELY next page
// (scan mode draws nothing) and load its missing glyphs from SD during idle, // (scan mode draws nothing) and load its missing glyphs from SD during idle,
// so the next turn's in-render prewarm is a cache hit instead of ~100 ms of // so the next turn's in-render prewarm is a cache hit instead of ~100 ms of
@@ -75,21 +70,6 @@ class EpubReaderActivity final : public Activity {
SavedPosition savedPositions[MAX_FOOTNOTE_DEPTH] = {}; SavedPosition savedPositions[MAX_FOOTNOTE_DEPTH] = {};
int footnoteDepth = 0; 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;
// Heap floor for rendering a page at all. Page deserialization (TextBlock word
// vectors/strings) and glyph caching allocate through throwing paths that abort()
// on OOM; below this floor render() sheds the BLE stack (~52 KB back, and it
// restores a large contiguous block) before touching the page. Field data: a
// session with no shed ground to <2.2 KB free and aborted on a page load.
static constexpr size_t RENDER_MIN_FREE_HEAP = 24 * 1024;
// Viewport of the last render(), captured so loop()'s lazy partial-extension start // Viewport of the last render(), captured so loop()'s lazy partial-extension start
// builds with IDENTICAL layout parameters to the pages already rendered (a mismatch // builds with IDENTICAL layout parameters to the pages already rendered (a mismatch
// would paginate differently than the partial being extended). 0 = no render yet. // would paginate differently than the partial being extended). 0 = no render yet.
@@ -114,27 +94,32 @@ class EpubReaderActivity final : public Activity {
static constexpr int BUILD_PAGES_PER_CHUNK = 8; static constexpr int BUILD_PAGES_PER_CHUNK = 8;
static constexpr int BACKGROUND_BUILD_PAGES_PER_TICK = 2; static constexpr int BACKGROUND_BUILD_PAGES_PER_TICK = 2;
// MEMFIX-PORT: background-build heap floor; portable
// Skip background build ticks below this free-heap floor. The parse path grows // Skip background build ticks below this free-heap floor. The parse path grows
// word vectors of heap strings — throwing allocations that abort() on OOM under // word vectors of heap strings — throwing allocations that abort() on OOM under
// -fno-exceptions (field crash: bad_alloc in ParsedText::addWord during a // -fno-exceptions (field crash: bad_alloc in ParsedText::addWord during a
// background tick with the BLE stack resident). The tick is deferrable work: // background tick under heap pressure). The tick is deferrable work:
// page-turn transients free up between turns and the build resumes; the render // page-turn transients free up between turns and the build resumes; the render
// path still builds the page it actually needs regardless of this floor. // path still builds the page it actually needs regardless of this floor.
// Calibrated BETWEEN the measured states: steady reading with BLE resident runs at static constexpr size_t BACKGROUND_BUILD_MIN_FREE_HEAP = 32 * 1024;
// ~29.4 KB free / ~16.4 KB largest block (ticks are safe there — a 2-page parse // Fragmentation floor for the same gate: a tick passed the free-heap floor at
// transient is a few KB), while the field crash happened at 34.7 KB free with an // 34.7 KB free but the largest block was ~11 KB, and a parse allocation inside the
// ~11 KB largest block. A first cut at 32 KB/16 KB sat just ABOVE the healthy // tick aborted anyway. Free heap says how much memory exists; maxAlloc says whether
// steady state, guaranteeing a pointless BLE shed the moment any build work was // any single allocation can actually have it. 16 KB also keeps the advance-table
// pending (the maxAlloc floor fired on a 12-byte shortfall). // batch path (16 KB scratch) viable during builds.
static constexpr size_t BACKGROUND_BUILD_MIN_FREE_HEAP = 26 * 1024; static constexpr size_t BACKGROUND_BUILD_MIN_MAX_ALLOC = 16 * 1024;
// Fragmentation floor for the same gate: free heap says how much memory exists;
// maxAlloc says whether any single allocation can actually have it.
static constexpr size_t BACKGROUND_BUILD_MIN_MAX_ALLOC = 13 * 1024;
// Gate for a background build tick: true when the heap can take parse allocations. // Gate for a background build tick: true when the heap can take parse allocations.
// When BLE is what's squeezing the heap, sheds it (build-pending deferral in the // Updates buildHeapPaused as a side effect.
// lifecycle then holds restarts off until the window is caught up) instead of
// stalling the build forever below the floors.
bool buildTickHeapGate(); bool buildTickHeapGate();
// True while the background build is gated on the heap floors. Lets skipLoopDelay()
// return the loop to normal delay/power-saving during the pause: isBuilding() stays
// true the whole time, and without this the loop would spin at full CPU speed doing
// no build work — indefinitely, if the build context itself keeps the heap low.
bool buildHeapPaused = false;
// Heap floor for optional render-adjacent work (idle prewarm). Page
// deserialization (TextBlock word vectors/strings) and glyph caching allocate
// through throwing paths that abort() on OOM; skip deferrable work below it.
static constexpr size_t RENDER_MIN_FREE_HEAP = 24 * 1024;
// How many pages to keep laid out ahead of the reader for a still-building section. A page // How many pages to keep laid out ahead of the reader for a still-building section. A page
// turn is ~1s on e-ink and a page builds in ~30ms, so the reader can't out-click the builder // turn is ~1s on e-ink and a page builds in ~30ms, so the reader can't out-click the builder
// -- a tiny buffer is enough. The background build stops once the watermark is this far // -- a tiny buffer is enough. The background build stops once the watermark is this far
@@ -192,10 +177,11 @@ class EpubReaderActivity final : public Activity {
// Full CPU speed + fast loop ticks while a section build runs: at the low-power // Full CPU speed + fast loop ticks while a section build runs: at the low-power
// frequency a giant chapter's background rebuild stretches from ~40s to many // frequency a giant chapter's background rebuild stretches from ~40s to many
// minutes, so the reader exits before it can finalize and the next open restarts // minutes, so the reader exits before it can finalize and the next open restarts
// it from page 0. Reverts to normal power behavior the moment the build finishes. // it from page 0. Reverts to normal power behavior the moment the build finishes,
bool skipLoopDelay() override { return section && section->isBuilding(); } // and while the build is heap-paused (no work is happening, so spinning at full
// speed would only burn battery; the paused gate still retries every loop pass).
bool skipLoopDelay() override { return section && section->isBuilding() && !buildHeapPaused; }
bool isReaderActivity() const override { return true; } bool isReaderActivity() const override { return true; }
void requestGhostCleanup() override { pagesUntilFullRefresh = 1; }
ScreenshotInfo getScreenshotInfo() const override; ScreenshotInfo getScreenshotInfo() const override;
CrossPointPosition getCurrentPosition() const; CrossPointPosition getCurrentPosition() const;
}; };
@@ -2,12 +2,8 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h> #include <I18n.h>
#include <Logging.h>
#include "BleInput.h"
#include "CrossPointSettings.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "SilentRestart.h"
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
@@ -26,7 +22,7 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu
std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes,
bool hasBookmarks) { bool hasBookmarks) {
std::vector<MenuItem> items; std::vector<MenuItem> items;
items.reserve(13); items.reserve(12);
items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}); items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER});
if (hasFootnotes) { if (hasFootnotes) {
items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES}); items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES});
@@ -40,7 +36,6 @@ std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuI
items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}); items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT});
items.push_back({MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON}); items.push_back({MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON});
items.push_back({MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR}); items.push_back({MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR});
items.push_back({MenuAction::TOGGLE_BLUETOOTH, StrId::STR_TOGGLE_BLUETOOTH});
items.push_back({MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON}); items.push_back({MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON});
items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS}); items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS});
items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}); items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE});
@@ -90,24 +85,6 @@ void EpubReaderMenuActivity::loop() {
return; return;
} }
if (selectedAction == MenuAction::TOGGLE_BLUETOOTH) {
// Just flip the preference and stay in the menu. The main-loop lifecycle check
// brings the BLE stack up/down to match, 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 wait
// 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;
}
setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation, selectedPageTurnOption}); setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation, selectedPageTurnOption});
finish(); finish();
return; return;
@@ -159,12 +136,6 @@ void EpubReaderMenuActivity::render(RenderLock&&) {
} else if (value == MenuAction::AUTO_PAGE_TURN) { } else if (value == MenuAction::AUTO_PAGE_TURN) {
// Render current page turn value on the right edge of the content area. // Render current page turn value on the right edge of the content area.
return pageTurnLabels[selectedPageTurnOption]; return pageTurnLabels[selectedPageTurnOption];
} else if (value == MenuAction::TOGGLE_BLUETOOTH) {
if (SETTINGS.bluetoothEnabled) {
if (!BleHid.isRunning()) return tr(STR_CONNECTING);
return BleHid.isConnected() ? tr(STR_STATE_ON) : tr(STR_CONNECTING);
}
return tr(STR_STATE_OFF);
} else { } else {
return ""; return "";
} }
@@ -24,8 +24,7 @@ class EpubReaderMenuActivity final : public Activity {
DISPLAY_QR, DISPLAY_QR,
GO_HOME, GO_HOME,
SYNC, SYNC,
DELETE_CACHE, DELETE_CACHE
TOGGLE_BLUETOOTH
}; };
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
+1 -2
View File
@@ -31,6 +31,5 @@ class ReaderActivity final : public Activity {
explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath) explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath)
: Activity("Reader", renderer, mappedInput), initialBookPath(std::move(initialBookPath)) {} : Activity("Reader", renderer, mappedInput), initialBookPath(std::move(initialBookPath)) {}
void onEnter() override; void onEnter() override;
bool isReaderActivity() const override { return false; } bool isReaderActivity() const override { return true; }
bool deferBluetoothStart() const override { return true; }
}; };
@@ -49,6 +49,5 @@ class TxtReaderActivity final : public Activity {
void loop() override; void loop() override;
void render(RenderLock&&) override; void render(RenderLock&&) override;
bool isReaderActivity() const override { return true; } bool isReaderActivity() const override { return true; }
void requestGhostCleanup() override { pagesUntilFullRefresh = 1; }
ScreenshotInfo getScreenshotInfo() const override; ScreenshotInfo getScreenshotInfo() const override;
}; };
@@ -46,6 +46,5 @@ class XtcReaderActivity final : public Activity {
void loop() override; void loop() override;
void render(RenderLock&&) override; void render(RenderLock&&) override;
bool isReaderActivity() const override { return true; } bool isReaderActivity() const override { return true; }
void requestGhostCleanup() override { pagesUntilFullRefresh = 1; }
ScreenshotInfo getScreenshotInfo() const override; ScreenshotInfo getScreenshotInfo() const override;
}; };
@@ -1,167 +0,0 @@
#include "BleButtonMapActivity.h"
#include <GfxRenderer.h>
#include <algorithm>
#include <cstdio>
#include <iterator>
#include "BleInput.h"
#include "CrossPointSettings.h"
#include "components/UITheme.h"
#include "fontIds.h"
// Logical functions offered for binding. Page navigation + confirm cover Free2 /
// Free3; the directions are included so a remote can also drive menu navigation.
const BleButtonMapActivity::Fn BleButtonMapActivity::kFunctions[] = {
{MappedInputManager::Button::PageForward, StrId::STR_BT_PAGE_FORWARD},
{MappedInputManager::Button::PageBack, StrId::STR_BT_PAGE_BACK},
{MappedInputManager::Button::Confirm, StrId::STR_CONFIRM},
{MappedInputManager::Button::Back, StrId::STR_BACK},
{MappedInputManager::Button::Up, StrId::STR_DIR_UP},
{MappedInputManager::Button::Down, StrId::STR_DIR_DOWN},
{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]));
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.
std::fill(std::begin(SETTINGS.bleKeyMap), std::end(SETTINGS.bleKeyMap), CrossPointSettings::BleKeyMapEntry{});
SETTINGS.saveToFile();
mappedInput.setBleCaptureMode(true);
requestUpdate();
}
void BleButtonMapActivity::onExit() {
mappedInput.setBleCaptureMode(false);
Activity::onExit();
}
bool BleButtonMapActivity::assignCapturedKey(MappedInputManager::Button button) {
const uint8_t btn = static_cast<uint8_t>(button);
// Mutated via std::replace_if below and through `slot`; cppcheck's CI parse
// (no include paths) can't see the writes and suggests const.
// cppcheck-suppress constVariableReference
auto& map = SETTINGS.bleKeyMap;
using Entry = CrossPointSettings::BleKeyMapEntry;
const uint8_t kind = capturedKind;
const uint8_t value = capturedValue;
// 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.
std::replace_if(
std::begin(map), std::end(map),
[&](const Entry& e) { return e.button == btn && !(e.keyKind == kind && e.keyValue == value); }, Entry{});
// Reuse the slot already bound to this key, else the first free slot.
auto* slot = std::find_if(std::begin(map), std::end(map), [&](const Entry& e) {
return e.button != 0xFF && e.keyKind == kind && e.keyValue == value;
});
if (slot == std::end(map)) {
slot = std::find_if(std::begin(map), std::end(map),
[](const Entry& e) { return e.button == 0xFF || e.keyKind == 0xFF; });
}
if (slot == std::end(map)) return false; // table full
slot->keyKind = kind;
slot->keyValue = value;
slot->button = btn;
SETTINGS.saveToFile();
return true;
}
void BleButtonMapActivity::loop() {
// Front Back button exits the mapping screen at any step.
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
finish();
return;
}
if (step == Step::WaitForKey) {
uint8_t kind = 0xFF;
uint8_t value = 0;
if (mappedInput.takeCapturedBleKey(kind, value)) {
capturedKind = kind;
capturedValue = value;
functionIndex = 0;
step = Step::SelectFunction;
requestUpdate();
}
return;
}
// Step::SelectFunction — pick a logical function for the captured key.
buttonNavigator.onNext([this] {
functionIndex = ButtonNavigator::nextIndex(functionIndex, kFunctionCount);
requestUpdate();
});
buttonNavigator.onPrevious([this] {
functionIndex = ButtonNavigator::previousIndex(functionIndex, kFunctionCount);
requestUpdate();
});
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
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();
}
}
void BleButtonMapActivity::render(RenderLock&&) {
renderer.clearScreen();
const auto& metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_BT_MAP_BUTTONS));
const int topOffset = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing;
const int contentHeight = pageHeight - topOffset - metrics.buttonHintsHeight - metrics.verticalSpacing;
if (step == Step::WaitForKey) {
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
tr(STR_BT_PRESS_REMOTE));
// Show the current mappings so the user sees progress.
int row = 0;
for (const auto& e : SETTINGS.bleKeyMap) {
if (e.button == 0xFF) continue;
char keyName[24];
bleinput::describeKey(e.keyKind, e.keyValue, keyName, sizeof(keyName));
const char* fnName = "";
for (uint8_t i = 0; i < kFunctionCount; i++) {
if (static_cast<uint8_t>(kFunctions[i].button) == e.button) {
fnName = I18N.get(kFunctions[i].label);
break;
}
}
char line[64];
snprintf(line, sizeof(line), "%s -> %s", keyName, fnName);
GUI.drawHelpText(renderer, Rect{0, topOffset + row * 22, pageWidth, 20}, line);
row++;
}
} else {
char captured[24];
bleinput::describeKey(capturedKind, capturedValue, captured, sizeof(captured));
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
captured);
GUI.drawList(
renderer, Rect{0, topOffset, pageWidth, contentHeight}, kFunctionCount, functionIndex,
[this](int i) { return std::string(I18N.get(kFunctions[i].label)); }, nullptr, nullptr, nullptr, false);
}
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);
renderer.displayBuffer();
}
@@ -1,47 +0,0 @@
#pragma once
#include <I18n.h>
#include <cstdint>
#include "MappedInputManager.h"
#include "activities/Activity.h"
#include "util/ButtonNavigator.h"
// Capture-then-assign mapping for BLE page-turner buttons. The user presses a
// button on the remote; we capture its decoded key identity (via the
// MappedInputManager BLE capture mode) and let them bind it to a logical button.
// Repeat to map each remote button; Back exits. Mirrors ButtonRemapActivity's
// flow, but the input source is the BLE host instead of the front buttons.
class BleButtonMapActivity final : public Activity {
public:
explicit BleButtonMapActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("BleButtonMap", renderer, mappedInput) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(RenderLock&&) override;
private:
// Logical functions a remote button can be bound to.
struct Fn {
MappedInputManager::Button button;
StrId label;
};
static const Fn kFunctions[];
static const uint8_t kFunctionCount;
enum class Step { WaitForKey, SelectFunction };
Step step = Step::WaitForKey;
uint8_t capturedKind = 0xFF;
uint8_t capturedValue = 0;
int functionIndex = 0;
ButtonNavigator buttonNavigator;
// Bind the captured key to the chosen logical button in SETTINGS.bleKeyMap and
// persist. Returns false when the table is full and the key is new.
bool assignCapturedKey(MappedInputManager::Button button);
};
@@ -1,314 +0,0 @@
#include "BluetoothSettingsActivity.h"
#include <BleKeyboardHost.h>
#include <GfxRenderer.h>
#include <Logging.h>
#include <cstdio>
#include "BleButtonMapActivity.h"
#include "BleInput.h"
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "components/UITheme.h"
#include "fontIds.h"
namespace {
constexpr unsigned long kBannerMs = 2000;
constexpr uint32_t kScanMs = 8000;
constexpr unsigned long kForgetHoldMs = 1200; // hold Confirm this long in the Paired view to forget
} // namespace
void BluetoothSettingsActivity::onEnter() {
Activity::onEnter();
view = View::Menu;
menuIndex = 0;
rebuildMenuRows();
requestUpdate();
}
void BluetoothSettingsActivity::onExit() {
if (BleHid.isScanning()) BleHid.stopScan();
Activity::onExit();
}
void BluetoothSettingsActivity::setBanner(const char* text) {
banner = text ? text : "";
bannerUntil = millis() + kBannerMs;
}
void BluetoothSettingsActivity::rebuildMenuRows() {
menuRows.clear();
menuRows.reserve(8);
menuRows.push_back({Action::ToggleBt, StrId::STR_BLUETOOTH});
if (SETTINGS.bluetoothEnabled) {
menuRows.push_back({Action::Scan, StrId::STR_BT_SCAN_PAIR});
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});
}
if (menuIndex >= static_cast<int>(menuRows.size())) menuIndex = 0;
}
void BluetoothSettingsActivity::startScanView() {
LOG_INF("BLEUI", "scan view: begin running=%d scanning=%d devices=%u paired=%u", BleHid.isRunning(),
BleHid.isScanning(), BleHid.deviceCount(), BleHid.pairedCount());
view = View::Scan;
scanIndex = 0;
awaitingConnect = false;
lastLoggedScanState = false;
lastLoggedDeviceCount = 0xFF;
// The main-loop lifecycle owns steady-state start/stop, but a scan needs the stack
// this instant — entering this screen can precede the lifecycle's next tick, or its
// heap gate may have deferred the start. ensureStarted() is idempotent, and with
// this screen on top the lifecycle keeps the stack up (keepsBluetoothAlive).
if (!BleHid.isRunning() && !bleinput::ensureStarted()) {
LOG_ERR("BLEUI", "scan: BLE start failed (heap=%u)", ESP.getFreeHeap());
}
BleHid.startScan(kScanMs);
LOG_INF("BLEUI", "scan view: startScan requested scanning=%d devices=%u", BleHid.isScanning(), BleHid.deviceCount());
requestUpdate();
}
void BluetoothSettingsActivity::handleMenuConfirm() {
if (menuRows.empty()) return;
const Action action = menuRows[menuIndex].action;
switch (action) {
case Action::ToggleBt:
// Flip the preference only; the main-loop lifecycle check starts/stops the BLE
// stack to match (and shows the "BT Connecting..." popup). Single owner.
SETTINGS.bluetoothEnabled = SETTINGS.bluetoothEnabled ? 0 : 1;
SETTINGS.saveToFile();
rebuildMenuRows();
requestUpdate();
break;
case Action::Scan:
startScanView();
break;
case Action::Disconnect:
BleHid.disconnect();
setBanner(tr(STR_BT_NOT_CONNECTED));
rebuildMenuRows();
requestUpdate();
break;
case Action::PairedDevices:
view = View::Paired;
pairedIndex = 0;
requestUpdate();
break;
case Action::MapButtons:
startActivityForResult(std::make_unique<BleButtonMapActivity>(renderer, mappedInput),
[this](const ActivityResult&) {
rebuildMenuRows();
requestUpdate();
});
break;
}
}
void BluetoothSettingsActivity::loop() {
// Clear an expired status banner.
if (bannerUntil > 0 && millis() > bannerUntil) {
banner.clear();
bannerUntil = 0;
requestUpdate();
}
// Watch for an async connect result (from either the scan list or the paired list).
if (awaitingConnect) {
char reason[48];
if (BleHid.isConnected()) {
awaitingConnect = false;
BleHid.releaseScanResults();
view = View::Menu;
rebuildMenuRows();
char buf[64];
snprintf(buf, sizeof(buf), tr(STR_BT_CONNECTED_TO), BleHid.connectedName());
setBanner(buf);
requestUpdate();
} else if (BleHid.takeConnectFailure(reason, sizeof(reason))) {
awaitingConnect = false;
setBanner(reason);
requestUpdate();
}
}
// Back returns to the menu from a sub-view, or leaves the screen from the menu.
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
if (view == View::Menu) {
finish();
} else {
if (BleHid.isScanning()) BleHid.stopScan();
view = View::Menu;
rebuildMenuRows();
requestUpdate();
}
return;
}
// Navigation within the active list.
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);
requestUpdate();
});
buttonNavigator.onPrevious([this, count, idx] {
if (count > 0) *idx = ButtonNavigator::previousIndex(*idx, count);
requestUpdate();
});
// Paired view: tap Confirm to connect, hold Confirm to forget. Uses release for
// connect so a hold can fire forget without also connecting on the same press.
if (view == View::Paired) {
if (mappedInput.isPressed(MappedInputManager::Button::Confirm)) {
if (!pairedActionTaken && mappedInput.getHeldTime() >= kForgetHoldMs && pairedIndex < BleHid.pairedCount()) {
const auto& p = BleHid.paired(static_cast<uint8_t>(pairedIndex));
BleHid.forget(p.addr);
if (pairedIndex > 0) pairedIndex--;
setBanner(tr(STR_FORGET_BUTTON));
pairedActionTaken = true;
rebuildMenuRows();
requestUpdate();
}
} else if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (!pairedActionTaken && !awaitingConnect && pairedIndex < BleHid.pairedCount()) {
const auto& p = BleHid.paired(static_cast<uint8_t>(pairedIndex));
awaitingConnect = true;
setBanner(tr(STR_CONNECTING));
BleHid.connect(p.addr);
requestUpdate();
}
pairedActionTaken = false;
}
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
if (view == View::Menu) {
handleMenuConfirm();
} else if (view == View::Scan) {
const int scanCount = BleHid.deviceCount();
if (!awaitingConnect && scanCount == 0 && !BleHid.isScanning()) {
LOG_INF("BLEUI", "scan view: restart scan requested");
if (!BleHid.isRunning() && !bleinput::ensureStarted()) {
LOG_ERR("BLEUI", "scan restart: BLE start failed (heap=%u)", ESP.getFreeHeap());
}
BleHid.startScan(kScanMs);
LOG_INF("BLEUI", "scan view: restart scan state scanning=%d devices=%u", BleHid.isScanning(),
BleHid.deviceCount());
requestUpdate();
} else if (!awaitingConnect && scanIndex < scanCount) {
if (BleHid.isScanning()) BleHid.stopScan();
const auto& d = BleHid.device(static_cast<uint8_t>(scanIndex));
LOG_INF("BLEUI", "scan view: connect addr=%s name='%s' rssi=%d type=%u hid=%d conn=%d", d.addr, d.name, d.rssi,
d.addrType, d.hid, d.connectable);
awaitingConnect = true;
setBanner(tr(STR_CONNECTING));
BleHid.connect(d.addr);
requestUpdate();
}
}
return;
}
// The scan list changes as devices are discovered — keep repainting while active.
if (view == View::Scan) {
const bool scanning = BleHid.isScanning();
const uint8_t deviceCount = BleHid.deviceCount();
if (scanning != lastLoggedScanState || deviceCount != lastLoggedDeviceCount) {
LOG_INF("BLEUI", "scan view: state scanning=%d devices=%u", scanning, deviceCount);
lastLoggedScanState = scanning;
lastLoggedDeviceCount = deviceCount;
}
if (scanning) requestUpdate();
}
}
std::string BluetoothSettingsActivity::deviceLabel(int index) const {
if (index >= BleHid.deviceCount()) return "";
const auto& d = BleHid.device(static_cast<uint8_t>(index));
return std::string(d.name);
}
std::string BluetoothSettingsActivity::pairedLabel(int index) const {
if (index >= BleHid.pairedCount()) return "";
const auto& p = BleHid.paired(static_cast<uint8_t>(index));
return std::string(p.name);
}
void BluetoothSettingsActivity::render(RenderLock&&) {
renderer.clearScreen();
const auto& metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
const char* title = tr(STR_BLUETOOTH);
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, title);
// Sub-header: connection status.
const char* status = BleHid.isConnected() ? BleHid.connectedName() : tr(STR_BT_NOT_CONNECTED);
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
status);
const int topOffset = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing;
const int contentHeight = pageHeight - topOffset - metrics.buttonHintsHeight - metrics.verticalSpacing;
const Rect listRect{0, topOffset, pageWidth, contentHeight};
if (view == View::Menu) {
GUI.drawList(
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);
return "";
},
true);
} else if (view == View::Scan) {
// Free2/3 remotes only advertise in the right slider mode — tell the user how.
GUI.drawHelpText(renderer, Rect{0, topOffset, pageWidth, 16}, tr(STR_BT_FREE_HINT1));
GUI.drawHelpText(renderer, Rect{0, topOffset + 16, pageWidth, 16}, tr(STR_BT_FREE_HINT2));
const int scanTop = topOffset + 38;
const int count = BleHid.deviceCount();
if (count == 0) {
GUI.drawHelpText(renderer, Rect{0, scanTop, pageWidth, 24},
BleHid.isScanning() ? tr(STR_SCANNING) : tr(STR_BT_NO_DEVICES));
} else {
GUI.drawList(
renderer, Rect{0, scanTop, pageWidth, contentHeight - 38}, count, scanIndex,
[this](int i) { return deviceLabel(i); }, nullptr, nullptr, nullptr, false);
}
} else { // Paired
const int count = BleHid.pairedCount();
if (count == 0) {
GUI.drawHelpText(renderer, Rect{0, topOffset + metrics.verticalSpacing, pageWidth, 24}, tr(STR_BT_NO_PAIRED));
} else {
GUI.drawList(
renderer, listRect, count, pairedIndex, [this](int i) { return pairedLabel(i); }, nullptr, nullptr, nullptr,
false);
}
}
// Transient banner above the hints.
if (!banner.empty()) {
GUI.drawHelpText(renderer, Rect{0, pageHeight - metrics.buttonHintsHeight - 22, pageWidth, 20}, banner.c_str());
}
// In the paired list, Confirm connects and a hold forgets — surface the hold hint.
if (view == View::Paired && BleHid.pairedCount() > 0 && banner.empty()) {
GUI.drawHelpText(renderer, Rect{0, pageHeight - metrics.buttonHintsHeight - 22, pageWidth, 20},
tr(STR_BT_FORGET_PROMPT));
}
// Button hints differ by view (Menu selects; Scan and Paired both connect).
const bool scanCanRestart = view == View::Scan && BleHid.deviceCount() == 0 && !BleHid.isScanning();
const char* confirm = view == View::Menu ? tr(STR_SELECT) : scanCanRestart ? "Scan" : tr(STR_CONNECT);
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);
renderer.displayBuffer();
}
@@ -1,65 +0,0 @@
#pragma once
#include <I18n.h>
#include <string>
#include <vector>
#include "activities/Activity.h"
#include "util/ButtonNavigator.h"
// Bluetooth page-turner settings. One screen with three views:
// Menu — enable/disable BT, scan & pair, disconnect, map buttons, presets.
// Scan — live list of discovered BLE HID devices; Confirm connects.
// Paired — bonded devices; Confirm forgets the selected one.
// All BLE access goes through the FreeInk BleHid singleton; everything no-ops
// gracefully when BLE is compiled out (BleHid.begin() returns false).
class BluetoothSettingsActivity final : public Activity {
public:
explicit BluetoothSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("BluetoothSettings", renderer, mappedInput) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(RenderLock&&) override;
bool keepsBluetoothAlive() const override { return true; }
private:
enum class View { Menu, Scan, Paired };
// Menu row actions.
enum class Action { ToggleBt, Scan, Disconnect, MapButtons, PairedDevices };
struct MenuRow {
Action action;
StrId label;
};
View view = View::Menu;
std::vector<MenuRow> menuRows;
int menuIndex = 0;
int scanIndex = 0;
int pairedIndex = 0;
ButtonNavigator buttonNavigator;
// Transient status banner (connect result, forget confirmation, etc.).
std::string banner;
unsigned long bannerUntil = 0;
// Set when a connect() has been issued and we're waiting for the async result.
bool awaitingConnect = false;
// Guards the Paired view's hold-to-forget so it fires once per hold and suppresses
// the tap-to-connect on the same press.
bool pairedActionTaken = false;
bool lastLoggedScanState = false;
uint8_t lastLoggedDeviceCount = 0xFF;
void rebuildMenuRows();
void handleMenuConfirm();
void startScanView();
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
};
@@ -17,8 +17,6 @@
#include "fontIds.h" #include "fontIds.h"
#include "network/HttpDownloader.h" #include "network/HttpDownloader.h"
#include "BleInput.h"
FontDownloadActivity::FontDownloadActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) FontDownloadActivity::FontDownloadActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("FontDownload", renderer, mappedInput), fontInstaller_(sdFontSystem.registry()) {} : Activity("FontDownload", renderer, mappedInput), fontInstaller_(sdFontSystem.registry()) {}
@@ -26,12 +24,6 @@ FontDownloadActivity::FontDownloadActivity(GfxRenderer& renderer, MappedInputMan
void FontDownloadActivity::onEnter() { void FontDownloadActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
// Free the BLE stack BEFORE bringing WiFi up (matches WifiSelectionActivity):
// the C3 shares one radio and heap between the stacks, and the WiFi driver
// sizes its RX/TX buffer pools at init — initializing it with NimBLE's ~50 KB
// still resident leaves WiFi permanently starved even after the lifecycle
// stops BLE a loop later. No-op when BLE is already off.
bleinput::stop();
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput), startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput),
[this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); }); [this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); });
@@ -11,8 +11,6 @@
#include "fontIds.h" #include "fontIds.h"
#include "network/OtaUpdater.h" #include "network/OtaUpdater.h"
#include "BleInput.h"
void OtaUpdateActivity::onWifiSelectionComplete(const bool success) { void OtaUpdateActivity::onWifiSelectionComplete(const bool success) {
if (!success) { if (!success) {
LOG_ERR("OTA", "WiFi connection failed, exiting"); LOG_ERR("OTA", "WiFi connection failed, exiting");
@@ -58,12 +56,6 @@ void OtaUpdateActivity::onEnter() {
// Turn on WiFi immediately // Turn on WiFi immediately
LOG_DBG("OTA", "Turning on WiFi..."); LOG_DBG("OTA", "Turning on WiFi...");
// Free the BLE stack BEFORE bringing WiFi up (matches WifiSelectionActivity):
// the C3 shares one radio and heap between the stacks, and the WiFi driver
// sizes its RX/TX buffer pools at init — initializing it with NimBLE's ~50 KB
// still resident leaves WiFi permanently starved even after the lifecycle
// stops BLE a loop later. No-op when BLE is already off.
bleinput::stop();
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
// Launch WiFi selection subactivity // Launch WiFi selection subactivity
@@ -7,7 +7,6 @@
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include "BluetoothSettingsActivity.h"
#include "ButtonRemapActivity.h" #include "ButtonRemapActivity.h"
#include "ClearCacheActivity.h" #include "ClearCacheActivity.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
@@ -60,7 +59,6 @@ void SettingsActivity::rebuildSettingsLists() {
// Append device-only ACTION items // Append device-only ACTION items
controlsSettings.insert(controlsSettings.begin(), controlsSettings.insert(controlsSettings.begin(),
SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons)); SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons));
controlsSettings.push_back(SettingInfo::Action(StrId::STR_BLUETOOTH, SettingAction::Bluetooth));
systemSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network)); systemSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network));
systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync)); systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync));
systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_SERVERS, SettingAction::OPDSBrowser)); systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_SERVERS, SettingAction::OPDSBrowser));
@@ -297,9 +295,6 @@ void SettingsActivity::toggleCurrentSetting() {
case SettingAction::Language: case SettingAction::Language:
startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler); startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);
break; break;
case SettingAction::Bluetooth:
startActivityForResult(std::make_unique<BluetoothSettingsActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::None: case SettingAction::None:
// Do nothing // Do nothing
break; break;
@@ -24,7 +24,6 @@ enum class SettingAction {
SdFirmwareUpdate, SdFirmwareUpdate,
Language, Language,
DownloadFonts, DownloadFonts,
Bluetooth,
}; };
struct SettingInfo { struct SettingInfo {
-7
View File
@@ -1,7 +0,0 @@
#pragma once
#include <cstdint>
// 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};
+4 -27
View File
@@ -13,7 +13,6 @@
#include "I18n.h" #include "I18n.h"
#include "RecentBooksStore.h" #include "RecentBooksStore.h"
#include "components/UITheme.h" #include "components/UITheme.h"
#include "components/icons/bluetooth.h"
#include "components/icons/bookmark.h" #include "components/icons/bookmark.h"
#include "fontIds.h" #include "fontIds.h"
@@ -24,10 +23,8 @@ constexpr int homeMarginTop = 30;
constexpr int subtitleY = 738; constexpr int subtitleY = 738;
constexpr int bookmarkStatusIconWidth = 16; constexpr int bookmarkStatusIconWidth = 16;
constexpr int bookmarkStatusIconHeight = 14; constexpr int bookmarkStatusIconHeight = 14;
constexpr int bookmarkStatusIconGap = 4;
constexpr int bookmarkStatusIconTopCrop = 2; constexpr int bookmarkStatusIconTopCrop = 2;
constexpr int bluetoothStatusIconWidth = 16;
constexpr int bluetoothStatusIconHeight = 16;
constexpr int statusIconGap = 4;
bool statusBarTextLaneVisible() { bool statusBarTextLaneVisible() {
return SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage || return SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage ||
@@ -46,17 +43,6 @@ 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 } // namespace
void BaseTheme::drawBatteryOutline(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight) { void BaseTheme::drawBatteryOutline(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight) {
@@ -763,8 +749,7 @@ void BaseTheme::fillPopupProgress(const GfxRenderer& renderer, const Rect& layou
void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage, void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage,
const int pageCount, std::string title, const int paddingBottom, const int textYOffset, const int pageCount, std::string title, const int paddingBottom, const int textYOffset,
const bool fillMargin, const bool isPageBookmarked, const bool pageCountEstimated, const bool fillMargin, const bool isPageBookmarked, const bool pageCountEstimated) const {
const bool bluetoothConnected) const {
auto metrics = UITheme::getInstance().getMetrics(); auto metrics = UITheme::getInstance().getMetrics();
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft; int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom, renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
@@ -860,17 +845,9 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c
} }
} }
// Draw status icons // Draw Bookmark
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) { if (showStatusBarTextLane && isPageBookmarked) {
const int bookmarkGap = leftClusterWidth > 0 ? statusIconGap : 0; const int bookmarkGap = leftClusterWidth > 0 ? bookmarkStatusIconGap : 0;
const int bookmarkX = leftClusterX + leftClusterWidth + bookmarkGap; const int bookmarkX = leftClusterX + leftClusterWidth + bookmarkGap;
const int bookmarkY = textY + 5; const int bookmarkY = textY + 5;
drawBookmarkStatusIcon(renderer, bookmarkX, bookmarkY); drawBookmarkStatusIcon(renderer, bookmarkX, bookmarkY);
+1 -1
View File
@@ -238,7 +238,7 @@ class BaseTheme {
void drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage, const int pageCount, void drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage, const int pageCount,
std::string title, const int paddingBottom = 0, const int textYOffset = 0, std::string title, const int paddingBottom = 0, const int textYOffset = 0,
const bool fillMargin = true, const bool isPageBookmarked = false, const bool fillMargin = true, const bool isPageBookmarked = false,
const bool pageCountEstimated = false, const bool bluetoothConnected = false) const; const bool pageCountEstimated = false) const;
void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) 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, virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode = false,
int contentStartX = 0, int contentWidth = 0) const; int contentStartX = 0, int contentWidth = 0) const;
+2 -133
View File
@@ -18,7 +18,6 @@
#include <cstring> #include <cstring>
#include "BleInput.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "CrossPointState.h" #include "CrossPointState.h"
#include "KOReaderCredentialStore.h" #include "KOReaderCredentialStore.h"
@@ -34,8 +33,6 @@
#include "images/LoadingIcon.h" #include "images/LoadingIcon.h"
#include "util/ButtonNavigator.h" #include "util/ButtonNavigator.h"
#include "util/ScreenshotUtil.h" #include "util/ScreenshotUtil.h"
#include <Memory.h>
#include "HeapMap.h"
GfxRenderer renderer(display); GfxRenderer renderer(display);
MappedInputManager mappedInputManager(gpio, renderer); MappedInputManager mappedInputManager(gpio, renderer);
@@ -130,12 +127,6 @@ enum class BootResume : uint8_t {
QuickResume, // wake from a quick-resume deep sleep (SD flag; survives power loss) 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 // Latched true once enterDeepSleep() commits to sleeping, before it tears down
// the current activity. WiFi activities call silentRestart() in onExit() to // 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 // clear heap fragmentation on the way out, but deep sleep is a full chip reset
@@ -271,10 +262,6 @@ void enterDeepSleep(bool fromTimeout = false) {
WiFi.mode(WIFI_OFF); WiFi.mode(WIFI_OFF);
} }
// Drop any BLE HID link cleanly so the page-turner sees the disconnect promptly.
// bluetoothEnabled persists, so the setting is restored on the next wake.
bleinput::stop();
halTiltSensor.deepSleep(); halTiltSensor.deepSleep();
display.deepSleep(); display.deepSleep();
LOG_DBG("MAIN", "Entering deep sleep"); LOG_DBG("MAIN", "Entering deep sleep");
@@ -338,7 +325,6 @@ void setup() {
(isSilentReboot && silentRebootTarget <= SILENT_REBOOT_TARGET_READER) ? silentRebootTarget : 0; (isSilentReboot && silentRebootTarget <= SILENT_REBOOT_TARGET_READER) ? silentRebootTarget : 0;
silentRebootMagic = 0; silentRebootMagic = 0;
silentRebootTarget = 0; silentRebootTarget = 0;
bootWasSilentRestartFlag = isSilentReboot;
gpio.begin(); gpio.begin();
powerManager.begin(); powerManager.begin();
@@ -492,78 +478,6 @@ void setup() {
// Ensure we're not still holding the power button before leaving setup // Ensure we're not still holding the power button before leaving setup
waitForPowerRelease(); waitForPowerRelease();
allowSleepAt = millis() + 2000; allowSleepAt = millis() + 2000;
// Bluetooth is started lazily by the lifecycle check in loop() once a reader or the
// Bluetooth settings screen is on the stack — not here at boot — so home/browser and
// WiFi activities keep the ~50 KB the BLE stack would otherwise hold.
}
// Bring the BLE stack up or down to match the current context. BLE is only resident
// while a reader (page-turner input) or the Bluetooth settings screen (pairing) is on
// the stack AND WiFi is off. Heap-heavy reader phases may stop BLE directly; this
// lifecycle then restarts it only after the normal activity/render/heap gates pass.
void updateBluetoothLifecycle() {
const bool wanted =
SETTINGS.bluetoothEnabled && activityManager.bluetoothShouldBeActive() && WiFi.getMode() == WIFI_MODE_NULL;
if (wanted && !BleHid.isRunning() && activityManager.bluetoothStartDeferred()) {
static uint32_t lastActivityDeferLogMs = 0;
if (millis() - lastActivityDeferLogMs > 10000) {
lastActivityDeferLogMs = millis();
LOG_INF("BLELC", "start deferred: activity busy heap=%u maxAlloc=%u", ESP.getFreeHeap(), ESP.getMaxAllocHeap());
}
return;
}
// Heap gate: NimBLE needs ~57 KB. If the reader cannot spare that yet, retry
// on the next loop without entering any separate BLE hold state.
// The BT settings screen is explicit user intent to run BLE right now (scan/pair is
// dead without the stack). Its floor only needs to cover NimBLE itself — the 100 KB
// reader floor reserves build/render headroom that never gets used there.
const bool explicitBtContext = activityManager.currentKeepsBluetoothAlive();
const size_t startFloor = explicitBtContext ? bleinput::kStartMinFreeHeapExplicit : bleinput::kStartMinFreeHeap;
if (wanted && !BleHid.isRunning() && activityManager.isReaderActivity() && !renderer.hasFrameBuffer()) {
static uint32_t lastFramebufferLoanDeferLogMs = 0;
if (millis() - lastFramebufferLoanDeferLogMs > 10000) {
lastFramebufferLoanDeferLogMs = millis();
LOG_INF("BLELC", "start deferred: framebuffer lent heap=%u maxAlloc=%u", ESP.getFreeHeap(),
ESP.getMaxAllocHeap());
}
return;
}
if (wanted && !BleHid.isRunning() && activityManager.isReaderActivity() && RenderLock::peek()) {
static uint32_t lastReaderRenderDeferLogMs = 0;
if (millis() - lastReaderRenderDeferLogMs > 10000) {
lastReaderRenderDeferLogMs = millis();
LOG_INF("BLELC", "start deferred: reader render in progress heap=%u maxAlloc=%u", ESP.getFreeHeap(),
ESP.getMaxAllocHeap());
}
return;
}
if (wanted && !BleHid.isRunning() && ESP.getFreeHeap() < startFloor) {
static uint32_t lastGateLogMs = 0;
if (millis() - lastGateLogMs > 10000) {
lastGateLogMs = millis();
LOG_INF("BLELC", "start deferred: heap %u floor %u", ESP.getFreeHeap(), (unsigned)startFloor);
}
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(),
WiFi.getMode(), BleHid.pairedCount(), ESP.getFreeHeap(), ESP.getMaxAllocHeap());
// Start immediately once the lifecycle gates pass. Do not draw a reconnect
// popup here: that schedules a reader redraw while BLE has just consumed ~53 KB,
// which can immediately trip the render heap shed path and create a start/stop loop.
if (!bleinput::ensureStarted()) {
LOG_ERR("BLELC", "start failed heap=%u maxAlloc=%u", ESP.getFreeHeap(), ESP.getMaxAllocHeap());
return;
}
LOG_INF("BLELC", "started paired=%u heap=%u maxAlloc=%u", BleHid.pairedCount(), ESP.getFreeHeap(),
ESP.getMaxAllocHeap());
} else if (!wanted && BleHid.isRunning()) {
LOG_INF("BLELC", "stop requested enabled=%u active=%d wifi=%d heap=%u maxAlloc=%u", SETTINGS.bluetoothEnabled,
activityManager.bluetoothShouldBeActive(), WiFi.getMode(), ESP.getFreeHeap(), ESP.getMaxAllocHeap());
bleinput::stop();
LOG_INF("BLELC", "stopped heap=%u maxAlloc=%u", ESP.getFreeHeap(), ESP.getMaxAllocHeap());
}
} }
void loop() { void loop() {
@@ -572,24 +486,6 @@ void loop() {
static unsigned long lastMemPrint = 0; static unsigned long lastMemPrint = 0;
gpio.update(); 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()); halTiltSensor.update(SETTINGS.tiltPageTurn, SETTINGS.orientation, activityManager.isReaderActivity());
renderer.setFadingFix(SETTINGS.fadingFix); renderer.setFadingFix(SETTINGS.fadingFix);
@@ -598,23 +494,6 @@ void loop() {
LOG_INF("MEM", "Free: %d bytes, Total: %d bytes, Min Free: %d bytes, MaxAlloc: %d bytes", ESP.getFreeHeap(), LOG_INF("MEM", "Free: %d bytes, Total: %d bytes, Min Free: %d bytes, MaxAlloc: %d bytes", ESP.getFreeHeap(),
ESP.getHeapSize(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap()); ESP.getHeapSize(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap());
lastMemPrint = millis(); lastMemPrint = millis();
// MEMFIX-PORT: per-task stack high-water audit; portable
// Task stack audit (~once/min): name, stack base (matches blocks in the
// heap map), and high-water free bytes — the margin available for
// right-sizing each stack. TRACE_FACILITY is on in this sdkconfig.
static uint8_t memPrintCount = 0;
if (++memPrintCount >= 6) {
memPrintCount = 0;
const UBaseType_t taskCount = uxTaskGetNumberOfTasks();
auto taskStatus = makeUniqueNoThrow<TaskStatus_t[]>(taskCount + 2);
if (taskStatus) {
const UBaseType_t got = uxTaskGetSystemState(taskStatus.get(), taskCount + 2, nullptr);
for (UBaseType_t i = 0; i < got; ++i) {
LOG_DBG("MEM", "task %-20s stackBase=%p highWaterFree=%u", taskStatus[i].pcTaskName,
taskStatus[i].pxStackBase, (unsigned)taskStatus[i].usStackHighWaterMark);
}
}
}
} }
// Handle incoming serial commands, // Handle incoming serial commands,
@@ -630,11 +509,6 @@ void loop() {
uint8_t* buf = display.getFrameBuffer(); uint8_t* buf = display.getFrameBuffer();
logSerial.write(buf, bufferSize); logSerial.write(buf, bufferSize);
logSerial.printf("SCREENSHOT_END\n"); logSerial.printf("SCREENSHOT_END\n");
// MEMFIX-PORT: on-demand heap map serial command; portable
} else if (cmd == "MEMMAP") {
// On-demand heap block map: capture the heap exactly when it looks
// interesting (e.g. maxAlloc degraded mid-session) without a reboot.
heapmap::dump();
} }
} }
} }
@@ -642,7 +516,7 @@ void loop() {
// Check for any user activity (button press or release) or active background work // Check for any user activity (button press or release) or active background work
static unsigned long lastActivityTime = millis(); static unsigned long lastActivityTime = millis();
if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || halTiltSensor.hadActivity() || if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || halTiltSensor.hadActivity() ||
mappedInputManager.bleHadActivityThisFrame() || activityManager.preventAutoSleep()) { activityManager.preventAutoSleep()) {
lastActivityTime = millis(); // Reset inactivity timer lastActivityTime = millis(); // Reset inactivity timer
powerManager.setPowerSaving(false); // Restore normal CPU frequency on user activity powerManager.setPowerSaving(false); // Restore normal CPU frequency on user activity
} }
@@ -723,16 +597,11 @@ void loop() {
powerManager.setPowerSaving(false); // Make sure we're at full performance when skipLoopDelay is requested powerManager.setPowerSaving(false); // Make sure we're at full performance when skipLoopDelay is requested
yield(); // Give FreeRTOS a chance to run tasks, but return immediately yield(); // Give FreeRTOS a chance to run tasks, but return immediately
} else { } else {
// The BLE controller cannot run at the 10 MHz low-power frequency — NimBLE's if (millis() - lastActivityTime >= HalPowerManager::IDLE_POWER_SAVING_MS) {
// controller reset/maintenance hangs the radio and trips the interrupt WDT (the
// same reason WiFi force-disables power saving in HalPowerManager). Keep full CPU
// speed whenever the BLE stack is actually resident, regardless of input idleness.
if (!BleHid.isRunning() && millis() - lastActivityTime >= HalPowerManager::IDLE_POWER_SAVING_MS) {
// If we've been inactive for a while, increase the delay to save power // If we've been inactive for a while, increase the delay to save power
powerManager.setPowerSaving(true); // Lower CPU frequency after extended inactivity powerManager.setPowerSaving(true); // Lower CPU frequency after extended inactivity
delay(50); delay(50);
} else { } else {
if (BleHid.isRunning()) powerManager.setPowerSaving(false); // keep the BLE radio stable
// Short delay to prevent tight loop while still being responsive // Short delay to prevent tight loop while still being responsive
delay(10); delay(10);
} }
+7 -40
View File
@@ -26,7 +26,6 @@
#include "html/SettingsPageHtml.generated.h" #include "html/SettingsPageHtml.generated.h"
#include "html/js/jszip_minJs.generated.h" #include "html/js/jszip_minJs.generated.h"
#include "util/BookCacheUtils.h" #include "util/BookCacheUtils.h"
#include <Memory.h>
namespace { namespace {
// Folders/files to hide from the web interface file browser // Folders/files to hide from the web interface file browser
@@ -502,33 +501,13 @@ void CrossPointWebServer::handleFileListData() const {
server->setContentLength(CONTENT_LENGTH_UNKNOWN); server->setContentLength(CONTENT_LENGTH_UNKNOWN);
server->send(200, "application/json", ""); server->send(200, "application/json", "");
server->sendContent("[");
// Batch entries into ~one-TCP-segment flushes. One sendContent per entry
// means one chunked frame in its own TCP segment per file; the client's
// delayed ACK (~40-200 ms each) turns a large directory into tens of
// seconds. ~1.4 KB batches collapse that to a handful of segments.
constexpr size_t BATCH_CAPACITY = 1400;
auto batch = makeUniqueNoThrow<char[]>(BATCH_CAPACITY);
size_t batchLen = 0;
const auto flushBatch = [&]() {
if (batchLen == 0) return;
server->sendContent(batch.get(), batchLen);
batchLen = 0;
};
char output[512]; char output[512];
constexpr size_t outputSize = sizeof(output); constexpr size_t outputSize = sizeof(output);
bool seenFirst = false; bool seenFirst = false;
JsonDocument doc; JsonDocument doc;
if (batch) { scanFiles(currentPath.c_str(), [this, &output, &doc, seenFirst](const FileInfo& info) mutable {
batch[batchLen++] = '[';
} else {
LOG_ERR("WEB", "OOM: file list batch buffer; falling back to per-entry sends");
server->sendContent("[");
}
scanFiles(currentPath.c_str(), [&](const FileInfo& info) {
doc.clear(); doc.clear();
doc["name"] = info.name; doc["name"] = info.name;
doc["size"] = info.size; doc["size"] = info.size;
@@ -542,26 +521,14 @@ void CrossPointWebServer::handleFileListData() const {
return; return;
} }
const size_t need = written + (seenFirst ? 1 : 0); if (seenFirst) {
if (batch) { server->sendContent(",");
if (batchLen + need > BATCH_CAPACITY) flushBatch();
if (seenFirst) batch[batchLen++] = ',';
memcpy(batch.get() + batchLen, output, written);
batchLen += written;
} else { } else {
if (seenFirst) server->sendContent(",");
server->sendContent(output);
}
seenFirst = true; seenFirst = true;
});
if (batch) {
if (batchLen + 1 > BATCH_CAPACITY) flushBatch();
batch[batchLen++] = ']';
flushBatch();
} else {
server->sendContent("]");
} }
server->sendContent(output);
});
server->sendContent("]");
// End of streamed response, empty chunk to signal client // End of streamed response, empty chunk to signal client
server->sendContent(""); server->sendContent("");
LOG_DBG("WEB", "Served file listing page for path: %s", currentPath.c_str()); LOG_DBG("WEB", "Served file listing page for path: %s", currentPath.c_str());