Files
Crosspoint/src/activities/boot_sleep/SleepActivity.cpp
T
jpirnayandXuan Son Nguyen cb24947477 feat: Add central logging pragma (#843)
## Summary

* Definition and use of a central LOG function, that can later be
extended or completely be removed (for public use where debugging
information may not be required) to save flash by suppressing the
-DENABLE_SERIAL_LOG like in the slim branch

* **What changes are included?**

## Additional Context
* By using the central logger the usual:
```
#include <HardwareSerial.h>
...
  Serial.printf("[%lu] [WCS] Obfuscating/deobfuscating %zu bytes\n", millis(), data.size());
```
would then become
```
#include <Logging.h>
...
  LOG_DBG("WCS", "Obfuscating/deobfuscating %zu bytes", data.size());
```
You do have ``LOG_DBG`` for debug messages, ``LOG_ERR`` for error
messages and ``LOG_INF`` for informational messages. Depending on the
verbosity level defined (see below) soe of these message types will be
suppressed/not-compiled.

* The normal compilation (default) will create a firmware.elf file of
42.194.356 bytes, the same code via slim will create 42.024.048 bytes -
170.308 bytes less
* Firmware.bin : 6.469.984 bytes for default, 6.418.672 bytes for slim -
51.312 bytes less


### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _NO_

---------

Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
2026-02-13 12:16:39 +01:00

280 lines
9.8 KiB
C++

#include "SleepActivity.h"
#include <Epub.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <Txt.h>
#include <Xtc.h>
#include "CrossPointSettings.h"
#include "CrossPointState.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "images/Logo120.h"
#include "util/StringUtils.h"
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<std::string> 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") {
LOG_DBG("SLP", "Skipping non-.bmp file name: %s", name);
file.close();
continue;
}
Bitmap bitmap(file);
if (bitmap.parseHeaders() != BmpReaderError::Ok) {
LOG_DBG("SLP", "Skipping invalid BMP file: %s", 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)) {
LOG_DBG("SLP", "Randomly loading: /sleep/%s", 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) {
LOG_DBG("SLP", "Loading: /sleep.bmp");
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 {
int x, y;
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
float cropX = 0, cropY = 0;
LOG_DBG("SLP", "bitmap %d x %d, screen %d x %d", bitmap.getWidth(), bitmap.getHeight(), pageWidth, pageHeight);
if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight) {
// image will scale, make sure placement is right
float ratio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
const float screenRatio = static_cast<float>(pageWidth) / static_cast<float>(pageHeight);
LOG_DBG("SLP", "bitmap ratio: %f, screen ratio: %f", ratio, screenRatio);
if (ratio > screenRatio) {
// image wider than viewport ratio, scaled down image needs to be centered vertically
if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) {
cropX = 1.0f - (screenRatio / ratio);
LOG_DBG("SLP", "Cropping bitmap x: %f", cropX);
ratio = (1.0f - cropX) * static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
}
x = 0;
y = std::round((static_cast<float>(pageHeight) - static_cast<float>(pageWidth) / ratio) / 2);
LOG_DBG("SLP", "Centering with ratio %f to y=%d", ratio, y);
} else {
// image taller than viewport ratio, scaled down image needs to be centered horizontally
if (SETTINGS.sleepScreenCoverMode == CrossPointSettings::SLEEP_SCREEN_COVER_MODE::CROP) {
cropY = 1.0f - (ratio / screenRatio);
LOG_DBG("SLP", "Cropping bitmap y: %f", cropY);
ratio = static_cast<float>(bitmap.getWidth()) / ((1.0f - cropY) * static_cast<float>(bitmap.getHeight()));
}
x = std::round((static_cast<float>(pageWidth) - static_cast<float>(pageHeight) * ratio) / 2);
y = 0;
LOG_DBG("SLP", "Centering with ratio %f to x=%d", ratio, x);
}
} else {
// center the image
x = (pageWidth - bitmap.getWidth()) / 2;
y = (pageHeight - bitmap.getHeight()) / 2;
}
LOG_DBG("SLP", "drawing to %d x %d", x, y);
renderer.clearScreen();
const bool hasGreyscale = bitmap.hasGreyscale() &&
SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::NO_FILTER;
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);
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
renderer.copyGrayscaleLsbBuffers();
bitmap.rewindToData();
renderer.clearScreen(0x00);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
renderer.copyGrayscaleMsbBuffers();
renderer.displayGrayBuffer();
renderer.setRenderMode(GfxRenderer::BW);
}
}
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()) {
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 (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()) {
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 (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)) {
LOG_ERR("SLP", "Failed to load last epub");
return (this->*renderNoCoverSleepScreen)();
}
if (!lastEpub.generateCoverBmp(cropped)) {
LOG_ERR("SLP", "Failed to generate cover bmp");
return (this->*renderNoCoverSleepScreen)();
}
coverBmpPath = lastEpub.getCoverBmpPath(cropped);
} else {
return (this->*renderNoCoverSleepScreen)();
}
FsFile 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::renderBlankSleepScreen() const {
renderer.clearScreen();
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
}