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>
242 lines
6.2 KiB
C++
242 lines
6.2 KiB
C++
// Tests for the real display driver (lib/BROSE9323, Nano build) on the PC, with the
|
|
// simulated panel of test/driver_mocks: pio test -e test_driver
|
|
//
|
|
// What matters most: drawing never blocks loop() (the Nano must keep reading the serial
|
|
// port, or bytes are lost and the picture ends up shifted), and the dots end up right.
|
|
|
|
#include <unity.h>
|
|
|
|
#include "../../lib/BROSE9323/BROSE9323.cpp"
|
|
|
|
static const int W = 84, H = 19;
|
|
|
|
// Like the user's settings: 3 pulses of 200 us with a 300 us gap
|
|
static BROSE9323 *make()
|
|
{
|
|
BROSE9323 *d = new BROSE9323(W, H, 28, 200);
|
|
d->setGap(300);
|
|
d->setPulses(3);
|
|
panel = FakePanel(); // before begin(): it sets the pins
|
|
d->begin();
|
|
return d;
|
|
}
|
|
|
|
// Runs update() until the frame and the refresh are done. Fails when one call takes
|
|
// longer than max_us (fake time): then loop() would not read the serial port in time.
|
|
static int finish(BROSE9323 &d, uint32_t max_us = 3000)
|
|
{
|
|
int calls = 0;
|
|
while (d.busy() || d.refreshing())
|
|
{
|
|
uint32_t start = fake_micros;
|
|
d.update();
|
|
uint32_t took = fake_micros - start;
|
|
if (took > max_us)
|
|
{
|
|
char msg[64];
|
|
snprintf(msg, sizeof(msg), "update() took %u us", (unsigned)took);
|
|
TEST_FAIL_MESSAGE(msg);
|
|
}
|
|
TEST_ASSERT_LESS_THAN(1000000, ++calls);
|
|
}
|
|
return calls;
|
|
}
|
|
|
|
static void assertPanelShows(BROSE9323 &d)
|
|
{
|
|
for (int x = 0; x < W; x++)
|
|
for (int y = 0; y < H; y++)
|
|
if (panel.dot[x][y] != d.shownPixel(x, y))
|
|
{
|
|
char msg[64];
|
|
snprintf(msg, sizeof(msg), "dot %d,%d differs", x, y);
|
|
TEST_FAIL_MESSAGE(msg);
|
|
}
|
|
}
|
|
|
|
static void drawPattern(BROSE9323 &d, int seed)
|
|
{
|
|
for (int x = 0; x < W; x++)
|
|
for (int y = 0; y < H; y++)
|
|
d.drawPixel(x, y, ((x * 7 + y * 3 + seed) % 5) == 0);
|
|
}
|
|
|
|
static int dotsFlippedAtLeast(int n)
|
|
{
|
|
int count = 0;
|
|
for (int x = 0; x < W; x++)
|
|
for (int y = 0; y < H; y++)
|
|
count += panel.flips[x][y] >= n;
|
|
return count;
|
|
}
|
|
|
|
void setUp() {}
|
|
void tearDown() {}
|
|
|
|
void test_first_frame_flips_every_dot_without_blocking()
|
|
{
|
|
BROSE9323 *d = make();
|
|
drawPattern(*d, 0);
|
|
uint32_t start = fake_micros;
|
|
d->display();
|
|
TEST_ASSERT_LESS_THAN(3000, fake_micros - start); // display() itself returns at once
|
|
TEST_ASSERT_TRUE(d->busy());
|
|
int calls = finish(*d);
|
|
TEST_ASSERT_GREATER_THAN(100, calls); // spread over many loop() passes
|
|
TEST_ASSERT_EQUAL(W * H, dotsFlippedAtLeast(1));
|
|
assertPanelShows(*d);
|
|
delete d;
|
|
}
|
|
|
|
void test_instant_frame_flips_only_the_changed_dots_with_all_pulses()
|
|
{
|
|
BROSE9323 *d = make();
|
|
drawPattern(*d, 0);
|
|
d->display();
|
|
finish(*d);
|
|
panel.clearCounts();
|
|
|
|
d->drawPixel(10, 5, !d->shownPixel(10, 5));
|
|
d->drawPixel(70, 18, !d->shownPixel(70, 18));
|
|
d->display(false, true);
|
|
finish(*d);
|
|
TEST_ASSERT_EQUAL(6, panel.strobes); // 2 dots, 3 pulses each
|
|
TEST_ASSERT_EQUAL(3, panel.flips[10][5]);
|
|
assertPanelShows(*d);
|
|
delete d;
|
|
}
|
|
|
|
void test_effects_end_with_the_right_image()
|
|
{
|
|
for (uint8_t effect = EFFECT_SPARKLE; effect < EFFECT_COUNT; effect++)
|
|
{
|
|
BROSE9323 *d = make();
|
|
d->setEffect(effect, 500);
|
|
drawPattern(*d, 0);
|
|
d->display();
|
|
finish(*d);
|
|
assertPanelShows(*d);
|
|
drawPattern(*d, 2);
|
|
d->display();
|
|
finish(*d, 5000);
|
|
assertPanelShows(*d);
|
|
delete d;
|
|
}
|
|
}
|
|
|
|
void test_refresh_flips_every_dot_once_without_blocking()
|
|
{
|
|
BROSE9323 *d = make();
|
|
drawPattern(*d, 0);
|
|
d->display();
|
|
finish(*d);
|
|
panel.clearCounts();
|
|
|
|
d->startRefresh();
|
|
TEST_ASSERT_TRUE(d->refreshing());
|
|
TEST_ASSERT_FALSE(d->busy()); // not a frame: loop() may take new frames meanwhile
|
|
int calls = finish(*d);
|
|
TEST_ASSERT_GREATER_THAN(100, calls);
|
|
TEST_ASSERT_EQUAL(W * H, panel.strobes); // one pulse each, nothing changed
|
|
TEST_ASSERT_EQUAL(W * H, dotsFlippedAtLeast(1));
|
|
assertPanelShows(*d);
|
|
delete d;
|
|
}
|
|
|
|
void test_refresh_pauses_while_a_frame_arrives_and_then_finishes()
|
|
{
|
|
BROSE9323 *d = make();
|
|
drawPattern(*d, 0);
|
|
d->display();
|
|
finish(*d);
|
|
panel.clearCounts();
|
|
|
|
d->startRefresh();
|
|
d->update(); // a part of the dots
|
|
int before = panel.strobes;
|
|
TEST_ASSERT_GREATER_THAN(0, before);
|
|
TEST_ASSERT_LESS_THAN(W * H, before);
|
|
|
|
// A new frame is being received: the refresh must not show half of it
|
|
bool old = d->shownPixel(83, 18);
|
|
d->drawPixel(83, 18, !old);
|
|
for (int i = 0; i < 50; i++)
|
|
d->update();
|
|
TEST_ASSERT_EQUAL(before, panel.strobes);
|
|
TEST_ASSERT_EQUAL(old, panel.dot[83][18]);
|
|
|
|
d->display(false, true);
|
|
finish(*d);
|
|
assertPanelShows(*d);
|
|
TEST_ASSERT_EQUAL(W * H, dotsFlippedAtLeast(1)); // the refresh got to the end
|
|
TEST_ASSERT_FALSE(d->refreshing());
|
|
delete d;
|
|
}
|
|
|
|
void test_refresh_waits_for_a_running_effect()
|
|
{
|
|
BROSE9323 *d = make();
|
|
d->setEffect(EFFECT_WIPE, 1000);
|
|
drawPattern(*d, 0);
|
|
d->display();
|
|
finish(*d);
|
|
drawPattern(*d, 1);
|
|
d->display();
|
|
d->startRefresh();
|
|
panel.clearCounts();
|
|
// The wipe goes left to right; the refresh would start at the left too and finish
|
|
// before the wipe reaches the right side if it ran at the same time
|
|
while (d->busy())
|
|
d->update();
|
|
TEST_ASSERT_TRUE(d->refreshing());
|
|
finish(*d);
|
|
assertPanelShows(*d);
|
|
delete d;
|
|
}
|
|
|
|
void test_interrupted_first_frame_still_reaches_every_dot()
|
|
{
|
|
BROSE9323 *d = make();
|
|
drawPattern(*d, 0);
|
|
d->display(); // after boot: every dot
|
|
for (int i = 0; i < 20; i++)
|
|
d->update();
|
|
TEST_ASSERT_LESS_THAN(W * H, dotsFlippedAtLeast(1));
|
|
|
|
// The next frame (scrolling text) arrives before it is done
|
|
d->drawPixel(0, 0, !d->shownPixel(0, 0));
|
|
d->display(false, true);
|
|
finish(*d);
|
|
TEST_ASSERT_EQUAL(W * H, dotsFlippedAtLeast(1));
|
|
assertPanelShows(*d);
|
|
delete d;
|
|
}
|
|
|
|
void test_crc_of_equal_images_is_equal()
|
|
{
|
|
BROSE9323 *a = make(), *b = make();
|
|
a->fillScreen(1); // also sets the unused bits at the end of each row
|
|
for (int x = 0; x < W; x++)
|
|
for (int y = 0; y < H; y++)
|
|
b->drawPixel(x, y, 1);
|
|
TEST_ASSERT_EQUAL_HEX16(a->crc(), b->crc());
|
|
b->drawPixel(40, 9, 0);
|
|
TEST_ASSERT_NOT_EQUAL(a->crc(), b->crc());
|
|
delete a;
|
|
delete b;
|
|
}
|
|
|
|
int main()
|
|
{
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_first_frame_flips_every_dot_without_blocking);
|
|
RUN_TEST(test_instant_frame_flips_only_the_changed_dots_with_all_pulses);
|
|
RUN_TEST(test_effects_end_with_the_right_image);
|
|
RUN_TEST(test_refresh_flips_every_dot_once_without_blocking);
|
|
RUN_TEST(test_refresh_pauses_while_a_frame_arrives_and_then_finishes);
|
|
RUN_TEST(test_refresh_waits_for_a_running_effect);
|
|
RUN_TEST(test_interrupted_first_frame_still_reaches_every_dot);
|
|
RUN_TEST(test_crc_of_equal_images_is_equal);
|
|
return UNITY_END();
|
|
}
|