#include "WidgetConfig.hpp" #include #include // 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(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(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++] = textAlign; buf[pos++] = isContainer ? 1 : 0; 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(action); buf[pos++] = targetScreen; // Icon properties buf[pos++] = iconCodepoint & 0xFF; buf[pos++] = (iconCodepoint >> 8) & 0xFF; buf[pos++] = (iconCodepoint >> 16) & 0xFF; buf[pos++] = (iconCodepoint >> 24) & 0xFF; buf[pos++] = iconPosition; buf[pos++] = iconSize; buf[pos++] = static_cast(iconGap); // Hierarchy buf[pos++] = static_cast(parentId); // Chart properties buf[pos++] = chartPeriod; buf[pos++] = chartSeriesCount; for (size_t i = 0; i < CHART_MAX_SERIES; ++i) { buf[pos++] = chartKnxAddress[i] & 0xFF; buf[pos++] = (chartKnxAddress[i] >> 8) & 0xFF; } for (size_t i = 0; i < CHART_MAX_SERIES; ++i) { buf[pos++] = static_cast(chartTextSource[i]); } for (size_t i = 0; i < CHART_MAX_SERIES; ++i) { buf[pos++] = chartSeriesColor[i].r; buf[pos++] = chartSeriesColor[i].g; buf[pos++] = chartSeriesColor[i].b; } // Secondary KNX address (left value) buf[pos++] = knxAddress2 & 0xFF; buf[pos++] = (knxAddress2 >> 8) & 0xFF; buf[pos++] = static_cast(textSource2); memcpy(&buf[pos], text2, MAX_FORMAT_LEN); pos += MAX_FORMAT_LEN; // Tertiary KNX address (right value) buf[pos++] = knxAddress3 & 0xFF; buf[pos++] = (knxAddress3 >> 8) & 0xFF; buf[pos++] = static_cast(textSource3); memcpy(&buf[pos], text3, MAX_FORMAT_LEN); pos += MAX_FORMAT_LEN; // Conditions buf[pos++] = conditionCount; for (size_t i = 0; i < MAX_CONDITIONS; ++i) { const StyleCondition& cond = conditions[i]; // threshold (4 bytes as float) memcpy(&buf[pos], &cond.threshold, sizeof(float)); pos += sizeof(float); buf[pos++] = static_cast(cond.op); buf[pos++] = static_cast(cond.source); buf[pos++] = cond.priority; // ConditionStyle buf[pos++] = cond.style.iconCodepoint & 0xFF; buf[pos++] = (cond.style.iconCodepoint >> 8) & 0xFF; buf[pos++] = (cond.style.iconCodepoint >> 16) & 0xFF; buf[pos++] = (cond.style.iconCodepoint >> 24) & 0xFF; buf[pos++] = cond.style.textColor.r; buf[pos++] = cond.style.textColor.g; buf[pos++] = cond.style.textColor.b; buf[pos++] = cond.style.bgColor.r; buf[pos++] = cond.style.bgColor.g; buf[pos++] = cond.style.bgColor.b; buf[pos++] = cond.style.bgOpacity; buf[pos++] = cond.style.flags; buf[pos++] = cond.enabled ? 1 : 0; } // RoomCard sub-buttons buf[pos++] = subButtonCount; buf[pos++] = subButtonSize; buf[pos++] = subButtonDistance; for (size_t i = 0; i < MAX_SUBBUTTONS; ++i) { const SubButtonConfig& sb = subButtons[i]; buf[pos++] = sb.iconCodepoint & 0xFF; buf[pos++] = (sb.iconCodepoint >> 8) & 0xFF; buf[pos++] = (sb.iconCodepoint >> 16) & 0xFF; buf[pos++] = (sb.iconCodepoint >> 24) & 0xFF; buf[pos++] = sb.knxAddrRead & 0xFF; buf[pos++] = (sb.knxAddrRead >> 8) & 0xFF; buf[pos++] = sb.knxAddrWrite & 0xFF; buf[pos++] = (sb.knxAddrWrite >> 8) & 0xFF; buf[pos++] = sb.colorOn.r; buf[pos++] = sb.colorOn.g; buf[pos++] = sb.colorOn.b; buf[pos++] = sb.colorOff.r; buf[pos++] = sb.colorOff.g; buf[pos++] = sb.colorOff.b; buf[pos++] = static_cast(sb.position); buf[pos++] = static_cast(sb.action); buf[pos++] = sb.targetScreen; buf[pos++] = sb.enabled ? 1 : 0; buf[pos++] = 0; // padding buf[pos++] = 0; // padding } } void WidgetConfig::deserialize(const uint8_t* buf) { size_t pos = 0; id = buf[pos++]; type = static_cast(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(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++]; textAlign = buf[pos++]; isContainer = buf[pos++] != 0; 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(buf[pos++]); shadow.offsetY = static_cast(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(buf[pos++]); targetScreen = buf[pos++]; // Icon properties iconCodepoint = buf[pos] | (buf[pos+1] << 8) | (buf[pos+2] << 16) | (buf[pos+3] << 24); pos += 4; iconPosition = buf[pos++]; iconSize = buf[pos++]; iconGap = static_cast(buf[pos++]); // Hierarchy parentId = static_cast(buf[pos++]); // Chart properties chartPeriod = buf[pos++]; chartSeriesCount = buf[pos++]; for (size_t i = 0; i < CHART_MAX_SERIES; ++i) { chartKnxAddress[i] = buf[pos] | (buf[pos + 1] << 8); pos += 2; } for (size_t i = 0; i < CHART_MAX_SERIES; ++i) { chartTextSource[i] = static_cast(buf[pos++]); } for (size_t i = 0; i < CHART_MAX_SERIES; ++i) { chartSeriesColor[i].r = buf[pos++]; chartSeriesColor[i].g = buf[pos++]; chartSeriesColor[i].b = buf[pos++]; } // Secondary KNX address (left value) - check bounds for backward compatibility if (pos + 19 <= SERIALIZED_SIZE) { knxAddress2 = buf[pos] | (buf[pos + 1] << 8); pos += 2; textSource2 = static_cast(buf[pos++]); memcpy(text2, &buf[pos], MAX_FORMAT_LEN); pos += MAX_FORMAT_LEN; text2[MAX_FORMAT_LEN - 1] = '\0'; } else { knxAddress2 = 0; textSource2 = TextSource::STATIC; text2[0] = '\0'; } // Tertiary KNX address (right value) if (pos + 19 <= SERIALIZED_SIZE) { knxAddress3 = buf[pos] | (buf[pos + 1] << 8); pos += 2; textSource3 = static_cast(buf[pos++]); memcpy(text3, &buf[pos], MAX_FORMAT_LEN); pos += MAX_FORMAT_LEN; text3[MAX_FORMAT_LEN - 1] = '\0'; } else { knxAddress3 = 0; textSource3 = TextSource::STATIC; text3[0] = '\0'; } // Conditions if (pos + 1 <= SERIALIZED_SIZE) { conditionCount = buf[pos++]; if (conditionCount > MAX_CONDITIONS) conditionCount = MAX_CONDITIONS; } else { conditionCount = 0; } for (size_t i = 0; i < MAX_CONDITIONS; ++i) { StyleCondition& cond = conditions[i]; if (pos + 20 <= SERIALIZED_SIZE) { memcpy(&cond.threshold, &buf[pos], sizeof(float)); pos += sizeof(float); cond.op = static_cast(buf[pos++]); cond.source = static_cast(buf[pos++]); cond.priority = buf[pos++]; cond.style.iconCodepoint = buf[pos] | (buf[pos + 1] << 8) | (buf[pos + 2] << 16) | (buf[pos + 3] << 24); pos += 4; cond.style.textColor.r = buf[pos++]; cond.style.textColor.g = buf[pos++]; cond.style.textColor.b = buf[pos++]; cond.style.bgColor.r = buf[pos++]; cond.style.bgColor.g = buf[pos++]; cond.style.bgColor.b = buf[pos++]; cond.style.bgOpacity = buf[pos++]; cond.style.flags = buf[pos++]; cond.enabled = buf[pos++] != 0; } else { cond = StyleCondition{}; } } // RoomCard sub-buttons if (pos + 3 <= SERIALIZED_SIZE) { subButtonCount = buf[pos++]; if (subButtonCount > MAX_SUBBUTTONS) subButtonCount = MAX_SUBBUTTONS; subButtonSize = buf[pos++]; if (subButtonSize == 0) subButtonSize = 40; // Default subButtonDistance = buf[pos++]; if (subButtonDistance == 0) subButtonDistance = 80; // Default 80px } else { subButtonCount = 0; subButtonSize = 40; subButtonDistance = 80; } for (size_t i = 0; i < MAX_SUBBUTTONS; ++i) { SubButtonConfig& sb = subButtons[i]; if (pos + 20 <= SERIALIZED_SIZE) { sb.iconCodepoint = buf[pos] | (buf[pos + 1] << 8) | (buf[pos + 2] << 16) | (buf[pos + 3] << 24); pos += 4; sb.knxAddrRead = buf[pos] | (buf[pos + 1] << 8); pos += 2; sb.knxAddrWrite = buf[pos] | (buf[pos + 1] << 8); pos += 2; sb.colorOn.r = buf[pos++]; sb.colorOn.g = buf[pos++]; sb.colorOn.b = buf[pos++]; sb.colorOff.r = buf[pos++]; sb.colorOff.g = buf[pos++]; sb.colorOff.b = buf[pos++]; sb.position = static_cast(buf[pos++]); sb.action = static_cast(buf[pos++]); sb.targetScreen = buf[pos++]; sb.enabled = buf[pos++] != 0; pos += 2; // padding } else { sb = SubButtonConfig{}; } } } WidgetConfig WidgetConfig::createLabel(uint8_t id, int16_t x, int16_t y, const char* labelText) { WidgetConfig cfg = {}; cfg.id = id; cfg.parentId = -1; // Root 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.textAlign = static_cast(TextAlign::LEFT); cfg.isContainer = false; cfg.textColor = {255, 255, 255}; cfg.bgColor = {0, 0, 0}; cfg.bgOpacity = 0; cfg.borderRadius = 0; cfg.shadow.enabled = false; // Icon defaults cfg.iconCodepoint = 0; cfg.iconPosition = 0; cfg.iconSize = 1; cfg.iconGap = 8; // Secondary/tertiary address defaults cfg.knxAddress2 = 0; cfg.textSource2 = TextSource::STATIC; cfg.text2[0] = '\0'; cfg.knxAddress3 = 0; cfg.textSource3 = TextSource::STATIC; cfg.text3[0] = '\0'; // Conditions cfg.conditionCount = 0; // Sub-buttons cfg.subButtonCount = 0; cfg.subButtonSize = 40; cfg.subButtonDistance = 80; 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.parentId = -1; // Root 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.textAlign = static_cast(TextAlign::CENTER); cfg.isContainer = true; 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; // Icon defaults cfg.iconCodepoint = 0; cfg.iconPosition = 0; cfg.iconSize = 1; cfg.iconGap = 8; // Secondary/tertiary address defaults cfg.knxAddress2 = 0; cfg.textSource2 = TextSource::STATIC; cfg.text2[0] = '\0'; cfg.knxAddress3 = 0; cfg.textSource3 = TextSource::STATIC; cfg.text3[0] = '\0'; // Conditions cfg.conditionCount = 0; // Sub-buttons cfg.subButtonCount = 0; cfg.subButtonSize = 40; cfg.subButtonDistance = 80; 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 bgImagePath[0] = '\0'; // No background image bgImageMode = BgImageMode::STRETCH; widgetCount = 0; memset(widgets, 0, sizeof(widgets)); memset(name, 0, sizeof(name)); if (newName && newName[0] != '\0') { strncpy(name, newName, sizeof(name) - 1); } // Modal defaults modalX = 0; // 0 = centered modalY = 0; // 0 = centered modalWidth = 0; // 0 = auto modalHeight = 0; // 0 = auto modalBorderRadius = 12; modalDimBackground = true; } 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; knxTimeAddress = 0; knxDateAddress = 0; knxDateTimeAddress = 0; knxNightModeAddress = 0; for (size_t i = 0; i < MAX_SCREENS; i++) { screens[i].clear(static_cast(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; }