#include "SleepActivity.h" #include #include #include #include #include #include #include #include #include "CrossPointSettings.h" #include "CrossPointState.h" #include "components/UITheme.h" #include "fontIds.h" #include "images/Logo120.h" #include "util/StringUtils.h" namespace { // Number of source pixels along the image edge to average for the gradient color constexpr int EDGE_SAMPLE_DEPTH = 20; // Map a 2-bit quantized pixel value to an 8-bit grayscale value constexpr uint8_t val2bitToGray(uint8_t val2bit) { return val2bit * 85; } // Edge gradient data produced by sampleBitmapEdges and consumed by drawLetterboxGradients. // edgeA is the "first" edge (top or left), edgeB is the "second" edge (bottom or right). struct LetterboxGradientData { uint8_t* edgeA = nullptr; uint8_t* edgeB = nullptr; int edgeCount = 0; int letterboxA = 0; // pixel size of the first letterbox area (top or left) int letterboxB = 0; // pixel size of the second letterbox area (bottom or right) bool horizontal = false; // true = top/bottom letterbox, false = left/right void free() { ::free(edgeA); ::free(edgeB); edgeA = nullptr; edgeB = nullptr; } }; // Binary cache version for edge data files constexpr uint8_t EDGE_CACHE_VERSION = 1; // Load cached edge data from a binary file. Returns true if the cache was valid and loaded successfully. // Validates cache version and screen dimensions to detect stale data. bool loadEdgeCache(const std::string& path, int screenWidth, int screenHeight, LetterboxGradientData& data) { FsFile file; if (!Storage.openFileForRead("SLP", path, file)) return false; uint8_t version; serialization::readPod(file, version); if (version != EDGE_CACHE_VERSION) { file.close(); return false; } uint16_t cachedW, cachedH; serialization::readPod(file, cachedW); serialization::readPod(file, cachedH); if (cachedW != static_cast(screenWidth) || cachedH != static_cast(screenHeight)) { file.close(); return false; } uint8_t horizontal; serialization::readPod(file, horizontal); data.horizontal = (horizontal != 0); uint16_t edgeCount; serialization::readPod(file, edgeCount); data.edgeCount = edgeCount; int16_t lbA, lbB; serialization::readPod(file, lbA); serialization::readPod(file, lbB); data.letterboxA = lbA; data.letterboxB = lbB; if (edgeCount == 0 || edgeCount > 2048) { file.close(); return false; } data.edgeA = static_cast(malloc(edgeCount)); data.edgeB = static_cast(malloc(edgeCount)); if (!data.edgeA || !data.edgeB) { data.free(); file.close(); return false; } if (file.read(data.edgeA, edgeCount) != static_cast(edgeCount) || file.read(data.edgeB, edgeCount) != static_cast(edgeCount)) { data.free(); file.close(); return false; } file.close(); Serial.printf("[%lu] [SLP] Loaded edge cache from %s (%d edges)\n", millis(), path.c_str(), edgeCount); return true; } // Save edge data to a binary cache file for reuse on subsequent sleep screens. bool saveEdgeCache(const std::string& path, int screenWidth, int screenHeight, const LetterboxGradientData& data) { if (!data.edgeA || !data.edgeB || data.edgeCount <= 0) return false; FsFile file; if (!Storage.openFileForWrite("SLP", path, file)) return false; serialization::writePod(file, EDGE_CACHE_VERSION); serialization::writePod(file, static_cast(screenWidth)); serialization::writePod(file, static_cast(screenHeight)); serialization::writePod(file, static_cast(data.horizontal ? 1 : 0)); serialization::writePod(file, static_cast(data.edgeCount)); serialization::writePod(file, static_cast(data.letterboxA)); serialization::writePod(file, static_cast(data.letterboxB)); file.write(data.edgeA, data.edgeCount); file.write(data.edgeB, data.edgeCount); file.close(); Serial.printf("[%lu] [SLP] Saved edge cache to %s (%d edges)\n", millis(), path.c_str(), data.edgeCount); return true; } // Read the bitmap once to sample the first/last EDGE_SAMPLE_DEPTH rows or columns. // Returns edge color arrays in source pixel resolution. Caller must call data.free() when done. // After sampling the bitmap is rewound via rewindToData(). LetterboxGradientData sampleBitmapEdges(const Bitmap& bitmap, int imgX, int imgY, int pageWidth, int pageHeight, float scale, float cropX, float cropY) { LetterboxGradientData data; const int cropPixX = static_cast(std::floor(bitmap.getWidth() * cropX / 2.0f)); const int cropPixY = static_cast(std::floor(bitmap.getHeight() * cropY / 2.0f)); const int visibleWidth = bitmap.getWidth() - 2 * cropPixX; const int visibleHeight = bitmap.getHeight() - 2 * cropPixY; if (visibleWidth <= 0 || visibleHeight <= 0) return data; const int outputRowSize = (bitmap.getWidth() + 3) / 4; auto* outputRow = static_cast(malloc(outputRowSize)); auto* rowBytes = static_cast(malloc(bitmap.getRowBytes())); if (!outputRow || !rowBytes) { ::free(outputRow); ::free(rowBytes); return data; } if (imgY > 0) { // Top/bottom letterboxing -- sample per-column averages of first/last N rows data.horizontal = true; data.edgeCount = visibleWidth; const int scaledHeight = static_cast(std::round(static_cast(visibleHeight) * scale)); data.letterboxA = imgY; data.letterboxB = pageHeight - imgY - scaledHeight; if (data.letterboxB < 0) data.letterboxB = 0; const int sampleRows = std::min(EDGE_SAMPLE_DEPTH, visibleHeight); auto* accumTop = static_cast(calloc(visibleWidth, sizeof(uint32_t))); auto* accumBot = static_cast(calloc(visibleWidth, sizeof(uint32_t))); data.edgeA = static_cast(malloc(visibleWidth)); data.edgeB = static_cast(malloc(visibleWidth)); if (!accumTop || !accumBot || !data.edgeA || !data.edgeB) { ::free(accumTop); ::free(accumBot); data.free(); ::free(outputRow); ::free(rowBytes); return data; } for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) { if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) break; const int logicalY = bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY; if (logicalY < cropPixY || logicalY >= bitmap.getHeight() - cropPixY) continue; const int outY = logicalY - cropPixY; const bool inTop = (outY < sampleRows); const bool inBot = (outY >= visibleHeight - sampleRows); if (!inTop && !inBot) continue; for (int bmpX = cropPixX; bmpX < bitmap.getWidth() - cropPixX; bmpX++) { const int outX = bmpX - cropPixX; const uint8_t val = (outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8))) & 0x3; const uint8_t gray = val2bitToGray(val); if (inTop) accumTop[outX] += gray; if (inBot) accumBot[outX] += gray; } } for (int i = 0; i < visibleWidth; i++) { data.edgeA[i] = static_cast(accumTop[i] / sampleRows); data.edgeB[i] = static_cast(accumBot[i] / sampleRows); } ::free(accumTop); ::free(accumBot); } else if (imgX > 0) { // Left/right letterboxing -- sample per-row averages of first/last N columns data.horizontal = false; data.edgeCount = visibleHeight; const int scaledWidth = static_cast(std::round(static_cast(visibleWidth) * scale)); data.letterboxA = imgX; data.letterboxB = pageWidth - imgX - scaledWidth; if (data.letterboxB < 0) data.letterboxB = 0; const int sampleCols = std::min(EDGE_SAMPLE_DEPTH, visibleWidth); auto* accumLeft = static_cast(calloc(visibleHeight, sizeof(uint32_t))); auto* accumRight = static_cast(calloc(visibleHeight, sizeof(uint32_t))); data.edgeA = static_cast(malloc(visibleHeight)); data.edgeB = static_cast(malloc(visibleHeight)); if (!accumLeft || !accumRight || !data.edgeA || !data.edgeB) { ::free(accumLeft); ::free(accumRight); data.free(); ::free(outputRow); ::free(rowBytes); return data; } for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) { if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) break; const int logicalY = bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY; if (logicalY < cropPixY || logicalY >= bitmap.getHeight() - cropPixY) continue; const int outY = logicalY - cropPixY; // Sample left edge columns for (int bmpX = cropPixX; bmpX < cropPixX + sampleCols; bmpX++) { const uint8_t val = (outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8))) & 0x3; accumLeft[outY] += val2bitToGray(val); } // Sample right edge columns for (int bmpX = bitmap.getWidth() - cropPixX - sampleCols; bmpX < bitmap.getWidth() - cropPixX; bmpX++) { const uint8_t val = (outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8))) & 0x3; accumRight[outY] += val2bitToGray(val); } } for (int i = 0; i < visibleHeight; i++) { data.edgeA[i] = static_cast(accumLeft[i] / sampleCols); data.edgeB[i] = static_cast(accumRight[i] / sampleCols); } ::free(accumLeft); ::free(accumRight); } ::free(outputRow); ::free(rowBytes); bitmap.rewindToData(); return data; } // Draw dithered fills in the letterbox areas using the sampled edge colors. // fillMode selects the fill algorithm: SOLID (single dominant shade), BLENDED (per-pixel edge color), // or GRADIENT (per-pixel edge color interpolated toward targetColor). // targetColor is the color the gradient fades toward (255=white, 0=black); only used in GRADIENT mode. // Must be called once per render pass (BW, GRAYSCALE_LSB, GRAYSCALE_MSB). void drawLetterboxFill(GfxRenderer& renderer, const LetterboxGradientData& data, float scale, uint8_t fillMode, int targetColor) { if (!data.edgeA || !data.edgeB || data.edgeCount <= 0) return; const bool isSolid = (fillMode == CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL::LETTERBOX_SOLID); const bool isGradient = (fillMode == CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL::LETTERBOX_GRADIENT); // For SOLID mode, compute the dominant (average) shade for each edge once uint8_t solidColorA = 0, solidColorB = 0; if (isSolid) { uint32_t sumA = 0, sumB = 0; for (int i = 0; i < data.edgeCount; i++) { sumA += data.edgeA[i]; sumB += data.edgeB[i]; } solidColorA = static_cast(sumA / data.edgeCount); solidColorB = static_cast(sumB / data.edgeCount); } // Helper: compute gray value for a pixel given the edge color and interpolation factor t (0..1) // GRADIENT interpolates from edgeColor toward targetColor; SOLID and BLENDED return edgeColor directly. auto computeGray = [&](int edgeColor, float t) -> int { if (isGradient) return edgeColor + static_cast(static_cast(targetColor - edgeColor) * t); return edgeColor; }; if (data.horizontal) { // Top letterbox if (data.letterboxA > 0) { const int imgTopY = data.letterboxA; for (int screenY = 0; screenY < imgTopY; screenY++) { const float t = static_cast(imgTopY - screenY) / static_cast(imgTopY); for (int screenX = 0; screenX < renderer.getScreenWidth(); screenX++) { int edgeColor; if (isSolid) { edgeColor = solidColorA; } else { int srcCol = static_cast(screenX / scale); srcCol = std::max(0, std::min(srcCol, data.edgeCount - 1)); edgeColor = data.edgeA[srcCol]; } const int gray = computeGray(edgeColor, t); renderer.drawPixelGray(screenX, screenY, quantizeNoiseDither(gray, screenX, screenY)); } } } // Bottom letterbox if (data.letterboxB > 0) { const int imgBottomY = renderer.getScreenHeight() - data.letterboxB; for (int screenY = imgBottomY; screenY < renderer.getScreenHeight(); screenY++) { const float t = static_cast(screenY - imgBottomY + 1) / static_cast(data.letterboxB); for (int screenX = 0; screenX < renderer.getScreenWidth(); screenX++) { int edgeColor; if (isSolid) { edgeColor = solidColorB; } else { int srcCol = static_cast(screenX / scale); srcCol = std::max(0, std::min(srcCol, data.edgeCount - 1)); edgeColor = data.edgeB[srcCol]; } const int gray = computeGray(edgeColor, t); renderer.drawPixelGray(screenX, screenY, quantizeNoiseDither(gray, screenX, screenY)); } } } } else { // Left letterbox if (data.letterboxA > 0) { const int imgLeftX = data.letterboxA; for (int screenX = 0; screenX < imgLeftX; screenX++) { const float t = static_cast(imgLeftX - screenX) / static_cast(imgLeftX); for (int screenY = 0; screenY < renderer.getScreenHeight(); screenY++) { int edgeColor; if (isSolid) { edgeColor = solidColorA; } else { int srcRow = static_cast(screenY / scale); srcRow = std::max(0, std::min(srcRow, data.edgeCount - 1)); edgeColor = data.edgeA[srcRow]; } const int gray = computeGray(edgeColor, t); renderer.drawPixelGray(screenX, screenY, quantizeNoiseDither(gray, screenX, screenY)); } } } // Right letterbox if (data.letterboxB > 0) { const int imgRightX = renderer.getScreenWidth() - data.letterboxB; for (int screenX = imgRightX; screenX < renderer.getScreenWidth(); screenX++) { const float t = static_cast(screenX - imgRightX + 1) / static_cast(data.letterboxB); for (int screenY = 0; screenY < renderer.getScreenHeight(); screenY++) { int edgeColor; if (isSolid) { edgeColor = solidColorB; } else { int srcRow = static_cast(screenY / scale); srcRow = std::max(0, std::min(srcRow, data.edgeCount - 1)); edgeColor = data.edgeB[srcRow]; } const int gray = computeGray(edgeColor, t); renderer.drawPixelGray(screenX, screenY, quantizeNoiseDither(gray, screenX, screenY)); } } } } } } // namespace void SleepActivity::onEnter() { Activity::onEnter(); GUI.drawPopup(renderer, "Entering Sleep..."); switch (SETTINGS.sleepScreen) { case (CrossPointSettings::SLEEP_SCREEN_MODE::BLANK): return renderBlankSleepScreen(); case (CrossPointSettings::SLEEP_SCREEN_MODE::CUSTOM): return renderCustomSleepScreen(); case (CrossPointSettings::SLEEP_SCREEN_MODE::COVER): case (CrossPointSettings::SLEEP_SCREEN_MODE::COVER_CUSTOM): return renderCoverSleepScreen(); default: return renderDefaultSleepScreen(); } } void SleepActivity::renderCustomSleepScreen() const { // Check if we have a /sleep directory auto dir = Storage.open("/sleep"); if (dir && dir.isDirectory()) { std::vector files; char name[500]; // collect all valid BMP files for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { if (file.isDirectory()) { file.close(); continue; } file.getName(name, sizeof(name)); auto filename = std::string(name); if (filename[0] == '.') { file.close(); continue; } if (filename.substr(filename.length() - 4) != ".bmp") { Serial.printf("[%lu] [SLP] Skipping non-.bmp file name: %s\n", millis(), name); file.close(); continue; } Bitmap bitmap(file); if (bitmap.parseHeaders() != BmpReaderError::Ok) { Serial.printf("[%lu] [SLP] Skipping invalid BMP file: %s\n", millis(), name); file.close(); continue; } files.emplace_back(filename); file.close(); } const auto numFiles = files.size(); if (numFiles > 0) { // Generate a random number between 1 and numFiles auto randomFileIndex = random(numFiles); // If we picked the same image as last time, reroll while (numFiles > 1 && randomFileIndex == APP_STATE.lastSleepImage) { randomFileIndex = random(numFiles); } APP_STATE.lastSleepImage = randomFileIndex; APP_STATE.saveToFile(); const auto filename = "/sleep/" + files[randomFileIndex]; FsFile file; if (Storage.openFileForRead("SLP", filename, file)) { Serial.printf("[%lu] [SLP] Randomly loading: /sleep/%s\n", millis(), files[randomFileIndex].c_str()); delay(100); Bitmap bitmap(file, true); if (bitmap.parseHeaders() == BmpReaderError::Ok) { renderBitmapSleepScreen(bitmap); dir.close(); return; } } } } if (dir) dir.close(); // Look for sleep.bmp on the root of the sd card to determine if we should // render a custom sleep screen instead of the default. FsFile file; if (Storage.openFileForRead("SLP", "/sleep.bmp", file)) { Bitmap bitmap(file, true); if (bitmap.parseHeaders() == BmpReaderError::Ok) { Serial.printf("[%lu] [SLP] Loading: /sleep.bmp\n", millis()); renderBitmapSleepScreen(bitmap); return; } } renderDefaultSleepScreen(); } void SleepActivity::renderDefaultSleepScreen() const { const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); renderer.clearScreen(); renderer.drawImage(Logo120, (pageWidth - 120) / 2, (pageHeight - 120) / 2, 120, 120); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, EpdFontFamily::BOLD); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING"); // Make sleep screen dark unless light is selected in settings if (SETTINGS.sleepScreen != CrossPointSettings::SLEEP_SCREEN_MODE::LIGHT) { renderer.invertScreen(); } renderer.displayBuffer(HalDisplay::HALF_REFRESH); } void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::string& edgeCachePath) const { int x, y; const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); float cropX = 0, cropY = 0; Serial.printf("[%lu] [SLP] bitmap %d x %d, screen %d x %d\n", millis(), bitmap.getWidth(), bitmap.getHeight(), pageWidth, pageHeight); // Always compute aspect-ratio-preserving scale and position (supports both larger and smaller images) float ratio = static_cast(bitmap.getWidth()) / static_cast(bitmap.getHeight()); const float screenRatio = static_cast(pageWidth) / static_cast(pageHeight); Serial.printf("[%lu] [SLP] bitmap ratio: %f, screen ratio: %f\n", millis(), ratio, screenRatio); if (ratio > screenRatio) { // image wider than viewport ratio, needs to be centered vertically if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) { cropX = 1.0f - (screenRatio / ratio); Serial.printf("[%lu] [SLP] Cropping bitmap x: %f\n", millis(), cropX); ratio = (1.0f - cropX) * static_cast(bitmap.getWidth()) / static_cast(bitmap.getHeight()); } x = 0; y = std::round((static_cast(pageHeight) - static_cast(pageWidth) / ratio) / 2); Serial.printf("[%lu] [SLP] Centering with ratio %f to y=%d\n", millis(), ratio, y); } else { // image taller than or equal to viewport ratio, needs to be centered horizontally if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) { cropY = 1.0f - (ratio / screenRatio); Serial.printf("[%lu] [SLP] Cropping bitmap y: %f\n", millis(), cropY); ratio = static_cast(bitmap.getWidth()) / ((1.0f - cropY) * static_cast(bitmap.getHeight())); } x = std::round((static_cast(pageWidth) - static_cast(pageHeight) * ratio) / 2); y = 0; Serial.printf("[%lu] [SLP] Centering with ratio %f to x=%d\n", millis(), ratio, x); } Serial.printf("[%lu] [SLP] drawing to %d x %d\n", millis(), x, y); // Compute the scale factor (same formula as drawBitmap) so we can map screen coords to source coords const float effectiveWidth = (1.0f - cropX) * bitmap.getWidth(); const float effectiveHeight = (1.0f - cropY) * bitmap.getHeight(); const float scale = std::min(static_cast(pageWidth) / effectiveWidth, static_cast(pageHeight) / effectiveHeight); // Determine letterbox fill settings const uint8_t fillMode = SETTINGS.sleepScreenLetterboxFill; const bool wantFill = (fillMode != CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL::LETTERBOX_NONE); const int targetColor = (SETTINGS.sleepScreenGradientDir == CrossPointSettings::SLEEP_SCREEN_GRADIENT_DIR::GRADIENT_TO_BLACK) ? 0 : 255; static const char* fillModeNames[] = {"none", "solid", "blended", "gradient"}; const char* fillModeName = (fillMode < 4) ? fillModeNames[fillMode] : "unknown"; // Load cached edge data or sample from bitmap (first pass over bitmap, then rewind) LetterboxGradientData gradientData; const bool hasLetterbox = (x > 0 || y > 0); if (hasLetterbox && wantFill) { bool cacheLoaded = false; if (!edgeCachePath.empty()) { cacheLoaded = loadEdgeCache(edgeCachePath, pageWidth, pageHeight, gradientData); } if (!cacheLoaded) { Serial.printf("[%lu] [SLP] Letterbox detected (x=%d, y=%d), sampling edges for %s fill\n", millis(), x, y, fillModeName); gradientData = sampleBitmapEdges(bitmap, x, y, pageWidth, pageHeight, scale, cropX, cropY); if (!edgeCachePath.empty() && gradientData.edgeA) { saveEdgeCache(edgeCachePath, pageWidth, pageHeight, gradientData); } } } renderer.clearScreen(); const bool hasGreyscale = bitmap.hasGreyscale() && SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::NO_FILTER; // Draw letterbox fill (BW pass) if (gradientData.edgeA) { drawLetterboxFill(renderer, gradientData, scale, fillMode, targetColor); } renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY); if (SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::INVERTED_BLACK_AND_WHITE) { renderer.invertScreen(); } renderer.displayBuffer(HalDisplay::HALF_REFRESH); if (hasGreyscale) { bitmap.rewindToData(); renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); if (gradientData.edgeA) { drawLetterboxFill(renderer, gradientData, scale, fillMode, targetColor); } renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY); renderer.copyGrayscaleLsbBuffers(); bitmap.rewindToData(); renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); if (gradientData.edgeA) { drawLetterboxFill(renderer, gradientData, scale, fillMode, targetColor); } renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY); renderer.copyGrayscaleMsbBuffers(); renderer.displayGrayBuffer(); renderer.setRenderMode(GfxRenderer::BW); } gradientData.free(); } void SleepActivity::renderCoverSleepScreen() const { void (SleepActivity::*renderNoCoverSleepScreen)() const; switch (SETTINGS.sleepScreen) { case (CrossPointSettings::SLEEP_SCREEN_MODE::COVER_CUSTOM): renderNoCoverSleepScreen = &SleepActivity::renderCustomSleepScreen; break; default: renderNoCoverSleepScreen = &SleepActivity::renderDefaultSleepScreen; break; } if (APP_STATE.openEpubPath.empty()) { return (this->*renderNoCoverSleepScreen)(); } std::string coverBmpPath; bool cropped = SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP; // Check if the current book is XTC, TXT, or EPUB if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtc") || StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtch")) { // Handle XTC file Xtc lastXtc(APP_STATE.openEpubPath, "/.crosspoint"); if (!lastXtc.load()) { Serial.printf("[%lu] [SLP] Failed to load last XTC\n", millis()); return (this->*renderNoCoverSleepScreen)(); } if (!lastXtc.generateCoverBmp()) { Serial.printf("[%lu] [SLP] Failed to generate XTC cover bmp\n", millis()); return (this->*renderNoCoverSleepScreen)(); } coverBmpPath = lastXtc.getCoverBmpPath(); } else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".txt")) { // Handle TXT file - looks for cover image in the same folder Txt lastTxt(APP_STATE.openEpubPath, "/.crosspoint"); if (!lastTxt.load()) { Serial.printf("[%lu] [SLP] Failed to load last TXT\n", millis()); return (this->*renderNoCoverSleepScreen)(); } if (!lastTxt.generateCoverBmp()) { Serial.printf("[%lu] [SLP] No cover image found for TXT file\n", millis()); return (this->*renderNoCoverSleepScreen)(); } coverBmpPath = lastTxt.getCoverBmpPath(); } else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".epub")) { // Handle EPUB file Epub lastEpub(APP_STATE.openEpubPath, "/.crosspoint"); // Skip loading css since we only need metadata here if (!lastEpub.load(true, true)) { Serial.printf("[%lu] [SLP] Failed to load last epub\n", millis()); return (this->*renderNoCoverSleepScreen)(); } if (!lastEpub.generateCoverBmp(cropped)) { Serial.printf("[%lu] [SLP] Failed to generate cover bmp\n", millis()); return (this->*renderNoCoverSleepScreen)(); } coverBmpPath = lastEpub.getCoverBmpPath(cropped); } else { return (this->*renderNoCoverSleepScreen)(); } // Derive edge cache path from cover BMP path (e.g. cover.bmp -> cover_edges.bin) std::string edgeCachePath; if (coverBmpPath.size() > 4) { edgeCachePath = coverBmpPath.substr(0, coverBmpPath.size() - 4) + "_edges.bin"; } FsFile file; if (Storage.openFileForRead("SLP", coverBmpPath, file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { Serial.printf("[%lu] [SLP] Rendering sleep cover: %s\n", millis(), coverBmpPath.c_str()); renderBitmapSleepScreen(bitmap, edgeCachePath); return; } } return (this->*renderNoCoverSleepScreen)(); } void SleepActivity::renderBlankSleepScreen() const { renderer.clearScreen(); renderer.displayBuffer(HalDisplay::HALF_REFRESH); }