Public Access
Icons: small images of their own size for pages
Bitmaps that only pages use (backgrounds, images by state) are now icons: own width and height, stored in /icons, not in the page lists or the Home Assistant dropdowns. Page backgrounds and image elements use icons or bitmaps; the renderer gets icons expanded at the top left, so pages look the same. Web UI: Icons tab with an editor, Make icon / Make bitmap, Save as icon on the Draw tab, icons in the backup. A one-time migration in time-boxed steps moves the uploaded bitmaps that only pages use, after the new firmware is marked good. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1704cbf17b
commit
ab001e5d18
37 files changed
+955
-78
No files matched your search
@@ -51,7 +51,7 @@ static String path(const char *name)
|
||||
bool bitmapStoreBegin()
|
||||
{
|
||||
mounted = LittleFS.begin(true);
|
||||
for (const char *dir : {DIR, "/pages", "/fonts"})
|
||||
for (const char *dir : {DIR, "/pages", "/fonts", "/icons"})
|
||||
if (mounted && !LittleFS.exists(dir))
|
||||
LittleFS.mkdir(dir);
|
||||
Serial.printf("Bitmap storage %s\n", mounted ? "ready" : "not available (old flash layout?)");
|
||||
@@ -108,6 +108,11 @@ uint32_t bitmapListRevision()
|
||||
return revision;
|
||||
}
|
||||
|
||||
bool builtinBitmap(const char *name)
|
||||
{
|
||||
return builtin(name) != nullptr;
|
||||
}
|
||||
|
||||
bool bitmapExists(const char *name)
|
||||
{
|
||||
if (builtin(name))
|
||||
|
||||
@@ -31,6 +31,8 @@ std::vector<BitmapInfo> listBitmaps();
|
||||
// Changes each time a bitmap is saved or deleted
|
||||
uint32_t bitmapListRevision();
|
||||
bool bitmapExists(const char *name);
|
||||
// Without reading the storage
|
||||
bool builtinBitmap(const char *name);
|
||||
bool loadBitmap(const char *name, uint8_t *bitmap);
|
||||
|
||||
// Return nullptr on success, otherwise a message for the user
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
#include "display/icon_store.h"
|
||||
|
||||
#include <LittleFS.h>
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <strings.h>
|
||||
|
||||
#include "display/bitmap_store.h"
|
||||
|
||||
static const char *DIR = "/icons";
|
||||
// Written when the bitmaps were moved (not a valid name, so never listed)
|
||||
static const char *MIGRATED = "/icons/.migrated";
|
||||
|
||||
static uint32_t revision = 0;
|
||||
// Counted once (listing the directory is slow), then kept up to date; -1: not yet
|
||||
static int count = -1;
|
||||
|
||||
// Like the bitmaps (display/bitmap_store.cpp): images try a name per state, and pages are
|
||||
// drawn for every frame and preview, so icons and missing names are kept in RAM
|
||||
struct CachedIcon
|
||||
{
|
||||
bool exists;
|
||||
Icon icon;
|
||||
};
|
||||
static std::map<String, CachedIcon> cache;
|
||||
static const size_t CACHE_MAX = 64;
|
||||
|
||||
static String path(const char *name)
|
||||
{
|
||||
return String(DIR) + "/" + name + ".bin";
|
||||
}
|
||||
|
||||
static bool ensureDir()
|
||||
{
|
||||
if (!bitmapStorageAvailable())
|
||||
return false;
|
||||
return LittleFS.exists(DIR) || LittleFS.mkdir(DIR);
|
||||
}
|
||||
|
||||
std::vector<String> listIcons()
|
||||
{
|
||||
std::vector<String> names = storedNames(DIR, ".bin");
|
||||
std::sort(names.begin(), names.end(),
|
||||
[](const String &a, const String &b) { return strcasecmp(a.c_str(), b.c_str()) < 0; });
|
||||
return names;
|
||||
}
|
||||
|
||||
uint32_t iconListRevision()
|
||||
{
|
||||
return revision;
|
||||
}
|
||||
|
||||
bool iconExists(const char *name)
|
||||
{
|
||||
Icon icon;
|
||||
return loadIcon(name, icon); // through the cache
|
||||
}
|
||||
|
||||
bool loadIcon(const char *name, Icon &icon)
|
||||
{
|
||||
if (!bitmapStorageAvailable() || !validBitmapName(name))
|
||||
return false;
|
||||
auto cached = cache.find(name);
|
||||
if (cached != cache.end())
|
||||
{
|
||||
if (cached->second.exists)
|
||||
icon = cached->second.icon;
|
||||
return cached->second.exists;
|
||||
}
|
||||
|
||||
// Checked first: open() logs an error for a missing file
|
||||
bool ok = false;
|
||||
Icon loaded;
|
||||
if (LittleFS.exists(path(name)))
|
||||
{
|
||||
File file = LittleFS.open(path(name), "r");
|
||||
if (file)
|
||||
{
|
||||
uint8_t size[2];
|
||||
ok = file.read(size, 2) == 2 && validIconSize(size[0], size[1]);
|
||||
if (ok)
|
||||
{
|
||||
loaded.width = size[0];
|
||||
loaded.height = size[1];
|
||||
size_t bytes = iconBytes(loaded.width, loaded.height);
|
||||
ok = file.read(loaded.data, bytes) == bytes;
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
if (cache.size() >= CACHE_MAX)
|
||||
cache.clear();
|
||||
CachedIcon &entry = cache[name];
|
||||
entry.exists = ok;
|
||||
if (ok)
|
||||
{
|
||||
entry.icon = loaded;
|
||||
icon = loaded;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
const char *saveIcon(const char *name, const Icon &icon)
|
||||
{
|
||||
if (!validBitmapName(name))
|
||||
return "Invalid name: use 1-24 letters, digits, spaces, - or _";
|
||||
if (!validIconSize(icon.width, icon.height))
|
||||
return "Icons are 1 x 1 to 84 x 19 dots";
|
||||
if (!ensureDir())
|
||||
return "No storage (install the new flash layout over USB, see README)";
|
||||
bool is_new = !LittleFS.exists(path(name));
|
||||
if (count < 0)
|
||||
count = storedNames(DIR, ".bin").size();
|
||||
if (is_new && count >= MAX_ICONS)
|
||||
return "Too many icons, delete some first";
|
||||
|
||||
File file = LittleFS.open(path(name), "w");
|
||||
if (!file)
|
||||
return "Could not write the file";
|
||||
uint8_t size[2] = {icon.width, icon.height};
|
||||
size_t bytes = iconBytes(icon.width, icon.height);
|
||||
bool ok = file.write(size, 2) == 2 && file.write(icon.data, bytes) == bytes;
|
||||
file.close();
|
||||
revision++;
|
||||
cache.clear();
|
||||
if (!ok)
|
||||
{
|
||||
LittleFS.remove(path(name)); // not half an icon
|
||||
count = -1;
|
||||
return "Storage full";
|
||||
}
|
||||
if (is_new)
|
||||
count++;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *deleteIcon(const char *name)
|
||||
{
|
||||
if (!bitmapStorageAvailable() || !validBitmapName(name) || !LittleFS.exists(path(name)))
|
||||
return "Icon not found";
|
||||
if (!LittleFS.remove(path(name)))
|
||||
return "Could not delete the file";
|
||||
if (count > 0)
|
||||
count--;
|
||||
revision++;
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
|
||||
#include "logic/icon_format.h"
|
||||
|
||||
// Icons (logic/icon_format.h): small images that pages use as images or backgrounds, never
|
||||
// shown on their own. Stored on LittleFS (mounted by display/bitmap_store) as
|
||||
// /icons/<name>.bin: width, height, then the rows. Icons, bitmaps and pages share one
|
||||
// namespace: app.cpp checks that a new name is not taken.
|
||||
|
||||
const int MAX_ICONS = 200;
|
||||
|
||||
// Sorted without case
|
||||
std::vector<String> listIcons();
|
||||
// Changes each time an icon is saved or deleted
|
||||
uint32_t iconListRevision();
|
||||
bool iconExists(const char *name);
|
||||
bool loadIcon(const char *name, Icon &icon);
|
||||
|
||||
// Return nullptr on success, otherwise a message for the user
|
||||
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();
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "display/bitmap_store.h"
|
||||
#include "display/font_store.h"
|
||||
#include "display/icon_store.h"
|
||||
#include "net/ha_api.h"
|
||||
|
||||
Stopwatch stopwatch;
|
||||
@@ -11,7 +12,13 @@ StoredPageResources pageResources(stopwatch);
|
||||
|
||||
bool StoredPageResources::background(const char *name, uint8_t *bitmap)
|
||||
{
|
||||
return loadBitmap(name, bitmap);
|
||||
if (loadBitmap(name, bitmap))
|
||||
return true;
|
||||
Icon icon;
|
||||
if (!loadIcon(name, icon))
|
||||
return false;
|
||||
iconToBitmap(icon, bitmap); // at the top left: images are drawn from their x, y
|
||||
return true;
|
||||
}
|
||||
|
||||
Font StoredPageResources::font(const char *name)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "logic/page_render.h"
|
||||
#include "logic/stopwatch.h"
|
||||
|
||||
// Bitmaps from display/bitmap_store, fonts from display/font_store, Home Assistant values
|
||||
// Bitmaps and icons from display/bitmap_store and display/icon_store, fonts from display/font_store, Home Assistant values
|
||||
// from net/ha_api and the stopwatch, for drawing dynamic pages on the display and in the
|
||||
// web UI
|
||||
class StoredPageResources : public PageResources
|
||||
|
||||
@@ -458,14 +458,9 @@ template <typename F> static String findPage(F uses)
|
||||
return found;
|
||||
}
|
||||
|
||||
String pageUsingBitmap(const char *bitmap)
|
||||
String pageUsingImage(const char *name)
|
||||
{
|
||||
return findPage([bitmap](const DynamicPage &page) {
|
||||
for (int i = 0; i < page.slot_count; i++)
|
||||
if (sameName(page.slots[i].bitmap, bitmap))
|
||||
return true;
|
||||
return false;
|
||||
});
|
||||
return findPage([name](const DynamicPage &page) { return pageUsesImage(page, name); });
|
||||
}
|
||||
|
||||
String pageUsingFont(const char *font)
|
||||
|
||||
@@ -30,6 +30,7 @@ bool loadPage(const char *name, DynamicPage &page);
|
||||
const char *savePage(const char *name, const DynamicPage &page);
|
||||
const char *deletePage(const char *name);
|
||||
|
||||
// The first stored page that uses the bitmap (as background) or the font; empty if none
|
||||
String pageUsingBitmap(const char *bitmap);
|
||||
// The first stored page that uses the bitmap or icon (logic/page.h pageUsesImage) or the
|
||||
// font; empty if none
|
||||
String pageUsingImage(const char *name);
|
||||
String pageUsingFont(const char *font);
|
||||
Reference in new issue
Block a user