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 archivolog.csven 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
Flujo conceptual: configuración local, emisión BLE y lectura desde el móvil.
Ruta de validación
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.
- Inicialización de la tarjeta SD y la red
- Método: Monitorea la salida en serie a través de
pio device monitorinmediatamente después del encendido. - Evidencia esperada: La consola debe mostrar explícitamente
SD Card mounted successfully.seguido deAssigned IP Address: [IP].
- Método: Monitorea la salida en serie a través de
- 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
debounceDelayde 50ms ocultó con éxito los transitorios mecánicos.
- 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) de1970-01-01.
- 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 correctoTimestamp,Eventy 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
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.




