Phase 7a: dual OTA partitions + signed, self-updating firmware - #2
Merged
Conversation
fetch_latest_release() can parse tag_name successfully but still fail (e.g. no mini_dns.bin asset), and run_check_cycle() was discarding that tag instead of surfacing it alongside last_error, making a partial failure look identical to "never checked" over /api/ota. Also add a 30s cooldown after a failed check cycle so a client hammering POST /api/ota/check can't trigger unbounded rapid retries once the in-progress guard clears.
s_last_failure_us defaults to 0, indistinguishable from esp_timer_get_time()'s own near-zero values during the first ~30s after boot, so the very first legitimate check request after a fresh flash was spuriously rejected as "within cooldown". Gate the comparison on s_last_failure_us != 0. Also tightens the latest_version doc comment to reflect its per-cycle reset behavior.
…health-gate timeout, synchronize status reads
run_check_cycle()'s two failure paths cleared s_check_in_progress before stamping s_last_failure_us, leaving a two-instruction window where a request landing between them saw in_progress=false and a stale (pre- failure) cooldown timestamp — reorder so the cooldown is armed first. Also brings README/ARCHITECTURE.md and the ota_updater_request_check() doc comment in line with the actual health-gate behavior (10-minute uptime fallback, periodic 6h interval, pending-verify rejection), and gives the 409 response accurate wording instead of always claiming "check already in progress" regardless of the real rejection reason.
CI's release build failed at the signing step with a cryptic espsecure
traceback ("Could not deserialize key data") — the secret was almost
certainly populated using README's own generate_signing_key command,
which is missing --scheme ecdsa256 and defaults to an RSA key the
project's ECDSA sdkconfig can't sign with. Add an explicit empty-secret
check and an ECDSA-header sanity check with an actionable error message
in both jobs that write the key, and fix the README command.
Provisioned monitoring/ (docker-compose, scrape config, Grafana datasource + dashboard) for the existing /metrics endpoint, plus a dashboard screenshot referenced from the README's Monitoring section. Sanitized the scrape target's hardcoded LAN IP to the placeholder the setup instructions already document, and moved the screenshot into docs/ with a space-free filename.
push had no branch/tag filter, so every push to phase7a-partition-ota triggered both the push and pull_request events for the same commit — two full firmware+host-test runs per push. Scope push to main and version tags (pull_request already covers feature-branch commits), and add a concurrency group so a new push cancels a still-running prior one instead of queuing behind it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ota_0/ota_1+otadata+coredump), replacing the singlefactorypartition — the prerequisite for any update mechanism.main/ota_updater.cpp/.h: a background task that periodically checks GitHub Releases (every 6h, or on demand viaPOST /api/ota/check), downloads updates over HTTPS viaesp_https_ota()with cert-bundle verification, and only cancels the bootloader's rollback after a real health check (uptime + at least one DNS query answered, with a 10-minute unconditional fallback so an idle-but-healthy device isn't stuck forever).CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT) without enabling full Secure Boot v2 or flash encryption — both would burn eFuses irreversibly, an explicit non-goal for this dev board.main/ota_version.h/.cpp) with 13 Unity test cases.GET /api/ota(status) andPOST /api/ota/check(auth-gated manual trigger) HTTP routes.This branch went through 3 review rounds (per-task + two whole-branch passes) that found and fixed: a signing-key scheme mismatch, a
latest_versiondiscard bug, a zero-sentinel cooldown regression, a stale CI flash offset (would have bricked a fresh flash), a missing periodic check despite docs claiming one, an unsynchronizedstd::stringstruct copied across FreeRTOS tasks (heap-UAF risk), a missing timeout escape from the health gate, and a narrow cooldown-bypass window.Test plan
host_test/:idf.py --preview set-target linux build && ./build/host_test.elf— 37/37 passingidf.py build(esp32s3) — clean, signedmini_dns.bin, 69% ofota_0freeerase-flash flash monitor), confirm health gate passes (~30s + first query), triggerPOST /api/ota/checkagainst a real tagged release, confirm OTA round-trip and bootloader rollback on a deliberately broken image🤖 Generated with Claude Code