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

View File

@ -1,5 +1,6 @@
#include "Gui.hpp"
#include "esp_lv_adapter.h"
#include "esp_log.h"
#include "Gui/WifiSetting.hpp"
#include "Gui/EthSetting.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()
{
// Initialize WidgetManager (loads config from SD card)
WidgetManager::instance().init();
if (esp_lv_adapter_lock(-1) == ESP_OK) {
// Add long press handler for settings
lv_obj_add_event_cb(lv_scr_act(), screen_long_press_handler,
LV_EVENT_LONG_PRESSED, NULL);
// TEMP: Disabled long press handler for testing
// lv_obj_add_event_cb(lv_scr_act(), screen_long_press_handler,
// LV_EVENT_LONG_PRESSED, NULL);
lv_timer_create(widget_manager_timer_cb, 10, nullptr);
esp_lv_adapter_unlock();
}

View File

@ -20,6 +20,7 @@ void EthSetting::show() {
if (esp_lv_adapter_lock(-1) == ESP_OK) {
createUI();
visible_ = true;
esp_lv_adapter_unlock();
}
}
@ -64,7 +65,7 @@ void EthSetting::createUI() {
lv_obj_set_size(closeBtn, 40, 40);
lv_obj_align(closeBtn, LV_ALIGN_LEFT_MID, 0, 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_label_set_text(closeLbl, "X");

View File

@ -86,7 +86,7 @@ void WifiSetting::createUI() {
lv_obj_set_size(closeBtn, 40, 40);
lv_obj_align(closeBtn, LV_ALIGN_LEFT_MID, 0, 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_label_set_text(closeLbl, "X");
@ -111,7 +111,7 @@ void WifiSetting::createUI() {
disconnectBtn_ = lv_btn_create(statusSection);
lv_obj_set_size(disconnectBtn_, 100, 40);
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_t* disconnectLbl = lv_label_create(disconnectBtn_);
@ -140,7 +140,7 @@ void WifiSetting::createUI() {
lv_obj_t* scanBtn = lv_btn_create(availableHeader);
lv_obj_set_size(scanBtn, 100, 35);
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_label_set_text(scanLbl, "Scannen");
@ -198,7 +198,7 @@ void WifiSetting::refreshNetworkList() {
(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_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);
char* ssidCopy = (char*)lv_malloc(ssidLen + 1);
@ -237,7 +237,7 @@ void WifiSetting::refreshSavedNetworks() {
lv_obj_set_size(deleteBtn, 80, 30);
lv_obj_align(deleteBtn, LV_ALIGN_RIGHT_MID, 0, 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);
strcpy(ssidCopy, ssid.c_str());
@ -312,7 +312,7 @@ void WifiSetting::createPasswordDialogUI() {
lv_obj_set_size(cancelBtn, 150, 50);
lv_obj_align(cancelBtn, LV_ALIGN_TOP_LEFT, 20, 150);
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_label_set_text(cancelLbl, "Abbrechen");
@ -322,7 +322,7 @@ void WifiSetting::createPasswordDialogUI() {
lv_obj_set_size(connectBtn, 150, 50);
lv_obj_align(connectBtn, LV_ALIGN_TOP_RIGHT, -20, 150);
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_label_set_text(connectLbl, "Verbinden");
@ -332,7 +332,7 @@ void WifiSetting::createPasswordDialogUI() {
lv_obj_set_size(keyboard_, LV_PCT(100), 300);
lv_obj_align(keyboard_, LV_ALIGN_BOTTOM_MID, 0, 0);
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");
}

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)
{
Touch* self = static_cast<Touch*>(lv_indev_get_user_data(indev));
if (!self) {
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.y = y[0];
data->state = LV_INDEV_STATE_PRESSED;
WidgetManager::instance().onUserActivity();
// TEMP: Disabled to test if this causes the freeze
// WidgetManager::instance().onUserActivity();
} else {
data->state = LV_INDEV_STATE_RELEASED;
}

View File

@ -22,6 +22,10 @@ WidgetManager& WidgetManager::instance() {
WidgetManager::WidgetManager() {
// 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();
activeScreenId_ = config_.startScreenId;
lastActivityUs_ = esp_timer_get_time();
@ -182,6 +186,8 @@ const ScreenConfig* WidgetManager::activeScreen() const {
}
void WidgetManager::applyScreen(uint8_t screenId) {
ESP_LOGI(TAG, "applyScreen(%d) - start", screenId);
ScreenConfig* screen = config_.findScreen(screenId);
if (!screen) {
ESP_LOGW(TAG, "Screen %d not found", screenId);
@ -189,17 +195,40 @@ void WidgetManager::applyScreen(uint8_t screenId) {
}
if (modalContainer_) {
ESP_LOGI(TAG, "Closing modal first");
closeModal();
}
// First destroy C++ widgets (which deletes LVGL objects)
destroyAllWidgets();
if (esp_lv_adapter_lock(-1) == ESP_OK) {
lv_obj_t* root = lv_scr_act();
createAllWidgets(*screen, root);
esp_lv_adapter_unlock();
ESP_LOGI(TAG, "Acquiring LVGL lock...");
if (esp_lv_adapter_lock(-1) != ESP_OK) {
ESP_LOGE(TAG, "Failed to acquire LVGL lock!");
return;
}
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) {
@ -207,11 +236,18 @@ void WidgetManager::showModalScreen(const ScreenConfig& screen) {
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
destroyAllWidgets();
if (esp_lv_adapter_lock(-1) != ESP_OK) return;
lv_disp_t* disp = lv_disp_get_default();
int32_t dispWidth = disp ? lv_disp_get_hor_res(disp) : 1280;
int32_t dispHeight = disp ? lv_disp_get_ver_res(disp) : 800;
@ -299,28 +335,40 @@ void WidgetManager::showModalScreen(const ScreenConfig& screen) {
void WidgetManager::closeModal() {
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)
destroyAllWidgets();
if (esp_lv_adapter_lock(-1) == ESP_OK) {
if (modalDimmer_) {
lv_obj_delete(modalDimmer_);
}
lv_obj_delete(modalContainer_);
esp_lv_adapter_unlock();
if (modalDimmer_) {
lv_obj_delete(modalDimmer_);
}
lv_obj_delete(modalContainer_);
esp_lv_adapter_unlock();
modalContainer_ = nullptr;
modalDimmer_ = nullptr;
modalScreenId_ = SCREEN_ID_NONE;
}
void WidgetManager::showScreen(uint8_t screenId) {
ESP_LOGI(TAG, "showScreen(%d) called", screenId);
ScreenConfig* screen = config_.findScreen(screenId);
if (!screen) {
ESP_LOGW(TAG, "Screen %d not found", screenId);
return;
}
ESP_LOGI(TAG, "Found screen '%s', mode=%d", screen->name,
static_cast<int>(screen->mode));
if (screen->mode == ScreenMode::MODAL) {
showModalScreen(*screen);
return;
@ -335,18 +383,25 @@ void WidgetManager::showScreen(uint8_t screenId) {
void WidgetManager::handleButtonAction(const WidgetConfig& cfg, lv_obj_t* target) {
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();
switch (cfg.action) {
case ButtonAction::JUMP:
navPending_ = true;
ESP_LOGI(TAG, "JUMP action: scheduling navigation to screen %d", cfg.targetScreen);
navAction_ = ButtonAction::JUMP;
navTargetScreen_ = cfg.targetScreen;
navPending_ = true;
navRequestUs_ = esp_timer_get_time();
break;
case ButtonAction::BACK:
navPending_ = true;
ESP_LOGI(TAG, "BACK action: scheduling navigation back");
navAction_ = ButtonAction::BACK;
navTargetScreen_ = SCREEN_ID_NONE;
navPending_ = true;
navRequestUs_ = esp_timer_get_time();
break;
case ButtonAction::KNX:
default: {
@ -393,14 +448,22 @@ void WidgetManager::enterStandby() {
}
void WidgetManager::loop() {
bool didUiNav = false;
if (navPending_) {
navPending_ = false;
if (navAction_ == ButtonAction::JUMP) {
showScreen(navTargetScreen_);
} else if (navAction_ == ButtonAction::BACK) {
goBack();
int64_t now = esp_timer_get_time();
// Increased delay to ensure touch events are fully processed
if (now - navRequestUs_ >= NAV_DELAY_US) {
navPending_ = false;
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_) {
@ -409,9 +472,13 @@ void WidgetManager::loop() {
activeScreenId_ = standbyWakeTarget_;
applyScreen(activeScreenId_);
}
return;
didUiNav = true;
}
processUiQueue();
if (didUiNav) return;
if (!config_.standbyEnabled || config_.standbyMinutes == 0) return;
if (standbyActive_) 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) {
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_) {
if (widget && widget->getKnxAddress() == groupAddr) {
widget->onKnxValue(value);
}
}
esp_lv_adapter_unlock();
}
void WidgetManager::onKnxSwitch(uint16_t groupAddr, bool value) {
if (esp_lv_adapter_lock(100) != ESP_OK) return;
void WidgetManager::applyKnxSwitch(uint16_t groupAddr, bool value) {
for (auto& widget : widgets_) {
if (widget && widget->getKnxAddress() == groupAddr) {
widget->onKnxSwitch(value);
}
}
esp_lv_adapter_unlock();
}
void WidgetManager::onKnxText(uint16_t groupAddr, const char* text) {
if (esp_lv_adapter_lock(100) != ESP_OK) return;
void WidgetManager::applyKnxText(uint16_t groupAddr, const char* text) {
for (auto& widget : widgets_) {
if (widget && widget->getKnxAddress() == groupAddr) {
widget->onKnxText(text);
}
}
esp_lv_adapter_unlock();
}
// Helper function to parse hex color string

View File

@ -3,6 +3,8 @@
#include "WidgetConfig.hpp"
#include "widgets/Widget.hpp"
#include "lvgl.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include <array>
#include <memory>
@ -35,7 +37,7 @@ public:
// User activity (resets standby timer)
void onUserActivity();
// KNX value update (called from KnxWorker)
// Thread-safe KNX updates (queued to UI thread)
void onKnxValue(uint16_t groupAddr, float value);
void onKnxSwitch(uint16_t groupAddr, bool value);
void onKnxText(uint16_t groupAddr, const char* text);
@ -54,10 +56,32 @@ private:
WidgetManager(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 saveToSdCard();
void destroyAllWidgets();
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 applyScreen(uint8_t screenId);
@ -69,6 +93,7 @@ private:
const ScreenConfig* activeScreen() const;
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_;
uint8_t activeScreenId_ = 0;
@ -81,6 +106,7 @@ private:
bool navPending_ = false;
ButtonAction navAction_ = ButtonAction::KNX;
uint8_t navTargetScreen_ = 0xFF;
int64_t navRequestUs_ = 0;
int64_t lastActivityUs_ = 0;
// Runtime widget instances (indexed by widget ID)
@ -88,4 +114,5 @@ private:
lv_obj_t* screen_ = nullptr;
lv_obj_t* modalContainer_ = 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.
# public: true
waveshare/esp_lcd_jd9365_10_1: '*'
lvgl/lvgl: ^9.4.0
espressif/esp_lvgl_port: ^2.3.0
espressif/esp_lvgl_port: ^2.7.0
espressif/esp_lcd_touch_gt911: '*'
espressif/esp_lvgl_adapter: '*'
espressif/esp_wifi_remote: '*'

View File

@ -17,6 +17,14 @@
#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
class Application {
@ -63,7 +71,11 @@ public:
}
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();
// Start WebServer for widget configuration
@ -72,8 +84,6 @@ public:
ESP_LOGI(TAG, "Application running");
while (true) {
vTaskDelay(pdMS_TO_TICKS(10));
knxWorker.loop();
WidgetManager::instance().loop();
}
}
@ -82,7 +92,6 @@ private:
Touch touch;
Gui gui;
Nvs nvs;
KnxWorker knxWorker;
};
extern "C" void app_main(void)

View File

@ -1,5 +1,13 @@
#include "ButtonWidget.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)
: 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) {
ButtonWidget* widget = static_cast<ButtonWidget*>(lv_event_get_user_data(e));
if (!widget) return;
lv_obj_t* target = static_cast<lv_obj_t*>(lv_event_get_target(e));
WidgetManager::instance().handleButtonAction(widget->getConfig(), target);
// Not used currently
(void)e;
}
lv_obj_t* ButtonWidget::create(lv_obj_t* 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) {
lv_obj_add_flag(obj_, LV_OBJ_FLAG_CHECKABLE);
}
// Test: free function callback
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_);
lv_label_set_text(label_, config_.text);
lv_obj_center(label_);
lv_obj_set_pos(obj_, config_.x, config_.y);
if (config_.width > 0 && config_.height > 0) {
lv_obj_set_size(obj_, config_.width, config_.height);
}
ESP_LOGI(TAG, "Created button '%s' at %d,%d", config_.text, config_.x, config_.y);
return obj_;
}

View File

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

View File

@ -908,6 +908,18 @@
if (!screen.bgColor) screen.bgColor = '#1A1A2E';
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);
}