In the self-host model each team runs the stack locally with one board 1:1 over
USB, so the claim code (which only disambiguated boards in a shared pool) is
unnecessary. Add a LOCAL_MODE that auto-registers + auto-binds the board:
- API: `LOCAL_MODE` env + `GET /mode`; `POST /claim {local:true}` codelessly
binds the single board (`claimLocal`, rebinding a stale claim on a fresh
single-team stack); `POST /nodes/:team/disconnect` for the participant's own
release. Shared/LAN code path is unchanged.
- Frontend: `useLocalMode()` + `LocalBoardConnect` — detect → auto-bind →
"connected"; USB drop keeps the binding and auto-reconnects; explicit
Disconnect releases and waits for Reconnect. TeamRegistration swaps the code
card for it only when the API reports localMode.
- deploy/lan: compose sets `LOCAL_MODE=true` + `host.docker.internal:host-gateway`
(so the containerized API reaches the adb-forwarded board); `connect-board.sh`
forwards the tunnels and registers the board with the local stack (`--watch`
re-attaches on every reconnect).
Validated end-to-end through the Docker stack: mode → auto-bind → live status +
matrix mirror → disconnect → reconnect. 222 tests pass.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
95 lines
3.5 KiB
Bash
Executable File
95 lines
3.5 KiB
Bash
Executable File
#!/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
|