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
@@ -73,9 +73,13 @@ bool validBitmapName(const char *name)
|
||||
if (!isalnum((unsigned char)c) && c != ' ' && c != '-' && c != '_')
|
||||
return false;
|
||||
}
|
||||
// "clock" is the clock page
|
||||
char lower[BITMAP_NAME_MAX + 1];
|
||||
for (size_t i = 0; i <= len; i++)
|
||||
lower[i] = tolower((unsigned char)name[i]);
|
||||
return strcmp(lower, "clock") != 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sameName(const char *a, const char *b)
|
||||
{
|
||||
for (; *a && *b; a++, b++)
|
||||
if (tolower((unsigned char)*a) != tolower((unsigned char)*b))
|
||||
return false;
|
||||
return *a == *b;
|
||||
}
|
||||
@@ -20,7 +20,9 @@ bool bitmapFromHex(const char *hex, uint8_t *bitmap);
|
||||
// out must hold BITMAP_HEX_LENGTH + 1 characters
|
||||
void bitmapToHex(const uint8_t *bitmap, char *out);
|
||||
|
||||
// Bitmap names: 1-24 characters, letters, digits, space, '-' and '_', no space at the
|
||||
// start or end, not "clock" (that is the clock page)
|
||||
// Names of bitmaps, dynamic pages and fonts: 1-24 characters, letters, digits, space, '-'
|
||||
// and '_', no space at the start or end. Safe in file paths and JSON.
|
||||
const int BITMAP_NAME_MAX = 24;
|
||||
bool validBitmapName(const char *name);
|
||||
// Names are compared without case (bitmaps and pages share one namespace)
|
||||
bool sameName(const char *a, const char *b);
|
||||
@@ -0,0 +1,173 @@
|
||||
// Generated by tools/make_builtin_fonts.py, do not edit.
|
||||
// Small: TomThumb (BSD license, Brian J. Swetland, Vassilii Khachaturov, Dan Marks,
|
||||
// Robey Pointer); Normal: Adafruit GFX glcdfont (BSD license, Adafruit Industries).
|
||||
|
||||
#include "logic/builtin_fonts.h"
|
||||
|
||||
#include "logic/bitmap_format.h"
|
||||
|
||||
static const uint8_t FONT_SMALL[] = {
|
||||
0x46, 0x44, 0x46, 0x31, 0x06, 0x01, 0x20, 0x5f, 0x02, 0x01, 0x03, 0x03, 0x03, 0x03, 0x03, 0x01,
|
||||
0x02, 0x02, 0x03, 0x03, 0x02, 0x03, 0x01, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
|
||||
0x03, 0x03, 0x01, 0x02, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
|
||||
0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
|
||||
0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x02, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
|
||||
0x03, 0x01, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
|
||||
0x03, 0x03, 0x03, 0x03, 0x01, 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x80, 0x80,
|
||||
0x00, 0x80, 0x00, 0xa0, 0xa0, 0x00, 0x00, 0x00, 0x00, 0xa0, 0xe0, 0xa0, 0xe0, 0xa0, 0x00, 0x60,
|
||||
0xc0, 0x60, 0xc0, 0x40, 0x00, 0x80, 0x20, 0x40, 0x80, 0x20, 0x00, 0xc0, 0xc0, 0xe0, 0xa0, 0x60,
|
||||
0x00, 0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x40, 0x80, 0x80, 0x80, 0x40, 0x00, 0x80, 0x40, 0x40,
|
||||
0x40, 0x80, 0x00, 0xa0, 0x40, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x40, 0xe0, 0x40, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x40, 0x80, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
|
||||
0x00, 0x20, 0x20, 0x40, 0x80, 0x80, 0x00, 0x60, 0xa0, 0xa0, 0xa0, 0xc0, 0x00, 0x20, 0x60, 0x20,
|
||||
0x20, 0x20, 0x00, 0xc0, 0x20, 0x40, 0x80, 0xe0, 0x00, 0xc0, 0x20, 0x40, 0x20, 0xc0, 0x00, 0xa0,
|
||||
0xa0, 0xe0, 0x20, 0x20, 0x00, 0xe0, 0x80, 0xc0, 0x20, 0xc0, 0x00, 0x60, 0x80, 0xe0, 0xa0, 0xe0,
|
||||
0x00, 0xe0, 0x20, 0x40, 0x80, 0x80, 0x00, 0xe0, 0xa0, 0xe0, 0xa0, 0xe0, 0x00, 0xe0, 0xa0, 0xe0,
|
||||
0x20, 0xc0, 0x00, 0x00, 0x80, 0x00, 0x80, 0x00, 0x00, 0x00, 0x40, 0x00, 0x40, 0x80, 0x00, 0x20,
|
||||
0x40, 0x80, 0x40, 0x20, 0x00, 0x00, 0xe0, 0x00, 0xe0, 0x00, 0x00, 0x80, 0x40, 0x20, 0x40, 0x80,
|
||||
0x00, 0xe0, 0x20, 0x40, 0x00, 0x40, 0x00, 0x40, 0xa0, 0xe0, 0x80, 0x60, 0x00, 0x40, 0xa0, 0xe0,
|
||||
0xa0, 0xa0, 0x00, 0xc0, 0xa0, 0xc0, 0xa0, 0xc0, 0x00, 0x60, 0x80, 0x80, 0x80, 0x60, 0x00, 0xc0,
|
||||
0xa0, 0xa0, 0xa0, 0xc0, 0x00, 0xe0, 0x80, 0xe0, 0x80, 0xe0, 0x00, 0xe0, 0x80, 0xe0, 0x80, 0x80,
|
||||
0x00, 0x60, 0x80, 0xe0, 0xa0, 0x60, 0x00, 0xa0, 0xa0, 0xe0, 0xa0, 0xa0, 0x00, 0xe0, 0x40, 0x40,
|
||||
0x40, 0xe0, 0x00, 0x20, 0x20, 0x20, 0xa0, 0x40, 0x00, 0xa0, 0xa0, 0xc0, 0xa0, 0xa0, 0x00, 0x80,
|
||||
0x80, 0x80, 0x80, 0xe0, 0x00, 0xa0, 0xe0, 0xe0, 0xa0, 0xa0, 0x00, 0xa0, 0xe0, 0xe0, 0xe0, 0xa0,
|
||||
0x00, 0x40, 0xa0, 0xa0, 0xa0, 0x40, 0x00, 0xc0, 0xa0, 0xc0, 0x80, 0x80, 0x00, 0x40, 0xa0, 0xa0,
|
||||
0xe0, 0x60, 0x00, 0xc0, 0xa0, 0xe0, 0xc0, 0xa0, 0x00, 0x60, 0x80, 0x40, 0x20, 0xc0, 0x00, 0xe0,
|
||||
0x40, 0x40, 0x40, 0x40, 0x00, 0xa0, 0xa0, 0xa0, 0xa0, 0x60, 0x00, 0xa0, 0xa0, 0xa0, 0x40, 0x40,
|
||||
0x00, 0xa0, 0xa0, 0xe0, 0xe0, 0xa0, 0x00, 0xa0, 0xa0, 0x40, 0xa0, 0xa0, 0x00, 0xa0, 0xa0, 0x40,
|
||||
0x40, 0x40, 0x00, 0xe0, 0x20, 0x40, 0x80, 0xe0, 0x00, 0xe0, 0x80, 0x80, 0x80, 0xe0, 0x00, 0x00,
|
||||
0x80, 0x40, 0x20, 0x00, 0x00, 0xe0, 0x20, 0x20, 0x20, 0xe0, 0x00, 0x40, 0xa0, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x80, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x60,
|
||||
0xa0, 0xe0, 0x00, 0x80, 0xc0, 0xa0, 0xa0, 0xc0, 0x00, 0x00, 0x60, 0x80, 0x80, 0x60, 0x00, 0x20,
|
||||
0x60, 0xa0, 0xa0, 0x60, 0x00, 0x00, 0x60, 0xa0, 0xc0, 0x60, 0x00, 0x20, 0x40, 0xe0, 0x40, 0x40,
|
||||
0x00, 0x00, 0x60, 0xa0, 0xe0, 0x20, 0x40, 0x80, 0xc0, 0xa0, 0xa0, 0xa0, 0x00, 0x80, 0x00, 0x80,
|
||||
0x80, 0x80, 0x00, 0x20, 0x00, 0x20, 0x20, 0xa0, 0x40, 0x80, 0xa0, 0xc0, 0xc0, 0xa0, 0x00, 0xc0,
|
||||
0x40, 0x40, 0x40, 0xe0, 0x00, 0x00, 0xe0, 0xe0, 0xe0, 0xa0, 0x00, 0x00, 0xc0, 0xa0, 0xa0, 0xa0,
|
||||
0x00, 0x00, 0x40, 0xa0, 0xa0, 0x40, 0x00, 0x00, 0xc0, 0xa0, 0xa0, 0xc0, 0x80, 0x00, 0x60, 0xa0,
|
||||
0xa0, 0x60, 0x20, 0x00, 0x60, 0x80, 0x80, 0x80, 0x00, 0x00, 0x60, 0xc0, 0x60, 0xc0, 0x00, 0x40,
|
||||
0xe0, 0x40, 0x40, 0x60, 0x00, 0x00, 0xa0, 0xa0, 0xa0, 0x60, 0x00, 0x00, 0xa0, 0xa0, 0xe0, 0x40,
|
||||
0x00, 0x00, 0xa0, 0xe0, 0xe0, 0xe0, 0x00, 0x00, 0xa0, 0x40, 0x40, 0xa0, 0x00, 0x00, 0xa0, 0xa0,
|
||||
0x60, 0x20, 0x40, 0x00, 0xe0, 0x60, 0xc0, 0xe0, 0x00, 0x60, 0x40, 0x80, 0x40, 0x60, 0x00, 0x80,
|
||||
0x80, 0x00, 0x80, 0x80, 0x00, 0xc0, 0x40, 0x20, 0x40, 0xc0, 0x00, 0x60, 0xc0, 0x00, 0x00, 0x00,
|
||||
0x00,
|
||||
};
|
||||
|
||||
static const uint8_t FONT_NORMAL[] = {
|
||||
0x46, 0x44, 0x46, 0x31, 0x08, 0x01, 0x20, 0x5f, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
|
||||
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
|
||||
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
|
||||
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
|
||||
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
|
||||
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
|
||||
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20,
|
||||
0x20, 0x20, 0x20, 0x20, 0x00, 0x20, 0x00, 0x50, 0x50, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50,
|
||||
0x50, 0xf8, 0x50, 0xf8, 0x50, 0x50, 0x00, 0x20, 0x78, 0xa0, 0x70, 0x28, 0xf0, 0x20, 0x00, 0xc0,
|
||||
0xc8, 0x10, 0x20, 0x40, 0x98, 0x18, 0x00, 0x40, 0xa0, 0xa0, 0x40, 0xa8, 0x90, 0x68, 0x00, 0x30,
|
||||
0x30, 0x20, 0x40, 0x00, 0x00, 0x00, 0x00, 0x10, 0x20, 0x40, 0x40, 0x40, 0x20, 0x10, 0x00, 0x40,
|
||||
0x20, 0x10, 0x10, 0x10, 0x20, 0x40, 0x00, 0x20, 0xa8, 0x70, 0xf8, 0x70, 0xa8, 0x20, 0x00, 0x00,
|
||||
0x20, 0x20, 0xf8, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x20, 0x40, 0x00,
|
||||
0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x00, 0x00,
|
||||
0x08, 0x10, 0x20, 0x40, 0x80, 0x00, 0x00, 0x70, 0x88, 0x98, 0xa8, 0xc8, 0x88, 0x70, 0x00, 0x20,
|
||||
0x60, 0x20, 0x20, 0x20, 0x20, 0x70, 0x00, 0x70, 0x88, 0x08, 0x70, 0x80, 0x80, 0xf8, 0x00, 0xf8,
|
||||
0x08, 0x10, 0x30, 0x08, 0x88, 0x70, 0x00, 0x10, 0x30, 0x50, 0x90, 0xf8, 0x10, 0x10, 0x00, 0xf8,
|
||||
0x80, 0xf0, 0x08, 0x08, 0x88, 0x70, 0x00, 0x38, 0x40, 0x80, 0xf0, 0x88, 0x88, 0x70, 0x00, 0xf8,
|
||||
0x08, 0x08, 0x10, 0x20, 0x40, 0x80, 0x00, 0x70, 0x88, 0x88, 0x70, 0x88, 0x88, 0x70, 0x00, 0x70,
|
||||
0x88, 0x88, 0x78, 0x08, 0x10, 0xe0, 0x00, 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x20, 0x00, 0x20, 0x20, 0x40, 0x00, 0x08, 0x10, 0x20, 0x40, 0x20, 0x10, 0x08, 0x00, 0x00,
|
||||
0x00, 0xf8, 0x00, 0xf8, 0x00, 0x00, 0x00, 0x40, 0x20, 0x10, 0x08, 0x10, 0x20, 0x40, 0x00, 0x70,
|
||||
0x88, 0x08, 0x30, 0x20, 0x00, 0x20, 0x00, 0x70, 0x88, 0xa8, 0xb8, 0xb0, 0x80, 0x78, 0x00, 0x20,
|
||||
0x50, 0x88, 0x88, 0xf8, 0x88, 0x88, 0x00, 0xf0, 0x88, 0x88, 0xf0, 0x88, 0x88, 0xf0, 0x00, 0x70,
|
||||
0x88, 0x80, 0x80, 0x80, 0x88, 0x70, 0x00, 0xf0, 0x88, 0x88, 0x88, 0x88, 0x88, 0xf0, 0x00, 0xf8,
|
||||
0x80, 0x80, 0xf0, 0x80, 0x80, 0xf8, 0x00, 0xf8, 0x80, 0x80, 0xf0, 0x80, 0x80, 0x80, 0x00, 0x78,
|
||||
0x88, 0x80, 0x80, 0x98, 0x88, 0x78, 0x00, 0x88, 0x88, 0x88, 0xf8, 0x88, 0x88, 0x88, 0x00, 0x70,
|
||||
0x20, 0x20, 0x20, 0x20, 0x20, 0x70, 0x00, 0x38, 0x10, 0x10, 0x10, 0x10, 0x90, 0x60, 0x00, 0x88,
|
||||
0x90, 0xa0, 0xc0, 0xa0, 0x90, 0x88, 0x00, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xf8, 0x00, 0x88,
|
||||
0xd8, 0xa8, 0xa8, 0xa8, 0x88, 0x88, 0x00, 0x88, 0x88, 0xc8, 0xa8, 0x98, 0x88, 0x88, 0x00, 0x70,
|
||||
0x88, 0x88, 0x88, 0x88, 0x88, 0x70, 0x00, 0xf0, 0x88, 0x88, 0xf0, 0x80, 0x80, 0x80, 0x00, 0x70,
|
||||
0x88, 0x88, 0x88, 0xa8, 0x90, 0x68, 0x00, 0xf0, 0x88, 0x88, 0xf0, 0xa0, 0x90, 0x88, 0x00, 0x70,
|
||||
0x88, 0x80, 0x70, 0x08, 0x88, 0x70, 0x00, 0xf8, 0xa8, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x88,
|
||||
0x88, 0x88, 0x88, 0x88, 0x88, 0x70, 0x00, 0x88, 0x88, 0x88, 0x88, 0x88, 0x50, 0x20, 0x00, 0x88,
|
||||
0x88, 0x88, 0xa8, 0xa8, 0xa8, 0x50, 0x00, 0x88, 0x88, 0x50, 0x20, 0x50, 0x88, 0x88, 0x00, 0x88,
|
||||
0x88, 0x50, 0x20, 0x20, 0x20, 0x20, 0x00, 0xf8, 0x08, 0x10, 0x70, 0x40, 0x80, 0xf8, 0x00, 0x78,
|
||||
0x40, 0x40, 0x40, 0x40, 0x40, 0x78, 0x00, 0x00, 0x80, 0x40, 0x20, 0x10, 0x08, 0x00, 0x00, 0x78,
|
||||
0x08, 0x08, 0x08, 0x08, 0x08, 0x78, 0x00, 0x20, 0x50, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x60, 0x60, 0x20, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x60, 0x10, 0x70, 0x90, 0x78, 0x00, 0x80, 0x80, 0xb0, 0xc8, 0x88, 0xc8, 0xb0, 0x00, 0x00,
|
||||
0x00, 0x70, 0x88, 0x80, 0x88, 0x70, 0x00, 0x08, 0x08, 0x68, 0x98, 0x88, 0x98, 0x68, 0x00, 0x00,
|
||||
0x00, 0x70, 0x88, 0xf8, 0x80, 0x70, 0x00, 0x10, 0x28, 0x20, 0x70, 0x20, 0x20, 0x20, 0x00, 0x00,
|
||||
0x00, 0x70, 0x98, 0x98, 0x68, 0x08, 0x70, 0x80, 0x80, 0xb0, 0xc8, 0x88, 0x88, 0x88, 0x00, 0x20,
|
||||
0x00, 0x60, 0x20, 0x20, 0x20, 0x70, 0x00, 0x10, 0x00, 0x10, 0x10, 0x10, 0x90, 0x60, 0x00, 0x80,
|
||||
0x80, 0x90, 0xa0, 0xc0, 0xa0, 0x90, 0x00, 0x60, 0x20, 0x20, 0x20, 0x20, 0x20, 0x70, 0x00, 0x00,
|
||||
0x00, 0xd0, 0xa8, 0xa8, 0xa8, 0xa8, 0x00, 0x00, 0x00, 0xb0, 0xc8, 0x88, 0x88, 0x88, 0x00, 0x00,
|
||||
0x00, 0x70, 0x88, 0x88, 0x88, 0x70, 0x00, 0x00, 0x00, 0xb0, 0xc8, 0xc8, 0xb0, 0x80, 0x80, 0x00,
|
||||
0x00, 0x68, 0x98, 0x98, 0x68, 0x08, 0x08, 0x00, 0x00, 0xb0, 0xc8, 0x80, 0x80, 0x80, 0x00, 0x00,
|
||||
0x00, 0x78, 0x80, 0x70, 0x08, 0xf0, 0x00, 0x20, 0x20, 0xf8, 0x20, 0x20, 0x28, 0x10, 0x00, 0x00,
|
||||
0x00, 0x88, 0x88, 0x88, 0x98, 0x68, 0x00, 0x00, 0x00, 0x88, 0x88, 0x88, 0x50, 0x20, 0x00, 0x00,
|
||||
0x00, 0x88, 0x88, 0xa8, 0xa8, 0x50, 0x00, 0x00, 0x00, 0x88, 0x50, 0x20, 0x50, 0x88, 0x00, 0x00,
|
||||
0x00, 0x88, 0x88, 0x78, 0x08, 0x88, 0x70, 0x00, 0x00, 0xf8, 0x10, 0x20, 0x40, 0xf8, 0x00, 0x10,
|
||||
0x20, 0x20, 0x40, 0x20, 0x20, 0x10, 0x00, 0x20, 0x20, 0x20, 0x00, 0x20, 0x20, 0x20, 0x00, 0x40,
|
||||
0x20, 0x20, 0x10, 0x20, 0x20, 0x40, 0x00, 0x40, 0xa8, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
static const uint8_t FONT_FULL[] = {
|
||||
0x46, 0x44, 0x46, 0x31, 0x13, 0x02, 0x20, 0x5f, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x03, 0x00, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a,
|
||||
0x0a, 0x0a, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0xfc, 0xfc, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xe0, 0xe0,
|
||||
0x7f, 0x80, 0xff, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0,
|
||||
0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0,
|
||||
0xe1, 0xc0, 0xff, 0xc0, 0x7f, 0x80, 0x01, 0x80, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0,
|
||||
0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0,
|
||||
0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0x80, 0x7f, 0x80, 0xff, 0xc0,
|
||||
0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x7f, 0xc0, 0xff, 0xc0,
|
||||
0xff, 0x80, 0xe0, 0x00, 0xe0, 0x00, 0xe0, 0x00, 0xe0, 0x00, 0xe0, 0x00, 0xe0, 0x00, 0xff, 0xc0,
|
||||
0x7f, 0x80, 0x7f, 0x80, 0xff, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0,
|
||||
0x01, 0xc0, 0x7f, 0xc0, 0xff, 0xc0, 0x7f, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0,
|
||||
0x01, 0xc0, 0x01, 0xc0, 0xff, 0xc0, 0x7f, 0x80, 0x61, 0x80, 0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0,
|
||||
0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xff, 0xc0, 0xff, 0xc0, 0x7f, 0xc0, 0x01, 0xc0,
|
||||
0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0x80, 0x7f, 0x80,
|
||||
0xff, 0xc0, 0xe0, 0x00, 0xe0, 0x00, 0xe0, 0x00, 0xe0, 0x00, 0xe0, 0x00, 0xe0, 0x00, 0xff, 0x80,
|
||||
0xff, 0xc0, 0x7f, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0,
|
||||
0xff, 0xc0, 0x7f, 0x80, 0x7f, 0x80, 0xff, 0xc0, 0xe0, 0x00, 0xe0, 0x00, 0xe0, 0x00, 0xe0, 0x00,
|
||||
0xe0, 0x00, 0xe0, 0x00, 0xff, 0x80, 0xff, 0xc0, 0xff, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0,
|
||||
0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xff, 0xc0, 0x7f, 0x80, 0x7f, 0x80, 0xff, 0xc0, 0x01, 0xc0,
|
||||
0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0,
|
||||
0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0x80,
|
||||
0x7f, 0x80, 0xff, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0,
|
||||
0xff, 0xc0, 0xff, 0xc0, 0xff, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0,
|
||||
0xe1, 0xc0, 0xff, 0xc0, 0x7f, 0x80, 0x7f, 0x80, 0xff, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0,
|
||||
0xe1, 0xc0, 0xe1, 0xc0, 0xe1, 0xc0, 0xff, 0xc0, 0xff, 0xc0, 0x7f, 0xc0, 0x01, 0xc0, 0x01, 0xc0,
|
||||
0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0xff, 0xc0, 0x7f, 0x80, 0x00, 0x00, 0x00, 0x00,
|
||||
0xe0, 0xe0, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xe0, 0xe0, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
const BuiltinFont BUILTIN_FONTS[] = {
|
||||
{"Small", FONT_SMALL, sizeof(FONT_SMALL), 1},
|
||||
{"Normal", FONT_NORMAL, sizeof(FONT_NORMAL), 1},
|
||||
{"Large", FONT_NORMAL, sizeof(FONT_NORMAL), 2},
|
||||
{"Full height", FONT_FULL, sizeof(FONT_FULL), 1},
|
||||
};
|
||||
const int BUILTIN_FONT_COUNT = sizeof(BUILTIN_FONTS) / sizeof(BUILTIN_FONTS[0]);
|
||||
|
||||
const BuiltinFont *findBuiltinFont(const char *name)
|
||||
{
|
||||
for (int i = 0; i < BUILTIN_FONT_COUNT; i++)
|
||||
if (sameName(BUILTIN_FONTS[i].name, name))
|
||||
return &BUILTIN_FONTS[i];
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Font builtinFont(const BuiltinFont &font)
|
||||
{
|
||||
Font f;
|
||||
f.data = font.data;
|
||||
f.size = font.size;
|
||||
f.scale = font.scale;
|
||||
return f;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "logic/font.h"
|
||||
|
||||
// The built-in fonts of the dynamic pages (data in builtin_fonts.cpp, generated by
|
||||
// tools/make_builtin_fonts.py): "Small" (3x5), "Normal" (5x7, the old clock font),
|
||||
// "Large" (Normal x 2) and "Full height" (19 dot digits, only 0-9 : . - and space).
|
||||
|
||||
struct BuiltinFont
|
||||
{
|
||||
const char *name;
|
||||
const uint8_t *data;
|
||||
size_t size;
|
||||
uint8_t scale;
|
||||
};
|
||||
|
||||
extern const BuiltinFont BUILTIN_FONTS[];
|
||||
extern const int BUILTIN_FONT_COUNT;
|
||||
|
||||
#define FONT_NORMAL_NAME "Normal"
|
||||
|
||||
// nullptr when there is no built-in font with this name (compared without case)
|
||||
const BuiltinFont *findBuiltinFont(const char *name);
|
||||
Font builtinFont(const BuiltinFont &font);
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "logic/builtin_pages.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "logic/builtin_fonts.h"
|
||||
|
||||
static const char *const NAMES[] = {BUILTIN_PAGE_CLOCK};
|
||||
|
||||
int builtinPageCount()
|
||||
{
|
||||
return sizeof(NAMES) / sizeof(NAMES[0]);
|
||||
}
|
||||
|
||||
const char *builtinPageName(int index)
|
||||
{
|
||||
return index >= 0 && index < builtinPageCount() ? NAMES[index] : nullptr;
|
||||
}
|
||||
|
||||
static void clockPage(DynamicPage &page)
|
||||
{
|
||||
page = DynamicPage();
|
||||
page.slot_count = 2;
|
||||
page.slots[0].start = 0;
|
||||
strcpy(page.slots[0].bitmap, "Pikachu");
|
||||
page.slots[1].start = 17 * 60;
|
||||
strcpy(page.slots[1].bitmap, "Pikachu yawning");
|
||||
|
||||
// The same spot as the old clock: in the thought bubble
|
||||
page.element_count = 1;
|
||||
PageElement &clock = page.elements[0];
|
||||
clock.type = ElementType::Clock;
|
||||
clock.x = 46;
|
||||
clock.y = 4;
|
||||
strcpy(clock.font, FONT_NORMAL_NAME);
|
||||
}
|
||||
|
||||
bool builtinPage(const char *name, DynamicPage &page)
|
||||
{
|
||||
if (name && sameName(name, BUILTIN_PAGE_CLOCK))
|
||||
{
|
||||
clockPage(page);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "logic/page.h"
|
||||
|
||||
// The built-in dynamic pages. They can't be changed or deleted; the web UI saves an edited
|
||||
// copy under a new name.
|
||||
//
|
||||
// Clock Pikachu with the time in a thought bubble, yawning from 17:00 (the clock of
|
||||
// the old firmware; also the default page)
|
||||
|
||||
#define BUILTIN_PAGE_CLOCK "Clock"
|
||||
|
||||
int builtinPageCount();
|
||||
const char *builtinPageName(int index);
|
||||
// False when there is no built-in page with this name (compared without case)
|
||||
bool builtinPage(const char *name, DynamicPage &page);
|
||||
@@ -11,7 +11,7 @@ void DisplayController::setSettings(const ScheduleSettings &schedule, const Cloc
|
||||
{
|
||||
_schedule = schedule;
|
||||
_clock = clock;
|
||||
_content_revision++; // e.g. evening time changed: redraw
|
||||
_content_revision++; // e.g. the No WiFi setting changed: redraw
|
||||
}
|
||||
|
||||
void DisplayController::setEffectTime(uint32_t ms)
|
||||
@@ -58,7 +58,7 @@ bool DisplayController::powerEnabled() const
|
||||
return _power_enabled;
|
||||
}
|
||||
|
||||
void DisplayController::update(uint32_t now, bool time_valid, int hour, int minute, bool wifi_connected)
|
||||
void DisplayController::update(uint32_t now, bool time_valid, const LocalTime &time, bool wifi_connected)
|
||||
{
|
||||
if (wifi_connected)
|
||||
_wifi_lost = false;
|
||||
@@ -106,7 +106,7 @@ void DisplayController::update(uint32_t now, bool time_valid, int hour, int minu
|
||||
if (!time_valid)
|
||||
return;
|
||||
|
||||
switch (scheduleFor(hour, minute, _schedule))
|
||||
switch (scheduleFor(time.hour, time.minute, _schedule))
|
||||
{
|
||||
case ScheduleMode::Night:
|
||||
if (_power.isOn())
|
||||
@@ -118,8 +118,8 @@ void DisplayController::update(uint32_t now, bool time_valid, int hour, int minu
|
||||
case ScheduleMode::Clock:
|
||||
_off_timer.cancel();
|
||||
_ensureOn();
|
||||
if (_page == Page::Clock)
|
||||
_showClock(hour, minute, show_no_wifi);
|
||||
if (_page == Page::Dynamic)
|
||||
_showDynamic(time, show_no_wifi);
|
||||
else
|
||||
_showOnce(Screen::Page);
|
||||
break;
|
||||
@@ -139,21 +139,20 @@ void DisplayController::_ensureOn()
|
||||
_screen = Screen::Unknown;
|
||||
}
|
||||
|
||||
void DisplayController::_showClock(int hour, int minute, bool no_wifi)
|
||||
// Drawn again each minute (time, date and background can change) and when the content changed
|
||||
void DisplayController::_showDynamic(const LocalTime &time, bool no_wifi)
|
||||
{
|
||||
Screen screen = no_wifi ? Screen::NoWifi : Screen::Clock;
|
||||
bool evening = isEvening(hour, minute, _clock);
|
||||
if (_screen == screen && _shown_hour == hour && _shown_minute == minute && _shown_evening == evening &&
|
||||
Screen screen = no_wifi ? Screen::NoWifi : Screen::Dynamic;
|
||||
if (_screen == screen && _shown_hour == time.hour && _shown_minute == time.minute &&
|
||||
_shown_revision == _content_revision)
|
||||
return;
|
||||
if (no_wifi)
|
||||
_frames.noWifi(hour, minute);
|
||||
_frames.noWifi(time.hour, time.minute);
|
||||
else
|
||||
_frames.clock(hour, minute, evening);
|
||||
_frames.dynamicPage(time);
|
||||
_screen = screen;
|
||||
_shown_hour = hour;
|
||||
_shown_minute = minute;
|
||||
_shown_evening = evening;
|
||||
_shown_hour = time.hour;
|
||||
_shown_minute = time.minute;
|
||||
_shown_revision = _content_revision;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "logic/local_time.h"
|
||||
#include "logic/power_timer.h"
|
||||
#include "logic/settings.h"
|
||||
|
||||
@@ -19,8 +20,9 @@ public:
|
||||
class Frames
|
||||
{
|
||||
public:
|
||||
virtual void clock(int hour, int minute, bool evening) = 0;
|
||||
// Shown instead of the clock while WiFi is down (the time keeps running without it)
|
||||
// The dynamic page selected as page, drawn for this time
|
||||
virtual void dynamicPage(const LocalTime &time) = 0;
|
||||
// Shown instead of a dynamic page while WiFi is down (the time keeps running without it)
|
||||
virtual void noWifi(int hour, int minute) = 0;
|
||||
// The bitmap selected as page
|
||||
virtual void page() = 0;
|
||||
@@ -32,8 +34,8 @@ public:
|
||||
|
||||
enum class Page : uint8_t
|
||||
{
|
||||
Clock, // Pikachu with the time
|
||||
Bitmap, // a bitmap (FlipdotFrames holds which one)
|
||||
Dynamic, // a dynamic page, redrawn every minute (FlipdotFrames holds which one)
|
||||
Bitmap, // a bitmap (FlipdotFrames holds which one)
|
||||
};
|
||||
|
||||
// Decides what the display shows and when it is powered, based on the time, the settings,
|
||||
@@ -49,8 +51,8 @@ public:
|
||||
// The power stays on that much longer after the last frame.
|
||||
void setEffectTime(uint32_t ms);
|
||||
|
||||
// What is shown during the day. Call pageChanged() when the page or off bitmap
|
||||
// itself changed.
|
||||
// What is shown during the day. Call pageChanged() when the page, its content (e.g. a
|
||||
// Home Assistant value) or the off bitmap changed.
|
||||
void setPage(Page page);
|
||||
void pageChanged();
|
||||
|
||||
@@ -66,14 +68,14 @@ public:
|
||||
bool powerEnabled() const;
|
||||
|
||||
// Call often (every loop). time_valid is false until the time is synced over NTP.
|
||||
// While WiFi is down for NO_WIFI_SHOW_TIME, "No WiFi" is shown instead of the clock.
|
||||
void update(uint32_t now, bool time_valid, int hour, int minute, bool wifi_connected);
|
||||
// While WiFi is down for NO_WIFI_SHOW_TIME, "No WiFi" is shown instead of a dynamic page.
|
||||
void update(uint32_t now, bool time_valid, const LocalTime &time, bool wifi_connected);
|
||||
|
||||
private:
|
||||
enum class Screen
|
||||
{
|
||||
Unknown, // nothing sent since the last power on
|
||||
Clock,
|
||||
Dynamic,
|
||||
NoWifi,
|
||||
Page,
|
||||
Live,
|
||||
@@ -86,7 +88,7 @@ private:
|
||||
|
||||
ScheduleSettings _schedule;
|
||||
ClockSettings _clock;
|
||||
Page _page = Page::Clock;
|
||||
Page _page = Page::Dynamic;
|
||||
uint32_t _effect_time = 0;
|
||||
|
||||
bool _power_enabled = true;
|
||||
@@ -94,8 +96,7 @@ private:
|
||||
Screen _screen = Screen::Unknown;
|
||||
int _shown_hour = -1;
|
||||
int _shown_minute = -1;
|
||||
bool _shown_evening = false;
|
||||
uint32_t _content_revision = 0; // changed page, bitmap or live image
|
||||
uint32_t _content_revision = 0; // changed page, its content, bitmap or live image
|
||||
uint32_t _shown_revision = 0;
|
||||
|
||||
bool _live = false;
|
||||
@@ -105,6 +106,6 @@ private:
|
||||
uint32_t _wifi_lost_since = 0;
|
||||
|
||||
void _ensureOn();
|
||||
void _showClock(int hour, int minute, bool no_wifi);
|
||||
void _showDynamic(const LocalTime &time, bool no_wifi);
|
||||
void _showOnce(Screen screen);
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
#include "logic/font.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "logic/bitmap_format.h"
|
||||
|
||||
static int rowBytes(int width)
|
||||
{
|
||||
return (width + 7) / 8;
|
||||
}
|
||||
|
||||
bool validFont(const uint8_t *data, size_t size)
|
||||
{
|
||||
if (data == nullptr || size < (size_t)FONT_HEADER_SIZE || size > FONT_MAX_SIZE || memcmp(data, "FDF1", 4) != 0)
|
||||
return false;
|
||||
int height = data[4], spacing = data[5], first = data[6], count = data[7];
|
||||
if (height < 1 || height > FONT_MAX_HEIGHT || spacing > FONT_MAX_SPACING || count == 0 || first + count > 256)
|
||||
return false;
|
||||
size_t expected = FONT_HEADER_SIZE + count;
|
||||
if (size < expected)
|
||||
return false;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int width = data[FONT_HEADER_SIZE + i];
|
||||
if (width > FONT_MAX_WIDTH)
|
||||
return false;
|
||||
expected += (size_t)height * rowBytes(width);
|
||||
}
|
||||
return size == expected;
|
||||
}
|
||||
|
||||
// The width and dots of a character; false when it is not in the font
|
||||
static bool glyph(const Font &font, unsigned char c, int &width, const uint8_t *&dots)
|
||||
{
|
||||
const uint8_t *d = font.data;
|
||||
int height = d[4], first = d[6], count = d[7];
|
||||
if (c < first || c >= first + count)
|
||||
return false;
|
||||
const uint8_t *p = d + FONT_HEADER_SIZE + count;
|
||||
for (int i = 0; i < c - first; i++)
|
||||
p += height * rowBytes(d[FONT_HEADER_SIZE + i]);
|
||||
width = d[FONT_HEADER_SIZE + c - first];
|
||||
dots = p;
|
||||
return width > 0;
|
||||
}
|
||||
|
||||
static bool usable(const Font &font)
|
||||
{
|
||||
return font.data && font.size >= (size_t)FONT_HEADER_SIZE && font.scale >= 1;
|
||||
}
|
||||
|
||||
int fontHeight(const Font &font)
|
||||
{
|
||||
return usable(font) ? font.data[4] * font.scale : 0;
|
||||
}
|
||||
|
||||
int textWidth(const Font &font, const char *text)
|
||||
{
|
||||
if (!usable(font) || text == nullptr)
|
||||
return 0;
|
||||
int total = 0, characters = 0;
|
||||
for (const char *c = text; *c; c++)
|
||||
{
|
||||
int width;
|
||||
const uint8_t *dots;
|
||||
if (!glyph(font, (unsigned char)*c, width, dots))
|
||||
continue;
|
||||
total += width;
|
||||
characters++;
|
||||
}
|
||||
if (characters > 1)
|
||||
total += (characters - 1) * font.data[5];
|
||||
return total * font.scale;
|
||||
}
|
||||
|
||||
static void drawDot(uint8_t *bitmap, int x, int y, DrawMode mode)
|
||||
{
|
||||
if (x < 0 || y < 0 || x >= BITMAP_WIDTH || y >= BITMAP_HEIGHT)
|
||||
return;
|
||||
bool on = mode == DrawMode::Invert ? !bitmapPixel(bitmap, x, y) : true;
|
||||
setBitmapPixel(bitmap, x, y, on);
|
||||
}
|
||||
|
||||
void drawText(uint8_t *bitmap, const Font &font, int x, int y, const char *text, DrawMode mode)
|
||||
{
|
||||
if (!usable(font) || text == nullptr)
|
||||
return;
|
||||
int height = font.data[4], spacing = font.data[5], scale = font.scale;
|
||||
bool first = true;
|
||||
for (const char *c = text; *c; c++)
|
||||
{
|
||||
int width;
|
||||
const uint8_t *dots;
|
||||
if (!glyph(font, (unsigned char)*c, width, dots))
|
||||
continue;
|
||||
if (!first)
|
||||
x += spacing * scale;
|
||||
first = false;
|
||||
for (int row = 0; row < height; row++)
|
||||
for (int col = 0; col < width; col++)
|
||||
{
|
||||
if (!(dots[row * rowBytes(width) + col / 8] & (0x80 >> (col & 7))))
|
||||
continue;
|
||||
for (int dy = 0; dy < scale; dy++)
|
||||
for (int dx = 0; dx < scale; dx++)
|
||||
drawDot(bitmap, x + col * scale + dx, y + row * scale + dy, mode);
|
||||
}
|
||||
x += width * scale;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// Dot 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). Pure logic, tested in
|
||||
// test/test_esp32.
|
||||
//
|
||||
// Font file format "FDF1" (the web UI writes the same format):
|
||||
// 0-3 "FDF1"
|
||||
// 4 height in dots (1-19)
|
||||
// 5 spacing: empty columns between two characters (0-4)
|
||||
// 6 first character code
|
||||
// 7 number of characters
|
||||
// 8... the width of each character (0-32, 0 = not in the font: skipped)
|
||||
// then the dots of each character: `height` rows, (width + 7) / 8 bytes per row, the
|
||||
// most significant bit is the leftmost dot (like the bitmaps)
|
||||
|
||||
const int FONT_HEADER_SIZE = 8;
|
||||
const int FONT_MAX_HEIGHT = 19;
|
||||
const int FONT_MAX_WIDTH = 32;
|
||||
const int FONT_MAX_SPACING = 4;
|
||||
const size_t FONT_MAX_SIZE = 8192;
|
||||
|
||||
struct Font
|
||||
{
|
||||
const uint8_t *data = nullptr;
|
||||
size_t size = 0;
|
||||
uint8_t scale = 1; // each dot drawn as scale x scale dots ("Large" is "Normal" x 2)
|
||||
};
|
||||
|
||||
// Checks the header and that the size matches the widths
|
||||
bool validFont(const uint8_t *data, size_t size);
|
||||
|
||||
// Height in dots (scaled), 0 for an invalid font
|
||||
int fontHeight(const Font &font);
|
||||
// Width of the text in dots (scaled), without spacing after the last character
|
||||
int textWidth(const Font &font, const char *text);
|
||||
|
||||
enum class DrawMode : uint8_t
|
||||
{
|
||||
Set, // the dots of the text are switched on
|
||||
Invert, // the dots of the text flip what is below them
|
||||
};
|
||||
|
||||
// Draws the text with its top left corner at x, y into a full screen bitmap
|
||||
// (logic/bitmap_format.h). Dots outside the screen are skipped.
|
||||
void drawText(uint8_t *bitmap, const Font &font, int x, int y, const char *text, DrawMode mode);
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
// The local date and time, as far as the display needs it (from struct tm). Pure logic.
|
||||
struct LocalTime
|
||||
{
|
||||
int year = 2000;
|
||||
int month = 1; // 1-12
|
||||
int day = 1; // 1-31
|
||||
int weekday = 6; // 0 = Sunday
|
||||
int hour = 0;
|
||||
int minute = 0;
|
||||
};
|
||||
@@ -0,0 +1,203 @@
|
||||
#include "logic/page.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static const uint16_t MINUTES_PER_DAY = 24 * 60;
|
||||
|
||||
static bool terminated(const char *text, size_t size)
|
||||
{
|
||||
return memchr(text, '\0', size) != nullptr;
|
||||
}
|
||||
|
||||
// Printable ASCII only: the fonts have no other characters
|
||||
static bool printable(const char *text, size_t size)
|
||||
{
|
||||
if (!terminated(text, size))
|
||||
return false;
|
||||
for (const char *c = text; *c; c++)
|
||||
if (*c < 32 || *c > 126)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool validEntityId(const char *entity)
|
||||
{
|
||||
if (entity == nullptr)
|
||||
return false;
|
||||
const char *dot = strchr(entity, '.');
|
||||
if (dot == nullptr || dot == entity || dot[1] == '\0' || strchr(dot + 1, '.'))
|
||||
return false;
|
||||
for (const char *c = entity; *c; c++)
|
||||
if (c != dot && !islower((unsigned char)*c) && !isdigit((unsigned char)*c) && *c != '_')
|
||||
return false;
|
||||
return strlen(entity) < (size_t)ELEMENT_ENTITY_SIZE;
|
||||
}
|
||||
|
||||
static bool validAttribute(const char *attribute)
|
||||
{
|
||||
for (const char *c = attribute; *c; c++)
|
||||
if (!isalnum((unsigned char)*c) && *c != '_')
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const char *validatePage(const DynamicPage &page)
|
||||
{
|
||||
if (page.slot_count > PAGE_SLOTS)
|
||||
return "Too many time slots";
|
||||
if (page.element_count > PAGE_ELEMENTS)
|
||||
return "Too many elements";
|
||||
|
||||
for (int i = 0; i < page.slot_count; i++)
|
||||
{
|
||||
const PageSlot &slot = page.slots[i];
|
||||
if (slot.start >= MINUTES_PER_DAY)
|
||||
return "Time slots must start before 24:00";
|
||||
if (i > 0 && slot.start <= page.slots[i - 1].start)
|
||||
return "Time slots must be in order, each at a different time";
|
||||
if (!terminated(slot.bitmap, sizeof(slot.bitmap)) || (slot.bitmap[0] && !validBitmapName(slot.bitmap)))
|
||||
return "Invalid background bitmap name";
|
||||
}
|
||||
|
||||
for (int i = 0; i < page.element_count; i++)
|
||||
{
|
||||
const PageElement &e = page.elements[i];
|
||||
if (e.type >= ElementType::Count || e.align >= Align::Count || e.overlay >= Overlay::Count ||
|
||||
e.language >= Language::Count)
|
||||
return "Unknown element setting";
|
||||
if (e.x < -BITMAP_WIDTH || e.x > 2 * BITMAP_WIDTH || e.y < -BITMAP_HEIGHT || e.y > BITMAP_HEIGHT)
|
||||
return "Element position out of range";
|
||||
if (!terminated(e.font, sizeof(e.font)) || !validBitmapName(e.font))
|
||||
return "Invalid font name";
|
||||
if (!printable(e.text, sizeof(e.text)) || !printable(e.suffix, sizeof(e.suffix)))
|
||||
return "Texts can only contain plain characters (no accents), up to 32";
|
||||
if (!terminated(e.entity, sizeof(e.entity)) || !terminated(e.attribute, sizeof(e.attribute)) ||
|
||||
!validAttribute(e.attribute))
|
||||
return "Invalid entity or attribute";
|
||||
if (e.decimals < DECIMALS_AS_IS || e.decimals > DECIMALS_MAX)
|
||||
return "Decimals must be 0-4, or as received";
|
||||
if (e.type == ElementType::Clock && e.format >= CLOCK_FORMAT_COUNT)
|
||||
return "Unknown clock format";
|
||||
if (e.type == ElementType::Date && e.format >= DATE_FORMAT_COUNT)
|
||||
return "Unknown date format";
|
||||
if (e.type == ElementType::HaValue && !validEntityId(e.entity))
|
||||
return "Home Assistant values need an entity id like sensor.outside_temperature";
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const PageSlot *activeSlot(const DynamicPage &page, int minute_of_day)
|
||||
{
|
||||
if (page.slot_count == 0)
|
||||
return nullptr;
|
||||
const PageSlot *active = &page.slots[page.slot_count - 1];
|
||||
for (int i = 0; i < page.slot_count; i++)
|
||||
if (page.slots[i].start <= minute_of_day)
|
||||
active = &page.slots[i];
|
||||
return active;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Texts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static const char *const WEEKDAYS[][7] = {
|
||||
{"zondag", "maandag", "dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag"},
|
||||
{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
|
||||
};
|
||||
static const char *const WEEKDAYS_SHORT[][7] = {
|
||||
{"zo", "ma", "di", "wo", "do", "vr", "za"},
|
||||
{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"},
|
||||
};
|
||||
static const char *const MONTHS[][12] = {
|
||||
{"januari", "februari", "maart", "april", "mei", "juni", "juli", "augustus", "september", "oktober",
|
||||
"november", "december"},
|
||||
{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October",
|
||||
"November", "December"},
|
||||
};
|
||||
static const char *const MONTHS_SHORT[][12] = {
|
||||
{"jan", "feb", "mrt", "apr", "mei", "jun", "jul", "aug", "sep", "okt", "nov", "dec"},
|
||||
{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"},
|
||||
};
|
||||
|
||||
static void formatDate(const PageElement &e, const LocalTime &t, char *out, size_t size)
|
||||
{
|
||||
int lang = e.language < Language::Count ? (int)e.language : 0;
|
||||
int wd = t.weekday >= 0 && t.weekday < 7 ? t.weekday : 0;
|
||||
int mo = t.month >= 1 && t.month <= 12 ? t.month - 1 : 0;
|
||||
switch (e.format)
|
||||
{
|
||||
case DATE_WEEKDAY_DAY_MONTH:
|
||||
snprintf(out, size, "%s %d %s", WEEKDAYS_SHORT[lang][wd], t.day, MONTHS_SHORT[lang][mo]);
|
||||
break;
|
||||
case DATE_DAY_MONTH:
|
||||
snprintf(out, size, "%d %s", t.day, MONTHS_SHORT[lang][mo]);
|
||||
break;
|
||||
case DATE_DAY_MONTH_LONG:
|
||||
snprintf(out, size, "%d %s", t.day, MONTHS[lang][mo]);
|
||||
break;
|
||||
case DATE_NUMERIC:
|
||||
snprintf(out, size, "%02d-%02d", t.day, t.month);
|
||||
break;
|
||||
case DATE_NUMERIC_YEAR:
|
||||
snprintf(out, size, "%02d-%02d-%04d", t.day, t.month, t.year);
|
||||
break;
|
||||
case DATE_WEEKDAY:
|
||||
snprintf(out, size, "%s", WEEKDAYS[lang][wd]);
|
||||
break;
|
||||
default:
|
||||
snprintf(out, size, "%s", WEEKDAYS_SHORT[lang][wd]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Rounded to the element's decimals when it is a number; "--" when unknown
|
||||
static void formatHaValue(const PageElement &e, const char *value, char *out, size_t size)
|
||||
{
|
||||
if (value == nullptr || value[0] == '\0' || strcmp(value, "unavailable") == 0 || strcmp(value, "unknown") == 0)
|
||||
value = "--";
|
||||
char number[24];
|
||||
if (e.decimals >= 0 && strcmp(value, "--") != 0)
|
||||
{
|
||||
char *end;
|
||||
double v = strtod(value, &end);
|
||||
if (end != value && *end == '\0')
|
||||
{
|
||||
snprintf(number, sizeof(number), "%.*f", e.decimals, v);
|
||||
if (strcmp(number, "-0") == 0 || strncmp(number, "-0.", 3) == 0)
|
||||
{
|
||||
// -0.0 after rounding: no minus sign
|
||||
bool zero = true;
|
||||
for (const char *c = number + 1; *c; c++)
|
||||
if (*c != '0' && *c != '.')
|
||||
zero = false;
|
||||
if (zero)
|
||||
memmove(number, number + 1, strlen(number));
|
||||
}
|
||||
value = number;
|
||||
}
|
||||
}
|
||||
snprintf(out, size, "%s%s%s", e.text, value, e.suffix);
|
||||
}
|
||||
|
||||
void elementText(const PageElement &e, const LocalTime &t, const char *ha_value, char *out, size_t size)
|
||||
{
|
||||
switch (e.type)
|
||||
{
|
||||
case ElementType::Clock:
|
||||
snprintf(out, size, e.format == CLOCK_H_MM ? "%d:%02d" : "%02d:%02d", t.hour, t.minute);
|
||||
break;
|
||||
case ElementType::Date:
|
||||
formatDate(e, t, out, size);
|
||||
break;
|
||||
case ElementType::HaValue:
|
||||
formatHaValue(e, ha_value, out, size);
|
||||
break;
|
||||
default:
|
||||
snprintf(out, size, "%s", e.text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "logic/bitmap_format.h"
|
||||
#include "logic/local_time.h"
|
||||
|
||||
// Dynamic pages: a background bitmap that depends on the time of day, with elements
|
||||
// (clock, date, Home Assistant value, text) drawn on top. Stored as a binary struct on
|
||||
// LittleFS (display/page_store), edited in the web UI. Pure logic, tested in
|
||||
// test/test_esp32.
|
||||
|
||||
const int PAGE_NAME_SIZE = BITMAP_NAME_MAX + 1;
|
||||
const int PAGE_SLOTS = 8;
|
||||
const int PAGE_ELEMENTS = 8;
|
||||
const int ELEMENT_TEXT_SIZE = 33;
|
||||
const int ELEMENT_SUFFIX_SIZE = 17;
|
||||
const int ELEMENT_ENTITY_SIZE = 65;
|
||||
const int ELEMENT_ATTRIBUTE_SIZE = 33;
|
||||
|
||||
enum class ElementType : uint8_t
|
||||
{
|
||||
Clock,
|
||||
Date,
|
||||
HaValue, // a Home Assistant entity state (or attribute)
|
||||
Text,
|
||||
Count,
|
||||
};
|
||||
|
||||
// How an element is drawn over the background
|
||||
enum class Overlay : uint8_t
|
||||
{
|
||||
OnTop, // its dots are switched on, the background stays
|
||||
Box, // its area (plus one dot around it) is cleared first
|
||||
Invert, // its dots flip the background
|
||||
Count,
|
||||
};
|
||||
|
||||
// What x means: the left edge, the middle or the right edge of the element
|
||||
enum class Align : uint8_t
|
||||
{
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
Count,
|
||||
};
|
||||
|
||||
enum class Language : uint8_t
|
||||
{
|
||||
Dutch,
|
||||
English,
|
||||
Count,
|
||||
};
|
||||
|
||||
// Clock formats
|
||||
const uint8_t CLOCK_HH_MM = 0; // 08:05
|
||||
const uint8_t CLOCK_H_MM = 1; // 8:05
|
||||
const uint8_t CLOCK_FORMAT_COUNT = 2;
|
||||
|
||||
// Date formats (Dutch / English examples)
|
||||
const uint8_t DATE_WEEKDAY_DAY_MONTH = 0; // do 15 okt / Thu 15 Oct
|
||||
const uint8_t DATE_DAY_MONTH = 1; // 15 okt / 15 Oct
|
||||
const uint8_t DATE_DAY_MONTH_LONG = 2; // 15 oktober / 15 October
|
||||
const uint8_t DATE_NUMERIC = 3; // 15-10
|
||||
const uint8_t DATE_NUMERIC_YEAR = 4; // 15-10-2026
|
||||
const uint8_t DATE_WEEKDAY = 5; // donderdag / Thursday
|
||||
const uint8_t DATE_WEEKDAY_SHORT = 6; // do / Thu
|
||||
const uint8_t DATE_FORMAT_COUNT = 7;
|
||||
|
||||
// Home Assistant values: decimals as received
|
||||
const int8_t DECIMALS_AS_IS = -1;
|
||||
const int8_t DECIMALS_MAX = 4;
|
||||
|
||||
// Background from `start` (minutes since midnight) until the next slot
|
||||
struct PageSlot
|
||||
{
|
||||
uint16_t start = 0;
|
||||
char bitmap[PAGE_NAME_SIZE] = ""; // empty: blank
|
||||
};
|
||||
|
||||
struct PageElement
|
||||
{
|
||||
ElementType type = ElementType::Clock;
|
||||
int16_t x = 0; // may be partly off screen
|
||||
int16_t y = 0;
|
||||
Align align = Align::Left;
|
||||
Overlay overlay = Overlay::OnTop;
|
||||
char font[PAGE_NAME_SIZE] = "Normal";
|
||||
uint8_t format = 0; // CLOCK_* or DATE_*
|
||||
Language language = Language::Dutch;
|
||||
int8_t decimals = DECIMALS_AS_IS;
|
||||
char text[ELEMENT_TEXT_SIZE] = ""; // Text: the text; HaValue: shown before the value
|
||||
char suffix[ELEMENT_SUFFIX_SIZE] = ""; // HaValue: shown after the value (unit)
|
||||
char entity[ELEMENT_ENTITY_SIZE] = ""; // HaValue: e.g. sensor.outside_temperature
|
||||
char attribute[ELEMENT_ATTRIBUTE_SIZE] = ""; // HaValue: empty = the state
|
||||
};
|
||||
|
||||
struct DynamicPage
|
||||
{
|
||||
uint8_t slot_count = 0;
|
||||
PageSlot slots[PAGE_SLOTS];
|
||||
uint8_t element_count = 0;
|
||||
PageElement elements[PAGE_ELEMENTS];
|
||||
};
|
||||
|
||||
// Returns nullptr when the page is fine, otherwise a message for the user. Does not check
|
||||
// that the bitmaps and fonts exist.
|
||||
const char *validatePage(const DynamicPage &page);
|
||||
|
||||
// The slot shown at this time: the last one that started, or the last slot of the day
|
||||
// before when it is earlier than the first one. nullptr without slots.
|
||||
const PageSlot *activeSlot(const DynamicPage &page, int minute_of_day);
|
||||
|
||||
// The text of an element. ha_value is the entity state (nullptr: not known yet, shown as
|
||||
// "--"). out_size must be at least 2.
|
||||
void elementText(const PageElement &element, const LocalTime &time, const char *ha_value, char *out,
|
||||
size_t out_size);
|
||||
|
||||
// Entity ids look like "sensor.outside_temperature" (lower case, digits, '_', one '.')
|
||||
bool validEntityId(const char *entity);
|
||||
@@ -0,0 +1,58 @@
|
||||
#include "logic/page_render.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "logic/bitmap_format.h"
|
||||
#include "logic/builtin_fonts.h"
|
||||
|
||||
static Font usableFont(PageResources &resources, const char *name)
|
||||
{
|
||||
Font font = resources.font(name);
|
||||
if (validFont(font.data, font.size) && font.scale >= 1)
|
||||
return font;
|
||||
return builtinFont(*findBuiltinFont(FONT_NORMAL_NAME));
|
||||
}
|
||||
|
||||
static void clearBox(uint8_t *bitmap, int x, int y, int width, int height)
|
||||
{
|
||||
for (int yy = y; yy < y + height; yy++)
|
||||
for (int xx = x; xx < x + width; xx++)
|
||||
setBitmapPixel(bitmap, xx, yy, false);
|
||||
}
|
||||
|
||||
void renderPage(const DynamicPage &page, const LocalTime &time, PageResources &resources, uint8_t *bitmap,
|
||||
ElementBox *boxes)
|
||||
{
|
||||
memset(bitmap, 0, BITMAP_BYTES);
|
||||
const PageSlot *slot = activeSlot(page, time.hour * 60 + time.minute);
|
||||
if (slot && slot->bitmap[0] && !resources.background(slot->bitmap, bitmap))
|
||||
memset(bitmap, 0, BITMAP_BYTES);
|
||||
|
||||
for (int i = 0; i < page.element_count && i < PAGE_ELEMENTS; i++)
|
||||
{
|
||||
const PageElement &e = page.elements[i];
|
||||
const char *ha_value = e.type == ElementType::HaValue ? resources.haValue(e) : nullptr;
|
||||
char text[64];
|
||||
elementText(e, time, ha_value, text, sizeof(text));
|
||||
|
||||
Font font = usableFont(resources, e.font);
|
||||
int width = textWidth(font, text), height = fontHeight(font);
|
||||
int x = e.x;
|
||||
if (e.align == Align::Center)
|
||||
x -= width / 2;
|
||||
else if (e.align == Align::Right)
|
||||
x -= width;
|
||||
|
||||
if (e.overlay == Overlay::Box)
|
||||
clearBox(bitmap, x - 1, e.y - 1, width + 2, height + 2);
|
||||
drawText(bitmap, font, x, e.y, text, e.overlay == Overlay::Invert ? DrawMode::Invert : DrawMode::Set);
|
||||
|
||||
if (boxes)
|
||||
{
|
||||
boxes[i].x = x;
|
||||
boxes[i].y = e.y;
|
||||
boxes[i].width = width;
|
||||
boxes[i].height = height;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "logic/font.h"
|
||||
#include "logic/local_time.h"
|
||||
#include "logic/page.h"
|
||||
|
||||
// Draws a dynamic page into a full screen bitmap (logic/bitmap_format.h). Used for the
|
||||
// display and for the previews in the web UI. Pure logic, tested in test/test_esp32.
|
||||
|
||||
// Where the page gets its bitmaps, fonts and Home Assistant values
|
||||
class PageResources
|
||||
{
|
||||
public:
|
||||
// Loads a background bitmap; false when it is gone (the background is blank then)
|
||||
virtual bool background(const char *name, uint8_t *bitmap) = 0;
|
||||
// A font by name; a font without data when it is gone ("Normal" is used then)
|
||||
virtual Font font(const char *name) = 0;
|
||||
// The current value of a Home Assistant element; nullptr when not known
|
||||
virtual const char *haValue(const PageElement &element) = 0;
|
||||
};
|
||||
|
||||
// The area an element covers on the screen (for the editor), may be partly off screen
|
||||
struct ElementBox
|
||||
{
|
||||
int16_t x = 0;
|
||||
int16_t y = 0;
|
||||
int16_t width = 0;
|
||||
int16_t height = 0;
|
||||
};
|
||||
|
||||
// boxes: nullptr, or room for page.element_count boxes
|
||||
void renderPage(const DynamicPage &page, const LocalTime &time, PageResources &resources, uint8_t *bitmap,
|
||||
ElementBox *boxes = nullptr);
|
||||
@@ -12,8 +12,3 @@ ScheduleMode scheduleFor(int hour, int minute, const ScheduleSettings &s)
|
||||
return ScheduleMode::Clock;
|
||||
return ScheduleMode::Standby;
|
||||
}
|
||||
|
||||
bool isEvening(int hour, int minute, const ClockSettings &s)
|
||||
{
|
||||
return s.evening_enabled && hour * 60 + minute >= s.evening_start;
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
// settings:
|
||||
//
|
||||
// 00:00 - 07:59 Night screen cleared, power off
|
||||
// 08:00 - 21:59 Clock page shown (the clock is updated every minute)
|
||||
// 08:00 - 21:59 Clock page shown (a dynamic page is updated every minute)
|
||||
// 22:00 - 23:30 Clock only during the minutes :00, :15, :30 and :45
|
||||
// Standby in between: power off, the last image stays on the dots
|
||||
// 23:31 - 23:59 Night
|
||||
@@ -19,6 +19,3 @@ enum class ScheduleMode
|
||||
};
|
||||
|
||||
ScheduleMode scheduleFor(int hour, int minute, const ScheduleSettings &settings);
|
||||
|
||||
// Evening: the yawning Pikachu is shown
|
||||
bool isEvening(int hour, int minute, const ClockSettings &settings);
|
||||
@@ -12,8 +12,7 @@ static bool terminated(const char *text, int size)
|
||||
|
||||
const char *validateSettings(const Settings &s)
|
||||
{
|
||||
if (!terminated(s.page, sizeof(s.page)) ||
|
||||
(strcmp(s.page, PAGE_CLOCK) != 0 && !validBitmapName(s.page)))
|
||||
if (!terminated(s.page, sizeof(s.page)) || !validBitmapName(s.page))
|
||||
return "Unknown page";
|
||||
if (!terminated(s.off_page, sizeof(s.off_page)) || (s.off_page[0] && !validBitmapName(s.off_page)))
|
||||
return "Unknown off image";
|
||||
@@ -24,9 +23,6 @@ const char *validateSettings(const Settings &s)
|
||||
if (t.day_start > t.late_start || t.late_start > t.night_start)
|
||||
return "The times must be in order: day start, late evening, night";
|
||||
|
||||
if (s.clock.evening_start >= MINUTES_PER_DAY)
|
||||
return "The evening must start before 24:00";
|
||||
|
||||
if (s.flip_time < SETTINGS_FLIP_TIME_MIN || s.flip_time > SETTINGS_FLIP_TIME_MAX)
|
||||
return "The flip time must be between 50 and 2000 us";
|
||||
|
||||
@@ -42,10 +38,13 @@ const char *validateSettings(const Settings &s)
|
||||
|
||||
const HomeAssistantSettings &ha = s.home_assistant;
|
||||
if (!terminated(ha.host, sizeof(ha.host)) || !terminated(ha.user, sizeof(ha.user)) ||
|
||||
!terminated(ha.password, sizeof(ha.password)))
|
||||
!terminated(ha.password, sizeof(ha.password)) || !terminated(ha.api_url, sizeof(ha.api_url)) ||
|
||||
!terminated(ha.token, sizeof(ha.token)))
|
||||
return "Home Assistant text too long";
|
||||
if (ha.port == 0)
|
||||
return "Invalid MQTT port";
|
||||
if (ha.api_url[0] && strncmp(ha.api_url, "http://", 7) != 0)
|
||||
return "The Home Assistant URL must start with http:// (https is not supported)";
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
+12
-10
@@ -2,6 +2,9 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "logic/builtin_pages.h"
|
||||
#include "logic/page.h"
|
||||
|
||||
// Everything that can be changed in the web UI. Stored in NVS (system/settings_store),
|
||||
// the defaults are the original behaviour. Pure logic, tested in test/test_esp32.
|
||||
//
|
||||
@@ -18,9 +21,7 @@ struct ScheduleSettings
|
||||
|
||||
struct ClockSettings
|
||||
{
|
||||
bool evening_enabled = true; // yawning Pikachu in the evening
|
||||
uint16_t evening_start = 17 * 60;
|
||||
bool no_wifi_screen = true; // "No WiFi" instead of the clock when WiFi is down
|
||||
bool no_wifi_screen = true; // "No WiFi" instead of a dynamic page when WiFi is down
|
||||
};
|
||||
|
||||
struct HomeAssistantSettings
|
||||
@@ -30,6 +31,9 @@ struct HomeAssistantSettings
|
||||
uint16_t port = 1883;
|
||||
char user[32] = "";
|
||||
char password[64] = "";
|
||||
// Values for the dynamic pages, read over Home Assistant's REST API (net/ha_api)
|
||||
char api_url[96] = ""; // empty: http://<MQTT broker>:8123
|
||||
char token[256] = ""; // long-lived access token (HA user profile, Security)
|
||||
};
|
||||
|
||||
// How the Nano drives the dots (besides the flip time). Sent to the Nano, which keeps
|
||||
@@ -50,25 +54,23 @@ struct DriveSettings
|
||||
bool effect_small_only = false; // big changes (a new page) appear instantly
|
||||
};
|
||||
|
||||
// What is shown during the day: "clock" or the name of a bitmap
|
||||
const int PAGE_NAME_SIZE = 25;
|
||||
#define PAGE_CLOCK "clock"
|
||||
// What is shown during the day: the name of a dynamic page (logic/page.h) or a bitmap.
|
||||
// Both share one namespace. When it is gone, the built-in Clock page is shown.
|
||||
#define DEFAULT_PAGE BUILTIN_PAGE_CLOCK
|
||||
// What is left on the dots when the display switches off: empty = blank, or a bitmap name
|
||||
#define OFF_PAGE_BLANK ""
|
||||
|
||||
struct Settings
|
||||
{
|
||||
char page[PAGE_NAME_SIZE] = PAGE_CLOCK;
|
||||
char page[PAGE_NAME_SIZE] = DEFAULT_PAGE;
|
||||
ScheduleSettings schedule;
|
||||
ClockSettings clock;
|
||||
HomeAssistantSettings home_assistant;
|
||||
uint16_t flip_time = 225; // flip pulse length in us, sent to the Nano
|
||||
bool status_led = true;
|
||||
// Added in settings version 2: keep it last, the fields before it are read from
|
||||
// version 1 records (system/settings_store.cpp)
|
||||
char off_page[PAGE_NAME_SIZE] = OFF_PAGE_BLANK;
|
||||
// Added in version 3
|
||||
DriveSettings drive;
|
||||
// New fields go here, at the end (see system/settings_store.cpp)
|
||||
};
|
||||
|
||||
// How long a new frame can take to appear on the dots in ms (0 when instant)
|
||||
|
||||
Reference in new issue
Block a user