Aquí voy a documentar mi experiencia programando la ESP32 CAM. Tal vez traiga guías más adelante, por ahora voy a contarte cómo usarla usando VSCode con PlatformIO.
Primero veamos algunas fotos. Yo tengo la ESP32CAM que viene con su sombrero para programarse y conectar el cable USB. Además tiene la cámara OV2640.
Vista frontal. Se aprecian sus pines, la cámara, el slot para la tarjeta micro SD y su LED que hace de flash.

Si le quitamos su hat y lo separamos se ve así:

También tengo una foto de la ESP32-CAM en su parte trasera:

Programando con VSCode y PlatformIO
Ya he programado la NodeMCU ESP8266 previamente con esta PC así que ya tengo el driver CH340.
Voy a ver si funciona simplemente así o hay que hacer algo adicional.
Cuando lo conecté encendió un LED rojo del hat. Abrí PlatformIO e hice un nuevo proyecto:
- Nombre del proyecto:
hola-esp32cam
- Board: AI Thinker ESP32-CAM
- Framework: Arduino
Ahora tocó esperar a que PlatformIO descargara todo. Se mostró el editor pero me pidió reiniciar vscode.
Hola mundo
#include <Arduino.h>
void setup() {
Serial.println("Hola ESP32-CAM");
}
void loop() {
// put your main code here, to run repeatedly:
}
Me dijo:
* Ejecutando tarea: C:\Users\parzibyte\.platformio\penv\Scripts\platformio.exe run --target upload
Processing esp32cam (platform: espressif32; board: esp32cam; framework: arduino)
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Tool Manager: Installing platformio/tool-mkspiffs @ ~2.230.0
Downloading [####################################] 100%
Unpacking [####################################] 100%
Tool Manager: tool-mkspiffs@2.230.0 has been installed!
Tool Manager: Installing platformio/tool-mkfatfs @ ~2.0.0
Downloading [####################################] 100%
Unpacking [####################################] 100%
Tool Manager: tool-mkfatfs@2.0.1 has been installed!
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32cam.html
PLATFORM: Espressif 32 (6.12.0) > AI Thinker ESP32-CAM
HARDWARE: ESP32 240MHz, 320KB RAM, 4MB Flash
DEBUG: Current (cmsis-dap) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
- framework-arduinoespressif32 @ 3.20017.241212+sha.dcc1105b
- tool-esptoolpy @ 2.40900.250804 (4.9.0)
- tool-mkfatfs @ 2.0.1
- tool-mklittlefs @ 1.203.210628 (2.3)
- tool-mkspiffs @ 2.230.0 (2.30)
- toolchain-xtensa-esp32 @ 8.4.0+2021r2-patch5
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 33 compatible libraries
Scanning dependencies...
No dependencies
Building in release mode
Compiling .pio\build\esp32cam\src\main.cpp.o
Building .pio\build\esp32cam\bootloader.bin
Generating partitions .pio\build\esp32cam\partitions.bin
Traceback (most recent call last):
File "C:\Users\parzibyte\.platformio\packages\tool-esptoolpy\esptool.py", line 41, in <module>
import esptool
File "C:\Users\parzibyte\.platformio\packages\tool-esptoolpy\esptool\__init__.py", line 42, in <module>
from esptool.bin_image import intel_hex_to_bin
File "C:\Users\parzibyte\.platformio\packages\tool-esptoolpy\esptool\bin_image.py", line 16, in <module>
from intelhex import HexRecordError, IntelHex
ModuleNotFoundError: No module named 'intelhex'
Compiling .pio\build\esp32cam\FrameworkArduino\Esp.cpp.o
*** [.pio\build\esp32cam\bootloader.bin] Error 1
========================================================================== [FAILED] Took 13.32 seconds ==========================================================================
* El proceso del terminal "C:\Users\parzibyte\.platformio\penv\Scripts\platformio.exe 'run', '--target', 'upload'" finalizó con el código de salida 1.
* Las tareas reutilizarán el terminal, presione cualquier tecla para cerrarlo.
Intenté:
C:\Users\parzibyte>pip install intelhex
Collecting intelhex
Obtaining dependency information for intelhex from https://files.pythonhosted.org/packages/97/78/79461288da2b13ed0a13deb65c4ad1428acb674b95278fa9abf1cefe62a2/intelhex-2.3.0-py2.py3-none-any.whl.metadata
Downloading intelhex-2.3.0-py2.py3-none-any.whl.metadata (2.7 kB)
Downloading intelhex-2.3.0-py2.py3-none-any.whl (50 kB)
---------------------------------------- 50.9/50.9 kB 57.9 kB/s eta 0:00:00
Installing collected packages: intelhex
Successfully installed intelhex-2.3.0
[notice] A new release of pip is available: 23.2.1 -> 25.2
[notice] To update, run: python.exe -m pip install --upgrade pip
C:\Users\parzibyte>
No funcionó. Intenté ir a donde PIO tiene su entorno y ahí instalé:
C:\Users\parzibyte\.platformio\penv\Scripts>python -m pip install intelhex
Collecting intelhex
Downloading intelhex-2.3.0-py2.py3-none-any.whl.metadata (2.7 kB)
Downloading intelhex-2.3.0-py2.py3-none-any.whl (50 kB)
Installing collected packages: intelhex
Successfully installed intelhex-2.3.0
C:\Users\parzibyte\.platformio\penv\Scripts>
Y con eso funcionó porque luego me dijo:
Processing esp32cam (platform: espressif32; board: esp32cam; framework: arduino)
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/espressif32/esp32cam.html
PLATFORM: Espressif 32 (6.12.0) > AI Thinker ESP32-CAM
HARDWARE: ESP32 240MHz, 320KB RAM, 4MB Flash
DEBUG: Current (cmsis-dap) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES:
- framework-arduinoespressif32 @ 3.20017.241212+sha.dcc1105b
- tool-esptoolpy @ 2.40900.250804 (4.9.0)
- tool-mkfatfs @ 2.0.1
- tool-mklittlefs @ 1.203.210628 (2.3)
- tool-mkspiffs @ 2.230.0 (2.30)
- toolchain-xtensa-esp32 @ 8.4.0+2021r2-patch5
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 33 compatible libraries
Scanning dependencies...
No dependencies
Building in release mode
Building .pio\build\esp32cam\bootloader.bin
esptool.py v4.9.0
Creating esp32 image...
Merged 1 ELF section
Successfully created esp32 image.
Compiling .pio\build\esp32cam\FrameworkArduino\HardwareSerial.cpp.o
Compiling .pio\build\esp32cam\FrameworkArduino\IPAddress.cpp.o
Compiling .pio\build\esp32cam\FrameworkArduino\IPv6Address.cpp.o
Compiling .pio\build\esp32cam\FrameworkArduino\MD5Builder.cpp.o
Compiling .pio\build\esp32cam\FrameworkArduino\Print.cpp.o
Compiling .pio\build\esp32cam\FrameworkArduino\Stream.cpp.o
Compiling .pio\build\esp32cam\FrameworkArduino\StreamString.cpp.o
Compiling .pio\build\esp32cam\FrameworkArduino\Tone.cpp.o
Compiling .pio\build\esp32cam\FrameworkArduino\USB.cpp.o
Compiling .pio\build\esp32cam\FrameworkArduino\USBCDC.cpp.o
Compiling .pio\build\esp32cam\FrameworkArduino\USBMSC.cpp.o
Compiling .pio\build\esp32cam\FrameworkArduino\WMath.cpp.o
Compiling .pio\build\esp32cam\FrameworkArduino\WString.cpp.o
Compiling .pio\build\esp32cam\FrameworkArduino\base64.cpp.o
Compiling .pio\build\esp32cam\FrameworkArduino\cbuf.cpp.o
Compiling .pio\build\esp32cam\FrameworkArduino\esp32-hal-adc.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\esp32-hal-bt.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\esp32-hal-cpu.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\esp32-hal-dac.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\esp32-hal-gpio.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\esp32-hal-i2c-slave.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\esp32-hal-i2c.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\esp32-hal-ledc.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\esp32-hal-matrix.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\esp32-hal-misc.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\esp32-hal-psram.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\esp32-hal-rgb-led.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\esp32-hal-rmt.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\esp32-hal-sigmadelta.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\esp32-hal-spi.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\esp32-hal-time.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\esp32-hal-timer.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\esp32-hal-tinyusb.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\esp32-hal-touch.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\esp32-hal-uart.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\firmware_msc_fat.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\libb64\cdecode.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\libb64\cencode.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\main.cpp.o
Compiling .pio\build\esp32cam\FrameworkArduino\stdlib_noniso.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\wiring_pulse.c.o
Compiling .pio\build\esp32cam\FrameworkArduino\wiring_shift.c.o
Archiving .pio\build\esp32cam\libFrameworkArduino.a
Indexing .pio\build\esp32cam\libFrameworkArduino.a
Linking .pio\build\esp32cam\firmware.elf
Retrieving maximum program size .pio\build\esp32cam\firmware.elf
Checking size .pio\build\esp32cam\firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [= ] 6.5% (used 21192 bytes from 327680 bytes)
Flash: [= ] 8.0% (used 251513 bytes from 3145728 bytes)
Building .pio\build\esp32cam\firmware.bin
esptool.py v4.9.0
Creating esp32 image...
Merged 2 ELF sections
Successfully created esp32 image.
Configuring upload protocol...
AVAILABLE: cmsis-dap, esp-bridge, esp-prog, espota, esptool, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa
CURRENT: upload_protocol = esptool
Looking for upload port...
Auto-detected: COM6
Uploading .pio\build\esp32cam\firmware.bin
esptool.py v4.9.0
Serial port COM6
Connecting....
Chip is ESP32-D0WD-V3 (revision v3.1)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme None
Crystal is 40MHz
MAC: 68:25:dd:2e:44:e8
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 460800
Changed.
Configuring flash size...
Flash will be erased from 0x00001000 to 0x00005fff...
Flash will be erased from 0x00008000 to 0x00008fff...
Flash will be erased from 0x0000e000 to 0x0000ffff...
Flash will be erased from 0x00010000 to 0x0004dfff...
SHA digest in image updated
Compressed 17536 bytes to 12202...
Writing at 0x00001000... (100 %)
Wrote 17536 bytes (12202 compressed) at 0x00001000 in 0.4 seconds (effective 348.1 kbit/s)...
Hash of data verified.
Compressed 3072 bytes to 137...
Writing at 0x00008000... (100 %)
Wrote 3072 bytes (137 compressed) at 0x00008000 in 0.1 seconds (effective 472.4 kbit/s)...
Hash of data verified.
Compressed 8192 bytes to 47...
Writing at 0x0000e000... (100 %)
Wrote 8192 bytes (47 compressed) at 0x0000e000 in 0.1 seconds (effective 1308.3 kbit/s)...
Hash of data verified.
Compressed 251872 bytes to 138607...
Writing at 0x00010000... (11 %)
Writing at 0x0001c617... (22 %)
Writing at 0x00024cb2... (33 %)
Writing at 0x00029f53... (44 %)
Writing at 0x0002f462... (55 %)
Writing at 0x00037b04... (66 %)
Writing at 0x00040108... (77 %)
Writing at 0x00045758... (88 %)
Writing at 0x0004adf7... (100 %)
Wrote 251872 bytes (138607 compressed) at 0x00010000 in 3.5 seconds (effective 568.5 kbit/s)...
Hash of data verified.
Leaving...
Hard resetting via RTS pin...
Y ya tenía mi Hola mundo:

Parpadear LED
Abrimos src/main.cpp
y encontramos el siguiente código básico:
#include <Arduino.h>
#define LED_FLASH 4
void setup() {
pinMode(LED_FLASH, OUTPUT);
}
void loop() {
digitalWrite(LED_FLASH, HIGH);
delay(500);
digitalWrite(LED_FLASH, LOW);
delay(500);
}
Monitor serial
Recomiendo que una vez que el código haya sido cargado se reinicie. Si no, muestra caracteres extraños.
Explicado con más detenimiento:
- Carga el código
- Espera a que el código termine de cargar
- Resetea la tarjeta
- Abre el monitor serial
- Vuelve a resetear la tarjeta
#include <Arduino.h>
#define LED_FLASH 4
void setup() {
pinMode(LED_FLASH, OUTPUT);
Serial.begin(115200);
}
void loop() {
Serial.println("Hola mundo");
digitalWrite(LED_FLASH, HIGH);
delay(500);
digitalWrite(LED_FLASH, LOW);
delay(500);
}
Tomar foto y guardar en Micro SD
Formateamos la memoria como FAT32 y dejamos las otras opciones sin modificar.
#include "esp_camera.h"
#include "FS.h"
#include "SD_MMC.h"
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
void takePhoto();
void setup() {
Serial.begin(115200);
delay(2000);
// Config cámara
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;
// Resolución
config.frame_size = FRAMESIZE_UXGA; // Máximo para mi cámara
config.jpeg_quality = 5; // calidad. Mejor calidad entre menor número
config.fb_count = 1;
// Inicializa cámara
if (esp_camera_init(&config) != ESP_OK) {
Serial.println("Error al iniciar cámara");
return;
}
// Inicializa SD
if (!SD_MMC.begin()) {
Serial.println("Error al montar SD");
return;
}
Serial.println("SD lista");
takePhoto();
}
void loop() {
}
void takePhoto() {
camera_fb_t * fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Error al tomar foto");
return;
}
File file = SD_MMC.open("/foto.jpg", FILE_WRITE);
if (!file) {
Serial.println("No se pudo abrir archivo en SD");
esp_camera_fb_return(fb);
return;
}
file.write(fb->buf, fb->len);
file.close();
esp_camera_fb_return(fb);
Serial.println("Foto guardada en /foto.jpg");
}
Fotos numeradas
Tomar varias fotos y numerarlas guardándolas en la Micro SD. (El contador se reinicia cuando reinicias la ESP32-CAM):
#include "esp_camera.h"
#include "FS.h"
#include "SD_MMC.h"
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
void takePhoto(int);
void setup()
{
Serial.begin(115200);
delay(2000);
// Config cámara
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;
// Resolución
config.frame_size = FRAMESIZE_UXGA; // Máximo para mi cámara
config.jpeg_quality = 5; // calidad. Mejor calidad entre menor número
config.fb_count = 1;
// Inicializa cámara
if (esp_camera_init(&config) != ESP_OK)
{
Serial.println("Error al iniciar cámara");
return;
}
// Inicializa SD
if (!SD_MMC.begin())
{
Serial.println("Error al montar SD");
return;
}
Serial.println("SD lista");
// takePhoto();
}
int numeroFoto = 1;
void loop()
{
takePhoto(numeroFoto);
numeroFoto++;
delay(1000);
}
void takePhoto(int numero)
{
camera_fb_t *fb = esp_camera_fb_get();
if (!fb)
{
Serial.println("Error al tomar foto");
return;
}
File file = SD_MMC.open("/foto_" + String(numero) + ".jpg", FILE_WRITE);
if (!file)
{
Serial.println("No se pudo abrir archivo en SD");
esp_camera_fb_return(fb);
return;
}
file.write(fb->buf, fb->len);
file.close();
esp_camera_fb_return(fb);
Serial.println("Foto guardada");
}
Tomar foto con ESP32-CAM y enviar a PHP
El código de la cámara es el siguiente. No olvides cambiar la IP y las credenciales de la red respectivamente.
#include "esp_camera.h"
#include <WiFi.h>
#include <HTTPClient.h>
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
const char *ssid = "red";
const char *password = "password";
void takePhoto(int);
void setup()
{
Serial.begin(115200);
delay(2000);
// Config cámara
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;
// Resolución
config.frame_size = FRAMESIZE_UXGA; // Máximo para mi cámara
config.jpeg_quality = 5; // calidad. Mejor calidad entre menor número
config.fb_count = 1;
// Inicializa cámara
if (esp_camera_init(&config) != ESP_OK)
{
Serial.println("Error al iniciar cámara");
return;
}
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi conectada");
// takePhoto();
}
int numeroFoto = 1;
void loop()
{
takePhoto(numeroFoto);
numeroFoto++;
delay(1000);
}
void takePhoto(int numero)
{
camera_fb_t *fb = esp_camera_fb_get();
if (!fb)
{
Serial.println("Error al tomar foto");
return;
}
HTTPClient http;
http.begin("http://192.168.1.83/recibir_fotos/index.php");
String boundary = "esp32boundary";
String header = "--" + boundary + "\r\nContent-Disposition: form-data; name=\"foto\"; filename=\"foto.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n";
String footer = "\r\n--" + boundary + "--\r\n";
int totalLen = header.length() + fb->len + footer.length();
uint8_t *payload = new uint8_t[totalLen];
memcpy(payload, header.c_str(), header.length());
memcpy(payload + header.length(), fb->buf, fb->len);
memcpy(payload + header.length() + fb->len, footer.c_str(), footer.length());
http.addHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
int code = http.POST(payload, totalLen);
Serial.println(code);
delete[] payload;
esp_camera_fb_return(fb);
http.end();
Serial.println("http end");
}
Recibir en PHP:
<?php
# La carpeta en donde guardaremos los archivos, en este caso es "subidas" pero podría ser
# cualquier otro, incluso podría ser aquí mismo sin subcarpetas
$rutaDeSubidas = __DIR__ . "/subidas2";
# Crear si no existe
if (!is_dir($rutaDeSubidas)) {
mkdir($rutaDeSubidas, 0777, true);
}
# Tomar el archivo. Recordemos que "archivo" es el atributo "name" de nuestro input
$informacionDelArchivo = $_FILES["foto"];
# La ubicación en donde PHP lo puso
$ubicacionTemporal = $informacionDelArchivo["tmp_name"];
#Nota: aquí tomamos el nombre que trae, pero recomiendo renombrarlo a otra cosa usando, por ejemplo, uniqid
$nombreArchivo = date("Y-m-d_H_i_s_") . $informacionDelArchivo["name"];
$nuevaUbicacion = $rutaDeSubidas . "/" . $nombreArchivo;
# Mover
$resultado = move_uploaded_file($ubicacionTemporal, $nuevaUbicacion);
if ($resultado === true) {
echo "Archivo subido correctamente";
} else {
echo "Error al subir archivo";
}
Errores
Cuando dice cam_hal: EV-EOF-OVF era porque le puse calidad en 1 así:
config.frame_size = FRAMESIZE_UXGA; // Máximo para mi cámara
config.jpeg_quality = 1; // calidad. Mejor calidad entre menor número
config.fb_count = 1;
Arreglado:
config.frame_size = FRAMESIZE_UXGA; // Máximo para mi cámara
config.jpeg_quality = 5; // calidad. Mejor calidad entre menor número
config.fb_count = 1;
Enviar a Telegram sin librerías
Con el siguiente código la ESP32-CAM se va a conectar a internet, sincronizar su fecha y hora con un servidor NTP y enviar fotos indefinidamente cada que se pueda.
También va a encender el flash desde las 18:00 hasta las 6:00. Tomará fotos durante el día y la noche.
No olvides configurar:
- Tu token de Telegram
- Credenciales de acceso a tu red Wi-Fi
- Id de chat al que se van a enviar las fotos
#include "esp_camera.h"
#include <WiFi.h>
#include <HTTPClient.h>
#include <time.h>
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
#define FLASH_PIN 4
const char *ssid = "nombre de tu red";
const char *password = "contraseña de tu red";
const char *ntpServer = "pool.ntp.org";
const long gmtOffset_sec = -21600; // Zona horaria para CDMX
const int daylightOffset_sec = 0;
void takePhoto();
void setup()
{
pinMode(FLASH_PIN, OUTPUT);
digitalWrite(FLASH_PIN, LOW);
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
Serial.begin(115200);
delay(2000);
// Config cámara
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;
// Resolución
config.frame_size = FRAMESIZE_UXGA; // Máximo para mi cámara
config.jpeg_quality = 5; // calidad. Mejor calidad entre menor número
config.fb_count = 1;
// Inicializa cámara
if (esp_camera_init(&config) != ESP_OK)
{
Serial.println("Error al iniciar cámara");
return;
}
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi conectada");
// takePhoto();
}
void loop()
{
takePhoto();
}
WiFiClientSecure client;
HTTPClient http;
void takePhoto()
{
if (WiFi.status() != WL_CONNECTED)
{
Serial.println("WiFi perdido, reconectando...");
WiFi.reconnect();
delay(2000);
return;
}
bool usarFlash = false;
String fecha = "sin fecha";
struct tm timeinfo;
if (getLocalTime(&timeinfo))
{
char buffer[25];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &timeinfo);
fecha = String(buffer);
int h = timeinfo.tm_hour;
if (h >= 19 || h < 6)
usarFlash = true;
}
sensor_t *s = esp_camera_sensor_get();
if (usarFlash)
{
digitalWrite(FLASH_PIN, HIGH);
delay(50);
}
camera_fb_t *fb = esp_camera_fb_get();
if (!fb)
{
Serial.println("Error al tomar foto");
return;
}
if (usarFlash)
{
digitalWrite(FLASH_PIN, LOW);
}
client.setInsecure();
http.begin(client, "https://api.telegram.org/botAQUÍ_VA_TU_TOKEN/sendPhoto");
http.setTimeout(5000);
String boundary = "esp32boundary";
String body = "";
body.reserve(1024);
body += "--" + boundary + "\r\n";
body += "Content-Disposition: form-data; name=\"chat_id\"\r\n\r\nAQUÍ_VA_EL_ID_CHAT\r\n";
body += "--" + boundary + "\r\n";
body += "Content-Disposition: form-data; name=\"caption\"\r\n\r\n" + fecha + "\n#ESP32CAM\r\n";
body += "--" + boundary + "\r\n";
body += "Content-Disposition: form-data; name=\"photo\"; filename=\"foto.jpg\"\r\n";
body += "Content-Type: image/jpeg\r\n\r\n";
String footer = "\r\n--" + boundary + "--\r\n";
int totalLen = body.length() + fb->len + footer.length();
uint8_t *payload = new uint8_t[totalLen];
memcpy(payload, body.c_str(), body.length());
memcpy(payload + body.length(), fb->buf, fb->len);
memcpy(payload + body.length() + fb->len, footer.c_str(), footer.length());
http.addHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
int code = http.POST(payload, totalLen);
Serial.println(code);
delete[] payload;
esp_camera_fb_return(fb);
http.end();
Serial.println("http end");
}
Para probar si funcionaba la puse a monitorear un árbol falso pimentero que tengo en una maceta. En total me envió 38,847 fotos a lo largo de una semana.

Puede que te parezcan muchas fotos pero en realidad debió enviarme muchas más, solo que no sé si fue culpa del internet lento que tengo o de alguna optimización que debo realizar.
Aquí te dejo con el timelapse del crecimiento de mi pirul por 8 días XD
Primera foto fue tomada el 2025-08-31 14:43:31
y la última fue tomada el 2025-09-07 14:43:44
. Aunque tenía varias fotos de todos
los días solo agarré una foto distinta en cada día pero en la misma hora.
Conclusión
Todo esto fueron pruebas rápidas. Pido disculpas por el código desordenado y mal comentado, tal vez lo arregle cuando haga la guía de cada cosa por separado.
Esta es mi experiencia con la ESP32-CAM, la verdad es que me gustó muchísimo pues tiene una buena calidad, el precio del dispositivo es barato y ya vi que puede trabajar al menos durante 8 días completos sin descanso.
Tal vez en el futuro intente crear un codificador de video, stremear la cámara, guardar las fotos en un servidor y procesarlas ahí aprovechando que el servidor tiene más recursos, detectar rostros, hacer un timbre, etcétera.