Caso práctico: Contador de personas con ESP32

Caso práctico: Contador de personas con ESP32 — hero

Objetivo y caso de uso

Lo que construirás: Construirás un contador direccional de personas para puertas utilizando sensores infrarrojos (IR) de barrera dobles para rastrear a los individuos que entran y salen de una habitación, transmitiendo el recuento de ocupación en tiempo real a través de Bluetooth de baja energía (BLE).

Por qué es importante / Casos de uso

  • Climatización y optimización energética: Ajusta dinámicamente el control de clima basándose en la ocupación real de la habitación, reduciendo el desperdicio de energía hasta en un 20%.
  • Mantenimiento basado en el uso: Activa alertas de limpieza automáticamente después de un umbral específico (por ejemplo, 50 entradas) en lugar de depender de horarios fijos ineficientes.
  • Análisis de espacios y comercio minorista: Rastrea las horas pico de visitantes y correlaciona el tráfico peatonal con los datos de ventas, o monitorea la utilización de las salas de conferencias para optimizar la distribución del edificio.

Resultado esperado

  • Un dispositivo edge implementado que rastrea con precisión el movimiento direccional con una latencia de sondeo de sensores de <50ms.
  • Anuncio BLE en tiempo real de la ocupación actual de la habitación con una tasa de actualización de 1Hz y un consumo de energía mínimo.
  • Lógica robusta de eliminación de rebotes (debouncing) que ignora cruces parciales o falsos disparos para mantener un recuento preciso.

Audiencia: Desarrolladores de IoT, Gerentes de instalaciones, Makers; Nivel: Intermedio

Arquitectura/flujo: Sensores IR de barrera dobles → Interrupciones GPIO del microcontrolador → Lógica de conteo direccional → Anuncio BLE → Puerta de enlace/Panel de control

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: 3 apartados, 3 tablas y 2 bloques de código detectados antes de publicar.
  • Código comprobado: 1 PlatformIO config + 1 ESP32 source/pio run.
  • 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 prototipo está diseñado estrictamente para fines educativos y de monitoreo básico. No debe usarse para el control crítico de capacidad, cumplimiento del código de incendios, seguimiento de evacuación de emergencia o control de acceso de seguridad. Los sensores IR utilizados en los kits estándar para aficionados son seguros para los ojos, pero se debe tener cuidado para asegurar que los cables que atraviesan una puerta no creen un peligro de tropiezo. Si se implementa en un entorno real, asegura todo el cableado de forma segura y usa cajas adecuadas.

Diagrama de bloques conceptual

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

Arquitectura funcional

Botón local

ESP32 BLE

Paquete advertising

LED de estado

Móvil escáner

Flujo conceptual: configuración local, emisión BLE y lectura desde el móvil.

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

  • Software: Visual Studio Code (VSCode) con la extensión PlatformIO IDE instalada.
  • Controladores: Controladores USB a UART CP210x o CH34x instalados en tu computadora host (dependiendo del puente USB de tu ESP32 DevKitC específico).
  • Aplicación móvil: Una aplicación de escáner BLE en tu teléfono inteligente (por ejemplo, LightBlue o BLE Scanner) para leer las transmisiones BLE.
  • Conocimientos: Familiaridad básica con el uso de protoboards y programación en C++.

Materiales

  • Microcontrolador: ESP32 DevKitC + sensores IR de barrera dobles + LED de estado
    • Nota: Los sensores IR de barrera generalmente vienen en pares (un emisor, un receptor). Necesitas dos pares completos para la detección direccional.
  • Fuente de alimentación: Cable Micro-USB o USB-C estándar (dependiendo de tu variante de DevKitC) conectado a tu computadora para alimentación y programación.
  • Cableado: Protoboard y cables puente variados de macho a macho y de macho a hembra.
  • Componentes pasivos: Una resistencia de 220Ω o 330Ω para el LED de estado externo (si no se usa el LED integrado).

Configuración/Conexión

La configuración de hardware requiere alinear los dos pares de sensores IR de barrera a través de una puerta o un arco de cartón simulado. Los emisores simplemente se alimentan, mientras que los receptores actúan como interruptores digitales conectados al ESP32. Cuando el haz no se interrumpe, el receptor emite una señal HIGH. Cuando un objeto interrumpe el haz, el receptor emite una señal LOW.

Monta los sensores horizontalmente. El Sensor A debe colocarse en el «exterior» del umbral, y el Sensor B debe colocarse en el «interior», separados entre sí unas 3 a 5 pulgadas, lo suficientemente cerca como para que una persona que pase interrumpa ambos haces secuencialmente, pero lo suficientemente lejos para detectar una secuencia direccional clara.

Componente Pin / Terminal Pin del ESP32 DevKitC Notas
Emisor IR A (Exterior) VCC / 5V 5V (VIN) Los emisores solo necesitan alimentación y tierra.
Emisor IR A (Exterior) GND GND Conectar al riel de tierra común.
Receptor IR A (Exterior) VCC / 5V 3.3V o 5V Verifica las especificaciones del sensor; la mayoría de la lógica es segura para 3.3V.
Receptor IR A (Exterior) GND GND Conectar al riel de tierra común.
Receptor IR A (Exterior) OUT / Señal GPIO 32 Entrada digital (HIGH = sin interrumpir, LOW = interrumpido).
Emisor IR B (Interior) VCC / 5V 5V (VIN) Alimentación para el segundo haz.
Emisor IR B (Interior) GND GND Conectar al riel de tierra común.
Receptor IR B (Interior) VCC / 5V 3.3V o 5V Verifica las especificaciones del sensor.
Receptor IR B (Interior) GND GND Conectar al riel de tierra común.
Receptor IR B (Interior) OUT / Señal GPIO 33 Entrada digital (HIGH = sin interrumpir, LOW = interrumpido).
LED de estado Ánodo (Pata larga) GPIO 2 A través de una resistencia de 220Ω. (A menudo se asigna al LED integrado).
LED de estado Cátodo (Pata corta) GND Conectar al riel de tierra común.

Nota: Si tus receptores IR emiten un nivel lógico alto de 5V, debes usar un convertidor de nivel lógico o un divisor de voltaje antes de conectarlos a los pines GPIO de 3.3V del ESP32 para evitar daños. Muchos receptores IR de barrera estándar son de colector abierto o funcionan bien con alimentación de 3.3V.


Código validado

Los siguientes archivos constituyen el proyecto completo de PlatformIO. La lógica utiliza una máquina de estados por sondeo para rastrear la secuencia de interrupciones de los sensores, filtrando el ruido y asegurando que un conteo solo se registre cuando una persona pase completamente a través de ambos haces en un orden específico.

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 <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// ---------------------------------------------------------
// Pin Definitions
// ---------------------------------------------------------
#define SENSOR_A_PIN 32 // Outside sensor
#define SENSOR_B_PIN 33 // Inside sensor
#define LED_PIN 2       // Status LED

// ---------------------------------------------------------
// BLE Configuration
// ---------------------------------------------------------
// Generate custom UUIDs using a tool like uuidgenerator.net
#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

BLEServer* pServer = nullptr;
BLECharacteristic* pCharacteristic = nullptr;
bool deviceConnected = false;
bool oldDeviceConnected = false;

// ---------------------------------------------------------
// Application State
// ---------------------------------------------------------
int peopleCount = 0;

enum DirectionState {
    STATE_IDLE,
    STATE_A_BROKEN_FIRST,
    STATE_B_BROKEN_FIRST,
    STATE_WAIT_CLEAR_ENTER,
    STATE_WAIT_CLEAR_EXIT
};

DirectionState currentState = STATE_IDLE;

// ---------------------------------------------------------
// BLE Server Callbacks
// ---------------------------------------------------------
class MyServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
      deviceConnected = true;
      Serial.println("BLE Client Connected");
    };

    void onDisconnect(BLEServer* pServer) {
      deviceConnected = false;
      Serial.println("BLE Client Disconnected");
    }
};

// ---------------------------------------------------------
// Helper Functions
// ---------------------------------------------------------
void updateBLECount() {
    if (deviceConnected && pCharacteristic != nullptr) {
        // Format the count as a readable string for easy validation
        char countStr[32];
        snprintf(countStr, sizeof(countStr), "Occupancy: %d", peopleCount);
        pCharacteristic->setValue((uint8_t*)countStr, strlen(countStr));
        pCharacteristic->notify();
        Serial.print("BLE Updated: ");
        Serial.println(countStr);
    }
}

void blinkLED() {
    digitalWrite(LED_PIN, HIGH);
    delay(200);
    digitalWrite(LED_PIN, LOW);
}

// ---------------------------------------------------------
// Setup
// ---------------------------------------------------------
void setup() {
    Serial.begin(115200);
    Serial.println("Starting BLE Doorway People Counter...");

    // Initialize Pins
    pinMode(SENSOR_A_PIN, INPUT_PULLUP);
    pinMode(SENSOR_B_PIN, INPUT_PULLUP);
    pinMode(LED_PIN, OUTPUT);
    digitalWrite(LED_PIN, LOW);

    // Initialize BLE
    BLEDevice::init("ESP32_Counter");
    pServer = BLEDevice::createServer();
    pServer->setCallbacks(new MyServerCallbacks());

    BLEService *pService = pServer->createService(SERVICE_UUID);

    // Create Characteristic with Read and Notify properties
    pCharacteristic = pService->createCharacteristic(
                        CHARACTERISTIC_UUID,
                        BLECharacteristic::PROPERTY_READ   |
                        BLECharacteristic::PROPERTY_NOTIFY
                      );

    // Add CCCD descriptor for notifications
    pCharacteristic->addDescriptor(new BLE2902());

    // Set initial value
    char initStr[32];
    snprintf(initStr, sizeof(initStr), "Occupancy: %d", peopleCount);
    pCharacteristic->setValue((uint8_t*)initStr, strlen(initStr));

    pService->start();

    // Start advertising
    BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
    pAdvertising->addServiceUUID(SERVICE_UUID);
    pAdvertising->setScanResponse(false);
    pAdvertising->setMinPreferred(0x0);
    BLEDevice::startAdvertising();

    Serial.println("BLE Advertising started. Waiting for connections...");
}
// ...

#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// ---------------------------------------------------------
// Pin Definitions
// ---------------------------------------------------------
#define SENSOR_A_PIN 32 // Outside sensor
#define SENSOR_B_PIN 33 // Inside sensor
#define LED_PIN 2       // Status LED

// ---------------------------------------------------------
// BLE Configuration
// ---------------------------------------------------------
// Generate custom UUIDs using a tool like uuidgenerator.net
#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

BLEServer* pServer = nullptr;
BLECharacteristic* pCharacteristic = nullptr;
bool deviceConnected = false;
bool oldDeviceConnected = false;

// ---------------------------------------------------------
// Application State
// ---------------------------------------------------------
int peopleCount = 0;

enum DirectionState {
    STATE_IDLE,
    STATE_A_BROKEN_FIRST,
    STATE_B_BROKEN_FIRST,
    STATE_WAIT_CLEAR_ENTER,
    STATE_WAIT_CLEAR_EXIT
};

DirectionState currentState = STATE_IDLE;

// ---------------------------------------------------------
// BLE Server Callbacks
// ---------------------------------------------------------
class MyServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
      deviceConnected = true;
      Serial.println("BLE Client Connected");
    };

    void onDisconnect(BLEServer* pServer) {
      deviceConnected = false;
      Serial.println("BLE Client Disconnected");
    }
};

// ---------------------------------------------------------
// Helper Functions
// ---------------------------------------------------------
void updateBLECount() {
    if (deviceConnected && pCharacteristic != nullptr) {
        // Format the count as a readable string for easy validation
        char countStr[32];
        snprintf(countStr, sizeof(countStr), "Occupancy: %d", peopleCount);
        pCharacteristic->setValue((uint8_t*)countStr, strlen(countStr));
        pCharacteristic->notify();
        Serial.print("BLE Updated: ");
        Serial.println(countStr);
    }
}

void blinkLED() {
    digitalWrite(LED_PIN, HIGH);
    delay(200);
    digitalWrite(LED_PIN, LOW);
}

// ---------------------------------------------------------
// Setup
// ---------------------------------------------------------
void setup() {
    Serial.begin(115200);
    Serial.println("Starting BLE Doorway People Counter...");

    // Initialize Pins
    pinMode(SENSOR_A_PIN, INPUT_PULLUP);
    pinMode(SENSOR_B_PIN, INPUT_PULLUP);
    pinMode(LED_PIN, OUTPUT);
    digitalWrite(LED_PIN, LOW);

    // Initialize BLE
    BLEDevice::init("ESP32_Counter");
    pServer = BLEDevice::createServer();
    pServer->setCallbacks(new MyServerCallbacks());

    BLEService *pService = pServer->createService(SERVICE_UUID);

    // Create Characteristic with Read and Notify properties
    pCharacteristic = pService->createCharacteristic(
                        CHARACTERISTIC_UUID,
                        BLECharacteristic::PROPERTY_READ   |
                        BLECharacteristic::PROPERTY_NOTIFY
                      );

    // Add CCCD descriptor for notifications
    pCharacteristic->addDescriptor(new BLE2902());

    // Set initial value
    char initStr[32];
    snprintf(initStr, sizeof(initStr), "Occupancy: %d", peopleCount);
    pCharacteristic->setValue((uint8_t*)initStr, strlen(initStr));

    pService->start();

    // Start advertising
    BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
    pAdvertising->addServiceUUID(SERVICE_UUID);
    pAdvertising->setScanResponse(false);
    pAdvertising->setMinPreferred(0x0);
    BLEDevice::startAdvertising();

    Serial.println("BLE Advertising started. Waiting for connections...");
}

// ---------------------------------------------------------
// Main Loop
// ---------------------------------------------------------
void loop() {
    // Handle BLE disconnection/reconnection gracefully
    if (!deviceConnected && oldDeviceConnected) {
        delay(500); // Give the bluetooth stack the chance to get things ready
        pServer->startAdvertising(); 
        Serial.println("Restarted BLE advertising");
        oldDeviceConnected = deviceConnected;
    }
    if (deviceConnected && !oldDeviceConnected) {
        oldDeviceConnected = deviceConnected;
    }

    // Read sensor states (LOW means the beam is broken)
    bool aBroken = (digitalRead(SENSOR_A_PIN) == LOW);
    bool bBroken = (digitalRead(SENSOR_B_PIN) == LOW);

    // State Machine for Directional Counting
    switch (currentState) {
        case STATE_IDLE:
            if (aBroken && !bBroken) {
                currentState = STATE_A_BROKEN_FIRST;
                Serial.println("State: A Broken (Potential Entry)");
            } else if (bBroken && !aBroken) {
                currentState = STATE_B_BROKEN_FIRST;
                Serial.println("State: B Broken (Potential Exit)");
            }
            break;

        case STATE_A_BROKEN_FIRST:
            // If B breaks while A is or was broken, person is moving forward
            if (bBroken) {
                currentState = STATE_WAIT_CLEAR_ENTER;
                Serial.println("State: B Broken (Completing Entry)");
            } else if (!aBroken && !bBroken) {
                // False alarm, person backed out before breaking B
                currentState = STATE_IDLE;
                Serial.println("State: Idle (False Alarm A)");
            }
            break;

        case STATE_B_BROKEN_FIRST:
            // If A breaks while B is or was broken, person is moving outward
            if (aBroken) {
                currentState = STATE_WAIT_CLEAR_EXIT;
                Serial.println("State: A Broken (Completing Exit)");
            } else if (!aBroken && !bBroken) {
                // False alarm, person backed out before breaking A
                currentState = STATE_IDLE;
                Serial.println("State: Idle (False Alarm B)");
            }
            break;

        case STATE_WAIT_CLEAR_ENTER:
            // Wait for both sensors to clear before counting to avoid multiple counts
            if (!aBroken && !bBroken) {
                peopleCount++;
                Serial.print("Person Entered! Total: ");
                Serial.println(peopleCount);
                updateBLECount();
                blinkLED();
                currentState = STATE_IDLE;
            }
            break;

        case STATE_WAIT_CLEAR_EXIT:
            // Wait for both sensors to clear before counting
            if (!aBroken && !bBroken) {
                if (peopleCount > 0) {
                    peopleCount--;
                }
                Serial.print("Person Exited! Total: ");
                Serial.println(peopleCount);
                updateBLECount();
                blinkLED();
                currentState = STATE_IDLE;
            }
            break;
    }

    // Small delay for debouncing and CPU yield
    delay(20);
}


Comandos de compilación/flasheo/ejecución

Usa la CLI de PlatformIO para compilar, cargar y monitorear el proyecto.

Acción Comando
Inicializar/Compilar pio run
Cargar al ESP32 pio run --target upload
Abrir monitor serie pio device monitor

Flujo de trabajo:
1. Abre tu terminal en VSCode en la raíz de tu proyecto de PlatformIO.
2. Ejecuta pio run para descargar el framework de ESP32 y compilar el código en C++. Asegúrate de que termine con un mensaje de SUCCESS.
3. Conecta el ESP32 DevKitC vía USB. Ejecuta pio run --target upload para flashear el firmware.
4. Ejecuta pio device monitor para observar los registros de arranque y las transiciones de estado de los sensores.


Validación paso a paso

Usa estos puntos de control para verificar la funcionalidad de tu contador de personas para puertas.

  1. Arranque e inicialización de BLE
    • Acción: Abre el monitor serie y presiona el botón EN (Reinicio) en el ESP32.
    • Observación esperada: El monitor imprime «Starting BLE Doorway People Counter…» seguido de «BLE Advertising started. Waiting for connections…».
    • Condición de aprobación: No ocurren bucles de arranque ni volcados de memoria por fallos.
  2. Descubrimiento y conexión BLE
    • Acción: Abre una aplicación de escáner BLE (como LightBlue) en tu teléfono inteligente. Escanea en busca de dispositivos y busca «ESP32_Counter». Toca «Connect».
    • Observación esperada: El monitor serie imprime «BLE Client Connected». La aplicación muestra el Servicio personalizado (4fafc201...) y la Característica (beb5483e...).
    • Condición de aprobación: Puedes suscribirte con éxito a las notificaciones para la característica en la aplicación.
  3. Detección de entrada (Ingreso)
    • Acción: Bloquea el Sensor A (Exterior) con tu mano, luego bloquea el Sensor B (Interior), luego retira tu mano de ambos.
    • Observación esperada: El monitor serie registra transiciones de estado que terminan con «Person Entered! Total: 1». El LED de estado parpadea una vez.
    • Condición de aprobación: La aplicación de escáner BLE se actualiza automáticamente para mostrar «Occupancy: 1».
  4. Detección de salida (Egreso)
    • Acción: Bloquea el Sensor B (Interior), luego bloquea el Sensor A (Exterior), luego retira tu mano de ambos.
    • Observación esperada: El monitor serie registra transiciones de estado que terminan con «Person Exited! Total: 0». El LED de estado parpadea una vez.
    • Condición de aprobación: La aplicación de escáner BLE se actualiza automáticamente para mostrar «Occupancy: 0».
  5. Manejo de falsas alarmas
    • Acción: Bloquea solo el Sensor A, luego retira tu mano sin bloquear el Sensor B.
    • Observación esperada: El monitor serie imprime «State: A Broken (Potential Entry)» seguido de «State: Idle (False Alarm A)». El conteo no cambia.
    • Condición de aprobación: El conteo de ocupación se mantiene estable y no se activa ninguna notificación BLE.

Solución de problemas

Síntoma Causa probable Solución
El código falla al cargarse en el ESP32 Falta el controlador CP210x/CH34x, o el ESP32 no está en modo de arranque. Instala los controladores. Mantén presionado el botón BOOT en el DevKitC cuando aparezca «Connecting…» durante la carga.
El conteo se incrementa múltiples veces por pasada Rebote del sensor o haces colocados demasiado cerca. Aumenta la distancia física entre el Sensor A y B. Asegúrate de que la eliminación de rebotes delay(20) en el código sea suficiente.
Los sensores nunca se activan (siempre en IDLE) Los receptores están mal cableados o los haces están desalineados. Asegúrate de que los emisores estén perfectamente alineados con los receptores. Verifica que los receptores estén conectados a los GPIO 32 y 33.
El conteo se vuelve negativo El conteo inicial era 0 y alguien salió, o la lógica direccional está invertida. El código evita conteos negativos (if (peopleCount > 0)). Si la entrada se registra como salida, intercambia los cables en los GPIO 32 y 33.
Dispositivo BLE no encontrado en el teléfono El ESP32 no se está anunciando o la caché del Bluetooth del teléfono está desactualizada. Reinicia el ESP32. Apaga y enciende el Bluetooth de tu teléfono para borrar la caché de escaneo.

Mejoras

Una vez que hayas dominado el prototipo básico, considera estas mejoras para hacer el dispositivo más robusto para una implementación en el mundo real:

Lógica avanzada y fiabilidad
* Manejo de tiempo de espera: Implementa un temporizador en la máquina de estados. Si una persona interrumpe el Sensor A pero se queda allí durante 10 segundos sin interrumpir el Sensor B, el estado debería restablecerse a IDLE para evitar que el sistema se cuelgue.
* Ajuste de eliminación de rebotes: Reemplaza el simple delay(20) con una eliminación de rebotes no bloqueante basada en millis() para asegurar que la pila BLE nunca se quede sin tiempo de CPU durante eventos rápidos de los sensores.

Integración de datos y escalabilidad
* Integración de puerta de enlace BLE: En lugar de un teléfono inteligente, usa una PC u otro ESP32 como dispositivo Central BLE para agregar datos de múltiples puertas y enviarlos a un broker MQTT.
* Registro de series temporales: Agrega un módulo de reloj de tiempo real (RTC) y un lector de tarjetas SD para registrar las marcas de tiempo de cada entrada y salida para su posterior análisis de datos.

Optimización de energía
* Modos de suspensión: Si funciona con batería, configura el ESP32 para entrar en suspensión ligera (light sleep), usando los pines GPIO conectados a los receptores IR como fuentes de reactivación. Ten en cuenta que el anuncio BLE consume mucha energía, por lo que es posible que desees agrupar los datos y transmitirlos solo periódicamente.


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é tipo de sensores se utilizan en el proyecto para rastrear a las personas?




Pregunta 2: ¿Qué tecnología de comunicación se utiliza para transmitir el recuento de ocupación en tiempo real?




Pregunta 3: ¿Cuál es un beneficio de ajustar dinámicamente la climatización según la ocupación real?




Pregunta 4: ¿Cómo se activan las alertas de limpieza en el caso de uso de mantenimiento basado en el uso?




Pregunta 5: ¿Para qué se utiliza el análisis de espacios en el comercio minorista según el texto?




Pregunta 6: ¿Cuál es la latencia de sondeo de sensores esperada en el dispositivo implementado?




Pregunta 7: ¿Qué tipo de dispositivo principal se construirá en este proyecto?




Pregunta 8: ¿Qué acción específica rastrea el dispositivo construido?




Pregunta 9: ¿Qué permite optimizar el monitoreo de la utilización de las salas de conferencias?




Pregunta 10: ¿Qué tipo de arquitectura de dispositivo se menciona en el resultado esperado?




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:


Caso práctico: Monitor de corriente AC con ESP32

Caso práctico: Monitor de corriente AC con ESP32 — ESP32 con pinza SCT-013 y cable de extractor de polvo

Construcción de un monitor de corriente para recolector de polvo con ESP32

Objetivo y caso de uso

Qué construirás: Un monitor de corriente no invasivo para un recolector de polvo que registra el consumo de energía de CA del sistema de recolección de polvo de tu taller para establecer un perfil base de uso eléctrico y detectar anomalías operativas.

Por qué es importante / Casos de uso

  • Monitorización de equipos: Detectar un impulsor atascado o rodamientos de motor defectuosos mediante la identificación de picos de corriente de funcionamiento anormales (por ejemplo, una sobretensión repentina muy por encima de la línea base).
  • Detección de filtros/obstrucciones: Identificar disminuciones graduales o caídas inesperadas en el consumo de corriente base que indican un flujo de aire restringido o un filtro de recolector de polvo obstruido (ya que los ventiladores centrífugos consumen menos corriente cuando el flujo de aire está restringido).
  • Perfilado de energía: Registrar el uso eléctrico continuo a lo largo del tiempo para calcular con precisión los costos de energía de hacer funcionar el recolector de polvo durante las horas de taller.

Resultado esperado

  • El ESP32 muestreará la forma de onda analógica de corriente alterna (CA) a alta velocidad para calcular la corriente eficaz (RMS) matemáticamente, sin depender de bibliotecas externas.
  • La salida del monitor serie mostrará el consumo de corriente en tiempo real en amperios para el recolector de polvo.
  • El sistema filtrará activamente el ruido eléctrico base para evitar lecturas de falsos positivos durante los estados de inactividad de la máquina.

Audiencia: Estudiantes de electrónica básica y programación que buscan interconectar sensores analógicos e implementar matemáticas de procesamiento de señales; Nivel: Intermedio

Arquitectura/flujo: Sensor de corriente TC no invasivo → ADC del ESP32 → Procesamiento de señales RMS personalizado → Salida del monitor serie

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: 3 apartados, 3 tablas y 2 bloques de código detectados antes de publicar.
  • Código comprobado: 1 PlatformIO config + 1 ESP32 source/pio run.
  • 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

ADVERTENCIA: ALTO VOLTAJE. Este tutorial implica la monitorización de electrodomésticos de red de CA. El SCT-013 es un transformador de corriente no invasivo. Nunca debes cortar, pelar ni exponer cables desnudos de la red de CA para usar este sensor. La pinza solo debe colocarse sobre cables con aislamiento intacto clasificado de fábrica.
Además:
* Nunca intentes conectar el SCT-013 directamente al ESP32 sin la resistencia de carga (burden) de 33Ω en su lugar. Sin una resistencia de carga, un transformador de corriente desconectado puede generar voltajes peligrosamente altos en sus pines de salida cuando se sujeta sobre un cable con corriente.
* Este prototipo es una herramienta educativa. No lo utilices como una desconexión de seguridad principal o mecanismo de monitorización industrial.
* Asegúrate siempre de que tus circuitos de microcontrolador de bajo voltaje estén físicamente aislados y a una distancia segura de las líneas de CA de alto voltaje.

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

Para completar con éxito este tutorial, necesitas:
* Un ordenador con Visual Studio Code y la extensión PlatformIO IDE instalada.
* Familiaridad básica con el uso de protoboards y conexiones de cables puente.
* Comprender la diferencia entre señales de CA y CC (específicamente, que los pines analógicos del ESP32 solo pueden leer voltajes positivos de CC entre 0V y 3.3V).
* Un electrodoméstico de prueba (como una lámpara de escritorio o un ventilador pequeño) enchufado a un divisor de CA donde los cables de fase y neutro estén separados físicamente, antes de pasar al recolector de polvo real.

Nota de configuración de hardware: Si tu ordenador no reconoce automáticamente el ESP32 DevKitC, es posible que necesites instalar los controladores USB a UART CP210x o CH34x específicos para el chip serie de tu placa.

Materiales

Necesitarás los siguientes componentes exactos para este montaje:
* ESP32 DevKitC (Placa de desarrollo estándar de 38 o 30 pines).
* Módulo transformador de corriente SCT-013 (Específicamente el SCT-013-000, que es un transformador de tipo corriente de 100A/50mA).
* Resistencia de carga (burden): 1x resistencia de 33Ω (1/4 de vatio es suficiente).
* Componentes de polarización de CC: 2x resistencias de 10kΩ y 1x condensador electrolítico de 10µF (necesarios para desplazar la onda de CA al rango de CC legible por el ESP32).
* Placa de adaptación para conector de audio (Breakout): Una placa de adaptación TRS hembra de 3.5mm (para conectar fácilmente el enchufe del SCT-013 a la protoboard).
* Protoboard y cables puente.

Configuración/Conexión

El SCT-013-000 emite una corriente de CA proporcional a la corriente que fluye a través del cable sujeto. Debido a que el convertidor analógico a digital (ADC) del ESP32 solo puede medir voltajes de CC positivos de hasta 3.3V, debemos hacer dos cosas:
1. Convertir la salida de corriente del sensor en un voltaje utilizando una resistencia de carga.
2. Desplazar la forma de onda de CA (que se vuelve positiva y negativa) por completo al rango positivo utilizando una red de polarización de CC (un divisor de voltaje).

Polarización de CC y cableado del sensor

  1. Conecta las dos resistencias de 10kΩ en serie entre el pin 3V3 del ESP32 y GND. La unión entre estas dos resistencias es tu «punto medio», que reposa exactamente a 1.65V.
  2. Conecta la pata positiva (ánodo) del condensador de 10µF a este punto medio de 1.65V, y la pata negativa (cátodo) a GND. Esto suaviza el ruido de la fuente de alimentación.
  3. Conecta la malla (Tierra/Blindaje – Sleeve) de la placa de adaptación del conector de audio de 3.5mm al punto medio de 1.65V.
  4. Conecta la punta (Tip) de la placa de adaptación del conector de audio de 3.5mm al pin 34 del ESP32 (un pin ADC de solo entrada).
  5. Coloca la resistencia de carga de 33Ω directamente a través de las conexiones de la punta (Tip) y la malla (Sleeve) de la placa de adaptación del conector de audio.

Tabla de mapeo de pines

Pin del ESP32 DevKitC Conexión del componente Función
3V3 Resistencia de 10kΩ #1 (Arriba) Proporciona energía de 3.3V para el divisor de polarización de CC.
GND Resistencia de 10kΩ #2 (Abajo), Condensador (-) Referencia de tierra común.
34 (ADC1_CH6) Punta del conector de audio, Lado A de la resistencia de carga Lee el voltaje de CA fluctuante.
N/A (Punto medio) Malla del conector de audio, Lado B de la resistencia de carga Proporciona un desplazamiento de tierra virtual de 1.65V.

Sujeción del sensor

Para medir la corriente, el SCT-013 debe sujetarse alrededor de un solo cable (ya sea el cable de fase/vivo O el cable neutro) del cable de alimentación del recolector de polvo. Si lo sujetas alrededor de un cable de alimentación estándar que contiene ambos cables, los campos magnéticos de las corrientes de salida y retorno se cancelarán entre sí, y el sensor leerá 0 amperios.

Código completo

El siguiente código calcula la corriente RMS matemáticamente muestreando el ADC rápidamente, calculando la varianza de la forma de onda y derivando el verdadero componente de CA. Esto evita dependencias de bibliotecas externas y garantiza la compilación. Crea un nuevo proyecto de PlatformIO para el ESP32 DevKitC y reemplaza los archivos predeterminados 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>

// ---------------------------------------------------------
// Configuration & Constants
// ---------------------------------------------------------
const int ADC_PIN = 34;               // Analog input pin connected to the SCT-013
const float V_REF = 3.3;              // ESP32 ADC reference voltage
const int ADC_RESOLUTION = 4095;      // 12-bit ADC maximum value
const float BURDEN_RESISTOR = 33.0;   // Burden resistor value in Ohms
const float CT_TURNS_RATIO = 2000.0;  // For SCT-013-000: 100A / 0.050A = 2000

void setup() {
    Serial.begin(115200);
    while (!Serial) {
        ; // Wait for serial port to connect
    }

    Serial.println("Dust Collector Current Monitor Initializing...");

    // Configure the ADC pin
    analogReadResolution(12);
    pinMode(ADC_PIN, INPUT);

    Serial.println("Initialization Complete. Monitoring Current...");
}

void loop() {
    unsigned long startMillis = millis();
    double sum = 0;
    double sumSquared = 0;
    int samples = 0;

    // Sample the waveform for 200ms (captures 10-12 full AC cycles)
    while (millis() - startMillis < 200) {
        int raw = analogRead(ADC_PIN);
        sum += raw;
        sumSquared += ((double)raw * raw);
        samples++;

        // Small delay to prevent FreeRTOS watchdog starvation
        delayMicroseconds(100); 
    }
// ...

#include <Arduino.h>

// ---------------------------------------------------------
// Configuration & Constants
// ---------------------------------------------------------
const int ADC_PIN = 34;               // Analog input pin connected to the SCT-013
const float V_REF = 3.3;              // ESP32 ADC reference voltage
const int ADC_RESOLUTION = 4095;      // 12-bit ADC maximum value
const float BURDEN_RESISTOR = 33.0;   // Burden resistor value in Ohms
const float CT_TURNS_RATIO = 2000.0;  // For SCT-013-000: 100A / 0.050A = 2000

void setup() {
    Serial.begin(115200);
    while (!Serial) {
        ; // Wait for serial port to connect
    }

    Serial.println("Dust Collector Current Monitor Initializing...");

    // Configure the ADC pin
    analogReadResolution(12);
    pinMode(ADC_PIN, INPUT);

    Serial.println("Initialization Complete. Monitoring Current...");
}

void loop() {
    unsigned long startMillis = millis();
    double sum = 0;
    double sumSquared = 0;
    int samples = 0;

    // Sample the waveform for 200ms (captures 10-12 full AC cycles)
    while (millis() - startMillis < 200) {
        int raw = analogRead(ADC_PIN);
        sum += raw;
        sumSquared += ((double)raw * raw);
        samples++;

        // Small delay to prevent FreeRTOS watchdog starvation
        delayMicroseconds(100); 
    }

    if (samples > 0) {
        // Calculate the statistical variance of the samples
        // Variance = E[X^2] - (E[X])^2
        double mean = sum / samples;
        double meanSquared = sumSquared / samples;
        double variance = meanSquared - (mean * mean);

        // Prevent negative variance due to floating point inaccuracies
        if (variance < 0) {
            variance = 0;
        }

        // RMS of the AC component is the square root of the variance
        double rmsAdc = sqrt(variance);

        // Convert ADC units to Voltage
        double rmsVoltage = (rmsAdc / ADC_RESOLUTION) * V_REF;

        // Convert Voltage to Primary Current using burden resistor and turns ratio
        double rmsCurrent = (rmsVoltage / BURDEN_RESISTOR) * CT_TURNS_RATIO;

        // Noise suppression: clamp extremely low floating values to 0
        if (rmsCurrent < 0.15) {
            rmsCurrent = 0.0;
        }

        // Print the calculated RMS current to the Serial Monitor
        Serial.print("Samples: ");
        Serial.print(samples);
        Serial.print(" | Dust Collector Current Draw: ");
        Serial.print(rmsCurrent, 2); // Print with 2 decimal places
        Serial.println(" A");
    }

    // Delay before the next sampling window
    delay(800);
}

Comandos de compilación/flasheo/ejecución

Utiliza la interfaz de línea de comandos (CLI) de PlatformIO para compilar, subir y monitorizar el proyecto.

Comando Descripción
pio run Compila el proyecto para verificar la sintaxis.
pio run --target upload Flashea el firmware compilado en el ESP32.
pio device monitor Inicia el monitor serie a 115200 baudios.

Flujo de trabajo de ejecución:
1. Conecta el ESP32 DevKitC a tu ordenador mediante un cable USB con capacidad de datos.
2. Abre tu terminal en el directorio del proyecto de PlatformIO.
3. Ejecuta pio run para verificar que no haya errores de sintaxis.
4. Ejecuta pio run --target upload para escribir el código en el ESP32. (Si la carga agota el tiempo de espera, mantén presionado el botón «BOOT» en el ESP32 cuando aparezca el mensaje «Connecting…»).
5. Ejecuta pio device monitor para ver las lecturas de corriente en tiempo real.

Validación paso a paso

Utiliza este procedimiento para verificar que tu prototipo funciona correctamente antes de implementarlo en el recolector de polvo.

  1. Verificar el rechazo del ruido base
  2. Acción: Enciende el ESP32 con el SCT-013 conectado pero completamente suelto de cualquier cable.
  3. Observación esperada: El monitor serie debería mostrar Dust Collector Current Draw: 0.00 A.
  4. Condición de aprobación: La lógica de supresión de ruido (if (rmsCurrent < 0.15)) fuerza con éxito el ruido flotante menor a cero.

  5. Verificar el punto medio de compensación de CA

  6. Acción: Usando un multímetro digital, mide el voltaje de CC entre el pin GND del ESP32 y el punto medio de las dos resistencias de 10kΩ.
  7. Observación esperada: El multímetro lee aproximadamente 1.65V.
  8. Condición de aprobación: El voltaje está entre 1.6V y 1.7V, lo que confirma que el circuito de polarización está empujando correctamente la onda de CA al rango legible por el ESP32.

  9. Prueba de baja potencia (Monitorización base)

  10. Acción: Sujeta el SCT-013 alrededor del cable de fase de un cable de extensión de CA dividido específicamente para pruebas. Enchufa un electrodoméstico de baja potencia (por ejemplo, una lámpara incandescente de 60W) y aplica energía.
  11. Observación esperada: El monitor serie lee aproximadamente 0.50 A (para una bombilla de 60W a 120V).
  12. Condición de aprobación: La lectura de corriente aumenta proporcionalmente a la carga y se mantiene estable.

  13. Prueba de alta potencia (Simulación de carga del recolector de polvo)

  14. Acción: Sujeta de forma segura el sensor en el cable de fase que alimenta el recolector de polvo real (o una carga de prueba de alta potencia como una pistola de calor de 1000W) y suministra energía.
  15. Observación esperada: El monitor serie lee una carga sostenida (por ejemplo, > 8.00 A).
  16. Condición de aprobación: La consola refleja con precisión el alto consumo de corriente, validando que las matemáticas se escalan correctamente con cargas más grandes.

Solución de problemas

Síntoma Causa probable Solución
La lectura se mantiene en 0.00A cuando la máquina está en marcha Sujeto sobre los cables de fase y neutro. Asegúrate de que el SCT-013 esté sujeto SOLAMENTE alrededor del cable de fase. Usa un divisor de línea de CA.
Lecturas que fluctúan descontroladamente (ej., 5A, 0A, 12A) Condensador de derivación de 10µF faltante o desconectado. Verifica que el condensador esté firmemente asentado entre el punto medio de 1.65V y GND.
Lectura alta constante (ej., 30A+) cuando la máquina está apagada La resistencia de carga está desconectada o tiene un valor incorrecto. Revisa la conexión de la resistencia de 33Ω a través de la punta y la malla del conector de audio.
El monitor serie imprime caracteres sin sentido Discrepancia en la tasa de baudios. Asegúrate de que la terminal esté configurada a 115200 baudios, coincidiendo con Serial.begin(115200).

Mejoras

Una vez que el monitor de corriente básico funcione de manera confiable, considera estas mejoras para una instalación permanente en el taller:

Integración inalámbrica y de IoT
* Publicación MQTT: Conecta el ESP32 a Wi-Fi y publica pasivamente la corriente RMS en un broker MQTT. Un servidor central puede suscribirse a este tema para registrar datos en un panel de Grafana y obtener un perfil de energía a largo plazo del recolector de polvo.
* Actualizaciones inalámbricas (OTA): Implementa ArduinoOTA para que puedas ajustar las constantes de calibración sin retirar físicamente el ESP32 de su carcasa a prueba de polvo.

Hardware y robustez
* Filtrado por hardware: Agrega un pequeño condensador de 10nF en paralelo con la resistencia de carga para que actúe como un filtro de paso bajo por hardware, suavizando aún más el ruido eléctrico de alta frecuencia del taller antes de que llegue al ADC.

Lista de verificación

  • [ ] Circuito de polarización de CC en protoboard construido (dos resistencias de 10kΩ, un condensador de 10µF).
  • [ ] Resistencia de carga de 33Ω asegurada a través de las líneas de salida del SCT-013.
  • [ ] Punta del SCT-013 conectada al pin 34 del ESP32; Malla conectada al punto medio de 1.65V.
  • [ ] platformio.ini configurado correctamente.
  • [ ] Código flasheado con éxito usando pio run --target upload.
  • [ ] SCT-013 sujeto de forma segura alrededor de un solo cable de CA aislado (fase o neutro, no ambos).
  • [ ] El monitor serie muestra >0A y se escala con precisión cuando se suministra energía al electrodoméstico de prueba.

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: ¿Cuál es el objetivo principal del proyecto descrito en el artículo?




Pregunta 2: ¿Qué microcontrolador se utiliza para la construcción de este monitor de corriente?




Pregunta 3: ¿Qué tipo de energía registra este monitor en el sistema de recolección de polvo?




Pregunta 4: ¿Para qué sirve establecer un perfil base de uso eléctrico en este proyecto?




Pregunta 5: ¿Qué problema mecánico puede indicar un pico de corriente anormalmente alto?




Pregunta 6: ¿Qué sucede con el consumo de corriente de los ventiladores centrífugos cuando el flujo de aire se restringe?




Pregunta 7: ¿Qué anomalía puede indicar una disminución gradual o caída inesperada en el consumo de corriente base?




Pregunta 8: ¿Cuál es el propósito del 'Perfilado de energía' mencionado en el texto?




Pregunta 9: ¿Qué característica define al monitor de corriente que se va a construir?




Pregunta 10: Según el texto, ¿qué componente específico del motor podría estar fallando si se detectan sobretensiones repentinas?




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:


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

Componente Pin / Cable del componente Pin del ESP32 DevKitC Notas
Interruptor de flotador bajo Cable 1 GPIO 27 Fondo del tanque (Protección contra funcionamiento en seco).
Interruptor de flotador bajo Cable 2 GND Se cierra a GND cuando el flotador está ARRIBA (presencia de agua).
Interruptor de flotador alto Cable 1 GPIO 14 Parte superior del tanque (Indicador de lleno).
Interruptor de flotador alto Cable 2 GND Se cierra a GND cuando el flotador está ARRIBA (tanque lleno).
Módulo de relé de 5 V VCC 5V / VIN Alimenta la bobina del relé.
Módulo de relé de 5 V GND GND Tierra común.
Módulo de relé de 5 V IN (Señal) GPIO 26 Señ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();
                });
        }
// ...

#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:


Caso práctico: Monitor de buzón IoT con ESP32

Caso práctico: Monitor de buzón IoT con ESP32 — ESP32 con reed switch, imán y módulo LiPo para detectar apertura de buzón

Objetivo y caso de uso

Qué vas a construir: Un monitor de buzón IoT alimentado por batería y altamente eficiente que permanece en un estado de suspensión profunda de micropotencia, se despierta instantáneamente cuando la puerta del buzón se abre, transmite una alerta por Wi-Fi a través de una solicitud HTTP GET y vuelve a suspenderse inmediatamente.

Por qué es importante / Casos de uso

  • Elimina viajes innecesarios: Los usuarios ya no necesitan revisar físicamente un buzón vacío en condiciones climáticas adversas o a través de entradas largas.
  • Maximiza el ciclo de vida de la batería: Al utilizar las capacidades de suspensión profunda del ESP32 (consumiendo ~10µA), el dispositivo minimiza los ciclos de trabajo activos, demostrando habilidades cruciales de gestión de energía.
  • Arquitectura orientada a eventos: Enseña la transición del sondeo continuo a los despertares impulsados por interrupciones de hardware, un concepto fundamental en las redes de sensores comerciales.
  • Seguridad y registro de acceso: La misma arquitectura puede reutilizarse para monitorear armarios restringidos, cajas fuertes o puertas perimetrales sin requerir energía cableada.

Resultado esperado

  • El ESP32 entra en un estado de suspensión profunda consumiendo una corriente mínima (menos de 15µA).
  • Abrir la puerta activa un despertar por hardware a través del GPIO del RTC (Reloj de Tiempo Real) en menos de 500 milisegundos.
  • El dispositivo se conecta exitosamente al Wi-Fi, transmite el payload HTTP y se apaga en menos de 3 segundos para preservar la vida útil de la batería.

Audiencia: Desarrolladores de IoT, Ingenieros de Sistemas Embebidos; Nivel: Intermedio

Arquitectura/flujo: Interruptor Reed (Interrupción de hardware) → Despertar RTC del ESP32 (latencia <500ms) → Conexión Wi-Fi → Solicitud HTTP GET → Suspensión profunda (Deep Sleep)

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: 3 apartados, 3 tablas y 2 bloques de código detectados antes de publicar.
  • Código comprobado: 1 PlatformIO config + 1 ESP32 source/pio run.
  • 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

Advertencia sobre los límites del prototipo: Este proyecto está destinado estrictamente como un prototipo educativo.
* Seguridad de la batería: Este proyecto involucra baterías de polímero de litio (LiPo). El mal manejo, la sobrecarga o el cortocircuito de las baterías LiPo pueden provocar incendios o explosiones. Utiliza siempre un controlador de carga dedicado (como el TP4056 con circuito de protección integrado) y nunca conectes una celda LiPo directamente al ESP32 sin una regulación de voltaje adecuada. No dejes circuitos de baterías experimentales desatendidos mientras se cargan. Asegúrate de que tu módulo de batería incluya protección contra sobredescarga para evitar daños a la celda durante despliegues prolongados.

Diagrama de bloques conceptual

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

Arquitectura funcional

Botón local

ESP32 BLE

Paquete advertising

LED de estado

Móvil escáner

Flujo conceptual: configuración local, emisión BLE y lectura desde el móvil.

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

Antes de comenzar este tutorial, asegúrate de tener listo lo siguiente:
* Una computadora con Windows, macOS o Linux.
* Visual Studio Code (VSCode) instalado.
* La extensión PlatformIO IDE instalada en VSCode.
* Familiaridad básica con la sintaxis de C++ y circuitos electrónicos (redes pull-up/pull-down).
* Una red Wi-Fi de 2.4 GHz (ESP32 no soporta Wi-Fi de 5 GHz).
* Un servidor HTTP local o un endpoint de webhook para recibir la alerta.

Materiales

Para este proyecto, utiliza la siguiente configuración exacta de hardware:
* ESP32 DevKitC: La placa del microcontrolador principal que incluye el módulo ESP-WROOM-32.
* Interruptor Reed: Un interruptor de contacto magnético (normalmente abierto). Cuando el imán está cerca, el interruptor se cierra. Cuando se retira el imán (se abre la puerta), el interruptor se abre.
* Módulo de batería LiPo: Una batería de polímero de litio de 3.7V emparejada con un módulo de carga/elevación de 5V (por ejemplo, un módulo de power bank basado en TP4056) para suministrar 5V de manera segura al pin 5V (o VIN) del ESP32 DevKitC.
* Componentes pasivos:
* Una resistencia de 10kΩ (usada como resistencia pull-down para el interruptor Reed).
* Accesorios: Protoboard, cables puente (jumpers) y un cable de datos micro-USB.

Configuración/Conexión

Notas de configuración de hardware

El ESP32 DevKitC requiere controladores específicos para comunicarse por USB. Dependiendo de la revisión exacta de tu placa, utilizará un puente USB a UART CP210x o CH34x. Si tu sistema operativo o PlatformIO no reconocen tu placa, descarga e instala los controladores oficiales CP210x de Silicon Labs o CH340 de WCH.

Lógica del circuito

El núcleo de este proyecto es la interrupción de hardware. El procesador principal del ESP32 se apaga durante la suspensión profunda. Solo el coprocesador de ultra bajo consumo (ULP) y la memoria/periféricos del RTC permanecen activos. Usaremos la fuente de despertar ext0, que permite que un GPIO del RTC despierte al procesador principal cuando se detecta un nivel lógico específico.

Usaremos el GPIO 33 (un pin compatible con RTC) para el interruptor Reed.
* Buzón cerrado: El imán está cerca del interruptor Reed. El interruptor está cerrado, conectando el GPIO 33 a 3.3V. El ESP32 ve una señal HIGH.
* Buzón abierto: La puerta se abre, el imán se aleja y el interruptor se abre. La resistencia pull-down de 10kΩ fuerza el GPIO 33 a LOW.
* Condición de despertar: Configuraremos el ESP32 para que se despierte cuando el GPIO 33 cambie a LOW.

Tabla de cableado

Componente Pin/Terminal Pin del ESP32 DevKitC Notas
Módulo de batería LiPo Salida 5V / VOUT 5V (o VIN) Proporciona energía cuando el USB está desconectado.
Módulo de batería LiPo GND / Tierra GND Tierra común.
Interruptor Reed Terminal 1 3V3 Proporciona 3.3V cuando el interruptor está cerrado.
Interruptor Reed Terminal 2 GPIO 33 Pin de señal para el despertar RTC.
Resistencia de 10kΩ Pata 1 GPIO 33 Configuración pull-down.
Resistencia de 10kΩ Pata 2 GND Configuración pull-down.

Nota: Desconecta la línea de 5V del módulo de batería LiPo del ESP32 cuando conectes el ESP32 a tu computadora por USB para evitar conflictos de energía.

Firmware de la aplicación

El siguiente código maneja la transición a la suspensión profunda, la conexión Wi-Fi, la solicitud HTTP y la lógica de despertar por hardware.

platformio.ini

Crea un nuevo proyecto de PlatformIO para el ESP32 DevKitC y reemplaza el contenido de platformio.ini con la siguiente configuración:

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

src/main.cpp

Reemplaza el contenido de src/main.cpp con el siguiente código. Actualiza las variables de red y del webhook para que coincidan con tu entorno antes de la implementación.

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 <HTTPClient.h>

// ==========================================
// Configuration
// ==========================================
const char* WIFI_SSID     = "IoT_Network";
const char* WIFI_PASSWORD = "SecurePassword123";
const char* WEBHOOK_URL   = "http://192.168.1.100/mailbox-alert";

// Pin Definitions
const gpio_num_t REED_SWITCH_PIN = GPIO_NUM_33; // Must be an RTC GPIO for ext0 wakeup

// Timeouts
const int WIFI_TIMEOUT_MS = 10000; // 10 seconds max to connect

// ==========================================
// Helper Functions
// ==========================================

void printWakeupReason() {
    esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();
    switch(wakeup_reason) {
        case ESP_SLEEP_WAKEUP_EXT0:     
            Serial.println("Wakeup caused by external signal using RTC_IO (Door Opened!)"); 
            break;
        case ESP_SLEEP_WAKEUP_TIMER:    
            Serial.println("Wakeup caused by timer"); 
            break;
        default:                        
            Serial.printf("Wakeup was not caused by deep sleep: %d\n", wakeup_reason); 
            break;
    }
}

void sendAlert() {
    Serial.print("Connecting to Wi-Fi: ");
    Serial.println(WIFI_SSID);

    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

    unsigned long startAttemptTime = millis();

    // Wait for connection with a timeout to prevent draining battery
    while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < WIFI_TIMEOUT_MS) {
        Serial.print(".");
        delay(500);
    }

    Serial.println();

    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("Failed to connect to Wi-Fi. Going back to sleep.");
        return; // Abort and let the main loop put the device to sleep
    }
// ...

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

// ==========================================
// Configuration
// ==========================================
const char* WIFI_SSID     = "IoT_Network";
const char* WIFI_PASSWORD = "SecurePassword123";
const char* WEBHOOK_URL   = "http://192.168.1.100/mailbox-alert";

// Pin Definitions
const gpio_num_t REED_SWITCH_PIN = GPIO_NUM_33; // Must be an RTC GPIO for ext0 wakeup

// Timeouts
const int WIFI_TIMEOUT_MS = 10000; // 10 seconds max to connect

// ==========================================
// Helper Functions
// ==========================================

void printWakeupReason() {
    esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();
    switch(wakeup_reason) {
        case ESP_SLEEP_WAKEUP_EXT0:     
            Serial.println("Wakeup caused by external signal using RTC_IO (Door Opened!)"); 
            break;
        case ESP_SLEEP_WAKEUP_TIMER:    
            Serial.println("Wakeup caused by timer"); 
            break;
        default:                        
            Serial.printf("Wakeup was not caused by deep sleep: %d\n", wakeup_reason); 
            break;
    }
}

void sendAlert() {
    Serial.print("Connecting to Wi-Fi: ");
    Serial.println(WIFI_SSID);

    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

    unsigned long startAttemptTime = millis();

    // Wait for connection with a timeout to prevent draining battery
    while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < WIFI_TIMEOUT_MS) {
        Serial.print(".");
        delay(500);
    }

    Serial.println();

    if (WiFi.status() != WL_CONNECTED) {
        Serial.println("Failed to connect to Wi-Fi. Going back to sleep.");
        return; // Abort and let the main loop put the device to sleep
    }

    Serial.println("Wi-Fi connected!");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());

    // Send HTTP GET request
    if (WiFi.status() == WL_CONNECTED) {
        HTTPClient http;
        Serial.print("Sending alert to: ");
        Serial.println(WEBHOOK_URL);

        http.begin(WEBHOOK_URL);
        int httpResponseCode = http.GET();

        if (httpResponseCode > 0) {
            Serial.print("HTTP Response code: ");
            Serial.println(httpResponseCode);
        } else {
            Serial.print("Error code: ");
            Serial.println(httpResponseCode);
        }
        http.end();
    }

    // Disconnect Wi-Fi to save power before sleeping
    WiFi.disconnect(true);
    WiFi.mode(WIFI_OFF);
}

// ==========================================
// Main Setup and Loop
// ==========================================

void setup() {
    Serial.begin(115200);
    delay(1000); // Allow serial monitor to catch up

    Serial.println("\n--- Mailbox Monitor Booting ---");

    // Determine why the ESP32 woke up
    esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();
    printWakeupReason();

    // If woke up because the door opened (EXT0)
    if (wakeup_reason == ESP_SLEEP_WAKEUP_EXT0) {
        sendAlert();
    } else {
        // Normal boot (e.g., first power on or reset)
        Serial.println("Initial boot. System is armed and ready.");
    }

    // Configure Deep Sleep Wakeup
    // Door open = magnet away = switch open = pull-down resistor pulls to LOW (0).
    // Wake up when GPIO 33 goes LOW (0).
    esp_sleep_enable_ext0_wakeup(REED_SWITCH_PIN, 0); 

    Serial.println("Entering deep sleep now. Waiting for door to open...");
    Serial.flush(); 

    // Enter Deep Sleep
    esp_deep_sleep_start();
}

void loop() {
    // The loop is intentionally empty.
    // The ESP32 goes to sleep in setup() and resets entirely upon waking.
}

Comandos de Compilación/Flasheo/Ejecución

Para desplegar el código en tu ESP32 DevKitC usando PlatformIO, utiliza la terminal integrada en VSCode.

Referencia de comandos

Acción Comando Propósito
Compilar código pio run Compila el código C++ y las bibliotecas sin subirlo.
Subir firmware pio run --target upload Flashea el binario compilado al ESP32 por USB.
Monitor serie pio device monitor Abre la terminal para ver las salidas de Serial.print.

Flujo de trabajo

  1. Conecta el ESP32 DevKitC a tu computadora usando un cable micro-USB con capacidad de transferencia de datos.
  2. Abre la terminal de VSCode en el directorio de tu proyecto.
  3. Compila y sube el firmware ejecutando:
    pio run --target upload
  4. Abre inmediatamente el monitor serie para observar la secuencia de arranque:
    pio device monitor

Validación paso a paso

Sigue estos puntos de control agrupados para asegurarte de que tu prototipo funcione correctamente.

  1. Arranque inicial y armado
    • Acción: Presiona el botón EN (Reinicio) en el ESP32.
    • Observación esperada: El monitor serie imprime «Initial boot. System is armed and ready.» Luego, el monitor imprime «Entering deep sleep now.»
    • Condición de éxito: El dispositivo entra exitosamente en suspensión profunda sin volver a despertarse inmediatamente.
  2. Simulando buzón cerrado
    • Acción: Coloca el imán directamente junto al interruptor Reed.
    • Observación esperada: No sucede nada. El ESP32 permanece en suspensión profunda.
    • Condición de éxito: El sistema ignora el estado cerrado (el GPIO 33 está en HIGH).
  3. Activando el despertar (Buzón abierto)
    • Acción: Aleja rápidamente el imán del interruptor Reed.
    • Observación esperada: El monitor serie imprime «Wakeup caused by external signal using RTC_IO (Door Opened!)».
    • Condición de éxito: La interrupción de hardware despierta exitosamente al ESP32 de la suspensión profunda.
  4. Conexión de red y transmisión
    • Acción: Espera de 2 a 5 segundos mientras observas el monitor serie.
    • Observación esperada: El monitor imprime «Wi-Fi connected!», seguido de la dirección IP y luego «HTTP Response code: 200».
    • Condición de éxito: El endpoint del webhook registra la solicitud GET entrante.
  5. Retorno a la suspensión
    • Acción: Observa las salidas finales del monitor serie.
    • Observación esperada: El monitor imprime «Entering deep sleep now. Waiting for door to open…»
    • Condición de éxito: El ESP32 interrumpe la conexión Wi-Fi y apaga su procesador principal, listo para el siguiente evento.

Solución de problemas

Si el sistema no se comporta como se espera, consulta la siguiente tabla.

Síntoma Causa probable Solución
El código no se sube (Tiempo de espera agotado) El ESP32 no está entrando en modo bootloader. Mantén presionado el botón BOOT en el DevKitC mientras ejecutas el comando de subida, y suéltalo cuando comience la conexión.
Bucles constantes de despertar La lógica del interruptor Reed está flotante. Verifica que la resistencia pull-down de 10kΩ esté conectada de forma segura entre el GPIO 33 y GND. Asegúrate de que el imán esté lo suficientemente cerca para cerrar el interruptor inicialmente.
La conexión Wi-Fi falla / Tiempo de espera agotado Credenciales incorrectas o red de 5GHz. Verifica WIFI_SSID y WIFI_PASSWORD. Asegúrate de que tu enrutador esté transmitiendo una red de 2.4GHz.
Detector de caída de voltaje (Brownout) activado Energía insuficiente durante la transmisión Wi-Fi. El puerto USB o el módulo LiPo no pueden suministrar el pico de ~300mA requerido para el Wi-Fi. Intenta usar un cable USB de mejor calidad o asegúrate de que la batería LiPo esté completamente cargada.
Sin salida en el monitor serie Discrepancia en los baudios. Asegúrate de que monitor_speed = 115200 esté configurado en platformio.ini y coincida con Serial.begin(115200) en el código.

Mejoras

Una vez que se haya validado el prototipo básico, considera estas mejoras para un dispositivo listo para producción:

  • Optimización de energía:
    • IP estática: Asigna una dirección IP estática al ESP32 en lugar de usar DHCP. Esto puede reducir el tiempo de conexión Wi-Fi de ~3 segundos a menos de 500ms, ahorrando drásticamente la vida útil de la batería.
    • Reemplazo del LDO de hardware: Las placas ESP32 DevKitC estándar utilizan un regulador de voltaje AMS1117 con una alta corriente de reposo (~1.5mA). Para un verdadero funcionamiento con micropotencia, haz la transición a una placa personalizada o a un regulador de baja caída (LDO) como el HT7333.
  • Conectividad y confiabilidad:
    • Implementación de MQTT: Reemplaza la solicitud HTTP GET con una publicación MQTT a través de la biblioteca PubSubClient. MQTT tiene menor sobrecarga y es más adecuado para ecosistemas IoT.
    • Monitoreo del voltaje de la batería: Conecta la salida de la batería LiPo a un pin ADC del ESP32 (a través de un divisor de voltaje) para transmitir el estado de la batería junto con la alerta de puerta abierta.

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: ¿Cuál es el objetivo principal del proyecto descrito en el artículo?




Pregunta 2: ¿Qué método utiliza el dispositivo para enviar la alerta cuando se abre el buzón?




Pregunta 3: ¿Cuál es el consumo de corriente aproximado del ESP32 durante el estado de suspensión profunda según el texto?




Pregunta 4: ¿Qué concepto fundamental de redes de sensores comerciales enseña este proyecto?




Pregunta 5: Además de buzones, ¿qué otro caso de uso de seguridad se menciona para esta arquitectura?




Pregunta 6: ¿Cuál es el límite máximo de consumo de corriente esperado cuando el ESP32 entra en estado de suspensión profunda?




Pregunta 7: ¿En cuánto tiempo se activa el despertar por hardware cuando se abre la puerta?




Pregunta 8: ¿A través de qué componente se activa el despertar por hardware del ESP32?




Pregunta 9: ¿Cuánto tiempo tarda el dispositivo en conectarse al Wi-Fi, transmitir el payload y apagarse?




Pregunta 10: ¿A qué nivel de audiencia está dirigido este artículo?




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:


Caso práctico: Monitor de lavadora con ESP32

Caso práctico: Monitor de lavadora con ESP32 — hero

Objetivo y caso de uso

Qué construirás: Un monitor no invasivo para lavadoras utilizando un ESP32 y un sensor de vibración SW-420 que detecta cuándo termina un ciclo de lavado. Utiliza lógica de tiempo de espera basada en flancos (edge-based) para activar una alarma local con zumbador/LED y una alerta HTTP por WiFi con latencia de subsegundos.

Por qué es importante / Casos de uso

  • Evita olvidar la ropa: Proporciona notificaciones inmediatas cuando la máquina se detiene, evitando el moho y la necesidad de volver a lavar.
  • Optimiza instalaciones compartidas: Reduce los tiempos de espera y los viajes innecesarios a la lavandería en residencias estudiantiles o edificios de apartamentos.
  • Monitorización no invasiva: Se adapta de forma segura a electrodomésticos antiguos «no inteligentes» al depender de vibraciones externas en lugar de circuitos de alto voltaje.
  • Lógica de máquina de estados basada en flancos: Enseña lógica de antirrebote (debouncing) y de tiempo de espera (timeout) para manejar las pausas normales de los electrodomésticos (ej. ventanas de inactividad de 3-5 minutos entre ciclos de lavado, enjuague y centrifugado).

Resultado esperado

  • Un prototipo funcional con ESP32 que detecta de manera fiable los estados de vibración y filtra las paradas falsas.
  • Alerta audiovisual local mediante un zumbador piezoeléctrico y un LED de estado.
  • Una notificación HTTP activada por WiFi entregada en < 500ms cuando la máquina completa oficialmente su ciclo.

Audiencia: Aficionados al IoT, estudiantes de electrónica y entusiastas de la domótica; Nivel: Principiante a Intermedio

Arquitectura/flujo: Sensor SW-420 → ESP32 (Máquina de estados con antirrebote y tiempo de espera) → Piezo/LED local y alerta HTTP POST por WiFi

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: 3 apartados, 3 tablas y 2 bloques de código detectados antes de publicar.
  • Código comprobado: 1 PlatformIO config + 1 ESP32 source/pio run.
  • 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 el montaje, verifica el esquema de pines (pinout) de tu placa ESP32 DevKitC exacta, mantén las señales de los GPIO dentro de los límites de 3.3 V, nunca apliques 5 V a las entradas del ESP32, desconecta la alimentación antes de cambiar el cableado y utiliza 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

Antes de comenzar este proyecto, asegúrate de tener listo lo siguiente:
* Entorno de software: Visual Studio Code (VS Code) con la extensión PlatformIO IDE instalada.
* Conocimientos básicos de C++: Familiaridad con variables, sentencias if/else y la función millis() para retardos no bloqueantes.
* Acceso a la red: Una red WiFi de 2.4GHz con credenciales de SSID y contraseña conocidas.
* Controladores USB: Los controladores USB a UART CP210x o CH34x instalados en tu computadora (dependiendo de tu variante específica de ESP32 DevKitC) para permitir la comunicación serial y el flasheo.


Materiales

Necesitarás los siguientes componentes exactos para este montaje:
* ESP32 DevKitC: El microcontrolador central, que proporciona tanto la lógica GPIO como capacidades WiFi de 2.4GHz integradas.
* Sensor de vibración SW-420: Un módulo de vibración digital que cuenta con un comparador de voltaje LM393 y un potenciómetro integrado para ajustar el umbral.
* Zumbador piezoeléctrico (Piezo buzzer): Un módulo de zumbador piezoeléctrico activo (suena continuamente cuando recibe una señal digital HIGH).
* LED de estado: Un LED estándar de 5mm (de cualquier color, ej. azul o verde) emparejado con una resistencia limitadora de corriente de 220Ω.
* Suministros de prototipado: Una placa de pruebas (breadboard) estándar, varios cables puente (jumper) macho-macho y macho-hembra, y un cable micro-USB para programación y alimentación.


Configuración/Conexión

El sensor SW-420 emite una señal digital: permanece en LOW cuando está quieto, y emite pulsos HIGH cuando la vibración supera el umbral establecido por su potenciómetro integrado. El ESP32 leerá este pin digital.

Realiza las conexiones de acuerdo con el siguiente mapeo. Asegúrate de que tu ESP32 esté desconectado de la alimentación USB mientras realizas el cableado.

Componente Pin del componente Pin del ESP32 DevKitC Notas
Sensor SW-420 VCC 3V3 Alimenta el comparador LM393.
Sensor SW-420 GND GND Tierra común.
Sensor SW-420 DO (Salida Digital) GPIO 13 Envía pulsos HIGH durante la vibración.
Zumbador piezoeléctrico VCC / + GPIO 14 Se pone en HIGH para hacer sonar la alarma.
Zumbador piezoeléctrico GND / – GND Tierra común.
LED de estado Ánodo (Pata larga) GPIO 27 Conectar mediante una resistencia de 220Ω.
LED de estado Cátodo (Pata corta) GND Tierra común.

Nota sobre la corriente del zumbador: Los zumbadores piezoeléctricos activos estándar consumen alrededor de 10-30mA, lo cual es seguro para manejar directamente desde un pin GPIO del ESP32 (máx. 40mA por pin). Si estás utilizando una sirena más grande o un zumbador de alta potencia, debes usar un transistor de conmutación (como un 2N2222) o un módulo de relé.


Código validado

El siguiente código implementa una máquina de estados no bloqueante. Para probar la lógica rápidamente, el tiempo de espera del ciclo se establece en 15 segundos. En una implementación en el mundo real, deberías aumentar esto a 3-5 minutos para tener en cuenta las pausas de remojo/vaciado en un ciclo de lavadora.

Crea un nuevo proyecto de PlatformIO para la placa esp32dev y reemplaza el contenido de los archivos generados 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 <HTTPClient.h>

// ---------------------------------------------------------
// Configuration: WiFi & Webhook
// ---------------------------------------------------------
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";

// For demonstration, we use httpbin.org to echo the GET request.
// In a real application, replace this with an IFTTT Webhook, 
// Home Assistant endpoint, or custom API URL.
const char* WEBHOOK_URL = "http://httpbin.org/get?laundry=finished";

// ---------------------------------------------------------
// Configuration: Hardware Pins
// ---------------------------------------------------------
const int SW420_PIN = 13;
const int BUZZER_PIN = 14;
const int LED_PIN = 27;

// ---------------------------------------------------------
// Configuration: Timing & State Machine
// ---------------------------------------------------------
// Time required without vibration to consider the cycle "finished".
// Set to 15 seconds (15000ms) for testing. 
// For real laundry machines, use 180000ms (3 minutes) or more.
const unsigned long CYCLE_TIMEOUT_MS = 15000; 

enum MachineState {
    STATE_IDLE,
    STATE_RUNNING,
    STATE_FINISHED
};

MachineState currentState = STATE_IDLE;
unsigned long lastVibrationTime = 0;
unsigned long lastAlertToggleTime = 0;
bool alertToggleState = false;

// Function prototypes
void connectToWiFi();
void sendWiFiAlert();
void handleAlertHardware();

void setup() {
    Serial.begin(115200);
    delay(1000);
    Serial.println("\n--- Laundry Vibration WiFi Alert ---");

    // Initialize pins
    pinMode(SW420_PIN, INPUT);
    pinMode(BUZZER_PIN, OUTPUT);
    pinMode(LED_PIN, OUTPUT);

    // Ensure outputs are off initially
    digitalWrite(BUZZER_PIN, LOW);
    digitalWrite(LED_PIN, LOW);

    connectToWiFi();

    Serial.println("System initialized. Waiting for vibration...");
}

void loop() {
    unsigned long currentMillis = millis();
    int vibrationDetected = digitalRead(SW420_PIN);

    // 1. Read Sensor & Update Timers
    if (vibrationDetected == HIGH) {
        lastVibrationTime = currentMillis;

        // If we were idle or already finished, a new vibration means a cycle is starting/restarting
        if (currentState == STATE_IDLE || currentState == STATE_FINISHED) {
            currentState = STATE_RUNNING;
            Serial.println("STATUS: Machine is now RUNNING.");
            // Ensure alert hardware is turned off when returning to running
            digitalWrite(BUZZER_PIN, LOW);
            digitalWrite(LED_PIN, HIGH); // Solid LED indicates running
        }
    }
// ...

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

// ---------------------------------------------------------
// Configuration: WiFi & Webhook
// ---------------------------------------------------------
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";

// For demonstration, we use httpbin.org to echo the GET request.
// In a real application, replace this with an IFTTT Webhook, 
// Home Assistant endpoint, or custom API URL.
const char* WEBHOOK_URL = "http://httpbin.org/get?laundry=finished";

// ---------------------------------------------------------
// Configuration: Hardware Pins
// ---------------------------------------------------------
const int SW420_PIN = 13;
const int BUZZER_PIN = 14;
const int LED_PIN = 27;

// ---------------------------------------------------------
// Configuration: Timing & State Machine
// ---------------------------------------------------------
// Time required without vibration to consider the cycle "finished".
// Set to 15 seconds (15000ms) for testing. 
// For real laundry machines, use 180000ms (3 minutes) or more.
const unsigned long CYCLE_TIMEOUT_MS = 15000; 

enum MachineState {
    STATE_IDLE,
    STATE_RUNNING,
    STATE_FINISHED
};

MachineState currentState = STATE_IDLE;
unsigned long lastVibrationTime = 0;
unsigned long lastAlertToggleTime = 0;
bool alertToggleState = false;

// Function prototypes
void connectToWiFi();
void sendWiFiAlert();
void handleAlertHardware();

void setup() {
    Serial.begin(115200);
    delay(1000);
    Serial.println("\n--- Laundry Vibration WiFi Alert ---");

    // Initialize pins
    pinMode(SW420_PIN, INPUT);
    pinMode(BUZZER_PIN, OUTPUT);
    pinMode(LED_PIN, OUTPUT);

    // Ensure outputs are off initially
    digitalWrite(BUZZER_PIN, LOW);
    digitalWrite(LED_PIN, LOW);

    connectToWiFi();

    Serial.println("System initialized. Waiting for vibration...");
}

void loop() {
    unsigned long currentMillis = millis();
    int vibrationDetected = digitalRead(SW420_PIN);

    // 1. Read Sensor & Update Timers
    if (vibrationDetected == HIGH) {
        lastVibrationTime = currentMillis;

        // If we were idle or already finished, a new vibration means a cycle is starting/restarting
        if (currentState == STATE_IDLE || currentState == STATE_FINISHED) {
            currentState = STATE_RUNNING;
            Serial.println("STATUS: Machine is now RUNNING.");
            // Ensure alert hardware is turned off when returning to running
            digitalWrite(BUZZER_PIN, LOW);
            digitalWrite(LED_PIN, HIGH); // Solid LED indicates running
        }
    }

    // 2. Evaluate State Machine
    if (currentState == STATE_RUNNING) {
        // Check if the timeout has elapsed since the last detected vibration
        if (currentMillis - lastVibrationTime > CYCLE_TIMEOUT_MS) {
            currentState = STATE_FINISHED;
            Serial.println("STATUS: Cycle FINISHED! Triggering alerts.");
            digitalWrite(LED_PIN, LOW); // Turn off solid LED
            sendWiFiAlert();
        }
    }

    // 3. Handle Hardware Outputs based on State
    if (currentState == STATE_FINISHED) {
        handleAlertHardware();
    } else if (currentState == STATE_IDLE) {
        // Optional: Pulse LED slowly to show system is alive but idle
        digitalWrite(LED_PIN, (currentMillis / 1000) % 2 == 0);
    }

    // Small delay to prevent tight-loop debouncing issues
    delay(50);
}

// ---------------------------------------------------------
// Helper Functions
// ---------------------------------------------------------

void connectToWiFi() {
    Serial.print("Connecting to WiFi: ");
    Serial.println(WIFI_SSID);

    WiFi.mode(WIFI_STA);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

    int attempts = 0;
    while (WiFi.status() != WL_CONNECTED && attempts < 20) {
        delay(500);
        Serial.print(".");
        attempts++;
    }

    if (WiFi.status() == WL_CONNECTED) {
        Serial.println("\nWiFi connected.");
        Serial.print("IP Address: ");
        Serial.println(WiFi.localIP());
    } else {
        Serial.println("\nFailed to connect to WiFi. Continuing offline.");
    }
}

void sendWiFiAlert() {
    if (WiFi.status() == WL_CONNECTED) {
        HTTPClient http;
        Serial.print("Sending HTTP GET to: ");
        Serial.println(WEBHOOK_URL);

        http.begin(WEBHOOK_URL);
        int httpResponseCode = http.GET();

        if (httpResponseCode > 0) {
            Serial.print("HTTP Response code: ");
            Serial.println(httpResponseCode);
        } else {
            Serial.print("Error code: ");
            Serial.println(httpResponseCode);
        }
        http.end();
    } else {
        Serial.println("WiFi disconnected. Cannot send webhook alert.");
    }
}

void handleAlertHardware() {
    // Non-blocking toggle for Buzzer and LED (beep every 500ms)
    unsigned long currentMillis = millis();
    if (currentMillis - lastAlertToggleTime >= 500) {
        lastAlertToggleTime = currentMillis;
        alertToggleState = !alertToggleState;

        digitalWrite(BUZZER_PIN, alertToggleState ? HIGH : LOW);
        digitalWrite(LED_PIN, alertToggleState ? HIGH : LOW);
    }
}


Comandos de Compilación/Flasheo/Ejecución

Utiliza la CLI de PlatformIO Core para compilar, subir y monitorizar el proyecto. Abre el terminal integrado en VS Code y ejecuta los siguientes comandos.

Comando Propósito
pio run Compila el código fuente en C++ y comprueba si hay errores de sintaxis.
pio run --target upload Flashea el firmware compilado en el ESP32 conectado a través de USB.
pio device monitor Abre el monitor serie para ver la salida de la consola del ESP32.

Flujo de trabajo de ejecución:
1. Actualiza WIFI_SSID y WIFI_PASSWORD en src/main.cpp con las credenciales reales de tu red.
2. Conecta el ESP32 DevKitC a tu computadora mediante micro-USB.
3. Ejecuta pio run para verificar que el código se compila correctamente.
4. Ejecuta pio run --target upload para flashear el dispositivo. (Si la subida no comienza, mantén presionado el botón «BOOT» en el ESP32 hasta que aparezca el porcentaje de progreso de flasheo).
5. Ejecuta pio device monitor para observar la secuencia de arranque, la conexión WiFi y los estados del sensor.


Validación paso a paso

Sigue estos puntos de control para asegurarte de que tu prototipo funciona correctamente.

  1. Arranque y conexión WiFi
    • Observación: Observa el monitor serie después de reiniciar el ESP32.
    • Condición de éxito: La consola muestra «Connecting to WiFi…», seguido de «WiFi connected.» y una dirección IP asignada.
  2. Verificación de estado inactivo (Idle)
    • Observación: Deja el sensor completamente quieto sobre tu escritorio.
    • Condición de éxito: El monitor serie dice «System initialized. Waiting for vibration…» y el LED de estado parpadea lentamente (1Hz). No suena ningún zumbador.
  3. Detección de vibración (Estado de funcionamiento)
    • Observación: Golpea suavemente el sensor SW-420 con tu dedo o golpea la mesa en la que descansa.
    • Condición de éxito: El monitor serie imprime inmediatamente «STATUS: Machine is now RUNNING.» El LED de estado se enciende de forma fija.
  4. Finalización del ciclo (Tiempo de espera y alerta)
    • Observación: Deja de golpear y espera exactamente 15 segundos (el CYCLE_TIMEOUT_MS).
    • Condición de éxito: El monitor serie imprime «STATUS: Cycle FINISHED! Triggering alerts.» El LED y el zumbador comienzan a encenderse y apagarse cada 500ms.
  5. Verificación de Webhook/Red
    • Observación: Inmediatamente después del tiempo de espera, observa la salida HTTP en el monitor serie.
    • Condición de éxito: La consola imprime «Sending HTTP GET…» seguido de «HTTP Response code: 200». Esto confirma que el ESP32 alcanzó con éxito el servidor externo.

Solución de problemas

Síntoma Causa probable Solución
Falla la conexión WiFi Credenciales incorrectas o red de 5GHz. Verifica el SSID/Contraseña. Asegúrate de que tu enrutador esté emitiendo en una banda de 2.4GHz (ESP32 no soporta 5GHz).
El sensor siempre se activa (Sin tiempo de espera) La sensibilidad del SW-420 está ajustada demasiado alta. Gira el potenciómetro azul del módulo SW-420 en sentido antihorario para reducir la sensibilidad hasta que el LED verde integrado se apague cuando esté quieto.
El sensor nunca se activa La sensibilidad del SW-420 está ajustada demasiado baja. Gira el potenciómetro en sentido horario hasta que se active fácilmente al golpear la mesa.
El zumbador no suena Polaridad invertida o pin incorrecto. Asegúrate de que la pata más larga (o la marca +) del zumbador vaya al GPIO 14, y la otra a GND.
El código de respuesta HTTP es -1 Fallo de DNS o falta de internet. Verifica que el ESP32 esté conectado a una red con acceso activo a internet, no solo a un enrutador local sin WAN.

Mejoras

Una vez que el prototipo básico esté funcionando, considera estas mejoras para una implementación permanente:

Robustez del hardware:
* Carcasa y montaje: Coloca el ESP32 en una carcasa impresa en 3D. Fija imanes de neodimio en la parte posterior de la carcasa para que pueda adherirse de forma segura al chasis metálico de la lavadora, asegurando una excelente transferencia de vibración al SW-420.
* Zumbador pasivo / Audio: Cambia el zumbador activo por un zumbador pasivo. Luego puedes usar las capacidades PWM del ESP32 (ledcWrite) para reproducir una melodía agradable (como el clásico timbre de un electrodoméstico) en lugar de un pitido áspero.

Software y lógica:
* Tiempos de espera realistas: Aumenta CYCLE_TIMEOUT_MS a 180,000 (3 minutos) para tener en cuenta las fases de remojo y vaciado donde una lavadora se queda temporalmente completamente quieta.
* Integración MQTT: Reemplaza la solicitud HTTP GET básica con un cliente MQTT (usando la biblioteca PubSubClient) para integrarlo sin problemas con Home Assistant, Node-RED u OpenHAB.
* Modo de suspensión profunda (Deep Sleep) / Bajo consumo: Si funciona con una batería LiPo, modifica el código para entrar en el modo de suspensión profunda del ESP32. Puedes usar el pin del SW-420 como un activador externo (external wake)

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: ¿Cuál es el objetivo principal del proyecto descrito?




Pregunta 2: ¿Qué microcontrolador se utiliza en la construcción de este monitor?




Pregunta 3: ¿Qué tipo de sensor se emplea para monitorizar el estado de la lavadora?




Pregunta 4: ¿Por qué se considera que este método de monitorización es 'no invasivo'?




Pregunta 5: ¿Qué tipo de lógica se utiliza para manejar las pausas normales de los electrodomésticos?




Pregunta 6: Según el texto, ¿cuál es la duración típica de las ventanas de inactividad (pausas) entre ciclos de lavado, enjuague y centrifugado?




Pregunta 7: ¿Qué componentes se utilizan para generar la alerta audiovisual local?




Pregunta 8: ¿Cómo se envía la alerta remota cuando la máquina completa oficialmente su ciclo?




Pregunta 9: ¿Cuál es la latencia de entrega esperada para la notificación HTTP?




Pregunta 10: ¿Qué problema evita la notificación inmediata al terminar el ciclo de lavado?




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:


Caso práctico: Registrador de contactos con ESP32

Caso práctico: Registrador de contactos con ESP32 — hero

case-device-block-diagram,
.prometeo-educational-note,
.prometeo-device-postcode-section,
.prometeo-device-section-card {
margin: 2.4rem 0;
padding: 1.45rem 1.55rem;
border: 1px solid rgba(148, 163, 184, 0.30);
border-radius: 14px;
background:
linear-gradient(135deg, rgba(30, 41, 59, 0.50), rgba(15, 23, 42, 0.18)),
rgba(17, 24, 39, 0.48);
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.20);
}
.case-objective > h2:first-of-type,
.case-device-block-diagram > h2:first-of-type,
.prometeo-device-postcode-section > h2:first-of-type,
.prometeo-device-section-card > h2:first-of-type {
margin-top: 0;
}
.prometeo-device-section-card > :last-child,
.case-objective > :last-child,
.case-device-block-diagram > :last-child,
.prometeo-device-postcode-section > :last-child {
margin-bottom: 0;
}
.prometeo-device-section-card.prometeo-device-section-card-code {
border-left: 4px solid rgba(56, 189, 248, 0.86);
background:
linear-gradient(135deg, rgba(8, 47, 73, 0.42), rgba(15, 23, 42, 0.18)),
rgba(15, 23, 42, 0.58);
}
.prometeo-device-section-card.prometeo-device-section-card-compact {
padding: 1.2rem 1.35rem;
}
.prometeo-device-section-card pre,
.case-objective pre,
.case-device-block-diagram pre,
.prometeo-device-postcode-section pre {
max-width: 100%;
}
.prometeo-device-postcode-section .prometeo-device-flow-item {
background:
linear-gradient(135deg, rgba(15, 23, 42, 0.50), rgba(30, 41, 59, 0.28)),
rgba(15, 23, 42, 0.28);
}
@media print {
.case-objective,
.case-device-block-diagram,
.prometeo-educational-note,
.prometeo-device-postcode-section,
.prometeo-device-section-card {
background: #fff;
color: #111827;
box-shadow: none;
break-inside: avoid;
page-break-inside: avoid;
}
}

Caso práctico: Registrador de contactos industrial con ESP32

Objetivo y caso de uso

Qué construirás: Un registrador de eventos ESP32 autónomo y conectado a la red que registra los cierres de contactos secos físicos en una tarjeta microSD y aloja un servidor web integrado para la recuperación remota de registros CSV a través de una red WiFi local.

Por qué es importante / Casos de uso

  • Auditoría de acceso a instalaciones: Rastrea cuándo se abre la puerta de un rack de servidores o un armario de almacenamiento seguro cableando un interruptor de lengüeta (reed switch) magnético a la entrada, capturando eventos con una latencia inferior a 50ms.
  • Registro de ciclos de máquinas: Registra ciclos mecánicos o intervenciones manuales en la planta de una fábrica sin requerir conectividad continua a la nube ni costosos PLC industriales.
  • Persistencia de datos sin conexión: Garantiza la captura del 100% de los eventos almacenados localmente de forma segura en medios físicos (microSD) para evitar la pérdida de datos durante cortes de red, con cero dependencia del tiempo de actividad de nubes externas.

Resultado esperado

  • El ESP32 se conecta a una red WiFi local designada y sincroniza su reloj interno a través de un servidor NTP para un control del tiempo de alta precisión.
  • Al cerrar el contacto físico se escribe una entrada con marca de tiempo (por ejemplo, 2023-10-27 14:32:10,Contact Closed) en un archivo log.csv en la tarjeta microSD en menos de 100ms.
  • Un servidor web asíncrono integrado permite a los usuarios descargar el historial de registro completo de forma fluida a través de un navegador web local.

Audiencia: Desarrolladores de IoT, Administradores de instalaciones e Integradores de sistemas; Nivel: Intermedio

Arquitectura/flujo: Contacto seco -> Interrupción GPIO del ESP32 -> MicroSD local (Escritura CSV) -> Servidor web asíncrono del ESP32 -> Solicitud de cliente WiFi local

Diagrama de bloques conceptual

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

Arquitectura funcional

Botón local

ESP32 BLE

Paquete advertising

LED de estado

Móvil escáner

Flujo conceptual: configuración local, emisión BLE y lectura desde el móvil.

Ruta de validación

Código fuente

PlatformIO build

Flash

Monitor serie

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

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, 3 tablas y 2 bloques de código detectados antes de publicar.
  • Código comprobado: 1 PlatformIO config + 1 ESP32 source/pio run.
  • 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 registrador de contactos es un prototipo educativo para señales de contacto seco o bajo voltaje, no un registrador certificado de seguridad. No conectes GPIO del ESP32 directamente a timbres, paneles de alarma, líneas telefónicas, red eléctrica o señales industriales; usa aislamiento, divisores o interfaces adecuadas cuando la señal no sea claramente compatible con 3,3 V. Desconecta la alimentación antes de cambiar el cableado.

Requisitos previos

Para completar con éxito esta implementación, necesitas una comprensión fundamental de C++ y conceptos de sistemas embebidos, específicamente Entrada/Salida de Propósito General (GPIO), comunicación de Interfaz Periférica Serial (SPI) y redes de área local.

Debes tener instalado Visual Studio Code con la extensión PlatformIO IDE. También requieres los controladores USB a UART adecuados (típicamente CP210x o CH34x) instalados para tu ESP32 DevKitC para garantizar la comunicación en serie.

Materiales

  • Microcontrolador: ESP32 DevKitC (versión de 38 pines o 30 pines).
  • Módulo de almacenamiento: Módulo microSD SPI (interfaz estándar de 6 pines: CS, SCK, MOSI, MISO, VCC, GND).
  • Medio de almacenamiento: Tarjeta microSD (Formateada en FAT32, capacidad máxima de 32GB).
  • Dispositivo de entrada: Interruptor de lengüeta (reed switch) magnético o bloque de contacto seco industrial.
  • Prototipado: Protoboard (placa de pruebas) y cables puente macho a macho variados.
  • Energía/Datos: Cable Micro-USB o USB-C con capacidad de transferencia de datos.

Configuración/Conexión

El ESP32 se comunica con el lector de tarjetas microSD utilizando el bus de hardware VSPI. El interruptor de contacto se conecta a un pin de entrada digital utilizando la resistencia pull-up interna del ESP32. El pin lee HIGH normalmente y LOW cuando el contacto se cierra (conectándolo a Tierra).

Tabla de cableado

Componente Pin del componente Pin del ESP32 DevKitC Notas
Módulo microSD VCC 5V (o VIN) La mayoría de los módulos estándar requieren 5V y lo reducen internamente a 3.3V.
Módulo microSD GND GND Tierra común.
Módulo microSD CS GPIO 5 Chip Select (CS) de VSPI.
Módulo microSD MOSI GPIO 23 MOSI de VSPI.
Módulo microSD MISO GPIO 19 MISO de VSPI.
Módulo microSD SCK GPIO 18 SCK de VSPI.
Interruptor de lengüeta Terminal 1 GPIO 4 Configurado con INPUT_PULLUP.
Interruptor de lengüeta Terminal 2 GND Cierra el circuito a tierra cuando se acciona.

Nota: Asegúrate de que tu tarjeta microSD esté formateada en FAT32. Los formatos exFAT o NTFS no son compatibles con la biblioteca SD estándar de Arduino.

Código de la aplicación

El proyecto utiliza PlatformIO. Crea un nuevo proyecto para el ESP32 DevKitC y completa los archivos de configuración y fuente de la siguiente manera.

Configuración de PlatformIO: platformio.ini

Este archivo configura el entorno de compilación, especifica la placa de destino y establece la velocidad de baudios del monitor serie.

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

Aplicación principal: src/main.cpp

Este archivo fuente maneja la conectividad de red, la sincronización de tiempo NTP, la eliminación de rebotes por hardware (debouncing), las operaciones de archivos en la tarjeta SD y el servidor HTTP. Actualiza WIFI_SSID y WIFI_PASS para que coincidan con tu red local antes de compilar.

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>
#include <SPI.h>
#include <SD.h>
#include <time.h>

// ==========================================
// Configuration Parameters
// ==========================================
#define WIFI_SSID "Facility_IoT_Network"
#define WIFI_PASS "SecureWLAN2024"

#define PIN_SD_CS 5
#define PIN_CONTACT 4

const char* ntpServer = "pool.ntp.org";
const long  gmtOffset_sec = 0;        
const int   daylightOffset_sec = 3600; 

const char* logFilePath = "/log.csv";

// ==========================================
// Global Variables
// ==========================================
WebServer server(80);

int contactState = HIGH;
int lastContactState = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50; // 50ms debounce claim

// ==========================================
// Helper Functions
// ==========================================

String getTimeString() {
    struct tm timeinfo;
    if (!getLocalTime(&timeinfo)) {
        return "1970-01-01 00:00:00";
    }
    char timeStringBuff[50];
    strftime(timeStringBuff, sizeof(timeStringBuff), "%Y-%m-%d %H:%M:%S", &timeinfo);
    return String(timeStringBuff);
}

void logEventToSD(const String& message) {
    File file = SD.open(logFilePath, FILE_APPEND);
    if (!file) {
        Serial.println("Error: Failed to open log file for appending.");
        return;
    }
    if (file.println(message)) {
        Serial.println("Event logged successfully: " + message);
    } else {
        Serial.println("Error: File append operation failed.");
    }
    file.close();
}

// ==========================================
// Web Server Handlers
// ==========================================

void handleRoot() {
    String html = "<!DOCTYPE html><html><head><title>ESP32 Logger Dashboard</title>";
    html += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">";
    html += "<style>body{font-family:Arial,sans-serif; margin:40px; background-color:#f4f4f9;}";
    html += "h1{color:#333;} .btn{display:inline-block; padding:10px 20px; ";
    html += "background-color:#0056b3; color:white; text-decoration:none; border-radius:5px;}</style></head><body>";
    html += "<h1>ESP32 Industrial Contact Logger</h1>";
    html += "<p>System Status: ONLINE</p>";
    html += "<p>Storage Status: MOUNTED</p>";
    html += "<a href=\"/log.csv\" class=\"btn\">Download CSV Log</a>";
    html += "</body></html>";

    server.send(200, "text/html", html);
}

void handleLogDownload() {
    if (!SD.exists(logFilePath)) {
        server.send(404, "text/plain", "Error 404: Log file not found. No events recorded yet.");
        return;
    }
// ...

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <SPI.h>
#include <SD.h>
#include <time.h>

// ==========================================
// Configuration Parameters
// ==========================================
#define WIFI_SSID "Facility_IoT_Network"
#define WIFI_PASS "SecureWLAN2024"

#define PIN_SD_CS 5
#define PIN_CONTACT 4

const char* ntpServer = "pool.ntp.org";
const long  gmtOffset_sec = 0;        
const int   daylightOffset_sec = 3600; 

const char* logFilePath = "/log.csv";

// ==========================================
// Global Variables
// ==========================================
WebServer server(80);

int contactState = HIGH;
int lastContactState = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50; // 50ms debounce claim

// ==========================================
// Helper Functions
// ==========================================

String getTimeString() {
    struct tm timeinfo;
    if (!getLocalTime(&timeinfo)) {
        return "1970-01-01 00:00:00";
    }
    char timeStringBuff[50];
    strftime(timeStringBuff, sizeof(timeStringBuff), "%Y-%m-%d %H:%M:%S", &timeinfo);
    return String(timeStringBuff);
}

void logEventToSD(const String& message) {
    File file = SD.open(logFilePath, FILE_APPEND);
    if (!file) {
        Serial.println("Error: Failed to open log file for appending.");
        return;
    }
    if (file.println(message)) {
        Serial.println("Event logged successfully: " + message);
    } else {
        Serial.println("Error: File append operation failed.");
    }
    file.close();
}

// ==========================================
// Web Server Handlers
// ==========================================

void handleRoot() {
    String html = "<!DOCTYPE html><html><head><title>ESP32 Logger Dashboard</title>";
    html += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">";
    html += "<style>body{font-family:Arial,sans-serif; margin:40px; background-color:#f4f4f9;}";
    html += "h1{color:#333;} .btn{display:inline-block; padding:10px 20px; ";
    html += "background-color:#0056b3; color:white; text-decoration:none; border-radius:5px;}</style></head><body>";
    html += "<h1>ESP32 Industrial Contact Logger</h1>";
    html += "<p>System Status: ONLINE</p>";
    html += "<p>Storage Status: MOUNTED</p>";
    html += "<a href=\"/log.csv\" class=\"btn\">Download CSV Log</a>";
    html += "</body></html>";

    server.send(200, "text/html", html);
}

void handleLogDownload() {
    if (!SD.exists(logFilePath)) {
        server.send(404, "text/plain", "Error 404: Log file not found. No events recorded yet.");
        return;
    }

    File file = SD.open(logFilePath, FILE_READ);
    if (!file) {
        server.send(500, "text/plain", "Error 500: Internal Server Error. Failed to open log file.");
        return;
    }

    server.streamFile(file, "text/csv");
    file.close();
}

// ==========================================
// Setup and Loop
// ==========================================

void setup() {
    Serial.begin(115200);
    while (!Serial) { delay(10); } 

    pinMode(PIN_CONTACT, INPUT_PULLUP);

    Serial.println("\n--- ESP32 Contact Logger Initializing ---");

    if (!SD.begin(PIN_SD_CS)) {
        Serial.println("CRITICAL ERROR: SD Card Mount Failed. System halted.");
        while (true) { delay(1000); }
    }
    Serial.println("SD Card mounted successfully.");

    if (!SD.exists(logFilePath)) {
        File file = SD.open(logFilePath, FILE_WRITE);
        if (file) {
            file.println("Timestamp,Event");
            file.close();
            Serial.println("Created new log.csv with headers.");
        }
    }

    Serial.print("Connecting to WiFi network: ");
    Serial.println(WIFI_SSID);
    WiFi.begin(WIFI_SSID, WIFI_PASS);

    int wifiAttempts = 0;
    while (WiFi.status() != WL_CONNECTED && wifiAttempts < 20) {
        delay(500);
        Serial.print(".");
        wifiAttempts++;
    }

    if (WiFi.status() == WL_CONNECTED) {
        Serial.println("\nWiFi connected.");
        Serial.print("Assigned IP Address: ");
        Serial.println(WiFi.localIP());

        configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
        Serial.println("Awaiting NTP time synchronization...");
        delay(2000); 
    } else {
        Serial.println("\nWARNING: WiFi connection failed. Timestamps will default to epoch.");
    }

    server.on("/", handleRoot);
    server.on("/log.csv", HTTP_GET, handleLogDownload);
    server.begin();
    Serial.println("HTTP server started and listening on port 80.");
}

void loop() {
    server.handleClient();

    int reading = digitalRead(PIN_CONTACT);

    if (reading != lastContactState) {
        lastDebounceTime = millis();
    }

    if ((millis() - lastDebounceTime) > debounceDelay) {
        if (reading != contactState) {
            contactState = reading;

            if (contactState == LOW) {
                String timeStr = getTimeString();
                String logEntry = timeStr + ",Contact Closed";
                logEventToSD(logEntry);
            }
        }
    }

    lastContactState = reading;
}

Comandos de Compilación/Flasheo/Ejecución

Para compilar y cargar el firmware, abre la terminal PlatformIO Core CLI en Visual Studio Code y ejecuta los siguientes comandos.

Comando Descripción
pio run Compila el proyecto localmente para verificar la sintaxis y la resolución de bibliotecas.
pio run --target upload Compila el firmware y lo flashea en el ESP32 conectado a través de USB.
pio device monitor Abre el monitor serie para ver los registros de arranque, el estado de la SD y la dirección IP asignada.

Validación paso a paso

Para garantizar las afirmaciones estrictas de rendimiento y precisión (específicamente la eliminación de rebotes por software de 50ms y el registro preciso de marcas de tiempo), realiza los siguientes pasos de validación.

  1. Inicialización de la tarjeta SD y la red
    • Método: Monitorea la salida en serie a través de pio device monitor inmediatamente después del encendido.
    • Evidencia esperada: La consola debe mostrar explícitamente SD Card mounted successfully. seguido de Assigned IP Address: [IP].
  2. Validación de precisión de eliminación de rebotes (Debounce)
    • Método: Conecta un canal del osciloscopio al GPIO 4 y tierra. Configura el disparador (trigger) para un flanco de bajada. Acciona el interruptor de lengüeta mecánico. Compara la duración física del rebote de voltaje en el osciloscopio con la salida del monitor serie.
    • Evidencia esperada: El osciloscopio mostrará picos de voltaje transitorios (rebote) que duran entre 5 y 20ms. El monitor serie debe registrar exactamente un evento por accionamiento físico, lo que demuestra que el debounceDelay de 50ms ocultó con éxito los transitorios mecánicos.
  3. Validación de sincronización de tiempo
    • Método: Desencadena un evento de cierre de contacto.
    • Evidencia esperada: El monitor serie debe emitir una marca de tiempo que coincida con la hora actual del mundo real (por ejemplo, 2023-10-27 14:32:10), lo que demuestra una resolución NTP exitosa en lugar del retroceso (fallback) a la época (epoch) de 1970-01-01.
  4. Comprobación de recuperación de datos
    • Método: Abre un navegador web en una máquina dentro de la misma subred. Ingresa la dirección IP del ESP32. Haz clic en «Download CSV Log».
    • Evidencia esperada: El navegador descarga con éxito log.csv. Al inspeccionar el archivo en una aplicación de hoja de cálculo se revela un encabezado correcto Timestamp,Event y filas precisas que corresponden a los accionamientos manuales.

Solución de problemas

Síntoma Causa probable Solución
ERROR CRÍTICO: Falló el montaje de la tarjeta SD Fallo en el cableado SPI o sistema de archivos incompatible. Verifica el cableado VSPI (CS al GPIO 5, MOSI al 23). Asegúrate de que la tarjeta SD esté formateada estrictamente en FAT32, no en exFAT.
Las marcas de tiempo vuelven por defecto a 1970-01-01 Servidor NTP inalcanzable. Verifica que la red local proporcione acceso saliente a Internet en el puerto UDP 123. Asegúrate de que las credenciales de WiFi sean correctas.
Múltiples entradas de registro por cada cierre individual Rebote severo del interruptor mecánico. Si usas un interruptor mecánico muy degradado, es posible que se superen los 50ms de rebote. Aumenta debounceDelay a 100 en main.cpp.
Interfaz web inalcanzable Aislamiento de subred o VPN del cliente activa. Asegúrate de que la PC cliente esté exactamente en la misma subred local (por ejemplo, 192.168.1.X). Desactiva las VPN del lado del cliente que secuestran el enrutamiento local.

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: ¿Cuál es el objetivo principal del proyecto descrito en el artículo?




Pregunta 2: ¿Qué tipo de servidor aloja el ESP32 para la recuperación remota de registros?




Pregunta 3: ¿Qué componente se sugiere usar para rastrear la apertura de la puerta de un rack de servidores?




Pregunta 4: ¿Cuál es la latencia de captura de eventos mencionada para la auditoría de acceso?




Pregunta 5: ¿Qué ventaja ofrece el ESP32 frente a los PLC industriales para el registro de ciclos de máquinas?




Pregunta 6: ¿Dónde se almacenan localmente los datos para garantizar su persistencia sin conexión?




Pregunta 7: ¿A través de qué tipo de red se recuperan los registros CSV remotamente?




Pregunta 8: ¿En qué formato se recuperan los registros desde el servidor web del ESP32?




Pregunta 9: ¿Qué tipo de cierres físicos registra el ESP32 según el texto?




Pregunta 10: ¿Cuál es el nivel de dependencia de nubes externas para evitar la pérdida de datos en este proyecto?




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: