Add a single-app invariant step: set the boot default to the canonical app, stop any stray *-main-1 container that would collide on :8080/:9999, and ensure the canonical app is running. Prevents the duplicate-"ZeroClaw Node" collision from recurring after a reboot/re-plug. Co-Authored-By: Claude Opus 4.8 <[email protected]>
127 lines
5.3 KiB
Bash
Executable File
127 lines
5.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# recover.sh — restore the Uno Q board's LOCAL DEV binding after a USB re-plug.
|
|
#
|
|
# On re-plug the Uno Q's Linux reboots and the laptop's adb tunnels vanish, so
|
|
# the local API (and the LED-matrix mirror) lose the board; the board also boots
|
|
# showing a fresh random claim code the API never received. This makes it all
|
|
# consistent again in one pass:
|
|
# 1. re-forward the adb tunnels (:8080 gateway, :9999 matrix relay)
|
|
# 2. assert the single-app invariant (canonical app is the boot default + the
|
|
# only thing on :8080/:9999; stop any stray duplicate)
|
|
# 3. wait for the node daemon (start the App Lab app if it isn't up)
|
|
# 4. re-register the node with the local API (restores its in-memory binding)
|
|
# 5. scroll a fixed claim code on the matrix so the board + API agree
|
|
#
|
|
# Usage:
|
|
# ./recover.sh # one recovery pass, then exit
|
|
# ./recover.sh --watch # run forever: recover on every (re)connect
|
|
#
|
|
# The App Lab node (apess-onboard) carries its own baked cloud token, so unlike
|
|
# the old host-daemon flow this needs NO secrets in the environment. Config via
|
|
# env (defaults suit the current dev board):
|
|
# SERIAL FLEET_SECRET KIT_ID CLAIM_CODE API_URL NODE_URL APP
|
|
set -uo pipefail
|
|
|
|
SERIAL="${SERIAL:-65301572}"
|
|
API="${API_URL:-http://127.0.0.1:3000}"
|
|
FLEET_SECRET="${FLEET_SECRET:-apess2026}"
|
|
KIT_ID="${KIT_ID:-crimson-node}"
|
|
CLAIM_CODE="${CLAIM_CODE:-7777}"
|
|
NODE_URL="${NODE_URL:-http://127.0.0.1:8080}"
|
|
APP="${APP:-/home/arduino/ArduinoApps/apess-onboard}"
|
|
CONTAINER="${APP##*/}-main-1" # App Lab names the container <app>-main-1
|
|
PORTS=(8080 9999)
|
|
|
|
# Resolve adb even under launchd's minimal PATH.
|
|
ADB="$(command -v adb || true)"
|
|
for c in /opt/homebrew/bin/adb /usr/local/bin/adb "$HOME/Library/Android/sdk/platform-tools/adb"; do
|
|
[ -n "$ADB" ] && break
|
|
[ -x "$c" ] && ADB="$c"
|
|
done
|
|
[ -n "$ADB" ] || { echo "recover: adb not found in PATH"; exit 127; }
|
|
|
|
log() { printf '\033[36m[recover]\033[0m %s\n' "$*"; }
|
|
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
|
|
warn() { printf ' \033[33m!\033[0m %s\n' "$*"; }
|
|
|
|
adb_present() { "$ADB" devices | grep -q "^${SERIAL}[[:space:]].*device$"; }
|
|
|
|
matrix_code() {
|
|
python3 - "$CLAIM_CODE" <<'PY' 2>/dev/null
|
|
import socket, sys
|
|
s = socket.create_connection(('127.0.0.1', 9999), timeout=3)
|
|
s.sendall(f"text {sys.argv[1]}\n".encode()); s.recv(16); s.close()
|
|
PY
|
|
}
|
|
|
|
recover_once() {
|
|
adb_present || { warn "board $SERIAL not connected"; return 1; }
|
|
|
|
# 1 · re-forward tunnels (they vanish on re-plug)
|
|
for p in "${PORTS[@]}"; do
|
|
"$ADB" -s "$SERIAL" forward --list 2>/dev/null | grep -q "tcp:$p" \
|
|
|| "$ADB" -s "$SERIAL" forward "tcp:$p" "tcp:$p" >/dev/null
|
|
done
|
|
ok "tunnels forwarded (${PORTS[*]})"
|
|
|
|
# 2 · single-app invariant: the canonical node app owns :8080/:9999. Make it
|
|
# the boot default, stop any OTHER user app (a stray duplicate would collide on
|
|
# our ports), and ensure it's the running container.
|
|
"$ADB" -s "$SERIAL" shell "arduino-app-cli properties set default $APP" >/dev/null 2>&1
|
|
local strays
|
|
strays="$("$ADB" -s "$SERIAL" shell \
|
|
"docker ps --format '{{.Names}}' 2>/dev/null | grep -E '\-main-1$' | grep -v '^${CONTAINER}$'" 2>/dev/null | tr -d '\r')"
|
|
if [ -n "$strays" ]; then
|
|
warn "stopping stray app container(s): $strays"
|
|
for c in $strays; do
|
|
"$ADB" -s "$SERIAL" shell "docker stop $c" >/dev/null 2>&1 || true
|
|
done
|
|
fi
|
|
if ! "$ADB" -s "$SERIAL" shell "docker ps --format '{{.Names}}'" 2>/dev/null | grep -q "^${CONTAINER}$"; then
|
|
warn "node app not running — starting ${APP##*/}"
|
|
"$ADB" -s "$SERIAL" shell "arduino-app-cli app start $APP" >/dev/null 2>&1 || true
|
|
fi
|
|
ok "single-app invariant (default + only ${APP##*/} on ${PORTS[*]})"
|
|
|
|
# 3 · wait for the node daemon (auto-starts on boot; start it if not)
|
|
local n=0
|
|
until curl -s -m2 "$NODE_URL/health" -o /dev/null 2>/dev/null; do
|
|
n=$((n + 1))
|
|
if [ "$n" -eq 20 ]; then
|
|
warn "daemon not up after ~40s — starting the app"
|
|
"$ADB" -s "$SERIAL" shell "arduino-app-cli app start $APP" >/dev/null 2>&1 || true
|
|
fi
|
|
if [ "$n" -gt 90 ]; then warn "daemon never came up ($NODE_URL/health)"; return 1; fi
|
|
sleep 2
|
|
done
|
|
ok "node daemon healthy"
|
|
|
|
# 4 · re-register with the local API (restores the in-memory node binding)
|
|
local r
|
|
r="$(curl -s -m5 -X POST "$API/nodes/self-register" \
|
|
-H "x-fleet-secret: $FLEET_SECRET" -H 'content-type: application/json' \
|
|
-d "{\"kitId\":\"$KIT_ID\",\"claimCode\":\"$CLAIM_CODE\",\"url\":\"$NODE_URL\",\"token\":\"open-lan\"}" 2>/dev/null)"
|
|
if echo "$r" | grep -q '"url"'; then ok "re-registered with API (code $CLAIM_CODE)"
|
|
else warn "API self-register failed — is the API up at $API? ($r)"; return 1; fi
|
|
|
|
# 5 · sync the claim code onto the matrix so board + API agree
|
|
matrix_code && ok "matrix showing $CLAIM_CODE" || warn "could not set matrix code (relay :9999)"
|
|
log "recovered — bind in the UI with code $CLAIM_CODE"
|
|
}
|
|
|
|
watch_loop() {
|
|
log "watching board $SERIAL — recover on every (re)connect (Ctrl-C to stop)"
|
|
while true; do
|
|
"$ADB" -s "$SERIAL" wait-for-device
|
|
sleep 3 # let Linux + the App Lab app finish booting
|
|
recover_once || warn "recovery pass incomplete; will retry on next reconnect"
|
|
while adb_present; do sleep 2; done
|
|
log "board disconnected — waiting for re-plug"
|
|
done
|
|
}
|
|
|
|
case "${1:-}" in
|
|
--watch | -w) watch_loop ;;
|
|
*) recover_once ;;
|
|
esac
|