1459 lines
49 KiB
C++
1459 lines
49 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 <cstdint>
|
|
#include <cstring>
|
|
#include <cstdlib>
|
|
|
|
static const char* TAG = "WidgetMgr";
|
|
static constexpr uint8_t SCREEN_ID_NONE = 0xFF;
|
|
|
|
static bool is_valid_utf8(const char* text, size_t len) {
|
|
size_t i = 0;
|
|
while (i < len) {
|
|
uint8_t c = static_cast<uint8_t>(text[i]);
|
|
if (c < 0x80) {
|
|
i++;
|
|
continue;
|
|
}
|
|
if ((c & 0xE0) == 0xC0) {
|
|
if (i + 1 >= len) return false;
|
|
uint8_t c1 = static_cast<uint8_t>(text[i + 1]);
|
|
if ((c1 & 0xC0) != 0x80) return false;
|
|
i += 2;
|
|
continue;
|
|
}
|
|
if ((c & 0xF0) == 0xE0) {
|
|
if (i + 2 >= len) return false;
|
|
uint8_t c1 = static_cast<uint8_t>(text[i + 1]);
|
|
uint8_t c2 = static_cast<uint8_t>(text[i + 2]);
|
|
if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80) return false;
|
|
i += 3;
|
|
continue;
|
|
}
|
|
if ((c & 0xF8) == 0xF0) {
|
|
if (i + 3 >= len) return false;
|
|
uint8_t c1 = static_cast<uint8_t>(text[i + 1]);
|
|
uint8_t c2 = static_cast<uint8_t>(text[i + 2]);
|
|
uint8_t c3 = static_cast<uint8_t>(text[i + 3]);
|
|
if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80 || (c3 & 0xC0) != 0x80) return false;
|
|
i += 4;
|
|
continue;
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void latin1_to_utf8(const char* src, size_t src_len, char* dst, size_t dst_size) {
|
|
if (dst_size == 0) return;
|
|
size_t di = 0;
|
|
for (size_t si = 0; si < src_len; ++si) {
|
|
uint8_t c = static_cast<uint8_t>(src[si]);
|
|
if (c < 0x80) {
|
|
if (di + 1 >= dst_size) break;
|
|
dst[di++] = static_cast<char>(c);
|
|
} else {
|
|
if (di + 2 >= dst_size) break;
|
|
dst[di++] = static_cast<char>(0xC0 | (c >> 6));
|
|
dst[di++] = static_cast<char>(0x80 | (c & 0x3F));
|
|
}
|
|
}
|
|
dst[di] = '\0';
|
|
}
|
|
|
|
static WidgetConfig makeButtonLabelChild(const WidgetConfig& button) {
|
|
WidgetConfig label = WidgetConfig::createLabel(0, 0, 0, button.text);
|
|
label.parentId = button.id;
|
|
if (button.width > 0) label.width = button.width;
|
|
if (button.height > 0) label.height = button.height;
|
|
label.fontSize = button.fontSize;
|
|
label.textAlign = button.textAlign;
|
|
label.textColor = button.textColor;
|
|
label.textSource = TextSource::STATIC;
|
|
label.bgOpacity = 0;
|
|
label.borderRadius = 0;
|
|
label.shadow.enabled = false;
|
|
// Preserve existing icon config if any
|
|
label.iconCodepoint = button.iconCodepoint;
|
|
label.iconPosition = button.iconPosition;
|
|
label.iconSize = button.iconSize;
|
|
label.iconGap = button.iconGap;
|
|
if (label.text[0] == '\0') {
|
|
strncpy(label.text, "Button", MAX_TEXT_LEN - 1);
|
|
label.text[MAX_TEXT_LEN - 1] = '\0';
|
|
}
|
|
return label;
|
|
}
|
|
|
|
static void ensureButtonLabels(ScreenConfig& screen) {
|
|
bool hasLabelChild[MAX_WIDGETS] = {};
|
|
for (uint8_t i = 0; i < screen.widgetCount; i++) {
|
|
const WidgetConfig& w = screen.widgets[i];
|
|
if (w.type == WidgetType::LABEL && w.parentId >= 0 && w.parentId < MAX_WIDGETS) {
|
|
hasLabelChild[w.parentId] = true;
|
|
}
|
|
}
|
|
|
|
const uint8_t initialCount = screen.widgetCount;
|
|
for (uint8_t i = 0; i < initialCount; i++) {
|
|
WidgetConfig& w = screen.widgets[i];
|
|
if (w.type != WidgetType::BUTTON) continue;
|
|
|
|
w.isContainer = true;
|
|
|
|
if (w.id < MAX_WIDGETS && hasLabelChild[w.id]) continue;
|
|
|
|
WidgetConfig label = makeButtonLabelChild(w);
|
|
int newId = screen.addWidget(label);
|
|
if (newId < 0) {
|
|
ESP_LOGW(TAG, "No space to add label child for button %d", w.id);
|
|
w.isContainer = false;
|
|
continue;
|
|
}
|
|
if (w.id < MAX_WIDGETS) {
|
|
hasLabelChild[w.id] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// WidgetManager implementation
|
|
WidgetManager& WidgetManager::instance() {
|
|
static WidgetManager inst;
|
|
return inst;
|
|
}
|
|
|
|
WidgetManager::WidgetManager() {
|
|
// widgets_ is default-initialized to nullptr
|
|
portMUX_INITIALIZE(&knxCacheMux_);
|
|
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);
|
|
|
|
ensureButtonLabels(screen);
|
|
|
|
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_);
|
|
}
|
|
|
|
const ScreenConfig* WidgetManager::currentScreen() const {
|
|
return activeScreen();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
ensureButtonLabels(*screen);
|
|
|
|
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");
|
|
|
|
lv_display_t* disp = lv_display_get_default();
|
|
bool invEnabled = true;
|
|
if (disp) {
|
|
invEnabled = lv_display_is_invalidation_enabled(disp);
|
|
lv_display_enable_invalidation(disp, false);
|
|
}
|
|
|
|
// 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();
|
|
lv_obj_clean(lv_scr_act());
|
|
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");
|
|
applyCachedValuesToWidgets();
|
|
|
|
if (disp) {
|
|
lv_display_enable_invalidation(disp, invEnabled);
|
|
}
|
|
lv_obj_invalidate(lv_scr_act());
|
|
|
|
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());
|
|
if (dimmer) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
applyCachedValuesToWidgets();
|
|
|
|
esp_lv_adapter_unlock();
|
|
ESP_LOGI(TAG, "Modal screen %d opened (%ldx%ld)", screen.id, modalWidth, modalHeight);
|
|
}
|
|
|
|
void WidgetManager::closeModal() {
|
|
printf("WM: closeModal Start. Container=%p\n", (void*)modalContainer_);
|
|
fflush(stdout);
|
|
if (!modalContainer_) {
|
|
return;
|
|
}
|
|
|
|
if (esp_lv_adapter_lock(-1) != ESP_OK) {
|
|
ESP_LOGE(TAG, "closeModal: Failed to lock LVGL");
|
|
return;
|
|
}
|
|
|
|
// Reset input devices
|
|
lv_indev_t* indev = lv_indev_get_next(nullptr);
|
|
while (indev) {
|
|
lv_indev_reset(indev, nullptr);
|
|
indev = lv_indev_get_next(indev);
|
|
}
|
|
|
|
// Destroy widgets first
|
|
printf("WM: closeModal destroying widgets...\n");
|
|
fflush(stdout);
|
|
destroyAllWidgets();
|
|
|
|
if (modalDimmer_) {
|
|
if (lv_obj_is_valid(modalDimmer_)) lv_obj_delete(modalDimmer_);
|
|
modalDimmer_ = nullptr;
|
|
}
|
|
|
|
if (lv_obj_is_valid(modalContainer_)) {
|
|
lv_obj_delete(modalContainer_);
|
|
} else {
|
|
printf("WM: Warning: modalContainer_ was invalid!\n");
|
|
fflush(stdout);
|
|
}
|
|
modalContainer_ = nullptr;
|
|
modalScreenId_ = SCREEN_ID_NONE;
|
|
|
|
esp_lv_adapter_unlock();
|
|
printf("WM: closeModal Complete\n");
|
|
fflush(stdout);
|
|
}
|
|
|
|
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) {
|
|
printf("WM: handleButtonAction btn=%d act=%d type=%d\n", cfg.id, (int)cfg.action, (int)cfg.type);
|
|
fflush(stdout);
|
|
|
|
if (cfg.type != WidgetType::BUTTON) {
|
|
printf("WM: Not a button!\n");
|
|
fflush(stdout);
|
|
return;
|
|
}
|
|
|
|
onUserActivity();
|
|
|
|
switch (cfg.action) {
|
|
case ButtonAction::JUMP:
|
|
printf("WM: Action JUMP to %d\n", cfg.targetScreen);
|
|
fflush(stdout);
|
|
navAction_ = ButtonAction::JUMP;
|
|
navTargetScreen_ = cfg.targetScreen;
|
|
navPending_ = true;
|
|
navRequestUs_ = esp_timer_get_time();
|
|
break;
|
|
case ButtonAction::BACK:
|
|
printf("WM: Action BACK\n");
|
|
fflush(stdout);
|
|
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() {
|
|
printf("WM: goBack called. Modal=%p Active=%d Prev=%d\n",
|
|
(void*)modalContainer_, activeScreenId_, previousScreenId_);
|
|
fflush(stdout);
|
|
|
|
if (modalContainer_) {
|
|
printf("WM: Closing modal...\n");
|
|
fflush(stdout);
|
|
closeModal();
|
|
printf("WM: Modal closed. Restoring screen %d\n", activeScreenId_);
|
|
fflush(stdout);
|
|
// Restore the active screen (which was in background)
|
|
if (config_.findScreen(activeScreenId_)) {
|
|
applyScreen(activeScreenId_);
|
|
} else {
|
|
ESP_LOGE(TAG, "Active screen %d not found after closing modal!", activeScreenId_);
|
|
if (config_.findScreen(config_.startScreenId)) {
|
|
activeScreenId_ = config_.startScreenId;
|
|
applyScreen(activeScreenId_);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (previousScreenId_ != SCREEN_ID_NONE && previousScreenId_ != activeScreenId_) {
|
|
printf("WM: Going back to screen %d\n", previousScreenId_);
|
|
fflush(stdout);
|
|
if (config_.findScreen(previousScreenId_)) {
|
|
activeScreenId_ = previousScreenId_;
|
|
previousScreenId_ = SCREEN_ID_NONE;
|
|
applyScreen(activeScreenId_);
|
|
} else {
|
|
ESP_LOGW(TAG, "Previous screen %d not found", previousScreenId_);
|
|
previousScreenId_ = SCREEN_ID_NONE;
|
|
}
|
|
} else {
|
|
printf("WM: No previous screen to go back to\n");
|
|
fflush(stdout);
|
|
}
|
|
}
|
|
|
|
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() {
|
|
ESP_LOGI(TAG, "destroyAllWidgets: Start (%d widgets)", widgets_.size());
|
|
// Destroy in reverse order (last created first)
|
|
for (int i = static_cast<int>(widgets_.size()) - 1; i >= 0; i--) {
|
|
if (widgets_[i]) {
|
|
ESP_LOGD(TAG, "Destroying widget %d", i);
|
|
widgets_[i].reset();
|
|
}
|
|
}
|
|
ESP_LOGI(TAG, "destroyAllWidgets: Complete");
|
|
}
|
|
|
|
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);
|
|
|
|
// Pass 1: Create root widgets (parentId == -1)
|
|
for (uint8_t i = 0; i < screen.widgetCount; i++) {
|
|
const WidgetConfig& cfg = screen.widgets[i];
|
|
if (cfg.parentId != -1) continue;
|
|
|
|
auto widget = WidgetFactory::create(cfg);
|
|
if (widget && cfg.id < MAX_WIDGETS) {
|
|
widget->create(parent);
|
|
widget->applyStyle();
|
|
widgets_[cfg.id] = std::move(widget);
|
|
}
|
|
}
|
|
|
|
// Pass 2: Create child widgets
|
|
// Simple 1-level depth support for now. For deeper nesting, we'd need a topological sort or multiple passes.
|
|
bool madeProgress = true;
|
|
int remainingPasses = 10; // Prevent infinite loops
|
|
|
|
while (madeProgress && remainingPasses > 0) {
|
|
madeProgress = false;
|
|
remainingPasses--;
|
|
|
|
for (uint8_t i = 0; i < screen.widgetCount; i++) {
|
|
const WidgetConfig& cfg = screen.widgets[i];
|
|
|
|
// Skip if already created
|
|
if (widgets_[cfg.id]) continue;
|
|
|
|
// Skip if it's a root widget (should be created in Pass 1, but if failed/skipped, ignore)
|
|
if (cfg.parentId == -1) continue;
|
|
|
|
// Check if parent exists
|
|
if (cfg.parentId >= 0 && cfg.parentId < MAX_WIDGETS && widgets_[cfg.parentId]) {
|
|
// Parent exists! Get its LVGL object
|
|
lv_obj_t* parentObj = widgets_[cfg.parentId]->getObj();
|
|
if (parentObj) {
|
|
auto widget = WidgetFactory::create(cfg);
|
|
if (widget && cfg.id < MAX_WIDGETS) {
|
|
widget->create(parentObj);
|
|
widget->applyStyle();
|
|
widgets_[cfg.id] = std::move(widget);
|
|
madeProgress = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void WidgetManager::onKnxValue(uint16_t groupAddr, float value, TextSource source) {
|
|
UiEvent event = {};
|
|
event.type = UiEventType::KNX_VALUE;
|
|
event.groupAddr = groupAddr;
|
|
event.textSource = source;
|
|
event.value = value;
|
|
cacheKnxValue(groupAddr, source, value);
|
|
enqueueUiEvent(event);
|
|
}
|
|
|
|
void WidgetManager::onKnxValue(uint16_t groupAddr, float value) {
|
|
onKnxValue(groupAddr, value, TextSource::KNX_DPT_TEMP);
|
|
}
|
|
|
|
void WidgetManager::onKnxSwitch(uint16_t groupAddr, bool value) {
|
|
UiEvent event = {};
|
|
event.type = UiEventType::KNX_SWITCH;
|
|
event.groupAddr = groupAddr;
|
|
event.state = value;
|
|
cacheKnxSwitch(groupAddr, value);
|
|
enqueueUiEvent(event);
|
|
}
|
|
|
|
void WidgetManager::onKnxText(uint16_t groupAddr, const char* text) {
|
|
UiEvent event = {};
|
|
event.type = UiEventType::KNX_TEXT;
|
|
event.groupAddr = groupAddr;
|
|
if (text) {
|
|
const size_t max_len = sizeof(event.text) - 1;
|
|
const size_t src_len = strnlen(text, max_len);
|
|
if (is_valid_utf8(text, src_len)) {
|
|
memcpy(event.text, text, src_len);
|
|
event.text[src_len] = '\0';
|
|
} else {
|
|
latin1_to_utf8(text, src_len, event.text, sizeof(event.text));
|
|
}
|
|
} else {
|
|
event.text[0] = '\0';
|
|
}
|
|
cacheKnxText(groupAddr, event.text);
|
|
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, event.textSource);
|
|
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::applyCachedValuesToWidgets() {
|
|
for (auto& widget : widgets_) {
|
|
if (!widget) continue;
|
|
|
|
uint16_t addr = widget->getKnxAddress();
|
|
if (addr == 0) continue;
|
|
|
|
TextSource source = widget->getTextSource();
|
|
if (source == TextSource::STATIC) continue;
|
|
|
|
if (source == TextSource::KNX_DPT_SWITCH) {
|
|
bool state = false;
|
|
if (getCachedKnxSwitch(addr, &state)) {
|
|
widget->onKnxSwitch(state);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (source == TextSource::KNX_DPT_TEXT) {
|
|
char text[MAX_TEXT_LEN] = {};
|
|
if (getCachedKnxText(addr, text, sizeof(text))) {
|
|
widget->onKnxText(text);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (isNumericTextSource(source)) {
|
|
float value = 0.0f;
|
|
if (getCachedKnxValue(addr, source, &value)) {
|
|
widget->onKnxValue(value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void WidgetManager::applyKnxValue(uint16_t groupAddr, float value, TextSource source) {
|
|
for (auto& widget : widgets_) {
|
|
if (widget && widget->getKnxAddress() == groupAddr &&
|
|
widget->getTextSource() == source) {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
void WidgetManager::cacheKnxValue(uint16_t groupAddr, TextSource source, float value) {
|
|
if (groupAddr == 0) return;
|
|
portENTER_CRITICAL(&knxCacheMux_);
|
|
|
|
size_t freeIndex = KNX_CACHE_SIZE;
|
|
for (size_t i = 0; i < KNX_CACHE_SIZE; ++i) {
|
|
auto& entry = knxNumericCache_[i];
|
|
if (entry.valid) {
|
|
if (entry.groupAddr == groupAddr && entry.source == source) {
|
|
entry.value = value;
|
|
portEXIT_CRITICAL(&knxCacheMux_);
|
|
return;
|
|
}
|
|
} else if (freeIndex == KNX_CACHE_SIZE) {
|
|
freeIndex = i;
|
|
}
|
|
}
|
|
|
|
size_t index = freeIndex;
|
|
if (index == KNX_CACHE_SIZE) {
|
|
index = knxNumericCacheNext_;
|
|
knxNumericCacheNext_ = (knxNumericCacheNext_ + 1) % KNX_CACHE_SIZE;
|
|
}
|
|
auto& entry = knxNumericCache_[index];
|
|
entry.groupAddr = groupAddr;
|
|
entry.source = source;
|
|
entry.value = value;
|
|
entry.valid = true;
|
|
portEXIT_CRITICAL(&knxCacheMux_);
|
|
}
|
|
|
|
void WidgetManager::cacheKnxSwitch(uint16_t groupAddr, bool value) {
|
|
if (groupAddr == 0) return;
|
|
portENTER_CRITICAL(&knxCacheMux_);
|
|
|
|
size_t freeIndex = KNX_CACHE_SIZE;
|
|
for (size_t i = 0; i < KNX_CACHE_SIZE; ++i) {
|
|
auto& entry = knxSwitchCache_[i];
|
|
if (entry.valid) {
|
|
if (entry.groupAddr == groupAddr) {
|
|
entry.value = value;
|
|
portEXIT_CRITICAL(&knxCacheMux_);
|
|
return;
|
|
}
|
|
} else if (freeIndex == KNX_CACHE_SIZE) {
|
|
freeIndex = i;
|
|
}
|
|
}
|
|
|
|
size_t index = freeIndex;
|
|
if (index == KNX_CACHE_SIZE) {
|
|
index = knxSwitchCacheNext_;
|
|
knxSwitchCacheNext_ = (knxSwitchCacheNext_ + 1) % KNX_CACHE_SIZE;
|
|
}
|
|
auto& entry = knxSwitchCache_[index];
|
|
entry.groupAddr = groupAddr;
|
|
entry.value = value;
|
|
entry.valid = true;
|
|
portEXIT_CRITICAL(&knxCacheMux_);
|
|
}
|
|
|
|
void WidgetManager::cacheKnxText(uint16_t groupAddr, const char* text) {
|
|
if (groupAddr == 0) return;
|
|
portENTER_CRITICAL(&knxCacheMux_);
|
|
|
|
size_t freeIndex = KNX_CACHE_SIZE;
|
|
for (size_t i = 0; i < KNX_CACHE_SIZE; ++i) {
|
|
auto& entry = knxTextCache_[i];
|
|
if (entry.valid) {
|
|
if (entry.groupAddr == groupAddr) {
|
|
if (text) {
|
|
strncpy(entry.text, text, MAX_TEXT_LEN - 1);
|
|
entry.text[MAX_TEXT_LEN - 1] = '\0';
|
|
} else {
|
|
entry.text[0] = '\0';
|
|
}
|
|
portEXIT_CRITICAL(&knxCacheMux_);
|
|
return;
|
|
}
|
|
} else if (freeIndex == KNX_CACHE_SIZE) {
|
|
freeIndex = i;
|
|
}
|
|
}
|
|
|
|
size_t index = freeIndex;
|
|
if (index == KNX_CACHE_SIZE) {
|
|
index = knxTextCacheNext_;
|
|
knxTextCacheNext_ = (knxTextCacheNext_ + 1) % KNX_CACHE_SIZE;
|
|
}
|
|
auto& entry = knxTextCache_[index];
|
|
entry.groupAddr = groupAddr;
|
|
if (text) {
|
|
strncpy(entry.text, text, MAX_TEXT_LEN - 1);
|
|
entry.text[MAX_TEXT_LEN - 1] = '\0';
|
|
} else {
|
|
entry.text[0] = '\0';
|
|
}
|
|
entry.valid = true;
|
|
portEXIT_CRITICAL(&knxCacheMux_);
|
|
}
|
|
|
|
bool WidgetManager::getCachedKnxValue(uint16_t groupAddr, TextSource source, float* out) const {
|
|
if (groupAddr == 0 || out == nullptr) return false;
|
|
bool found = false;
|
|
portENTER_CRITICAL(&knxCacheMux_);
|
|
for (const auto& entry : knxNumericCache_) {
|
|
if (entry.valid && entry.groupAddr == groupAddr && entry.source == source) {
|
|
*out = entry.value;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
portEXIT_CRITICAL(&knxCacheMux_);
|
|
return found;
|
|
}
|
|
|
|
bool WidgetManager::getCachedKnxSwitch(uint16_t groupAddr, bool* out) const {
|
|
if (groupAddr == 0 || out == nullptr) return false;
|
|
bool found = false;
|
|
portENTER_CRITICAL(&knxCacheMux_);
|
|
for (const auto& entry : knxSwitchCache_) {
|
|
if (entry.valid && entry.groupAddr == groupAddr) {
|
|
*out = entry.value;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
portEXIT_CRITICAL(&knxCacheMux_);
|
|
return found;
|
|
}
|
|
|
|
bool WidgetManager::getCachedKnxText(uint16_t groupAddr, char* out, size_t outSize) const {
|
|
if (groupAddr == 0 || out == nullptr || outSize == 0) return false;
|
|
bool found = false;
|
|
portENTER_CRITICAL(&knxCacheMux_);
|
|
for (const auto& entry : knxTextCache_) {
|
|
if (entry.valid && entry.groupAddr == groupAddr) {
|
|
strncpy(out, entry.text, outSize - 1);
|
|
out[outSize - 1] = '\0';
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
portEXIT_CRITICAL(&knxCacheMux_);
|
|
return found;
|
|
}
|
|
|
|
bool WidgetManager::isNumericTextSource(TextSource source) {
|
|
return source == TextSource::KNX_DPT_TEMP ||
|
|
source == TextSource::KNX_DPT_PERCENT ||
|
|
source == TextSource::KNX_DPT_POWER ||
|
|
source == TextSource::KNX_DPT_ENERGY ||
|
|
source == TextSource::KNX_DPT_DECIMALFACTOR;
|
|
}
|
|
|
|
// 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);
|
|
cJSON_AddNumberToObject(widget, "textAlign", w.textAlign);
|
|
cJSON_AddBoolToObject(widget, "isContainer", w.isContainer);
|
|
|
|
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);
|
|
|
|
// Icon properties
|
|
cJSON_AddNumberToObject(widget, "iconCodepoint", w.iconCodepoint);
|
|
cJSON_AddNumberToObject(widget, "iconPosition", w.iconPosition);
|
|
cJSON_AddNumberToObject(widget, "iconSize", w.iconSize);
|
|
cJSON_AddNumberToObject(widget, "iconGap", w.iconGap);
|
|
|
|
cJSON_AddNumberToObject(widget, "parentId", w.parentId);
|
|
|
|
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;
|
|
w.textAlign = static_cast<uint8_t>(TextAlign::CENTER);
|
|
w.isContainer = false;
|
|
|
|
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* textAlign = cJSON_GetObjectItem(widget, "textAlign");
|
|
if (cJSON_IsNumber(textAlign)) w.textAlign = textAlign->valueint;
|
|
|
|
cJSON* isContainer = cJSON_GetObjectItem(widget, "isContainer");
|
|
if (cJSON_IsBool(isContainer)) w.isContainer = cJSON_IsTrue(isContainer);
|
|
|
|
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;
|
|
|
|
// Icon properties
|
|
cJSON* iconCodepoint = cJSON_GetObjectItem(widget, "iconCodepoint");
|
|
if (cJSON_IsNumber(iconCodepoint)) w.iconCodepoint = static_cast<uint32_t>(iconCodepoint->valuedouble);
|
|
|
|
cJSON* iconPosition = cJSON_GetObjectItem(widget, "iconPosition");
|
|
if (cJSON_IsNumber(iconPosition)) w.iconPosition = iconPosition->valueint;
|
|
|
|
cJSON* iconSize = cJSON_GetObjectItem(widget, "iconSize");
|
|
if (cJSON_IsNumber(iconSize)) w.iconSize = iconSize->valueint;
|
|
|
|
cJSON* iconGap = cJSON_GetObjectItem(widget, "iconGap");
|
|
if (cJSON_IsNumber(iconGap)) w.iconGap = iconGap->valueint;
|
|
|
|
cJSON* parentId = cJSON_GetObjectItem(widget, "parentId");
|
|
if (cJSON_IsNumber(parentId)) {
|
|
w.parentId = static_cast<int8_t>(parentId->valueint);
|
|
} else {
|
|
w.parentId = -1; // Default to root
|
|
}
|
|
|
|
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;
|
|
}
|
|
ensureButtonLabels(screen);
|
|
|
|
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;
|
|
}
|
|
ensureButtonLabels(screen);
|
|
}
|
|
|
|
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;
|
|
}
|