Files
Flipdot-display/lib/BROSE9323/BROSE9323.cpp
T

532 lines
13 KiB
C++
Raw Normal View History

#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
2026-10-13 01:34:04 +02:00
// 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();
2026-10-13 01:52:34 +02:00
// 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;
2026-10-13 01:52:34 +02:00
// 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
2026-10-13 01:52:34 +02:00
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);
2026-10-13 01:52:34 +02:00
}
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
}