Merge pull request #48 from davidmonterocrespo24/feat/raspberryPi_emuation

Feat/raspberry pi emuation
pull/52/head
David Montero Crespo 2026-03-23 11:30:54 -03:00 committed by GitHub
commit 42f24c8569
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 222 additions and 638 deletions

View File

@ -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:

View File

@ -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<number, number> {
const ledcGpioMap = new Map<number, number>();
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', () => {

View File

@ -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 => (
<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`, `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<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 `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
? <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`):
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

View File

@ -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."

View File

@ -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()