Files
espresso_frame/firmware
tfaour d395cf3bb9
Build and push server image / build-and-push (push) Successful in 35s
Add two physical buttons: factory-reset and next-photo
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
..

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, streams it straight to the panel over SPI, and goes back to deep sleep.

See docs/architecture.md for the full boot/fetch cycle and docs/hardware.md for wiring.

Build and flash

Requires ESP-IDF (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 pins an 8MB flash size and a custom 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_RESET_BUTTON_GPIO 3 Factory-reset button GPIO (-1 to disable). Must be 0-7 (ESP32-C6's deep-sleep-wakeup-capable pins)
FRAME_RESET_BUTTON_HOLD_MS 10000 How long the button must be held to trigger a reset
FRAME_NEXT_BUTTON_GPIO 2 Next-photo button GPIO (-1 to disable). Must be 0-7

Under E-Paper Display (epd7in3e) Configuration: SPI/GPIO pin assignments and SPI clock speed -- see 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) 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 -- not your Immich server). Saving reboots the device, which then connects to your home network and starts its normal fetch/sleep cycle.

Skipping to the next photo

Wire a momentary push button between GPIO2 and GND (internal pull-up, active-low, same wiring style as the reset button). A press wakes the device (if asleep) and tells the server to advance to the next photo right away, regardless of the configured refresh interval -- no long hold needed, unlike the factory-reset button, since advancing is easily reversible by pressing again. See FRAME_NEXT_BUTTON_GPIO above to change the pin or disable the feature.

Normal wakes and reboots never advance the photo on their own -- the server decides when to advance based on its own clock (see server/README.md), so an unplanned reboot just redisplays whatever was already showing instead of skipping ahead.

Resetting to provisioning mode

Wire a momentary push button between GPIO3 and GND (internal pull-up, active-low -- no external resistor needed). Hold it for 10 seconds (from either power-on or while the device is deep-asleep -- GPIO3 is armed as a wakeup source) and it clears the stored WiFi/server config and restarts into provisioning. Releasing it early is a no-op; nothing happens until the full hold duration elapses, so a brief accidental bump won't reprovision the device. See FRAME_RESET_BUTTON_GPIO/ FRAME_RESET_BUTTON_HOLD_MS above to change the pin or hold duration, or disable the feature.

Without the button wired up (or with FRAME_RESET_BUTTON_GPIO set to -1), reconfiguring still works by erasing the NVS partition over USB:

python -m esptool --chip esp32c6 -p PORT erase-region 0x9000 0x6000

(Offset/size match the nvs entry in partitions.csv; this only wipes the config, not the app itself.)