diff --git a/libs/display/EInkDisplay/include/EInkDisplay.h b/libs/display/EInkDisplay/include/EInkDisplay.h new file mode 100644 index 0000000..104632b --- /dev/null +++ b/libs/display/EInkDisplay/include/EInkDisplay.h @@ -0,0 +1,93 @@ +#pragma once +#include +#include + +class EInkDisplay { + public: + // Constructor with pin configuration + EInkDisplay(int8_t sclk, int8_t mosi, int8_t cs, int8_t dc, int8_t rst, int8_t busy); + + // Destructor + ~EInkDisplay(); + + // Refresh modes (guarded to avoid redefinition in test builds) + enum RefreshMode { + FULL_REFRESH, // Full refresh with complete waveform + HALF_REFRESH, // Half refresh (1720ms) - balanced quality and speed + FAST_REFRESH // Fast refresh using custom LUT + }; + + // Initialize the display hardware and driver + void begin(); + + // Display dimensions + static const uint16_t DISPLAY_WIDTH = 800; + static const uint16_t DISPLAY_HEIGHT = 480; + static const uint16_t DISPLAY_WIDTH_BYTES = DISPLAY_WIDTH / 8; + static const uint32_t BUFFER_SIZE = DISPLAY_WIDTH_BYTES * DISPLAY_HEIGHT; + + // Frame buffer operations + void clearScreen(uint8_t color = 0xFF); + void drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h, bool fromProgmem = false); + + void swapBuffers(); + void setFramebuffer(const uint8_t* bwBuffer); + + void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer); + void copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer); + void copyGrayscaleMsbBuffers(const uint8_t* msbBuffer); + + void displayBuffer(RefreshMode mode = FAST_REFRESH); + void displayGrayBuffer(bool turnOffScreen = false); + + void refreshDisplay(RefreshMode mode = FAST_REFRESH, bool turnOffScreen = false); + + // debug function + void grayscaleRevert(); + + // LUT control + void setCustomLUT(bool enabled, const unsigned char* lutData = nullptr); + + // Power management + void deepSleep(); + + // Access to frame buffer + uint8_t* getFrameBuffer() { + return frameBuffer; + } + + // Save the current framebuffer to a PBM file (desktop/test builds only) + void saveFrameBufferAsPBM(const char* filename); + + private: + // Pin configuration + int8_t _sclk, _mosi, _cs, _dc, _rst, _busy; + + // Frame buffer (statically allocated) + uint8_t frameBuffer0[BUFFER_SIZE]; + uint8_t frameBuffer1[BUFFER_SIZE]; + + uint8_t* frameBuffer; + uint8_t* frameBufferActive; + + // SPI settings + SPISettings spiSettings; + + // State + bool isScreenOn; + bool customLutActive; + bool inGrayscaleMode; + bool drawGrayscale; + + // Low-level display control + void resetDisplay(); + void sendCommand(uint8_t command); + void sendData(uint8_t data); + void sendData(const uint8_t* data, uint16_t length); + void waitWhileBusy(const char* comment = nullptr); + void initDisplayController(); + + // Low-level display operations + void setRamArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h); + void writeRamBuffer(uint8_t ramBuffer, const uint8_t* data, uint32_t size); +}; diff --git a/libs/display/EInkDisplay/library.json b/libs/display/EInkDisplay/library.json new file mode 100644 index 0000000..a57fa19 --- /dev/null +++ b/libs/display/EInkDisplay/library.json @@ -0,0 +1,14 @@ +{ + "name": "EInkDisplay", + "version": "1.0.0", + "description": "Low level E-Ink display driver, with support for grayscale and partial refresh modes.", + "authors": [ + { + "name": "CidVonHighwind", + "url": "https://github.com/CidVonHighwind" + } + ], + "dependencies": {}, + "platforms": "espressif32", + "frameworks": ["arduino"] +} diff --git a/libs/display/EInkDisplay/src/EInkDisplay.cpp b/libs/display/EInkDisplay/src/EInkDisplay.cpp new file mode 100644 index 0000000..092c7f8 --- /dev/null +++ b/libs/display/EInkDisplay/src/EInkDisplay.cpp @@ -0,0 +1,573 @@ +#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), + frameBufferActive(nullptr), + 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); +} + +EInkDisplay::~EInkDisplay() { + // No dynamic memory to clean up (buffers are statically allocated) +} + +void EInkDisplay::begin() { + Serial.printf("[%lu] EInkDisplay: begin() called\n", millis()); + + frameBuffer = frameBuffer0; + frameBufferActive = frameBuffer1; + + // Initialize to white + memset(frameBuffer0, 0xFF, BUFFER_SIZE); + memset(frameBuffer1, 0xFF, BUFFER_SIZE); + + Serial.printf("[%lu] Static frame buffers (2 x %lu bytes = 96KB)\n", millis(), BUFFER_SIZE); + 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(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { + const uint16_t WIDTH = 800; + const uint16_t HEIGHT = 480; + const uint8_t DATA_ENTRY_X_INC_Y_DEC = 0x01; + + // Reverse Y coordinate (gates are reversed on this display) + y = 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(uint8_t color) { + memset(frameBuffer, color, BUFFER_SIZE); +} + +void EInkDisplay::drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h, + bool fromProgmem) { + if (!frameBuffer) { + Serial.printf("[%lu] ERROR: Frame buffer not allocated!\n", millis()); + return; + } + + // Calculate bytes per line for the image + uint16_t imageWidthBytes = w / 8; + + // Copy image data to frame buffer + for (uint16_t row = 0; row < h; row++) { + uint16_t destY = y + row; + if (destY >= DISPLAY_HEIGHT) + break; + + uint16_t destOffset = destY * DISPLAY_WIDTH_BYTES + (x / 8); + 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"; + unsigned long startTime = millis(); + Serial.printf("[%lu] Writing frame buffer to %s RAM (%lu bytes)...\n", startTime, bufferName, size); + + sendCommand(ramBuffer); + sendData(data, size); + + 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) { + memcpy(frameBuffer, bwBuffer, BUFFER_SIZE); +} + +void EInkDisplay::swapBuffers() { + uint8_t* temp = frameBuffer; + frameBuffer = frameBufferActive; + frameBufferActive = temp; +} + +void EInkDisplay::grayscaleRevert() { + inGrayscaleMode = false; + + // Load the revert LUT + setCustomLUT(true, lut_grayscale_revert); + refreshDisplay(FAST_REFRESH, false); + 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); +} + +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) { + inGrayscaleMode = false; + grayscaleRevert(); + } + + // 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); + writeRamBuffer(CMD_WRITE_RAM_RED, frameBufferActive, BUFFER_SIZE); + } + + // swap active buffer for next time + swapBuffers(); + + // Refresh the display + refreshDisplay(mode, false); +} + +void EInkDisplay::displayGrayBuffer(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(RefreshMode mode, 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(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() { + // 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 +}