Public Access
Home Assistant: remove the Clock switch (its entity is deleted on connect), add an IP sensor and Mode / Off image dropdowns whose options follow the saved bitmaps. Remove the built-in "Nintendo border" bitmap. Off image: a bitmap (or blank) left on the dots when the display switches off. Nano: pause after each flip pulse and repeat pulses for changed dots, both stored in EEPROM (G/N commands). Refresh effects (E command): instant, sparkle, wipe, rain, from the middle out, diagonal, spiral, flip all then reveal, push and random, optionally only for small changes. Effects run in steps from loop() so serial input keeps flowing. Web UI: flip settings, refresh effect and test patterns under Settings; the System tab shows why the ESP32 last restarted. Settings records from versions 1 and 2 are migrated. Firmware 2.3.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
208 lines
4.9 KiB
C++
208 lines
4.9 KiB
C++
#pragma once
|
|
|
|
#include <stdint.h>
|
|
|
|
// The order in which display() flips the dots, and how it is spread over time.
|
|
// The numbers are part of the serial protocol (E command) and the ESP32 settings.
|
|
enum RefreshEffect : uint8_t
|
|
{
|
|
EFFECT_INSTANT = 0, // column by column, left to right, as fast as possible
|
|
EFFECT_SPARKLE = 1, // the changed dots one at a time, in a scrambled order
|
|
EFFECT_WIPE = 2, // column by column, left to right
|
|
EFFECT_RAIN = 3, // row by row, top to bottom
|
|
EFFECT_CENTER = 4, // column by column, from the middle outwards
|
|
EFFECT_DIAGONAL = 5, // from the top left to the bottom right corner
|
|
EFFECT_SPIRAL = 6, // from the outside in, clockwise
|
|
// The ones below are run by BROSE9323 itself (DotSequence uses column order for them)
|
|
EFFECT_FLAP = 7, // every dot to yellow (wipe), then the new image (wipe)
|
|
EFFECT_PUSH = 8, // the new image slides in from the right, pushing the old one out
|
|
EFFECT_RANDOM = 9, // one of the effects 1-8, chosen for each frame
|
|
EFFECT_COUNT = 10,
|
|
};
|
|
|
|
// Flags for BROSE9323::setEffect()
|
|
const uint8_t EFFECT_FLAG_SMALL_ONLY = 1; // big changes (> 1/10 of the dots) are instant
|
|
|
|
// 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
|
|
// of range values skipped), scrambled with a multiplication modulo the dot count.
|
|
class DotSequence
|
|
{
|
|
public:
|
|
void begin(uint8_t effect, uint8_t w, uint8_t h, uint16_t seed)
|
|
{
|
|
_effect = effect < EFFECT_COUNT ? effect : EFFECT_INSTANT;
|
|
_w = w;
|
|
_h = h;
|
|
_total = (uint16_t)w * h;
|
|
_pos = 0;
|
|
_diagonal = 0;
|
|
_left = 0;
|
|
_top = 0;
|
|
_right = w - 1;
|
|
_bottom = h - 1;
|
|
_x = 0;
|
|
_y = 0;
|
|
_direction = 0;
|
|
if (_effect != EFFECT_SPARKLE || _total == 0)
|
|
return;
|
|
|
|
uint8_t bits = 2;
|
|
while (bits < 16 && ((1UL << bits) - 1) < _total)
|
|
bits++;
|
|
// Galois LFSR feedback masks with a maximal period, for 2..16 bits
|
|
static const uint16_t masks[15] = {0x3, 0x6, 0xC, 0x14, 0x30, 0x60, 0xB8, 0x110,
|
|
0x240, 0x500, 0xE08, 0x1C80, 0x3802, 0x6000, 0xD008};
|
|
_mask = masks[bits - 2];
|
|
_period = (uint16_t)((1UL << bits) - 1);
|
|
_lfsr = seed % _period + 1; // any state except 0
|
|
|
|
// A multiplier without common factors with the total keeps the mapping 1:1
|
|
_mult = 997 % _total;
|
|
while (_mult == 0 || gcd(_mult, _total) != 1)
|
|
_mult++;
|
|
}
|
|
|
|
// The next dot; false when every dot was visited
|
|
bool next(uint8_t &x, uint8_t &y)
|
|
{
|
|
if (_effect == EFFECT_SPARKLE)
|
|
{
|
|
while (_pos < _period)
|
|
{
|
|
_pos++;
|
|
uint16_t index = _lfsr - 1;
|
|
_lfsr = (_lfsr & 1) ? (_lfsr >> 1) ^ _mask : _lfsr >> 1;
|
|
if (index < _total)
|
|
{
|
|
uint16_t p = (uint32_t)index * _mult % _total;
|
|
x = p % _w;
|
|
y = p / _w;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (_pos >= _total)
|
|
return false;
|
|
|
|
if (_effect == EFFECT_DIAGONAL)
|
|
{
|
|
// Diagonal d holds the dots with x + y = d, top to bottom
|
|
while (true)
|
|
{
|
|
if (_y > (int16_t)_diagonal || _y >= _h)
|
|
{
|
|
_diagonal++;
|
|
_y = 0;
|
|
continue;
|
|
}
|
|
uint16_t dx = _diagonal - _y;
|
|
uint8_t dy = _y++;
|
|
if (dx < _w)
|
|
{
|
|
x = dx;
|
|
y = dy;
|
|
_pos++;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_effect == EFFECT_SPIRAL)
|
|
{
|
|
x = _x;
|
|
y = _y;
|
|
_pos++;
|
|
// Walk along the edge of the remaining rectangle, then make it smaller
|
|
switch (_direction)
|
|
{
|
|
case 0: // right along the top
|
|
if (_x < _right)
|
|
_x++;
|
|
else
|
|
{
|
|
_top++;
|
|
_direction = 1;
|
|
_y++;
|
|
}
|
|
break;
|
|
case 1: // down the right side
|
|
if (_y < _bottom)
|
|
_y++;
|
|
else
|
|
{
|
|
_right--;
|
|
_direction = 2;
|
|
_x--;
|
|
}
|
|
break;
|
|
case 2: // left along the bottom
|
|
if (_x > _left)
|
|
_x--;
|
|
else
|
|
{
|
|
_bottom--;
|
|
_direction = 3;
|
|
_y--;
|
|
}
|
|
break;
|
|
default: // up the left side
|
|
if (_y > _top)
|
|
_y--;
|
|
else
|
|
{
|
|
_left++;
|
|
_direction = 0;
|
|
_x++;
|
|
}
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
uint16_t p = _pos++;
|
|
if (_effect == EFFECT_RAIN)
|
|
{
|
|
x = p % _w;
|
|
y = p / _w;
|
|
return true;
|
|
}
|
|
uint8_t column = p / _h;
|
|
y = p % _h;
|
|
if (_effect == EFFECT_CENTER)
|
|
{
|
|
uint8_t middle = _w / 2;
|
|
x = (column & 1) ? middle - (column + 1) / 2 : middle + column / 2;
|
|
}
|
|
else
|
|
x = column;
|
|
return true;
|
|
}
|
|
|
|
// Number of next() calls that return true
|
|
uint16_t total() const { return _total; }
|
|
|
|
private:
|
|
static uint16_t gcd(uint16_t a, uint16_t b)
|
|
{
|
|
while (b)
|
|
{
|
|
uint16_t t = a % b;
|
|
a = b;
|
|
b = t;
|
|
}
|
|
return a;
|
|
}
|
|
|
|
uint8_t _effect = EFFECT_INSTANT;
|
|
uint8_t _w = 0, _h = 0;
|
|
uint16_t _total = 0, _pos = 0;
|
|
uint16_t _lfsr = 1, _mask = 0, _period = 0, _mult = 1;
|
|
uint16_t _diagonal = 0; // diagonal
|
|
int16_t _left = 0, _top = 0, _right = 0, _bottom = 0; // spiral
|
|
int16_t _x = 0, _y = 0; // spiral; _y also diagonal
|
|
uint8_t _direction = 0; // spiral
|
|
};
|