Public Access
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>
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)
|
|
{
|
|
#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
|
|
}
|