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

+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();
}