183 lines
5.7 KiB
Python
183 lines
5.7 KiB
Python
"""
|
|
Spawn esp32_worker.py directly (bypassing the WS+uvicorn stack) so we can
|
|
SEE the QEMU stderr where our fprintf debug lines from esp32_i2s_cam.c go.
|
|
|
|
Usage:
|
|
python test/test-esp32-cam/tests/debug_worker_direct.py
|
|
|
|
Compiles webcam_demo.ino, launches the worker, pushes a JPEG every 100 ms,
|
|
prints worker stderr + stdout to terminal in real time. Kill with Ctrl+C.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import time
|
|
|
|
THIS_DIR = pathlib.Path(__file__).resolve().parent
|
|
TEST_ROOT = THIS_DIR.parent
|
|
REPO_ROOT = TEST_ROOT.parent.parent
|
|
SKETCH = TEST_ROOT / "sketches" / "webcam_demo" / "webcam_demo.ino"
|
|
WORKER_SCRIPT = REPO_ROOT / "backend" / "app" / "services" / "esp32_worker.py"
|
|
LIB_XTENSA = REPO_ROOT / "backend" / "app" / "services" / "libqemu-xtensa.dll"
|
|
|
|
sys.path.insert(0, str(THIS_DIR))
|
|
|
|
|
|
def compile_sketch() -> str:
|
|
"""POST sketch to /api/compile/ and return base64-encoded firmware.
|
|
Caches the result in /tmp/webcam_demo_fw.b64 keyed by sketch SHA256."""
|
|
import hashlib
|
|
import httpx
|
|
sketch = SKETCH.read_text(encoding="utf-8")
|
|
h = hashlib.sha256(sketch.encode()).hexdigest()[:16]
|
|
cache = pathlib.Path(os.environ.get("TEMP", "C:/temp")) / f"webcam_demo_fw_{h}.b64"
|
|
cache.parent.mkdir(parents=True, exist_ok=True)
|
|
if cache.exists():
|
|
fw = cache.read_text().strip()
|
|
print(f"[compile] cache HIT ({h}): {len(fw)} chars b64")
|
|
return fw
|
|
print(f"[compile] cache MISS ({h}), compiling via /api/compile/...")
|
|
r = httpx.post(
|
|
"http://localhost:8001/api/compile/",
|
|
json={
|
|
"files": [{"name": "webcam_demo.ino", "content": sketch}],
|
|
"board_fqbn": "esp32:esp32:esp32cam",
|
|
},
|
|
timeout=300.0,
|
|
)
|
|
r.raise_for_status()
|
|
body = r.json()
|
|
if not body.get("success"):
|
|
raise RuntimeError(f"compile failed: {body.get('error', body)[:600]}")
|
|
fw = body.get("binary_content") or body.get("firmware_b64")
|
|
if not fw:
|
|
raise RuntimeError("compile OK but no firmware")
|
|
cache.write_text(fw)
|
|
print(f"[compile] OK, firmware: {len(fw)} chars b64 "
|
|
f"(~{len(fw) * 3 // 4 // 1024} KiB raw), cached to {cache}")
|
|
return fw
|
|
|
|
|
|
def main() -> int:
|
|
print(f"[paths] worker={WORKER_SCRIPT}")
|
|
print(f"[paths] dll={LIB_XTENSA} exists={LIB_XTENSA.exists()}")
|
|
print(f"[paths] sketch={SKETCH} exists={SKETCH.exists()}")
|
|
|
|
fw = compile_sketch()
|
|
|
|
from webcam_helper import get_test_jpeg
|
|
jpeg, src = get_test_jpeg()
|
|
print(f"[jpeg] source={src}, {len(jpeg)} bytes, head={jpeg[:8].hex()}")
|
|
jpeg_b64 = base64.b64encode(jpeg).decode("ascii")
|
|
|
|
config = {
|
|
"lib_path": str(LIB_XTENSA),
|
|
"firmware_b64": fw,
|
|
"machine": "esp32-picsimlab",
|
|
"sensors": [],
|
|
"wifi_enabled": False,
|
|
}
|
|
|
|
print(f"[spawn] launching worker via {sys.executable}")
|
|
proc = subprocess.Popen(
|
|
[sys.executable, str(WORKER_SCRIPT)],
|
|
stdin=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
|
|
# Send config
|
|
proc.stdin.write((json.dumps(config) + "\n").encode())
|
|
proc.stdin.flush()
|
|
print("[spawn] config sent")
|
|
|
|
# Threads to stream stderr + stdout
|
|
stop = threading.Event()
|
|
|
|
def stream_stderr():
|
|
for line in proc.stderr:
|
|
if stop.is_set():
|
|
return
|
|
sys.stderr.write("[STDERR] " + line.decode(errors="replace"))
|
|
sys.stderr.flush()
|
|
|
|
def stream_stdout():
|
|
line_buf = bytearray()
|
|
if proc.stdout is None:
|
|
return
|
|
for line in proc.stdout:
|
|
if stop.is_set():
|
|
return
|
|
try:
|
|
msg = json.loads(line)
|
|
except Exception:
|
|
continue
|
|
t = msg.get("type", "?")
|
|
if t == "uart_tx" and msg.get("uart") == 0:
|
|
b = msg.get("byte", 0)
|
|
line_buf.append(b)
|
|
if b == ord("\n") or len(line_buf) >= 200:
|
|
text = line_buf.decode(errors="replace").rstrip()
|
|
line_buf.clear()
|
|
if text:
|
|
sys.stdout.write(f"[SERIAL] {text}\n")
|
|
sys.stdout.flush()
|
|
elif t == "system":
|
|
sys.stdout.write(f"[SYSTEM] {msg}\n"); sys.stdout.flush()
|
|
|
|
t1 = threading.Thread(target=stream_stderr, daemon=True)
|
|
t2 = threading.Thread(target=stream_stdout, daemon=True)
|
|
t1.start(); t2.start()
|
|
|
|
# Wait for boot
|
|
time.sleep(6.0)
|
|
|
|
# Attach + push frames
|
|
print("[push] sending camera_attach")
|
|
proc.stdin.write((json.dumps({"cmd": "camera_attach"}) + "\n").encode())
|
|
proc.stdin.flush()
|
|
|
|
print("[push] starting frame push loop (10 fps)")
|
|
push_count = 0
|
|
try:
|
|
while True:
|
|
time.sleep(0.1)
|
|
if proc.stdin is None:
|
|
break
|
|
proc.stdin.write((json.dumps({
|
|
"cmd": "camera_frame",
|
|
"b64": jpeg_b64,
|
|
"fmt": "jpeg",
|
|
"w": 320,
|
|
"h": 240,
|
|
}) + "\n").encode())
|
|
proc.stdin.flush()
|
|
push_count += 1
|
|
if push_count == 1 or push_count % 50 == 0:
|
|
print(f"[push] {push_count} frames pushed")
|
|
if push_count > 300:
|
|
print("[push] hit 300 frames, stopping")
|
|
break
|
|
except KeyboardInterrupt:
|
|
print("\n[main] Ctrl+C received")
|
|
finally:
|
|
stop.set()
|
|
try:
|
|
proc.terminate()
|
|
proc.wait(timeout=3)
|
|
except Exception:
|
|
proc.kill()
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|