mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 23:08:11 +00:00
Portrait mode (122px wide) matches the OLED's ~21 chars/line with a 1× font. Landscape mode (250px wide) was using the same small 6×8 font, leaving text physically tiny and the wide display underutilised. Size 1 now scales by orientation: 1× (6×8 / Lemon 5×10) in portrait, 2× (12×16 / Lemon 10×20) in landscape. Size 2 stays at 12×16 and size 3 at FreeSans18pt in both orientations — their layout Y-positions are hardcoded so they cannot be scaled without a full layout refactor. fontAscender() updated to accept a scale parameter; Lemon ascender becomes 8×scale (8px portrait, 16px landscape) to keep top-of-cell coordinate semantics consistent with the rest of the UI code. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
182 lines
5.2 KiB
C++
182 lines
5.2 KiB
C++
#include "GxEPDDisplay.h"
|
||
|
||
#ifdef EXP_PIN_BACKLIGHT
|
||
#include <PCA9557.h>
|
||
extern PCA9557 expander;
|
||
#endif
|
||
|
||
#ifndef DISPLAY_ROTATION
|
||
#define DISPLAY_ROTATION 0
|
||
#endif
|
||
|
||
#ifdef ESP32
|
||
SPIClass SPI1 = SPIClass(FSPI);
|
||
#endif
|
||
|
||
// 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.
|
||
static int fontAscender(int sz, bool use_lemon, int scale) {
|
||
if (sz == 3) return 26; // FreeSans18pt7b: proportional, baseline origin
|
||
if (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
|
||
}
|
||
|
||
bool GxEPDDisplay::begin() {
|
||
display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0));
|
||
#ifdef ESP32
|
||
SPI1.begin(PIN_DISPLAY_SCLK, PIN_DISPLAY_MISO, PIN_DISPLAY_MOSI, PIN_DISPLAY_CS);
|
||
#else
|
||
SPI1.begin();
|
||
#endif
|
||
display.init(115200, true, 2, false);
|
||
display.setRotation(DISPLAY_ROTATION);
|
||
setTextSize(1);
|
||
display.setPartialWindow(0, 0, display.width(), display.height());
|
||
display.fillScreen(GxEPD_WHITE);
|
||
display.display(true);
|
||
#if DISP_BACKLIGHT
|
||
digitalWrite(DISP_BACKLIGHT, LOW);
|
||
pinMode(DISP_BACKLIGHT, OUTPUT);
|
||
#endif
|
||
_init = true;
|
||
return true;
|
||
}
|
||
|
||
void GxEPDDisplay::turnOn() {
|
||
if (!_init) begin();
|
||
#if defined(DISP_BACKLIGHT) && !defined(BACKLIGHT_BTN)
|
||
digitalWrite(DISP_BACKLIGHT, HIGH);
|
||
#elif defined(EXP_PIN_BACKLIGHT) && !defined(BACKLIGHT_BTN)
|
||
expander.digitalWrite(EXP_PIN_BACKLIGHT, HIGH);
|
||
#endif
|
||
_isOn = true;
|
||
}
|
||
|
||
void GxEPDDisplay::turnOff() {
|
||
#if defined(DISP_BACKLIGHT) && !defined(BACKLIGHT_BTN)
|
||
digitalWrite(DISP_BACKLIGHT, LOW);
|
||
#elif defined(EXP_PIN_BACKLIGHT) && !defined(BACKLIGHT_BTN)
|
||
expander.digitalWrite(EXP_PIN_BACKLIGHT, LOW);
|
||
#endif
|
||
_isOn = false;
|
||
}
|
||
|
||
void GxEPDDisplay::clear() {
|
||
display.fillScreen(GxEPD_WHITE);
|
||
display.setTextColor(GxEPD_BLACK);
|
||
display_crc.reset();
|
||
}
|
||
|
||
void GxEPDDisplay::startFrame(Color bkg) {
|
||
display.fillScreen(GxEPD_WHITE);
|
||
display.setTextColor(_curr_color = GxEPD_BLACK);
|
||
_text_sz = 1;
|
||
int sc = (width() >= height()) ? 2 : 1;
|
||
display.setFont(_use_lemon ? &Lemon : NULL);
|
||
display.setTextSize(sc);
|
||
display_crc.reset();
|
||
}
|
||
|
||
void GxEPDDisplay::setTextSize(int sz) {
|
||
_text_sz = sz;
|
||
display_crc.update<int>(sz);
|
||
// 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.
|
||
int sc = (width() >= height()) ? 2 : 1;
|
||
switch (sz) {
|
||
case 3:
|
||
display.setFont(&FreeSans18pt7b);
|
||
display.setTextSize(1);
|
||
break;
|
||
case 2:
|
||
display.setFont(NULL);
|
||
display.setTextSize(2);
|
||
break;
|
||
default:
|
||
display.setFont(_use_lemon ? &Lemon : NULL);
|
||
display.setTextSize(sc);
|
||
break;
|
||
}
|
||
}
|
||
|
||
void GxEPDDisplay::setColor(Color c) {
|
||
display_crc.update<Color>(c);
|
||
// e-ink: DARK background = white paper, LIGHT foreground = black ink
|
||
if (c == DARK) {
|
||
display.setTextColor(_curr_color = GxEPD_WHITE);
|
||
} else {
|
||
display.setTextColor(_curr_color = GxEPD_BLACK);
|
||
}
|
||
}
|
||
|
||
void GxEPDDisplay::setCursor(int x, int y) {
|
||
display_crc.update<int>(x);
|
||
display_crc.update<int>(y);
|
||
// 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.
|
||
int sc = (width() >= height()) ? 2 : 1;
|
||
display.setCursor(x, y + fontAscender(_text_sz, _use_lemon, sc));
|
||
}
|
||
|
||
void GxEPDDisplay::print(const char* str) {
|
||
display_crc.update<char>(str, strlen(str));
|
||
display.print(str);
|
||
}
|
||
|
||
void GxEPDDisplay::fillRect(int x, int y, int w, int h) {
|
||
display_crc.update<int>(x);
|
||
display_crc.update<int>(y);
|
||
display_crc.update<int>(w);
|
||
display_crc.update<int>(h);
|
||
display.fillRect(x, y, w, h, _curr_color);
|
||
}
|
||
|
||
void GxEPDDisplay::drawRect(int x, int y, int w, int h) {
|
||
display_crc.update<int>(x);
|
||
display_crc.update<int>(y);
|
||
display_crc.update<int>(w);
|
||
display_crc.update<int>(h);
|
||
display.drawRect(x, y, w, h, _curr_color);
|
||
}
|
||
|
||
void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
|
||
display_crc.update<int>(x);
|
||
display_crc.update<int>(y);
|
||
display_crc.update<int>(w);
|
||
display_crc.update<int>(h);
|
||
display_crc.update<uint8_t>(bits, w * h / 8);
|
||
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);
|
||
if (pgm_read_byte(bits + byteOffset) & bitMask) {
|
||
display.drawPixel(x + bx, y + by, _curr_color);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
uint16_t GxEPDDisplay::getTextWidth(const char* str) {
|
||
int16_t x1, y1;
|
||
uint16_t w, h;
|
||
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
|
||
return w;
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
void GxEPDDisplay::endFrame() {
|
||
uint32_t crc = display_crc.finalize();
|
||
if (crc != last_display_crc_value) {
|
||
display.display(true);
|
||
last_display_crc_value = crc;
|
||
}
|
||
}
|