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:
co-authored by
Claude Opus 4.8
parent
1ad6da64f7
commit
6688ced6b3
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user