Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
- File Browser long-press folder action for choosing a custom sleep-image folder instead of only `/.sleep` or `/sleep`.
- Expanded X3 Reading Stats, including streaks, time charts, editable dates, all-time backups, reset controls, an idle-time threshold, and the `Minimal Stats` sleep screen.
- `Reset Reading Pace` in the EPUB reader menu when Time Left is enabled, for clearing only the time-left pace estimate while keeping book reading stats.
- Added TRMNL as a sleep-screen wallpaper source with configurable server, API key, device ID, and orientation.

### Changed
- Display, Reader, and Controls settings now open list menus instead of cycling through options one by one.
Expand Down
21 changes: 18 additions & 3 deletions lib/Epub/Epub/converters/PngToFramebufferConverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <Logging.h>
#include <MemoryBudget.h>
#include <PNGdec.h>
#include <PngGraySample.h>

#include <cstdlib>
#include <new>
Expand Down Expand Up @@ -106,10 +107,24 @@ int requiredPngInternalBufferBytes(int srcWidth, int pixelType) {
// Convert entire source line to grayscale with alpha blending to white background.
// For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
// Processing the whole line at once improves cache locality and reduces per-pixel overhead.
void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, uint8_t* palette, int hasAlpha) {
void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, uint8_t* palette, int hasAlpha,
int bpp) {
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
switch (pixelType) {
case PNG_PIXEL_GRAYSCALE:
memcpy(grayLine, pPixels, width);
if (bpp == 8) {
// Fast bulk path for the common case -- see the file-level comment above
// on why processing the whole line at once matters here.
memcpy(grayLine, pPixels, width);
} else {
// Sub-8-bit grayscale is packed MSB-first (8/4/2 samples per byte); 16-bit
// grayscale is 2 bytes/sample, big-endian. pngUnpackGraySample() unpacks
// and scales any valid PNG grayscale depth; shared with SleepActivity.cpp's
// pngOverlayDraw() so the two PNG consumers can't drift apart. The old
// straight memcpy assumed 8-bit and sheared other depths horizontally.
for (int x = 0; x < width; x++) {
grayLine[x] = pngUnpackGraySample(pPixels, x, bpp);
}
}
break;

case PNG_PIXEL_TRUECOLOR:
Expand Down Expand Up @@ -185,7 +200,7 @@ int pngDrawCallback(PNGDRAW* pDraw) {

// Convert entire source line to grayscale (improves cache locality)
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->pPalette,
pDraw->iHasAlpha);
pDraw->iHasAlpha, pDraw->iBpp);

// Render scaled row using Bresenham-style integer stepping (no floating-point division)
int dstWidth = ctx->dstWidth;
Expand Down
9 changes: 9 additions & 0 deletions lib/I18n/translations/english.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,15 @@ STR_THEME_MINIMAL: "Minimal"
STR_THEME_DASHBOARD: "Dashboard"
STR_THEME_MINIMAL_STATS: "Minimal Stats"
STR_QUICK_RESUME: "Quick Resume"
STR_TRMNL: "TRMNL"
STR_TRMNL_SETTINGS: "TRMNL Settings"
STR_TRMNL_SERVER_URL: "TRMNL Server URL"
STR_TRMNL_API_KEY: "TRMNL API Key"
STR_TRMNL_DEVICE_ID: "TRMNL Device ID"
STR_TRMNL_ORIENTATION: "TRMNL Orientation"
STR_TRMNL_HORIZONTAL: "Horizontal"
STR_TRMNL_VERTICAL: "Vertical"
STR_TRMNL_FETCH_FAILED: "TRMNL fetch failed"
STR_RECENT_BOOKS_VIEW: "Recent Books View"
STR_LIST_VIEW: "List"
STR_GRID_VIEW: "Grid"
Expand Down
88 changes: 88 additions & 0 deletions lib/JsonParser/TrmnlDisplayJsonParser.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#include "TrmnlDisplayJsonParser.h"

#include <cstdlib>
#include <cstring>

namespace {
void safeCopy(char* dst, const size_t dstSize, const char* src, const size_t srcLen) {
const size_t n = srcLen < dstSize - 1 ? srcLen : dstSize - 1;
memcpy(dst, src, n);
dst[n] = '\0';
}
} // namespace

TrmnlDisplayJsonParser::TrmnlDisplayJsonParser()
: parser(JsonCallbacks{this, sOnKey, sOnString, sOnNumber, sOnBool, sOnNull, sOnObjectStart, sOnObjectEnd,
sOnArrayStart, sOnArrayEnd}) {
reset();
}

void TrmnlDisplayJsonParser::reset() {
parser.reset();
lastKey = LastKey::NONE;
depth = 0;
imageUrl[0] = '\0';
filename[0] = '\0';
refreshRateSeconds = 0;
imageUrlFound = false;
}

void TrmnlDisplayJsonParser::feed(const char* data, const size_t len) { parser.feed(data, len); }
bool TrmnlDisplayJsonParser::foundImageUrl() const { return imageUrlFound; }
bool TrmnlDisplayJsonParser::hasError() const { return parser.hasError(); }
const char* TrmnlDisplayJsonParser::getImageUrl() const { return imageUrl; }
const char* TrmnlDisplayJsonParser::getFilename() const { return filename; }
uint32_t TrmnlDisplayJsonParser::getRefreshRateSeconds() const { return refreshRateSeconds; }

void TrmnlDisplayJsonParser::sOnKey(void* ctx, const char* key, const size_t len) {
auto* self = static_cast<TrmnlDisplayJsonParser*>(ctx);
if (self->depth != 1) {
self->lastKey = LastKey::NONE;
return;
}
if (len == 9 && memcmp(key, "image_url", 9) == 0) {
self->lastKey = LastKey::IMAGE_URL;
} else if ((len == 8 && memcmp(key, "filename", 8) == 0) || (len == 10 && memcmp(key, "image_name", 10) == 0)) {
self->lastKey = LastKey::FILENAME;
} else if (len == 12 && memcmp(key, "refresh_rate", 12) == 0) {
self->lastKey = LastKey::REFRESH_RATE;
} else {
self->lastKey = LastKey::NONE;
}
}

void TrmnlDisplayJsonParser::sOnString(void* ctx, const char* value, const size_t len) {
auto* self = static_cast<TrmnlDisplayJsonParser*>(ctx);
if (self->depth == 1 && self->lastKey == LastKey::IMAGE_URL) {
safeCopy(self->imageUrl, sizeof(self->imageUrl), value, len);
self->imageUrlFound = self->imageUrl[0] != '\0';
} else if (self->depth == 1 && self->lastKey == LastKey::FILENAME) {
safeCopy(self->filename, sizeof(self->filename), value, len);
}
self->lastKey = LastKey::NONE;
}

void TrmnlDisplayJsonParser::sOnNumber(void* ctx, const char* value, const size_t /*len*/) {
auto* self = static_cast<TrmnlDisplayJsonParser*>(ctx);
if (self->depth == 1 && self->lastKey == LastKey::REFRESH_RATE) {
self->refreshRateSeconds = static_cast<uint32_t>(strtoul(value, nullptr, 10));
}
self->lastKey = LastKey::NONE;
}

void TrmnlDisplayJsonParser::sOnBool(void* ctx, bool /*value*/) {
static_cast<TrmnlDisplayJsonParser*>(ctx)->lastKey = LastKey::NONE;
}
void TrmnlDisplayJsonParser::sOnNull(void* ctx) { static_cast<TrmnlDisplayJsonParser*>(ctx)->lastKey = LastKey::NONE; }
void TrmnlDisplayJsonParser::sOnObjectStart(void* ctx) {
auto* self = static_cast<TrmnlDisplayJsonParser*>(ctx);
self->depth++;
self->lastKey = LastKey::NONE;
}
void TrmnlDisplayJsonParser::sOnObjectEnd(void* ctx) {
auto* self = static_cast<TrmnlDisplayJsonParser*>(ctx);
if (self->depth > 0) self->depth--;
self->lastKey = LastKey::NONE;
}
void TrmnlDisplayJsonParser::sOnArrayStart(void* ctx) { sOnObjectStart(ctx); }
void TrmnlDisplayJsonParser::sOnArrayEnd(void* ctx) { sOnObjectEnd(ctx); }
40 changes: 40 additions & 0 deletions lib/JsonParser/TrmnlDisplayJsonParser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#pragma once

#include <cstddef>
#include <cstdint>

#include "StreamingJsonParser.h"

class TrmnlDisplayJsonParser {
public:
TrmnlDisplayJsonParser();
void reset();
void feed(const char* data, size_t len);

bool foundImageUrl() const;
bool hasError() const;
const char* getImageUrl() const;
const char* getFilename() const;
uint32_t getRefreshRateSeconds() const;

private:
enum class LastKey : uint8_t { NONE, IMAGE_URL, FILENAME, REFRESH_RATE };

static void sOnKey(void* ctx, const char* key, size_t len);
static void sOnString(void* ctx, const char* value, size_t len);
static void sOnNumber(void* ctx, const char* value, size_t len);
static void sOnBool(void* ctx, bool value);
static void sOnNull(void* ctx);
static void sOnObjectStart(void* ctx);
static void sOnObjectEnd(void* ctx);
static void sOnArrayStart(void* ctx);
static void sOnArrayEnd(void* ctx);

StreamingJsonParser parser;
LastKey lastKey = LastKey::NONE;
uint16_t depth = 0;
char imageUrl[512] = "";
char filename[128] = "";
uint32_t refreshRateSeconds = 0;
bool imageUrlFound = false;
};
38 changes: 38 additions & 0 deletions lib/PngGraySample/PngGraySample.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#pragma once

#include <cstdint>

// Reads a single grayscale sample from a PNGdec scanline at column `x` and
// returns it scaled to a full 0..255 intensity.
//
// PNG grayscale images are encoded at 1, 2, 4, 8, or 16 bits per sample (PNG
// spec, IHDR bit depth for color type 0). PNGdec delivers each scanline
// packed at that native bit depth:
// - 1/2/4-bit: samples are packed MSB-first, multiple samples per byte.
// - 8-bit: one byte per sample.
// - 16-bit: two bytes per sample, big-endian per spec; we use the high
// byte as the 8-bit intensity, matching how the rest of the rendering
// pipeline treats channel data.
// Any other bpp is not a valid PNG grayscale bit depth and should not occur
// from PNGdec; rather than trust that invariant and read out of bounds or
// misinterpret the buffer, fall back to a neutral mid-gray.
inline uint8_t pngUnpackGraySample(const uint8_t* pixels, const int x, const int bpp) {
switch (bpp) {
case 8:
return pixels[x];
case 16:
return pixels[x * 2];
case 1:
case 2:
case 4: {
const int ppb = 8 / bpp; // samples per byte
const int mask = (1 << bpp) - 1; // max sample value
const uint8_t byte = pixels[x / ppb];
const int shift = (ppb - 1 - (x % ppb)) * bpp; // MSB-first within the byte
const int sample = (byte >> shift) & mask;
return static_cast<uint8_t>(sample * 255 / mask); // scale sample to 0..255
}
default:
return 128; // unexpected bit depth; avoid reading out of bounds or misinterpreting data
}
}
1 change: 1 addition & 0 deletions src/CrossPointSettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ constexpr uint8_t SLEEP_SCREEN_STORAGE_ORDER[] = {
static_cast<uint8_t>(CrossPointSettings::QUICK_RESUME),
static_cast<uint8_t>(CrossPointSettings::MINIMAL_STATS_SLEEP),
static_cast<uint8_t>(CrossPointSettings::DASHBOARD_SLEEP),
static_cast<uint8_t>(CrossPointSettings::TRMNL),
};
constexpr uint8_t SLEEP_SCREEN_STORAGE_ORDER_COUNT =
sizeof(SLEEP_SCREEN_STORAGE_ORDER) / sizeof(SLEEP_SCREEN_STORAGE_ORDER[0]);
Expand Down
6 changes: 6 additions & 0 deletions src/CrossPointSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class CrossPointSettings {
QUICK_RESUME = 9,
MINIMAL_STATS_SLEEP = 10,
DASHBOARD_SLEEP = 11,
TRMNL = 12,
SLEEP_SCREEN_MODE_COUNT
};
enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT };
Expand All @@ -40,6 +41,7 @@ class CrossPointSettings {
INVERTED_BLACK_AND_WHITE = 2,
SLEEP_SCREEN_COVER_FILTER_COUNT
};
enum TRMNL_ORIENTATION { TRMNL_LANDSCAPE = 0, TRMNL_PORTRAIT = 1, TRMNL_ORIENTATION_COUNT };

// Status bar enum - legacy
enum STATUS_BAR_MODE {
Expand Down Expand Up @@ -387,6 +389,10 @@ class CrossPointSettings {
char opdsServerUrl[128] = "";
char opdsUsername[64] = "";
char opdsPassword[64] = "";
char trmnlServerUrl[160] = "";
char trmnlApiKey[128] = "";
char trmnlDeviceId[32] = "";
uint8_t trmnlOrientation = TRMNL_LANDSCAPE;
// Hide battery percentage
uint8_t hideBatteryPercentage = HIDE_NEVER;
// Long-press page turn button behavior
Expand Down
17 changes: 15 additions & 2 deletions src/SettingsList.h
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ inline SettingInfo buildSleepScreenSetting() {
StrId::STR_SLEEP_SCREEN, &CrossPointSettings::sleepScreen,
{StrId::STR_NONE_OPT, StrId::STR_DARK, StrId::STR_LIGHT, StrId::STR_CUSTOM, StrId::STR_COVER,
StrId::STR_COVER_CUSTOM, StrId::STR_PAGE_OVERLAY, StrId::STR_READING_STATS, StrId::STR_THEME_MINIMAL,
StrId::STR_THEME_MINIMAL_STATS, StrId::STR_THEME_DASHBOARD, StrId::STR_QUICK_RESUME},
StrId::STR_THEME_MINIMAL_STATS, StrId::STR_THEME_DASHBOARD, StrId::STR_TRMNL, StrId::STR_QUICK_RESUME},
"sleepScreen", StrId::STR_CAT_DISPLAY);
s.withEnumRawValues({
static_cast<uint8_t>(CrossPointSettings::BLANK),
Expand All @@ -254,7 +254,9 @@ inline SettingInfo buildSleepScreenSetting() {
static_cast<uint8_t>(CrossPointSettings::MINIMAL_SLEEP),
static_cast<uint8_t>(CrossPointSettings::MINIMAL_STATS_SLEEP),
static_cast<uint8_t>(CrossPointSettings::DASHBOARD_SLEEP),
static_cast<uint8_t>(CrossPointSettings::TRMNL),
static_cast<uint8_t>(CrossPointSettings::QUICK_RESUME),
static_cast<uint8_t>(CrossPointSettings::TRMNL),
});
return s;
}
Expand Down Expand Up @@ -587,6 +589,16 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
add(SettingInfo::Toggle(StrId::STR_TRACK_READING_STATS, &CrossPointSettings::trackReadingStats, "trackReadingStats",
StrId::STR_CAT_SYSTEM));
#endif
add(SettingInfo::String(StrId::STR_TRMNL_SERVER_URL, SETTINGS.trmnlServerUrl, sizeof(SETTINGS.trmnlServerUrl),
"trmnlServerUrl", StrId::STR_TRMNL_SETTINGS));
add(SettingInfo::String(StrId::STR_TRMNL_API_KEY, SETTINGS.trmnlApiKey, sizeof(SETTINGS.trmnlApiKey),
"trmnlApiKey", StrId::STR_TRMNL_SETTINGS)
.withObfuscated());
add(SettingInfo::String(StrId::STR_TRMNL_DEVICE_ID, SETTINGS.trmnlDeviceId, sizeof(SETTINGS.trmnlDeviceId),
"trmnlDeviceId", StrId::STR_TRMNL_SETTINGS));
add(SettingInfo::Enum(StrId::STR_TRMNL_ORIENTATION, &CrossPointSettings::trmnlOrientation,
{StrId::STR_TRMNL_HORIZONTAL, StrId::STR_TRMNL_VERTICAL}, "trmnlOrientation",
StrId::STR_TRMNL_SETTINGS));

// --- KOReader Sync (web-only, uses KOReaderCredentialStore) ---
add(SettingInfo::DynamicString(
Expand Down Expand Up @@ -914,11 +926,12 @@ inline std::vector<SettingInfo> buildDisplaySleepSettingsList(const std::vector<

inline std::vector<SettingInfo> buildSystemSettingsParentList(const std::vector<SettingInfo>& allSettings) {
std::vector<SettingInfo> systemSettings;
systemSettings.reserve(8);
systemSettings.reserve(9);
systemSettings.push_back(SettingInfo::Submenu(StrId::STR_SYSTEM_DEVICE, SettingAction::SystemDevice));
systemSettings.push_back(SettingInfo::Submenu(StrId::STR_SYSTEM_FILES_CACHE, SettingAction::SystemFilesCache));
systemSettings.push_back(SettingInfo::Submenu(StrId::STR_READING_STATS, SettingAction::SystemReadingStats));
systemSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network));
systemSettings.push_back(SettingInfo::Action(StrId::STR_TRMNL_SETTINGS, SettingAction::TrmnlSettings));
systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync));
systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_SERVERS, SettingAction::OPDSBrowser));
systemSettings.push_back(SettingInfo::Action(StrId::STR_CHECK_UPDATES, SettingAction::CheckForUpdates));
Expand Down
Loading