Guías · 12 min de lectura

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.

Módulo de cámara ESP32-CAM para un sistema de fotos por Telegram
ESP32-CAM: cámara OV2640, LED de flash y Wi-Fi en una sola placa. Con un sensor PIR puede enviar una foto a Telegram al detectar movimiento.

¿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.

PiezaCaracterísticaNota
ESP32-CAM (AI-Thinker)OV2640, 4 MB de PSRAMLa versión con PSRAM maneja fotos XGA sin problema
ESP32-CAM-MB o conversor USB-TTLCH340 / CP2102Para cargar el código; la placa no tiene USB
Sensor PIRHC-SR501 (5 V) o AM312 (3,3 V)La salida OUT es de 3,3 V y se conecta directamente al ESP32
Alimentación5 V, al menos 2 AEl Wi-Fi y el flash consumen mucha corriente a la vez
Condensador electrolítico470–1000 µFEntre 5V y GND, cerca de la placa
OpcionalBatería 18650 + elevador a 5 V, iluminador IR de 850 nmPara 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.

PinSin tarjeta SD¿Despierta del deep sleep?Nota
GPIO 13LibreSí (ext0)La mejor opción para el PIR
GPIO 14LibreSíPuede mostrar una breve señal PWM al arrancar; sin problema como entrada
GPIO 15LibreSíPin de arranque; en LOW silencia los mensajes de arranque
GPIO 2LibreSíPin de arranque; no debe estar en HIGH al cargar
GPIO 12LibreSíSi está en HIGH al arrancar, la placa no inicia; no lo uses
GPIO 4LED de flashSíReservado para el flash; no lo uses para otra cosa
GPIO 3 (U0R)RX serieNoSolo si no necesitas el puerto serie
GPIO 16PSRAMNoNo lo uses nunca
Pin del PIRPin de la ESP32-CAMDescripción
VCC5VEl HC-SR501 necesita al menos 4,5 V; con un AM312 usa 3V3
OUTGPIO 133,3 V cuando hay movimiento, 0 V cuando no
GNDGNDTierra común
El pin de la placa marcado como VCC no es una entrada, sino la salida del regulador de 3,3 V; para alimentar el PIR o la placa lleva siempre los 5 V al pin 5V. No conectes nunca el PIR a GPIO 12: si el sensor da HIGH al arrancar, el ESP32 intenta hacer funcionar su memoria flash a una tensión incorrecta y la placa no arranca.

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

  1. Abre @BotFather en Telegram, escribe /newbot y dale a tu bot un nombre y un nombre de usuario que termine en bot.
  2. Copia el token con formato 1234567890:AAE... que te da BotFather; irá en la línea BOT_TOKEN del código.
  3. Para conocer tu número de usuario, escribe /start a @userinfobot; el valor Id que devuelve es tu CHAT_ID.
  4. Abre tu propio bot y escribe /start una vez; Telegram no permite que un bot escriba a alguien con quien nunca ha hablado.
  5. En el Gestor de bibliotecas del Arduino IDE instala UniversalTelegramBot (Brian Lough) y ArduinoJson.
  6. En Herramientas > Placa elige AI Thinker ESP32-CAM y comprueba que el Partition Scheme sea Huge APP (3MB No OTA/1MB SPIFFS).
El token es la contraseña de tu bot: borra esa línea siempre antes de compartir tu código o subirlo a cualquier sitio. Si crees que se ha filtrado, consigue uno nuevo con el comando /revoke en BotFather. Explicamos con más detalle la creación del bot, el chat ID y el certificado en Bot de Telegram con ESP32.

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

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}
ParteQué 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.

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}

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.

EscenarioCorriente 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.

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

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íntomaCausa probableSolución
Llega el mensaje, pero no la fotoFotograma demasiado grande, PSRAM desactivada o subida cortadaElige 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 comandoToken o CHAT_ID incorrectos, o nunca escribiste /start al botVuelve a copiar el token, comprueba tu ID con @userinfobot, escribe /start a tu bot
Llegan fotos de movimiento sin motivoEl PIR capta la señal de la antena Wi-Fi o una fuente de calorSepara 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 dormirseLa salida del PIR sigue en HIGH al dormirEspera 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ñoGPIO 4 queda al aireEscribe LOW y llama a rtc_gpio_hold_en(GPIO_NUM_4) antes de dormir
La foto sale oscura o verdosaLos primeros fotogramas justo tras iniciar la cámaraToma y descarta 2–3 fotogramas antes de la foto real
La placa se reinicia al enviar una fotoBrownout: la alimentación no aguanta la corriente del Wi-Fi y el flashUsa un adaptador de 5 V / 2 A, cables cortos y un condensador de 470–1000 µF entre 5V y GND
Al enviar a un grupo, el ID del grupo empieza con un signo menos (p. ej. -1001234567890); escríbelo con el signo en la línea CHAT_ID. Para que el bot pueda leer los mensajes del grupo puede que tengas que desactivar /setprivacy en BotFather o enviar los comandos como /foto@nombre_bot; el código ya elimina esos sufijos.

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.

Ver todos los proyectos
Sistema de Alarma de Movimiento ESP32 (PIR + Buzzer + LED + Telegram)
#145Avanzado
Premium

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

0.0(0)
9
Iluminación automática con el sensor de movimiento PIR del ESP32
#121Intermedio
Premium

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

4.4(24)
38
Modo de suspensión profunda del ESP32 — Ahorro de batería (OLED + contador en RTC RAM)
#135Avanzado
Premium

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

4.3(10)
16
Iluminación Inteligente ESP32 (PIR + LDR + Relé + OLED)
#141Avanzado
Premium

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

4.3(6)
11
Conexión Wi-Fi STA con ESP32 (con pantalla LCD)
#127Avanzado

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.

4.4(21)
34
ESP32 mDNS: Acceso al dispositivo mediante bluegrays-esp.local (con pantalla LCD)
#131Avanzado
Premium

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

4.8(14)
23
Detector de movimiento con sensor de movimiento
#24Principiante
Premium

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

4.6(47)
67