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

+81 -12
View File
@@ -7,21 +7,77 @@
static const char *NAMESPACE = "flipdot";
static const char *KEY = "settings";
// Increase when the Settings struct changes; old records are then ignored (defaults),
// unless OLD_VERSIONS says how to read them
static const uint16_t VERSION = 3;
// unless the code below says how to read them. When fields are only added at the end,
// an old record is the start of the new layout: add an OldVersion for it like v3 does.
static const uint16_t VERSION = 4;
// Older versions: fields were only added at the end, so the start of an old record has
// the same layout. New fields keep their defaults.
struct OldVersion
{
uint16_t version;
size_t fields; // bytes of Settings that version had
size_t fields; // bytes of the settings struct that version had
};
static const OldVersion OLD_VERSIONS[] = {
// Versions 1-3: the layout before the dynamic pages (version 4 removed the evening
// settings of the clock and added the Home Assistant API). Frozen copy, read as a prefix
// (version 1 lacked off_page and drive, version 2 drive) and converted.
namespace v3
{
struct ClockSettings
{
bool evening_enabled = true;
uint16_t evening_start = 17 * 60;
bool no_wifi_screen = true;
};
struct HomeAssistantSettings
{
bool enabled = true;
char host[64] = "";
uint16_t port = 1883;
char user[32] = "";
char password[64] = "";
};
struct Settings
{
char page[25] = "clock";
ScheduleSettings schedule;
ClockSettings clock;
HomeAssistantSettings home_assistant;
uint16_t flip_time = 225;
bool status_led = true;
char off_page[25] = "";
DriveSettings drive;
};
static const OldVersion VERSIONS[] = {
{1, offsetof(Settings, off_page)},
{2, offsetof(Settings, drive)},
{3, sizeof(Settings)},
};
static void convert(const Settings &old, ::Settings &s)
{
s = ::Settings();
memcpy(s.page, old.page, sizeof(s.page));
s.schedule = old.schedule;
s.clock.no_wifi_screen = old.clock.no_wifi_screen;
const HomeAssistantSettings &ha = old.home_assistant;
s.home_assistant.enabled = ha.enabled;
memcpy(s.home_assistant.host, ha.host, sizeof(ha.host));
s.home_assistant.port = ha.port;
memcpy(s.home_assistant.user, ha.user, sizeof(ha.user));
memcpy(s.home_assistant.password, ha.password, sizeof(ha.password));
s.flip_time = old.flip_time;
s.status_led = old.status_led;
memcpy(s.off_page, old.off_page, sizeof(s.off_page));
s.drive = old.drive;
// The evening (yawning Pikachu from 17:00) is now a time slot of the built-in Clock page
}
} // namespace v3
static_assert(sizeof(v3::Settings) <= sizeof(Settings), "the old record must fit the buffer");
struct StoredSettings
{
uint16_t version;
@@ -29,6 +85,16 @@ struct StoredSettings
Settings settings;
};
// Reads a record whose first `fields` bytes have the layout of T
template <typename T> static bool readOld(const StoredSettings &stored, size_t read, size_t fields, T &out)
{
if (stored.size < fields || stored.size > sizeof(T) || read != offsetof(StoredSettings, settings) + stored.size)
return false;
out = T();
memcpy((void *)&out, &stored.settings, fields);
return true;
}
void loadSettings(Settings &settings)
{
settings = Settings();
@@ -44,14 +110,17 @@ void loadSettings(Settings &settings)
loaded = stored.settings;
else
{
const OldVersion *old = nullptr;
for (const OldVersion &o : OLD_VERSIONS)
bool ok = false;
for (const OldVersion &o : v3::VERSIONS)
if (o.version == stored.version)
old = &o;
if (!old || stored.size < old->fields || stored.size > sizeof(Settings) ||
read != offsetof(StoredSettings, settings) + stored.size)
{
v3::Settings old;
ok = readOld(stored, read, o.fields, old);
if (ok)
v3::convert(old, loaded);
}
if (!ok)
return;
memcpy((void *)&loaded, &stored.settings, old->fields);
}
if (validateSettings(loaded) == nullptr)
settings = loaded;