Public Access
Pages choose their own refresh effect (or the Hardware setting), used when the page appears and for its own changes. Schedule blocks that switch instantly and rules still switch instantly. The ESP32 sends page effects to the Nano with a new no-save flag (E ...,2), so page switches don't wear the Nano's EEPROM. Page format 7 (version 6 pages are converted). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
755 lines
21 KiB
C++
755 lines
21 KiB
C++
// Tests for the Nano firmware (src/nano), run on the PC: pio test -e native
|
|
//
|
|
// The real Arduino, EEPROM and display driver are replaced by the fakes in test/mocks.
|
|
// Tests push bytes into Serial.in, call loop() and check what the display was asked to do.
|
|
|
|
#include <unity.h>
|
|
|
|
#include "Arduino.h"
|
|
#include "BROSE9323.h"
|
|
#include "EEPROM.h"
|
|
#include "buffer_crc.h" // the real one, from lib/BROSE9323
|
|
#include "frame_commands.h" // the real one, from lib/BROSE9323
|
|
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// From src/nano/main.cpp
|
|
extern BROSE9323 display;
|
|
extern uint32_t last_refresh;
|
|
void setup();
|
|
void loop();
|
|
|
|
// Send bytes to the Nano and let it process them
|
|
static void send(const std::string &data)
|
|
{
|
|
for (char c : data)
|
|
Serial.in.push_back(c);
|
|
loop();
|
|
}
|
|
|
|
static bool replied(const char *text)
|
|
{
|
|
return Serial.out.find(text) != std::string::npos;
|
|
}
|
|
|
|
void setUp()
|
|
{
|
|
// Finish any half-received line from the previous test
|
|
send("\n");
|
|
fake_millis += 1;
|
|
last_refresh = fake_millis; // no full redraw due unless a test asks for one
|
|
display.calls.clear();
|
|
display.direct = false;
|
|
Serial.out.clear();
|
|
}
|
|
|
|
void tearDown() {}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Boot
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void test_boot_does_not_flip_dots()
|
|
{
|
|
EEPROM.erase();
|
|
setup();
|
|
TEST_ASSERT_EQUAL(1, display.count("begin"));
|
|
TEST_ASSERT_EQUAL(0, display.count("display"));
|
|
TEST_ASSERT_EQUAL(0, display.count("fill"));
|
|
}
|
|
|
|
void test_boot_sends_ready()
|
|
{
|
|
setup();
|
|
TEST_ASSERT_TRUE(replied("READY\n"));
|
|
}
|
|
|
|
void test_boot_uses_default_flip_time_with_empty_eeprom()
|
|
{
|
|
EEPROM.erase();
|
|
setup();
|
|
TEST_ASSERT_EQUAL(200, display.last("timing")->a);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Commands
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void test_pixel_runs_without_waiting_for_next_command()
|
|
{
|
|
send("s0a051\n");
|
|
TEST_ASSERT_EQUAL(1, display.count("pixel"));
|
|
TEST_ASSERT_EQUAL(10, display.last("pixel")->a);
|
|
TEST_ASSERT_EQUAL(5, display.last("pixel")->b);
|
|
TEST_ASSERT_EQUAL(1, display.last("pixel")->c);
|
|
}
|
|
|
|
void test_direct_pixel_flips_immediately_and_resets_direct_mode()
|
|
{
|
|
send("S010200\n");
|
|
TEST_ASSERT_EQUAL(1, display.count("directPixel"));
|
|
TEST_ASSERT_FALSE(display.direct);
|
|
}
|
|
|
|
void test_fill()
|
|
{
|
|
send("F1\n");
|
|
TEST_ASSERT_EQUAL(1, display.last("fill")->a);
|
|
send("F0\n");
|
|
TEST_ASSERT_EQUAL(0, display.last("fill")->a);
|
|
}
|
|
|
|
void test_display()
|
|
{
|
|
send("D\n");
|
|
TEST_ASSERT_EQUAL(1, display.count("display"));
|
|
TEST_ASSERT_EQUAL(0, display.last("display")->a); // not forced
|
|
TEST_ASSERT_EQUAL(0, display.last("display")->b); // with the effect
|
|
}
|
|
|
|
void test_display_instant_frame()
|
|
{
|
|
send("D1\n");
|
|
TEST_ASSERT_EQUAL(1, display.count("display"));
|
|
TEST_ASSERT_EQUAL(0, display.last("display")->a);
|
|
TEST_ASSERT_EQUAL(1, display.last("display")->b); // without the effect
|
|
}
|
|
|
|
void test_refresh_starts_between_instant_frames()
|
|
{
|
|
// The driver pauses the refresh while frames arrive, so it may start at any time
|
|
last_refresh = fake_millis;
|
|
fake_millis += 120000;
|
|
send("D1\n");
|
|
TEST_ASSERT_EQUAL(1, display.count("display"));
|
|
TEST_ASSERT_EQUAL(0, display.last("display")->a); // the frame is not forced
|
|
TEST_ASSERT_EQUAL(1, display.count("refresh"));
|
|
}
|
|
|
|
void test_lowercase_and_uppercase_commands()
|
|
{
|
|
send("f1\nd\n");
|
|
TEST_ASSERT_EQUAL(1, display.count("fill"));
|
|
TEST_ASSERT_EQUAL(1, display.count("display"));
|
|
}
|
|
|
|
void test_command_split_over_several_reads_with_crlf()
|
|
{
|
|
send("s0");
|
|
send("1");
|
|
send("021\r\n");
|
|
TEST_ASSERT_EQUAL(1, display.count("pixel"));
|
|
TEST_ASSERT_EQUAL(1, display.last("pixel")->a);
|
|
TEST_ASSERT_EQUAL(2, display.last("pixel")->b);
|
|
}
|
|
|
|
void test_print_text_with_newline_escape()
|
|
{
|
|
send("P0102115\\nab\n");
|
|
TEST_ASSERT_EQUAL(1, display.last("cursor")->a);
|
|
TEST_ASSERT_EQUAL(2, display.last("cursor")->b);
|
|
TEST_ASSERT_EQUAL(1, display.last("textColor")->a);
|
|
TEST_ASSERT_EQUAL(0, display.last("textColor")->b);
|
|
TEST_ASSERT_EQUAL(1, display.last("textSize")->a);
|
|
TEST_ASSERT_EQUAL(3, display.count("write")); // '5', 'a', 'b' and no garbage
|
|
TEST_ASSERT_EQUAL(1, display.count("newline"));
|
|
}
|
|
|
|
void test_print_buffer()
|
|
{
|
|
send("R\n");
|
|
TEST_ASSERT_EQUAL(1, display.count("printBuffer"));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Bitmaps
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void test_bitmap_pixels()
|
|
{
|
|
// 8x2 at (4,3), color 1, rows F0 and 81
|
|
send("B040308021F081\n");
|
|
const int expected[][2] = {{4, 3}, {5, 3}, {6, 3}, {7, 3}, {4, 4}, {11, 4}};
|
|
TEST_ASSERT_EQUAL(6, display.count("pixel"));
|
|
int i = 0;
|
|
for (const DisplayCall &call : display.calls)
|
|
{
|
|
if (call.what != "pixel")
|
|
continue;
|
|
TEST_ASSERT_EQUAL(expected[i][0], call.a);
|
|
TEST_ASSERT_EQUAL(expected[i][1], call.b);
|
|
TEST_ASSERT_EQUAL(1, call.c);
|
|
i++;
|
|
}
|
|
}
|
|
|
|
void test_full_screen_bitmap_is_streamed()
|
|
{
|
|
// 84x19 = 399 hex digits, much longer than the 64 character line buffer
|
|
send("B000054131" + std::string(399, 'F') + "\n");
|
|
TEST_ASSERT_EQUAL(84 * 19, display.count("pixel"));
|
|
}
|
|
|
|
// The commands the ESP32 driver sends for a frame (lib/BROSE9323/frame_commands.h), run
|
|
// through this parser: the drawn dots must give the wanted image.
|
|
static const int IMG_BYTES = 11 * 19;
|
|
|
|
static std::string frame(const uint8_t *wanted, const uint8_t *shown, bool instant)
|
|
{
|
|
std::string text;
|
|
frameCommands(wanted, shown, 84, 19, instant, [&text](const char *t) { text += t; });
|
|
return text;
|
|
}
|
|
|
|
static void setDot(uint8_t *img, int x, int y, bool on)
|
|
{
|
|
if (on)
|
|
img[y * 11 + x / 8] |= 0x80 >> (x & 7);
|
|
else
|
|
img[y * 11 + x / 8] &= ~(0x80 >> (x & 7));
|
|
}
|
|
|
|
// The recorded fill and pixel calls applied to an image
|
|
static void applyCalls(uint8_t *img)
|
|
{
|
|
for (const DisplayCall &call : display.calls)
|
|
if (call.what == "fill")
|
|
memset(img, call.a ? 0xFF : 0, IMG_BYTES);
|
|
else if (call.what == "pixel")
|
|
setDot(img, call.a, call.b, call.c);
|
|
}
|
|
|
|
static void pattern(uint8_t *img, int seed)
|
|
{
|
|
for (int i = 0; i < IMG_BYTES; i++)
|
|
img[i] = (uint8_t)(i * 37 + seed * 11);
|
|
for (int y = 0; y < 19; y++) // nothing past the 84th dot
|
|
img[y * 11 + 10] &= 0xF0;
|
|
}
|
|
|
|
void test_frame_commands_send_only_the_changed_dots()
|
|
{
|
|
uint8_t shown[IMG_BYTES], wanted[IMG_BYTES];
|
|
pattern(shown, 1);
|
|
memcpy(wanted, shown, IMG_BYTES);
|
|
int changed = 0;
|
|
for (int x = 30; x < 50; x++) // a scroll window, 6 rows high
|
|
for (int y = 12; y < 18; y++)
|
|
if ((x + y) % 3 == 0)
|
|
{
|
|
bool now = (shown[y * 11 + x / 8] >> (7 - (x & 7))) & 1;
|
|
setDot(wanted, x, y, !now);
|
|
changed++;
|
|
}
|
|
|
|
std::string text = frame(wanted, shown, true);
|
|
TEST_ASSERT_TRUE(text.size() < 150); // instead of the whole screen
|
|
send(text);
|
|
TEST_ASSERT_FALSE(replied("ERR"));
|
|
TEST_ASSERT_EQUAL(0, display.count("fill"));
|
|
TEST_ASSERT_EQUAL(changed, display.count("pixel")); // only the changed dots
|
|
uint8_t result[IMG_BYTES];
|
|
memcpy(result, shown, IMG_BYTES);
|
|
applyCalls(result);
|
|
TEST_ASSERT_EQUAL_MEMORY(wanted, result, IMG_BYTES);
|
|
TEST_ASSERT_EQUAL(1, display.count("display"));
|
|
TEST_ASSERT_EQUAL(1, display.last("display")->b); // D1: instant
|
|
}
|
|
|
|
void test_frame_commands_full_frame()
|
|
{
|
|
uint8_t wanted[IMG_BYTES];
|
|
pattern(wanted, 2);
|
|
std::string text = frame(wanted, nullptr, false);
|
|
TEST_ASSERT_TRUE(text.size() < 420);
|
|
send(text);
|
|
TEST_ASSERT_FALSE(replied("ERR"));
|
|
TEST_ASSERT_EQUAL(1, display.count("fill"));
|
|
uint8_t result[IMG_BYTES];
|
|
memset(result, 0xAA, IMG_BYTES);
|
|
applyCalls(result);
|
|
TEST_ASSERT_EQUAL_MEMORY(wanted, result, IMG_BYTES);
|
|
TEST_ASSERT_EQUAL(0, display.last("display")->b); // D: with the effect
|
|
}
|
|
|
|
void test_frame_commands_edge_and_nothing_changed()
|
|
{
|
|
uint8_t shown[IMG_BYTES] = {}, wanted[IMG_BYTES] = {};
|
|
TEST_ASSERT_EQUAL_STRING("", frame(wanted, shown, true).c_str());
|
|
setDot(wanted, 83, 18, true); // the last dot: the B width (4) goes past the edge
|
|
std::string text = frame(wanted, shown, false);
|
|
send(text);
|
|
TEST_ASSERT_FALSE(replied("ERR"));
|
|
TEST_ASSERT_EQUAL(1, display.count("pixel"));
|
|
TEST_ASSERT_EQUAL(83, display.last("pixel")->a);
|
|
TEST_ASSERT_EQUAL(18, display.last("pixel")->b);
|
|
}
|
|
|
|
void test_invalid_bitmap_data_is_not_run_as_commands()
|
|
{
|
|
// width 5 is invalid; the data contains the command letters B, D and F
|
|
send("B0000050100BDFDFDF\nD\n");
|
|
TEST_ASSERT_TRUE(replied("ERR"));
|
|
TEST_ASSERT_EQUAL(0, display.count("fill"));
|
|
TEST_ASSERT_EQUAL(1, display.count("display"));
|
|
}
|
|
|
|
void test_incomplete_bitmap_header()
|
|
{
|
|
send("B00\nD\n");
|
|
TEST_ASSERT_TRUE(replied("ERR"));
|
|
TEST_ASSERT_EQUAL(1, display.count("display"));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Recovering from bad input
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void test_too_long_line_is_rejected()
|
|
{
|
|
send(std::string(200, 'x') + "\nF1\n");
|
|
TEST_ASSERT_TRUE(replied("Line too long"));
|
|
TEST_ASSERT_EQUAL(1, display.count("fill"));
|
|
}
|
|
|
|
void test_truncated_command_is_ignored()
|
|
{
|
|
send("s01\nF1\n");
|
|
TEST_ASSERT_EQUAL(0, display.count("pixel"));
|
|
TEST_ASSERT_EQUAL(1, display.count("fill"));
|
|
}
|
|
|
|
void test_unknown_command_is_ignored()
|
|
{
|
|
send("xyz\nF1\n");
|
|
TEST_ASSERT_EQUAL(1, display.count("fill"));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Flip time
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void test_timing_is_applied_immediately()
|
|
{
|
|
send("T225\n");
|
|
TEST_ASSERT_EQUAL(225, display.last("timing")->a);
|
|
}
|
|
|
|
void test_timing_above_255_is_stored_and_restored()
|
|
{
|
|
send("T300\n");
|
|
TEST_ASSERT_EQUAL(300, display.last("timing")->a);
|
|
display.calls.clear();
|
|
setup(); // reboot
|
|
TEST_ASSERT_EQUAL(300, display.last("timing")->a);
|
|
}
|
|
|
|
void test_timing_out_of_range_is_rejected()
|
|
{
|
|
send("T5\n");
|
|
send("T99999\n");
|
|
TEST_ASSERT_EQUAL(0, display.count("timing"));
|
|
TEST_ASSERT_TRUE(replied("ERR"));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Flip gap, pulses and refresh effect
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void test_defaults_with_empty_eeprom()
|
|
{
|
|
EEPROM.erase();
|
|
setup();
|
|
TEST_ASSERT_EQUAL(200, display.last("gap")->a);
|
|
TEST_ASSERT_EQUAL(2, display.last("pulses")->a);
|
|
TEST_ASSERT_EQUAL(EFFECT_INSTANT, display.last("effect")->a);
|
|
TEST_ASSERT_EQUAL(1500, display.last("effect")->b);
|
|
TEST_ASSERT_EQUAL(0, display.last("effect")->c);
|
|
}
|
|
|
|
void test_eeprom_from_before_the_new_settings_keeps_flip_time()
|
|
{
|
|
// Old firmware only stored the magic and the flip time; the rest is still 0xFF
|
|
EEPROM.erase();
|
|
send("T300\n");
|
|
EEPROM.put(4, (uint16_t)0xFFFF);
|
|
EEPROM.put(6, (uint8_t)0xFF);
|
|
EEPROM.put(7, (uint8_t)0xFF);
|
|
EEPROM.put(8, (uint16_t)0xFFFF);
|
|
EEPROM.put(10, (uint8_t)0xFF);
|
|
display.calls.clear();
|
|
setup();
|
|
TEST_ASSERT_EQUAL(300, display.last("timing")->a);
|
|
TEST_ASSERT_EQUAL(200, display.last("gap")->a);
|
|
TEST_ASSERT_EQUAL(2, display.last("pulses")->a);
|
|
TEST_ASSERT_EQUAL(EFFECT_INSTANT, display.last("effect")->a);
|
|
}
|
|
|
|
void test_gap_is_applied_and_stored()
|
|
{
|
|
send("G350\n");
|
|
TEST_ASSERT_EQUAL(350, display.last("gap")->a);
|
|
send("G0\n");
|
|
TEST_ASSERT_EQUAL(0, display.last("gap")->a);
|
|
send("G1200\n");
|
|
display.calls.clear();
|
|
setup();
|
|
TEST_ASSERT_EQUAL(1200, display.last("gap")->a);
|
|
}
|
|
|
|
void test_gap_out_of_range_is_rejected()
|
|
{
|
|
send("G5001\n");
|
|
send("G\n");
|
|
TEST_ASSERT_EQUAL(0, display.count("gap"));
|
|
TEST_ASSERT_TRUE(replied("ERR"));
|
|
}
|
|
|
|
void test_pulses_are_applied_and_stored()
|
|
{
|
|
send("N3\n");
|
|
TEST_ASSERT_EQUAL(3, display.last("pulses")->a);
|
|
send("N1\n");
|
|
display.calls.clear();
|
|
setup();
|
|
TEST_ASSERT_EQUAL(1, display.last("pulses")->a);
|
|
}
|
|
|
|
void test_pulses_out_of_range_are_rejected()
|
|
{
|
|
send("N0\n");
|
|
send("N4\n");
|
|
TEST_ASSERT_EQUAL(0, display.count("pulses"));
|
|
TEST_ASSERT_TRUE(replied("ERR"));
|
|
}
|
|
|
|
void test_effect_is_applied_and_stored()
|
|
{
|
|
send("E1,2500\n");
|
|
TEST_ASSERT_EQUAL(EFFECT_SPARKLE, display.last("effect")->a);
|
|
TEST_ASSERT_EQUAL(2500, display.last("effect")->b);
|
|
display.calls.clear();
|
|
setup();
|
|
TEST_ASSERT_EQUAL(EFFECT_SPARKLE, display.last("effect")->a);
|
|
TEST_ASSERT_EQUAL(2500, display.last("effect")->b);
|
|
TEST_ASSERT_EQUAL(0, display.last("effect")->c); // no flags given
|
|
send("E9,1000,1\n");
|
|
TEST_ASSERT_EQUAL(EFFECT_RANDOM, display.last("effect")->a);
|
|
TEST_ASSERT_EQUAL(EFFECT_FLAG_SMALL_ONLY, display.last("effect")->c);
|
|
display.calls.clear();
|
|
setup();
|
|
TEST_ASSERT_EQUAL(EFFECT_FLAG_SMALL_ONLY, display.last("effect")->c);
|
|
send("E0,1500,0\n");
|
|
}
|
|
|
|
// A page's own effect: used, but the stored one stays (no EEPROM write per page change)
|
|
void test_effect_without_saving()
|
|
{
|
|
send("E1,2500,1\n");
|
|
send("E5,1000,3\n");
|
|
TEST_ASSERT_EQUAL(5, display.last("effect")->a);
|
|
TEST_ASSERT_EQUAL(1000, display.last("effect")->b);
|
|
TEST_ASSERT_EQUAL(EFFECT_FLAG_SMALL_ONLY, display.last("effect")->c);
|
|
TEST_ASSERT_FALSE(replied("ERR"));
|
|
display.calls.clear();
|
|
setup();
|
|
TEST_ASSERT_EQUAL(EFFECT_SPARKLE, display.last("effect")->a);
|
|
TEST_ASSERT_EQUAL(2500, display.last("effect")->b);
|
|
send("E0,1500,0\n");
|
|
}
|
|
|
|
void test_invalid_effect_is_rejected()
|
|
{
|
|
send("E10,1500\n"); // unknown effect
|
|
send("E1,1500,4\n"); // unknown flag
|
|
send("E1,50\n"); // too fast
|
|
send("E1\n"); // no duration
|
|
TEST_ASSERT_EQUAL(0, display.count("effect"));
|
|
TEST_ASSERT_TRUE(replied("ERR"));
|
|
}
|
|
|
|
// Every effect order visits each dot exactly once
|
|
static void checkSequence(uint8_t effect, uint8_t w, uint8_t h, uint16_t seed)
|
|
{
|
|
std::vector<int> seen(w * h, 0);
|
|
DotSequence sequence;
|
|
sequence.begin(effect, w, h, seed);
|
|
uint8_t x, y;
|
|
int n = 0;
|
|
while (sequence.next(x, y))
|
|
{
|
|
TEST_ASSERT_TRUE(x < w && y < h);
|
|
seen[y * w + x]++;
|
|
n++;
|
|
}
|
|
TEST_ASSERT_EQUAL(w * h, n);
|
|
TEST_ASSERT_EQUAL(w * h, sequence.total());
|
|
for (int count : seen)
|
|
TEST_ASSERT_EQUAL(1, count);
|
|
}
|
|
|
|
void test_every_effect_visits_each_dot_once()
|
|
{
|
|
for (uint8_t effect = 0; effect < EFFECT_COUNT; effect++)
|
|
{
|
|
checkSequence(effect, 84, 19, 0);
|
|
checkSequence(effect, 84, 19, 12345);
|
|
checkSequence(effect, 7, 5, 3);
|
|
checkSequence(effect, 5, 7, 3);
|
|
checkSequence(effect, 1, 1, 0);
|
|
checkSequence(effect, 1, 6, 0);
|
|
checkSequence(effect, 6, 1, 0);
|
|
checkSequence(effect, 2, 2, 0);
|
|
}
|
|
// Sparkle with every LFSR size up to 4095 dots
|
|
for (int n = 2; n < 4096; n = n * 2 + 1)
|
|
checkSequence(EFFECT_SPARKLE, n > 255 ? 255 : n, n > 255 ? (n + 254) / 255 : 1, 7);
|
|
}
|
|
|
|
void test_wipe_goes_left_to_right_and_center_starts_in_the_middle()
|
|
{
|
|
DotSequence sequence;
|
|
uint8_t x, y;
|
|
sequence.begin(EFFECT_WIPE, 84, 19, 0);
|
|
sequence.next(x, y);
|
|
TEST_ASSERT_EQUAL(0, x);
|
|
sequence.begin(EFFECT_CENTER, 84, 19, 0);
|
|
sequence.next(x, y);
|
|
TEST_ASSERT_EQUAL(42, x);
|
|
sequence.begin(EFFECT_DIAGONAL, 84, 19, 0);
|
|
sequence.next(x, y);
|
|
TEST_ASSERT_TRUE(x == 0 && y == 0);
|
|
sequence.next(x, y);
|
|
TEST_ASSERT_TRUE(x == 1 && y == 0);
|
|
sequence.next(x, y);
|
|
TEST_ASSERT_TRUE(x == 0 && y == 1);
|
|
// Spiral: the whole top row, then down the right side
|
|
sequence.begin(EFFECT_SPIRAL, 84, 19, 0);
|
|
for (int i = 0; i < 84; i++)
|
|
{
|
|
sequence.next(x, y);
|
|
TEST_ASSERT_TRUE(x == i && y == 0);
|
|
}
|
|
sequence.next(x, y);
|
|
TEST_ASSERT_TRUE(x == 83 && y == 1);
|
|
sequence.begin(EFFECT_RAIN, 84, 19, 0);
|
|
sequence.next(x, y);
|
|
sequence.next(x, y);
|
|
TEST_ASSERT_EQUAL(1, x);
|
|
TEST_ASSERT_EQUAL(0, y);
|
|
}
|
|
|
|
void test_sparkle_order_is_scrambled()
|
|
{
|
|
// Neighbouring dots should rarely follow each other
|
|
DotSequence sequence;
|
|
sequence.begin(EFFECT_SPARKLE, 84, 19, 99);
|
|
uint8_t x, y, px = 0, py = 0;
|
|
int neighbours = 0, n = 0;
|
|
while (sequence.next(x, y))
|
|
{
|
|
if (n++ && abs(x - px) <= 1 && abs(y - py) <= 1)
|
|
neighbours++;
|
|
px = x;
|
|
py = y;
|
|
}
|
|
TEST_ASSERT_LESS_THAN(n / 20, neighbours);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Periodic full redraw
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void test_refresh_when_due()
|
|
{
|
|
last_refresh = fake_millis;
|
|
fake_millis += 119999;
|
|
loop();
|
|
TEST_ASSERT_EQUAL(0, display.count("refresh"));
|
|
fake_millis += 1;
|
|
loop();
|
|
TEST_ASSERT_EQUAL(1, display.count("refresh"));
|
|
TEST_ASSERT_EQUAL(0, display.count("display")); // no frame, the driver does it
|
|
}
|
|
|
|
void test_refresh_only_once_per_interval()
|
|
{
|
|
last_refresh = fake_millis;
|
|
fake_millis += 120000;
|
|
send("D\n");
|
|
send("D\n");
|
|
loop();
|
|
TEST_ASSERT_EQUAL(1, display.count("refresh"));
|
|
}
|
|
|
|
void test_running_refresh_is_not_restarted()
|
|
{
|
|
last_refresh = fake_millis;
|
|
fake_millis += 120000;
|
|
display.refresh_running = true;
|
|
loop();
|
|
display.refresh_running = false;
|
|
TEST_ASSERT_EQUAL(0, display.count("refresh"));
|
|
}
|
|
|
|
void test_no_refresh_storm_after_millis_wraps()
|
|
{
|
|
fake_millis = 0xFFFFFFFFu - 1000;
|
|
last_refresh = fake_millis;
|
|
fake_millis += 5000; // wraps past 0
|
|
for (int i = 0; i < 5; i++)
|
|
send("D\n");
|
|
TEST_ASSERT_EQUAL(0, display.count("refresh"));
|
|
|
|
fake_millis += 120000;
|
|
send("D\n");
|
|
TEST_ASSERT_EQUAL(1, display.count("refresh"));
|
|
}
|
|
|
|
void test_refresh_interval_is_applied_and_stored()
|
|
{
|
|
send("I5\n");
|
|
TEST_ASSERT_FALSE(replied("ERR"));
|
|
fake_millis += 120000;
|
|
loop();
|
|
TEST_ASSERT_EQUAL(0, display.count("refresh"));
|
|
fake_millis += 180000;
|
|
loop();
|
|
TEST_ASSERT_EQUAL(1, display.count("refresh"));
|
|
|
|
// Kept over a power cycle
|
|
setup();
|
|
last_refresh = fake_millis;
|
|
display.calls.clear();
|
|
fake_millis += 120000;
|
|
loop();
|
|
TEST_ASSERT_EQUAL(0, display.count("refresh"));
|
|
send("I2\n");
|
|
}
|
|
|
|
void test_refresh_interval_zero_never_refreshes()
|
|
{
|
|
send("I0\n");
|
|
TEST_ASSERT_FALSE(replied("ERR"));
|
|
fake_millis += 24UL * 3600 * 1000;
|
|
loop();
|
|
TEST_ASSERT_EQUAL(0, display.count("refresh"));
|
|
send("I2\n");
|
|
}
|
|
|
|
void test_refresh_interval_out_of_range_is_rejected()
|
|
{
|
|
send("I1441\n");
|
|
send("I-1\n");
|
|
send("I\n");
|
|
TEST_ASSERT_TRUE(replied("ERR"));
|
|
fake_millis += 120000;
|
|
loop();
|
|
TEST_ASSERT_EQUAL(1, display.count("refresh")); // still every 2 minutes
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Checksum
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void test_checksum_reply()
|
|
{
|
|
display.crc_value = 0x0A1F;
|
|
send("C\n");
|
|
TEST_ASSERT_EQUAL_STRING("C 0A1F\n", Serial.out.c_str());
|
|
display.crc_value = 0xFFFF;
|
|
send("c\n");
|
|
TEST_ASSERT_EQUAL_STRING("C 0A1F\nC FFFF\n", Serial.out.c_str());
|
|
}
|
|
|
|
void test_checksum_follows_the_frame()
|
|
{
|
|
// The reply comes after the frame's commands ran (they are handled in order)
|
|
display.crc_value = 0x1234;
|
|
send("B0000040118\nD\nC\n");
|
|
TEST_ASSERT_EQUAL(1, display.count("display"));
|
|
TEST_ASSERT_EQUAL(1, display.count("pixel"));
|
|
TEST_ASSERT_EQUAL_STRING("C 1234\n", Serial.out.c_str());
|
|
}
|
|
|
|
void test_crc_counts_only_the_dots()
|
|
{
|
|
// 84 x 19: 11 bytes per row, the top 4 bits of each 11th byte are not dots
|
|
uint8_t a[11 * 19] = {}, b[11 * 19] = {};
|
|
TEST_ASSERT_EQUAL_HEX16(bufferCrc(a, 84, 19), bufferCrc(b, 84, 19));
|
|
for (int y = 0; y < 19; y++)
|
|
b[y * 11 + 10] = 0xF0; // what fillScreen(1) leaves there
|
|
TEST_ASSERT_EQUAL_HEX16(bufferCrc(a, 84, 19), bufferCrc(b, 84, 19));
|
|
b[5] = 0x01; // one dot
|
|
TEST_ASSERT_NOT_EQUAL(bufferCrc(a, 84, 19), bufferCrc(b, 84, 19));
|
|
// CRC-16/CCITT-FALSE check value
|
|
const uint8_t check[9] = {'1', '2', '3', '4', '5', '6', '7', '8', '9'};
|
|
TEST_ASSERT_EQUAL_HEX16(0x29B1, bufferCrc(check, 72, 1));
|
|
}
|
|
|
|
int main()
|
|
{
|
|
UNITY_BEGIN();
|
|
|
|
RUN_TEST(test_boot_does_not_flip_dots);
|
|
RUN_TEST(test_boot_sends_ready);
|
|
RUN_TEST(test_boot_uses_default_flip_time_with_empty_eeprom);
|
|
|
|
RUN_TEST(test_pixel_runs_without_waiting_for_next_command);
|
|
RUN_TEST(test_direct_pixel_flips_immediately_and_resets_direct_mode);
|
|
RUN_TEST(test_fill);
|
|
RUN_TEST(test_display);
|
|
RUN_TEST(test_display_instant_frame);
|
|
RUN_TEST(test_refresh_starts_between_instant_frames);
|
|
RUN_TEST(test_lowercase_and_uppercase_commands);
|
|
RUN_TEST(test_command_split_over_several_reads_with_crlf);
|
|
RUN_TEST(test_print_text_with_newline_escape);
|
|
RUN_TEST(test_print_buffer);
|
|
|
|
RUN_TEST(test_bitmap_pixels);
|
|
RUN_TEST(test_full_screen_bitmap_is_streamed);
|
|
RUN_TEST(test_frame_commands_send_only_the_changed_dots);
|
|
RUN_TEST(test_frame_commands_full_frame);
|
|
RUN_TEST(test_frame_commands_edge_and_nothing_changed);
|
|
RUN_TEST(test_invalid_bitmap_data_is_not_run_as_commands);
|
|
RUN_TEST(test_incomplete_bitmap_header);
|
|
|
|
RUN_TEST(test_too_long_line_is_rejected);
|
|
RUN_TEST(test_truncated_command_is_ignored);
|
|
RUN_TEST(test_unknown_command_is_ignored);
|
|
|
|
RUN_TEST(test_timing_is_applied_immediately);
|
|
RUN_TEST(test_timing_above_255_is_stored_and_restored);
|
|
RUN_TEST(test_timing_out_of_range_is_rejected);
|
|
|
|
RUN_TEST(test_defaults_with_empty_eeprom);
|
|
RUN_TEST(test_eeprom_from_before_the_new_settings_keeps_flip_time);
|
|
RUN_TEST(test_gap_is_applied_and_stored);
|
|
RUN_TEST(test_gap_out_of_range_is_rejected);
|
|
RUN_TEST(test_pulses_are_applied_and_stored);
|
|
RUN_TEST(test_pulses_out_of_range_are_rejected);
|
|
RUN_TEST(test_effect_is_applied_and_stored);
|
|
RUN_TEST(test_effect_without_saving);
|
|
RUN_TEST(test_invalid_effect_is_rejected);
|
|
RUN_TEST(test_every_effect_visits_each_dot_once);
|
|
RUN_TEST(test_wipe_goes_left_to_right_and_center_starts_in_the_middle);
|
|
RUN_TEST(test_sparkle_order_is_scrambled);
|
|
|
|
RUN_TEST(test_refresh_when_due);
|
|
RUN_TEST(test_refresh_only_once_per_interval);
|
|
RUN_TEST(test_running_refresh_is_not_restarted);
|
|
RUN_TEST(test_no_refresh_storm_after_millis_wraps);
|
|
RUN_TEST(test_refresh_interval_is_applied_and_stored);
|
|
RUN_TEST(test_refresh_interval_zero_never_refreshes);
|
|
RUN_TEST(test_refresh_interval_out_of_range_is_rejected);
|
|
|
|
RUN_TEST(test_checksum_reply);
|
|
RUN_TEST(test_checksum_follows_the_frame);
|
|
RUN_TEST(test_crc_counts_only_the_dots);
|
|
|
|
return UNITY_END();
|
|
}
|