Files
Flipdot-display/lib/BROSE9323/BROSE9323.cpp
T
mangavalkandClaude Opus 5.5 fc6fa1a180 Add scrolling, countdown, stopwatch, images, seconds, multiplier and degree sign
- Text elements can scroll in a window at a set speed; clock, countdown and
  stopwatch can show seconds. Moving pages are sent as instant frames (new
  Nano command D1) and paced by the estimated flip time.
- Countdown to a date and time (smart, days, days + time, with seconds),
  stopwatch (web UI, HA switch and reset button), image element (fixed or
  by Home Assistant state), multiplier for HA values, degree sign (char 127)
  in all fonts and the font converter.
- Page format version 2; version 1 pages are converted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 21:01:14 +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, bool instant)
{
#ifdef ESPSERIAL
stream->print(instant ? "D1\n" : "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 || instant)
{
_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
}