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

Как работает система отправки фото 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-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 µ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 16 | PSRAM | Нет | Никогда не используй |
| Пин PIR | Пин ESP32-CAM | Описание |
|---|---|---|
| VCC | 5V | HC-SR501 нужно не меньше 4,5 V; для AM312 используй 3V3 |
| OUT | GPIO 13 | 3,3 V при движении, 0 V без движения |
| GND | GND | Общая земля |
Из двух подстроечных резисторов на HC-SR501 поверни резистор времени (Tx) на минимум (до упора против часовой стрелки, около 3 секунд); время ожидания возьмёт на себя код. Перемычку режима срабатывания оставь в положении H (повторное срабатывание), а чувствительность выставь где-то между 3 и 7 метрами, как нужно. После включения датчик около минуты привыкает к обстановке и в это время срабатывает случайно, поэтому код игнорирует первые 60 секунд. Подробно о том, как работает датчик, — в статье PIR-датчик движения.
Настройка Telegram-бота: токен и chat ID
- Открой в Telegram @BotFather, набери /newbot и задай боту имя и имя пользователя, которое заканчивается на bot.
- Скопируй токен, который выдаёт BotFather, вида 1234567890:AAE...; он вставляется в строку BOT_TOKEN в коде.
- Чтобы узнать свой номер пользователя, отправь /start боту @userinfobot; значение Id в ответе и есть твой CHAT_ID.
- Открой своего бота и один раз отправь /start; Telegram не позволяет ботам писать тем, с кем у них ещё не было переписки.
- В «Менеджере библиотек» (Library Manager) Arduino IDE установи библиотеки UniversalTelegramBot (Brian Lough) и ArduinoJson.
- Выбери AI Thinker ESP32-CAM в меню Инструменты › Плата (Tools › Board) и проверь, что Partition Scheme — Huge APP (3MB No OTA/1MB SPIFFS).
Отправка фото в Telegram: как работает sendPhotoByBinary?
Функция sendPhotoByBinary() из библиотеки UniversalTelegramBot загружает фото точно так же, как файл. Сначала она отправляет заголовок запроса и поле chat ID, а затем запрашивает данные у трёх небольших функций, которые ты передаёшь: veriVarMi() сообщает, остался ли ещё кусок для отправки, sonrakiTampon() отдаёт адрес данных в памяти, а tamponBoyu() — их размер в байтах. Поскольку буфер кадра камеры уже содержит один готовый JPEG, код передаёт всё фото за один раз и сбрасывает флаг. Фото 1024×768 с качеством 12 весит 50–120 КБ в зависимости от сцены и загружается за 1–2 секунды при хорошем Wi-Fi. Telegram принимает фото до 10 МБ, так что до ограничения по размеру ты даже не приблизишься; настоящий предел — время загрузки.
Полный код: команда /foto, управление вспышкой и сигнализация движения
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 занимает несколько секунд, а снимок до подключения ловит именно момент движения.
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 и никогда не подключай светодиоды прямо к пину.
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 |
Вместе с 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)
Умная охранная сигнализация, которая при обнаружении движения PIR-датчиком включает зуммер и светодиод и одновременно сразу отправляет владельцу уведомление в Telegram. В ней есть 30-секундная пауза, ограничивающая частоту повторных тревог; этот механизм не проверяет срабатывания PIR и не устраняет ложные тревоги. Подходит для дома, гаража, склада или мастерской. С ESP32-CAM можно добавить уведомления с фото, а с обычной ESP32 DevKit V1 приходят текстовые.

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

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

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

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

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

Детектор движения на датчике движения
Простой охранник, который сообщает, что кто-то вошёл в комнату. Датчик движения улавливает тепло человеческого тела, а Arduino держит светодиод включённым, пока есть движение, и гасит его, когда движе
Другие статьи
Приложение BlueGrays
Собери эту схему шаг за шагом в телефоне
Схема подключения, код и список деталей на одном экране. 200+ проектов на Arduino, ESP32 и Pico готовы к сборке.
Наведи камеру телефона
Наведи камеру на код — откроется магазин приложений на телефоне.
или открыть магазин

