Public Access
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:
1 parent
968492ca35
commit
9959c1aa74
14 files changed
+28
-996
No files matched your search
@@ -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/<name>.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/<name>.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`.
|
||||
|
||||
+2
-150
@@ -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);
|
||||
|
||||
@@ -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
@@ -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};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
@@ -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,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);
|
||||
@@ -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());
|
||||
|
||||
Reference in new issue
Block a user