10 KiB
Plan: Multi-Board Simulator UX — Full Design
Context
The codebase already has a solid multi-board foundation: boards[] array in useSimulatorStore, per-board file groups in useEditorStore, separate simulator instances (simulatorMap, pinManagerMap), QEMU bridges for Raspberry Pi 3 and ESP32, and board-specific compilation via BOARD_KIND_FQBN. However, several UX layers are missing: the editor doesn't visually indicate which board you're editing, there's no way to compile/run all boards at once, the serial monitor only shows one board, and the Raspberry Pi 3 has no special terminal/VFS interface.
What Already Exists (Do NOT Re-implement)
boards[]+addBoard()/removeBoard()/setActiveBoardId()inuseSimulatorStorefileGroups/createFileGroup()/setActiveGroup()inuseEditorStoresimulatorMap,pinManagerMap,bridgeMap,esp32BridgeMapruntime mapscompileBoardProgram(),startBoard(),stopBoard(),resetBoard()RaspberryPi3BridgewithsendSerialBytes()/onSerialDatacallbackBOARD_KIND_FQBN,BOARD_KIND_LABELSinfrontend/src/types/board.tsBoardPickerModal,BoardOnCanvas,SerialMonitorcomponents
Phase 1 — Board-Aware Editor UI (Foundation)
1A. Canvas → Editor Sync
File: frontend/src/components/simulator/BoardOnCanvas.tsx
- Add
onBoardClick?: (boardId: string) => voidprop - On click (not drag — detect by < 4px mouse movement), call
onBoardClick(board.id) - In
SimulatorCanvas.tsx, passonBoardClick={(id) => useSimulatorStore.getState().setActiveBoardId(id)}—setActiveBoardIdalready callsuseEditorStore.getState().setActiveGroup(), so this is one line
1B. Board-Grouped FileExplorer
File: frontend/src/components/editor/FileExplorer.tsx
Replace flat files.map() with a grouped tree:
boards.map(board => (
<BoardSection isActive={board.id === activeBoardId} onClick={() => setActiveBoardId(board.id)}>
{fileGroups[board.activeFileGroupId].map(file => <FileItem />)}
<NewFileButton /> // scoped to this group
</BoardSection>
))
- Section header: board emoji icon + label + status dot (green=running, amber=compiled, gray=idle)
createFile,deleteFile,renameFileoperate on active group — clicking section header first sets the active board
1C. Board Context Pill in EditorToolbar
File: frontend/src/components/editor/EditorToolbar.tsx
Add a colored pill at the left of the toolbar:
- Shows:
{emoji} Arduino Uno #1 - Color by family: Arduino=blue, Raspberry Pi=red, ESP32=green
- Clickable — opens a small dropdown to switch active board without going to the canvas
Phase 2 — Compile All / Run All Orchestration
2A. New component: CompileAllProgress
File: frontend/src/components/editor/CompileAllProgress.tsx (new)
Sliding panel showing per-board compile status:
interface BoardCompileStatus {
boardId: string;
boardKind: BoardKind;
label: string;
state: 'pending' | 'compiling' | 'success' | 'error' | 'skipped';
error?: string;
}
- Each row: board icon + label + spinner/checkmark/X
- Error rows expand to show compiler stderr
- "Run All" button at bottom — enabled after all compilations finish; only starts boards that succeeded or skipped
2B. "Compile All" + "Run All" buttons in EditorToolbar
File: frontend/src/components/editor/EditorToolbar.tsx
handleCompileAll logic:
- Iterate
boards[]sequentially (not parallel —arduino-cliis CPU-heavy, shares temp dirs) - For
raspberry-pi-3: markskipped, continue - For each other board: read files via
useEditorStore.getState().getGroupFiles(board.activeFileGroupId) - Call
compileCode(sketchFiles, fqbn), on success callcompileBoardProgram(boardId, program) - Always continue to next board on error — never abort
- If panel is closed mid-run, compilation continues in background
handleRunAll: iterate boards, call startBoard(id) for all that have compiledProgram !== null or are Pi/ESP32
Phase 3 — Multi-Board Serial Monitor
File: frontend/src/components/simulator/SerialMonitor.tsx
Redesign with a tab strip (one tab per board):
- Each tab: board emoji + short label + unread dot (new output since tab last viewed)
- Output area/input row operates on
activeTabboard - Add to
useSimulatorStore:serialWriteToBoard(boardId, text)— likeserialWritebut with explicit boardId (6 lines)clearBoardSerialOutput(boardId)— likeclearSerialOutputwith explicit boardId (4 lines)
- Default active tab follows
activeBoardId
Phase 4 — Raspberry Pi 3 Special Workspace
This is the most complex phase. When activeBoard.boardKind === 'raspberry-pi-3', the left panel switches from Monaco editor to a specialized workspace.
4A. Install xterm.js
cd frontend
npm install @xterm/xterm @xterm/addon-fit
Use scoped packages (@xterm/xterm v5+), NOT deprecated xterm v4.
4B. New store: useVfsStore
File: frontend/src/store/useVfsStore.ts (new)
Keep separate from useSimulatorStore (which is already 970+ lines). VFS is a tree structure, fundamentally different from the flat file-group lists in useEditorStore.
interface VfsNode {
id: string;
name: string;
type: 'file' | 'directory';
content?: string;
children?: VfsNode[];
parentId: string | null;
}
// State: trees: Record<boardId, VfsNode> (root "/" per board)
// Actions: createNode, deleteNode, renameNode, setContent, setSelectedNode, initBoardVfs
Default tree for new Pi board:
/
home/pi/
script.py (default Python template)
hello.sh
Call initBoardVfs(boardId) inside useSimulatorStore.addBoard() when boardKind === 'raspberry-pi-3'.
4C. New: PiTerminal.tsx
File: frontend/src/components/raspberry-pi/PiTerminal.tsx (new)
- Mounts xterm.js Terminal into a
refdiv term.onData(data => bridge.sendSerialBytes(...))— input → QEMU- Intercept
bridge.onSerialDatato write to terminal (save+restore prev callback to keep store'sserialOutputin sync) ResizeObserver→fitAddon.fit()for responsive layout- Lazy-loaded via
React.lazy()in EditorPage to keep xterm.js out of the main bundle
4D. New: VirtualFileSystem.tsx
File: frontend/src/components/raspberry-pi/VirtualFileSystem.tsx (new)
- Recursive tree component, reads from
useVfsStore - Expand/collapse directories
- Click file → calls
onFileSelect(nodeId, content, filename) - Right-click context menu: New File, New Folder, Rename, Delete
- "Upload to Pi" button in header: serializes tree to
{path, content}[]and callsbridge.sendFile(path, content)for each node (requires newsendFilemethod onRaspberryPi3Bridgeand backend protocol message{ type: 'vfs_write', data: { path, content } })
4E. New: RaspberryPiWorkspace.tsx
File: frontend/src/components/raspberry-pi/RaspberryPiWorkspace.tsx (new)
Two-pane layout:
- Left:
VirtualFileSystem - Right: Tab strip with "Terminal" tab + open file tabs
- Terminal tab →
PiTerminal - File tab → Monaco
CodeEditor(content synced touseVfsStore.setContent)
- Terminal tab →
- Pi-specific toolbar: Connect / Disconnect / Upload Files to Pi
4F. EditorPage — conditional render
File: frontend/src/pages/EditorPage.tsx
const isRaspberryPi3 = activeBoard?.boardKind === 'raspberry-pi-3';
// In JSX:
{isRaspberryPi3 && activeBoardId
? <React.Suspense fallback={<div>Loading...</div>}>
<RaspberryPiWorkspace boardId={activeBoardId} />
</React.Suspense>
: <CodeEditor />
}
Hide FileTabs when in Pi mode (VFS replaces them).
Phase 5 — Board Status Indicators on Canvas
File: frontend/src/components/simulator/BoardOnCanvas.tsx
Add two overlays (absolutely positioned, pointerEvents: none):
- Active board highlight ring: 2px
#007accborder around board bounds whenboard.id === activeBoardId - Status dot: 12px circle at top-right corner
- Green (
#22c55e) = running - Amber (
#f59e0b) = compiled, not running - Gray (
#6b7280) = idle
- Green (
Pass activeBoardId as a prop from SimulatorCanvas.
Implementation Order
Phase 1A → 1B → 1C (foundation — pure UI, no new deps)
│
↓
Phase 5 (30 min, canvas badges — immediate visual feedback)
│
↓
Phase 3 (serial monitor tabs — adds 2 store actions)
│
↓
Phase 2 (compile all — uses existing store APIs)
│
↓
Phase 4A → 4B → 4C → 4D → 4E → 4F (Pi workspace — most complex, new npm dep)
Critical Files to Modify
| File | Phase | Changes |
|---|---|---|
frontend/src/components/simulator/BoardOnCanvas.tsx |
1A, 5 | onBoardClick prop, status badges, active ring |
frontend/src/components/simulator/SimulatorCanvas.tsx |
1A | Pass onBoardClick handler |
frontend/src/components/editor/FileExplorer.tsx |
1B | Board-grouped tree replacing flat list |
frontend/src/components/editor/EditorToolbar.tsx |
1C, 2B | Board pill, Compile All, Run All |
frontend/src/store/useSimulatorStore.ts |
3 | Add serialWriteToBoard, clearBoardSerialOutput |
frontend/src/components/simulator/SerialMonitor.tsx |
3 | Board tabs |
frontend/src/pages/EditorPage.tsx |
4F | Conditional Pi workspace vs CodeEditor |
New Files to Create
| File | Phase |
|---|---|
frontend/src/components/editor/CompileAllProgress.tsx |
2A |
frontend/src/store/useVfsStore.ts |
4B |
frontend/src/components/raspberry-pi/PiTerminal.tsx |
4C |
frontend/src/components/raspberry-pi/VirtualFileSystem.tsx |
4D |
frontend/src/components/raspberry-pi/RaspberryPiWorkspace.tsx |
4E |
Verification
- Phase 1: Click an Arduino on the canvas → FileExplorer highlights that board's files, toolbar pill updates
- Phase 2: Add 2 Arduino boards with different code → "Compile All" → progress panel shows both → "Run All" starts both
- Phase 3: 2 boards running → Serial Monitor has 2 tabs, unread dot appears when output arrives on background tab
- Phase 4: Add Raspberry Pi 3 → editor area switches to VFS/terminal; create a
script.pyin VFS, upload to Pi, run it from terminal - Phase 5: Canvas shows green dot on running boards, amber on compiled-not-running, gray on idle; blue ring on active board