Skip redundant panel refreshes and fetch the image before the config check

The panel driver now splits writing a frame into its SPI buffer
(epd_write_frame(), which also computes a CRC32 as it streams) from
actually triggering the physical refresh (epd_turn_on_display()).
frame_client.c compares the new CRC against the last one that was
actually refreshed (persisted in NVS) and skips the refresh entirely
when they match -- e.g. a reboot redisplaying the same photo before the
server's refresh interval elapsed no longer causes a visible flash for
no visual change.

Also reorders the per-wake fetch cycle: the image fetch (15s timeout)
now goes before the config check (3s timeout), instead of after. The
config check's tighter timeout was intermittently tripping on
connection-setup latency that's common on the first request after
waking from a long deep sleep (e.g. stale ARP); putting the more
tolerant request first absorbs that latency, and the config check then
rides the connection it already warmed up.
This commit is contained in:
2026-07-18 23:51:01 -04:00
parent d395cf3bb9
commit b9649c35ec
6 changed files with 184 additions and 60 deletions
+54 -27
View File
@@ -248,11 +248,30 @@ static esp_err_t fetch_and_display(const frame_config_t *cfg, bool force_advance
ESP_LOGI(TAG, "Fetching frame (%d bytes) from '%s'", content_length, url);
http_read_ctx_t ctx = { .client = client };
err = epd_display_stream(http_read_fn, &ctx);
uint32_t crc = 0;
err = epd_write_frame(http_read_fn, &ctx, &crc);
esp_http_client_close(client);
esp_http_client_cleanup(client);
if (err != ESP_OK) {
return err;
}
uint32_t previous_crc;
if (frame_config_get_last_display_crc32(&previous_crc) == ESP_OK && previous_crc == crc) {
/* Same photo already on screen (e.g. redisplayed after a reboot,
* before the refresh interval elapsed server-side) -- skip the
* physical refresh, avoiding its visible flash and 15-30s
* duration for no visual change. */
ESP_LOGI(TAG, "Frame unchanged since last display, skipping refresh");
return ESP_OK;
}
err = epd_turn_on_display();
if (err == ESP_OK) {
frame_config_set_last_display_crc32(crc);
}
return err;
}
@@ -276,35 +295,43 @@ void frame_client_run(const frame_config_t *cfg, bool force_advance)
}
}
frame_server_config_t server_cfg = fetch_frame_config(cfg->toolsserver);
uint32_t sleep_seconds = server_cfg.refresh_interval_s;
if (!server_cfg.reachable) {
/* Nothing's been drawn yet this cycle (aside from the optional
* PENDING screen above), so this is cheap diagnostics without
* compounding flashing. */
ESP_LOGW(TAG, "Tools server not reachable, retrying sooner");
sleep_seconds = CONFIG_FRAME_RETRY_INTERVAL_S;
if (have_display) {
/* The image fetch goes before the config check, not after. It has a
* far more generous timeout (CONFIG_FRAME_FETCH_TIMEOUT_MS, 15s by
* default, vs. the config check's 3s), so it comfortably absorbs the
* extra connection-setup latency that's common on the very first
* request after waking from a long deep sleep (stale ARP entries and
* the like) -- confirmed on hardware: the config check's tight
* timeout was intermittently tripping on exactly that latency while
* it went first, even though the image fetch right after it (on an
* already-warm connection) never had trouble. Trade-off: on a fully
* down server, the device now waits up to the image fetch's longer
* timeout to notice, instead of the config check's shorter one --
* worth it to stop false-failing on the common case. */
bool image_ok = true;
if (have_display) {
esp_err_t fetch_err = fetch_and_display(cfg, force_advance);
image_ok = (fetch_err == ESP_OK);
if (!image_ok) {
/* epd_display_stream() never triggers a physical refresh on a
* failed/short/wrong-size stream (see epd7in3e.c), so the
* visible screen is guaranteed untouched here -- always safe
* to show what went wrong instead of leaving stale content
* with no indication anything failed. */
ESP_LOGW(TAG, "Fetch/display failed (%s), retrying sooner", esp_err_to_name(fetch_err));
status_screen_show(cfg->sta_ssid, STATUS_OK, cfg->toolsserver, STATUS_FAILED);
}
} else {
if (first_connection && have_display) {
} else if (first_connection) {
status_screen_show(cfg->sta_ssid, STATUS_OK, cfg->toolsserver, STATUS_OK);
}
if (have_display) {
esp_err_t fetch_err = fetch_and_display(cfg, force_advance);
if (fetch_err != ESP_OK) {
/* epd_display_stream() never triggers a physical refresh on
* a failed/short/wrong-size stream (see epd7in3e.c), so the
* visible screen is guaranteed untouched here -- always
* safe to show what went wrong instead of leaving stale
* content with no indication anything failed. */
ESP_LOGW(TAG, "Fetch/display failed (%s), retrying sooner", esp_err_to_name(fetch_err));
sleep_seconds = CONFIG_FRAME_RETRY_INTERVAL_S;
status_screen_show(cfg->sta_ssid, STATUS_OK, cfg->toolsserver, STATUS_FAILED);
}
}
}
/* Only worth asking for the refresh interval if the image fetch
* actually worked -- a failed fetch already means CONFIG_FRAME_RETRY_INTERVAL_S,
* so there's nothing to gain from a config request whose result would
* just be discarded. */
uint32_t sleep_seconds = CONFIG_FRAME_RETRY_INTERVAL_S;
if (image_ok) {
frame_server_config_t server_cfg = fetch_frame_config(cfg->toolsserver);
sleep_seconds = server_cfg.reachable ? server_cfg.refresh_interval_s : CONFIG_FRAME_RETRY_INTERVAL_S;
}
if (have_display) {
+23
View File
@@ -144,6 +144,29 @@ void frame_config_clear(void)
nvs_close(handle);
}
esp_err_t frame_config_get_last_display_crc32(uint32_t *out)
{
nvs_handle_t handle;
esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle);
if (err != ESP_OK) {
return err;
}
err = nvs_get_u32(handle, "last_crc32", out);
nvs_close(handle);
return err;
}
void frame_config_set_last_display_crc32(uint32_t crc32)
{
nvs_handle_t handle;
if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle) != ESP_OK) {
return;
}
nvs_set_u32(handle, "last_crc32", crc32);
nvs_commit(handle);
nvs_close(handle);
}
static void generate_ap_password(char *out, size_t out_size)
{
size_t len = MIN(FRAME_AP_PASSWORD_LEN, out_size - 1);
+13 -1
View File
@@ -41,10 +41,22 @@ void frame_config_mark_connected_once(void);
* device falls back into provisioning on its next boot. Leaves the softAP
* identity (SSID/password) untouched, since that's tied to the device
* itself, not a particular home network -- regenerating it on every reset
* would force re-scanning the join QR code for no reason.
* would force re-scanning the join QR code for no reason. Also leaves the
* last-displayed-photo CRC (below) untouched -- it describes what's
* physically on screen, not network config, and stays valid regardless.
*/
void frame_config_clear(void);
/**
* Returns the CRC32 of the last frame actually written to the panel via a
* physical refresh. Returns ESP_ERR_NVS_NOT_FOUND if nothing's been
* displayed yet.
*/
esp_err_t frame_config_get_last_display_crc32(uint32_t *out);
/** Records the CRC32 of the frame just displayed, for next time. */
void frame_config_set_last_display_crc32(uint32_t crc32);
/**
* Returns this device's provisioning AP identity: a fixed SSID (from
* Kconfig) and a password that's generated once on first use and persisted