Files
espresso_frame/server
tfaour 5e7c47e2eb Add FastAPI server: pulls from Immich, pre-processes for the panel
Implements the server side of the architecture decided on: the ESP32-C6
has no PSRAM and a tight RAM budget, so all the heavy lifting (JPEG
decode, resize, Floyd-Steinberg dithering, 6-color quantization, 4bpp
packing) happens here instead of on-device. The frame just does a single
GET and streams the response straight to SPI.

- GET /frame/image: looks up the current cursor's asset in the configured
  Immich album, downloads its preview thumbnail, and returns it packed
  into the panel's exact 800x480/4bpp/2px-per-byte format
  (application/octet-stream, always exactly 192,000 bytes).
- GET / + POST /api/config + GET /api/albums: a small web UI for entering
  the Immich URL/API key and picking an album, rather than cramming that
  into the ESP32's captive portal form.
- Config (Immich creds, selected album, cursor) persists to a JSON file
  via a docker-compose volume mount.

Verified locally with a venv (Docker isn't available in this environment):
unit-tested image_pipeline against a synthetic image (exact byte count,
valid panel color codes only), and ran a full end-to-end pass against a
mock Immich HTTP server exercising the real /frame/image path.

Pinned dependency versions in requirements.txt after hitting a real bug
with unpinned floors: the latest starlette (1.3.1) resolved by `pip
install fastapi` breaks Jinja2Templates outright.

Not yet wired to the ESP32 side (task 6) or authenticated -- /frame/image
is unauthenticated for now, fine on a trusted LAN but worth revisiting
once the firmware sends a shared device token.
2026-07-18 14:35:14 -04:00
..

ESPresso Frame Server

Pulls photos from an Immich album, resizes/dithers/quantizes them to the E Ink Spectra 6 panel's exact 6-color format, and serves the frame a ready-to-display image once an hour. All the image processing happens here so the ESP32 never has to decode a JPEG or run a dithering algorithm itself -- it just streams the response straight to the panel.

Setup

  1. Get an Immich API key: in Immich, go to Account Settings -> API Keys -> New API Key. Read-only access to albums/assets is enough.
  2. Run the server:
    docker compose up -d
    
  3. Open http://<this-machine>:8420/ in a browser, enter your Immich URL and API key, click Load Albums, pick one, and Save.
  4. On the ESP32's captive portal setup form, set the Tools Server field to <this-machine>:8420.

Endpoints

  • GET / -- config UI
  • GET /api/albums -- lists Immich albums (used by the config UI)
  • POST /api/config -- saves Immich URL/API key/album/order
  • GET /frame/image -- returns the current photo pre-processed into the panel's raw 800x480, 4-bit-per-pixel, 2-pixels-per-byte format (application/octet-stream, exactly 192,000 bytes)
  • GET /health -- liveness check

Notes

  • Config (including the Immich API key) is stored in ./data/config.json on the host via the compose volume mount.
  • /frame/image isn't authenticated yet. That's fine on a trusted home LAN for now, but worth revisiting once the ESP32 side is wired up to send a shared device token.
  • The 6-color palette RGB values in app/image_pipeline.py are approximations, not measured values (Waveshare doesn't publish exact color primaries for this panel) -- tune them once you can compare a rendered test image against the real panel.

Local development (without Docker)

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
CONFIG_PATH=./data/config.json uvicorn app.main:app --reload --port 8420