feat(gallery): add 2 ESP32-CAM examples to the in-app gallery

Adds two listed examples to the gallery (book icon → Examples) so
users can one-click load the new ESP32-CAM emulation:

1. ESP32-CAM: Webcam Demo (sensors / beginner)
   Minimal sketch — init OV2640, verify SCCB chip-id, loop on
   esp_camera_fb_get() printing frame metadata to Serial. Proves
   the emulation is alive without any external components.

2. ESP32-CAM + ILI9341 Live Preview (displays / intermediate)
   Full demo — decode JPEG with jpg2rgb565() (built-in to
   esp32-camera/conversions, header exposed by the Velxio compile
   template) and render the resulting RGB565 bitmap to a 320×240
   SPI TFT. Pre-wired diagram: ILI9341 connected via VSPI to GPIOs
   12-15 (the only block free after OV2640 takes over the rest of
   the AI-Thinker pins).

Type changes:
- ExampleProject.boardType union extended with 'esp32-cam'
- BOARD_TABS in ExamplesGallery.tsx gets a new "ESP32-CAM" tab
  (orange #d35400)

Both examples use boardFilter: 'esp32-cam' so they show under
the new tab and not the generic ESP32 one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Montero Crespo 2026-05-02 22:21:19 -03:00
parent 20bda8e427
commit 155b962c21
2 changed files with 338 additions and 1 deletions

View File

@ -30,6 +30,7 @@ const BOARD_TABS: BoardTab[] = [
{ id: 'raspberry-pi-pico', label: 'Pico', color: '#ffffff', bg: '#c11c31' },
{ id: 'pi-pico-w', label: 'Pico W (Wi-Fi)', color: '#ffffff', bg: '#8c0e1e' },
{ id: 'esp32', label: 'ESP32 (Xtensa)', color: '#ffffff', bg: '#e77d11' },
{ id: 'esp32-cam', label: 'ESP32-CAM', color: '#ffffff', bg: '#d35400' },
{ id: 'esp32-c3', label: 'ESP32-C3 (RISC-V)', color: '#ffffff', bg: '#27ae60' },
{ id: 'attiny85', label: 'ATtiny85', color: '#ffffff', bg: '#5d4037' },
{ id: 'multi', label: 'Multi-Board', color: '#ffffff', bg: '#7b2d8b' },

View File

@ -36,7 +36,8 @@ export interface ExampleProject {
| 'raspberry-pi-pico'
| 'pi-pico-w'
| 'esp32'
| 'esp32-c3';
| 'esp32-c3'
| 'esp32-cam';
/** Board filter key used in the gallery board selector. Derived from boardType if omitted. */
boardFilter?: string;
/**
@ -6870,6 +6871,341 @@ void loop() {
},
],
},
// ── ESP32-CAM examples ─────────────────────────────────────────────────────
// Demos the QEMU-emulated OV2640 + I²S camera path. The user's webcam
// (browser getUserMedia) feeds the firmware's esp_camera_fb_get() through
// the simulator's velxio_push_camera_frame ctypes binding. See
// test/test-esp32-cam/autosearch/14_complete_emulation.md for the
// forensic trace of the 9 silent bugs that had to be fixed to make this
// work. Both examples assume the user clicks the "Camera" button in the
// canvas header and grants webcam permission.
{
id: 'esp32cam-webcam-demo',
title: 'ESP32-CAM: Webcam Demo',
description:
'Init the OV2640 camera, verify chip-id over SCCB, then loop on esp_camera_fb_get() and print frame metadata to Serial. The simplest "is the emulation alive" sketch — click Camera in the canvas header to start streaming your webcam.',
category: 'sensors',
difficulty: 'beginner',
boardType: 'esp32-cam',
boardFilter: 'esp32-cam',
code: `// Velxio ESP32-CAM webcam demo — minimal
// Click the "Camera" button in the canvas header to start streaming
// your webcam frames into the emulated OV2640.
#include "esp_camera.h"
// AI-Thinker ESP32-CAM pinout
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("=== Velxio ESP32-CAM webcam demo ===");
camera_config_t cfg = {};
cfg.ledc_channel = LEDC_CHANNEL_0;
cfg.ledc_timer = LEDC_TIMER_0;
cfg.pin_d0 = Y2_GPIO_NUM; cfg.pin_d1 = Y3_GPIO_NUM;
cfg.pin_d2 = Y4_GPIO_NUM; cfg.pin_d3 = Y5_GPIO_NUM;
cfg.pin_d4 = Y6_GPIO_NUM; cfg.pin_d5 = Y7_GPIO_NUM;
cfg.pin_d6 = Y8_GPIO_NUM; cfg.pin_d7 = Y9_GPIO_NUM;
cfg.pin_xclk = XCLK_GPIO_NUM;
cfg.pin_pclk = PCLK_GPIO_NUM;
cfg.pin_vsync = VSYNC_GPIO_NUM;
cfg.pin_href = HREF_GPIO_NUM;
cfg.pin_sccb_sda = SIOD_GPIO_NUM;
cfg.pin_sccb_scl = SIOC_GPIO_NUM;
cfg.pin_pwdn = PWDN_GPIO_NUM;
cfg.pin_reset = RESET_GPIO_NUM;
cfg.xclk_freq_hz = 20000000;
cfg.pixel_format = PIXFORMAT_JPEG;
cfg.frame_size = FRAMESIZE_QVGA;
cfg.jpeg_quality = 12;
cfg.fb_count = 1;
cfg.fb_location = CAMERA_FB_IN_DRAM;
esp_err_t err = esp_camera_init(&cfg);
if (err != ESP_OK) {
Serial.printf("camera_init FAIL: 0x%x\\n", err);
while (1) delay(1000);
}
Serial.println("camera_init OK");
sensor_t* s = esp_camera_sensor_get();
if (s) {
Serial.printf("OV2640 PID=0x%02X VER=0x%02X MIDH=0x%02X MIDL=0x%02X\\n",
s->id.PID, s->id.VER, s->id.MIDH, s->id.MIDL);
}
Serial.println("Click 'Camera' in the canvas header to start streaming.");
}
void loop() {
static uint32_t n = 0;
camera_fb_t* fb = esp_camera_fb_get();
if (!fb) {
static uint32_t nulls = 0;
if (++nulls % 20 == 0) Serial.printf("waiting... (%u nulls)\\n", nulls);
delay(200);
return;
}
n++;
Serial.printf("frame %u: %u bytes %ux%u fmt=%d head=%02X %02X %02X %02X\\n",
n, fb->len, fb->width, fb->height, fb->format,
fb->buf[0], fb->buf[1], fb->buf[2], fb->buf[3]);
esp_camera_fb_return(fb);
delay(100);
}
`,
components: [],
wires: [],
},
{
id: 'esp32cam-lcd-preview',
title: 'ESP32-CAM + ILI9341 Live Preview',
description:
'Webcam frames decoded in-place with jpg2rgb565() and rendered to a 320×240 SPI TFT (160×120 centered, 1/2 scale). Status bar shows fps, frame counter, decode-fail counter and a live pulse. Requires Adafruit GFX + Adafruit ILI9341 libraries.',
category: 'displays',
difficulty: 'intermediate',
boardType: 'esp32-cam',
boardFilter: 'esp32-cam',
code: `// ESP32-CAM live webcam preview on ILI9341 320×240 TFT
// Click "Camera" in the canvas header → grant permission → see your
// face on the TFT.
//
// Wiring (already done by this example's diagram):
// ILI9341 ↔ ESP32-CAM
// CS → 15 RST → 2 D/C → 14
// MOSI→ 13 SCK → 12 MISO unused
#include "esp_camera.h"
#include "img_converters.h" // jpg2rgb565 — built into esp32-camera
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
// Camera pins (AI-Thinker)
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
// TFT pins
#define TFT_CS 15
#define TFT_DC 14
#define TFT_RST 2
#define TFT_MOSI 13
#define TFT_SCK 12
SPIClass tftSPI(VSPI);
Adafruit_ILI9341 tft = Adafruit_ILI9341(&tftSPI, TFT_DC, TFT_CS, TFT_RST);
#define PREVIEW_W 160
#define PREVIEW_H 120
#define PREVIEW_X 80
#define PREVIEW_Y 60
static uint8_t rgbBuf[PREVIEW_W * PREVIEW_H * 2];
uint32_t frame_count = 0, decode_fails = 0, null_fb = 0;
unsigned long start_ms = 0;
void draw_status_bar(uint32_t bytes_in, bool decoded) {
tft.fillRect(0, 0, 320, PREVIEW_Y, ILI9341_BLACK);
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.setCursor(8, 6);
tft.print("VELXIO ESP32-CAM live preview");
tft.setCursor(8, 22);
tft.printf("frame %4u %4u B %s",
(unsigned)frame_count, (unsigned)bytes_in,
decoded ? "decode OK " : "decode FAIL ");
unsigned long elapsed = millis() - start_ms;
float fps = elapsed > 0 ? (1000.0f * frame_count) / (float)elapsed : 0.0f;
tft.setCursor(8, 38);
tft.printf("fps %4.1f fails %u nulls %u",
fps, (unsigned)decode_fails, (unsigned)null_fb);
tft.fillCircle(308, 14, 6,
(frame_count & 1) ? ILI9341_GREEN : ILI9341_DARKGREEN);
}
void setup() {
Serial.begin(115200);
delay(500);
Serial.println("=== ESP32-CAM + ILI9341 live preview ===");
tftSPI.begin(TFT_SCK, -1, TFT_MOSI, TFT_CS);
tft.begin();
tft.setRotation(1);
tft.fillScreen(ILI9341_NAVY);
tft.setTextSize(3);
tft.setTextColor(ILI9341_WHITE);
tft.setCursor(20, 10); tft.print("VELXIO");
tft.setTextSize(1);
tft.setCursor(20, 38);
tft.setTextColor(ILI9341_CYAN);
tft.print("ESP32-CAM live preview");
camera_config_t cfg = {};
cfg.ledc_channel = LEDC_CHANNEL_0;
cfg.ledc_timer = LEDC_TIMER_0;
cfg.pin_d0 = Y2_GPIO_NUM; cfg.pin_d1 = Y3_GPIO_NUM;
cfg.pin_d2 = Y4_GPIO_NUM; cfg.pin_d3 = Y5_GPIO_NUM;
cfg.pin_d4 = Y6_GPIO_NUM; cfg.pin_d5 = Y7_GPIO_NUM;
cfg.pin_d6 = Y8_GPIO_NUM; cfg.pin_d7 = Y9_GPIO_NUM;
cfg.pin_xclk = XCLK_GPIO_NUM; cfg.pin_pclk = PCLK_GPIO_NUM;
cfg.pin_vsync = VSYNC_GPIO_NUM; cfg.pin_href = HREF_GPIO_NUM;
cfg.pin_sccb_sda = SIOD_GPIO_NUM;
cfg.pin_sccb_scl = SIOC_GPIO_NUM;
cfg.pin_pwdn = PWDN_GPIO_NUM;
cfg.pin_reset = RESET_GPIO_NUM;
cfg.xclk_freq_hz = 20000000;
cfg.pixel_format = PIXFORMAT_JPEG;
cfg.frame_size = FRAMESIZE_QVGA;
cfg.jpeg_quality = 12;
cfg.fb_count = 1;
cfg.fb_location = CAMERA_FB_IN_DRAM;
esp_err_t err = esp_camera_init(&cfg);
if (err != ESP_OK) {
tft.setCursor(20, 100);
tft.setTextColor(ILI9341_RED);
tft.setTextSize(2);
tft.printf("camera_init FAIL 0x%x", err);
while (1) delay(1000);
}
tft.setCursor(20, 100);
tft.setTextColor(ILI9341_GREEN);
tft.print("camera_init OK");
tft.setCursor(20, 130);
tft.setTextColor(ILI9341_YELLOW);
tft.print("Waiting for webcam frames...");
tft.setCursor(20, 150);
tft.print("Click 'Camera' in the toolbar.");
start_ms = millis();
delay(800);
tft.fillScreen(ILI9341_BLACK);
}
void loop() {
camera_fb_t* fb = esp_camera_fb_get();
if (!fb) {
null_fb++;
delay(50);
return;
}
frame_count++;
size_t fb_len = fb->len;
bool ok = jpg2rgb565(fb->buf, fb->len, rgbBuf, JPG_SCALE_2X);
esp_camera_fb_return(fb);
if (ok) {
tft.drawRGBBitmap(PREVIEW_X, PREVIEW_Y,
(uint16_t*)rgbBuf, PREVIEW_W, PREVIEW_H);
} else {
decode_fails++;
tft.fillRect(PREVIEW_X, PREVIEW_Y,
PREVIEW_W, PREVIEW_H, ILI9341_DARKGREY);
tft.drawLine(PREVIEW_X, PREVIEW_Y,
PREVIEW_X + PREVIEW_W, PREVIEW_Y + PREVIEW_H, ILI9341_RED);
tft.drawLine(PREVIEW_X + PREVIEW_W, PREVIEW_Y,
PREVIEW_X, PREVIEW_Y + PREVIEW_H, ILI9341_RED);
}
draw_status_bar((uint32_t)fb_len, ok);
delay(20);
}
`,
components: [
{
type: 'wokwi-ili9341',
id: 'tft1',
x: 320,
y: 60,
properties: {},
},
],
wires: [
// SPI bus
{
id: 'cam-w-mosi',
start: { componentId: 'esp32-cam', pinName: '13' },
end: { componentId: 'tft1', pinName: 'MOSI' },
color: '#3498db',
},
{
id: 'cam-w-sck',
start: { componentId: 'esp32-cam', pinName: '12' },
end: { componentId: 'tft1', pinName: 'SCK' },
color: '#27ae60',
},
{
id: 'cam-w-cs',
start: { componentId: 'esp32-cam', pinName: '15' },
end: { componentId: 'tft1', pinName: 'CS' },
color: '#e67e22',
},
{
id: 'cam-w-dc',
start: { componentId: 'esp32-cam', pinName: '14' },
end: { componentId: 'tft1', pinName: 'D/C' },
color: '#f1c40f',
},
{
id: 'cam-w-rst',
start: { componentId: 'esp32-cam', pinName: '2' },
end: { componentId: 'tft1', pinName: 'RST' },
color: '#ecf0f1',
},
// Power
{
id: 'cam-w-vcc',
start: { componentId: 'esp32-cam', pinName: '3V3' },
end: { componentId: 'tft1', pinName: 'VCC' },
color: '#e74c3c',
},
{
id: 'cam-w-led',
start: { componentId: 'esp32-cam', pinName: '3V3' },
end: { componentId: 'tft1', pinName: 'LED' },
color: '#e74c3c',
},
{
id: 'cam-w-gnd',
start: { componentId: 'esp32-cam', pinName: 'GND.2' },
end: { componentId: 'tft1', pinName: 'GND' },
color: '#2c3e50',
},
],
},
];
// Merge legacy examples with circuit-focused examples (analog, digital gates,