Public Access
- 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>
533 lines
13 KiB
C++
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
|
|
}
|