feat: rework GxEPDDisplay for rotation support and integrate e-ink build

- 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 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-05-20 19:12:17 +02:00
parent 67bccd3dd4
commit cffe960f3f
3 changed files with 120 additions and 102 deletions

View File

@@ -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<int>(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<Color> (c);
// colours need to be inverted for epaper displays
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 {
@@ -98,7 +99,9 @@ void GxEPDDisplay::setColor(Color c) {
void GxEPDDisplay::setCursor(int x, int y) {
display_crc.update<int>(x);
display_crc.update<int>(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<int>(y);
display_crc.update<int>(w);
display_crc.update<int>(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<int>(y);
display_crc.update<int>(w);
display_crc.update<int>(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<int>(w);
display_crc.update<int>(h);
display_crc.update<uint8_t>(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() {

View File

@@ -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<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT> 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<GxEPD2_150_BN, 200> 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;

View File

@@ -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}
+<WioTrackerL1Board.cpp>
+<../variants/wio-tracker-l1>
+<helpers/ui/GxEPDDisplay.cpp>
+<helpers/sensors>
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}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/ui/buzzer.cpp>
+<helpers/nrf52/SerialBLEInterface.cpp>
+<helpers/sensors>
+<../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