Build and push server image / build-and-push (push) Successful in 35s
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.
119 lines
5.5 KiB
Markdown
119 lines
5.5 KiB
Markdown
# 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_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`](../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.
|
|
|
|
## 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`](../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`](partitions.csv);
|
|
this only wipes the config, not the app itself.)
|