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
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
58 changed files with 532 additions and 877 deletions
+6
View File
@@ -25,3 +25,9 @@ lib/EpdFont/scripts/output/
# (worktrees, scheduled-task locks, settings.local, scout CLEANUP.md) out.
.claude/*
!.claude/skills/
/managed_components
/.dummy
/CMakeLists.txt
/dependencies.lock
/sdkconfig.default
/sdkconfig.defaults
+44 -17
View File
@@ -68,6 +68,22 @@ bool collectUniqueCodepoints(const char* text, uint32_t* codepoints, uint32_t& c
const char* asCStr(const std::string& s) { return s.c_str(); }
const char* asCStr(const char* s) { return s; }
// Keep-if-fits buffer reuse: only reallocate when the needed size exceeds the
// current capacity. Freeing + reallocating slightly different sizes every page
// turn punches non-coalescing holes in the heap (the freed block rarely fits the
// next page's need), eroding the largest contiguous block all session. With
// reuse, capacities converge on the book's max page after a few turns and page
// turns stop touching the allocator. Only three small instantiations exist
// (interval/glyph/byte arrays), so template bloat is negligible.
template <typename T, typename CapT>
bool ensureArrayCapacity(T*& buf, CapT& capacity, const uint32_t needed) {
if (buf && capacity >= needed) return true;
delete[] buf;
buf = new (std::nothrow) T[needed > 0 ? needed : 1];
capacity = buf ? static_cast<CapT>(needed) : 0;
return buf != nullptr;
}
} // namespace
SdCardFont::~SdCardFont() { freeAll(); }
@@ -83,6 +99,9 @@ void SdCardFont::freeStyleMiniData(PerStyle& s) {
s.miniBitmap = nullptr;
s.miniIntervalCount = 0;
s.miniGlyphCount = 0;
s.miniIntervalCapacity = 0;
s.miniGlyphCapacity = 0;
s.miniBitmapCapacity = 0;
freeStyleMiniKern(s);
memset(&s.miniData, 0, sizeof(s.miniData));
s.epdFont.data = &s.stubData;
@@ -109,6 +128,9 @@ void SdCardFont::freeStyleMiniKern(PerStyle& s) {
s.miniKernRightEntryCount = 0;
s.miniKernLeftClassCount = 0;
s.miniKernRightClassCount = 0;
s.miniKernLeftCapacity = 0;
s.miniKernRightCapacity = 0;
s.miniKernMatrixCapacity = 0;
}
void SdCardFont::freeStyleAll(PerStyle& s) {
@@ -311,13 +333,13 @@ bool SdCardFont::buildMiniKernMatrix(PerStyle& s, const uint32_t* codepoints, ui
if (miniLookupKernClass(s.kernRightClasses, s.header.kernRightEntryCount, codepoints[i]) != 0) miniRightCount++;
}
// Step 4: allocate the three mini buffers. The matrix is <1KB in practice
// (<30 × <30 × 1 byte) so fragmentation is a non-issue.
// Step 4: size the three mini buffers (reused across pages when they fit; the
// per-page sizes vary by a few entries, which as free+realloc churn was punching
// non-coalescing holes in the heap every page turn).
const uint32_t matrixBytes = static_cast<uint32_t>(numLeft) * numRight;
s.miniKernLeftClasses = new (std::nothrow) EpdKernClassEntry[miniLeftCount];
s.miniKernRightClasses = new (std::nothrow) EpdKernClassEntry[miniRightCount];
s.miniKernMatrix = new (std::nothrow) int8_t[matrixBytes];
if (!s.miniKernLeftClasses || !s.miniKernRightClasses || !s.miniKernMatrix) {
if (!ensureArrayCapacity(s.miniKernLeftClasses, s.miniKernLeftCapacity, miniLeftCount) ||
!ensureArrayCapacity(s.miniKernRightClasses, s.miniKernRightCapacity, miniRightCount) ||
!ensureArrayCapacity(s.miniKernMatrix, s.miniKernMatrixCapacity, matrixBytes)) {
LOG_ERR("SDCF", "Failed to allocate mini kern (%u+%u+%u bytes)", miniLeftCount * 3u, miniRightCount * 3u,
matrixBytes);
freeStyleMiniKern(s);
@@ -793,12 +815,19 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
return missed;
}
// Build mini intervals from sorted codepoints
freeStyleMiniData(s);
// Build mini intervals from sorted codepoints. Reset counts and fall back to the
// stub until the rebuild completes, but KEEP the existing buffers (keep-if-fits
// reuse) — the free-and-realloc-per-page pattern here was a primary fragmenter.
s.miniIntervalCount = 0;
s.miniGlyphCount = 0;
s.miniKernLeftEntryCount = 0;
s.miniKernRightEntryCount = 0;
s.miniKernLeftClassCount = 0;
s.miniKernRightClassCount = 0;
memset(&s.miniData, 0, sizeof(s.miniData));
s.epdFont.data = &s.stubData;
uint32_t intervalCapacity = validCount;
s.miniIntervals = new (std::nothrow) EpdUnicodeInterval[intervalCapacity];
if (!s.miniIntervals) {
if (!ensureArrayCapacity(s.miniIntervals, s.miniIntervalCapacity, validCount)) {
LOG_ERR("SDCF", "Failed to allocate mini intervals for style %u", styleIdx);
delete[] mappings;
return static_cast<int>(cpCount);
@@ -816,15 +845,14 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
}
}
// Allocate mini glyph array
s.miniGlyphCount = validCount;
s.miniGlyphs = new (std::nothrow) EpdGlyph[s.miniGlyphCount];
if (!s.miniGlyphs) {
// Mini glyph array (reused across pages when it fits)
if (!ensureArrayCapacity(s.miniGlyphs, s.miniGlyphCapacity, validCount)) {
LOG_ERR("SDCF", "Failed to allocate mini glyphs for style %u", styleIdx);
delete[] mappings;
freeStyleMiniData(s);
return static_cast<int>(cpCount);
}
s.miniGlyphCount = validCount;
// Build sorted read order for sequential I/O
uint32_t* readOrder = new (std::nothrow) uint32_t[validCount];
@@ -891,8 +919,7 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
totalBitmapSize += s.miniGlyphs[i].dataLength;
}
s.miniBitmap = new (std::nothrow) uint8_t[totalBitmapSize > 0 ? totalBitmapSize : 1];
if (!s.miniBitmap) {
if (!ensureArrayCapacity(s.miniBitmap, s.miniBitmapCapacity, totalBitmapSize)) {
LOG_ERR("SDCF", "Failed to allocate mini bitmap (%u bytes) for style %u", totalBitmapSize, styleIdx);
delete[] readOrder;
delete[] mappings;
+14 -1
View File
@@ -168,13 +168,22 @@ class SdCardFont {
// Stub EpdFontData returned when not prewarmed
EpdFontData stubData{};
// Mini EpdFontData built during prewarm
// Mini EpdFontData built during prewarm. Buffers are kept-if-fits across pages
// (capacities below track allocated sizes): freeing and reallocating slightly
// different sizes on every page turn was a primary heap fragmenter — each page's
// freed hole rarely fit the next page's need, so maxAlloc eroded all session.
// After a few pages the capacities converge on the book's max and page turns
// stop allocating entirely. freeStyleMiniData() still releases everything (and
// zeroes capacities) for style eviction / font unload.
EpdFontData miniData{};
EpdUnicodeInterval* miniIntervals = nullptr;
EpdGlyph* miniGlyphs = nullptr;
uint8_t* miniBitmap = nullptr;
uint32_t miniIntervalCount = 0;
uint32_t miniGlyphCount = 0;
uint32_t miniIntervalCapacity = 0;
uint32_t miniGlyphCapacity = 0;
uint32_t miniBitmapCapacity = 0;
// Per-page mini kern matrix (built by buildMiniKernMatrix on each full
// prewarm). miniKernLeftClasses/miniKernRightClasses map ONLY the codepoints
@@ -189,6 +198,10 @@ class SdCardFont {
uint8_t miniKernLeftClassCount = 0;
uint8_t miniKernRightClassCount = 0;
int8_t* miniKernMatrix = nullptr;
// Kept-if-fits capacities, same rationale as the mini glyph buffers above.
uint16_t miniKernLeftCapacity = 0;
uint16_t miniKernRightCapacity = 0;
uint32_t miniKernMatrixCapacity = 0;
// The EpdFont whose data pointer we manage
EpdFont epdFont{&stubData};
+13 -19
View File
@@ -21,7 +21,6 @@
// Minimum file size (in bytes) to show indexing popup - smaller chapters don't benefit from it
constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB
constexpr size_t PARSE_BUFFER_SIZE = 1024;
constexpr size_t MAX_BUFFERED_TEXT_WORDS = 300;
// Hard cap on the number of anchor IDs recorded per chapter. Legitimate navigation
// anchors (TOC entries, footnotes, cross-references) rarely exceed a few hundred per
@@ -202,8 +201,6 @@ void ChapterHtmlSlimParser::flushPendingAnchor() {
// flush the contents of partWordBuffer to currentTextBlock
void ChapterHtmlSlimParser::flushPartWordBuffer() {
flushLongTextBlockIfNeeded();
// Determine font style from depth-based tracking and CSS effective style
const bool isBold = boldUntilDepth < depth || effectiveBold;
const bool isItalic = italicUntilDepth < depth || effectiveItalic;
@@ -231,20 +228,6 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
listItemBulletOnly = false;
}
void ChapterHtmlSlimParser::flushLongTextBlockIfNeeded() {
if (!currentTextBlock || currentTextBlock->size() <= MAX_BUFFERED_TEXT_WORDS) {
return;
}
LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
const int horizontalInset = currentTextBlock->getBlockStyle().totalHorizontalInset();
const uint16_t effectiveWidth =
(horizontalInset < viewportWidth) ? static_cast<uint16_t>(viewportWidth - horizontalInset) : viewportWidth;
currentTextBlock->layoutAndExtractLines(
renderer, fontId, effectiveWidth,
[this](const std::shared_ptr<TextBlock>& textBlock) { this->addLineToPage(textBlock); }, false);
}
// start a new text block if needed
void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
nextWordContinues = false; // New block = new paragraph, no continuation
@@ -1172,9 +1155,20 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char
self->partWordBuffer[self->partWordBufferIndex++] = s[i];
}
// If we have a large number of words buffered up, perform the layout and consume out all but the last line.
// If we have > 750 words buffered up, perform the layout and consume out all but the last line
// There should be enough here to build out 1-2 full pages and doing this will free up a lot of
// memory.
// Spotted when reading Intermezzo, there are some really long text blocks in there.
self->flushLongTextBlockIfNeeded();
if (self->currentTextBlock->size() > 750) {
LOG_DBG("EHP", "Text block too long, splitting into multiple pages");
const int horizontalInset = self->currentTextBlock->getBlockStyle().totalHorizontalInset();
const uint16_t effectiveWidth = (horizontalInset < self->viewportWidth)
? static_cast<uint16_t>(self->viewportWidth - horizontalInset)
: self->viewportWidth;
self->currentTextBlock->layoutAndExtractLines(
self->renderer, self->fontId, effectiveWidth,
[self](const std::shared_ptr<TextBlock>& textBlock) { self->addLineToPage(textBlock); }, false);
}
}
void XMLCALL ChapterHtmlSlimParser::defaultHandlerExpand(void* userData, const XML_Char* s, const int len) {
@@ -4,7 +4,6 @@
#include <expat.h>
#include <climits>
#include <deque>
#include <functional>
#include <memory>
#include <string>
@@ -85,7 +84,7 @@ class ChapterHtmlSlimParser {
// Anchor-to-page mapping: tracks which page each HTML id attribute lands on
int completedPageCount = 0;
std::deque<std::pair<std::string, uint16_t>> anchorData;
std::vector<std::pair<std::string, uint16_t>> anchorData;
std::string pendingAnchorId; // deferred until after previous text block is flushed
std::vector<std::string> tocAnchors; // the list of anchors that are TOC chapter boundaries
uint16_t xpathParagraphIndex = 0;
@@ -113,7 +112,6 @@ class ChapterHtmlSlimParser {
void startNewTextBlock(const BlockStyle& blockStyle);
void flushPendingAnchor();
void flushPartWordBuffer();
void flushLongTextBlockIfNeeded();
void makePages();
static EpdFontFamily::Style fontStyleForTextDecoration(CssTextDecoration decoration);
static void applyDirectionToEntry(StyleStackEntry& entry, const CssStyle& css);
@@ -175,7 +173,7 @@ class ChapterHtmlSlimParser {
void abortParse(); // tear down without flushing (error / abandon)
void addLineToPage(std::shared_ptr<TextBlock> line);
const std::deque<std::pair<std::string, uint16_t>>& getAnchors() const { return anchorData; }
const std::vector<std::pair<std::string, uint16_t>>& getAnchors() const { return anchorData; }
// Byte progress of the in-flight parse, used to estimate a still-building section's total page
// count (a giant single-spine book never fully lays out, so its real count is unknown). Valid
+14
View File
@@ -1451,6 +1451,20 @@ void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const
display.displayBuffer(refreshMode, fadingFix);
}
void GfxRenderer::displayBufferAsync(const HalDisplay::RefreshMode refreshMode) const {
// The async path has no turn-off-screen hook, which the sunlight fading fix
// relies on; keep those users on the blocking path.
if (fadingFix) {
display.displayBuffer(refreshMode, fadingFix);
return;
}
display.displayBufferAsync(refreshMode);
}
void GfxRenderer::waitRefreshComplete() const { display.waitRefreshComplete(); }
bool GfxRenderer::supportsAsyncRefresh() const { return !fadingFix && display.supportsAsyncRefresh(); }
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
const EpdFontFamily::Style style) const {
if (!text || maxWidth <= 0) return "";
+11
View File
@@ -135,6 +135,17 @@ class GfxRenderer {
int getScreenWidth() const;
int getScreenHeight() const;
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
// Non-blocking refresh: starts the waveform and returns so CPU work (e.g.
// grayscale strip rendering) can overlap the panel's refresh time. The
// framebuffer must stay untouched until waitRefreshComplete(). Falls back to
// a blocking refresh when fadingFix is enabled or the panel lacks deferral
// support. See HalDisplay::displayBufferAsync for the baseline contract.
void displayBufferAsync(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
void waitRefreshComplete() const;
// True when displayBufferAsync() genuinely overlaps: panel defers and
// fadingFix isn't forcing the blocking path. Callers can skip overlap
// scaffolding (e.g. whole-plane grayscale buffers) when false.
bool supportsAsyncRefresh() const;
// EXPERIMENTAL: Windowed update - display only a rectangular region
// void displayWindow(int x, int y, int width, int height) const;
void invertScreen() const;
-5
View File
@@ -298,11 +298,6 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u хв"
STR_SLEEP_NEVER: "Ніколі"
STR_STEP_HINT_FRONT: "Пярэднія кнопкі:"
STR_STEP_HINT_SIDE: "Бакавыя кнопкі:"
STR_OPDS_DOWNLOAD_FOLDER: "Папка для спампоўкі"
STR_OPDS_FILENAME_FORMAT: "Фармат назвы файла"
STR_FMT_AUTHOR_TITLE: "Аўтар - Назва"
STR_FMT_TITLE_AUTHOR: "Назва - Аўтар"
STR_FMT_TITLE: "Назва"
STR_AUTO_TURN_ENABLED: "Аўтаперагортванне: "
STR_AUTO_TURN_PAGES_PER_MIN: "Аўтаперагортванне (старонак за хвіліну)"
STR_TILT_PAGE_TURN: "Перагортванне нахілам"
-393
View File
@@ -1,393 +0,0 @@
_language_name: "Bosanski"
_language_code: "BS"
_order: "29"
STR_CROSSPOINT: "CrossPoint"
STR_BOOTING: "POKRETANJE"
STR_SLEEPING: "REŽIM MIROVANJA"
STR_ENTERING_SLEEP: "Prelazak u režim mirovanja"
STR_BROWSE_FILES: "Pregled datoteka"
STR_FILE_TRANSFER: "Prijenos datoteka"
STR_SETTINGS_TITLE: "Postavke"
STR_CONTINUE_READING: "Nastavi čitanje"
STR_NO_OPEN_BOOK: "Nema otvorene knjige"
STR_START_READING: "Počni čitati ispod"
STR_NO_FILES_FOUND: "Nema pronađenih datoteka"
STR_SELECT_CHAPTER: "Odaberi poglavlje"
STR_NO_CHAPTERS: "Nema poglavlja"
STR_END_OF_BOOK: "Kraj knjige"
STR_EMPTY_CHAPTER: "Prazno poglavlje"
STR_INDEXING: "Indeksiranje"
STR_INDEX_FAILED: "Indeksiranje nije uspjelo - nevažeća knjiga"
STR_MEMORY_ERROR: "Greška memorije"
STR_PAGE_LOAD_ERROR: "Greška pri učitavanju stranice"
STR_EMPTY_FILE: "Prazna datoteka"
STR_OUT_OF_BOUNDS: "Izvan granica"
STR_LOADING: "Učitavanje..."
STR_LOADING_POPUP: "Učitavanje"
STR_WIFI_NETWORKS: "Wi-Fi mreže"
STR_NO_NETWORKS: "Nema pronađenih mreža"
STR_NETWORKS_FOUND: "%zu pronađenih mreža"
STR_SCANNING: "Skeniranje..."
STR_FINDING_SAVED_WIFI: "Traženje sačuvanog Wi-Fi-a..."
STR_CONNECTING: "Povezivanje..."
STR_CONNECTING_SAVED_WIFI: "Povezivanje na sačuvani Wi-Fi..."
STR_SHOW_NETWORKS: "Prikaži"
STR_CONNECTED: "Povezano!"
STR_CONNECTION_FAILED: "Povezivanje nije uspjelo"
STR_FORGET_NETWORK: "Zaboraviti mrežu?"
STR_SAVE_PASSWORD: "Sačuvati lozinku za sljedeći put?"
STR_PRESS_OK_SCAN: "Pritisni OK za ponovno skeniranje"
STR_JOIN_NETWORK: "Pridruži se mreži"
STR_CREATE_HOTSPOT: "Kreiraj hotspot"
STR_JOIN_DESC: "Poveži se na postojeću Wi-Fi mrežu"
STR_HOTSPOT_DESC: "Kreiraj Wi-Fi mrežu kojoj se drugi mogu pridružiti"
STR_STARTING_HOTSPOT: "Pokretanje hotspota..."
STR_HOTSPOT_MODE: "Hotspot način rada"
STR_CONNECT_WIFI_HINT: "Poveži svoj uređaj na ovu Wi-Fi mrežu"
STR_OPEN_URL_HINT: "Otvori ovaj URL u svom pregledniku"
STR_OR_HTTP_PREFIX: "ili http://"
STR_SCAN_QR_HINT: "ili skeniraj QR kod svojim telefonom:"
STR_CALIBRE_WIRELESS: "Calibre bežično"
STR_NETWORK_LEGEND: "* = Šifrovano | + = Sačuvano"
STR_MAC_ADDRESS: "MAC adresa:"
STR_CHECKING_WIFI: "Provjera Wi-Fi-a..."
STR_ENTER_WIFI_PASSWORD: "Unesi Wi-Fi lozinku"
STR_ADD_HIDDEN_NETWORK: "Dodaj skrivenu mrežu..."
STR_ENTER_WIFI_SSID: "Unesi naziv mreže (SSID)"
STR_TO_PREFIX: "na "
STR_CALIBRE_RECEIVING: "Primanje: "
STR_CALIBRE_RECEIVED: "Primljeno: "
STR_CALIBRE_INSTRUCTION_1: "1) Instaliraj CrossPoint Reader dodatak"
STR_CALIBRE_INSTRUCTION_2: "2) Budi na istoj Wi-Fi mreži"
STR_CALIBRE_INSTRUCTION_3: "3) U Calibreu: \"Pošalji na uređaj\""
STR_CALIBRE_INSTRUCTION_4: "\"Drži ovaj ekran otvorenim tokom slanja\""
STR_CAT_DISPLAY: "Ekran"
STR_CAT_READER: "Čitač"
STR_CAT_CONTROLS: "Kontrole"
STR_CAT_SYSTEM: "Sistem"
STR_SLEEP_SCREEN: "Ekran mirovanja"
STR_QUICK_RESUME_TIMEOUT: "Brzi nastavak nakon isteka vremena"
STR_SLEEP_COVER_MODE: "Način prikaza korica na ekranu mirovanja"
STR_HIDE_BATTERY: "Sakrij % baterije"
STR_EXTRA_SPACING: "Dodatni razmak između paragrafa"
STR_TEXT_AA: "Zaglađivanje teksta"
STR_IMAGES: "Slike"
STR_IMAGES_DISPLAY: "Prikaz"
STR_IMAGES_PLACEHOLDER: "Rezervisano mjesto"
STR_IMAGES_SUPPRESS: "Sakrij"
STR_EOB_HOME: "Početna"
STR_EOB_CONTINUE_WITH: "Nastavi sa"
STR_SHORT_PWR_BTN: "Kratak klik dugmeta napajanja"
STR_ORIENTATION: "Orijentacija čitanja"
STR_SIDE_BTN_LAYOUT: "Raspored bočnih dugmadi (čitač)"
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orijentiši prednja dugmad"
STR_LONG_PRESS_BEHAVIOR: "Ponašanje dugog pritiska dugmeta"
STR_LONG_PRESS_BEHAVIOR_OFF: "ISKLJUČENO"
STR_LONG_PRESS_BEHAVIOR_SKIP: "Preskakanje poglavlja"
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Promjena orijentacije"
STR_LONG_PRESS_MENU: "Meni dugog pritiska"
STR_FONT_PREVIEW_TEXT: "Brza smeđa lisica preskače lijenog psa"
STR_FONT_FAMILY: "Porodica fonta čitača"
STR_FONT_SIZE: "Veličina fonta čitača"
STR_LINE_SPACING: "Razmak između redova čitača"
STR_SCREEN_MARGIN: "Margina ekrana čitača"
STR_PARA_ALIGNMENT: "Poravnanje paragrafa čitača"
STR_HYPHENATION: "Rastavljanje riječi"
STR_TIME_TO_SLEEP: "Vrijeme do mirovanja"
STR_SHOW_HIDDEN_FILES: "Prikaži skrivene datoteke"
STR_REMOVE_READ_FROM_RECENTS: "Ukloni pročitane knjige sa liste nedavnih"
STR_MOVE_FINISHED_TO_READ: "Premjesti završene knjige u fasciklu Read"
STR_REFRESH_FREQ: "Učestalost osvježavanja"
STR_KOREADER_SYNC: "KOReader sinhronizacija"
STR_CHECK_UPDATES: "Provjeri ažuriranja"
STR_LANGUAGE: "Jezik"
STR_CLEAR_READING_CACHE: "Očisti keš čitanja"
STR_USERNAME: "Korisničko ime"
STR_PASSWORD: "Lozinka"
STR_SYNC_SERVER_URL: "URL servera za sinhronizaciju"
STR_DOCUMENT_MATCHING: "Podudaranje dokumenata"
STR_SEND_METADATA: "Pošalji metapodatke dokumenta"
STR_AUTHENTICATE: "Autentifikuj"
STR_KOREADER_USERNAME: "KOReader korisničko ime"
STR_KOREADER_PASSWORD: "KOReader lozinka"
STR_FILENAME: "Naziv datoteke"
STR_BINARY: "Binarno"
STR_SET_CREDENTIALS_FIRST: "Prvo postavi vjerodajnice"
STR_WIFI_CONN_FAILED: "Wi-Fi povezivanje nije uspjelo"
STR_AUTHENTICATING: "Autentifikacija..."
STR_AUTH_SUCCESS: "Uspješno autentifikovano!"
STR_KOREADER_AUTH: "KOReader autentifikacija"
STR_SYNC_READY: "KOReader sinhronizacija je spremna za upotrebu"
STR_AUTH_FAILED: "Autentifikacija nije uspjela"
STR_DONE: "Gotovo"
STR_CLEAR_CACHE_WARNING_1: "Ovo će očistiti sve keširane podatke knjiga."
STR_CLEAR_CACHE_WARNING_2: "Sav napredak čitanja će biti izgubljen!"
STR_CLEAR_CACHE_WARNING_3: "Knjige će morati biti ponovo indeksirane"
STR_CLEAR_CACHE_WARNING_4: "kada se ponovo otvore."
STR_CLEARING_CACHE: "Čišćenje keša..."
STR_CACHE_CLEARED: "Keš očišćen"
STR_ITEMS_REMOVED: "stavki uklonjeno"
STR_FAILED_LOWER: "nije uspjelo"
STR_CLEAR_CACHE_FAILED: "Čišćenje keša nije uspjelo"
STR_CHECK_SERIAL_OUTPUT: "Provjeri serijski izlaz za detalje"
STR_DARK: "Tamno"
STR_LIGHT: "Svijetlo"
STR_CUSTOM: "Prilagođeno"
STR_COVER: "Korice"
STR_NONE_OPT: "Ništa"
STR_FIT: "Prilagodi"
STR_CROP: "Isjeci"
STR_NEVER: "Nikad"
STR_IN_READER: "U čitaču"
STR_ALWAYS: "Uvijek"
STR_IGNORE: "Ignoriši"
STR_SLEEP: "Mirovanje"
STR_PAGE_TURN: "Okretanje stranice"
STR_FORCE_REFRESH: "Osvježi ekran"
STR_PORTRAIT: "Uspravno"
STR_LANDSCAPE_CW: "Položeno (u smjeru kazaljke na satu)"
STR_INVERTED: "Obrnuto"
STR_ORIENTATION_INVERTED: "Uspravno 180°"
STR_LANDSCAPE_CCW: "Položeno (suprotno smjeru kazaljke na satu)"
STR_PREV_NEXT: "Prethodno/Sljedeće"
STR_NEXT_PREV: "Sljedeće/Prethodno"
STR_KOSYNC: "KOSync"
STR_BOOKMARK_OPTION: "Obilježivač"
STR_DISABLED: "Onemogućeno"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
STR_SMALL: "Malo"
STR_MEDIUM: "Srednje"
STR_LARGE: "Veliko"
STR_X_LARGE: "Vrlo veliko"
STR_TIGHT: "Usko"
STR_NORMAL: "Normalno"
STR_WIDE: "Široko"
STR_JUSTIFY: "Poravnaj obostrano"
STR_ALIGN_LEFT: "Lijevo"
STR_CENTER: "Centrirano"
STR_ALIGN_RIGHT: "Desno"
STR_PAGES_1: "1 stranica"
STR_PAGES_5: "5 stranica"
STR_PAGES_10: "10 stranica"
STR_PAGES_15: "15 stranica"
STR_PAGES_30: "30 stranica"
STR_UPDATE: "Ažuriraj"
STR_CHECKING_UPDATE: "Provjera ažuriranja..."
STR_NEW_UPDATE: "Dostupno je novo ažuriranje!"
STR_CURRENT_VERSION: "Trenutna verzija: "
STR_NEW_VERSION: "Nova verzija: "
STR_UPDATING: "Ažuriranje..."
STR_NO_UPDATE: "Nema dostupnih ažuriranja"
STR_UPDATE_FAILED: "Ažuriranje nije uspjelo"
STR_UPDATE_COMPLETE: "Ažuriranje završeno"
STR_POWER_ON_HINT: "Pritisni i drži dugme napajanja da ponovo uključiš"
STR_RESTARTING_HINT: "Ponovno pokretanje... Ako se uređaj ne pokrene ponovo, drži dugme napajanja nekoliko sekundi."
STR_NO_ENTRIES: "Nema pronađenih unosa"
STR_DOWNLOADING: "Preuzimanje..."
STR_DOWNLOAD_FAILED: "Preuzimanje nije uspjelo"
STR_ERROR_MSG: "Greška:"
STR_UNNAMED: "Neimenovano"
STR_HOLD_OPEN_TO_DELETE: "Drži Otvori za brisanje"
STR_NO_SERVER_URL: "Nije podešen URL servera"
STR_FETCH_FEED_FAILED: "Dohvatanje feeda nije uspjelo"
STR_PARSE_FEED_FAILED: "Obrada feeda nije uspjela"
STR_NEXT_PAGE: "Sljedeća stranica »"
STR_PREV_PAGE: "« Prethodna stranica"
STR_NETWORK_PREFIX: "Mreža: "
STR_IP_ADDRESS_PREFIX: "IP adresa: "
STR_ERROR_GENERAL_FAILURE: "Greška: Opšta greška"
STR_ERROR_NETWORK_NOT_FOUND: "Greška: Mreža nije pronađena"
STR_ERROR_CONNECTION_TIMEOUT: "Greška: Isteklo vrijeme veze"
STR_SD_CARD: "SD kartica"
STR_BACK: "« Nazad"
STR_EXIT: "« Izlaz"
STR_HOME: "« Početna"
STR_SELECT: "Odaberi"
STR_SELECTED: "Odabrano"
STR_TOGGLE: "Promijeni"
STR_TOGGLE_BOOKMARK: "Promijeni obilježivač"
STR_CONFIRM: "Potvrdi"
STR_CANCEL: "Otkaži"
STR_CONNECT: "Poveži"
STR_OPEN: "Otvori"
STR_DOWNLOAD: "Preuzmi"
STR_RETRY: "Pokušaj ponovo"
STR_YES: "Da"
STR_NO: "Ne"
STR_SHOW: "Prikaži"
STR_HIDE: "Sakrij"
STR_STATE_ON: "UKLJUČENO"
STR_STATE_OFF: "ISKLJUČENO"
STR_NOT_SET: "Nije postavljeno"
STR_DIR_LEFT: "Lijevo"
STR_DIR_RIGHT: "Desno"
STR_DIR_UP: "Gore"
STR_DIR_DOWN: "Dolje"
STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Filter korica ekrana mirovanja"
STR_FILTER_CONTRAST: "Kontrast"
STR_CUSTOMISE_STATUS_BAR: "Prilagodi statusnu traku"
STR_CHAPTER_PAGE_COUNT: "Broj stranica poglavlja"
STR_BOOK_PROGRESS_PERCENTAGE: "Procenat napretka knjige"
STR_PROGRESS_BAR: "Traka napretka"
STR_PROGRESS_BAR_THICKNESS: "Debljina trake napretka"
STR_PROGRESS_BAR_THIN: "Tanka"
STR_PROGRESS_BAR_MEDIUM: "Srednja"
STR_PROGRESS_BAR_THICK: "Debela"
STR_BOOK: "Knjiga"
STR_CHAPTER: "Poglavlje"
STR_EXAMPLE_CHAPTER: "Poglavlje 21"
STR_EXAMPLE_BOOK: "Naslov knjige"
STR_PREVIEW: "Pregled"
STR_TITLE: "Naslov"
STR_BATTERY: "Baterija"
STR_XTC_STATUS_BAR: "XTC statusna traka"
STR_BOTTOM: "Dolje"
STR_TOP: "Gore"
STR_CLOCK: "Sat"
STR_CLOCK_UTC_OFFSET: "UTC odstupanje sata"
STR_CLOCK_FORMAT: "Format sata"
STR_CLOCK_FORMAT_24H: "24-časovni"
STR_CLOCK_FORMAT_12H: "12-časovni"
STR_CURRENT_TIME: "Trenutno vrijeme:"
STR_NEXT_FIELD: "Sljedeće"
STR_CLOCK_SYNC: "Sinhronizuj sat"
STR_CLOCK_SYNC_NOW: "Sinhronizuj sat sada"
STR_CLOCK_SYNCING: "Sinhronizacija sa NTP-a..."
STR_CLOCK_SYNC_OK: "Sat sinhronizovan"
STR_CLOCK_SYNC_FAIL: "Sinhronizacija nije uspjela"
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi nije povezan"
STR_CLOCK_SYNC_NO_WIFI_HINT: "Prvo se poveži na Wi-Fi, pa pokušaj ponovo."
STR_CLOCK_SYNCED: "Sat sinhronizovan"
STR_UI_THEME: "Tema korisničkog interfejsa"
STR_THEME_CLASSIC: "Klasična"
STR_THEME_LYRA: "Lyra"
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
STR_THEME_LYRA_EXTENDED: "Lyra proširena"
STR_SUNLIGHT_FADING_FIX: "Ispravka blijeđenja na suncu"
STR_REMAP_FRONT_BUTTONS: "Premapiraj prednja dugmad"
STR_BOOKMARKS: "Obilježivači"
STR_BOOKMARK_ADDED: "Obilježivač dodan."
STR_BOOKMARK_REMOVED: "Obilježivač uklonjen."
STR_OPDS_BROWSER: "OPDS pregledač"
STR_SEARCH: "Pretraga"
STR_COVER_CUSTOM: "Korice + Prilagođeno"
STR_QUICK_RESUME: "Brzi nastavak"
STR_MENU_RECENT_BOOKS: "Nedavne knjige"
STR_REMOVE_FROM_RECENTS: "Ukloniti iz nedavnih knjiga?"
STR_NO_RECENT_BOOKS: "Nema nedavnih knjiga"
STR_CALIBRE_DESC: "Koristi Calibre bežični prijenos uređaja"
STR_FORGET_AND_REMOVE: "Zaboraviti mrežu i ukloniti sačuvanu lozinku?"
STR_FORGET_BUTTON: "Zaboravi"
STR_CALIBRE_STARTING: "Pokretanje Calibrea..."
STR_CALIBRE_SETUP: "Podešavanje"
STR_CALIBRE_STATUS: "Status"
STR_CLEAR_BUTTON: "Očisti"
STR_DEFAULT_VALUE: "Zadano"
STR_REMAP_PROMPT: "Pritisni prednje dugme za svaku ulogu"
STR_UNASSIGNED: "Nedodijeljeno"
STR_ALREADY_ASSIGNED: "Već dodijeljeno"
STR_REMAP_RESET_HINT: "Bočno dugme Gore: Vrati na zadani raspored"
STR_REMAP_CANCEL_HINT: "Bočno dugme Dolje: Otkaži premapiranje"
STR_HW_BACK_LABEL: "Nazad (1. dugme)"
STR_HW_CONFIRM_LABEL: "Potvrdi (2. dugme)"
STR_HW_LEFT_LABEL: "Lijevo (3. dugme)"
STR_HW_RIGHT_LABEL: "Desno (4. dugme)"
STR_GO_TO_PERCENT: "Idi na %"
STR_GO_HOME_BUTTON: "Idi na početnu"
STR_SYNC_PROGRESS: "Sinhronizuj napredak"
STR_DELETE_CACHE: "Obriši keš knjige"
STR_DELETE: "Obriši"
STR_CONFIRM_DELETE_BOOKMARK: "Obrisati ovaj obilježivač?"
STR_DISPLAY_QR: "Prikaži stranicu kao QR"
STR_CHAPTER_PREFIX: "Poglavlje: "
STR_PAGES_SEPARATOR: " stranica | "
STR_BOOK_PREFIX: "Knjiga: "
STR_CALIBRE_URL_HINT: "Za Calibre, dodaj /opds na svoj URL"
STR_SYNCING_TIME: "Sinhronizacija vremena..."
STR_CALC_HASH: "Izračunavanje heša dokumenta..."
STR_HASH_FAILED: "Izračunavanje heša dokumenta nije uspjelo"
STR_FETCH_PROGRESS: "Dohvatanje udaljenog napretka..."
STR_UPLOAD_PROGRESS: "Slanje napretka..."
STR_NO_CREDENTIALS_MSG: "Nisu podešene vjerodajnice"
STR_KOREADER_SETUP_HINT: "Podesi KOReader nalog u Postavkama"
STR_PROGRESS_FOUND: "Napredak pronađen!"
STR_REMOTE_LABEL: "Udaljeno:"
STR_LOCAL_LABEL: "Lokalno:"
STR_PAGE_OVERALL_FORMAT: "Stranica %d, %.2f%% ukupno"
STR_PAGE_TOTAL_OVERALL_FORMAT: "Stranica %d/%d, %.2f%% ukupno"
STR_DEVICE_FROM_FORMAT: " Sa: %s"
STR_APPLY_REMOTE: "Primijeni udaljeni napredak"
STR_UPLOAD_LOCAL: "Pošalji lokalni napredak"
STR_NO_REMOTE_MSG: "Nije pronađen udaljeni napredak"
STR_UPLOAD_PROMPT: "Poslati trenutnu poziciju?"
STR_UPLOAD_SUCCESS: "Napredak poslan!"
STR_SYNC_FAILED_MSG: "Sinhronizacija nije uspjela"
STR_SAVE_PROGRESS_FAILED: "Napredak nije mogao biti sačuvan"
STR_SECTION_PREFIX: "Sekcija "
STR_UPLOAD: "Pošalji"
STR_BOOK_S_STYLE: "Stil knjige"
STR_EMBEDDED_STYLE: "Ugrađeni stil"
STR_FOCUS_READING: "Fokusirano čitanje"
STR_OPDS_SERVER_URL: "URL OPDS servera"
STR_PWR_BTN_FOOTNOTE_BACK: "Brzi povratak iz fusnota"
STR_SET_SLEEP_COVER: "Postavi korice"
STR_FOOTNOTES: "Fusnote"
STR_NO_FOOTNOTES: "Nema fusnota na ovoj stranici"
STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Snimi ekran"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikad"
STR_STEP_HINT_FRONT: "Prednja dugmad:"
STR_STEP_HINT_SIDE: "Bočna dugmad:"
STR_ADD_SERVER: "Dodaj server"
STR_SERVER_NAME: "Naziv servera"
STR_NO_SERVERS: "Nema podešenih OPDS servera"
STR_DELETE_SERVER: "Obriši server"
STR_OPDS_SERVERS: "OPDS serveri"
STR_AUTO_TURN_ENABLED: "Automatsko okretanje omogućeno: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatsko okretanje (stranica po minuti)"
STR_MANAGE_FONTS: "Upravljaj fontovima"
STR_FONT_BROWSER: "Pregledač fontova"
STR_LOADING_FONT_LIST: "Učitavanje liste fontova..."
STR_NO_FONTS_AVAILABLE: "Nema dostupnih fontova"
STR_FONT_INSTALLED: "Font instaliran!"
STR_FONT_INSTALL_FAILED: "Instalacija fonta nije uspjela"
STR_INSTALLED: "Instalirano"
STR_DOWNLOAD_ALL: "Preuzmi sve"
STR_UPDATE_ALL: "Ažuriraj sve"
STR_UPDATE_AVAILABLE: "Ažuriranje"
STR_CRASH_TITLE: "Sistemski pad"
STR_CRASH_DESCRIPTION: "Detaljan izvještaj je sačuvan u crash_report.txt. Molimo priloži ovu datoteku u svoj izvještaj o grešci."
STR_CRASH_REASON: "Razlog pada:"
STR_CRASH_NO_REASON: "(Razlog nije zabilježen)"
STR_TILT_PAGE_TURN: "Okretanje stranice naginjanjem"
STR_KB_HINT_MOVE_CURSOR: "Pritisni LIJEVO ili DESNO za pomjeranje kursora"
STR_KB_HINT_RETURN_CURSOR: "Pritisni LIJEVO za povratak na poziciju kursora"
STR_KB_HINT_HIDE_PASSWORD: "Drži DESNO pa pritisni [***] za skrivanje lozinke"
STR_KB_HINT_SHOW_PASSWORD: "Drži DESNO pa pritisni [abc] za prikaz lozinke"
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Pritisni [***] za skrivanje lozinke"
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Pritisni [abc] za prikaz lozinke"
STR_KB_HINT_EDIT_ENTRY: "Drži GORE za uređivanje unosa"
STR_KB_TIPS: "Savjeti:"
STR_KB_HINT_RETURN_KEYBOARD: "Pritisni DOLJE za povratak na tastaturu"
STR_KB_HINT_EXIT_URL_MODE: "Pritisni ABC za izlazak iz URL načina"
STR_KB_HINT_CLEAR_TEXT: "Drži DEL za brisanje cijelog teksta"
STR_KB_HINT_SECONDARY_CHAR: "Drži SELECT za sekundarni znak"
STR_KB_HINT_UPPER_SECONDARY: "Drži SELECT za VELIKA SLOVA ili sekundarni znak"
STR_KB_HINT_LOWER_SECONDARY: "Drži SELECT za mala slova ili sekundarni znak"
STR_KB_HINT_URL_SNIPPETS: "Pritisni URL za isječke"
STR_SD_FIRMWARE_UPDATE: "Ažuriranje firmvera sa SD kartice"
STR_SELECT_FIRMWARE_FILE: "Odaberi datoteku firmvera (.bin)"
STR_NO_BIN_FILES: "Nema pronađenih .bin datoteka"
STR_VALIDATING_FIRMWARE: "Provjera firmvera..."
STR_INVALID_FIRMWARE: "Nevažeća datoteka firmvera"
STR_FIRMWARE_TOO_LARGE: "Firmver je prevelik za particiju"
STR_FIRMWARE_TOO_SMALL: "Datoteka firmvera je premala"
STR_FIRMWARE_UPDATE_PROMPT: "Ažurirati firmver?"
STR_FIRMWARE_FILE_OPEN_FAILED: "Datoteka se ne može otvoriti"
STR_FIRMWARE_WRITE_FAILED: "Upisivanje firmvera nije uspjelo"
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Ne isključuj napajanje!"
STR_RECOVERY_MODE: "Način oporavka"
STR_RECOVERY_MODE_HINT: "Stavi firmware.bin u korijenski direktorij SD kartice i odaberi ga"
-5
View File
@@ -347,11 +347,6 @@ STR_SERVER_NAME: "Nom del servidor"
STR_NO_SERVERS: "No hi ha servidors OPDS configurats"
STR_DELETE_SERVER: "Suprimeix el servidor"
STR_OPDS_SERVERS: "Servidors OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Carpeta de baixades"
STR_OPDS_FILENAME_FORMAT: "Format del nom de fitxer"
STR_FMT_AUTHOR_TITLE: "Autor - Títol"
STR_FMT_TITLE_AUTHOR: "Títol - Autor"
STR_FMT_TITLE: "Títol"
STR_MANAGE_FONTS: "Gestiona les fonts"
STR_FONT_BROWSER: "Navegador de fonts"
STR_LOADING_FONT_LIST: "S'està carregant la llista de fonts..."
-5
View File
@@ -270,11 +270,6 @@ STR_BOOK_S_STYLE: "Styl knihy"
STR_EMBEDDED_STYLE: "Vložený styl"
STR_FOCUS_READING: "Soustředěné čtení"
STR_OPDS_SERVER_URL: "URL serveru OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Složka pro stahování"
STR_OPDS_FILENAME_FORMAT: "Formát názvu souboru"
STR_FMT_AUTHOR_TITLE: "Autor - Název"
STR_FMT_TITLE_AUTHOR: "Název - Autor"
STR_FMT_TITLE: "Název"
STR_SCREENSHOT_BUTTON: "Udělat snímek obrazovky"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikdy"
-5
View File
@@ -300,11 +300,6 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Aldrig"
STR_STEP_HINT_FRONT: "Frontknapper:"
STR_STEP_HINT_SIDE: "Sideknapper:"
STR_OPDS_DOWNLOAD_FOLDER: "Downloadmappe"
STR_OPDS_FILENAME_FORMAT: "Filnavnsformat"
STR_FMT_AUTHOR_TITLE: "Forfatter - Titel"
STR_FMT_TITLE_AUTHOR: "Titel - Forfatter"
STR_FMT_TITLE: "Titel"
STR_SCREENSHOT_BUTTON: "Tag skærmbillede"
STR_AUTO_TURN_ENABLED: "Automatisk sidevendning aktiveret: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vending (sider per minut)"
-5
View File
@@ -300,11 +300,6 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nooit"
STR_STEP_HINT_FRONT: "Voorknoppen:"
STR_STEP_HINT_SIDE: "Zijknoppen:"
STR_OPDS_DOWNLOAD_FOLDER: "Downloadmap"
STR_OPDS_FILENAME_FORMAT: "Bestandsnaamformaat"
STR_FMT_AUTHOR_TITLE: "Auteur - Titel"
STR_FMT_TITLE_AUTHOR: "Titel - Auteur"
STR_FMT_TITLE: "Titel"
STR_SCREENSHOT_BUTTON: "Screenshot maken"
STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: "
STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)"
-6
View File
@@ -347,12 +347,6 @@ STR_SERVER_NAME: "Server Name"
STR_NO_SERVERS: "No OPDS servers configured"
STR_DELETE_SERVER: "Delete Server"
STR_OPDS_SERVERS: "OPDS Servers"
STR_OPDS_DOWNLOAD_FOLDER: "Download folder"
STR_OPDS_FILENAME_FORMAT: "Filename format"
STR_FMT_AUTHOR_TITLE: "Author - Title"
STR_FMT_TITLE_AUTHOR: "Title - Author"
STR_FMT_TITLE: "Title"
STR_OPDS_SD_ROOT: "SD root"
STR_AUTO_TURN_ENABLED: "Auto Turn Enabled: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto Turn (Pages Per Minute)"
STR_MANAGE_FONTS: "Manage Fonts"
-5
View File
@@ -268,11 +268,6 @@ STR_BOOK_S_STYLE: "Kirjan tyyli"
STR_EMBEDDED_STYLE: "Upotettu tyyli"
STR_FOCUS_READING: "Keskittynyt lukeminen"
STR_OPDS_SERVER_URL: "OPDS-palvelimen osoite"
STR_OPDS_DOWNLOAD_FOLDER: "Latauskansio"
STR_OPDS_FILENAME_FORMAT: "Tiedostonimen muoto"
STR_FMT_AUTHOR_TITLE: "Tekijä - Nimi"
STR_FMT_TITLE_AUTHOR: "Nimi - Tekijä"
STR_FMT_TITLE: "Nimi"
STR_SCREENSHOT_BUTTON: "Ota kuvakaappaus"
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Ei koskaan"
-5
View File
@@ -301,11 +301,6 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Jamais"
STR_STEP_HINT_FRONT: "Boutons avant :"
STR_STEP_HINT_SIDE: "Boutons latéraux :"
STR_OPDS_DOWNLOAD_FOLDER: "Dossier de téléchargement"
STR_OPDS_FILENAME_FORMAT: "Format du nom de fichier"
STR_FMT_AUTHOR_TITLE: "Auteur - Titre"
STR_FMT_TITLE_AUTHOR: "Titre - Auteur"
STR_FMT_TITLE: "Titre"
STR_SCREENSHOT_BUTTON: "Capture d'écran"
STR_AUTO_TURN_ENABLED: "Tourne-page auto : "
STR_AUTO_TURN_PAGES_PER_MIN: "Tourne-page auto (pages par minute)"
-5
View File
@@ -329,11 +329,6 @@ STR_SERVER_NAME: "Servername"
STR_NO_SERVERS: "Keine OPDS-Server konfiguriert"
STR_DELETE_SERVER: "Server entfernen"
STR_OPDS_SERVERS: "OPDS-Server"
STR_OPDS_DOWNLOAD_FOLDER: "Download-Ordner"
STR_OPDS_FILENAME_FORMAT: "Dateinamenformat"
STR_FMT_AUTHOR_TITLE: "Autor - Titel"
STR_FMT_TITLE_AUTHOR: "Titel - Autor"
STR_FMT_TITLE: "Titel"
STR_AUTO_TURN_ENABLED: "Auto-Umblättern aktiv: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-Umblättern (Seiten/Min.)"
STR_IMAGES: "Bilder"
-5
View File
@@ -306,11 +306,6 @@ STR_NO_SERVERS: "לא הוגדרו שרתי OPDS"
STR_DELETE_SERVER: "מחק שרת"
STR_DELETE_CONFIRM: "למחוק שרת זה?"
STR_OPDS_SERVERS: "שרתי OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "תיקיית הורדות"
STR_OPDS_FILENAME_FORMAT: "תבנית שם קובץ"
STR_FMT_AUTHOR_TITLE: "מחבר - כותרת"
STR_FMT_TITLE_AUTHOR: "כותרת - מחבר"
STR_FMT_TITLE: "כותרת"
STR_AUTO_TURN_ENABLED: "דפדוף אוטומטי פועל: "
STR_AUTO_TURN_PAGES_PER_MIN: "דפדוף אוטומטי (דפים בדקה)"
STR_MANAGE_FONTS: "ניהול גופנים"
-5
View File
@@ -299,11 +299,6 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u perc"
STR_SLEEP_NEVER: "Soha"
STR_STEP_HINT_FRONT: "Elülső gombok:"
STR_STEP_HINT_SIDE: "Oldalsó gombok:"
STR_OPDS_DOWNLOAD_FOLDER: "Letöltési mappa"
STR_OPDS_FILENAME_FORMAT: "Fájlnév formátuma"
STR_FMT_AUTHOR_TITLE: "Szerző - Cím"
STR_FMT_TITLE_AUTHOR: "Cím - Szerző"
STR_FMT_TITLE: "Cím"
STR_SCREENSHOT_BUTTON: "Képernyőkép készítése"
STR_AUTO_TURN_ENABLED: "Automatikus lapozás bekapcsolva: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatikus lapozás (oldal/perc)"
-5
View File
@@ -315,11 +315,6 @@ STR_SERVER_NAME: "Nome server"
STR_NO_SERVERS: "Nessun server OPDS configurato"
STR_DELETE_SERVER: "Elimina server"
STR_OPDS_SERVERS: "Server OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Cartella download"
STR_OPDS_FILENAME_FORMAT: "Formato nome file"
STR_FMT_AUTHOR_TITLE: "Autore - Titolo"
STR_FMT_TITLE_AUTHOR: "Titolo - Autore"
STR_FMT_TITLE: "Titolo"
STR_AUTO_TURN_ENABLED: "Volta pagina automatico: "
STR_AUTO_TURN_PAGES_PER_MIN: "Volta pagina automatico (pag/min)"
STR_MANAGE_FONTS: "Gestisci font"
-5
View File
@@ -296,11 +296,6 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин"
STR_SLEEP_NEVER: "Ешқашан"
STR_STEP_HINT_FRONT: "Алдыңғы түймелер:"
STR_STEP_HINT_SIDE: "Бүйір түймелері:"
STR_OPDS_DOWNLOAD_FOLDER: "Жүктеп алу қалтасы"
STR_OPDS_FILENAME_FORMAT: "Файл атауының пішімі"
STR_FMT_AUTHOR_TITLE: "Автор - Атауы"
STR_FMT_TITLE_AUTHOR: "Атауы - Автор"
STR_FMT_TITLE: "Атауы"
STR_SCREENSHOT_BUTTON: "Скриншот түсіру"
STR_AUTO_TURN_ENABLED: "Автоматты бет аудару қосулы: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоматты бет аудару (минутына бет саны)"
-5
View File
@@ -297,11 +297,6 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min."
STR_SLEEP_NEVER: "Niekada"
STR_STEP_HINT_FRONT: "Priekiniai mygtukai:"
STR_STEP_HINT_SIDE: "Šoniniai mygtukai:"
STR_OPDS_DOWNLOAD_FOLDER: "Atsisiuntimų aplankas"
STR_OPDS_FILENAME_FORMAT: "Failo pavadinimo formatas"
STR_FMT_AUTHOR_TITLE: "Autorius - Pavadinimas"
STR_FMT_TITLE_AUTHOR: "Pavadinimas - Autorius"
STR_FMT_TITLE: "Pavadinimas"
STR_SCREENSHOT_BUTTON: "Ekrano nuotrauka"
STR_AUTO_TURN_ENABLED: "Auto-vertimas: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-vertimas (psl/min)"
-5
View File
@@ -316,11 +316,6 @@ STR_SERVER_NAME: "Nazwa serwera"
STR_NO_SERVERS: "Brak skonfigurowanych serwerów OPDS"
STR_DELETE_SERVER: "Usuń serwer"
STR_OPDS_SERVERS: "Serwery OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Folder pobierania"
STR_OPDS_FILENAME_FORMAT: "Format nazwy pliku"
STR_FMT_AUTHOR_TITLE: "Autor - Tytuł"
STR_FMT_TITLE_AUTHOR: "Tytuł - Autor"
STR_FMT_TITLE: "Tytuł"
STR_AUTO_TURN_ENABLED: "Auto-kartkowanie: "
STR_AUTO_TURN_PAGES_PER_MIN: "Auto-kartkowanie (str./min)"
STR_DOWNLOAD_FONTS: "Pobierz czcionki"
-5
View File
@@ -339,11 +339,6 @@ STR_SERVER_NAME: "Nome do Servidor"
STR_NO_SERVERS: "Nenhum servidor OPDS configurado"
STR_DELETE_SERVER: "Excluir Servidor"
STR_OPDS_SERVERS: "Servidores OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Pasta de downloads"
STR_OPDS_FILENAME_FORMAT: "Formato do nome do arquivo"
STR_FMT_AUTHOR_TITLE: "Autor - Título"
STR_FMT_TITLE_AUTHOR: "Título - Autor"
STR_FMT_TITLE: "Título"
STR_AUTO_TURN_ENABLED: "Virada automática ativada: "
STR_AUTO_TURN_PAGES_PER_MIN: "Virada automática (páginas por minuto)"
STR_MANAGE_FONTS: "Gerenciar fontes"
-5
View File
@@ -300,11 +300,6 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Niciodată"
STR_STEP_HINT_FRONT: "Butoane frontale:"
STR_STEP_HINT_SIDE: "Butoane laterale:"
STR_OPDS_DOWNLOAD_FOLDER: "Dosar descărcări"
STR_OPDS_FILENAME_FORMAT: "Format nume fișier"
STR_FMT_AUTHOR_TITLE: "Autor - Titlu"
STR_FMT_TITLE_AUTHOR: "Titlu - Autor"
STR_FMT_TITLE: "Titlu"
STR_SCREENSHOT_BUTTON: "Captură ecran"
STR_AUTO_TURN_ENABLED: "Răsfoire automată: "
STR_AUTO_TURN_PAGES_PER_MIN: "Pagini pe minut"
-5
View File
@@ -339,11 +339,6 @@ STR_SERVER_NAME: "Имя сервера"
STR_NO_SERVERS: "Нет настроенных серверов OPDS"
STR_DELETE_SERVER: "Удалить сервер"
STR_OPDS_SERVERS: "Серверы OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Папка загрузок"
STR_OPDS_FILENAME_FORMAT: "Формат имени файла"
STR_FMT_AUTHOR_TITLE: "Автор - Название"
STR_FMT_TITLE_AUTHOR: "Название - Автор"
STR_FMT_TITLE: "Название"
STR_AUTO_TURN_ENABLED: "Автоперелистывание: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоперелистывание (стр./мин)"
STR_MANAGE_FONTS: "Управление шрифтами"
-5
View File
@@ -335,11 +335,6 @@ STR_SERVER_NAME: "Názov servera"
STR_NO_SERVERS: "Nie sú nakonfigurované žiadne OPDS servery"
STR_DELETE_SERVER: "Odstrániť server"
STR_OPDS_SERVERS: "OPDS servery"
STR_OPDS_DOWNLOAD_FOLDER: "Priečinok sťahovania"
STR_OPDS_FILENAME_FORMAT: "Formát názvu súboru"
STR_FMT_AUTHOR_TITLE: "Autor - Názov"
STR_FMT_TITLE_AUTHOR: "Názov - Autor"
STR_FMT_TITLE: "Názov"
STR_AUTO_TURN_ENABLED: "Automatické otáčanie strán zapnuté: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatické otáčanie (strán za minútu)"
STR_MANAGE_FONTS: "Správa písiem"
-5
View File
@@ -297,11 +297,6 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
STR_SLEEP_NEVER: "Nikoli"
STR_STEP_HINT_FRONT: "Sprednji gumbi:"
STR_STEP_HINT_SIDE: "Stranski gumbi:"
STR_OPDS_DOWNLOAD_FOLDER: "Mapa za prenose"
STR_OPDS_FILENAME_FORMAT: "Oblika imena datoteke"
STR_FMT_AUTHOR_TITLE: "Avtor - Naslov"
STR_FMT_TITLE_AUTHOR: "Naslov - Avtor"
STR_FMT_TITLE: "Naslov"
STR_SCREENSHOT_BUTTON: "Naredi posnetek zaslona"
STR_AUTO_TURN_ENABLED: "Samodejno obračanje: "
STR_AUTO_TURN_PAGES_PER_MIN: "Samodejno obračanje (strani na minuto)"
-5
View File
@@ -345,11 +345,6 @@ STR_SERVER_NAME: "Nombre de servidor"
STR_NO_SERVERS: "No se configuraron servidores OPDS"
STR_DELETE_SERVER: "Borrar servidor"
STR_OPDS_SERVERS: "Servidores OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Carpeta de descargas"
STR_OPDS_FILENAME_FORMAT: "Formato del nombre de archivo"
STR_FMT_AUTHOR_TITLE: "Autor - Título"
STR_FMT_TITLE_AUTHOR: "Título - Autor"
STR_FMT_TITLE: "Título"
STR_AUTO_TURN_ENABLED: "Avance activado: "
STR_AUTO_TURN_PAGES_PER_MIN: "Avance auto. (pág./min)"
STR_MANAGE_FONTS: "Gestionar tipografías"
-5
View File
@@ -342,11 +342,6 @@ STR_SERVER_NAME: "Servernamn"
STR_NO_SERVERS: "Inga OPDS-servrar konfigurerade"
STR_DELETE_SERVER: "Ta bort server"
STR_OPDS_SERVERS: "OPDS-servrar"
STR_OPDS_DOWNLOAD_FOLDER: "Hämtningsmapp"
STR_OPDS_FILENAME_FORMAT: "Filnamnsformat"
STR_FMT_AUTHOR_TITLE: "Författare - Titel"
STR_FMT_TITLE_AUTHOR: "Titel - Författare"
STR_FMT_TITLE: "Titel"
STR_AUTO_TURN_ENABLED: "Automatisk vändning aktiverad: "
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vändning (sidor per minut)"
STR_MANAGE_FONTS: "Hantera teckensnitt"
-5
View File
@@ -291,11 +291,6 @@ STR_SLEEP_TIMER_VALUE_FORMAT: "%u dak"
STR_SLEEP_NEVER: "Asla"
STR_STEP_HINT_FRONT: "Ön tuşlar:"
STR_STEP_HINT_SIDE: "Yan tuşlar:"
STR_OPDS_DOWNLOAD_FOLDER: "İndirme klasörü"
STR_OPDS_FILENAME_FORMAT: "Dosya adı biçimi"
STR_FMT_AUTHOR_TITLE: "Yazar - Başlık"
STR_FMT_TITLE_AUTHOR: "Başlık - Yazar"
STR_FMT_TITLE: "Başlık"
STR_NO_FILES_FOUND: "Dosya bulunamadı"
STR_NO_FOOTNOTES: "Bu sayfada dipnot yok"
STR_PREVIEW: "Önizleme"
-5
View File
@@ -336,11 +336,6 @@ STR_SERVER_NAME: "Назва сервера"
STR_NO_SERVERS: "Не налаштовано жодного сервера OPDS"
STR_DELETE_SERVER: "Видалити сервер"
STR_OPDS_SERVERS: "Сервери OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Папка завантажень"
STR_OPDS_FILENAME_FORMAT: "Формат імені файлу"
STR_FMT_AUTHOR_TITLE: "Автор - Назва"
STR_FMT_TITLE_AUTHOR: "Назва - Автор"
STR_FMT_TITLE: "Назва"
STR_AUTO_TURN_ENABLED: "Автоперегортання увімк: "
STR_AUTO_TURN_PAGES_PER_MIN: "Автоперегортання (ст/хв)"
STR_MANAGE_FONTS: "Керування шрифтами"
-5
View File
@@ -348,11 +348,6 @@ STR_SERVER_NAME: "Nom del servidor"
STR_NO_SERVERS: "No hi ha servidors OPDS configurats"
STR_DELETE_SERVER: "Elimina el servidor"
STR_OPDS_SERVERS: "Servidors OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Carpeta de baixades"
STR_OPDS_FILENAME_FORMAT: "Format del nom de fitxer"
STR_FMT_AUTHOR_TITLE: "Autor - Títol"
STR_FMT_TITLE_AUTHOR: "Títol - Autor"
STR_FMT_TITLE: "Títol"
STR_MANAGE_FONTS: "Gestiona les fonts"
STR_FONT_BROWSER: "Navegador de fonts"
STR_LOADING_FONT_LIST: "S'està carregant la llista de fonts..."
-5
View File
@@ -335,11 +335,6 @@ STR_SERVER_NAME: "Tên máy chủ"
STR_NO_SERVERS: "Chưa cấu hình máy chủ OPDS"
STR_DELETE_SERVER: "Xóa máy chủ"
STR_OPDS_SERVERS: "Máy chủ OPDS"
STR_OPDS_DOWNLOAD_FOLDER: "Thư mục tải xuống"
STR_OPDS_FILENAME_FORMAT: "Định dạng tên tệp"
STR_FMT_AUTHOR_TITLE: "Tác giả - Tựa đề"
STR_FMT_TITLE_AUTHOR: "Tựa đề - Tác giả"
STR_FMT_TITLE: "Tựa đề"
STR_AUTO_TURN_ENABLED: "Tự lật trang: "
STR_AUTO_TURN_PAGES_PER_MIN: "Tự lật (số trang mỗi phút)"
STR_MANAGE_FONTS: "Quản lý phông chữ"
+18 -4
View File
@@ -22,7 +22,21 @@ constexpr char DEVICE_ID[] = "crosspoint-reader";
// footprint is smaller than mbedTLS's old ~48KB peak, but keep a conservative
// floor. Check both total free heap and largest contiguous block so fragmented
// heap does not fall through into a failed TLS allocation path.
constexpr uint32_t MIN_HEAP_FOR_TLS = 55000;
// MEMFIX-PORT: TLS heap gate; portable
// Field data (July 2026): launching sync from a reader session lands at
// 51.9-58.2 KB free / 42-53 KB maxAlloc after WiFi comes up. wolfSSL handles
// allocation failure by returning MEMORY_E (no abort under -fno-exceptions),
// so an optimistic attempt degrades to the same clean "sync failed" as the
// gate — the gate only needs to keep out states where a doomed handshake
// would waste tens of seconds, not guarantee success.
//
// Free and largest-block have separate requirements: with SP ECC
// (WOLFSSL_HAVE_SP_ECC) the handshake's crypto uses fixed 256-bit arrays, so
// the largest single TLS allocation is the ~17 KB wolfSSL record buffer, not
// a run of fast-math bignums. A handshake was measured succeeding inside a
// 43 KB largest block; requiring 50 KB contiguous refused syncs that fit.
constexpr uint32_t MIN_FREE_FOR_TLS = 50000;
constexpr uint32_t MIN_BLOCK_FOR_TLS = 20000;
// Apply the shared KOSync auth headers after begin(). x-auth-* is the native
// KOSync scheme; Basic auth is added for Calibre-Web-Automated compatibility.
@@ -39,9 +53,9 @@ void applyAuthHeaders(freeink::SecureHttpClient& http) {
bool insufficientHeap() {
const uint32_t freeHeap = ESP.getFreeHeap();
const uint32_t maxAllocHeap = ESP.getMaxAllocHeap();
if (freeHeap < MIN_HEAP_FOR_TLS || maxAllocHeap < MIN_HEAP_FOR_TLS) {
LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free, %u max alloc (need %u)", freeHeap,
maxAllocHeap, MIN_HEAP_FOR_TLS);
if (freeHeap < MIN_FREE_FOR_TLS || maxAllocHeap < MIN_BLOCK_FOR_TLS) {
LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u), %u max alloc (need %u)", freeHeap,
MIN_FREE_FOR_TLS, maxAllocHeap, MIN_BLOCK_FOR_TLS);
return true;
}
return false;
+12
View File
@@ -65,6 +65,18 @@ void HalDisplay::displayBuffer(HalDisplay::RefreshMode mode, bool turnOffScreen)
einkDisplay.displayBuffer(convertRefreshMode(mode), turnOffScreen);
}
void HalDisplay::displayBufferAsync(HalDisplay::RefreshMode mode) {
if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) {
einkDisplay.requestResync(1);
}
einkDisplay.displayBufferAsyncNoShadow(convertRefreshMode(mode));
}
void HalDisplay::waitRefreshComplete() { einkDisplay.waitRefreshComplete(); }
bool HalDisplay::supportsAsyncRefresh() const { return einkDisplay.supportsAsyncRefresh(); }
void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) {
if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) {
einkDisplay.requestResync(1);
+11
View File
@@ -39,6 +39,17 @@ class HalDisplay {
bool fromProgmem = false) const;
void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
// Non-blocking refresh (shadow-free): starts the panel waveform and returns
// while the panel refreshes on its own. The framebuffer must stay untouched
// until waitRefreshComplete(), and the caller must rebuild the differential
// baseline before the next differential update (the tiled grayscale cleanup
// does). Panels without deferral fall back to a blocking refresh.
void displayBufferAsync(RefreshMode mode = RefreshMode::FAST_REFRESH);
// Block until a pending deferred refresh completes (no-op when none is).
void waitRefreshComplete();
// True when displayBufferAsync() genuinely overlaps (panel driver defers);
// false where it falls back to a blocking refresh.
bool supportsAsyncRefresh() const;
void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false);
// Power management
+19 -8
View File
@@ -225,17 +225,29 @@ unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); }
unsigned long HalGPIO::getPowerButtonHeldTime() const { return inputMgr.getPowerButtonHeldTime(); }
bool HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed) {
void HalGPIO::startDeepSleep() {
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
while (inputMgr.isPressed(BTN_POWER)) {
delay(50);
inputMgr.update();
}
// Arm the wakeup trigger *after* the button is released
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
// Enter Deep Sleep
esp_deep_sleep_start();
}
void HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed) {
if (shortPressAllowed) {
// Fast path - no duration check needed
return true;
return;
}
// TODO: Intermittent edge case remains: a single tap followed by another single tap
// can still power on the device. Tighten wake debounce/state handling here.
// Calibrate: subtract boot time already elapsed, assuming button held since boot.
const unsigned long calibration = millis();
const unsigned long calibratedDuration = (calibration < requiredDurationMs) ? (requiredDurationMs - calibration) : 1;
// Calibrate: subtract boot time already elapsed, assuming button held since boot
const uint16_t calibration = millis();
const uint16_t calibratedDuration = (calibration < requiredDurationMs) ? (requiredDurationMs - calibration) : 1;
const auto start = millis();
inputMgr.update();
@@ -250,12 +262,11 @@ bool HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPre
inputMgr.update();
} while (inputMgr.isPressed(BTN_POWER) && inputMgr.getPowerButtonHeldTime() < calibratedDuration);
if (inputMgr.getPowerButtonHeldTime() < calibratedDuration) {
return false;
startDeepSleep();
}
} else {
return false;
startDeepSleep();
}
return true;
}
bool HalGPIO::isUsbConnected() const {
+5 -2
View File
@@ -72,10 +72,13 @@ class HalGPIO {
unsigned long getHeldTime() const;
unsigned long getPowerButtonHeldTime() const;
// Setup wake up GPIO and enter deep sleep
void startDeepSleep();
// Verify power button was held long enough after wakeup.
// Returns true if verification succeeded, false if device should return to sleep.
// If verification fails, enters deep sleep and does not return.
// Should only be called when wakeup reason is PowerButton.
bool verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed);
void verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPressAllowed);
// Check if USB is connected
bool isUsbConnected() const;
+53 -2
View File
@@ -13,7 +13,10 @@ framework = arduino
monitor_speed = 115200
upload_speed = 921600
check_tool = cppcheck
check_flags = --enable=all --suppress=missingIncludeSystem --suppress=unusedFunction --suppress=unmatchedSuppression --suppress=*:*/.pio/* --inline-suppr
; missingInclude (project headers) is suppressed alongside missingIncludeSystem: on a
; fresh CI checkout cppcheck has no resolved include paths, so it reports every
; 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_skip_packages = yes
board_upload.flash_size = 16MB
@@ -42,7 +45,14 @@ build_flags =
-DWOLFSSL_OPTIONS_H
-DWOLFSSL_CLIENT_EXAMPLE
-DWOLFSSL_TLS13
-DWOLFSSL_SP_RISCV32
# MEMFIX-PORT: single-precision ECC (sp_c32.c). Without it every P-256 operation
# (TLS 1.3 key_share keygen, ECDHE, ECDSA cert verify) runs on fast-math bignums
# that WOLFSSL_SMALL_STACK heap-allocates at FP_MAX_BITS size -- tens of KB of
# temporaries, which OOMs (MP_MEM) at the ~50KB free heap a reading session
# leaves. SP uses fixed 256-bit arrays: a few KB, and several times faster.
# SP_SMALL trades the large precomputed point tables for smaller flash.
-DWOLFSSL_HAVE_SP_ECC
-DWOLFSSL_SP_SMALL
-DHAVE_TLS_EXTENSIONS
-DHAVE_SUPPORTED_CURVES
-DHAVE_HKDF
@@ -63,6 +73,47 @@ board_build.flash_mode = dio
board_build.flash_size = 16MB
board_build.partitions = partitions.csv
; MEMFIX-PORT: custom_sdkconfig heap reclamation (~32-37 KB). Rebuilds the
; Arduino core libs on first build (slower once, cached after; needs the CMake
; pin in platformio.local.ini on macOS).
;
; If an interrupted rebuild fails with "multiple definition of 'app_main'"
; (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 =
; 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
; 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.
CONFIG_ESP_TIMER_TASK_STACK_SIZE=4096
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2560
; Move the WiFi stack's non-critical hot paths out of IRAM into flash.
; On the C3, IRAM and DRAM share one SRAM pool, so the ~25-30 KB this
; frees lands directly in the heap — paid for with lower WiFi throughput
; during transfers (occasional sync/OTA use, not streaming: acceptable).
; IRAM cost is static, so the heap gain applies even with WiFi off.
CONFIG_ESP_WIFI_IRAM_OPT=n
CONFIG_ESP_WIFI_RX_IRAM_OPT=n
; Keep the Arduino wrappers for the removed cloud components (below) out of
; the core source list; all other bundled libraries default to enabled.
CONFIG_ARDUINO_SELECTIVE_COMPILATION=y
CONFIG_ARDUINO_SELECTIVE_RainMaker=n
CONFIG_ARDUINO_SELECTIVE_Insights=n
; Drop unused cloud components from the core rebuild. esp_insights/rainmaker
; require embedded server certs the lib builder can't generate
; ("https_server.crt.S not found"); this firmware uses none of them.
custom_component_remove =
espressif/esp_insights
espressif/esp_rainmaker
espressif/esp_diagnostics
espressif/esp_diag_data_store
espressif/esp_schedule
espressif/esp_rcp_update
espressif/esp_secure_cert_mgr
espressif/cbor
extra_scripts =
pre:scripts/patch_wolfssl.py
pre:scripts/build_html.py
+5 -1
View File
@@ -12,8 +12,12 @@ OVERRIDES = f"""
#ifndef HAVE_FFDHE_2048
#define HAVE_FFDHE_2048
#endif
/* MEMFIX-PORT: 8192 handles up to RSA-4096 keys (the public-CA maximum,
ISRG Root X1 included) with half the per-bignum heap of 16384: with
WOLFSSL_SMALL_STACK each fast-math temp is FP_MAX_BITS/8 * 2 bytes on the
heap, and TLS cert verification allocates dozens at once. */
#undef FP_MAX_BITS
#define FP_MAX_BITS 16384
#define FP_MAX_BITS 8192
"""
-8
View File
@@ -238,14 +238,6 @@ class CrossPointSettings {
// Reader screen margin settings
uint8_t screenMargin = 5;
// OPDS download destination folder ("" = SD root). Global; edited from the
// OPDS server list. Persisted via a category-less SettingInfo::String in
// SettingsList.h, so it stays out of the on-device Settings screen.
char opdsDownloadFolder[64] = "";
// On-disk filename format for OPDS downloads (0=Author-Title default, 1=Title-Author,
// 2=Title). See OpdsFilenameFormat. Persisted via a category-less SettingInfo::Enum,
// edited from the OPDS server list; hidden from the on-device Settings screen.
uint8_t opdsFilenameFormat = 0;
// Hide battery percentage
uint8_t hideBatteryPercentage = HIDE_NEVER;
// Long-press page turn button behavior
-10
View File
@@ -194,16 +194,6 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
SettingInfo::Toggle(StrId::STR_MOVE_FINISHED_TO_READ, &CrossPointSettings::moveFinishedToReadFolder,
"moveFinishedToReadFolder", StrId::STR_CAT_SYSTEM),
// OPDS download folder: persisted + web-exposed, but category-less so it
// is hidden from the on-device Settings screen (edited via OPDS UI).
SettingInfo::String(StrId::STR_OPDS_DOWNLOAD_FOLDER, &SETTINGS.opdsDownloadFolder[0],
sizeof(SETTINGS.opdsDownloadFolder), "opdsDownloadFolder"),
// OPDS download filename format: persisted + web-exposed, category-less so it
// is hidden from the on-device Settings screen (cycled from the OPDS UI).
SettingInfo::Enum(StrId::STR_OPDS_FILENAME_FORMAT, &CrossPointSettings::opdsFilenameFormat,
{StrId::STR_FMT_AUTHOR_TITLE, StrId::STR_FMT_TITLE_AUTHOR, StrId::STR_FMT_TITLE},
"opdsFilenameFormat"),
// --- KOReader Sync (web-only, uses KOReaderCredentialStore) ---
SettingInfo::DynamicString(
StrId::STR_KOREADER_USERNAME, [] { return KOREADER_STORE.getUsername(); },
@@ -2,13 +2,11 @@
#include <Arduino.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <I18n.h>
#include <Logging.h>
#include <OpdsStream.h>
#include <WiFi.h>
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "SilentRestart.h"
#include "activities/network/WifiSelectionActivity.h"
@@ -17,7 +15,6 @@
#include "fontIds.h"
#include "network/HttpDownloader.h"
#include "util/BookCacheUtils.h"
#include "util/OpdsFilename.h"
#include "util/StringUtils.h"
#include "util/UrlUtils.h"
@@ -282,26 +279,8 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
// Build full download URL relative to the current feed, not the root server URL
const std::string feedUrl = UrlUtils::buildUrl(server.url, currentPath);
std::string downloadUrl = UrlUtils::buildUrl(feedUrl, book.href);
// opdsDownloadFolder is already a null-terminated char[64]; use it directly —
// no std::string copy. exists()/mkdir() take const char*.
const char* folder = SETTINGS.opdsDownloadFolder; // "" => SD root
bool haveFolder = folder[0] != '\0';
if (haveFolder && !Storage.exists(folder) && !Storage.mkdir(folder)) {
// exists()-guard first: mkdir's return-on-existing is unconfirmed, and every
// existing caller checks exists() before mkdir. On real failure, fall back
// to SD root so the download is never lost.
LOG_ERR("OPDS", "mkdir failed for %s, using SD root", folder);
haveFolder = false;
}
// downloadToFile() needs a std::string, and titles are unbounded (a fixed
// char[] would truncate). Cold path (a multi-second download follows), so one
// reserve'd, in-place-appended owning string is the right call.
std::string filename;
filename.reserve(96);
if (haveFolder) filename += folder;
filename += '/';
filename += opdsBookFilename(book.author, book.title, static_cast<OpdsFilenameFormat>(SETTINGS.opdsFilenameFormat));
std::string filename =
"/" + StringUtils::sanitizeFilename((book.author.empty() ? "" : book.author + " - ") + book.title) + ".epub";
LOG_DBG("OPDS", "Downloading: %s -> %s", downloadUrl.c_str(), filename.c_str());
int lastRenderedPercent = -1;
+157 -15
View File
@@ -257,6 +257,18 @@ void EpubReaderActivity::openReaderMenu() {
});
}
bool EpubReaderActivity::buildTickHeapGate() {
const size_t freeHeap = ESP.getFreeHeap();
const size_t maxBlock = ESP.getMaxAllocHeap();
// Below the floors: just wait. The tick is deferrable — page-turn transients
// 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
// no build work is actually happening (the gate can stay closed for a long
// stretch if the retained build context itself holds the heap down).
buildHeapPaused = freeHeap < BACKGROUND_BUILD_MIN_FREE_HEAP || maxBlock < BACKGROUND_BUILD_MIN_MAX_ALLOC;
return !buildHeapPaused;
}
void EpubReaderActivity::loop() {
if (!epub) {
// Should never happen
@@ -264,6 +276,40 @@ void EpubReaderActivity::loop() {
return;
}
// Idle glyph prewarm for the likely next page (currentPage + 1). The scan
// pass draws nothing (FCM scan mode suppresses pixels), so the displayed
// framebuffer is untouched; endScanAndPrewarm loads only glyphs not already
// cached. Debounced past rapid page-flipping, one attempt per position, and
// deferred while a render/build owns the CPU or the heap is at the render
// floor. Cross-chapter prewarm is deliberately out of scope (next spine's
// section isn't loaded).
constexpr unsigned long IDLE_PREWARM_DEBOUNCE_MS = 400;
if (section && !section->isBuilding() && !RenderLock::peek() && renderer.hasFrameBuffer() &&
lastRenderCompleteMs != 0 && millis() - lastRenderCompleteMs > IDLE_PREWARM_DEBOUNCE_MS &&
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;
const int nextPage = section->currentPage + 1;
if (nextPage < static_cast<int>(section->pageCount)) {
if (const auto p = section->loadPage(nextPage)) {
if (auto* fcm = renderer.getFontCacheManager()) {
const auto t0 = millis();
auto scope = fcm->createPrewarmScope();
p->render(renderer, SETTINGS.getReaderFontId(), 0, 0); // scan only, no pixels
scope.endScanAndPrewarm();
LOG_DBG("ERS", "Idle prewarm: page %d in %lums", nextPage, millis() - t0);
}
}
}
}
}
// 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
// session, so reopening a partial deliberately does NOT start it (see the deferral in
@@ -299,14 +345,17 @@ void EpubReaderActivity::loop() {
// "far enough ahead" and stall the build at 0 pages -- then the first turn past the
// watermark re-parses the whole chapter synchronously. Keep ticking until it finalizes.
if (section && section->isBuilding() && !RenderLock::peek() &&
(section->isPartial() || static_cast<int>(section->pageCount) < section->currentPage + BUILD_WINDOW_AHEAD)) {
(section->isPartial() || static_cast<int>(section->pageCount) < section->currentPage + BUILD_WINDOW_AHEAD) &&
buildTickHeapGate()) {
RenderLock lock;
// 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
// buildSomeMore() would fail and wrongly reset the section. cppcheck can't see the cross-task
// mutation, so it flags this as always true.
// buildSomeMore() would fail and wrongly reset the section. The heap gate must be re-read
// 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
if (section->isBuilding()) {
if (section->isBuilding() && buildTickHeapGate()) {
if (!section->buildSomeMore(BACKGROUND_BUILD_PAGES_PER_TICK)) {
LOG_ERR("ERS", "Background section build failed");
section.reset();
@@ -1282,6 +1331,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
const auto start = millis();
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
LOG_DBG("ERS", "Rendered page in %dms", millis() - start);
lastRenderCompleteMs = millis();
}
// 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.
@@ -1351,6 +1401,13 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
const bool pageHasImagesNeedingDecode = pageHasImages && page->hasImagesNeedingDecode();
const bool needsTextGrayscale = SETTINGS.textAntiAliasing;
const bool needsAnyGrayscale = needsTextGrayscale || pageHasImages;
const bool tiledGrayscale = needsAnyGrayscale && renderer.supportsStripGrayscale();
// Whole-plane buffering only pays when the BW refresh genuinely runs async
// underneath it; on blocking panels (X3) it would just spend ~50 KB for the
// identical serial timing. Image pages take the blocking double-FAST path
// 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 = [&]() {
if (needsTextGrayscale) {
page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop);
@@ -1396,28 +1453,112 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
// regardless of residue.
pagesUntilFullRefresh = 1;
} else {
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh);
// Async form: start the waveform and return so the grayscale plane rendering
// below overlaps the panel's refresh time instead of following it.
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh, overlapRefresh);
}
const auto tDisplay = millis();
// Tiled grayscale: render each plane band-by-band into a small scratch and
// stream straight to the controller, leaving the BW framebuffer intact so no
// full-frame storeBwBuffer is needed; controller RAM is re-synced from the
// live framebuffer afterward. The page is re-rendered ceil(H/STRIP_ROWS) times
// per plane, but renderCharImpl culls out-of-band glyphs before decode so the
// cost stays close to one render. Both text (drawPixel) and images
// (DirectPixelWriter) honor the active strip target.
if (needsAnyGrayscale && renderer.supportsStripGrayscale()) {
// Tiled grayscale: render each plane band-by-band, leaving the BW
// framebuffer intact so no full-frame storeBwBuffer is needed; controller
// RAM is re-synced from the live framebuffer afterward. The page is
// re-rendered ceil(H/STRIP_ROWS) times per plane, but renderCharImpl culls
// out-of-band glyphs before decode so the cost stays close to one render.
// Both text (drawPixel) and images (DirectPixelWriter) honor the active
// strip target. When the BW refresh above went out async, the plane
// rendering below overlaps the panel's refresh time; only the controller
// RAM writes wait for BUSY.
if (tiledGrayscale) {
constexpr int STRIP_ROWS = 80;
const int gh = renderer.getDisplayHeight();
const int gwBytes = renderer.getDisplayWidthBytes();
const size_t planeBytes = static_cast<size_t>(gwBytes) * gh;
// Render one plane band-by-band into a whole-plane buffer without touching
// the controller, so it can run while the refresh is still in flight.
auto renderPlaneToBuffer = [&](const bool lsbPlane, uint8_t* buf) {
renderer.setRenderMode(lsbPlane ? GfxRenderer::GRAYSCALE_LSB : GfxRenderer::GRAYSCALE_MSB);
for (int y = 0; y < gh; y += STRIP_ROWS) {
const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS;
renderer.beginStripTarget(buf + static_cast<size_t>(y) * gwBytes, y, rows);
renderer.clearScreen(0x00);
renderGrayscalePass();
renderer.endStripTarget();
}
};
// Tiered on heap pressure: two plane buffers hide both plane renders
// 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
// no overlap. Each buffer is only attempted when it leaves ~60 KB free so
// the pass never starves concurrent allocations: the next page re-render
// allocates through throwing std::string paths that abort() on OOM under
// -fno-exceptions, so a plane buffer that "fits" but eats the render
// headroom is worse than the strip fallback. Blocking panels skip the
// buffers entirely (nothing to overlap).
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) {
renderPlaneToBuffer(true, lsbPlaneBuf.get());
if (msbPlaneBuf) renderPlaneToBuffer(false, msbPlaneBuf.get());
const auto tGrayRender = millis();
renderer.waitRefreshComplete();
const auto tWait = millis();
renderer.writeGrayscalePlaneStrip(true, lsbPlaneBuf.get(), 0, gh);
if (msbPlaneBuf) {
renderer.writeGrayscalePlaneStrip(false, msbPlaneBuf.get(), 0, gh);
} else {
renderPlaneToBuffer(false, lsbPlaneBuf.get());
renderer.writeGrayscalePlaneStrip(false, lsbPlaneBuf.get(), 0, gh);
}
const auto tGrayWrite = millis();
renderer.setRenderMode(GfxRenderer::BW);
renderer.displayGrayBuffer();
const auto tGrayDisplay = millis();
// BW framebuffer is intact; re-sync controller RAM for the next
// differential page turn directly from it.
renderer.cleanupGrayscaleWithFrameBuffer();
const auto tEnd = millis();
LOG_DBG("ERS",
"Page render (tiled async): prewarm=%lums bw_render=%lums display=%lums gray_render=%lums "
"wait=%lums gray_write=%lums gray_display=%lums cleanup=%lums total=%lums (planes buffered: %d)",
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tGrayRender - tDisplay, tWait - tGrayRender,
tGrayWrite - tWait, tGrayDisplay - tGrayWrite, tEnd - tGrayDisplay, tEnd - t0, msbPlaneBuf ? 2 : 1);
} else {
// Per-strip scratch tier: blocking panels (X3) and the OOM fallback.
// The strip writes below need the panel idle, so wait out any pending
// async refresh first (no-op on blocking panels).
auto scratch = makeUniqueNoThrow<uint8_t[]>(static_cast<size_t>(gwBytes) * STRIP_ROWS);
renderer.waitRefreshComplete();
if (!scratch) {
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 {
// Bands may be streamed in any order: X4 windows each via setRamArea, X3
// via PTL.
// Bands may be streamed in any order: X4 windows each via setRamArea,
// X3 via PTL.
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
for (int y = 0; y < gh; y += STRIP_ROWS) {
const int rows = (gh - y < STRIP_ROWS) ? (gh - y) : STRIP_ROWS;
@@ -1457,6 +1598,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tGrayLsb - tDisplay, tGrayMsb - tGrayLsb,
tGrayDisplay - tGrayMsb, tCleanup - tGrayDisplay, tEnd - t0);
}
}
} else {
// Fallback path for a controller without strip support. grayscale rendering
// TODO: Only do this if font supports it
+38 -2
View File
@@ -41,6 +41,13 @@ class EpubReaderActivity final : public Activity {
bool showBookmarkMessage = false;
bool ignoreNextConfirmRelease = false;
bool currentPageBookmarked = false;
// 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,
// so the next turn's in-render prewarm is a cache hit instead of ~100 ms of
// SD reads on the page-turn critical path. One attempt per position.
int idlePrewarmSpine = -1;
int idlePrewarmPage = -1;
unsigned long lastRenderCompleteMs = 0;
bool bookmarkRemoved = false; // true when last toggle removed (controls popup text)
std::vector<BookmarkEntry> cachedBookmarks;
// Tracks whether this book is currently removed from Recent Books by the
@@ -86,6 +93,33 @@ class EpubReaderActivity final : public Activity {
// background build chunk never noticeably delays input or a pending render.
static constexpr int BUILD_PAGES_PER_CHUNK = 8;
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
// word vectors of heap strings — throwing allocations that abort() on OOM under
// -fno-exceptions (field crash: bad_alloc in ParsedText::addWord during a
// background tick under heap pressure). The tick is deferrable work:
// 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.
static constexpr size_t BACKGROUND_BUILD_MIN_FREE_HEAP = 32 * 1024;
// Fragmentation floor for the same gate: a tick passed the free-heap floor at
// 34.7 KB free but the largest block was ~11 KB, and a parse allocation inside the
// tick aborted anyway. Free heap says how much memory exists; maxAlloc says whether
// any single allocation can actually have it. 16 KB also keeps the advance-table
// batch path (16 KB scratch) viable during builds.
static constexpr size_t BACKGROUND_BUILD_MIN_MAX_ALLOC = 16 * 1024;
// Gate for a background build tick: true when the heap can take parse allocations.
// Updates buildHeapPaused as a side effect.
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
// 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
@@ -143,8 +177,10 @@ class EpubReaderActivity final : public Activity {
// 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
// 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.
bool skipLoopDelay() override { return section && section->isBuilding(); }
// it from page 0. Reverts to normal power behavior the moment the build finishes,
// 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; }
ScreenshotInfo getScreenshotInfo() const override;
CrossPointPosition getCurrentPosition() const;
+1 -13
View File
@@ -208,27 +208,15 @@ void KOReaderSyncActivity::performUpload() {
// Optionally include document metadata (KOReader PR #15306)
if (KOREADER_STORE.getSendMetadata()) {
// The Epub is released before the sync network calls and is only reloaded on the
// remote-progress path (performSync). When uploading from NO_REMOTE_PROGRESS the
// Epub is still null, so reload it here and guard the title/author reads to avoid
// dereferencing a null Epub. Filename is derived from the path and is always safe.
ensureEpubLoaded();
KOReaderMetadata meta;
// Extract filename from path
const auto lastSlash = epubPath.rfind('/');
meta.filename = (lastSlash != std::string::npos) ? epubPath.substr(lastSlash + 1) : epubPath;
if (epub) {
meta.title = epub->getTitle();
meta.authors = epub->getAuthor();
} else {
LOG_ERR("KOSync", "Epub unavailable for metadata; sending filename only");
}
progress.metadata = std::move(meta);
}
// Release the Epub before the network call so the TLS handshake has enough free heap
// (consistent with the release-before-sync pattern in performSync); nothing below needs it.
epub.reset();
const auto result = KOReaderSyncClient::updateProgress(progress);
// Drop the radio while user reads the result; full teardown happens at silent reboot.
+12 -3
View File
@@ -59,12 +59,21 @@ inline PageTurnResult detectPageTurn(const MappedInputManager& input) {
return {prev, next, tiltPrev || tiltNext};
}
inline void displayWithRefreshCycle(const GfxRenderer& renderer, int& pagesUntilFullRefresh) {
// One helper, blocking or deferred: the async form starts the refresh and
// returns so the caller can overlap CPU work with the panel's refresh time.
// Async callers must not touch the framebuffer until
// renderer.waitRefreshComplete() and must rebuild the differential baseline
// before the next page turn (the tiled grayscale cleanup does).
inline void displayWithRefreshCycle(const GfxRenderer& renderer, int& pagesUntilFullRefresh, bool async = false) {
const auto mode = (pagesUntilFullRefresh <= 1) ? HalDisplay::HALF_REFRESH : HalDisplay::FAST_REFRESH;
if (async) {
renderer.displayBufferAsync(mode);
} else {
renderer.displayBuffer(mode);
}
if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
} else {
renderer.displayBuffer();
pagesUntilFullRefresh--;
}
}
@@ -3,51 +3,19 @@
#include <GfxRenderer.h>
#include <I18n.h>
#include <cstring>
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "OpdsServerStore.h"
#include "OpdsSettingsActivity.h"
#include "activities/ActivityManager.h"
#include "activities/browser/OpdsBookBrowserActivity.h"
#include "activities/util/KeyboardEntryActivity.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "util/OpdsFilename.h"
namespace {
// Normalizes a user-typed folder: trims spaces, "" => SD root, otherwise a
// single leading '/' and no trailing '/'. Cold path (runs once per edit).
std::string normalizeFolder(std::string v) {
while (!v.empty() && (v.front() == ' ' || v.front() == '\t')) v.erase(v.begin());
while (!v.empty() && (v.back() == ' ' || v.back() == '\t')) v.pop_back();
if (v.empty()) return "";
if (v.front() != '/') v.insert(v.begin(), '/');
while (v.size() > 1 && v.back() == '/') v.pop_back();
if (v == "/") return ""; // a bare slash is SD root, same as empty
return v;
}
// Label shown for the current OPDS filename format in the list subtitle.
StrId opdsFormatLabel(uint8_t format) {
switch (format) {
case static_cast<uint8_t>(OpdsFilenameFormat::TitleAuthor):
return StrId::STR_FMT_TITLE_AUTHOR;
case static_cast<uint8_t>(OpdsFilenameFormat::TitleOnly):
return StrId::STR_FMT_TITLE;
default:
return StrId::STR_FMT_AUTHOR_TITLE;
}
}
} // namespace
int OpdsServerListActivity::getItemCount() const {
int count = static_cast<int>(OPDS_STORE.getCount());
// Settings mode appends three virtual items: "Add Server", "Download folder"
// and "Filename format".
// In settings mode, append a virtual "Add Server" item; in picker mode, only show real servers
if (!pickerMode) {
count += 3;
count++;
}
return count;
}
@@ -106,34 +74,6 @@ void OpdsServerListActivity::handleSelection() {
return;
}
// Index layout: [servers 0..serverCount-1], [Add Server], [Download folder], [Filename format].
if (selectedIndex == serverCount + 1) {
auto folderHandler = [this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& kb = std::get<KeyboardResult>(result.data);
const std::string norm = normalizeFolder(kb.text);
strncpy(SETTINGS.opdsDownloadFolder, norm.c_str(), sizeof(SETTINGS.opdsDownloadFolder) - 1);
SETTINGS.opdsDownloadFolder[sizeof(SETTINGS.opdsDownloadFolder) - 1] = '\0';
SETTINGS.saveToFile();
requestUpdate();
}
};
startActivityForResult(
std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_OPDS_DOWNLOAD_FOLDER),
std::string(SETTINGS.opdsDownloadFolder), 63, InputType::Text),
folderHandler);
return;
}
// "Filename format": tap cycles through the available formats.
if (selectedIndex == serverCount + 2) {
SETTINGS.opdsFilenameFormat =
static_cast<uint8_t>((SETTINGS.opdsFilenameFormat + 1) % static_cast<uint8_t>(OpdsFilenameFormat::Count));
SETTINGS.saveToFile();
requestUpdate();
return;
}
// Settings mode: open editor for selected server, or create a new one
auto resultHandler = [this](const ActivityResult&) {
// Reload server list when returning from editor
@@ -171,30 +111,17 @@ void OpdsServerListActivity::render(RenderLock&&) {
// Secondary label: server URL (shown as subtitle when name is set).
GUI.drawList(
renderer, Rect{0, contentTop, pageWidth, contentHeight}, itemCount, selectedIndex,
[&servers, serverCount](int index) -> std::string {
[&servers, serverCount](int index) {
if (index < serverCount) {
const auto& server = servers[index];
return server.name.empty() ? server.url : server.name;
}
if (index == serverCount) {
return std::string(I18n::getInstance().get(StrId::STR_ADD_SERVER));
}
if (index == serverCount + 1) {
return std::string(I18n::getInstance().get(StrId::STR_OPDS_DOWNLOAD_FOLDER));
}
return std::string(I18n::getInstance().get(StrId::STR_OPDS_FILENAME_FORMAT));
},
[&servers, serverCount](int index) -> std::string {
[&servers, serverCount](int index) {
if (index < serverCount && !servers[index].name.empty()) {
return servers[index].url;
}
if (index == serverCount + 1) {
const char* f = SETTINGS.opdsDownloadFolder;
return f[0] ? std::string(f) : std::string(I18n::getInstance().get(StrId::STR_OPDS_SD_ROOT));
}
if (index == serverCount + 2) {
return std::string(I18n::getInstance().get(opdsFormatLabel(SETTINGS.opdsFilenameFormat)));
}
return std::string("");
});
}
+45 -4
View File
@@ -159,6 +159,49 @@ void silentRestartToReader() {
ESP.restart();
}
// Verify power button press duration on wake-up from deep sleep
// Pre-condition: isWakeupByPowerButton() == true
void verifyPowerButtonDuration() {
if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP) {
// Fast path for short press
// Needed because inputManager.isPressed() may take up to ~500ms to return the correct state
return;
}
// Give the user up to 1000ms to start holding the power button, and must hold for SETTINGS.getPowerButtonDuration()
const auto start = millis();
bool abort = false;
// Subtract the current time, because inputManager only starts counting the HeldTime from the first update()
// This way, we remove the time we already took to reach here from the duration,
// assuming the button was held until now from millis()==0 (i.e. device start time).
const uint16_t calibration = start;
const uint16_t calibratedPressDuration =
(calibration < SETTINGS.getPowerButtonDuration()) ? SETTINGS.getPowerButtonDuration() - calibration : 1;
gpio.update();
// Needed because inputManager.isPressed() may take up to ~500ms to return the correct state
while (!gpio.isPressed(HalGPIO::BTN_POWER) && millis() - start < 1000) {
delay(10); // only wait 10ms each iteration to not delay too much in case of short configured duration.
gpio.update();
}
t2 = millis();
if (gpio.isPressed(HalGPIO::BTN_POWER)) {
do {
delay(10);
gpio.update();
} while (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getPowerButtonHeldTime() < calibratedPressDuration);
abort = gpio.getPowerButtonHeldTime() < calibratedPressDuration;
} else {
abort = true;
}
if (abort) {
// Button released too early. Returning to sleep.
// IMPORTANT: Re-arm the wakeup trigger before sleeping again
powerManager.startDeepSleep(gpio);
}
}
void waitForPowerRelease() {
gpio.update();
while (gpio.isPressed(HalGPIO::BTN_POWER)) {
@@ -314,10 +357,8 @@ void setup() {
switch (wakeupReason) {
case HalGPIO::WakeupReason::PowerButton:
LOG_DBG("MAIN", "Verifying power button press duration");
if (!gpio.verifyPowerButtonWakeup(SETTINGS.getPowerButtonDuration(),
SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP)) {
powerManager.startDeepSleep(gpio);
}
gpio.verifyPowerButtonWakeup(SETTINGS.getPowerButtonDuration(),
SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP);
break;
case HalGPIO::WakeupReason::AfterUSBPower:
// If USB power caused a cold boot, go back to sleep
+17
View File
@@ -200,6 +200,14 @@ void CrossPointWebServer::begin() {
udpActive = udp.begin(LOCAL_UDP_PORT);
LOG_DBG("WEB", "Discovery UDP %s on port %d", udpActive ? "enabled" : "failed", LOCAL_UDP_PORT);
// All request handlers run on the task that calls handleClient(). Register
// that task before any handler can call esp_task_wdt_reset().
const esp_err_t watchdogResult = esp_task_wdt_add(nullptr);
watchdogTaskRegistered = watchdogResult == ESP_OK;
if (!watchdogTaskRegistered) {
LOG_ERR("WEB", "Failed to register web server task with watchdog: %s", esp_err_to_name(watchdogResult));
}
running = true;
LOG_DBG("WEB", "Web server started on port %d", port);
@@ -229,6 +237,10 @@ void CrossPointWebServer::abortWsUpload(const char* tag) {
void CrossPointWebServer::stop() {
if (!running || !server) {
LOG_DBG("WEB", "stop() called but already stopped (running=%d, server=%p)", running, server.get());
if (watchdogTaskRegistered) {
esp_task_wdt_delete(nullptr);
watchdogTaskRegistered = false;
}
return;
}
@@ -269,6 +281,11 @@ void CrossPointWebServer::stop() {
LOG_DBG("WEB", "Web server stopped and deleted");
LOG_DBG("WEB", "[MEM] Free heap after delete server: %d bytes", ESP.getFreeHeap());
if (watchdogTaskRegistered) {
esp_task_wdt_delete(nullptr);
watchdogTaskRegistered = false;
}
// Note: Static upload variables (uploadFileName, uploadPath, uploadError) are declared
// later in the file and will be cleared when they go out of scope or on next upload
LOG_DBG("WEB", "[MEM] Free heap final: %d bytes", ESP.getFreeHeap());
+1
View File
@@ -72,6 +72,7 @@ class CrossPointWebServer {
std::unique_ptr<WebServer> server = nullptr;
std::unique_ptr<WebSocketsServer> wsServer = nullptr;
bool running = false;
bool watchdogTaskRegistered = false;
bool apMode = false; // true when running in AP mode, false for STA mode
uint16_t port = 80;
uint16_t wsPort = 81; // WebSocket port
-23
View File
@@ -1,23 +0,0 @@
#include "OpdsFilename.h"
#include "StringUtils.h"
std::string opdsBookFilename(const std::string& author, const std::string& title, OpdsFilenameFormat format) {
std::string base;
switch (format) {
case OpdsFilenameFormat::TitleAuthor:
base = author.empty() ? title : title + " - " + author;
break;
case OpdsFilenameFormat::TitleOnly:
base = title;
break;
case OpdsFilenameFormat::AuthorTitle:
default:
base = author.empty() ? title : author + " - " + title;
break;
}
// sanitizeFilename caps at 100 bytes and never returns empty (falls back to
// "book"); ".epub" is appended after so the extension is never truncated —
// identical treatment to the previous inline construction.
return StringUtils::sanitizeFilename(base) + ".epub";
}
-18
View File
@@ -1,18 +0,0 @@
#pragma once
#include <cstdint>
#include <string>
// On-disk filename format for books downloaded from an OPDS server. Stored as a
// uint8_t in CrossPointSettings; cast to this enum at the call sites. `Count` is
// the number of selectable formats (used to cycle the setting in the UI).
enum class OpdsFilenameFormat : uint8_t {
AuthorTitle = 0, // "Author - Title.epub" (default; matches legacy behaviour)
TitleAuthor = 1, // "Title - Author.epub"
TitleOnly = 2, // "Title.epub"
Count = 3,
};
// Composes and sanitizes the on-disk filename (including the ".epub" extension)
// for a downloaded OPDS book, according to `format`. When the author is empty,
// every format collapses to just the sanitized title. Pure: no I/O, no globals.
std::string opdsBookFilename(const std::string& author, const std::string& title, OpdsFilenameFormat format);
-1
View File
@@ -43,6 +43,5 @@ add_subdirectory(release_json_parser)
add_subdirectory(differential_rounding)
add_subdirectory(hyphenation_eval)
add_subdirectory(utf8_compose)
add_subdirectory(opds_filename)
add_subdirectory(minibidi_arabic)
add_subdirectory(combining_marks)
-18
View File
@@ -1,18 +0,0 @@
add_executable(OpdsFilenameTest
OpdsFilenameTest.cpp
${REPO_ROOT}/src/util/OpdsFilename.cpp
${REPO_ROOT}/src/util/StringUtils.cpp
${REPO_ROOT}/lib/Utf8/Utf8.cpp
)
target_include_directories(OpdsFilenameTest PRIVATE
${REPO_ROOT}/src/util
${REPO_ROOT}/lib/Utf8
)
target_link_libraries(OpdsFilenameTest PRIVATE
crosspoint_test_common
GTest::gtest_main
)
gtest_discover_tests(OpdsFilenameTest)
-52
View File
@@ -1,52 +0,0 @@
#include <gtest/gtest.h>
#include <string>
#include "OpdsFilename.h"
namespace {
TEST(OpdsFilename, AuthorTitleIsDefaultOrder) {
EXPECT_EQ(opdsBookFilename("J. Doe", "My Book", OpdsFilenameFormat::AuthorTitle), "J. Doe - My Book.epub");
}
TEST(OpdsFilename, TitleAuthorSwapsOrder) {
EXPECT_EQ(opdsBookFilename("J. Doe", "My Book", OpdsFilenameFormat::TitleAuthor), "My Book - J. Doe.epub");
}
TEST(OpdsFilename, TitleOnlyIgnoresAuthor) {
EXPECT_EQ(opdsBookFilename("J. Doe", "My Book", OpdsFilenameFormat::TitleOnly), "My Book.epub");
}
TEST(OpdsFilename, EmptyAuthorCollapsesToTitleForEveryFormat) {
EXPECT_EQ(opdsBookFilename("", "My Book", OpdsFilenameFormat::AuthorTitle), "My Book.epub");
EXPECT_EQ(opdsBookFilename("", "My Book", OpdsFilenameFormat::TitleAuthor), "My Book.epub");
EXPECT_EQ(opdsBookFilename("", "My Book", OpdsFilenameFormat::TitleOnly), "My Book.epub");
}
TEST(OpdsFilename, IllegalCharactersAreSanitized) {
// '/' ':' '*' '?' etc. are replaced with '_' by sanitizeFilename.
EXPECT_EQ(opdsBookFilename("A/B", "C:D*E?", OpdsFilenameFormat::AuthorTitle), "A_B - C_D_E_.epub");
}
TEST(OpdsFilename, EmptyAuthorAndTitleFallsBackToBook) {
// sanitizeFilename returns "book" when nothing usable remains.
EXPECT_EQ(opdsBookFilename("", "", OpdsFilenameFormat::AuthorTitle), "book.epub");
EXPECT_EQ(opdsBookFilename("", "", OpdsFilenameFormat::TitleOnly), "book.epub");
}
TEST(OpdsFilename, LongNameIsTruncatedToByteBudgetBeforeExtension) {
// sanitizeFilename caps the base at 100 bytes; ".epub" is appended after.
const std::string longTitle(200, 'a');
const std::string result = opdsBookFilename("", longTitle, OpdsFilenameFormat::TitleOnly);
EXPECT_EQ(result, std::string(100, 'a') + ".epub");
EXPECT_EQ(result.size(), 105u);
}
TEST(OpdsFilename, UnknownFormatValueFallsBackToAuthorTitle) {
// Defensive: a persisted value outside the enum still yields a valid name.
const auto bogus = static_cast<OpdsFilenameFormat>(99);
EXPECT_EQ(opdsBookFilename("J. Doe", "My Book", bogus), "J. Doe - My Book.epub");
}
} // namespace