velxio/fancy-splashing-shannon.md

11 KiB

Plan: Construcción de Emulador Arduino Local (Wokwi Clone)

Resumen Ejecutivo

Crear una aplicación web local que permita editar, compilar y emular código Arduino con visualización de componentes electrónicos en tiempo real.

Arquitectura: Monolito web (React + Vite) con backend FastAPI para compilación Prioridades: Editor de código + compilación, emulación Arduino Uno, componentes básicos (LED, resistencias) Persistencia: SQLite

Tecnologías Core

Frontend

  • React + Vite + TypeScript - Framework principal
  • Monaco Editor (@monaco-editor/react) - Editor de código
  • avr8js - Emulador AVR8 (ATmega328p = Arduino Uno)
  • @wokwi/elements - Componentes electrónicos web (LEDs, resistencias)
  • Zustand - State management

Backend

  • FastAPI + Python - API REST
  • arduino-cli - Compilador de Arduino
  • SQLAlchemy + SQLite - Base de datos

Estructura del Proyecto

wokwi_clon/
├── frontend/                         # React + Vite
│   ├── src/
│   │   ├── components/
│   │   │   ├── editor/
│   │   │   │   ├── CodeEditor.tsx       # Monaco Editor wrapper
│   │   │   │   └── EditorToolbar.tsx    # Compile/Run/Stop
│   │   │   ├── simulator/
│   │   │   │   ├── SimulatorCanvas.tsx  # Canvas principal
│   │   │   │   └── ArduinoBoard.tsx     # Visualización Arduino Uno
│   │   │   ├── components-wokwi/
│   │   │   │   ├── LED.tsx              # Wrapper para wokwi-led
│   │   │   │   └── Resistor.tsx         # Wrapper para wokwi-resistor
│   │   │   └── projects/
│   │   │       ├── ProjectList.tsx      # Lista de proyectos
│   │   │       └── ProjectDialog.tsx    # Guardar/Cargar
│   │   ├── simulation/
│   │   │   ├── AVRSimulator.ts          # Core: avr8js wrapper
│   │   │   ├── PinManager.ts            # Gestión de pines
│   │   │   └── ComponentRegistry.ts     # Registro de componentes
│   │   ├── store/
│   │   │   ├── useSimulatorStore.ts     # Estado de simulación (Zustand)
│   │   │   ├── useEditorStore.ts        # Estado del editor
│   │   │   └── useProjectStore.ts       # Estado de proyectos
│   │   ├── services/
│   │   │   ├── api.ts                   # Cliente API
│   │   │   └── compilation.ts           # Servicio de compilación
│   │   └── utils/
│   │       └── hexParser.ts             # Parser de archivos .hex
│   ├── package.json
│   └── vite.config.ts
│
├── backend/                          # Python + FastAPI
│   ├── app/
│   │   ├── main.py                      # Entry point
│   │   ├── api/routes/
│   │   │   ├── compile.py               # POST /api/compile
│   │   │   └── projects.py              # CRUD proyectos
│   │   ├── services/
│   │   │   └── arduino_cli.py           # Integración arduino-cli
│   │   ├── models/
│   │   │   └── project.py               # Modelo SQLAlchemy
│   │   └── database/
│   │       └── connection.py            # Conexión SQLite
│   └── requirements.txt
│
└── README.md

Flujo de Datos Principal

1. Compilación (Editor → Backend → Hex)

Usuario escribe código en Monaco Editor
  ↓
Click "Compile"
  ↓
POST /api/compile { code: "...", board_fqbn: "arduino:avr:uno" }
  ↓
Backend: arduino-cli compila código a .hex
  ↓
Backend retorna { success: true, hex_content: "..." }
  ↓
Frontend: useSimulatorStore.loadHex(hex)

2. Emulación (Hex → CPU → Pines → Componentes)

AVRSimulator.loadHex(hexString)
  ↓
Parse hex → Uint16Array (program memory)
  ↓
Inicializar CPU (ATmega328p)
  ↓
Click "Run"
  ↓
Execution loop (requestAnimationFrame)
  ↓
CPU ejecuta ~267k cycles/frame @ 60fps
  ↓
CPU escribe en PORTB/PORTC/PORTD
  ↓
Write hooks → PinManager.updatePort()
  ↓
PinManager notifica componentes conectados
  ↓
LED actualiza estado visual (ref.current.value = true/false)

Archivos Críticos para Implementar

1. frontend/src/simulation/AVRSimulator.ts

Motor de emulación

  • Integra avr8js (CPU, AVRTimer, AVRUSART)
  • Carga archivos .hex a memoria de programa
  • Loop de ejecución con requestAnimationFrame
  • Write hooks en registros PORT (0x25=PORTB, 0x28=PORTC, 0x2B=PORTD)
export class AVRSimulator {
  private cpu: CPU | null = null;
  private program: Uint16Array | null = null;

  loadHex(hexContent: string) {
    const bytes = hexToUint8Array(hexContent);
    this.program = new Uint16Array(16384); // 32KB
    // Load bytes into program memory...
    this.cpu = new CPU(this.program);
    this.setupPinHooks();
  }

  start() {
    const execute = () => {
      for (let i = 0; i < 267000; i++) {
        this.cpu.tick();
      }
      requestAnimationFrame(execute);
    };
    requestAnimationFrame(execute);
  }
}

2. frontend/src/components/components-wokwi/LED.tsx

Wrapper React para Web Components

  • Importa @wokwi/elements
  • Usa useRef para manipular DOM directamente
  • Propiedades via asignación directa (no atributos)
import '@wokwi/elements';

export const LED = ({ color, value, x, y }) => {
  const ledRef = useRef<HTMLElement>(null);

  useEffect(() => {
    if (ledRef.current) {
      (ledRef.current as any).value = value;
      (ledRef.current as any).color = color;
    }
  }, [value, color]);

  return <wokwi-led ref={ledRef} style={{ position: 'absolute', left: x, top: y }} />;
};

3. backend/app/services/arduino_cli.py

Integración con arduino-cli

  • Compilación asíncrona con asyncio
  • Manejo de archivos temporales
  • Parse de errores de compilación
async def compile(code: str, board_fqbn: str = "arduino:avr:uno") -> dict:
    with tempfile.TemporaryDirectory() as temp_dir:
        sketch_dir = Path(temp_dir) / "sketch"
        sketch_dir.mkdir()
        (sketch_dir / "sketch.ino").write_text(code)

        process = await asyncio.create_subprocess_exec(
            "arduino-cli", "compile",
            "--fqbn", board_fqbn,
            "--output-dir", str(sketch_dir / "build"),
            str(sketch_dir),
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE
        )

        stdout, stderr = await process.communicate()

        if process.returncode == 0:
            hex_file = sketch_dir / "build" / "sketch.ino.hex"
            return {
                "success": True,
                "hex_content": hex_file.read_text()
            }
        else:
            return {
                "success": False,
                "error": stderr.decode()
            }

4. frontend/src/store/useSimulatorStore.ts

Estado global de simulación (Zustand)

  • Simulator instance
  • Estado running/stopped
  • Lista de componentes
  • Actions: loadHex, start, stop, addComponent
export const useSimulatorStore = create<SimulatorState>((set, get) => ({
  simulator: null,
  running: false,
  components: [],

  loadHex: (hex: string) => {
    const { simulator } = get();
    simulator?.loadHex(hex);
  },

  startSimulation: () => {
    get().simulator?.start();
    set({ running: true });
  },

  addComponent: (component) => {
    set(state => ({
      components: [...state.components, component]
    }));
  }
}));

5. backend/app/models/project.py

Modelo de base de datos

class Project(Base):
    __tablename__ = "projects"

    id = Column(Integer, primary_key=True)
    name = Column(String(255), nullable=False)
    code = Column(Text, nullable=False)
    circuit = Column(JSON)  # { components: [...], wires: [...] }
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, onupdate=datetime.utcnow)

Circuit JSON Structure:

{
  "components": [
    { "id": "led1", "type": "led", "x": 250, "y": 150, "properties": { "color": "red" } }
  ],
  "wires": [
    { "from": { "component": "arduino", "pin": 13 }, "to": { "component": "led1", "pin": "A" } }
  ]
}

Dependencias Clave

Frontend

{
  "@monaco-editor/react": "^4.6.0",
  "avr8js": "^0.30.0",
  "@wokwi/elements": "^1.9.1",
  "zustand": "^4.5.0",
  "axios": "^1.7.0",
  "react": "^18.3.1",
  "vite": "^5.4.0"
}

Backend

fastapi==0.115.0
uvicorn[standard]==0.32.0
sqlalchemy==2.0.36
aiosqlite==0.20.0
pydantic==2.9.2

Herramientas Externas

# Instalar arduino-cli
# Windows:
choco install arduino-cli

# Inicializar:
arduino-cli core update-index
arduino-cli core install arduino:avr

Fases de Implementación

Fase 1: Foundation (Prioridad Alta)

  • [frontend] Inicializar proyecto Vite + React + TypeScript
  • [frontend] Integrar Monaco Editor con syntax highlighting C++
  • [frontend] Layout básico (editor izquierda, canvas derecha)
  • [backend] Setup FastAPI + endpoint /api/compile
  • [backend] Integración arduino-cli para compilación
  • Entregable: Compilar código y recibir .hex

Fase 2: Emulation Core (Prioridad Alta)

  • [frontend] Implementar AVRSimulator.ts con avr8js
  • [frontend] Parser de archivos .hex
  • [frontend] PinManager para tracking de pines
  • [frontend] Botones Run/Stop/Reset
  • Entregable: Ejecutar Blink example y ver cambios de pin

Fase 3: Visual Components (Prioridad Alta)

  • [frontend] Wrappers React para wokwi-led, wokwi-resistor
  • [frontend] SimulatorCanvas con drag & drop
  • [frontend] Conectar LEDs a PinManager
  • Entregable: LED se enciende/apaga con digitalWrite()

Fase 4: Project Persistence (Prioridad Media)

  • [backend] Setup SQLite con SQLAlchemy
  • [backend] CRUD endpoints para proyectos
  • [frontend] UI para guardar/cargar proyectos
  • Entregable: Persistir código + circuito

Fase 5: Polish (Prioridad Baja)

  • Más componentes (botones, potenciómetros)
  • Serial monitor
  • Control de velocidad
  • Ejemplos pre-cargados
  • Entregable: App completa y pulida

Puntos Críticos de Integración

Monaco Editor en Vite

  • Usar @monaco-editor/react (no monaco-editor directamente)
  • No requiere configuración especial de Vite
  • Syntax highlighting C++ funciona con defaultLanguage="cpp"

Web Components en React

  • Web Components requieren manipulación directa del DOM
  • Usar useRef + useEffect para setear propiedades
  • Declarar tipos JSX en vite-env.d.ts

avr8js Performance

  • Ejecutar en batches (~267k cycles/frame @ 16MHz/60fps)
  • Usar requestAnimationFrame para smooth simulation
  • Evitar re-renders de React (usar refs)

arduino-cli

  • Sketch name debe coincidir con directory name
  • Output: <sketch_name>.ino.hex
  • Requiere arduino:avr core instalado

Comandos de Desarrollo

# Backend
cd backend
python -m venv venv
venv\Scripts\activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000

# Frontend
cd frontend
npm install
npm run dev

# Acceso
Frontend: http://localhost:5173
Backend:  http://localhost:8000
API Docs: http://localhost:8000/docs

Archivos de Configuración Esenciales

frontend/vite.config.ts

frontend/tsconfig.json

frontend/package.json

backend/app/main.py

backend/requirements.txt