# 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 | 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 and the "Tools Server" address (`host:port` of the [server](../server/) -- **not** your Immich server). Saving reboots the device, which then connects to your home network and starts its normal fetch/sleep cycle. ## Resetting to provisioning mode There's currently no in-field way to force the device back into provisioning (a future addition) -- reconfiguring means erasing its NVS partition over USB: ``` python -m esptool --chip esp32c6 -p PORT erase-region 0x9000 0x6000 ``` (Offset/size match the `nvs` entry in [`partitions.csv`](partitions.csv); this only wipes the config, not the app itself.)