Add scrolling, countdown, stopwatch, images, seconds, multiplier and degree sign

- Text elements can scroll in a window at a set speed; clock, countdown and
  stopwatch can show seconds. Moving pages are sent as instant frames (new
  Nano command D1) and paced by the estimated flip time.
- Countdown to a date and time (smart, days, days + time, with seconds),
  stopwatch (web UI, HA switch and reset button), image element (fixed or
  by Home Assistant state), multiplier for HA values, degree sign (char 127)
  in all fonts and the font converter.
- Page format version 2; version 1 pages are converted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-15 21:01:14 +02:00
1 parent ca8d78ea8a
commit fc6fa1a180
38 files changed
+1370 -277

No files matched your search

+119 -116
View File
@@ -7,144 +7,147 @@
#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,
0x46, 0x44, 0x46, 0x31, 0x06, 0x01, 0x20, 0x60, 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,
0x03, 0x03, 0x03, 0x03, 0x01, 0x03, 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, 0x40, 0xa0, 0x40, 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,
0x46, 0x44, 0x46, 0x31, 0x08, 0x01, 0x20, 0x60, 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,
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x04, 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,
0x60, 0x90, 0x90, 0x60, 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,
0x46, 0x44, 0x46, 0x31, 0x13, 0x02, 0x20, 0x60, 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, 0x06, 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,
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,
0x78, 0xcc, 0x84, 0x84, 0xcc, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00,
};
const BuiltinFont BUILTIN_FONTS[] = {
+15 -6
View File
@@ -119,7 +119,7 @@ void DisplayController::update(uint32_t now, bool time_valid, const LocalTime &t
_off_timer.cancel();
_ensureOn();
if (_page == Page::Dynamic)
_showDynamic(time, show_no_wifi);
_showDynamic(now, time, show_no_wifi);
else
_showOnce(Screen::Page);
break;
@@ -139,17 +139,26 @@ void DisplayController::_ensureOn()
_screen = Screen::Unknown;
}
// Drawn again each minute (time, date and background can change) and when the content changed
void DisplayController::_showDynamic(const LocalTime &time, bool no_wifi)
// Drawn again each minute (time, date and background can change), when the content changed,
// and as often as a moving page asks for (scrolling, seconds)
void DisplayController::_showDynamic(uint32_t now, const LocalTime &time, bool no_wifi)
{
Screen screen = no_wifi ? Screen::NoWifi : Screen::Dynamic;
if (_screen == screen && _shown_hour == time.hour && _shown_minute == time.minute &&
_shown_revision == _content_revision)
bool redraw = _screen != screen || _shown_revision != _content_revision;
bool due = screen == Screen::Dynamic && _dynamic_moving && (int32_t)(now - _dynamic_due) >= 0;
if (!redraw && !due && _shown_hour == time.hour && _shown_minute == time.minute)
return;
if (no_wifi)
{
_frames.noWifi(time.hour, time.minute);
_dynamic_moving = false;
}
else
_frames.dynamicPage(time);
{
uint32_t wait = _frames.dynamicPage(time, now, redraw);
_dynamic_moving = wait != 0;
_dynamic_due = now + wait;
}
_screen = screen;
_shown_hour = time.hour;
_shown_minute = time.minute;
+8 -3
View File
@@ -20,8 +20,11 @@ public:
class Frames
{
public:
// The dynamic page selected as page, drawn for this time
virtual void dynamicPage(const LocalTime &time) = 0;
// The dynamic page selected as page, drawn for this time. now: millis(). redraw: send it
// even when it looks the same as the last frame (after power on, a new page). Returns
// the ms until it should be drawn again (scrolling, seconds), or 0 when it only changes
// with the minute or pageChanged().
virtual uint32_t dynamicPage(const LocalTime &time, uint32_t now, bool redraw) = 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
@@ -96,6 +99,8 @@ private:
Screen _screen = Screen::Unknown;
int _shown_hour = -1;
int _shown_minute = -1;
uint32_t _dynamic_due = 0; // millis() when a moving dynamic page is drawn again
bool _dynamic_moving = false;
uint32_t _content_revision = 0; // changed page, its content, bitmap or live image
uint32_t _shown_revision = 0;
@@ -106,6 +111,6 @@ private:
uint32_t _wifi_lost_since = 0;
void _ensureOn();
void _showDynamic(const LocalTime &time, bool no_wifi);
void _showDynamic(uint32_t now, const LocalTime &time, bool no_wifi);
void _showOnce(Screen screen);
};
+7 -2
View File
@@ -81,7 +81,8 @@ static void drawDot(uint8_t *bitmap, int x, int y, DrawMode mode)
setBitmapPixel(bitmap, x, y, on);
}
void drawText(uint8_t *bitmap, const Font &font, int x, int y, const char *text, DrawMode mode)
void drawText(uint8_t *bitmap, const Font &font, int x, int y, const char *text, DrawMode mode, int clip_left,
int clip_right)
{
if (!usable(font) || text == nullptr)
return;
@@ -103,7 +104,11 @@ void drawText(uint8_t *bitmap, const Font &font, int x, int y, const char *text,
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);
{
int dot_x = x + col * scale + dx;
if (dot_x >= clip_left && dot_x < clip_right)
drawDot(bitmap, dot_x, y + row * scale + dy, mode);
}
}
x += width * scale;
}
+6 -2
View File
@@ -45,5 +45,9 @@ enum class DrawMode : uint8_t
};
// 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);
// (logic/bitmap_format.h). Dots outside the screen, and left of clip_left or from
// clip_right on, are skipped.
const int NO_CLIP_LEFT = -32768;
const int NO_CLIP_RIGHT = 32767;
void drawText(uint8_t *bitmap, const Font &font, int x, int y, const char *text, DrawMode mode,
int clip_left = NO_CLIP_LEFT, int clip_right = NO_CLIP_RIGHT);
+2
View File
@@ -9,4 +9,6 @@ struct LocalTime
int weekday = 6; // 0 = Sunday
int hour = 0;
int minute = 0;
int second = 0;
int millisecond = 0;
};
+228 -29
View File
@@ -1,24 +1,26 @@
#include "logic/page.h"
#include <ctype.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static const uint16_t MINUTES_PER_DAY = 24 * 60;
static const uint32_t MAX_TARGET = 36525UL * MINUTES_PER_DAY; // 2100-01-01
static bool terminated(const char *text, size_t size)
{
return memchr(text, '\0', size) != nullptr;
}
// Printable ASCII only: the fonts have no other characters
// Plain ASCII and the degree sign 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)
if (*c < 32 || *c > DEGREE_SIGN)
return false;
return true;
}
@@ -44,6 +46,23 @@ static bool validAttribute(const char *attribute)
return true;
}
static uint8_t formatCount(ElementType type)
{
switch (type)
{
case ElementType::Clock:
return CLOCK_FORMAT_COUNT;
case ElementType::Date:
return DATE_FORMAT_COUNT;
case ElementType::Countdown:
return COUNTDOWN_FORMAT_COUNT;
case ElementType::Stopwatch:
return STOPWATCH_FORMAT_COUNT;
default:
return 255; // not used
}
}
const char *validatePage(const DynamicPage &page)
{
if (page.slot_count > PAGE_SLOTS)
@@ -73,18 +92,32 @@ const char *validatePage(const DynamicPage &page)
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";
return "Texts can only contain plain characters (no accents) and the degree sign, 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.format >= formatCount(e.type))
return "Unknown format";
if (!isfinite(e.factor) || e.factor == 0)
return "The multiplier must be a number other than 0";
if (e.scroll_width > 2 * BITMAP_WIDTH ||
(e.scroll_width && (e.scroll_speed < SCROLL_SPEED_MIN || e.scroll_speed > SCROLL_SPEED_MAX)))
return "Scrolling: window up to 168 dots, speed 1-30 dots per second";
if (e.type == ElementType::HaValue && !validEntityId(e.entity))
return "Home Assistant values need an entity id like sensor.outside_temperature";
if (e.type == ElementType::Countdown && e.target >= MAX_TARGET)
return "The countdown date must be before 2100";
if (e.type == ElementType::Image)
{
if (e.entity[0] && !validEntityId(e.entity))
return "Invalid entity id";
char name[ELEMENT_TEXT_SIZE + 1];
snprintf(name, sizeof(name), "%sx", e.text); // a prefix must be the start of a valid name
if (e.entity[0] ? (e.text[0] == ' ' || !validBitmapName(name)) : !validBitmapName(e.text))
return "Images need a bitmap name (or the start of one, with an entity)";
}
}
return nullptr;
}
@@ -100,6 +133,71 @@ const PageSlot *activeSlot(const DynamicPage &page, int minute_of_day)
return active;
}
// ---------------------------------------------------------------------------
// Dates
// ---------------------------------------------------------------------------
// Days since 1970-01-01 (proleptic Gregorian calendar)
static int32_t daysFromCivil(int y, int m, int d)
{
y -= m <= 2;
int32_t era = (y >= 0 ? y : y - 399) / 400;
int32_t yoe = y - era * 400;
int32_t doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
int32_t doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
return era * 146097 + doe - 719468;
}
static void civilFromDays(int32_t z, int &y, int &m, int &d)
{
z += 719468;
int32_t era = (z >= 0 ? z : z - 146096) / 146097;
int32_t doe = z - era * 146097;
int32_t yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
int32_t doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
int32_t mp = (5 * doy + 2) / 153;
d = doy - (153 * mp + 2) / 5 + 1;
m = mp < 10 ? mp + 3 : mp - 9;
y = yoe + era * 400 + (m <= 2);
}
static const int32_t DAYS_1970_TO_2000 = 10957;
uint32_t minutesSince2000(const LocalTime &t)
{
int32_t days = daysFromCivil(t.year, t.month, t.day) - DAYS_1970_TO_2000;
if (days < 0)
return 0;
return (uint32_t)days * MINUTES_PER_DAY + t.hour * 60 + t.minute;
}
bool parseTarget(const char *text, uint32_t &minutes)
{
if (text == nullptr || strlen(text) != 16)
return false;
int y, mo, d, h, mi;
char extra;
if (sscanf(text, "%4d-%2d-%2dT%2d:%2d%c", &y, &mo, &d, &h, &mi, &extra) != 5)
return false;
if (y < 2000 || y > 2099 || mo < 1 || mo > 12 || d < 1 || d > 31 || h > 23 || mi > 59 || h < 0 || mi < 0)
return false;
int cy, cm, cd; // rejects e.g. 31 February
int32_t days = daysFromCivil(y, mo, d);
civilFromDays(days, cy, cm, cd);
if (cd != d)
return false;
minutes = (uint32_t)(days - DAYS_1970_TO_2000) * MINUTES_PER_DAY + h * 60 + mi;
return true;
}
void formatTarget(uint32_t minutes, char *out)
{
int y, m, d;
civilFromDays(minutes / MINUTES_PER_DAY + DAYS_1970_TO_2000, y, m, d);
unsigned minute_of_day = minutes % MINUTES_PER_DAY;
snprintf(out, 17, "%04d-%02d-%02dT%02u:%02u", y % 10000, m, d, minute_of_day / 60, minute_of_day % 60);
}
// ---------------------------------------------------------------------------
// Texts
// ---------------------------------------------------------------------------
@@ -122,10 +220,18 @@ 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"},
};
// Countdown units: singular, plural
static const char *const DAYS_WORD[][2] = {{"dag", "dagen"}, {"day", "days"}};
static const char *const HOURS_WORD[][2] = {{"uur", "uur"}, {"hour", "hours"}};
static int language(const PageElement &e)
{
return e.language < Language::Count ? (int)e.language : 0;
}
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 lang = language(e);
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)
@@ -154,50 +260,143 @@ static void formatDate(const PageElement &e, const LocalTime &t, char *out, size
}
}
// Rounded to the element's decimals when it is a number; "--" when unknown
// Rounded to the element's decimals (after the multiplier) 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 (strcmp(value, "--") != 0 && end != value && *end == '\0' && (e.decimals >= 0 || e.factor != 1))
{
char *end;
double v = strtod(value, &end);
if (end != value && *end == '\0')
{
v *= e.factor;
if (e.decimals >= 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;
}
else
snprintf(number, sizeof(number), "%.6g", v);
// -0.0 after rounding: no minus sign
if (number[0] == '-' && strspn(number + 1, "0.") == strlen(number + 1))
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)
// Seconds until the target; negative when it has passed
static int64_t secondsLeft(const PageElement &e, const LocalTime &t)
{
return (int64_t)e.target * 60 - ((int64_t)minutesSince2000(t) * 60 + t.second);
}
static void formatCountdown(const PageElement &e, const LocalTime &t, char *out, size_t size)
{
int64_t left = secondsLeft(e, t);
if (left <= 0)
{
snprintf(out, size, "%s%s", e.text, e.suffix[0] ? e.suffix : "0");
return;
}
int lang = language(e);
long days = (long)(left / 86400), hours = (long)(left % 86400 / 3600), minutes = (long)(left % 3600 / 60),
seconds = (long)(left % 60);
char value[32];
switch (e.format)
{
case COUNTDOWN_DAYS:
{
// Calendar days: tomorrow is 1 day, whatever the time
long calendar = (long)(e.target / MINUTES_PER_DAY) - (long)(minutesSince2000(t) / MINUTES_PER_DAY);
snprintf(value, sizeof(value), "%ld %s", calendar, DAYS_WORD[lang][calendar != 1]);
break;
}
case COUNTDOWN_DAYS_TIME:
if (days)
snprintf(value, sizeof(value), "%ldd %02ld:%02ld", days, hours, minutes);
else
snprintf(value, sizeof(value), "%02ld:%02ld", hours, minutes);
break;
case COUNTDOWN_SECONDS:
if (days)
snprintf(value, sizeof(value), "%ldd %02ld:%02ld:%02ld", days, hours, minutes, seconds);
else
snprintf(value, sizeof(value), "%02ld:%02ld:%02ld", hours, minutes, seconds);
break;
default: // smart: the largest unit
if (days)
snprintf(value, sizeof(value), "%ld %s", days, DAYS_WORD[lang][days != 1]);
else if (hours)
snprintf(value, sizeof(value), "%ld %s", hours, HOURS_WORD[lang][hours != 1]);
else if (minutes)
snprintf(value, sizeof(value), "%ld min", minutes);
else
snprintf(value, sizeof(value), "0:%02ld", seconds);
break;
}
snprintf(out, size, "%s%s", e.text, value);
}
static void formatStopwatch(const PageElement &e, uint32_t ms, char *out, size_t size)
{
unsigned long s = ms / 1000, h = s / 3600, m = s % 3600 / 60, sec = s % 60;
char value[24];
if (e.format == STOPWATCH_H_MM)
snprintf(value, sizeof(value), "%lu:%02lu", h, m);
else if (e.format == STOPWATCH_H_MM_SS || h)
snprintf(value, sizeof(value), "%lu:%02lu:%02lu", h, m, sec);
else
snprintf(value, sizeof(value), "%lu:%02lu", m, sec);
snprintf(out, size, "%s%s%s", e.text, value, e.suffix);
}
void elementText(const PageElement &e, const LocalTime &t, const ElementValues &values, 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);
if (e.format == CLOCK_HH_MM_SS || e.format == CLOCK_H_MM_SS)
snprintf(out, size, e.format == CLOCK_H_MM_SS ? "%d:%02d:%02d" : "%02d:%02d:%02d", t.hour, t.minute,
t.second);
else
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);
formatHaValue(e, values.ha_value, out, size);
break;
case ElementType::Countdown:
formatCountdown(e, t, out, size);
break;
case ElementType::Stopwatch:
formatStopwatch(e, values.stopwatch_ms, out, size);
break;
case ElementType::Image:
out[0] = '\0';
break;
default:
snprintf(out, size, "%s", e.text);
break;
}
}
bool elementShowsSeconds(const PageElement &e, const LocalTime &t, bool stopwatch_running)
{
switch (e.type)
{
case ElementType::Clock:
return e.format == CLOCK_HH_MM_SS || e.format == CLOCK_H_MM_SS;
case ElementType::Stopwatch:
return stopwatch_running && e.format != STOPWATCH_H_MM;
case ElementType::Countdown:
{
int64_t left = secondsLeft(e, t);
if (left <= 0)
return false;
return e.format == COUNTDOWN_SECONDS || (e.format == COUNTDOWN_SMART && left < 60);
}
default:
return false;
}
}
+64 -14
View File
@@ -7,9 +7,11 @@
#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.
// (clock, date, Home Assistant value, text, countdown, stopwatch, image) 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.
//
// Texts are plain ASCII; character 127 is the degree sign (the web UI shows it as °).
const int PAGE_NAME_SIZE = BITMAP_NAME_MAX + 1;
const int PAGE_SLOTS = 8;
@@ -18,6 +20,7 @@ const int ELEMENT_TEXT_SIZE = 33;
const int ELEMENT_SUFFIX_SIZE = 17;
const int ELEMENT_ENTITY_SIZE = 65;
const int ELEMENT_ATTRIBUTE_SIZE = 33;
const char DEGREE_SIGN = 127;
enum class ElementType : uint8_t
{
@@ -25,6 +28,9 @@ enum class ElementType : uint8_t
Date,
HaValue, // a Home Assistant entity state (or attribute)
Text,
Countdown, // time left until a date and time
Stopwatch, // the stopwatch (web UI and Home Assistant start, stop and reset it)
Image, // a bitmap, fixed or chosen by a Home Assistant state
Count,
};
@@ -54,9 +60,11 @@ enum class Language : uint8_t
};
// 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;
const uint8_t CLOCK_HH_MM = 0; // 08:05
const uint8_t CLOCK_H_MM = 1; // 8:05
const uint8_t CLOCK_HH_MM_SS = 2; // 08:05:09
const uint8_t CLOCK_H_MM_SS = 3; // 8:05:09
const uint8_t CLOCK_FORMAT_COUNT = 4;
// Date formats (Dutch / English examples)
const uint8_t DATE_WEEKDAY_DAY_MONTH = 0; // do 15 okt / Thu 15 Oct
@@ -68,10 +76,28 @@ const uint8_t DATE_WEEKDAY = 5; // donderdag / Thursday
const uint8_t DATE_WEEKDAY_SHORT = 6; // do / Thu
const uint8_t DATE_FORMAT_COUNT = 7;
// Countdown formats
const uint8_t COUNTDOWN_SMART = 0; // 12 dagen, 5 uur, 34 min, then 0:45
const uint8_t COUNTDOWN_DAYS = 1; // 12 dagen (calendar days)
const uint8_t COUNTDOWN_DAYS_TIME = 2; // 12d 04:05, below a day 04:05
const uint8_t COUNTDOWN_SECONDS = 3; // 12d 04:05:33, below a day 04:05:33
const uint8_t COUNTDOWN_FORMAT_COUNT = 4;
// Stopwatch formats
const uint8_t STOPWATCH_M_SS = 0; // 4:05, from an hour on 1:04:05
const uint8_t STOPWATCH_H_MM = 1; // 0:04 (hours and minutes)
const uint8_t STOPWATCH_H_MM_SS = 2; // 0:04:05
const uint8_t STOPWATCH_FORMAT_COUNT = 3;
// Home Assistant values: decimals as received
const int8_t DECIMALS_AS_IS = -1;
const int8_t DECIMALS_MAX = 4;
// Scrolling: dots per second, and the empty dots between the end and the next start
const uint8_t SCROLL_SPEED_MIN = 1;
const uint8_t SCROLL_SPEED_MAX = 30;
const int SCROLL_GAP = 12;
// Background from `start` (minutes since midnight) until the next slot
struct PageSlot
{
@@ -87,13 +113,20 @@ struct PageElement
Align align = Align::Left;
Overlay overlay = Overlay::OnTop;
char font[PAGE_NAME_SIZE] = "Normal";
uint8_t format = 0; // CLOCK_* or DATE_*
uint8_t format = 0; // CLOCK_*, DATE_*, COUNTDOWN_* or STOPWATCH_*
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
// Text: the text; HaValue, Countdown, Stopwatch: shown before the value; Image: the
// bitmap name, or with an entity the start of it (the state is added)
char text[ELEMENT_TEXT_SIZE] = "";
// HaValue, Stopwatch: shown after the value; Countdown: shown when the time has passed
char suffix[ELEMENT_SUFFIX_SIZE] = "";
char entity[ELEMENT_ENTITY_SIZE] = ""; // HaValue, Image: e.g. sensor.outside_temperature
char attribute[ELEMENT_ATTRIBUTE_SIZE] = ""; // empty = the state
float factor = 1; // HaValue: numbers are multiplied by it (e.g. 1000: m3 -> l)
uint8_t scroll_width = 0; // texts: 0, or the width of a window the text scrolls in
uint8_t scroll_speed = 8; // dots per second
uint32_t target = 0; // Countdown: minutes since 2000-01-01 00:00 (local time)
};
struct DynamicPage
@@ -112,10 +145,27 @@ const char *validatePage(const DynamicPage &page);
// 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,
// What a text element needs besides the time
struct ElementValues
{
const char *ha_value = nullptr; // nullptr: not known yet, shown as "--"
uint32_t stopwatch_ms = 0;
};
// The text of an element (not for images). out_size must be at least 2.
void elementText(const PageElement &element, const LocalTime &time, const ElementValues &values, char *out,
size_t out_size);
// Whether the element's text shows seconds now (it then changes every second)
bool elementShowsSeconds(const PageElement &element, const LocalTime &time, bool stopwatch_running);
// Entity ids look like "sensor.outside_temperature" (lower case, digits, '_', one '.')
bool validEntityId(const char *entity);
// Countdown targets: minutes since 2000-01-01 00:00 <-> "YYYY-MM-DDTHH:MM" (17 characters,
// the format of an HTML datetime-local input). Years 2000-2099.
bool parseTarget(const char *text, uint32_t &minutes);
// out must hold 17 characters
void formatTarget(uint32_t minutes, char *out);
// The local time in minutes since 2000-01-01 00:00 (seconds left out)
uint32_t minutesSince2000(const LocalTime &time);
+140 -23
View File
@@ -1,5 +1,6 @@
#include "logic/page_render.h"
#include <stdio.h>
#include <string.h>
#include "logic/bitmap_format.h"
@@ -20,39 +21,155 @@ static void clearBox(uint8_t *bitmap, int x, int y, int width, int height)
setBitmapPixel(bitmap, xx, yy, false);
}
void renderPage(const DynamicPage &page, const LocalTime &time, PageResources &resources, uint8_t *bitmap,
ElementBox *boxes)
// The earlier of two "next change" times (0 = none)
static uint32_t sooner(uint32_t a, uint32_t b)
{
if (a == 0)
return b;
if (b == 0)
return a;
return a < b ? a : b;
}
// Image: a bitmap drawn with its top left corner at x, y. With an entity the name is the
// element's text followed by the state, or followed by "default" when there is no bitmap
// for the state.
static void drawImage(const PageElement &e, PageResources &resources, uint8_t *bitmap, ElementBox &box)
{
uint8_t image[BITMAP_BYTES];
bool found;
if (e.entity[0])
{
char name[64];
const char *state = resources.haValue(e);
snprintf(name, sizeof(name), "%s%s", e.text, state ? state : "");
found = state && resources.background(name, image);
if (!found)
{
snprintf(name, sizeof(name), "%sdefault", e.text);
found = resources.background(name, image);
}
}
else
found = resources.background(e.text, image);
box = ElementBox();
box.x = e.x;
box.y = e.y;
if (!found)
return;
// The box of its dots
int left = BITMAP_WIDTH, right = -1, top = BITMAP_HEIGHT, bottom = -1;
for (int y = 0; y < BITMAP_HEIGHT; y++)
for (int x = 0; x < BITMAP_WIDTH; x++)
if (bitmapPixel(image, x, y))
{
left = x < left ? x : left;
right = x > right ? x : right;
top = y < top ? y : top;
bottom = y > bottom ? y : bottom;
}
if (right < 0)
return;
box.x = e.x + left;
box.y = e.y + top;
box.width = right - left + 1;
box.height = bottom - top + 1;
if (e.overlay == Overlay::Box)
clearBox(bitmap, box.x - 1, box.y - 1, box.width + 2, box.height + 2);
for (int y = top; y <= bottom; y++)
for (int x = left; x <= right; x++)
if (bitmapPixel(image, x, y))
{
int bx = e.x + x, by = e.y + y;
bool on = e.overlay == Overlay::Invert ? !bitmapPixel(bitmap, bx, by) : true;
setBitmapPixel(bitmap, bx, by, on);
}
}
// A text element; returns the ms until it scrolls on (0: it doesn't scroll)
static uint32_t drawTextElement(const PageElement &e, const char *text, uint32_t now_ms, PageResources &resources,
uint8_t *bitmap, ElementBox &box)
{
Font font = usableFont(resources, e.font);
int width = textWidth(font, text), height = fontHeight(font);
DrawMode mode = e.overlay == Overlay::Invert ? DrawMode::Invert : DrawMode::Set;
// Without scrolling the element is as wide as its text, otherwise the window
int area = e.scroll_width ? e.scroll_width : width;
int left = e.x;
if (e.align == Align::Center)
left -= area / 2;
else if (e.align == Align::Right)
left -= area;
box.x = left;
box.y = e.y;
box.width = area;
box.height = height;
if (e.overlay == Overlay::Box)
clearBox(bitmap, left - 1, e.y - 1, area + 2, height + 2);
if (width <= area)
{
// Fits: aligned in the window like the element
int x = left;
if (e.align == Align::Center)
x += (area - width) / 2;
else if (e.align == Align::Right)
x += area - width;
drawText(bitmap, font, x, e.y, text, mode);
return 0;
}
// Scrolls from right to left, the next round follows after SCROLL_GAP dots
uint32_t period = width + SCROLL_GAP;
uint64_t steps = (uint64_t)now_ms * e.scroll_speed / 1000;
int offset = (int)(steps % period);
drawText(bitmap, font, left - offset, e.y, text, mode, left, left + area);
drawText(bitmap, font, left - offset + (int)period, e.y, text, mode, left, left + area);
// Until the next step
uint64_t next_step_ms = ((steps + 1) * 1000 + e.scroll_speed - 1) / e.scroll_speed;
uint32_t wait = (uint32_t)(next_step_ms - now_ms);
return wait ? wait : 1;
}
uint32_t renderPage(const DynamicPage &page, const LocalTime &time, uint32_t now_ms, 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);
uint32_t next = 0;
bool running = resources.stopwatchRunning();
uint32_t stopwatch_ms = resources.stopwatchMs();
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)
ElementBox box;
if (e.type == ElementType::Image)
drawImage(e, resources, bitmap, box);
else
{
boxes[i].x = x;
boxes[i].y = e.y;
boxes[i].width = width;
boxes[i].height = height;
ElementValues values;
if (e.type == ElementType::HaValue)
values.ha_value = resources.haValue(e);
values.stopwatch_ms = stopwatch_ms;
char text[80];
elementText(e, time, values, text, sizeof(text));
next = sooner(next, drawTextElement(e, text, now_ms, resources, bitmap, box));
if (elementShowsSeconds(e, time, running)) // the stopwatch's seconds count from its start
next = sooner(next, e.type == ElementType::Stopwatch ? 1000 - stopwatch_ms % 1000
: 1000 - (time.millisecond % 1000));
else if (e.type == ElementType::Stopwatch && running) // H:MM: the next stopwatch minute
next = sooner(next, 60000 - stopwatch_ms % 60000);
}
if (boxes)
boxes[i] = box;
}
return next;
}
+13 -6
View File
@@ -9,19 +9,22 @@
// 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
// Where the page gets its bitmaps, fonts, Home Assistant values and the stopwatch
class PageResources
{
public:
// Loads a background bitmap; false when it is gone (the background is blank then)
// Loads a bitmap (background or image); false when it is gone
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;
virtual uint32_t stopwatchMs() { return 0; }
virtual bool stopwatchRunning() { return false; }
};
// The area an element covers on the screen (for the editor), may be partly off screen
// The area an element covers on the screen (for the editor), may be partly off screen.
// For scrolling texts, the window.
struct ElementBox
{
int16_t x = 0;
@@ -30,6 +33,10 @@ struct ElementBox
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);
// now_ms: a running millisecond counter (millis()), for scrolling. boxes: nullptr, or room
// for page.element_count boxes.
// Returns the ms until the page looks different again (scrolling, seconds, a running
// stopwatch), or 0 when it only changes with the minute or its content (Home Assistant
// values, the stopwatch being started or stopped).
uint32_t renderPage(const DynamicPage &page, const LocalTime &time, uint32_t now_ms, PageResources &resources,
uint8_t *bitmap, ElementBox *boxes = nullptr);
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <stdint.h>
// The stopwatch of the dynamic pages, started and stopped in the web UI and Home Assistant.
// Times are millis(). Pure logic, tested in test/test_esp32.
class Stopwatch
{
public:
void start(uint32_t now)
{
if (_running)
return;
_running = true;
_started = now;
}
void stop(uint32_t now)
{
if (!_running)
return;
_before += now - _started;
_running = false;
}
// Back to 0, stopped
void reset()
{
_running = false;
_before = 0;
}
bool running() const { return _running; }
uint32_t elapsed(uint32_t now) const { return _before + (_running ? now - _started : 0); }
private:
bool _running = false;
uint32_t _started = 0;
uint32_t _before = 0; // ms counted before the last start
};