From 51fbd0657c400e09ca9ee625ed156f1029937f1d Mon Sep 17 00:00:00 2001 From: Thomas Faour Date: Sat, 18 Jul 2026 16:38:43 -0400 Subject: [PATCH] Never physically refresh the panel with a short/wrong-size frame Found on hardware: the Tools Server field was pointed at Immich's own port instead of the frame server's, so /frame/image was actually hitting Immich and getting back a small error response (~10KB) instead of a 192,000-byte frame. epd_display_stream() logged a size-mismatch warning but called epd_turn_on_display() anyway, physically refreshing the panel with a buffer that was ~95% whatever was left over from before -- visible as "garbage" on screen, overwriting a previously-good image. epd_display_stream() now returns ESP_ERR_INVALID_SIZE instead of refreshing when the stream doesn't supply exactly EPD_FRAME_BYTES. Since this check happens before epd_turn_on_display() is ever called, the pixel data that *did* arrive only ever reached the panel's internal RAM over SPI, not the physically visible display, so aborting here leaves the screen exactly as it was. This also means fetch_and_display() failing now always implies the panel was never touched -- simplified frame_client_run() accordingly (dropped the now-always-true/false out-param that used to distinguish "failed before vs. during streaming", and always shows the FAILED status screen on any fetch/display error, since it's now guaranteed safe to do so). --- firmware/components/epd7in3e/epd7in3e.c | 12 +++++++++- firmware/main/frame_client.c | 30 ++++++++++--------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/firmware/components/epd7in3e/epd7in3e.c b/firmware/components/epd7in3e/epd7in3e.c index 9a4f846..0ed3596 100644 --- a/firmware/components/epd7in3e/epd7in3e.c +++ b/firmware/components/epd7in3e/epd7in3e.c @@ -234,7 +234,17 @@ esp_err_t epd_display_stream(epd_read_fn_t read_fn, void *ctx) EPD_CHECK(err); if (total != EPD_FRAME_BYTES) { - ESP_LOGW(TAG, "Stream supplied %u bytes, expected %u", (unsigned)total, (unsigned)EPD_FRAME_BYTES); + /* Whatever was received has already been clocked into the panel's + * internal RAM over SPI, but epd_turn_on_display() (the actual + * physical refresh trigger) hasn't been called yet -- returning + * here instead leaves the visible screen exactly as it was, rather + * than refreshing onto a mostly-garbage buffer. Confirmed on + * hardware: a misdirected fetch that returned a ~10KB error page + * instead of a 192,000-byte frame still triggered a refresh before + * this check existed, painting garbage over a previously-good image. */ + ESP_LOGE(TAG, "Stream supplied %u bytes, expected %u -- aborting refresh", + (unsigned)total, (unsigned)EPD_FRAME_BYTES); + return ESP_ERR_INVALID_SIZE; } return epd_turn_on_display(); diff --git a/firmware/main/frame_client.c b/firmware/main/frame_client.c index 244282d..9d0e77a 100644 --- a/firmware/main/frame_client.c +++ b/firmware/main/frame_client.c @@ -213,13 +213,12 @@ static size_t http_read_fn(uint8_t *chunk, size_t chunk_size, void *ctx_) } /* GETs /frame/image and streams the response directly into the panel. - * *out_started_stream tells the caller whether the panel was actually - * touched -- false means the failure happened before any pixel data was - * sent (bad status, connection error), so nothing was drawn this cycle. */ -static esp_err_t fetch_and_display(const frame_config_t *cfg, bool *out_started_stream) + * Returning non-ESP_OK means the panel was never actually refreshed -- + * epd_display_stream() (see epd7in3e.c) refuses to trigger a physical + * refresh on a short/wrong-size stream, so a failure here always leaves + * the visible screen exactly as it was. */ +static esp_err_t fetch_and_display(const frame_config_t *cfg) { - *out_started_stream = false; - char url[160]; snprintf(url, sizeof(url), "http://%s/frame/image", cfg->toolsserver); @@ -247,7 +246,6 @@ static esp_err_t fetch_and_display(const frame_config_t *cfg, bool *out_started_ } ESP_LOGI(TAG, "Fetching frame (%d bytes) from '%s'", content_length, url); - *out_started_stream = true; http_read_ctx_t ctx = { .client = client }; err = epd_display_stream(http_read_fn, &ctx); @@ -294,20 +292,16 @@ void frame_client_run(const frame_config_t *cfg) status_screen_show(cfg->sta_ssid, STATUS_OK, cfg->toolsserver, STATUS_OK); } if (have_display) { - bool stream_started = false; - esp_err_t fetch_err = fetch_and_display(cfg, &stream_started); + esp_err_t fetch_err = fetch_and_display(cfg); 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; - if (!stream_started) { - /* The panel was never touched this cycle (e.g. a bad - * HTTP status before any pixel data was sent) -- show - * the failure instead of leaving whatever was on - * screen before. A mid-stream failure, by contrast, - * already refreshed the panel with something, so - * skip compounding it with a second refresh here. */ - status_screen_show(cfg->sta_ssid, STATUS_OK, cfg->toolsserver, STATUS_FAILED); - } + status_screen_show(cfg->sta_ssid, STATUS_OK, cfg->toolsserver, STATUS_FAILED); } } }