#!/usr/bin/env bash # Fleet onboarding provisioner. For each board in an assignments file, pushes # its per-kit env (apess-node.env) + the self-register script over adb, enables # the self-register systemd timer (root boards) or an equivalent cron (no-root, # the default), and flashes the default boot animation onto the MCU. Run after # gen-kit-codes.sh. # # ./provision-fleet.sh [kit-codes-dir] # # assignments.csv — map each kit to its board's adb serial (`adb devices`): # kit,serial # KIT-01,65301572 # KIT-02,88a1b2c3 # kit-codes-dir default: ./kit-codes (expects env/KIT-NN.env from gen-kit-codes.sh) # # Env: # MODE=cron (default, no root) | systemd (needs a root-capable board) # FLASH_DEFAULT=1 (default; 0 skips flashing the boot animation) # FLASH_SKETCH= (default: sketches/matrix_rain) set -uo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" ASSIGN="${1:?usage: provision-fleet.sh [kit-codes-dir]}" KCDIR="${2:-./kit-codes}" ENVDIR="$KCDIR/env" MODE="${MODE:-cron}" FLASH_DEFAULT="${FLASH_DEFAULT:-1}" FLASH_SKETCH="${FLASH_SKETCH:-$HERE/sketches/matrix_rain}" [ -r "$ASSIGN" ] || { echo "no assignments file: $ASSIGN" >&2; exit 1; } [ -d "$ENVDIR" ] || { echo "no env dir: $ENVDIR (run gen-kit-codes.sh first)" >&2; exit 1; } [ -r "$HERE/apess-selfregister.sh" ] || { echo "missing apess-selfregister.sh next to this script" >&2; exit 1; } if [ "$FLASH_DEFAULT" = 1 ]; then [ -x "$HERE/flash-sketch.sh" ] || { echo "FLASH_DEFAULT=1 but flash-sketch.sh is missing/not executable (FLASH_DEFAULT=0 to skip)" >&2; exit 1; } [ -d "$FLASH_SKETCH" ] || { echo "FLASH_DEFAULT=1 but sketch dir not found: $FLASH_SKETCH" >&2; exit 1; } fi provision() { # kit serial -> 0 ok / 1 fail local kit="$1" serial="$2" env="$ENVDIR/$1.env" [ -r "$env" ] || { echo " ! no env file for $kit ($env)"; return 1; } # workshop WiFi so the board can reach the cloud brain (persisted by NetworkManager). # Baked default is FabLab Torino; override with WIFI_SSID/WIFI_PASS. Best-effort. if [ -x "$HERE/provision-wifi.sh" ]; then if "$HERE/provision-wifi.sh" "$serial" >/dev/null 2>&1; then echo " ok — WiFi joined (${WIFI_SSID:-Fablab_Torino})" else echo " ! WiFi join failed — check creds/coverage (agent cloud brain needs it)" fi fi adb -s "$serial" shell 'mkdir -p /home/arduino/.zeroclaw' >/dev/null 2>&1 || return 1 adb -s "$serial" push "$env" /home/arduino/.zeroclaw/apess-node.env >/dev/null 2>&1 || return 1 adb -s "$serial" push "$HERE/apess-selfregister.sh" /home/arduino/ >/dev/null 2>&1 || return 1 adb -s "$serial" shell 'chmod +x /home/arduino/apess-selfregister.sh' >/dev/null 2>&1 || return 1 # persistence: systemd timer (root) or cron (no root) local persisted="" if [ "$MODE" = systemd ]; then adb -s "$serial" push "$HERE/systemd/apess-selfregister.service" /tmp/ >/dev/null 2>&1 adb -s "$serial" push "$HERE/systemd/apess-selfregister.timer" /tmp/ >/dev/null 2>&1 if adb -s "$serial" shell 'sudo cp /tmp/apess-selfregister.service /tmp/apess-selfregister.timer /etc/systemd/system/ \ && sudo systemctl enable --now apess-selfregister.timer' >/dev/null 2>&1; then persisted="systemd timer" else echo " ! systemd enable failed (no root?) — falling back to cron" fi fi if [ -z "$persisted" ]; then adb -s "$serial" shell 'setsid /home/arduino/apess-selfregister.sh >/tmp/selfreg.log 2>&1 /dev/null 2>&1 || true adb -s "$serial" shell '(crontab -l 2>/dev/null | grep -v apess-selfregister.sh; \ echo "@reboot /home/arduino/apess-selfregister.sh"; \ echo "*/5 * * * * /home/arduino/apess-selfregister.sh") | crontab -' >/dev/null 2>&1 || true persisted="cron: @reboot + every 5 min" fi echo " ok — onboarding ($persisted)" # modalities — reload-watcher (applies browser dashboard config edits, e.g. a # team enabling its Telegram bot) + the lockdown script. Best-effort. if [ -r "$HERE/zeroclaw-reload-watcher.sh" ]; then adb -s "$serial" push "$HERE/zeroclaw-reload-watcher.sh" /home/arduino/ >/dev/null 2>&1 adb -s "$serial" push "$HERE/zeroclaw-lockdown.sh" /home/arduino/ >/dev/null 2>&1 adb -s "$serial" shell 'chmod +x /home/arduino/zeroclaw-reload-watcher.sh /home/arduino/zeroclaw-lockdown.sh' >/dev/null 2>&1 adb -s "$serial" shell 'pgrep -f "[z]eroclaw-reload-watcher.sh" >/dev/null 2>&1 || \ setsid nohup /home/arduino/zeroclaw-reload-watcher.sh >/tmp/zc-reload.log 2>&1 /dev/null 2>&1 adb -s "$serial" shell '(crontab -l 2>/dev/null | grep -v zeroclaw-reload-watcher.sh; \ echo "@reboot /home/arduino/zeroclaw-reload-watcher.sh") | crontab -' >/dev/null 2>&1 || true echo " ok — modalities (reload-watcher up; lockdown staged)" fi # participant workspace — bind ~/sketches + ~/ArduinoApps + ~/Arduino/libraries # into the app container (/app/workspace) so the agent can read/fix student code. # mount(2) is root-only and can't fall back to cron, so this is systemd-only, # best-effort. See USER-WORKSPACE.md. if [ -r "$HERE/mount-user-workspace.sh" ]; then adb -s "$serial" push "$HERE/mount-user-workspace.sh" /home/arduino/ >/dev/null 2>&1 adb -s "$serial" shell 'chmod +x /home/arduino/mount-user-workspace.sh' >/dev/null 2>&1 adb -s "$serial" push "$HERE/systemd/apess-user-workspace.service" /tmp/ >/dev/null 2>&1 if adb -s "$serial" shell 'sudo -n cp /tmp/apess-user-workspace.service /etc/systemd/system/ \ && sudo -n systemctl daemon-reload && sudo -n systemctl enable --now apess-user-workspace.service' >/dev/null 2>&1; then adb -s "$serial" shell 'TMPDIR=/tmp arduino-app-cli app restart /home/arduino/ArduinoApps/apess-onboard >/dev/null 2>&1 || true' >/dev/null 2>&1 echo " ok — participant workspace mounted (/app/workspace)" else echo " ! workspace mount needs root (sudo -n failed) — staged; enable apess-user-workspace.service on the board" fi fi # default skills — install every bundled skill (the comprehensive arduino-uno-q # reference + the fork's granular set) into every agent's workspace, so each # node has them by default. Best-effort. if [ -x "$HERE/push-skill.sh" ] && [ -d "$HERE/skills" ]; then if "$HERE/push-skill.sh" "$serial" "$HERE/skills" >/dev/null 2>&1; then echo " ok — skills (all bundled → agent workspaces)" else echo " ! skill install failed (onboarding still ok)" fi fi # default boot animation — best-effort (a flash hiccup doesn't fail onboarding) if [ "$FLASH_DEFAULT" = 1 ]; then if "$HERE/flash-sketch.sh" "$serial" "$FLASH_SKETCH" >/dev/null 2>&1; then echo " ok — boot animation ($(basename "$FLASH_SKETCH"))" else echo " ! boot-animation flash failed (onboarding still ok)" fi fi return 0 } ok=0 fail=0 while IFS=',' read -r kit serial _rest || [ -n "$kit" ]; do kit="$(printf '%s' "$kit" | tr -d ' \r')" serial="$(printf '%s' "$serial" | tr -d ' \r')" [ -z "$kit" ] && continue [ "$kit" = "kit" ] && continue # header case "$kit" in \#*) continue ;; esac # comment echo "==> $kit ($serial)" if [ -z "$serial" ]; then echo " ! no serial — skipping"; fail=$((fail + 1)); continue; fi if provision "$kit" "$serial"; then ok=$((ok + 1)); else echo " FAILED"; fail=$((fail + 1)); fi done < "$ASSIGN" anim="skipped"; [ "$FLASH_DEFAULT" = 1 ] && anim="$(basename "$FLASH_SKETCH")" echo "----" echo "provisioned $ok board(s), $fail failed/skipped (mode: $MODE, boot-animation: $anim)" [ "$fail" -eq 0 ]