Files
Flipdot-display/lib/BROSE9323/dot_sequence.h
T
mangavalkandClaude Opus 5.5 a904615c51 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>
2026-10-17 16:57:31 +02:00

211 lines
5.1 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
// 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
// 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
};