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

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