knxdisplay/main/WidgetManager.cpp
2026-01-24 18:31:31 +01:00

981 lines
33 KiB
C++

#include "WidgetManager.hpp"
#include "widgets/WidgetFactory.hpp"
#include "SdCard.hpp"
#include "esp_lv_adapter.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "cJSON.h"
#include <memory>
#include <new>
#include <cstdio>
#include <cstring>
#include <cstdlib>
static const char* TAG = "WidgetMgr";
static constexpr uint8_t SCREEN_ID_NONE = 0xFF;
// WidgetManager implementation
WidgetManager& WidgetManager::instance() {
static WidgetManager inst;
return inst;
}
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();
}
void WidgetManager::createDefaultConfig() {
config_.clear();
config_.screenCount = 1;
ScreenConfig& screen = config_.screens[0];
screen.clear(0, "Screen 1");
// Default: Temperature label
auto tempLabel = WidgetConfig::createKnxLabel(0, 50, 20,
TextSource::KNX_DPT_TEMP, 1, "%.1f °C");
tempLabel.fontSize = 3; // 28pt
screen.addWidget(tempLabel);
// Default: KNX Prog button
auto progBtn = WidgetConfig::createButton(1, 50, 100, "KNX Prog", 0, true);
progBtn.bgColor = {200, 50, 50}; // Red
screen.addWidget(progBtn);
config_.startScreenId = screen.id;
config_.standbyEnabled = false;
config_.standbyScreenId = 0xFF;
config_.standbyMinutes = 0;
activeScreenId_ = screen.id;
}
void WidgetManager::init() {
loadFromSdCard();
if (config_.findScreen(config_.startScreenId)) {
activeScreenId_ = config_.startScreenId;
} else if (config_.screenCount > 0) {
activeScreenId_ = config_.screens[0].id;
} else {
activeScreenId_ = 0;
}
lastActivityUs_ = esp_timer_get_time();
ESP_LOGI(TAG, "WidgetManager initialized with %d screens", config_.screenCount);
}
void WidgetManager::loadFromSdCard() {
if (!SdCard::instance().isMounted()) {
ESP_LOGI(TAG, "SD card not mounted, using defaults");
return;
}
FILE* f = fopen(CONFIG_FILE, "r");
if (!f) {
ESP_LOGI(TAG, "No config file found, using defaults");
return;
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
if (size <= 0 || size > 32768) {
ESP_LOGE(TAG, "Invalid config file size: %ld", size);
fclose(f);
return;
}
char* json = new char[size + 1];
if (!json) {
ESP_LOGE(TAG, "Failed to allocate memory for config");
fclose(f);
return;
}
size_t read = fread(json, 1, size, f);
fclose(f);
json[read] = '\0';
bool success = updateConfigFromJson(json);
delete[] json;
if (success) {
ESP_LOGI(TAG, "Loaded %d screens from SD card", config_.screenCount);
} else {
ESP_LOGE(TAG, "Failed to parse config file");
}
}
void WidgetManager::saveToSdCard() {
if (!SdCard::instance().isMounted()) {
ESP_LOGE(TAG, "SD card not mounted, cannot save config");
return;
}
char* json = new char[32768];
if (!json) {
ESP_LOGE(TAG, "Failed to allocate memory for JSON");
return;
}
getConfigJson(json, 32768);
FILE* f = fopen(CONFIG_FILE, "w");
if (!f) {
ESP_LOGE(TAG, "Failed to open config file for writing");
delete[] json;
return;
}
size_t written = fwrite(json, 1, strlen(json), f);
fclose(f);
delete[] json;
if (written > 0) {
ESP_LOGI(TAG, "Saved %d screens to SD card", config_.screenCount);
} else {
ESP_LOGE(TAG, "Failed to write config file");
}
}
void WidgetManager::applyConfig() {
if (!config_.findScreen(activeScreenId_)) {
if (config_.findScreen(config_.startScreenId)) {
activeScreenId_ = config_.startScreenId;
} else if (config_.screenCount > 0) {
activeScreenId_ = config_.screens[0].id;
}
}
applyScreen(activeScreenId_);
}
void WidgetManager::saveAndApply() {
saveToSdCard();
applyConfig();
ESP_LOGI(TAG, "Config saved and applied");
}
void WidgetManager::resetToDefaults() {
closeModal();
standbyActive_ = false;
standbyWakePending_ = false;
standbyReturnScreenId_ = SCREEN_ID_NONE;
previousScreenId_ = SCREEN_ID_NONE;
createDefaultConfig();
saveAndApply();
ESP_LOGI(TAG, "Reset to defaults");
}
ScreenConfig* WidgetManager::activeScreen() {
if (modalContainer_ && modalScreenId_ != SCREEN_ID_NONE) {
return config_.findScreen(modalScreenId_);
}
return config_.findScreen(activeScreenId_);
}
const ScreenConfig* WidgetManager::activeScreen() const {
if (modalContainer_ && modalScreenId_ != SCREEN_ID_NONE) {
return config_.findScreen(modalScreenId_);
}
return config_.findScreen(activeScreenId_);
}
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);
return;
}
if (modalContainer_) {
ESP_LOGI(TAG, "Closing modal first");
closeModal();
}
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) {
if (modalContainer_) {
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();
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;
// Create semi-transparent background overlay if dimming enabled
lv_obj_t* dimmer = nullptr;
if (screen.modalDimBackground) {
dimmer = lv_obj_create(lv_layer_top());
lv_obj_remove_style_all(dimmer);
lv_obj_set_size(dimmer, dispWidth, dispHeight);
lv_obj_set_style_bg_color(dimmer, lv_color_black(), 0);
lv_obj_set_style_bg_opa(dimmer, LV_OPA_50, 0);
lv_obj_clear_flag(dimmer, LV_OBJ_FLAG_SCROLLABLE);
}
// Create modal container
lv_obj_t* modal = lv_obj_create(lv_layer_top());
lv_obj_clear_flag(modal, LV_OBJ_FLAG_SCROLLABLE);
// Calculate modal size
int32_t modalWidth = screen.modalWidth;
int32_t modalHeight = screen.modalHeight;
// Auto-size: calculate from widget bounds if not specified
if (modalWidth <= 0 || modalHeight <= 0) {
int32_t maxX = 0, maxY = 0;
for (uint8_t i = 0; i < screen.widgetCount; i++) {
const WidgetConfig& w = screen.widgets[i];
if (w.visible) {
int32_t right = w.x + w.width;
int32_t bottom = w.y + w.height;
if (right > maxX) maxX = right;
if (bottom > maxY) maxY = bottom;
}
}
if (modalWidth <= 0) modalWidth = maxX + 40; // Add padding
if (modalHeight <= 0) modalHeight = maxY + 40;
}
lv_obj_set_size(modal, modalWidth, modalHeight);
// Position modal (0 = centered)
if (screen.modalX == 0 && screen.modalY == 0) {
lv_obj_center(modal);
} else {
lv_obj_set_pos(modal, screen.modalX, screen.modalY);
}
// Style modal
lv_obj_set_style_bg_color(modal, lv_color_make(
screen.backgroundColor.r,
screen.backgroundColor.g,
screen.backgroundColor.b), 0);
lv_obj_set_style_bg_opa(modal, LV_OPA_COVER, 0);
lv_obj_set_style_radius(modal, screen.modalBorderRadius, 0);
lv_obj_set_style_border_width(modal, 0, 0);
lv_obj_set_style_pad_all(modal, 0, 0);
// Add shadow for modal
lv_obj_set_style_shadow_color(modal, lv_color_black(), 0);
lv_obj_set_style_shadow_opa(modal, LV_OPA_30, 0);
lv_obj_set_style_shadow_width(modal, 20, 0);
lv_obj_set_style_shadow_spread(modal, 5, 0);
modalContainer_ = modal;
modalDimmer_ = dimmer;
modalScreenId_ = screen.id;
// Create widgets inside modal (not on full screen)
screen_ = modal;
for (uint8_t i = 0; i < screen.widgetCount; i++) {
const WidgetConfig& cfg = screen.widgets[i];
auto widget = WidgetFactory::create(cfg);
if (widget && cfg.id < MAX_WIDGETS) {
widget->create(modal);
widget->applyStyle();
widgets_[cfg.id] = std::move(widget);
}
}
esp_lv_adapter_unlock();
ESP_LOGI(TAG, "Modal screen %d opened (%ldx%ld)", screen.id, modalWidth, modalHeight);
}
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 (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;
}
previousScreenId_ = activeScreenId_;
activeScreenId_ = screen->id;
standbyActive_ = false;
applyScreen(activeScreenId_);
}
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:
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:
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: {
if (cfg.knxAddressWrite > 0) {
bool state = false;
if (target) {
state = (lv_obj_get_state(target) & LV_STATE_CHECKED) != 0;
}
ESP_LOGI(TAG, "Button %d clicked, KNX write to %d, state=%d",
cfg.id, cfg.knxAddressWrite, state);
// TODO: Send KNX telegram
}
break;
}
}
}
void WidgetManager::goBack() {
if (modalContainer_) {
closeModal();
applyScreen(activeScreenId_);
return;
}
if (previousScreenId_ != SCREEN_ID_NONE && previousScreenId_ != activeScreenId_) {
activeScreenId_ = previousScreenId_;
previousScreenId_ = SCREEN_ID_NONE;
applyScreen(activeScreenId_);
}
}
void WidgetManager::enterStandby() {
if (!config_.standbyEnabled || config_.standbyMinutes == 0) return;
if (standbyActive_) return;
if (config_.standbyScreenId == SCREEN_ID_NONE) return;
ScreenConfig* standbyScreen = config_.findScreen(config_.standbyScreenId);
if (!standbyScreen) return;
standbyReturnScreenId_ = activeScreenId_;
standbyActive_ = true;
activeScreenId_ = standbyScreen->id;
applyScreen(activeScreenId_);
}
void WidgetManager::loop() {
bool didUiNav = false;
if (navPending_) {
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");
}
}
if (standbyWakePending_) {
standbyWakePending_ = false;
if (standbyWakeTarget_ != SCREEN_ID_NONE) {
activeScreenId_ = standbyWakeTarget_;
applyScreen(activeScreenId_);
}
didUiNav = true;
}
processUiQueue();
if (didUiNav) return;
if (!config_.standbyEnabled || config_.standbyMinutes == 0) return;
if (standbyActive_) return;
if (config_.standbyScreenId == SCREEN_ID_NONE) return;
int64_t now = esp_timer_get_time();
int64_t idleUs = now - lastActivityUs_;
int64_t timeoutUs = static_cast<int64_t>(config_.standbyMinutes) * 60 * 1000000LL;
if (idleUs >= timeoutUs) {
enterStandby();
}
}
void WidgetManager::onUserActivity() {
lastActivityUs_ = esp_timer_get_time();
if (standbyActive_) {
standbyActive_ = false;
uint8_t returnId = standbyReturnScreenId_;
if (returnId == SCREEN_ID_NONE) {
returnId = config_.startScreenId;
}
standbyWakeTarget_ = returnId;
standbyWakePending_ = true;
}
}
void WidgetManager::destroyAllWidgets() {
for (auto& widget : widgets_) {
widget.reset();
}
}
void WidgetManager::createAllWidgets(const ScreenConfig& screen, lv_obj_t* parent) {
screen_ = parent;
lv_obj_set_style_bg_color(parent, lv_color_make(
screen.backgroundColor.r,
screen.backgroundColor.g,
screen.backgroundColor.b), 0);
lv_obj_set_style_bg_opa(parent, LV_OPA_COVER, 0);
for (uint8_t i = 0; i < screen.widgetCount; i++) {
const WidgetConfig& cfg = screen.widgets[i];
auto widget = WidgetFactory::create(cfg);
if (widget && cfg.id < MAX_WIDGETS) {
widget->create(parent);
widget->applyStyle();
widgets_[cfg.id] = std::move(widget);
}
}
}
void WidgetManager::onKnxValue(uint16_t groupAddr, float value) {
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);
}
}
}
void WidgetManager::applyKnxSwitch(uint16_t groupAddr, bool value) {
for (auto& widget : widgets_) {
if (widget && widget->getKnxAddress() == groupAddr) {
widget->onKnxSwitch(value);
}
}
}
void WidgetManager::applyKnxText(uint16_t groupAddr, const char* text) {
for (auto& widget : widgets_) {
if (widget && widget->getKnxAddress() == groupAddr) {
widget->onKnxText(text);
}
}
}
// Helper function to parse hex color string
static uint32_t parseHexColor(const char* colorStr) {
if (!colorStr || colorStr[0] != '#') return 0;
return strtoul(colorStr + 1, nullptr, 16);
}
void WidgetManager::getConfigJson(char* buf, size_t bufSize) const {
cJSON* root = cJSON_CreateObject();
if (!root) {
snprintf(buf, bufSize, "{}");
return;
}
cJSON_AddNumberToObject(root, "startScreen", config_.startScreenId);
cJSON* standby = cJSON_AddObjectToObject(root, "standby");
cJSON_AddBoolToObject(standby, "enabled", config_.standbyEnabled);
cJSON_AddNumberToObject(standby, "screen", config_.standbyScreenId);
cJSON_AddNumberToObject(standby, "minutes", config_.standbyMinutes);
cJSON* screens = cJSON_AddArrayToObject(root, "screens");
for (uint8_t s = 0; s < config_.screenCount; s++) {
const ScreenConfig& screen = config_.screens[s];
cJSON* screenJson = cJSON_CreateObject();
cJSON_AddNumberToObject(screenJson, "id", screen.id);
cJSON_AddStringToObject(screenJson, "name", screen.name);
cJSON_AddNumberToObject(screenJson, "mode", static_cast<int>(screen.mode));
char bgColorStr[8];
snprintf(bgColorStr, sizeof(bgColorStr), "#%02X%02X%02X",
screen.backgroundColor.r, screen.backgroundColor.g, screen.backgroundColor.b);
cJSON_AddStringToObject(screenJson, "bgColor", bgColorStr);
// Modal-specific properties
if (screen.mode == ScreenMode::MODAL) {
cJSON* modal = cJSON_AddObjectToObject(screenJson, "modal");
cJSON_AddNumberToObject(modal, "x", screen.modalX);
cJSON_AddNumberToObject(modal, "y", screen.modalY);
cJSON_AddNumberToObject(modal, "w", screen.modalWidth);
cJSON_AddNumberToObject(modal, "h", screen.modalHeight);
cJSON_AddNumberToObject(modal, "radius", screen.modalBorderRadius);
cJSON_AddBoolToObject(modal, "dim", screen.modalDimBackground);
}
cJSON* widgets = cJSON_AddArrayToObject(screenJson, "widgets");
for (uint8_t i = 0; i < screen.widgetCount; i++) {
const WidgetConfig& w = screen.widgets[i];
cJSON* widget = cJSON_CreateObject();
cJSON_AddNumberToObject(widget, "id", w.id);
cJSON_AddNumberToObject(widget, "type", static_cast<int>(w.type));
cJSON_AddNumberToObject(widget, "x", w.x);
cJSON_AddNumberToObject(widget, "y", w.y);
cJSON_AddNumberToObject(widget, "w", w.width);
cJSON_AddNumberToObject(widget, "h", w.height);
cJSON_AddBoolToObject(widget, "visible", w.visible);
cJSON_AddNumberToObject(widget, "textSrc", static_cast<int>(w.textSource));
cJSON_AddStringToObject(widget, "text", w.text);
cJSON_AddNumberToObject(widget, "knxAddr", w.knxAddress);
cJSON_AddNumberToObject(widget, "fontSize", w.fontSize);
char textColorStr[8];
snprintf(textColorStr, sizeof(textColorStr), "#%02X%02X%02X",
w.textColor.r, w.textColor.g, w.textColor.b);
cJSON_AddStringToObject(widget, "textColor", textColorStr);
char widgetBgColorStr[8];
snprintf(widgetBgColorStr, sizeof(widgetBgColorStr), "#%02X%02X%02X",
w.bgColor.r, w.bgColor.g, w.bgColor.b);
cJSON_AddStringToObject(widget, "bgColor", widgetBgColorStr);
cJSON_AddNumberToObject(widget, "bgOpacity", w.bgOpacity);
cJSON_AddNumberToObject(widget, "radius", w.borderRadius);
cJSON* shadow = cJSON_AddObjectToObject(widget, "shadow");
cJSON_AddBoolToObject(shadow, "enabled", w.shadow.enabled);
cJSON_AddNumberToObject(shadow, "x", w.shadow.offsetX);
cJSON_AddNumberToObject(shadow, "y", w.shadow.offsetY);
cJSON_AddNumberToObject(shadow, "blur", w.shadow.blur);
cJSON_AddNumberToObject(shadow, "spread", w.shadow.spread);
char shadowColorStr[8];
snprintf(shadowColorStr, sizeof(shadowColorStr), "#%02X%02X%02X",
w.shadow.color.r, w.shadow.color.g, w.shadow.color.b);
cJSON_AddStringToObject(shadow, "color", shadowColorStr);
cJSON_AddBoolToObject(widget, "isToggle", w.isToggle);
cJSON_AddNumberToObject(widget, "knxAddrWrite", w.knxAddressWrite);
cJSON_AddNumberToObject(widget, "action", static_cast<int>(w.action));
cJSON_AddNumberToObject(widget, "targetScreen", w.targetScreen);
cJSON_AddItemToArray(widgets, widget);
}
cJSON_AddItemToArray(screens, screenJson);
}
char* jsonStr = cJSON_PrintUnformatted(root);
if (jsonStr) {
strncpy(buf, jsonStr, bufSize - 1);
buf[bufSize - 1] = '\0';
free(jsonStr);
} else {
snprintf(buf, bufSize, "{}");
}
cJSON_Delete(root);
}
bool WidgetManager::updateConfigFromJson(const char* json) {
cJSON* root = cJSON_Parse(json);
if (!root) {
ESP_LOGE(TAG, "Failed to parse JSON");
return false;
}
std::unique_ptr<GuiConfig> newConfig(new (std::nothrow) GuiConfig());
if (!newConfig) {
ESP_LOGE(TAG, "Out of memory for config");
cJSON_Delete(root);
return false;
}
newConfig->clear();
auto parseWidgets = [&](cJSON* widgets, ScreenConfig& screen) -> bool {
if (!cJSON_IsArray(widgets)) return false;
screen.widgetCount = 0;
cJSON* widget = nullptr;
cJSON_ArrayForEach(widget, widgets) {
if (screen.widgetCount >= MAX_WIDGETS) break;
WidgetConfig& w = screen.widgets[screen.widgetCount];
memset(&w, 0, sizeof(w));
w.visible = true;
w.action = ButtonAction::KNX;
w.targetScreen = 0;
cJSON* id = cJSON_GetObjectItem(widget, "id");
if (cJSON_IsNumber(id)) w.id = id->valueint;
cJSON* type = cJSON_GetObjectItem(widget, "type");
if (cJSON_IsNumber(type)) w.type = static_cast<WidgetType>(type->valueint);
cJSON* x = cJSON_GetObjectItem(widget, "x");
if (cJSON_IsNumber(x)) w.x = x->valueint;
cJSON* y = cJSON_GetObjectItem(widget, "y");
if (cJSON_IsNumber(y)) w.y = y->valueint;
cJSON* width = cJSON_GetObjectItem(widget, "w");
if (cJSON_IsNumber(width)) w.width = width->valueint;
cJSON* height = cJSON_GetObjectItem(widget, "h");
if (cJSON_IsNumber(height)) w.height = height->valueint;
cJSON* visible = cJSON_GetObjectItem(widget, "visible");
if (cJSON_IsBool(visible)) w.visible = cJSON_IsTrue(visible);
cJSON* textSrc = cJSON_GetObjectItem(widget, "textSrc");
if (cJSON_IsNumber(textSrc)) w.textSource = static_cast<TextSource>(textSrc->valueint);
cJSON* text = cJSON_GetObjectItem(widget, "text");
if (cJSON_IsString(text)) {
strncpy(w.text, text->valuestring, MAX_TEXT_LEN - 1);
w.text[MAX_TEXT_LEN - 1] = '\0';
}
cJSON* knxAddr = cJSON_GetObjectItem(widget, "knxAddr");
if (cJSON_IsNumber(knxAddr)) w.knxAddress = knxAddr->valueint;
cJSON* fontSize = cJSON_GetObjectItem(widget, "fontSize");
if (cJSON_IsNumber(fontSize)) w.fontSize = fontSize->valueint;
cJSON* textColor = cJSON_GetObjectItem(widget, "textColor");
if (cJSON_IsString(textColor)) {
w.textColor = Color::fromHex(parseHexColor(textColor->valuestring));
}
cJSON* widgetBgColor = cJSON_GetObjectItem(widget, "bgColor");
if (cJSON_IsString(widgetBgColor)) {
w.bgColor = Color::fromHex(parseHexColor(widgetBgColor->valuestring));
}
cJSON* bgOpacity = cJSON_GetObjectItem(widget, "bgOpacity");
if (cJSON_IsNumber(bgOpacity)) w.bgOpacity = bgOpacity->valueint;
cJSON* radius = cJSON_GetObjectItem(widget, "radius");
if (cJSON_IsNumber(radius)) w.borderRadius = radius->valueint;
cJSON* shadow = cJSON_GetObjectItem(widget, "shadow");
if (cJSON_IsObject(shadow)) {
cJSON* enabled = cJSON_GetObjectItem(shadow, "enabled");
if (cJSON_IsBool(enabled)) w.shadow.enabled = cJSON_IsTrue(enabled);
cJSON* sx = cJSON_GetObjectItem(shadow, "x");
if (cJSON_IsNumber(sx)) w.shadow.offsetX = sx->valueint;
cJSON* sy = cJSON_GetObjectItem(shadow, "y");
if (cJSON_IsNumber(sy)) w.shadow.offsetY = sy->valueint;
cJSON* blur = cJSON_GetObjectItem(shadow, "blur");
if (cJSON_IsNumber(blur)) w.shadow.blur = blur->valueint;
cJSON* spread = cJSON_GetObjectItem(shadow, "spread");
if (cJSON_IsNumber(spread)) w.shadow.spread = spread->valueint;
cJSON* shadowColor = cJSON_GetObjectItem(shadow, "color");
if (cJSON_IsString(shadowColor)) {
w.shadow.color = Color::fromHex(parseHexColor(shadowColor->valuestring));
}
}
cJSON* isToggle = cJSON_GetObjectItem(widget, "isToggle");
if (cJSON_IsBool(isToggle)) w.isToggle = cJSON_IsTrue(isToggle);
cJSON* knxAddrWrite = cJSON_GetObjectItem(widget, "knxAddrWrite");
if (cJSON_IsNumber(knxAddrWrite)) w.knxAddressWrite = knxAddrWrite->valueint;
cJSON* action = cJSON_GetObjectItem(widget, "action");
if (cJSON_IsNumber(action)) w.action = static_cast<ButtonAction>(action->valueint);
cJSON* targetScreen = cJSON_GetObjectItem(widget, "targetScreen");
if (cJSON_IsNumber(targetScreen)) w.targetScreen = targetScreen->valueint;
screen.widgetCount++;
}
return true;
};
cJSON* screens = cJSON_GetObjectItem(root, "screens");
if (cJSON_IsArray(screens)) {
cJSON* screenJson = nullptr;
cJSON_ArrayForEach(screenJson, screens) {
if (newConfig->screenCount >= MAX_SCREENS) break;
uint8_t screenId = newConfig->screenCount;
const char* screenName = nullptr;
cJSON* id = cJSON_GetObjectItem(screenJson, "id");
if (cJSON_IsNumber(id)) {
int idVal = id->valueint;
if (idVal < 0) idVal = 0;
screenId = static_cast<uint8_t>(idVal);
}
cJSON* name = cJSON_GetObjectItem(screenJson, "name");
if (cJSON_IsString(name)) screenName = name->valuestring;
ScreenConfig& screen = newConfig->screens[newConfig->screenCount];
screen.clear(screenId, screenName);
if (!screen.name[0]) {
char fallback[16];
snprintf(fallback, sizeof(fallback), "Screen %d", screenId);
strncpy(screen.name, fallback, sizeof(screen.name) - 1);
}
cJSON* mode = cJSON_GetObjectItem(screenJson, "mode");
if (cJSON_IsNumber(mode)) screen.mode = static_cast<ScreenMode>(mode->valueint);
cJSON* bgColor = cJSON_GetObjectItem(screenJson, "bgColor");
if (cJSON_IsString(bgColor)) {
screen.backgroundColor = Color::fromHex(parseHexColor(bgColor->valuestring));
}
// Parse modal-specific properties
cJSON* modal = cJSON_GetObjectItem(screenJson, "modal");
if (cJSON_IsObject(modal)) {
cJSON* mx = cJSON_GetObjectItem(modal, "x");
if (cJSON_IsNumber(mx)) screen.modalX = mx->valueint;
cJSON* my = cJSON_GetObjectItem(modal, "y");
if (cJSON_IsNumber(my)) screen.modalY = my->valueint;
cJSON* mw = cJSON_GetObjectItem(modal, "w");
if (cJSON_IsNumber(mw)) screen.modalWidth = mw->valueint;
cJSON* mh = cJSON_GetObjectItem(modal, "h");
if (cJSON_IsNumber(mh)) screen.modalHeight = mh->valueint;
cJSON* mr = cJSON_GetObjectItem(modal, "radius");
if (cJSON_IsNumber(mr)) screen.modalBorderRadius = mr->valueint;
cJSON* dim = cJSON_GetObjectItem(modal, "dim");
if (cJSON_IsBool(dim)) screen.modalDimBackground = cJSON_IsTrue(dim);
}
cJSON* widgets = cJSON_GetObjectItem(screenJson, "widgets");
if (!parseWidgets(widgets, screen)) {
screen.widgetCount = 0;
}
newConfig->screenCount++;
}
} else {
newConfig->screenCount = 1;
ScreenConfig& screen = newConfig->screens[0];
screen.clear(0, "Screen 1");
cJSON* bgColor = cJSON_GetObjectItem(root, "bgColor");
if (cJSON_IsString(bgColor)) {
screen.backgroundColor = Color::fromHex(parseHexColor(bgColor->valuestring));
}
cJSON* widgets = cJSON_GetObjectItem(root, "widgets");
if (!parseWidgets(widgets, screen)) {
cJSON_Delete(root);
return false;
}
}
cJSON* startScreen = cJSON_GetObjectItem(root, "startScreen");
if (cJSON_IsNumber(startScreen)) {
int val = startScreen->valueint;
if (val < 0) val = 0;
newConfig->startScreenId = static_cast<uint8_t>(val);
}
cJSON* standby = cJSON_GetObjectItem(root, "standby");
if (cJSON_IsObject(standby)) {
cJSON* enabled = cJSON_GetObjectItem(standby, "enabled");
if (cJSON_IsBool(enabled)) newConfig->standbyEnabled = cJSON_IsTrue(enabled);
cJSON* screen = cJSON_GetObjectItem(standby, "screen");
if (cJSON_IsNumber(screen)) {
int val = screen->valueint;
if (val < 0) {
newConfig->standbyScreenId = SCREEN_ID_NONE;
} else {
newConfig->standbyScreenId = static_cast<uint8_t>(val);
}
}
cJSON* minutes = cJSON_GetObjectItem(standby, "minutes");
if (cJSON_IsNumber(minutes)) {
int val = minutes->valueint;
if (val < 0) val = 0;
newConfig->standbyMinutes = static_cast<uint16_t>(val);
}
}
if (newConfig->screenCount == 0) {
cJSON_Delete(root);
return false;
}
if (!newConfig->findScreen(newConfig->startScreenId)) {
newConfig->startScreenId = newConfig->screens[0].id;
}
if (!newConfig->findScreen(newConfig->standbyScreenId)) {
newConfig->standbyEnabled = false;
newConfig->standbyScreenId = SCREEN_ID_NONE;
}
config_ = *newConfig;
cJSON_Delete(root);
ESP_LOGI(TAG, "Parsed %d screens from JSON", config_.screenCount);
return true;
}