# ESPresso Frame Firmware ESP-IDF firmware for the ESP32-C6. On first boot it provisions itself over a WiFi captive portal; after that it wakes on a timer, fetches an already-processed frame from the [server](../server/), streams it straight to the panel over SPI, and goes back to deep sleep. See [`docs/architecture.md`](../docs/architecture.md) for the full boot/fetch cycle and [`docs/hardware.md`](../docs/hardware.md) for wiring. ## Build and flash Requires [ESP-IDF](https://docs.espressif.com/projects/esp-idf/en/stable/esp32c6/get-started/) (developed against v5.x/v6.x) with the environment sourced (`. $IDF_PATH/export.sh` or your distro's equivalent). ``` idf.py set-target esp32c6 idf.py build idf.py -p PORT flash monitor ``` (`Ctrl-]` exits the monitor.) The committed [`sdkconfig.defaults`](sdkconfig.defaults) pins an 8MB flash size and a custom [`partitions.csv`](partitions.csv) (2MB app partition -- the default "single app" ~1MB partition runs out of room once the HTTP client, TLS, and vendored fonts/QR library are linked in). If your board has less flash, you'll need to shrink the app partition and drop features to fit. ## Configuration (`idf.py menuconfig`) Under **ESPresso Frame Configuration**: | Option | Default | What it does | | --- | --- | --- | | `ESP_AP_SSID` | `ESPRESSO` | Provisioning softAP SSID prefix (device appends `_XXXXXX` from its MAC) | | `ESP_MAX_STA_CONN` | 4 | Max clients on the provisioning softAP | | `ESP_ENABLE_DHCP_CAPTIVEPORTAL` | on | DHCP Option 114 captive portal detection | | `FRAME_STA_CONNECT_MAX_RETRIES` | 3 | Home WiFi connect attempts before falling back to provisioning | | `FRAME_STA_CONNECT_TIMEOUT_MS` | 15000 | Per-attempt WiFi connect timeout | | `FRAME_SERVER_CHECK_TIMEOUT_MS` | 3000 | Timeout for `GET /frame/config` (reachability check + refresh interval) | | `FRAME_FETCH_TIMEOUT_MS` | 15000 | Timeout for `GET /frame/image` | | `FRAME_SLEEP_INTERVAL_S` | 3600 | **Fallback only** -- the refresh interval is normally set server-side; see below | | `FRAME_RETRY_INTERVAL_S` | 300 | Sleep duration after a failed cycle, before retrying | | `FRAME_NEXT_BUTTON_GPIO` | 2 | Next-photo button GPIO (-1 to disable). Must be 0-7 (ESP32-C6's deep-sleep-wakeup-capable pins) | | `FRAME_BACK_BUTTON_GPIO` | 0 | Back-photo button GPIO (-1 to disable). Must be 0-7 | | `FRAME_COMBO_BUTTON_GPIO` | 1 | Menu/reset button GPIO (-1 to disable). Must be 0-7 | | `FRAME_COMBO_SOFT_RESET_HOLD_MS` | 3000 | How long the combo button must be held (then released) to soft-reset | | `FRAME_COMBO_FACTORY_RESET_HOLD_MS` | 15000 | How long the combo button must be held to factory-reset | Under **E-Paper Display (epd7in3e) Configuration**: SPI/GPIO pin assignments and SPI clock speed -- see [`docs/hardware.md`](../docs/hardware.md) for the wiring these correspond to. ### Why `FRAME_SLEEP_INTERVAL_S` says "fallback" The actual refresh interval is set from the server's web UI (see [`server/README.md`](../server/README.md)) and delivered to the device on every wake via `GET /frame/config`, so it can be changed without reflashing. The Kconfig value only applies before the device has ever successfully reached a configured server, or if the response doesn't include a valid interval. ## First boot With no stored WiFi config (a fresh device, or after erasing NVS -- see below), the device brings up the display before anything else and shows a two-step setup screen: 1. **Connect to WiFi** -- a QR code encoding `WIFI:T:WPA;S:...;P:...;;` for the device's own `ESPRESSO_XXXXXX` softAP, with the SSID and password also printed underneath for anyone provisioning from a desktop/laptop that can't scan a QR code. 2. **Configure device** -- a QR code linking straight to the captive portal's config page (`http://192.168.4.1/` by default), for a one-scan shortcut once you've joined the AP. The config page asks for your home WiFi SSID/password, the "Tools Server" address (`host:port` of the [server](../server/) -- **not** your Immich server; see below for the `https://` form), and an optional "Access Token" (see below). Saving reboots the device, which then connects to your home network and starts its normal fetch/sleep cycle. ## HTTP vs HTTPS The Tools Server field accepts either: - `host:port` (e.g. `192.168.1.50:8420`) -- plain HTTP, talks straight to the [server](../server/), which never speaks TLS itself. This is the default and needs nothing extra. - `https://host[:port]` (e.g. `https://frame.example.com`) -- HTTPS, for a TLS-terminating reverse proxy (nginx, etc.) sitting in front of the server. Every URL the device builds (image fetch, config check, manage-menu overlay data, the QR codes' own links) uses whichever scheme you enter. The firmware does **not** use ESP-IDF's general public CA bundle -- it pins one specific certificate, embedded at build time from [`main/certs/tools_server_ca.pem`](main/certs/tools_server_ca.pem) and trusted directly via `cert_pem` on every request. (The public bundle was tried first and rejected: it does an exact byte-level match against its compiled-in table, and a real-world root that's been re-issued under a new serial/signature but the same name and key -- as Google did for GTS Root R4 -- doesn't match it, confirmed on hardware.) This means a normal publicly-trusted certificate (Let's Encrypt, a Cloudflare-proxied hostname, etc.) does **not** automatically work -- only whichever certificate is actually embedded in `certs/tools_server_ca.pem` is trusted. To point the device at a different reverse proxy, extract that proxy's actual certificate and replace the file's contents: ``` openssl s_client -connect :443 -showcerts