diff --git a/.cache/clangd/index/ArcWidget.cpp.C855CC6EB2D676E3.idx b/.cache/clangd/index/ArcWidget.cpp.C855CC6EB2D676E3.idx new file mode 100644 index 0000000..042a7b2 Binary files /dev/null and b/.cache/clangd/index/ArcWidget.cpp.C855CC6EB2D676E3.idx differ diff --git a/.cache/clangd/index/ArcWidget.hpp.6C1EF191718DCFF6.idx b/.cache/clangd/index/ArcWidget.hpp.6C1EF191718DCFF6.idx new file mode 100644 index 0000000..0da1f0f Binary files /dev/null and b/.cache/clangd/index/ArcWidget.hpp.6C1EF191718DCFF6.idx differ diff --git a/.cache/clangd/index/ButtonMatrixWidget.cpp.13156E59C1946347.idx b/.cache/clangd/index/ButtonMatrixWidget.cpp.13156E59C1946347.idx new file mode 100644 index 0000000..3092e2c Binary files /dev/null and b/.cache/clangd/index/ButtonMatrixWidget.cpp.13156E59C1946347.idx differ diff --git a/.cache/clangd/index/ButtonMatrixWidget.hpp.580A8B6A2F8B776F.idx b/.cache/clangd/index/ButtonMatrixWidget.hpp.580A8B6A2F8B776F.idx new file mode 100644 index 0000000..36f64b5 Binary files /dev/null and b/.cache/clangd/index/ButtonMatrixWidget.hpp.580A8B6A2F8B776F.idx differ diff --git a/.cache/clangd/index/ChartWidget.cpp.344F8561CCF752B4.idx b/.cache/clangd/index/ChartWidget.cpp.344F8561CCF752B4.idx index f12d307..2cf07f5 100644 Binary files a/.cache/clangd/index/ChartWidget.cpp.344F8561CCF752B4.idx and b/.cache/clangd/index/ChartWidget.cpp.344F8561CCF752B4.idx differ diff --git a/.cache/clangd/index/ClockWidget.cpp.6053C0339E915CC8.idx b/.cache/clangd/index/ClockWidget.cpp.6053C0339E915CC8.idx index f0067e6..717de59 100644 Binary files a/.cache/clangd/index/ClockWidget.cpp.6053C0339E915CC8.idx and b/.cache/clangd/index/ClockWidget.cpp.6053C0339E915CC8.idx differ diff --git a/.cache/clangd/index/IconWidget.cpp.C804601C628F2FF0.idx b/.cache/clangd/index/IconWidget.cpp.C804601C628F2FF0.idx index d6d477f..c2fbeac 100644 Binary files a/.cache/clangd/index/IconWidget.cpp.C804601C628F2FF0.idx and b/.cache/clangd/index/IconWidget.cpp.C804601C628F2FF0.idx differ diff --git a/.cache/clangd/index/LabelWidget.cpp.86E5A1BF3C34B7BC.idx b/.cache/clangd/index/LabelWidget.cpp.86E5A1BF3C34B7BC.idx index 6a30001..9261bf6 100644 Binary files a/.cache/clangd/index/LabelWidget.cpp.86E5A1BF3C34B7BC.idx and b/.cache/clangd/index/LabelWidget.cpp.86E5A1BF3C34B7BC.idx differ diff --git a/.cache/clangd/index/LedWidget.cpp.3E101A9B7D9821AD.idx b/.cache/clangd/index/LedWidget.cpp.3E101A9B7D9821AD.idx index a837a94..7d1f1fc 100644 Binary files a/.cache/clangd/index/LedWidget.cpp.3E101A9B7D9821AD.idx and b/.cache/clangd/index/LedWidget.cpp.3E101A9B7D9821AD.idx differ diff --git a/.cache/clangd/index/PowerFlowWidget.cpp.8A280FD116CAAFED.idx b/.cache/clangd/index/PowerFlowWidget.cpp.8A280FD116CAAFED.idx index 43aafcd..6e5b7a1 100644 Binary files a/.cache/clangd/index/PowerFlowWidget.cpp.8A280FD116CAAFED.idx and b/.cache/clangd/index/PowerFlowWidget.cpp.8A280FD116CAAFED.idx differ diff --git a/.cache/clangd/index/PowerNodeWidget.cpp.D068C7972720D9A3.idx b/.cache/clangd/index/PowerNodeWidget.cpp.D068C7972720D9A3.idx index f2bc892..8c85d71 100644 Binary files a/.cache/clangd/index/PowerNodeWidget.cpp.D068C7972720D9A3.idx and b/.cache/clangd/index/PowerNodeWidget.cpp.D068C7972720D9A3.idx differ diff --git a/.cache/clangd/index/RectangleWidget.cpp.80D39D0F2AD3509D.idx b/.cache/clangd/index/RectangleWidget.cpp.80D39D0F2AD3509D.idx new file mode 100644 index 0000000..aa2b8a0 Binary files /dev/null and b/.cache/clangd/index/RectangleWidget.cpp.80D39D0F2AD3509D.idx differ diff --git a/.cache/clangd/index/RectangleWidget.hpp.C8D9E5E0438E507D.idx b/.cache/clangd/index/RectangleWidget.hpp.C8D9E5E0438E507D.idx new file mode 100644 index 0000000..48a6e48 Binary files /dev/null and b/.cache/clangd/index/RectangleWidget.hpp.C8D9E5E0438E507D.idx differ diff --git a/.cache/clangd/index/RoomCardBubbleWidget.cpp.0672D353A93A1667.idx b/.cache/clangd/index/RoomCardBubbleWidget.cpp.0672D353A93A1667.idx index 0290589..ff5e481 100644 Binary files a/.cache/clangd/index/RoomCardBubbleWidget.cpp.0672D353A93A1667.idx and b/.cache/clangd/index/RoomCardBubbleWidget.cpp.0672D353A93A1667.idx differ diff --git a/.cache/clangd/index/RoomCardTileWidget.cpp.4F5E3794B098F0B3.idx b/.cache/clangd/index/RoomCardTileWidget.cpp.4F5E3794B098F0B3.idx index 09d0921..d301aba 100644 Binary files a/.cache/clangd/index/RoomCardTileWidget.cpp.4F5E3794B098F0B3.idx and b/.cache/clangd/index/RoomCardTileWidget.cpp.4F5E3794B098F0B3.idx differ diff --git a/.cache/clangd/index/TabPageWidget.cpp.8BA3B0713CBB6420.idx b/.cache/clangd/index/TabPageWidget.cpp.8BA3B0713CBB6420.idx index 5f70b51..461abf9 100644 Binary files a/.cache/clangd/index/TabPageWidget.cpp.8BA3B0713CBB6420.idx and b/.cache/clangd/index/TabPageWidget.cpp.8BA3B0713CBB6420.idx differ diff --git a/.cache/clangd/index/Widget.cpp.63DB7B9186B85891.idx b/.cache/clangd/index/Widget.cpp.63DB7B9186B85891.idx index a38a563..080f7ba 100644 Binary files a/.cache/clangd/index/Widget.cpp.63DB7B9186B85891.idx and b/.cache/clangd/index/Widget.cpp.63DB7B9186B85891.idx differ diff --git a/.cache/clangd/index/WidgetConfig.cpp.FD56F9F36C29A5DA.idx b/.cache/clangd/index/WidgetConfig.cpp.FD56F9F36C29A5DA.idx index b1ae1f0..2bed32f 100644 Binary files a/.cache/clangd/index/WidgetConfig.cpp.FD56F9F36C29A5DA.idx and b/.cache/clangd/index/WidgetConfig.cpp.FD56F9F36C29A5DA.idx differ diff --git a/.cache/clangd/index/WidgetConfig.hpp.CAEFE2EEEB2A6996.idx b/.cache/clangd/index/WidgetConfig.hpp.CAEFE2EEEB2A6996.idx index d2ca489..bf327c0 100644 Binary files a/.cache/clangd/index/WidgetConfig.hpp.CAEFE2EEEB2A6996.idx and b/.cache/clangd/index/WidgetConfig.hpp.CAEFE2EEEB2A6996.idx differ diff --git a/.cache/clangd/index/WidgetFactory.cpp.1026CAEFCA630F22.idx b/.cache/clangd/index/WidgetFactory.cpp.1026CAEFCA630F22.idx index 6bad1a2..f4e0e5e 100644 Binary files a/.cache/clangd/index/WidgetFactory.cpp.1026CAEFCA630F22.idx and b/.cache/clangd/index/WidgetFactory.cpp.1026CAEFCA630F22.idx differ diff --git a/.cache/clangd/index/WidgetManager.cpp.D8CE609DC911F13E.idx b/.cache/clangd/index/WidgetManager.cpp.D8CE609DC911F13E.idx index 6e038f1..d343169 100644 Binary files a/.cache/clangd/index/WidgetManager.cpp.D8CE609DC911F13E.idx and b/.cache/clangd/index/WidgetManager.cpp.D8CE609DC911F13E.idx differ diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index c257d5e..ef464a3 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -13,6 +13,8 @@ idf_component_register(SRCS "HistoryStore.cpp" "KnxWorker.cpp" "Nvs.cpp" "main.c "widgets/ChartWidget.cpp" "widgets/ClockWidget.cpp" "widgets/RectangleWidget.cpp" + "widgets/ArcWidget.cpp" + "widgets/ButtonMatrixWidget.cpp" "widgets/RoomCardWidgetBase.cpp" "widgets/RoomCardBubbleWidget.cpp" "widgets/RoomCardTileWidget.cpp" diff --git a/main/WidgetConfig.cpp b/main/WidgetConfig.cpp index 59f7014..51d933b 100644 --- a/main/WidgetConfig.cpp +++ b/main/WidgetConfig.cpp @@ -140,6 +140,20 @@ void WidgetConfig::serialize(uint8_t* buf) const { buf[pos++] = 0; // padding buf[pos++] = 0; // padding } + + // Arc properties + buf[pos++] = arcMin & 0xFF; buf[pos++] = (arcMin >> 8) & 0xFF; + buf[pos++] = arcMax & 0xFF; buf[pos++] = (arcMax >> 8) & 0xFF; + buf[pos++] = arcUnit; + buf[pos++] = arcShowValue ? 1 : 0; + buf[pos++] = static_cast(arcScaleOffset); + buf[pos++] = arcScaleColor.r; + buf[pos++] = arcScaleColor.g; + buf[pos++] = arcScaleColor.b; + buf[pos++] = arcValueColor.r; + buf[pos++] = arcValueColor.g; + buf[pos++] = arcValueColor.b; + buf[pos++] = arcValueFontSize; } void WidgetConfig::deserialize(const uint8_t* buf) { @@ -310,6 +324,39 @@ void WidgetConfig::deserialize(const uint8_t* buf) { sb = SubButtonConfig{}; } } + + // Arc properties + if (pos + 6 <= SERIALIZED_SIZE) { + arcMin = static_cast(buf[pos] | (buf[pos + 1] << 8)); pos += 2; + arcMax = static_cast(buf[pos] | (buf[pos + 1] << 8)); pos += 2; + arcUnit = buf[pos++]; + arcShowValue = buf[pos++] != 0; + } else { + arcMin = 0; + arcMax = 100; + arcUnit = 0; + arcShowValue = true; + } + + if (pos + 4 <= SERIALIZED_SIZE) { + arcScaleOffset = static_cast(buf[pos++]); + arcScaleColor.r = buf[pos++]; + arcScaleColor.g = buf[pos++]; + arcScaleColor.b = buf[pos++]; + } else { + arcScaleOffset = 0; + arcScaleColor = textColor; + } + + if (pos + 4 <= SERIALIZED_SIZE) { + arcValueColor.r = buf[pos++]; + arcValueColor.g = buf[pos++]; + arcValueColor.b = buf[pos++]; + arcValueFontSize = buf[pos++]; + } else { + arcValueColor = textColor; + arcValueFontSize = fontSize; + } } WidgetConfig WidgetConfig::createLabel(uint8_t id, int16_t x, int16_t y, const char* labelText) { @@ -334,6 +381,14 @@ WidgetConfig WidgetConfig::createLabel(uint8_t id, int16_t x, int16_t y, const c cfg.borderWidth = 0; cfg.borderColor = {255, 255, 255}; cfg.borderOpacity = 0; + cfg.arcMin = 0; + cfg.arcMax = 100; + cfg.arcUnit = 0; + cfg.arcShowValue = true; + cfg.arcScaleOffset = 0; + cfg.arcScaleColor = cfg.textColor; + cfg.arcValueColor = cfg.textColor; + cfg.arcValueFontSize = cfg.fontSize; cfg.shadow.enabled = false; // Icon defaults cfg.iconCodepoint = 0; @@ -387,6 +442,14 @@ WidgetConfig WidgetConfig::createButton(uint8_t id, int16_t x, int16_t y, cfg.borderWidth = 0; cfg.borderColor = {255, 255, 255}; cfg.borderOpacity = 0; + cfg.arcMin = 0; + cfg.arcMax = 100; + cfg.arcUnit = 0; + cfg.arcShowValue = true; + cfg.arcScaleOffset = 0; + cfg.arcScaleColor = cfg.textColor; + cfg.arcValueColor = cfg.textColor; + cfg.arcValueFontSize = cfg.fontSize; cfg.shadow.enabled = true; cfg.shadow.offsetX = 2; cfg.shadow.offsetY = 2; diff --git a/main/WidgetConfig.hpp b/main/WidgetConfig.hpp index 05794a9..3e5af61 100644 --- a/main/WidgetConfig.hpp +++ b/main/WidgetConfig.hpp @@ -30,6 +30,8 @@ enum class WidgetType : uint8_t { CLOCK = 10, ROOMCARD = 11, RECTANGLE = 12, + ARC = 13, + BUTTONMATRIX = 14, }; enum class IconPosition : uint8_t { @@ -283,9 +285,19 @@ struct WidgetConfig { uint8_t textLineCount; TextLineConfig textLines[MAX_TEXTLINES]; + // Arc properties + int16_t arcMin; + int16_t arcMax; + uint8_t arcUnit; // 0=none, 1=percent, 2=C + bool arcShowValue; // Show center value label + int8_t arcScaleOffset; // Radial offset for scale (ticks/labels) + Color arcScaleColor; // Scale (tick/label) color + Color arcValueColor; // Center value text color + uint8_t arcValueFontSize; // Center value font size index + // Serialization size (fixed for NVS storage) - // 326 + 5 (borderWidth + borderColor + borderOpacity) = 331 - static constexpr size_t SERIALIZED_SIZE = 331; + // 331 + 14 (arcMin + arcMax + arcUnit + arcShowValue + arcScaleOffset + arcScaleColor + arcValueColor + arcValueFontSize) = 345 + static constexpr size_t SERIALIZED_SIZE = 345; void serialize(uint8_t* buf) const; void deserialize(const uint8_t* buf); diff --git a/main/WidgetManager.cpp b/main/WidgetManager.cpp index 3a8d0f6..f91908f 100644 --- a/main/WidgetManager.cpp +++ b/main/WidgetManager.cpp @@ -561,8 +561,8 @@ 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"); + if (cfg.type != WidgetType::BUTTON && cfg.type != WidgetType::BUTTONMATRIX) { + printf("WM: Not a clickable widget!\n"); fflush(stdout); return; } @@ -1552,6 +1552,20 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const { cJSON_AddNumberToObject(widget, "iconGap", w.iconGap); cJSON_AddNumberToObject(widget, "iconPositionX", w.iconPositionX); cJSON_AddNumberToObject(widget, "iconPositionY", w.iconPositionY); + cJSON_AddNumberToObject(widget, "arcMin", w.arcMin); + cJSON_AddNumberToObject(widget, "arcMax", w.arcMax); + cJSON_AddNumberToObject(widget, "arcUnit", w.arcUnit); + cJSON_AddBoolToObject(widget, "arcShowValue", w.arcShowValue); + cJSON_AddNumberToObject(widget, "arcScaleOffset", w.arcScaleOffset); + char arcScaleColorStr[8]; + snprintf(arcScaleColorStr, sizeof(arcScaleColorStr), "#%02X%02X%02X", + w.arcScaleColor.r, w.arcScaleColor.g, w.arcScaleColor.b); + cJSON_AddStringToObject(widget, "arcScaleColor", arcScaleColorStr); + char arcValueColorStr[8]; + snprintf(arcValueColorStr, sizeof(arcValueColorStr), "#%02X%02X%02X", + w.arcValueColor.r, w.arcValueColor.g, w.arcValueColor.b); + cJSON_AddStringToObject(widget, "arcValueColor", arcValueColorStr); + cJSON_AddNumberToObject(widget, "arcValueFontSize", w.arcValueFontSize); cJSON_AddNumberToObject(widget, "parentId", w.parentId); @@ -1748,6 +1762,14 @@ bool WidgetManager::updateConfigFromJson(const char* json) { w.chartTextSource[i] = TextSource::KNX_DPT_TEMP; w.chartSeriesColor[i] = defaultChartColor(i); } + w.arcMin = 0; + w.arcMax = 100; + w.arcUnit = 0; + w.arcShowValue = true; + w.arcScaleOffset = 0; + w.arcScaleColor = w.textColor; + w.arcValueColor = w.textColor; + w.arcValueFontSize = w.fontSize; cJSON* id = cJSON_GetObjectItem(widget, "id"); if (cJSON_IsNumber(id)) w.id = id->valueint; @@ -1871,6 +1893,38 @@ bool WidgetManager::updateConfigFromJson(const char* json) { cJSON* iconPositionY = cJSON_GetObjectItem(widget, "iconPositionY"); if (cJSON_IsNumber(iconPositionY)) w.iconPositionY = iconPositionY->valueint; + + cJSON* arcMin = cJSON_GetObjectItem(widget, "arcMin"); + if (cJSON_IsNumber(arcMin)) w.arcMin = arcMin->valueint; + + cJSON* arcMax = cJSON_GetObjectItem(widget, "arcMax"); + if (cJSON_IsNumber(arcMax)) w.arcMax = arcMax->valueint; + + cJSON* arcUnit = cJSON_GetObjectItem(widget, "arcUnit"); + if (cJSON_IsNumber(arcUnit)) w.arcUnit = arcUnit->valueint; + + cJSON* arcShowValue = cJSON_GetObjectItem(widget, "arcShowValue"); + if (cJSON_IsBool(arcShowValue)) { + w.arcShowValue = cJSON_IsTrue(arcShowValue); + } else if (cJSON_IsNumber(arcShowValue)) { + w.arcShowValue = arcShowValue->valueint != 0; + } + + cJSON* arcScaleOffset = cJSON_GetObjectItem(widget, "arcScaleOffset"); + if (cJSON_IsNumber(arcScaleOffset)) w.arcScaleOffset = static_cast(arcScaleOffset->valueint); + + cJSON* arcScaleColor = cJSON_GetObjectItem(widget, "arcScaleColor"); + if (cJSON_IsString(arcScaleColor)) { + w.arcScaleColor = Color::fromHex(parseHexColor(arcScaleColor->valuestring)); + } + + cJSON* arcValueColor = cJSON_GetObjectItem(widget, "arcValueColor"); + if (cJSON_IsString(arcValueColor)) { + w.arcValueColor = Color::fromHex(parseHexColor(arcValueColor->valuestring)); + } + + cJSON* arcValueFontSize = cJSON_GetObjectItem(widget, "arcValueFontSize"); + if (cJSON_IsNumber(arcValueFontSize)) w.arcValueFontSize = arcValueFontSize->valueint; cJSON* parentId = cJSON_GetObjectItem(widget, "parentId"); if (cJSON_IsNumber(parentId)) { diff --git a/main/widgets/ArcWidget.cpp b/main/widgets/ArcWidget.cpp new file mode 100644 index 0000000..039d4f5 --- /dev/null +++ b/main/widgets/ArcWidget.cpp @@ -0,0 +1,247 @@ +#include "ArcWidget.hpp" + +#include +#include +#include +#include + +ArcWidget::ArcWidget(const WidgetConfig& config) + : Widget(config) +{ +} + +static int32_t clampToRange(float value, int32_t minValue, int32_t maxValue) { + if (maxValue < minValue) std::swap(maxValue, minValue); + if (value < minValue) value = static_cast(minValue); + if (value > maxValue) value = static_cast(maxValue); + return static_cast(std::lround(value)); +} + +int32_t ArcWidget::initialValueFromText() const { + if (config_.text[0] == '\0') return 75; + + char* end = nullptr; + long parsed = strtol(config_.text, &end, 10); + if (end == config_.text) return 75; + + return static_cast(parsed); +} + +lv_obj_t* ArcWidget::create(lv_obj_t* parent) { + obj_ = lv_arc_create(parent); + if (!obj_) return nullptr; + + lv_obj_set_pos(obj_, config_.x, config_.y); + lv_obj_set_size(obj_, + config_.width > 0 ? config_.width : 180, + config_.height > 0 ? config_.height : 180); + lv_obj_clear_flag(obj_, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(obj_, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_flag(obj_, LV_OBJ_FLAG_OVERFLOW_VISIBLE); + lv_obj_set_style_pad_all(obj_, 0, 0); + + lv_arc_set_rotation(obj_, 0); + lv_arc_set_mode(obj_, LV_ARC_MODE_NORMAL); + lv_arc_set_bg_angles(obj_, 135, 45); + + int32_t minValue = config_.arcMin; + int32_t maxValue = config_.arcMax; + if (maxValue <= minValue) { + minValue = 0; + maxValue = 100; + } + lv_arc_set_range(obj_, minValue, maxValue); + + lastValue_ = static_cast(initialValueFromText()); + lv_arc_set_value(obj_, clampToRange(lastValue_, minValue, maxValue)); + + // Create scale as child of arc + scale_ = lv_scale_create(obj_); + if (scale_) { + lv_obj_clear_flag(scale_, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_remove_flag(scale_, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_flag(scale_, LV_OBJ_FLAG_OVERFLOW_VISIBLE); + lv_obj_set_style_bg_opa(scale_, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(scale_, 0, 0); + lv_obj_set_style_pad_all(scale_, 0, 0); + } + + valueLabel_ = lv_label_create(obj_); + if (valueLabel_) { + lv_obj_center(valueLabel_); + updateValueLabel(lastValue_); + } + + return obj_; +} + +void ArcWidget::applyStyle() { + if (!obj_) return; + + applyCommonStyle(); + lv_obj_set_style_bg_opa(obj_, LV_OPA_TRANSP, 0); + + int32_t minValue = config_.arcMin; + int32_t maxValue = config_.arcMax; + if (maxValue <= minValue) { + minValue = 0; + maxValue = 100; + } + lv_arc_set_range(obj_, minValue, maxValue); + + int16_t arcWidth = config_.borderRadius > 0 ? static_cast(config_.borderRadius) : 12; + if (arcWidth < 2) arcWidth = 2; + if (arcWidth > 48) arcWidth = 48; + + lv_color_t trackColor = lv_color_make(config_.bgColor.r, config_.bgColor.g, config_.bgColor.b); + uint8_t trackOpa = config_.bgOpacity > 0 ? config_.bgOpacity : static_cast(LV_OPA_40); + + lv_color_t indicatorColor = lv_color_make( + config_.textColor.r, config_.textColor.g, config_.textColor.b); + + lv_obj_set_style_arc_width(obj_, arcWidth, LV_PART_MAIN); + lv_obj_set_style_arc_color(obj_, trackColor, LV_PART_MAIN); + lv_obj_set_style_arc_opa(obj_, trackOpa, LV_PART_MAIN); + lv_obj_set_style_arc_rounded(obj_, true, LV_PART_MAIN); + + lv_obj_set_style_arc_width(obj_, arcWidth, LV_PART_INDICATOR); + lv_obj_set_style_arc_color(obj_, indicatorColor, LV_PART_INDICATOR); + lv_obj_set_style_arc_opa(obj_, LV_OPA_COVER, LV_PART_INDICATOR); + lv_obj_set_style_arc_rounded(obj_, true, LV_PART_INDICATOR); + + int16_t knobSize = std::max(8, static_cast(arcWidth + 6)); + lv_obj_set_style_arc_width(obj_, knobSize, LV_PART_KNOB); + lv_obj_set_style_bg_color(obj_, indicatorColor, LV_PART_KNOB); + lv_obj_set_style_bg_opa(obj_, LV_OPA_COVER, LV_PART_KNOB); + + if (scale_) { + // Use config dimensions directly, not from arc object (which may have content area limitations) + int16_t scaleWidth = config_.width > 0 ? static_cast(config_.width) : 180; + int16_t scaleHeight = config_.height > 0 ? static_cast(config_.height) : 180; + + lv_obj_set_size(scale_, scaleWidth, scaleHeight); + lv_obj_set_pos(scale_, 0, 0); + lv_obj_set_align(scale_, LV_ALIGN_CENTER); + + lv_scale_set_mode(scale_, LV_SCALE_MODE_ROUND_INNER); + lv_scale_set_angle_range(scale_, 270); + lv_scale_set_rotation(scale_, 135); + lv_scale_set_range(scale_, minValue, maxValue); + lv_scale_set_total_tick_count(scale_, 21); + lv_scale_set_major_tick_every(scale_, 5); + lv_scale_set_label_show(scale_, true); + + int16_t majorLen = std::max(3, static_cast(arcWidth / 3 + 1)); + int16_t minorLen = std::max(2, static_cast(arcWidth / 4 + 1)); + lv_obj_set_style_length(scale_, majorLen, LV_PART_INDICATOR); + lv_obj_set_style_length(scale_, minorLen, LV_PART_ITEMS); + + // Use radial_offset to position ticks at inner edge of arc stroke. + // Positive arcScaleOffset should move the scale outward. + int16_t scaleOffset = static_cast(-config_.arcScaleOffset); + int16_t tickOffset = static_cast(-arcWidth + scaleOffset); + lv_obj_set_style_radial_offset(scale_, tickOffset, LV_PART_INDICATOR); + lv_obj_set_style_radial_offset(scale_, tickOffset, LV_PART_ITEMS); + + lv_obj_set_style_arc_width(scale_, 1, LV_PART_MAIN); + lv_obj_set_style_arc_opa(scale_, LV_OPA_TRANSP, LV_PART_MAIN); + + lv_color_t scaleColor = lv_color_make( + config_.arcScaleColor.r, config_.arcScaleColor.g, config_.arcScaleColor.b); + lv_obj_set_style_line_color(scale_, scaleColor, LV_PART_INDICATOR); + lv_obj_set_style_line_color(scale_, scaleColor, LV_PART_ITEMS); + lv_obj_set_style_line_width(scale_, 2, LV_PART_INDICATOR); + lv_obj_set_style_line_width(scale_, 1, LV_PART_ITEMS); + lv_obj_set_style_line_opa(scale_, LV_OPA_80, LV_PART_INDICATOR); + lv_obj_set_style_line_opa(scale_, LV_OPA_40, LV_PART_ITEMS); + + uint8_t scaleFont = config_.fontSize > 0 ? static_cast(config_.fontSize - 1) : config_.fontSize; + lv_obj_set_style_text_color(scale_, scaleColor, LV_PART_INDICATOR); + lv_obj_set_style_text_opa(scale_, LV_OPA_80, LV_PART_INDICATOR); + lv_obj_set_style_text_font(scale_, getFontBySize(scaleFont), LV_PART_INDICATOR); + lv_obj_set_style_transform_rotation(scale_, LV_SCALE_LABEL_ROTATE_MATCH_TICKS | LV_SCALE_LABEL_ROTATE_KEEP_UPRIGHT, + LV_PART_INDICATOR); + int16_t labelPad = static_cast(-(arcWidth / 2 + 4) + scaleOffset); + lv_obj_set_style_pad_radial(scale_, labelPad, LV_PART_INDICATOR); + } + + if (valueLabel_) { + lv_color_t valueColor = lv_color_make( + config_.arcValueColor.r, config_.arcValueColor.g, config_.arcValueColor.b); + lv_obj_set_style_text_color(valueLabel_, valueColor, 0); + lv_obj_set_style_text_font(valueLabel_, getFontBySize(config_.arcValueFontSize), 0); + lv_obj_center(valueLabel_); + updateValueLabel(lastValue_); + } +} + +void ArcWidget::onKnxValue(float value) { + if (!obj_) return; + + switch (config_.textSource) { + case TextSource::KNX_DPT_TEMP: + case TextSource::KNX_DPT_PERCENT: + case TextSource::KNX_DPT_POWER: + case TextSource::KNX_DPT_ENERGY: + case TextSource::KNX_DPT_DECIMALFACTOR: + lastValue_ = value; + lv_arc_set_value(obj_, clampToRange(value, config_.arcMin, config_.arcMax)); + updateValueLabel(value); + break; + default: + break; + } +} + +void ArcWidget::onKnxSwitch(bool value) { + if (!obj_) return; + if (config_.textSource != TextSource::KNX_DPT_SWITCH) return; + + int32_t minValue = config_.arcMin; + int32_t maxValue = config_.arcMax; + if (maxValue <= minValue) { + minValue = 0; + maxValue = 100; + } + lastValue_ = value ? static_cast(maxValue) : static_cast(minValue); + lv_arc_set_value(obj_, value ? maxValue : minValue); + updateValueLabel(lastValue_); +} + +const char* ArcWidget::unitSuffix(uint8_t unit) { + switch (unit) { + case 1: return "%"; + case 2: return "C"; + default: return ""; + } +} + +void ArcWidget::updateValueLabel(float value) { + if (!valueLabel_) return; + if (!config_.arcShowValue) { + lv_obj_add_flag(valueLabel_, LV_OBJ_FLAG_HIDDEN); + return; + } + lv_obj_clear_flag(valueLabel_, LV_OBJ_FLAG_HIDDEN); + + int32_t minValue = config_.arcMin; + int32_t maxValue = config_.arcMax; + if (maxValue <= minValue) { + minValue = 0; + maxValue = 100; + } + + float clamped = value; + if (clamped < minValue) clamped = static_cast(minValue); + if (clamped > maxValue) clamped = static_cast(maxValue); + + char buf[24]; + const char* suffix = unitSuffix(config_.arcUnit); + bool useDecimal = (config_.textSource == TextSource::KNX_DPT_TEMP); + if (useDecimal) { + std::snprintf(buf, sizeof(buf), "%.1f%s", clamped, suffix); + } else { + std::snprintf(buf, sizeof(buf), "%.0f%s", clamped, suffix); + } + lv_label_set_text(valueLabel_, buf); +} diff --git a/main/widgets/ArcWidget.hpp b/main/widgets/ArcWidget.hpp new file mode 100644 index 0000000..2bf085e --- /dev/null +++ b/main/widgets/ArcWidget.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "Widget.hpp" + +class ArcWidget : public Widget { +public: + explicit ArcWidget(const WidgetConfig& config); + + lv_obj_t* create(lv_obj_t* parent) override; + void applyStyle() override; + + void onKnxValue(float value) override; + void onKnxSwitch(bool value) override; + +private: + int32_t initialValueFromText() const; + void updateValueLabel(float value); + static const char* unitSuffix(uint8_t unit); + + lv_obj_t* scale_ = nullptr; + lv_obj_t* valueLabel_ = nullptr; + float lastValue_ = 0.0f; +}; diff --git a/main/widgets/ButtonMatrixWidget.cpp b/main/widgets/ButtonMatrixWidget.cpp new file mode 100644 index 0000000..0955a2b --- /dev/null +++ b/main/widgets/ButtonMatrixWidget.cpp @@ -0,0 +1,157 @@ +#include "ButtonMatrixWidget.hpp" + +#include "../WidgetManager.hpp" + +#include +#include + +namespace { +std::string trimToken(const std::string& token) { + size_t start = 0; + size_t end = token.size(); + + while (start < end && std::isspace(static_cast(token[start])) != 0) { + start++; + } + while (end > start && std::isspace(static_cast(token[end - 1])) != 0) { + end--; + } + + return token.substr(start, end - start); +} +} + +ButtonMatrixWidget::ButtonMatrixWidget(const WidgetConfig& config) + : Widget(config) +{ +} + +ButtonMatrixWidget::~ButtonMatrixWidget() { + if (obj_) { + lv_obj_remove_event_cb(obj_, valueChangedCallback); + } +} + +void ButtonMatrixWidget::rebuildMapStorage() { + mapStorage_.clear(); + mapPointers_.clear(); + + auto addButton = [&](const std::string& token) { + std::string trimmed = trimToken(token); + if (!trimmed.empty()) { + mapStorage_.push_back(trimmed); + } + }; + + if (config_.text[0] != '\0') { + std::string token; + for (size_t i = 0; config_.text[i] != '\0'; ++i) { + const char ch = config_.text[i]; + if (ch == ';' || ch == ',') { + addButton(token); + token.clear(); + } else if (ch == '\n' || ch == '\r') { + addButton(token); + token.clear(); + if (!mapStorage_.empty() && mapStorage_.back() != "\n") { + mapStorage_.push_back("\n"); + } + if (ch == '\r' && config_.text[i + 1] == '\n') { + ++i; + } + } else { + token.push_back(ch); + } + } + addButton(token); + + while (!mapStorage_.empty() && mapStorage_.back() == "\n") { + mapStorage_.pop_back(); + } + } + + if (mapStorage_.empty()) { + mapStorage_ = {"1", "2", "3", "\n", "4", "5", "6", "\n", "7", "8", "9"}; + } + + mapPointers_.reserve(mapStorage_.size() + 1); + for (const std::string& entry : mapStorage_) { + mapPointers_.push_back(entry.c_str()); + } + mapPointers_.push_back(""); +} + +lv_obj_t* ButtonMatrixWidget::create(lv_obj_t* parent) { + obj_ = lv_buttonmatrix_create(parent); + if (!obj_) return nullptr; + + lv_obj_set_pos(obj_, config_.x, config_.y); + lv_obj_set_size(obj_, + config_.width > 0 ? config_.width : 220, + config_.height > 0 ? config_.height : 140); + lv_obj_clear_flag(obj_, LV_OBJ_FLAG_SCROLLABLE); + + rebuildMapStorage(); + lv_buttonmatrix_set_map(obj_, mapPointers_.data()); + lv_buttonmatrix_set_button_ctrl_all(obj_, LV_BUTTONMATRIX_CTRL_CLICK_TRIG); + + if (config_.isToggle) { + lv_buttonmatrix_set_button_ctrl_all(obj_, LV_BUTTONMATRIX_CTRL_CHECKABLE); + } + + lv_obj_add_event_cb(obj_, valueChangedCallback, LV_EVENT_VALUE_CHANGED, this); + + return obj_; +} + +void ButtonMatrixWidget::applyStyle() { + if (!obj_) return; + + applyCommonStyle(); + + lv_color_t textColor = lv_color_make( + config_.textColor.r, config_.textColor.g, config_.textColor.b); + lv_color_t buttonColor = lv_color_make( + config_.bgColor.r, config_.bgColor.g, config_.bgColor.b); + + uint8_t buttonOpa = config_.bgOpacity > 0 ? config_.bgOpacity : static_cast(LV_OPA_30); + + lv_obj_set_style_text_font(obj_, getFontBySize(config_.fontSize), LV_PART_ITEMS); + lv_obj_set_style_text_color(obj_, textColor, LV_PART_ITEMS); + lv_obj_set_style_bg_color(obj_, buttonColor, LV_PART_ITEMS); + lv_obj_set_style_bg_opa(obj_, buttonOpa, LV_PART_ITEMS); + + if (config_.borderRadius > 0) { + lv_obj_set_style_radius(obj_, config_.borderRadius, LV_PART_ITEMS); + } + + if (config_.borderWidth > 0 && config_.borderOpacity > 0) { + lv_obj_set_style_border_width(obj_, config_.borderWidth, LV_PART_ITEMS); + lv_obj_set_style_border_color(obj_, lv_color_make( + config_.borderColor.r, config_.borderColor.g, config_.borderColor.b), LV_PART_ITEMS); + lv_obj_set_style_border_opa(obj_, config_.borderOpacity, LV_PART_ITEMS); + } else { + lv_obj_set_style_border_width(obj_, 0, LV_PART_ITEMS); + } + + lv_style_selector_t pressedSel = static_cast(LV_PART_ITEMS) | + static_cast(LV_STATE_PRESSED); + lv_style_selector_t checkedSel = static_cast(LV_PART_ITEMS) | + static_cast(LV_STATE_CHECKED); + + lv_obj_set_style_bg_color(obj_, textColor, pressedSel); + lv_obj_set_style_bg_opa(obj_, LV_OPA_40, pressedSel); + lv_obj_set_style_text_color(obj_, lv_color_white(), pressedSel); + + lv_obj_set_style_bg_color(obj_, textColor, checkedSel); + lv_obj_set_style_bg_opa(obj_, LV_OPA_50, checkedSel); + lv_obj_set_style_text_color(obj_, lv_color_white(), checkedSel); +} + +void ButtonMatrixWidget::valueChangedCallback(lv_event_t* e) { + ButtonMatrixWidget* widget = static_cast(lv_event_get_user_data(e)); + if (!widget) return; + + lv_obj_t* target = static_cast(lv_event_get_target(e)); + WidgetManager::instance().handleButtonAction(widget->getConfig(), target); +} diff --git a/main/widgets/ButtonMatrixWidget.hpp b/main/widgets/ButtonMatrixWidget.hpp new file mode 100644 index 0000000..3d06a22 --- /dev/null +++ b/main/widgets/ButtonMatrixWidget.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "Widget.hpp" + +#include +#include + +class ButtonMatrixWidget : public Widget { +public: + explicit ButtonMatrixWidget(const WidgetConfig& config); + ~ButtonMatrixWidget() override; + + lv_obj_t* create(lv_obj_t* parent) override; + void applyStyle() override; + +private: + static void valueChangedCallback(lv_event_t* e); + void rebuildMapStorage(); + + std::vector mapStorage_; + std::vector mapPointers_; +}; diff --git a/main/widgets/WidgetFactory.cpp b/main/widgets/WidgetFactory.cpp index e000247..6bc123a 100644 --- a/main/widgets/WidgetFactory.cpp +++ b/main/widgets/WidgetFactory.cpp @@ -13,6 +13,8 @@ #include "RoomCardBubbleWidget.hpp" #include "RoomCardTileWidget.hpp" #include "RectangleWidget.hpp" +#include "ArcWidget.hpp" +#include "ButtonMatrixWidget.hpp" std::unique_ptr WidgetFactory::create(const WidgetConfig& config) { if (!config.visible) return nullptr; @@ -48,6 +50,10 @@ std::unique_ptr WidgetFactory::create(const WidgetConfig& config) { return std::make_unique(config); case WidgetType::RECTANGLE: return std::make_unique(config); + case WidgetType::ARC: + return std::make_unique(config); + case WidgetType::BUTTONMATRIX: + return std::make_unique(config); default: return nullptr; } diff --git a/web-interface/src/components/SidebarLeft.vue b/web-interface/src/components/SidebarLeft.vue index d2e0050..1b56367 100644 --- a/web-interface/src/components/SidebarLeft.vue +++ b/web-interface/src/components/SidebarLeft.vue @@ -46,6 +46,14 @@ Rechteck Form + +