Add HA dropdowns, off image, flip fixes and refresh effects

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>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-15 01:29:01 +02:00
1 parent 4a5f6a8b5d
commit 1d3b084de1
42 files changed
+1631 -230

No files matched your search

+235 -41
View File
@@ -75,38 +75,17 @@ void BROSE9323::display(bool force)
#else
if (_direct_mode)
return;
if (_force_next)
bool all = force || _force_next;
_force_next = false;
_drawing = false;
// An explicit full redraw is always instant (the periodic refresh)
if (_effect == EFFECT_INSTANT || force)
{
force = true;
_force_next = false;
_animating = false;
_drawAll(all);
}
for (uint8_t x = 0; x < width(); x++)
{
_selectPanel(x / _panel_width);
_selectColumn(x % _panel_width);
for (uint8_t y = 0; y < height(); y++)
{
bool b = _new_buffer[y * _buffer_width + x / 8] & (1 << (x & 7));
#if !defined(__AVR_ATmega168P__) && !defined(__AVR_ATmega168PB__) && !defined(__AVR_ATmega168__)
if (!force && (bool)(_old_buffer[y * _buffer_width + x / 8] & (1 << (x & 7))) == b)
{
continue;
}
#endif
_selectRow(y);
_setData(_new_buffer[y * _buffer_width + x / 8] & (1 << (x & 7)));
_strobe();
}
}
#if !defined(__AVR_ATmega168P__) && !defined(__AVR_ATmega168PB__) && !defined(__AVR_ATmega168__)
// Store currently displayed content in old buffer
memcpy(_old_buffer, _new_buffer, _buffer_size);
#endif
else
_startEffect(all);
#endif
}
@@ -136,6 +115,8 @@ void BROSE9323::drawPixel(int16_t x, int16_t y, uint16_t color)
#else
if (y >= height() || y < 0 || x >= width() || x < 0)
return;
if (!_direct_mode)
_drawing = true;
if (color)
{
if (_new_buffer[y * _buffer_width + x / 8] & (1 << (x & 7)))
@@ -158,18 +139,8 @@ void BROSE9323::drawPixel(int16_t x, int16_t y, uint16_t color)
_selectColumn(x % _panel_width);
_selectRow(y);
_setData(_new_buffer[y * _buffer_width + x / 8] & (1 << (x & 7)));
_strobe();
#if !defined(__AVR_ATmega168P__) && !defined(__AVR_ATmega168PB__) && !defined(__AVR_ATmega168__)
// The dot is flipped now, so the next display() does not have to flip it again
if (color)
_old_buffer[y * _buffer_width + x / 8] |= 1 << (x & 7);
else
_old_buffer[y * _buffer_width + x / 8] &= ~(1 << (x & 7));
#endif
_flipDot(x, y, _pulses);
}
#endif
}
@@ -181,6 +152,8 @@ void BROSE9323::fillScreen(uint16_t color)
stream->print(color ? "F1\n" : "F0\n");
#else
memset(_new_buffer, color ? 0xFF : 0x00, _buffer_size);
if (!_direct_mode)
_drawing = true;
if (_direct_mode)
{
_setData(color);
@@ -219,6 +192,45 @@ void BROSE9323::setTiming(uint16_t t)
#endif
}
void BROSE9323::setGap(uint16_t t)
{
#ifdef ESPSERIAL
stream->write('G');
stream->print(t);
stream->write('\n');
#else
_flip_gap = t;
#endif
}
void BROSE9323::setPulses(uint8_t n)
{
#ifdef ESPSERIAL
stream->write('N');
stream->print(n);
stream->write('\n');
#else
_pulses = n ? n : 1;
#endif
}
void BROSE9323::setEffect(uint8_t effect, uint16_t time_ms, uint8_t flags)
{
#ifdef ESPSERIAL
stream->write('E');
stream->print(effect);
stream->write(',');
stream->print(time_ms);
stream->write(',');
stream->print(flags);
stream->write('\n');
#else
_effect = effect < EFFECT_COUNT ? effect : EFFECT_INSTANT;
_effect_time = time_ms;
_effect_flags = flags;
#endif
}
void BROSE9323::setDirect(bool d)
{
_direct_mode = d;
@@ -297,6 +309,188 @@ void BROSE9323::_strobe(void)
digitalWrite(ENABLE, 0);
delayMicroseconds(_flip_time);
digitalWrite(ENABLE, 1);
if (_flip_gap)
delayMicroseconds(_flip_gap);
}
// What the panel shows now (without an old buffer: unknown, so the opposite of the
// wanted state, which makes every dot count as changed)
bool BROSE9323::_shown(uint8_t x, uint8_t y) const
{
#if !defined(__AVR_ATmega168P__) && !defined(__AVR_ATmega168PB__) && !defined(__AVR_ATmega168__)
return _old_buffer[y * _buffer_width + x / 8] & (1 << (x & 7));
#else
return !_wanted(x, y);
#endif
}
bool BROSE9323::_changed(uint8_t x, uint8_t y) const
{
return _shown(x, y) != _wanted(x, y);
}
bool BROSE9323::_wanted(uint8_t x, uint8_t y) const
{
return _new_buffer[y * _buffer_width + x / 8] & (1 << (x & 7));
}
// Flip one dot and remember that it is shown
void BROSE9323::_flipTo(uint8_t x, uint8_t y, bool on, uint8_t pulses)
{
_selectPanel(x / _panel_width);
_selectColumn(x % _panel_width);
_selectRow(y);
_setData(on);
for (uint8_t i = 0; i < pulses; i++)
_strobe();
#if !defined(__AVR_ATmega168P__) && !defined(__AVR_ATmega168PB__) && !defined(__AVR_ATmega168__)
if (on)
_old_buffer[y * _buffer_width + x / 8] |= 1 << (x & 7);
else
_old_buffer[y * _buffer_width + x / 8] &= ~(1 << (x & 7));
#endif
}
// Flip one dot to its state in the new buffer
void BROSE9323::_flipDot(uint8_t x, uint8_t y, uint8_t pulses)
{
_flipTo(x, y, _wanted(x, y), pulses);
}
// Column by column, as fast as possible. all: also the dots that look unchanged (one
// pulse each, they are only repeated in case they didn't flip before).
void BROSE9323::_drawAll(bool all)
{
for (uint8_t x = 0; x < width(); x++)
for (uint8_t y = 0; y < height(); y++)
{
bool changed = _changed(x, y);
if (changed || all)
_flipDot(x, y, changed ? _pulses : 1);
}
}
void BROSE9323::_startEffect(bool all)
{
uint16_t total = (uint16_t)width() * height();
uint16_t changed = 0;
for (uint8_t y = 0; y < height(); y++)
for (uint8_t x = 0; x < width(); x++)
changed += _changed(x, y);
if (changed == 0 && !all)
{
_animating = false;
return;
}
// E.g. only the clock's minutes get the effect, a new page appears at once
if ((_effect_flags & EFFECT_FLAG_SMALL_ONLY) && (all || changed > total / 10))
{
_animating = false;
_drawAll(all);
return;
}
uint8_t effect = _effect;
if (effect == EFFECT_RANDOM)
effect = 1 + micros() % (EFFECT_RANDOM - 1);
// Pushing needs to know what is shown; after boot that is unknown
if (all && effect == EFFECT_PUSH)
effect = EFFECT_WIPE;
_running_effect = effect;
_animate_all = all;
_phase = 0;
uint32_t steps;
if (effect == EFFECT_PUSH)
{
// One step per column shift; the dots of a shift are flipped as fast as possible
_push_shift = 1;
_push_pos = 0;
steps = width();
}
else
{
_sequence.begin(effect == EFFECT_FLAP ? EFFECT_WIPE : effect, width(), height(), (uint16_t)micros());
// Sparkle: only the changed dots take time, so a small change sparkles as long as a big one
steps = (effect == EFFECT_SPARKLE && !all) ? changed : total;
if (effect == EFFECT_FLAP)
steps *= 2;
}
_step_time = (uint32_t)_effect_time * 1000 / steps;
_next_step = micros();
_animating = true;
}
// One dot of the current push step. Shift s (1..width) shows old columns s.. on the left
// and new columns 0..s-1 on the right: each column takes over its right neighbour (not
// changed yet in this step, as the columns go left to right), the last one a new column.
// False when the last shift is done.
bool BROSE9323::_pushDot(void)
{
uint8_t w = width(), h = height();
uint8_t x = _push_pos / h, y = _push_pos % h;
bool target = x + 1 < w ? _shown(x + 1, y) : _wanted(_push_shift - 1, y);
if (_shown(x, y) != target)
_flipTo(x, y, target, _push_shift == w ? _pulses : 1);
if (++_push_pos < (uint16_t)w * h)
return true;
_push_pos = 0;
_next_step += _step_time;
return ++_push_shift <= w;
}
bool BROSE9323::busy(void) const
{
return _animating && !_drawing;
}
bool BROSE9323::update(void)
{
// Wait for the D that ends the frame being drawn, it starts the effect again
if (!_animating || _drawing)
return busy();
// At most about 1 ms per call, so loop() keeps reading the serial port
uint32_t start = micros();
while ((int32_t)(micros() - _next_step) >= 0 && micros() - start < 1000)
{
if (_running_effect == EFFECT_PUSH)
{
if (!_pushDot())
{
_animating = false;
break;
}
continue;
}
uint8_t x, y;
if (!_sequence.next(x, y))
{
if (_running_effect == EFFECT_FLAP && _phase == 0)
{
// Everything is yellow now: reveal the new image
_phase = 1;
_sequence.begin(EFFECT_WIPE, width(), height(), 0);
continue;
}
_animating = false;
break;
}
// Flap: first every dot to yellow (one pulse), then the new image
bool yellow_phase = _running_effect == EFFECT_FLAP && _phase == 0;
bool target = yellow_phase ? true : _wanted(x, y);
bool changed = _shown(x, y) != target;
if (!changed && !_animate_all)
{
if (_running_effect == EFFECT_SPARKLE)
continue; // unchanged dots take no time
}
else
_flipTo(x, y, target, changed && !yellow_phase ? _pulses : 1);
_next_step += _step_time;
}
return busy();
}
#endif