Fix stale documentation found by a doc-accuracy audit

firmware/README.md's HTTP vs HTTPS section still described the
public-CA-bundle trust approach that was tried and abandoned in favor
of pinning one specific certificate -- rewritten to match what's
actually there. docs/architecture.md was missing the back-photo button
entirely (sequence diagram and boot-flow bullets only covered next) and
still said the system talks "over plain HTTP" despite HTTPS support.
docker-compose.yml.example's MANAGEMENT_TOKEN comment understated its
scope (said "the web UI", omitting that every /frame/* endpoint is
gated too).
This commit is contained in:
2026-07-19 15:20:12 -04:00
parent aef8a10029
commit d5de882b1e
3 changed files with 48 additions and 25 deletions
+18 -10
View File
@@ -1,8 +1,10 @@
# Architecture
Two independent pieces talk over plain HTTP on the local network: the
ESP32-C6 firmware, and a small FastAPI server that sits between it and
Immich.
Two independent pieces talk over HTTP or HTTPS (the server itself always
speaks plain HTTP; HTTPS means a reverse proxy in front of it, see
[`firmware/README.md`](../firmware/README.md#http-vs-https)) on the local
network: the ESP32-C6 firmware, and a small FastAPI server that sits
between it and Immich.
```mermaid
sequenceDiagram
@@ -16,11 +18,14 @@ sequenceDiagram
Note over Frame: User scans WiFi QR, then config QR -> fills in<br/>home WiFi + "Tools Server" host:port
Frame->>Frame: Save config to NVS, reboot
Note over Frame: Every wake (deep sleep timer, next-photo button,<br/>or any other reboot)
Note over Frame: Every wake (deep sleep timer, next/back-photo button,<br/>or any other reboot)
Frame->>Frame: Connect to home WiFi
alt next-photo button pressed
Frame->>Server: POST /frame/advance
Server->>Server: Force-advance to next queued photo, reset interval clock
else back-photo button pressed
Frame->>Server: POST /frame/back
Server->>Server: Return to previously-current photo (bounded history),<br/>reset interval clock
else normal wake
Frame->>Server: GET /frame/image
Server->>Server: Advance only if refresh_interval_s has elapsed<br/>since the current photo was set -- otherwise a no-op
@@ -52,12 +57,15 @@ sequenceDiagram
2. **Stored config exists**: connect to the saved WiFi network (a few
retries before falling back to provisioning if it fails), then run the
fetch cycle in `frame_client.c`:
- Check the next-photo button (`next_button_check()`) -- if it was
what woke the device (checked via the latched
`esp_sleep_get_gpio_wakeup_status()`, not a live pin read, since a
quick tap can release before boot gets around to polling it) or is
currently held, the fetch below hits `POST /frame/advance` instead
of `GET /frame/image`, forcing the server to skip ahead immediately.
- Check the next-photo and back-photo buttons (`next_button_check()`,
`back_button_check()`) -- if either was what woke the device
(checked via the latched `esp_sleep_get_gpio_wakeup_status()`, not
a live pin read, since a quick tap can release before boot gets
around to polling it) or is currently held, the fetch below hits
`POST /frame/advance` or `POST /frame/back` instead of
`GET /frame/image`, forcing the server to move in that direction
immediately (next takes priority if somehow both read pressed at
once).
- Fetch the frame and write it into the panel's SPI buffer
(`epd_write_frame()`), computing a CRC32 as it streams -- never
buffering the full ~192KB frame in RAM. The panel driver refuses to
+24 -11
View File
@@ -96,17 +96,30 @@ The Tools Server field accepts either:
manage-menu overlay data, the QR codes' own links) uses whichever
scheme you enter.
The firmware trusts the standard public CA bundle ESP-IDF ships
(`esp_crt_bundle_attach`, the same root store a browser trusts) -- so
any reverse proxy with a normal publicly-trusted certificate just
works: Let's Encrypt, a Cloudflare-proxied hostname (Cloudflare's own
edge certificate, issued by Google Trust Services or similar -- **not**
Cloudflare's Origin CA cert, which only ever sits on the Cloudflare-to-
origin leg and is never presented to a public client, ESP32 or browser
alike), or any other public CA. If your proxy uses a private/self-signed
cert instead (no public CA in the chain at all), the public bundle won't
trust it -- that's not supported today, would need switching back to
embedding that specific cert.
The firmware does **not** use ESP-IDF's general public CA bundle --
it pins one specific certificate, embedded at build time from
[`main/certs/tools_server_ca.pem`](main/certs/tools_server_ca.pem) and
trusted directly via `cert_pem` on every request. (The public bundle
was tried first and rejected: it does an exact byte-level match against
its compiled-in table, and a real-world root that's been re-issued
under a new serial/signature but the same name and key -- as Google did
for GTS Root R4 -- doesn't match it, confirmed on hardware.)
This means a normal publicly-trusted certificate (Let's Encrypt, a
Cloudflare-proxied hostname, etc.) does **not** automatically work --
only whichever certificate is actually embedded in
`certs/tools_server_ca.pem` is trusted. To point the device at a
different reverse proxy, extract that proxy's actual certificate and
replace the file's contents:
```
openssl s_client -connect <host>:443 -showcerts </dev/null
```
Pick whichever certificate in the printed chain you want as the trust
anchor (typically the root) and rebuild. A private/self-signed cert
works exactly the same way -- there's no requirement that it chain to
a public CA at all, since the device trusts this one file directly.
The device does perform normal hostname verification (it's not
skipped), so the Tools Server field's hostname has to match what the
+6 -4
View File
@@ -10,9 +10,11 @@ services:
- CONFIG_PATH=/data/config.json
- IMMICH_URL=http://your-immich-host:2283
- IMMICH_API_KEY=your-immich-api-key-here
# Optional: gates the web UI (/, /api/*) behind this shared secret --
# leave unset to keep it open on a trusted LAN, same as before. Paste
# the same value into the ESP32's captive portal setup form (Access
# Token field) so its manage-menu QR code embeds it automatically.
# Optional: gates the entire server -- the web UI (/, /api/*) AND
# every device-facing /frame/* endpoint -- behind this shared secret.
# Leave unset to keep it all open on a trusted LAN, same as before.
# Paste the same value into the ESP32's captive portal setup form
# (Access Token field) so it's sent on every device request and gets
# embedded automatically in the manage-menu/share QR codes.
- MANAGEMENT_TOKEN=changeme
restart: unless-stopped