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,120 @@
|
||||
#include "logic/frame_check.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "config.h"
|
||||
|
||||
static int hexDigit(char c)
|
||||
{
|
||||
if (c >= '0' && c <= '9')
|
||||
return c - '0';
|
||||
if (c >= 'A' && c <= 'F')
|
||||
return c - 'A' + 10;
|
||||
if (c >= 'a' && c <= 'f')
|
||||
return c - 'a' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
void FrameCheck::reset(uint32_t now)
|
||||
{
|
||||
_pending = false;
|
||||
_checked_any = false;
|
||||
_replied = false;
|
||||
_problem = false;
|
||||
// Switching on sends the whole image anyway
|
||||
_last_resync = now;
|
||||
}
|
||||
|
||||
bool FrameCheck::due(uint32_t frames_sent, uint32_t now) const
|
||||
{
|
||||
// A repair is coming anyway: no need to count the same problem again
|
||||
if (_pending || _problem)
|
||||
return false;
|
||||
if (!_checked_any)
|
||||
return true;
|
||||
return frames_sent != _checked_frames && now - _last_check >= FRAME_CHECK_INTERVAL;
|
||||
}
|
||||
|
||||
void FrameCheck::sent(uint16_t expected, uint32_t frames_sent, uint32_t now)
|
||||
{
|
||||
_pending = true;
|
||||
_expected = expected;
|
||||
_sent_at = now;
|
||||
_checked_any = true;
|
||||
_checked_frames = frames_sent;
|
||||
_last_check = now;
|
||||
}
|
||||
|
||||
void FrameCheck::line(const char *text, uint32_t now)
|
||||
{
|
||||
(void)now;
|
||||
size_t len = strlen(text);
|
||||
while (len && (text[len - 1] == '\r' || text[len - 1] == ' '))
|
||||
len--;
|
||||
|
||||
if (len == 6 && text[0] == 'C' && text[1] == ' ')
|
||||
{
|
||||
int value = 0;
|
||||
for (int i = 2; i < 6; i++)
|
||||
{
|
||||
int d = hexDigit(text[i]);
|
||||
if (d < 0)
|
||||
return;
|
||||
value = value << 4 | d;
|
||||
}
|
||||
_replied = true;
|
||||
if (!_pending)
|
||||
return; // late answer to a check that already timed out
|
||||
_pending = false;
|
||||
if (value == _expected)
|
||||
_stats.checks++;
|
||||
else
|
||||
{
|
||||
_stats.mismatches++;
|
||||
_problem = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (strncmp(text, "ERR", 3) == 0)
|
||||
{
|
||||
_replied = true;
|
||||
_stats.errors++;
|
||||
size_t n = len < sizeof(_stats.last_error) - 1 ? len : sizeof(_stats.last_error) - 1;
|
||||
memcpy(_stats.last_error, text, n);
|
||||
_stats.last_error[n] = '\0';
|
||||
return;
|
||||
}
|
||||
if (len >= 5 && strncmp(text, "READY", 5) == 0)
|
||||
{
|
||||
// The Nano restarted while it was on: its image is gone
|
||||
_replied = true;
|
||||
_stats.restarts++;
|
||||
_pending = false;
|
||||
_problem = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool FrameCheck::resyncNow(uint32_t now, uint32_t resync_ms)
|
||||
{
|
||||
if (_pending && now - _sent_at >= FRAME_CHECK_TIMEOUT)
|
||||
{
|
||||
_pending = false;
|
||||
_stats.no_reply++;
|
||||
// Only when answers can arrive at all (old Nano firmware, TX not wired: never)
|
||||
if (_replied)
|
||||
_problem = true;
|
||||
}
|
||||
if (_problem && now - _last_resync >= FRAME_REPAIR_MIN_GAP)
|
||||
{
|
||||
_problem = false;
|
||||
_stats.repairs++;
|
||||
_last_resync = now;
|
||||
return true;
|
||||
}
|
||||
if (resync_ms && now - _last_resync >= resync_ms)
|
||||
{
|
||||
_last_resync = now;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// Checks that the Nano shows what was sent. The Nano has no acknowledgement and drops
|
||||
// bytes when its serial buffer overflows; a lost byte in a bitmap shifts the rest of the
|
||||
// image (the dots after it land too early). After a frame the ESP32 asks for the checksum
|
||||
// of the Nano's image (C command, bufferCrc() in lib/BROSE9323) and compares it with its
|
||||
// own copy. A wrong checksum, a missing answer or a restarted Nano (READY) makes it send
|
||||
// the whole image again (repair). Besides that the whole image is sent again every
|
||||
// resync interval, which also fixes what the check can't see.
|
||||
//
|
||||
// Pure logic (tested in test/test_esp32); nano_link/nano_monitor does the serial part.
|
||||
|
||||
struct FrameCheckStats
|
||||
{
|
||||
uint32_t checks = 0; // answers that matched
|
||||
uint32_t mismatches = 0; // answers that didn't
|
||||
uint32_t no_reply = 0; // checks without an answer in FRAME_CHECK_TIMEOUT
|
||||
uint32_t errors = 0; // ERR: lines from the Nano
|
||||
uint32_t restarts = 0; // READY while it was on: the Nano restarted (power dip?)
|
||||
uint32_t repairs = 0; // whole image sent again because of one of the above
|
||||
char last_error[48] = "";
|
||||
};
|
||||
|
||||
class FrameCheck
|
||||
{
|
||||
public:
|
||||
// The Nano was switched on or off: forget the check in progress
|
||||
void reset(uint32_t now);
|
||||
|
||||
// A check (C) should be sent now: a frame was sent since the last one (frames_sent
|
||||
// changed), none is waiting for its answer and the last one is FRAME_CHECK_INTERVAL ago
|
||||
bool due(uint32_t frames_sent, uint32_t now) const;
|
||||
// The C was sent; expected: the checksum of the ESP32's copy
|
||||
void sent(uint16_t expected, uint32_t frames_sent, uint32_t now);
|
||||
|
||||
// One line from the Nano (without the '\n')
|
||||
void line(const char *text, uint32_t now);
|
||||
|
||||
// Call each loop(). True: send the whole image now (repair, or every resync_ms; 0 = no
|
||||
// periodic resync)
|
||||
bool resyncNow(uint32_t now, uint32_t resync_ms);
|
||||
|
||||
const FrameCheckStats &stats() const { return _stats; }
|
||||
|
||||
private:
|
||||
FrameCheckStats _stats;
|
||||
bool _pending = false;
|
||||
uint16_t _expected = 0;
|
||||
uint32_t _sent_at = 0;
|
||||
bool _checked_any = false; // since reset()
|
||||
uint32_t _checked_frames = 0;
|
||||
uint32_t _last_check = 0;
|
||||
bool _replied = false; // the Nano answered since reset(): its TX is wired
|
||||
bool _problem = false; // repair at the next chance
|
||||
uint32_t _last_resync = 0;
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// How busy loop() is. It never sleeps, so a CPU percentage would always be 100 %; what
|
||||
// matters is how long one pass takes (a slow pass delays OTA, the web UI and the frames).
|
||||
// tick() is called at the start of every pass with micros(); the numbers cover the last
|
||||
// complete window of LOOP_STATS_WINDOW_US.
|
||||
class LoopStats
|
||||
{
|
||||
public:
|
||||
static const uint32_t WINDOW_US = 10000000UL; // 10 s
|
||||
|
||||
void tick(uint32_t now_us)
|
||||
{
|
||||
if (!_started)
|
||||
{
|
||||
_started = true;
|
||||
_last = now_us;
|
||||
_window_start = now_us;
|
||||
return;
|
||||
}
|
||||
uint32_t pass = now_us - _last;
|
||||
_last = now_us;
|
||||
_count++;
|
||||
if (pass > _max)
|
||||
_max = pass;
|
||||
if (pass > _max_ever)
|
||||
_max_ever = pass;
|
||||
|
||||
uint32_t window = now_us - _window_start;
|
||||
if (window >= WINDOW_US)
|
||||
{
|
||||
_loops_per_s = (uint32_t)((uint64_t)_count * 1000000UL / window);
|
||||
_avg_us = window / _count;
|
||||
_window_max_us = _max;
|
||||
_count = 0;
|
||||
_max = 0;
|
||||
_window_start = now_us;
|
||||
}
|
||||
}
|
||||
|
||||
// Passes per second, average and longest pass in us (last window), longest since boot
|
||||
uint32_t loopsPerSecond() const { return _loops_per_s; }
|
||||
uint32_t averageUs() const { return _avg_us; }
|
||||
uint32_t maxUs() const { return _window_max_us; }
|
||||
uint32_t maxEverUs() const { return _max_ever; }
|
||||
|
||||
private:
|
||||
bool _started = false;
|
||||
uint32_t _last = 0, _window_start = 0;
|
||||
uint32_t _count = 0, _max = 0, _max_ever = 0;
|
||||
uint32_t _loops_per_s = 0, _avg_us = 0, _window_max_us = 0;
|
||||
};
|
||||
@@ -35,6 +35,8 @@ const char *validateSettings(const Settings &s)
|
||||
return "Unknown refresh effect";
|
||||
if (d.effect_time < 100 || d.effect_time > 10000)
|
||||
return "The effect time must be between 100 and 10000 ms";
|
||||
if (s.refresh.dots > REFRESH_MINUTES_MAX || s.refresh.image > REFRESH_MINUTES_MAX)
|
||||
return "Refresh intervals must be between 0 and 1440 minutes";
|
||||
|
||||
const HomeAssistantSettings &ha = s.home_assistant;
|
||||
if (!terminated(ha.host, sizeof(ha.host)) || !terminated(ha.user, sizeof(ha.user)) ||
|
||||
|
||||
@@ -54,6 +54,18 @@ struct DriveSettings
|
||||
bool effect_small_only = false; // big changes (a new page) appear instantly
|
||||
};
|
||||
|
||||
// Full redraws that fix the picture, in minutes (0 = never)
|
||||
const uint16_t REFRESH_MINUTES_MAX = 1440;
|
||||
struct RefreshSettings
|
||||
{
|
||||
// The Nano flips every dot once more (fixes dots that didn't flip). Sent to the Nano,
|
||||
// which keeps it in its EEPROM (I command, default there: 2).
|
||||
uint16_t dots = 2;
|
||||
// The ESP32 sends the whole image again (fixes a wrong image on the Nano, e.g. after
|
||||
// lost bytes). Only the dots that differ flip, so a correct image doesn't change.
|
||||
uint16_t image = 10;
|
||||
};
|
||||
|
||||
// What is shown during the day: the name of a dynamic page (logic/page.h) or a bitmap.
|
||||
// Both share one namespace. When it is gone, the built-in Clock page is shown.
|
||||
#define DEFAULT_PAGE BUILTIN_PAGE_CLOCK
|
||||
@@ -70,6 +82,7 @@ struct Settings
|
||||
bool status_led = true;
|
||||
char off_page[PAGE_NAME_SIZE] = OFF_PAGE_BLANK;
|
||||
DriveSettings drive;
|
||||
RefreshSettings refresh;
|
||||
// New fields go here, at the end (see system/settings_store.cpp)
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user