ESP32-CAM Telegram: enviar una foto al detectar movimiento
Envía fotos de la ESP32-CAM a Telegram: conexión del PIR y pin libre, comando /foto, control del flash, deep sleep para usar batería, fotos nocturnas y código.
Cuando montas un bot de ESP32-CAM Telegram, la placa toma una foto en el momento en que detecta movimiento y te la envía al móvil en pocos segundos. No necesitas instalar ninguna aplicación, alquilar un servidor ni abrir puertos en el router: la placa sube la foto directamente a los servidores de Telegram y Telegram te la entrega como una notificación instantánea. Desde el mismo bot puedes escribir /foto para ver la puerta de casa cuando quieras, encender o apagar el flash y desactivar la alarma.
En esta guía verás paso a paso a cuál de los pocos pines libres de la ESP32-CAM conectar un sensor PIR, cómo se sube la foto a Telegram, el código completo controlado por comandos y cómo usar bien el flash. Después harás funcionar la placa con batería durante días gracias al deep sleep, tomarás fotos nocturnas con iluminación IR y descubrirás dónde se queda corto este montaje como cámara de seguridad. Si todavía no has configurado la placa ni subido tu primer programa, lee antes Cómo usar la ESP32-CAM.

¿Cómo funciona un sistema de fotos ESP32-CAM con Telegram?
La cadena tiene cuatro pasos. El sensor PIR nota un cambio en el calor que emite un cuerpo humano y pone su pin OUT a 3,3 V. Cuando el ESP32 ve ese flanco de subida, pide a la cámara OV2640 un fotograma JPEG, que se guarda en la PSRAM de 4 MB de la placa. Después el ESP32 abre una conexión HTTPS cifrada y envía una petición POST en formato multipart/form-data al método sendPhoto de la API de bots de Telegram; es el mismo método que usa un navegador al subir un archivo. Telegram añade la foto a tu chat y tu móvil muestra una notificación. Los comandos van en sentido contrario: cada segundo el ESP32 pregunta con getUpdates si hay mensajes nuevos y, si ha llegado /foto, hace la misma subida para tu petición.
| Pieza | Característica | Nota |
|---|---|---|
| ESP32-CAM (AI-Thinker) | OV2640, 4 MB de PSRAM | La versión con PSRAM maneja fotos XGA sin problema |
| ESP32-CAM-MB o conversor USB-TTL | CH340 / CP2102 | Para cargar el código; la placa no tiene USB |
| Sensor PIR | HC-SR501 (5 V) o AM312 (3,3 V) | La salida OUT es de 3,3 V y se conecta directamente al ESP32 |
| Alimentación | 5 V, al menos 2 A | El Wi-Fi y el flash consumen mucha corriente a la vez |
| Condensador electrolítico | 470–1000 µF | Entre 5V y GND, cerca de la placa |
| Opcional | Batería 18650 + elevador a 5 V, iluminador IR de 850 nm | Para funcionar con batería y para fotos nocturnas |
Conexión del sensor PIR y elección de un pin libre
La ESP32-CAM tiene 16 pines en sus bordes, pero la cámara sola usa 15 GPIO y la ranura microSD comparte otros seis. La mayoría de los que quedan son pines de arranque (strapping) o hacen falta para el puerto serie. Como este proyecto no usa tarjeta SD, las líneas de la SD quedan libres y la más segura es GPIO 13: no afecta al arranque, tiene resistencia pull-down interna y, al ser un pin RTC, puede despertar la placa del deep sleep. La tabla siguiente compara también los demás candidatos.
| Pin | Sin tarjeta SD | ¿Despierta del deep sleep? | Nota |
|---|---|---|---|
| GPIO 13 | Libre | Sí (ext0) | La mejor opción para el PIR |
| GPIO 14 | Libre | Sí | Puede mostrar una breve señal PWM al arrancar; sin problema como entrada |
| GPIO 15 | Libre | Sí | Pin de arranque; en LOW silencia los mensajes de arranque |
| GPIO 2 | Libre | Sí | Pin de arranque; no debe estar en HIGH al cargar |
| GPIO 12 | Libre | Sí | Si está en HIGH al arrancar, la placa no inicia; no lo uses |
| GPIO 4 | LED de flash | Sí | Reservado para el flash; no lo uses para otra cosa |
| GPIO 3 (U0R) | RX serie | No | Solo si no necesitas el puerto serie |
| GPIO 16 | PSRAM | No | No lo uses nunca |
| Pin del PIR | Pin de la ESP32-CAM | Descripción |
|---|---|---|
| VCC | 5V | El HC-SR501 necesita al menos 4,5 V; con un AM312 usa 3V3 |
| OUT | GPIO 13 | 3,3 V cuando hay movimiento, 0 V cuando no |
| GND | GND | Tierra común |
De los dos potenciómetros del HC-SR501, gira el de tiempo (Tx) a su posición más corta (todo en sentido antihorario, unos 3 segundos); el tiempo de espera lo gestionará el código. Deja el jumper de disparo en H (disparo repetido) y ajusta la sensibilidad entre 3 y 7 metros según lo que necesites. Tras encenderse, el sensor tarda aproximadamente un minuto en adaptarse al entorno y durante ese tiempo se dispara al azar; por eso el código ignora los primeros 60 segundos. Los detalles de su funcionamiento están en Sensor de movimiento PIR.
Preparar el bot de Telegram: token y chat ID
- Abre @BotFather en Telegram, escribe /newbot y dale a tu bot un nombre y un nombre de usuario que termine en bot.
- Copia el token con formato 1234567890:AAE... que te da BotFather; irá en la línea BOT_TOKEN del código.
- Para conocer tu número de usuario, escribe /start a @userinfobot; el valor Id que devuelve es tu CHAT_ID.
- Abre tu propio bot y escribe /start una vez; Telegram no permite que un bot escriba a alguien con quien nunca ha hablado.
- En el Gestor de bibliotecas del Arduino IDE instala UniversalTelegramBot (Brian Lough) y ArduinoJson.
- En Herramientas > Placa elige AI Thinker ESP32-CAM y comprueba que el Partition Scheme sea Huge APP (3MB No OTA/1MB SPIFFS).
Enviar una foto a Telegram: ¿cómo funciona sendPhotoByBinary?
La función sendPhotoByBinary() de la biblioteca UniversalTelegramBot sube la foto igual que un archivo. Primero envía la cabecera de la petición y el campo del chat ID, y luego pide los datos a tres pequeñas funciones que tú le das: veriVarMi() indica si queda algún trozo por enviar, sonrakiTampon() da la dirección de los datos en memoria y tamponBoyu() dice cuántos bytes son. Como el búfer de la cámara ya es un único JPEG, el código entrega la foto completa de una vez y baja la bandera. Una foto de 1024×768 con calidad 12 ocupa entre 50 y 120 KB según la escena y se sube en 1–2 segundos con un buen Wi-Fi. Telegram admite fotos de hasta 10 MB, así que nunca te acercarás al límite de tamaño; el límite real es el tiempo de subida.
Código completo: comando /foto, control del flash y alarma de movimiento
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}| Parte | Qué hace |
|---|---|
| kameraBaslat() | Inicia la cámara en XGA (1024×768) con dos búferes si hay PSRAM, o en SVGA (800×600) si no la hay |
| fotoGonder() | Enciende el flash si se pide, descarta el fotograma viejo y toma uno nuevo, envía primero un mensaje corto y después la foto |
| mesajlariIsle() | Solo responde a /start, /foto, /flas, /alarm y /durum cuando llegan desde CHAT_ID |
| Parte PIR de loop() | Comprueba el flanco de subida, los 60 s de calentamiento y los 30 s de espera |
| Parte Telegram de loop() | Pregunta por mensajes nuevos una vez por segundo y procesa en orden los que se hayan acumulado |
El código vigila el flanco de subida del PIR, no su nivel: aunque el sensor siga en HIGH durante 3 segundos, solo se toma una foto. La espera de 30 segundos evita que alguien que camina por la habitación te llene el móvil de decenas de fotos por minuto y que Telegram frene más de un mensaje por segundo al mismo chat. Como cada consulta es una petición HTTPS, la llamada a getUpdates puede detener el bucle cerca de medio segundo; como la salida del HC-SR501 se mantiene en HIGH al menos 3 segundos, no se pierde ningún movimiento mientras tanto. Con la placa siempre encendida, entre el movimiento y la llegada de la foto al móvil suelen pasar 2–4 segundos.
Control del flash: ¿cuándo encenderlo?
El LED blanco del frente se controla mediante un transistor conectado a GPIO 4 y, a máximo brillo, consume más de 100 mA de la alimentación. El código enciende el flash solo en el momento de la foto y espera unos 150 ms; como la exposición automática de la cámara necesita varios fotogramas para adaptarse a la nueva luz, también se descarta el primer fotograma del búfer. El flash es eficaz hasta 1–2 metros; una puerta o un jardín más lejos no se iluminan. Si fotografías a través de un cristal, apaga el flash o en la foto solo verás el reflejo brillante en el vidrio. Si quieres un flash más suave puedes aplicar PWM a GPIO 4; el control deslizante LED Intensity del ejemplo CameraWebServer hace exactamente eso. Recuerda que un flash blanco que se dispara de noche también delata dónde está la cámara; para una vigilancia discreta es mejor la sección de IR de más abajo.
Ahorro de batería con deep sleep
Una ESP32-CAM con el Wi-Fi encendido consume 150–200 mA todo el tiempo y agota una batería 18650 de 2600 mAh en medio día. Si vas a usar batería, la placa debe pasar casi todo el tiempo en deep sleep y despertar solo cuando se dispare el PIR. En el código siguiente, GPIO 13 es la fuente de despertar ext0: cuando la salida del PIR pasa a HIGH, la placa arranca desde cero, toma primero la foto, después se conecta al Wi-Fi, envía la foto y vuelve a dormir. El orden importa: la conexión Wi-Fi tarda unos segundos, y tomar la foto antes de conectarse capta el momento exacto del movimiento.
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}Hay cuatro detalles del código fáciles de pasar por alto. Primero, la placa espera a que la salida del PIR vuelva a LOW antes de dormir; si duerme con la señal aún en HIGH, despierta al instante y entra en un bucle. Segundo, rtc_gpio_hold_en() fija el pin del flash en LOW durante el sueño; sin esta línea GPIO 4 queda al aire y el flash brilla débilmente mientras duerme, gastando batería y llamando la atención. Tercero, al despertar, rtc_gpio_deinit() devuelve GPIO 13 a entrada normal; si no, digitalRead() puede leer mal. Cuarto, el contador marcado con RTC_DATA_ATTR se conserva durante el sueño, así que cada mensaje te dice qué número de movimiento es. Todos los modos de sueño y fuentes de despertar están en Deep sleep en el ESP32.
| Escenario | Corriente media (lado de la batería) | Con una 18650 de 2600 mAh |
|---|---|---|
| Siempre encendida, Wi-Fi conectado (primer código) | ≈ 290 mA | ≈ 9 horas |
| Deep sleep + 20 movimientos al día | ≈ 11 mA | ≈ 10 días |
| Circuito de autorretención (placa totalmente apagada) + 20 movimientos al día | ≈ 0,7 mA | ≈ 4–5 meses (teórico) |
Los valores de la tabla suponen un elevador de 5 V con un 85 % de eficiencia. Aunque el chip ESP32 solo gasta unos 10 µA en deep sleep, la placa ESP32-CAM sigue consumiendo unos 6 mA dormida por culpa del regulador, la PSRAM y la cámara, y eso es lo que realmente fija la duración de la batería. Cada despertar dura 6–10 segundos: arranque, cámara, conexión Wi-Fi y dos peticiones HTTPS. Si quieres meses de autonomía necesitas un circuito de autorretención (power latch) que corte por completo la alimentación de la placa con un MOSFET, la encienda cuando se dispare el PIR y deje que la propia placa se apague al terminar. Puedes calcular tu escenario con la Calculadora de duración de batería y, si hay reinicios frecuentes, consulta Brownout en el ESP32.
Fotos nocturnas: flash, LED IR y ajustes de la cámara
Las lentes estándar de la ESP32-CAM llevan un filtro IR-cut fijo que bloquea la luz infrarroja; por eso de día los colores se ven naturales, pero una escena iluminada solo con IR sigue a oscuras. Hay dos formas de ver en la oscuridad: el flash blanco o la iluminación IR con una lente sin filtro. Los módulos OV2640 que se venden como night vision o 850 nm no tienen ese filtro; de día los colores tiran a rosa, pero una escena iluminada con LED IR de 850 nm se ve nítida en blanco y negro. Los LED de 940 nm son totalmente invisibles, pero la OV2640 es mucho menos sensible a esa longitud de onda y la imagen sale bastante más oscura. Alimenta el iluminador IR desde una fuente aparte de 5 V o 12 V; los módulos ya hechos con sensor de luz se encienden solos al anochecer. Si quieres controlarlo desde un GPIO, usa un MOSFET y nunca conectes los LED directamente al pin.
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() {}Estos ajustes suben el techo de ganancia y el tiempo de exposición del sensor. El precio es que de noche baja la velocidad de fotogramas, una persona en movimiento sale borrosa y la ganancia alta añade grano a la imagen. La forma más eficaz de mejorar una foto nocturna sigue siendo la luz; incluso un iluminador IR débil da un resultado más claro que subir la ganancia. Puedes añadir el código justo después de llamar a kameraBaslat() y, si quieres, medir la luz ambiente con una LDR para pasar automáticamente de los ajustes de día a los de noche.
Sus límites como cámara de seguridad
- No graba vídeo: Este sistema solo envía fotos. Para grabación continua o revisar lo ocurrido necesitas una cámara de seguridad de verdad.
- Hay retraso: 2–4 segundos con la placa encendida y 6–10 segundos al despertar del deep sleep; alguien que pase deprisa puede haber salido ya del encuadre.
- El PIR no ve a través del cristal: El vidrio no deja pasar la radiación térmica infrarroja. Un sensor detrás de una ventana no detecta el movimiento exterior, aunque el sol y los radiadores sí pueden dispararlo en falso.
- Depende de internet: Si se cae el Wi-Fi o internet, la foto no se envía. Para un uso crítico, guardar también la foto en una tarjeta SD sirve de copia de seguridad.
- No tiene protección física: La placa se desmonta o se apaga con facilidad y en exteriores le afectan la humedad y el sol; si la usas fuera necesitas una caja estanca y una alimentación que aguante la temperatura.
- La calidad de imagen es limitada: La OV2640 de 2 MP no basta para reconocer caras ni leer matrículas a unos metros; las lentes gran angular amplían el encuadre pero reducen el detalle.
- Normas de privacidad: Apunta la cámara a tu propio espacio; antes de grabar el jardín del vecino, zonas comunes o la calle, revisa las normas legales de tu lugar de residencia.
Ideas de proyectos con ESP32-CAM y Telegram
- Cámara de timbre: Conecta el pulsador del timbre a GPIO 13 para que al pulsarlo fotografíe la entrada y te lo envíe; mira cómo conectar un pulsador para el cableado.
- Aviso de buzón: Coloca un interruptor reed en la tapa para que al abrirla la placa despierte y envíe una foto del interior.
- Cámara para nido o mascota: Que tome una foto cuando haya movimiento y lo revises durante el día con /foto.
- Ojos para un sistema de alarma: Haz que un sistema de alarma con Arduino avise a la ESP32-CAM al dispararse para que fotografíe el lugar.
- Seguimiento de plantas e invernadero: Que despierte por temporizador cada hora para enviar una foto, y lleva tus mediciones a un panel con MQTT y Home Assistant.
- Grupo familiar: Añade el bot a un grupo de Telegram y usa el ID del grupo para que la foto llegue a todos a la vez.
Errores frecuentes y soluciones
| Síntoma | Causa probable | Solución |
|---|---|---|
| Llega el mensaje, pero no la foto | Fotograma demasiado grande, PSRAM desactivada o subida cortada | Elige la placa AI Thinker ESP32-CAM, baja la resolución a SVGA, mejora la señal Wi-Fi |
| El bot no responde a ningún comando | Token o CHAT_ID incorrectos, o nunca escribiste /start al bot | Vuelve a copiar el token, comprueba tu ID con @userinfobot, escribe /start a tu bot |
| Llegan fotos de movimiento sin motivo | El PIR capta la señal de la antena Wi-Fi o una fuente de calor | Separa el PIR 10–20 cm de la placa, pon 100 nF entre VCC y GND, no apuntes el sensor al sol ni a radiadores |
| La placa vuelve a despertar nada más dormirse | La salida del PIR sigue en HIGH al dormir | Espera a que el pin vuelva a LOW antes de dormir, pon al mínimo el potenciómetro de tiempo del HC-SR501 |
| El flash brilla débilmente durante el sueño | GPIO 4 queda al aire | Escribe LOW y llama a rtc_gpio_hold_en(GPIO_NUM_4) antes de dormir |
| La foto sale oscura o verdosa | Los primeros fotogramas justo tras iniciar la cámara | Toma y descarta 2–3 fotogramas antes de la foto real |
| La placa se reinicia al enviar una foto | Brownout: la alimentación no aguanta la corriente del Wi-Fi y el flash | Usa un adaptador de 5 V / 2 A, cables cortos y un condensador de 470–1000 µF entre 5V y GND |
Junto con Telegram, la ESP32-CAM convierte una placa económica en un ojo inteligente que envía fotos a tu móvil. Monta primero el sistema con el código siempre encendido y ajusta el PIR; después, si lo necesitas, pasa a la versión con deep sleep. En la app BlueGrays encontrarás la alarma de movimiento por Telegram con PIR, zumbador y LED, el deep sleep del ESP32 y la iluminación automática con PIR, con esquemas de conexión y código listo paso a paso.
Preguntas frecuentes
¿Cómo enviar una foto de la ESP32-CAM a Telegram?
Creas un bot con BotFather para obtener un token, instalas la biblioteca UniversalTelegramBot y subes el búfer de la cámara al método sendPhoto de Telegram con la función sendPhotoByBinary(). La biblioteca envía la foto por HTTPS en formato multipart, como una subida de archivo en el navegador. En el código solo tienes que cambiar los datos del Wi-Fi, el token y tu chat ID.
¿A qué pin de la ESP32-CAM se conecta un sensor PIR?
Si no usas tarjeta SD, GPIO 13 es la opción más segura: no afecta al arranque y, al ser un pin RTC, puede despertar la placa del deep sleep. Conecta VCC del PIR al pin 5V, GND a GND y OUT a GPIO 13. Evita GPIO 12; si está en HIGH al arrancar, la placa no inicia.
¿Cuánto dura una ESP32-CAM con batería?
Con el Wi-Fi siempre encendido, una 18650 de 2600 mAh dura unas 9 horas. Si usas deep sleep y solo despiertas con movimiento, sube a unos 10 días; incluso dormida, la placa consume unos 6 mA por el regulador y la PSRAM. Para meses de autonomía necesitas un circuito de autorretención que corte por completo la alimentación.
¿La ESP32-CAM puede ver de noche?
El filtro IR-cut de los módulos estándar bloquea la luz infrarroja, así que a oscuras solo obtienes imagen hasta 1–2 metros con el flash blanco. Con un módulo OV2640 sin filtro (night vision) y un iluminador IR de 850 nm puedes sacar fotos nítidas en blanco y negro en la oscuridad. Subir el techo de ganancia en los ajustes también ayuda, pero añade grano.
¿La ESP32-CAM puede enviar vídeo a Telegram?
En la práctica, no. La ESP32-CAM genera fotogramas JPEG sueltos y no tiene potencia ni memoria para convertirlos en un vídeo MP4. Enviar varias fotos seguidas o ver la imagen en directo en tu red con CameraWebServer son opciones más realistas.
¿Se puede enviar la foto a varias personas?
Sí. Si añades el bot a un grupo de Telegram y usas como CHAT_ID el ID del grupo, que empieza con un signo menos, la foto llega a todos los miembros a la vez. También puedes poner varios chat ID en un array y enviar la foto a cada uno por turnos; cada envío es una subida aparte, así que tarda más.
Proyectos para hacer después de leer esto
Ver todos los proyectos
Sistema de Alarma de Movimiento ESP32 (PIR + Buzzer + LED + Telegram)
Un sistema inteligente de alarma de seguridad que activa el buzzer y el LED cuando el sensor PIR detecta movimiento, y envía simultáneamente una notificación instantánea al propietario vía Telegram. Cuenta con un tiempo de espera de 30 segundos para limitar la frecuencia de las alarmas repetidas; este mecanismo no comprueba la validez de las detecciones del sensor PIR ni elimina las falsas detecciones. Ideal para seguridad del hogar, vigilancia de garajes, almacenes o salas de trabajo. Con ESP32

Iluminación automática con el sensor de movimiento PIR del ESP32
En este proyecto, utilizaremos el sensor de movimiento PIR HC-SR501 para detectar a las personas que entren en la habitación y encender el LED automáticamente. Controlaremos el tiempo de encendido del LED con millis(); cada lectura HIGH del sensor actualizará el instante del último movimiento, y el LED se apagará cuando hayan transcurrido al menos 10 segundos desde la última lectura HIGH. El código actual también espera 50 ms en cada iteración mediante delay(50), por lo que el bucle de la ap

Modo de suspensión profunda del ESP32 — Ahorro de batería (OLED + contador en RTC RAM)
El modo «Deep Sleep» del ESP32 reduce el consumo de corriente a unos 10 µA al desactivar la CPU y la mayoría de los periféricos, lo cual es de vital importancia para los dispositivos IoT que funcionan con pilas. En este proyecto utilizamos tres fuentes de activación diferentes: el temporizador (15 s), un pin externo (EXT0 — botón GPIO 33) y el sensor táctil (T0/GPIO 4). Las variables marcadas con RTC_DATA_ATTR se conservan durante el modo Deep Sleep: el contador de arranque, el contador de act

Iluminación Inteligente ESP32 (PIR + LDR + Relé + OLED)
¡Convierte tu pasillo, baño o despensa en iluminación inteligente con ESP32! Utilizando un sensor de movimiento PIR, un sensor de luz LDR y un módulo de relé, construimos el escenario clásico "la luz se enciende cuando se detecta movimiento en la oscuridad" — si el ambiente ya está iluminado, el movimiento NO activa la luz, ahorrando energía durante el día. La luz permanece encendida durante 30 segundos; si el movimiento continúa, el temporizador se reinicia y la luz permanece encendida más tiem

Conexión Wi-Fi STA con ESP32 (con pantalla LCD)
En este proyecto, conectamos el ESP32 a la red Wi-Fi doméstica en modo Station (STA) de forma segura y tolerante a fallos. Separamos el SSID y la contraseña en el archivo secrets.h para evitar que se filtren a Git, configuramos un mecanismo de reconexión automática y mostramos el nivel de señal RSSI y la dirección IP en una pantalla LCD I2C. El LED interno actúa como indicador de estado.

ESP32 mDNS: Acceso al dispositivo mediante bluegrays-esp.local (con pantalla LCD)
En este proyecto, al añadir el servicio mDNS (Multicast DNS) al ESP32, permitimos el acceso mediante un nombre de host legible, como bluegrays-esp.local, en lugar de una dirección IP. Aunque la IP asignada por el DHCP cambie, siempre podrás acceder al dispositivo con el mismo nombre. Además, al ejecutar un pequeño servidor web en él, enviamos comandos de control de los LED desde el navegador mediante la dirección http://bluegrays-esp.local/. En la pantalla LCD I2C se muestran el nombre de ho

Detector de movimiento con sensor de movimiento
Un vigilante sencillo que te avisa cuando alguien entra en la habitación. El sensor de movimiento capta el calor que desprende el cuerpo humano, y el Arduino mantiene el LED encendido mientras hay mov
Más artículos
App BlueGrays
Monta este circuito paso a paso en tu teléfono
Esquema, código y lista de piezas en una sola pantalla. Más de 200 proyectos de Arduino, ESP32 y Pico listos para montar.
Escanea con tu teléfono
Apunta la cámara al código y se abrirá la tienda de apps de tu teléfono.
o ábrela en la tienda

