mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-08-13 14:21:57 -07:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d09c755cf2 | ||
|
|
bccb4cc43f | ||
|
|
f4378bae4f | ||
|
|
7ab9c21afc | ||
|
|
f449ce9e7d | ||
|
|
48e39eb0ee | ||
|
|
9b1fb712de | ||
|
|
e715532f90 | ||
|
|
70faa29dac | ||
|
|
cb995809ba | ||
|
|
f16451c729 | ||
|
|
268c27f7d0 | ||
|
|
b1d53ba810 |
No files matched your search
@@ -1,7 +1,6 @@
|
||||
name: "PR Formatting"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
@@ -9,7 +8,7 @@ on:
|
||||
- edited
|
||||
|
||||
permissions:
|
||||
statuses: write
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
title-check:
|
||||
|
||||
@@ -36,7 +36,7 @@ CrossPoint is open-source e-reader firmware - community-built, fully hackable, f
|
||||
- OPDS browser with saved servers (up to 8), search, pagination, and direct download
|
||||
- OTA update checks and installs from GitHub releases
|
||||
|
||||
- **Customization**: multiple themes (Classic, Lyra, Lyra Extended, RoundedRaff), sleep screen modes, front/side button remapping, status bar controls, power-button behavior, refresh cadence, and more.
|
||||
- **Customization**: multiple themes (Classic, Lyra, Lyra Extended, RoundedRaff), sleep screen modes including transparent overlays, front/side button remapping, status bar controls, power-button behavior, refresh cadence, and more.
|
||||
|
||||
- **Localization**: 24 UI languages and counting. RTL support.
|
||||
|
||||
|
||||
+14
-4
@@ -186,7 +186,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
||||
- "None" - A blank screen
|
||||
- "Cover + Custom" - The book cover image while actively reading, falls back to "Custom" behavior otherwise
|
||||
- "Quick resume" - The text of the last page read will be displayed on the sleep screen and a moon icon is shown on the edge of the screen. Waking up the device will return to the same page of the opened book. This is useful for quickly resuming reading without waiting for the device to fully wake up and load the book.
|
||||
|
||||
- "Transparent" - A transparent overlay image drawn over the current screen; see [Sleep Screen](#37-sleep-screen) below for more information
|
||||
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
|
||||
|
||||
- "Fit" (default) - Scale the image down to fit centered on the screen, padding with white borders as necessary
|
||||
@@ -501,6 +501,7 @@ The **Sleep Screen** setting controls what is displayed when the device goes to
|
||||
| **Custom** | A custom image from the SD card (see below). Falls back to **Dark** if no custom image is found. |
|
||||
| **Cover** | The cover of the currently open book. Falls back to **Dark** if no book is open. |
|
||||
| **Cover + Custom** | The cover of the currently open book, shown only while actively reading. Falls back to **Custom** behavior when not reading. |
|
||||
| **Transparent** | A BMP overlay drawn over the current screen. Supports alpha transparency in 32-bit BGRA images and treats white as transparent in regular BMPs. Falls back to **Dark** if no valid overlay image is found. |
|
||||
| **None** | A blank screen. |
|
||||
|
||||
#### Cover settings
|
||||
@@ -515,12 +516,21 @@ When using **Cover** or **Cover + Custom**, two additional settings apply:
|
||||
To use custom sleep images, set the sleep screen mode to **Custom** or **Cover + Custom**, then place images on the SD card:
|
||||
|
||||
- **Multiple Images (recommended):** Create a `.sleep` directory in the root of the SD card and place any number of `.bmp` images inside. One will be randomly selected each time the device sleeps. (A directory named `sleep` is also accepted as a fallback.)
|
||||
- **Single Image:** Place a file named `sleep.bmp` in the root directory. This is used as a fallback if no valid images are found in the `.sleep`/`sleep` directory.
|
||||
- **Single Image:** Place a file named `sleep.bmp` in the root directory. This takes priority over the `.sleep`/`sleep` directories.
|
||||
|
||||
#### Transparent overlay images
|
||||
|
||||
To use transparent sleep overlays, set the sleep screen mode to **Transparent**, then place BMP files on the SD card:
|
||||
|
||||
- **Multiple Images (recommended):** Create a `.sleep-overlay` directory in the root of the SD card and place any number of valid overlay `.bmp` images inside. One will be randomly selected each time the device sleeps. A directory named `sleep-overlay` is also accepted as a fallback.
|
||||
- **Single Image:** Place a file named `sleep-overlay.bmp` in the root directory. This takes priority over the `.sleep-overlay`/`sleep-overlay` directories.
|
||||
|
||||
Transparent overlay files are intentionally separate from normal sleep images. Regular BMP formats supported by CrossPoint are accepted; white pixels leave the existing screen unchanged. For per-pixel alpha transparency, use a 32-bit BGRA BMP with both visible and non-opaque pixels. Opaque white pixels in these alpha images erase the content behind them.
|
||||
|
||||
> [!TIP]
|
||||
> For best results:
|
||||
>
|
||||
> - Use uncompressed BMP files with 24-bit color depth
|
||||
> - For non-transparent **Custom** mode, use uncompressed BMP files with 24-bit color depth.
|
||||
> - For **Transparent** mode, use a regular BMP for white-as-transparent artwork or an uncompressed 32-bit BGRA BMP for per-pixel alpha.
|
||||
> - X4: Use a resolution of 480x800 pixels to match the device's screen resolution.
|
||||
> - X3: Use a resolution of 528x792 pixels to match the device's screen resolution.
|
||||
|
||||
|
||||
+3
-1
@@ -13,7 +13,7 @@ A dictionary folder must contain:
|
||||
- `.syn` — synonym index (optional; maps alternate spellings and irregular forms to their headword)
|
||||
- `.ifo` — metadata (optional)
|
||||
|
||||
Not supported: dictionaries with 64-bit index offsets (`idxoffsetbits=64` in the `.ifo` — rare, and rejected with an error), and HTML-formatted definitions render as raw markup rather than styled text.
|
||||
Not supported: dictionaries with 64-bit index offsets (`idxoffsetbits=64` in the `.ifo` — rare, and rejected with an error).
|
||||
|
||||
## Setting Up a Dictionary
|
||||
|
||||
@@ -49,6 +49,8 @@ On the very first lookup with a dictionary (and again whenever the `.idx` or `.s
|
||||
|
||||
When a word is found, the definition screen shows the matched headword at the top and the definition text below, with a page counter for long definitions.
|
||||
|
||||
HTML dictionaries that declare `sametypesequence=h` use the EPUB text-layout engine for semantic formatting such as headings, bold, italics, lists, and line breaks. Images and CSS styling are ignored. Definitions that are too large or cannot be laid out within the available memory fall back to plain text.
|
||||
|
||||
- **Left/Right** or side **Up/Down** — previous / next page
|
||||
- **Back** — return to word selection
|
||||
|
||||
|
||||
+1
-1
Submodule freeink-sdk updated: 83d49345d8...57f05e80c0.
@@ -0,0 +1,431 @@
|
||||
_language_name: "Български"
|
||||
_language_code: "BG"
|
||||
_order: "31"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "ЗАРЕЖДАНЕ"
|
||||
STR_SLEEPING: "СПЯЩ РЕЖИМ"
|
||||
STR_ENTERING_SLEEP: "Преминаване в спящ режим"
|
||||
STR_BROWSE_FILES: "Разглеждане на файлове"
|
||||
STR_FILE_TRANSFER: "Прехвърляне на файлове"
|
||||
STR_SETTINGS_TITLE: "Настройки"
|
||||
STR_CONTINUE_READING: "Продължи четенето"
|
||||
STR_RESUME: "Продължи"
|
||||
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_INVALID_BMP_FILE: "Невалиден BMP файл"
|
||||
STR_FILE_OPEN_FAILED: "Файлът не може да бъде отворен"
|
||||
STR_OUT_OF_BOUNDS: "Извън обхвата"
|
||||
STR_LOADING: "Зареждане..."
|
||||
STR_LOADING_POPUP: "Зареждане"
|
||||
STR_LOOKUP: "Търсене"
|
||||
STR_DICT_LOOKING_UP: "Търсене..."
|
||||
STR_DICT_INDEXING: "Индексиране на речника..."
|
||||
STR_DICT_NOT_FOUND: "Не е намерено"
|
||||
STR_DICT_NO_DICT_SET: "Не е зададен речник"
|
||||
STR_DICT_ERROR: "Грешка в речника"
|
||||
STR_DICT_READ_FAILED: "Определението не може да бъде прочетено"
|
||||
STR_DICT_DECOMPRESS_ERROR: "Определението не може да бъде декомпресирано"
|
||||
STR_DICT_LOW_MEMORY: "Недостатъчна памет"
|
||||
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: "Натисни OK за ново сканиране"
|
||||
STR_JOIN_NETWORK: "Присъединяване към мрежа"
|
||||
STR_CREATE_HOTSPOT: "Създаване на hotspot"
|
||||
STR_JOIN_DESC: "Свързване към съществуваща Wi-Fi мрежа"
|
||||
STR_HOTSPOT_DESC: "Създаване на Wi-Fi мрежа, за други устройства"
|
||||
STR_STARTING_HOTSPOT: "Стартиране на hotspot..."
|
||||
STR_HOTSPOT_MODE: "Режим на hotspot"
|
||||
STR_CONNECT_WIFI_HINT: "Свържи устройството си към Wi-Fi мрежа"
|
||||
STR_OPEN_URL_HINT: "Отвори този URL адрес в браузъра си"
|
||||
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: "Въведи Wi-Fi парола"
|
||||
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_TOUCH_READER_CONTROLS: "Сензорно управление на четеца"
|
||||
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_TEXT_SETTINGS: "Настройки на текста"
|
||||
STR_FONT: "Шрифт"
|
||||
STR_SIZE: "Размер"
|
||||
STR_LAYOUT: "Изглед"
|
||||
STR_ALIGNMENT: "Подравняване"
|
||||
STR_STYLE: "Стил"
|
||||
STR_NOT_IN_PREVIEW: "Не се показва в предварителния преглед"
|
||||
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: "Измести завършените в папка 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: "URL адрес на сървъра за синхронизация"
|
||||
STR_DOCUMENT_MATCHING: "Съпоставяне на документи"
|
||||
STR_SEND_METADATA: "Изпращане на метаданни на документа"
|
||||
STR_SYNC_BEHAVIOR: "Поведение при синхронизация"
|
||||
STR_ASK_EVERY_TIME: "Питай всеки път"
|
||||
STR_SMART_SYNC: "Умна синхронизация"
|
||||
STR_AUTHENTICATE: "Удостовери се"
|
||||
STR_SIGN_UP: "Регистрация"
|
||||
STR_CREATING_ACCOUNT: "Създаване на профил..."
|
||||
STR_ACCOUNT_CREATED: "Профилът е създаден"
|
||||
STR_SIGNUP_FAILED: "Неуспешна регистрация"
|
||||
STR_USERNAME_TAKEN: "Потребителското име вече е регистрирано"
|
||||
STR_KOREADER_USERNAME: "KOReader потребителско име"
|
||||
STR_KOREADER_PASSWORD: "KOReader парола"
|
||||
STR_FILENAME: "Име на файла"
|
||||
STR_BINARY: "Двоичен"
|
||||
STR_SET_CREDENTIALS_FIRST: "Първо задай данни за вход"
|
||||
STR_WIFI_CONN_FAILED: "Неуспешна Wi-Fi връзка"
|
||||
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_DICTIONARY: "Речник"
|
||||
STR_DISABLED: "Изключено"
|
||||
STR_NOTO_SERIF: "Noto Serif"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_TIGHT: "Тесен"
|
||||
STR_NORMAL: "Нормален"
|
||||
STR_WIDE: "Широк"
|
||||
STR_JUSTIFY: "Двустранно"
|
||||
STR_ALIGN_LEFT: "Ляво"
|
||||
STR_CENTER: "Центрирано"
|
||||
STR_ALIGN_RIGHT: "Дясно"
|
||||
STR_PAGES_1: "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: "Не е зададен 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_TAP_TO_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: "OK"
|
||||
STR_KEY_SHIFT: "Shift"
|
||||
STR_KEY_MODE_SYMBOLS: "?123"
|
||||
STR_KEY_MODE_ABC: "abc"
|
||||
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 Extended"
|
||||
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 към твоя URL адрес"
|
||||
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_ALREADY_SYNCED: "Вече е синхронизиран"
|
||||
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: "URL адрес на OPDS сървъра"
|
||||
STR_PWR_BTN_FOOTNOTE_BACK: "Бързо връщане от бележки под линия"
|
||||
STR_BACK_SHORT_TO_FILE_BROWSER: "Натиск. назад към файловия браузър"
|
||||
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_OPDS_DOWNLOAD_FOLDER: "Папка за изтегляне"
|
||||
STR_OPDS_FILENAME_FORMAT: "Формат на името на файла"
|
||||
STR_FMT_AUTHOR_TITLE: "Автор - Заглавие"
|
||||
STR_FMT_TITLE_AUTHOR: "Заглавие - Автор"
|
||||
STR_FMT_TITLE: "Заглавие"
|
||||
STR_OPDS_SD_ROOT: "Корен на SD картата"
|
||||
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: "Задръж SELECT за символи"
|
||||
STR_KB_HINT_UPPER_SECONDARY: "Задръж SELECT за ГЛАВНИ БУКВИ или символи"
|
||||
STR_KB_HINT_LOWER_SECONDARY: "Задръж SELECT за малки букви или символи"
|
||||
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 картата и го избери"
|
||||
@@ -273,11 +273,13 @@ STR_SUNLIGHT_FADING_FIX: "Correcció de l'esvaïment pel sol"
|
||||
STR_QUICK_RESUME_TIMEOUT: "Represa ràpida per inactivitat"
|
||||
STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals"
|
||||
STR_BOOKMARKS: "Punts de llibre"
|
||||
STR_NO_BOOKMARKS: "No hi ha punts de llibre"
|
||||
STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre."
|
||||
STR_BOOKMARK_REMOVED: "S'ha eliminat el punt de llibre."
|
||||
STR_OPDS_BROWSER: "Navegador OPDS"
|
||||
STR_COVER_CUSTOM: "Portada + Personalitzat"
|
||||
STR_QUICK_RESUME: "Represa ràpida"
|
||||
STR_TRANSPARENT: "Transparent"
|
||||
STR_MENU_RECENT_BOOKS: "Llibres recents"
|
||||
STR_REMOVE_FROM_RECENTS: "Vols suprimir-lo de Llibres recents?"
|
||||
STR_NO_RECENT_BOOKS: "No hi ha llibres recents"
|
||||
@@ -422,6 +424,7 @@ STR_VALIDATING_FIRMWARE: "S'està validant el firmware..."
|
||||
STR_INVALID_FIRMWARE: "Fitxer de firmware no vàlid"
|
||||
STR_FIRMWARE_TOO_LARGE: "El firmware és massa gran per a la partició"
|
||||
STR_FIRMWARE_TOO_SMALL: "El fitxer de firmware és massa petit"
|
||||
STR_FIRMWARE_WRONG_DEVICE: "El firmware és per a un altre dispositiu"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "Vols actualitzar el firmware?"
|
||||
STR_FIRMWARE_FILE_OPEN_FAILED: "No es pot obrir el fitxer"
|
||||
STR_FIRMWARE_WRITE_FAILED: "Ha fallat l'escriptura del firmware"
|
||||
|
||||
@@ -306,6 +306,7 @@ STR_OPDS_BROWSER: "OPDS Browser"
|
||||
STR_SEARCH: "Search"
|
||||
STR_COVER_CUSTOM: "Cover + Custom"
|
||||
STR_QUICK_RESUME: "Quick Resume"
|
||||
STR_TRANSPARENT: "Transparent"
|
||||
STR_MENU_RECENT_BOOKS: "Recent Books"
|
||||
STR_REMOVE_FROM_RECENTS: "Remove from Recent Books?"
|
||||
STR_NO_RECENT_BOOKS: "No recent books"
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
_language_name: "Orangutan"
|
||||
_language_code: "OOK"
|
||||
_order: "34"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "OOK"
|
||||
STR_SLEEPING: "Oooook"
|
||||
STR_ENTERING_SLEEP: "Ook..."
|
||||
STR_BROWSE_FILES: "Ook? OOK"
|
||||
STR_FILE_TRANSFER: "Oook!"
|
||||
STR_SETTINGS_TITLE: "OOK!"
|
||||
STR_CONTINUE_READING: "Oooook ook"
|
||||
STR_RESUME: "OOk!"
|
||||
STR_NO_OPEN_BOOK: "OOK?"
|
||||
STR_START_READING: "Ook ook"
|
||||
STR_NO_FILES_FOUND: "Oook?"
|
||||
STR_SELECT_CHAPTER: "Ook? Oook OOK!"
|
||||
STR_NO_CHAPTERS: "Ook..."
|
||||
STR_END_OF_BOOK: "Oooook."
|
||||
STR_EMPTY_CHAPTER: "OOk?"
|
||||
STR_INDEXING: "Oook..."
|
||||
STR_LOADING: "Ooook..."
|
||||
STR_LOADING_POPUP: "Oook"
|
||||
STR_LOOKUP: "Ook? Oook! OOK"
|
||||
STR_WIFI_NETWORKS: "Wi-Fi OOK"
|
||||
STR_NO_NETWORKS: "Oook?"
|
||||
STR_NETWORKS_FOUND: "%zu OOK"
|
||||
STR_SCANNING: "Oook..."
|
||||
STR_FINDING_SAVED_WIFI: "Oook Wi-Fi..."
|
||||
STR_CONNECTING: "Ooook..."
|
||||
STR_CONNECTING_SAVED_WIFI: "OOk Wi-Fi..."
|
||||
STR_SHOW_NETWORKS: "OOK"
|
||||
STR_CONNECTED: "Oook!"
|
||||
STR_CONNECTION_FAILED: "OOk?"
|
||||
STR_FORGET_NETWORK: "Oook?"
|
||||
STR_SAVE_PASSWORD: "OOK?"
|
||||
STR_PRESS_OK_SCAN: "OOk OK"
|
||||
STR_JOIN_NETWORK: "Oook!"
|
||||
STR_CREATE_HOTSPOT: "Ooook"
|
||||
STR_JOIN_DESC: "Oook Wi-Fi"
|
||||
STR_HOTSPOT_DESC: "Oooook Wi-Fi"
|
||||
STR_STARTING_HOTSPOT: "Ooook..."
|
||||
STR_HOTSPOT_MODE: "OOk!"
|
||||
STR_CONNECT_WIFI_HINT: "Oook Wi-Fi"
|
||||
STR_OPEN_URL_HINT: "Oook URL"
|
||||
STR_SCAN_QR_HINT: "Oook QR:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre OOK"
|
||||
STR_NETWORK_LEGEND: "* = OOK | + = OOk"
|
||||
STR_MAC_ADDRESS: "MAC!"
|
||||
STR_CHECKING_WIFI: "OOk Wi-Fi..."
|
||||
STR_ENTER_WIFI_PASSWORD: "Oook Wi-Fi"
|
||||
STR_ADD_HIDDEN_NETWORK: "Oook..."
|
||||
STR_ENTER_WIFI_SSID: "OOk (SSID)"
|
||||
STR_TO_PREFIX: "Oo "
|
||||
STR_CALIBRE_RECEIVING: "Oook: "
|
||||
STR_CALIBRE_RECEIVED: "OOK!: "
|
||||
STR_CAT_DISPLAY: "OOK"
|
||||
STR_CAT_READER: "OOk"
|
||||
STR_CAT_CONTROLS: "Ook!"
|
||||
STR_CAT_SYSTEM: "Oook"
|
||||
STR_SLEEP_SCREEN: "Oooook"
|
||||
STR_NIGHT_MODE: "Ook!"
|
||||
STR_QUICK_RESUME_TIMEOUT: "Oook ook"
|
||||
STR_SLEEP_COVER_MODE: "Ook ook"
|
||||
STR_HIDE_BATTERY: "OOk %"
|
||||
STR_EXTRA_SPACING: "Oook ook"
|
||||
STR_TEXT_AA: "Ook-ook"
|
||||
STR_IMAGES: "OOk!"
|
||||
STR_IMAGES_DISPLAY: "Oook"
|
||||
STR_IMAGES_PLACEHOLDER: "Ooook"
|
||||
STR_IMAGES_SUPPRESS: "OOK"
|
||||
STR_EOB_HOME: "Oook"
|
||||
STR_EOB_CONTINUE_WITH: "Oook ook"
|
||||
STR_SHORT_PWR_BTN: "Oooook OOK"
|
||||
STR_ORIENTATION: "Ook? Oooook OOK"
|
||||
STR_SIDE_BTN_LAYOUT: "Oook ook"
|
||||
STR_TOUCH_READER_CONTROLS: "OOk! OOK"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Oook OOK"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Oooook OOK"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "OFF"
|
||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Oook!"
|
||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Ook? OOK"
|
||||
STR_LONG_PRESS_MENU: "Oooook!"
|
||||
STR_FONT_PREVIEW_TEXT: "Ook ook ook."
|
||||
STR_FONT_FAMILY: "Oook OOK"
|
||||
STR_FONT_SIZE: "OOk OOK"
|
||||
STR_TEXT_SETTINGS: "Oooook OOK: Ook!"
|
||||
STR_FONT: "OOk!"
|
||||
STR_SIZE: "OOK"
|
||||
STR_LAYOUT: "Oook"
|
||||
STR_ALIGNMENT: "Oooook"
|
||||
STR_STYLE: "OOk!"
|
||||
STR_NOT_IN_PREVIEW: "Oook?"
|
||||
STR_LINE_SPACING: "Oook ook"
|
||||
STR_SCREEN_MARGIN: "OOk ook"
|
||||
STR_PARA_ALIGNMENT: "Ook OOK"
|
||||
STR_HYPHENATION: "Ooook"
|
||||
STR_TIME_TO_SLEEP: "Oook..."
|
||||
STR_SHOW_HIDDEN_FILES: "OOk?"
|
||||
STR_REMOVE_READ_FROM_RECENTS: "Oook OOK"
|
||||
STR_MOVE_FINISHED_TO_READ: "Ook! OOK"
|
||||
STR_REFRESH_FREQ: "Oook!"
|
||||
STR_KOREADER_SYNC: "KOReader OOK"
|
||||
STR_CHECK_UPDATES: "Oook!"
|
||||
STR_LANGUAGE: "OOk?"
|
||||
STR_CLEAR_READING_CACHE: "Oook OOK"
|
||||
STR_USERNAME: "Oook"
|
||||
STR_PASSWORD: "OOk!"
|
||||
STR_SYNC_SERVER_URL: "Oook URL"
|
||||
STR_DOCUMENT_MATCHING: "Oook OOK"
|
||||
STR_SEND_METADATA: "Ook! OOK"
|
||||
STR_SYNC_BEHAVIOR: "OOk ook"
|
||||
STR_ASK_EVERY_TIME: "Oook?"
|
||||
STR_SMART_SYNC: "Oook!"
|
||||
STR_AUTHENTICATE: "Oooook"
|
||||
STR_SIGN_UP: "OOk!"
|
||||
STR_KOREADER_USERNAME: "KOReader OOK"
|
||||
STR_KOREADER_PASSWORD: "KOReader OOk"
|
||||
STR_FILENAME: "Oook"
|
||||
STR_KOREADER_AUTH: "KOReader OOK"
|
||||
STR_DONE: "Oook"
|
||||
STR_DARK: "OOk!"
|
||||
STR_LIGHT: "Oook"
|
||||
STR_CUSTOM: "Ooook"
|
||||
STR_COVER: "OOk?"
|
||||
STR_NONE_OPT: "OOk!"
|
||||
STR_FIT: "OOk"
|
||||
STR_CROP: "OOK!"
|
||||
STR_NEVER: "Oook!"
|
||||
STR_IN_READER: "Ook? OOK"
|
||||
STR_ALWAYS: "Oooook"
|
||||
STR_IGNORE: "OOk?"
|
||||
STR_SLEEP: "Oook!"
|
||||
STR_PAGE_TURN: "Oook OOK"
|
||||
STR_FORCE_REFRESH: "Ook! OOK"
|
||||
STR_PORTRAIT: "Oook"
|
||||
STR_LANDSCAPE_CW: "Oook OOK"
|
||||
STR_INVERTED: "OOk!"
|
||||
STR_ORIENTATION_INVERTED: "Ook 180°"
|
||||
STR_LANDSCAPE_CCW: "OOk OOK"
|
||||
STR_PREV_NEXT: "OOk/OOK"
|
||||
STR_NEXT_PREV: "OOK/OOk"
|
||||
STR_KOSYNC: "KOSync"
|
||||
STR_BOOKMARK_OPTION: "Oook!"
|
||||
STR_DICTIONARY: "Oooook"
|
||||
STR_DISABLED: "OOk!"
|
||||
STR_NOTO_SERIF: "Noto OOK"
|
||||
STR_NOTO_SANS: "Noto OOk"
|
||||
STR_TIGHT: "OOk!"
|
||||
STR_NORMAL: "Oook"
|
||||
STR_WIDE: "OOk!"
|
||||
STR_JUSTIFY: "Ooook!"
|
||||
STR_ALIGN_LEFT: "OOk"
|
||||
STR_CENTER: "Oook"
|
||||
STR_ALIGN_RIGHT: "OOK!"
|
||||
STR_PAGES_1: "1 OOK"
|
||||
STR_PAGES_5: "5 OOk"
|
||||
STR_PAGES_10: "10 OOK"
|
||||
STR_PAGES_15: "15 OOk"
|
||||
STR_PAGES_30: "30 OooK"
|
||||
|
||||
STR_UPDATE: "Oook!"
|
||||
STR_CURRENT_VERSION: "OOK: "
|
||||
STR_NEW_VERSION: "OOk: "
|
||||
STR_DOWNLOADING: "Oooook..."
|
||||
STR_UNNAMED: "OOk?"
|
||||
STR_HOLD_OPEN_TO_DELETE: "Oook OOK"
|
||||
STR_NEXT_PAGE: "OOk »"
|
||||
STR_PREV_PAGE: "« OOK"
|
||||
STR_NETWORK_PREFIX: "OOk: "
|
||||
STR_IP_ADDRESS_PREFIX: "IP: "
|
||||
STR_SD_CARD: "SD OOK"
|
||||
STR_BACK: "« OOK"
|
||||
STR_EXIT: "« OOk"
|
||||
STR_HOME: "« Oook"
|
||||
STR_SELECT: "OOk!"
|
||||
STR_SELECTED: "Oooook"
|
||||
STR_TOGGLE: "Oook!"
|
||||
STR_TOGGLE_BOOKMARK: "Ook? OOK! Oooook"
|
||||
STR_CONFIRM: "Oooook"
|
||||
STR_CANCEL: "OOk?"
|
||||
STR_CONNECT: "Oooook"
|
||||
STR_OPEN: "Oook"
|
||||
STR_DOWNLOAD: "Oooook"
|
||||
STR_RETRY: "Oook!"
|
||||
STR_YES: "OOk"
|
||||
STR_NO: "Oo"
|
||||
STR_SHOW: "Oook"
|
||||
STR_HIDE: "OOk!"
|
||||
STR_STATE_ON: "OK"
|
||||
STR_STATE_OFF: "OOk"
|
||||
STR_NOT_SET: "Oook"
|
||||
STR_DIR_LEFT: "OOk!"
|
||||
STR_DIR_RIGHT: "OOK!"
|
||||
STR_DIR_UP: "OK"
|
||||
STR_DIR_DOWN: "OOk!"
|
||||
STR_OK_BUTTON: "OK"
|
||||
STR_KEY_SHIFT: "OOk!"
|
||||
STR_KEY_MODE_SYMBOLS: "?123"
|
||||
STR_KEY_MODE_ABC: "abc"
|
||||
|
||||
STR_SLEEP_COVER_FILTER: "Oook OOK"
|
||||
STR_FILTER_CONTRAST: "Oooook"
|
||||
STR_CUSTOMISE_STATUS_BAR: "Oook OOK"
|
||||
STR_CHAPTER_PAGE_COUNT: "OOk OOK"
|
||||
STR_BOOK_PROGRESS_PERCENTAGE: "Oook OOK"
|
||||
STR_PROGRESS_BAR: "Oook OOK"
|
||||
STR_PROGRESS_BAR_THICKNESS: "Oook OOK"
|
||||
STR_PROGRESS_BAR_THIN: "OOk!"
|
||||
STR_PROGRESS_BAR_MEDIUM: "Oook"
|
||||
STR_PROGRESS_BAR_THICK: "Oook!"
|
||||
STR_BOOK: "Oook"
|
||||
STR_CHAPTER: "OOk!"
|
||||
STR_EXAMPLE_CHAPTER: "OOk 21"
|
||||
STR_EXAMPLE_BOOK: "Oook OOK"
|
||||
STR_PREVIEW: "Oook!"
|
||||
STR_TITLE: "OOk?"
|
||||
STR_BATTERY: "Oook"
|
||||
STR_XTC_STATUS_BAR: "XTC OOK"
|
||||
STR_BOTTOM: "Oooook"
|
||||
STR_TOP: "OOk"
|
||||
STR_CLOCK: "Oook!"
|
||||
STR_CLOCK_UTC_OFFSET: "Oook OOK"
|
||||
STR_CLOCK_FORMAT: "OOk OOK"
|
||||
STR_CLOCK_FORMAT_24H: "24-OOK"
|
||||
STR_CLOCK_FORMAT_12H: "12-OOk"
|
||||
STR_CURRENT_TIME: "OOk:"
|
||||
STR_NEXT_FIELD: "Oook"
|
||||
STR_CLOCK_SYNC: "OOk OOK"
|
||||
STR_CLOCK_SYNC_NOW: "Oook OOK"
|
||||
STR_CLOCK_SYNCING: "Oooook..."
|
||||
STR_CLOCK_SYNC_OK: "Oook!"
|
||||
STR_CLOCK_SYNC_FAIL: "OOk?"
|
||||
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi OOK"
|
||||
STR_CLOCK_SYNCED: "Oooook"
|
||||
STR_UI_THEME: "OOk OOK"
|
||||
STR_THEME_CLASSIC: "Oook!"
|
||||
STR_THEME_LYRA: "OOk!"
|
||||
STR_THEME_ROUNDEDRAFF: "Oook"
|
||||
STR_THEME_LYRA_EXTENDED: "Oook OOK"
|
||||
STR_SUNLIGHT_FADING_FIX: "Oook OOK"
|
||||
STR_REMAP_FRONT_BUTTONS: "Oook OOK"
|
||||
|
||||
STR_BOOKMARKS: "Ook! OOK Oooook"
|
||||
STR_NO_BOOKMARKS: "OOk?"
|
||||
STR_BOOKMARK_ADDED: "Ook!"
|
||||
STR_BOOKMARK_REMOVED: "Oook!"
|
||||
STR_OPDS_BROWSER: "OPDS OOK"
|
||||
STR_SEARCH: "Oook!"
|
||||
STR_COVER_CUSTOM: "OOk + OOK"
|
||||
STR_QUICK_RESUME: "Oook OOK"
|
||||
STR_MENU_RECENT_BOOKS: "Oook OOK"
|
||||
STR_REMOVE_FROM_RECENTS: "OOk?"
|
||||
STR_NO_RECENT_BOOKS: "Oook?"
|
||||
STR_CALIBRE_DESC: "Calibre OOK"
|
||||
STR_FORGET_AND_REMOVE: "Oook?"
|
||||
STR_FORGET_BUTTON: "Oook!"
|
||||
STR_CALIBRE_STARTING: "Calibre OOK"
|
||||
STR_CALIBRE_SETUP: "OOk!"
|
||||
STR_CALIBRE_STATUS: "Oook"
|
||||
STR_CLEAR_BUTTON: "Oook!"
|
||||
STR_DEFAULT_VALUE: "OOk OOK"
|
||||
STR_UNASSIGNED: "OOk?"
|
||||
STR_ALREADY_ASSIGNED: "Oook OOK"
|
||||
STR_GO_TO_PERCENT: "Oook! Ook? %"
|
||||
STR_GO_HOME_BUTTON: "Oook OOK! Oooook"
|
||||
STR_SYNC_PROGRESS: "Oooook OOK! Ook!"
|
||||
STR_DELETE_CACHE: "Oook OOK? Oooook!"
|
||||
STR_DELETE: "Oooook"
|
||||
STR_CONFIRM_DELETE_BOOKMARK: "Oook?"
|
||||
STR_DISPLAY_QR: "Oooook OOK QR!"
|
||||
STR_CHAPTER_PREFIX: "OOk: "
|
||||
STR_BOOK_PREFIX: "Oook: "
|
||||
STR_SYNCING_TIME: "Oook..."
|
||||
STR_FETCH_PROGRESS: "OOk..."
|
||||
STR_UPLOAD_PROGRESS: "Oooook..."
|
||||
STR_KOREADER_SETUP_HINT: "KOReader OOK"
|
||||
STR_PROGRESS_FOUND: "Oook!"
|
||||
STR_REMOTE_LABEL: "OOk:"
|
||||
STR_LOCAL_LABEL: "Oook:"
|
||||
STR_PAGE_OVERALL_FORMAT: "OOk %d, %.2f%%"
|
||||
STR_PAGE_TOTAL_OVERALL_FORMAT: "Oook %d/%d, %.2f%%"
|
||||
STR_DEVICE_FROM_FORMAT: "OOk: %s"
|
||||
STR_APPLY_REMOTE: "Oook OOK"
|
||||
STR_UPLOAD_LOCAL: "OOk OOK"
|
||||
STR_UPLOAD_PROMPT: "Oook?"
|
||||
STR_UPLOAD_SUCCESS: "Oooook!"
|
||||
STR_ALREADY_SYNCED: "Oook!"
|
||||
STR_SECTION_PREFIX: "OOk "
|
||||
STR_UPLOAD: "Oooook"
|
||||
STR_BOOK_S_STYLE: "Oook OOK"
|
||||
STR_EMBEDDED_STYLE: "OOk OOK"
|
||||
STR_FOCUS_READING: "Oook OOK"
|
||||
STR_OPDS_SERVER_URL: "OPDS OOK"
|
||||
STR_PWR_BTN_FOOTNOTE_BACK: "Oook OOK"
|
||||
STR_BACK_SHORT_TO_FILE_BROWSER: "OOk OOK"
|
||||
STR_SET_SLEEP_COVER: "Oook OOK"
|
||||
STR_FOOTNOTES: "Ook! Oook OOK"
|
||||
STR_NO_FOOTNOTES: "Oook?"
|
||||
STR_LINK: "[Ook]"
|
||||
STR_SCREENSHOT_BUTTON: "Oook! OOK? Ook!"
|
||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u OOK"
|
||||
STR_SLEEP_NEVER: "Oook!"
|
||||
STR_ADD_SERVER: "Oook!"
|
||||
STR_SERVER_NAME: "OOk OOK"
|
||||
STR_DELETE_SERVER: "Oook OOK"
|
||||
STR_OPDS_SERVERS: "OPDS OOK"
|
||||
STR_OPDS_DOWNLOAD_FOLDER: "Oook OOK"
|
||||
STR_OPDS_FILENAME_FORMAT: "OOk OOK"
|
||||
STR_FMT_AUTHOR_TITLE: "OOk - OOK"
|
||||
STR_FMT_TITLE_AUTHOR: "OOK - OOk"
|
||||
STR_FMT_TITLE: "Oook"
|
||||
STR_OPDS_SD_ROOT: "SD OOK"
|
||||
STR_AUTO_TURN_ENABLED: "OOk: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Oooook OOK (Ook!)"
|
||||
STR_MANAGE_FONTS: "Oook OOK"
|
||||
STR_FONT_BROWSER: "OOk OOK"
|
||||
STR_LOADING_FONT_LIST: "Oooook..."
|
||||
STR_NO_FONTS_AVAILABLE: "Oook?"
|
||||
STR_FONT_INSTALLED: "Oook!"
|
||||
STR_INSTALLED: "OOk!"
|
||||
STR_DOWNLOAD_ALL: "Oook OOK"
|
||||
STR_UPDATE_ALL: "OOk OOK"
|
||||
STR_UPDATE_AVAILABLE: "Oook!"
|
||||
STR_TILT_PAGE_TURN: "Oook OOK"
|
||||
STR_SD_FIRMWARE_UPDATE: "SD Oooook Ooook Ooooook"
|
||||
STR_STEP_HINT_FRONT: "Ooook Ooooook:"
|
||||
STR_STEP_HINT_SIDE: "OOK Oook:"
|
||||
@@ -297,12 +297,14 @@ STR_SUNLIGHT_FADING_FIX: "Corrección de desvanecimiento"
|
||||
STR_QUICK_RESUME_TIMEOUT: "Reanudación rápida tras tiempo"
|
||||
STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales"
|
||||
STR_BOOKMARKS: "Marcadores"
|
||||
STR_NO_BOOKMARKS: "No hay marcadores"
|
||||
STR_BOOKMARK_ADDED: "Marcador añadido."
|
||||
STR_BOOKMARK_REMOVED: "Marcador eliminado."
|
||||
STR_OPDS_BROWSER: "Navegador OPDS"
|
||||
STR_SEARCH: "Buscar"
|
||||
STR_COVER_CUSTOM: "Portada + Pers."
|
||||
STR_QUICK_RESUME: "Reanudación rápida"
|
||||
STR_TRANSPARENT: "Transparente"
|
||||
STR_MENU_RECENT_BOOKS: "Libros recientes"
|
||||
STR_REMOVE_FROM_RECENTS: "¿Eliminar de Libros recientes?"
|
||||
STR_NO_RECENT_BOOKS: "No hay libros recientes"
|
||||
@@ -422,6 +424,7 @@ STR_VALIDATING_FIRMWARE: "Validando el firmware..."
|
||||
STR_INVALID_FIRMWARE: "Archivo de firmware no válido"
|
||||
STR_FIRMWARE_TOO_LARGE: "Firmware demasiado grande p/la partición"
|
||||
STR_FIRMWARE_TOO_SMALL: "Firmware demasiado pequeño"
|
||||
STR_FIRMWARE_WRONG_DEVICE: "El firmware es para otro dispositivo"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "¿Actualizar el firmware?"
|
||||
STR_FIRMWARE_FILE_OPEN_FAILED: "No se puede abrir el archivo"
|
||||
STR_FIRMWARE_WRITE_FAILED: "Falló la escritura del firmware"
|
||||
|
||||
@@ -381,6 +381,7 @@ 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_FIRMWARE_WRONG_DEVICE: "Firmware farklı bir cihaz için"
|
||||
STR_FONT_BROWSER: "Yazı Tipi Tarayıcısı"
|
||||
STR_FONT_INSTALLED: "Yazı tipi yüklendi!"
|
||||
STR_FONT_INSTALL_FAILED: "Yazı tipi yüklenemedi"
|
||||
|
||||
@@ -278,11 +278,13 @@ STR_THEME_LYRA_EXTENDED: "Lyra Ampliat"
|
||||
STR_SUNLIGHT_FADING_FIX: "Correcció de l'esvaïment pel sol"
|
||||
STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals"
|
||||
STR_BOOKMARKS: "Punts de llibre"
|
||||
STR_NO_BOOKMARKS: "No hi ha punts de llibre"
|
||||
STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre."
|
||||
STR_BOOKMARK_REMOVED: "S'ha eliminat el punt de llibre."
|
||||
STR_OPDS_BROWSER: "Navegador OPDS"
|
||||
STR_COVER_CUSTOM: "Portada + Personalitzat"
|
||||
STR_QUICK_RESUME: "Represa ràpida"
|
||||
STR_TRANSPARENT: "Transparent"
|
||||
STR_MENU_RECENT_BOOKS: "Llibres recents"
|
||||
STR_REMOVE_FROM_RECENTS: "Vols eliminar-lo de Llibres recents?"
|
||||
STR_NO_RECENT_BOOKS: "No hi ha llibres recents"
|
||||
@@ -422,6 +424,7 @@ STR_VALIDATING_FIRMWARE: "S'està validant el firmware..."
|
||||
STR_INVALID_FIRMWARE: "Arxiu de firmware no vàlid"
|
||||
STR_FIRMWARE_TOO_LARGE: "El firmware és massa gran per a la partició"
|
||||
STR_FIRMWARE_TOO_SMALL: "L'arxiu de firmware és massa menut"
|
||||
STR_FIRMWARE_WRONG_DEVICE: "El firmware és per a un altre dispositiu"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "Vols actualitzar el firmware?"
|
||||
STR_FIRMWARE_FILE_OPEN_FAILED: "No es pot obrir l'arxiu"
|
||||
STR_FIRMWARE_WRITE_FAILED: "Ha fallat l'escriptura del firmware"
|
||||
|
||||
@@ -93,6 +93,10 @@ bool HalDisplay::isInverted() const { return einkDisplay.isInverted(); }
|
||||
|
||||
void HalDisplay::deepSleep() { einkDisplay.deepSleep(); }
|
||||
|
||||
void HalDisplay::setBusyWaitSliceHook(bool (*sliceHook)(int8_t busyPin, uint8_t busyLevel)) {
|
||||
einkDisplay.setBusyWaitSliceHook(sliceHook);
|
||||
}
|
||||
|
||||
uint8_t* HalDisplay::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); }
|
||||
|
||||
uint8_t* HalDisplay::lendFrameBufferStorage(uint32_t* sizeOut) { return einkDisplay.lendBuildStorage(sizeOut); }
|
||||
|
||||
@@ -61,6 +61,10 @@ class HalDisplay {
|
||||
// Power management
|
||||
void deepSleep();
|
||||
|
||||
// Install the slice hook that replaces the BUSY poll delay on proven-long
|
||||
// waits (see EpdBus::setBusyWaitSliceHook for the contract)
|
||||
void setBusyWaitSliceHook(bool (*sliceHook)(int8_t busyPin, uint8_t busyLevel));
|
||||
|
||||
// Access to frame buffer
|
||||
uint8_t* getFrameBuffer() const;
|
||||
|
||||
|
||||
+45
-2
@@ -6,6 +6,7 @@
|
||||
#include <Wire.h>
|
||||
#include <XteinkDetect.h>
|
||||
#include <esp_sleep.h>
|
||||
#include <soc/usb_serial_jtag_struct.h>
|
||||
|
||||
// Global HalGPIO instance
|
||||
HalGPIO gpio;
|
||||
@@ -140,11 +141,44 @@ void HalGPIO::begin() {
|
||||
|
||||
void HalGPIO::update() {
|
||||
inputMgr.update();
|
||||
const bool connected = isUsbConnected();
|
||||
updateUsbState(millis());
|
||||
}
|
||||
|
||||
void HalGPIO::updateUsbState(const unsigned long now) {
|
||||
// SOF-based host-link sampling (see the member comment). A cheap register
|
||||
// read, so it runs at its own short cadence on both devices and is never
|
||||
// behind the I2C throttle below — a fresh enumeration must cancel light
|
||||
// sleep within a poll or two, or the next slice kills the CDC link again.
|
||||
if (sofLastSampleMs == 0 || now - sofLastSampleMs >= SOF_SAMPLE_MS) {
|
||||
const auto sof = static_cast<uint16_t>(USB_SERIAL_JTAG.fram_num.sof_frame_index);
|
||||
usbSofActive = (sof != lastSofFrameIndex);
|
||||
lastSofFrameIndex = sof;
|
||||
sofLastSampleMs = now;
|
||||
}
|
||||
|
||||
// Throttle the X3's I2C-based USB detection; see USB_POLL_X3_MS. First call
|
||||
// (usbLastPollMs == 0) always polls so boot state is correct. The combined
|
||||
// verdict below is still recomputed every call so a SOF-detected attach is
|
||||
// not held back by the throttle window.
|
||||
if (usbLastPollMs == 0 || !deviceIsX3() || now - usbLastPollMs >= USB_POLL_X3_MS) {
|
||||
usbLastPollMs = now;
|
||||
usbElectricalConnected = isUsbElectricalConnected();
|
||||
}
|
||||
const bool connected = usbSofActive || usbElectricalConnected;
|
||||
usbStateChanged = (connected != lastUsbConnected);
|
||||
lastUsbConnected = connected;
|
||||
}
|
||||
|
||||
void HalGPIO::pollUsbState() {
|
||||
// Wait out the SOF sample floor so the comparison sees a real frame delta:
|
||||
// two reads inside one USB frame compare equal and read as "no host".
|
||||
const unsigned long elapsed = millis() - sofLastSampleMs;
|
||||
if (sofLastSampleMs != 0 && elapsed < SOF_SAMPLE_MS) {
|
||||
delay(SOF_SAMPLE_MS - elapsed);
|
||||
}
|
||||
updateUsbState(millis());
|
||||
}
|
||||
|
||||
bool HalGPIO::wasUsbStateChanged() const { return usbStateChanged; }
|
||||
|
||||
bool HalGPIO::isPressed(uint8_t buttonIndex) const { return inputMgr.isPressed(buttonIndex); }
|
||||
@@ -157,6 +191,8 @@ bool HalGPIO::wasReleased(uint8_t buttonIndex) const { return inputMgr.wasReleas
|
||||
|
||||
bool HalGPIO::wasAnyReleased() const { return inputMgr.wasAnyReleased(); }
|
||||
|
||||
bool HalGPIO::isDebouncePending() const { return inputMgr.isDebouncePending(); }
|
||||
|
||||
unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); }
|
||||
|
||||
unsigned long HalGPIO::getPowerButtonHeldTime() const { return inputMgr.getPowerButtonHeldTime(); }
|
||||
@@ -244,9 +280,16 @@ bool HalGPIO::verifyPowerButtonWakeup(uint16_t requiredDurationMs, bool shortPre
|
||||
}
|
||||
|
||||
bool HalGPIO::isUsbConnected() const {
|
||||
// Recent SOF activity means an enumerated host regardless of what the
|
||||
// electrical check says (false at boot until update() has sampled twice).
|
||||
return usbSofActive || isUsbElectricalConnected();
|
||||
}
|
||||
|
||||
bool HalGPIO::isUsbElectricalConnected() const {
|
||||
if (deviceIsX3()) {
|
||||
// X3: infer USB/charging via BQ27220 Current() register (0x0C, signed mA).
|
||||
// Positive current means charging.
|
||||
// Positive current means charging. Misses a data-only cable and a full
|
||||
// battery — the SOF check in update() covers those.
|
||||
for (uint8_t attempt = 0; attempt < 2; ++attempt) {
|
||||
int16_t currentMa = 0;
|
||||
if (X3GPIO::readBQ27220CurrentMA(¤tMa)) {
|
||||
|
||||
@@ -45,6 +45,35 @@ class HalGPIO {
|
||||
|
||||
bool lastUsbConnected = false;
|
||||
bool usbStateChanged = false;
|
||||
unsigned long usbLastPollMs = 0;
|
||||
bool usbElectricalConnected = false; // last result of the per-device electrical/charge check
|
||||
|
||||
// X3 USB detection is a BQ27220 I2C read (~0.3-1 ms of awake CPU per call);
|
||||
// polled every loop it costs a few percent of the light-sleep idle floor for
|
||||
// nothing. At >=1 s intervals the energy cost is unmeasurable (~µC/s), so 1 s
|
||||
// is chosen for prompt plug/unplug UX (battery icon, light-sleep USB guard /
|
||||
// CDC recovery). X4 detection is a single digitalRead and stays per-loop.
|
||||
static constexpr unsigned long USB_POLL_X3_MS = 1000;
|
||||
|
||||
// USB-Serial-JTAG SOF activity, sampled by update(): the host sends a SOF
|
||||
// frame every 1 ms while the bus is enumerated, so a frame index that moved
|
||||
// between two samples means a live host link. Catches what the charge-based
|
||||
// X3 check misses: a data-only cable, and any cable once the battery is full
|
||||
// (charge current ~0). Samples must be >SOF_SAMPLE_MS apart — update() can be
|
||||
// called back-to-back (inner input loops), and adjacent reads would compare
|
||||
// equal and flicker the verdict.
|
||||
uint16_t lastSofFrameIndex = 0;
|
||||
unsigned long sofLastSampleMs = 0;
|
||||
bool usbSofActive = false;
|
||||
static constexpr unsigned long SOF_SAMPLE_MS = 10;
|
||||
|
||||
// Per-device electrical/charge-inference USB check (fresh read; X3 = BQ27220
|
||||
// charge current over I2C, X4 = VBUS-driven level on GPIO20).
|
||||
bool isUsbElectricalConnected() const;
|
||||
|
||||
// Shared body of update()/pollUsbState(): SOF sampling + throttled electrical
|
||||
// check + combined-verdict edge tracking.
|
||||
void updateUsbState(unsigned long now);
|
||||
|
||||
public:
|
||||
enum class DeviceType : uint8_t { X4, X3 };
|
||||
@@ -76,6 +105,11 @@ class HalGPIO {
|
||||
bool wasAnyPressed() const;
|
||||
bool wasReleased(uint8_t buttonIndex) const;
|
||||
bool wasAnyReleased() const;
|
||||
// True while a raw button-state change is still inside the debounce window.
|
||||
// The idle loop polls fast while this is set so the confirming sample lands
|
||||
// ~10 ms after the first; at the 50 ms light-sleep cadence a short tap can
|
||||
// otherwise appear in a single sample and never commit (dropped press).
|
||||
bool isDebouncePending() const;
|
||||
unsigned long getHeldTime() const;
|
||||
unsigned long getPowerButtonHeldTime() const;
|
||||
bool hasTouch() const;
|
||||
@@ -108,6 +142,19 @@ class HalGPIO {
|
||||
// Check if USB is connected
|
||||
bool isUsbConnected() const;
|
||||
|
||||
// Sample USB state without a full input update. Called during setup() BEFORE
|
||||
// the first e-ink refresh: the boot paint happens before loop() ever runs
|
||||
// update(), so without this the light-sleep slice guards see the unsampled
|
||||
// default ("no USB") and sleeping through the boot refresh kills a live CDC
|
||||
// link (charge-based X3 detection also reads false whenever the battery is
|
||||
// full). Two calls >=SOF_SAMPLE_MS apart establish the SOF verdict; the
|
||||
// method itself waits out the floor if called too soon after the last sample.
|
||||
void pollUsbState();
|
||||
|
||||
// USB state as sampled by the last update() call.
|
||||
// Prefer this in per-loop polling: isUsbConnected() performs a fresh I2C read on X3.
|
||||
bool isUsbConnectedCached() const { return lastUsbConnected; }
|
||||
|
||||
// Returns true once per edge (plug or unplug) since the last update()
|
||||
bool wasUsbStateChanged() const;
|
||||
|
||||
|
||||
+175
-7
@@ -4,6 +4,7 @@
|
||||
#include <Logging.h>
|
||||
#include <PowerManager.h>
|
||||
#include <WiFi.h>
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_sleep.h>
|
||||
#include <soc/soc_caps.h>
|
||||
|
||||
@@ -13,13 +14,21 @@
|
||||
|
||||
HalPowerManager powerManager; // Singleton instance
|
||||
|
||||
// GPIO13 must stay high during light sleep on the C3 Xteink boards: it controls
|
||||
// the X4 battery latch and the X3 SD power rail. Other boards use it for
|
||||
// unrelated signals, including the X4 Pro display chip select.
|
||||
static constexpr gpio_num_t XTEINK_C3_GPIO13 = GPIO_NUM_13;
|
||||
|
||||
void HalPowerManager::begin() {
|
||||
if (BoardConfig::ACTIVE.batteryAdc >= 0) {
|
||||
pinMode(BoardConfig::ACTIVE.batteryAdc, INPUT);
|
||||
}
|
||||
mainLoopTask = xTaskGetCurrentTaskHandle(); // Arduino runs setup() on the loop task
|
||||
normalFreq = getCpuFrequencyMhz();
|
||||
modeMutex = xSemaphoreCreateMutex();
|
||||
assert(modeMutex != nullptr);
|
||||
sleepMutex = xSemaphoreCreateMutex();
|
||||
assert(sleepMutex != nullptr);
|
||||
}
|
||||
|
||||
void HalPowerManager::setPowerSaving(bool enabled) {
|
||||
@@ -67,13 +76,19 @@ void HalPowerManager::startDeepSleep(HalGPIO& gpio) const {
|
||||
#endif
|
||||
|
||||
#if !SOC_PM_SUPPORT_EXT1_WAKEUP
|
||||
if (gpio.isXteinkDevice() && !gpio.deviceIsX3()) {
|
||||
// X4 GPIO13 is connected to the battery latch MOSFET. Keeping it low powers
|
||||
// the MCU off on battery, while the SDK wake source still handles USB power.
|
||||
constexpr gpio_num_t GPIO_SPIWP = GPIO_NUM_13;
|
||||
gpio_set_direction(GPIO_SPIWP, GPIO_MODE_OUTPUT);
|
||||
gpio_set_level(GPIO_SPIWP, 0);
|
||||
gpio_hold_en(GPIO_SPIWP);
|
||||
if (gpio.isXteinkDevice()) {
|
||||
// GPIO13 gates the battery MOSFET on both Xteink C3 boards; driving it low
|
||||
// is the battery power-off (the SDK wake source still handles USB power).
|
||||
// Unlike upstream, X3 is included: lightSleep() pad-holds the latch HIGH
|
||||
// persistently while running (the IDF flash-leakage workaround would drop
|
||||
// it mid-slice, hard-powering the device off), so the implicit low that
|
||||
// upstream's X3 relies on never happens here — release the hold and drive
|
||||
// the latch low explicitly. The hold_en survives deep sleep via the SDK's
|
||||
// deepSleep() (esp_sleep_config_gpio_isolate + gpio_deep_sleep_hold_en).
|
||||
gpio_hold_dis(XTEINK_C3_GPIO13);
|
||||
gpio_set_direction(XTEINK_C3_GPIO13, GPIO_MODE_OUTPUT);
|
||||
gpio_set_level(XTEINK_C3_GPIO13, 0);
|
||||
gpio_hold_en(XTEINK_C3_GPIO13);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -91,6 +106,159 @@ void HalPowerManager::startDeepSleep(HalGPIO& gpio) const {
|
||||
freeink::PowerManager::deepSleepUntilPowerButton();
|
||||
}
|
||||
|
||||
bool HalPowerManager::lightSleep(const HalGPIO& gpio) const {
|
||||
// A performance Lock means a render (or similar) task is mid-flight; light sleep
|
||||
// freezes the whole chip, so it would stall that task.
|
||||
// Note: like setPowerSaving(), read without the mutex — stale in either
|
||||
// direction is acceptable: a stale lock delays sleeping by one 50 ms slice;
|
||||
// a Lock acquired after this check freezes that task for at most one slice
|
||||
// (timer wake; RAM/peripheral state retained), which is cheaper than
|
||||
// serializing Lock ctor/dtor against the entire sleep window.
|
||||
if (currentLockMode != None) {
|
||||
return false;
|
||||
}
|
||||
// Light sleep drops a WiFi association and kills an enumerated USB-CDC link.
|
||||
if (WiFi.getMode() != WIFI_MODE_NULL || gpio.isUsbConnectedCached()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Serialize wake-source arming with the render task's BUSY-wait slice (see
|
||||
// sleepMutex declaration for the failure mode this prevents).
|
||||
xSemaphoreTake(sleepMutex, portMAX_DELAY);
|
||||
|
||||
// Timer wake keeps the button/tilt poll cadence identical to the delay() it
|
||||
// replaces: the front/side buttons are ADC resistor-ladder inputs whose
|
||||
// pressed levels sit mid-rail, invisible to a digital GPIO wake, so they
|
||||
// must be polled. The power button (a true GPIO) is ALSO armed, as a level
|
||||
// wake at its pressed level: committing a press needs two update() samples
|
||||
// >=5 ms apart, so at the timer-only 50 ms cadence a tap shorter than a
|
||||
// slice could land in one sample — or none — and be dropped entirely
|
||||
// (field-reported as unreliable short-press sleep). The wake is only an
|
||||
// early poll: update()'s debounce still decides whether it was a real
|
||||
// press, so a misread around the sleep transition costs one extra wake
|
||||
// blip, never a phantom press. Idle cost is zero — the pin only holds its
|
||||
// pressed level while a finger is on it.
|
||||
esp_sleep_enable_timer_wakeup(static_cast<uint64_t>(LIGHT_SLEEP_SLICE_MS) * 1000ULL);
|
||||
const int8_t powerPin = BoardConfig::ACTIVE.input.power;
|
||||
if (powerPin >= 0) {
|
||||
gpio_wakeup_enable(static_cast<gpio_num_t>(powerPin),
|
||||
BoardConfig::ACTIVE.input.powerActiveHigh ? GPIO_INTR_HIGH_LEVEL : GPIO_INTR_LOW_LEVEL);
|
||||
esp_sleep_enable_gpio_wakeup();
|
||||
}
|
||||
|
||||
// The C3 flash-leakage workaround pulls its DIO-unused SPIWP pad low during
|
||||
// light sleep. Hold GPIO13 high only on the X3/X4 boards that use that pad as
|
||||
// a power control; on other boards GPIO13 may be a bus signal.
|
||||
if (gpio.isXteinkDevice()) {
|
||||
gpio_set_level(XTEINK_C3_GPIO13, 1);
|
||||
gpio_set_direction(XTEINK_C3_GPIO13, GPIO_MODE_OUTPUT);
|
||||
gpio_hold_en(XTEINK_C3_GPIO13);
|
||||
}
|
||||
|
||||
const esp_err_t err = esp_light_sleep_start();
|
||||
|
||||
// Disarm immediately: an armed timer wake persists across sleep calls and would
|
||||
// carry over into startDeepSleep(), waking the device on USB power after 50 ms.
|
||||
// gpio_wakeup_disable() clears only the wake-enable bit — the pin's LEVEL
|
||||
// interrupt type survives it. A leftover level type is live ammunition for
|
||||
// any later-installed GPIO ISR service: an asserted level feeding the
|
||||
// shared service with no per-pin handler registered re-enters the ISR
|
||||
// forever and livelocks the CPU. Clear the type explicitly.
|
||||
esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL);
|
||||
if (powerPin >= 0) {
|
||||
gpio_wakeup_disable(static_cast<gpio_num_t>(powerPin));
|
||||
gpio_set_intr_type(static_cast<gpio_num_t>(powerPin), GPIO_INTR_DISABLE);
|
||||
}
|
||||
|
||||
xSemaphoreGive(sleepMutex);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
// e.g. ESP_ERR_SLEEP_REJECT — we did not actually sleep
|
||||
LOG_DBG("PWR", "Light sleep rejected: %d", static_cast<int>(err));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HalPowerManager::onEinkBusyWaitSlice(const int8_t busyPin, const uint8_t busyLevel) {
|
||||
// Same exclusions as lightSleep(): light sleep drops a WiFi association and
|
||||
// kills an enumerated USB-CDC link. No LOG here — this runs ~50x/s mid-refresh.
|
||||
if (WiFi.getMode() != WIFI_MODE_NULL || gpio.isUsbConnectedCached()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mid-debounce: committing needs a second sample, so let the SDK's short poll
|
||||
// delay run instead of halting the chip. Same guard lightSleep() gets in loop().
|
||||
// Unsynchronized like the checks above; stale costs at most one slice.
|
||||
if (gpio.isDebouncePending()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
xSemaphoreTake(sleepMutex, portMAX_DELAY);
|
||||
|
||||
// Wake the instant BUSY leaves its active level (refresh complete). Level
|
||||
// wake (not edge) means an already-completed refresh returns immediately
|
||||
// instead of sleeping a full slice. The timer bound only exists so the main
|
||||
// loop gets scheduling windows to keep sampling the ADC-ladder buttons.
|
||||
const auto pin = static_cast<gpio_num_t>(busyPin);
|
||||
gpio_wakeup_enable(pin, busyLevel == HIGH ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL);
|
||||
// Also wake the instant the power button is pressed, so the main loop's poll
|
||||
// sees even sub-slice taps mid-refresh (same rationale and safety argument
|
||||
// as lightSleep(): the wake is an early poll, never a synthesized press).
|
||||
const int8_t powerPin = BoardConfig::ACTIVE.input.power;
|
||||
if (powerPin >= 0) {
|
||||
gpio_wakeup_enable(static_cast<gpio_num_t>(powerPin),
|
||||
BoardConfig::ACTIVE.input.powerActiveHigh ? GPIO_INTR_HIGH_LEVEL : GPIO_INTR_LOW_LEVEL);
|
||||
}
|
||||
esp_sleep_enable_gpio_wakeup();
|
||||
esp_sleep_enable_timer_wakeup(static_cast<uint64_t>(BUSY_SLEEP_SLICE_MS) * 1000ULL);
|
||||
|
||||
// Preserve the C3 Xteink GPIO13 power control during this sleep slice without
|
||||
// reconfiguring GPIO13 on boards where it is a bus signal.
|
||||
if (gpio.isXteinkDevice()) {
|
||||
gpio_set_level(XTEINK_C3_GPIO13, 1);
|
||||
gpio_set_direction(XTEINK_C3_GPIO13, GPIO_MODE_OUTPUT);
|
||||
gpio_hold_en(XTEINK_C3_GPIO13);
|
||||
}
|
||||
|
||||
const esp_err_t err = esp_light_sleep_start();
|
||||
|
||||
// Disarm everything armed above; an armed source persisting into
|
||||
// startDeepSleep() would wake the device on USB power (see lightSleep()).
|
||||
// As in lightSleep(): also clear the LEVEL interrupt types, which
|
||||
// gpio_wakeup_disable() leaves behind (see the livelock note there).
|
||||
esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL);
|
||||
gpio_wakeup_disable(pin);
|
||||
gpio_set_intr_type(pin, GPIO_INTR_DISABLE);
|
||||
if (powerPin >= 0) {
|
||||
gpio_wakeup_disable(static_cast<gpio_num_t>(powerPin));
|
||||
gpio_set_intr_type(static_cast<gpio_num_t>(powerPin), GPIO_INTR_DISABLE);
|
||||
}
|
||||
|
||||
xSemaphoreGive(sleepMutex);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
return false; // e.g. ESP_ERR_SLEEP_REJECT — fall back to the SDK's poll delay
|
||||
}
|
||||
|
||||
// Yield until the equal-priority main loop finishes an iteration, not just one
|
||||
// tick: one tick is less awake CPU than an iteration needs, so we would re-halt
|
||||
// the chip mid-poll. Capped so a loop parked on SD I/O can't hold the panel awake.
|
||||
const uint32_t seenIterations = mainLoopIterations;
|
||||
// Skipped entirely when the loop cannot make progress and the yield would just
|
||||
// burn the cap awake: parked in requestUpdateAndWait() on this very render;
|
||||
// never started (setup-time paints, iterations still 0); or this task IS the
|
||||
// loop task (direct displayBuffer calls from setup()/loop() busy-wait here).
|
||||
const bool loopCanPoll = !mainLoopBlocked && seenIterations != 0 && xTaskGetCurrentTaskHandle() != mainLoopTask;
|
||||
unsigned yieldTicks = 0;
|
||||
if (loopCanPoll) {
|
||||
for (; yieldTicks < SLICE_YIELD_MAX_TICKS && mainLoopIterations == seenIterations; ++yieldTicks) {
|
||||
vTaskDelay(1);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint16_t HalPowerManager::getBatteryPercentage() const {
|
||||
static const BatteryMonitor battery;
|
||||
if (BoardConfig::ACTIVE.batteryGauge.gaugeAddr != 0) {
|
||||
|
||||
@@ -23,6 +23,26 @@ class HalPowerManager {
|
||||
enum LockMode { None, NormalSpeed };
|
||||
LockMode currentLockMode = None;
|
||||
SemaphoreHandle_t modeMutex = nullptr; // Protect access to currentLockMode
|
||||
// Bumped by the main loop, read by the render task (see onEinkBusyWaitSlice).
|
||||
// Single writer, naturally aligned: the 32-bit access is atomic on RISC-V.
|
||||
volatile uint32_t mainLoopIterations = 0;
|
||||
|
||||
// Cap on how long one slice may stay awake waiting for that iteration.
|
||||
// Productive yields measure ~0.4 ms, so two ticks is ample headroom.
|
||||
static constexpr unsigned SLICE_YIELD_MAX_TICKS = 2;
|
||||
|
||||
// Set while the main loop is blocked in requestUpdateAndWait(). Captured in
|
||||
// begin(), which Arduino runs on the loop task, so the handle compare below
|
||||
// ignores any other task that might wait on a render.
|
||||
TaskHandle_t mainLoopTask = nullptr;
|
||||
volatile bool mainLoopBlocked = false;
|
||||
|
||||
// Serializes wake-source arm -> esp_light_sleep_start() -> disarm across the
|
||||
// two sleepers (main-loop lightSleep() and the render task's BUSY-wait slice).
|
||||
// Without it, lightSleep()'s deliberately unsynchronized lock-mode read could
|
||||
// let both tasks interleave arming/disarming: the loser's disarm-all can strip
|
||||
// the winner's wake sources and leave it sleeping with none armed.
|
||||
SemaphoreHandle_t sleepMutex = nullptr;
|
||||
|
||||
public:
|
||||
#if BOARD_HAS_PSRAM
|
||||
@@ -30,8 +50,20 @@ class HalPowerManager {
|
||||
#else
|
||||
static constexpr int LOW_POWER_FREQ = 10; // MHz
|
||||
#endif
|
||||
static constexpr unsigned long IDLE_POWER_SAVING_MS = 3000; // ms
|
||||
static constexpr unsigned long BATTERY_POLL_MS = 1500; // ms
|
||||
|
||||
// Two-stage idle backoff. Renders re-raise the clock via Lock regardless, so the
|
||||
// full-speed window only needs to cover rapid consecutive input (avoids clock
|
||||
// thrash); polling stays at 100 Hz until light sleep takes over the cadence.
|
||||
static constexpr unsigned long IDLE_DOWNCLOCK_MS = 500; // full speed -> LOW_POWER_FREQ
|
||||
static constexpr unsigned long IDLE_LIGHT_SLEEP_MS = 1000; // 100 Hz polling -> light sleep
|
||||
|
||||
static constexpr unsigned long BATTERY_POLL_MS = 1500; // ms
|
||||
static constexpr unsigned long LIGHT_SLEEP_SLICE_MS = 50; // ms
|
||||
|
||||
// Timer bound for one BUSY-wait light-sleep slice. The refresh end itself
|
||||
// wakes exactly via GPIO level wake on the BUSY pin; the timer only bounds
|
||||
// how coarse main-loop button sampling gets during a long refresh.
|
||||
static constexpr unsigned long BUSY_SLEEP_SLICE_MS = 20; // ms
|
||||
|
||||
void begin();
|
||||
|
||||
@@ -42,6 +74,37 @@ class HalPowerManager {
|
||||
// Should be called inside main loop() to handle the currentLockMode
|
||||
void startDeepSleep(HalGPIO& gpio) const;
|
||||
|
||||
// Light-sleep the CPU for LIGHT_SLEEP_SLICE_MS (timer wake; buttons are polled on
|
||||
// wake at the same cadence as the delay() this replaces). Returns false WITHOUT
|
||||
// sleeping when unsafe: a performance Lock is held (render in flight), WiFi is
|
||||
// active, or USB is connected (light sleep kills the CDC link). The caller must
|
||||
// fall back to delay() in that case.
|
||||
bool lightSleep(const HalGPIO& gpio) const;
|
||||
|
||||
// BUSY-wait slice hook (EpdBus::setBusyWaitSliceHook): light-sleep the chip
|
||||
// for up to BUSY_SLEEP_SLICE_MS while the panel refreshes autonomously,
|
||||
// waking early the moment the BUSY pin leaves `busyLevel`. Returns false
|
||||
// WITHOUT sleeping when unsafe (WiFi active, USB connected, or sleep
|
||||
// rejected) — the SDK then falls back to its plain poll delay. Called from
|
||||
// the render task; safe by construction (the waiter IS the locked task).
|
||||
// Leaves the CPU clock alone: the chip is asleep for most of the wait, so a
|
||||
// downclock buys nothing and only slows the main loop's awake windows.
|
||||
bool onEinkBusyWaitSlice(int8_t busyPin, uint8_t busyLevel);
|
||||
|
||||
// Call at the end of every main-loop iteration; paces the slice hook's yield.
|
||||
// Read-then-assign, not ++: C++20 deprecates increment on a volatile lvalue.
|
||||
void noteMainLoopIteration() { mainLoopIterations = mainLoopIterations + 1; }
|
||||
|
||||
// Bracket a blocking wait on the render task (requestUpdateAndWait). While the
|
||||
// main loop is parked there it cannot poll, so the slice hook skips its yield
|
||||
// instead of burning the full cap awake once per slice for nothing.
|
||||
void noteRenderWaitBegin() {
|
||||
if (xTaskGetCurrentTaskHandle() == mainLoopTask) mainLoopBlocked = true;
|
||||
}
|
||||
void noteRenderWaitEnd() {
|
||||
if (xTaskGetCurrentTaskHandle() == mainLoopTask) mainLoopBlocked = false;
|
||||
}
|
||||
|
||||
// Get battery percentage (range 0-100)
|
||||
uint16_t getBatteryPercentage() const;
|
||||
|
||||
|
||||
+163
-66
@@ -32,6 +32,7 @@ import re
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
|
||||
@@ -66,6 +67,11 @@ def build_arg_parser() -> argparse.ArgumentParser:
|
||||
default="",
|
||||
help="Suppress lines containing this keyword (case-insensitive)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-graph",
|
||||
action="store_true",
|
||||
help="Disable the matplotlib memory graph (plain serial console + command prompt only)",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
@@ -121,6 +127,13 @@ data_lock: threading.Lock = threading.Lock() # Prevent reading while writing
|
||||
# Global shutdown flag
|
||||
shutdown_event = threading.Event()
|
||||
|
||||
# Command-ack handshake: the firmware answers every CMD: line with CMDACK:<cmd>
|
||||
# (a handler ran) or CMDERR:<reason>:<cmd> (e.g. unknown — includes commands
|
||||
# compiled out of the running build). The reader thread sets ack_event when
|
||||
# either arrives so input_worker can report success, rejection, or a timeout.
|
||||
ACK_TIMEOUT_S = 2.0
|
||||
ack_event = threading.Event()
|
||||
|
||||
# Initialize colors
|
||||
init(autoreset=True)
|
||||
|
||||
@@ -232,12 +245,43 @@ def parse_memory_line(line: str) -> tuple[int | None, int | None, int | None]:
|
||||
)
|
||||
|
||||
|
||||
def serial_worker(ser, kwargs: dict[str, str]) -> None:
|
||||
def reopen_serial(ser_holder: dict) -> bool:
|
||||
"""
|
||||
Close the (possibly dead) serial port and retry opening it until it comes back
|
||||
or shutdown is requested. Devices re-enumerate on reboot/deep-sleep, so the
|
||||
monitor must survive the port vanishing and reappearing. Returns True once
|
||||
reconnected, False if shutting down.
|
||||
"""
|
||||
try:
|
||||
ser_holder["ser"].close()
|
||||
except (OSError, serial.SerialException):
|
||||
pass
|
||||
print(f"{Fore.YELLOW}Serial disconnected - waiting for device to come back...{Style.RESET_ALL}")
|
||||
while not shutdown_event.is_set():
|
||||
try:
|
||||
# Same no-reset open as the initial connect: deassert DTR/RTS
|
||||
# before open() so the reconnect never reboots the device.
|
||||
ser = serial.Serial(None, ser_holder["baud"], timeout=0.1)
|
||||
ser.port = ser_holder["port"]
|
||||
ser.dtr = False
|
||||
ser.rts = False
|
||||
ser.open()
|
||||
ser_holder["ser"] = ser
|
||||
print(f"{Fore.GREEN}Reconnected to {ser_holder['port']}{Style.RESET_ALL}")
|
||||
return True
|
||||
except (OSError, serial.SerialException):
|
||||
time.sleep(0.25)
|
||||
return False
|
||||
|
||||
|
||||
def serial_worker(ser_holder: dict, kwargs: dict[str, str]) -> None:
|
||||
"""
|
||||
Runs in a background thread. Handles reading serial data, printing to console,
|
||||
updating memory usage data for graphing, and processing screenshot data.
|
||||
Automatically reconnects when the device reboots or is replugged.
|
||||
Monitors the global shutdown event for graceful termination.
|
||||
"""
|
||||
ser = ser_holder["ser"]
|
||||
print(f"{Fore.CYAN}--- Opening serial port ---{Style.RESET_ALL}")
|
||||
filter_keyword = kwargs.get("filter", "").lower()
|
||||
suppress = kwargs.get("suppress", "").lower()
|
||||
@@ -262,73 +306,88 @@ def serial_worker(ser, kwargs: dict[str, str]) -> None:
|
||||
|
||||
try:
|
||||
while not shutdown_event.is_set():
|
||||
if expecting_screenshot:
|
||||
data = ser.read(screenshot_size - len(screenshot_data))
|
||||
if not data:
|
||||
try:
|
||||
if expecting_screenshot:
|
||||
data = ser.read(screenshot_size - len(screenshot_data))
|
||||
if not data:
|
||||
continue
|
||||
screenshot_data += data
|
||||
if len(screenshot_data) == screenshot_size:
|
||||
if Image:
|
||||
img = Image.frombytes("1", (800, 480), screenshot_data)
|
||||
# We need to rotate the image because the raw data is in landscape mode
|
||||
img = img.transpose(Image.ROTATE_270)
|
||||
img.save("screenshot.bmp")
|
||||
print(
|
||||
f"{Fore.GREEN}Screenshot saved to screenshot.bmp{Style.RESET_ALL}"
|
||||
)
|
||||
else:
|
||||
with open("screenshot.raw", "wb") as f:
|
||||
f.write(screenshot_data)
|
||||
print(
|
||||
f"{Fore.GREEN}Screenshot saved to screenshot.raw (PIL not available){Style.RESET_ALL}"
|
||||
)
|
||||
expecting_screenshot = False
|
||||
screenshot_data = b""
|
||||
continue
|
||||
screenshot_data += data
|
||||
if len(screenshot_data) == screenshot_size:
|
||||
if Image:
|
||||
img = Image.frombytes("1", (800, 480), screenshot_data)
|
||||
# We need to rotate the image because the raw data is in landscape mode
|
||||
img = img.transpose(Image.ROTATE_270)
|
||||
img.save("screenshot.bmp")
|
||||
print(
|
||||
f"{Fore.GREEN}Screenshot saved to screenshot.bmp{Style.RESET_ALL}"
|
||||
)
|
||||
else:
|
||||
with open("screenshot.raw", "wb") as f:
|
||||
f.write(screenshot_data)
|
||||
print(
|
||||
f"{Fore.GREEN}Screenshot saved to screenshot.raw (PIL not available){Style.RESET_ALL}"
|
||||
)
|
||||
expecting_screenshot = False
|
||||
screenshot_data = b""
|
||||
else:
|
||||
try:
|
||||
raw_data = ser.readline().decode("utf-8", errors="replace")
|
||||
|
||||
if not raw_data:
|
||||
continue
|
||||
raw_data = ser.readline().decode("utf-8", errors="replace")
|
||||
|
||||
clean_line = raw_data.strip()
|
||||
if not clean_line:
|
||||
continue
|
||||
if not raw_data:
|
||||
continue
|
||||
|
||||
if clean_line.startswith("SCREENSHOT_START:"):
|
||||
screenshot_size = int(clean_line.split(":")[1])
|
||||
expecting_screenshot = True
|
||||
continue
|
||||
elif clean_line == "SCREENSHOT_END":
|
||||
continue # ignore
|
||||
clean_line = raw_data.strip()
|
||||
if not clean_line:
|
||||
continue
|
||||
|
||||
# Add PC timestamp
|
||||
pc_time = datetime.now().strftime("%H:%M:%S")
|
||||
formatted_line = re.sub(r"^\[\d+\]", f"[{pc_time}]", clean_line)
|
||||
# Command acks bypass filter/suppress: they are direct feedback
|
||||
# for a command the user just typed, never routine log noise.
|
||||
if clean_line.startswith("CMDACK:"):
|
||||
print(f"{Fore.GREEN}Command OK: {clean_line[len('CMDACK:'):]}{Style.RESET_ALL}")
|
||||
ack_event.set()
|
||||
continue
|
||||
if clean_line.startswith("CMDERR:"):
|
||||
print(f"{Fore.RED}Command REJECTED: {clean_line[len('CMDERR:'):]}{Style.RESET_ALL}")
|
||||
ack_event.set()
|
||||
continue
|
||||
|
||||
# Check for Memory Line
|
||||
if "[MEM]" in formatted_line:
|
||||
free_val, total_val, max_alloc_val = parse_memory_line(formatted_line)
|
||||
if free_val is not None and total_val is not None:
|
||||
with data_lock:
|
||||
time_data.append(pc_time)
|
||||
free_mem_data.append(free_val / 1024)
|
||||
total_mem_data.append(total_val / 1024)
|
||||
max_alloc_data.append((max_alloc_val or 0) / 1024)
|
||||
# Apply filters
|
||||
if filter_keyword and filter_keyword not in formatted_line.lower():
|
||||
continue
|
||||
if suppress and suppress in formatted_line.lower():
|
||||
continue
|
||||
# Print to console
|
||||
line_color = get_color_for_line(formatted_line)
|
||||
print(f"{line_color}{formatted_line}")
|
||||
if clean_line.startswith("SCREENSHOT_START:"):
|
||||
screenshot_size = int(clean_line.split(":")[1])
|
||||
expecting_screenshot = True
|
||||
continue
|
||||
elif clean_line == "SCREENSHOT_END":
|
||||
continue # ignore
|
||||
|
||||
except (OSError, UnicodeDecodeError):
|
||||
print(
|
||||
f"{Fore.RED}Device disconnected or data error.{Style.RESET_ALL}"
|
||||
)
|
||||
# Add PC timestamp
|
||||
pc_time = datetime.now().strftime("%H:%M:%S")
|
||||
formatted_line = re.sub(r"^\[\d+\]", f"[{pc_time}]", clean_line)
|
||||
|
||||
# Check for Memory Line
|
||||
if "[MEM]" in formatted_line:
|
||||
free_val, total_val, max_alloc_val = parse_memory_line(formatted_line)
|
||||
if free_val is not None and total_val is not None:
|
||||
with data_lock:
|
||||
time_data.append(pc_time)
|
||||
free_mem_data.append(free_val / 1024)
|
||||
total_mem_data.append(total_val / 1024)
|
||||
max_alloc_data.append((max_alloc_val or 0) / 1024)
|
||||
# Apply filters
|
||||
if filter_keyword and filter_keyword not in formatted_line.lower():
|
||||
continue
|
||||
if suppress and suppress in formatted_line.lower():
|
||||
continue
|
||||
# Print to console
|
||||
line_color = get_color_for_line(formatted_line)
|
||||
print(f"{line_color}{formatted_line}")
|
||||
|
||||
except (OSError, serial.SerialException):
|
||||
# Device rebooted, deep-slept, or was replugged: drop any partial
|
||||
# screenshot transfer and wait for the port to come back.
|
||||
expecting_screenshot = False
|
||||
screenshot_data = b""
|
||||
if not reopen_serial(ser_holder):
|
||||
break
|
||||
ser = ser_holder["ser"]
|
||||
except KeyboardInterrupt:
|
||||
# If thread is killed violently (e.g. main exit), silence errors
|
||||
pass
|
||||
@@ -336,17 +395,31 @@ def serial_worker(ser, kwargs: dict[str, str]) -> None:
|
||||
pass # ser closed in main
|
||||
|
||||
|
||||
def input_worker(ser) -> None:
|
||||
def input_worker(ser_holder: dict) -> None:
|
||||
"""
|
||||
Runs in a background thread. Handles user input to send commands to the ESP32 device.
|
||||
Monitors the global shutdown event for graceful termination on Ctrl-C.
|
||||
"""
|
||||
while not shutdown_event.is_set():
|
||||
try:
|
||||
cmd = input("Command: ")
|
||||
ser.write(f"CMD:{cmd}\n".encode())
|
||||
cmd = input("Command: ").strip()
|
||||
if not cmd:
|
||||
continue
|
||||
ack_event.clear()
|
||||
ser_holder["ser"].write(f"CMD:{cmd}\n".encode())
|
||||
# The reader thread prints the ack/rejection line itself; this wait
|
||||
# only exists to catch silence. SCREENSHOT acks after the ~48KB
|
||||
# transfer, which finishes well inside the timeout at 115200 baud
|
||||
# over USB-CDC (native USB, not actually rate-limited).
|
||||
if not ack_event.wait(ACK_TIMEOUT_S):
|
||||
print(
|
||||
f"{Fore.YELLOW}No response to CMD:{cmd} after {ACK_TIMEOUT_S:g}s - device may be "
|
||||
f"asleep/rebooting, or running firmware without command acks.{Style.RESET_ALL}"
|
||||
)
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
break
|
||||
except (OSError, serial.SerialException):
|
||||
print(f"{Fore.YELLOW}Device not connected - command dropped.{Style.RESET_ALL}")
|
||||
|
||||
|
||||
def update_graph(frame) -> list: # pylint: disable=unused-argument
|
||||
@@ -458,26 +531,50 @@ def main() -> None:
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
ser = serial.Serial(port, args.baud, timeout=0.1)
|
||||
# Deassert DTR/RTS BEFORE opening: passing the port to the constructor
|
||||
# opens immediately with DTR asserted, and the USB-Serial-JTAG
|
||||
# peripheral interprets that as a reset — rebooting the device on every
|
||||
# monitor attach (the `reset=11` bench artifact) and destroying any
|
||||
# wedged state we're trying to observe post-mortem.
|
||||
ser = serial.Serial(None, args.baud, timeout=0.1)
|
||||
ser.port = port
|
||||
ser.dtr = False
|
||||
ser.rts = False
|
||||
ser.open()
|
||||
except serial.SerialException as e:
|
||||
print(f"{Fore.RED}Error opening port: {e}{Style.RESET_ALL}")
|
||||
return
|
||||
|
||||
# Shared holder so the reader thread can transparently reconnect (device
|
||||
# reboots / deep sleeps / replugs) and the input thread always writes to
|
||||
# the live port object.
|
||||
ser_holder = {"ser": ser, "port": port, "baud": args.baud}
|
||||
|
||||
# Set up signal handler for graceful shutdown
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
# 1. Start the Serial Reader in a separate thread
|
||||
# Daemon=True means this thread dies when the main program closes
|
||||
myargs = vars(args) # Convert Namespace to dict for easier passing
|
||||
t = threading.Thread(target=serial_worker, args=(ser, myargs), daemon=True)
|
||||
t = threading.Thread(target=serial_worker, args=(ser_holder, myargs), daemon=True)
|
||||
t.start()
|
||||
|
||||
# Start input thread
|
||||
input_thread = threading.Thread(target=input_worker, args=(ser,), daemon=True)
|
||||
input_thread = threading.Thread(target=input_worker, args=(ser_holder,), daemon=True)
|
||||
input_thread.start()
|
||||
|
||||
if args.no_graph:
|
||||
# Plain console mode: keep the main thread alive until Ctrl-C
|
||||
print(f"{Fore.YELLOW}Graph disabled (--no-graph). Press Ctrl-C to exit.{Style.RESET_ALL}")
|
||||
try:
|
||||
while not shutdown_event.is_set():
|
||||
time.sleep(0.5)
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n{Fore.YELLOW}Exiting...{Style.RESET_ALL}")
|
||||
finally:
|
||||
shutdown_event.set()
|
||||
return
|
||||
|
||||
# 2. Set up the Graph (Main Thread)
|
||||
try:
|
||||
import matplotlib.style as mplstyle # pylint: disable=import-outside-toplevel
|
||||
|
||||
@@ -21,6 +21,7 @@ class CrossPointSettings : public PersistableStore<CrossPointSettings> {
|
||||
COVER_CUSTOM = 4,
|
||||
BLANK = 5,
|
||||
QUICK_RESUME = 6,
|
||||
TRANSPARENT_CUSTOM = 7,
|
||||
SLEEP_SCREEN_MODE_COUNT
|
||||
};
|
||||
enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT };
|
||||
|
||||
Loaded 20 of 41 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user