Commit Graph
11 Commits
Author SHA1 Message Date
tfaour f74085cedf Add third button: "scan to manage" QR overlay on the current photo
Pressing the manage button (GPIO1) overlays a small QR code -- "SCAN TO
MANAGE" -- in the top-right corner of whatever photo is currently on
screen, linking to the server's config page, then reverts to the plain
photo after 30 seconds.

The overlay is spliced into the existing streaming fetch as chunks pass
through (frame_client.c's http_read_fn), rather than buffering the full
192,000-byte frame in RAM: only the small overlay rectangle itself
(~30KB) is ever held in memory, generated via new stride-parameterized
drawing helpers (epd_draw_*_ex in epd_draw.c) that let the existing
QR/text drawing code target an arbitrarily-sized buffer instead of a
full-frame one. epd7in3e.c is untouched -- it has no idea an overlay
exists.
2026-07-19 00:50:07 -04:00
tfaour b9649c35ec 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.
2026-07-18 23:51:01 -04:00
tfaour d395cf3bb9 Add two physical buttons: factory-reset and next-photo
Build and push server image / build-and-push (push) Successful in 35s
Factory-reset (GPIO3, hold 10s): clears stored WiFi/server config and
restarts into provisioning -- the deliberate, USB-free replacement for
the earlier reverted RST-based auto-reprovisioning idea.

Next-photo (GPIO2, tap): wakes the device and forces the server to
advance immediately via a new POST /frame/advance, instead of waiting
for the refresh interval. Both buttons arm themselves as deep-sleep GPIO
wakeup sources so a press is noticed promptly even while asleep.

Also makes GET /frame/image side-effect-free: it now only advances once
refresh_interval_s has elapsed since the current photo was set (tracked
server-side), so a device reboot for any reason just redisplays the
current photo instead of silently skipping ahead. The server maintains a
small reorderable upcoming-photos queue, viewable and rearrangeable from
the web UI.
2026-07-18 23:28:36 -04:00
tfaour 51fbd0657c 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).
2026-07-18 16:38:43 -04:00
tfaour 012c6dda8e Revert "Add two ways back into provisioning: RST press and repeated server failure"
This reverts commit d32236d832.
2026-07-18 16:24:38 -04:00
tfaour d32236d832 Add two ways back into provisioning: RST press and repeated server failure
RST/power-on trigger: checks esp_reset_reason() at the very top of boot.
ESP32-C6 can't electrically distinguish the RST/EN button from a genuine
power-on (both report ESP_RST_POWERON -- confirmed against ESP-IDF's own
docs, ESP_RST_EXT is explicitly "not applicable"), so POWERON is treated
as "user wants to reconfigure" and routes straight to provisioning. Safe
because the device's only normal restart path is ESP_RST_DEEPSLEEP (its
own scheduled wake), and crash-type resets (brownout/watchdog/panic)
report their own distinct reasons, not POWERON -- so a flaky power supply
or transient crash won't get bounced into provisioning, only an actual
power cycle or RST press will (which plausibly means the frame is being
moved/redeployed anyway).

Auto-fallback: a new NVS-persisted consecutive-failure counter
(frame_config_record_server_failure/reset_server_failures) tracks wakes
where the tools server was unreachable. After
CONFIG_FRAME_REPROVISION_AFTER_FAILURES in a row (default 12, ~1hr at the
retry interval), the device clears its stored WiFi config and
esp_restart()s rather than calling wifi_provisioning_start() directly --
doing that inline would mean initializing the display driver a second
time in the same session (frame_client_run already did once), the same
class of double-init bug hit earlier with WiFi. The next boot's
frame_config_load() naturally reports "not provisioned" and routes
through the existing, already-tested provisioning path with a single
fresh epd_init(). Solves the "I moved the server to a new address" case
without needing USB access.

Both frame_config_save() (fresh provisioning) and any successful server
contact reset the failure counter.
2026-07-18 16:10:03 -04:00
tfaour 21af41ff66 Fetch refresh interval from server instead of hardcoding it
Replaces check_server_reachable() (bare bool, GET /health) with
fetch_frame_config(), which GETs the server's new /frame/config endpoint
instead -- doubles as the reachability check (any completed HTTP response
counts, same as before) and delivers the server-configured
refresh_interval_s, used for the success-path deep sleep duration instead
of the Kconfig-only default.

Parses the tiny JSON response with a hand-rolled scalar extractor
(json_extract_uint) rather than pulling in a JSON library -- cJSON isn't
bundled in this ESP-IDF install, and a single flat integer field doesn't
justify a new dependency. Verified standalone against exactly the JSON
shape the server emits, including a missing-field fallback case.

FRAME_SLEEP_INTERVAL_S (Kconfig) is now just the fallback used before the
device has ever reached a configured server, or if the response is
missing/unparseable -- documented as such in its help text.
2026-07-18 15:34:37 -04:00
tfaour 878909b302 Fix status screen logic: first-connection screen, false-negative skip
Two bugs found testing against a real (partially-configured) server:

- The reachability check hit HEAD / with -- our server only registers
  GET on that route, so it always got a 405. Harmless for the check
  itself (any completed HTTP response counts as "reachable"), but noisy
  and semantically wrong. Points at GET /health instead, which exists
  for exactly this.

- fetch_and_display() failing before any pixel data was sent (e.g. a
  400/404 on /frame/image) was treated the same as a mid-stream failure,
  which skips drawing a status screen to avoid compounding flashing on
  top of an already-refreshed panel. But a pre-stream failure never
  touches the panel at all, so skipping the status screen there just
  left the old provisioning QR code on screen with no indication
  anything had gone wrong. fetch_and_display() now reports whether
  streaming ever started so the caller can tell the two cases apart.

- The status screen now always shows on the very first successful
  connection after (re)provisioning, regardless of outcome, via a new
  "connected_once" NVS flag that frame_config_save() resets on every
  fresh provisioning event. Later wakes skip it on success (straight to
  the photo) but still show it on any failure, matching the intent from
  the original status-screen feature.
2026-07-18 15:06:50 -04:00
tfaour 1ff91c8a71 Wire the real fetch -> display -> deep-sleep cycle
frame_client_run() now does what it was always meant to: probe the tools
server, GET /frame/image and stream the response straight into the panel
via epd_display_stream() (esp_http_client's manual open/fetch_headers/read
API pulls in exactly the shape epd_display_stream()'s read_fn expects, so
the ~192KB frame never sits in RAM at once), then epd_sleep() and
esp_deep_sleep_start() for an hour.

Skips the WiFi/server status checklist screen on the happy path now that
there's a real photo to show instead -- three full refreshes every single
hour (status-pending, status-final, photo) wasn't worth it once bring-up
was actually working. Still shows it (status FAILED) when the server
isn't reachable, since nothing's been drawn yet that cycle and it's the
cheapest useful diagnostic. A mid-fetch failure after the panel's already
started refreshing just logs and retries sooner, rather than compounding
with a second refresh.

New Kconfig knobs: FRAME_FETCH_TIMEOUT_MS, FRAME_SLEEP_INTERVAL_S
(default 3600s), FRAME_RETRY_INTERVAL_S (default 300s on failure).
2026-07-18 14:48:21 -04:00
tfaour a518cbdbaf Add post-connect status screen with WiFi/server checklist
After a successful home WiFi connect, frame_client_run() now redraws the
panel as a two-row checklist (WiFi row with a checkmark, server row) so
the connection sequence is visible on-device rather than only in serial
logs. Refreshes once with the server row pending, probes the tools server
with a plain HTTP HEAD (any response, even 404, confirms the socket-level
connection works -- there's no real server yet), then refreshes again with
the final result. Two refreshes rather than one to actually show staged
progress, at the cost of the extra refresh time inherent to this panel.

Also fixes a second hardware-verified bug in the same area: on a failed
STA connect falling back to provisioning, wifi_init_softap()'s
esp_wifi_init() call was aborting with ESP_ERR_INVALID_STATE, because
frame_wifi_connect_sta() only stopped the WiFi driver on failure rather
than fully deinitializing it (and destroying the STA netif) before
handing back control.
2026-07-18 14:08:19 -04:00
tfaour de449d5973 Finish captive portal: NVS config, POST handler, STA connect, AP identity
Splits provisioning (softAP + captive portal + NVS-backed config) into
wifi_provisioning.c and home-network connection into frame_client.c.
/save_config now parses the form body and persists it to NVS; on boot the
device goes straight to STA mode if a config exists, retrying a few times
before falling back to provisioning if the home network is unreachable.

The provisioning AP is now always named ESPRESSO with a random per-device
password (generated once, persisted in NVS) instead of a fixed Kconfig
value, drawn from a charset that avoids visually ambiguous characters since
it'll be read off the e-ink panel and possibly typed by hand.
2026-07-18 10:48:52 -04:00