5.4 KiB
06 — existing ESP32 test patterns we copy
The DHT22 and HC-SR04 simulations have been proving the same WebSocket+QEMU pipeline that the camera will reuse. This note extracts the exact patterns so the camera tests don't reinvent anything.
File layout
| Layer purpose | Where to put it |
|---|---|
| Pure unit (Python, mocks the route) | test/test-esp32-cam/tests/*.py |
| Frontend integration (Vitest, mocks WS) | frontend/src/__tests__/esp32-*.test.ts |
| Live e2e (Node, real backend, real QEMU) | test/backend/e2e/test_*.mjs |
The live e2e tests in test/backend/e2e/ use plain Node with
fetch() + WebSocket (Node 20+). Both the DHT22 and HC-SR04 scripts
are ~250 lines and follow the same skeleton:
- POST
/api/compile/with the.inosource → getfirmware_b64. - Open
ws://localhost:8001/api/simulation/ws/<session_id>. - On
open, sendstart_esp32with{board, firmware_b64, sensors[]}. - Stream
serial_outputmessages, buffer until\n, scan for expected lines. - Send
esp32_sensor_updateto mutate state mid-run. - Exit 0 (pass) / 1 (fail) with a clear diagnostic block at the end.
This is the contract our live camera test has to satisfy. We won't
fight the pattern — we'll add camera_frame as a sibling of the
existing sensor messages.
The simulation WebSocket message types we'll touch
From backend/app/api/routes/simulation.py:90-260:
| Message type | Direction | What it does |
|---|---|---|
start_esp32 |
client → server | Boots QEMU with firmware |
stop_esp32 |
client → server | Tears QEMU down |
esp32_sensor_attach |
client → server | Generic sensor protocol register |
esp32_sensor_update |
client → server | Mutate sensor state |
esp32_sensor_detach |
client → server | Drop sensor |
esp32_uart{1,2}_input |
client → server | UART byte stream |
esp32_i2c_response |
client → server | I²C slave transaction reply |
serial_output |
server → client | Buffered UART out |
gpio_change |
server → client | Pin state transition |
system / error |
server → client | Lifecycle / faults |
For camera we'll add two new types — no need to overload the generic sensor channel:
esp32_camera_attach client → server {board, jpeg_quality, frame_size}
esp32_camera_frame client → server binary JPEG payload (or base64 in JSON)
esp32_camera_detach client → server ()
This keeps the camera as a first-class peripheral instead of pretending
it's a "sensor", which would imply pin semantics that don't apply.
Patterns to copy verbatim from test_dht22_simulation.mjs
- Session ID with timestamp:
test-camera-${Date.now()}so parallel runs don't collide on the backend's connection map. - Line buffer for
serial_output— chunks arrive partial:let _buf = ''; // … on serial_output: _buf += data?.data ?? ''; while ((nl = _buf.indexOf('\n')) !== -1) { const line = _buf.slice(0, nl).replace(/\r$/, ''); _buf = _buf.slice(nl + 1); // …match expected lines… } - ANSI-coloured logging helpers (
info,ok,err,serial,gpio) — copy the helpers, don't re-invent them. - Pass/fail decision at the end with concrete diagnostic guidance
("→ Check that
camera_framepropagates to backend ring buffer"). --timeout=Nand--backend=URLCLI flags so the same script runs in CI and locally.
Patterns to NOT copy
- The DHT22 script hard-codes the sensor on GPIO4 in two places. For the camera test we keep the pin map in one const block at the top so a wiring change is one edit.
- The DHT22 script exits 1 on partial pass with a "PARTIAL" label. The camera test doesn't have a "partial" state — either the firmware reports back the JPEG size correctly or it doesn't. Binary pass/fail.
Where each phase of the camera plan plugs in
Phase 1 (SCCB stub) → Python pytest layer (test_camera_websocket.py exists)
Phase 2 (I²S + DMA) → Python pytest layer + new C unit-tests inside QEMU
Phase 3 (frame injection) → New live e2e: test/backend/e2e/test_camera_simulation.mjs
Phase 4 (build) → Update third-party/qemu-lcgamboa/build_libqemu-esp32.sh
Phase 5 (frontend) → frontend/src/__tests__/esp32-camera-frame.test.ts
Phase 6 (backfill tests) → flip @expectedFailure off in tests we already wrote
The Phase-3 e2e is the one that proves end-to-end that the camera works. Pseudo-code of what it'll do:
// 1. compile camera_init.ino
// 2. ws.send(start_esp32 with board='esp32-cam')
// 3. ws.send(esp32_camera_attach)
// 4. ws.send(esp32_camera_frame with a 4x4 red square JPEG)
// 5. wait for serial line "got frame: <N> bytes 320x240 fmt=4"
// 6. ws.send another frame with different bytes
// 7. wait for the next serial line — bytes must differ
// 8. exit 0
That's the regression contract. If we ever break the frame transport, Phase-3 e2e fails with a precise message.