2025-04-15 22:37:50 +02:00
|
|
|
|
#include "GxEPDDisplay.h"
|
2026-05-24 16:39:41 +02:00
|
|
|
|
#include "LemonIcons.h"
|
2025-04-15 22:37:50 +02:00
|
|
|
|
|
2025-11-28 10:33:19 +01:00
|
|
|
|
#ifdef EXP_PIN_BACKLIGHT
|
|
|
|
|
|
#include <PCA9557.h>
|
|
|
|
|
|
extern PCA9557 expander;
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
2025-04-29 17:32:08 +02:00
|
|
|
|
#ifndef DISPLAY_ROTATION
|
2026-05-20 19:12:17 +02:00
|
|
|
|
#define DISPLAY_ROTATION 0
|
2025-04-29 17:32:08 +02:00
|
|
|
|
#endif
|
|
|
|
|
|
|
2025-11-27 21:49:04 +01:00
|
|
|
|
#ifdef ESP32
|
|
|
|
|
|
SPIClass SPI1 = SPIClass(FSPI);
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
2026-05-20 19:12:17 +02:00
|
|
|
|
// GFX fonts use the baseline as the cursor origin. UI code assumes top-of-cell
|
|
|
|
|
|
// coordinates (same convention as the OLED driver). Add the font ascender so
|
|
|
|
|
|
// the two conventions match.
|
2026-05-22 17:26:03 +02:00
|
|
|
|
static int fontAscender(int sz, bool use_lemon, int scale) {
|
2026-05-22 19:11:25 +02:00
|
|
|
|
if (sz == 3) return 26; // FreeSans18pt7b: proportional, baseline origin
|
|
|
|
|
|
if (sz == 1 && use_lemon) return 8 * scale; // Lemon GFX font: baseline origin, ascender 8px×scale
|
|
|
|
|
|
return 0; // GFX built-in font: cursor is top-left of cell
|
2026-05-20 19:12:17 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 16:39:41 +02:00
|
|
|
|
// y is the GFX baseline (display.getCursorY()), which equals original_y + 8*sc.
|
|
|
|
|
|
// Pixels are placed at y + yo*sc + row*sc — identical to how GFX would render
|
|
|
|
|
|
// a scaled GFX font, but bypassing GFX so multi-byte UTF-8 is decoded correctly.
|
2026-05-24 20:01:46 +02:00
|
|
|
|
int16_t GxEPDDisplay::drawLemonChar(int16_t x, int16_t y, uint32_t cp, int sc) {
|
2026-05-24 16:39:41 +02:00
|
|
|
|
for (uint8_t i = 0; i < lemonIconCount; i++) {
|
|
|
|
|
|
if (pgm_read_dword(&lemonIconCPs[i]) == cp) {
|
|
|
|
|
|
const GFXglyph* g = &lemonIconGlyphs[i];
|
|
|
|
|
|
uint8_t w = pgm_read_byte(&g->width), h = pgm_read_byte(&g->height);
|
|
|
|
|
|
int8_t xo = (int8_t)pgm_read_byte(&g->xOffset), yo = (int8_t)pgm_read_byte(&g->yOffset);
|
|
|
|
|
|
uint8_t xa = pgm_read_byte(&g->xAdvance);
|
|
|
|
|
|
uint16_t bo = pgm_read_word(&g->bitmapOffset);
|
|
|
|
|
|
uint8_t bits = 0, bit = 0;
|
|
|
|
|
|
for (uint8_t row = 0; row < h; row++)
|
|
|
|
|
|
for (uint8_t col = 0; col < w; col++) {
|
|
|
|
|
|
if (!bit) { bits = pgm_read_byte(&lemonIconBitmaps[bo++]); bit = 0x80; }
|
|
|
|
|
|
if (bits & bit) {
|
|
|
|
|
|
if (sc == 1) display.drawPixel(x + xo + col, y + yo + row, _curr_color);
|
|
|
|
|
|
else display.fillRect(x + xo*sc + col*sc, y + yo*sc + row*sc, sc, sc, _curr_color);
|
|
|
|
|
|
}
|
|
|
|
|
|
bit >>= 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
return x + xa * sc;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (cp < Lemon.first || cp > Lemon.last) {
|
2026-05-24 16:55:50 +02:00
|
|
|
|
if (cp >= 0x20) display.fillRect(x + sc, y - 8*sc, 4*sc, 6*sc, _curr_color);
|
2026-05-24 16:39:41 +02:00
|
|
|
|
return x + 6 * sc;
|
|
|
|
|
|
}
|
|
|
|
|
|
const GFXglyph* g = &lemonGlyphs[cp - Lemon.first];
|
|
|
|
|
|
uint8_t w = pgm_read_byte(&g->width), h = pgm_read_byte(&g->height);
|
|
|
|
|
|
int8_t xo = (int8_t)pgm_read_byte(&g->xOffset), yo = (int8_t)pgm_read_byte(&g->yOffset);
|
|
|
|
|
|
uint8_t xa = pgm_read_byte(&g->xAdvance);
|
|
|
|
|
|
uint16_t bo = pgm_read_word(&g->bitmapOffset);
|
|
|
|
|
|
uint8_t bits = 0, bit = 0;
|
|
|
|
|
|
for (uint8_t row = 0; row < h; row++)
|
|
|
|
|
|
for (uint8_t col = 0; col < w; col++) {
|
|
|
|
|
|
if (!bit) { bits = pgm_read_byte(&lemonBitmaps[bo++]); bit = 0x80; }
|
|
|
|
|
|
if (bits & bit) {
|
|
|
|
|
|
if (sc == 1) display.drawPixel(x + xo + col, y + yo + row, _curr_color);
|
|
|
|
|
|
else display.fillRect(x + xo*sc + col*sc, y + yo*sc + row*sc, sc, sc, _curr_color);
|
|
|
|
|
|
}
|
|
|
|
|
|
bit >>= 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
return x + xa * sc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 20:01:46 +02:00
|
|
|
|
uint8_t GxEPDDisplay::lemonXAdvance(uint32_t cp, int sc) {
|
2026-05-24 16:39:41 +02:00
|
|
|
|
uint8_t xa;
|
|
|
|
|
|
if (cp < Lemon.first || cp > Lemon.last) xa = 6;
|
|
|
|
|
|
else xa = pgm_read_byte(&lemonGlyphs[cp - Lemon.first].xAdvance);
|
2026-05-24 20:01:46 +02:00
|
|
|
|
return xa * sc;
|
2026-05-24 16:39:41 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-15 22:37:50 +02:00
|
|
|
|
bool GxEPDDisplay::begin() {
|
|
|
|
|
|
display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0));
|
2025-11-27 21:49:04 +01:00
|
|
|
|
#ifdef ESP32
|
|
|
|
|
|
SPI1.begin(PIN_DISPLAY_SCLK, PIN_DISPLAY_MISO, PIN_DISPLAY_MOSI, PIN_DISPLAY_CS);
|
|
|
|
|
|
#else
|
2025-04-15 22:37:50 +02:00
|
|
|
|
SPI1.begin();
|
2025-11-27 21:49:04 +01:00
|
|
|
|
#endif
|
2025-04-15 22:37:50 +02:00
|
|
|
|
display.init(115200, true, 2, false);
|
2025-04-29 17:32:08 +02:00
|
|
|
|
display.setRotation(DISPLAY_ROTATION);
|
2026-05-20 19:12:17 +02:00
|
|
|
|
setTextSize(1);
|
2025-04-15 22:37:50 +02:00
|
|
|
|
display.setPartialWindow(0, 0, display.width(), display.height());
|
|
|
|
|
|
display.fillScreen(GxEPD_WHITE);
|
2025-04-20 16:44:30 +02:00
|
|
|
|
display.display(true);
|
2026-05-20 19:12:17 +02:00
|
|
|
|
#if DISP_BACKLIGHT
|
2025-09-02 11:43:48 +02:00
|
|
|
|
digitalWrite(DISP_BACKLIGHT, LOW);
|
2025-04-20 17:10:57 +02:00
|
|
|
|
pinMode(DISP_BACKLIGHT, OUTPUT);
|
2026-05-20 19:12:17 +02:00
|
|
|
|
#endif
|
2025-04-15 22:37:50 +02:00
|
|
|
|
_init = true;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GxEPDDisplay::turnOn() {
|
|
|
|
|
|
if (!_init) begin();
|
2025-09-03 17:22:11 +02:00
|
|
|
|
#if defined(DISP_BACKLIGHT) && !defined(BACKLIGHT_BTN)
|
2025-04-20 17:10:57 +02:00
|
|
|
|
digitalWrite(DISP_BACKLIGHT, HIGH);
|
2025-11-28 10:33:19 +01:00
|
|
|
|
#elif defined(EXP_PIN_BACKLIGHT) && !defined(BACKLIGHT_BTN)
|
|
|
|
|
|
expander.digitalWrite(EXP_PIN_BACKLIGHT, HIGH);
|
2025-04-20 17:10:57 +02:00
|
|
|
|
#endif
|
2025-08-16 18:13:50 +02:00
|
|
|
|
_isOn = true;
|
2025-04-15 22:37:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GxEPDDisplay::turnOff() {
|
2025-09-02 11:43:48 +02:00
|
|
|
|
#if defined(DISP_BACKLIGHT) && !defined(BACKLIGHT_BTN)
|
2025-04-20 17:10:57 +02:00
|
|
|
|
digitalWrite(DISP_BACKLIGHT, LOW);
|
2025-11-28 10:33:19 +01:00
|
|
|
|
#elif defined(EXP_PIN_BACKLIGHT) && !defined(BACKLIGHT_BTN)
|
|
|
|
|
|
expander.digitalWrite(EXP_PIN_BACKLIGHT, LOW);
|
2025-04-20 17:10:57 +02:00
|
|
|
|
#endif
|
|
|
|
|
|
_isOn = false;
|
2025-04-15 22:37:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GxEPDDisplay::clear() {
|
|
|
|
|
|
display.fillScreen(GxEPD_WHITE);
|
|
|
|
|
|
display.setTextColor(GxEPD_BLACK);
|
2025-09-03 18:17:37 +02:00
|
|
|
|
display_crc.reset();
|
2025-04-15 22:37:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GxEPDDisplay::startFrame(Color bkg) {
|
|
|
|
|
|
display.fillScreen(GxEPD_WHITE);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
display.setTextColor(_curr_color = GxEPD_BLACK);
|
2026-05-20 19:12:17 +02:00
|
|
|
|
_text_sz = 1;
|
2026-05-24 20:01:46 +02:00
|
|
|
|
int sc = scale();
|
2026-05-22 17:20:20 +02:00
|
|
|
|
display.setFont(_use_lemon ? &Lemon : NULL);
|
2026-05-22 17:26:03 +02:00
|
|
|
|
display.setTextSize(sc);
|
2025-09-03 18:17:37 +02:00
|
|
|
|
display_crc.reset();
|
2025-04-15 22:37:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GxEPDDisplay::setTextSize(int sz) {
|
2026-05-20 19:12:17 +02:00
|
|
|
|
_text_sz = sz;
|
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
|
|
|
|
_vw_dirty = true;
|
2025-09-03 18:17:37 +02:00
|
|
|
|
display_crc.update<int>(sz);
|
2026-05-22 17:26:03 +02:00
|
|
|
|
// Size 1 scales with orientation: 1× in portrait (≈OLED width), 2× in landscape.
|
|
|
|
|
|
// Size 2 always uses 2× built-in (12×16) — fixed because layout Y-positions are hardcoded.
|
|
|
|
|
|
// Size 3 always uses FreeSans18pt for large headings.
|
2026-05-24 20:01:46 +02:00
|
|
|
|
int sc = scale();
|
2026-05-20 19:12:17 +02:00
|
|
|
|
switch (sz) {
|
2026-05-22 17:20:20 +02:00
|
|
|
|
case 3:
|
|
|
|
|
|
display.setFont(&FreeSans18pt7b);
|
|
|
|
|
|
display.setTextSize(1);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 2:
|
|
|
|
|
|
display.setFont(NULL);
|
2026-05-24 20:01:46 +02:00
|
|
|
|
display.setTextSize(scale() * 2);
|
2026-05-22 17:20:20 +02:00
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
display.setFont(_use_lemon ? &Lemon : NULL);
|
2026-05-22 17:26:03 +02:00
|
|
|
|
display.setTextSize(sc);
|
2026-05-22 17:20:20 +02:00
|
|
|
|
break;
|
2025-05-31 15:22:59 -07:00
|
|
|
|
}
|
2025-04-15 22:37:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GxEPDDisplay::setColor(Color c) {
|
2026-05-20 19:12:17 +02:00
|
|
|
|
display_crc.update<Color>(c);
|
|
|
|
|
|
// e-ink: DARK background = white paper, LIGHT foreground = black ink
|
2025-08-08 20:01:31 +10:00
|
|
|
|
if (c == DARK) {
|
|
|
|
|
|
display.setTextColor(_curr_color = GxEPD_WHITE);
|
2025-08-09 00:21:10 +12:00
|
|
|
|
} else {
|
|
|
|
|
|
display.setTextColor(_curr_color = GxEPD_BLACK);
|
2025-08-08 20:01:31 +10:00
|
|
|
|
}
|
2025-04-15 22:37:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GxEPDDisplay::setCursor(int x, int y) {
|
2025-09-03 18:17:37 +02:00
|
|
|
|
display_crc.update<int>(x);
|
|
|
|
|
|
display_crc.update<int>(y);
|
2026-05-20 19:12:17 +02:00
|
|
|
|
// Offset y by the font ascender: callers pass top-of-cell y, GFX fonts
|
|
|
|
|
|
// expect baseline y. Without this, text would be clipped at the top.
|
2026-05-24 20:01:46 +02:00
|
|
|
|
int sc = scale();
|
2026-05-22 17:26:03 +02:00
|
|
|
|
display.setCursor(x, y + fontAscender(_text_sz, _use_lemon, sc));
|
2025-04-15 22:37:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GxEPDDisplay::print(const char* str) {
|
2025-09-03 18:17:37 +02:00
|
|
|
|
display_crc.update<char>(str, strlen(str));
|
fix(eink): gate Lemon print path on _text_sz == 1
setTextSize(2/3) switches GFX to non-Lemon fonts (built-in scaled or
FreeSans18pt). The Lemon path bypassed GFX entirely and rendered glyphs
at size 1 scale, shrinking splash version and clock when Lemon font
was active.
Refresh CODE_REVIEW.md: remove resolved issues (signed-int timing vars,
abs() no-op, XBM CRC, loadPrefs nesting, lock-screen LPP thrash, reply
prefix raw UTF-8), add new findings (upstream-merge surface, virtual
width()/height() per glyph, page_order sentinel), list enhancement
ideas.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:45:19 +02:00
|
|
|
|
// Lemon path only for sz=1 — setTextSize(2/3) switches GFX to non-Lemon fonts.
|
|
|
|
|
|
if (_use_lemon && _text_sz == 1) {
|
2026-05-24 16:39:41 +02:00
|
|
|
|
int16_t cx = display.getCursorX();
|
|
|
|
|
|
int16_t cy = display.getCursorY();
|
2026-05-24 20:01:46 +02:00
|
|
|
|
const int sc = scale();
|
2026-05-24 16:39:41 +02:00
|
|
|
|
const uint8_t* p = (const uint8_t*)str;
|
|
|
|
|
|
while (*p) {
|
2026-05-24 20:25:35 +02:00
|
|
|
|
uint32_t cp = decodeCodepoint(p);
|
2026-05-24 16:39:41 +02:00
|
|
|
|
if (cp == '\n') { cy += Lemon.yAdvance * sc; cx = 0; }
|
2026-05-24 20:01:46 +02:00
|
|
|
|
else { cx = drawLemonChar(cx, cy, cp, sc); }
|
2026-05-24 16:39:41 +02:00
|
|
|
|
}
|
|
|
|
|
|
display.setCursor(cx, cy);
|
2026-05-23 14:45:24 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-05-24 20:01:46 +02:00
|
|
|
|
int sc = scale();
|
2026-05-23 14:45:24 +02:00
|
|
|
|
for (const char* p = str; *p; p++) {
|
|
|
|
|
|
if ((uint8_t)*p == 0xDB) {
|
|
|
|
|
|
int cx = display.getCursorX();
|
|
|
|
|
|
int cy = display.getCursorY();
|
2026-05-27 17:33:31 +02:00
|
|
|
|
// Default GFX font: cursor is top-left of cell (fontAscender=0), so cy=original_y.
|
|
|
|
|
|
// Draw block at cy (not cy-8*sc — that formula assumes Lemon's baseline offset).
|
|
|
|
|
|
display.fillRect(cx, cy, 5 * sc, 8 * sc, _curr_color);
|
2026-05-23 14:45:24 +02:00
|
|
|
|
display.setCursor(cx + 6 * sc, cy);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
char tmp[2] = {*p, 0};
|
|
|
|
|
|
display.print(tmp);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-15 22:37:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GxEPDDisplay::fillRect(int x, int y, int w, int h) {
|
2025-09-03 18:17:37 +02:00
|
|
|
|
display_crc.update<int>(x);
|
|
|
|
|
|
display_crc.update<int>(y);
|
|
|
|
|
|
display_crc.update<int>(w);
|
|
|
|
|
|
display_crc.update<int>(h);
|
2026-05-20 19:12:17 +02:00
|
|
|
|
display.fillRect(x, y, w, h, _curr_color);
|
2025-04-15 22:37:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GxEPDDisplay::drawRect(int x, int y, int w, int h) {
|
2025-09-03 18:17:37 +02:00
|
|
|
|
display_crc.update<int>(x);
|
|
|
|
|
|
display_crc.update<int>(y);
|
|
|
|
|
|
display_crc.update<int>(w);
|
|
|
|
|
|
display_crc.update<int>(h);
|
2026-05-20 19:12:17 +02:00
|
|
|
|
display.drawRect(x, y, w, h, _curr_color);
|
2026-05-24 20:01:46 +02:00
|
|
|
|
if (scale() == 2 && w > 2 && h > 2)
|
2026-05-22 23:13:27 +02:00
|
|
|
|
display.drawRect(x + 1, y + 1, w - 2, h - 2, _curr_color);
|
2025-04-15 22:37:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
|
2025-09-03 18:17:37 +02:00
|
|
|
|
display_crc.update<int>(x);
|
|
|
|
|
|
display_crc.update<int>(y);
|
|
|
|
|
|
display_crc.update<int>(w);
|
|
|
|
|
|
display_crc.update<int>(h);
|
refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
|
|
|
|
display_crc.update<uintptr_t>((uintptr_t)bits);
|
2025-05-27 19:20:35 -07:00
|
|
|
|
uint16_t widthInBytes = (w + 7) / 8;
|
|
|
|
|
|
for (uint16_t by = 0; by < h; by++) {
|
|
|
|
|
|
for (uint16_t bx = 0; bx < w; bx++) {
|
|
|
|
|
|
uint16_t byteOffset = (by * widthInBytes) + (bx / 8);
|
|
|
|
|
|
uint8_t bitMask = 0x80 >> (bx & 7);
|
2026-05-20 19:12:17 +02:00
|
|
|
|
if (pgm_read_byte(bits + byteOffset) & bitMask) {
|
|
|
|
|
|
display.drawPixel(x + bx, y + by, _curr_color);
|
2025-05-27 19:20:35 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-15 22:37:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-04 18:17:18 -07:00
|
|
|
|
uint16_t GxEPDDisplay::getTextWidth(const char* str) {
|
fix(eink): gate Lemon print path on _text_sz == 1
setTextSize(2/3) switches GFX to non-Lemon fonts (built-in scaled or
FreeSans18pt). The Lemon path bypassed GFX entirely and rendered glyphs
at size 1 scale, shrinking splash version and clock when Lemon font
was active.
Refresh CODE_REVIEW.md: remove resolved issues (signed-int timing vars,
abs() no-op, XBM CRC, loadPrefs nesting, lock-screen LPP thrash, reply
prefix raw UTF-8), add new findings (upstream-merge surface, virtual
width()/height() per glyph, page_order sentinel), list enhancement
ideas.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:45:19 +02:00
|
|
|
|
if (_use_lemon && _text_sz == 1) {
|
2026-05-24 16:39:41 +02:00
|
|
|
|
uint16_t total = 0;
|
2026-05-24 20:01:46 +02:00
|
|
|
|
const int sc = scale();
|
2026-05-24 16:39:41 +02:00
|
|
|
|
const uint8_t* p = (const uint8_t*)str;
|
2026-05-24 20:25:35 +02:00
|
|
|
|
while (*p) total += lemonXAdvance(decodeCodepoint(p), sc);
|
2026-05-24 16:39:41 +02:00
|
|
|
|
return total;
|
|
|
|
|
|
}
|
2026-05-23 14:45:24 +02:00
|
|
|
|
display.setTextWrap(false);
|
2025-05-04 18:17:18 -07:00
|
|
|
|
int16_t x1, y1;
|
|
|
|
|
|
uint16_t w, h;
|
|
|
|
|
|
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
|
2026-05-23 14:45:24 +02:00
|
|
|
|
display.setTextWrap(true);
|
2026-05-20 19:12:17 +02:00
|
|
|
|
return w;
|
2025-05-04 18:17:18 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-20 21:53:40 +02:00
|
|
|
|
void GxEPDDisplay::setDisplayRotation(uint8_t rot) {
|
|
|
|
|
|
display.setRotation(rot & 3);
|
|
|
|
|
|
setDimensions(display.width(), display.height());
|
|
|
|
|
|
last_display_crc_value = -1; // force redraw on next endFrame
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-15 22:37:50 +02:00
|
|
|
|
void GxEPDDisplay::endFrame() {
|
2025-09-03 18:17:37 +02:00
|
|
|
|
uint32_t crc = display_crc.finalize();
|
|
|
|
|
|
if (crc != last_display_crc_value) {
|
2026-05-24 10:22:03 +02:00
|
|
|
|
bool partial = true;
|
|
|
|
|
|
if (_full_refresh_interval > 0 && ++_partial_count >= _full_refresh_interval) {
|
|
|
|
|
|
partial = false;
|
|
|
|
|
|
_partial_count = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
display.display(partial);
|
2025-09-03 18:17:37 +02:00
|
|
|
|
last_display_crc_value = crc;
|
|
|
|
|
|
}
|
2025-04-15 22:37:50 +02:00
|
|
|
|
}
|