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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user