Files
Flipdot-display/src/nano/command_parser.cpp
T
mangavalkandClaude Opus 5.5 090c2bf8ae 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>
2026-10-16 15:20:26 +02:00

343 lines
7.4 KiB
C++

#include "command_parser.h"
#include "settings.h"
// ---------------------------------------------------------------------------
// Hex helpers
// ---------------------------------------------------------------------------
static bool isHex(char c)
{
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
}
static uint8_t hex2dec(char c)
{
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
return 0;
}
static uint8_t hex2byte(const char *s)
{
return (hex2dec(s[0]) << 4) | hex2dec(s[1]);
}
static bool isBitmapCommand(char c)
{
return c == 'b' || c == 'B';
}
// Length of a bitmap header: BxxyywwhhC
static const uint8_t BITMAP_HEADER_LEN = 10;
// ---------------------------------------------------------------------------
// Input handling
// ---------------------------------------------------------------------------
CommandParser::CommandParser(BROSE9323 &display, Print &reply) : _display(display), _reply(reply)
{
}
void CommandParser::feed(char c)
{
if (_bitmap_active)
{
if (c == '\n')
_bitmap_active = false;
else
_bitmapData(c);
return;
}
if (c == '\n')
{
if (!_discard_line)
_runLine();
_line_len = 0;
_discard_line = false;
return;
}
if (c == '\r' || _discard_line)
return;
if (_line_len >= LINE_MAX)
{
_error(F("Line too long"));
_discard_line = true;
return;
}
_line[_line_len++] = c;
// Bitmap header complete: draw the pixel data as it arrives
if (_line_len == BITMAP_HEADER_LEN && isBitmapCommand(_line[0]))
{
_line_len = 0;
if (!_bitmapStart())
_discard_line = true;
}
}
// Run one complete command line (without the '\n')
void CommandParser::_runLine()
{
if (_line_len == 0)
return;
_line[_line_len] = '\0';
char cmd = _line[0];
switch (cmd)
{
case 'b':
case 'B':
// Complete bitmap headers are handled in feed()
_error(F("Incomplete bitmap header"));
break;
case 'c':
case 'C':
_checksum();
break;
case 'e':
case 'E':
_effect();
break;
case 'g':
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
_display.display(false, _line[1] == '1');
break;
case 'f':
case 'F':
_fill();
break;
case 'p':
case 'P':
_print();
break;
case 'r':
case 'R':
_display.printBuffer();
_reply.flush();
break;
case 's':
case 'S':
_pixel(cmd == 'S');
break;
case 't':
case 'T':
_timing();
break;
default:
#ifdef DEBUG
_reply.print(F("0x"));
_reply.print(cmd >> 4, HEX);
_reply.print(cmd & 0xF, HEX);
_reply.println(F(" - command not known."));
#endif
break;
}
}
void CommandParser::_error(const __FlashStringHelper *msg)
{
_reply.print(F("ERR: "));
_reply.println(msg);
}
// ---------------------------------------------------------------------------
// Commands
// ---------------------------------------------------------------------------
// BxxyywwhhcBITMAP (xx/ww = 8 bit hex x position/width; yy/hh = 8 bit hex y position/height;
// c = 0/1 color; BITMAP = one hex digit per 4 pixels, row by row, width must be a multiple of 4)
bool CommandParser::_bitmapStart()
{
_bitmap_x = hex2byte(_line + 1);
_bitmap_y = hex2byte(_line + 3);
_bitmap_w = hex2byte(_line + 5);
_bitmap_h = hex2byte(_line + 7);
_bitmap_color = _line[9] != '0';
if (_bitmap_w == 0 || _bitmap_w % 4)
{
_error(F("Invalid bitmap width (not dividable by 4)"));
return false;
}
if (_bitmap_h == 0)
{
_error(F("Invalid bitmap height (0)"));
return false;
}
_bitmap_nibble = 0;
_bitmap_active = true;
return true;
}
void CommandParser::_bitmapData(char c)
{
if (!isHex(c))
return;
uint16_t pixel = _bitmap_nibble * 4;
if (pixel >= (uint16_t)_bitmap_w * _bitmap_h)
return; // more data than the bitmap size, ignore
uint8_t b = hex2dec(c);
for (uint8_t i = 0; i < 4; i++)
{
if (b & (0x8 >> i))
{
uint16_t p = pixel + i;
_display.drawPixel(_bitmap_x + p % _bitmap_w, _bitmap_y + p / _bitmap_w, _bitmap_color);
}
}
_bitmap_nibble++;
}
// Fc (c = 0/1 color)
void CommandParser::_fill()
{
if (_line_len < 2)
{
_error(F("Missing color"));
return;
}
_display.fillScreen(_line[1] != '0');
}
// PxxyycsTEXT (xx = 8 bit hex x position; yy = 8 bit hex y position; c = 0/1 color;
// s = 1-9 size; TEXT = string, newline = escaped \n)
void CommandParser::_print()
{
if (_line_len < 7)
{
_error(F("Incomplete print command"));
return;
}
bool color = _line[5] != '0';
_display.setCursor(hex2byte(_line + 1), hex2byte(_line + 3));
_display.setTextColor(color, !color);
_display.setTextSize(_line[6] - '0');
for (uint8_t i = 7; i < _line_len; i++)
{
if (_line[i] == '\\' && _line[i + 1] == 'n')
{
_display.println();
i++;
}
else
{
_display.write(_line[i]);
}
}
}
// sxxyyc / Sxxyyc (xx = 8 bit hex x position; yy = 8 bit hex y position; c = 0/1 color)
// s only changes the buffer, S flips the dot immediately
void CommandParser::_pixel(bool direct)
{
if (_line_len < 6)
{
_error(F("Incomplete pixel command"));
return;
}
_display.setDirect(direct);
_display.drawPixel(hex2byte(_line + 1), hex2byte(_line + 3), hex2dec(_line[5]));
_display.setDirect(false);
}
// Tttt (ttt = decimal flip time in us), stored in EEPROM
void CommandParser::_timing()
{
long t = atol(_line + 1);
if (t < FLIP_TIME_MIN || t > FLIP_TIME_MAX)
{
_error(F("Flip time out of range (50-2000 us)"));
return;
}
_display.setTiming(t);
saveFlipTime(t);
}
// Gttt (ttt = decimal pause after each pulse in us, 0-5000), stored in EEPROM
void CommandParser::_gap()
{
long t = atol(_line + 1);
if (_line_len < 2 || t < 0 || t > FLIP_GAP_MAX)
{
_error(F("Flip gap out of range (0-5000 us)"));
return;
}
_display.setGap(t);
saveFlipGap(t);
}
// Nn (n = pulses for each changed dot, 1-3), stored in EEPROM
void CommandParser::_pulses()
{
long n = atol(_line + 1);
if (n < 1 || n > PULSES_MAX)
{
_error(F("Pulses out of range (1-3)"));
return;
}
_display.setPulses(n);
savePulses(n);
}
// Ee,ttt[,f] (e = effect number, ttt = decimal duration in ms, 100-10000, f = flags,
// EFFECT_FLAG_... in dot_sequence.h, default 0), stored in EEPROM
void CommandParser::_effect()
{
const char *comma = strchr(_line + 1, ',');
const char *comma2 = comma ? strchr(comma + 1, ',') : nullptr;
long effect = atol(_line + 1);
long t = comma ? atol(comma + 1) : 0;
long flags = comma2 ? atol(comma2 + 1) : 0;
if (_line_len < 2 || !comma || effect < 0 || effect >= EFFECT_COUNT || t < EFFECT_TIME_MIN ||
t > EFFECT_TIME_MAX || flags < 0 || flags > EFFECT_FLAGS_MAX)
{
_error(F("Invalid effect (Ee,ms,flags: e 0-9, 100-10000 ms, flags 0-1)"));
return;
}
_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);
}