Public Access
Split Nano firmware into modules
- config.h: all constants and debug defines - command_parser.h/.cpp: CommandParser class, one method per command - settings.h/.cpp: EEPROM flip time storage - main.cpp: setup/loop and the periodic full redraw only - Full redraw now checks frameEnded() after the serial buffer is drained - Drop the SPI/Adafruit_I2CDevice include workaround (not needed) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
75f677a2eb
commit
80f3f78152
8 files changed
+428
-278
No files matched your search
@@ -42,6 +42,8 @@ See README.md for the overview.
|
|||||||
|
|
||||||
## Nano serial parser (`src/nano/main.cpp`)
|
## Nano serial parser (`src/nano/main.cpp`)
|
||||||
|
|
||||||
|
- Files: `main.cpp` (setup/loop, refresh), `config.h` (all constants and debug
|
||||||
|
defines), `command_parser.*` (protocol, one method per command), `settings.*` (EEPROM).
|
||||||
- Line based and non-blocking: bytes are collected into a 64-character buffer and a
|
- Line based and non-blocking: bytes are collected into a 64-character buffer and a
|
||||||
command runs when `\n` arrives. Bitmap (`B`) pixel data is the exception. It is drawn
|
command runs when `\n` arrives. Bitmap (`B`) pixel data is the exception. It is drawn
|
||||||
while it arrives, so it can be longer than the buffer. Keep `loop()` free of busy-waits.
|
while it arrives, so it can be longer than the buffer. Keep `loop()` free of busy-waits.
|
||||||
@@ -53,7 +55,7 @@ See README.md for the overview.
|
|||||||
- The Nano only replies `READY` (at boot) and `ERR: ...`. There is no per-frame
|
- The Nano only replies `READY` (at boot) and `ERR: ...`. There is no per-frame
|
||||||
acknowledgement. Whether the Nano's TX is wired to the ESP's RX is unconfirmed, so every
|
acknowledgement. Whether the Nano's TX is wired to the ESP's RX is unconfirmed, so every
|
||||||
wait for a reply needs a timeout.
|
wait for a reply needs a timeout.
|
||||||
- To test the parser without hardware, compile `main.cpp` with g++ (Strawberry Perl
|
- To test the parser without hardware, compile all `src/nano/*.cpp` files with g++ (Strawberry Perl
|
||||||
provides it at `C:\Strawberry\c\bin`) and mock headers for `Arduino.h`, `EEPROM.h` and
|
provides it at `C:\Strawberry\c\bin`) and mock headers for `Arduino.h`, `EEPROM.h` and
|
||||||
`BROSE9323.h`.
|
`BROSE9323.h`.
|
||||||
|
|
||||||
|
|||||||
+12
-1
@@ -6,6 +6,17 @@ The Nano is connected directly to the Brose panel electronics. It keeps an 84 ×
|
|||||||
buffer and takes drawing commands over serial (115200 baud). It has no clock or network
|
buffer and takes drawing commands over serial (115200 baud). It has no clock or network
|
||||||
logic.
|
logic.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Contents |
|
||||||
|
|------|----------|
|
||||||
|
| `main.cpp` | `setup()` / `loop()`, feeds serial bytes to the parser, periodic full redraw |
|
||||||
|
| `config.h` | All settings: debug options, display size, flip time limits, timings |
|
||||||
|
| `command_parser.h/.cpp` | `CommandParser`: the serial protocol, one method per command |
|
||||||
|
| `settings.h/.cpp` | Flip time stored in EEPROM |
|
||||||
|
|
||||||
|
The display driver itself is in [lib/BROSE9323](../../lib/BROSE9323/README.md).
|
||||||
|
|
||||||
## Behaviour
|
## Behaviour
|
||||||
|
|
||||||
- **Boot:** the Nano loads the flip time from EEPROM, sets up the pins and sends `READY`.
|
- **Boot:** the Nano loads the flip time from EEPROM, sets up the pins and sends `READY`.
|
||||||
@@ -24,7 +35,7 @@ logic.
|
|||||||
|
|
||||||
### Debug options
|
### Debug options
|
||||||
|
|
||||||
Uncomment these at the top of `main.cpp`:
|
Uncomment these in `config.h`:
|
||||||
|
|
||||||
| Define | Effect |
|
| Define | Effect |
|
||||||
|--------|--------|
|
|--------|--------|
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
#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;
|
||||||
|
_frame_ended = false;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CommandParser::frameEnded() const
|
||||||
|
{
|
||||||
|
return _frame_ended;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 'd':
|
||||||
|
case 'D':
|
||||||
|
_display.display();
|
||||||
|
_frame_ended = true;
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#include "BROSE9323.h"
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
// Parses the line based serial protocol from the ESP32 (see README.md) and draws on the
|
||||||
|
// display. Bytes are fed one by one; a command runs when its '\n' arrives, so nothing
|
||||||
|
// here blocks while waiting for data.
|
||||||
|
class CommandParser
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CommandParser(BROSE9323 &display, Print &reply);
|
||||||
|
|
||||||
|
// Handle one received byte
|
||||||
|
void feed(char c);
|
||||||
|
|
||||||
|
// True when the last command was D (end of a frame) and no new command has started
|
||||||
|
bool frameEnded() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
BROSE9323 &_display;
|
||||||
|
Print &_reply;
|
||||||
|
|
||||||
|
char _line[LINE_MAX + 1];
|
||||||
|
uint8_t _line_len = 0;
|
||||||
|
bool _discard_line = false; // skip everything until the next '\n'
|
||||||
|
bool _frame_ended = false;
|
||||||
|
|
||||||
|
// Bitmap (B) pixel data is drawn while it arrives instead of being buffered
|
||||||
|
bool _bitmap_active = false;
|
||||||
|
uint8_t _bitmap_x, _bitmap_y, _bitmap_w, _bitmap_h;
|
||||||
|
bool _bitmap_color;
|
||||||
|
uint16_t _bitmap_nibble;
|
||||||
|
|
||||||
|
void _runLine();
|
||||||
|
void _error(const __FlashStringHelper *msg);
|
||||||
|
|
||||||
|
// Commands
|
||||||
|
bool _bitmapStart();
|
||||||
|
void _bitmapData(char c);
|
||||||
|
void _fill();
|
||||||
|
void _print();
|
||||||
|
void _pixel(bool direct);
|
||||||
|
void _timing();
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Debug options (uncomment to enable)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Print unknown command bytes on the serial port
|
||||||
|
//#define DEBUG
|
||||||
|
|
||||||
|
// Time a full redraw at boot and print the duration in ms
|
||||||
|
//#define MEASURE_REFRESH
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Display
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Three Brose panels of 28 x 19 next to each other
|
||||||
|
const uint8_t DISPLAY_WIDTH = 84;
|
||||||
|
const uint8_t DISPLAY_HEIGHT = 19;
|
||||||
|
const uint8_t PANEL_WIDTH = 28;
|
||||||
|
|
||||||
|
// Flip pulse length in us, stored in EEPROM and changed with the T command
|
||||||
|
const uint16_t FLIP_TIME_DEFAULT = 200;
|
||||||
|
const uint16_t FLIP_TIME_MIN = 50;
|
||||||
|
const uint16_t FLIP_TIME_MAX = 2000;
|
||||||
|
|
||||||
|
// Full redraw interval, fixes dots that did not flip
|
||||||
|
const uint32_t REFRESH_TIME = 120000UL; // 2 min
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Serial link to the ESP32
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const uint32_t SERIAL_BAUD = 115200;
|
||||||
|
|
||||||
|
// Serial line quiet for this long: safe to do a due full redraw without waiting for a D
|
||||||
|
const uint32_t IDLE_TIME = 2000UL;
|
||||||
|
|
||||||
|
// Longest command line (bitmap data is streamed and does not count)
|
||||||
|
const uint8_t LINE_MAX = 64;
|
||||||
+29
-276
@@ -1,257 +1,35 @@
|
|||||||
|
// Arduino Nano firmware: drives the Brose flipdot panels and draws what the ESP32 sends
|
||||||
|
// over serial. See README.md for the protocol and wiring.
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
#include "BROSE9323.h"
|
#include "BROSE9323.h"
|
||||||
// fixes
|
#include "command_parser.h"
|
||||||
#include <SPI.h>
|
#include "config.h"
|
||||||
#include <Adafruit_I2CDevice.h>
|
#include "settings.h"
|
||||||
|
|
||||||
#include <EEPROM.h>
|
BROSE9323 display(DISPLAY_WIDTH, DISPLAY_HEIGHT, PANEL_WIDTH, FLIP_TIME_DEFAULT);
|
||||||
|
CommandParser parser(display, Serial);
|
||||||
//#define DEBUG
|
|
||||||
// Print how long a full redraw takes (in ms) at startup
|
|
||||||
//#define MEASURE_REFRESH
|
|
||||||
|
|
||||||
// Flip pulse length in us, stored in EEPROM and changed with the T command
|
|
||||||
const uint16_t FLIP_TIME_DEFAULT = 200;
|
|
||||||
const uint16_t FLIP_TIME_MIN = 50;
|
|
||||||
const uint16_t FLIP_TIME_MAX = 2000;
|
|
||||||
|
|
||||||
// EEPROM layout: magic marker (marks the stored value as valid), then the flip time
|
|
||||||
const int EEPROM_MAGIC_ADDR = 0;
|
|
||||||
const int EEPROM_FLIP_TIME_ADDR = 2;
|
|
||||||
const uint16_t EEPROM_MAGIC = 0xF11D;
|
|
||||||
|
|
||||||
// Full redraw interval, fixes dots that did not flip
|
|
||||||
const uint32_t REFRESH_TIME = 120000UL; // 2 min
|
|
||||||
// Serial line quiet for this long: safe to do a due full redraw without waiting for a D
|
|
||||||
const uint32_t IDLE_TIME = 2000UL;
|
|
||||||
|
|
||||||
// Longest command line (bitmap data is streamed and does not count)
|
|
||||||
const uint8_t LINE_MAX = 64;
|
|
||||||
|
|
||||||
BROSE9323 display(84, 19, 28, FLIP_TIME_DEFAULT);
|
|
||||||
|
|
||||||
char line[LINE_MAX + 1];
|
|
||||||
uint8_t line_len = 0;
|
|
||||||
bool discard_line = false; // skip everything until the next '\n'
|
|
||||||
|
|
||||||
// Bitmap (B) pixel data is drawn while it arrives instead of being buffered
|
|
||||||
bool bitmap_active = false;
|
|
||||||
uint8_t bitmap_x, bitmap_y, bitmap_w, bitmap_h;
|
|
||||||
bool bitmap_color;
|
|
||||||
uint16_t bitmap_nibble;
|
|
||||||
|
|
||||||
uint32_t last_refresh;
|
uint32_t last_refresh;
|
||||||
uint32_t last_rx;
|
uint32_t last_rx;
|
||||||
|
|
||||||
uint8_t hex2dec(char c)
|
// Force a full redraw every REFRESH_TIME. The redraw blocks long enough to overflow the
|
||||||
{
|
// 64 byte serial buffer, so it only runs when no data is expected: right after the ESP
|
||||||
if (c >= '0' && c <= '9')
|
// finished a frame, or when the serial line is idle.
|
||||||
return c - '0';
|
|
||||||
if (c >= 'A' && c <= 'F')
|
|
||||||
return c - 'A' + 10;
|
|
||||||
if (c >= 'a' && c <= 'f')
|
|
||||||
return c - 'a' + 10;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isHex(char c)
|
|
||||||
{
|
|
||||||
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t hex2byte(const char *s)
|
|
||||||
{
|
|
||||||
return (hex2dec(s[0]) << 4) | hex2dec(s[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
void printError(const __FlashStringHelper *msg)
|
|
||||||
{
|
|
||||||
Serial.print(F("ERR: "));
|
|
||||||
Serial.println(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t loadFlipTime()
|
|
||||||
{
|
|
||||||
uint16_t magic, t;
|
|
||||||
EEPROM.get(EEPROM_MAGIC_ADDR, magic);
|
|
||||||
EEPROM.get(EEPROM_FLIP_TIME_ADDR, t);
|
|
||||||
if (magic != EEPROM_MAGIC || t < FLIP_TIME_MIN || t > FLIP_TIME_MAX)
|
|
||||||
return FLIP_TIME_DEFAULT;
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
void saveFlipTime(uint16_t t)
|
|
||||||
{
|
|
||||||
// put() only writes bytes that changed
|
|
||||||
EEPROM.put(EEPROM_FLIP_TIME_ADDR, t);
|
|
||||||
EEPROM.put(EEPROM_MAGIC_ADDR, EEPROM_MAGIC);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force a full redraw every REFRESH_TIME. Only called when no data is expected,
|
|
||||||
// because the redraw blocks long enough to overflow the 64 byte serial buffer.
|
|
||||||
void refreshIfDue()
|
void refreshIfDue()
|
||||||
{
|
{
|
||||||
if (millis() - last_refresh >= REFRESH_TIME)
|
bool quiet = parser.frameEnded() || millis() - last_rx >= IDLE_TIME;
|
||||||
|
if (quiet && millis() - last_refresh >= REFRESH_TIME)
|
||||||
{
|
{
|
||||||
display.display(true);
|
display.display(true);
|
||||||
last_refresh = millis();
|
last_refresh = millis();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 startBitmap()
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
printError(F("Invalid bitmap width (not dividable by 4)"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (bitmap_h == 0)
|
|
||||||
{
|
|
||||||
printError(F("Invalid bitmap height (0)"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
bitmap_nibble = 0;
|
|
||||||
bitmap_active = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void 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++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle one complete command line (without the '\n')
|
|
||||||
void processLine()
|
|
||||||
{
|
|
||||||
if (line_len == 0)
|
|
||||||
return;
|
|
||||||
line[line_len] = '\0';
|
|
||||||
|
|
||||||
char cmd = line[0];
|
|
||||||
switch (cmd)
|
|
||||||
{
|
|
||||||
// Bitmap header shorter than 10 characters (complete headers are handled in loop())
|
|
||||||
case 'b':
|
|
||||||
case 'B':
|
|
||||||
printError(F("Incomplete bitmap header"));
|
|
||||||
break;
|
|
||||||
// Display
|
|
||||||
case 'd':
|
|
||||||
case 'D': // D
|
|
||||||
display.display();
|
|
||||||
// The ESP sends D at the end of a frame, so the line is quiet now
|
|
||||||
refreshIfDue();
|
|
||||||
break;
|
|
||||||
// Fill Screen
|
|
||||||
case 'f':
|
|
||||||
case 'F': // Fc (c = 0/1 color)
|
|
||||||
if (line_len < 2)
|
|
||||||
{
|
|
||||||
printError(F("Missing color"));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
display.fillScreen(line[1] != '0');
|
|
||||||
break;
|
|
||||||
// Print Text
|
|
||||||
case 'p':
|
|
||||||
case 'P': // 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)
|
|
||||||
{
|
|
||||||
if (line_len < 7)
|
|
||||||
{
|
|
||||||
printError(F("Incomplete print command"));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Return buffer (show pixels on console)
|
|
||||||
case 'r':
|
|
||||||
case 'R': // R
|
|
||||||
display.printBuffer();
|
|
||||||
Serial.flush();
|
|
||||||
break;
|
|
||||||
// Set Pixel (S = flip immediately, s = buffer only)
|
|
||||||
case 'S':
|
|
||||||
case 's': // Sxxyyc (xx = 8 bit hex x position; yy = 8 bit hex y position; c = 0/1 color)
|
|
||||||
if (line_len < 6)
|
|
||||||
{
|
|
||||||
printError(F("Incomplete pixel command"));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
display.setDirect(cmd == 'S');
|
|
||||||
display.drawPixel(hex2byte(line + 1), hex2byte(line + 3), hex2dec(line[5]));
|
|
||||||
display.setDirect(false);
|
|
||||||
break;
|
|
||||||
// Set Timing
|
|
||||||
case 'T':
|
|
||||||
case 't': // Tttt (ttt = decimal flip time in us)
|
|
||||||
{
|
|
||||||
long t = atol(line + 1);
|
|
||||||
if (t < FLIP_TIME_MIN || t > FLIP_TIME_MAX)
|
|
||||||
{
|
|
||||||
printError(F("Flip time out of range (50-2000 us)"));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
display.setTiming(t);
|
|
||||||
saveFlipTime(t);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
#ifdef DEBUG
|
|
||||||
Serial.print("0x");
|
|
||||||
Serial.print(cmd >> 4, HEX);
|
|
||||||
Serial.print(cmd & 0xF, HEX);
|
|
||||||
Serial.println(" - command not known.");
|
|
||||||
Serial.flush();
|
|
||||||
#endif
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setup()
|
|
||||||
{
|
|
||||||
Serial.begin(115200);
|
|
||||||
|
|
||||||
display.setTiming(loadFlipTime());
|
|
||||||
display.begin();
|
|
||||||
|
|
||||||
#ifdef MEASURE_REFRESH
|
#ifdef MEASURE_REFRESH
|
||||||
|
void measureRefresh()
|
||||||
|
{
|
||||||
delay(1000);
|
delay(1000);
|
||||||
|
|
||||||
display.fillScreen(1);
|
display.fillScreen(1);
|
||||||
@@ -261,6 +39,18 @@ void setup()
|
|||||||
display.display();
|
display.display();
|
||||||
uint32_t t2 = millis();
|
uint32_t t2 = millis();
|
||||||
Serial.println(t2 - t1);
|
Serial.println(t2 - t1);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void setup()
|
||||||
|
{
|
||||||
|
Serial.begin(SERIAL_BAUD);
|
||||||
|
|
||||||
|
display.setTiming(loadFlipTime());
|
||||||
|
display.begin();
|
||||||
|
|
||||||
|
#ifdef MEASURE_REFRESH
|
||||||
|
measureRefresh();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
last_refresh = millis();
|
last_refresh = millis();
|
||||||
@@ -274,46 +64,9 @@ void loop()
|
|||||||
{
|
{
|
||||||
while (Serial.available())
|
while (Serial.available())
|
||||||
{
|
{
|
||||||
char c = Serial.read();
|
|
||||||
last_rx = millis();
|
last_rx = millis();
|
||||||
|
parser.feed(Serial.read());
|
||||||
if (bitmap_active)
|
|
||||||
{
|
|
||||||
if (c == '\n')
|
|
||||||
bitmap_active = false;
|
|
||||||
else
|
|
||||||
bitmapData(c);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c == '\n')
|
|
||||||
{
|
|
||||||
if (!discard_line)
|
|
||||||
processLine();
|
|
||||||
line_len = 0;
|
|
||||||
discard_line = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (c == '\r' || discard_line)
|
|
||||||
continue;
|
|
||||||
if (line_len >= LINE_MAX)
|
|
||||||
{
|
|
||||||
printError(F("Line too long"));
|
|
||||||
discard_line = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
line[line_len++] = c;
|
|
||||||
|
|
||||||
// Bitmap header complete: draw the pixel data as it arrives
|
|
||||||
if (line_len == 10 && (line[0] == 'b' || line[0] == 'B'))
|
|
||||||
{
|
|
||||||
line_len = 0;
|
|
||||||
if (!startBitmap())
|
|
||||||
discard_line = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No frames coming in (e.g. ESP idle): do the full redraw now if it is due
|
|
||||||
if (millis() - last_rx >= IDLE_TIME)
|
|
||||||
refreshIfDue();
|
refreshIfDue();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#include "settings.h"
|
||||||
|
|
||||||
|
#include <EEPROM.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
// EEPROM layout: magic marker (marks the stored value as valid), then the flip time
|
||||||
|
const int EEPROM_MAGIC_ADDR = 0;
|
||||||
|
const int EEPROM_FLIP_TIME_ADDR = 2;
|
||||||
|
const uint16_t EEPROM_MAGIC = 0xF11D;
|
||||||
|
|
||||||
|
uint16_t loadFlipTime()
|
||||||
|
{
|
||||||
|
uint16_t magic, t;
|
||||||
|
EEPROM.get(EEPROM_MAGIC_ADDR, magic);
|
||||||
|
EEPROM.get(EEPROM_FLIP_TIME_ADDR, t);
|
||||||
|
if (magic != EEPROM_MAGIC || t < FLIP_TIME_MIN || t > FLIP_TIME_MAX)
|
||||||
|
return FLIP_TIME_DEFAULT;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveFlipTime(uint16_t t)
|
||||||
|
{
|
||||||
|
// put() only writes bytes that changed
|
||||||
|
EEPROM.put(EEPROM_FLIP_TIME_ADDR, t);
|
||||||
|
EEPROM.put(EEPROM_MAGIC_ADDR, EEPROM_MAGIC);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
// Settings that survive a power cycle (stored in EEPROM)
|
||||||
|
|
||||||
|
// Stored flip time, or FLIP_TIME_DEFAULT when nothing valid is stored
|
||||||
|
uint16_t loadFlipTime();
|
||||||
|
|
||||||
|
void saveFlipTime(uint16_t t);
|
||||||
Reference in new issue
Block a user