1
0
Fork 0
mirror of https://github.com/imjasonh/esp32 synced 2026-07-06 23:52:24 +00:00

README: refresh to reflect operational OTA, add CI badges

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jason Hall 2026-05-02 14:53:18 -04:00
parent 15a19eb46c
commit 0ebc860e6f

129
README.md
View file

@ -1,117 +1,88 @@
# ESP32 Rust
Std Rust on an Inland ESP-WROOM-32 dev board. Currently a Wi-Fi + HTTPS
demo; e-ink display work coming next (see [`eink-plan.md`](eink-plan.md)).
[![CI](https://github.com/imjasonh/esp32/actions/workflows/ci.yml/badge.svg)](https://github.com/imjasonh/esp32/actions/workflows/ci.yml)
[![Publish OTA](https://github.com/imjasonh/esp32/actions/workflows/publish.yml/badge.svg)](https://github.com/imjasonh/esp32/actions/workflows/publish.yml)
Std Rust on an Inland ESP-WROOM-32 dev board with end-to-end OTA over
GHCR + cosign keyless signing. E-ink display work coming next (see
[`eink-plan.md`](eink-plan.md)).
## What it does today
- Connects to Wi-Fi, fetches public IP and weather over HTTPS.
- Polls `ghcr.io/imjasonh/esp32:latest` every ~60s for new firmware.
- For each new digest, fetches the cosign Sigstore Bundle, verifies the
signature, the cert chain to the bundled Sigstore root, and that the
signer's identity matches the compile-time allowlist in
[`src/trust.rs`](src/trust.rs).
- On verify-pass, streams the layer to the inactive OTA partition,
reboots, and only marks the new image valid after Wi-Fi + registry
bringup checks pass. Any post-OTA failure auto-rolls back via the
bootloader.
Push to `main` → CI builds → publish workflow pushes a signed image to
GHCR → device picks it up on its next poll. See
[`ota-plan.md`](ota-plan.md) for the full design.
## Hardware
- **Board**: Inland ESP-WROOM-32 (Micro Center SKU 027466). Connects as
`/dev/cu.usbserial-0001` on macOS via the onboard CP210x USB-UART.
- **No user LED**: this specific board variant only has a power LED ("D1",
always on). There is no software-controllable LED. For visible output,
attach an external LED + ~330Ω resistor to a GPIO, or watch logs via
- **No user LED** on this variant — only a power LED. Watch logs via
`make monitor`.
## Prerequisites (one-time, macOS)
```bash
# Cargo tools:
# espup installs the Xtensa Rust toolchain (rustc fork + LLVM + GCC)
# espflash flashes firmware over USB serial
# ldproxy linker shim used by esp-idf-sys
cargo install espup espflash ldproxy
# C build deps for the ESP-IDF SDK
brew install cmake ninja dfu-util
# Xtensa Rust toolchain + GCC + esp-clang, installed under
# ~/.rustup/toolchains/esp/. The Makefile finds them via wildcards
# so future espup updates don't require any path edits.
brew install cmake ninja dfu-util cosign
espup install --targets esp32
# uv (the Makefile uses it to install Python 3.12 and shim it into PATH
# so the ESP-IDF Python venv is bootstrapped against 3.12 instead of
# Apple's 3.9). Skip if you already have uv.
curl -LsSf https://astral.sh/uv/install.sh | sh
curl -LsSf https://astral.sh/uv/install.sh | sh # if you don't have uv
```
## First build
## First flash (USB)
```bash
make wifi.env # creates wifi.env from template
$EDITOR wifi.env # fill in WIFI_SSID and WIFI_PASS
make run # build, flash, open serial monitor
make flash-all run # full erase + bootloader + partition + app, then monitor
```
The first build clones ESP-IDF v5.2.2 into `.embuild/` and bootstraps a
Python venv inside it. Plan on 510 minutes. Subsequent builds are fast.
The first build clones ESP-IDF v5.2.2 into `.embuild/` (510 min).
Subsequent builds are fast.
## Day-to-day
```
make build Compile (requires wifi.env)
make flash Build (if needed) and flash app to PORT
make flash-all Erase + write bootloader, partition table, and app
make flash Build + flash app (use flash-all after partitions change)
make monitor Open serial monitor; Ctrl+C to exit
make run Build + flash + monitor
make publish Build, push OCI artifact to ghcr.io/imjasonh/esp32, cosign sign
make clean cargo clean
make flash PORT=/dev/cu.usbserial-XXXX # override the port
```
Use `make flash-all` after partition-table changes, or as the recovery
path if both OTA slots end up bad and the bootloader can't auto-revert
(see "USB recovery" in [`ota-plan.md`](ota-plan.md)). Day-to-day use is
just `make flash` or `make run`.
## Publishing firmware to GHCR (OTA, phase 1)
The firmware is packaged as an OCI artifact and pushed to
`ghcr.io/imjasonh/esp32`. The device will pull it back down (phase 2,
not yet implemented).
One-time setup:
1. Mint a classic PAT at https://github.com/settings/tokens/new with the
`write:packages` scope. (Fine-grained PATs don't currently expose a
Packages permission for user-owned packages.)
2. `echo 'export GH_TOKEN=ghp_...' > gh.env` (gitignored)
3. After the first push, set the package's visibility to public at
https://github.com/users/imjasonh/packages/container/esp32/settings —
the device will pull anonymously, no token on the ESP32.
Then:
```
make publish # build firmware, save .bin, push :latest and :sha-<short>
```
The publisher tool lives at [`tools/publisher/`](tools/publisher) — Rust,
uses `oci-distribution`. It's a separate cargo project so it builds for
the host, not for the ESP32.
## What the firmware does
`src/main.rs` connects to Wi-Fi using the credentials baked in at compile
time from `wifi.env`, then issues two HTTPS GETs and logs the responses
over serial:
- `https://api.ipify.org?format=json` — public IP, JSON
- `https://wttr.in/?format=3` — one-line weather for your location
`make publish` requires `gh.env` (see [`ota-plan.md`](ota-plan.md) for
PAT setup) and a real cosign OIDC flow the first time per ~10min window
— a browser pops to authenticate. CI does this automatically via the
GitHub Actions workflow's ambient OIDC token.
## Project layout
```
src/main.rs firmware
Cargo.toml deps: esp-idf-svc, embedded-svc, log, anyhow
build.rs embuild + cargo:rerun-if-env-changed for WIFI_*
.cargo/config.toml target, linker, runner, build-std
rust-toolchain.toml pins the `esp` rustc toolchain
sdkconfig.defaults ESP-IDF kconfig overrides (stack, mbedtls bundle)
Makefile canonical build / flash / monitor entrypoint
src/main.rs firmware entrypoint, Wi-Fi + HTTPS + OTA loop
src/ota.rs OTA polling, manifest fetch, blob streaming
src/sig.rs cosign Sigstore Bundle verification
src/trust.rs compile-time allowlist of signer identities
trust/ bundled Sigstore Fulcio root + intermediate certs
tools/publisher/ host-side Rust tool that pushes signed OCI artifacts
.github/workflows/ ci.yml (PRs) + publish.yml (push to main)
Cargo.toml firmware deps
partitions.csv OTA-capable partition table (1.75 MB app slots)
sdkconfig.defaults.in ESP-IDF kconfig (Makefile substitutes paths)
Makefile build / flash / monitor / publish entrypoints
wifi.env.example template for compile-time Wi-Fi creds
eink-plan.md plan for the upcoming e-ink display work
ota-plan.md plan for OCI-artifact-based OTA updates
tools/publisher/ Rust tool that pushes firmware as an OCI artifact
notes.txt internal design notes and issues hit during setup
eink-plan.md planned e-ink display work
ota-plan.md full OTA design + per-phase status
notes.txt internal design notes and setup gotchas
```