¿Cuántas veces has tenido que subir al techo, o caminar hasta el fondo de la parcela, solo para ver si el estanque todavía tiene agua? Un sensor de nivel con flotador o con electrodos metidos en el agua resuelve el problema, pero se ensucia, se corroe y obliga a perforar el estanque. Este proyecto toma otro camino: un sensor capacitivo pegado por fuera de la pared del estanque detecta si hay agua a esa altura, y un ESP32 manda el dato por radio a otro ESP32 dentro de la casa usando ESP NOW, sin router de por medio.
Vas a aprender cómo un sensor capacitivo "ve" el agua a través del plástico, cómo conectar su salida de 5 V a un ESP32 de 3,3 V sin dañarlo, cómo funciona el enlace ESP NOW entre dos placas y qué ajustes necesitas para que el transmisor a batería dure más que unas horas.
Cómo detecta el agua sin tocarla
El sensor tiene un electrodo plano que, junto con el entorno, forma un condensador. El agua tiene una constante dieléctrica cercana a 80, contra 1 del aire y entre 2 y 4 de los plásticos comunes. Cuando el agua sube y queda frente al electrodo, la capacitancia medida salta de forma notoria; el circuito del sensor compara ese valor con un umbral (el trimpot ajusta la sensibilidad) y conmuta un transistor de salida. Por eso funciona a través de vidrio, plástico, acrílico o cerámica de hasta 20 mm de espesor, y por eso no funciona en estanques metálicos: el metal apantalla el campo y el sensor no ve nada.
De esta física salen dos consecuencias prácticas:
- Un sensor es un nivel. El sensor responde sí o no a la pregunta "¿hay agua a esta altura?". Para saber si el estanque está lleno, a la mitad o vacío, necesitas un sensor en cada altura que te interese.
- El contacto con la pared importa. Una burbuja de aire entre el sensor y el estanque cambia la lectura. Pégalo firme, con cinta de doble contacto de buena calidad o con una abrazadera, y lejos de la entrada de agua, donde la turbulencia hace que la salida parpadee.
El sensor: pines y modo de salida
El tutorial original usa el SemLab CLS-24BP, un módulo de hardware abierto. Funciona igual que los sensores sin contacto de la familia XKC-Y25 que se venden en Chile: alimentación de 5 a 24 V, salida digital y un tiempo de respuesta de hasta 800 ms.

| Pin | Función |
|---|---|
| VIN | Alimentación positiva, de 5 a 24 V |
| MODE | Selección del tipo de salida (NPN o PNP) |
| OUT | Salida del transistor, hacia el microcontrolador, un relé o un PLC |
| GND | Tierra común |
Aquí el texto original se contradice: en una parte dice que dejar MODE al aire da salida NPN y en el resultado dice que da PNP. No adivines: alimenta el sensor, acerca una botella con agua y mide la salida OUT con un multímetro con y sin agua. Lo que veas es la lógica de tu sensor, y el código se adapta a eso (una sola línea, ver más abajo). El LED del propio sensor te confirma cuándo detecta.
Hardware: transmisor a batería y receptor por USB
El proyecto tiene dos lados. El transmisor va junto al estanque: sensor, ESP32, batería LiPo de 3,7 V, un elevador de voltaje a 5 V (el original usa un XL3608) y un interruptor deslizante para encender y apagar. El receptor es un segundo ESP32 conectado por USB a tu computador, que muestra lo que llega en el monitor serial.

El elevador sube los 3,7 V de la batería a 5 V y alimenta tanto al sensor (VIN) como al ESP32 (pin 5V o VIN de la placa). La salida OUT del sensor va al GPIO 13 del ESP32.

El detalle que no puedes saltarte: 5 V contra 3,3 V
Alimentado con 5 V, el sensor puede entregar cerca de 5 V en OUT, y los GPIO del ESP32 aguantan como máximo 3,6 V. Conectarlo directo funciona un tiempo y después el pin muere. Hay dos soluciones, según cómo sea la salida de tu sensor:
- Salida con voltaje (empuja hacia VIN): usa un divisor resistivo. Pon una resistencia de 10 kΩ entre OUT y el GPIO 13, y una de 22 kΩ entre el GPIO 13 y GND. El pin ve 5 V × 22 / (10 + 22) ≈ 3,4 V, dentro de lo seguro, y aún lejos del umbral de nivel alto.
- Salida de colector abierto (NPN que solo tira a GND): no hace falta divisor. Conecta OUT directo al GPIO y activa la resistencia interna de pull up del ESP32 cambiando
pinMode(SensePin, INPUT);porpinMode(SensePin, INPUT_PULLUP);. El pin queda a 3,3 V en reposo y el sensor lo baja a 0 V cuando detecta.
La prueba con el multímetro de la sección anterior te dice cuál es tu caso: si sin carga mides cerca de 5 V en OUT, es la primera; si mides 0 V o el valor flota, es la segunda.
El montaje
El transmisor del original va dentro de una caja impresa en 3D con la batería y el interruptor, y el ESP32 en la cara inferior. El receptor es solo la placa con su cable USB.

Software: ESP NOW de un ESP32 a otro
ESP NOW es un protocolo de Espressif que manda paquetes cortos (hasta 250 bytes) directo de una placa a otra usando la radio WiFi, sin conectarse a un router. Para un dato de un bit cada segundo es ideal: no hay que esperar la conexión a la red, la latencia es de pocos milisegundos y el alcance en exteriores supera con facilidad el de un WiFi doméstico.
Transmisor
El código completo del transmisor:
#include <esp_now.h>
#include <WiFi.h>
const int SensePin = 13;
// Replace with your Receiver's MAC Address
uint8_t broadcastAddress[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
typedef struct struct_message {
bool liquidDetected;
} struct_message;
struct_message myData;
esp_now_peer_info_t peerInfo;
// Updated callback function signature for ESP32 Arduino Core v3.x
void OnDataSent(const wifi_tx_info_t *info, esp_now_send_status_t status) {
Serial.print("Delivery Status: ");
Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Success" : "Fail");
}
void setup() {
Serial.begin(115200);
pinMode(SensePin, INPUT);
WiFi.mode(WIFI_STA);
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing ESP-NOW");
return;
}
esp_now_register_send_cb(OnDataSent);
memcpy(peerInfo.peer_addr, broadcastAddress, 6);
peerInfo.channel = 0;
peerInfo.encrypt = false;
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
Serial.println("Failed to add peer");
return;
}
}
void loop() {
bool currentState = digitalRead(SensePin);
myData.liquidDetected = currentState;
if (currentState) {
Serial.println("Liquid Detected - Sending state...");
} else {
Serial.println("No Liquid Available - Sending state...");
}
esp_now_send(broadcastAddress, (uint8_t *) &myData, sizeof(myData));
delay(1000);
}
Lo importante, por bloques:
broadcastAddressguarda la dirección MAC del receptor. ConFF:FF:FF:FF:FF:FF, como viene, el paquete va en difusión a cualquier ESP32 que escuche. Funciona, pero si pones la MAC real del receptor (la imprimeWiFi.macAddress()en su monitor serial) ganas dos cosas: el emisor recibe confirmación de entrega y el callbackOnDataSentinforma "Success" o "Fail" con sentido.struct_messagees el formato del paquete: un solo booleano. Transmisor y receptor tienen que usar exactamente la misma estructura, porque el receptor copia los bytes tal cual llegan.- En
setup(),WiFi.mode(WIFI_STA)pone la radio en modo estación (ESP NOW lo necesita),esp_now_init()arranca el protocolo yesp_now_add_peer()registra al receptor en el canal 0 (el canal actual) y sin cifrado. - En
loop(), se lee el GPIO 13, se arma el paquete, se envía conesp_now_send()y se espera un segundo.
La firma de OnDataSent con wifi_tx_info_t es la del núcleo ESP32 para Arduino versión 3.x. Si al compilar te aparece un error en esa línea, revisa en el gestor de placas que tengas la versión 3 instalada.
Receptor
El receptor solo escucha. Esta función se ejecuta cada vez que llega un paquete:
void OnDataRecv(const esp_now_recv_info *info, const uint8_t *data, int len) {
memcpy(&incomingData, data, sizeof(incomingData));
if (incomingData.liquidDetected) {
Serial.println("Received: Liquid Detected");
} else {
Serial.println("Received: No Liquid Available");
}
}
esp_now_recv_info trae los datos del emisor (su MAC y la intensidad de señal, RSSI), memcpy copia los bytes recibidos a la estructura incomingData y según el booleano se imprime si hay o no líquido. La configuración es la misma del transmisor, con el registro del callback de recepción:
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
if (esp_now_init() != ESP_OK) {
Serial.println("Error initializing ESP-NOW");
return;
}
esp_now_register_recv_cb(OnDataRecv);
}
Y el loop() queda vacío, porque todo ocurre en el callback cuando llega un paquete:
void loop() {
// Callback handles data as it arrives
}
El archivo completo del receptor, con la declaración de incomingData y la misma struct_message del transmisor, está en el repositorio del proyecto (ver Recursos).
Pruebas
Antes de ir al estanque, prueba en la mesa con una botella plástica: pega el sensor a la botella, ajusta el trimpot hasta que el LED del sensor cambie justo cuando el agua pasa frente a él, y llena o vacía la botella.

En el monitor serial del receptor deberías ver alternar "Received: Liquid Detected" y "Received: No Liquid Available". Si ves lo contrario a lo esperado, no cambies cables: invierte la lectura en el transmisor (bool currentState = !digitalRead(SensePin);) o cambia el modo del sensor con el pin MODE.

Diagnóstico
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
| El LED del sensor nunca enciende | Sin alimentación, polaridad invertida, menos de 5 V o sensibilidad muy baja | Revisa el cableado y mide la salida del elevador |
| La salida parpadea | Aire entre sensor y pared, fuente con ruido o turbulencia | Vuelve a pegar el sensor, aléjalo de la entrada de agua |
| La lectura sale invertida | MODE al revés de lo que asume el código | Invierte en código o cambia MODE |
| El receptor no muestra nada | ESP NOW no inició o las placas están fuera de alcance | Revisa "Error initializing ESP NOW", usa WIFI_STA en ambas, acércalas |
| El receptor deja de recibir al conectarse a WiFi | El router lo movió a otro canal | Fija el mismo canal en ambas placas |
Que la batería dure: el ajuste que falta
Con el código tal como está, el transmisor mantiene la radio encendida todo el tiempo. Un ESP32 así consume del orden de 100 mA, y con las pérdidas del elevador una LiPo de 600 mAh no llega a un día completo. Para un estanque, que cambia de nivel en horas, no necesitas un dato por segundo:
- Envía el estado, y después duerme con
esp_deep_sleep()durante uno a cinco minutos. En sueño profundo el ESP32 baja a decenas de microamperes. - Mejor aún, despierta solo cuando el nivel cambia: configura el GPIO del sensor como fuente de despertar con
esp_sleep_enable_ext0_wakeup(). Ojo, para esto el pin tiene que ser de RTC (en el ESP32 clásico el 13 lo es). - El sensor y el elevador siguen consumiendo mientras el ESP32 duerme. Para autonomía de semanas, corta su alimentación con un transistor controlado por un GPIO y enciéndelos solo para medir.
Variantes y mejoras
- Alerta al celular. Si el receptor está al alcance de tu WiFi, puede conectarse a la red en el mismo canal que usa ESP NOW y mandar un aviso cuando el estanque baje del nivel crítico, por ejemplo con un bot de Telegram o con una notificación de Home Assistant. Es la versión natural para un estanque de parcela que se llena con camión aljibe: te enteras antes de quedarte sin agua.
- Tres niveles con una pantalla. Pon tres sensores (lleno, mitad, reserva) en el transmisor, cambia el booleano de la estructura por un número de 0 a 3 y muestra el nivel en una pantalla OLED SSD1306 en el receptor, colgada en la cocina.
- Control de la bomba. Agrega un relé al receptor que encienda la bomba cuando el nivel bajo detecte aire y la apague cuando el nivel alto detecte agua. Con dos sensores tienes histéresis natural y la bomba no se prende y apaga cada segundo.
Personalización para Chile
En Chile consigues todo lo necesario en MechatronicStore:
- Sensor de Nivel Líquido Sin Contacto 5-24V tipo NPN (SKU G-310): equivalente al SemLab del original, misma alimentación de 5 a 24 V y salida digital. Haz la prueba del multímetro para saber si usar divisor o pull up.
- ESP32 con Wifi y Bluetooth ESP WROOM-32 Micro USB (SKU N-303): necesitas dos, uno para el transmisor y otro para el receptor.
- Batería LiPo 852540 3.7V 600mAh (SKU GE1-11): alimenta el transmisor. Para más autonomía, aplica los ajustes de sueño profundo.
- Convertidor Dc Dc Step Up Mt3608 2a Regulable Boost (SKU GP2-5): sube los 3,7 V a 5 V. Es regulable: ajusta el tornillo con el multímetro hasta leer 5 V antes de conectar nada.
- Mini Switch Interruptor Proto 3 Pin (SKU GF1-14): el interruptor de encendido del transmisor.
- Resistencias 1/4W 1 unidad 10kΩ (SKU GK2-15) y Resistencias 1/4W 1 unidad 22kΩ (SKU GK6-15): el divisor que baja la salida de 5 V del sensor a un nivel seguro para el ESP32.
- Cables macho hembra 40 piezas 10cm (SKU C-415): para unir los módulos.
Si en el tutorial original usan el SemLab CLS-24BP, el sensor sin contacto G-310 cumple la misma función. Para el elevador, el original menciona un XL3608; el MT3608 hace el mismo trabajo a esta corriente.
Recursos
- Tutorial original (inglés): Contactless Water Level Monitor with ESP32 & ESP NOW, de Anand D en Circuit Digest. Esta versión está basada en él.
- Repositorio GitHub: Circuit Digest/WATER LEVEL MONITORING, con el código del transmisor y del receptor.
- Archivos descargables: repositorio completo en .zip
- Documentación del sensor: wiki del SemLab Contactless Liquid Level Sensor, con calibración y especificaciones.
Versión chilena con componentes en stock local.










