velxio/test/test-esp32-cam/tests/diag_sccb_probe.py

103 lines
3.3 KiB
Python
Raw Normal View History

feat: ESP32-CAM emulation with webcam frame bridge First open-source end-to-end emulation of the AI-Thinker ESP32-CAM in QEMU, paired with a browser webcam → firmware bridge so users can develop camera sketches without hardware. Status: esp_camera_init() returns ESP_OK; OV2640 chip-id verifies (PID/VER/MIDH/MIDL exactly match the datasheet); GPIO 25 VSYNC NEGEDGE interrupt enabled by the upstream driver. Final piece (cam_task accepting frames) is in progress — descriptor walker fix landed in this commit. Backend (Python/FastAPI): - simulation.py: camera_attach/frame/detach WS handlers - esp32_worker.py: ctypes binding to velxio_push_camera_frame + feature-detection fallback for older DLLs - esp32_lib_manager.py: forward camera commands to the worker stdin - esp-idf-template/main/CMakeLists.txt: esp32-camera headers added via add_prebuilt_library + REQUIRES driver (resolves i2c_master_* symbols). LED_BUILTIN=2 fallback for sketches that hardcode it. Frontend (React/TS): - EditorToolbar.tsx: ESP32-CAM (and the rest of the ESP32 family) added to isQemuBoard list — Run button now starts the QEMU bridge for these boards instead of falling through to the AVR path - useWebcamFrames.ts: getUserMedia → OffscreenCanvas → toBlob('image/jpeg') → base64 → WS at ~10 fps - CameraToggle.tsx: header button with status colors + frame counter - SimulatorCanvas.tsx: render CameraToggle for esp32-cam boards - Esp32Bridge.ts: sendCameraAttach/Frame/Detach + chunked btoa - useSimulatorStore.ts: diagnostic log on compileBoardProgram - components-metadata.json: regen including esp32-cam component Submodule pointer: - wokwi-libs/qemu-lcgamboa → ff8eee0 (camera devices commit on davidmonterocrespo24/qemu-lcgamboa branch picsimlab-esp32) Investigation + tests in test/test-esp32-cam/: - 13 autosearch markdown docs (overview, SOTA, OV2640 spec, DVP/I2S spec, build blueprint, blockers resolved, descriptor walker fix) - 5 sketches (camera_init, sccb_probe, dma_smoke, frame_roundtrip, webcam_demo) + 8 live + WS regression tests - README with the user-facing flow .gitignore: - libqemu-*.dll.{pre-camera,new,bak} (rollback points, regenerated) - wokwi-libs/esp32-camera/ (clone consumed by arduino-esp32 path, not part of this repo) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 05:28:55 +07:00
"""
Diagnostic runs sccb_probe.ino against the live backend and dumps
every serial line (no assertions, no xfail). Use to see what the
firmware actually printed when the formal test reports XFAIL.
Usage:
VELXIO_BACKEND_URL=http://127.0.0.1:8001 \
python test/test-esp32-cam/tests/diag_sccb_probe.py
"""
from __future__ import annotations
import asyncio
import json
import os
import pathlib
import sys
from urllib.parse import urlparse
THIS = pathlib.Path(__file__).resolve()
SKETCH = THIS.parent.parent / "sketches" / "sccb_probe" / "sccb_probe.ino"
def base() -> str:
u = os.environ.get("VELXIO_BACKEND_URL", "http://127.0.0.1:8001")
return u.rstrip("/")
def ws_url(client_id: str) -> str:
u = urlparse(base())
scheme = "wss" if u.scheme == "https" else "ws"
host = u.hostname or "localhost"
port = f":{u.port}" if u.port else ""
return f"{scheme}://{host}{port}/api/simulation/ws/{client_id}"
async def main():
import httpx
import websockets
sketch = SKETCH.read_text(encoding="utf-8")
print(f"[diag] compiling {SKETCH.name}", flush=True)
async with httpx.AsyncClient(base_url=base(), timeout=300.0) as http:
res = await http.post(
"/api/compile/",
json={
"files": [{"name": SKETCH.name, "content": sketch}],
"board_fqbn": "esp32:esp32:esp32cam",
},
)
body = res.json()
if not body.get("success"):
print(f"[diag] compile FAILED: "
f"{(body.get('error') or body.get('stderr', ''))[:600]}",
flush=True)
return 1
fw = body.get("binary_content") or body.get("firmware_b64")
print(f"[diag] firmware {len(fw)//1024} KB", flush=True)
cid = f"diag-sccb-{int(asyncio.get_event_loop().time()*1000)}"
print(f"[diag] WS connect {ws_url(cid)}", flush=True)
async with websockets.connect(ws_url(cid), ping_interval=None,
max_size=4 * 1024 * 1024) as ws:
await ws.send(json.dumps({
"type": "start_esp32",
"data": {"board": "esp32-cam", "firmware_b64": fw},
}))
deadline = asyncio.get_event_loop().time() + 60.0
try:
while asyncio.get_event_loop().time() < deadline:
rem = deadline - asyncio.get_event_loop().time()
if rem <= 0:
break
raw = await asyncio.wait_for(ws.recv(), timeout=rem)
try:
m = json.loads(raw)
except json.JSONDecodeError:
continue
t = m.get("type")
d = m.get("data", {}) or {}
if t == "serial_output":
txt = d.get("data", "")
sys.stdout.write(txt)
sys.stdout.flush()
elif t == "system":
print(f"\n[diag] system: {d}", flush=True)
elif t == "error":
print(f"\n[diag] ERROR: {d}", flush=True)
except asyncio.TimeoutError:
pass
finally:
try:
await ws.send(json.dumps({"type": "stop_esp32", "data": {}}))
except Exception:
pass
print("\n[diag] done", flush=True)
return 0
if __name__ == "__main__":
sys.exit(asyncio.run(main()))