8 Commits
Author SHA1 Message Date
tfaour 454c03586e Port real epd13in3e driver from vendor code; fix wire-raster stride bug
Build and push server image / test (push) Successful in 43s
Firmware build check / build-check (push) Successful in 2m47s
Build and push server image / build-and-push (push) Successful in 4m34s
Build and push server image / deploy (push) Failing after 1m27s
Vendored the panel's init/LUT/refresh register sequence from three
independent Waveshare reference drivers for this exact panel+controller
(RaspberryPi/c, ESP32, and the ESP32-S3-ePaper-13.3E6 ESP-IDF example),
which all agree byte-for-byte. The epd13in3e.c #error is gone; it
compiles clean and links (verified via /build-firmware ee02).

That vendor code also revealed the panel's SPI wire raster is a native
1200x1600 (portrait), not 1600x1200 as previously assumed -- rotated 90
degrees from the panel's landscape mount/marketing size. The old
assumption wasn't just a rotation bug: 1600x1200 and 1200x1600 don't
share a row stride, so packing at the wrong one would have shredded
images into a repeating diagonal garble on real hardware, not just
displayed them sideways. Fixed with a new PANEL_WIRE_TRANSPOSE in
image_pipeline.py, applied after the existing per-frame
ORIENTATION_TRANSPOSE, with a direction-agnostic regression test that
catches the stride bug specifically (a byte-count check alone can't,
since both orientations pack to the same total size).

A full ee02 build still fails, but no longer because of this driver --
main/{back,next,combo}_button.c call an ESP32-C6-only deep-sleep
GPIO-wakeup API with no ESP32-S3 fallback, a separate pre-existing gap
that was simply hidden behind the panel driver's old #error. See
docs/hardware.md for details; CI's continue-on-error on this board
stays in place until that's fixed too.
2026-08-04 22:00:18 +00:00
tfaour 474b92a282 Add server-side support for a second panel (13.3in Spectra 6 / EE02) and scaffold its firmware target
Build and push server image / test (push) Successful in 45s
Firmware build check / build-check (push) Successful in 2m50s
Build and push server image / build-and-push (push) Successful in 4m36s
Build and push server image / deploy (push) Failing after 1m34s
Server: Frame.panel_type (new column + migration) is auto-derived from
the device's reported board (X-Frame-Board), never user-set -- the
panel is a property of the hardware, not a picker in the UI.
image_pipeline's packing/render pipeline is parameterized by panel
geometry instead of hardcoded 800x480 globals, with the real confirmed
13.3in geometry (1600x1200) registered alongside the original 7.3in
panel. Existing 7.3in frames are unaffected (column default + board
mapping both resolve to the original panel).

Board identifiers are also renamed (devkit/xiao -> devkit_esp32c6/
xiao_esp32c6, plus new "ee02") since the EE02 board also carries a XIAO
module -- "xiao" alone stopped disambiguating hardware. The server
keeps accepting the legacy bare names indefinitely for already-flashed
devices.

Firmware: scaffolds a third build target (ee02, ESP32-S3 -- a real
chip-target change, not just a same-chip Kconfig variant like xiao) and
a new epd13in3e driver component skeleton. The actual panel init/LUT/
refresh register sequence isn't ported from vendor demo code yet (none
was available), so that component deliberately fails to compile
(#error) rather than risk sending unverified register values to real
hardware -- devkit/xiao are unaffected and build identically to before.
CI's ee02 build step is continue-on-error for the same reason.
2026-08-04 20:08:22 +00:00
tfaour b9649c35ec Skip redundant panel refreshes and fetch the image before the config check
The panel driver now splits writing a frame into its SPI buffer
(epd_write_frame(), which also computes a CRC32 as it streams) from
actually triggering the physical refresh (epd_turn_on_display()).
frame_client.c compares the new CRC against the last one that was
actually refreshed (persisted in NVS) and skips the refresh entirely
when they match -- e.g. a reboot redisplaying the same photo before the
server's refresh interval elapsed no longer causes a visible flash for
no visual change.

Also reorders the per-wake fetch cycle: the image fetch (15s timeout)
now goes before the config check (3s timeout), instead of after. The
config check's tighter timeout was intermittently tripping on
connection-setup latency that's common on the first request after
waking from a long deep sleep (e.g. stale ARP); putting the more
tolerant request first absorbs that latency, and the config check then
rides the connection it already warmed up.
2026-07-18 23:51:01 -04:00
tfaour 51fbd0657c Never physically refresh the panel with a short/wrong-size frame
Found on hardware: the Tools Server field was pointed at Immich's own
port instead of the frame server's, so /frame/image was actually hitting
Immich and getting back a small error response (~10KB) instead of a
192,000-byte frame. epd_display_stream() logged a size-mismatch warning
but called epd_turn_on_display() anyway, physically refreshing the panel
with a buffer that was ~95% whatever was left over from before -- visible
as "garbage" on screen, overwriting a previously-good image.

epd_display_stream() now returns ESP_ERR_INVALID_SIZE instead of
refreshing when the stream doesn't supply exactly EPD_FRAME_BYTES. Since
this check happens before epd_turn_on_display() is ever called, the
pixel data that *did* arrive only ever reached the panel's internal RAM
over SPI, not the physically visible display, so aborting here leaves the
screen exactly as it was.

This also means fetch_and_display() failing now always implies the panel
was never touched -- simplified frame_client_run() accordingly (dropped
the now-always-true/false out-param that used to distinguish "failed
before vs. during streaming", and always shows the FAILED status screen
on any fetch/display error, since it's now guaranteed safe to do so).
2026-07-18 16:38:43 -04:00
tfaour 1b9226326a Fix hardware-verified bugs: EPD stack overflow and busy-wait spin
Two crashes found flashing to real hardware:

- epd_display_stream's 4KB SPI chunk buffer was a stack local, but the
  default main task stack (3584 bytes) is smaller than that alone --
  Guru Meditation stack protection fault. Made it static instead, and
  bumped CONFIG_ESP_MAIN_TASK_STACK_SIZE to 8192 for headroom in the rest
  of the boot call chain (provisioning -> QR render -> eventually the
  HTTP fetch cycle all run in this one task).

- epd_wait_busy() polled with a 1ms vTaskDelay, which rounds down to 0
  FreeRTOS ticks at the default 100Hz tick rate -- so it never actually
  blocked, tight-spinning the CPU for the panel's real refresh time
  (15-30+s for a full-color pass) and starving the idle task long enough
  to trip the 5s task watchdog. Bumped to 20ms, safely >=1 tick regardless
  of tick rate.

Also updates the EPD pin defaults to the board's actual wiring
(CLK=20 MOSI=19 CS=18 DC=9 RST=10 BUSY=11), confirmed working on hardware.
2026-07-18 14:06:54 -04:00
tfaour 1dd02da70a Add QR-code WiFi onboarding screen
Vendors two small MIT/BSD-3-Clause libraries rather than hand-rolling
either: Nayuki's qrcodegen (QR matrix generation) and Waveshare's Font24
bitmap table from their e-Paper repo (same repo the epd7in3e driver came
from) for rendering readable text on the panel.

qr_onboarding_show() builds a standard WIFI:T:WPA;S:...;P:...;; payload,
rasterizes the QR module matrix plus the SSID and password as plaintext
underneath (for anyone provisioning from a device that can't scan a QR)
onto a malloc'd frame buffer, and pushes it to the panel via
epd_display_buffer(). The buffer is heap-allocated on demand rather than
statically reserved, since 192KB held permanently in BSS would eat into
the RAM budget the HTTP fetch path (task 6) is specifically trying to keep
free.

Wired into wifi_provisioning_start() before the softAP comes up, so the
join instructions are already on-screen by the time the network is
joinable.
2026-07-18 11:20:43 -04:00
tfaour 85a5238724 Add epd7in3e display driver component
Ports Waveshare's official EPD_7in3e.c register/refresh sequence (the
panel has no public datasheet, so their reference driver is the source of
truth) to an ESP-IDF component using spi_master + gpio instead of the
bcm2835/RPi hardware abstraction the reference targets.

Unlike the reference driver, which toggles CS around every single byte,
this holds CS low for each logical command/data phase and DMAs pixel data
in 4KB chunks -- sending ~192,000 individual one-byte SPI transactions
would make a full refresh impractically slow.

Exposes a streaming API (epd_display_stream, pulling chunks from a
caller-supplied read_fn) so the eventual HTTP fetch path can feed the panel
without holding a full ~192KB frame in RAM, plus an in-memory convenience
wrapper (epd_display_buffer) for cases like the upcoming QR code screen
where buffering the whole frame is fine.

Wired into wifi_provisioning_start() as an init + white-clear for now, to
prove the driver builds/links/runs at the right point in the boot sequence
ahead of the actual QR code content.
2026-07-18 11:05:06 -04:00
tfaour cb50c99c98 Restructure repo as monorepo: firmware/ + server/ + docs/
Moves the existing ESP-IDF captive_portal example into firmware/ to make
room for the new FastAPI server and project docs, ahead of building out the
full ESPresso Frame project (ESP32-C6 + Immich-backed e-ink photo frame).
2026-07-18 10:20:25 -04:00