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/avr8js/dist/
|
||||||
wokwi-libs/wokwi-elements/dist/
|
wokwi-libs/wokwi-elements/dist/
|
||||||
|
|
||||||
|
# QEMU source & Windows builds (only prebuilt/qemu/ .so files are needed)
|
||||||
|
wokwi-libs/qemu-lcgamboa/
|
||||||
|
|
||||||
# IDE and OS
|
# IDE and OS
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,12 @@ backend/app/services/esp32-v3-rom.bin
|
||||||
backend/app/services/esp32-v3-rom-app.bin
|
backend/app/services/esp32-v3-rom-app.bin
|
||||||
backend/app/services/esp32c3-rom.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)
|
# ESP32 build artifacts (ELF/MAP debug symbols — large, not needed for tests)
|
||||||
test/esp32-emulator/**/*.elf
|
test/esp32-emulator/**/*.elf
|
||||||
test/esp32-emulator/**/*.map
|
test/esp32-emulator/**/*.map
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,30 @@
|
||||||
# ---- Stage 0: Compile lcgamboa QEMU as Linux .so (full ESP32 GPIO emulation) ----
|
# ---- Stage 0: QEMU .so + ROM binaries ----
|
||||||
FROM ubuntu:22.04 AS qemu-builder
|
# 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 \
|
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
|
||||||
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/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# QEMU 8.x requires meson >= 1.0
|
ARG QEMU_RELEASE_URL=https://github.com/davidmonterocrespo24/velxio/releases/download/qemu-prebuilt
|
||||||
RUN pip3 install meson
|
|
||||||
|
|
||||||
# Clone lcgamboa fork (picsimlab-esp32 branch adds GPIO/ADC/UART/RMT C callback API)
|
# Copy the prebuilt directory (may contain .so+ROM files or just the .gitkeep)
|
||||||
RUN git clone --depth=1 --branch picsimlab-esp32 \
|
RUN mkdir -p /qemu
|
||||||
https://github.com/lcgamboa/qemu /qemu-lcgamboa
|
COPY prebuilt/qemu/ /qemu/
|
||||||
|
|
||||||
WORKDIR /qemu-lcgamboa
|
# Download any missing files from GitHub Release
|
||||||
|
RUN cd /qemu \
|
||||||
# Build libqemu-xtensa.so (ESP32/S3) and libqemu-riscv32.so (ESP32-C3).
|
&& for f in libqemu-xtensa.so libqemu-riscv32.so esp32-v3-rom.bin esp32-v3-rom-app.bin esp32c3-rom.bin; do \
|
||||||
# The script: configures with --extra-cflags=-fPIC, builds normally, then
|
if [ ! -f "$f" ]; then \
|
||||||
# re-links without softmmu_main.c.o and with -shared.
|
echo "Downloading $f from release..." ; \
|
||||||
RUN bash build_libqemu-esp32.sh
|
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 ----
|
# ---- 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
|
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 ────────────────────────
|
# ── 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-xtensa.so → ESP32 / ESP32-S3 (Xtensa LX6/LX7)
|
||||||
# libqemu-riscv32.so → ESP32-C3 (RISC-V RV32IMC)
|
# libqemu-riscv32.so → ESP32-C3 (RISC-V RV32IMC)
|
||||||
# esp32-v3-rom*.bin → boot/app ROM images required by esp32-picsimlab machine
|
# 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
|
# NOTE: ROM files must live in the same directory as the .so (worker passes -L
|
||||||
# to QEMU pointing at os.path.dirname(lib_path))
|
# to QEMU pointing at os.path.dirname(lib_path))
|
||||||
RUN mkdir -p /app/lib
|
RUN mkdir -p /app/lib
|
||||||
COPY --from=qemu-builder /qemu-lcgamboa/build/libqemu-xtensa.so /app/lib/
|
COPY --from=qemu-provider /qemu/ /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/
|
|
||||||
|
|
||||||
# Activate ESP32 emulation
|
# Activate ESP32 emulation
|
||||||
# QEMU_ESP32_LIB → Xtensa library (ESP32, ESP32-S3)
|
# QEMU_ESP32_LIB → Xtensa library (ESP32, ESP32-S3)
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,62 @@ class ESPIDFCompiler:
|
||||||
code
|
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:
|
def _translate_sketch_to_espidf(self, sketch_code: str) -> str:
|
||||||
"""
|
"""
|
||||||
Translate an Arduino WiFi+WebServer sketch to pure ESP-IDF C code.
|
Translate an Arduino WiFi+WebServer sketch to pure ESP-IDF C code.
|
||||||
|
|
@ -400,26 +456,11 @@ class ESPIDFCompiler:
|
||||||
if not main_content and files:
|
if not main_content and files:
|
||||||
main_content = files[0]['content']
|
main_content = files[0]['content']
|
||||||
|
|
||||||
# Auto-fix missing channel argument in WiFi.begin for QEMU compatibility
|
# ── QEMU WiFi compatibility ──────────────────────────────────────
|
||||||
# QEMU's mock WiFi AP is on channel 6, and active scanning hangs the simulator.
|
# QEMU's WiFi AP broadcasts "Velxio-GUEST" on channel 6.
|
||||||
# Match WiFi.begin(...) without a channel, catching both literals and variables.
|
# We normalize ANY user SSID → "Velxio-GUEST", enforce channel 6,
|
||||||
# 1 arg: WiFi.begin(ssid) -> WiFi.begin(ssid, "", 6)
|
# and use open auth (empty password) so the connection always works.
|
||||||
main_content = re.sub(
|
main_content = self._normalize_wifi_for_qemu(main_content)
|
||||||
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.
|
|
||||||
|
|
||||||
if self.has_arduino:
|
if self.has_arduino:
|
||||||
# Arduino-as-component mode: copy sketch as .cpp
|
# 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:
|
services:
|
||||||
backend:
|
velxio:
|
||||||
build:
|
build:
|
||||||
context: ./backend
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile.standalone
|
||||||
ports:
|
container_name: velxio-dev
|
||||||
- "8001:8001"
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3080:80"
|
||||||
|
env_file:
|
||||||
|
- ./backend/.env
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=sqlite+aiosqlite:////app/data/velxio.db
|
- DATABASE_URL=sqlite+aiosqlite:////app/data/velxio.db
|
||||||
- DATA_DIR=/app/data
|
- DATA_DIR=/app/data
|
||||||
|
- IDF_PATH=/opt/esp-idf
|
||||||
|
- IDF_TOOLS_PATH=/root/.espressif
|
||||||
|
- ARDUINO_ESP32_PATH=/opt/arduino-esp32
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
|
- arduino-libs:/root/.arduino15
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8001/health')"]
|
test: ["CMD", "curl", "-f", "http://localhost/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 30s
|
start_period: 90s
|
||||||
|
|
||||||
frontend:
|
volumes:
|
||||||
build:
|
arduino-libs:
|
||||||
context: .
|
|
||||||
dockerfile: frontend/Dockerfile
|
|
||||||
ports:
|
|
||||||
- "3000:80"
|
|
||||||
depends_on:
|
|
||||||
backend:
|
|
||||||
condition: service_healthy
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
Loading…
Reference in New Issue