From e64155ed63f994ddccf84b9b6dc0267107e61d5c Mon Sep 17 00:00:00 2001 From: Wylan Swets Date: Fri, 8 May 2026 23:01:31 -0400 Subject: [PATCH] fix: free Epub RAM and simplify KOSync navigation via ActivityManager (#1860) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary * **What is the goal of this PR?** Fix KOSync failing with "Network error" on large/complex EPUBs, and simplify the sync navigation flow by removing the callback/result pattern. * **What changes are included?** This PR combines the approaches from #1855 and #1760 into a single, cleaner solution: **Memory fix (from #1855):** - `EpubReaderActivity` pre-computes the local KOReader position and chapter name, then explicitly releases `epub` and `section` before launching `KOReaderSyncActivity`. This frees ~65KB measured on device, giving the TLS handshake sufficient heap. The root cause was `MBEDTLS_ERR_X509_ALLOC_FAILED` (-0x2880) when a 3-cert chain consumed ~48KB during the handshake with only ~50KB available. - `KOReaderSyncActivity` no longer receives a `shared_ptr` at construction — it lazy-loads the Epub after TLS only if remote progress is found (`ensureEpubLoaded()`). - Added `MIN_HEAP_FOR_TLS = 55000` guard in `KOReaderSyncClient` — returns `LOW_MEMORY` early if aggregate free heap is too low before attempting a TLS connection. **Navigation simplification (from #1760):** - Replaced `startActivityForResult` + callback with `activityManager.replaceActivity` / `activityManager.goToReader`. Progress is saved to `progress.bin` before the epub is released (cancel/upload paths) and in `saveProgressAndReturn` (apply remote path). The reader re-launches from the saved position naturally via `goToReader`, eliminating the need to reload the epub in a callback. - Extracted `ReaderUtils::saveProgress()` as a shared helper used by both `EpubReaderActivity` and `KOReaderSyncActivity`. - Added `STR_SAVE_PROGRESS_FAILED` to all 22 language files for the case where writing the synced position to SD fails. **Orientation fix (found during device testing):** - `EpubReaderActivity::onExit()` resets the renderer to portrait before destruction. With `replaceActivity` the reader is fully torn down before KOSync starts, so KOSync was always rendering in portrait even when reading in landscape. Fixed by calling `ReaderUtils::applyOrientation` in `KOReaderSyncActivity::onEnter()`. ## Additional Context Heap measurements on device (large EPUB with complex CSS): | Metric | Before | After | |---|---|---| | Heap before Epub release | 88,156 bytes | — | | Heap after Epub release | — | 153,892 bytes (+65,736) | | Heap at TLS handshake | ~50,000 bytes (fails) | ~116,384 bytes (passes) | | Min-free-ever during sync session | 2,600 bytes | 33,052 bytes | | TLS result | `MBEDTLS_ERR_X509_ALLOC_FAILED` | HTTP 200 | Tested on device: sync from inside a large EPUB in both portrait and landscape, cancel, apply remote progress, upload local progress. --- ### AI Usage Did you use AI tools to help write this code? _**YES**_ --------- Co-authored-by: Claude Sonnet 4.6 --- lib/I18n/translations/belarusian.yaml | 1 + lib/I18n/translations/catalan.yaml | 1 + lib/I18n/translations/czech.yaml | 1 + lib/I18n/translations/danish.yaml | 1 + lib/I18n/translations/dutch.yaml | 1 + lib/I18n/translations/english.yaml | 1 + lib/I18n/translations/finnish.yaml | 1 + lib/I18n/translations/french.yaml | 1 + lib/I18n/translations/german.yaml | 1 + lib/I18n/translations/hungarian.yaml | 1 + lib/I18n/translations/italian.yaml | 1 + lib/I18n/translations/kazakh.yaml | 1 + lib/I18n/translations/lithuanian.yaml | 1 + lib/I18n/translations/polish.yaml | 1 + lib/I18n/translations/portuguese.yaml | 1 + lib/I18n/translations/romanian.yaml | 1 + lib/I18n/translations/russian.yaml | 1 + lib/I18n/translations/slovenian.yaml | 1 + lib/I18n/translations/spanish.yaml | 1 + lib/I18n/translations/swedish.yaml | 1 + lib/I18n/translations/turkish.yaml | 1 + lib/I18n/translations/ukrainian.yaml | 1 + lib/KOReaderSync/KOReaderSyncClient.cpp | 30 ++++- lib/KOReaderSync/KOReaderSyncClient.h | 2 +- src/activities/reader/EpubReaderActivity.cpp | 89 ++++++++------ src/activities/reader/EpubReaderActivity.h | 3 +- src/activities/reader/EpubReaderUtils.h | 37 ++++++ .../reader/KOReaderSyncActivity.cpp | 111 ++++++++++-------- src/activities/reader/KOReaderSyncActivity.h | 18 +-- 29 files changed, 221 insertions(+), 91 deletions(-) create mode 100644 src/activities/reader/EpubReaderUtils.h diff --git a/lib/I18n/translations/belarusian.yaml b/lib/I18n/translations/belarusian.yaml index 6ce34536..245ecc10 100644 --- a/lib/I18n/translations/belarusian.yaml +++ b/lib/I18n/translations/belarusian.yaml @@ -254,6 +254,7 @@ 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: "Стыль кнігі" diff --git a/lib/I18n/translations/catalan.yaml b/lib/I18n/translations/catalan.yaml index 5bc09c10..377ab05e 100644 --- a/lib/I18n/translations/catalan.yaml +++ b/lib/I18n/translations/catalan.yaml @@ -282,6 +282,7 @@ STR_NO_REMOTE_MSG: "No s'ha trobat progrés remot" STR_UPLOAD_PROMPT: "Voleu pujar la posició actual?" STR_UPLOAD_SUCCESS: "Progrés pujat!" STR_SYNC_FAILED_MSG: "Sincronització fallida" +STR_SAVE_PROGRESS_FAILED: "No s'ha pogut desar el progrés" STR_SECTION_PREFIX: "Secció " STR_UPLOAD: "Puja" STR_BOOK_S_STYLE: "Estil del llibre" diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml index e2b0ef51..a769aa07 100644 --- a/lib/I18n/translations/czech.yaml +++ b/lib/I18n/translations/czech.yaml @@ -259,6 +259,7 @@ STR_NO_REMOTE_MSG: "Nenalezen žádný vzdálený postup" STR_UPLOAD_PROMPT: "Nahrát aktuální pozici?" STR_UPLOAD_SUCCESS: "Postup nahrán!" STR_SYNC_FAILED_MSG: "Synchronizace se nezdařila" +STR_SAVE_PROGRESS_FAILED: "Nepodařilo se uložit pokrok" STR_SECTION_PREFIX: "Sekce" STR_UPLOAD: "Nahrát" STR_BOOK_S_STYLE: "Styl knihy" diff --git a/lib/I18n/translations/danish.yaml b/lib/I18n/translations/danish.yaml index 36ebc506..91b13025 100644 --- a/lib/I18n/translations/danish.yaml +++ b/lib/I18n/translations/danish.yaml @@ -282,6 +282,7 @@ STR_NO_REMOTE_MSG: "Ingen fjernfremskridt fundet" STR_UPLOAD_PROMPT: "Upload nuværende position?" STR_UPLOAD_SUCCESS: "Fremskridt uploadet!" STR_SYNC_FAILED_MSG: "Synkronisering mislykkedes" +STR_SAVE_PROGRESS_FAILED: "Kunne ikke gemme fremskridt" STR_SECTION_PREFIX: "Afsnit " STR_UPLOAD: "Upload" STR_BOOK_S_STYLE: "Bogens stil" diff --git a/lib/I18n/translations/dutch.yaml b/lib/I18n/translations/dutch.yaml index 63a37be9..47041116 100644 --- a/lib/I18n/translations/dutch.yaml +++ b/lib/I18n/translations/dutch.yaml @@ -282,6 +282,7 @@ STR_NO_REMOTE_MSG: "Geen externe voortgang gevonden" STR_UPLOAD_PROMPT: "Huidige positie uploaden?" STR_UPLOAD_SUCCESS: "Voortgang geüpload!" STR_SYNC_FAILED_MSG: "Sync mislukt" +STR_SAVE_PROGRESS_FAILED: "Voortgang opslaan mislukt" STR_SECTION_PREFIX: "Sectie " STR_UPLOAD: "Uploaden" STR_BOOK_S_STYLE: "Stijl van boek" diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 9592808d..969af61c 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -291,6 +291,7 @@ STR_NO_REMOTE_MSG: "No remote progress found" STR_UPLOAD_PROMPT: "Upload current position?" STR_UPLOAD_SUCCESS: "Progress uploaded!" STR_SYNC_FAILED_MSG: "Sync failed" +STR_SAVE_PROGRESS_FAILED: "Could not save progress" STR_SECTION_PREFIX: "Section " STR_UPLOAD: "Upload" STR_BOOK_S_STYLE: "Book's Style" diff --git a/lib/I18n/translations/finnish.yaml b/lib/I18n/translations/finnish.yaml index 6832dd26..99a4d974 100644 --- a/lib/I18n/translations/finnish.yaml +++ b/lib/I18n/translations/finnish.yaml @@ -257,6 +257,7 @@ STR_NO_REMOTE_MSG: "Etäedistymistä ei löytynyt" STR_UPLOAD_PROMPT: "Lähetä nykyinen sijainti?" STR_UPLOAD_SUCCESS: "Edistyminen lähetetty!" STR_SYNC_FAILED_MSG: "Synkronointi epäonnistui" +STR_SAVE_PROGRESS_FAILED: "Edistymisen tallennus epäonnistui" STR_SECTION_PREFIX: "Osio " STR_UPLOAD: "Lähetä" STR_BOOK_S_STYLE: "Kirjan tyyli" diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index fe5e062a..fb299ab7 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -283,6 +283,7 @@ STR_NO_REMOTE_MSG: "Aucune progression en ligne" STR_UPLOAD_PROMPT: "Envoyer position actuelle ?" STR_UPLOAD_SUCCESS: "Progression envoyée !" STR_SYNC_FAILED_MSG: "Échec de la synchro" +STR_SAVE_PROGRESS_FAILED: "Échec de l'enregistrement" STR_SECTION_PREFIX: "Section " STR_UPLOAD: "Envoyer" STR_BOOK_S_STYLE: "Style du livre" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index 61d36a1c..28312b81 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -284,6 +284,7 @@ STR_NO_REMOTE_MSG: "Kein externer Fortschritt" STR_UPLOAD_PROMPT: "Aktuelle Position hochladen?" STR_UPLOAD_SUCCESS: "Hochgeladen!" STR_SYNC_FAILED_MSG: "Fehlgeschlagen" +STR_SAVE_PROGRESS_FAILED: "Fortschritt konnte nicht gespeichert werden" STR_SECTION_PREFIX: "Abschnitt" STR_UPLOAD: "Hochladen" STR_BOOK_S_STYLE: "Buch-Stil" diff --git a/lib/I18n/translations/hungarian.yaml b/lib/I18n/translations/hungarian.yaml index 0343c197..759768aa 100644 --- a/lib/I18n/translations/hungarian.yaml +++ b/lib/I18n/translations/hungarian.yaml @@ -279,6 +279,7 @@ STR_NO_REMOTE_MSG: "Nem található távoli haladás" STR_UPLOAD_PROMPT: "Jelenlegi pozíció feltöltése?" STR_UPLOAD_SUCCESS: "Haladás feltöltve!" STR_SYNC_FAILED_MSG: "Szinkronizálás sikertelen" +STR_SAVE_PROGRESS_FAILED: "A haladás mentése sikertelen" STR_SECTION_PREFIX: "Szakasz " STR_UPLOAD: "Feltöltés" STR_BOOK_S_STYLE: "Könyv stílusa" diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index 25de9853..3c3bb17b 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -283,6 +283,7 @@ STR_NO_REMOTE_MSG: "Nessun avanzamento remoto trovato" STR_UPLOAD_PROMPT: "Inviare la posizione attuale?" STR_UPLOAD_SUCCESS: "Avanzamenti inviati!" STR_SYNC_FAILED_MSG: "Sincronizzazione non riuscita" +STR_SAVE_PROGRESS_FAILED: "Impossibile salvare il progresso" STR_SECTION_PREFIX: "Sezione " STR_UPLOAD: "Carica" STR_BOOK_S_STYLE: "Stile libro" diff --git a/lib/I18n/translations/kazakh.yaml b/lib/I18n/translations/kazakh.yaml index 8ff2c99d..fbee8021 100644 --- a/lib/I18n/translations/kazakh.yaml +++ b/lib/I18n/translations/kazakh.yaml @@ -253,6 +253,7 @@ 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: "Кітап стилі" diff --git a/lib/I18n/translations/lithuanian.yaml b/lib/I18n/translations/lithuanian.yaml index c004c49e..bae0764a 100644 --- a/lib/I18n/translations/lithuanian.yaml +++ b/lib/I18n/translations/lithuanian.yaml @@ -279,6 +279,7 @@ STR_NO_REMOTE_MSG: "Progresų nerasta" STR_UPLOAD_PROMPT: "Įkelti poziciją?" STR_UPLOAD_SUCCESS: "Įkelta!" STR_SYNC_FAILED_MSG: "Nepavyko" +STR_SAVE_PROGRESS_FAILED: "Nepavyko išsaugoti progreso" STR_SECTION_PREFIX: "Dalis " STR_UPLOAD: "Įkelti" STR_BOOK_S_STYLE: "Knygos stilius" diff --git a/lib/I18n/translations/polish.yaml b/lib/I18n/translations/polish.yaml index 4a5c0af7..565fad83 100644 --- a/lib/I18n/translations/polish.yaml +++ b/lib/I18n/translations/polish.yaml @@ -282,6 +282,7 @@ STR_NO_REMOTE_MSG: "Nie znaleziono zdalnego postępu" STR_UPLOAD_PROMPT: "Wysłać obecną pozycję?" STR_UPLOAD_SUCCESS: "Postęp wysłany!" STR_SYNC_FAILED_MSG: "Synchronizacja nieudana" +STR_SAVE_PROGRESS_FAILED: "Nie udało się zapisać postępu" STR_SECTION_PREFIX: "Sekcja " STR_UPLOAD: "Wyślij" STR_BOOK_S_STYLE: "Styl książki" diff --git a/lib/I18n/translations/portuguese.yaml b/lib/I18n/translations/portuguese.yaml index f7379c16..e6249adb 100644 --- a/lib/I18n/translations/portuguese.yaml +++ b/lib/I18n/translations/portuguese.yaml @@ -259,6 +259,7 @@ STR_NO_REMOTE_MSG: "Nenhum progresso remoto encontrado" STR_UPLOAD_PROMPT: "Enviar posição atual?" STR_UPLOAD_SUCCESS: "Progresso enviado!" STR_SYNC_FAILED_MSG: "Falha na sincronização" +STR_SAVE_PROGRESS_FAILED: "Não foi possível salvar o progresso" STR_SECTION_PREFIX: "Seção" STR_UPLOAD: "Enviar" STR_BOOK_S_STYLE: "Estilo do livro" diff --git a/lib/I18n/translations/romanian.yaml b/lib/I18n/translations/romanian.yaml index 2a4856a3..d4e004db 100644 --- a/lib/I18n/translations/romanian.yaml +++ b/lib/I18n/translations/romanian.yaml @@ -282,6 +282,7 @@ STR_NO_REMOTE_MSG: "Niciun progres remote găsit" STR_UPLOAD_PROMPT: "Încărcaţi poziţia curentă?" STR_UPLOAD_SUCCESS: "Progres încărcat!" STR_SYNC_FAILED_MSG: "Sincronizare eşuată" +STR_SAVE_PROGRESS_FAILED: "Nu s-a putut salva progresul" STR_SECTION_PREFIX: "Secţiune " STR_UPLOAD: "Încărcare" STR_BOOK_S_STYLE: "Stilul cărţii" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index dda0acc0..6f00d6c3 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -286,6 +286,7 @@ 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: "Стиль книги" diff --git a/lib/I18n/translations/slovenian.yaml b/lib/I18n/translations/slovenian.yaml index 19dcf784..20103772 100644 --- a/lib/I18n/translations/slovenian.yaml +++ b/lib/I18n/translations/slovenian.yaml @@ -279,6 +279,7 @@ STR_NO_REMOTE_MSG: "Oddaljen napredek ni bil najden" STR_UPLOAD_PROMPT: "Naložim trenutno pozicijo?" STR_UPLOAD_SUCCESS: "Napredek naložen!" STR_SYNC_FAILED_MSG: "Sinhronizacija ni uspela" +STR_SAVE_PROGRESS_FAILED: "Napredka ni bilo mogoče shraniti" STR_SECTION_PREFIX: "Razdelek " STR_UPLOAD: "Naloži" STR_BOOK_S_STYLE: "Slog knjige" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index 5cb76a23..ea2e56dc 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -283,6 +283,7 @@ STR_NO_REMOTE_MSG: "No se encontró progreso remoto" STR_UPLOAD_PROMPT: "¿Subir posición actual?" STR_UPLOAD_SUCCESS: "¡Progreso subido!" STR_SYNC_FAILED_MSG: "Fallo de sincronización" +STR_SAVE_PROGRESS_FAILED: "No se pudo guardar el progreso" STR_SECTION_PREFIX: "Secc.:" STR_UPLOAD: "Subir" STR_BOOK_S_STYLE: "Estilo del libro" diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index 73444fad..2505c935 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -288,6 +288,7 @@ STR_NO_REMOTE_MSG: "Inga fjärrframsteg funna" STR_UPLOAD_PROMPT: "Ladda upp nuvarande position?" STR_UPLOAD_SUCCESS: "Framsteg uppladdade!" STR_SYNC_FAILED_MSG: "Synkronisering misslyckades" +STR_SAVE_PROGRESS_FAILED: "Kunde inte spara framsteg" STR_SECTION_PREFIX: "Sektion" STR_UPLOAD: "Uppladdning" STR_BOOK_S_STYLE: "Bokstil" diff --git a/lib/I18n/translations/turkish.yaml b/lib/I18n/translations/turkish.yaml index 3c8c679a..066af673 100644 --- a/lib/I18n/translations/turkish.yaml +++ b/lib/I18n/translations/turkish.yaml @@ -257,6 +257,7 @@ STR_NO_REMOTE_MSG: "Uzak ilerleme bulunamadı" STR_UPLOAD_PROMPT: "Mevcut konumu yükle?" STR_UPLOAD_SUCCESS: "İlerleme yüklendi!" STR_SYNC_FAILED_MSG: "Senkronizasyon başarısız" +STR_SAVE_PROGRESS_FAILED: "İlerleme kaydedilemedi" STR_SECTION_PREFIX: "Bölüm " STR_UPLOAD: "Yükle" STR_BOOK_S_STYLE: "Kitabın Stili" diff --git a/lib/I18n/translations/ukrainian.yaml b/lib/I18n/translations/ukrainian.yaml index c556aef3..dfe0d019 100644 --- a/lib/I18n/translations/ukrainian.yaml +++ b/lib/I18n/translations/ukrainian.yaml @@ -287,6 +287,7 @@ 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: "Стиль книги" diff --git a/lib/KOReaderSync/KOReaderSyncClient.cpp b/lib/KOReaderSync/KOReaderSyncClient.cpp index 34c35c8c..e0bc410d 100644 --- a/lib/KOReaderSync/KOReaderSyncClient.cpp +++ b/lib/KOReaderSync/KOReaderSyncClient.cpp @@ -22,6 +22,13 @@ constexpr char DEVICE_ID[] = "crosspoint-reader"; // Default 16KB buffers cause OOM during TLS handshake. constexpr int HTTP_BUF_SIZE = 2048; +// Cloudflare tunnels send a 3-cert Google Trust Services chain. During the TLS handshake +// mbedTLS makes many small allocations that collectively consume ~48KB of heap. With only +// ~50KB free after WiFi connects, the session drove min-free-ever down to 2600 bytes before +// failing with MBEDTLS_ERR_X509_ALLOC_FAILED (-0x2880). Check total free heap (not max +// contiguous block) because the failure mode is aggregate exhaustion, not one large alloc. +constexpr uint32_t MIN_HEAP_FOR_TLS = 55000; + // Response buffer for reading HTTP body struct ResponseBuffer { char* data = nullptr; @@ -102,7 +109,12 @@ KOReaderSyncClient::Error KOReaderSyncClient::authenticate() { } std::string url = KOREADER_STORE.getBaseUrl() + "/users/auth"; - LOG_DBG("KOSync", "Authenticating: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap()); + const uint32_t freeHeap = ESP.getFreeHeap(); + LOG_DBG("KOSync", "Authenticating: %s (heap: %u)", url.c_str(), (unsigned)freeHeap); + if (freeHeap < MIN_HEAP_FOR_TLS) { + LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u)", freeHeap, MIN_HEAP_FOR_TLS); + return LOW_MEMORY; + } ResponseBuffer buf; esp_http_client_handle_t client = createClient(url.c_str(), &buf); @@ -130,7 +142,12 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc } std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress/" + documentHash; - LOG_DBG("KOSync", "Getting progress: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap()); + const uint32_t freeHeap = ESP.getFreeHeap(); + LOG_DBG("KOSync", "Getting progress: %s (heap: %u)", url.c_str(), (unsigned)freeHeap); + if (freeHeap < MIN_HEAP_FOR_TLS) { + LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u)", freeHeap, MIN_HEAP_FOR_TLS); + return LOW_MEMORY; + } ResponseBuffer buf; esp_http_client_handle_t client = createClient(url.c_str(), &buf); @@ -178,7 +195,12 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr } std::string url = KOREADER_STORE.getBaseUrl() + "/syncs/progress"; - LOG_DBG("KOSync", "Updating progress: %s (heap: %u)", url.c_str(), (unsigned)ESP.getFreeHeap()); + const uint32_t freeHeap = ESP.getFreeHeap(); + LOG_DBG("KOSync", "Updating progress: %s (heap: %u)", url.c_str(), (unsigned)freeHeap); + if (freeHeap < MIN_HEAP_FOR_TLS) { + LOG_ERR("KOSync", "Insufficient heap for TLS handshake: %u bytes free (need %u)", freeHeap, MIN_HEAP_FOR_TLS); + return LOW_MEMORY; + } // Build JSON body JsonDocument doc; @@ -233,6 +255,8 @@ const char* KOReaderSyncClient::errorString(Error error) { return "JSON parse error"; case NOT_FOUND: return "No progress found"; + case LOW_MEMORY: + return "Not enough memory for sync — please retry"; default: return "Unknown error"; } diff --git a/lib/KOReaderSync/KOReaderSyncClient.h b/lib/KOReaderSync/KOReaderSyncClient.h index 7832ef8f..7c0208d9 100644 --- a/lib/KOReaderSync/KOReaderSyncClient.h +++ b/lib/KOReaderSync/KOReaderSyncClient.h @@ -29,7 +29,7 @@ struct KOReaderProgress { */ class KOReaderSyncClient { public: - enum Error { OK = 0, NO_CREDENTIALS, NETWORK_ERROR, AUTH_FAILED, SERVER_ERROR, JSON_ERROR, NOT_FOUND }; + enum Error { OK = 0, NO_CREDENTIALS, NETWORK_ERROR, AUTH_FAILED, SERVER_ERROR, JSON_ERROR, NOT_FOUND, LOW_MEMORY }; /** * Authenticate with the sync server (validate credentials). diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index cf7e3257..3de6d69d 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -18,9 +18,11 @@ #include "EpubReaderChapterSelectionActivity.h" #include "EpubReaderFootnotesActivity.h" #include "EpubReaderPercentSelectionActivity.h" +#include "EpubReaderUtils.h" #include "KOReaderCredentialStore.h" #include "KOReaderSyncActivity.h" #include "MappedInputManager.h" +#include "ProgressMapper.h" #include "QrDisplayActivity.h" #include "ReaderUtils.h" #include "RecentBooksStore.h" @@ -392,7 +394,9 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction section.reset(); epub->clearCache(); epub->setupCacheDir(); - saveProgress(backupSpine, backupPage, backupPageCount); + if (!saveProgress(backupSpine, backupPage, backupPageCount)) { + LOG_ERR("ERS", "Failed to save progress before cache clear"); + } } } onGoHome(); @@ -418,23 +422,42 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction paragraphIndex = *pIdx; } } - startActivityForResult( - std::make_unique(renderer, mappedInput, epub, epub->getPath(), currentSpineIndex, - currentPage, totalPages, paragraphIndex), - [this](const ActivityResult& result) { - if (!result.isCancelled) { - const auto& sync = std::get(result.data); - if (currentSpineIndex != sync.spineIndex || (section && section->currentPage != sync.page)) { - RenderLock lock(*this); - currentSpineIndex = sync.spineIndex; - nextPageNumber = sync.page; - cachedChapterTotalPageCount = 0; // Prevent rescaling sync page - pendingPageJump.reset(); - saveProgress(currentSpineIndex, nextPageNumber, 0); - section.reset(); - } - } - }); + + // Pre-compute local KO position and chapter name while Epub is still in RAM. + CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPages}; + if (paragraphIndex.has_value()) { + localPos.paragraphIndex = *paragraphIndex; + localPos.hasParagraphIndex = true; + } + KOReaderPosition localKoPos = ProgressMapper::toKOReader(epub, localPos); + const int tocIdx = epub->getTocIndexForSpineIndex(currentSpineIndex); + std::string localChapterName = (tocIdx >= 0) ? epub->getTocItem(tocIdx).title : ""; + const std::string savedEpubPath = epub->getPath(); + + // Persist current position so the reader resumes at the right page on return. + // goToReader() depends on this file, so abort the sync if the write fails. + if (!saveProgress(currentSpineIndex, currentPage, totalPages)) { + LOG_ERR("KOSync", "Aborting sync because current progress could not be saved"); + pendingSyncSaveError = true; + requestUpdate(); + return; + } + + // Release Epub and Section to free ~65KB RAM for the TLS handshake. + LOG_DBG("KOSync", "Releasing epub for sync (heap before: %u)", (unsigned)ESP.getFreeHeap()); + { + RenderLock lock(*this); + if (section) { + nextPageNumber = section->currentPage; + } + section.reset(); + epub.reset(); + } + LOG_DBG("KOSync", "Epub released (heap after: %u)", (unsigned)ESP.getFreeHeap()); + + activityManager.replaceActivity(std::make_unique( + renderer, mappedInput, savedEpubPath, currentSpineIndex, currentPage, totalPages, std::move(localKoPos), + std::move(localChapterName), paragraphIndex)); } break; } @@ -530,6 +553,12 @@ void EpubReaderActivity::render(RenderLock&& lock) { return; } + const auto showPendingSyncSaveError = [this]() { + if (!pendingSyncSaveError) return; + pendingSyncSaveError = false; + GUI.drawPopup(renderer, tr(STR_SAVE_PROGRESS_FAILED)); + }; + // edge case handling for sub-zero spine index if (currentSpineIndex < 0) { currentSpineIndex = 0; @@ -545,6 +574,7 @@ void EpubReaderActivity::render(RenderLock&& lock) { renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD); renderer.displayBuffer(); automaticPageTurnActive = false; + showPendingSyncSaveError(); return; } @@ -592,6 +622,7 @@ void EpubReaderActivity::render(RenderLock&& lock) { SETTINGS.imageRendering, popupFn)) { LOG_ERR("ERS", "Failed to persist page data to SD"); section.reset(); + showPendingSyncSaveError(); return; } } else { @@ -655,6 +686,7 @@ void EpubReaderActivity::render(RenderLock&& lock) { renderStatusBar(); renderer.displayBuffer(); automaticPageTurnActive = false; + showPendingSyncSaveError(); return; } @@ -664,6 +696,7 @@ void EpubReaderActivity::render(RenderLock&& lock) { renderStatusBar(); renderer.displayBuffer(); automaticPageTurnActive = false; + showPendingSyncSaveError(); return; } @@ -676,6 +709,7 @@ void EpubReaderActivity::render(RenderLock&& lock) { requestUpdate(); // Try again after clearing cache // TODO: prevent infinite loop if the page keeps failing to load for some reason automaticPageTurnActive = false; + showPendingSyncSaveError(); return; } @@ -689,6 +723,8 @@ void EpubReaderActivity::render(RenderLock&& lock) { silentIndexNextChapterIfNeeded(viewportWidth, viewportHeight); saveProgress(currentSpineIndex, section->currentPage, section->pageCount); + showPendingSyncSaveError(); + if (pendingScreenshot) { pendingScreenshot = false; ScreenshotUtil::takeScreenshot(renderer); @@ -727,21 +763,8 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW } } -void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) { - FsFile f; - if (Storage.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) { - uint8_t data[6]; - data[0] = currentSpineIndex & 0xFF; - data[1] = (currentSpineIndex >> 8) & 0xFF; - data[2] = currentPage & 0xFF; - data[3] = (currentPage >> 8) & 0xFF; - data[4] = pageCount & 0xFF; - data[5] = (pageCount >> 8) & 0xFF; - f.write(data, 6); - LOG_DBG("ERS", "Progress saved: Chapter %d, Page %d", spineIndex, currentPage); - } else { - LOG_ERR("ERS", "Could not save progress!"); - } +bool EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) { + return EpubReaderUtils::saveProgress(*epub, spineIndex, currentPage, pageCount); } void EpubReaderActivity::renderContents(std::unique_ptr page, const int orientedMarginTop, const int orientedMarginRight, const int orientedMarginBottom, diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 76a6df26..01de3fde 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -28,6 +28,7 @@ class EpubReaderActivity final : public Activity { // Normalized 0.0-1.0 progress within the target spine item, computed from book percentage. float pendingSpineProgress = 0.0f; bool pendingScreenshot = false; + bool pendingSyncSaveError = false; bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit bool automaticPageTurnActive = false; @@ -45,7 +46,7 @@ class EpubReaderActivity final : public Activity { int orientedMarginBottom, int orientedMarginLeft); void renderStatusBar() const; void silentIndexNextChapterIfNeeded(uint16_t viewportWidth, uint16_t viewportHeight); - void saveProgress(int spineIndex, int currentPage, int pageCount); + bool saveProgress(int spineIndex, int currentPage, int pageCount); // Jump to a percentage of the book (0-100), mapping it to spine and page. void jumpToPercent(int percent); void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action); diff --git a/src/activities/reader/EpubReaderUtils.h b/src/activities/reader/EpubReaderUtils.h new file mode 100644 index 00000000..f2d2a747 --- /dev/null +++ b/src/activities/reader/EpubReaderUtils.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +namespace EpubReaderUtils { + +// Persists reader progress for an EPUB to its cache directory. Returns true on success. +inline bool saveProgress(Epub& epub, int spineIndex, int pageNumber, int pageCount) { + if (spineIndex < 0 || spineIndex > 0xFFFF || pageNumber < 0 || pageNumber > 0xFFFF || pageCount < 0 || + pageCount > 0xFFFF) { + LOG_ERR("ERS", "Progress values out of range: spine=%d page=%d count=%d", spineIndex, pageNumber, pageCount); + return false; + } + FsFile f; + if (!Storage.openFileForWrite("ERS", epub.getCachePath() + "/progress.bin", f)) { + LOG_ERR("ERS", "Could not open progress file for write!"); + return false; + } + uint8_t data[6]; + data[0] = spineIndex & 0xFF; + data[1] = (spineIndex >> 8) & 0xFF; + data[2] = pageNumber & 0xFF; + data[3] = (pageNumber >> 8) & 0xFF; + data[4] = pageCount & 0xFF; + data[5] = (pageCount >> 8) & 0xFF; + const size_t written = f.write(data, sizeof(data)); + if (written != sizeof(data)) { + LOG_ERR("ERS", "Short write saving progress: %u/%u bytes", (unsigned)written, (unsigned)sizeof(data)); + return false; + } + LOG_DBG("ERS", "Progress saved: spine=%d page=%d", spineIndex, pageNumber); + return true; +} + +} // namespace EpubReaderUtils diff --git a/src/activities/reader/KOReaderSyncActivity.cpp b/src/activities/reader/KOReaderSyncActivity.cpp index 9dc2e9be..d0e77571 100644 --- a/src/activities/reader/KOReaderSyncActivity.cpp +++ b/src/activities/reader/KOReaderSyncActivity.cpp @@ -1,30 +1,26 @@ #include "KOReaderSyncActivity.h" #include +#include #include #include #include #include +#include + #include "Epub/Section.h" +#include "EpubReaderUtils.h" #include "KOReaderCredentialStore.h" #include "KOReaderDocumentId.h" #include "MappedInputManager.h" +#include "ReaderUtils.h" +#include "activities/ActivityManager.h" #include "activities/network/WifiSelectionActivity.h" #include "components/UITheme.h" #include "fontIds.h" namespace { -CrossPointPosition makeLocalPositionWithParagraph(const int spineIndex, const int page, const int totalPages, - const std::optional& paragraphIndex) { - CrossPointPosition pos = {spineIndex, page, totalPages}; - if (paragraphIndex.has_value()) { - pos.paragraphIndex = *paragraphIndex; - pos.hasParagraphIndex = true; - } - return pos; -} - void syncTimeWithNTP() { // Stop SNTP if already running (can't reconfigure while running) if (esp_sntp_enabled()) { @@ -61,13 +57,43 @@ void wifiOff() { } } // namespace +void KOReaderSyncActivity::ensureEpubLoaded() { + if (!epub) { + LOG_DBG("KOSync", "Loading epub for progress mapping (heap: %u)", (unsigned)ESP.getFreeHeap()); + epub = std::make_shared(epubPath, "/.crosspoint"); + epub->setupCacheDir(); + // Load metadata only (no CSS needed for progress mapping, don't rebuild if cache is missing). + if (!epub->load(false, true)) { + LOG_ERR("KOSync", "Failed to load epub for progress mapping"); + epub.reset(); + return; + } + LOG_DBG("KOSync", "Epub loaded (heap: %u)", (unsigned)ESP.getFreeHeap()); + } +} + +void KOReaderSyncActivity::saveProgressAndReturn(int spineIndex, int page) { + // epub is guaranteed non-null here: ensureEpubLoaded() was called in performSync() before + // SHOWING_RESULT state is entered, and this method is only called from that state. + assert(epub); + if (!EpubReaderUtils::saveProgress(*epub, spineIndex, page, 0)) { + { + RenderLock lock(*this); + state = SYNC_FAILED; + statusMessage = tr(STR_SAVE_PROGRESS_FAILED); + } + requestUpdate(true); + return; + } + returnToReader(); +} + +void KOReaderSyncActivity::returnToReader() { activityManager.goToReader(epubPath); } + void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) { if (!success) { LOG_DBG("KOSync", "WiFi connection failed, exiting"); - ActivityResult result; - result.isCancelled = true; - setResult(std::move(result)); - finish(); + returnToReader(); return; } @@ -141,8 +167,19 @@ void KOReaderSyncActivity::performSync() { return; } - // Convert remote progress to CrossPoint position + // Epub was released before sync to free RAM for the TLS handshake — reload it now. hasRemoteProgress = true; + ensureEpubLoaded(); + if (!epub) { + { + RenderLock lock(*this); + state = SYNC_FAILED; + statusMessage = ""; + } + requestUpdate(true); + return; + } + KOReaderPosition koPos = {remoteProgress.progress, remoteProgress.percentage}; remotePosition = ProgressMapper::toCrossPoint(epub, koPos, currentSpineIndex, totalPagesInSpine); @@ -157,11 +194,7 @@ void KOReaderSyncActivity::performSync() { remotePosition.pageNumber = *paragraphPage; } } - - // Calculate local progress in KOReader format (for display) - CrossPointPosition localPos = - makeLocalPositionWithParagraph(currentSpineIndex, currentPage, totalPagesInSpine, currentParagraphIndex); - localProgress = ProgressMapper::toKOReader(epub, localPos); + // localProgress was pre-computed in EpubReaderActivity before the Epub was released. { RenderLock lock(*this); @@ -185,15 +218,11 @@ void KOReaderSyncActivity::performUpload() { } requestUpdateAndWait(); - // Convert current position to KOReader format - CrossPointPosition localPos = - makeLocalPositionWithParagraph(currentSpineIndex, currentPage, totalPagesInSpine, currentParagraphIndex); - KOReaderPosition koPos = ProgressMapper::toKOReader(epub, localPos); - + // localProgress was pre-computed in EpubReaderActivity before the Epub was released. KOReaderProgress progress; progress.document = documentHash; - progress.progress = koPos.xpath; - progress.percentage = koPos.percentage; + progress.progress = localProgress.xpath; + progress.percentage = localProgress.percentage; const auto result = KOReaderSyncClient::updateProgress(progress); @@ -218,6 +247,7 @@ void KOReaderSyncActivity::performUpload() { void KOReaderSyncActivity::onEnter() { Activity::onEnter(); + ReaderUtils::applyOrientation(renderer, SETTINGS.orientation); // Check for credentials first if (!KOREADER_STORE.hasCredentials()) { @@ -271,15 +301,15 @@ void KOReaderSyncActivity::render(RenderLock&&) { // Show comparison renderer.drawCenteredText(UI_10_FONT_ID, 120, tr(STR_PROGRESS_FOUND), true, EpdFontFamily::BOLD); - // Get chapter names from TOC + // Remote chapter name requires Epub (loaded lazily in performSync before this state). const int remoteTocIndex = epub->getTocIndexForSpineIndex(remotePosition.spineIndex); - const int localTocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex); const std::string remoteChapter = (remoteTocIndex >= 0) ? epub->getTocItem(remoteTocIndex).title : (std::string(tr(STR_SECTION_PREFIX)) + std::to_string(remotePosition.spineIndex + 1)); + // Local chapter name was pre-computed before Epub was released. const std::string localChapter = - (localTocIndex >= 0) ? epub->getTocItem(localTocIndex).title - : (std::string(tr(STR_SECTION_PREFIX)) + std::to_string(currentSpineIndex + 1)); + !localChapterName.empty() ? localChapterName + : (std::string(tr(STR_SECTION_PREFIX)) + std::to_string(currentSpineIndex + 1)); // Remote progress - chapter and page renderer.drawText(UI_10_FONT_ID, 20, 160, tr(STR_REMOTE_LABEL), true); @@ -362,10 +392,7 @@ void KOReaderSyncActivity::render(RenderLock&&) { void KOReaderSyncActivity::loop() { if (state == NO_CREDENTIALS || state == SYNC_FAILED || state == UPLOAD_COMPLETE) { if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - ActivityResult result; - result.isCancelled = true; - setResult(std::move(result)); - finish(); + returnToReader(); } return; } @@ -384,9 +411,7 @@ void KOReaderSyncActivity::loop() { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (selectedOption == 0) { - // Wifi will be turned off in onExit() - setResult(SyncResult{remotePosition.spineIndex, remotePosition.pageNumber}); - finish(); + saveProgressAndReturn(remotePosition.spineIndex, remotePosition.pageNumber); } else if (selectedOption == 1) { // Upload local progress performUpload(); @@ -394,10 +419,7 @@ void KOReaderSyncActivity::loop() { } if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - ActivityResult result; - result.isCancelled = true; - setResult(std::move(result)); - finish(); + returnToReader(); } return; } @@ -416,10 +438,7 @@ void KOReaderSyncActivity::loop() { } if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - ActivityResult result; - result.isCancelled = true; - setResult(std::move(result)); - finish(); + returnToReader(); } return; } diff --git a/src/activities/reader/KOReaderSyncActivity.h b/src/activities/reader/KOReaderSyncActivity.h index ec4e1fde..1f07e4f7 100644 --- a/src/activities/reader/KOReaderSyncActivity.h +++ b/src/activities/reader/KOReaderSyncActivity.h @@ -21,20 +21,20 @@ */ class KOReaderSyncActivity final : public Activity { public: - explicit KOReaderSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::shared_ptr& epub, const std::string& epubPath, int currentSpineIndex, - int currentPage, int totalPagesInSpine, + explicit KOReaderSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& epubPath, + int currentSpineIndex, int currentPage, int totalPagesInSpine, + KOReaderPosition localKoPos, std::string localChapterName, std::optional currentParagraphIndex = std::nullopt) : Activity("KOReaderSync", renderer, mappedInput), - epub(epub), epubPath(epubPath), currentSpineIndex(currentSpineIndex), currentPage(currentPage), totalPagesInSpine(totalPagesInSpine), currentParagraphIndex(currentParagraphIndex), + localChapterName(std::move(localChapterName)), remoteProgress{}, remotePosition{}, - localProgress{} {} + localProgress(std::move(localKoPos)) {} void onEnter() override; void onExit() override; @@ -55,8 +55,9 @@ class KOReaderSyncActivity final : public Activity { NO_CREDENTIALS }; - std::shared_ptr epub; + std::shared_ptr epub; // null until lazy-loaded after TLS in performSync() std::string epubPath; + std::string localChapterName; int currentSpineIndex; int currentPage; int totalPagesInSpine; @@ -71,7 +72,7 @@ class KOReaderSyncActivity final : public Activity { KOReaderProgress remoteProgress; CrossPointPosition remotePosition; - // Local progress as KOReader format (for display) + // Local progress as KOReader format (pre-computed before Epub was released) KOReaderPosition localProgress; // Selection in result screen (0=Apply, 1=Upload) @@ -80,4 +81,7 @@ class KOReaderSyncActivity final : public Activity { void onWifiSelectionComplete(bool success); void performSync(); void performUpload(); + void ensureEpubLoaded(); + void saveProgressAndReturn(int spineIndex, int page); + void returnToReader(); };