#!/usr/bin/env bash # connect-board.sh — attach the USB board to your LOCAL self-host stack. # # The stack runs in Docker on this laptop; the board is on USB, reached over adb. # This forwards the tunnels and registers the board with the containerized API # (which reaches it at host.docker.internal). In LOCAL_MODE the API auto-binds the # board to your team in the browser — no claim code. Run with --watch to re-attach # automatically on every (re)connect. # # Usage: # ./connect-board.sh # attach once # ./connect-board.sh --watch # attach on every (re)connect (leave it running) # # Env: SERIAL (auto-detected if unset) · WEB_URL (default http://localhost:8090) # FLEET_SECRET (default apess2026) · KIT_ID · NODE_URL set -uo pipefail WEB="${WEB_URL:-http://localhost:8090}" API="$WEB/api" FLEET_SECRET="${FLEET_SECRET:-apess2026}" KIT_ID="${KIT_ID:-crimson-node}" # How the API *container* reaches the board (adb binds host loopback; the API is # in Docker, so it uses the host gateway alias — see docker-compose extra_hosts). NODE_URL="${NODE_URL:-http://host.docker.internal:8080}" PORTS=(8080 9999) 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 "connect-board: adb not found in PATH"; exit 127; } log() { printf '\033[36m[connect]\033[0m %s\n' "$*"; } ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; } warn() { printf ' \033[33m!\033[0m %s\n' "$*"; } # First attached device unless SERIAL is pinned. detect_serial() { [ -n "${SERIAL:-}" ] && { echo "$SERIAL"; return; } "$ADB" devices | awk '/\tdevice$/{print $1; exit}' } connect_once() { local serial; serial="$(detect_serial)" [ -n "$serial" ] || { warn "no board attached over USB"; return 1; } ok "board $serial attached" # 1 · forward tunnels (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 · wait for the board daemon (App Lab app auto-starts on boot) local n=0 until curl -s -m2 http://127.0.0.1:8080/health -o /dev/null 2>/dev/null; do n=$((n + 1)); [ "$n" -gt 90 ] && { warn "board daemon never came up"; return 1; } sleep 2 done ok "board daemon healthy" # 3 · register with the LOCAL stack (LOCAL_MODE auto-binds it in the browser). # The claim code is irrelevant in local mode but the endpoint wants one. 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\":\"local\",\"url\":\"$NODE_URL\",\"token\":\"open-lan\"}" 2>/dev/null)" if echo "$r" | grep -q '"url"'; then ok "registered with the local stack" log "attached — it auto-connects in the browser (no code needed)" else warn "register failed — is the stack up? ($WEB) · $r" return 1 fi } watch_loop() { log "watching for the board — will attach on every (re)connect (Ctrl-C to stop)" while true; do "$ADB" wait-for-device sleep 3 # let Linux + the App Lab app finish booting connect_once || warn "attach incomplete; will retry on next reconnect" # wait until it disconnects while [ -n "$(detect_serial)" ]; do sleep 2; done log "board disconnected — waiting for re-plug" done } case "${1:-}" in --watch | -w) watch_loop ;; *) connect_once ;; esac