chore(uno-q): auto-recover the board on USB re-plug
Replace the obsolete host-daemon recover.sh with the current App-Lab-node flow: re-forward the adb tunnels (:8080 gateway, :9999 matrix relay), wait for the daemon (start the app if needed), re-register the node with the local API, and sync a fixed claim code onto the matrix so the board + API agree. Adds a `--watch` mode and a launchd agent (com.redclaw.apess-board-recover.plist) that runs it on every reconnect — so a re-plug heals itself, no manual step. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1a39457940
commit
e76e27f06d
@@ -0,0 +1,51 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
launchd agent: auto-run the board recovery watcher.
|
||||||
|
|
||||||
|
It keeps `recover.sh --watch` alive, which sits on `adb wait-for-device` and
|
||||||
|
re-forwards tunnels + re-registers the node with the local API every time the
|
||||||
|
Uno Q reconnects — so a USB re-plug heals itself with no manual step.
|
||||||
|
|
||||||
|
Install (per-user):
|
||||||
|
cp deploy/uno-q/com.redclaw.apess-board-recover.plist ~/Library/LaunchAgents/
|
||||||
|
launchctl load ~/Library/LaunchAgents/com.redclaw.apess-board-recover.plist
|
||||||
|
Stop / uninstall:
|
||||||
|
launchctl unload ~/Library/LaunchAgents/com.redclaw.apess-board-recover.plist
|
||||||
|
Logs: ~/Library/Logs/apess-board-recover.log
|
||||||
|
|
||||||
|
Edit the paths below if your checkout lives elsewhere.
|
||||||
|
-->
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.redclaw.apess-board-recover</string>
|
||||||
|
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/bin/bash</string>
|
||||||
|
<string>/Users/quantum/projects/apress/deploy/uno-q/recover.sh</string>
|
||||||
|
<string>--watch</string>
|
||||||
|
</array>
|
||||||
|
|
||||||
|
<key>EnvironmentVariables</key>
|
||||||
|
<dict>
|
||||||
|
<!-- launchd's PATH is minimal; add Homebrew + platform-tools for adb/python3/curl. -->
|
||||||
|
<key>PATH</key>
|
||||||
|
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
||||||
|
<!-- Override any of recover.sh's knobs here if needed, e.g.: -->
|
||||||
|
<!-- <key>CLAIM_CODE</key><string>7777</string> -->
|
||||||
|
</dict>
|
||||||
|
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
<key>ThrottleInterval</key>
|
||||||
|
<integer>10</integer>
|
||||||
|
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>/Users/quantum/Library/Logs/apess-board-recover.log</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>/Users/quantum/Library/Logs/apess-board-recover.log</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
+90
-67
@@ -1,81 +1,104 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# recover.sh — one-command recovery for the APESS Uno Q demo node after a USB drop.
|
# recover.sh — restore the Uno Q board's LOCAL DEV binding after a USB re-plug.
|
||||||
#
|
#
|
||||||
# On a disconnect the daemon/llama/bridge die and the cloud token (env-only) is lost.
|
# On re-plug the Uno Q's Linux reboots and the laptop's adb tunnels vanish, so
|
||||||
# This re-tunnels, relaunches the supervisor WITH the token in its environment,
|
# the local API (and the LED-matrix mirror) lose the board; the board also boots
|
||||||
# restarts the matrix bridge app, and verifies the whole chain end-to-end.
|
# 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. wait for the node daemon (start the App Lab app if it isn't up)
|
||||||
|
# 3. re-register the node with the local API (restores its in-memory binding)
|
||||||
|
# 4. scroll a fixed claim code on the matrix so the board + API agree
|
||||||
#
|
#
|
||||||
# Secrets are read from the environment — NEVER hardcoded here. Export first:
|
# Usage:
|
||||||
# export ANTHROPIC_OAUTH_TOKEN=sk-ant-oat01-... # required: cloud brain
|
# ./recover.sh # one recovery pass, then exit
|
||||||
# export NODE_TOKEN=zc_... # optional: end-to-end verify
|
# ./recover.sh --watch # run forever: recover on every (re)connect
|
||||||
# ./recover.sh
|
|
||||||
#
|
#
|
||||||
# Env knobs: SERIAL (default 65301572), the two tokens above.
|
# The App Lab node (apess-onboard) carries its own baked cloud token, so unlike
|
||||||
set -u
|
# 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}"
|
SERIAL="${SERIAL:-65301572}"
|
||||||
A(){ adb -s "$SERIAL" "$@"; }
|
API="${API_URL:-http://127.0.0.1:3000}"
|
||||||
S(){ adb -s "$SERIAL" shell "$@"; }
|
FLEET_SECRET="${FLEET_SECRET:-apess2026}"
|
||||||
ok(){ printf ' \033[32m✓\033[0m %s\n' "$*"; }
|
KIT_ID="${KIT_ID:-crimson-node}"
|
||||||
bad(){ printf ' \033[31m✗\033[0m %s\n' "$*"; }
|
CLAIM_CODE="${CLAIM_CODE:-7777}"
|
||||||
step(){ printf '\n\033[1m%s\033[0m\n' "$*"; }
|
NODE_URL="${NODE_URL:-http://127.0.0.1:8080}"
|
||||||
|
APP="${APP:-/home/arduino/ArduinoApps/apess-onboard}"
|
||||||
|
PORTS=(8080 9999)
|
||||||
|
|
||||||
step "0· Preconditions"
|
# Resolve adb even under launchd's minimal PATH.
|
||||||
if ! adb devices | grep -q "^${SERIAL}[[:space:]]*device"; then
|
ADB="$(command -v adb || true)"
|
||||||
bad "board $SERIAL not attached — re-plug USB, then re-run"; exit 1
|
for c in /opt/homebrew/bin/adb /usr/local/bin/adb "$HOME/Library/Android/sdk/platform-tools/adb"; do
|
||||||
fi
|
[ -n "$ADB" ] && break
|
||||||
ok "board $SERIAL attached"
|
[ -x "$c" ] && ADB="$c"
|
||||||
[ -n "${ANTHROPIC_OAUTH_TOKEN:-}" ] || { bad "ANTHROPIC_OAUTH_TOKEN not set — cloud brain will fail. export it and re-run"; exit 1; }
|
done
|
||||||
ok "cloud token present in env"
|
[ -n "$ADB" ] || { echo "recover: adb not found in PATH"; exit 127; }
|
||||||
|
|
||||||
step "1· Tunnel"
|
log() { printf '\033[36m[recover]\033[0m %s\n' "$*"; }
|
||||||
A forward tcp:8080 tcp:8080 >/dev/null && ok "adb forward :8080 → laptop localhost:8080"
|
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
|
||||||
|
warn() { printf ' \033[33m!\033[0m %s\n' "$*"; }
|
||||||
|
|
||||||
step "2· Stop stale supervisor + daemons (preserve llama)"
|
adb_present() { "$ADB" devices | grep -q "^${SERIAL}[[:space:]].*device$"; }
|
||||||
S 'for p in $(ps -C zeroclaw-supervisor -o pid= 2>/dev/null); do kill -9 $p 2>/dev/null; done
|
|
||||||
for p in $(ps -C zeroclaw -o pid= 2>/dev/null); do kill -9 $p 2>/dev/null; done
|
|
||||||
rm -f /home/arduino/.zc-supervisor.lock; sleep 2
|
|
||||||
echo " daemons left: $(ps -C zeroclaw -o pid= 2>/dev/null | wc -l)"'
|
|
||||||
|
|
||||||
step "3· Relaunch supervisor WITH token env (env-only, never on disk)"
|
matrix_code() {
|
||||||
S "export ANTHROPIC_OAUTH_TOKEN='$ANTHROPIC_OAUTH_TOKEN'; \
|
python3 - "$CLAIM_CODE" <<'PY' 2>/dev/null
|
||||||
export ZEROCLAW_providers__models__anthropic__max__api_key='$ANTHROPIC_OAUTH_TOKEN'; \
|
import socket, sys
|
||||||
setsid nohup /home/arduino/zeroclaw-supervisor.sh >/dev/null 2>&1 </dev/null & sleep 2; echo done" >/dev/null
|
s = socket.create_connection(('127.0.0.1', 9999), timeout=3)
|
||||||
S 'pgrep -f "[z]eroclaw-supervisor" >/dev/null' && ok "supervisor relaunched" || bad "supervisor did NOT start"
|
s.sendall(f"text {sys.argv[1]}\n".encode()); s.recv(16); s.close()
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
step "4· Matrix bridge app (start only if down)"
|
recover_once() {
|
||||||
if [ "$(S 'printf "ping\n" | timeout 4 nc 127.0.0.1 9999 2>/dev/null')" = "pong" ]; then
|
adb_present || { warn "board $SERIAL not connected"; return 1; }
|
||||||
ok "bridge already running"
|
|
||||||
else
|
|
||||||
S 'cd ~/ArduinoApps/uno-q-bridge && TMPDIR=/tmp arduino-app-cli app start ~/ArduinoApps/uno-q-bridge 2>&1 | tail -1'
|
|
||||||
fi
|
|
||||||
|
|
||||||
step "5· Wait for services"
|
# 1 · re-forward tunnels (they vanish on re-plug)
|
||||||
for i in $(seq 1 30); do
|
for p in "${PORTS[@]}"; do
|
||||||
L=$(S 'curl -sf -m3 http://127.0.0.1:8083/health >/dev/null 2>&1 && echo 1 || echo 0')
|
"$ADB" -s "$SERIAL" forward --list 2>/dev/null | grep -q "tcp:$p" \
|
||||||
D=$(S 'curl -sf -m3 http://127.0.0.1:8080/health >/dev/null 2>&1 && echo 1 || echo 0')
|
|| "$ADB" -s "$SERIAL" forward "tcp:$p" "tcp:$p" >/dev/null
|
||||||
printf '\r [%02d] llama=%s daemon=%s ' "$i" "$L" "$D"
|
done
|
||||||
[ "$D" = 1 ] && break; sleep 6
|
ok "tunnels forwarded (${PORTS[*]})"
|
||||||
done; echo
|
|
||||||
[ "$L" = 1 ] && ok "llama :8083 healthy" || bad "llama :8083 DOWN (cold load can take 3–5 min; re-check)"
|
|
||||||
[ "$D" = 1 ] && ok "daemon :8080 healthy" || { bad "daemon :8080 DOWN"; exit 1; }
|
|
||||||
|
|
||||||
step "6· Bridge (matrix responder)"
|
# 2 · wait for the node daemon (auto-starts on boot; start it if not)
|
||||||
P=$(S 'printf "ping\n" | timeout 4 nc 127.0.0.1 9999 2>/dev/null')
|
local n=0
|
||||||
[ "$P" = "pong" ] && ok "bridge :9999 responds (ping→pong)" || bad "bridge :9999 not responding — re-run step 4"
|
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"
|
||||||
|
|
||||||
step "7· End-to-end: demo agent = cloud sonnet + matrix fires"
|
# 3 · re-register with the local API (restores the in-memory node binding)
|
||||||
if [ -n "${NODE_TOKEN:-}" ]; then
|
local r
|
||||||
S 'printf "matrix 0\n" | timeout 5 nc 127.0.0.1 9999 >/dev/null 2>&1'
|
r="$(curl -s -m5 -X POST "$API/nodes/self-register" \
|
||||||
R=$(curl -s -m 30 -X POST "http://127.0.0.1:8080/webhook?agent=demo" \
|
-H "x-fleet-secret: $FLEET_SECRET" -H 'content-type: application/json' \
|
||||||
-H "Authorization: Bearer $NODE_TOKEN" -H 'Content-Type: application/json' \
|
-d "{\"kitId\":\"$KIT_ID\",\"claimCode\":\"$CLAIM_CODE\",\"url\":\"$NODE_URL\",\"token\":\"open-lan\"}" 2>/dev/null)"
|
||||||
-d '{"message":"Show the rain animation on the LED matrix"}')
|
if echo "$r" | grep -q '"url"'; then ok "re-registered with API (code $CLAIM_CODE)"
|
||||||
echo "$R" | grep -q "claude-sonnet-5" && ok "agent=demo on claude-sonnet-5" || bad "agent NOT on sonnet — token may not have loaded: $R"
|
else warn "API self-register failed — is the API up at $API? ($r)"; return 1; fi
|
||||||
M=$(S "docker logs --since 40s uno-q-bridge-main-1 2>&1 | grep -c \"parts=\['matrix', '1'\]\"")
|
|
||||||
[ "${M:-0}" -ge 1 ] && ok "matrix_pattern fired (rain)" || bad "matrix did not change"
|
|
||||||
else
|
|
||||||
echo " (NODE_TOKEN unset — skipping authenticated end-to-end check)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
step "Recovery complete."
|
# 4 · sync the claim code onto the matrix so board + API agree
|
||||||
echo " Voice proxy (laptop): if it was running it auto-recovers via the re-armed tunnel."
|
matrix_code && ok "matrix showing $CLAIM_CODE" || warn "could not set matrix code (relay :9999)"
|
||||||
echo " If not running: NODE_URL=http://127.0.0.1:8080 NODE_TOKEN=\$NODE_TOKEN python3 deploy/voice-client/serve.py 8090"
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user