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/PowerNodeWidget.cpp"
|
||||||
"widgets/PowerLinkWidget.cpp"
|
"widgets/PowerLinkWidget.cpp"
|
||||||
"widgets/ChartWidget.cpp"
|
"widgets/ChartWidget.cpp"
|
||||||
|
"widgets/ClockWidget.cpp"
|
||||||
"webserver/WebServer.cpp"
|
"webserver/WebServer.cpp"
|
||||||
"webserver/StaticFileHandlers.cpp"
|
"webserver/StaticFileHandlers.cpp"
|
||||||
"webserver/ConfigHandlers.cpp"
|
"webserver/ConfigHandlers.cpp"
|
||||||
|
|||||||
@ -22,6 +22,7 @@ enum class WidgetType : uint8_t {
|
|||||||
POWERNODE = 7,
|
POWERNODE = 7,
|
||||||
POWERLINK = 8,
|
POWERLINK = 8,
|
||||||
CHART = 9,
|
CHART = 9,
|
||||||
|
CLOCK = 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class IconPosition : uint8_t {
|
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 "PowerNodeWidget.hpp"
|
||||||
#include "PowerLinkWidget.hpp"
|
#include "PowerLinkWidget.hpp"
|
||||||
#include "ChartWidget.hpp"
|
#include "ChartWidget.hpp"
|
||||||
|
#include "ClockWidget.hpp"
|
||||||
|
|
||||||
std::unique_ptr<Widget> WidgetFactory::create(const WidgetConfig& config) {
|
std::unique_ptr<Widget> WidgetFactory::create(const WidgetConfig& config) {
|
||||||
if (!config.visible) return nullptr;
|
if (!config.visible) return nullptr;
|
||||||
@ -34,6 +35,8 @@ std::unique_ptr<Widget> WidgetFactory::create(const WidgetConfig& config) {
|
|||||||
return std::make_unique<PowerLinkWidget>(config);
|
return std::make_unique<PowerLinkWidget>(config);
|
||||||
case WidgetType::CHART:
|
case WidgetType::CHART:
|
||||||
return std::make_unique<ChartWidget>(config);
|
return std::make_unique<ChartWidget>(config);
|
||||||
|
case WidgetType::CLOCK:
|
||||||
|
return std::make_unique<ClockWidget>(config);
|
||||||
default:
|
default:
|
||||||
return nullptr;
|
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" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>web-interface</title>
|
<title>web-interface</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DYaUUEn9.js"></script>
|
<script type="module" crossorigin src="/assets/index-itIBE803.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-kFitTaMN.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BV9BQMzn.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@ -14,13 +14,17 @@
|
|||||||
<span class="text-[13px] font-semibold">Button</span>
|
<span class="text-[13px] font-semibold">Button</span>
|
||||||
<span class="text-[11px] text-muted mt-0.5 block">Aktion</span>
|
<span class="text-[11px] text-muted mt-0.5 block">Aktion</span>
|
||||||
</button>
|
</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')">
|
<button :class="btnClass" @click="store.addWidget('led')">
|
||||||
<span class="text-[13px] font-semibold">LED</span>
|
<span class="material-symbols-outlined text-[24px]">light_mode</span>
|
||||||
<span class="text-[11px] text-muted mt-0.5 block">Status</span>
|
<span class="text-[10px]">LED</span>
|
||||||
</button>
|
</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')">
|
<button :class="btnClass" @click="store.addWidget('clock')">
|
||||||
<span class="text-[13px] font-semibold">Icon</span>
|
<span class="material-symbols-outlined text-[24px]">schedule</span>
|
||||||
<span class="text-[11px] text-muted mt-0.5 block">Symbol</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>
|
||||||
<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')">
|
<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>
|
<span class="text-[13px] font-semibold">Tabs</span>
|
||||||
|
|||||||
@ -248,6 +248,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</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 -->
|
<!-- Typography -->
|
||||||
<template v-if="key === 'label'">
|
<template v-if="key === 'label'">
|
||||||
<h4 :class="headingClass">Typo</h4>
|
<h4 :class="headingClass">Typo</h4>
|
||||||
|
|||||||
@ -12,7 +12,8 @@ export const WIDGET_TYPES = {
|
|||||||
POWERFLOW: 6,
|
POWERFLOW: 6,
|
||||||
POWERNODE: 7,
|
POWERNODE: 7,
|
||||||
POWERLINK: 8,
|
POWERLINK: 8,
|
||||||
CHART: 9
|
CHART: 9,
|
||||||
|
CLOCK: 10
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ICON_POSITIONS = {
|
export const ICON_POSITIONS = {
|
||||||
@ -44,7 +45,8 @@ export const TYPE_KEYS = {
|
|||||||
6: 'powerflow',
|
6: 'powerflow',
|
||||||
7: 'powernode',
|
7: 'powernode',
|
||||||
8: 'powerlink',
|
8: 'powerlink',
|
||||||
9: 'chart'
|
9: 'chart',
|
||||||
|
10: 'clock'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TYPE_LABELS = {
|
export const TYPE_LABELS = {
|
||||||
@ -57,7 +59,8 @@ export const TYPE_LABELS = {
|
|||||||
powerflow: 'Power Flow',
|
powerflow: 'Power Flow',
|
||||||
powernode: 'Power Node',
|
powernode: 'Power Node',
|
||||||
powerlink: 'Power Link',
|
powerlink: 'Power Link',
|
||||||
chart: 'Chart'
|
chart: 'Chart',
|
||||||
|
clock: 'Uhr (Analog)'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -99,7 +102,8 @@ export const sourceOptions = {
|
|||||||
icon: [0, 2],
|
icon: [0, 2],
|
||||||
powernode: [0, 1, 2, 3, 4, 5, 6, 7],
|
powernode: [0, 1, 2, 3, 4, 5, 6, 7],
|
||||||
powerlink: [0, 1, 3, 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 = [
|
export const chartPeriods = [
|
||||||
@ -363,5 +367,27 @@ export const WIDGET_DEFAULTS = {
|
|||||||
{ knxAddr: 0, textSrc: 1, color: '#EF6351' }
|
{ 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 'powernode': typeValue = WIDGET_TYPES.POWERNODE; break;
|
||||||
case 'powerlink': typeValue = WIDGET_TYPES.POWERLINK; break;
|
case 'powerlink': typeValue = WIDGET_TYPES.POWERLINK; break;
|
||||||
case 'chart': typeValue = WIDGET_TYPES.CHART; break;
|
case 'chart': typeValue = WIDGET_TYPES.CHART; break;
|
||||||
|
case 'clock': typeValue = WIDGET_TYPES.CLOCK; break;
|
||||||
default: typeValue = WIDGET_TYPES.LABEL;
|
default: typeValue = WIDGET_TYPES.LABEL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user