--- name: build-firmware description: 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 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 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 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.