#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 BUSY_ACTIVE_LEVEL 0 // BUSY pin is active low #define BUSY_INACTIVE_LEVEL 1 #define DMA_TRANSFER_CHUNK_SIZE 4096 // 4KB chunk size for DMA transfers static uint8_t white_data[DISPLAY_BUFFER_SIZE] = { 0xFF }; // all white data EInkDisplayHandler::EInkDisplayHandler() { spi_mutex_ = xSemaphoreCreateMutex(); if (spi_mutex_ == nullptr) { ESP_LOGE(TAG, "Failed to create SPI mutex"); } spi_transaction_mutex_ = xSemaphoreCreateMutex(); if (spi_transaction_mutex_ == nullptr) { ESP_LOGE(TAG, "Failed to create SPI transaction mutex"); } refresh_mutex_ = xSemaphoreCreateMutex(); if (refresh_mutex_ == nullptr) { ESP_LOGE(TAG, "Failed to create refresh mutex"); } } EInkDisplayHandler::~EInkDisplayHandler() { if (spi_mutex_ != nullptr) { vSemaphoreDelete(spi_mutex_); } if (spi_transaction_mutex_ != nullptr) { vSemaphoreDelete(spi_transaction_mutex_); } if (refresh_mutex_ != nullptr) { vSemaphoreDelete(refresh_mutex_); } if (spi_ != nullptr) { spi_bus_remove_device(spi_); } if (tp_handle_ != nullptr) { esp_lcd_touch_del(tp_handle_); } if (tp_io_handle_ != nullptr) { esp_lcd_panel_io_del(tp_io_handle_); } } esp_err_t EInkDisplayHandler::refresh_display() { esp_err_t err = ESP_OK; ESP_LOGI(TAG, "Waiting for display to be idle..."); { TransactionGuard transaction_guard(*this); 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; } wait_for_idle(); ESP_LOGI(TAG, "Starting display refresh..."); err = 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_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 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_LOGI(TAG, "Refresh complete"); return ESP_OK; } esp_err_t EInkDisplayHandler::full_write(const uint8_t* framebuffer) { ESP_LOGI(TAG, "Starting full refresh (3 seconds)..."); esp_err_t err = ESP_OK; { TransactionGuard transaction_guard(*this); 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; } wait_for_idle(); // Step 1: Write old data (0x10) - Arduino uses 0xFF (all white) for base map { err = 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 = transfer_spi_data(white_data, DISPLAY_BUFFER_SIZE, transaction_guard.transaction_id()); // Send all white data 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_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 = transfer_spi_data(framebuffer, 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_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_LOGI(TAG, "Display refresh triggered, BUSY pin: %d", gpio_get_level(PIN_BUSY)); // Wait for refresh to complete wait_for_idle(); } ESP_LOGI(TAG, "Full refresh complete"); return ESP_OK; } esp_err_t EInkDisplayHandler::partial_refresh(const uint8_t* partial_framebuffer, const RefreshArea& area) { ESP_LOGI(TAG, "Starting partial refresh (0.3 seconds)..."); esp_err_t err = ESP_OK; { TransactionGuard transaction_guard(*this); 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; } wait_for_idle(); // Step 1 VCOM setting std::vector vcom_data = { 0xA9, 0x07 }; err = 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_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 { 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)"); return ESP_ERR_INVALID_ARG; } // no rounding needed, area is expected to be aligned std::vector window_data = { // x start static_cast((area.x1 >> 8) & 0xFF), // x start high byte static_cast(area.x1 & 0x07), // x start low byte // x end static_cast((area.x2 >> 8) & 0xFF), static_cast(area.x2 & 0x07), // y start static_cast((area.y1 >> 8) & 0xFF), static_cast(area.y1 & 0xFF), // y end static_cast((area.y2 >> 8) & 0xFF), static_cast(area.y2 & 0xFF), 0x01 // Gates scan both inside and outside of the partial window }; err = 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 4: Write new data (0x13) { err = 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)); return err; } err = transfer_spi_data(partial_framebuffer, DISPLAY_BUFFER_SIZE, transaction_guard.transaction_id()); // Send new framebuffer data if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send partial_framebuffer data for partial refresh: %s", esp_err_to_name(err)); return err; } } // Step 5: Trigger partial display refresh (DRF) err = 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 wait_for_idle(); // Step 6: Exit partial mode err = 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_LOGI(TAG, "Partial refresh complete"); if (force_full_refresh_) { ESP_LOGI(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_LOGI(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; } } return ESP_OK; } esp_err_t EInkDisplayHandler::clear_display(void) { ESP_LOGI(TAG, "Clearing display to all white..."); esp_err_t err = full_write(white_data); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to clear display: %s", esp_err_to_name(err)); return err; } ESP_LOGI(TAG, "Display cleared to all white"); return ESP_OK; } // 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_LOGI(TAG, "Full refresh requested"); } else { ESP_LOGE(TAG, "Failed to take refresh mutex to request full refresh"); } } // Check if display is busy (refreshing) bool EInkDisplayHandler::is_busy(void) const { return gpio_get_level(PIN_BUSY) == BUSY_ACTIVE_LEVEL; // BUSY is active LOW } void EInkDisplayHandler::wait_for_idle(void) const { ESP_LOGI(TAG, "Waiting for display ready (BUSY pin)..."); int initial_level = gpio_get_level(PIN_BUSY); ESP_LOGI(TAG, "Initial BUSY pin level: %d (0=BUSY, 1=FREE)", initial_level); // If already free, no need to wait if (initial_level == BUSY_INACTIVE_LEVEL) { ESP_LOGI(TAG, "Display already ready (BUSY pin = 1)"); return; } while (gpio_get_level(PIN_BUSY) != BUSY_INACTIVE_LEVEL) { vTaskDelay(pdMS_TO_TICKS(10)); } ESP_LOGI(TAG, "Display is now ready (BUSY pin = 1)"); } 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 = epd_init_(); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to initialize EPD: %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_LOGI(TAG, "Display marked as ready"); } return ESP_OK; } esp_err_t EInkDisplayHandler::init_display_pins_(void) { ESP_LOGI(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; } // Initialize SPI bus spi_bus_config_t buscfg = {}; buscfg.mosi_io_num = 11; // MOSI pin buscfg.miso_io_num = -1; // No MISO for e-paper buscfg.sclk_io_num = 12; // SCK pin buscfg.quadwp_io_num = -1; buscfg.quadhd_io_num = -1; buscfg.max_transfer_sz = DISPLAY_BUFFER_SIZE; ret = spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO); if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to initialize SPI bus: %s", esp_err_to_name(ret)); return ret; } // Add SPI device spi_device_interface_config_t devcfg = {}; devcfg.clock_speed_hz = 10 * 1000 * 1000; // 10 MHz devcfg.mode = 0; // SPI mode 0 devcfg.spics_io_num = PIN_CS; devcfg.queue_size = 7; // Queue size for non-blocking transactions devcfg.pre_cb = nullptr; ret = spi_bus_add_device(SPI2_HOST, &devcfg, &spi_); if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to add SPI device: %s", esp_err_to_name(ret)); return ret; } return ESP_OK; } // required to be called by inheriting class after SPI device is created esp_err_t EInkDisplayHandler::epd_init_(void) { ESP_LOGI(TAG, "Initializing EPD..."); esp_err_t err; { TransactionGuard transaction_guard(*this); esp_err_t begin_err = transaction_guard.begin(); if (begin_err != ESP_OK) { ESP_LOGE(TAG, "Failed to begin transaction: %s", esp_err_to_name(begin_err)); return begin_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_write_cmd_with_data(0x00, panel_setting_data, transaction_guard.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_write_cmd_with_data(0x50, vcom_data, transaction_guard.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_write_cmd(0x04, transaction_guard.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_LOGI(TAG, "Waiting for EPD to be ready after power on..."); ESP_LOGI(TAG, "BUSY pin level after power on: %d (0=BUSY, 1=FREE)", gpio_get_level(PIN_BUSY)); int busy_timeout = 0; while (gpio_get_level(PIN_BUSY) == BUSY_ACTIVE_LEVEL) { // BUSY is active LOW vTaskDelay(pdMS_TO_TICKS(MINIMUM_PIN_SETUP_DELAY_MS)); busy_timeout++; if (busy_timeout > 500) { // 5 second timeout ESP_LOGE(TAG, "EPD power on timeout! BUSY pin stuck at 0"); return ESP_ERR_TIMEOUT; } if (busy_timeout % 50 == 0) { // Log every 500ms ESP_LOGW(TAG, "Still waiting for EPD power on, timeout: %d/500", busy_timeout); } } ESP_LOGI(TAG, "EPD power on complete after %d * 10ms, BUSY pin: %d", busy_timeout, gpio_get_level(PIN_BUSY)); std::vector booster_data = { 0x27, 0x27, 0x18, 0x17 }; err = epd_write_cmd_with_data(0x06, booster_data, transaction_guard.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_write_cmd_with_data(0xE0, e0_data, transaction_guard.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_write_cmd_with_data(0xE5, e5_data, transaction_guard.transaction_id()); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send Enhanced Display Drive command: %s", esp_err_to_name(err)); return err; } } return err; } esp_err_t EInkDisplayHandler::init_touch_() { ESP_LOGI(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_LOGI("DisplayHandler", "I2C driver installed"); // 2. Initialize GT911 ESP_LOGI("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_LOGI("DisplayHandler", "GT911 touch controller initialized successfully"); } else { ESP_LOGE("DisplayHandler", "GT911 touch controller initialization failed: %s", esp_err_to_name(err)); tp_handle_ = nullptr; } return err; } esp_err_t EInkDisplayHandler::epd_write_cmd(const uint8_t cmd, uint32_t transaction_id) { ESP_LOGI(TAG, "epd_write_cmd: waiting to send 0x%02X", cmd); SemaphoreGuard transaction_guard(spi_transaction_mutex_); esp_err_t err = wait_for_transaction_end_(pdMS_TO_TICKS(5000), transaction_id, transaction_guard); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to wait for previous transaction end before sending cmd 0x%02X: %s", cmd, esp_err_to_name(err)); return err; } SemaphoreGuard guard(spi_mutex_); if (!guard.take(pdMS_TO_TICKS(5000))) { ESP_LOGE(TAG, "SPI mutex timeout for cmd 0x%02X", cmd); return ESP_ERR_TIMEOUT; } err = dangerous_epd_write_cmd_without_lock_(cmd); ESP_LOGI(TAG, "epd_write_cmd: 0x%02X done", cmd); return err; } esp_err_t EInkDisplayHandler::epd_write_data(const uint8_t data, uint32_t transaction_id) { ESP_LOGI(TAG, "epd_write_data: waiting to send 0x%02X", data); SemaphoreGuard transaction_guard(spi_transaction_mutex_); esp_err_t err = wait_for_transaction_end_(pdMS_TO_TICKS(5000), transaction_id, transaction_guard); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to wait for previous transaction end before sending data 0x%02X: %s", data, esp_err_to_name(err)); return err; } SemaphoreGuard guard(spi_mutex_); if (!guard.take(pdMS_TO_TICKS(5000))) { ESP_LOGE(TAG, "SPI mutex timeout for data 0x%02X", data); return ESP_ERR_TIMEOUT; } err = dangerous_epd_write_data_without_lock_(data); ESP_LOGI(TAG, "epd_write_data: 0x%02X done", data); return err; } esp_err_t EInkDisplayHandler::epd_write_cmd_with_data(const uint8_t cmd, std::vector& data, uint32_t transaction_id) { const size_t data_len = data.size(); ESP_LOGI(TAG, "epd_write_cmd_with_data: waiting to send cmd 0x%02X with %u bytes of data", cmd, data_len); SemaphoreGuard transaction_guard(spi_transaction_mutex_); esp_err_t err = wait_for_transaction_end_(pdMS_TO_TICKS(5000), transaction_id, transaction_guard); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to wait for previous transaction end before sending cmd 0x%02X: %s, with data", cmd, esp_err_to_name(err)); return err; } SemaphoreGuard guard(spi_mutex_); if (!guard.take(pdMS_TO_TICKS(5000))) { ESP_LOGE(TAG, "SPI mutex timeout for cmd with data 0x%02X", cmd); return ESP_ERR_TIMEOUT; } err = dangerous_epd_write_cmd_without_lock_(cmd); if (err != ESP_OK) { return err; }; for (size_t i = 0; i < data_len; ++i) { err = dangerous_epd_write_data_without_lock_(data[i]); if (err != ESP_OK) { return err; } } ESP_LOGI(TAG, "epd_write_cmd_with_data: cmd 0x%02X with %u bytes of data done", cmd, data_len); return ESP_OK; } esp_err_t EInkDisplayHandler::dangerous_epd_write_cmd_without_lock_(const uint8_t cmd) { ESP_LOGI(TAG, "dangerous_epd_write_cmd_without_lock_: sending 0x%02X", cmd); gpio_set_level(PIN_DC, 0); // Command mode spi_transaction_t t {}; t.length = 8;t.tx_buffer = &cmd; esp_err_t err = spi_device_polling_transmit(spi_, &t); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send data 0x%02X", cmd); } else { ESP_LOGI(TAG, "dangerous_epd_write_cmd_without_lock_: 0x%02X sent", cmd); } return err; } esp_err_t EInkDisplayHandler::dangerous_epd_write_data_without_lock_(const uint8_t data) { ESP_LOGI(TAG, "dangerous_epd_write_data_without_lock_: sending 0x%02X", data); gpio_set_level(PIN_DC, 1); // Data mode spi_transaction_t t = { }; t.length = 8; t.tx_buffer = &data; esp_err_t err = spi_device_polling_transmit(spi_, &t); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to send data 0x%02X", data); } else { ESP_LOGI(TAG, "dangerous_epd_write_data_without_lock_: 0x%02X sent", data); } return err; } esp_err_t EInkDisplayHandler::transfer_spi_data(const uint8_t* data, const size_t& length, uint32_t transaction_id) { ESP_LOGI(TAG, "transfer_spi_data: waiting to send %zu bytes of data", length); SemaphoreGuard transaction_guard(spi_transaction_mutex_); esp_err_t err = wait_for_transaction_end_(pdMS_TO_TICKS(5000), transaction_id, transaction_guard); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to wait for previous transaction end before sending data of %zu bytes: %s", length, esp_err_to_name(err)); return err; } SemaphoreGuard guard(spi_mutex_); if (!guard.take(pdMS_TO_TICKS(5000))) { ESP_LOGE(TAG, "SPI mutex timeout for data transfer of %zu bytes", length); return ESP_ERR_TIMEOUT; } ESP_LOGI(TAG, "transfer_spi_data: starting to send %zu bytes of data", length); size_t offset = 0; size_t remaining = length; gpio_set_level(PIN_DC, 1); // Data mode while (remaining > 0) { size_t transfer_size = (remaining < DMA_TRANSFER_CHUNK_SIZE) ? remaining : DMA_TRANSFER_CHUNK_SIZE; spi_transaction_t t = {}; t.length = transfer_size * 8; // Length in bits t.tx_buffer = data + offset; esp_err_t ret = spi_device_polling_transmit(spi_, &t); if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to send SPI chunk at offset %zu: %s", offset, esp_err_to_name(ret)); return ret; } remaining -= transfer_size; offset += transfer_size; // Yield every 16KB to prevent watchdog timeout if (offset % (16 * 1024) == 0) { ESP_LOGI(TAG, "New data progress: %zu/%zu bytes sent, yielding...", offset, length); vTaskDelay(pdMS_TO_TICKS(1)); } } ESP_LOGI(TAG, "transfer_spi_data: completed sending %zu bytes of data", length); return ESP_OK; } esp_err_t EInkDisplayHandler::begin_transaction_(TickType_t timeout, uint32_t& out_id) { ESP_LOGI(TAG, "begin_transaction_: waiting to obtain transaction mutex"); if (xSemaphoreTake(spi_transaction_mutex_, timeout) != pdTRUE) { ESP_LOGE(TAG, "begin_transaction_: transaction mutex timeout"); return ESP_ERR_TIMEOUT; } out_id = ++spi_transaction_id; ESP_LOGI(TAG, "begin_transaction_: transaction mutex obtained"); return ESP_OK; } esp_err_t EInkDisplayHandler::end_transaction_(void) { ESP_LOGI(TAG, "end_transaction_: releasing transaction mutex"); if (xSemaphoreGive(spi_transaction_mutex_) != pdTRUE) { ESP_LOGE(TAG, "end_transaction_: failed to release transaction mutex"); return ESP_FAIL; } ESP_LOGI(TAG, "end_transaction_: transaction mutex released"); return ESP_OK; } esp_err_t EInkDisplayHandler::wait_for_transaction_end_(TickType_t timeout, uint32_t awaiting_transaction_id, SemaphoreGuard& out_transaction_guard) { // Validate transaction ID if provided if (awaiting_transaction_id != 0 && awaiting_transaction_id != spi_transaction_id) { // Invalid transaction ID ESP_LOGE(TAG, "Invalid transaction ID 0x%08X while waiting, current transaction ID: 0x%08X", awaiting_transaction_id, spi_transaction_id); return ESP_ERR_INVALID_ARG; } SemaphoreGuard transaction_guard(spi_transaction_mutex_); if (awaiting_transaction_id == 0) { // wait for current transaction to complete ESP_LOGV(TAG, "Waiting for current transaction 0x%08X to complete", spi_transaction_id); // take the mutex to ensure no transaction is active if (!transaction_guard.take(timeout)) { ESP_LOGE(TAG, "SPI transaction mutex timeout while waiting for transaction end"); return ESP_ERR_TIMEOUT; } } // awaited_transaction_id is valid and matches current transaction ID or 0 out_transaction_guard = std::move(transaction_guard); return ESP_OK; }