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

+35 -15
View File
@@ -71,7 +71,6 @@ void CommandParser::feed(char c)
return;
}
_line[_line_len++] = c;
_frame_ended = false;
// Bitmap header complete: draw the pixel data as it arrives
if (_line_len == BITMAP_HEADER_LEN && isBitmapCommand(_line[0]))
@@ -82,11 +81,6 @@ void CommandParser::feed(char c)
}
}
bool CommandParser::frameEnded() const
{
return _frame_ended;
}
// Run one complete command line (without the '\n')
void CommandParser::_runLine()
{
@@ -102,6 +96,10 @@ void CommandParser::_runLine()
// Complete bitmap headers are handled in feed()
_error(F("Incomplete bitmap header"));
break;
case 'c':
case 'C':
_checksum();
break;
case 'e':
case 'E':
_effect();
@@ -110,21 +108,18 @@ void CommandParser::_runLine()
case 'G':
_gap();
break;
case 'i':
case 'I':
_refreshInterval();
break;
case 'n':
case 'N':
_pulses();
break;
case 'd':
case 'D':
// D1: a frame of moving content, shown without the effect. More frames follow
// soon, so the periodic full redraw waits until the serial line is idle.
if (_line[1] == '1')
_display.display(false, true);
else
{
_display.display();
_frame_ended = true;
}
// D1: a frame of moving content, shown without the effect
_display.display(false, _line[1] == '1');
break;
case 'f':
case 'F':
@@ -320,3 +315,28 @@ void CommandParser::_effect()
_display.setEffect(effect, t, flags);
saveEffect(effect, t, flags);
}
// C: replies "C xxxx", the bufferCrc() of the image received so far as 4 hex digits. The
// ESP32 sends it after a frame and compares it with what it sent.
void CommandParser::_checksum()
{
uint16_t crc = _display.crc();
char text[7] = "C ";
for (uint8_t i = 0; i < 4; i++)
text[2 + i] = "0123456789ABCDEF"[(crc >> (12 - 4 * i)) & 0xF];
text[6] = '\0';
_reply.println(text);
}
// Immm (mmm = decimal minutes between full redraws, 0 = never, up to 1440), stored in EEPROM
void CommandParser::_refreshInterval()
{
long m = atol(_line + 1);
if (_line_len < 2 || m < 0 || m > REFRESH_MINUTES_MAX)
{
_error(F("Refresh interval out of range (0-1440 min)"));
return;
}
_refresh_minutes = m;
saveRefreshMinutes(m);
}