This commit is contained in:
Thomas Peterson 2026-01-25 11:56:24 +01:00
parent ed03347331
commit 872c004b76
31 changed files with 268 additions and 39 deletions

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +1,4 @@
idf_component_register(SRCS "KnxWorker.cpp" "Nvs.cpp" "main.cpp" "Display.cpp" "Touch.cpp" "Gui.cpp" "Wifi.cpp" "LvglIdle.c" "Gui/WifiSetting.cpp" "Gui/EthSetting.cpp" "Hardware/Eth.cpp" "WidgetManager.cpp" "WidgetConfig.cpp" "SdCard.cpp"
idf_component_register(SRCS "KnxWorker.cpp" "Nvs.cpp" "main.cpp" "Display.cpp" "Touch.cpp" "Gui.cpp" "Wifi.cpp" "LvglIdle.c" "Gui/WifiSetting.cpp" "Gui/EthSetting.cpp" "Hardware/Eth.cpp" "WidgetManager.cpp" "WidgetConfig.cpp" "SdCard.cpp" "Fonts.cpp"
"widgets/Widget.cpp"
"widgets/LabelWidget.cpp"
"widgets/ButtonWidget.cpp"

120
main/Fonts.cpp Normal file
View File

@ -0,0 +1,120 @@
#include "Fonts.hpp"
#include "esp_log.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <cstddef>
#if CONFIG_ESP_LVGL_ADAPTER_ENABLE_FREETYPE
#include "esp_lv_adapter.h"
#include <sys/stat.h>
#endif
namespace {
static const char* TAG = "Fonts";
static constexpr const char* kFontPath = "/sdcard/fonts/Montserrat-Medium.ttf";
static constexpr uint16_t kFontSizes[] = {14, 18, 22, 28, 36, 48};
static constexpr size_t kFontCount = sizeof(kFontSizes) / sizeof(kFontSizes[0]);
static const lv_font_t* s_fonts[kFontCount] = {nullptr};
static bool s_initialized = false;
const lv_font_t* fallbackFont(uint8_t sizeIndex) {
switch (sizeIndex) {
case 0: return &lv_font_montserrat_14;
#if LV_FONT_MONTSERRAT_18
case 1: return &lv_font_montserrat_18;
#endif
#if LV_FONT_MONTSERRAT_22
case 2: return &lv_font_montserrat_22;
#endif
#if LV_FONT_MONTSERRAT_28
case 3: return &lv_font_montserrat_28;
#endif
#if LV_FONT_MONTSERRAT_36
case 4: return &lv_font_montserrat_36;
#endif
#if LV_FONT_MONTSERRAT_48
case 5: return &lv_font_montserrat_48;
#endif
default: return &lv_font_montserrat_14;
}
}
#if CONFIG_ESP_LVGL_ADAPTER_ENABLE_FREETYPE
bool fontFileExists() {
struct stat st;
return stat(kFontPath, &st) == 0;
}
#endif
} // namespace
void Fonts::init() {
#if CONFIG_ESP_LVGL_ADAPTER_ENABLE_FREETYPE
ESP_LOGI(TAG, "FreeType enabled");
if (s_initialized) {
ESP_LOGI(TAG, "Already initialized");
return;
}
s_initialized = true;
if (!fontFileExists()) {
ESP_LOGW(TAG, "Font file not found: %s", kFontPath);
return;
}
ESP_LOGI(TAG, "Font file exists: %s", kFontPath);
// Give LVGL task time to start and release mutex
ESP_LOGI(TAG, "Waiting for LVGL task...");
vTaskDelay(pdMS_TO_TICKS(500));
// Test if LVGL lock is available
ESP_LOGI(TAG, "Testing LVGL lock...");
if (esp_lv_adapter_lock(1000) == ESP_OK) { // 1 second timeout
ESP_LOGI(TAG, "Lock acquired OK, releasing...");
esp_lv_adapter_unlock();
} else {
ESP_LOGE(TAG, "Could not acquire LVGL lock - something is holding it!");
return; // Don't try to init fonts if lock is stuck
}
for (size_t i = 0; i < kFontCount; ++i) {
ESP_LOGI(TAG, "Loading font size %u...", kFontSizes[i]);
esp_lv_adapter_ft_font_config_t cfg = {};
cfg.name = kFontPath;
cfg.size = kFontSizes[i];
cfg.style = ESP_LV_ADAPTER_FT_FONT_STYLE_NORMAL;
cfg.mem = nullptr;
cfg.mem_size = 0;
esp_lv_adapter_ft_font_handle_t handle = nullptr;
ESP_LOGI(TAG, "Calling esp_lv_adapter_ft_font_init (free heap: %lu)...",
(unsigned long)esp_get_free_heap_size());
esp_err_t ret = esp_lv_adapter_ft_font_init(&cfg, &handle);
ESP_LOGI(TAG, "esp_lv_adapter_ft_font_init returned %d", ret);
if (ret != ESP_OK || handle == nullptr) {
ESP_LOGW(TAG, "Failed to load FreeType font size %u (err=%d)", cfg.size, ret);
continue;
}
s_fonts[i] = esp_lv_adapter_ft_font_get(handle);
if (!s_fonts[i]) {
ESP_LOGW(TAG, "FreeType font handle returned null for size %u", cfg.size);
}
ESP_LOGI(TAG, "Font size %u loaded successfully", kFontSizes[i]);
}
ESP_LOGI(TAG, "Font initialization complete");
#else
ESP_LOGI(TAG, "FreeType disabled, using built-in fonts");
#endif
}
const lv_font_t* Fonts::bySizeIndex(uint8_t sizeIndex) {
#if CONFIG_ESP_LVGL_ADAPTER_ENABLE_FREETYPE
if (sizeIndex < kFontCount && s_fonts[sizeIndex]) {
return s_fonts[sizeIndex];
}
#endif
return fallbackFont(sizeIndex);
}

10
main/Fonts.hpp Normal file
View File

@ -0,0 +1,10 @@
#pragma once
#include <cstdint>
#include "lvgl.h"
class Fonts {
public:
static void init();
static const lv_font_t* bySizeIndex(uint8_t sizeIndex);
};

View File

@ -1,4 +1,5 @@
#include "EthSetting.hpp"
#include "../Fonts.hpp"
#include "esp_lv_adapter.h"
#include "esp_log.h"
#include <cstring>
@ -51,6 +52,7 @@ void EthSetting::createUI() {
lv_obj_set_size(overlay_, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_bg_color(overlay_, lv_color_hex(0x202020), 0);
lv_obj_set_style_bg_opa(overlay_, LV_OPA_COVER, 0);
lv_obj_set_style_text_font(overlay_, Fonts::bySizeIndex(0), 0);
lv_obj_set_style_pad_all(overlay_, 10, 0);
lv_obj_clear_flag(overlay_, LV_OBJ_FLAG_SCROLLABLE);
@ -73,7 +75,7 @@ void EthSetting::createUI() {
lv_obj_t* titleLbl = lv_label_create(header);
lv_label_set_text(titleLbl, "Eth Settings");
lv_obj_set_style_text_font(titleLbl, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_font(titleLbl, Fonts::bySizeIndex(0), 0);
lv_obj_align(titleLbl, LV_ALIGN_CENTER, 0, 0);
}

View File

@ -1,5 +1,6 @@
#include "WifiSetting.hpp"
#include "../Wifi.hpp"
#include "../Fonts.hpp"
#include "esp_lv_adapter.h"
#include "esp_log.h"
#include <cstring>
@ -72,6 +73,7 @@ void WifiSetting::createUI() {
lv_obj_set_size(overlay_, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_bg_color(overlay_, lv_color_hex(0x202020), 0);
lv_obj_set_style_bg_opa(overlay_, LV_OPA_COVER, 0);
lv_obj_set_style_text_font(overlay_, Fonts::bySizeIndex(0), 0);
lv_obj_set_style_pad_all(overlay_, 10, 0);
lv_obj_clear_flag(overlay_, LV_OBJ_FLAG_SCROLLABLE);
@ -94,7 +96,7 @@ void WifiSetting::createUI() {
lv_obj_t* titleLbl = lv_label_create(header);
lv_label_set_text(titleLbl, "WiFi Settings");
lv_obj_set_style_text_font(titleLbl, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_font(titleLbl, Fonts::bySizeIndex(0), 0);
lv_obj_align(titleLbl, LV_ALIGN_CENTER, 0, 0);
lv_obj_t* statusSection = lv_obj_create(overlay_);

View File

@ -95,7 +95,8 @@ bool SdCard::init() {
struct stat st;
const char* dirs[] = {
"/sdcard/webseite",
"/sdcard/images"
"/sdcard/images",
"/sdcard/fonts"
};
for (const char* dir : dirs) {

View File

@ -8,12 +8,67 @@
#include <memory>
#include <new>
#include <cstdio>
#include <cstdint>
#include <cstring>
#include <cstdlib>
static const char* TAG = "WidgetMgr";
static constexpr uint8_t SCREEN_ID_NONE = 0xFF;
static bool is_valid_utf8(const char* text, size_t len) {
size_t i = 0;
while (i < len) {
uint8_t c = static_cast<uint8_t>(text[i]);
if (c < 0x80) {
i++;
continue;
}
if ((c & 0xE0) == 0xC0) {
if (i + 1 >= len) return false;
uint8_t c1 = static_cast<uint8_t>(text[i + 1]);
if ((c1 & 0xC0) != 0x80) return false;
i += 2;
continue;
}
if ((c & 0xF0) == 0xE0) {
if (i + 2 >= len) return false;
uint8_t c1 = static_cast<uint8_t>(text[i + 1]);
uint8_t c2 = static_cast<uint8_t>(text[i + 2]);
if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80) return false;
i += 3;
continue;
}
if ((c & 0xF8) == 0xF0) {
if (i + 3 >= len) return false;
uint8_t c1 = static_cast<uint8_t>(text[i + 1]);
uint8_t c2 = static_cast<uint8_t>(text[i + 2]);
uint8_t c3 = static_cast<uint8_t>(text[i + 3]);
if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80 || (c3 & 0xC0) != 0x80) return false;
i += 4;
continue;
}
return false;
}
return true;
}
static void latin1_to_utf8(const char* src, size_t src_len, char* dst, size_t dst_size) {
if (dst_size == 0) return;
size_t di = 0;
for (size_t si = 0; si < src_len; ++si) {
uint8_t c = static_cast<uint8_t>(src[si]);
if (c < 0x80) {
if (di + 1 >= dst_size) break;
dst[di++] = static_cast<char>(c);
} else {
if (di + 2 >= dst_size) break;
dst[di++] = static_cast<char>(0xC0 | (c >> 6));
dst[di++] = static_cast<char>(0x80 | (c & 0x3F));
}
}
dst[di] = '\0';
}
// WidgetManager implementation
WidgetManager& WidgetManager::instance() {
static WidgetManager inst;
@ -384,7 +439,9 @@ void WidgetManager::handleButtonAction(const WidgetConfig& cfg, lv_obj_t* target
ESP_LOGI(TAG, "handleButtonAction: button=%d action=%d targetScreen=%d type=%d",
cfg.id, static_cast<int>(cfg.action), cfg.targetScreen, static_cast<int>(cfg.type));
if (cfg.type != WidgetType::BUTTON) return;
if (cfg.type != WidgetType::BUTTON) {
return;
}
onUserActivity();
@ -551,7 +608,14 @@ void WidgetManager::onKnxText(uint16_t groupAddr, const char* text) {
event.type = UiEventType::KNX_TEXT;
event.groupAddr = groupAddr;
if (text) {
strncpy(event.text, text, sizeof(event.text) - 1);
const size_t max_len = sizeof(event.text) - 1;
const size_t src_len = strnlen(text, max_len);
if (is_valid_utf8(text, src_len)) {
memcpy(event.text, text, src_len);
event.text[src_len] = '\0';
} else {
latin1_to_utf8(text, src_len, event.text, sizeof(event.text));
}
} else {
event.text[0] = '\0';
}

View File

@ -14,6 +14,7 @@
#include "Hardware/Eth.hpp"
#include "WebServer.hpp"
#include "SdCard.hpp"
#include "Fonts.hpp"
#define TAG "App"
@ -61,6 +62,16 @@ public:
ESP_ERROR_CHECK(esp_lv_adapter_start());
// Test LVGL lock right after start
ESP_LOGI(TAG, "Testing LVGL lock after adapter start...");
vTaskDelay(pdMS_TO_TICKS(100));
if (esp_lv_adapter_lock(1000) == ESP_OK) {
ESP_LOGI(TAG, "LVGL lock OK after adapter start");
esp_lv_adapter_unlock();
} else {
ESP_LOGE(TAG, "LVGL lock BLOCKED right after adapter start!");
}
ESP_LOGI(TAG, "INIT SUCCESS");
}
@ -70,16 +81,18 @@ public:
ESP_LOGW(TAG, "SD card not available, using defaults");
}
ESP_LOGI(TAG, "Creating UI");
ESP_LOGI(TAG, "START KNX");
BaseType_t knx_ok = xTaskCreatePinnedToCore(
knx_task, "knx", 4096, &Gui::knxWorker, 5, nullptr, 1);
if (knx_ok != pdPASS) {
ESP_LOGE(TAG, "Failed to start KNX task");
}
gui.create();
// Start WebServer for widget configuration
ESP_LOGI(TAG, "START WEBSERVER");
WebServer::instance().start();
ESP_LOGI(TAG, "INIT FONTS");
Fonts::init();
ESP_LOGI(TAG, "CREATE GUI");
gui.create();
ESP_LOGI(TAG, "Application running");
while (true) {

View File

@ -2,6 +2,7 @@
#include "../SdCard.hpp"
#include "esp_log.h"
#include <cstring>
#include <string.h>
#include <cstdio>
#include <sys/stat.h>
#include <dirent.h>
@ -101,8 +102,26 @@ esp_err_t WebServer::filesUploadHandler(httpd_req_t* req) {
return sendJsonError(req, "Invalid multipart boundary");
}
boundaryStart += 9;
char boundary[72];
snprintf(boundary, sizeof(boundary), "--%.68s", boundaryStart);
// Skip leading quote if present
if (*boundaryStart == '"') {
boundaryStart++;
}
// Copy boundary and remove trailing quote if present
char boundaryValue[72];
strncpy(boundaryValue, boundaryStart, sizeof(boundaryValue) - 1);
boundaryValue[sizeof(boundaryValue) - 1] = '\0';
// Remove trailing quote or any trailing whitespace/semicolon
char* endPtr = boundaryValue;
while (*endPtr && *endPtr != '"' && *endPtr != ';' && *endPtr != ' ' && *endPtr != '\r' && *endPtr != '\n') {
endPtr++;
}
*endPtr = '\0';
char boundary[80];
snprintf(boundary, sizeof(boundary), "--%s", boundaryValue);
size_t contentLen = req->content_len;
char* fullData = new char[contentLen + 1];
@ -146,11 +165,17 @@ esp_err_t WebServer::filesUploadHandler(httpd_req_t* req) {
}
dataStart += 4;
char endBoundary[80];
snprintf(endBoundary, sizeof(endBoundary), "\r\n%.72s", boundary);
char* dataEnd = strstr(dataStart, endBoundary);
// Use memmem for binary-safe search (fonts/images may contain null bytes)
char endBoundary[84];
snprintf(endBoundary, sizeof(endBoundary), "\r\n%s", boundary);
size_t endBoundaryLen = strlen(endBoundary);
size_t searchLen = totalReceived - (dataStart - fullData);
char* dataEnd = (char*)memmem(dataStart, searchLen, endBoundary, endBoundaryLen);
if (!dataEnd) {
dataEnd = strstr(dataStart, boundary);
// Try without leading \r\n
size_t boundaryLen = strlen(boundary);
dataEnd = (char*)memmem(dataStart, searchLen, boundary, boundaryLen);
}
if (!dataEnd) {
delete[] fullData;

View File

@ -1,4 +1,5 @@
#include "Widget.hpp"
#include "../Fonts.hpp"
Widget::Widget(const WidgetConfig& config)
: config_(config)
@ -74,24 +75,5 @@ void Widget::applyShadowStyle() {
}
const lv_font_t* Widget::getFontBySize(uint8_t sizeIndex) {
// Font sizes: 0=14, 1=18, 2=22, 3=28, 4=36, 5=48
switch (sizeIndex) {
case 0: return &lv_font_montserrat_14;
#if LV_FONT_MONTSERRAT_18
case 1: return &lv_font_montserrat_18;
#endif
#if LV_FONT_MONTSERRAT_22
case 2: return &lv_font_montserrat_22;
#endif
#if LV_FONT_MONTSERRAT_28
case 3: return &lv_font_montserrat_28;
#endif
#if LV_FONT_MONTSERRAT_36
case 4: return &lv_font_montserrat_36;
#endif
#if LV_FONT_MONTSERRAT_48
case 5: return &lv_font_montserrat_48;
#endif
default: return &lv_font_montserrat_14;
}
return Fonts::bySizeIndex(sizeIndex);
}

View File

@ -1,4 +1,4 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 0x200000,
factory, app, factory, 0x10000, 0x300000,

1 # Name Type SubType Offset Size Flags
2 nvs data nvs 0x9000 0x6000
3 phy_init data phy 0xf000 0x1000
4 factory app factory 0x10000 0x200000 0x300000

Binary file not shown.

View File

@ -6,3 +6,13 @@ CONFIG_LV_FONT_MONTSERRAT_22=y
CONFIG_LV_FONT_MONTSERRAT_28=y
CONFIG_LV_FONT_MONTSERRAT_36=y
CONFIG_LV_FONT_MONTSERRAT_48=y
# Keep LVGL draw thread stack reasonable to avoid xTaskCreate failures
CONFIG_LV_DRAW_THREAD_STACK_SIZE=32768
# Enable FreeType fonts for extended glyph coverage (e.g. umlauts)
CONFIG_LV_USE_FREETYPE=y
CONFIG_ESP_LVGL_ADAPTER_ENABLE_FREETYPE=y
# Increase main task stack for FreeType (needs ~16KB+)
CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384