fix(web): name the Arduino Uno Q (4 GB) as the workshop node

Front-end still told the old UNO R4 WiFi story (Cortex-M4 / Renesas
RA4M1 / ~6 MB binary / UART→ESP32-S3 / browser-direct Web Serial).
The actual workshop node is the dual-brain Uno Q: ZeroClaw runs on
the quad-A53 Linux side, hosts a local Qwen model, and flashes the
on-board STM32U585 MCU it drives.

- Landing: hero, hardware spec tile, kit pickup, Stack cards, prereqs
- Lecture: Movement 3 (ZeroClaw) rewritten to the Uno Q architecture
- serial.ts + callers: device identity strings uno-r4-wifi -> uno-q
  (Web Serial transport mechanism unchanged)
- README stack line
- tests updated to match

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-06 13:02:27 -07:00
co-authored by Claude Opus 4.8
parent 1ad6da64f7
commit 6688ced6b3
8 changed files with 20 additions and 20 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ Guides 60+ PhD students across 15 teams from kit unboxing → ZeroClaw firmware
- **React Router v7** for client routing - **React Router v7** for client routing
- **Zustand** for cross-screen session state (sessionStorage-persisted) - **Zustand** for cross-screen session state (sessionStorage-persisted)
- **Vitest** + React Testing Library + jsdom for strict TDD - **Vitest** + React Testing Library + jsdom for strict TDD
- **Web Serial API** for live USB serial to Arduino UNO R4 WiFi - **Web Serial API** for live USB serial to the Arduino Uno Q (mock fallback without hardware)
## Conventions ## Conventions
+1 -1
View File
@@ -52,7 +52,7 @@ describe('requestPort (mock fallback)', () => {
it('returns a mocked connection when Web Serial is unsupported', async () => { it('returns a mocked connection when Web Serial is unsupported', async () => {
const conn = await requestPort() const conn = await requestPort()
expect(conn.mocked).toBe(true) expect(conn.mocked).toBe(true)
expect(conn.port).toBe('mock-serial://uno-r4-wifi') expect(conn.port).toBe('mock-serial://uno-q')
await conn.close() await conn.close()
}) })
+6 -6
View File
@@ -1,7 +1,7 @@
// Web Serial bridge to the Arduino UNO R4 WiFi, with a deterministic mock // USB bridge to the team's Arduino Uno Q, with a deterministic mock fallback so
// fallback so the workshop flow + tests run without hardware (or in browsers // the workshop flow + tests run without hardware (or in browsers that lack the
// that lack the Web Serial API). Modules 1 & 2 consume `requestPort` via the // Web Serial API). Modules 1 & 2 consume `requestPort` via the `useSerial` hook;
// `useSerial` hook; the classifier is pure so it is trivially unit-tested. // the classifier is pure so it is trivially unit-tested.
export interface ImuFrame { export interface ImuFrame {
ax: number ax: number
@@ -25,7 +25,7 @@ export interface SerialConn {
close(): Promise<void> close(): Promise<void>
} }
const MOCK_PORT = 'mock-serial://uno-r4-wifi' const MOCK_PORT = 'mock-serial://uno-q'
const BAUD_RATE = 115200 const BAUD_RATE = 115200
export function serialSupported(): boolean { export function serialSupported(): boolean {
@@ -142,7 +142,7 @@ async function createRealConn(): Promise<SerialConn> {
void pump() void pump()
return { return {
port: typeof port.getInfo === 'function' ? `usb-serial://uno-r4-wifi` : 'usb-serial', port: typeof port.getInfo === 'function' ? `usb-serial://uno-q` : 'usb-serial',
mocked: false, mocked: false,
onFrame(cb) { onFrame(cb) {
subs.add(cb) subs.add(cb)
+1 -1
View File
@@ -46,7 +46,7 @@ describe('EnvSetup', () => {
it('passes once frames flow, enabling Proceed without polluting stats', async () => { it('passes once frames flow, enabling Proceed without polluting stats', async () => {
useSession.getState().setMode('live') useSession.getState().setMode('live')
useSession.getState().setDevice({ connected: true, port: 'mock-serial://uno-r4-wifi', uptimeS: 0 }) useSession.getState().setDevice({ connected: true, port: 'mock-serial://uno-q', uptimeS: 0 })
renderPage() renderPage()
const proceed = screen.getByRole('button', { name: /proceed/i }) const proceed = screen.getByRole('button', { name: /proceed/i })
+1 -1
View File
@@ -23,7 +23,7 @@ describe('Landing', () => {
renderLanding() renderLanding()
expect(screen.getByText(/5h 30m/i)).toBeInTheDocument() expect(screen.getByText(/5h 30m/i)).toBeInTheDocument()
expect(screen.getByText(/15 teams/i)).toBeInTheDocument() expect(screen.getByText(/15 teams/i)).toBeInTheDocument()
expect(screen.getAllByText(/Arduino UNO R4/i).length).toBeGreaterThan(0) expect(screen.getAllByText(/Arduino Uno Q/i).length).toBeGreaterThan(0)
}) })
it('has a primary CTA linking to /workshop', () => { it('has a primary CTA linking to /workshop', () => {
+7 -7
View File
@@ -11,7 +11,7 @@ interface ProgrammeRow {
} }
const PROGRAMME: ProgrammeRow[] = [ const PROGRAMME: ProgrammeRow[] = [
{ time: '13:00', title: 'Arrival & kit pickup', desc: 'Teams collect Arduino UNO R4 WiFi + sensor kits.', tags: ['setup'] }, { time: '13:00', title: 'Arrival & kit pickup', desc: 'Teams collect Arduino Uno Q boards + sensor kits.', tags: ['setup'] },
{ time: '14:00', title: 'Lecture · 5 movements', desc: 'Agency, perception loop, ZeroClaw, failure modes, edge vs cloud.', tags: ['lecture'] }, { time: '14:00', title: 'Lecture · 5 movements', desc: 'Agency, perception loop, ZeroClaw, failure modes, edge vs cloud.', tags: ['lecture'] },
{ time: '15:00', title: 'Module 1 · Sense → Reason', desc: 'Live IMU feed, actor map, Layer-1 ADD draft.', tags: ['build'] }, { time: '15:00', title: 'Module 1 · Sense → Reason', desc: 'Live IMU feed, actor map, Layer-1 ADD draft.', tags: ['build'] },
{ time: '16:15', title: 'Module 2 · Harness engineering', desc: 'Tune harness.toml, fire test triggers, Layers 2 + 3.', tags: ['build'] }, { time: '16:15', title: 'Module 2 · Harness engineering', desc: 'Tune harness.toml, fire test triggers, Layers 2 + 3.', tags: ['build'] },
@@ -20,13 +20,13 @@ const PROGRAMME: ProgrammeRow[] = [
] ]
const STACK = [ const STACK = [
{ icon: '⚙', name: 'Rust on Cortex-M4', desc: 'ZeroClaw firmware compiles to a ~6 MB binary for the Renesas RA4M1.' }, { icon: '⚙', name: 'Rust on the Uno Q', desc: 'ZeroClaw runs on the Uno Q’s quad-core Linux side and flashes its on-board STM32 MCU.' },
{ icon: '⌬', name: 'Anthropic + Groq', desc: 'LLM reasoning over UART → ESP32-S3 → cloud proxy with per-team rate limits.' }, { icon: '⌬', name: 'Local + cloud reasoning', desc: 'An on-board Qwen model decides locally; ambiguous calls escalate to a per-team rate-limited cloud proxy.' },
{ icon: '⚡', name: 'Web Serial', desc: 'Browser talks directly to the board over USB — no drivers, no intermediary.' }, { icon: '⚡', name: 'On-board agent', desc: 'Each board hosts its own ZeroClaw agent and local LLM — decisions happen on the device, offline-capable.' },
] ]
const PREREQS = [ const PREREQS = [
'Laptop with Chrome or Edge (Web Serial supported)', 'Laptop with Chrome or Edge (to reach your board)',
'USB-C data cable (provided in kit)', 'USB-C data cable (provided in kit)',
'Basic Rust familiarity helpful but not required', 'Basic Rust familiarity helpful but not required',
'Hands-on attitude — you will flash hardware today', 'Hands-on attitude — you will flash hardware today',
@@ -61,7 +61,7 @@ export function Landing() {
<span className="text-primary">for structural intelligence</span> <span className="text-primary">for structural intelligence</span>
</h1> </h1>
<p className="text-base md:text-lg text-muted-foreground leading-relaxed max-w-2xl mx-auto"> <p className="text-base md:text-lg text-muted-foreground leading-relaxed max-w-2xl mx-auto">
Build a ZeroClaw agent that senses, reasons, and responds autonomously on an Arduino UNO R4 WiFi. Build a ZeroClaw agent that senses, reasons, and responds autonomously on an Arduino Uno Q.
Complete a five-layer Agent Design Document in a single 5.5-hour session. Complete a five-layer Agent Design Document in a single 5.5-hour session.
</p> </p>
<div className="flex flex-wrap gap-3 justify-center pt-4"> <div className="flex flex-wrap gap-3 justify-center pt-4">
@@ -78,7 +78,7 @@ export function Landing() {
['Duration', '5h 30m'], ['Duration', '5h 30m'],
['Teams', '15 teams'], ['Teams', '15 teams'],
['Per team', '3–5 students'], ['Per team', '3–5 students'],
['Hardware', 'Arduino UNO R4 WiFi'], ['Hardware', 'Arduino Uno Q · 4 GB'],
].map(([k, v]) => ( ].map(([k, v]) => (
<div key={k} className="bg-background p-4 space-y-1"> <div key={k} className="bg-background p-4 space-y-1">
<div className="font-mono text-[9px] uppercase tracking-widest text-muted-foreground">{k}</div> <div className="font-mono text-[9px] uppercase tracking-widest text-muted-foreground">{k}</div>
+2 -2
View File
@@ -50,9 +50,9 @@ const MOVEMENTS: Movement[] = [
n: 3, n: 3,
id: 'zeroclaw', id: 'zeroclaw',
title: 'ZeroClaw', title: 'ZeroClaw',
thesis: 'A Rust agent runtime small enough to live on a Cortex-M4 and route reasoning off-device when it must.', thesis: 'A Rust agent runtime that lives on the Uno Q’s Linux side, hosts a local model, and routes reasoning off-device only when it must.',
body: [ body: [
'ZeroClaw compiles to a ~6 MB binary for the Renesas RA4M1. It owns the perception loop on-device, classifies frames locally against your harness, and escalates only the ambiguous cases over UART → ESP32-S3 → a rate-limited cloud proxy.', 'ZeroClaw runs on the Uno Q’s quad-core Cortex-A53 Linux side, hosting a local Qwen model and driving the on-board STM32U585 MCU it flashes over SWD. It owns the perception loop on-device, classifies frames locally against your harness, and escalates only the ambiguous cases to a rate-limited cloud proxy.',
'This is the edge-agent pattern in miniature: cheap, fast, private decisions stay local; expensive judgement is borrowed sparingly. The harness you tune today is the contract between those two worlds.', 'This is the edge-agent pattern in miniature: cheap, fast, private decisions stay local; expensive judgement is borrowed sparingly. The harness you tune today is the contract between those two worlds.',
], ],
takeaways: [ takeaways: [
+1 -1
View File
@@ -35,7 +35,7 @@ export function TeamRegistration() {
// Without Web Serial (tests, unsupported browsers) connect synchronously to // Without Web Serial (tests, unsupported browsers) connect synchronously to
// the mock so the flow stays usable; otherwise prompt for the real board. // the mock so the flow stays usable; otherwise prompt for the real board.
if (!serialSupported()) { if (!serialSupported()) {
setDevice({ connected: true, port: 'mock-serial://uno-r4-wifi', uptimeS: 0 }) setDevice({ connected: true, port: 'mock-serial://uno-q', uptimeS: 0 })
return return
} }
const conn = await requestPort() const conn = await requestPort()