Public Access
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:
1 parent
58a7cb03c2
commit
090c2bf8ae
45 files changed
+1265
-157
No files matched your search
@@ -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;
|
||||
};
|
||||
@@ -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); }
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
Reference in new issue
Block a user