50 Commits

Author SHA1 Message Date
GW_MC
de2f166151 Enhance EInkDisplayHandler and LVGLHandler with deep sleep functionality and partial refresh improvements 2026-01-27 19:12:37 +08:00
GW_MC
dc2a76e131 Fixed partial refresh problem, but refresh may blackout un touched pixels 2026-01-27 11:50:12 +08:00
GW_MC
5f491dff6e Fix lvgl not mem correctly 2026-01-26 22:50:27 +08:00
GW_MC
6fbbfcde4f Added lvgl init, display not refresh correctly 2026-01-26 22:50:02 +08:00
GW_MC
30dfdd630a Re implement display 2026-01-26 18:17:39 +08:00
GW_MC
abe840b65d Remove incorrect watchdog reset 2026-01-25 21:54:26 +08:00
GW_MC
f3dfc4f43f refactor: improve watchdog handling and screen loading in UI and display handlers 2026-01-25 19:39:21 +08:00
GW_MC
5865f6d383 Fix display not init 2026-01-25 18:58:20 +08:00
GW_MC
259660a0bc Fix touch screen not responding, but screen still not refreshed. 2026-01-25 15:51:49 +08:00
GW_MC
57f698425b no more error in display, but no refresh 2026-01-25 15:28:07 +08:00
GW_MC
580d6a0a5b Merge branch 'setup' into feature/mtr-app 2026-01-24 18:25:11 +08:00
GW_MC
68f2c821fa refactor: comment out logging for key-value storage and adjust string allocation in NVS handler 2026-01-24 18:25:00 +08:00
GW_MC
d0a1e8c80f feat: enhance EInk display handler with improved resource management and non-blocking SPI transactions 2026-01-24 18:24:41 +08:00
GW_MC
9487efff0e Merge branch 'setup' into feature/mtr-app 2026-01-24 17:15:20 +08:00
GW_MC
143a28de90 feat: add support for build-time WiFi credentials from .env file 2026-01-24 17:14:32 +08:00
GW_MC
d091625cea feat: add MTR Next Train application with multi-page navigation and real-time arrival info 2026-01-24 16:46:00 +08:00
GW_MC
d01167fd77 feat: implement MTR Next Train Handler with arrival and line info parsing 2026-01-24 16:45:53 +08:00
GW_MC
694ead2b42 Merge branch 'display' into feature/mtr-app 2026-01-24 13:31:18 +08:00
GW_MC
39c4cfd85f feat: add sample code directory to .gitignore 2026-01-24 13:22:29 +08:00
GW_MC
89cc04951f feat: add DiscordApp for voice control integration with UDP communication 2026-01-24 13:22:17 +08:00
GW_MC
dd1702e3e9 feat: implement PageStack class for multi-page navigation in LVGL apps 2026-01-24 13:13:28 +08:00
GW_MC
dfd8959f58 feat: implement UDPClient class for non-blocking UDP communication 2026-01-24 13:13:18 +08:00
GW_MC
162b3710eb 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.
2026-01-24 10:40:09 +08:00
GW_MC
86e102adc7 feat: add DemoApp and ShutdownApp classes for interactive UI and shutdown management 2026-01-24 10:39:44 +08:00
GW_MC
ccae9e89da feat: add DemoApp and ShutdownApp classes for interactive UI components and shutdown management 2026-01-24 10:39:37 +08:00
GW_MC
0c26d91565 feat: implement RootLayout and UIHandler for improved UI structure and app management 2026-01-24 10:39:30 +08:00
GW_MC
6ad55c7efc feat: add AppRegistry, RootLayout, UIHandler, and UIApp classes for improved UI management 2026-01-24 10:39:16 +08:00
GW_MC
d248557614 feat: implement EInkDisplayHandler for enhanced E-Ink display management and touch input handling 2026-01-24 10:39:03 +08:00
GW_MC
4f7418c77a feat: enhance display handling with EInkDisplayHandler class and update DisplayHandler interface 2026-01-24 10:38:58 +08:00
GW_MC
4fa8dc608f feat: add display and touch initialization in DisplayHandler 2026-01-21 14:10:39 +08:00
GW_MC
44fb9aa632 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.
2026-01-21 14:00:04 +08:00
GW_MC
14f4b8fdc0 feat: update dependencies and configuration for esp32s3 support 2026-01-21 13:58:25 +08:00
GW_MC
fae9d30e3a feat: refactor header files and add info for psram 2026-01-20 20:15:44 +08:00
GW_MC
e163392532 Remove exception throwing 2026-01-20 20:15:05 +08:00
GW_MC
8f9f89cb32 feat: add structures for MTR arrival and station information handling 2026-01-20 20:11:29 +08:00
GW_MC
4d19dd7294 feat: update cJSON and add JSON minification for MTR_LINE_STATION 2026-01-20 20:11:04 +08:00
GW_MC
654a0bc0f7 feat: add mtr_line_station.json with station details and codes for multiple lines 2026-01-20 20:10:24 +08:00
GW_MC
a1404a196e feat: enhance WifiHandler initialization with event handling and TCP/IP stack setup 2026-01-20 10:04:24 +08:00
GW_MC
41516374f0 fix: invalid const and declaration ordering and added smart pointer for get 2026-01-19 21:09:11 +08:00
GW_MC
4cda7d2de3 feat: integrate NetworkHandler and WifiHandler for network initialization 2026-01-19 20:44:52 +08:00
GW_MC
a801caaae6 feat: implement HttpHandler and WifiHandler classes for network management 2026-01-19 20:44:28 +08:00
GW_MC
89e8014798 feat: implement HttpHandler and WifiHandler classes for HTTP client management 2026-01-19 20:44:08 +08:00
GW_MC
1d12dc5160 feat: add esp_wifi to required components in CMakeLists.txt 2026-01-19 20:42:07 +08:00
GW_MC
0b26e0c7c9 feat: semaphore guard helper 2026-01-19 20:38:51 +08:00
GW_MC
89daff2267 Merge branch 'setup' into network-control 2026-01-19 12:55:38 +08:00
GW_MC
18ac21e257 Enhance NVSStorageHandler with filtering capabilities and update constructor to accept namespace 2026-01-19 12:55:12 +08:00
GW_MC
821fb0d9d7 added network dependencies 2026-01-19 11:19:59 +08:00
GW_MC
01c36669cf Fix event group reference in app_main for shutdown handling 2026-01-18 14:46:31 +08:00
GW_MC
d339a1f4c3 Add NVS storage handler and integrate with main application logic 2026-01-18 14:46:25 +08:00
GW_MC
e458256193 Add main application logic and touch handling functionality
- Implemented main application entry point in main.cpp, initializing queues and event groups.
- Created TouchHandler and EInkTouchHandler classes for handling touch events.
- Added a minimal event loop for touch processing in touch.cpp.
- Introduced unit tests for the hello world application in pytest_hello_world.py.
- Added configuration files for CI and Wokwi support.
- Created empty header files for network and UI modules.
2026-01-17 20:09:33 +08:00
68 changed files with 4113 additions and 4836 deletions

View File

@@ -51,16 +51,6 @@ dependencies:
source:
type: idf
version: 5.5.2
joltwallet/littlefs:
component_hash: 1808d73e99168f6f3c26dd31799a248484762b3a320ec4962dec11a145f4277f
dependencies:
- name: idf
require: private
version: '>=5.0'
source:
registry_url: https://components.espressif.com/
type: service
version: 1.20.3
lvgl/lvgl:
component_hash: 17e68bfd21f0edf4c3ee838e2273da840bf3930e5dbc3bfa6c1190c3aed41f9f
dependencies: []
@@ -73,8 +63,7 @@ direct_dependencies:
- espressif/esp_lcd_touch_gt911
- espressif/esp_lvgl_port
- idf
- joltwallet/littlefs
- lvgl/lvgl
manifest_hash: 534b6804ed0fcb2390bfe237db938fe86c9ba00561b361035a89dde4847214f2
manifest_hash: 2010806782b4d2486b02b853afa44a545717d3d0593eb60f9aa6e5c696270f8f
target: esp32s3
version: 2.0.0

View File

@@ -1,5 +1,5 @@
set(requires esp-tls spi_flash nvs_flash esp_event esp_netif esp_http_client esp_http_server esp_wifi esp_psram esp_lvgl_port)
file(GLOB_RECURSE SRCS "main.cpp" "*.cpp" "*.c" "ui/**/*.cpp" "ui/**/*.c" "external/**/*.cpp" "external/**/*.c")
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" "**/*.cpp" "ui/**/*.cpp" "ui/**/*.c" "external/**/*.cpp" "external/**/*.c")
# Path to the source JSON in this component

View File

@@ -1,30 +0,0 @@
#pragma once
class NetworkHandler;
class WifiHandler;
/**
* @brief System context providing access to global system components
*/
class SystemContext {
public:
static SystemContext& instance() {
static SystemContext context;
return context;
}
void set_network_handler(NetworkHandler* handler) {
network_handler_ = handler;
}
NetworkHandler* get_network_handler() const {
return network_handler_;
}
private:
SystemContext() = default;
SystemContext(const SystemContext&) = delete;
SystemContext& operator=(const SystemContext&) = delete;
NetworkHandler* network_handler_ = nullptr;
};

View File

@@ -0,0 +1,199 @@
#include "display/display.h"
#include "common/constants.h"
#include "esp_log.h"
#include "esp_lcd_touch_gt911.h"
#define BUSY_ACTIVE_LEVEL 0 // BUSY pin is active low
#define BUSY_INACTIVE_LEVEL 1
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 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);
}
}
void DisplayHandler::epd_write_cmd(uint8_t cmd) {
ESP_LOGI("DisplayHandler", "epd_write_cmd: waiting to send 0x%02X", cmd);
if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) {
ESP_LOGE("DisplayHandler", "SPI mutex timeout for cmd 0x%02X", cmd);
return;
}
_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);
if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) {
ESP_LOGE("DisplayHandler", "SPI mutex timeout for data 0x%02X", data);
return;
}
_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);
if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) {
ESP_LOGE("DisplayHandler", "SPI mutex timeout for cmd with data 0x%02X", cmd);
return;
}
_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 {
ESP_LOGI("DisplayHandler", "_dangerous_epd_write_cmd_without_lock: 0x%02X sent", cmd);
}
}
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);
}
}
// 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 with detailed logging
ESP_LOGI("DisplayHandler", "Waiting for EPD to be ready after power on...");
ESP_LOGI("DisplayHandler", "BUSY pin level after power on: %d (0=BUSY, 1=FREE)", gpio_get_level(PIN_BUSY));
int busy_timeout = 0;
while (gpio_get_level(PIN_BUSY) == BUSY_ACTIVE_LEVEL) { // BUSY is active LOW
vTaskDelay(pdMS_TO_TICKS(10));
busy_timeout++;
if (busy_timeout > 500) { // 5 second timeout
ESP_LOGE("DisplayHandler", "EPD power on timeout! BUSY pin stuck at 0");
break;
}
if (busy_timeout % 50 == 0) { // Log every 500ms
ESP_LOGW("DisplayHandler", "Still waiting for EPD power on, timeout: %d/500", busy_timeout);
}
}
ESP_LOGI("DisplayHandler", "EPD power on complete after %d * 10ms, BUSY pin: %d", busy_timeout, gpio_get_level(PIN_BUSY));
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) {
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);
// GT911-specific config with I2C address (0x5D = INT low during reset)
static esp_lcd_touch_io_gt911_config_t gt911_config = {
.dev_addr = ESP_LCD_TOUCH_IO_I2C_GT911_ADDRESS // 0x5D
};
esp_lcd_touch_config_t tp_cfg = {};
tp_cfg.x_max = 800;
tp_cfg.y_max = 480;
tp_cfg.rst_gpio_num = PIN_TOUCH_RST;
tp_cfg.int_gpio_num = PIN_TOUCH_IRQ;
tp_cfg.driver_data = &gt911_config; // Pass GT911-specific config for automatic reset
esp_err_t touch_ret = esp_lcd_touch_new_i2c_gt911(_tp_io_handle, &tp_cfg, &_tp_handle);
if (touch_ret == ESP_OK && _tp_handle != nullptr) {
ESP_LOGI("DisplayHandler", "GT911 touch controller initialized successfully");
} else {
ESP_LOGE("DisplayHandler", "GT911 touch controller initialization failed: %s", esp_err_to_name(touch_ret));
_tp_handle = nullptr;
}
}

View File

@@ -0,0 +1,42 @@
#pragma once
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_lcd_touch_gt911.h"
#include "display/constants.h"
#include <driver/i2c.h>
class DisplayHandler {
public:
DisplayHandler(
EventGroupHandle_t system_event_group
) : _system_event_group(system_event_group) { }
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
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);
protected:
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);
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,661 @@
#include "display/eink_display_handler.h"
#include "display/constants.h"
#include "common/constants.h"
#include "esp_log.h"
#include "esp_heap_caps.h"
#include "esp_task_wdt.h"
#include <cstring>
#define TAG "EInkDisplayHandler"
#define BUSY_ACTIVE_LEVEL 0 // BUSY pin is active low
#define BUSY_INACTIVE_LEVEL 1
EInkDisplayHandler::EInkDisplayHandler(EventGroupHandle_t system_event_group)
: DisplayHandler(system_event_group) {
_refresh_mutex = xSemaphoreCreateMutex();
if (_refresh_mutex == nullptr) {
ESP_LOGE(TAG, "Failed to create refresh mutex");
}
}
EInkDisplayHandler::~EInkDisplayHandler() {
if (_refresh_task_handle != nullptr) {
vTaskDelete(_refresh_task_handle);
}
if (_touch_task_handle != nullptr) {
vTaskDelete(_touch_task_handle);
}
if (_refresh_queue != nullptr) {
vQueueDelete(_refresh_queue);
}
if (_lvgl_display != nullptr) {
lv_display_delete(_lvgl_display);
_lvgl_display = nullptr;
if (_lvgl_draw_buf != nullptr) {
lv_draw_buf_destroy(_lvgl_draw_buf);
_lvgl_draw_buf = nullptr;
}
}
if (_lvgl_touch_indev != nullptr) {
lvgl_port_remove_touch(_lvgl_touch_indev);
}
if (_framebuffer != nullptr) {
heap_caps_free(_framebuffer);
}
if (_refresh_mutex != nullptr) {
vSemaphoreDelete(_refresh_mutex);
}
}
void EInkDisplayHandler::init() {
ESP_LOGI(TAG, "Initializing E-Ink display handler...");
// Initialize GPIO pins
gpio_config_t io_conf = {};
io_conf.pin_bit_mask = (1ULL << PIN_DC) | (1ULL << PIN_RST);
io_conf.mode = GPIO_MODE_OUTPUT;
io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
io_conf.intr_type = GPIO_INTR_DISABLE;
gpio_config(&io_conf);
// Configure BUSY pin as input (no pull-up like sample code)
io_conf.pin_bit_mask = (1ULL << PIN_BUSY);
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
gpio_config(&io_conf);
// Initialize SPI bus
spi_bus_config_t buscfg = {};
buscfg.mosi_io_num = 11; // MOSI pin
buscfg.miso_io_num = -1; // No MISO for e-paper
buscfg.sclk_io_num = 12; // SCK pin
buscfg.quadwp_io_num = -1;
buscfg.quadhd_io_num = -1;
buscfg.max_transfer_sz = DISPLAY_BUFFER_SIZE;
esp_err_t ret = spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize SPI bus: %s", esp_err_to_name(ret));
return;
}
// Add SPI device
spi_device_interface_config_t devcfg = {};
devcfg.clock_speed_hz = 6 * 1000 * 1000; // 6 MHz (reduced for reliability)
devcfg.mode = 0; // SPI mode 0
devcfg.spics_io_num = PIN_CS;
devcfg.queue_size = 7; // Queue size for non-blocking transactions
devcfg.pre_cb = nullptr;
ret = spi_bus_add_device(SPI2_HOST, &devcfg, &_spi);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to add SPI device: %s", esp_err_to_name(ret));
return;
}
// Initialize base display and touch devices
init_devices(false); // Don't set ready bit yet
// Create refresh queue (queue 5 refresh requests)
_refresh_queue = xQueueCreate(5, sizeof(bool));
if (_refresh_queue == nullptr) {
ESP_LOGE(TAG, "Failed to create refresh queue");
return;
}
// Create refresh task
BaseType_t ret_task = xTaskCreatePinnedToCore(
_refresh_task,
"eink_refresh",
8192,
this,
5, // Priority - lower than LVGL task
&_refresh_task_handle,
1 // Pin to core 1
);
if (ret_task != pdPASS) {
ESP_LOGE(TAG, "Failed to create refresh task");
return;
}
// Allocate framebuffer - try PSRAM first, fallback to internal RAM
// Note: Internal framebuffer excludes the 8-byte palette (raw pixel data only)
const size_t fb_size = DISPLAY_BUFFER_SIZE - 8; // Exclude palette from internal storage
_framebuffer = (uint8_t*)heap_caps_malloc(fb_size, MALLOC_CAP_SPIRAM);
if (_framebuffer != nullptr) {
_framebuffer_in_psram = true;
ESP_LOGI(TAG, "Framebuffer allocated in PSRAM (%zu bytes, LVGL buffer: %d bytes)",
fb_size, DISPLAY_BUFFER_SIZE);
} else {
ESP_LOGW(TAG, "PSRAM not available, allocating framebuffer in internal RAM");
_framebuffer = (uint8_t*)heap_caps_malloc(fb_size, MALLOC_CAP_INTERNAL);
_framebuffer_in_psram = false;
if (_framebuffer == nullptr) {
ESP_LOGE(TAG, "Failed to allocate framebuffer");
return;
}
ESP_LOGI(TAG, "Framebuffer allocated in internal RAM (%zu bytes, LVGL buffer: %d bytes)",
fb_size, DISPLAY_BUFFER_SIZE);
}
memset(_framebuffer, 0xFF, fb_size); // Initialize to white
// Perform initial full refresh to clear display BEFORE creating LVGL display
// This prevents LVGL from trying to render during the initial clear
ESP_LOGI(TAG, "Performing initial display clear...");
_perform_full_refresh(_framebuffer);
ESP_LOGI(TAG, "Initial display clear complete");
// Create LVGL display manually (no esp_lcd panel for e-paper)
lv_display_t* disp = lv_display_create(DISPLAY_WIDTH, DISPLAY_HEIGHT);
if (disp == nullptr) {
ESP_LOGE(TAG, "Failed to create LVGL display");
return;
}
/* 1-bit e-paper display */
lv_display_set_color_format(disp, LV_COLOR_FORMAT_I1);
/* Disable antialiasing for monochrome display to ensure crisp 1px lines */
lv_display_set_antialiasing(disp, false);
/* Create a draw buffer covering ~40 lines */
_lvgl_draw_buf = lv_draw_buf_create(DISPLAY_WIDTH, DISPLAY_HEIGHT, LV_COLOR_FORMAT_I1, LV_STRIDE_AUTO);
if (_lvgl_draw_buf == nullptr) {
ESP_LOGE(TAG, "Failed to create LVGL draw buffer");
lv_display_delete(disp);
return;
}
lv_display_set_draw_buffers(disp, _lvgl_draw_buf, NULL);
lv_display_set_render_mode(disp, LV_DISPLAY_RENDER_MODE_DIRECT);
// Set custom flush callback and user data
lv_display_set_flush_cb(disp, _lvgl_flush_cb);
lv_display_set_user_data(disp, this);
_lvgl_display = disp;
ESP_LOGI(TAG, "LVGL display registered");
// Register GT911 touch input with LVGL, only if touch handle is valid
esp_lcd_touch_handle_t tp_handle = get_touch_handle();
if (tp_handle == nullptr) {
ESP_LOGE(TAG, "Touch handle is NULL — touch initialization failed; skipping LVGL touch registration");
} else {
const lvgl_port_touch_cfg_t touch_cfg = {
.disp = _lvgl_display,
.handle = tp_handle,
.scale = {}, // Default scaling
};
_lvgl_touch_indev = lvgl_port_add_touch(&touch_cfg);
if (_lvgl_touch_indev == nullptr) {
ESP_LOGE(TAG, "Failed to register LVGL touch input");
return;
}
// Override touch read callback to check BUSY pin
lv_indev_set_read_cb(_lvgl_touch_indev, _lvgl_touch_read_cb);
lv_indev_set_user_data(_lvgl_touch_indev, this);
ESP_LOGI(TAG, "LVGL touch input registered");
}
// Set display ready bits
xEventGroupSetBits(_system_event_group, DISPLAY_READY_BIT | TOUCH_CALIBRATED_BIT);
ESP_LOGI(TAG, "E-Ink display handler initialized successfully");
}
void EInkDisplayHandler::start_touch_task() {
// Note: With lvgl_port_add_touch, the ESP-IDF LVGL port handles touch reading internally
// We don't need a separate touch task unless we want custom processing
ESP_LOGI(TAG, "Touch input handled by LVGL port");
}
void EInkDisplayHandler::request_full_refresh() {
if (xSemaphoreTake(_refresh_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
_force_full_refresh = true;
_partial_refresh_count = 0;
xSemaphoreGive(_refresh_mutex);
ESP_LOGI(TAG, "Full refresh requested");
}
}
bool EInkDisplayHandler::is_busy() const {
return gpio_get_level(PIN_BUSY) == BUSY_ACTIVE_LEVEL; // BUSY is active LOW
}
void EInkDisplayHandler::_lvgl_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map) {
EInkDisplayHandler* handler = static_cast<EInkDisplayHandler*>(lv_display_get_user_data(disp));
if (handler == nullptr) {
ESP_LOGE(TAG, "Invalid handler in flush callback");
lv_display_flush_ready(disp);
return;
}
// Check if display is busy with detailed logging
int busy_level = gpio_get_level(PIN_BUSY);
ESP_LOGI(TAG, "Flush callback: BUSY pin = %d, is_busy() = %d", busy_level, handler->is_busy());
if (handler->is_busy()) {
ESP_LOGW(TAG, "Display busy (BUSY pin = 0), skipping flush");
lv_display_flush_ready(disp);
return;
}
// Wait for any ongoing refresh to complete
handler->_wait_for_busy();
bool perform_full_refresh = false;
if (xSemaphoreTake(handler->_refresh_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
// Check if full refresh is needed
if (handler->_force_full_refresh) {
perform_full_refresh = true;
handler->_force_full_refresh = false;
handler->_partial_refresh_count = 0;
} else {
handler->_partial_refresh_count++;
if (handler->_partial_refresh_count >= PARTIAL_REFRESH_THRESHOLD) {
perform_full_refresh = true;
handler->_partial_refresh_count = 0;
}
}
xSemaphoreGive(handler->_refresh_mutex);
}
// Copy LVGL buffer to framebuffer
// For 1-bit mode, LVGL provides data in packed format (8 pixels per byte)
// CRITICAL: Skip first 8 bytes (LVGL I1 palette) as per LVGL documentation
uint8_t* pixel_data = px_map + 8; // Skip 8-byte palette
int32_t w = lv_area_get_width(area);
int32_t h = lv_area_get_height(area);
ESP_LOGI(TAG, "Flushing area: x=%d, y=%d, w=%d, h=%d, full_refresh=%d",
area->x1, area->y1, w, h, perform_full_refresh);
ESP_LOGI(TAG, "Buffer: px_map=%p, pixel_data=%p, palette skipped: %d bytes",
(void*)px_map, (void*)pixel_data, 8);
// Check if this is a full screen update - if so, simple copy
if (area->x1 == 0 && area->y1 == 0 && w == DISPLAY_WIDTH && h == DISPLAY_HEIGHT) {
ESP_LOGI(TAG, "Full screen update, direct copy (skipping palette)");
memcpy(handler->_framebuffer, pixel_data, DISPLAY_BUFFER_SIZE - 8);
} else {
ESP_LOGI(TAG, "Partial area update");
// In DIRECT render mode, px_map points to the full screen buffer
// The stride is always the full display width
const uint32_t stride = DISPLAY_WIDTH / 8; // 800 / 8 = 100 bytes per row
// Check if we can do row-by-row copy (byte-aligned on both x1 and width)
bool byte_aligned = (area->x1 % 8 == 0) && (w % 8 == 0);
if (byte_aligned) {
// Optimized: byte-aligned row copy
ESP_LOGI(TAG, "Byte-aligned copy: x=%ld, y=%ld, w=%ld, h=%ld",
(long)area->x1, (long)area->y1, (long)w, (long)h);
uint32_t x_byte = area->x1 / 8;
uint32_t width_bytes = w / 8;
for (int32_t y = 0; y < h; y++) {
int32_t fb_y = area->y1 + y;
if (fb_y >= DISPLAY_HEIGHT) break;
uint8_t* src = pixel_data + (fb_y * stride + x_byte);
uint8_t* dst = handler->_framebuffer + (fb_y * stride + x_byte);
memcpy(dst, src, width_bytes);
}
} else {
// Bit-level copy for non-aligned regions
ESP_LOGI(TAG, "Bit-level copy: x=%ld, y=%ld, w=%ld, h=%ld",
(long)area->x1, (long)area->y1, (long)w, (long)h);
for (int32_t y = 0; y < h; y++) {
int32_t fb_y = area->y1 + y;
if (fb_y >= DISPLAY_HEIGHT) break;
for (int32_t x = 0; x < w; x++) {
int32_t fb_x = area->x1 + x;
if (fb_x >= DISPLAY_WIDTH) break;
// Get pixel from source buffer (using full screen coordinates)
size_t src_byte_idx = fb_y * stride + (fb_x / 8);
size_t src_bit_idx = fb_x % 8;
uint8_t src_bit = (pixel_data[src_byte_idx] >> (7 - src_bit_idx)) & 0x01;
// Set pixel in destination buffer
size_t dst_byte_idx = fb_y * stride + (fb_x / 8);
size_t dst_bit_idx = fb_x % 8;
if (dst_byte_idx < (DISPLAY_BUFFER_SIZE - 8)) {
if (src_bit) {
handler->_framebuffer[dst_byte_idx] |= (1 << (7 - dst_bit_idx));
} else {
handler->_framebuffer[dst_byte_idx] &= ~(1 << (7 - dst_bit_idx));
}
}
}
}
}
}
// Queue refresh request (non-blocking)
if (handler->_refresh_queue != nullptr) {
if (xQueueSend(handler->_refresh_queue, &perform_full_refresh, 0) != pdPASS) {
ESP_LOGW(TAG, "Refresh queue full, skipping refresh");
} else {
ESP_LOGI(TAG, "Queued %s refresh", perform_full_refresh ? "full" : "partial");
}
}
lv_display_flush_ready(disp);
}
void EInkDisplayHandler::_lvgl_touch_read_cb(lv_indev_t* indev, lv_indev_data_t* data) {
EInkDisplayHandler* handler = static_cast<EInkDisplayHandler*>(lv_indev_get_user_data(indev));
// Disable touch input during display refresh (BUSY)
if (handler->is_busy()) {
data->state = LV_INDEV_STATE_RELEASED;
data->continue_reading = false;
return;
}
esp_lcd_touch_handle_t tp_handle = handler->get_touch_handle();
if (tp_handle == nullptr) {
data->state = LV_INDEV_STATE_RELEASED;
return;
}
// Read touch data from GT911
esp_err_t ret = esp_lcd_touch_read_data(tp_handle);
if (ret == ESP_OK) {
uint8_t touch_cnt = 0;
// Get touch data using new API
esp_lcd_touch_point_data_t point_data[1];
esp_lcd_touch_get_data(tp_handle, point_data, &touch_cnt, 1);
if (touch_cnt > 0) {
ESP_LOGI(TAG, "Touch data read successfully: x=%d, y=%d", point_data[0].x, point_data[0].y);
data->point.x = point_data[0].x;
data->point.y = point_data[0].y;
data->state = LV_INDEV_STATE_PRESSED;
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
data->continue_reading = false;
}
void EInkDisplayHandler::_perform_full_refresh(const uint8_t* framebuffer) {
ESP_LOGI(TAG, "Starting full refresh (3 seconds)...");
_wait_for_busy();
// Step 1: Write old data (0x10) - Arduino uses 0xFF (all white) for base map
epd_write_cmd(0x10);
if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) {
ESP_LOGE(TAG, "SPI mutex timeout in full refresh step 1");
return;
}
gpio_set_level(PIN_DC, 1); // Data mode
ESP_LOGI(TAG, "Starting SPI data transmission for old data (0x10)...");
// Send 0xFF (white) for all old data, matching Arduino EPD_SetRAMValue_BaseMap
// Use DMA transfers in chunks for better performance
static uint8_t white_buffer[4096]; // 4KB chunk buffer
memset(white_buffer, 0xFF, sizeof(white_buffer));
const size_t CHUNK_SIZE = sizeof(white_buffer);
size_t remaining = DISPLAY_BUFFER_SIZE - 8; // Exclude palette from transmission
size_t offset = 0;
while (remaining > 0) {
size_t transfer_size = (remaining < CHUNK_SIZE) ? remaining : CHUNK_SIZE;
spi_transaction_t t = {};
t.length = transfer_size * 8; // Length in bits
t.tx_buffer = white_buffer;
esp_err_t ret = spi_device_polling_transmit(_spi, &t);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to send SPI chunk at offset %zu: %s", offset, esp_err_to_name(ret));
break;
}
remaining -= transfer_size;
offset += transfer_size;
// Yield every 16KB to prevent watchdog timeout
if (offset % (16 * 1024) == 0) {
ESP_LOGI(TAG, "Old data progress: %zu/%zu bytes (%.1f%%)", offset, remaining,
(float)offset * 100.0f / (float)remaining);
vTaskDelay(pdMS_TO_TICKS(1));
}
}
ESP_LOGI(TAG, "Completed SPI data transmission for old data");
xSemaphoreGive(_spi_mutex);
// Step 2: Write new data (0x13)
epd_write_cmd(0x13);
if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) {
ESP_LOGE(TAG, "SPI mutex timeout in full refresh step 2");
return;
}
gpio_set_level(PIN_DC, 1); // Data mode
ESP_LOGI(TAG, "Starting SPI data transmission for new data (0x13)...");
// Send actual framebuffer data in chunks using DMA for better performance
offset = 0;
remaining = DISPLAY_BUFFER_SIZE - 8; // Reset remaining for step 2
while (remaining > 0) {
size_t transfer_size = (remaining < CHUNK_SIZE) ? remaining : CHUNK_SIZE;
spi_transaction_t t = {};
t.length = transfer_size * 8; // Length in bits
t.tx_buffer = framebuffer + offset;
esp_err_t ret = spi_device_polling_transmit(_spi, &t);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to send SPI chunk at offset %zu: %s", offset, esp_err_to_name(ret));
break;
}
remaining -= transfer_size;
offset += transfer_size;
// Yield every 16KB to prevent watchdog timeout
if (offset % (16 * 1024) == 0) {
ESP_LOGI(TAG, "New data progress: %zu/%zu bytes (%.1f%%)", offset, remaining,
(float)offset * 100.0f / (float)remaining);
vTaskDelay(pdMS_TO_TICKS(1));
}
}
ESP_LOGI(TAG, "Completed SPI data transmission for new data");
xSemaphoreGive(_spi_mutex);
// Step 3: Trigger display refresh (DRF)
epd_write_cmd(0x12);
// Critical delay - sample code says "!!!The delay here is necessary, 200uS at least!!!"
vTaskDelay(pdMS_TO_TICKS(10));
ESP_LOGI(TAG, "Display refresh triggered, BUSY pin: %d", gpio_get_level(PIN_BUSY));
// Wait for refresh to complete
_wait_for_busy();
ESP_LOGI(TAG, "Full refresh complete");
}
void EInkDisplayHandler::_perform_partial_refresh(const uint8_t* framebuffer) {
ESP_LOGI(TAG, "Starting partial refresh (0.3 seconds)...");
_wait_for_busy();
// Step 1: Configure VCOM for partial refresh
const uint8_t vcom_data[] = { 0xA9, 0x07 };
epd_write_cmd_with_data(0x50, vcom_data, 2);
// Step 2: Enter partial refresh mode
epd_write_cmd(0x91);
// Step 3: Define partial window (full screen for now)
// Format: 0x90 + 9 bytes (x_start_H, x_start_L, x_end_H, x_end_L, y_start_H, y_start_L, y_end_H, y_end_L, 0x01)
// For full screen: x=0 to 799 (0x031F), y=0 to 479 (0x01DF)
const uint8_t window_data[] = {
0x00, 0x00, // x_start = 0
0x03, 0x1F, // x_end = 799 (0x31F)
0x00, 0x00, // y_start = 0
0x01, 0xDF, // y_end = 479 (0x1DF)
0x01 // PT_SCAN
};
epd_write_cmd_with_data(0x90, window_data, 9);
// Step 4: Write new data (0x13 command)
epd_write_cmd(0x13);
if (xSemaphoreTake(_spi_mutex, pdMS_TO_TICKS(5000)) != pdTRUE) {
ESP_LOGE(TAG, "SPI mutex timeout in partial refresh");
return;
}
gpio_set_level(PIN_DC, 1); // Data mode
ESP_LOGI(TAG, "Starting SPI data transmission for partial refresh...");
// Send framebuffer data in chunks using DMA for better performance
const size_t CHUNK_SIZE = 4096; // 4KB chunks
size_t remaining = DISPLAY_BUFFER_SIZE - 8; // Exclude palette from transmission
size_t offset = 0;
while (remaining > 0) {
size_t transfer_size = (remaining < CHUNK_SIZE) ? remaining : CHUNK_SIZE;
spi_transaction_t t = {};
t.length = transfer_size * 8; // Length in bits
t.tx_buffer = framebuffer + offset;
esp_err_t ret = spi_device_polling_transmit(_spi, &t);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to send SPI chunk at offset %zu: %s", offset, esp_err_to_name(ret));
break;
}
remaining -= transfer_size;
offset += transfer_size;
// Yield every 16KB to prevent watchdog timeout
if (offset % (16 * 1024) == 0) {
ESP_LOGI(TAG, "Partial refresh progress: %zu/%zu bytes (%.1f%%)", offset, remaining,
(float)offset * 100.0f / (float)remaining);
vTaskDelay(pdMS_TO_TICKS(1));
}
}
ESP_LOGI(TAG, "Completed SPI data transmission for partial refresh");
xSemaphoreGive(_spi_mutex);
// Step 5: Trigger partial display refresh (DRF)
epd_write_cmd(0x12);
// Critical delay - sample code says "!!!The delay here is necessary, 200uS at least!!!"
vTaskDelay(pdMS_TO_TICKS(10));
ESP_LOGI(TAG, "Partial refresh triggered, BUSY pin: %d", gpio_get_level(PIN_BUSY));
// Wait for refresh to complete
_wait_for_busy();
// Step 6: Exit partial refresh mode
epd_write_cmd(0x92);
ESP_LOGI(TAG, "Partial refresh complete");
}
void EInkDisplayHandler::_refresh_task(void* param) {
EInkDisplayHandler* handler = static_cast<EInkDisplayHandler*>(param);
bool perform_full_refresh = false;
ESP_LOGI(TAG, "Refresh task started");
while (true) {
// Wait for refresh request
if (xQueueReceive(handler->_refresh_queue, &perform_full_refresh, portMAX_DELAY) == pdTRUE) {
// Perform the requested refresh type
if (perform_full_refresh) {
ESP_LOGI(TAG, "Refresh task: Performing full refresh...");
handler->_perform_full_refresh(handler->_framebuffer);
} else {
ESP_LOGI(TAG, "Refresh task: Performing partial refresh...");
handler->_perform_partial_refresh(handler->_framebuffer);
}
}
}
}
void EInkDisplayHandler::_wait_for_busy() {
ESP_LOGI(TAG, "Waiting for display ready (BUSY pin)...");
int initial_level = gpio_get_level(PIN_BUSY);
ESP_LOGI(TAG, "Initial BUSY pin level: %d (0=BUSY, 1=FREE)", initial_level);
// If already free, no need to wait
if (initial_level == BUSY_INACTIVE_LEVEL) {
ESP_LOGI(TAG, "Display already ready (BUSY pin = 1)");
return;
}
int timeout = 0;
while (gpio_get_level(PIN_BUSY) == BUSY_ACTIVE_LEVEL) { // 0=BUSY, 1=FREE
vTaskDelay(pdMS_TO_TICKS(100));
timeout++;
if (timeout > 100) { // 10 second timeout
ESP_LOGE(TAG, "Display BUSY timeout! Pin level: %d", gpio_get_level(PIN_BUSY));
ESP_LOGW(TAG, "Attempting hardware reset...");
// Hardware reset sequence
gpio_set_level(PIN_RST, 0);
vTaskDelay(pdMS_TO_TICKS(10));
gpio_set_level(PIN_RST, 1);
vTaskDelay(pdMS_TO_TICKS(100));
// Re-initialize display
ESP_LOGI(TAG, "Re-initializing display after reset...");
_epd_init();
// Check if reset worked
int reset_timeout = 0;
while (gpio_get_level(PIN_BUSY) == BUSY_ACTIVE_LEVEL) {
vTaskDelay(pdMS_TO_TICKS(100));
reset_timeout++;
if (reset_timeout > 50) { // 5 second timeout after reset
ESP_LOGE(TAG, "Display reset failed! Still busy after reset.");
break;
}
}
if (gpio_get_level(PIN_BUSY) != BUSY_ACTIVE_LEVEL) {
ESP_LOGI(TAG, "Display reset successful after %d tenths of a second", reset_timeout);
}
break;
}
// Log every 2 seconds to track progress
if (timeout % 20 == 0) {
ESP_LOGW(TAG, "Still waiting for BUSY pin, timeout: %d/100, level: %d",
timeout, gpio_get_level(PIN_BUSY));
}
}
ESP_LOGI(TAG, "Display ready after %d tenths of a second", timeout);
}
void EInkDisplayHandler::_convert_buffer_to_epaper(const uint8_t* lvgl_buf, uint8_t* epd_buf, size_t size) {
// LVGL 1-bit format is already compatible with e-paper
// Just copy directly
memcpy(epd_buf, lvgl_buf, size);
}

View File

@@ -5,9 +5,9 @@
#include "common/semaphore_guard.h"
#include <vector>
#include <atomic>
#include "epd_handler.h"
// Refresh mode configuration
#define PARTIAL_REFRESH_THRESHOLD 10 // Full refresh every N partial refreshes
#define DISPLAY_WIDTH 800
#define DISPLAY_HEIGHT 480
@@ -56,43 +56,71 @@ public:
esp_err_t refresh_display(void);
esp_err_t full_write(const uint8_t* framebuffer, const bool white_basemap = true);
esp_err_t partial_refresh(const uint8_t* framebuffer, const RefreshArea& area, const bool is_last_partial_update = true);
esp_err_t partial_refresh(const uint8_t* framebuffer, const RefreshArea& area);
esp_err_t clear_display(void);
esp_err_t deep_sleep_display(void);
// Request a full refresh on next flush
void request_full_refresh(void);
bool is_busy() {
return epd_handler_.is_busy();
}
// Check if display is busy (refreshing)
bool is_busy(void) const;
void wait_for_idle(void) const;
esp_lcd_touch_handle_t get_touch_handle() const { return tp_handle_; }
protected:
esp_err_t epd_write_cmd(const uint8_t cmd, uint32_t transaction_id);
esp_err_t epd_write_data(const uint8_t data, uint32_t transaction_id);
esp_err_t epd_write_cmd_with_data(const uint8_t cmd, std::vector<uint8_t>& data, uint32_t transaction_id);
esp_err_t transfer_spi_data(const uint8_t* data, const size_t& length, uint32_t transaction_id);
private:
esp_err_t init_display_pins_(void);
esp_err_t epd_init_internal_(uint32_t transaction_id); // full fast refresh init
esp_err_t epd_init_(void); // full fast refresh init
esp_err_t epd_init_partial_(void); // partial refresh init (standalone)
esp_err_t epd_init_partial_internal_(uint32_t transaction_id); // partial refresh init (within existing transaction)
esp_err_t init_touch_(void);
esp_err_t dangerous_epd_write_cmd_without_lock_(const uint8_t cmd);
esp_err_t dangerous_epd_write_data_without_lock_(const uint8_t data);
// write to the internal draw buffer
void write_to_buffer_(const uint8_t* src, const RefreshArea& area);
// write the internal draw buffer to the display's old sram
esp_err_t refresh_old_buffer_(uint32_t transaction_id);
esp_err_t begin_transaction_(TickType_t timeout, uint32_t& out_id);
esp_err_t end_transaction_(void);
// given a transaction ID, wait for current transaction to complete. The transaction ID will determine if the wait is needed.
esp_err_t wait_for_transaction_end_(TickType_t timeout, uint32_t awaiting_transaction_id, SemaphoreGuard& out_transaction_guard);
friend class TransactionGuard;
EPDHandler epd_handler_;
uint32_t partial_refresh_count_ = 0;
bool force_full_refresh_ = false;
std::atomic<bool> is_deep_sleep_ { false };
SemaphoreHandle_t spi_mutex_ = nullptr;
SemaphoreHandle_t spi_transaction_mutex_ = nullptr;
SemaphoreHandle_t refresh_mutex_ = nullptr;
uint32_t spi_transaction_id = 0; // For tracking SPI transactions
spi_device_handle_t spi_ = nullptr;
esp_lcd_panel_io_handle_t tp_io_handle_ = nullptr;
esp_lcd_touch_handle_t tp_handle_ = nullptr;
// this buffer reflects the current display state (1=black, 0=white)
uint8_t* draw_buffer_ = nullptr;
uint8_t* old_buffer_ = nullptr;
RefreshArea refresh_area_ = { 0, 0, 0, 0 };
};
class TransactionGuard {
public:
TransactionGuard(EInkDisplayHandler& handler, TickType_t timeout = portMAX_DELAY)
: handler_(handler) { }
~TransactionGuard() { if (transaction_id_) handler_.end_transaction_(); }
esp_err_t begin(TickType_t timeout = portMAX_DELAY) {
esp_err_t err = handler_.begin_transaction_(timeout, transaction_id_);
return err;
}
uint32_t transaction_id() const { return transaction_id_; }
bool is_active() const { return transaction_id_ != 0; }
private:
// delete copy constructor and assignment operator
TransactionGuard(const TransactionGuard&) = delete;
TransactionGuard& operator=(const TransactionGuard&) = delete;
EInkDisplayHandler& handler_;
uint32_t transaction_id_ = 0;
};

View File

@@ -0,0 +1,66 @@
#pragma once
#include "display/display.h"
#include "lvgl.h"
#include "esp_lvgl_port.h"
#include "freertos/semphr.h"
// Refresh mode configuration
#define PARTIAL_REFRESH_THRESHOLD 10 // Full refresh every N partial refreshes
#define DISPLAY_WIDTH 800
#define DISPLAY_HEIGHT 480
#define DISPLAY_BUFFER_SIZE (((DISPLAY_WIDTH * DISPLAY_HEIGHT) / 8) + 8) // 1-bit per pixel + 8-byte palette
class EInkDisplayHandler : public DisplayHandler {
public:
EInkDisplayHandler(EventGroupHandle_t system_event_group);
virtual ~EInkDisplayHandler();
void init();
void start_touch_task();
// Request a full refresh on next flush
void request_full_refresh();
// Check if display is busy (refreshing)
bool is_busy() const;
private:
// LVGL display and input device handles
lv_display_t* _lvgl_display = nullptr;
lv_indev_t* _lvgl_touch_indev = nullptr;
lv_draw_buf_t* _lvgl_draw_buf = nullptr;
// Framebuffer
uint8_t* _framebuffer = nullptr;
bool _framebuffer_in_psram = false;
// Refresh tracking
uint32_t _partial_refresh_count = 0;
bool _force_full_refresh = false;
SemaphoreHandle_t _refresh_mutex = nullptr;
// Touch task
TaskHandle_t _touch_task_handle = nullptr;
// Refresh task and queue
TaskHandle_t _refresh_task_handle = nullptr;
QueueHandle_t _refresh_queue = nullptr;
// LVGL callbacks
static void _lvgl_flush_cb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map);
static void _lvgl_touch_read_cb(lv_indev_t* indev, lv_indev_data_t* data);
// Display operations
void _perform_full_refresh(const uint8_t* framebuffer);
void _perform_partial_refresh(const uint8_t* framebuffer);
void _wait_for_busy();
// Touch task
static void _touch_task(void* param);
// Refresh task
static void _refresh_task(void* param);
// Helper to convert LVGL 1-bit buffer to e-paper format
void _convert_buffer_to_epaper(const uint8_t* lvgl_buf, uint8_t* epd_buf, size_t size);
};

View File

@@ -1,326 +0,0 @@
#include "display/epd_handler.h"
#include "esp_log.h"
#include "display/constants.h"
#include "common/constants.h"
#include "esp_lcd_touch_gt911.h"
#include <driver/i2c.h>
#define TAG "EPDHandler"
#define BUSY_ACTIVE_LEVEL 0 // BUSY pin is active low
#define BUSY_INACTIVE_LEVEL 1
#define DMA_TRANSFER_CHUNK_SIZE 4096 // 4KB chunk size for DMA transfers
EPDHandler::EPDHandler() {
spi_mutex_ = xSemaphoreCreateMutex();
if (spi_mutex_ == nullptr) {
ESP_LOGE(TAG, "Failed to create SPI mutex");
}
spi_transaction_mutex_ = xSemaphoreCreateMutex();
if (spi_transaction_mutex_ == nullptr) {
ESP_LOGE(TAG, "Failed to create SPI transaction mutex");
}
}
EPDHandler::~EPDHandler() {
if (spi_mutex_ != nullptr) {
vSemaphoreDelete(spi_mutex_);
}
if (spi_transaction_mutex_ != nullptr) {
vSemaphoreDelete(spi_transaction_mutex_);
}
if (spi_ != nullptr) {
spi_bus_remove_device(spi_);
}
}
esp_err_t EPDHandler::init() {
esp_err_t err;
// 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 = DMA_TRANSFER_CHUNK_SIZE;
err = spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize SPI bus: %s", esp_err_to_name(err));
return err;
}
// Add SPI device
spi_device_interface_config_t devcfg = {};
devcfg.clock_speed_hz = 10 * 1000 * 1000; // 10 MHz
devcfg.mode = 0; // SPI mode 0
devcfg.spics_io_num = PIN_CS;
devcfg.queue_size = 7; // Queue size for non-blocking transactions
devcfg.pre_cb = nullptr;
err = spi_bus_add_device(SPI2_HOST, &devcfg, &spi_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to add SPI device: %s", esp_err_to_name(err));
return err;
}
return ESP_OK;
}
// Check if display is busy (refreshing)
bool EPDHandler::is_busy(void) const {
return gpio_get_level(PIN_BUSY) != BUSY_ACTIVE_LEVEL; // BUSY is active LOW
}
void EPDHandler::wait_for_idle(void) const {
ESP_LOGV(TAG, "Waiting for display ready (BUSY pin)...");
int initial_level = gpio_get_level(PIN_BUSY);
ESP_LOGV(TAG, "Initial BUSY pin level: %d (0=BUSY, 1=FREE)", initial_level);
// If already free, no need to wait
if (initial_level == BUSY_INACTIVE_LEVEL) {
ESP_LOGV(TAG, "Display already ready (BUSY pin = 1)");
return;
}
while (gpio_get_level(PIN_BUSY) != BUSY_INACTIVE_LEVEL) {
vTaskDelay(pdMS_TO_TICKS(10));
}
ESP_LOGV(TAG, "Display is now ready (BUSY pin = 1)");
}
esp_err_t EPDHandler::epd_write_cmd(const uint8_t cmd, uint32_t transaction_id) {
ESP_LOGV(TAG, "epd_write_cmd: waiting to send 0x%02X", cmd);
SemaphoreGuard transaction_guard(spi_transaction_mutex_);
esp_err_t err =
wait_for_transaction_end_(pdMS_TO_TICKS(5000), transaction_id, transaction_guard);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to wait for previous transaction end before sending cmd 0x%02X: %s",
cmd, esp_err_to_name(err));
return err;
}
SemaphoreGuard guard(spi_mutex_);
if (!guard.take(pdMS_TO_TICKS(5000))) {
ESP_LOGE(TAG, "SPI mutex timeout for cmd 0x%02X", cmd);
return ESP_ERR_TIMEOUT;
}
err = dangerous_epd_write_cmd_without_lock_(cmd);
ESP_LOGV(TAG, "epd_write_cmd: 0x%02X done", cmd);
return err;
}
esp_err_t EPDHandler::epd_write_data(const uint8_t data, uint32_t transaction_id) {
ESP_LOGV(TAG, "epd_write_data: waiting to send 0x%02X", data);
SemaphoreGuard transaction_guard(spi_transaction_mutex_);
esp_err_t err =
wait_for_transaction_end_(pdMS_TO_TICKS(5000), transaction_id, transaction_guard);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to wait for previous transaction end before sending data 0x%02X: %s",
data, esp_err_to_name(err));
return err;
}
SemaphoreGuard guard(spi_mutex_);
if (!guard.take(pdMS_TO_TICKS(5000))) {
ESP_LOGE(TAG, "SPI mutex timeout for data 0x%02X", data);
return ESP_ERR_TIMEOUT;
}
err = dangerous_epd_write_data_without_lock_(data);
ESP_LOGV(TAG, "epd_write_data: 0x%02X done", data);
return err;
}
esp_err_t EPDHandler::epd_write_cmd_with_data(const uint8_t cmd, std::vector<uint8_t>& data, uint32_t transaction_id) {
const size_t data_len = data.size();
ESP_LOGV(TAG, "epd_write_cmd_with_data: waiting to send cmd 0x%02X with %u bytes of data", cmd, data_len);
SemaphoreGuard transaction_guard(spi_transaction_mutex_);
esp_err_t err =
wait_for_transaction_end_(pdMS_TO_TICKS(5000), transaction_id, transaction_guard);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to wait for previous transaction end before sending cmd 0x%02X: %s, with data",
cmd, esp_err_to_name(err));
return err;
}
SemaphoreGuard guard(spi_mutex_);
if (!guard.take(pdMS_TO_TICKS(5000))) {
ESP_LOGE(TAG, "SPI mutex timeout for cmd with data 0x%02X", cmd);
return ESP_ERR_TIMEOUT;
}
err = dangerous_epd_write_cmd_without_lock_(cmd);
if (err != ESP_OK) {
return err;
};
for (size_t i = 0; i < data_len; ++i) {
err = dangerous_epd_write_data_without_lock_(data[i]);
if (err != ESP_OK) {
return err;
}
}
ESP_LOGV(TAG, "epd_write_cmd_with_data: cmd 0x%02X with %u bytes of data done", cmd, data_len);
return ESP_OK;
}
esp_err_t EPDHandler::dangerous_epd_write_cmd_without_lock_(const uint8_t cmd) {
ESP_LOGV(TAG, "dangerous_epd_write_cmd_without_lock_: sending 0x%02X", cmd);
gpio_set_level(PIN_DC, 0); // Command mode
spi_transaction_t t {};
t.length = 8;t.tx_buffer = &cmd;
esp_err_t err = spi_device_polling_transmit(spi_, &t);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send data 0x%02X", cmd);
} else {
ESP_LOGV(TAG, "dangerous_epd_write_cmd_without_lock_: 0x%02X sent", cmd);
}
return err;
}
esp_err_t EPDHandler::dangerous_epd_write_data_without_lock_(const uint8_t data) {
ESP_LOGV(TAG, "dangerous_epd_write_data_without_lock_: sending 0x%02X", data);
gpio_set_level(PIN_DC, 1); // Data mode
spi_transaction_t t = { };
t.length = 8; t.tx_buffer = &data;
esp_err_t err = spi_device_polling_transmit(spi_, &t);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send data 0x%02X", data);
} else {
ESP_LOGV(TAG, "dangerous_epd_write_data_without_lock_: 0x%02X sent", data);
}
return err;
}
esp_err_t EPDHandler::transfer_spi_data(const uint8_t* data, const size_t& length, uint32_t transaction_id, bool inverted) {
ESP_LOGV(TAG, "transfer_spi_data: waiting to send %zu bytes of data", length);
SemaphoreGuard transaction_guard(spi_transaction_mutex_);
esp_err_t err =
wait_for_transaction_end_(pdMS_TO_TICKS(5000), transaction_id, transaction_guard);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to wait for previous transaction end before sending data of %zu bytes: %s",
length, esp_err_to_name(err));
return err;
}
SemaphoreGuard guard(spi_mutex_);
if (!guard.take(pdMS_TO_TICKS(5000))) {
ESP_LOGE(TAG, "SPI mutex timeout for data transfer of %zu bytes", length);
return ESP_ERR_TIMEOUT;
}
ESP_LOGV(TAG, "transfer_spi_data: starting to send %zu bytes of data", length);
size_t offset = 0;
size_t remaining = length;
gpio_set_level(PIN_DC, 1); // Data mode
// Allocate a temporary buffer for inverted data (only if inverted)
uint8_t* temp_transfer_buffer = nullptr;
if (inverted) {
temp_transfer_buffer = (uint8_t*)heap_caps_malloc(DMA_TRANSFER_CHUNK_SIZE, MALLOC_CAP_DMA);
if (temp_transfer_buffer == nullptr) {
ESP_LOGE(TAG, "Failed to allocate memory for inverted data transfer buffer");
ESP_LOGI(TAG, "Current free heap size: %u bytes", esp_get_free_heap_size());
ESP_LOGI(TAG, "Current free DMA-capable memory size: %u bytes",
heap_caps_get_free_size(MALLOC_CAP_DMA));
return ESP_ERR_NO_MEM;
}
}
while (remaining > 0) {
size_t transfer_size = (remaining < DMA_TRANSFER_CHUNK_SIZE) ? remaining : DMA_TRANSFER_CHUNK_SIZE;
const uint8_t* transfer_buffer = nullptr;
if (inverted) {
// Invert only the current chunk into the temporary buffer
for (size_t i = 0; i < transfer_size; ++i) {
temp_transfer_buffer[i] = ~data[offset + i];
}
transfer_buffer = temp_transfer_buffer;
} else {
transfer_buffer = data + offset;
}
spi_transaction_t t = {};
t.length = transfer_size * 8; // Length in bits
t.tx_buffer = transfer_buffer;
esp_err_t ret = spi_device_polling_transmit(spi_, &t);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to send SPI chunk at offset %zu: %s", offset, esp_err_to_name(ret));
if (ret == ESP_ERR_NO_MEM) {
ESP_LOGE(TAG, "Current free heap size: %u bytes", esp_get_free_heap_size());
ESP_LOGE(TAG, "Current free DMA-capable memory size: %u bytes",
heap_caps_get_free_size(MALLOC_CAP_DMA));
}
if (inverted && temp_transfer_buffer != nullptr) {
// Free the temporary inverted buffer
heap_caps_free(temp_transfer_buffer);
}
return ret;
}
remaining -= transfer_size;
offset += transfer_size;
// Yield every 16KB to prevent watchdog timeout
if (offset % (16 * 1024) == 0) {
ESP_LOGV(TAG, "New data progress: %zu/%zu bytes sent, yielding...", offset, length);
vTaskDelay(pdMS_TO_TICKS(1));
}
}
if (inverted && temp_transfer_buffer != nullptr) {
// Free the temporary inverted buffer
heap_caps_free(temp_transfer_buffer);
}
ESP_LOGV(TAG, "transfer_spi_data: completed sending %zu bytes of data", length);
return ESP_OK;
}
esp_err_t EPDHandler::begin_transaction_(TickType_t timeout, uint32_t& out_id) {
ESP_LOGV(TAG, "begin_transaction_: waiting to obtain transaction mutex");
if (xSemaphoreTake(spi_transaction_mutex_, timeout) != pdTRUE) {
ESP_LOGE(TAG, "begin_transaction_: transaction mutex timeout");
return ESP_ERR_TIMEOUT;
}
out_id = ++spi_transaction_id;
ESP_LOGV(TAG, "begin_transaction_: transaction mutex obtained");
return ESP_OK;
}
esp_err_t EPDHandler::end_transaction_(void) {
ESP_LOGV(TAG, "end_transaction_: releasing transaction mutex");
if (xSemaphoreGive(spi_transaction_mutex_) != pdTRUE) {
ESP_LOGE(TAG, "end_transaction_: failed to release transaction mutex");
return ESP_FAIL;
}
ESP_LOGV(TAG, "end_transaction_: transaction mutex released");
return ESP_OK;
}
esp_err_t EPDHandler::wait_for_transaction_end_(TickType_t timeout, uint32_t awaiting_transaction_id, SemaphoreGuard& out_transaction_guard) {
// Validate transaction ID if provided
if (awaiting_transaction_id != 0 && awaiting_transaction_id != spi_transaction_id) {
// Invalid transaction ID
ESP_LOGE(TAG, "Invalid transaction ID 0x%08X while waiting, current transaction ID: 0x%08X",
awaiting_transaction_id, spi_transaction_id);
return ESP_ERR_INVALID_ARG;
}
SemaphoreGuard transaction_guard(spi_transaction_mutex_);
if (awaiting_transaction_id == 0) {
// wait for current transaction to complete
ESP_LOGV(TAG, "Waiting for current transaction 0x%08X to complete",
spi_transaction_id);
// take the mutex to ensure no transaction is active
if (!transaction_guard.take(timeout)) {
ESP_LOGE(TAG, "SPI transaction mutex timeout while waiting for transaction end");
return ESP_ERR_TIMEOUT;
}
}
// awaited_transaction_id is valid and matches current transaction ID or 0
out_transaction_guard = std::move(transaction_guard);
return ESP_OK;
}

View File

@@ -1,41 +0,0 @@
#pragma once
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "driver/spi_master.h"
#include "common/semaphore_guard.h"
#include <vector>
#include "display/transaction_guard.h"
class EPDHandler : public WithTransaction {
public:
EPDHandler();
~EPDHandler();
esp_err_t init();
esp_err_t epd_write_cmd(const uint8_t cmd, uint32_t transaction_id);
esp_err_t epd_write_data(const uint8_t data, uint32_t transaction_id);
esp_err_t epd_write_cmd_with_data(const uint8_t cmd, std::vector<uint8_t>& data, uint32_t transaction_id);
esp_err_t transfer_spi_data(const uint8_t* data, const size_t& length, uint32_t transaction_id, bool inverted = false);
bool is_busy(void) const;
void wait_for_idle(void) const;
private:
esp_err_t dangerous_epd_write_cmd_without_lock_(const uint8_t cmd);
esp_err_t dangerous_epd_write_data_without_lock_(const uint8_t data);
esp_err_t begin_transaction_(TickType_t timeout, uint32_t& out_id) override;
esp_err_t end_transaction_(void) override;
// given a transaction ID, wait for current transaction to complete. The transaction ID will determine if the wait is needed.
esp_err_t wait_for_transaction_end_(TickType_t timeout, uint32_t awaiting_transaction_id, SemaphoreGuard& out_transaction_guard);
spi_device_handle_t spi_ = nullptr;
SemaphoreHandle_t spi_mutex_ = nullptr;
SemaphoreHandle_t spi_transaction_mutex_ = nullptr;
uint32_t spi_transaction_id = 0; // For tracking SPI transactions
friend class TransactionGuard;
};

View File

@@ -11,7 +11,12 @@
LVGLHandler::LVGLHandler(
std::unique_ptr<EInkDisplayHandler> display_handler_in
) : display_handler_(std::move(display_handler_in)) { }
) : display_handler_(std::move(display_handler_in)) {
lvgl_mutex_ = xSemaphoreCreateMutex();
if (lvgl_mutex_ == nullptr) {
ESP_LOGE(TAG, "Failed to create LVGL mutex");
}
}
LVGLHandler::~LVGLHandler() {
if (lvgl_display_ != nullptr) {
@@ -26,9 +31,13 @@ LVGLHandler::~LVGLHandler() {
lv_draw_buf_destroy(lvgl_draw_buf_);
lvgl_draw_buf_ = nullptr;
}
if (lvgl_draw_buf_2_ != nullptr) {
lv_draw_buf_destroy(lvgl_draw_buf_2_);
lvgl_draw_buf_2_ = nullptr;
if (framebuffer_ != nullptr) {
heap_caps_free(framebuffer_);
framebuffer_ = nullptr;
}
if (lvgl_mutex_ != nullptr) {
vSemaphoreDelete(lvgl_mutex_);
lvgl_mutex_ = nullptr;
}
}
@@ -94,63 +103,101 @@ void LVGLHandler::flush_cb_(lv_display_t* disp, const lv_area_t* area, uint8_t*
return;
}
LVGLHandler* handler = static_cast<LVGLHandler*>(lv_display_get_user_data(disp));
if (handler == nullptr || handler->display_handler_ == nullptr) {
ESP_LOGE(TAG, "Invalid handler in flush callback");
if (handler == nullptr || handler->display_handler_ == nullptr || handler->framebuffer_ == nullptr) {
ESP_LOGE(TAG, "Invalid handler or framebuffer in flush callback");
lv_display_flush_ready(disp);
return;
}
uint8_t* pixel_data = px_map + 8; // Skip palette
//
ESP_LOGI(TAG, "Flush callback: x1=%d, y1=%d, x2=%d, y2=%d", area->x1, area->y1, area->x2, area->y2);
// take mutex
SemaphoreGuard guard(handler->lvgl_mutex_);
if (!guard.take(pdMS_TO_TICKS(5000))) {
ESP_LOGE(TAG, "LVGL mutex timeout in flush callback");
lv_display_flush_ready(disp);
return;
}
// copy data to framebuffer
int32_t area_w = lv_area_get_width(area);
int32_t area_h = lv_area_get_height(area);
if (area->x1 == 0 && area->y1 == 0 && area_w == DISPLAY_WIDTH && area_h == DISPLAY_HEIGHT) {
// revert the pixel data for e-ink (LVGL: 1=white, 0=black; E-Ink: 1=black, 0=white)
for (size_t i = 0; i < DISPLAY_BUFFER_SIZE; ++i) {
pixel_data[i] = ~pixel_data[i];
// Check if content actually changed before triggering expensive e-ink refresh
if (memcmp(handler->framebuffer_, pixel_data, DISPLAY_BUFFER_SIZE) == 0) {
ESP_LOGD(TAG, "Full screen flush with no changes - skipping e-ink refresh");
lv_display_flush_ready(disp);
return;
}
esp_err_t err = handler->display_handler_->full_write(
pixel_data,
true // white basemap
);
ESP_LOGI(TAG, "Full screen update");
memcpy(handler->framebuffer_, pixel_data, DISPLAY_BUFFER_SIZE);
// invert the framebuffer for e-ink display
for (size_t i = 0; i < DISPLAY_BUFFER_SIZE; ++i) {
handler->framebuffer_[i] = ~handler->framebuffer_[i];
}
// request full refresh
esp_err_t err = handler->display_handler_->full_write(handler->framebuffer_, true);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Full refresh request failed: %s", esp_err_to_name(err));
}
} else {
// partial update
ESP_LOGI(TAG, "Partial update: x1=%d, y1=%d, w=%d, h=%d", area->x1, area->y1, area_w, area_h);
// Prepare partial buffer
const uint32_t area_width_bytes = (area->x2 - area->x1 + 1) / 8;
const uint32_t area_height = area->y2 - area->y1 + 1;
const size_t partial_buffer_size = area_width_bytes * area_height;
uint8_t* partial_buffer = new uint8_t[partial_buffer_size];
if (partial_buffer == nullptr) {
ESP_LOGE(TAG, "Failed to allocate partial buffer for flush callback");
lv_display_flush_ready(disp);
return;
}
// Copy pixel data to partial buffer and invert for e-ink
for (int32_t row = 0; row < area_height; ++row) {
for (int32_t col = 0; col < area_width_bytes; ++col) {
size_t src_index = row * area_width_bytes + col;
partial_buffer[src_index] = ~pixel_data[src_index];
// update the framebuffer with the partial data
for (int32_t row = 0; row < area_h; ++row) {
int32_t fb_y = area->y1 + row;
int32_t fb_x_byte_start = area->x1 / 8;
int32_t fb_x_byte_end = area->x2 / 8;
uint8_t* fb_ptr = &handler->framebuffer_[fb_y * (DISPLAY_WIDTH / 8) + fb_x_byte_start];
const uint8_t* src_ptr = &pixel_data[row * (area_w / 8)];
// invert the partial framebuffer data for e-ink display
for (int32_t i = 0; i < (fb_x_byte_end - fb_x_byte_start + 1); ++i) {
fb_ptr[i] = ~src_ptr[i];
}
}
// update the refresh area
handler->refresh_area_.expand_to_include(area->x1, area->y1, area->x2, area->y2);
//
esp_err_t err = handler->display_handler_->partial_refresh(partial_buffer,
RefreshArea {
area->x1,
area->y1,
area->x2,
area->y2
}, lv_display_flush_is_last(disp));
delete[] partial_buffer;
if (lv_display_flush_is_last(disp) && !handler->refresh_area_.is_empty()) {
ESP_LOGI(TAG, "Last flush in batch - performing partial refresh");
ESP_LOGI(TAG, "Refresh area: x1=%d, y1=%d, x2=%d, y2=%d",
handler->refresh_area_.x1, handler->refresh_area_.y1,
handler->refresh_area_.x2, handler->refresh_area_.y2);
// copy the area to refresh
uint8_t* partial_buffer = new uint8_t[handler->refresh_area_.area() / 8];
if (partial_buffer == nullptr) {
ESP_LOGE(TAG, "Failed to allocate partial buffer for refresh");
lv_display_flush_ready(disp);
return;
}
// loop the refresh area and copy data
uint32_t x1 = handler->refresh_area_.x1;
uint32_t x2 = handler->refresh_area_.x2;
uint32_t y1 = handler->refresh_area_.y1;
uint32_t y2 = handler->refresh_area_.y2;
uint32_t height = y2 - y1 + 1;
uint32_t width = x2 - x1 + 1;
if (err != ESP_OK) {
ESP_LOGE(TAG, "Partial refresh request failed: %s", esp_err_to_name(err));
for (uint32_t row = 0; row < height; ++row) {
uint32_t fb_y = y1 + row;
uint32_t fb_x_byte_start = x1 / 8;
uint32_t fb_x_byte_end = x2 / 8;
uint8_t* fb_ptr = &handler->framebuffer_[fb_y * (DISPLAY_WIDTH / 8) + fb_x_byte_start];
uint8_t* dest_ptr = &partial_buffer[row * (width / 8)];
for (uint32_t i = 0; i < (fb_x_byte_end - fb_x_byte_start + 1); ++i) {
dest_ptr[i] = ~fb_ptr[i];
}
}
esp_err_t err = handler->display_handler_->partial_refresh(partial_buffer,
handler->refresh_area_);
delete[] partial_buffer;
if (err != ESP_OK) {
ESP_LOGE(TAG, "Partial refresh request failed: %s", esp_err_to_name(err));
}
handler->refresh_area_.reset();
}
}
//
@@ -187,7 +234,7 @@ void LVGLHandler::touch_read_cb_(lv_indev_t* indev, lv_indev_data_t* data) {
esp_lcd_touch_get_data(tp_handle, point_data, &touch_cnt, 1);
if (touch_cnt > 0) {
// ESP_LOGI(TAG, "Touch data read successfully: x=%d, y=%d", point_data[0].x, point_data[0].y);
ESP_LOGI(TAG, "Touch data read successfully: x=%d, y=%d", point_data[0].x, point_data[0].y);
data->point.x = point_data[0].x;
data->point.y = point_data[0].y;
data->state = LV_INDEV_STATE_PRESSED;
@@ -221,31 +268,38 @@ esp_err_t LVGLHandler::initLVGLDisplay_() {
return ESP_FAIL;
}
// Create two draw buffers for double buffering to improve performance
// set framebuffer
framebuffer_ = (uint8_t*)heap_caps_malloc(LVGL_BUFFER_SIZE, MALLOC_CAP_SPIRAM);
if (framebuffer_ != nullptr) {
framebuffer_in_psram_ = true;
ESP_LOGI(TAG, "Framebuffer allocated in PSRAM (%zu bytes)", LVGL_BUFFER_SIZE);
} else {
ESP_LOGW(TAG, "PSRAM not available, allocating framebuffer in internal RAM");
framebuffer_ = (uint8_t*)heap_caps_malloc(LVGL_BUFFER_SIZE, MALLOC_CAP_INTERNAL);
framebuffer_in_psram_ = false;
if (framebuffer_ == nullptr) {
ESP_LOGE(TAG, "Failed to allocate framebuffer");
lv_display_delete(lvgl_display_);
lvgl_display_ = nullptr;
lvgl_port_unlock();
return ESP_FAIL;
}
ESP_LOGI(TAG, "Framebuffer allocated in internal RAM (%zu bytes)", LVGL_BUFFER_SIZE);
}
memset(framebuffer_, 0xFF, LVGL_BUFFER_SIZE); // Initialize to white
// Create a draw buffer covering the entire display
lvgl_draw_buf_ = lv_draw_buf_create(DISPLAY_WIDTH, DISPLAY_HEIGHT, LV_COLOR_FORMAT_I1, LV_STRIDE_AUTO);
if (lvgl_draw_buf_ == nullptr) {
ESP_LOGE(TAG, "Failed to create LVGL draw buffer 1");
ESP_LOGE(TAG, "Failed to create LVGL draw buffer");
heap_caps_free(framebuffer_);
framebuffer_ = nullptr;
lv_display_delete(lvgl_display_);
lvgl_display_ = nullptr;
lvgl_port_unlock();
return ESP_FAIL;
}
lvgl_draw_buf_2_ = lv_draw_buf_create(DISPLAY_WIDTH, DISPLAY_HEIGHT, LV_COLOR_FORMAT_I1, LV_STRIDE_AUTO);
if (lvgl_draw_buf_2_ == nullptr) {
ESP_LOGE(TAG, "Failed to create LVGL draw buffer 2");
lv_draw_buf_destroy(lvgl_draw_buf_);
lvgl_draw_buf_ = nullptr;
lv_display_delete(lvgl_display_);
lvgl_display_ = nullptr;
lvgl_port_unlock();
return ESP_FAIL;
}
// Set both buffers for double buffering
lv_display_set_draw_buffers(lvgl_display_, lvgl_draw_buf_, lvgl_draw_buf_2_);
lv_display_set_draw_buffers(lvgl_display_, lvgl_draw_buf_, nullptr);
lv_display_set_render_mode(lvgl_display_, LV_DISPLAY_RENDER_MODE);
//
// Configure LVGL display
lv_display_set_color_format(lvgl_display_, LV_COLOR_FORMAT_I1);

View File

@@ -31,5 +31,10 @@ private:
lv_display_t* lvgl_display_ = nullptr;
lv_indev_t* lvgl_touch_indev_ = nullptr;
lv_draw_buf_t* lvgl_draw_buf_ = nullptr;
lv_draw_buf_t* lvgl_draw_buf_2_ = nullptr;
uint8_t* framebuffer_ = nullptr;
bool framebuffer_in_psram_ = false;
RefreshArea refresh_area_ = { 0, 0, 0, 0 };
SemaphoreHandle_t lvgl_mutex_ = nullptr;
};

View File

@@ -1,33 +0,0 @@
#pragma once
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include <esp_err.h>
#include <type_traits>
class WithTransaction {
protected:
virtual esp_err_t end_transaction_() = 0;
virtual esp_err_t begin_transaction_(TickType_t timeout, uint32_t& out_id) = 0;
friend class TransactionGuard;
};
class TransactionGuard {
public:
TransactionGuard(WithTransaction& handler, TickType_t timeout = portMAX_DELAY)
: handler_(handler) { }
~TransactionGuard() { if (transaction_id_) handler_.end_transaction_(); }
esp_err_t begin(TickType_t timeout = portMAX_DELAY) {
esp_err_t err = handler_.begin_transaction_(timeout, transaction_id_);
return err;
}
uint32_t transaction_id() const { return transaction_id_; }
bool is_active() const { return transaction_id_ != 0; }
private:
// delete copy constructor and assignment operator
TransactionGuard(const TransactionGuard&) = delete;
TransactionGuard& operator=(const TransactionGuard&) = delete;
WithTransaction& handler_;
uint32_t transaction_id_ = 0;
};

View File

@@ -18,4 +18,3 @@ dependencies:
espressif/esp_lcd_touch_gt911: ^1.2.0~1
espressif/esp_lvgl_port: ^2.7.0
espressif/cjson: ^1.7.19
joltwallet/littlefs: ==1.20.3

View File

@@ -23,8 +23,7 @@ void display_chip_info() {
(chip_info.features & CHIP_FEATURE_BLE) ? "BLE" : "",
(chip_info.features & CHIP_FEATURE_IEEE802154) ? ", 802.15.4 (Zigbee/Thread), " : "",
// psram
(chip_info.features & CHIP_FEATURE_EMB_PSRAM) ? "with embedded 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;
@@ -40,8 +39,5 @@ void display_chip_info() {
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());
// dma size
printf("DMA-capable memory size: %u bytes\n", heap_caps_get_free_size(MALLOC_CAP_DMA));
printf("DMA-capable internal memory size: %u bytes\n", heap_caps_get_free_size(MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL));
}

View File

@@ -1,552 +0,0 @@
#include "io/fs_handler.h"
#include <sys/stat.h>
#include <dirent.h>
#include <errno.h>
#include <cstring>
#include "esp_partition.h"
#define TAG "LittleFSHandler"
#define PARTITION_LABEL "storage"
#define BLOCK_SIZE 512 // Match typical flash sector size
//
// FSGuard implementation
//
FSGuard::FSGuard(LittleFSHandler* fs_handler, const std::string& relative_path, const char* flags)
: fs_handler_(fs_handler), file_(nullptr) {
if (fs_handler_ != nullptr) {
fs_handler_->open_file_(relative_path, flags, file_);
} else {
ESP_LOGE("FSGuard", "FSGuard initialized with null LittleFSHandler");
}
}
FSGuard::~FSGuard() {
this->close();
}
esp_err_t FSGuard::close() {
if (file_ != nullptr && fs_handler_ != nullptr) {
esp_err_t err = fs_handler_->close_file_(file_);
file_ = nullptr;
fs_handler_ = nullptr;
if (err != ESP_OK) {
ESP_LOGE("FSGuard", "Error closing file: %s", esp_err_to_name(err));
}
return err;
}
return ESP_OK;
}
//
// LittleFSHandler implementation
//
LittleFSHandler::LittleFSHandler() {
this->fs_mutex_ = xSemaphoreCreateMutex();
if (this->fs_mutex_ == nullptr) {
ESP_LOGE(TAG, "Failed to create filesystem mutex");
}
}
LittleFSHandler::~LittleFSHandler() {
if (this->is_initialized_()) {
esp_vfs_littlefs_unregister(PARTITION_LABEL);
this->initialized_ = false;
}
if (this->fs_mutex_ != nullptr) {
vSemaphoreDelete(this->fs_mutex_);
this->fs_mutex_ = nullptr;
}
}
esp_err_t LittleFSHandler::init(std::string base_path) {
// default config
esp_vfs_littlefs_conf_t config = {};
config.dont_mount = false;
config.partition_label = PARTITION_LABEL;
config.base_path = base_path.c_str();
config.format_if_mount_failed = true;
//
base_path_ = base_path;
return init(config);
}
esp_err_t LittleFSHandler::init(const esp_vfs_littlefs_conf_t& config) {
base_path_ = std::string(config.base_path);
if (this->is_initialized_()) {
ESP_LOGW(TAG, "LittleFS already initialized, skipping");
return ESP_OK;
}
esp_err_t err = esp_vfs_littlefs_register(&config);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize LittleFS: %s", esp_err_to_name(err));
if (err == ESP_ERR_NOT_FOUND) {
ESP_LOGE(TAG, "Listing all available partitions:");
esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL);
while (it != NULL) {
const esp_partition_t* part = esp_partition_get(it);
ESP_LOGE(TAG, " - Label: '%s', Type: 0x%02x, Subtype: 0x%02x, Address: 0x%08x, Size: 0x%08x",
part->label, part->type, part->subtype, part->address, part->size);
it = esp_partition_next(it);
}
esp_partition_iterator_release(it);
}
return ESP_ERR_INVALID_STATE;
}
this->initialized_ = true;
return ESP_OK;
}
std::string LittleFSHandler::get_base_path() const {
if (!this->is_initialized_()) {
ESP_LOGE(TAG, "LittleFS is not initialized, cannot get base path");
return "";
}
return base_path_;
}
std::string LittleFSHandler::get_full_path(const std::string& relative_path) const {
if (!this->is_initialized_()) {
ESP_LOGE(TAG, "LittleFS is not initialized, cannot get full path");
return "";
}
return base_path_ + "/" + relative_path;
}
esp_err_t LittleFSHandler::write_file(const std::string& relative_path, const uint8_t* data, size_t size, size_t& out_bytes_written) {
if (!this->is_initialized_()) {
ESP_LOGE(TAG, "LittleFS is not initialized");
return ESP_ERR_INVALID_STATE;
}
if (data == nullptr) {
ESP_LOGE(TAG, "Data pointer is null");
return ESP_ERR_INVALID_ARG;
}
SemaphoreGuard guard(this->fs_mutex_);
if (guard.take() != pdTRUE) {
ESP_LOGE(TAG, "Failed to acquire filesystem mutex");
return ESP_ERR_TIMEOUT;
}
// Try to open with r+b first to preserve existing content for comparison
FSGuard file_guard(this, relative_path, "r+b");
// If file doesn't exist, open with wb
if (!file_guard.is_open()) {
FSGuard new_file_guard(this, relative_path, "wb");
if (!new_file_guard.is_open()) {
return ESP_ERR_NOT_FOUND;
}
return this->write_if_different_(new_file_guard.get_file(), data, size, out_bytes_written);
}
return this->write_if_different_(file_guard.get_file(), data, size, out_bytes_written);
}
// Caller is responsible for opening the file in appropriate mode
// If the file doesn't exist, use write_file with "wb" mode
// If the file exists, use "r+b" mode to read and write
esp_err_t LittleFSHandler::write_file(FILE* file, const uint8_t* data, size_t size, size_t& out_bytes_written) {
return this->write_if_different_(file, data, size, out_bytes_written);
}
esp_err_t LittleFSHandler::append_file(const std::string& relative_path, const uint8_t* data, size_t size, size_t& out_bytes_written) {
if (!this->is_initialized_()) {
ESP_LOGE(TAG, "LittleFS is not initialized");
return ESP_ERR_INVALID_STATE;
}
if (data == nullptr) {
ESP_LOGE(TAG, "Data pointer is null");
return ESP_ERR_INVALID_ARG;
}
SemaphoreGuard guard(this->fs_mutex_);
if (guard.take() != pdTRUE) {
ESP_LOGE(TAG, "Failed to acquire filesystem mutex");
return ESP_ERR_TIMEOUT;
}
FSGuard file_guard(this, relative_path, "ab");
FILE* file = file_guard.get_file();
if (file == nullptr) {
return ESP_ERR_NOT_FOUND;
}
return this->append_file(file, data, size, out_bytes_written);
}
esp_err_t LittleFSHandler::append_file(FILE* file, const uint8_t* data, size_t size, size_t& out_bytes_written) {
if (file == nullptr) {
ESP_LOGE(TAG, "File pointer is null");
return ESP_ERR_INVALID_ARG;
}
if (data == nullptr) {
ESP_LOGE(TAG, "Data pointer is null");
return ESP_ERR_INVALID_ARG;
}
if (fseek(file, 0, SEEK_END) != 0) {
ESP_LOGE(TAG, "Failed to seek to end of file");
return ESP_ERR_INVALID_STATE;
}
// write data with POSIX
size_t bytes_written = fwrite(data, 1, size, file);
if (bytes_written != size) {
ESP_LOGE(TAG, "Failed to write all data to file, expected %zu bytes, wrote %zu bytes", size, bytes_written);
return ESP_ERR_NO_MEM;
}
if (fflush(file) != 0) {
ESP_LOGE(TAG, "Failed to flush file");
return ESP_ERR_INVALID_STATE;
}
out_bytes_written = bytes_written;
return ESP_OK;
}
esp_err_t LittleFSHandler::read_file(const std::string& relative_path, const size_t max_size, uint8_t* out_data, size_t& out_size) {
if (!this->is_initialized_()) {
ESP_LOGE(TAG, "LittleFS is not initialized");
return ESP_ERR_INVALID_STATE;
}
if (out_data == nullptr) {
ESP_LOGE(TAG, "Output data pointer is null");
return ESP_ERR_INVALID_ARG;
}
SemaphoreGuard guard(this->fs_mutex_);
if (guard.take() != pdTRUE) {
ESP_LOGE(TAG, "Failed to acquire filesystem mutex");
return ESP_ERR_TIMEOUT;
}
FSGuard file_guard(this, relative_path, "rb");
FILE* file = file_guard.get_file();
if (file == nullptr) {
return ESP_ERR_NOT_FOUND;
}
return this->read_file(file, max_size, out_data, out_size);
}
esp_err_t LittleFSHandler::read_file(FILE* file, const size_t max_size, uint8_t* out_data, size_t& out_size) {
if (file == nullptr) {
ESP_LOGE(TAG, "File pointer is null");
return ESP_ERR_INVALID_ARG;
}
if (out_data == nullptr) {
ESP_LOGE(TAG, "Output data pointer is null");
return ESP_ERR_INVALID_ARG;
}
size_t bytes_read = fread(out_data, 1, max_size, file);
if (bytes_read == 0 && ferror(file)) {
ESP_LOGE(TAG, "Failed to read from file");
return ESP_ERR_INVALID_STATE;
}
out_size = bytes_read;
return ESP_OK;
}
esp_err_t LittleFSHandler::delete_file(const std::string& relative_path) {
if (!this->is_initialized_()) {
ESP_LOGE(TAG, "LittleFS is not initialized");
return ESP_ERR_INVALID_STATE;
}
SemaphoreGuard guard(this->fs_mutex_);
if (guard.take() != pdTRUE) {
ESP_LOGE(TAG, "Failed to acquire filesystem mutex");
return ESP_ERR_TIMEOUT;
}
std::string full_path = this->get_full_path(relative_path);
if (remove(full_path.c_str()) != 0) {
ESP_LOGE(TAG, "Failed to delete file %s: %s", full_path.c_str(), strerror(errno));
return ESP_ERR_NOT_FOUND;
}
return ESP_OK;
}
bool LittleFSHandler::file_exists(const std::string& relative_path) {
if (!this->is_initialized_()) {
ESP_LOGE(TAG, "LittleFS is not initialized");
return false;
}
SemaphoreGuard guard(this->fs_mutex_);
if (guard.take() != pdTRUE) {
ESP_LOGE(TAG, "Failed to acquire filesystem mutex");
return false;
}
std::string full_path = this->get_full_path(relative_path);
struct stat st;
return (stat(full_path.c_str(), &st) == 0 && S_ISREG(st.st_mode));
}
esp_err_t LittleFSHandler::get_file_size(const std::string& relative_path, size_t& out_size) {
if (!this->is_initialized_()) {
ESP_LOGE(TAG, "LittleFS is not initialized");
return ESP_ERR_INVALID_STATE;
}
SemaphoreGuard guard(this->fs_mutex_);
if (guard.take() != pdTRUE) {
ESP_LOGE(TAG, "Failed to acquire filesystem mutex");
return ESP_ERR_TIMEOUT;
}
std::string full_path = this->get_full_path(relative_path);
struct stat st;
if (stat(full_path.c_str(), &st) != 0) {
ESP_LOGE(TAG, "Failed to stat file %s", full_path.c_str());
return ESP_ERR_NOT_FOUND;
}
out_size = st.st_size;
return ESP_OK;
}
esp_err_t LittleFSHandler::create_directory(const std::string& relative_path) {
if (!this->is_initialized_()) {
ESP_LOGE(TAG, "LittleFS is not initialized");
return ESP_ERR_INVALID_STATE;
}
SemaphoreGuard guard(this->fs_mutex_);
if (guard.take() != pdTRUE) {
ESP_LOGE(TAG, "Failed to acquire filesystem mutex");
return ESP_ERR_TIMEOUT;
}
std::string full_path = this->get_full_path(relative_path);
if (mkdir(full_path.c_str(), 0755) != 0) {
if (errno == EEXIST) {
ESP_LOGW(TAG, "Directory %s already exists", full_path.c_str());
return ESP_OK;
}
ESP_LOGE(TAG, "Failed to create directory %s: %s", full_path.c_str(), strerror(errno));
return ESP_ERR_INVALID_STATE;
}
return ESP_OK;
}
esp_err_t LittleFSHandler::list_directory(const std::string& relative_path, std::vector<std::string>& out_entries) {
if (!this->is_initialized_()) {
ESP_LOGE(TAG, "LittleFS is not initialized");
return ESP_ERR_INVALID_STATE;
}
SemaphoreGuard guard(this->fs_mutex_);
if (guard.take() != pdTRUE) {
ESP_LOGE(TAG, "Failed to acquire filesystem mutex");
return ESP_ERR_TIMEOUT;
}
std::string full_path = this->get_full_path(relative_path);
DIR* dir = opendir(full_path.c_str());
if (dir == nullptr) {
ESP_LOGE(TAG, "Failed to open directory %s", full_path.c_str());
return ESP_ERR_NOT_FOUND;
}
out_entries.clear();
struct dirent* entry;
while ((entry = readdir(dir)) != nullptr) {
// Skip . and ..
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
out_entries.push_back(entry->d_name);
}
closedir(dir);
return ESP_OK;
}
//
// Private methods
//
esp_err_t LittleFSHandler::open_file_(const std::string& relative_path, const char* flags, FILE*& out_file) {
if (!this->is_initialized_()) {
ESP_LOGE(TAG, "LittleFS is not initialized, cannot open file");
return ESP_ERR_INVALID_STATE;
}
std::string full_path = this->get_full_path(relative_path);
FILE* file = fopen(full_path.c_str(), flags);
if (file == nullptr) {
// Use debug level if file doesn't exist (ENOENT), warning level for other errors
if (errno == ENOENT) {
ESP_LOGD(TAG, "File %s does not exist (flags %s)", full_path.c_str(), flags);
} else {
ESP_LOGW(TAG, "Failed to open file %s with flags %s: %s", full_path.c_str(), flags, strerror(errno));
}
return ESP_ERR_NOT_FOUND;
}
out_file = file;
return ESP_OK;
}
esp_err_t LittleFSHandler::close_file_(FILE* file) {
if (file == nullptr) {
return ESP_OK;
}
if (fclose(file) != 0) {
ESP_LOGE(TAG, "Failed to close file: %s", strerror(errno));
return ESP_ERR_INVALID_STATE;
}
return ESP_OK;
}
esp_err_t LittleFSHandler::write_if_different_(FILE* file, const uint8_t* data, size_t size) {
size_t out_bytes_written = 0;
return this->write_if_different_(file, data, size, out_bytes_written);
}
esp_err_t LittleFSHandler::write_if_different_(FILE* file, const uint8_t* data, size_t size, size_t& out_bytes_written) {
if (file == nullptr || data == nullptr) {
ESP_LOGE(TAG, "Invalid parameters");
return ESP_ERR_INVALID_ARG;
}
// Get existing file size
if (fseek(file, 0, SEEK_END) != 0) {
ESP_LOGE(TAG, "Failed to seek to end of file");
return ESP_ERR_INVALID_STATE;
}
long file_size_long = ftell(file);
if (file_size_long < 0) {
ESP_LOGE(TAG, "Failed to get file size");
return ESP_ERR_INVALID_STATE;
}
size_t file_size = (size_t)file_size_long;
if (fseek(file, 0, SEEK_SET) != 0) {
ESP_LOGE(TAG, "Failed to seek to beginning of file");
return ESP_ERR_INVALID_STATE;
}
out_bytes_written = 0;
size_t compare_size = (file_size < size) ? file_size : size;
// Read entire file content for comparison
std::vector<uint8_t> existing_data;
if (file_size > 0) {
existing_data.resize(file_size);
size_t bytes_read = fread(existing_data.data(), 1, file_size, file);
if (bytes_read != file_size) {
ESP_LOGE(TAG, "Failed to read existing file data");
return ESP_ERR_INVALID_STATE;
}
}
// Compare and identify blocks that need updating
std::vector<bool> block_needs_update((size + BLOCK_SIZE - 1) / BLOCK_SIZE, false);
bool any_changes = false;
for (size_t offset = 0; offset < compare_size; offset += BLOCK_SIZE) {
size_t chunk_size = BLOCK_SIZE;
if (offset + chunk_size > compare_size) {
chunk_size = compare_size - offset;
}
if (memcmp(existing_data.data() + offset, data + offset, chunk_size) != 0) {
block_needs_update[offset / BLOCK_SIZE] = true;
any_changes = true;
}
}
// Check if size changed or there are additional blocks to write
if (size != file_size) {
any_changes = true;
}
if (!any_changes) {
ESP_LOGD(TAG, "File content unchanged, skipping write");
return ESP_OK;
}
// Seek to beginning to start writing
if (fseek(file, 0, SEEK_SET) != 0) {
ESP_LOGE(TAG, "Failed to seek to beginning for writing");
return ESP_ERR_INVALID_STATE;
}
// Write only changed blocks
for (size_t offset = 0; offset < compare_size; offset += BLOCK_SIZE) {
size_t block_index = offset / BLOCK_SIZE;
if (!block_needs_update[block_index]) {
// Skip unchanged block
if (fseek(file, offset + BLOCK_SIZE, SEEK_SET) != 0) {
// If at end of compare region, this is OK
if (offset + BLOCK_SIZE > compare_size) {
if (fseek(file, compare_size, SEEK_SET) != 0) {
ESP_LOGE(TAG, "Failed to seek past unchanged block");
return ESP_ERR_INVALID_STATE;
}
} else {
ESP_LOGE(TAG, "Failed to seek past unchanged block at %zu", offset);
return ESP_ERR_INVALID_STATE;
}
}
continue;
}
size_t chunk_size = BLOCK_SIZE;
if (offset + chunk_size > compare_size) {
chunk_size = compare_size - offset;
}
if (fseek(file, offset, SEEK_SET) != 0) {
ESP_LOGE(TAG, "Failed to seek to offset %zu", offset);
return ESP_ERR_INVALID_STATE;
}
size_t written = fwrite(data + offset, 1, chunk_size, file);
if (written != chunk_size) {
ESP_LOGE(TAG, "Failed to write block at offset %zu", offset);
return ESP_ERR_INVALID_STATE;
}
out_bytes_written += written;
}
// Handle size differences
if (size > file_size) {
// Write additional data beyond original file size
if (fseek(file, file_size, SEEK_SET) != 0) {
ESP_LOGE(TAG, "Failed to seek to end for appending");
return ESP_ERR_INVALID_STATE;
}
size_t written = fwrite(data + file_size, 1, size - file_size, file);
if (written != (size - file_size)) {
ESP_LOGE(TAG, "Failed to write additional data");
return ESP_ERR_INVALID_STATE;
}
out_bytes_written += written;
} else if (size < file_size) {
// Truncate file to new size
if (ftruncate(fileno(file), size) != 0) {
ESP_LOGE(TAG, "Failed to truncate file to size %zu", size);
return ESP_ERR_INVALID_STATE;
}
}
// Flush to ensure data is written to storage
if (fflush(file) != 0) {
ESP_LOGE(TAG, "Failed to flush file after write");
return ESP_ERR_INVALID_STATE;
}
return ESP_OK;
}
bool LittleFSHandler::is_initialized_() const {
return this->initialized_;
}

View File

@@ -1,95 +0,0 @@
#pragma once
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_littlefs.h"
#include "esp_err.h"
#include <string>
#include <vector>
#include <map>
#include <memory>
#include "esp_log.h"
#include <semaphore_guard.h>
// Forward declaration
class LittleFSHandler;
class FSGuard {
public:
FSGuard(LittleFSHandler* fs_handler, const std::string& relative_path, const char* flags);
~FSGuard();
esp_err_t close();
FILE* get_file() {
return file_;
}
bool is_open() const {
return file_ != nullptr;
}
private:
LittleFSHandler* fs_handler_ = nullptr;
FILE* file_;
// prevent copying and moving
FSGuard(const FSGuard&) = delete;
FSGuard& operator=(const FSGuard&) = delete;
FSGuard(FSGuard&& other) = delete;
FSGuard& operator=(FSGuard&& other) = delete;
};
//LittleFSHandler interface
// All paths are relative to the mounted filesystem root
// Implementations should handle initialization of the filesystem, and mounting if necessary
// When destroyed, implementations should unmount the filesystem if necessary
// All paths are relative to the mounted filesystem root, e.g. if mounted at /littlefs, and file is /data.txt, the full path is /littlefs/data.txt
// File operations use standard C FILE* wrapped in FSGuard for RAII
class LittleFSHandler {
public:
LittleFSHandler();
~LittleFSHandler();
esp_err_t init(std::string base_path);
esp_err_t init(const esp_vfs_littlefs_conf_t& config);
std::string get_base_path() const;
std::string get_full_path(const std::string& relative_path) const;
// File operations
esp_err_t write_file(const std::string& relative_path, const uint8_t* data, size_t size, size_t& out_bytes_written);
esp_err_t write_file(FILE* file, const uint8_t* data, size_t size, size_t& out_bytes_written);
//
esp_err_t append_file(const std::string& relative_path, const uint8_t* data, size_t size, size_t& out_bytes_written);
esp_err_t append_file(FILE* file, const uint8_t* data, size_t size, size_t& out_bytes_written);
//
esp_err_t read_file(const std::string& relative_path, const size_t max_size, uint8_t* out_data, size_t& out_size);
esp_err_t read_file(FILE* file, const size_t max_size, uint8_t* out_data, size_t& out_size);
//
esp_err_t delete_file(const std::string& relative_path);
//
bool file_exists(const std::string& relative_path);
esp_err_t get_file_size(const std::string& relative_path, size_t& out_size);
// Directory operations
esp_err_t create_directory(const std::string& relative_path);
esp_err_t list_directory(const std::string& relative_path, std::vector<std::string>& out_entries);
protected:
esp_err_t open_file_(const std::string& relative_path, const char* flags, FILE*& out_file);
esp_err_t close_file_(FILE* file);
// uses standard C FILE* for file operations
esp_err_t write_if_different_(FILE* file, const uint8_t* data, size_t size);
esp_err_t write_if_different_(FILE* file, const uint8_t* data, size_t size, size_t& out_bytes_written);
friend class FSGuard;
private:
//
bool is_initialized_() const;
SemaphoreHandle_t fs_mutex_ = nullptr;
bool initialized_ = false;
std::string base_path_;
};

View File

@@ -20,7 +20,6 @@ NVSStorageHandler::~NVSStorageHandler() {
void NVSStorageHandler::init(const EventGroupHandle_t& system_event_group) {
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_LOGW(TAG, "NVS Flash init failed with %s, erasing and retrying...", esp_err_to_name(err));
nvs_flash_erase();
err = nvs_flash_init();
}
@@ -44,26 +43,11 @@ void NVSStorageHandler::put(const std::string& key, const std::string& value) {
}
esp_err_t err = nvs_set_str(this->nvsHandle, key.c_str(), value.c_str());
if (err == ESP_ERR_NVS_NOT_ENOUGH_SPACE) {
ESP_LOGE(TAG, "NVS storage full! Cannot store key '%s'. Consider clearing old data.", key.c_str());
ESP_LOGI(TAG, "Attempting to erase and retry...");
// Try to commit pending changes first
nvs_commit(this->nvsHandle);
// Retry once
err = nvs_set_str(this->nvsHandle, key.c_str(), value.c_str());
if (err != ESP_OK) {
ESP_LOGE(TAG, "Retry failed: %s", esp_err_to_name(err));
return;
}
} else if (err != ESP_OK) {
ESP_LOGE(TAG, "Error (%s) setting key-value pair in NVS!", esp_err_to_name(err));
return;
}
// Commit successful write
err = nvs_commit(this->nvsHandle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error (%s) committing to NVS!", 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);
// ESP_LOGI(TAG, "Key-value pair (%s, %s) stored in NVS.", key.c_str(), value.c_str());
}
}

View File

@@ -8,19 +8,19 @@
#include "esp_flash.h"
#include "esp_system.h"
#include "esp_log.h"
#include "esp_event.h"
//
#include "common/constants.h"
#include "common/queue_defs.h"
#include "common/system_context.h"
#include "io/nvs_handler.h"
#include "io/fs_handler.h"
#include "info/info.h"
#include "display/eink_display_handler.h"
#include "display/lvgl_handler.h"
#include "ui/ui_handler.h"
#include "ui/apps/registry.h"
#include "ui/app_registry.h"
#include "ui/apps/shutdown_app.h"
#include "ui/apps/discord_app.h"
#include "ui/apps/mtr_app.h"
#include <tick/lv_tick.h>
#include "esp_lvgl_port.h"
#include "lvgl.h"
@@ -31,6 +31,7 @@
// nvs storage namespaces, 15 characters max
#define DEFAULT_STORAGE_NAMESPACE "storage"
#define WIFI_CREDENTIALS_STORAGE_NAMESPACE "wifi_cred"
#define TAG "Main"
extern "C" void app_main(void);
@@ -41,18 +42,224 @@ void init_queues(
EventGroupHandle_t& system_lifecycle_event_group
);
void EInk_Checkerboard(
EInkDisplayHandler* display_handler
) {
struct CheckerboardTaskParams {
EInkDisplayHandler* display_handler;
};
auto checkerboard_task_fn = [](void* pvParameters) {
CheckerboardTaskParams* params = static_cast<CheckerboardTaskParams*>(pvParameters);
if (params != nullptr && params->display_handler != nullptr) {
// Add this task to the watchdog timer
esp_err_t wdt_err = esp_task_wdt_add(NULL);
if (wdt_err != ESP_OK) {
ESP_LOGW(TAG, "Failed to add checkerboard task to watchdog: %s", esp_err_to_name(wdt_err));
}
EInkDisplayHandler* display_handler = params->display_handler;
const size_t DISPLAY_BUFFER_SIZE = DISPLAY_WIDTH * DISPLAY_HEIGHT / 8;
uint8_t* framebuffer = new uint8_t[DISPLAY_BUFFER_SIZE];
if (framebuffer == nullptr) {
ESP_LOGE(TAG, "Failed to allocate framebuffer for checkerboard task");
if (wdt_err == ESP_OK) {
esp_task_wdt_delete(NULL);
}
vTaskDelete(NULL);
return;
}
// Create checkerboard pattern
for (size_t y = 0; y < DISPLAY_HEIGHT; y++) {
for (size_t x = 0; x < DISPLAY_WIDTH; x++) {
size_t byte_index = (y * DISPLAY_WIDTH + x) / 8;
size_t bit_index = 7 - (x % 8);
bool is_white = ((x / 20) % 2) == ((y / 20) % 2);
if (is_white) {
framebuffer[byte_index] |= (1 << bit_index); // Set bit to 1 for white
} else {
framebuffer[byte_index] &= ~(1 << bit_index); // Clear bit to 0 for black
}
}
// Yield and reset watchdog periodically
const size_t YIELD_INTERVAL = 16;
if (y % YIELD_INTERVAL == 0) {
if (wdt_err == ESP_OK) {
esp_task_wdt_reset();
}
vTaskDelay(1 / portTICK_PERIOD_MS);
// partial refresh to show progress
int32_t y_start = static_cast<int32_t>((y >= YIELD_INTERVAL - 1) ? (y - (YIELD_INTERVAL - 1)) : 0);
int32_t y_end = static_cast<int32_t>(y);
// get the partial framebuffer for this area
uint8_t* partial_framebuffer = &framebuffer[y_start * (DISPLAY_WIDTH / 8)];
esp_err_t err = display_handler->partial_refresh(partial_framebuffer, RefreshArea { 0, y_start, DISPLAY_WIDTH - 1, y_end });
if (err != ESP_OK) {
ESP_LOGE(TAG, "Partial refresh failed at y=%d: %s", y, esp_err_to_name(err));
}
// wait for 4 seconds to prevent spamming the display
// vTaskDelay(2000 / portTICK_PERIOD_MS);
}
}
// Perform full write to display
// esp_err_t err = display_handler->full_write(framebuffer);
// if (err != ESP_OK) {
// ESP_LOGE(TAG, "Checkerboard full write failed: %s", esp_err_to_name(err));
// } else {
// ESP_LOGI(TAG, "Checkerboard pattern displayed successfully.");
// }
delete[] framebuffer;
// Remove task from watchdog before deletion
if (wdt_err == ESP_OK) {
esp_task_wdt_delete(NULL);
}
} else {
ESP_LOGE(TAG, "Invalid parameters for checkerboard task");
}
vTaskDelete(NULL);
};
CheckerboardTaskParams* checker_params = new CheckerboardTaskParams();
checker_params->display_handler = display_handler;
BaseType_t res = xTaskCreate(
checkerboard_task_fn,
"checkerboard_task",
8192,
static_cast<void*>(checker_params),
tskIDLE_PRIORITY + 1,
NULL
);
if (res != pdPASS) {
ESP_LOGE(TAG, "Failed to create checkerboard task");
delete checker_params;
}
}
void LVGL_Checkerboard(
LVGLHandler* lvgl_handler
) {
struct CheckerboardTaskParams {
LVGLHandler* lvgl_handler;
};
auto checkerboard_task_fn = [](void* pvParameters) {
CheckerboardTaskParams* params = static_cast<CheckerboardTaskParams*>(pvParameters);
if (params == nullptr || params->lvgl_handler == nullptr) {
ESP_LOGE(TAG, "Invalid parameters for LVGL checkerboard task");
delete params;
vTaskDelete(NULL);
return;
}
auto* handler = static_cast<LVGLHandler*>(params->lvgl_handler);
// Add safety checks
if (!handler) {
ESP_LOGE("LVGL", "Handler is null!");
delete params;
vTaskDelete(NULL);
return;
}
ESP_LOGI("HEAP", "Free: %d", esp_get_free_heap_size());
// Wait for LVGL system to fully initialize
vTaskDelay(pdMS_TO_TICKS(200));
// Acquire LVGL lock with proper timeout
if (!lvgl_port_lock(pdMS_TO_TICKS(5000))) {
ESP_LOGE(TAG, "Failed to acquire LVGL lock for checkerboard");
delete params;
vTaskDelete(NULL);
return;
}
// Verify LVGL is properly initialized
if (lv_display_get_default() == nullptr) {
ESP_LOGE(TAG, "LVGL default display not available");
lvgl_port_unlock();
delete params;
vTaskDelete(NULL);
return;
}
// Create LVGL objects for checkerboard
lv_obj_t* scr = lv_scr_act();
if (scr == nullptr) {
ESP_LOGE(TAG, "Failed to get active LVGL screen");
lvgl_port_unlock();
delete params;
vTaskDelete(NULL);
return;
}
lv_obj_t* checkerboard = lv_obj_create(scr);
if (checkerboard == nullptr) {
ESP_LOGE(TAG, "Failed to create LVGL checkerboard object");
lvgl_port_unlock();
delete params;
vTaskDelete(NULL);
return;
}
lv_obj_set_size(checkerboard, DISPLAY_WIDTH, DISPLAY_HEIGHT);
// remove border and padding
lv_obj_set_style_pad_all(checkerboard, 0, 0);
lv_obj_set_style_border_width(checkerboard, 0, 0);
const int CELL_SIZE = 40;
lvgl_port_unlock();
// Create checkerboard pattern using LVGL
for (int y = 0; y < DISPLAY_HEIGHT; y += CELL_SIZE) {
lvgl_port_lock(pdMS_TO_TICKS(1000));
for (int x = 0; x < DISPLAY_WIDTH; x += CELL_SIZE) {
lv_color_t color = (((x / CELL_SIZE) % 2) == ((y / CELL_SIZE) % 2)) ? lv_color_hex(0xFFFFFF) : lv_color_hex(0x000000);
lv_obj_t* cell = lv_obj_create(checkerboard);
if (cell == nullptr) {
ESP_LOGE(TAG, "Failed to create LVGL checkerboard cell");
lvgl_port_unlock();
continue;
}
lv_obj_set_size(cell, CELL_SIZE, CELL_SIZE);
lv_obj_set_style_bg_color(cell, color, 0);
lv_obj_set_pos(cell, x, y);
// remove border and padding
lv_obj_set_style_pad_all(cell, 0, 0);
lv_obj_set_style_border_width(cell, 0, 0);
lv_obj_t* label = lv_label_create(cell);
if (label != nullptr) {
lv_label_set_text_fmt(label, "(%d,%d)", x, y);
lv_obj_center(label);
}
}
lvgl_port_unlock();
// Yield to allow LVGL to process rendering
vTaskDelay(5000 / portTICK_PERIOD_MS);
}
ESP_LOGI(TAG, "LVGL Checkerboard pattern displayed successfully.");
delete params;
vTaskDelete(NULL);
};
CheckerboardTaskParams* checker_params = new CheckerboardTaskParams();
checker_params->lvgl_handler = lvgl_handler;
BaseType_t res = xTaskCreate(
checkerboard_task_fn,
"lvgl_checkerboard_task",
8192,
static_cast<void*>(checker_params),
tskIDLE_PRIORITY + 1,
NULL
);
if (res != pdPASS) {
ESP_LOGE(TAG, "Failed to create LVGL checkerboard task");
delete checker_params;
}
}
void app_main(void) {
display_chip_info();
// Initialize default event loop early - required for UI events
esp_err_t err = esp_event_loop_create_default();
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "Failed to create default event loop: %s", esp_err_to_name(err));
vTaskDelay(5000 / portTICK_PERIOD_MS);
return esp_restart();
}
ESP_LOGI(TAG, "Default event loop created.\n");
QueueHandle_t touch_event_queue = NULL;
EventGroupHandle_t system_event_group = NULL, system_lifecycle_event_group = NULL;
@@ -65,20 +272,14 @@ void app_main(void) {
ESP_LOGI(TAG, "Queues initialized.\n");
//
KVStorageHandler* kv_storage_handler = new NVSStorageHandler(
DEFAULT_STORAGE_NAMESPACE
);
// KVStorageHandler* kv_storage_handler = new NVSStorageHandler(
// DEFAULT_STORAGE_NAMESPACE
// );
auto fs_handler = std::make_shared<LittleFSHandler>();
esp_err_t fs_err = fs_handler->init("/littlefs");
if (fs_err != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize LittleFS: %s", esp_err_to_name(fs_err));
vTaskDelay(5000 / portTICK_PERIOD_MS);
return esp_restart();
}
auto wifi_handler = std::make_unique<WifiHandler>(fs_handler);
NetworkHandler* network_handler = new NetworkHandler(std::move(wifi_handler));
// auto wifi_handler = std::make_unique<WifiHandler>(
// std::unique_ptr<KVStorageHandler>(new NVSStorageHandler(WIFI_CREDENTIALS_STORAGE_NAMESPACE))
// );
// NetworkHandler* network_handler = new NetworkHandler(std::move(wifi_handler));
EInkDisplayHandler* display_handler = new EInkDisplayHandler();
// Initialize display and touch
// display_handler->init_devices(system_event_group);
@@ -87,7 +288,7 @@ void app_main(void) {
// LVGL Handler
std::unique_ptr<EInkDisplayHandler> display_uptr(display_handler);
LVGLHandler lvgl_handler(std::move(display_uptr));
err = lvgl_handler.initLVGL(system_event_group);
esp_err_t err = lvgl_handler.initLVGL(system_event_group);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize LVGL handler: %s", esp_err_to_name(err));
vTaskDelay(5000 / portTICK_PERIOD_MS);
@@ -95,11 +296,8 @@ void app_main(void) {
}
//
kv_storage_handler->init(system_event_group);
network_handler->init(system_event_group);
// Make network handler available to apps
SystemContext::instance().set_network_handler(network_handler);
// kv_storage_handler->init(system_event_group);
// network_handler->init(system_event_group);
//
ESP_LOGI(TAG, "Waiting for system to be ready...\n");
@@ -114,25 +312,40 @@ void app_main(void) {
);
ESP_LOGI(TAG, "System is ready. Starting main application...\n");
AppRegistry& app_registry = AppRegistry::instance();
err = app_registry.init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize App Registry: %s", esp_err_to_name(err));
vTaskDelay(5000 / portTICK_PERIOD_MS);
return esp_restart();
}
UIHandler ui_handler;
err = ui_handler.init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize UI handler: %s", esp_err_to_name(err));
vTaskDelay(5000 / portTICK_PERIOD_MS);
return esp_restart();
}
ESP_LOGI(TAG, "UI handler initialized.\n");
// Allow LVGL system to stabilize before creating objects
vTaskDelay(pdMS_TO_TICKS(100));
// Show checkerboard pattern on display for testing
// EInk_Checkerboard(display_handler);
LVGL_Checkerboard(&lvgl_handler);
// Register apps with AppRegistry by creating their descriptors
// Each descriptor will create and register the app instance
// DemoAppDescriptor* demo_descriptor = new DemoAppDescriptor();
// ShutdownAppDescriptor* shutdown_descriptor = new ShutdownAppDescriptor();
// DiscordAppDescriptor::instance(); // Use singleton pattern for Discord app
// MtrAppDescriptor* mtr_descriptor = new MtrAppDescriptor();
// Pass network handler to MtrApp so it can fetch arrival data
// MtrApp* mtr_app = dynamic_cast<MtrApp*>(mtr_descriptor->get_app_instance());
// if (mtr_app) {
// mtr_app->set_network_handler(network_handler);
// }
// 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(

View File

@@ -19,7 +19,7 @@ UDPClient::~UDPClient() {
close();
}
esp_err_t UDPClient::init(uint16_t local_port) {
esp_err_t UDPClient::init() {
if (initialized_) {
ESP_LOGW(TAG, "Already initialized");
return ESP_OK;
@@ -31,23 +31,6 @@ esp_err_t UDPClient::init(uint16_t local_port) {
return ESP_FAIL;
}
// Bind to local port if specified
if (local_port > 0) {
struct sockaddr_in local_addr;
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sin_family = AF_INET;
local_addr.sin_addr.s_addr = htonl(INADDR_ANY);
local_addr.sin_port = htons(local_port);
if (bind(sock_fd_, (struct sockaddr*)&local_addr, sizeof(local_addr)) < 0) {
ESP_LOGE(TAG, "Failed to bind to port %u: errno %d", local_port, errno);
::close(sock_fd_);
sock_fd_ = -1;
return ESP_FAIL;
}
ESP_LOGI(TAG, "Bound to local port %u", local_port);
}
// Set socket to non-blocking mode
esp_err_t err = set_nonblocking();
if (err != ESP_OK) {
@@ -112,7 +95,7 @@ esp_err_t UDPClient::send_command(const std::string& command) {
}
ssize_t sent = sendto(sock_fd_, command.c_str(), command.length(), 0,
(struct sockaddr*)&remote_addr_, sizeof(remote_addr_));
(struct sockaddr*)&remote_addr_, sizeof(remote_addr_));
if (sent < 0) {
ESP_LOGE(TAG, "Failed to send command '%s': errno %d", command.c_str(), errno);
@@ -161,7 +144,7 @@ esp_err_t UDPClient::receive_response(std::string& response, int timeout_ms) {
socklen_t from_len = sizeof(from_addr);
ssize_t received = recvfrom(sock_fd_, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr*)&from_addr, &from_len);
(struct sockaddr*)&from_addr, &from_len);
if (received < 0) {
ESP_LOGE(TAG, "recvfrom() failed: errno %d", errno);

View File

@@ -19,10 +19,9 @@ public:
/**
* @brief Initialize UDP socket
* @param local_port Local port to bind to (0 = system assigns port)
* @return ESP_OK on success, error code otherwise
*/
esp_err_t init(uint16_t local_port = 0);
esp_err_t init();
/**
* @brief Configure remote endpoint

View File

@@ -1,113 +0,0 @@
#include "web_server_handler.h"
#include "esp_log.h"
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
static const char* TAG = "WebServerHandler";
WebServerHandler::WebServerHandler() { }
WebServerHandler::~WebServerHandler() {
stop();
}
uint16_t WebServerHandler::start(const std::string& auth_key, uint16_t base_port) {
if (server_ != nullptr) {
ESP_LOGW(TAG, "Server already running on port %d", current_port_);
return current_port_;
}
auth_key_ = auth_key;
// Try to find a free port
uint16_t port = base_port;
const uint16_t max_attempts = 100;
for (uint16_t attempt = 0; attempt < max_attempts; attempt++) {
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = port;
config.ctrl_port = port + 1000; // Control port
config.max_open_sockets = 7;
config.lru_purge_enable = true;
esp_err_t err = httpd_start(&server_, &config);
if (err == ESP_OK) {
current_port_ = port;
ESP_LOGI(TAG, "Web server started successfully on port %d", current_port_);
return current_port_;
} else if (err == ESP_ERR_HTTPD_ALLOC_MEM) {
ESP_LOGE(TAG, "Failed to allocate memory for web server");
return 0;
} else {
// Port likely in use, try next port
ESP_LOGD(TAG, "Port %d in use, trying next port", port);
port++;
}
}
ESP_LOGE(TAG, "Failed to find free port after %d attempts", max_attempts);
return 0;
}
esp_err_t WebServerHandler::stop() {
if (server_ == nullptr) {
return ESP_OK;
}
esp_err_t err = httpd_stop(server_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to stop web server: %s", esp_err_to_name(err));
return err;
}
server_ = nullptr;
current_port_ = 0;
auth_key_.clear();
ESP_LOGI(TAG, "Web server stopped");
return ESP_OK;
}
esp_err_t WebServerHandler::register_uri_handler(const httpd_uri_t* uri_handler) {
if (server_ == nullptr) {
ESP_LOGE(TAG, "Server not running, cannot register URI handler");
return ESP_ERR_INVALID_STATE;
}
esp_err_t err = httpd_register_uri_handler(server_, uri_handler);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to register URI handler: %s", esp_err_to_name(err));
return err;
}
return ESP_OK;
}
bool WebServerHandler::validate_auth(const char* query_string) const {
if (!query_string || auth_key_.empty()) {
return false;
}
// Look for "auth=<key>" in query string
const char* auth_param = strstr(query_string, "auth=");
if (!auth_param) {
return false;
}
// Skip "auth="
auth_param += 5;
// Find end of auth value (& or end of string)
const char* end = strchr(auth_param, '&');
size_t auth_len = end ? (size_t)(end - auth_param) : strlen(auth_param);
// Compare with stored auth key
if (auth_len != auth_key_.length()) {
return false;
}
return strncmp(auth_param, auth_key_.c_str(), auth_len) == 0;
}

View File

@@ -1,42 +0,0 @@
#pragma once
#include "esp_http_server.h"
#include "esp_err.h"
#include <string>
#include <functional>
class WebServerHandler {
public:
WebServerHandler();
~WebServerHandler();
// Start web server, finds a free port starting from base_port
// Returns the actual port used, or 0 on failure
uint16_t start(const std::string& auth_key, uint16_t base_port = 8080);
// Stop web server
esp_err_t stop();
// Check if server is running
bool is_running() const { return server_ != nullptr; }
// Get the current port
uint16_t get_port() const { return current_port_; }
// Get the auth key
std::string get_auth_key() const { return auth_key_; }
// Register a URI handler
esp_err_t register_uri_handler(const httpd_uri_t* uri_handler);
// Validate auth key from query string
bool validate_auth(const char* query_string) const;
private:
// Prevent copying
WebServerHandler(const WebServerHandler&) = delete;
WebServerHandler& operator=(const WebServerHandler&) = delete;
httpd_handle_t server_ = nullptr;
uint16_t current_port_ = 0;
std::string auth_key_;
};

View File

@@ -8,27 +8,16 @@
#include "common/semaphore_guard.h"
#include "cJSON.h"
#define TAG "WifiHandler"
#define WIFI_CRED_FILE_PATH "wifi_credentials.json"
/*
* WiFi Credentials JSON Structure:
* {
* "current_ssid": "MyWiFi",
* "credentials": {
* "MyWiFi": {
* "password": "mypassword123"
* },
* "OtherNetwork": {
* "password": "otherpass456"
* }
* }
* }
*/
static const char* TAG = "WifiHandler";
static const char* WIFI_SSID_KEY = "ssid";
static const char* WIFI_PASSWORD_STORE_KEY = "psw";
WifiHandler::WifiHandler(
std::shared_ptr<LittleFSHandler> fs_handler_
) : fs_handler_(std::move(fs_handler_)) {
// 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
std::unique_ptr<KVStorageHandler> 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");
@@ -45,13 +34,10 @@ WifiHandler::WifiHandler(
if (!this->credential_mutex) {
ESP_LOGE(TAG, "Failed to create credential mutex");
}
if (this->fs_handler_ == nullptr) {
ESP_LOGW(TAG, "FSHandler is null, WiFi credentials will not be stored");
if (this->kvs == nullptr) {
ESP_LOGW(TAG, "KVStorageHandler is null, WiFi credentials will not be stored");
} else {
esp_err_t err = this->fs_handler_->init("/littlefs");
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize FSHandler: %s", esp_err_to_name(err));
}
this->kvs->init(nullptr);
}
}
@@ -69,8 +55,8 @@ WifiHandler::~WifiHandler() {
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;
//
this->fs_handler_ = nullptr;
// unique_ptr will automatically delete the object
this->kvs = nullptr;
}
}
@@ -174,7 +160,6 @@ esp_err_t WifiHandler::connect(const std::string& ssid, const std::string& passw
this->current_ssid.clear();
}
this->current_ssid = ssid;
this->current_password = password;
//
wifi_config_t wifi_config = {};
@@ -197,8 +182,8 @@ esp_err_t WifiHandler::connect(const std::string& ssid, const std::string& passw
return err;
}
// Note: Credentials will be stored in the event handler after successful connection
// to avoid storing credentials for failed connection attempts
// store credentials after successful connection attempt
this->store_wifi_credentials(this->current_ssid, password);
return ESP_OK;
}
@@ -320,10 +305,6 @@ void WifiHandler::wifi_event_handler(void* arg, esp_event_base_t event_base, int
self->s_wifi_event_group,
WIFI_CONNECTED_BIT
);
// Store credentials only after successful connection
if (!self->current_ssid.empty() && !self->current_password.empty()) {
self->store_wifi_credentials(self->current_ssid, self->current_password);
}
break;
}
default:
@@ -337,8 +318,8 @@ void WifiHandler::wifi_event_handler(void* arg, esp_event_base_t event_base, int
//
void WifiHandler::store_wifi_credentials(const std::string& ssid, const std::string& password) {
if (!fs_handler_) {
ESP_LOGW(TAG, "FSHandler not set, cannot store WiFi credentials");
if (!kvs) {
ESP_LOGW(TAG, "KVStorageHandler not set, cannot store WiFi credentials");
return;
}
SemaphoreGuard guard(this->credential_mutex);
@@ -347,187 +328,95 @@ void WifiHandler::store_wifi_credentials(const std::string& ssid, const std::str
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;
// Try to read existing credentials file
if (fs_handler_->file_exists(WIFI_CRED_FILE_PATH)) {
// Read existing file
size_t file_size = 0;
esp_err_t err = fs_handler_->get_file_size(WIFI_CRED_FILE_PATH, file_size);
if (err == ESP_OK && file_size > 0) {
std::vector<uint8_t> file_data(file_size + 1); // +1 for null terminator
size_t bytes_read = 0;
err = fs_handler_->read_file(WIFI_CRED_FILE_PATH, file_size, file_data.data(), bytes_read);
if (err == ESP_OK) {
file_data[bytes_read] = '\0'; // Null terminate
json = cJSON_Parse(reinterpret_cast<const char*>(file_data.data()));
if (json == nullptr) {
ESP_LOGE(TAG, "Failed to parse existing WiFi credentials JSON, creating new");
}
}
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();
}
}
// Create new JSON if parsing failed or file doesn't exist
if (json == nullptr) {
json = cJSON_CreateObject();
}
// Set current SSID
cJSON* current_ssid_item = cJSON_GetObjectItem(json, "current_ssid");
if (current_ssid_item != nullptr) {
cJSON_SetValuestring(current_ssid_item, ssid.c_str());
} else {
cJSON_AddStringToObject(json, "current_ssid", ssid.c_str());
}
// Get or create credentials object
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);
}
// Limit stored credentials to prevent excessive file size (keep max 10 SSIDs)
int credential_count = cJSON_GetArraySize(credentials);
if (credential_count >= 10) {
ESP_LOGW(TAG, "Too many stored credentials (%d), keeping only current SSID", credential_count);
// Keep only the current SSID's credentials
cJSON* new_credentials = cJSON_CreateObject();
cJSON_ReplaceItemInObject(json, "credentials", new_credentials);
credentials = new_credentials;
}
// Remove existing entry for this SSID to update it
cJSON_DeleteItemFromObject(credentials, ssid.c_str());
// Create SSID object with password
// 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);
// Serialize and write to file
char* json_str = cJSON_PrintUnformatted(json);
if (json_str) {
size_t bytes_written = 0;
esp_err_t err = fs_handler_->write_file(
WIFI_CRED_FILE_PATH,
reinterpret_cast<const uint8_t*>(json_str),
strlen(json_str),
bytes_written
);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write WiFi credentials to file: %s", esp_err_to_name(err));
} else {
ESP_LOGI(TAG, "Stored WiFi credentials for SSID: %s", ssid.c_str());
}
cJSON_free(json_str);
} else {
ESP_LOGE(TAG, "Failed to serialize WiFi credentials JSON");
// 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(std::string& out_ssid, std::string& out_password) {
if (!fs_handler_) {
ESP_LOGW(TAG, "FSHandler not set, cannot get WiFi credentials");
out_ssid = "";
out_password = "";
if (!kvs) {
ESP_LOGW(TAG, "KVStorageHandler not set, cannot get 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;
}
out_ssid = kvs->get(WIFI_SSID_KEY);
if (out_ssid.empty()) {
out_ssid = "";
out_password = "";
return;
}
// Check if credentials file exists
if (!fs_handler_->file_exists(WIFI_CRED_FILE_PATH)) {
ESP_LOGD(TAG, "WiFi credentials file does not exist");
out_ssid = "";
// password is from KV storage, may be nullptr
std::string password_key_store = kvs->get(WIFI_PASSWORD_STORE_KEY);
if (password_key_store.empty()) {
out_password = "";
return;
}
// Read credentials file
size_t file_size = 0;
esp_err_t err = fs_handler_->get_file_size(WIFI_CRED_FILE_PATH, file_size);
if (err != ESP_OK || file_size == 0) {
ESP_LOGE(TAG, "Failed to get WiFi credentials file size");
out_ssid = "";
out_password = "";
return;
}
std::vector<uint8_t> file_data(file_size + 1); // +1 for null terminator
size_t bytes_read = 0;
err = fs_handler_->read_file(WIFI_CRED_FILE_PATH, file_size, file_data.data(), bytes_read);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to read WiFi credentials file: %s", esp_err_to_name(err));
out_ssid = "";
out_password = "";
return;
}
file_data[bytes_read] = '\0'; // Null terminate
// Parse JSON
cJSON* json = cJSON_Parse(reinterpret_cast<const char*>(file_data.data()));
// parse from json
cJSON* json = cJSON_Parse(password_key_store.c_str());
if (json == nullptr) {
ESP_LOGE(TAG, "Failed to parse WiFi credentials JSON");
out_ssid = "";
ESP_LOGE(TAG, "Failed to parse WiFi password JSON");
out_password = "";
return;
}
// Get current SSID
cJSON* current_ssid_item = cJSON_GetObjectItem(json, "current_ssid");
if (current_ssid_item == nullptr || !cJSON_IsString(current_ssid_item)) {
ESP_LOGE(TAG, "WiFi credentials JSON does not contain valid 'current_ssid' field");
out_ssid = "";
out_password = "";
cJSON_Delete(json);
return;
}
out_ssid = current_ssid_item->valuestring;
// Get credentials object
cJSON* credentials = cJSON_GetObjectItem(json, "credentials");
if (credentials == nullptr || !cJSON_IsObject(credentials)) {
ESP_LOGE(TAG, "WiFi credentials JSON does not contain valid 'credentials' object");
out_ssid = "";
ESP_LOGE(TAG, "WiFi password JSON does not contain valid 'credentials' object");
out_password = "";
cJSON_Delete(json);
return;
}
// Get SSID entry
// 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 credentials JSON does not contain entry for SSID: %s", out_ssid.c_str());
out_ssid = "";
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;
}
// Get password
cJSON* password_item = cJSON_GetObjectItem(ssid_item, "password");
if (password_item == nullptr || !cJSON_IsString(password_item)) {
ESP_LOGE(TAG, "WiFi credentials JSON does not contain valid 'password' field for SSID: %s", out_ssid.c_str());
out_ssid = "";
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_item->valuestring;
out_password = password->valuestring;
cJSON_Delete(json);
ESP_LOGD(TAG, "Retrieved WiFi credentials for SSID: %s", out_ssid.c_str());
}
EventBits_t WifiHandler::wait_for_connection(TickType_t ticks_to_wait) {
@@ -539,23 +428,3 @@ EventBits_t WifiHandler::wait_for_connection(TickType_t ticks_to_wait) {
ticks_to_wait
);
}
std::string WifiHandler::get_current_ip() const {
esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (!netif) {
ESP_LOGW(TAG, "Failed to get netif handle");
return "";
}
esp_netif_ip_info_t ip_info;
esp_err_t err = esp_netif_get_ip_info(netif, &ip_info);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to get IP info: %s", esp_err_to_name(err));
return "";
}
// Convert IP address to string
char ip_str[16];
snprintf(ip_str, sizeof(ip_str), IPSTR, IP2STR(&ip_info.ip));
return std::string(ip_str);
}

View File

@@ -1,6 +1,5 @@
#pragma once
#include "freertos/FreeRTOS.h"
#include "io/fs_handler.h"
#include "io/io.h"
#include "esp_wifi.h"
#include "freertos/event_groups.h"
@@ -14,7 +13,7 @@ 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
std::shared_ptr<LittleFSHandler> fs_handler_
std::unique_ptr<KVStorageHandler> kvs
);
~WifiHandler();
@@ -31,9 +30,6 @@ public:
uint16_t& ap_count
);
// Get current IP address (empty string if not connected)
std::string get_current_ip() const;
static void wifi_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data);
private:
@@ -48,15 +44,13 @@ private:
void get_wifi_credentials(std::string& out_ssid, std::string& out_password);
bool initialized = false;
std::shared_ptr<LittleFSHandler> fs_handler_ = nullptr;
std::unique_ptr<KVStorageHandler> 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
std::string current_ssid;
// current password (temporarily stored for successful connection event)
std::string current_password;
// prevent auto-reconnect on expected disconnection, e.g. when user calls disconnect()
// should be reset to false after connect()
bool expect_disconnected = false;

39
main/ui/app_registry.h Normal file
View File

@@ -0,0 +1,39 @@
#pragma once
#include "ui/ui_app.h"
#include <vector>
/**
* @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<AppDescriptor*>& get_app_descriptors() const {
return _app_descriptors;
}
private:
AppRegistry() = default;
~AppRegistry() = default;
std::vector<AppDescriptor*> _app_descriptors = {};
};

View File

@@ -0,0 +1,652 @@
#include "discord_app.h"
#include "esp_log.h"
#include "network/network.h"
#include <sstream>
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) {
// Set up main page with flex column layout
lv_obj_set_flex_flow(page, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(page, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(page, 10, 0);
// === Top Section: Error Notification ===
error_notification_ = lv_obj_create(page);
lv_obj_set_width(error_notification_, LV_PCT(90));
lv_obj_set_height(error_notification_, LV_SIZE_CONTENT);
lv_obj_set_style_bg_color(error_notification_, lv_color_hex(0xFF0000), 0);
lv_obj_set_style_bg_opa(error_notification_, LV_OPA_70, 0);
lv_obj_set_style_pad_all(error_notification_, 10, 0);
lv_obj_set_style_radius(error_notification_, 8, 0);
lv_obj_add_flag(error_notification_, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_flex_flow(error_notification_, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(error_notification_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t* error_label = lv_label_create(error_notification_);
lv_label_set_text(error_label, LV_SYMBOL_WARNING " Connection Lost");
lv_obj_set_style_text_color(error_label, lv_color_white(), 0);
// === Center Section: Main Content ===
lv_obj_t* center_container = lv_obj_create(page);
lv_obj_set_size(center_container, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(center_container, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(center_container, 0, 0);
lv_obj_set_style_pad_all(center_container, 0, 0);
lv_obj_set_flex_flow(center_container, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(center_container, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_row(center_container, 15, 0);
lv_obj_set_flex_grow(center_container, 1);
// Status icon (large, centered)
status_icon_label_ = lv_label_create(center_container);
lv_label_set_text(status_icon_label_, LV_SYMBOL_MUTE);
// Status text
status_text_label_ = lv_label_create(center_container);
lv_label_set_text(status_text_label_, "Unknown Status");
// Mute button
mute_button_ = lv_btn_create(center_container);
lv_obj_set_size(mute_button_, 200, 60);
lv_obj_add_event_cb(mute_button_, on_mute_button_clicked, LV_EVENT_CLICKED, this);
lv_obj_t* mute_label = lv_label_create(mute_button_);
lv_label_set_text(mute_label, "MUTE");
lv_obj_center(mute_label);
// === Bottom Section: Settings and Config Prompt ===
lv_obj_t* bottom_container = lv_obj_create(page);
lv_obj_set_size(bottom_container, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(bottom_container, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(bottom_container, 0, 0);
lv_obj_set_style_pad_all(bottom_container, 0, 0);
lv_obj_set_flex_flow(bottom_container, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(bottom_container, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
// Config prompt (left side)
if (!settings_configured_) {
lv_obj_t* config_prompt = lv_label_create(bottom_container);
lv_label_set_text(config_prompt, "Tap " LV_SYMBOL_SETTINGS " to configure");
lv_obj_set_style_text_color(config_prompt, lv_color_hex(0x888888), 0);
} else {
// Empty spacer if configured
lv_obj_t* spacer = lv_obj_create(bottom_container);
lv_obj_set_size(spacer, 0, 0);
lv_obj_set_style_bg_opa(spacer, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(spacer, 0, 0);
}
// Settings button (right side)
lv_obj_t* settings_btn = lv_btn_create(bottom_container);
lv_obj_set_size(settings_btn, 60, 60);
lv_obj_add_event_cb(settings_btn, on_settings_button_clicked, LV_EVENT_CLICKED, this);
lv_obj_t* settings_icon = lv_label_create(settings_btn);
lv_label_set_text(settings_icon, LV_SYMBOL_SETTINGS);
lv_obj_center(settings_icon);
// Update display with current state
update_status_display();
}
// ============================================================================
// 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<DiscordApp*>(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<DiscordApp*>(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<DiscordApp*>(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<DiscordApp*>(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<DiscordApp*>(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);
}

123
main/ui/apps/discord_app.h Normal file
View File

@@ -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 <string>
/**
* @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();
};

View File

@@ -1,131 +0,0 @@
#include "ui/apps/iotdis/app.h"
#include "ui/apps/iotdis/ui/main_handler.h"
#include "ui/apps/iotdis/ui/settings_handler.h"
#include "common/system_context.h"
#include "esp_log.h"
static const char* TAG = "IotDisApp";
// ============================================================================
// IotDisApp Implementation
// ============================================================================
IotDisApp::IotDisApp()
: main_ui_handler_(nullptr)
, settings_ui_handler_(nullptr)
, current_page_(Page::MAIN)
, setting_handler_(nullptr)
, interaction_handler_(nullptr) {
setting_handler_ = std::make_unique<SettingHandler>(
std::make_unique<NVSStorageHandler>(IotDisApp::NVS_NAMESPACE)
);
}
IotDisApp::~IotDisApp() { }
esp_err_t IotDisApp::init(lv_obj_t* container, InteractionHandler* interaction_handler) {
ESP_LOGI(TAG, "Initializing Discord app");
container_ = container;
interaction_handler_ = interaction_handler;
// Initialize storage
setting_handler_->init(nullptr);
// Load saved settings
setting_handler_->load_settings();
// Create main UI handler
main_ui_handler_ = std::make_unique<MainUIHandler>();
main_ui_handler_->init(container, interaction_handler_, setting_handler_.get());
// Register settings button callback
main_ui_handler_->register_on_settings_button_clicked(on_settings_button_clicked_static, this);
current_page_ = Page::MAIN;
return ESP_OK;
}
esp_err_t IotDisApp::deinit() {
ESP_LOGI(TAG, "Deinitializing Discord app");
// Clean up UI handlers
if (settings_ui_handler_) {
settings_ui_handler_->deinit();
settings_ui_handler_.reset();
}
if (main_ui_handler_) {
main_ui_handler_->deinit();
main_ui_handler_.reset();
}
return ESP_OK;
}
std::string IotDisApp::get_name() const {
return "Discord";
}
bool IotDisApp::on_back_button_pressed() {
// If on settings page, go back to main page
if (current_page_ == Page::SETTINGS) {
// Clean up settings handler
if (settings_ui_handler_) {
settings_ui_handler_->deinit();
settings_ui_handler_.reset();
}
// Reload settings in case they were updated
setting_handler_->load_settings();
// Recreate main UI handler with updated settings
if (!main_ui_handler_) {
main_ui_handler_ = std::make_unique<MainUIHandler>();
main_ui_handler_->init(container_, interaction_handler_, setting_handler_.get());
main_ui_handler_->register_on_settings_button_clicked(on_settings_button_clicked_static, this);
}
// Update UI with configuration status
main_ui_handler_->update_config_prompt(setting_handler_->is_configured());
current_page_ = Page::MAIN;
return true;
}
// Let system handle back (return to app icons)
return false;
}
// ============================================================================
// Private Methods
// ============================================================================
// Settings page with web server and QR code
void IotDisApp::show_settings_page() {
ESP_LOGI(TAG, "Showing settings page");
// Hide main UI handler
if (main_ui_handler_) {
main_ui_handler_->deinit();
main_ui_handler_.reset();
}
// Create settings UI handler
settings_ui_handler_ = std::make_unique<SettingsUIHandler>();
settings_ui_handler_->init(container_, interaction_handler_, setting_handler_.get());
current_page_ = Page::SETTINGS;
}
// ============================================================================
// Static Callbacks
// ============================================================================
void IotDisApp::on_settings_button_clicked_static(lv_event_t* e) {
IotDisApp* app = static_cast<IotDisApp*>(lv_event_get_user_data(e));
if (app) {
app->show_settings_page();
}
}

View File

@@ -1,61 +0,0 @@
#pragma once
#include "ui/apps/app.h"
#include "ui/apps/iotdis/settings/settings_handler.h"
#include "ui/apps/iotdis/ui/main_handler.h"
#include "ui/apps/iotdis/ui/settings_handler.h"
#include "io/nvs_handler.h"
#include <string>
#include <memory>
// Forward declarations
class MainUIHandler;
class SettingsUIHandler;
/**
* @brief IotDis (Discord Integration) App
*
* Manages Discord voice state monitoring and control via UDP bridge.
* Features:
* - Real-time voice state monitoring (muted/unmuted)
* - Manual mute/unmute control
* - Settings for bridge IP/port configuration
* - Connection error detection and notification
* - NVS storage for persistent settings
*/
class IotDisApp : public UIApp {
public:
IotDisApp();
~IotDisApp() override;
esp_err_t init(lv_obj_t* container, InteractionHandler* interaction_handler) override;
esp_err_t deinit(void) override;
std::string get_name(void) const override;
bool on_back_button_pressed(void) override;
private:
// UI handlers
std::unique_ptr<MainUIHandler> main_ui_handler_;
std::unique_ptr<SettingsUIHandler> settings_ui_handler_;
// Current page tracking
enum class Page {
MAIN,
SETTINGS
};
Page current_page_;
// Settings handler (shared across handlers)
std::unique_ptr<SettingHandler> setting_handler_;
// Interaction handler (not owned)
InteractionHandler* interaction_handler_;
static constexpr const char* NVS_NAMESPACE = "discord_app";
// Private methods
void show_settings_page();
// UI callback forwarders
static void on_settings_button_clicked_static(lv_event_t* e);
};

View File

@@ -1,257 +0,0 @@
#include "ui/apps/iotdis/bridge/bridge.h"
#include "esp_err.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#define TAG "IotDisBridge"
#define MUTE_COMMAND "MUTE"
#define STATUS_COMMAND "STATUS"
#define MUTED_RESPONSE "MUTED"
#define UNMUTED_RESPONSE "UNMUTED"
IotDisBridge::~IotDisBridge() {
stop_polling_task();
}
void IotDisBridge::start_polling_task() {
if (poll_task_handle_) {
ESP_LOGW(TAG, "Polling task already running");
return;
}
udp_client_.init(setting_handler_->get_local_port());
udp_client_.configure(
setting_handler_->get_remote_ip(),
setting_handler_->get_remote_port()
);
stop_polling_ = false;
xTaskCreate(poll_task_, "discord_poll", 4096, this, 5, &poll_task_handle_);
}
void IotDisBridge::stop_polling_task() {
if (!poll_task_handle_) {
if (udp_client_.is_configured()) {
udp_client_.close();
}
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;
}
if (udp_client_.is_configured()) {
udp_client_.close();
}
on_status_update_callback_ = nullptr;
status_event_user_data_ = nullptr;
consecutive_failures_ = 0;
}
esp_err_t IotDisBridge::send_mute_command() {
if (!setting_handler_->is_configured()) {
ESP_LOGW(TAG, "Cannot send command: not configured");
return ESP_FAIL;
}
ESP_LOGI(TAG, "Sending MUTE command");
esp_err_t err = udp_client_.send_command(MUTE_COMMAND);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send MUTE command");
return err;
}
return ESP_OK;
}
bool IotDisBridge::test_connection(const std::string& ip, uint16_t port, uint16_t local_port) {
ESP_LOGI(TAG, "Testing connection to %s:%u (local port: %u)", ip.c_str(), port, local_port);
// Create temporary UDP client for testing
UDPClient test_client;
esp_err_t err = test_client.init(local_port);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize test UDP client");
return false;
}
err = test_client.configure(ip, port);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to configure test UDP client");
return false;
}
err = test_client.send_command(STATUS_COMMAND);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send STATUS command");
return false;
}
ESP_LOGI(TAG, "STATUS command sent, waiting for response (timeout: %dms)", RESPONSE_TIMEOUT_MS);
std::string response;
err = test_client.receive_response(response, RESPONSE_TIMEOUT_MS);
if (err == ESP_OK) {
ESP_LOGI(TAG, "Received response: %s", response.c_str());
bool valid = (response == MUTED_RESPONSE || response == UNMUTED_RESPONSE);
if (!valid) {
ESP_LOGW(TAG, "Unexpected response (expected MUTED or UNMUTED)");
}
test_client.close();
return valid;
} else if (err == ESP_ERR_TIMEOUT) {
ESP_LOGW(TAG, "Timeout waiting for response");
} else {
ESP_LOGE(TAG, "Error receiving response: %d", err);
}
test_client.close();
return false;
}
//
// private methods
//
void IotDisBridge::poll_task_(void* param) {
IotDisBridge* bridge = static_cast<IotDisBridge*>(param);
ESP_LOGI(TAG, "Polling task started");
while (!bridge->stop_polling_) {
ESP_LOGI(TAG, "Polling for status update...");
bridge->poll_status_();
ESP_LOGI(TAG, "poll_status_() returned");
// Yield to allow display updates to complete
taskYIELD();
// Use longer interval if in error state
int interval = (bridge->consecutive_failures_ >= MAX_FAILURES_BEFORE_ERROR)
? ERROR_POLL_INTERVAL_MS
: POLL_INTERVAL_MS;
ESP_LOGI(TAG, "Next poll in %d ms", interval);
vTaskDelay(pdMS_TO_TICKS(interval));
}
ESP_LOGI(TAG, "Polling task stopped");
bridge->poll_task_handle_ = nullptr;
vTaskDelete(nullptr);
}
void IotDisBridge::poll_status_() {
if (!setting_handler_->is_configured()) {
// Don't poll if not configured
return;
}
// First check for any unsolicited push messages (non-blocking)
std::string push_message;
esp_err_t err = udp_client_.receive_response(push_message, 0); // 0 = non-blocking
if (err == ESP_OK && !push_message.empty()) {
// Received push update from remote
ESP_LOGI(TAG, "Received push update: %s", push_message.c_str());
StatusUpdateEventData event_data {
.state = StatusUpdateEventData::VoiceState::UNKNOWN
};
if (push_message == MUTED_RESPONSE) {
event_data.state = StatusUpdateEventData::VoiceState::MUTED;
} else if (push_message == UNMUTED_RESPONSE) {
event_data.state = StatusUpdateEventData::VoiceState::UNMUTED;
}
// Reset failure counter on successful push update
if (event_data.state != StatusUpdateEventData::VoiceState::UNKNOWN) {
consecutive_failures_ = 0;
}
if (on_status_update_callback_) {
on_status_update_callback_(event_data, status_event_user_data_);
}
// Got push update, skip polling
if (event_data.state != StatusUpdateEventData::VoiceState::UNKNOWN) {
return;
}
}
// Send STATUS command for polling
ESP_LOGI(TAG, "Sending STATUS command for polling");
err = udp_client_.send_command(STATUS_COMMAND);
if (err != ESP_OK) {
consecutive_failures_++;
ESP_LOGW(TAG, "Failed to send STATUS command for polling. Consecutive failures: %d",
consecutive_failures_);
if (consecutive_failures_ >= MAX_FAILURES_BEFORE_ERROR) {
if (on_status_update_callback_) {
on_status_update_callback_(
StatusUpdateEventData { .state = StatusUpdateEventData::VoiceState::ERROR },
status_event_user_data_
);
}
}
return;
}
// Wait for response to STATUS command
ESP_LOGI(TAG, "Waiting for response to STATUS command (timeout: %dms)", RESPONSE_TIMEOUT_MS);
std::string response;
err = udp_client_.receive_response(response, RESPONSE_TIMEOUT_MS);
ESP_LOGI(TAG, "Received response from STATUS command: err=%d", err);
if (err == ESP_OK) {
// Success - reset failure counter
consecutive_failures_ = 0;
ESP_LOGI(TAG, "STATUS response: %s", response.c_str());
StatusUpdateEventData event_data {
.state = StatusUpdateEventData::VoiceState::UNKNOWN
};
if (response == MUTED_RESPONSE) {
event_data.state = StatusUpdateEventData::VoiceState::MUTED;
} else if (response == UNMUTED_RESPONSE) {
event_data.state = StatusUpdateEventData::VoiceState::UNMUTED;
}
ESP_LOGI(TAG, "Invoking status update callback with state: %d", event_data.state);
if (on_status_update_callback_) {
on_status_update_callback_(event_data, status_event_user_data_);
ESP_LOGI(TAG, "Status update callback returned");
}
} else {
// Timeout or error
consecutive_failures_++;
ESP_LOGW(TAG, "No response to STATUS (failures: %d, error: %d)", consecutive_failures_, err);
if (consecutive_failures_ >= MAX_FAILURES_BEFORE_ERROR) {
ESP_LOGW(TAG, "Max failures reached, sending ERROR state");
if (on_status_update_callback_) {
on_status_update_callback_(
StatusUpdateEventData { .state = StatusUpdateEventData::VoiceState::ERROR },
status_event_user_data_
);
}
}
}
ESP_LOGI(TAG, "poll_status_ complete");
}

View File

@@ -1,67 +0,0 @@
#pragma once
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "ui/apps/iotdis/settings/settings_handler.h"
#include <string>
#include <cstdint>
#include <memory>
#include "esp_err.h"
#include "network/udp_client.h"
struct StatusUpdateEventData {
enum class VoiceState {
UNKNOWN,
MUTED,
UNMUTED,
ERROR
} state;
};
using StatusEventCallback = void(*)(StatusUpdateEventData, void*);
class IotDisBridge {
public:
IotDisBridge(
SettingHandler* setting_handler
) : setting_handler_(setting_handler) { }
~IotDisBridge();
void start_polling_task();
void stop_polling_task();
esp_err_t send_mute_command();
bool test_connection(const std::string& ip, uint16_t port) {
return test_connection(ip, port, setting_handler_->get_local_port());
}
bool test_connection(const std::string& ip, uint16_t port, uint16_t local_port);
void register_on_status_update_callback(
StatusEventCallback callback,
void* status_event_user_data
) {
on_status_update_callback_ = callback;
status_event_user_data_ = status_event_user_data;
}
private:
static constexpr int POLL_INTERVAL_MS = 10000;
static constexpr int ERROR_POLL_INTERVAL_MS = 20000;
static constexpr int RESPONSE_TIMEOUT_MS = 1000;
static constexpr int MAX_FAILURES_BEFORE_ERROR = 3;
void poll_status_();
// Polling task
static void poll_task_(void* param);
TaskHandle_t poll_task_handle_ = nullptr;
bool stop_polling_ = false;
int consecutive_failures_ = 0;
SettingHandler* setting_handler_ = nullptr;
UDPClient udp_client_;
StatusEventCallback on_status_update_callback_ = nullptr;
void* status_event_user_data_ = nullptr;
};

View File

@@ -1,11 +0,0 @@
#include "ui/apps/iotdis/descriptor.h"
IotDisDescriptor::IotDisDescriptor()
: AppDescriptor("IotDis", std::make_unique<IotDisApp>()) { }
void IotDisDescriptor::draw_icon(lv_obj_t* parent) {
// Draw Discord icon (call/phone symbol)
lv_obj_t* icon = lv_label_create(parent);
lv_label_set_text(icon, LV_SYMBOL_CALL);
lv_obj_center(icon);
}

View File

@@ -1,12 +0,0 @@
#pragma once
#include "ui/apps/app.h"
#include "ui/apps/iotdis/app.h"
class IotDisDescriptor : public AppDescriptor {
public:
IotDisDescriptor();
~IotDisDescriptor() override = default;
void draw_icon(lv_obj_t* parent) override;
};

View File

@@ -1,52 +0,0 @@
#include "ui/apps/iotdis/settings/settings_handler.h"
#include "esp_log.h"
#define TAG "SettingHandler"
void SettingHandler::load_settings() {
remote_ip_ = storage_->get(NVS_KEY_IP);
std::string port_str = storage_->get(NVS_KEY_PORT);
std::string local_port_str = storage_->get(NVS_KEY_LOCAL_PORT);
if (!remote_ip_.empty() && !port_str.empty()) {
remote_port_ = static_cast<uint16_t>(atoi(port_str.c_str()));
// Load local port, default to DEFAULT_LOCAL_PORT if not configured
if (!local_port_str.empty()) {
local_port_ = static_cast<uint16_t>(atoi(local_port_str.c_str()));
} else {
local_port_ = DEFAULT_LOCAL_PORT;
}
ESP_LOGI(TAG, "Loaded settings: %s:%u (local port: %u)", remote_ip_.c_str(), remote_port_, local_port_);
} else {
local_port_ = DEFAULT_LOCAL_PORT;
ESP_LOGI(TAG, "No settings found, user setup required");
}
}
void SettingHandler::save_settings(const std::string& ip, uint16_t port, uint16_t local_port) {
if (ip.empty() || port == 0 || local_port == 0) {
ESP_LOGW(TAG, "Cannot save: invalid settings");
return;
}
// Save to NVS
storage_->put(NVS_KEY_IP, ip);
char port_str[8];
snprintf(port_str, sizeof(port_str), "%u", port);
storage_->put(NVS_KEY_PORT, port_str);
char local_port_str[8];
snprintf(local_port_str, sizeof(local_port_str), "%u", local_port);
storage_->put(NVS_KEY_LOCAL_PORT, local_port_str);
// Update local config
remote_ip_ = ip;
remote_port_ = port;
local_port_ = local_port;
ESP_LOGI(TAG, "Settings saved: %s:%u (local port: %u)", remote_ip_.c_str(), remote_port_, local_port_);
}

View File

@@ -1,45 +0,0 @@
#pragma once
#include "freertos/FreeRTOS.h"
#include <string>
#include <memory>
#include "io/nvs_handler.h"
class SettingHandler {
public:
SettingHandler(std::unique_ptr<NVSStorageHandler> storage) :
remote_ip_(""),
remote_port_(0),
local_port_(0),
storage_(std::move(storage)) { }
~SettingHandler() = default;
esp_err_t init(const EventGroupHandle_t& system_event_group) {
storage_->init(system_event_group);
return ESP_OK;
}
void load_settings();
void save_settings(const std::string& ip, uint16_t port) {
save_settings(ip, port, local_port_);
}
void save_settings(const std::string& ip, uint16_t port, uint16_t local_port);
bool is_configured() const { return !remote_ip_.empty() && remote_port_ != 0 && local_port_ != 0; }
std::string get_remote_ip() const { return remote_ip_; }
uint16_t get_remote_port() const { return remote_port_; }
uint16_t get_local_port() const { return local_port_; }
private:
static constexpr uint16_t DEFAULT_LOCAL_PORT = 4212;
static constexpr const char* NVS_KEY_IP = "bridge_ip";
static constexpr const char* NVS_KEY_PORT = "bridge_port";
static constexpr const char* NVS_KEY_LOCAL_PORT = "local_port";
std::string remote_ip_;
uint16_t remote_port_;
uint16_t local_port_;
std::unique_ptr<NVSStorageHandler> storage_;
};

View File

@@ -1,214 +0,0 @@
#include "ui/apps/iotdis/ui/main.h"
#include "ui/apps/iotdis/app.h"
#include "ui/interaction_handler.h"
#include "esp_log.h"
#include "esp_lvgl_port.h"
#define LVGL_LOCK_TIMEOUT 2000 // milliseconds
static const char* TAG = "MainUI";
MainUI::~MainUI() {
deinit();
}
esp_err_t MainUI::init(lv_obj_t* parent, InteractionHandler* interaction_handler) {
container_ = parent;
create_ui_(parent);
return ESP_OK;
}
esp_err_t MainUI::deinit(void) {
// LVGL will clean up children automatically when parent is deleted
error_notification_ = nullptr;
status_icon_label_ = nullptr;
status_text_label_ = nullptr;
mute_button_ = nullptr;
settings_button_ = nullptr;
config_prompt_ = nullptr;
container_ = nullptr;
return ESP_OK;
}
void MainUI::create_ui_(lv_obj_t* parent) {
if (!parent) {
ESP_LOGE(TAG, "Parent LVGL object is null");
return;
}
if (!lvgl_port_lock(pdMS_TO_TICKS(LVGL_LOCK_TIMEOUT))) {
ESP_LOGE(TAG, "Failed to acquire LVGL lock for UI creation");
return;
}
// Set up main page with flex column layout
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(parent, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(parent, 10, 0);
// === Top Section: Error Notification ===
error_notification_ = lv_obj_create(parent);
lv_obj_set_width(error_notification_, LV_PCT(90));
lv_obj_set_height(error_notification_, LV_SIZE_CONTENT);
lv_obj_set_style_bg_color(error_notification_, lv_color_white(), 0);
lv_obj_set_style_bg_opa(error_notification_, LV_OPA_COVER, 0);
lv_obj_set_style_border_color(error_notification_, lv_color_black(), 0);
lv_obj_set_style_border_width(error_notification_, 2, 0);
lv_obj_set_style_pad_all(error_notification_, 10, 0);
lv_obj_set_style_radius(error_notification_, 8, 0);
lv_obj_add_flag(error_notification_, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_flex_flow(error_notification_, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(error_notification_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_t* error_label = lv_label_create(error_notification_);
lv_label_set_text(error_label, LV_SYMBOL_WARNING " Connection Lost");
lv_obj_set_style_text_color(error_label, lv_color_black(), 0);
// === Center Section: Main Content ===
lv_obj_t* center_container = lv_obj_create(parent);
lv_obj_set_size(center_container, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(center_container, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(center_container, 0, 0);
lv_obj_set_style_pad_all(center_container, 0, 0);
lv_obj_set_flex_flow(center_container, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(center_container, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_row(center_container, 15, 0);
lv_obj_set_flex_grow(center_container, 1);
// Status icon (large, centered)
status_icon_label_ = lv_label_create(center_container);
lv_label_set_text(status_icon_label_, LV_SYMBOL_MUTE);
// Status text
status_text_label_ = lv_label_create(center_container);
lv_label_set_text(status_text_label_, "Unknown Status");
// Mute button
mute_button_ = lv_btn_create(center_container);
lv_obj_set_size(mute_button_, 200, 60);
lv_obj_set_style_bg_color(mute_button_, lv_color_white(), 0);
lv_obj_set_style_border_color(mute_button_, lv_color_black(), 0);
lv_obj_set_style_border_width(mute_button_, 2, 0);
lv_obj_t* mute_label = lv_label_create(mute_button_);
lv_label_set_text(mute_label, "MUTE");
lv_obj_center(mute_label);
lv_obj_set_style_text_color(mute_label, lv_color_black(), 0);
// === Bottom Section: Settings and Config Prompt ===
lv_obj_t* bottom_container = lv_obj_create(parent);
lv_obj_set_size(bottom_container, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(bottom_container, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(bottom_container, 0, 0);
lv_obj_set_style_pad_all(bottom_container, 0, 0);
lv_obj_set_flex_flow(bottom_container, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(bottom_container, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
// Config prompt (left side)
config_prompt_ = lv_label_create(bottom_container);
lv_label_set_text(config_prompt_, "Tap " LV_SYMBOL_SETTINGS " to configure");
lv_obj_set_style_text_color(config_prompt_, lv_color_black(), 0);
// Settings button (right side)
settings_button_ = lv_btn_create(bottom_container);
lv_obj_set_size(settings_button_, 60, 60);
lv_obj_set_style_bg_color(settings_button_, lv_color_white(), 0);
lv_obj_set_style_border_color(settings_button_, lv_color_black(), 0);
lv_obj_set_style_border_width(settings_button_, 2, 0);
lv_obj_t* settings_icon = lv_label_create(settings_button_);
lv_label_set_text(settings_icon, LV_SYMBOL_SETTINGS);
lv_obj_center(settings_icon);
lv_obj_set_style_text_color(settings_icon, lv_color_black(), 0);
ESP_LOGI(TAG, "Main UI created");
lvgl_port_unlock();
}
esp_err_t MainUI::register_on_settings_button_clicked(lv_event_cb_t cb, void* user_data) {
if (!settings_button_) {
return ESP_ERR_INVALID_STATE;
}
lv_obj_add_event_cb(settings_button_, cb, LV_EVENT_CLICKED, user_data);
return ESP_OK;
}
esp_err_t MainUI::register_on_mute_button_clicked(lv_event_cb_t cb, void* user_data) {
if (!mute_button_) {
return ESP_ERR_INVALID_STATE;
}
lv_obj_add_event_cb(mute_button_, cb, LV_EVENT_CLICKED, user_data);
return ESP_OK;
}
bool MainUI::update_status(VoiceState state) {
if (!status_icon_label_ || !status_text_label_) {
return false;
}
if (!lvgl_port_lock(pdMS_TO_TICKS(LVGL_LOCK_TIMEOUT))) {
ESP_LOGW(TAG, "Failed to acquire LVGL lock for status update");
return false;
}
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_black(), 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_black(), 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_black(), 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_black(), 0);
break;
}
lvgl_port_unlock();
return true;
}
void MainUI::show_error_notification(bool show) {
if (!error_notification_) {
return;
}
if (!lvgl_port_lock(pdMS_TO_TICKS(LVGL_LOCK_TIMEOUT))) {
ESP_LOGW(TAG, "Failed to acquire LVGL lock for error notification update");
return;
}
if (show) {
lv_obj_clear_flag(error_notification_, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(error_notification_, LV_OBJ_FLAG_HIDDEN);
}
lvgl_port_unlock();
}
void MainUI::update_config_prompt(bool configured) {
if (!config_prompt_) {
return;
}
if (!lvgl_port_lock(pdMS_TO_TICKS(LVGL_LOCK_TIMEOUT))) {
ESP_LOGW(TAG, "Failed to acquire LVGL lock for config prompt update");
return;
}
if (configured) {
lv_obj_add_flag(config_prompt_, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_clear_flag(config_prompt_, LV_OBJ_FLAG_HIDDEN);
}
lvgl_port_unlock();
}

View File

@@ -1,85 +0,0 @@
#pragma once
#include "lvgl.h"
#include "esp_err.h"
#include <string>
#include "ui/events.h"
#include "ui/apps/iotdis/bridge/bridge.h"
#include "ui/interaction_handler.h"
// Voice state enumeration
enum class VoiceState {
UNKNOWN,
MUTED,
UNMUTED,
ERROR
};
// Forward declarations
class InteractionHandler;
/**
* @brief Main UI for Discord app
*
* Displays:
* - Current voice state (muted/unmuted/error/unknown)
* - Large status icon
* - Status text
* - Mute toggle button
* - Error notification banner (when connection lost)
* - Settings button
* - Configuration prompt (if not configured)
*/
class MainUI {
public:
MainUI() = default;
~MainUI();
esp_err_t init(lv_obj_t* parent, InteractionHandler* interaction_handler);
esp_err_t deinit(void);
/**
* @brief Register callback for settings button clicks
* @param cb Callback function
* @param user_data User data to pass to callback
*/
esp_err_t register_on_settings_button_clicked(lv_event_cb_t cb, void* user_data);
/**
* @brief Register callback for mute button clicks
* @param cb Callback function
* @param user_data User data to pass to callback
*/
esp_err_t register_on_mute_button_clicked(lv_event_cb_t cb, void* user_data);
/**
* @brief Update status display with current voice state
* @param state Current voice state
*/
bool update_status(VoiceState state);
/**
* @brief Show or hide error notification banner
* @param show true to show, false to hide
*/
void show_error_notification(bool show);
/**
* @brief Update configuration prompt visibility
* @param configured true if settings are configured
*/
void update_config_prompt(bool configured);
private:
void create_ui_(lv_obj_t* parent);
lv_obj_t* container_ = nullptr;
// UI elements
lv_obj_t* error_notification_ = nullptr;
lv_obj_t* status_icon_label_ = nullptr;
lv_obj_t* status_text_label_ = nullptr;
lv_obj_t* mute_button_ = nullptr;
lv_obj_t* settings_button_ = nullptr;
lv_obj_t* config_prompt_ = nullptr;
};

View File

@@ -1,199 +0,0 @@
#include "ui/apps/iotdis/ui/main_handler.h"
#include "esp_log.h"
static const char* TAG = "MainUIHandler";
MainUIHandler::MainUIHandler() {
state_mutex_ = xSemaphoreCreateMutex();
}
MainUIHandler::~MainUIHandler() {
deinit();
if (state_mutex_) {
vSemaphoreDelete(state_mutex_);
state_mutex_ = nullptr;
}
}
esp_err_t MainUIHandler::init(lv_obj_t* parent, InteractionHandler* interaction_handler, SettingHandler* setting_handler) {
ESP_LOGI(TAG, "Initializing Main UI Handler");
setting_handler_ = setting_handler;
// Create unique bridge instance for this handler
bridge_ = std::make_unique<IotDisBridge>(setting_handler_);
// Register status update callback
bridge_->register_on_status_update_callback(on_status_update_static_, this);
// Create main UI
main_ui_ = std::make_unique<MainUI>();
main_ui_->init(parent, interaction_handler);
// Register mute button callback
main_ui_->register_on_mute_button_clicked(on_mute_button_clicked_static_, this);
// Update UI with current configuration
main_ui_->update_config_prompt(setting_handler_->is_configured());
update_ui_();
// Start polling task
bridge_->start_polling_task();
return ESP_OK;
}
esp_err_t MainUIHandler::deinit(void) {
ESP_LOGI(TAG, "Deinitializing Main UI Handler");
// Stop polling
if (bridge_) {
bridge_->stop_polling_task();
bridge_.reset();
}
// Clean up UI
if (main_ui_) {
main_ui_->deinit();
main_ui_.reset();
}
return ESP_OK;
}
esp_err_t MainUIHandler::register_on_settings_button_clicked(lv_event_cb_t cb, void* user_data) {
on_settings_callback_ = cb;
settings_callback_user_data_ = user_data;
if (main_ui_) {
main_ui_->register_on_settings_button_clicked(cb, user_data);
} else {
ESP_LOGE(TAG, "Main UI not initialized");
return ESP_ERR_INVALID_STATE;
}
return ESP_OK;
}
void MainUIHandler::update_config_prompt(bool is_configured) {
if (main_ui_) {
main_ui_->update_config_prompt(is_configured);
} else {
ESP_LOGE(TAG, "Main UI not initialized");
}
}
void MainUIHandler::update_status() {
update_ui_();
}
// ============================================================================
// Private Methods
// ============================================================================
void MainUIHandler::send_mute_command_() {
if (!setting_handler_->is_configured()) {
ESP_LOGW(TAG, "Cannot send command: not configured");
return;
}
ESP_LOGI(TAG, "Sending MUTE command");
esp_err_t err = bridge_->send_mute_command();
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to send MUTE command");
}
}
void MainUIHandler::on_status_update_(StatusUpdateEventData data) {
ESP_LOGI(TAG, "on_status_update_ called with state: %d, current_state_: %d", data.state, current_state_);
// Update state in thread-safe manner
bool update_ui = false;
if (state_mutex_ && xSemaphoreTake(state_mutex_, pdMS_TO_TICKS(100)) == pdTRUE) {
if (data.state != current_state_) {
update_ui = true;
}
xSemaphoreGive(state_mutex_);
ESP_LOGI(TAG, "State updated in mutex");
}
// Update UI
if (!update_ui) {
ESP_LOGI(TAG, "State unchanged, skipping UI update");
return;
}
ESP_LOGI(TAG, "Calling update_ui_()");
update_ui_(&data.state);
ESP_LOGI(TAG, "on_status_update_ complete");
}
void MainUIHandler::update_ui_(StatusUpdateEventData::VoiceState* state_ptr) {
ESP_LOGI(TAG, "update_ui_ called");
if (main_ui_) {
StatusUpdateEventData::VoiceState state;
if (state_mutex_ && xSemaphoreTake(state_mutex_, pdMS_TO_TICKS(100)) == pdTRUE) {
state = state_ptr ? *state_ptr : current_state_;
xSemaphoreGive(state_mutex_);
} else {
state = StatusUpdateEventData::VoiceState::UNKNOWN;
}
ESP_LOGI(TAG, "Converting state: %d", state);
// Convert to MainUI VoiceState
VoiceState ui_state;
switch (state) {
case StatusUpdateEventData::VoiceState::MUTED:
ui_state = VoiceState::MUTED;
break;
case StatusUpdateEventData::VoiceState::UNMUTED:
ui_state = VoiceState::UNMUTED;
break;
case StatusUpdateEventData::VoiceState::ERROR:
ui_state = VoiceState::ERROR;
break;
default:
ui_state = VoiceState::UNKNOWN;
break;
}
ESP_LOGI(TAG, "Calling main_ui_->update_status() with ui_state: %d", ui_state);
// Lock LVGL before calling UI functions from another task
bool success = main_ui_->update_status(ui_state);
if (!success) {
ESP_LOGW(TAG, "main_ui_->update_status() failed");
} else {
// Update current state only on successful UI update
current_state_ = state;
}
ESP_LOGI(TAG, "main_ui_->update_status() returned");
}
ESP_LOGI(TAG, "update_ui_ complete");
}
// ============================================================================
// Static Callbacks
// ============================================================================
void MainUIHandler::on_mute_button_clicked_static_(lv_event_t* e) {
MainUIHandler* handler = static_cast<MainUIHandler*>(lv_event_get_user_data(e));
if (handler) {
handler->on_mute_button_clicked_();
}
}
void MainUIHandler::on_mute_button_clicked_() {
send_mute_command_();
}
void MainUIHandler::on_status_update_static_(StatusUpdateEventData data, void* user_data) {
MainUIHandler* handler = static_cast<MainUIHandler*>(user_data);
if (handler) {
handler->on_status_update_(data);
}
}

View File

@@ -1,51 +0,0 @@
#pragma once
#include "ui/apps/iotdis/ui/main.h"
#include "ui/interaction_handler.h"
#include "ui/apps/iotdis/bridge/bridge.h"
#include "ui/apps/iotdis/settings/settings_handler.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_err.h"
#include <memory>
/**
* @brief Main UI Handler for Discord App
*
* Manages the MainUI instance and interaction with the InteractionHandler.
* Each handler instance has its own IotDisBridge to prevent conflicts.
*/
class MainUIHandler {
public:
MainUIHandler();
~MainUIHandler();
esp_err_t init(lv_obj_t* parent, InteractionHandler* interaction_handler, SettingHandler* setting_handler);
esp_err_t deinit(void);
esp_err_t register_on_settings_button_clicked(lv_event_cb_t cb, void* user_data);
void update_config_prompt(bool is_configured);
void update_status();
private:
static void on_mute_button_clicked_static_(lv_event_t* e);
static void on_status_update_static_(StatusUpdateEventData data, void* user_data);
void on_mute_button_clicked_();
void on_status_update_(StatusUpdateEventData data);
void send_mute_command_();
void update_ui_(StatusUpdateEventData::VoiceState* state = nullptr);
std::unique_ptr<MainUI> main_ui_ = nullptr;
std::unique_ptr<IotDisBridge> bridge_ = nullptr;
SettingHandler* setting_handler_ = nullptr; // Not owned
// Voice state tracking
StatusUpdateEventData::VoiceState current_state_ = StatusUpdateEventData::VoiceState::UNKNOWN;
SemaphoreHandle_t state_mutex_ = nullptr;
// Callback for settings button
lv_event_cb_t on_settings_callback_ = nullptr;
void* settings_callback_user_data_ = nullptr;
};

View File

@@ -1,74 +0,0 @@
#include "ui/apps/iotdis/ui/settings.h"
#include "ui/interaction_handler.h"
#include "esp_log.h"
#include <cstring>
static const char* TAG = "SettingsUI";
SettingsUI::~SettingsUI() {
deinit();
}
esp_err_t SettingsUI::init(lv_obj_t* parent, InteractionHandler* interaction_handler) {
container_ = parent;
create_ui_(parent, interaction_handler);
return ESP_OK;
}
esp_err_t SettingsUI::deinit(void) {
// LVGL will clean up children automatically when parent is deleted
qr_code_ = nullptr;
status_label_ = nullptr;
container_ = nullptr;
return ESP_OK;
}
void SettingsUI::create_ui_(lv_obj_t* parent, InteractionHandler* interaction_handler) {
// Title
lv_obj_t* title = lv_label_create(parent);
lv_label_set_text(title, "Scan to Configure");
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 20);
lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0);
// Instruction text
lv_obj_t* instruction = lv_label_create(parent);
lv_label_set_text(instruction, "Scan this QR code with your mobile\ndevice to configure settings");
lv_obj_align(instruction, LV_ALIGN_TOP_MID, 0, 60);
lv_obj_set_style_text_align(instruction, LV_TEXT_ALIGN_CENTER, 0);
// QR code (centered)
qr_code_ = lv_qrcode_create(parent);
lv_qrcode_set_size(qr_code_, 250);
lv_qrcode_set_dark_color(qr_code_, lv_color_black());
lv_qrcode_set_light_color(qr_code_, lv_color_white());
lv_obj_align(qr_code_, LV_ALIGN_CENTER, 0, 0);
// Status label below QR code
status_label_ = lv_label_create(parent);
lv_label_set_text(status_label_, "Initializing...");
lv_obj_align(status_label_, LV_ALIGN_BOTTOM_MID, 0, -40);
lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_CENTER, 0);
ESP_LOGI(TAG, "Settings UI created with QR code display");
}
void SettingsUI::set_config_url(const std::string& url) {
if (!qr_code_ || url.empty()) {
ESP_LOGW(TAG, "Cannot set config URL: qr_code=%p, url=%s", qr_code_, url.c_str());
return;
}
lv_result_t result = lv_qrcode_update(qr_code_, url.c_str(), url.length());
if (result != LV_RESULT_OK) {
ESP_LOGE(TAG, "Failed to update QR code");
set_status_message("Error: Failed to generate QR code");
} else {
ESP_LOGI(TAG, "QR code updated with URL: %s", url.c_str());
}
}
void SettingsUI::set_status_message(const std::string& message) {
if (status_label_) {
lv_label_set_text(status_label_, message.c_str());
}
}

View File

@@ -1,41 +0,0 @@
#pragma once
#include "lvgl.h"
#include "esp_err.h"
#include <string>
// Forward declaration
class InteractionHandler;
/**
* @brief Settings UI for Discord app
*
* Displays a QR code that links to a web-based configuration interface
*/
class SettingsUI {
public:
SettingsUI() = default;
~SettingsUI();
esp_err_t init(lv_obj_t* parent, InteractionHandler* interaction_handler);
esp_err_t deinit(void);
/**
* @brief Set the configuration URL to display in QR code
* @param url Full URL including IP, port, and auth key
*/
void set_config_url(const std::string& url);
/**
* @brief Update status message below QR code
* @param message Status message to display
*/
void set_status_message(const std::string& message);
private:
void create_ui_(lv_obj_t* parent, InteractionHandler* interaction_handler);
lv_obj_t* container_ = nullptr;
lv_obj_t* qr_code_ = nullptr;
lv_obj_t* status_label_ = nullptr;
};

View File

@@ -1,90 +0,0 @@
#include "ui/apps/iotdis/ui/settings_handler.h"
#include "network/network.h"
#include "esp_log.h"
#include <sstream>
#include <iomanip>
static const char* TAG = "SettingsUIHandler";
SettingsUIHandler::SettingsUIHandler() { }
SettingsUIHandler::~SettingsUIHandler() {
deinit();
}
esp_err_t SettingsUIHandler::init(lv_obj_t* parent, InteractionHandler* interaction_handler, SettingHandler* setting_handler) {
ESP_LOGI(TAG, "Initializing Settings UI Handler");
setting_handler_ = setting_handler;
// Create unique bridge instance for this handler
bridge_ = std::make_unique<IotDisBridge>(setting_handler_);
// Create web handler with unique bridge
web_handler_ = std::make_unique<WebHandler>(setting_handler_, bridge_.get());
// Create settings UI
settings_ui_ = std::make_unique<SettingsUI>();
settings_ui_->init(parent, interaction_handler);
// Start web server and setup
setup_web_server_();
return ESP_OK;
}
esp_err_t SettingsUIHandler::deinit(void) {
ESP_LOGI(TAG, "Deinitializing Settings UI Handler");
// Stop web server
if (web_handler_) {
web_handler_->stop_web_server();
web_handler_.reset();
}
// Stop bridge
if (bridge_) {
bridge_->stop_polling_task();
bridge_.reset();
}
// Clean up UI
if (settings_ui_) {
settings_ui_->deinit();
settings_ui_.reset();
}
return ESP_OK;
}
// ============================================================================
// Private Methods
// ============================================================================
void SettingsUIHandler::setup_web_server_() {
// Start web server
web_handler_->start_web_server();
if (web_handler_->is_running()) {
std::string device_ip = web_handler_->get_device_ip();
uint16_t port = web_handler_->get_port();
if (!device_ip.empty()) {
std::string url = web_handler_->get_url();
settings_ui_->set_config_url(url);
std::ostringstream status;
status << "Server running on " << device_ip << ":" << port;
settings_ui_->set_status_message(status.str());
ESP_LOGI(TAG, "QR code URL: %s", url.c_str());
} else {
settings_ui_->set_status_message("Error: No IP address");
ESP_LOGE(TAG, "Failed to get device IP address");
}
} else {
settings_ui_->set_status_message("Error: Failed to start server");
ESP_LOGE(TAG, "Web server failed to start");
}
}

View File

@@ -1,33 +0,0 @@
#pragma once
#include "ui/apps/iotdis/ui/settings.h"
#include "ui/interaction_handler.h"
#include "ui/apps/iotdis/bridge/bridge.h"
#include "ui/apps/iotdis/settings/settings_handler.h"
#include "ui/apps/iotdis/web/web_handlers.h"
#include "esp_err.h"
#include <memory>
/**
* @brief Settings UI Handler for Discord App
*
* Manages the SettingsUI instance, web server, and interaction with the InteractionHandler.
* Each handler instance has its own IotDisBridge to prevent conflicts.
*/
class SettingsUIHandler {
public:
SettingsUIHandler();
~SettingsUIHandler();
esp_err_t init(lv_obj_t* parent, InteractionHandler* interaction_handler, SettingHandler* setting_handler);
esp_err_t deinit(void);
private:
void setup_web_server_();
std::unique_ptr<SettingsUI> settings_ui_ = nullptr;
std::unique_ptr<IotDisBridge> bridge_ = nullptr;
std::unique_ptr<WebHandler> web_handler_ = nullptr;
SettingHandler* setting_handler_ = nullptr; // Not owned
};

View File

@@ -1,362 +0,0 @@
#include "web_handlers.h"
#include "../app.h"
#include "esp_log.h"
#include "network/network.h"
#include "common/system_context.h"
#include "esp_random.h"
#include <sstream>
#include <iomanip>
static const char* TAG = "DiscordWebHandler";
WebHandler::~WebHandler() {
stop_web_server();
}
esp_err_t WebHandler::start_web_server() {
if (web_server_ && web_server_->is_running()) {
ESP_LOGI(TAG, "Web server already running");
return ESP_OK;
}
auth_key_ = generate_auth_key_();
uint16_t port = web_server_->start(
auth_key_,
8080
);
if (port == 0) {
ESP_LOGE(TAG, "Failed to start web server");
return ESP_FAIL;
}
esp_err_t ret = register_web_endpoints_();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to register web endpoints");
web_server_->stop();
return ret;
}
ESP_LOGI(TAG, "Web server started");
return ESP_OK;
}
esp_err_t WebHandler::stop_web_server() {
if (web_server_) {
web_server_->stop();
ESP_LOGI(TAG, "Web server stopped");
}
auth_key_.clear();
return ESP_OK;
}
std::string WebHandler::get_url() const {
if (web_server_ && web_server_->is_running()) {
NetworkHandler* network_handler = SystemContext::instance().get_network_handler();
if (!network_handler) {
ESP_LOGE(TAG, "Network handler not available in system context");
return "";
}
WifiHandler& wifi_handler = network_handler->get_wifi_handler();
std::string device_ip = wifi_handler.get_current_ip();
if (device_ip.empty()) {
ESP_LOGW(TAG, "Device not connected to WiFi");
return "";
}
uint16_t port = web_server_->get_port();
std::ostringstream url;
url << "http://" << device_ip << ":" << port << "/?auth=" << auth_key_;
return url.str();
}
return "";
}
std::string WebHandler::get_device_ip() const {
if (web_server_ && web_server_->is_running()) {
NetworkHandler* network_handler = SystemContext::instance().get_network_handler();
if (!network_handler) {
ESP_LOGE(TAG, "Network handler not available in system context");
return "";
}
WifiHandler& wifi_handler = network_handler->get_wifi_handler();
return wifi_handler.get_current_ip();
}
return "";
}
uint16_t WebHandler::get_port() const {
if (web_server_ && web_server_->is_running()) {
return web_server_->get_port();
}
return 0;
}
//
//
//
std::string WebHandler::generate_auth_key_() {
// Generate 128-bit random key using ESP32 hardware RNG
uint32_t rand_values[4];
for (int i = 0; i < 4; i++) {
rand_values[i] = esp_random();
}
// Convert to hex string
std::ostringstream oss;
oss << std::hex << std::setfill('0');
for (int i = 0; i < 4; i++) {
oss << std::setw(8) << rand_values[i];
}
return oss.str();
}
esp_err_t WebHandler::register_web_endpoints_() {
if (!web_server_ || !web_server_->is_running()) {
return ESP_FAIL;
}
// GET / - Serve settings page
httpd_uri_t settings_page_uri = {
.uri = "/",
.method = HTTP_GET,
.handler = settings_page_handler_,
.user_ctx = this
};
web_server_->register_uri_handler(&settings_page_uri);
// POST /save - Save settings
httpd_uri_t save_settings_uri = {
.uri = "/save",
.method = HTTP_POST,
.handler = save_settings_handler_,
.user_ctx = this
};
web_server_->register_uri_handler(&save_settings_uri);
// POST /test - Test connection
httpd_uri_t test_connection_uri = {
.uri = "/test",
.method = HTTP_POST,
.handler = test_connection_handler_,
.user_ctx = this
};
web_server_->register_uri_handler(&test_connection_uri);
return ESP_OK;
}
esp_err_t WebHandler::settings_page_handler_(httpd_req_t* req) {
WebHandler* self = static_cast<WebHandler*>(req->user_ctx);
// Validate auth
size_t query_len = httpd_req_get_url_query_len(req);
if (query_len == 0) {
httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
return ESP_FAIL;
}
char* query = new char[query_len + 1];
if (httpd_req_get_url_query_str(req, query, query_len + 1) != ESP_OK) {
delete[] query;
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Bad Request");
return ESP_FAIL;
}
if (!self->web_server_->validate_auth(query)) {
delete[] query;
httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
return ESP_FAIL;
}
delete[] query;
// Get current settings (access private members via friend)
std::string current_ip = self->setting_handler_->get_remote_ip();
uint16_t current_port = self->setting_handler_->get_remote_port();
uint16_t current_local_port = self->setting_handler_->get_local_port();
// Build HTML page
std::ostringstream html;
html << "<!DOCTYPE html><html><head>"
<< "<meta name='viewport' content='width=device-width, initial-scale=1'>"
<< "<title>Discord Bridge Settings</title>"
<< "<style>"
<< "body{font-family:Arial,sans-serif;max-width:600px;margin:50px auto;padding:20px;}"
<< "h1{color:#333;}"
<< "label{display:block;margin-top:15px;font-weight:bold;}"
<< "input{width:100%;padding:10px;margin-top:5px;box-sizing:border-box;font-size:16px;}"
<< "button{width:100%;padding:12px;margin-top:20px;font-size:16px;cursor:pointer;}"
<< ".btn-primary{background:#4CAF50;color:white;border:none;}"
<< ".btn-secondary{background:#008CBA;color:white;border:none;}"
<< "#result{margin-top:20px;padding:10px;border-radius:5px;display:none;}"
<< ".success{background:#d4edda;color:#155724;border:1px solid #c3e6cb;}"
<< ".error{background:#f8d7da;color:#721c24;border:1px solid #f5c6cb;}"
<< "</style></head><body>"
<< "<h1>Discord Bridge Settings</h1>"
<< "<form id='settingsForm'>"
<< "<label for='ip'>Bridge IP Address:</label>"
<< "<input type='text' id='ip' name='ip' placeholder='e.g., 192.168.1.100' value='" << current_ip << "' required>"
<< "<label for='port'>Bridge Port:</label>"
<< "<input type='number' id='port' name='port' placeholder='e.g., 4211' value='" << current_port << "' required min='1' max='65535'>"
<< "<label for='localPort'>ESP32 Local Port:</label>"
<< "<input type='number' id='localPort' name='localPort' placeholder='e.g., 4212' value='" << current_local_port << "' required min='1' max='65535'>"
<< "<button type='button' class='btn-secondary' onclick='testConnection()'>Test Connection</button>"
<< "<button type='submit' class='btn-primary'>Save Settings</button>"
<< "</form>"
<< "<div id='result'></div>"
<< "<script>"
<< "const form=document.getElementById('settingsForm');"
<< "const result=document.getElementById('result');"
<< "function showResult(msg,isSuccess){"
<< "result.textContent=msg;"
<< "result.className=isSuccess?'success':'error';"
<< "result.style.display='block';"
<< "}"
<< "function testConnection(){"
<< "const ip=document.getElementById('ip').value;"
<< "const port=document.getElementById('port').value;"
<< "const localPort=document.getElementById('localPort').value;"
<< "if(!ip||!port||!localPort){showResult('Please fill all fields',false);return;}"
<< "showResult('Testing connection...',false);"
<< "fetch('/test',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},"
<< "body:'ip='+encodeURIComponent(ip)+'&port='+encodeURIComponent(port)+'&localPort='+encodeURIComponent(localPort)})"
<< ".then(r=>r.json()).then(data=>showResult(data.message,data.success))"
<< ".catch(()=>showResult('Request failed',false));"
<< "}"
<< "form.addEventListener('submit',function(e){"
<< "e.preventDefault();"
<< "const ip=document.getElementById('ip').value;"
<< "const port=document.getElementById('port').value;"
<< "const localPort=document.getElementById('localPort').value;"
<< "if(!ip||!port||!localPort){showResult('Please fill all fields',false);return;}"
<< "fetch('/save',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},"
<< "body:'ip='+encodeURIComponent(ip)+'&port='+encodeURIComponent(port)+'&localPort='+encodeURIComponent(localPort)})"
<< ".then(r=>r.json()).then(data=>{showResult(data.message,data.success);"
<< "if(data.success)setTimeout(()=>result.style.display='none',3000);})"
<< ".catch(()=>showResult('Request failed',false));"
<< "});"
<< "</script></body></html>";
std::string html_str = html.str();
httpd_resp_set_type(req, "text/html");
httpd_resp_send(req, html_str.c_str(), html_str.length());
return ESP_OK;
}
esp_err_t WebHandler::save_settings_handler_(httpd_req_t* req) {
WebHandler* self = static_cast<WebHandler*>(req->user_ctx);
// Read POST data
char* buf = new char[req->content_len + 1];
int ret = httpd_req_recv(req, buf, req->content_len);
if (ret <= 0) {
delete[] buf;
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to read request");
return ESP_FAIL;
}
buf[ret] = '\0';
// Parse form data
char ip[64] = { 0 };
char port_str[8] = { 0 };
char local_port_str[8] = { 0 };
httpd_query_key_value(buf, "ip", ip, sizeof(ip));
httpd_query_key_value(buf, "port", port_str, sizeof(port_str));
httpd_query_key_value(buf, "localPort", local_port_str, sizeof(local_port_str));
delete[] buf;
if (strlen(ip) == 0 || strlen(port_str) == 0 || strlen(local_port_str) == 0) {
const char* resp = "{\"success\":false,\"message\":\"Missing fields\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
uint16_t port = static_cast<uint16_t>(atoi(port_str));
uint16_t local_port = static_cast<uint16_t>(atoi(local_port_str));
if (port == 0 || local_port == 0) {
const char* resp = "{\"success\":false,\"message\":\"Invalid port\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
// Save settings
if (self && self->setting_handler_) {
self->setting_handler_->save_settings(std::string(ip), port, local_port);
}
const char* resp = "{\"success\":true,\"message\":\"Settings saved successfully\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
ESP_LOGI(TAG, "Settings saved via web interface: %s:%u (local port: %u)", ip, port, local_port);
return ESP_OK;
}
esp_err_t WebHandler::test_connection_handler_(httpd_req_t* req) {
WebHandler* self = static_cast<WebHandler*>(req->user_ctx);
IotDisBridge* bridge = self ? self->bridge_ : nullptr;
// Read POST data
char* buf = new char[req->content_len + 1];
int ret = httpd_req_recv(req, buf, req->content_len);
if (ret <= 0) {
delete[] buf;
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Failed to read request");
return ESP_FAIL;
}
buf[ret] = '\0';
// Parse form data
char ip[64] = { 0 };
char port_str[8] = { 0 };
char local_port_str[8] = { 0 };
httpd_query_key_value(buf, "ip", ip, sizeof(ip));
httpd_query_key_value(buf, "port", port_str, sizeof(port_str));
httpd_query_key_value(buf, "localPort", local_port_str, sizeof(local_port_str));
delete[] buf;
if (strlen(ip) == 0 || strlen(port_str) == 0 || strlen(local_port_str) == 0) {
const char* resp = "{\"success\":false,\"message\":\"Missing fields\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
uint16_t port = static_cast<uint16_t>(atoi(port_str));
uint16_t local_port = static_cast<uint16_t>(atoi(local_port_str));
if (port == 0 || local_port == 0) {
const char* resp = "{\"success\":false,\"message\":\"Invalid port\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
return ESP_OK;
}
// Test connection
bool success = false;
if (bridge) {
success = bridge->test_connection(std::string(ip), port, local_port);
} else {
ESP_LOGE(TAG, "IotDisBridge pointer is null, cannot test connection");
}
const char* resp = success
? "{\"success\":true,\"message\":\"Connection successful!\"}"
: "{\"success\":false,\"message\":\"No response from bridge\"}";
httpd_resp_set_type(req, "application/json");
httpd_resp_send(req, resp, strlen(resp));
ESP_LOGI(TAG, "Connection test via web interface: %s:%u (local port: %u) - %s", ip, port, local_port, success ? "SUCCESS" : "FAILED");
return ESP_OK;
}

View File

@@ -1,81 +0,0 @@
#pragma once
#include "esp_http_server.h"
#include <string>
#include "network/web_server_handler.h"
#include "ui/apps/iotdis/settings/settings_handler.h"
#include "ui/apps/iotdis/bridge/bridge.h"
/**
* @brief HTTP request handlers for Discord Bridge settings web interface
*
* These handlers serve the web configuration page and process
* settings updates and connection tests.
*/
class WebHandler {
public:
WebHandler(
SettingHandler* setting_handler,
IotDisBridge* bridge
) :
web_server_(std::make_unique<WebServerHandler>())
, setting_handler_(setting_handler)
, bridge_(bridge) { }
~WebHandler();
esp_err_t start_web_server();
esp_err_t stop_web_server();
std::string get_url() const;
std::string get_device_ip() const;
uint16_t get_port() const;
bool is_running() const {
return web_server_ && web_server_->is_running();
}
private:
std::string generate_auth_key_();
esp_err_t register_web_endpoints_();
/**
* @brief Serve the main settings configuration page
*
* Validates authentication and serves an HTML form with current settings.
* Requires auth query parameter matching the session key.
*
* @param req HTTP request object
* @return ESP_OK on success
*/
static esp_err_t settings_page_handler_(httpd_req_t* req);
/**
* @brief Save bridge connection settings
*
* Parses POST data containing ip, port, and localPort fields.
* Validates and persists settings to NVS storage.
*
* @param req HTTP request object
* @return ESP_OK on success
*/
static esp_err_t save_settings_handler_(httpd_req_t* req);
/**
* @brief Test connection to Discord bridge
*
* Creates temporary UDP client to test connectivity with provided settings.
* Returns JSON response indicating success or failure.
*
* @param req HTTP request object
* @return ESP_OK on success
*/
static esp_err_t test_connection_handler_(httpd_req_t* req);
std::unique_ptr<WebServerHandler> web_server_;
SettingHandler* setting_handler_ = nullptr; ///< Pointer to settings handler (not owned)
std::string auth_key_;
IotDisBridge* bridge_ = nullptr; ///< Pointer to IotDisBridge (not owned)
};

399
main/ui/apps/mtr_app.cpp Normal file
View File

@@ -0,0 +1,399 @@
#include "apps/mtr_app.h"
#include "external/mtr/arrival.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define TAG "MtrApp"
// Event type for network ready
#define EVENT_NETWORK_READY 1
MtrApp::MtrApp() {
_mtr_handler = std::make_unique<MTRNextTrainHandler>();
}
esp_err_t MtrApp::init(lv_obj_t* container) {
if (!container) {
ESP_LOGE(TAG, "Container is null");
return ESP_ERR_INVALID_ARG;
}
_container = container;
ESP_LOGI(TAG, "Initializing MTR app...");
// Create page stack
_page_stack = std::make_unique<PageStack>(container);
// Load all lines
_all_lines = _mtr_handler->get_lines();
ESP_LOGI(TAG, "Loaded %zu MTR lines", _all_lines.size());
// Build initial line selection page
_page_stack->push([this](lv_obj_t* page) {
this->build_line_selection_page(page);
});
ESP_LOGI(TAG, "MTR app initialized successfully");
return ESP_OK;
}
esp_err_t MtrApp::deinit(void) {
ESP_LOGI(TAG, "Deinitializing MTR app");
// Clear page stack
if (_page_stack) {
_page_stack->clear();
_page_stack.reset();
}
// Clear state
_selected_line_code.clear();
_selected_station_code.clear();
_selected_line_info = nullptr;
_all_lines.clear();
return ESP_OK;
}
std::string MtrApp::get_name(void) const {
return "MTR";
}
bool MtrApp::on_back_button_pressed(void) {
if (_page_stack && _page_stack->depth() > 1) {
_page_stack->pop();
return true; // Handled
}
return false; // Not handled, go back to main menu
}
void MtrApp::handle_event(uint32_t event_type, void* event_data) {
if (event_type == EVENT_NETWORK_READY) {
ESP_LOGI(TAG, "Network ready event received");
}
}
void MtrApp::build_line_selection_page(lv_obj_t* page_container) {
ESP_LOGI(TAG, "Building line selection page");
// Title
lv_obj_t* title = lv_label_create(page_container);
lv_label_set_text(title, "選擇路綫 Select Line");
lv_obj_set_style_text_color(title, lv_color_black(), 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);
// Scrollable container for line buttons
lv_obj_t* scroll_container = lv_obj_create(page_container);
lv_obj_set_size(scroll_container, lv_pct(95), lv_pct(85));
lv_obj_align(scroll_container, LV_ALIGN_TOP_MID, 0, 40);
lv_obj_set_flex_flow(scroll_container, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(scroll_container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(scroll_container, 5, 0);
lv_obj_set_style_pad_row(scroll_container, 8, 0);
// Create button for each line
for (size_t i = 0; i < _all_lines.size(); i++) {
LineInfo* line = &_all_lines[i];
lv_obj_t* btn = lv_btn_create(scroll_container);
lv_obj_set_size(btn, lv_pct(95), 60);
// Set button color based on line color
uint32_t color = parse_color_hex(line->color());
lv_obj_set_style_bg_color(btn, lv_color_hex(color), 0);
// Button label
lv_obj_t* label = lv_label_create(btn);
lv_label_set_text_fmt(label, "%s", line->code());
lv_obj_set_style_text_color(label, lv_color_white(), 0);
lv_obj_center(label);
// Store line pointer in user data
lv_obj_add_event_cb(btn, line_button_event_cb, LV_EVENT_CLICKED, this);
lv_obj_set_user_data(btn, (void*)line);
}
ESP_LOGI(TAG, "Created %zu line buttons", _all_lines.size());
}
void MtrApp::build_station_selection_page(lv_obj_t* page_container) {
ESP_LOGI(TAG, "Building station selection page for line: %s", _selected_line_code.c_str());
if (!_selected_line_info) {
ESP_LOGE(TAG, "No line info selected");
return;
}
// Title with line code
lv_obj_t* title = lv_label_create(page_container);
lv_label_set_text_fmt(title, "%s 路綫車站", _selected_line_code.c_str());
lv_obj_set_style_text_color(title, lv_color_black(), 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);
// Scrollable container for station buttons
lv_obj_t* scroll_container = lv_obj_create(page_container);
lv_obj_set_size(scroll_container, lv_pct(95), lv_pct(85));
lv_obj_align(scroll_container, LV_ALIGN_TOP_MID, 0, 40);
lv_obj_set_flex_flow(scroll_container, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(scroll_container, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(scroll_container, 5, 0);
lv_obj_set_style_pad_row(scroll_container, 6, 0);
// Create button for each station
const std::vector<StationInfo>* stations = _selected_line_info->stations();
for (size_t i = 0; i < stations->size(); i++) {
const StationInfo* station = &(*stations)[i];
lv_obj_t* btn = lv_btn_create(scroll_container);
lv_obj_set_size(btn, lv_pct(95), 50);
lv_obj_set_style_bg_color(btn, lv_color_hex(0x4CAF50), 0);
// Button label with station name and code
lv_obj_t* label = lv_label_create(btn);
lv_label_set_text_fmt(label, "%s (%s)", station->name(), station->code());
lv_obj_set_style_text_color(label, lv_color_white(), 0);
lv_obj_center(label);
// Store station pointer in user data
lv_obj_add_event_cb(btn, station_button_event_cb, LV_EVENT_CLICKED, this);
lv_obj_set_user_data(btn, (void*)station);
}
ESP_LOGI(TAG, "Created %zu station buttons", stations->size());
}
void MtrApp::build_arrival_page(lv_obj_t* page_container) {
ESP_LOGI(TAG, "Building arrival page");
// Title
lv_obj_t* title = lv_label_create(page_container);
lv_label_set_text_fmt(title, "%s - %s", _selected_line_code.c_str(), _selected_station_code.c_str());
lv_obj_set_style_text_color(title, lv_color_black(), 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);
// Loading message
lv_obj_t* loading_label = lv_label_create(page_container);
lv_label_set_text(loading_label, "載入中... Loading...");
lv_obj_set_style_text_color(loading_label, lv_color_black(), 0);
lv_obj_center(loading_label);
// Refresh button
lv_obj_t* refresh_btn = lv_btn_create(page_container);
lv_obj_set_size(refresh_btn, 120, 50);
lv_obj_align(refresh_btn, LV_ALIGN_BOTTOM_MID, 0, -10);
lv_obj_add_event_cb(refresh_btn, refresh_button_event_cb, LV_EVENT_CLICKED, this);
lv_obj_t* refresh_label = lv_label_create(refresh_btn);
lv_label_set_text(refresh_label, LV_SYMBOL_REFRESH " 重新整理");
lv_obj_set_style_text_color(refresh_label, lv_color_white(), 0);
lv_obj_center(refresh_label);
// Load arrival data asynchronously
load_arrival_data(page_container);
}
void MtrApp::load_arrival_data(lv_obj_t* page_container) {
if (!_network_handler) {
ESP_LOGW(TAG, "Network handler not set, cannot fetch arrival data");
// Update UI to show error
lv_obj_t* error_label = lv_label_create(page_container);
lv_label_set_text(error_label, "網絡未就緒\nNetwork not ready");
lv_obj_set_style_text_color(error_label, lv_color_black(), 0);
lv_obj_align(error_label, LV_ALIGN_CENTER, 0, -30);
return;
}
ESP_LOGI(TAG, "Fetching arrival data for %s/%s", _selected_line_code.c_str(), _selected_station_code.c_str());
StationArrivalInfo* arrival_info = nullptr;
MtrArrivalErrorCode error_code = _mtr_handler->get_next_arrival_info(
_network_handler,
_selected_line_code,
_selected_station_code,
arrival_info,
Language::TC
);
// Clear loading message
lv_obj_clean(page_container);
// Recreate title
lv_obj_t* title = lv_label_create(page_container);
lv_label_set_text_fmt(title, "%s - %s", _selected_line_code.c_str(), _selected_station_code.c_str());
lv_obj_set_style_text_color(title, lv_color_black(), 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);
if (error_code != MtrArrivalErrorCode::NONE || !arrival_info) {
ESP_LOGE(TAG, "Failed to fetch arrival info, error code: %d", (int)error_code);
lv_obj_t* error_label = lv_label_create(page_container);
lv_label_set_text(error_label, "無法取得班次資料\nFailed to fetch arrival data");
lv_obj_set_style_text_color(error_label, lv_color_black(), 0);
lv_obj_center(error_label);
return;
}
// Create scrollable container for arrivals
lv_obj_t* scroll_container = lv_obj_create(page_container);
lv_obj_set_size(scroll_container, lv_pct(95), lv_pct(75));
lv_obj_align(scroll_container, LV_ALIGN_TOP_MID, 0, 45);
lv_obj_set_style_pad_all(scroll_container, 10, 0);
int y_offset = 0;
// Display UP direction trains
lv_obj_t* up_header = lv_label_create(scroll_container);
lv_label_set_text(up_header, "上行 UP:");
lv_obj_set_style_text_color(up_header, lv_color_black(), 0);
lv_obj_set_pos(up_header, 0, y_offset);
y_offset += 30;
const std::vector<ArrivalInfo>* up_arrivals = arrival_info->up_arrivals();
if (up_arrivals->empty()) {
lv_obj_t* no_train = lv_label_create(scroll_container);
lv_label_set_text(no_train, " 暫無班次 No trains");
lv_obj_set_style_text_color(no_train, lv_color_hex(0x666666), 0);
lv_obj_set_pos(no_train, 10, y_offset);
y_offset += 25;
} else {
for (const auto& arrival : *up_arrivals) {
lv_obj_t* arrival_label = lv_label_create(scroll_container);
lv_label_set_text_fmt(arrival_label, " %s → %s", arrival.arrival_time(), arrival.destination());
lv_obj_set_style_text_color(arrival_label, lv_color_black(), 0);
lv_obj_set_pos(arrival_label, 10, y_offset);
y_offset += 25;
}
}
y_offset += 10;
// Display DOWN direction trains
lv_obj_t* down_header = lv_label_create(scroll_container);
lv_label_set_text(down_header, "下行 DOWN:");
lv_obj_set_style_text_color(down_header, lv_color_black(), 0);
lv_obj_set_pos(down_header, 0, y_offset);
y_offset += 30;
const std::vector<ArrivalInfo>* down_arrivals = arrival_info->down_arrivals();
if (down_arrivals->empty()) {
lv_obj_t* no_train = lv_label_create(scroll_container);
lv_label_set_text(no_train, " 暫無班次 No trains");
lv_obj_set_style_text_color(no_train, lv_color_hex(0x666666), 0);
lv_obj_set_pos(no_train, 10, y_offset);
y_offset += 25;
} else {
for (const auto& arrival : *down_arrivals) {
lv_obj_t* arrival_label = lv_label_create(scroll_container);
lv_label_set_text_fmt(arrival_label, " %s → %s", arrival.arrival_time(), arrival.destination());
lv_obj_set_style_text_color(arrival_label, lv_color_black(), 0);
lv_obj_set_pos(arrival_label, 10, y_offset);
y_offset += 25;
}
}
// Clean up
if (arrival_info != nullptr) {
delete arrival_info;
}
// Refresh button
lv_obj_t* refresh_btn = lv_btn_create(page_container);
lv_obj_set_size(refresh_btn, 120, 50);
lv_obj_align(refresh_btn, LV_ALIGN_BOTTOM_MID, 0, -10);
lv_obj_add_event_cb(refresh_btn, refresh_button_event_cb, LV_EVENT_CLICKED, this);
lv_obj_t* refresh_label = lv_label_create(refresh_btn);
lv_label_set_text(refresh_label, LV_SYMBOL_REFRESH " 重新整理");
lv_obj_set_style_text_color(refresh_label, lv_color_white(), 0);
lv_obj_center(refresh_label);
ESP_LOGI(TAG, "Arrival data displayed successfully");
}
uint32_t MtrApp::parse_color_hex(const char* hex_str) {
if (!hex_str || hex_str[0] != '#') {
return 0x808080; // Default gray
}
// Skip the '#' character
hex_str++;
uint32_t color = 0;
sscanf(hex_str, "%" SCNx32, &color);
return color;
}
void MtrApp::line_button_event_cb(lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
MtrApp* app = (MtrApp*)lv_event_get_user_data(e);
lv_obj_t* btn = (lv_obj_t*)lv_event_get_target(e);
LineInfo* line = (LineInfo*)lv_obj_get_user_data(btn);
if (app && line) {
ESP_LOGI(TAG, "Line selected: %s", line->code());
app->_selected_line_code = line->code();
app->_selected_line_info = line;
// Push station selection page
app->_page_stack->push([app](lv_obj_t* page) {
app->build_station_selection_page(page);
});
}
}
}
void MtrApp::station_button_event_cb(lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
MtrApp* app = (MtrApp*)lv_event_get_user_data(e);
lv_obj_t* btn = (lv_obj_t*)lv_event_get_target(e);
const StationInfo* station = (const StationInfo*)lv_obj_get_user_data(btn);
if (app && station) {
ESP_LOGI(TAG, "Station selected: %s (%s)", station->name(), station->code());
app->_selected_station_code = station->code();
// Push arrival page
app->_page_stack->push([app](lv_obj_t* page) {
app->build_arrival_page(page);
});
}
}
}
void MtrApp::refresh_button_event_cb(lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_CLICKED) {
MtrApp* app = (MtrApp*)lv_event_get_user_data(e);
if (app && app->_page_stack && app->_page_stack->current_page()) {
ESP_LOGI(TAG, "Refresh button clicked");
app->load_arrival_data(app->_page_stack->current_page());
}
}
}
// MtrAppDescriptor implementation
MtrApp* MtrAppDescriptor::_app_instance = nullptr;
MtrAppDescriptor::MtrAppDescriptor()
: AppDescriptor("MTR", []() -> UIApp* {
if (!MtrAppDescriptor::_app_instance) {
MtrAppDescriptor::_app_instance = new MtrApp();
}
return MtrAppDescriptor::_app_instance;
}()) {
// Register with AppRegistry
AppRegistry::instance().register_app(this);
ESP_LOGI(TAG, "MtrApp registered with AppRegistry");
}
void MtrAppDescriptor::draw_icon(lv_obj_t* parent) {
// Create MTR icon with train symbol
lv_obj_t* icon_label = lv_label_create(parent);
lv_label_set_text(icon_label, LV_SYMBOL_GPS "\nMTR");
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);
}

71
main/ui/apps/mtr_app.h Normal file
View File

@@ -0,0 +1,71 @@
#pragma once
#include "ui/ui_app.h"
#include "ui/app_registry.h"
#include "ui/page_stack.h"
#include "external/mtr/mtr.h"
#include "external/mtr/line_info.h"
#include "external/mtr/station_info.h"
#include "network/network.h"
#include <memory>
#include <string>
/**
* @brief MTR Next Train application
*
* Provides multi-page navigation for:
* 1. Line selection - choose MTR line
* 2. Station selection - choose station within selected line
* 3. Arrival display - show real-time train arrival information
*/
class MtrApp : public UIApp {
public:
MtrApp();
virtual ~MtrApp() = default;
esp_err_t init(lv_obj_t* container) override;
esp_err_t deinit(void) override;
std::string get_name(void) const override;
bool on_back_button_pressed(void) override;
void handle_event(uint32_t event_type, void* event_data) override;
// Set network handler (must be called before using app)
void set_network_handler(NetworkHandler* handler) { _network_handler = handler; }
private:
std::unique_ptr<MTRNextTrainHandler> _mtr_handler;
std::unique_ptr<PageStack> _page_stack;
NetworkHandler* _network_handler = nullptr;
// Current selection state
std::string _selected_line_code;
std::string _selected_station_code;
LineInfo* _selected_line_info = nullptr;
std::vector<LineInfo> _all_lines;
// Page builders
void build_line_selection_page(lv_obj_t* page_container);
void build_station_selection_page(lv_obj_t* page_container);
void build_arrival_page(lv_obj_t* page_container);
// Event handlers
static void line_button_event_cb(lv_event_t* e);
static void station_button_event_cb(lv_event_t* e);
static void refresh_button_event_cb(lv_event_t* e);
// Helper functions
void load_arrival_data(lv_obj_t* page_container);
uint32_t parse_color_hex(const char* hex_str);
};
/**
* @brief AppDescriptor for MtrApp
*/
class MtrAppDescriptor : public AppDescriptor {
public:
MtrAppDescriptor();
void draw_icon(lv_obj_t* parent) override;
private:
static MtrApp* _app_instance;
};

View File

@@ -1,9 +0,0 @@
#include "ui/apps/registry.h"
#include "ui/apps/iotdis/descriptor.h"
esp_err_t AppRegistry::init(void) {
register_app(std::make_unique<IotDisDescriptor>());
return ESP_OK;
}

View File

@@ -1,59 +0,0 @@
#pragma once
#include "ui/apps/app.h"
#include <map>
#include <string>
#include "esp_log.h"
#include <memory>
class AppRegistry {
public:
static AppRegistry& instance() {
static AppRegistry registry;
return registry;
}
/**
* @brief Initialize the app registry with built-in apps
*
*/
esp_err_t init(void);
void register_app(std::unique_ptr<AppDescriptor> app_descriptor) {
if (app_descriptors_.find(app_descriptor->get_name()) != app_descriptors_.end()) {
// App already registered
ESP_LOGW("AppRegistry", "App '%s' is already registered", app_descriptor->get_name().c_str());
return;
}
app_descriptors_.emplace(app_descriptor->get_name(), std::move(app_descriptor));
}
size_t size() const {
return app_descriptors_.size();
}
// iterators to access registered apps
auto begin() { return app_descriptors_.begin(); }
auto begin() const { return app_descriptors_.begin(); }
auto end() { return app_descriptors_.end(); }
auto end() const { return app_descriptors_.end(); }
// [] operator to get app by name
AppDescriptor* operator[](const std::string& name) {
auto it = app_descriptors_.find(name);
if (it != app_descriptors_.end()) {
return it->second.get();
}
return nullptr;
}
private:
std::map<std::string, std::unique_ptr<AppDescriptor>> app_descriptors_ = {};
AppRegistry() = default;
// Disable copy and move semantics
AppRegistry(const AppRegistry&) = delete;
AppRegistry& operator=(const AppRegistry&) = delete;
AppRegistry(AppRegistry&&) = delete;
AppRegistry& operator=(AppRegistry&&) = delete;
};

View File

@@ -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);
}

View File

@@ -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;
};

View File

@@ -1,4 +0,0 @@
#include "events.h"
// Define the event base
ESP_EVENT_DEFINE_BASE(UI_EVENT_BASE);

View File

@@ -1,15 +0,0 @@
#pragma once
#include "esp_event.h"
#include "lvgl.h"
ESP_EVENT_DECLARE_BASE(UI_EVENT_BASE);
struct KeyboardEventData {
lv_obj_t* textarea; ///< The textarea that triggered the keyboard event, nullptr if not applicable or for hide event
};
enum EventId {
UI_EVENT_KEYBOARD_SHOWN = 1, ///< Event ID for keyboard shown event
UI_EVENT_KEYBOARD_HIDDEN = 2 ///< Event ID for keyboard hidden event
};

View File

@@ -1,179 +0,0 @@
#include "ui/interaction_handler.h"
#include "ui/events.h"
#include "esp_err.h"
#include "esp_log.h"
#define TAG "InteractionHandler"
InteractionHandler::~InteractionHandler() {
esp_err_t err = deinit();
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error during InteractionHandler deinit: %s", esp_err_to_name(err));
}
}
esp_err_t InteractionHandler::init(lv_obj_t* parent_container) {
if (!parent_container) {
ESP_LOGE(TAG, "Invalid argument: parent_container is nullptr");
return ESP_ERR_INVALID_ARG;
}
parent_container_ = parent_container;
keyboard_ = lv_keyboard_create(parent_container_);
if (!keyboard_) {
ESP_LOGE(TAG, "Failed to create keyboard object");
return ESP_ERR_NO_MEM;
}
ESP_LOGI(TAG, "Keyboard created successfully at %p", keyboard_);
lv_obj_add_flag(keyboard_, LV_OBJ_FLAG_HIDDEN); // start hidden
lv_obj_add_event_cb(
keyboard_,
[](lv_event_t* e) {
InteractionHandler* handler = static_cast<InteractionHandler*>(lv_event_get_user_data(e));
handler->on_keyboard_event_(e);
}
, LV_EVENT_ALL, this);
return ESP_OK;
}
esp_err_t InteractionHandler::deinit(void) {
if (keyboard_) {
lv_obj_del(keyboard_);
keyboard_ = nullptr;
}
return ESP_OK;
}
esp_err_t InteractionHandler::register_text_area_keyboard_support(lv_obj_t* text_area) {
if (!text_area) {
ESP_LOGE(TAG, "Invalid argument: text_area is nullptr");
return ESP_ERR_INVALID_ARG;
}
lv_obj_add_event_cb(
text_area,
[](lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(e);
if (code != LV_EVENT_FOCUSED) {
return;
}
InteractionHandler* handler = static_cast<InteractionHandler*>(lv_event_get_user_data(e));
esp_err_t err = handler->show_keyboard_for_textarea_(static_cast<lv_obj_t*>(lv_event_get_target(e)));
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to show keyboard: %s", esp_err_to_name(err));
}
}
, LV_EVENT_FOCUSED, this);
lv_obj_add_event_cb(
text_area,
[](lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(e);
if (code != LV_EVENT_DEFOCUSED) {
return;
}
InteractionHandler* handler = static_cast<InteractionHandler*>(lv_event_get_user_data(e));
esp_err_t err = handler->hide_keyboard_();
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to hide keyboard: %s", esp_err_to_name(err));
}
}
, LV_EVENT_DEFOCUSED, this);
return ESP_OK;
}
//
// Private methods
//
void InteractionHandler::on_keyboard_event_(lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_READY || code == LV_EVENT_CANCEL) {
// Keyboard is cancelled
esp_err_t err = hide_keyboard_();
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to hide keyboard: %s", esp_err_to_name(err));
}
if (focused_textarea_) {
lv_obj_clear_state(focused_textarea_, LV_STATE_FOCUSED);
lv_keyboard_set_textarea(keyboard_, nullptr);
focused_textarea_ = nullptr;
}
}
}
esp_err_t InteractionHandler::show_keyboard_for_textarea_(lv_obj_t* textarea) {
if (!textarea) {
ESP_LOGE(TAG, "Invalid argument: textarea is nullptr");
return ESP_ERR_INVALID_ARG;
}
if (!keyboard_) {
ESP_LOGE(TAG, "Keyboard object is nullptr - was InteractionHandler properly initialized?");
return ESP_ERR_INVALID_STATE;
}
// Verify keyboard object is still valid
if (!lv_obj_is_valid(keyboard_)) {
ESP_LOGE(TAG, "Keyboard object is no longer valid - it may have been deleted");
keyboard_ = nullptr;
return ESP_ERR_INVALID_STATE;
}
ESP_LOGI(TAG, "Showing keyboard for textarea %p", textarea);
focused_textarea_ = textarea;
lv_keyboard_set_textarea(keyboard_, textarea);
lv_obj_clear_flag(keyboard_, LV_OBJ_FLAG_HIDDEN);
// emit keyboard shown event
KeyboardEventData event_data = {
.textarea = textarea
};
esp_err_t err = esp_event_post_to(
NULL,
UI_EVENT_BASE,
UI_EVENT_KEYBOARD_SHOWN,
&event_data,
sizeof(event_data),
portMAX_DELAY
);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to post keyboard shown event: %s", esp_err_to_name(err));
}
return ESP_OK;
}
esp_err_t InteractionHandler::hide_keyboard_(void) {
if (!keyboard_) {
return ESP_ERR_INVALID_STATE;
}
lv_obj_add_flag(keyboard_, LV_OBJ_FLAG_HIDDEN);
// emit keyboard hidden event
KeyboardEventData event_data = {
.textarea = nullptr
};
esp_err_t err = esp_event_post_to(
NULL,
UI_EVENT_BASE,
UI_EVENT_KEYBOARD_HIDDEN,
&event_data,
sizeof(event_data),
portMAX_DELAY
);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to post keyboard hidden event: %s", esp_err_to_name(err));
}
return ESP_OK;
}

View File

@@ -1,71 +0,0 @@
#pragma once
#include "esp_err.h"
#include "lvgl.h"
#include "ui/events.h"
/**
* @brief Interaction Handler - manages user interactions
*
* This class is responsible for handling user inputs
* such as touch events, button presses, and gestures.
* It routes these interactions to the appropriate UI components
* or apps based on the current context. And it also handles the respective UI widgets.
*
* For example, it manages:
* Textarea focus and display of the on-screen keyboard
*/
class InteractionHandler {
public:
InteractionHandler() = default;
~InteractionHandler();
/**
* @brief Initialize the Interaction Handler
*
* Sets up necessary event listeners and state.
*
* @param parent_container Parent container for keyboard (typically the screen)
* @return ESP_OK on success, error code otherwise
*/
esp_err_t init(lv_obj_t* parent_container);
/**
* @brief Deinitialize the Interaction Handler
*
* Cleans up resources and event listeners.
*
* @return ESP_OK on success, error code otherwise
*/
esp_err_t deinit(void);
/**
* @brief Add keyboard support to a textarea widget
*
* @param text_area Pointer to the textarea lvgl object
* @return esp_err_t ESP_OK on success, error code otherwise
*/
esp_err_t register_text_area_keyboard_support(lv_obj_t* text_area);
private:
// Event handler for keyboard show/hide events
// It should be registered with event callbacks of the keyboard object
void on_keyboard_event_(lv_event_t* e);
esp_err_t show_keyboard_for_textarea_(lv_obj_t* textarea);
esp_err_t hide_keyboard_(void);
// Parent container (typically screen), reference only
lv_obj_t* parent_container_ = nullptr;
// owned keyboard object
lv_obj_t* keyboard_ = nullptr;
// Currently focused textarea, reference only
lv_obj_t* focused_textarea_ = nullptr;
InteractionHandler(const InteractionHandler&) = delete;
InteractionHandler& operator=(const InteractionHandler&) = delete;
};

115
main/ui/page_stack.cpp Normal file
View File

@@ -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;
}

86
main/ui/page_stack.h Normal file
View File

@@ -0,0 +1,86 @@
#pragma once
#include "lvgl.h"
#include <vector>
#include <functional>
/**
* @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<void(lv_obj_t* page_container)>;
/**
* @brief Page cleanup callback
* @param page_container The LVGL container being destroyed
*/
using PageCleanup = std::function<void(lv_obj_t* page_container)>;
/**
* @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<Page> pages_;
/**
* @brief Create a page container
*/
lv_obj_t* create_page_container();
};

View File

@@ -1,93 +1,123 @@
#include "ui/root_layout.h"
#include "ui/events.h"
#include "ui/ui_handler.h"
#include "ui/app_registry.h"
#include "esp_log.h"
#include "esp_event.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() {
deinit();
// forward-declare local event callback
static void on_home_button_clicked(lv_event_t* event);
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::init(lv_obj_t* parent, UIHandler* ui_handler) {
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) {
// Configure parent as flexbox column layout
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(parent, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_set_style_pad_all(parent, 0, 0);
lv_obj_set_style_pad_gap(parent, 0, 0);
//
// Create header (top, fixed height)
header_obj_ = lv_obj_create(parent);
lv_obj_set_width(header_obj_, lv_pct(100));
lv_obj_set_height(header_obj_, HEADER_HEIGHT);
lv_obj_set_style_bg_color(header_obj_, lv_color_white(), 0);
lv_obj_set_style_border_width(header_obj_, 0, 0);
lv_obj_set_style_border_color(header_obj_, lv_color_black(), 0);
lv_obj_set_style_border_width(header_obj_, 1, LV_BORDER_SIDE_BOTTOM);
lv_obj_set_style_pad_all(header_obj_, 0, 0);
lv_obj_set_style_radius(header_obj_, 0, 0);
//
header_label_ = lv_label_create(header_obj_);
lv_label_set_text(header_label_, "App");
lv_obj_set_style_text_color(header_label_, lv_color_black(), 0);
lv_obj_align(header_label_, LV_ALIGN_LEFT_MID, 10, 0);
//
// Create app container (middle, flexible height)
app_container_ = lv_obj_create(parent);
lv_obj_set_width(app_container_, lv_pct(100));
lv_obj_set_flex_grow(app_container_, 1);
lv_obj_set_style_bg_color(app_container_, lv_color_white(), 0);
lv_obj_set_style_border_width(app_container_, 0, 0);
lv_obj_set_style_pad_all(app_container_, 0, 0);
lv_obj_set_style_radius(app_container_, 0, 0);
_header = lv_obj_create(parent);
lv_obj_set_width(_header, lv_pct(100));
lv_obj_set_height(_header, HEADER_HEIGHT);
lv_obj_set_style_bg_color(_header, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_border_width(_header, 0, 0);
lv_obj_set_style_border_color(_header, lv_color_hex(0x000000), 0);
lv_obj_set_style_border_width(_header, 1, LV_BORDER_SIDE_BOTTOM);
lv_obj_set_style_pad_all(_header, 0, 0);
lv_obj_set_style_radius(_header, 0, 0);
_header_label = lv_label_create(_header);
lv_label_set_text(_header_label, "App");
lv_obj_set_style_text_color(_header_label, lv_color_black(), 0);
lv_obj_align(_header_label, LV_ALIGN_LEFT_MID, 10, 0);
// Create app container (middle, flexible - grows to fill available space)
_app_container = lv_obj_create(parent);
lv_obj_set_width(_app_container, lv_pct(100));
lv_obj_set_flex_grow(_app_container, 1);
lv_obj_set_style_bg_color(_app_container, lv_color_white(), 0);
lv_obj_set_style_border_width(_app_container, 0, 0);
lv_obj_set_style_pad_all(_app_container, 0, 0);
lv_obj_set_style_radius(_app_container, 0, 0);
//
// Create navigation bar (bottom, fixed height)
nav_bar_obj_ = lv_obj_create(parent);
lv_obj_set_width(nav_bar_obj_, lv_pct(100));
lv_obj_set_height(nav_bar_obj_, NAV_BAR_HEIGHT);
lv_obj_set_style_bg_color(nav_bar_obj_, lv_color_white(), 0);
lv_obj_set_style_border_color(nav_bar_obj_, lv_color_black(), 0);
lv_obj_set_style_border_width(nav_bar_obj_, 1, LV_BORDER_SIDE_TOP);
lv_obj_set_style_pad_all(nav_bar_obj_, 5, 0);
lv_obj_set_style_radius(nav_bar_obj_, 0, 0);
_nav_bar = lv_obj_create(parent);
lv_obj_set_width(_nav_bar, lv_pct(100));
lv_obj_set_height(_nav_bar, NAV_BAR_HEIGHT);
lv_obj_set_style_bg_color(_nav_bar, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_border_color(_nav_bar, lv_color_hex(0x000000), 0);
lv_obj_set_style_border_width(_nav_bar, 1, LV_BORDER_SIDE_TOP);
lv_obj_set_style_pad_all(_nav_bar, 5, 0);
lv_obj_set_style_radius(_nav_bar, 0, 0);
// Configure nav bar as flexbox row layout with space-between
lv_obj_set_flex_flow(_nav_bar, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(_nav_bar, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
// Create back button (aligned to start by flex layout)
back_button_ = lv_btn_create(nav_bar_obj_);
lv_obj_set_size(back_button_, 60, NAV_BAR_HEIGHT - 10);
lv_obj_set_style_bg_color(back_button_, lv_color_white(), 0);
lv_obj_add_flag(back_button_, LV_OBJ_FLAG_HIDDEN);
lv_obj_t* back_label = lv_label_create(back_button_);
_back_button = lv_btn_create(_nav_bar);
lv_obj_set_size(_back_button, 60, NAV_BAR_HEIGHT - 10);
lv_obj_set_style_bg_color(_back_button, lv_color_hex(0x555555), 0);
lv_obj_add_event_cb(_back_button, on_back_button_clicked, LV_EVENT_CLICKED, _ui_handler);
lv_obj_add_flag(_back_button, LV_OBJ_FLAG_HIDDEN);
// Add back arrow label
lv_obj_t* back_label = lv_label_create(_back_button);
lv_label_set_text(back_label, LV_SYMBOL_LEFT);
lv_obj_set_style_text_color(back_label, lv_color_black(), 0);
lv_obj_align(back_label, LV_ALIGN_CENTER, 0, 0);
// Create home button (aligned to end by flex layout)
home_button_ = lv_btn_create(nav_bar_obj_);
lv_obj_set_size(home_button_, 60, NAV_BAR_HEIGHT - 10);
lv_obj_set_style_bg_color(home_button_, lv_color_white(), 0);
lv_obj_t* home_label = lv_label_create(home_button_);
lv_obj_t* home_button = lv_btn_create(_nav_bar);
lv_obj_set_size(home_button, 60, NAV_BAR_HEIGHT - 10);
lv_obj_set_style_bg_color(home_button, lv_color_hex(0x555555), 0);
lv_obj_t* home_label = lv_label_create(home_button);
lv_label_set_text(home_label, LV_SYMBOL_HOME);
lv_obj_set_style_text_color(home_label, lv_color_black(), 0);
lv_obj_set_style_text_color(home_label, lv_color_white(), 0);
lv_obj_align(home_label, LV_ALIGN_CENTER, 0, 0);
// Register keyboard event handler
esp_err_t err = esp_event_handler_instance_register(
UI_EVENT_BASE,
ESP_EVENT_ANY_ID,
[](void* handler_args, esp_event_base_t base, int32_t id, void* event_data) {
RootLayout* root_layout = static_cast<RootLayout*>(handler_args);
root_layout->on_keyboard_event_(handler_args, base, id, event_data);
},
this,
&keyboard_event_handler_instance_
);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to register keyboard event handler: %s", esp_err_to_name(err));
}
lv_obj_add_event_cb(home_button, on_home_button_clicked, LV_EVENT_CLICKED, _ui_handler);
ESP_LOGI(TAG, "Layout created with flexible design: Header=%d, NavBar=%d",
HEADER_HEIGHT, NAV_BAR_HEIGHT);
@@ -95,155 +125,140 @@ esp_err_t RootLayout::init(lv_obj_t* parent, UIHandler* ui_handler) {
return ESP_OK;
}
esp_err_t RootLayout::deinit(void) {
// Unregister keyboard event handler
if (keyboard_event_handler_instance_) {
esp_event_handler_instance_unregister(
UI_EVENT_BASE,
ESP_EVENT_ANY_ID,
keyboard_event_handler_instance_
);
keyboard_event_handler_instance_ = nullptr;
}
header_obj_ = nullptr;
header_label_ = nullptr;
//
app_container_ = nullptr;
//
nav_bar_obj_ = nullptr;
back_button_ = nullptr;
home_button_ = nullptr;
return ESP_OK;
}
void RootLayout::hide_nav_bar(void) const {
if (nav_bar_obj_) {
lv_obj_add_flag(nav_bar_obj_, LV_OBJ_FLAG_HIDDEN);
} else {
ESP_LOGW(TAG, "Navigation bar not initialized");
}
}
void RootLayout::show_nav_bar(void) const {
if (nav_bar_obj_) {
lv_obj_clear_flag(nav_bar_obj_, LV_OBJ_FLAG_HIDDEN);
} else {
ESP_LOGW(TAG, "Navigation bar not initialized");
}
}
void RootLayout::show_back_button(void) const {
if (back_button_) {
lv_obj_clear_flag(back_button_, LV_OBJ_FLAG_HIDDEN);
} else {
ESP_LOGW(TAG, "Back button not initialized");
}
}
void RootLayout::show_home_button(void) const {
if (home_button_) {
lv_obj_clear_flag(home_button_, LV_OBJ_FLAG_HIDDEN);
} else {
ESP_LOGW(TAG, "Home button not found in navigation bar");
}
}
void RootLayout::hide_back_button(void) const {
if (back_button_) {
lv_obj_add_flag(back_button_, LV_OBJ_FLAG_HIDDEN);
} else {
ESP_LOGW(TAG, "Back button not initialized");
}
}
void RootLayout::hide_home_button(void) const {
if (home_button_) {
lv_obj_add_flag(home_button_, LV_OBJ_FLAG_HIDDEN);
} else {
ESP_LOGW(TAG, "Home button not found in navigation bar");
}
}
esp_err_t RootLayout::register_back_button_callback(lv_event_cb_t callback, void* user_data, lv_event_dsc_t** out_event_dsc) const {
if (!back_button_) {
ESP_LOGE(TAG, "Back button not initialized");
return ESP_ERR_INVALID_STATE;
}
if (!callback) {
ESP_LOGE(TAG, "Invalid argument: callback is nullptr");
return ESP_ERR_INVALID_ARG;
}
if (out_event_dsc == nullptr) {
ESP_LOGE(TAG, "Invalid argument: out_event_dsc is nullptr");
return ESP_ERR_INVALID_ARG;
}
*out_event_dsc = lv_obj_add_event_cb(back_button_, callback, LV_EVENT_CLICKED, user_data);
if (*out_event_dsc == nullptr) {
ESP_LOGE(TAG, "Failed to register back button callback");
return ESP_FAIL;
}
return ESP_OK;
}
esp_err_t RootLayout::register_home_button_callback(lv_event_cb_t callback, void* user_data, lv_event_dsc_t** out_event_dsc) const {
if (!home_button_) {
ESP_LOGE(TAG, "Home button not found in navigation bar");
return ESP_ERR_NOT_FOUND;
}
if (!callback) {
ESP_LOGE(TAG, "Invalid argument: callback is nullptr");
return ESP_ERR_INVALID_ARG;
}
if (out_event_dsc == nullptr) {
ESP_LOGE(TAG, "Invalid argument: out_event_dsc is nullptr");
return ESP_ERR_INVALID_ARG;
}
*out_event_dsc = lv_obj_add_event_cb(home_button_, callback, LV_EVENT_CLICKED, user_data);
if (*out_event_dsc == nullptr) {
ESP_LOGE(TAG, "Failed to register home button callback");
return ESP_FAIL;
}
return ESP_OK;
}
esp_err_t RootLayout::update_header(const std::string& title) const {
if (!header_label_) {
return ESP_ERR_INVALID_STATE;
}
if (title.empty() == false) {
lv_label_set_text(header_label_, title.c_str());
} else {
lv_label_set_text(header_label_, "App");
}
return ESP_OK;
}
void RootLayout::on_keyboard_event_(void* handler_args, esp_event_base_t base, int32_t id, void* event_data) {
if (base != UI_EVENT_BASE) {
void RootLayout::update_header(std::string app_name) {
if (!_header_label) {
return;
}
switch (id) {
case UI_EVENT_KEYBOARD_SHOWN:
hide_nav_bar();
break;
case UI_EVENT_KEYBOARD_HIDDEN:
show_nav_bar();
break;
default:
ESP_LOGW(TAG, "Unknown keyboard event ID: %ld", id);
break;
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 app container content (icons are rendered in the app area)
if (!_app_container) {
ESP_LOGE(TAG, "App container not initialized");
return ESP_FAIL;
}
lv_obj_clean(_app_container);
// Get all registered apps from registry
const auto& app_descriptors = AppRegistry::instance().get_app_descriptors();
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", (int)app_descriptors.size());
// Calculate icon spacing inside the app container
int icon_count = app_descriptors.size();
int icon_width = 96;
int icon_height = 96;
int icon_spacing = DISPLAY_WIDTH / (icon_count + 1);
int x_offset = icon_spacing;
int y_offset = (APP_CONTAINER_HEIGHT - icon_height) / 2;
// Render each app icon into the app container
for (size_t i = 0; i < app_descriptors.size(); i++) {
AppDescriptor* descriptor = app_descriptors[i];
lv_obj_t* icon_container = lv_obj_create(_app_container);
lv_obj_set_size(icon_container, icon_width, icon_height);
lv_obj_set_pos(icon_container, x_offset - icon_width / 2, y_offset);
lv_obj_set_style_bg_opa(icon_container, LV_OPA_TRANSP, 0);
lv_obj_set_style_pad_all(icon_container, 0, 0);
// add a border for debugging
lv_obj_set_style_border_color(icon_container, lv_color_hex(0x000000), 0);
lv_obj_set_style_border_width(icon_container, 1, 0);
lv_obj_set_user_data(icon_container, descriptor);
descriptor->draw_icon(icon_container);
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;
}
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) {
// Use the current target (the object the callback was attached to)
// instead of the event target, which may be a child (like a label).
lv_obj_t* icon_container = static_cast<lv_obj_t*>(lv_event_get_current_target(event));
UIHandler* handler = static_cast<UIHandler*>(lv_event_get_user_data(event));
AppDescriptor* descriptor = static_cast<AppDescriptor*>(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<UIHandler*>(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();
}
}
static void on_home_button_clicked(lv_event_t* event) {
UIHandler* handler = static_cast<UIHandler*>(lv_event_get_user_data(event));
if (!handler) {
ESP_LOGE(TAG, "Invalid handler in home button click");
return;
}
handler->return_to_main_screen();
}

View File

@@ -1,126 +1,138 @@
#pragma once
#include "esp_err.h"
#include "esp_event.h"
#include "lvgl.h"
#include "esp_err.h"
#include <string>
// Forward declaration to avoid circular dependency
// 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:
RootLayout() = default;
~RootLayout();
/**
* @brief Construct a new RootLayout object
*
* @param ui_handler Pointer to the UIHandler (for callbacks)
*/
RootLayout(UIHandler* ui_handler);
/**
* @brief Initialize the root layout within the given parent object
* @brief Initialize the layout
*
* Sets up the header, app container, and navigation bar.
* Creates the main screen with header, app container, and navigation bar.
*
* @param parent Parent LVGL object to contain the layout
* @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, UIHandler* ui_handler);
esp_err_t init(lv_obj_t* parent);
/**
* @brief Deinitialize the root layout
* @brief Deinitialize the layout
*
* Cleans up references to layout components.
* Cleans up all layout widgets.
*
* @return ESP_OK on success, error code otherwise
*/
esp_err_t deinit(void);
/**
* @brief Show the back button in the navigation bar
*/
void show_back_button(void) const;
/**
* @brief Hide the back button in the navigation bar
*/
void hide_back_button(void) const;
/**
* @brief Show the home button in the navigation bar
*/
void show_home_button(void) const;
/**
* @brief Hide the home button in the navigation bar
*/
void hide_home_button(void) const;
/**
* @brief Show navigation bar
* @brief Render app icons in the navigation bar
*
*/
void show_nav_bar(void) const;
/**
* @brief Hide navigation bar
* Queries the AppRegistry for all registered apps and
* renders their icons in the navigation bar. Also creates
* the back button.
*
*/
void hide_nav_bar(void) const;
/**
* @brief Register a callback for back button presses
*
*
* @param callback
* @param user_data
* @param out_event_dsc
* @return ESP_OK on success, error code otherwise
*/
esp_err_t register_back_button_callback(lv_event_cb_t callback, void* user_data, lv_event_dsc_t** out_event_dsc) const;
esp_err_t render_app_icons(void);
/**
* @brief Register a callback for home button presses
* @brief Update header with app name
*
* @param callback
* @param user_data
* @param out_event_dsc
* @return ESP_OK on success, error code otherwise
* @param app_name Name to display in header (nullptr for default)
*/
esp_err_t register_home_button_callback(lv_event_cb_t callback, void* user_data, lv_event_dsc_t** out_event_dsc) const;
void update_header(std::string app_name);
/**
* @brief Update the header title text
*
* @param title New title text
* @return ESP_OK on success, error code otherwise
* @brief Show the back button
*/
esp_err_t update_header(const std::string& title) const;
void show_back_button(void);
/**
* @brief Get the app container object, which holds the active app's UI
* Caller can add/remove app UI elements to/from this container.
* Caller must not delete this object directly or edit its layout properties.
*
* @return lv_obj_t*
* @brief Hide the back button
*/
lv_obj_t* get_app_container() const {
return app_container_;
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
// Event handler for keyboard show/hide events
void on_keyboard_event_(void* handler_args, esp_event_base_t base, int32_t id, void* event_data);
/**
* @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);
// layout objects
// header
lv_obj_t* header_obj_ = nullptr; ///< Header area object
lv_obj_t* header_label_ = nullptr; ///< Header title label
// app container
lv_obj_t* app_container_ = nullptr; ///< App container object
// navigation bar
lv_obj_t* nav_bar_obj_ = nullptr; ///< Navigation bar object
lv_obj_t* back_button_ = nullptr; ///< Back button object
lv_obj_t* home_button_ = nullptr; ///< Home button object
/**
* @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);
esp_event_handler_instance_t keyboard_event_handler_instance_ = nullptr; ///< Event handler instance for keyboard events
/**
* @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);
};

View File

@@ -3,10 +3,6 @@
#include "lvgl.h"
#include "esp_err.h"
#include <string>
#include <memory>
// Forward declaration
class InteractionHandler;
/**
* @brief Base class for all UI applications
@@ -28,10 +24,9 @@ public:
* between the header and navigation bar.
*
* @param container LVGL container object for this app
* @param interaction_handler Pointer to interaction handler for keyboard support
* @return ESP_OK on success, error code otherwise
*/
virtual esp_err_t init(lv_obj_t* container, InteractionHandler* interaction_handler) = 0;
virtual esp_err_t init(lv_obj_t* container) = 0;
/**
* @brief Deinitialize and clean up app resources
@@ -53,39 +48,51 @@ public:
virtual std::string get_name(void) const = 0;
/**
* @brief Handle back button press
* @brief Handle system events passed from UIHandler
*
* Called when the back button is pressed.
* The app can choose to handle it (e.g., close a dialog)
* or return false to let UIHandler handle it (e.g., return to main screen).
* System events include network status changes, storage ready,
* display refresh, and other system-level events.
*
* @return true if the event was handled, false otherwise
* @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
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_;
return _name;
}
UIApp* get_app_instance() const {
return app_instance_.get();
return _app_instance;
}
protected:
AppDescriptor(std::string name, std::unique_ptr<UIApp> app_instance)
: name_(name), app_instance_(std::move(app_instance)) { }
AppDescriptor(std::string name, UIApp* app_instance)
: _name(name), _app_instance(app_instance) { }
std::string name_;
std::unique_ptr<UIApp> app_instance_;
};
std::string _name;
UIApp* _app_instance;
};

View File

@@ -1,69 +1,131 @@
#include "ui/ui_handler.h"
#include "ui/apps/registry.h"
#include "ui/root_layout.h"
#include "ui/app_registry.h"
#include "esp_log.h"
#include "lvgl.h"
#define TAG "UIHandler"
struct AppClickUserData {
UIHandler* ui_handler;
std::string app_name;
};
// Display dimensions from constants.h
#define DISPLAY_WIDTH 800
#define DISPLAY_HEIGHT 480
UIHandler::~UIHandler() {
deinit();
}
// 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) {
lv_obj_t* screen = lv_scr_act();
esp_err_t ret = ESP_OK;
ESP_LOGI(TAG, "Initializing UIHandler");
// Create main screen layout
ret = create_main_screen_(screen);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to create main screen layout");
return ret;
// 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;
}
// Initialize InteractionHandler with screen as parent (not app_container)
// so keyboard survives app switches
ret = interaction_handler_.init(screen);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize InteractionHandler");
return ret;
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;
}
// Show the main screen
lv_scr_load(screen);
// Render app icons from registry
if (_root_layout->render_app_icons() != ESP_OK) {
ESP_LOGW(TAG, "Failed to render app icons");
}
return ret;
// Defer screen loading to prevent blocking during initialization
// Use LVGL timer to load screen after allowing watchdog reset
lv_timer_create([](lv_timer_t* timer) {
lv_obj_t* screen = static_cast<lv_obj_t*>(lv_timer_get_user_data(timer));
ESP_LOGI("UIHandler", "Loading main screen via timer");
lv_screen_load(screen);
lv_timer_del(timer);
}, 100, _main_screen); // 100ms delay to allow watchdog reset
ESP_LOGI(TAG, "UIHandler initialized successfully");
return ESP_OK;
}
esp_err_t UIHandler::deinit(void) {
// Deinitialize current app if any
if (active_descriptor_) {
UIApp* app = active_descriptor_->get_app_instance();
if (app) {
esp_err_t ret = app->deinit();
if (ret != ESP_OK) {
ESP_LOGE("UIHandler", "Failed to deinitialize current app");
return ret;
}
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_descriptor_ = nullptr;
_active_app = nullptr;
}
// Destroy main screen layout
esp_err_t ret = destroy_main_screen_();
if (ret != ESP_OK) {
ESP_LOGE("UIHandler", "Failed to destroy main screen layout");
return ret;
// Delete shutdown app if cached
if (_shutdown_app) {
delete _shutdown_app;
_shutdown_app = nullptr;
}
// Deinitialize interaction handler
ret = interaction_handler_.deinit();
if (ret != ESP_OK) {
ESP_LOGE("UIHandler", "Failed to deinitialize InteractionHandler");
return ret;
// 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;
@@ -71,265 +133,76 @@ esp_err_t UIHandler::deinit(void) {
esp_err_t UIHandler::switch_app(AppDescriptor* app_descriptor) {
if (!app_descriptor) {
ESP_LOGE(TAG, "Invalid app descriptor");
ESP_LOGE(TAG, "Cannot switch to null app descriptor");
return ESP_ERR_INVALID_ARG;
}
esp_err_t ret = ESP_OK;
// Deinitialize current app if any
if (active_descriptor_) {
UIApp* current_app = active_descriptor_->get_app_instance();
if (current_app) {
ret = current_app->deinit();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to deinitialize current app");
}
}
UIApp* app = app_descriptor->get_app_instance();
if (!app) {
ESP_LOGE(TAG, "App descriptor has null app instance");
return ESP_ERR_INVALID_ARG;
}
// Clear the app container
lv_obj_t* app_container = root_layout_.get_app_container();
if (app_container) {
lv_obj_clean(app_container);
} else {
ESP_LOGE(TAG, "App container not available");
return ESP_ERR_INVALID_STATE;
}
// Set the new app as active
active_descriptor_ = app_descriptor;
// Initialize the new app
UIApp* new_app = active_descriptor_->get_app_instance();
if (!new_app) {
ESP_LOGE(TAG, "App instance not available");
active_descriptor_ = nullptr;
return ESP_ERR_INVALID_STATE;
}
ret = new_app->init(app_container, &interaction_handler_);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize app: %s", new_app->get_name().c_str());
active_descriptor_ = nullptr;
return ret;
}
// Update header with app name
ret = update_header_title(new_app->get_name());
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Failed to update header title");
}
// Show back button when in an app
root_layout_.show_back_button();
ESP_LOGI(TAG, "Switched to app: %s", new_app->get_name().c_str());
return ESP_OK;
return switch_app(app);
}
esp_err_t UIHandler::show_shutdown_screen(const std::string& message) {
// Deinitialize current app if any
if (active_descriptor_) {
UIApp* app = active_descriptor_->get_app_instance();
if (app) {
app->deinit();
}
active_descriptor_ = nullptr;
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_t* app_container = root_layout_.get_app_container();
if (app_container) {
lv_obj_clean(app_container);
lv_obj_clean(app_container);
// Create a simple shutdown message screen
lv_obj_t* label = lv_label_create(app_container);
if (message.empty()) {
lv_label_set_text(label, "Shutting down...");
} else {
lv_label_set_text(label, message.c_str());
}
lv_obj_set_style_text_font(label, &lv_font_montserrat_14, 0);
lv_obj_center(label);
// 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");
}
// Update header
update_header_title("System");
// Hide navigation buttons
root_layout_.hide_back_button();
root_layout_.hide_home_button();
ESP_LOGI(TAG, "Showing shutdown screen: %s", message.c_str());
return ESP_OK;
}
esp_err_t UIHandler::return_to_main_screen(void) {
esp_err_t ret = ESP_OK;
ESP_LOGI(TAG, "Returning to main screen");
// Deinitialize current app if any
if (active_descriptor_) {
UIApp* app = active_descriptor_->get_app_instance();
if (app) {
ret = app->deinit();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to deinitialize app");
return ret;
}
// Deinit current app
if (_active_app) {
if (_active_app->deinit() != ESP_OK) {
ESP_LOGW(TAG, "Error deinitializing app: %s", _active_app->get_name());
}
active_descriptor_ = nullptr;
_active_app = nullptr;
}
// Clear the app container
lv_obj_t* app_container = root_layout_.get_app_container();
lv_obj_t* app_container = get_app_container();
if (app_container) {
lv_obj_clean(app_container);
// TODO: Display app launcher/home screen with app icons
// For now, just show a placeholder message
lv_obj_t* label = lv_label_create(app_container);
lv_label_set_text(label, "Home Screen\n\nApp icons will go here");
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_center(label);
} else {
ESP_LOGE(TAG, "App container not available");
return ESP_ERR_INVALID_STATE;
}
// Update header
ret = update_header_title("Home");
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Failed to update header title");
// Update header and hide back button through RootLayout
if (_root_layout) {
_root_layout->update_header("");
_root_layout->hide_back_button();
}
// Hide back button on home screen
root_layout_.hide_back_button();
ESP_LOGI(TAG, "Returned to main screen");
return ESP_OK;
}
esp_err_t UIHandler::update_header_title(const std::string& title) {
return root_layout_.update_header(title);
}
//
// Private methods
//
void UIHandler::on_back_button_pressed_(void) {
if (active_descriptor_) {
UIApp* app = active_descriptor_->get_app_instance();
if (app) {
bool handled = app->on_back_button_pressed();
if (!handled) {
// App didn't handle it, return to main screen
return_to_main_screen();
}
}
} else {
ESP_LOGW(TAG, "Back button pressed but no active app");
}
}
esp_err_t UIHandler::create_main_screen_(lv_obj_t* parent) {
esp_err_t ret = ESP_OK;
// Initialize root layout
ret = root_layout_.init(parent, this);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize RootLayout");
return ret;
}
// render all apps
for (const auto& [name, descriptor] : AppRegistry::instance()) {
lv_obj_t* app_icon_container = lv_obj_create(root_layout_.get_app_container());
lv_obj_set_size(app_icon_container, 100, 100);
lv_obj_set_style_pad_all(app_icon_container, 10, 0);
lv_obj_set_flex_flow(app_icon_container, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(app_icon_container, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
// Draw the app icon
descriptor->draw_icon(app_icon_container);
// App name label
lv_obj_t* label = lv_label_create(app_icon_container);
lv_label_set_text(label, name.c_str());
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);
// Center the icon container
lv_obj_center(app_icon_container);
// Register click event to switch to the app
lv_obj_add_event_cb(app_icon_container,
[](lv_event_t* e) {
AppClickUserData* user_data = static_cast<AppClickUserData*>(lv_event_get_user_data(e));
UIHandler* ui_handler = user_data->ui_handler;
std::string app_name = user_data->app_name;
AppDescriptor* descriptor = AppRegistry::instance()[app_name];
if (descriptor) {
ui_handler->switch_app(descriptor);
} else {
ESP_LOGE(TAG, "App descriptor not found for app: %s", app_name.c_str());
}
},
LV_EVENT_CLICKED,
new AppClickUserData { this, name }
);
}
// Register back button callback
lv_event_dsc_t* back_event_dsc = nullptr;
ret = root_layout_.register_back_button_callback(
[](lv_event_t* e) {
UIHandler* ui_handler = static_cast<UIHandler*>(lv_event_get_user_data(e));
ui_handler->on_back_button_pressed_();
},
this,
&back_event_dsc
);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to register back button callback");
return ret;
}
// Register home button callback
lv_event_dsc_t* home_event_dsc = nullptr;
ret = root_layout_.register_home_button_callback(
[](lv_event_t* e) {
UIHandler* ui_handler = static_cast<UIHandler*>(lv_event_get_user_data(e));
ui_handler->return_to_main_screen();
},
this,
&home_event_dsc
);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to register home button callback");
return ret;
}
ESP_LOGI(TAG, "Main screen layout created successfully");
return ESP_OK;
}
esp_err_t UIHandler::destroy_main_screen_(void) {
esp_err_t ret = root_layout_.deinit();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to deinitialize RootLayout");
return ret;
}
ESP_LOGI(TAG, "Main screen layout destroyed successfully");
return ESP_OK;
}

View File

@@ -1,12 +1,12 @@
#pragma once
#include "ui_app.h"
#include "app_registry.h"
#include "root_layout.h"
#include "esp_err.h"
#include "ui/apps/app.h"
#include "ui/events.h"
#include "ui/root_layout.h"
#include "ui/interaction_handler.h"
#include "lvgl.h"
#include <memory>
// Forward declaration
class RootLayout;
/**
* @brief UI Handler - manages app lifecycle and rendering
@@ -20,10 +20,6 @@
*/
class UIHandler {
public:
UIHandler() = default;
~UIHandler();
/**
* @brief Initialize the UI system with default layout
*
@@ -32,10 +28,6 @@ public:
* - App container (middle)
* - Navigation bar (bottom)
*
* And display the main screen.
*
* And initializes the InteractionHandler, callbacks, etc.
*
* @return ESP_OK on success, error code otherwise
*/
esp_err_t init(void);
@@ -50,17 +42,46 @@ public:
esp_err_t deinit(void);
/**
* @brief Switch to a new app by its descriptor
* @brief Switch to a new app
*
* Deinitializes the current app (if any), initializes the new app,
* and updates the display. The descriptor must remain valid in the
* AppRegistry for the lifetime of the app.
* and updates the display.
*
* @param app_descriptor Pointer to the app descriptor (managed by AppRegistry)
* @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
*
@@ -70,7 +91,7 @@ public:
* @param message Optional message to display (e.g., "Shutting down...")
* @return ESP_OK on success, error code otherwise
*/
esp_err_t show_shutdown_screen(const std::string& message = "");
esp_err_t show_shutdown_screen(std::string message = "");
/**
* @brief Get the main screen object
@@ -78,10 +99,35 @@ public:
* @return lv_obj_t* pointer to the main screen
*/
lv_obj_t* get_main_screen(void) const {
return main_screen_;
return _main_screen;
}
esp_err_t update_header_title(const std::string& title);
/**
* @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)
@@ -94,25 +140,8 @@ public:
esp_err_t return_to_main_screen(void);
private:
// Handle back button press, route to active app if any
void on_back_button_pressed_(void);
// Helper to create the main screen layout
esp_err_t create_main_screen_(lv_obj_t* parent);
// Helper to destroy the main screen layout
esp_err_t destroy_main_screen_(void);
// delete copy constructor and assignment operator
// to prevent copying of the UIHandler instance
UIHandler(const UIHandler&) = delete;
UIHandler& operator=(const UIHandler&) = delete;
InteractionHandler interaction_handler_; ///< Manages user interactions
lv_obj_t* main_screen_ = nullptr; ///< Root screen
RootLayout root_layout_; ///< Main screen layout manager
AppDescriptor* active_descriptor_ = nullptr; ///< Currently active app descriptor (managed by AppRegistry)
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
};

View File

@@ -1,10 +0,0 @@
#include "ui/widgets/textarea.h"
lv_obj_t* textarea_create(lv_obj_t* parent) {
lv_obj_t* textarea = lv_textarea_create(parent);
// disable animations for cursor and selection for instant response
lv_obj_set_style_anim_time(textarea, 0, LV_PART_CURSOR | LV_STATE_FOCUSED);
return textarea;
}

View File

@@ -1,4 +0,0 @@
#pragma once
#include "lvgl.h"
lv_obj_t* textarea_create(lv_obj_t* parent);

View File

@@ -1,12 +0,0 @@
# Name, Type, SubType, Offset, Size, Flags
# NVS 256KB
nvs, data, nvs, , 0x40000,
# OTA Data 8KB
otadata, data, ota, , 0x2000,
# PHY Init 4KB
phy_init, data, phy, , 0x1000,
# OTA Partitions 10MB
ota_0, app, ota_0, , 0xA00000,
ota_1, app, ota_1, , 0xA00000,
# LittleFS 11MB
storage, data, littlefs, , 0xB00000,
1 # Name, Type, SubType, Offset, Size, Flags
2 # NVS 256KB
3 nvs, data, nvs, , 0x40000,
4 # OTA Data 8KB
5 otadata, data, ota, , 0x2000,
6 # PHY Init 4KB
7 phy_init, data, phy, , 0x1000,
8 # OTA Partitions 10MB
9 ota_0, app, ota_0, , 0xA00000,
10 ota_1, app, ota_1, , 0xA00000,
11 # LittleFS 11MB
12 storage, data, littlefs, , 0xB00000,

View File

@@ -430,9 +430,9 @@ CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
# end of Recovery Bootloader and Rollback
CONFIG_BOOTLOADER_OFFSET_IN_FLASH=0x0
# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE is not set
CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y
# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_DEBUG is not set
CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_PERF=y
# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_PERF is not set
# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_NONE is not set
#
@@ -469,7 +469,8 @@ CONFIG_BOOTLOADER_LOG_MODE_TEXT=y
# CONFIG_BOOTLOADER_FLASH_DC_AWARE is not set
CONFIG_BOOTLOADER_FLASH_XMC_SUPPORT=y
CONFIG_BOOTLOADER_FLASH_32BIT_ADDR=y
CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_OCTAL_FLASH=y
CONFIG_BOOTLOADER_FLASH_NEEDS_32BIT_FEAT=y
CONFIG_BOOTLOADER_FLASH_NEEDS_32BIT_ADDR_QUAD_FLASH=y
# end of Serial Flash Configurations
CONFIG_BOOTLOADER_VDDSDIO_BOOST_1_9V=y
@@ -551,12 +552,14 @@ CONFIG_BOOT_ROM_LOG_ALWAYS_ON=y
# Serial flasher config
#
# CONFIG_ESPTOOLPY_NO_STUB is not set
CONFIG_ESPTOOLPY_OCT_FLASH=y
# CONFIG_ESPTOOLPY_OCT_FLASH is not set
CONFIG_ESPTOOLPY_FLASH_MODE_AUTO_DETECT=y
CONFIG_ESPTOOLPY_FLASHMODE_OPI=y
# CONFIG_ESPTOOLPY_FLASHMODE_QIO is not set
# CONFIG_ESPTOOLPY_FLASHMODE_QOUT is not set
CONFIG_ESPTOOLPY_FLASHMODE_DIO=y
# CONFIG_ESPTOOLPY_FLASHMODE_DOUT is not set
CONFIG_ESPTOOLPY_FLASH_SAMPLE_MODE_STR=y
# CONFIG_ESPTOOLPY_FLASH_SAMPLE_MODE_DTR is not set
CONFIG_ESPTOOLPY_FLASHMODE="dout"
CONFIG_ESPTOOLPY_FLASHMODE="dio"
# CONFIG_ESPTOOLPY_FLASHFREQ_120M is not set
CONFIG_ESPTOOLPY_FLASHFREQ_80M=y
# CONFIG_ESPTOOLPY_FLASHFREQ_40M is not set
@@ -587,10 +590,10 @@ CONFIG_ESPTOOLPY_MONITOR_BAUD=115200
# CONFIG_PARTITION_TABLE_SINGLE_APP is not set
# CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE is not set
# CONFIG_PARTITION_TABLE_TWO_OTA is not set
# CONFIG_PARTITION_TABLE_TWO_OTA_LARGE is not set
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_TWO_OTA_LARGE=y
# CONFIG_PARTITION_TABLE_CUSTOM is not set
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions_two_ota_large.csv"
CONFIG_PARTITION_TABLE_OFFSET=0x8000
CONFIG_PARTITION_TABLE_MD5=y
# end of Partition Table
@@ -1105,14 +1108,14 @@ CONFIG_SPIRAM_SPEED=80
CONFIG_SPIRAM_BOOT_HW_INIT=y
CONFIG_SPIRAM_BOOT_INIT=y
CONFIG_SPIRAM_PRE_CONFIGURE_MEMORY_PROTECTION=y
# CONFIG_SPIRAM_IGNORE_NOTFOUND is not set
CONFIG_SPIRAM_IGNORE_NOTFOUND=y
# CONFIG_SPIRAM_USE_MEMMAP is not set
# CONFIG_SPIRAM_USE_CAPS_ALLOC is not set
CONFIG_SPIRAM_USE_MALLOC=y
CONFIG_SPIRAM_MEMTEST=y
# CONFIG_SPIRAM_MEMTEST is not set
CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=16384
CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y
CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=65536
CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=32768
# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY is not set
# CONFIG_SPIRAM_ALLOW_NOINIT_SEG_EXTERNAL_MEMORY is not set
# end of SPI RAM config
@@ -1266,9 +1269,9 @@ CONFIG_ESP_WIFI_ENABLED=y
CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=10
CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=32
CONFIG_ESP_WIFI_STATIC_TX_BUFFER=y
# CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER is not set
CONFIG_ESP_WIFI_TX_BUFFER_TYPE=0
CONFIG_ESP_WIFI_STATIC_TX_BUFFER_NUM=16
CONFIG_ESP_WIFI_CACHE_TX_BUFFER_NUM=32
CONFIG_ESP_WIFI_STATIC_RX_MGMT_BUFFER=y
# CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUFFER is not set
CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUF=0
@@ -1278,15 +1281,14 @@ CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=y
CONFIG_ESP_WIFI_TX_BA_WIN=6
CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=y
CONFIG_ESP_WIFI_RX_BA_WIN=6
# CONFIG_ESP_WIFI_AMSDU_TX_ENABLED is not set
CONFIG_ESP_WIFI_NVS_ENABLED=y
# CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_0 is not set
CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_1=y
CONFIG_ESP_WIFI_SOFTAP_BEACON_MAX_LEN=752
CONFIG_ESP_WIFI_MGMT_SBUF_NUM=32
# CONFIG_ESP_WIFI_IRAM_OPT is not set
CONFIG_ESP_WIFI_IRAM_OPT=y
# CONFIG_ESP_WIFI_EXTRA_IRAM_OPT is not set
# CONFIG_ESP_WIFI_RX_IRAM_OPT is not set
CONFIG_ESP_WIFI_RX_IRAM_OPT=y
CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y
CONFIG_ESP_WIFI_ENABLE_SAE_PK=y
CONFIG_ESP_WIFI_ENABLE_SAE_H2E=y
@@ -1566,7 +1568,6 @@ CONFIG_LWIP_TCP_OOSEQ_MAX_PBUFS=4
CONFIG_LWIP_TCP_OVERSIZE_MSS=y
# CONFIG_LWIP_TCP_OVERSIZE_QUARTER_MSS is not set
# CONFIG_LWIP_TCP_OVERSIZE_DISABLE is not set
# CONFIG_LWIP_WND_SCALE is not set
CONFIG_LWIP_TCP_RTO_TIME=1500
# end of TCP
@@ -1670,9 +1671,9 @@ CONFIG_LWIP_HOOK_IP6_INPUT_DEFAULT=y
#
# mbedTLS
#
# CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC is not set
CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC=y
# CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC is not set
CONFIG_MBEDTLS_DEFAULT_MEM_ALLOC=y
# CONFIG_MBEDTLS_DEFAULT_MEM_ALLOC is not set
# CONFIG_MBEDTLS_CUSTOM_MEM_ALLOC is not set
CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=y
CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384
@@ -1834,7 +1835,7 @@ CONFIG_STDATOMIC_S32C1I_SPIRAM_WORKAROUND=y
# CONFIG_NVS_ENCRYPTION is not set
# CONFIG_NVS_ASSERT_ERROR_CHECK is not set
# CONFIG_NVS_LEGACY_DUP_KEYS_COMPATIBILITY is not set
CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM=y
# CONFIG_NVS_ALLOCATE_CACHE_IN_SPIRAM is not set
# end of NVS
#
@@ -1876,6 +1877,12 @@ CONFIG_SPI_FLASH_BROWNOUT_RESET=y
#
# Features here require specific hardware (READ DOCS FIRST!)
#
# CONFIG_SPI_FLASH_HPM_ENA is not set
CONFIG_SPI_FLASH_HPM_AUTO=y
# CONFIG_SPI_FLASH_HPM_DIS is not set
CONFIG_SPI_FLASH_HPM_ON=y
CONFIG_SPI_FLASH_HPM_DC_AUTO=y
# CONFIG_SPI_FLASH_HPM_DC_DISABLE is not set
# CONFIG_SPI_FLASH_AUTO_SUSPEND is not set
CONFIG_SPI_FLASH_SUSPEND_TSUS_VAL_US=50
# CONFIG_SPI_FLASH_FORCE_ENABLE_XMC_C_SUSPEND is not set
@@ -1890,6 +1897,7 @@ CONFIG_SPI_FLASH_PLACE_FUNCTIONS_IN_IRAM=y
# CONFIG_SPI_FLASH_VERIFY_WRITE is not set
# CONFIG_SPI_FLASH_ENABLE_COUNTERS is not set
CONFIG_SPI_FLASH_ROM_DRIVER_PATCH=y
# CONFIG_SPI_FLASH_ROM_IMPL is not set
CONFIG_SPI_FLASH_DANGEROUS_WRITE_ABORTS=y
# CONFIG_SPI_FLASH_DANGEROUS_WRITE_FAILS is not set
# CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED is not set
@@ -1969,36 +1977,6 @@ CONFIG_ESP_LCD_TOUCH_MAX_BUTTONS=1
#
# end of ESP LVGL PORT
#
# LittleFS
#
# CONFIG_LITTLEFS_SDMMC_SUPPORT is not set
CONFIG_LITTLEFS_MAX_PARTITIONS=3
CONFIG_LITTLEFS_PAGE_SIZE=256
CONFIG_LITTLEFS_OBJ_NAME_LEN=64
CONFIG_LITTLEFS_READ_SIZE=128
CONFIG_LITTLEFS_WRITE_SIZE=128
CONFIG_LITTLEFS_LOOKAHEAD_SIZE=128
CONFIG_LITTLEFS_CACHE_SIZE=512
CONFIG_LITTLEFS_BLOCK_CYCLES=512
CONFIG_LITTLEFS_USE_MTIME=y
# CONFIG_LITTLEFS_USE_ONLY_HASH is not set
CONFIG_LITTLEFS_HUMAN_READABLE=y
CONFIG_LITTLEFS_MTIME_USE_SECONDS=y
# CONFIG_LITTLEFS_MTIME_USE_NONCE is not set
# CONFIG_LITTLEFS_SPIFFS_COMPAT is not set
# CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE is not set
# CONFIG_LITTLEFS_FCNTL_GET_PATH is not set
# CONFIG_LITTLEFS_MULTIVERSION is not set
# CONFIG_LITTLEFS_MALLOC_STRATEGY_DISABLE is not set
CONFIG_LITTLEFS_MALLOC_STRATEGY_DEFAULT=y
# CONFIG_LITTLEFS_MALLOC_STRATEGY_INTERNAL is not set
# CONFIG_LITTLEFS_MALLOC_STRATEGY_SPIRAM is not set
CONFIG_LITTLEFS_ASSERTS=y
# CONFIG_LITTLEFS_MMAP_PARTITION is not set
# CONFIG_LITTLEFS_WDT_RESET is not set
# end of LittleFS
#
# LVGL configuration
#
@@ -2303,10 +2281,10 @@ CONFIG_LV_USE_WIN=y
#
CONFIG_LV_USE_THEME_DEFAULT=y
# CONFIG_LV_THEME_DEFAULT_DARK is not set
# CONFIG_LV_THEME_DEFAULT_GROW is not set
CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=0
CONFIG_LV_THEME_DEFAULT_GROW=y
CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=80
CONFIG_LV_USE_THEME_SIMPLE=y
CONFIG_LV_USE_THEME_MONO=y
# CONFIG_LV_USE_THEME_MONO is not set
# end of Themes
#
@@ -2338,7 +2316,7 @@ CONFIG_LV_FS_DEFAULT_DRIVER_LETTER=0
# CONFIG_LV_USE_GIF is not set
# CONFIG_LV_BIN_DECODER_RAM_LOAD is not set
# CONFIG_LV_USE_RLE is not set
CONFIG_LV_USE_QRCODE=y
# CONFIG_LV_USE_QRCODE is not set
# CONFIG_LV_USE_BARCODE is not set
# CONFIG_LV_USE_FREETYPE is not set
# CONFIG_LV_USE_TINY_TTF is not set
@@ -2426,8 +2404,11 @@ CONFIG_LOG_BOOTLOADER_LEVEL_INFO=y
# CONFIG_LOG_BOOTLOADER_LEVEL_DEBUG is not set
# CONFIG_LOG_BOOTLOADER_LEVEL_VERBOSE is not set
CONFIG_LOG_BOOTLOADER_LEVEL=3
CONFIG_SPI_FLASH_OCTAL_32BIT_ADDR_ENABLE=y
# CONFIG_FLASH_ENCRYPTION_ENABLED is not set
# CONFIG_FLASHMODE_QIO is not set
# CONFIG_FLASHMODE_QOUT is not set
CONFIG_FLASHMODE_DIO=y
# CONFIG_FLASHMODE_DOUT is not set
CONFIG_MONITOR_BAUD=115200
# CONFIG_OPTIMIZATION_LEVEL_DEBUG is not set
# CONFIG_COMPILER_OPTIMIZATION_LEVEL_DEBUG is not set
@@ -2520,22 +2501,21 @@ CONFIG_ESP32_WIFI_ENABLED=y
CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=10
CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=32
CONFIG_ESP32_WIFI_STATIC_TX_BUFFER=y
# CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER is not set
CONFIG_ESP32_WIFI_TX_BUFFER_TYPE=0
CONFIG_ESP32_WIFI_STATIC_TX_BUFFER_NUM=16
CONFIG_ESP32_WIFI_CACHE_TX_BUFFER_NUM=32
# CONFIG_ESP32_WIFI_CSI_ENABLED is not set
CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED=y
CONFIG_ESP32_WIFI_TX_BA_WIN=6
CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED=y
CONFIG_ESP32_WIFI_RX_BA_WIN=6
# CONFIG_ESP32_WIFI_AMSDU_TX_ENABLED is not set
CONFIG_ESP32_WIFI_NVS_ENABLED=y
# CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_0 is not set
CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_1=y
CONFIG_ESP32_WIFI_SOFTAP_BEACON_MAX_LEN=752
CONFIG_ESP32_WIFI_MGMT_SBUF_NUM=32
# CONFIG_ESP32_WIFI_IRAM_OPT is not set
# CONFIG_ESP32_WIFI_RX_IRAM_OPT is not set
CONFIG_ESP32_WIFI_IRAM_OPT=y
CONFIG_ESP32_WIFI_RX_IRAM_OPT=y
CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y
CONFIG_ESP32_WIFI_ENABLE_WPA3_OWE_STA=y
CONFIG_WPA_MBEDTLS_CRYPTO=y