feat: pre-built QEMU binaries from GitHub Release + WiFi SSID normalization

- Dockerfile: download pre-built .so + ROM from velxio public release
  instead of building from private qemu-lcgamboa source
- espidf_compiler: normalize any WiFi SSID → "Velxio-GUEST" for QEMU
  compatibility (channel 6, open auth)
- docker-compose.yml: unified dev/prod using Dockerfile.standalone
- .dockerignore: exclude qemu-lcgamboa source from Docker context
- .gitignore: ignore prebuilt/ binaries, keep .gitkeep

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pull/91/head
David Montero Crespo 2026-04-04 14:27:52 -03:00
parent 1afcbf7718
commit 0a724b7566
6 changed files with 115 additions and 62 deletions

View File

@ -15,6 +15,9 @@ frontend/.vite/
wokwi-libs/avr8js/dist/
wokwi-libs/wokwi-elements/dist/
# QEMU source & Windows builds (only prebuilt/qemu/ .so files are needed)
wokwi-libs/qemu-lcgamboa/
# IDE and OS
.vscode/
.idea/

6
.gitignore vendored
View File

@ -89,6 +89,12 @@ backend/app/services/esp32-v3-rom.bin
backend/app/services/esp32-v3-rom-app.bin
backend/app/services/esp32c3-rom.bin
# Pre-built QEMU .so for Docker (built by build-qemu.sh — ~30MB Linux binaries)
prebuilt/*
!prebuilt/qemu/
prebuilt/qemu/*
!prebuilt/qemu/.gitkeep
# ESP32 build artifacts (ELF/MAP debug symbols — large, not needed for tests)
test/esp32-emulator/**/*.elf
test/esp32-emulator/**/*.map

View File

@ -1,27 +1,30 @@
# ---- Stage 0: Compile lcgamboa QEMU as Linux .so (full ESP32 GPIO emulation) ----
FROM ubuntu:22.04 AS qemu-builder
# ---- Stage 0: QEMU .so + ROM binaries ----
# Two modes:
# 1. Local: place files in prebuilt/qemu/ (from build-qemu.sh or manual copy)
# 2. CI/CD: downloads from GitHub Release (requires qemu-lcgamboa repo to be public)
# The COPY always runs; the RUN only downloads missing files.
FROM ubuntu:22.04 AS qemu-provider
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 \
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# QEMU 8.x requires meson >= 1.0
RUN pip3 install meson
ARG QEMU_RELEASE_URL=https://github.com/davidmonterocrespo24/velxio/releases/download/qemu-prebuilt
# 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
# Copy the prebuilt directory (may contain .so+ROM files or just the .gitkeep)
RUN mkdir -p /qemu
COPY prebuilt/qemu/ /qemu/
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
# Download any missing files from GitHub Release
RUN cd /qemu \
&& for f in libqemu-xtensa.so libqemu-riscv32.so esp32-v3-rom.bin esp32-v3-rom-app.bin esp32c3-rom.bin; do \
if [ ! -f "$f" ]; then \
echo "Downloading $f from release..." ; \
curl -fSL -o "$f" "${QEMU_RELEASE_URL}/$f" ; \
else \
echo "Using local $f ($(stat -c%s "$f") bytes)" ; \
fi ; \
done \
&& ls -lh /qemu/
# ---- Stage 0.5: ESP-IDF toolchain for ESP32 compilation ----
@ -137,7 +140,8 @@ COPY --from=frontend-builder /app/frontend/dist /usr/share/nginx/html
COPY deploy/entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
# ── ESP32 emulation: lcgamboa QEMU .so + ROM binaries ────────────────────────
# ── ESP32 emulation: pre-built QEMU .so + ROM binaries ──────────────────────
# Downloaded from GitHub Release (public — no access to qemu-lcgamboa needed)
# libqemu-xtensa.so → ESP32 / ESP32-S3 (Xtensa LX6/LX7)
# libqemu-riscv32.so → ESP32-C3 (RISC-V RV32IMC)
# esp32-v3-rom*.bin → boot/app ROM images required by esp32-picsimlab machine
@ -145,11 +149,7 @@ RUN chmod +x /app/entrypoint.sh
# NOTE: ROM files must live in the same directory as the .so (worker passes -L
# to QEMU pointing at os.path.dirname(lib_path))
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/
COPY --from=qemu-builder /qemu-lcgamboa/pc-bios/esp32c3-rom.bin /app/lib/
COPY --from=qemu-provider /qemu/ /app/lib/
# Activate ESP32 emulation
# QEMU_ESP32_LIB → Xtensa library (ESP32, ESP32-S3)

View File

@ -105,6 +105,62 @@ class ESPIDFCompiler:
code
))
def _normalize_wifi_for_qemu(self, code: str) -> str:
"""
Normalize WiFi SSID/password/channel in Arduino sketches for QEMU.
QEMU's WiFi AP broadcasts "Velxio-GUEST" on channel 6 with open auth.
This method rewrites the user's sketch so that:
- Any SSID string literal "Velxio-GUEST"
- Password "" (open auth)
- Channel 6
The user's editor still shows their original code; only the compiled
binary is modified.
"""
if not self._detect_wifi_usage(code):
return code
# 1) Replace SSID variable definitions:
# const char* ssid = "anything" → "Velxio-GUEST"
# char ssid[] = "anything" → "Velxio-GUEST"
# #define WIFI_SSID "anything" → "Velxio-GUEST"
code = re.sub(
r'((?:const\s+)?char\s*\*?\s*ssid\s*\[?\]?\s*=\s*)"[^"]*"',
rf'\1"{_QEMU_WIFI_SSID}"',
code,
flags=re.IGNORECASE
)
code = re.sub(
r'(#define\s+\w*SSID\w*\s+)"[^"]*"',
rf'\1"{_QEMU_WIFI_SSID}"',
code,
flags=re.IGNORECASE
)
# 2) Normalize WiFi.begin() calls:
# WiFi.begin("X") → WiFi.begin("Velxio-GUEST", "", 6)
# WiFi.begin("X", "pass") → WiFi.begin("Velxio-GUEST", "", 6)
# WiFi.begin(ssid, pass, N) → WiFi.begin(ssid, "", 6)
# WiFi.begin(ssid) → WiFi.begin(ssid, "", 6)
def _rewrite_wifi_begin(m: re.Match) -> str:
args = m.group(1)
parts = [a.strip() for a in args.split(',')]
ssid_arg = parts[0]
# If SSID is a string literal, force to Velxio-GUEST
if ssid_arg.startswith('"'):
ssid_arg = f'"{_QEMU_WIFI_SSID}"'
return f'WiFi.begin({ssid_arg}, "", 6)'
code = re.sub(
r'WiFi\.begin\s*\(([^)]+)\)',
_rewrite_wifi_begin,
code
)
logger.info('[espidf] WiFi normalized: SSID→%s, channel→6, open auth', _QEMU_WIFI_SSID)
return code
def _translate_sketch_to_espidf(self, sketch_code: str) -> str:
"""
Translate an Arduino WiFi+WebServer sketch to pure ESP-IDF C code.
@ -400,26 +456,11 @@ class ESPIDFCompiler:
if not main_content and files:
main_content = files[0]['content']
# Auto-fix missing channel argument in WiFi.begin for QEMU compatibility
# QEMU's mock WiFi AP is on channel 6, and active scanning hangs the simulator.
# Match WiFi.begin(...) without a channel, catching both literals and variables.
# 1 arg: WiFi.begin(ssid) -> WiFi.begin(ssid, "", 6)
main_content = re.sub(
r'WiFi\.begin\s*\(\s*([^,]+?)\s*\)',
r'WiFi.begin(\1, "", 6)',
main_content
)
# 2 args: WiFi.begin(ssid, pass) -> WiFi.begin(ssid, pass, 6)
# Exclude matching if the second arg is obviously an int channel (which is wrong but possible)
# We match if there are exactly two args. We have to be careful with commas.
main_content = re.sub(
r'WiFi\.begin\s*\(\s*([^,]+?)\s*,\s*([^,]+?)\s*\)',
r'WiFi.begin(\1, \2, 6)',
main_content
)
# Since QEMU's AP accepts any SSID (as long as it's on channel 6),
# we let the user use Velxio-GUEST or whatever they want.
# ── QEMU WiFi compatibility ──────────────────────────────────────
# QEMU's WiFi AP broadcasts "Velxio-GUEST" on channel 6.
# We normalize ANY user SSID → "Velxio-GUEST", enforce channel 6,
# and use open auth (empty password) so the connection always works.
main_content = self._normalize_wifi_for_qemu(main_content)
if self.has_arduino:
# Arduino-as-component mode: copy sketch as .cpp

View File

@ -1,30 +1,32 @@
# Development Docker Compose — same image as production for full parity.
# Usage: docker compose up --build
# Access: http://localhost:3080
services:
backend:
velxio:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "8001:8001"
context: .
dockerfile: Dockerfile.standalone
container_name: velxio-dev
restart: unless-stopped
ports:
- "3080:80"
env_file:
- ./backend/.env
environment:
- DATABASE_URL=sqlite+aiosqlite:////app/data/velxio.db
- DATA_DIR=/app/data
- IDF_PATH=/opt/esp-idf
- IDF_TOOLS_PATH=/root/.espressif
- ARDUINO_ESP32_PATH=/opt/arduino-esp32
volumes:
- ./data:/app/data
- arduino-libs:/root/.arduino15
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8001/health')"]
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
start_period: 90s
frontend:
build:
context: .
dockerfile: frontend/Dockerfile
ports:
- "3000:80"
depends_on:
backend:
condition: service_healthy
restart: unless-stopped
volumes:
arduino-libs:

1
prebuilt/qemu/.gitkeep Normal file
View File

@ -0,0 +1 @@