Files
tfaour 474b92a282
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
Add server-side support for a second panel (13.3in Spectra 6 / EE02) and scaffold its firmware target
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

6.7 KiB

name, description
name description
build-firmware Compile the espresso_frame firmware (firmware/) for all three board variants (ESP32-C6 devkit/xiao, ESP32-S3 ee02) without Docker -- a native, non-container ESP-IDF v6.0 install. Use when asked to build the firmware, verify a firmware/main/*.c change actually compiles, or check the devkit/xiao/ee02 board targets.

Compiles firmware/ (ESP-IDF, targeting ESP32-C6 for devkit/xiao and ESP32-S3 for ee02) locally, without Docker -- CI's firmware-build-check.yml/firmware-release-build.yml build inside the espressif/idf:release-v6.0 container image, but this sandbox cannot run containers at all: docker.io installs and dockerd starts fine even as root, but the sandbox strips cap_sys_admin (and blocks the bare unshare syscall) from the capability set regardless of uid, which container image-layer extraction and namespace setup both require. Confirmed by hand: docker run hello-world fails to extract even the tiny hello-world layer ("failed to extract layer... operation not permitted" with the overlayfs snapshotter; "unshare: operation not permitted" even with the vfs storage driver instead). This is a hard restriction of the sandbox itself, not a permissions/setup problem -- don't spend time re-trying --privileged-equivalent flags or alternate storage drivers, none of it routes around a missing cap_sys_admin.

The workaround: skip containers entirely and install ESP-IDF the same way a developer would set it up on their own machine (git clone + ESP-IDF's own install.sh) -- that path needs nothing this sandbox disallows, just normal file/process operations.

Setup (once per fresh container)

bash .claude/skills/build-firmware/setup.sh

Installs (via real apt-get -- this container actually has root and a working package manager, unlike run-server's Chromium bootstrap which had neither):

  • OS build deps: python3/venv/pip, cmake, ninja-build, flex/bison/gperf, build-essential, libusb-1.0-0.
  • ESP-IDF itself: a shallow, single-branch, recursive-submodule clone of release/v6.0 (~700MB) into ~/.espressif-idf/esp-idf -- matches the IDF version CI's Docker image pins. Only clones once; re-running setup.sh never touches an existing checkout.
  • The esp32c6+esp32s3 toolchains + Python venv, via ESP-IDF's own ./install.sh esp32c6,esp32s3 -- scoped to just this project's two chip targets (see firmware/README.md's board table: devkit/xiao are esp32c6, ee02 is esp32s3), not every chip ESP-IDF supports, to keep the download/disk footprint down. install.sh is already idempotent on its own, so setup.sh always calls it rather than duplicating that check -- a re-run costs a few seconds once everything's cached.

Takes a few minutes on a cold run (mostly install.sh's own pip/tool downloads), well under a minute on a re-run. Needs real root (apt-get install) -- if this container ever runs as non-root, this setup doesn't apply as-is (would need the same non-root apt-download + dpkg-deb -x extraction dance run-server's setup.sh uses for Chromium).

Disk: budget ~4GB free before starting (esp-idf checkout + toolchain + Python env land around 3.4GB in ~/.espressif, plus the ~700MB checkout itself). Confirmed working with as little as ~7GB free.

Build

bash .claude/skills/build-firmware/build.sh            # devkit (default)
bash .claude/skills/build-firmware/build.sh xiao
bash .claude/skills/build-firmware/build.sh ee02
bash .claude/skills/build-firmware/build.sh both        # devkit + xiao
bash .claude/skills/build-firmware/build.sh all         # devkit + xiao + ee02

Each board gets its own build directory and generated sdkconfig (see firmware/build_for_board.sh's own comment) -- building one never disturbs the other. build.sh auto-runs set-target (esp32c6 for devkit/xiao, esp32s3 for ee02) the very first time a board is built (no generated sdkconfig yet); later builds skip straight to idf.py build. Extra arguments pass straight through to idf.py, e.g.:

bash .claude/skills/build-firmware/build.sh xiao flash -p /dev/ttyUSB0

flash/monitor need an actual attached device and serial port -- this sandbox has neither, so those only work when this skill runs somewhere hardware is actually plugged in (a real dev machine, or a differently-configured environment with device passthrough).

A clean build of one board takes ~30s once the target's already been configured (~1,000 build steps total split across the boards, most of it ESP-IDF's own components -- this project's own firmware/main/*.c and firmware/components/* sources are a small fraction of that and compile in a few seconds). Output lands at firmware/build/espresso_frame.bin (devkit), firmware/build_xiao/espresso_frame.bin (xiao), or firmware/build_ee02/espresso_frame.bin (ee02, once its driver actually compiles -- see the note above) -- all three paths are gitignored (the repo root .gitignore's "ESP-IDF firmware build output" section), so nothing here needs cleaning up before a commit.

Verified

Both board variants (devkit set-target esp32c6 + build, xiao set-target esp32c6 + build) built successfully end-to-end using this exact setup.sh/build.sh pair, producing real espresso_frame.bin images with normal free-space margins (41%/36% of their respective app partitions) and no errors -- only one pre-existing, unrelated warning (battery.c's unused TAG when that file's logging is compiled out). This is a real compile check, not just a syntax read -- if a future change breaks the build, this skill will actually catch it.

Troubleshooting

  • docker: ... unshare: operation not permitted / failed to extract layer ... operation not permitted: expected in this sandbox, see the top of this file. Don't debug it further -- use this skill's native install instead.
  • ESP-IDF not found at ... -- run setup.sh first: build.sh's own check for a missing $IDF_DIR/export.sh -- run setup.sh (see above) before the first build.
  • idf.py: command not found if you try to run it directly: same gotcha firmware/build_for_board.sh already documents -- idf.py is normally a shell function from ESP-IDF's export.sh, not on PATH as a real executable, so it isn't inherited into a script's own subshell even after sourcing export.sh in your interactive shell first. Use build.sh (or build_for_board.sh, which calls python "$IDF_PATH/tools/idf.py" directly) instead of typing idf.py in a fresh script/subshell.
  • Disk pressure during install.sh: this environment runs close to full (single-digit GB free is normal, not a sign of a leak) -- df -h / before running setup.sh if a build mysteriously fails partway with a "no space left on device"-shaped error.