From cffe960f3f30230e6b9d660887b592384ee4b800 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Wed, 20 May 2026 19:12:17 +0200 Subject: [PATCH] feat: rework GxEPDDisplay for rotation support and integrate e-ink build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove fixed 128×128 virtual space and scaling; use 1:1 pixel mapping - Display dimensions derived from panel native size + DISPLAY_ROTATION - setCursor compensates for GFX font baseline offset (ascender per font size) - Add getLineHeight() / getCharWidth() overrides for FreeSans fonts - New envs: landscape (250×122) and portrait (122×250), BLE and dual variants - Integrate all wio-tracker-l1-improvements features into e-ink build Co-Authored-By: Claude Sonnet 4.6 --- src/helpers/ui/GxEPDDisplay.cpp | 80 +++++++----------- src/helpers/ui/GxEPDDisplay.h | 49 ++++++++--- variants/wio-tracker-l1-eink/platformio.ini | 93 ++++++++++++--------- 3 files changed, 120 insertions(+), 102 deletions(-) diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index ad47754b..c1bcfaa4 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -1,4 +1,3 @@ - #include "GxEPDDisplay.h" #ifdef EXP_PIN_BACKLIGHT @@ -7,13 +6,22 @@ #endif #ifndef DISPLAY_ROTATION - #define DISPLAY_ROTATION 3 + #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) { + if (sz == 3) return 26; // FreeSans18pt7b + if (sz == 2) return 17; // FreeSansBold12pt7b + return 13; // FreeSans9pt7b (sz == 1) +} + bool GxEPDDisplay::begin() { display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); #ifdef ESP32 @@ -23,15 +31,14 @@ bool GxEPDDisplay::begin() { #endif display.init(115200, true, 2, false); display.setRotation(DISPLAY_ROTATION); - setTextSize(1); // Default to size 1 + setTextSize(1); display.setPartialWindow(0, 0, display.width(), display.height()); - display.fillScreen(GxEPD_WHITE); display.display(true); - #if DISP_BACKLIGHT +#if DISP_BACKLIGHT digitalWrite(DISP_BACKLIGHT, LOW); pinMode(DISP_BACKLIGHT, OUTPUT); - #endif +#endif _init = true; return true; } @@ -64,30 +71,24 @@ void GxEPDDisplay::clear() { void GxEPDDisplay::startFrame(Color bkg) { display.fillScreen(GxEPD_WHITE); display.setTextColor(_curr_color = GxEPD_BLACK); + _text_sz = 1; + display.setFont(&FreeSans9pt7b); display_crc.reset(); } void GxEPDDisplay::setTextSize(int sz) { + _text_sz = sz; display_crc.update(sz); - switch(sz) { - case 1: // Small - display.setFont(&FreeSans9pt7b); - break; - case 2: // Medium Bold - display.setFont(&FreeSansBold12pt7b); - break; - case 3: // Large - display.setFont(&FreeSans18pt7b); - break; - default: - display.setFont(&FreeSans9pt7b); - break; + switch (sz) { + case 2: display.setFont(&FreeSansBold12pt7b); break; + case 3: display.setFont(&FreeSans18pt7b); break; + default: display.setFont(&FreeSans9pt7b); break; } } void GxEPDDisplay::setColor(Color c) { - display_crc.update (c); - // colours need to be inverted for epaper displays + display_crc.update(c); + // e-ink: DARK background = white paper, LIGHT foreground = black ink if (c == DARK) { display.setTextColor(_curr_color = GxEPD_WHITE); } else { @@ -98,7 +99,9 @@ void GxEPDDisplay::setColor(Color c) { void GxEPDDisplay::setCursor(int x, int y) { display_crc.update(x); display_crc.update(y); - display.setCursor((x+offset_x)*scale_x, (y+offset_y)*scale_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. + display.setCursor(x, y + fontAscender(_text_sz)); } void GxEPDDisplay::print(const char* str) { @@ -111,7 +114,7 @@ void GxEPDDisplay::fillRect(int x, int y, int w, int h) { display_crc.update(y); display_crc.update(w); display_crc.update(h); - display.fillRect(x*scale_x, y*scale_y, w*scale_x, h*scale_y, _curr_color); + display.fillRect(x, y, w, h, _curr_color); } void GxEPDDisplay::drawRect(int x, int y, int w, int h) { @@ -119,7 +122,7 @@ void GxEPDDisplay::drawRect(int x, int y, int w, int h) { display_crc.update(y); display_crc.update(w); display_crc.update(h); - display.drawRect(x*scale_x, y*scale_y, w*scale_x, h*scale_y, _curr_color); + display.drawRect(x, y, w, h, _curr_color); } void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { @@ -128,36 +131,13 @@ void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { display_crc.update(w); display_crc.update(h); display_crc.update(bits, w * h / 8); - // Calculate the base position in display coordinates - uint16_t startX = x * scale_x; - uint16_t startY = y * scale_y; - - // Width in bytes for bitmap processing uint16_t widthInBytes = (w + 7) / 8; - - // Process the bitmap row by row for (uint16_t by = 0; by < h; by++) { - // Calculate the target y-coordinates for this logical row - int y1 = startY + (int)(by * scale_y); - int y2 = startY + (int)((by + 1) * scale_y); - int block_h = y2 - y1; - - // Scan across the row bit by bit for (uint16_t bx = 0; bx < w; bx++) { - // Calculate the target x-coordinates for this logical column - int x1 = startX + (int)(bx * scale_x); - int x2 = startX + (int)((bx + 1) * scale_x); - int block_w = x2 - x1; - - // Get the current bit uint16_t byteOffset = (by * widthInBytes) + (bx / 8); uint8_t bitMask = 0x80 >> (bx & 7); - bool bitSet = pgm_read_byte(bits + byteOffset) & bitMask; - - // If the bit is set, draw a block of pixels - if (bitSet) { - // Draw the block as a filled rectangle - display.fillRect(x1, y1, block_w, block_h, _curr_color); + if (pgm_read_byte(bits + byteOffset) & bitMask) { + display.drawPixel(x + bx, y + by, _curr_color); } } } @@ -167,7 +147,7 @@ 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 ceil((w + 1) / scale_x); + return w; } void GxEPDDisplay::endFrame() { diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index 1a04cc24..b7f53e43 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -16,37 +16,62 @@ #include "DisplayDriver.h" -class GxEPDDisplay : public DisplayDriver { +#ifndef DISPLAY_ROTATION + #define DISPLAY_ROTATION 0 +#endif +// Panel native dimensions before rotation. Derived from the model class when +// EINK_DISPLAY_MODEL is set; override with EINK_PANEL_W/H for other panels. +#if defined(EINK_DISPLAY_MODEL) + #ifndef EINK_PANEL_W + #define EINK_PANEL_W EINK_DISPLAY_MODEL::WIDTH + #endif + #ifndef EINK_PANEL_H + #define EINK_PANEL_H EINK_DISPLAY_MODEL::HEIGHT + #endif +#else + #ifndef EINK_PANEL_W + #define EINK_PANEL_W 200 + #define EINK_PANEL_H 200 + #endif +#endif + +// Odd rotations (1, 3) swap width and height. +#define EINK_DISP_W ((DISPLAY_ROTATION & 1) ? EINK_PANEL_H : EINK_PANEL_W) +#define EINK_DISP_H ((DISPLAY_ROTATION & 1) ? EINK_PANEL_W : EINK_PANEL_H) + +class GxEPDDisplay : public DisplayDriver { #if defined(EINK_DISPLAY_MODEL) GxEPD2_BW display; - const float scale_x = EINK_SCALE_X; - const float scale_y = EINK_SCALE_Y; - const float offset_x = EINK_X_OFFSET; - const float offset_y = EINK_Y_OFFSET; #else GxEPD2_BW display; - const float scale_x = 1.5625f; - const float scale_y = 1.5625f; - const float offset_x = 0; - const float offset_y = 10; #endif bool _init = false; bool _isOn = false; uint16_t _curr_color; CRC32 display_crc; int last_display_crc_value = 0; + int _text_sz = 1; public: #if defined(EINK_DISPLAY_MODEL) - GxEPDDisplay() : DisplayDriver(128, 128), display(EINK_DISPLAY_MODEL(PIN_DISPLAY_CS, PIN_DISPLAY_DC, PIN_DISPLAY_RST, PIN_DISPLAY_BUSY)) {} + GxEPDDisplay() : DisplayDriver(EINK_DISP_W, EINK_DISP_H), + display(EINK_DISPLAY_MODEL(PIN_DISPLAY_CS, PIN_DISPLAY_DC, PIN_DISPLAY_RST, PIN_DISPLAY_BUSY)) {} #else - GxEPDDisplay() : DisplayDriver(128, 128), display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) {} + GxEPDDisplay() : DisplayDriver(EINK_DISP_W, EINK_DISP_H), + display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) {} #endif + // Line height and approx. char width for each font size: + // 1 = FreeSans9pt (lineH=16, charW≈9) + // 2 = FreeSansBold12pt (lineH=20, charW≈12) + // 3 = FreeSans18pt (lineH=28, charW≈17) + int getCharWidth() const override { return _text_sz == 3 ? 17 : _text_sz == 2 ? 12 : 9; } + int getLineHeight() const override { return _text_sz == 3 ? 28 : _text_sz == 2 ? 20 : 16; } + bool begin(); - bool isOn() override {return _isOn;}; + bool isOn() override { return _isOn; } void turnOn() override; void turnOff() override; void clear() override; diff --git a/variants/wio-tracker-l1-eink/platformio.ini b/variants/wio-tracker-l1-eink/platformio.ini index 0afe327f..04a0df6e 100644 --- a/variants/wio-tracker-l1-eink/platformio.ini +++ b/variants/wio-tracker-l1-eink/platformio.ini @@ -1,12 +1,14 @@ [WioTrackerL1Eink] extends = nrf52_base board = seeed-wio-tracker-l1 -board_build.ldscript = boards/nrf52840_s140_v7.ld +board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld +board_upload.maximum_size = 708608 build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I lib/nrf52/s140_nrf52_7.3.0_API/include -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52 -I variants/wio-tracker-l1 + -I examples/companion_radio/ui-new -D WIO_TRACKER_L1 -D WIO_TRACKER_L1_EINK -D USE_SX1262 @@ -15,57 +17,68 @@ build_flags = ${nrf52_base.build_flags} -D LORA_TX_POWER=22 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 - -D PIN_OLED_RESET=-1 -D EINK_DISPLAY_MODEL=GxEPD2_213_B74 - -D EINK_SCALE_X=1.953125f - -D EINK_SCALE_Y=1.28f - -D EINK_X_OFFSET=0 - -D EINK_Y_OFFSET=10 - -D DISPLAY_ROTATION=1 - -D DISABLE_DIAGNOSTIC_OUTPUT - -D AUTO_OFF_MILLIS=0 -D GPS_BAUD_RATE=9600 -D ENV_PIN_SDA=PIN_WIRE1_SDA -D ENV_PIN_SCL=PIN_WIRE1_SCL + -D DISABLE_DIAGNOSTIC_OUTPUT + -D AUTO_OFF_MILLIS=0 + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D DISPLAY_CLASS=GxEPDDisplay + -D UI_HAS_JOYSTICK=1 + -D UI_HAS_JOYSTICK_UPDOWN=1 + -D PIN_BUZZER=12 + -D QSPIFLASH=1 + -D FIRMWARE_PLUS_BUILD=1 + -D UI_SENSORS_PAGE=1 + -D BLE_PIN_CODE=123456 build_src_filter = ${nrf52_base.build_src_filter} + +<../variants/wio-tracker-l1> + - + -lib_deps= ${nrf52_base.lib_deps} - ${sensor_base.lib_deps} - adafruit/Adafruit GFX Library @ ^1.12.1 - zinggjm/GxEPD2 @ 1.6.2 - bakercp/CRC32 @ ^2.0.0 - -[env:WioTrackerL1Eink_companion_radio_ble] -extends = WioTrackerL1Eink -board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld -board_upload.maximum_size = 708608 -build_flags = ${WioTrackerL1Eink.build_flags} - -I examples/companion_radio/ui-new - -D MAX_CONTACTS=350 - -D MAX_GROUP_CHANNELS=40 - -D BLE_PIN_CODE=123456 - -D BLE_DEBUG_LOGGING=1 - -D OFFLINE_QUEUE_SIZE=256 - -D DISPLAY_CLASS=GxEPDDisplay - -D UI_HAS_JOYSTICK=1 - -D PIN_BUZZER=12 - -D QSPIFLASH=1 - ; -D MESH_PACKET_LOGGING=1 - ; -D MESH_DEBUG=1 - -D UI_RECENT_LIST_SIZE=6 - -D UI_SENSORS_PAGE=1 - -D DUAL_SERIAL -build_src_filter = ${WioTrackerL1Eink.build_src_filter} - + + + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> -lib_deps = ${WioTrackerL1Eink.lib_deps} +lib_deps = ${nrf52_base.lib_deps} + ${sensor_base.lib_deps} + adafruit/Adafruit GFX Library @ ^1.12.1 adafruit/RTClib @ ^2.1.3 + zinggjm/GxEPD2 @ 1.6.2 + bakercp/CRC32 @ ^2.0.0 densaugeo/base64 @ ~1.4.0 end2endzone/NonBlockingRTTTL@^1.3.0 -debug_tool=stlink + +; Landscape: 250×122 px (physical panel rotated 90°) +[env:WioTrackerL1Eink_companion_radio_ble_landscape] +extends = WioTrackerL1Eink +build_flags = ${WioTrackerL1Eink.build_flags} + -D DISPLAY_ROTATION=1 +extra_scripts = post:create-uf2.py + +; Portrait: 122×250 px (physical panel in native orientation) +[env:WioTrackerL1Eink_companion_radio_ble_portrait] +extends = WioTrackerL1Eink +build_flags = ${WioTrackerL1Eink.build_flags} + -D DISPLAY_ROTATION=0 +extra_scripts = post:create-uf2.py + +; Dual BLE+USB — landscape +[env:WioTrackerL1Eink_companion_radio_dual_landscape] +extends = WioTrackerL1Eink +build_flags = ${WioTrackerL1Eink.build_flags} + -D DISPLAY_ROTATION=1 + -D DUAL_SERIAL=1 +extra_scripts = post:create-uf2.py + +; Dual BLE+USB — portrait +[env:WioTrackerL1Eink_companion_radio_dual_portrait] +extends = WioTrackerL1Eink +build_flags = ${WioTrackerL1Eink.build_flags} + -D DISPLAY_ROTATION=0 + -D DUAL_SERIAL=1 +extra_scripts = post:create-uf2.py