Fixes
This commit is contained in:
parent
7e451b4e3b
commit
bef0d5504b
@ -11,6 +11,7 @@ idf_component_register(SRCS "HistoryStore.cpp" "KnxWorker.cpp" "Nvs.cpp" "main.c
|
||||
"widgets/PowerNodeWidget.cpp"
|
||||
"widgets/PowerLinkWidget.cpp"
|
||||
"widgets/ChartWidget.cpp"
|
||||
"widgets/ClockWidget.cpp"
|
||||
"webserver/WebServer.cpp"
|
||||
"webserver/StaticFileHandlers.cpp"
|
||||
"webserver/ConfigHandlers.cpp"
|
||||
|
||||
@ -22,6 +22,7 @@ enum class WidgetType : uint8_t {
|
||||
POWERNODE = 7,
|
||||
POWERLINK = 8,
|
||||
CHART = 9,
|
||||
CLOCK = 10,
|
||||
};
|
||||
|
||||
enum class IconPosition : uint8_t {
|
||||
|
||||
151
main/widgets/ClockWidget.cpp
Normal file
151
main/widgets/ClockWidget.cpp
Normal file
@ -0,0 +1,151 @@
|
||||
#include "ClockWidget.hpp"
|
||||
#include <cmath>
|
||||
|
||||
#ifndef PI
|
||||
#define PI 3.14159265358979323846f
|
||||
#endif
|
||||
|
||||
ClockWidget::ClockWidget(const WidgetConfig& config)
|
||||
: Widget(config)
|
||||
{
|
||||
}
|
||||
|
||||
lv_obj_t* ClockWidget::create(lv_obj_t* parent) {
|
||||
obj_ = lv_obj_create(parent);
|
||||
if (!obj_) return nullptr;
|
||||
|
||||
lv_obj_remove_style_all(obj_);
|
||||
lv_obj_set_pos(obj_, config_.x, config_.y);
|
||||
lv_obj_set_size(obj_,
|
||||
config_.width > 0 ? config_.width : 200,
|
||||
config_.height > 0 ? config_.height : 200);
|
||||
lv_obj_clear_flag(obj_, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
// Create hands
|
||||
hourHand_ = lv_line_create(obj_);
|
||||
minuteHand_ = lv_line_create(obj_);
|
||||
secondHand_ = lv_line_create(obj_);
|
||||
centerDot_ = lv_obj_create(obj_);
|
||||
|
||||
// Style center dot
|
||||
lv_obj_remove_style_all(centerDot_);
|
||||
lv_obj_set_size(centerDot_, 8, 8);
|
||||
lv_obj_set_style_radius(centerDot_, LV_RADIUS_CIRCLE, 0);
|
||||
lv_obj_center(centerDot_);
|
||||
|
||||
// Initial update to set positions (will be updated by loop)
|
||||
time_t now;
|
||||
time(&now);
|
||||
struct tm t;
|
||||
localtime_r(&now, &t);
|
||||
updateHands(t);
|
||||
|
||||
return obj_;
|
||||
}
|
||||
|
||||
void ClockWidget::applyStyle() {
|
||||
if (!obj_) return;
|
||||
|
||||
// Face style (background)
|
||||
if (config_.bgOpacity > 0) {
|
||||
lv_obj_set_style_bg_color(obj_, lv_color_make(
|
||||
config_.bgColor.r, config_.bgColor.g, config_.bgColor.b), 0);
|
||||
lv_obj_set_style_bg_opa(obj_, config_.bgOpacity, 0);
|
||||
}
|
||||
|
||||
if (config_.borderRadius > 0) {
|
||||
lv_obj_set_style_radius(obj_, config_.borderRadius, 0); // Usually 50% for circle
|
||||
} else {
|
||||
lv_obj_set_style_radius(obj_, LV_RADIUS_CIRCLE, 0);
|
||||
}
|
||||
|
||||
lv_obj_set_style_border_width(obj_, 2, 0);
|
||||
lv_obj_set_style_border_color(obj_, lv_color_make(
|
||||
config_.textColor.r, config_.textColor.g, config_.textColor.b), 0);
|
||||
|
||||
// Hand colors
|
||||
lv_color_t handColor = lv_color_make(
|
||||
config_.textColor.r, config_.textColor.g, config_.textColor.b);
|
||||
lv_color_t secColor = lv_color_make(200, 50, 50); // Red for second hand
|
||||
|
||||
if (hourHand_) {
|
||||
lv_obj_set_style_line_width(hourHand_, 6, 0);
|
||||
lv_obj_set_style_line_color(hourHand_, handColor, 0);
|
||||
lv_obj_set_style_line_rounded(hourHand_, true, 0);
|
||||
}
|
||||
if (minuteHand_) {
|
||||
lv_obj_set_style_line_width(minuteHand_, 4, 0);
|
||||
lv_obj_set_style_line_color(minuteHand_, handColor, 0);
|
||||
lv_obj_set_style_line_rounded(minuteHand_, true, 0);
|
||||
}
|
||||
if (secondHand_) {
|
||||
lv_obj_set_style_line_width(secondHand_, 2, 0);
|
||||
lv_obj_set_style_line_color(secondHand_, secColor, 0);
|
||||
lv_obj_set_style_line_rounded(secondHand_, true, 0);
|
||||
}
|
||||
if (centerDot_) {
|
||||
lv_obj_set_style_bg_color(centerDot_, handColor, 0);
|
||||
}
|
||||
|
||||
// Force redraw of hands with new style/dimensions
|
||||
lastSecond_ = -1;
|
||||
time_t now;
|
||||
time(&now);
|
||||
struct tm t;
|
||||
localtime_r(&now, &t);
|
||||
updateHands(t);
|
||||
}
|
||||
|
||||
void ClockWidget::updateHands(const struct tm& t) {
|
||||
if (t.tm_sec == lastSecond_ && t.tm_min == lastMinute_ && t.tm_hour == lastHour_) return;
|
||||
|
||||
lastSecond_ = t.tm_sec;
|
||||
lastMinute_ = t.tm_min;
|
||||
lastHour_ = t.tm_hour;
|
||||
|
||||
int32_t w = lv_obj_get_width(obj_);
|
||||
int32_t h = lv_obj_get_height(obj_);
|
||||
int32_t cx = w / 2;
|
||||
int32_t cy = h / 2;
|
||||
int32_t radius = (w < h ? w : h) / 2;
|
||||
|
||||
// Lengths
|
||||
int32_t lenSec = radius - 10;
|
||||
int32_t lenMin = radius - 20;
|
||||
int32_t lenHour = radius * 0.6f;
|
||||
|
||||
// Angles (0 is top, clockwise)
|
||||
// Second: 6 deg per sec
|
||||
float angleSec = t.tm_sec * 6.0f;
|
||||
// Minute: 6 deg per min + 0.1 deg per sec
|
||||
float angleMin = t.tm_min * 6.0f + t.tm_sec * 0.1f;
|
||||
// Hour: 30 deg per hour + 0.5 deg per min
|
||||
float angleHour = (t.tm_hour % 12) * 30.0f + t.tm_min * 0.5f;
|
||||
|
||||
auto calcPoints = [&](float angle, int32_t len, lv_point_precise_t* p) {
|
||||
float rad = (angle - 90.0f) * PI / 180.0f;
|
||||
p[0].x = cx;
|
||||
p[0].y = cy;
|
||||
p[1].x = cx + static_cast<int32_t>(cos(rad) * len);
|
||||
p[1].y = cy + static_cast<int32_t>(sin(rad) * len);
|
||||
};
|
||||
|
||||
if (secondHand_) {
|
||||
calcPoints(angleSec, lenSec, secondPoints_);
|
||||
lv_line_set_points(secondHand_, secondPoints_, 2);
|
||||
}
|
||||
if (minuteHand_) {
|
||||
calcPoints(angleMin, lenMin, minutePoints_);
|
||||
lv_line_set_points(minuteHand_, minutePoints_, 2);
|
||||
}
|
||||
if (hourHand_) {
|
||||
calcPoints(angleHour, lenHour, hourPoints_);
|
||||
lv_line_set_points(hourHand_, hourPoints_, 2);
|
||||
}
|
||||
}
|
||||
|
||||
void ClockWidget::onKnxTime(const struct tm& value, TextSource source) {
|
||||
// Only accept system time updates or explicitly configured KNX time sources if we supported them
|
||||
// For now, ClockWidget listens to SYSTEM_TIME via WidgetManager broadcast
|
||||
updateHands(value);
|
||||
}
|
||||
32
main/widgets/ClockWidget.hpp
Normal file
32
main/widgets/ClockWidget.hpp
Normal file
@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "Widget.hpp"
|
||||
#include <ctime>
|
||||
|
||||
class ClockWidget : public Widget {
|
||||
public:
|
||||
explicit ClockWidget(const WidgetConfig& config);
|
||||
|
||||
lv_obj_t* create(lv_obj_t* parent) override;
|
||||
void applyStyle() override;
|
||||
void onKnxTime(const struct tm& value, TextSource source) override;
|
||||
|
||||
private:
|
||||
void drawFace();
|
||||
void updateHands(const struct tm& time);
|
||||
|
||||
lv_obj_t* hourHand_ = nullptr;
|
||||
lv_obj_t* minuteHand_ = nullptr;
|
||||
lv_obj_t* secondHand_ = nullptr;
|
||||
lv_obj_t* centerDot_ = nullptr;
|
||||
|
||||
// Persistent point arrays for lines (LVGL does not copy them)
|
||||
lv_point_precise_t hourPoints_[2];
|
||||
lv_point_precise_t minutePoints_[2];
|
||||
lv_point_precise_t secondPoints_[2];
|
||||
|
||||
// Cache current time to avoid redrawing if not changed
|
||||
int lastHour_ = -1;
|
||||
int lastMinute_ = -1;
|
||||
int lastSecond_ = -1;
|
||||
};
|
||||
@ -9,6 +9,7 @@
|
||||
#include "PowerNodeWidget.hpp"
|
||||
#include "PowerLinkWidget.hpp"
|
||||
#include "ChartWidget.hpp"
|
||||
#include "ClockWidget.hpp"
|
||||
|
||||
std::unique_ptr<Widget> WidgetFactory::create(const WidgetConfig& config) {
|
||||
if (!config.visible) return nullptr;
|
||||
@ -34,6 +35,8 @@ std::unique_ptr<Widget> WidgetFactory::create(const WidgetConfig& config) {
|
||||
return std::make_unique<PowerLinkWidget>(config);
|
||||
case WidgetType::CHART:
|
||||
return std::make_unique<ChartWidget>(config);
|
||||
case WidgetType::CLOCK:
|
||||
return std::make_unique<ClockWidget>(config);
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
1
sdcard_content/webseite/assets/index-BV9BQMzn.css
Normal file
1
sdcard_content/webseite/assets/index-BV9BQMzn.css
Normal file
File diff suppressed because one or more lines are too long
9
sdcard_content/webseite/assets/index-C2PsAL_z.js
Normal file
9
sdcard_content/webseite/assets/index-C2PsAL_z.js
Normal file
File diff suppressed because one or more lines are too long
9
sdcard_content/webseite/assets/index-itIBE803.js
Normal file
9
sdcard_content/webseite/assets/index-itIBE803.js
Normal file
File diff suppressed because one or more lines are too long
@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>web-interface</title>
|
||||
<script type="module" crossorigin src="/assets/index-DYaUUEn9.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-kFitTaMN.css">
|
||||
<script type="module" crossorigin src="/assets/index-itIBE803.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BV9BQMzn.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@ -14,13 +14,17 @@
|
||||
<span class="text-[13px] font-semibold">Button</span>
|
||||
<span class="text-[11px] text-muted mt-0.5 block">Aktion</span>
|
||||
</button>
|
||||
<button class="bg-panel-2 border border-border rounded-xl p-2.5 text-left transition hover:-translate-y-0.5 hover:border-accent" @click="store.addWidget('led')">
|
||||
<span class="text-[13px] font-semibold">LED</span>
|
||||
<span class="text-[11px] text-muted mt-0.5 block">Status</span>
|
||||
<button :class="btnClass" @click="store.addWidget('led')">
|
||||
<span class="material-symbols-outlined text-[24px]">light_mode</span>
|
||||
<span class="text-[10px]">LED</span>
|
||||
</button>
|
||||
<button class="bg-panel-2 border border-border rounded-xl p-2.5 text-left transition hover:-translate-y-0.5 hover:border-accent" @click="store.addWidget('icon')">
|
||||
<span class="text-[13px] font-semibold">Icon</span>
|
||||
<span class="text-[11px] text-muted mt-0.5 block">Symbol</span>
|
||||
<button :class="btnClass" @click="store.addWidget('clock')">
|
||||
<span class="material-symbols-outlined text-[24px]">schedule</span>
|
||||
<span class="text-[10px]">Uhr</span>
|
||||
</button>
|
||||
<button :class="btnClass" @click="store.addWidget('icon')">
|
||||
<span class="material-symbols-outlined text-[24px]">image</span>
|
||||
<span class="text-[10px]">Icon</span>
|
||||
</button>
|
||||
<button class="bg-panel-2 border border-border rounded-xl p-2.5 text-left transition hover:-translate-y-0.5 hover:border-accent" @click="store.addWidget('tabview')">
|
||||
<span class="text-[13px] font-semibold">Tabs</span>
|
||||
|
||||
@ -248,6 +248,14 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="key === 'clock'">
|
||||
<h4 :class="headingClass">Uhr</h4>
|
||||
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.bgColor"></div>
|
||||
<div :class="rowClass"><label :class="labelClass">Zeiger</label><input class="h-[30px] w-[44px] cursor-pointer border-0 bg-transparent p-0" type="color" v-model="w.textColor"></div>
|
||||
<div :class="rowClass"><label :class="labelClass">Deckkraft</label><input :class="inputClass" type="number" min="0" max="255" v-model.number="w.bgOpacity"></div>
|
||||
<div :class="rowClass"><label :class="labelClass">Radius</label><input :class="inputClass" type="number" v-model.number="w.radius"></div>
|
||||
</template>
|
||||
|
||||
<!-- Typography -->
|
||||
<template v-if="key === 'label'">
|
||||
<h4 :class="headingClass">Typo</h4>
|
||||
|
||||
@ -12,7 +12,8 @@ export const WIDGET_TYPES = {
|
||||
POWERFLOW: 6,
|
||||
POWERNODE: 7,
|
||||
POWERLINK: 8,
|
||||
CHART: 9
|
||||
CHART: 9,
|
||||
CLOCK: 10
|
||||
};
|
||||
|
||||
export const ICON_POSITIONS = {
|
||||
@ -44,7 +45,8 @@ export const TYPE_KEYS = {
|
||||
6: 'powerflow',
|
||||
7: 'powernode',
|
||||
8: 'powerlink',
|
||||
9: 'chart'
|
||||
9: 'chart',
|
||||
10: 'clock'
|
||||
};
|
||||
|
||||
export const TYPE_LABELS = {
|
||||
@ -57,7 +59,8 @@ export const TYPE_LABELS = {
|
||||
powerflow: 'Power Flow',
|
||||
powernode: 'Power Node',
|
||||
powerlink: 'Power Link',
|
||||
chart: 'Chart'
|
||||
chart: 'Chart',
|
||||
clock: 'Uhr (Analog)'
|
||||
};
|
||||
|
||||
|
||||
@ -99,7 +102,8 @@ export const sourceOptions = {
|
||||
icon: [0, 2],
|
||||
powernode: [0, 1, 2, 3, 4, 5, 6, 7],
|
||||
powerlink: [0, 1, 3, 5, 6, 7],
|
||||
chart: [1, 3, 5, 6, 7]
|
||||
chart: [1, 3, 5, 6, 7],
|
||||
clock: [11]
|
||||
};
|
||||
|
||||
export const chartPeriods = [
|
||||
@ -363,5 +367,27 @@ export const WIDGET_DEFAULTS = {
|
||||
{ knxAddr: 0, textSrc: 1, color: '#EF6351' }
|
||||
]
|
||||
}
|
||||
},
|
||||
clock: {
|
||||
w: 200,
|
||||
h: 200,
|
||||
text: '',
|
||||
textSrc: 11, // System Time
|
||||
fontSize: 1,
|
||||
textAlign: TEXT_ALIGNS.CENTER,
|
||||
textColor: '#FFFFFF',
|
||||
bgColor: '#16202c',
|
||||
bgOpacity: 255,
|
||||
radius: 100, // Circular default
|
||||
shadow: { enabled: false, x: 0, y: 0, blur: 0, spread: 0, color: '#000000' },
|
||||
isToggle: false,
|
||||
knxAddrWrite: 0,
|
||||
knxAddr: 0,
|
||||
action: 0,
|
||||
targetScreen: 0,
|
||||
iconCodepoint: 0,
|
||||
iconPosition: 0,
|
||||
iconSize: 1,
|
||||
iconGap: 0
|
||||
}
|
||||
};
|
||||
|
||||
@ -320,6 +320,7 @@ export const useEditorStore = defineStore('editor', () => {
|
||||
case 'powernode': typeValue = WIDGET_TYPES.POWERNODE; break;
|
||||
case 'powerlink': typeValue = WIDGET_TYPES.POWERLINK; break;
|
||||
case 'chart': typeValue = WIDGET_TYPES.CHART; break;
|
||||
case 'clock': typeValue = WIDGET_TYPES.CLOCK; break;
|
||||
default: typeValue = WIDGET_TYPES.LABEL;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user