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
@@ -0,0 +1,204 @@
|
||||
"""Generates src/esp32/logic/builtin_fonts.cpp: the built-in fonts of the dynamic pages.
|
||||
|
||||
python tools/make_builtin_fonts.py
|
||||
|
||||
Needs the Adafruit GFX library in .pio/libdeps/esp32 (run `pio run -e esp32` once):
|
||||
Small TomThumb 3x5 (Fonts/TomThumb.h, BSD license, see that file)
|
||||
Normal the classic 5x7 font (glcdfont.c), the font the clock always used
|
||||
Full height digits drawn below (seven segments with rounded corners, 19 dots high)
|
||||
"Large" is Normal drawn twice as big, so it has no data of its own.
|
||||
|
||||
The font format is described in src/esp32/logic/font.h. Only the Python standard
|
||||
library is used.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
GFX = os.path.join(ROOT, ".pio", "libdeps", "esp32", "Adafruit GFX Library")
|
||||
OUT = os.path.join(ROOT, "src", "esp32", "logic", "builtin_fonts.cpp")
|
||||
|
||||
FIRST = 32
|
||||
LAST = 126
|
||||
|
||||
|
||||
def numbers(text):
|
||||
return [int(v, 0) for v in re.findall(r"0x[0-9A-Fa-f]+|-?\d+", text)]
|
||||
|
||||
|
||||
def array_body(source, name):
|
||||
match = re.search(name + r"\[\]\s*PROGMEM\s*=\s*\{(.*?)\};", source, re.S)
|
||||
if not match:
|
||||
sys.exit("Not found: " + name)
|
||||
# drop comments, they contain numbers too
|
||||
return re.sub(r"/\*.*?\*/|//[^\n]*", "", match.group(1), flags=re.S)
|
||||
|
||||
|
||||
def encode(name, height, spacing, glyphs):
|
||||
"""glyphs: list of (width, rows) per character FIRST..LAST, rows = list of strings"""
|
||||
data = bytearray(b"FDF1")
|
||||
data += bytes([height, spacing, FIRST, len(glyphs)])
|
||||
data += bytes(width for width, _ in glyphs)
|
||||
for width, rows in glyphs:
|
||||
assert len(rows) == height, name
|
||||
for row in rows:
|
||||
for start in range(0, width, 8):
|
||||
byte = 0
|
||||
for bit in range(8):
|
||||
x = start + bit
|
||||
if x < width and row[x] == "#":
|
||||
byte |= 0x80 >> bit
|
||||
data.append(byte)
|
||||
return bytes(data)
|
||||
|
||||
|
||||
def classic():
|
||||
source = open(os.path.join(GFX, "glcdfont.c")).read()
|
||||
font = numbers(array_body(source, "font"))
|
||||
glyphs = []
|
||||
for c in range(FIRST, LAST + 1):
|
||||
columns = font[c * 5 : c * 5 + 5]
|
||||
rows = ["".join("#" if columns[x] >> y & 1 else "." for x in range(5)) for y in range(8)]
|
||||
glyphs.append((5, rows))
|
||||
return encode("Normal", 8, 1, glyphs)
|
||||
|
||||
|
||||
def tom_thumb():
|
||||
source = open(os.path.join(GFX, "Fonts", "TomThumb.h")).read()
|
||||
bitmaps = numbers(array_body(source, "TomThumbBitmaps"))
|
||||
table = numbers(array_body(source, "TomThumbGlyphs"))
|
||||
glyphs = []
|
||||
for i, c in enumerate(range(FIRST, LAST + 1)):
|
||||
offset, w, h, advance, x_offset, y_offset = table[i * 6 : i * 6 + 6]
|
||||
width = max(advance - 1, 1)
|
||||
rows = [["."] * width for _ in range(6)]
|
||||
bit = 0
|
||||
for y in range(h):
|
||||
for x in range(w):
|
||||
byte = bitmaps[offset + bit // 8]
|
||||
if byte & (0x80 >> (bit % 8)):
|
||||
rows[5 + y_offset + y][x_offset + x] = "#"
|
||||
bit += 1
|
||||
if c == ord(" "):
|
||||
rows = [["."] * 2 for _ in range(6)]
|
||||
width = 2
|
||||
glyphs.append((width, ["".join(r) for r in rows]))
|
||||
# Digits all as wide as the widest one (right aligned), so a clock doesn't move
|
||||
digits = range(ord("0") - FIRST, ord("9") - FIRST + 1)
|
||||
widest = max(glyphs[i][0] for i in digits)
|
||||
for i in digits:
|
||||
width, rows = glyphs[i]
|
||||
glyphs[i] = (widest, ["." * (widest - width) + r for r in rows])
|
||||
return encode("Small", 6, 1, glyphs)
|
||||
|
||||
|
||||
# Full height digits: seven segments, 10 x 19 dots
|
||||
SEGMENTS = {
|
||||
"a": (0, 0, 10, 2), # x, y, width, height
|
||||
"b": (7, 0, 3, 11),
|
||||
"c": (7, 8, 3, 11),
|
||||
"d": (0, 17, 10, 2),
|
||||
"e": (0, 8, 3, 11),
|
||||
"f": (0, 0, 3, 11),
|
||||
"g": (0, 8, 10, 3),
|
||||
}
|
||||
DIGITS = ["abcdef", "bc", "abdeg", "abcdg", "bcfg", "acdfg", "acdefg", "abc", "abcdefg", "abcdfg"]
|
||||
|
||||
|
||||
def rounded(rows):
|
||||
"""Rounds the outer corners: clears a corner dot of the glyph box or of the middle bar
|
||||
when it has no neighbour above or below and none left or right"""
|
||||
height, width = len(rows), len(rows[0])
|
||||
on = lambda x, y: 0 <= x < width and 0 <= y < height and rows[y][x] == "#"
|
||||
out = [list(r) for r in rows]
|
||||
for y in (0, 8, 10, height - 1):
|
||||
for x in (0, width - 1):
|
||||
if on(x, y) and not (on(x - 1, y) and on(x + 1, y)) and not (on(x, y - 1) and on(x, y + 1)) and (not on(x - 1, y) or not on(x + 1, y)) and (not on(x, y - 1) or not on(x, y + 1)):
|
||||
out[y][x] = "."
|
||||
return ["".join(r) for r in out]
|
||||
|
||||
|
||||
def box(width, height, dots):
|
||||
rows = [["."] * width for _ in range(height)]
|
||||
for x, y, w, h in dots:
|
||||
for yy in range(y, y + h):
|
||||
for xx in range(x, x + w):
|
||||
rows[yy][xx] = "#"
|
||||
return ["".join(r) for r in rows]
|
||||
|
||||
|
||||
def full_height():
|
||||
special = {
|
||||
" ": box(4, 19, []),
|
||||
":": box(3, 19, [(0, 4, 3, 3), (0, 12, 3, 3)]),
|
||||
".": box(3, 19, [(0, 16, 3, 3)]),
|
||||
"-": box(6, 19, [(0, 8, 6, 3)]),
|
||||
}
|
||||
glyphs = []
|
||||
for c in range(FIRST, LAST + 1):
|
||||
ch = chr(c)
|
||||
if ch.isdigit():
|
||||
rows = rounded(box(10, 19, [SEGMENTS[s] for s in DIGITS[int(ch)]]))
|
||||
glyphs.append((10, rows))
|
||||
elif ch in special:
|
||||
glyphs.append((len(special[ch][0]), special[ch]))
|
||||
else:
|
||||
glyphs.append((0, [""] * 19)) # not in this font: skipped
|
||||
return encode("Full height", 19, 2, glyphs)
|
||||
|
||||
|
||||
def c_array(name, data):
|
||||
lines = []
|
||||
for i in range(0, len(data), 16):
|
||||
lines.append("\t" + ", ".join("0x%02x" % b for b in data[i : i + 16]) + ",")
|
||||
return "static const uint8_t %s[] = {\n%s\n};\n" % (name, "\n".join(lines))
|
||||
|
||||
|
||||
LOOKUP = """
|
||||
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;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
fonts = [("FONT_SMALL", tom_thumb()), ("FONT_NORMAL", classic()), ("FONT_FULL", full_height())]
|
||||
out = [
|
||||
"// Generated by tools/make_builtin_fonts.py, do not edit.\n",
|
||||
"// Small: TomThumb (BSD license, Brian J. Swetland, Vassilii Khachaturov, Dan Marks,\n",
|
||||
"// Robey Pointer); Normal: Adafruit GFX glcdfont (BSD license, Adafruit Industries).\n\n",
|
||||
'#include "logic/builtin_fonts.h"\n\n',
|
||||
'#include "logic/bitmap_format.h"\n\n',
|
||||
]
|
||||
for name, data in fonts:
|
||||
out.append(c_array(name, data) + "\n")
|
||||
out.append("const BuiltinFont BUILTIN_FONTS[] = {\n")
|
||||
out.append('\t{"Small", FONT_SMALL, sizeof(FONT_SMALL), 1},\n')
|
||||
out.append('\t{"Normal", FONT_NORMAL, sizeof(FONT_NORMAL), 1},\n')
|
||||
out.append('\t{"Large", FONT_NORMAL, sizeof(FONT_NORMAL), 2},\n')
|
||||
out.append('\t{"Full height", FONT_FULL, sizeof(FONT_FULL), 1},\n')
|
||||
out.append("};\n")
|
||||
out.append("const int BUILTIN_FONT_COUNT = sizeof(BUILTIN_FONTS) / sizeof(BUILTIN_FONTS[0]);\n")
|
||||
out.append(LOOKUP)
|
||||
with open(OUT, "w", newline="\n") as f:
|
||||
f.write("".join(out))
|
||||
print("Wrote", OUT)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user