Руководства · 10 мин чтения

ESP32-CAM и Telegram: отправка фото при обнаружении движения

Отправка фото с ESP32-CAM в Telegram: подключение PIR-датчика и свободные пины, команда /foto, управление вспышкой, глубокий сон для работы от батарей, ночная съёмка и полный код.

С ботом ESP32-CAM Telegram плата делает снимок в момент обнаружения движения и через несколько секунд отправляет его на телефон. Не нужно ставить приложение, арендовать сервер или открывать порт на роутере: плата загружает фото прямо на серверы Telegram, а Telegram доставляет его тебе как мгновенное уведомление. Через того же бота можно в любой момент набрать /foto и посмотреть на входную дверь, включить или выключить вспышку и снять сигнализацию с охраны.

В этом руководстве пошагово разберём, какой из немногих свободных пинов ESP32-CAM взять для PIR-датчика, как фото загружается в Telegram, полный код с управлением командами и как правильно пользоваться вспышкой. После этого заставим плату днями работать от батарей с глубоким сном, будем снимать ночью с ИК-подсветкой и посмотрим, где эта система уступает настоящей камере наблюдения. Если плата ещё не настроена и первый скетч не загружен, сначала прочитай статью Как пользоваться ESP32-CAM.

Модуль камеры ESP32-CAM для системы отправки фото в Telegram
ESP32-CAM: камера OV2640, светодиод вспышки и Wi-Fi на одной плате. Вместе с PIR-датчиком она может отправлять фото в Telegram при обнаружении движения.

Как работает система отправки фото ESP32-CAM в Telegram?

Цепочка состоит из четырёх шагов. PIR-датчик замечает изменение теплового излучения человеческого тела и поднимает свой пин OUT до 3,3 V. Увидев этот нарастающий фронт, ESP32 запрашивает у камеры OV2640 кадр JPEG, который записывается в 4 МБ PSRAM на плате. Затем ESP32 открывает зашифрованное HTTPS-соединение и отправляет POST-запрос в формате multipart/form-data на метод sendPhoto Telegram Bot API; это тот же способ, которым браузер загружает файл. Telegram добавляет фото в твой чат, и телефон показывает уведомление. Команды идут в обратную сторону: каждую секунду ESP32 вызывает getUpdates и спрашивает, нет ли новых сообщений, и если пришла команда /foto, выполняет ту же загрузку по твоему запросу.

ДетальХарактеристикиПримечание
ESP32-CAM (AI-Thinker)OV2640, 4 МБ PSRAMВерсия с PSRAM легко справляется с фото в XGA
ESP32-CAM-MB или преобразователь USB-TTLCH340 / CP2102Для загрузки кода; на самой плате нет USB
PIR-датчикHC-SR501 (5 V) или AM312 (3,3 V)Сигнал OUT — 3,3 V, подключается прямо к ESP32
Источник питания5 V, не меньше 2 AWi-Fi и вспышка одновременно потребляют большой ток
Электролитический конденсатор470–1000 µFМежду 5V и GND, рядом с платой
ДополнительноАккумулятор 18650 + повышающий преобразователь на 5 V, ИК-прожектор 850 нмДля работы от батарей и ночной съёмки

Подключение PIR-датчика и выбор свободного пина

У ESP32-CAM по краям 16 пинов, но одна только камера занимает 15 GPIO, а слот microSD делит ещё шесть. Большинство оставшихся — это либо пины конфигурации при старте (strapping), либо они нужны для последовательного порта. Поскольку в этом проекте SD-карта не используется, линии SD свободны, и самый безопасный из них — GPIO 13: он не влияет на загрузку, у него есть встроенный стягивающий резистор (pull-down), а поскольку это RTC-пин, он может будить плату из глубокого сна. В таблице ниже сравниваются и другие кандидаты.

ПинБез SD-картыБудит из глубокого сна?Примечание
GPIO 13СвободенДа (ext0)Лучший выбор для PIR
GPIO 14СвободенДаПри загрузке может ненадолго выдавать ШИМ-сигнал; как вход подходит
GPIO 15СвободенДаПин конфигурации (strapping); в состоянии LOW глушит сообщения при загрузке
GPIO 2СвободенДаПин конфигурации (strapping); во время загрузки кода не должен быть HIGH
GPIO 12СвободенДаЕсли при старте он в HIGH, плата не запустится; не используй его
GPIO 4Светодиод вспышкиДаЗанят вспышкой; не используй его для другого
GPIO 3 (U0R)RX последовательного портаНетТолько если последовательный порт не нужен
GPIO 16PSRAMНетНикогда не используй
Пин PIRПин ESP32-CAMОписание
VCC5VHC-SR501 нужно не меньше 4,5 V; для AM312 используй 3V3
OUTGPIO 133,3 V при движении, 0 V без движения
GNDGNDОбщая земля
Пин платы с надписью VCC — это не вход, а выход стабилизатора 3,3 V; для питания PIR-датчика или платы всегда подавай 5 V на пин 5V. Никогда не подключай PIR к GPIO 12: если при старте датчик выдаёт HIGH, ESP32 пытается запустить флеш-память с неправильным напряжением, и плата вообще не стартует.

Из двух подстроечных резисторов на HC-SR501 поверни резистор времени (Tx) на минимум (до упора против часовой стрелки, около 3 секунд); время ожидания возьмёт на себя код. Перемычку режима срабатывания оставь в положении H (повторное срабатывание), а чувствительность выставь где-то между 3 и 7 метрами, как нужно. После включения датчик около минуты привыкает к обстановке и в это время срабатывает случайно, поэтому код игнорирует первые 60 секунд. Подробно о том, как работает датчик, — в статье PIR-датчик движения.

Настройка Telegram-бота: токен и chat ID

  1. Открой в Telegram @BotFather, набери /newbot и задай боту имя и имя пользователя, которое заканчивается на bot.
  2. Скопируй токен, который выдаёт BotFather, вида 1234567890:AAE...; он вставляется в строку BOT_TOKEN в коде.
  3. Чтобы узнать свой номер пользователя, отправь /start боту @userinfobot; значение Id в ответе и есть твой CHAT_ID.
  4. Открой своего бота и один раз отправь /start; Telegram не позволяет ботам писать тем, с кем у них ещё не было переписки.
  5. В «Менеджере библиотек» (Library Manager) Arduino IDE установи библиотеки UniversalTelegramBot (Brian Lough) и ArduinoJson.
  6. Выбери AI Thinker ESP32-CAM в меню Инструменты › Плата (Tools › Board) и проверь, что Partition Scheme — Huge APP (3MB No OTA/1MB SPIFFS).
Токен — это пароль твоего бота: всегда удаляй эту строку, прежде чем делиться кодом или куда-то его выкладывать. Если есть подозрение, что токен утёк, получи новый командой /revoke в BotFather. Создание бота, chat ID и сертификат подробнее разобраны в статье Telegram-бот на ESP32.

Отправка фото в Telegram: как работает sendPhotoByBinary?

Функция sendPhotoByBinary() из библиотеки UniversalTelegramBot загружает фото точно так же, как файл. Сначала она отправляет заголовок запроса и поле chat ID, а затем запрашивает данные у трёх небольших функций, которые ты передаёшь: veriVarMi() сообщает, остался ли ещё кусок для отправки, sonrakiTampon() отдаёт адрес данных в памяти, а tamponBoyu() — их размер в байтах. Поскольку буфер кадра камеры уже содержит один готовый JPEG, код передаёт всё фото за один раз и сбрасывает флаг. Фото 1024×768 с качеством 12 весит 50–120 КБ в зависимости от сцены и загружается за 1–2 секунды при хорошем Wi-Fi. Telegram принимает фото до 10 МБ, так что до ограничения по размеру ты даже не приблизишься; настоящий предел — время загрузки.

Полный код: команда /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()Запускает камеру в XGA (1024×768) с двумя буферами кадра, если есть PSRAM, иначе в SVGA (800×600)
fotoGonder()По запросу включает вспышку, выбрасывает устаревший кадр и берёт свежий, сначала отправляет короткое сообщение, а потом фото
mesajlariIsle()Отвечает только на команды /start, /foto, /flas, /alarm и /durum, пришедшие с CHAT_ID
Часть loop() для PIRПроверяет нарастающий фронт, 60 с прогрева и 30 с паузы (cooldown)
Часть loop() для TelegramРаз в секунду запрашивает новые сообщения и по очереди обрабатывает накопившиеся

Код следит за нарастающим фронтом сигнала PIR, а не за его уровнем: даже если датчик 3 секунды остаётся в HIGH, делается только один снимок. 30-секундная пауза не даёт человеку, который ходит по комнате, завалить телефон десятками фото в минуту, и не даёт Telegram включить ограничение: в один чат он пропускает не больше одного сообщения в секунду. Каждый опрос — это HTTPS-запрос, поэтому вызов getUpdates может задержать цикл примерно на полсекунды; поскольку выход HC-SR501 держится в HIGH не меньше 3 секунд, за это время ни одно движение не теряется. Если плата всё время включена, между движением и появлением фото на телефоне обычно проходит 2–4 секунды.

Управление вспышкой: когда её использовать?

Белый светодиод на лицевой стороне управляется через транзистор, подключённый к GPIO 4, и на полной яркости берёт от источника больше 100 mA. Код зажигает вспышку только в момент съёмки и ждёт около 150 мс; поскольку автоэкспозиции камеры нужно несколько кадров, чтобы подстроиться под новый свет, первый кадр в буфере тоже выбрасывается. Вспышка эффективна на расстоянии до 1–2 метров; дверь или сад дальше она не осветит. Если снимать через стекло, выключи вспышку, иначе на фото будет видно только яркое отражение в стекле. Для более слабой вспышки можно подать ШИМ на GPIO 4; ползунок LED Intensity в примере CameraWebServer делает именно это. Помни, что белая вспышка ночью ещё и выдаёт, где стоит камера; для незаметного наблюдения лучше подойдёт ИК-подсветка из раздела ниже.

Экономия батареи с глубоким сном

ESP32-CAM с включённым Wi-Fi постоянно потребляет 150–200 mA, и аккумулятор 18650 на 2600 mAh разряжается за полдня. При работе от батарей плата должна почти всё время проводить в глубоком сне и просыпаться только при срабатывании 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}

В коде есть четыре детали, которые легко упустить. Во-первых, перед сном плата ждёт, пока выход PIR вернётся в LOW; если заснуть, пока сигнал ещё в HIGH, плата тут же просыпается и застревает в цикле. Во-вторых, rtc_gpio_hold_en() фиксирует пин вспышки в LOW на время сна; без этой строки GPIO 4 «висит в воздухе», и вспышка во сне слабо светится, разряжая батарею и привлекая внимание. В-третьих, после пробуждения rtc_gpio_deinit() возвращает GPIO 13 в режим обычного входа, иначе digitalRead() может читать его неправильно. В-четвёртых, счётчик с пометкой RTC_DATA_ATTR переживает сон, поэтому в каждом сообщении указан номер события движения. Все режимы сна и источники пробуждения разобраны в статье Глубокий сон ESP32.

СценарийСредний ток (со стороны батареи)С аккумулятором 18650 на 2600 mAh
Всё время включена, Wi-Fi подключён (первый скетч)≈ 290 mA≈ 9 ч
Глубокий сон + 20 событий движения в день≈ 11 mA≈ 10 дней
Схема самоблокировки питания (плата полностью выключена) + 20 событий движения в день≈ 0,7 mA≈ 4–5 месяцев (теоретически)

Значения в таблице рассчитаны при КПД повышающего преобразователя на 5 V 85%. Хотя сам чип ESP32 в глубоком сне потребляет всего около 10 µA, плата ESP32-CAM во сне всё равно берёт около 6 mA из-за стабилизатора, PSRAM и камеры, и именно это определяет время работы от батареи. Каждое пробуждение занимает 6–10 секунд: загрузка, камера, подключение к Wi-Fi и два HTTPS-запроса. Для работы месяцами нужна схема самоблокировки питания (power latch), которая полностью отключает питание платы MOSFET-транзистором, включает плату при срабатывании PIR и позволяет плате самой выключиться по завершении. Свой сценарий можно посчитать в утилите «калькулятор времени работы от батареи», а при частых перезагрузках смотри статью ESP32 brownout.

Ночная съёмка: вспышка, ИК-светодиоды и настройки камеры

В стандартных объективах ESP32-CAM стоит несъёмный ИК-фильтр (IR-cut), который не пропускает инфракрасный свет; поэтому днём цвета выглядят естественно, а сцена, освещённая только ИК, остаётся тёмной. Видеть в темноте можно двумя способами: белой вспышкой или ИК-подсветкой с объективом без фильтра. У модулей OV2640, которые продаются как night vision или 850 нм, этого фильтра нет; днём цвета уходят в розовый, зато сцену, освещённую ИК-светодиодами на 850 нм, хорошо видно в чёрно-белом изображении. Светодиоды на 940 нм совсем невидимы, но на этой длине волны OV2640 гораздо менее чувствительна, и картинка заметно темнеет. Питай ИК-прожектор от отдельного источника 5 V или 12 V; готовые платы с датчиком освещённости сами включаются, когда темнеет. Если нужно управлять им с GPIO, используй MOSFET и никогда не подключай светодиоды прямо к пину.

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() {}

Эти настройки поднимают верхний предел усиления датчика и время экспозиции. Цена такая: в темноте частота кадров падает, движущийся человек получается смазанным, а высокое усиление добавляет в изображение зерно. Самый действенный способ улучшить ночное фото — по-прежнему свет; даже слабая ИК-подсветка даёт более чёткий результат, чем увеличение усиления. Код можно вставить сразу после вызова kameraBaslat(), а при желании — измерять внешнюю освещённость фоторезистором (LDR) и автоматически переключаться между дневными и ночными настройками.

Пределы в роли камеры наблюдения

  • Видео не записывается: эта система отправляет только статичные фото. Для непрерывной записи или просмотра архива нужна полноценная камера наблюдения.
  • Есть задержка: 2–4 секунды при включённой плате и 6–10 секунд при пробуждении из глубокого сна; быстро проходящий человек может уже выйти из кадра.
  • PIR не видит сквозь стекло: стекло не пропускает инфракрасное тепловое излучение. Датчик за окном не заметит движения снаружи, но солнечный свет и батареи отопления всё равно могут вызывать ложные срабатывания.
  • Всё зависит от интернета: если пропадёт Wi-Fi или интернет, фото не отправится. Для важных задач стоит дополнительно сохранять фото на SD-карту как резервную копию.
  • Нет физической защиты: плату легко снять или выключить, а на улице на неё действуют влага и солнце; для установки снаружи нужны герметичный корпус и источник питания, рассчитанный на перепады температуры.
  • Качество изображения ограничено: 2-мегапиксельной OV2640 не хватает, чтобы распознать лицо или прочитать номерной знак с нескольких метров; широкоугольные объективы расширяют кадр, но уменьшают детализацию.
  • Правила приватности: направляй камеру на свою территорию; прежде чем снимать соседский сад, общие зоны или улицу, проверь законы, которые действуют там, где ты живёшь.

Идеи проектов с ESP32-CAM и Telegram

  • Камера у дверного звонка: подключи кнопку звонка к GPIO 13, чтобы при нажатии плата фотографировала порог и присылала снимок тебе; подключение — в статье как подключить кнопку.
  • Оповещение о почте: поставь на крышку почтового ящика геркон, чтобы при открытии плата просыпалась и присылала фото содержимого.
  • Камера в скворечнике или для питомца: делай снимок при движении и заглядывай в течение дня командой /foto.
  • Глаза для сигнализации: пусть сигнализация на Arduino при срабатывании подаёт сигнал на ESP32-CAM, чтобы та сфотографировала место.
  • Наблюдение за растениями и теплицей: пусть плата раз в час просыпается по таймеру и отправляет фото, а показания датчиков можно выводить на дашборд через MQTT и Home Assistant.
  • Семейная группа: добавь бота в группу Telegram и используй ID группы, чтобы фото сразу получали все.

Частые проблемы и решения

СимптомВероятная причинаРешение
Сообщение приходит, а фото нетСлишком большой кадр, PSRAM выключена или загрузка оборваласьВыбери плату AI Thinker ESP32-CAM, снизь разрешение до SVGA, улучши сигнал Wi-Fi
Бот не отвечает ни на одну командуНеверный токен или CHAT_ID либо боту ни разу не отправлена команда /startСкопируй токен заново, проверь свой ID через @userinfobot, отправь /start своему боту
Фото о движении приходят без причиныPIR ловит сигнал антенны Wi-Fi или источник теплаОтнеси PIR на 10–20 см от платы, поставь 100 nF между VCC и GND, не направляй датчик на солнце или батареи отопления
Плата снова просыпается сразу после засыпанияПри уходе в сон выход PIR ещё в HIGHПеред сном дождись, пока пин вернётся в LOW, выставь подстроечный резистор времени HC-SR501 на минимум
Вспышка слабо светится во снеGPIO 4 оставлен «висеть в воздухе»Перед сном запиши LOW и вызови rtc_gpio_hold_en(GPIO_NUM_4)
Фото тёмное или зеленоватоеПервые кадры сразу после запуска камерыПеред настоящим снимком возьми и выброси 2–3 кадра
Плата перезагружается во время отправки фотоBrownout: источник не справляется с током Wi-Fi и вспышкиИспользуй адаптер 5 V / 2 A, короткие провода и конденсатор на 470–1000 µF между 5V и GND
При отправке в группу ID группы начинается со знака минус (например, -1001234567890); впиши его в строку CHAT_ID вместе со знаком. Чтобы бот читал сообщения группы, может понадобиться отключить /setprivacy в BotFather или отправлять команды в виде /foto@bot_name; код уже отрезает такие окончания.

Вместе с Telegram ESP32-CAM превращает недорогую плату в умный глаз, который присылает фото на телефон. Сначала собери систему со скетчем, где плата всё время включена, и настрой PIR, а затем при необходимости переходи на версию с глубоким сном. В приложении BlueGrays есть проекты «Сигнализация движения на ESP32 (PIR + зуммер + светодиод + Telegram)», «Глубокий сон ESP32: экономия батареи (OLED + счётчик в RTC RAM)» и «Автоматическое освещение с PIR-датчиком движения на ESP32» — у каждого пошаговые схемы подключения и готовый код.

Частые вопросы

Как отправить фото с ESP32-CAM в Telegram?

Создай бота через BotFather и получи токен, установи библиотеку UniversalTelegramBot и загрузи буфер кадра камеры на метод sendPhoto в Telegram функцией sendPhotoByBinary(). Библиотека отправляет фото по HTTPS в формате multipart, как при загрузке файла в браузере. В коде нужно поменять только данные Wi-Fi, токен и свой chat ID.

К какому пину ESP32-CAM подключить PIR-датчик?

Если SD-карта не используется, самый безопасный выбор — GPIO 13: он не влияет на загрузку и, как RTC-пин, может будить плату из глубокого сна. Подключи VCC PIR-датчика к пину 5V, GND к GND, а OUT к GPIO 13. Избегай GPIO 12: если при старте он в HIGH, плата не запустится.

Сколько ESP32-CAM проработает от батареи?

С постоянно включённым Wi-Fi аккумулятора 18650 на 2600 mAh хватает примерно на 9 часов. Если использовать глубокий сон и просыпаться только при движении, срок растёт примерно до 10 дней; даже во сне плата потребляет около 6 mA из-за стабилизатора и PSRAM. Для работы месяцами нужна схема самоблокировки питания, которая полностью отключает питание платы.

Видит ли ESP32-CAM ночью?

ИК-фильтр на стандартных модулях не пропускает инфракрасный свет, поэтому в темноте изображение получается только на расстоянии до 1–2 метров с белой вспышкой. С модулем OV2640 без фильтра (night vision) и ИК-прожектором 850 нм можно делать в темноте чёткие чёрно-белые снимки. Повышение верхнего предела усиления в настройках камеры тоже помогает, но добавляет в изображение зерно.

Может ли ESP32-CAM отправлять видео в Telegram?

На практике нет. ESP32-CAM выдаёт отдельные кадры JPEG, и у неё не хватает вычислительной мощности и памяти, чтобы собрать из них видео MP4. Реалистичнее отправлять несколько фото подряд или смотреть видеопоток в домашней сети через CameraWebServer.

Можно ли отправлять фото нескольким людям?

Да. Если добавить бота в группу Telegram и указать в CHAT_ID ID группы, который начинается со знака минус, фото сразу получат все участники группы. Другой вариант — записать в коде несколько chat ID в массив и отправлять фото каждому по очереди; каждая отправка — отдельная загрузка, поэтому это дольше.

Все проекты
Сигнализация движения на ESP32 (PIR + зуммер + светодиод + Telegram)
#145Продвинутый
Premium

Сигнализация движения на ESP32 (PIR + зуммер + светодиод + Telegram)

Умная охранная сигнализация, которая при обнаружении движения PIR-датчиком включает зуммер и светодиод и одновременно сразу отправляет владельцу уведомление в Telegram. В ней есть 30-секундная пауза, ограничивающая частоту повторных тревог; этот механизм не проверяет срабатывания PIR и не устраняет ложные тревоги. Подходит для дома, гаража, склада или мастерской. С ESP32-CAM можно добавить уведомления с фото, а с обычной ESP32 DevKit V1 приходят текстовые.

0.0(0)
9
Автоматическое освещение с PIR-датчиком движения на ESP32
#121Средний
Premium

Автоматическое освещение с PIR-датчиком движения на ESP32

В этом проекте PIR-датчик движения HC-SR501 замечает, что кто-то вошёл в комнату, и автоматически включает светодиод. Время свечения отслеживается через millis(); каждое показание HIGH обновляет время последнего движения, и светодиод гаснет не раньше чем через 10 секунд после последнего HIGH. Текущий код ещё и ждёт 50 ms на каждом цикле через delay(50), поэтому цикл не полностью неблокирующий. Подходит для освещения лестниц, шкафов и ванной.

4.4(24)
38
Глубокий сон ESP32: экономия батареи (OLED + счётчик в RTC RAM)
#135Продвинутый
Premium

Глубокий сон ESP32: экономия батареи (OLED + счётчик в RTC RAM)

Режим глубокого сна (Deep Sleep) у ESP32 снижает потребление до ~10 µA, выключая процессор и большую часть периферии, — это важно для IoT-устройств на батарейках. В этом проекте используются три источника пробуждения: таймер (15 секунд), внешний пин (EXT0 — кнопка GPIO 33) и касание (T0/GPIO 4). Переменные с RTC_DATA_ATTR сохраняются во время глубокого сна — счётчики загрузок, пробуждений по таймеру и по кнопке ведутся непрерывно.

4.3(10)
16
Умное освещение на ESP32 (PIR + LDR + реле + OLED)
#141Продвинутый
Premium

Умное освещение на ESP32 (PIR + LDR + реле + OLED)

Сделай умное освещение для коридора, ванной или кладовки на ESP32! С PIR-датчиком движения, датчиком освещённости LDR и модулем реле собирается классический сценарий «свет включается при движении в темноте»: если вокруг уже светло, движение свет НЕ включает, и днём энергия экономится. Свет горит 30 секунд; если движение продолжается, таймер сбрасывается, и свет горит дольше. OLED SSD1306 крупно показывает состояние (ON / DAYTIME / WAITING) и обратный отсчёт.

4.3(6)
11
Подключение ESP32 к Wi-Fi в режиме STA (с экраном LCD)
#127Продвинутый

Подключение ESP32 к Wi-Fi в режиме STA (с экраном LCD)

В этом проекте ESP32 надёжно и устойчиво к сбоям подключается к домашней сети Wi-Fi в режиме станции (STA). SSID и пароль хранятся в отдельном файле secrets.h, чтобы не утекли в Git; настроено автоматическое переподключение, а уровень сигнала RSSI и IP-адрес выводятся на I2C LCD-экран. Встроенный светодиод служит индикатором состояния. Это менеджер подключения уровня реальных устройств.

4.4(21)
34
mDNS на ESP32: доступ к устройству через bluegrays-esp.local (с экраном LCD)
#131Продвинутый
Premium

mDNS на ESP32: доступ к устройству через bluegrays-esp.local (с экраном LCD)

В этом проекте к ESP32 добавляется служба mDNS (Multicast DNS), и к устройству можно обращаться по понятному имени вроде bluegrays-esp.local вместо IP-адреса. Даже если IP, выданный DHCP, меняется, устройство всегда доступно по одному имени. На устройстве также работает маленький веб-сервер, через который по адресу http://bluegrays-esp.local/ из браузера отправляются команды светодиоду. I2C LCD показывает имя хоста, IP-адрес и состояние светодиода.

4.8(14)
23
Детектор движения на датчике движения
#24Новичок
Premium

Детектор движения на датчике движения

Простой охранник, который сообщает, что кто-то вошёл в комнату. Датчик движения улавливает тепло человеческого тела, а Arduino держит светодиод включённым, пока есть движение, и гасит его, когда движе

4.6(47)
67