#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); }