Caso práctico: Controlador de bomba de agua con ESP32

Caso práctico: Controlador de bomba de agua con ESP32 — hero

Objetivo y caso de uso

Qué construirás: Un controlador de bomba de agua inteligente con conectividad web que monitorea los niveles del tanque mediante interruptores de flotador físicos y corta automáticamente la energía para prevenir un funcionamiento en seco catastrófico.

Por qué es importante / Casos de uso

  • Protección del equipo: Previene la destrucción del motor y los sellos en cuestión de minutos al proteger activamente contra el funcionamiento en seco en bombas centrífugas y sumergibles.
  • Automatización agrícola: Garantiza que las bombas de suministro de nutrientes hidropónicos operen solo cuando el depósito tiene suficiente líquido.
  • Transferencia de agua fuera de la red (off-grid): Automatiza las transferencias de cisterna a tanque de retención, pausando instantáneamente si la fuente se queda seca.
  • Control local primero: Opera completamente en un Punto de Acceso (AP) WiFi local sin dependencia de internet, la nube o MQTT externo.

Resultado esperado

  • El ESP32 transmite una red WiFi independiente (ESP32_Pump_Guard) para un acceso local directo y de baja latencia.
  • Un panel de control web alojado muestra métricas en tiempo real: Estado del tanque, Estado de la bomba y el estado activo/inactivo de la Protección contra Funcionamiento en Seco.

Audiencia: Aficionados al IoT, ingenieros agrícolas y entusiastas del off-grid; Nivel: Intermedio

Arquitectura/flujo: Entradas del interruptor de flotador → Microcontrolador ESP32 → Módulo de relé (control de energía de la bomba) & Servidor Web Local (panel de interfaz de usuario).

Nota educativa de validación

Antes de publicar este caso, el contenido pasó la puerta automática de validación de Prometeo con estado PASS. Para este perfil ESP32 DevKitC, el proyecto se comprobó como proyecto PlatformIO: el validador extrajo platformio.ini y src/main.cpp, creó un proyecto temporal y ejecutó pio run contra platform = espressif32, board = esp32dev y framework = arduino. También revisó la estructura del artículo, que los comandos sean copiables con guiones ASCII, y que no aparezcan stacks no soportados como ESP-IDF directo o placas ESP32 no acotadas.

Evidencia de validación publicada

  • Resultado automático: PASS.
  • Estructura parseada: 4 apartados, 1 tablas y 5 bloques de código detectados antes de publicar.
  • Código comprobado: 1 PlatformIO config + 1 ESP32 source/pio run, 3 Bash/copy-paste checks.
  • Catálogo soportado: el texto se contrastó contra los perfiles de dispositivo validables de Prometeo y los stacks no soportados bloquean la publicación.
  • Hallazgos del informe: sin hallazgos bloqueantes.

Esta validación confirma compatibilidad sintáctica y de herramientas para el código publicado, pero no sustituye la prueba física sobre tu placa ESP32 DevKitC exacta, tu cableado, alimentación y entorno WiFi local.

Nota educativa de seguridad

Este proyecto es un prototipo educativo de bajo voltaje, no un producto certificado. Antes de encender la configuración, verifique la distribución de pines de su placa ESP32 DevKitC exacta, mantenga las señales GPIO dentro de los límites de 3.3 V, nunca aplique 5 V a las entradas del ESP32, desconecte la alimentación antes de cambiar el cableado y use fuentes externas adecuadas para relés, motores o cargas, compartiendo GND solo cuando el esquema lo requiera.

Diagrama de bloques conceptual

Vista de alto nivel: qué entra, qué procesa cada bloque y qué sale del sistema.

Arquitectura funcional

Sonda de agua

GPIO/ADC ESP32

Lógica de umbral

LED/buzzer

Alerta Wi-Fi

Flujo conceptual: detección de humedad, decisión local y aviso al usuario.

Ruta de validación

Código fuente

PlatformIO build

Flash

Monitor serie

Resumen conceptual de las herramientas usadas para comprobar el proyecto ESP32 publicado.

Requisitos previos y materiales

  • Software: Visual Studio Code (VSCode) con la extensión PlatformIO IDE instalada.
  • Microcontrolador: ESP32 DevKitC V4 (variante de 38 pines o 30 pines).
  • Sensores: 2x Interruptores de flotador de nivel de líquido verticales (plástico PP estándar).
  • Actuador: 1x Módulo de relé de 5 V (estándar de 1 canal, Activo-ALTO).
  • Alimentación: Fuente de alimentación USB de 5 V / 2 A.
  • Nota de configuración de hardware (Controladores): Dependiendo de su fabricante específico de ESP32 DevKitC, es posible que necesite instalar los controladores del puente USB a UART CP210x o CH34x en su computadora para permitir que PlatformIO reconozca la placa a través de USB.

Configuración y conexión

Este proyecto utiliza las resistencias pull-up internas del ESP32 para los interruptores de flotador, simplificando el cableado al eliminar la necesidad de resistencias externas.

Tabla de cableado

ComponentePin / Cable del componentePin del ESP32 DevKitCNotas
Interruptor de flotador bajoCable 1GPIO 27Fondo del tanque (Protección contra funcionamiento en seco).
Interruptor de flotador bajoCable 2GNDSe cierra a GND cuando el flotador está ARRIBA (presencia de agua).
Interruptor de flotador altoCable 1GPIO 14Parte superior del tanque (Indicador de lleno).
Interruptor de flotador altoCable 2GNDSe cierra a GND cuando el flotador está ARRIBA (tanque lleno).
Módulo de relé de 5 VVCC5V / VINAlimenta la bobina del relé.
Módulo de relé de 5 VGNDGNDTierra común.
Módulo de relé de 5 VIN (Señal)GPIO 26Señal Activo-ALTO para activar el relé.

Orientación del interruptor de flotador: Configure ambos interruptores de manera que cuando el flotador esté descansando en el fondo (sin agua), el interruptor esté ABIERTO (desconectado, lee ALTO a través del pull-up). Cuando el agua levanta el flotador ARRIBA, el interruptor se CIERRA (conectado a GND, lee BAJO).

Código del proyecto

Cree un nuevo proyecto de PlatformIO para el ESP32 DevKitC. Reemplace el contenido de platformio.ini y src/main.cpp con el código a continuación.

platformio.ini

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200

src/main.cpp

Vista pública parcial del archivo validado. El código completo se muestra a miembros y en PDF/Print.

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>

// ---------------------------------------------------------
// Pin Definitions
// ---------------------------------------------------------
const int RELAY_PIN = 26;
const int LOW_FLOAT_PIN = 27;
const int HIGH_FLOAT_PIN = 14;

// ---------------------------------------------------------
// State Variables
// ---------------------------------------------------------
bool pumpIsOn = false;
bool lowFloatActive = false;  // true = tank empty (dry run risk)
bool highFloatActive = false; // true = tank full

// ---------------------------------------------------------
// Network & Web Server Setup
// ---------------------------------------------------------
const char* AP_SSID = "ESP32_Pump_Guard";
const char* AP_PASS = "admin1234";

WebServer server(80);

// ---------------------------------------------------------
// HTML Dashboard (Stored in PROGMEM)
// ---------------------------------------------------------
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pump Dry-Run Guard</title>
    <style>
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f7f6; color: #333; text-align: center; margin: 0; padding: 20px; }
        .card { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); max-width: 500px; margin: 0 auto 20px auto; }
        h1 { color: #2c3e50; }
        .status-badge { display: inline-block; padding: 8px 15px; border-radius: 20px; font-weight: bold; color: white; margin-bottom: 10px; }
        .bg-green { background-color: #27ae60; }
        .bg-red { background-color: #e74c3c; }
        .bg-gray { background-color: #7f8c8d; }
        button { padding: 12px 24px; font-size: 16px; margin: 10px; border: none; border-radius: 5px; cursor: pointer; transition: 0.3s; color: white; font-weight: bold;}
        .btn-on { background-color: #3498db; }
        .btn-on:hover { background-color: #2980b9; }
        .btn-off { background-color: #95a5a6; }
        .btn-off:hover { background-color: #7f8c8d; }
        .data-row { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #eee; }
        .data-row:last-child { border-bottom: none; }
    </style>
</head>
<body>
    <div class="card">
        <h1>Pump Controller</h1>
        <div id="guard-badge" class="status-badge bg-gray">Loading Status...</div>

        <div class="data-row">
            <span>Tank Level:</span>
            <strong id="tank-level">Unknown</strong>
        </div>
        <div class="data-row">
            <span>Pump State:</span>
            <strong id="pump-state">Unknown</strong>
        </div>

        <div style="margin-top: 20px;">
            <button class="btn-on" onclick="controlPump('on')">Start Pump</button>
            <button class="btn-off" onclick="controlPump('off')">Stop Pump</button>
        </div>
    </div>

    <script>
        function updateDashboard() {
            fetch('/api/status')
                .then(response => response.json())
                .then(data => {
                    document.getElementById('tank-level').innerText = data.tank_status;
                    document.getElementById('pump-state').innerText = data.pump_on ? "RUNNING" : "STOPPED";
                    const badge = document.getElementById('guard-badge');
                    if (data.dry_run_guard) {
                        badge.innerText = "DRY RUN GUARD ACTIVE";
                        badge.className = "status-badge bg-red";
                    } else {
                        badge.innerText = "SYSTEM SAFE";
                        badge.className = "status-badge bg-green";
                    }
                })
                .catch(err => console.error('Error fetching status:', err));
        }

        function controlPump(action) {
            fetch('/api/pump?state=' + action, { method: 'POST' })
                .then(response => response.json())
                .then(data => {
                    if (data.error) {
                        alert("Command rejected: " + data.error);
                    }
                    updateDashboard();
                });
        }
// ...

🔒 Este contenido es premium. Con la suscripción mensual (7 días gratis) podrás desbloquear el material didáctico completo y el pack PDF listo para imprimir.🔓 Desbloquéalo — 7 días gratis
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>

// ---------------------------------------------------------
// Pin Definitions
// ---------------------------------------------------------
const int RELAY_PIN = 26;
const int LOW_FLOAT_PIN = 27;
const int HIGH_FLOAT_PIN = 14;

// ---------------------------------------------------------
// State Variables
// ---------------------------------------------------------
bool pumpIsOn = false;
bool lowFloatActive = false;  // true = tank empty (dry run risk)
bool highFloatActive = false; // true = tank full

// ---------------------------------------------------------
// Network & Web Server Setup
// ---------------------------------------------------------
const char* AP_SSID = "ESP32_Pump_Guard";
const char* AP_PASS = "admin1234";

WebServer server(80);

// ---------------------------------------------------------
// HTML Dashboard (Stored in PROGMEM)
// ---------------------------------------------------------
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Pump Dry-Run Guard</title>
    <style>
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f7f6; color: #333; text-align: center; margin: 0; padding: 20px; }
        .card { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); max-width: 500px; margin: 0 auto 20px auto; }
        h1 { color: #2c3e50; }
        .status-badge { display: inline-block; padding: 8px 15px; border-radius: 20px; font-weight: bold; color: white; margin-bottom: 10px; }
        .bg-green { background-color: #27ae60; }
        .bg-red { background-color: #e74c3c; }
        .bg-gray { background-color: #7f8c8d; }
        button { padding: 12px 24px; font-size: 16px; margin: 10px; border: none; border-radius: 5px; cursor: pointer; transition: 0.3s; color: white; font-weight: bold;}
        .btn-on { background-color: #3498db; }
        .btn-on:hover { background-color: #2980b9; }
        .btn-off { background-color: #95a5a6; }
        .btn-off:hover { background-color: #7f8c8d; }
        .data-row { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #eee; }
        .data-row:last-child { border-bottom: none; }
    </style>
</head>
<body>
    <div class="card">
        <h1>Pump Controller</h1>
        <div id="guard-badge" class="status-badge bg-gray">Loading Status...</div>

        <div class="data-row">
            <span>Tank Level:</span>
            <strong id="tank-level">Unknown</strong>
        </div>
        <div class="data-row">
            <span>Pump State:</span>
            <strong id="pump-state">Unknown</strong>
        </div>

        <div style="margin-top: 20px;">
            <button class="btn-on" onclick="controlPump('on')">Start Pump</button>
            <button class="btn-off" onclick="controlPump('off')">Stop Pump</button>
        </div>
    </div>

    <script>
        function updateDashboard() {
            fetch('/api/status')
                .then(response => response.json())
                .then(data => {
                    document.getElementById('tank-level').innerText = data.tank_status;
                    document.getElementById('pump-state').innerText = data.pump_on ? "RUNNING" : "STOPPED";
                    const badge = document.getElementById('guard-badge');
                    if (data.dry_run_guard) {
                        badge.innerText = "DRY RUN GUARD ACTIVE";
                        badge.className = "status-badge bg-red";
                    } else {
                        badge.innerText = "SYSTEM SAFE";
                        badge.className = "status-badge bg-green";
                    }
                })
                .catch(err => console.error('Error fetching status:', err));
        }

        function controlPump(action) {
            fetch('/api/pump?state=' + action, { method: 'POST' })
                .then(response => response.json())
                .then(data => {
                    if (data.error) {
                        alert("Command rejected: " + data.error);
                    }
                    updateDashboard();
                });
        }

        setInterval(updateDashboard, 2000);
        updateDashboard();
    </script>
</body>
</html>
)rawliteral";

// ---------------------------------------------------------
// Helper Functions
// ---------------------------------------------------------
void updateSensorStates() {
    // Switch Open (Float down/Empty) -> Pin is HIGH
    // Switch Closed (Float up/Water present) -> Pin is LOW
    lowFloatActive = (digitalRead(LOW_FLOAT_PIN) == HIGH); 
    highFloatActive = (digitalRead(HIGH_FLOAT_PIN) == LOW); 

    // Hardware Dry-Run Override Protection
    if (lowFloatActive && pumpIsOn) {
        Serial.println("CRITICAL: Dry run detected! Forcing pump OFF.");
        pumpIsOn = false;
        digitalWrite(RELAY_PIN, LOW); // Turn off relay safely
    }
}

String getTankStatusString() {
    if (lowFloatActive) return "Empty (Low Level)";
    if (highFloatActive) return "Full (High Level)";
    return "Normal (Mid Level)";
}

// ---------------------------------------------------------
// Web Server Route Handlers
// ---------------------------------------------------------
void handleRoot() {
    server.send(200, "text/html", index_html);
}

void handleApiStatus() {
    updateSensorStates();

    String json = "{";
    json += "\"pump_on\":" + String(pumpIsOn ? "true" : "false") + ",";
    json += "\"tank_status\":\"" + getTankStatusString() + "\",";
    json += "\"dry_run_guard\":" + String(lowFloatActive ? "true" : "false");
    json += "}";

    server.send(200, "application/json", json);
}

void handleApiPump() {
    if (!server.hasArg("state")) {
        server.send(400, "application/json", "{\"error\":\"Missing state argument\"}");
        return;
    }

    String stateArg = server.arg("state");
    updateSensorStates();

    if (stateArg == "on") {
        if (lowFloatActive) {
            Serial.println("API: Pump ON command rejected. Dry run guard is active.");
            server.send(403, "application/json", "{\"error\":\"Dry run guard active. Tank is empty.\"}");
            return;
        } else {
            pumpIsOn = true;
            digitalWrite(RELAY_PIN, HIGH);
            Serial.println("API: Pump turned ON manually.");
        }
    } else if (stateArg == "off") {
        pumpIsOn = false;
        digitalWrite(RELAY_PIN, LOW);
        Serial.println("API: Pump turned OFF manually.");
    }

    handleApiStatus();
}

// ---------------------------------------------------------
// Main Setup & Loop
// ---------------------------------------------------------
void setup() {
    Serial.begin(115200);
    delay(1000);
    Serial.println("\n--- ESP32 Water Pump Dry-Run Guard ---");

    // Initialize Pins
    pinMode(RELAY_PIN, OUTPUT);
    digitalWrite(RELAY_PIN, LOW); // Ensure pump is OFF at boot

    // Use internal pull-ups for float switches
    pinMode(LOW_FLOAT_PIN, INPUT_PULLUP);
    pinMode(HIGH_FLOAT_PIN, INPUT_PULLUP);

    // Setup WiFi Access Point
    Serial.print("Setting up WiFi AP...");
    WiFi.softAP(AP_SSID, AP_PASS);

    Serial.println("Done.");
    Serial.print("AP IP Address: ");
    Serial.println(WiFi.softAPIP());

    // Configure Web Server Routes
    server.on("/", handleRoot);
    server.on("/api/status", handleApiStatus);
    server.on("/api/pump", HTTP_POST, handleApiPump);

    server.begin();
    Serial.println("HTTP server started.");
}

void loop() {
    server.handleClient();
    updateSensorStates();
    delay(50); // Small yield to prevent watchdog starvation
}

Compilación y carga

Para compilar y grabar el proyecto en su ESP32 DevKitC, abra la terminal de VSCode y ejecute los siguientes comandos exactos de PlatformIO:

  1. Compile el firmware para asegurarse de que no haya errores de sintaxis:

bash
pio run

  1. Cargue el firmware compilado en el ESP32:

bash
pio run --target upload

  1. Abra el monitor serie para ver los registros de creación del AP y los mensajes de depuración:

bash
pio device monitor

Método de validación y evidencia esperada

Para validar rigurosamente la seguridad y precisión de la protección contra el funcionamiento en seco:

  1. Conexión de red: Conecte un teléfono inteligente o computadora portátil a la red WiFi ESP32_Pump_Guard (Contraseña: admin1234).
  2. Acceso al panel de control: Abra un navegador y navegue a http://192.168.4.1.
  3. Prueba de funcionamiento normal: Asegúrese de mantener el interruptor de flotador bajo manualmente ARRIBA (simulando presencia de agua). Haga clic en «Start Pump» en la interfaz de usuario web.
    • Evidencia esperada: El relé físico debería hacer clic (activarse), el estado de la bomba en la interfaz de usuario debería indicar «RUNNING», y el monitor serie debería registrar API: Pump turned ON manually.
  4. Prueba de corte de hardware por funcionamiento en seco: Mientras la bomba está en funcionamiento, deje caer físicamente el interruptor de flotador bajo ABAJO (simulando que el tanque se queda seco).
    • Evidencia esperada: El relé debe desactivarse inmediatamente haciendo clic. El monitor serie debe imprimir CRITICAL: Dry run detected! Forcing pump OFF. El panel de control web se actualizará automáticamente en 2 segundos para mostrar «DRY RUN GUARD ACTIVE» en rojo.
  5. Prueba de rechazo de la API: Mientras el interruptor de flotador bajo sigue ABAJO, intente hacer clic en «Start Pump» nuevamente en la interfaz de usuario.
    • Evidencia esperada: El navegador mostrará una alerta: «Command rejected: Dry run guard active. Tank is empty.» El relé no se activará. El monitor serie imprimirá API: Pump ON command rejected. Dry run guard is active.

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a Amazon

Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.

Quiz rápido

Pregunta 1: ¿Qué dispositivo se construye en este proyecto?




Pregunta 2: ¿Qué mecanismo físico utiliza el sistema para monitorear los niveles del tanque?




Pregunta 3: ¿Cuál es el propósito principal de cortar automáticamente la energía de la bomba?




Pregunta 4: ¿Qué tipo de bombas protege este sistema contra la destrucción del motor y los sellos?




Pregunta 5: En el contexto de la automatización agrícola, ¿para qué se utiliza este controlador?




Pregunta 6: ¿Qué sucede en la transferencia de agua fuera de la red si la fuente se queda seca?




Pregunta 7: ¿Qué significa que el sistema tenga un 'Control local primero'?




Pregunta 8: ¿Qué microcontrolador se menciona como el encargado de transmitir la red WiFi?




Pregunta 9: ¿Cuál es el nombre de la red WiFi independiente que transmite el dispositivo?




Pregunta 10: ¿Qué ventaja ofrece la red WiFi independiente generada por el ESP32?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:
Scroll al inicio