#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); }