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
parent
1afcbf7718
commit
0a724b7566
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
|
||||
Loading…
Reference in New Issue