Fix shifted pictures: non-blocking Nano drawing, frame check, refresh settings

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>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-16 15:20:26 +02:00
1 parent 58a7cb03c2
commit 090c2bf8ae
45 files changed
+1265 -157

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
// Just enough Arduino and Adafruit GFX to run the real BROSE9323 driver (Nano build) on
// the PC, with a simulated panel: test/test_driver. The panel decodes the pin levels at
// each strobe (ENABLE low) into the dot that flips, like the Brose panel electronics.
#pragma once
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#define A0 14
#define A1 15
#define A2 16
#define A3 17
#define A4 18
#define A5 19
#define OUTPUT 1
// Every micros() call takes a microsecond, so loops that wait for the time end
inline uint32_t fake_micros = 0;
inline uint32_t micros() { return ++fake_micros; }
inline uint32_t millis() { return fake_micros / 1000; }
inline void delayMicroseconds(unsigned int us) { fake_micros += us; }
inline void pinMode(uint8_t, uint8_t) {}
struct FakePanel
{
static const int W = 84, H = 19, PANEL_W = 28;
bool dot[W][H] = {};
int flips[W][H] = {}; // strobes per dot
int strobes = 0;
bool pins[20] = {};
void clearCounts()
{
memset(flips, 0, sizeof(flips));
strobes = 0;
}
void write(uint8_t pin, bool level)
{
pins[pin] = level;
if (pin == 7 && !level) // ENABLE low: flip the addressed dot
strobe();
}
void strobe()
{
int panel = pins[3] | pins[5] << 1 | pins[6] << 2;
int col_code = pins[8] | pins[10] << 1 | pins[9] << 2 | pins[11] << 3 | pins[12] << 4;
int row_code = pins[A3] | pins[A4] << 1 | pins[A2] << 2 | pins[A0] << 3 | pins[A1] << 4;
int col = -1, row = -1;
for (int c = 0; c < PANEL_W; c++)
if (c + 1 + c / 7 == col_code)
col = c;
for (int r = 0; r < H; r++)
if (r + r / 7 == row_code)
row = r;
if (col < 0 || row < 0 || panel > 2)
{
fprintf(stderr, "bad address: panel %d col code %d row code %d\n", panel, col_code, row_code);
abort();
}
int x = panel * PANEL_W + col;
dot[x][row] = !pins[4]; // COL_DATA low sets the dot (yellow)
flips[x][row]++;
strobes++;
}
};
inline FakePanel panel;
inline void digitalWrite(uint8_t pin, int level) { panel.write(pin, level != 0); }
struct FakeSerialOut
{
void write(char) {}
void println() {}
};
inline FakeSerialOut Serial;
// Only what BROSE9323::print() uses
class String
{
public:
String(const char *s) : _s(s) {}
void replace(const char *from, const char *to)
{
size_t pos = 0, n = strlen(from);
while ((pos = _s.find(from, pos)) != std::string::npos)
{
_s.replace(pos, n, to);
pos += strlen(to);
}
}
const char *c_str() const { return _s.c_str(); }
private:
std::string _s;
};
class Adafruit_GFX
{
public:
Adafruit_GFX(int16_t w, int16_t h) : _width(w), _height(h) {}
virtual ~Adafruit_GFX() {}
virtual void drawPixel(int16_t x, int16_t y, uint16_t color) = 0;
int16_t width() const { return _width; }
int16_t height() const { return _height; }
size_t print(const char *s) { return strlen(s); }
protected:
int16_t _width, _height;
};
+5
View File
@@ -19,6 +19,8 @@ public:
std::vector<DisplayCall> calls;
bool direct = false;
bool effect_running = false; // what busy() returns
bool refresh_running = false; // what refreshing() returns
uint16_t crc_value = 0; // what crc() returns
BROSE9323(uint8_t, uint8_t, uint8_t, uint16_t) {}
@@ -33,6 +35,9 @@ public:
void setEffect(uint8_t effect, uint16_t time_ms, uint8_t flags = 0) { log("effect", effect, time_ms, flags); }
bool update() { return effect_running; }
bool busy() const { return effect_running; }
void startRefresh() { log("refresh"); }
bool refreshing() const { return refresh_running; }
uint16_t crc() const { return crc_value; }
void printBuffer() { log("printBuffer"); }
void setCursor(int16_t x, int16_t y) { log("cursor", x, y); }
void setTextColor(uint16_t color, uint16_t background) { log("textColor", color, background); }
+241
View File
@@ -0,0 +1,241 @@
// 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();
}
+136
View File
@@ -12,6 +12,8 @@
#include <vector>
#include "config.h"
#include "logic/frame_check.h"
#include "logic/loop_stats.h"
#include "logic/bitmap_format.h"
#include "logic/builtin_fonts.h"
#include "logic/builtin_pages.h"
@@ -1900,6 +1902,133 @@ void test_ha_text_map()
TEST_ASSERT_NOT_NULL(validatePage(page));
}
void test_loop_stats()
{
LoopStats stats;
TEST_ASSERT_EQUAL_UINT32(0, stats.loopsPerSecond());
// 995 passes of 10 ms (the micros() counter wraps on the way), one of them 50 ms
uint32_t t = 0xFFFFFFFFUL - 5000000UL;
stats.tick(t);
for (int i = 0; i < 995; i++)
{
t += i == 500 ? 50000 : 10000;
stats.tick(t);
}
TEST_ASSERT_EQUAL_UINT32(0, stats.loopsPerSecond()); // window not complete yet
t += 10000;
stats.tick(t);
TEST_ASSERT_EQUAL_UINT32(99, stats.loopsPerSecond());
TEST_ASSERT_EQUAL_UINT32(10040, stats.averageUs());
TEST_ASSERT_EQUAL_UINT32(50000, stats.maxUs());
TEST_ASSERT_EQUAL_UINT32(50000, stats.maxEverUs());
// Next window without the slow pass: the window max drops, the boot max stays
for (int i = 0; i < 1000; i++)
{
t += 10000;
stats.tick(t);
}
TEST_ASSERT_EQUAL_UINT32(100, stats.loopsPerSecond());
TEST_ASSERT_EQUAL_UINT32(10000, stats.maxUs());
TEST_ASSERT_EQUAL_UINT32(50000, stats.maxEverUs());
}
void test_frame_check_ok()
{
FrameCheck c;
c.reset(1000);
TEST_ASSERT_TRUE(c.due(5, 1000)); // first check right after switching on
c.sent(0x1234, 5, 1000);
TEST_ASSERT_FALSE(c.due(6, 5000)); // waiting for the answer
c.line("C 1234\r", 1010);
TEST_ASSERT_EQUAL_UINT32(1, c.stats().checks);
TEST_ASSERT_FALSE(c.resyncNow(1020, 0));
// The next one after a new frame, at most every FRAME_CHECK_INTERVAL
TEST_ASSERT_FALSE(c.due(5, 10000)); // no new frame
TEST_ASSERT_FALSE(c.due(6, 1000 + FRAME_CHECK_INTERVAL - 1));
TEST_ASSERT_TRUE(c.due(6, 1000 + FRAME_CHECK_INTERVAL));
}
void test_frame_check_mismatch_repairs()
{
FrameCheck c;
c.reset(0);
c.sent(0x1234, 1, 20000);
c.line("C 1235", 20010);
TEST_ASSERT_EQUAL_UINT32(1, c.stats().mismatches);
TEST_ASSERT_FALSE(c.due(2, 30000)); // the repair comes first
TEST_ASSERT_TRUE(c.resyncNow(20010, 0));
TEST_ASSERT_EQUAL_UINT32(1, c.stats().repairs);
TEST_ASSERT_FALSE(c.resyncNow(20020, 0)); // once
TEST_ASSERT_TRUE(c.due(2, 30000)); // then the repaired frame is checked
}
void test_frame_check_repairs_at_most_every_gap()
{
FrameCheck c;
c.reset(0);
c.sent(1, 1, 100);
c.line("C 0002", 110);
TEST_ASSERT_FALSE(c.resyncNow(FRAME_REPAIR_MIN_GAP - 1, 0)); // switched on just now
TEST_ASSERT_TRUE(c.resyncNow(FRAME_REPAIR_MIN_GAP, 0));
}
void test_frame_check_no_reply()
{
FrameCheck c;
c.reset(0);
// Never answered (old Nano firmware, TX not wired): counted, but no repairs
c.sent(1, 1, 100);
TEST_ASSERT_FALSE(c.resyncNow(100 + FRAME_CHECK_TIMEOUT - 1, 0));
TEST_ASSERT_FALSE(c.resyncNow(100 + FRAME_CHECK_TIMEOUT + FRAME_REPAIR_MIN_GAP, 0));
TEST_ASSERT_EQUAL_UINT32(1, c.stats().no_reply);
TEST_ASSERT_EQUAL_UINT32(0, c.stats().repairs);
// It answered before: a lost answer means lost bytes, repair
uint32_t t = 100000;
c.sent(1, 2, t);
c.line("C 0001", t + 5);
t += FRAME_CHECK_INTERVAL;
c.sent(1, 3, t);
TEST_ASSERT_TRUE(c.resyncNow(t + FRAME_CHECK_TIMEOUT, 0));
TEST_ASSERT_EQUAL_UINT32(2, c.stats().no_reply);
TEST_ASSERT_EQUAL_UINT32(1, c.stats().repairs);
// A late answer to the timed out check is ignored
c.line("C 0009", t + FRAME_CHECK_TIMEOUT + 10);
TEST_ASSERT_EQUAL_UINT32(0, c.stats().mismatches);
}
void test_frame_check_errors_and_restarts()
{
FrameCheck c;
c.reset(0);
c.line("ERR: Line too long", 10);
c.line("ERR: Invalid bitmap width (not dividable by 4) and a lot more text than fits", 20);
TEST_ASSERT_EQUAL_UINT32(2, c.stats().errors);
TEST_ASSERT_EQUAL_UINT32(sizeof(c.stats().last_error) - 1, strlen(c.stats().last_error));
TEST_ASSERT_EQUAL(0, strncmp(c.stats().last_error, "ERR: Invalid bitmap", 19));
TEST_ASSERT_FALSE(c.resyncNow(FRAME_REPAIR_MIN_GAP, 0)); // errors alone: the check decides
c.line("READY", 20000);
TEST_ASSERT_EQUAL_UINT32(1, c.stats().restarts);
TEST_ASSERT_TRUE(c.resyncNow(20000, 0));
c.line("garbage", 20010);
c.line("C 12", 20010);
TEST_ASSERT_EQUAL_UINT32(0, c.stats().checks + c.stats().mismatches);
}
void test_frame_check_periodic_resync()
{
FrameCheck c;
c.reset(1000);
TEST_ASSERT_FALSE(c.resyncNow(1000 + 600000 - 1, 600000));
TEST_ASSERT_TRUE(c.resyncNow(1000 + 600000, 600000));
TEST_ASSERT_FALSE(c.resyncNow(1000 + 600001, 600000));
TEST_ASSERT_FALSE(c.resyncNow(0xFFFFFFF0u, 0)); // 0: never
TEST_ASSERT_EQUAL_UINT32(0, c.stats().repairs); // not counted as a repair
}
int main()
{
UNITY_BEGIN();
@@ -2022,6 +2151,13 @@ int main()
RUN_TEST(test_ha_duration);
RUN_TEST(test_ha_timestamps);
RUN_TEST(test_ha_text_map);
RUN_TEST(test_loop_stats);
RUN_TEST(test_frame_check_ok);
RUN_TEST(test_frame_check_mismatch_repairs);
RUN_TEST(test_frame_check_repairs_at_most_every_gap);
RUN_TEST(test_frame_check_no_reply);
RUN_TEST(test_frame_check_errors_and_restarts);
RUN_TEST(test_frame_check_periodic_resync);
return UNITY_END();
}
+113 -38
View File
@@ -8,6 +8,7 @@
#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>
@@ -117,16 +118,15 @@ void test_display_instant_frame()
TEST_ASSERT_EQUAL(1, display.last("display")->b); // without the effect
}
void test_refresh_waits_for_idle_after_instant_frames()
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")); // 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);
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()
@@ -455,19 +455,6 @@ void test_invalid_effect_is_rejected()
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)
{
@@ -560,15 +547,16 @@ void test_sparkle_order_is_scrambled()
// Periodic full redraw
// ---------------------------------------------------------------------------
void test_refresh_waits_for_end_of_frame()
void test_refresh_when_due()
{
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
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()
@@ -576,18 +564,19 @@ 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"));
loop();
TEST_ASSERT_EQUAL(1, display.count("refresh"));
}
void test_refresh_when_serial_is_idle()
void test_running_refresh_is_not_restarted()
{
last_refresh = fake_millis;
fake_millis += 120000;
display.refresh_running = true;
loop();
TEST_ASSERT_EQUAL(1, display.count("display"));
TEST_ASSERT_EQUAL(1, display.last("display")->a);
display.refresh_running = false;
TEST_ASSERT_EQUAL(0, display.count("refresh"));
}
void test_no_refresh_storm_after_millis_wraps()
@@ -597,12 +586,92 @@ void test_no_refresh_storm_after_millis_wraps()
fake_millis += 5000; // wraps past 0
for (int i = 0; i < 5; i++)
send("D\n");
TEST_ASSERT_EQUAL(5, display.count("display"));
TEST_ASSERT_EQUAL(0, display.count("refresh"));
fake_millis += 120000;
display.calls.clear();
send("D\n");
TEST_ASSERT_EQUAL(2, display.count("display"));
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()
@@ -618,7 +687,7 @@ int main()
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_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);
@@ -648,15 +717,21 @@ int main()
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_when_due);
RUN_TEST(test_refresh_only_once_per_interval);
RUN_TEST(test_refresh_when_serial_is_idle);
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();
}