#include "display/eink_display_handler.h" #include "display/constants.h" #include "common/constants.h" #include "esp_lcd_touch_gt911.h" #include "esp_log.h" #include #include #include "common/semaphore_guard.h" #define TAG "EInkDisplayHandler" #define DISPLAY_BUFFER_SIZE (EINK_HEIGHT* EINK_WIDTH) / 8 // 1 bit per pixels #define MINIMUM_PIN_SETUP_DELAY_MS 10 #define MINIMUM_POWER_ON_DELAY_MS 100 #define PARTIAL_REFRESH_THRESHOLD 5 // Full refresh every N partial refreshes // Static flag to prevent multiple instances (these buffers are large, only one display allowed) static bool display_instance_exists = false; EInkDisplayHandler::EInkDisplayHandler() { if (display_instance_exists) { ESP_LOGE(TAG, "Only one EInkDisplayHandler instance allowed!"); return; } display_instance_exists = true; black_data_ = static_cast(heap_caps_malloc(DISPLAY_BUFFER_SIZE, MALLOC_CAP_SPIRAM)); white_data_ = static_cast(heap_caps_malloc(DISPLAY_BUFFER_SIZE, MALLOC_CAP_SPIRAM)); draw_buffer_ = static_cast(heap_caps_malloc(DISPLAY_BUFFER_SIZE, MALLOC_CAP_SPIRAM)); old_buffer_ = static_cast(heap_caps_malloc(DISPLAY_BUFFER_SIZE, MALLOC_CAP_SPIRAM)); // Check for allocation failures if (!black_data_ || !white_data_ || !draw_buffer_ || !old_buffer_) { ESP_LOGE(TAG, "Failed to allocate display buffers!"); return; } memset(black_data_, 0xFF, DISPLAY_BUFFER_SIZE); // eink uses 1 for black memset(white_data_, 0x00, DISPLAY_BUFFER_SIZE); memset(draw_buffer_, 0x00, DISPLAY_BUFFER_SIZE); // start with all white (0 = white in e-ink) memset(old_buffer_, 0x00, DISPLAY_BUFFER_SIZE); // start with all white (0 = white in e-ink) refresh_mutex_ = xSemaphoreCreateMutex(); if (refresh_mutex_ == nullptr) { ESP_LOGE(TAG, "Failed to create refresh mutex"); } } EInkDisplayHandler::~EInkDisplayHandler() { if (refresh_mutex_ != nullptr) { vSemaphoreDelete(refresh_mutex_); } if (tp_handle_ != nullptr) { esp_lcd_touch_del(tp_handle_); } if (tp_io_handle_ != nullptr) { esp_lcd_panel_io_del(tp_io_handle_); } if (black_data_ != nullptr) { heap_caps_free(black_data_); black_data_ = nullptr; } if (white_data_ != nullptr) { heap_caps_free(white_data_); white_data_ = nullptr; } if (draw_buffer_ != nullptr) { heap_caps_free(draw_buffer_); draw_buffer_ = nullptr; } if (old_buffer_ != nullptr) { heap_caps_free(old_buffer_); old_buffer_ = nullptr; } display_instance_exists = false; } esp_err_t EInkDisplayHandler::deep_sleep_display(void) { ESP_LOGV(TAG, "Putting display into deep sleep mode..."); if (is_deep_sleep_) { ESP_LOGW(TAG, "Display is already in deep sleep mode"); return ESP_OK; } { esp_err_t err = ESP_OK; TransactionGuard transaction_guard(this->epd_handler_); err = transaction_guard.begin(pdMS_TO_TICKS(5000)); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to begin transaction for deep sleep: %s", esp_err_to_name(err)); return err; } epd_handler_.wait_for_idle(); err = epd_handler_.epd_write_cmd(0x02, transaction_guard.transaction_id()); // power off if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send power off command: %s", esp_err_to_name(err)); return err; } epd_handler_.wait_for_idle(); err = epd_handler_.epd_write_cmd(0x07, transaction_guard.transaction_id()); //deep sleep if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send deep sleep command: %s", esp_err_to_name(err)); return err; } err = epd_handler_.epd_write_data(0xA5, transaction_guard.transaction_id()); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send deep sleep data: %s", esp_err_to_name(err)); return err; } is_deep_sleep_ = true; return err; } } esp_err_t EInkDisplayHandler::refresh_display() { esp_err_t err = ESP_OK; if (is_deep_sleep_) { err = full_write(draw_buffer_, true); if (err != ESP_OK) { ESP_LOGE(TAG, "Full write failed during refresh_display: %s", esp_err_to_name(err)); return err; } } else { // refresh does not correctly work after recovering from deep sleep due to sram reset { ESP_LOGV(TAG, "Waiting for display to be idle..."); TransactionGuard transaction_guard(this->epd_handler_); err = transaction_guard.begin(pdMS_TO_TICKS(10000)); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to begin transaction for display refresh: %s", esp_err_to_name(err)); return err; } if (is_deep_sleep_) { epd_init_internal_(transaction_guard.transaction_id()); } epd_handler_.wait_for_idle(); ESP_LOGV(TAG, "Starting display refresh..."); err = epd_handler_.epd_write_cmd(0x92, transaction_guard.transaction_id()); // enter normal mode if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to enter normal mode: %s", esp_err_to_name(err)); return err; } err = epd_handler_.epd_write_cmd(0x12, transaction_guard.transaction_id()); // display refresh if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send display refresh command: %s", esp_err_to_name(err)); return err; } vTaskDelay(pdMS_TO_TICKS(MINIMUM_PIN_SETUP_DELAY_MS)); // at least 200us delay epd_handler_.wait_for_idle(); } } { SemaphoreGuard guard(refresh_mutex_); if (guard.take(pdMS_TO_TICKS(5000)) != pdTRUE) { ESP_LOGE(TAG, "Refresh mutex timeout in refresh_display"); return ESP_ERR_TIMEOUT; } partial_refresh_count_ = 0; force_full_refresh_ = false; } ESP_LOGV(TAG, "Refresh complete"); return ESP_OK; } esp_err_t EInkDisplayHandler::full_write(const uint8_t* framebuffer, const bool white_basemap) { ESP_LOGV(TAG, "Starting full refresh (3 seconds)..."); esp_err_t err = ESP_OK; { TransactionGuard transaction_guard(this->epd_handler_); err = transaction_guard.begin(pdMS_TO_TICKS(10000)); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to begin transaction for full refresh: %s", esp_err_to_name(err)); return err; } if (is_deep_sleep_) { epd_init_internal_(transaction_guard.transaction_id()); } write_to_buffer_(framebuffer, RefreshArea { 0, 0, DISPLAY_WIDTH - 1, DISPLAY_HEIGHT - 1 }); epd_handler_.wait_for_idle(); // Step 0: Enter normal mode err = epd_handler_.epd_write_cmd(0x92, transaction_guard.transaction_id()); // enter normal mode if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to enter normal mode: %s", esp_err_to_name(err)); return err; } // Step 1: Write old data (0x10) - Arduino uses 0xFF (all white) for base map { err = epd_handler_.epd_write_cmd(0x10, transaction_guard.transaction_id()); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send old data command: %s", esp_err_to_name(err)); return err; } err = epd_handler_.transfer_spi_data(white_basemap ? black_data_ : white_data_, DISPLAY_BUFFER_SIZE, transaction_guard.transaction_id()); // Send all white data (0xFF) if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send all white data for old data: %s", esp_err_to_name(err)); return err; } } // Step 2: Write new data (0x13) { err = epd_handler_.epd_write_cmd(0x13, transaction_guard.transaction_id()); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send new data command: %s", esp_err_to_name(err)); return err; } err = epd_handler_.transfer_spi_data(draw_buffer_, DISPLAY_BUFFER_SIZE, transaction_guard.transaction_id()); // Send new framebuffer data if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send framebuffer data for new data: %s", esp_err_to_name(err)); return err; } } // Step 3: Trigger display refresh (DRF) err = epd_handler_.epd_write_cmd(0x12, transaction_guard.transaction_id()); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send display refresh command: %s", esp_err_to_name(err)); return err; } vTaskDelay(pdMS_TO_TICKS(MINIMUM_PIN_SETUP_DELAY_MS)); // at least 200us delay ESP_LOGV(TAG, "Display refresh triggered, BUSY pin: %d", gpio_get_level(PIN_BUSY)); // Wait for refresh to complete epd_handler_.wait_for_idle(); } err = deep_sleep_display(); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to enter deep sleep after full refresh: %s", esp_err_to_name(err)); return err; } refresh_area_.reset(); memcpy(old_buffer_, draw_buffer_, DISPLAY_BUFFER_SIZE); ESP_LOGV(TAG, "Full refresh complete"); return ESP_OK; } // TODO: Partial refresh is inverted in color esp_err_t EInkDisplayHandler::partial_refresh(const uint8_t* incoming_partial_framebuffer, const RefreshArea& incoming_area, const bool is_last_partial_update) { ESP_LOGV(TAG, "Starting partial refresh (0.3 seconds)..."); esp_err_t err = ESP_OK; write_to_buffer_(incoming_partial_framebuffer, incoming_area); // Always expand refresh_area_ to include incoming_area refresh_area_.expand_to_include(incoming_area); if (!is_last_partial_update) { ESP_LOGV(TAG, "Partial refresh skipped (not last partial update)"); return ESP_OK; } { TransactionGuard transaction_guard(this->epd_handler_); err = transaction_guard.begin(pdMS_TO_TICKS(5000)); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to begin transaction for partial refresh: %s", esp_err_to_name(err)); return err; } // Wake display from deep sleep INSIDE the transaction to prevent race conditions if (is_deep_sleep_) { err = epd_init_internal_(transaction_guard.transaction_id()); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to initialize EPD for partial refresh: %s", esp_err_to_name(err)); return err; } err = refresh_old_buffer_(transaction_guard.transaction_id()); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to refresh old buffer during partial refresh init: %s", esp_err_to_name(err)); return err; } } RefreshArea area = refresh_area_; if (area.x1 % 8 != 0 || area.x2 % 8 != 7) { ESP_LOGE(TAG, "Partial refresh area x1 and x2 must be byte-aligned (x1 %% 8 == 0 and x2 %% 8 == 7)"); ESP_LOGV(TAG, "Given area: x1=%d, x2=%d", area.x1, area.x2); return ESP_ERR_INVALID_ARG; } // Calculate partial buffer size based on the refresh area const uint32_t area_width_bytes = (area.x2 - area.x1 + 1) / 8; const uint32_t area_height = area.y2 - area.y1 + 1; const size_t partial_buffer_size = area_width_bytes * area_height; // uint8_t* partial_buffer = new uint8_t[partial_buffer_size]; uint8_t* partial_buffer = static_cast(heap_caps_malloc(partial_buffer_size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL)); if (partial_buffer == nullptr) { ESP_LOGE(TAG, "Failed to allocate partial buffer for partial refresh"); return ESP_ERR_NO_MEM; } // Copy the relevant area from draw_buffer_ to partial_buffer for (int32_t row = 0; row < area_height; ++row) { uint32_t fb_y = area.y1 + row; uint32_t fb_x_byte_start = area.x1 / 8; uint8_t* fb_ptr = &draw_buffer_[fb_y * (DISPLAY_WIDTH / 8) + fb_x_byte_start]; uint8_t* dest_ptr = &partial_buffer[row * area_width_bytes]; memcpy(dest_ptr, fb_ptr, area_width_bytes); } epd_handler_.wait_for_idle(); // Step 1 VCOM setting std::vector vcom_data = { 0xA9, 0x07 }; err = epd_handler_.epd_write_cmd_with_data(0x50, vcom_data, transaction_guard.transaction_id()); // VCOM for partial refresh if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to set VCOM for partial refresh: %s", esp_err_to_name(err)); return err; } // Step 2: Enter partial refresh mode err = epd_handler_.epd_write_cmd(0x91, transaction_guard.transaction_id()); // Enter partial mode if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to enter partial refresh mode: %s", esp_err_to_name(err)); return err; } // Step 3: Set partial window { // ------DD // DDDDD000 // ------DD // DDDDD111 // ------DD // DDDDDDDD // ------DD // DDDDDDDD // -------D // area should be multiple of 8 in x direction const int32_t x_bank_start = area.x1 >> 3; const int32_t x_bank_end = area.x2 >> 3; std::vector window_data = { // x start, [9:8] bit -> 6 and 7 bits of x_bank_start static_cast((x_bank_start >> 5) & 0x03), // x start, [7:3] bit + 3 bits of 0 -> 5 bits of x_bank_start and pad 3 LSBs as 0 static_cast((x_bank_start & 0x1F) << 3), // x end, [9:8] bit static_cast((x_bank_end >> 5) & 0x03), // x end, [7:3] bit + 3 bits of 1 static_cast(((x_bank_end & 0x1F) << 3) | 0x07), // y start, [9:8] bit static_cast((area.y1 >> 8) & 0x03), // y start, [7:0] bit static_cast(area.y1 & 0xFF), // y end, [9:8] bit static_cast((area.y2 >> 8) & 0x03), // y end, [7:0] bit static_cast(area.y2 & 0xFF), 0x01 // Gates scan both inside and outside of the partial window }; ESP_LOGV(TAG, "Setting partial window: x1=%d, y1=%d, x2=%d, y2=%d", area.x1, area.y1, area.x2, area.y2); ESP_LOGV(TAG, "Partial window data: %02X %02X %02X %02X %02X %02X %02X %02X", window_data[0], window_data[1], window_data[2], window_data[3], window_data[4], window_data[5], window_data[6], window_data[7]); err = epd_handler_.epd_write_cmd_with_data(0x90, window_data, transaction_guard.transaction_id()); // Set partial window if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send set partial window command: %s", esp_err_to_name(err)); return err; } } // Step 5: Write new data (0x13) { err = epd_handler_.epd_write_cmd(0x13, transaction_guard.transaction_id()); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send new data command for partial refresh: %s", esp_err_to_name(err)); heap_caps_free(partial_buffer); return err; } // Send only the partial area data, not the full display buffer ESP_LOGV(TAG, "Sending new partial buffer: %zu bytes (area: %dx%d)", partial_buffer_size, area_width_bytes * 8, area_height); err = epd_handler_.transfer_spi_data(partial_buffer, partial_buffer_size, transaction_guard.transaction_id(), true); // Inverted for partial refresh if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send partial_buffer data for partial refresh: %s", esp_err_to_name(err)); heap_caps_free(partial_buffer); return err; } memcpy(old_buffer_, draw_buffer_, DISPLAY_BUFFER_SIZE); } // Clean up partial buffer heap_caps_free(partial_buffer); // Step 6: Trigger partial display refresh (DRF) // Use 0x12 (Display Update) command - same as full refresh, per sample code err = epd_handler_.epd_write_cmd(0x12, transaction_guard.transaction_id()); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send display refresh command for partial refresh: %s", esp_err_to_name(err)); return err; } vTaskDelay(pdMS_TO_TICKS(MINIMUM_PIN_SETUP_DELAY_MS)); // at least 200us delay epd_handler_.wait_for_idle(); // Step 7: Exit partial mode err = epd_handler_.epd_write_cmd(0x92, transaction_guard.transaction_id()); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to exit partial refresh mode: %s", esp_err_to_name(err)); return err; } } ESP_LOGV(TAG, "Partial refresh complete"); err = deep_sleep_display(); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to enter deep sleep after partial refresh: %s", esp_err_to_name(err)); return err; } if (force_full_refresh_) { ESP_LOGV(TAG, "Full refresh already requested, skipping partial refresh count increment"); err = refresh_display(); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to perform forced full refresh: %s", esp_err_to_name(err)); return err; } return ESP_OK; } { SemaphoreGuard guard(refresh_mutex_); if (guard.take(pdMS_TO_TICKS(5000)) != pdTRUE) { ESP_LOGE(TAG, "Refresh mutex timeout in partial_refresh"); return ESP_ERR_TIMEOUT; } if (partial_refresh_count_ < UINT32_MAX) { partial_refresh_count_++; } if (partial_refresh_count_ >= PARTIAL_REFRESH_THRESHOLD) { ESP_LOGV(TAG, "Partial refresh count %u reached threshold %u, next refresh will be full", partial_refresh_count_, PARTIAL_REFRESH_THRESHOLD); force_full_refresh_ = true; partial_refresh_count_ = 0; } } refresh_area_.reset(); return ESP_OK; } esp_err_t EInkDisplayHandler::clear_display(void) { ESP_LOGV(TAG, "Clearing display to all white..."); esp_err_t err = full_write(white_data_, false); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to clear display: %s", esp_err_to_name(err)); return err; } ESP_LOGV(TAG, "Display cleared to all white"); return ESP_OK; } void EInkDisplayHandler::write_to_buffer_(const uint8_t* src_buffer, const RefreshArea& area) { // Copy the relevant area from src_buffer to draw_buffer_ const uint32_t area_width_bytes = (area.x2 - area.x1 + 1) / 8; const uint32_t area_height = area.y2 - area.y1 + 1; for (int32_t row = 0; row < area_height; ++row) { uint32_t fb_y = area.y1 + row; uint32_t fb_x_byte_start = area.x1 / 8; const uint8_t* src_ptr = &src_buffer[row * area_width_bytes]; uint8_t* dest_ptr = &draw_buffer_[fb_y * (DISPLAY_WIDTH / 8) + fb_x_byte_start]; memcpy(dest_ptr, src_ptr, area_width_bytes); } } // Request a full refresh on next flush void EInkDisplayHandler::request_full_refresh(void) { SemaphoreGuard guard(refresh_mutex_); if (guard.take(pdMS_TO_TICKS(100))) { force_full_refresh_ = true; partial_refresh_count_ = 0; ESP_LOGV(TAG, "Full refresh requested"); } else { ESP_LOGE(TAG, "Failed to take refresh mutex to request full refresh"); } } esp_err_t EInkDisplayHandler::init_devices(EventGroupHandle_t system_event_group) { esp_err_t err; err = init_display_pins_(); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to initialize display pins: %s", esp_err_to_name(err)); return err; } err = this->epd_handler_.init(); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to initialize EPD handler: %s", esp_err_to_name(err)); return err; } err = init_touch_(); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to initialize touch: %s", esp_err_to_name(err)); return err; } // if system_event_group is provided, set display ready bits if (system_event_group != nullptr) { // Indicate that display is ready xEventGroupSetBits(system_event_group, DISPLAY_READY_BIT | TOUCH_CALIBRATED_BIT); ESP_LOGV(TAG, "Display marked as ready"); } return ESP_OK; } esp_err_t EInkDisplayHandler::init_display_pins_(void) { ESP_LOGV(TAG, "Initializing E-Ink display handler..."); esp_err_t ret; // Initialize GPIO pins gpio_config_t io_conf = {}; io_conf.pin_bit_mask = (1ULL << PIN_DC) | (1ULL << PIN_RST); io_conf.mode = GPIO_MODE_OUTPUT; io_conf.pull_up_en = GPIO_PULLUP_DISABLE; io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; io_conf.intr_type = GPIO_INTR_DISABLE; ret = gpio_config(&io_conf); if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to configure GPIO pins: %s", esp_err_to_name(ret)); return ret; } // Configure BUSY pin as input (no pull-up like sample code) io_conf.pin_bit_mask = (1ULL << PIN_BUSY); io_conf.mode = GPIO_MODE_INPUT; io_conf.pull_up_en = GPIO_PULLUP_DISABLE; io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; ret = gpio_config(&io_conf); if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to configure BUSY pin: %s", esp_err_to_name(ret)); return ret; } return ESP_OK; } esp_err_t EInkDisplayHandler::epd_init_internal_(uint32_t transaction_id) { ESP_LOGV(TAG, "Initializing EPD..."); esp_err_t err; // 1. Hardware Reset err = gpio_set_level(PIN_RST, 0); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to set PIN_RST low: %s", esp_err_to_name(err)); return err; } vTaskDelay(pdMS_TO_TICKS(MINIMUM_PIN_SETUP_DELAY_MS)); err = gpio_set_level(PIN_RST, 1); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to set PIN_RST high: %s", esp_err_to_name(err)); return err; } vTaskDelay(pdMS_TO_TICKS(MINIMUM_PIN_SETUP_DELAY_MS)); // 2. Initialization Sequence std::vector panel_setting_data = { 0x1F }; err = epd_handler_.epd_write_cmd_with_data(0x00, panel_setting_data, transaction_id); // Panel Setting if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send Panel Setting command: %s", esp_err_to_name(err)); return err; } vTaskDelay(pdMS_TO_TICKS(MINIMUM_PIN_SETUP_DELAY_MS)); std::vector vcom_data = { 0x10, 0x07 }; err = epd_handler_.epd_write_cmd_with_data(0x50, vcom_data, transaction_id); // VCOM if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send VCOM command: %s", esp_err_to_name(err)); return err; } vTaskDelay(pdMS_TO_TICKS(MINIMUM_PIN_SETUP_DELAY_MS)); err = epd_handler_.epd_write_cmd(0x04, transaction_id); // Power ON if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send Power ON command: %s", esp_err_to_name(err)); return err; } vTaskDelay(pdMS_TO_TICKS(MINIMUM_POWER_ON_DELAY_MS)); // Wait for power on // Check BUSY pin with detailed logging ESP_LOGV(TAG, "Waiting for EPD to be ready after power on..."); ESP_LOGV(TAG, "BUSY pin level after power on: %d (0=BUSY, 1=FREE)", gpio_get_level(PIN_BUSY)); epd_handler_.wait_for_idle(); std::vector booster_data = { 0x27, 0x27, 0x18, 0x17 }; err = epd_handler_.epd_write_cmd_with_data(0x06, booster_data, transaction_id); // Booster Soft Start if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send Booster Soft Start command: %s", esp_err_to_name(err)); return err; } vTaskDelay(pdMS_TO_TICKS(MINIMUM_PIN_SETUP_DELAY_MS)); // Enhanced display drive commands std::vector e0_data = { 0x02 }; err = epd_handler_.epd_write_cmd_with_data(0xE0, e0_data, transaction_id); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send Enhanced Display Drive command: %s", esp_err_to_name(err)); return err; } std::vector e5_data = { 0x5A }; err = epd_handler_.epd_write_cmd_with_data(0xE5, e5_data, transaction_id); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send Enhanced Display Drive command: %s", esp_err_to_name(err)); return err; } is_deep_sleep_ = false; return err; } // Internal version that uses an existing transaction (no separate TransactionGuard) esp_err_t EInkDisplayHandler::epd_init_partial_internal_(uint32_t transaction_id) { ESP_LOGV(TAG, "Initializing EPD for partial refresh (internal)..."); esp_err_t err = ESP_OK; // 1. Hardware Reset err = gpio_set_level(PIN_RST, 0); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to set PIN_RST low: %s", esp_err_to_name(err)); return err; } vTaskDelay(pdMS_TO_TICKS(MINIMUM_PIN_SETUP_DELAY_MS)); err = gpio_set_level(PIN_RST, 1); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to set PIN_RST high: %s", esp_err_to_name(err)); return err; } vTaskDelay(pdMS_TO_TICKS(MINIMUM_PIN_SETUP_DELAY_MS)); // 2. Panel Setting std::vector panel_setting_data = { 0x1F }; err = epd_handler_.epd_write_cmd_with_data(0x00, panel_setting_data, transaction_id); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send Panel Setting command: %s", esp_err_to_name(err)); return err; } vTaskDelay(pdMS_TO_TICKS(MINIMUM_PIN_SETUP_DELAY_MS)); // 3. Power ON err = epd_handler_.epd_write_cmd(0x04, transaction_id); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send Power ON command: %s", esp_err_to_name(err)); return err; } vTaskDelay(pdMS_TO_TICKS(MINIMUM_POWER_ON_DELAY_MS)); epd_handler_.wait_for_idle(); // 4. Partial initialization sequence - Enhanced Display Drive std::vector e0_data = { 0x02 }; err = epd_handler_.epd_write_cmd_with_data(0xE0, e0_data, transaction_id); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send Enhanced Display Drive command (E0): %s", esp_err_to_name(err)); return err; } std::vector e5_data = { 0x6E }; err = epd_handler_.epd_write_cmd_with_data(0xE5, e5_data, transaction_id); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send Enhanced Display Drive command (E5): %s", esp_err_to_name(err)); return err; } is_deep_sleep_ = false; err = refresh_old_buffer_(transaction_id); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to refresh old buffer during partial init: %s", esp_err_to_name(err)); return err; } ESP_LOGV(TAG, "EPD partial init (internal) complete"); return ESP_OK; } esp_err_t EInkDisplayHandler::init_touch_() { ESP_LOGV(TAG, "Initializing touch..."); esp_err_t err; // 1. Initialize I2C Bus i2c_config_t conf = {}; conf.mode = I2C_MODE_MASTER; conf.sda_io_num = PIN_TOUCH_SDA; conf.scl_io_num = PIN_TOUCH_SCL; conf.sda_pullup_en = GPIO_PULLUP_ENABLE; conf.scl_pullup_en = GPIO_PULLUP_ENABLE; conf.master.clk_speed = 400000; err = i2c_param_config(I2C_NUM_0, &conf); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to configure I2C parameters: %s", esp_err_to_name(err)); return err; } err = i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to install I2C driver: %s", esp_err_to_name(err)); return err; } ESP_LOGV("DisplayHandler", "I2C driver installed"); // 2. Initialize GT911 ESP_LOGV("DisplayHandler", "Initializing GT911 touch controller..."); esp_lcd_panel_io_i2c_config_t tp_io_config = {}; // temporarily disable -Wmissing-field-initializers, as ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG macro does not set all fields #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wmissing-field-initializers" esp_lcd_panel_io_i2c_config_t default_tp_io_config = ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG(); #pragma GCC diagnostic pop tp_io_config.dev_addr = default_tp_io_config.dev_addr; tp_io_config.control_phase_bytes = default_tp_io_config.control_phase_bytes; tp_io_config.dc_bit_offset = default_tp_io_config.dc_bit_offset; tp_io_config.lcd_cmd_bits = default_tp_io_config.lcd_cmd_bits; tp_io_config.flags = default_tp_io_config.flags; esp_lcd_new_panel_io_i2c(I2C_NUM_0, &tp_io_config, &tp_io_handle_); // GT911-specific config with I2C address (0x5D = INT low during reset) static esp_lcd_touch_io_gt911_config_t gt911_config = { .dev_addr = ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS // 0x5D }; esp_lcd_touch_config_t tp_cfg = {}; tp_cfg.x_max = DISPLAY_WIDTH; tp_cfg.y_max = DISPLAY_HEIGHT; tp_cfg.rst_gpio_num = PIN_TOUCH_RST; tp_cfg.int_gpio_num = PIN_TOUCH_IRQ; tp_cfg.driver_data = >911_config; // Pass GT911-specific config for automatic reset err = esp_lcd_touch_new_i2c_gt911(tp_io_handle_, &tp_cfg, &tp_handle_); if (err == ESP_OK && tp_handle_ != nullptr) { ESP_LOGV(TAG, "GT911 touch controller initialized successfully"); } else { ESP_LOGE(TAG, "GT911 touch controller initialization failed: %s", esp_err_to_name(err)); tp_handle_ = nullptr; } return err; } esp_err_t EInkDisplayHandler::refresh_old_buffer_(uint32_t transaction_id) { ESP_LOGV(TAG, "Refreshing display SRAM to restore state after wake..."); esp_err_t err; err = epd_handler_.epd_write_cmd(0x92, transaction_id); // enter normal mode if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to enter normal mode: %s", esp_err_to_name(err)); return err; } // Write OLD data (0x10) as all 0x00 (white in e-ink terms) // This tells the controller: "assume display was all white" // Matches sample's EPD_WhiteScreen_ALL() which uses 0x00 for old SRAM // The differential refresh: old=0 + new=0 → stay white, old=0 + new=1 → drive to black err = epd_handler_.epd_write_cmd(0x10, transaction_id); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send old data command: %s", esp_err_to_name(err)); return err; } // Send the old buffer as old data err = epd_handler_.transfer_spi_data(old_buffer_, DISPLAY_BUFFER_SIZE, transaction_id); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send white baseline to old SRAM: %s", esp_err_to_name(err)); return err; } // Write NEW data (0x13) with the actual display content // This restores the display to show old_buffer_ content err = epd_handler_.epd_write_cmd(0x13, transaction_id); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send new data command: %s", esp_err_to_name(err)); return err; } // Send the last displayed content to new SRAM err = epd_handler_.transfer_spi_data(old_buffer_, DISPLAY_BUFFER_SIZE, transaction_id); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send display content to new SRAM: %s", esp_err_to_name(err)); return err; } ESP_LOGV(TAG, "Display SRAM restored successfully"); return ESP_OK; }