knxdisplay/main/WidgetConfig.cpp
2026-02-01 20:49:09 +01:00

488 lines
16 KiB
C++

#include "WidgetConfig.hpp"
#include <cstring>
#include <cstdio>
// 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++] = 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<uint8_t>(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<uint8_t>(iconGap);
// Hierarchy
buf[pos++] = static_cast<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(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<uint8_t>(cond.op);
buf[pos++] = static_cast<uint8_t>(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<uint8_t>(sb.position);
buf[pos++] = static_cast<uint8_t>(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<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++];
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<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++];
// 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<int8_t>(buf[pos++]);
// Hierarchy
parentId = static_cast<int8_t>(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<TextSource>(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<TextSource>(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<TextSource>(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<ConditionOp>(buf[pos++]);
cond.source = static_cast<ConditionSource>(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<SubButtonPosition>(buf[pos++]);
sb.action = static_cast<SubButtonAction>(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<uint8_t>(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<uint8_t>(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<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;
}