feat: enhance ESP32 emulation with QEMU integration and update documentation

pull/47/head
David Montero Crespo 2026-03-14 14:35:59 -03:00
parent d2e7dc2f34
commit a99a9d512f
6 changed files with 298 additions and 153 deletions

View File

@ -1,3 +1,29 @@
# ---- Stage 0: Compile lcgamboa QEMU as Linux .so (full ESP32 GPIO emulation) ----
FROM ubuntu:22.04 AS qemu-builder
RUN apt-get update && apt-get install -y --no-install-recommends \
git python3 python3-pip python3-setuptools \
ninja-build pkg-config flex bison \
gcc g++ make ca-certificates \
libglib2.0-dev libgcrypt20-dev libslirp-dev \
libpixman-1-dev libfdt-dev \
&& rm -rf /var/lib/apt/lists/*
# QEMU 8.x requires meson >= 1.0
RUN pip3 install meson
# Clone lcgamboa fork (picsimlab-esp32 branch adds GPIO/ADC/UART/RMT C callback API)
RUN git clone --depth=1 --branch picsimlab-esp32 \
https://github.com/lcgamboa/qemu /qemu-lcgamboa
WORKDIR /qemu-lcgamboa
# Build libqemu-xtensa.so (ESP32/S3) and libqemu-riscv32.so (ESP32-C3).
# The script: configures with --extra-cflags=-fPIC, builds normally, then
# re-links without softmmu_main.c.o and with -shared.
RUN bash build_libqemu-esp32.sh
# ---- Stage 1: Build frontend and wokwi-libs ---- # ---- Stage 1: Build frontend and wokwi-libs ----
FROM node:20 AS frontend-builder FROM node:20 AS frontend-builder
@ -33,11 +59,15 @@ RUN npm install && npm run build:docker
# ---- Stage 2: Final Production Image ---- # ---- Stage 2: Final Production Image ----
FROM python:3.12-slim FROM python:3.12-slim
# Install system dependencies and nginx # Install system dependencies, nginx, and QEMU .so runtime libraries
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
curl \ curl \
ca-certificates \ ca-certificates \
nginx \ nginx \
libglib2.0-0 \
libgcrypt20 \
libslirp0 \
libpixman-1-0 \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
@ -75,6 +105,19 @@ COPY --from=frontend-builder /app/frontend/dist /usr/share/nginx/html
COPY deploy/entrypoint.sh /app/entrypoint.sh COPY deploy/entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh
# ── ESP32 emulation: lcgamboa QEMU .so + ROM binaries ────────────────────────
# libqemu-xtensa.so → ESP32 / ESP32-S3 (Xtensa LX6/LX7)
# libqemu-riscv32.so → ESP32-C3 (RISC-V)
# esp32-v3-rom*.bin → boot/app ROM images required by esp32-picsimlab machine
RUN mkdir -p /app/lib
COPY --from=qemu-builder /qemu-lcgamboa/build/libqemu-xtensa.so /app/lib/
COPY --from=qemu-builder /qemu-lcgamboa/build/libqemu-riscv32.so /app/lib/
COPY --from=qemu-builder /qemu-lcgamboa/pc-bios/esp32-v3-rom.bin /app/lib/
COPY --from=qemu-builder /qemu-lcgamboa/pc-bios/esp32-v3-rom-app.bin /app/lib/
# Activate full ESP32 emulation (GPIO + ADC + UART + PWM + NeoPixel + WiFi)
ENV QEMU_ESP32_LIB=/app/lib/libqemu-xtensa.so
EXPOSE 80 EXPOSE 80
CMD ["/app/entrypoint.sh"] CMD ["/app/entrypoint.sh"]

View File

@ -96,6 +96,22 @@ Component Picker showing 48 available components with visual previews, search, a
- **UART0** serial output displayed in Serial Monitor - **UART0** serial output displayed in Serial Monitor
- **ADC** — 12-bit, 3.3V reference on GPIO 26-29 (A0-A3) - **ADC** — 12-bit, 3.3V reference on GPIO 26-29 (A0-A3)
### ESP32 Simulation (via lcgamboa QEMU)
- **Real Xtensa LX6 dual-core emulation** via [lcgamboa/qemu](https://github.com/lcgamboa/qemu) fork
- **Full GPIO** — all 40 GPIO pins with direction tracking and state callbacks
- **UART0/1/2** — multi-UART serial with baud-rate detection
- **ADC** — 12-bit, 3.3V reference on all ADC-capable pins (03300 mV injection)
- **I2C** — synchronous bus with virtual device response support
- **SPI** — full-duplex with configurable MISO byte injection
- **RMT / NeoPixel** — hardware RMT decoder with WS2812 24-bit GRB frame decoding
- **LEDC/PWM** — 16-channel duty cycle readout, mapped to LED brightness
- **WiFi** — SLIRP NAT emulation (`WiFi.begin("PICSimLabWifi", "")`)
- **arduino-esp32 2.0.17 (IDF 4.4.x)** — only compatible version with lcgamboa WiFi emulation
- **Crash detection** — banner notification in UI with dismiss button
- **Fully included in the Docker image** — zero extra setup required
See [docs/ESP32_EMULATION.md](docs/ESP32_EMULATION.md) for the complete installation guide.
### Serial Monitor ### Serial Monitor
- **Live serial output** — characters as the sketch sends them via `Serial.print()` - **Live serial output** — characters as the sketch sends them via `Serial.print()`
- **Auto baud-rate detection** — reads hardware registers, no manual configuration needed - **Auto baud-rate detection** — reads hardware registers, no manual configuration needed

View File

@ -1,4 +1,4 @@
.0from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware

View File

@ -43,6 +43,7 @@ import ctypes
import logging import logging
import os import os
import pathlib import pathlib
import sys
import tempfile import tempfile
import threading import threading
@ -51,8 +52,9 @@ logger = logging.getLogger(__name__)
# MinGW64 bin — Windows needs this on the DLL search path for glib2/libgcrypt deps # MinGW64 bin — Windows needs this on the DLL search path for glib2/libgcrypt deps
_MINGW64_BIN = r"C:\msys64\mingw64\bin" _MINGW64_BIN = r"C:\msys64\mingw64\bin"
# Default DLL path: same directory as this module (copied there after build) # Default library path: .dll on Windows, .so on Linux/macOS
_DEFAULT_LIB = str(pathlib.Path(__file__).parent / "libqemu-xtensa.dll") _LIB_NAME = "libqemu-xtensa.dll" if sys.platform == "win32" else "libqemu-xtensa.so"
_DEFAULT_LIB = str(pathlib.Path(__file__).parent / _LIB_NAME)
# ── GPIO pinmap ────────────────────────────────────────────────────────────── # ── GPIO pinmap ──────────────────────────────────────────────────────────────
# pinmap[0] = total number of pin slots (40 for ESP32) # pinmap[0] = total number of pin slots (40 for ESP32)
@ -105,6 +107,7 @@ class Esp32LibBridge:
""" """
def __init__(self, lib_path: str, loop: asyncio.AbstractEventLoop): def __init__(self, lib_path: str, loop: asyncio.AbstractEventLoop):
self._lib_path = lib_path
if os.name == 'nt' and os.path.isdir(_MINGW64_BIN): if os.name == 'nt' and os.path.isdir(_MINGW64_BIN):
os.add_dll_directory(_MINGW64_BIN) os.add_dll_directory(_MINGW64_BIN)
self._lib: ctypes.CDLL = ctypes.CDLL(lib_path) self._lib: ctypes.CDLL = ctypes.CDLL(lib_path)
@ -160,8 +163,8 @@ class Esp32LibBridge:
tmp.close() tmp.close()
self._firmware_path = tmp.name self._firmware_path = tmp.name
# ROM directory: esp32-v3-rom.bin lives beside the DLL # ROM directory: esp32-v3-rom.bin lives beside the library
rom_dir = str(pathlib.Path(_DEFAULT_LIB).parent).encode() rom_dir = str(pathlib.Path(self._lib_path).parent).encode()
args_bytes = [ args_bytes = [
b'qemu', b'qemu',

View File

@ -1,13 +1,13 @@
""" """
EspLibManager ESP32 emulation via lcgamboa libqemu-xtensa.dll. EspLibManager ESP32 emulation via lcgamboa libqemu-xtensa (.dll/.so).
Exposes the same public API as EspQemuManager so simulation.py can Exposes the same public API as EspQemuManager so simulation.py can
transparently switch between the two backends: transparently switch between the two backends:
- DLL available full GPIO + ADC + UART + I2C + SPI + RMT + WiFi (this module) - lib available full GPIO + ADC + UART + I2C + SPI + RMT + WiFi (this module)
- DLL missing serial-only via subprocess (esp_qemu_manager.py) - lib missing serial-only via subprocess (esp_qemu_manager.py)
Activation: set environment variable QEMU_ESP32_LIB to the DLL path, Activation: set environment variable QEMU_ESP32_LIB to the library path,
or place libqemu-xtensa.dll in the same directory as this module. or place libqemu-xtensa.dll (Windows) / libqemu-xtensa.so (Linux) beside this module.
Events emitted via callback(event_type, data): Events emitted via callback(event_type, data):
system {event: 'booting'|'booted'|'crash'|'reboot'} system {event: 'booting'|'booted'|'crash'|'reboot'}

View File

@ -2,28 +2,30 @@
> Estado: **Funcional** · Backend completo · Frontend completo > Estado: **Funcional** · Backend completo · Frontend completo
> Motor: **lcgamboa/qemu-8.1.3** · Plataforma: **arduino-esp32 2.0.17 (IDF 4.4.x)** > Motor: **lcgamboa/qemu-8.1.3** · Plataforma: **arduino-esp32 2.0.17 (IDF 4.4.x)**
> Disponible en: **Windows** (`.dll`) · **Linux / Docker** (`.so`, incluido en imagen oficial)
--- ---
## Índice ## Índice
1. [Instalación rápida](#1-instalación-rápida) 1. [Instalación rápida — Windows](#1-instalación-rápida--windows)
2. [Arquitectura general](#2-arquitectura-general) 2. [Instalación rápida — Docker / Linux](#2-instalación-rápida--docker--linux)
3. [Componentes del sistema](#3-componentes-del-sistema) 3. [Arquitectura general](#3-arquitectura-general)
4. [Firmware — Requisitos para lcgamboa](#4-firmware--requisitos-para-lcgamboa) 4. [Componentes del sistema](#4-componentes-del-sistema)
5. [WiFi emulada](#5-wifi-emulada) 5. [Firmware — Requisitos para lcgamboa](#5-firmware--requisitos-para-lcgamboa)
6. [I2C emulado](#6-i2c-emulado) 6. [WiFi emulada](#6-wifi-emulada)
7. [RMT / NeoPixel (WS2812)](#7-rmt--neopixel-ws2812) 7. [I2C emulado](#7-i2c-emulado)
8. [LEDC / PWM](#8-ledc--pwm) 8. [RMT / NeoPixel (WS2812)](#8-rmt--neopixel-ws2812)
9. [Compilación de la DLL](#9-compilación-de-la-dll) 9. [LEDC / PWM](#9-ledc--pwm)
10. [Tests](#10-tests) 10. [Compilar la librería manualmente](#10-compilar-la-librería-manualmente)
11. [Frontend — Eventos implementados](#11-frontend--eventos-implementados) 11. [Tests](#11-tests)
12. [Limitaciones conocidas](#12-limitaciones-conocidas) 12. [Frontend — Eventos implementados](#12-frontend--eventos-implementados)
13. [Variables de entorno](#13-variables-de-entorno) 13. [Limitaciones conocidas](#13-limitaciones-conocidas)
14. [Variables de entorno](#14-variables-de-entorno)
--- ---
## 1. Instalación rápida ## 1. Instalación rápida — Windows
Esta sección cubre todo lo necesario para tener la emulación ESP32 funcionando desde cero en Windows. Esta sección cubre todo lo necesario para tener la emulación ESP32 funcionando desde cero en Windows.
@ -100,29 +102,17 @@ La DLL es el motor principal de la emulación. Hay que compilarla una vez desde
# Asegurarse de tener el submodule # Asegurarse de tener el submodule
git submodule update --init wokwi-libs/qemu-lcgamboa git submodule update --init wokwi-libs/qemu-lcgamboa
# Abrir terminal MSYS2 MINGW64 y navegar al repo # En terminal MSYS2 MINGW64:
cd /e/Hardware/wokwi_clon # ajusta la ruta cd /e/Hardware/wokwi_clon/wokwi-libs/qemu-lcgamboa
bash build_libqemu-esp32-win.sh
# Paso 1: Configurar QEMU para Xtensa # Genera: build/libqemu-xtensa.dll y build/libqemu-riscv32.dll
cd wokwi-libs/qemu-lcgamboa
./configure \
--target-list=xtensa-softmmu \
--disable-werror \
--enable-tcg \
--enable-gcrypt \
--enable-slirp \
--enable-iconv \
--without-default-features
# Paso 2: Compilar el binario principal (5-20 min según CPU)
ninja -j$(nproc) qemu-system-xtensa.exe
# Paso 3: Relinkar como DLL (script automático)
cd /e/Hardware/wokwi_clon
bash build_qemu_step4.sh
``` ```
El script `build_qemu_step4.sh` genera `libqemu-xtensa.dll` y la copia automáticamente a `backend/app/services/`. Copia la DLL al backend:
```bash
cp build/libqemu-xtensa.dll /e/Hardware/wokwi_clon/backend/app/services/
```
**Verificar que la DLL se creó:** **Verificar que la DLL se creó:**
```bash ```bash
@ -138,7 +128,7 @@ objdump -p backend/app/services/libqemu-xtensa.dll | grep -i "qemu_picsimlab\|qe
### 1.6 Obtener los ROM binaries del ESP32 ### 1.6 Obtener los ROM binaries del ESP32
La DLL necesita dos archivos ROM de Espressif para arrancar el ESP32. Vienen incluidos en la instalación de Espressif QEMU: La DLL necesita dos archivos ROM de Espressif para arrancar el ESP32. Deben colocarse en la misma carpeta que la DLL:
**Opción A — Desde esp-qemu (si está instalado):** **Opción A — Desde esp-qemu (si está instalado):**
```bash ```bash
@ -146,13 +136,10 @@ copy "C:\esp-qemu\qemu\share\qemu\esp32-v3-rom.bin" backend\app\services\
copy "C:\esp-qemu\qemu\share\qemu\esp32-v3-rom-app.bin" backend\app\services\ copy "C:\esp-qemu\qemu\share\qemu\esp32-v3-rom-app.bin" backend\app\services\
``` ```
**Opción B — Descargar directamente:** **Opción B — Desde el submodule lcgamboa (más fácil):**
Los ROM binaries son del repositorio oficial de Espressif:
```bash ```bash
# Busca en: https://github.com/espressif/qemu/tree/esp-develop/pc-bios cp wokwi-libs/qemu-lcgamboa/pc-bios/esp32-v3-rom.bin backend/app/services/
# Descargar: esp32-v3-rom.bin y esp32-v3-rom-app.bin cp wokwi-libs/qemu-lcgamboa/pc-bios/esp32-v3-rom-app.bin backend/app/services/
# Colocarlos en backend/app/services/
``` ```
**Verificar:** **Verificar:**
@ -199,7 +186,7 @@ uvicorn app.main:app --reload --port 8001
El sistema detecta automáticamente la DLL. Verifica en los logs: El sistema detecta automáticamente la DLL. Verifica en los logs:
``` ```
INFO: libqemu-xtensa.dll found at backend/app/services/libqemu-xtensa.dll INFO: libqemu-xtensa.dll found at backend/app/services/libqemu-xtensa.dll
INFO: EspLibManager: DLL mode active (GPIO, ADC, UART, WiFi, I2C, SPI, RMT, LEDC) INFO: EspLibManager: lib mode active (GPIO, ADC, UART, WiFi, I2C, SPI, RMT, LEDC)
``` ```
Si no aparece, verifica con: Si no aparece, verifica con:
@ -207,7 +194,7 @@ Si no aparece, verifica con:
python -c " python -c "
import sys; sys.path.insert(0,'backend') import sys; sys.path.insert(0,'backend')
from app.services.esp32_lib_manager import esp_lib_manager from app.services.esp32_lib_manager import esp_lib_manager
print('DLL disponible:', esp_lib_manager.is_available()) print('lib disponible:', esp_lib_manager.is_available())
" "
``` ```
@ -235,20 +222,98 @@ El archivo `firmware.merged.bin` es el que se carga en la emulación.
--- ---
## 2. Arquitectura general ## 2. Instalación rápida — Docker / Linux
**La emulación ESP32 completa está incluida en la imagen Docker oficial.** No requiere ninguna instalación adicional — la `libqemu-xtensa.so` se compila automáticamente durante el build de la imagen a partir del fork lcgamboa.
### 2.1 Usar la imagen precompilada (recomendado)
```bash
docker run -d \
--name velxio \
-p 3080:80 \
-v $(pwd)/data:/app/data \
-e SECRET_KEY=tu-secreto \
ghcr.io/davidmonterocrespo24/velxio:master
```
La emulación ESP32 con GPIO completo está activa automáticamente. No se necesita ninguna variable de entorno adicional.
### 2.2 Build local de la imagen
```bash
git clone https://github.com/davidmonterocrespo24/velxio.git
cd velxio
docker build -f Dockerfile.standalone -t velxio .
docker run -d -p 3080:80 -e SECRET_KEY=secreto velxio
```
> **Nota de build time:** La compilación de QEMU tarda 15-30 minutos la primera vez.
> Los builds posteriores usan la capa Docker cacheada — son instantáneos mientras no
> cambie el source de lcgamboa.
### 2.3 Verificar emulación ESP32 en el container
```bash
# Verificar que .so y ROMs están presentes
docker exec <container_id> ls -lh /app/lib/
# Verificar que ctypes puede cargar la .so
docker exec <container_id> python3 -c \
"import ctypes; ctypes.CDLL('/app/lib/libqemu-xtensa.so'); print('OK')"
# Verificar que el manager la detecta
docker exec <container_id> python3 -c \
"import sys; sys.path.insert(0,'/app')
from app.services.esp32_lib_manager import esp_lib_manager
print('ESP32 lib disponible:', esp_lib_manager.is_available())"
```
### 2.4 Linux (sin Docker)
Si corres el backend directamente en Linux:
```bash
# 1. Instalar dependencias de runtime
sudo apt-get install -y libglib2.0-0 libgcrypt20 libslirp0 libpixman-1-0
# 2. Compilar la .so (requiere herramientas de build)
sudo apt-get install -y git python3-pip ninja-build pkg-config flex bison \
gcc g++ make libglib2.0-dev libgcrypt20-dev libslirp-dev libpixman-1-dev libfdt-dev
pip3 install meson
git clone --depth=1 --branch picsimlab-esp32 \
https://github.com/lcgamboa/qemu /tmp/qemu-lcgamboa
cd /tmp/qemu-lcgamboa
bash build_libqemu-esp32.sh
# → build/libqemu-xtensa.so
# 3. Copiar .so y ROMs junto al módulo Python
cp build/libqemu-xtensa.so /ruta/al/proyecto/backend/app/services/
cp pc-bios/esp32-v3-rom.bin /ruta/al/proyecto/backend/app/services/
cp pc-bios/esp32-v3-rom-app.bin /ruta/al/proyecto/backend/app/services/
# 4. Arrancar backend (auto-detecta la .so)
cd /ruta/al/proyecto/backend
uvicorn app.main:app --reload --port 8001
```
---
## 3. Arquitectura general
``` ```
Usuario (browser) Usuario (browser)
└── WebSocket (/ws/{client_id}) └── WebSocket (/ws/{client_id})
└── simulation.py (FastAPI router) └── simulation.py (FastAPI router)
├── EspLibManager ← backend con DLL (GPIO, WiFi, I2C, SPI, RMT…) ├── EspLibManager ← backend con .so/.dll (GPIO, WiFi, I2C, SPI, RMT…)
└── EspQemuManager ← fallback solo-UART via subprocess └── EspQemuManager ← fallback solo-UART via subprocess
[QEMU_ESP32_LIB=libqemu-xtensa.dll] [QEMU_ESP32_LIB=libqemu-xtensa.so|.dll]
Esp32LibBridge (ctypes) Esp32LibBridge (ctypes)
libqemu-xtensa.dll ← lcgamboa fork de QEMU 8.1.3 libqemu-xtensa.so/.dll ← lcgamboa fork de QEMU 8.1.3
Machine: esp32-picsimlab Machine: esp32-picsimlab
@ -259,47 +324,55 @@ Usuario (browser)
``` ```
El sistema selecciona backend automáticamente: El sistema selecciona backend automáticamente:
- **DLL disponible** → `EspLibManager` (GPIO completo + todos los periféricos) - **lib disponible** → `EspLibManager` (GPIO completo + todos los periféricos)
- **DLL ausente** → `EspQemuManager` (solo UART serial via TCP, subprocess QEMU) - **lib ausente** → `EspQemuManager` (solo UART serial via TCP, subprocess QEMU)
Activación de DLL: colocar `libqemu-xtensa.dll` en `backend/app/services/` o setear: Detección automática:
```bash | Plataforma | Lib buscada | Fuente |
QEMU_ESP32_LIB=C:/ruta/a/libqemu-xtensa.dll uvicorn app.main:app |------------|-------------|--------|
``` | Docker / Linux | `/app/lib/libqemu-xtensa.so` | Compilada en el Dockerfile |
| Windows (desarrollo) | `backend/app/services/libqemu-xtensa.dll` | Compilada con MSYS2 |
| Custom | `$QEMU_ESP32_LIB` | Variable de entorno |
--- ---
## 3. Componentes del sistema ## 4. Componentes del sistema
### 3.1 `libqemu-xtensa.dll` ### 4.1 `libqemu-xtensa.so` / `libqemu-xtensa.dll`
Compilada desde el fork [lcgamboa/qemu](https://github.com/lcgamboa/qemu) rama `qemu-8.1.3`. Compilada desde el fork [lcgamboa/qemu](https://github.com/lcgamboa/qemu) rama `picsimlab-esp32`.
**Dependencias en runtime (Windows) — resueltas automáticamente:** **Dependencias en runtime:**
*Windows (resueltas automáticamente desde `C:\msys64\mingw64\bin\`):*
``` ```
C:\msys64\mingw64\bin\ libglib-2.0-0.dll, libgcrypt-20.dll, libslirp-0.dll,
libglib-2.0-0.dll libgpg-error-0.dll, libintl-8.dll, libpcre2-8-0.dll (+~15 DLLs MinGW64)
libgcrypt-20.dll
libgpg-error-0.dll
libslirp-0.dll
libintl-8.dll
libpcre2-8-0.dll
(y ~15 DLLs más de MinGW64)
``` ```
El bridge las registra automáticamente con `os.add_dll_directory()`. *Linux / Docker (paquetes del sistema):*
**ROM binaries requeridas** (en la misma carpeta que la DLL):
``` ```
backend/app/services/ libglib2.0-0, libgcrypt20, libslirp0, libpixman-1-0
```
**ROM binaries requeridas** (en la misma carpeta que la lib):
```
# Windows (backend/app/services/):
libqemu-xtensa.dll ← motor principal (no en git — 43 MB) libqemu-xtensa.dll ← motor principal (no en git — 43 MB)
esp32-v3-rom.bin ← ROM de boot del ESP32 (no en git — 446 KB) esp32-v3-rom.bin ← ROM de boot del ESP32 (no en git — 446 KB)
esp32-v3-rom-app.bin ← ROM de aplicación (no en git — 446 KB) esp32-v3-rom-app.bin ← ROM de aplicación (no en git — 446 KB)
# Docker (/app/lib/):
libqemu-xtensa.so ← compilada en Stage 0 del Dockerfile
libqemu-riscv32.so ← ESP32-C3 (RISC-V)
esp32-v3-rom.bin ← copiada de pc-bios/ del repo lcgamboa
esp32-v3-rom-app.bin
``` ```
> Estos archivos están en `.gitignore` por su tamaño. Cada desarrollador los genera/obtiene localmente (ver sección 1.5 y 1.6). > En Windows estos archivos están en `.gitignore` por su tamaño. Cada desarrollador los genera localmente.
> En Docker se incluyen automáticamente en la imagen.
**Exports de la DLL:** **Exports de la librería:**
```c ```c
void qemu_init(int argc, char** argv, char** envp) void qemu_init(int argc, char** argv, char** envp)
void qemu_main_loop(void) void qemu_main_loop(void)
@ -327,7 +400,7 @@ typedef struct {
--- ---
### 3.2 GPIO Pinmap ### 4.2 GPIO Pinmap
```python ```python
# Identity mapping: QEMU IRQ slot i → GPIO number i-1 # Identity mapping: QEMU IRQ slot i → GPIO number i-1
@ -344,7 +417,7 @@ El bridge traduce automáticamente slot → GPIO real antes de notificar listene
--- ---
### 3.3 `Esp32LibBridge` (Python ctypes) ### 4.3 `Esp32LibBridge` (Python ctypes)
Archivo: `backend/app/services/esp32_lib_bridge.py` Archivo: `backend/app/services/esp32_lib_bridge.py`
@ -378,12 +451,12 @@ bridge.get_tiocm() # UART modem lines bitmask
``` ```
**Threading crítico:** **Threading crítico:**
`qemu_init()` y `qemu_main_loop()` **deben correr en el mismo thread** (BQL — Big QEMU Lock es thread-local). El bridge los ejecuta en un único daemon thread y usa `threading.Event` para sincronizar el inicio: `qemu_init()` y `qemu_main_loop()` **deben correr en el mismo thread** (BQL — Big QEMU Lock es thread-local). El bridge los ejecuta en un único daemon thread:
```python ```python
# Correcto: # Correcto:
def _qemu_thread(): def _qemu_thread():
lib.qemu_init(argc, argv, None) # init + init_done.set() lib.qemu_init(argc, argv, None) # init
lib.qemu_main_loop() # bloquea indefinidamente lib.qemu_main_loop() # bloquea indefinidamente
# Incorrecto: # Incorrecto:
@ -393,7 +466,7 @@ lib.qemu_main_loop() # en thread B ← crash: "qemu_mutex_unlock_iothread
--- ---
### 3.4 `EspLibManager` (Python) ### 4.4 `EspLibManager` (Python)
Archivo: `backend/app/services/esp32_lib_manager.py` Archivo: `backend/app/services/esp32_lib_manager.py`
@ -414,7 +487,6 @@ Convierte callbacks de hardware en **eventos WebSocket** para el frontend:
**Detección de crash y reboot:** **Detección de crash y reboot:**
```python ```python
# El firmware imprime en UART cuando crashea:
"Cache disabled but cached memory region accessed" → event: crash "Cache disabled but cached memory region accessed" → event: crash
"Rebooting..." → event: reboot "Rebooting..." → event: reboot
``` ```
@ -440,7 +512,7 @@ manager.get_status(client_id) # → dict con runtime sta
--- ---
### 3.5 `simulation.py` — Mensajes WebSocket ### 4.5 `simulation.py` — Mensajes WebSocket
**Frontend → Backend (mensajes entrantes):** **Frontend → Backend (mensajes entrantes):**
@ -460,9 +532,9 @@ manager.get_status(client_id) # → dict con runtime sta
--- ---
## 4. Firmware — Requisitos para lcgamboa ## 5. Firmware — Requisitos para lcgamboa
### 4.1 Versión de plataforma requerida ### 5.1 Versión de plataforma requerida
**✅ Usar: arduino-esp32 2.x (IDF 4.4.x)** **✅ Usar: arduino-esp32 2.x (IDF 4.4.x)**
**❌ No usar: arduino-esp32 3.x (IDF 5.x)** **❌ No usar: arduino-esp32 3.x (IDF 5.x)**
@ -480,7 +552,7 @@ Cache disabled but cached memory region accessed
EXCCAUSE: 0x00000007 EXCCAUSE: 0x00000007
``` ```
### 4.2 Imagen de flash ### 5.2 Imagen de flash
La imagen debe ser un archivo binario completo de **4 MB** (formato merged flash): La imagen debe ser un archivo binario completo de **4 MB** (formato merged flash):
@ -502,7 +574,7 @@ esptool --chip esp32 merge_bin \
El backend (`arduino_cli.py`) fuerza `FlashMode=dio` automáticamente para todos los targets `esp32:*`. El backend (`arduino_cli.py`) fuerza `FlashMode=dio` automáticamente para todos los targets `esp32:*`.
### 4.3 Sketch compatible con lcgamboa (ejemplo mínimo IRAM-safe) ### 5.3 Sketch compatible con lcgamboa (ejemplo mínimo IRAM-safe)
Para sketches que necesiten máxima compatibilidad (sin Arduino framework): Para sketches que necesiten máxima compatibilidad (sin Arduino framework):
@ -542,7 +614,7 @@ void IRAM_ATTR loop() { ets_delay_us(1000000); }
--- ---
## 5. WiFi emulada ## 6. WiFi emulada
lcgamboa implementa una WiFi simulada con SSIDs hardcoded: lcgamboa implementa una WiFi simulada con SSIDs hardcoded:
@ -560,12 +632,12 @@ El ESP32 emulado puede:
**Limitaciones:** **Limitaciones:**
- No hay forma de configurar las SSIDs o contraseñas desde Python - No hay forma de configurar las SSIDs o contraseñas desde Python
- La IP del "router" virtual es `10.0.2.2` (host Windows) - La IP del "router" virtual es `10.0.2.2` (host)
- El ESP32 emulado es accesible en `localhost:PORT` via port forwarding SLIRP - El ESP32 emulado es accesible en `localhost:PORT` via port forwarding SLIRP
--- ---
## 6. I2C emulado ## 7. I2C emulado
El callback I2C es **síncrono** — QEMU espera la respuesta antes de continuar: El callback I2C es **síncrono** — QEMU espera la respuesta antes de continuar:
@ -590,7 +662,7 @@ Desde WebSocket:
--- ---
## 7. RMT / NeoPixel (WS2812) ## 8. RMT / NeoPixel (WS2812)
El evento RMT lleva un item de 32 bits codificado así: El evento RMT lleva un item de 32 bits codificado así:
``` ```
@ -620,7 +692,7 @@ El evento emitido al frontend:
--- ---
## 8. LEDC / PWM ## 9. LEDC / PWM
`qemu_picsimlab_get_internals(0)` retorna un puntero a un array de 16 `uint32_t` con el duty cycle de cada canal LEDC. Llamar periódicamente (cada ~50 ms): `qemu_picsimlab_get_internals(0)` retorna un puntero a un array de 16 `uint32_t` con el duty cycle de cada canal LEDC. Llamar periódicamente (cada ~50 ms):
@ -633,32 +705,20 @@ El duty máximo típico es 8192 (timer de 13 bits). Para brillo de LED: `duty_pc
--- ---
## 9. Compilación de la DLL ## 10. Compilar la librería manualmente
### 9.1 Proceso completo (resumen) ### 10.1 Windows (MSYS2 MINGW64)
El script `build_libqemu-esp32-win.sh` en `wokwi-libs/qemu-lcgamboa/` automatiza el proceso:
```bash ```bash
# En MSYS2 MINGW64: # En MSYS2 MINGW64:
cd wokwi-libs/qemu-lcgamboa cd wokwi-libs/qemu-lcgamboa
bash build_libqemu-esp32-win.sh
./configure \ # Genera: build/libqemu-xtensa.dll y build/libqemu-riscv32.dll
--target-list=xtensa-softmmu \
--disable-werror \
--enable-tcg \
--enable-gcrypt \
--enable-slirp \
--enable-iconv \
--without-default-features
ninja -j$(nproc) qemu-system-xtensa.exe
# Desde bash normal (no MSYS2):
bash build_qemu_step4.sh
``` ```
### 9.2 Detalle del relink como DLL El script configura QEMU con `--extra-cflags=-fPIC` (necesario para Windows/PE con ASLR), compila el binario completo y luego relinks eliminando `softmmu_main.c.obj` (que contiene `main()`):
El proceso extrae el comando de link de `build.ninja`, elimina `softmmu_main.c.obj` (que contiene `main()`), y agrega flags de DLL:
```bash ```bash
cc -m64 -mcx16 -shared \ cc -m64 -mcx16 -shared \
@ -668,23 +728,34 @@ cc -m64 -mcx16 -shared \
@dll_link.rsp # todos los .obj excepto softmmu_main @dll_link.rsp # todos los .obj excepto softmmu_main
``` ```
### 9.3 Verificar exports ### 10.2 Linux
El script `build_libqemu-esp32.sh` produce `.so`:
```bash ```bash
objdump -p libqemu-xtensa.dll | grep -i "qemu_picsimlab\|qemu_init\|qemu_main" cd wokwi-libs/qemu-lcgamboa
# Debe mostrar: bash build_libqemu-esp32.sh
# qemu_init # Genera: build/libqemu-xtensa.so y build/libqemu-riscv32.so
# qemu_main_loop
# qemu_cleanup
# qemu_picsimlab_register_callbacks
# qemu_picsimlab_set_pin
# qemu_picsimlab_set_apin
# qemu_picsimlab_uart_receive
# qemu_picsimlab_get_internals
# qemu_picsimlab_get_TIOCM
``` ```
### 9.4 Parche requerido en scripts/symlink-install-tree.py ### 10.3 Verificar exports (ambas plataformas)
```bash
# Linux:
nm -D build/libqemu-xtensa.so | grep -i "qemu_picsimlab\|qemu_init\|qemu_main"
# Windows:
objdump -p build/libqemu-xtensa.dll | grep -i "qemu_picsimlab\|qemu_init"
# Debe mostrar:
# qemu_init, qemu_main_loop, qemu_cleanup
# qemu_picsimlab_register_callbacks
# qemu_picsimlab_set_pin, qemu_picsimlab_set_apin
# qemu_picsimlab_uart_receive
# qemu_picsimlab_get_internals, qemu_picsimlab_get_TIOCM
```
### 10.4 Parche requerido en Windows (symlink-install-tree.py)
Windows no permite crear symlinks sin privilegios de administrador. El script de QEMU falla con `WinError 1314`. Parche aplicado: Windows no permite crear symlinks sin privilegios de administrador. El script de QEMU falla con `WinError 1314`. Parche aplicado:
@ -703,9 +774,9 @@ if os.name == 'nt':
--- ---
## 10. Tests ## 11. Tests
### 10.1 Test suite principal (28 tests) ### 11.1 Test suite principal (28 tests)
Archivo: `test/esp32/test_esp32_lib_bridge.py` Archivo: `test/esp32/test_esp32_lib_bridge.py`
@ -716,13 +787,13 @@ python -m pytest test/esp32/test_esp32_lib_bridge.py -v
| Grupo | Tests | Qué verifica | | Grupo | Tests | Qué verifica |
|-------|-------|--------------| |-------|-------|--------------|
| `TestDllExists` | 5 | Rutas de DLL, ROM binaries, MinGW64 | | `TestDllExists` | 5 | Rutas de lib, ROM binaries, dependencias de plataforma |
| `TestDllLoads` | 3 | Carga de DLL, symbols exportados | | `TestDllLoads` | 3 | Carga de lib, symbols exportados |
| `TestPinmap` | 3 | Estructura del pinmap, GPIO2 en slot 3 | | `TestPinmap` | 3 | Estructura del pinmap, GPIO2 en slot 3 |
| `TestManagerAvailability` | 2 | `is_available()`, API surface | | `TestManagerAvailability` | 2 | `is_available()`, API surface |
| `TestEsp32LibIntegration` | 15 | QEMU real con firmware blink: boot, UART, GPIO, ADC, SPI, I2C | | `TestEsp32LibIntegration` | 15 | QEMU real con firmware blink: boot, UART, GPIO, ADC, SPI, I2C |
### 10.2 Test integración Arduino ↔ ESP32 (13 tests) ### 11.2 Test integración Arduino ↔ ESP32 (13 tests)
Archivo: `test/esp32/test_arduino_esp32_integration.py` Archivo: `test/esp32/test_arduino_esp32_integration.py`
@ -747,13 +818,13 @@ python -m pytest test/esp32/test_arduino_esp32_integration.py -v
**Firmware de test:** `test/esp32-emulator/binaries_lcgamboa/serial_led.ino.merged.bin` **Firmware de test:** `test/esp32-emulator/binaries_lcgamboa/serial_led.ino.merged.bin`
Sketch fuente: `test/esp32-emulator/sketches/serial_led/serial_led.ino` Sketch fuente: `test/esp32-emulator/sketches/serial_led/serial_led.ino`
### 10.3 Omitir tests de integración (solo unitarios) ### 11.3 Omitir tests de integración (solo unitarios)
```bash ```bash
SKIP_LIB_INTEGRATION=1 python -m pytest test/esp32/ -v SKIP_LIB_INTEGRATION=1 python -m pytest test/esp32/ -v
``` ```
### 10.4 Recompilar el firmware de test ### 11.4 Recompilar el firmware de test
Si necesitas recompilar los binarios de test: Si necesitas recompilar los binarios de test:
@ -787,7 +858,7 @@ esptool --chip esp32 merge_bin --fill-flash-size 4MB \
--- ---
## 11. Frontend — Eventos implementados ## 12. Frontend — Eventos implementados
Todos los eventos del backend están conectados al frontend: Todos los eventos del backend están conectados al frontend:
@ -826,37 +897,49 @@ bridge.setSpiResponse(response) // Byte MISO de dispositivo SPI
--- ---
## 12. Limitaciones conocidas (no solucionables sin modificar QEMU) ## 13. Limitaciones conocidas (no solucionables sin modificar QEMU)
| Limitación | Causa | Workaround | | Limitación | Causa | Workaround |
|------------|-------|------------| |------------|-------|------------|
| **Una sola instancia ESP32 por proceso** | QEMU usa estado global en variables estáticas | Lanzar múltiples procesos Python | | **Una sola instancia ESP32 por proceso** | QEMU usa estado global en variables estáticas | Lanzar múltiples procesos Python |
| **WiFi solo con SSIDs hardcoded** | lcgamboa codifica "PICSimLabWifi" y "Espressif" en C | Modificar y recompilar la DLL | | **WiFi solo con SSIDs hardcoded** | lcgamboa codifica "PICSimLabWifi" y "Espressif" en C | Modificar y recompilar la lib |
| **Sin BLE / Bluetooth Classic** | No implementado en lcgamboa | No disponible | | **Sin BLE / Bluetooth Classic** | No implementado en lcgamboa | No disponible |
| **Sin touch capacitivo** | `touchRead()` no tiene callback en picsimlab | No disponible | | **Sin touch capacitivo** | `touchRead()` no tiene callback en picsimlab | No disponible |
| **Sin DAC** | GPIO25/GPIO26 analógico no expuesto por picsimlab | No disponible | | **Sin DAC** | GPIO25/GPIO26 analógico no expuesto por picsimlab | No disponible |
| **Flash fija en 4MB** | Hardcoded en la machine esp32-picsimlab | Recompilar DLL | | **Flash fija en 4MB** | Hardcoded en la machine esp32-picsimlab | Recompilar lib |
| **arduino-esp32 3.x causa crash** | IDF 5.x maneja caché diferente al WiFi emulado | Usar 2.x (IDF 4.4.x) | | **arduino-esp32 3.x causa crash** | IDF 5.x maneja caché diferente al WiFi emulado | Usar 2.x (IDF 4.4.x) |
--- ---
## 13. Variables de entorno ## 14. Variables de entorno
| Variable | Valor | Efecto | | Variable | Valor de ejemplo | Efecto |
|----------|-------|--------| |----------|-----------------|--------|
| `QEMU_ESP32_LIB` | ruta a `libqemu-xtensa.dll` | Fuerza ruta de DLL (override auto-detect) | | `QEMU_ESP32_LIB` | `/app/lib/libqemu-xtensa.so` | Fuerza ruta de lib (override auto-detect) |
| `QEMU_ESP32_BINARY` | ruta a `qemu-system-xtensa.exe` | Fallback subprocess (sin DLL) | | `QEMU_ESP32_BINARY` | `/usr/bin/qemu-system-xtensa` | Fallback subprocess (sin lib) |
| `SKIP_LIB_INTEGRATION` | `1` | Omite tests de integración QEMU en pytest | | `SKIP_LIB_INTEGRATION` | `1` | Omite tests de integración QEMU en pytest |
Si `QEMU_ESP32_LIB` no está seteado, el sistema busca `libqemu-xtensa.dll` en la misma carpeta que `esp32_lib_bridge.py` (`backend/app/services/`). **Auto-detección por plataforma:**
| Plataforma | Lib buscada automáticamente |
|------------|----------------------------|
| Docker / Linux | `/app/lib/libqemu-xtensa.so` (via `QEMU_ESP32_LIB`) |
| Windows | `backend/app/services/libqemu-xtensa.dll` |
| Custom | `$QEMU_ESP32_LIB` (si está seteado, tiene prioridad) |
**Ejemplos de arranque:**
**Ejemplo arranque completo:**
```bash ```bash
# Con DLL (emulación completa GPIO + WiFi + ADC + I2C + SPI + RMT + LEDC): # Docker — todo automático, no requiere variables extra:
docker run -d -p 3080:80 -e SECRET_KEY=secreto ghcr.io/davidmonterocrespo24/velxio:master
# Windows con lib (emulación completa GPIO + WiFi + ADC + I2C + SPI + RMT + LEDC):
cd backend && venv\Scripts\activate cd backend && venv\Scripts\activate
uvicorn app.main:app --reload --port 8001 uvicorn app.main:app --reload --port 8001
# Sin DLL (fallback: solo UART serial via subprocess QEMU): # Linux con lib en ruta custom:
QEMU_ESP32_BINARY=C:/esp-qemu/qemu/bin/qemu-system-xtensa.exe \ QEMU_ESP32_LIB=/opt/velxio/libqemu-xtensa.so uvicorn app.main:app --port 8001
uvicorn app.main:app --reload --port 8001
# Sin lib (fallback: solo UART serial via subprocess QEMU):
QEMU_ESP32_BINARY=/usr/bin/qemu-system-xtensa uvicorn app.main:app --port 8001
``` ```