Public Access
Add dynamic pages with clock, date, Home Assistant values and fonts
The hardcoded clock is now the built-in dynamic page "Clock". A dynamic page has a background bitmap per time of day and up to 8 elements (clock, date, Home Assistant value, text), each with position, alignment, font and overlay. Pages are drawn on the ESP32, also for the web previews. - Pages tab in the web UI: page editor with drag and drop, font upload (TTF/OTF converted in the browser to the FDF1 dot font format) - Built-in fonts Small, Normal, Large and Full height (generated by tools/make_builtin_fonts.py), built-in bitmaps Pikachu and Pikachu yawning - Home Assistant values over the REST API (URL and token in the HA tab) - Settings version 4: evening settings removed (now time slots of the Clock page), old records are converted - HA Mode dropdown lists the dynamic pages and bitmaps Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1d3b084de1
commit
0411804a2f
49 files changed
+3058
-331
No files matched your search
@@ -15,6 +15,8 @@ struct BuiltinBitmap
|
||||
|
||||
static const BuiltinBitmap BUILTIN[] = {
|
||||
{"Nintendo", nintendo_bitmap_Nintendo_new},
|
||||
{"Pikachu", pikachu_clock_bitmap},
|
||||
{"Pikachu yawning", pikachu_yawning_clock_bitmap},
|
||||
};
|
||||
|
||||
static bool mounted = false;
|
||||
@@ -36,8 +38,9 @@ static String path(const char *name)
|
||||
bool bitmapStoreBegin()
|
||||
{
|
||||
mounted = LittleFS.begin(true);
|
||||
if (mounted && !LittleFS.exists(DIR))
|
||||
LittleFS.mkdir(DIR);
|
||||
for (const char *dir : {DIR, "/pages", "/fonts"})
|
||||
if (mounted && !LittleFS.exists(dir))
|
||||
LittleFS.mkdir(dir);
|
||||
Serial.printf("Bitmap storage %s\n", mounted ? "ready" : "not available (old flash layout?)");
|
||||
return mounted;
|
||||
}
|
||||
@@ -53,27 +56,37 @@ void bitmapStorageUsage(size_t &used, size_t &total)
|
||||
total = mounted ? LittleFS.totalBytes() : 0;
|
||||
}
|
||||
|
||||
std::vector<BitmapInfo> listBitmaps()
|
||||
std::vector<String> storedNames(const char *dir_path, const char *extension)
|
||||
{
|
||||
std::vector<BitmapInfo> list;
|
||||
for (const BuiltinBitmap &b : BUILTIN)
|
||||
list.push_back({b.name, true});
|
||||
std::vector<String> names;
|
||||
if (!mounted)
|
||||
return list;
|
||||
|
||||
File dir = LittleFS.open(DIR);
|
||||
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(".bin"))
|
||||
if (!name.endsWith(extension))
|
||||
continue;
|
||||
name.remove(name.length() - 4);
|
||||
if (validBitmapName(name.c_str()) && !builtin(name.c_str()))
|
||||
list.push_back({name, false});
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
|
||||
// Full screen bitmaps (84 x 19, see logic/bitmap_format.h): built-in ones from
|
||||
// display/bitmaps.h and the ones uploaded in the web UI, stored on LittleFS
|
||||
// (partition "spiffs" in partitions.csv) as /bitmaps/<name>.bin.
|
||||
// (partition "spiffs" in partitions.csv) as /bitmaps/<name>.bin. Bitmaps and dynamic
|
||||
// pages share one namespace: app.cpp checks that a new name is not taken.
|
||||
|
||||
struct BitmapInfo
|
||||
{
|
||||
@@ -20,8 +21,11 @@ const int MAX_BITMAPS = 200;
|
||||
// Mounts LittleFS (formats it the first time). False without the partition (old flash
|
||||
// layout): only the built-in bitmaps are available then.
|
||||
bool bitmapStoreBegin();
|
||||
// (LittleFS is shared with display/page_store and display/font_store)
|
||||
bool bitmapStorageAvailable();
|
||||
void bitmapStorageUsage(size_t &used, size_t &total);
|
||||
// The valid names of the files with this extension in a LittleFS directory
|
||||
std::vector<String> storedNames(const char *dir, const char *extension);
|
||||
|
||||
std::vector<BitmapInfo> listBitmaps();
|
||||
// Changes each time a bitmap is saved or deleted
|
||||
|
||||
+32
-28
@@ -39,34 +39,38 @@ const unsigned char yawning_pikachu_bitmap_yawning_pikachu [] PROGMEM = {
|
||||
0xfe, 0xff, 0xf7, 0x6f, 0xe0, 0xfe, 0x7f, 0xfb, 0x8f, 0xe0, 0xfe, 0xfe, 0x0b, 0xaf, 0xe0
|
||||
};
|
||||
|
||||
// 'yawning_pikachu_black_border', 35x19px
|
||||
const unsigned char yawning_pikachu_bitmap_yawning_pikachu_black_border [] PROGMEM = {
|
||||
0x00, 0x03, 0xf8, 0x00, 0x00, 0x00, 0x1f, 0xff, 0x00, 0x00, 0x01, 0xff, 0xff, 0xf0, 0x00, 0x07,
|
||||
0xff, 0xff, 0xfc, 0x00, 0x1f, 0xff, 0xff, 0xff, 0x00, 0x3f, 0x7f, 0xff, 0xdf, 0x80, 0x7f, 0x73,
|
||||
0xf9, 0xdf, 0xc0, 0xfe, 0xcf, 0xbe, 0x6f, 0xe0, 0xfd, 0xff, 0xff, 0xf7, 0xe0, 0x01, 0xff, 0x1f,
|
||||
0xf0, 0x00, 0x03, 0x9e, 0x0f, 0x38, 0x00, 0x03, 0x0e, 0xee, 0x18, 0x00, 0x03, 0x0e, 0xee, 0x18,
|
||||
0x00, 0x03, 0x9e, 0xe3, 0x38, 0x00, 0x03, 0xff, 0x0d, 0xf8, 0x00, 0x01, 0xff, 0xee, 0xf0, 0x00,
|
||||
0x01, 0xff, 0xf7, 0x70, 0x00, 0x00, 0x7f, 0xfb, 0x80, 0x00, 0x01, 0xfe, 0x0b, 0xb0, 0x00
|
||||
// 'Pikachu', 84x19px: Pikachu with an empty thought bubble (background of the Clock page)
|
||||
const unsigned char pikachu_clock_bitmap[] PROGMEM = {
|
||||
0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xe0, 0x00,
|
||||
0x3f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x3f, 0xff, 0xfe, 0x00, 0x40, 0x00, 0x00, 0x00, 0x02,
|
||||
0x00, 0x00, 0xff, 0xff, 0xff, 0x80, 0x80, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0xff, 0xff, 0xff,
|
||||
0xe0, 0x80, 0x00, 0x00, 0x00, 0x01, 0x00, 0x07, 0xee, 0x7f, 0x3b, 0xf0, 0x80, 0x00, 0x00, 0x00,
|
||||
0x01, 0x00, 0x0f, 0xed, 0x3e, 0x9b, 0xf8, 0x80, 0x00, 0x00, 0x00, 0x01, 0x00, 0x1f, 0xdc, 0x3e,
|
||||
0x1d, 0xfc, 0x80, 0x00, 0x00, 0x00, 0x01, 0x00, 0x1f, 0xbe, 0x7f, 0x3e, 0xfc, 0x80, 0x00, 0x00,
|
||||
0x00, 0x01, 0x00, 0x00, 0x3f, 0xf7, 0xfe, 0x00, 0x80, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x73,
|
||||
0xff, 0xe7, 0x00, 0x80, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x61, 0xff, 0xc3, 0x00, 0x80, 0x00,
|
||||
0x00, 0x00, 0x01, 0x00, 0x00, 0x61, 0xff, 0xc3, 0x00, 0x40, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00,
|
||||
0x73, 0xb6, 0xe7, 0x00, 0x20, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x7f, 0xc9, 0xff, 0x00, 0x41,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xff, 0xfe, 0x00, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x3f, 0xff, 0xfe, 0x01, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xf0, 0x02,
|
||||
0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xc1, 0xfe, 0x03, 0x80, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00,
|
||||
};
|
||||
|
||||
// 'clock thought bubble', 46x19px
|
||||
const unsigned char clock_thought_bubble_bitmap_clock_thought_bubble [] PROGMEM = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0x00, 0x10, 0x00, 0x00, 0x00,
|
||||
0x00, 0x80, 0x20, 0x00, 0x00, 0x00, 0x00, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x40, 0x20, 0x00,
|
||||
0x00, 0x00, 0x00, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x40,
|
||||
0x20, 0x00, 0x00, 0x00, 0x00, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x40, 0x20, 0x00, 0x00, 0x00,
|
||||
0x00, 0x40, 0x20, 0x00, 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x00, 0x00, 0x00, 0x80, 0x08, 0x3f,
|
||||
0xff, 0xff, 0xff, 0x00, 0x10, 0x40, 0x00, 0x00, 0x00, 0x00, 0x21, 0x80, 0x00, 0x00, 0x00, 0x00,
|
||||
0x46, 0x00, 0x00, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00
|
||||
};
|
||||
|
||||
// 'pikachu_black_border', 35x19px
|
||||
const unsigned char pikachu_bitmap_pikachu_black_border [] PROGMEM = {
|
||||
0x00, 0x03, 0xf8, 0x00, 0x00, 0x00, 0x1f, 0xff, 0x00, 0x00, 0x01, 0xff, 0xff, 0xf0, 0x00, 0x07,
|
||||
0xff, 0xff, 0xfc, 0x00, 0x1f, 0xff, 0xff, 0xff, 0x00, 0x3f, 0x73, 0xf9, 0xdf, 0x80, 0x7f, 0x69,
|
||||
0xf4, 0xdf, 0xc0, 0xfe, 0xe1, 0xf0, 0xef, 0xe0, 0xfd, 0xf3, 0xf9, 0xf7, 0xe0, 0x01, 0xff, 0xbf,
|
||||
0xf0, 0x00, 0x03, 0x9f, 0xff, 0x38, 0x00, 0x03, 0x0f, 0xfe, 0x18, 0x00, 0x03, 0x0f, 0xfe, 0x18,
|
||||
0x00, 0x03, 0x9d, 0xb7, 0x38, 0x00, 0x03, 0xfe, 0x4f, 0xf8, 0x00, 0x01, 0xff, 0xff, 0xf0, 0x00,
|
||||
0x01, 0xff, 0xff, 0xf0, 0x00, 0x00, 0x7f, 0xff, 0x80, 0x00, 0x01, 0xfe, 0x0f, 0xf0, 0x00
|
||||
// 'Pikachu yawning', 84x19px: Pikachu with an empty thought bubble (background of the Clock page)
|
||||
const unsigned char pikachu_yawning_clock_bitmap[] PROGMEM = {
|
||||
0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xe0, 0x00,
|
||||
0x3f, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x3f, 0xff, 0xfe, 0x00, 0x40, 0x00, 0x00, 0x00, 0x02,
|
||||
0x00, 0x00, 0xff, 0xff, 0xff, 0x80, 0x80, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0xff, 0xff, 0xff,
|
||||
0xe0, 0x80, 0x00, 0x00, 0x00, 0x01, 0x00, 0x07, 0xef, 0xff, 0xfb, 0xf0, 0x80, 0x00, 0x00, 0x00,
|
||||
0x01, 0x00, 0x0f, 0xee, 0x7f, 0x3b, 0xf8, 0x80, 0x00, 0x00, 0x00, 0x01, 0x00, 0x1f, 0xd9, 0xf7,
|
||||
0xcd, 0xfc, 0x80, 0x00, 0x00, 0x00, 0x01, 0x00, 0x1f, 0xbf, 0xff, 0xfe, 0xfc, 0x80, 0x00, 0x00,
|
||||
0x00, 0x01, 0x00, 0x00, 0x3f, 0xe3, 0xfe, 0x00, 0x80, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x73,
|
||||
0xc1, 0xe7, 0x00, 0x80, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x61, 0xdd, 0xc3, 0x00, 0x80, 0x00,
|
||||
0x00, 0x00, 0x01, 0x00, 0x00, 0x61, 0xdd, 0xc3, 0x00, 0x40, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00,
|
||||
0x73, 0xdc, 0x67, 0x00, 0x20, 0xff, 0xff, 0xff, 0xfc, 0x00, 0x00, 0x7f, 0xe1, 0xbf, 0x00, 0x41,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xfd, 0xde, 0x00, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x3f, 0xfe, 0xee, 0x01, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0x70, 0x02,
|
||||
0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0xc1, 0x76, 0x03, 0x80, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00,
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
#include "display/font_store.h"
|
||||
|
||||
#include <LittleFS.h>
|
||||
#include <map>
|
||||
|
||||
#include "display/bitmap_store.h"
|
||||
#include "logic/bitmap_format.h"
|
||||
#include "logic/builtin_fonts.h"
|
||||
|
||||
static const char *DIR = "/fonts";
|
||||
static const char *EXTENSION = ".fnt";
|
||||
|
||||
static uint32_t revision = 0;
|
||||
// Loaded fonts by name; cleared when a font is saved or deleted
|
||||
static std::map<String, std::vector<uint8_t>> cache;
|
||||
|
||||
static String path(const char *name)
|
||||
{
|
||||
return String(DIR) + "/" + name + EXTENSION;
|
||||
}
|
||||
|
||||
std::vector<FontInfo> listFonts()
|
||||
{
|
||||
std::vector<FontInfo> list;
|
||||
for (int i = 0; i < BUILTIN_FONT_COUNT; i++)
|
||||
list.push_back({BUILTIN_FONTS[i].name, true, (uint8_t)fontHeight(builtinFont(BUILTIN_FONTS[i]))});
|
||||
for (const String &name : storedNames(DIR, EXTENSION))
|
||||
if (!findBuiltinFont(name.c_str()))
|
||||
list.push_back({name, false, (uint8_t)fontHeight(findFont(name.c_str()))});
|
||||
return list;
|
||||
}
|
||||
|
||||
uint32_t fontListRevision()
|
||||
{
|
||||
return revision;
|
||||
}
|
||||
|
||||
bool fontExists(const char *name)
|
||||
{
|
||||
if (findBuiltinFont(name))
|
||||
return true;
|
||||
return bitmapStorageAvailable() && validBitmapName(name) && LittleFS.exists(path(name));
|
||||
}
|
||||
|
||||
Font findFont(const char *name)
|
||||
{
|
||||
const BuiltinFont *builtin = findBuiltinFont(name);
|
||||
if (builtin)
|
||||
return builtinFont(*builtin);
|
||||
|
||||
Font font;
|
||||
if (!bitmapStorageAvailable() || !validBitmapName(name))
|
||||
return font;
|
||||
auto cached = cache.find(name);
|
||||
if (cached == cache.end())
|
||||
{
|
||||
if (cache.size() >= 16)
|
||||
cache.clear();
|
||||
std::vector<uint8_t> data;
|
||||
File file;
|
||||
if (LittleFS.exists(path(name))) // open() logs an error for a missing file
|
||||
file = LittleFS.open(path(name), "r");
|
||||
if (file && file.size() <= FONT_MAX_SIZE)
|
||||
{
|
||||
data.resize(file.size());
|
||||
if (file.read(data.data(), data.size()) != data.size() || !validFont(data.data(), data.size()))
|
||||
data.clear();
|
||||
}
|
||||
if (file)
|
||||
file.close();
|
||||
cached = cache.emplace(name, std::move(data)).first; // also remembers a missing font
|
||||
}
|
||||
if (!cached->second.empty())
|
||||
{
|
||||
font.data = cached->second.data();
|
||||
font.size = cached->second.size();
|
||||
}
|
||||
return font;
|
||||
}
|
||||
|
||||
const char *saveFont(const char *name, const uint8_t *data, size_t size)
|
||||
{
|
||||
if (!validBitmapName(name))
|
||||
return "Invalid name: use 1-24 letters, digits, spaces, - or _";
|
||||
if (findBuiltinFont(name))
|
||||
return "A built-in font has this name";
|
||||
if (!validFont(data, size))
|
||||
return "Invalid font data";
|
||||
if (!bitmapStorageAvailable())
|
||||
return "No storage (install the new flash layout over USB, see README)";
|
||||
if (!LittleFS.exists(path(name)) && storedNames(DIR, EXTENSION).size() >= (size_t)MAX_FONTS)
|
||||
return "Too many fonts, delete some first";
|
||||
|
||||
File file = LittleFS.open(path(name), "w");
|
||||
bool ok = file && file.write(data, size) == size;
|
||||
if (file)
|
||||
file.close();
|
||||
cache.clear();
|
||||
revision++;
|
||||
return ok ? nullptr : "Could not write the file (storage full?)";
|
||||
}
|
||||
|
||||
const char *deleteFont(const char *name)
|
||||
{
|
||||
if (findBuiltinFont(name))
|
||||
return "Built-in fonts can't be deleted";
|
||||
if (!bitmapStorageAvailable() || !validBitmapName(name) || !LittleFS.exists(path(name)))
|
||||
return "Font not found";
|
||||
if (!LittleFS.remove(path(name)))
|
||||
return "Could not delete the file";
|
||||
cache.clear();
|
||||
revision++;
|
||||
return nullptr;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
|
||||
#include "logic/font.h"
|
||||
|
||||
// Fonts of the dynamic pages: the built-in ones (logic/builtin_fonts.h) and the ones
|
||||
// converted from TTF/OTF in the web UI, stored on LittleFS as /fonts/<name>.fnt in the
|
||||
// format of logic/font.h. Font names have their own namespace.
|
||||
|
||||
struct FontInfo
|
||||
{
|
||||
String name;
|
||||
bool builtin;
|
||||
uint8_t height; // in dots (scaled)
|
||||
};
|
||||
|
||||
const int MAX_FONTS = 20;
|
||||
|
||||
// Built-in fonts first, then the stored ones
|
||||
std::vector<FontInfo> listFonts();
|
||||
// Changes each time a font is saved or deleted
|
||||
uint32_t fontListRevision();
|
||||
bool fontExists(const char *name);
|
||||
|
||||
// A font by name: built-in, or loaded from LittleFS and kept in memory. A font without
|
||||
// data when it is gone. The data stays valid until the next findFont(), saveFont() or
|
||||
// deleteFont().
|
||||
Font findFont(const char *name);
|
||||
|
||||
// Return nullptr on success, otherwise a message for the user
|
||||
const char *saveFont(const char *name, const uint8_t *data, size_t size);
|
||||
const char *deleteFont(const char *name);
|
||||
@@ -3,33 +3,17 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "display/bitmaps.h"
|
||||
#include "display/text.h"
|
||||
|
||||
// Horizontal offset that centers the 81 pixel wide clock on the 84 pixel display
|
||||
static const int CLOCK_X = 3;
|
||||
|
||||
FlipdotFrames::FlipdotFrames(BROSE9323 &display) : _display(display)
|
||||
FlipdotFrames::FlipdotFrames(BROSE9323 &display, PageResources &resources) : _display(display), _resources(resources)
|
||||
{
|
||||
}
|
||||
|
||||
void FlipdotFrames::clock(int hour, int minute, bool evening)
|
||||
void FlipdotFrames::dynamicPage(const LocalTime &time)
|
||||
{
|
||||
char time[6];
|
||||
snprintf(time, sizeof(time), "%02d:%02d", hour, minute);
|
||||
|
||||
_display.fillScreen(0);
|
||||
if (evening)
|
||||
_display.drawBitmap(CLOCK_X, 0, yawning_pikachu_bitmap_yawning_pikachu_black_border, 35, 19, 1);
|
||||
else
|
||||
_display.drawBitmap(CLOCK_X, 0, pikachu_bitmap_pikachu_black_border, 35, 19, 1);
|
||||
_display.drawBitmap(CLOCK_X + 35, 0, clock_thought_bubble_bitmap_clock_thought_bubble, 46, 19, 1);
|
||||
|
||||
_display.setTextSize(1);
|
||||
_display.setTextWrap(false);
|
||||
_display.setCursor(CLOCK_X + 43, 4);
|
||||
_display.print(time);
|
||||
_display.display();
|
||||
uint8_t bitmap[BITMAP_BYTES];
|
||||
renderPage(_dynamic, time, _resources, bitmap);
|
||||
_drawFullScreen(bitmap);
|
||||
}
|
||||
|
||||
void FlipdotFrames::noWifi(int hour, int minute)
|
||||
@@ -55,6 +39,11 @@ void FlipdotFrames::off()
|
||||
_drawFullScreen(_off);
|
||||
}
|
||||
|
||||
void FlipdotFrames::setDynamicPage(const DynamicPage &page)
|
||||
{
|
||||
_dynamic = page;
|
||||
}
|
||||
|
||||
void FlipdotFrames::setPageBitmap(const uint8_t *bitmap)
|
||||
{
|
||||
memcpy(_page, bitmap, BITMAP_BYTES);
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
#include "BROSE9323.h"
|
||||
#include "logic/bitmap_format.h"
|
||||
#include "logic/display_controller.h"
|
||||
#include "logic/page_render.h"
|
||||
|
||||
// Draws the screens on the flipdot display. Each method sends one complete frame.
|
||||
class FlipdotFrames : public Frames
|
||||
{
|
||||
public:
|
||||
explicit FlipdotFrames(BROSE9323 &display);
|
||||
FlipdotFrames(BROSE9323 &display, PageResources &resources);
|
||||
|
||||
// Pikachu with a thought bubble showing HH:MM (yawning in the evening)
|
||||
void clock(int hour, int minute, bool evening) override;
|
||||
// The page set with setDynamicPage(), drawn for this time
|
||||
void dynamicPage(const LocalTime &time) override;
|
||||
// "No WiFi" with the time below it
|
||||
void noWifi(int hour, int minute) override;
|
||||
// The bitmap set with setPageBitmap()
|
||||
@@ -21,6 +22,7 @@ public:
|
||||
// Blank, or the bitmap set with setOffBitmap()
|
||||
void off() override;
|
||||
|
||||
void setDynamicPage(const DynamicPage &page);
|
||||
void setPageBitmap(const uint8_t *bitmap);
|
||||
void setLiveBitmap(const uint8_t *bitmap);
|
||||
// nullptr: blank
|
||||
@@ -28,6 +30,8 @@ public:
|
||||
|
||||
private:
|
||||
BROSE9323 &_display;
|
||||
PageResources &_resources;
|
||||
DynamicPage _dynamic;
|
||||
uint8_t _page[BITMAP_BYTES] = {};
|
||||
uint8_t _live[BITMAP_BYTES] = {};
|
||||
uint8_t _off[BITMAP_BYTES] = {}; // all zero: blank
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
#include "display/page_resources.h"
|
||||
|
||||
#include "display/bitmap_store.h"
|
||||
#include "display/font_store.h"
|
||||
#include "net/ha_api.h"
|
||||
|
||||
StoredPageResources pageResources;
|
||||
|
||||
bool StoredPageResources::background(const char *name, uint8_t *bitmap)
|
||||
{
|
||||
return loadBitmap(name, bitmap);
|
||||
}
|
||||
|
||||
Font StoredPageResources::font(const char *name)
|
||||
{
|
||||
return findFont(name);
|
||||
}
|
||||
|
||||
const char *StoredPageResources::haValue(const PageElement &element)
|
||||
{
|
||||
return haApiValue(element.entity, element.attribute);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "logic/page_render.h"
|
||||
|
||||
// Bitmaps from display/bitmap_store, fonts from display/font_store and Home Assistant
|
||||
// values from net/ha_api, for drawing dynamic pages on the display and in the web UI
|
||||
class StoredPageResources : public PageResources
|
||||
{
|
||||
public:
|
||||
bool background(const char *name, uint8_t *bitmap) override;
|
||||
Font font(const char *name) override;
|
||||
const char *haValue(const PageElement &element) override;
|
||||
};
|
||||
|
||||
extern StoredPageResources pageResources;
|
||||
@@ -0,0 +1,150 @@
|
||||
#include "display/page_store.h"
|
||||
|
||||
#include <LittleFS.h>
|
||||
|
||||
#include "display/bitmap_store.h"
|
||||
#include "logic/builtin_pages.h"
|
||||
|
||||
static const char *DIR = "/pages";
|
||||
static const char *EXTENSION = ".pg";
|
||||
// Increase when DynamicPage changes (older files are then ignored)
|
||||
static const uint16_t VERSION = 1;
|
||||
|
||||
struct StoredPage
|
||||
{
|
||||
uint16_t version;
|
||||
uint16_t size;
|
||||
DynamicPage page;
|
||||
};
|
||||
|
||||
static uint32_t revision = 0;
|
||||
|
||||
static String path(const char *name)
|
||||
{
|
||||
return String(DIR) + "/" + name + EXTENSION;
|
||||
}
|
||||
|
||||
std::vector<PageInfo> listPages()
|
||||
{
|
||||
std::vector<PageInfo> list;
|
||||
for (int i = 0; i < builtinPageCount(); i++)
|
||||
list.push_back({builtinPageName(i), true});
|
||||
for (const String &name : storedNames(DIR, EXTENSION))
|
||||
if (!builtinPageExists(name.c_str()))
|
||||
list.push_back({name, false});
|
||||
return list;
|
||||
}
|
||||
|
||||
uint32_t pageListRevision()
|
||||
{
|
||||
return revision;
|
||||
}
|
||||
|
||||
bool builtinPageExists(const char *name)
|
||||
{
|
||||
for (int i = 0; i < builtinPageCount(); i++)
|
||||
if (sameName(builtinPageName(i), name))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool pageExists(const char *name)
|
||||
{
|
||||
if (builtinPageExists(name))
|
||||
return true;
|
||||
return bitmapStorageAvailable() && validBitmapName(name) && LittleFS.exists(path(name));
|
||||
}
|
||||
|
||||
bool loadPage(const char *name, DynamicPage &page)
|
||||
{
|
||||
if (builtinPage(name, page))
|
||||
return true;
|
||||
if (!bitmapStorageAvailable() || !validBitmapName(name) || !LittleFS.exists(path(name)))
|
||||
return false;
|
||||
File file = LittleFS.open(path(name), "r");
|
||||
if (!file)
|
||||
return false;
|
||||
StoredPage *stored = new StoredPage;
|
||||
bool ok = file.read((uint8_t *)stored, sizeof(StoredPage)) == sizeof(StoredPage) &&
|
||||
stored->version == VERSION && stored->size == sizeof(DynamicPage) &&
|
||||
validatePage(stored->page) == nullptr;
|
||||
file.close();
|
||||
if (ok)
|
||||
page = stored->page;
|
||||
delete stored;
|
||||
return ok;
|
||||
}
|
||||
|
||||
const char *savePage(const char *name, const DynamicPage &page)
|
||||
{
|
||||
if (!validBitmapName(name))
|
||||
return "Invalid name: use 1-24 letters, digits, spaces, - or _";
|
||||
if (builtinPageExists(name))
|
||||
return "A built-in page has this name";
|
||||
const char *error = validatePage(page);
|
||||
if (error)
|
||||
return error;
|
||||
if (!bitmapStorageAvailable())
|
||||
return "No storage (install the new flash layout over USB, see README)";
|
||||
if (!LittleFS.exists(path(name)) && storedNames(DIR, EXTENSION).size() >= (size_t)MAX_PAGES)
|
||||
return "Too many pages, delete some first";
|
||||
|
||||
StoredPage *stored = new StoredPage;
|
||||
stored->version = VERSION;
|
||||
stored->size = sizeof(DynamicPage);
|
||||
stored->page = page;
|
||||
File file = LittleFS.open(path(name), "w");
|
||||
bool ok = file && file.write((const uint8_t *)stored, sizeof(StoredPage)) == sizeof(StoredPage);
|
||||
if (file)
|
||||
file.close();
|
||||
delete stored;
|
||||
revision++;
|
||||
return ok ? nullptr : "Could not write the file (storage full?)";
|
||||
}
|
||||
|
||||
const char *deletePage(const char *name)
|
||||
{
|
||||
if (builtinPageExists(name))
|
||||
return "Built-in pages can't be deleted";
|
||||
if (!bitmapStorageAvailable() || !validBitmapName(name) || !LittleFS.exists(path(name)))
|
||||
return "Page not found";
|
||||
if (!LittleFS.remove(path(name)))
|
||||
return "Could not delete the file";
|
||||
revision++;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// The first stored page for which `uses` is true
|
||||
template <typename F> static String findPage(F uses)
|
||||
{
|
||||
DynamicPage *page = new DynamicPage;
|
||||
String found;
|
||||
for (const String &name : storedNames(DIR, EXTENSION))
|
||||
if (loadPage(name.c_str(), *page) && uses(*page))
|
||||
{
|
||||
found = name;
|
||||
break;
|
||||
}
|
||||
delete page;
|
||||
return found;
|
||||
}
|
||||
|
||||
String pageUsingBitmap(const char *bitmap)
|
||||
{
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
String pageUsingFont(const char *font)
|
||||
{
|
||||
return findPage([font](const DynamicPage &page) {
|
||||
for (int i = 0; i < page.element_count; i++)
|
||||
if (sameName(page.elements[i].font, font))
|
||||
return true;
|
||||
return false;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
|
||||
#include "logic/page.h"
|
||||
|
||||
// Dynamic pages (logic/page.h): the built-in ones (logic/builtin_pages.h) and the ones
|
||||
// made in the web UI, stored on LittleFS as /pages/<name>.pg (the struct with a version).
|
||||
// Pages and bitmaps share one namespace: app.cpp checks that a new name is not taken.
|
||||
|
||||
struct PageInfo
|
||||
{
|
||||
String name;
|
||||
bool builtin;
|
||||
};
|
||||
|
||||
const int MAX_PAGES = 50;
|
||||
|
||||
// Built-in pages first, then the stored ones
|
||||
std::vector<PageInfo> listPages();
|
||||
// Changes each time a page is saved or deleted
|
||||
uint32_t pageListRevision();
|
||||
bool pageExists(const char *name);
|
||||
bool builtinPageExists(const char *name);
|
||||
bool loadPage(const char *name, DynamicPage &page);
|
||||
|
||||
// Return nullptr on success, otherwise a message for the user. savePage() validates the
|
||||
// page, but does not check that its bitmaps and fonts exist.
|
||||
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);
|
||||
String pageUsingFont(const char *font);
|
||||
Reference in new issue
Block a user