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

ComponentePin del componentePin del ESP32 DevKitCNotas
Módulo microSDVCC5V (o VIN)La mayoría de los módulos estándar requieren 5V y lo reducen internamente a 3.3V.
Módulo microSDGNDGNDTierra común.
Módulo microSDCSGPIO 5Chip Select (CS) de VSPI.
Módulo microSDMOSIGPIO 23MOSI de VSPI.
Módulo microSDMISOGPIO 19MISO de VSPI.
Módulo microSDSCKGPIO 18SCK de VSPI.
Interruptor de lengüetaTerminal 1GPIO 4Configurado con INPUT_PULLUP.
Interruptor de lengüetaTerminal 2GNDCierra 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;
    }
// ...

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

ComandoDescripción
pio runCompila el proyecto localmente para verificar la sintaxis y la resolución de bibliotecas.
pio run --target uploadCompila el firmware y lo flashea en el ESP32 conectado a través de USB.
pio device monitorAbre 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íntomaCausa probableSolución
ERROR CRÍTICO: Falló el montaje de la tarjeta SDFallo 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-01Servidor 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 individualRebote 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 inalcanzableAislamiento 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:
Scroll al inicio