diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index d304c4e4..0991faf8 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -12,7 +12,7 @@ CrossPointSettings CrossPointSettings::instance; namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields -constexpr uint8_t SETTINGS_COUNT = 13; +constexpr uint8_t SETTINGS_COUNT = 14; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; } // namespace @@ -40,6 +40,7 @@ bool CrossPointSettings::saveToFile() const { serialization::writePod(outputFile, paragraphAlignment); serialization::writePod(outputFile, sleepTimeout); serialization::writePod(outputFile, refreshFrequency); + serialization::writePod(outputFile, screenMargin); outputFile.close(); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); @@ -92,6 +93,8 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, refreshFrequency); if (++settingsRead >= fileSettingsCount) break; + serialization::readPod(inputFile, screenMargin); + if (++settingsRead >= fileSettingsCount) break; } while (false); inputFile.close(); @@ -167,6 +170,22 @@ int CrossPointSettings::getRefreshFrequency() const { } } +int CrossPointSettings::getScreenMargin() const { + switch (screenMargin) { + case MARGIN_0: + return 0; + case MARGIN_5: + default: + return 5; + case MARGIN_10: + return 10; + case MARGIN_15: + return 15; + case MARGIN_20: + return 20; + } +} + int CrossPointSettings::getReaderFontId() const { switch (fontFamily) { case BOOKERLY: diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index bb38df68..579f607e 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -51,6 +51,9 @@ class CrossPointSettings { // E-ink refresh frequency (pages between full refreshes) enum REFRESH_FREQUENCY { REFRESH_1 = 0, REFRESH_5 = 1, REFRESH_10 = 2, REFRESH_15 = 3, REFRESH_30 = 4 }; + // Screen margin options + enum SCREEN_MARGIN { MARGIN_0 = 0, MARGIN_5 = 1, MARGIN_10 = 2, MARGIN_15 = 3, MARGIN_20 = 4 }; + // Sleep screen settings uint8_t sleepScreen = DARK; // Status bar settings @@ -74,6 +77,8 @@ class CrossPointSettings { uint8_t sleepTimeout = SLEEP_10_MIN; // E-ink refresh frequency (default 15 pages) uint8_t refreshFrequency = REFRESH_15; + // Screen margin setting (default 5px) + uint8_t screenMargin = MARGIN_5; ~CrossPointSettings() = default; @@ -89,6 +94,7 @@ class CrossPointSettings { float getReaderLineCompression() const; unsigned long getSleepTimeoutMs() const; int getRefreshFrequency() const; + int getScreenMargin() const; }; // Helper macro to access settings diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 2208d5b4..ac642a89 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include "CrossPointSettings.h" @@ -12,10 +13,12 @@ namespace { constexpr unsigned long goHomeMs = 1000; -constexpr int topPadding = 10; -constexpr int horizontalPadding = 15; constexpr int statusBarMargin = 25; constexpr size_t CHUNK_SIZE = 8 * 1024; // 8KB chunk for reading + +// Cache file magic and version +constexpr uint32_t CACHE_MAGIC = 0x54585449; // "TXTI" +constexpr uint8_t CACHE_VERSION = 2; // Increment when cache format changes } // namespace void TxtReaderActivity::taskTrampoline(void* param) { @@ -139,18 +142,23 @@ void TxtReaderActivity::initializeReader() { return; } + // Store current settings for cache validation + cachedFontId = SETTINGS.getReaderFontId(); + cachedScreenMargin = SETTINGS.getScreenMargin(); + cachedParagraphAlignment = SETTINGS.paragraphAlignment; + // Calculate viewport dimensions int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft; renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom, &orientedMarginLeft); - orientedMarginTop += topPadding; - orientedMarginLeft += horizontalPadding; - orientedMarginRight += horizontalPadding; + orientedMarginTop += cachedScreenMargin; + orientedMarginLeft += cachedScreenMargin; + orientedMarginRight += cachedScreenMargin; orientedMarginBottom += statusBarMargin; viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight; const int viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom; - const int lineHeight = renderer.getLineHeight(SETTINGS.getReaderFontId()); + const int lineHeight = renderer.getLineHeight(cachedFontId); linesPerPage = viewportHeight / lineHeight; if (linesPerPage < 1) linesPerPage = 1; @@ -291,7 +299,7 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector // Word wrap if needed while (!line.empty() && static_cast(outLines.size()) < linesPerPage) { - int lineWidth = renderer.getTextWidth(SETTINGS.getReaderFontId(), line.c_str()); + int lineWidth = renderer.getTextWidth(cachedFontId, line.c_str()); if (lineWidth <= viewportWidth) { outLines.push_back(line); @@ -301,7 +309,7 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector // Find break point size_t breakPos = line.length(); while (breakPos > 0 && - renderer.getTextWidth(SETTINGS.getReaderFontId(), line.substr(0, breakPos).c_str()) > viewportWidth) { + renderer.getTextWidth(cachedFontId, line.substr(0, breakPos).c_str()) > viewportWidth) { // Try to break at space size_t spacePos = line.rfind(' ', breakPos - 1); if (spacePos != std::string::npos && spacePos > 0) { @@ -393,21 +401,51 @@ void TxtReaderActivity::renderPage() { int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft; renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom, &orientedMarginLeft); - orientedMarginTop += topPadding; - orientedMarginLeft += horizontalPadding; - orientedMarginRight += horizontalPadding; + orientedMarginTop += cachedScreenMargin; + orientedMarginLeft += cachedScreenMargin; + orientedMarginRight += cachedScreenMargin; orientedMarginBottom += statusBarMargin; - const int lineHeight = renderer.getLineHeight(SETTINGS.getReaderFontId()); + const int lineHeight = renderer.getLineHeight(cachedFontId); + const int contentWidth = viewportWidth; - int y = orientedMarginTop; - for (const auto& line : currentPageLines) { - if (!line.empty()) { - renderer.drawText(SETTINGS.getReaderFontId(), orientedMarginLeft, y, line.c_str()); + // Render text lines with alignment + auto renderLines = [&]() { + int y = orientedMarginTop; + for (const auto& line : currentPageLines) { + if (!line.empty()) { + int x = orientedMarginLeft; + + // Apply text alignment + switch (cachedParagraphAlignment) { + case CrossPointSettings::LEFT_ALIGN: + default: + // x already set to left margin + break; + case CrossPointSettings::CENTER_ALIGN: { + int textWidth = renderer.getTextWidth(cachedFontId, line.c_str()); + x = orientedMarginLeft + (contentWidth - textWidth) / 2; + break; + } + case CrossPointSettings::RIGHT_ALIGN: { + int textWidth = renderer.getTextWidth(cachedFontId, line.c_str()); + x = orientedMarginLeft + contentWidth - textWidth; + break; + } + case CrossPointSettings::JUSTIFIED: + // For plain text, justified is treated as left-aligned + // (true justification would require word spacing adjustments) + break; + } + + renderer.drawText(cachedFontId, x, y, line.c_str()); + } + y += lineHeight; } - y += lineHeight; - } + }; + // First pass: BW rendering + renderLines(); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); if (pagesUntilFullRefresh <= 1) { @@ -417,6 +455,28 @@ void TxtReaderActivity::renderPage() { renderer.displayBuffer(); pagesUntilFullRefresh--; } + + // Save BW buffer for restoration after grayscale pass + renderer.storeBwBuffer(); + + // Grayscale rendering pass (for anti-aliased fonts) + { + renderer.clearScreen(0x00); + renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); + renderLines(); + renderer.copyGrayscaleLsbBuffers(); + + renderer.clearScreen(0x00); + renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); + renderLines(); + renderer.copyGrayscaleMsbBuffers(); + + renderer.displayGrayBuffer(); + renderer.setRenderMode(GfxRenderer::BW); + } + + // Restore BW buffer + renderer.restoreBwBuffer(); } void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int orientedMarginBottom, @@ -492,13 +552,17 @@ void TxtReaderActivity::loadProgress() { } bool TxtReaderActivity::loadPageIndexCache() { - // Cache file format: - // - 4 bytes: magic "TXTI" - // - 4 bytes: file size (to validate cache) - // - 4 bytes: viewport width - // - 4 bytes: lines per page - // - 4 bytes: total pages count - // - N * 4 bytes: page offsets (size_t stored as uint32_t) + // Cache file format (using serialization module): + // - uint32_t: magic "TXTI" + // - uint8_t: cache version + // - uint32_t: file size (to validate cache) + // - int32_t: viewport width + // - int32_t: lines per page + // - int32_t: font ID (to invalidate cache on font change) + // - int32_t: screen margin (to invalidate cache on margin change) + // - uint8_t: paragraph alignment (to invalidate cache on alignment change) + // - uint32_t: total pages count + // - N * uint32_t: page offsets std::string cachePath = txt->getCachePath() + "/index.bin"; FsFile f; @@ -507,58 +571,81 @@ bool TxtReaderActivity::loadPageIndexCache() { return false; } - // Read and validate header - uint8_t header[20]; - if (f.read(header, 20) != 20) { + // Read and validate header using serialization module + uint32_t magic; + serialization::readPod(f, magic); + if (magic != CACHE_MAGIC) { + Serial.printf("[%lu] [TRS] Cache magic mismatch, rebuilding\n", millis()); f.close(); return false; } - // Check magic - if (header[0] != 'T' || header[1] != 'X' || header[2] != 'T' || header[3] != 'I') { + uint8_t version; + serialization::readPod(f, version); + if (version != CACHE_VERSION) { + Serial.printf("[%lu] [TRS] Cache version mismatch (%d != %d), rebuilding\n", millis(), version, CACHE_VERSION); f.close(); return false; } - // Check file size matches - uint32_t cachedFileSize = header[4] | (header[5] << 8) | (header[6] << 16) | (header[7] << 24); - if (cachedFileSize != txt->getFileSize()) { + uint32_t fileSize; + serialization::readPod(f, fileSize); + if (fileSize != txt->getFileSize()) { Serial.printf("[%lu] [TRS] Cache file size mismatch, rebuilding\n", millis()); f.close(); return false; } - // Check viewport width matches - uint32_t cachedViewportWidth = header[8] | (header[9] << 8) | (header[10] << 16) | (header[11] << 24); - if (static_cast(cachedViewportWidth) != viewportWidth) { + int32_t cachedWidth; + serialization::readPod(f, cachedWidth); + if (cachedWidth != viewportWidth) { Serial.printf("[%lu] [TRS] Cache viewport width mismatch, rebuilding\n", millis()); f.close(); return false; } - // Check lines per page matches - uint32_t cachedLinesPerPage = header[12] | (header[13] << 8) | (header[14] << 16) | (header[15] << 24); - if (static_cast(cachedLinesPerPage) != linesPerPage) { + int32_t cachedLines; + serialization::readPod(f, cachedLines); + if (cachedLines != linesPerPage) { Serial.printf("[%lu] [TRS] Cache lines per page mismatch, rebuilding\n", millis()); f.close(); return false; } - // Read total pages - uint32_t cachedTotalPages = header[16] | (header[17] << 8) | (header[18] << 16) | (header[19] << 24); + int32_t fontId; + serialization::readPod(f, fontId); + if (fontId != cachedFontId) { + Serial.printf("[%lu] [TRS] Cache font ID mismatch (%d != %d), rebuilding\n", millis(), fontId, cachedFontId); + f.close(); + return false; + } + + int32_t margin; + serialization::readPod(f, margin); + if (margin != cachedScreenMargin) { + Serial.printf("[%lu] [TRS] Cache screen margin mismatch, rebuilding\n", millis()); + f.close(); + return false; + } + + uint8_t alignment; + serialization::readPod(f, alignment); + if (alignment != cachedParagraphAlignment) { + Serial.printf("[%lu] [TRS] Cache paragraph alignment mismatch, rebuilding\n", millis()); + f.close(); + return false; + } + + uint32_t numPages; + serialization::readPod(f, numPages); // Read page offsets pageOffsets.clear(); - pageOffsets.reserve(cachedTotalPages); + pageOffsets.reserve(numPages); - for (uint32_t i = 0; i < cachedTotalPages; i++) { - uint8_t offsetData[4]; - if (f.read(offsetData, 4) != 4) { - f.close(); - pageOffsets.clear(); - return false; - } - uint32_t offset = offsetData[0] | (offsetData[1] << 8) | (offsetData[2] << 16) | (offsetData[3] << 24); + for (uint32_t i = 0; i < numPages; i++) { + uint32_t offset; + serialization::readPod(f, offset); pageOffsets.push_back(offset); } @@ -576,49 +663,20 @@ void TxtReaderActivity::savePageIndexCache() const { return; } - // Write header - uint8_t header[20]; - header[0] = 'T'; - header[1] = 'X'; - header[2] = 'T'; - header[3] = 'I'; - - // File size - uint32_t fileSize = txt->getFileSize(); - header[4] = fileSize & 0xFF; - header[5] = (fileSize >> 8) & 0xFF; - header[6] = (fileSize >> 16) & 0xFF; - header[7] = (fileSize >> 24) & 0xFF; - - // Viewport width - header[8] = viewportWidth & 0xFF; - header[9] = (viewportWidth >> 8) & 0xFF; - header[10] = (viewportWidth >> 16) & 0xFF; - header[11] = (viewportWidth >> 24) & 0xFF; - - // Lines per page - header[12] = linesPerPage & 0xFF; - header[13] = (linesPerPage >> 8) & 0xFF; - header[14] = (linesPerPage >> 16) & 0xFF; - header[15] = (linesPerPage >> 24) & 0xFF; - - // Total pages - uint32_t numPages = pageOffsets.size(); - header[16] = numPages & 0xFF; - header[17] = (numPages >> 8) & 0xFF; - header[18] = (numPages >> 16) & 0xFF; - header[19] = (numPages >> 24) & 0xFF; - - f.write(header, 20); + // Write header using serialization module + serialization::writePod(f, CACHE_MAGIC); + serialization::writePod(f, CACHE_VERSION); + serialization::writePod(f, static_cast(txt->getFileSize())); + serialization::writePod(f, static_cast(viewportWidth)); + serialization::writePod(f, static_cast(linesPerPage)); + serialization::writePod(f, static_cast(cachedFontId)); + serialization::writePod(f, static_cast(cachedScreenMargin)); + serialization::writePod(f, cachedParagraphAlignment); + serialization::writePod(f, static_cast(pageOffsets.size())); // Write page offsets for (size_t offset : pageOffsets) { - uint8_t offsetData[4]; - offsetData[0] = offset & 0xFF; - offsetData[1] = (offset >> 8) & 0xFF; - offsetData[2] = (offset >> 16) & 0xFF; - offsetData[3] = (offset >> 24) & 0xFF; - f.write(offsetData, 4); + serialization::writePod(f, static_cast(offset)); } f.close(); diff --git a/src/activities/reader/TxtReaderActivity.h b/src/activities/reader/TxtReaderActivity.h index 0b9a64fd..41ccbfbb 100644 --- a/src/activities/reader/TxtReaderActivity.h +++ b/src/activities/reader/TxtReaderActivity.h @@ -7,6 +7,7 @@ #include +#include "CrossPointSettings.h" #include "activities/ActivityWithSubactivity.h" class TxtReaderActivity final : public ActivityWithSubactivity { @@ -27,6 +28,11 @@ class TxtReaderActivity final : public ActivityWithSubactivity { int viewportWidth = 0; bool initialized = false; + // Cached settings for cache validation (different fonts/margins require re-indexing) + int cachedFontId = 0; + int cachedScreenMargin = 0; + uint8_t cachedParagraphAlignment = CrossPointSettings::LEFT_ALIGN; + static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); void renderScreen();