Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
36 kB · 894 lines
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895#include "SleepActivity.h"
#include <BitmapHelpers.h>#include <Epub.h>#include <Epub/converters/PngToFramebufferConverter.h>#include <FontCacheManager.h>#include <FsHelpers.h>#include <GfxRenderer.h>#include <HalDisplay.h>#include <HalGPIO.h>#include <HalStorage.h>#include <I18n.h>#include <Memory.h>#include <PNGdec.h>#include <Txt.h>#include <Xtc.h>
#include <algorithm>#include <cmath>#include <cstdint>#include <cstring>#include <limits>#include <string>
#include "CrossPointSettings.h"#include "CrossPointState.h"#include "activities/reader/ReaderUtils.h"#include "components/UITheme.h"#include "fontIds.h"#include "images/Logo120.h"#include "images/MoonIcon.h"
namespace {
HalDisplay::GrayscaleMode sleepGrayscaleMode(const GfxRenderer& renderer) { return renderer.grayscaleCapabilities(HalDisplay::GrayscaleMode::Direct).supported() ? HalDisplay::GrayscaleMode::Direct : HalDisplay::GrayscaleMode::Absolute;}
// Kept separate from /sleep.bmp and /.sleep so alpha-overlay art does not mix with full-screen wallpapers.constexpr char TRANSPARENT_SLEEP_ROOT_BMP[] = "/sleep-overlay.bmp";constexpr char TRANSPARENT_SLEEP_ROOT_PNG[] = "/sleep-overlay.png";constexpr char TRANSPARENT_SLEEP_DIR[] = "/.sleep-overlay";constexpr char TRANSPARENT_SLEEP_LEGACY_DIR[] = "/sleep-overlay";constexpr size_t MAX_SLEEP_FILE_NAME_LEN = 256;constexpr uint8_t MIN_VISIBLE_ALPHA = 8;
struct BitmapPlacement { int x = 0; int y = 0; float cropX = 0.0f; float cropY = 0.0f;};
struct OverlayBmpInfo { int width = 0; int height = 0; bool topDown = false; uint32_t dataOffset = 0; uint32_t rowBytes = 0;};
uint16_t readLE16(HalFile& file) { const int c0 = file.read(); const int c1 = file.read(); const auto b0 = static_cast<uint8_t>(c0 < 0 ? 0 : c0); const auto b1 = static_cast<uint8_t>(c1 < 0 ? 0 : c1); return static_cast<uint16_t>(b0) | (static_cast<uint16_t>(b1) << 8);}
uint32_t readLE32(HalFile& file) { const int c0 = file.read(); const int c1 = file.read(); const int c2 = file.read(); const int c3 = file.read(); const auto b0 = static_cast<uint8_t>(c0 < 0 ? 0 : c0); const auto b1 = static_cast<uint8_t>(c1 < 0 ? 0 : c1); const auto b2 = static_cast<uint8_t>(c2 < 0 ? 0 : c2); const auto b3 = static_cast<uint8_t>(c3 < 0 ? 0 : c3); return static_cast<uint32_t>(b0) | (static_cast<uint32_t>(b1) << 8) | (static_cast<uint32_t>(b2) << 16) | (static_cast<uint32_t>(b3) << 24);}
uint32_t readBE32(HalFile& file) { const int c0 = file.read(); const int c1 = file.read(); const int c2 = file.read(); const int c3 = file.read(); if (c0 < 0 || c1 < 0 || c2 < 0 || c3 < 0) return 0; return (static_cast<uint32_t>(c0) << 24) | (static_cast<uint32_t>(c1) << 16) | (static_cast<uint32_t>(c2) << 8) | static_cast<uint32_t>(c3);}
bool isValidPngHeader(HalFile& file) { static constexpr uint8_t PNG_SIGNATURE[8] = {0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}; static constexpr uint32_t MAX_SOURCE_PIXELS = 2048u * 1536u; uint8_t signature[8]; if (!file.seek(0) || file.read(signature, sizeof(signature)) != static_cast<int>(sizeof(signature)) || !std::equal(std::begin(signature), std::end(signature), std::begin(PNG_SIGNATURE))) { return false; }
const uint32_t ihdrLength = readBE32(file); char chunkType[4]; if (file.read(reinterpret_cast<uint8_t*>(chunkType), sizeof(chunkType)) != static_cast<int>(sizeof(chunkType)) || ihdrLength != 13 || !std::equal(std::begin(chunkType), std::end(chunkType), "IHDR")) { return false; }
const uint32_t width = readBE32(file); const uint32_t height = readBE32(file); const int bitDepth = file.read(); const int colorType = file.read(); const int compression = file.read(); const int filter = file.read(); const int interlace = file.read();
const bool supportedBitDepth = bitDepth == 8 || ((colorType == PNG_PIXEL_GRAYSCALE || colorType == PNG_PIXEL_INDEXED) && (bitDepth == 1 || bitDepth == 2 || bitDepth == 4)); const bool supportedColorType = colorType == PNG_PIXEL_GRAYSCALE || colorType == PNG_PIXEL_TRUECOLOR || colorType == PNG_PIXEL_INDEXED || colorType == PNG_PIXEL_GRAY_ALPHA || colorType == PNG_PIXEL_TRUECOLOR_ALPHA; return width > 0 && height > 0 && width <= 2048 && height <= 3072 && width * height <= MAX_SOURCE_PIXELS && supportedBitDepth && supportedColorType && compression == 0 && filter == 0 && interlace == 0;}
BitmapPlacement calculateBitmapPlacement(const int bitmapWidth, const int bitmapHeight, const GfxRenderer& renderer) { BitmapPlacement placement; const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight();
if (bitmapWidth > pageWidth || bitmapHeight > pageHeight) { float ratio = static_cast<float>(bitmapWidth) / static_cast<float>(bitmapHeight); const float screenRatio = static_cast<float>(pageWidth) / static_cast<float>(pageHeight);
if (ratio > screenRatio) { if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) { placement.cropX = 1.0f - (screenRatio / ratio); ratio = (1.0f - placement.cropX) * static_cast<float>(bitmapWidth) / static_cast<float>(bitmapHeight); } placement.x = 0; placement.y = std::round((static_cast<float>(pageHeight) - static_cast<float>(pageWidth) / ratio) / 2); } else { if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) { placement.cropY = 1.0f - (ratio / screenRatio); ratio = static_cast<float>(bitmapWidth) / ((1.0f - placement.cropY) * static_cast<float>(bitmapHeight)); } placement.x = std::round((static_cast<float>(pageWidth) - static_cast<float>(pageHeight) * ratio) / 2); placement.y = 0; } } else { placement.x = (pageWidth - bitmapWidth) / 2; placement.y = (pageHeight - bitmapHeight) / 2; }
return placement;}
bool parseOverlayBmpHeader(HalFile& file, OverlayBmpInfo& info, const bool logErrors) { if (!file) return false; if (!file.seek(0)) return false;
if (readLE16(file) != 0x4D42) { if (logErrors) LOG_ERR("SLP", "Transparent overlay is not a BMP"); return false; }
file.seekCur(8); info.dataOffset = readLE32(file);
const uint32_t dibSize = readLE32(file); if (dibSize < 40) { if (logErrors) LOG_ERR("SLP", "Unsupported BMP DIB header: %u", static_cast<unsigned>(dibSize)); return false; }
info.width = static_cast<int32_t>(readLE32(file)); const auto rawHeight = static_cast<int32_t>(readLE32(file)); if (rawHeight == std::numeric_limits<int32_t>::min()) { if (logErrors) LOG_ERR("SLP", "Bad transparent overlay dimensions: %dx%d", info.width, rawHeight); return false; } info.topDown = rawHeight < 0; info.height = info.topDown ? -rawHeight : rawHeight;
const uint16_t planes = readLE16(file); const uint16_t bpp = readLE16(file); const uint32_t compression = readLE32(file);
// Match Bitmap::parseHeaders(): accept BI_RGB (0) and 32bpp BI_BITFIELDS (3), but keep the same // byte-layout assumption as custom sleep BMPs. The renderer below treats pixels as BGRA and does not parse masks. if (planes != 1 || bpp != 32 || !(compression == 0 || compression == 3)) { if (logErrors) { LOG_ERR("SLP", "Transparent overlay must be 32-bit BGRA BMP (planes=%u bpp=%u comp=%u)", planes, bpp, static_cast<unsigned>(compression)); } return false; }
constexpr int MAX_IMAGE_WIDTH = 2048; constexpr int MAX_IMAGE_HEIGHT = 3072; if (info.width <= 0 || info.height <= 0 || info.width > MAX_IMAGE_WIDTH || info.height > MAX_IMAGE_HEIGHT) { if (logErrors) LOG_ERR("SLP", "Bad transparent overlay dimensions: %dx%d", info.width, info.height); return false; }
info.rowBytes = static_cast<uint32_t>(info.width) * 4u; if (!file.seek(info.dataOffset)) { if (logErrors) LOG_ERR("SLP", "Failed to seek transparent overlay pixel data"); return false; }
return true;}
uint8_t bayerThreshold4x4(const int x, const int y) { static constexpr uint8_t BAYER_4X4[16] = {0, 128, 32, 160, 192, 64, 224, 96, 48, 176, 16, 144, 240, 112, 208, 80}; return BAYER_4X4[((y & 0x03) << 2) | (x & 0x03)];}
enum class TransparentOverlayPass : uint8_t { BW, GrayscaleLsb, GrayscaleMsb };
uint8_t quantizeOverlayLum(const uint8_t lum) { // Match Bitmap's native-palette path: 0, 85, 170, 255 map directly to levels 0..3. return lum >> 6;}
bool renderTransparentOverlayPass(HalFile& file, const OverlayBmpInfo& info, const BitmapPlacement& placement, const GfxRenderer& renderer, uint8_t* row, const TransparentOverlayPass pass) { if (!file.seek(info.dataOffset)) { LOG_ERR("SLP", "Failed to seek transparent overlay pixel data"); return false; }
const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); const int cropPixX = std::floor(info.width * placement.cropX / 2.0f); const int cropPixY = std::floor(info.height * placement.cropY / 2.0f); const float croppedWidth = (1.0f - placement.cropX) * static_cast<float>(info.width); const float croppedHeight = (1.0f - placement.cropY) * static_cast<float>(info.height);
float scale = 1.0f; if (croppedWidth > 0.0f && croppedHeight > 0.0f) { const float widthScale = static_cast<float>(pageWidth) / croppedWidth; const float heightScale = static_cast<float>(pageHeight) / croppedHeight; scale = std::min(widthScale, heightScale); if (scale > 1.0f) scale = 1.0f; } const bool isScaled = scale < 1.0f;
for (int bmpY = 0; bmpY < info.height; bmpY++) { if (file.read(row, info.rowBytes) != static_cast<int>(info.rowBytes)) { LOG_ERR("SLP", "Short read in transparent overlay row %d", bmpY); return false; }
int screenY = -cropPixY + (info.topDown ? bmpY : info.height - 1 - bmpY); if (isScaled) screenY = std::floor(screenY * scale); screenY += placement.y;
if (screenY >= pageHeight) { if (info.topDown) break; continue; } if (screenY < 0) { if (!info.topDown) break; continue; }
for (int bmpX = cropPixX; bmpX < info.width - cropPixX; bmpX++) { int screenX = bmpX - cropPixX; if (isScaled) screenX = std::floor(screenX * scale); screenX += placement.x;
if (screenX >= renderer.getScreenWidth()) break; if (screenX < 0) continue;
const uint8_t* pixel = row + (static_cast<size_t>(bmpX) * 4u); const uint8_t alpha = pixel[3]; if (alpha < MIN_VISIBLE_ALPHA || alpha <= bayerThreshold4x4(screenX, screenY)) continue;
const uint8_t lum = (77u * pixel[2] + 150u * pixel[1] + 29u * pixel[0]) >> 8; const uint8_t level = quantizeOverlayLum(lum);
switch (pass) { case TransparentOverlayPass::BW: // Same first pass as custom bitmap sleep: all non-white levels are painted black. // Transparent overlay's only difference is that opaque white explicitly erases underlying text. renderer.drawPixel(screenX, screenY, level < 3); break; case TransparentOverlayPass::GrayscaleLsb: case TransparentOverlayPass::GrayscaleMsb: { const auto planePixel = grayPlanePixel(level, pass == TransparentOverlayPass::GrayscaleMsb, renderer.grayPlanesAreAbsolute()); if (planePixel.write) renderer.drawPixel(screenX, screenY, planePixel.black); break; } } } }
return true;}
enum class AlphaOverlayResult : uint8_t { Rendered, NotAlphaOverlay, Error };enum class AlphaScanResult : uint8_t { Useful, NotUseful, Error };
AlphaScanResult scanForUsefulAlpha(HalFile& file, const OverlayBmpInfo& info, uint8_t* row) { if (!file.seek(info.dataOffset)) { LOG_ERR("SLP", "Failed to seek transparent overlay pixel data"); return AlphaScanResult::Error; }
bool hasVisiblePixel = false; bool hasNonOpaquePixel = false; for (int bmpY = 0; bmpY < info.height; bmpY++) { if (file.read(row, info.rowBytes) != static_cast<int>(info.rowBytes)) { LOG_ERR("SLP", "Short read while checking transparent overlay row %d", bmpY); return AlphaScanResult::Error; }
for (int bmpX = 0; bmpX < info.width; bmpX++) { const uint8_t alpha = row[static_cast<size_t>(bmpX) * 4u + 3u]; hasVisiblePixel |= alpha >= MIN_VISIBLE_ALPHA; hasNonOpaquePixel |= alpha < 255; if (hasVisiblePixel && hasNonOpaquePixel) return AlphaScanResult::Useful; } }
return AlphaScanResult::NotUseful;}
AlphaOverlayResult tryRenderTransparentOverlayBmp(HalFile& file, GfxRenderer& renderer, const char* pathForLog) { OverlayBmpInfo info; if (!parseOverlayBmpHeader(file, info, false)) return AlphaOverlayResult::NotAlphaOverlay;
const auto placement = calculateBitmapPlacement(info.width, info.height, renderer); auto row = makeUniqueNoThrow<uint8_t[]>(info.rowBytes); if (!row) { LOG_ERR("SLP", "OOM: transparent overlay row (%u bytes)", static_cast<unsigned>(info.rowBytes)); return AlphaOverlayResult::Error; }
const auto alphaScanResult = scanForUsefulAlpha(file, info, row.get()); if (alphaScanResult == AlphaScanResult::Error) return AlphaOverlayResult::Error; if (alphaScanResult == AlphaScanResult::NotUseful) return AlphaOverlayResult::NotAlphaOverlay;
LOG_DBG("SLP", "Rendering transparent overlay: %s (%dx%d)", pathForLog, info.width, info.height);
if (!renderTransparentOverlayPass(file, info, placement, renderer, row.get(), TransparentOverlayPass::BW)) return AlphaOverlayResult::Error; const bool absolute = renderer.grayscaleCapabilities(sleepGrayscaleMode(renderer)).supported(); if (absolute) { if (!renderer.displayGrayscaleBase(sleepGrayscaleMode(renderer))) return AlphaOverlayResult::Error; } else { renderer.displayGrayscaleBase(HalDisplay::HALF_REFRESH); }
// Absolute planes retain B/W background bits; each visible overlay pixel is rewritten in both passes. if (!absolute) renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); if (!renderTransparentOverlayPass(file, info, placement, renderer, row.get(), TransparentOverlayPass::GrayscaleLsb)) { renderer.setRenderMode(GfxRenderer::BW); // Keep the current display instead of trying another overlay with a // framebuffer that now contains an incomplete gray plane. return AlphaOverlayResult::Rendered; } renderer.copyGrayscaleLsbBuffers();
if (!absolute) renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); if (!renderTransparentOverlayPass(file, info, placement, renderer, row.get(), TransparentOverlayPass::GrayscaleMsb)) { renderer.setRenderMode(GfxRenderer::BW); return AlphaOverlayResult::Rendered; } renderer.copyGrayscaleMsbBuffers();
renderer.displayGrayBuffer(); renderer.setRenderMode(GfxRenderer::BW); return AlphaOverlayResult::Rendered;}
enum class SleepRecentKind : uint8_t { Standard, Overlay };
bool isRecentSleepIndex(const SleepRecentKind recentKind, const uint16_t idx, const uint8_t window) { return recentKind == SleepRecentKind::Overlay ? APP_STATE.isRecentOverlaySleep(idx, window) : APP_STATE.isRecentSleep(idx, window);}
void pushRecentSleepIndex(const SleepRecentKind recentKind, const uint16_t idx) { if (recentKind == SleepRecentKind::Overlay) { APP_STATE.pushRecentOverlaySleep(idx); } else { APP_STATE.pushRecentSleep(idx); }}
bool findNextValidSleepImage(HalFile& dir, const SleepRecentKind recentKind, char* name) { for (auto dirFile = dir.openNextFile(); dirFile; dirFile = dir.openNextFile()) { if (dirFile.isDirectory()) continue;
dirFile.getName(name, MAX_SLEEP_FILE_NAME_LEN); if (name[0] == '\0' || name[0] == '.') continue;
const bool isBmp = FsHelpers::hasBmpExtension(name); const bool isPng = recentKind == SleepRecentKind::Overlay && FsHelpers::hasPngExtension(std::string_view{name}); if (!isBmp && !isPng) { LOG_DBG("SLP", "Skipping unsupported sleep image: %s", name); continue; }
const bool isValid = isBmp ? [&dirFile]() { Bitmap bitmap(dirFile); return bitmap.parseHeaders() == BmpReaderError::Ok; }() : isValidPngHeader(dirFile); if (!isValid) { LOG_DBG("SLP", "Skipping invalid sleep image: %s", name); continue; } return true; } return false;}
bool selectRandomSleepFile(const char* dirPath, const SleepRecentKind recentKind, std::string& selectedPath) { auto dir = Storage.open(dirPath); if (!dir || !dir.isDirectory()) return false;
auto name = makeUniqueNoThrow<char[]>(MAX_SLEEP_FILE_NAME_LEN); if (!name) { LOG_ERR("SLP", "OOM: sleep filename buffer"); return false; }
uint16_t fileCount = 0; while (fileCount < UINT16_MAX && findNextValidSleepImage(dir, recentKind, name.get())) ++fileCount; if (fileCount == 0) return false;
// Pick a random wallpaper, excluding recently shown ones. // Window: up to SLEEP_RECENT_COUNT entries, capped at fileCount-1. const uint8_t recentFill = recentKind == SleepRecentKind::Overlay ? APP_STATE.recentOverlaySleepFill : APP_STATE.recentSleepFill; const uint8_t window = static_cast<uint8_t>(std::min<uint16_t>(recentFill, fileCount - 1)); auto randomFileIndex = static_cast<uint16_t>(random(fileCount)); for (uint8_t attempt = 0; attempt < 20 && isRecentSleepIndex(recentKind, randomFileIndex, window); attempt++) { randomFileIndex = static_cast<uint16_t>(random(fileCount)); }
dir.rewindDirectory(); for (uint16_t index = 0; index <= randomFileIndex; ++index) { if (!findNextValidSleepImage(dir, recentKind, name.get())) return false; }
selectedPath.reserve(strlen(dirPath) + 1 + strlen(name.get())); selectedPath = dirPath; selectedPath += "/"; selectedPath += name.get(); pushRecentSleepIndex(recentKind, randomFileIndex); APP_STATE.saveToFile(); return true;}
bool drawSleepPopupPreservingFrame(GfxRenderer& renderer) { const auto& metrics = UITheme::getInstance().getMetrics(); const int frameThickness = metrics.popupFrameThickness; const int popupY = static_cast<int>(renderer.getScreenHeight() * metrics.popupTopOffsetRatio); const int popupHeight = renderer.getLineHeight(UI_12_FONT_ID) + metrics.popupMarginY * 2; const int bandTop = std::max(0, popupY - frameThickness); const int bandBottom = std::min(renderer.getScreenHeight(), popupY + popupHeight + frameThickness); const int bandHeight = bandBottom - bandTop; const size_t bandBytes = renderer.getRegionByteSize(0, bandTop, renderer.getScreenWidth(), bandHeight);
auto savedBand = makeUniqueNoThrow<uint8_t[]>(bandBytes); if (!savedBand) { LOG_ERR("SLP", "OOM: sleep popup background (%u bytes)", static_cast<unsigned>(bandBytes)); return false; } if (!renderer.copyRegionToBuffer(0, bandTop, renderer.getScreenWidth(), bandHeight, savedBand.get(), bandBytes)) { LOG_ERR("SLP", "Failed to save sleep popup background"); return false; }
GUI.drawPopup(renderer, tr(STR_ENTERING_SLEEP)); if (!renderer.copyBufferToRegion(0, bandTop, renderer.getScreenWidth(), bandHeight, savedBand.get(), bandBytes)) { LOG_ERR("SLP", "Failed to restore sleep popup background"); return false; } return true;}
void releaseSdFontCachesForDecode(const GfxRenderer& renderer) { if (auto* fcm = renderer.getFontCacheManager()) { LOG_DBG("SLP", "Free heap before SD font cache release: %d bytes", ESP.getFreeHeap()); fcm->releaseSdFontCaches(); LOG_DBG("SLP", "Free heap before sleep image decode: %d bytes", ESP.getFreeHeap()); }}
} // namespace
void SleepActivity::onEnter() { Activity::onEnter();
const bool renderQuickResume = SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::QUICK_RESUME || (fromTimeout && SETTINGS.quickResumeSleepScreen == CrossPointSettings::QUICK_RESUME_SLEEP_SCREEN::QUICK_RESUME_AFTER_TIMEOUT);
if (renderQuickResume) { // Quick Resume keeps the current frame as-is, so the driver's inversion // state stays too: a night-mode page sleeps in night polarity, and the // moon icon inverts with it at transfer like any other draw. return renderLastScreenSleepScreen(); }
const bool frameWasInverted = display.isInverted();
// The remaining sleep screens draw fresh content in normal polarity. This // activity draws directly from onEnter (outside ActivityManager's // per-render polarity resolution), so clear any inversion left over from a // night-mode reader render. display.setInverted(false);
if (SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::TRANSPARENT_CUSTOM) { // Transparent mode retains the current framebuffer. Materialize any // output-level inversion first so the retained content keeps its visible // polarity after the display driver returns to normal. if (frameWasInverted) renderer.invertScreen(); if (APP_STATE.lastSleepFromReader) { ReaderUtils::applyOrientation(renderer, SETTINGS.orientation); } drawSleepPopupPreservingFrame(renderer); if (APP_STATE.lastSleepFromReader) { renderer.setOrientation(GfxRenderer::Orientation::Portrait); } releaseSdFontCachesForDecode(renderer); return renderTransparentCustomSleepScreen(); }
// Show popup with reader orientation only when going to sleep from reader if (APP_STATE.lastSleepFromReader) { ReaderUtils::applyOrientation(renderer, SETTINGS.orientation); GUI.drawPopup(renderer, tr(STR_ENTERING_SLEEP)); renderer.setOrientation(GfxRenderer::Orientation::Portrait); } else { GUI.drawPopup(renderer, tr(STR_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): return renderCoverSleepScreen(); case (CrossPointSettings::SLEEP_SCREEN_MODE::COVER_CUSTOM): if (APP_STATE.lastSleepFromReader) { return renderCoverSleepScreen(); } else { return renderCustomSleepScreen(); } default: return renderDefaultSleepScreen(); }}
void SleepActivity::renderCustomSleepScreen() const { // 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. // This takes priority over the /sleep folder. HalFile file; if (Storage.openFileForRead("SLP", "/sleep.bmp", file)) { Bitmap bitmap(file, true, renderer.grayscaleCapabilities(sleepGrayscaleMode(renderer)).supported() && display.getController() == HalDisplay::Controller::SSD1677 && SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::NO_FILTER); if (bitmap.parseHeaders() == BmpReaderError::Ok) { LOG_DBG("SLP", "Loading: /sleep.bmp"); renderBitmapSleepScreen(bitmap); file.close(); return; } file.close(); }
std::string selectedPath; if (!selectRandomSleepFile("/.sleep", SleepRecentKind::Standard, selectedPath)) { selectRandomSleepFile("/sleep", SleepRecentKind::Standard, selectedPath); }
if (!selectedPath.empty()) { HalFile randFile; if (Storage.openFileForRead("SLP", selectedPath, randFile)) { LOG_DBG("SLP", "Randomly loading: %s", selectedPath.c_str()); delay(100); Bitmap bitmap(randFile, true, renderer.grayscaleCapabilities(sleepGrayscaleMode(renderer)).supported() && display.getController() == HalDisplay::Controller::SSD1677 && SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::NO_FILTER); if (bitmap.parseHeaders() == BmpReaderError::Ok) { renderBitmapSleepScreen(bitmap); randFile.close(); return; } randFile.close(); } }
renderDefaultSleepScreen();}
// Sleep screens paint with a single HALF refresh (stock parity): the OEM X4// firmware's only clean refresh in normal operation is the single-pass 0xD7// sequence, used once for the sleep image. It never runs the multi-flash GC// waveform (0xF7) that FULL_REFRESH selects (#2471's blinking complaint).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, tr(STR_CROSSPOINT), true, EpdFontFamily::BOLD); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, tr(STR_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 bool preserveBackground) const { const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); const auto placement = calculateBitmapPlacement(bitmap.getWidth(), bitmap.getHeight(), renderer); const int x = placement.x; const int y = placement.y; const float cropX = placement.cropX; const float cropY = placement.cropY;
LOG_DBG("SLP", "bitmap %d x %d, screen %d x %d", bitmap.getWidth(), bitmap.getHeight(), pageWidth, pageHeight); LOG_DBG("SLP", "drawing to %d x %d", x, y); if (!preserveBackground) renderer.clearScreen();
const bool hasGreyscale = bitmap.hasGreyscale() && (preserveBackground || SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::NO_FILTER);
if (!renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY, preserveBackground)) { renderer.displayBuffer(HalDisplay::HALF_REFRESH); return; }
if (!preserveBackground && SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::INVERTED_BLACK_AND_WHITE) { renderer.invertScreen(); }
const bool absolute = hasGreyscale && renderer.grayscaleCapabilities(sleepGrayscaleMode(renderer)).supported(); if (absolute) { if (!renderer.displayGrayscaleBase(sleepGrayscaleMode(renderer))) return; } else if (hasGreyscale) { // OEM grayscale pipeline base. Must stay HALF: the gray nudge LUT is // calibrated against the pixel state the single-pass HALF waveform leaves // behind. A FULL (GC) base parks pixels in a different charge state and // the differential nudge then lands unevenly (blotchy noise in gray areas). renderer.displayGrayscaleBase(HalDisplay::HALF_REFRESH); } else { renderer.displayBuffer(HalDisplay::HALF_REFRESH); }
if (hasGreyscale) { bool ready = true; for (const auto plane : {GfxRenderer::GRAYSCALE_LSB, GfxRenderer::GRAYSCALE_MSB}) { if (bitmap.rewindToData() != BmpReaderError::Ok) { ready = false; break; } if (!absolute || !preserveBackground) renderer.clearScreen(absolute ? 0xFF : 0x00); renderer.setRenderMode(plane); if (!renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY, preserveBackground)) { ready = false; break; } if (plane == GfxRenderer::GRAYSCALE_LSB) renderer.copyGrayscaleLsbBuffers(); else renderer.copyGrayscaleMsbBuffers(); } if (ready) renderer.displayGrayBuffer(); else LOG_ERR("SLP", "Incomplete grayscale image; keeping the current display"); renderer.setRenderMode(GfxRenderer::BW); }}
bool SleepActivity::renderSleepOverlayFile(HalFile& file, const char* pathForLog) const { const auto alphaResult = tryRenderTransparentOverlayBmp(file, renderer, pathForLog); if (alphaResult == AlphaOverlayResult::Rendered) return true; if (alphaResult == AlphaOverlayResult::Error) return false;
Bitmap bitmap(file); const auto parseResult = bitmap.parseHeaders(); if (parseResult != BmpReaderError::Ok) { LOG_ERR("SLP", "Invalid sleep overlay BMP %s: %s", pathForLog, Bitmap::errorToString(parseResult)); return false; }
LOG_DBG("SLP", "Rendering regular BMP sleep overlay: %s (%dx%d)", pathForLog, bitmap.getWidth(), bitmap.getHeight()); // drawBitmap leaves white pixels untouched; skipping the initial clear makes // them transparent while retaining the existing grayscale pipeline. renderBitmapSleepScreen(bitmap, true); return true;}
bool SleepActivity::renderTransparentOverlayPng(const std::string& path) const { ImageDimensions dimensions; if (!PngToFramebufferConverter::getDimensionsStatic(path, dimensions)) return false;
const auto placement = calculateBitmapPlacement(dimensions.width, dimensions.height, renderer); RenderConfig config; config.x = placement.x; config.y = placement.y; config.maxWidth = renderer.getScreenWidth(); config.maxHeight = renderer.getScreenHeight(); config.useDithering = false; config.sourceCropX = placement.cropX; config.sourceCropY = placement.cropY; config.useExactDimensions = placement.cropX > 0.0f || placement.cropY > 0.0f; config.preserveAlpha = true;
PngToFramebufferConverter converter; LOG_DBG("SLP", "Rendering transparent PNG overlay: %s (%dx%d)", path.c_str(), dimensions.width, dimensions.height);
if (!converter.decodeToFramebuffer(path, renderer, config)) return false; const bool absolute = renderer.grayscaleCapabilities(sleepGrayscaleMode(renderer)).supported(); if (absolute) { if (!renderer.displayGrayscaleBase(sleepGrayscaleMode(renderer))) return false; } else { renderer.displayGrayscaleBase(HalDisplay::HALF_REFRESH); }
// Absolute planes retain B/W background bits; each visible overlay pixel is rewritten in both passes. if (!absolute) renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); if (!converter.decodeToFramebuffer(path, renderer, config)) { renderer.setRenderMode(GfxRenderer::BW); return true; } renderer.copyGrayscaleLsbBuffers();
if (!absolute) renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); if (!converter.decodeToFramebuffer(path, renderer, config)) { renderer.setRenderMode(GfxRenderer::BW); return true; } renderer.copyGrayscaleMsbBuffers();
renderer.displayGrayBuffer(); renderer.setRenderMode(GfxRenderer::BW); return true;}
bool SleepActivity::renderSleepOverlayPath(const std::string& path) const { if (FsHelpers::hasPngExtension(path)) { return Storage.exists(path.c_str()) && renderTransparentOverlayPng(path); }
HalFile file; return Storage.openFileForRead("SLP", path, file) && renderSleepOverlayFile(file, path.c_str());}
void SleepActivity::renderTransparentCustomSleepScreen() const { if (renderSleepOverlayPath(TRANSPARENT_SLEEP_ROOT_BMP)) return; if (renderSleepOverlayPath(TRANSPARENT_SLEEP_ROOT_PNG)) return;
std::string selectedPath; if (!selectRandomSleepFile(TRANSPARENT_SLEEP_DIR, SleepRecentKind::Overlay, selectedPath)) { selectRandomSleepFile(TRANSPARENT_SLEEP_LEGACY_DIR, SleepRecentKind::Overlay, selectedPath); }
if (!selectedPath.empty() && renderSleepOverlayPath(selectedPath)) return;
LOG_ERR("SLP", "No valid transparent sleep overlay found"); renderDefaultSleepScreen();}
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)(); }
// SSD absolute images use the new thresholds; other panels retain legacy tuning. const bool originalThresholds = renderer.grayscaleCapabilities(sleepGrayscaleMode(renderer)).supported() && display.getController() == HalDisplay::Controller::SSD1677 && SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::NO_FILTER; std::string coverBmpPath; bool cropped = SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP;
// Check if the current book is XTC, TXT, or EPUB if (FsHelpers::hasXtcExtension(APP_STATE.openEpubPath)) { // Handle XTC file Xtc lastXtc(APP_STATE.openEpubPath, "/.crosspoint"); if (!lastXtc.load()) { LOG_ERR("SLP", "Failed to load last XTC"); return (this->*renderNoCoverSleepScreen)(); }
if (!lastXtc.generateCoverBmp()) { LOG_ERR("SLP", "Failed to generate XTC cover bmp"); return (this->*renderNoCoverSleepScreen)(); }
coverBmpPath = lastXtc.getCoverBmpPath(); } else if (FsHelpers::hasTxtExtension(APP_STATE.openEpubPath)) { // Handle TXT file - looks for cover image in the same folder Txt lastTxt(APP_STATE.openEpubPath, "/.crosspoint"); if (!lastTxt.load()) { LOG_ERR("SLP", "Failed to load last TXT"); return (this->*renderNoCoverSleepScreen)(); }
if (!lastTxt.generateCoverBmp()) { LOG_ERR("SLP", "No cover image found for TXT file"); return (this->*renderNoCoverSleepScreen)(); }
coverBmpPath = lastTxt.getCoverBmpPath(); } else if (FsHelpers::hasEpubExtension(APP_STATE.openEpubPath)) { // Handle EPUB file Epub lastEpub(APP_STATE.openEpubPath, "/.crosspoint"); // Skip loading css since we only need metadata here if (!lastEpub.load(true, true)) { LOG_ERR("SLP", "Failed to load last epub"); return (this->*renderNoCoverSleepScreen)(); }
if (!lastEpub.generateCoverBmp(cropped, originalThresholds)) { LOG_ERR("SLP", "Failed to generate cover bmp"); return (this->*renderNoCoverSleepScreen)(); }
coverBmpPath = lastEpub.getCoverBmpPath(cropped, originalThresholds); } else { return (this->*renderNoCoverSleepScreen)(); }
HalFile file; if (Storage.openFileForRead("SLP", coverBmpPath, file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { LOG_DBG("SLP", "Rendering sleep cover: %s", coverBmpPath.c_str()); renderBitmapSleepScreen(bitmap); return; } }
return (this->*renderNoCoverSleepScreen)();}
void SleepActivity::renderLastScreenSleepScreen() const { const auto pageHeight = renderer.getScreenHeight(); renderer.drawImage(MoonIcon, 0, pageHeight - MOONICON_HEIGHT, MOONICON_WIDTH, MOONICON_HEIGHT); // Only the moon differs from the displayed frame, so a differential FAST // update adds it without the flashing clean pass (which sweeps the panel // through the inverse — a full white flash on a night-mode page). if (gpio.deviceIsX3()) { renderer.displayGrayscaleBase(HalDisplay::FAST_REFRESH); } else { renderer.displayBuffer(HalDisplay::FAST_REFRESH); }}
void SleepActivity::renderBlankSleepScreen() const { renderer.clearScreen(); renderer.displayBuffer(HalDisplay::HALF_REFRESH);}