3. 自作監視カメラの概要と準備
(ESP32-CAMを使う)

更新日: 2026.06.21

概要

今回は自作監視カメラ製作の下準備です。監視カメラ製作には幾つかのモジュールとアプリを使用しています。 それらを使用する上での注意点まとめました。

自作監視カメラ構成部品

下記の部品を使って監視カメラを作ります。

Module table
  • ESP32-CAM
    • 本体:AI Thinker
  • 2軸SG90サーボモータテーブル
    • 先端のフォルダーにESP32−CAMを取り付ける。
    • これにより上下と左右(回転)が出来る。
  • 人感センサ
    • AM312を使って人感センサ(PIR)です。
    • 人を感知したら自動で撮影する。

回路図

G90サーボモータx2で信号線は2本。人感センサの信号線は1本。合計3個のポート必要です。 ここでESP32-CAMヘッダーピンのおさらい。

左側用途右側用途
GPIO04フラッシュGNDGND
GPIO02SD_MMCGPIO01通信
GPIO14SD_MMCGPIO03通信
GPIO15SD_MMC3.3/5V3.3/5V
GPIO13SD_MMCGNDGND
GPIO12SD_MMCGPIO00モードセレクト
GNDGNDGPIO16PSRAM管理
5V5V3.3V3.3V

GPIO1,3はシリアル通信デバッグ用に残すとすると、使用出来る汎用ポートを確保する為に SD_MMC を諦める事にしました。 今回は監視カメラなので多くの写真を撮るとは想定していない。写真の容量は1枚約20kb位でLittleFSでも十分と判断しました。

これによりGPIO2,14,15,13,12の5つが空く。ただ、GPIO02は起動時モードセレクトに関係しており、 GPIO12はSPAMの電圧に関係しているのでこの2つは避ける。そこで13,14,15を使って下記の回路としました。

ピン用途
GPIO13サーボモータ(上下用)
GPIO15サーボモータ(左右用)
GPIO14人感センサ
Schematic of ESP32 & Modules
  • フラッシュ(GPIO04)はそのまま残して使用。
  • 電源は5V。ESP32-CAMとサーボモータに供給。
  • 電源安定用に470uFの電解コンデンサを5VとGNDに使用。
  • 人感センサの3.3VはESP32-CAMから供給。

各モジュールの動作確認

フラッシュ(GPIO04)

ここー>フラッシュで確認済み。

人感センサ(GPIO14)

AM312人感センサを参照。 これを使って動体を検知したらESP32-CAMに割り込みを発生させ写真とメールを送る機能をもたせる。写真を添付したメールの送信は、メールを送るを参照。

SG90サーボモータ(GPIO13,15)

このカメラの性質上、サーボモータでカメラを動かしている時間より止まっている時間の方が圧倒的に長いので節電の為 モータを動かす時以外はサーボをオフにしていました。最初SG90を参照にスケッチを書いたのですが、 良く誤動作が発生しました。

誤動作は以下の状況の時に良く起こりました

  • 2つのサーボモータを同時に動かす事をさけ別々に動かす。
  • 一方のモータの動作が終了するまで待ってそのモータのサーボをオフにする。
  • 続いて他方のモータのサーボをオンにして動かすとモータが時々暴走する。

この現象はサーボをオフしなければ起こらない。オンオフする時のみ起こる現象でした。

servo_motor.ino arduino
#include "Arduino.h" #include <ESP32Servo.h> //サーボモータ制御用のライブラリ #define r_l_pin 13 #define u_d_pin 15 Servo r_l; // 左右回転のサーボモータ Servo u_d; // 上下回転のサーボモータ //左右回転 void r_l_servo(int ang) { r_l.attach(r_l_pin); r_l.write(ang); delay(150); r_l.detach(); } //上下回転 void u_d_servo(int ang) { u_d.attach(u_d_pin); u_d.write(ang); delay(150); u_d.detach(); } void test(){ r_l_servo(80); // まずは左右に回転(引数は角度) u_d_servo(60); // 引き続き上下回転を行うと、こちらがたまに暴走する。 }

色々試したのですが暴走の原因が分からず、ライブラリ”ESP32Servo.h”を諦めArduino IDE 組み込み関数でコードを書いてみました。

servo_motor_new.ino arduino
#include "Arduino.h" #define r_l_pin 13 #define u_d_pin 15 //左右回転 void r_l_servo(int ang) { ledcAttach(r_l_pin, 50, 14); ledcWrite(r_l_pin, calculateDuty14Bit(ang)); delay(150); ledcDetach(r_l_pin); } //上下回転 void u_d_servo(int ang) { ledcAttach(u_d_pin, 50, 14); ledcWrite(u_d_pin, calculateDuty14Bit(ang)); delay(150); // モーターが回り切るのを待つ ledcDetach(u_d_pin) } uint32_t calculateDuty14Bit(int angle) { // 角度をマイクロ秒に変換 long us = map(angle, 0, 180, ud_min_pwm, ud_max_pwm); // 50Hz(周期 20,000us)において、14bit解像度(最大値 16383)でのDuty値を計算 // 式: (目標のμs / 20000μs) * 16383 uint32_t duty = (us * 16383) / 20000; return duty; } void test(){ r_l_servo(80); // まずは左右に回転(引数は角度) u_d_servo(60); // 引き続き上下回転。これだと暴走しません。 }

ライブラリ "ESP32Servo.h" では回転位置の指定は "r_l.write(ang);" の様に角度を指定すれば良かったのですが 新しい関数では角度をマイクロ秒に変換する必要が有ります。変換方法は8,14,16ビットと有りますが14ビット で変換すると上手く動く事が分かりました。この関数ではサーボをオンオフしても暴走しません。今回はこの関数をサーボモータ 制御用に使用しています。

Webサーバ

このカメラにはWebサーバが2つ必要です。一つはクライアントに静止画を連続で送る Streaming用サーバ。 もう一つはカメラ管理(サーボモータを使ってパン・ティルトを行う等)用のサーバです。

今回は、Webサーバー用のライブラリとして "WebServer.h" ではなく "esp_http_server.h" を使用しています。 "esp_http_server.h" はESP-IDFに組み込まれているのですがArduino IDEでも動きます。

"WebServer.h" と"esp_http_server.h" の大きな違いは、"WebServer.h" がシングルタスクに対し、 "esp_http_server.h" はマルチタスクである事です。 今回の様に静止画をひたすら送る操作の間にカメラを制御するにはマルチタスクである "esp_http_server.h" の方が シングルタスクの "WebServer.h" より断然向いています。

"esp_http_server.h" を使ったサーバは下記の3つからなります。

関数機能
ハンドラURLにアクセスされたら関数を実行
URI構造体の設定URLのパスと、HTTPメソッド、ハンドラの紐付
サーバーの起動と登録サーバーを初期化し、構造体を登録。

下記は "esp_http_server.h" を用いたサーバスケッチの例です。サーバーにアクセスすると "Hello,ESP32!" と表示します

hello.ino arduino
#include <WiFi.h> #include "esp_http_server.h" // ハンドラ関数(アクセスされたときに実行される処理) static esp_err_t hello_get_handler(httpd_req_t *req) { const char* resp_str = "Hello, ESP32!"; httpd_resp_send(req, resp_str, HTTPD_RESP_USE_STRLEN); return ESP_OK; } // URI構造体の設定(URLとハンドラの紐付け) static const httpd_uri_t hello_uri = { .uri = "/", // アクセスするURLのパス .method = HTTP_GET, // HTTPメソッド(GETリクエスト) .handler = hello_get_handler, // 呼び出すハンドラ関数 .user_ctx = NULL // 自由に使いたいデータ(今回はなし) }; // サーバーの本体(ハンドル)を保持する変数 httpd_handle_t server = NULL; void startServer() { // サーバーのデフォルト設定を読み込む httpd_config_t config = HTTPD_DEFAULT_CONFIG(); // サーバーの起動 if (httpd_start(&server, &config) == ESP_OK) { httpd_register_uri_handler(server, &hello_uri); Serial.println("Server started!"); } } void setup() { Serial.begin(115200); // Wi-Fiに接続(SSIDとパスワードは環境に合わせてください) WiFi.begin("YOUR_SSID", "YOUR_PASSWORD"); while (WiFi.status() != WL_CONNECTED) { delay(500); } Serial.print("IP Address: "); Serial.println(WiFi.localIP()); // サーバー起動関数の呼び出し startServer(); } void loop() { // loopの中身は完全に空っぽでOK! // サーバーはバックグラウンド(自動)で動きます。 }
  • ハンドラ:4から9行
    • ここでは実行したい内容の記述を行います。
    • 今回はアクセスされたら "Hello, ESP32!" と画面に表示します
  • URI構造体の設定:11から17行
    • ここでクライアントからのアクセスとハンドラ等の設定を行います。
      • .uri = "/" アクセスするURLのパスはルート
      • .method = HTTP_GET リクエストの方法は GET
      • .handler = hello_get_handler 実行される関数
  • サーバーの起動と登録:19から31行
    • 27行: httpd_start(&server, &config) ここでサーバを起動し
    • 28行: httpd_register_uri_handler(server, &hello_uri); ここで登録します
  • Loop()関数が空
    • Loop()関数が空なのが "esp_http_server.h" の特徴
    • "WebServer.h"ではここに server.handleClient();が有りループ内いで監視していました

この例を元したStreamingを行う簡易サーバのコードは以下の様になります。

streaming.ino arduino
#include "esp_camera.h" #include "Arduino.h" #include #include "esp_http_server.h" // HTTPサーバー用ライブラリに変更 #include "stream.h" #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 #define LED_GPIO_NUM 4 //ハンドラ関数(アクセスされたときに実行される処理) static esp_err_t stream_handler(httpd_req_t *req){ camera_fb_t * fb = NULL; esp_err_t res = ESP_OK; char part_buf[64]; res = httpd_resp_set_type(req, "multipart/x-mixed-replace; boundary=--frame"); if(res != ESP_OK) return res; while(true){ fb = esp_camera_fb_get(); if (!fb) { Serial.println("Camera capture failed"); res = ESP_FAIL; break; } // ヘッダー文字列の組み立て((char *)の無理な型変換も不要になり安全です) size_t hlen = snprintf(part_buf, sizeof(part_buf), "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n", fb->len); res = httpd_resp_send_chunk(req, part_buf, hlen); if(res == ESP_OK){ res = httpd_resp_send_chunk(req, (const char *)fb->buf, fb->len); } if(res == ESP_OK){ res = httpd_resp_send_chunk(req, "\r\n", 2); } esp_camera_fb_return(fb); if(res != ESP_OK) break; delay(10); } return res; } static esp_err_t index_handler(httpd_req_t *req){ httpd_resp_set_type(req, "text/html; charset=utf-8"); return httpd_resp_send(req, stream, HTTPD_RESP_USE_STRLEN); } // サーバーの本体(ハンドル)を保持する変数 httpd_handle_t stream_httpd = NULL; // Streaming用 httpd_handle_t web_httpd = NULL; // カメラ管理用 void startServer() { // カメラ管理用 httpd_config_t web_config = HTTPD_DEFAULT_CONFIG(); httpd_uri_t index_uri = { .uri = "/", .method = HTTP_GET, .handler = index_handler, .user_ctx = NULL }; if (httpd_start(&web_httpd, &web_config) == ESP_OK) { httpd_register_uri_handler(web_httpd, &index_uri); } // Streaming用 httpd_config_t stream_config = HTTPD_DEFAULT_CONFIG(); stream_config.server_port = 81; stream_config.ctrl_port = 32769; // コントロールポートも重複しないようにずらす stream_config.max_open_sockets = 2; // ストリーミング用なので少数でOK httpd_uri_t stream_uri = { .uri = "/stream", .method = HTTP_GET, .handler = stream_handler, .user_ctx = NULL }; if (httpd_start(&stream_httpd, &stream_config) == ESP_OK) { httpd_register_uri_handler(stream_httpd, &stream_uri); } } void setup() { Serial.begin(115200); // Wi-Fiに接続(SSIDとパスワードは環境に合わせてください) WiFi.begin("XXXXXXXX", "YYYYYYYY"); while (WiFi.status() != WL_CONNECTED) { delay(500); } Serial.print("IP Address: "); Serial.println(WiFi.localIP()); init_cam(); // サーバー起動関数の呼び出し startServer(); } void loop() { } void init_cam() { camera_config_t config; config.ledc_channel = LEDC_CHANNEL_0; config.ledc_timer = LEDC_TIMER_0; config.pin_d0 = Y2_GPIO_NUM; config.pin_d1 = Y3_GPIO_NUM; config.pin_d2 = Y4_GPIO_NUM; config.pin_d3 = Y5_GPIO_NUM; config.pin_d4 = Y6_GPIO_NUM; config.pin_d5 = Y7_GPIO_NUM; config.pin_d6 = Y8_GPIO_NUM; config.pin_d7 = Y9_GPIO_NUM; config.pin_xclk = XCLK_GPIO_NUM; config.pin_pclk = PCLK_GPIO_NUM; config.pin_vsync = VSYNC_GPIO_NUM; config.pin_href = HREF_GPIO_NUM; config.pin_sscb_sda = SIOD_GPIO_NUM; config.pin_sscb_scl = SIOC_GPIO_NUM; config.pin_pwdn = PWDN_GPIO_NUM; config.pin_reset = RESET_GPIO_NUM; config.xclk_freq_hz = 20000000; config.pixel_format = PIXFORMAT_JPEG; config.frame_size = FRAMESIZE_VGA; config.fb_count = 2; config.grab_mode = CAMERA_GRAB_LATEST; // 最新のフレームを掴む(遅延対策) config.fb_location = CAMERA_FB_IN_PSRAM; // PSRAMを利用 config.jpeg_quality = 10; // 画質設定(10〜12バランスが良いです) // カメラの初期化実行 esp_err_t err = esp_camera_init(&config); if (err != ESP_OK) { Serial.printf("Camera init failed with error 0x%x\n", err); return; // 初期化に失敗した場合はここで処理を止める } Serial.println("Camera Init Success! (800x600 mode)"); }
  • カメラ管理用とStreaming用の2つのサーバを定義しています。
  • カメラ管理用サーバの仕事
    • クライアントから GET / のリクエストが有ったらHPのコードを返す
    • "Start" ボタンで Streaming開始。"Stop" ボタンで停止。
  • Streaming用サーバの仕事:27〜58行
    • リクエストが有ったらひたすら静止画を送る。
      • 36行:fb = esp_camera_fb_get(); ここで静止画を撮影
      • 47行:res = httpd_resp_send_chunk(req, (const char *)fb->buf, fb->len); 画像の送信
      • 52行:esp_camera_fb_return(fb); バッファーの解除

"stream.h" を "stream.ino" と同じフォルダーに保存してコンパイル実行して下さい。 実行するとシリアルモニタにHPのアドレスが表示されます。それをブラウザのURL欄い入力すると下記の画面が表示されます

ESP32 Streaming server

"Start" "Stop" ボタンでStreamigが開始、停止します。

次回は

監視カメラを作る為の基礎データは揃ったのでこれを元に次回は監視カメラを製作します。

SINCE 2026