706 lines
22 KiB
C++
706 lines
22 KiB
C++
#include "WidgetManager.hpp"
|
|
#include "SdCard.hpp"
|
|
#include "esp_lv_adapter.h"
|
|
#include "esp_log.h"
|
|
#include "Gui.hpp"
|
|
#include "cJSON.h"
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <cstdlib>
|
|
|
|
static const char* TAG = "WidgetMgr";
|
|
|
|
// 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 && cfg->knxAddressWrite > 0) {
|
|
lv_obj_t* target = static_cast<lv_obj_t*>(lv_event_get_target(e));
|
|
bool 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);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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;
|
|
return cfg;
|
|
}
|
|
|
|
// ScreenConfig implementation
|
|
void ScreenConfig::clear() {
|
|
backgroundColor = {26, 26, 46}; // Dark blue background
|
|
widgetCount = 0;
|
|
memset(widgets, 0, sizeof(widgets));
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// WidgetManager implementation
|
|
WidgetManager& WidgetManager::instance() {
|
|
static WidgetManager inst;
|
|
return inst;
|
|
}
|
|
|
|
WidgetManager::WidgetManager() {
|
|
widgetObjects_.fill(nullptr);
|
|
createDefaultConfig();
|
|
}
|
|
|
|
void WidgetManager::createDefaultConfig() {
|
|
config_.clear();
|
|
|
|
// Default: Temperature label
|
|
auto tempLabel = WidgetConfig::createKnxLabel(0, 50, 20,
|
|
TextSource::KNX_DPT_TEMP, 1, "%.1f °C");
|
|
tempLabel.fontSize = 3; // 28pt
|
|
config_.addWidget(tempLabel);
|
|
|
|
// Default: KNX Prog button
|
|
auto progBtn = WidgetConfig::createButton(1, 50, 100, "KNX Prog", 0, true);
|
|
progBtn.bgColor = {200, 50, 50}; // Red
|
|
config_.addWidget(progBtn);
|
|
}
|
|
|
|
void WidgetManager::init() {
|
|
loadFromSdCard();
|
|
ESP_LOGI(TAG, "WidgetManager initialized with %d widgets", config_.widgetCount);
|
|
}
|
|
|
|
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 widgets from SD card", config_.widgetCount);
|
|
} 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[8192];
|
|
if (!json) {
|
|
ESP_LOGE(TAG, "Failed to allocate memory for JSON");
|
|
return;
|
|
}
|
|
|
|
getConfigJson(json, 8192);
|
|
|
|
// 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 widgets to SD card", config_.widgetCount);
|
|
} else {
|
|
ESP_LOGE(TAG, "Failed to write config file");
|
|
}
|
|
}
|
|
|
|
void WidgetManager::applyConfig() {
|
|
if (esp_lv_adapter_lock(-1) == ESP_OK) {
|
|
destroyAllWidgets();
|
|
createAllWidgets();
|
|
esp_lv_adapter_unlock();
|
|
}
|
|
}
|
|
|
|
void WidgetManager::saveAndApply() {
|
|
saveToSdCard();
|
|
applyConfig();
|
|
ESP_LOGI(TAG, "Config saved and applied");
|
|
}
|
|
|
|
void WidgetManager::resetToDefaults() {
|
|
createDefaultConfig();
|
|
saveAndApply();
|
|
ESP_LOGI(TAG, "Reset to defaults");
|
|
}
|
|
|
|
void WidgetManager::destroyAllWidgets() {
|
|
for (auto& obj : widgetObjects_) {
|
|
if (obj != nullptr) {
|
|
lv_obj_delete(obj);
|
|
obj = nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
void WidgetManager::createAllWidgets() {
|
|
screen_ = lv_scr_act();
|
|
|
|
// Set background color
|
|
lv_obj_set_style_bg_color(screen_, lv_color_make(
|
|
config_.backgroundColor.r,
|
|
config_.backgroundColor.g,
|
|
config_.backgroundColor.b), 0);
|
|
|
|
// Create all widgets
|
|
for (uint8_t i = 0; i < config_.widgetCount; i++) {
|
|
WidgetConfig& cfg = config_.widgets[i];
|
|
lv_obj_t* obj = createWidget(cfg);
|
|
if (obj != nullptr && cfg.id < MAX_WIDGETS) {
|
|
widgetObjects_[cfg.id] = obj;
|
|
}
|
|
}
|
|
}
|
|
|
|
lv_obj_t* WidgetManager::createWidget(const WidgetConfig& cfg) {
|
|
if (!cfg.visible) return nullptr;
|
|
|
|
lv_obj_t* obj = nullptr;
|
|
|
|
switch (cfg.type) {
|
|
case WidgetType::LABEL: {
|
|
obj = lv_label_create(screen_);
|
|
lv_label_set_text(obj, cfg.text);
|
|
break;
|
|
}
|
|
case WidgetType::BUTTON: {
|
|
obj = lv_btn_create(screen_);
|
|
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_center(label);
|
|
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);
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
for (uint8_t i = 0; i < config_.widgetCount; i++) {
|
|
const WidgetConfig& cfg = config_.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;
|
|
|
|
for (uint8_t i = 0; i < config_.widgetCount; i++) {
|
|
const WidgetConfig& cfg = config_.widgets[i];
|
|
if (cfg.knxAddress == groupAddr && cfg.textSource == TextSource::KNX_DPT_SWITCH) {
|
|
lv_obj_t* obj = widgetObjects_[cfg.id];
|
|
if (obj != nullptr) {
|
|
lv_label_set_text(obj, value ? "EIN" : "AUS");
|
|
}
|
|
}
|
|
}
|
|
|
|
esp_lv_adapter_unlock();
|
|
}
|
|
|
|
void WidgetManager::onKnxText(uint16_t groupAddr, const char* text) {
|
|
if (esp_lv_adapter_lock(100) != ESP_OK) return;
|
|
|
|
for (uint8_t i = 0; i < config_.widgetCount; i++) {
|
|
const WidgetConfig& cfg = config_.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;
|
|
}
|
|
|
|
// Add background color
|
|
char bgColorStr[8];
|
|
snprintf(bgColorStr, sizeof(bgColorStr), "#%02X%02X%02X",
|
|
config_.backgroundColor.r, config_.backgroundColor.g, config_.backgroundColor.b);
|
|
cJSON_AddStringToObject(root, "bgColor", bgColorStr);
|
|
|
|
// Add widgets array
|
|
cJSON* widgets = cJSON_AddArrayToObject(root, "widgets");
|
|
|
|
for (uint8_t i = 0; i < config_.widgetCount; i++) {
|
|
const WidgetConfig& w = config_.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_AddItemToArray(widgets, widget);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Parse background color
|
|
cJSON* bgColor = cJSON_GetObjectItem(root, "bgColor");
|
|
if (cJSON_IsString(bgColor)) {
|
|
config_.backgroundColor = Color::fromHex(parseHexColor(bgColor->valuestring));
|
|
}
|
|
|
|
// Parse widgets array
|
|
cJSON* widgets = cJSON_GetObjectItem(root, "widgets");
|
|
if (!cJSON_IsArray(widgets)) {
|
|
cJSON_Delete(root);
|
|
return false;
|
|
}
|
|
|
|
config_.widgetCount = 0;
|
|
|
|
cJSON* widget = nullptr;
|
|
cJSON_ArrayForEach(widget, widgets) {
|
|
if (config_.widgetCount >= MAX_WIDGETS) break;
|
|
|
|
WidgetConfig& w = config_.widgets[config_.widgetCount];
|
|
memset(&w, 0, sizeof(w));
|
|
|
|
// 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;
|
|
|
|
config_.widgetCount++;
|
|
}
|
|
|
|
cJSON_Delete(root);
|
|
ESP_LOGI(TAG, "Parsed %d widgets from JSON", config_.widgetCount);
|
|
return true;
|
|
}
|