From e163392532524bd3386f8198da96236da52dd3a9 Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Tue, 20 Jan 2026 20:15:05 +0800 Subject: [PATCH 01/16] Remove exception throwing --- main/main.cpp | 240 +++++++++++++++++++++++--------------------------- 1 file changed, 112 insertions(+), 128 deletions(-) diff --git a/main/main.cpp b/main/main.cpp index d438bb1..fea18b0 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -14,6 +14,7 @@ #include "esp_chip_info.h" #include "esp_flash.h" #include "esp_system.h" +#include "esp_log.h" // #include "common/constants.h" @@ -40,148 +41,131 @@ void init_queues( void app_main(void) { display_chip_info(); - try { - QueueHandle_t touch_event_queue = NULL; - EventGroupHandle_t system_event_group = NULL, system_lifecycle_event_group = NULL; + QueueHandle_t touch_event_queue = NULL; + EventGroupHandle_t system_event_group = NULL, system_lifecycle_event_group = NULL; - init_queues(touch_event_queue, system_event_group, system_lifecycle_event_group); - if (touch_event_queue == NULL || system_event_group == NULL || system_lifecycle_event_group == NULL) { - throw std::runtime_error("Failed to create one or more queues/event groups"); - } - printf("Queues initialized.\n"); - SemaphoreHandle_t lvgl_mutex = xSemaphoreCreateMutex(); - if (lvgl_mutex == NULL) { - throw std::runtime_error("Failed to create LVGL mutex"); - } - // - WifiHandler wifi_handler( - new NVSStorageHandler(WIFI_CREDENTIALS_STORAGE_NAMESPACE) - ); - NetworkHandler* network_handler = new NetworkHandler(std::move(wifi_handler)); - KVStorageHandler* kv_storage_handler = new NVSStorageHandler( - DEFAULT_STORAGE_NAMESPACE - ); - DisplayHandler* display_handler = new EInkDisplayHandler(touch_event_queue, lvgl_mutex); - TouchHandler* touch_handler = new EInkTouchHandler(touch_event_queue); - // - network_handler->init(system_event_group); - kv_storage_handler->init(system_event_group); - display_handler->init(system_event_group); - touch_handler->init(system_event_group); - // - // LVGL tick timer - auto lvgl_tick_timer_callback = [](TimerHandle_t xTimer) { - lv_tick_inc(5); - }; - TimerHandle_t lvgl_tick_timer = xTimerCreate( - "lvgl_tick_timer", - pdMS_TO_TICKS(5), - pdTRUE, - NULL, - lvgl_tick_timer_callback - ); - if (lvgl_tick_timer == NULL) { - throw std::runtime_error("Failed to create LVGL tick timer"); - } - xTimerStart(lvgl_tick_timer, 0); + init_queues(touch_event_queue, system_event_group, system_lifecycle_event_group); + if (touch_event_queue == NULL || system_event_group == NULL || system_lifecycle_event_group == NULL) { + ESP_LOGE("Main", "Failed to create one or more queues/event groups"); + vTaskDelay(5000 / portTICK_PERIOD_MS); + return esp_restart(); + } + printf("Queues initialized.\n"); + SemaphoreHandle_t lvgl_mutex = xSemaphoreCreateMutex(); + if (lvgl_mutex == NULL) { + ESP_LOGE("Main", "Failed to create LVGL mutex"); + vTaskDelay(5000 / portTICK_PERIOD_MS); + return esp_restart(); + } + // + WifiHandler wifi_handler( + new NVSStorageHandler(WIFI_CREDENTIALS_STORAGE_NAMESPACE) + ); + NetworkHandler* network_handler = new NetworkHandler(std::move(wifi_handler)); + KVStorageHandler* kv_storage_handler = new NVSStorageHandler( + DEFAULT_STORAGE_NAMESPACE + ); + DisplayHandler* display_handler = new EInkDisplayHandler(touch_event_queue, lvgl_mutex); + TouchHandler* touch_handler = new EInkTouchHandler(touch_event_queue); + // + network_handler->init(system_event_group); + kv_storage_handler->init(system_event_group); + display_handler->init(system_event_group); + touch_handler->init(system_event_group); + // + // LVGL tick timer + auto lvgl_tick_timer_callback = [](TimerHandle_t xTimer) { + lv_tick_inc(5); + }; + TimerHandle_t lvgl_tick_timer = xTimerCreate( + "lvgl_tick_timer", + pdMS_TO_TICKS(5), + 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(); + } + xTimerStart(lvgl_tick_timer, 0); - // - printf("Waiting for system to be ready...\n"); - xEventGroupWaitBits( - system_event_group, - DISPLAY_READY_BIT | TOUCH_CALIBRATED_BIT | STORAGE_READY_BIT | NETWORK_READY_BIT, - // do not clear on exit, require explicit reset - pdFALSE, - pdTRUE, - portMAX_DELAY - ); - printf("System is ready. Starting main application...\n"); - // starting event loops - display_handler->start_event_loop(); - touch_handler->start_event_loop(); - // wait for shutdown signal + // + printf("Waiting for system to be ready...\n"); + xEventGroupWaitBits( + system_event_group, + DISPLAY_READY_BIT | TOUCH_CALIBRATED_BIT | STORAGE_READY_BIT | NETWORK_READY_BIT, + // do not clear on exit, require explicit reset + pdFALSE, + pdTRUE, + portMAX_DELAY + ); + printf("System is ready. Starting main application...\n"); + // starting event loops + display_handler->start_event_loop(); + touch_handler->start_event_loop(); + // wait for shutdown signal + EventBits_t bits = xEventGroupWaitBits( + system_lifecycle_event_group, + SYSTEM_SHUTDOWN_BIT | SYSTEM_RESTART_BIT, + // do not clear on exit, require explicit reset + pdFALSE, + pdFALSE, + portMAX_DELAY + ); + printf("Shutdown signal received. Cleaning up...\n"); + + // cleanup + shutdown_display_handlerFunc shutdown_display_handler = display_handler->get_shutdown_display_handler(); + restart_display_handlerFunc restart_display_handler = display_handler->get_restart_display_handler(); + delete display_handler; + delete touch_handler; + vSemaphoreDelete(lvgl_mutex); + vEventGroupDelete(system_event_group); + vQueueDelete(touch_event_queue); + printf("Cleanup complete.\n"); + + // handle shutdown or restart + if (bits & SYSTEM_SHUTDOWN_BIT) { + if (shutdown_display_handler != nullptr) { + printf("Calling display shutdown handler...\n"); + shutdown_display_handler(); + } else { + printf("No display shutdown handler to call.\n"); + } + printf("System is shutting down.\n"); + fflush(stdout); + // wait for start bit to be set again if future restart is desired, else expect manual power cycle EventBits_t bits = xEventGroupWaitBits( system_lifecycle_event_group, - SYSTEM_SHUTDOWN_BIT | SYSTEM_RESTART_BIT, - // do not clear on exit, require explicit reset + SYSTEM_START_BIT, pdFALSE, pdFALSE, portMAX_DELAY ); - printf("Shutdown signal received. Cleaning up...\n"); - - // cleanup - shutdown_display_handlerFunc shutdown_display_handler = display_handler->get_shutdown_display_handler(); - restart_display_handlerFunc restart_display_handler = display_handler->get_restart_display_handler(); - delete display_handler; - delete touch_handler; - vSemaphoreDelete(lvgl_mutex); - vEventGroupDelete(system_event_group); - vQueueDelete(touch_event_queue); - printf("Cleanup complete.\n"); - - // handle shutdown or restart - if (bits & SYSTEM_SHUTDOWN_BIT) { - if (shutdown_display_handler != nullptr) { - printf("Calling display shutdown handler...\n"); - shutdown_display_handler(); - } else { - printf("No display shutdown handler to call.\n"); - } - printf("System is shutting down.\n"); - fflush(stdout); - // wait for start bit to be set again if future restart is desired, else expect manual power cycle - EventBits_t bits = xEventGroupWaitBits( - system_lifecycle_event_group, - SYSTEM_START_BIT, - pdFALSE, - pdFALSE, - portMAX_DELAY - ); - if (bits & SYSTEM_START_BIT) { - printf("SYSTEM_START_BIT received, restarting system.\n"); - } else { - printf("No restart signal received, waiting for manual power cycle.\n"); - while (true) { - vTaskDelay(portMAX_DELAY); - } - } - } else if (bits & SYSTEM_RESTART_BIT) { - if (restart_display_handler != nullptr) { - printf("Calling display restart handler...\n"); - restart_display_handler(); - } else { - printf("No display restart handler to call.\n"); - } - printf("System is restarting.\n"); - fflush(stdout); + if (bits & SYSTEM_START_BIT) { + printf("SYSTEM_START_BIT received, restarting system.\n"); } else { - printf("Unknown shutdown signal received. Restarting by default.\n"); - fflush(stdout); + printf("No restart signal received, waiting for manual power cycle.\n"); + while (true) { + vTaskDelay(portMAX_DELAY); + } } - - return esp_restart(); - } - catch (const std::exception& e) { - printf("Exception occurred during initialization: %s\n", e.what()); - printf("System will restart due to the error.\n"); - for (int i = 5; i >= 0; --i) { - printf("Restarting in %d seconds...\n", i); - vTaskDelay(1000 / portTICK_PERIOD_MS); + } else if (bits & SYSTEM_RESTART_BIT) { + if (restart_display_handler != nullptr) { + printf("Calling display restart handler...\n"); + restart_display_handler(); + } else { + printf("No display restart handler to call.\n"); } - printf("Restarting now.\n"); + printf("System is restarting.\n"); + fflush(stdout); + } else { + printf("Unknown shutdown signal received. Restarting by default.\n"); fflush(stdout); - return esp_restart(); } - printf("Reached end of app_main unexpectedly.\n"); - printf("System will restart in 10 seconds...\n"); - for (int i = 10; i >= 0; --i) { - printf("Restarting in %d seconds...\n", i); - vTaskDelay(1000 / portTICK_PERIOD_MS); - } - printf("Restarting now.\n"); - fflush(stdout); return esp_restart(); } From fae9d30e3ad546faa567c24c46b57c639ebfb003 Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Tue, 20 Jan 2026 20:15:44 +0800 Subject: [PATCH 02/16] feat: refactor header files and add info for psram --- main/display/display.h | 8 +++++++- main/info/info.cpp | 17 +++++++++++++++-- main/info/info.h | 9 --------- main/touch/touch.h | 8 +++++++- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/main/display/display.h b/main/display/display.h index 7f3921f..b31efe7 100644 --- a/main/display/display.h +++ b/main/display/display.h @@ -1,4 +1,10 @@ -#include "info/info.h" +#pragma once + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" typedef void (*shutdown_display_handlerFunc)(void); typedef void (*restart_display_handlerFunc)(void); diff --git a/main/info/info.cpp b/main/info/info.cpp index 9e52d13..421ae2e 100644 --- a/main/info/info.cpp +++ b/main/info/info.cpp @@ -1,3 +1,12 @@ +#include +#include +#include "sdkconfig.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_chip_info.h" +#include "esp_flash.h" +#include "esp_system.h" +#include "esp_psram.h" #include "info.h" void display_chip_info() { @@ -6,13 +15,15 @@ void display_chip_info() { esp_chip_info_t chip_info; uint32_t flash_size; esp_chip_info(&chip_info); - printf("This is %s chip with %d CPU core(s), %s%s%s%s, ", + printf("This is %s chip with %d CPU core(s), %s%s%s%s%s, ", CONFIG_IDF_TARGET, chip_info.cores, (chip_info.features & CHIP_FEATURE_WIFI_BGN) ? "WiFi/" : "", (chip_info.features & CHIP_FEATURE_BT) ? "BT" : "", (chip_info.features & CHIP_FEATURE_BLE) ? "BLE" : "", - (chip_info.features & CHIP_FEATURE_IEEE802154) ? ", 802.15.4 (Zigbee/Thread)" : ""); + (chip_info.features & CHIP_FEATURE_IEEE802154) ? ", 802.15.4 (Zigbee/Thread), " : "", + // psram + (chip_info.features & CHIP_FEATURE_EMB_PSRAM) ? "with embedded PSRAM, " : ""); unsigned major_rev = chip_info.revision / 100; unsigned minor_rev = chip_info.revision % 100; @@ -26,5 +37,7 @@ void display_chip_info() { (chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external"); printf("Minimum free heap size: %" PRIu32 " bytes\n", esp_get_minimum_free_heap_size()); + // psram + printf("PSRAM size: %u bytes\n", esp_psram_get_size()); } \ No newline at end of file diff --git a/main/info/info.h b/main/info/info.h index 286bd9d..2b324f4 100644 --- a/main/info/info.h +++ b/main/info/info.h @@ -1,10 +1 @@ -#include -#include -#include "sdkconfig.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "esp_chip_info.h" -#include "esp_flash.h" -#include "esp_system.h" - void display_chip_info(); diff --git a/main/touch/touch.h b/main/touch/touch.h index 899c050..a06796c 100644 --- a/main/touch/touch.h +++ b/main/touch/touch.h @@ -1,4 +1,10 @@ -#include "info/info.h" +#pragma once + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" class TouchHandler { public: From 14f4b8fdc03007045817fa692fb1b05f66e5798e Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:58:25 +0800 Subject: [PATCH 03/16] feat: update dependencies and configuration for esp32s3 support --- dependencies.lock | 15 +++++++++++++-- diagram.json | 2 +- main/CMakeLists.txt | 2 +- main/idf_component.yml | 1 + 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/dependencies.lock b/dependencies.lock index 7077b96..0686d75 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -1,4 +1,14 @@ dependencies: + espressif/cjson: + component_hash: 9372811fb197926f522c467627cf4a8e72b681e0366e17879631da801103aef3 + dependencies: + - name: idf + require: private + version: '>=5.0' + source: + registry_url: https://components.espressif.com/ + type: service + version: 1.7.19 espressif/esp_lcd_touch: component_hash: 3f85a7d95af876f1a6ecca8eb90a81614890d0f03a038390804e5a77e2caf862 dependencies: @@ -49,10 +59,11 @@ dependencies: type: service version: 9.4.0 direct_dependencies: +- espressif/cjson - espressif/esp_lcd_touch_gt911 - espressif/esp_lvgl_port - idf - lvgl/lvgl -manifest_hash: fef450d0c399587685f90aba8ae661965ef507d04a5fcf17633db86d5d0fbcff -target: esp32 +manifest_hash: 2010806782b4d2486b02b853afa44a545717d3d0593eb60f9aa6e5c696270f8f +target: esp32s3 version: 2.0.0 diff --git a/diagram.json b/diagram.json index 30b0c62..c0e3bd4 100644 --- a/diagram.json +++ b/diagram.json @@ -4,7 +4,7 @@ "editor": "wokwi", "parts": [ { - "type": "board-esp32-devkit-c-v4", + "type": "board-esp32-s3-devkitc-1", "id": "esp", "top": 0, "left": 0, diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 086d4d6..52dc8c3 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,4 +1,4 @@ -set(requires esp-tls spi_flash nvs_flash esp_event esp_netif esp_http_client esp_wifi) +set(requires esp-tls spi_flash nvs_flash esp_event esp_netif esp_http_client esp_wifi esp_psram) file(GLOB SRCS "main.cpp" "*.cpp" "*.c" "**/*.cpp" "**/*.c") idf_component_register(SRCS ${SRCS} diff --git a/main/idf_component.yml b/main/idf_component.yml index a967005..01296f1 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -17,3 +17,4 @@ dependencies: lvgl/lvgl: ^9.4.0 espressif/esp_lcd_touch_gt911: ^1.2.0~1 espressif/esp_lvgl_port: ^2.7.0 + espressif/cjson: ^1.7.19 From 44fb9aa632d64cc46aabfa303f705eecb73df3a2 Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:00:04 +0800 Subject: [PATCH 04/16] Refactor NVS and WiFi handlers for improved memory management and logging - Updated KVStorageHandler interface to use std::string instead of char* for key-value operations. - Enhanced NVSStorageHandler to utilize ESP_LOG for error and info messages instead of printf. - Refactored WifiHandler to manage WiFi credentials using JSON format for better structure and storage. - Replaced raw pointers with std::unique_ptr in WifiHandler and NetworkHandler for automatic memory management. - Removed unused TouchHandler and EInkTouchHandler classes to clean up the codebase. - Adjusted CMakeLists.txt to remove unnecessary include directories. - Updated lv_conf.h to enable FreeRTOS and gesture recognition features. --- main/CMakeLists.txt | 2 +- main/io/io.h | 15 +- main/io/nvs_handler.cpp | 83 ++++++----- main/io/nvs_handler.h | 8 +- main/lv_conf.h | 18 ++- main/main.cpp | 97 +++++++------ main/network/network.cpp | 8 +- main/network/network.h | 4 +- main/network/wifi_handler.cpp | 253 ++++++++++++++++++++++------------ main/network/wifi_handler.h | 27 ++-- main/touch/touch.cpp | 53 ------- main/touch/touch.h | 38 ----- 12 files changed, 302 insertions(+), 304 deletions(-) delete mode 100644 main/touch/touch.cpp delete mode 100644 main/touch/touch.h diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 52dc8c3..07d655a 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -3,4 +3,4 @@ file(GLOB SRCS "main.cpp" "*.cpp" "*.c" "**/*.cpp" "**/*.c") idf_component_register(SRCS ${SRCS} PRIV_REQUIRES ${requires} - INCLUDE_DIRS "." "display" "touch" "network" "ui" "io" "common") + INCLUDE_DIRS "." "display" "network" "ui" "io" "common") diff --git a/main/io/io.h b/main/io/io.h index ad396f9..4f46722 100644 --- a/main/io/io.h +++ b/main/io/io.h @@ -3,8 +3,8 @@ #include "freertos/event_groups.h" #include -typedef bool(*FilterFunc)(const char* const& key); -typedef void (*KeyValueProcessor)(void* arg, const char* const& key, const char* const& value); +typedef bool(*FilterFunc)(const std::string& key); +typedef void (*KeyValueProcessor)(void* arg, const std::string& key, const std::string& value); class KVStorageHandler { public: @@ -13,15 +13,14 @@ public: virtual void init(const EventGroupHandle_t& system_event_group) = 0; // Store a key-value pair - virtual void put(const char* const& key, const char* const& value) = 0; + virtual void put(const std::string& key, const std::string& value) = 0; - // Retrieve a value by key, returns nullptr if key not found - // The caller is responsible for freeing the returned memory - virtual std::unique_ptr get(const char* const& key) const = 0; + // Retrieve a value by key, returns empty string if key not found + virtual std::string get(const std::string& key) const = 0; virtual esp_err_t process_all(KeyValueProcessor processor, void* arg) const = 0; - virtual esp_err_t process_filtered(const char* const& key_prefix, KeyValueProcessor processor, void* arg) const = 0; + virtual esp_err_t process_filtered(const std::string& key_prefix, KeyValueProcessor processor, void* arg) const = 0; virtual esp_err_t process_filtered(FilterFunc filter_func, KeyValueProcessor processor, void* arg) const = 0; // Delete a key-value pair - virtual void remove(const char* const& key) = 0; + virtual void remove(const std::string& key) = 0; }; \ No newline at end of file diff --git a/main/io/nvs_handler.cpp b/main/io/nvs_handler.cpp index 2a0ad9a..187b6cf 100644 --- a/main/io/nvs_handler.cpp +++ b/main/io/nvs_handler.cpp @@ -2,6 +2,9 @@ #include "io/nvs_handler.h" #include "nvs_flash.h" #include "string.h" +#include "esp_log.h" + +#define TAG "NVSStorageHandler" NVSStorageHandler::NVSStorageHandler( const char* name_space @@ -24,49 +27,51 @@ void NVSStorageHandler::init(const EventGroupHandle_t& system_event_group) { err = nvs_open(this->name_space, NVS_READWRITE, &this->nvsHandle); if (err != ESP_OK) { - printf("Error (%s) opening NVS handle!\n", esp_err_to_name(err)); + ESP_LOGE(TAG, "Error (%s) opening NVS handle!", esp_err_to_name(err)); } else { - xEventGroupSetBits(system_event_group, STORAGE_READY_BIT); - printf("NVS Storage initialized.\n"); + if (system_event_group != nullptr) { + xEventGroupSetBits(system_event_group, STORAGE_READY_BIT); + } + ESP_LOGI(TAG, "NVS Storage initialized."); } } -void NVSStorageHandler::put(const char* const& key, const char* const& value) { +void NVSStorageHandler::put(const std::string& key, const std::string& value) { if (this->nvsHandle == 0) { - printf("NVS handle is not initialized.\n"); + ESP_LOGE(TAG, "NVS handle is not initialized."); return; } - esp_err_t err = nvs_set_str(this->nvsHandle, key, value); + esp_err_t err = nvs_set_str(this->nvsHandle, key.c_str(), value.c_str()); if (err != ESP_OK) { - printf("Error (%s) setting key-value pair in NVS!\n", esp_err_to_name(err)); + ESP_LOGE(TAG, "Error (%s) setting key-value pair in NVS!", esp_err_to_name(err)); } else { nvs_commit(this->nvsHandle); - printf("Key-value pair (%s, %s) stored in NVS.\n", key, value); + ESP_LOGI(TAG, "Key-value pair (%s, %s) stored in NVS.", key.c_str(), value.c_str()); } } -std::unique_ptr NVSStorageHandler::get(const char* const& key) const { +std::string NVSStorageHandler::get(const std::string& key) const { if (this->nvsHandle == 0) { - printf("NVS handle is not initialized.\n"); - return nullptr; + ESP_LOGE(TAG, "NVS handle is not initialized."); + return ""; } size_t required_size = 0; - esp_err_t err = nvs_get_str(this->nvsHandle, key, nullptr, &required_size); + esp_err_t err = nvs_get_str(this->nvsHandle, key.c_str(), nullptr, &required_size); if (err == ESP_ERR_NVS_NOT_FOUND) { - printf("Key %s not found in NVS.\n", key); - return nullptr; + ESP_LOGW(TAG, "Key %s not found in NVS.", key.c_str()); + return ""; } else if (err != ESP_OK) { - printf("Error (%s) getting size for key %s from NVS!\n", esp_err_to_name(err), key); - return nullptr; + ESP_LOGE(TAG, "Error (%s) getting size for key %s from NVS!", esp_err_to_name(err), key.c_str()); + return ""; } - std::unique_ptr value(new char[required_size]); - err = nvs_get_str(this->nvsHandle, key, value.get(), &required_size); + std::string value; + err = nvs_get_str(this->nvsHandle, key.c_str(), value.data(), &required_size); if (err != ESP_OK) { - printf("Error (%s) getting value for key %s from NVS!\n", esp_err_to_name(err), key); - return nullptr; + ESP_LOGE(TAG, "Error (%s) getting value for key %s from NVS!", esp_err_to_name(err), key.c_str()); + return ""; } return value; @@ -76,7 +81,7 @@ NVSIteratorGuard NVSStorageHandler::create_iterator() const { nvs_iterator_t it = nullptr; esp_err_t err = nvs_entry_find(NVS_DEFAULT_PART_NAME, this->name_space, NVS_TYPE_ANY, &it); if (err != ESP_OK) { - printf("Error (%s) creating NVS iterator!\n", esp_err_to_name(err)); + ESP_LOGE(TAG, "Error (%s) creating NVS iterator!", esp_err_to_name(err)); return NVSIteratorGuard(nullptr, err); } @@ -94,22 +99,23 @@ esp_err_t NVSStorageHandler::process_all(KeyValueProcessor processor, void* arg) nvs_entry_info_t info; esp_err_t err = nvs_entry_info(it, &info); if (err != ESP_OK) { - printf("Error (%s) getting NVS entry info!\n", esp_err_to_name(err)); + ESP_LOGE(TAG, "Error (%s) getting NVS entry info!", esp_err_to_name(err)); return err; } nvs_handle_t temp_handle; err = nvs_open(this->name_space, NVS_READONLY, &temp_handle); if (err != ESP_OK) { - printf("Error (%s) opening NVS handle for reading!\n", esp_err_to_name(err)); + ESP_LOGE(TAG, "Error (%s) opening NVS handle for reading!", esp_err_to_name(err)); return err; } // call the processor with the key and value - processor(arg, info.key, this->get(info.key).get()); + std::string key_str = info.key; + processor(arg, key_str, this->get(key_str)); } return ESP_OK; } -esp_err_t NVSStorageHandler::process_filtered(const char* const& key_prefix, KeyValueProcessor processor, void* arg) const { +esp_err_t NVSStorageHandler::process_filtered(const std::string& key_prefix, KeyValueProcessor processor, void* arg) const { NVSIteratorGuard iterator_guard = this->create_iterator(); if (!iterator_guard.is_valid()) { return iterator_guard.get_error(); @@ -120,19 +126,19 @@ esp_err_t NVSStorageHandler::process_filtered(const char* const& key_prefix, Key nvs_entry_info_t info; esp_err_t err = nvs_entry_info(it, &info); if (err != ESP_OK) { - printf("Error (%s) getting NVS entry info!\n", esp_err_to_name(err)); + ESP_LOGE(TAG, "Error (%s) getting NVS entry info!", esp_err_to_name(err)); return err; } // check if the key matches the prefix - if (strncmp(info.key, key_prefix, strlen(key_prefix)) == 0) { + if (strncmp(info.key, key_prefix.c_str(), key_prefix.length()) == 0) { nvs_handle_t temp_handle; err = nvs_open(this->name_space, NVS_READONLY, &temp_handle); if (err != ESP_OK) { - printf("Error (%s) opening NVS handle for reading!\n", esp_err_to_name(err)); + ESP_LOGE(TAG, "Error (%s) opening NVS handle for reading!", esp_err_to_name(err)); return err; } // call the processor with the key and value - processor(arg, info.key, this->get(info.key).get()); + processor(arg, std::string(info.key), this->get(std::string(info.key))); } } return ESP_OK; @@ -149,35 +155,36 @@ esp_err_t NVSStorageHandler::process_filtered(FilterFunc filter_func, KeyValuePr nvs_entry_info_t info; esp_err_t err = nvs_entry_info(it, &info); if (err != ESP_OK) { - printf("Error (%s) getting NVS entry info!\n", esp_err_to_name(err)); + ESP_LOGE(TAG, "Error (%s) getting NVS entry info!", esp_err_to_name(err)); return err; } // check if the key matches the filter function - if (filter_func(info.key)) { + std::string key_str(info.key); + if (filter_func(key_str)) { nvs_handle_t temp_handle; err = nvs_open(this->name_space, NVS_READONLY, &temp_handle); if (err != ESP_OK) { - printf("Error (%s) opening NVS handle for reading!\n", esp_err_to_name(err)); + ESP_LOGE(TAG, "Error (%s) opening NVS handle for reading!", esp_err_to_name(err)); return err; } // call the processor with the key and value - processor(arg, info.key, this->get(info.key).get()); + processor(arg, key_str, this->get(key_str)); } } return ESP_OK; } -void NVSStorageHandler::remove(const char* const& key) { +void NVSStorageHandler::remove(const std::string& key) { if (this->nvsHandle == 0) { - printf("NVS handle is not initialized.\n"); + ESP_LOGE(TAG, "NVS handle is not initialized."); return; } - esp_err_t err = nvs_erase_key(this->nvsHandle, key); + esp_err_t err = nvs_erase_key(this->nvsHandle, key.c_str()); if (err != ESP_OK) { - printf("Error (%s) deleting key %s from NVS!\n", esp_err_to_name(err), key); + ESP_LOGE(TAG, "Error (%s) deleting key %s from NVS!", esp_err_to_name(err), key.c_str()); } else { nvs_commit(this->nvsHandle); - printf("Key %s deleted from NVS.\n", key); + ESP_LOGI(TAG, "Key %s deleted from NVS.", key.c_str()); } } diff --git a/main/io/nvs_handler.h b/main/io/nvs_handler.h index 2761551..3b20e39 100644 --- a/main/io/nvs_handler.h +++ b/main/io/nvs_handler.h @@ -53,14 +53,14 @@ public: void init(const EventGroupHandle_t& system_event_group) override; - void put(const char* const& key, const char* const& value) override; + void put(const std::string& key, const std::string& value) override; - std::unique_ptr get(const char* const& key) const override; + std::string get(const std::string& key) const override; esp_err_t process_all(KeyValueProcessor processor, void* arg) const override; - esp_err_t process_filtered(const char* const& key_prefix, KeyValueProcessor processor, void* arg) const override; + esp_err_t process_filtered(const std::string& key_prefix, KeyValueProcessor processor, void* arg) const override; esp_err_t process_filtered(FilterFunc filter_func, KeyValueProcessor processor, void* arg) const override; - void remove(const char* const& key) override; + void remove(const std::string& key) override; private: NVSIteratorGuard create_iterator() const; diff --git a/main/lv_conf.h b/main/lv_conf.h index c500dd0..cce9f55 100644 --- a/main/lv_conf.h +++ b/main/lv_conf.h @@ -109,7 +109,8 @@ * - LV_OS_MQX * - LV_OS_SDL2 * - LV_OS_CUSTOM */ -#define LV_USE_OS LV_OS_NONE + // #define LV_USE_OS LV_OS_NONE +#define LV_USE_OS LV_OS_FREERTOS #if LV_USE_OS == LV_OS_CUSTOM #define LV_OS_CUSTOM_INCLUDE @@ -574,7 +575,8 @@ /* Enable the multi-touch gesture recognition feature */ /* Gesture recognition requires the use of floats */ -#define LV_USE_GESTURE_RECOGNITION 0 +// #define LV_USE_GESTURE_RECOGNITION 0 +#define LV_USE_GESTURE_RECOGNITION 1 /*===================== * COMPILER SETTINGS @@ -617,7 +619,8 @@ #define LV_ATTRIBUTE_EXTERN_DATA /** Use `float` as `lv_value_precise_t` */ -#define LV_USE_FLOAT 0 +// #define LV_USE_FLOAT 0 +#define LV_USE_FLOAT 1 /** Enable matrix support * - Requires `LV_USE_FLOAT = 1` */ @@ -1189,7 +1192,8 @@ /** 1: Enable Pinyin input method * - Requires: lv_keyboard */ -#define LV_USE_IME_PINYIN 0 + // #define LV_USE_IME_PINYIN 0 +#define LV_USE_IME_PINYIN 1 #if LV_USE_IME_PINYIN /** 1: Use default thesaurus. * @note If you do not use the default thesaurus, be sure to use `lv_ime_pinyin` after setting the thesaurus. */ @@ -1436,10 +1440,12 @@ *======================*/ /** Enable examples to be built with the library. */ -#define LV_BUILD_EXAMPLES 1 +// #define LV_BUILD_EXAMPLES 1 +#define LV_BUILD_EXAMPLES 0 /** Build the demos */ -#define LV_BUILD_DEMOS 1 +// #define LV_BUILD_DEMOS 1 +#define LV_BUILD_DEMOS 0 /*=================== * DEMO USAGE diff --git a/main/main.cpp b/main/main.cpp index fea18b0..c0b76bd 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -1,10 +1,3 @@ -/* - * SPDX-FileCopyrightText: 2010-2022 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: CC0-1.0 - */ - - #include #include #include @@ -16,18 +9,19 @@ #include "esp_system.h" #include "esp_log.h" - // +// #include "common/constants.h" #include "common/queue_defs.h" #include "io/nvs_handler.h" #include "info/info.h" #include "display/display.h" -#include "touch/touch.h" #include #include "network.h" +// nvs storage namespaces, 15 characters max #define DEFAULT_STORAGE_NAMESPACE "storage" -#define WIFI_CREDENTIALS_STORAGE_NAMESPACE "wifi_credentials" +#define WIFI_CREDENTIALS_STORAGE_NAMESPACE "wifi_cred" +#define TAG "Main" extern "C" void app_main(void); @@ -50,7 +44,7 @@ void app_main(void) { vTaskDelay(5000 / portTICK_PERIOD_MS); return esp_restart(); } - printf("Queues initialized.\n"); + ESP_LOGI(TAG, "Queues initialized.\n"); SemaphoreHandle_t lvgl_mutex = xSemaphoreCreateMutex(); if (lvgl_mutex == NULL) { ESP_LOGE("Main", "Failed to create LVGL mutex"); @@ -58,28 +52,33 @@ void app_main(void) { return esp_restart(); } // - WifiHandler wifi_handler( - new NVSStorageHandler(WIFI_CREDENTIALS_STORAGE_NAMESPACE) - ); - NetworkHandler* network_handler = new NetworkHandler(std::move(wifi_handler)); KVStorageHandler* kv_storage_handler = new NVSStorageHandler( DEFAULT_STORAGE_NAMESPACE ); - DisplayHandler* display_handler = new EInkDisplayHandler(touch_event_queue, lvgl_mutex); - TouchHandler* touch_handler = new EInkTouchHandler(touch_event_queue); + + auto wifi_handler = std::make_unique( + std::unique_ptr(new NVSStorageHandler(WIFI_CREDENTIALS_STORAGE_NAMESPACE)) + ); + NetworkHandler* network_handler = new NetworkHandler(std::move(wifi_handler)); + // DisplayHandler* display_handler = new EInkDisplayHandler(touch_event_queue, lvgl_mutex); // - network_handler->init(system_event_group); kv_storage_handler->init(system_event_group); - display_handler->init(system_event_group); - touch_handler->init(system_event_group); + network_handler->init(system_event_group); + + + // display_handler->init(system_event_group); // // 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 + } TimerHandle_t lvgl_tick_timer = xTimerCreate( "lvgl_tick_timer", - pdMS_TO_TICKS(5), + lvgl_tick_period, pdTRUE, NULL, lvgl_tick_timer_callback @@ -92,20 +91,21 @@ void app_main(void) { xTimerStart(lvgl_tick_timer, 0); // - printf("Waiting for system to be ready...\n"); + ESP_LOGI(TAG, "Waiting for system to be ready...\n"); xEventGroupWaitBits( system_event_group, - DISPLAY_READY_BIT | TOUCH_CALIBRATED_BIT | STORAGE_READY_BIT | NETWORK_READY_BIT, + // DISPLAY_READY_BIT | TOUCH_CALIBRATED_BIT | + STORAGE_READY_BIT | NETWORK_READY_BIT, // do not clear on exit, require explicit reset pdFALSE, pdTRUE, portMAX_DELAY ); - printf("System is ready. Starting main application...\n"); + ESP_LOGI(TAG, "System is ready. Starting main application...\n"); // starting event loops - display_handler->start_event_loop(); - touch_handler->start_event_loop(); + // display_handler->start_event_loop(); // wait for shutdown signal + ESP_LOGI(TAG, "Waiting for shutdown signal...\n"); EventBits_t bits = xEventGroupWaitBits( system_lifecycle_event_group, SYSTEM_SHUTDOWN_BIT | SYSTEM_RESTART_BIT, @@ -114,27 +114,26 @@ void app_main(void) { pdFALSE, portMAX_DELAY ); - printf("Shutdown signal received. Cleaning up...\n"); + ESP_LOGI(TAG, "Shutdown signal received. Cleaning up...\n"); // cleanup - shutdown_display_handlerFunc shutdown_display_handler = display_handler->get_shutdown_display_handler(); - restart_display_handlerFunc restart_display_handler = display_handler->get_restart_display_handler(); - delete display_handler; - delete touch_handler; + // shutdown_display_handlerFunc shutdown_display_handler = display_handler->get_shutdown_display_handler(); + // restart_display_handlerFunc restart_display_handler = display_handler->get_restart_display_handler(); + // delete display_handler; vSemaphoreDelete(lvgl_mutex); vEventGroupDelete(system_event_group); vQueueDelete(touch_event_queue); - printf("Cleanup complete.\n"); + ESP_LOGI(TAG, "Cleanup complete.\n"); // handle shutdown or restart if (bits & SYSTEM_SHUTDOWN_BIT) { - if (shutdown_display_handler != nullptr) { - printf("Calling display shutdown handler...\n"); - shutdown_display_handler(); - } else { - printf("No display shutdown handler to call.\n"); - } - printf("System is shutting down.\n"); + // if (shutdown_display_handler != nullptr) { + // ESP_LOGI(TAG, "Calling display shutdown handler...\n"); + // shutdown_display_handler(); + // } else { + // ESP_LOGI(TAG, "No display shutdown handler to call.\n"); + // } + ESP_LOGI(TAG, "System is shutting down.\n"); fflush(stdout); // wait for start bit to be set again if future restart is desired, else expect manual power cycle EventBits_t bits = xEventGroupWaitBits( @@ -145,24 +144,24 @@ void app_main(void) { portMAX_DELAY ); if (bits & SYSTEM_START_BIT) { - printf("SYSTEM_START_BIT received, restarting system.\n"); + ESP_LOGI(TAG, "SYSTEM_START_BIT received, restarting system.\n"); } else { - printf("No restart signal received, waiting for manual power cycle.\n"); + ESP_LOGW(TAG, "No restart signal received, waiting for manual power cycle.\n"); while (true) { vTaskDelay(portMAX_DELAY); } } } else if (bits & SYSTEM_RESTART_BIT) { - if (restart_display_handler != nullptr) { - printf("Calling display restart handler...\n"); - restart_display_handler(); - } else { - printf("No display restart handler to call.\n"); - } - printf("System is restarting.\n"); + // if (restart_display_handler != nullptr) { + // ESP_LOGI(TAG, "Calling display restart handler...\n"); + // restart_display_handler(); + // } else { + // ESP_LOGI(TAG, "No display restart handler to call.\n"); + // } + ESP_LOGI(TAG, "System is restarting.\n"); fflush(stdout); } else { - printf("Unknown shutdown signal received. Restarting by default.\n"); + ESP_LOGW(TAG, "Unknown shutdown signal received. Restarting by default.\n"); fflush(stdout); } diff --git a/main/network/network.cpp b/main/network/network.cpp index 432d6a7..d02a717 100644 --- a/main/network/network.cpp +++ b/main/network/network.cpp @@ -4,7 +4,7 @@ #include "common/constants.h" NetworkHandler::NetworkHandler( - WifiHandler&& wifiHandler + std::unique_ptr wifiHandler ) : wifiHandler(std::move(wifiHandler)) { } NetworkHandler::~NetworkHandler() { } @@ -14,7 +14,7 @@ void NetworkHandler::init(EventGroupHandle_t system_event_group) { ESP_LOGW("NetworkHandler", "Already initialized, skipping"); return; } - this->wifiHandler.init(); + this->wifiHandler->init(); this->initialized = true; xEventGroupSetBits( system_event_group, @@ -23,10 +23,10 @@ void NetworkHandler::init(EventGroupHandle_t system_event_group) { } WifiHandler& NetworkHandler::get_wifi_handler() { - return this->wifiHandler; + return *this->wifiHandler; } std::unique_ptr NetworkHandler::get_http_handler(const esp_http_client_config_t&& config) { - return std::unique_ptr(new HttpHandler(std::move(config), &this->wifiHandler)); + return std::unique_ptr(new HttpHandler(std::move(config), this->wifiHandler.get())); } diff --git a/main/network/network.h b/main/network/network.h index ba7eaac..edcc46c 100644 --- a/main/network/network.h +++ b/main/network/network.h @@ -11,7 +11,7 @@ class HttpHandler; class NetworkHandler { public: NetworkHandler( - WifiHandler&& wifiHandler + std::unique_ptr wifiHandler ); ~NetworkHandler(); @@ -22,6 +22,6 @@ public: private: - WifiHandler wifiHandler; + std::unique_ptr wifiHandler; bool initialized = false; }; diff --git a/main/network/wifi_handler.cpp b/main/network/wifi_handler.cpp index 5a70a2a..38d8d01 100644 --- a/main/network/wifi_handler.cpp +++ b/main/network/wifi_handler.cpp @@ -6,38 +6,39 @@ #include "esp_log.h" #include "freertos/semphr.h" #include "common/semaphore_guard.h" +#include "cJSON.h" static const char* TAG = "WifiHandler"; -static const char* WIFI_SSID_KEY = "wifi_ssid"; -static const char* WIFI_PASSWORD_KEY = "wifi_password"; +static const char* WIFI_SSID_KEY = "ssid"; +static const char* WIFI_PASSWORD_STORE_KEY = "psw"; WifiHandler::WifiHandler( // this handler is used to store/retrieve WiFi credentials // should have a unique namespace for WiFi credentials // it will be owned by WifiHandler and deleted in its destructor - KVStorageHandler* kvs -) : kvs(kvs) { + std::unique_ptr kvs +) : kvs(std::move(kvs)) { this->s_wifi_event_group = xEventGroupCreate(); + if (!this->s_wifi_event_group) { + ESP_LOGE(TAG, "Failed to create WiFi event group"); + } this->scan_mutex = xSemaphoreCreateMutex(); + if (!this->scan_mutex) { + ESP_LOGE(TAG, "Failed to create scan mutex"); + } this->connection_mutex = xSemaphoreCreateMutex(); -} - -// Move constructor: transfer ownership of resources -WifiHandler::WifiHandler(WifiHandler&& other) noexcept - : initialized(other.initialized), - kvs(other.kvs), - s_wifi_event_group(other.s_wifi_event_group), - scan_mutex(other.scan_mutex), - connection_mutex(other.connection_mutex), - current_ssid(other.current_ssid), - expect_disconnected(other.expect_disconnected) { - other.kvs = nullptr; - other.initialized = false; - other.s_wifi_event_group = 0; - other.scan_mutex = nullptr; - other.connection_mutex = nullptr; - other.current_ssid = nullptr; - other.expect_disconnected = false; + if (!this->connection_mutex) { + ESP_LOGE(TAG, "Failed to create connection mutex"); + } + this->credential_mutex = xSemaphoreCreateMutex(); + if (!this->credential_mutex) { + ESP_LOGE(TAG, "Failed to create credential mutex"); + } + if (this->kvs == nullptr) { + ESP_LOGW(TAG, "KVStorageHandler is null, WiFi credentials will not be stored"); + } else { + this->kvs->init(nullptr); + } } WifiHandler::~WifiHandler() { @@ -46,20 +47,23 @@ WifiHandler::~WifiHandler() { // Check if it should be called esp_wifi_deinit(); vEventGroupDelete(this->s_wifi_event_group); - if (this->current_ssid) { - delete[] this->current_ssid; + if (!this->current_ssid.empty()) { + this->current_ssid.clear(); } vSemaphoreDelete(this->scan_mutex); vSemaphoreDelete(this->connection_mutex); esp_event_handler_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, &WifiHandler::wifi_event_handler); esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &WifiHandler::wifi_event_handler); + this->initialized = false; + // unique_ptr will automatically delete the object + this->kvs = nullptr; } } -void WifiHandler::init() { +esp_err_t WifiHandler::init() { if (this->initialized) { ESP_LOGW(TAG, "Already initialized, skipping"); - return; + return ESP_OK; } esp_err_t err; @@ -67,13 +71,13 @@ void WifiHandler::init() { err = esp_netif_init(); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_netif_init failed: %s", esp_err_to_name(err)); - return; + return err; } err = esp_event_loop_create_default(); if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { ESP_LOGE(TAG, "esp_event_loop_create_default failed: %s", esp_err_to_name(err)); - return; + return err; } // create default WiFi station @@ -84,32 +88,40 @@ void WifiHandler::init() { err = esp_wifi_init(&cfg); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_wifi_init failed: %s", esp_err_to_name(err)); - return; + return err; } // register event handlers for WiFi and IP events - esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &WifiHandler::wifi_event_handler, this); - esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &WifiHandler::wifi_event_handler, this); + err = esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &WifiHandler::wifi_event_handler, this); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_event_handler_register failed: %s", esp_err_to_name(err)); + return err; + } + err = esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &WifiHandler::wifi_event_handler, this); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_event_handler_register failed: %s", esp_err_to_name(err)); + return err; + } err = esp_wifi_set_mode(WIFI_MODE_STA); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_wifi_set_mode failed: %s", esp_err_to_name(err)); - return; + return err; } err = esp_wifi_start(); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_wifi_start failed: %s", esp_err_to_name(err)); - return; + return err; } // get WiFi credentials from KV storage if available - char* ssid = nullptr; - char* password = nullptr; + std::string ssid; + std::string password; this->get_wifi_credentials(ssid, password); - if (ssid && password) { - ESP_LOGI(TAG, "Found stored WiFi credentials, connecting to SSID: %s", ssid); + if (!ssid.empty() && !password.empty()) { + ESP_LOGI(TAG, "Found stored WiFi credentials, connecting to SSID: %s", ssid.c_str()); err = this->connect(ssid, password); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to connect to stored WiFi credentials: %s", esp_err_to_name(err)); @@ -118,13 +130,11 @@ void WifiHandler::init() { ESP_LOGI(TAG, "No stored WiFi credentials found, not connecting"); } - delete[] ssid; - delete[] password; - initialized = true; + return ESP_OK; } -esp_err_t WifiHandler::connect(const char* ssid, const char* password) { +esp_err_t WifiHandler::connect(const std::string& ssid, const std::string& password) { SemaphoreGuard guard(this->connection_mutex); // wait up to 5 seconds to take the mutex if (!guard.take(5000 / portTICK_PERIOD_MS)) { @@ -133,24 +143,21 @@ esp_err_t WifiHandler::connect(const char* ssid, const char* password) { } expect_disconnected = false; - if (this->current_ssid) { - delete[] this->current_ssid; + if (!this->current_ssid.empty()) { + this->current_ssid.clear(); } - size_t ssid_len = strlen(ssid); - this->current_ssid = new char[ssid_len + 1]; - strncpy(this->current_ssid, ssid, ssid_len + 1); - this->current_ssid[ssid_len] = '\0'; + this->current_ssid = ssid; // wifi_config_t wifi_config = {}; - strncpy((char*)wifi_config.sta.ssid, this->current_ssid, sizeof(wifi_config.sta.ssid)); + strncpy((char*)wifi_config.sta.ssid, this->current_ssid.c_str(), sizeof(wifi_config.sta.ssid)); wifi_config.sta.ssid[sizeof(wifi_config.sta.ssid) - 1] = '\0'; - strncpy((char*)wifi_config.sta.password, password, sizeof(wifi_config.sta.password)); + strncpy((char*)wifi_config.sta.password, password.c_str(), sizeof(wifi_config.sta.password)); wifi_config.sta.password[sizeof(wifi_config.sta.password) - 1] = '\0'; // set auth mode to WPA2_PSK minimum wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK; - ESP_LOGI(TAG, "Connecting to SSID: %s", this->current_ssid); + ESP_LOGI(TAG, "Connecting to SSID: %s", this->current_ssid.c_str()); esp_err_t err = esp_wifi_set_config(wifi_interface_t::WIFI_IF_STA, &wifi_config); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to set WiFi config: %s", esp_err_to_name(err)); @@ -162,40 +169,26 @@ esp_err_t WifiHandler::connect(const char* ssid, const char* password) { return err; } - // store credentials - this->kvs->put(WIFI_SSID_KEY, this->current_ssid); - // store password under key derived from SSID - char* password_key = this->build_password_key(this->current_ssid); - this->kvs->put(password_key, password); - delete[] password_key; - - // set connected bit on successful connection - xEventGroupSetBits( - this->s_wifi_event_group, - WIFI_CONNECTED_BIT - ); + // store credentials after successful connection attempt + this->store_wifi_credentials(this->current_ssid, password); return ESP_OK; } -esp_err_t WifiHandler::connect(const char* ssid) { - char* stored_ssid = nullptr; - char* stored_password = nullptr; +esp_err_t WifiHandler::connect(const std::string& ssid) { + std::string stored_ssid; + std::string stored_password; this->get_wifi_credentials(stored_ssid, stored_password); - if (!stored_ssid || strcmp(stored_ssid, ssid) != 0) { - ESP_LOGE(TAG, "No stored credentials for SSID: %s", ssid); - delete[] stored_ssid; - delete[] stored_password; + if (stored_ssid.empty() || stored_ssid != ssid) { + ESP_LOGE(TAG, "No stored credentials for SSID: %s", ssid.c_str()); return ESP_FAIL; } - esp_err_t err = this->connect(stored_ssid, stored_password ? stored_password : ""); - delete[] stored_ssid; - delete[] stored_password; + esp_err_t err = this->connect(stored_ssid, stored_password); return err; } esp_err_t WifiHandler::reconnect() { - if (!this->current_ssid) { + if (this->current_ssid.empty()) { ESP_LOGE(TAG, "No current SSID set, cannot reconnect"); return ESP_FAIL; } @@ -270,10 +263,15 @@ void WifiHandler::wifi_event_handler(void* arg, esp_event_base_t event_base, int case WIFI_EVENT_STA_START: // When the station starts, attempt to connect ESP_LOGI(TAG, "WIFI_EVENT_STA_START"); - if (!self->expect_disconnected && self->current_ssid) { - ESP_LOGI(TAG, "Station started, attempting to connect to SSID: %s", self->current_ssid); + if (!self->expect_disconnected && !self->current_ssid.empty()) { + ESP_LOGI(TAG, "Station started, attempting to connect to SSID: %s", self->current_ssid.c_str()); self->reconnect(); } + // set the event bit to indicate started + xEventGroupSetBits( + self->s_wifi_event_group, + WIFI_STARTED_BIT + ); break; case WIFI_EVENT_STA_DISCONNECTED: ESP_LOGI(TAG, "WIFI_EVENT_STA_DISCONNECTED"); @@ -306,29 +304,106 @@ void WifiHandler::wifi_event_handler(void* arg, esp_event_base_t event_base, int // private methods // -char* WifiHandler::build_password_key(const char* ssid) { - // `{WIFI_PASSWORD_KEY}_{ssid}` - size_t password_key_len = strlen(WIFI_PASSWORD_KEY) + 1 + strlen(ssid) + 1; - char* password_key_buff = new char[password_key_len]; - snprintf(password_key_buff, password_key_len, "%s_%s", WIFI_PASSWORD_KEY, ssid); - return password_key_buff; +void WifiHandler::store_wifi_credentials(const std::string& ssid, const std::string& password) { + if (!kvs) { + ESP_LOGW(TAG, "KVStorageHandler not set, cannot store WiFi credentials"); + return; + } + SemaphoreGuard guard(this->credential_mutex); + // wait up to 5 seconds to take the mutex + if (!guard.take(5000 / portTICK_PERIOD_MS)) { + ESP_LOGE(TAG, "Failed to take credential mutex"); + return; + } + // store the password according to the JSON structure + std::string password_key_store = kvs->get(WIFI_PASSWORD_STORE_KEY); + cJSON* json = nullptr; + if (password_key_store.empty()) { + // create new JSON object + json = cJSON_CreateObject(); + } else { + // parse existing JSON + json = cJSON_Parse(password_key_store.c_str()); + if (json == nullptr) { + ESP_LOGE(TAG, "Failed to parse existing WiFi password JSON, creating new"); + json = cJSON_CreateObject(); + } + } + cJSON* credentials = cJSON_GetObjectItem(json, "credentials"); + if (credentials == nullptr || !cJSON_IsObject(credentials)) { + // create credentials object if it doesn't exist + credentials = cJSON_CreateObject(); + cJSON_AddItemToObject(json, "credentials", credentials); + } + // create SSID object + cJSON* ssid_item = cJSON_CreateObject(); + // add password field + cJSON_AddStringToObject(ssid_item, "password", password.c_str()); + // add SSID object to credentials + cJSON_AddItemToObject(credentials, ssid.c_str(), ssid_item); + // store updated JSON string + char* updated_json_str = cJSON_PrintUnformatted(json); + if (updated_json_str) { + kvs->put(WIFI_PASSWORD_STORE_KEY, std::string(updated_json_str)); + cJSON_free(updated_json_str); + } + cJSON_Delete(json); } -void WifiHandler::get_wifi_credentials(char*& ssid, char*& password) { +void WifiHandler::get_wifi_credentials(std::string& out_ssid, std::string& out_password) { if (!kvs) { ESP_LOGW(TAG, "KVStorageHandler not set, cannot get WiFi credentials"); return; } - ssid = kvs->get(WIFI_SSID_KEY).get(); - if (!ssid) { - ssid = nullptr; - password = nullptr; + SemaphoreGuard guard(this->credential_mutex); + // wait up to 5 seconds to take the mutex + if (!guard.take(5000 / portTICK_PERIOD_MS)) { + ESP_LOGE(TAG, "Failed to take credential mutex"); + return; + } + out_ssid = kvs->get(WIFI_SSID_KEY); + if (out_ssid.empty()) { + out_ssid = ""; + out_password = ""; return; } // password is from KV storage, may be nullptr - char* password_key = this->build_password_key(ssid); - password = kvs->get(password_key).get(); - delete[] password_key; + std::string password_key_store = kvs->get(WIFI_PASSWORD_STORE_KEY); + if (password_key_store.empty()) { + out_password = ""; + return; + } + // parse from json + cJSON* json = cJSON_Parse(password_key_store.c_str()); + if (json == nullptr) { + ESP_LOGE(TAG, "Failed to parse WiFi password JSON"); + out_password = ""; + return; + } + cJSON* credentials = cJSON_GetObjectItem(json, "credentials"); + if (credentials == nullptr || !cJSON_IsObject(credentials)) { + ESP_LOGE(TAG, "WiFi password JSON does not contain valid 'credentials' object"); + out_password = ""; + cJSON_Delete(json); + return; + } + // get the ssid value + cJSON* ssid_item = cJSON_GetObjectItem(credentials, out_ssid.c_str()); + if (ssid_item == nullptr || !cJSON_IsObject(ssid_item)) { + ESP_LOGE(TAG, "WiFi password JSON does not contain valid SSID field for SSID: %s", out_ssid.c_str()); + out_password = ""; + cJSON_Delete(json); + return; + } + cJSON* password = cJSON_GetObjectItem(ssid_item, "password"); + if (password == nullptr || !cJSON_IsString(password)) { + ESP_LOGE(TAG, "WiFi password JSON does not contain valid 'password' field for SSID: %s", out_ssid.c_str()); + out_password = ""; + cJSON_Delete(json); + return; + } + out_password = password->valuestring; + cJSON_Delete(json); } EventBits_t WifiHandler::wait_for_connection(TickType_t ticks_to_wait) { diff --git a/main/network/wifi_handler.h b/main/network/wifi_handler.h index 5614e36..31a0f17 100644 --- a/main/network/wifi_handler.h +++ b/main/network/wifi_handler.h @@ -3,7 +3,9 @@ #include "esp_wifi.h" #include "freertos/event_groups.h" -#define WIFI_CONNECTED_BIT (1 << 0) +#define WIFI_STARTED_BIT (1 << 0) +#define WIFI_CONNECTED_BIT (1 << 1) + class WifiHandler { public: @@ -11,16 +13,13 @@ public: // this handler is used to store/retrieve WiFi credentials // should have a unique namespace for WiFi credentials // it will be owned by WifiHandler and deleted in its destructor - KVStorageHandler* kvs + std::unique_ptr kvs ); ~WifiHandler(); - // move semantics - WifiHandler(WifiHandler&& other) noexcept; - - void init(); - esp_err_t connect(const char* ssid, const char* password); - esp_err_t connect(const char* ssid); // connect using stored password + esp_err_t init(); + esp_err_t connect(const std::string& ssid, const std::string& password); + esp_err_t connect(const std::string& ssid); // connect using stored password esp_err_t reconnect(); // reconnect to current SSID void disconnect(); EventBits_t wait_for_connection(TickType_t ticks_to_wait); @@ -37,17 +36,21 @@ private: // prevent copying WifiHandler(const WifiHandler&) = delete; WifiHandler& operator=(const WifiHandler&) = delete; + // prevent moving + WifiHandler(WifiHandler&& other) = delete; + WifiHandler& operator=(WifiHandler&& other) = delete; - char* build_password_key(const char* ssid); - void get_wifi_credentials(char*& ssid, char*& password); + void store_wifi_credentials(const std::string& ssid, const std::string& password); + void get_wifi_credentials(std::string& out_ssid, std::string& out_password); bool initialized = false; - KVStorageHandler* kvs = nullptr; + std::unique_ptr kvs = nullptr; EventGroupHandle_t s_wifi_event_group = 0; SemaphoreHandle_t scan_mutex = nullptr; SemaphoreHandle_t connection_mutex = nullptr; + SemaphoreHandle_t credential_mutex = nullptr; // current connected / preferred SSID - char* current_ssid = nullptr; + std::string current_ssid; // prevent auto-reconnect on expected disconnection, e.g. when user calls disconnect() // should be reset to false after connect() bool expect_disconnected = false; diff --git a/main/touch/touch.cpp b/main/touch/touch.cpp deleted file mode 100644 index e82a7b4..0000000 --- a/main/touch/touch.cpp +++ /dev/null @@ -1,53 +0,0 @@ -#include "touch.h" -#include "common/constants.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "freertos/event_groups.h" -// TODO: implement actual touch functionality - -TouchHandler::TouchHandler(QueueHandle_t touch_queue) { - (void)touch_queue; -} - -TouchHandler::~TouchHandler() { } - -EInkTouchHandler::EInkTouchHandler(QueueHandle_t touch_queue) - : TouchHandler(touch_queue) { } - -EInkTouchHandler::~EInkTouchHandler() { } - -void EInkTouchHandler::init(EventGroupHandle_t system_event_group) { - if (system_event_group != NULL) { - xEventGroupSetBits(system_event_group, TOUCH_CALIBRATED_BIT); - } -} - -void EInkTouchHandler::start_event_loop() { - // Minimal background task to represent touch processing - xTaskCreate( - // use static adapter and pass `this` as task parameter - EInkTouchHandler::task_adapter, - "touch_task", - 2048, - this, - tskIDLE_PRIORITY + 1, - nullptr - ); -} - -// static -void EInkTouchHandler::task_adapter(void* arg) { - EInkTouchHandler* self = static_cast(arg); - if (self) { - self->run_event_loop(); - } else { - printf("EInkTouchHandler::task_adapter received null pointer\n"); - } - vTaskDelete(NULL); -} - -void EInkTouchHandler::run_event_loop() { - for (;;) { - vTaskDelay(pdMS_TO_TICKS(1000)); - } -} diff --git a/main/touch/touch.h b/main/touch/touch.h deleted file mode 100644 index a06796c..0000000 --- a/main/touch/touch.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include -#include -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "esp_system.h" - -class TouchHandler { -public: - TouchHandler(QueueHandle_t touch_queue); - // the system_event_group is used to set touch-calibrated bit - virtual void init(EventGroupHandle_t system_event_group) = 0; - virtual void start_event_loop() = 0; - virtual ~TouchHandler() = 0; -private: - TouchHandler(const TouchHandler&) = delete; - TouchHandler& operator=(const TouchHandler&) = delete; -}; - -class EInkTouchHandler : public TouchHandler { -public: - EInkTouchHandler(QueueHandle_t touch_queue); - void init(EventGroupHandle_t system_event_group) override; - void start_event_loop() override; - ~EInkTouchHandler() override; - -private: - // Task adapter used for FreeRTOS task creation. Forwards to - // `run_event_loop()` using the `this` pointer passed as the task param. - static void task_adapter(void* arg); - - // Instance method implementing the touch event loop. - void run_event_loop(); - // prevent copying - EInkTouchHandler(const EInkTouchHandler&) = delete; - EInkTouchHandler& operator=(const EInkTouchHandler&) = delete; -}; From 4fa8dc608fb848e0eea60984690d30fcc83714f0 Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:10:39 +0800 Subject: [PATCH 05/16] feat: add display and touch initialization in DisplayHandler --- main/display/constants.h | 11 +++ main/display/display.cpp | 193 +++++++++++++++++++++++++++++---------- main/display/display.h | 68 ++++++-------- 3 files changed, 184 insertions(+), 88 deletions(-) create mode 100644 main/display/constants.h diff --git a/main/display/constants.h b/main/display/constants.h new file mode 100644 index 0000000..da77ac1 --- /dev/null +++ b/main/display/constants.h @@ -0,0 +1,11 @@ +#pragma once +#include "driver/spi_master.h" +#include "driver/gpio.h" + +#define PIN_TOUCH_IRQ GPIO_NUM_4 +#define PIN_TOUCH_SDA GPIO_NUM_5 +#define PIN_TOUCH_SCL GPIO_NUM_6 +#define PIN_BUSY GPIO_NUM_7 +#define PIN_RST GPIO_NUM_8 +#define PIN_DC GPIO_NUM_9 +#define PIN_CS GPIO_NUM_10 diff --git a/main/display/display.cpp b/main/display/display.cpp index 738eefe..961e21b 100644 --- a/main/display/display.cpp +++ b/main/display/display.cpp @@ -1,62 +1,157 @@ -#include "display.h" +#include "display/display.h" #include "common/constants.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "freertos/event_groups.h" -// TODO: implement actual display functionality +#include "esp_log.h" -DisplayHandler::DisplayHandler(QueueHandle_t touch_queue, SemaphoreHandle_t lvgl_mutex) { - (void)touch_queue; - (void)lvgl_mutex; -} - -DisplayHandler::~DisplayHandler() { } - -EInkDisplayHandler::EInkDisplayHandler(QueueHandle_t touch_queue, SemaphoreHandle_t lvgl_mutex) - : DisplayHandler(touch_queue, lvgl_mutex) { } - -EInkDisplayHandler::~EInkDisplayHandler() { } - -void EInkDisplayHandler::init(EventGroupHandle_t system_event_group) { - if (system_event_group != NULL) { - xEventGroupSetBits(system_event_group, DISPLAY_READY_BIT); +DisplayHandler::~DisplayHandler() { + if (_spi_mutex != nullptr) { + vSemaphoreDelete(_spi_mutex); + } + if (_spi != nullptr) { + spi_bus_remove_device(_spi); + } + if (_tp_handle != nullptr) { + esp_lcd_touch_del(_tp_handle); + } + if (_tp_io_handle != nullptr) { + esp_lcd_panel_io_del(_tp_io_handle); } } -void EInkDisplayHandler::start_event_loop() { - // Minimal background task to represent display processing - xTaskCreate( - // use the static adapter and pass `this` as the task parameter - EInkDisplayHandler::task_adapter, - "display_task", - 2048, - this, - tskIDLE_PRIORITY + 1, - nullptr - ); + +void DisplayHandler::init_devices(bool set_display_ready /*= true*/) { + ESP_LOGI("DisplayHandler", "Initializing display and touch..."); + _epd_init(); + _touch_init(); + ESP_LOGI("DisplayHandler", "Display and touch initialized."); + if (set_display_ready) { + ESP_LOGI("DisplayHandler", "Setting display ready bit."); + xEventGroupSetBits(_system_event_group, DISPLAY_READY_BIT | TOUCH_CALIBRATED_BIT); + } } -// static -void EInkDisplayHandler::task_adapter(void* arg) { - EInkDisplayHandler* self = static_cast(arg); - if (self) { - self->run_event_loop(); + +void DisplayHandler::epd_write_cmd(uint8_t cmd) { + ESP_LOGI("DisplayHandler", "epd_write_cmd: waiting to send 0x%02X", cmd); + xSemaphoreTake(_spi_mutex, portMAX_DELAY); + _dangerous_epd_write_cmd_without_lock(cmd); + xSemaphoreGive(_spi_mutex); + ESP_LOGI("DisplayHandler", "epd_write_cmd: 0x%02X done", cmd); +} + +void DisplayHandler::epd_write_data(uint8_t data) { + ESP_LOGI("DisplayHandler", "epd_write_data: waiting to send 0x%02X", data); + xSemaphoreTake(_spi_mutex, portMAX_DELAY); + _dangerous_epd_write_data_without_lock(data); + xSemaphoreGive(_spi_mutex); + ESP_LOGI("DisplayHandler", "epd_write_data: 0x%02X done", data); +} + +void DisplayHandler::epd_write_cmd_with_data(uint8_t cmd, const uint8_t* data, size_t data_len) { + ESP_LOGI("DisplayHandler", "epd_write_cmd_with_data: waiting to send cmd 0x%02X with %u bytes of data", cmd, (unsigned)data_len); + xSemaphoreTake(_spi_mutex, portMAX_DELAY); + _dangerous_epd_write_cmd_without_lock(cmd); + for (size_t i = 0; i < data_len; ++i) { + _dangerous_epd_write_data_without_lock(data[i]); + } + xSemaphoreGive(_spi_mutex); + ESP_LOGI("DisplayHandler", "epd_write_cmd_with_data: cmd 0x%02X with %u bytes of data done", cmd, (unsigned)data_len); +} + +// +// Private methods +// + +void DisplayHandler::_dangerous_epd_write_cmd_without_lock(uint8_t cmd) { + ESP_LOGI("DisplayHandler", "_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("DisplayHandler", "Failed to send data 0x%02X", cmd); } else { - printf("EInkDisplayHandler::task_adapter received null pointer\n"); - } - // If run_event_loop ever returns, delete the task. - vTaskDelete(NULL); -} - -void EInkDisplayHandler::run_event_loop() { - for (;;) { - vTaskDelay(pdMS_TO_TICKS(1000)); + ESP_LOGI("DisplayHandler", "_dangerous_epd_write_cmd_without_lock: 0x%02X sent", cmd); } } -shutdown_display_handlerFunc EInkDisplayHandler::get_shutdown_display_handler() { - return nullptr; +void DisplayHandler::_dangerous_epd_write_data_without_lock(uint8_t data) { + ESP_LOGI("DisplayHandler", "_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("DisplayHandler", "Failed to send data 0x%02X", data); + } else { + ESP_LOGI("DisplayHandler", "_dangerous_epd_write_data_without_lock: 0x%02X sent", data); + } } -restart_display_handlerFunc EInkDisplayHandler::get_restart_display_handler() { - return nullptr; +// required to be called by inheriting class after SPI device is created +void DisplayHandler::_epd_init(void) { + ESP_LOGI("DisplayHandler", "Initializing EPD..."); + // 1. Hardware Reset + gpio_set_level(PIN_RST, 0); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(PIN_RST, 1); + vTaskDelay(pdMS_TO_TICKS(10)); + + // 2. Initialization Sequence + const uint8_t panel_setting_data[] = { 0x1F }; + epd_write_cmd_with_data(0x00, panel_setting_data, 1); // Panel Setting + vTaskDelay(pdMS_TO_TICKS(10)); + const uint8_t vcom_data[] = { 0x10, 0x07 }; + epd_write_cmd_with_data(0x50, vcom_data, 2); // VCOM + vTaskDelay(pdMS_TO_TICKS(10)); + epd_write_cmd(0x04); // Power ON + vTaskDelay(pdMS_TO_TICKS(100)); // Wait for power on + + // Check BUSY pin + ESP_LOGI("DisplayHandler", "Waiting for EPD to be ready..."); + while (gpio_get_level(PIN_BUSY) == 1) { + vTaskDelay(pdMS_TO_TICKS(10)); + } + ESP_LOGI("DisplayHandler", "EPD is ready."); + const uint8_t booster_data[] = { 0x27, 0x27 }; + epd_write_cmd_with_data(0x06, booster_data, 2); // Booster Soft Start + vTaskDelay(pdMS_TO_TICKS(10)); +} + +void DisplayHandler::_touch_init(void) { + ESP_LOGI("DisplayHandler", "Initializing touch..."); + // 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; + + i2c_param_config(I2C_NUM_0, &conf); + i2c_driver_install(I2C_NUM_0, I2C_MODE_MASTER, 0, 0, 0); + 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); + + esp_lcd_touch_config_t tp_cfg = {}; + tp_cfg.x_max = 800; + tp_cfg.y_max = 480; + tp_cfg.rst_gpio_num = PIN_RST; + tp_cfg.int_gpio_num = PIN_TOUCH_IRQ; + + esp_lcd_touch_new_i2c_gt911(_tp_io_handle, &tp_cfg, &_tp_handle); + ESP_LOGI("DisplayHandler", "GT911 touch controller initialized"); } diff --git a/main/display/display.h b/main/display/display.h index b31efe7..729a6ee 100644 --- a/main/display/display.h +++ b/main/display/display.h @@ -1,48 +1,38 @@ #pragma once - -#include -#include +#include "driver/spi_master.h" +#include "driver/gpio.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" -#include "esp_system.h" - -typedef void (*shutdown_display_handlerFunc)(void); -typedef void (*restart_display_handlerFunc)(void); +#include "esp_lcd_touch_gt911.h" +#include "display/constants.h" +#include class DisplayHandler { public: - DisplayHandler(QueueHandle_t touch_queue, SemaphoreHandle_t lvgl_mutex); - // the system_event_group is used to set display-ready bit - virtual void init(EventGroupHandle_t system_event_group) = 0; - virtual void start_event_loop() = 0; - // get a handler to perform display shutdown cleanup, this is called after event loop ends and DisplayHandler is deleted - virtual shutdown_display_handlerFunc get_shutdown_display_handler() = 0; - virtual restart_display_handlerFunc get_restart_display_handler() = 0; - virtual ~DisplayHandler() = 0; + DisplayHandler( + EventGroupHandle_t system_event_group + ) : _system_event_group(system_event_group) { } + ~DisplayHandler(); + + // required to be called by inheriting class after SPI device is created + // set set_display_ready to false if further initialization is needed before marking display ready + void init_devices(bool set_display_ready = true); + + void epd_write_cmd(uint8_t cmd); + void epd_write_data(uint8_t data); + + void epd_write_cmd_with_data(uint8_t cmd, const uint8_t* data, size_t data_len); private: - DisplayHandler(const DisplayHandler&) = delete; - DisplayHandler& operator=(const DisplayHandler&) = delete; -}; - -class EInkDisplayHandler : public DisplayHandler { -public: - EInkDisplayHandler(QueueHandle_t touch_queue, SemaphoreHandle_t lvgl_mutex); - void init(EventGroupHandle_t system_event_group) override; - void start_event_loop() override; - shutdown_display_handlerFunc get_shutdown_display_handler() override; - restart_display_handlerFunc get_restart_display_handler() override; - ~EInkDisplayHandler() override; - -private: - // Task adapter used for FreeRTOS task creation. It forwards to the - // instance `run_event_loop()` method using the `this` pointer passed - // as the task parameter. - static void task_adapter(void* arg); - - // Instance method that implements the display task loop. - void run_event_loop(); - // prevent copying - EInkDisplayHandler(const EInkDisplayHandler&) = delete; - EInkDisplayHandler& operator=(const EInkDisplayHandler&) = delete; + SemaphoreHandle_t _spi_mutex = xSemaphoreCreateMutex(); + spi_device_handle_t _spi = nullptr; + EventGroupHandle_t _system_event_group = nullptr; + esp_lcd_panel_io_handle_t _tp_io_handle = nullptr; + esp_lcd_touch_handle_t _tp_handle = nullptr; + + void _dangerous_epd_write_cmd_without_lock(uint8_t cmd); + void _dangerous_epd_write_data_without_lock(uint8_t data); + + void _epd_init(void); + void _touch_init(void); }; From 4f7418c77ac5b2de34fc352ec4922ef160b6fef6 Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Sat, 24 Jan 2026 10:38:58 +0800 Subject: [PATCH 06/16] feat: enhance display handling with EInkDisplayHandler class and update DisplayHandler interface --- main/display/display.h | 12 ++++-- main/display/eink_display_handler.h | 58 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 main/display/eink_display_handler.h diff --git a/main/display/display.h b/main/display/display.h index 729a6ee..dcffdf2 100644 --- a/main/display/display.h +++ b/main/display/display.h @@ -12,18 +12,22 @@ public: DisplayHandler( EventGroupHandle_t system_event_group ) : _system_event_group(system_event_group) { } - ~DisplayHandler(); - + virtual ~DisplayHandler(); + // required to be called by inheriting class after SPI device is created // set set_display_ready to false if further initialization is needed before marking display ready - void init_devices(bool set_display_ready = true); + virtual void init_devices(bool set_display_ready = true); + +protected: + // Allow derived classes to access touch handle + esp_lcd_touch_handle_t get_touch_handle() const { return _tp_handle; } void epd_write_cmd(uint8_t cmd); void epd_write_data(uint8_t data); void epd_write_cmd_with_data(uint8_t cmd, const uint8_t* data, size_t data_len); -private: +protected: SemaphoreHandle_t _spi_mutex = xSemaphoreCreateMutex(); spi_device_handle_t _spi = nullptr; EventGroupHandle_t _system_event_group = nullptr; diff --git a/main/display/eink_display_handler.h b/main/display/eink_display_handler.h new file mode 100644 index 0000000..4c5b53a --- /dev/null +++ b/main/display/eink_display_handler.h @@ -0,0 +1,58 @@ +#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) // 1-bit per pixel + +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; + + // 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; + + // 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); + + // 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); +}; From d2485576140ede5819d5027c03919d8e5db5406f Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Sat, 24 Jan 2026 10:39:03 +0800 Subject: [PATCH 07/16] feat: implement EInkDisplayHandler for enhanced E-Ink display management and touch input handling --- main/display/display.cpp | 12 +- main/display/eink_display_handler.cpp | 415 ++++++++++++++++++++++++++ 2 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 main/display/eink_display_handler.cpp diff --git a/main/display/display.cpp b/main/display/display.cpp index 961e21b..56938de 100644 --- a/main/display/display.cpp +++ b/main/display/display.cpp @@ -107,13 +107,19 @@ void DisplayHandler::_epd_init(void) { // Check BUSY pin ESP_LOGI("DisplayHandler", "Waiting for EPD to be ready..."); - while (gpio_get_level(PIN_BUSY) == 1) { + while (gpio_get_level(PIN_BUSY) == 0) { // 0=BUSY, 1=FREE vTaskDelay(pdMS_TO_TICKS(10)); } ESP_LOGI("DisplayHandler", "EPD is ready."); - const uint8_t booster_data[] = { 0x27, 0x27 }; - epd_write_cmd_with_data(0x06, booster_data, 2); // Booster Soft Start + const uint8_t booster_data[] = { 0x27, 0x27, 0x18, 0x17 }; + epd_write_cmd_with_data(0x06, booster_data, 4); // Booster Soft Start vTaskDelay(pdMS_TO_TICKS(10)); + + // Enhanced display drive commands + const uint8_t e0_data[] = { 0x02 }; + epd_write_cmd_with_data(0xE0, e0_data, 1); + const uint8_t e5_data[] = { 0x5A }; + epd_write_cmd_with_data(0xE5, e5_data, 1); } void DisplayHandler::_touch_init(void) { diff --git a/main/display/eink_display_handler.cpp b/main/display/eink_display_handler.cpp new file mode 100644 index 0000000..164efd2 --- /dev/null +++ b/main/display/eink_display_handler.cpp @@ -0,0 +1,415 @@ +#include "display/eink_display_handler.h" +#include "display/constants.h" +#include "common/constants.h" +#include "esp_log.h" +#include "esp_heap_caps.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 (_touch_task_handle != nullptr) { + vTaskDelete(_touch_task_handle); + } + if (_lvgl_display != nullptr) { + lvgl_port_remove_disp(_lvgl_display); + } + 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 + io_conf.pin_bit_mask = (1ULL << PIN_BUSY); + io_conf.mode = GPIO_MODE_INPUT; + io_conf.pull_up_en = GPIO_PULLUP_ENABLE; + 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 = 10 * 1000 * 1000; // 10 MHz (max for GDEY075T7) + devcfg.mode = 0; // SPI mode 0 + devcfg.spics_io_num = PIN_CS; + devcfg.queue_size = 1; + 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 + + // 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 + + // Create LVGL display driver + lvgl_port_display_cfg_t disp_cfg = {}; + + disp_cfg.io_handle = nullptr; + disp_cfg.panel_handle = nullptr; + disp_cfg.buffer_size = DISPLAY_WIDTH * 40; // 40 lines buffer + disp_cfg.double_buffer = false; + disp_cfg.hres = DISPLAY_WIDTH; + disp_cfg.vres = DISPLAY_HEIGHT; + disp_cfg.monochrome = true; + + disp_cfg.rotation.swap_xy = false; + disp_cfg.rotation.mirror_x = false; + disp_cfg.rotation.mirror_y = false; + + disp_cfg.flags.buff_dma = _framebuffer_in_psram ? false : true; + disp_cfg.flags.buff_spiram = _framebuffer_in_psram; + disp_cfg.flags.swap_bytes = false; + disp_cfg.flags.full_refresh = false; + disp_cfg.flags.direct_mode = false; + + _lvgl_display = lvgl_port_add_disp(&disp_cfg); + if (_lvgl_display == nullptr) { + ESP_LOGE(TAG, "Failed to create LVGL display"); + return; + } + + // Set custom flush callback + lv_display_set_flush_cb(_lvgl_display, _lvgl_flush_cb); + lv_display_set_user_data(_lvgl_display, this); + + ESP_LOGI(TAG, "LVGL display registered"); + + // Register GT911 touch input with LVGL + const lvgl_port_touch_cfg_t touch_cfg = { + .disp = _lvgl_display, + .handle = get_touch_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"); + + // Perform initial full refresh to clear display + ESP_LOGI(TAG, "Performing initial display clear..."); + _perform_full_refresh(_framebuffer); + + // 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 + if (handler->is_busy()) { + ESP_LOGW(TAG, "Display busy, 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) + 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]; + } + } + } + + // 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); +} + +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) { + 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) - typically all zeros for full refresh + epd_write_cmd(0x10); + + xSemaphoreTake(_spi_mutex, portMAX_DELAY); + gpio_set_level(PIN_DC, 1); // Data mode + + for (size_t i = 0; i < DISPLAY_BUFFER_SIZE; i++) { + spi_transaction_t t = {}; + t.length = 8; + uint8_t byte = 0x00; // Old data (cleared screen) + t.tx_buffer = &byte; + spi_device_polling_transmit(_spi, &t); + } + xSemaphoreGive(_spi_mutex); + + // Step 2: Write new data (0x13) with data inversion + epd_write_cmd(0x13); + + xSemaphoreTake(_spi_mutex, portMAX_DELAY); + gpio_set_level(PIN_DC, 1); // Data mode + + for (size_t i = 0; i < DISPLAY_BUFFER_SIZE; i++) { + spi_transaction_t t = {}; + t.length = 8; + uint8_t byte = ~framebuffer[i]; // Invert data per manufacturer spec + t.tx_buffer = &byte; + spi_device_polling_transmit(_spi, &t); + } + xSemaphoreGive(_spi_mutex); + + // Step 3: Trigger display refresh (DRF) + epd_write_cmd(0x12); + vTaskDelay(pdMS_TO_TICKS(10)); + + // 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 with inversion (0x13 command) + epd_write_cmd(0x13); + + xSemaphoreTake(_spi_mutex, portMAX_DELAY); + gpio_set_level(PIN_DC, 1); // Data mode + + for (size_t i = 0; i < DISPLAY_BUFFER_SIZE; i++) { + spi_transaction_t t = {}; + t.length = 8; + uint8_t byte = ~framebuffer[i]; // Invert data per manufacturer spec + t.tx_buffer = &byte; + spi_device_polling_transmit(_spi, &t); + } + xSemaphoreGive(_spi_mutex); + + // Step 5: Trigger partial display refresh (DRF) + epd_write_cmd(0x12); + vTaskDelay(pdMS_TO_TICKS(10)); + + // 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 timeout = 0; + while (gpio_get_level(PIN_BUSY) == BUSY_INACTIVE_LEVEL) { // 0=BUSY, 1=FREE + vTaskDelay(pdMS_TO_TICKS(100)); + timeout++; + if (timeout > 50) { // 5 second timeout + ESP_LOGW(TAG, "Display BUSY timeout!"); + break; + } + } + ESP_LOGI(TAG, "Display ready"); +} + +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); +} From 6ad55c7efc20874eb52fa47203346dce7ef7e3c7 Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Sat, 24 Jan 2026 10:39:16 +0800 Subject: [PATCH 08/16] feat: add AppRegistry, RootLayout, UIHandler, and UIApp classes for improved UI management --- main/ui/app_registry.h | 39 +++++++++++ main/ui/root_layout.h | 138 ++++++++++++++++++++++++++++++++++++++ main/ui/ui.h | 0 main/ui/ui_app.h | 98 +++++++++++++++++++++++++++ main/ui/ui_handler.h | 147 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 422 insertions(+) create mode 100644 main/ui/app_registry.h create mode 100644 main/ui/root_layout.h delete mode 100644 main/ui/ui.h create mode 100644 main/ui/ui_app.h create mode 100644 main/ui/ui_handler.h diff --git a/main/ui/app_registry.h b/main/ui/app_registry.h new file mode 100644 index 0000000..1b3b463 --- /dev/null +++ b/main/ui/app_registry.h @@ -0,0 +1,39 @@ +#pragma once +#include "ui/ui_app.h" +#include + +/** + * @brief Registry for all available apps + * + * This singleton class maintains a list of all registered + * AppDescriptor instances, allowing the UIHandler or other + * components to query available apps. + */ +class AppRegistry { +public: + static AppRegistry& instance() { + static AppRegistry registry; + return registry; + } + + AppRegistry(const AppRegistry&) = delete; + void operator=(const AppRegistry&) = delete; + AppRegistry(AppRegistry&&) = delete; + void operator=(AppRegistry&&) = delete; + + + // Register a new app descriptor + // The registry takes ownership of the descriptor pointer. + void register_app(AppDescriptor* app_descriptor) { + _app_descriptors.push_back(app_descriptor); + } + + const std::vector& get_app_descriptors() const { + return _app_descriptors; + } + +private: + AppRegistry() = default; + ~AppRegistry() = default; + std::vector _app_descriptors = {}; +}; \ No newline at end of file diff --git a/main/ui/root_layout.h b/main/ui/root_layout.h new file mode 100644 index 0000000..c4a4f28 --- /dev/null +++ b/main/ui/root_layout.h @@ -0,0 +1,138 @@ +#pragma once + +#include "lvgl.h" +#include "esp_err.h" +#include + +// Forward declaration +class UIHandler; + +/** + * @brief Root Layout Manager - manages the main screen layout + * + * The RootLayout class is responsible for: + * - Creating and managing the main screen structure (header, app container, nav bar) + * - Rendering app icons from the AppRegistry + * - Managing the back button + * - Updating header content + */ +class RootLayout { +public: + /** + * @brief Construct a new RootLayout object + * + * @param ui_handler Pointer to the UIHandler (for callbacks) + */ + RootLayout(UIHandler* ui_handler); + + /** + * @brief Initialize the layout + * + * Creates the main screen with header, app container, and navigation bar. + * + * @param parent Parent LVGL object to attach layout to + * @return ESP_OK on success, error code otherwise + */ + esp_err_t init(lv_obj_t* parent); + + /** + * @brief Deinitialize the layout + * + * Cleans up all layout widgets. + * + * @return ESP_OK on success, error code otherwise + */ + esp_err_t deinit(void); + + /** + * @brief Render app icons in the navigation bar + * + * Queries the AppRegistry for all registered apps and + * renders their icons in the navigation bar. Also creates + * the back button. + * + * @return ESP_OK on success, error code otherwise + */ + esp_err_t render_app_icons(void); + + /** + * @brief Update header with app name + * + * @param app_name Name to display in header (nullptr for default) + */ + void update_header(std::string app_name); + + /** + * @brief Show the back button + */ + void show_back_button(void); + + /** + * @brief Hide the back button + */ + void hide_back_button(void); + + /** + * @brief Get the header object + * + * @return lv_obj_t* pointer to the header container + */ + lv_obj_t* get_header(void) const { + return _header; + } + + /** + * @brief Get the app container (where apps render) + * + * @return lv_obj_t* pointer to the app container + */ + lv_obj_t* get_app_container(void) const { + return _app_container; + } + + /** + * @brief Get the navigation bar object + * + * @return lv_obj_t* pointer to the navigation bar container + */ + lv_obj_t* get_nav_bar(void) const { + return _nav_bar; + } + +private: + UIHandler* _ui_handler = nullptr; ///< Reference to UIHandler for callbacks + lv_obj_t* _header = nullptr; ///< Header area (top) + lv_obj_t* _header_label = nullptr; ///< Header text label + lv_obj_t* _app_container = nullptr; ///< Container for app widgets (middle) + lv_obj_t* _nav_bar = nullptr; ///< Navigation bar (bottom) + lv_obj_t* _back_button = nullptr; ///< Back button in navigation bar + + /** + * @brief Create the layout structure + * + * Sets up header, app container, and navigation bar with + * appropriate dimensions and positioning. + * + * @param parent Parent object to attach layout to + * @return ESP_OK on success, error code otherwise + */ + esp_err_t create_layout(lv_obj_t* parent); + + /** + * @brief Handle app icon click event + * + * Static callback for LVGL event handling. + * + * @param event LVGL event object + */ + static void on_app_icon_clicked(lv_event_t* event); + + /** + * @brief Handle back button click event + * + * Static callback for LVGL event handling. + * + * @param event LVGL event object + */ + static void on_back_button_clicked(lv_event_t* event); +}; diff --git a/main/ui/ui.h b/main/ui/ui.h deleted file mode 100644 index e69de29..0000000 diff --git a/main/ui/ui_app.h b/main/ui/ui_app.h new file mode 100644 index 0000000..42b6bac --- /dev/null +++ b/main/ui/ui_app.h @@ -0,0 +1,98 @@ +#pragma once + +#include "lvgl.h" +#include "esp_err.h" +#include + +/** + * @brief Base class for all UI applications + * + * All UI applications (apps) must inherit from this class. + * Each app is responsible for managing its own widgets within + * the provided LVGL container. The UIHandler will manage the + * lifecycle of apps and event routing. + */ +class UIApp { +public: + virtual ~UIApp() = default; + + /** + * @brief Initialize the app with the given container + * + * The app should create all its widgets as children of the + * provided container. The container is already positioned + * between the header and navigation bar. + * + * @param container LVGL container object for this app + * @return ESP_OK on success, error code otherwise + */ + virtual esp_err_t init(lv_obj_t* container) = 0; + + /** + * @brief Deinitialize and clean up app resources + * + * The app should delete all widgets and release any resources. + * The container itself will be handled by UIHandler. + * + * @return ESP_OK on success, error code otherwise + */ + virtual esp_err_t deinit(void) = 0; + + /** + * @brief Get the display name of this app + * + * Used for logging and potentially showing in navigation. + * + * @return std::string app name + */ + virtual std::string get_name(void) const = 0; + + /** + * @brief Handle system events passed from UIHandler + * + * System events include network status changes, storage ready, + * display refresh, and other system-level events. + * + * @param event_type Type/ID of the event + * @param event_data Optional event data payload + */ + virtual void handle_event(uint32_t event_type, void* event_data = nullptr) { } + + virtual bool on_back_button_pressed(void) { + return false; // default: not handled + } + + /** + * @brief Get the app's root container + * + * @return lv_obj_t* pointer to the app's container + */ + lv_obj_t* get_container(void) const { + return _container; + } + +protected: + lv_obj_t* _container = nullptr; ///< LVGL container provided by UIHandler +}; + + +class AppDescriptor { +public: + virtual ~AppDescriptor() = default; + virtual void draw_icon(lv_obj_t* parent) = 0; + + std::string get_name() const { + return _name; + } + + UIApp* get_app_instance() const { + return _app_instance; + } + +protected: + AppDescriptor(std::string name, UIApp* app_instance) + : _name(name), _app_instance(app_instance) { } + + std::string _name; + UIApp* _app_instance; +}; \ No newline at end of file diff --git a/main/ui/ui_handler.h b/main/ui/ui_handler.h new file mode 100644 index 0000000..4ec419a --- /dev/null +++ b/main/ui/ui_handler.h @@ -0,0 +1,147 @@ +#pragma once + +#include "ui_app.h" +#include "app_registry.h" +#include "root_layout.h" +#include "esp_err.h" + +// Forward declaration +class RootLayout; + +/** + * @brief UI Handler - manages app lifecycle and rendering + * + * The UIHandler manages: + * - Creation and destruction of UI apps + * - Switching between apps + * - Main screen layout (header, app container, navigation bar) + * - System event routing to active app + * - Displaying special screens (shutdown, etc.) + */ +class UIHandler { +public: + /** + * @brief Initialize the UI system with default layout + * + * Creates the main screen with: + * - Header area (top) + * - App container (middle) + * - Navigation bar (bottom) + * + * @return ESP_OK on success, error code otherwise + */ + esp_err_t init(void); + + /** + * @brief Deinitialize the UI system + * + * Cleans up the current app and destroys the main screen. + * + * @return ESP_OK on success, error code otherwise + */ + esp_err_t deinit(void); + + /** + * @brief Switch to a new app + * + * Deinitializes the current app (if any), initializes the new app, + * and updates the display. + * + * @param app Pointer to the new app to switch to + * @return ESP_OK on success, error code otherwise + */ + esp_err_t switch_app(UIApp* app); + + /** + * @brief Switch to an app by its descriptor + * + * Convenience method that extracts the UIApp from the descriptor + * and calls switch_app(). + * + * @param app_descriptor Pointer to the app descriptor + * @return ESP_OK on success, error code otherwise + */ + esp_err_t switch_app(AppDescriptor* app_descriptor); + + /** + * @brief Get the currently active app + * + * @return Pointer to the active UIApp, or nullptr if none + */ + UIApp* get_active_app(void) const { + return _active_app; + } + + /** + * @brief Route a system event to the active app + * + * If an app is active, this forwards the event to it. + * + * @param event_type Type/ID of the event + * @param event_data Optional event data payload + */ + void route_event(uint32_t event_type, void* event_data = nullptr); + + /** + * @brief Display shutdown screen + * + * Shows a shutdown screen with a message. Typically called + * before the system enters deep sleep or powers off. + * + * @param message Optional message to display (e.g., "Shutting down...") + * @return ESP_OK on success, error code otherwise + */ + esp_err_t show_shutdown_screen(std::string message = ""); + + /** + * @brief Get the main screen object + * + * @return lv_obj_t* pointer to the main screen + */ + lv_obj_t* get_main_screen(void) const { + return _main_screen; + } + + /** + * @brief Get the app container (where apps render) + * + * @return lv_obj_t* pointer to the app container + */ + lv_obj_t* get_app_container(void) const { + return _root_layout ? _root_layout->get_app_container() : nullptr; + } + + /** + * @brief Get the header object + * + * @return lv_obj_t* pointer to the header container + */ + lv_obj_t* get_header(void) const { + return _root_layout ? _root_layout->get_header() : nullptr; + } + + /** + * @brief Get the navigation bar object + * + * @return lv_obj_t* pointer to the navigation bar container + */ + lv_obj_t* get_nav_bar(void) const { + return _root_layout ? _root_layout->get_nav_bar() : nullptr; + } + + /** + * @brief Return to main screen (deinit app and show app icons) + * + * Deinitializes the active app and displays the app icons + * in the navigation bar, returning to the home screen. + * + * @return ESP_OK on success, error code otherwise + */ + esp_err_t return_to_main_screen(void); + +private: + lv_obj_t* _main_screen = nullptr; ///< Root screen + RootLayout* _root_layout = nullptr; ///< Root layout manager + UIApp* _active_app = nullptr; ///< Currently active app + UIApp* _shutdown_app = nullptr; ///< Cached shutdown app +}; From 0c26d915653f0a76f74bdd73be1cfa96e384242c Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Sat, 24 Jan 2026 10:39:30 +0800 Subject: [PATCH 09/16] feat: implement RootLayout and UIHandler for improved UI structure and app management --- main/ui/root_layout.cpp | 220 ++++++++++++++++++++++++++++++++++++++++ main/ui/ui_handler.cpp | 201 ++++++++++++++++++++++++++++++++++++ 2 files changed, 421 insertions(+) create mode 100644 main/ui/root_layout.cpp create mode 100644 main/ui/ui_handler.cpp diff --git a/main/ui/root_layout.cpp b/main/ui/root_layout.cpp new file mode 100644 index 0000000..fd745a3 --- /dev/null +++ b/main/ui/root_layout.cpp @@ -0,0 +1,220 @@ +#include "ui/root_layout.h" +#include "ui/ui_handler.h" +#include "ui/app_registry.h" +#include "esp_log.h" + +#define TAG "RootLayout" + +// Display dimensions +#define DISPLAY_WIDTH 800 +#define DISPLAY_HEIGHT 480 + +// Layout dimensions +#define HEADER_HEIGHT 40 +#define NAV_BAR_HEIGHT 50 +#define APP_CONTAINER_HEIGHT (DISPLAY_HEIGHT - HEADER_HEIGHT - NAV_BAR_HEIGHT) + +RootLayout::RootLayout(UIHandler* ui_handler) + : _ui_handler(ui_handler) { } + +esp_err_t RootLayout::init(lv_obj_t* parent) { + if (!parent) { + ESP_LOGE(TAG, "Parent object is null"); + return ESP_ERR_INVALID_ARG; + } + + ESP_LOGI(TAG, "Initializing RootLayout"); + + if (create_layout(parent) != ESP_OK) { + ESP_LOGE(TAG, "Failed to create layout"); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "RootLayout initialized successfully"); + return ESP_OK; +} + +esp_err_t RootLayout::deinit(void) { + ESP_LOGI(TAG, "Deinitializing RootLayout"); + + // LVGL will handle cleanup when parent is destroyed + _header = nullptr; + _header_label = nullptr; + _app_container = nullptr; + _nav_bar = nullptr; + _back_button = nullptr; + + return ESP_OK; +} + +esp_err_t RootLayout::create_layout(lv_obj_t* parent) { + // Create header (top) + _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_style_border_width(_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_align(_header_label, LV_ALIGN_LEFT_MID, 10, 0); + + // Create app container (middle) + _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_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); + + // Create navigation bar (bottom) + _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); + + ESP_LOGI(TAG, "Layout created: Header=%d, AppContainer=%d, NavBar=%d", + HEADER_HEIGHT, APP_CONTAINER_HEIGHT, NAV_BAR_HEIGHT); + + return ESP_OK; +} + +void RootLayout::update_header(std::string app_name) { + if (!_header_label) { + return; + } + + if (app_name.empty() == false) { + lv_label_set_text(_header_label, app_name.c_str()); + } else { + lv_label_set_text(_header_label, "App"); + } +} + +esp_err_t RootLayout::render_app_icons(void) { + if (!_nav_bar) { + ESP_LOGE(TAG, "Navigation bar not initialized"); + return ESP_FAIL; + } + + // Clear existing nav bar content + lv_obj_clean(_nav_bar); + + // Get all registered apps from registry + const auto& app_descriptors = AppRegistry::instance().get_app_descriptors(); + + if (app_descriptors.empty()) { + ESP_LOGW(TAG, "No apps registered in AppRegistry"); + lv_obj_t* nav_label = lv_label_create(_nav_bar); + lv_label_set_text(nav_label, "No apps available"); + lv_obj_set_style_text_color(nav_label, lv_color_white(), 0); + lv_obj_align(nav_label, LV_ALIGN_CENTER, 0, 0); + return ESP_OK; + } + + ESP_LOGI(TAG, "Rendering %d app icons", app_descriptors.size()); + + // Calculate icon spacing + int icon_count = app_descriptors.size(); + int icon_spacing = DISPLAY_WIDTH / (icon_count + 1); + int x_offset = icon_spacing; + + // Render each app icon + 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_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); + + // 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; +} + +void RootLayout::show_back_button(void) { + if (_back_button) { + lv_obj_clear_flag(_back_button, LV_OBJ_FLAG_HIDDEN); + } +} + +void RootLayout::hide_back_button(void) { + if (_back_button) { + lv_obj_add_flag(_back_button, LV_OBJ_FLAG_HIDDEN); + } +} + +void RootLayout::on_app_icon_clicked(lv_event_t* event) { + lv_obj_t* icon_container = static_cast(lv_event_get_target(event)); + UIHandler* handler = static_cast(lv_event_get_user_data(event)); + AppDescriptor* descriptor = static_cast(lv_obj_get_user_data(icon_container)); + + if (!handler || !descriptor) { + ESP_LOGE(TAG, "Invalid event data in app icon click"); + return; + } + + ESP_LOGI(TAG, "App icon clicked: %s", descriptor->get_name().c_str()); + handler->switch_app(descriptor); +} + +void RootLayout::on_back_button_clicked(lv_event_t* event) { + UIHandler* handler = static_cast(lv_event_get_user_data(event)); + + if (!handler) { + ESP_LOGE(TAG, "Invalid handler in back button click"); + return; + } + + // Get the active app + UIApp* active_app = handler->get_active_app(); + if (!active_app) { + ESP_LOGW(TAG, "Back button pressed but no active app"); + return; + } + + // Let the app handle the back button press + bool handled = active_app->on_back_button_pressed(); + + if (handled) { + ESP_LOGI(TAG, "Back button handled by app: %s", active_app->get_name()); + } else { + ESP_LOGI(TAG, "Back button not handled by app, returning to main screen"); + handler->return_to_main_screen(); + } +} diff --git a/main/ui/ui_handler.cpp b/main/ui/ui_handler.cpp new file mode 100644 index 0000000..e31ebfd --- /dev/null +++ b/main/ui/ui_handler.cpp @@ -0,0 +1,201 @@ +#include "ui/ui_handler.h" +#include "ui/root_layout.h" +#include "ui/app_registry.h" +#include "esp_log.h" + +#define TAG "UIHandler" + +// Display dimensions from constants.h +#define DISPLAY_WIDTH 800 +#define DISPLAY_HEIGHT 480 + +// Layout dimensions +#define HEADER_HEIGHT 40 +#define NAV_BAR_HEIGHT 50 +#define _APP_CONTAINERHEIGHT (DISPLAY_HEIGHT - HEADER_HEIGHT - NAV_BAR_HEIGHT) + +esp_err_t UIHandler::init(void) { + ESP_LOGI(TAG, "Initializing UIHandler"); + + // Create main screen + _main_screen = lv_obj_create(NULL); + if (!_main_screen) { + ESP_LOGE(TAG, "Failed to create main screen"); + return ESP_FAIL; + } + lv_obj_set_style_bg_color(_main_screen, lv_color_black(), 0); + lv_obj_set_size(_main_screen, DISPLAY_WIDTH, DISPLAY_HEIGHT); + + // Create root layout + _root_layout = new RootLayout(this); + if (!_root_layout) { + ESP_LOGE(TAG, "Failed to allocate RootLayout"); + return ESP_FAIL; + } + + if (_root_layout->init(_main_screen) != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize root layout"); + delete _root_layout; + _root_layout = nullptr; + return ESP_FAIL; + } + + // Render app icons from registry + if (_root_layout->render_app_icons() != ESP_OK) { + ESP_LOGW(TAG, "Failed to render app icons"); + } + + // Load the main screen + lv_screen_load(_main_screen); + + ESP_LOGI(TAG, "UIHandler initialized successfully"); + return ESP_OK; +} + +esp_err_t UIHandler::deinit(void) { + ESP_LOGI(TAG, "Deinitializing UIHandler"); + + // Deinit current app + if (_active_app) { + if (_active_app->deinit() != ESP_OK) { + ESP_LOGW(TAG, "Error deinitializing active app: %s", _active_app->get_name()); + } + _active_app = nullptr; + } + + // Delete shutdown app if cached + if (_shutdown_app) { + delete _shutdown_app; + _shutdown_app = nullptr; + } + + // Clean up root layout + if (_root_layout) { + _root_layout->deinit(); + delete _root_layout; + _root_layout = nullptr; + } + + // Main screen will be cleaned up by LVGL + _main_screen = nullptr; + + return ESP_OK; +} + +esp_err_t UIHandler::switch_app(UIApp* app) { + if (!app) { + ESP_LOGE(TAG, "Cannot switch to null app"); + return ESP_ERR_INVALID_ARG; + } + + lv_obj_t* app_container = get_app_container(); + if (!app_container) { + ESP_LOGE(TAG, "App container not initialized"); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "Switching to app: %s", app->get_name()); + + // Deinit current app + if (_active_app) { + if (_active_app->deinit() != ESP_OK) { + ESP_LOGW(TAG, "Error deinitializing app: %s", _active_app->get_name()); + } + } + + // Clear the app container + lv_obj_clean(app_container); + + // Initialize new app + if (app->init(app_container) != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize app: %s", app->get_name()); + _active_app = nullptr; + return ESP_FAIL; + } + + _active_app = app; + + // Update header through RootLayout + if (_root_layout) { + _root_layout->update_header(_active_app->get_name()); + _root_layout->show_back_button(); + } + + return ESP_OK; +} + +esp_err_t UIHandler::switch_app(AppDescriptor* app_descriptor) { + if (!app_descriptor) { + ESP_LOGE(TAG, "Cannot switch to null app descriptor"); + return ESP_ERR_INVALID_ARG; + } + + UIApp* app = app_descriptor->get_app_instance(); + if (!app) { + ESP_LOGE(TAG, "App descriptor has null app instance"); + return ESP_ERR_INVALID_ARG; + } + + return switch_app(app); +} + +void UIHandler::route_event(uint32_t event_type, void* event_data) { + if (_active_app) { + _active_app->handle_event(event_type, event_data); + } +} + +esp_err_t UIHandler::show_shutdown_screen(std::string message) { + ESP_LOGI(TAG, "Showing shutdown screen"); + + lv_obj_t* app_container = get_app_container(); + if (!app_container) { + ESP_LOGE(TAG, "App container not initialized"); + return ESP_FAIL; + } + + // Clear current app reference + _active_app = nullptr; + + // Clear the app container + lv_obj_clean(app_container); + + // Create shutdown message + lv_obj_t* shutdown_label = lv_label_create(app_container); + lv_label_set_text(shutdown_label, message.empty() ? "Shutting down..." : message.c_str()); + lv_obj_set_style_text_color(shutdown_label, lv_color_white(), 0); + lv_obj_align(shutdown_label, LV_ALIGN_CENTER, 0, 0); + + // Update header through RootLayout + if (_root_layout) { + _root_layout->update_header("System Shutdown"); + } + + return ESP_OK; +} + +esp_err_t UIHandler::return_to_main_screen(void) { + ESP_LOGI(TAG, "Returning to main screen"); + + // Deinit current app + if (_active_app) { + if (_active_app->deinit() != ESP_OK) { + ESP_LOGW(TAG, "Error deinitializing app: %s", _active_app->get_name()); + } + _active_app = nullptr; + } + + // Clear the app container + lv_obj_t* app_container = get_app_container(); + if (app_container) { + lv_obj_clean(app_container); + } + + // Update header and hide back button through RootLayout + if (_root_layout) { + _root_layout->update_header(""); + _root_layout->hide_back_button(); + } + + return ESP_OK; +} From ccae9e89da7f2c8b15321bb24ae712da69d2ede3 Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Sat, 24 Jan 2026 10:39:37 +0800 Subject: [PATCH 10/16] feat: add DemoApp and ShutdownApp classes for interactive UI components and shutdown management --- main/ui/apps/demo_app.h | 53 +++++++++++++++++++++++++++++++++++++ main/ui/apps/shutdown_app.h | 39 +++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 main/ui/apps/demo_app.h create mode 100644 main/ui/apps/shutdown_app.h diff --git a/main/ui/apps/demo_app.h b/main/ui/apps/demo_app.h new file mode 100644 index 0000000..afa9c62 --- /dev/null +++ b/main/ui/apps/demo_app.h @@ -0,0 +1,53 @@ +#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/shutdown_app.h b/main/ui/apps/shutdown_app.h new file mode 100644 index 0000000..87c72d4 --- /dev/null +++ b/main/ui/apps/shutdown_app.h @@ -0,0 +1,39 @@ +#pragma once + +#include "ui/ui_app.h" +#include "ui/app_registry.h" + +/** + * @brief Shutdown application - displays shutdown message + * + * Shown when the system is about to enter deep sleep or power off. + * Displays a message and optionally a spinner animation. + */ +class ShutdownApp : public UIApp { +public: + ShutdownApp(std::string message = ""); + virtual ~ShutdownApp() = default; + + esp_err_t init(lv_obj_t* container) override; + esp_err_t deinit(void) override; + std::string get_name(void) const override; + +private: + std::string _message; + lv_obj_t* _label_message = nullptr; +}; + +/** + * @brief AppDescriptor for ShutdownApp + * + * Note: Shutdown app is typically not shown in the navigation bar + * as it's only used during system shutdown. + */ +class ShutdownAppDescriptor : public AppDescriptor { +public: + ShutdownAppDescriptor(); + void draw_icon(lv_obj_t* parent) override; + +private: + static ShutdownApp* _app_instance; +}; From 86e102adc708c8001930eca612d40eb55e7a6570 Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Sat, 24 Jan 2026 10:39:44 +0800 Subject: [PATCH 11/16] feat: add DemoApp and ShutdownApp classes for interactive UI and shutdown management --- main/ui/apps/demo_app.cpp | 151 ++++++++++++++++++++++++++++++++++ main/ui/apps/shutdown_app.cpp | 64 ++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 main/ui/apps/demo_app.cpp create mode 100644 main/ui/apps/shutdown_app.cpp diff --git a/main/ui/apps/demo_app.cpp b/main/ui/apps/demo_app.cpp new file mode 100644 index 0000000..5d65300 --- /dev/null +++ b/main/ui/apps/demo_app.cpp @@ -0,0 +1,151 @@ +#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/shutdown_app.cpp b/main/ui/apps/shutdown_app.cpp new file mode 100644 index 0000000..19593cd --- /dev/null +++ b/main/ui/apps/shutdown_app.cpp @@ -0,0 +1,64 @@ +#include "apps/shutdown_app.h" +#include "esp_log.h" + +#define TAG "ShutdownApp" + +ShutdownApp::ShutdownApp(std::string message) + : _message(message.empty() ? "System Shutting Down..." : message) { } + +esp_err_t ShutdownApp::init(lv_obj_t* container) { + if (!container) { + ESP_LOGE(TAG, "Container is null"); + return ESP_ERR_INVALID_ARG; + } + + _container = container; + ESP_LOGI(TAG, "Initializing shutdown app with message: %s", _message.c_str()); + + // Main message label + _label_message = lv_label_create(_container); + lv_label_set_text(_label_message, _message.c_str()); + lv_obj_set_style_text_color(_label_message, lv_color_white(), 0); + lv_obj_align(_label_message, LV_ALIGN_CENTER, 0, 0); + + // Optional: Add spinner animation + lv_obj_t* spinner = lv_spinner_create(_container); + lv_obj_set_size(spinner, 80, 80); + lv_obj_align(spinner, LV_ALIGN_CENTER, 0, 80); + lv_obj_set_style_arc_color(spinner, lv_color_white(), LV_PART_INDICATOR); + + ESP_LOGI(TAG, "Shutdown app initialized successfully"); + return ESP_OK; +} + +esp_err_t ShutdownApp::deinit(void) { + ESP_LOGI(TAG, "Deinitializing shutdown app"); + _label_message = nullptr; + return ESP_OK; +} + +std::string ShutdownApp::get_name(void) const { + return "Shutdown"; +} + +// ShutdownAppDescriptor implementation +ShutdownApp* ShutdownAppDescriptor::_app_instance = nullptr; + +ShutdownAppDescriptor::ShutdownAppDescriptor() + : AppDescriptor("Shutdown", nullptr) { + // Create singleton app instance with default message + if (!_app_instance) { + _app_instance = new ShutdownApp(); + } + + // it's only used during system shutdown, not as a user-launchable app +} + +void ShutdownAppDescriptor::draw_icon(lv_obj_t* parent) { + // Create a simple icon (not normally shown in nav bar) + lv_obj_t* icon_label = lv_label_create(parent); + lv_label_set_text(icon_label, LV_SYMBOL_POWER "\nShutdown"); + 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); +} From 162b3710eb2e1f218fc58a2d3c43875cad3467b5 Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Sat, 24 Jan 2026 10:40:09 +0800 Subject: [PATCH 12/16] feat: Integrate LVGL and UI handling in app_main - Initialize LVGL with appropriate configuration and error handling. - Create and initialize UIHandler to manage app icons and interactions. - Register DemoApp and ShutdownApp with AppRegistry. - Implement touch task and display initialization for EInkDisplayHandler. - Handle shutdown signal by switching to ShutdownApp and performing cleanup. --- main/CMakeLists.txt | 4 +- main/lv_conf.h | 1520 +------------------------------------------ main/main.cpp | 63 +- 3 files changed, 55 insertions(+), 1532 deletions(-) diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 07d655a..7e072c4 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,6 +1,6 @@ -set(requires esp-tls spi_flash nvs_flash esp_event esp_netif esp_http_client esp_wifi esp_psram) +set(requires esp-tls spi_flash nvs_flash esp_event esp_netif esp_http_client esp_wifi esp_psram esp_lvgl_port) file(GLOB SRCS "main.cpp" "*.cpp" "*.c" "**/*.cpp" "**/*.c") idf_component_register(SRCS ${SRCS} PRIV_REQUIRES ${requires} - INCLUDE_DIRS "." "display" "network" "ui" "io" "common") + INCLUDE_DIRS "." "display" "network" "ui" "ui/apps" "io" "common") diff --git a/main/lv_conf.h b/main/lv_conf.h index cce9f55..f6c72aa 100644 --- a/main/lv_conf.h +++ b/main/lv_conf.h @@ -1,1519 +1 @@ -/** - * @file lv_conf.h - * Configuration file for v9.5.0-dev - */ - - /* - * Copy this file as `lv_conf.h` - * 1. simply next to `lvgl` folder - * 2. or to any other place and - * - define `LV_CONF_INCLUDE_SIMPLE`; - * - add the path as an include path. - */ - - /* clang-format off */ -#if 1 /* Set this to "1" to enable content */ - -#ifndef LV_CONF_H -#define LV_CONF_H - -/* If you need to include anything here, do it inside the `__ASSEMBLY__` guard */ -#if 0 && defined(__ASSEMBLY__) -#include "my_include.h" -#endif - -/*==================== - COLOR SETTINGS - *====================*/ - - /** Color depth: 1 (I1), 8 (L8), 16 (RGB565), 24 (RGB888), 32 (XRGB8888) */ -#define LV_COLOR_DEPTH 1 - -/*========================= - STDLIB WRAPPER SETTINGS - *=========================*/ - - /** Possible values - * - LV_STDLIB_BUILTIN: LVGL's built in implementation - * - LV_STDLIB_CLIB: Standard C functions, like malloc, strlen, etc - * - LV_STDLIB_MICROPYTHON: MicroPython implementation - * - LV_STDLIB_RTTHREAD: RT-Thread implementation - * - LV_STDLIB_CUSTOM: Implement the functions externally - */ -#define LV_USE_STDLIB_MALLOC LV_STDLIB_BUILTIN - - /** Possible values - * - LV_STDLIB_BUILTIN: LVGL's built in implementation - * - LV_STDLIB_CLIB: Standard C functions, like malloc, strlen, etc - * - LV_STDLIB_MICROPYTHON: MicroPython implementation - * - LV_STDLIB_RTTHREAD: RT-Thread implementation - * - LV_STDLIB_CUSTOM: Implement the functions externally - */ -#define LV_USE_STDLIB_STRING LV_STDLIB_BUILTIN - - /** Possible values - * - LV_STDLIB_BUILTIN: LVGL's built in implementation - * - LV_STDLIB_CLIB: Standard C functions, like malloc, strlen, etc - * - LV_STDLIB_MICROPYTHON: MicroPython implementation - * - LV_STDLIB_RTTHREAD: RT-Thread implementation - * - LV_STDLIB_CUSTOM: Implement the functions externally - */ -#define LV_USE_STDLIB_SPRINTF LV_STDLIB_BUILTIN - -#define LV_STDINT_INCLUDE -#define LV_STDDEF_INCLUDE -#define LV_STDBOOL_INCLUDE -#define LV_INTTYPES_INCLUDE -#define LV_LIMITS_INCLUDE -#define LV_STDARG_INCLUDE - -#if LV_USE_STDLIB_MALLOC == LV_STDLIB_BUILTIN - /** Size of memory available for `lv_malloc()` in bytes (>= 2kB) */ -#define LV_MEM_SIZE (64 * 1024U) /**< [bytes] */ - -/** Size of the memory expand for `lv_malloc()` in bytes */ -#define LV_MEM_POOL_EXPAND_SIZE 0 - -/** Set an address for the memory pool instead of allocating it as a normal array. Can be in external SRAM too. */ -#define LV_MEM_ADR 0 /**< 0: unused*/ -/* Instead of an address give a memory allocator that will be called to get a memory pool for LVGL. E.g. my_malloc */ -#if LV_MEM_ADR == 0 -#undef LV_MEM_POOL_INCLUDE -#undef LV_MEM_POOL_ALLOC -#endif -#endif /*LV_USE_STDLIB_MALLOC == LV_STDLIB_BUILTIN*/ - -/*==================== - HAL SETTINGS - *====================*/ - - /** Default display refresh, input device read and animation step period. */ -#define LV_DEF_REFR_PERIOD 33 /**< [ms] */ - -/** Default Dots Per Inch. Used to initialize default sizes such as widgets sized, style paddings. - * (Not so important, you can adjust it to modify default sizes and spaces.) */ - // #define LV_DPI_DEF 130 /**< [px/inch] */ -#define LV_DPI_DEF 124 /**< [px/inch] */ - - - /*================= - * OPERATING SYSTEM - *=================*/ - /** Select operating system to use. Possible options: - * - LV_OS_NONE - * - LV_OS_PTHREAD - * - LV_OS_FREERTOS - * - LV_OS_CMSIS_RTOS2 - * - LV_OS_RTTHREAD - * - LV_OS_WINDOWS - * - LV_OS_MQX - * - LV_OS_SDL2 - * - LV_OS_CUSTOM */ - // #define LV_USE_OS LV_OS_NONE -#define LV_USE_OS LV_OS_FREERTOS - -#if LV_USE_OS == LV_OS_CUSTOM -#define LV_OS_CUSTOM_INCLUDE -#endif -#if LV_USE_OS == LV_OS_FREERTOS - /* - * Unblocking an RTOS task with a direct notification is 45% faster and uses less RAM - * than unblocking a task using an intermediary object such as a binary semaphore. - * RTOS task notifications can only be used when there is only one task that can be the recipient of the event. - */ -#define LV_USE_FREERTOS_TASK_NOTIFY 1 -#endif - - /*======================== - * RENDERING CONFIGURATION - *========================*/ - - /** Align stride of all layers and images to this bytes */ -#define LV_DRAW_BUF_STRIDE_ALIGN 1 - -/** Align start address of draw_buf addresses to this bytes*/ -#define LV_DRAW_BUF_ALIGN 4 - -/** Using matrix for transformations. - * Requirements: - * - `LV_USE_MATRIX = 1`. - * - Rendering engine needs to support 3x3 matrix transformations. */ -#define LV_DRAW_TRANSFORM_USE_MATRIX 0 - - /* If a widget has `style_opa < 255` (not `bg_opa`, `text_opa` etc) or not NORMAL blend mode - * it is buffered into a "simple" layer before rendering. The widget can be buffered in smaller chunks. - * "Transformed layers" (if `transform_angle/zoom` are set) use larger buffers - * and can't be drawn in chunks. */ - - /** The target buffer size for simple layer chunks. */ -#define LV_DRAW_LAYER_SIMPLE_BUF_SIZE (24 * 1024) /**< [bytes]*/ - -/* Limit the max allocated memory for simple and transformed layers. - * It should be at least `LV_DRAW_LAYER_SIMPLE_BUF_SIZE` sized but if transformed layers are also used - * it should be enough to store the largest widget too (width x height x 4 area). - * Set it to 0 to have no limit. */ -#define LV_DRAW_LAYER_MAX_MEMORY 0 /**< No limit by default [bytes]*/ - - /** Stack size of drawing thread. - * NOTE: If FreeType or ThorVG is enabled, it is recommended to set it to 32KB or more. - */ -#define LV_DRAW_THREAD_STACK_SIZE (8 * 1024) /**< [bytes]*/ - - /** Thread priority of the drawing task. - * Higher values mean higher priority. - * Can use values from lv_thread_prio_t enum in lv_os.h: LV_THREAD_PRIO_LOWEST, - * LV_THREAD_PRIO_LOW, LV_THREAD_PRIO_MID, LV_THREAD_PRIO_HIGH, LV_THREAD_PRIO_HIGHEST - * Make sure the priority value aligns with the OS-specific priority levels. - * On systems with limited priority levels (e.g., FreeRTOS), a higher value can improve - * rendering performance but might cause other tasks to starve. */ -#define LV_DRAW_THREAD_PRIO LV_THREAD_PRIO_HIGH - -#define LV_USE_DRAW_SW 1 -#if LV_USE_DRAW_SW == 1 - /* - * Selectively disable color format support in order to reduce code size. - * NOTE: some features use certain color formats internally, e.g. - * - gradients use RGB888 - * - bitmaps with transparency may use ARGB8888 - */ -#define LV_DRAW_SW_SUPPORT_RGB565 1 -#define LV_DRAW_SW_SUPPORT_RGB565_SWAPPED 1 -#define LV_DRAW_SW_SUPPORT_RGB565A8 1 -#define LV_DRAW_SW_SUPPORT_RGB888 1 -#define LV_DRAW_SW_SUPPORT_XRGB8888 1 -#define LV_DRAW_SW_SUPPORT_ARGB8888 1 -#define LV_DRAW_SW_SUPPORT_ARGB8888_PREMULTIPLIED 1 -#define LV_DRAW_SW_SUPPORT_L8 1 -#define LV_DRAW_SW_SUPPORT_AL88 1 -#define LV_DRAW_SW_SUPPORT_A8 1 -#define LV_DRAW_SW_SUPPORT_I1 1 - - /* The threshold of the luminance to consider a pixel as - * active in indexed color format */ -#define LV_DRAW_SW_I1_LUM_THRESHOLD 127 - - /** Set number of draw units. - * - > 1 requires operating system to be enabled in `LV_USE_OS`. - * - > 1 means multiple threads will render the screen in parallel. */ -#define LV_DRAW_SW_DRAW_UNIT_CNT 1 - - /** Use Arm-2D to accelerate software (sw) rendering. */ -#define LV_USE_DRAW_ARM2D_SYNC 0 - -/** Enable native helium assembly to be compiled. */ -#define LV_USE_NATIVE_HELIUM_ASM 0 - -/** - * - 0: Use a simple renderer capable of drawing only simple rectangles with gradient, images, text, and straight lines only. - * - 1: Use a complex renderer capable of drawing rounded corners, shadow, skew lines, and arcs too. */ -#define LV_DRAW_SW_COMPLEX 1 - -#if LV_DRAW_SW_COMPLEX == 1 - /** Allow buffering some shadow calculation. - * LV_DRAW_SW_SHADOW_CACHE_SIZE is the maximum shadow size to buffer, where shadow size is - * `shadow_width + radius`. Caching has LV_DRAW_SW_SHADOW_CACHE_SIZE^2 RAM cost. */ -#define LV_DRAW_SW_SHADOW_CACHE_SIZE 0 - - /** Set number of maximally-cached circle data. - * The circumference of 1/4 circle are saved for anti-aliasing. - * `radius * 4` bytes are used per circle (the most often used radiuses are saved). - * - 0: disables caching */ -#define LV_DRAW_SW_CIRCLE_CACHE_SIZE 4 -#endif - -#define LV_USE_DRAW_SW_ASM LV_DRAW_SW_ASM_NONE - -#if LV_USE_DRAW_SW_ASM == LV_DRAW_SW_ASM_CUSTOM -#define LV_DRAW_SW_ASM_CUSTOM_INCLUDE "" -#endif - - /** Enable drawing complex gradients in software: linear at an angle, radial or conical */ -#define LV_USE_DRAW_SW_COMPLEX_GRADIENTS 0 - -#endif - -/*Use TSi's aka (Think Silicon) NemaGFX */ -#define LV_USE_NEMA_GFX 0 - -#if LV_USE_NEMA_GFX - /** Select which NemaGFX HAL to use. Possible options: - * - LV_NEMA_HAL_CUSTOM - * - LV_NEMA_HAL_STM32 */ -#define LV_USE_NEMA_HAL LV_NEMA_HAL_CUSTOM -#if LV_USE_NEMA_HAL == LV_NEMA_HAL_STM32 -#define LV_NEMA_STM32_HAL_INCLUDE -#endif - - /*Enable Vector Graphics Operations. Available only if NemaVG library is present*/ -#define LV_USE_NEMA_VG 0 -#if LV_USE_NEMA_VG - /*Define application's resolution used for VG related buffer allocation */ -#define LV_NEMA_GFX_MAX_RESX 800 -#define LV_NEMA_GFX_MAX_RESY 600 -#endif -#endif - -/** Use NXP's PXP on iMX RTxxx platforms. */ -#define LV_USE_PXP 0 - -#if LV_USE_PXP - /** Use PXP for drawing.*/ -#define LV_USE_DRAW_PXP 1 - -/** Use PXP to rotate display.*/ -#define LV_USE_ROTATE_PXP 0 - -#if LV_USE_DRAW_PXP && LV_USE_OS - /** Use additional draw thread for PXP processing.*/ -#define LV_USE_PXP_DRAW_THREAD 1 -#endif - -/** Enable PXP asserts. */ -#define LV_USE_PXP_ASSERT 0 -#endif - -/** Use NXP's G2D on MPU platforms. */ -#define LV_USE_G2D 0 - -#if LV_USE_G2D - /** Use G2D for drawing. **/ -#define LV_USE_DRAW_G2D 1 - -/** Use G2D to rotate display. **/ -#define LV_USE_ROTATE_G2D 0 - -/** Maximum number of buffers that can be stored for G2D draw unit. - * Includes the frame buffers and assets. */ -#define LV_G2D_HASH_TABLE_SIZE 50 - -#if LV_USE_DRAW_G2D && LV_USE_OS - /** Use additional draw thread for G2D processing.*/ -#define LV_USE_G2D_DRAW_THREAD 1 -#endif - -/** Enable G2D asserts. */ -#define LV_USE_G2D_ASSERT 0 -#endif - -/** Use Renesas Dave2D on RA platforms. */ -#define LV_USE_DRAW_DAVE2D 0 - -/** Draw using cached SDL textures*/ -#define LV_USE_DRAW_SDL 0 - -/** Use VG-Lite GPU. */ -#define LV_USE_DRAW_VG_LITE 0 -#if LV_USE_DRAW_VG_LITE - /** Enable VG-Lite custom external 'gpu_init()' function */ -#define LV_VG_LITE_USE_GPU_INIT 0 - -/** Enable VG-Lite assert. */ -#define LV_VG_LITE_USE_ASSERT 0 - -/** VG-Lite flush commit trigger threshold. GPU will try to batch these many draw tasks. */ -#define LV_VG_LITE_FLUSH_MAX_COUNT 8 - -/** Enable border to simulate shadow. - * NOTE: which usually improves performance, - * but does not guarantee the same rendering quality as the software. */ -#define LV_VG_LITE_USE_BOX_SHADOW 1 - - /** VG-Lite gradient maximum cache number. - * @note The memory usage of a single gradient image is 4K bytes. */ -#define LV_VG_LITE_GRAD_CACHE_CNT 32 - - /** VG-Lite stroke maximum cache number. */ -#define LV_VG_LITE_STROKE_CACHE_CNT 32 - -/** VG-Lite unaligned bitmap font maximum cache number. */ -#define LV_VG_LITE_BITMAP_FONT_CACHE_CNT 256 - -/** Remove VLC_OP_CLOSE path instruction (Workaround for NXP) **/ -#define LV_VG_LITE_DISABLE_VLC_OP_CLOSE 0 - -/** Disable blit rectangular offset to resolve certain hardware errors. */ -#define LV_VG_LITE_DISABLE_BLIT_RECT_OFFSET 0 - -/** Disable linear gradient extension for some older versions of drivers. */ -#define LV_VG_LITE_DISABLE_LINEAR_GRADIENT_EXT 0 - -/** Maximum path dump print length (in points) */ -#define LV_VG_LITE_PATH_DUMP_MAX_LEN 1000 - -/** Enable usage of the LVGL's built-in vg_lite driver */ -#define LV_USE_VG_LITE_DRIVER 0 -#if LV_USE_VG_LITE_DRIVER - /** Used to pick the correct GPU series folder valid options are gc255, gc355 and gc555*/ -#define LV_VG_LITE_HAL_GPU_SERIES gc255 - -/** Used to pick the correct GPU revision header it depends on the vendor */ -#define LV_VG_LITE_HAL_GPU_REVISION 0x40 - -/** Base memory address of the GPU IP it depends on SoC, - * default value is for NXP based devices */ -#define LV_VG_LITE_HAL_GPU_BASE_ADDRESS 0x40240000 -#endif /*LV_USE_VG_LITE_DRIVER*/ - - /** Use ThorVG (a software vector library) as VG-Lite driver to allow testing VGLite on PC - * Requires: LV_USE_THORVG_INTERNAL or LV_USE_THORVG_EXTERNAL */ -#define LV_USE_VG_LITE_THORVG 0 -#if LV_USE_VG_LITE_THORVG - /** Enable LVGL's blend mode support */ -#define LV_VG_LITE_THORVG_LVGL_BLEND_SUPPORT 0 - -/** Enable YUV color format support */ -#define LV_VG_LITE_THORVG_YUV_SUPPORT 0 - -/** Enable Linear gradient extension support */ -#define LV_VG_LITE_THORVG_LINEAR_GRADIENT_EXT_SUPPORT 0 - -/** Enable alignment on 16 pixels */ -#define LV_VG_LITE_THORVG_16PIXELS_ALIGN 1 - -/** Buffer address alignment */ -#define LV_VG_LITE_THORVG_BUF_ADDR_ALIGN 64 - -/** Enable multi-thread render */ -#define LV_VG_LITE_THORVG_THREAD_RENDER 0 -#endif /*LV_USE_VG_LITE_THORVG*/ -#endif - -/** Accelerate blends, fills, etc. with STM32 DMA2D */ -#define LV_USE_DRAW_DMA2D 0 -#if LV_USE_DRAW_DMA2D -#define LV_DRAW_DMA2D_HAL_INCLUDE "stm32h7xx_hal.h" - -/* if enabled, the user is required to call `lv_draw_dma2d_transfer_complete_interrupt_handler` - * upon receiving the DMA2D global interrupt - */ -#define LV_USE_DRAW_DMA2D_INTERRUPT 0 -#endif - - /** Draw using cached OpenGLES textures. Requires LV_USE_OPENGLES */ -#define LV_USE_DRAW_OPENGLES 0 -#if LV_USE_DRAW_OPENGLES -#define LV_DRAW_OPENGLES_TEXTURE_CACHE_COUNT 64 -#endif - -/** Draw using espressif PPA accelerator */ -#define LV_USE_PPA 0 -#if LV_USE_PPA -#define LV_USE_PPA_IMG 0 -#endif - -/* Use EVE FT81X GPU. */ -#define LV_USE_DRAW_EVE 0 -#if LV_USE_DRAW_EVE - /* EVE_GEN value: 2, 3, or 4 */ -#define LV_DRAW_EVE_EVE_GENERATION 4 - -/* The maximum number of bytes to buffer before a single SPI transmission. - * Set it to 0 to disable write buffering. - */ -#define LV_DRAW_EVE_WRITE_BUFFER_SIZE 2048 -#endif - - /** Use NanoVG Renderer - * - Requires LV_USE_NANOVG, LV_USE_MATRIX. - */ -#define LV_USE_DRAW_NANOVG 0 -#if LV_USE_DRAW_NANOVG - /** Select OpenGL backend for NanoVG: - * - LV_NANOVG_BACKEND_GL2: OpenGL 2.0 - * - LV_NANOVG_BACKEND_GL3: OpenGL 3.0+ - * - LV_NANOVG_BACKEND_GLES2: OpenGL ES 2.0 - * - LV_NANOVG_BACKEND_GLES3: OpenGL ES 3.0+ - */ -#define LV_NANOVG_BACKEND LV_NANOVG_BACKEND_GLES2 - - /** Draw image texture cache count. */ -#define LV_NANOVG_IMAGE_CACHE_CNT 128 - -/** Draw letter texture cache count. */ -#define LV_NANOVG_LETTER_CACHE_CNT 512 -#endif - -/*======================= - * FEATURE CONFIGURATION - *=======================*/ - - /*------------- - * Logging - *-----------*/ - - /** Enable log module */ -#define LV_USE_LOG 0 -#if LV_USE_LOG - /** Set value to one of the following levels of logging detail: - * - LV_LOG_LEVEL_TRACE Log detailed information. - * - LV_LOG_LEVEL_INFO Log important events. - * - LV_LOG_LEVEL_WARN Log if something unwanted happened but didn't cause a problem. - * - LV_LOG_LEVEL_ERROR Log only critical issues, when system may fail. - * - LV_LOG_LEVEL_USER Log only custom log messages added by the user. - * - LV_LOG_LEVEL_NONE Do not log anything. */ -#define LV_LOG_LEVEL LV_LOG_LEVEL_WARN - - /** - 1: Print log with 'printf'; - * - 0: User needs to register a callback with `lv_log_register_print_cb()`. */ -#define LV_LOG_PRINTF 0 - - /** Set callback to print logs. - * E.g `my_print`. The prototype should be `void my_print(lv_log_level_t level, const char * buf)`. - * Can be overwritten by `lv_log_register_print_cb`. */ - //#define LV_LOG_PRINT_CB - - /** - 1: Enable printing timestamp; - * - 0: Disable printing timestamp. */ -#define LV_LOG_USE_TIMESTAMP 1 - - /** - 1: Print file and line number of the log; - * - 0: Do not print file and line number of the log. */ -#define LV_LOG_USE_FILE_LINE 1 - - /* Enable/disable LV_LOG_TRACE in modules that produces a huge number of logs. */ -#define LV_LOG_TRACE_MEM 1 /**< Enable/disable trace logs in memory operations. */ -#define LV_LOG_TRACE_TIMER 1 /**< Enable/disable trace logs in timer operations. */ -#define LV_LOG_TRACE_INDEV 1 /**< Enable/disable trace logs in input device operations. */ -#define LV_LOG_TRACE_DISP_REFR 1 /**< Enable/disable trace logs in display re-draw operations. */ -#define LV_LOG_TRACE_EVENT 1 /**< Enable/disable trace logs in event dispatch logic. */ -#define LV_LOG_TRACE_OBJ_CREATE 1 /**< Enable/disable trace logs in object creation (core `obj` creation plus every widget). */ -#define LV_LOG_TRACE_LAYOUT 1 /**< Enable/disable trace logs in flex- and grid-layout operations. */ -#define LV_LOG_TRACE_ANIM 1 /**< Enable/disable trace logs in animation logic. */ -#define LV_LOG_TRACE_CACHE 1 /**< Enable/disable trace logs in cache operations. */ -#endif /*LV_USE_LOG*/ - -/*------------- - * Asserts - *-----------*/ - - /* Enable assertion failures if an operation fails or invalid data is found. - * If LV_USE_LOG is enabled, an error message will be printed on failure. */ -#define LV_USE_ASSERT_NULL 1 /**< Check if the parameter is NULL. (Very fast, recommended) */ -#define LV_USE_ASSERT_MALLOC 1 /**< Checks is the memory is successfully allocated or no. (Very fast, recommended) */ -#define LV_USE_ASSERT_STYLE 0 /**< Check if the styles are properly initialized. (Very fast, recommended) */ -#define LV_USE_ASSERT_MEM_INTEGRITY 0 /**< Check the integrity of `lv_mem` after critical operations. (Slow) */ -#define LV_USE_ASSERT_OBJ 0 /**< Check the object's type and existence (e.g. not deleted). (Slow) */ - - /** Add a custom handler when assert happens e.g. to restart MCU. */ -#define LV_ASSERT_HANDLER_INCLUDE -#define LV_ASSERT_HANDLER while(1); /**< Halt by default */ - -/*------------- - * Debug - *-----------*/ - - /** 1: Draw random colored rectangles over the redrawn areas. */ -#define LV_USE_REFR_DEBUG 0 - -/** 1: Draw a red overlay for ARGB layers and a green overlay for RGB layers*/ -#define LV_USE_LAYER_DEBUG 0 - -/** 1: Adds the following behaviors for debugging: - * - Draw overlays with different colors for each draw_unit's tasks. - * - Draw index number of draw unit on white background. - * - For layers, draws index number of draw unit on black background. */ -#define LV_USE_PARALLEL_DRAW_DEBUG 0 - - /*------------- - * Others - *-----------*/ - -#define LV_ENABLE_GLOBAL_CUSTOM 0 -#if LV_ENABLE_GLOBAL_CUSTOM - /** Header to include for custom 'lv_global' function" */ -#define LV_GLOBAL_CUSTOM_INCLUDE -#endif - -/** Default cache size in bytes. - * Used by image decoders such as `lv_lodepng` to keep the decoded image in memory. - * If size is not set to 0, the decoder will fail to decode when the cache is full. - * If size is 0, the cache function is not enabled and the decoded memory will be - * released immediately after use. */ -#define LV_CACHE_DEF_SIZE 0 - - /** Default number of image header cache entries. The cache is used to store the headers of images - * The main logic is like `LV_CACHE_DEF_SIZE` but for image headers. */ -#define LV_IMAGE_HEADER_CACHE_DEF_CNT 0 - - /** Number of stops allowed per gradient. Increase this to allow more stops. - * This adds (sizeof(lv_color_t) + 1) bytes per additional stop. */ -#define LV_GRADIENT_MAX_STOPS 2 - - /** Adjust color mix functions rounding. GPUs might calculate color mix (blending) differently. - * - 0: round down, - * - 64: round up from x.75, - * - 128: round up from half, - * - 192: round up from x.25, - * - 254: round up */ -#define LV_COLOR_MIX_ROUND_OFS 0 - - /** Add 2 x 32-bit variables to each `lv_obj_t` to speed up getting style properties */ -#define LV_OBJ_STYLE_CACHE 0 - -/** Add `id` field to `lv_obj_t` */ -#define LV_USE_OBJ_ID 0 - -/** Enable support widget names*/ -#define LV_USE_OBJ_NAME 0 - -/** Automatically assign an ID when obj is created */ -#define LV_OBJ_ID_AUTO_ASSIGN LV_USE_OBJ_ID - -/** Use builtin obj ID handler functions: -* - lv_obj_assign_id: Called when a widget is created. Use a separate counter for each widget class as an ID. -* - lv_obj_id_compare: Compare the ID to decide if it matches with a requested value. -* - lv_obj_stringify_id: Return string-ified identifier, e.g. "button3". -* - lv_obj_free_id: Does nothing, as there is no memory allocation for the ID. -* When disabled these functions needs to be implemented by the user.*/ -#define LV_USE_OBJ_ID_BUILTIN 1 - -/** Use obj property set/get API. */ -#define LV_USE_OBJ_PROPERTY 0 - -/** Enable property name support. */ -#define LV_USE_OBJ_PROPERTY_NAME 1 - -/* Enable the multi-touch gesture recognition feature */ -/* Gesture recognition requires the use of floats */ -// #define LV_USE_GESTURE_RECOGNITION 0 -#define LV_USE_GESTURE_RECOGNITION 1 - -/*===================== - * COMPILER SETTINGS - *====================*/ - - /** For big endian systems set to 1 */ -#define LV_BIG_ENDIAN_SYSTEM 0 - -/** Define a custom attribute for `lv_tick_inc` function */ -#define LV_ATTRIBUTE_TICK_INC - -/** Define a custom attribute for `lv_timer_handler` function */ -#define LV_ATTRIBUTE_TIMER_HANDLER - -/** Define a custom attribute for `lv_display_flush_ready` function */ -#define LV_ATTRIBUTE_FLUSH_READY - -/** Align VG_LITE buffers on this number of bytes. - * @note vglite_src_buf_aligned() uses this value to validate alignment of passed buffer pointers. */ -#define LV_ATTRIBUTE_MEM_ALIGN_SIZE 1 - - /** Will be added where memory needs to be aligned (with -Os data might not be aligned to boundary by default). - * E.g. __attribute__((aligned(4)))*/ -#define LV_ATTRIBUTE_MEM_ALIGN - - /** Attribute to mark large constant arrays, for example for font bitmaps */ -#define LV_ATTRIBUTE_LARGE_CONST - -/** Compiler prefix for a large array declaration in RAM */ -#define LV_ATTRIBUTE_LARGE_RAM_ARRAY - -/** Place performance critical functions into a faster memory (e.g RAM) */ -#define LV_ATTRIBUTE_FAST_MEM - -/** Export integer constant to binding. This macro is used with constants in the form of LV_ that - * should also appear on LVGL binding API such as MicroPython. */ -#define LV_EXPORT_CONST_INT(int_value) struct _silence_gcc_warning /**< The default value just prevents GCC warning */ - - /** Prefix all global extern data with this */ -#define LV_ATTRIBUTE_EXTERN_DATA - -/** Use `float` as `lv_value_precise_t` */ -// #define LV_USE_FLOAT 0 -#define LV_USE_FLOAT 1 - -/** Enable matrix support - * - Requires `LV_USE_FLOAT = 1` */ -#define LV_USE_MATRIX 0 - - /** Include `lvgl_private.h` in `lvgl.h` to access internal data and functions by default */ -#ifndef LV_USE_PRIVATE_API -#define LV_USE_PRIVATE_API 0 -#endif - -/*================== - * FONT USAGE - *===================*/ - - /* Montserrat fonts with ASCII range and some symbols using bpp = 4 - * https://fonts.google.com/specimen/Montserrat */ -#define LV_FONT_MONTSERRAT_8 0 -#define LV_FONT_MONTSERRAT_10 0 -#define LV_FONT_MONTSERRAT_12 0 -#define LV_FONT_MONTSERRAT_14 1 -#define LV_FONT_MONTSERRAT_16 0 -#define LV_FONT_MONTSERRAT_18 0 -#define LV_FONT_MONTSERRAT_20 0 -#define LV_FONT_MONTSERRAT_22 0 -#define LV_FONT_MONTSERRAT_24 0 -#define LV_FONT_MONTSERRAT_26 0 -#define LV_FONT_MONTSERRAT_28 0 -#define LV_FONT_MONTSERRAT_30 0 -#define LV_FONT_MONTSERRAT_32 0 -#define LV_FONT_MONTSERRAT_34 0 -#define LV_FONT_MONTSERRAT_36 0 -#define LV_FONT_MONTSERRAT_38 0 -#define LV_FONT_MONTSERRAT_40 0 -#define LV_FONT_MONTSERRAT_42 0 -#define LV_FONT_MONTSERRAT_44 0 -#define LV_FONT_MONTSERRAT_46 0 -#define LV_FONT_MONTSERRAT_48 0 - - /* Demonstrate special features */ -#define LV_FONT_MONTSERRAT_28_COMPRESSED 0 /**< bpp = 3 */ -#define LV_FONT_DEJAVU_16_PERSIAN_HEBREW 0 /**< Hebrew, Arabic, Persian letters and all their forms */ -#define LV_FONT_SOURCE_HAN_SANS_SC_14_CJK 0 /**< 1338 most common CJK radicals */ -#define LV_FONT_SOURCE_HAN_SANS_SC_16_CJK 0 /**< 1338 most common CJK radicals */ - -/** Pixel perfect monospaced fonts */ -#define LV_FONT_UNSCII_8 0 -#define LV_FONT_UNSCII_16 0 - -/** Optionally declare custom fonts here. - * - * You can use any of these fonts as the default font too and they will be available - * globally. Example: - * - * @code - * #define LV_FONT_CUSTOM_DECLARE LV_FONT_DECLARE(my_font_1) LV_FONT_DECLARE(my_font_2) - * @endcode - */ -#define LV_FONT_CUSTOM_DECLARE - - /** Always set a default font */ -#define LV_FONT_DEFAULT &lv_font_montserrat_14 - -/** Enable handling large font and/or fonts with a lot of characters. - * The limit depends on the font size, font face and bpp. - * A compiler error will be triggered if a font needs it. */ -#define LV_FONT_FMT_TXT_LARGE 0 - - /** Enables/disables support for compressed fonts. */ -#define LV_USE_FONT_COMPRESSED 0 - -/** Enable drawing placeholders when glyph dsc is not found. */ -#define LV_USE_FONT_PLACEHOLDER 1 - -/*================= - * TEXT SETTINGS - *=================*/ - - /** - * Select a character encoding for strings. - * Your IDE or editor should have the same character encoding. - * - LV_TXT_ENC_UTF8 - * - LV_TXT_ENC_ASCII - */ -#define LV_TXT_ENC LV_TXT_ENC_UTF8 - - /** While rendering text strings, break (wrap) text on these chars. */ -#define LV_TXT_BREAK_CHARS " ,.;:-_)]}" - -/** If a word is at least this long, will break wherever "prettiest". - * To disable, set to a value <= 0. */ -#define LV_TXT_LINE_BREAK_LONG_LEN 0 - - /** Minimum number of characters in a long word to put on a line before a break. - * Depends on LV_TXT_LINE_BREAK_LONG_LEN. */ -#define LV_TXT_LINE_BREAK_LONG_PRE_MIN_LEN 3 - - /** Minimum number of characters in a long word to put on a line after a break. - * Depends on LV_TXT_LINE_BREAK_LONG_LEN. */ -#define LV_TXT_LINE_BREAK_LONG_POST_MIN_LEN 3 - - /** Support bidirectional text. Allows mixing Left-to-Right and Right-to-Left text. - * The direction will be processed according to the Unicode Bidirectional Algorithm: - * https://www.w3.org/International/articles/inline-bidi-markup/uba-basics */ -#define LV_USE_BIDI 0 -#if LV_USE_BIDI - /*Set the default direction. Supported values: - *`LV_BASE_DIR_LTR` Left-to-Right - *`LV_BASE_DIR_RTL` Right-to-Left - *`LV_BASE_DIR_AUTO` detect text base direction*/ -#define LV_BIDI_BASE_DIR_DEF LV_BASE_DIR_AUTO -#endif - - /** Enable Arabic/Persian processing - * In these languages characters should be replaced with another form based on their position in the text */ -#define LV_USE_ARABIC_PERSIAN_CHARS 0 - - /*The control character to use for signaling text recoloring*/ -#define LV_TXT_COLOR_CMD "#" - -/*================== - * WIDGETS - *================*/ - /* Documentation for widgets can be found here: https://docs.lvgl.io/master/widgets/index.html . */ - - /** 1: Causes these widgets to be given default values at creation time. - * - lv_buttonmatrix_t: Get default maps: {"Btn1", "Btn2", "Btn3", "\n", "Btn4", "Btn5", ""}, else map not set. - * - lv_checkbox_t : String label set to "Check box", else set to empty string. - * - lv_dropdown_t : Options set to "Option 1", "Option 2", "Option 3", else no values are set. - * - lv_roller_t : Options set to "Option 1", "Option 2", "Option 3", "Option 4", "Option 5", else no values are set. - * - lv_label_t : Text set to "Text", else empty string. - * - lv_arclabel_t : Text set to "Arced Text", else empty string. - * */ -#define LV_WIDGETS_HAS_DEFAULT_VALUE 1 - -#define LV_USE_ANIMIMG 1 - -#define LV_USE_ARC 1 - -#define LV_USE_ARCLABEL 1 - -#define LV_USE_BAR 1 - -#define LV_USE_BUTTON 1 - -#define LV_USE_BUTTONMATRIX 1 - -#define LV_USE_CALENDAR 1 -#if LV_USE_CALENDAR -#define LV_CALENDAR_WEEK_STARTS_MONDAY 0 -#if LV_CALENDAR_WEEK_STARTS_MONDAY -#define LV_CALENDAR_DEFAULT_DAY_NAMES {"Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"} -#else -#define LV_CALENDAR_DEFAULT_DAY_NAMES {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"} -#endif - -#define LV_CALENDAR_DEFAULT_MONTH_NAMES {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"} -#define LV_USE_CALENDAR_HEADER_ARROW 1 -#define LV_USE_CALENDAR_HEADER_DROPDOWN 1 -#define LV_USE_CALENDAR_CHINESE 0 -#endif /*LV_USE_CALENDAR*/ - -#define LV_USE_CANVAS 1 - -#define LV_USE_CHART 1 - -#define LV_USE_CHECKBOX 1 - -#define LV_USE_DROPDOWN 1 /**< Requires: lv_label */ - -#define LV_USE_IMAGE 1 /**< Requires: lv_label */ - -#define LV_USE_IMAGEBUTTON 1 - -#define LV_USE_KEYBOARD 1 - -#define LV_USE_LABEL 1 -#if LV_USE_LABEL -#define LV_LABEL_TEXT_SELECTION 1 /**< Enable selecting text of the label */ -#define LV_LABEL_LONG_TXT_HINT 1 /**< Store some extra info in labels to speed up drawing of very long text */ -#define LV_LABEL_WAIT_CHAR_COUNT 3 /**< The count of wait chart */ -#endif - -#define LV_USE_LED 1 - -#define LV_USE_LINE 1 - -#define LV_USE_LIST 1 - -#define LV_USE_LOTTIE 0 /**< Requires: lv_canvas, thorvg */ - -#define LV_USE_MENU 1 - -#define LV_USE_MSGBOX 1 - -#define LV_USE_ROLLER 1 /**< Requires: lv_label */ - -#define LV_USE_SCALE 1 - -#define LV_USE_SLIDER 1 /**< Requires: lv_bar */ - -#define LV_USE_SPAN 1 -#if LV_USE_SPAN - /** A line of text can contain this maximum number of span descriptors. */ -#define LV_SPAN_SNIPPET_STACK_SIZE 64 -#endif - -#define LV_USE_SPINBOX 1 - -#define LV_USE_SPINNER 1 - -#define LV_USE_SWITCH 1 - -#define LV_USE_TABLE 1 - -#define LV_USE_TABVIEW 1 - -#define LV_USE_TEXTAREA 1 /**< Requires: lv_label */ -#if LV_USE_TEXTAREA != 0 -#define LV_TEXTAREA_DEF_PWD_SHOW_TIME 1500 /**< [ms] */ -#endif - -#define LV_USE_TILEVIEW 1 - -#define LV_USE_WIN 1 - -#define LV_USE_3DTEXTURE 0 - -/*================== - * THEMES - *==================*/ - /* Documentation for themes can be found here: https://docs.lvgl.io/master/common-widget-features/styles/styles.html#themes . */ - - /** A simple, impressive and very complete theme */ -#define LV_USE_THEME_DEFAULT 1 -#if LV_USE_THEME_DEFAULT - /** 0: Light mode; 1: Dark mode */ -#define LV_THEME_DEFAULT_DARK 0 - -/** 1: Enable grow on press */ -#define LV_THEME_DEFAULT_GROW 1 - -/** Default transition time in ms. */ -#define LV_THEME_DEFAULT_TRANSITION_TIME 80 -#endif /*LV_USE_THEME_DEFAULT*/ - -/** A very simple theme that is a good starting point for a custom theme */ -#define LV_USE_THEME_SIMPLE 1 - -/** A theme designed for monochrome displays */ -#define LV_USE_THEME_MONO 1 - -/*================== - * LAYOUTS - *==================*/ - /* Documentation for layouts can be found here: https://docs.lvgl.io/master/common-widget-features/layouts/index.html . */ - - /** A layout similar to Flexbox in CSS. */ -#define LV_USE_FLEX 1 - -/** A layout similar to Grid in CSS. */ -#define LV_USE_GRID 1 - -/*==================== - * 3RD PARTS LIBRARIES - *====================*/ - /* Documentation for libraries can be found here: https://docs.lvgl.io/master/libs/index.html . */ - - /* File system interfaces for common APIs */ - - /** Setting a default driver letter allows skipping the driver prefix in filepaths. - * Documentation about how to use the below driver-identifier letters can be found at - * https://docs.lvgl.io/master/main-modules/fs.html#lv-fs-identifier-letters . */ -#define LV_FS_DEFAULT_DRIVER_LETTER '\0' - - /** API for fopen, fread, etc. */ -#define LV_USE_FS_STDIO 0 -#if LV_USE_FS_STDIO -#define LV_FS_STDIO_LETTER '\0' /**< Set an upper-case driver-identifier letter for this driver (e.g. 'A'). */ -#define LV_FS_STDIO_PATH "" /**< Set the working directory. File/directory paths will be appended to it. */ -#define LV_FS_STDIO_CACHE_SIZE 0 /**< >0 to cache this number of bytes in lv_fs_read() */ -#endif - -/** API for open, read, etc. */ -#define LV_USE_FS_POSIX 0 -#if LV_USE_FS_POSIX -#define LV_FS_POSIX_LETTER '\0' /**< Set an upper-case driver-identifier letter for this driver (e.g. 'A'). */ -#define LV_FS_POSIX_PATH "" /**< Set the working directory. File/directory paths will be appended to it. */ -#define LV_FS_POSIX_CACHE_SIZE 0 /**< >0 to cache this number of bytes in lv_fs_read() */ -#endif - -/** API for CreateFile, ReadFile, etc. */ -#define LV_USE_FS_WIN32 0 -#if LV_USE_FS_WIN32 -#define LV_FS_WIN32_LETTER '\0' /**< Set an upper-case driver-identifier letter for this driver (e.g. 'A'). */ -#define LV_FS_WIN32_PATH "" /**< Set the working directory. File/directory paths will be appended to it. */ -#define LV_FS_WIN32_CACHE_SIZE 0 /**< >0 to cache this number of bytes in lv_fs_read() */ -#endif - -/** API for FATFS (needs to be added separately). Uses f_open, f_read, etc. */ -#define LV_USE_FS_FATFS 0 -#if LV_USE_FS_FATFS -#define LV_FS_FATFS_LETTER '\0' /**< Set an upper-case driver-identifier letter for this driver (e.g. 'A'). */ -#define LV_FS_FATFS_PATH "" /**< Set the working directory. File/directory paths will be appended to it. */ -#define LV_FS_FATFS_CACHE_SIZE 0 /**< >0 to cache this number of bytes in lv_fs_read() */ -#endif - -/** API for memory-mapped file access. */ -#define LV_USE_FS_MEMFS 0 -#if LV_USE_FS_MEMFS -#define LV_FS_MEMFS_LETTER '\0' /**< Set an upper-case driver-identifier letter for this driver (e.g. 'A'). */ -#endif - -/** API for LittleFs. */ -#define LV_USE_FS_LITTLEFS 0 -#if LV_USE_FS_LITTLEFS -#define LV_FS_LITTLEFS_LETTER '\0' /**< Set an upper-case driver-identifier letter for this driver (e.g. 'A'). */ -#define LV_FS_LITTLEFS_PATH "" /**< Set the working directory. File/directory paths will be appended to it. */ -#endif - -/** API for Arduino LittleFs. */ -#define LV_USE_FS_ARDUINO_ESP_LITTLEFS 0 -#if LV_USE_FS_ARDUINO_ESP_LITTLEFS -#define LV_FS_ARDUINO_ESP_LITTLEFS_LETTER '\0' /**< Set an upper-case driver-identifier letter for this driver (e.g. 'A'). */ -#define LV_FS_ARDUINO_ESP_LITTLEFS_PATH "" /**< Set the working directory. File/directory paths will be appended to it. */ -#endif - -/** API for Arduino Sd. */ -#define LV_USE_FS_ARDUINO_SD 0 -#if LV_USE_FS_ARDUINO_SD -#define LV_FS_ARDUINO_SD_LETTER '\0' /**< Set an upper-case driver-identifier letter for this driver (e.g. 'A'). */ -#define LV_FS_ARDUINO_SD_PATH "" /**< Set the working directory. File/directory paths will be appended to it. */ -#endif - -/** API for UEFI */ -#define LV_USE_FS_UEFI 0 -#if LV_USE_FS_UEFI -#define LV_FS_UEFI_LETTER '\0' /**< Set an upper-case driver-identifier letter for this driver (e.g. 'A'). */ -#endif - -#define LV_USE_FS_FROGFS 0 -#if LV_USE_FS_FROGFS -#define LV_FS_FROGFS_LETTER '\0' -#endif - -/** LODEPNG decoder library */ -#define LV_USE_LODEPNG 0 - -/** PNG decoder(libpng) library */ -#define LV_USE_LIBPNG 0 - -/** BMP decoder library */ -#define LV_USE_BMP 0 - -/** JPG + split JPG decoder library. - * Split JPG is a custom format optimized for embedded systems. */ -#define LV_USE_TJPGD 0 - - /** libjpeg-turbo decoder library. - * - Supports complete JPEG specifications and high-performance JPEG decoding. */ -#define LV_USE_LIBJPEG_TURBO 0 - - /** WebP decoder library */ -#define LV_USE_LIBWEBP 0 - -/** GIF decoder library */ -#define LV_USE_GIF 0 -#if LV_USE_GIF - /** GIF decoder accelerate */ -#define LV_GIF_CACHE_DECODE_DATA 0 -#endif - -/** GStreamer library */ -#define LV_USE_GSTREAMER 0 - -/** Decode bin images to RAM */ -#define LV_BIN_DECODER_RAM_LOAD 0 - -/** RLE decompress library */ -#define LV_USE_RLE 0 - -/** QR code library */ -#define LV_USE_QRCODE 0 - -/** Barcode code library */ -#define LV_USE_BARCODE 0 - -/** FreeType library */ -#define LV_USE_FREETYPE 0 -#if LV_USE_FREETYPE - /** Let FreeType use LVGL memory and file porting */ -#define LV_FREETYPE_USE_LVGL_PORT 0 - -/** Cache count of glyphs in FreeType, i.e. number of glyphs that can be cached. - * The higher the value, the more memory will be used. */ -#define LV_FREETYPE_CACHE_FT_GLYPH_CNT 256 -#endif - - /** Built-in TTF decoder */ -#define LV_USE_TINY_TTF 0 -#if LV_USE_TINY_TTF - /* Enable loading TTF data from files */ -#define LV_TINY_TTF_FILE_SUPPORT 0 -#define LV_TINY_TTF_CACHE_GLYPH_CNT 128 -#define LV_TINY_TTF_CACHE_KERNING_CNT 256 -#endif - -/** Rlottie library */ -#define LV_USE_RLOTTIE 0 - -/** Requires `LV_USE_3DTEXTURE = 1` */ -#define LV_USE_GLTF 0 - -/** Enable Vector Graphic APIs - * Requires `LV_USE_MATRIX = 1` - * and a rendering engine supporting vector graphics, e.g. - * (LV_USE_DRAW_SW and LV_USE_THORVG) or LV_USE_DRAW_VG_LITE or LV_USE_NEMA_VG. */ -#define LV_USE_VECTOR_GRAPHIC 0 - - /** Enable ThorVG (vector graphics library) from the src/libs folder. - * Requires LV_USE_VECTOR_GRAPHIC */ -#define LV_USE_THORVG_INTERNAL 0 - - /** Enable ThorVG by assuming that its installed and linked to the project - * Requires LV_USE_VECTOR_GRAPHIC */ -#define LV_USE_THORVG_EXTERNAL 0 - - /** Enable NanoVG (vector graphics library) */ -#define LV_USE_NANOVG 0 - -/** Use lvgl built-in LZ4 lib */ -#define LV_USE_LZ4_INTERNAL 0 - -/** Use external LZ4 library */ -#define LV_USE_LZ4_EXTERNAL 0 - -/*SVG library - * - Requires `LV_USE_VECTOR_GRAPHIC = 1` */ -#define LV_USE_SVG 0 -#define LV_USE_SVG_ANIMATION 0 -#define LV_USE_SVG_DEBUG 0 - - /** FFmpeg library for image decoding and playing videos. - * Supports all major image formats so do not enable other image decoder with it. */ -#define LV_USE_FFMPEG 0 -#if LV_USE_FFMPEG - /** Dump input information to stderr */ -#define LV_FFMPEG_DUMP_FORMAT 0 -/** Use lvgl file path in FFmpeg Player widget - * You won't be able to open URLs after enabling this feature. - * Note that FFmpeg image decoder will always use lvgl file system. */ -#define LV_FFMPEG_PLAYER_USE_LV_FS 0 -#endif - - /*================== - * OTHERS - *==================*/ - /* Documentation for several of the below items can be found here: https://docs.lvgl.io/master/auxiliary-modules/index.html . */ - - /** 1: Enable API to take snapshot for object */ -#define LV_USE_SNAPSHOT 0 - -/** 1: Enable system monitor component */ -#define LV_USE_SYSMON 0 -#if LV_USE_SYSMON - /** Get the idle percentage. E.g. uint32_t my_get_idle(void); */ -#define LV_SYSMON_GET_IDLE lv_os_get_idle_percent -/** 1: Enable usage of lv_os_get_proc_idle_percent.*/ -#define LV_SYSMON_PROC_IDLE_AVAILABLE 0 -#if LV_SYSMON_PROC_IDLE_AVAILABLE - /** Get the applications idle percentage. - * - Requires `LV_USE_OS == LV_OS_PTHREAD` */ -#define LV_SYSMON_GET_PROC_IDLE lv_os_get_proc_idle_percent -#endif - - /** 1: Show CPU usage and FPS count. - * - Requires `LV_USE_SYSMON = 1` */ -#define LV_USE_PERF_MONITOR 0 -#if LV_USE_PERF_MONITOR -#define LV_USE_PERF_MONITOR_POS LV_ALIGN_BOTTOM_RIGHT - - /** 0: Displays performance data on the screen; 1: Prints performance data using log. */ -#define LV_USE_PERF_MONITOR_LOG_MODE 0 -#endif - -/** 1: Show used memory and memory fragmentation. - * - Requires `LV_USE_STDLIB_MALLOC = LV_STDLIB_BUILTIN` - * - Requires `LV_USE_SYSMON = 1`*/ -#define LV_USE_MEM_MONITOR 0 -#if LV_USE_MEM_MONITOR -#define LV_USE_MEM_MONITOR_POS LV_ALIGN_BOTTOM_LEFT -#endif -#endif /*LV_USE_SYSMON*/ - - /** 1: Enable runtime performance profiler */ -#define LV_USE_PROFILER 0 -#if LV_USE_PROFILER - /** 1: Enable the built-in profiler */ -#define LV_USE_PROFILER_BUILTIN 1 -#if LV_USE_PROFILER_BUILTIN - /** Default profiler trace buffer size */ -#define LV_PROFILER_BUILTIN_BUF_SIZE (16 * 1024) /**< [bytes] */ -#define LV_PROFILER_BUILTIN_DEFAULT_ENABLE 1 -#define LV_USE_PROFILER_BUILTIN_POSIX 0 /**< Enable POSIX profiler port */ -#endif - -/** Header to include for profiler */ -#define LV_PROFILER_INCLUDE "lvgl/src/misc/lv_profiler_builtin.h" - -/** Profiler start point function */ -#define LV_PROFILER_BEGIN LV_PROFILER_BUILTIN_BEGIN - -/** Profiler end point function */ -#define LV_PROFILER_END LV_PROFILER_BUILTIN_END - -/** Profiler start point function with custom tag */ -#define LV_PROFILER_BEGIN_TAG LV_PROFILER_BUILTIN_BEGIN_TAG - -/** Profiler end point function with custom tag */ -#define LV_PROFILER_END_TAG LV_PROFILER_BUILTIN_END_TAG - -/*Enable layout profiler*/ -#define LV_PROFILER_LAYOUT 1 - -/*Enable disp refr profiler*/ -#define LV_PROFILER_REFR 1 - -/*Enable draw profiler*/ -#define LV_PROFILER_DRAW 1 - -/*Enable indev profiler*/ -#define LV_PROFILER_INDEV 1 - -/*Enable decoder profiler*/ -#define LV_PROFILER_DECODER 1 - -/*Enable font profiler*/ -#define LV_PROFILER_FONT 1 - -/*Enable fs profiler*/ -#define LV_PROFILER_FS 1 - -/*Enable style profiler*/ -#define LV_PROFILER_STYLE 0 - -/*Enable timer profiler*/ -#define LV_PROFILER_TIMER 1 - -/*Enable cache profiler*/ -#define LV_PROFILER_CACHE 1 - -/*Enable event profiler*/ -#define LV_PROFILER_EVENT 1 -#endif - -/** 1: Enable Monkey test */ -#define LV_USE_MONKEY 0 - -/** 1: Enable grid navigation */ -#define LV_USE_GRIDNAV 0 - -/** 1: Enable `lv_obj` fragment logic */ -#define LV_USE_FRAGMENT 0 - -/** 1: Support using images as font in label or span widgets */ -#define LV_USE_IMGFONT 0 - -/** 1: Enable an observer pattern implementation */ -#define LV_USE_OBSERVER 1 - -/** 1: Enable Pinyin input method - * - Requires: lv_keyboard */ - // #define LV_USE_IME_PINYIN 0 -#define LV_USE_IME_PINYIN 1 -#if LV_USE_IME_PINYIN - /** 1: Use default thesaurus. - * @note If you do not use the default thesaurus, be sure to use `lv_ime_pinyin` after setting the thesaurus. */ -#define LV_IME_PINYIN_USE_DEFAULT_DICT 1 - /** Set maximum number of candidate panels that can be displayed. - * @note This needs to be adjusted according to size of screen. */ -#define LV_IME_PINYIN_CAND_TEXT_NUM 6 - - /** Use 9-key input (k9). */ -#define LV_IME_PINYIN_USE_K9_MODE 1 -#if LV_IME_PINYIN_USE_K9_MODE == 1 -#define LV_IME_PINYIN_K9_CAND_TEXT_NUM 3 -#endif /*LV_IME_PINYIN_USE_K9_MODE*/ -#endif - -/** 1: Enable file explorer. - * - Requires: lv_table */ -#define LV_USE_FILE_EXPLORER 0 -#if LV_USE_FILE_EXPLORER - /** Maximum length of path */ -#define LV_FILE_EXPLORER_PATH_MAX_LEN (128) -/** Quick access bar, 1:use, 0:do not use. - * - Requires: lv_list */ -#define LV_FILE_EXPLORER_QUICK_ACCESS 1 -#endif - - /** 1: Enable Font manager */ -#define LV_USE_FONT_MANAGER 0 -#if LV_USE_FONT_MANAGER - -/**Font manager name max length*/ -#define LV_FONT_MANAGER_NAME_MAX_LEN 32 - -#endif - -/** Enable emulated input devices, time emulation, and screenshot compares. */ -#define LV_USE_TEST 0 -#if LV_USE_TEST - -/** Enable `lv_test_screenshot_compare`. - * Requires lodepng and a few MB of extra RAM. */ -#define LV_USE_TEST_SCREENSHOT_COMPARE 0 - -#if LV_USE_TEST_SCREENSHOT_COMPARE - /** 1: Automatically create missing reference images*/ -#define LV_TEST_SCREENSHOT_CREATE_REFERENCE_IMAGE 1 -#endif /*LV_USE_TEST_SCREENSHOT_COMPARE*/ - -#endif /*LV_USE_TEST*/ - -/** Enable loading XML UIs runtime */ -#define LV_USE_XML 0 - -/** 1: Enable text translation support */ -#define LV_USE_TRANSLATION 0 - -/*1: Enable color filter style*/ -#define LV_USE_COLOR_FILTER 0 - -/*================== - * DEVICES - *==================*/ - - /** Use SDL to open window on PC and handle mouse and keyboard. */ -#define LV_USE_SDL 0 -#if LV_USE_SDL -#define LV_SDL_INCLUDE_PATH -#define LV_SDL_RENDER_MODE LV_DISPLAY_RENDER_MODE_DIRECT /**< LV_DISPLAY_RENDER_MODE_DIRECT is recommended for best performance */ -#define LV_SDL_BUF_COUNT 1 /**< 1 or 2 */ -#define LV_SDL_ACCELERATED 1 /**< 1: Use hardware acceleration*/ -#define LV_SDL_FULLSCREEN 0 /**< 1: Make the window full screen by default */ -#define LV_SDL_DIRECT_EXIT 1 /**< 1: Exit the application when all SDL windows are closed */ -#define LV_SDL_MOUSEWHEEL_MODE LV_SDL_MOUSEWHEEL_MODE_ENCODER /*LV_SDL_MOUSEWHEEL_MODE_ENCODER/CROWN*/ -#endif - -/** Use X11 to open window on Linux desktop and handle mouse and keyboard */ -#define LV_USE_X11 0 -#if LV_USE_X11 -#define LV_X11_DIRECT_EXIT 1 /**< Exit application when all X11 windows have been closed */ -#define LV_X11_DOUBLE_BUFFER 1 /**< Use double buffers for rendering */ -/* Select only 1 of the following render modes (LV_X11_RENDER_MODE_PARTIAL preferred!). */ -#define LV_X11_RENDER_MODE_PARTIAL 1 /**< Partial render mode (preferred) */ -#define LV_X11_RENDER_MODE_DIRECT 0 /**< Direct render mode */ -#define LV_X11_RENDER_MODE_FULL 0 /**< Full render mode */ -#endif - -/** Use Wayland to open a window and handle input on Linux or BSD desktops */ -#define LV_USE_WAYLAND 0 -#if LV_USE_WAYLAND -#define LV_WAYLAND_DIRECT_EXIT 1 /**< 1: Exit the application when all Wayland windows are closed */ -#endif - -/** Driver for /dev/fb */ -#define LV_USE_LINUX_FBDEV 0 -#if LV_USE_LINUX_FBDEV -#define LV_LINUX_FBDEV_BSD 0 -#define LV_LINUX_FBDEV_RENDER_MODE LV_DISPLAY_RENDER_MODE_PARTIAL -#define LV_LINUX_FBDEV_BUFFER_COUNT 0 -#define LV_LINUX_FBDEV_BUFFER_SIZE 60 -#define LV_LINUX_FBDEV_MMAP 1 -#endif - -/** Use Nuttx to open window and handle touchscreen */ -#define LV_USE_NUTTX 0 - -#if LV_USE_NUTTX -#define LV_USE_NUTTX_INDEPENDENT_IMAGE_HEAP 0 - -/** Use independent image heap for default draw buffer */ -#define LV_NUTTX_DEFAULT_DRAW_BUF_USE_INDEPENDENT_IMAGE_HEAP 0 - -#define LV_USE_NUTTX_LIBUV 0 - -/** Use Nuttx custom init API to open window and handle touchscreen */ -#define LV_USE_NUTTX_CUSTOM_INIT 0 - -/** Driver for /dev/lcd */ -#define LV_USE_NUTTX_LCD 0 -#if LV_USE_NUTTX_LCD -#define LV_NUTTX_LCD_BUFFER_COUNT 0 -#define LV_NUTTX_LCD_BUFFER_SIZE 60 -#endif - -/** Driver for /dev/input */ -#define LV_USE_NUTTX_TOUCHSCREEN 0 - -/** Touchscreen cursor size in pixels(<=0: disable cursor) */ -#define LV_NUTTX_TOUCHSCREEN_CURSOR_SIZE 0 - -/** Driver for /dev/mouse */ -#define LV_USE_NUTTX_MOUSE 0 - -/** Mouse movement step (pixels) */ -#define LV_USE_NUTTX_MOUSE_MOVE_STEP 1 - -/*NuttX trace file and its path*/ -#define LV_USE_NUTTX_TRACE_FILE 0 -#if LV_USE_NUTTX_TRACE_FILE -#define LV_NUTTX_TRACE_FILE_PATH "/data/lvgl-trace.log" -#endif - -#endif - -/** Driver for /dev/dri/card */ -#define LV_USE_LINUX_DRM 0 - -#if LV_USE_LINUX_DRM - - /* Use the MESA GBM library to allocate DMA buffers that can be - * shared across sub-systems and libraries using the Linux DMA-BUF API. - * The GBM library aims to provide a platform independent memory management system - * it supports the major GPU vendors - This option requires linking with libgbm */ -#define LV_USE_LINUX_DRM_GBM_BUFFERS 0 - -#define LV_LINUX_DRM_USE_EGL 0 -#endif - - /** Interface for TFT_eSPI */ -#define LV_USE_TFT_ESPI 0 - -/** Interface for Lovyan_GFX */ -#define LV_USE_LOVYAN_GFX 0 - -#if LV_USE_LOVYAN_GFX -#define LV_LGFX_USER_INCLUDE "lv_lgfx_user.hpp" - -#endif /*LV_USE_LOVYAN_GFX*/ - -/** Driver for evdev input devices */ -#define LV_USE_EVDEV 0 - -/** Driver for libinput input devices */ -#define LV_USE_LIBINPUT 0 - -#if LV_USE_LIBINPUT -#define LV_LIBINPUT_BSD 0 - -/** Full keyboard support */ -#define LV_LIBINPUT_XKB 0 -#if LV_LIBINPUT_XKB - /** "setxkbmap -query" can help find the right values for your keyboard */ -#define LV_LIBINPUT_XKB_KEY_MAP { .rules = NULL, .model = "pc101", .layout = "us", .variant = NULL, .options = NULL } -#endif -#endif - -/* Drivers for LCD devices connected via SPI/parallel port */ -#define LV_USE_ST7735 0 -#define LV_USE_ST7789 0 -#define LV_USE_ST7796 0 -#define LV_USE_ILI9341 0 -#define LV_USE_FT81X 0 -#define LV_USE_NV3007 0 - -#if (LV_USE_ST7735 | LV_USE_ST7789 | LV_USE_ST7796 | LV_USE_ILI9341 | LV_USE_NV3007) -#define LV_USE_GENERIC_MIPI 1 -#else -#define LV_USE_GENERIC_MIPI 0 -#endif - -/** Driver for Renesas GLCD */ -#define LV_USE_RENESAS_GLCDC 0 - -/** Driver for ST LTDC */ -#define LV_USE_ST_LTDC 0 -#if LV_USE_ST_LTDC - /* Only used for partial. */ -#define LV_ST_LTDC_USE_DMA2D_FLUSH 0 -#endif - -/** Driver for NXP ELCDIF */ -#define LV_USE_NXP_ELCDIF 0 - -/** LVGL Windows backend */ -#define LV_USE_WINDOWS 0 - -/** LVGL UEFI backend */ -#define LV_USE_UEFI 0 -#if LV_USE_UEFI -#define LV_USE_UEFI_INCLUDE "myefi.h" /**< Header that hides the actual framework (EDK2, gnu-efi, ...) */ -#define LV_UEFI_USE_MEMORY_SERVICES 0 /**< Use the memory functions from the boot services table */ -#endif - -/** Use a generic OpenGL driver that can be used to embed in other applications or used with GLFW/EGL */ -#define LV_USE_OPENGLES 0 -#if LV_USE_OPENGLES -#define LV_USE_OPENGLES_DEBUG 1 /**< Enable or disable debug for opengles */ -#endif - -/** Use GLFW to open window on PC and handle mouse and keyboard. Requires*/ -#define LV_USE_GLFW 0 - - -/** QNX Screen display and input drivers */ -#define LV_USE_QNX 0 -#if LV_USE_QNX -#define LV_QNX_BUF_COUNT 1 /**< 1 or 2 */ -#endif - -/** Enable or disable for external data and destructor function */ -#define LV_USE_EXT_DATA 0 - -/*===================== -* BUILD OPTIONS -*======================*/ - -/** Enable examples to be built with the library. */ -// #define LV_BUILD_EXAMPLES 1 -#define LV_BUILD_EXAMPLES 0 - -/** Build the demos */ -// #define LV_BUILD_DEMOS 1 -#define LV_BUILD_DEMOS 0 - -/*=================== - * DEMO USAGE - ====================*/ - -#if LV_BUILD_DEMOS - /** Show some widgets. This might be required to increase `LV_MEM_SIZE`. */ -#define LV_USE_DEMO_WIDGETS 0 - -/** Demonstrate usage of encoder and keyboard. */ -#define LV_USE_DEMO_KEYPAD_AND_ENCODER 0 - -/** Benchmark your system */ -#define LV_USE_DEMO_BENCHMARK 0 - -#if LV_USE_DEMO_BENCHMARK - /** Use fonts where bitmaps are aligned 16 byte and has Nx16 byte stride */ -#define LV_DEMO_BENCHMARK_ALIGNED_FONTS 0 -#endif - -/** Render test for each primitive. - * - Requires at least 480x272 display. */ -#define LV_USE_DEMO_RENDER 0 - - /** Stress test for LVGL */ -#define LV_USE_DEMO_STRESS 0 - -/** Music player demo */ -#define LV_USE_DEMO_MUSIC 0 -#if LV_USE_DEMO_MUSIC -#define LV_DEMO_MUSIC_SQUARE 0 -#define LV_DEMO_MUSIC_LANDSCAPE 0 -#define LV_DEMO_MUSIC_ROUND 0 -#define LV_DEMO_MUSIC_LARGE 0 -#define LV_DEMO_MUSIC_AUTO_PLAY 0 -#endif - -/** Vector graphic demo */ -#define LV_USE_DEMO_VECTOR_GRAPHIC 0 - -/** GLTF demo */ -#define LV_USE_DEMO_GLTF 0 - -/*--------------------------- - * Demos from lvgl/lv_demos - ---------------------------*/ - - /** Flex layout demo */ -#define LV_USE_DEMO_FLEX_LAYOUT 0 - -/** Smart-phone like multi-language demo */ -#define LV_USE_DEMO_MULTILANG 0 - -/*E-bike demo with Lottie animations (if LV_USE_LOTTIE is enabled)*/ -#define LV_USE_DEMO_EBIKE 0 -#if LV_USE_DEMO_EBIKE -#define LV_DEMO_EBIKE_PORTRAIT 0 /*0: for 480x270..480x320, 1: for 480x800..720x1280*/ -#endif - -/** High-resolution demo */ -#define LV_USE_DEMO_HIGH_RES 0 - -/* Smart watch demo */ -#define LV_USE_DEMO_SMARTWATCH 0 -#endif /* LV_BUILD_DEMOS */ - -/*--END OF LV_CONF_H--*/ - -#endif /*LV_CONF_H*/ - -#endif /*End of "Content enable"*/ \ No newline at end of file +// placeholder file to avoid build errors, the actual lv_conf.h will be injected by esp-idf during build \ No newline at end of file diff --git a/main/main.cpp b/main/main.cpp index c0b76bd..0630673 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -15,7 +15,14 @@ #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 +#include "esp_lvgl_port.h" +#include "lvgl.h" #include "network.h" // nvs storage namespaces, 15 characters max @@ -45,6 +52,17 @@ void app_main(void) { return esp_restart(); } ESP_LOGI(TAG, "Queues initialized.\n"); + + // Initialize LVGL + const lvgl_port_cfg_t lvgl_cfg = ESP_LVGL_PORT_INIT_CONFIG(); + esp_err_t err = lvgl_port_init(&lvgl_cfg); + if (err != ESP_OK) { + ESP_LOGE(TAG, "LVGL port initialization failed: %s", esp_err_to_name(err)); + vTaskDelay(5000 / portTICK_PERIOD_MS); + return esp_restart(); + } + ESP_LOGI(TAG, "LVGL port initialized successfully.\n"); + SemaphoreHandle_t lvgl_mutex = xSemaphoreCreateMutex(); if (lvgl_mutex == NULL) { ESP_LOGE("Main", "Failed to create LVGL mutex"); @@ -60,13 +78,14 @@ void app_main(void) { std::unique_ptr(new NVSStorageHandler(WIFI_CREDENTIALS_STORAGE_NAMESPACE)) ); NetworkHandler* network_handler = new NetworkHandler(std::move(wifi_handler)); - // DisplayHandler* display_handler = new EInkDisplayHandler(touch_event_queue, lvgl_mutex); + EInkDisplayHandler* display_handler = new EInkDisplayHandler(system_event_group); // kv_storage_handler->init(system_event_group); network_handler->init(system_event_group); - - // display_handler->init(system_event_group); + // Initialize display and touch + display_handler->init(); + display_handler->start_touch_task(); // // LVGL tick timer auto lvgl_tick_timer_callback = [](TimerHandle_t xTimer) { @@ -94,16 +113,30 @@ void app_main(void) { ESP_LOGI(TAG, "Waiting for system to be ready...\n"); xEventGroupWaitBits( system_event_group, - // DISPLAY_READY_BIT | TOUCH_CALIBRATED_BIT | - STORAGE_READY_BIT | NETWORK_READY_BIT, + DISPLAY_READY_BIT | STORAGE_READY_BIT | NETWORK_READY_BIT, // do not clear on exit, require explicit reset pdFALSE, pdTRUE, portMAX_DELAY ); ESP_LOGI(TAG, "System is ready. Starting main application...\n"); - // starting event loops - // display_handler->start_event_loop(); + + // 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(); + 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(); + } + 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"); EventBits_t bits = xEventGroupWaitBits( @@ -116,10 +149,18 @@ void app_main(void) { ); ESP_LOGI(TAG, "Shutdown signal received. Cleaning up...\n"); - // cleanup - // shutdown_display_handlerFunc shutdown_display_handler = display_handler->get_shutdown_display_handler(); - // restart_display_handlerFunc restart_display_handler = display_handler->get_restart_display_handler(); - // delete display_handler; + // 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); + } + vTaskDelay(1000 / portTICK_PERIOD_MS); // Display shutdown message briefly + + // Cleanup + ui_handler.deinit(); + delete demo_descriptor; + delete shutdown_descriptor; + delete display_handler; vSemaphoreDelete(lvgl_mutex); vEventGroupDelete(system_event_group); vQueueDelete(touch_event_queue); From dfd8959f58e7da6c7a12bb50b8d30ea36fffa961 Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Sat, 24 Jan 2026 13:13:18 +0800 Subject: [PATCH 13/16] feat: implement UDPClient class for non-blocking UDP communication --- main/network/udp_client.cpp | 172 ++++++++++++++++++++++++++++++++++++ main/network/udp_client.h | 83 +++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 main/network/udp_client.cpp create mode 100644 main/network/udp_client.h diff --git a/main/network/udp_client.cpp b/main/network/udp_client.cpp new file mode 100644 index 0000000..7929913 --- /dev/null +++ b/main/network/udp_client.cpp @@ -0,0 +1,172 @@ +#include "udp_client.h" +#include +#include +#include +#include +#include "esp_log.h" + +static const char* TAG = "UDPClient"; + +UDPClient::UDPClient() + : sock_fd_(-1) + , remote_port_(0) + , configured_(false) + , initialized_(false) { + memset(&remote_addr_, 0, sizeof(remote_addr_)); +} + +UDPClient::~UDPClient() { + close(); +} + +esp_err_t UDPClient::init() { + if (initialized_) { + ESP_LOGW(TAG, "Already initialized"); + return ESP_OK; + } + + sock_fd_ = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock_fd_ < 0) { + ESP_LOGE(TAG, "Failed to create socket: errno %d", errno); + return ESP_FAIL; + } + + // Set socket to non-blocking mode + esp_err_t err = set_nonblocking(); + if (err != ESP_OK) { + ::close(sock_fd_); + sock_fd_ = -1; + return err; + } + + initialized_ = true; + ESP_LOGI(TAG, "UDP client initialized (fd=%d)", sock_fd_); + return ESP_OK; +} + +esp_err_t UDPClient::set_nonblocking() { + int flags = fcntl(sock_fd_, F_GETFL, 0); + if (flags < 0) { + ESP_LOGE(TAG, "Failed to get socket flags: errno %d", errno); + return ESP_FAIL; + } + + if (fcntl(sock_fd_, F_SETFL, flags | O_NONBLOCK) < 0) { + ESP_LOGE(TAG, "Failed to set non-blocking mode: errno %d", errno); + return ESP_FAIL; + } + + return ESP_OK; +} + +esp_err_t UDPClient::configure(const std::string& ip, uint16_t port) { + if (ip.empty() || port == 0) { + ESP_LOGE(TAG, "Invalid IP or port"); + return ESP_ERR_INVALID_ARG; + } + + struct in_addr addr; + if (inet_pton(AF_INET, ip.c_str(), &addr) != 1) { + ESP_LOGE(TAG, "Invalid IP address format: %s", ip.c_str()); + return ESP_ERR_INVALID_ARG; + } + + remote_addr_.sin_family = AF_INET; + remote_addr_.sin_port = htons(port); + remote_addr_.sin_addr = addr; + + remote_ip_ = ip; + remote_port_ = port; + configured_ = true; + + ESP_LOGI(TAG, "Configured endpoint: %s:%u", ip.c_str(), port); + return ESP_OK; +} + +esp_err_t UDPClient::send_command(const std::string& command) { + if (!initialized_) { + ESP_LOGE(TAG, "Not initialized"); + return ESP_FAIL; + } + + if (!configured_) { + ESP_LOGE(TAG, "Endpoint not configured"); + return ESP_FAIL; + } + + ssize_t sent = sendto(sock_fd_, command.c_str(), command.length(), 0, + (struct sockaddr*)&remote_addr_, sizeof(remote_addr_)); + + if (sent < 0) { + ESP_LOGE(TAG, "Failed to send command '%s': errno %d", command.c_str(), errno); + return ESP_FAIL; + } + + ESP_LOGD(TAG, "Sent command: %s (%d bytes)", command.c_str(), (int)sent); + return ESP_OK; +} + +esp_err_t UDPClient::receive_response(std::string& response, int timeout_ms) { + if (!initialized_) { + ESP_LOGE(TAG, "Not initialized"); + return ESP_FAIL; + } + + // Setup select() for timeout + fd_set read_fds; + FD_ZERO(&read_fds); + FD_SET(sock_fd_, &read_fds); + + struct timeval timeout; + struct timeval* timeout_ptr = nullptr; + + if (timeout_ms >= 0) { + timeout.tv_sec = timeout_ms / 1000; + timeout.tv_usec = (timeout_ms % 1000) * 1000; + timeout_ptr = &timeout; + } + + int ret = select(sock_fd_ + 1, &read_fds, nullptr, nullptr, timeout_ptr); + + if (ret < 0) { + ESP_LOGE(TAG, "select() failed: errno %d", errno); + return ESP_FAIL; + } + + if (ret == 0) { + ESP_LOGD(TAG, "Receive timeout (%d ms)", timeout_ms); + return ESP_ERR_TIMEOUT; + } + + // Data is available + char buffer[512]; + struct sockaddr_in from_addr; + socklen_t from_len = sizeof(from_addr); + + ssize_t received = recvfrom(sock_fd_, buffer, sizeof(buffer) - 1, 0, + (struct sockaddr*)&from_addr, &from_len); + + if (received < 0) { + ESP_LOGE(TAG, "recvfrom() failed: errno %d", errno); + return ESP_FAIL; + } + + buffer[received] = '\0'; + response = std::string(buffer, received); + + ESP_LOGD(TAG, "Received response: %s (%d bytes)", response.c_str(), (int)received); + return ESP_OK; +} + +void UDPClient::close() { + if (sock_fd_ >= 0) { + ::close(sock_fd_); + ESP_LOGI(TAG, "Socket closed"); + sock_fd_ = -1; + } + + initialized_ = false; + configured_ = false; + remote_ip_.clear(); + remote_port_ = 0; +} diff --git a/main/network/udp_client.h b/main/network/udp_client.h new file mode 100644 index 0000000..6e9bf49 --- /dev/null +++ b/main/network/udp_client.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include +#include +#include "esp_err.h" + +/** + * @brief UDP client for sending commands and receiving responses + * + * Implements non-blocking UDP communication with configurable timeouts. + * Socket remains open for the lifetime of the instance. + */ +class UDPClient { +public: + UDPClient(); + ~UDPClient(); + + /** + * @brief Initialize UDP socket + * @return ESP_OK on success, error code otherwise + */ + esp_err_t init(); + + /** + * @brief Configure remote endpoint + * @param ip Remote IP address (e.g., "192.168.50.201") + * @param port Remote port number (e.g., 4211) + * @return ESP_OK on success, ESP_ERR_INVALID_ARG if IP is invalid + */ + esp_err_t configure(const std::string& ip, uint16_t port); + + /** + * @brief Send command to remote endpoint + * @param command Command string to send (e.g., "TOGGLE", "STATUS", "MUTE", "UNMUTE") + * @return ESP_OK on success, ESP_FAIL if not configured or send failed + */ + esp_err_t send_command(const std::string& command); + + /** + * @brief Receive response from remote endpoint (non-blocking) + * @param response Output string for received data + * @param timeout_ms Timeout in milliseconds (0 = no wait, -1 = wait forever) + * @return ESP_OK on success, ESP_ERR_TIMEOUT on timeout, ESP_FAIL on error + */ + esp_err_t receive_response(std::string& response, int timeout_ms = 1000); + + /** + * @brief Check if client is configured with valid endpoint + * @return true if IP and port are configured + */ + bool is_configured() const { return configured_; } + + /** + * @brief Get current remote IP + */ + std::string get_ip() const { return remote_ip_; } + + /** + * @brief Get current remote port + */ + uint16_t get_port() const { return remote_port_; } + + /** + * @brief Close socket and reset configuration + */ + void close(); + +private: + int sock_fd_; // Socket file descriptor + struct sockaddr_in remote_addr_; // Remote endpoint address + std::string remote_ip_; // Remote IP address + uint16_t remote_port_; // Remote port number + bool configured_; // Whether endpoint is configured + bool initialized_; // Whether socket is initialized + + /** + * @brief Set socket to non-blocking mode + * @return ESP_OK on success + */ + esp_err_t set_nonblocking(); +}; From dd1702e3e90db0291d5459661e471c3c06085731 Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Sat, 24 Jan 2026 13:13:28 +0800 Subject: [PATCH 14/16] feat: implement PageStack class for multi-page navigation in LVGL apps --- main/ui/page_stack.cpp | 115 +++++++++++++++++++++++++++++++++++++++++ main/ui/page_stack.h | 86 ++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 main/ui/page_stack.cpp create mode 100644 main/ui/page_stack.h diff --git a/main/ui/page_stack.cpp b/main/ui/page_stack.cpp new file mode 100644 index 0000000..3b65173 --- /dev/null +++ b/main/ui/page_stack.cpp @@ -0,0 +1,115 @@ +#include "page_stack.h" +#include "esp_log.h" + +static const char* TAG = "PageStack"; + +PageStack::PageStack(lv_obj_t* parent_container) + : parent_container_(parent_container) { + if (!parent_container_) { + ESP_LOGE(TAG, "Parent container is null"); + } +} + +PageStack::~PageStack() { + clear(); +} + +lv_obj_t* PageStack::create_page_container() { + lv_obj_t* page = lv_obj_create(parent_container_); + + // Fill parent container + lv_obj_set_size(page, LV_PCT(100), LV_PCT(100)); + lv_obj_set_pos(page, 0, 0); + + // Remove padding and scrollbars + lv_obj_set_style_pad_all(page, 0, 0); + lv_obj_set_scrollbar_mode(page, LV_SCROLLBAR_MODE_OFF); + + // White background + lv_obj_set_style_bg_color(page, lv_color_white(), 0); + lv_obj_set_style_bg_opa(page, LV_OPA_COVER, 0); + + // Remove border + lv_obj_set_style_border_width(page, 0, 0); + + return page; +} + +lv_obj_t* PageStack::push(PageBuilder builder, PageCleanup cleanup) { + if (!parent_container_) { + ESP_LOGE(TAG, "Cannot push page: parent container is null"); + return nullptr; + } + + if (!builder) { + ESP_LOGE(TAG, "Cannot push page: builder is null"); + return nullptr; + } + + // Hide current page if any + if (!pages_.empty()) { + lv_obj_add_flag(pages_.back().container, LV_OBJ_FLAG_HIDDEN); + } + + // Create new page container + lv_obj_t* page = create_page_container(); + + // Build page content + builder(page); + + // Add to stack + pages_.push_back({page, cleanup}); + + ESP_LOGD(TAG, "Pushed page (depth: %d)", pages_.size()); + return page; +} + +bool PageStack::pop() { + if (pages_.empty()) { + ESP_LOGW(TAG, "Cannot pop: stack is empty"); + return false; + } + + // Get and remove current page + Page current = pages_.back(); + pages_.pop_back(); + + // Call cleanup callback if provided + if (current.cleanup) { + current.cleanup(current.container); + } + + // Delete page container + lv_obj_del(current.container); + + // Show previous page if any + if (!pages_.empty()) { + lv_obj_clear_flag(pages_.back().container, LV_OBJ_FLAG_HIDDEN); + } + + ESP_LOGD(TAG, "Popped page (depth: %d)", pages_.size()); + return true; +} + +void PageStack::clear() { + ESP_LOGD(TAG, "Clearing all pages (depth: %d)", pages_.size()); + + // Pop all pages (calls cleanup callbacks) + while (!pages_.empty()) { + Page current = pages_.back(); + pages_.pop_back(); + + if (current.cleanup) { + current.cleanup(current.container); + } + + lv_obj_del(current.container); + } +} + +lv_obj_t* PageStack::current_page() const { + if (pages_.empty()) { + return nullptr; + } + return pages_.back().container; +} diff --git a/main/ui/page_stack.h b/main/ui/page_stack.h new file mode 100644 index 0000000..0a3633a --- /dev/null +++ b/main/ui/page_stack.h @@ -0,0 +1,86 @@ +#pragma once + +#include "lvgl.h" +#include +#include + +/** + * @brief Reusable page stack for multi-page navigation within LVGL apps + * + * Manages a stack of LVGL containers, allowing apps to push/pop pages + * and implement hierarchical navigation. Thread-safe for use with LVGL. + */ +class PageStack { +public: + /** + * @brief Page builder callback + * @param page_container The LVGL container to build the page in + */ + using PageBuilder = std::function; + + /** + * @brief Page cleanup callback + * @param page_container The LVGL container being destroyed + */ + using PageCleanup = std::function; + + /** + * @brief Construct page stack with parent container + * @param parent_container Parent LVGL container for pages + */ + explicit PageStack(lv_obj_t* parent_container); + + /** + * @brief Destructor - clears all pages + */ + ~PageStack(); + + /** + * @brief Push a new page onto the stack + * @param builder Function to build page content + * @param cleanup Optional cleanup function called when page is popped + * @return The created page container + */ + lv_obj_t* push(PageBuilder builder, PageCleanup cleanup = nullptr); + + /** + * @brief Pop the current page and return to previous + * @return true if page was popped, false if stack is empty + */ + bool pop(); + + /** + * @brief Clear all pages from the stack + */ + void clear(); + + /** + * @brief Get the current (top) page container + * @return Current page or nullptr if stack is empty + */ + lv_obj_t* current_page() const; + + /** + * @brief Get the number of pages in the stack + */ + size_t depth() const { return pages_.size(); } + + /** + * @brief Check if stack is empty + */ + bool empty() const { return pages_.empty(); } + +private: + struct Page { + lv_obj_t* container; + PageCleanup cleanup; + }; + + lv_obj_t* parent_container_; + std::vector pages_; + + /** + * @brief Create a page container + */ + lv_obj_t* create_page_container(); +}; From 89cc04951f20cdda078aac2d2c8b65ccb790fabd Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Sat, 24 Jan 2026 13:22:17 +0800 Subject: [PATCH 15/16] feat: add DiscordApp for voice control integration with UDP communication --- main/CMakeLists.txt | 20 +- main/main.cpp | 2 + main/ui/apps/discord_app.cpp | 628 +++++++++++++++++++++++++++++++++++ main/ui/apps/discord_app.h | 123 +++++++ 4 files changed, 772 insertions(+), 1 deletion(-) create mode 100644 main/ui/apps/discord_app.cpp create mode 100644 main/ui/apps/discord_app.h diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 7e072c4..981069d 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,5 +1,23 @@ set(requires esp-tls spi_flash nvs_flash esp_event esp_netif esp_http_client esp_wifi esp_psram esp_lvgl_port) -file(GLOB SRCS "main.cpp" "*.cpp" "*.c" "**/*.cpp" "**/*.c") +file(GLOB SRCS "main.cpp" "*.cpp" "*.c" "**/*.cpp" "**/*.cpp" "ui/**/*.cpp" "ui/**/*.c") +# Explicitly list all source files to ensure build system picks them up +# set(SRCS +# "main.cpp" +# "display/display.cpp" +# "display/eink_display_handler.cpp" +# "info/info.cpp" +# "io/nvs_handler.cpp" +# "network/http_handler.cpp" +# "network/network.cpp" +# "network/udp_client.cpp" +# "network/wifi_handler.cpp" +# "ui/page_stack.cpp" +# "ui/root_layout.cpp" +# "ui/ui_handler.cpp" +# "ui/apps/demo_app.cpp" +# "ui/apps/discord_app.cpp" +# "ui/apps/shutdown_app.cpp" +# ) idf_component_register(SRCS ${SRCS} PRIV_REQUIRES ${requires} diff --git a/main/main.cpp b/main/main.cpp index 0630673..92f37d7 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -20,6 +20,7 @@ #include "ui/app_registry.h" #include "ui/apps/demo_app.h" #include "ui/apps/shutdown_app.h" +#include "ui/apps/discord_app.h" #include #include "esp_lvgl_port.h" #include "lvgl.h" @@ -125,6 +126,7 @@ void app_main(void) { // 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 ESP_LOGI(TAG, "Apps registered with AppRegistry\n"); // Initialize UI Handler (will render app icons from registry) diff --git a/main/ui/apps/discord_app.cpp b/main/ui/apps/discord_app.cpp new file mode 100644 index 0000000..c416747 --- /dev/null +++ b/main/ui/apps/discord_app.cpp @@ -0,0 +1,628 @@ +#include "discord_app.h" +#include "esp_log.h" +#include "network/network.h" +#include + +static const char* TAG = "DiscordApp"; + +// ============================================================================ +// DiscordApp Implementation +// ============================================================================ + +DiscordApp::DiscordApp() + : page_stack_(nullptr) + , status_icon_label_(nullptr) + , status_text_label_(nullptr) + , mute_button_(nullptr) + , error_notification_(nullptr) + , ip_textarea_(nullptr) + , port_textarea_(nullptr) + , test_result_label_(nullptr) + , remote_port_(0) + , settings_configured_(false) + , current_state_(VoiceState::UNKNOWN) + , state_mutex_(nullptr) + , poll_task_handle_(nullptr) + , stop_polling_(false) + , consecutive_failures_(0) + , storage_(nullptr) { + + // Create mutex for thread-safe state access + state_mutex_ = xSemaphoreCreateMutex(); + + // Initialize storage + storage_ = new NVSStorageHandler(NVS_NAMESPACE); +} + +DiscordApp::~DiscordApp() { + stop_polling_task(); + + if (state_mutex_) { + vSemaphoreDelete(state_mutex_); + } + + if (storage_) { + delete storage_; + } +} + +esp_err_t DiscordApp::init(lv_obj_t* container) { + ESP_LOGI(TAG, "Initializing Discord app"); + + _container = container; + + // Initialize storage + storage_->init(nullptr); + + // Load saved settings + load_settings(); + + // Initialize UDP client + udp_client_.init(); + + // Configure UDP if settings are available + if (settings_configured_) { + udp_client_.configure(remote_ip_, remote_port_); + } + + // Create page stack + page_stack_ = new PageStack(container); + + // Build main page + page_stack_->push([this](lv_obj_t* page) { + build_main_page(page); + }); + + // Start polling task + start_polling_task(); + + return ESP_OK; +} + +esp_err_t DiscordApp::deinit() { + ESP_LOGI(TAG, "Deinitializing Discord app"); + + // Stop polling + stop_polling_task(); + + // Clean up page stack + if (page_stack_) { + delete page_stack_; + page_stack_ = nullptr; + } + + // Close UDP client + udp_client_.close(); + + // Reset widget pointers + status_icon_label_ = nullptr; + status_text_label_ = nullptr; + mute_button_ = nullptr; + error_notification_ = nullptr; + ip_textarea_ = nullptr; + port_textarea_ = nullptr; + test_result_label_ = nullptr; + + return ESP_OK; +} + +void DiscordApp::handle_event(uint32_t event_type, void* event_data) { + // Handle system events if needed +} + +bool DiscordApp::on_back_button_pressed() { + // If on settings page, go back to main page + if (page_stack_ && page_stack_->depth() > 1) { + page_stack_->pop(); + return true; + } + + // Let system handle back (return to app icons) + return false; +} + +// ============================================================================ +// Main Page UI +// ============================================================================ + +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); + + // 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) + 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_style_bg_color(error_notification_, lv_color_hex(0xFF0000), 0); + lv_obj_set_style_bg_opa(error_notification_, LV_OPA_70, 0); + lv_obj_add_flag(error_notification_, LV_OBJ_FLAG_HIDDEN); + + 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 + if (!settings_configured_) { + lv_obj_t* config_prompt = lv_label_create(page); + 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); + } + + // Update display with current state + update_status_display(); +} + +// ============================================================================ +// Settings Page UI +// ============================================================================ + +void DiscordApp::build_settings_page(lv_obj_t* page) { + // Title + lv_obj_t* title = lv_label_create(page); + lv_label_set_text(title, "Discord Bridge Settings"); + // Using default font + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 20); + + // IP address label + lv_obj_t* ip_label = lv_label_create(page); + lv_label_set_text(ip_label, "Bridge IP Address:"); + lv_obj_align(ip_label, LV_ALIGN_TOP_LEFT, 20, 70); + + // IP address textarea + ip_textarea_ = lv_textarea_create(page); + lv_obj_set_size(ip_textarea_, 300, 50); + lv_obj_align(ip_textarea_, LV_ALIGN_TOP_LEFT, 20, 100); + lv_textarea_set_one_line(ip_textarea_, true); + lv_textarea_set_placeholder_text(ip_textarea_, "e.g., 192.168.1.100"); + + if (!remote_ip_.empty()) { + lv_textarea_set_text(ip_textarea_, remote_ip_.c_str()); + } + + // Port label + lv_obj_t* port_label = lv_label_create(page); + lv_label_set_text(port_label, "Bridge Port:"); + lv_obj_align(port_label, LV_ALIGN_TOP_LEFT, 20, 170); + + // Port textarea + port_textarea_ = lv_textarea_create(page); + lv_obj_set_size(port_textarea_, 150, 50); + lv_obj_align(port_textarea_, LV_ALIGN_TOP_LEFT, 20, 200); + lv_textarea_set_one_line(port_textarea_, true); + lv_textarea_set_placeholder_text(port_textarea_, "e.g., 4211"); + lv_textarea_set_accepted_chars(port_textarea_, "0123456789"); + lv_textarea_set_max_length(port_textarea_, 5); + + if (remote_port_ > 0) { + char port_str[8]; + snprintf(port_str, sizeof(port_str), "%u", remote_port_); + lv_textarea_set_text(port_textarea_, port_str); + } + + // Test connection button + lv_obj_t* test_btn = lv_btn_create(page); + lv_obj_set_size(test_btn, 200, 50); + lv_obj_align(test_btn, LV_ALIGN_TOP_MID, 0, 270); + lv_obj_add_event_cb(test_btn, on_test_connection_clicked, LV_EVENT_CLICKED, this); + + lv_obj_t* test_label = lv_label_create(test_btn); + lv_label_set_text(test_label, "Test Connection"); + lv_obj_center(test_label); + + // Test result label + test_result_label_ = lv_label_create(page); + lv_label_set_text(test_result_label_, ""); + lv_obj_align(test_result_label_, LV_ALIGN_TOP_MID, 0, 330); + + // Save button + lv_obj_t* save_btn = lv_btn_create(page); + lv_obj_set_size(save_btn, 150, 50); + lv_obj_align(save_btn, LV_ALIGN_BOTTOM_MID, 0, -20); + lv_obj_add_event_cb(save_btn, on_save_settings_clicked, LV_EVENT_CLICKED, this); + lv_obj_set_style_bg_color(save_btn, lv_color_hex(0x00AA00), 0); + + lv_obj_t* save_label = lv_label_create(save_btn); + lv_label_set_text(save_label, LV_SYMBOL_SAVE " Save"); + lv_obj_set_style_text_color(save_label, lv_color_white(), 0); + lv_obj_center(save_label); +} + +void DiscordApp::show_settings_page() { + page_stack_->push([this](lv_obj_t* page) { + build_settings_page(page); + }); +} + +// ============================================================================ +// Event Callbacks +// ============================================================================ + +void DiscordApp::on_mute_button_clicked(lv_event_t* e) { + DiscordApp* app = static_cast(lv_event_get_user_data(e)); + if (app) { + app->send_mute_command(); + } +} + +void DiscordApp::on_settings_button_clicked(lv_event_t* e) { + DiscordApp* app = static_cast(lv_event_get_user_data(e)); + if (app) { + app->show_settings_page(); + } +} + +void DiscordApp::on_save_settings_clicked(lv_event_t* e) { + DiscordApp* app = static_cast(lv_event_get_user_data(e)); + if (app) { + app->save_settings(); + + // Go back to main page + if (app->page_stack_->depth() > 1) { + app->page_stack_->pop(); + } + } +} + +void DiscordApp::on_test_connection_clicked(lv_event_t* e) { + DiscordApp* app = static_cast(lv_event_get_user_data(e)); + if (!app || !app->test_result_label_) return; + + // Get values from textareas + const char* ip = lv_textarea_get_text(app->ip_textarea_); + const char* port_str = lv_textarea_get_text(app->port_textarea_); + + if (strlen(ip) == 0 || strlen(port_str) == 0) { + lv_label_set_text(app->test_result_label_, LV_SYMBOL_CLOSE " Please fill all fields"); + lv_obj_set_style_text_color(app->test_result_label_, lv_color_hex(0xFF0000), 0); + return; + } + + uint16_t port = atoi(port_str); + if (port == 0) { + lv_label_set_text(app->test_result_label_, LV_SYMBOL_CLOSE " Invalid port"); + lv_obj_set_style_text_color(app->test_result_label_, lv_color_hex(0xFF0000), 0); + return; + } + + // Configure UDP temporarily + UDPClient test_client; + test_client.init(); + esp_err_t err = test_client.configure(ip, port); + + if (err != ESP_OK) { + lv_label_set_text(app->test_result_label_, LV_SYMBOL_CLOSE " Invalid IP address"); + lv_obj_set_style_text_color(app->test_result_label_, lv_color_hex(0xFF0000), 0); + return; + } + + lv_label_set_text(app->test_result_label_, "Testing..."); + lv_obj_set_style_text_color(app->test_result_label_, lv_color_hex(0x0000FF), 0); + + // Send STATUS command + err = test_client.send_command("STATUS"); + if (err != ESP_OK) { + lv_label_set_text(app->test_result_label_, LV_SYMBOL_CLOSE " Failed to send"); + lv_obj_set_style_text_color(app->test_result_label_, lv_color_hex(0xFF0000), 0); + return; + } + + // Wait for response + std::string response; + err = test_client.receive_response(response, 3000); + + if (err == ESP_OK && (response == "MUTED" || response == "UNMUTED")) { + lv_label_set_text(app->test_result_label_, LV_SYMBOL_OK " Connection successful!"); + lv_obj_set_style_text_color(app->test_result_label_, lv_color_hex(0x00AA00), 0); + } else { + lv_label_set_text(app->test_result_label_, LV_SYMBOL_CLOSE " No response from bridge"); + lv_obj_set_style_text_color(app->test_result_label_, lv_color_hex(0xFF0000), 0); + } +} + +// ============================================================================ +// UDP Communication +// ============================================================================ + +void DiscordApp::send_mute_command() { + if (!settings_configured_) { + ESP_LOGW(TAG, "Cannot send command: not configured"); + return; + } + + esp_err_t err = udp_client_.send_command("MUTE"); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to send MUTE command"); + } +} + +bool DiscordApp::test_connection() { + if (!settings_configured_) { + return false; + } + + esp_err_t err = udp_client_.send_command("STATUS"); + if (err != ESP_OK) { + return false; + } + + std::string response; + err = udp_client_.receive_response(response, RESPONSE_TIMEOUT_MS); + + return (err == ESP_OK && (response == "MUTED" || response == "UNMUTED")); +} + +void DiscordApp::update_status_display() { + if (!status_icon_label_ || !status_text_label_) { + return; + } + + // Thread-safe state access + VoiceState state; + if (xSemaphoreTake(state_mutex_, pdMS_TO_TICKS(100)) == pdTRUE) { + state = current_state_; + xSemaphoreGive(state_mutex_); + } else { + return; + } + + switch (state) { + case VoiceState::MUTED: + lv_label_set_text(status_icon_label_, LV_SYMBOL_MUTE); + lv_label_set_text(status_text_label_, "Muted"); + lv_obj_set_style_text_color(status_icon_label_, lv_color_hex(0xFF0000), 0); + break; + + case VoiceState::UNMUTED: + lv_label_set_text(status_icon_label_, LV_SYMBOL_VOLUME_MAX); + lv_label_set_text(status_text_label_, "Unmuted"); + lv_obj_set_style_text_color(status_icon_label_, lv_color_hex(0x00AA00), 0); + break; + + case VoiceState::ERROR: + lv_label_set_text(status_icon_label_, LV_SYMBOL_WARNING); + lv_label_set_text(status_text_label_, "Connection Error"); + lv_obj_set_style_text_color(status_icon_label_, lv_color_hex(0xFF8800), 0); + break; + + case VoiceState::UNKNOWN: + default: + lv_label_set_text(status_icon_label_, LV_SYMBOL_BLUETOOTH); + lv_label_set_text(status_text_label_, "Unknown Status"); + lv_obj_set_style_text_color(status_icon_label_, lv_color_hex(0x888888), 0); + break; + } +} + +void DiscordApp::show_error_notification(bool show) { + if (error_notification_) { + if (show) { + lv_obj_clear_flag(error_notification_, LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(error_notification_, LV_OBJ_FLAG_HIDDEN); + } + } +} + +// ============================================================================ +// Settings Management +// ============================================================================ + +void DiscordApp::load_settings() { + remote_ip_ = storage_->get(NVS_KEY_IP); + std::string port_str = storage_->get(NVS_KEY_PORT); + + if (!remote_ip_.empty() && !port_str.empty()) { + remote_port_ = atoi(port_str.c_str()); + settings_configured_ = (remote_port_ > 0); + ESP_LOGI(TAG, "Loaded settings: %s:%u", remote_ip_.c_str(), remote_port_); + } else { + settings_configured_ = false; + ESP_LOGI(TAG, "No settings found, user setup required"); + } +} + +void DiscordApp::save_settings() { + if (!ip_textarea_ || !port_textarea_) { + return; + } + + const char* ip = lv_textarea_get_text(ip_textarea_); + const char* port_str = lv_textarea_get_text(port_textarea_); + + if (strlen(ip) == 0 || strlen(port_str) == 0) { + ESP_LOGW(TAG, "Cannot save: empty fields"); + return; + } + + uint16_t port = atoi(port_str); + if (port == 0) { + ESP_LOGW(TAG, "Cannot save: invalid port"); + return; + } + + // Save to NVS + storage_->put(NVS_KEY_IP, ip); + storage_->put(NVS_KEY_PORT, port_str); + + // Update local config + remote_ip_ = ip; + remote_port_ = port; + settings_configured_ = true; + + // Reconfigure UDP client + udp_client_.configure(remote_ip_, remote_port_); + + // Reset failure counter + consecutive_failures_ = 0; + + ESP_LOGI(TAG, "Settings saved: %s:%u", remote_ip_.c_str(), remote_port_); +} + +// ============================================================================ +// Polling Task +// ============================================================================ + +void DiscordApp::poll_task(void* param) { + DiscordApp* app = static_cast(param); + + ESP_LOGI(TAG, "Polling task started"); + + while (!app->stop_polling_) { + app->poll_status(); + + // Use longer interval if in error state + int interval = (app->consecutive_failures_ >= MAX_FAILURES_BEFORE_ERROR) + ? ERROR_POLL_INTERVAL_MS + : POLL_INTERVAL_MS; + + vTaskDelay(pdMS_TO_TICKS(interval)); + } + + ESP_LOGI(TAG, "Polling task stopped"); + app->poll_task_handle_ = nullptr; + vTaskDelete(nullptr); +} + +void DiscordApp::start_polling_task() { + if (poll_task_handle_) { + ESP_LOGW(TAG, "Polling task already running"); + return; + } + + stop_polling_ = false; + xTaskCreate(poll_task, "discord_poll", 4096, this, 5, &poll_task_handle_); +} + +void DiscordApp::stop_polling_task() { + if (!poll_task_handle_) { + return; + } + + ESP_LOGI(TAG, "Stopping polling task"); + stop_polling_ = true; + + // Wait for task to finish (max 2 seconds) + int wait_count = 0; + while (poll_task_handle_ && wait_count < 20) { + vTaskDelay(pdMS_TO_TICKS(100)); + wait_count++; + } + + if (poll_task_handle_) { + ESP_LOGW(TAG, "Force deleting polling task"); + vTaskDelete(poll_task_handle_); + poll_task_handle_ = nullptr; + } +} + +void DiscordApp::poll_status() { + if (!settings_configured_) { + // Don't poll if not configured + return; + } + + // Send STATUS command + esp_err_t err = udp_client_.send_command("STATUS"); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to send STATUS command"); + consecutive_failures_++; + + if (consecutive_failures_ >= MAX_FAILURES_BEFORE_ERROR) { + if (xSemaphoreTake(state_mutex_, pdMS_TO_TICKS(100)) == pdTRUE) { + current_state_ = VoiceState::ERROR; + xSemaphoreGive(state_mutex_); + } + show_error_notification(true); + } + return; + } + + // Wait for response + std::string response; + err = udp_client_.receive_response(response, RESPONSE_TIMEOUT_MS); + + if (err == ESP_OK) { + // Success - reset failure counter + consecutive_failures_ = 0; + show_error_notification(false); + + // Update state + VoiceState new_state = VoiceState::UNKNOWN; + if (response == "MUTED") { + new_state = VoiceState::MUTED; + } else if (response == "UNMUTED") { + new_state = VoiceState::UNMUTED; + } + + if (xSemaphoreTake(state_mutex_, pdMS_TO_TICKS(100)) == pdTRUE) { + current_state_ = new_state; + xSemaphoreGive(state_mutex_); + } + + update_status_display(); + + } else { + // Timeout or error + consecutive_failures_++; + ESP_LOGW(TAG, "No response (failures: %d)", consecutive_failures_); + + if (consecutive_failures_ >= MAX_FAILURES_BEFORE_ERROR) { + if (xSemaphoreTake(state_mutex_, pdMS_TO_TICKS(100)) == pdTRUE) { + current_state_ = VoiceState::ERROR; + xSemaphoreGive(state_mutex_); + } + update_status_display(); + show_error_notification(true); + } + } +} + +// ============================================================================ +// DiscordAppDescriptor Implementation +// ============================================================================ + +DiscordAppDescriptor::DiscordAppDescriptor() + : AppDescriptor("Discord", new DiscordApp()) { + // Auto-register on construction + AppRegistry::instance().register_app(this); +} + +void DiscordAppDescriptor::draw_icon(lv_obj_t* parent) { + lv_obj_t* icon = lv_label_create(parent); + lv_label_set_text(icon, LV_SYMBOL_CALL); + lv_obj_center(icon); +} diff --git a/main/ui/apps/discord_app.h b/main/ui/apps/discord_app.h new file mode 100644 index 0000000..d1e5f6e --- /dev/null +++ b/main/ui/apps/discord_app.h @@ -0,0 +1,123 @@ +#pragma once + +#include "ui/ui_app.h" +#include "ui/page_stack.h" +#include "ui/app_registry.h" +#include "network/udp_client.h" +#include "io/nvs_handler.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include + +/** + * @brief Discord voice control app + * + * Allows control of Discord voice settings (mute/unmute) via UDP communication + * with the IotDis Node.js bridge. Features: + * - Main page: Status icon + mute button + * - Settings page: IP/port configuration with connection test + * - Periodic status polling with automatic retry + * - Error notification when remote is unreachable + */ +class DiscordApp : public UIApp { +public: + DiscordApp(); + ~DiscordApp() override; + + // UIApp interface + esp_err_t init(lv_obj_t* container) override; + esp_err_t deinit() override; + std::string get_name() const override { return "Discord"; } + void handle_event(uint32_t event_type, void* event_data = nullptr) override; + bool on_back_button_pressed() override; + +private: + // Voice state enum + enum class VoiceState { + UNKNOWN, + MUTED, + UNMUTED, + ERROR + }; + + // Page management + PageStack* page_stack_; + void build_main_page(lv_obj_t* page); + void build_settings_page(lv_obj_t* page); + void show_settings_page(); + + // Main page widgets + lv_obj_t* status_icon_label_; + lv_obj_t* status_text_label_; + lv_obj_t* mute_button_; + lv_obj_t* error_notification_; + + // Settings page widgets + lv_obj_t* ip_textarea_; + lv_obj_t* port_textarea_; + lv_obj_t* test_result_label_; + + // UDP client and configuration + UDPClient udp_client_; + std::string remote_ip_; + uint16_t remote_port_; + bool settings_configured_; + + // Voice state + VoiceState current_state_; + SemaphoreHandle_t state_mutex_; + + // Polling task + TaskHandle_t poll_task_handle_; + bool stop_polling_; + int consecutive_failures_; + static constexpr int MAX_FAILURES_BEFORE_ERROR = 3; + static constexpr int POLL_INTERVAL_MS = 5000; + static constexpr int ERROR_POLL_INTERVAL_MS = 15000; + static constexpr int RESPONSE_TIMEOUT_MS = 2000; + + // NVS storage + NVSStorageHandler* storage_; + static constexpr const char* NVS_NAMESPACE = "discord"; + static constexpr const char* NVS_KEY_IP = "remote_ip"; + static constexpr const char* NVS_KEY_PORT = "remote_port"; + + // Event callbacks + static void on_mute_button_clicked(lv_event_t* e); + static void on_settings_button_clicked(lv_event_t* e); + static void on_save_settings_clicked(lv_event_t* e); + static void on_test_connection_clicked(lv_event_t* e); + + // UDP communication + void send_mute_command(); + bool test_connection(); + void update_status_display(); + void show_error_notification(bool show); + + // Settings management + void load_settings(); + void save_settings(); + + // Polling task + static void poll_task(void* param); + void start_polling_task(); + void stop_polling_task(); + void poll_status(); +}; + +/** + * @brief Discord app descriptor for registration + */ +class DiscordAppDescriptor : public AppDescriptor { +public: + static DiscordAppDescriptor& instance() { + static DiscordAppDescriptor instance; + return instance; + } + + void draw_icon(lv_obj_t* parent) override; + +private: + DiscordAppDescriptor(); +}; From 39c4cfd85fba66a5c22f64b64d1ccd905ad7a14f Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Sat, 24 Jan 2026 13:22:29 +0800 Subject: [PATCH 16/16] feat: add sample code directory to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index a93d5f0..33e9252 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,6 @@ Desktop.ini # vscode settings .vscode/ + +# sample code +sample-code/