src.nth.io/

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Hoersten <[email protected]>2026-07-17 20:09:51 -0500
committerLuke Hoersten <[email protected]>2026-07-17 20:09:51 -0500
commit6c16b354e00e916b7e252b2304b71af87981f518 (patch)
tree17ffe1ef9575d761eb87def9423e10ee79cf88c5
parent81306bf3039f24f7ace8ce9cafca31ae747ee630 (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--Makefile37
-rwxr-xr-xtools/ota.sh74
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