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:
mangavalkandClaude Opus 5.5 committed 2026-10-16 15:20:26 +02:00
1 parent 58a7cb03c2
commit 090c2bf8ae
45 files changed
+1265 -157

No files matched your search

+79 -45
View File
@@ -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)