Replaces the stub eprintln sender with the real implementation: - mint_access_token: builds + signs a service-account JWT (RS256 over base64url(header).base64url(claims)), POSTs to oauth2.googleapis.com with the standard grant_type=...jwt-bearer form body, parses the access_token + expires_in. - CachedToken: held in the sender thread's local state. Refreshes 300 seconds before expiry (Google issues 1h tokens; we re-mint at T+55min). - post_batch: serializes the WriteEntriesRequest with logName, resource (generic_node + project + MAC node_id), and entries (severity + jsonPayload + optional timestamp), POSTs to logging.googleapis.com/v2/entries:write with the bearer. - Backoff on failure: sleep doubles on each consecutive failure (capped at 5 min). Batches dropped on POST failure rather than re-enqueued; loss is surfaced via dropped_before on the next entry. Crates added: rsa 0.9 (with sha2 feature for AssociatedOid; PKCS#1 v1.5 needs the DigestInfo prefix), time 0.3 (RFC3339 formatting). main.rs: SNTP startup is now gated on gcp.is_some(). The JWT auth needs a real wall-clock for / (Google rejects ~5min skew), so devices with cloud logging pay a one-time ~few-second sync at boot. Devices without [gcp] skip it. Log entry timestamps themselves are still optional — when omitted, GCP server-side assigns them. Cloud Logging severity mapping: tracing TRACE/DEBUG -> DEBUG, INFO -> INFO, WARN -> WARNING, ERROR -> ERROR (matches GCP's LogSeverity enum). device_mac() reads via esp_efuse_mac_get_default and uses the lower 6 bytes as the node_id resource label, so multi-device logs can be filtered by MAC. Firmware size: 1.60 MB -> 1.71 MB (+rsa, +time, +sender code). Still under the 1.94 MB slot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .cargo | ||
| .github | ||
| src | ||
| tools | ||
| trust | ||
| .gitignore | ||
| build.rs | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| eink-plan.md | ||
| LICENSE | ||
| logs-plan.md | ||
| Makefile | ||
| notes.txt | ||
| ota.md | ||
| partitions.csv | ||
| provisioning.toml.example | ||
| README.md | ||
| sdkconfig.defaults.in | ||
ESP32 Rust
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).
What it does today
- Connects to Wi-Fi, fetches public IP and weather over HTTPS.
- Polls
ghcr.io/imjasonh/esp32:latestevery ~60s for new firmware. - For each new digest, fetches the cosign Sigstore Bundle, verifies the signature, the cert chain to the Sigstore root, and that the signer's identity matches the allowlist provisioned into the device's NVS.
- 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.md for the full design.
Hardware
- Board: Inland ESP-WROOM-32 (Micro Center SKU 027466). Connects as
/dev/cu.usbserial-0001on macOS via the onboard CP210x USB-UART. - No user LED on this variant — only a power LED. Watch logs via
make monitor.
Prerequisites (one-time, macOS)
cargo install espup espflash ldproxy
brew install cmake ninja dfu-util cosign
espup install --targets esp32
curl -LsSf https://astral.sh/uv/install.sh | sh # if you don't have uv
First flash (USB)
make provisioning.toml # creates from template
$EDITOR provisioning.toml # fill in wifi creds + trust identities
make bootstrap # build, flash everything, write NVS
make monitor # watch it boot and connect
The first build clones ESP-IDF v5.2.2 into .embuild/ (5–10 min).
Subsequent builds are fast.
The OTA-distributed firmware contains no secrets — Wi-Fi creds and
trust roots live in NVS, written via USB by make provision. See
ota.md for the full design.
Day-to-day
make build Compile firmware
make flash Build + flash app (use flash-all after partitions change)
make flash-all Erase + write bootloader, partition table, app
make provision Write NVS partition from provisioning.toml over USB
make bootstrap flash-all + provision (new device setup)
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 publish requires gh.env (see ota.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.