knxdisplay/main/WidgetManager.cpp
2026-01-24 09:25:30 +01:00

1151 lines
37 KiB
C++

#include "WidgetManager.hpp"
#include "SdCard.hpp"
#include "esp_lv_adapter.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "Gui.hpp"
#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;
// Button click callback
static void button_click_cb(lv_event_t* e) {
WidgetConfig* cfg = static_cast<WidgetConfig*>(lv_event_get_user_data(e));
if (!cfg) return;
lv_obj_t* target = static_cast<lv_obj_t*>(lv_event_get_target(e));
WidgetManager::instance().handleButtonAction(*cfg, target);
}
// WidgetConfig implementation
void WidgetConfig::serialize(uint8_t* buf) const {
memset(buf, 0, SERIALIZED_SIZE);
size_t pos = 0;
buf[pos++] = id;
buf[pos++] = static_cast<uint8_t>(type);
buf[pos++] = x & 0xFF; buf[pos++] = (x >> 8) & 0xFF;
buf[pos++] = y & 0xFF; buf[pos++] = (y >> 8) & 0xFF;
buf[pos++] = width & 0xFF; buf[pos++] = (width >> 8) & 0xFF;
buf[pos++] = height & 0xFF; buf[pos++] = (height >> 8) & 0xFF;
buf[pos++] = visible ? 1 : 0;
buf[pos++] = static_cast<uint8_t>(textSource);
memcpy(&buf[pos], text, MAX_TEXT_LEN); pos += MAX_TEXT_LEN;
buf[pos++] = knxAddress & 0xFF; buf[pos++] = (knxAddress >> 8) & 0xFF;
buf[pos++] = fontSize;
buf[pos++] = textColor.r; buf[pos++] = textColor.g; buf[pos++] = textColor.b;
buf[pos++] = bgColor.r; buf[pos++] = bgColor.g; buf[pos++] = bgColor.b;
buf[pos++] = bgOpacity;
buf[pos++] = borderRadius;
buf[pos++] = shadow.offsetX;
buf[pos++] = shadow.offsetY;
buf[pos++] = shadow.blur;
buf[pos++] = shadow.spread;
buf[pos++] = shadow.color.r; buf[pos++] = shadow.color.g; buf[pos++] = shadow.color.b;
buf[pos++] = shadow.enabled ? 1 : 0;
buf[pos++] = isToggle ? 1 : 0;
buf[pos++] = knxAddressWrite & 0xFF; buf[pos++] = (knxAddressWrite >> 8) & 0xFF;
buf[pos++] = static_cast<uint8_t>(action);
buf[pos++] = targetScreen;
}
void WidgetConfig::deserialize(const uint8_t* buf) {
size_t pos = 0;
id = buf[pos++];
type = static_cast<WidgetType>(buf[pos++]);
x = buf[pos] | (buf[pos+1] << 8); pos += 2;
y = buf[pos] | (buf[pos+1] << 8); pos += 2;
width = buf[pos] | (buf[pos+1] << 8); pos += 2;
height = buf[pos] | (buf[pos+1] << 8); pos += 2;
visible = buf[pos++] != 0;
textSource = static_cast<TextSource>(buf[pos++]);
memcpy(text, &buf[pos], MAX_TEXT_LEN); pos += MAX_TEXT_LEN;
text[MAX_TEXT_LEN - 1] = '\0';
knxAddress = buf[pos] | (buf[pos+1] << 8); pos += 2;
fontSize = buf[pos++];
textColor.r = buf[pos++]; textColor.g = buf[pos++]; textColor.b = buf[pos++];
bgColor.r = buf[pos++]; bgColor.g = buf[pos++]; bgColor.b = buf[pos++];
bgOpacity = buf[pos++];
borderRadius = buf[pos++];
shadow.offsetX = static_cast<int8_t>(buf[pos++]);
shadow.offsetY = static_cast<int8_t>(buf[pos++]);
shadow.blur = buf[pos++];
shadow.spread = buf[pos++];
shadow.color.r = buf[pos++]; shadow.color.g = buf[pos++]; shadow.color.b = buf[pos++];
shadow.enabled = buf[pos++] != 0;
isToggle = buf[pos++] != 0;
knxAddressWrite = buf[pos] | (buf[pos+1] << 8);
pos += 2;
action = static_cast<ButtonAction>(buf[pos++]);
targetScreen = buf[pos++];
}
WidgetConfig WidgetConfig::createLabel(uint8_t id, int16_t x, int16_t y, const char* labelText) {
WidgetConfig cfg = {};
cfg.id = id;
cfg.type = WidgetType::LABEL;
cfg.x = x;
cfg.y = y;
cfg.width = 150;
cfg.height = 40;
cfg.visible = true;
cfg.textSource = TextSource::STATIC;
strncpy(cfg.text, labelText, MAX_TEXT_LEN - 1);
cfg.fontSize = 1; // 18pt
cfg.textColor = {255, 255, 255};
cfg.bgColor = {0, 0, 0};
cfg.bgOpacity = 0;
cfg.borderRadius = 0;
cfg.shadow.enabled = false;
return cfg;
}
WidgetConfig WidgetConfig::createKnxLabel(uint8_t id, int16_t x, int16_t y,
TextSource source, uint16_t knxAddr, const char* format) {
WidgetConfig cfg = createLabel(id, x, y, format);
cfg.textSource = source;
cfg.knxAddress = knxAddr;
return cfg;
}
WidgetConfig WidgetConfig::createButton(uint8_t id, int16_t x, int16_t y,
const char* labelText, uint16_t knxAddrWrite, bool toggle) {
WidgetConfig cfg = {};
cfg.id = id;
cfg.type = WidgetType::BUTTON;
cfg.x = x;
cfg.y = y;
cfg.width = 120;
cfg.height = 50;
cfg.visible = true;
cfg.textSource = TextSource::STATIC;
strncpy(cfg.text, labelText, MAX_TEXT_LEN - 1);
cfg.fontSize = 1;
cfg.textColor = {255, 255, 255};
cfg.bgColor = {33, 150, 243}; // Blue
cfg.bgOpacity = 255;
cfg.borderRadius = 8;
cfg.shadow.enabled = true;
cfg.shadow.offsetX = 2;
cfg.shadow.offsetY = 2;
cfg.shadow.blur = 8;
cfg.shadow.spread = 0;
cfg.shadow.color = {0, 0, 0};
cfg.isToggle = toggle;
cfg.knxAddressWrite = knxAddrWrite;
cfg.action = ButtonAction::KNX;
cfg.targetScreen = 0;
return cfg;
}
// ScreenConfig implementation
void ScreenConfig::clear(uint8_t newId, const char* newName) {
id = newId;
mode = ScreenMode::FULLSCREEN;
backgroundColor = {26, 26, 46}; // Dark blue background
widgetCount = 0;
memset(widgets, 0, sizeof(widgets));
memset(name, 0, sizeof(name));
if (newName && newName[0] != '\0') {
strncpy(name, newName, sizeof(name) - 1);
}
}
int ScreenConfig::addWidget(const WidgetConfig& widget) {
if (widgetCount >= MAX_WIDGETS) return -1;
// Find next free ID
uint8_t newId = 0;
for (uint8_t i = 0; i < widgetCount; i++) {
if (widgets[i].id >= newId) newId = widgets[i].id + 1;
}
widgets[widgetCount] = widget;
widgets[widgetCount].id = newId;
widgetCount++;
return newId;
}
bool ScreenConfig::removeWidget(uint8_t id) {
for (uint8_t i = 0; i < widgetCount; i++) {
if (widgets[i].id == id) {
// Shift remaining widgets
for (uint8_t j = i; j < widgetCount - 1; j++) {
widgets[j] = widgets[j + 1];
}
widgetCount--;
return true;
}
}
return false;
}
WidgetConfig* ScreenConfig::findWidget(uint8_t id) {
for (uint8_t i = 0; i < widgetCount; i++) {
if (widgets[i].id == id) return &widgets[i];
}
return nullptr;
}
const WidgetConfig* ScreenConfig::findWidget(uint8_t id) const {
for (uint8_t i = 0; i < widgetCount; i++) {
if (widgets[i].id == id) return &widgets[i];
}
return nullptr;
}
// GuiConfig implementation
void GuiConfig::clear() {
screenCount = 0;
startScreenId = 0;
standbyEnabled = false;
standbyScreenId = 0xFF;
standbyMinutes = 0;
for (size_t i = 0; i < MAX_SCREENS; i++) {
screens[i].clear(static_cast<uint8_t>(i), nullptr);
}
}
ScreenConfig* GuiConfig::findScreen(uint8_t id) {
for (uint8_t i = 0; i < screenCount; i++) {
if (screens[i].id == id) return &screens[i];
}
return nullptr;
}
const ScreenConfig* GuiConfig::findScreen(uint8_t id) const {
for (uint8_t i = 0; i < screenCount; i++) {
if (screens[i].id == id) return &screens[i];
}
return nullptr;
}
// WidgetManager implementation
WidgetManager& WidgetManager::instance() {
static WidgetManager inst;
return inst;
}
WidgetManager::WidgetManager() {
widgetObjects_.fill(nullptr);
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;
}
// Get file size
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;
}
// Read file content
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';
// Parse JSON using cJSON
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;
}
// Generate JSON using cJSON
char* json = new char[32768];
if (!json) {
ESP_LOGE(TAG, "Failed to allocate memory for JSON");
return;
}
getConfigJson(json, 32768);
// Write to file
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) {
ScreenConfig* screen = config_.findScreen(screenId);
if (!screen) {
ESP_LOGW(TAG, "Screen %d not found", screenId);
return;
}
if (modalContainer_) {
closeModal();
}
if (esp_lv_adapter_lock(-1) == ESP_OK) {
lv_obj_t* root = lv_scr_act();
lv_obj_clean(root);
widgetObjects_.fill(nullptr);
createAllWidgets(*screen, root);
esp_lv_adapter_unlock();
}
}
void WidgetManager::showModalScreen(const ScreenConfig& screen) {
if (modalContainer_) {
closeModal();
}
if (esp_lv_adapter_lock(-1) != ESP_OK) return;
lv_obj_t* overlay = lv_obj_create(lv_layer_top());
lv_obj_set_style_bg_opa(overlay, LV_OPA_COVER, 0);
lv_obj_clear_flag(overlay, LV_OBJ_FLAG_SCROLLABLE);
lv_disp_t* disp = lv_disp_get_default();
int32_t hor = disp ? lv_disp_get_hor_res(disp) : 1280;
int32_t ver = disp ? lv_disp_get_ver_res(disp) : 800;
lv_obj_set_size(overlay, hor, ver);
modalContainer_ = overlay;
modalScreenId_ = screen.id;
createAllWidgets(screen, modalContainer_);
esp_lv_adapter_unlock();
}
void WidgetManager::closeModal() {
if (!modalContainer_) return;
if (esp_lv_adapter_lock(-1) == ESP_OK) {
lv_obj_delete(modalContainer_);
esp_lv_adapter_unlock();
}
modalContainer_ = nullptr;
modalScreenId_ = SCREEN_ID_NONE;
widgetObjects_.fill(nullptr);
}
void WidgetManager::showScreen(uint8_t screenId) {
ScreenConfig* screen = config_.findScreen(screenId);
if (!screen) {
ESP_LOGW(TAG, "Screen %d not found", screenId);
return;
}
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;
onUserActivity();
switch (cfg.action) {
case ButtonAction::JUMP:
navPending_ = true;
navAction_ = ButtonAction::JUMP;
navTargetScreen_ = cfg.targetScreen;
break;
case ButtonAction::BACK:
navPending_ = true;
navAction_ = ButtonAction::BACK;
navTargetScreen_ = SCREEN_ID_NONE;
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
// Gui::knxWorker.writeSwitch(cfg.knxAddressWrite, state);
}
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() {
if (navPending_) {
navPending_ = false;
if (navAction_ == ButtonAction::JUMP) {
showScreen(navTargetScreen_);
} else if (navAction_ == ButtonAction::BACK) {
goBack();
}
return;
}
if (standbyWakePending_) {
standbyWakePending_ = false;
if (standbyWakeTarget_ != SCREEN_ID_NONE) {
activeScreenId_ = standbyWakeTarget_;
applyScreen(activeScreenId_);
}
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& obj : widgetObjects_) {
if (obj != nullptr) {
lv_obj_delete(obj);
obj = nullptr;
}
}
}
void WidgetManager::createAllWidgets(const ScreenConfig& screen, lv_obj_t* parent) {
screen_ = parent;
widgetObjects_.fill(nullptr);
// Set background color
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);
// Create all widgets
for (uint8_t i = 0; i < screen.widgetCount; i++) {
const WidgetConfig& cfg = screen.widgets[i];
lv_obj_t* obj = createWidget(cfg, parent);
if (obj != nullptr && cfg.id < MAX_WIDGETS) {
widgetObjects_[cfg.id] = obj;
}
}
}
lv_obj_t* WidgetManager::createWidget(const WidgetConfig& cfg, lv_obj_t* parent) {
if (!cfg.visible) return nullptr;
lv_obj_t* obj = nullptr;
switch (cfg.type) {
case WidgetType::LABEL: {
obj = lv_label_create(parent);
lv_label_set_text(obj, cfg.text);
break;
}
case WidgetType::BUTTON: {
obj = lv_btn_create(parent);
if (cfg.isToggle) {
lv_obj_add_flag(obj, LV_OBJ_FLAG_CHECKABLE);
}
lv_obj_add_event_cb(obj, button_click_cb, LV_EVENT_CLICKED,
const_cast<WidgetConfig*>(&cfg));
// Create label inside button
lv_obj_t* label = lv_label_create(obj);
lv_label_set_text(label, cfg.text);
lv_obj_set_style_text_color(label, lv_color_make(
cfg.textColor.r, cfg.textColor.g, cfg.textColor.b), 0);
lv_obj_set_style_text_font(label, getFontBySize(cfg.fontSize), 0);
lv_obj_center(label);
break;
}
case WidgetType::LED: {
obj = lv_led_create(parent);
break;
}
}
if (obj != nullptr) {
lv_obj_set_pos(obj, cfg.x, cfg.y);
if (cfg.width > 0 && cfg.height > 0) {
lv_obj_set_size(obj, cfg.width, cfg.height);
}
if (cfg.type == WidgetType::LED) {
applyLedStyle(obj, cfg);
} else {
applyStyle(obj, cfg);
}
}
return obj;
}
void WidgetManager::applyStyle(lv_obj_t* obj, const WidgetConfig& cfg) {
// Text color
lv_obj_set_style_text_color(obj, lv_color_make(
cfg.textColor.r, cfg.textColor.g, cfg.textColor.b), 0);
// Font
lv_obj_set_style_text_font(obj, getFontBySize(cfg.fontSize), 0);
// Background (for buttons and labels with bg)
if (cfg.bgOpacity > 0) {
lv_obj_set_style_bg_color(obj, lv_color_make(
cfg.bgColor.r, cfg.bgColor.g, cfg.bgColor.b), 0);
lv_obj_set_style_bg_opa(obj, cfg.bgOpacity, 0);
}
// Border radius
if (cfg.borderRadius > 0) {
lv_obj_set_style_radius(obj, cfg.borderRadius, 0);
}
// Shadow
if (cfg.shadow.enabled) {
lv_obj_set_style_shadow_color(obj, lv_color_make(
cfg.shadow.color.r, cfg.shadow.color.g, cfg.shadow.color.b), 0);
lv_obj_set_style_shadow_opa(obj, 180, 0);
lv_obj_set_style_shadow_width(obj, cfg.shadow.blur, 0);
lv_obj_set_style_shadow_spread(obj, cfg.shadow.spread, 0);
lv_obj_set_style_shadow_offset_x(obj, cfg.shadow.offsetX, 0);
lv_obj_set_style_shadow_offset_y(obj, cfg.shadow.offsetY, 0);
}
}
void WidgetManager::applyLedStyle(lv_obj_t* obj, const WidgetConfig& cfg) {
lv_obj_set_style_radius(obj, LV_RADIUS_CIRCLE, 0);
lv_led_set_color(obj, lv_color_make(
cfg.bgColor.r, cfg.bgColor.g, cfg.bgColor.b));
lv_led_set_brightness(obj, cfg.bgOpacity);
if (cfg.shadow.enabled) {
lv_obj_set_style_shadow_color(obj, lv_color_make(
cfg.shadow.color.r, cfg.shadow.color.g, cfg.shadow.color.b), 0);
lv_obj_set_style_shadow_opa(obj, 180, 0);
lv_obj_set_style_shadow_width(obj, cfg.shadow.blur, 0);
lv_obj_set_style_shadow_spread(obj, cfg.shadow.spread, 0);
lv_obj_set_style_shadow_offset_x(obj, cfg.shadow.offsetX, 0);
lv_obj_set_style_shadow_offset_y(obj, cfg.shadow.offsetY, 0);
}
}
const lv_font_t* WidgetManager::getFontBySize(uint8_t sizeIndex) {
// Font sizes: 0=14, 1=18, 2=22, 3=28, 4=36, 5=48
// These must be enabled in sdkconfig (CONFIG_LV_FONT_MONTSERRAT_*)
switch (sizeIndex) {
case 0: return &lv_font_montserrat_14;
#if LV_FONT_MONTSERRAT_18
case 1: return &lv_font_montserrat_18;
#endif
#if LV_FONT_MONTSERRAT_22
case 2: return &lv_font_montserrat_22;
#endif
#if LV_FONT_MONTSERRAT_28
case 3: return &lv_font_montserrat_28;
#endif
#if LV_FONT_MONTSERRAT_36
case 4: return &lv_font_montserrat_36;
#endif
#if LV_FONT_MONTSERRAT_48
case 5: return &lv_font_montserrat_48;
#endif
default: return &lv_font_montserrat_14;
}
}
void WidgetManager::onKnxValue(uint16_t groupAddr, float value) {
if (esp_lv_adapter_lock(100) != ESP_OK) return;
const ScreenConfig* screen = activeScreen();
if (!screen) {
esp_lv_adapter_unlock();
return;
}
for (uint8_t i = 0; i < screen->widgetCount; i++) {
const WidgetConfig& cfg = screen->widgets[i];
if (cfg.knxAddress == groupAddr && cfg.textSource == TextSource::KNX_DPT_TEMP) {
lv_obj_t* obj = widgetObjects_[cfg.id];
if (obj != nullptr) {
char buf[32];
snprintf(buf, sizeof(buf), cfg.text, value);
lv_label_set_text(obj, buf);
}
}
}
esp_lv_adapter_unlock();
}
void WidgetManager::onKnxSwitch(uint16_t groupAddr, bool value) {
if (esp_lv_adapter_lock(100) != ESP_OK) return;
const ScreenConfig* screen = activeScreen();
if (!screen) {
esp_lv_adapter_unlock();
return;
}
for (uint8_t i = 0; i < screen->widgetCount; i++) {
const WidgetConfig& cfg = screen->widgets[i];
if (cfg.knxAddress == groupAddr && cfg.textSource == TextSource::KNX_DPT_SWITCH) {
lv_obj_t* obj = widgetObjects_[cfg.id];
if (obj == nullptr) continue;
if (cfg.type == WidgetType::LABEL) {
lv_label_set_text(obj, value ? "EIN" : "AUS");
} else if (cfg.type == WidgetType::LED) {
uint8_t brightness = value ? (cfg.bgOpacity > 0 ? cfg.bgOpacity : 255) : 0;
lv_led_set_brightness(obj, brightness);
}
}
}
esp_lv_adapter_unlock();
}
void WidgetManager::onKnxText(uint16_t groupAddr, const char* text) {
if (esp_lv_adapter_lock(100) != ESP_OK) return;
const ScreenConfig* screen = activeScreen();
if (!screen) {
esp_lv_adapter_unlock();
return;
}
for (uint8_t i = 0; i < screen->widgetCount; i++) {
const WidgetConfig& cfg = screen->widgets[i];
if (cfg.knxAddress == groupAddr && cfg.textSource == TextSource::KNX_DPT_TEXT) {
lv_obj_t* obj = widgetObjects_[cfg.id];
if (obj != nullptr) {
lv_label_set_text(obj, text);
}
}
}
esp_lv_adapter_unlock();
}
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);
// Add screens array
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);
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);
// Text color
char textColorStr[8];
snprintf(textColorStr, sizeof(textColorStr), "#%02X%02X%02X",
w.textColor.r, w.textColor.g, w.textColor.b);
cJSON_AddStringToObject(widget, "textColor", textColorStr);
// Background color
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);
// Shadow object
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);
}
// Print to buffer
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);
}
// 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);
}
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;
// Parse basic properties
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;
// Parse colors
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;
// Parse shadow
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));
}
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;
}