diff --git a/plan_multiborad.md b/plan_multiborad.md deleted file mode 100644 index fabceb0..0000000 --- a/plan_multiborad.md +++ /dev/null @@ -1,252 +0,0 @@ -# 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()` in `useSimulatorStore` -- `fileGroups` / `createFileGroup()` / `setActiveGroup()` in `useEditorStore` -- `simulatorMap`, `pinManagerMap`, `bridgeMap`, `esp32BridgeMap` runtime maps -- `compileBoardProgram()`, `startBoard()`, `stopBoard()`, `resetBoard()` -- `RaspberryPi3Bridge` with `sendSerialBytes()` / `onSerialData` callback -- `BOARD_KIND_FQBN`, `BOARD_KIND_LABELS` in `frontend/src/types/board.ts` -- `BoardPickerModal`, `BoardOnCanvas`, `SerialMonitor` components - ---- - -## Phase 1 — Board-Aware Editor UI (Foundation) - -### 1A. Canvas → Editor Sync -**File:** `frontend/src/components/simulator/BoardOnCanvas.tsx` - -- Add `onBoardClick?: (boardId: string) => void` prop -- On click (not drag — detect by < 4px mouse movement), call `onBoardClick(board.id)` -- In `SimulatorCanvas.tsx`, pass `onBoardClick={(id) => useSimulatorStore.getState().setActiveBoardId(id)}` - — `setActiveBoardId` already calls `useEditorStore.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 => ( - setActiveBoardId(board.id)}> - {fileGroups[board.activeFileGroupId].map(file => )} - // scoped to this group - -)) -``` -- Section header: board emoji icon + label + status dot (green=running, amber=compiled, gray=idle) -- `createFile`, `deleteFile`, `renameFile` operate 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: -```typescript -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: -1. Iterate `boards[]` **sequentially** (not parallel — `arduino-cli` is CPU-heavy, shares temp dirs) -2. For `raspberry-pi-3`: mark `skipped`, continue -3. For each other board: read files via `useEditorStore.getState().getGroupFiles(board.activeFileGroupId)` -4. Call `compileCode(sketchFiles, fqbn)`, on success call `compileBoardProgram(boardId, program)` -5. **Always continue to next board on error** — never abort -6. 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 `activeTab` board -- Add to `useSimulatorStore`: - - `serialWriteToBoard(boardId, text)` — like `serialWrite` but with explicit boardId (6 lines) - - `clearBoardSerialOutput(boardId)` — like `clearSerialOutput` with 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 -```bash -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`. - -```typescript -interface VfsNode { - id: string; - name: string; - type: 'file' | 'directory'; - content?: string; - children?: VfsNode[]; - parentId: string | null; -} - -// State: trees: Record (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 `ref` div -- `term.onData(data => bridge.sendSerialBytes(...))` — input → QEMU -- Intercept `bridge.onSerialData` to write to terminal (save+restore prev callback to keep store's `serialOutput` in 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 calls `bridge.sendFile(path, content)` for each node (requires new `sendFile` method on `RaspberryPi3Bridge` and 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 to `useVfsStore.setContent`) -- Pi-specific toolbar: Connect / Disconnect / Upload Files to Pi - -### 4F. EditorPage — conditional render -**File:** `frontend/src/pages/EditorPage.tsx` - -```typescript -const isRaspberryPi3 = activeBoard?.boardKind === 'raspberry-pi-3'; - -// In JSX: -{isRaspberryPi3 && activeBoardId - ? Loading...}> - - - : -} -``` -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`): - -1. **Active board highlight ring**: 2px `#007acc` border around board bounds when `board.id === activeBoardId` -2. **Status dot**: 12px circle at top-right corner - - Green (`#22c55e`) = running - - Amber (`#f59e0b`) = compiled, not running - - Gray (`#6b7280`) = idle - -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 - -1. **Phase 1**: Click an Arduino on the canvas → FileExplorer highlights that board's files, toolbar pill updates -2. **Phase 2**: Add 2 Arduino boards with different code → "Compile All" → progress panel shows both → "Run All" starts both -3. **Phase 3**: 2 boards running → Serial Monitor has 2 tabs, unread dot appears when output arrives on background tab -4. **Phase 4**: Add Raspberry Pi 3 → editor area switches to VFS/terminal; create a `script.py` in VFS, upload to Pi, run it from terminal -5. **Phase 5**: Canvas shows green dot on running boards, amber on compiled-not-running, gray on idle; blue ring on active board diff --git a/strategy_plan.md b/strategy_plan.md deleted file mode 100644 index 25c0b0b..0000000 --- a/strategy_plan.md +++ /dev/null @@ -1,128 +0,0 @@ -# Plan Estratégico Integral: Alternativa a Wokwi Open Source - -Basado en el análisis profundo del código ([README.md](file:///e:/Hardware/wokwi_clon/README.md), [CLAUDE.md](file:///e:/Hardware/wokwi_clon/CLAUDE.md)), tu proyecto cuenta con una base técnica muy sólida de emulación local (AVR8 y RP2040) utilizando `arduino-cli` localmente. Para competir directamente con Wokwi y escalar el proyecto, aquí tienes un plan maestro detallado. - ---- - -## 1. Naming: El Nombre del Proyecto - -El proyecto ahora usa el nombre oficial **Velxio**. El rebrand desde "OpenWokwi" fue necesario por temas de copyright y marca registrada (Trademark) por parte de Wokwi, lo que podría resultar en un [DMCA Takedown](https://docs.github.com/en/site-policy/content-removal-policies/dmca-takedown-policy) en GitHub. - -**Nombre Ganador y Oficial: `VELXIO`** - -Es un neologismo (palabra inventada) de alto nivel técnico. -Las sílabas y letras significan: -- *"Vel"*: Sugiere **Velocidad** (compilación local ultrarrápida sin lag). -- *"X"*: e**X**ecution (Ejecución y rendimiento puro). -- *"IO"*: In/Out (Entrada/Salida de pines, la esencia de Arduino y hardware). - -**Pros de VELXIO:** -✅ Pronunciación limpia en español e inglés ("Velk-si-o"). -✅ 100% Libre en la base de datos de OMPI/WIPO (Sin riesgo legal). -✅ 100% Libre en GitHub y NPM. -✅ Dominio `.dev` premium disponible (`velxio.dev`). - -De aquí en adelante usaremos **VELXIO** como el nombre oficial para este plan e implementación. - ---- - -## 2. Análisis de Mercado (VELXIO vs Wokwi) - -Para competir, no puedes hacer "lo mismo", debes atacar los puntos débiles de Wokwi para robarle cuota de mercado. - -| Característica | Wokwi (La Competencia) | VELXIO (Tu Proyecto) | -| :--- | :--- | :--- | -| **Ejecución** | 100% Nube (Requiere internet) | 100% Local (Docker / Python) | -| **Latencia Compilación** | Alta (Envía a servidores remotos) | Muy Baja (Usa `arduino-cli` en la misma máquina) | -| **Privacidad de Código** | Servidores de 3ros, Proyectos públicos | **100% Privado**, el código no sale de tu PC | -| **Librerías** | Depende de lo que provea el sistema | Acceso instantáneo a TODO el índice de Arduino | -| **Modelo de Negocio** | Restricciones Freemium/Premium | 100% Open Source (Donaciones/Sponsors) | -| **Integración Local** | Nula / Difícil | Puede guardar directo al disco duro (`.ino`) | - -**Tu Público Objetivo Ideal:** -- Profesores e institutos sin buena conexión de internet. -- Empresas que no pueden subir código propietario a servidores de Wokwi (NDAs, privacidad). -- Geeks y Makers que prefieren tener todo contenido en Docker auto-alojado. - ---- - -## 3. Branding y Estilo Visual - -Si tu plataforma parece "casera", los desarrolladores no la usarán. Necesita un **"Rich Aesthetic"** y sentirse premium. - -- **Logo**: Un chip microcontrolador Isométrico donde los pines forman unas escaleras o puertos de conexión, usando degradados vibrantes. -- **Paleta de Colores (Estilo "Cyber-Maker")**: - - **Fondo General**: `#0A0A0A` (Casi negro absoluto, estilo moderno de desarrolladores como Vercel/Linear). - - **Superficies**: `#151515` con bordes sutiles en gris oscuro `rgba(255,255,255,0.1)`. - - **Color Primario (Acción/Marca)**: Cyan Neón `#00E5FF` (Representa tecnología brillante). - - **Color Secundario (Éxito/Arduino)**: Verde Hacker `#00FF66` (Representa la electrónica tradicional). - - **Acentos**: Morado Eléctrico `#B300FF` (Para botones o llamadas a la acción en la web). -- **Tipografía**: **[Inter](https://fonts.google.com/specimen/Inter)** o **[Geist](https://vercel.com/font)** para la interfaz web general, y **[Fira Code](https://github.com/tonsky/FiraCode)** o **JetBrains Mono** para el código. - ---- - -## 4. Estrategia de la Página Web - -Para alojar la promoción, la página no debe ser simplemente el emulador de inmediato. Necesitas un **Landing Page** que "venda" el producto. - -**Arquitectura recomendada:** -- **Stack**: Next.js (para SEO de la landing) o simplemente aprovechar tu Frontend actual en Vite añadiendo SSR/SSG. Emplea diseño "Glassmorphism" (fondos semi-transparentes desenfocados) y microanimaciones de entrada. -- **Estructura del Landing**: - 1. **Hero Section (Arriba)**: - - Título: *"The Ultimate Local Arduino & RP2040 Simulator."* - - Subtítulo: *"Open Source. Ultra-fast local compilation. Ultimate privacy."* - - Botones: "View on GitHub", "Quick Start Guide". - - Imagen/Video: Un GIF en bucle o videoclip que muestre la Raspberry Pi Pico y la TFT ILI9341 corriendo a 60 FPS dentro de tu aplicación con el Dark Mode. - 2. **Features (Grilla de 3x2)**: - - "Blazing Fast Local Compilation", "Hardware Accurate", "48+ Component Library", "Zero Cloud Telemetry". - 3. **Showcase**: Un carrusel de imágenes demostrando proyectos complejos (Simon Says, TFT displays). - 4. **Instalación de 1 click**: Un bloque de código de copiar y pegar enorme que muestre: `docker compose up --build`. A los devs les encanta esto. - ---- - -## 5. Promoción en Google (Estrategia SEO) - -No podrás superar al principio la keyword "Arduino Simulator" contra Tinkercad, Proteus o Wokwi. -Debes apuntar hacia **"Long Tail Keywords"** (Búsquedas específicas de nicho). - -**Búsquedas que debes dominar (Inclúyelas en tu [README.md](file:///e:/Hardware/wokwi_clon/README.md) y Landing Page H1/H2 tags):** -- *Local offline Arduino simulator* -- *Self-hosted Wokwi alternative* -- *Open source ESP32 RP2040 emulator* -- *Arduino hardware simulator Docker* - -**Estrategia Accionable de Promoción:** -1. Escribe un caso de estudio (Blog) titulado: *"Why I built a local alternative to Wokwi Simulator"*. Súbelo a plataformas dev (Medium, dev.to, Hashnode). -2. Lanza el proyecto formalmente publicándolo en: - - **Reddit**: `/r/arduino`, `/r/esp32`, `/r/raspberrypipico`, `/r/selfhosted`, `/r/programming`. - - **Hacker News (Y Combinator)**: Título sugerido: *Show HN: An open-source, fully offline Arduino emulator inside your browser.* - - **GitHub Trending**: Si logras mucha tracción inicial el mismo día (Stars), GitHub te destacará en correos a desarrolladores. - ---- - -## 6. Estrategia del Video para YouTube - -YouTube es el motor #2 de búsquedas en el mundo y donde los "Makers" pasan todo su tiempo viendo tutoriales. Aquí está el plan de un video que se vuelva viral y robe usuarios de la competencia. - -**Título del Video:** -> *Stop Using Cloud Simulators! I Built a Local Arduino Emulator.* -> o *The Best FREE Alternative to Wokwi (100% Local & Open Source)*. - -**Thumbnail (Miniatura):** -De un lado el logo de Wokwi, del otro lado una terminal con código cayendo, y flechas brillantes con texto gigante apuntando "OFFLINE" y "ZERO LAG". - -**Guion Estructurado (5 a 8 minutos):** -1. **El Gancho (0:00 - 0:45)**: - *Muestra un video muy rápido usando el simulador, conectando un motor y LED.* - "Si eres un Maker, seguramente has usado Wokwi o Tinkercad. Son geniales, **pero** compilar lleva tiempo porque viaja a servidores lejanos, te limitan, y olvídate de usarlos en un avión o sin internet. ¿No sería increíble tener todo ese poder ejecutándose directamente en los procesadores locales de TU máquina? Bueno... lo he programado." -2. **Introducción del Proyecto (0:45 - 2:00)**: - Muestra la arquitectura general de "VELXIO". Habla de cómo usas Docker, FastAPI y `arduino-cli` directo para compilar en tu disco SSD, destrozando los tiempos de retardo de la nube. -3. **Tutorial Paso a Paso / Demo "Wow" (2:00 - 5:00)**: - *Abre la aplicación de cero.* - - Muestra qué tan rápido se abre el Administrador de Librerías (que como dice tu README, carga completo al instante). - - Compila un ejemplo difícil, uno usando Raspberry Pi Pico y la pantalla TFT 240x320 renderizando formas gráficas o un jueguito como Simon Says. - - Demuestra la monitorización de la terminal Serial. -4. **Bajo el Capot para Geeks (5:00 - 6:30)**: - Muestra un poquito de código TypeScript y FastAPI. Al público Maker le encanta ver que es transparente. Menciona que usas AVR8js. -5. **Call to Action (6:30 - Fin)**: - "El proyecto es completamente Open Source. Si quieres usarlo ahora mismo, corre el comando Docker de la descripción. El mayor favor que me puedes hacer es darle una Estrella en GitHub para ayudarme contra la tracción de los simuladores gigantes de la nube. Nos vemos en el próximo video." diff --git a/test_esp32_emulation.py b/test_esp32_emulation.py deleted file mode 100644 index bc42db7..0000000 --- a/test_esp32_emulation.py +++ /dev/null @@ -1,241 +0,0 @@ -#!/usr/bin/env python3 -""" -ESP32 Emulation Integration Test -================================= -Tests the full pipeline: - 1. Compile ESP32 Blink sketch via HTTP POST /api/compile - 2. Connect WebSocket to /api/simulation/ws/test-esp32 - 3. Send start_esp32 with the compiled 4MB firmware - 4. Wait for system events (booting, booted) and gpio_change events - 5. Report success/failure - -Usage: - python test_esp32_emulation.py - python test_esp32_emulation.py --base http://localhost:8001 -""" -import argparse -import asyncio -import io -import json -import sys -import time - -# Force UTF-8 on Windows so checkmarks/symbols don't crash -sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') -sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') -import httpx -import websockets - -BLINK_SKETCH = """\ -// ESP32 Blink LED - Test Sketch -// Blinks GPIO4 at 500ms intervals, outputs status on Serial -#define LED_PIN 4 - -void setup() { - Serial.begin(115200); - pinMode(LED_PIN, OUTPUT); - Serial.println("ESP32 Blink ready!"); -} - -void loop() { - digitalWrite(LED_PIN, HIGH); - Serial.println("LED ON"); - delay(500); - digitalWrite(LED_PIN, LOW); - Serial.println("LED OFF"); - delay(500); -} -""" - - -def print_section(title: str): - print(f"\n{'='*60}") - print(f" {title}") - print(f"{'='*60}") - - -async def run_test(base_url: str): - ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://") - - # ── Step 1: Compile ─────────────────────────────────────────────────────── - print_section("Step 1: Compile ESP32 Blink sketch") - - async with httpx.AsyncClient(base_url=base_url, timeout=120.0) as client: - payload = { - "files": [{"name": "sketch.ino", "content": BLINK_SKETCH}], - "board_fqbn": "esp32:esp32:esp32", - } - print(f" POST {base_url}/api/compile/") - t0 = time.time() - resp = await client.post("/api/compile/", json=payload) - elapsed = time.time() - t0 - - print(f" Status: {resp.status_code} ({elapsed:.1f}s)") - if resp.status_code != 200: - print(f" FAIL: {resp.text}") - return False - - data = resp.json() - if not data.get("success"): - print(f" FAIL: compilation failed") - print(f" stderr: {data.get('stderr', '')[:500]}") - return False - - firmware_b64: str = data.get("binary_content", "") - fw_bytes = len(firmware_b64) * 3 // 4 - print(f" OK — firmware {fw_bytes // 1024} KB base64-encoded") - - if fw_bytes < 1024 * 1024: - print(f" WARN: firmware < 1 MB ({fw_bytes} bytes). " - f"QEMU needs a 4MB merged image. Expected ~4194304 bytes.") - print(f" This suggests the esptool merge step did not run.") - else: - print(f" OK — firmware size looks like a full flash image ✓") - - # ── Step 2: WebSocket Simulation ───────────────────────────────────────── - print_section("Step 2: Connect WebSocket and start ESP32 emulation") - - ws_endpoint = f"{ws_url}/api/simulation/ws/test-esp32" - print(f" Connecting to {ws_endpoint}") - - results = { - "connected": False, - "booting": False, - "booted": False, - "serial_lines": [], - "gpio_changes": [], - "errors": [], - } - - try: - async with websockets.connect(ws_endpoint, open_timeout=10) as ws: - results["connected"] = True - print(" WebSocket connected ✓") - - # Send start_esp32 with firmware - msg = json.dumps({ - "type": "start_esp32", - "data": { - "board": "esp32", - "firmware_b64": firmware_b64, - }, - }) - await ws.send(msg) - print(" Sent start_esp32 (firmware attached)") - - # Listen for events for up to 20 seconds - deadline = time.time() + 20 - print(" Waiting for events (up to 20s)...") - - while time.time() < deadline: - remaining = deadline - time.time() - try: - raw = await asyncio.wait_for(ws.recv(), timeout=min(remaining, 2.0)) - evt = json.loads(raw) - evt_type = evt.get("type", "") - evt_data = evt.get("data", {}) - - if evt_type == "system": - event_name = evt_data.get("event", "") - print(f" [system] {event_name}") - if event_name == "booting": - results["booting"] = True - elif event_name == "booted": - results["booted"] = True - elif event_name == "crash": - print(f" CRASH: {json.dumps(evt_data)}") - results["errors"].append(f"crash: {evt_data}") - - elif evt_type == "serial_output": - text = evt_data.get("data", "") - sys.stdout.write(f" [serial] {text}") - sys.stdout.flush() - results["serial_lines"].append(text) - - elif evt_type == "gpio_change": - pin = evt_data.get("pin") - state = evt_data.get("state") - label = "HIGH" if state == 1 else "LOW" - print(f" [gpio] pin={pin} → {label}") - results["gpio_changes"].append((pin, state)) - - elif evt_type == "gpio_dir": - pin = evt_data.get("pin") - direction = "OUTPUT" if evt_data.get("dir") == 1 else "INPUT" - print(f" [gpio_dir] pin={pin} → {direction}") - - elif evt_type == "error": - msg_text = evt_data.get("message", "") - print(f" [error] {msg_text}") - results["errors"].append(msg_text) - - # Stop early if we got at least 2 gpio toggles on pin 4 - pin4_toggles = [(p, s) for p, s in results["gpio_changes"] if p == 4] - if len(pin4_toggles) >= 2: - print(f"\n Got {len(pin4_toggles)} GPIO4 toggles — stopping early ✓") - break - - except asyncio.TimeoutError: - continue - - except Exception as e: - print(f" WebSocket error: {e}") - results["errors"].append(str(e)) - - # ── Step 3: Report ──────────────────────────────────────────────────────── - print_section("Test Results") - - ok = True - - checks = [ - ("WebSocket connected", results["connected"]), - ("QEMU booting event", results["booting"]), - ("QEMU booted event", results["booted"]), - ("Serial output received", bool(results["serial_lines"])), - ("GPIO4 toggled at least once", any(p == 4 for p, _ in results["gpio_changes"])), - ("GPIO4 toggled HIGH+LOW", ( - any(p == 4 and s == 1 for p, s in results["gpio_changes"]) and - any(p == 4 and s == 0 for p, s in results["gpio_changes"]) - )), - ] - - for label, passed in checks: - icon = "✓" if passed else "✗" - print(f" {icon} {label}") - if not passed: - ok = False - - if results["errors"]: - print(f"\n Errors encountered:") - for e in results["errors"]: - print(f" - {e}") - - if results["gpio_changes"]: - print(f"\n GPIO changes recorded: {results['gpio_changes'][:10]}") - - if results["serial_lines"]: - joined = "".join(results["serial_lines"]) - print(f"\n Serial output (first 300 chars):\n {joined[:300]!r}") - - print() - if ok: - print(" ALL CHECKS PASSED ✓ — ESP32 emulation is working end-to-end") - else: - print(" SOME CHECKS FAILED ✗ — see above for details") - print() - - return ok - - -def main(): - parser = argparse.ArgumentParser(description="ESP32 emulation integration test") - parser.add_argument("--base", default="http://localhost:8001", - help="Backend base URL (default: http://localhost:8001)") - args = parser.parse_args() - - ok = asyncio.run(run_test(args.base)) - sys.exit(0 if ok else 1) - - -if __name__ == "__main__": - main()