Public Access
Add HA dropdowns, off image, flip fixes and refresh effects
Home Assistant: remove the Clock switch (its entity is deleted on connect), add an IP sensor and Mode / Off image dropdowns whose options follow the saved bitmaps. Remove the built-in "Nintendo border" bitmap. Off image: a bitmap (or blank) left on the dots when the display switches off. Nano: pause after each flip pulse and repeat pulses for changed dots, both stored in EEPROM (G/N commands). Refresh effects (E command): instant, sparkle, wipe, rain, from the middle out, diagonal, spiral, flip all then reveal, push and random, optionally only for small changes. Effects run in steps from loop() so serial input keeps flowing. Web UI: flip settings, refresh effect and test patterns under Settings; the System tab shows why the ESP32 last restarted. Settings records from versions 1 and 2 are migrated. Firmware 2.3.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4a5f6a8b5d
commit
1d3b084de1
42 files changed
+1631
-230
No files matched your search
@@ -9,6 +9,9 @@
|
||||
#include "BROSE9323.h"
|
||||
#include "EEPROM.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
|
||||
// From src/nano/main.cpp
|
||||
extern BROSE9323 display;
|
||||
extern uint32_t last_refresh;
|
||||
@@ -233,6 +236,207 @@ void test_timing_out_of_range_is_rejected()
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -312,6 +516,19 @@ int main()
|
||||
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);
|
||||
|
||||
Reference in new issue
Block a user