diff options
| author | Luke Hoersten <[email protected]> | 2026-07-17 20:09:51 -0500 |
|---|---|---|
| committer | Luke Hoersten <[email protected]> | 2026-07-17 20:09:51 -0500 |
| commit | 6c16b354e00e916b7e252b2304b71af87981f518 (patch) | |
| tree | 17ffe1ef9575d761eb87def9423e10ee79cf88c5 | |
| parent | 81306bf3039f24f7ace8ce9cafca31ae747ee630 (diff) | |
tools: make targets + ota.sh for the build/OTA/verify loop
make build / cleanbuild / ota / verify / check wrap the recurring dev
loop, sourcing the ESP-IDF env per-recipe. `make ota` reconfigures first
(the embedded git hash stamps at configure time only), builds, pushes,
and verifies. tools/ota.sh encodes the OTA acceptance criterion — the
new image must show pending-verify on a fresh boot before flipping to
valid; "valid" with pending-verify never seen means the device silently
booted the old slot — and re-pushes once automatically on the known
first-push rollback quirk.
| -rw-r--r-- | Makefile | 37 | ||||
| -rwxr-xr-x | tools/ota.sh | 74 |
2 files changed, 111 insertions, 0 deletions
diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..aec0253 --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +# Helper targets for the common dev loops. idf.py needs the ESP-IDF +# environment; each recipe sources it so `make build` works from a fresh +# shell (Homebrew python first on PATH, per the toolchain setup). +# +# make build incremental firmware build +# make cleanbuild wipe build/ + rebuild (fixes stale CMake cache) +# make ota fresh-stamp build + OTA push + verify (auto-retry) +# make ota VIEWPORT=<host> same, against a specific device +# make verify post-push pending-verify -> valid check only +# make check type-check the Scrypted plugin +SHELL := /bin/bash + +VIEWPORT ?= 10.0.13.83 +ESP_ENV ?= ../../env.sh +IDF = export PATH="/opt/homebrew/bin:$$PATH" && source $(ESP_ENV) >/dev/null 2>&1 && idf.py + +.PHONY: build cleanbuild ota verify check + +build: + $(IDF) build + +cleanbuild: + rm -rf build + $(IDF) build + +# The embedded git hash stamps at CMake configure time only, so an OTA of +# fresh commits must reconfigure first or the binary reports a stale SHA. +ota: + $(IDF) reconfigure >/dev/null + $(IDF) build + tools/ota.sh push $(VIEWPORT) + +verify: + tools/ota.sh verify $(VIEWPORT) + +check: + cd scrypted && npx tsc --noEmit diff --git a/tools/ota.sh b/tools/ota.sh new file mode 100755 index 0000000..f03bcc3 --- /dev/null +++ b/tools/ota.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# OTA push + verification for the viewport firmware. +# +# The bootloader reverts to the previous slot unless the new image reaches +# ota_state=valid (the firmware's 30 s healthy timer marks it). Known quirk: +# the FIRST push of a new binary sometimes silently rolls back — the +# definitive tell is /state reading "valid" at low uptime instead of +# "pending-verify". `push` detects that and re-pushes once automatically. +# +# usage: tools/ota.sh push <host> [bin] # push + verify (+1 auto-retry) +# tools/ota.sh verify <host> # post-push verification only +set -euo pipefail + +cmd="${1:?usage: tools/ota.sh {push|verify} <host> [bin]}" +host="${2:?usage: tools/ota.sh {push|verify} <host> [bin]}" +bin="${3:-build/scrypted-viewport.bin}" + +state_line() { + curl -sS --max-time 3 "http://$host/state" 2>/dev/null | + python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["ota_state"], d["uptime_ms"], d["version"])' \ + 2>/dev/null || true +} + +# Poll /state through the reboot. Success = pending-verify observed on a +# fresh boot, then valid (healthy timer fired). Failure = valid with +# pending-verify never seen — the device booted the OLD slot (or we +# started polling too late to tell; treated as rollback to be safe). +verify() { + local saw_pending=0 i ota up ver + for i in $(seq 1 25); do + sleep 4 + # shellcheck disable=SC2046 + set -- $(state_line) + if [ $# -lt 3 ]; then continue; fi # rebooting / unreachable + ota=$1; up=$2; ver=$3 + echo " t+$((i * 4))s: $ota uptime=${up}ms v$ver" + if [ "$ota" = "pending-verify" ]; then + saw_pending=1 + elif [ "$ota" = "valid" ]; then + if [ "$saw_pending" = 1 ]; then + echo "OK: new image verified (pending-verify -> valid, v$ver)" + return 0 + fi + echo "ROLLBACK: valid at ${up}ms uptime, pending-verify never seen — old slot is running" + return 1 + fi + done + echo "TIMEOUT: never reached valid" + return 1 +} + +push() { + echo "pushing $bin -> http://$host/firmware" + curl -sS --max-time 60 -X POST --data-binary @"$bin" "http://$host/firmware" + echo +} + +case "$cmd" in + push) + push + if ! verify; then + echo "re-pushing (known first-push rollback quirk)" + push + verify || { echo "FAILED: rolled back again after retry"; exit 1; } + fi + ;; + verify) + verify + ;; + *) + echo "usage: tools/ota.sh {push|verify} <host> [bin]" >&2 + exit 2 + ;; +esac |
