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

61 commits

Author SHA1 Message Date
b501cafdf3
Merge pull request #18 from imjasonh/dependabot/cargo/tools/publisher/host-tools-6abb98af4c
Bump tokio from 1.52.1 to 1.52.2 in /tools/publisher in the host-tools group across 1 directory
2026-05-04 09:03:05 -04:00
dependabot[bot]
64d866f8fb
Bump tokio
Bumps the host-tools group with 1 update in the /tools/publisher directory: [tokio](https://github.com/tokio-rs/tokio).


Updates `tokio` from 1.52.1 to 1.52.2
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.52.1...tokio-1.52.2)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.52.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: host-tools
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-04 12:57:01 +00:00
50b7950ba3
Merge pull request #17 from imjasonh/fix-publish-pipefail 2026-05-04 06:49:48 -04:00
Claude
187f4ec588
publish: use bash for the recipe so set -eo pipefail works
The `set -eo pipefail` I added in PR #15 broke `make publish` on CI:
Make uses /bin/sh (dash on Ubuntu), and dash rejects `pipefail` with
"Illegal option -o pipefail" before any other command runs. Step 11
(Build, push, sign) failed at line 1 of the recipe.

Fix with a target-specific `SHELL := /bin/bash` so only this recipe
runs under bash; the rest of the Makefile stays on /bin/sh.

https://claude.ai/code/session_01XkNKhsMjCMg4HMTzf3ZZYC
2026-05-04 10:25:46 +00:00
ee27ceed21
Merge pull request #16 from imjasonh/claude/code-review-k7Nr8 2026-05-04 06:24:51 -04:00
Claude
d30c07fd16
restore cloud_log.rs review changes; AtomicU64 → AtomicU32 (fixes CI)
Root cause of the failed firmware build: AtomicU64 is unavailable on
xtensa-esp32-espidf — ESP32 LX6 is 32-bit with max-atomic-width=32 and
no 64-bit CAS, so std::sync::atomic::AtomicU64 isn't compiled in for
that target.

Switch the insertId monotonic counter to AtomicU32. Wraps at ~4.3 B,
which is plenty given our flush rate; insertId only needs uniqueness
within a single second of wall time anyway.

Also re-applies the rest of the cloud_log.rs review changes from the
reverted PR #15: nvs_util migration, LogQueue mutex unwrap_or_else,
module_path!() TARGET cross-references, backoff cap fix, insertId field.

The earlier bisect probes (f786c6d disabling validity/panic-hook/wifi
watchdog, 8cf968a full revert) were misleading — they all kept the
broken AtomicU64 import, so the link error persisted and made it look
like those features were the culprit.

https://claude.ai/code/session_01XkNKhsMjCMg4HMTzf3ZZYC
2026-05-04 03:37:20 +00:00
Claude
f8050d493d
DEBUG: re-add all firmware changes EXCEPT cloud_log.rs
Bisect step: full revert built successfully. Re-adding everything
except cloud_log.rs to check whether cloud_log is the culprit.

If this passes, the bug is in cloud_log.rs.
If this fails, narrow down within main/sig/ota/etc.

https://claude.ai/code/session_01XkNKhsMjCMg4HMTzf3ZZYC
2026-05-04 03:29:46 +00:00
f4e828295a
Merge pull request #15 from imjasonh/claude/code-review-k7Nr8 2026-05-03 23:28:30 -04:00
Claude
8cf968aac4
DEBUG: full firmware revert to main — confirm build works
Bisect probe (f786c6d) still failed even after disabling validity check,
panic hook, and wifi watchdog. Reverting ALL firmware src changes to
main to confirm the failure is in my changes (not environmental).

If this CI passes: the bug is in one of my remaining changes that the
prior probe didn't disable. Will re-add changes file by file.
If this CI fails: the issue is environmental (toolchain roll, runner
change, etc.) and unrelated to my edits.

Tools and Makefile changes are kept (they already build).

https://claude.ai/code/session_01XkNKhsMjCMg4HMTzf3ZZYC
2026-05-04 03:23:39 +00:00
Claude
f786c6db6b
DEBUG: bisect probe — disable validity check, panic hook, wifi watchdog
CI failed on the previous commit and the logs aren't accessible from
this sandbox. Temporarily commenting out the three most novel additions
(sig.rs cert validity check + helper, main.rs install_panic_restart_hook
call, main.rs wifi connect watchdog) so the next CI run tells me
whether the failure is in one of those or elsewhere.

Will revert / re-enable in the next commit once we know which one (if any)
is the culprit.

https://claude.ai/code/session_01XkNKhsMjCMg4HMTzf3ZZYC
2026-05-04 03:17:15 +00:00
Claude
b3bcf72fe5
review fixes across firmware + host tools
sig.rs:
- reject empty DSSE signatures array (was index panic)
- check DSSE payloadType is application/vnd.in-toto+json
- check each cert's notBefore/notAfter window vs wall clock
- drop dead Sha256/Sha384 imports + _unused_keep_imports stub

main.rs:
- drop the demo ipify/wttr fetches; rollback no longer depends on
  third-party uptime
- add a Wi-Fi connect watchdog that reboots after 60 s of hang
- install_panic_restart_hook so panics flush+restart instead of
  landing in IDF's generic panic path
- comment the dual-path tracing (registry layer + log-always to
  EspLogger) and the NVS-error-vs-missing semantics

cloud_log.rs:
- LogQueue mutex now recovers from poison via unwrap_or_else
- replace hardcoded "esp32_blinky::*" target strings with
  module_path!() constants exported from each module
- add insertId on every entry so Cloud Logging preserves stable
  within-second order
- raise backoff exp cap to 6 so the 5 min ceiling is actually
  reachable

ota.rs:
- widen NVS string buffer for repo/tag (96 -> 256 B); 96 silently
  fell back to defaults if a repo string overflowed
- debug_assert base >= 1 s in jittered() to catch a 0 s misconfig
- assert sig index has exactly 1 manifest instead of silently
  picking the first
- log verify_bundle elapsed ms (Xtensa P-256/P-384 perf canary)
- note token-reuse risk between phase 1 and phase 2

provision tool:
- --dry-run for parsing checks without embuild artifacts
- warn loudly when sa_key_pem is inside the worktree
- delete staged identities JSON after the partition image is
  generated

publisher tool:
- pull-verify now asserts layer mediaType matches the firmware
  type, not just layer count

Makefile:
- set -eo pipefail in the publish recipe so a publisher crash
  surfaces directly instead of being swallowed by the digest
  pipeline

new module src/nvs_util.rs consolidates the read_str / read_blob
helpers that were duplicated across cloud_log/trust/ota/main.

https://claude.ai/code/session_01XkNKhsMjCMg4HMTzf3ZZYC
2026-05-04 01:34:45 +00:00
3a765e6a48
Merge pull request #14 from imjasonh/docs-observability
docs: reorganise into docs/, fold logs/monitoring plans into observability.md
2026-05-03 08:35:44 -04:00
7e32f6decb docs: reorganise into docs/, fold logs-plan + monitoring-plan into observability.md
- New `docs/observability.md`: descriptive (present-tense) write-up
  of the Cloud Logging + Cloud Monitoring pipelines as currently
  shipped. Replaces the historical `logs-plan.md` + `monitoring-plan.md`
  planning docs.
- New `docs/setup.md`: prerequisites, first flash, day-to-day Make
  targets, and the optional GCP setup (lifted out of the README so the
  README can stay terse). Includes the Python 3.12-shim explanation
  from the old `notes.txt`.
- Move `ota.md` → `docs/ota.md`.
- Move `eink-plan.md` → `docs/eink-plan.md`. Per-feature plans still
  use the `<feature>-plan.md` name; once shipped they get rewritten
  in present tense alongside the other docs.
- Delete `logs-plan.md`, `monitoring-plan.md`, `notes.txt`. Their
  user-facing content is now in `docs/setup.md`; their LLM-relevant
  bits (architectural rationale, partition-table CMake quirk, Python
  shim, no-LED, `make` conventions, NVS key length cap) are in
  `CLAUDE.md`.
- Trim `README.md` to a top-level overview + links into `docs/`.
- Update internal cross-references (Makefile, ota.md, eink-plan.md,
  tools/provision/src/main.rs doc-comment).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 08:30:43 -04:00
9dcfb35eb1
Merge pull request #13 from imjasonh/ci-cache-order
ci: restore caches before `cargo install espflash`
2026-05-03 00:22:44 -04:00
ed1d18d8df ci: restore caches before cargo install espflash
`cargo install espflash --locked` builds from source. Moving the
.embuild + Swatinem cargo-cache restores ahead of it lets the install
use the cached registry index + downloaded crates instead of fetching
them fresh each run — shaves ~30 s off the install on warm caches.

Pure step-reordering; no behavioural change. Both caches still depend
on inputs that exist at checkout, so an earlier restore is safe.

Bigger speedup is available by replacing the cargo install with a
prebuilt-binary install (e.g. taiki-e/install-action drops espflash
from ~3-5 min to ~2 s). Deferred to its own PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:17:25 -04:00
dbe679d6ea
Merge pull request #12 from imjasonh/ota-config-fw-version
Provision OTA repo/tag/poll_secs; tag logs+metrics with fw_version
2026-05-03 00:12:18 -04:00
01c8c2da75 provision OTA repo/tag/poll_secs; tag logs+metrics with fw_version
Two related additions for fleet observability + tunability:

1. `[ota]` block in provisioning.toml. Optional, individually-optional
   fields: `repo`, `tag`, `poll_secs`. Writes to the existing `ota` NVS
   namespace (key names match src/ota.rs's NVS_REPO / NVS_TAG /
   NVS_POLL_SECS). Missing keys leave the firmware on its compile-time
   defaults (ghcr.io/imjasonh/esp32:latest, 60 s poll).

2. `fw_version` (the GIT_SHA baked in at build time) on every log
   entry and every metric series, so behaviour can be correlated with
   a release across the fleet.
   - cloud_log: jsonPayload.fw_version on every entry.
   - metrics: metric.labels.fw_version on every TimeSeries (resource
     labels can't carry it — `generic_node` has a fixed schema; metric
     labels let queries split heap, rssi, etc. by fw).

Both threaded from main.rs's existing FW_VERSION constant; no new
config plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:56:26 -04:00
e525bb2bbf
Merge pull request #11 from imjasonh/serialize-https
Serialize cloud HTTPS, pause senders during OTA download
2026-05-02 21:00:48 -04:00
219e35903f Pause cloud_log + metrics during OTA blob download
Even with serialization + dynamic mbedtls buffers + KEEP_PEER_CERT=n,
two concurrent TLS sessions don't fit on this chip's heap. The OTA
download holds its session open for tens of seconds while
cloud_log + metrics POSTs need ~25-30 KB of contiguous heap on top
for their handshake — and the second session reliably triggers
MBEDTLS_ERR_SSL_ALLOC_FAILED, killing the OTA download mid-stream.

Add an `OTA_DOWNLOAD_IN_PROGRESS` AtomicBool in `gcp_auth`. OTA's
`download_and_apply` flips it true via an `OtaDownloadGuard` (RAII so
the flag clears even on `?` returns). cloud_log + metrics check the
flag at the top of each cycle and skip — but don't drain — when set.

Effects:
- cloud_log entries accumulate in the bounded queue and flush in a
  single batched POST once the download completes (or fails).
- metrics misses at most one snapshot per download.
- OTA download succeeds reliably — verified end-to-end (~19s clean,
  no TLS errors, "download complete, SHA verified" reached).

Downloads only happen when there's a genuinely new image, so this
pause is rare in steady state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 20:56:09 -04:00
f0c23bbe1b Serialize short HTTPS calls; tighten mbedtls per-session heap
The metrics work in PR #10 added a third concurrent TLS client
(metrics → monitoring.googleapis.com) on top of cloud_log → logging
and OTA → ghcr. Three concurrent handshakes pinned ~90 KB of mbedtls
context and pushed `min_free_heap` into the single-digit-KB range,
producing intermittent ESP_ERR_HTTP_CONNECT failures (mbedtls
ALLOC_FAILED at -0x7F00).

Serialize via a shared `Arc<Mutex<()>>` constructed in main and held
**at the call sites** in each sender — std `Mutex` isn't reentrant and
`auth.get_or_refresh()` mints a token (an HTTPS POST to oauth2) that
must run inside the same critical section. Lock granularity:

- cloud_log: token-refresh + entries:write under one lock window.
- metrics: token-refresh + timeSeries.create under one lock window.
- ota::poll_once: token + manifest under lock; release for the
  signature bundle fetch's lock; release; verify (CPU, no TLS);
  download_and_apply UNLOCKED. Releasing between phases avoids
  blocking cloud_log/metrics behind ~2s of pure-Rust X.509/ECDSA work.
- ota::download_and_apply: deliberately not in the lock — multi-second
  blob downloads must not block per-5-s log flushes or per-30-s
  metrics POSTs.

Plus mbedtls heap knobs that compose with the dynamic-buffer config
already in sdkconfig:

- CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE=n: drop the parsed peer
  cert chain after handshake. We don't reuse sessions or do mutual
  TLS, so we never need it post-handshake. Saves ~3-5 KB per active
  session.

Known limitation, deferred to a follow-up: cloud_log/metrics handshakes
can still collide with the held-open OTA download session and OOM the
download mid-stream. The plan there is an `OTA_DOWNLOAD_IN_PROGRESS`
atomic that cloud_log + metrics check before locking — they skip the
POST and let the queue accumulate until the download finishes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 20:48:53 -04:00
e03f6e92e7
Merge pull request #10 from imjasonh/metrics
metrics: ship chip-health stats to Cloud Monitoring
2026-05-02 19:43:28 -04:00
99da9c6b87 metrics: ship Cloud Monitoring time series; refactor gcp_auth
Implements monitoring-plan.md.

- src/gcp_auth.rs: shared TokenProvider. Mints a multi-scope
  (logging.write + monitoring.write) JWT once and caches the bearer;
  cloud_log + metrics threads share it. Also moves http_post,
  parse_signing_key, b64url, now_unix_secs, unix_to_rfc3339,
  device_mac out of cloud_log.rs.
- src/cloud_log.rs: cloud_log::run takes Arc<TokenProvider> instead
  of owning the JWT/token state. LogQueue::stats() exposes depth +
  lifetime drop count for the metrics path.
- src/metrics.rs: periodic snapshot → POST /v3/projects/{id}/timeSeries.
  GAUGE INT64 metrics under custom.googleapis.com/esp32/<name>:
  free_heap, free_heap_internal, min_free_heap, largest_free_block,
  stack_hwm (label task=main|ota|cloud_log|metrics), wifi_rssi,
  wifi_channel, cpu_freq_mhz, uptime_secs, nvs_used/free_entries,
  cloud_log_queue_depth, cloud_log_dropped_total. Auto-creates
  MetricDescriptors on first POST. Cadence via NVS metric_intvl
  (gcp namespace), default 300s, 0 disables.
- Each spawned thread publishes its TaskHandle_t into a module-level
  AtomicUsize so metrics can read uxTaskGetStackHighWaterMark per
  task without holding handles for them. (Xtensa StackType_t is
  uint8_t, so the API returns bytes — no word-size multiplication.)

Plus:
- sdkconfig: enable CONFIG_MBEDTLS_DYNAMIC_BUFFER (+ free_config_data,
  free_ca_cert) so three concurrent TLS clients (cloud_log + metrics
  + OTA) don't blow our heap budget. Recovers ~15-25 KB per session.
- Makefile: `make flash` and `make run` now pass --partition-table.
  Without it, espflash 4.x writes a *default* (factory-only) partition
  table over our OTA layout, silently bricking OTA on the device.
- README + provisioning.toml.example: gcloud command for
  roles/monitoring.metricWriter; document metrics_interval_secs.
- tools/provision: write metric_intvl NVS u32 (default 300).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:38:04 -04:00
4a6d8d77c2 monitoring-plan.md: ship metrics to Cloud Monitoring
Plan for src/metrics.rs — periodic snapshot of heap, stack, wifi, cpu,
uptime, NVS, cloud_log queue stats; POSTed as time series to
monitoring.googleapis.com/v3/timeSeries. Shares the cloud_log SA + key
via a multi-scope JWT and a refactored src/gcp_auth.rs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 19:01:00 -04:00
bf8d552ab9
Merge pull request #9 from imjasonh/cloud-logging
Cloud Logging: ship tracing events to GCP via service-account auth
2026-05-02 18:06:30 -04:00
62b50fde76 Merge main into cloud-logging
main bumped sha2 0.10 -> 0.11 (PR #8) and switched format!("{:x}", ...)
to hex::encode in src/ota.rs. The auto-merge handled both, plus
Cargo.toml union of dep lists. Cargo.lock had one residual marker around
the firmware deps block (kept main's qualified "sha2 0.11.0" + retained
cloud-logging's rsa/time/tracing-subscriber).

rsa = 0.9 still depends on sha2 = 0.10 internally, so SigningKey<Sha256>
must be parameterised against rsa's re-export (rsa::sha2::Sha256), not
our top-level sha2 0.11.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 18:01:58 -04:00
03b4b223a8 cloud_log: tolerate missing NVS, trim PEM, demote tick logs
- ESP_ERR_NVS_NOT_FOUND from EspNvs::new(gcp,...) is normal for devices
  without a [gcp] block; return Ok(None) rather than crashing.
- parse_signing_key trims trailing whitespace; jq -r appends a newline
  on top of the PEM's own trailing newline and pem-rfc7468 then errors
  trying to parse a second (empty) block.
- Demote per-poll OTA lines (sleeping / no change / manifest) and
  per-flush cloud_log lines (posted batch / minted token) from info to
  debug. They fire continuously without actionable info, and at INFO
  they flood Cloud Logging. INFO stays for OTA update + verify events.

Plus README GCP setup steps and .gitignore for SA key files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:56:32 -04:00
1264ca7da1 cloud_log: real sender — JWT, RSA-SHA256, OAuth2, POST to logging.googleapis.com
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>
2026-05-02 17:13:03 -04:00
7cb4e8c75c cloud_log: NVS-loaded config, tracing layer, queue, stub sender
First commit of cloud-logging work (logs-plan.md). Foundation only:
the actual NTP/JWT/RSA/HTTPS-POST sender is the next commit; this
sender is a stub that writes 'would POST' to serial.

src/cloud_log.rs:
- GcpConfig::load reads the optional 'gcp' NVS namespace. If any
  required key (project_id / sa_email / sa_key_id / sa_key_pem) is
  missing, returns Ok(None) — cloud logging is opt-in per device.
- LogQueue: Mutex<VecDeque>-backed bounded ring buffer (256 entries),
  drops oldest when full and surfaces the drop count on the next push.
- CloudLogLayer: tracing_subscriber Layer that captures events,
  extracts structured fields via field::Visit, applies the configured
  min_severity filter, and pushes onto the queue. Uses wall-clock
  time when SystemTime::now() is past 2020 (NTP synced); else None
  so Cloud Logging assigns server-side timestamps.

main.rs:
- Take NVS first, before any tracing events fire.
- If the gcp NVS namespace is populated, install CloudLogLayer as
  the global tracing subscriber and spawn the sender thread (32 KB
  stack).
- tracing now uses the 'log-always' feature so events still emit
  log records even with a subscriber installed — keeps EspLogger
  writing to serial regardless.

tools/provision/:
- Optional [gcp] section in provisioning.toml. Tool emits the gcp
  namespace into the NVS CSV when present, validates min_severity
  spelling early.

ota.md: gcp namespace added to NVS schema docs.
provisioning.toml.example: commented [gcp] block.

Firmware size: 1.52 MB -> 1.60 MB (tracing-subscriber + cloud_log code).
Plenty of slot headroom remaining (1.94 MB).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 17:06:00 -04:00
02a9af0653
Merge pull request #8 from imjasonh/dependabot/cargo/firmware-7fd8197374
Bump the firmware group with 3 updates
2026-05-02 16:55:56 -04:00
8aa29329ff firmware: use hex::encode for SHA digests (sha2 0.11 dropped LowerHex)
Same fix as the publisher in PR #5: sha2 0.11's digest output type
(`Array<u8, _>`) doesn't impl `LowerHex`, so `format!("{:x}",
Sha256::digest(...))` no longer compiles. Switch to `hex::encode`
and add `hex = "0.4"` as a direct dep.

Two call sites in src/ota.rs (manifest digest hash + downloaded layer
hash). esp-idf-svc 0.52 + embedded-svc 0.29 had no API changes that
affected our code paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:51:14 -04:00
892716bbeb
Merge pull request #7 from imjasonh/dependabot/cargo/tools/provision/host-tools-4fd9825042
Bump toml from 0.8.23 to 1.1.2+spec-1.1.0 in /tools/provision in the host-tools group across 1 directory
2026-05-02 16:47:15 -04:00
dependabot[bot]
c3628fe03c
Bump the firmware group with 3 updates
Bumps the firmware group with 3 updates: [esp-idf-svc](https://github.com/esp-rs/esp-idf-svc), [embedded-svc](https://github.com/esp-rs/embedded-svc) and [sha2](https://github.com/RustCrypto/hashes).


Updates `esp-idf-svc` from 0.51.0 to 0.52.1
- [Changelog](https://github.com/esp-rs/esp-idf-svc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/esp-rs/esp-idf-svc/commits)

Updates `embedded-svc` from 0.28.1 to 0.29.0
- [Changelog](https://github.com/esp-rs/embedded-svc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/esp-rs/embedded-svc/compare/v0.28.1...v0.29.0)

Updates `sha2` from 0.10.9 to 0.11.0
- [Commits](https://github.com/RustCrypto/hashes/compare/sha2-v0.10.9...sha2-v0.11.0)

---
updated-dependencies:
- dependency-name: esp-idf-svc
  dependency-version: 0.52.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: firmware
- dependency-name: embedded-svc
  dependency-version: 0.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: firmware
- dependency-name: sha2
  dependency-version: 0.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: firmware
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-02 20:35:56 +00:00
dependabot[bot]
fa044f4be9
Bump toml in /tools/provision in the host-tools group across 1 directory
Bumps the host-tools group with 1 update in the /tools/provision directory: [toml](https://github.com/toml-rs/toml).


Updates `toml` from 0.8.23 to 1.1.2+spec-1.1.0
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.23...toml-v1.1.2)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 1.1.2+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: host-tools
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-02 20:35:47 +00:00
fa32f0b774 Fix Dependabot on firmware crate; fold provisioning-plan.md into ota.md
Dependabot fix: drop the root rust-toolchain.toml so Dependabot's
runner doesn't try to use the (uninstalled) `esp` toolchain when
running `cargo update`. The firmware Make recipe now invokes
`cargo +esp build` explicitly so local + CI builds still pin the
right toolchain. Host tools (publisher, provision) keep their own
rust-toolchain.toml = stable.

Dependabot config: batch tools/publisher + tools/provision into one
weekly PR using the `directories:` (plural) field. Firmware Cargo
deps and GHA action versions stay in their own PRs.

ota.md: fold in the provisioning-plan.md content (now that
provisioning is shipped). Updates partition layout numbers, NVS
schema, bootstrap workflow, and trust-config description (no longer
compile-time consts). Adds a 'Soft vs hard trust' section folded
from the plan. Future Work merged.

provisioning-plan.md deleted; README and CLAUDE.md updated to point
at ota.md and drop the dropped file from the project layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:34:47 -04:00
042f26d376
Merge pull request #6 from imjasonh/provisioning
Provision Wi-Fi creds + trust roots via NVS (no secrets in OTA image)
2026-05-02 16:26:45 -04:00
1928dcfd47 Provision Wi-Fi creds + trust roots via NVS instead of compile-time embed
Implements provisioning-plan.md. The OTA-distributed firmware no longer
contains any secrets or per-device config — those live in NVS and are
written via USB by `make provision` from a (gitignored)
`provisioning.toml`. Same firmware bytes can run on any device.

Firmware changes:
- src/main.rs reads wifi/ssid + wifi/pass from NVS namespace `wifi`.
  Strict-inert (sit-and-log) when missing; no compile-time fallback.
  WIFI_SSID/WIFI_PASS env! macros gone.
- src/trust.rs becomes a `TrustConfig::load(nvs)` loader. Identities
  come from NVS namespace `trust`, key `identities` (JSON-encoded
  array). Sigstore root + intermediate PEMs come from `trust/fulcio_root`
  and `trust/fulcio_inter` blobs. include_str! gone.
- src/sig.rs and src/ota.rs thread &TrustConfig through verify_bundle
  and the OTA loop instead of reading global consts.
- build.rs no longer tracks WIFI_* env changes.

New tool:
- tools/provision/ host-side cargo crate. Reads provisioning.toml,
  emits an NVS CSV, shells out to ESP-IDF's nvs_partition_gen.py to
  produce a binary NVS image, optionally `espflash write-bin`'s it.

Make targets:
- `make provision` — build NVS image + flash it.
- `make bootstrap` — flash-all + provision (new device setup).
- `make build` no longer requires wifi.env.
- wifi.env removed from prereqs / WIFI_ENV variable removed.

Workflows:
- ci.yml: drops the placeholder wifi.env step, adds a `build provision`
  job alongside the firmware + publisher jobs.
- publish.yml: drops WIFI_SSID/WIFI_PASS secret reads. Only secret
  needed is the auto-injected GITHUB_TOKEN.

Docs:
- README updated to use `make bootstrap` + `make provision` flow.
  Project layout moved to a new repo-local CLAUDE.md.
- provisioning.toml.example committed as the template the user copies.
- wifi.env.example removed (no longer used).

Migration: existing devices need `make bootstrap` over USB. The new
firmware has no embedded creds; OTAing to it without provisioning
would just sit in strict-inert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 16:14:26 -04:00
89a80d7ba3 Plan: secrets + trust config provisioned via NVS, OTA image device-agnostic
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:58:47 -04:00
979c862941 Aggressive release-profile flags + grow OTA slots to 1.94 MB
`[profile.release]` now uses lto, codegen-units=1, strip, and
panic=abort (last one matches the build-std=panic_abort already in
.cargo/config.toml). Combined: firmware shrinks from 1.71 MB to
1.52 MB (~9% smaller).

partitions.csv: grow each OTA slot from 1.75 MB to 1.9375 MB by
absorbing the previously-unused 384 KB at the end of flash. Now no
unused space at the end. Repartitioning is a USB-only migration
(`make flash-all`); device migrated locally before pushing.

Net headroom per slot: 126 KB -> 463 KB (3.7x). Plenty for the
upcoming e-ink driver + embedded-graphics stack.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:39:33 -04:00
d6d1a7ba1a Rename ota-plan.md -> ota.md; rewrite as descriptive system doc
Was structured as a multi-phase plan with each phase marked DONE; now
that the system is operational, restructure as documentation describing
what's built (architecture, components, partition layout, update flow,
anti-bricking + USB recovery, CI signing, on-device verification).

Remaining ideas moved to a 'Future work' section at the end. Dropped
explicit support for legacy cosign .sig formats from the doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:17:49 -04:00
0ebc860e6f README: refresh to reflect operational OTA, add CI badges
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:53:44 -04:00
15a19eb46c
Merge pull request #5 from imjasonh/dependabot/cargo/tools/publisher/publisher-f34648a8fe
Bump sha2 from 0.10.9 to 0.11.0 in /tools/publisher in the publisher group
2026-05-02 14:51:37 -04:00
24dc6b5fe1 Makefile: nuke stale python-shim symlinks before recreating
CI failure: `embuild ... Could not install esp-idf ... Failed to locate
python ... Too many levels of symbolic links`. Root cause: when
.embuild is restored from a previous workflow's cache, the stale
python-shim/python3 symlink can collide with what `uv python find`
returns -- producing a self-reference that the kernel surfaces as
ELOOP on the next access.

Fix: `rm -f` the shim symlinks before resolving uv's 3.12 path, so
each run starts from a known-empty state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:50:41 -04:00
43ca7f98ba publisher: use hex::encode for SHA digests (sha2 0.11 dropped LowerHex)
sha2 0.11 changed the digest return type from `GenericArray<u8, _>`
to `Array<u8, _>`, which doesn't implement `LowerHex` -- so the
existing `format!("{:x}", Sha256::digest(...))` calls no longer
compile. Switch to `hex::encode` (added as a dep), which works on
any `AsRef<[u8]>`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:47:32 -04:00
dependabot[bot]
bd8406e8fa
Bump sha2 in /tools/publisher in the publisher group
Bumps the publisher group in /tools/publisher with 1 update: [sha2](https://github.com/RustCrypto/hashes).


Updates `sha2` from 0.10.9 to 0.11.0
- [Commits](https://github.com/RustCrypto/hashes/compare/sha2-v0.10.9...sha2-v0.11.0)

---
updated-dependencies:
- dependency-name: sha2
  dependency-version: 0.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: publisher
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-02 18:42:22 +00:00
f94f0f9f8a Add Dependabot config: weekly grouped updates for GHA + Cargo deps
Three ecosystems: GitHub Actions (workflow files), firmware Cargo
deps (root), publisher Cargo deps (tools/publisher). Each ecosystem
groups all updates into a single weekly PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:41:15 -04:00
6596cfe0e0 publish.yml: pin cosign-installer to v4.1.1 (no v4 moving tag)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:38:22 -04:00
689270f4c9
Merge pull request #4 from imjasonh/bump-deps-and-ci
Add CI workflow, bump GHA actions, commit Cargo.lock
2026-05-02 14:33:25 -04:00
0e9559f375 CI: drop .embuild cache (corruption across runners), pass --target on publisher
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:27:42 -04:00
9ef15ba16d CI: pin setup-uv to v7 (no v8 moving tag), add publisher rust-toolchain
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:22:34 -04:00
71fa55f189 Add CI workflow, bump GHA actions, refresh + commit Cargo.lock
CI workflow (.github/workflows/ci.yml):
- Triggers on PRs (and main pushes for redundant fast feedback).
- Two parallel jobs: build the firmware (with placeholder wifi.env),
  build the host-side publisher tool. No publishing or signing -- that
  remains publish.yml's job.
- Per-PR concurrency: a new push to a branch cancels in-flight CI for
  the previous commit on that branch.

GHA action bumps (publish.yml + ci.yml both use the new versions):
- actions/checkout    v4 -> v6
- astral-sh/setup-uv  v3 -> v8
- esp-rs/xtensa-toolchain  v1.5 -> v1.7
- sigstore/cosign-installer  v3 -> v4 (defaults to cosign 3, which
  still produces the Sigstore Bundle v0.3 format we verify on-device)
- actions/cache       v4 -> v5
- Swatinem/rust-cache stays v2 (already current)

Gets us off the deprecated Node.js 20 actions runtime.

Cargo.lock: now tracked. Both crates (firmware and publisher) are
binaries, and binary Cargo.lock should be committed for reproducible
builds. `cargo update` was a no-op except for a couple transitive
patch bumps in the firmware (generic-array 0.14.7 -> 0.14.9).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 14:16:55 -04:00