Public Access
Fix shifted pictures: non-blocking Nano drawing, frame check, refresh settings
The Nano's periodic full redraw blocked for ~0.8 s. A frame that arrived meanwhile overflowed its 64-byte serial buffer; lost bytes in a bitmap shift the rest of the image (picture moved up), and diff frames never repaired it. - Nano: all drawing (frames, effects, the dot refresh) runs from update() in ~1 ms slices; the refresh pauses while a frame is received or shown - New commands: C (reply with the image checksum) and I (refresh minutes, EEPROM) - ESP32: NanoMonitor asks for the checksum after frames and sends the whole image again on a mismatch, missing answer or Nano restart; counts ERR replies. Periodic whole-image resync. - Settings: "Dot refresh" and "Image resync" intervals (settings v5, v4 records are migrated) - System tab: loop timing, free memory, frame check counts - test_driver: the real driver against a simulated panel Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
58a7cb03c2
commit
090c2bf8ae
45 files changed
+1265
-157
No files matched your search
+79
-45
@@ -42,6 +42,8 @@ size_t BROSE9323::showBitmap(const uint8_t *bitmap, bool changes_only, bool inst
|
||||
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++)
|
||||
@@ -102,6 +104,7 @@ 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
|
||||
@@ -110,14 +113,11 @@ void BROSE9323::display(bool force, bool instant)
|
||||
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);
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -224,6 +224,15 @@ void BROSE9323::setTiming(uint16_t 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
|
||||
@@ -389,22 +398,12 @@ 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)
|
||||
// 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)
|
||||
{
|
||||
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 total = width() * height();
|
||||
uint16_t changed = 0;
|
||||
for (uint8_t y = 0; y < height(); y++)
|
||||
for (uint8_t x = 0; x < width(); x++)
|
||||
@@ -414,15 +413,11 @@ void BROSE9323::_startEffect(bool 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;
|
||||
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
|
||||
@@ -432,27 +427,59 @@ void BROSE9323::_startEffect(bool all)
|
||||
_animate_all = all;
|
||||
_phase = 0;
|
||||
|
||||
uint32_t steps;
|
||||
if (effect == EFFECT_PUSH)
|
||||
if (effect == EFFECT_INSTANT)
|
||||
{
|
||||
// One step per column shift; the dots of a shift are flipped as fast as possible
|
||||
_push_shift = 1;
|
||||
_push_pos = 0;
|
||||
steps = width();
|
||||
// Column order, no waiting between the dots
|
||||
_sequence.begin(EFFECT_INSTANT, width(), height(), 0);
|
||||
_step_time = 0;
|
||||
}
|
||||
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;
|
||||
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;
|
||||
}
|
||||
_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.
|
||||
@@ -478,12 +505,19 @@ bool BROSE9323::busy(void) const
|
||||
|
||||
bool BROSE9323::update(void)
|
||||
{
|
||||
// Wait for the D that ends the frame being drawn, it starts the effect again
|
||||
if (!_animating || _drawing)
|
||||
// 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)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <Adafruit_GFX.h>
|
||||
|
||||
#include "buffer_crc.h"
|
||||
#include "dot_sequence.h"
|
||||
#ifdef ESPSERIAL
|
||||
#include "frame_commands.h"
|
||||
@@ -29,6 +30,7 @@ private:
|
||||
|
||||
#ifdef ESPSERIAL
|
||||
Stream *stream;
|
||||
uint32_t _frames = 0; // frames sent (display() and showBitmap() calls that sent something)
|
||||
#else
|
||||
uint8_t _active_panel = 255;
|
||||
uint8_t _active_col = 255;
|
||||
@@ -49,6 +51,11 @@ private:
|
||||
uint32_t _step_time = 0; // us per step
|
||||
uint32_t _next_step = 0; // micros() of the next step
|
||||
|
||||
// The full redraw (startRefresh()): one pulse for every dot, column by column, while
|
||||
// no frame is being received or drawn
|
||||
bool _refreshing = false;
|
||||
uint16_t _refresh_pos = 0;
|
||||
|
||||
#ifndef FLIPDOT_PLCC_ADAPTER
|
||||
const uint8_t ENABLE = 7,
|
||||
ADDR_0 = 3,
|
||||
@@ -99,8 +106,8 @@ private:
|
||||
void _flipTo(uint8_t x, uint8_t y, bool on, uint8_t pulses);
|
||||
void _flipDot(uint8_t x, uint8_t y, uint8_t pulses);
|
||||
bool _pushDot(void);
|
||||
void _drawAll(bool all);
|
||||
void _startEffect(bool all);
|
||||
void _startEffect(bool all, bool instant);
|
||||
void _refreshStep(uint32_t start);
|
||||
#endif
|
||||
public:
|
||||
BROSE9323(uint8_t, uint8_t, uint8_t, uint16_t ft = 280);
|
||||
@@ -111,10 +118,16 @@ public:
|
||||
// (frame_commands.h); otherwise the whole screen. Returns the characters sent (0:
|
||||
// nothing changed, nothing sent).
|
||||
size_t showBitmap(const uint8_t *bitmap, bool changes_only, bool instant = false);
|
||||
// Frames sent so far (display() and showBitmap() calls that sent something)
|
||||
uint32_t framesSent(void) const { return _frames; }
|
||||
// How often the Nano flips every dot again in minutes, 0 = never (I command)
|
||||
void setRefreshInterval(uint16_t minutes);
|
||||
#endif
|
||||
void begin(void);
|
||||
// Shows the buffer: the changed dots, all with force. instant: without the refresh
|
||||
// effect (frames of moving content, e.g. scrolling text). ESP32: sends D or D1.
|
||||
// Nano: never blocks, the dots are flipped from update() (also for instant frames,
|
||||
// as fast as possible), so the serial port is read while they flip.
|
||||
void display(bool force = false, bool instant = false);
|
||||
void drawPixel(int16_t x, int16_t y, uint16_t color);
|
||||
void fillScreen(uint16_t);
|
||||
@@ -130,10 +143,15 @@ public:
|
||||
// Effects run in steps from update(), so call that often (every loop).
|
||||
void setEffect(uint8_t effect, uint16_t time_ms, uint8_t flags = 0);
|
||||
#ifndef ESPSERIAL
|
||||
// Runs the next steps of an effect. Returns busy().
|
||||
// Flips the next dots of a frame or of the full redraw, at most about 1 ms per call
|
||||
// (plus one dot), so loop() keeps reading the serial port. Returns busy().
|
||||
bool update(void);
|
||||
// An effect is running (paused while a new frame is being drawn into the buffer)
|
||||
// A frame is being shown (paused while a new frame is being drawn into the buffer)
|
||||
bool busy(void) const;
|
||||
// Flip every dot once more (fixes dots that didn't flip), from update() whenever no
|
||||
// frame is being received or shown. Starts again when one is running.
|
||||
void startRefresh(void);
|
||||
bool refreshing(void) const { return _refreshing; }
|
||||
#endif
|
||||
#ifndef ESPSERIAL
|
||||
void printBuffer(void);
|
||||
@@ -141,5 +159,8 @@ public:
|
||||
size_t print(const char *ifsh);
|
||||
// State of a dot as of the last display() call (ESP32: shadow copy for the web UI)
|
||||
bool shownPixel(int16_t x, int16_t y) const;
|
||||
// bufferCrc() of the drawn image (Nano: what it received, the C command; ESP32: what
|
||||
// it sent)
|
||||
uint16_t crc(void) const { return bufferCrc(_new_buffer, width(), height()); }
|
||||
};
|
||||
#endif //BROSE9323_H
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// CRC-16/CCITT-FALSE of a display buffer as BROSE9323 keeps it: rows of (width + 7) / 8
|
||||
// bytes, bit (x & 7) is dot x. The unused bits at the end of each row are left out, so
|
||||
// only the dots count (fillScreen sets them, drawPixel never does). The Nano answers the
|
||||
// C command with it; the ESP32 compares it with its own copy (src/esp32/nano_link).
|
||||
inline uint16_t bufferCrc(const uint8_t *buffer, uint8_t width, uint8_t height)
|
||||
{
|
||||
const uint8_t row_bytes = (width + 7) / 8;
|
||||
const uint8_t last_mask = (width & 7) ? (uint8_t)((1 << (width & 7)) - 1) : 0xFF;
|
||||
uint16_t crc = 0xFFFF;
|
||||
for (uint8_t y = 0; y < height; y++)
|
||||
for (uint8_t i = 0; i < row_bytes; i++)
|
||||
{
|
||||
uint8_t b = buffer[y * row_bytes + i];
|
||||
if (i == row_bytes - 1)
|
||||
b &= last_mask;
|
||||
crc ^= (uint16_t)b << 8;
|
||||
for (uint8_t k = 0; k < 8; k++)
|
||||
crc = (crc & 0x8000) ? (uint16_t)((crc << 1) ^ 0x1021) : (uint16_t)(crc << 1);
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
Reference in new issue
Block a user