# ESPresso Frame Server Pulls photos from an [Immich](https://immich.app) 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. **Copy the compose file and fill in your Immich details**: ``` cp docker-compose.yml.example docker-compose.yml ``` Edit `docker-compose.yml` and set `IMMICH_URL`/`IMMICH_API_KEY` under `environment:`. `docker-compose.yml` is gitignored (it'll hold your real API key) -- `docker-compose.yml.example` is the one that's committed. 3. **Run the server**: ``` docker compose up -d ``` 4. Open `http://:8420/` in a browser, click **Load Albums**, pick one, and **Save**. (The Immich URL/API key fields will already be populated from the environment; changing them in the UI has no effect as long as the env vars are set -- they win on every load.) 5. On the ESP32's captive portal setup form, set the **Tools Server** field to `:8420`. ## Endpoints - `GET /` -- config UI (album, order, refresh interval, face-aware crop toggle, now-displaying + reorderable upcoming photos -- not Immich URL/API key, see Setup above) - `GET /api/albums` -- lists Immich albums (used by the config UI) - `POST /api/config` -- saves album/order/refresh_interval_s/smart_crop_faces - `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). **Side-effect-free** by default: it only actually advances to the next photo once `refresh_interval_s` has elapsed since the current one was set, so calling it repeatedly (e.g. the device rebooting unexpectedly) just redisplays the same photo instead of skipping ahead. - `POST /frame/advance` -- forces an immediate advance to the next photo, ignoring `refresh_interval_s`, and resets the interval clock from now. Same response shape as `/frame/image`. Used by the device's next-photo button (see `firmware/README.md`). - `GET /frame/config` -- `{"refresh_interval_s": ...}`, polled by the frame each wake alongside its reachability check - `GET /api/queue` -- `{"current": {...} | null, "upcoming": [...]}`, each entry an asset id + thumbnail URL; used by the config UI - `POST /api/queue/reorder` -- reorders the upcoming queue; body is `{"queue": [asset_id, ...]}`, must be exactly a permutation of the current queue - `GET /api/photo-thumbnail/{asset_id}` -- proxies an Immich thumbnail so the browser never needs the Immich API key directly - `GET /health` -- liveness check ## Notes - Album/order/refresh-interval/current photo/upcoming queue/etc. are stored in `./data/config.json` on the host via the compose volume mount. Immich URL/API key are too if set via the web UI, but `IMMICH_URL`/`IMMICH_API_KEY` env vars (see Setup above) always take precedence when present. - The upcoming queue is a bounded lookahead (10 photos), not the whole album -- it's topped up automatically as photos are consumed (`app/photo_queue.py`), in sequential or shuffle order per the Order setting. Reordering only rearranges those 10; it doesn't add or remove photos from the album. - `/frame/image` and `/frame/advance` aren'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. ## Deploying a pre-built image Every push to `main` that touches `server/` triggers a Gitea Actions workflow (`.gitea/workflows/server-docker-build.yml`) that builds this image and pushes it to this repo's Gitea Container Registry at `git.thumeit.com/tfaour/espresso-frame-server`. `docker-compose.yml` (copied from `docker-compose.yml.example`, see Setup above) already points at that image, so a deploy host doesn't need this repo's build context at all -- just the compose file: ``` docker compose pull docker compose up -d ``` `docker compose build` (or `up --build`) still works too, for local iteration against your own Dockerfile changes. ## 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 --host 0.0.0.0 --port 8420 ``` `--host 0.0.0.0` matters here: without it, uvicorn defaults to `127.0.0.1` (localhost-only), which the ESP32 can't reach over the LAN. The Docker image already binds `0.0.0.0` by default.