Commit Graph
12 Commits
Author SHA1 Message Date
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 ecc42f23c0 Redesign setup screen: title + two-step layout with a config QR
Adds a "2. CONFIGURATION" step alongside WiFi setup: a second QR code
linking straight to the captive portal page (http://<ap-ip>/), for anyone
who's joined the AP but wants a one-scan shortcut to the config form
instead of relying on the captive-portal popup. The AP netif is now
created (but not started) before the display renders, since the AP's IP
is fixed at netif creation and needed for this QR before the network is
actually up.

Pulls the pixel/text drawing primitives (previously private to
qr_onboarding.c) out into epd_draw.c/.h so the upcoming status screen can
reuse them instead of duplicating.
2026-07-18 14:07:56 -04:00
tfaour 1dd02da70a Add QR-code WiFi onboarding screen
Vendors two small MIT/BSD-3-Clause libraries rather than hand-rolling
either: Nayuki's qrcodegen (QR matrix generation) and Waveshare's Font24
bitmap table from their e-Paper repo (same repo the epd7in3e driver came
from) for rendering readable text on the panel.

qr_onboarding_show() builds a standard WIFI:T:WPA;S:...;P:...;; payload,
rasterizes the QR module matrix plus the SSID and password as plaintext
underneath (for anyone provisioning from a device that can't scan a QR)
onto a malloc'd frame buffer, and pushes it to the panel via
epd_display_buffer(). The buffer is heap-allocated on demand rather than
statically reserved, since 192KB held permanently in BSS would eat into
the RAM budget the HTTP fetch path (task 6) is specifically trying to keep
free.

Wired into wifi_provisioning_start() before the softAP comes up, so the
join instructions are already on-screen by the time the network is
joinable.
2026-07-18 11:20:43 -04:00
tfaour 85a5238724 Add epd7in3e display driver component
Ports Waveshare's official EPD_7in3e.c register/refresh sequence (the
panel has no public datasheet, so their reference driver is the source of
truth) to an ESP-IDF component using spi_master + gpio instead of the
bcm2835/RPi hardware abstraction the reference targets.

Unlike the reference driver, which toggles CS around every single byte,
this holds CS low for each logical command/data phase and DMAs pixel data
in 4KB chunks -- sending ~192,000 individual one-byte SPI transactions
would make a full refresh impractically slow.

Exposes a streaming API (epd_display_stream, pulling chunks from a
caller-supplied read_fn) so the eventual HTTP fetch path can feed the panel
without holding a full ~192KB frame in RAM, plus an in-memory convenience
wrapper (epd_display_buffer) for cases like the upcoming QR code screen
where buffering the whole frame is fine.

Wired into wifi_provisioning_start() as an init + white-clear for now, to
prove the driver builds/links/runs at the right point in the boot sequence
ahead of the actual QR code content.
2026-07-18 11:05:06 -04:00
tfaour 87c2eccfdf Suffix provisioning AP SSID with device MAC to avoid collisions
Two frames on the same network would otherwise both advertise the same
ESPRESSO SSID during provisioning. Appends the last 3 bytes of the WiFi MAC
(read via esp_read_mac(), available before the WiFi driver starts) so each
device's softAP is uniquely named.
2026-07-18 10:52:56 -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
tfaour cb50c99c98 Restructure repo as monorepo: firmware/ + server/ + docs/
Moves the existing ESP-IDF captive_portal example into firmware/ to make
room for the new FastAPI server and project docs, ahead of building out the
full ESPresso Frame project (ESP32-C6 + Immich-backed e-ink photo frame).
2026-07-18 10:20:25 -04:00