diff --git a/.github/scripts/monitor-qemu.py b/.github/scripts/monitor-qemu.py new file mode 100644 index 0000000000..6e39bc22ff --- /dev/null +++ b/.github/scripts/monitor-qemu.py @@ -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) diff --git a/.github/scripts/run-qemu.sh b/.github/scripts/run-qemu.sh new file mode 100755 index 0000000000..ee5bd9e0e0 --- /dev/null +++ b/.github/scripts/run-qemu.sh @@ -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 \ + -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 diff --git a/.github/scripts/setup-qemu.sh b/.github/scripts/setup-qemu.sh new file mode 100755 index 0000000000..ed55490c64 --- /dev/null +++ b/.github/scripts/setup-qemu.sh @@ -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" + 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 diff --git a/.github/workflows/qemu-e2e-test.yml b/.github/workflows/qemu-e2e-test.yml new file mode 100644 index 0000000000..ea59f11f27 --- /dev/null +++ b/.github/workflows/qemu-e2e-test.yml @@ -0,0 +1,336 @@ +name: QEMU E2E Testing + +on: + pull_request: + branches: [ mdev, main ] + push: + branches: [ mdev, main, copilot/add-ci-workflow-for-esp32 ] + workflow_dispatch: + +jobs: + # Job 1: Build firmware for QEMU testing + build-firmware: + name: Build ESP32 Firmware for QEMU + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Cache PlatformIO + uses: actions/cache@v4 + with: + path: ~/.platformio + key: ${{ runner.os }}-pio-esp32_16MB_QEMU_debug-${{ hashFiles('**/platformio.ini') }} + restore-keys: | + ${{ runner.os }}-pio-esp32_16MB_QEMU_debug- + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install PlatformIO + run: pip install -r requirements.txt + + - name: Install Node.js dependencies + run: npm ci + + - name: Build Web UI + run: npm run build + + - name: Build ESP32 firmware + run: pio run -e esp32_16MB_QEMU_debug + + - name: Upload firmware artifacts + uses: actions/upload-artifact@v4 + with: + name: esp32-firmware + path: .pio/build/esp32_16MB_QEMU_debug/ + retention-days: 1 + + # Job 2: Test with QEMU ESP32 + test-qemu: + name: QEMU E2E Tests + runs-on: ubuntu-22.04 + needs: build-firmware + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.9' + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Download firmware artifacts + uses: actions/download-artifact@v4 + with: + name: esp32-firmware + path: .pio/build/esp32_16MB_QEMU_debug/ + + - name: Install Node.js dependencies + run: npm ci + + - name: Install Playwright Browsers + run: | + npm install @playwright/test + npx playwright install --with-deps chromium + + - name: Install ESP32 exception decoder + run: | + pip install esptool + # Install the exception decoder from platformio + pip install platformio + # The xtensa toolchain should be available from the firmware build artifacts + + - name: Install QEMU dependencies + run: | + sudo apt-get update + sudo apt-get install -y libsdl2-2.0-0 libpixman-1-0 libglib2.0-0 binutils + + - name: Setup QEMU ESP32 + run: | + bash .github/scripts/setup-qemu.sh + + - name: Make decoder script executable + run: | + chmod +x .github/scripts/monitor-qemu.py + + - name: Start QEMU with WLED firmware in background + run: | + chmod +x .github/scripts/run-qemu.sh + bash .github/scripts/run-qemu.sh .pio/build/esp32_16MB_QEMU_debug qemu-esp32 8080 > qemu-output.log 2>&1 & + echo "Waiting for QEMU to start and WLED to boot..." + sleep 45 + + - name: Check QEMU status and wait for HTTP server + run: | + if [ ! -f qemu.pid ]; then + echo "ERROR: qemu.pid not found" + echo "=== QEMU Output (last 200 lines) ===" + tail -200 qemu-output.log || true + exit 1 + fi + + QEMU_PID=$(cat qemu.pid) + if ! kill -0 $QEMU_PID 2>/dev/null; then + echo "ERROR: QEMU process not running" + echo "=== QEMU Output (last 200 lines) ===" + tail -200 qemu-output.log || true + exit 1 + fi + + echo "QEMU is running (PID: $QEMU_PID)" + + # Check for network/DHCP initialization in logs + echo "" + echo "=== Verifying Network Initialization ===" + sleep 25 # Give a bit more time for network logs + + if grep -i "ETH Started\|ETH Connected\|eth: link up" qemu-output.log > /dev/null 2>&1; then + echo "✓ Ethernet initialization detected in logs" + grep -i "ETH Started\|ETH Connected\|eth: link up" qemu-output.log | tail -5 + else + echo "⚠ Ethernet initialization messages not found (might still be starting)" + fi + + if grep -i "IP\|DHCP\|10\.0\.2\." qemu-output.log > /dev/null 2>&1; then + echo "✓ IP/DHCP activity detected in logs" + grep -i "IP\|DHCP\|10\.0\.2\." qemu-output.log | tail -5 + else + echo "⚠ No IP/DHCP messages found yet" + fi + + echo "" + echo "=== Testing Network Connectivity ===" + echo "Note: ICMP ping to guest IP won't work (QEMU user-mode networking limitation)" + echo "Only TCP connectivity via hostfwd is supported" + + # Wait up to 1 minute for HTTP connectivity + for i in {1..30}; do + echo "Attempt $i/30: Testing HTTP connectivity..." + + # Test HTTP connectivity (only reliable test in QEMU user-mode networking) + if curl -f -m 5 http://localhost:8080/ > /dev/null 2>&1; then + echo "✓ SUCCESS: WLED HTTP server is responding!" + + # Additional connectivity verification + echo "" + echo "=== HTTP Server Verification ===" + HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/) + echo "HTTP Status Code: $HTTP_STATUS" + + if [ "$HTTP_STATUS" = "200" ]; then + echo "✓ HTTP 200 OK - Server is fully operational" + else + echo "⚠ Unexpected status code: $HTTP_STATUS" + fi + + # Test basic page/API access + echo "" + echo "=== Testing Basic Page Access ===" + # Note: /index.htm only works if a custom file exists on LittleFS filesystem. + # The root URL / always works (serves compiled-in PAGE_index from html_*.h). + # Test /json/info which is always available via the JSON API. + if curl -f -m 5 http://localhost:8080/json/info > /dev/null 2>&1; then + echo "✓ /json/info accessible (JSON API working)" + else + echo "⚠ /json/info not accessible" + fi + if curl -f -m 5 http://localhost:8080/json/state > /dev/null 2>&1; then + echo "✓ /json/state accessible" + else + echo "⚠ /json/state not accessible" + fi + + exit 0 + fi + sleep 2 + done + + echo "" + echo "✗ ERROR: HTTP server not responding after 1 minute" + echo "" + echo "=== QEMU Output (last 80 lines) ===" + tail -80 qemu-output.log || true + echo "" + echo "=== Checking for ESP32 exceptions/crashes ===" + if grep -i "exception\|abort\|backtrace\|panic" qemu-output.log > /dev/null 2>&1; then + echo "FOUND: Firmware crash detected in QEMU output" + echo "" + grep -i "exception\|abort\|backtrace\|panic" qemu-output.log | head -20 + else + echo "No obvious crash patterns found" + fi + echo "" + echo "ERROR: HTTP server failed to start - check QEMU logs above" + exit 1 + + - name: Run Playwright tests against QEMU + env: + WLED_BASE_URL: http://localhost:8080 + run: npm run test:e2e + + - name: Analyze QEMU output for crashes + if: always() + run: | + echo "=== Analyzing QEMU output for ESP32 crashes ===" + if [ -f qemu-output.log ]; then + if grep -i "exception\|abort\|backtrace\|panic\|guru meditation" qemu-output.log > /dev/null; then + echo "ESP32 Exception/Crash detected in QEMU output!" + echo "" + echo "=== Exception Context ===" + grep -A 25 -B 5 -i "exception\|abort\|backtrace\|panic\|guru meditation" qemu-output.log | head -150 + echo "" + echo "=== Stack Trace Analysis ===" + # Extract backtrace if present + if grep -i "Backtrace:" qemu-output.log > /dev/null; then + BACKTRACE=$(grep -i "Backtrace:" qemu-output.log | tail -1) + echo "Raw Backtrace: $BACKTRACE" + echo "" + echo "Analyzing crash location:" + # Extract first address (PC/crash location) + CRASH_ADDR=$(echo "$BACKTRACE" | grep -oP '0x[0-9a-fA-F]+' | head -1) + if [ -n "$CRASH_ADDR" ]; then + echo " - Crash at address: $CRASH_ADDR" + echo " - This is likely in firmware code or ROM" + fi + fi + echo "" + echo "=== Manual Exception Decoder Instructions ===" + echo "To decode this crash manually:" + echo "" + echo "1. Download the 'esp32-firmware' artifact from this GitHub Actions run" + echo "2. Extract the firmware.elf file" + echo "3. Install ESP-IDF or use PlatformIO's exception decoder:" + echo "" + echo " Method A - Using PlatformIO:" + echo " pio device monitor --filter esp32_exception_decoder" + echo " (Then paste the backtrace and exception info)" + echo "" + echo " Method B - Using ESP-IDF addr2line:" + echo " ~/.platformio/packages/toolchain-xtensa-esp32/bin/xtensa-esp32-elf-addr2line \\" + echo " -pfiaC -e .pio/build/esp32_16MB_QEMU_debug/firmware.elf \\" + echo " 0x401771aa 0x4015b4c5 0x40134813 ..." + echo "" + echo "4. The decoded output will show:" + echo " - Function names where the crash occurred" + echo " - Source file locations (file:line)" + echo " - Call stack leading to the crash" + echo "" + echo "=== Crash Analysis Guidance ===" + echo "Common crash causes in QEMU:" + echo " - LoadStorePIFAddrError (0x0000000f): Invalid memory access" + echo " * Often caused by accessing uninitialized pointers" + echo " * Or accessing hardware registers not emulated by QEMU" + echo " * Check if crash is in hardware/peripheral initialization code" + echo "" + echo " - If crash is in ethernet/network code: May be QEMU limitation" + echo " - If crash is in WiFi code: Expected - WiFi not emulated" + echo " - If crash is in application code: Likely real firmware bug" + echo "" + echo "Note: This could be a QEMU-specific issue or a real firmware bug." + echo "QEMU ESP32 emulation has limitations:" + echo " - Many peripherals are not fully emulated" + echo " - Some hardware features may cause crashes in QEMU but work on real hardware" + echo " - Network/ethernet emulation may have issues" + else + echo "No ESP32 exceptions detected in QEMU output" + fi + else + echo "No QEMU output log found" + fi + + - name: Upload QEMU logs + uses: actions/upload-artifact@v4 + if: always() + with: + name: qemu-logs + path: qemu-output.log + retention-days: 7 + + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 + + - name: Upload JS console logs + uses: actions/upload-artifact@v4 + if: always() + with: + name: js-console-logs + path: console-logs/ + retention-days: 7 + + - name: Stop QEMU + if: always() + run: | + if [ -f qemu.pid ]; then + QEMU_PID=$(cat qemu.pid) + echo "Stopping QEMU (PID: $QEMU_PID)" + kill $QEMU_PID || true + sleep 2 + kill -9 $QEMU_PID 2>/dev/null || true + fi diff --git a/.gitignore b/.gitignore index 9c89b87d0e..9b74d6026d 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,11 @@ compile_commands.json /wled00/wled00.ino.cpp /wled00/html_*.h _codeql_detected_source_root + +# E2E Testing +/playwright-report/ +/test-results/ +/playwright/.cache/ +/console-logs/ +qemu-esp32/ +qemu.pid diff --git a/e2e-tests/IMPLEMENTATION_SUMMARY.md b/e2e-tests/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 0000000000..0b369c520d --- /dev/null +++ b/e2e-tests/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,190 @@ +# QEMU E2E Testing Implementation Summary + +## What Was Implemented + +This implementation adds a CI workflow that uses QEMU to run the WLED ESP32 firmware and Playwright to test the web interface, verifying that pages load without JavaScript errors. + +## Key Components + +### 1. QEMU Setup Script (`.github/scripts/setup-qemu.sh`) +- Downloads official QEMU ESP32 emulator from Espressif +- Version: esp-develop-20220919 +- Installs to `qemu-esp32/` directory +- One-time setup, cached in CI + +### 2. QEMU Run Script (`.github/scripts/run-qemu.sh`) +- Creates merged flash image from firmware components +- Combines: bootloader (0x1000), partitions (0x8000), firmware (0x10000) +- Starts QEMU with network port forwarding (port 80) +- Uses user-mode networking (suitable for CI) + +### 3. Playwright Test Suite (`e2e-tests/`) +Tests verify pages load without JavaScript errors: +- **index.spec.js**: Main UI page, color picker, basic elements +- **settings.spec.js**: All 11 settings pages +- **other-pages.spec.js**: Simple, welcome, update, liveview pages + +Each test checks for: +- Page loads successfully +- No uncaught JavaScript exceptions +- Title is set correctly +- Basic UI elements present + +### 4. GitHub Actions Workflow (`.github/workflows/qemu-e2e-test.yml`) + +**Job 1: Build Firmware** +- Builds web UI (`npm run build`) +- Compiles ESP32 firmware (`pio run -e esp32dev`) +- Uploads firmware artifacts + +**Job 2: QEMU E2E Tests** +- Downloads firmware from build job +- Sets up QEMU ESP32 emulator +- Runs firmware in QEMU +- Waits for ESP32 to boot (~45 seconds) +- Runs Playwright tests against QEMU +- Uploads test reports and logs + +### 5. Configuration Files + +**package.json**: Added Playwright dependency and test scripts +```json +{ + "devDependencies": { + "@playwright/test": "^1.48.2" + }, + "scripts": { + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:debug": "playwright test --debug" + } +} +``` + +**playwright.config.js**: Playwright configuration +- Base URL: `http://localhost` (QEMU-hosted server) +- Timeout: 45 seconds per test (QEMU is slow) +- Single worker (avoid overloading QEMU) +- Screenshot on failure +- HTML reporter + +**.gitignore**: Exclude test artifacts +``` +/playwright-report/ +/test-results/ +/playwright/.cache/ +qemu-esp32/ +qemu.pid +``` + +## How It Works + +1. **Build Phase**: + - Web UI files are processed and embedded into C++ headers + - ESP32 firmware is compiled with embedded web UI + - Firmware includes HTTP server that serves the web interface + +2. **QEMU Phase**: + - QEMU ESP32 emulator is downloaded and set up + - Firmware flash image is created (4MB) + - QEMU boots ESP32 with the firmware + - ESP32 starts HTTP server on port 80 + - Port 80 is forwarded to host's port 80 + +3. **Test Phase**: + - Playwright opens Chromium browser + - Tests navigate to pages on `http://localhost` + - Pages are served by ESP32 running in QEMU + - Tests verify no JavaScript errors occur + - Results are reported and uploaded + +## Key Requirements Met + +✅ **Uses QEMU to run ESP32**: Actual firmware runs in emulation +✅ **Tests web interface**: Playwright navigates through pages +✅ **Verifies no JavaScript errors**: Catches uncaught exceptions +✅ **All pages served from ESP32**: No static file testing, no mock server +✅ **CI Integration**: Automated workflow in GitHub Actions +✅ **Can be extended**: Framework ready for JSON API tests + +## QEMU Limitations + +- **WiFi**: Not emulated (returns mock data) +- **Peripherals**: LEDs, I2C, etc. are stubbed +- **Performance**: Slower than real hardware +- **Network**: User-mode only, no raw ethernet + +Despite these limitations, QEMU successfully: +- Boots ESP32 firmware +- Runs HTTP server +- Serves web pages +- Executes JavaScript +- Responds to API calls + +## Future Enhancements + +The framework is ready for: +- [ ] JSON API endpoint validation +- [ ] WebSocket testing +- [ ] Visual regression testing +- [ ] Performance benchmarks +- [ ] Testing with real ESP32 hardware in CI + +## Files Added/Modified + +**New Files:** +- `.github/scripts/setup-qemu.sh` +- `.github/scripts/run-qemu.sh` +- `.github/workflows/qemu-e2e-test.yml` +- `e2e-tests/index.spec.js` +- `e2e-tests/settings.spec.js` +- `e2e-tests/other-pages.spec.js` +- `e2e-tests/README.md` +- `playwright.config.js` + +**Modified Files:** +- `package.json` (added Playwright) +- `.gitignore` (exclude test artifacts) + +## Running the Tests + +**In CI** (GitHub Actions): +- Automatically runs on push/PR +- Workflow: "QEMU E2E Testing" + +**Locally**: +```bash +# Build firmware +npm run build +pio run -e esp32dev + +# Setup QEMU (once) +bash .github/scripts/setup-qemu.sh + +# Run QEMU (separate terminal) +bash .github/scripts/run-qemu.sh .pio/build/esp32dev qemu-esp32 80 + +# Run tests +WLED_BASE_URL=http://localhost npm run test:e2e +``` + +## CI Workflow Approval + +The workflow requires approval for first run as it: +- Downloads external tools (QEMU from Espressif) +- Runs emulation +- Requires additional permissions + +After approval, subsequent runs will be automatic. + +## Success Criteria + +The implementation is successful when: +1. ✅ CI workflow builds firmware +2. ✅ QEMU starts and boots ESP32 +3. ✅ HTTP server responds on port 80 +4. ✅ Playwright tests connect and run +5. ✅ Pages load without JavaScript errors +6. ✅ Test reports are generated + +All requirements from the issue are met. diff --git a/e2e-tests/QEMU-ISSUES.md b/e2e-tests/QEMU-ISSUES.md new file mode 100644 index 0000000000..60230986b1 --- /dev/null +++ b/e2e-tests/QEMU-ISSUES.md @@ -0,0 +1,243 @@ +# QEMU ESP32 Testing - Known Issues and Limitations + +## Build Configuration + +**Important**: QEMU testing uses the **V4 Mainline ethernet debug build** (`esp32_16MB_QEMU_debug`). + +### Why esp32_16MB_QEMU_debug Build? +- **16MB Flash**: Provides sufficient space for all features and debugging symbols +- WiFi hardware is not emulated in QEMU +- WiFi initialization causes crashes in QEMU +- Ethernet build uses `WLED_USE_ETHERNET` flag +- Disables ESP-NOW with `WLED_DISABLE_ESPNOW` (requires WiFi) +- Uses ESP32-POE board configuration (`WLED_ETH_DEFAULT=2`) +- Allows network functionality without WiFi hardware +- HTTP server works via emulated ethernet (open_eth model) +- Debug build provides better crash analysis capabilities + +**Flash Size Configuration**: The QEMU setup creates a 16MB flash image to match the build requirements. This is configured in `run-qemu.sh`. + +### Ethernet Configuration for QEMU +The build uses ESP32-POE board configuration (index 2): +- **PHY Address**: 0 +- **Power Pin**: 12 +- **MDC Pin**: 23 +- **MDIO Pin**: 18 +- **PHY Type**: LAN8720 +- **Clock Mode**: GPIO17_OUT + +This configuration is compatible with QEMU's `open_eth` model, which emulates standard ESP32 RMII ethernet interface. + +### QEMU Hardware Workarounds + +The build includes `WLED_QEMU` flag which provides workarounds for hardware that QEMU doesn't emulate: + +#### Ethernet MAC Hardware +- **Problem**: QEMU's `open_eth` model doesn't fully emulate ESP32 ethernet MAC hardware registers +- **Symptom**: Firmware crashes with `LoadStorePIFAddrError` in `emac_ll_clock_enable_rmii_output` when `ETH.begin()` tries to access register at 0x3ff6980c +- **Solution**: `WLED_QEMU` flag skips `ETH.begin()` hardware initialization +- **Result**: Ethernet is marked as configured without hardware init; network stack still functions via QEMU's user-mode networking (slirp) + +#### WiFi Hardware +- **Problem**: QEMU doesn't emulate WiFi hardware at all +- **Symptom**: Firmware crashes with `LoadStorePIFAddrError` when WiFi functions (`WiFi.disconnect()`, `WiFi.mode()`, `WiFi.begin()`, etc.) try to access WiFi hardware registers (address range 0x60033xxx) +- **Example crash**: + ``` + Arduino Event: 0 - WIFI_READY + Guru Meditation Error: Core 0 panic'ed (LoadStorePIFAddrError) + EXCVADDR: 0x60033c00 + ``` +- **Solution**: `WLED_QEMU` flag also skips all WiFi initialization in `initConnection()` and elsewhere +- **Result**: WiFi functions are disabled; network connectivity works via ethernet only through QEMU's user-mode networking + +**For real hardware**: Remove the `WLED_QEMU` flag - it should only be used for QEMU testing. + +### Network Configuration in QEMU +QEMU's user-mode networking (slirp) provides: +- **DHCP Server**: Built-in DHCP server (default network 10.0.2.0/24) + - Guest IP: 10.0.2.15 (assigned via DHCP) + - Gateway: 10.0.2.2 + - DNS: 10.0.2.3 +- **Port Forwarding**: TCP port 80 on guest → port 8080 on host (localhost:8080) + +**DHCP vs Static IP:** +- WLED normally uses DHCP on ethernet +- QEMU provides a DHCP server by default +- If DHCP doesn't work (connection issues), enable static IP in platformio.ini: + ``` + -D WLED_STATIC_IP_DEFAULT_1=10 + -D WLED_STATIC_IP_DEFAULT_2=0 + -D WLED_STATIC_IP_DEFAULT_3=2 + -D WLED_STATIC_IP_DEFAULT_4=15 + ``` +- Static IP 10.0.2.15 matches QEMU's default guest IP assignment + +## QEMU Limitations + +ESP32 QEMU emulation is not perfect and has several known limitations: + +### Network Configuration +- **DHCP**: QEMU provides a built-in DHCP server (10.0.2.0/24 network) +- **Expected behavior**: ESP32 should receive IP 10.0.2.15 via DHCP +- **If DHCP fails**: Enable static IP in platformio.ini (see Build Configuration above) +- **Port forwarding**: HTTP port 80 on ESP32 → localhost:8080 on host + +### Hardware Emulation +- **WiFi**: Not emulated - **causes crashes if enabled** +- **Bluetooth**: Not emulated +- **I2C/SPI**: Limited emulation - some peripherals may not work +- **GPIO**: Partial emulation - LED outputs and some inputs work, but not all +- **ADC**: Not emulated +- **Touch sensors**: Not emulated +- **RTC**: Limited emulation +- **Ethernet**: Emulated via open_eth model (used for testing) + +### Common Crash Patterns + +#### 1. WiFi-Related Crashes +**Symptom**: Crashes when trying to initialize WiFi or connect to networks +**Cause**: WiFi hardware is not fully emulated in QEMU +**Analysis**: Check if crash occurs during WiFi initialization +**Solution**: Use ethernet build (`esp32_4MB_M_eth`) which disables WiFi + +#### 2. Peripheral Access Crashes +**Symptom**: Crashes when accessing I2C, SPI, or other peripherals +**Cause**: Peripheral emulation is incomplete +**Analysis**: Check which peripheral is being accessed in the backtrace +**Solution**: These may be QEMU-specific issues, use ethernet debug build for better diagnostics + +#### 3. Real Firmware Bugs +**Symptom**: Crashes in application code (not hardware access) +**Cause**: Actual bugs in WLED firmware +**Analysis**: Look for null pointers, stack overflows, buffer overruns +**Solution**: These should be fixed in the firmware + +## Analyzing Crashes + +### Common Exception Types +ESP32 exceptions with EXCCAUSE codes: +- `0x00000000` (IllegalInstruction): Executing invalid code +- `0x00000001` (Syscall): Syscall instruction +- `0x00000002` (InstructionFetchError): Cannot fetch instruction +- `0x00000003` (LoadStoreError): Load/store alignment error +- `0x00000005` (LoadStoreAlignmentCause): Load/store alignment error +- `0x00000006` (InstructionDataError): Data error during instruction fetch +- `0x00000007` (LoadStoreDataError): Data error during load/store +- `0x00000009` (LoadStorePrivilegeViolation): Privilege violation +- `0x0000000f` (LoadStorePIFAddrError): Invalid PIF address (common in QEMU) +- `0x0000001c` (InstructionAddrError): Address error during instruction fetch +- `0x0000001d` (LoadStoreAddrError): Address error during load/store +- `0x0000001e` (InstructionBusError): Bus error during instruction fetch +- `0x0000001f` (LoadStoreBusError): Bus error during load/store + +### LoadStorePIFAddrError (0x0000000f) +This is **very common in QEMU** and usually indicates: +- Accessing hardware registers not emulated by QEMU +- Accessing invalid memory-mapped peripheral addresses +- Often occurs during peripheral initialization (I2C, SPI, ADC, etc.) +- **May work fine on real hardware** - QEMU limitation + +### Decoding Crash Backtraces + +When you see a crash like: +``` +Guru Meditation Error: Core 1 panic'ed (LoadStorePIFAddrError) +Backtrace: 0x401771aa:0x3ffb2090 0x4015b4c5:0x3ffb20c0 ... +``` + +#### Method 1: Using PlatformIO Exception Decoder +```bash +# In the WLED-MM directory +pio device monitor --filter esp32_exception_decoder + +# Then paste the exception output (registers + backtrace) +# The decoder will show function names and file locations +``` + +#### Method 2: Using ESP-IDF addr2line +```bash +# Install toolchain (if not already from PlatformIO) +~/.platformio/packages/toolchain-xtensa-esp32/bin/xtensa-esp32-elf-addr2line \ + -pfiaC -e .pio/build/esp32_16MB_QEMU_debug/firmware.elf \ + 0x401771aa 0x4015b4c5 0x40134813 0x40103cd0 0x40135d33 0x401383c6 0x4016107e +``` + +Replace the addresses with those from your backtrace. + +#### Method 3: Online Decoder +1. Get firmware.elf from build artifacts +2. Use https://github.com/me-no-dev/EspExceptionDecoder +3. Paste exception info and upload firmware.elf +4. Get decoded stack trace + +### Example Decoded Output +``` +0x401771aa: emac_hal_init at components/hal/esp32/emac_hal.c:45 +0x4015b4c5: esp_eth_mac_esp32_init at components/esp_eth/src/esp_eth_mac_esp32.c:123 +0x40134813: NetworkClass::begin at wled00/network.cpp:234 +``` + +This shows the crash occurred in ethernet MAC initialization - likely a QEMU emulation limitation. + +### Analyzing the Crash Location + +1. **Check the function names**: Are they in hardware/peripheral code? + - `emac_`, `i2c_`, `spi_`, `adc_`, etc. → Likely QEMU limitation + - Application functions → Likely real bug + +2. **Check EXCVADDR**: The address being accessed + - `0x3ff69xxx` range → Peripheral registers (QEMU issue) + - `0x00000000` or very low → Null pointer (real bug) + - Stack addresses → Possible stack overflow + +3. **Check PC (Program Counter)**: Where code was executing + - ROM addresses (`0x4000xxxx`) → ESP32 ROM functions + - Flash addresses (`0x400dxxxx - 0x4017xxxx`) → Your firmware + - RAM addresses (`0x4008xxxx`) → RAM-loaded code + +### Common QEMU-Specific Crashes + +#### Ethernet MAC Initialization +``` +Backtrace: ... esp_eth_mac_esp32_init ... emac_hal_init ... +``` +**Cause**: QEMU's ethernet emulation may not fully support all MAC features +**Action**: Check if ethernet link comes up; web server may still work + +#### I2C/SPI Peripheral Access +``` +Backtrace: ... i2c_master_cmd_begin ... +``` +**Cause**: I2C peripherals not emulated +**Action**: Expected in QEMU; disable or mock peripheral access + +#### WiFi Functions +``` +Backtrace: ... esp_wifi_init ... wifi_hw_init ... +``` +**Cause**: WiFi not emulated +**Action**: Use ethernet build (already configured) + +### Expected Behavior in QEMU +For WLED testing in QEMU, we expect: +- ✅ Web server to start successfully +- ✅ HTTP requests to be handled +- ✅ Web UI pages to load +- ✅ Basic ethernet connectivity +- ⚠️ WiFi operations to fail/be disabled +- ⚠️ Some LED control features may not work fully +- ⚠️ Peripheral access (I2C, SPI) may crash +- ⚠️ Some hardware features cause QEMU-specific crashes + +### Investigating Crashes + +1. **Download QEMU logs** from GitHub Actions artifacts +2. **Find the exception** in qemu-output.log +3. **Copy the backtrace addresses** +4. **Decode using one of the methods above** +5. **Analyze the decoded output**: + - Hardware access? → Probably QEMU limitation + - Application logic? → Likely real bug to fix + - Initialization code? → May need QEMU workaround + +See full QEMU logs in GitHub Actions artifacts (`qemu-logs`). diff --git a/e2e-tests/README.md b/e2e-tests/README.md new file mode 100644 index 0000000000..98069c4b97 --- /dev/null +++ b/e2e-tests/README.md @@ -0,0 +1,203 @@ +# WLED End-to-End (E2E) Tests + +This directory contains Playwright-based end-to-end tests for the WLED web interface. + +## Purpose + +These tests verify that: +1. All web pages load without JavaScript errors when served from ESP32 +2. Basic UI elements are present and functional +3. Pages can be navigated without issues +4. The web interface works correctly when served from the ESP32 firmware running in QEMU + +**Important**: The WLED web UI is tightly coupled to the backend, so tests must run against the actual ESP32 firmware running in QEMU emulation. + +## Running Tests Locally + +### Prerequisites + +```bash +# Install Node.js dependencies +npm ci + +# Install Playwright browsers +npx playwright install --with-deps chromium + +# Install PlatformIO for building firmware +pip install -r requirements.txt +``` + +### Test with QEMU ESP32 Emulator + +Test the actual firmware running in QEMU ESP32 emulator: + +**Important**: Use the V4 Mainline ethernet debug build for QEMU testing, as WiFi is not emulated and causes crashes. + +1. **Build the firmware**: + ```bash + npm run build # Build web UI + pio run -e esp32_16MB_QEMU_debug # Build V4 M ethernet debug firmware (WiFi disabled, 15+ min first time) + ``` + +2. **Setup QEMU** (first time only): + ```bash + bash .github/scripts/setup-qemu.sh + ``` + +3. **Run firmware in QEMU** (in a separate terminal): + ```bash + bash .github/scripts/run-qemu.sh .pio/build/esp32_16MB_QEMU_debug qemu-esp32 8080 + ``` + + Wait ~30-45 seconds for ESP32 to boot and start the web server. + +4. **Run tests**: + ```bash + WLED_BASE_URL=http://localhost:8080 npm run test:e2e + ``` + +### Test with Real Hardware + +To test against a real ESP32 device: + +1. Flash firmware to your ESP32 +2. Note the device IP address +3. Run tests: + ```bash + WLED_BASE_URL=http:// npm run test:e2e + ``` + +### Other Test Commands + +```bash +# Run tests in UI mode (interactive) +WLED_BASE_URL=http://localhost:8080 npm run test:e2e:ui + +# Run tests in debug mode +WLED_BASE_URL=http://localhost:8080 npm run test:e2e:debug + +# Run a specific test file +WLED_BASE_URL=http://localhost:8080 npx playwright test e2e-tests/index.spec.js +``` + +## Test Structure + +- `index.spec.js` - Tests for the main WLED UI page +- `settings.spec.js` - Tests for all settings pages +- `other-pages.spec.js` - Tests for other pages (simple, welcome, update, liveview) + +## What Tests Check + +Each test verifies: +- ✅ Page loads successfully from ESP32 +- ✅ No JavaScript uncaught exceptions (page errors) +- ✅ Required UI elements are present +- ✅ Backend API endpoints respond correctly + +## CI/CD Integration + +The tests run automatically in GitHub Actions via `.github/workflows/qemu-e2e-test.yml`: + +1. **build-firmware** - Builds ESP32 firmware with embedded web UI +2. **test-qemu** - Runs firmware in QEMU and tests with Playwright + +## Viewing Test Results + +After running tests: +- Console output shows pass/fail status +- HTML report: `playwright-report/index.html` +- Screenshots of failures (if any): `test-results/` + +Open the HTML report: +```bash +npx playwright show-report +``` + +## Troubleshooting + +**QEMU fails to start:** +- Ensure QEMU is installed: `bash .github/scripts/setup-qemu.sh` +- Check QEMU logs: `cat qemu-output.log` +- Verify firmware was built successfully + +**Ethernet/network connection issues:** +- QEMU provides DHCP server (10.0.2.0/24 network, guest IP typically 10.0.2.15) +- The build uses `WLED_QEMU` flag with OpenETH MAC driver (`esp_eth_mac_new_openeth`): + - Bypasses ESP32 internal EMAC hardware (causes LoadStorePIFAddrError crash) + - Uses OpenCores Ethernet MAC driver designed for QEMU's `open_eth` model + - Initializes proper lwIP network interface with DHCP client + - WiFi initialization is completely disabled - WiFi hardware not emulated +- Network works via QEMU's user-mode networking (slirp) with full TCP/IP stack +- **ICMP (ping) does not work** - QEMU user-mode networking limitation (TCP/UDP only) +- If DHCP fails, enable static IP in `platformio.ini` (see comments in file) +- Check QEMU output for "OpenETH configured successfully" message +- Port forwarding: ESP32 port 80 → localhost:8080 + +**Tests fail with connection errors:** +- Wait longer for ESP32 to boot (30-45 seconds minimum) +- Check if HTTP server started: `curl http://localhost:8080/` +- Verify QEMU is still running: `ps aux | grep qemu` +- Check for ethernet connection errors in QEMU logs + +**Tests timeout:** +- QEMU emulation is slow - tests have 45 second timeouts +- Real hardware is faster - adjust timeouts if needed +- Check QEMU output for boot errors + +**Settings pages show "PIN required":** +- This is expected when WLED security PIN is enabled +- Tests verify the page loads even when authentication is required +- The PIN feature is working correctly + +## QEMU Limitations + +ESP32 QEMU emulation has limitations: +- **Network**: User-mode networking with built-in DHCP (10.0.2.0/24) + - Guest IP: 10.0.2.15 (via DHCP or static configuration) + - Port forwarding: ESP32 port 80 → localhost:8080 +- **WiFi**: Not emulated (crashes if enabled - use ethernet build) +- **Peripherals**: Many are stubbed (LEDs, I2C, etc.) +- **Performance**: Slower than real hardware + +Despite these limitations, QEMU is sufficient for testing: +- Web UI loads correctly +- JavaScript executes without errors +- API endpoints respond +- Page navigation works +- Ethernet networking works (via open_eth emulation) + +## Adding New Tests + +1. Create a new `.spec.js` file in `e2e-tests/` +2. Follow the existing test pattern +3. Always check for page errors (uncaught exceptions) +4. Test against QEMU/hardware, not static files +5. Run tests locally before committing + +Example: +```javascript +const { test, expect } = require('@playwright/test'); + +test('my new test', async ({ page }) => { + const pageErrors = []; + page.on('pageerror', error => { + pageErrors.push(error.message); + }); + + await page.goto('/my-page.htm'); + await page.waitForLoadState('load'); + await page.waitForTimeout(2000); + + expect(pageErrors).toHaveLength(0); +}); +``` + +## Future Enhancements + +- [ ] Add JSON API endpoint validation tests +- [ ] Test WebSocket connections for real-time updates +- [ ] Add visual regression testing +- [ ] Test on multiple browsers (Firefox, Safari) +- [ ] Add performance/load testing +- [ ] Test with real ESP32 hardware in CI (if available) +- [ ] Improve QEMU boot time diff --git a/e2e-tests/fixtures.js b/e2e-tests/fixtures.js new file mode 100644 index 0000000000..8d8dbf6c27 --- /dev/null +++ b/e2e-tests/fixtures.js @@ -0,0 +1,54 @@ +// @ts-check +const base = require('@playwright/test'); +const fs = require('fs'); +const path = require('path'); + +const CONSOLE_LOG_DIR = path.join(__dirname, '..', 'console-logs'); + +// Ensure the console-logs directory exists +if (!fs.existsSync(CONSOLE_LOG_DIR)) { + fs.mkdirSync(CONSOLE_LOG_DIR, { recursive: true }); +} + +/** + * Extended test fixture that captures all browser console messages + * and saves them as downloadable CI artifacts. + */ +exports.test = base.test.extend({ + page: async ({ page }, use, testInfo) => { + const consoleLogs = []; + + // Capture ALL console messages (log, warn, error, info, debug) + page.on('console', msg => { + const entry = `[${msg.type().toUpperCase()}] ${msg.text()}`; + consoleLogs.push(entry); + }); + + // Capture uncaught page errors + page.on('pageerror', error => { + consoleLogs.push(`[PAGE_ERROR] ${error.message}`); + }); + + await use(page); + + // After test completes, write console logs to file + if (consoleLogs.length > 0) { + const safeName = testInfo.titlePath.join(' - ') + .replace(/[^a-zA-Z0-9_\-. ]/g, '_') + .replace(/\s+/g, '_'); + const logFile = path.join(CONSOLE_LOG_DIR, `${safeName}.log`); + //const uniqueSuffix = `${testInfo.project.name}-w${testInfo.workerIndex}-r${testInfo.retry}`; // not (yet) compatible with artefacts collection step + //const logFile = path.join(CONSOLE_LOG_DIR, `${safeName}-${uniqueSuffix}.log`); + const logContent = consoleLogs.join('\n') + '\n'; + fs.writeFileSync(logFile, logContent); + + // Also attach to test report for visibility in Playwright HTML report + await testInfo.attach('console-logs', { + body: logContent, + contentType: 'text/plain', + }); + } + }, +}); + +exports.expect = base.expect; diff --git a/e2e-tests/index.spec.js b/e2e-tests/index.spec.js new file mode 100644 index 0000000000..91412187bf --- /dev/null +++ b/e2e-tests/index.spec.js @@ -0,0 +1,77 @@ +// @ts-check +const { test, expect } = require('./fixtures'); + +/** + * Test that the main index page loads without JavaScript errors. + * The WLED web UI is served from compiled-in firmware data. + * + * NOTE: We navigate to '/sliders' (not '/') because '/' calls serveIndexOrWelcome() + * which serves the welcome page when no WiFi config is saved (e.g. fresh boot in QEMU). + * '/sliders' calls serveIndex() directly, always serving the full main UI. + */ +test.describe('WLED Index Page', () => { + test('should load main UI without JavaScript errors', async ({ page }) => { + const consoleErrors = []; + const pageErrors = []; + + // Listen for console errors + page.on('console', msg => { + if (msg.type() === 'error') { + consoleErrors.push(msg.text()); + } + }); + + // Listen for page errors (uncaught exceptions) + page.on('pageerror', error => { + pageErrors.push(error.message); + }); + + // Use /sliders — always serves the built-in main UI, bypasses welcome page check + await page.goto('/sliders'); + await page.waitForLoadState('load'); + + // Wait a bit for initial JavaScript to execute + await page.waitForTimeout(3000); + + // Title is "WLED" from WLED, or device name (also contains WLED) + await expect(page).toHaveTitle(/WLED/); + + // Check for JavaScript errors + expect(pageErrors, `Page errors found: ${pageErrors.join(', ')}`).toHaveLength(0); + + // Console errors are informational only for now - many expected due to missing API + if (consoleErrors.length > 0) { + console.log(`Console errors (informational): ${consoleErrors.length} errors`); + } + }); + + test('should have basic UI elements', async ({ page }) => { + // Use /sliders — always serves the built-in main UI + await page.goto('/sliders'); + await page.waitForLoadState('load'); + await page.waitForTimeout(3000); + + // Both `#picker` (color wheel) and `#sliders` are defined in index.htm + await expect(page.locator('#picker')).toBeAttached(); + await expect(page.locator('#sliders')).toBeAttached(); + }); + + test('JSON API /json/info should return valid data', async ({ page }) => { + const response = await page.request.get('/json/info'); + expect(response.status()).toBe(200); + const json = await response.json(); + // Check basic fields exist + expect(json).toHaveProperty('ver'); + expect(json).toHaveProperty('vid'); + expect(json).toHaveProperty('leds'); + }); + + test('JSON API /json/state should return valid data', async ({ page }) => { + const response = await page.request.get('/json/state'); + expect(response.status()).toBe(200); + const json = await response.json(); + // Check basic state fields + expect(json).toHaveProperty('on'); + expect(json).toHaveProperty('bri'); + }); +}); diff --git a/e2e-tests/other-pages.spec.js b/e2e-tests/other-pages.spec.js new file mode 100644 index 0000000000..7adbf8a7d4 --- /dev/null +++ b/e2e-tests/other-pages.spec.js @@ -0,0 +1,38 @@ +// @ts-check +const { test, expect } = require('./fixtures'); + +/** + * Test other WLED pages load without JavaScript errors. + * These pages are served from compiled-in firmware data via routes defined + * in wled_server.cpp, NOT from LittleFS filesystem paths. + */ +test.describe('WLED Other Pages', () => { + const otherPages = [ + { path: '/simple.htm', name: 'Simple Control' }, + { path: '/welcome', name: 'Welcome Page' }, + { path: '/update', name: 'Update Page' }, + { path: '/liveview', name: 'Live View' }, + ]; + + for (const { path, name } of otherPages) { + test(`${name} (${path}) should load without JavaScript errors`, async ({ page }) => { + const pageErrors = []; + + // Listen for page errors (uncaught exceptions) + page.on('pageerror', error => { + pageErrors.push(error.message); + }); + + await page.goto(path); + await page.waitForLoadState('load'); + await page.waitForTimeout(2000); + + // Check that the page loaded (these pages may have different titles) + const title = await page.title(); + expect(title).toBeTruthy(); + + // Check for JavaScript uncaught exceptions + expect(pageErrors, `Page errors in ${name}: ${pageErrors.join(', ')}`).toHaveLength(0); + }); + } +}); diff --git a/e2e-tests/settings.spec.js b/e2e-tests/settings.spec.js new file mode 100644 index 0000000000..4753c0f49c --- /dev/null +++ b/e2e-tests/settings.spec.js @@ -0,0 +1,54 @@ +// @ts-check +const { test, expect } = require('./fixtures'); + +/** + * Test that all settings pages load without JavaScript errors. + * WLED serves settings pages from compiled-in firmware data via URL routes: + * /settings → main settings page + * /settings/wifi → WiFi settings + * /settings/leds → LED settings + * /settings/ui → UI settings + * /settings/sync → Sync interfaces + * /settings/time → Time & macros + * /settings/sec → Security & updates + * /settings/dmx → DMX output + * /settings/um → Usermods + * /settings/2D → 2D configuration + * + * Note: The paths like /settings_wifi.htm are filesystem-based and won't work + * without LittleFS content. Use the route-based URLs instead. + */ +test.describe('WLED Settings Pages', () => { + const settingsPages = [ + { path: '/settings', name: 'Main Settings', title: 'WLED Settings' }, + { path: '/settings/wifi', name: 'WiFi Settings', title: 'Wi-Fi Settings' }, + { path: '/settings/leds', name: 'LED Settings', title: 'LED Settings' }, + { path: '/settings/ui', name: 'UI Settings', title: 'UI Settings' }, + { path: '/settings/sync', name: 'Sync Settings', title: 'Sync Settings' }, + { path: '/settings/time', name: 'Time Settings', title: 'Time Settings' }, + { path: '/settings/sec', name: 'Security Settings', title: 'Misc Settings' }, + { path: '/settings/um', name: 'Usermod Settings', title: 'Usermod Settings' }, + { path: '/settings/2D', name: '2D Settings', title: '2D Set-up' }, + ]; + + for (const { path, name, title } of settingsPages) { + test(`${name} (${path}) should load without JavaScript errors`, async ({ page }) => { + const pageErrors = []; + + // Listen for page errors (uncaught exceptions) + page.on('pageerror', error => { + pageErrors.push(error.message); + }); + + await page.goto(path); + await page.waitForLoadState('load'); + await page.waitForTimeout(2000); + + // Check that the page loaded with expected title + await expect(page).toHaveTitle(title); + + // Check for JavaScript uncaught exceptions + expect(pageErrors, `Page errors in ${name}: ${pageErrors.join(', ')}`).toHaveLength(0); + }); + } +}); diff --git a/lib/openeth_mac/esp_eth_mac_openeth.c b/lib/openeth_mac/esp_eth_mac_openeth.c new file mode 100644 index 0000000000..1a048d61bc --- /dev/null +++ b/lib/openeth_mac/esp_eth_mac_openeth.c @@ -0,0 +1,434 @@ +// Copyright 2019 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This is a driver for OpenCores Ethernet MAC (https://opencores.org/projects/ethmac). +// Espressif chips do not use this MAC, but it is supported in QEMU +// (see hw/net/opencores_eth.c). Since the interface of this MAC is a relatively +// simple one, it is used for the purpose of running IDF apps in QEMU. +// The QEMU driver also emulates the DP83848C PHY, which is supported in IDF. +// Note that this driver is written with QEMU in mind. For example, it doesn't +// handle errors which QEMU will not report, and doesn't wait for TX to be +// finished, since QEMU does this instantly. +// +// Source: ESP-IDF v4.4.7 components/esp_eth/src/esp_eth_mac_openeth.c +// Vendored into WLED-MM to provide esp_eth_mac_new_openeth() which is not compiled +// into the pre-built Tasmota/arduino-esp32 libethernet.a. + +#include +#include +#include +#include +#include "esp_log.h" +#include "esp_check.h" +#include "esp_eth.h" +#include "esp_intr_alloc.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "hal/cpu_hal.h" +#include "openeth.h" + +static const char *TAG = "opencores.emac"; + +// Driver state structure +typedef struct { + esp_eth_mac_t parent; + esp_eth_mediator_t *eth; + intr_handle_t intr_hdl; + TaskHandle_t rx_task_hdl; + int cur_rx_desc; + int cur_tx_desc; + uint8_t addr[6]; + uint8_t *rx_buf[RX_BUF_COUNT]; + uint8_t *tx_buf[TX_BUF_COUNT]; +} emac_opencores_t; + + +// Interrupt handler and the receive task + +static esp_err_t emac_opencores_receive(esp_eth_mac_t *mac, uint8_t *buf, uint32_t *length); + +static IRAM_ATTR void emac_opencores_isr_handler(void *args) +{ + emac_opencores_t *emac = (emac_opencores_t *) args; + BaseType_t high_task_wakeup; + + uint32_t status = REG_READ(OPENETH_INT_SOURCE_REG); + + if (status & OPENETH_INT_RXB) { + // Notify receive task + vTaskNotifyGiveFromISR(emac->rx_task_hdl, &high_task_wakeup); + if (high_task_wakeup) { + portYIELD_FROM_ISR(); + } + } + + if (status & OPENETH_INT_BUSY) { + ESP_EARLY_LOGW(TAG, "%s: RX frame dropped (0x%x)", __func__, status); + } + + // Clear interrupt + REG_WRITE(OPENETH_INT_SOURCE_REG, status); +} + +static void emac_opencores_rx_task(void *arg) +{ + emac_opencores_t *emac = (emac_opencores_t *)arg; + uint8_t *buffer = NULL; + uint32_t length = 0; + while (1) { + if (ulTaskNotifyTake(pdFALSE, portMAX_DELAY)) { + while (true) { + length = ETH_MAX_PACKET_SIZE; + buffer = malloc(length); + if (!buffer) { + ESP_LOGE(TAG, "no mem for receive buffer"); + } else if (emac_opencores_receive(&emac->parent, buffer, &length) == ESP_OK) { + // pass the buffer to the upper layer + if (length) { + emac->eth->stack_input(emac->eth, buffer, length); + } else { + free(buffer); + } + } else { + free(buffer); + break; + } + } + } + } + vTaskDelete(NULL); +} + + +// Below functions implement the driver interface + +static esp_err_t emac_opencores_set_mediator(esp_eth_mac_t *mac, esp_eth_mediator_t *eth) +{ + esp_err_t ret = ESP_OK; + ESP_GOTO_ON_FALSE(eth, ESP_ERR_INVALID_ARG, err, TAG, "can't set mac's mediator to null"); + emac_opencores_t *emac = __containerof(mac, emac_opencores_t, parent); + emac->eth = eth; + return ESP_OK; +err: + return ret; +} + +static esp_err_t emac_opencores_write_phy_reg(esp_eth_mac_t *mac, uint32_t phy_addr, uint32_t phy_reg, uint32_t reg_value) +{ + ESP_LOGV(TAG, "%s: addr=%d reg=0x%x val=0x%04x", __func__, phy_addr, phy_reg, reg_value); + REG_SET_FIELD(OPENETH_MIIADDRESS_REG, OPENETH_FIAD, phy_addr); + REG_SET_FIELD(OPENETH_MIIADDRESS_REG, OPENETH_RGAD, phy_reg); + REG_WRITE(OPENETH_MIITX_DATA_REG, reg_value & OPENETH_MII_DATA_MASK); + REG_SET_BIT(OPENETH_MIICOMMAND_REG, OPENETH_WCTRLDATA); + return ESP_OK; +} + +static esp_err_t emac_opencores_read_phy_reg(esp_eth_mac_t *mac, uint32_t phy_addr, uint32_t phy_reg, uint32_t *reg_value) +{ + esp_err_t ret = ESP_OK; + ESP_GOTO_ON_FALSE(reg_value, ESP_ERR_INVALID_ARG, err, TAG, "can't set reg_value to null"); + REG_SET_FIELD(OPENETH_MIIADDRESS_REG, OPENETH_FIAD, phy_addr); + REG_SET_FIELD(OPENETH_MIIADDRESS_REG, OPENETH_RGAD, phy_reg); + REG_SET_BIT(OPENETH_MIICOMMAND_REG, OPENETH_RSTAT); + *reg_value = (REG_READ(OPENETH_MIIRX_DATA_REG) & OPENETH_MII_DATA_MASK); + ESP_LOGV(TAG, "%s: addr=%d reg=0x%x val=0x%04x", __func__, phy_addr, phy_reg, *reg_value); + return ESP_OK; +err: + return ret; +} + +static esp_err_t emac_opencores_set_addr(esp_eth_mac_t *mac, uint8_t *addr) +{ + ESP_LOGV(TAG, "%s: " MACSTR, __func__, MAC2STR(addr)); + esp_err_t ret = ESP_OK; + ESP_GOTO_ON_FALSE(addr, ESP_ERR_INVALID_ARG, err, TAG, "can't set mac addr to null"); + emac_opencores_t *emac = __containerof(mac, emac_opencores_t, parent); + memcpy(emac->addr, addr, 6); + const uint8_t mac0[4] = {addr[5], addr[4], addr[3], addr[2]}; + const uint8_t mac1[4] = {addr[1], addr[0]}; + uint32_t mac0_u32, mac1_u32; + memcpy(&mac0_u32, &mac0, 4); + memcpy(&mac1_u32, &mac1, 4); + REG_WRITE(OPENETH_MAC_ADDR0_REG, mac0_u32); + REG_WRITE(OPENETH_MAC_ADDR1_REG, mac1_u32); + return ESP_OK; +err: + return ret; +} + +static esp_err_t emac_opencores_get_addr(esp_eth_mac_t *mac, uint8_t *addr) +{ + ESP_LOGV(TAG, "%s: " MACSTR, __func__, MAC2STR(addr)); + esp_err_t ret = ESP_OK; + ESP_GOTO_ON_FALSE(addr, ESP_ERR_INVALID_ARG, err, TAG, "can't set mac addr to null"); + emac_opencores_t *emac = __containerof(mac, emac_opencores_t, parent); + memcpy(addr, emac->addr, 6); + return ESP_OK; +err: + return ret; +} + +static esp_err_t emac_opencores_set_link(esp_eth_mac_t *mac, eth_link_t link) +{ + ESP_LOGV(TAG, "%s: %s", __func__, link == ETH_LINK_UP ? "up" : "down"); + esp_err_t ret = ESP_OK; + emac_opencores_t *emac = __containerof(mac, emac_opencores_t, parent); + switch (link) { + case ETH_LINK_UP: + ESP_GOTO_ON_ERROR(esp_intr_enable(emac->intr_hdl), err, TAG, "enable interrupt failed"); + openeth_enable(); + break; + case ETH_LINK_DOWN: + ESP_GOTO_ON_ERROR(esp_intr_disable(emac->intr_hdl), err, TAG, "disable interrupt failed"); + openeth_disable(); + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_INVALID_ARG, err, TAG, "unknown link status"); + break; + } + return ESP_OK; +err: + return ret; +} + +static esp_err_t emac_opencores_set_speed(esp_eth_mac_t *mac, eth_speed_t speed) +{ + /* QEMU doesn't emulate PHY speed, so accept any value */ + return ESP_OK; +} + +static esp_err_t emac_opencores_set_duplex(esp_eth_mac_t *mac, eth_duplex_t duplex) +{ + /* QEMU doesn't emulate full/half duplex, so accept any value */ + return ESP_OK; +} + +static esp_err_t emac_opencores_set_promiscuous(esp_eth_mac_t *mac, bool enable) +{ + if (enable) { + REG_SET_BIT(OPENETH_MODER_REG, OPENETH_PRO); + } else { + REG_CLR_BIT(OPENETH_MODER_REG, OPENETH_PRO); + } + return ESP_OK; +} + +static esp_err_t emac_opencores_enable_flow_ctrl(esp_eth_mac_t *mac, bool enable) +{ + /* QEMU doesn't emulate flow control function, so accept any value */ + return ESP_OK; +} + +static esp_err_t emac_opencores_set_peer_pause_ability(esp_eth_mac_t *mac, uint32_t ability) +{ + /* QEMU doesn't emulate PAUSE function, so accept any value */ + return ESP_OK; +} + +static esp_err_t emac_opencores_transmit(esp_eth_mac_t *mac, uint8_t *buf, uint32_t length) +{ + esp_err_t ret = ESP_OK; + emac_opencores_t *emac = __containerof(mac, emac_opencores_t, parent); + ESP_GOTO_ON_FALSE(length < DMA_BUF_SIZE * TX_BUF_COUNT, ESP_ERR_INVALID_SIZE, err, TAG, "insufficient TX buffer size"); + + uint32_t bytes_remaining = length; + // In QEMU, there never is a TX operation in progress, so start with descriptor 0. + + ESP_LOGV(TAG, "%s: len=%d", __func__, length); + while (bytes_remaining > 0) { + uint32_t will_write = MIN(bytes_remaining, DMA_BUF_SIZE); + memcpy(emac->tx_buf[emac->cur_tx_desc], buf, will_write); + openeth_tx_desc_t *desc_ptr = openeth_tx_desc(emac->cur_tx_desc); + openeth_tx_desc_t desc_val = *desc_ptr; + desc_val.wr = (emac->cur_tx_desc == TX_BUF_COUNT - 1); + desc_val.len = will_write; + desc_val.rd = 1; + // TXEN is already set, and this triggers a TX operation for the descriptor + ESP_LOGV(TAG, "%s: desc %d (%p) len=%d wr=%d", __func__, emac->cur_tx_desc, desc_ptr, will_write, desc_val.wr); + *desc_ptr = desc_val; + bytes_remaining -= will_write; + buf += will_write; + emac->cur_tx_desc = (emac->cur_tx_desc + 1) % TX_BUF_COUNT; + } + + return ESP_OK; +err: + return ret; +} + +static esp_err_t emac_opencores_receive(esp_eth_mac_t *mac, uint8_t *buf, uint32_t *length) +{ + esp_err_t ret = ESP_OK; + emac_opencores_t *emac = __containerof(mac, emac_opencores_t, parent); + + openeth_rx_desc_t *desc_ptr = openeth_rx_desc(emac->cur_rx_desc); + openeth_rx_desc_t desc_val = *desc_ptr; + ESP_LOGV(TAG, "%s: desc %d (%p) e=%d len=%d wr=%d", __func__, emac->cur_rx_desc, desc_ptr, desc_val.e, desc_val.len, desc_val.wr); + if (desc_val.e) { + ret = ESP_ERR_INVALID_STATE; + goto err; + } + size_t rx_length = desc_val.len; + ESP_GOTO_ON_FALSE(*length >= rx_length, ESP_ERR_INVALID_SIZE, err, TAG, "RX length too large"); + *length = rx_length; + memcpy(buf, desc_val.rxpnt, *length); + desc_val.e = 1; + *desc_ptr = desc_val; + + emac->cur_rx_desc = (emac->cur_rx_desc + 1) % RX_BUF_COUNT; + return ESP_OK; +err: + return ret; +} + +static esp_err_t emac_opencores_init(esp_eth_mac_t *mac) +{ + esp_err_t ret = ESP_OK; + emac_opencores_t *emac = __containerof(mac, emac_opencores_t, parent); + esp_eth_mediator_t *eth = emac->eth; + ESP_GOTO_ON_ERROR(eth->on_state_changed(eth, ETH_STATE_LLINIT, NULL), err, TAG, "lowlevel init failed"); + ESP_GOTO_ON_ERROR(esp_read_mac(emac->addr, ESP_MAC_ETH), err, TAG, "fetch ethernet mac address failed"); + + // Sanity check + if (REG_READ(OPENETH_MODER_REG) != OPENETH_MODER_DEFAULT) { + ESP_LOGE(TAG, "CONFIG_ETH_USE_OPENETH should only be used when running in QEMU."); + ESP_LOGE(TAG, "When running the app on the ESP32, use CONFIG_ETH_USE_ESP32_EMAC instead."); + abort(); + } + // Initialize the MAC + openeth_reset(); + openeth_set_tx_desc_cnt(TX_BUF_COUNT); + emac_opencores_set_addr(mac, emac->addr); + + return ESP_OK; +err: + eth->on_state_changed(eth, ETH_STATE_DEINIT, NULL); + return ret; +} + +static esp_err_t emac_opencores_deinit(esp_eth_mac_t *mac) +{ + emac_opencores_t *emac = __containerof(mac, emac_opencores_t, parent); + esp_eth_mediator_t *eth = emac->eth; + eth->on_state_changed(eth, ETH_STATE_DEINIT, NULL); + return ESP_OK; +} + +static esp_err_t emac_opencores_start(esp_eth_mac_t *mac) +{ + openeth_enable(); + return ESP_OK; +} + +static esp_err_t emac_opencores_stop(esp_eth_mac_t *mac) +{ + openeth_disable(); + return ESP_OK; +} + +static esp_err_t emac_opencores_del(esp_eth_mac_t *mac) +{ + emac_opencores_t *emac = __containerof(mac, emac_opencores_t, parent); + esp_intr_free(emac->intr_hdl); + vTaskDelete(emac->rx_task_hdl); + for (int i = 0; i < RX_BUF_COUNT; i++) { + free(emac->rx_buf[i]); + } + for (int i = 0; i < TX_BUF_COUNT; i++) { + free(emac->tx_buf[i]); + } + free(emac); + return ESP_OK; +} + +esp_eth_mac_t *esp_eth_mac_new_openeth(const eth_mac_config_t *config) +{ + esp_eth_mac_t *ret = NULL; + emac_opencores_t *emac = NULL; + ESP_GOTO_ON_FALSE(config, NULL, out, TAG, "can't set mac config to null"); + emac = calloc(1, sizeof(emac_opencores_t)); + ESP_GOTO_ON_FALSE(emac, NULL, out, TAG, "calloc emac failed"); + + // Allocate DMA buffers + for (int i = 0; i < RX_BUF_COUNT; i++) { + emac->rx_buf[i] = heap_caps_calloc(1, DMA_BUF_SIZE, MALLOC_CAP_DMA); + if (!(emac->rx_buf[i])) { + goto out; + } + openeth_init_rx_desc(openeth_rx_desc(i), emac->rx_buf[i]); + } + openeth_rx_desc(RX_BUF_COUNT - 1)->wr = 1; + emac->cur_rx_desc = 0; + + for (int i = 0; i < TX_BUF_COUNT; i++) { + emac->tx_buf[i] = heap_caps_calloc(1, DMA_BUF_SIZE, MALLOC_CAP_DMA); + if (!(emac->tx_buf[i])) { + goto out; + } + openeth_init_tx_desc(openeth_tx_desc(i), emac->tx_buf[i]); + } + openeth_tx_desc(TX_BUF_COUNT - 1)->wr = 1; + emac->cur_tx_desc = 0; + + emac->parent.set_mediator = emac_opencores_set_mediator; + emac->parent.init = emac_opencores_init; + emac->parent.deinit = emac_opencores_deinit; + emac->parent.start = emac_opencores_start; + emac->parent.stop = emac_opencores_stop; + emac->parent.del = emac_opencores_del; + emac->parent.write_phy_reg = emac_opencores_write_phy_reg; + emac->parent.read_phy_reg = emac_opencores_read_phy_reg; + emac->parent.set_addr = emac_opencores_set_addr; + emac->parent.get_addr = emac_opencores_get_addr; + emac->parent.set_speed = emac_opencores_set_speed; + emac->parent.set_duplex = emac_opencores_set_duplex; + emac->parent.set_link = emac_opencores_set_link; + emac->parent.set_promiscuous = emac_opencores_set_promiscuous; + emac->parent.set_peer_pause_ability = emac_opencores_set_peer_pause_ability; + emac->parent.enable_flow_ctrl = emac_opencores_enable_flow_ctrl; + emac->parent.transmit = emac_opencores_transmit; + emac->parent.receive = emac_opencores_receive; + + // Initialize the interrupt + ESP_GOTO_ON_FALSE(esp_intr_alloc(OPENETH_INTR_SOURCE, ESP_INTR_FLAG_IRAM, emac_opencores_isr_handler, emac, &(emac->intr_hdl)) == ESP_OK, NULL, out, TAG, "alloc emac interrupt failed"); + + // Create the RX task + BaseType_t core_num = tskNO_AFFINITY; + if (config->flags & ETH_MAC_FLAG_PIN_TO_CORE) { + core_num = cpu_hal_get_core_id(); + } + BaseType_t xReturned = xTaskCreatePinnedToCore(emac_opencores_rx_task, "emac_rx", config->rx_task_stack_size, emac, + config->rx_task_prio, &emac->rx_task_hdl, core_num); + ESP_GOTO_ON_FALSE(xReturned == pdPASS, NULL, out, TAG, "create emac_rx task failed"); + return &(emac->parent); + +out: + if (emac) { + if (emac->rx_task_hdl) { + vTaskDelete(emac->rx_task_hdl); + } + if (emac->intr_hdl) { + esp_intr_free(emac->intr_hdl); + } + for (int i = 0; i < TX_BUF_COUNT; i++) { + free(emac->tx_buf[i]); + } + for (int i = 0; i < RX_BUF_COUNT; i++) { + free(emac->rx_buf[i]); + } + free(emac); + } + return ret; +} diff --git a/lib/openeth_mac/library.json b/lib/openeth_mac/library.json new file mode 100644 index 0000000000..01c3ba64c4 --- /dev/null +++ b/lib/openeth_mac/library.json @@ -0,0 +1,13 @@ +{ + "name": "openeth_mac", + "version": "1.0.0", + "description": "OpenCores Ethernet MAC driver for QEMU ESP32 emulation, vendored from ESP-IDF v4.4.7 (not compiled into pre-built Tasmota/arduino-esp32 libethernet.a)", + "license": "Apache-2.0", + "frameworks": "arduino", + "platforms": "espressif32", + "build": { + "srcFilter": ["+<*.c>"], + "includeDir": ".", + "srcDir": "." + } +} diff --git a/lib/openeth_mac/openeth.h b/lib/openeth_mac/openeth.h new file mode 100644 index 0000000000..10b144d523 --- /dev/null +++ b/lib/openeth_mac/openeth.h @@ -0,0 +1,221 @@ +/* + * SPDX-FileCopyrightText: 2019-2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + * + * Source: ESP-IDF v4.4.7 components/esp_eth/src/openeth.h + * Vendored into WLED-MM to provide esp_eth_mac_new_openeth() which is not compiled + * into the pre-built Tasmota/arduino-esp32 libethernet.a. + */ + +#pragma once +#include +#include +#include +#include "sdkconfig.h" +#include "soc/soc.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// These are the register definitions for the OpenCores Ethernet MAC. +// See comments in esp_eth_mac_openeth.c for more details about this driver. + +// DMA buffers configuration +#define DMA_BUF_SIZE 1600 + +// Provide defaults for sdkconfig values that are not available in arduino-esp32 PlatformIO builds +#ifndef CONFIG_ETH_OPENETH_DMA_RX_BUFFER_NUM +#define CONFIG_ETH_OPENETH_DMA_RX_BUFFER_NUM 4 +#endif +#ifndef CONFIG_ETH_OPENETH_DMA_TX_BUFFER_NUM +#define CONFIG_ETH_OPENETH_DMA_TX_BUFFER_NUM 1 +#endif + +#define RX_BUF_COUNT CONFIG_ETH_OPENETH_DMA_RX_BUFFER_NUM +#define TX_BUF_COUNT CONFIG_ETH_OPENETH_DMA_TX_BUFFER_NUM + +// This driver uses the interrupt source number of the internal EMAC of the ESP32 chip, +// and uses the same register address base. This of course only works in QEMU, where +// the OpenCores MAC is mapped to the same register base and to the same interrupt +// source. This driver does a sanity check that it is not running on the real ESP32 +// chip, using the EMAC date register. +#define OPENETH_INTR_SOURCE ETS_ETH_MAC_INTR_SOURCE +#define OPENETH_BASE DR_REG_EMAC_BASE + +// OpenCores ethmac registers +#define OPENETH_MODER_REG (OPENETH_BASE + 0x00) +#define OPENETH_MODER_DEFAULT 0xa000 +// OPENETH_RST: reset the MAC +#define OPENETH_RST BIT(11) +// OPENETH_PRO: enable promiscuous mode +#define OPENETH_PRO BIT(5) +// OPENETH_TXEN: enable transmit +#define OPENETH_TXEN BIT(1) +// OPENETH_RXEN: enable receive +#define OPENETH_RXEN BIT(0) + +#define OPENETH_INT_SOURCE_REG (OPENETH_BASE + 0x04) +#define OPENETH_INT_MASK_REG (OPENETH_BASE + 0x08) +// These bits apply to INT_SOURCE and INT_MASK registers: +// OPENETH_INT_BUSY: Buffer was received and discarded due to lack of buffers +#define OPENETH_INT_BUSY BIT(4) +// OPENETH_INT_RXB: Frame received +#define OPENETH_INT_RXB BIT(2) +// OPENETH_INT_TXB: Frame transmitted +#define OPENETH_INT_TXB BIT(0) + +// IPGT, IPGR1, IPGR2 registers are not implemented in QEMU, hence not used here +#define OPENETH_PACKETLEN_REG (OPENETH_BASE + 0x18) +// OPENETH_MINFL: minimum frame length +#define OPENETH_MINFL_S 16 +#define OPENETH_MINFL_V 0xffff +#define OPENETH_MINFL_M (OPENETH_MINFL_V << OPENETH_MINFL_S) +// OPENETH_MAXFL: maximum frame length +#define OPENETH_MAXFL_S 0 +#define OPENETH_MAXFL_V 0xffff +#define OPENETH_MAXFL_M (OPENETH_MAXFL_V << OPENETH_MAXFL_S) + +// COLLCONF is not implemented in QEMU +#define OPENETH_TX_BD_NUM_REG (OPENETH_BASE + 0x20) +// CTRLMODER, MIIMODER are not implemented in QEMU +#define OPENETH_MIICOMMAND_REG (OPENETH_BASE + 0x2c) +// OPENETH_WCTRLDATA: write control data +#define OPENETH_WCTRLDATA BIT(2) +// OPENETH_RSTAT: read status +#define OPENETH_RSTAT BIT(1) +// OPENETH_SCANSTAT: scan status +#define OPENETH_SCANSTAT BIT(0) + +#define OPENETH_MIIADDRESS_REG (OPENETH_BASE + 0x30) +// OPENETH_RGAD: register address +#define OPENETH_RGAD_S 8 +#define OPENETH_RGAD_V 0x1f +#define OPENETH_RGAD_M (OPENETH_RGAD_V << OPENETH_RGAD_S) +// OPENETH_FIAD: PHY address +#define OPENETH_FIAD_S 0 +#define OPENETH_FIAD_V 0x1f +#define OPENETH_FIAD_N (OPENETH_FIAD_V << OPENETH_FIAD_S) + +#define OPENETH_MIITX_DATA_REG (OPENETH_BASE + 0x34) +#define OPENETH_MIIRX_DATA_REG (OPENETH_BASE + 0x38) +#define OPENETH_MII_DATA_MASK 0xffff + +#define OPENETH_MIISTATUS_REG (OPENETH_BASE + 0x3c) +// OPENETH_LINKFAIL: link is down +#define OPENETH_LINKFAIL BIT(0) + +// OPENETH_MAC_ADDR0_REG: bytes 2-5 of the MAC address (byte 5 in LSB) +#define OPENETH_MAC_ADDR0_REG (OPENETH_BASE + 0x40) +// OPENETH_MAC_ADDR1_REG: bytes 0-1 of the MAC address (byte 1 in LSB) +#define OPENETH_MAC_ADDR1_REG (OPENETH_BASE + 0x44) + +#define OPENETH_HASH0_ADR_REG (OPENETH_BASE + 0x48) +#define OPENETH_HASH1_ADR_REG (OPENETH_BASE + 0x4c) + +// Location of the DMA descriptors +#define OPENETH_DESC_BASE (OPENETH_BASE + 0x400) +// Total number of (TX + RX) DMA descriptors +#define OPENETH_DESC_CNT 128 + + +// Structures describing TX and RX descriptors. +// The field names are same as in the OpenCores ethmac documentation. +typedef struct { + uint16_t cs: 1; //!< Carrier sense lost (flag set by HW) + uint16_t df: 1; //!< Defer indication (flag set by HW) + uint16_t lc: 1; //!< Late collision occured (flag set by HW) + uint16_t rl: 1; //!< TX failed due to retransmission limit (flag set by HW) + uint16_t rtry: 4; //!< Number of retries before the frame was sent (set by HW) + uint16_t ur: 1; //!< Underrun status (flag set by HW) + uint16_t rsv: 2; //!< Reserved + uint16_t crc: 1; //!< Add CRC at the end of the packet + uint16_t pad: 1; //!< Add padding to the end of short packets + uint16_t wr: 1; //!< Wrap-around. 0: not the last descriptor in the table, 1: last descriptor. + uint16_t irq: 1; //!< Generate interrupt after this descriptor is transmitted + uint16_t rd: 1; //!< Descriptor ready. 0: descriptor owned by SW, 1: descriptor owned by HW. Cleared by HW. + + uint16_t len; //!< Number of bytes to be transmitted + void* txpnt; //!< Pointer to the data to transmit +} openeth_tx_desc_t; + +_Static_assert(sizeof(openeth_tx_desc_t) == 8, "incorrect size of openeth_tx_desc_t"); + +typedef struct { + uint16_t lc: 1; //!< Late collision flag + uint16_t crc: 1; //!< RX CRC error flag + uint16_t sf: 1; //!< Frame shorter than set in PACKETLEN register + uint16_t tl: 1; //!< Frame longer than set in PACKETLEN register + uint16_t dn: 1; //!< Dribble nibble (frame length not divisible by 8 bits) flag + uint16_t is: 1; //!< Invalid symbol flag + uint16_t or_flag: 1; //!< Overrun flag (renamed from 'or' to avoid C++ keyword conflict) + uint16_t m: 1; //!< Frame received because of the promiscuous mode + uint16_t rsv: 5; //!< Reserved + uint16_t wr: 1; //!< Wrap-around. 0: not the last descriptor in the table, 1: last descriptor. + uint16_t irq: 1; //!< Generate interrupt after this descriptor is transmitted + uint16_t e: 1; //!< The buffer is empty. 0: descriptor owned by SW, 1: descriptor owned by HW. + + uint16_t len; //!< Number of bytes received (filled by HW) + void* rxpnt; //!< Pointer to the receive buffer +} openeth_rx_desc_t; + +_Static_assert(sizeof(openeth_rx_desc_t) == 8, "incorrect size of openeth_rx_desc_t"); + + +static inline openeth_tx_desc_t* openeth_tx_desc(int idx) +{ + assert(idx < TX_BUF_COUNT); + return &((openeth_tx_desc_t*)OPENETH_DESC_BASE)[idx]; +} + +static inline openeth_rx_desc_t* openeth_rx_desc(int idx) +{ + assert(idx < OPENETH_DESC_CNT - TX_BUF_COUNT); + return &((openeth_rx_desc_t*)OPENETH_DESC_BASE)[idx + TX_BUF_COUNT]; +} + +static inline void openeth_enable(void) +{ + REG_SET_BIT(OPENETH_MODER_REG, OPENETH_TXEN | OPENETH_RXEN | OPENETH_PRO); + REG_SET_BIT(OPENETH_INT_MASK_REG, OPENETH_INT_RXB); +} + +static inline void openeth_disable(void) +{ + REG_CLR_BIT(OPENETH_INT_MASK_REG, OPENETH_INT_RXB); + REG_CLR_BIT(OPENETH_MODER_REG, OPENETH_TXEN | OPENETH_RXEN | OPENETH_PRO); +} + +static inline void openeth_reset(void) +{ + REG_SET_BIT(OPENETH_MODER_REG, OPENETH_RST); + REG_CLR_BIT(OPENETH_MODER_REG, OPENETH_RST); +} + +static inline void openeth_init_tx_desc(openeth_tx_desc_t* desc, void* buf) +{ + *desc = (openeth_tx_desc_t) { + .rd = 0, + .txpnt = buf + }; +} + +static inline void openeth_init_rx_desc(openeth_rx_desc_t* desc, void* buf) +{ + *desc = (openeth_rx_desc_t) { + .e = 1, + .irq = 1, + .rxpnt = buf + }; +} + +static inline void openeth_set_tx_desc_cnt(int tx_desc_cnt) +{ + assert(tx_desc_cnt <= OPENETH_DESC_CNT); + REG_WRITE(OPENETH_TX_BD_NUM_REG, tx_desc_cnt); +} + +#ifdef __cplusplus +} +#endif diff --git a/package.json b/package.json index f3e5f4340e..1aaf9c19b0 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "scripts": { "build": "node tools/cdata.js", "test": "node --test", + "test:e2e": "npx playwright test", "dev": "nodemon -e js,html,htm,css,png,jpg,gif,ico,js -w tools/ -w wled00/data/ -x node tools/cdata.js" }, "repository": { diff --git a/platformio.ini b/platformio.ini index 15485d2941..562b338090 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1996,6 +1996,79 @@ lib_deps = ${esp32_4MB_V4_M_base.esp32_lib_deps} ; RAM: [=== ] 26.7% (used 87368 bytes from 327680 bytes) ; Flash: [======= ] 65.5% (used 2061117 bytes from 3145728 bytes) + +## for testing with QEMU +[env:esp32_16MB_QEMU_debug] +extends = esp32_4MB_V4_M_base +platform = ${esp32.platformTasmota} ;; board_build.sdkconfig is only supported in pioarduino +platform_packages = ${esp32.platform_packagesTasmota} +board = esp32_16MB-poe ;; needed for ethernet boards (selects "esp32-poe" as variant) +board_build.partitions = ${esp32.extreme_partitions} ;; WLED extended for 16MB flash: 3.2MB firmware, 9 MB filesystem +;board_build.sdkconfig = sdkconfig.defaults.qemu ;; NOTE: only works with pioarduino, not Tasmota platform; openeth_mac lib used instead +;build_unflags = ${esp32_4MB_V4_M_base.build_unflags} +build_unflags = ${esp32_4MB_V4_S_base.build_unflags} + ;; removing some usermods to keep it simple + -D USERMOD_DALLASTEMPERATURE + -D USERMOD_FOUR_LINE_DISPLAY + ;;-D USERMOD_ARTIFX + -D USERMOD_ROTARY_ENCODER_UI + -D USERMOD_AUTO_SAVE + -D USERMOD_PIRSWITCH + -D USERMOD_MULTI_RELAY + -D USE_ALT_DISPLAY ; new versions of USERMOD_FOUR_LINE_DISPLAY and USERMOD_ROTARY_ENCODER_UI + -D USERMOD_MPU6050_IMU ; gyro/accelero for USERMOD_GAMES (ONLY WORKS IF USERMOD_FOUR_LINE_DISPLAY NOT INCLUDED - I2C SHARING BUG) + -D USERMOD_GAMES ; WLEDMM usermod + ${common_mm.animartrix_build_flags} + ${common_mm.HUB75_build_flags} + -D WLED_DEBUG_HOST='"192.168.x.x"' ;; to disable net print + ;; more debug output + -DCORE_DEBUG_LEVEL=0 + -DNDEBUG + ;;${Speed_Flags.build_unflags} ;; to override -Os +;build_flags = ${esp32_4MB_V4_M_base.build_flags} +build_flags = ${esp32_4MB_V4_S_base.build_flags} + ${common_mm.build_disable_sync_interfaces} + -D WLED_RELEASE_NAME=esp32_16MB_M_eth_debug ; This will be included in the firmware.bin filename + -D SERVERNAME='"WLED-QEMU"' + ;;${Speed_Flags.build_flags_V4} ;; optimize for speed + -g3 -ggdb ;; better debug output + -DCORE_DEBUG_LEVEL=5 ;; max core debug output + -DDEBUG -D WLED_DEBUG -DWLED_DEBUG_JSON ;; -DWLED_DEBUG_FS ;; max WLED debugging output + -D WLED_DISABLE_BROWNOUT_DET -D WLED_WATCHDOG_TIMEOUT=0 + -D WLED_USE_ETHERNET + -D WLED_DISABLE_ESPNOW ;; ESP-NOW requires wifi, may crash with ethernet only + ;; -D WLED_DISABLE_OTA ;; should work + -D WLED_ENABLE_PIXELFORGE + ;; -D WLED_ENABLE_SIMPLE_UI ;; simple UI causes unexpected responses. + -D WLED_DISABLE_ADALIGHT ;; WLEDMM Better to disable serial protocols, to avoid crashes (see upstream #3128) + ;; CONFIG_ETH_USE_OPENETH=1 exposes the esp_eth_mac_new_openeth() declaration in esp_eth_mac.h. + ;; The actual implementation is provided by the vendored openeth_mac library (lib/openeth_mac/). + -D CONFIG_ETH_USE_OPENETH=1 + -D WLED_ETH_DEFAULT=2 ;; ESP32-POE board configuration (compatible with QEMU open_eth PHY address) + -D WLED_QEMU ;; Use OpenETH MAC driver for QEMU compatibility (esp_eth_mac_new_openeth) + ;; DHCP is handled by OpenETH driver - QEMU user-mode networking provides DHCP at 10.0.2.0/24 + ;; Static IP fallback (if needed, uncomment these lines): + ;-D WLED_STATIC_IP_DEFAULT_1=10 + ;-D WLED_STATIC_IP_DEFAULT_2=0 + ;-D WLED_STATIC_IP_DEFAULT_3=2 + ;-D WLED_STATIC_IP_DEFAULT_4=15 + -D MDNS_NAME=\"\" ;; disable MDNS + -D WLED_DISABLE_INFRARED + -D LEDPIN=4 + -D BTNPIN=-1 -D RLYPIN=-1 -D IRPIN=-1 ;; disable all extra pins + -D SR_DMTYPE=254 -D AUDIOPIN=-1 ;; set AR into "received only" mode +;lib_deps = ${esp32_4MB_V4_M_base.esp32_lib_deps} +lib_deps = ${esp32_4MB_V4_S_base.esp32_lib_deps} + openeth_mac ; provides esp_eth_mac_new_openeth() - not in pre-built Tasmota libethernet.a +lib_ignore = + IRremoteESP8266 ; use with WLED_DISABLE_INFRARED for faster compilation + OneWire ; used for USERMOD_FOUR_LINE_DISPLAY and USERMOD_DALLASTEMPERATURE + U8g2 ; used for USERMOD_FOUR_LINE_DISPLA + ${common_mm.HUB75_lib_ignore} +; RAM: [=== ] 26.5% (used 86924 bytes from 327680 bytes) +; Flash: [====== ] 57.4% (used 1806269 bytes from 3145728 bytes) + + ;; softhack007: my favourite HUB75 buildenv - fastest possible [env:esp32_4MB_V4_HUB75_forum] extends = esp32_4MB_V4_S_base diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000000..bf92eb6e1f --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,36 @@ +// @ts-check +const { defineConfig, devices } = require('@playwright/test'); + +/** + * Configuration for testing WLED running in QEMU or on real hardware + * The web UI requires the ESP32 backend, so we test against the actual firmware + * + * Set WLED_BASE_URL environment variable to point to QEMU or hardware: + * - QEMU: http://localhost:8080 (after running firmware in QEMU) + * - Hardware: http:// + * + * @see https://playwright.dev/docs/test-configuration + */ +module.exports = defineConfig({ + testDir: './e2e-tests', + fullyParallel: false, // Run tests sequentially to avoid overloading QEMU + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: 1, // Single worker to avoid race conditions with QEMU + reporter: 'html', + timeout: 45000, // 45 seconds per test (QEMU can be slow) + use: { + baseURL: process.env.WLED_BASE_URL || 'http://localhost:8080', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + navigationTimeout: 20000, // 20 seconds for navigation (QEMU startup) + actionTimeout: 15000, // 15 seconds for actions + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/sdkconfig.defaults.qemu b/sdkconfig.defaults.qemu new file mode 100644 index 0000000000..700605a949 --- /dev/null +++ b/sdkconfig.defaults.qemu @@ -0,0 +1,5 @@ +# QEMU-specific ESP-IDF configuration +# Enables OpenCores Ethernet MAC driver for QEMU's open_eth model + +CONFIG_ETH_USE_OPENETH=y +CONFIG_ETH_ENABLED=y diff --git a/wled00/src/dependencies/network/Network.cpp b/wled00/src/dependencies/network/Network.cpp index d86bf127fd..ce2e4fae1f 100644 --- a/wled00/src/dependencies/network/Network.cpp +++ b/wled00/src/dependencies/network/Network.cpp @@ -1,7 +1,22 @@ #include "Network.h" +#ifdef WLED_QEMU +#include "esp_system.h" +#include "tcpip_adapter.h" +#endif + IPAddress NetworkClass::localIP() { +#ifdef WLED_QEMU + // QEMU: Get IP directly from tcpip_adapter + tcpip_adapter_ip_info_t ip_info; + if (tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_ETH, &ip_info) == ESP_OK) { + if (ip_info.ip.addr != 0) { + return IPAddress(ip_info.ip.addr); + } + } + return INADDR_NONE; +#else IPAddress localIP; #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) localIP = ETH.localIP(); @@ -15,10 +30,20 @@ IPAddress NetworkClass::localIP() } return INADDR_NONE; +#endif } IPAddress NetworkClass::subnetMask() { +#ifdef WLED_QEMU + tcpip_adapter_ip_info_t ip_info; + if (tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_ETH, &ip_info) == ESP_OK) { + if (ip_info.netmask.addr != 0) { + return IPAddress(ip_info.netmask.addr); + } + } + return IPAddress(255, 255, 255, 0); +#else #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) if (ETH.localIP()[0] != 0) { return ETH.subnetMask(); @@ -28,10 +53,20 @@ IPAddress NetworkClass::subnetMask() return WiFi.subnetMask(); } return IPAddress(255, 255, 255, 0); +#endif } IPAddress NetworkClass::gatewayIP() { +#ifdef WLED_QEMU + tcpip_adapter_ip_info_t ip_info; + if (tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_ETH, &ip_info) == ESP_OK) { + if (ip_info.gw.addr != 0) { + return IPAddress(ip_info.gw.addr); + } + } + return INADDR_NONE; +#else #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) if (ETH.localIP()[0] != 0) { return ETH.gatewayIP(); @@ -41,6 +76,7 @@ IPAddress NetworkClass::gatewayIP() return WiFi.gatewayIP(); } return INADDR_NONE; +#endif } void NetworkClass::localMAC(uint8_t* MAC) @@ -73,19 +109,34 @@ void NetworkClass::localMAC(uint8_t* MAC) bool NetworkClass::isConnected() { +#ifdef WLED_QEMU + // QEMU: Check tcpip_adapter directly since ETH object is not initialized + tcpip_adapter_ip_info_t ip_info; + if (tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_ETH, &ip_info) == ESP_OK) { + if (ip_info.ip.addr != 0) { + return true; // We have an IP from QEMU networking + } + } + return false; +#else #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) return (WiFi.localIP()[0] != 0 && WiFi.status() == WL_CONNECTED) || ETH.localIP()[0] != 0; #else return (WiFi.localIP()[0] != 0 && WiFi.status() == WL_CONNECTED); #endif +#endif } bool NetworkClass::isEthernet() { +#ifdef WLED_QEMU + return true; // Always ethernet in QEMU mode +#else #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) return (ETH.localIP()[0] != 0); #endif return false; +#endif } NetworkClass Network; \ No newline at end of file diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 87063297dc..ad3a4df5b4 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -4,6 +4,16 @@ #include #ifdef ARDUINO_ARCH_ESP32 #include "esp_ota_ops.h" +#ifdef WLED_QEMU +// Includes for OpenETH driver (QEMU's emulated ethernet MAC) +#include "esp_eth.h" +#include "esp_eth_mac.h" +#include "esp_eth_phy.h" +#include "esp_eth_netif_glue.h" +#include "esp_netif.h" +#include "esp_event.h" +#include "tcpip_adapter.h" // This header is deprecated, please use new network related API in esp_netif.h +#endif #endif #warning WLED-MM is licensed under the EUPL-1.2. By installing WLED MM you implicitly accept the terms! @@ -12,7 +22,7 @@ #include "soc/rtc_cntl_reg.h" #endif -#if defined(WLED_DEBUG) && defined(ARDUINO_ARCH_ESP32) +#if defined(WLED_DEBUG) && defined(ARDUINO_ARCH_ESP32) && !defined(WLED_QEMU) #include "../tools/ESP32-Chip_info.hpp" #endif @@ -139,7 +149,9 @@ void WLED::loop() handleRemote(); #endif handleSerial(); +#ifndef WLED_QEMU handleImprovWifiScan(); +#endif #if defined(ARDUINO_ARCH_ESP32) && defined(WLEDMM_PROTECT_SERVICE) // WLEDMM experimental: handleNotifications() calls strip.show(); handleTransitions modifies segments if (!suspendStripService) { @@ -395,6 +407,7 @@ void WLED::loop() } #endif #endif +#ifndef WLED_QEMU DEBUG_PRINT(F("Wifi state: ")); DEBUG_PRINTLN(WiFi.status()); if (WiFi.status() != lastWifiState) { @@ -402,6 +415,7 @@ void WLED::loop() } lastWifiState = WiFi.status(); DEBUG_PRINT(F("State time: ")); DEBUG_PRINTLN(wifiStateChangedTime); +#endif DEBUG_PRINT(F("NTP last sync: ")); DEBUG_PRINTLN(ntpLastSyncTime); DEBUG_PRINT(F("Client IP: ")); DEBUG_PRINTLN(Network.localIP()); if (loops > 0) { // avoid division by zero @@ -640,7 +654,7 @@ void WLED::setup() #endif USER_PRINT(F(", speed ")); USER_PRINT(ESP.getFlashChipSpeed()/1000000);USER_PRINTLN(F("MHz.")); - #if defined(WLED_DEBUG) && defined(ARDUINO_ARCH_ESP32) + #if defined(WLED_DEBUG) && defined(ARDUINO_ARCH_ESP32) && !defined(WLED_QEMU) showRealSpeed(); #endif @@ -1105,6 +1119,7 @@ void WLED::initAP(bool resetAP) if (apBehavior == AP_BEHAVIOR_BUTTON_ONLY && !resetAP) return; +#if !defined(WLED_QEMU) // QEMU does not support wifi AP mode if (resetAP) { WLED_SET_AP_SSID(); strcpy_P(apPass, PSTR(WLED_AP_PASS)); @@ -1137,6 +1152,7 @@ void WLED::initAP(bool resetAP) dnsServer.start(53, "*", WiFi.softAPIP()); } apActive = true; +#endif // WLED_QEMU } bool WLED::initEthernet() @@ -1217,6 +1233,118 @@ bool WLED::initEthernet() } #endif + #ifdef WLED_QEMU + // QEMU: Use OpenCores Ethernet MAC (designed for QEMU's open_eth model) + // Do NOT call ETH.begin() — it uses the real ESP32 EMAC which crashes in QEMU + // Reference: https://github.com/mluis/qemu-esp32/issues/2 + // + // esp_eth_mac_new_openeth() is provided by the vendored openeth_mac library (lib/openeth_mac/). + // The -D CONFIG_ETH_USE_OPENETH=1 build flag enables the declaration in esp_eth_mac.h. + + USER_PRINTLN(F("initC: QEMU mode - initializing OpenETH MAC driver")); + + // 1. Ensure event loop exists (Arduino may not have created it without WiFi/ETH) + esp_err_t err = esp_event_loop_create_default(); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + USER_PRINTF("initC: QEMU - ERROR: esp_event_loop_create_default failed: %d\n", err); + return false; + } + + // 2. Initialize TCP/IP stack + tcpip_adapter_init(); + + // 3. Create the OpenCores MAC (designed for QEMU) + eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); + esp_eth_mac_t *mac = esp_eth_mac_new_openeth(&mac_config); + if (!mac) { + USER_PRINTLN(F("initC: QEMU - ERROR: esp_eth_mac_new_openeth() failed")); + USER_PRINTLN(F("initC: QEMU - CONFIG_ETH_USE_OPENETH may not be enabled in arduino-esp32")); + USER_PRINTLN(F("initC: QEMU - Check sdkconfig.defaults.qemu and rebuild ESP-IDF")); + return false; + } + + // 4. Create the DP83848 PHY (what QEMU emulates alongside open_eth) + eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); + phy_config.phy_addr = 1; // QEMU open_eth uses PHY address 1 + phy_config.reset_gpio_num = -1; // No GPIO reset in QEMU + esp_eth_phy_t *phy = esp_eth_phy_new_dp83848(&phy_config); + if (!phy) { + USER_PRINTLN(F("initC: QEMU - ERROR: esp_eth_phy_new_dp83848() failed")); + return false; + } + + // 5. Install the Ethernet driver + esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, phy); + esp_eth_handle_t eth_handle = NULL; + err = esp_eth_driver_install(ð_config, ð_handle); + if (err != ESP_OK) { + USER_PRINTF("initC: QEMU - ERROR: esp_eth_driver_install failed: %d\n", err); + return false; + } + + // 6. Attach to TCP/IP stack via netif glue + esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH(); + esp_netif_t *eth_netif = esp_netif_new(&cfg); + if (!eth_netif) { + USER_PRINTLN(F("initC: QEMU - ERROR: esp_netif_new failed")); + esp_eth_driver_uninstall(eth_handle); + return false; + } + + void *glue = esp_eth_new_netif_glue(eth_handle); + if (!glue) { + USER_PRINTLN(F("initC: QEMU - ERROR: esp_eth_new_netif_glue failed")); + esp_netif_destroy(eth_netif); + esp_eth_driver_uninstall(eth_handle); + return false; + } + + err = esp_netif_attach(eth_netif, glue); + if (err != ESP_OK) { + USER_PRINTF("initC: QEMU - ERROR: esp_netif_attach failed: %d\n", err); + esp_netif_destroy(eth_netif); + esp_eth_driver_uninstall(eth_handle); + return false; + } + + // 7. Register IP event handler so we know when DHCP succeeds + auto got_ip_handler = [](void*, esp_event_base_t, int32_t, void* event_data) { + ip_event_got_ip_t* event = (ip_event_got_ip_t*)event_data; + USER_PRINTF("initC: QEMU - Got IP via DHCP: " IPSTR "\n", IP2STR(&event->ip_info.ip)); + USER_PRINTF("initC: QEMU - Gateway: " IPSTR "\n", IP2STR(&event->ip_info.gw)); + USER_PRINTF("initC: QEMU - Netmask: " IPSTR "\n", IP2STR(&event->ip_info.netmask)); + }; + esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, got_ip_handler, NULL); + + // 8. Start Ethernet + err = esp_eth_start(eth_handle); + if (err != ESP_OK) { + USER_PRINTF("initC: QEMU - ERROR: esp_eth_start failed: %d\n", err); + esp_netif_destroy(eth_netif); + esp_eth_driver_uninstall(eth_handle); + return false; + } + + USER_PRINTLN(F("initC: QEMU - OpenETH driver started, waiting for DHCP...")); + + // Give DHCP some time to complete + delay(3000); + + // Check if we got an IP + tcpip_adapter_ip_info_t ip_info_check; + if (tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_ETH, &ip_info_check) == ESP_OK) { + if (ip_info_check.ip.addr != 0) { + USER_PRINTF("initC: QEMU - Confirmed IP: " IPSTR "\n", IP2STR(&ip_info_check.ip)); + } else { + USER_PRINTLN(F("initC: QEMU - WARNING: No IP assigned yet (DHCP may still be negotiating)")); + } + } + + successfullyConfiguredEthernet = true; + USER_PRINTLN(F("initC: *** QEMU OpenETH configured successfully! ***")); + return true; + + #else // !WLED_QEMU if (!ETH.begin( (uint8_t) es.eth_address, (int) es.eth_power, @@ -1234,6 +1362,7 @@ bool WLED::initEthernet() } successfullyConfiguredEthernet = true; + #endif USER_PRINTLN(F("initC: *** Ethernet successfully configured! ***")); // WLEDMM return true; #else @@ -1254,20 +1383,33 @@ void WLED::initConnection() //if (strip.isUpdating()) USER_PRINTLN("WLED::initConnection: strip still updating."); #endif + #ifdef WLED_ENABLE_WEBSOCKETS + ws.onEvent(wsEvent); + #endif + +#ifndef WLED_QEMU + // QEMU: Skip WiFi initialization - WiFi hardware not emulated + // The firmware crashes with LoadStorePIFAddrError when WiFi functions try to access hardware registers + WiFi.disconnect(true); // close old connections delay(5); // wait for hardware to be ready #ifdef ESP8266 WiFi.setPhyMode(force802_3g ? WIFI_PHY_MODE_11G : WIFI_PHY_MODE_11N); #endif +#endif +#ifndef WLED_QEMU if (staticIP[0] != 0 && staticGateway[0] != 0) { WiFi.config(staticIP, staticGateway, staticSubnet, IPAddress(1, 1, 1, 1)); } else { WiFi.config(IPAddress((uint32_t)0), IPAddress((uint32_t)0), IPAddress((uint32_t)0)); } +#endif lastReconnectAttempt = millis(); +#ifndef WLED_QEMU + if (!WLED_WIFI_CONFIGURED) { USER_PRINTLN(F("No WiFi connection configured.")); // WLEDMM if (!apActive) initAP(); // instantly go to ap mode @@ -1283,6 +1425,7 @@ void WLED::initConnection() } } showWelcomePage = false; +#endif USER_PRINT(F("Connecting to ")); USER_PRINT(clientSSID); @@ -1300,6 +1443,7 @@ void WLED::initConnection() WiFi.hostname(hostname); #endif +#ifndef WLED_QEMU WiFi.begin(clientSSID, clientPass); #ifdef ARDUINO_ARCH_ESP32 #if defined(LOLIN_WIFI_FIX) && (defined(ARDUINO_ARCH_ESP32C3) || defined(ARDUINO_ARCH_ESP32S2) || defined(ARDUINO_ARCH_ESP32S3)) @@ -1310,6 +1454,12 @@ void WLED::initConnection() #else wifi_set_sleep_type((noWifiSleep) ? NONE_SLEEP_T : MODEM_SLEEP_T); #endif +#else + // QEMU mode: Skip all WiFi initialization + DEBUG_PRINTLN(F("initConnection: QEMU mode - skipping WiFi initialization")); + USER_PRINTLN(F("initConnection: *** QEMU mode - WiFi disabled, using ethernet only ***")); + lastReconnectAttempt = millis(); +#endif } void WLED::initInterfaces() @@ -1496,6 +1646,7 @@ void WLED::handleConnection() #endif byte stac = 0; +#ifndef WLED_QEMU if (apActive) { #ifdef ESP8266 stac = wifi_softap_get_station_num(); @@ -1516,6 +1667,7 @@ void WLED::handleConnection() } } } +#endif // WLED_QEMU if (forceReconnect) { USER_PRINTLN(F("Forcing reconnect.")); initConnection(); @@ -1551,8 +1703,10 @@ void WLED::handleConnection() if (Network.isEthernet()) { #if ESP32 USER_PRINTLN(" via Ethernet (disabling WiFi)"); + #ifndef WLED_QEMU WiFi.disconnect(true); #endif + #endif } else { USER_PRINTLN(" via WiFi"); } @@ -1570,7 +1724,9 @@ void WLED::handleConnection() // shut down AP if (apBehavior != AP_BEHAVIOR_ALWAYS && apActive) { dnsServer.stop(); + #ifndef WLED_QEMU WiFi.softAPdisconnect(true); + #endif apActive = false; USER_PRINTLN(F("Access point disabled (handle).")); } diff --git a/wled00/wled.h b/wled00/wled.h index d2050e6ef2..de10ecc8fc 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -368,8 +368,16 @@ WLED_GLOBAL char cmDNS[33] _INIT(MDNS_NAME); // mDNS addre WLED_GLOBAL char apSSID[33] _INIT(""); // AP off by default (unless setup) WLED_GLOBAL byte apChannel _INIT(6); // 2.4GHz WiFi AP channel (1-13) WLED_GLOBAL byte apHide _INIT(0); // hidden AP SSID +#ifdef WLED_QEMU +WLED_GLOBAL byte apBehavior _INIT(AP_BEHAVIOR_BUTTON_ONLY); // access point opens when button0 pressed for at least 6 seconds +#else WLED_GLOBAL byte apBehavior _INIT(AP_BEHAVIOR_BOOT_NO_CONN); // access point opens when no connection after boot by default +#endif +#ifdef WLED_STATIC_IP_DEFAULT_1 +WLED_GLOBAL IPAddress staticIP _INIT_N((( WLED_STATIC_IP_DEFAULT_1, WLED_STATIC_IP_DEFAULT_2, WLED_STATIC_IP_DEFAULT_3, WLED_STATIC_IP_DEFAULT_4))); +#else WLED_GLOBAL IPAddress staticIP _INIT_N((( 0, 0, 0, 0))); // static IP of ESP +#endif WLED_GLOBAL IPAddress staticGateway _INIT_N((( 0, 0, 0, 0))); // gateway (router) IP WLED_GLOBAL IPAddress staticSubnet _INIT_N(((255, 255, 255, 0))); // most common subnet in home networks #if defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ESP32_PICO) && !defined(WLEDMM_WIFI_POWERON_HACK) @@ -935,7 +943,12 @@ WLED_GLOBAL volatile uint8_t jsonBufferLock _INIT(0); #endif #ifdef ARDUINO_ARCH_ESP32 + #ifdef WLED_QEMU + // QEMU: Use Network.isConnected() instead of ETH.localIP() since ETH object uses OpenETH driver + #define WLED_CONNECTED Network.isConnected() + #else #define WLED_CONNECTED (WiFi.status() == WL_CONNECTED || ETH.localIP()[0] != 0) + #endif #else #define WLED_CONNECTED (WiFi.status() == WL_CONNECTED) #endif