Files
Flipdot-display/lib/BROSE9323/BROSE9323.cpp
T
mangavalkandClaude Opus 5.5 1d3b084de1 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>
2026-10-15 01:29:01 +02:00

533 lines
13 KiB
C++

#include "BROSE9323.h"
BROSE9323::BROSE9323(uint8_t w, uint8_t h, uint8_t pw, uint16_t ft) : Adafruit_GFX(w, h),
_flip_time(ft),
_panel_width(pw),
_buffer_width((w + 7) / 8),
_buffer_size(_buffer_width * h)
{
// Buffer width should fit at least [width] bits
//_buffer_width = (w + 7) / 8;
//_buffer_size = _buffer_width * h;
#if !defined(__AVR_ATmega168P__) && !defined(__AVR_ATmega168PB__) && !defined(__AVR_ATmega168__)
_old_buffer = (uint8_t *)calloc(_buffer_size, sizeof(uint8_t));
#endif
_new_buffer = (uint8_t *)calloc(_buffer_size, sizeof(uint8_t));
}
#ifdef ESPSERIAL
// Only sets the serial port; the first frame is up to the caller
void BROSE9323::begin(Stream *s)
{
stream = s;
}
#else
void BROSE9323::begin(void)
{
digitalWrite(ADDR_0, 1);
digitalWrite(ADDR_1, 1);
digitalWrite(ADDR_2, 1);
digitalWrite(COL_0, 1);
digitalWrite(COL_1, 1);
digitalWrite(COL_2, 1);
digitalWrite(COL_3, 1);
digitalWrite(COL_4, 1);
digitalWrite(ROW_0, 1);
digitalWrite(ROW_1, 1);
digitalWrite(ROW_2, 1);
digitalWrite(ROW_3, 1);
digitalWrite(ROW_4, 1);
digitalWrite(ROW_RESET, 1);
digitalWrite(ROW_SET, 1);
digitalWrite(COL_DATA, 1);
digitalWrite(ENABLE, 1);
pinMode(ADDR_0, OUTPUT);
pinMode(ADDR_1, OUTPUT);
pinMode(ADDR_2, OUTPUT);
pinMode(COL_0, OUTPUT);
pinMode(COL_1, OUTPUT);
pinMode(COL_2, OUTPUT);
pinMode(COL_3, OUTPUT);
pinMode(COL_4, OUTPUT);
pinMode(ROW_0, OUTPUT);
pinMode(ROW_1, OUTPUT);
pinMode(ROW_2, OUTPUT);
pinMode(ROW_3, OUTPUT);
pinMode(ROW_4, OUTPUT);
pinMode(ROW_RESET, OUTPUT);
pinMode(ROW_SET, OUTPUT);
pinMode(COL_DATA, OUTPUT);
pinMode(ENABLE, OUTPUT);
_setData(1);
_setData(0);
}
#endif
void BROSE9323::display(bool force)
{
#ifdef ESPSERIAL
stream->print("D\n");
stream->flush();
// Shadow copy of what the dots show now (for the web UI)
memcpy(_old_buffer, _new_buffer, _buffer_size);
#else
if (_direct_mode)
return;
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)
{
_animating = false;
_drawAll(all);
}
else
_startEffect(all);
#endif
}
void BROSE9323::drawPixel(int16_t x, int16_t y, uint16_t color)
{
#ifdef ESPSERIAL
if (y >= height() || y < 0 || x >= width() || x < 0)
return;
// Shadow copy for the web UI; S (direct mode) is visible right away
if (color)
_new_buffer[y * _buffer_width + x / 8] |= 1 << (x & 7);
else
_new_buffer[y * _buffer_width + x / 8] &= ~(1 << (x & 7));
if (_direct_mode)
{
if (color)
_old_buffer[y * _buffer_width + x / 8] |= 1 << (x & 7);
else
_old_buffer[y * _buffer_width + x / 8] &= ~(1 << (x & 7));
}
char s[10];
sprintf(s, "%c%02x%02x%d\n", _direct_mode ? 'S' : 's', x, y, (bool)color);
stream->print(s);
stream->flush();
#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)))
{
return;
}
_new_buffer[y * _buffer_width + x / 8] |= 1 << (x & 7);
}
else
{
if (~_new_buffer[y * _buffer_width + x / 8] & (1 << (x & 7)))
{
return;
}
_new_buffer[y * _buffer_width + x / 8] &= ~(1 << (x & 7));
}
if (_direct_mode)
{
_selectPanel(x / _panel_width);
_selectColumn(x % _panel_width);
// The dot is flipped now, so the next display() does not have to flip it again
_flipDot(x, y, _pulses);
}
#endif
}
void BROSE9323::fillScreen(uint16_t color)
{
#ifdef ESPSERIAL
memset(_new_buffer, color ? 0xFF : 0x00, _buffer_size);
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);
for (uint8_t x = 0; x < width(); x++)
{
if (x % _panel_width == 0)
{
_selectPanel(x / _panel_width);
}
_selectColumn(x % _panel_width);
for (uint8_t y = 0; y < height(); y++)
{
_selectRow(y);
_strobe();
}
}
#if !defined(__AVR_ATmega168P__) && !defined(__AVR_ATmega168PB__) && !defined(__AVR_ATmega168__)
memset(_old_buffer, color ? 0xFF : 0x00, _buffer_size);
_force_next = false;
#endif
}
#endif
}
void BROSE9323::setTiming(uint16_t t)
{
#ifdef ESPSERIAL
stream->write('T');
stream->print(t);
stream->write('\n');
#else
_flip_time = 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;
}
#ifndef ESPSERIAL
void BROSE9323::printBuffer(void)
{
for (uint8_t y = 0; y < height(); y++)
{
for (uint8_t x = 0; x < width(); x++)
{
Serial.write(_new_buffer[y * _buffer_width + x / 8] & (1 << (x & 7)) ? '*' : ' ');
}
Serial.println();
}
}
void BROSE9323::_selectColumn(uint8_t col)
{
if (_active_col == col)
return;
_active_col = col;
col += 1 + col / 7;
digitalWrite(COL_0, col & 1);
digitalWrite(COL_1, col & 2);
digitalWrite(COL_2, col & 4);
digitalWrite(COL_3, col & 8);
digitalWrite(COL_4, col & 16);
}
void BROSE9323::_selectPanel(uint8_t panel)
{
if (_active_panel == panel)
return;
_active_panel = panel;
digitalWrite(ADDR_0, panel & 1);
digitalWrite(ADDR_1, panel & 2);
digitalWrite(ADDR_2, panel & 4);
}
void BROSE9323::_selectRow(uint8_t row)
{
if (_active_row == row)
return;
_active_row = row;
row += row / 7;
digitalWrite(ROW_0, row & 1);
digitalWrite(ROW_1, row & 2);
digitalWrite(ROW_2, row & 4);
digitalWrite(ROW_3, row & 8);
digitalWrite(ROW_4, row & 16);
}
void BROSE9323::_setData(bool data)
{
if (_active_data == data)
return;
_active_data = data;
if (data)
{
digitalWrite(ROW_RESET, 1);
digitalWrite(ROW_SET, 0);
digitalWrite(COL_DATA, 0);
}
else
{
digitalWrite(ROW_SET, 1);
digitalWrite(ROW_RESET, 0);
digitalWrite(COL_DATA, 1);
}
}
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
size_t BROSE9323::print(const char *ifsh)
{
// replace these characters
/*
UMLAUT:
Ä \x8E
ä \x84
Ö \x99
ö \x94
Ü \x9A
ü \x81
ß \xE0
*/
String temp = ifsh;
temp.replace("Ä", "\x8E");
temp.replace("ä", "\x84");
temp.replace("Ö", "\x99");
temp.replace("ö", "\x94");
temp.replace("Ü", "\x9A");
temp.replace("ü", "\x81");
temp.replace("ß", "\xE0");
ifsh = temp.c_str();
return Adafruit_GFX::print(ifsh);
}
bool BROSE9323::shownPixel(int16_t x, int16_t y) const
{
if (y >= height() || y < 0 || x >= width() || x < 0)
return false;
#if defined(__AVR_ATmega168P__) || defined(__AVR_ATmega168PB__) || defined(__AVR_ATmega168__)
return _new_buffer[y * _buffer_width + x / 8] & (1 << (x & 7));
#else
return _old_buffer[y * _buffer_width + x / 8] & (1 << (x & 7));
#endif
}