Public Access
Home Assistant requests in a task, progress bar, Plex page, bitmap cache
- net/ha_api: the HTTP requests run in a FreeRTOS task on core 0; loop()
only hands over copies through queues. The web UI's test button polls.
No more 3-4 s loop() stalls once a minute; timeout raised to 5 s.
- Progress bar page element (page format 4, v3 pages are converted)
- Plex user setting (settings v6): {plex_user} in entity ids
- Plex page (examples): watchers, play/pause icon, title, progress
- Bitmaps cached in RAM: a LittleFS read took ~100 ms on every render
and web preview; long JSON lists sent in 2 KB pieces
- Backup includes the refresh settings and the Plex user
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
090c2bf8ae
commit
490cefc360
42 files changed
+1074
-175
No files matched your search
@@ -1,6 +1,7 @@
|
||||
#include "display/bitmap_store.h"
|
||||
|
||||
#include <LittleFS.h>
|
||||
#include <map>
|
||||
#include <strings.h>
|
||||
|
||||
#include "display/bitmaps.h"
|
||||
@@ -22,6 +23,18 @@ static const BuiltinBitmap BUILTIN[] = {
|
||||
static bool mounted = false;
|
||||
static uint32_t revision = 0;
|
||||
|
||||
// Bitmaps read before, also the names that don't exist (images try a name per state, then
|
||||
// "default"). Reading one from LittleFS takes about 100 ms, and pages are drawn again for
|
||||
// every frame (scrolling, seconds) and for every preview in the web UI. Cleared when a
|
||||
// bitmap is saved or deleted, and when it holds CACHE_MAX names.
|
||||
struct CachedBitmap
|
||||
{
|
||||
bool exists;
|
||||
uint8_t data[BITMAP_BYTES];
|
||||
};
|
||||
static std::map<String, CachedBitmap> cache;
|
||||
static const size_t CACHE_MAX = 64;
|
||||
|
||||
static const BuiltinBitmap *builtin(const char *name)
|
||||
{
|
||||
for (const BuiltinBitmap &b : BUILTIN)
|
||||
@@ -99,7 +112,8 @@ bool bitmapExists(const char *name)
|
||||
{
|
||||
if (builtin(name))
|
||||
return true;
|
||||
return mounted && validBitmapName(name) && LittleFS.exists(path(name));
|
||||
uint8_t bitmap[BITMAP_BYTES];
|
||||
return loadBitmap(name, bitmap); // through the cache
|
||||
}
|
||||
|
||||
bool loadBitmap(const char *name, uint8_t *bitmap)
|
||||
@@ -110,14 +124,33 @@ bool loadBitmap(const char *name, uint8_t *bitmap)
|
||||
memcpy_P(bitmap, b->data, BITMAP_BYTES);
|
||||
return true;
|
||||
}
|
||||
if (!mounted || !validBitmapName(name))
|
||||
return false;
|
||||
auto cached = cache.find(name);
|
||||
if (cached != cache.end())
|
||||
{
|
||||
if (cached->second.exists)
|
||||
memcpy(bitmap, cached->second.data, BITMAP_BYTES);
|
||||
return cached->second.exists;
|
||||
}
|
||||
|
||||
// Checked first: open() logs an error for a missing file (images try names by state)
|
||||
if (!mounted || !validBitmapName(name) || !LittleFS.exists(path(name)))
|
||||
return false;
|
||||
File file = LittleFS.open(path(name), "r");
|
||||
if (!file)
|
||||
return false;
|
||||
bool ok = file.read(bitmap, BITMAP_BYTES) == (size_t)BITMAP_BYTES;
|
||||
file.close();
|
||||
bool ok = false;
|
||||
if (LittleFS.exists(path(name)))
|
||||
{
|
||||
File file = LittleFS.open(path(name), "r");
|
||||
if (file)
|
||||
{
|
||||
ok = file.read(bitmap, BITMAP_BYTES) == (size_t)BITMAP_BYTES;
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
if (cache.size() >= CACHE_MAX)
|
||||
cache.clear();
|
||||
CachedBitmap &entry = cache[name];
|
||||
entry.exists = ok;
|
||||
if (ok)
|
||||
memcpy(entry.data, bitmap, BITMAP_BYTES);
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -138,6 +171,7 @@ const char *saveBitmap(const char *name, const uint8_t *bitmap)
|
||||
bool ok = file.write(bitmap, BITMAP_BYTES) == (size_t)BITMAP_BYTES;
|
||||
file.close();
|
||||
revision++;
|
||||
cache.clear();
|
||||
return ok ? nullptr : "Storage full";
|
||||
}
|
||||
|
||||
@@ -150,5 +184,6 @@ const char *deleteBitmap(const char *name)
|
||||
if (!LittleFS.remove(path(name)))
|
||||
return "Could not delete the file";
|
||||
revision++;
|
||||
cache.clear();
|
||||
return nullptr;
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
static const char *DIR = "/pages";
|
||||
static const char *EXTENSION = ".pg";
|
||||
// Increase when DynamicPage changes; older files are ignored unless converted below
|
||||
static const uint16_t VERSION = 3;
|
||||
static const uint16_t VERSION = 4;
|
||||
|
||||
struct StoredPage
|
||||
{
|
||||
@@ -149,7 +149,66 @@ static void convert(const Page &old, DynamicPage &page)
|
||||
}
|
||||
} // namespace v2
|
||||
|
||||
static_assert(sizeof(v1::Stored) != sizeof(v2::Stored) && sizeof(v2::Stored) != sizeof(StoredPage),
|
||||
// Version 3 (before the progress bar): the elements lacked the fields from `bar_width` on.
|
||||
// The rest has the same layout, so it is copied as a block.
|
||||
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[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(PageElement, map) == offsetof(Element, map), "same layout up to map");
|
||||
|
||||
static void convert(const Page &old, DynamicPage &page)
|
||||
{
|
||||
page = DynamicPage();
|
||||
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 < PAGE_ELEMENTS; i++)
|
||||
memcpy((void *)&page.elements[i], &old.elements[i], COMMON); // the bar fields keep their defaults
|
||||
}
|
||||
} // namespace v3
|
||||
|
||||
static_assert(sizeof(v1::Stored) != sizeof(v2::Stored) && sizeof(v2::Stored) != sizeof(v3::Stored) &&
|
||||
sizeof(v3::Stored) != sizeof(StoredPage),
|
||||
"the versions are told apart by their size");
|
||||
|
||||
static uint32_t revision = 0;
|
||||
@@ -210,6 +269,18 @@ bool loadPage(const char *name, DynamicPage &page)
|
||||
page = stored->page;
|
||||
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;
|
||||
|
||||
Reference in new issue
Block a user