Build and push server image / build-and-push (push) Successful in 32s
Two rounds of follow-up work on the manage-button overlay:
1. Location formatting: US/Canada now show abbreviated state/province
("CA", "ON") instead of the full name, other countries show the full
country name, and each is its own line (was one line, now wraps to
two) so longer international place names have more room without
threatening to overlap the top-right QR box. The bottom-left share QR
also gets a "SCAN TO DOWNLOAD" caption.
2. Escalating menu: pressing the manage button again while its overlay
is already up adds a second level -- each Immich-identified person's
name labeled next to their face in the photo (using Immich's own
face recognition/People data, no detection/recognition added to this
project). A third press exits immediately instead of waiting out the
30s auto-revert timer. No new Immich API needed -- GET /api/faces
already embeds a nullable person.name per face; new
server/app/face_labels.py maps a named face's box into the final
800x480 frame's pixel space (reusing crop-box math extracted from
image_pipeline.py's face-aware cropping). Capped at 4 named faces,
sized to a real firmware RAM budget: each label is its own malloc'd
overlay region on the device, alongside the 4 fixed corner regions
already in use. New GET /frame/face-labels returns a flattened
fixed-slot JSON shape (not a real array) so firmware's existing
flat-scalar parser can read it without needing an actual array
parser. No persistent state needed for the escalation itself -- it's
all local control flow within one continuous awake session
(frame_client.c's run_management_menu()).
151 lines
7.2 KiB
Markdown
151 lines
7.2 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 |
|
|
| `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`](../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.
|
|
|
|
## 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 several
|
|
corners of whatever photo is currently showing, leaving the middle of
|
|
the photo visible and unchanged:
|
|
|
|
- **Top-right**: a QR code -- "SCAN TO MANAGE" -- linking to the
|
|
server's config page.
|
|
- **Top-left**: the photo's location, if Immich reverse-geocoded it from
|
|
GPS EXIF (skipped entirely if not).
|
|
- **Bottom-right**: the date the photo was taken, if known.
|
|
- **Bottom-left**: a QR code linking to a public, view-only Immich share
|
|
link for that exact photo. The link is only created once someone
|
|
actually scans it, and expires 30 minutes after that.
|
|
|
|
After 30 seconds with no further presses it automatically reverts to the
|
|
plain photo. Pressing the button *again* while this is up escalates to a
|
|
second menu level -- everything above, plus the name of anyone Immich
|
|
has identified labeled right next to their face in the photo (skipped
|
|
for faces Immich hasn't been told a name for; no face detection happens
|
|
on the device or the server, this is entirely Immich's own People
|
|
feature). A third press exits immediately rather than waiting out the
|
|
30-second timer. See `FRAME_MANAGE_BUTTON_GPIO` above to change the pin
|
|
or disable the feature.
|
|
|
|
The device stays awake the whole time (up to three physical refreshes:
|
|
the base overlay, the escalated one, and reverting), 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`](partitions.csv);
|
|
this only wipes the config, not the app itself.)
|