velxio/frontend/src/components/simulator/CameraToggle.tsx

99 lines
2.9 KiB
TypeScript
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
/**
* Camera button rendered on the canvas header for ESP32-CAM boards.
*
* Click → asks for webcam permission → starts streaming JPEG frames at
* 10 fps over the existing simulator WebSocket. The frames flow into
* the QEMU OV2640+I²S peripheral so user sketches that call
* `esp_camera_fb_get()` see real webcam content (subject to the
* upstream-driver caveats documented in test/test-esp32-cam/autosearch/10).
*
* Visual states:
* idle grey camera icon
* requesting orange (pulsing) — browser permission prompt visible
* streaming green + frame counter — frames flowing
* denied red, click-to-retry
* error red, hover for message
*/
import React from 'react';
import { useWebcamFrames } from '../../hooks/useWebcamFrames';
interface CameraToggleProps {
boardId: string | null;
}
export const CameraToggle: React.FC<CameraToggleProps> = ({ boardId }) => {
const { status, errorMessage, framesSent, lastFrameBytes, start, stop } =
useWebcamFrames();
const handleClick = () => {
if (!boardId) return;
if (status === 'streaming') {
stop();
} else {
void start(boardId);
}
};
const isOn = status === 'streaming';
const tooltip =
status === 'streaming'
? `Streaming webcam (${framesSent} frames, last=${(lastFrameBytes / 1024).toFixed(1)} KB) — click to stop`
: status === 'requesting'
? 'Asking for camera permission…'
: status === 'denied'
? `Permission denied. Click to retry.`
: status === 'error'
? `Webcam error: ${errorMessage ?? 'unknown'}`
: 'Use webcam as ESP32-CAM camera';
const color =
status === 'streaming'
? '#3fb950'
: status === 'requesting'
? '#f0883e'
: status === 'denied' || status === 'error'
? '#f85149'
: '#ccc';
return (
<button
onClick={handleClick}
disabled={!boardId || status === 'requesting'}
title={tooltip}
style={{
background: isOn ? 'rgba(63,185,80,0.15)' : 'transparent',
border: `1px solid ${isOn ? '#3fb950' : '#555'}`,
borderRadius: 4,
padding: '4px 10px',
display: 'flex',
alignItems: 'center',
gap: 6,
color,
fontSize: 13,
cursor: boardId ? 'pointer' : 'not-allowed',
animation:
status === 'requesting'
? 'velxio-pulse 1s ease-in-out infinite'
: 'none',
}}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
<circle cx="12" cy="13" r="4" />
</svg>
<span>
{isOn ? `Camera ${framesSent}` : 'Camera'}
</span>
</button>
);
};