#include "EInkDisplay.h" #include #include #include // SSD1677 command definitions // Initialization and reset #define CMD_SOFT_RESET 0x12 // Soft reset #define CMD_BOOSTER_SOFT_START 0x0C // Booster soft-start control #define CMD_DRIVER_OUTPUT_CONTROL 0x01 // Driver output control #define CMD_BORDER_WAVEFORM 0x3C // Border waveform control #define CMD_TEMP_SENSOR_CONTROL 0x18 // Temperature sensor control // RAM and buffer management #define CMD_DATA_ENTRY_MODE 0x11 // Data entry mode #define CMD_SET_RAM_X_RANGE 0x44 // Set RAM X address range #define CMD_SET_RAM_Y_RANGE 0x45 // Set RAM Y address range #define CMD_SET_RAM_X_COUNTER 0x4E // Set RAM X address counter #define CMD_SET_RAM_Y_COUNTER 0x4F // Set RAM Y address counter #define CMD_WRITE_RAM_BW 0x24 // Write to BW RAM (current frame) #define CMD_WRITE_RAM_RED 0x26 // Write to RED RAM (used for fast refresh) #define CMD_AUTO_WRITE_BW_RAM 0x46 // Auto write BW RAM #define CMD_AUTO_WRITE_RED_RAM 0x47 // Auto write RED RAM // Display update and refresh #define CMD_DISPLAY_UPDATE_CTRL1 0x21 // Display update control 1 #define CMD_DISPLAY_UPDATE_CTRL2 0x22 // Display update control 2 #define CMD_MASTER_ACTIVATION 0x20 // Master activation #define CTRL1_NORMAL 0x00 // Normal mode - compare RED vs BW for partial #define CTRL1_BYPASS_RED 0x40 // Bypass RED RAM (treat as 0) - for full refresh // LUT and voltage settings #define CMD_WRITE_LUT 0x32 // Write LUT #define CMD_GATE_VOLTAGE 0x03 // Gate voltage #define CMD_SOURCE_VOLTAGE 0x04 // Source voltage #define CMD_WRITE_VCOM 0x2C // Write VCOM #define CMD_WRITE_TEMP 0x1A // Write temperature // Power management #define CMD_DEEP_SLEEP 0x10 // Deep sleep // Custom LUT for fast refresh const unsigned char lut_grayscale[] PROGMEM = { // 00 black/white 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 01 light gray 0x54, 0x54, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 10 gray 0xAA, 0xA0, 0xA8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 11 dark gray 0xA2, 0x22, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // L4 (VCOM) 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TP/RP groups (global timing) 0x01, 0x01, 0x01, 0x01, 0x00, // G0: A=1 B=1 C=1 D=1 RP=0 (4 frames) 0x01, 0x01, 0x01, 0x01, 0x00, // G1: A=1 B=1 C=1 D=1 RP=0 (4 frames) 0x01, 0x01, 0x01, 0x01, 0x00, // G2: A=0 B=0 C=0 D=0 RP=0 (4 frames) 0x00, 0x00, 0x00, 0x00, 0x00, // G3: A=0 B=0 C=0 D=0 RP=0 0x00, 0x00, 0x00, 0x00, 0x00, // G4: A=0 B=0 C=0 D=0 RP=0 0x00, 0x00, 0x00, 0x00, 0x00, // G5: A=0 B=0 C=0 D=0 RP=0 0x00, 0x00, 0x00, 0x00, 0x00, // G6: A=0 B=0 C=0 D=0 RP=0 0x00, 0x00, 0x00, 0x00, 0x00, // G7: A=0 B=0 C=0 D=0 RP=0 0x00, 0x00, 0x00, 0x00, 0x00, // G8: A=0 B=0 C=0 D=0 RP=0 0x00, 0x00, 0x00, 0x00, 0x00, // G9: A=0 B=0 C=0 D=0 RP=0 // Frame rate 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, // Voltages (VGH, VSH1, VSH2, VSL, VCOM) 0x17, 0x41, 0xA8, 0x32, 0x30, // Reserved 0x00, 0x00}; const unsigned char lut_grayscale_revert[] PROGMEM = { // 00 black/white 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 10 gray 0x54, 0x54, 0x54, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 01 light gray 0xA8, 0xA8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 11 dark gray 0xFC, 0xFC, 0xFC, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // L4 (VCOM) 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // TP/RP groups (global timing) 0x01, 0x01, 0x01, 0x01, 0x01, // G0: A=1 B=1 C=1 D=1 RP=0 (4 frames) 0x01, 0x01, 0x01, 0x01, 0x01, // G1: A=1 B=1 C=1 D=1 RP=0 (4 frames) 0x01, 0x01, 0x01, 0x01, 0x00, // G2: A=0 B=0 C=0 D=0 RP=0 (4 frames) 0x01, 0x01, 0x01, 0x01, 0x00, // G3: A=0 B=0 C=0 D=0 RP=0 0x00, 0x00, 0x00, 0x00, 0x00, // G4: A=0 B=0 C=0 D=0 RP=0 0x00, 0x00, 0x00, 0x00, 0x00, // G5: A=0 B=0 C=0 D=0 RP=0 0x00, 0x00, 0x00, 0x00, 0x00, // G6: A=0 B=0 C=0 D=0 RP=0 0x00, 0x00, 0x00, 0x00, 0x00, // G7: A=0 B=0 C=0 D=0 RP=0 0x00, 0x00, 0x00, 0x00, 0x00, // G8: A=0 B=0 C=0 D=0 RP=0 0x00, 0x00, 0x00, 0x00, 0x00, // G9: A=0 B=0 C=0 D=0 RP=0 // Frame rate 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, // Voltages (VGH, VSH1, VSH2, VSL, VCOM) 0x17, 0x41, 0xA8, 0x32, 0x30, // Reserved 0x00, 0x00}; EInkDisplay::EInkDisplay(int8_t sclk, int8_t mosi, int8_t cs, int8_t dc, int8_t rst, int8_t busy) : _sclk(sclk), _mosi(mosi), _cs(cs), _dc(dc), _rst(rst), _busy(busy), frameBuffer(nullptr), #ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE frameBufferActive(nullptr), #endif customLutActive(false) { Serial.printf("[%lu] EInkDisplay: Constructor called\n", millis()); Serial.printf("[%lu] SCLK=%d, MOSI=%d, CS=%d, DC=%d, RST=%d, BUSY=%d\n", millis(), sclk, mosi, cs, dc, rst, busy); } void EInkDisplay::begin() { Serial.printf("[%lu] EInkDisplay: begin() called\n", millis()); frameBuffer = frameBuffer0; #ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE frameBufferActive = frameBuffer1; #endif // Initialize to white memset(frameBuffer0, 0xFF, BUFFER_SIZE); #ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE Serial.printf("[%lu] Static frame buffer (%lu bytes = 48KB)\n", millis(), BUFFER_SIZE); #else memset(frameBuffer1, 0xFF, BUFFER_SIZE); Serial.printf("[%lu] Static frame buffers (2 x %lu bytes = 96KB)\n", millis(), BUFFER_SIZE); #endif Serial.printf("[%lu] Initializing e-ink display driver...\n", millis()); // Initialize SPI with custom pins SPI.begin(_sclk, -1, _mosi, _cs); spiSettings = SPISettings(40000000, MSBFIRST, SPI_MODE0); // MODE0 is standard for SSD1677 Serial.printf("[%lu] SPI initialized at 40 MHz, Mode 0\n", millis()); // Setup GPIO pins pinMode(_cs, OUTPUT); pinMode(_dc, OUTPUT); pinMode(_rst, OUTPUT); pinMode(_busy, INPUT); digitalWrite(_cs, HIGH); digitalWrite(_dc, HIGH); Serial.printf("[%lu] GPIO pins configured\n", millis()); // Reset display resetDisplay(); // Initialize display controller initDisplayController(); Serial.printf("[%lu] E-ink display driver initialized\n", millis()); } // ============================================================================ // Low-level display control methods // ============================================================================ void EInkDisplay::resetDisplay() { Serial.printf("[%lu] Resetting display...\n", millis()); digitalWrite(_rst, HIGH); delay(20); digitalWrite(_rst, LOW); delay(2); digitalWrite(_rst, HIGH); delay(20); Serial.printf("[%lu] Display reset complete\n", millis()); } void EInkDisplay::sendCommand(uint8_t command) { SPI.beginTransaction(spiSettings); digitalWrite(_dc, LOW); // Command mode digitalWrite(_cs, LOW); // Select chip SPI.transfer(command); digitalWrite(_cs, HIGH); // Deselect chip SPI.endTransaction(); } void EInkDisplay::sendData(uint8_t data) { SPI.beginTransaction(spiSettings); digitalWrite(_dc, HIGH); // Data mode digitalWrite(_cs, LOW); // Select chip SPI.transfer(data); digitalWrite(_cs, HIGH); // Deselect chip SPI.endTransaction(); } void EInkDisplay::sendData(const uint8_t* data, uint16_t length) { SPI.beginTransaction(spiSettings); digitalWrite(_dc, HIGH); // Data mode digitalWrite(_cs, LOW); // Select chip SPI.writeBytes(data, length); // Transfer all bytes digitalWrite(_cs, HIGH); // Deselect chip SPI.endTransaction(); } void EInkDisplay::waitWhileBusy(const char* comment) { unsigned long start = millis(); while (digitalRead(_busy) == HIGH) { delay(1); if (millis() - start > 10000) { Serial.printf("[%lu] Timeout waiting for busy%s\n", millis(), comment ? comment : ""); break; } } if (comment) { Serial.printf("[%lu] Wait complete: %s (%lu ms)\n", millis(), comment, millis() - start); } } void EInkDisplay::initDisplayController() { Serial.printf("[%lu] Initializing SSD1677 controller...\n", millis()); const uint8_t TEMP_SENSOR_INTERNAL = 0x80; // Soft reset sendCommand(CMD_SOFT_RESET); waitWhileBusy(" CMD_SOFT_RESET"); // Temperature sensor control (internal) sendCommand(CMD_TEMP_SENSOR_CONTROL); sendData(TEMP_SENSOR_INTERNAL); // Booster soft-start control (GDEQ0426T82 specific values) sendCommand(CMD_BOOSTER_SOFT_START); sendData(0xAE); sendData(0xC7); sendData(0xC3); sendData(0xC0); sendData(0x40); // Driver output control: set display height (480) and scan direction const uint16_t HEIGHT = 480; sendCommand(CMD_DRIVER_OUTPUT_CONTROL); sendData((HEIGHT - 1) % 256); // gates A0..A7 (low byte) sendData((HEIGHT - 1) / 256); // gates A8..A9 (high byte) sendData(0x02); // SM=1 (interlaced), TB=0 // Border waveform control sendCommand(CMD_BORDER_WAVEFORM); sendData(0x01); // Set up full screen RAM area setRamArea(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT); Serial.printf("[%lu] Clearing RAM buffers...\n", millis()); sendCommand(CMD_AUTO_WRITE_BW_RAM); // Auto write BW RAM sendData(0xF7); waitWhileBusy(" CMD_AUTO_WRITE_BW_RAM"); sendCommand(CMD_AUTO_WRITE_RED_RAM); // Auto write RED RAM sendData(0xF7); // Fill with white pattern waitWhileBusy(" CMD_AUTO_WRITE_RED_RAM"); Serial.printf("[%lu] SSD1677 controller initialized\n", millis()); } void EInkDisplay::setRamArea(const uint16_t x, uint16_t y, uint16_t w, uint16_t h) { constexpr uint8_t DATA_ENTRY_X_INC_Y_DEC = 0x01; // Reverse Y coordinate (gates are reversed on this display) y = DISPLAY_HEIGHT - y - h; // Set data entry mode (X increment, Y decrement for reversed gates) sendCommand(CMD_DATA_ENTRY_MODE); sendData(DATA_ENTRY_X_INC_Y_DEC); // Set RAM X address range (start, end) - X is in PIXELS sendCommand(CMD_SET_RAM_X_RANGE); sendData(x % 256); // start low byte sendData(x / 256); // start high byte sendData((x + w - 1) % 256); // end low byte sendData((x + w - 1) / 256); // end high byte // Set RAM Y address range (start, end) - Y is in PIXELS sendCommand(CMD_SET_RAM_Y_RANGE); sendData((y + h - 1) % 256); // start low byte sendData((y + h - 1) / 256); // start high byte sendData(y % 256); // end low byte sendData(y / 256); // end high byte // Set RAM X address counter - X is in PIXELS sendCommand(CMD_SET_RAM_X_COUNTER); sendData(x % 256); // low byte sendData(x / 256); // high byte // Set RAM Y address counter - Y is in PIXELS sendCommand(CMD_SET_RAM_Y_COUNTER); sendData((y + h - 1) % 256); // low byte sendData((y + h - 1) / 256); // high byte } void EInkDisplay::clearScreen(const uint8_t color) const { memset(frameBuffer, color, BUFFER_SIZE); } void EInkDisplay::drawImage(const uint8_t* imageData, const uint16_t x, const uint16_t y, const uint16_t w, const uint16_t h, const bool fromProgmem) const { if (!frameBuffer) { Serial.printf("[%lu] ERROR: Frame buffer not allocated!\n", millis()); return; } // Calculate bytes per line for the image const uint16_t imageWidthBytes = w / 8; // Copy image data to frame buffer for (uint16_t row = 0; row < h; row++) { const uint16_t destY = y + row; if (destY >= DISPLAY_HEIGHT) break; const uint16_t destOffset = destY * DISPLAY_WIDTH_BYTES + (x / 8); const uint16_t srcOffset = row * imageWidthBytes; for (uint16_t col = 0; col < imageWidthBytes; col++) { if ((x / 8 + col) >= DISPLAY_WIDTH_BYTES) break; if (fromProgmem) { frameBuffer[destOffset + col] = pgm_read_byte(&imageData[srcOffset + col]); } else { frameBuffer[destOffset + col] = imageData[srcOffset + col]; } } } Serial.printf("[%lu] Image drawn to frame buffer\n", millis()); } void EInkDisplay::writeRamBuffer(uint8_t ramBuffer, const uint8_t* data, uint32_t size) { const char* bufferName = (ramBuffer == CMD_WRITE_RAM_BW) ? "BW" : "RED"; const unsigned long startTime = millis(); Serial.printf("[%lu] Writing frame buffer to %s RAM (%lu bytes)...\n", startTime, bufferName, size); sendCommand(ramBuffer); sendData(data, size); const unsigned long duration = millis() - startTime; Serial.printf("[%lu] %s RAM write complete (%lu ms)\n", millis(), bufferName, duration); } void EInkDisplay::setFramebuffer(const uint8_t* bwBuffer) const { memcpy(frameBuffer, bwBuffer, BUFFER_SIZE); } #ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE void EInkDisplay::swapBuffers() { uint8_t* temp = frameBuffer; frameBuffer = frameBufferActive; frameBufferActive = temp; } #endif void EInkDisplay::grayscaleRevert() { if (!inGrayscaleMode) { return; } inGrayscaleMode = false; // Load the revert LUT setCustomLUT(true, lut_grayscale_revert); refreshDisplay(FAST_REFRESH); setCustomLUT(false); } void EInkDisplay::copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer) { setRamArea(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT); writeRamBuffer(CMD_WRITE_RAM_BW, lsbBuffer, BUFFER_SIZE); } void EInkDisplay::copyGrayscaleMsbBuffers(const uint8_t* msbBuffer) { setRamArea(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT); writeRamBuffer(CMD_WRITE_RAM_RED, msbBuffer, BUFFER_SIZE); } void EInkDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer) { setRamArea(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT); writeRamBuffer(CMD_WRITE_RAM_BW, lsbBuffer, BUFFER_SIZE); writeRamBuffer(CMD_WRITE_RAM_RED, msbBuffer, BUFFER_SIZE); } #ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE /** * In single buffer mode, this should be called with the previously written BW buffer * to restore proper BW state after a grayscale display. * * The approach: Don't call grayscaleRevert() at all. Instead, just sync the RAMs with * BW data and clear the grayscale mode flag. The physical pixels will stay in their * current grayscale states, but subsequent refreshes will naturally transition them * as the new BW content is displayed. */ void EInkDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) { // Clear grayscale mode - we don't want grayscaleRevert to be called later // because the RAMs won't have valid grayscale data anymore inGrayscaleMode = false; // Sync both RAMs with BW content for proper fast refresh behavior setRamArea(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT); writeRamBuffer(CMD_WRITE_RAM_BW, bwBuffer, BUFFER_SIZE); writeRamBuffer(CMD_WRITE_RAM_RED, bwBuffer, BUFFER_SIZE); } #endif void EInkDisplay::displayBuffer(RefreshMode mode) { if (!isScreenOn) { // Force half refresh if screen is off mode = HALF_REFRESH; } // If currently in grayscale mode, revert first to black/white if (inGrayscaleMode) { grayscaleRevert(); // grayscaleRevert() sets inGrayscaleMode = false internally } // Set up full screen RAM area setRamArea(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT); if (mode != FAST_REFRESH) { // For full refresh, write to both buffers before refresh writeRamBuffer(CMD_WRITE_RAM_BW, frameBuffer, BUFFER_SIZE); writeRamBuffer(CMD_WRITE_RAM_RED, frameBuffer, BUFFER_SIZE); } else { // For fast refresh, write to BW buffer only writeRamBuffer(CMD_WRITE_RAM_BW, frameBuffer, BUFFER_SIZE); // In single buffer mode, the RED RAM should already contain the previous frame // In dual buffer mode, we write back frameBufferActive which is the last frame #ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE writeRamBuffer(CMD_WRITE_RAM_RED, frameBufferActive, BUFFER_SIZE); #endif } #ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE swapBuffers(); #endif // Refresh the display refreshDisplay(mode); #ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE // In single buffer mode always sync RED RAM after refresh to prepare for next fast refresh // This ensures RED contains the currently displayed frame for differential comparison setRamArea(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT); writeRamBuffer(CMD_WRITE_RAM_RED, frameBuffer, BUFFER_SIZE); #endif } // EXPERIMENTAL: Windowed update support // Displays only a rectangular region of the frame buffer, preserving the rest of the screen. // Requirements: x and w must be byte-aligned (multiples of 8 pixels) void EInkDisplay::displayWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { Serial.printf("[%lu] Displaying window at (%d,%d) size (%dx%d)\n", millis(), x, y, w, h); // Validate bounds if (x + w > DISPLAY_WIDTH || y + h > DISPLAY_HEIGHT) { Serial.printf("[%lu] ERROR: Window bounds exceed display dimensions!\n", millis()); return; } // Validate byte alignment if (x % 8 != 0 || w % 8 != 0) { Serial.printf("[%lu] ERROR: Window x and width must be byte-aligned (multiples of 8)!\n", millis()); return; } if (!frameBuffer) { Serial.printf("[%lu] ERROR: Frame buffer not allocated!\n", millis()); return; } // displayWindow is not supported while the rest of the screen has grayscale content, revert it if (inGrayscaleMode) { grayscaleRevert(); // grayscaleRevert() sets inGrayscaleMode = false internally } // Calculate window buffer size const uint16_t windowWidthBytes = w / 8; const uint32_t windowBufferSize = windowWidthBytes * h; Serial.printf("[%lu] Window buffer size: %lu bytes (%d x %d pixels)\n", millis(), windowBufferSize, w, h); // Allocate temporary buffer on stack std::vector windowBuffer(windowBufferSize); // Extract window region from frame buffer for (uint16_t row = 0; row < h; row++) { const uint16_t srcY = y + row; const uint16_t srcOffset = srcY * DISPLAY_WIDTH_BYTES + (x / 8); const uint16_t dstOffset = row * windowWidthBytes; memcpy(&windowBuffer[dstOffset], &frameBuffer[srcOffset], windowWidthBytes); } // Configure RAM area for window setRamArea(x, y, w, h); // Write to BW RAM (current frame) writeRamBuffer(CMD_WRITE_RAM_BW, windowBuffer.data(), windowBufferSize); #ifndef EINK_DISPLAY_SINGLE_BUFFER_MODE // Dual buffer: Extract window from frameBufferActive (previous frame) std::vector previousWindowBuffer(windowBufferSize); for (uint16_t row = 0; row < h; row++) { const uint16_t srcY = y + row; const uint16_t srcOffset = srcY * DISPLAY_WIDTH_BYTES + (x / 8); const uint16_t dstOffset = row * windowWidthBytes; memcpy(&previousWindowBuffer[dstOffset], &frameBufferActive[srcOffset], windowWidthBytes); } writeRamBuffer(CMD_WRITE_RAM_RED, previousWindowBuffer.data(), windowBufferSize); #endif // Perform fast refresh refreshDisplay(FAST_REFRESH); #ifdef EINK_DISPLAY_SINGLE_BUFFER_MODE // Post-refresh: Sync RED RAM with current window (for next fast refresh) setRamArea(x, y, w, h); writeRamBuffer(CMD_WRITE_RAM_RED, windowBuffer.data(), windowBufferSize); #endif Serial.printf("[%lu] Window display complete\n", millis()); } void EInkDisplay::displayGrayBuffer(const bool turnOffScreen) { drawGrayscale = false; inGrayscaleMode = true; // activate the custom LUT for grayscale rendering and refresh setCustomLUT(true, lut_grayscale); refreshDisplay(FAST_REFRESH, turnOffScreen); setCustomLUT(false); } void EInkDisplay::refreshDisplay(const RefreshMode mode, const bool turnOffScreen) { // Configure Display Update Control 1 sendCommand(CMD_DISPLAY_UPDATE_CTRL1); sendData((mode == FAST_REFRESH) ? CTRL1_NORMAL : CTRL1_BYPASS_RED); // Configure buffer comparison mode // best guess at display mode bits: // bit | hex | name | effect // ----+-----+--------------------------+------------------------------------------- // 7 | 80 | CLOCK_ON | Start internal oscillator // 6 | 40 | ANALOG_ON | Enable analog power rails (VGH/VGL drivers) // 5 | 20 | TEMP_LOAD | Load temperature (internal or I2C) // 4 | 10 | LUT_LOAD | Load waveform LUT // 3 | 08 | MODE_SELECT | Mode 1/2 // 2 | 04 | DISPLAY_START | Run display // 1 | 02 | ANALOG_OFF_PHASE | Shutdown step 1 (undocumented) // 0 | 01 | CLOCK_OFF | Disable internal oscillator // Select appropriate display mode based on refresh type uint8_t displayMode = 0x00; // Enable counter and analog if not already on if (!isScreenOn) { isScreenOn = true; displayMode |= 0xC0; // Set CLOCK_ON and ANALOG_ON bits } // Turn off screen if requested if (turnOffScreen) { isScreenOn = false; displayMode |= 0x03; // Set ANALOG_OFF_PHASE and CLOCK_OFF bits } if (mode == FULL_REFRESH) { displayMode |= 0x34; } else if (mode == HALF_REFRESH) { // Write high temp to the register for a faster refresh sendCommand(CMD_WRITE_TEMP); sendData(0x5A); displayMode |= 0xD4; } else { // FAST_REFRESH displayMode |= customLutActive ? 0x0C : 0x1C; } // Power on and refresh display const char* refreshType = (mode == FULL_REFRESH) ? "full" : (mode == HALF_REFRESH) ? "half" : "fast"; Serial.printf("[%lu] Powering on display 0x%02X (%s refresh)...\n", millis(), displayMode, refreshType); sendCommand(CMD_DISPLAY_UPDATE_CTRL2); sendData(displayMode); sendCommand(CMD_MASTER_ACTIVATION); // Wait for display to finish updating Serial.printf("[%lu] Waiting for display refresh...\n", millis()); waitWhileBusy(refreshType); } void EInkDisplay::setCustomLUT(const bool enabled, const unsigned char* lutData) { if (enabled) { Serial.printf("[%lu] Loading custom LUT...\n", millis()); // Load custom LUT (first 105 bytes: VS + TP/RP + frame rate) sendCommand(CMD_WRITE_LUT); for (uint16_t i = 0; i < 105; i++) { sendData(pgm_read_byte(&lutData[i])); } // Set voltage values from bytes 105-109 sendCommand(CMD_GATE_VOLTAGE); // VGH sendData(pgm_read_byte(&lutData[105])); sendCommand(CMD_SOURCE_VOLTAGE); // VSH1, VSH2, VSL sendData(pgm_read_byte(&lutData[106])); // VSH1 sendData(pgm_read_byte(&lutData[107])); // VSH2 sendData(pgm_read_byte(&lutData[108])); // VSL sendCommand(CMD_WRITE_VCOM); // VCOM sendData(pgm_read_byte(&lutData[109])); customLutActive = true; Serial.printf("[%lu] Custom LUT loaded\n", millis()); } else { customLutActive = false; Serial.printf("[%lu] Custom LUT disabled\n", millis()); } } void EInkDisplay::deepSleep() { Serial.printf("[%lu] Preparing display for deep sleep...\n", millis()); // First, power down the display properly // This shuts down the analog power rails and clock if (isScreenOn) { sendCommand(CMD_DISPLAY_UPDATE_CTRL1); sendData(CTRL1_BYPASS_RED); // Normal mode sendCommand(CMD_DISPLAY_UPDATE_CTRL2); sendData(0x03); // Set ANALOG_OFF_PHASE (bit 1) and CLOCK_OFF (bit 0) sendCommand(CMD_MASTER_ACTIVATION); // Wait for the power-down sequence to complete waitWhileBusy(" display power-down"); isScreenOn = false; } // Now enter deep sleep mode Serial.printf("[%lu] Entering deep sleep mode...\n", millis()); sendCommand(CMD_DEEP_SLEEP); sendData(0x01); // Enter deep sleep } void EInkDisplay::saveFrameBufferAsPBM(const char* filename) { #ifndef ARDUINO const uint8_t* buffer = getFrameBuffer(); std::ofstream file(filename, std::ios::binary); if (!file) { Serial.printf("Failed to open %s for writing\n", filename); return; } // Rotate the image 90 degrees counterclockwise when saving // Original buffer: 800x480 (landscape) // Output image: 480x800 (portrait) const int DISPLAY_WIDTH_LOCAL = DISPLAY_WIDTH; // 800 const int DISPLAY_HEIGHT_LOCAL = DISPLAY_HEIGHT; // 480 const int DISPLAY_WIDTH_BYTES_LOCAL = DISPLAY_WIDTH_LOCAL / 8; file << "P4\n"; // Binary PBM file << DISPLAY_HEIGHT_LOCAL << " " << DISPLAY_WIDTH_LOCAL << "\n"; // Create rotated buffer std::vector rotatedBuffer((DISPLAY_HEIGHT_LOCAL / 8) * DISPLAY_WIDTH_LOCAL, 0); for (int outY = 0; outY < DISPLAY_WIDTH_LOCAL; outY++) { for (int outX = 0; outX < DISPLAY_HEIGHT_LOCAL; outX++) { int inX = outY; int inY = DISPLAY_HEIGHT_LOCAL - 1 - outX; int inByteIndex = inY * DISPLAY_WIDTH_BYTES_LOCAL + (inX / 8); int inBitPosition = 7 - (inX % 8); bool isWhite = (buffer[inByteIndex] >> inBitPosition) & 1; int outByteIndex = outY * (DISPLAY_HEIGHT_LOCAL / 8) + (outX / 8); int outBitPosition = 7 - (outX % 8); if (!isWhite) { // Invert: e-ink white=1 -> PBM black=1 rotatedBuffer[outByteIndex] |= (1 << outBitPosition); } } } file.write(reinterpret_cast(rotatedBuffer.data()), rotatedBuffer.size()); file.close(); Serial.printf("Saved framebuffer to %s\n", filename); #else (void)filename; Serial.println("saveFrameBufferAsPBM is not supported on Arduino builds."); #endif }