Files
Flipdot-display/src/esp32/system/settings_store.cpp
T
mangavalkandClaude Opus 5.5 0411804a2f 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>
2026-10-15 19:43:59 +02:00

140 lines
3.7 KiB
C++

#include "system/settings_store.h"
#include <Preferences.h>
#include <stddef.h>
#include <string.h>
static const char *NAMESPACE = "flipdot";
static const char *KEY = "settings";
// Increase when the Settings struct changes; old records are then ignored (defaults),
// 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;
struct OldVersion
{
uint16_t version;
size_t fields; // bytes of the settings struct that version had
};
// 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;
uint16_t size;
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();
StoredSettings stored;
memset(&stored, 0, sizeof(stored));
Preferences prefs;
prefs.begin(NAMESPACE, true);
size_t read = prefs.getBytes(KEY, &stored, sizeof(stored));
prefs.end();
Settings loaded;
if (read == sizeof(stored) && stored.version == VERSION && stored.size == sizeof(Settings))
loaded = stored.settings;
else
{
bool ok = false;
for (const OldVersion &o : v3::VERSIONS)
if (o.version == stored.version)
{
v3::Settings old;
ok = readOld(stored, read, o.fields, old);
if (ok)
v3::convert(old, loaded);
}
if (!ok)
return;
}
if (validateSettings(loaded) == nullptr)
settings = loaded;
}
void saveSettings(const Settings &settings)
{
StoredSettings stored;
stored.version = VERSION;
stored.size = sizeof(Settings);
stored.settings = settings;
Preferences prefs;
prefs.begin(NAMESPACE, false);
prefs.putBytes(KEY, &stored, sizeof(stored));
prefs.end();
}