diff --git a/main/common/semaphore_guard.h b/main/common/semaphore_guard.h index f285c60..14aabec 100644 --- a/main/common/semaphore_guard.h +++ b/main/common/semaphore_guard.h @@ -1,14 +1,18 @@ #pragma once #include "freertos/semphr.h" #include "freertos/portmacro.h" +#include "esp_log.h" struct SemaphoreGuard { public: - const SemaphoreHandle_t semaphore; SemaphoreGuard(SemaphoreHandle_t semaphore) : semaphore(semaphore) { } portBASE_TYPE take(TickType_t ticks_to_wait = portMAX_DELAY) { + if (this->semaphore == nullptr) { + ESP_LOGE("SemaphoreGuard", "Attempted to take a null semaphore"); + return pdFALSE; + } portBASE_TYPE result = xSemaphoreTake(this->semaphore, ticks_to_wait); taken = (result == pdTRUE); return result; @@ -20,9 +24,26 @@ public: } } + // allow move semantics + SemaphoreGuard(SemaphoreGuard&& other) noexcept + : semaphore(other.semaphore), taken(other.taken) { + other.taken = false; + } + SemaphoreGuard& operator=(SemaphoreGuard&& other) noexcept { + if (this != &other) { + // move from other + taken = other.taken; + other.taken = false; + semaphore = other.semaphore; + other.semaphore = nullptr; + } + return *this; + } + private: // prevent copying SemaphoreGuard(const SemaphoreGuard&) = delete; SemaphoreGuard& operator=(const SemaphoreGuard&) = delete; + SemaphoreHandle_t semaphore = nullptr; bool taken = false; }; diff --git a/main/display/display.cpp b/main/display/display.cpp.old similarity index 100% rename from main/display/display.cpp rename to main/display/display.cpp.old diff --git a/main/display/display.h b/main/display/display.h.old similarity index 100% rename from main/display/display.h rename to main/display/display.h.old diff --git a/main/display/eink_display_handler.cpp b/main/display/eink_display_handler.cpp index ec53c5c..7e76a84 100644 --- a/main/display/eink_display_handler.cpp +++ b/main/display/eink_display_handler.cpp @@ -1,49 +1,345 @@ #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 "esp_heap_caps.h" -#include "esp_task_wdt.h" -#include +#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 -EInkDisplayHandler::EInkDisplayHandler(EventGroupHandle_t system_event_group) - : DisplayHandler(system_event_group) { - _refresh_mutex = xSemaphoreCreateMutex(); - if (_refresh_mutex == nullptr) { +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 (_touch_task_handle != nullptr) { - vTaskDelete(_touch_task_handle); + if (spi_mutex_ != nullptr) { + vSemaphoreDelete(spi_mutex_); } - if (_lvgl_display != nullptr) { - lv_display_delete(_lvgl_display); - _lvgl_display = nullptr; - if (_lvgl_draw_buf != nullptr) { - lv_draw_buf_destroy(_lvgl_draw_buf); - _lvgl_draw_buf = nullptr; - } + if (spi_transaction_mutex_ != nullptr) { + vSemaphoreDelete(spi_transaction_mutex_); } - if (_lvgl_touch_indev != nullptr) { - lvgl_port_remove_touch(_lvgl_touch_indev); + if (refresh_mutex_ != nullptr) { + vSemaphoreDelete(refresh_mutex_); } - if (_framebuffer != nullptr) { - heap_caps_free(_framebuffer); + if (spi_ != nullptr) { + spi_bus_remove_device(spi_); } - 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_); } } -void EInkDisplayHandler::init() { +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* 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 + { + std::vector window_data = { + // x start + static_cast((area.x1 >> 8) & 0xFF), // x start high byte + static_cast(area.x1 & 0xFF), // x start low byte + // x end + static_cast((area.x2 >> 8) & 0xFF), + static_cast(area.x2 & 0xFF), + // 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(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 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); @@ -51,13 +347,22 @@ void EInkDisplayHandler::init() { io_conf.pull_up_en = GPIO_PULLUP_DISABLE; io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; io_conf.intr_type = GPIO_INTR_DISABLE; - gpio_config(&io_conf); + 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; - gpio_config(&io_conf); + 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 = {}; @@ -68,483 +373,378 @@ void EInkDisplayHandler::init() { buscfg.quadhd_io_num = -1; buscfg.max_transfer_sz = DISPLAY_BUFFER_SIZE; - esp_err_t ret = spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO); + 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; + return ret; } // Add SPI device spi_device_interface_config_t devcfg = {}; - devcfg.clock_speed_hz = 6 * 1000 * 1000; // 6 MHz (reduced for reliability) + 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); + 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; + return ret; } + return ESP_OK; +} - // Initialize base display and touch devices - init_devices(false); // Don't set ready bit yet - // Allocate framebuffer - try PSRAM first, fallback to internal RAM - _framebuffer = (uint8_t*)heap_caps_malloc(DISPLAY_BUFFER_SIZE, MALLOC_CAP_SPIRAM); - if (_framebuffer != nullptr) { - _framebuffer_in_psram = true; - ESP_LOGI(TAG, "Framebuffer allocated in PSRAM (%d bytes)", DISPLAY_BUFFER_SIZE); - } else { - ESP_LOGW(TAG, "PSRAM not available, allocating framebuffer in internal RAM"); - _framebuffer = (uint8_t*)heap_caps_malloc(DISPLAY_BUFFER_SIZE, MALLOC_CAP_INTERNAL); - _framebuffer_in_psram = false; - if (_framebuffer == nullptr) { - ESP_LOGE(TAG, "Failed to allocate framebuffer"); - return; - } - ESP_LOGI(TAG, "Framebuffer allocated in internal RAM (%d bytes)", DISPLAY_BUFFER_SIZE); - } - memset(_framebuffer, 0xFF, DISPLAY_BUFFER_SIZE); // Initialize to white +// 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; - // Perform initial full refresh to clear display BEFORE creating LVGL display - // This prevents LVGL from trying to render during the initial clear - ESP_LOGI(TAG, "Performing initial display clear..."); - _perform_full_refresh(_framebuffer); - ESP_LOGI(TAG, "Initial display clear complete"); - - // Create LVGL display manually (no esp_lcd panel for e-paper) - lv_display_t* disp = lv_display_create(DISPLAY_WIDTH, DISPLAY_HEIGHT); - if (disp == nullptr) { - ESP_LOGE(TAG, "Failed to create LVGL display"); - return; - } - - /* 1-bit e-paper display */ - lv_display_set_color_format(disp, LV_COLOR_FORMAT_I1); - - /* Create a draw buffer covering ~40 lines */ - _lvgl_draw_buf = lv_draw_buf_create(DISPLAY_WIDTH, DISPLAY_HEIGHT, LV_COLOR_FORMAT_I1, LV_STRIDE_AUTO); - if (_lvgl_draw_buf == nullptr) { - ESP_LOGE(TAG, "Failed to create LVGL draw buffer"); - lv_display_delete(disp); - return; - } - - lv_display_set_draw_buffers(disp, _lvgl_draw_buf, NULL); - lv_display_set_render_mode(disp, LV_DISPLAY_RENDER_MODE_DIRECT); - - // Set custom flush callback and user data - lv_display_set_flush_cb(disp, _lvgl_flush_cb); - lv_display_set_user_data(disp, this); - - _lvgl_display = disp; - - ESP_LOGI(TAG, "LVGL display registered"); - - // Register GT911 touch input with LVGL, only if touch handle is valid - esp_lcd_touch_handle_t tp_handle = get_touch_handle(); - if (tp_handle == nullptr) { - ESP_LOGE(TAG, "Touch handle is NULL — touch initialization failed; skipping LVGL touch registration"); - } else { - const lvgl_port_touch_cfg_t touch_cfg = { - .disp = _lvgl_display, - .handle = tp_handle, - .scale = {}, // Default scaling - }; - - _lvgl_touch_indev = lvgl_port_add_touch(&touch_cfg); - if (_lvgl_touch_indev == nullptr) { - ESP_LOGE(TAG, "Failed to register LVGL touch input"); - return; + { + 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; } - // Override touch read callback to check BUSY pin - lv_indev_set_read_cb(_lvgl_touch_indev, _lvgl_touch_read_cb); - lv_indev_set_user_data(_lvgl_touch_indev, this); + // 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)); - ESP_LOGI(TAG, "LVGL touch input registered"); - } + // 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 - // Set display ready bits - xEventGroupSetBits(_system_event_group, DISPLAY_READY_BIT | TOUCH_CALIBRATED_BIT); - ESP_LOGI(TAG, "E-Ink display handler initialized successfully"); -} + // 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)); -void EInkDisplayHandler::start_touch_task() { - // Note: With lvgl_port_add_touch, the ESP-IDF LVGL port handles touch reading internally - // We don't need a separate touch task unless we want custom processing - ESP_LOGI(TAG, "Touch input handled by LVGL port"); -} - -void EInkDisplayHandler::request_full_refresh() { - if (xSemaphoreTake(_refresh_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { - _force_full_refresh = true; - _partial_refresh_count = 0; - xSemaphoreGive(_refresh_mutex); - ESP_LOGI(TAG, "Full refresh requested"); - } -} - -bool EInkDisplayHandler::is_busy() const { - return gpio_get_level(PIN_BUSY) == BUSY_ACTIVE_LEVEL; // BUSY is active LOW -} - -void EInkDisplayHandler::_lvgl_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map) { - EInkDisplayHandler* handler = static_cast(lv_display_get_user_data(disp)); - if (handler == nullptr) { - ESP_LOGE(TAG, "Invalid handler in flush callback"); - lv_display_flush_ready(disp); - return; - } - - // Check if display is busy with detailed logging - int busy_level = gpio_get_level(PIN_BUSY); - ESP_LOGI(TAG, "Flush callback: BUSY pin = %d, is_busy() = %d", busy_level, handler->is_busy()); - - if (handler->is_busy()) { - ESP_LOGW(TAG, "Display busy (BUSY pin = 0), skipping flush"); - lv_display_flush_ready(disp); - return; - } - - // Wait for any ongoing refresh to complete - handler->_wait_for_busy(); - - bool perform_full_refresh = false; - - if (xSemaphoreTake(handler->_refresh_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { - // Check if full refresh is needed - if (handler->_force_full_refresh) { - perform_full_refresh = true; - handler->_force_full_refresh = false; - handler->_partial_refresh_count = 0; - } else { - handler->_partial_refresh_count++; - if (handler->_partial_refresh_count >= PARTIAL_REFRESH_THRESHOLD) { - perform_full_refresh = true; - handler->_partial_refresh_count = 0; + 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); } } - xSemaphoreGive(handler->_refresh_mutex); - } + 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)); - // Copy LVGL buffer to framebuffer - // For 1-bit mode, LVGL provides data in packed format (8 pixels per byte) - int32_t w = lv_area_get_width(area); - int32_t h = lv_area_get_height(area); - - ESP_LOGI(TAG, "Flushing area: x=%d, y=%d, w=%d, h=%d, full_refresh=%d", - area->x1, area->y1, w, h, perform_full_refresh); - - // For simplicity with e-paper, we'll do full frame updates - // Copy the entire buffer - for (int32_t y = 0; y < h; y++) { - int32_t fb_y = area->y1 + y; - if (fb_y >= DISPLAY_HEIGHT) break; - - for (int32_t x = 0; x < w; x += 8) { - int32_t fb_x = area->x1 + x; - if (fb_x >= DISPLAY_WIDTH) break; - - // Calculate byte position in framebuffer (row-major, 1-bit packed) - size_t fb_byte_idx = (fb_y * DISPLAY_WIDTH + fb_x) / 8; - size_t px_byte_idx = (y * w + x) / 8; - - if (fb_byte_idx < DISPLAY_BUFFER_SIZE && px_byte_idx < (w * h / 8)) { - handler->_framebuffer[fb_byte_idx] = px_map[px_byte_idx]; - } + // 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; } } - - // Perform refresh - if (perform_full_refresh) { - ESP_LOGI(TAG, "Performing full refresh..."); - handler->_perform_full_refresh(handler->_framebuffer); - } else { - ESP_LOGI(TAG, "Performing partial refresh..."); - handler->_perform_partial_refresh(handler->_framebuffer); - } - - lv_display_flush_ready(disp); + return err; } -void EInkDisplayHandler::_lvgl_touch_read_cb(lv_indev_t* indev, lv_indev_data_t* data) { - EInkDisplayHandler* handler = static_cast(lv_indev_get_user_data(indev)); - // Disable touch input during display refresh (BUSY) - if (handler->is_busy()) { - data->state = LV_INDEV_STATE_RELEASED; - data->continue_reading = false; - return; +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; } - - esp_lcd_touch_handle_t tp_handle = handler->get_touch_handle(); - if (tp_handle == nullptr) { - data->state = LV_INDEV_STATE_RELEASED; - return; + 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_); - // Read touch data from GT911 - esp_err_t ret = esp_lcd_touch_read_data(tp_handle); - if (ret == ESP_OK) { - uint8_t touch_cnt = 0; - // Get touch data using new API - esp_lcd_touch_point_data_t point_data[1]; - esp_lcd_touch_get_data(tp_handle, point_data, &touch_cnt, 1); - - if (touch_cnt > 0) { - ESP_LOGI(TAG, "Touch data read successfully: x=%d, y=%d", point_data[0].x, point_data[0].y); - data->point.x = point_data[0].x; - data->point.y = point_data[0].y; - data->state = LV_INDEV_STATE_PRESSED; - } else { - data->state = LV_INDEV_STATE_RELEASED; - } - } else { - data->state = LV_INDEV_STATE_RELEASED; - } - - data->continue_reading = false; -} - -void EInkDisplayHandler::_perform_full_refresh(const uint8_t* framebuffer) { - ESP_LOGI(TAG, "Starting full refresh (3 seconds)..."); - - _wait_for_busy(); - - spi_transaction_t* rtrans; // Declare once for entire function - - // Step 1: Write old data (0x10) - typically all zeros for full refresh - epd_write_cmd(0x10); - - if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) { - ESP_LOGE(TAG, "SPI mutex timeout in full refresh step 1"); - return; - } - gpio_set_level(PIN_DC, 1); // Data mode - - ESP_LOGI(TAG, "Starting SPI data transmission for old data (0x10)..."); - - // Use simpler polling transmission instead of queued to avoid complexity - static uint8_t zero_byte = 0x00; // Static to persist - esp_err_t ret; - for (size_t i = 0; i < DISPLAY_BUFFER_SIZE; i++) { - spi_transaction_t t = {}; - t.length = 8; - t.tx_buffer = &zero_byte; - - ret = spi_device_polling_transmit(_spi, &t); - if (ret != ESP_OK) { - ESP_LOGE(TAG, "Failed to send SPI byte %zu: %s", i, esp_err_to_name(ret)); - break; - } - - // Yield every 1000 bytes to prevent watchdog timeout - if (i % 1000 == 999) { - xSemaphoreGive(_spi_mutex); - vTaskDelay(pdMS_TO_TICKS(1)); // Small delay - if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) { - ESP_LOGE(TAG, "SPI mutex timeout during yield at byte %zu", i); - return; - } - gpio_set_level(PIN_DC, 1); // Re-set data mode after yield - ESP_LOGI(TAG, "Transmitted %zu/%zu bytes (%.1f%%)", i + 1, DISPLAY_BUFFER_SIZE, - (float)(i + 1) * 100.0f / DISPLAY_BUFFER_SIZE); - } - } - ESP_LOGI(TAG, "Completed SPI data transmission for old data"); - xSemaphoreGive(_spi_mutex); - - // Step 2: Write new data (0x13) - epd_write_cmd(0x13); - - if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) { - ESP_LOGE(TAG, "SPI mutex timeout in full refresh step 2"); - return; - } - gpio_set_level(PIN_DC, 1); // Data mode - - ESP_LOGI(TAG, "Starting SPI data transmission for new data (0x13)..."); - - // Use polling transmission for simplicity and reliability - for (size_t i = 0; i < DISPLAY_BUFFER_SIZE; i++) { - uint8_t data_byte = framebuffer[i]; // Write data directly (no inversion) - - spi_transaction_t t = {}; - t.length = 8; - t.tx_buffer = &data_byte; - - esp_err_t ret = spi_device_polling_transmit(_spi, &t); - if (ret != ESP_OK) { - ESP_LOGE(TAG, "Failed to send SPI byte %zu: %s", i, esp_err_to_name(ret)); - break; - } - - // Yield every 100 bytes to prevent watchdog timeout - if (i % 100 == 99) { - xSemaphoreGive(_spi_mutex); - vTaskDelay(pdMS_TO_TICKS(5)); // Increased delay for better yielding - if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) { - ESP_LOGE(TAG, "SPI mutex timeout during yield at byte %zu", i); - return; - } - gpio_set_level(PIN_DC, 1); // Re-set data mode after yield - ESP_LOGI(TAG, "Transmitted %zu/%zu bytes (%.1f%%)", i + 1, DISPLAY_BUFFER_SIZE, - (float)(i + 1) * 100.0f / DISPLAY_BUFFER_SIZE); - } - } - - ESP_LOGI(TAG, "Completed SPI data transmission for new data"); - xSemaphoreGive(_spi_mutex); - - // Step 3: Trigger display refresh (DRF) - epd_write_cmd(0x12); - // Critical delay - sample code says "!!!The delay here is necessary, 200uS at least!!!" - vTaskDelay(pdMS_TO_TICKS(10)); - ESP_LOGI(TAG, "Display refresh triggered, BUSY pin: %d", gpio_get_level(PIN_BUSY)); - - // Wait for refresh to complete - _wait_for_busy(); - - ESP_LOGI(TAG, "Full refresh complete"); -} - -void EInkDisplayHandler::_perform_partial_refresh(const uint8_t* framebuffer) { - ESP_LOGI(TAG, "Starting partial refresh (0.3 seconds)..."); - - _wait_for_busy(); - - // Step 1: Configure VCOM for partial refresh - const uint8_t vcom_data[] = { 0xA9, 0x07 }; - epd_write_cmd_with_data(0x50, vcom_data, 2); - - // Step 2: Enter partial refresh mode - epd_write_cmd(0x91); - - // Step 3: Define partial window (full screen for now) - // Format: 0x90 + 9 bytes (x_start_H, x_start_L, x_end_H, x_end_L, y_start_H, y_start_L, y_end_H, y_end_L, 0x01) - // For full screen: x=0 to 799 (0x031F), y=0 to 479 (0x01DF) - const uint8_t window_data[] = { - 0x00, 0x00, // x_start = 0 - 0x03, 0x1F, // x_end = 799 (0x31F) - 0x00, 0x00, // y_start = 0 - 0x01, 0xDF, // y_end = 479 (0x1DF) - 0x01 // PT_SCAN + // 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 }; - epd_write_cmd_with_data(0x90, window_data, 9); - // Step 4: Write new data (0x13 command) - epd_write_cmd(0x13); + 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 - if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) { - ESP_LOGE(TAG, "SPI mutex timeout in partial refresh"); - return; + 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; } - gpio_set_level(PIN_DC, 1); // Data mode + return err; +} - ESP_LOGI(TAG, "Starting SPI data transmission for partial refresh..."); - // Send data in chunks with task yields to prevent blocking - static uint8_t tx_byte; - const size_t CHUNK_SIZE = 1000; // Send 1000 bytes between yields - for (size_t i = 0; i < DISPLAY_BUFFER_SIZE; i++) { - tx_byte = framebuffer[i]; // Write data directly (no inversion) +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 = 8; - t.tx_buffer = &tx_byte; - spi_device_polling_transmit(_spi, &t); + t.length = transfer_size * 8; // Length in bits + t.tx_buffer = data + offset; - // Yield to other tasks every CHUNK_SIZE bytes - if (i % CHUNK_SIZE == (CHUNK_SIZE - 1)) { - xSemaphoreGive(_spi_mutex); - vTaskDelay(pdMS_TO_TICKS(1)); // Allow other tasks to run - if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) { - ESP_LOGE(TAG, "SPI mutex timeout during partial refresh yield"); - return; - } - gpio_set_level(PIN_DC, 1); // Re-set data mode after yield - ESP_LOGI(TAG, "Partial refresh progress: %zu/%zu bytes (%.1f%%)", i + 1, DISPLAY_BUFFER_SIZE, - (float)(i + 1) * 100.0f / DISPLAY_BUFFER_SIZE); - } - } - ESP_LOGI(TAG, "Completed SPI data transmission for partial refresh"); - xSemaphoreGive(_spi_mutex); - - // Step 5: Trigger partial display refresh (DRF) - epd_write_cmd(0x12); - // Critical delay - sample code says "!!!The delay here is necessary, 200uS at least!!!" - vTaskDelay(pdMS_TO_TICKS(10)); - ESP_LOGI(TAG, "Partial refresh triggered, BUSY pin: %d", gpio_get_level(PIN_BUSY)); - - // Wait for refresh to complete - _wait_for_busy(); - - // Step 6: Exit partial refresh mode - epd_write_cmd(0x92); - - ESP_LOGI(TAG, "Partial refresh complete"); -} - -void EInkDisplayHandler::_wait_for_busy() { - 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; - } - - int timeout = 0; - while (gpio_get_level(PIN_BUSY) == BUSY_ACTIVE_LEVEL) { // 0=BUSY, 1=FREE - vTaskDelay(pdMS_TO_TICKS(100)); - timeout++; - if (timeout > 100) { // 10 second timeout - ESP_LOGE(TAG, "Display BUSY timeout! Pin level: %d", gpio_get_level(PIN_BUSY)); - ESP_LOGW(TAG, "Attempting hardware reset..."); - - // Hardware reset sequence - gpio_set_level(PIN_RST, 0); - vTaskDelay(pdMS_TO_TICKS(10)); - gpio_set_level(PIN_RST, 1); - vTaskDelay(pdMS_TO_TICKS(100)); - - // Re-initialize display - ESP_LOGI(TAG, "Re-initializing display after reset..."); - _epd_init(); - - // Check if reset worked - int reset_timeout = 0; - while (gpio_get_level(PIN_BUSY) == BUSY_ACTIVE_LEVEL) { - vTaskDelay(pdMS_TO_TICKS(100)); - reset_timeout++; - if (reset_timeout > 50) { // 5 second timeout after reset - ESP_LOGE(TAG, "Display reset failed! Still busy after reset."); - break; - } - } - - if (gpio_get_level(PIN_BUSY) != BUSY_ACTIVE_LEVEL) { - ESP_LOGI(TAG, "Display reset successful after %d tenths of a second", reset_timeout); - } - break; + 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; } - // Log every 2 seconds to track progress - if (timeout % 20 == 0) { - ESP_LOGW(TAG, "Still waiting for BUSY pin, timeout: %d/100, level: %d", - timeout, gpio_get_level(PIN_BUSY)); + 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, "Display ready after %d tenths of a second", timeout); + + ESP_LOGI(TAG, "transfer_spi_data: completed sending %zu bytes of data", length); + return ESP_OK; } -void EInkDisplayHandler::_convert_buffer_to_epaper(const uint8_t* lvgl_buf, uint8_t* epd_buf, size_t size) { - // LVGL 1-bit format is already compatible with e-paper - // Just copy directly - memcpy(epd_buf, lvgl_buf, size); +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; +} \ No newline at end of file diff --git a/main/display/eink_display_handler.cpp.old b/main/display/eink_display_handler.cpp.old new file mode 100644 index 0000000..8f6d855 --- /dev/null +++ b/main/display/eink_display_handler.cpp.old @@ -0,0 +1,661 @@ +#include "display/eink_display_handler.h" +#include "display/constants.h" +#include "common/constants.h" +#include "esp_log.h" +#include "esp_heap_caps.h" +#include "esp_task_wdt.h" +#include + +#define TAG "EInkDisplayHandler" +#define BUSY_ACTIVE_LEVEL 0 // BUSY pin is active low +#define BUSY_INACTIVE_LEVEL 1 + +EInkDisplayHandler::EInkDisplayHandler(EventGroupHandle_t system_event_group) + : DisplayHandler(system_event_group) { + _refresh_mutex = xSemaphoreCreateMutex(); + if (_refresh_mutex == nullptr) { + ESP_LOGE(TAG, "Failed to create refresh mutex"); + } +} + +EInkDisplayHandler::~EInkDisplayHandler() { + if (_refresh_task_handle != nullptr) { + vTaskDelete(_refresh_task_handle); + } + if (_touch_task_handle != nullptr) { + vTaskDelete(_touch_task_handle); + } + if (_refresh_queue != nullptr) { + vQueueDelete(_refresh_queue); + } + if (_lvgl_display != nullptr) { + lv_display_delete(_lvgl_display); + _lvgl_display = nullptr; + if (_lvgl_draw_buf != nullptr) { + lv_draw_buf_destroy(_lvgl_draw_buf); + _lvgl_draw_buf = nullptr; + } + } + if (_lvgl_touch_indev != nullptr) { + lvgl_port_remove_touch(_lvgl_touch_indev); + } + if (_framebuffer != nullptr) { + heap_caps_free(_framebuffer); + } + if (_refresh_mutex != nullptr) { + vSemaphoreDelete(_refresh_mutex); + } +} + +void EInkDisplayHandler::init() { + ESP_LOGI(TAG, "Initializing E-Ink display handler..."); + + // 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; + gpio_config(&io_conf); + + // 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; + gpio_config(&io_conf); + + // 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; + + esp_err_t 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; + } + + // Add SPI device + spi_device_interface_config_t devcfg = {}; + devcfg.clock_speed_hz = 6 * 1000 * 1000; // 6 MHz (reduced for reliability) + 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; + } + + // Initialize base display and touch devices + init_devices(false); // Don't set ready bit yet + + // Create refresh queue (queue 5 refresh requests) + _refresh_queue = xQueueCreate(5, sizeof(bool)); + if (_refresh_queue == nullptr) { + ESP_LOGE(TAG, "Failed to create refresh queue"); + return; + } + + // Create refresh task + BaseType_t ret_task = xTaskCreatePinnedToCore( + _refresh_task, + "eink_refresh", + 8192, + this, + 5, // Priority - lower than LVGL task + &_refresh_task_handle, + 1 // Pin to core 1 + ); + if (ret_task != pdPASS) { + ESP_LOGE(TAG, "Failed to create refresh task"); + return; + } + + // Allocate framebuffer - try PSRAM first, fallback to internal RAM + // Note: Internal framebuffer excludes the 8-byte palette (raw pixel data only) + const size_t fb_size = DISPLAY_BUFFER_SIZE - 8; // Exclude palette from internal storage + _framebuffer = (uint8_t*)heap_caps_malloc(fb_size, MALLOC_CAP_SPIRAM); + if (_framebuffer != nullptr) { + _framebuffer_in_psram = true; + ESP_LOGI(TAG, "Framebuffer allocated in PSRAM (%zu bytes, LVGL buffer: %d bytes)", + fb_size, DISPLAY_BUFFER_SIZE); + } else { + ESP_LOGW(TAG, "PSRAM not available, allocating framebuffer in internal RAM"); + _framebuffer = (uint8_t*)heap_caps_malloc(fb_size, MALLOC_CAP_INTERNAL); + _framebuffer_in_psram = false; + if (_framebuffer == nullptr) { + ESP_LOGE(TAG, "Failed to allocate framebuffer"); + return; + } + ESP_LOGI(TAG, "Framebuffer allocated in internal RAM (%zu bytes, LVGL buffer: %d bytes)", + fb_size, DISPLAY_BUFFER_SIZE); + } + memset(_framebuffer, 0xFF, fb_size); // Initialize to white + + // Perform initial full refresh to clear display BEFORE creating LVGL display + // This prevents LVGL from trying to render during the initial clear + ESP_LOGI(TAG, "Performing initial display clear..."); + _perform_full_refresh(_framebuffer); + ESP_LOGI(TAG, "Initial display clear complete"); + + // Create LVGL display manually (no esp_lcd panel for e-paper) + lv_display_t* disp = lv_display_create(DISPLAY_WIDTH, DISPLAY_HEIGHT); + if (disp == nullptr) { + ESP_LOGE(TAG, "Failed to create LVGL display"); + return; + } + + /* 1-bit e-paper display */ + lv_display_set_color_format(disp, LV_COLOR_FORMAT_I1); + + /* Disable antialiasing for monochrome display to ensure crisp 1px lines */ + lv_display_set_antialiasing(disp, false); + + /* Create a draw buffer covering ~40 lines */ + _lvgl_draw_buf = lv_draw_buf_create(DISPLAY_WIDTH, DISPLAY_HEIGHT, LV_COLOR_FORMAT_I1, LV_STRIDE_AUTO); + if (_lvgl_draw_buf == nullptr) { + ESP_LOGE(TAG, "Failed to create LVGL draw buffer"); + lv_display_delete(disp); + return; + } + + lv_display_set_draw_buffers(disp, _lvgl_draw_buf, NULL); + lv_display_set_render_mode(disp, LV_DISPLAY_RENDER_MODE_DIRECT); + + // Set custom flush callback and user data + lv_display_set_flush_cb(disp, _lvgl_flush_cb); + lv_display_set_user_data(disp, this); + + _lvgl_display = disp; + + ESP_LOGI(TAG, "LVGL display registered"); + + // Register GT911 touch input with LVGL, only if touch handle is valid + esp_lcd_touch_handle_t tp_handle = get_touch_handle(); + if (tp_handle == nullptr) { + ESP_LOGE(TAG, "Touch handle is NULL — touch initialization failed; skipping LVGL touch registration"); + } else { + const lvgl_port_touch_cfg_t touch_cfg = { + .disp = _lvgl_display, + .handle = tp_handle, + .scale = {}, // Default scaling + }; + + _lvgl_touch_indev = lvgl_port_add_touch(&touch_cfg); + if (_lvgl_touch_indev == nullptr) { + ESP_LOGE(TAG, "Failed to register LVGL touch input"); + return; + } + + // Override touch read callback to check BUSY pin + lv_indev_set_read_cb(_lvgl_touch_indev, _lvgl_touch_read_cb); + lv_indev_set_user_data(_lvgl_touch_indev, this); + + ESP_LOGI(TAG, "LVGL touch input registered"); + } + + // Set display ready bits + xEventGroupSetBits(_system_event_group, DISPLAY_READY_BIT | TOUCH_CALIBRATED_BIT); + ESP_LOGI(TAG, "E-Ink display handler initialized successfully"); +} + +void EInkDisplayHandler::start_touch_task() { + // Note: With lvgl_port_add_touch, the ESP-IDF LVGL port handles touch reading internally + // We don't need a separate touch task unless we want custom processing + ESP_LOGI(TAG, "Touch input handled by LVGL port"); +} + +void EInkDisplayHandler::request_full_refresh() { + if (xSemaphoreTake(_refresh_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { + _force_full_refresh = true; + _partial_refresh_count = 0; + xSemaphoreGive(_refresh_mutex); + ESP_LOGI(TAG, "Full refresh requested"); + } +} + +bool EInkDisplayHandler::is_busy() const { + return gpio_get_level(PIN_BUSY) == BUSY_ACTIVE_LEVEL; // BUSY is active LOW +} + +void EInkDisplayHandler::_lvgl_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map) { + EInkDisplayHandler* handler = static_cast(lv_display_get_user_data(disp)); + if (handler == nullptr) { + ESP_LOGE(TAG, "Invalid handler in flush callback"); + lv_display_flush_ready(disp); + return; + } + + // Check if display is busy with detailed logging + int busy_level = gpio_get_level(PIN_BUSY); + ESP_LOGI(TAG, "Flush callback: BUSY pin = %d, is_busy() = %d", busy_level, handler->is_busy()); + + if (handler->is_busy()) { + ESP_LOGW(TAG, "Display busy (BUSY pin = 0), skipping flush"); + lv_display_flush_ready(disp); + return; + } + + // Wait for any ongoing refresh to complete + handler->_wait_for_busy(); + + bool perform_full_refresh = false; + + if (xSemaphoreTake(handler->_refresh_mutex, pdMS_TO_TICKS(100)) == pdTRUE) { + // Check if full refresh is needed + if (handler->_force_full_refresh) { + perform_full_refresh = true; + handler->_force_full_refresh = false; + handler->_partial_refresh_count = 0; + } else { + handler->_partial_refresh_count++; + if (handler->_partial_refresh_count >= PARTIAL_REFRESH_THRESHOLD) { + perform_full_refresh = true; + handler->_partial_refresh_count = 0; + } + } + xSemaphoreGive(handler->_refresh_mutex); + } + + // Copy LVGL buffer to framebuffer + // For 1-bit mode, LVGL provides data in packed format (8 pixels per byte) + // CRITICAL: Skip first 8 bytes (LVGL I1 palette) as per LVGL documentation + uint8_t* pixel_data = px_map + 8; // Skip 8-byte palette + int32_t w = lv_area_get_width(area); + int32_t h = lv_area_get_height(area); + + ESP_LOGI(TAG, "Flushing area: x=%d, y=%d, w=%d, h=%d, full_refresh=%d", + area->x1, area->y1, w, h, perform_full_refresh); + ESP_LOGI(TAG, "Buffer: px_map=%p, pixel_data=%p, palette skipped: %d bytes", + (void*)px_map, (void*)pixel_data, 8); + + // Check if this is a full screen update - if so, simple copy + if (area->x1 == 0 && area->y1 == 0 && w == DISPLAY_WIDTH && h == DISPLAY_HEIGHT) { + ESP_LOGI(TAG, "Full screen update, direct copy (skipping palette)"); + memcpy(handler->_framebuffer, pixel_data, DISPLAY_BUFFER_SIZE - 8); + } else { + ESP_LOGI(TAG, "Partial area update"); + // In DIRECT render mode, px_map points to the full screen buffer + // The stride is always the full display width + const uint32_t stride = DISPLAY_WIDTH / 8; // 800 / 8 = 100 bytes per row + + // Check if we can do row-by-row copy (byte-aligned on both x1 and width) + bool byte_aligned = (area->x1 % 8 == 0) && (w % 8 == 0); + + if (byte_aligned) { + // Optimized: byte-aligned row copy + ESP_LOGI(TAG, "Byte-aligned copy: x=%ld, y=%ld, w=%ld, h=%ld", + (long)area->x1, (long)area->y1, (long)w, (long)h); + + uint32_t x_byte = area->x1 / 8; + uint32_t width_bytes = w / 8; + + for (int32_t y = 0; y < h; y++) { + int32_t fb_y = area->y1 + y; + if (fb_y >= DISPLAY_HEIGHT) break; + + uint8_t* src = pixel_data + (fb_y * stride + x_byte); + uint8_t* dst = handler->_framebuffer + (fb_y * stride + x_byte); + memcpy(dst, src, width_bytes); + } + } else { + // Bit-level copy for non-aligned regions + ESP_LOGI(TAG, "Bit-level copy: x=%ld, y=%ld, w=%ld, h=%ld", + (long)area->x1, (long)area->y1, (long)w, (long)h); + + for (int32_t y = 0; y < h; y++) { + int32_t fb_y = area->y1 + y; + if (fb_y >= DISPLAY_HEIGHT) break; + + for (int32_t x = 0; x < w; x++) { + int32_t fb_x = area->x1 + x; + if (fb_x >= DISPLAY_WIDTH) break; + + // Get pixel from source buffer (using full screen coordinates) + size_t src_byte_idx = fb_y * stride + (fb_x / 8); + size_t src_bit_idx = fb_x % 8; + uint8_t src_bit = (pixel_data[src_byte_idx] >> (7 - src_bit_idx)) & 0x01; + + // Set pixel in destination buffer + size_t dst_byte_idx = fb_y * stride + (fb_x / 8); + size_t dst_bit_idx = fb_x % 8; + + if (dst_byte_idx < (DISPLAY_BUFFER_SIZE - 8)) { + if (src_bit) { + handler->_framebuffer[dst_byte_idx] |= (1 << (7 - dst_bit_idx)); + } else { + handler->_framebuffer[dst_byte_idx] &= ~(1 << (7 - dst_bit_idx)); + } + } + } + } + } + } + + // Queue refresh request (non-blocking) + if (handler->_refresh_queue != nullptr) { + if (xQueueSend(handler->_refresh_queue, &perform_full_refresh, 0) != pdPASS) { + ESP_LOGW(TAG, "Refresh queue full, skipping refresh"); + } else { + ESP_LOGI(TAG, "Queued %s refresh", perform_full_refresh ? "full" : "partial"); + } + } + + lv_display_flush_ready(disp); +} + +void EInkDisplayHandler::_lvgl_touch_read_cb(lv_indev_t* indev, lv_indev_data_t* data) { + EInkDisplayHandler* handler = static_cast(lv_indev_get_user_data(indev)); + + // Disable touch input during display refresh (BUSY) + if (handler->is_busy()) { + data->state = LV_INDEV_STATE_RELEASED; + data->continue_reading = false; + return; + } + + esp_lcd_touch_handle_t tp_handle = handler->get_touch_handle(); + if (tp_handle == nullptr) { + data->state = LV_INDEV_STATE_RELEASED; + return; + } + + + // Read touch data from GT911 + esp_err_t ret = esp_lcd_touch_read_data(tp_handle); + if (ret == ESP_OK) { + uint8_t touch_cnt = 0; + // Get touch data using new API + esp_lcd_touch_point_data_t point_data[1]; + esp_lcd_touch_get_data(tp_handle, point_data, &touch_cnt, 1); + + if (touch_cnt > 0) { + ESP_LOGI(TAG, "Touch data read successfully: x=%d, y=%d", point_data[0].x, point_data[0].y); + data->point.x = point_data[0].x; + data->point.y = point_data[0].y; + data->state = LV_INDEV_STATE_PRESSED; + } else { + data->state = LV_INDEV_STATE_RELEASED; + } + } else { + data->state = LV_INDEV_STATE_RELEASED; + } + + data->continue_reading = false; +} + +void EInkDisplayHandler::_perform_full_refresh(const uint8_t* framebuffer) { + ESP_LOGI(TAG, "Starting full refresh (3 seconds)..."); + + _wait_for_busy(); + + // Step 1: Write old data (0x10) - Arduino uses 0xFF (all white) for base map + epd_write_cmd(0x10); + + if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) { + ESP_LOGE(TAG, "SPI mutex timeout in full refresh step 1"); + return; + } + gpio_set_level(PIN_DC, 1); // Data mode + + ESP_LOGI(TAG, "Starting SPI data transmission for old data (0x10)..."); + + // Send 0xFF (white) for all old data, matching Arduino EPD_SetRAMValue_BaseMap + // Use DMA transfers in chunks for better performance + static uint8_t white_buffer[4096]; // 4KB chunk buffer + memset(white_buffer, 0xFF, sizeof(white_buffer)); + + const size_t CHUNK_SIZE = sizeof(white_buffer); + size_t remaining = DISPLAY_BUFFER_SIZE - 8; // Exclude palette from transmission + size_t offset = 0; + + while (remaining > 0) { + size_t transfer_size = (remaining < CHUNK_SIZE) ? remaining : CHUNK_SIZE; + + spi_transaction_t t = {}; + t.length = transfer_size * 8; // Length in bits + t.tx_buffer = white_buffer; + + 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)); + break; + } + + remaining -= transfer_size; + offset += transfer_size; + + // Yield every 16KB to prevent watchdog timeout + if (offset % (16 * 1024) == 0) { + ESP_LOGI(TAG, "Old data progress: %zu/%zu bytes (%.1f%%)", offset, remaining, + (float)offset * 100.0f / (float)remaining); + vTaskDelay(pdMS_TO_TICKS(1)); + } + } + ESP_LOGI(TAG, "Completed SPI data transmission for old data"); + xSemaphoreGive(_spi_mutex); + + // Step 2: Write new data (0x13) + epd_write_cmd(0x13); + + if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) { + ESP_LOGE(TAG, "SPI mutex timeout in full refresh step 2"); + return; + } + gpio_set_level(PIN_DC, 1); // Data mode + + ESP_LOGI(TAG, "Starting SPI data transmission for new data (0x13)..."); + + // Send actual framebuffer data in chunks using DMA for better performance + offset = 0; + remaining = DISPLAY_BUFFER_SIZE - 8; // Reset remaining for step 2 + + while (remaining > 0) { + size_t transfer_size = (remaining < CHUNK_SIZE) ? remaining : CHUNK_SIZE; + + spi_transaction_t t = {}; + t.length = transfer_size * 8; // Length in bits + t.tx_buffer = framebuffer + 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)); + break; + } + + 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 (%.1f%%)", offset, remaining, + (float)offset * 100.0f / (float)remaining); + vTaskDelay(pdMS_TO_TICKS(1)); + } + } + + ESP_LOGI(TAG, "Completed SPI data transmission for new data"); + xSemaphoreGive(_spi_mutex); + + // Step 3: Trigger display refresh (DRF) + epd_write_cmd(0x12); + // Critical delay - sample code says "!!!The delay here is necessary, 200uS at least!!!" + vTaskDelay(pdMS_TO_TICKS(10)); + ESP_LOGI(TAG, "Display refresh triggered, BUSY pin: %d", gpio_get_level(PIN_BUSY)); + + // Wait for refresh to complete + _wait_for_busy(); + + ESP_LOGI(TAG, "Full refresh complete"); +} + +void EInkDisplayHandler::_perform_partial_refresh(const uint8_t* framebuffer) { + ESP_LOGI(TAG, "Starting partial refresh (0.3 seconds)..."); + + _wait_for_busy(); + + // Step 1: Configure VCOM for partial refresh + const uint8_t vcom_data[] = { 0xA9, 0x07 }; + epd_write_cmd_with_data(0x50, vcom_data, 2); + + // Step 2: Enter partial refresh mode + epd_write_cmd(0x91); + + // Step 3: Define partial window (full screen for now) + // Format: 0x90 + 9 bytes (x_start_H, x_start_L, x_end_H, x_end_L, y_start_H, y_start_L, y_end_H, y_end_L, 0x01) + // For full screen: x=0 to 799 (0x031F), y=0 to 479 (0x01DF) + const uint8_t window_data[] = { + 0x00, 0x00, // x_start = 0 + 0x03, 0x1F, // x_end = 799 (0x31F) + 0x00, 0x00, // y_start = 0 + 0x01, 0xDF, // y_end = 479 (0x1DF) + 0x01 // PT_SCAN + }; + epd_write_cmd_with_data(0x90, window_data, 9); + + // Step 4: Write new data (0x13 command) + epd_write_cmd(0x13); + + if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) { + ESP_LOGE(TAG, "SPI mutex timeout in partial refresh"); + return; + } + gpio_set_level(PIN_DC, 1); // Data mode + + ESP_LOGI(TAG, "Starting SPI data transmission for partial refresh..."); + + // Send framebuffer data in chunks using DMA for better performance + const size_t CHUNK_SIZE = 4096; // 4KB chunks + size_t remaining = DISPLAY_BUFFER_SIZE - 8; // Exclude palette from transmission + size_t offset = 0; + + while (remaining > 0) { + size_t transfer_size = (remaining < CHUNK_SIZE) ? remaining : CHUNK_SIZE; + + spi_transaction_t t = {}; + t.length = transfer_size * 8; // Length in bits + t.tx_buffer = framebuffer + 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)); + break; + } + + remaining -= transfer_size; + offset += transfer_size; + + // Yield every 16KB to prevent watchdog timeout + if (offset % (16 * 1024) == 0) { + ESP_LOGI(TAG, "Partial refresh progress: %zu/%zu bytes (%.1f%%)", offset, remaining, + (float)offset * 100.0f / (float)remaining); + vTaskDelay(pdMS_TO_TICKS(1)); + } + } + ESP_LOGI(TAG, "Completed SPI data transmission for partial refresh"); + xSemaphoreGive(_spi_mutex); + + // Step 5: Trigger partial display refresh (DRF) + epd_write_cmd(0x12); + // Critical delay - sample code says "!!!The delay here is necessary, 200uS at least!!!" + vTaskDelay(pdMS_TO_TICKS(10)); + ESP_LOGI(TAG, "Partial refresh triggered, BUSY pin: %d", gpio_get_level(PIN_BUSY)); + + // Wait for refresh to complete + _wait_for_busy(); + + // Step 6: Exit partial refresh mode + epd_write_cmd(0x92); + + ESP_LOGI(TAG, "Partial refresh complete"); +} + +void EInkDisplayHandler::_refresh_task(void* param) { + EInkDisplayHandler* handler = static_cast(param); + bool perform_full_refresh = false; + + ESP_LOGI(TAG, "Refresh task started"); + + while (true) { + // Wait for refresh request + if (xQueueReceive(handler->_refresh_queue, &perform_full_refresh, portMAX_DELAY) == pdTRUE) { + // Perform the requested refresh type + if (perform_full_refresh) { + ESP_LOGI(TAG, "Refresh task: Performing full refresh..."); + handler->_perform_full_refresh(handler->_framebuffer); + } else { + ESP_LOGI(TAG, "Refresh task: Performing partial refresh..."); + handler->_perform_partial_refresh(handler->_framebuffer); + } + } + } +} + +void EInkDisplayHandler::_wait_for_busy() { + 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; + } + + int timeout = 0; + while (gpio_get_level(PIN_BUSY) == BUSY_ACTIVE_LEVEL) { // 0=BUSY, 1=FREE + vTaskDelay(pdMS_TO_TICKS(100)); + timeout++; + if (timeout > 100) { // 10 second timeout + ESP_LOGE(TAG, "Display BUSY timeout! Pin level: %d", gpio_get_level(PIN_BUSY)); + ESP_LOGW(TAG, "Attempting hardware reset..."); + + // Hardware reset sequence + gpio_set_level(PIN_RST, 0); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(PIN_RST, 1); + vTaskDelay(pdMS_TO_TICKS(100)); + + // Re-initialize display + ESP_LOGI(TAG, "Re-initializing display after reset..."); + _epd_init(); + + // Check if reset worked + int reset_timeout = 0; + while (gpio_get_level(PIN_BUSY) == BUSY_ACTIVE_LEVEL) { + vTaskDelay(pdMS_TO_TICKS(100)); + reset_timeout++; + if (reset_timeout > 50) { // 5 second timeout after reset + ESP_LOGE(TAG, "Display reset failed! Still busy after reset."); + break; + } + } + + if (gpio_get_level(PIN_BUSY) != BUSY_ACTIVE_LEVEL) { + ESP_LOGI(TAG, "Display reset successful after %d tenths of a second", reset_timeout); + } + break; + } + + // Log every 2 seconds to track progress + if (timeout % 20 == 0) { + ESP_LOGW(TAG, "Still waiting for BUSY pin, timeout: %d/100, level: %d", + timeout, gpio_get_level(PIN_BUSY)); + } + } + ESP_LOGI(TAG, "Display ready after %d tenths of a second", timeout); +} + +void EInkDisplayHandler::_convert_buffer_to_epaper(const uint8_t* lvgl_buf, uint8_t* epd_buf, size_t size) { + // LVGL 1-bit format is already compatible with e-paper + // Just copy directly + memcpy(epd_buf, lvgl_buf, size); +} diff --git a/main/display/eink_display_handler.h b/main/display/eink_display_handler.h index b49cf1d..589c43e 100644 --- a/main/display/eink_display_handler.h +++ b/main/display/eink_display_handler.h @@ -1,59 +1,99 @@ #pragma once -#include "display/display.h" -#include "lvgl.h" -#include "esp_lvgl_port.h" +#include "freertos/FreeRTOS.h" #include "freertos/semphr.h" +#include "esp_lcd_touch_gt911.h" +#include "common/semaphore_guard.h" +#include // Refresh mode configuration #define PARTIAL_REFRESH_THRESHOLD 10 // Full refresh every N partial refreshes #define DISPLAY_WIDTH 800 #define DISPLAY_HEIGHT 480 -#define DISPLAY_BUFFER_SIZE ((DISPLAY_WIDTH * DISPLAY_HEIGHT) / 8) // 1-bit per pixel -class EInkDisplayHandler : public DisplayHandler { +// forward declarations +class EInkDisplayHandler; + +struct RefreshArea { public: - EInkDisplayHandler(EventGroupHandle_t system_event_group); + RefreshArea(int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end) + : x1(x_start), y1(y_start), x2(x_end), y2(y_end) { } + int32_t x1; + int32_t y1; + int32_t x2; + int32_t y2; +}; + +class EInkDisplayHandler { +public: + EInkDisplayHandler(); virtual ~EInkDisplayHandler(); - void init(); - void start_touch_task(); + esp_err_t init_devices(EventGroupHandle_t system_event_group = nullptr); + + esp_err_t refresh_display(void); + esp_err_t full_write(const uint8_t* framebuffer); + esp_err_t partial_refresh(const uint8_t* framebuffer, const RefreshArea& area); + esp_err_t clear_display(void); // Request a full refresh on next flush - void request_full_refresh(); + void request_full_refresh(void); // Check if display is busy (refreshing) - bool is_busy() const; + bool is_busy(void) const; + void wait_for_idle(void) const; + + esp_lcd_touch_handle_t get_touch_handle() const { return tp_handle_; } + +protected: + esp_err_t epd_write_cmd(const uint8_t cmd, uint32_t transaction_id); + esp_err_t epd_write_data(const uint8_t data, uint32_t transaction_id); + esp_err_t epd_write_cmd_with_data(const uint8_t cmd, std::vector& data, uint32_t transaction_id); + esp_err_t transfer_spi_data(const uint8_t* data, const size_t& length, uint32_t transaction_id); private: - // LVGL display and input device handles - lv_display_t* _lvgl_display = nullptr; - lv_indev_t* _lvgl_touch_indev = nullptr; - lv_draw_buf_t* _lvgl_draw_buf = nullptr; - // Framebuffer - uint8_t* _framebuffer = nullptr; - bool _framebuffer_in_psram = false; + esp_err_t init_display_pins_(void); + esp_err_t epd_init_(void); + esp_err_t init_touch_(void); + esp_err_t dangerous_epd_write_cmd_without_lock_(const uint8_t cmd); + esp_err_t dangerous_epd_write_data_without_lock_(const uint8_t data); - // Refresh tracking - uint32_t _partial_refresh_count = 0; - bool _force_full_refresh = false; - SemaphoreHandle_t _refresh_mutex = nullptr; + esp_err_t begin_transaction_(TickType_t timeout, uint32_t& out_id); + esp_err_t end_transaction_(void); + // given a transaction ID, wait for current transaction to complete. The transaction ID will determine if the wait is needed. + esp_err_t wait_for_transaction_end_(TickType_t timeout, uint32_t awaiting_transaction_id, SemaphoreGuard& out_transaction_guard); - // Touch task - TaskHandle_t _touch_task_handle = nullptr; + friend class TransactionGuard; - // LVGL callbacks - static void _lvgl_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map); - static void _lvgl_touch_read_cb(lv_indev_t* indev, lv_indev_data_t* data); + uint32_t partial_refresh_count_ = 0; + bool force_full_refresh_ = false; - // Display operations - void _perform_full_refresh(const uint8_t* framebuffer); - void _perform_partial_refresh(const uint8_t* framebuffer); - void _wait_for_busy(); - - // Touch task - static void _touch_task(void* param); - - // Helper to convert LVGL 1-bit buffer to e-paper format - void _convert_buffer_to_epaper(const uint8_t* lvgl_buf, uint8_t* epd_buf, size_t size); + SemaphoreHandle_t spi_mutex_ = nullptr; + SemaphoreHandle_t spi_transaction_mutex_ = nullptr; + SemaphoreHandle_t refresh_mutex_ = nullptr; + uint32_t spi_transaction_id = 0; // For tracking SPI transactions + spi_device_handle_t spi_ = nullptr; + esp_lcd_panel_io_handle_t tp_io_handle_ = nullptr; + esp_lcd_touch_handle_t tp_handle_ = nullptr; +}; + + +class TransactionGuard { +public: + TransactionGuard(EInkDisplayHandler& handler, TickType_t timeout = portMAX_DELAY) + : handler_(handler) { } + ~TransactionGuard() { if (transaction_id_) handler_.end_transaction_(); } + + esp_err_t begin(TickType_t timeout = portMAX_DELAY) { + esp_err_t err = handler_.begin_transaction_(timeout, transaction_id_); + return err; + } + uint32_t transaction_id() const { return transaction_id_; } + bool is_active() const { return transaction_id_ != 0; } +private: + // delete copy constructor and assignment operator + TransactionGuard(const TransactionGuard&) = delete; + TransactionGuard& operator=(const TransactionGuard&) = delete; + EInkDisplayHandler& handler_; + uint32_t transaction_id_ = 0; }; diff --git a/main/display/eink_display_handler.h.old b/main/display/eink_display_handler.h.old new file mode 100644 index 0000000..8497e10 --- /dev/null +++ b/main/display/eink_display_handler.h.old @@ -0,0 +1,66 @@ +#pragma once +#include "display/display.h" +#include "lvgl.h" +#include "esp_lvgl_port.h" +#include "freertos/semphr.h" + +// Refresh mode configuration +#define PARTIAL_REFRESH_THRESHOLD 10 // Full refresh every N partial refreshes +#define DISPLAY_WIDTH 800 +#define DISPLAY_HEIGHT 480 +#define DISPLAY_BUFFER_SIZE (((DISPLAY_WIDTH * DISPLAY_HEIGHT) / 8) + 8) // 1-bit per pixel + 8-byte palette + +class EInkDisplayHandler : public DisplayHandler { +public: + EInkDisplayHandler(EventGroupHandle_t system_event_group); + virtual ~EInkDisplayHandler(); + + void init(); + void start_touch_task(); + + // Request a full refresh on next flush + void request_full_refresh(); + + // Check if display is busy (refreshing) + bool is_busy() const; + +private: + // LVGL display and input device handles + lv_display_t* _lvgl_display = nullptr; + lv_indev_t* _lvgl_touch_indev = nullptr; + lv_draw_buf_t* _lvgl_draw_buf = nullptr; + + // Framebuffer + uint8_t* _framebuffer = nullptr; + bool _framebuffer_in_psram = false; + + // Refresh tracking + uint32_t _partial_refresh_count = 0; + bool _force_full_refresh = false; + SemaphoreHandle_t _refresh_mutex = nullptr; + + // Touch task + TaskHandle_t _touch_task_handle = nullptr; + + // Refresh task and queue + TaskHandle_t _refresh_task_handle = nullptr; + QueueHandle_t _refresh_queue = nullptr; + + // LVGL callbacks + static void _lvgl_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map); + static void _lvgl_touch_read_cb(lv_indev_t* indev, lv_indev_data_t* data); + + // Display operations + void _perform_full_refresh(const uint8_t* framebuffer); + void _perform_partial_refresh(const uint8_t* framebuffer); + void _wait_for_busy(); + + // Touch task + static void _touch_task(void* param); + + // Refresh task + static void _refresh_task(void* param); + + // Helper to convert LVGL 1-bit buffer to e-paper format + void _convert_buffer_to_epaper(const uint8_t* lvgl_buf, uint8_t* epd_buf, size_t size); +}; diff --git a/main/main.cpp b/main/main.cpp index c790b20..8edc750 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -14,11 +14,9 @@ #include "common/queue_defs.h" #include "io/nvs_handler.h" #include "info/info.h" -#include "display/display.h" #include "display/eink_display_handler.h" #include "ui/ui_handler.h" #include "ui/app_registry.h" -#include "ui/apps/demo_app.h" #include "ui/apps/shutdown_app.h" #include "ui/apps/discord_app.h" #include "ui/apps/mtr_app.h" @@ -26,6 +24,7 @@ #include "esp_lvgl_port.h" #include "lvgl.h" #include "network.h" +#include // nvs storage namespaces, 15 characters max #define DEFAULT_STORAGE_NAMESPACE "storage" @@ -72,54 +71,56 @@ void app_main(void) { return esp_restart(); } // - KVStorageHandler* kv_storage_handler = new NVSStorageHandler( - DEFAULT_STORAGE_NAMESPACE - ); + // KVStorageHandler* kv_storage_handler = new NVSStorageHandler( + // DEFAULT_STORAGE_NAMESPACE + // ); - auto wifi_handler = std::make_unique( - std::unique_ptr(new NVSStorageHandler(WIFI_CREDENTIALS_STORAGE_NAMESPACE)) - ); - NetworkHandler* network_handler = new NetworkHandler(std::move(wifi_handler)); - EInkDisplayHandler* display_handler = new EInkDisplayHandler(system_event_group); + // auto wifi_handler = std::make_unique( + // std::unique_ptr(new NVSStorageHandler(WIFI_CREDENTIALS_STORAGE_NAMESPACE)) + // ); + // NetworkHandler* network_handler = new NetworkHandler(std::move(wifi_handler)); + EInkDisplayHandler* display_handler = new EInkDisplayHandler(); // - kv_storage_handler->init(system_event_group); - network_handler->init(system_event_group); + // kv_storage_handler->init(system_event_group); + // network_handler->init(system_event_group); // Initialize display and touch - display_handler->init(); - ESP_LOGV(TAG, "Starting touch task...\n"); - display_handler->start_touch_task(); - ESP_LOGV(TAG, "Touch task started.\n"); + display_handler->init_devices(system_event_group); + display_handler->clear_display(); + // ESP_LOGV(TAG, "Starting touch task...\n"); + // display_handler->start_touch_task(); + // ESP_LOGV(TAG, "Touch task started.\n"); // // LVGL tick timer - auto lvgl_tick_timer_callback = [](TimerHandle_t xTimer) { - lv_tick_inc(5); - }; - TickType_t lvgl_tick_period = pdMS_TO_TICKS(5); - if (lvgl_tick_period == 0) { - lvgl_tick_period = 1; // ensure at least 1 tick to avoid FreeRTOS assert - } - ESP_LOGV(TAG, "Creating LVGL tick timer with period %u ticks...\n", (unsigned)lvgl_tick_period); - TimerHandle_t lvgl_tick_timer = xTimerCreate( - "lvgl_tick_timer", - lvgl_tick_period, - pdTRUE, - NULL, - lvgl_tick_timer_callback - ); - if (lvgl_tick_timer == NULL) { - ESP_LOGE("Main", "Failed to create LVGL tick timer"); - vTaskDelay(5000 / portTICK_PERIOD_MS); - return esp_restart(); - } - ESP_LOGV(TAG, "Starting LVGL tick timer...\n"); - xTimerStart(lvgl_tick_timer, 0); + // auto lvgl_tick_timer_callback = [](TimerHandle_t xTimer) { + // lv_tick_inc(5); + // }; + // TickType_t lvgl_tick_period = pdMS_TO_TICKS(5); + // if (lvgl_tick_period == 0) { + // lvgl_tick_period = 1; // ensure at least 1 tick to avoid FreeRTOS assert + // } + // ESP_LOGV(TAG, "Creating LVGL tick timer with period %u ticks...\n", (unsigned)lvgl_tick_period); + // TimerHandle_t lvgl_tick_timer = xTimerCreate( + // "lvgl_tick_timer", + // lvgl_tick_period, + // pdTRUE, + // NULL, + // lvgl_tick_timer_callback + // ); + // if (lvgl_tick_timer == NULL) { + // ESP_LOGE("Main", "Failed to create LVGL tick timer"); + // vTaskDelay(5000 / portTICK_PERIOD_MS); + // return esp_restart(); + // } + // ESP_LOGV(TAG, "Starting LVGL tick timer...\n"); + // xTimerStart(lvgl_tick_timer, 0); // ESP_LOGI(TAG, "Waiting for system to be ready...\n"); xEventGroupWaitBits( system_event_group, - DISPLAY_READY_BIT | STORAGE_READY_BIT | NETWORK_READY_BIT, + // DISPLAY_READY_BIT | STORAGE_READY_BIT | NETWORK_READY_BIT, + DISPLAY_READY_BIT, // do not clear on exit, require explicit reset pdFALSE, pdTRUE, @@ -129,28 +130,106 @@ void app_main(void) { // Register apps with AppRegistry by creating their descriptors // Each descriptor will create and register the app instance - DemoAppDescriptor* demo_descriptor = new DemoAppDescriptor(); - ShutdownAppDescriptor* shutdown_descriptor = new ShutdownAppDescriptor(); - DiscordAppDescriptor::instance(); // Use singleton pattern for Discord app - MtrAppDescriptor* mtr_descriptor = new MtrAppDescriptor(); + // DemoAppDescriptor* demo_descriptor = new DemoAppDescriptor(); + // ShutdownAppDescriptor* shutdown_descriptor = new ShutdownAppDescriptor(); + // DiscordAppDescriptor::instance(); // Use singleton pattern for Discord app + // MtrAppDescriptor* mtr_descriptor = new MtrAppDescriptor(); // Pass network handler to MtrApp so it can fetch arrival data - MtrApp* mtr_app = dynamic_cast(mtr_descriptor->get_app_instance()); - if (mtr_app) { - mtr_app->set_network_handler(network_handler); - } + // MtrApp* mtr_app = dynamic_cast(mtr_descriptor->get_app_instance()); + // if (mtr_app) { + // mtr_app->set_network_handler(network_handler); + // } - ESP_LOGI(TAG, "Apps registered with AppRegistry\n"); + // ESP_LOGI(TAG, "Apps registered with AppRegistry\n"); // Initialize UI Handler (will render app icons from registry) - UIHandler ui_handler; - if (ui_handler.init() != ESP_OK) { - ESP_LOGE(TAG, "Failed to initialize UI handler"); - vTaskDelay(5000 / portTICK_PERIOD_MS); - return esp_restart(); + // UIHandler ui_handler; + // if (ui_handler.init() != ESP_OK) { + // ESP_LOGE(TAG, "Failed to initialize UI handler"); + // vTaskDelay(5000 / portTICK_PERIOD_MS); + // return esp_restart(); + // } + // ESP_LOGI(TAG, "UI handler initialized successfully\n"); + // ESP_LOGI(TAG, "Main screen displayed with app icons. Tap an icon to launch an app.\n"); + + // Run checkerboard draw in its own FreeRTOS task to avoid watchdog triggers + struct CheckerboardTaskParams { + EInkDisplayHandler* display_handler; + }; + auto checkerboard_task_fn = [](void* pvParameters) { + CheckerboardTaskParams* params = static_cast(pvParameters); + if (params != nullptr && params->display_handler != nullptr) { + // Add this task to the watchdog timer + esp_err_t wdt_err = esp_task_wdt_add(NULL); + if (wdt_err != ESP_OK) { + ESP_LOGW(TAG, "Failed to add checkerboard task to watchdog: %s", esp_err_to_name(wdt_err)); + } + + EInkDisplayHandler* display_handler = params->display_handler; + const size_t DISPLAY_BUFFER_SIZE = DISPLAY_WIDTH * DISPLAY_HEIGHT / 8; + uint8_t* framebuffer = new uint8_t[DISPLAY_BUFFER_SIZE]; + if (framebuffer == nullptr) { + ESP_LOGE(TAG, "Failed to allocate framebuffer for checkerboard task"); + if (wdt_err == ESP_OK) { + esp_task_wdt_delete(NULL); + } + vTaskDelete(NULL); + return; + } + // Create checkerboard pattern + for (size_t y = 0; y < DISPLAY_HEIGHT; y++) { + for (size_t x = 0; x < DISPLAY_WIDTH; x++) { + size_t byte_index = (y * DISPLAY_WIDTH + x) / 8; + size_t bit_index = 7 - (x % 8); + bool is_white = ((x / 20) % 2) == ((y / 20) % 2); + if (is_white) { + framebuffer[byte_index] |= (1 << bit_index); // Set bit to 1 for white + } else { + framebuffer[byte_index] &= ~(1 << bit_index); // Clear bit to 0 for black + } + } + // Yield and reset watchdog periodically + if (y % 50 == 0) { + if (wdt_err == ESP_OK) { + esp_task_wdt_reset(); + } + vTaskDelay(1 / portTICK_PERIOD_MS); + } + } + // Perform full write to display + esp_err_t err = display_handler->full_write(framebuffer); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Checkerboard full write failed: %s", esp_err_to_name(err)); + } else { + ESP_LOGI(TAG, "Checkerboard pattern displayed successfully."); + } + delete[] framebuffer; + + // Remove task from watchdog before deletion + if (wdt_err == ESP_OK) { + esp_task_wdt_delete(NULL); + } + } else { + ESP_LOGE(TAG, "Invalid parameters for checkerboard task"); + } + vTaskDelete(NULL); + }; + CheckerboardTaskParams* checker_params = new CheckerboardTaskParams(); + checker_params->display_handler = display_handler; + BaseType_t res = xTaskCreate( + checkerboard_task_fn, + "checkerboard_task", + 8192, + static_cast(checker_params), + tskIDLE_PRIORITY + 1, + NULL + ); + + if (res != pdPASS) { + ESP_LOGE(TAG, "Failed to create checkerboard task"); + delete checker_params; } - ESP_LOGI(TAG, "UI handler initialized successfully\n"); - ESP_LOGI(TAG, "Main screen displayed with app icons. Tap an icon to launch an app.\n"); // wait for shutdown signal ESP_LOGI(TAG, "Waiting for shutdown signal...\n"); @@ -165,17 +244,17 @@ void app_main(void) { ESP_LOGI(TAG, "Shutdown signal received. Cleaning up...\n"); // Show shutdown screen using the shutdown descriptor's app instance - ShutdownApp* shutdown_app = dynamic_cast(shutdown_descriptor->get_app_instance()); - if (shutdown_app) { - ui_handler.switch_app(shutdown_app); - } + // ShutdownApp* shutdown_app = dynamic_cast(shutdown_descriptor->get_app_instance()); + // if (shutdown_app) { + // ui_handler.switch_app(shutdown_app); + // } vTaskDelay(1000 / portTICK_PERIOD_MS); // Display shutdown message briefly // Cleanup - ui_handler.deinit(); - delete demo_descriptor; - delete shutdown_descriptor; - delete mtr_descriptor; + // ui_handler.deinit(); + // delete demo_descriptor; + // delete shutdown_descriptor; + // delete mtr_descriptor; delete display_handler; vSemaphoreDelete(lvgl_mutex); vEventGroupDelete(system_event_group); diff --git a/main/ui/apps/demo_app.cpp b/main/ui/apps/demo_app.cpp deleted file mode 100644 index 5d65300..0000000 --- a/main/ui/apps/demo_app.cpp +++ /dev/null @@ -1,151 +0,0 @@ -#include "apps/demo_app.h" -#include "esp_log.h" - -#define TAG "DemoApp" - -esp_err_t DemoApp::init(lv_obj_t* container) { - if (!container) { - ESP_LOGE(TAG, "Container is null"); - return ESP_ERR_INVALID_ARG; - } - - _container = container; - ESP_LOGI(TAG, "Initializing demo app..."); - - // Header label - _label_header = lv_label_create(_container); - lv_label_set_text(_label_header, "Counter & Brightness Demo"); - lv_obj_set_style_text_color(_label_header, lv_color_black(), 0); - lv_obj_align(_label_header, LV_ALIGN_TOP_MID, 0, 20); - - // Counter label - _label_counter = lv_label_create(_container); - lv_label_set_text(_label_counter, "Count: 0"); - lv_obj_set_style_text_color(_label_counter, lv_color_black(), 0); - lv_obj_align(_label_counter, LV_ALIGN_CENTER, 0, -80); - - // Increment button - _btn_increment = lv_btn_create(_container); - lv_obj_set_size(_btn_increment, 150, 60); - lv_obj_align(_btn_increment, LV_ALIGN_CENTER, -100, -20); - lv_obj_add_event_cb(_btn_increment, btn_increment_event_cb, LV_EVENT_CLICKED, this); - - lv_obj_t* label_inc = lv_label_create(_btn_increment); - lv_label_set_text(label_inc, "+"); - lv_obj_set_style_text_color(label_inc, lv_color_black(), 0); - lv_obj_center(label_inc); - - // Decrement button - _btn_decrement = lv_btn_create(_container); - lv_obj_set_size(_btn_decrement, 150, 60); - lv_obj_align(_btn_decrement, LV_ALIGN_CENTER, 100, -20); - lv_obj_add_event_cb(_btn_decrement, btn_decrement_event_cb, LV_EVENT_CLICKED, this); - - lv_obj_t* label_dec = lv_label_create(_btn_decrement); - lv_label_set_text(label_dec, "-"); - lv_obj_set_style_text_color(label_dec, lv_color_black(), 0); - lv_obj_center(label_dec); - - // Slider - _slider_brightness = lv_slider_create(_container); - lv_obj_set_width(_slider_brightness, 400); - lv_obj_align(_slider_brightness, LV_ALIGN_CENTER, 0, 80); - lv_slider_set_range(_slider_brightness, 0, 100); - lv_slider_set_value(_slider_brightness, 50, LV_ANIM_OFF); - lv_obj_add_event_cb(_slider_brightness, slider_event_cb, LV_EVENT_VALUE_CHANGED, this); - - // Slider value label - _label_slider_value = lv_label_create(_container); - lv_label_set_text(_label_slider_value, "Brightness: 50%"); - lv_obj_set_style_text_color(_label_slider_value, lv_color_black(), 0); - lv_obj_align(_label_slider_value, LV_ALIGN_CENTER, 0, 130); - - // Info text at bottom - lv_obj_t* label_info = lv_label_create(_container); - lv_label_set_text(label_info, "Touch buttons and slider to test"); - lv_obj_set_style_text_color(label_info, lv_color_black(), 0); - lv_obj_align(label_info, LV_ALIGN_BOTTOM_MID, 0, -20); - - ESP_LOGI(TAG, "Demo app initialized successfully"); - return ESP_OK; -} - -esp_err_t DemoApp::deinit(void) { - ESP_LOGI(TAG, "Deinitializing demo app"); - - // All widgets will be automatically deleted when container is cleaned - _label_header = nullptr; - _label_counter = nullptr; - _btn_increment = nullptr; - _btn_decrement = nullptr; - _slider_brightness = nullptr; - _label_slider_value = nullptr; - _counter = 0; - - return ESP_OK; -} - -std::string DemoApp::get_name(void) const { - return "Demo"; -} - -void DemoApp::btn_increment_event_cb(lv_event_t* e) { - lv_event_code_t code = lv_event_get_code(e); - if (code == LV_EVENT_CLICKED) { - DemoApp* app = (DemoApp*)lv_event_get_user_data(e); - if (app) { - app->_counter++; - lv_label_set_text_fmt(app->_label_counter, "Count: %d", app->_counter); - ESP_LOGI(TAG, "Increment button clicked, count: %d", app->_counter); - } - } -} - -void DemoApp::btn_decrement_event_cb(lv_event_t* e) { - lv_event_code_t code = lv_event_get_code(e); - if (code == LV_EVENT_CLICKED) { - DemoApp* app = (DemoApp*)lv_event_get_user_data(e); - if (app) { - app->_counter--; - lv_label_set_text_fmt(app->_label_counter, "Count: %d", app->_counter); - ESP_LOGI(TAG, "Decrement button clicked, count: %d", app->_counter); - } - } -} - -void DemoApp::slider_event_cb(lv_event_t* e) { - lv_event_code_t code = lv_event_get_code(e); - if (code == LV_EVENT_VALUE_CHANGED) { - DemoApp* app = (DemoApp*)lv_event_get_user_data(e); - if (app) { - lv_obj_t* slider = (lv_obj_t*)lv_event_get_target(e); - int32_t value = lv_slider_get_value(slider); - lv_label_set_text_fmt(app->_label_slider_value, "Brightness: %d%%", (int)value); - ESP_LOGI(TAG, "Slider value changed: %d", (int)value); - } - } -} - -// DemoAppDescriptor implementation -DemoApp* DemoAppDescriptor::_app_instance = nullptr; - -DemoAppDescriptor::DemoAppDescriptor() - : AppDescriptor("Demo", nullptr) { - // Create singleton app instance - if (!_app_instance) { - _app_instance = new DemoApp(); - } - - // Register with AppRegistry - AppRegistry::instance().register_app(this); - ESP_LOGI(TAG, "DemoApp registered with AppRegistry"); -} - -void DemoAppDescriptor::draw_icon(lv_obj_t* parent) { - // Create a simple icon with text and a symbol - lv_obj_t* icon_label = lv_label_create(parent); - lv_label_set_text(icon_label, LV_SYMBOL_SETTINGS "\nDemo"); - lv_obj_set_style_text_color(icon_label, lv_color_white(), 0); - lv_obj_set_style_text_align(icon_label, LV_TEXT_ALIGN_CENTER, 0); - lv_obj_center(icon_label); -} diff --git a/main/ui/apps/demo_app.h b/main/ui/apps/demo_app.h deleted file mode 100644 index afa9c62..0000000 --- a/main/ui/apps/demo_app.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once - -#include "ui/ui_app.h" -#include "ui/app_registry.h" - -/** - * @brief Demo application - counter and brightness slider - * - * Demonstrates interactive UI components with touch input: - * - Counter display with increment/decrement buttons - * - Brightness slider - */ -class DemoApp : public UIApp { -public: - DemoApp() = default; - virtual ~DemoApp() = default; - - esp_err_t init(lv_obj_t* container) override; - esp_err_t deinit(void) override; - std::string get_name(void) const override; - -private: - // UI components - lv_obj_t* _label_header= nullptr; - lv_obj_t* _label_counter= nullptr; - lv_obj_t* _btn_increment= nullptr; - lv_obj_t* _btn_decrement= nullptr; - lv_obj_t* _slider_brightness= nullptr; - lv_obj_t* _label_slider_value= nullptr; - - // State - int _counter= 0; - - // Event callbacks - static void btn_increment_event_cb(lv_event_t* e); - static void btn_decrement_event_cb(lv_event_t* e); - static void slider_event_cb(lv_event_t* e); -}; - -/** - * @brief AppDescriptor for DemoApp - * - * Registers the demo app with the AppRegistry and provides - * icon rendering functionality. - */ -class DemoAppDescriptor : public AppDescriptor { -public: - DemoAppDescriptor(); - void draw_icon(lv_obj_t* parent) override; - -private: - static DemoApp* _app_instance; -}; diff --git a/main/ui/apps/discord_app.cpp b/main/ui/apps/discord_app.cpp index c416747..954aba6 100644 --- a/main/ui/apps/discord_app.cpp +++ b/main/ui/apps/discord_app.cpp @@ -126,62 +126,86 @@ bool DiscordApp::on_back_button_pressed() { // ============================================================================ void DiscordApp::build_main_page(lv_obj_t* page) { - // Status icon (large, centered) - status_icon_label_ = lv_label_create(page); - lv_label_set_text(status_icon_label_, LV_SYMBOL_MUTE); - // Using default font (only montserrat_14 is enabled) - lv_obj_align(status_icon_label_, LV_ALIGN_CENTER, 0, -80); + // Set up main page with flex column layout + lv_obj_set_flex_flow(page, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(page, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_all(page, 10, 0); - // Status text - status_text_label_ = lv_label_create(page); - lv_label_set_text(status_text_label_, "Unknown Status"); - // Using default font - lv_obj_align(status_text_label_, LV_ALIGN_CENTER, 0, -20); - - // Mute button - mute_button_ = lv_btn_create(page); - lv_obj_set_size(mute_button_, 200, 60); - lv_obj_align(mute_button_, LV_ALIGN_CENTER, 0, 50); - lv_obj_add_event_cb(mute_button_, on_mute_button_clicked, LV_EVENT_CLICKED, this); - - lv_obj_t* mute_label = lv_label_create(mute_button_); - lv_label_set_text(mute_label, "MUTE"); - // Using default font - lv_obj_center(mute_label); - - // Settings button (gear icon in corner) - lv_obj_t* settings_btn = lv_btn_create(page); - lv_obj_set_size(settings_btn, 60, 60); - lv_obj_align(settings_btn, LV_ALIGN_BOTTOM_RIGHT, -10, -10); - lv_obj_add_event_cb(settings_btn, on_settings_button_clicked, LV_EVENT_CLICKED, this); - - lv_obj_t* settings_icon = lv_label_create(settings_btn); - lv_label_set_text(settings_icon, LV_SYMBOL_SETTINGS); - // Using default font - lv_obj_center(settings_icon); - - // Error notification (hidden by default) + // === Top Section: Error Notification === error_notification_ = lv_obj_create(page); - lv_obj_set_size(error_notification_, 250, 50); - lv_obj_align(error_notification_, LV_ALIGN_TOP_MID, 0, 10); + lv_obj_set_width(error_notification_, LV_PCT(90)); + lv_obj_set_height(error_notification_, LV_SIZE_CONTENT); lv_obj_set_style_bg_color(error_notification_, lv_color_hex(0xFF0000), 0); lv_obj_set_style_bg_opa(error_notification_, LV_OPA_70, 0); + lv_obj_set_style_pad_all(error_notification_, 10, 0); + lv_obj_set_style_radius(error_notification_, 8, 0); lv_obj_add_flag(error_notification_, LV_OBJ_FLAG_HIDDEN); + lv_obj_set_flex_flow(error_notification_, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(error_notification_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_t* error_label = lv_label_create(error_notification_); lv_label_set_text(error_label, LV_SYMBOL_WARNING " Connection Lost"); lv_obj_set_style_text_color(error_label, lv_color_white(), 0); - lv_obj_center(error_label); - // Show config prompt if not configured + // === Center Section: Main Content === + lv_obj_t* center_container = lv_obj_create(page); + lv_obj_set_size(center_container, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(center_container, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(center_container, 0, 0); + lv_obj_set_style_pad_all(center_container, 0, 0); + lv_obj_set_flex_flow(center_container, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(center_container, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_row(center_container, 15, 0); + lv_obj_set_flex_grow(center_container, 1); + + // Status icon (large, centered) + status_icon_label_ = lv_label_create(center_container); + lv_label_set_text(status_icon_label_, LV_SYMBOL_MUTE); + + // Status text + status_text_label_ = lv_label_create(center_container); + lv_label_set_text(status_text_label_, "Unknown Status"); + + // Mute button + mute_button_ = lv_btn_create(center_container); + lv_obj_set_size(mute_button_, 200, 60); + lv_obj_add_event_cb(mute_button_, on_mute_button_clicked, LV_EVENT_CLICKED, this); + + lv_obj_t* mute_label = lv_label_create(mute_button_); + lv_label_set_text(mute_label, "MUTE"); + lv_obj_center(mute_label); + + // === Bottom Section: Settings and Config Prompt === + lv_obj_t* bottom_container = lv_obj_create(page); + lv_obj_set_size(bottom_container, LV_PCT(100), LV_SIZE_CONTENT); + lv_obj_set_style_bg_opa(bottom_container, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(bottom_container, 0, 0); + lv_obj_set_style_pad_all(bottom_container, 0, 0); + lv_obj_set_flex_flow(bottom_container, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(bottom_container, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + // Config prompt (left side) if (!settings_configured_) { - lv_obj_t* config_prompt = lv_label_create(page); + lv_obj_t* config_prompt = lv_label_create(bottom_container); lv_label_set_text(config_prompt, "Tap " LV_SYMBOL_SETTINGS " to configure"); - // Using default font lv_obj_set_style_text_color(config_prompt, lv_color_hex(0x888888), 0); - lv_obj_align(config_prompt, LV_ALIGN_BOTTOM_LEFT, 10, -10); + } else { + // Empty spacer if configured + lv_obj_t* spacer = lv_obj_create(bottom_container); + lv_obj_set_size(spacer, 0, 0); + lv_obj_set_style_bg_opa(spacer, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(spacer, 0, 0); } + // Settings button (right side) + lv_obj_t* settings_btn = lv_btn_create(bottom_container); + lv_obj_set_size(settings_btn, 60, 60); + lv_obj_add_event_cb(settings_btn, on_settings_button_clicked, LV_EVENT_CLICKED, this); + + lv_obj_t* settings_icon = lv_label_create(settings_btn); + lv_label_set_text(settings_icon, LV_SYMBOL_SETTINGS); + lv_obj_center(settings_icon); + // Update display with current state update_status_display(); } diff --git a/main/ui/root_layout.cpp b/main/ui/root_layout.cpp index fd745a3..1e7c0a8 100644 --- a/main/ui/root_layout.cpp +++ b/main/ui/root_layout.cpp @@ -14,6 +14,9 @@ #define NAV_BAR_HEIGHT 50 #define APP_CONTAINER_HEIGHT (DISPLAY_HEIGHT - HEADER_HEIGHT - NAV_BAR_HEIGHT) +// forward-declare local event callback +static void on_home_button_clicked(lv_event_t* event); + RootLayout::RootLayout(UIHandler* ui_handler) : _ui_handler(ui_handler) { } @@ -48,35 +51,76 @@ esp_err_t RootLayout::deinit(void) { } esp_err_t RootLayout::create_layout(lv_obj_t* parent) { - // Create header (top) + // Configure parent as flexbox column layout + lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(parent, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + lv_obj_set_style_pad_all(parent, 0, 0); + lv_obj_set_style_pad_gap(parent, 0, 0); + + // Create header (top, fixed height) _header = lv_obj_create(parent); - lv_obj_set_size(_header, DISPLAY_WIDTH, HEADER_HEIGHT); - lv_obj_set_pos(_header, 0, 0); - lv_obj_set_style_bg_color(_header, lv_color_hex(0x333333), 0); + lv_obj_set_width(_header, lv_pct(100)); + lv_obj_set_height(_header, HEADER_HEIGHT); + lv_obj_set_style_bg_color(_header, lv_color_hex(0xFFFFFF), 0); lv_obj_set_style_border_width(_header, 0, 0); + lv_obj_set_style_border_color(_header, lv_color_hex(0x000000), 0); + lv_obj_set_style_border_width(_header, 1, LV_BORDER_SIDE_BOTTOM); + lv_obj_set_style_pad_all(_header, 0, 0); + lv_obj_set_style_radius(_header, 0, 0); _header_label = lv_label_create(_header); lv_label_set_text(_header_label, "App"); - lv_obj_set_style_text_color(_header_label, lv_color_white(), 0); + lv_obj_set_style_text_color(_header_label, lv_color_black(), 0); lv_obj_align(_header_label, LV_ALIGN_LEFT_MID, 10, 0); - // Create app container (middle) + // Create app container (middle, flexible - grows to fill available space) _app_container = lv_obj_create(parent); - lv_obj_set_size(_app_container, DISPLAY_WIDTH, APP_CONTAINER_HEIGHT); - lv_obj_set_pos(_app_container, 0, HEADER_HEIGHT); + lv_obj_set_width(_app_container, lv_pct(100)); + lv_obj_set_flex_grow(_app_container, 1); lv_obj_set_style_bg_color(_app_container, lv_color_white(), 0); lv_obj_set_style_border_width(_app_container, 0, 0); lv_obj_set_style_pad_all(_app_container, 0, 0); + lv_obj_set_style_radius(_app_container, 0, 0); - // Create navigation bar (bottom) + // Create navigation bar (bottom, fixed height) _nav_bar = lv_obj_create(parent); - lv_obj_set_size(_nav_bar, DISPLAY_WIDTH, NAV_BAR_HEIGHT); - lv_obj_set_pos(_nav_bar, 0, HEADER_HEIGHT + APP_CONTAINER_HEIGHT); - lv_obj_set_style_bg_color(_nav_bar, lv_color_hex(0x333333), 0); - lv_obj_set_style_border_width(_nav_bar, 0, 0); + lv_obj_set_width(_nav_bar, lv_pct(100)); + lv_obj_set_height(_nav_bar, NAV_BAR_HEIGHT); + lv_obj_set_style_bg_color(_nav_bar, lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_border_color(_nav_bar, lv_color_hex(0x000000), 0); + lv_obj_set_style_border_width(_nav_bar, 1, LV_BORDER_SIDE_TOP); + lv_obj_set_style_pad_all(_nav_bar, 5, 0); + lv_obj_set_style_radius(_nav_bar, 0, 0); - ESP_LOGI(TAG, "Layout created: Header=%d, AppContainer=%d, NavBar=%d", - HEADER_HEIGHT, APP_CONTAINER_HEIGHT, NAV_BAR_HEIGHT); + // Configure nav bar as flexbox row layout with space-between + lv_obj_set_flex_flow(_nav_bar, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(_nav_bar, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + + // Create back button (aligned to start by flex layout) + _back_button = lv_btn_create(_nav_bar); + lv_obj_set_size(_back_button, 60, NAV_BAR_HEIGHT - 10); + lv_obj_set_style_bg_color(_back_button, lv_color_hex(0x555555), 0); + lv_obj_add_event_cb(_back_button, on_back_button_clicked, LV_EVENT_CLICKED, _ui_handler); + lv_obj_add_flag(_back_button, LV_OBJ_FLAG_HIDDEN); + + // Add back arrow label + lv_obj_t* back_label = lv_label_create(_back_button); + lv_label_set_text(back_label, LV_SYMBOL_LEFT); + lv_obj_set_style_text_color(back_label, lv_color_black(), 0); + lv_obj_align(back_label, LV_ALIGN_CENTER, 0, 0); + + // Create home button (aligned to end by flex layout) + lv_obj_t* home_button = lv_btn_create(_nav_bar); + lv_obj_set_size(home_button, 60, NAV_BAR_HEIGHT - 10); + lv_obj_set_style_bg_color(home_button, lv_color_hex(0x555555), 0); + lv_obj_t* home_label = lv_label_create(home_button); + lv_label_set_text(home_label, LV_SYMBOL_HOME); + lv_obj_set_style_text_color(home_label, lv_color_white(), 0); + lv_obj_align(home_label, LV_ALIGN_CENTER, 0, 0); + lv_obj_add_event_cb(home_button, on_home_button_clicked, LV_EVENT_CLICKED, _ui_handler); + + ESP_LOGI(TAG, "Layout created with flexible design: Header=%d, NavBar=%d", + HEADER_HEIGHT, NAV_BAR_HEIGHT); return ESP_OK; } @@ -99,8 +143,12 @@ esp_err_t RootLayout::render_app_icons(void) { return ESP_FAIL; } - // Clear existing nav bar content - lv_obj_clean(_nav_bar); + // Clear existing app container content (icons are rendered in the app area) + if (!_app_container) { + ESP_LOGE(TAG, "App container not initialized"); + return ESP_FAIL; + } + lv_obj_clean(_app_container); // Get all registered apps from registry const auto& app_descriptors = AppRegistry::instance().get_app_descriptors(); @@ -114,56 +162,39 @@ esp_err_t RootLayout::render_app_icons(void) { return ESP_OK; } - ESP_LOGI(TAG, "Rendering %d app icons", app_descriptors.size()); + ESP_LOGI(TAG, "Rendering %d app icons", (int)app_descriptors.size()); - // Calculate icon spacing + // Calculate icon spacing inside the app container int icon_count = app_descriptors.size(); + int icon_width = 96; + int icon_height = 96; int icon_spacing = DISPLAY_WIDTH / (icon_count + 1); int x_offset = icon_spacing; + int y_offset = (APP_CONTAINER_HEIGHT - icon_height) / 2; - // Render each app icon + // Render each app icon into the app container for (size_t i = 0; i < app_descriptors.size(); i++) { AppDescriptor* descriptor = app_descriptors[i]; - // Create a container for this app icon - lv_obj_t* icon_container = lv_obj_create(_nav_bar); - lv_obj_set_size(icon_container, icon_spacing - 10, NAV_BAR_HEIGHT - 10); - lv_obj_set_pos(icon_container, x_offset - (icon_spacing - 10) / 2, 5); + lv_obj_t* icon_container = lv_obj_create(_app_container); + lv_obj_set_size(icon_container, icon_width, icon_height); + lv_obj_set_pos(icon_container, x_offset - icon_width / 2, y_offset); lv_obj_set_style_bg_opa(icon_container, LV_OPA_TRANSP, 0); - lv_obj_set_style_border_width(icon_container, 0, 0); lv_obj_set_style_pad_all(icon_container, 0, 0); + // add a border for debugging + lv_obj_set_style_border_color(icon_container, lv_color_hex(0x000000), 0); + lv_obj_set_style_border_width(icon_container, 1, 0); - // Store both the descriptor and ui_handler as user data lv_obj_set_user_data(icon_container, descriptor); - // Let the descriptor draw its icon descriptor->draw_icon(icon_container); - // Add click event handler lv_obj_add_flag(icon_container, LV_OBJ_FLAG_CLICKABLE); lv_obj_add_event_cb(icon_container, on_app_icon_clicked, LV_EVENT_CLICKED, _ui_handler); x_offset += icon_spacing; } - // Create back button on the left side of the nav bar - _back_button = lv_btn_create(_nav_bar); - lv_obj_set_size(_back_button, 60, NAV_BAR_HEIGHT - 10); - lv_obj_set_pos(_back_button, 5, 5); - lv_obj_set_style_bg_color(_back_button, lv_color_hex(0x555555), 0); - - // Add back arrow label - lv_obj_t* back_label = lv_label_create(_back_button); - lv_label_set_text(back_label, LV_SYMBOL_LEFT); - lv_obj_set_style_text_color(back_label, lv_color_white(), 0); - lv_obj_align(back_label, LV_ALIGN_CENTER, 0, 0); - - // Add click event handler - lv_obj_add_event_cb(_back_button, on_back_button_clicked, LV_EVENT_CLICKED, _ui_handler); - - // Initially hide back button (shown when app is active) - lv_obj_add_flag(_back_button, LV_OBJ_FLAG_HIDDEN); - return ESP_OK; } @@ -180,7 +211,9 @@ void RootLayout::hide_back_button(void) { } void RootLayout::on_app_icon_clicked(lv_event_t* event) { - lv_obj_t* icon_container = static_cast(lv_event_get_target(event)); + // Use the current target (the object the callback was attached to) + // instead of the event target, which may be a child (like a label). + lv_obj_t* icon_container = static_cast(lv_event_get_current_target(event)); UIHandler* handler = static_cast(lv_event_get_user_data(event)); AppDescriptor* descriptor = static_cast(lv_obj_get_user_data(icon_container)); @@ -218,3 +251,14 @@ void RootLayout::on_back_button_clicked(lv_event_t* event) { handler->return_to_main_screen(); } } + +static void on_home_button_clicked(lv_event_t* event) { + UIHandler* handler = static_cast(lv_event_get_user_data(event)); + + if (!handler) { + ESP_LOGE(TAG, "Invalid handler in home button click"); + return; + } + + handler->return_to_main_screen(); +}