Public Access
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:
1 parent
ca8d78ea8a
commit
fc6fa1a180
38 files changed
+1370
-277
No files matched your search
@@ -1,6 +1,7 @@
|
||||
#include "app.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "config.h"
|
||||
@@ -70,6 +71,7 @@ static void apply()
|
||||
controller.setSettings(settings.schedule, settings.clock);
|
||||
controller.setEffectTime(effectTime(settings.drive));
|
||||
status.setDrive(settings.flip_time, settings.drive);
|
||||
frames.setDrive(settings.flip_time, settings.drive);
|
||||
haApiSetSettings(settings.home_assistant);
|
||||
applyOffPage();
|
||||
applyPage();
|
||||
@@ -230,6 +232,17 @@ const char *appDeleteFont(const char *name)
|
||||
return deleteFont(name);
|
||||
}
|
||||
|
||||
void appStopwatch(StopwatchAction action)
|
||||
{
|
||||
if (action == StopwatchAction::Start)
|
||||
stopwatch.start(millis());
|
||||
else if (action == StopwatchAction::Stop)
|
||||
stopwatch.stop(millis());
|
||||
else
|
||||
stopwatch.reset();
|
||||
controller.pageChanged();
|
||||
}
|
||||
|
||||
void appShowLive(const uint8_t *bitmap)
|
||||
{
|
||||
frames.setLiveBitmap(bitmap);
|
||||
@@ -252,6 +265,10 @@ bool appLocalTime(LocalTime &time)
|
||||
time.weekday = now.tm_wday;
|
||||
time.hour = now.tm_hour;
|
||||
time.minute = now.tm_min;
|
||||
time.second = now.tm_sec;
|
||||
struct timeval tv;
|
||||
gettimeofday(&tv, nullptr);
|
||||
time.millisecond = tv.tv_usec / 1000;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,15 @@ const char *appDeletePage(const char *name);
|
||||
const char *appSaveFont(const char *name, const uint8_t *data, size_t size);
|
||||
const char *appDeleteFont(const char *name);
|
||||
|
||||
// The stopwatch of the dynamic pages (web UI and Home Assistant); redraws the display
|
||||
enum class StopwatchAction
|
||||
{
|
||||
Start,
|
||||
Stop,
|
||||
Reset,
|
||||
};
|
||||
void appStopwatch(StopwatchAction action);
|
||||
|
||||
// Live mode of the web UI
|
||||
void appShowLive(const uint8_t *bitmap);
|
||||
void appStopLive();
|
||||
|
||||
+4
-1
@@ -49,6 +49,9 @@ const uint32_t POWER_OFF_DELAY = 30000;
|
||||
// timeout (a read blocks loop() for at most about twice this)
|
||||
const uint32_t HA_POLL_INTERVAL = 60000;
|
||||
const uint32_t HA_HTTP_TIMEOUT = 1500;
|
||||
// Moving dynamic pages (scrolling, seconds): sending a frame to the Nano takes about this
|
||||
// long (a bitmap at 115200 baud), plus the flip time of the changed dots
|
||||
const uint32_t FRAME_SEND_TIME = 60;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Flashing the Nano over WiFi (see README.md, pio run -e nano_ota -t upload)
|
||||
@@ -99,7 +102,7 @@ const uint32_t SETUP_AP_DELAY = 20000; // open the AP when WiFi fails thi
|
||||
const uint32_t WIFI_CONNECT_TIMEOUT = 30000;
|
||||
const uint32_t WIFI_RESTART_TIME = 10UL * 60 * 1000;
|
||||
|
||||
#define FIRMWARE_VERSION "2.4.0"
|
||||
#define FIRMWARE_VERSION "2.5.0"
|
||||
|
||||
// Home Assistant device id (do not change: Home Assistant's entities are linked to it)
|
||||
const uint8_t HA_DEVICE_ID[] = {0x34, 0x86, 0x5d, 0x3a, 0x08, 0x28};
|
||||
|
||||
@@ -110,7 +110,8 @@ bool loadBitmap(const char *name, uint8_t *bitmap)
|
||||
memcpy_P(bitmap, b->data, BITMAP_BYTES);
|
||||
return true;
|
||||
}
|
||||
if (!mounted || !validBitmapName(name))
|
||||
// Checked first: open() logs an error for a missing file (images try names by state)
|
||||
if (!mounted || !validBitmapName(name) || !LittleFS.exists(path(name)))
|
||||
return false;
|
||||
File file = LittleFS.open(path(name), "r");
|
||||
if (!file)
|
||||
|
||||
@@ -3,17 +3,41 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "display/text.h"
|
||||
|
||||
FlipdotFrames::FlipdotFrames(BROSE9323 &display, PageResources &resources) : _display(display), _resources(resources)
|
||||
{
|
||||
}
|
||||
|
||||
void FlipdotFrames::dynamicPage(const LocalTime &time)
|
||||
uint32_t FlipdotFrames::dynamicPage(const LocalTime &time, uint32_t now, bool redraw)
|
||||
{
|
||||
uint8_t bitmap[BITMAP_BYTES];
|
||||
renderPage(_dynamic, time, _resources, bitmap);
|
||||
_drawFullScreen(bitmap);
|
||||
uint32_t wait = renderPage(_dynamic, time, now, _resources, bitmap);
|
||||
bool moving = wait != 0;
|
||||
if (!redraw && memcmp(bitmap, _last_dynamic, BITMAP_BYTES) == 0)
|
||||
return wait;
|
||||
// The Nano has no acknowledgement: wait until it has flipped the dots of this frame,
|
||||
// or the next frame overflows its serial buffer
|
||||
uint32_t busy = _flipMs(bitmap, redraw);
|
||||
_drawFullScreen(bitmap, moving);
|
||||
memcpy(_last_dynamic, bitmap, BITMAP_BYTES);
|
||||
return moving && wait < busy ? busy : wait;
|
||||
}
|
||||
|
||||
void FlipdotFrames::setDrive(uint16_t flip_time, const DriveSettings &drive)
|
||||
{
|
||||
_dot_us = (uint32_t)drive.pulses * (flip_time + drive.flip_gap);
|
||||
}
|
||||
|
||||
// About how long the Nano takes to receive and show the bitmap (all: every dot flips)
|
||||
uint32_t FlipdotFrames::_flipMs(const uint8_t *bitmap, bool all) const
|
||||
{
|
||||
uint32_t dots = 0;
|
||||
for (int y = 0; y < BITMAP_HEIGHT; y++)
|
||||
for (int x = 0; x < BITMAP_WIDTH; x++)
|
||||
dots += all || bitmapPixel(bitmap, x, y) != _display.shownPixel(x, y);
|
||||
return FRAME_SEND_TIME + dots * _dot_us / 1000;
|
||||
}
|
||||
|
||||
void FlipdotFrames::noWifi(int hour, int minute)
|
||||
@@ -62,9 +86,9 @@ void FlipdotFrames::setOffBitmap(const uint8_t *bitmap)
|
||||
memset(_off, 0, BITMAP_BYTES);
|
||||
}
|
||||
|
||||
void FlipdotFrames::_drawFullScreen(const uint8_t *bitmap)
|
||||
void FlipdotFrames::_drawFullScreen(const uint8_t *bitmap, bool instant)
|
||||
{
|
||||
_display.fillScreen(0);
|
||||
_display.drawBitmap(0, 0, bitmap, BITMAP_WIDTH, BITMAP_HEIGHT, 1);
|
||||
_display.display();
|
||||
_display.display(false, instant);
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "logic/bitmap_format.h"
|
||||
#include "logic/display_controller.h"
|
||||
#include "logic/page_render.h"
|
||||
#include "logic/settings.h"
|
||||
|
||||
// Draws the screens on the flipdot display. Each method sends one complete frame.
|
||||
class FlipdotFrames : public Frames
|
||||
@@ -11,8 +12,10 @@ class FlipdotFrames : public Frames
|
||||
public:
|
||||
FlipdotFrames(BROSE9323 &display, PageResources &resources);
|
||||
|
||||
// The page set with setDynamicPage(), drawn for this time
|
||||
void dynamicPage(const LocalTime &time) override;
|
||||
// The page set with setDynamicPage(), drawn for this time. Not sent when it looks the
|
||||
// same as the last frame (unless redraw). Moving pages are sent without the refresh
|
||||
// effect, and not faster than the Nano can flip the dots.
|
||||
uint32_t dynamicPage(const LocalTime &time, uint32_t now, bool redraw) override;
|
||||
// "No WiFi" with the time below it
|
||||
void noWifi(int hour, int minute) override;
|
||||
// The bitmap set with setPageBitmap()
|
||||
@@ -23,6 +26,8 @@ public:
|
||||
void off() override;
|
||||
|
||||
void setDynamicPage(const DynamicPage &page);
|
||||
// How the Nano flips the dots: for the time a frame takes
|
||||
void setDrive(uint16_t flip_time, const DriveSettings &drive);
|
||||
void setPageBitmap(const uint8_t *bitmap);
|
||||
void setLiveBitmap(const uint8_t *bitmap);
|
||||
// nullptr: blank
|
||||
@@ -32,9 +37,12 @@ private:
|
||||
BROSE9323 &_display;
|
||||
PageResources &_resources;
|
||||
DynamicPage _dynamic;
|
||||
uint8_t _last_dynamic[BITMAP_BYTES] = {};
|
||||
uint32_t _dot_us = 1000; // per changed dot: pulses x (flip time + gap)
|
||||
uint8_t _page[BITMAP_BYTES] = {};
|
||||
uint8_t _live[BITMAP_BYTES] = {};
|
||||
uint8_t _off[BITMAP_BYTES] = {}; // all zero: blank
|
||||
|
||||
void _drawFullScreen(const uint8_t *bitmap);
|
||||
void _drawFullScreen(const uint8_t *bitmap, bool instant = false);
|
||||
uint32_t _flipMs(const uint8_t *bitmap, bool all) const;
|
||||
};
|
||||
@@ -1,10 +1,13 @@
|
||||
#include "display/page_resources.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "display/bitmap_store.h"
|
||||
#include "display/font_store.h"
|
||||
#include "net/ha_api.h"
|
||||
|
||||
StoredPageResources pageResources;
|
||||
Stopwatch stopwatch;
|
||||
StoredPageResources pageResources(stopwatch);
|
||||
|
||||
bool StoredPageResources::background(const char *name, uint8_t *bitmap)
|
||||
{
|
||||
@@ -20,3 +23,13 @@ const char *StoredPageResources::haValue(const PageElement &element)
|
||||
{
|
||||
return haApiValue(element.entity, element.attribute);
|
||||
}
|
||||
|
||||
uint32_t StoredPageResources::stopwatchMs()
|
||||
{
|
||||
return _stopwatch.elapsed(millis());
|
||||
}
|
||||
|
||||
bool StoredPageResources::stopwatchRunning()
|
||||
{
|
||||
return _stopwatch.running();
|
||||
}
|
||||
@@ -1,15 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "logic/page_render.h"
|
||||
#include "logic/stopwatch.h"
|
||||
|
||||
// Bitmaps from display/bitmap_store, fonts from display/font_store and Home Assistant
|
||||
// values from net/ha_api, for drawing dynamic pages on the display and in the web UI
|
||||
// Bitmaps from display/bitmap_store, fonts from display/font_store, Home Assistant values
|
||||
// from net/ha_api and the stopwatch, for drawing dynamic pages on the display and in the
|
||||
// web UI
|
||||
class StoredPageResources : public PageResources
|
||||
{
|
||||
public:
|
||||
explicit StoredPageResources(Stopwatch &stopwatch) : _stopwatch(stopwatch) {}
|
||||
|
||||
bool background(const char *name, uint8_t *bitmap) override;
|
||||
Font font(const char *name) override;
|
||||
const char *haValue(const PageElement &element) override;
|
||||
uint32_t stopwatchMs() override;
|
||||
bool stopwatchRunning() override;
|
||||
|
||||
private:
|
||||
Stopwatch &_stopwatch;
|
||||
};
|
||||
|
||||
extern Stopwatch stopwatch;
|
||||
extern StoredPageResources pageResources;
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
static const char *DIR = "/pages";
|
||||
static const char *EXTENSION = ".pg";
|
||||
// Increase when DynamicPage changes (older files are then ignored)
|
||||
static const uint16_t VERSION = 1;
|
||||
// Increase when DynamicPage changes; older files are ignored unless converted below
|
||||
static const uint16_t VERSION = 2;
|
||||
|
||||
struct StoredPage
|
||||
{
|
||||
@@ -17,6 +17,68 @@ struct StoredPage
|
||||
DynamicPage page;
|
||||
};
|
||||
|
||||
// Version 1 (before scrolling, countdown, stopwatch, images and the multiplier): frozen
|
||||
// copy of the layout, converted when loaded. Saving writes version 2.
|
||||
namespace v1
|
||||
{
|
||||
struct Element
|
||||
{
|
||||
ElementType type;
|
||||
int16_t x, y;
|
||||
Align align;
|
||||
Overlay overlay;
|
||||
char font[PAGE_NAME_SIZE];
|
||||
uint8_t format;
|
||||
Language language;
|
||||
int8_t decimals;
|
||||
char text[ELEMENT_TEXT_SIZE];
|
||||
char suffix[ELEMENT_SUFFIX_SIZE];
|
||||
char entity[ELEMENT_ENTITY_SIZE];
|
||||
char attribute[ELEMENT_ATTRIBUTE_SIZE];
|
||||
};
|
||||
|
||||
struct Page
|
||||
{
|
||||
uint8_t slot_count;
|
||||
PageSlot slots[PAGE_SLOTS];
|
||||
uint8_t element_count;
|
||||
Element elements[PAGE_ELEMENTS];
|
||||
};
|
||||
|
||||
struct Stored
|
||||
{
|
||||
uint16_t version;
|
||||
uint16_t size;
|
||||
Page page;
|
||||
};
|
||||
|
||||
static void convert(const Page &old, DynamicPage &page)
|
||||
{
|
||||
page = DynamicPage();
|
||||
page.slot_count = old.slot_count;
|
||||
memcpy(page.slots, old.slots, sizeof(page.slots));
|
||||
page.element_count = old.element_count;
|
||||
for (int i = 0; i < PAGE_ELEMENTS; i++)
|
||||
{
|
||||
const Element &o = old.elements[i];
|
||||
PageElement &e = page.elements[i];
|
||||
e.type = o.type;
|
||||
e.x = o.x;
|
||||
e.y = o.y;
|
||||
e.align = o.align;
|
||||
e.overlay = o.overlay;
|
||||
memcpy(e.font, o.font, sizeof(e.font));
|
||||
e.format = o.format;
|
||||
e.language = o.language;
|
||||
e.decimals = o.decimals;
|
||||
memcpy(e.text, o.text, sizeof(e.text));
|
||||
memcpy(e.suffix, o.suffix, sizeof(e.suffix));
|
||||
memcpy(e.entity, o.entity, sizeof(e.entity));
|
||||
memcpy(e.attribute, o.attribute, sizeof(e.attribute));
|
||||
}
|
||||
}
|
||||
} // namespace v1
|
||||
|
||||
static uint32_t revision = 0;
|
||||
|
||||
static String path(const char *name)
|
||||
@@ -64,14 +126,30 @@ bool loadPage(const char *name, DynamicPage &page)
|
||||
File file = LittleFS.open(path(name), "r");
|
||||
if (!file)
|
||||
return false;
|
||||
StoredPage *stored = new StoredPage;
|
||||
bool ok = file.read((uint8_t *)stored, sizeof(StoredPage)) == sizeof(StoredPage) &&
|
||||
stored->version == VERSION && stored->size == sizeof(DynamicPage) &&
|
||||
validatePage(stored->page) == nullptr;
|
||||
bool ok = false;
|
||||
size_t size = file.size();
|
||||
if (size == sizeof(StoredPage))
|
||||
{
|
||||
StoredPage *stored = new StoredPage;
|
||||
ok = file.read((uint8_t *)stored, sizeof(StoredPage)) == sizeof(StoredPage) && stored->version == VERSION &&
|
||||
stored->size == sizeof(DynamicPage) && validatePage(stored->page) == nullptr;
|
||||
if (ok)
|
||||
page = stored->page;
|
||||
delete stored;
|
||||
}
|
||||
else if (size == sizeof(v1::Stored))
|
||||
{
|
||||
v1::Stored *stored = new v1::Stored;
|
||||
ok = file.read((uint8_t *)stored, sizeof(v1::Stored)) == sizeof(v1::Stored) && stored->version == 1 &&
|
||||
stored->size == sizeof(v1::Page);
|
||||
if (ok)
|
||||
{
|
||||
v1::convert(stored->page, page);
|
||||
ok = validatePage(page) == nullptr;
|
||||
}
|
||||
delete stored;
|
||||
}
|
||||
file.close();
|
||||
if (ok)
|
||||
page = stored->page;
|
||||
delete stored;
|
||||
return ok;
|
||||
}
|
||||
|
||||
|
||||
+119
-116
@@ -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[] = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
|
||||
};
|
||||
@@ -70,7 +70,8 @@ void haApiWatch(const DynamicPage *page)
|
||||
for (int i = 0; i < page->element_count; i++)
|
||||
{
|
||||
const PageElement &element = page->elements[i];
|
||||
if (element.type != ElementType::HaValue || !validEntityId(element.entity))
|
||||
bool uses_state = element.type == ElementType::HaValue || element.type == ElementType::Image;
|
||||
if (!uses_state || !validEntityId(element.entity))
|
||||
continue;
|
||||
Entry &e = add(element.entity, element.attribute);
|
||||
if (!e.watched)
|
||||
@@ -127,6 +128,7 @@ static const char *fetch(const String &entity, const String &attribute, String &
|
||||
value = "";
|
||||
serializeJson(v, value); // numbers, true/false
|
||||
}
|
||||
value.replace("\xC2\xB0", String(DEGREE_SIGN)); // UTF-8 degree sign: the fonts have it as 127
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "config.h"
|
||||
#include "credentials.h"
|
||||
#include "display/bitmap_store.h"
|
||||
#include "display/page_resources.h"
|
||||
#include "display/page_store.h"
|
||||
#include "net/ha_page_select.h"
|
||||
#include "system/boot_target.h"
|
||||
@@ -27,6 +28,8 @@ static HAPageSelect offSelect("FlipDotDisplayOffImage");
|
||||
static HASensor firmwareSensor("FlipDotDisplayFirmware");
|
||||
static HASensor ipSensor("FlipDotDisplayIP");
|
||||
static HAButton recoveryButton("FlipDotDisplayRecovery");
|
||||
static HASwitch stopwatchSwitch("FlipDotDisplayStopwatch");
|
||||
static HAButton stopwatchReset("FlipDotDisplayStopwatchReset");
|
||||
|
||||
// Removed entities: their retained discovery config is cleared, so Home Assistant
|
||||
// deletes them instead of showing them as unavailable
|
||||
@@ -215,6 +218,17 @@ void homeAssistantBegin(DisplayController &controller, const HomeAssistantSettin
|
||||
offSelect.onCommand([](const char *option) { offImage.command(option); });
|
||||
offImage.update();
|
||||
|
||||
stopwatchSwitch.setName("FlipDot Display Stopwatch");
|
||||
stopwatchSwitch.setIcon("mdi:timer-outline");
|
||||
stopwatchSwitch.setCurrentState(stopwatch.running());
|
||||
stopwatchSwitch.onCommand([](bool state, HASwitch *sender) {
|
||||
appStopwatch(state ? StopwatchAction::Start : StopwatchAction::Stop);
|
||||
sender->setState(stopwatch.running());
|
||||
});
|
||||
stopwatchReset.setName("FlipDot Display Stopwatch reset");
|
||||
stopwatchReset.setIcon("mdi:timer-refresh-outline");
|
||||
stopwatchReset.onCommand([](HAButton *) { appStopwatch(StopwatchAction::Reset); });
|
||||
|
||||
recoveryButton.setName("FlipDot Display Recovery");
|
||||
recoveryButton.setIcon("mdi:lifebuoy");
|
||||
recoveryButton.onCommand([](HAButton *) { rebootToRecovery(RecoveryReason::Manual); });
|
||||
@@ -260,6 +274,9 @@ void homeAssistantLoop()
|
||||
firmware.loop();
|
||||
mode.update();
|
||||
offImage.update();
|
||||
// Started or stopped in the web UI
|
||||
if (stopwatchSwitch.getCurrentState() != stopwatch.running())
|
||||
stopwatchSwitch.setState(stopwatch.running());
|
||||
}
|
||||
|
||||
void homeAssistantSetFirmwareStatus(const char *status)
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
// FlipDot Display IP sensor: the IP address
|
||||
// FlipDot Display Firmware sensor: version and health, e.g. "2.1.0, ok, 0 crashes, ..."
|
||||
// FlipDot Display Recovery button: restart into the recovery app (for 10 minutes)
|
||||
// FlipDot Display Stopwatch switch: the stopwatch of the dynamic pages runs
|
||||
// FlipDot Display Stopwatch reset button: back to 0
|
||||
//
|
||||
// The broker comes from the settings (web UI); an empty host means BROKER_ADDR, MQTT_USER
|
||||
// and MQTT_PASSWORD from secrets.h. Changes need a restart.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
#include "logic/settings.h"
|
||||
|
||||
static const char *const TYPES[] = {"clock", "date", "ha", "text"};
|
||||
static const char *const TYPES[] = {"clock", "date", "ha", "text", "countdown", "stopwatch", "image"};
|
||||
static const char *const ALIGNS[] = {"left", "center", "right"};
|
||||
static const char *const OVERLAYS[] = {"top", "box", "invert"};
|
||||
static const char *const LANGUAGES[] = {"nl", "en"};
|
||||
@@ -18,6 +18,15 @@ template <size_t N> static int indexOf(const char *const (&names)[N], const char
|
||||
return -1;
|
||||
}
|
||||
|
||||
static const char *DEGREE_UTF8 = "\xC2\xB0";
|
||||
|
||||
static String withDegrees(const char *text)
|
||||
{
|
||||
String s = text;
|
||||
s.replace(String(DEGREE_SIGN), DEGREE_UTF8);
|
||||
return s;
|
||||
}
|
||||
|
||||
void pageToJson(const DynamicPage &page, JsonObject out)
|
||||
{
|
||||
char time[6];
|
||||
@@ -43,20 +52,27 @@ void pageToJson(const DynamicPage &page, JsonObject out)
|
||||
o["format"] = e.format;
|
||||
o["language"] = LANGUAGES[(int)e.language];
|
||||
o["decimals"] = e.decimals;
|
||||
o["text"] = e.text;
|
||||
o["suffix"] = e.suffix;
|
||||
o["text"] = withDegrees(e.text);
|
||||
o["suffix"] = withDegrees(e.suffix);
|
||||
o["entity"] = e.entity;
|
||||
o["attribute"] = e.attribute;
|
||||
o["factor"] = e.factor;
|
||||
o["scroll_width"] = e.scroll_width;
|
||||
o["scroll_speed"] = e.scroll_speed;
|
||||
char target[17];
|
||||
formatTarget(e.target, target);
|
||||
o["target"] = String(target);
|
||||
}
|
||||
}
|
||||
|
||||
// False when the text does not fit
|
||||
// False when the text does not fit. The degree sign (UTF-8) becomes DEGREE_SIGN.
|
||||
static bool copyText(JsonVariantConst value, char *out, size_t size)
|
||||
{
|
||||
const char *text = value | "";
|
||||
if (strlen(text) >= size)
|
||||
String text = value | "";
|
||||
text.replace(DEGREE_UTF8, String(DEGREE_SIGN));
|
||||
if (text.length() >= size)
|
||||
return false;
|
||||
strcpy(out, text);
|
||||
strcpy(out, text.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -95,6 +111,12 @@ const char *pageFromJson(JsonVariantConst in, DynamicPage &page)
|
||||
e.y = constrain(o["y"] | 0, -1000, 1000);
|
||||
e.format = constrain(o["format"] | 0, 0, 255);
|
||||
e.decimals = constrain(o["decimals"] | (int)DECIMALS_AS_IS, -100, 100);
|
||||
e.factor = o["factor"] | 1.0f;
|
||||
e.scroll_width = constrain(o["scroll_width"] | 0, 0, 255);
|
||||
e.scroll_speed = constrain(o["scroll_speed"] | 8, 0, 255);
|
||||
const char *target = o["target"] | "";
|
||||
if (e.type == ElementType::Countdown && !parseTarget(target, e.target))
|
||||
return "Countdown: enter a date and time";
|
||||
if (!copyText(o["font"], e.font, sizeof(e.font)) || !copyText(o["text"], e.text, sizeof(e.text)) ||
|
||||
!copyText(o["suffix"], e.suffix, sizeof(e.suffix)) || !copyText(o["entity"], e.entity, sizeof(e.entity)) ||
|
||||
!copyText(o["attribute"], e.attribute, sizeof(e.attribute)))
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
// {slots: [{start: "08:00", bitmap: "Pikachu"}],
|
||||
// elements: [{type: "clock"|"date"|"ha"|"text", x, y, align: "left"|"center"|"right",
|
||||
// overlay: "top"|"box"|"invert", font, format, language: "nl"|"en",
|
||||
// decimals (-1 = as received), text, suffix, entity, attribute}]}
|
||||
// decimals (-1 = as received), text, suffix, entity, attribute, factor,
|
||||
// scroll_width, scroll_speed, target ("YYYY-MM-DDTHH:MM")}]}
|
||||
// type also "countdown", "stopwatch", "image". Texts use the UTF-8 degree sign.
|
||||
|
||||
void pageToJson(const DynamicPage &page, JsonObject out);
|
||||
// Returns nullptr or a message for the user. Validates the page (validatePage()).
|
||||
|
||||
+48
-21
@@ -50,6 +50,8 @@ button.d{color:var(--bad)}
|
||||
<section id="t-show"><div class="card"><h2>What to show during the day</h2>
|
||||
<div id="livebox" class="row" style="display:none"><b>Live mode is on.</b><button class="s" onclick="stopLive()">Back to normal</button></div>
|
||||
<div class="grid" id="pages"></div>
|
||||
<div class="row"><b>Stopwatch</b> <span id="swtime">0:00</span><button class="s" onclick="stopwatch('start')">Start</button>
|
||||
<button class="s" onclick="stopwatch('stop')">Stop</button><button class="s" onclick="stopwatch('reset')">Reset</button></div>
|
||||
<p class="hint">The schedule (Settings) decides when the display is on. The page can also be chosen with the Home Assistant "Mode" dropdown.</p></div>
|
||||
<div class="card"><h2>Left on the display when it switches off</h2><div class="grid" id="offpages"></div>
|
||||
<p class="hint">Drawn at night and when the Home Assistant Power switch turns the display off; the dots keep it without power. Also in Home Assistant: "Off image".</p></div></section>
|
||||
@@ -68,8 +70,10 @@ button.d{color:var(--bad)}
|
||||
<p class="hint">Each background is shown from its time until the next one; the last one continues after midnight.</p>
|
||||
<h3>Elements</h3><div id="pe_elems"></div>
|
||||
<div class="row"><button class="s" onclick="peAdd('clock')">+ Clock</button><button class="s" onclick="peAdd('date')">+ Date</button>
|
||||
<button class="s" onclick="peAdd('ha')">+ Home Assistant value</button><button class="s" onclick="peAdd('text')">+ Text</button></div>
|
||||
<p class="hint">Drag elements on the preview, or select one and use the arrow keys. X is the left edge, the middle or the right edge (Align). Box clears the dots behind the element, Inverted flips the dots below it. Home Assistant values need the URL and token in the Home Assistant tab.</p></div>
|
||||
<button class="s" onclick="peAdd('ha')">+ Home Assistant value</button><button class="s" onclick="peAdd('text')">+ Text</button>
|
||||
<button class="s" onclick="peAdd('countdown')">+ Countdown</button><button class="s" onclick="peAdd('stopwatch')">+ Stopwatch</button>
|
||||
<button class="s" onclick="peAdd('image')">+ Image</button></div>
|
||||
<p class="hint">Drag elements on the preview, or select one and use the arrow keys. X is the left edge, the middle or the right edge (Align). Box clears the dots behind the element, Inverted flips the dots below it. Home Assistant values need the URL and token in the Home Assistant tab. Type ° for degrees. A text wider than its scroll window scrolls; moving pages (scrolling, seconds) are shown without the refresh effect, as fast as the dots can flip.</p></div>
|
||||
<div class="card"><h2>Fonts</h2><div class="form" id="fontlist"></div>
|
||||
<h3>Add a font</h3>
|
||||
<div class="row"><label><input type="file" id="f_file" accept=".ttf,.otf,.woff,.woff2"> TTF / OTF file</label></div>
|
||||
@@ -81,7 +85,7 @@ button.d{color:var(--bad)}
|
||||
<label>Threshold <input type="range" id="f_thr" min="1" max="254" value="128"></label>
|
||||
<label><input type="checkbox" id="f_mono" checked> equal width digits</label></div>
|
||||
<div id="f_prev" style="display:none"><canvas id="f_canvas" width="840" height="190"></canvas>
|
||||
<div class="row"><input id="f_sample" value="12:34 do 15 okt" maxlength="40"><input id="f_name" placeholder="font name" maxlength="24">
|
||||
<div class="row"><input id="f_sample" value="12:34 do 15 okt 17.5°C" maxlength="40"><input id="f_name" placeholder="font name" maxlength="24">
|
||||
<button class="b" onclick="fontSave()">Save font</button></div></div>
|
||||
<p class="hint">Converted in the browser to dots. Pixel fonts look best at their own size. Height: the dots of one line (19 at most); Baseline: the row the letters stand on; Size: the font size it is drawn with. Only plain characters (no accents).</p></div></section>
|
||||
|
||||
@@ -183,7 +187,7 @@ async function pollScreen(){try{const s=await api('/api/screen');draw($('mirror'
|
||||
setTimeout(pollScreen,document.hidden?5000:1500)}
|
||||
async function pollStatus(){try{status=await api('/api/status');
|
||||
$('statusline').textContent=(status.time||'--:--')+' | WiFi '+(status.wifi.rssi||'')+' dBm | '+status.firmware;
|
||||
$('livebox').style.display=status.live?'flex':'none';sysinfo()}catch(e){}setTimeout(pollStatus,5000)}
|
||||
$('livebox').style.display=status.live?'flex':'none';swShow(status.stopwatch);sysinfo()}catch(e){}setTimeout(pollStatus,5000)}
|
||||
function sysinfo(){if(!status)return;const r=[['Firmware',status.firmware],['Last restart',status.reset_reason],['Uptime',Math.floor(status.uptime/3600)+' h '+Math.floor(status.uptime%3600/60)+' min'],
|
||||
['WiFi',status.wifi.ssid+' ('+status.wifi.ip+', '+status.wifi.rssi+' dBm)'],['Home Assistant',status.ha.enabled?(status.ha.connected?'connected':'not connected'):'disabled'],
|
||||
['HA power switch',status.power_enabled?'on':'off'],
|
||||
@@ -219,13 +223,20 @@ async function selectOff(p){const r=await post('/api/settings',{off_page:p});set
|
||||
async function delBitmap(n){if(!confirm('Delete '+n+'?'))return;await post('/api/bitmap/delete',{name:n});msg('Deleted');settings=await api('/api/settings');loadLists()}
|
||||
async function delPage(n){if(!confirm('Delete page '+n+'?'))return;await post('/api/pages/delete',{name:n});msg('Deleted');if(pe&&same(pe.orig,n))peClose();settings=await api('/api/settings');loadLists()}
|
||||
async function delFont(n){if(!confirm('Delete font '+n+'?'))return;await post('/api/fonts/delete',{name:n});msg('Deleted');loadLists()}
|
||||
function swText(ms){const s=Math.floor(ms/1000),h=Math.floor(s/3600),m=Math.floor(s%3600/60),p=n=>String(n).padStart(2,'0');return(h?h+':'+p(m):m)+':'+p(s%60)}
|
||||
function swShow(sw){if(sw)$('swtime').textContent=swText(sw.ms)+(sw.running?' (running)':'')}
|
||||
async function stopwatch(action){swShow(await post('/api/stopwatch',{action}));if(pe)pePreview()}
|
||||
async function live(bits){await post('/api/live',{data:toHex(bits)});msg('Shown on the display')}
|
||||
async function stopLive(){await post('/api/live/stop');msg('Back to normal')}
|
||||
// page editor: the ESP32 draws the preview and returns the element boxes
|
||||
const ETYPES={clock:'Clock',date:'Date',ha:'Home Assistant value',text:'Text'};
|
||||
const ETYPES={clock:'Clock',date:'Date',ha:'Home Assistant value',text:'Text',countdown:'Countdown',stopwatch:'Stopwatch',image:'Image'};
|
||||
const TEXTTYPES=['clock','date','ha','text','countdown','stopwatch'];
|
||||
const CDF=[['smart: 12 dagen, 5 uur, 34 min, 0:45','smart: 12 days, 5 hours, 34 min, 0:45'],['days: 12 dagen','days: 12 days'],['12d 04:05','12d 04:05'],['12d 04:05:33 (every second)','12d 04:05:33 (every second)']];
|
||||
const DATEF=[['do 15 okt','Thu 15 Oct'],['15 okt','15 Oct'],['15 oktober','15 October'],['15-10','15-10'],['15-10-2026','15-10-2026'],['donderdag','Thursday'],['do','Thu']];
|
||||
let pe=null,peTimer=null,peSeq=0,peData=null,peDrag=null;
|
||||
function newEl(t){return{type:t,x:t=='clock'?27:0,y:t=='clock'?5:0,align:'left',overlay:'top',font:'Normal',format:0,language:'nl',decimals:-1,text:t=='text'?'Hallo':'',suffix:'',entity:'',attribute:''}}
|
||||
function tomorrow(){const d=new Date(Date.now()+864e5),p=n=>String(n).padStart(2,'0');return d.getFullYear()+'-'+p(d.getMonth()+1)+'-'+p(d.getDate())+'T12:00'}
|
||||
function newEl(t){return{type:t,x:t=='clock'?27:0,y:t=='clock'?5:0,align:'left',overlay:'top',font:'Normal',format:0,language:'nl',decimals:-1,
|
||||
text:t=='text'?'Hallo':'',suffix:'',entity:'',attribute:'',factor:1,scroll_width:0,scroll_speed:8,target:tomorrow()}}
|
||||
function peShow(title,name){$('pe_title').textContent=title;$('pe_name').value=name;$('pe').style.display='block';peData=null;peBuild();pePreview();$('pe').scrollIntoView({behavior:'smooth'})}
|
||||
function peOpen(pg,copy){pe={orig:copy?'':pg.name,page:JSON.parse(JSON.stringify(pg.page)),sel:-1,boxes:[],pos:[]};peShow(copy?'Copy of '+pg.name:'Edit '+pg.name,copy?'':pg.name)}
|
||||
function peNew(){pe={orig:'',page:{slots:[{start:'00:00',bitmap:''}],elements:[newEl('clock')]},sel:-1,boxes:[],pos:[]};peShow('New page','')}
|
||||
@@ -233,6 +244,7 @@ function peClose(){pe=null;$('pe').style.display='none';$('t-pages').scrollIntoV
|
||||
function peBuild(){peSlots();peElems()}
|
||||
function field(label,input){return mk('label',{},label+' ',input)}
|
||||
function inp(o,k,type,props){const i=mk('input',Object.assign({type:type||'text',value:o[k]},props||{}));i.oninput=()=>{o[k]=type=='number'?Math.round(+i.value||0):i.value;pePreview()};return i}
|
||||
function inpf(o,k,props){const i=mk('input',Object.assign({type:'number',step:'any',value:o[k]},props||{}));i.oninput=()=>{if(i.value!==''&&+i.value)o[k]=+i.value;pePreview()};return i}
|
||||
function sel(o,k,opts,after){const s=mk('select');opts.forEach(([v,t])=>s.append(mk('option',{value:v,textContent:t})));s.value=o[k];
|
||||
s.onchange=()=>{o[k]=typeof o[k]=='number'?+s.value:s.value;if(after)after();pePreview()};return s}
|
||||
function peSlots(){const d=$('pe_slots');d.innerHTML='';const opts=[['','Blank']].concat(bitmapList.map(b=>[b.name,b.name]));
|
||||
@@ -242,21 +254,36 @@ function peSlots(){const d=$('pe_slots');d.innerHTML='';const opts=[['','Blank']
|
||||
function peAddSlot(){if(pe.page.slots.length>=8){msg('8 time slots at most',true);return}pe.page.slots.push({start:'12:00',bitmap:''});peSort()}
|
||||
function bySlot(a,b){return a.start<b.start?-1:a.start>b.start?1:0}
|
||||
function peSort(){pe.page.slots.sort(bySlot);peSlots();pePreview()}
|
||||
function peElems(){const d=$('pe_elems');d.innerHTML='';
|
||||
function peElems(){const d=$('pe_elems');d.innerHTML='';d.append(mk('datalist',{id:'bmnames'},...bitmapList.map(b=>mk('option',{value:b.name}))));
|
||||
pe.page.elements.forEach((e,i)=>{const box=mk('div',{className:'el'+(i==pe.sel?' sel':'')});box.onclick=()=>{if(pe.sel!=i){pe.sel=i;peMark()}};
|
||||
const rows=[mk('div',{className:'row'},mk('b',{textContent:(i+1)+'. '+ETYPES[e.type]}),btn('Remove',()=>{pe.page.elements.splice(i,1);pe.sel=-1;peElems();pePreview()},'d'))];
|
||||
if(e.type=='clock')rows.push(mk('div',{className:'row'},field('Format',sel(e,'format',[[0,'08:05'],[1,'8:05']]))));
|
||||
if(e.type=='date')rows.push(mk('div',{className:'row'},field('Format',sel(e,'format',DATEF.map((x,k)=>[k,x[e.language=='en'?1:0]]))),
|
||||
field('Language',sel(e,'language',[['nl','Nederlands'],['en','English']],peElems))));
|
||||
if(e.type=='ha'){rows.push(mk('div',{className:'row'},field('Entity',inp(e,'entity','text',{placeholder:'sensor.outside_temperature',size:28,maxLength:64})),
|
||||
field('Attribute',inp(e,'attribute','text',{placeholder:'(the state)',size:12,maxLength:32})),btn('Test',()=>haTest(e))));
|
||||
rows.push(mk('div',{className:'row'},field('Before',inp(e,'text','text',{size:8,maxLength:32})),field('After',inp(e,'suffix','text',{size:6,maxLength:16})),
|
||||
field('Decimals',sel(e,'decimals',[[-1,'as received'],[0,'0'],[1,'1'],[2,'2'],[3,'3'],[4,'4']]))))}
|
||||
if(e.type=='text')rows.push(mk('div',{className:'row'},field('Text',inp(e,'text','text',{size:24,maxLength:32}))));
|
||||
const row=(...k)=>rows.push(mk('div',{className:'row'},...k));
|
||||
if(e.type=='clock')row(field('Format',sel(e,'format',[[0,'08:05'],[1,'8:05'],[2,'08:05:09 (every second)'],[3,'8:05:09 (every second)']])));
|
||||
if(e.type=='date')row(field('Format',sel(e,'format',DATEF.map((x,k)=>[k,x[e.language=='en'?1:0]]))),
|
||||
field('Language',sel(e,'language',[['nl','Nederlands'],['en','English']],peElems)));
|
||||
if(e.type=='ha'){row(field('Entity',inp(e,'entity','text',{placeholder:'sensor.outside_temperature',size:28,maxLength:64})),
|
||||
field('Attribute',inp(e,'attribute','text',{placeholder:'(the state)',size:12,maxLength:32})),btn('Test',()=>haTest(e)));
|
||||
row(field('Before',inp(e,'text','text',{size:8,maxLength:32})),field('After',inp(e,'suffix','text',{size:6,maxLength:16})),
|
||||
field('Multiply by',inpf(e,'factor',{style:'width:6em'})),field('Decimals',sel(e,'decimals',[[-1,'as received'],[0,'0'],[1,'1'],[2,'2'],[3,'3'],[4,'4']])))}
|
||||
if(e.type=='text')row(field('Text',inp(e,'text','text',{size:24,maxLength:32})));
|
||||
if(e.type=='countdown'){row(field('Until',inp(e,'target','datetime-local')),field('Format',sel(e,'format',CDF.map((x,k)=>[k,x[e.language=='en'?1:0]]))),
|
||||
field('Language',sel(e,'language',[['nl','Nederlands'],['en','English']],peElems)));
|
||||
row(field('Before',inp(e,'text','text',{size:10,maxLength:32})),field('When passed',inp(e,'suffix','text',{size:8,maxLength:16,placeholder:'0'})))}
|
||||
if(e.type=='stopwatch'){row(field('Format',sel(e,'format',[[0,'4:05 (every second)'],[1,'0:04 (hours, minutes)'],[2,'0:04:05 (every second)']])),
|
||||
field('Before',inp(e,'text','text',{size:8,maxLength:32})),field('After',inp(e,'suffix','text',{size:6,maxLength:16})));
|
||||
row(btn('Start',()=>stopwatch('start')),btn('Stop',()=>stopwatch('stop')),btn('Reset',()=>stopwatch('reset')))}
|
||||
if(e.type=='image'){const name=inp(e,'text','text',{size:20,maxLength:24,placeholder:e.entity?'weather ':'name'});name.setAttribute('list','bmnames');
|
||||
row(field(e.entity?'Bitmap name start':'Bitmap',name),field('Entity (optional)',inp(e,'entity','text',{placeholder:'weather.knmi_home',size:24,maxLength:64})),
|
||||
field('Attribute',inp(e,'attribute','text',{placeholder:'(the state)',size:10,maxLength:32})),btn('Test',()=>haTest(e)));
|
||||
rows.push(mk('p',{className:'hint',textContent:'With an entity the bitmap is the name start + the state (e.g. "weather " + "cloudy" = "weather cloudy"), or the name start + "default" when there is none. The image is drawn with its top left corner at X, Y.'}))}
|
||||
const fonts=fontList.map(f=>[f.name,f.name+' ('+f.height+')']);if(!fontList.some(f=>same(f.name,e.font)))fonts.push([e.font,e.font+' (gone)']);
|
||||
rows.push(mk('div',{className:'row'},field('X',inp(e,'x','number',{style:'width:5em'})),field('Y',inp(e,'y','number',{style:'width:5em'})),
|
||||
field('Align',sel(e,'align',[['left','left'],['center','center'],['right','right']])),field('Font',sel(e,'font',fonts)),
|
||||
field('Overlay',sel(e,'overlay',[['top','on top'],['box','box (clear behind)'],['invert','inverted']]))));
|
||||
const pos=[field('X',inp(e,'x','number',{style:'width:5em'})),field('Y',inp(e,'y','number',{style:'width:5em'}))];
|
||||
const ovl=field('Overlay',sel(e,'overlay',[['top','on top'],['box','box (clear behind)'],['invert','inverted']]));
|
||||
if(TEXTTYPES.includes(e.type)){
|
||||
row(...pos,field('Align',sel(e,'align',[['left','left'],['center','center'],['right','right']])),field('Font',sel(e,'font',fonts)),ovl);
|
||||
row(field('Scroll in a window of',inp(e,'scroll_width','number',{style:'width:5em',min:0,max:168})),'dots (0: off), at',
|
||||
inp(e,'scroll_speed','number',{style:'width:4em',min:1,max:30}),'dots per second')}
|
||||
else row(...pos,ovl);
|
||||
box.append(...rows);d.append(box)})}
|
||||
function peAdd(t){if(pe.page.elements.length>=8){msg('8 elements at most',true);return}pe.page.elements.push(newEl(t));pe.sel=pe.page.elements.length-1;peElems();pePreview()}
|
||||
function peMark(){document.querySelectorAll('#pe_elems .el').forEach((d,i)=>d.classList.toggle('sel',i==pe.sel));peDraw()}
|
||||
@@ -297,18 +324,18 @@ $('f_h').oninput=()=>{const h=+$('f_h').value;$('f_base').value=Math.round(h*0.8
|
||||
function fontConvert(){if(!fontFam)return;const h=Math.min(19,Math.max(4,Math.round(+$('f_h').value||10))),size=+$('f_size').value||h,base=+$('f_base').value||h,thr=+$('f_thr').value;
|
||||
const c=document.createElement('canvas');c.width=Math.ceil(size*2)+16;c.height=h;const g=c.getContext('2d',{willReadFrequently:true});g.font=size+'px "'+fontFam+'"';g.textBaseline='alphabetic';
|
||||
const glyphs=[];
|
||||
for(let k=32;k<127;k++){const ch=String.fromCharCode(k);g.clearRect(0,0,c.width,h);g.fillStyle='#fff';g.fillText(ch,8,base);
|
||||
for(let k=32;k<128;k++){const ch=k==127?'\u00b0':String.fromCharCode(k);g.clearRect(0,0,c.width,h);g.fillStyle='#fff';g.fillText(ch,8,base);
|
||||
const d=g.getImageData(0,0,c.width,h).data,on=(x,y)=>d[(y*c.width+x)*4+3]>thr;let x0=c.width,x1=-1;
|
||||
for(let x=0;x<c.width;x++)for(let y=0;y<h;y++)if(on(x,y)){x0=Math.min(x0,x);x1=Math.max(x1,x)}
|
||||
let w=x1<0?0:Math.min(32,x1-x0+1);const blank=k==32;if(blank)w=Math.min(32,Math.max(1,Math.round(g.measureText(' ').width)));
|
||||
const rows=[];for(let y=0;y<h;y++){const r=[];for(let x=0;x<w;x++)r.push(!blank&&on(x0+x,y)?1:0);rows.push(r)}glyphs.push({w,rows})}
|
||||
if($('f_mono').checked){const dg=glyphs.slice(16,26),mw=Math.max(...dg.map(x=>x.w));
|
||||
dg.forEach(x=>{const l=Math.floor((mw-x.w)/2),r=mw-x.w-l;x.rows=x.rows.map(row=>Array(l).fill(0).concat(row,Array(r).fill(0)));x.w=mw})}
|
||||
const sp=Math.min(4,Math.max(0,Math.round(+$('f_sp').value||0))),bytes=[70,68,70,49,h,sp,32,95].concat(glyphs.map(x=>x.w));
|
||||
const sp=Math.min(4,Math.max(0,Math.round(+$('f_sp').value||0))),bytes=[70,68,70,49,h,sp,32,96].concat(glyphs.map(x=>x.w));
|
||||
glyphs.forEach(x=>x.rows.forEach(row=>{for(let i=0;i<x.w;i+=8){let v=0;for(let b=0;b<8;b++)if(row[i+b])v|=0x80>>b;bytes.push(v)}}));
|
||||
fontData={bytes,glyphs,h,sp};fontPreview()}
|
||||
function fontPreview(){const b=new Uint8Array(W*H);let x=0,first=true;
|
||||
for(const ch of $('f_sample').value){const k=ch.charCodeAt(0)-32;if(k<0||k>94)continue;const gl=fontData.glyphs[k];if(!gl.w)continue;if(!first)x+=fontData.sp;first=false;
|
||||
for(const ch of $('f_sample').value){const k=ch=='\u00b0'?95:ch.charCodeAt(0)-32;if(k<0||k>95)continue;const gl=fontData.glyphs[k];if(!gl.w)continue;if(!first)x+=fontData.sp;first=false;
|
||||
for(let y=0;y<fontData.h;y++)for(let i=0;i<gl.w;i++)if(gl.rows[y][i]&&x+i<W)b[y*W+x+i]=1;x+=gl.w}
|
||||
draw($('f_canvas'),b)}
|
||||
async function fontSave(){const n=$('f_name').value.trim();if(!n||!fontData){msg('Choose a font file and a name',true);return}
|
||||
|
||||
@@ -161,6 +161,9 @@ static void handleStatus()
|
||||
doc["page"] = appSettings().page;
|
||||
doc["off_page"] = appSettings().off_page;
|
||||
doc["power_enabled"] = controller.powerEnabled();
|
||||
JsonObject sw = doc["stopwatch"].to<JsonObject>();
|
||||
sw["running"] = stopwatch.running();
|
||||
sw["ms"] = stopwatch.elapsed(millis());
|
||||
|
||||
JsonObject ha = doc["ha"].to<JsonObject>();
|
||||
ha["enabled"] = appSettings().home_assistant.enabled;
|
||||
@@ -282,7 +285,7 @@ static void handlePages()
|
||||
{
|
||||
if (!loadPage(info.name.c_str(), *page))
|
||||
continue;
|
||||
renderPage(*page, time, pageResources, bitmap);
|
||||
renderPage(*page, time, millis(), pageResources, bitmap);
|
||||
bitmapToHex(bitmap, hex);
|
||||
JsonDocument doc;
|
||||
doc["name"] = info.name;
|
||||
@@ -346,7 +349,7 @@ static void handlePagePreview()
|
||||
|
||||
uint8_t bitmap[BITMAP_BYTES];
|
||||
ElementBox boxes[PAGE_ELEMENTS];
|
||||
renderPage(*page, time, pageResources, bitmap, boxes);
|
||||
renderPage(*page, time, millis(), pageResources, bitmap, boxes);
|
||||
char hex[BITMAP_HEX_LENGTH + 1];
|
||||
bitmapToHex(bitmap, hex);
|
||||
JsonDocument doc;
|
||||
@@ -479,6 +482,30 @@ static void handleLive()
|
||||
sendOk();
|
||||
}
|
||||
|
||||
// {action: "start" | "stop" | "reset"}
|
||||
static void handleStopwatch()
|
||||
{
|
||||
JsonDocument body;
|
||||
if (!readBody(body))
|
||||
return;
|
||||
const char *action = body["action"] | "";
|
||||
if (strcmp(action, "start") == 0)
|
||||
appStopwatch(StopwatchAction::Start);
|
||||
else if (strcmp(action, "stop") == 0)
|
||||
appStopwatch(StopwatchAction::Stop);
|
||||
else if (strcmp(action, "reset") == 0)
|
||||
appStopwatch(StopwatchAction::Reset);
|
||||
else
|
||||
{
|
||||
sendError(400, "Unknown stopwatch action");
|
||||
return;
|
||||
}
|
||||
JsonDocument doc;
|
||||
doc["running"] = stopwatch.running();
|
||||
doc["ms"] = stopwatch.elapsed(millis());
|
||||
sendJson(200, doc);
|
||||
}
|
||||
|
||||
static void handleLiveStop()
|
||||
{
|
||||
appStopLive();
|
||||
@@ -647,6 +674,7 @@ void webUiBegin()
|
||||
server.on("/api/page", HTTP_POST, handlePage);
|
||||
server.on("/api/live", HTTP_POST, handleLive);
|
||||
server.on("/api/live/stop", HTTP_POST, handleLiveStop);
|
||||
server.on("/api/stopwatch", HTTP_POST, handleStopwatch);
|
||||
server.on("/api/settings", HTTP_GET, handleSettingsGet);
|
||||
server.on("/api/settings", HTTP_POST, handleSettingsPost);
|
||||
server.on("/api/restart", HTTP_POST, handleRestart);
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
// POST /api/page {page: a dynamic page or bitmap name}
|
||||
// POST /api/live show now {data}
|
||||
// POST /api/live/stop
|
||||
// POST /api/stopwatch {action: "start" | "stop" | "reset"} -> {running, ms}
|
||||
// GET /api/settings (the MQTT password and HA token only with ?secrets=1, for
|
||||
// the export; otherwise only *_set)
|
||||
// POST /api/settings changed fields; {settings, restart}
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ skipped and the next line starts clean. Coordinates are 2-digit **hex**. Color i
|
||||
| `s` | `sXXYYc` | Set pixel in the buffer |
|
||||
| `S` | `SXXYYc` | Set pixel and flip it right away (direct mode) |
|
||||
| `F` | `Fc` | Fill the whole buffer |
|
||||
| `D` | `D` | Push the buffer to the display (changed dots only) |
|
||||
| `D` | `D` or `D1` | Push the buffer to the display (changed dots only). `D1`: a frame of moving content (scrolling text, seconds), shown without the refresh effect; the periodic full redraw then waits until the serial line is idle. |
|
||||
| `P` | `PXXYYcsTEXT` | Print text at (XX,YY), color `c`, size `s`. A literal `\n` (backslash, n) in TEXT starts a new line. The whole line can be at most 64 characters. |
|
||||
| `B` | `BXXYYWWHHcHEX...` | Draw a bitmap. WW must be a multiple of 4. Pixel data is one hex digit per 4 pixels, row by row. The data is drawn while it arrives, so it is not limited to 64 characters. |
|
||||
| `R` | `R` | Print the buffer as ASCII art on the serial port |
|
||||
|
||||
@@ -116,8 +116,15 @@ void CommandParser::_runLine()
|
||||
break;
|
||||
case 'd':
|
||||
case 'D':
|
||||
_display.display();
|
||||
_frame_ended = true;
|
||||
// D1: a frame of moving content, shown without the effect. More frames follow
|
||||
// soon, so the periodic full redraw waits until the serial line is idle.
|
||||
if (_line[1] == '1')
|
||||
_display.display(false, true);
|
||||
else
|
||||
{
|
||||
_display.display();
|
||||
_frame_ended = true;
|
||||
}
|
||||
break;
|
||||
case 'f':
|
||||
case 'F':
|
||||
|
||||
Reference in new issue
Block a user