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).
|
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
|
- 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
|
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
|
- 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.
|
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,
|
- 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
|
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()`
|
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.
|
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()`)
|
- 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
|
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
|
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()`
|
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,
|
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.
|
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
|
- Pages hold 16 elements (format 7). A `DynamicPage` is about 4 KB: allocate it with `new`, and reset
|
||||||
`OLD_PAGE_ELEMENTS` (8). 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).
|
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
|
- 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`.
|
forecast comes from the template sensor in `docs/home-assistant.md`.
|
||||||
|
|||||||
+2
-150
@@ -109,9 +109,6 @@ static void reloadPage()
|
|||||||
controller.pageChanged();
|
controller.pageChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool settings_converted = false;
|
|
||||||
static bool schedule_converted = false;
|
|
||||||
|
|
||||||
const Settings &appSettings()
|
const Settings &appSettings()
|
||||||
{
|
{
|
||||||
return settings;
|
return settings;
|
||||||
@@ -119,8 +116,8 @@ const Settings &appSettings()
|
|||||||
|
|
||||||
void appLoadSettings()
|
void appLoadSettings()
|
||||||
{
|
{
|
||||||
settings_converted = loadSettings(settings);
|
loadSettings(settings);
|
||||||
schedule_converted = loadSchedule(page_schedule);
|
loadSchedule(page_schedule);
|
||||||
selector.setSchedule(&page_schedule);
|
selector.setSchedule(&page_schedule);
|
||||||
selector.setChosen(settings.page);
|
selector.setChosen(settings.page);
|
||||||
haApiWatchRules(&page_schedule);
|
haApiWatchRules(&page_schedule);
|
||||||
@@ -470,151 +467,6 @@ const char *appIconToBitmap(const char *name)
|
|||||||
return nullptr;
|
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 *appSaveFont(const char *name, const uint8_t *data, size_t size)
|
||||||
{
|
{
|
||||||
const char *error = saveFont(name, data, 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
|
// the other way around; not for bitmaps that are shown on their own
|
||||||
const char *appBitmapToIcon(const char *name);
|
const char *appBitmapToIcon(const char *name);
|
||||||
const char *appIconToBitmap(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 *appSavePage(const char *name, const DynamicPage &page);
|
||||||
const char *appDeletePage(const char *name);
|
const char *appDeletePage(const char *name);
|
||||||
const char *appSaveFont(const char *name, const uint8_t *data, size_t size);
|
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_CONNECT_TIMEOUT = 30000;
|
||||||
const uint32_t WIFI_RESTART_TIME = 10UL * 60 * 1000;
|
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)
|
// 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};
|
const uint8_t HA_DEVICE_ID[] = {0x34, 0x86, 0x5d, 0x3a, 0x08, 0x28};
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
|
|
||||||
static const char *DIR = "/icons";
|
static const char *DIR = "/icons";
|
||||||
// Written when the bitmaps were moved (not a valid name, so never listed)
|
// 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
|
// 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
|
// be edited, and the fallback when they are not stored
|
||||||
@@ -197,20 +196,3 @@ const char *deleteIcon(const char *name)
|
|||||||
cache.clear();
|
cache.clear();
|
||||||
return nullptr;
|
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 *saveIcon(const char *name, const Icon &icon);
|
||||||
const char *deleteIcon(const char *name);
|
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 *DIR = "/pages";
|
||||||
static const char *EXTENSION = ".pg";
|
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;
|
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
|
struct StoredPage
|
||||||
{
|
{
|
||||||
@@ -19,379 +18,6 @@ struct StoredPage
|
|||||||
DynamicPage page;
|
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 uint32_t revision = 0;
|
||||||
|
|
||||||
static String path(const char *name)
|
static String path(const char *name)
|
||||||
@@ -460,99 +86,10 @@ bool loadPage(const char *name, DynamicPage &page)
|
|||||||
page = stored->page;
|
page = stored->page;
|
||||||
delete stored;
|
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();
|
file.close();
|
||||||
return ok || builtinPage(name, page);
|
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)
|
const char *savePage(const char *name, const DynamicPage &page)
|
||||||
{
|
{
|
||||||
if (!validBitmapName(name))
|
if (!validBitmapName(name))
|
||||||
|
|||||||
@@ -31,10 +31,6 @@ bool pageExists(const char *name);
|
|||||||
// A built-in page that no stored page replaces
|
// A built-in page that no stored page replaces
|
||||||
bool builtinPageExists(const char *name);
|
bool builtinPageExists(const char *name);
|
||||||
bool loadPage(const char *name, DynamicPage &page);
|
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
|
// Return nullptr on success, otherwise a message for the user. savePage() validates the
|
||||||
// page, but does not check that its bitmaps and fonts exist.
|
// page, but does not check that its bitmaps and fonts exist.
|
||||||
|
|||||||
@@ -85,12 +85,6 @@ void loop()
|
|||||||
homeAssistantLoop();
|
homeAssistantLoop();
|
||||||
webUiLoop();
|
webUiLoop();
|
||||||
appLoop();
|
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)
|
// Home Assistant values of the shown page (read by a task, never blocks)
|
||||||
if (haApiLoop(wifiConnected()))
|
if (haApiLoop(wifiConnected()))
|
||||||
controller.pageChanged();
|
controller.pageChanged();
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
|
|
||||||
static const char *NAMESPACE = "flipdot";
|
static const char *NAMESPACE = "flipdot";
|
||||||
static const char *KEY = "schedule";
|
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;
|
static const uint16_t VERSION = 4;
|
||||||
|
|
||||||
struct StoredSchedule
|
struct StoredSchedule
|
||||||
@@ -15,128 +17,9 @@ struct StoredSchedule
|
|||||||
PageSchedule schedule;
|
PageSchedule schedule;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Version 1: the rules had no blocks (they were always active). Frozen copy, converted.
|
void loadSchedule(PageSchedule &schedule)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
schedule = PageSchedule();
|
schedule = PageSchedule();
|
||||||
bool converted = false;
|
|
||||||
Preferences prefs;
|
Preferences prefs;
|
||||||
prefs.begin(NAMESPACE, true);
|
prefs.begin(NAMESPACE, true);
|
||||||
size_t length = prefs.isKey(KEY) ? prefs.getBytesLength(KEY) : 0;
|
size_t length = prefs.isKey(KEY) ? prefs.getBytesLength(KEY) : 0;
|
||||||
@@ -149,42 +32,7 @@ bool loadSchedule(PageSchedule &schedule)
|
|||||||
schedule = stored->schedule;
|
schedule = stored->schedule;
|
||||||
delete stored;
|
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();
|
prefs.end();
|
||||||
return converted;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveSchedule(const PageSchedule &schedule)
|
void saveSchedule(const PageSchedule &schedule)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
#include "logic/page_schedule.h"
|
#include "logic/page_schedule.h"
|
||||||
|
|
||||||
// The page schedule and trigger rules in NVS (namespace "flipdot", key "schedule"). A
|
// 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
|
// missing, outdated or invalid record gives an empty schedule.
|
||||||
// had an older layout and was converted (save it again to keep it).
|
void loadSchedule(PageSchedule &schedule);
|
||||||
bool loadSchedule(PageSchedule &schedule);
|
|
||||||
void saveSchedule(const PageSchedule &schedule);
|
void saveSchedule(const PageSchedule &schedule);
|
||||||
@@ -6,155 +6,19 @@
|
|||||||
|
|
||||||
static const char *NAMESPACE = "flipdot";
|
static const char *NAMESPACE = "flipdot";
|
||||||
static const char *KEY = "settings";
|
static const char *KEY = "settings";
|
||||||
// Increase when the Settings struct changes; old records are then ignored (defaults),
|
// Increase when the Settings struct changes; records of other versions are ignored
|
||||||
// unless the code below says how to read them. When fields are only added at the end,
|
// (defaults). 2.15.0 stored all older records again in version 7. To keep the settings over
|
||||||
// an old record is the start of the new layout: add an OldVersion for it like v3 does.
|
// a change, add a frozen copy of the old layout and convert it (see git history).
|
||||||
static const uint16_t VERSION = 7;
|
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
|
struct StoredSettings
|
||||||
{
|
{
|
||||||
uint16_t version;
|
uint16_t version;
|
||||||
uint16_t size;
|
uint16_t size;
|
||||||
alignas(4) uint8_t settings[RECORD_SIZE];
|
Settings settings;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reads a record whose first `fields` bytes have the layout of T
|
void loadSettings(Settings &settings)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
settings = Settings();
|
settings = Settings();
|
||||||
StoredSettings stored;
|
StoredSettings stored;
|
||||||
@@ -164,37 +28,9 @@ bool loadSettings(Settings &settings)
|
|||||||
size_t read = prefs.getBytes(KEY, &stored, sizeof(stored));
|
size_t read = prefs.getBytes(KEY, &stored, sizeof(stored));
|
||||||
prefs.end();
|
prefs.end();
|
||||||
|
|
||||||
Settings loaded;
|
if (read == offsetof(StoredSettings, settings) + sizeof(Settings) && stored.version == VERSION &&
|
||||||
bool converted = false;
|
stored.size == sizeof(Settings) && validateSettings(stored.settings) == nullptr)
|
||||||
if (stored.version == VERSION)
|
settings = stored.settings;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveSettings(const Settings &settings)
|
void saveSettings(const Settings &settings)
|
||||||
@@ -202,7 +38,7 @@ void saveSettings(const Settings &settings)
|
|||||||
StoredSettings stored;
|
StoredSettings stored;
|
||||||
stored.version = VERSION;
|
stored.version = VERSION;
|
||||||
stored.size = sizeof(Settings);
|
stored.size = sizeof(Settings);
|
||||||
memcpy(stored.settings, (const void *)&settings, sizeof(Settings));
|
stored.settings = settings;
|
||||||
Preferences prefs;
|
Preferences prefs;
|
||||||
prefs.begin(NAMESPACE, false);
|
prefs.begin(NAMESPACE, false);
|
||||||
prefs.putBytes(KEY, &stored, offsetof(StoredSettings, settings) + sizeof(Settings));
|
prefs.putBytes(KEY, &stored, offsetof(StoredSettings, settings) + sizeof(Settings));
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#include "logic/settings.h"
|
#include "logic/settings.h"
|
||||||
|
|
||||||
// The web UI settings in NVS. A missing or unknown record gives the defaults. Returns true
|
// The web UI settings in NVS. A missing, outdated or invalid record gives the defaults.
|
||||||
// when the record had an older layout and was converted (save it again to keep it).
|
void loadSettings(Settings &settings);
|
||||||
bool loadSettings(Settings &settings);
|
|
||||||
void saveSettings(const Settings &settings);
|
void saveSettings(const Settings &settings);
|
||||||
@@ -168,7 +168,6 @@ static void handleStatus()
|
|||||||
doc["off_page"] = appSettings().off_page;
|
doc["off_page"] = appSettings().off_page;
|
||||||
doc["power_enabled"] = controller.powerEnabled();
|
doc["power_enabled"] = controller.powerEnabled();
|
||||||
doc["slow_mode"] = controller.slowModeActive();
|
doc["slow_mode"] = controller.slowModeActive();
|
||||||
doc["old_data_left"] = appOldDataLeft();
|
|
||||||
JsonObject sw = doc["stopwatch"].to<JsonObject>();
|
JsonObject sw = doc["stopwatch"].to<JsonObject>();
|
||||||
sw["running"] = stopwatch.running();
|
sw["running"] = stopwatch.running();
|
||||||
sw["ms"] = stopwatch.elapsed(millis());
|
sw["ms"] = stopwatch.elapsed(millis());
|
||||||
|
|||||||
Reference in new issue
Block a user