#!/usr/bin/env bash # Runs ON the Uno Q. Announces this board into APESS's unclaimed pool so an # attendee can claim it (kit + code) with no operator in the loop — the # board-side half of the "preloaded + self-register + claim" onboarding. # # It pairs locally for a bearer token, discovers the board's LAN IP, and POSTs # {kitId, url, token, claimCode} to APESS /nodes/self-register (authorized by # the shared fleet secret, NOT the admin code). Idempotent + retrying: safe to # run on every boot and on a timer (re-announces if the DHCP lease changes). # # Config: /home/arduino/.zeroclaw/apess-node.env (see apess-node.env.example) — # KIT_ID, CLAIM_CODE, FLEET_SECRET, APESS_URL [, GATEWAY_PORT] set -euo pipefail ENV_FILE="${APESS_NODE_ENV:-/home/arduino/.zeroclaw/apess-node.env}" ZEROCLAW="${ZEROCLAW_BIN:-/home/arduino/zeroclaw}" [ -r "$ENV_FILE" ] || { echo "missing $ENV_FILE" >&2; exit 1; } # shellcheck disable=SC1090 . "$ENV_FILE" : "${KIT_ID:?KIT_ID required}" : "${CLAIM_CODE:?CLAIM_CODE required}" : "${FLEET_SECRET:?FLEET_SECRET required}" : "${APESS_URL:?APESS_URL required}" PORT="${GATEWAY_PORT:-8080}" GW="http://127.0.0.1:${PORT}" log() { echo "[selfregister] $*"; } # Wait for the local daemon to be up (systemd orders us After=, but be safe). for _ in $(seq 1 60); do curl -sf --max-time 3 "$GW/health" >/dev/null 2>&1 && break sleep 2 done # Pair locally for a bearer token (the token stays server-side once APESS has it). # Under LAN-open setup (require_pairing=false) the gateway mints no code — the # open board ignores auth, so announce with a placeholder token; lockdown later # establishes real pairing (and, if wanted, re-announces a valid token). CODE=$("$ZEROCLAW" gateway get-paircode --new --port "$PORT" 2>/dev/null | grep -oE '[0-9]{6}' | head -1 || true) TOKEN="" if [ -n "$CODE" ]; then TOKEN=$(curl -s -X POST "$GW/pair" -H "X-Pairing-Code: ${CODE}" \ | python3 -c 'import sys,json;print(json.load(sys.stdin).get("token",""))' 2>/dev/null || true) fi TOKEN="${TOKEN:-open-lan}" # placeholder while the board is LAN-open # The LAN IP participants (and APESS) reach; fall back to localhost for adb-only. IP=$(ip -4 -o addr show 2>/dev/null | grep -oE 'inet [0-9.]+' | grep -v '127.0.0.1' \ | awk '{print $2}' | head -1) URL="http://${IP:-127.0.0.1}:${PORT}" payload=$(python3 - "$KIT_ID" "$URL" "$TOKEN" "$CLAIM_CODE" <<'PY' import json, sys kit, url, token, code = sys.argv[1:5] print(json.dumps({"kitId": kit, "url": url, "token": token, "claimCode": code})) PY ) # Announce, retrying until APESS is reachable (it may boot after the boards). for attempt in $(seq 1 30); do if curl -sf -X POST "${APESS_URL%/}/nodes/self-register" \ -H "x-fleet-secret: ${FLEET_SECRET}" -H 'content-type: application/json' \ -d "$payload" >/dev/null; then log "announced ${KIT_ID} at ${URL}" exit 0 fi log "APESS unreachable (attempt ${attempt}/30) — retrying" sleep 10 done echo "could not reach APESS at ${APESS_URL}" >&2 exit 1