Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98118d6e24 | ||
|
|
3cc4069faa | ||
|
|
a3b8897977 | ||
|
|
f7f1a6f8bf | ||
|
|
7cad3cfb0e | ||
|
|
f180069643 | ||
|
|
a0ec5812b1 | ||
|
|
e84840b473 | ||
|
|
94ae750e89 | ||
|
|
8c5cd67f38 | ||
|
|
e66de575a1 | ||
|
|
bae29fc6ba | ||
|
|
379f49d165 |
@@ -2,6 +2,8 @@
|
||||
.idea
|
||||
.DS_Store
|
||||
.vscode
|
||||
open-x4-sdk
|
||||
fs_
|
||||
lib/EpdFont/fontsrc
|
||||
lib/I18n/I18nKeys.h
|
||||
lib/I18n/I18nStrings.h
|
||||
@@ -23,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
|
||||
|
||||
+1
-1
Submodule freeink-sdk updated: e7d336191b...4350bc7a0e
+44
-17
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -431,6 +431,11 @@ CssStyle CssParser::parseDeclarations(std::string_view declBlock) {
|
||||
// Rule processing
|
||||
|
||||
void CssParser::processRuleBlockWithStyle(std::string_view selectorGroup, const CssStyle& style) {
|
||||
// Skip rules that don't define any supported properties to save RAM.
|
||||
if (!style.defined.anySet()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we've reached the rule limit before processing
|
||||
if (rulesBySelector_.size() >= MAX_RULES) {
|
||||
LOG_DBG("CSS", "Reached max rules limit (%zu), stopping CSS parsing", MAX_RULES);
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
class CssParser {
|
||||
public:
|
||||
// Bump when CSS cache format or rules change; section caches are invalidated when this changes
|
||||
static constexpr uint8_t CSS_CACHE_VERSION = 7;
|
||||
static constexpr uint8_t CSS_CACHE_VERSION = 8;
|
||||
|
||||
explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {}
|
||||
~CssParser() = default;
|
||||
|
||||
@@ -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 "";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
_language_name: "العربية"
|
||||
_language_code: "AR"
|
||||
_order: "28"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "جاري بدء التشغيل"
|
||||
STR_SLEEPING: "وضع السكون"
|
||||
STR_ENTERING_SLEEP: "جاري الدخول في وضع السكون"
|
||||
STR_BROWSE_FILES: "تصفح الملفات"
|
||||
STR_FILE_TRANSFER: "نقل الملفات"
|
||||
STR_SETTINGS_TITLE: "الإعدادات"
|
||||
STR_CONTINUE_READING: "متابعة القراءة"
|
||||
STR_NO_OPEN_BOOK: "لا يوجد كتاب مفتوح"
|
||||
STR_START_READING: "ابدأ القراءة أدناه"
|
||||
STR_NO_FILES_FOUND: "لم يتم العثور على ملفات"
|
||||
STR_SELECT_CHAPTER: "اختر الفصل"
|
||||
STR_NO_CHAPTERS: "لا توجد فصول"
|
||||
STR_END_OF_BOOK: "نهاية الكتاب"
|
||||
STR_EMPTY_CHAPTER: "فصل فارغ"
|
||||
STR_INDEXING: "جاري الفهرسة"
|
||||
STR_INDEX_FAILED: "فشلت الفهرسة - كتاب غير صالح"
|
||||
STR_MEMORY_ERROR: "خطأ في الذاكرة"
|
||||
STR_PAGE_LOAD_ERROR: "خطأ في تحميل الصفحة"
|
||||
STR_EMPTY_FILE: "ملف فارغ"
|
||||
STR_OUT_OF_BOUNDS: "خارج النطاق"
|
||||
STR_LOADING: "جاري التحميل..."
|
||||
STR_LOADING_POPUP: "جاري التحميل"
|
||||
STR_WIFI_NETWORKS: "شبكات Wi-Fi"
|
||||
STR_NO_NETWORKS: "لم يتم العثور على شبكات"
|
||||
STR_NETWORKS_FOUND: "تم العثور على %zu شبكة"
|
||||
STR_SCANNING: "جاري البحث..."
|
||||
STR_FINDING_SAVED_WIFI: "جاري البحث عن شبكة Wi-Fi محفوظة..."
|
||||
STR_CONNECTING: "جاري الاتصال..."
|
||||
STR_CONNECTING_SAVED_WIFI: "جاري الاتصال بشبكة Wi-Fi محفوظة..."
|
||||
STR_SHOW_NETWORKS: "عرض"
|
||||
STR_CONNECTED: "تم الاتصال!"
|
||||
STR_CONNECTION_FAILED: "فشل الاتصال"
|
||||
STR_FORGET_NETWORK: "نسيان هذه الشبكة؟"
|
||||
STR_SAVE_PASSWORD: "حفظ كلمة المرور للمرة القادمة؟"
|
||||
STR_PRESS_OK_SCAN: "اضغط موافق لإعادة البحث"
|
||||
STR_JOIN_NETWORK: "الانضمام إلى شبكة"
|
||||
STR_CREATE_HOTSPOT: "إنشاء نقطة اتصال"
|
||||
STR_JOIN_DESC: "الاتصال بشبكة Wi-Fi موجودة"
|
||||
STR_HOTSPOT_DESC: "إنشاء شبكة Wi-Fi يمكن للآخرين الانضمام إليها"
|
||||
STR_STARTING_HOTSPOT: "جاري تشغيل نقطة الاتصال..."
|
||||
STR_HOTSPOT_MODE: "وضع نقطة الاتصال"
|
||||
STR_CONNECT_WIFI_HINT: "قم بتوصيل جهازك بشبكة Wi-Fi هذه"
|
||||
STR_OPEN_URL_HINT: "افتح هذا العنوان في المتصفح"
|
||||
STR_OR_HTTP_PREFIX: "أو http://"
|
||||
STR_SCAN_QR_HINT: "أو امسح رمز QR بهاتفك:"
|
||||
STR_CALIBRE_WIRELESS: "اتصال Calibre اللاسلكي"
|
||||
STR_NETWORK_LEGEND: "* = مشفرة | + = محفوظة"
|
||||
STR_MAC_ADDRESS: "عنوان MAC:"
|
||||
STR_CHECKING_WIFI: "جاري فحص Wi-Fi..."
|
||||
STR_ENTER_WIFI_PASSWORD: "أدخل كلمة مرور الشبكة"
|
||||
STR_ADD_HIDDEN_NETWORK: "إضافة شبكة مخفية..."
|
||||
STR_ENTER_WIFI_SSID: "أدخل اسم الشبكة (SSID)"
|
||||
STR_TO_PREFIX: "إلى "
|
||||
STR_CALIBRE_RECEIVING: "جاري الاستلام: "
|
||||
STR_CALIBRE_RECEIVED: "تم الاستلام: "
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) قم بتثبيت إضافة CrossPoint Reader"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) اتصل بنفس شبكة Wi-Fi"
|
||||
STR_CALIBRE_INSTRUCTION_3: "3) في Calibre: اختر \"إرسال إلى الجهاز\""
|
||||
STR_CALIBRE_INSTRUCTION_4: "اترك هذه الشاشة مفتوحة أثناء الإرسال"
|
||||
STR_CAT_DISPLAY: "الشاشة"
|
||||
STR_CAT_READER: "القراءة"
|
||||
STR_CAT_CONTROLS: "الأزرار"
|
||||
STR_CAT_SYSTEM: "النظام"
|
||||
STR_SLEEP_SCREEN: "شاشة السكون"
|
||||
STR_QUICK_RESUME_TIMEOUT: "استئناف سريع بعد المهلة"
|
||||
STR_SLEEP_COVER_MODE: "عرض الغلاف في وضع السكون"
|
||||
STR_HIDE_BATTERY: "إخفاء نسبة البطارية"
|
||||
STR_EXTRA_SPACING: "تباعد إضافي بين الفقرات"
|
||||
STR_TEXT_AA: "تنعيم حواف النص"
|
||||
STR_IMAGES: "الصور"
|
||||
STR_IMAGES_DISPLAY: "عرض"
|
||||
STR_IMAGES_PLACEHOLDER: "عنصر نائب"
|
||||
STR_IMAGES_SUPPRESS: "إخفاء"
|
||||
STR_EOB_HOME: "الرئيسية"
|
||||
STR_EOB_CONTINUE_WITH: "المتابعة إلى"
|
||||
STR_SHORT_PWR_BTN: "ضغطة قصيرة على زر التشغيل"
|
||||
STR_ORIENTATION: "اتجاه القراءة"
|
||||
STR_SIDE_BTN_LAYOUT: "تخطيط الأزرار الجانبية (أثناء القراءة)"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "تدوير الأزرار الأمامية مع الشاشة"
|
||||
STR_LONG_PRESS_BEHAVIOR: "سلوك الضغطة الطويلة"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "إيقاف"
|
||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "تخطي الفصل"
|
||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "تغيير الاتجاه"
|
||||
STR_LONG_PRESS_MENU: "قائمة الضغطة الطويلة"
|
||||
STR_FONT_PREVIEW_TEXT: "نص حكيم له سر قاطع وذو شأن عظيم مكتوب على ثوب أخضر ومغلف بجلد أزرق"
|
||||
STR_FONT_FAMILY: "خط القراءة"
|
||||
STR_FONT_SIZE: "حجم الخط"
|
||||
STR_LINE_SPACING: "تباعد الأسطر"
|
||||
STR_SCREEN_MARGIN: "هوامش شاشة القراءة"
|
||||
STR_PARA_ALIGNMENT: "محاذاة الفقرات"
|
||||
STR_HYPHENATION: "تقسيم الكلمات"
|
||||
STR_TIME_TO_SLEEP: "مهلة الدخول في السكون"
|
||||
STR_SHOW_HIDDEN_FILES: "عرض الملفات المخفية"
|
||||
STR_REMOVE_READ_FROM_RECENTS: "إزالة الكتب المقروءة من قائمة الكتب الأخيرة"
|
||||
STR_MOVE_FINISHED_TO_READ: "نقل الكتب المنتهية إلى مجلد المقروءة"
|
||||
STR_REFRESH_FREQ: "معدل تحديث الشاشة"
|
||||
STR_KOREADER_SYNC: "مزامنة KOReader"
|
||||
STR_CHECK_UPDATES: "التحقق من التحديثات"
|
||||
STR_LANGUAGE: "اللغة"
|
||||
STR_CLEAR_READING_CACHE: "مسح ذاكرة القراءة المؤقتة"
|
||||
STR_USERNAME: "اسم المستخدم"
|
||||
STR_PASSWORD: "كلمة المرور"
|
||||
STR_SYNC_SERVER_URL: "عنوان خادم المزامنة"
|
||||
STR_DOCUMENT_MATCHING: "مطابقة المستندات"
|
||||
STR_SEND_METADATA: "إرسال البيانات الوصفية للمستند"
|
||||
STR_AUTHENTICATE: "تسجيل الدخول"
|
||||
STR_KOREADER_USERNAME: "اسم مستخدم KOReader"
|
||||
STR_KOREADER_PASSWORD: "كلمة مرور KOReader"
|
||||
STR_FILENAME: "اسم الملف"
|
||||
STR_BINARY: "ثنائي"
|
||||
STR_SET_CREDENTIALS_FIRST: "أدخل بيانات الاعتماد أولا"
|
||||
STR_WIFI_CONN_FAILED: "فشل الاتصال بالشبكة"
|
||||
STR_AUTHENTICATING: "جاري التحقق..."
|
||||
STR_AUTH_SUCCESS: "تم تسجيل الدخول بنجاح!"
|
||||
STR_KOREADER_AUTH: "تسجيل الدخول إلى KOReader"
|
||||
STR_SYNC_READY: "مزامنة KOReader جاهزة للاستخدام"
|
||||
STR_AUTH_FAILED: "فشل تسجيل الدخول"
|
||||
STR_DONE: "تم"
|
||||
STR_CLEAR_CACHE_WARNING_1: "سيؤدي هذا إلى مسح جميع بيانات الكتب المخزنة."
|
||||
STR_CLEAR_CACHE_WARNING_2: "سيتم فقدان كل تقدم القراءة!"
|
||||
STR_CLEAR_CACHE_WARNING_3: "ستحتاج الكتب إلى إعادة فهرسة"
|
||||
STR_CLEAR_CACHE_WARNING_4: "عند فتحها مرة أخرى."
|
||||
STR_CLEARING_CACHE: "جاري مسح الذاكرة المؤقتة..."
|
||||
STR_CACHE_CLEARED: "تم مسح الذاكرة المؤقتة"
|
||||
STR_ITEMS_REMOVED: "عناصر تمت إزالتها"
|
||||
STR_FAILED_LOWER: "فشل"
|
||||
STR_CLEAR_CACHE_FAILED: "فشل مسح الذاكرة المؤقتة"
|
||||
STR_CHECK_SERIAL_OUTPUT: "راجع مخرجات المنفذ التسلسلي للتفاصيل"
|
||||
STR_DARK: "داكن"
|
||||
STR_LIGHT: "فاتح"
|
||||
STR_CUSTOM: "مخصص"
|
||||
STR_COVER: "الغلاف"
|
||||
STR_NONE_OPT: "بدون"
|
||||
STR_FIT: "ملاءمة"
|
||||
STR_CROP: "قص"
|
||||
STR_NEVER: "أبدا"
|
||||
STR_IN_READER: "أثناء القراءة"
|
||||
STR_ALWAYS: "دائما"
|
||||
STR_IGNORE: "تجاهل"
|
||||
STR_SLEEP: "سكون"
|
||||
STR_PAGE_TURN: "تقليب الصفحة"
|
||||
STR_FORCE_REFRESH: "تحديث الشاشة"
|
||||
STR_PORTRAIT: "عمودي"
|
||||
STR_LANDSCAPE_CW: "أفقي (يمين)"
|
||||
STR_INVERTED: "عكس الألوان"
|
||||
STR_ORIENTATION_INVERTED: "عمودي 180°"
|
||||
STR_LANDSCAPE_CCW: "أفقي (يسار)"
|
||||
STR_PREV_NEXT: "السابق/التالي"
|
||||
STR_NEXT_PREV: "التالي/السابق"
|
||||
STR_KOSYNC: "KOSync"
|
||||
STR_BOOKMARK_OPTION: "إشارة مرجعية"
|
||||
STR_DISABLED: "معطل"
|
||||
STR_NOTO_SERIF: "Noto Serif"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_SMALL: "صغير"
|
||||
STR_MEDIUM: "متوسط"
|
||||
STR_LARGE: "كبير"
|
||||
STR_X_LARGE: "كبير جدا"
|
||||
STR_TIGHT: "ضيق"
|
||||
STR_NORMAL: "عادي"
|
||||
STR_WIDE: "واسع"
|
||||
STR_JUSTIFY: "ضبط الطرفين"
|
||||
STR_ALIGN_LEFT: "يسار"
|
||||
STR_CENTER: "وسط"
|
||||
STR_ALIGN_RIGHT: "يمين"
|
||||
STR_PAGES_1: "صفحة واحدة"
|
||||
STR_PAGES_5: "5 صفحات"
|
||||
STR_PAGES_10: "10 صفحات"
|
||||
STR_PAGES_15: "15 صفحة"
|
||||
STR_PAGES_30: "30 صفحة"
|
||||
STR_UPDATE: "تحديث"
|
||||
STR_CHECKING_UPDATE: "جاري التحقق من التحديثات..."
|
||||
STR_NEW_UPDATE: "يتوفر تحديث جديد!"
|
||||
STR_CURRENT_VERSION: "الإصدار الحالي: "
|
||||
STR_NEW_VERSION: "الإصدار الجديد: "
|
||||
STR_UPDATING: "جاري التحديث..."
|
||||
STR_NO_UPDATE: "لا يتوفر تحديث"
|
||||
STR_UPDATE_FAILED: "فشل التحديث"
|
||||
STR_UPDATE_COMPLETE: "اكتمل التحديث"
|
||||
STR_POWER_ON_HINT: "اضغط مطولا على زر التشغيل لتشغيل الجهاز من جديد"
|
||||
STR_RESTARTING_HINT: "جاري إعادة التشغيل... إذا لم يعمل الجهاز، اضغط مطولا على زر التشغيل لعدة ثوان."
|
||||
STR_NO_ENTRIES: "لم يتم العثور على عناصر"
|
||||
STR_DOWNLOADING: "جاري التنزيل..."
|
||||
STR_DOWNLOAD_FAILED: "فشل التنزيل"
|
||||
STR_ERROR_MSG: "خطأ:"
|
||||
STR_UNNAMED: "بدون اسم"
|
||||
STR_HOLD_OPEN_TO_DELETE: "اضغط مطولا على فتح للحذف"
|
||||
STR_NO_SERVER_URL: "لم يتم تحديد عنوان الخادم"
|
||||
STR_FETCH_FEED_FAILED: "فشل جلب القائمة"
|
||||
STR_PARSE_FEED_FAILED: "فشل تحليل القائمة"
|
||||
STR_NEXT_PAGE: "الصفحة التالية"
|
||||
STR_PREV_PAGE: "الصفحة السابقة"
|
||||
STR_NETWORK_PREFIX: "الشبكة: "
|
||||
STR_IP_ADDRESS_PREFIX: "عنوان IP: "
|
||||
STR_ERROR_GENERAL_FAILURE: "خطأ: فشل عام"
|
||||
STR_ERROR_NETWORK_NOT_FOUND: "خطأ: الشبكة غير موجودة"
|
||||
STR_ERROR_CONNECTION_TIMEOUT: "خطأ: انتهت مهلة الاتصال"
|
||||
STR_SD_CARD: "بطاقة SD"
|
||||
STR_BACK: "رجوع »"
|
||||
STR_EXIT: "خروج »"
|
||||
STR_HOME: "الرئيسية »"
|
||||
STR_SELECT: "اختيار"
|
||||
STR_SELECTED: "محدد"
|
||||
STR_TOGGLE: "تبديل"
|
||||
STR_TOGGLE_BOOKMARK: "إضافة/إزالة إشارة مرجعية"
|
||||
STR_CONFIRM: "تأكيد"
|
||||
STR_CANCEL: "إلغاء"
|
||||
STR_CONNECT: "اتصال"
|
||||
STR_OPEN: "فتح"
|
||||
STR_DOWNLOAD: "تنزيل"
|
||||
STR_RETRY: "إعادة المحاولة"
|
||||
STR_YES: "نعم"
|
||||
STR_NO: "لا"
|
||||
STR_SHOW: "عرض"
|
||||
STR_HIDE: "إخفاء"
|
||||
STR_STATE_ON: "تشغيل"
|
||||
STR_STATE_OFF: "إيقاف"
|
||||
STR_NOT_SET: "غير محدد"
|
||||
STR_DIR_LEFT: "يسار"
|
||||
STR_DIR_RIGHT: "يمين"
|
||||
STR_DIR_UP: "أعلى"
|
||||
STR_DIR_DOWN: "أسفل"
|
||||
STR_OK_BUTTON: "موافق"
|
||||
STR_SLEEP_COVER_FILTER: "مرشح غلاف شاشة السكون"
|
||||
STR_FILTER_CONTRAST: "التباين"
|
||||
STR_CUSTOMISE_STATUS_BAR: "تخصيص شريط الحالة"
|
||||
STR_CHAPTER_PAGE_COUNT: "عدد صفحات الفصل"
|
||||
STR_BOOK_PROGRESS_PERCENTAGE: "نسبة التقدم في الكتاب"
|
||||
STR_PROGRESS_BAR: "شريط التقدم"
|
||||
STR_PROGRESS_BAR_THICKNESS: "سمك شريط التقدم"
|
||||
STR_PROGRESS_BAR_THIN: "رفيع"
|
||||
STR_PROGRESS_BAR_MEDIUM: "متوسط"
|
||||
STR_PROGRESS_BAR_THICK: "سميك"
|
||||
STR_BOOK: "الكتاب"
|
||||
STR_CHAPTER: "الفصل"
|
||||
STR_EXAMPLE_CHAPTER: "الفصل 21"
|
||||
STR_EXAMPLE_BOOK: "عنوان الكتاب"
|
||||
STR_PREVIEW: "معاينة"
|
||||
STR_TITLE: "العنوان"
|
||||
STR_BATTERY: "البطارية"
|
||||
STR_XTC_STATUS_BAR: "شريط حالة XTC"
|
||||
STR_BOTTOM: "أسفل"
|
||||
STR_TOP: "أعلى"
|
||||
STR_CLOCK: "الساعة"
|
||||
STR_CLOCK_UTC_OFFSET: "فرق التوقيت عن UTC"
|
||||
STR_CLOCK_FORMAT: "تنسيق الساعة"
|
||||
STR_CLOCK_FORMAT_24H: "24 ساعة"
|
||||
STR_CLOCK_FORMAT_12H: "12 ساعة"
|
||||
STR_CURRENT_TIME: "الوقت الحالي:"
|
||||
STR_NEXT_FIELD: "التالي"
|
||||
STR_CLOCK_SYNC: "مزامنة الساعة"
|
||||
STR_CLOCK_SYNC_NOW: "مزامنة الساعة الآن"
|
||||
STR_CLOCK_SYNCING: "جاري المزامنة عبر NTP..."
|
||||
STR_CLOCK_SYNC_OK: "تمت مزامنة الساعة"
|
||||
STR_CLOCK_SYNC_FAIL: "فشلت المزامنة"
|
||||
STR_CLOCK_SYNC_NO_WIFI: "لا يوجد اتصال Wi-Fi"
|
||||
STR_CLOCK_SYNC_NO_WIFI_HINT: "اتصل بشبكة Wi-Fi أولا، ثم حاول مرة أخرى."
|
||||
STR_CLOCK_SYNCED: "تمت مزامنة الساعة"
|
||||
STR_UI_THEME: "مظهر الواجهة"
|
||||
STR_THEME_CLASSIC: "كلاسيكي"
|
||||
STR_THEME_LYRA: "Lyra"
|
||||
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||
STR_THEME_LYRA_EXTENDED: "Lyra موسع"
|
||||
STR_SUNLIGHT_FADING_FIX: "إصلاح بهتان الشاشة في الشمس"
|
||||
STR_REMAP_FRONT_BUTTONS: "تغيير وظائف الأزرار الأمامية"
|
||||
STR_BOOKMARKS: "الإشارات المرجعية"
|
||||
STR_BOOKMARK_ADDED: "تمت إضافة الإشارة المرجعية."
|
||||
STR_BOOKMARK_REMOVED: "تمت إزالة الإشارة المرجعية."
|
||||
STR_OPDS_BROWSER: "متصفح OPDS"
|
||||
STR_SEARCH: "بحث"
|
||||
STR_COVER_CUSTOM: "الغلاف + مخصص"
|
||||
STR_QUICK_RESUME: "استئناف سريع"
|
||||
STR_MENU_RECENT_BOOKS: "الكتب الأخيرة"
|
||||
STR_REMOVE_FROM_RECENTS: "إزالة من الكتب الأخيرة؟"
|
||||
STR_NO_RECENT_BOOKS: "لا توجد كتب أخيرة"
|
||||
STR_CALIBRE_DESC: "استخدم النقل اللاسلكي من Calibre"
|
||||
STR_FORGET_AND_REMOVE: "نسيان الشبكة وحذف كلمة المرور المحفوظة؟"
|
||||
STR_FORGET_BUTTON: "نسيان"
|
||||
STR_CALIBRE_STARTING: "جاري تشغيل Calibre..."
|
||||
STR_CALIBRE_SETUP: "الإعداد"
|
||||
STR_CALIBRE_STATUS: "الحالة"
|
||||
STR_CLEAR_BUTTON: "مسح"
|
||||
STR_DEFAULT_VALUE: "افتراضي"
|
||||
STR_REMAP_PROMPT: "اضغط زرا أماميا لكل وظيفة"
|
||||
STR_UNASSIGNED: "غير معين"
|
||||
STR_ALREADY_ASSIGNED: "معين مسبقا"
|
||||
STR_REMAP_RESET_HINT: "الزر الجانبي العلوي: استعادة التخطيط الافتراضي"
|
||||
STR_REMAP_CANCEL_HINT: "الزر الجانبي السفلي: إلغاء التغيير"
|
||||
STR_HW_BACK_LABEL: "رجوع (الزر 1)"
|
||||
STR_HW_CONFIRM_LABEL: "تأكيد (الزر 2)"
|
||||
STR_HW_LEFT_LABEL: "يسار (الزر 3)"
|
||||
STR_HW_RIGHT_LABEL: "يمين (الزر 4)"
|
||||
STR_GO_TO_PERCENT: "الانتقال إلى نسبة %"
|
||||
STR_GO_HOME_BUTTON: "العودة إلى الرئيسية"
|
||||
STR_SYNC_PROGRESS: "مزامنة التقدم"
|
||||
STR_DELETE_CACHE: "حذف ذاكرة الكتاب المؤقتة"
|
||||
STR_DELETE: "حذف"
|
||||
STR_CONFIRM_DELETE_BOOKMARK: "حذف هذه الإشارة المرجعية؟"
|
||||
STR_DISPLAY_QR: "عرض الصفحة كرمز QR"
|
||||
STR_CHAPTER_PREFIX: "الفصل: "
|
||||
STR_PAGES_SEPARATOR: " صفحات | "
|
||||
STR_BOOK_PREFIX: "الكتاب: "
|
||||
STR_CALIBRE_URL_HINT: "في Calibre، أضف /opds إلى العنوان"
|
||||
STR_SYNCING_TIME: "جاري مزامنة الوقت..."
|
||||
STR_CALC_HASH: "جاري حساب بصمة المستند..."
|
||||
STR_HASH_FAILED: "فشل حساب بصمة المستند"
|
||||
STR_FETCH_PROGRESS: "جاري جلب التقدم من الخادم..."
|
||||
STR_UPLOAD_PROGRESS: "جاري رفع التقدم..."
|
||||
STR_NO_CREDENTIALS_MSG: "لم يتم إعداد بيانات الاعتماد"
|
||||
STR_KOREADER_SETUP_HINT: "قم بإعداد حساب KOReader في الإعدادات"
|
||||
STR_PROGRESS_FOUND: "تم العثور على تقدم سابق!"
|
||||
STR_REMOTE_LABEL: "الخادم:"
|
||||
STR_LOCAL_LABEL: "الجهاز:"
|
||||
STR_PAGE_OVERALL_FORMAT: "صفحة %d، %.2f%% إجمالا"
|
||||
STR_PAGE_TOTAL_OVERALL_FORMAT: "صفحة %d من %d، %.2f%% إجمالا"
|
||||
STR_DEVICE_FROM_FORMAT: " من: %s"
|
||||
STR_APPLY_REMOTE: "استخدام تقدم الخادم"
|
||||
STR_UPLOAD_LOCAL: "رفع التقدم المحلي"
|
||||
STR_NO_REMOTE_MSG: "لا يوجد تقدم على الخادم"
|
||||
STR_UPLOAD_PROMPT: "رفع الموضع الحالي؟"
|
||||
STR_UPLOAD_SUCCESS: "تم رفع التقدم!"
|
||||
STR_SYNC_FAILED_MSG: "فشلت المزامنة"
|
||||
STR_SAVE_PROGRESS_FAILED: "تعذر حفظ التقدم"
|
||||
STR_SECTION_PREFIX: "القسم "
|
||||
STR_UPLOAD: "رفع"
|
||||
STR_BOOK_S_STYLE: "تنسيق الكتاب الأصلي"
|
||||
STR_EMBEDDED_STYLE: "التنسيق المضمن"
|
||||
STR_FOCUS_READING: "قراءة مركزة"
|
||||
STR_OPDS_SERVER_URL: "عنوان خادم OPDS"
|
||||
STR_PWR_BTN_FOOTNOTE_BACK: "عودة سريعة من الحواشي"
|
||||
STR_SET_SLEEP_COVER: "تعيين الغلاف"
|
||||
STR_FOOTNOTES: "الحواشي"
|
||||
STR_NO_FOOTNOTES: "لا توجد حواش في هذه الصفحة"
|
||||
STR_LINK: "[رابط]"
|
||||
STR_SCREENSHOT_BUTTON: "التقاط لقطة شاشة"
|
||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u دقيقة"
|
||||
STR_SLEEP_NEVER: "أبدا"
|
||||
STR_STEP_HINT_FRONT: "الأزرار الأمامية:"
|
||||
STR_STEP_HINT_SIDE: "الأزرار الجانبية:"
|
||||
STR_ADD_SERVER: "إضافة خادم"
|
||||
STR_SERVER_NAME: "اسم الخادم"
|
||||
STR_NO_SERVERS: "لم يتم إعداد خوادم OPDS"
|
||||
STR_DELETE_SERVER: "حذف الخادم"
|
||||
STR_OPDS_SERVERS: "خوادم OPDS"
|
||||
STR_AUTO_TURN_ENABLED: "التقليب التلقائي مفعل: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "التقليب التلقائي (صفحات في الدقيقة)"
|
||||
STR_MANAGE_FONTS: "إدارة الخطوط"
|
||||
STR_FONT_BROWSER: "متصفح الخطوط"
|
||||
STR_LOADING_FONT_LIST: "جاري تحميل قائمة الخطوط..."
|
||||
STR_NO_FONTS_AVAILABLE: "لا تتوفر خطوط"
|
||||
STR_FONT_INSTALLED: "تم تثبيت الخط!"
|
||||
STR_FONT_INSTALL_FAILED: "فشل تثبيت الخط"
|
||||
STR_INSTALLED: "مثبت"
|
||||
STR_DOWNLOAD_ALL: "تنزيل الكل"
|
||||
STR_UPDATE_ALL: "تحديث الكل"
|
||||
STR_UPDATE_AVAILABLE: "تحديث"
|
||||
STR_CRASH_TITLE: "تعطل النظام"
|
||||
STR_CRASH_DESCRIPTION: "تم حفظ تقرير مفصل في الملف crash_report.txt. يرجى إرفاق هذا الملف عند الإبلاغ عن المشكلة."
|
||||
STR_CRASH_REASON: "سبب التعطل:"
|
||||
STR_CRASH_NO_REASON: "(لم يتم تسجيل سبب)"
|
||||
STR_TILT_PAGE_TURN: "تقليب الصفحة بالإمالة"
|
||||
STR_KB_HINT_MOVE_CURSOR: "اضغط زر اليمين أو اليسار لتحريك المؤشر"
|
||||
STR_KB_HINT_RETURN_CURSOR: "اضغط زر اليسار للعودة إلى موضع المؤشر"
|
||||
STR_KB_HINT_HIDE_PASSWORD: "اضغط مطولا على زر اليمين ثم اضغط [***] لإخفاء كلمة المرور"
|
||||
STR_KB_HINT_SHOW_PASSWORD: "اضغط مطولا على زر اليمين ثم اضغط [abc] لإظهار كلمة المرور"
|
||||
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "اضغط [***] لإخفاء كلمة المرور"
|
||||
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "اضغط [abc] لإظهار كلمة المرور"
|
||||
STR_KB_HINT_EDIT_ENTRY: "اضغط مطولا على زر الأعلى للتعديل"
|
||||
STR_KB_TIPS: "نصائح:"
|
||||
STR_KB_HINT_RETURN_KEYBOARD: "اضغط زر الأسفل للعودة إلى لوحة المفاتيح"
|
||||
STR_KB_HINT_EXIT_URL_MODE: "اضغط ABC للخروج من وضع URL"
|
||||
STR_KB_HINT_CLEAR_TEXT: "اضغط مطولا على DEL لمسح كل النص"
|
||||
STR_KB_HINT_SECONDARY_CHAR: "اضغط مطولا على زر الاختيار للحرف الثانوي"
|
||||
STR_KB_HINT_UPPER_SECONDARY: "اضغط مطولا على زر الاختيار للحرف الكبير أو الثانوي"
|
||||
STR_KB_HINT_LOWER_SECONDARY: "اضغط مطولا على زر الاختيار للحرف الصغير أو الثانوي"
|
||||
STR_KB_HINT_URL_SNIPPETS: "اضغط URL للاختصارات"
|
||||
STR_SD_FIRMWARE_UPDATE: "تحديث البرنامج الثابت من بطاقة SD"
|
||||
STR_SELECT_FIRMWARE_FILE: "اختر ملف البرنامج الثابت (.bin)"
|
||||
STR_NO_BIN_FILES: "لم يتم العثور على ملفات .bin"
|
||||
STR_VALIDATING_FIRMWARE: "جاري التحقق من البرنامج الثابت..."
|
||||
STR_INVALID_FIRMWARE: "ملف برنامج ثابت غير صالح"
|
||||
STR_FIRMWARE_TOO_LARGE: "البرنامج الثابت أكبر من القسم المخصص له"
|
||||
STR_FIRMWARE_TOO_SMALL: "ملف البرنامج الثابت صغير جدا"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "تحديث البرنامج الثابت؟"
|
||||
STR_FIRMWARE_FILE_OPEN_FAILED: "تعذر فتح الملف"
|
||||
STR_FIRMWARE_WRITE_FAILED: "فشلت كتابة البرنامج الثابت"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "لا تطفئ الجهاز!"
|
||||
STR_RECOVERY_MODE: "وضع الاستعادة"
|
||||
STR_RECOVERY_MODE_HINT: "ضع الملف firmware.bin في المجلد الرئيسي لبطاقة SD ثم اختره"
|
||||
@@ -0,0 +1,385 @@
|
||||
_language_name: "Norsk bokmål"
|
||||
_language_code: "NB"
|
||||
_order: "26"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "STARTER"
|
||||
STR_SLEEPING: "HVILER"
|
||||
STR_ENTERING_SLEEP: "Går i hvilemodus"
|
||||
STR_BROWSE_FILES: "Bla i filer"
|
||||
STR_FILE_TRANSFER: "Filoverføring"
|
||||
STR_SETTINGS_TITLE: "Innstillinger"
|
||||
STR_CONTINUE_READING: "Fortsett å lese"
|
||||
STR_NO_OPEN_BOOK: "Ingen åpen bok"
|
||||
STR_START_READING: "Start å lese nedenfor"
|
||||
STR_NO_FILES_FOUND: "Ingen filer funnet"
|
||||
STR_SELECT_CHAPTER: "Velg kapittel"
|
||||
STR_NO_CHAPTERS: "Ingen kapitler"
|
||||
STR_END_OF_BOOK: "Slutten av boken"
|
||||
STR_EMPTY_CHAPTER: "Tomt kapittel"
|
||||
STR_INDEXING: "Indekserer"
|
||||
STR_MEMORY_ERROR: "Minnefeil"
|
||||
STR_PAGE_LOAD_ERROR: "Feil ved sidelasting"
|
||||
STR_EMPTY_FILE: "Tom fil"
|
||||
STR_OUT_OF_BOUNDS: "Utenfor område"
|
||||
STR_LOADING: "Laster..."
|
||||
STR_LOADING_POPUP: "Laster"
|
||||
STR_WIFI_NETWORKS: "WiFi-nettverk"
|
||||
STR_NO_NETWORKS: "Ingen nettverk funnet"
|
||||
STR_NETWORKS_FOUND: "%zu nettverk funnet"
|
||||
STR_SCANNING: "Skanner..."
|
||||
STR_CONNECTING: "Kobler til..."
|
||||
STR_CONNECTED: "Tilkoblet!"
|
||||
STR_CONNECTION_FAILED: "Tilkobling mislyktes"
|
||||
STR_FORGET_NETWORK: "Glem nettverk?"
|
||||
STR_SAVE_PASSWORD: "Lagre passord til neste gang?"
|
||||
STR_PRESS_OK_SCAN: "Trykk OK for å skanne på nytt"
|
||||
STR_JOIN_NETWORK: "Koble til et nettverk"
|
||||
STR_CREATE_HOTSPOT: "Opprett hotspot"
|
||||
STR_JOIN_DESC: "Koble til et eksisterende WiFi-nettverk"
|
||||
STR_HOTSPOT_DESC: "Opprett et WiFi-nettverk andre kan koble seg til"
|
||||
STR_STARTING_HOTSPOT: "Starter hotspot..."
|
||||
STR_HOTSPOT_MODE: "Hotspot-modus"
|
||||
STR_CONNECT_WIFI_HINT: "Koble enheten din til dette WiFi-nettverket"
|
||||
STR_OPEN_URL_HINT: "Åpne denne URL-en i nettleseren din"
|
||||
STR_OR_HTTP_PREFIX: "eller http://"
|
||||
STR_SCAN_QR_HINT: "eller skann QR-kode med telefonen din:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||
STR_NETWORK_LEGEND: "* = Kryptert | + = Lagret"
|
||||
STR_MAC_ADDRESS: "MAC-adresse:"
|
||||
STR_CHECKING_WIFI: "Sjekker WiFi..."
|
||||
STR_ENTER_WIFI_PASSWORD: "Skriv inn WiFi-passord"
|
||||
STR_TO_PREFIX: "til "
|
||||
STR_CALIBRE_RECEIVING: "Mottar: "
|
||||
STR_CALIBRE_RECEIVED: "Mottatt: "
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Installer CrossPoint Reader-plugin"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) Vær på samme WiFi-nettverk"
|
||||
STR_CALIBRE_INSTRUCTION_3: "3) I Calibre: \"Send til enhet\""
|
||||
STR_CALIBRE_INSTRUCTION_4: "\"Hold denne skjermen åpen mens du sender\""
|
||||
STR_CAT_DISPLAY: "Skjerm"
|
||||
STR_CAT_READER: "Leser"
|
||||
STR_CAT_CONTROLS: "Betjening"
|
||||
STR_CAT_SYSTEM: "System"
|
||||
STR_SLEEP_SCREEN: "Hvileskjerm"
|
||||
STR_QUICK_RESUME_TIMEOUT: "Hurtig gjenopptak ved tidsavbrudd"
|
||||
STR_SLEEP_COVER_MODE: "Omslagsmodus for hvileskjerm"
|
||||
STR_HIDE_BATTERY: "Skjul batteri %"
|
||||
STR_EXTRA_SPACING: "Ekstra avsnittsavstand"
|
||||
STR_TEXT_AA: "Tekstutjevning"
|
||||
STR_IMAGES: "Bilder"
|
||||
STR_IMAGES_DISPLAY: "Vis"
|
||||
STR_IMAGES_PLACEHOLDER: "Plassholder"
|
||||
STR_IMAGES_SUPPRESS: "Skjul"
|
||||
STR_SHORT_PWR_BTN: "Kort trykk på av/på-knapp"
|
||||
STR_ORIENTATION: "Leseretning"
|
||||
STR_SIDE_BTN_LAYOUT: "Sideknapp-oppsett (leser)"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orienter frontknapper"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Atferd ved langt trykk"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "AV"
|
||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Kapittelhopp"
|
||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Endre orientering"
|
||||
STR_LONG_PRESS_MENU: "Langt trykk på Meny"
|
||||
STR_FONT_PREVIEW_TEXT: "Høvdingens kjære squaw får litt pizza i Mexico by"
|
||||
STR_FONT_FAMILY: "Skrifttype i leser"
|
||||
STR_FONT_SIZE: "Skriftstørrelse i leser"
|
||||
STR_LINE_SPACING: "Linjeavstand i leser"
|
||||
STR_SCREEN_MARGIN: "Skjermmarg i leser"
|
||||
STR_PARA_ALIGNMENT: "Avsnittsjustering i leser"
|
||||
STR_HYPHENATION: "Orddeling"
|
||||
STR_TIME_TO_SLEEP: "Tid før hvile"
|
||||
STR_SHOW_HIDDEN_FILES: "Vis skjulte filer"
|
||||
STR_REMOVE_READ_FROM_RECENTS: "Fjern leste bøker fra Nylig-listen"
|
||||
STR_MOVE_FINISHED_TO_READ: "Flytt fullførte bøker til Read-mappen"
|
||||
STR_REFRESH_FREQ: "Oppdateringsfrekvens"
|
||||
STR_KOREADER_SYNC: "KOReader Sync"
|
||||
STR_CHECK_UPDATES: "Se etter oppdateringer"
|
||||
STR_LANGUAGE: "Språk"
|
||||
STR_CLEAR_READING_CACHE: "Tøm lesebuffer"
|
||||
STR_USERNAME: "Brukernavn"
|
||||
STR_PASSWORD: "Passord"
|
||||
STR_SYNC_SERVER_URL: "URL til synk-server"
|
||||
STR_DOCUMENT_MATCHING: "Dokumentgjenkjenning"
|
||||
STR_AUTHENTICATE: "Godkjenn"
|
||||
STR_KOREADER_USERNAME: "KOReader-brukernavn"
|
||||
STR_KOREADER_PASSWORD: "KOReader-passord"
|
||||
STR_FILENAME: "Filnavn"
|
||||
STR_BINARY: "Binær"
|
||||
STR_SET_CREDENTIALS_FIRST: "Angi påloggingsinfo først"
|
||||
STR_WIFI_CONN_FAILED: "WiFi-tilkobling mislyktes"
|
||||
STR_AUTHENTICATING: "Godkjenner..."
|
||||
STR_AUTH_SUCCESS: "Godkjenning vellykket!"
|
||||
STR_KOREADER_AUTH: "KOReader-godkjenning"
|
||||
STR_SYNC_READY: "KOReader-synk er klar til bruk"
|
||||
STR_AUTH_FAILED: "Godkjenning mislyktes"
|
||||
STR_DONE: "Ferdig"
|
||||
STR_CLEAR_CACHE_WARNING_1: "Dette tømmer alle bufrede bokdata."
|
||||
STR_CLEAR_CACHE_WARNING_2: "All lesefremdrift går tapt!"
|
||||
STR_CLEAR_CACHE_WARNING_3: "Bøker må indekseres på nytt"
|
||||
STR_CLEAR_CACHE_WARNING_4: "når de åpnes igjen."
|
||||
STR_CLEARING_CACHE: "Tømmer buffer..."
|
||||
STR_CACHE_CLEARED: "Buffer tømt"
|
||||
STR_ITEMS_REMOVED: "elementer fjernet"
|
||||
STR_FAILED_LOWER: "mislyktes"
|
||||
STR_CLEAR_CACHE_FAILED: "Kunne ikke tømme buffer"
|
||||
STR_CHECK_SERIAL_OUTPUT: "Sjekk seriell utdata for detaljer"
|
||||
STR_DARK: "Mørk"
|
||||
STR_LIGHT: "Lys"
|
||||
STR_CUSTOM: "Egendefinert"
|
||||
STR_COVER: "Omslag"
|
||||
STR_NONE_OPT: "Ingen"
|
||||
STR_FIT: "Tilpass"
|
||||
STR_CROP: "Beskjær"
|
||||
STR_NEVER: "Aldri"
|
||||
STR_IN_READER: "I leseren"
|
||||
STR_ALWAYS: "Alltid"
|
||||
STR_IGNORE: "Ignorer"
|
||||
STR_SLEEP: "Hvile"
|
||||
STR_PAGE_TURN: "Bla om"
|
||||
STR_FORCE_REFRESH: "Oppdater skjerm"
|
||||
STR_PORTRAIT: "Stående"
|
||||
STR_LANDSCAPE_CW: "Liggende med klokken"
|
||||
STR_INVERTED: "Opp ned"
|
||||
STR_ORIENTATION_INVERTED: "Portrett 180°"
|
||||
STR_LANDSCAPE_CCW: "Liggende mot klokken"
|
||||
STR_PREV_NEXT: "Forrige/Neste"
|
||||
STR_NEXT_PREV: "Neste/Forrige"
|
||||
STR_KOSYNC: "KOSync"
|
||||
STR_BOOKMARK_OPTION: "Bokmerke"
|
||||
STR_DISABLED: "Deaktivert"
|
||||
STR_NOTO_SERIF: "Noto Serif"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_SMALL: "Liten"
|
||||
STR_MEDIUM: "Middels"
|
||||
STR_LARGE: "Stor"
|
||||
STR_X_LARGE: "Ekstra stor"
|
||||
STR_TIGHT: "Tett"
|
||||
STR_NORMAL: "Normal"
|
||||
STR_WIDE: "Bred"
|
||||
STR_JUSTIFY: "Blokkjustert"
|
||||
STR_ALIGN_LEFT: "Venstre"
|
||||
STR_CENTER: "Midtstilt"
|
||||
STR_ALIGN_RIGHT: "Høyre"
|
||||
STR_PAGES_1: "1 side"
|
||||
STR_PAGES_5: "5 sider"
|
||||
STR_PAGES_10: "10 sider"
|
||||
STR_PAGES_15: "15 sider"
|
||||
STR_PAGES_30: "30 sider"
|
||||
STR_UPDATE: "Oppdater"
|
||||
STR_CHECKING_UPDATE: "Ser etter oppdatering..."
|
||||
STR_NEW_UPDATE: "Ny oppdatering tilgjengelig!"
|
||||
STR_CURRENT_VERSION: "Nåværende versjon: "
|
||||
STR_NEW_VERSION: "Ny versjon: "
|
||||
STR_UPDATING: "Oppdaterer..."
|
||||
STR_NO_UPDATE: "Ingen oppdatering tilgjengelig"
|
||||
STR_UPDATE_FAILED: "Oppdatering mislyktes"
|
||||
STR_UPDATE_COMPLETE: "Oppdatering fullført"
|
||||
STR_POWER_ON_HINT: "Trykk og hold av/på-knappen for å slå på igjen"
|
||||
STR_RESTARTING_HINT: "Starter på nytt... Hvis enheten ikke starter, hold av/på-knappen i noen sekunder."
|
||||
STR_NO_ENTRIES: "Ingen oppføringer funnet"
|
||||
STR_DOWNLOADING: "Laster ned..."
|
||||
STR_DOWNLOAD_FAILED: "Nedlasting mislyktes"
|
||||
STR_ERROR_MSG: "Feil:"
|
||||
STR_UNNAMED: "Uten navn"
|
||||
STR_HOLD_OPEN_TO_DELETE: "Hold Åpne for å slette"
|
||||
STR_NO_SERVER_URL: "Ingen server-URL konfigurert"
|
||||
STR_FETCH_FEED_FAILED: "Kunne ikke hente feed"
|
||||
STR_PARSE_FEED_FAILED: "Kunne ikke tolke feed"
|
||||
STR_NEXT_PAGE: "Neste side »"
|
||||
STR_PREV_PAGE: "« Forrige side"
|
||||
STR_NETWORK_PREFIX: "Nettverk: "
|
||||
STR_IP_ADDRESS_PREFIX: "IP-adresse: "
|
||||
STR_ERROR_GENERAL_FAILURE: "Feil: Generell feil"
|
||||
STR_ERROR_NETWORK_NOT_FOUND: "Feil: Nettverk ikke funnet"
|
||||
STR_ERROR_CONNECTION_TIMEOUT: "Feil: Tidsavbrudd for tilkobling"
|
||||
STR_SD_CARD: "SD-kort"
|
||||
STR_BACK: "« Tilbake"
|
||||
STR_EXIT: "« Avslutt"
|
||||
STR_HOME: "« Hjem"
|
||||
STR_SELECT: "Velg"
|
||||
STR_SELECTED: "Valgt"
|
||||
STR_TOGGLE: "Veksle"
|
||||
STR_TOGGLE_BOOKMARK: "Veksle bokmerke"
|
||||
STR_CONFIRM: "Bekreft"
|
||||
STR_CANCEL: "Avbryt"
|
||||
STR_CONNECT: "Koble til"
|
||||
STR_OPEN: "Åpne"
|
||||
STR_DOWNLOAD: "Last ned"
|
||||
STR_RETRY: "Prøv igjen"
|
||||
STR_YES: "Ja"
|
||||
STR_NO: "Nei"
|
||||
STR_SHOW: "Vis"
|
||||
STR_HIDE: "Skjul"
|
||||
STR_STATE_ON: "PÅ"
|
||||
STR_STATE_OFF: "AV"
|
||||
STR_NOT_SET: "Ikke angitt"
|
||||
STR_DIR_LEFT: "Venstre"
|
||||
STR_DIR_RIGHT: "Høyre"
|
||||
STR_DIR_UP: "Opp"
|
||||
STR_DIR_DOWN: "Ned"
|
||||
STR_OK_BUTTON: "OK"
|
||||
STR_SLEEP_COVER_FILTER: "Omslagsfilter for hvileskjerm"
|
||||
STR_FILTER_CONTRAST: "Kontrast"
|
||||
STR_CUSTOMISE_STATUS_BAR: "Tilpass statuslinje"
|
||||
STR_CHAPTER_PAGE_COUNT: "Sideantall i kapittel"
|
||||
STR_BOOK_PROGRESS_PERCENTAGE: "Bokfremdrift i prosent"
|
||||
STR_PROGRESS_BAR: "Fremdriftslinje"
|
||||
STR_PROGRESS_BAR_THICKNESS: "Tykkelse på fremdriftslinje"
|
||||
STR_PROGRESS_BAR_THIN: "Tynn"
|
||||
STR_PROGRESS_BAR_MEDIUM: "Middels"
|
||||
STR_PROGRESS_BAR_THICK: "Tykk"
|
||||
STR_BOOK: "Bok"
|
||||
STR_CHAPTER: "Kapittel"
|
||||
STR_EXAMPLE_CHAPTER: "Kapittel 21"
|
||||
STR_EXAMPLE_BOOK: "Boktittel"
|
||||
STR_PREVIEW: "Forhåndsvisning"
|
||||
STR_TITLE: "Tittel"
|
||||
STR_BATTERY: "Batteri"
|
||||
STR_XTC_STATUS_BAR: "XTC-statuslinje"
|
||||
STR_BOTTOM: "Nederst"
|
||||
STR_TOP: "Øverst"
|
||||
STR_CLOCK: "Klokke"
|
||||
STR_CLOCK_UTC_OFFSET: "UTC-forskyvning for klokke"
|
||||
STR_CLOCK_FORMAT: "Klokkeformat"
|
||||
STR_CLOCK_FORMAT_24H: "24-timers"
|
||||
STR_CLOCK_FORMAT_12H: "12-timers"
|
||||
STR_CURRENT_TIME: "Nåværende tid:"
|
||||
STR_NEXT_FIELD: "Neste"
|
||||
STR_CLOCK_SYNC: "Synkroniser klokke"
|
||||
STR_CLOCK_SYNC_NOW: "Synkroniser klokke nå"
|
||||
STR_CLOCK_SYNCING: "Synkroniserer fra NTP..."
|
||||
STR_CLOCK_SYNC_OK: "Klokke synkronisert"
|
||||
STR_CLOCK_SYNC_FAIL: "Synkronisering mislyktes"
|
||||
STR_CLOCK_SYNC_NO_WIFI: "WiFi ikke tilkoblet"
|
||||
STR_CLOCK_SYNC_NO_WIFI_HINT: "Koble til WiFi først, og prøv igjen."
|
||||
STR_CLOCK_SYNCED: "Klokke synkronisert"
|
||||
STR_UI_THEME: "Grensesnittstema"
|
||||
STR_THEME_CLASSIC: "Klassisk"
|
||||
STR_THEME_LYRA: "Lyra"
|
||||
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
|
||||
STR_SUNLIGHT_FADING_FIX: "Retting for soluttoning"
|
||||
STR_REMAP_FRONT_BUTTONS: "Tilordne frontknapper på nytt"
|
||||
STR_BOOKMARKS: "Bokmerker"
|
||||
STR_BOOKMARK_ADDED: "Bokmerke lagt til."
|
||||
STR_BOOKMARK_REMOVED: "Bokmerke fjernet."
|
||||
STR_OPDS_BROWSER: "OPDS-leser"
|
||||
STR_SEARCH: "Søk"
|
||||
STR_COVER_CUSTOM: "Omslag + Egendefinert"
|
||||
STR_QUICK_RESUME: "Hurtig gjenopptak"
|
||||
STR_MENU_RECENT_BOOKS: "Nylige bøker"
|
||||
STR_REMOVE_FROM_RECENTS: "Fjern fra Nylige bøker?"
|
||||
STR_NO_RECENT_BOOKS: "Ingen nylige bøker"
|
||||
STR_CALIBRE_DESC: "Bruk trådløs enhetsoverføring med Calibre"
|
||||
STR_FORGET_AND_REMOVE: "Glem nettverk og fjern lagret passord?"
|
||||
STR_FORGET_BUTTON: "Glem"
|
||||
STR_CALIBRE_STARTING: "Starter Calibre..."
|
||||
STR_CALIBRE_SETUP: "Oppsett"
|
||||
STR_CALIBRE_STATUS: "Status"
|
||||
STR_CLEAR_BUTTON: "Tøm"
|
||||
STR_DEFAULT_VALUE: "Standard"
|
||||
STR_REMAP_PROMPT: "Trykk en frontknapp for hver rolle"
|
||||
STR_UNASSIGNED: "Ikke tilordnet"
|
||||
STR_ALREADY_ASSIGNED: "Allerede tilordnet"
|
||||
STR_REMAP_RESET_HINT: "Sideknapp opp: Nullstill til standardoppsett"
|
||||
STR_REMAP_CANCEL_HINT: "Sideknapp ned: Avbryt omtilordning"
|
||||
STR_HW_BACK_LABEL: "Tilbake (1. knapp)"
|
||||
STR_HW_CONFIRM_LABEL: "Bekreft (2. knapp)"
|
||||
STR_HW_LEFT_LABEL: "Venstre (3. knapp)"
|
||||
STR_HW_RIGHT_LABEL: "Høyre (4. knapp)"
|
||||
STR_GO_TO_PERCENT: "Gå til %"
|
||||
STR_GO_HOME_BUTTON: "Hjem"
|
||||
STR_SYNC_PROGRESS: "Synkroniser fremdrift"
|
||||
STR_DELETE_CACHE: "Slett bokbuffer"
|
||||
STR_DELETE: "Slett"
|
||||
STR_CONFIRM_DELETE_BOOKMARK: "Slette dette bokmerket?"
|
||||
STR_DISPLAY_QR: "Vis side som QR"
|
||||
STR_CHAPTER_PREFIX: "Kapittel: "
|
||||
STR_PAGES_SEPARATOR: " sider | "
|
||||
STR_BOOK_PREFIX: "Bok: "
|
||||
STR_CALIBRE_URL_HINT: "For Calibre, legg til /opds i URL-en din"
|
||||
STR_PERCENT_STEP_HINT: "Venstre/Høyre: 1% Opp/Ned: 10%"
|
||||
STR_SYNCING_TIME: "Synkroniserer tid..."
|
||||
STR_CALC_HASH: "Beregner dokument-hash..."
|
||||
STR_HASH_FAILED: "Kunne ikke beregne dokument-hash"
|
||||
STR_FETCH_PROGRESS: "Henter ekstern fremdrift..."
|
||||
STR_UPLOAD_PROGRESS: "Laster opp fremdrift..."
|
||||
STR_NO_CREDENTIALS_MSG: "Ingen påloggingsinfo angitt"
|
||||
STR_KOREADER_SETUP_HINT: "Sett opp KOReader-konto i Innstillinger"
|
||||
STR_PROGRESS_FOUND: "Fremdrift funnet!"
|
||||
STR_REMOTE_LABEL: "Ekstern:"
|
||||
STR_LOCAL_LABEL: "Lokal:"
|
||||
STR_PAGE_OVERALL_FORMAT: "Side %d, %.2f%% totalt"
|
||||
STR_PAGE_TOTAL_OVERALL_FORMAT: "Side %d/%d, %.2f%% totalt"
|
||||
STR_DEVICE_FROM_FORMAT: " Fra: %s"
|
||||
STR_APPLY_REMOTE: "Bruk ekstern fremdrift"
|
||||
STR_UPLOAD_LOCAL: "Last opp lokal fremdrift"
|
||||
STR_NO_REMOTE_MSG: "Ingen ekstern fremdrift funnet"
|
||||
STR_UPLOAD_PROMPT: "Last opp nåværende posisjon?"
|
||||
STR_UPLOAD_SUCCESS: "Fremdrift lastet opp!"
|
||||
STR_SYNC_FAILED_MSG: "Synkronisering mislyktes"
|
||||
STR_SAVE_PROGRESS_FAILED: "Kunne ikke lagre fremdrift"
|
||||
STR_SECTION_PREFIX: "Del "
|
||||
STR_UPLOAD: "Last opp"
|
||||
STR_BOOK_S_STYLE: "Bokens stil"
|
||||
STR_EMBEDDED_STYLE: "Innebygd stil"
|
||||
STR_FOCUS_READING: "Fokuslesing"
|
||||
STR_OPDS_SERVER_URL: "OPDS-server-URL"
|
||||
STR_PWR_BTN_FOOTNOTE_BACK: "Hurtig retur fra fotnoter"
|
||||
STR_SET_SLEEP_COVER: "Velg omslag"
|
||||
STR_FOOTNOTES: "Fotnoter"
|
||||
STR_NO_FOOTNOTES: "Ingen fotnoter på denne siden"
|
||||
STR_LINK: "[lenke]"
|
||||
STR_SCREENSHOT_BUTTON: "Ta skjermbilde"
|
||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u min"
|
||||
STR_SLEEP_NEVER: "Aldri"
|
||||
STR_SLEEP_TIMER_STEP_HINT: "Venstre/Høyre: 1 min Opp/Ned: 5 min"
|
||||
STR_ADD_SERVER: "Legg til server"
|
||||
STR_SERVER_NAME: "Servernavn"
|
||||
STR_NO_SERVERS: "Ingen OPDS-servere konfigurert"
|
||||
STR_DELETE_SERVER: "Slett server"
|
||||
STR_OPDS_SERVERS: "OPDS-servere"
|
||||
STR_AUTO_TURN_ENABLED: "Automatisk bla aktivert: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk bla (sider per minutt)"
|
||||
STR_MANAGE_FONTS: "Administrer skrifttyper"
|
||||
STR_FONT_BROWSER: "Bla i skrifttyper"
|
||||
STR_LOADING_FONT_LIST: "Laster skrifttypeliste..."
|
||||
STR_NO_FONTS_AVAILABLE: "Ingen skrifttyper tilgjengelig"
|
||||
STR_FONT_INSTALLED: "Skrifttype installert!"
|
||||
STR_FONT_INSTALL_FAILED: "Installasjon av skrifttype mislyktes"
|
||||
STR_INSTALLED: "Installert"
|
||||
STR_DOWNLOAD_ALL: "Last ned alle"
|
||||
STR_UPDATE_ALL: "Oppdater alle"
|
||||
STR_UPDATE_AVAILABLE: "Oppdater"
|
||||
STR_CRASH_TITLE: "Systemkrasj"
|
||||
STR_CRASH_DESCRIPTION: "En detaljert rapport ble lagret i crash_report.txt. Legg ved denne filen i feilrapporten din."
|
||||
STR_CRASH_REASON: "Krasjårsak:"
|
||||
STR_CRASH_NO_REASON: "(Ingen årsak ble registrert)"
|
||||
STR_TILT_PAGE_TURN: "Vipp for å bla"
|
||||
STR_KB_HINT_MOVE_CURSOR: "Trykk VENSTRE eller HØYRE for å flytte markøren"
|
||||
STR_KB_HINT_RETURN_CURSOR: "Trykk VENSTRE for å gå tilbake til markørposisjonen"
|
||||
STR_KB_HINT_HIDE_PASSWORD: "Hold HØYRE og trykk [***] for å skjule passord"
|
||||
STR_KB_HINT_SHOW_PASSWORD: "Hold HØYRE og trykk [abc] for å vise passord"
|
||||
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Trykk [***] for å skjule passord"
|
||||
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Trykk [abc] for å vise passord"
|
||||
STR_KB_HINT_EDIT_ENTRY: "Hold OPP for å redigere oppføring"
|
||||
STR_KB_TIPS: "Tips:"
|
||||
STR_KB_HINT_RETURN_KEYBOARD: "Trykk NED for å gå tilbake til tastaturet"
|
||||
STR_KB_HINT_EXIT_URL_MODE: "Trykk ABC for å avslutte URL-modus"
|
||||
STR_KB_HINT_CLEAR_TEXT: "Hold DEL for å slette all tekst"
|
||||
STR_KB_HINT_SECONDARY_CHAR: "Hold VELG for sekundærtegn"
|
||||
STR_KB_HINT_UPPER_SECONDARY: "Hold VELG for STORE BOKSTAVER eller sekundærtegn"
|
||||
STR_KB_HINT_LOWER_SECONDARY: "Hold VELG for små bokstaver eller sekundærtegn"
|
||||
STR_KB_HINT_URL_SNIPPETS: "Trykk URL for tekstbiter"
|
||||
STR_SD_FIRMWARE_UPDATE: "Fastvareoppdatering fra SD-kort"
|
||||
STR_SELECT_FIRMWARE_FILE: "Velg fastvarefil (.bin)"
|
||||
STR_NO_BIN_FILES: "Ingen .bin-filer funnet"
|
||||
STR_VALIDATING_FIRMWARE: "Validerer fastvare..."
|
||||
STR_INVALID_FIRMWARE: "Ugyldig fastvarefil"
|
||||
STR_FIRMWARE_TOO_LARGE: "Fastvaren er for stor for partisjonen"
|
||||
STR_FIRMWARE_TOO_SMALL: "Fastvarefilen er for liten"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "Oppdatere fastvare?"
|
||||
STR_FIRMWARE_FILE_OPEN_FAILED: "Kan ikke åpne filen"
|
||||
STR_FIRMWARE_WRITE_FAILED: "Skriving av fastvare mislyktes"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Ikke slå av!"
|
||||
STR_RECOVERY_MODE: "Gjenopprettingsmodus"
|
||||
STR_RECOVERY_MODE_HINT: "Legg firmware.bin i roten av SD-kortet og velg den"
|
||||
@@ -68,10 +68,10 @@ STR_SHORT_PWR_BTN: "Kısa Güç Tuşu Tıklaması"
|
||||
STR_ORIENTATION: "Okuma Yönü"
|
||||
STR_SIDE_BTN_LAYOUT: "Yan Tuş Dizilimi (okuyucu)"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Ön düğmeleri yönlendir"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Long-press button behavior"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Chapter skip"
|
||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Orientation change"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Uzun basma tuş davranışı"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "KAPALI"
|
||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Bölüm atlama"
|
||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Yön değiştirme"
|
||||
STR_FONT_PREVIEW_TEXT: "Pijamalı hasta yağız şoföre çabucak güvendi"
|
||||
STR_FONT_FAMILY: "Okuyucu Yazı Tipi Ailesi"
|
||||
STR_FONT_SIZE: "Arayüz Yazı Boyutu"
|
||||
@@ -306,3 +306,89 @@ STR_TITLE: "Başlık"
|
||||
STR_TILT_PAGE_TURN: "Eğerek sayfa çevirme"
|
||||
STR_ADD_HIDDEN_NETWORK: "Gizli ağ ekle..."
|
||||
STR_ENTER_WIFI_SSID: "Ağ adını girin (SSID)"
|
||||
STR_ADD_SERVER: "Sunucu Ekle"
|
||||
STR_BOOKMARKS: "Yer İmleri"
|
||||
STR_BOOKMARK_ADDED: "Yer imi eklendi."
|
||||
STR_BOOKMARK_OPTION: "Yer İmi"
|
||||
STR_BOTTOM: "Alt"
|
||||
STR_CLOCK: "Saat"
|
||||
STR_CLOCK_FORMAT: "Saat Biçimi"
|
||||
STR_CLOCK_FORMAT_12H: "12 saat"
|
||||
STR_CLOCK_FORMAT_24H: "24 saat"
|
||||
STR_CLOCK_SYNC: "Saati Eşitle"
|
||||
STR_CLOCK_SYNCED: "Saat Eşitlendi"
|
||||
STR_CLOCK_SYNCING: "NTP'den eşitleniyor..."
|
||||
STR_CLOCK_SYNC_FAIL: "Eşitleme başarısız"
|
||||
STR_CLOCK_SYNC_NOW: "Saati şimdi eşitle"
|
||||
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi bağlı değil"
|
||||
STR_CLOCK_SYNC_NO_WIFI_HINT: "Önce Wi-Fi'ye bağlanın, sonra tekrar deneyin."
|
||||
STR_CLOCK_SYNC_OK: "Saat eşitlendi"
|
||||
STR_CLOCK_UTC_OFFSET: "Saat UTC Farkı"
|
||||
STR_CONFIRM_DELETE_BOOKMARK: "Bu yer imi silinsin mi?"
|
||||
STR_CONNECTING_SAVED_WIFI: "Kayıtlı Wi-Fi'ye bağlanılıyor..."
|
||||
STR_CRASH_DESCRIPTION: "Ayrıntılı rapor crash_report.txt dosyasına kaydedildi. Lütfen hata bildiriminize bu dosyayı ekleyin."
|
||||
STR_CRASH_NO_REASON: "(Neden kaydedilmedi)"
|
||||
STR_CRASH_REASON: "Çökme nedeni:"
|
||||
STR_CRASH_TITLE: "Sistem Çökmesi"
|
||||
STR_CURRENT_TIME: "Şu anki saat:"
|
||||
STR_DELETE_SERVER: "Sunucuyu Sil"
|
||||
STR_DISABLED: "Devre dışı"
|
||||
STR_DOWNLOAD_ALL: "Tümünü İndir"
|
||||
STR_EOB_CONTINUE_WITH: "Şununla devam et"
|
||||
STR_EOB_HOME: "Ana Ekran"
|
||||
STR_FINDING_SAVED_WIFI: "Kayıtlı Wi-Fi aranıyor..."
|
||||
STR_FIRMWARE_FILE_OPEN_FAILED: "Dosya açılamıyor"
|
||||
STR_FIRMWARE_TOO_LARGE: "Firmware, bölümlemeye sığmıyor"
|
||||
STR_FIRMWARE_TOO_SMALL: "Firmware dosyası çok küçük"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Cihazı kapatmayın!"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "Firmware güncellensin mi?"
|
||||
STR_FIRMWARE_WRITE_FAILED: "Firmware yazılamadı"
|
||||
STR_FONT_BROWSER: "Yazı Tipi Tarayıcısı"
|
||||
STR_FONT_INSTALLED: "Yazı tipi yüklendi!"
|
||||
STR_FONT_INSTALL_FAILED: "Yazı tipi yüklenemedi"
|
||||
STR_FORCE_REFRESH: "Ekranı Yenile"
|
||||
STR_INDEX_FAILED: "Endeksleme başarısız - geçersiz kitap"
|
||||
STR_INSTALLED: "Yüklü"
|
||||
STR_INVALID_FIRMWARE: "Geçersiz firmware dosyası"
|
||||
STR_KB_HINT_CLEAR_TEXT: "Tüm metni silmek için DEL tuşunu basılı tutun"
|
||||
STR_KB_HINT_EDIT_ENTRY: "Girdiyi düzenlemek için UP tuşunu basılı tutun"
|
||||
STR_KB_HINT_EXIT_URL_MODE: "URL modundan çıkmak için ABC'ye basın"
|
||||
STR_KB_HINT_HIDE_PASSWORD: "Şifreyi gizlemek için RIGHT'ı basılı tutup [***] tuşuna basın"
|
||||
STR_KB_HINT_LOWER_SECONDARY: "Küçük harf veya ikincil karakter için SELECT'i basılı tutun"
|
||||
STR_KB_HINT_MOVE_CURSOR: "İmleci taşımak için LEFT veya RIGHT'a basın"
|
||||
STR_KB_HINT_RETURN_CURSOR: "İmleç konumuna dönmek için LEFT'e basın"
|
||||
STR_KB_HINT_RETURN_KEYBOARD: "Klavyeye dönmek için DOWN'a basın"
|
||||
STR_KB_HINT_SECONDARY_CHAR: "İkincil karakter için SELECT'i basılı tutun"
|
||||
STR_KB_HINT_SHOW_PASSWORD: "Şifreyi göstermek için RIGHT'ı basılı tutup [abc] tuşuna basın"
|
||||
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Şifreyi gizlemek için [***] tuşuna basın"
|
||||
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Şifreyi göstermek için [abc] tuşuna basın"
|
||||
STR_KB_HINT_UPPER_SECONDARY: "BÜYÜK harf veya ikincil karakter için SELECT'i basılı tutun"
|
||||
STR_KB_HINT_URL_SNIPPETS: "Hazır kalıplar için URL'ye basın"
|
||||
STR_KB_TIPS: "İpuçları:"
|
||||
STR_KOSYNC: "KOSync"
|
||||
STR_LOADING_FONT_LIST: "Yazı tipi listesi yükleniyor..."
|
||||
STR_LONG_PRESS_MENU: "Uzun Basma Menüsü"
|
||||
STR_MANAGE_FONTS: "Yazı Tiplerini Yönet"
|
||||
STR_NEXT_FIELD: "Sonraki"
|
||||
STR_NEXT_PAGE: "Sonraki Sayfa »"
|
||||
STR_NO_BIN_FILES: ".bin dosyası bulunamadı"
|
||||
STR_NO_FONTS_AVAILABLE: "Kullanılabilir yazı tipi yok"
|
||||
STR_NO_SERVERS: "Yapılandırılmış OPDS sunucusu yok"
|
||||
STR_OPDS_SERVERS: "OPDS Sunucuları"
|
||||
STR_PREV_PAGE: "« Önceki Sayfa"
|
||||
STR_PWR_BTN_FOOTNOTE_BACK: "Dipnottan hızlı dönüş"
|
||||
STR_RECOVERY_MODE: "Kurtarma Modu"
|
||||
STR_RECOVERY_MODE_HINT: "firmware.bin dosyasını SD kartın köküne koyup seçin"
|
||||
STR_RESTARTING_HINT: "Yeniden başlatılıyor... Cihaz yeniden başlamazsa güç düğmesini birkaç saniye basılı tutun."
|
||||
STR_SD_FIRMWARE_UPDATE: "SD Karttan Firmware Güncelleme"
|
||||
STR_SEARCH: "Ara"
|
||||
STR_SELECT_FIRMWARE_FILE: "Firmware dosyası seçin (.bin)"
|
||||
STR_SERVER_NAME: "Sunucu Adı"
|
||||
STR_SET_SLEEP_COVER: "Kapak Ayarla"
|
||||
STR_SHOW_NETWORKS: "Göster"
|
||||
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||
STR_TOP: "Üst"
|
||||
STR_UPDATE_ALL: "Tümünü Güncelle"
|
||||
STR_UPDATE_AVAILABLE: "Güncelle"
|
||||
STR_VALIDATING_FIRMWARE: "Firmware doğrulanıyor..."
|
||||
STR_XTC_STATUS_BAR: "XTC Durum Çubuğu"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
+53
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -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,50 +1453,79 @@ 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;
|
||||
|
||||
auto scratch = makeUniqueNoThrow<uint8_t[]>(static_cast<size_t>(gwBytes) * STRIP_ROWS);
|
||||
if (!scratch) {
|
||||
LOG_ERR("ERS", "OOM: grayscale strip scratch (%d bytes); skipping AA this page", gwBytes * STRIP_ROWS);
|
||||
} else {
|
||||
// Bands may be streamed in any order: X4 windows each via setRamArea, X3
|
||||
// via PTL.
|
||||
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
|
||||
// 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(scratch.get(), y, rows);
|
||||
renderer.beginStripTarget(buf + static_cast<size_t>(y) * gwBytes, y, rows);
|
||||
renderer.clearScreen(0x00);
|
||||
renderGrayscalePass();
|
||||
renderer.endStripTarget();
|
||||
renderer.writeGrayscalePlaneStrip(true, scratch.get(), y, rows);
|
||||
}
|
||||
const auto tGrayLsb = millis();
|
||||
};
|
||||
|
||||
// MSB plane.
|
||||
renderer.setRenderMode(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(scratch.get(), y, rows);
|
||||
renderer.clearScreen(0x00);
|
||||
renderGrayscalePass();
|
||||
renderer.endStripTarget();
|
||||
renderer.writeGrayscalePlaneStrip(false, scratch.get(), y, rows);
|
||||
// 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 tGrayMsb = millis();
|
||||
const auto tGrayWrite = millis();
|
||||
|
||||
renderer.setRenderMode(GfxRenderer::BW);
|
||||
renderer.displayGrayBuffer();
|
||||
@@ -1448,14 +1534,70 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
||||
// BW framebuffer is intact; re-sync controller RAM for the next
|
||||
// differential page turn directly from it.
|
||||
renderer.cleanupGrayscaleWithFrameBuffer();
|
||||
const auto tCleanup = millis();
|
||||
|
||||
const auto tEnd = millis();
|
||||
|
||||
LOG_DBG("ERS",
|
||||
"Page render (tiled): prewarm=%lums bw_render=%lums display=%lums gray_lsb=%lums "
|
||||
"gray_msb=%lums gray_display=%lums cleanup=%lums total=%lums",
|
||||
tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tGrayLsb - tDisplay, tGrayMsb - tGrayLsb,
|
||||
tGrayDisplay - tGrayMsb, tCleanup - tGrayDisplay, tEnd - t0);
|
||||
"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.
|
||||
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;
|
||||
renderer.beginStripTarget(scratch.get(), y, rows);
|
||||
renderer.clearScreen(0x00);
|
||||
renderGrayscalePass();
|
||||
renderer.endStripTarget();
|
||||
renderer.writeGrayscalePlaneStrip(true, scratch.get(), y, rows);
|
||||
}
|
||||
const auto tGrayLsb = millis();
|
||||
|
||||
// MSB plane.
|
||||
renderer.setRenderMode(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(scratch.get(), y, rows);
|
||||
renderer.clearScreen(0x00);
|
||||
renderGrayscalePass();
|
||||
renderer.endStripTarget();
|
||||
renderer.writeGrayscalePlaneStrip(false, scratch.get(), y, rows);
|
||||
}
|
||||
const auto tGrayMsb = 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 tCleanup = millis();
|
||||
|
||||
const auto tEnd = millis();
|
||||
LOG_DBG("ERS",
|
||||
"Page render (tiled): prewarm=%lums bw_render=%lums display=%lums gray_lsb=%lums "
|
||||
"gray_msb=%lums gray_display=%lums cleanup=%lums total=%lums",
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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--;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user