diff --git a/backend/app/services/esp32_worker.py b/backend/app/services/esp32_worker.py index c844d6d..795864c 100644 --- a/backend/app/services/esp32_worker.py +++ b/backend/app/services/esp32_worker.py @@ -421,14 +421,14 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker) _dht22_sync_step() return # always return for GPIO_IN syncs (fast path) marker = direction & 0xF000 - if marker == 0x2000: # GPIO_FUNCX_OUT_SEL_CFG change - gpio_pin = direction & 0xFF - signal = (direction >> 8) & 0xFF - # Signal 72-79 = LEDC HS ch 0-7; 80-87 = LEDC LS ch 0-7 - if 72 <= signal <= 87: - ledc_ch = signal - 72 # ch 0-15 - _ledc_gpio_map[ledc_ch] = gpio_pin - _log(f'LEDC map: ch{ledc_ch} → GPIO{gpio_pin} (signal={signal})') + if marker == 0x5000: # LEDC duty change (from esp32_ledc.c) + ledc_ch = (direction >> 8) & 0x0F + intensity = direction & 0xFF # 0-100 percentage + gpio = _ledc_gpio_map.get(ledc_ch, -1) + _emit({'type': 'ledc_update', 'channel': ledc_ch, + 'duty': intensity, + 'duty_pct': intensity, + 'gpio': gpio}) return # ── DHT22: track direction changes + trigger sync response ─────── @@ -588,25 +588,36 @@ def main() -> None: # noqa: C901 (complexity OK for inline worker) # Track last-emitted duty to avoid flooding identical updates _last_duty = [0.0] * 16 _diag_count = [0] + _first_nonzero_logged = [False] _log('LEDC poll thread started') while not _stopped.wait(0.1): try: ptr = lib.qemu_picsimlab_get_internals(6) # LEDC_CHANNEL_DUTY _diag_count[0] += 1 - # Log first 5 polls for diagnostics - if _diag_count[0] <= 5: - _log(f'LEDC poll #{_diag_count[0]}: ptr={ptr} ' - f'(type={type(ptr).__name__}) gpio_map={dict(_ledc_gpio_map)}') if ptr is None or ptr == 0: - if _diag_count[0] <= 5: - _log(f'LEDC poll: ptr is NULL/0, skipping') continue # duty[] is float[16] in QEMU (percentage 0-100) arr = (ctypes.c_float * 16).from_address(ptr) - if _diag_count[0] <= 5: + # Refresh LEDC→GPIO mapping from gpio_out_sel[40] registers + out_sel_ptr = lib.qemu_picsimlab_get_internals(2) + if out_sel_ptr: + out_sel = (ctypes.c_uint32 * 40).from_address(out_sel_ptr) + for gpio_pin in range(40): + signal = int(out_sel[gpio_pin]) & 0xFF + # Signal 72-79 = LEDC HS ch 0-7; 80-87 = LEDC LS ch 0-7 + if 72 <= signal <= 87: + ledc_ch = signal - 72 + if _ledc_gpio_map.get(ledc_ch) != gpio_pin: + _ledc_gpio_map[ledc_ch] = gpio_pin + _log(f'LEDC map: ch{ledc_ch} → GPIO{gpio_pin} (signal={signal})') + # Log once when nonzero duties first appear + if not _first_nonzero_logged[0]: nonzero = {ch: round(float(arr[ch]), 2) for ch in range(16) if float(arr[ch]) != 0.0} - _log(f'LEDC poll: nonzero duties={nonzero}') + if nonzero: + _log(f'LEDC first nonzero at poll #{_diag_count[0]}: ' + f'duties={nonzero} gpio_map={dict(_ledc_gpio_map)}') + _first_nonzero_logged[0] = True for ch in range(16): duty_pct = float(arr[ch]) if abs(duty_pct - _last_duty[ch]) < 0.01: diff --git a/frontend/src/__tests__/esp32-servo-pot.test.ts b/frontend/src/__tests__/esp32-servo-pot.test.ts index a022d97..27c6aee 100644 --- a/frontend/src/__tests__/esp32-servo-pot.test.ts +++ b/frontend/src/__tests__/esp32-servo-pot.test.ts @@ -470,7 +470,201 @@ describe('ESP32 ADC channel mapping', () => { }); // ───────────────────────────────────────────────────────────────────────────── -// 10. LEDC polling — data type and internal config +// 10. LEDC 0x5000 marker decoding — channel extraction fix +// ───────────────────────────────────────────────────────────────────────────── + +describe('LEDC 0x5000 marker decoding', () => { + // QEMU fires: qemu_set_irq(ledc_sync, 0x5000 | (ledn << 8) | intensity) + // Worker must extract: ledc_ch = (direction >> 8) & 0x0F (NOT & 0xFF) + + function decodeLedc(direction: number) { + const marker = direction & 0xF000; + if (marker !== 0x5000) return null; + const ledc_ch = (direction >> 8) & 0x0F; // correct: strips marker bits + const intensity = direction & 0xFF; + return { ledc_ch, intensity }; + } + + function decodeLedcBroken(direction: number) { + const marker = direction & 0xF000; + if (marker !== 0x5000) return null; + const ledc_ch = (direction >> 8) & 0xFF; // BUG: includes marker bits + const intensity = direction & 0xFF; + return { ledc_ch, intensity }; + } + + it('HS channel 0 (ledn=0): direction=0x500B → ch=0, not ch=80', () => { + const direction = 0x5000 | (0 << 8) | 11; // 0x500B + const correct = decodeLedc(direction)!; + const broken = decodeLedcBroken(direction)!; + + expect(correct.ledc_ch).toBe(0); // correct + expect(broken.ledc_ch).toBe(80); // BUG: 0x50 = 80 + expect(correct.intensity).toBe(11); + }); + + it('LS channel 0 (ledn=8): direction=0x5811 → ch=8, not ch=88', () => { + const direction = 0x5000 | (8 << 8) | 17; // 0x5811 + const correct = decodeLedc(direction)!; + const broken = decodeLedcBroken(direction)!; + + expect(correct.ledc_ch).toBe(8); // correct + expect(broken.ledc_ch).toBe(88); // BUG: 0x58 = 88 + expect(correct.intensity).toBe(17); + }); + + it('HS channel 7 (ledn=7): direction=0x5732 → ch=7', () => { + const direction = 0x5000 | (7 << 8) | 50; // 0x5732 + expect(decodeLedc(direction)!.ledc_ch).toBe(7); + expect(decodeLedc(direction)!.intensity).toBe(50); + }); + + it('LS channel 7 (ledn=15): direction=0x5F64 → ch=15', () => { + const direction = 0x5000 | (15 << 8) | 100; // 0x5F64 + expect(decodeLedc(direction)!.ledc_ch).toBe(15); + expect(decodeLedc(direction)!.intensity).toBe(100); + }); + + it('all 16 channels decode correctly', () => { + for (let ledn = 0; ledn < 16; ledn++) { + const direction = 0x5000 | (ledn << 8) | 42; + const result = decodeLedc(direction)!; + expect(result.ledc_ch).toBe(ledn); + expect(result.intensity).toBe(42); + } + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// 11. GPIO out_sel scanning — LEDC→GPIO mapping +// ───────────────────────────────────────────────────────────────────────────── + +describe('GPIO out_sel scanning for LEDC mapping', () => { + // Simulates what the LEDC poll thread does: read gpio_out_sel[40] and + // scan for LEDC signal values (72-87) to build _ledc_gpio_map + + function scanOutSel(outSel: number[]): Map { + const ledcGpioMap = new Map(); + for (let gpioPin = 0; gpioPin < outSel.length; gpioPin++) { + const signal = outSel[gpioPin] & 0xFF; + if (signal >= 72 && signal <= 87) { + const ledcCh = signal - 72; + ledcGpioMap.set(ledcCh, gpioPin); + } + } + return ledcGpioMap; + } + + it('detects LEDC HS ch0 (signal=72) on GPIO 13', () => { + const outSel = new Array(40).fill(256); // 256 = no function + outSel[13] = 72; // LEDC HS ch0 → GPIO 13 + const map = scanOutSel(outSel); + + expect(map.get(0)).toBe(13); + expect(map.size).toBe(1); + }); + + it('detects LEDC LS ch0 (signal=80) on GPIO 2', () => { + const outSel = new Array(40).fill(256); + outSel[2] = 80; // LEDC LS ch0 → GPIO 2 + const map = scanOutSel(outSel); + + expect(map.get(8)).toBe(2); // ch8 = LS ch0 + }); + + it('detects multiple LEDC channels', () => { + const outSel = new Array(40).fill(256); + outSel[13] = 72; // HS ch0 → GPIO 13 + outSel[12] = 73; // HS ch1 → GPIO 12 + outSel[14] = 80; // LS ch0 → GPIO 14 + const map = scanOutSel(outSel); + + expect(map.get(0)).toBe(13); + expect(map.get(1)).toBe(12); + expect(map.get(8)).toBe(14); + expect(map.size).toBe(3); + }); + + it('ignores non-LEDC signals (< 72 or > 87)', () => { + const outSel = new Array(40).fill(256); + outSel[5] = 71; // signal 71 = not LEDC + outSel[6] = 88; // signal 88 = not LEDC + outSel[7] = 0; // signal 0 = GPIO matrix simple + const map = scanOutSel(outSel); + + expect(map.size).toBe(0); + }); + + it('explains why 0x2000 marker was broken for LEDC signals', () => { + // QEMU fires: 0x2000 | ((signal & 0xFF) << 8) | (gpio & 0xFF) + // For signal=72 (0x48), gpio=13: direction = 0x2000 | 0x4800 | 0x0D = 0x680D + // marker = direction & 0xF000 = 0x6000 ≠ 0x2000 → NEVER MATCHED! + const signal = 72; + const gpio = 13; + const direction = 0x2000 | ((signal & 0xFF) << 8) | (gpio & 0xFF); + + expect(direction).toBe(0x680D); + expect(direction & 0xF000).toBe(0x6000); // NOT 0x2000! + expect(direction & 0xF000).not.toBe(0x2000); // confirms the bug + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// 12. End-to-end: LEDC update with correct GPIO routes to servo +// ───────────────────────────────────────────────────────────────────────────── + +describe('End-to-end: LEDC → servo angle', () => { + const logic = () => PartSimulationRegistry.get('servo')!; + + it('ledc_update with gpio=13 → updatePwm(13, duty) → servo moves', () => { + const shim = makeEsp32Shim(); + const el = makeElement() as any; + logic().attachEvents!(el, shim as any, pinMap({ PWM: 13 }), 'servo-e2e'); + + // Simulate what useSimulatorStore.onLedcUpdate does: + const update = { channel: 0, duty: 7.36, duty_pct: 7.36, gpio: 13 }; + const targetPin = (update.gpio >= 0) ? update.gpio : update.channel; + const dutyCycleFraction = update.duty_pct / 100; + + // This is what the store calls: + shim.pinManager.updatePwm(targetPin, dutyCycleFraction); + + // The servo's onPwmChange callback should have been triggered + const cb = shim._getPwmCallback(); + expect(cb).not.toBeNull(); + + // Manually invoke the callback (simulating PinManager dispatching) + cb!(13, dutyCycleFraction); + + // 7.36% duty = 1472µs pulse → ~90° + expect(el.angle).toBeGreaterThanOrEqual(88); + expect(el.angle).toBeLessThanOrEqual(92); + }); + + it('ledc_update with WRONG ch=80 and gpio=-1 would NOT reach servo on pin 13', () => { + // This demonstrates the bug that was fixed: + // ch=80 (from broken & 0xFF) with gpio=-1 → updatePwm(80, duty) + // But servo listens on pin 13 → callback never fires + const shim = makeEsp32Shim(); + const el = makeElement() as any; + logic().attachEvents!(el, shim as any, pinMap({ PWM: 13 }), 'servo-bug-demo'); + + const cb = shim._getPwmCallback(); + + // With the bug: updatePwm would be called with pin=80 (wrong) + // The servo registered on pin 13, so this would NOT trigger it + // (PinManager only dispatches to callbacks registered for that pin) + expect(cb).not.toBeNull(); + + // Calling with wrong pin does nothing (servo registered on 13, not 80) + cb!(80, 0.075); // wrong pin + // angle still 0 since the real PinManager wouldn't route pin 80 to pin 13's callback + // (In our mock, the callback is directly invoked, but in production it wouldn't fire) + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// 13. LEDC polling — data type and internal config // ───────────────────────────────────────────────────────────────────────────── describe('LEDC polling — data format', () => { 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()