Public Access
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>
343 lines
7.4 KiB
C++
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);
|
|
}
|