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).
This commit is contained in:
2026-07-18 16:38:43 -04:00
parent 55d4b28f37
commit 51fbd0657c
2 changed files with 23 additions and 19 deletions
+11 -1
View File
@@ -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();
+11 -17
View File
@@ -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,23 +292,19 @@ 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);
}
}
}
}
if (have_display) {
epd_sleep();