Update remaining docs for the second panel/board (missed in the previous commit)
docs/hardware.md and firmware/README.md were updated already; this catches the root README, docs/architecture.md, docs/widgets.md, and server/README.md -- all still described the project as single-panel/ single-chip (800x480, ESP32-C6 only) even after image_pipeline.py stopped hardcoding that.
This commit is contained in:
@@ -1,9 +1,12 @@
|
|||||||
# ESPresso Frame
|
# ESPresso Frame
|
||||||
|
|
||||||
A DIY e-ink photo frame: an ESP32-C6 pulls photos from your
|
A DIY e-ink photo frame: an ESP32 board pulls photos from your
|
||||||
[Immich](https://immich.app) library and displays them on a 7.3" full-color
|
[Immich](https://immich.app) library and displays them on a full-color
|
||||||
e-paper panel, waking on a timer to refresh and spending the rest of its
|
e-paper panel, waking on a timer to refresh and spending the rest of its
|
||||||
time in deep sleep.
|
time in deep sleep. The original build is a 7.3" panel on an ESP32-C6;
|
||||||
|
a larger 13.3" panel on Seeed's EE02 (ESP32-S3) is supported
|
||||||
|
server-side, but its firmware driver isn't working yet -- see
|
||||||
|
[`docs/hardware.md`](docs/hardware.md).
|
||||||
|
|
||||||
- **No cables to a computer, no SD card shuffling.** Provisioning is a
|
- **No cables to a computer, no SD card shuffling.** Provisioning is a
|
||||||
captive portal with a QR code drawn on the panel itself -- scan, join,
|
captive portal with a QR code drawn on the panel itself -- scan, join,
|
||||||
@@ -12,7 +15,9 @@ time in deep sleep.
|
|||||||
all the work (pulling from Immich, cropping, dithering, packing into
|
all the work (pulling from Immich, cropping, dithering, packing into
|
||||||
the panel's exact pixel format) and hands the device a stream it can
|
the panel's exact pixel format) and hands the device a stream it can
|
||||||
write straight to SPI. The ESP32-C6 has no PSRAM and not much SRAM to
|
write straight to SPI. The ESP32-C6 has no PSRAM and not much SRAM to
|
||||||
spare -- keeping it a dumb display client is what makes that workable.
|
spare -- keeping it a dumb display client is what makes that workable
|
||||||
|
(the same design carries over to the ESP32-S3 board even though it
|
||||||
|
does have PSRAM, for consistency).
|
||||||
- **Crops toward faces, not just the center**, using face bounding boxes
|
- **Crops toward faces, not just the center**, using face bounding boxes
|
||||||
Immich already computed for its own People feature -- no bundled face
|
Immich already computed for its own People feature -- no bundled face
|
||||||
detector.
|
detector.
|
||||||
@@ -21,8 +26,13 @@ time in deep sleep.
|
|||||||
|
|
||||||
## Hardware
|
## Hardware
|
||||||
|
|
||||||
- ESP32-C6 dev board (8MB flash)
|
- ESP32-C6 dev board (8MB flash), or Seeed's XIAO ESP32-C6 (production
|
||||||
- [Waveshare 7.3" E Ink Spectra 6 (E6)](https://www.waveshare.com/7.3inch-e-paper-hat-e.htm) panel -- 800x480, 6-color, SPI
|
board) -- both drive the panel below.
|
||||||
|
- [Waveshare 7.3" E Ink Spectra 6 (E6)](https://www.waveshare.com/7.3inch-e-paper-hat-e.htm) panel -- 800x480, 6-color, SPI.
|
||||||
|
- Experimental, not yet working: [Waveshare 13.3" E Ink Spectra 6](https://www.waveshare.com/13.3inch-e-paper-hat-plus-e.htm)
|
||||||
|
(1600x1200) on [Seeed's EE02](https://www.seeedstudio.com/XIAO-ePaper-DIY-Kit-EE02-for-13-3-Spectratm-6-E-Ink.html)
|
||||||
|
(ESP32-S3) -- server-side support exists, but the firmware driver's
|
||||||
|
panel init sequence isn't ported from vendor code yet.
|
||||||
|
|
||||||
See [`docs/hardware.md`](docs/hardware.md) for wiring,
|
See [`docs/hardware.md`](docs/hardware.md) for wiring,
|
||||||
[`docs/architecture.md`](docs/architecture.md) for how the two halves talk
|
[`docs/architecture.md`](docs/architecture.md) for how the two halves talk
|
||||||
@@ -34,14 +44,14 @@ placeable photos/calendar/whiteboard widget system.
|
|||||||
1. **[`server/`](server/)** -- run the FastAPI server first (Docker
|
1. **[`server/`](server/)** -- run the FastAPI server first (Docker
|
||||||
Compose, points at your Immich instance). See
|
Compose, points at your Immich instance). See
|
||||||
[`server/README.md`](server/README.md).
|
[`server/README.md`](server/README.md).
|
||||||
2. **[`firmware/`](firmware/)** -- build and flash the ESP32-C6, then
|
2. **[`firmware/`](firmware/)** -- build and flash the board, then
|
||||||
scan the QR codes it draws on first boot to provision it. See
|
scan the QR codes it draws on first boot to provision it. See
|
||||||
[`firmware/README.md`](firmware/README.md).
|
[`firmware/README.md`](firmware/README.md).
|
||||||
|
|
||||||
## Repo layout
|
## Repo layout
|
||||||
|
|
||||||
```
|
```
|
||||||
firmware/ ESP-IDF project for the ESP32-C6
|
firmware/ ESP-IDF project (ESP32-C6 devkit/xiao boards, ESP32-S3 ee02)
|
||||||
server/ FastAPI server: Immich -> crop/dither/pack -> the frame
|
server/ FastAPI server: Immich -> crop/dither/pack -> the frame
|
||||||
docs/ Wiring and architecture notes
|
docs/ Wiring and architecture notes
|
||||||
```
|
```
|
||||||
|
|||||||
+11
-7
@@ -3,14 +3,15 @@
|
|||||||
Two independent pieces talk over HTTP or HTTPS (the server itself always
|
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
|
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
|
[`firmware/README.md`](../firmware/README.md#http-vs-https)) on the local
|
||||||
network: the ESP32-C6 firmware, and a small FastAPI server that sits
|
network: the ESP32 firmware (ESP32-C6 for the devkit/xiao boards,
|
||||||
between it and Immich.
|
ESP32-S3 for ee02 -- see [`docs/hardware.md`](hardware.md)), and a small
|
||||||
|
FastAPI server that sits between it and Immich.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant Immich
|
participant Immich
|
||||||
participant Server as ESPresso Frame Server
|
participant Server as ESPresso Frame Server
|
||||||
participant Frame as ESP32-C6 Frame
|
participant Frame as ESP32 Frame
|
||||||
|
|
||||||
Note over Frame: First boot / never provisioned
|
Note over Frame: First boot / never provisioned
|
||||||
Frame->>Frame: Generate AP SSID/password, draw QR + config QR on panel
|
Frame->>Frame: Generate AP SSID/password, draw QR + config QR on panel
|
||||||
@@ -33,7 +34,7 @@ sequenceDiagram
|
|||||||
Server->>Immich: List album assets / download preview / faces<br/>(once per photo widget on the panel)
|
Server->>Immich: List album assets / download preview / faces<br/>(once per photo widget on the panel)
|
||||||
Immich-->>Server: JPEG + face bounding boxes
|
Immich-->>Server: JPEG + face bounding boxes
|
||||||
Server->>Server: Composite every widget's region onto one canvas,<br/>then enhance/overlay/quantize (dither)/pack 4bpp once
|
Server->>Server: Composite every widget's region onto one canvas,<br/>then enhance/overlay/quantize (dither)/pack 4bpp once
|
||||||
Server-->>Frame: 192,000 raw bytes, streamed
|
Server-->>Frame: packed 4bpp bytes, streamed<br/>(192,000 for the 7.3" panel; sized to whichever<br/>panel this frame's device reports, see Frame.panel_type)
|
||||||
Frame->>Frame: Write to panel SPI buffer, compute CRC32
|
Frame->>Frame: Write to panel SPI buffer, compute CRC32
|
||||||
alt CRC unchanged since last physical refresh
|
alt CRC unchanged since last physical refresh
|
||||||
Frame->>Frame: Skip refresh (nothing visually changed)
|
Frame->>Frame: Skip refresh (nothing visually changed)
|
||||||
@@ -83,8 +84,9 @@ placement grid, and button-action dispatch.
|
|||||||
once).
|
once).
|
||||||
- Fetch the frame and write it into the panel's SPI buffer
|
- Fetch the frame and write it into the panel's SPI buffer
|
||||||
(`epd_write_frame()`), computing a CRC32 as it streams -- never
|
(`epd_write_frame()`), computing a CRC32 as it streams -- never
|
||||||
buffering the full ~192KB frame in RAM. The panel driver refuses to
|
buffering the full packed frame in RAM (~192KB for the 7.3" panel;
|
||||||
write a short/wrong-size response into the buffer at all, so a
|
proportionally more for the 13.3" panel). The panel driver refuses
|
||||||
|
to write a short/wrong-size response into the buffer at all, so a
|
||||||
truncated fetch can't corrupt what's already there.
|
truncated fetch can't corrupt what's already there.
|
||||||
- Compare the new CRC32 against the last one that was actually
|
- Compare the new CRC32 against the last one that was actually
|
||||||
refreshed onto the panel (persisted in NVS). If it matches -- the
|
refreshed onto the panel (persisted in NVS). If it matches -- the
|
||||||
@@ -117,7 +119,9 @@ git history). Decoding a JPEG, then resizing/dithering/quantizing it to
|
|||||||
the panel's 6-color palette, would be expensive on-device in both memory
|
the panel's 6-color palette, would be expensive on-device in both memory
|
||||||
and battery. Instead, the server does all of that with Pillow and hands
|
and battery. Instead, the server does all of that with Pillow and hands
|
||||||
the frame a pre-packed, ready-to-stream buffer -- the device never
|
the frame a pre-packed, ready-to-stream buffer -- the device never
|
||||||
decodes an image at all.
|
decodes an image at all. The ee02 board's ESP32-S3 does have PSRAM, but
|
||||||
|
the same server-side design applies there too, for consistency and
|
||||||
|
battery reasons rather than because the C6's memory limit forces it.
|
||||||
|
|
||||||
## Why face detection isn't run on-device (or even on the server)
|
## Why face detection isn't run on-device (or even on the server)
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -160,7 +160,13 @@ Calendar widgets pick from discrete size tiers (`calendar_render.py`'s
|
|||||||
`_SIZE_TIERS`) for font size/margins/row heights based on their actual
|
`_SIZE_TIERS`) for font size/margins/row heights based on their actual
|
||||||
grid footprint, rather than continuously scaling constants tuned for a
|
grid footprint, rather than continuously scaling constants tuned for a
|
||||||
full ~800x480 canvas -- falls back to agenda view if a widget is too small
|
full ~800x480 canvas -- falls back to agenda view if a widget is too small
|
||||||
for month view to stay legible.
|
for month view to stay legible. These tiers are pixel-size constants
|
||||||
|
tuned against the 7.3" panel specifically; they aren't re-tuned or
|
||||||
|
verified yet for the 13.3" panel's larger native resolution (see
|
||||||
|
`docs/hardware.md`'s EE02 section) -- a widget's *grid footprint* (cell
|
||||||
|
count) works the same on either panel, but its rendered legibility at
|
||||||
|
that footprint's actual pixel size hasn't been checked on the bigger
|
||||||
|
panel.
|
||||||
|
|
||||||
### "Modern" render style (experimental)
|
### "Modern" render style (experimental)
|
||||||
|
|
||||||
|
|||||||
+24
-16
@@ -43,17 +43,21 @@ algorithm itself -- it just streams the response straight to the panel.
|
|||||||
server, before any admin account exists.
|
server, before any admin account exists.
|
||||||
6. **Optional: auto-update firmware from Gitea releases.** If you're
|
6. **Optional: auto-update firmware from Gitea releases.** If you're
|
||||||
pushing this repo to a Gitea instance, `.gitea/workflows/firmware-release-build.yml`
|
pushing this repo to a Gitea instance, `.gitea/workflows/firmware-release-build.yml`
|
||||||
builds both supported boards and publishes them as release assets
|
builds every supported board and publishes them as release assets
|
||||||
(`firmware-xiao.bin`/`firmware-devkit.bin`) whenever `firmware/version.txt`
|
(`firmware-devkit_esp32c6.bin`/`firmware-xiao_esp32c6.bin`/
|
||||||
changes on `main`. In a frame's **Configuration** tab, set the
|
`firmware-ee02.bin`, plus `firmware-devkit.bin`/`firmware-xiao.bin`
|
||||||
**Gitea repo URL**; if the repo is private, also set
|
duplicates for devices still on pre-rename firmware) whenever
|
||||||
`GITEA_FIRMWARE_TOKEN` (a read-only PAT) in `docker-compose.yml`.
|
`firmware/version.txt` changes on `main`. In a frame's
|
||||||
Which board's build to fetch is learned from the frame itself (its
|
**Configuration** tab, set the **Gitea repo URL**; if the repo is
|
||||||
`X-Frame-Board` header) -- nothing to pick by hand. The server then
|
private, also set `GITEA_FIRMWARE_TOKEN` (a read-only PAT) in
|
||||||
periodically checks for a newer release and either shows an "Update
|
`docker-compose.yml`. Which board's build to fetch is learned from the
|
||||||
frame" button or, with **Automatically apply updates** checked,
|
frame itself (its `X-Frame-Board` header) -- nothing to pick by hand.
|
||||||
stages it itself -- either way the frame only actually updates on
|
The same header also determines which EPD panel the frame renders for
|
||||||
its own next wake.
|
(`Frame.panel_type`, see `docs/hardware.md`'s board identifiers
|
||||||
|
section) -- also never a manual setting. The server then periodically
|
||||||
|
checks for a newer release and either shows an "Update frame" button
|
||||||
|
or, with **Automatically apply updates** checked, stages it itself --
|
||||||
|
either way the frame only actually updates on its own next wake.
|
||||||
|
|
||||||
## Users, frames, and control
|
## Users, frames, and control
|
||||||
|
|
||||||
@@ -106,9 +110,12 @@ Pages: `/` (routing hub), `/setup`, `/login`, `/claim`, `/settings`,
|
|||||||
### Device protocol (`/frame/*` -- paths frozen; auth = `?id=` + `?token=`)
|
### Device protocol (`/frame/*` -- paths frozen; auth = `?id=` + `?token=`)
|
||||||
|
|
||||||
- `GET /frame/image` -- the frame's current image, pre-processed into
|
- `GET /frame/image` -- the frame's current image, pre-processed into
|
||||||
the panel's raw 800x480, 4-bit-per-pixel, 2-pixels-per-byte format
|
the panel's raw 4-bit-per-pixel, 2-pixels-per-byte format
|
||||||
(`application/octet-stream`, exactly 192,000 bytes). **Side-effect-free**
|
(`application/octet-stream`) -- 800x480/exactly 192,000 bytes for the
|
||||||
by default: it only actually advances once `refresh_interval_s` has
|
original 7.3" panel, 1600x1200/exactly 960,000 bytes for the 13.3"
|
||||||
|
panel (see `Frame.panel_type`/`image_pipeline.PANEL_SPECS`; a given
|
||||||
|
device's byte count is fixed by which firmware/panel it actually is).
|
||||||
|
**Side-effect-free** by default: it only actually advances once `refresh_interval_s` has
|
||||||
elapsed since the current photo was set, so an unexpected reboot just
|
elapsed since the current photo was set, so an unexpected reboot just
|
||||||
redisplays the same photo. An unclaimed or not-yet-configured frame
|
redisplays the same photo. An unclaimed or not-yet-configured frame
|
||||||
gets a rendered instruction placeholder (with a claim QR) instead of
|
gets a rendered instruction placeholder (with a claim QR) instead of
|
||||||
@@ -131,8 +138,9 @@ Pages: `/` (routing hub), `/setup`, `/login`, `/claim`, `/settings`,
|
|||||||
*this* frame and 302s to it. Authenticated by the frame's own
|
*this* frame and 302s to it. Authenticated by the frame's own
|
||||||
`manage_token` (see the manage QR below), not device credentials -- a
|
`manage_token` (see the manage QR below), not device credentials -- a
|
||||||
phone scanning the QR has no way to supply `?id=`/`?token=`.
|
phone scanning the QR has no way to supply `?id=`/`?token=`.
|
||||||
- `GET /frame/face-labels` -- up to 4 named faces with 800x480
|
- `GET /frame/face-labels` -- up to 4 named faces with positions in the
|
||||||
positions, flattened (`name_0`/`x_0`/`y_0`, ...) for the device's
|
frame's own panel space (800x480 for the 7.3" panel, 1600x1200 for the
|
||||||
|
13.3"), flattened (`name_0`/`x_0`/`y_0`, ...) for the device's
|
||||||
flat-scalar parser.
|
flat-scalar parser.
|
||||||
- `POST /frame/battery` -- `{"percent": 0-100}`; per-discharge-cycle
|
- `POST /frame/battery` -- `{"percent": 0-100}`; per-discharge-cycle
|
||||||
history (feeds the runtime estimate) plus a permanent per-frame
|
history (feeds the runtime estimate) plus a permanent per-frame
|
||||||
|
|||||||
Reference in New Issue
Block a user