Public Access
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>
195 lines
4.7 KiB
C++
195 lines
4.7 KiB
C++
#include "display/bitmap_store.h"
|
|
|
|
#include <LittleFS.h>
|
|
#include <map>
|
|
#include <strings.h>
|
|
|
|
#include "display/bitmaps.h"
|
|
|
|
static const char *DIR = "/bitmaps";
|
|
|
|
struct BuiltinBitmap
|
|
{
|
|
const char *name;
|
|
const uint8_t *data; // PROGMEM, BITMAP_BYTES
|
|
};
|
|
|
|
static const BuiltinBitmap BUILTIN[] = {
|
|
{"Nintendo", nintendo_bitmap_Nintendo_new},
|
|
{"Pikachu", pikachu_clock_bitmap},
|
|
{"Pikachu yawning", pikachu_yawning_clock_bitmap},
|
|
};
|
|
|
|
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)
|
|
if (strcasecmp(b.name, name) == 0)
|
|
return &b;
|
|
return nullptr;
|
|
}
|
|
|
|
static String path(const char *name)
|
|
{
|
|
return String(DIR) + "/" + name + ".bin";
|
|
}
|
|
|
|
bool bitmapStoreBegin()
|
|
{
|
|
mounted = LittleFS.begin(true);
|
|
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?)");
|
|
return mounted;
|
|
}
|
|
|
|
bool bitmapStorageAvailable()
|
|
{
|
|
return mounted;
|
|
}
|
|
|
|
void bitmapStorageUsage(size_t &used, size_t &total)
|
|
{
|
|
used = mounted ? LittleFS.usedBytes() : 0;
|
|
total = mounted ? LittleFS.totalBytes() : 0;
|
|
}
|
|
|
|
std::vector<String> storedNames(const char *dir_path, const char *extension)
|
|
{
|
|
std::vector<String> names;
|
|
if (!mounted)
|
|
return names;
|
|
File dir = LittleFS.open(dir_path);
|
|
if (!dir)
|
|
return names;
|
|
for (File file = dir.openNextFile(); file; file = dir.openNextFile())
|
|
{
|
|
String name = file.name();
|
|
int slash = name.lastIndexOf('/');
|
|
if (slash >= 0)
|
|
name = name.substring(slash + 1);
|
|
if (!name.endsWith(extension))
|
|
continue;
|
|
name.remove(name.length() - strlen(extension));
|
|
if (validBitmapName(name.c_str()))
|
|
names.push_back(name);
|
|
}
|
|
return names;
|
|
}
|
|
|
|
std::vector<BitmapInfo> listBitmaps()
|
|
{
|
|
std::vector<BitmapInfo> list;
|
|
for (const BuiltinBitmap &b : BUILTIN)
|
|
list.push_back({b.name, true});
|
|
for (const String &name : storedNames(DIR, ".bin"))
|
|
if (!builtin(name.c_str()))
|
|
list.push_back({name, false});
|
|
return list;
|
|
}
|
|
|
|
uint32_t bitmapListRevision()
|
|
{
|
|
return revision;
|
|
}
|
|
|
|
bool builtinBitmap(const char *name)
|
|
{
|
|
return builtin(name) != nullptr;
|
|
}
|
|
|
|
bool bitmapExists(const char *name)
|
|
{
|
|
if (builtin(name))
|
|
return true;
|
|
uint8_t bitmap[BITMAP_BYTES];
|
|
return loadBitmap(name, bitmap); // through the cache
|
|
}
|
|
|
|
bool loadBitmap(const char *name, uint8_t *bitmap)
|
|
{
|
|
const BuiltinBitmap *b = builtin(name);
|
|
if (b)
|
|
{
|
|
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)
|
|
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;
|
|
}
|
|
|
|
const char *saveBitmap(const char *name, const uint8_t *bitmap)
|
|
{
|
|
if (!validBitmapName(name))
|
|
return "Invalid name: use 1-24 letters, digits, spaces, - or _";
|
|
if (builtin(name))
|
|
return "A built-in bitmap has this name";
|
|
if (!mounted)
|
|
return "No bitmap storage (install the new flash layout over USB, see README)";
|
|
if (!LittleFS.exists(path(name)) && listBitmaps().size() >= (size_t)MAX_BITMAPS + (sizeof(BUILTIN) / sizeof(BUILTIN[0])))
|
|
return "Too many bitmaps, delete some first";
|
|
|
|
File file = LittleFS.open(path(name), "w");
|
|
if (!file)
|
|
return "Could not write the file";
|
|
bool ok = file.write(bitmap, BITMAP_BYTES) == (size_t)BITMAP_BYTES;
|
|
file.close();
|
|
revision++;
|
|
cache.clear();
|
|
return ok ? nullptr : "Storage full";
|
|
}
|
|
|
|
const char *deleteBitmap(const char *name)
|
|
{
|
|
if (builtin(name))
|
|
return "Built-in bitmaps can't be deleted";
|
|
if (!mounted || !validBitmapName(name) || !LittleFS.exists(path(name)))
|
|
return "Bitmap not found";
|
|
if (!LittleFS.remove(path(name)))
|
|
return "Could not delete the file";
|
|
revision++;
|
|
cache.clear();
|
|
return nullptr;
|
|
}
|