Files
espresso_frame/firmware
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
..

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
FRAME_MANAGE_BUTTON_GPIO 1 "Scan to manage" 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.

Scanning to manage the queue

Wire a momentary push button between GPIO1 and GND (same wiring style as the other two buttons). A press wakes the device and overlays a small QR code -- "SCAN TO MANAGE" -- in the top-right corner of whatever photo is currently showing, linking to the server's config page. The rest of the photo stays visible and unchanged. After 30 seconds it automatically reverts to the plain photo. See FRAME_MANAGE_BUTTON_GPIO above to change the pin or disable the feature.

The device stays awake for the full 30 seconds (two physical refreshes, one for the overlay and one to revert), so this costs meaningfully more power than a normal wake -- expected for a deliberate, occasional action, same tradeoff as the other two buttons.

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.)