Refresh effect per page
Pages choose their own refresh effect (or the Hardware setting), used when the page appears and for its own changes. Schedule blocks that switch instantly and rules still switch instantly. The ESP32 sends page effects to the Nano with a new no-save flag (E ...,2), so page switches don't wear the Nano's EEPROM. Page format 7 (version 6 pages are converted). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No files matched your search
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 370 KiB After Width: | Height: | Size: 367 KiB |
|
Before Width: | Height: | Size: 405 KiB After Width: | Height: | Size: 369 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 339 KiB After Width: | Height: | Size: 349 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 150 KiB |
@@ -46,6 +46,13 @@ the degree sign. Fonts: Small (3x5), Normal (5x7), Large (5x7 doubled), Full hei
|
|||||||
digits), and your own: upload a TTF/OTF, choose the height in dots, the browser converts
|
digits), and your own: upload a TTF/OTF, choose the height in dots, the browser converts
|
||||||
it.
|
it.
|
||||||
|
|
||||||
|
Each page can have its own refresh effect (e.g. Clock: sparkle, Weather: wipe), or use the
|
||||||
|
one from Settings. It is used when the page appears and for its own changes, like the
|
||||||
|
clock's minutes. A schedule block that switches instantly and rules still switch
|
||||||
|
instantly; "small changes only" and the effect time stay global settings. The ESP32 sends
|
||||||
|
it to the Nano without storing it in the Nano's EEPROM (`E` with flag 2), so switching
|
||||||
|
pages often doesn't wear the EEPROM.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Schedule
|
## Schedule
|
||||||
@@ -111,7 +118,7 @@ The display schedule (on/off, when the display is on, the late-evening quarter-h
|
|||||||
when it switches off); the "No WiFi" screen; the flip time, the pause after each pulse,
|
when it switches off); the "No WiFi" screen; the flip time, the pause after each pulse,
|
||||||
pulses per dot and the refresh effect (instant, sparkle, wipe, rain, from the middle out,
|
pulses per dot and the refresh effect (instant, sparkle, wipe, rain, from the middle out,
|
||||||
diagonal, spiral, flip all then reveal, push, random; optionally only for small changes
|
diagonal, spiral, flip all then reveal, push, random; optionally only for small changes
|
||||||
like the clock's minutes); how often every dot is flipped again ("Dot refresh", default
|
like the clock's minutes; pages can choose their own effect); how often every dot is flipped again ("Dot refresh", default
|
||||||
2 min) and how often the whole image is sent again ("Image resync", default 10 min; 0
|
2 min) and how often the whole image is sent again ("Image resync", default 10 min; 0
|
||||||
switches either off); test patterns; the status LED.
|
switches either off); test patterns; the status LED.
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ enum RefreshEffect : uint8_t
|
|||||||
|
|
||||||
// Flags for BROSE9323::setEffect()
|
// Flags for BROSE9323::setEffect()
|
||||||
const uint8_t EFFECT_FLAG_SMALL_ONLY = 1; // big changes (> 1/10 of the dots) are instant
|
const uint8_t EFFECT_FLAG_SMALL_ONLY = 1; // big changes (> 1/10 of the dots) are instant
|
||||||
|
// E command only: use the effect until the next E command or power cycle, without storing
|
||||||
|
// it in the Nano's EEPROM (pages with their own effect switch it often)
|
||||||
|
const uint8_t EFFECT_FLAG_NO_SAVE = 2;
|
||||||
|
|
||||||
// Visits every dot of a w x h display exactly once, in the order of an effect. Needs no
|
// Visits every dot of a w x h display exactly once, in the order of an effect. Needs no
|
||||||
// buffer: the sparkle order comes from a maximal length LFSR (all states except 0, out
|
// buffer: the sparkle order comes from a maximal length LFSR (all states except 0, out
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ static bool time_known = false;
|
|||||||
static void showDynamicPage()
|
static void showDynamicPage()
|
||||||
{
|
{
|
||||||
frames.setDynamicPage(loaded_page);
|
frames.setDynamicPage(loaded_page);
|
||||||
|
frames.setPageEffect(loaded_page.effect);
|
||||||
haApiWatch(&loaded_page);
|
haApiWatch(&loaded_page);
|
||||||
controller.setPage(Page::Dynamic);
|
controller.setPage(Page::Dynamic);
|
||||||
}
|
}
|
||||||
@@ -63,6 +64,7 @@ static void applyPage(bool instant = false)
|
|||||||
if (loadBitmap(shown, bitmap))
|
if (loadBitmap(shown, bitmap))
|
||||||
{
|
{
|
||||||
frames.setPageBitmap(bitmap);
|
frames.setPageBitmap(bitmap);
|
||||||
|
frames.setPageEffect(PAGE_EFFECT_DEFAULT);
|
||||||
haApiWatch(nullptr);
|
haApiWatch(nullptr);
|
||||||
controller.setPage(Page::Bitmap);
|
controller.setPage(Page::Bitmap);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -118,7 +118,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_CONNECT_TIMEOUT = 30000;
|
||||||
const uint32_t WIFI_RESTART_TIME = 10UL * 60 * 1000;
|
const uint32_t WIFI_RESTART_TIME = 10UL * 60 * 1000;
|
||||||
|
|
||||||
#define FIRMWARE_VERSION "2.13.0"
|
#define FIRMWARE_VERSION "2.14.0"
|
||||||
|
|
||||||
// Home Assistant device id (do not change: Home Assistant's entities are linked to it)
|
// 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};
|
const uint8_t HA_DEVICE_ID[] = {0x34, 0x86, 0x5d, 0x3a, 0x08, 0x28};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ uint32_t FlipdotFrames::dynamicPage(const LocalTime &time, uint32_t now, bool re
|
|||||||
bool moving = wait != 0;
|
bool moving = wait != 0;
|
||||||
// The Nano has no acknowledgement: the next frame waits until it has received this one
|
// The Nano has no acknowledgement: the next frame waits until it has received this one
|
||||||
// and flipped its dots, or the next frame overflows its serial buffer
|
// and flipped its dots, or the next frame overflows its serial buffer
|
||||||
|
_useEffect(_page_effect, redraw);
|
||||||
uint32_t busy = _show(bitmap, !redraw, moving || _instant_next);
|
uint32_t busy = _show(bitmap, !redraw, moving || _instant_next);
|
||||||
_instant_next = false;
|
_instant_next = false;
|
||||||
return moving && wait < busy ? busy : wait;
|
return moving && wait < busy ? busy : wait;
|
||||||
@@ -26,6 +27,19 @@ uint32_t FlipdotFrames::dynamicPage(const LocalTime &time, uint32_t now, bool re
|
|||||||
void FlipdotFrames::setDrive(uint16_t flip_time, const DriveSettings &drive)
|
void FlipdotFrames::setDrive(uint16_t flip_time, const DriveSettings &drive)
|
||||||
{
|
{
|
||||||
_dot_us = (uint32_t)drive.pulses * (flip_time + drive.flip_gap);
|
_dot_us = (uint32_t)drive.pulses * (flip_time + drive.flip_gap);
|
||||||
|
_drive = drive;
|
||||||
|
_nano_effect = EFFECT_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlipdotFrames::_useEffect(uint8_t effect, bool full)
|
||||||
|
{
|
||||||
|
if (effect == PAGE_EFFECT_DEFAULT || effect >= DRIVE_EFFECT_COUNT)
|
||||||
|
effect = _drive.effect;
|
||||||
|
if (effect == _nano_effect && !full)
|
||||||
|
return;
|
||||||
|
uint8_t flags = EFFECT_FLAG_NO_SAVE | (_drive.effect_small_only ? EFFECT_FLAG_SMALL_ONLY : 0);
|
||||||
|
_display.setEffect(effect, _drive.effect_time, flags);
|
||||||
|
_nano_effect = effect;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sends the bitmap; returns about how long the Nano takes to receive and show it in ms
|
// Sends the bitmap; returns about how long the Nano takes to receive and show it in ms
|
||||||
@@ -45,12 +59,14 @@ void FlipdotFrames::noWifi(int hour, int minute)
|
|||||||
{
|
{
|
||||||
char time[6];
|
char time[6];
|
||||||
snprintf(time, sizeof(time), "%02d:%02d", hour, minute);
|
snprintf(time, sizeof(time), "%02d:%02d", hour, minute);
|
||||||
|
_useEffect(PAGE_EFFECT_DEFAULT, false);
|
||||||
drawTextLines(_display, "No WiFi", time);
|
drawTextLines(_display, "No WiFi", time);
|
||||||
_display.display();
|
_display.display();
|
||||||
}
|
}
|
||||||
|
|
||||||
void FlipdotFrames::page()
|
void FlipdotFrames::page()
|
||||||
{
|
{
|
||||||
|
_useEffect(_page_effect, true);
|
||||||
_show(_page, false, _instant_next);
|
_show(_page, false, _instant_next);
|
||||||
_instant_next = false;
|
_instant_next = false;
|
||||||
}
|
}
|
||||||
@@ -90,5 +106,6 @@ void FlipdotFrames::setOffBitmap(const uint8_t *bitmap)
|
|||||||
|
|
||||||
void FlipdotFrames::_drawFullScreen(const uint8_t *bitmap)
|
void FlipdotFrames::_drawFullScreen(const uint8_t *bitmap)
|
||||||
{
|
{
|
||||||
|
_useEffect(PAGE_EFFECT_DEFAULT, true);
|
||||||
_show(bitmap, false, false);
|
_show(bitmap, false, false);
|
||||||
}
|
}
|
||||||
@@ -29,8 +29,15 @@ public:
|
|||||||
// The next page frame is shown without the refresh effect (a rule's page, a schedule
|
// The next page frame is shown without the refresh effect (a rule's page, a schedule
|
||||||
// block that switches instantly)
|
// block that switches instantly)
|
||||||
void showNextInstant() { _instant_next = true; }
|
void showNextInstant() { _instant_next = true; }
|
||||||
// How the Nano flips the dots: for the time a frame takes
|
// How the Nano flips the dots: for the time a frame takes, and the refresh effect
|
||||||
void setDrive(uint16_t flip_time, const DriveSettings &drive);
|
void setDrive(uint16_t flip_time, const DriveSettings &drive);
|
||||||
|
// The refresh effect of the shown page (PAGE_EFFECT_DEFAULT: the one of the drive
|
||||||
|
// settings). Sent to the Nano before page frames, without storing it in its EEPROM;
|
||||||
|
// the other screens (off, live, No WiFi) use the drive settings' effect.
|
||||||
|
void setPageEffect(uint8_t effect) { _page_effect = effect; }
|
||||||
|
// The Nano got the stored drive settings again (StatusScreen): its effect is the
|
||||||
|
// default one now
|
||||||
|
void driveSent() { _nano_effect = EFFECT_UNKNOWN; }
|
||||||
void setPageBitmap(const uint8_t *bitmap);
|
void setPageBitmap(const uint8_t *bitmap);
|
||||||
void setLiveBitmap(const uint8_t *bitmap);
|
void setLiveBitmap(const uint8_t *bitmap);
|
||||||
// nullptr: blank
|
// nullptr: blank
|
||||||
@@ -40,6 +47,10 @@ private:
|
|||||||
BROSE9323 &_display;
|
BROSE9323 &_display;
|
||||||
PageResources &_resources;
|
PageResources &_resources;
|
||||||
DynamicPage _dynamic;
|
DynamicPage _dynamic;
|
||||||
|
static const uint8_t EFFECT_UNKNOWN = 254;
|
||||||
|
DriveSettings _drive;
|
||||||
|
uint8_t _page_effect = PAGE_EFFECT_DEFAULT;
|
||||||
|
uint8_t _nano_effect = EFFECT_UNKNOWN; // the effect last sent by _useEffect()
|
||||||
uint32_t _dot_us = 1000;
|
uint32_t _dot_us = 1000;
|
||||||
bool _instant_next = false; // per changed dot: pulses x (flip time + gap)
|
bool _instant_next = false; // per changed dot: pulses x (flip time + gap)
|
||||||
uint8_t _page[BITMAP_BYTES] = {};
|
uint8_t _page[BITMAP_BYTES] = {};
|
||||||
@@ -47,5 +58,8 @@ private:
|
|||||||
uint8_t _off[BITMAP_BYTES] = {}; // all zero: blank
|
uint8_t _off[BITMAP_BYTES] = {}; // all zero: blank
|
||||||
|
|
||||||
void _drawFullScreen(const uint8_t *bitmap);
|
void _drawFullScreen(const uint8_t *bitmap);
|
||||||
|
// Makes the Nano use this effect (PAGE_EFFECT_DEFAULT: the drive settings' one). Sent
|
||||||
|
// when it changed, and always with full frames: a restarted Nano has the stored one.
|
||||||
|
void _useEffect(uint8_t effect, bool full);
|
||||||
uint32_t _show(const uint8_t *bitmap, bool changes_only, bool instant);
|
uint32_t _show(const uint8_t *bitmap, bool changes_only, bool instant);
|
||||||
};
|
};
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
static const char *DIR = "/pages";
|
static const char *DIR = "/pages";
|
||||||
static const char *EXTENSION = ".pg";
|
static const char *EXTENSION = ".pg";
|
||||||
// Increase when DynamicPage changes; older files are ignored unless converted below
|
// Increase when DynamicPage changes; older files are ignored unless converted below
|
||||||
static const uint16_t VERSION = 6;
|
static const uint16_t VERSION = 7;
|
||||||
// Elements per page up to version 5 (the old formats below are frozen with it)
|
// Elements per page up to version 5 (the old formats below are frozen with it)
|
||||||
static const int OLD_PAGE_ELEMENTS = 8;
|
static const int OLD_PAGE_ELEMENTS = 8;
|
||||||
|
|
||||||
@@ -356,9 +356,40 @@ static void convert(const Page &old, DynamicPage &page)
|
|||||||
}
|
}
|
||||||
} // namespace v5
|
} // namespace v5
|
||||||
|
|
||||||
|
// Version 6 (before the refresh effect per page): the current layout without effect.
|
||||||
|
// Frozen copy; the elements did not change since, so they are copied as a block.
|
||||||
|
namespace v6
|
||||||
|
{
|
||||||
|
struct Page
|
||||||
|
{
|
||||||
|
uint8_t slot_count;
|
||||||
|
PageSlot slots[PAGE_SLOTS];
|
||||||
|
uint8_t element_count;
|
||||||
|
PageElement elements[PAGE_ELEMENTS];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Stored
|
||||||
|
{
|
||||||
|
uint16_t version;
|
||||||
|
uint16_t size;
|
||||||
|
Page page;
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(offsetof(DynamicPage, elements) == offsetof(Page, elements), "same layout up to the elements");
|
||||||
|
|
||||||
|
static void convert(const Page &old, DynamicPage &page)
|
||||||
|
{
|
||||||
|
clearPage(page); // the default effect
|
||||||
|
page.slot_count = old.slot_count;
|
||||||
|
memcpy(page.slots, old.slots, sizeof(page.slots));
|
||||||
|
page.element_count = old.element_count;
|
||||||
|
memcpy((void *)page.elements, old.elements, sizeof(page.elements));
|
||||||
|
}
|
||||||
|
} // namespace v6
|
||||||
|
|
||||||
static_assert(sizeof(v1::Stored) != sizeof(v2::Stored) && sizeof(v2::Stored) != sizeof(v3::Stored) &&
|
static_assert(sizeof(v1::Stored) != sizeof(v2::Stored) && sizeof(v2::Stored) != sizeof(v3::Stored) &&
|
||||||
sizeof(v3::Stored) != sizeof(v4::Stored) && sizeof(v4::Stored) != sizeof(v5::Stored) &&
|
sizeof(v3::Stored) != sizeof(v4::Stored) && sizeof(v4::Stored) != sizeof(v5::Stored) &&
|
||||||
sizeof(v5::Stored) != sizeof(StoredPage),
|
sizeof(v5::Stored) != sizeof(v6::Stored) && sizeof(v6::Stored) != sizeof(StoredPage),
|
||||||
"the versions are told apart by their size");
|
"the versions are told apart by their size");
|
||||||
|
|
||||||
static uint32_t revision = 0;
|
static uint32_t revision = 0;
|
||||||
@@ -429,6 +460,18 @@ bool loadPage(const char *name, DynamicPage &page)
|
|||||||
page = stored->page;
|
page = stored->page;
|
||||||
delete stored;
|
delete stored;
|
||||||
}
|
}
|
||||||
|
else if (size == sizeof(v6::Stored))
|
||||||
|
{
|
||||||
|
v6::Stored *stored = new v6::Stored;
|
||||||
|
ok = file.read((uint8_t *)stored, sizeof(v6::Stored)) == sizeof(v6::Stored) && stored->version == 6 &&
|
||||||
|
stored->size == sizeof(v6::Page);
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
v6::convert(stored->page, page);
|
||||||
|
ok = validatePage(page) == nullptr;
|
||||||
|
}
|
||||||
|
delete stored;
|
||||||
|
}
|
||||||
else if (size == sizeof(v5::Stored))
|
else if (size == sizeof(v5::Stored))
|
||||||
{
|
{
|
||||||
v5::Stored *stored = new v5::Stored;
|
v5::Stored *stored = new v5::Stored;
|
||||||
|
|||||||
@@ -22,16 +22,17 @@ void StatusScreen::sendDrive()
|
|||||||
_drive_pending = true;
|
_drive_pending = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void StatusScreen::loop()
|
bool StatusScreen::loop()
|
||||||
{
|
{
|
||||||
if (!_drive_pending || !_started || !_nano.isOn())
|
if (!_drive_pending || !_started || !_nano.isOn())
|
||||||
return;
|
return false;
|
||||||
_drive_pending = false;
|
_drive_pending = false;
|
||||||
_display.setTiming(_flip_time);
|
_display.setTiming(_flip_time);
|
||||||
_display.setGap(_drive.flip_gap);
|
_display.setGap(_drive.flip_gap);
|
||||||
_display.setPulses(_drive.pulses);
|
_display.setPulses(_drive.pulses);
|
||||||
_display.setEffect(_drive.effect, _drive.effect_time, _drive.effect_small_only ? EFFECT_FLAG_SMALL_ONLY : 0);
|
_display.setEffect(_drive.effect, _drive.effect_time, _drive.effect_small_only ? EFFECT_FLAG_SMALL_ONLY : 0);
|
||||||
_display.setRefreshInterval(_dot_refresh);
|
_display.setRefreshInterval(_dot_refresh);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void StatusScreen::show(const char *line1, const char *line2, bool keep_on)
|
void StatusScreen::show(const char *line1, const char *line2, bool keep_on)
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ public:
|
|||||||
// Send them again (they changed), from loop()
|
// Send them again (they changed), from loop()
|
||||||
void sendDrive();
|
void sendDrive();
|
||||||
// Sends changed drive settings once the display is on. Not while the Nano is flashed.
|
// Sends changed drive settings once the display is on. Not while the Nano is flashed.
|
||||||
void loop();
|
// True when they were sent.
|
||||||
|
bool loop();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
BROSE9323 &_display;
|
BROSE9323 &_display;
|
||||||
|
|||||||
@@ -161,6 +161,8 @@ const char *validatePage(const DynamicPage &page)
|
|||||||
return "Too many time slots";
|
return "Too many time slots";
|
||||||
if (page.element_count > PAGE_ELEMENTS)
|
if (page.element_count > PAGE_ELEMENTS)
|
||||||
return "Too many elements";
|
return "Too many elements";
|
||||||
|
if (page.effect != PAGE_EFFECT_DEFAULT && page.effect >= PAGE_EFFECT_COUNT)
|
||||||
|
return "Unknown refresh effect";
|
||||||
|
|
||||||
for (int i = 0; i < page.slot_count; i++)
|
for (int i = 0; i < page.slot_count; i++)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ const int ELEMENT_ENTITY_SIZE = 65;
|
|||||||
const int ELEMENT_ATTRIBUTE_SIZE = 33;
|
const int ELEMENT_ATTRIBUTE_SIZE = 33;
|
||||||
const int ELEMENT_MAP_SIZE = 64;
|
const int ELEMENT_MAP_SIZE = 64;
|
||||||
const char DEGREE_SIGN = 127;
|
const char DEGREE_SIGN = 127;
|
||||||
|
// DynamicPage::effect: the refresh effect of the Hardware settings
|
||||||
|
const uint8_t PAGE_EFFECT_DEFAULT = 255;
|
||||||
|
const uint8_t PAGE_EFFECT_COUNT = 10; // DRIVE_EFFECT_COUNT (checked in logic/settings.cpp)
|
||||||
|
|
||||||
enum class ElementType : uint8_t
|
enum class ElementType : uint8_t
|
||||||
{
|
{
|
||||||
@@ -184,6 +187,10 @@ struct DynamicPage
|
|||||||
PageSlot slots[PAGE_SLOTS];
|
PageSlot slots[PAGE_SLOTS];
|
||||||
uint8_t element_count = 0;
|
uint8_t element_count = 0;
|
||||||
PageElement elements[PAGE_ELEMENTS];
|
PageElement elements[PAGE_ELEMENTS];
|
||||||
|
// Added in page format 7 (keep new fields at the end, see display/page_store.cpp).
|
||||||
|
// The refresh effect while the page is shown (switching to it and its own changes):
|
||||||
|
// PAGE_EFFECT_DEFAULT or a DRIVE_EFFECT_* number of logic/settings.h
|
||||||
|
uint8_t effect = PAGE_EFFECT_DEFAULT;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Back to an empty page with the defaults, without `page = DynamicPage()`: that can put a
|
// Back to an empty page with the defaults, without `page = DynamicPage()`: that can put a
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
#include "logic/bitmap_format.h"
|
#include "logic/bitmap_format.h"
|
||||||
|
|
||||||
|
static_assert(PAGE_EFFECT_COUNT == DRIVE_EFFECT_COUNT, "pages choose from the same effects");
|
||||||
|
|
||||||
static bool terminated(const char *text, int size)
|
static bool terminated(const char *text, int size)
|
||||||
{
|
{
|
||||||
return memchr(text, '\0', size) != nullptr;
|
return memchr(text, '\0', size) != nullptr;
|
||||||
|
|||||||
@@ -95,7 +95,8 @@ void loop()
|
|||||||
// While the Nano is being flashed, nothing else may talk to it
|
// While the Nano is being flashed, nothing else may talk to it
|
||||||
if (flashBridge.handle())
|
if (flashBridge.handle())
|
||||||
return;
|
return;
|
||||||
status.loop(); // changed flip settings, once the display is on
|
if (status.loop()) // changed flip settings, once the display is on
|
||||||
|
frames.driveSent(); // the effect of a page is sent again
|
||||||
|
|
||||||
LocalTime now;
|
LocalTime now;
|
||||||
bool time_valid = appLocalTime(now);
|
bool time_valid = appLocalTime(now);
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ void pageToJson(const DynamicPage &page, JsonObject out)
|
|||||||
slot["start"] = String(time);
|
slot["start"] = String(time);
|
||||||
slot["bitmap"] = page.slots[i].bitmap;
|
slot["bitmap"] = page.slots[i].bitmap;
|
||||||
}
|
}
|
||||||
|
out["effect"] = page.effect == PAGE_EFFECT_DEFAULT ? -1 : page.effect; // -1: Hardware setting
|
||||||
JsonArray elements = out["elements"].to<JsonArray>();
|
JsonArray elements = out["elements"].to<JsonArray>();
|
||||||
for (int i = 0; i < page.element_count; i++)
|
for (int i = 0; i < page.element_count; i++)
|
||||||
{
|
{
|
||||||
@@ -96,7 +97,11 @@ const char *pageFromJson(JsonVariantConst in, DynamicPage &page)
|
|||||||
if (slots.size() > (size_t)PAGE_SLOTS)
|
if (slots.size() > (size_t)PAGE_SLOTS)
|
||||||
return "Too many time slots (8 at most)";
|
return "Too many time slots (8 at most)";
|
||||||
if (elements.size() > (size_t)PAGE_ELEMENTS)
|
if (elements.size() > (size_t)PAGE_ELEMENTS)
|
||||||
return "Too many elements (8 at most)";
|
return "Too many elements (16 at most)";
|
||||||
|
int effect = in["effect"] | -1;
|
||||||
|
if (effect < -1 || effect >= DRIVE_EFFECT_COUNT)
|
||||||
|
return "Unknown refresh effect";
|
||||||
|
page.effect = effect < 0 ? PAGE_EFFECT_DEFAULT : effect;
|
||||||
|
|
||||||
for (JsonVariantConst s : slots)
|
for (JsonVariantConst s : slots)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ details{margin-top:10px}summary{cursor:pointer;color:var(--dim);font-size:13px;m
|
|||||||
<h3>Background</h3><div id="pe_slots"></div>
|
<h3>Background</h3><div id="pe_slots"></div>
|
||||||
<div class="row"><button class="s" onclick="peAddSlot()">Add time slot</button></div>
|
<div class="row"><button class="s" onclick="peAddSlot()">Add time slot</button></div>
|
||||||
<p class="hint">Each background is shown from its time until the next one; the last one continues after midnight.</p>
|
<p class="hint">Each background is shown from its time until the next one; the last one continues after midnight.</p>
|
||||||
|
<h3>Refresh effect</h3><div class="row"><select id="pe_effect" onchange="pe.page.effect=+this.value"></select></div>
|
||||||
|
<p class="hint">Used when this page appears and for its own changes (like the clock's minutes). A schedule block that switches instantly and rules still switch instantly; "small changes only" and the effect time are in Settings, Hardware.</p>
|
||||||
<h3>Elements</h3><div id="pe_elems"></div>
|
<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>
|
<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>
|
<button class="s" onclick="peAdd('ha')">+ Home Assistant value</button><button class="s" onclick="peAdd('text')">+ Text</button>
|
||||||
@@ -367,7 +369,10 @@ function peShow(title,name){$('pe_title').textContent=title;$('pe_name').value=n
|
|||||||
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 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','')}
|
function peNew(){pe={orig:'',page:{slots:[{start:'00:00',bitmap:''}],elements:[newEl('clock')]},sel:-1,boxes:[],pos:[]};peShow('New page','')}
|
||||||
function peClose(){pe=null;$('pe').style.display='none';$('t-pages').scrollIntoView({behavior:'smooth'})}
|
function peClose(){pe=null;$('pe').style.display='none';$('t-pages').scrollIntoView({behavior:'smooth'})}
|
||||||
function peBuild(){peSlots();peElems()}
|
function peBuild(){peSlots();peElems();peEffect()}
|
||||||
|
function peEffect(){const s=$('pe_effect');if(!s.options.length){s.append(mk('option',{value:-1,textContent:'Default (Hardware setting)'}));
|
||||||
|
for(const o of $('h_effect').options)s.append(mk('option',{value:o.value,textContent:o.textContent}))}
|
||||||
|
if(pe.page.effect==null)pe.page.effect=-1;s.value=pe.page.effect}
|
||||||
function field(label,input){return mk('label',{},label+' ',input)}
|
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 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 chk(o,k,label){const c=mk('input',{type:'checkbox',checked:!!o[k]});c.onchange=()=>{o[k]=c.checked;pePreview()};return mk('label',{},c,' '+label)}
|
function chk(o,k,label){const c=mk('input',{type:'checkbox',checked:!!o[k]});c.onchange=()=>{o[k]=c.checked;pePreview()};return mk('label',{},c,' '+label)}
|
||||||
|
|||||||
@@ -307,13 +307,14 @@ void CommandParser::_effect()
|
|||||||
long t = comma ? atol(comma + 1) : 0;
|
long t = comma ? atol(comma + 1) : 0;
|
||||||
long flags = comma2 ? atol(comma2 + 1) : 0;
|
long flags = comma2 ? atol(comma2 + 1) : 0;
|
||||||
if (_line_len < 2 || !comma || effect < 0 || effect >= EFFECT_COUNT || t < EFFECT_TIME_MIN ||
|
if (_line_len < 2 || !comma || effect < 0 || effect >= EFFECT_COUNT || t < EFFECT_TIME_MIN ||
|
||||||
t > EFFECT_TIME_MAX || flags < 0 || flags > EFFECT_FLAGS_MAX)
|
t > EFFECT_TIME_MAX || flags < 0 || flags > EFFECT_COMMAND_FLAGS_MAX)
|
||||||
{
|
{
|
||||||
_error(F("Invalid effect (Ee,ms,flags: e 0-9, 100-10000 ms, flags 0-1)"));
|
_error(F("Invalid effect (Ee,ms,flags: e 0-9, 100-10000 ms, flags 0-3)"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_display.setEffect(effect, t, flags);
|
_display.setEffect(effect, t, flags & EFFECT_FLAG_SMALL_ONLY);
|
||||||
saveEffect(effect, t, flags);
|
if (!(flags & EFFECT_FLAG_NO_SAVE))
|
||||||
|
saveEffect(effect, t, flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
// C: replies "C xxxx", the bufferCrc() of the image received so far as 4 hex digits. The
|
// C: replies "C xxxx", the bufferCrc() of the image received so far as 4 hex digits. The
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ const uint8_t EFFECT_DEFAULT = 0; // instant
|
|||||||
const uint16_t EFFECT_TIME_DEFAULT = 1500;
|
const uint16_t EFFECT_TIME_DEFAULT = 1500;
|
||||||
const uint16_t EFFECT_TIME_MIN = 100;
|
const uint16_t EFFECT_TIME_MIN = 100;
|
||||||
const uint16_t EFFECT_TIME_MAX = 10000;
|
const uint16_t EFFECT_TIME_MAX = 10000;
|
||||||
const uint8_t EFFECT_FLAGS_MAX = 1; // EFFECT_FLAG_SMALL_ONLY
|
const uint8_t EFFECT_FLAGS_MAX = 1; // EFFECT_FLAG_SMALL_ONLY (stored)
|
||||||
|
const uint8_t EFFECT_COMMAND_FLAGS_MAX = 3; // plus EFFECT_FLAG_NO_SAVE (E command)
|
||||||
|
|
||||||
// Full redraw interval in minutes, fixes dots that did not flip (I command, EEPROM;
|
// Full redraw interval in minutes, fixes dots that did not flip (I command, EEPROM;
|
||||||
// 0 = never). It runs from update() between frames and never blocks the serial port.
|
// 0 = never). It runs from update() between frames and never blocks the serial port.
|
||||||
|
|||||||
@@ -964,6 +964,12 @@ void test_page_validation()
|
|||||||
page = twoSlotPage();
|
page = twoSlotPage();
|
||||||
strcpy(page.slots[0].bitmap, "../x");
|
strcpy(page.slots[0].bitmap, "../x");
|
||||||
TEST_ASSERT_NOT_NULL(validatePage(page));
|
TEST_ASSERT_NOT_NULL(validatePage(page));
|
||||||
|
page = twoSlotPage();
|
||||||
|
TEST_ASSERT_EQUAL(PAGE_EFFECT_DEFAULT, page.effect);
|
||||||
|
page.effect = DRIVE_EFFECT_RANDOM;
|
||||||
|
TEST_ASSERT_NULL(validatePage(page));
|
||||||
|
page.effect = DRIVE_EFFECT_COUNT;
|
||||||
|
TEST_ASSERT_NOT_NULL(validatePage(page)); // unknown refresh effect
|
||||||
|
|
||||||
page = DynamicPage();
|
page = DynamicPage();
|
||||||
page.element_count = 1;
|
page.element_count = 1;
|
||||||
|
|||||||
@@ -445,10 +445,26 @@ void test_effect_is_applied_and_stored()
|
|||||||
send("E0,1500,0\n");
|
send("E0,1500,0\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A page's own effect: used, but the stored one stays (no EEPROM write per page change)
|
||||||
|
void test_effect_without_saving()
|
||||||
|
{
|
||||||
|
send("E1,2500,1\n");
|
||||||
|
send("E5,1000,3\n");
|
||||||
|
TEST_ASSERT_EQUAL(5, display.last("effect")->a);
|
||||||
|
TEST_ASSERT_EQUAL(1000, display.last("effect")->b);
|
||||||
|
TEST_ASSERT_EQUAL(EFFECT_FLAG_SMALL_ONLY, display.last("effect")->c);
|
||||||
|
TEST_ASSERT_FALSE(replied("ERR"));
|
||||||
|
display.calls.clear();
|
||||||
|
setup();
|
||||||
|
TEST_ASSERT_EQUAL(EFFECT_SPARKLE, display.last("effect")->a);
|
||||||
|
TEST_ASSERT_EQUAL(2500, display.last("effect")->b);
|
||||||
|
send("E0,1500,0\n");
|
||||||
|
}
|
||||||
|
|
||||||
void test_invalid_effect_is_rejected()
|
void test_invalid_effect_is_rejected()
|
||||||
{
|
{
|
||||||
send("E10,1500\n"); // unknown effect
|
send("E10,1500\n"); // unknown effect
|
||||||
send("E1,1500,2\n"); // unknown flag
|
send("E1,1500,4\n"); // unknown flag
|
||||||
send("E1,50\n"); // too fast
|
send("E1,50\n"); // too fast
|
||||||
send("E1\n"); // no duration
|
send("E1\n"); // no duration
|
||||||
TEST_ASSERT_EQUAL(0, display.count("effect"));
|
TEST_ASSERT_EQUAL(0, display.count("effect"));
|
||||||
@@ -716,6 +732,7 @@ int main()
|
|||||||
RUN_TEST(test_pulses_are_applied_and_stored);
|
RUN_TEST(test_pulses_are_applied_and_stored);
|
||||||
RUN_TEST(test_pulses_out_of_range_are_rejected);
|
RUN_TEST(test_pulses_out_of_range_are_rejected);
|
||||||
RUN_TEST(test_effect_is_applied_and_stored);
|
RUN_TEST(test_effect_is_applied_and_stored);
|
||||||
|
RUN_TEST(test_effect_without_saving);
|
||||||
RUN_TEST(test_invalid_effect_is_rejected);
|
RUN_TEST(test_invalid_effect_is_rejected);
|
||||||
RUN_TEST(test_every_effect_visits_each_dot_once);
|
RUN_TEST(test_every_effect_visits_each_dot_once);
|
||||||
RUN_TEST(test_wipe_goes_left_to_right_and_center_starts_in_the_middle);
|
RUN_TEST(test_wipe_goes_left_to_right_and_center_starts_in_the_middle);
|
||||||
|
|||||||