148 lines
6.1 KiB
Python
148 lines
6.1 KiB
Python
"""ESP32 + Custom Chip I2C round-trip — the full architecture validated end-to-end.
|
|
|
|
This is the test that proves the WASM-in-backend approach works:
|
|
|
|
- Compile the user's chip C source via /api/compile-chip → wasm_b64.
|
|
- Compile an Arduino sketch (Wire.h) via /api/compile/ → firmware_b64.
|
|
- Send both to the backend in a single `start_esp32` payload.
|
|
- The backend's worker subprocess loads the chip's WASM in-process,
|
|
registers it as an I2C slave (synchronous to QEMU), and boots the firmware.
|
|
- The sketch's Wire.beginTransmission/endTransmission/requestFrom calls all
|
|
end up calling the chip's WASM I2C callbacks SYNCHRONOUSLY — no WebSocket
|
|
round-trip, no race condition.
|
|
- The four bytes (0xAA..0xDD) flow back to the sketch via Serial output,
|
|
proving the chain works end-to-end.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import base64
|
|
import json
|
|
import os
|
|
import pathlib
|
|
|
|
import pytest
|
|
import websockets
|
|
|
|
from .conftest import REPO_ROOT, BACKEND_URL, WS_URL
|
|
|
|
|
|
# Pre-flight: ESP32 backend must be available (lcgamboa libqemu-xtensa).
|
|
def _esp32_available() -> bool:
|
|
services_dir = REPO_ROOT / "backend" / "app" / "services"
|
|
for name in ("libqemu-xtensa.dll", "libqemu-xtensa.so"):
|
|
if (services_dir / name).is_file():
|
|
return True
|
|
if os.environ.get("QEMU_ESP32_LIB") and pathlib.Path(os.environ["QEMU_ESP32_LIB"]).is_file():
|
|
return True
|
|
return False
|
|
|
|
|
|
SKIP_REASON: str | None = None
|
|
if not _esp32_available():
|
|
SKIP_REASON = (
|
|
"ESP32 backend toolchain unavailable (libqemu-xtensa not in backend/app/services/). "
|
|
"See test/test_custom_chips_boards/test_esp32_gpio_bridge.py docstring for setup."
|
|
)
|
|
|
|
pytestmark = pytest.mark.skipif(SKIP_REASON is not None, reason=SKIP_REASON or "")
|
|
|
|
|
|
_SKETCH_PATH = (
|
|
REPO_ROOT
|
|
/ "test" / "test_custom_chips_boards" / "sketches" / "esp32_eeprom_demo"
|
|
/ "esp32_eeprom_demo.ino"
|
|
)
|
|
_CHIP_C_PATH = (
|
|
REPO_ROOT / "test" / "test_custom_chips" / "sdk" / "examples" / "eeprom-24c01.c"
|
|
)
|
|
|
|
|
|
async def _wait_for_serial_text(ws, needle: str, *, timeout: float = 30.0) -> str:
|
|
buf = ""
|
|
deadline = asyncio.get_event_loop().time() + timeout
|
|
while needle not in buf:
|
|
remaining = deadline - asyncio.get_event_loop().time()
|
|
if remaining <= 0:
|
|
raise asyncio.TimeoutError(f"serial never contained {needle!r}; got {buf!r}")
|
|
raw = await asyncio.wait_for(ws.recv(), timeout=remaining)
|
|
try:
|
|
msg = json.loads(raw)
|
|
except Exception:
|
|
continue
|
|
if msg.get("type") == "serial_output":
|
|
buf += str(msg.get("data", {}).get("data", ""))
|
|
return buf
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_esp32_sketch_talks_to_wasm_eeprom_chip(http):
|
|
"""Full round-trip: ESP32 sketch ↔ user-supplied 24C01 chip running in the QEMU worker."""
|
|
|
|
# ── 1. Compile the chip WASM ───────────────────────────────────────────────
|
|
chip_c = _CHIP_C_PATH.read_text(encoding="utf-8")
|
|
res = await http.post("/api/compile-chip/", json={"source": chip_c}, timeout=120.0)
|
|
assert res.status_code == 200, res.text
|
|
chip_data = res.json()
|
|
assert chip_data["success"], f"chip compile failed: {chip_data.get('stderr', '')}"
|
|
wasm_b64 = chip_data["wasm_base64"]
|
|
assert wasm_b64, "no wasm_base64 in chip compile response"
|
|
|
|
# ── 2. Compile the ESP32 sketch ────────────────────────────────────────────
|
|
sketch_ino = _SKETCH_PATH.read_text(encoding="utf-8")
|
|
res = await http.post(
|
|
"/api/compile/",
|
|
json={
|
|
"files": [{"name": "esp32_eeprom_demo.ino", "content": sketch_ino}],
|
|
"board_fqbn": "esp32:esp32:esp32",
|
|
},
|
|
timeout=300.0,
|
|
)
|
|
assert res.status_code == 200, res.text
|
|
sketch_data = res.json()
|
|
if not sketch_data.get("success"):
|
|
pytest.skip(
|
|
f"ESP32 sketch compile failed on this backend: "
|
|
f"{sketch_data.get('error')}\n{sketch_data.get('stderr', '')[:600]}"
|
|
)
|
|
firmware_b64 = sketch_data.get("binary_content")
|
|
assert firmware_b64, f"sketch built but no binary_content; full response: {sketch_data}"
|
|
|
|
# ── 3. Boot ESP32 with the chip attached as a custom-chip sensor ────────────
|
|
ws_url = f"{WS_URL}/api/simulation/ws/test-custom-chip-esp32-i2c"
|
|
|
|
async with websockets.connect(ws_url, max_size=16 * 1024 * 1024) as ws:
|
|
await ws.send(json.dumps({
|
|
"type": "start_esp32",
|
|
"data": {
|
|
"board": "esp32",
|
|
"firmware_b64": firmware_b64,
|
|
"sensors": [
|
|
{
|
|
"sensor_type": "custom-chip",
|
|
"pin": 0, # virtual / unused
|
|
"wasm_b64": wasm_b64,
|
|
"attrs": {},
|
|
},
|
|
],
|
|
},
|
|
}))
|
|
|
|
# ── 4. Wait for sketch boot banner ─────────────────────────────────────
|
|
await _wait_for_serial_text(ws, "READY", timeout=60.0)
|
|
|
|
# ── 5. Wait for DONE marker (after 4 BYTE=… lines) ─────────────────────
|
|
full_serial = await _wait_for_serial_text(ws, "DONE", timeout=30.0)
|
|
|
|
# ── 6. Assert the four bytes appear in the right order ─────────────────
|
|
# Sketch prints "BYTE=170" / "BYTE=187" / "BYTE=204" / "BYTE=221" sequentially.
|
|
for expected in ("BYTE=170", "BYTE=187", "BYTE=204", "BYTE=221"):
|
|
assert expected in full_serial, (
|
|
f"missing {expected} in serial output; got: {full_serial!r}"
|
|
)
|
|
# And in order
|
|
idx = [full_serial.index(s) for s in ("BYTE=170", "BYTE=187", "BYTE=204", "BYTE=221")]
|
|
assert idx == sorted(idx), f"bytes out of order: indices {idx}"
|
|
|
|
await ws.send(json.dumps({"type": "stop_esp32"}))
|