¿Cuántas veces te ha llegado el aviso de "intentamos entregar tu pedido" justo el día que estabas en casa con audífonos puestos? Un timbre común solo sirve si alguien lo escucha. Este proyecto le da al timbre de la puerta tres cosas que no tiene: una cámara que saca una foto de quien toca, un aviso al celular con esa foto y la posibilidad de contestarle a la visita con un mensaje de voz grabado, estés en el living o en la oficina.

Al final vas a tener un timbre inalámbrico armado con un ESP32-CAM, un amplificador I2S MAX98357A y un parlante, y vas a entender tres técnicas que sirven para muchos otros proyectos: guardar audio dentro de la memoria flash del microcontrolador, reproducirlo por I2S sin pelear con la cámara y reaccionar a botones remotos que llegan por MQTT.

Cómo funciona el timbre, de punta a punta

Antes de soldar nada conviene tener clara la secuencia completa, porque casi todos los problemas de este proyecto aparecen cuando dos de estas etapas se pisan.

  1. La visita aprieta el botón. El pulsador está en GPIO 13 con la resistencia pull up interna activada, así que en reposo el pin lee HIGH y al apretar baja a LOW. Una interrupción marca la hora del flanco y el loop() confirma 50 ms después que el botón siga apretado (el antirrebote).
  2. Suena el aviso en la puerta. El ESP32-CAM reproduce desde la flash el mensaje "Doorbell ringing, please wait" hacia el MAX98357A, que mueve el parlante de 8 Ω que queda afuera.
  3. La cámara saca la foto. Se ajustan balance de blancos, exposición y ganancia del sensor OV2640, se descartan dos cuadros viejos que quedaron en el búfer y recién ahí se captura un JPEG nuevo.
  4. La foto sale a internet. Se sube al panel de CircuitDigest Cloud y, en paralelo, se manda por HTTPS a su API de mensajería, que la reenvía a tu WhatsApp.
  5. Suena el buzzer de adentro. Tres segundos después de apretar el botón, un buzzer activo en GPIO 12 suena otros tres segundos. Lo maneja un temporizador sin bloqueo, para que el resto del programa siga corriendo.
  6. Tú contestas. En el panel hay cuatro interruptores. Cuando activas uno, llega un mensaje MQTT al ESP32-CAM y este reproduce la respuesta correspondiente por el parlante de la puerta.

Las respuestas que trae el proyecto original, en inglés, son estas:

Botón del panel Lo que escucha la visita
(al tocar el timbre) "Doorbell ringing, please wait"
Entregas y repartidores "Please leave the package by the door. Thank you!"
Ocupado o descansando "We can't come to the door right now. Please check back later."
Ganar tiempo "Please wait! We'll be right there."
No estamos en casa "We're not home right now. Please give us a call!"

Para usarlo en Chile lo lógico es grabar las tuyas en español: "Déjalo en la conserjería, por favor", "Ya voy, espérame un minuto" o "No estamos, llámame al celular". Más abajo está cómo convertir tus propios audios.

Hardware: qué se conecta con qué

El montaje es chico: seis componentes más un par de condensadores.

Componente Especificación Cantidad
Microcontrolador con cámara ESP32-CAM (AI Thinker, OV2640) 1
Amplificador de audio MAX98357A I2S 1
Pulsador momentáneo 1
Parlante 8 Ω 1
Buzzer activo 1
Módulo de entrada de 5 V TP4056 (se usa su conector USB) 1
Condensador electrolítico 100 µF 2

Además necesitas un programador USB serial (FTDI o la base ESP32-CAM MB) para cargar el código, porque el ESP32-CAM no trae USB propio.

Diagrama de conexión del timbre con ESP32-CAM, MAX98357A, buzzer y pulsador

Alimentación y pulsador

  • IN+ del TP4056 va al pin 5V del ESP32-CAM e **IN, ** al GND. En este esquema el TP4056 no carga ninguna batería: se aprovecha su conector USB como entrada de 5 V con pines cómodos para soldar.
  • Un terminal del pulsador va a GPIO 13 y el otro a GND.
  • Un condensador de 100 µF entre 5V y GND y otro de 100 µF entre 3V3 y GND, lo más cerca posible de los pines del módulo.

Esos dos condensadores no son decorativos. Cuando el ESP32 transmite por WiFi pide picos de corriente de varios cientos de miliamperes durante milisegundos, y justo en ese momento el amplificador puede estar sacando audio. Si la fuente o el cable no alcanzan a responder, el voltaje cae, se dispara el detector de caída de tensión (brownout) y el módulo se reinicia. Los condensadores entregan esa corriente de golpe mientras la fuente se pone al día.

Buzzer interior

Pin del buzzer Pin del ESP32-CAM
VCC 3V3
I/O GPIO 12
GND GND

Amplificador y parlante

Pin del MAX98357A Pin del ESP32-CAM Parlante
LRCLK GPIO 14 -
BCLK GPIO 2 -
DIN GPIO 15 -
VO+ - terminal positivo
VO, - terminal negativo

Alimenta el MAX98357A desde los mismos 5 V del sistema (VIN a 5V y GND a GND). A 5 V entrega bastante más potencia que a 3,3 V y el sonido se escucha claro a través de una puerta.

Por qué justo esos pines (y la trampa del GPIO 12)

En el ESP32-CAM casi no quedan pines libres: la cámara se come la mayoría y el lector microSD usa GPIO 2, 4, 12, 13, 14 y 15. Por eso este proyecto no usa la microSD: sus pines quedan tomados por el I2S, el botón y el buzzer, y los audios viven en la flash.

Hay además tres pines "de arranque" (strapping) en la lista: GPIO 2, GPIO 12 y GPIO 15. El chip lee su nivel en el instante en que se enciende para decidir cómo arrancar. El más delicado es GPIO 12: si está en HIGH al encender, el ESP32 configura la flash a 1,8 V y el módulo no arranca o se reinicia en bucle. El código define el buzzer como activo en bajo (BUZZER_ACTIVE_HIGH 0), así que después de arrancar deja el pin en HIGH para mantenerlo apagado, y eso no molesta. El problema aparece si tu módulo de buzzer trae una resistencia pull up hacia VCC: esa resistencia puede subir GPIO 12 durante el encendido. Si el timbre no arranca con el buzzer conectado y sí arranca sin él, ya sabes dónde mirar: cambia el buzzer de pin o quita esa resistencia.

Software: audio, cámara y nube

Los audios van dentro de la flash

Para no depender de una tarjeta SD, las frases se guardan como arreglos de bytes en C dentro de audio_data.h. El proceso del autor fue:

  1. Convertir cada clip a WAV mono, 16 kHz y 16 bits PCM con un conversor de audio en línea.
  2. Pasar cada WAV por la herramienta web FileToCArray, que genera un arreglo en C con todos sus bytes.
  3. Guardar esos arreglos con PROGMEM en audio_data.h, con los nombres voice_doorbell y voice_switch1 a voice_switch4.

Vale la pena hacer la cuenta del espacio: 16.000 muestras por segundo por 2 bytes da 32 KB por cada segundo de audio. Una frase de tres segundos ocupa unos 96 KB, así que las cinco frases entran de sobra en los 4 MB de flash del ESP32-CAM. Si quieres frases más largas, bajar a 8 kHz divide el tamaño por dos y la voz se sigue entendiendo, aunque suena más "telefónica". Si cambias la frecuencia, cambia también AUDIO_SAMPLE_RATE para que coincida.

Un detalle del código: playAudioBuffer() se salta los primeros 44 bytes de cada arreglo. Son la cabecera del archivo WAV (formato, frecuencia, largo), no sonido. Si los mandaras al amplificador escucharías un "clic" al inicio de cada frase.

Configuración y pines

El primer bloque carga las librerías (WiFi, TLS, I2S, cámara y hora por NTP), define las credenciales de la nube y del WiFi, y fija los pines del botón, del buzzer y del amplificador. También declara las claves del panel que el equipo va a escuchar: capture-1 para pedir una foto a distancia y analog-input-1 a analog-input-4 para las cuatro respuestas.

C++
#include <CircuitDigestCloud.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include "driver/i2s.h"
#include "audio_data.h" // Holds voice_doorbell, voice_switch1..4
#include "time.h"       // NTP time sync (needed for TLS handshake to MQTT broker)
// ── Select Board Model ───────────────────────────────────────────────────────
#define CAMERA_MODEL_AI_THINKER
#include "camera.h"
// ── Cloud Credentials ───────────────────────────────────────────────────────
#define WIFI_SSID      "your-Wi-Fi-name"
#define WIFI_PASS      "Your-Wi-Fi-password"
#define DEVICE_ID      "Your-Device-ID"
#define CONNECTION_KEY "Your-Connection-Key"
#define API_KEY        "Your-API-Key"
// ── WhatsApp Alert (Circuit Digest Cloud) ───────────────────────────────────
#define WHATSAPP_PHONE_NUMBER "yourphone number" //91xxxxxxxxxx
#define WHATSAPP_TEMPLATE_ID  "image_capture_alert"
#define WHATSAPP_HOST         "www.circuitdigest.cloud"
// Dashboard Keys
#define REMOTE_CAPTURE "capture-1"
#define SLOT_SWITCH_1  "analog-input-1"
#define SLOT_SWITCH_2  "analog-input-2"
#define SLOT_SWITCH_3  "analog-input-3"
#define SLOT_SWITCH_4  "analog-input-4"
// Pins & Durations
#define DOORBELL_BTN_PIN   13
#define BUZZER_PIN         12
#define DEBOUNCE_MS        50
#define BUZZER_DURATION_MS 3000
#define BUZZER_ACTIVE_HIGH 0
#define BUZZER_ON  (BUZZER_ACTIVE_HIGH ? HIGH : LOW)
#define BUZZER_OFF (BUZZER_ACTIVE_HIGH ? LOW  : HIGH)
#define I2S_LRC  14
#define I2S_BCLK 2
#define I2S_DOUT 15
#define I2S_NUM  I2S_NUM_1
CircuitDigestCloud CDcloud;

Fíjate en I2S_NUM_1. El ESP32 tiene dos periféricos I2S y el controlador de la cámara usa el I2S0 para leer el sensor. Si el audio usara también el I2S0, la cámara y el parlante se pelearían por el mismo hardware. Con el I2S1 cada uno tiene el suyo.

Botón, buzzer y reproducción de audio

La rutina de interrupción onDoorbellButton() va marcada con IRAM_ATTR para que viva en RAM y responda aunque la flash esté ocupada, y solo guarda la hora del flanco: todo el trabajo pesado se hace después, en el loop(). startI2SAudio() configura el I2S1 para audio mono de 16 bits, y stopI2SAudio() lo libera al terminar cada frase.

C++
volatile uint32_t lastEdgeMs = 0;
bool remoteCapture = false;
volatile bool captureBusy = false;
uint32_t buzzerStartTime = 0;
bool buzzerActive = false;
#define BUZZER_DELAY_MS 3000
bool buzzerPending = false;
uint32_t buzzerPendingSince = 0;
volatile int playResponseIndex = 0;
void IRAM_ATTR onDoorbellButton() {
 lastEdgeMs = millis();
}
void triggerBuzzer() {
 digitalWrite(BUZZER_PIN, BUZZER_ON);
 buzzerStartTime = millis();
 buzzerActive = true;
}
bool startI2SAudio() {
 i2s_config_t i2s_config = {
   .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
   .sample_rate = AUDIO_SAMPLE_RATE,
   .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
   .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
   .communication_format = I2S_COMM_FORMAT_STAND_I2S,
   .intr_alloc_flags = 0,
   .dma_buf_count = 8,
   .dma_buf_len = 64,
   .use_apll = false
 };
 i2s_pin_config_t pin_config = {
   .bck_io_num = I2S_BCLK,
   .ws_io_num = I2S_LRC,
   .data_out_num = I2S_DOUT,
   .data_in_num = I2S_PIN_NO_CHANGE
 };
 esp_err_t err = i2s_driver_install(I2S_NUM, &i2s_config, 0, NULL);
 if (err != ESP_OK) return false;
 err = i2s_set_pin(I2S_NUM, &pin_config);
 if (err != ESP_OK) {
   i2s_driver_uninstall(I2S_NUM);
   return false;
 }
 return true;
}
void stopI2SAudio() {
 i2s_driver_uninstall(I2S_NUM);
}
void playAudioBuffer(const unsigned char* audio_arr, size_t arr_len) {
 if (arr_len <= 44) return;
 if (!startI2SAudio()) return;
 size_t bytesWritten = 0;
 const uint8_t* audio_data = audio_arr + 44; // Skip 44-byte WAV header
 size_t audio_len = arr_len - 44;
 i2s_write(I2S_NUM, audio_data, audio_len, &bytesWritten, portMAX_DELAY);
 delay(100);
 stopI2SAudio();
}

Instalar y desinstalar el driver I2S en cada reproducción parece un desperdicio, pero tiene una razón: con el driver apagado el MAX98357A no recibe reloj y queda en silencio, lo que evita el zumbido de fondo mientras el timbre está en reposo.

Aviso por WhatsApp con la foto

Esta función abre una conexión HTTPS hacia la API de mensajería de CircuitDigest Cloud y arma a mano una petición multipart/form-data: el número de destino, la plantilla del mensaje, algunas variables (evento, ubicación, nombre del equipo) y el JPEG completo. La imagen se escribe directo al socket sin copiarla a otro búfer, algo importante en un equipo con tan poca RAM libre.

C++
void sendWhatsAppAlertWithImage(const uint8_t* imageBuf, size_t imageLen, const char* eventLabel) {
 if (!imageBuf || imageLen == 0) return;
 WiFiClientSecure client;
 client.setInsecure();
 if (!client.connect(WHATSAPP_HOST, 443)) {
   Serial.println("WhatsApp: connection failed");
   return;
 }
 String boundary = "ESP32CAMBound7MA4YWxkTrZu0gW";
 String crlf = "\r\n";
 String dash = "--";
 // Force "captured_time" directly to "Just now" without NTP formatting
 String variablesJson = String("{\"event_type\":\"") + eventLabel + "\","
                      + "\"location\":\"Front Door\","
                      + "\"device_name\":\"Outdoor Doorbell\","
                      + "\"captured_time\":\"Just now\"}";
 String bodyHead = dash + boundary + crlf +
                   "Content-Disposition: form-data; name=\"phone_number\"" + crlf + crlf +
                   WHATSAPP_PHONE_NUMBER + crlf +
                   dash + boundary + crlf +
                   "Content-Disposition: form-data; name=\"template_id\"" + crlf + crlf +
                   WHATSAPP_TEMPLATE_ID + crlf +
                   dash + boundary + crlf +
                   "Content-Disposition: form-data; name=\"variables\"" + crlf + crlf +
                   variablesJson + crlf +
                   dash + boundary + crlf +
                   "Content-Disposition: form-data; name=\"image\"; filename=\"p.jpg\"" + crlf +
                   "Content-Type: image/jpeg" + crlf + crlf;
 String bodyTail = crlf + dash + boundary + dash + crlf;
 size_t totalContentLength = bodyHead.length() + imageLen + bodyTail.length();
 client.println("POST /api/v1/whatsapp/send-with-image HTTP/1.1");
 client.print("Host: "); client.println(WHATSAPP_HOST);
 client.print("Authorization: "); client.println(API_KEY);
 client.print("Content-Type: multipart/form-data; boundary="); client.println(boundary);
 client.print("Content-Length: "); client.println(totalContentLength);
 client.println();
 client.print(bodyHead);
 client.write(imageBuf, imageLen);
 client.print(bodyTail);
 uint32_t startMs = millis();
 while ((client.connected() || client.available()) && millis() - startMs < 8000) {
   if (client.available()) {
     Serial.println(client.readStringUntil('\n'));
   }
 }
 client.stop();
}

Ojo con client.setInsecure(): cifra la conexión pero no verifica el certificado del servidor, así que alguien en tu misma red podría hacerse pasar por él. Para un prototipo en casa se acepta; para dejarlo instalado, carga el certificado raíz del servidor con client.setCACert().

Recuperar la cámara cuando falla

El OV2640 a veces queda pegado, sobre todo después de horas encendido o de una caída de voltaje. resetCamera() apaga el driver, corta y devuelve la energía del sensor con el pin PWDN y lo vuelve a inicializar. Solo si eso falla dos veces el programa reinicia el ESP32 completo.

C++
bool resetCamera() {
 esp_camera_deinit();
 delay(50);
 #ifdef PWDN_GPIO_NUM
 if (PWDN_GPIO_NUM != -1) {
   pinMode(PWDN_GPIO_NUM, OUTPUT);
   digitalWrite(PWDN_GPIO_NUM, HIGH); // Power down camera sensor
   delay(100);
   digitalWrite(PWDN_GPIO_NUM, LOW);  // Power back up
   delay(100);
 }
 #endif
 return initCamera();
}

Captura y subida

Las funciones handleCapture() y handleSwitch1() a handleSwitch4() reciben los mensajes del panel. No hacen nada pesado: solo levantan una bandera y devuelven el interruptor a cero en la nube, para que quede listo para la próxima vez. captureAndUpload() es el corazón del timbre: reproduce el aviso, ajusta el sensor, descarta dos cuadros viejos, saca la foto, la sube y manda el WhatsApp.

C++
void handleCapture(float value) {
 if ((bool)value && !captureBusy) remoteCapture = true;
 CDcloud.publish(REMOTE_CAPTURE, 0.0f);
}
void handleSwitch1(float value) {
 if ((bool)value) { playResponseIndex = 1; CDcloud.publish(SLOT_SWITCH_1, 0.0f); }
}
void handleSwitch2(float value) {
 if ((bool)value) { playResponseIndex = 2; CDcloud.publish(SLOT_SWITCH_2, 0.0f); }
}
void handleSwitch3(float value) {
 if ((bool)value) { playResponseIndex = 3; CDcloud.publish(SLOT_SWITCH_3, 0.0f); }
}
void handleSwitch4(float value) {
 if ((bool)value) { playResponseIndex = 4; CDcloud.publish(SLOT_SWITCH_4, 0.0f); }
}
static void captureAndUpload() {
 if (captureBusy) return;
 captureBusy = true;
 buzzerPending = true;
 buzzerPendingSince = millis();
 playAudioBuffer(voice_doorbell, voice_doorbell_len);
 sensor_t * s = esp_camera_sensor_get();
 if (s != NULL) {
   s->set_whitebal(s, 1);       
   s->set_awb_gain(s, 1);       
   s->set_wb_mode(s, 0);        
   s->set_exposure_ctrl(s, 1);  
   s->set_aec2(s, 1);           
   s->set_gain_ctrl(s, 1);      
   s->set_bpc(s, 1);            
   s->set_wpc(s, 1);            
   s->set_raw_gma(s, 1);        
   s->set_lenc(s, 1);           
   s->set_special_effect(s, 0); 
   s->set_saturation(s, 0);     
 }
 camera_fb_t* dummy_fb = esp_camera_fb_get();
 if (dummy_fb) esp_camera_fb_return(dummy_fb);
 dummy_fb = esp_camera_fb_get();
 if (dummy_fb) esp_camera_fb_return(dummy_fb);
 delay(200);
 Serial.println("Capturing photo...");
 camera_fb_t* fb = esp_camera_fb_get();
 if (!fb) {
   bool recovered = false;
   for (int attempt = 1; attempt <= 2 && !recovered; attempt++) {
     if (resetCamera()) {
       delay(300);
       fb = esp_camera_fb_get();
       if (fb) recovered = true;
     }
   }
   if (!recovered) ESP.restart();
 }
 if (!fb) {
   captureBusy = false;
   return;
 }
 bool ok = CDcloud.sendImage(fb->buf, fb->len, "image/jpeg");
 sendWhatsAppAlertWithImage(fb->buf, fb->len, "Doorbell Pressed");
 esp_camera_fb_return(fb);
 captureBusy = false;
}

¿Por qué descartar dos cuadros? El controlador de la cámara va llenando búferes todo el tiempo. Si pides una foto sin vaciarlos, recibes una imagen de hace varios segundos, que puede mostrar la puerta vacía en vez de a la persona que tocó. Además los primeros cuadros después de un ajuste de exposición suelen salir oscuros o verdosos.

Setup y loop

setup() configura los pines, engancha la interrupción del botón, inicia la cámara (con tres reintentos), se conecta al WiFi y a la nube, sincroniza la hora por NTP y se suscribe a las cinco claves del panel. La hora importa más de lo que parece: sin ella el ESP32 cree que está en 1970 y la conexión TLS con el broker MQTT falla porque todos los certificados parecen "del futuro".

C++
void waitForNTPSync() {
 Serial.print("Syncing time via NTP");
 configTime(0, 0, "time.google.com", "pool.ntp.org", "1.1.1.1");
 time_t now = time(nullptr);
 uint32_t startMs = millis();
 while (now < 8 * 3600 * 2 && millis() - startMs < 15000) {
   delay(250);
   now = time(nullptr);
 }
}
void setup() {
 Serial.begin(115200);
 pinMode(DOORBELL_BTN_PIN, INPUT_PULLUP);
 attachInterrupt(digitalPinToInterrupt(DOORBELL_BTN_PIN), onDoorbellButton, FALLING);
 pinMode(BUZZER_PIN, OUTPUT);
 digitalWrite(BUZZER_PIN, BUZZER_OFF);
 delay(300);
 bool camOk = initCamera();
 for (int attempt = 1; !camOk && attempt <= 3; attempt++) {
   delay(500);
   camOk = initCamera();
 }
 if (!camOk) ESP.restart();
 WiFi.mode(WIFI_STA);
 if (!CDcloud.begin(WIFI_SSID, WIFI_PASS, DEVICE_ID, CONNECTION_KEY, API_KEY)) {
   while (true) delay(1000);
 }
 if (WiFi.status() == WL_CONNECTED) WiFi.setSleep(false);
 waitForNTPSync();
 CDcloud.subscribe(REMOTE_CAPTURE, handleCapture);
 CDcloud.subscribe(SLOT_SWITCH_1, handleSwitch1);
 CDcloud.subscribe(SLOT_SWITCH_2, handleSwitch2);
 CDcloud.subscribe(SLOT_SWITCH_3, handleSwitch3);
 CDcloud.subscribe(SLOT_SWITCH_4, handleSwitch4);
}
void loop() {
 if (WiFi.status() != WL_CONNECTED) {
   static uint32_t lastReconnectAttempt = 0;
   uint32_t currentMs = millis();
   if (currentMs - lastReconnectAttempt >= 5000) {
     lastReconnectAttempt = currentMs;
     WiFi.disconnect();
     WiFi.reconnect();
   }
   delay(100);
   return;
 }
 CDcloud.loop();
 if (buzzerPending && (millis() - buzzerPendingSince >= BUZZER_DELAY_MS)) {
   buzzerPending = false;
   triggerBuzzer();
 }
 if (buzzerActive && (millis() - buzzerStartTime >= BUZZER_DURATION_MS)) {
   digitalWrite(BUZZER_PIN, BUZZER_OFF);
   buzzerActive = false;
 }
 if (lastEdgeMs && (millis() - lastEdgeMs >= DEBOUNCE_MS)) {
   lastEdgeMs = 0;
   if (digitalRead(DOORBELL_BTN_PIN) == LOW) {
     captureAndUpload();
   }
 }
 if (remoteCapture) {
   remoteCapture = false;
   captureAndUpload();
 }
 if (playResponseIndex > 0) {
   int choice = playResponseIndex;
   playResponseIndex = 0;
   switch (choice) {
     case 1: playAudioBuffer(voice_switch1, voice_switch1_len); break;
     case 2: playAudioBuffer(voice_switch2, voice_switch2_len); break;
     case 3: playAudioBuffer(voice_switch3, voice_switch3_len); break;
     case 4: playAudioBuffer(voice_switch4, voice_switch4_len); break;
   }
 }
}

El loop() nunca se queda esperando: revisa el WiFi y reconecta cada 5 segundos si se cayó, atiende la nube, maneja los dos temporizadores del buzzer, confirma el botón con el antirrebote y reproduce la respuesta que haya pedido el panel. Así una respuesta desde el celular llega al parlante aunque justo se esté subiendo una foto.

WiFi.setSleep(false) desactiva el ahorro de energía del WiFi. Gasta un poco más, pero sin eso los mensajes MQTT pueden llegar con uno o dos segundos de retraso, y la respuesta de voz sonaría cuando la visita ya se fue.

Panel en CircuitDigest Cloud

El proyecto original usa la nube gratuita de CircuitDigest para el panel y los avisos. La configuración son seis pasos:

  1. Crear la cuenta o iniciar sesión con correo y contraseña.

Página de registro e inicio de sesión de CircuitDigest Cloud

  1. Agregar el equipo: en Dashboard, entra a la sección de equipos, usa Add New Device y ponle un nombre, por ejemplo "Timbre". Anota el Device ID y la Connection Key: van en el código.
  2. Crear las variables: en Variables elige tu equipo y crea cuatro variables con Add Variable, en dirección bidireccional. La primera, "entregas", con la clave analog-input-1.
  3. Asignar las claves: analog-input-1 para entregas, analog-input-2 para ocupado, analog-input-3 para ganar tiempo y analog-input-4 para familia o vecinos.
  4. Agregar los widgets: en el panel, Add Widget, Toggle Switch Widget, elige el equipo y la variable. Uno por cada respuesta.
  5. Conectar WhatsApp: en Home, busca la notificación de WhatsApp, vincula tu número y confírmalo con el código que te llega.

Al número de WhatsApp en el código se le pone el código de país sin el signo más: para Chile, 569 seguido de los 8 dígitos del celular.

Prueba final

Con todo cargado, instala el timbre afuera y aprieta el botón. En la puerta se escucha el aviso, en tu celular llega la foto por WhatsApp a los pocos segundos y la imagen queda también en el panel. Desde el panel activas cualquiera de los cuatro interruptores y la respuesta suena afuera casi al instante.

Timbre con ESP32-CAM terminado, instalado en la puerta, con la cámara, el botón y el parlante

Si algo falla

  • ¿Qué parlante usar? Sirven de 4, 6 u 8 Ω. Nunca uses uno de menos de 4 Ω: el MAX98357A se calienta y puede dañarse.
  • El audio sale distorsionado, con chasquidos o se corta. Casi siempre es alimentación. Revisa que el amplificador reciba 5 V estables y que los dos condensadores estén cerca del módulo.
  • El ESP32-CAM se reinicia justo cuando empieza a sonar. El amplificador y la radio WiFi juntos piden más de lo que entrega el puerto USB de un computador. Usa una fuente dedicada de 5 V y 2 A.
  • Zumbido agudo en el parlante cuando no suena nada. Une todas las tierras (ESP32-CAM, MAX98357A y buzzer) en un solo punto común.
  • No arranca con el buzzer conectado. Es el GPIO 12, pin de arranque: revisa la explicación de más arriba.
  • La foto sale oscura de noche. El OV2640 no ve en la oscuridad. El flash LED del módulo (GPIO 4) enceguece a menos de un metro; es mejor una luz de entrada con sensor de movimiento.

Variantes y mejoras

  • Telegram en vez de la nube del autor. Con un bot de Telegram (librería UniversalTelegramBot) el ESP32-CAM puede mandarte la foto directo, sin cuentas de terceros. Los botones de respuesta pasan a ser comandos como /entregas o /ya_voy, y el bot los lee en el mismo loop().
  • Integrarlo a Home Assistant por MQTT. Si ya tienes Home Assistant con un broker Mosquitto, cambia CDcloud.subscribe() por suscripciones de PubSubClient a tópicos como timbre/respuesta. Así el timbre puede encender la luz del pasillo, pausar la música o mostrar la foto en una tablet de la cocina.
  • Respuesta automática según el horario. Con la hora que ya se sincroniza por NTP, el timbre puede decidir solo: de noche reproduce "No estamos, llámanos" sin despertar a nadie con el buzzer, y en horario de despacho responde directo con el mensaje de entregas.
  • Sensor de presencia para la foto. Un sensor PIR en un pin libre permite sacar la foto cuando alguien se acerca, aunque no toque. Útil para ver quién dejó un paquete y se fue.

Personalización para Chile

En Chile consigues los componentes de este montaje en MechatronicStore:

  • ESP32-CAM con cámara OV2640: el cerebro del timbre, con WiFi y cámara en la misma placa.
  • Programador ESP32-CAM MB o adaptador USB serial FTDI: para cargar el código, porque el ESP32-CAM no trae USB.
  • Amplificador de audio I2S MAX98357A: recibe el audio digital y mueve el parlante.
  • Parlante de 8 Ω: el que queda del lado de afuera. Uno de 4 Ω también sirve.
  • Buzzer activo de 5 V: el aviso de adentro. En el esquema va a 3,3 V; revisa que el tuyo suene a ese voltaje.
  • Pulsador momentáneo: el botón del timbre. Si va a la intemperie, conviene uno metálico sellado.
  • Módulo TP4056 con USB-C: la entrada de 5 V del esquema. Si prefieres una fuente con jack, un conector DC hembra cumple la misma función.
  • Condensadores electrolíticos de 100 µF: estabilizan los 5 V y los 3,3 V.
  • Fuente de 5 V y 2 A: indispensable para que no se reinicie al reproducir audio.

Recursos

Versión chilena con componentes en stock local en MechatronicStore.