#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; } size_t BROSE9323::showBitmap(const uint8_t *bitmap, bool changes_only, bool instant) { // What the Nano shows now, in drawBitmap format (the buffers keep the bits the other // way around) uint8_t *shown = (uint8_t *)malloc(_buffer_size); if (!shown) return 0; for (uint16_t i = 0; i < _buffer_size; i++) { uint8_t b = _old_buffer[i], r = 0; for (uint8_t k = 0; k < 8; k++) if (b & (1 << k)) r |= 0x80 >> k; shown[i] = r; } size_t sent = frameCommands(bitmap, changes_only ? shown : nullptr, width(), height(), instant, [this](const char *text) { stream->print(text); }); free(shown); if (sent) _frames++; for (uint8_t y = 0; y < height(); y++) for (uint8_t x = 0; x < width(); x++) { uint8_t &byte = _new_buffer[y * _buffer_width + x / 8]; if (bitmap[y * _buffer_width + x / 8] & (0x80 >> (x & 7))) byte |= 1 << (x & 7); else byte &= ~(1 << (x & 7)); } memcpy(_old_buffer, _new_buffer, _buffer_size); return sent; } #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(); _frames++; // 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; // A redraw of every dot (after boot) that this frame interrupts: the refresh finishes it if (_animating && _animate_all) startRefresh(); // An explicit full redraw is always instant _startEffect(all, _effect == EFFECT_INSTANT || force || instant); #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 } #ifdef ESPSERIAL void BROSE9323::setRefreshInterval(uint16_t minutes) { stream->write('I'); stream->print(minutes); stream->write('\n'); } #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::drawDots(int16_t x, int16_t y, uint8_t bits, bool color) { if (!(bits & 0xF) || y < 0 || y >= height()) return; // Never used in direct mode (B commands), so only the buffer changes _drawing = true; uint8_t *row = _new_buffer + y * _buffer_width; for (uint8_t mask = 8; mask; mask >>= 1, x++) { if (!(bits & mask) || x < 0 || x >= width()) continue; uint8_t bit = 1 << (x & 7); if (color) row[x >> 3] |= bit; else row[x >> 3] &= ~bit; } } 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); } // Starts showing the new buffer from update(). instant: column by column as fast as // possible, otherwise with the effect. all: also the dots that look unchanged (one pulse // each, they are only repeated in case they didn't flip before). void BROSE9323::_startEffect(bool all, bool instant) { uint16_t total = 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; } uint8_t effect = instant ? EFFECT_INSTANT : _effect; // E.g. only the clock's minutes get the effect, a new page appears at once if (effect != EFFECT_INSTANT && (_effect_flags & EFFECT_FLAG_SMALL_ONLY) && (all || changed > total / 10)) effect = EFFECT_INSTANT; 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; if (effect == EFFECT_INSTANT) { // Column order, no waiting between the dots _sequence.begin(EFFECT_INSTANT, width(), height(), 0); _step_time = 0; } else { 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; } void BROSE9323::startRefresh(void) { _refreshing = true; _refresh_pos = 0; } // The next dots of the full redraw, until about 1 ms after start void BROSE9323::_refreshStep(uint32_t start) { uint8_t h = height(); uint16_t total = width() * h; do { uint8_t x = _refresh_pos / h, y = _refresh_pos % h; _flipDot(x, y, _changed(x, y) ? _pulses : 1); if (++_refresh_pos >= total) { _refreshing = false; return; } } while (micros() - start < 1000); } // 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 (and the // refresh must not show half a frame) if (_drawing) return busy(); // At most about 1 ms per call, so loop() keeps reading the serial port uint32_t start = micros(); if (!_animating) { if (_refreshing) _refreshStep(start); return busy(); } 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 }