feat(deploy): Windows board-attach scripts + WiFi provisioning + Win11 docs

- connect-board.ps1 / .bat: PowerShell twin of connect-board.sh so teams on
  Windows 11 attach the USB Uno Q to the local self-host stack (adb forward +
  self-register; -Watch for auto-reattach; .bat bypasses execution policy)
- provision-wifi.sh: idempotent nmcli join over adb, venue WiFi baked as default
- provision-fleet.sh: run the WiFi step per board during fleet bring-up
- REQUIREMENTS.md: Windows 11 section (Docker+WSL2, native adb, USB driver),
  'does adb see your board?' check, and correct the WiFi reality (board needs
  venue WiFi for the cloud brain; we pre-join it)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-27 14:58:16 +02:00
co-authored by Claude Opus 4.8
parent 05207ba986
commit 77373c6fc7
5 changed files with 202 additions and 6 deletions
+8
View File
@@ -0,0 +1,8 @@
@echo off
REM connect-board.bat - double-click launcher for connect-board.ps1 on Windows.
REM Runs the PowerShell script with the execution policy bypassed for this run
REM only (nothing is changed system-wide). Pass -Watch to keep re-attaching:
REM connect-board.bat -Watch
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0connect-board.ps1" %*
echo.
pause
+105
View File
@@ -0,0 +1,105 @@
<#
.SYNOPSIS
Attach the USB Uno Q board to your LOCAL self-host stack (Windows 11).
.DESCRIPTION
PowerShell twin of connect-board.sh. Runs adb ON the Windows host so Docker
Desktop's host.docker.internal reaches the forwarded port. Forwards the tunnels
and registers the board with the containerized API; in LOCAL_MODE the API
auto-binds the board to your team in the browser — no claim code.
.EXAMPLE
.\connect-board.ps1 # attach once
.\connect-board.ps1 -Watch # re-attach on every (re)connect (leave running)
.NOTES
Env overrides: WEB_URL, FLEET_SECRET, KIT_ID, NODE_URL, SERIAL
If Windows blocks the script, run it as:
powershell -NoProfile -ExecutionPolicy Bypass -File .\connect-board.ps1
(or just double-click connect-board.bat).
#>
param([switch]$Watch)
$ErrorActionPreference = 'Stop'
$Web = if ($env:WEB_URL) { $env:WEB_URL } else { 'http://localhost:8090' }
$Api = "$Web/api"
$Secret = if ($env:FLEET_SECRET) { $env:FLEET_SECRET } else { 'apess2026' }
$KitId = if ($env:KIT_ID) { $env:KIT_ID } else { 'crimson-node' }
# How the API *container* reaches the board: adb binds the Windows host loopback,
# and Docker Desktop maps host.docker.internal to the Windows host.
$NodeUrl = if ($env:NODE_URL) { $env:NODE_URL } else { 'http://host.docker.internal:8080' }
$Ports = @(8080, 9999)
function Log($m) { Write-Host "[connect] $m" -ForegroundColor Cyan }
function Ok($m) { Write-Host " [OK] $m" -ForegroundColor Green }
function Warn($m) { Write-Host " [!] $m" -ForegroundColor Yellow }
if (-not (Get-Command adb -ErrorAction SilentlyContinue)) {
Write-Host "connect-board: adb not found on PATH. Install Android platform-tools and reopen the terminal." -ForegroundColor Red
exit 127
}
function Get-Serial {
if ($env:SERIAL) { return $env:SERIAL }
foreach ($line in (& adb devices)) {
if ($line -match '^(\S+)\s+device$') { return $Matches[1] }
}
return $null
}
function Connect-Once {
$serial = Get-Serial
if (-not $serial) { Warn 'no board attached over USB'; return $false }
Ok "board $serial attached"
# 1 - forward tunnels (they vanish on re-plug)
$existing = (& adb -s $serial forward --list) -join "`n"
foreach ($p in $Ports) {
if ($existing -notmatch "tcp:$p") { & adb -s $serial forward "tcp:$p" "tcp:$p" | Out-Null }
}
Ok "tunnels forwarded ($($Ports -join ' '))"
# 2 - wait for the board daemon (App Lab app auto-starts on boot)
$n = 0
while ($true) {
try { Invoke-WebRequest -Uri 'http://127.0.0.1:8080/health' -TimeoutSec 2 -UseBasicParsing | Out-Null; break }
catch {
$n++
if ($n -gt 90) { Warn 'board daemon never came up'; return $false }
Start-Sleep -Seconds 2
}
}
Ok 'board daemon healthy'
# 3 - register with the LOCAL stack (LOCAL_MODE auto-binds it in the browser)
$body = @{ kitId = $KitId; claimCode = 'local'; url = $NodeUrl; token = 'open-lan' } | ConvertTo-Json -Compress
try {
$r = Invoke-RestMethod -Uri "$Api/nodes/self-register" -Method Post `
-Headers @{ 'x-fleet-secret' = $Secret; 'content-type' = 'application/json' } `
-Body $body -TimeoutSec 5
if ($r.url) {
Ok 'registered with the local stack'
Log 'attached — it auto-connects in the browser (no code needed)'
return $true
}
Warn "register returned an unexpected response: $($r | ConvertTo-Json -Compress)"
return $false
} catch {
Warn "register failed — is the stack up? ($Web) · $($_.Exception.Message)"
return $false
}
}
if ($Watch) {
Log 'watching for the board — will attach on every (re)connect (Ctrl-C to stop)'
while ($true) {
& adb wait-for-device | Out-Null
Start-Sleep -Seconds 3 # let Linux + the App Lab app finish booting
if (-not (Connect-Once)) { Warn 'attach incomplete; will retry on next reconnect' }
while (Get-Serial) { Start-Sleep -Seconds 2 }
Log 'board disconnected — waiting for re-plug'
}
} else {
[void](Connect-Once)
}
+38 -6
View File
@@ -11,10 +11,14 @@ Do these **before you arrive** so we spend the session building, not installing.
## How it runs (so the prerequisites make sense) ## How it runs (so the prerequisites make sense)
Each team runs the **whole platform on its own laptop** — a small Docker stack (web + API) that Each team runs the **whole platform on its own laptop** — a small Docker stack (web + API) that
comes up with **one command**. Your **Arduino Uno Q** plugs into that same laptop over **USB**. comes up with **one command**. Your **Arduino Uno Q** plugs into that same laptop over **USB**.
Everything is **localhost**: the browser, the API, and the board all talk on your machine. Once Everything on your **laptop** is **localhost**: the browser, the API, and the board all talk on your
the stack is up and the board is plugged in, it **auto-connects to your team — no codes, no machine. Once the stack is up and the board is plugged in, it **auto-connects to your team — no
accounts, nothing over the network.** (WiFi isn't used during the workshop; it's only for a future codes, no accounts.** The one thing that leaves the box: the **board** reaches its **AI cloud brain
step that registers boards with our production cloud.) over the venue WiFi** — but *we* pre-join each board to that network before you get it, so there's
nothing for you to set up.
> **Most teams are on Windows 11** (a few Macs). Both work the same way; the only difference is the
> command you run to attach the board — see the Windows / macOS notes below.
So each team needs **one "board laptop"** with a few things pre-installed. Extra teammates just So each team needs **one "board laptop"** with a few things pre-installed. Extra teammates just
need a browser pointed at that laptop. need a browser pointed at that laptop.
@@ -32,6 +36,8 @@ need a browser pointed at that laptop.
- **Arduino Uno Q (4 GB)** board + **USB-C cable** — one per team. - **Arduino Uno Q (4 GB)** board + **USB-C cable** — one per team.
- **ADXL355 accelerometer(s)** + wiring — the FabLab kit. - **ADXL355 accelerometer(s)** + wiring — the FabLab kit.
- **Cloud AI access** — baked into the board app. **No Anthropic/Claude account needed.** - **Cloud AI access** — baked into the board app. **No Anthropic/Claude account needed.**
- **Boards pre-joined to the venue WiFi** — the board uses it only to reach the AI cloud; you don't
configure any network.
- The **web app** itself (you run it locally from the bundle below). - The **web app** itself (you run it locally from the bundle below).
--- ---
@@ -52,6 +58,30 @@ need a browser pointed at that laptop.
``` ```
(Also pre-pull the App Lab base image on the board — it's fetched on first Run.) (Also pre-pull the App Lab base image on the board — it's fetched on first Run.)
### On Windows 11 (most teams)
- **Docker Desktop** with the **WSL2 backend** (enable it in the installer). `docker run hello-world`.
- **adb** — download **Android SDK platform-tools for Windows**, unzip it, and add the folder to your
**PATH** (so `adb version` works in a new terminal). *Run adb on Windows itself — not inside WSL.*
- **Git for Windows** — gives you `git` + `curl` (used by the stack).
- **Uno Q USB driver** — plug the board in; Windows usually installs a driver automatically. It's
working when **`adb devices`** lists the board as `device` (see the check below). If it shows
nothing or `unauthorized`, reinstall the driver / re-plug and accept any prompt on the board.
- To attach the board you'll run **`connect-board.bat`** (double-click) or **`connect-board.ps1`** —
both live in `deploy\lan\`.
### On macOS (a few teams)
- Docker Desktop, `brew install android-platform-tools` (adb), `git`. Attach with
`./deploy/lan/connect-board.sh`.
### First check: does adb see your board?
The #1 thing to get right up front. Plug the Uno Q in over USB and run:
```
adb devices
```
You want a line ending in **`device`**, e.g. `65301572 device`. If it's empty, `offline`, or
`unauthorized`: re-plug, try a different USB port/cable, and on Windows reinstall the USB driver.
**Get this working before the day** — everything else assumes adb sees the board.
## Everyone else on the team ## Everyone else on the team
- A **current browser** (Chrome/Edge recommended; Firefox works). That's it — you'll open the board - A **current browser** (Chrome/Edge recommended; Firefox works). That's it — you'll open the board
laptop's local URL. laptop's local URL.
@@ -62,8 +92,10 @@ need a browser pointed at that laptop.
1. **Bring the stack up:** `cd deploy/lan && docker compose up -d` → open **`http://localhost:8090/`**. 1. **Bring the stack up:** `cd deploy/lan && docker compose up -d` → open **`http://localhost:8090/`**.
2. **Get the board app:** in Team Registration, click **Download the board app** (served by your own 2. **Get the board app:** in Team Registration, click **Download the board app** (served by your own
stack), then on the Uno Q: **App Lab → Import an app → pick the zip → Run.** `[instructor: confirm the App Lab access flow for the room]` stack), then on the Uno Q: **App Lab → Import an app → pick the zip → Run.** `[instructor: confirm the App Lab access flow for the room]`
3. **Attach the board:** plug the Uno Q into the board laptop over USB and run 3. **Attach the board:** plug the Uno Q into the board laptop over USB, then:
**`./deploy/lan/connect-board.sh`** (or `--watch` to keep it auto-attaching). - **Windows:** double-click **`deploy\lan\connect-board.bat`** (or run `.\connect-board.ps1 -Watch`
to keep it auto-attaching on re-plug).
- **macOS:** run **`./deploy/lan/connect-board.sh`** (or `--watch`).
4. **It just connects:** type your team name and the board **auto-binds to your team** — no code. 4. **It just connects:** type your team name and the board **auto-binds to your team** — no code.
Unplug/replug is handled automatically; a **Disconnect / Reconnect** control is there if you need it. Unplug/replug is handled automatically; a **Disconnect / Reconnect** control is there if you need it.
5. **Build:** walk Modules 1–3, submit your Agent Design Document. 5. **Build:** walk Modules 1–3, submit your Agent Design Document.
+10
View File
@@ -38,6 +38,16 @@ fi
provision() { # kit serial -> 0 ok / 1 fail provision() { # kit serial -> 0 ok / 1 fail
local kit="$1" serial="$2" env="$ENVDIR/$1.env" local kit="$1" serial="$2" env="$ENVDIR/$1.env"
[ -r "$env" ] || { echo " ! no env file for $kit ($env)"; return 1; } [ -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" 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 "$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" push "$HERE/apess-selfregister.sh" /home/arduino/ >/dev/null 2>&1 || return 1
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# provision-wifi.sh — join a Uno Q board to the workshop WiFi over adb and persist
# it. NetworkManager saves the connection profile, so the board auto-reconnects on
# every boot. The board reaches the cloud brain (api.anthropic.com) NAT'd out
# through this WiFi, so every workshop board needs it.
#
# The venue network is baked in as the default (override with WIFI_SSID/WIFI_PASS).
#
# Usage:
# ./provision-wifi.sh # first attached board
# ./provision-wifi.sh <adb-serial> # a specific board
# # every attached board at once:
# for s in $(adb devices | awk 'NR>1 && $2=="device"{print $1}'); do ./provision-wifi.sh "$s"; done
set -uo pipefail
# ── workshop WiFi (FabLab Torino) — override per venue with WIFI_SSID / WIFI_PASS ──
WIFI_SSID="${WIFI_SSID:-Fablab_Torino}"
WIFI_PASS="${WIFI_PASS:-Fablab.Torino!}"
S="${1:-$(adb devices 2>/dev/null | awk '/\tdevice$/{print $1; exit}')}"
[ -n "$S" ] || { echo "provision-wifi: no board attached over USB" >&2; exit 1; }
echo "==> [$S] joining WiFi '$WIFI_SSID'"
# Idempotent: if a saved profile already exists, just bring it up; otherwise scan
# and create it (a persistent NetworkManager profile that auto-reconnects on boot).
adb -s "$S" shell "nmcli radio wifi on >/dev/null 2>&1; sleep 1
if nmcli -t -f NAME connection show 2>/dev/null | grep -qx '$WIFI_SSID'; then
nmcli connection up '$WIFI_SSID'
else
nmcli device wifi rescan >/dev/null 2>&1; sleep 4
nmcli device wifi connect '$WIFI_SSID' password '$WIFI_PASS'
fi" 2>&1 | sed 's/^/ /'
# verify link + that the cloud is reachable through it
adb -s "$S" shell 'ip -brief addr show wlan0 2>/dev/null | sed "s/^/ wlan0: /"'
if adb -s "$S" shell 'getent hosts api.anthropic.com >/dev/null 2>&1'; then
echo " cloud DNS: resolves ✓ — board can reach the agent brain"
else
echo " cloud DNS: FAILS — check WiFi coverage / credentials" >&2
exit 1
fi