Webserver
This commit is contained in:
parent
da30730029
commit
f34eb810da
@ -2,6 +2,7 @@
|
||||
#include "driver/i2c_master.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_lcd_touch_gt911.h"
|
||||
#include "WidgetManager.hpp"
|
||||
|
||||
// Common display resolutions, used for touch dimensions
|
||||
#define LCD_H_RES 800
|
||||
@ -75,6 +76,7 @@ void Touch::lv_indev_read_cb(lv_indev_t *indev, lv_indev_data_t *data)
|
||||
data->point.x = x[0];
|
||||
data->point.y = y[0];
|
||||
data->state = LV_INDEV_STATE_PRESSED;
|
||||
WidgetManager::instance().onUserActivity();
|
||||
} else {
|
||||
data->state = LV_INDEV_STATE_RELEASED;
|
||||
}
|
||||
|
||||
@ -6,14 +6,28 @@
|
||||
|
||||
// Maximum number of widgets
|
||||
static constexpr size_t MAX_WIDGETS = 32;
|
||||
static constexpr size_t MAX_SCREENS = 8;
|
||||
static constexpr size_t MAX_TEXT_LEN = 32;
|
||||
static constexpr size_t MAX_SCREEN_NAME_LEN = 24;
|
||||
|
||||
enum class WidgetType : uint8_t {
|
||||
LABEL = 0,
|
||||
BUTTON = 1,
|
||||
LED = 2,
|
||||
// Future: GAUGE, IMAGE, ARC, etc.
|
||||
};
|
||||
|
||||
enum class ScreenMode : uint8_t {
|
||||
FULLSCREEN = 0,
|
||||
MODAL = 1,
|
||||
};
|
||||
|
||||
enum class ButtonAction : uint8_t {
|
||||
KNX = 0,
|
||||
JUMP = 1,
|
||||
BACK = 2,
|
||||
};
|
||||
|
||||
// Text source: static text or KNX group address
|
||||
enum class TextSource : uint8_t {
|
||||
STATIC = 0, // Static text
|
||||
@ -77,9 +91,11 @@ struct WidgetConfig {
|
||||
// Button specific
|
||||
bool isToggle; // For buttons: toggle mode
|
||||
uint16_t knxAddressWrite; // KNX address to write on click
|
||||
ButtonAction action; // Button action (KNX, Jump, Back)
|
||||
uint8_t targetScreen; // Target screen ID for jump
|
||||
|
||||
// Serialization size (fixed for NVS storage)
|
||||
static constexpr size_t SERIALIZED_SIZE = 64;
|
||||
static constexpr size_t SERIALIZED_SIZE = 68;
|
||||
|
||||
void serialize(uint8_t* buf) const;
|
||||
void deserialize(const uint8_t* buf);
|
||||
@ -98,13 +114,29 @@ struct WidgetConfig {
|
||||
|
||||
// Screen configuration (holds all widgets)
|
||||
struct ScreenConfig {
|
||||
uint8_t id;
|
||||
char name[MAX_SCREEN_NAME_LEN];
|
||||
ScreenMode mode;
|
||||
Color backgroundColor;
|
||||
uint8_t widgetCount;
|
||||
WidgetConfig widgets[MAX_WIDGETS];
|
||||
|
||||
void clear();
|
||||
void clear(uint8_t newId = 0, const char* newName = nullptr);
|
||||
int addWidget(const WidgetConfig& widget); // Returns widget ID or -1
|
||||
bool removeWidget(uint8_t id);
|
||||
WidgetConfig* findWidget(uint8_t id);
|
||||
const WidgetConfig* findWidget(uint8_t id) const;
|
||||
};
|
||||
|
||||
struct GuiConfig {
|
||||
uint8_t screenCount;
|
||||
ScreenConfig screens[MAX_SCREENS];
|
||||
uint8_t startScreenId;
|
||||
bool standbyEnabled;
|
||||
uint8_t standbyScreenId;
|
||||
uint16_t standbyMinutes;
|
||||
|
||||
void clear();
|
||||
ScreenConfig* findScreen(uint8_t id);
|
||||
const ScreenConfig* findScreen(uint8_t id) const;
|
||||
};
|
||||
|
||||
@ -2,25 +2,24 @@
|
||||
#include "SdCard.hpp"
|
||||
#include "esp_lv_adapter.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "Gui.hpp"
|
||||
#include "cJSON.h"
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
|
||||
static const char* TAG = "WidgetMgr";
|
||||
static constexpr uint8_t SCREEN_ID_NONE = 0xFF;
|
||||
|
||||
// Button click callback
|
||||
static void button_click_cb(lv_event_t* e) {
|
||||
WidgetConfig* cfg = static_cast<WidgetConfig*>(lv_event_get_user_data(e));
|
||||
if (cfg && cfg->knxAddressWrite > 0) {
|
||||
if (!cfg) return;
|
||||
lv_obj_t* target = static_cast<lv_obj_t*>(lv_event_get_target(e));
|
||||
bool state = (lv_obj_get_state(target) & LV_STATE_CHECKED) != 0;
|
||||
ESP_LOGI(TAG, "Button %d clicked, KNX write to %d, state=%d",
|
||||
cfg->id, cfg->knxAddressWrite, state);
|
||||
// TODO: Send KNX telegram
|
||||
// Gui::knxWorker.writeSwitch(cfg->knxAddressWrite, state);
|
||||
}
|
||||
WidgetManager::instance().handleButtonAction(*cfg, target);
|
||||
}
|
||||
|
||||
// WidgetConfig implementation
|
||||
@ -55,6 +54,8 @@ void WidgetConfig::serialize(uint8_t* buf) const {
|
||||
|
||||
buf[pos++] = isToggle ? 1 : 0;
|
||||
buf[pos++] = knxAddressWrite & 0xFF; buf[pos++] = (knxAddressWrite >> 8) & 0xFF;
|
||||
buf[pos++] = static_cast<uint8_t>(action);
|
||||
buf[pos++] = targetScreen;
|
||||
}
|
||||
|
||||
void WidgetConfig::deserialize(const uint8_t* buf) {
|
||||
@ -88,6 +89,9 @@ void WidgetConfig::deserialize(const uint8_t* buf) {
|
||||
|
||||
isToggle = buf[pos++] != 0;
|
||||
knxAddressWrite = buf[pos] | (buf[pos+1] << 8);
|
||||
pos += 2;
|
||||
action = static_cast<ButtonAction>(buf[pos++]);
|
||||
targetScreen = buf[pos++];
|
||||
}
|
||||
|
||||
WidgetConfig WidgetConfig::createLabel(uint8_t id, int16_t x, int16_t y, const char* labelText) {
|
||||
@ -143,14 +147,22 @@ WidgetConfig WidgetConfig::createButton(uint8_t id, int16_t x, int16_t y,
|
||||
cfg.shadow.color = {0, 0, 0};
|
||||
cfg.isToggle = toggle;
|
||||
cfg.knxAddressWrite = knxAddrWrite;
|
||||
cfg.action = ButtonAction::KNX;
|
||||
cfg.targetScreen = 0;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
// ScreenConfig implementation
|
||||
void ScreenConfig::clear() {
|
||||
void ScreenConfig::clear(uint8_t newId, const char* newName) {
|
||||
id = newId;
|
||||
mode = ScreenMode::FULLSCREEN;
|
||||
backgroundColor = {26, 26, 46}; // Dark blue background
|
||||
widgetCount = 0;
|
||||
memset(widgets, 0, sizeof(widgets));
|
||||
memset(name, 0, sizeof(name));
|
||||
if (newName && newName[0] != '\0') {
|
||||
strncpy(name, newName, sizeof(name) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
int ScreenConfig::addWidget(const WidgetConfig& widget) {
|
||||
@ -196,6 +208,32 @@ const WidgetConfig* ScreenConfig::findWidget(uint8_t id) const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// GuiConfig implementation
|
||||
void GuiConfig::clear() {
|
||||
screenCount = 0;
|
||||
startScreenId = 0;
|
||||
standbyEnabled = false;
|
||||
standbyScreenId = 0xFF;
|
||||
standbyMinutes = 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;
|
||||
}
|
||||
|
||||
// WidgetManager implementation
|
||||
WidgetManager& WidgetManager::instance() {
|
||||
static WidgetManager inst;
|
||||
@ -205,26 +243,45 @@ WidgetManager& WidgetManager::instance() {
|
||||
WidgetManager::WidgetManager() {
|
||||
widgetObjects_.fill(nullptr);
|
||||
createDefaultConfig();
|
||||
activeScreenId_ = config_.startScreenId;
|
||||
lastActivityUs_ = esp_timer_get_time();
|
||||
}
|
||||
|
||||
void WidgetManager::createDefaultConfig() {
|
||||
config_.clear();
|
||||
config_.screenCount = 1;
|
||||
ScreenConfig& screen = config_.screens[0];
|
||||
screen.clear(0, "Screen 1");
|
||||
|
||||
// Default: Temperature label
|
||||
auto tempLabel = WidgetConfig::createKnxLabel(0, 50, 20,
|
||||
TextSource::KNX_DPT_TEMP, 1, "%.1f °C");
|
||||
tempLabel.fontSize = 3; // 28pt
|
||||
config_.addWidget(tempLabel);
|
||||
screen.addWidget(tempLabel);
|
||||
|
||||
// Default: KNX Prog button
|
||||
auto progBtn = WidgetConfig::createButton(1, 50, 100, "KNX Prog", 0, true);
|
||||
progBtn.bgColor = {200, 50, 50}; // Red
|
||||
config_.addWidget(progBtn);
|
||||
screen.addWidget(progBtn);
|
||||
|
||||
config_.startScreenId = screen.id;
|
||||
config_.standbyEnabled = false;
|
||||
config_.standbyScreenId = 0xFF;
|
||||
config_.standbyMinutes = 0;
|
||||
activeScreenId_ = screen.id;
|
||||
}
|
||||
|
||||
void WidgetManager::init() {
|
||||
loadFromSdCard();
|
||||
ESP_LOGI(TAG, "WidgetManager initialized with %d widgets", config_.widgetCount);
|
||||
if (config_.findScreen(config_.startScreenId)) {
|
||||
activeScreenId_ = config_.startScreenId;
|
||||
} else if (config_.screenCount > 0) {
|
||||
activeScreenId_ = config_.screens[0].id;
|
||||
} else {
|
||||
activeScreenId_ = 0;
|
||||
}
|
||||
lastActivityUs_ = esp_timer_get_time();
|
||||
ESP_LOGI(TAG, "WidgetManager initialized with %d screens", config_.screenCount);
|
||||
}
|
||||
|
||||
void WidgetManager::loadFromSdCard() {
|
||||
@ -267,7 +324,7 @@ void WidgetManager::loadFromSdCard() {
|
||||
delete[] json;
|
||||
|
||||
if (success) {
|
||||
ESP_LOGI(TAG, "Loaded %d widgets from SD card", config_.widgetCount);
|
||||
ESP_LOGI(TAG, "Loaded %d screens from SD card", config_.screenCount);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to parse config file");
|
||||
}
|
||||
@ -280,13 +337,13 @@ void WidgetManager::saveToSdCard() {
|
||||
}
|
||||
|
||||
// Generate JSON using cJSON
|
||||
char* json = new char[8192];
|
||||
char* json = new char[32768];
|
||||
if (!json) {
|
||||
ESP_LOGE(TAG, "Failed to allocate memory for JSON");
|
||||
return;
|
||||
}
|
||||
|
||||
getConfigJson(json, 8192);
|
||||
getConfigJson(json, 32768);
|
||||
|
||||
// Write to file
|
||||
FILE* f = fopen(CONFIG_FILE, "w");
|
||||
@ -301,18 +358,21 @@ void WidgetManager::saveToSdCard() {
|
||||
delete[] json;
|
||||
|
||||
if (written > 0) {
|
||||
ESP_LOGI(TAG, "Saved %d widgets to SD card", config_.widgetCount);
|
||||
ESP_LOGI(TAG, "Saved %d screens to SD card", config_.screenCount);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to write config file");
|
||||
}
|
||||
}
|
||||
|
||||
void WidgetManager::applyConfig() {
|
||||
if (esp_lv_adapter_lock(-1) == ESP_OK) {
|
||||
destroyAllWidgets();
|
||||
createAllWidgets();
|
||||
esp_lv_adapter_unlock();
|
||||
if (!config_.findScreen(activeScreenId_)) {
|
||||
if (config_.findScreen(config_.startScreenId)) {
|
||||
activeScreenId_ = config_.startScreenId;
|
||||
} else if (config_.screenCount > 0) {
|
||||
activeScreenId_ = config_.screens[0].id;
|
||||
}
|
||||
}
|
||||
applyScreen(activeScreenId_);
|
||||
}
|
||||
|
||||
void WidgetManager::saveAndApply() {
|
||||
@ -322,11 +382,209 @@ void WidgetManager::saveAndApply() {
|
||||
}
|
||||
|
||||
void WidgetManager::resetToDefaults() {
|
||||
closeModal();
|
||||
standbyActive_ = false;
|
||||
standbyWakePending_ = false;
|
||||
standbyReturnScreenId_ = SCREEN_ID_NONE;
|
||||
previousScreenId_ = SCREEN_ID_NONE;
|
||||
createDefaultConfig();
|
||||
saveAndApply();
|
||||
ESP_LOGI(TAG, "Reset to defaults");
|
||||
}
|
||||
|
||||
ScreenConfig* WidgetManager::activeScreen() {
|
||||
if (modalContainer_ && modalScreenId_ != SCREEN_ID_NONE) {
|
||||
return config_.findScreen(modalScreenId_);
|
||||
}
|
||||
return config_.findScreen(activeScreenId_);
|
||||
}
|
||||
|
||||
const ScreenConfig* WidgetManager::activeScreen() const {
|
||||
if (modalContainer_ && modalScreenId_ != SCREEN_ID_NONE) {
|
||||
return config_.findScreen(modalScreenId_);
|
||||
}
|
||||
return config_.findScreen(activeScreenId_);
|
||||
}
|
||||
|
||||
void WidgetManager::applyScreen(uint8_t screenId) {
|
||||
ScreenConfig* screen = config_.findScreen(screenId);
|
||||
if (!screen) {
|
||||
ESP_LOGW(TAG, "Screen %d not found", screenId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (modalContainer_) {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
if (esp_lv_adapter_lock(-1) == ESP_OK) {
|
||||
lv_obj_t* root = lv_scr_act();
|
||||
lv_obj_clean(root);
|
||||
widgetObjects_.fill(nullptr);
|
||||
createAllWidgets(*screen, root);
|
||||
esp_lv_adapter_unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void WidgetManager::showModalScreen(const ScreenConfig& screen) {
|
||||
if (modalContainer_) {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
if (esp_lv_adapter_lock(-1) != ESP_OK) return;
|
||||
|
||||
lv_obj_t* overlay = lv_obj_create(lv_layer_top());
|
||||
lv_obj_set_style_bg_opa(overlay, LV_OPA_COVER, 0);
|
||||
lv_obj_clear_flag(overlay, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
lv_disp_t* disp = lv_disp_get_default();
|
||||
int32_t hor = disp ? lv_disp_get_hor_res(disp) : 1280;
|
||||
int32_t ver = disp ? lv_disp_get_ver_res(disp) : 800;
|
||||
lv_obj_set_size(overlay, hor, ver);
|
||||
|
||||
modalContainer_ = overlay;
|
||||
modalScreenId_ = screen.id;
|
||||
createAllWidgets(screen, modalContainer_);
|
||||
|
||||
esp_lv_adapter_unlock();
|
||||
}
|
||||
|
||||
void WidgetManager::closeModal() {
|
||||
if (!modalContainer_) return;
|
||||
|
||||
if (esp_lv_adapter_lock(-1) == ESP_OK) {
|
||||
lv_obj_delete(modalContainer_);
|
||||
esp_lv_adapter_unlock();
|
||||
}
|
||||
modalContainer_ = nullptr;
|
||||
modalScreenId_ = SCREEN_ID_NONE;
|
||||
widgetObjects_.fill(nullptr);
|
||||
}
|
||||
|
||||
void WidgetManager::showScreen(uint8_t screenId) {
|
||||
ScreenConfig* screen = config_.findScreen(screenId);
|
||||
if (!screen) {
|
||||
ESP_LOGW(TAG, "Screen %d not found", screenId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (screen->mode == ScreenMode::MODAL) {
|
||||
showModalScreen(*screen);
|
||||
return;
|
||||
}
|
||||
|
||||
previousScreenId_ = activeScreenId_;
|
||||
activeScreenId_ = screen->id;
|
||||
standbyActive_ = false;
|
||||
applyScreen(activeScreenId_);
|
||||
}
|
||||
|
||||
void WidgetManager::handleButtonAction(const WidgetConfig& cfg, lv_obj_t* target) {
|
||||
if (cfg.type != WidgetType::BUTTON) return;
|
||||
|
||||
onUserActivity();
|
||||
|
||||
switch (cfg.action) {
|
||||
case ButtonAction::JUMP:
|
||||
navPending_ = true;
|
||||
navAction_ = ButtonAction::JUMP;
|
||||
navTargetScreen_ = cfg.targetScreen;
|
||||
break;
|
||||
case ButtonAction::BACK:
|
||||
navPending_ = true;
|
||||
navAction_ = ButtonAction::BACK;
|
||||
navTargetScreen_ = SCREEN_ID_NONE;
|
||||
break;
|
||||
case ButtonAction::KNX:
|
||||
default: {
|
||||
if (cfg.knxAddressWrite > 0) {
|
||||
bool state = false;
|
||||
if (target) {
|
||||
state = (lv_obj_get_state(target) & LV_STATE_CHECKED) != 0;
|
||||
}
|
||||
ESP_LOGI(TAG, "Button %d clicked, KNX write to %d, state=%d",
|
||||
cfg.id, cfg.knxAddressWrite, state);
|
||||
// TODO: Send KNX telegram
|
||||
// Gui::knxWorker.writeSwitch(cfg.knxAddressWrite, state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WidgetManager::goBack() {
|
||||
if (modalContainer_) {
|
||||
closeModal();
|
||||
applyScreen(activeScreenId_);
|
||||
return;
|
||||
}
|
||||
|
||||
if (previousScreenId_ != SCREEN_ID_NONE && previousScreenId_ != activeScreenId_) {
|
||||
activeScreenId_ = previousScreenId_;
|
||||
previousScreenId_ = SCREEN_ID_NONE;
|
||||
applyScreen(activeScreenId_);
|
||||
}
|
||||
}
|
||||
|
||||
void WidgetManager::enterStandby() {
|
||||
if (!config_.standbyEnabled || config_.standbyMinutes == 0) return;
|
||||
if (standbyActive_) return;
|
||||
if (config_.standbyScreenId == SCREEN_ID_NONE) return;
|
||||
|
||||
ScreenConfig* standbyScreen = config_.findScreen(config_.standbyScreenId);
|
||||
if (!standbyScreen) return;
|
||||
|
||||
standbyReturnScreenId_ = activeScreenId_;
|
||||
standbyActive_ = true;
|
||||
activeScreenId_ = standbyScreen->id;
|
||||
applyScreen(activeScreenId_);
|
||||
}
|
||||
|
||||
void WidgetManager::loop() {
|
||||
if (navPending_) {
|
||||
navPending_ = false;
|
||||
if (navAction_ == ButtonAction::JUMP) {
|
||||
showScreen(navTargetScreen_);
|
||||
} else if (navAction_ == ButtonAction::BACK) {
|
||||
goBack();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (standbyWakePending_) {
|
||||
standbyWakePending_ = false;
|
||||
if (standbyWakeTarget_ != SCREEN_ID_NONE) {
|
||||
activeScreenId_ = standbyWakeTarget_;
|
||||
applyScreen(activeScreenId_);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!config_.standbyEnabled || config_.standbyMinutes == 0) return;
|
||||
if (standbyActive_) return;
|
||||
if (config_.standbyScreenId == SCREEN_ID_NONE) return;
|
||||
|
||||
int64_t now = esp_timer_get_time();
|
||||
int64_t idleUs = now - lastActivityUs_;
|
||||
int64_t timeoutUs = static_cast<int64_t>(config_.standbyMinutes) * 60 * 1000000LL;
|
||||
if (idleUs >= timeoutUs) {
|
||||
enterStandby();
|
||||
}
|
||||
}
|
||||
|
||||
void WidgetManager::onUserActivity() {
|
||||
lastActivityUs_ = esp_timer_get_time();
|
||||
if (standbyActive_) {
|
||||
standbyActive_ = false;
|
||||
uint8_t returnId = standbyReturnScreenId_;
|
||||
if (returnId == SCREEN_ID_NONE) {
|
||||
returnId = config_.startScreenId;
|
||||
}
|
||||
standbyWakeTarget_ = returnId;
|
||||
standbyWakePending_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void WidgetManager::destroyAllWidgets() {
|
||||
for (auto& obj : widgetObjects_) {
|
||||
if (obj != nullptr) {
|
||||
@ -336,38 +594,40 @@ void WidgetManager::destroyAllWidgets() {
|
||||
}
|
||||
}
|
||||
|
||||
void WidgetManager::createAllWidgets() {
|
||||
screen_ = lv_scr_act();
|
||||
void WidgetManager::createAllWidgets(const ScreenConfig& screen, lv_obj_t* parent) {
|
||||
screen_ = parent;
|
||||
widgetObjects_.fill(nullptr);
|
||||
|
||||
// Set background color
|
||||
lv_obj_set_style_bg_color(screen_, lv_color_make(
|
||||
config_.backgroundColor.r,
|
||||
config_.backgroundColor.g,
|
||||
config_.backgroundColor.b), 0);
|
||||
lv_obj_set_style_bg_color(parent, lv_color_make(
|
||||
screen.backgroundColor.r,
|
||||
screen.backgroundColor.g,
|
||||
screen.backgroundColor.b), 0);
|
||||
lv_obj_set_style_bg_opa(parent, LV_OPA_COVER, 0);
|
||||
|
||||
// Create all widgets
|
||||
for (uint8_t i = 0; i < config_.widgetCount; i++) {
|
||||
WidgetConfig& cfg = config_.widgets[i];
|
||||
lv_obj_t* obj = createWidget(cfg);
|
||||
for (uint8_t i = 0; i < screen.widgetCount; i++) {
|
||||
const WidgetConfig& cfg = screen.widgets[i];
|
||||
lv_obj_t* obj = createWidget(cfg, parent);
|
||||
if (obj != nullptr && cfg.id < MAX_WIDGETS) {
|
||||
widgetObjects_[cfg.id] = obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lv_obj_t* WidgetManager::createWidget(const WidgetConfig& cfg) {
|
||||
lv_obj_t* WidgetManager::createWidget(const WidgetConfig& cfg, lv_obj_t* parent) {
|
||||
if (!cfg.visible) return nullptr;
|
||||
|
||||
lv_obj_t* obj = nullptr;
|
||||
|
||||
switch (cfg.type) {
|
||||
case WidgetType::LABEL: {
|
||||
obj = lv_label_create(screen_);
|
||||
obj = lv_label_create(parent);
|
||||
lv_label_set_text(obj, cfg.text);
|
||||
break;
|
||||
}
|
||||
case WidgetType::BUTTON: {
|
||||
obj = lv_btn_create(screen_);
|
||||
obj = lv_btn_create(parent);
|
||||
if (cfg.isToggle) {
|
||||
lv_obj_add_flag(obj, LV_OBJ_FLAG_CHECKABLE);
|
||||
}
|
||||
@ -377,9 +637,16 @@ lv_obj_t* WidgetManager::createWidget(const WidgetConfig& cfg) {
|
||||
// Create label inside button
|
||||
lv_obj_t* label = lv_label_create(obj);
|
||||
lv_label_set_text(label, cfg.text);
|
||||
lv_obj_set_style_text_color(label, lv_color_make(
|
||||
cfg.textColor.r, cfg.textColor.g, cfg.textColor.b), 0);
|
||||
lv_obj_set_style_text_font(label, getFontBySize(cfg.fontSize), 0);
|
||||
lv_obj_center(label);
|
||||
break;
|
||||
}
|
||||
case WidgetType::LED: {
|
||||
obj = lv_led_create(parent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (obj != nullptr) {
|
||||
@ -387,8 +654,12 @@ lv_obj_t* WidgetManager::createWidget(const WidgetConfig& cfg) {
|
||||
if (cfg.width > 0 && cfg.height > 0) {
|
||||
lv_obj_set_size(obj, cfg.width, cfg.height);
|
||||
}
|
||||
if (cfg.type == WidgetType::LED) {
|
||||
applyLedStyle(obj, cfg);
|
||||
} else {
|
||||
applyStyle(obj, cfg);
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
@ -425,6 +696,23 @@ void WidgetManager::applyStyle(lv_obj_t* obj, const WidgetConfig& cfg) {
|
||||
}
|
||||
}
|
||||
|
||||
void WidgetManager::applyLedStyle(lv_obj_t* obj, const WidgetConfig& cfg) {
|
||||
lv_obj_set_style_radius(obj, LV_RADIUS_CIRCLE, 0);
|
||||
lv_led_set_color(obj, lv_color_make(
|
||||
cfg.bgColor.r, cfg.bgColor.g, cfg.bgColor.b));
|
||||
lv_led_set_brightness(obj, cfg.bgOpacity);
|
||||
|
||||
if (cfg.shadow.enabled) {
|
||||
lv_obj_set_style_shadow_color(obj, lv_color_make(
|
||||
cfg.shadow.color.r, cfg.shadow.color.g, cfg.shadow.color.b), 0);
|
||||
lv_obj_set_style_shadow_opa(obj, 180, 0);
|
||||
lv_obj_set_style_shadow_width(obj, cfg.shadow.blur, 0);
|
||||
lv_obj_set_style_shadow_spread(obj, cfg.shadow.spread, 0);
|
||||
lv_obj_set_style_shadow_offset_x(obj, cfg.shadow.offsetX, 0);
|
||||
lv_obj_set_style_shadow_offset_y(obj, cfg.shadow.offsetY, 0);
|
||||
}
|
||||
}
|
||||
|
||||
const lv_font_t* WidgetManager::getFontBySize(uint8_t sizeIndex) {
|
||||
// Font sizes: 0=14, 1=18, 2=22, 3=28, 4=36, 5=48
|
||||
// These must be enabled in sdkconfig (CONFIG_LV_FONT_MONTSERRAT_*)
|
||||
@ -452,8 +740,14 @@ const lv_font_t* WidgetManager::getFontBySize(uint8_t sizeIndex) {
|
||||
void WidgetManager::onKnxValue(uint16_t groupAddr, float value) {
|
||||
if (esp_lv_adapter_lock(100) != ESP_OK) return;
|
||||
|
||||
for (uint8_t i = 0; i < config_.widgetCount; i++) {
|
||||
const WidgetConfig& cfg = config_.widgets[i];
|
||||
const ScreenConfig* screen = activeScreen();
|
||||
if (!screen) {
|
||||
esp_lv_adapter_unlock();
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < screen->widgetCount; i++) {
|
||||
const WidgetConfig& cfg = screen->widgets[i];
|
||||
if (cfg.knxAddress == groupAddr && cfg.textSource == TextSource::KNX_DPT_TEMP) {
|
||||
lv_obj_t* obj = widgetObjects_[cfg.id];
|
||||
if (obj != nullptr) {
|
||||
@ -470,12 +764,23 @@ void WidgetManager::onKnxValue(uint16_t groupAddr, float value) {
|
||||
void WidgetManager::onKnxSwitch(uint16_t groupAddr, bool value) {
|
||||
if (esp_lv_adapter_lock(100) != ESP_OK) return;
|
||||
|
||||
for (uint8_t i = 0; i < config_.widgetCount; i++) {
|
||||
const WidgetConfig& cfg = config_.widgets[i];
|
||||
const ScreenConfig* screen = activeScreen();
|
||||
if (!screen) {
|
||||
esp_lv_adapter_unlock();
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < screen->widgetCount; i++) {
|
||||
const WidgetConfig& cfg = screen->widgets[i];
|
||||
if (cfg.knxAddress == groupAddr && cfg.textSource == TextSource::KNX_DPT_SWITCH) {
|
||||
lv_obj_t* obj = widgetObjects_[cfg.id];
|
||||
if (obj != nullptr) {
|
||||
if (obj == nullptr) continue;
|
||||
|
||||
if (cfg.type == WidgetType::LABEL) {
|
||||
lv_label_set_text(obj, value ? "EIN" : "AUS");
|
||||
} else if (cfg.type == WidgetType::LED) {
|
||||
uint8_t brightness = value ? (cfg.bgOpacity > 0 ? cfg.bgOpacity : 255) : 0;
|
||||
lv_led_set_brightness(obj, brightness);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -486,8 +791,14 @@ void WidgetManager::onKnxSwitch(uint16_t groupAddr, bool value) {
|
||||
void WidgetManager::onKnxText(uint16_t groupAddr, const char* text) {
|
||||
if (esp_lv_adapter_lock(100) != ESP_OK) return;
|
||||
|
||||
for (uint8_t i = 0; i < config_.widgetCount; i++) {
|
||||
const WidgetConfig& cfg = config_.widgets[i];
|
||||
const ScreenConfig* screen = activeScreen();
|
||||
if (!screen) {
|
||||
esp_lv_adapter_unlock();
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < screen->widgetCount; i++) {
|
||||
const WidgetConfig& cfg = screen->widgets[i];
|
||||
if (cfg.knxAddress == groupAddr && cfg.textSource == TextSource::KNX_DPT_TEXT) {
|
||||
lv_obj_t* obj = widgetObjects_[cfg.id];
|
||||
if (obj != nullptr) {
|
||||
@ -506,17 +817,32 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add background color
|
||||
cJSON_AddNumberToObject(root, "startScreen", config_.startScreenId);
|
||||
|
||||
cJSON* standby = cJSON_AddObjectToObject(root, "standby");
|
||||
cJSON_AddBoolToObject(standby, "enabled", config_.standbyEnabled);
|
||||
cJSON_AddNumberToObject(standby, "screen", config_.standbyScreenId);
|
||||
cJSON_AddNumberToObject(standby, "minutes", config_.standbyMinutes);
|
||||
|
||||
// Add screens array
|
||||
cJSON* screens = cJSON_AddArrayToObject(root, "screens");
|
||||
|
||||
for (uint8_t s = 0; s < config_.screenCount; s++) {
|
||||
const ScreenConfig& screen = config_.screens[s];
|
||||
cJSON* screenJson = cJSON_CreateObject();
|
||||
|
||||
cJSON_AddNumberToObject(screenJson, "id", screen.id);
|
||||
cJSON_AddStringToObject(screenJson, "name", screen.name);
|
||||
cJSON_AddNumberToObject(screenJson, "mode", static_cast<int>(screen.mode));
|
||||
|
||||
char bgColorStr[8];
|
||||
snprintf(bgColorStr, sizeof(bgColorStr), "#%02X%02X%02X",
|
||||
config_.backgroundColor.r, config_.backgroundColor.g, config_.backgroundColor.b);
|
||||
cJSON_AddStringToObject(root, "bgColor", bgColorStr);
|
||||
screen.backgroundColor.r, screen.backgroundColor.g, screen.backgroundColor.b);
|
||||
cJSON_AddStringToObject(screenJson, "bgColor", bgColorStr);
|
||||
|
||||
// Add widgets array
|
||||
cJSON* widgets = cJSON_AddArrayToObject(root, "widgets");
|
||||
|
||||
for (uint8_t i = 0; i < config_.widgetCount; i++) {
|
||||
const WidgetConfig& w = config_.widgets[i];
|
||||
cJSON* widgets = cJSON_AddArrayToObject(screenJson, "widgets");
|
||||
for (uint8_t i = 0; i < screen.widgetCount; i++) {
|
||||
const WidgetConfig& w = screen.widgets[i];
|
||||
cJSON* widget = cJSON_CreateObject();
|
||||
|
||||
cJSON_AddNumberToObject(widget, "id", w.id);
|
||||
@ -560,10 +886,15 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const {
|
||||
|
||||
cJSON_AddBoolToObject(widget, "isToggle", w.isToggle);
|
||||
cJSON_AddNumberToObject(widget, "knxAddrWrite", w.knxAddressWrite);
|
||||
cJSON_AddNumberToObject(widget, "action", static_cast<int>(w.action));
|
||||
cJSON_AddNumberToObject(widget, "targetScreen", w.targetScreen);
|
||||
|
||||
cJSON_AddItemToArray(widgets, widget);
|
||||
}
|
||||
|
||||
cJSON_AddItemToArray(screens, screenJson);
|
||||
}
|
||||
|
||||
// Print to buffer
|
||||
char* jsonStr = cJSON_PrintUnformatted(root);
|
||||
if (jsonStr) {
|
||||
@ -590,27 +921,27 @@ bool WidgetManager::updateConfigFromJson(const char* json) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse background color
|
||||
cJSON* bgColor = cJSON_GetObjectItem(root, "bgColor");
|
||||
if (cJSON_IsString(bgColor)) {
|
||||
config_.backgroundColor = Color::fromHex(parseHexColor(bgColor->valuestring));
|
||||
}
|
||||
|
||||
// Parse widgets array
|
||||
cJSON* widgets = cJSON_GetObjectItem(root, "widgets");
|
||||
if (!cJSON_IsArray(widgets)) {
|
||||
std::unique_ptr<GuiConfig> newConfig(new (std::nothrow) GuiConfig());
|
||||
if (!newConfig) {
|
||||
ESP_LOGE(TAG, "Out of memory for config");
|
||||
cJSON_Delete(root);
|
||||
return false;
|
||||
}
|
||||
newConfig->clear();
|
||||
|
||||
config_.widgetCount = 0;
|
||||
auto parseWidgets = [&](cJSON* widgets, ScreenConfig& screen) -> bool {
|
||||
if (!cJSON_IsArray(widgets)) return false;
|
||||
|
||||
screen.widgetCount = 0;
|
||||
cJSON* widget = nullptr;
|
||||
cJSON_ArrayForEach(widget, widgets) {
|
||||
if (config_.widgetCount >= MAX_WIDGETS) break;
|
||||
if (screen.widgetCount >= MAX_WIDGETS) break;
|
||||
|
||||
WidgetConfig& w = config_.widgets[config_.widgetCount];
|
||||
WidgetConfig& w = screen.widgets[screen.widgetCount];
|
||||
memset(&w, 0, sizeof(w));
|
||||
w.visible = true;
|
||||
w.action = ButtonAction::KNX;
|
||||
w.targetScreen = 0;
|
||||
|
||||
// Parse basic properties
|
||||
cJSON* id = cJSON_GetObjectItem(widget, "id");
|
||||
@ -696,10 +1027,124 @@ bool WidgetManager::updateConfigFromJson(const char* json) {
|
||||
cJSON* knxAddrWrite = cJSON_GetObjectItem(widget, "knxAddrWrite");
|
||||
if (cJSON_IsNumber(knxAddrWrite)) w.knxAddressWrite = knxAddrWrite->valueint;
|
||||
|
||||
config_.widgetCount++;
|
||||
cJSON* action = cJSON_GetObjectItem(widget, "action");
|
||||
if (cJSON_IsNumber(action)) w.action = static_cast<ButtonAction>(action->valueint);
|
||||
|
||||
cJSON* targetScreen = cJSON_GetObjectItem(widget, "targetScreen");
|
||||
if (cJSON_IsNumber(targetScreen)) w.targetScreen = targetScreen->valueint;
|
||||
|
||||
screen.widgetCount++;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
cJSON* screens = cJSON_GetObjectItem(root, "screens");
|
||||
if (cJSON_IsArray(screens)) {
|
||||
cJSON* screenJson = nullptr;
|
||||
cJSON_ArrayForEach(screenJson, screens) {
|
||||
if (newConfig->screenCount >= MAX_SCREENS) break;
|
||||
|
||||
uint8_t screenId = newConfig->screenCount;
|
||||
const char* screenName = nullptr;
|
||||
|
||||
cJSON* id = cJSON_GetObjectItem(screenJson, "id");
|
||||
if (cJSON_IsNumber(id)) {
|
||||
int idVal = id->valueint;
|
||||
if (idVal < 0) idVal = 0;
|
||||
screenId = static_cast<uint8_t>(idVal);
|
||||
}
|
||||
|
||||
cJSON* name = cJSON_GetObjectItem(screenJson, "name");
|
||||
if (cJSON_IsString(name)) screenName = name->valuestring;
|
||||
|
||||
ScreenConfig& screen = newConfig->screens[newConfig->screenCount];
|
||||
screen.clear(screenId, screenName);
|
||||
|
||||
if (!screen.name[0]) {
|
||||
char fallback[16];
|
||||
snprintf(fallback, sizeof(fallback), "Screen %d", screenId);
|
||||
strncpy(screen.name, fallback, sizeof(screen.name) - 1);
|
||||
}
|
||||
|
||||
cJSON* mode = cJSON_GetObjectItem(screenJson, "mode");
|
||||
if (cJSON_IsNumber(mode)) screen.mode = static_cast<ScreenMode>(mode->valueint);
|
||||
|
||||
cJSON* bgColor = cJSON_GetObjectItem(screenJson, "bgColor");
|
||||
if (cJSON_IsString(bgColor)) {
|
||||
screen.backgroundColor = Color::fromHex(parseHexColor(bgColor->valuestring));
|
||||
}
|
||||
|
||||
cJSON* widgets = cJSON_GetObjectItem(screenJson, "widgets");
|
||||
if (!parseWidgets(widgets, screen)) {
|
||||
screen.widgetCount = 0;
|
||||
}
|
||||
|
||||
newConfig->screenCount++;
|
||||
}
|
||||
} else {
|
||||
newConfig->screenCount = 1;
|
||||
ScreenConfig& screen = newConfig->screens[0];
|
||||
screen.clear(0, "Screen 1");
|
||||
|
||||
cJSON* bgColor = cJSON_GetObjectItem(root, "bgColor");
|
||||
if (cJSON_IsString(bgColor)) {
|
||||
screen.backgroundColor = Color::fromHex(parseHexColor(bgColor->valuestring));
|
||||
}
|
||||
|
||||
cJSON* widgets = cJSON_GetObjectItem(root, "widgets");
|
||||
if (!parseWidgets(widgets, screen)) {
|
||||
cJSON_Delete(root);
|
||||
ESP_LOGI(TAG, "Parsed %d widgets from JSON", config_.widgetCount);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
cJSON* startScreen = cJSON_GetObjectItem(root, "startScreen");
|
||||
if (cJSON_IsNumber(startScreen)) {
|
||||
int val = startScreen->valueint;
|
||||
if (val < 0) val = 0;
|
||||
newConfig->startScreenId = static_cast<uint8_t>(val);
|
||||
}
|
||||
|
||||
cJSON* standby = cJSON_GetObjectItem(root, "standby");
|
||||
if (cJSON_IsObject(standby)) {
|
||||
cJSON* enabled = cJSON_GetObjectItem(standby, "enabled");
|
||||
if (cJSON_IsBool(enabled)) newConfig->standbyEnabled = cJSON_IsTrue(enabled);
|
||||
|
||||
cJSON* screen = cJSON_GetObjectItem(standby, "screen");
|
||||
if (cJSON_IsNumber(screen)) {
|
||||
int val = screen->valueint;
|
||||
if (val < 0) {
|
||||
newConfig->standbyScreenId = SCREEN_ID_NONE;
|
||||
} else {
|
||||
newConfig->standbyScreenId = static_cast<uint8_t>(val);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON* minutes = cJSON_GetObjectItem(standby, "minutes");
|
||||
if (cJSON_IsNumber(minutes)) {
|
||||
int val = minutes->valueint;
|
||||
if (val < 0) val = 0;
|
||||
newConfig->standbyMinutes = static_cast<uint16_t>(val);
|
||||
}
|
||||
}
|
||||
|
||||
if (newConfig->screenCount == 0) {
|
||||
cJSON_Delete(root);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!newConfig->findScreen(newConfig->startScreenId)) {
|
||||
newConfig->startScreenId = newConfig->screens[0].id;
|
||||
}
|
||||
|
||||
if (!newConfig->findScreen(newConfig->standbyScreenId)) {
|
||||
newConfig->standbyEnabled = false;
|
||||
newConfig->standbyScreenId = SCREEN_ID_NONE;
|
||||
}
|
||||
|
||||
config_ = *newConfig;
|
||||
cJSON_Delete(root);
|
||||
ESP_LOGI(TAG, "Parsed %d screens from JSON", config_.screenCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -27,14 +27,24 @@ public:
|
||||
// Reset to factory defaults
|
||||
void resetToDefaults();
|
||||
|
||||
// Periodic tasks (standby handling)
|
||||
void loop();
|
||||
|
||||
// User activity (resets standby timer)
|
||||
void onUserActivity();
|
||||
|
||||
// KNX value update (called from KnxWorker)
|
||||
void onKnxValue(uint16_t groupAddr, float value);
|
||||
void onKnxSwitch(uint16_t groupAddr, bool value);
|
||||
void onKnxText(uint16_t groupAddr, const char* text);
|
||||
|
||||
// Button action handler
|
||||
void handleButtonAction(const WidgetConfig& cfg, lv_obj_t* target);
|
||||
void goBack();
|
||||
|
||||
// Direct config access
|
||||
ScreenConfig& getConfig() { return config_; }
|
||||
const ScreenConfig& getConfig() const { return config_; }
|
||||
GuiConfig& getConfig() { return config_; }
|
||||
const GuiConfig& getConfig() const { return config_; }
|
||||
|
||||
private:
|
||||
WidgetManager();
|
||||
@ -45,18 +55,38 @@ private:
|
||||
void loadFromSdCard();
|
||||
void saveToSdCard();
|
||||
void destroyAllWidgets();
|
||||
void createAllWidgets();
|
||||
lv_obj_t* createWidget(const WidgetConfig& cfg);
|
||||
void createAllWidgets(const ScreenConfig& screen, lv_obj_t* parent);
|
||||
lv_obj_t* createWidget(const WidgetConfig& cfg, lv_obj_t* parent);
|
||||
void applyStyle(lv_obj_t* obj, const WidgetConfig& cfg);
|
||||
void applyLedStyle(lv_obj_t* obj, const WidgetConfig& cfg);
|
||||
const lv_font_t* getFontBySize(uint8_t sizeIndex);
|
||||
|
||||
void createDefaultConfig();
|
||||
void applyScreen(uint8_t screenId);
|
||||
void showScreen(uint8_t screenId);
|
||||
void showModalScreen(const ScreenConfig& screen);
|
||||
void closeModal();
|
||||
void enterStandby();
|
||||
ScreenConfig* activeScreen();
|
||||
const ScreenConfig* activeScreen() const;
|
||||
|
||||
static constexpr const char* CONFIG_FILE = "/sdcard/lvgl.json";
|
||||
|
||||
ScreenConfig config_;
|
||||
GuiConfig config_;
|
||||
uint8_t activeScreenId_ = 0;
|
||||
uint8_t previousScreenId_ = 0xFF;
|
||||
uint8_t standbyReturnScreenId_ = 0xFF;
|
||||
uint8_t modalScreenId_ = 0xFF;
|
||||
bool standbyActive_ = false;
|
||||
bool standbyWakePending_ = false;
|
||||
uint8_t standbyWakeTarget_ = 0xFF;
|
||||
bool navPending_ = false;
|
||||
ButtonAction navAction_ = ButtonAction::KNX;
|
||||
uint8_t navTargetScreen_ = 0xFF;
|
||||
int64_t lastActivityUs_ = 0;
|
||||
|
||||
// Runtime widget references (indexed by widget ID)
|
||||
std::array<lv_obj_t*, MAX_WIDGETS> widgetObjects_;
|
||||
lv_obj_t* screen_ = nullptr;
|
||||
lv_obj_t* modalContainer_ = nullptr;
|
||||
};
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
#include "Display.hpp"
|
||||
#include "Touch.hpp"
|
||||
#include "Gui.hpp"
|
||||
#include "WidgetManager.hpp"
|
||||
#include "Nvs.hpp"
|
||||
#include "KnxWorker.hpp"
|
||||
#include "Wifi.hpp"
|
||||
@ -72,6 +73,7 @@ public:
|
||||
while (true) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
knxWorker.loop();
|
||||
WidgetManager::instance().loop();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,13 +6,13 @@
|
||||
static const char* TAG = "WebServer";
|
||||
|
||||
esp_err_t WebServer::getConfigHandler(httpd_req_t* req) {
|
||||
char* buf = new char[8192];
|
||||
char* buf = new char[32768];
|
||||
if (buf == nullptr) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Out of memory");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
WidgetManager::instance().getConfigJson(buf, 8192);
|
||||
WidgetManager::instance().getConfigJson(buf, 32768);
|
||||
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
httpd_resp_send(req, buf, strlen(buf));
|
||||
@ -23,7 +23,7 @@ esp_err_t WebServer::getConfigHandler(httpd_req_t* req) {
|
||||
|
||||
esp_err_t WebServer::postConfigHandler(httpd_req_t* req) {
|
||||
int total_len = req->content_len;
|
||||
if (total_len > 8192) {
|
||||
if (total_len > 32768) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Content too large");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user