

このサンプルでは、OV3660 を VGA (640 x 480) 解像度および JPEG 出力形式に設定します。Stamp-S3Bat はカメラが出力する JPEG 画像を直接読み取り、HTTP サーバーを介して MJPEG ライブストリームと JPEG 静止画撮影機能を提供します。
このサンプルは、AP と STA の 2 種類の Wi-Fi 動作モードに対応しています。CAMERA_USE_AP_MODE の値を変更してモードを切り替えます。AP モードでは 1、STA モードでは 0 に設定してください。
#define CAMERA_USE_AP_MODE 1 Stamp-S3Bat は、Stamp-AddOn Cam3660 という名前、12345678 というパスワードの Wi-Fi アクセスポイントを作成します。パソコンまたはモバイルデバイスをこのアクセスポイントに接続した後、デフォルトの IP アドレス 192.168.4.1 でカメラにアクセスします。
http://192.168.4.1/ または http://192.168.4.1/stream にアクセスすると、MJPEG ライブストリームを表示できます。http://192.168.4.1/capture にアクセスすると、JPEG 静止画を取得できます。Stamp-S3Bat は既存の Wi-Fi ネットワークに接続します。使用前に、STA 設定部分の ssid と password を変更してください。接続後、ルーターからデバイスにローカル IP アドレスが割り当てられます。割り当てられたアドレスはシリアルモニタで確認できます。以下の URL にある <デバイス IP> を、シリアルモニタに表示された実際の IP アドレスに置き換えてください。
http://<デバイス IP>/ または http://<デバイス IP>/stream にアクセスすると、MJPEG ライブストリームを表示できます。http://<デバイス IP>/capture にアクセスすると、JPEG 静止画を取得できます。#include <WiFi.h>
#include <WebServer.h>
#include "esp_camera.h"
#define CAMERA_USE_AP_MODE 1
#if CAMERA_USE_AP_MODE
const char *ssid = "Stamp-AddOn Cam3660";
const char *password = "12345678";
#else
const char *ssid = "ssid";
const char *password = "password";
#endif
WebServer server(80);
#define PWDN_GPIO_NUM 39
#define RESET_GPIO_NUM 41
#define XCLK_GPIO_NUM 46
#define SIOD_GPIO_NUM 48
#define SIOC_GPIO_NUM 47
#define Y9_GPIO_NUM 40
#define Y8_GPIO_NUM 38
#define Y7_GPIO_NUM 12
#define Y6_GPIO_NUM 14
#define Y5_GPIO_NUM 15
#define Y4_GPIO_NUM 16
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 21
#define VSYNC_GPIO_NUM 42
#define HREF_GPIO_NUM 17
#define PCLK_GPIO_NUM 13
static camera_config_t camera_config = {
.pin_pwdn = PWDN_GPIO_NUM,
.pin_reset = RESET_GPIO_NUM,
.pin_xclk = XCLK_GPIO_NUM,
.pin_sscb_sda = SIOD_GPIO_NUM,
.pin_sscb_scl = SIOC_GPIO_NUM,
.pin_d7 = Y9_GPIO_NUM,
.pin_d6 = Y8_GPIO_NUM,
.pin_d5 = Y7_GPIO_NUM,
.pin_d4 = Y6_GPIO_NUM,
.pin_d3 = Y5_GPIO_NUM,
.pin_d2 = Y4_GPIO_NUM,
.pin_d1 = Y3_GPIO_NUM,
.pin_d0 = Y2_GPIO_NUM,
.pin_vsync = VSYNC_GPIO_NUM,
.pin_href = HREF_GPIO_NUM,
.pin_pclk = PCLK_GPIO_NUM,
.xclk_freq_hz = 20000000,
.ledc_timer = LEDC_TIMER_0,
.ledc_channel = LEDC_CHANNEL_0,
.pixel_format = PIXFORMAT_JPEG,
.frame_size = FRAMESIZE_VGA,
// .frame_size = FRAMESIZE_QXGA,
.jpeg_quality = 12,
.fb_count = 2,
.fb_location = CAMERA_FB_IN_PSRAM,
.grab_mode = CAMERA_GRAB_WHEN_EMPTY,
.sccb_i2c_port = -1,
};
static constexpr char STREAM_BOUNDARY[] = "123456789000000000000987654321";
static bool writeAll(WiFiClient &client, const uint8_t *data, size_t length)
{
while (length > 0 && client.connected()) {
const size_t written = client.write(data, length);
if (written == 0) {
return false;
}
data += written;
length -= written;
yield();
}
return length == 0;
}
static bool cameraBegin()
{
// Keep the sensor powered up and reset it before initializing the camera driver.
pinMode(PWDN_GPIO_NUM, OUTPUT);
digitalWrite(PWDN_GPIO_NUM, LOW);
pinMode(RESET_GPIO_NUM, OUTPUT);
digitalWrite(RESET_GPIO_NUM, LOW);
delay(20);
digitalWrite(RESET_GPIO_NUM, HIGH);
delay(100);
esp_err_t err = esp_camera_init(&camera_config);
if (err != ESP_OK) {
Serial.printf("Camera Init Fail: 0x%x\r\n", err);
return false;
}
sensor_t *sensor = esp_camera_sensor_get();
if (!sensor) {
Serial.println("Camera sensor get failed");
return false;
}
if (sensor->id.PID != OV3660_PID) {
Serial.printf("Unexpected camera PID: 0x%04x\r\n", sensor->id.PID);
esp_camera_deinit();
return false;
}
// Correct the OV3660 default orientation and color characteristics.
sensor->set_vflip(sensor, 0);
sensor->set_hmirror(sensor, 1);
sensor->set_brightness(sensor, 1);
sensor->set_saturation(sensor, -2);
sensor->set_sharpness(sensor, 1);
Serial.println("Camera Init Success");
return true;
}
static void handleCapture()
{
WiFiClient client = server.client();
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
server.send(503, "text/plain", "Camera capture failed\n");
return;
}
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: image/jpeg");
client.printf("Content-Length: %u\r\n", static_cast<unsigned int>(fb->len));
client.println("Content-Disposition: inline; filename=capture.jpg");
client.println("Cache-Control: no-store, no-cache, must-revalidate");
client.println("Pragma: no-cache");
client.println("Access-Control-Allow-Origin: *");
client.println("Connection: close");
client.println();
writeAll(client, fb->buf, fb->len);
esp_camera_fb_return(fb);
client.stop();
}
static void handleStream()
{
WiFiClient client = server.client();
int64_t lastFrameUs = esp_timer_get_time();
client.println("HTTP/1.1 200 OK");
client.printf("Content-Type: multipart/x-mixed-replace; boundary=%s\r\n", STREAM_BOUNDARY);
client.println("Cache-Control: no-store, no-cache, must-revalidate");
client.println("Pragma: no-cache");
client.println("Access-Control-Allow-Origin: *");
client.println("Connection: close");
client.println();
while (client.connected()) {
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Camera capture failed");
delay(30);
continue;
}
// OV3660 outputs JPEG directly, so the frame buffer can be sent without conversion.
client.printf("--%s\r\n", STREAM_BOUNDARY);
client.printf("Content-Type: image/jpeg\r\n");
client.printf("Content-Length: %u\r\n\r\n", (unsigned int)fb->len);
const bool sent = writeAll(client, fb->buf, fb->len);
const size_t frameSize = fb->len;
esp_camera_fb_return(fb);
if (!sent || client.print("\r\n") == 0) {
break;
}
const int64_t nowUs = esp_timer_get_time();
const int64_t frameTimeMs = (nowUs - lastFrameUs) / 1000;
lastFrameUs = nowUs;
const float fps = frameTimeMs > 0 ? 1000.0f / frameTimeMs : 0.0f;
Serial.printf("MJPG: %uKB %lldms (%.1ffps)\r\n", static_cast<unsigned int>(frameSize / 1024), frameTimeMs, fps);
yield();
}
client.stop();
}
static IPAddress startWiFi()
{
#if CAMERA_USE_AP_MODE
WiFi.mode(WIFI_AP);
if (!WiFi.softAP(ssid, password)) {
Serial.println("WiFi AP start failed");
return IPAddress();
}
const IPAddress ip = WiFi.softAPIP();
Serial.printf("WiFi AP: %s\r\n", ssid);
Serial.printf("IP address: %s\r\n", ip.toString().c_str());
return ip;
#else
WiFi.mode(WIFI_STA);
WiFi.setSleep(false);
WiFi.begin(ssid, password);
Serial.print("Connecting to WiFi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
const IPAddress ip = WiFi.localIP();
Serial.printf("IP address: %s\r\n", ip.toString().c_str());
return ip;
#endif
}
void setup()
{
Serial.begin(115200);
delay(1000);
if (!cameraBegin()) {
while (true) {
delay(1000);
}
}
const IPAddress ip = startWiFi();
if (ip == IPAddress()) {
while (true) {
delay(1000);
}
}
server.on("/", HTTP_GET, handleStream);
server.on("/stream", HTTP_GET, handleStream);
server.on("/capture", HTTP_GET, handleCapture);
server.onNotFound([]() { server.send(404, "text/plain", "Not found\n"); });
server.begin();
Serial.println("Camera HTTP server started");
Serial.printf("Stream: http://%s/stream\r\n", ip.toString().c_str());
Serial.printf("Capture: http://%s/capture\r\n", ip.toString().c_str());
}
void loop()
{
server.handleClient();
delay(1);
}M5StampS3Bat ボードとデバイスに対応するポートを選択します。プログラムを起動したら、パソコンまたはモバイルデバイスを Wi-Fi アクセスポイント Stamp-AddOn Cam3660 に接続します。パスワードは 12345678 です。接続後、ブラウザで http://192.168.4.1/stream を開くとライブストリームを表示でき、http://192.168.4.1/capture を開くと静止画を取得できます。シリアルモニタには、デバイスの IP アドレス、アクセス URL、フレームサイズ、転送時間、およびフレームレートが表示されます。
シリアル出力例:
Camera Init Success
WiFi AP: Stamp-AddOn Cam3660
IP address: 192.168.4.1
Camera HTTP server started
Stream: http://192.168.4.1/stream
Capture: http://192.168.4.1/capture
JPG: 14KB 156ms (6.4fps)
MJPG: 13KB 63ms (15.9fps)
MJPG: 13KB 33ms (30.3fps)
MJPG: 12KB 60ms (16.7fps)
MJPG: 14KB 53ms (18.9fps)
MJPG: 13KB 199ms (5.0fps)
MJPG: 13KB 34ms (29.4fps)
MJPG: 13KB 65ms (15.4fps)
MJPG: 12KB 59ms (16.9fps)
MJPG: 14KB 38ms (26.3fps)
MJPG: 13KB 61ms (16.4fps)
MJPG: 14KB 39ms (25.6fps)
MJPG: 14KB 60ms (16.7fps)
MJPG: 14KB 71ms (14.1fps) ブラウザアクセス例:
