docker-compose.yml is tracked in a repo meant for publishing, so it can't hold a real API key. Renamed it to docker-compose.yml.example (placeholder values, safe to commit) and gitignored the real docker-compose.yml -- deploying is now "cp the example, fill in real values, docker compose up", no .env file needed. config.load() now reads IMMICH_URL/IMMICH_API_KEY from the environment and applies them on top of whatever's in config.json, so setting them in the compose file's environment: block takes effect without ever touching the web UI. Env vars always win over the UI-saved values when both are present -- verified they survive a save() with different UI-entered values still in place.
3.4 KiB
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
- Get an Immich API key: in Immich, go to Account Settings -> API Keys -> New API Key. Read-only access to albums/assets is enough.
- Copy the compose file and fill in your Immich details:
Edit
cp docker-compose.yml.example docker-compose.ymldocker-compose.ymland setIMMICH_URL/IMMICH_API_KEYunderenvironment:.docker-compose.ymlis gitignored (it'll hold your real API key) --docker-compose.yml.exampleis the one that's committed. - Run the server:
docker compose up -d - Open
http://<this-machine>: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.) - On the ESP32's captive portal setup form, set the Tools Server field
to
<this-machine>:8420.
Endpoints
GET /-- config UIGET /api/albums-- lists Immich albums (used by the config UI)POST /api/config-- saves Immich URL/API key/album/orderGET /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
- Album/order/refresh-interval/etc. are stored in
./data/config.jsonon the host via the compose volume mount. Immich URL/API key are too if set via the web UI, butIMMICH_URL/IMMICH_API_KEYenv vars (see Setup above) always take precedence when present. /frame/imageisn'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.pyare 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.