diff --git a/CLAUDE.md b/CLAUDE.md index 8fc1968..e4f98df 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -152,7 +152,11 @@ See README.md for the overview. ESP32 only handles 418-character hex bitmaps (`logic/bitmap_format.h`, MSB = left dot). - Settings: `logic/settings.h` struct with defaults = original behaviour, stored as an NVS blob with a version (`system/settings_store.cpp`). When the struct changes, bump - `VERSION` there (old records then fall back to defaults). + `VERSION` there (old records then fall back to defaults). Only the current version is + read: 2.15.0 converted all old settings, schedules and pages on the device, and 2.15.1 + dropped the converters. To keep data over a format change, add a frozen copy of the old + layout with a converter (git history before 2.15.1 has examples), and store the + converted data only once the firmware is marked good (a rollback can't read it). - Bitmaps on LittleFS in the `spiffs` partition (`/bitmaps/.bin`, 209 bytes). Names are restricted (`validBitmapName`), which also makes them safe in paths and JSON. - Icons (`logic/icon_format.h`, `display/icon_store`, `/icons/.bin`: width, height, @@ -160,7 +164,6 @@ See README.md for the overview. the top left); they are never shown on their own and are not in the page lists or HA dropdowns. Bitmaps, icons and pages share one namespace. `PageResources::background()` tries bitmaps, then icons (expanded at 0,0), so the renderer doesn't know the difference. - `appMigrateIcons()` ran once (marker `/icons/.migrated`) after 2.10.0 was marked good. - LittleFS is slow: listing a directory (`listBitmaps()`, `listPages()`, `storedNames()`) takes about a second with 40 files. Never list per item in a loop. The first icon migration did that, hit the 30 s watchdog and crash-looped into the recovery app; work @@ -174,8 +177,7 @@ See README.md for the overview. old hardcoded clock (also the default and the HA "Clock" option). `pageStoreBegin()` stores a copy of each built-in page (`logic/builtin_pages`) at boot when none is stored, so it can be edited; the built-in one is the fallback, and deleting the copy resets it. -- Pages hold 16 elements (format 6). The old formats in `page_store.cpp` are frozen at - `OLD_PAGE_ELEMENTS` (8). A `DynamicPage` is about 4 KB: allocate it with `new`, and reset +- Pages hold 16 elements (format 7). A `DynamicPage` is about 4 KB: allocate it with `new`, and reset it with `clearPage()`, not `page = DynamicPage()` (a temporary on the 8 KB loop stack). - HA attributes may be paths into lists (`forecast.1.condition`, `net/ha_api`). The forecast comes from the template sensor in `docs/home-assistant.md`. diff --git a/src/esp32/app.cpp b/src/esp32/app.cpp index 50b62b0..f1d5d3b 100644 --- a/src/esp32/app.cpp +++ b/src/esp32/app.cpp @@ -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 migration_bitmaps; // uploaded bitmaps not shown on their own -static std::vector migration_used; -static std::vector 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 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); diff --git a/src/esp32/app.h b/src/esp32/app.h index 665ab78..c05df9d 100644 --- a/src/esp32/app.h +++ b/src/esp32/app.h @@ -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); diff --git a/src/esp32/config.h b/src/esp32/config.h index b65a274..31c8330 100644 --- a/src/esp32/config.h +++ b/src/esp32/config.h @@ -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}; diff --git a/src/esp32/display/icon_store.cpp b/src/esp32/display/icon_store.cpp index ab9e2bc..87044a3 100644 --- a/src/esp32/display/icon_store.cpp +++ b/src/esp32/display/icon_store.cpp @@ -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(); - } -} diff --git a/src/esp32/display/icon_store.h b/src/esp32/display/icon_store.h index e69e59e..99b6258 100644 --- a/src/esp32/display/icon_store.h +++ b/src/esp32/display/icon_store.h @@ -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(); diff --git a/src/esp32/display/page_store.cpp b/src/esp32/display/page_store.cpp index cee628a..fbbc17c 100644 --- a/src/esp32/display/page_store.cpp +++ b/src/esp32/display/page_store.cpp @@ -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 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)) diff --git a/src/esp32/display/page_store.h b/src/esp32/display/page_store.h index 7b79996..7208516 100644 --- a/src/esp32/display/page_store.h +++ b/src/esp32/display/page_store.h @@ -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 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. diff --git a/src/esp32/main.cpp b/src/esp32/main.cpp index 0304f98..26ef319 100644 --- a/src/esp32/main.cpp +++ b/src/esp32/main.cpp @@ -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(); diff --git a/src/esp32/system/schedule_store.cpp b/src/esp32/system/schedule_store.cpp index a278553..6f37ce1 100644 --- a/src/esp32/system/schedule_store.cpp +++ b/src/esp32/system/schedule_store.cpp @@ -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) diff --git a/src/esp32/system/schedule_store.h b/src/esp32/system/schedule_store.h index e88b307..de0614f 100644 --- a/src/esp32/system/schedule_store.h +++ b/src/esp32/system/schedule_store.h @@ -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); diff --git a/src/esp32/system/settings_store.cpp b/src/esp32/system/settings_store.cpp index 5064968..0e56563 100644 --- a/src/esp32/system/settings_store.cpp +++ b/src/esp32/system/settings_store.cpp @@ -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 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)); diff --git a/src/esp32/system/settings_store.h b/src/esp32/system/settings_store.h index 39d1212..fb3d3e9 100644 --- a/src/esp32/system/settings_store.h +++ b/src/esp32/system/settings_store.h @@ -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); diff --git a/src/esp32/web/web_ui.cpp b/src/esp32/web/web_ui.cpp index de9d6d4..6e717f2 100644 --- a/src/esp32/web/web_ui.cpp +++ b/src/esp32/web/web_ui.cpp @@ -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(); sw["running"] = stopwatch.running(); sw["ms"] = stopwatch.elapsed(millis());