Drop the converters for old data

2.15.0 stored all settings, schedules and pages on the display in the
current formats (old_data_left 0), so only those are read now: settings
version 7, schedule version 4, page format 7. The one-time migrations
(icons from 2.10.0, data from 2.15.0) are removed too. About 9 KB less
flash. CLAUDE.md says how to keep data over a future format change.

Version 2.15.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-17 21:21:25 +02:00
1 parent 968492ca35
commit 9959c1aa74
14 files changed
+28 -996

No files matched your search

+2 -150
View File
@@ -109,9 +109,6 @@ static void reloadPage()
controller.pageChanged();
}
static bool settings_converted = false;
static bool schedule_converted = false;
const Settings &appSettings()
{
return settings;
@@ -119,8 +116,8 @@ const Settings &appSettings()
void appLoadSettings()
{
settings_converted = loadSettings(settings);
schedule_converted = loadSchedule(page_schedule);
loadSettings(settings);
loadSchedule(page_schedule);
selector.setSchedule(&page_schedule);
selector.setChosen(settings.page);
haApiWatchRules(&page_schedule);
@@ -470,151 +467,6 @@ const char *appIconToBitmap(const char *name)
return nullptr;
}
// The one-time move, in steps over many loop() passes: LittleFS is slow (listing a
// directory of 40 files takes about a second), and loop() must never get near the
// watchdog. Each pass works for about MIGRATION_STEP_MS.
enum class Migration
{
Start,
Pages, // which candidates the pages use (one page after the other)
Move,
Done,
};
static const uint32_t MIGRATION_STEP_MS = 1000;
static Migration migration = Migration::Start;
static std::vector<String> migration_bitmaps; // uploaded bitmaps not shown on their own
static std::vector<bool> migration_used;
static std::vector<String> migration_pages;
static size_t migration_next = 0;
static int migration_moved = 0;
static uint32_t migration_started = 0;
void appMigrateIcons()
{
if (migration == Migration::Done)
return;
uint32_t pass = millis();
auto time_left = [pass]() { return millis() - pass < MIGRATION_STEP_MS; };
if (migration == Migration::Start)
{
if (iconMigrationDone())
{
migration = Migration::Done;
return;
}
migration_started = pass;
for (const BitmapInfo &b : listBitmaps())
if (!b.builtin && !shownOnItsOwn(b.name.c_str()))
migration_bitmaps.push_back(b.name);
migration_used.assign(migration_bitmaps.size(), false);
for (const PageInfo &p : listPages())
migration_pages.push_back(p.name);
migration_next = 0;
migration = Migration::Pages;
return;
}
if (migration == Migration::Pages)
{
DynamicPage *page = new DynamicPage;
while (migration_next < migration_pages.size() && time_left())
if (loadPage(migration_pages[migration_next++].c_str(), *page))
for (size_t i = 0; i < migration_bitmaps.size(); i++)
if (pageUsesImage(*page, migration_bitmaps[i].c_str(), true))
migration_used[i] = true;
delete page;
if (migration_next >= migration_pages.size())
{
migration_next = 0;
migration = Migration::Move;
}
return;
}
while (migration_next < migration_bitmaps.size() && time_left())
{
size_t i = migration_next++;
const char *name = migration_bitmaps[i].c_str();
// Checked again: the web UI may have changed something in the meantime
if (!migration_used[i] || shownOnItsOwn(name))
continue;
const char *error = moveToIcon(name);
if (error)
Serial.printf("Bitmap \"%s\" stays a bitmap: %s\n", name, error);
else
migration_moved++;
}
if (migration_next < migration_bitmaps.size())
return;
setIconMigrationDone();
Serial.printf("%d bitmaps that only pages use became icons (%lu ms)\n", migration_moved,
(unsigned long)(millis() - migration_started));
migration_bitmaps.clear();
migration_used.clear();
migration_pages.clear();
migration = Migration::Done;
if (migration_moved)
reloadPage();
}
// Same steps as the icon migration: one page per check, MIGRATION_STEP_MS per pass
static bool data_listed = false;
static bool data_done = false;
static std::vector<String> data_pages;
static size_t data_next = 0;
static int data_left = -1;
void appMigrateData()
{
if (data_done || migration != Migration::Done)
return;
uint32_t pass = millis();
if (settings_converted)
{
saveSettings(settings);
settings_converted = false;
Serial.println("Settings stored in the new layout");
}
if (schedule_converted)
{
saveSchedule(page_schedule);
schedule_converted = false;
Serial.println("Schedule stored in the new layout");
}
if (!data_listed)
{
data_pages = storedPageNames();
data_listed = true;
data_left = 0;
return;
}
DynamicPage *page = new DynamicPage;
while (data_next < data_pages.size() && millis() - pass < MIGRATION_STEP_MS)
{
const char *name = data_pages[data_next++].c_str();
if (!pageInOldFormat(name))
continue;
const char *error = loadPage(name, *page) ? savePage(name, *page) : "unreadable";
Serial.printf("Page \"%s\" in the new format: %s\n", name, error ? error : "ok");
if (error)
data_left++;
}
delete page;
if (data_next < data_pages.size())
return;
data_pages.clear();
data_done = true;
}
int appOldDataLeft()
{
if (!data_done)
return -1;
return data_left + (settings_converted ? 1 : 0) + (schedule_converted ? 1 : 0);
}
const char *appSaveFont(const char *name, const uint8_t *data, size_t size)
{
const char *error = saveFont(name, data, size);
-9
View File
@@ -65,15 +65,6 @@ const char *appDeleteIcon(const char *name);
// the other way around; not for bitmaps that are shown on their own
const char *appBitmapToIcon(const char *name);
const char *appIconToBitmap(const char *name);
// Once: the uploaded bitmaps that only pages use become icons. Call from loop(); returns at
// once when it is done.
void appMigrateIcons();
// Once per boot, after the firmware was marked good (the old firmware can't read the new
// formats): settings and schedule read in an older layout are stored again, and pages in an
// older format are rewritten. Call from loop(); returns at once when it is done.
void appMigrateData();
// Records still in an older format (-1 until appMigrateData() checked the pages)
int appOldDataLeft();
const char *appSavePage(const char *name, const DynamicPage &page);
const char *appDeletePage(const char *name);
const char *appSaveFont(const char *name, const uint8_t *data, size_t size);
+1 -1
View File
@@ -118,7 +118,7 @@ const uint32_t SETUP_AP_DELAY = 20000; // open the AP when WiFi fails thi
const uint32_t WIFI_CONNECT_TIMEOUT = 30000;
const uint32_t WIFI_RESTART_TIME = 10UL * 60 * 1000;
#define FIRMWARE_VERSION "2.15.0"
#define FIRMWARE_VERSION "2.15.1"
// Home Assistant device id (do not change: Home Assistant's entities are linked to it)
const uint8_t HA_DEVICE_ID[] = {0x34, 0x86, 0x5d, 0x3a, 0x08, 0x28};
-18
View File
@@ -10,7 +10,6 @@
static const char *DIR = "/icons";
// Written when the bitmaps were moved (not a valid name, so never listed)
static const char *MIGRATED = "/icons/.migrated";
// Built into the firmware (full screen bitmaps, cut to icons): stored at boot so they can
// be edited, and the fallback when they are not stored
@@ -197,20 +196,3 @@ const char *deleteIcon(const char *name)
cache.clear();
return nullptr;
}
bool iconMigrationDone()
{
return !bitmapStorageAvailable() || LittleFS.exists(MIGRATED);
}
void setIconMigrationDone()
{
if (!ensureDir())
return;
File file = LittleFS.open(MIGRATED, "w");
if (file)
{
file.print("1");
file.close();
}
}
-3
View File
@@ -29,6 +29,3 @@ bool loadIcon(const char *name, Icon &icon);
const char *saveIcon(const char *name, const Icon &icon);
const char *deleteIcon(const char *name);
// The one-time move of the bitmaps that only pages use (app.cpp) is done
bool iconMigrationDone();
void setIconMigrationDone();
+2 -465
View File
@@ -7,10 +7,9 @@
static const char *DIR = "/pages";
static const char *EXTENSION = ".pg";
// Increase when DynamicPage changes; older files are ignored unless converted below
// Increase when DynamicPage changes; files of other versions are ignored (the built-in page
// of that name, if any, is used). 2.15.0 rewrote all older pages in this version.
static const uint16_t VERSION = 7;
// Elements per page up to version 5 (the old formats below are frozen with it)
static const int OLD_PAGE_ELEMENTS = 8;
struct StoredPage
{
@@ -19,379 +18,6 @@ struct StoredPage
DynamicPage page;
};
// Version 1 (before scrolling, countdown, stopwatch, images and the multiplier): frozen
// copy of the layout, converted when loaded. Saving writes the current version.
namespace v1
{
struct Element
{
ElementType type;
int16_t x, y;
Align align;
Overlay overlay;
char font[PAGE_NAME_SIZE];
uint8_t format;
Language language;
int8_t decimals;
char text[ELEMENT_TEXT_SIZE];
char suffix[ELEMENT_SUFFIX_SIZE];
char entity[ELEMENT_ENTITY_SIZE];
char attribute[ELEMENT_ATTRIBUTE_SIZE];
};
struct Page
{
uint8_t slot_count;
PageSlot slots[PAGE_SLOTS];
uint8_t element_count;
Element elements[OLD_PAGE_ELEMENTS];
};
struct Stored
{
uint16_t version;
uint16_t size;
Page page;
};
static void convert(const Page &old, DynamicPage &page)
{
clearPage(page);
page.slot_count = old.slot_count;
memcpy(page.slots, old.slots, sizeof(page.slots));
page.element_count = old.element_count;
for (int i = 0; i < OLD_PAGE_ELEMENTS; i++)
{
const Element &o = old.elements[i];
PageElement &e = page.elements[i];
e.type = o.type;
e.x = o.x;
e.y = o.y;
e.align = o.align;
e.overlay = o.overlay;
memcpy(e.font, o.font, sizeof(e.font));
e.format = o.format;
e.language = o.language;
e.decimals = o.decimals;
memcpy(e.text, o.text, sizeof(e.text));
memcpy(e.suffix, o.suffix, sizeof(e.suffix));
memcpy(e.entity, o.entity, sizeof(e.entity));
memcpy(e.attribute, o.attribute, sizeof(e.attribute));
}
}
} // namespace v1
// Version 2 (before the Home Assistant value conversions): the elements lacked the fields
// from `show` on. Frozen copy, converted when loaded.
namespace v2
{
struct Element
{
ElementType type;
int16_t x, y;
Align align;
Overlay overlay;
char font[PAGE_NAME_SIZE];
uint8_t format;
Language language;
int8_t decimals;
char text[ELEMENT_TEXT_SIZE];
char suffix[ELEMENT_SUFFIX_SIZE];
char entity[ELEMENT_ENTITY_SIZE];
char attribute[ELEMENT_ATTRIBUTE_SIZE];
float factor;
uint8_t scroll_width;
uint8_t scroll_speed;
uint32_t target;
};
struct Page
{
uint8_t slot_count;
PageSlot slots[PAGE_SLOTS];
uint8_t element_count;
Element elements[OLD_PAGE_ELEMENTS];
};
struct Stored
{
uint16_t version;
uint16_t size;
Page page;
};
static void convert(const Page &old, DynamicPage &page)
{
clearPage(page);
page.slot_count = old.slot_count;
memcpy(page.slots, old.slots, sizeof(page.slots));
page.element_count = old.element_count;
for (int i = 0; i < OLD_PAGE_ELEMENTS; i++)
{
const Element &o = old.elements[i];
PageElement &e = page.elements[i];
e.type = o.type;
e.x = o.x;
e.y = o.y;
e.align = o.align;
e.overlay = o.overlay;
memcpy(e.font, o.font, sizeof(e.font));
e.format = o.format;
e.language = o.language;
e.decimals = o.decimals;
memcpy(e.text, o.text, sizeof(e.text));
memcpy(e.suffix, o.suffix, sizeof(e.suffix));
memcpy(e.entity, o.entity, sizeof(e.entity));
memcpy(e.attribute, o.attribute, sizeof(e.attribute));
e.factor = o.factor;
e.scroll_width = o.scroll_width;
e.scroll_speed = o.scroll_speed;
e.target = o.target;
}
}
} // namespace v2
// Version 4 (before scroll speeds below 1): scroll_speed was a whole number (uint8_t).
// Frozen copy, converted when loaded. Version 3 is converted through it.
namespace v4
{
struct Element
{
ElementType type;
int16_t x, y;
Align align;
Overlay overlay;
char font[PAGE_NAME_SIZE];
uint8_t format;
Language language;
int8_t decimals;
char text[ELEMENT_TEXT_SIZE];
char suffix[ELEMENT_SUFFIX_SIZE];
char entity[ELEMENT_ENTITY_SIZE];
char attribute[ELEMENT_ATTRIBUTE_SIZE];
float factor;
uint8_t scroll_width;
uint8_t scroll_speed;
uint32_t target;
HaShow show;
uint8_t input_unit;
bool auto_unit;
bool absolute;
char map[ELEMENT_MAP_SIZE];
uint8_t bar_width = 40;
uint8_t bar_height = 3;
float bar_max = 100;
bool bar_frame = true;
};
struct Page
{
uint8_t slot_count;
PageSlot slots[PAGE_SLOTS];
uint8_t element_count;
Element elements[OLD_PAGE_ELEMENTS];
};
struct Stored
{
uint16_t version;
uint16_t size;
Page page;
};
// Up to scroll_speed the layout is the same, so that part is copied as a block
static const size_t BEFORE_SPEED = offsetof(Element, scroll_speed);
static_assert(offsetof(PageElement, scroll_speed) >= BEFORE_SPEED &&
offsetof(PageElement, scroll_width) == offsetof(Element, scroll_width),
"same layout up to scroll_width");
static void convert(const Page &old, DynamicPage &page)
{
clearPage(page);
page.slot_count = old.slot_count;
memcpy(page.slots, old.slots, sizeof(page.slots));
page.element_count = old.element_count;
for (int i = 0; i < OLD_PAGE_ELEMENTS; i++)
{
const Element &o = old.elements[i];
PageElement &e = page.elements[i];
memcpy((void *)&e, &o, BEFORE_SPEED);
e.scroll_speed = o.scroll_speed;
e.target = o.target;
e.show = o.show;
e.input_unit = o.input_unit;
e.auto_unit = o.auto_unit;
e.absolute = o.absolute;
memcpy(e.map, o.map, sizeof(e.map));
e.bar_width = o.bar_width;
e.bar_height = o.bar_height;
e.bar_max = o.bar_max;
e.bar_frame = o.bar_frame;
}
}
} // namespace v4
// Version 3 (before the progress bar): the version 4 elements without the fields from
// `bar_width` on. The rest has the same layout, so it is copied as a block, then
// converted like version 4.
namespace v3
{
struct Element
{
ElementType type;
int16_t x, y;
Align align;
Overlay overlay;
char font[PAGE_NAME_SIZE];
uint8_t format;
Language language;
int8_t decimals;
char text[ELEMENT_TEXT_SIZE];
char suffix[ELEMENT_SUFFIX_SIZE];
char entity[ELEMENT_ENTITY_SIZE];
char attribute[ELEMENT_ATTRIBUTE_SIZE];
float factor;
uint8_t scroll_width;
uint8_t scroll_speed;
uint32_t target;
HaShow show;
uint8_t input_unit;
bool auto_unit;
bool absolute;
char map[ELEMENT_MAP_SIZE];
};
struct Page
{
uint8_t slot_count;
PageSlot slots[PAGE_SLOTS];
uint8_t element_count;
Element elements[OLD_PAGE_ELEMENTS];
};
struct Stored
{
uint16_t version;
uint16_t size;
Page page;
};
static const size_t COMMON = offsetof(Element, map) + sizeof(Element::map);
static_assert(offsetof(v4::Element, map) == offsetof(Element, map), "same layout up to map");
static void convert(const Page &old, DynamicPage &page)
{
v4::Page *mid = new v4::Page; // bar fields at their defaults
mid->slot_count = old.slot_count;
memcpy(mid->slots, old.slots, sizeof(mid->slots));
mid->element_count = old.element_count;
for (int i = 0; i < OLD_PAGE_ELEMENTS; i++)
memcpy((void *)&mid->elements[i], &old.elements[i], COMMON);
v4::convert(*mid, page);
delete mid;
}
} // namespace v3
// Version 5 (before 16 elements and the day offset of dates): 8 elements of the current
// layout without day_offset. Frozen copy, the elements are copied as a block.
namespace v5
{
struct Element
{
ElementType type;
int16_t x, y;
Align align;
Overlay overlay;
char font[PAGE_NAME_SIZE];
uint8_t format;
Language language;
int8_t decimals;
char text[ELEMENT_TEXT_SIZE];
char suffix[ELEMENT_SUFFIX_SIZE];
char entity[ELEMENT_ENTITY_SIZE];
char attribute[ELEMENT_ATTRIBUTE_SIZE];
float factor;
uint8_t scroll_width;
float scroll_speed;
uint32_t target;
HaShow show;
uint8_t input_unit;
bool auto_unit;
bool absolute;
char map[ELEMENT_MAP_SIZE];
uint8_t bar_width;
uint8_t bar_height;
float bar_max;
bool bar_frame;
};
struct Page
{
uint8_t slot_count;
PageSlot slots[PAGE_SLOTS];
uint8_t element_count;
Element elements[OLD_PAGE_ELEMENTS];
};
struct Stored
{
uint16_t version;
uint16_t size;
Page page;
};
static const size_t COMMON = offsetof(Element, bar_frame) + sizeof(Element::bar_frame);
static_assert(offsetof(PageElement, bar_frame) == offsetof(Element, bar_frame) &&
offsetof(PageElement, map) == offsetof(Element, map),
"same layout up to bar_frame");
static void convert(const Page &old, DynamicPage &page)
{
clearPage(page); // day_offset 0
page.slot_count = old.slot_count;
memcpy(page.slots, old.slots, sizeof(page.slots));
page.element_count = old.element_count;
for (int i = 0; i < OLD_PAGE_ELEMENTS; i++)
memcpy((void *)&page.elements[i], &old.elements[i], COMMON);
}
} // namespace v5
// Version 6 (before the refresh effect per page): the current layout without effect.
// Frozen copy; the elements did not change since, so they are copied as a block.
namespace v6
{
struct Page
{
uint8_t slot_count;
PageSlot slots[PAGE_SLOTS];
uint8_t element_count;
PageElement elements[PAGE_ELEMENTS];
};
struct Stored
{
uint16_t version;
uint16_t size;
Page page;
};
static_assert(offsetof(DynamicPage, elements) == offsetof(Page, elements), "same layout up to the elements");
static void convert(const Page &old, DynamicPage &page)
{
clearPage(page); // the default effect
page.slot_count = old.slot_count;
memcpy(page.slots, old.slots, sizeof(page.slots));
page.element_count = old.element_count;
memcpy((void *)page.elements, old.elements, sizeof(page.elements));
}
} // namespace v6
static_assert(sizeof(v1::Stored) != sizeof(v2::Stored) && sizeof(v2::Stored) != sizeof(v3::Stored) &&
sizeof(v3::Stored) != sizeof(v4::Stored) && sizeof(v4::Stored) != sizeof(v5::Stored) &&
sizeof(v5::Stored) != sizeof(v6::Stored) && sizeof(v6::Stored) != sizeof(StoredPage),
"the versions are told apart by their size");
static uint32_t revision = 0;
static String path(const char *name)
@@ -460,99 +86,10 @@ bool loadPage(const char *name, DynamicPage &page)
page = stored->page;
delete stored;
}
else if (size == sizeof(v6::Stored))
{
v6::Stored *stored = new v6::Stored;
ok = file.read((uint8_t *)stored, sizeof(v6::Stored)) == sizeof(v6::Stored) && stored->version == 6 &&
stored->size == sizeof(v6::Page);
if (ok)
{
v6::convert(stored->page, page);
ok = validatePage(page) == nullptr;
}
delete stored;
}
else if (size == sizeof(v5::Stored))
{
v5::Stored *stored = new v5::Stored;
ok = file.read((uint8_t *)stored, sizeof(v5::Stored)) == sizeof(v5::Stored) && stored->version == 5 &&
stored->size == sizeof(v5::Page);
if (ok)
{
v5::convert(stored->page, page);
ok = validatePage(page) == nullptr;
}
delete stored;
}
else if (size == sizeof(v4::Stored))
{
v4::Stored *stored = new v4::Stored;
ok = file.read((uint8_t *)stored, sizeof(v4::Stored)) == sizeof(v4::Stored) && stored->version == 4 &&
stored->size == sizeof(v4::Page);
if (ok)
{
v4::convert(stored->page, page);
ok = validatePage(page) == nullptr;
}
delete stored;
}
else if (size == sizeof(v3::Stored))
{
v3::Stored *stored = new v3::Stored;
ok = file.read((uint8_t *)stored, sizeof(v3::Stored)) == sizeof(v3::Stored) && stored->version == 3 &&
stored->size == sizeof(v3::Page);
if (ok)
{
v3::convert(stored->page, page);
ok = validatePage(page) == nullptr;
}
delete stored;
}
else if (size == sizeof(v2::Stored))
{
v2::Stored *stored = new v2::Stored;
ok = file.read((uint8_t *)stored, sizeof(v2::Stored)) == sizeof(v2::Stored) && stored->version == 2 &&
stored->size == sizeof(v2::Page);
if (ok)
{
v2::convert(stored->page, page);
ok = validatePage(page) == nullptr;
}
delete stored;
}
else if (size == sizeof(v1::Stored))
{
v1::Stored *stored = new v1::Stored;
ok = file.read((uint8_t *)stored, sizeof(v1::Stored)) == sizeof(v1::Stored) && stored->version == 1 &&
stored->size == sizeof(v1::Page);
if (ok)
{
v1::convert(stored->page, page);
ok = validatePage(page) == nullptr;
}
delete stored;
}
file.close();
return ok || builtinPage(name, page);
}
std::vector<String> storedPageNames()
{
return storedNames(DIR, EXTENSION);
}
bool pageInOldFormat(const char *name)
{
if (!storedPage(name))
return false;
File file = LittleFS.open(path(name), "r");
if (!file)
return false;
bool old = file.size() != sizeof(StoredPage);
file.close();
return old;
}
const char *savePage(const char *name, const DynamicPage &page)
{
if (!validBitmapName(name))
-4
View File
@@ -31,10 +31,6 @@ bool pageExists(const char *name);
// A built-in page that no stored page replaces
bool builtinPageExists(const char *name);
bool loadPage(const char *name, DynamicPage &page);
// The stored pages (slow: lists the directory) and whether one is stored in an older
// format (loadPage() converts it, savePage() writes the current one)
std::vector<String> storedPageNames();
bool pageInOldFormat(const char *name);
// Return nullptr on success, otherwise a message for the user. savePage() validates the
// page, but does not check that its bitmaps and fonts exist.
-6
View File
@@ -85,12 +85,6 @@ void loop()
homeAssistantLoop();
webUiLoop();
appLoop();
// Once the firmware can't be rolled back anymore (the old one doesn't know icons)
if (!firmwareGuardPending())
{
appMigrateIcons();
appMigrateData();
}
// Home Assistant values of the shown page (read by a task, never blocks)
if (haApiLoop(wifiConnected()))
controller.pageChanged();
+4 -156
View File
@@ -5,7 +5,9 @@
static const char *NAMESPACE = "flipdot";
static const char *KEY = "schedule";
// Increase when PageSchedule changes; older records are ignored unless converted below
// Increase when PageSchedule changes; records of other versions are ignored (an empty
// schedule). 2.15.0 stored all older records again in version 4. To keep the schedule over
// a change, add a frozen copy of the old layout and convert it (see git history).
static const uint16_t VERSION = 4;
struct StoredSchedule
@@ -15,128 +17,9 @@ struct StoredSchedule
PageSchedule schedule;
};
// Version 1: the rules had no blocks (they were always active). Frozen copy, converted.
namespace v1
{
struct Rule
{
char entity[ELEMENT_ENTITY_SIZE];
char attribute[ELEMENT_ATTRIBUTE_SIZE];
Compare compare;
char value[RULE_VALUE_SIZE];
char page[PAGE_NAME_SIZE];
TriggerMode mode;
uint16_t seconds;
uint16_t interval;
};
struct Schedule
{
bool enabled;
uint8_t block_count;
ScheduleBlock blocks[SCHEDULE_BLOCKS];
uint8_t rule_count;
Rule rules[TRIGGER_RULES];
};
struct Stored
{
uint16_t version;
uint16_t size;
Schedule schedule;
};
static void convert(const Schedule &old, PageSchedule &s)
{
s = PageSchedule();
s.enabled = old.enabled;
s.block_count = old.block_count;
memcpy(s.blocks, old.blocks, sizeof(s.blocks));
s.rule_count = old.rule_count; // over_manual: the default (true), like before
for (int i = 0; i < TRIGGER_RULES; i++)
{
const Rule &o = old.rules[i];
TriggerRule &r = s.rules[i];
memcpy(r.entity, o.entity, sizeof(r.entity));
memcpy(r.attribute, o.attribute, sizeof(r.attribute));
r.compare = o.compare;
memcpy(r.value, o.value, sizeof(r.value));
memcpy(r.page, o.page, sizeof(r.page));
r.mode = o.mode;
r.seconds = o.seconds;
r.interval = o.interval;
}
}
} // namespace v1
// Versions 2 and 3: the rules had no over_slow_mode. Version 2 had the same layout as 3, but
// over_manual was padding (set to true). Frozen copy, converted.
namespace v3
{
struct Rule
{
char entity[ELEMENT_ENTITY_SIZE];
char attribute[ELEMENT_ATTRIBUTE_SIZE];
Compare compare;
char value[RULE_VALUE_SIZE];
char page[PAGE_NAME_SIZE];
TriggerMode mode;
uint16_t seconds;
uint16_t interval;
uint8_t blocks;
bool over_manual;
};
static_assert(sizeof(Rule) == 164, "the frozen layout of versions 2 and 3");
struct Schedule
{
bool enabled;
uint8_t block_count;
ScheduleBlock blocks[SCHEDULE_BLOCKS];
uint8_t rule_count;
Rule rules[TRIGGER_RULES];
};
struct Stored
{
uint16_t version;
uint16_t size;
Schedule schedule;
};
static void convert(const Schedule &old, uint16_t version, PageSchedule &s)
{
s = PageSchedule();
s.enabled = old.enabled;
s.block_count = old.block_count;
memcpy(s.blocks, old.blocks, sizeof(s.blocks));
s.rule_count = old.rule_count;
for (int i = 0; i < TRIGGER_RULES; i++)
{
const Rule &o = old.rules[i];
TriggerRule &r = s.rules[i];
memcpy(r.entity, o.entity, sizeof(r.entity));
memcpy(r.attribute, o.attribute, sizeof(r.attribute));
r.compare = o.compare;
memcpy(r.value, o.value, sizeof(r.value));
memcpy(r.page, o.page, sizeof(r.page));
r.mode = o.mode;
r.seconds = o.seconds;
r.interval = o.interval;
r.blocks = o.blocks;
r.over_manual = version == 2 ? true : o.over_manual; // over_slow_mode: false
}
}
} // namespace v3
static_assert(sizeof(v1::Stored) != sizeof(StoredSchedule) && sizeof(v3::Stored) != sizeof(StoredSchedule) &&
sizeof(v1::Stored) != sizeof(v3::Stored),
"the versions are told apart by their size");
bool loadSchedule(PageSchedule &schedule)
void loadSchedule(PageSchedule &schedule)
{
schedule = PageSchedule();
bool converted = false;
Preferences prefs;
prefs.begin(NAMESPACE, true);
size_t length = prefs.isKey(KEY) ? prefs.getBytesLength(KEY) : 0;
@@ -149,42 +32,7 @@ bool loadSchedule(PageSchedule &schedule)
schedule = stored->schedule;
delete stored;
}
else if (length == sizeof(v3::Stored))
{
v3::Stored *stored = new v3::Stored;
PageSchedule *page = new PageSchedule;
if (prefs.getBytes(KEY, stored, sizeof(v3::Stored)) == sizeof(v3::Stored) &&
(stored->version == 2 || stored->version == 3) && stored->size == sizeof(v3::Schedule))
{
v3::convert(stored->schedule, stored->version, *page);
if (validateSchedule(*page) == nullptr)
{
schedule = *page;
converted = true;
}
}
delete page;
delete stored;
}
else if (length == sizeof(v1::Stored))
{
v1::Stored *stored = new v1::Stored;
PageSchedule *page = new PageSchedule;
if (prefs.getBytes(KEY, stored, sizeof(v1::Stored)) == sizeof(v1::Stored) && stored->version == 1 &&
stored->size == sizeof(v1::Schedule))
{
v1::convert(stored->schedule, *page);
if (validateSchedule(*page) == nullptr)
{
schedule = *page;
converted = true;
}
}
delete page;
delete stored;
}
prefs.end();
return converted;
}
void saveSchedule(const PageSchedule &schedule)
+2 -3
View File
@@ -3,7 +3,6 @@
#include "logic/page_schedule.h"
// The page schedule and trigger rules in NVS (namespace "flipdot", key "schedule"). A
// missing, unknown or invalid record gives an empty schedule. Returns true when the record
// had an older layout and was converted (save it again to keep it).
bool loadSchedule(PageSchedule &schedule);
// missing, outdated or invalid record gives an empty schedule.
void loadSchedule(PageSchedule &schedule);
void saveSchedule(const PageSchedule &schedule);
+9 -173
View File
@@ -6,155 +6,19 @@
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.
// Increase when the Settings struct changes; records of other versions are ignored
// (defaults). 2.15.0 stored all older records again in version 7. To keep the settings over
// a change, add a frozen copy of the old layout and convert it (see git history).
static const uint16_t VERSION = 7;
struct OldVersion
{
uint16_t version;
size_t fields; // bytes of the settings struct that version had
};
// Versions 4-6: the layout before slow mode (version 7 removed the late-evening quarter
// hours of the schedule and added slow_mode). Frozen copy; versions 4 and 5 lacked refresh
// and plex_user (added at the end, so they are a prefix of it).
namespace v6
{
struct ScheduleSettings
{
bool enabled = true;
uint16_t day_start = 8 * 60;
uint16_t late_start = 22 * 60;
bool quarter_hours = true;
uint16_t night_start = 23 * 60 + 31;
};
struct Settings
{
char page[PAGE_NAME_SIZE] = DEFAULT_PAGE;
ScheduleSettings schedule;
ClockSettings clock;
HomeAssistantSettings home_assistant;
uint16_t flip_time = 225;
bool status_led = true;
char off_page[PAGE_NAME_SIZE] = OFF_PAGE_BLANK;
DriveSettings drive;
RefreshSettings refresh;
char plex_user[PLEX_USER_SIZE] = "";
};
static const OldVersion VERSIONS[] = {
{4, offsetof(Settings, refresh)},
{5, offsetof(Settings, plex_user)},
{6, sizeof(Settings)},
};
static void convert(const Settings &old, ::Settings &s)
{
s = ::Settings();
memcpy(s.page, old.page, sizeof(s.page));
s.schedule.enabled = old.schedule.enabled;
s.schedule.day_start = old.schedule.day_start;
s.schedule.night_start = old.schedule.night_start;
s.clock = old.clock;
s.home_assistant = old.home_assistant;
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;
s.refresh = old.refresh;
memcpy(s.plex_user, old.plex_user, sizeof(s.plex_user));
// The quarter hours are replaced by slow mode, which starts off
}
} // namespace v6
// 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";
v6::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, v6::Settings &s)
{
s = v6::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 constexpr size_t maxSize(size_t a, size_t b)
{
return a > b ? a : b;
}
// Big enough for every layout (an old record can be longer than the current one)
static const size_t RECORD_SIZE = maxSize(sizeof(Settings), maxSize(sizeof(v6::Settings), sizeof(v3::Settings)));
struct StoredSettings
{
uint16_t version;
uint16_t size;
alignas(4) uint8_t settings[RECORD_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;
}
bool loadSettings(Settings &settings)
void loadSettings(Settings &settings)
{
settings = Settings();
StoredSettings stored;
@@ -164,37 +28,9 @@ bool loadSettings(Settings &settings)
size_t read = prefs.getBytes(KEY, &stored, sizeof(stored));
prefs.end();
Settings loaded;
bool converted = false;
if (stored.version == VERSION)
{
if (stored.size != sizeof(Settings) || !readOld(stored, read, sizeof(Settings), loaded))
return false;
}
else
{
bool ok = false;
v6::Settings old;
for (const OldVersion &o : v6::VERSIONS)
if (o.version == stored.version)
ok = readOld(stored, read, o.fields, old);
for (const OldVersion &o : v3::VERSIONS)
if (o.version == stored.version)
{
v3::Settings older;
ok = readOld(stored, read, o.fields, older);
if (ok)
v3::convert(older, old);
}
if (!ok)
return false;
v6::convert(old, loaded);
converted = true;
}
if (validateSettings(loaded) != nullptr)
return false;
settings = loaded;
return converted;
if (read == offsetof(StoredSettings, settings) + sizeof(Settings) && stored.version == VERSION &&
stored.size == sizeof(Settings) && validateSettings(stored.settings) == nullptr)
settings = stored.settings;
}
void saveSettings(const Settings &settings)
@@ -202,7 +38,7 @@ void saveSettings(const Settings &settings)
StoredSettings stored;
stored.version = VERSION;
stored.size = sizeof(Settings);
memcpy(stored.settings, (const void *)&settings, sizeof(Settings));
stored.settings = settings;
Preferences prefs;
prefs.begin(NAMESPACE, false);
prefs.putBytes(KEY, &stored, offsetof(StoredSettings, settings) + sizeof(Settings));
+2 -3
View File
@@ -2,7 +2,6 @@
#include "logic/settings.h"
// The web UI settings in NVS. A missing or unknown record gives the defaults. Returns true
// when the record had an older layout and was converted (save it again to keep it).
bool loadSettings(Settings &settings);
// The web UI settings in NVS. A missing, outdated or invalid record gives the defaults.
void loadSettings(Settings &settings);
void saveSettings(const Settings &settings);
-1
View File
@@ -168,7 +168,6 @@ static void handleStatus()
doc["off_page"] = appSettings().off_page;
doc["power_enabled"] = controller.powerEnabled();
doc["slow_mode"] = controller.slowModeActive();
doc["old_data_left"] = appOldDataLeft();
JsonObject sw = doc["stopwatch"].to<JsonObject>();
sw["running"] = stopwatch.running();
sw["ms"] = stopwatch.elapsed(millis());