Add dynamic pages with clock, date, Home Assistant values and fonts

The hardcoded clock is now the built-in dynamic page "Clock". A dynamic page
has a background bitmap per time of day and up to 8 elements (clock, date,
Home Assistant value, text), each with position, alignment, font and overlay.
Pages are drawn on the ESP32, also for the web previews.

- Pages tab in the web UI: page editor with drag and drop, font upload
  (TTF/OTF converted in the browser to the FDF1 dot font format)
- Built-in fonts Small, Normal, Large and Full height (generated by
  tools/make_builtin_fonts.py), built-in bitmaps Pikachu and Pikachu yawning
- Home Assistant values over the REST API (URL and token in the HA tab)
- Settings version 4: evening settings removed (now time slots of the Clock
  page), old records are converted
- HA Mode dropdown lists the dynamic pages and bitmaps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-15 19:43:59 +02:00
1 parent 1d3b084de1
commit 0411804a2f
49 files changed
+3058 -331

No files matched your search

+114
View File
@@ -0,0 +1,114 @@
#include "display/font_store.h"
#include <LittleFS.h>
#include <map>
#include "display/bitmap_store.h"
#include "logic/bitmap_format.h"
#include "logic/builtin_fonts.h"
static const char *DIR = "/fonts";
static const char *EXTENSION = ".fnt";
static uint32_t revision = 0;
// Loaded fonts by name; cleared when a font is saved or deleted
static std::map<String, std::vector<uint8_t>> cache;
static String path(const char *name)
{
return String(DIR) + "/" + name + EXTENSION;
}
std::vector<FontInfo> listFonts()
{
std::vector<FontInfo> list;
for (int i = 0; i < BUILTIN_FONT_COUNT; i++)
list.push_back({BUILTIN_FONTS[i].name, true, (uint8_t)fontHeight(builtinFont(BUILTIN_FONTS[i]))});
for (const String &name : storedNames(DIR, EXTENSION))
if (!findBuiltinFont(name.c_str()))
list.push_back({name, false, (uint8_t)fontHeight(findFont(name.c_str()))});
return list;
}
uint32_t fontListRevision()
{
return revision;
}
bool fontExists(const char *name)
{
if (findBuiltinFont(name))
return true;
return bitmapStorageAvailable() && validBitmapName(name) && LittleFS.exists(path(name));
}
Font findFont(const char *name)
{
const BuiltinFont *builtin = findBuiltinFont(name);
if (builtin)
return builtinFont(*builtin);
Font font;
if (!bitmapStorageAvailable() || !validBitmapName(name))
return font;
auto cached = cache.find(name);
if (cached == cache.end())
{
if (cache.size() >= 16)
cache.clear();
std::vector<uint8_t> data;
File file;
if (LittleFS.exists(path(name))) // open() logs an error for a missing file
file = LittleFS.open(path(name), "r");
if (file && file.size() <= FONT_MAX_SIZE)
{
data.resize(file.size());
if (file.read(data.data(), data.size()) != data.size() || !validFont(data.data(), data.size()))
data.clear();
}
if (file)
file.close();
cached = cache.emplace(name, std::move(data)).first; // also remembers a missing font
}
if (!cached->second.empty())
{
font.data = cached->second.data();
font.size = cached->second.size();
}
return font;
}
const char *saveFont(const char *name, const uint8_t *data, size_t size)
{
if (!validBitmapName(name))
return "Invalid name: use 1-24 letters, digits, spaces, - or _";
if (findBuiltinFont(name))
return "A built-in font has this name";
if (!validFont(data, size))
return "Invalid font data";
if (!bitmapStorageAvailable())
return "No storage (install the new flash layout over USB, see README)";
if (!LittleFS.exists(path(name)) && storedNames(DIR, EXTENSION).size() >= (size_t)MAX_FONTS)
return "Too many fonts, delete some first";
File file = LittleFS.open(path(name), "w");
bool ok = file && file.write(data, size) == size;
if (file)
file.close();
cache.clear();
revision++;
return ok ? nullptr : "Could not write the file (storage full?)";
}
const char *deleteFont(const char *name)
{
if (findBuiltinFont(name))
return "Built-in fonts can't be deleted";
if (!bitmapStorageAvailable() || !validBitmapName(name) || !LittleFS.exists(path(name)))
return "Font not found";
if (!LittleFS.remove(path(name)))
return "Could not delete the file";
cache.clear();
revision++;
return nullptr;
}