This commit is contained in:
Thomas Peterson 2026-01-24 18:31:31 +01:00
parent a4cefd7c09
commit df29f89977
18 changed files with 265 additions and 77 deletions

View File

@ -42,7 +42,7 @@ dependencies:
version: 1.1.4 version: 1.1.4
espressif/esp_hosted: espressif/esp_hosted:
component_hash: component_hash:
b6422e810fe97acd87ac184f7da1653e69e885fd209e13c5b1ae20c5787d914d 49424510d8cf3659aa4bcf787e7b4abbf11848a25e7e5f133cf7f3324860d066
dependencies: dependencies:
- name: idf - name: idf
require: private require: private
@ -50,7 +50,7 @@ dependencies:
source: source:
registry_url: https://components.espressif.com registry_url: https://components.espressif.com
type: service type: service
version: 2.11.1 version: 2.11.3
espressif/esp_lcd_touch: espressif/esp_lcd_touch:
component_hash: component_hash:
3f85a7d95af876f1a6ecca8eb90a81614890d0f03a038390804e5a77e2caf862 3f85a7d95af876f1a6ecca8eb90a81614890d0f03a038390804e5a77e2caf862
@ -377,7 +377,7 @@ dependencies:
17e68bfd21f0edf4c3ee838e2273da840bf3930e5dbc3bfa6c1190c3aed41f9f 17e68bfd21f0edf4c3ee838e2273da840bf3930e5dbc3bfa6c1190c3aed41f9f
dependencies: [] dependencies: []
source: source:
registry_url: https://components.espressif.com/ registry_url: https://components.espressif.com
type: service type: service
version: 9.4.0 version: 9.4.0
waveshare/esp_lcd_jd9365_10_1: waveshare/esp_lcd_jd9365_10_1:
@ -408,8 +408,7 @@ direct_dependencies:
- espressif/esp_tinyusb - espressif/esp_tinyusb
- espressif/esp_wifi_remote - espressif/esp_wifi_remote
- idf - idf
- lvgl/lvgl
- waveshare/esp_lcd_jd9365_10_1 - waveshare/esp_lcd_jd9365_10_1
manifest_hash: 77a45d81439c2b8bff313cb08784154731b4451f54eecd31bcd4c2fc83b0f096 manifest_hash: 4224a9627ef40f4e86cf66ea3d5037d4b4dbfca46c368078b42e6988300c7f2f
target: esp32p4 target: esp32p4
version: 2.0.0 version: 2.0.0

View File

@ -1,5 +1,6 @@
#include "Gui.hpp" #include "Gui.hpp"
#include "esp_lv_adapter.h" #include "esp_lv_adapter.h"
#include "esp_log.h"
#include "Gui/WifiSetting.hpp" #include "Gui/WifiSetting.hpp"
#include "Gui/EthSetting.hpp" #include "Gui/EthSetting.hpp"
#include "WidgetManager.hpp" #include "WidgetManager.hpp"
@ -14,15 +15,27 @@ static void screen_long_press_handler(lv_event_t * e)
} }
} }
static void widget_manager_timer_cb(lv_timer_t* timer)
{
(void)timer;
// Debug: Log every 100th call to verify timer is running
static uint32_t callCount = 0;
if (++callCount % 100 == 0) {
ESP_LOGI("Gui", "Timer tick %lu", callCount);
}
WidgetManager::instance().loop();
}
void Gui::create() void Gui::create()
{ {
// Initialize WidgetManager (loads config from SD card) // Initialize WidgetManager (loads config from SD card)
WidgetManager::instance().init(); WidgetManager::instance().init();
if (esp_lv_adapter_lock(-1) == ESP_OK) { if (esp_lv_adapter_lock(-1) == ESP_OK) {
// Add long press handler for settings // TEMP: Disabled long press handler for testing
lv_obj_add_event_cb(lv_scr_act(), screen_long_press_handler, // lv_obj_add_event_cb(lv_scr_act(), screen_long_press_handler,
LV_EVENT_LONG_PRESSED, NULL); // LV_EVENT_LONG_PRESSED, NULL);
lv_timer_create(widget_manager_timer_cb, 10, nullptr);
esp_lv_adapter_unlock(); esp_lv_adapter_unlock();
} }

View File

@ -20,6 +20,7 @@ void EthSetting::show() {
if (esp_lv_adapter_lock(-1) == ESP_OK) { if (esp_lv_adapter_lock(-1) == ESP_OK) {
createUI(); createUI();
visible_ = true; visible_ = true;
esp_lv_adapter_unlock();
} }
} }
@ -64,7 +65,7 @@ void EthSetting::createUI() {
lv_obj_set_size(closeBtn, 40, 40); lv_obj_set_size(closeBtn, 40, 40);
lv_obj_align(closeBtn, LV_ALIGN_LEFT_MID, 0, 0); lv_obj_align(closeBtn, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_bg_color(closeBtn, lv_color_hex(0x804040), 0); lv_obj_set_style_bg_color(closeBtn, lv_color_hex(0x804040), 0);
lv_obj_add_event_cb(closeBtn, onCloseClick, LV_EVENT_CLICKED, nullptr); //lv_obj_add_event_cb(closeBtn, onCloseClick, LV_EVENT_CLICKED, nullptr);
lv_obj_t* closeLbl = lv_label_create(closeBtn); lv_obj_t* closeLbl = lv_label_create(closeBtn);
lv_label_set_text(closeLbl, "X"); lv_label_set_text(closeLbl, "X");

View File

@ -86,7 +86,7 @@ void WifiSetting::createUI() {
lv_obj_set_size(closeBtn, 40, 40); lv_obj_set_size(closeBtn, 40, 40);
lv_obj_align(closeBtn, LV_ALIGN_LEFT_MID, 0, 0); lv_obj_align(closeBtn, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_bg_color(closeBtn, lv_color_hex(0x804040), 0); lv_obj_set_style_bg_color(closeBtn, lv_color_hex(0x804040), 0);
lv_obj_add_event_cb(closeBtn, onCloseClick, LV_EVENT_CLICKED, nullptr); //lv_obj_add_event_cb(closeBtn, onCloseClick, LV_EVENT_CLICKED, nullptr);
lv_obj_t* closeLbl = lv_label_create(closeBtn); lv_obj_t* closeLbl = lv_label_create(closeBtn);
lv_label_set_text(closeLbl, "X"); lv_label_set_text(closeLbl, "X");
@ -111,7 +111,7 @@ void WifiSetting::createUI() {
disconnectBtn_ = lv_btn_create(statusSection); disconnectBtn_ = lv_btn_create(statusSection);
lv_obj_set_size(disconnectBtn_, 100, 40); lv_obj_set_size(disconnectBtn_, 100, 40);
lv_obj_align(disconnectBtn_, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_align(disconnectBtn_, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_event_cb(disconnectBtn_, onDisconnectClick, LV_EVENT_CLICKED, nullptr); //lv_obj_add_event_cb(disconnectBtn_, onDisconnectClick, LV_EVENT_CLICKED, nullptr);
lv_obj_add_flag(disconnectBtn_, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(disconnectBtn_, LV_OBJ_FLAG_HIDDEN);
lv_obj_t* disconnectLbl = lv_label_create(disconnectBtn_); lv_obj_t* disconnectLbl = lv_label_create(disconnectBtn_);
@ -140,7 +140,7 @@ void WifiSetting::createUI() {
lv_obj_t* scanBtn = lv_btn_create(availableHeader); lv_obj_t* scanBtn = lv_btn_create(availableHeader);
lv_obj_set_size(scanBtn, 100, 35); lv_obj_set_size(scanBtn, 100, 35);
lv_obj_align(scanBtn, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_align(scanBtn, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_add_event_cb(scanBtn, onScanClick, LV_EVENT_CLICKED, nullptr); //lv_obj_add_event_cb(scanBtn, onScanClick, LV_EVENT_CLICKED, nullptr);
lv_obj_t* scanLbl = lv_label_create(scanBtn); lv_obj_t* scanLbl = lv_label_create(scanBtn);
lv_label_set_text(scanLbl, "Scannen"); lv_label_set_text(scanLbl, "Scannen");
@ -198,7 +198,7 @@ void WifiSetting::refreshNetworkList() {
(const char*)ap.ssid, barSymbols[bars - 1], ap.rssi); (const char*)ap.ssid, barSymbols[bars - 1], ap.rssi);
lv_obj_t* btn = lv_list_add_btn(settings.networkList_, LV_SYMBOL_WIFI, signalStr); lv_obj_t* btn = lv_list_add_btn(settings.networkList_, LV_SYMBOL_WIFI, signalStr);
lv_obj_add_event_cb(btn, onNetworkSelect, LV_EVENT_CLICKED, nullptr); //lv_obj_add_event_cb(btn, onNetworkSelect, LV_EVENT_CLICKED, nullptr);
size_t ssidLen = strlen((const char*)ap.ssid); size_t ssidLen = strlen((const char*)ap.ssid);
char* ssidCopy = (char*)lv_malloc(ssidLen + 1); char* ssidCopy = (char*)lv_malloc(ssidLen + 1);
@ -237,7 +237,7 @@ void WifiSetting::refreshSavedNetworks() {
lv_obj_set_size(deleteBtn, 80, 30); lv_obj_set_size(deleteBtn, 80, 30);
lv_obj_align(deleteBtn, LV_ALIGN_RIGHT_MID, 0, 0); lv_obj_align(deleteBtn, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_bg_color(deleteBtn, lv_color_hex(0x804040), 0); lv_obj_set_style_bg_color(deleteBtn, lv_color_hex(0x804040), 0);
lv_obj_add_event_cb(deleteBtn, onDeleteSavedClick, LV_EVENT_CLICKED, nullptr); //lv_obj_add_event_cb(deleteBtn, onDeleteSavedClick, LV_EVENT_CLICKED, nullptr);
char* ssidCopy = (char*)lv_malloc(ssid.size() + 1); char* ssidCopy = (char*)lv_malloc(ssid.size() + 1);
strcpy(ssidCopy, ssid.c_str()); strcpy(ssidCopy, ssid.c_str());
@ -312,7 +312,7 @@ void WifiSetting::createPasswordDialogUI() {
lv_obj_set_size(cancelBtn, 150, 50); lv_obj_set_size(cancelBtn, 150, 50);
lv_obj_align(cancelBtn, LV_ALIGN_TOP_LEFT, 20, 150); lv_obj_align(cancelBtn, LV_ALIGN_TOP_LEFT, 20, 150);
lv_obj_set_style_bg_color(cancelBtn, lv_color_hex(0x606060), 0); lv_obj_set_style_bg_color(cancelBtn, lv_color_hex(0x606060), 0);
lv_obj_add_event_cb(cancelBtn, onCancelClick, LV_EVENT_CLICKED, nullptr); //lv_obj_add_event_cb(cancelBtn, onCancelClick, LV_EVENT_CLICKED, nullptr);
lv_obj_t* cancelLbl = lv_label_create(cancelBtn); lv_obj_t* cancelLbl = lv_label_create(cancelBtn);
lv_label_set_text(cancelLbl, "Abbrechen"); lv_label_set_text(cancelLbl, "Abbrechen");
@ -322,7 +322,7 @@ void WifiSetting::createPasswordDialogUI() {
lv_obj_set_size(connectBtn, 150, 50); lv_obj_set_size(connectBtn, 150, 50);
lv_obj_align(connectBtn, LV_ALIGN_TOP_RIGHT, -20, 150); lv_obj_align(connectBtn, LV_ALIGN_TOP_RIGHT, -20, 150);
lv_obj_set_style_bg_color(connectBtn, lv_color_hex(0x408040), 0); lv_obj_set_style_bg_color(connectBtn, lv_color_hex(0x408040), 0);
lv_obj_add_event_cb(connectBtn, onConnectClick, LV_EVENT_CLICKED, nullptr); //lv_obj_add_event_cb(connectBtn, onConnectClick, LV_EVENT_CLICKED, nullptr);
lv_obj_t* connectLbl = lv_label_create(connectBtn); lv_obj_t* connectLbl = lv_label_create(connectBtn);
lv_label_set_text(connectLbl, "Verbinden"); lv_label_set_text(connectLbl, "Verbinden");
@ -332,7 +332,7 @@ void WifiSetting::createPasswordDialogUI() {
lv_obj_set_size(keyboard_, LV_PCT(100), 300); lv_obj_set_size(keyboard_, LV_PCT(100), 300);
lv_obj_align(keyboard_, LV_ALIGN_BOTTOM_MID, 0, 0); lv_obj_align(keyboard_, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_keyboard_set_textarea(keyboard_, passwordInput_); lv_keyboard_set_textarea(keyboard_, passwordInput_);
lv_obj_add_event_cb(keyboard_, onKeyboardReady, LV_EVENT_READY, nullptr); //lv_obj_add_event_cb(keyboard_, onKeyboardReady, LV_EVENT_READY, nullptr);
ESP_LOGI(TAG, "Password dialog created"); ESP_LOGI(TAG, "Password dialog created");
} }

View File

@ -62,6 +62,7 @@ esp_lcd_touch_handle_t Touch::getTouchHandle() const {
void Touch::lv_indev_read_cb(lv_indev_t *indev, lv_indev_data_t *data) void Touch::lv_indev_read_cb(lv_indev_t *indev, lv_indev_data_t *data)
{ {
Touch* self = static_cast<Touch*>(lv_indev_get_user_data(indev)); Touch* self = static_cast<Touch*>(lv_indev_get_user_data(indev));
if (!self) { if (!self) {
data->state = LV_INDEV_STATE_RELEASED; data->state = LV_INDEV_STATE_RELEASED;
@ -76,7 +77,8 @@ void Touch::lv_indev_read_cb(lv_indev_t *indev, lv_indev_data_t *data)
data->point.x = x[0]; data->point.x = x[0];
data->point.y = y[0]; data->point.y = y[0];
data->state = LV_INDEV_STATE_PRESSED; data->state = LV_INDEV_STATE_PRESSED;
WidgetManager::instance().onUserActivity(); // TEMP: Disabled to test if this causes the freeze
// WidgetManager::instance().onUserActivity();
} else { } else {
data->state = LV_INDEV_STATE_RELEASED; data->state = LV_INDEV_STATE_RELEASED;
} }

View File

@ -22,6 +22,10 @@ WidgetManager& WidgetManager::instance() {
WidgetManager::WidgetManager() { WidgetManager::WidgetManager() {
// widgets_ is default-initialized to nullptr // widgets_ is default-initialized to nullptr
uiQueue_ = xQueueCreate(UI_EVENT_QUEUE_LEN, sizeof(UiEvent));
if (!uiQueue_) {
ESP_LOGE(TAG, "Failed to create UI event queue");
}
createDefaultConfig(); createDefaultConfig();
activeScreenId_ = config_.startScreenId; activeScreenId_ = config_.startScreenId;
lastActivityUs_ = esp_timer_get_time(); lastActivityUs_ = esp_timer_get_time();
@ -182,6 +186,8 @@ const ScreenConfig* WidgetManager::activeScreen() const {
} }
void WidgetManager::applyScreen(uint8_t screenId) { void WidgetManager::applyScreen(uint8_t screenId) {
ESP_LOGI(TAG, "applyScreen(%d) - start", screenId);
ScreenConfig* screen = config_.findScreen(screenId); ScreenConfig* screen = config_.findScreen(screenId);
if (!screen) { if (!screen) {
ESP_LOGW(TAG, "Screen %d not found", screenId); ESP_LOGW(TAG, "Screen %d not found", screenId);
@ -189,17 +195,40 @@ void WidgetManager::applyScreen(uint8_t screenId) {
} }
if (modalContainer_) { if (modalContainer_) {
ESP_LOGI(TAG, "Closing modal first");
closeModal(); closeModal();
} }
// First destroy C++ widgets (which deletes LVGL objects) ESP_LOGI(TAG, "Acquiring LVGL lock...");
destroyAllWidgets(); if (esp_lv_adapter_lock(-1) != ESP_OK) {
ESP_LOGE(TAG, "Failed to acquire LVGL lock!");
if (esp_lv_adapter_lock(-1) == ESP_OK) { return;
lv_obj_t* root = lv_scr_act();
createAllWidgets(*screen, root);
esp_lv_adapter_unlock();
} }
ESP_LOGI(TAG, "LVGL lock acquired");
// Reset all input devices BEFORE destroying widgets to clear any
// pending input state and prevent use-after-free on widget objects
ESP_LOGI(TAG, "Resetting input devices...");
lv_indev_t* indev = lv_indev_get_next(nullptr);
while (indev) {
lv_indev_reset(indev, nullptr);
indev = lv_indev_get_next(indev);
}
ESP_LOGI(TAG, "Input devices reset");
// Now destroy C++ widgets (which deletes LVGL objects) under LVGL lock
ESP_LOGI(TAG, "Destroying widgets...");
destroyAllWidgets();
ESP_LOGI(TAG, "Widgets destroyed");
ESP_LOGI(TAG, "Creating new widgets for screen '%s' (%d widgets)...",
screen->name, screen->widgetCount);
lv_obj_t* root = lv_scr_act();
createAllWidgets(*screen, root);
ESP_LOGI(TAG, "Widgets created");
esp_lv_adapter_unlock();
ESP_LOGI(TAG, "applyScreen(%d) - complete", screenId);
} }
void WidgetManager::showModalScreen(const ScreenConfig& screen) { void WidgetManager::showModalScreen(const ScreenConfig& screen) {
@ -207,11 +236,18 @@ void WidgetManager::showModalScreen(const ScreenConfig& screen) {
closeModal(); closeModal();
} }
if (esp_lv_adapter_lock(-1) != ESP_OK) return;
// Reset all input devices BEFORE destroying widgets
lv_indev_t* indev = lv_indev_get_next(nullptr);
while (indev) {
lv_indev_reset(indev, nullptr);
indev = lv_indev_get_next(indev);
}
// Destroy any existing widgets before creating modal widgets // Destroy any existing widgets before creating modal widgets
destroyAllWidgets(); destroyAllWidgets();
if (esp_lv_adapter_lock(-1) != ESP_OK) return;
lv_disp_t* disp = lv_disp_get_default(); lv_disp_t* disp = lv_disp_get_default();
int32_t dispWidth = disp ? lv_disp_get_hor_res(disp) : 1280; int32_t dispWidth = disp ? lv_disp_get_hor_res(disp) : 1280;
int32_t dispHeight = disp ? lv_disp_get_ver_res(disp) : 800; int32_t dispHeight = disp ? lv_disp_get_ver_res(disp) : 800;
@ -299,28 +335,40 @@ void WidgetManager::showModalScreen(const ScreenConfig& screen) {
void WidgetManager::closeModal() { void WidgetManager::closeModal() {
if (!modalContainer_) return; if (!modalContainer_) return;
if (esp_lv_adapter_lock(-1) != ESP_OK) return;
// Reset all input devices BEFORE destroying widgets
lv_indev_t* indev = lv_indev_get_next(nullptr);
while (indev) {
lv_indev_reset(indev, nullptr);
indev = lv_indev_get_next(indev);
}
// First destroy C++ widgets (which deletes their LVGL objects) // First destroy C++ widgets (which deletes their LVGL objects)
destroyAllWidgets(); destroyAllWidgets();
if (esp_lv_adapter_lock(-1) == ESP_OK) { if (modalDimmer_) {
if (modalDimmer_) { lv_obj_delete(modalDimmer_);
lv_obj_delete(modalDimmer_);
}
lv_obj_delete(modalContainer_);
esp_lv_adapter_unlock();
} }
lv_obj_delete(modalContainer_);
esp_lv_adapter_unlock();
modalContainer_ = nullptr; modalContainer_ = nullptr;
modalDimmer_ = nullptr; modalDimmer_ = nullptr;
modalScreenId_ = SCREEN_ID_NONE; modalScreenId_ = SCREEN_ID_NONE;
} }
void WidgetManager::showScreen(uint8_t screenId) { void WidgetManager::showScreen(uint8_t screenId) {
ESP_LOGI(TAG, "showScreen(%d) called", screenId);
ScreenConfig* screen = config_.findScreen(screenId); ScreenConfig* screen = config_.findScreen(screenId);
if (!screen) { if (!screen) {
ESP_LOGW(TAG, "Screen %d not found", screenId); ESP_LOGW(TAG, "Screen %d not found", screenId);
return; return;
} }
ESP_LOGI(TAG, "Found screen '%s', mode=%d", screen->name,
static_cast<int>(screen->mode));
if (screen->mode == ScreenMode::MODAL) { if (screen->mode == ScreenMode::MODAL) {
showModalScreen(*screen); showModalScreen(*screen);
return; return;
@ -335,18 +383,25 @@ void WidgetManager::showScreen(uint8_t screenId) {
void WidgetManager::handleButtonAction(const WidgetConfig& cfg, lv_obj_t* target) { void WidgetManager::handleButtonAction(const WidgetConfig& cfg, lv_obj_t* target) {
if (cfg.type != WidgetType::BUTTON) return; if (cfg.type != WidgetType::BUTTON) return;
ESP_LOGI(TAG, "handleButtonAction: button=%d action=%d targetScreen=%d",
cfg.id, static_cast<int>(cfg.action), cfg.targetScreen);
onUserActivity(); onUserActivity();
switch (cfg.action) { switch (cfg.action) {
case ButtonAction::JUMP: case ButtonAction::JUMP:
navPending_ = true; ESP_LOGI(TAG, "JUMP action: scheduling navigation to screen %d", cfg.targetScreen);
navAction_ = ButtonAction::JUMP; navAction_ = ButtonAction::JUMP;
navTargetScreen_ = cfg.targetScreen; navTargetScreen_ = cfg.targetScreen;
navPending_ = true;
navRequestUs_ = esp_timer_get_time();
break; break;
case ButtonAction::BACK: case ButtonAction::BACK:
navPending_ = true; ESP_LOGI(TAG, "BACK action: scheduling navigation back");
navAction_ = ButtonAction::BACK; navAction_ = ButtonAction::BACK;
navTargetScreen_ = SCREEN_ID_NONE; navTargetScreen_ = SCREEN_ID_NONE;
navPending_ = true;
navRequestUs_ = esp_timer_get_time();
break; break;
case ButtonAction::KNX: case ButtonAction::KNX:
default: { default: {
@ -393,14 +448,22 @@ void WidgetManager::enterStandby() {
} }
void WidgetManager::loop() { void WidgetManager::loop() {
bool didUiNav = false;
if (navPending_) { if (navPending_) {
navPending_ = false; int64_t now = esp_timer_get_time();
if (navAction_ == ButtonAction::JUMP) { // Increased delay to ensure touch events are fully processed
showScreen(navTargetScreen_); if (now - navRequestUs_ >= NAV_DELAY_US) {
} else if (navAction_ == ButtonAction::BACK) { navPending_ = false;
goBack(); ESP_LOGI(TAG, "Executing navigation: action=%d target=%d",
static_cast<int>(navAction_), navTargetScreen_);
if (navAction_ == ButtonAction::JUMP) {
showScreen(navTargetScreen_);
} else if (navAction_ == ButtonAction::BACK) {
goBack();
}
didUiNav = true;
ESP_LOGI(TAG, "Navigation complete");
} }
return;
} }
if (standbyWakePending_) { if (standbyWakePending_) {
@ -409,9 +472,13 @@ void WidgetManager::loop() {
activeScreenId_ = standbyWakeTarget_; activeScreenId_ = standbyWakeTarget_;
applyScreen(activeScreenId_); applyScreen(activeScreenId_);
} }
return; didUiNav = true;
} }
processUiQueue();
if (didUiNav) return;
if (!config_.standbyEnabled || config_.standbyMinutes == 0) return; if (!config_.standbyEnabled || config_.standbyMinutes == 0) return;
if (standbyActive_) return; if (standbyActive_) return;
if (config_.standbyScreenId == SCREEN_ID_NONE) return; if (config_.standbyScreenId == SCREEN_ID_NONE) return;
@ -464,39 +531,88 @@ void WidgetManager::createAllWidgets(const ScreenConfig& screen, lv_obj_t* paren
} }
void WidgetManager::onKnxValue(uint16_t groupAddr, float value) { void WidgetManager::onKnxValue(uint16_t groupAddr, float value) {
if (esp_lv_adapter_lock(100) != ESP_OK) return; UiEvent event = {};
event.type = UiEventType::KNX_VALUE;
event.groupAddr = groupAddr;
event.value = value;
enqueueUiEvent(event);
}
void WidgetManager::onKnxSwitch(uint16_t groupAddr, bool value) {
UiEvent event = {};
event.type = UiEventType::KNX_SWITCH;
event.groupAddr = groupAddr;
event.state = value;
enqueueUiEvent(event);
}
void WidgetManager::onKnxText(uint16_t groupAddr, const char* text) {
UiEvent event = {};
event.type = UiEventType::KNX_TEXT;
event.groupAddr = groupAddr;
if (text) {
strncpy(event.text, text, sizeof(event.text) - 1);
} else {
event.text[0] = '\0';
}
enqueueUiEvent(event);
}
bool WidgetManager::enqueueUiEvent(const UiEvent& event) {
if (!uiQueue_) return false;
return xQueueSend(uiQueue_, &event, 0) == pdTRUE;
}
void WidgetManager::processUiQueue() {
if (!uiQueue_) return;
if (uxQueueMessagesWaiting(uiQueue_) == 0) return;
if (esp_lv_adapter_lock(-1) != ESP_OK) return;
UiEvent event = {};
static constexpr size_t kMaxEventsPerLoop = 8;
size_t processed = 0;
while (processed < kMaxEventsPerLoop &&
xQueueReceive(uiQueue_, &event, 0) == pdTRUE) {
switch (event.type) {
case UiEventType::KNX_VALUE:
applyKnxValue(event.groupAddr, event.value);
break;
case UiEventType::KNX_SWITCH:
applyKnxSwitch(event.groupAddr, event.state);
break;
case UiEventType::KNX_TEXT:
applyKnxText(event.groupAddr, event.text);
break;
}
processed++;
}
esp_lv_adapter_unlock();
}
void WidgetManager::applyKnxValue(uint16_t groupAddr, float value) {
for (auto& widget : widgets_) { for (auto& widget : widgets_) {
if (widget && widget->getKnxAddress() == groupAddr) { if (widget && widget->getKnxAddress() == groupAddr) {
widget->onKnxValue(value); widget->onKnxValue(value);
} }
} }
esp_lv_adapter_unlock();
} }
void WidgetManager::onKnxSwitch(uint16_t groupAddr, bool value) { void WidgetManager::applyKnxSwitch(uint16_t groupAddr, bool value) {
if (esp_lv_adapter_lock(100) != ESP_OK) return;
for (auto& widget : widgets_) { for (auto& widget : widgets_) {
if (widget && widget->getKnxAddress() == groupAddr) { if (widget && widget->getKnxAddress() == groupAddr) {
widget->onKnxSwitch(value); widget->onKnxSwitch(value);
} }
} }
esp_lv_adapter_unlock();
} }
void WidgetManager::onKnxText(uint16_t groupAddr, const char* text) { void WidgetManager::applyKnxText(uint16_t groupAddr, const char* text) {
if (esp_lv_adapter_lock(100) != ESP_OK) return;
for (auto& widget : widgets_) { for (auto& widget : widgets_) {
if (widget && widget->getKnxAddress() == groupAddr) { if (widget && widget->getKnxAddress() == groupAddr) {
widget->onKnxText(text); widget->onKnxText(text);
} }
} }
esp_lv_adapter_unlock();
} }
// Helper function to parse hex color string // Helper function to parse hex color string

View File

@ -3,6 +3,8 @@
#include "WidgetConfig.hpp" #include "WidgetConfig.hpp"
#include "widgets/Widget.hpp" #include "widgets/Widget.hpp"
#include "lvgl.h" #include "lvgl.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include <array> #include <array>
#include <memory> #include <memory>
@ -35,7 +37,7 @@ public:
// User activity (resets standby timer) // User activity (resets standby timer)
void onUserActivity(); void onUserActivity();
// KNX value update (called from KnxWorker) // Thread-safe KNX updates (queued to UI thread)
void onKnxValue(uint16_t groupAddr, float value); void onKnxValue(uint16_t groupAddr, float value);
void onKnxSwitch(uint16_t groupAddr, bool value); void onKnxSwitch(uint16_t groupAddr, bool value);
void onKnxText(uint16_t groupAddr, const char* text); void onKnxText(uint16_t groupAddr, const char* text);
@ -54,10 +56,32 @@ private:
WidgetManager(const WidgetManager&) = delete; WidgetManager(const WidgetManager&) = delete;
WidgetManager& operator=(const WidgetManager&) = delete; WidgetManager& operator=(const WidgetManager&) = delete;
static constexpr size_t UI_EVENT_QUEUE_LEN = 16;
static constexpr size_t UI_EVENT_TEXT_LEN = MAX_TEXT_LEN;
enum class UiEventType : uint8_t {
KNX_VALUE = 0,
KNX_SWITCH = 1,
KNX_TEXT = 2,
};
struct UiEvent {
UiEventType type;
uint16_t groupAddr;
float value;
bool state;
char text[UI_EVENT_TEXT_LEN];
};
void loadFromSdCard(); void loadFromSdCard();
void saveToSdCard(); void saveToSdCard();
void destroyAllWidgets(); void destroyAllWidgets();
void createAllWidgets(const ScreenConfig& screen, lv_obj_t* parent); void createAllWidgets(const ScreenConfig& screen, lv_obj_t* parent);
bool enqueueUiEvent(const UiEvent& event);
void processUiQueue();
void applyKnxValue(uint16_t groupAddr, float value);
void applyKnxSwitch(uint16_t groupAddr, bool value);
void applyKnxText(uint16_t groupAddr, const char* text);
void createDefaultConfig(); void createDefaultConfig();
void applyScreen(uint8_t screenId); void applyScreen(uint8_t screenId);
@ -69,6 +93,7 @@ private:
const ScreenConfig* activeScreen() const; const ScreenConfig* activeScreen() const;
static constexpr const char* CONFIG_FILE = "/sdcard/lvgl.json"; static constexpr const char* CONFIG_FILE = "/sdcard/lvgl.json";
static constexpr int64_t NAV_DELAY_US = 200 * 1000; // 200ms delay for touch release
GuiConfig config_; GuiConfig config_;
uint8_t activeScreenId_ = 0; uint8_t activeScreenId_ = 0;
@ -81,6 +106,7 @@ private:
bool navPending_ = false; bool navPending_ = false;
ButtonAction navAction_ = ButtonAction::KNX; ButtonAction navAction_ = ButtonAction::KNX;
uint8_t navTargetScreen_ = 0xFF; uint8_t navTargetScreen_ = 0xFF;
int64_t navRequestUs_ = 0;
int64_t lastActivityUs_ = 0; int64_t lastActivityUs_ = 0;
// Runtime widget instances (indexed by widget ID) // Runtime widget instances (indexed by widget ID)
@ -88,4 +114,5 @@ private:
lv_obj_t* screen_ = nullptr; lv_obj_t* screen_ = nullptr;
lv_obj_t* modalContainer_ = nullptr; lv_obj_t* modalContainer_ = nullptr;
lv_obj_t* modalDimmer_ = nullptr; lv_obj_t* modalDimmer_ = nullptr;
QueueHandle_t uiQueue_ = nullptr;
}; };

View File

@ -15,8 +15,7 @@ dependencies:
# # All dependencies of `main` are public by default. # # All dependencies of `main` are public by default.
# public: true # public: true
waveshare/esp_lcd_jd9365_10_1: '*' waveshare/esp_lcd_jd9365_10_1: '*'
lvgl/lvgl: ^9.4.0 espressif/esp_lvgl_port: ^2.7.0
espressif/esp_lvgl_port: ^2.3.0
espressif/esp_lcd_touch_gt911: '*' espressif/esp_lcd_touch_gt911: '*'
espressif/esp_lvgl_adapter: '*' espressif/esp_lvgl_adapter: '*'
espressif/esp_wifi_remote: '*' espressif/esp_wifi_remote: '*'

View File

@ -17,6 +17,14 @@
#define TAG "App" #define TAG "App"
static void knx_task(void* arg) {
KnxWorker* worker = static_cast<KnxWorker*>(arg);
worker->init();
while (true) {
worker->loop();
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// This is a simple wrapper for the application logic // This is a simple wrapper for the application logic
class Application { class Application {
@ -63,7 +71,11 @@ public:
} }
ESP_LOGI(TAG, "Creating UI"); ESP_LOGI(TAG, "Creating UI");
knxWorker.init(); BaseType_t knx_ok = xTaskCreatePinnedToCore(
knx_task, "knx", 4096, &Gui::knxWorker, 5, nullptr, 1);
if (knx_ok != pdPASS) {
ESP_LOGE(TAG, "Failed to start KNX task");
}
gui.create(); gui.create();
// Start WebServer for widget configuration // Start WebServer for widget configuration
@ -72,8 +84,6 @@ public:
ESP_LOGI(TAG, "Application running"); ESP_LOGI(TAG, "Application running");
while (true) { while (true) {
vTaskDelay(pdMS_TO_TICKS(10)); vTaskDelay(pdMS_TO_TICKS(10));
knxWorker.loop();
WidgetManager::instance().loop();
} }
} }
@ -82,7 +92,6 @@ private:
Touch touch; Touch touch;
Gui gui; Gui gui;
Nvs nvs; Nvs nvs;
KnxWorker knxWorker;
}; };
extern "C" void app_main(void) extern "C" void app_main(void)

View File

@ -1,5 +1,13 @@
#include "ButtonWidget.hpp" #include "ButtonWidget.hpp"
#include "../WidgetManager.hpp" #include "../WidgetManager.hpp"
#include "esp_log.h"
static const char* TAG = "ButtonWidget";
// Free function instead of static member
static void button_event_cb(lv_event_t* e) {
ESP_LOGI(TAG, "button_event_cb called");
}
ButtonWidget::ButtonWidget(const WidgetConfig& config) ButtonWidget::ButtonWidget(const WidgetConfig& config)
: Widget(config) : Widget(config)
@ -7,32 +15,33 @@ ButtonWidget::ButtonWidget(const WidgetConfig& config)
{ {
} }
ButtonWidget::~ButtonWidget() {
// Remove event callback BEFORE the base class destructor deletes the object
if (obj_) {
lv_obj_remove_event_cb(obj_, button_event_cb);
}
label_ = nullptr;
}
void ButtonWidget::clickCallback(lv_event_t* e) { void ButtonWidget::clickCallback(lv_event_t* e) {
ButtonWidget* widget = static_cast<ButtonWidget*>(lv_event_get_user_data(e)); // Not used currently
if (!widget) return; (void)e;
lv_obj_t* target = static_cast<lv_obj_t*>(lv_event_get_target(e));
WidgetManager::instance().handleButtonAction(widget->getConfig(), target);
} }
lv_obj_t* ButtonWidget::create(lv_obj_t* parent) { lv_obj_t* ButtonWidget::create(lv_obj_t* parent) {
obj_ = lv_btn_create(parent); obj_ = lv_btn_create(parent);
lv_obj_set_pos(obj_, config_.x, config_.y);
lv_obj_set_size(obj_, config_.width > 0 ? config_.width : 100,
config_.height > 0 ? config_.height : 50);
if (config_.isToggle) { // Test: free function callback
lv_obj_add_flag(obj_, LV_OBJ_FLAG_CHECKABLE); lv_obj_add_event_cb(obj_, button_event_cb, LV_EVENT_CLICKED, NULL);
}
lv_obj_add_event_cb(obj_, clickCallback, LV_EVENT_CLICKED, this);
// Create label inside button
label_ = lv_label_create(obj_); label_ = lv_label_create(obj_);
lv_label_set_text(label_, config_.text); lv_label_set_text(label_, config_.text);
lv_obj_center(label_); lv_obj_center(label_);
lv_obj_set_pos(obj_, config_.x, config_.y); ESP_LOGI(TAG, "Created button '%s' at %d,%d", config_.text, config_.x, config_.y);
if (config_.width > 0 && config_.height > 0) {
lv_obj_set_size(obj_, config_.width, config_.height);
}
return obj_; return obj_;
} }

View File

@ -5,6 +5,7 @@
class ButtonWidget : public Widget { class ButtonWidget : public Widget {
public: public:
explicit ButtonWidget(const WidgetConfig& config); explicit ButtonWidget(const WidgetConfig& config);
~ButtonWidget() override;
lv_obj_t* create(lv_obj_t* parent) override; lv_obj_t* create(lv_obj_t* parent) override;
void applyStyle() override; void applyStyle() override;

View File

@ -908,6 +908,18 @@
if (!screen.bgColor) screen.bgColor = '#1A1A2E'; if (!screen.bgColor) screen.bgColor = '#1A1A2E';
if (!Array.isArray(screen.widgets)) screen.widgets = []; if (!Array.isArray(screen.widgets)) screen.widgets = [];
// Modal defaults
if (!screen.modal) {
screen.modal = { x: 0, y: 0, w: 0, h: 0, radius: 12, dim: true };
} else {
if (screen.modal.x === undefined) screen.modal.x = 0;
if (screen.modal.y === undefined) screen.modal.y = 0;
if (screen.modal.w === undefined) screen.modal.w = 0;
if (screen.modal.h === undefined) screen.modal.h = 0;
if (screen.modal.radius === undefined) screen.modal.radius = 12;
if (screen.modal.dim === undefined) screen.modal.dim = true;
}
screen.widgets.forEach(normalizeWidget); screen.widgets.forEach(normalizeWidget);
} }