Cuando una olla a presión silba, lo que pasa físicamente es que la válvula suelta vapor en ráfagas cortas y muy sonoras. Cada ráfaga es un evento acústico limpio: el nivel de sonido sube varios decibeles sobre el ruido de fondo de la cocina, se mantiene un instante y cae. Ese patrón. sube, se sostiene, baja. es algo que un microcontrolador detecta sin problemas, y mucho mejor que una persona que está en otra pieza.
Sobre esa idea vas a construir un contador de silbatos con un ESP32 y un módulo de micrófono MAX4466. El equipo escucha la cocina, cuenta los silbatos y te avisa al teléfono cuando llega al número que definiste. Al terminar vas a saber detectar un evento sonoro leyendo solo el ADC (sin FFT ni librerías de audio), por qué se mide la amplitud peak to peak en vez de la muestra instantánea, cómo evitar que un silbato largo se cuente dos veces, y cómo reemplazar la nube del proyecto original por un bot de Telegram si prefieres no depender de un servicio de terceros.
Concepto: qué se mide realmente
El MAX4466 es un micrófono electret con un amplificador operacional de ganancia ajustable. No entrega "sonido" en el sentido digital: entrega una tensión analógica que oscila alrededor de un punto de reposo de aproximadamente 1,65 V (la mitad de 3,3 V). En silencio la señal se queda casi plana en ese valor; con ruido, oscila hacia arriba y hacia abajo.
El ESP32 lee esa tensión con su ADC interno de 12 bits, que devuelve números entre 0 y 4095. Si tomaras una sola muestra no sabrías nada útil: podrías caer justo en el cruce por cero de una onda fuerte y leer "silencio". Por eso el firmware muestrea durante una ventana de tiempo (SAMPLE_WINDOW, 20 ms) y guarda el máximo y el mínimo de todas las muestras de esa ventana. La diferencia entre ambos es la amplitud peak to peak:
ΔV = Vmax - Vmin
Esa amplitud sí representa cuán fuerte fue el sonido en esos 20 ms. Después se convierte a decibeles con una curva logarítmica:
dB = 41.52 · log₁₀(ΔV) + 64.02
Un detalle importante que conviene tener claro desde el principio: esos decibeles no son decibeles absolutos calibrados. La constante 64.02 es un ajuste empírico que depende de la ganancia que tenga puesto el trimmer del módulo. Sirve perfectamente como escala relativa. "esto es más fuerte que aquello". que es todo lo que necesita el detector, pero no lo uses para medir contaminación acústica ni para comparar contra una norma. Si quieres afinar la escala, pon un sonómetro de celular al lado del micrófono y ajusta el trimmer hasta que el ruido ambiente de tu cocina marque alrededor de 30 dB en el monitor serial.
También vale la pena entender por qué este enfoque funciona con una olla y no funcionaría con, por ejemplo, detectar una palabra hablada. El silbato es un tono angosto y muy energético que domina completamente el espectro mientras dura. Con solo mirar la envolvente de amplitud alcanza para distinguirlo. Si quisieras distinguir dos sonidos de energía parecida (el silbato de la olla contra el pito del microondas) ahí sí necesitarías un análisis de frecuencia, y el costo computacional sube muchísimo.
Hardware: tres cables y una decisión de pin
El circuito completo son tres conexiones. No hay resistencias, ni divisores, ni nada que soldar.
| Pin del MAX4466 | Pin del ESP32 |
|---|---|
| OUT | GPIO 34 |
| GND | GND |
| VCC | 3.3V |

La elección de GPIO34 no es casual y es probablemente lo más importante del montaje:
- GPIO34 pertenece al ADC1. En el ESP32, el ADC2 comparte recursos internos con la radio WiFi. Si lees un pin del ADC2 con el WiFi activo, las lecturas se vuelven erráticas o directamente fallan. Como este proyecto vive conectado a WiFi todo el tiempo, usar ADC1 no es opcional.
- GPIO34 es solo entrada. No tiene driver de salida ni resistencias de pull up/pull down internas. Para un sensor analógico eso es exactamente lo que quieres: nada tirando de la línea.
- No uses GPIO1. Aparece como "A0" en algunos ejemplos escritos para placas XIAO ESP32-S3, pero en una DevKitC genérica GPIO1 es el TX0 del puerto serie USB. Si conectas el micrófono ahí, vas a pelear contra el monitor serial y la placa se va a comportar de forma rara al arrancar.
Sobre la alimentación: el módulo puede trabajar a 3,3 V o a 5 V, pero alimentarlo desde los 3,3 V del ESP32 mantiene la señal dentro del rango que el ADC puede leer sin recortes. Si lo alimentas a 5 V, los picos fuertes pueden superar los 3,3 V de entrada y vas a perder información justo en los silbatos, que es lo único que te interesa medir.
Software: la configuración base
El firmware se compila en el IDE de Arduino con el core de ESP32 instalado. La primera parte del sketch son las credenciales y las constantes de detección:
#include <CircuitDigestCloud.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
// ── Fill in your credentials ────────────────────────────────────────────
#define WIFI_SSID "YOUR_WIFI_SSID"
#define WIFI_PASS "YOUR_WIFI_PASSWORD"
#define DEVICE_ID "YOUR_DEVICE_ID"
#define CONNECTION_KEY "YOUR_CONNECTION_KEY"
#define API_KEY "YOUR_API_KEY"
#define PHONE_NUMBER "YOUR_PHONE_NUMBER"
#define KEY_TARGET_SET "analog-input-1" // slider: desired whistle count
#define KEY_LISTEN_SW "analog-input-2" // switch: start/stop listening
#define KEY_LIVE_COUNT "analog-input-3" // live whistle count readback
#define KEY_LIVE_DB "analog-input-4" // live dB level readback
const char* host = " www.circuitdigest.cloud ";
// ──────────────────────────────────────────────────────────────────────
CircuitDigestCloud CDcloud;
// ---------- Mic Config ----------
#define AUDIO_PIN 34 // GPIO34 (ADC1_CH6) — generic ESP32 Dev Kit / DevKitC
#define SAMPLE_WINDOW 20 // ms per envelope sample (fast, for bursts)
#define ADC_RESOLUTION 4095.0
#define V_REF 3.3
// ---------- Whistle Detection ----------
#define WHISTLE_ON_DB 60.0 // dB level that counts as "whistle sound"
#define WHISTLE_OFF_DB 50.0 // must drop below this to end the whistle
#define MIN_WHISTLE_MS 100UL // must stay loud at least this long
#define WHISTLE_COOLDOWN_MS 10000UL // ignore new whistles for this long after one ends
// ---------- State ----------
bool isListening = false;
int targetWhistles = 0; // starts at 0 until dashboard slider sets it
int whistleCount = 0;
bool inWhistle = false; // currently inside a loud burst
unsigned long whistleStartMs = 0;
unsigned long cooldownUntilMs = 0;
#define CLOUD_PUBLISH_MS 1000UL // how often live dB is pushed to the dashboard
unsigned long lastCloudPublish = 0;
Presta atención a dos cosas de este bloque antes de compilar:
- La línea
const char* host = " www.circuitdigest.cloud ";viene con espacios antes y después del dominio. Tal como está, algunos stacks TLS igual resuelven el nombre, pero otros fallan sin dar un error claro. Si la conexión al servidor no levanta, saca esos espacios: debe quedar"www.circuitdigest.cloud". Lo mismo aplica a la cabeceraHost:que se arma más abajo en la función de alerta. MIN_WHISTLE_MSestá definido en 100 ms, no en 300 ms. El texto del proyecto original menciona 300 ms, pero el código publicado usa 100. Es un valor que vas a querer ajustar igual según tu olla, así que anda directo a la constante y no te fíes de la descripción.
El corazón del detector
Toda la medición pasa por una sola función, que muestrea durante la ventana, calcula la amplitud y la convierte a decibeles:
float readDBLevel() {
unsigned long startMillis = millis();
unsigned int signalMax = 0;
unsigned int signalMin = 4095;
while (millis() - startMillis < SAMPLE_WINDOW) {
unsigned int sample = analogRead(AUDIO_PIN);
if (sample < 4095) {
if (sample > signalMax) signalMax = sample;
if (sample < signalMin) signalMin = sample;
}
}
unsigned int peakToPeak = signalMax - signalMin;
lastSignalMax = signalMax;
lastSignalMin = signalMin;
lastPeakToPeak = peakToPeak;
// Fault detection: only a SUSTAINED flatline counts as disconnected.
// A loud transient (snap/whistle) has large peakToPeak and resets the
// streak immediately, so it's never mistaken for a fault.
if (peakToPeak <= FLATLINE_P2P_MAX) {
if (flatlineStreak < 65000) flatlineStreak++;
} else {
flatlineStreak = 0;
}
lastReadingValid = (flatlineStreak < FLATLINE_FAULT_COUNT);
float voltage = (peakToPeak * V_REF) / ADC_RESOLUTION;
if (voltage < 0.01) voltage = 0.01;
float rawDb = (41.52 * log10(voltage)) + 64.02;
lastRawDB = rawDb;
float db = rawDb;
if (db < 30.0) db = 30.0;
// Invalid (persistently flatlined/disconnected) readings never count as
// loud — report them as silence rather than letting a stuck pin trigger
// a whistle. A real loud sound always has nonzero peakToPeak, so it is
// never affected by this.
if (!lastReadingValid) db = 30.0;
return db;
}
Hay una pieza acá que no suele aparecer en tutoriales de este tipo y que vale la pena mirar: la detección de micrófono desconectado. Un módulo con el cable suelto no entrega cero, entrega un valor flotante que se queda plano. El código aprovecha esa diferencia: si la amplitud peak to peak se mantiene bajo FLATLINE_P2P_MAX durante FLATLINE_FAULT_COUNT muestras seguidas (unos 300 ms), marca la lectura como inválida. Un sonido fuerte real, en cambio, tiene amplitud grande y dura poco, así que nunca dispara esa condición. Sin este chequeo, un cable suelto se vería igual que una cocina en silencio y nunca sabrías por qué el contador no avanza.
La lógica de conteo propiamente tal es una máquina de estados de dos posiciones:
if (isListening && !inWhistle && db > WHISTLE_ON_DB && now > cooldownUntilMs) {
inWhistle = true; whistleStartMs = now;
} else if (inWhistle && db < WHISTLE_OFF_DB) {
inWhistle = false;
if ((now - whistleStartMs) >= MIN_WHISTLE_MS) {
whistleCount++;
cooldownUntilMs = now + WHISTLE_COOLDOWN_MS; // WHISTLE_COOLDOWN_MS = 10000UL
CDcloud.publish(KEY_LIVE_COUNT, (float)whistleCount);
}
}
Se cuenta un silbato solo cuando la señal cruza hacia arriba WHISTLE_ON_DB (60 dB), se mantiene arriba al menos MIN_WHISTLE_MS, y después baja de WHISTLE_OFF_DB (50 dB). Ese esquema de dos umbrales distintos es una histéresis, y es lo que impide que una señal que oscila justo en el límite genere una lluvia de conteos falsos. Los 10 segundos de cooldownUntilMs cumplen otra función: un silbato real de olla dura y tiene ecos; sin ese bloqueo, la cola del mismo silbato se contaría como el siguiente. Ojo acá también con la documentación original, que habla de un cooldown de 800 ms: el código usa 10000UL, o sea 10 segundos.
El arranque deja todo en cero deliberadamente, porque un dashboard en la nube guarda el último valor publicado y, tras un reinicio, mostraría un conteo viejo que ya no corresponde:
void setup() {
Serial.begin(115200);
delay(1000);
pinMode(AUDIO_PIN, INPUT);
analogSetAttenuation(ADC_11db);
// ---- Force a clean slate on every power-up/reset ----
isListening = false;
whistleCount = 0;
targetWhistles = 0;
inWhistle = false;
cooldownUntilMs = 0;
CDcloud.subscribe(KEY_TARGET_SET, onTargetSlider);
CDcloud.subscribe(KEY_LISTEN_SW, onListenSwitch);
Serial.println("Connecting to Wi-Fi & CircuitDigest Cloud...");
if (!CDcloud.begin(WIFI_SSID, WIFI_PASS, DEVICE_ID, CONNECTION_KEY, API_KEY)) {
Serial.println("CDcloud begin() failed — check credentials. Rebooting...");
delay(2000);
ESP.restart();
} else {
Serial.println("CDcloud initialized successfully.");
}
// CDcloud.begin() succeeding only means WiFi + the initial handshake
// worked — the cloud session isn't necessarily ready to accept publishes
// the instant it returns. Pump the loop for a bit so it fully settles
// before we push the reset values, or they can silently get dropped.
for (int i = 0; i < 15; i++) {
CDcloud.loop();
delay(200);
}
// Push the reset state to the dashboard so it never shows a stale value
// from before this boot (switch OFF, count/db back to zero). Each
// publish is retried a few times since a single attempt can still land
// before the connection is fully ready.
publishWithRetry(KEY_LISTEN_SW, 0.0f);
publishWithRetry(KEY_LIVE_COUNT, 0.0f);
publishWithRetry(KEY_LIVE_DB, 0.0f);
Serial.println("System Ready. Set target on analog-input-1, arm with analog-input-2.");
}
El aviso al teléfono
Cuando el conteo llega al objetivo, el ESP32 abre una conexión TLS y manda un POST con el resumen:
void sendWhatsAppAlert(int completedWhistles) {
WiFiClientSecure client;
client.setInsecure();
client.setTimeout(5000);
Serial.println("Attempting WhatsApp alert connection...");
if (!client.connect(host, 443)) {
Serial.println("WhatsApp API Connection FAILED - could not reach host");
return;
}
Serial.println("Connected to CircuitDigest Cloud host. Sending request...");
// NOTE: "threshold_violation_alert" is reused here since it's a generic
// template with device/parameter/measured_value/limit/location fields.
// If your CircuitDigest Cloud account has a dedicated whistle-alert
// template, swap template_id below to that instead.
String payload =
"{\"phone_number\":\"" + String(PHONE_NUMBER) + "\","
"\"template_id\":\"threshold_violation_alert\","
"\"variables\":{"
"\"device_name\":\"ESP32 Cooker Whistle Counter\","
"\"parameter\":\"Whistle Count\","
"\"measured_value\":\"" + String(completedWhistles) + " whistles\","
"\"limit\":\"" + String(completedWhistles) + " whistles\","
"\"location\":\"Kitchen\"}}";
client.println("POST /api/v1/whatsapp/send HTTP/1.1");
client.println("Host: www.circuitdigest.cloud ");
client.println("X-API-Key: " + String(API_KEY));
client.println("Content-Type: application/json");
client.println("Connection: close");
client.print("Content-Length: ");
client.println(payload.length());
client.println();
client.print(payload);
unsigned long responseStart = millis();
while (client.connected() && !client.available()) {
if (millis() - responseStart > 5000) {
Serial.println("WhatsApp API response TIMEOUT - no reply from server");
client.stop();
return;
}
delay(10);
}
Serial.println("---- Server Response ----");
String responseBody = "";
while (client.connected() || client.available()) {
if (client.available()) {
String line = client.readStringUntil('\n');
Serial.println(line);
responseBody += line;
}
}
Serial.println("--------------------------");
if (responseBody.indexOf("200") > 0 || responseBody.indexOf("201") > 0) {
Serial.println("WhatsApp Alert appears to have SENT successfully.");
} else {
Serial.println("WhatsApp Alert may have FAILED - check response above for error details.");
}
client.stop();
}
Tres observaciones sobre esta función:
client.setInsecure()desactiva la validación del certificado del servidor. Funciona, pero significa que el ESP32 no verifica con quién está hablando. Para un proyecto doméstico es aceptable; si esto fuera a producción, corresponde cargar el certificado raíz conclient.setCACert().- La plantilla que se usa es
threshold_violation_alert, una plantilla genérica de alerta por umbral. Por eso el mensaje llega con campos comoparameterymeasured_value, en vez de un texto escrito para silbatos. - Después de enviar,
isListeningpasa afalse. Sin eso, cualquier ruido posterior seguiría contando y te llegaría un mensaje tras otro.
Configurar el dashboard y la alerta
El proyecto original apoya toda la interfaz en CircuitDigest Cloud, que es gratis y no requiere levantar un servidor. El flujo de configuración es este:
Cuenta y acceso. Crea una cuenta con tu correo y entra al panel.

Registrar el dispositivo. En Dashboard, abre la sección de dispositivos y agrega uno nuevo con el nombre que quieras. De acá salen el DEVICE_ID y la CONNECTION_KEY que van en el sketch.

Crear las variables. Necesitas cuatro, y las claves tienen que coincidir exactamente con los #define del código.

| Clave | Para qué sirve | Dirección |
|---|---|---|
| analog input-1 | Cantidad de silbatos objetivo | del dashboard al equipo |
| analog input-2 | Interruptor de inicio/detención | del dashboard al equipo |
| analog input-3 | Conteo en vivo | del equipo al dashboard |
| analog input-4 | Nivel de dB en vivo | del equipo al dashboard |

Agregar los widgets. Un slider para el objetivo, un interruptor para armar el sistema y dos indicadores numéricos para el conteo y los decibeles.

Vincular el teléfono. En la pestaña Home está la sección de notificaciones. Ingresas tu número, confirmas el código que te llega y queda habilitado el envío.

El flujo completo del programa, desde el encendido hasta el aviso, queda así:

Calibrar y probar
Antes de confiarle la once a este aparato, dedícale diez minutos a calibrarlo:
- Carga el firmware y abre el monitor serial a 115200 baudios. El código imprime el dB en vivo, el valor sin recortar, y el mínimo, máximo y peak to peak del ADC. Ese detalle es oro para depurar: si el dB se queda pegado, los valores crudos te dicen si el problema es el micrófono o la fórmula.
- Con la cocina en silencio, gira el trimmer del módulo hasta que el nivel se estacione en torno a 30 dB. En sentido antihorario sube la ganancia, en horario la baja.
- Haz sonar la olla (o silba tú, que sirve igual para la prueba) y anota qué dB alcanza. Si el pico no supera los 60 dB de
WHISTLE_ON_DB, baja ese umbral y el de apagado, manteniendo entre ambos una separación de unos 10 dB. - Recién ahí pon el objetivo en el slider, activa el interruptor y verifica que el conteo avance de a uno por silbato.
Ubica el micrófono a un metro o metro y medio de la olla y fuera de la corriente de vapor. El vapor caliente sobre el electret le acorta la vida y, mientras tanto, mete ruido en la medición.
Cuando algo no funciona
| Síntoma | Qué revisar |
|---|---|
| Una habitación en silencio marca 80 dB | Ganancia demasiado alta: gira el trimmer en sentido horario. Si no baja, el módulo puede estar dañado. |
El dB no se mueve y el monitor dice DISCONNECTED? |
Se activó la detección de flatline: revisa el cable de OUT y que VCC esté en 3.3 V. |
| El ESP32 no se conecta al WiFi | Credenciales mal escritas, o red de 5 GHz. El ESP32 solo trabaja en 2,4 GHz. |
| El silbato no se detecta | Baja WHISTLE_ON_DB y WHISTLE_OFF_DB, o acerca el micrófono. |
| Un silbato cuenta doble | Sube WHISTLE_COOLDOWN_MS. |
| El mensaje nunca llega | Saca los espacios de host y de la cabecera Host:, y confirma que el número quedó verificado en el panel. |

Variantes y mejoras
Cambia la nube propietaria por un bot de Telegram. Es la modificación que más vale la pena si no quieres depender de una cuenta externa. Creas un bot con @BotFather, obtienes el token y el chat ID, y reemplazas el POST a la nube por una llamada a https://api.telegram.org/bot<TOKEN>/sendMessage. Pierdes el dashboard con slider, así que el objetivo de silbatos pasa a ser una constante en el código o un valor que fijas con un par de botones. A cambio, el equipo deja de depender de un servicio de terceros y el mensaje que llega lo escribes tú.
Hazlo funcionar sin internet. Toda la detección es local; lo único que necesita WiFi es el aviso. Si le agregas un buzzer activo y una pantalla OLED SSD1306 por I2C, tienes un contador autónomo que suena en la cocina y muestra el conteo. Sirve además como respaldo: si se cae la red, el aparato sigue contando.
Umbral adaptativo en vez de fijo. El umbral de 60 dB fijo falla si la cocina cambia de ruido de fondo. una campana extractora encendida sube el piso varios decibeles. . En vez de comparar contra una constante, guarda un promedio móvil del nivel ambiente de los últimos 30 segundos y dispara cuando la lectura supere ese promedio por, digamos, 15 dB. Es un cambio de pocas líneas y vuelve el detector mucho más robusto.
Personalización para Chile
El proyecto se arma con componentes que se consiguen sin problema en el mercado local. Lista de materiales con los nombres que vas a encontrar en el catálogo de MechatronicStore:
- Placa ESP32 DevKit. es el cerebro del proyecto. Cualquier variante DevKit/DevKitC genérica sirve; no necesitas una placa de marca. Lo único que tienes que respetar es usar un pin del ADC1 (GPIO32 a GPIO39) para el micrófono.
- Módulo micrófono MAX4466. el sensor acústico con ganancia ajustable. Si no lo encuentras, el MAX9814 es el reemplazo honesto: también es un electret amplificado con salida analógica y funciona con el mismo código, aunque trae control automático de ganancia, así que la calibración con el trimmer cambia. El KY-038, en cambio, no es equivalente directo: su salida útil es digital con umbral por potenciómetro, y el código de este proyecto necesita la salida analógica para calcular decibeles.
- Protoboard 830 puntos. para el montaje sin soldadura.
- Jumpers macho hembra 20cm. tres bastan para conectar el módulo a la placa.
- Cable USB-A a micro USB. alimentación y carga del firmware. Verifica que sea de datos y no de solo carga, que es la causa número uno de "mi placa no aparece en el IDE".
La olla a presión, obviamente, la pones tú. El proyecto no distingue marcas: lo único que importa es que la válvula silbe de forma audible, que es el caso de prácticamente cualquier olla doméstica.
Sobre la alimentación: si vas a dejarlo instalado en la cocina, no lo alimentes desde el notebook. Un cargador USB de 5 V y 1 A a la red domiciliaria de 220 V sobra, y le da al ESP32 una fuente más estable que un puerto USB compartido. Mantén el conjunto lejos del vapor y del calor directo de la cocina.
Recursos
- Tutorial original: ESP32 Automatic Cooker Whistle Counter + WhatsApp Alert. Circuit Digest, por Mohammed Shaheen K S
- Repositorio GitHub: Smart Cooker Whistle Counter using ESP32
- Código completo (.zip): descarga directa desde GitHub
- Documentación adicional: Guía del ADC del ESP32 (Espressif)
Versión chilena, inspirada en el proyecto de Circuit Digest, con componentes en stock local en MechatronicStore.




