-
-
Notifications
You must be signed in to change notification settings - Fork 135
Add QEMU E2E CI workflow with Playwright tests for ESP32 #303
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: mdev
Are you sure you want to change the base?
Changes from all commits
c0aa75c
d8934bb
afbe8fc
98accba
314bf7f
19df216
129a138
e3f25f6
0bcd288
79a1d5c
3c9a68f
a957eed
104e4ec
28a0d00
e5dfccb
3a8b123
9bda207
4beb79b
cee5708
dafd718
949d42f
496b5c3
d947fdd
4b3f5ed
b99ebed
f79f0bd
6f7138a
451103e
13e9f60
495cd69
d541c21
c811a0c
e47b3ef
2dd31c6
ddd148e
d1d71f7
aa580c3
6308df9
db57333
f0ee919
01a5df1
7c1aef6
947f959
be3b2cf
6b8a2fc
0885fff
a49ffac
970a289
b7827cf
5ade91c
4b12993
d1dc072
4174af8
9fe5cd8
b226767
14d506a
be2e5a6
89157fb
c57b788
2a2fd32
54c1894
e61c732
3ff7128
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| #!/usr/bin/env python3 | ||
| """ | ||
| Monitor QEMU ESP32 serial output and decode exceptions | ||
| This script watches the QEMU serial output and uses the ESP32 exception decoder | ||
| to translate stack traces into human-readable format. | ||
| """ | ||
|
|
||
| import sys | ||
| import re | ||
| import subprocess | ||
| import os | ||
|
|
||
| def find_elf_file(firmware_dir): | ||
| """Find the ELF file for symbol resolution""" | ||
| elf_path = os.path.join(firmware_dir, "firmware.elf") | ||
| if os.path.exists(elf_path): | ||
| return elf_path | ||
| return None | ||
|
|
||
| def decode_exception(lines, elf_file): | ||
| """Decode an ESP32 exception using addr2line""" | ||
| if not elf_file or not os.path.exists(elf_file): | ||
| return None | ||
|
|
||
| # Extract addresses from backtrace | ||
| addresses = [] | ||
| for line in lines: | ||
| # Look for patterns like: 0x4008xxxx:0x3ffbxxxx | ||
| matches = re.findall(r'0x[0-9a-fA-F]{8}', line) | ||
| addresses.extend(matches) | ||
|
|
||
| if not addresses: | ||
| return None | ||
|
|
||
| # Use addr2line to decode addresses | ||
| try: | ||
| # Get the toolchain path from environment or use default | ||
| toolchain_prefix = os.environ.get('TOOLCHAIN_PREFIX', 'xtensa-esp32-elf-') | ||
| addr2line = f"{toolchain_prefix}addr2line" | ||
|
|
||
| cmd = [addr2line, '-e', elf_file, '-f', '-C'] + addresses | ||
| result = subprocess.run(cmd, capture_output=True, text=True, timeout=5) | ||
|
|
||
| if result.returncode == 0 and result.stdout: | ||
| return result.stdout | ||
| except Exception as e: | ||
| print(f"[Decoder] Error decoding: {e}", file=sys.stderr) | ||
|
|
||
| return None | ||
|
|
||
| def monitor_output(firmware_dir): | ||
| """Monitor stdin and decode exceptions""" | ||
| elf_file = find_elf_file(firmware_dir) | ||
|
|
||
| if elf_file: | ||
| print(f"[Decoder] Using ELF file: {elf_file}", file=sys.stderr) | ||
| else: | ||
| print(f"[Decoder] Warning: ELF file not found in {firmware_dir}", file=sys.stderr) | ||
| print(f"[Decoder] Exception decoding will not be available", file=sys.stderr) | ||
|
|
||
| exception_lines = [] | ||
| in_exception = False | ||
|
|
||
| for line in sys.stdin: | ||
| # Print the original line | ||
| print(line, end='', flush=True) | ||
|
|
||
| # Detect exception start | ||
| if 'Guru Meditation Error' in line or 'Backtrace:' in line or 'abort()' in line: | ||
| in_exception = True | ||
| exception_lines = [line] | ||
| print("\n[Decoder] ========== ESP32 EXCEPTION DETECTED ==========", file=sys.stderr) | ||
| elif in_exception: | ||
| exception_lines.append(line) | ||
|
|
||
| # Check if exception block ended | ||
| if line.strip() == '' or 'ELF file SHA256' in line or len(exception_lines) > 20: | ||
| # Try to decode | ||
| decoded = decode_exception(exception_lines, elf_file) | ||
| if decoded: | ||
| print("\n[Decoder] Decoded stack trace:", file=sys.stderr) | ||
| print(decoded, file=sys.stderr) | ||
| print("[Decoder] ================================================\n", file=sys.stderr) | ||
| else: | ||
| print("[Decoder] Could not decode exception (toolchain not available)", file=sys.stderr) | ||
| print("[Decoder] ================================================\n", file=sys.stderr) | ||
|
|
||
| in_exception = False | ||
| exception_lines = [] | ||
|
|
||
| if __name__ == '__main__': | ||
| firmware_dir = sys.argv[1] if len(sys.argv) > 1 else '.pio/build/esp32_16MB_QEMU_debug' | ||
| monitor_output(firmware_dir) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| #!/bin/bash | ||
| # Run WLED firmware in QEMU ESP32 | ||
| # This script starts QEMU with the compiled firmware and enables network access | ||
| # | ||
| # Note: QEMU ESP32 emulation has limitations: | ||
| # - Not all peripherals are fully emulated (WiFi, I2C, some GPIOs) | ||
| # - Some firmware features may crash in QEMU but work on real hardware | ||
| # - This is expected behavior for testing web UI functionality | ||
|
|
||
| set -e | ||
|
|
||
| FIRMWARE_DIR="${1:-.pio/build/esp32_16MB_QEMU_debug}" | ||
| QEMU_DIR="${2:-qemu-esp32}" | ||
| HTTP_PORT="${3:-8080}" # Default to 8080 (non-privileged port) | ||
|
|
||
| if [ ! -d "$FIRMWARE_DIR" ]; then | ||
| echo "Error: Firmware directory not found: $FIRMWARE_DIR" | ||
| exit 1 | ||
| fi | ||
|
|
||
| if [ ! -f "${QEMU_DIR}/qemu-system-xtensa" ] && [ ! -f "${QEMU_DIR}/bin/qemu-system-xtensa" ]; then | ||
| echo "Error: QEMU not found at ${QEMU_DIR}/qemu-system-xtensa or ${QEMU_DIR}/bin/qemu-system-xtensa" | ||
| echo "Please run setup-qemu.sh first" | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Determine QEMU binary location | ||
| if [ -f "${QEMU_DIR}/qemu-system-xtensa" ]; then | ||
| QEMU_BIN="${QEMU_DIR}/qemu-system-xtensa" | ||
| else | ||
| QEMU_BIN="${QEMU_DIR}/bin/qemu-system-xtensa" | ||
| fi | ||
|
|
||
| # Check for required firmware files | ||
| BOOTLOADER="${FIRMWARE_DIR}/bootloader.bin" | ||
| PARTITIONS="${FIRMWARE_DIR}/partitions.bin" | ||
| FIRMWARE="${FIRMWARE_DIR}/firmware.bin" | ||
|
|
||
| if [ ! -f "$BOOTLOADER" ]; then | ||
| echo "Error: Bootloader not found: $BOOTLOADER" | ||
| exit 1 | ||
| fi | ||
|
|
||
| if [ ! -f "$FIRMWARE" ]; then | ||
| echo "Error: Firmware not found: $FIRMWARE" | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "Starting QEMU ESP32 with WLED firmware" | ||
| echo "Firmware directory: $FIRMWARE_DIR" | ||
| echo "HTTP will be accessible at: http://localhost:${HTTP_PORT}" | ||
|
|
||
| # Create a merged flash image as QEMU expects | ||
| FLASH_IMAGE="/tmp/wled_flash.bin" | ||
| echo "Creating flash image at $FLASH_IMAGE" | ||
|
|
||
| # Create a 16MB flash image (0x1000000 bytes) for esp32_16MB_QEMU_debug | ||
| dd if=/dev/zero of="$FLASH_IMAGE" bs=1M count=16 2>/dev/null | ||
|
|
||
| # Write bootloader at 0x1000 | ||
| if [ -f "$BOOTLOADER" ]; then | ||
| dd if="$BOOTLOADER" of="$FLASH_IMAGE" bs=1 seek=$((0x1000)) conv=notrunc 2>/dev/null | ||
| fi | ||
|
|
||
| # Write partitions at 0x8000 | ||
| if [ -f "$PARTITIONS" ]; then | ||
| dd if="$PARTITIONS" of="$FLASH_IMAGE" bs=1 seek=$((0x8000)) conv=notrunc 2>/dev/null | ||
| fi | ||
|
|
||
| # Write firmware at 0x10000 | ||
| dd if="$FIRMWARE" of="$FLASH_IMAGE" bs=1 seek=$((0x10000)) conv=notrunc 2>/dev/null | ||
|
|
||
| echo "Flash image created successfully" | ||
|
|
||
| # Run QEMU ESP32 | ||
| # Note: ESP32 in QEMU has limited peripheral support | ||
| # Network configuration uses user-mode networking with port forwarding | ||
| # -nic user,model=open_eth,id=lo0,hostfwd=tcp:127.0.0.1:PORT_HOST-:PORT_GUEST # for port forwarding | ||
| # -global driver=timer.esp32.timg,property=wdt_disable,value=true # disables TG watchdog timers | ||
| echo "Starting QEMU..." | ||
| ${QEMU_BIN} \ | ||
| -nographic \ | ||
| -machine esp32 \ | ||
| -drive file=${FLASH_IMAGE},if=mtd,format=raw \ | ||
| -nic user,model=open_eth,id=lo0,hostfwd=tcp::${HTTP_PORT}-:80 \ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bind QEMU host-forwarded HTTP port to localhost explicitly. Line 85 currently forwards on all interfaces ( Suggested fix- -nic user,model=open_eth,id=lo0,hostfwd=tcp::${HTTP_PORT}-:80 \
+ -nic "user,model=open_eth,id=lo0,hostfwd=tcp:127.0.0.1:${HTTP_PORT}-:80" \🧰 Tools🪛 Shellcheck (0.11.0)[info] 85-85: Double quote to prevent globbing and word splitting. (SC2086) 🤖 Prompt for AI Agents |
||
| -global driver=timer.esp32.timg,property=wdt_disable,value=true \ | ||
| -serial mon:stdio & | ||
|
|
||
| QEMU_PID=$! | ||
| echo "QEMU started with PID: $QEMU_PID" | ||
| echo $QEMU_PID > qemu.pid | ||
|
|
||
| # Wait for QEMU to initialize | ||
| echo "Waiting for QEMU to initialize (30 seconds)..." | ||
| sleep 30 | ||
|
|
||
| # Check if QEMU is still running | ||
| if ! kill -0 $QEMU_PID 2>/dev/null; then | ||
| echo "Error: QEMU process died" | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "QEMU is running" | ||
| echo "To stop QEMU: kill $QEMU_PID" | ||
| echo "Or use: kill \$(cat qemu.pid)" | ||
|
|
||
| # Wait for QEMU process | ||
| wait $QEMU_PID | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| #!/bin/bash | ||
| # Setup QEMU ESP32 emulation environment | ||
| # This script downloads and sets up QEMU for ESP32 | ||
|
|
||
| set -e | ||
|
|
||
| QEMU_DIR="qemu-esp32" | ||
|
|
||
| echo "Setting up QEMU ESP32..." | ||
|
|
||
| # Create directory for QEMU | ||
| mkdir -p ${QEMU_DIR} | ||
|
|
||
| # Check if QEMU is already installed | ||
| if [ -f "${QEMU_DIR}/qemu-system-xtensa" ]; then | ||
| echo "QEMU ESP32 already installed" | ||
| echo "QEMU binary: ${QEMU_DIR}/qemu-system-xtensa" | ||
| exit 0 | ||
| fi | ||
|
|
||
| # Try multiple QEMU sources in order of preference | ||
| echo "Attempting to download QEMU ESP32..." | ||
|
|
||
| # List of potential QEMU download URLs to try | ||
| # Using the latest stable releases from Espressif | ||
| QEMU_URLS=( | ||
| "esp-develop-9.2.2-20250817|https://github.com/espressif/qemu/releases/download/esp-develop-9.2.2-20250817/qemu-xtensa-softmmu-esp_develop_9.2.2_20250817-x86_64-linux-gnu.tar.xz" | ||
| "esp-develop-9.1.0-20240606|https://github.com/espressif/qemu/releases/download/esp-develop-9.1.0-20240606/qemu-xtensa-softmmu-esp_develop_9.1.0_20240606-x86_64-linux-gnu.tar.xz" | ||
| "esp-develop-9.0.0-20231220|https://github.com/espressif/qemu/releases/download/esp-develop-9.0.0-20231220/qemu-xtensa-softmmu-esp_develop_9.0.0_20231220-x86_64-linux-gnu.tar.xz" | ||
| ) | ||
|
|
||
| DOWNLOAD_SUCCESS=false | ||
|
|
||
| for ENTRY in "${QEMU_URLS[@]}"; do | ||
| VERSION="${ENTRY%%|*}" | ||
| URL="${ENTRY##*|}" | ||
|
|
||
| echo "Trying version ${VERSION}..." | ||
| echo "URL: ${URL}" | ||
|
|
||
| if wget --spider -q "${URL}" 2>/dev/null; then | ||
| echo "Found available version: ${VERSION}" | ||
| echo "Downloading from ${URL}..." | ||
|
|
||
| if wget -q "${URL}" -O qemu.tar.xz; then | ||
| echo "Download successful, extracting..." | ||
| if tar -xf qemu.tar.xz -C ${QEMU_DIR} --strip-components=1; then | ||
| rm qemu.tar.xz | ||
| DOWNLOAD_SUCCESS=true | ||
| echo "QEMU ESP32 version ${VERSION} installed successfully" | ||
|
Comment on lines
+45
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verify downloaded QEMU archive integrity before extraction. The script downloads a prebuilt binary and extracts it immediately without checksum/signature validation. In CI, this is a supply-chain risk. Suggested hardening QEMU_URLS=(
- "esp-develop-9.2.2-20250817|https://github.com/espressif/qemu/releases/download/esp-develop-9.2.2-20250817/qemu-xtensa-softmmu-esp_develop_9.2.2_20250817-x86_64-linux-gnu.tar.xz"
+ "esp-develop-9.2.2-20250817|https://github.com/espressif/qemu/releases/download/esp-develop-9.2.2-20250817/qemu-xtensa-softmmu-esp_develop_9.2.2_20250817-x86_64-linux-gnu.tar.xz|<SHA256_HERE>"
)
@@
- URL="${ENTRY##*|}"
+ URL="$(echo "$ENTRY" | cut -d'|' -f2)"
+ EXPECTED_SHA="$(echo "$ENTRY" | cut -d'|' -f3)"
@@
- if wget -q "${URL}" -O qemu.tar.xz; then
+ if wget -q "${URL}" -O qemu.tar.xz; then
+ echo "${EXPECTED_SHA} qemu.tar.xz" | sha256sum -c -
echo "Download successful, extracting..."🤖 Prompt for AI Agents |
||
| break | ||
| else | ||
| echo "Extraction failed, trying next source..." | ||
| rm -f qemu.tar.xz | ||
| fi | ||
| else | ||
| echo "Download failed, trying next source..." | ||
| rm -f qemu.tar.xz | ||
| fi | ||
| else | ||
| echo "Version ${VERSION} not available, trying next..." | ||
| fi | ||
| done | ||
|
|
||
| if [ "$DOWNLOAD_SUCCESS" = false ]; then | ||
| echo "ERROR: Could not download QEMU ESP32 from any source" | ||
| echo "Please check https://github.com/espressif/qemu/releases for available versions" | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Make QEMU executable (try both possible locations) | ||
| if [ -f "${QEMU_DIR}/qemu-system-xtensa" ]; then | ||
| chmod +x ${QEMU_DIR}/qemu-system-xtensa | ||
| QEMU_BIN="${QEMU_DIR}/qemu-system-xtensa" | ||
| elif [ -f "${QEMU_DIR}/bin/qemu-system-xtensa" ]; then | ||
| chmod +x ${QEMU_DIR}/bin/qemu-system-xtensa | ||
| # Create symlink for easier access | ||
| ln -sf bin/qemu-system-xtensa ${QEMU_DIR}/qemu-system-xtensa | ||
| QEMU_BIN="${QEMU_DIR}/bin/qemu-system-xtensa" | ||
| else | ||
| echo "ERROR: Could not find qemu-system-xtensa binary" | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "QEMU ESP32 setup complete" | ||
| echo "QEMU binary: ${QEMU_BIN}" | ||
|
|
||
| # Verify QEMU can run by checking for required libraries | ||
| echo "Verifying QEMU dependencies..." | ||
| if ! ldd "${QEMU_BIN}" | grep -q "not found"; then | ||
| echo "All required libraries found" | ||
| ${QEMU_BIN} --version | ||
| else | ||
| echo "WARNING: Missing required libraries:" | ||
| ldd "${QEMU_BIN}" | grep "not found" | ||
| echo "" | ||
| echo "Install missing dependencies with:" | ||
| echo " sudo apt-get update" | ||
| echo " sudo apt-get install -y libsdl2-2.0-0 libpixman-1-0 libglib2.0-0" | ||
| exit 1 | ||
| fi | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Decode pending exception block on EOF.
If input ends before an empty line/marker appears, the captured exception is never decoded.
Suggested fix
for line in sys.stdin: # Print the original line print(line, end='', flush=True) @@ in_exception = False exception_lines = [] + + # Flush trailing exception block at EOF + if in_exception and exception_lines: + decoded = decode_exception(exception_lines, elf_file) + if decoded: + print("\n[Decoder] Decoded stack trace:", file=sys.stderr) + print(decoded, file=sys.stderr) + else: + print("[Decoder] Could not decode trailing exception block", file=sys.stderr)🤖 Prompt for AI Agents