From 0a724b7566fe21560398546fc21650302a784c05 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Sat, 4 Apr 2026 14:27:52 -0300 Subject: [PATCH] feat: pre-built QEMU binaries from GitHub Release + WiFi SSID normalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .dockerignore | 3 + .gitignore | 6 ++ Dockerfile.standalone | 50 +++++++-------- backend/app/services/espidf_compiler.py | 81 +++++++++++++++++++------ docker-compose.yml | 36 +++++------ prebuilt/qemu/.gitkeep | 1 + 6 files changed, 115 insertions(+), 62 deletions(-) create mode 100644 prebuilt/qemu/.gitkeep diff --git a/.dockerignore b/.dockerignore index 8b51619..b0c3f24 100644 --- a/.dockerignore +++ b/.dockerignore @@ -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/ diff --git a/.gitignore b/.gitignore index 7980fdc..1214e91 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Dockerfile.standalone b/Dockerfile.standalone index 90444f0..69ab6be 100644 --- a/Dockerfile.standalone +++ b/Dockerfile.standalone @@ -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) diff --git a/backend/app/services/espidf_compiler.py b/backend/app/services/espidf_compiler.py index b9e80a6..890de4a 100644 --- a/backend/app/services/espidf_compiler.py +++ b/backend/app/services/espidf_compiler.py @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index be38022..8500a4c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/prebuilt/qemu/.gitkeep b/prebuilt/qemu/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/prebuilt/qemu/.gitkeep @@ -0,0 +1 @@ +