151 lines
5.0 KiB
Python
151 lines
5.0 KiB
Python
"""ESP32 + SPI chip round-trip via the backend WASM runtime.
|
|
|
|
Sketch wires GPIOs to a 74HC595 chip running in the backend:
|
|
- SPI.transfer(0xA5) → _on_spi_event → chip's vx_spi_start buffer fills
|
|
with 0xA5 → chip's on_done stores it in shift_reg
|
|
- Sketch pulses RCLK (a GPIO output via qemu_picsimlab_set_pin) → chip's
|
|
vx_pin_watch on RCLK fires (synchronously, in QEMU thread) → chip
|
|
latches shift_reg to Q0..Q7 via vx_pin_write → those calls hit
|
|
qemu_picsimlab_set_pin → ESP32's GPIO inputs read 0xA5 LSB-first.
|
|
|
|
This is the strictest end-to-end test we have: it exercises SPI transfer +
|
|
pin_watch (edge detection) + GPIO output from chip → real GPIO IN read by
|
|
firmware. Every link of the chain runs synchronously inside the worker.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import pathlib
|
|
|
|
import pytest
|
|
import websockets
|
|
|
|
from .conftest import REPO_ROOT, WS_URL
|
|
|
|
|
|
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"
|
|
|
|
pytestmark = pytest.mark.skipif(SKIP_REASON is not None, reason=SKIP_REASON or "")
|
|
|
|
|
|
_SKETCH_PATH = (
|
|
REPO_ROOT / "test" / "test_custom_chips_boards"
|
|
/ "sketches" / "esp32_spi_chip_demo" / "esp32_spi_chip_demo.ino"
|
|
)
|
|
_CHIP_C_PATH = (
|
|
REPO_ROOT / "test" / "test_custom_chips" / "sdk" / "examples" / "sn74hc595.c"
|
|
)
|
|
|
|
|
|
async def _wait_for_serial_text(ws, needle: str, *, timeout: float = 60.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_drives_74hc595_chip_via_spi(http):
|
|
chip_c = _CHIP_C_PATH.read_text(encoding="utf-8")
|
|
sketch_ino = _SKETCH_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"], chip_data
|
|
wasm_b64 = chip_data["wasm_base64"]
|
|
|
|
res = await http.post(
|
|
"/api/compile/",
|
|
json={
|
|
"files": [{"name": "esp32_spi_chip_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: {sketch_data.get('stderr', '')[:600]}")
|
|
firmware_b64 = sketch_data["binary_content"]
|
|
|
|
pin_map = {
|
|
"SER": 23,
|
|
"SRCLK": 18,
|
|
"RCLK": 5,
|
|
"SRCLR": 22,
|
|
"OE": 21,
|
|
"QH": 19,
|
|
"Q0": 13,
|
|
"Q1": 14,
|
|
"Q2": 15,
|
|
"Q3": 16,
|
|
"Q4": 17,
|
|
"Q5": 25,
|
|
"Q6": 26,
|
|
"Q7": 27,
|
|
}
|
|
|
|
ws_url = f"{WS_URL}/api/simulation/ws/test-custom-chip-esp32-spi"
|
|
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,
|
|
"wasm_b64": wasm_b64,
|
|
"attrs": {},
|
|
"pin_map": pin_map,
|
|
},
|
|
],
|
|
},
|
|
}))
|
|
|
|
await _wait_for_serial_text(ws, "READY", timeout=60.0)
|
|
full = await _wait_for_serial_text(ws, "Q=", timeout=30.0)
|
|
|
|
# Find the Q=... line and extract its 8 bits.
|
|
for line in full.splitlines():
|
|
if line.startswith("Q="):
|
|
q_pattern = line[2:].strip()
|
|
break
|
|
else:
|
|
pytest.fail(f"no Q= line in serial output: {full!r}")
|
|
|
|
# 0xA5 = 10100101 LSB-first → Q0=1,Q1=0,Q2=1,Q3=0,Q4=0,Q5=1,Q6=0,Q7=1
|
|
# Sketch prints them in pin order (Q0 first), so the pattern is "10100101".
|
|
assert q_pattern == "10100101", (
|
|
f"chip should latch 0xA5 LSB-first to Q0..Q7; expected '10100101', got {q_pattern!r}\n"
|
|
f"full serial: {full!r}"
|
|
)
|
|
|
|
await ws.send(json.dumps({"type": "stop_esp32"}))
|