2026-10-15 01:29:01 +02:00
|
|
|
#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
|
2026-10-17 16:57:31 +02:00
|
|
|
// 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;
|
2026-10-15 01:29:01 +02:00
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
};
|