ガイド · 読了まで13分

ESP32-CAMとTelegram:動きを検知したら写真を送る

ESP32-CAMからTelegramに写真を送る方法を解説します。PIRの配線と空きピン、/fotoコマンド、フラッシュの制御、電池駆動のためのディープスリープ、夜間撮影、完全なコードまで紹介します。

ESP32-CAM Telegramボットを作ると、ボードは動きを検知した瞬間に写真を撮り、数秒以内にスマホへ送ってくれます。アプリのインストールも、サーバーのレンタルも、ルーターのポート開放も必要ありません。ボードが写真を直接Telegramのサーバーにアップロードし、Telegramがそれを即時通知として届けます。同じボットに/foto(トルコ語で「写真」)と入力すれば、好きなときに玄関の様子を確認でき、フラッシュのオン・オフや警報の解除もできます。

このガイドでは、ESP32-CAMの数少ない空きピンのうちPIRセンサーに使うべきピン、写真をTelegramにアップロードする仕組み、コマンドで動く完全なコード、フラッシュの正しい使い方をステップごとに解説します。その後、ディープスリープでボードを電池で何日も動かす方法、IR照明での夜間撮影、そして防犯カメラとしてこの構成が力不足になる点を紹介します。まだボードのセットアップと最初のスケッチの書き込みを済ませていない場合は、先に「ESP32-CAMの使い方」を読んでください。

Telegram写真システム用のESP32-CAMカメラモジュール
ESP32-CAM:OV2640カメラ、フラッシュLED、Wi-Fiを1枚のボードに搭載。PIRセンサーと組み合わせれば、動きを検知したときに写真をTelegramに送れます。

ESP32-CAMのTelegram写真システムはどう動く?

処理の流れは4つのステップです。PIRセンサーは人の体が発する熱の変化に気づくと、OUTピンを3.3 Vにします。ESP32はこの立ち上がりエッジを検出すると、OV2640カメラにJPEGのフレームを要求し、そのフレームはボードの4 MBのPSRAMに書き込まれます。次にESP32は暗号化されたHTTPS接続を開き、Telegram Bot APIのsendPhotoエンドポイントにmultipart/form-data形式のPOSTリクエストを送ります。ブラウザでファイルをアップロードするときと同じ方法です。Telegramは写真をチャットに追加し、スマホに通知が表示されます。コマンドは逆方向に流れます。ESP32は毎秒getUpdatesを呼び出して新しいメッセージがあるかを確認し、/fotoが届いていれば同じアップロード処理をリクエストに応じて実行します。

部品仕様メモ
ESP32-CAM (AI-Thinker)OV2640, 4 MB PSRAMPSRAM搭載版ならXGAの写真も余裕
ESP32-CAM-MBまたはUSB-TTL変換モジュールCH340 / CP2102コードの書き込み用(ボードにUSBはない)
PIRセンサーHC-SR501(5 V)またはAM312(3.3 V)OUT信号は3.3 VなのでESP32に直結できる
電源5 V、2 A以上Wi-Fiとフラッシュが同時に大きな電流を消費
電解コンデンサ470〜1000 µF5VとGNDの間、ボードの近くに
オプション18650電池+5 V昇圧モジュール、850 nmのIR照明電池駆動と夜間撮影用

PIRセンサーの配線と空きピンの選び方

ESP32-CAMの縁には16本のピンがありますが、カメラだけで15本のGPIOを使い、さらにmicroSDスロットが6本を共有しています。残りのほとんどは起動時の動作に関わるストラッピングピンか、シリアルポートに必要なピンです。このプロジェクトではSDカードを使わないのでSDの信号線が空き、その中で最も安全なのがGPIO 13です。起動に影響せず、内部プルダウン抵抗があり、RTCピンなのでディープスリープからボードを起こすこともできます。下の表でほかの候補も比較しています。

ピンSDカードなしのときディープスリープから起こせる?メモ
GPIO 13空きはい(ext0)PIRに最適
GPIO 14空きはい起動時に短いPWM信号が出ることがあるが、入力なら問題なし
GPIO 15空きはいストラッピングピン。LOWにすると起動メッセージが出なくなる
GPIO 2空きはいストラッピングピン。書き込み中はHIGHにしない
GPIO 12空きはい起動時にHIGHだとボードが起動しない。使わない
GPIO 4フラッシュLEDはいフラッシュ専用。ほかの用途に使わない
GPIO 3 (U0R)シリアルRXいいえシリアルポートが不要な場合のみ
GPIO 16PSRAMいいえ絶対に使わない
PIRのピンESP32-CAMのピン説明
VCC5VHC-SR501は4.5 V以上が必要。AM312なら3V3
OUTGPIO 13動きがあるとき3.3 V、ないとき0 V
GNDGND共通グラウンド
ボードのVCCと書かれたピンは入力ではなく、3.3 Vレギュレーターの出力です。PIRやボードに電源を供給するときは、必ず5 Vを5Vピンに入れてください。PIRをGPIO 12には絶対につながないでください。起動時にセンサーがHIGHを出力すると、ESP32はフラッシュメモリを間違った電圧で動かそうとし、ボードがまったく起動しなくなります。

HC-SR501の2つの半固定抵抗(トリマー)のうち、時間(Tx)のほうを最短(反時計回りにいっぱい、約3秒)に回してください。待ち時間はコードで管理します。トリガーのジャンパーはH(リトリガー)の位置のままにし、感度は必要に応じて3〜7メートルの範囲で調整します。電源を入れた後、センサーは約1分かけて周囲の環境に慣れ、その間はランダムに反応するので、コードでは最初の60秒を無視しています。センサーの仕組みの詳細は「PIR人感センサー」で解説しています。

Telegramボットの準備:トークンとチャットID

  1. Telegramで@BotFatherを開いて/newbotと入力し、ボットの名前と、末尾がbotで終わるユーザー名を付けます。
  2. BotFatherが発行する1234567890:AAE...のようなトークンをコピーします。これをコードのBOT_TOKENの行に入れます。
  3. 自分のユーザー番号を調べるには、@userinfobotに/startを送ります。返ってくるIdの値がCHAT_IDです。
  4. 自分のボットを開き、一度/startを送っておきます。Telegramでは、ボットは一度も会話したことのない相手にメッセージを送れません。
  5. Arduino IDEのライブラリマネージャで、UniversalTelegramBot(Brian Lough)とArduinoJsonのライブラリをインストールします。
  6. 「ツール」>「ボード」でAI Thinker ESP32-CAMを選び、Partition SchemeがHuge APP (3MB No OTA/1MB SPIFFS)になっていることを確認します。
トークンはボットのパスワードです。コードをシェアしたりどこかにアップロードしたりする前に、必ずその行を削除してください。トークンが漏れたと思ったら、BotFatherの/revokeコマンドで新しいトークンを取得します。ボットの作成、チャットID、証明書については「ESP32のTelegramボット」で詳しく解説しています。

Telegramに写真を送る:sendPhotoByBinaryの仕組み

UniversalTelegramBotライブラリのsendPhotoByBinary()関数は、写真をファイルと同じようにアップロードします。まずリクエストのヘッダーとチャットIDのフィールドを送り、続いて自分で用意した3つの小さな関数からデータを受け取ります。veriVarMi()(データがあるか)は送るべき部分が残っているかを、sonrakiTampon()(次のバッファ)はデータのメモリ上のアドレスを、tamponBoyu()(バッファのサイズ)は何バイトあるかを返します。カメラのフレームバッファはすでに1つのJPEGなので、コードは写真全体を一度に渡してフラグを下ろします。品質12で1024×768の写真は、シーンにもよりますが50〜120 KBで、良好なWi-Fi接続なら1〜2秒でアップロードできます。Telegramは写真を10 MBまで受け付けるので、サイズの上限に近づくことはありません。本当の制約はアップロード時間です。

完全なコード:/fotoコマンド、フラッシュの制御、動き検知アラーム

C++ (Arduino IDE)
1#include <WiFi.h>
2#include <WiFiClientSecure.h>
3#include <UniversalTelegramBot.h>
4#include <ArduinoJson.h>
5#include <time.h>
6#include "esp_camera.h"
7
8const char* WIFI_SSID = "AgAdi";          // Wi-Fi adın (yalnız 2,4 GHz) / your Wi-Fi name (2.4 GHz only)
9const char* WIFI_PASS = "Sifre123";       // Wi-Fi şifren / your Wi-Fi password
10#define BOT_TOKEN "1234567890:AAExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"   // BotFather'ın verdiği token / token from BotFather
11#define CHAT_ID   "123456789"             // yalnız bu ID'ye yanıt verilir / only this ID gets answers
12
13// AI-Thinker ESP32-CAM kamera pinleri / AI-Thinker ESP32-CAM camera pins
14#define PWDN_GPIO_NUM     32
15#define RESET_GPIO_NUM    -1
16#define XCLK_GPIO_NUM      0
17#define SIOD_GPIO_NUM     26
18#define SIOC_GPIO_NUM     27
19#define Y9_GPIO_NUM       35
20#define Y8_GPIO_NUM       34
21#define Y7_GPIO_NUM       39
22#define Y6_GPIO_NUM       36
23#define Y5_GPIO_NUM       21
24#define Y4_GPIO_NUM       19
25#define Y3_GPIO_NUM       18
26#define Y2_GPIO_NUM        5
27#define VSYNC_GPIO_NUM    25
28#define HREF_GPIO_NUM     23
29#define PCLK_GPIO_NUM     22
30
31#define FLAS_LED   4                          // beyaz flaş LED'i / white flash LED
32#define PIR_PIN   13                          // PIR OUT (SD kart kullanılmıyor) / PIR OUT (no SD card)
33
34const unsigned long PIR_ISINMA = 60000;       // PIR ilk 60 sn dengelenir / the PIR settles in the first 60 s
35const unsigned long BEKLEME = 30000;          // iki hareket fotoğrafı arası en az 30 sn / at least 30 s between motion photos
36const unsigned long KONTROL_ARALIGI = 1000;   // Telegram'a saniyede bir sor / poll Telegram once per second
37
38WiFiClientSecure client;
39UniversalTelegramBot bot(BOT_TOKEN, client);
40
41camera_fb_t *fb = nullptr;                    // gönderilecek kare / the frame being sent
42bool veriBekliyor = false;                    // kütüphanenin geri çağırma bayrağı / flag for the library callbacks
43bool alarmKurulu = true;                      // hareket alarmı açık mı / is the motion alarm armed
44bool flasAcik = false;                        // fotoğrafta flaş kullanılsın mı / use the flash for photos
45bool pirOnceki = false;
46unsigned long sonFoto = 0;
47unsigned long sonKontrol = 0;
48
49bool kameraBaslat() {
50  camera_config_t config = {};
51  config.ledc_channel = LEDC_CHANNEL_0;
52  config.ledc_timer   = LEDC_TIMER_0;
53  config.pin_d0 = Y2_GPIO_NUM;
54  config.pin_d1 = Y3_GPIO_NUM;
55  config.pin_d2 = Y4_GPIO_NUM;
56  config.pin_d3 = Y5_GPIO_NUM;
57  config.pin_d4 = Y6_GPIO_NUM;
58  config.pin_d5 = Y7_GPIO_NUM;
59  config.pin_d6 = Y8_GPIO_NUM;
60  config.pin_d7 = Y9_GPIO_NUM;
61  config.pin_xclk     = XCLK_GPIO_NUM;
62  config.pin_pclk     = PCLK_GPIO_NUM;
63  config.pin_vsync    = VSYNC_GPIO_NUM;
64  config.pin_href     = HREF_GPIO_NUM;
65  config.pin_sccb_sda = SIOD_GPIO_NUM;
66  config.pin_sccb_scl = SIOC_GPIO_NUM;
67  config.pin_pwdn     = PWDN_GPIO_NUM;
68  config.pin_reset    = RESET_GPIO_NUM;
69  config.xclk_freq_hz = 20000000;             // 20 MHz kamera saati / 20 MHz camera clock
70  config.pixel_format = PIXFORMAT_JPEG;
71
72  if (psramFound()) {
73    config.frame_size   = FRAMESIZE_XGA;      // 1024x768: net ve hızlı gönderilir / sharp and quick to send
74    config.jpeg_quality = 12;                 // küçük = daha kaliteli / lower = better quality
75    config.fb_count     = 2;
76    config.fb_location  = CAMERA_FB_IN_PSRAM;
77    config.grab_mode    = CAMERA_GRAB_LATEST;
78  } else {
79    config.frame_size   = FRAMESIZE_SVGA;     // PSRAM yoksa 800x600 / 800x600 without PSRAM
80    config.jpeg_quality = 12;
81    config.fb_count     = 1;
82    config.fb_location  = CAMERA_FB_IN_DRAM;
83    config.grab_mode    = CAMERA_GRAB_WHEN_EMPTY;
84  }
85  return esp_camera_init(&config) == ESP_OK;
86}
87
88// sendPhotoByBinary için üç geri çağırma: kareyi tek parça verir
89// Three callbacks for sendPhotoByBinary: they hand over the frame in one piece
90bool veriVarMi() {
91  if (veriBekliyor) {
92    veriBekliyor = false;
93    return true;
94  }
95  return false;
96}
97byte *sonrakiTampon() { return fb ? fb->buf : nullptr; }
98int tamponBoyu() { return fb ? fb->len : 0; }
99
100String saat() {                               // "14:05:31" biçiminde saat / time as "14:05:31"
101  struct tm t;
102  if (!getLocalTime(&t, 100)) return "--:--:--";
103  char buf[9];
104  strftime(buf, sizeof(buf), "%H:%M:%S", &t);
105  return String(buf);
106}
107
108bool fotoGonder(const String& chatId, const String& yazi) {
109  if (flasAcik) {
110    digitalWrite(FLAS_LED, HIGH);             // flaşı yak / flash on
111    delay(150);                               // pozlama yeni ışığa uysun / let exposure adapt
112  }
113  fb = esp_camera_fb_get();                   // tampondaki eski kareyi at / discard the stale frame
114  if (fb) esp_camera_fb_return(fb);
115  fb = esp_camera_fb_get();                   // asıl fotoğraf / the real photo
116  digitalWrite(FLAS_LED, LOW);                // flaşı söndür / flash off
117
118  if (!fb) {
119    bot.sendMessage(chatId, "Kamera hatası / camera error");
120    return false;
121  }
122  bot.sendMessage(chatId, yazi);              // önce kısa bildirim / a quick notification first
123  veriBekliyor = true;
124  String cevap = bot.sendPhotoByBinary(chatId, "image/jpeg", fb->len,
125                                       veriVarMi, nullptr, sonrakiTampon, tamponBoyu);
126  Serial.printf("Foto / photo: %u bayt / bytes\n", fb->len);
127  esp_camera_fb_return(fb);                   // kareyi sürücüye geri ver / give the frame back
128  fb = nullptr;
129  return cevap.indexOf("\"ok\":true") >= 0;  // Telegram başarıyı böyle bildirir / how Telegram reports success
130}
131
132void mesajlariIsle(int adet) {
133  for (int i = 0; i < adet; i++) {
134    String chatId = bot.messages[i].chat_id;
135    String metin  = bot.messages[i].text;
136    if (chatId != CHAT_ID) continue;          // yabancılara yanıt yok / never answer strangers
137
138    int at = metin.indexOf('@');              // gruptaki "/foto@bot_adi" -> "/foto" / "/foto@bot_name" -> "/foto"
139    if (at > 0) metin = metin.substring(0, at);
140
141    if (metin == "/start") {
142      String yardim = "/foto - fotoğraf çek / take a photo\n";
143      yardim += "/flas - flaşı aç ya da kapat / toggle the flash\n";
144      yardim += "/alarm - hareket alarmını aç ya da kapat / arm or disarm\n";
145      yardim += "/durum - son durum / status";
146      bot.sendMessage(chatId, yardim);
147    } else if (metin == "/foto") {
148      if (!fotoGonder(chatId, "Fotoğraf / photo " + saat())) {
149        bot.sendMessage(chatId, "Gönderilemedi / send failed");
150      }
151    } else if (metin == "/flas") {
152      flasAcik = !flasAcik;
153      bot.sendMessage(chatId, flasAcik ? "Flaş AÇIK / flash ON" : "Flaş KAPALI / flash OFF");
154    } else if (metin == "/alarm") {
155      alarmKurulu = !alarmKurulu;
156      bot.sendMessage(chatId, alarmKurulu ? "Alarm KURULU / ARMED" : "Alarm KAPALI / DISARMED");
157    } else if (metin == "/durum") {
158      String s = "Alarm: " + String(alarmKurulu ? "KURULU / ARMED" : "KAPALI / DISARMED");
159      s += "\nFlaş / flash: " + String(flasAcik ? "AÇIK / ON" : "KAPALI / OFF");
160      s += "\nWi-Fi: " + String(WiFi.RSSI()) + " dBm";
161      bot.sendMessage(chatId, s);
162    }
163  }
164}
165
166void setup() {
167  Serial.begin(115200);
168  pinMode(FLAS_LED, OUTPUT);
169  digitalWrite(FLAS_LED, LOW);
170  pinMode(PIR_PIN, INPUT_PULLDOWN);           // PIR bağlı değilken pin boşta kalmasın / keep the pin defined without a PIR
171
172  if (!kameraBaslat()) {
173    Serial.println("Kamera baslatilamadi / camera init failed");
174    delay(3000);
175    ESP.restart();
176  }
177
178  WiFi.mode(WIFI_STA);
179  WiFi.begin(WIFI_SSID, WIFI_PASS);
180  while (WiFi.status() != WL_CONNECTED) delay(500);
181  Serial.println(WiFi.localIP());
182
183  configTzTime("<+03>-3", "pool.ntp.org");    // Türkiye saati (UTC+3) / Turkey time (UTC+3)
184  client.setCACert(TELEGRAM_CERTIFICATE_ROOT); // api.telegram.org kök sertifikası / root certificate
185  bot.sendMessage(CHAT_ID, "Kamera hazır, /start yaz / camera ready, send /start");
186}
187
188void loop() {
189  bool pir = digitalRead(PIR_PIN);
190  // Yalnız yükselen kenarda, ısınmadan sonra ve bekleme süresi dolunca
191  // Only on a rising edge, after warm-up and once the cooldown has passed
192  if (pir && !pirOnceki && alarmKurulu && millis() > PIR_ISINMA && millis() - sonFoto > BEKLEME) {
193    sonFoto = millis();
194    fotoGonder(CHAT_ID, "Hareket algılandı! / Motion detected! " + saat());
195  }
196  pirOnceki = pir;
197
198  if (millis() - sonKontrol > KONTROL_ARALIGI) {
199    int adet = bot.getUpdates(bot.last_message_received + 1);
200    while (adet) {
201      mesajlariIsle(adet);
202      adet = bot.getUpdates(bot.last_message_received + 1);
203    }
204    sonKontrol = millis();
205  }
206}
部分役割
kameraBaslat()(カメラの起動)PSRAMがあればXGA(1024×768)と2つのフレームバッファで、なければSVGA(800×600)でカメラを起動
fotoGonder()(写真の送信)必要に応じてフラッシュを点灯し、古いフレームを捨てて新しいフレームを撮影。先に短いメッセージを、続いて写真を送信
mesajlariIsle()(メッセージの処理)CHAT_IDから届いた/start、/foto、/flas(フラッシュ)、/alarm、/durum(状態)コマンドにだけ応答
loop()のPIR部分立ち上がりエッジ、60秒のウォームアップ、30秒のクールダウンを確認
loop()のTelegram部分1秒に1回新しいメッセージを確認し、たまったメッセージがあれば順番にすべて処理

コードはPIRのレベルではなく立ち上がりエッジを見ています。センサーが3秒間HIGHのままでも、撮影される写真は1枚だけです。30秒のクールダウンがあるので、部屋を歩き回る人のせいでスマホに1分間に何十枚もの写真が届くことはなく、同じチャットへ1秒に1通を超えるメッセージを送ってTelegramに速度制限されることも防げます。確認のたびにHTTPSリクエストが発生するため、getUpdatesの呼び出しでloopが0.5秒ほど止まることがあります。HC-SR501の出力は最低3秒間HIGHのままなので、その間に動きを見逃すことはありません。ボードが常時オンの場合、動きを検知してから写真がスマホに届くまで通常2〜4秒です。

フラッシュの制御:使うべきタイミングは?

前面の白色LEDはGPIO 4につながったトランジスタで駆動され、最大の明るさでは電源から100 mA以上の電流を消費します。コードでは撮影の瞬間だけフラッシュを点灯し、約150 ms待ちます。カメラの自動露出は新しい光に慣れるまで数フレームかかるので、バッファ内の最初のフレームも捨てています。フラッシュが届くのは1〜2メートルまでで、それより遠いドアや庭は照らせません。ガラス越しに撮影する場合はフラッシュをオフにしてください。そうしないと写真にはガラスの明るい反射しか写りません。フラッシュを暗めにしたい場合はGPIO 4にPWMをかけます。CameraWebServerのスケッチ例にあるLED Intensityのスライダーはまさにこれを行っています。夜に白いフラッシュが光るとカメラの位置も知られてしまうことを忘れないでください。目立たずに見守るなら、下のIRの章のほうが向いています。

ディープスリープで電池を節約する

Wi-FiがオンのESP32-CAMは常に150〜200 mAを消費し、2600 mAhの18650電池を半日で使い切ってしまいます。電池で動かすなら、ボードはほぼすべての時間をディープスリープで過ごし、PIRが反応したときだけ起きる必要があります。下のコードではGPIO 13がext0のウェイクアップ要因です。PIRの出力がHIGHになるとボードは最初から起動し、まず写真を撮り、それからWi-Fiに接続して写真を送り、再びスリープします。この順番が重要です。Wi-Fi接続には数秒かかるので、接続前に撮影すれば動きのまさにその瞬間をとらえられます。

C++ (Arduino IDE)
1#include <WiFi.h>
2#include <WiFiClientSecure.h>
3#include <UniversalTelegramBot.h>
4#include <ArduinoJson.h>
5#include "esp_camera.h"
6#include "driver/rtc_io.h"
7
8const char* WIFI_SSID = "AgAdi";
9const char* WIFI_PASS = "Sifre123";
10#define BOT_TOKEN "1234567890:AAExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
11#define CHAT_ID   "123456789"
12
13// AI-Thinker ESP32-CAM kamera pinleri / AI-Thinker ESP32-CAM camera pins
14#define PWDN_GPIO_NUM     32
15#define RESET_GPIO_NUM    -1
16#define XCLK_GPIO_NUM      0
17#define SIOD_GPIO_NUM     26
18#define SIOC_GPIO_NUM     27
19#define Y9_GPIO_NUM       35
20#define Y8_GPIO_NUM       34
21#define Y7_GPIO_NUM       39
22#define Y6_GPIO_NUM       36
23#define Y5_GPIO_NUM       21
24#define Y4_GPIO_NUM       19
25#define Y3_GPIO_NUM       18
26#define Y2_GPIO_NUM        5
27#define VSYNC_GPIO_NUM    25
28#define HREF_GPIO_NUM     23
29#define PCLK_GPIO_NUM     22
30
31#define FLAS_LED   4                          // flaş LED'i (RTC pini) / flash LED (RTC pin)
32#define PIR_PIN   13                          // PIR OUT, ext0 ile uyandırır / PIR OUT, wakes via ext0
33
34RTC_DATA_ATTR int hareketSayisi = 0;          // uykuda korunur / survives deep sleep
35
36WiFiClientSecure client;
37UniversalTelegramBot bot(BOT_TOKEN, client);
38camera_fb_t *fb = nullptr;
39bool veriBekliyor = false;
40
41bool veriVarMi() {
42  if (veriBekliyor) {
43    veriBekliyor = false;
44    return true;
45  }
46  return false;
47}
48byte *sonrakiTampon() { return fb ? fb->buf : nullptr; }
49int tamponBoyu() { return fb ? fb->len : 0; }
50
51bool kameraBaslat() {
52  camera_config_t config = {};
53  config.ledc_channel = LEDC_CHANNEL_0;
54  config.ledc_timer   = LEDC_TIMER_0;
55  config.pin_d0 = Y2_GPIO_NUM;
56  config.pin_d1 = Y3_GPIO_NUM;
57  config.pin_d2 = Y4_GPIO_NUM;
58  config.pin_d3 = Y5_GPIO_NUM;
59  config.pin_d4 = Y6_GPIO_NUM;
60  config.pin_d5 = Y7_GPIO_NUM;
61  config.pin_d6 = Y8_GPIO_NUM;
62  config.pin_d7 = Y9_GPIO_NUM;
63  config.pin_xclk     = XCLK_GPIO_NUM;
64  config.pin_pclk     = PCLK_GPIO_NUM;
65  config.pin_vsync    = VSYNC_GPIO_NUM;
66  config.pin_href     = HREF_GPIO_NUM;
67  config.pin_sccb_sda = SIOD_GPIO_NUM;
68  config.pin_sccb_scl = SIOC_GPIO_NUM;
69  config.pin_pwdn     = PWDN_GPIO_NUM;
70  config.pin_reset    = RESET_GPIO_NUM;
71  config.xclk_freq_hz = 20000000;
72  config.pixel_format = PIXFORMAT_JPEG;
73  config.frame_size   = psramFound() ? FRAMESIZE_XGA : FRAMESIZE_SVGA;
74  config.jpeg_quality = 12;
75  config.fb_count     = 1;                    // tek kare yeter / one frame is enough
76  config.fb_location  = psramFound() ? CAMERA_FB_IN_PSRAM : CAMERA_FB_IN_DRAM;
77  config.grab_mode    = CAMERA_GRAB_WHEN_EMPTY;
78  return esp_camera_init(&config) == ESP_OK;
79}
80
81void uyu() {
82  // PIR çıkışı LOW'a dönmeden uyursan kart hemen yeniden uyanır
83  // If you sleep while the PIR output is still HIGH, the board wakes straight away
84  unsigned long t = millis();
85  while (digitalRead(PIR_PIN) == HIGH && millis() - t < 30000) delay(100);
86
87  digitalWrite(FLAS_LED, LOW);
88  rtc_gpio_hold_en(GPIO_NUM_4);               // flaş uykuda sönük kalsın / keep the flash off while asleep
89  esp_sleep_enable_ext0_wakeup(GPIO_NUM_13, 1);   // GPIO 13 HIGH olunca uyan / wake when GPIO 13 goes HIGH
90  Serial.println("Uyku / sleep");
91  Serial.flush();
92  esp_deep_sleep_start();
93}
94
95void setup() {
96  Serial.begin(115200);
97  rtc_gpio_hold_dis(GPIO_NUM_4);              // flaş pinini serbest bırak / release the flash pin
98  rtc_gpio_deinit(GPIO_NUM_13);               // pini yeniden normal GPIO yap / make the pin a normal GPIO again
99  pinMode(FLAS_LED, OUTPUT);
100  digitalWrite(FLAS_LED, LOW);
101  pinMode(PIR_PIN, INPUT);
102
103  if (esp_sleep_get_wakeup_cause() != ESP_SLEEP_WAKEUP_EXT0) {
104    Serial.println("Ilk acilis: PIR 60 sn isiniyor / first boot: PIR warming up 60 s");
105    delay(60000);
106    uyu();
107  }
108  hareketSayisi++;
109
110  // 1) Fotoğrafı hemen çek, Wi-Fi'yi sonra aç: an kaçmasın
111  // 1) Take the photo at once and start Wi-Fi later, so the moment is not lost
112  if (kameraBaslat()) {
113    for (int i = 0; i < 3; i++) {             // ilk kareler karanlık ya da yeşil olur / first frames are dark or green
114      fb = esp_camera_fb_get();
115      if (fb) esp_camera_fb_return(fb);
116    }
117    fb = esp_camera_fb_get();
118  }
119
120  // 2) Wi-Fi'ye bağlan, en fazla 15 sn bekle / connect to Wi-Fi, wait at most 15 s
121  WiFi.mode(WIFI_STA);
122  WiFi.begin(WIFI_SSID, WIFI_PASS);
123  unsigned long t0 = millis();
124  while (WiFi.status() != WL_CONNECTED && millis() - t0 < 15000) delay(100);
125
126  // 3) Gönder ve uyu / send and go back to sleep
127  if (WiFi.status() == WL_CONNECTED) {
128    client.setCACert(TELEGRAM_CERTIFICATE_ROOT);
129    bot.sendMessage(CHAT_ID, "Hareket / motion #" + String(hareketSayisi));
130    if (fb) {
131      veriBekliyor = true;
132      bot.sendPhotoByBinary(CHAT_ID, "image/jpeg", fb->len,
133                            veriVarMi, nullptr, sonrakiTampon, tamponBoyu);
134    } else {
135      bot.sendMessage(CHAT_ID, "Kamera hatası / camera error");
136    }
137  }
138  if (fb) esp_camera_fb_return(fb);
139  Serial.printf("Uyanik kalma / awake: %lu ms\n", millis());
140  uyu();
141}
142
143void loop() {
144}

コードには見落としやすい点が4つあります。1つ目に、ボードはスリープする前にPIRの出力がLOWに戻るのを待ちます。信号がまだHIGHのままスリープすると、ボードはすぐに起きてしまい、ループに陥ります。2つ目に、rtc_gpio_hold_en()はスリープ中にフラッシュのピンをLOWに固定します。この行がないとGPIO 4がフローティング状態になり、スリープ中にフラッシュがうっすら光って電池を消耗させるうえ、人目も引きます。3つ目に、起きた後はrtc_gpio_deinit()でGPIO 13を通常の入力に戻します。そうしないとdigitalRead()が正しく読めないことがあります。4つ目に、RTC_DATA_ATTRを付けたカウンターはスリープ中も保持されるので、どのメッセージにも何回目の動き検知かが表示されます。スリープモードとウェイクアップ要因については「ESP32のディープスリープ」ですべて解説しています。

使い方平均電流(電池側)2600 mAhの18650の場合
常時オン、Wi-Fi接続(最初のスケッチ)≈ 290 mA≈ 9時間
ディープスリープ+1日20回の動き検知≈ 11 mA≈ 10日
電源ラッチ回路(ボードを完全にオフ)+1日20回の動き検知≈ 0.7 mA≈ 4〜5か月(理論値)

表の値は、5 V昇圧モジュールの効率を85%と仮定して計算しています。ESP32チップ自体はディープスリープ中に約10 µAしか使いませんが、ESP32-CAMのボードはレギュレーター、PSRAM、カメラのせいでスリープ中でも約6 mAを消費し、これが電池の持ちを実質的に決めます。1回の起動には6〜10秒かかります。内訳は起動、カメラ、Wi-Fi接続、2回のHTTPSリクエストです。何か月も動かしたいなら、MOSFETでボードの電源を完全に遮断し、PIRが反応したらボードをオンにして、処理が終わったらボード自身が電源を切る電源ラッチ(power latch)回路が必要です。自分の使い方は「電池寿命計算ツール」で計算でき、再起動が頻繁に起きる場合は「ESP32のブラウンアウト」を参照してください。

夜間撮影:フラッシュ、IR LED、カメラの設定

標準的なESP32-CAMのレンズには、赤外線を遮る固定のIRカットフィルターが入っています。そのため昼間は自然な色に見えますが、IRの光だけで照らした場面は暗いままです。暗闇で見るには2つの方法があります。白いフラッシュか、フィルターのないレンズとIR照明の組み合わせです。night visionや850 nmとして売られているOV2640モジュールにはこのフィルターがありません。昼間は色がピンクがかりますが、850 nmのIR LEDで照らした場面ははっきりとした白黒で見えます。940 nmのLEDはまったく目に見えませんが、OV2640はこの波長での感度がかなり低いので、画像は目に見えて暗くなります。IR照明は別の5 Vまたは12 Vの電源から給電してください。光センサー付きの市販モジュールなら、暗くなると自動で点灯します。GPIOから制御したい場合はMOSFETを使い、LEDを絶対にピンへ直結しないでください。

C++ (Arduino IDE)
1#include "esp_camera.h"
2void setup() {
3// esp_camera_init() başarılı olduktan sonra / after esp_camera_init() succeeds
4sensor_t *s = esp_camera_sensor_get();
5s->set_gainceiling(s, GAINCEILING_32X);   // karanlıkta daha yüksek kazanç / more gain in the dark
6s->set_aec2(s, 1);                        // DSP pozlaması: gece modu / DSP exposure: night mode
7s->set_ae_level(s, 2);                    // pozlamayı artır (-2..2) / raise exposure (-2..2)
8s->set_whitebal(s, 1);                    // otomatik beyaz dengesi / auto white balance
9s->set_special_effect(s, 2);              // 2 = siyah-beyaz: IR'de renk zaten bozulur / 2 = grayscale: IR spoils colour anyway
10}
11void loop() {}

これらの設定は、センサーのゲインの上限と露出時間を引き上げます。その代わり、暗い場所ではフレームレートが下がり、動いている人はぶれて写り、高いゲインのせいで画像にざらつきが出ます。夜の写真を改善する最も効果的な方法は、やはり光です。弱いIR照明でも、ゲインを上げるよりはっきりした結果が得られます。このコードはkameraBaslat()の呼び出しの直後に追加できます。必要ならLDRで周囲の明るさを測り、昼と夜の設定を自動で切り替えることもできます。

防犯カメラとしての限界

  • 動画は録画できません:このシステムが送るのは静止画だけです。常時録画や過去の映像を見返したい場合は、市販の防犯カメラが必要です。
  • 遅延があります:ボードがオンなら2〜4秒、ディープスリープから起きる場合は6〜10秒かかります。素早く通り過ぎた人は、写真のフレームからすでに外れているかもしれません。
  • PIRはガラスの向こうを検知できません:ガラスは赤外線の熱放射を通しません。窓の内側に置いたセンサーは屋外の動きを検知しませんが、日光や暖房器具による誤作動は起こりえます。
  • インターネットに依存します:Wi-Fiやインターネットが切れると写真は送られません。重要な用途では、写真をSDカードにも保存しておくとバックアップになります。
  • 物理的な保護がありません:ボードは簡単に取り外したり電源を切ったりでき、屋外では湿気や日光の影響を受けます。屋外で使うなら、密閉したケースと温度に耐えられる電源が必要です。
  • 画質には限界があります:2 MPのOV2640では、数メートル離れた場所から顔を見分けたりナンバープレートを読んだりするには足りません。広角レンズは画角を広げますが、細部は粗くなります。
  • プライバシーのルール:カメラは自分の敷地に向けてください。隣家の庭、共用部分、道路を撮影する前に、住んでいる地域の法律を確認してください。

ESP32-CAMとTelegramのプロジェクトアイデア

  • 玄関チャイムのカメラ:チャイムのボタンをGPIO 13につなぎ、押されたら玄関先を撮影して送るようにします。配線は「Arduinoに押しボタンをつなぐ方法」を参照してください。
  • 郵便受けの通知:ふたにリードスイッチを取り付け、ふたが開くとボードが起きて中の写真を送るようにします。
  • 巣箱やペットのカメラ:動きがあったら写真を撮り、日中は/fotoで様子を確認します。
  • アラームシステムの目:Arduinoのアラームシステムが作動したときにESP32-CAMへ信号を送り、現場を撮影させます。
  • 植物と温室の見守り:タイマーで1時間に1回起きて写真を送り、測定値はMQTTとHome Assistantでダッシュボードにも送ります。
  • 家族のグループ:ボットをTelegramのグループに追加してグループのIDを使えば、写真が全員に同時に届きます。

よくあるトラブルと対処法

症状考えられる原因対処法
メッセージは届くが写真が届かないフレームが大きすぎる、PSRAMが無効、またはアップロードが途中で切れたAI Thinker ESP32-CAMのボードを選び、解像度をSVGAに下げ、Wi-Fiの電波を改善する
ボットがどのコマンドにも応答しないトークンかCHAT_IDが間違っている、またはボットに一度も/startを送っていないトークンをコピーし直し、@userinfobotでIDを確認して、ボットに/startを送る
理由もなく動き検知の写真が届くPIRがWi-Fiアンテナの電波や熱源の影響を受けているPIRをボードから10〜20 cm離し、VCC-GND間に100 nFを追加して、センサーを太陽や暖房器具に向けない
スリープした直後にボードがまた起きるスリープ時にPIRの出力がまだHIGHスリープ前にピンがLOWに戻るのを待ち、HC-SR501の時間のトリマーを最短にする
スリープ中にフラッシュがうっすら光るGPIO 4がフローティング状態になっているスリープ前にLOWを書き込み、rtc_gpio_hold_en(GPIO_NUM_4)を呼び出す
写真が暗い、または緑っぽいカメラ起動直後の最初のフレーム本番の写真の前に2〜3フレームを撮って捨てる
写真の送信中にボードが再起動するブラウンアウト:電源がWi-Fiとフラッシュの電流をまかなえない5 V / 2 AのACアダプター、短い配線、5V-GND間に470〜1000 µFのコンデンサを使う
グループに送る場合、グループIDはマイナス記号で始まります(例:-1001234567890)。CHAT_IDの行には記号ごと書いてください。ボットがグループのメッセージを読めるようにするには、BotFatherで/setprivacyをオフにするか、コマンドを/foto@bot_nameの形で送る必要があるかもしれません。こうした末尾の部分はコードがすでに取り除いています。

Telegramと組み合わせると、ESP32-CAMは安価なボードを、スマホに写真を送るスマートな目に変えてくれます。まずは常時オンのスケッチでシステムを組んでPIRを調整し、必要ならディープスリープ版に移行しましょう。BlueGraysアプリでは、PIR・ブザー・LEDを使ったTelegramの動き検知アラーム、ESP32のディープスリープ、PIRによる自動照明のプロジェクトを、ステップごとの回路図とすぐに使えるコード付きで紹介しています。

よくある質問

ESP32-CAMからTelegramに写真を送るには?

BotFatherでボットを作ってトークンを取得し、UniversalTelegramBotライブラリをインストールして、sendPhotoByBinary()関数でカメラのフレームバッファをTelegramのsendPhotoエンドポイントにアップロードします。ライブラリはブラウザでのファイルアップロードと同じように、写真をmultipart形式でHTTPS経由で送ります。コードで変更するのは、Wi-Fiの情報、トークン、自分のチャットIDだけです。

ESP32-CAMのどのピンにPIRセンサーをつなげばいいですか?

SDカードを使わないなら、GPIO 13が最も安全です。起動に影響せず、RTCピンなのでディープスリープからボードを起こせます。PIRのVCCを5Vピンに、GNDをGNDに、OUTをGPIO 13につなぎます。GPIO 12は避けてください。起動時にHIGHだとボードが起動しません。

ESP32-CAMは電池でどのくらい動きますか?

Wi-Fiを常時オンにすると、2600 mAhの18650電池で約9時間です。ディープスリープを使って動きがあるときだけ起きるようにすれば、約10日まで延びます。ボードはレギュレーターとPSRAMのせいで、スリープ中でも約6 mAを消費します。何か月も動かすには、ボードの電源を完全に遮断する電源ラッチ回路が必要です。

ESP32-CAMは夜でも見えますか?

標準のモジュールはIRカットフィルターが赤外線を遮るので、暗闇で撮影できるのは白いフラッシュで1〜2メートルまでです。フィルターのない(night vision)OV2640モジュールと850 nmのIR照明を使えば、暗闇でも白黒ながらはっきりした写真を撮れます。カメラ設定でゲインの上限を上げるのも効果がありますが、画像にざらつきが出ます。

ESP32-CAMはTelegramに動画を送れますか?

実際には送れません。ESP32-CAMが作るのは1枚ずつのJPEGフレームで、それをMP4の動画に変換する処理能力もメモリもありません。数枚の写真を連続で送るか、CameraWebServerで自宅のネットワーク内からライブ映像を見るほうが現実的です。

写真を複数の人に送れますか?

はい。ボットをTelegramのグループに追加し、マイナス記号で始まるグループのIDをCHAT_IDとして使えば、写真がグループの全員に同時に届きます。別の方法として、コードで複数のチャットIDを配列に入れ、順番にそれぞれへ写真を送ることもできます。送信ごとに別々のアップロードになるので、時間は長くかかります。

全プロジェクトを見る
ESP32動体検知アラーム(PIR + ブザー + LED + Telegram)
#145上級
プレミアム

ESP32動体検知アラーム(PIR + ブザー + LED + Telegram)

PIRセンサーが動きを検知するとブザーとLEDを作動させ、同時にTelegramで持ち主に即座に通知するスマート防犯アラームです。アラームが繰り返される頻度を抑えるため、30秒のクールダウン(待ち時間)を設けています。この仕組みはPIRの検知が正しいかを確認したり、誤検知をなくしたりするものではありません。家の防犯、ガレージの監視、倉庫や作業部屋などに使えます。ESP32-CAMなら写真付きの通知を追加でき、通常のESP32 DevKit V1ではテキストで通知します。

0.0(0)
9
ESP32 PIR人感センサーによる自動照明
#121中級
プレミアム

ESP32 PIR人感センサーによる自動照明

このプロジェクトでは、HC-SR501 PIR人感センサーで部屋に入った人を検知し、LEDを自動で点灯させます。millis()で点灯時間を管理します。センサーの値がHIGHの間は最後に動きを検知した時刻を更新し、最後にHIGHを読み取ってから少なくとも10秒後にLEDを消灯します。現在のコードは各ループでdelay(50)により50 ms待機するため、完全にノンブロッキングではありません。階段や廊下、浴室、戸棚内の照明などに応用できます。

4.4(24)
38
ESP32のディープスリープで節電(OLED + RTC RAMカウンター)
#135上級
プレミアム

ESP32のディープスリープで節電(OLED + RTC RAMカウンター)

ESP32のディープスリープモードは、CPUとほとんどの周辺機器を停止し、消費電流を約10 µAまで抑えます。電池で動くIoT機器に欠かせない機能です。このプロジェクトでは、タイマー(15秒)、外部ピン(EXT0、ボタンはGPIO 33)、タッチ(T0/GPIO 4)の3種類の復帰方法を使います。RTC_DATA_ATTRを付けた変数はディープスリープ中も保持され、起動回数、タイマーによる復帰回数、ボタンによる復帰回数を継続して記録できます。復帰するたびにSSD1306 OLEDディスプレイに復帰理由と回数を表示します。

4.3(10)
16
ESP32スマート照明(PIR + LDR + リレー + OLED)
#141上級
プレミアム

ESP32スマート照明(PIR + LDR + リレー + OLED)

ESP32で廊下やトイレ、食品庫の照明をスマート化します!PIRモーションセンサー、LDR光センサー、リレーモジュールを使い、「暗い場所で動きを検知すると点灯する」定番の仕組みを作ります。周囲が明るければ動きを検知しても点灯しないため、日中の無駄な電力消費を抑えられます。照明は30秒間点灯し、動きが続くとタイマーがリセットされて点灯時間が延びます。SSD1306 OLEDには大きな文字で状態(ON / DAYTIME / WAITING)とカウントダウンを表示します。

4.3(6)
11
ESP32 Wi-Fi STA接続(LCDディスプレイ付き)
#127上級

ESP32 Wi-Fi STA接続(LCDディスプレイ付き)

このプロジェクトでは、ESP32をステーション(STA)モードで家庭のWi-Fiネットワークに接続し、安全で障害に強い接続を実現します。SSIDとパスワードを別のsecrets.hファイルに保存してGitへの流出を防ぎ、自動再接続の仕組みを実装します。RSSIによる信号強度とIPアドレスはI2C LCDディスプレイに表示します。内蔵LEDは状態表示に使います。実運用を想定した接続管理の仕組みです。

4.4(21)
34
ESP32 mDNS:bluegrays-esp.localでデバイスにアクセス(LCD付き)
#131上級
プレミアム

ESP32 mDNS:bluegrays-esp.localでデバイスにアクセス(LCD付き)

このプロジェクトでは、ESP32にmDNS(Multicast DNS)サービスを追加し、IPアドレスの代わりにbluegrays-esp.localのような覚えやすいホスト名でアクセスできるようにします。DHCPで割り当てられるIPアドレスが変わっても、同じ名前でデバイスにアクセスできます。小さなWebサーバーも動かし、ブラウザーでhttp://bluegrays-esp.local/を開いてLEDを操作します。I2C LCDディスプレイにはホスト名、IPアドレス、LEDの状態をトルコ語と英語で表示します。iOS、macOS、LinuxはmDNSに標準対応しています。

4.8(14)
23
人感センサーで動きを検出
#24初級
プレミアム

人感センサーで動きを検出

誰かが部屋に入ると知らせる、シンプルな見張り役です。人感センサーが人体から放出される熱を捉え、Arduinoは動きがある間LEDを点灯させ、動きが止まると消灯させます。玄関先で自動点灯する照明や、警報システムの基本となる仕組みです。

4.6(47)
67