Public Access
Frames were sent as F0 plus one s command per yellow dot (thousands of characters). During scrolling that overran the Nano's serial buffer, and half-received frames redrew the whole screen. showBitmap() now sends two B commands over the changed rectangle (dots off, dots on), or one B command for a full frame, and the pacing counts the characters sent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
663 lines
18 KiB
C++
663 lines
18 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 "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_waits_for_idle_after_instant_frames()
|
|
{
|
|
last_refresh = fake_millis;
|
|
fake_millis += 120000;
|
|
send("D1\n");
|
|
TEST_ASSERT_EQUAL(1, display.count("display")); // no full redraw: more frames follow
|
|
fake_millis += 3000;
|
|
loop();
|
|
TEST_ASSERT_EQUAL(2, display.count("display"));
|
|
TEST_ASSERT_EQUAL(1, display.calls.back().a);
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
void test_invalid_effect_is_rejected()
|
|
{
|
|
send("E10,1500\n"); // unknown effect
|
|
send("E1,1500,2\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"));
|
|
}
|
|
|
|
void test_no_full_redraw_while_an_effect_runs()
|
|
{
|
|
last_refresh = fake_millis;
|
|
fake_millis += 120000;
|
|
display.effect_running = true;
|
|
send("D\n");
|
|
TEST_ASSERT_EQUAL(1, display.count("display")); // only the frame itself
|
|
display.effect_running = false;
|
|
loop();
|
|
TEST_ASSERT_EQUAL(2, display.count("display"));
|
|
TEST_ASSERT_EQUAL(1, display.last("display")->a);
|
|
}
|
|
|
|
// 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_waits_for_end_of_frame()
|
|
{
|
|
last_refresh = fake_millis;
|
|
fake_millis += 120000;
|
|
send("s000001\n");
|
|
TEST_ASSERT_EQUAL(0, display.count("display"));
|
|
send("D\n");
|
|
TEST_ASSERT_EQUAL(2, display.count("display"));
|
|
TEST_ASSERT_EQUAL(1, display.calls.back().a); // forced full redraw after the frame
|
|
}
|
|
|
|
void test_refresh_only_once_per_interval()
|
|
{
|
|
last_refresh = fake_millis;
|
|
fake_millis += 120000;
|
|
send("D\n");
|
|
display.calls.clear();
|
|
send("D\n");
|
|
TEST_ASSERT_EQUAL(1, display.count("display"));
|
|
}
|
|
|
|
void test_refresh_when_serial_is_idle()
|
|
{
|
|
last_refresh = fake_millis;
|
|
fake_millis += 120000;
|
|
loop();
|
|
TEST_ASSERT_EQUAL(1, display.count("display"));
|
|
TEST_ASSERT_EQUAL(1, display.last("display")->a);
|
|
}
|
|
|
|
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(5, display.count("display"));
|
|
|
|
fake_millis += 120000;
|
|
display.calls.clear();
|
|
send("D\n");
|
|
TEST_ASSERT_EQUAL(2, display.count("display"));
|
|
}
|
|
|
|
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_waits_for_idle_after_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_invalid_effect_is_rejected);
|
|
RUN_TEST(test_no_full_redraw_while_an_effect_runs);
|
|
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_waits_for_end_of_frame);
|
|
RUN_TEST(test_refresh_only_once_per_interval);
|
|
RUN_TEST(test_refresh_when_serial_is_idle);
|
|
RUN_TEST(test_no_refresh_storm_after_millis_wraps);
|
|
|
|
return UNITY_END();
|
|
}
|