From e653b74e7c2b36a757aa76bbcebcd0e2ed003a27 Mon Sep 17 00:00:00 2001 From: benchav Date: Fri, 22 May 2026 21:34:00 -0600 Subject: [PATCH 1/7] feat: implement IoT dashboard with real-time sensor simulation and Arduino management page --- src/pages/IotPage.tsx | 913 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 838 insertions(+), 75 deletions(-) diff --git a/src/pages/IotPage.tsx b/src/pages/IotPage.tsx index 6cfa47c..0b69e56 100644 --- a/src/pages/IotPage.tsx +++ b/src/pages/IotPage.tsx @@ -1,91 +1,854 @@ +import { useState, useEffect } from 'react'; import { PageSection } from '../components/layout/PageSection'; +interface Arduino { + id: string; + name: string; + location: string; + status: 'active' | 'inactive'; + baudRate: number; + frequency: number; + description: string; +} + +interface Sensor { + id: string; + name: string; + type: string; + value: string; + numericValue: number; + unit: string; + location: string; + status: 'OK' | 'Atención' | 'Crítico'; + tone: 'emerald' | 'amber' | 'red' | 'cyan'; + arduinoId: string; +} + +interface Alert { + id: string; + emoji: string; + title: string; + description: string; + time: string; + severity: 'red' | 'amber'; + resolved: boolean; +} + +// Semillas de datos iniciales +const initialArduinos: Arduino[] = [ + { + id: 'ARD-MEGA-01', + name: 'Arduino Mega - Principal', + location: 'Parcela Norte', + status: 'active', + baudRate: 115200, + frequency: 2, + description: 'Controlador de sensores de suelo de alta precisión en parcela norte.', + }, + { + id: 'ARD-UNO-02', + name: 'Arduino Uno - Invernadero', + location: 'Sector 2A', + status: 'active', + baudRate: 9600, + frequency: 5, + description: 'Monitoreo de temperatura y humedad interna en domo invernadero.', + }, + { + id: 'ARD-NANO-03', + name: 'Arduino Nano - Riego', + location: 'Zona Crítica', + status: 'inactive', + baudRate: 9600, + frequency: 10, + description: 'Control de electroválvulas y sensor de flujo de agua en sector sur.', + }, +]; + +const initialSensors: Sensor[] = [ + { + id: 'S01', + name: 'Humedad del Suelo', + type: 'Humedad', + value: '68%', + numericValue: 68, + unit: '%', + location: 'Parcela Norte', + status: 'OK', + tone: 'emerald', + arduinoId: 'ARD-MEGA-01', + }, + { + id: 'S05', + name: 'Temperatura Suelo', + type: 'Temperatura', + value: '28°C', + numericValue: 28, + unit: '°C', + location: 'Sector 2A', + status: 'Atención', + tone: 'amber', + arduinoId: 'ARD-UNO-02', + }, + { + id: 'S09', + name: 'Humedad Zona Crítica', + type: 'Humedad', + value: '34%', + numericValue: 34, + unit: '%', + location: 'Zona Crítica', + status: 'Crítico', + tone: 'red', + arduinoId: 'ARD-NANO-03', + }, + { + id: 'S12', + name: 'pH del Suelo', + type: 'pH', + value: '7.2 pH', + numericValue: 7.2, + unit: ' pH', + location: 'Parcela Sur', + status: 'OK', + tone: 'cyan', + arduinoId: 'ARD-MEGA-01', + }, + { + id: 'S14', + name: 'Nitrógeno (N)', + type: 'Nutrientes', + value: '185 mg/kg', + numericValue: 185, + unit: ' mg/kg', + location: 'Parcela Norte', + status: 'OK', + tone: 'emerald', + arduinoId: 'ARD-MEGA-01', + }, + { + id: 'S18', + name: 'CO₂ del Suelo', + type: 'Gas', + value: '820 ppm', + numericValue: 820, + unit: ' ppm', + location: 'Sector 2A', + status: 'Atención', + tone: 'amber', + arduinoId: 'ARD-UNO-02', + }, +]; + +const initialAlerts: Alert[] = [ + { + id: 'A01', + emoji: '🐛', + title: 'Anomalía de presión — Posible plaga subterránea', + description: 'Parcela Norte: Variación anormal en sensores S01, S03. Patrón consistente con actividad de roedores.', + time: 'hace 12m', + severity: 'red', + resolved: false, + }, + { + id: 'A02', + emoji: '🦗', + title: 'Posible actividad de insectos — Sector 2A', + description: 'Temperatura de suelo elevada con patrón de vibración en sensor S05. Monitoreo continuo activo.', + time: 'hace 58m', + severity: 'amber', + resolved: false, + }, +]; + export function IotPage() { + // Pestaña activa + const [activeTab, setActiveTab] = useState<'monitor' | 'arduino' | 'tutorials'>('monitor'); + + // Estados cargados desde LocalStorage + const [arduinos, setArduinos] = useState(() => { + const saved = localStorage.getItem('ac_arduinos'); + return saved ? JSON.parse(saved) : initialArduinos; + }); + + const [sensors, setSensors] = useState(() => { + const saved = localStorage.getItem('ac_sensors'); + return saved ? JSON.parse(saved) : initialSensors; + }); + + const [alerts, setAlerts] = useState(() => { + const saved = localStorage.getItem('ac_alerts'); + return saved ? JSON.parse(saved) : initialAlerts; + }); + + // Estado de simulación + const [isSimulating, setIsSimulating] = useState(true); + + // Estado del formulario de nuevo Arduino + const [newArdId, setNewArdId] = useState(''); + const [newArdName, setNewArdName] = useState(''); + const [newArdLoc, setNewArdLoc] = useState('Parcela Norte'); + const [newArdStatus, setNewArdStatus] = useState<'active' | 'inactive'>('active'); + const [newArdBaud, setNewArdBaud] = useState(9600); + const [newArdFreq, setNewArdFreq] = useState(5); + const [newArdDesc, setNewArdDesc] = useState(''); + const [formError, setFormError] = useState(''); + + // Persistir en LocalStorage al cambiar estados + useEffect(() => { + localStorage.setItem('ac_arduinos', JSON.stringify(arduinos)); + }, [arduinos]); + + useEffect(() => { + localStorage.setItem('ac_sensors', JSON.stringify(sensors)); + }, [sensors]); + + useEffect(() => { + localStorage.setItem('ac_alerts', JSON.stringify(alerts)); + }, [alerts]); + + // Simulación en tiempo real (fluctuación sutil de valores) + useEffect(() => { + if (!isSimulating) return; + + const interval = setInterval(() => { + setSensors((prevSensors) => + prevSensors.map((sensor) => { + // Solo actualizamos sensores asociados a Arduinos activos + const parentArd = arduinos.find((a) => a.id === sensor.arduinoId); + if (parentArd && parentArd.status === 'inactive') { + return { + ...sensor, + value: '---', + status: 'Crítico', + tone: 'red', + }; + } + + let delta = 0; + let newValue = sensor.numericValue; + + if (sensor.type === 'Humedad') { + delta = (Math.random() - 0.5) * 4; // Fluctúa +/- 2% + newValue = Math.max(0, Math.min(100, sensor.numericValue + delta)); + } else if (sensor.type === 'Temperatura') { + delta = (Math.random() - 0.5) * 0.8; // Fluctúa +/- 0.4°C + newValue = Math.max(10, Math.min(50, sensor.numericValue + delta)); + } else if (sensor.type === 'pH') { + delta = (Math.random() - 0.5) * 0.1; // Fluctúa +/- 0.05 pH + newValue = Math.max(4, Math.min(10, sensor.numericValue + delta)); + } else if (sensor.type === 'Nutrientes') { + delta = (Math.random() - 0.5) * 6; // Fluctúa +/- 3 mg/kg + newValue = Math.max(50, Math.min(300, sensor.numericValue + delta)); + } else if (sensor.type === 'Gas') { + delta = (Math.random() - 0.5) * 15; // Fluctúa +/- 7.5 ppm + newValue = Math.max(300, Math.min(1500, sensor.numericValue + delta)); + } + + // Formatear valor visual + let formattedVal = `${newValue.toFixed(sensor.type === 'pH' ? 1 : 0)}${sensor.unit}`; + + // Calcular tono y estado según rangos realistas + let status: 'OK' | 'Atención' | 'Crítico' = 'OK'; + let tone: 'emerald' | 'amber' | 'red' | 'cyan' = 'emerald'; + + if (sensor.type === 'Humedad') { + if (newValue < 40) { + status = 'Crítico'; + tone = 'red'; + } else if (newValue < 60) { + status = 'Atención'; + tone = 'amber'; + } else { + status = 'OK'; + tone = 'emerald'; + } + } else if (sensor.type === 'Temperatura') { + if (newValue > 32) { + status = 'Crítico'; + tone = 'red'; + } else if (newValue > 27) { + status = 'Atención'; + tone = 'amber'; + } else { + status = 'OK'; + tone = 'emerald'; + } + } else if (sensor.type === 'pH') { + tone = 'cyan'; + if (newValue < 6.0 || newValue > 8.0) { + status = 'Atención'; + } else { + status = 'OK'; + } + } + + return { + ...sensor, + numericValue: newValue, + value: formattedVal, + status, + tone, + }; + }) + ); + }, 3000); + + return () => clearInterval(interval); + }, [isSimulating, arduinos]); + + // Cálculos estadísticos dinámicos + const activeArduinosCount = arduinos.filter((a) => a.status === 'active').length; + const totalArduinosCount = arduinos.length; + + const humiditySensors = sensors.filter((s) => s.type === 'Humedad' && s.value !== '---'); + const avgHumidity = + humiditySensors.length > 0 + ? Math.round(humiditySensors.reduce((acc, curr) => acc + curr.numericValue, 0) / humiditySensors.length) + : 0; + + const activeAlertsCount = alerts.filter((a) => !a.resolved).length; + const onlineSensorsCount = sensors.filter((s) => s.value !== '---').length; + + // Manejo de agregación de Arduino + const handleAddArduino = (e: React.FormEvent) => { + e.preventDefault(); + if (!newArdId || !newArdName) { + setFormError('El ID de placa y el Nombre son requeridos.'); + return; + } + + if (arduinos.some((a) => a.id.toUpperCase() === newArdId.toUpperCase())) { + setFormError('Ya existe una placa Arduino con este ID.'); + return; + } + + const newArduino: Arduino = { + id: newArdId.toUpperCase(), + name: newArdName, + location: newArdLoc, + status: newArdStatus, + baudRate: newArdBaud, + frequency: newArdFreq, + description: newArdDesc || `Dispositivo de control en ${newArdLoc}.`, + }; + + // Agregar sensores ficticios automáticamente a este nuevo Arduino para darle vida + const newSensor: Sensor = { + id: `S${Math.floor(Math.random() * 90) + 20}`, + name: `Humedad de Suelo (${newArduino.id})`, + type: 'Humedad', + value: '70%', + numericValue: 70, + unit: '%', + location: newArduino.location, + status: 'OK', + tone: 'emerald', + arduinoId: newArduino.id, + }; + + setArduinos((prev) => [...prev, newArduino]); + setSensors((prev) => [...prev, newSensor]); + + // Resetear formulario + setNewArdId(''); + setNewArdName(''); + setNewArdDesc(''); + setFormError(''); + }; + + // Alternar estado de un Arduino (Activo/Inactivo) + const toggleArduinoStatus = (id: string) => { + setArduinos((prev) => + prev.map((a) => (a.id === id ? { ...a, status: a.status === 'active' ? 'inactive' : 'active' } : a)) + ); + }; + + // Eliminar placa Arduino + const handleDeleteArduino = (id: string) => { + setArduinos((prev) => prev.filter((a) => a.id !== id)); + // Los sensores asociados se quedan apagados (---) + setSensors((prev) => prev.filter((s) => s.arduinoId !== id)); + }; + + // Resolver alerta + const handleResolveAlert = (id: string) => { + setAlerts((prev) => prev.map((a) => (a.id === id ? { ...a, resolved: true } : a))); + }; + return ( -
-

Integración Arduino · 24 sensores · Datos en tiempo real

- -
- {[ - ['Humedad del Suelo', '68%', 'Parcela Norte · S01', '✔ Nivel óptimo', 'emerald'], - ['Temperatura Suelo', '28°C', 'Sector 2A · S05', '⚠ Ligeramente alta', 'amber'], - ['Humedad Zona Crítica', '34%', 'Zona Crítica · S09', '🔴 Riego urgente', 'red'], - ['pH del Suelo', '7.2 pH', 'Parcela Sur · S12', '✔ Óptimo', 'cyan'], - ['Nitrógeno (N)', '185 mg/kg', 'Parcela Norte · S14', '✔ Buena disponibilidad', 'emerald'], - ['CO₂ del Suelo', '820 ppm', 'Sector 3B · S18', '⚠ Actividad microbiana alta', 'amber'], - ].map(([title, value, meta, state, tone]) => ( -
-
{title}
-
{value}
-
{meta}
-
- {state} -
+
+ {/* Encabezado */} +
+
+

Panel Administrativo de Control y Telemetría Industrial

+

Consola Central IoT

+
+ + {/* Toggle de Simulación en tiempo real */} + +
+ + {/* Tarjetas Estadísticas Dinámicas */} +
+ {/* Tarjeta 1: Estado de Arduinos */} +
+
Placas Arduino
+
+ {activeArduinosCount} + / {totalArduinosCount} Activas +
+
+ + + {activeArduinosCount === totalArduinosCount ? 'Todos los nodos OK' : 'Hay placas inactivas'} +
- ))} +
+ + {/* Tarjeta 2: Humedad Promedio */} +
+
Humedad de Suelo Promedio
+
{avgHumidity}%
+
Calculado sobre {humiditySensors.length} sensores activos
+
+ + {/* Tarjeta 3: Sensores En Línea */} +
+
Sensores en Línea
+
{onlineSensorsCount}
+
De un total de {sensors.length} instalados
+
+ + {/* Tarjeta 4: Alertas Activas */} +
+
Alertas Activas
+
+ 0 ? 'text-red-400' : 'text-emerald-400'}`}> + {activeAlertsCount} + + requieren atención +
+
Monitoreo automático 24/7
+
- -
-
- 🐛 -
-
Anomalía de presión — Posible plaga subterránea
-
- Parcela Norte: Variación anormal en sensores S01, S03. Patrón consistente con actividad de roedores o insectos. + {/* Pestañas de Navegación del Panel Administrativo */} +
+ + + +
+ + {/* CONTENIDO DE PESTAÑA: MONITOREO */} + {activeTab === 'monitor' && ( +
+ {/* Tarjetas Principales (Grid rápido) */} +
+ {sensors.map((sensor) => ( +
+
+
{sensor.name}
+ + {sensor.arduinoId} + +
+
{sensor.value}
+
{sensor.location} · {sensor.id}
+
+
+ {sensor.value === '---' ? '📴 Placa Desconectada' : `✔ ${sensor.status}`} +
+
-
-
hace 12m
+ ))}
-
- 🦗 -
-
Posible actividad de insectos — Sector 2A
-
- Temperatura de suelo elevada con patrón de vibración en sensor S05. Monitoreo continuo activo. -
+ + {/* Detección de Plagas Terrestres / Alertas */} + +
+ {alerts.filter(a => !a.resolved).length === 0 ? ( +
+

🟢 No hay anomalías activas detectadas en el sistema.

+
+ ) : ( + alerts + .filter((a) => !a.resolved) + .map((alert) => ( +
+ {alert.emoji} +
+
{alert.title}
+
{alert.description}
+ +
+
+
{alert.time}
+ + {alert.severity === 'red' ? 'Crítico' : 'Advertencia'} + +
+
+ )) + )}
-
hace 58m
+
+ + {/* Tabla de Todos los Sensores */} + +
+ + + + + + + + + + + + + {sensors.map((sensor) => ( + + + + + + + + + ))} + +
SensorTipoUbicaciónPlaca ArduinoLecturaEstado
{sensor.id}{sensor.name}{sensor.location}{sensor.arduinoId}{sensor.value} + + {sensor.value === '---' ? 'Inactivo' : sensor.status} + +
+
+
+
+ )} + + {/* CONTENIDO DE PESTAÑA: ADMINISTRACIÓN ARDUINO */} + {activeTab === 'arduino' && ( +
+ {/* Listado de Arduinos */} +
+ +
+ {arduinos.map((arduino) => { + const isActive = arduino.status === 'active'; + return ( +
+
+
+ + {arduino.id} + + {isActive ? 'Activo' : 'Inactivo'} + +
+

{arduino.name}

+

{arduino.description}

+
+ 📍 {arduino.location} + ⚡ {arduino.baudRate} baudios + 🕒 {arduino.frequency}s de lectura +
+
+ +
+ {/* Botón Encender / Apagar */} + + {/* Botón Eliminar */} + +
+
+ ); + })} +
+
+
+ + {/* Formulario Agregar Arduino */} +
+ +
+ {formError && ( +
+ {formError} +
+ )} + +
+ + setNewArdId(e.target.value)} + className="w-full rounded-xl border border-white/10 bg-white/5 p-3 text-sm text-white placeholder-slate-500 focus:border-emerald-500/50 focus:outline-none" + /> +
+ +
+ + setNewArdName(e.target.value)} + className="w-full rounded-xl border border-white/10 bg-white/5 p-3 text-sm text-white placeholder-slate-500 focus:border-emerald-500/50 focus:outline-none" + /> +
+ +
+
+ + +
+ +
+ + +
+
+ +
+
+ + setNewArdFreq(Number(e.target.value))} + className="w-full rounded-xl border border-white/10 bg-white/5 p-3 text-sm text-white focus:border-emerald-500/50 focus:outline-none" + /> +
+ +
+ + +
+
+ +
+ +