#include "GxEPDDisplay.h" #ifdef EXP_PIN_BACKLIGHT #include extern PCA9557 expander; #endif #ifndef DISPLAY_ROTATION #define DISPLAY_ROTATION 0 #endif #ifdef ESP32 // E-ink and LoRa SHARE the same SPI bus (SCK=36, MOSI=33) // They MUST use the same SPI peripheral (HSPI) to avoid GPIO conflicts // Different chip selects allow both to coexist: E-ink CS=34, LoRa CS=3 SPIClass displaySpi(HSPI); #endif bool GxEPDDisplay::begin() { #ifdef ESP32 // Initialize HSPI with shared pins // SCK=36, MISO=47 (for LoRa receive), MOSI=33, SS=34 (e-ink CS) displaySpi.begin(PIN_DISPLAY_SCLK, 47, PIN_DISPLAY_MOSI, PIN_DISPLAY_CS); // Tell GxEPD2 to use our SPI instance // Using slower speed (4MHz) for reliable e-ink communication display.epd2.selectSPI(displaySpi, SPISettings(4000000, MSBFIRST, SPI_MODE0)); #endif // Initialize with: // - 115200 baud for debug serial // - initial=true (do initial update) // - reset_duration=2 (ms) // - pulldown_rst_mode=false display.init(115200, true, 2, false); display.setRotation(DISPLAY_ROTATION); setTextSize(1); // Default to size 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; _isOn = true; // Set display as "on" after initialization 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); display_crc.reset(); } void GxEPDDisplay::setTextSize(int sz) { display_crc.update(sz); switch(sz) { case 0: // Tiny - built-in 6x8 pixel font display.setFont(NULL); display.setTextSize(1); break; case 1: // Small - use 9pt (was 9pt) display.setFont(&FreeSans9pt7b); break; case 2: // Medium Bold - use 9pt bold instead of 12pt display.setFont(&FreeSans9pt7b); break; case 3: // Large - use 12pt instead of 18pt display.setFont(&FreeSansBold12pt7b); break; default: display.setFont(&FreeSans9pt7b); break; } } void GxEPDDisplay::setColor(Color c) { display_crc.update (c); // colours need to be inverted for epaper displays 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(x); display_crc.update(y); // Add extra offset (+5) to push text baseline down, preventing ascenders from overlapping elements above display.setCursor((x+offset_x)*scale_x, (y+offset_y+5)*scale_y); } void GxEPDDisplay::print(const char* str) { display_crc.update(str, strlen(str)); display.print(str); } void GxEPDDisplay::fillRect(int x, int y, int w, int h) { display_crc.update(x); display_crc.update(y); display_crc.update(w); display_crc.update(h); display.fillRect((x+offset_x)*scale_x, (y+offset_y)*scale_y, w*scale_x, h*scale_y, _curr_color); } void GxEPDDisplay::drawRect(int x, int y, int w, int h) { display_crc.update(x); display_crc.update(y); display_crc.update(w); display_crc.update(h); display.drawRect((x+offset_x)*scale_x, (y+offset_y)*scale_y, w*scale_x, h*scale_y, _curr_color); } void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { display_crc.update(x); display_crc.update(y); display_crc.update(w); display_crc.update(h); display_crc.update(bits, w * h / 8); // Calculate the base position in display coordinates (with offset applied) uint16_t startX = (x + offset_x) * scale_x; uint16_t startY = (y + offset_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); } } } } 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); } void GxEPDDisplay::endFrame() { uint32_t crc = display_crc.finalize(); if (crc != last_display_crc_value) { display.display(true); // Partial refresh last_display_crc_value = crc; } }