07. Diseno de software - diegogopa/Challenge-IOT GitHub Wiki
El firmware AquaMonitor_v3_2.ino se organiza en módulos funcionales que separan adquisición, procesamiento, fusión, detección de fallas, notificación y comunicación.
AquaMonitor v3.2
│
├── Configuración
│ ├── Credenciales WiFi y web
│ ├── Pines y direcciones I2C
│ ├── Rango de nivel, filtro y parámetros UV
│ ├── Constantes de calibración (a·x + b)
│ ├── Umbrales de fusión y ventanas de tiempo
│ └── Parámetros de detección de fallas
│
├── Tipos compartidos
│ ├── enum Estado
│ ├── struct Datos (estado actual)
│ ├── struct Muestra (histórico, 60)
│ └── struct Evento (registro, 10)
│
├── Utilidades de medición y cálculo
│ ├── leerUV() validación de la señal UV
│ ├── medirDistanciaUnica() / medirDistanciaMediana()
│ ├── calcularIndiceEvaporacion()
│ ├── categoriaUV()
│ └── calcularEstado() lógica de fusión
│
├── Tarea de sensado (FreeRTOS)
│ ├── DHT22
│ ├── BMP180 (I2C, con verificación de presencia)
│ ├── GUVA-S12SD
│ ├── HC-SR04 (mediana + EMA)
│ ├── Tendencia, tasa, evaporación, ΔP
│ ├── Fusión y clasificación
│ └── Eventos e histórico
│
├── Gestión de fallas
│ ├── aplicarFalla() / reportarFalla()
│ └── Bits F_DHT, F_BMP, F_UV, F_ULTRA, F_OLED, F_WIFI
│
├── Sincronización
│ ├── mutexDatos (struct Datos, eventos, histórico)
│ └── mutexI2C (bus compartido OLED + BMP180)
│
├── Notificación local (loop)
│ ├── Buzzer (alarma / chirrido de falla)
│ └── OLED (pantalla de inicio, mensajes, pantalla principal)
│
├── Conectividad
│ ├── conectarWiFi() conexión inicial con timeout de 15 s
│ └── gestionarWiFi() reconexión cada 10 s
│
└── Servidor web
├── acceso() subred + autenticación
├── GET / · GET /data · GET /historico · POST /silenciar
└── Página HTML/CSS/JS embebida (PROGMEM)
#include "DHT.h" // DHT sensor library (Adafruit)
#include <Wire.h> // I2C
#include <Adafruit_GFX.h> // Gráficos
#include <Adafruit_SH110X.h> // Controlador SH1106
#include <Adafruit_BMP085.h> // BMP085/BMP180
#include <WiFi.h> // WiFi del ESP32
#include <ESPAsyncWebServer.h> // Servidor web asíncrono (requiere AsyncTCP)// Pines
#define DHTPIN 4
#define GUVA_PIN 34
#define TRIG_PIN 5
#define ECHO_PIN 18
#define BUZZER_PIN 33
#define SDA_PIN 21
#define SCL_PIN 23
#define OLED_ADDR 0x3C
#define BMP_ADDR 0x77
// UV
#define UV_V_POR_INDICE 0.1f // V por unidad de índice UV
#define UV_V_OFFSET 0.14f // voltaje de oscuridad
#define UV_RUIDO_MAX_MV 150 // dispersión máxima entre 16 muestras
#define UV_V_SATURADO 1.20f // por encima: falla
// Nivel
#define DIST_TANQUE_LLENO 4.0f // cm → 100 %
#define DIST_TANQUE_VACIO 9.0f // cm → 0 %
#define DIST_MIN_VALIDA 3.0f
#define DIST_MAX_VALIDA 25.0f
#define N_MUESTRAS_DIST 5
#define NIVEL_EMA_ALFA 0.5f
// Tiempos
#define PERIODO_SENSADO_MS 2000UL
#define VENTANA_TASA_MS 300000UL // 5 min (60 000 en pruebas aceleradas)
#define TENDENCIA_MS 10000UL
#define PRES_VENTANA_MS 10800000UL // 3 h (60 000 en pruebas aceleradas)
// Fusión
#define TASA_EVAP_MAX_PCT_HORA 4.0f
#define UMBRAL_FUGA_EXTRA_PCT_HORA -2.5f
#define NIVEL_DESBORDE_ON 95.0f
#define NIVEL_DESBORDE_OFF 90.0f
#define NIVEL_SEQUIA_ON 20.0f
#define NIVEL_SEQUIA_OFF 25.0f
#define NIVEL_RIESGO 35.0f
#define EVAP_RIESGO 70.0f
#define PRES_CAIDA_HPA 2.0f
#define EVAP_RIESGO_PRES 55.0f
// Fallas
#define FALLAS_NIVEL_MAX 5 // ≈ 12 s
#define FALLAS_SENSOR_MAX 3 // ≈ 7 s
#define CHEQUEO_OLED_MS 3000ULLas constantes de calibración CAL_T_A/B, CAL_H_A/B y CAL_P_A/B (forma valor = a·lectura + b) están en 1.0 / 0.0 en la versión entregada; los valores propuestos a partir de la calibración están en la sección 11.2.
HC-SR04: 5 disparos (cada 50 ms), velocidad del sonido compensada con T del DHT22
↓
Descartar muestras fuera de 3–25 cm
↓
¿≥ 3 válidas? ── no ──► contador de fallas +1 (5 seguidas → ERROR_SENSOR)
│ sí
↓
Mediana
↓
Distancia → % de nivel (4 cm = 100 %, 9 cm = 0 %), acotado a 0–100 %
↓
EMA (α = 0.5)
↓
Nivel filtrado
- Tendencia: cada 10 s se compara el nivel EMA con el de hace 10 s; si sube más de 1 punto porcentual → ↑, si baja más de 1 → ↓, si no → =.
-
Tasa real: cada 5 min (ventana
VENTANA_TASA_MS) se calcula(nivel_actual − nivel_inicio_ventana) / horasen %/h. Hasta completar la primera ventana la tasa no es válida y no se evalúa FUGA.
- Evaporación potencial (0–100): combinación ponderada de temperatura, humedad y UV normalizados (sección 8.9).
-
Pérdida esperada por evaporación:
−(evap / 100) × 4 %/h. -
Variación de presión: cada 3 h se compara la presión actual con la del inicio de la ventana; si cayó ≥ 2 hPa se activa la bandera
presionCae, que endurece la regla de sequía por riesgo.
Ver el diagrama de estados en la sección 7.12.3 y las reglas completas en la sección 8.4. El orden de prioridad es:
ERROR_SENSOR > DESBORDE > FUGA > SEQUIA > OK
Cada componente tiene un bit en datos.fallas:
| Bit | Componente | Criterio de falla | Efecto |
|---|---|---|---|
F_DHT 0x01 |
DHT22 | 3 lecturas inválidas seguidas |
ERR en OLED y tablero, chirrido, evento |
F_BMP 0x02 |
BMP180 | No responde en I2C o lectura fuera de 300–1100 hPa, 3 ciclos |
P:ERR, chirrido, evento, regla de presión desactivada |
F_UV 0x04 |
GUVA-S12SD | Dispersión > 150 mV, > 1.2 V o 0 mV exactos, 3 ciclos |
ERR, índice UV = 0, chirrido, evento |
F_ULTRA 0x08 |
HC-SR04 | < 3 muestras válidas, 5 ciclos | Estado ERROR_SENSOR, alarma continua |
F_OLED 0x10 |
OLED | No responde en I2C (verificación cada 3 s) | Aviso en tablero, chirrido, evento |
F_WIFI 0x20 |
WiFi | WiFi.status() != WL_CONNECTED |
x en OLED, evento; no suena el buzzer |
Al volver el componente, el bit se limpia, se registra RECUPERADO_x y, en el caso del BMP180 y la OLED, el dispositivo se reinicializa sin reiniciar el ESP32.
| Endpoint | Método | Respuesta |
|---|---|---|
/ |
GET | Página HTML del tablero (embebida en flash, PROGMEM) |
/data |
GET | JSON con el estado actual |
/historico |
GET | JSON con hasta 60 muestras |
/silenciar |
POST | Silencia el buzzer si hay alarma o falla sonora |
| otra | — | 404 |
Campos de /data: up, nivel, dist, temp, hum, presion, tempBmp, presDelta, uvV, uvIdx, evap, tasa, esperada, perdida, tendencia, catUV, estado, silenciada, presionCae, fallas (máscara de bits), rssi, eventos[].
El tablero (HTML + CSS + JavaScript, sin librerías externas) consulta los endpoints cada 4 s y muestra:
- Recuadro de estado con color por estado y aviso "alarma silenciada".
- Recuadro de fallas: "Sensor desconectado o sin lectura: …".
- Botón "Silenciar alarma física", visible solo si hay alarma o falla y no está silenciada.
- Nivel actual, distancia en cm, categoría UV, tendencia, ΔP y RSSI del WiFi.
- Tarjetas de temperatura, humedad, presión, índice UV y evaporación (
ERRsi el sensor falla). - Análisis de flujo: tasa real, pérdida esperada y pérdida extra.
- Registro de los últimos 10 eventos y tabla del histórico.
- Notificación activa: símbolo ⚠ en el título de la pestaña y vibración en celulares compatibles.
Control de acceso: acceso() compara la IP del cliente con la subred del ESP32 (IP & máscara); si no coincide responde 403. Si coincide, exige usuario y contraseña con HTTP Basic; si no son correctos responde 401.
Tarea de sensado ──escribe──► [mutexDatos] ◄──lee (snapshot)── loop()
▲
└──────lee (copia)──────── servidor web
Tarea de sensado (BMP180) ──► [mutexI2C] ◄── loop() (OLED: refresco y verificación)
- Todas las tomas de mutex usan timeout (50–200 ms), de modo que ningún flujo queda bloqueado indefinidamente.
-
snapshotDatos()devuelve la última copia válida si no consigue el mutex a tiempo. - El servidor responde 503 si no puede leer los datos.
- En
setup()la tarea de sensado se crea antes de conectar el WiFi; la conexión inicial espera hasta 15 s, pero la medición ya está corriendo. - En
loop(),gestionarWiFi()reintenta cada 10 s sin bloquear y registraFALLA_WIFI/RECUPERADO_WIFI. - Sin WiFi, el sensado, la clasificación, la OLED y el buzzer siguen funcionando; solo el tablero queda inaccesible.
Las credenciales reales no se publican: el repositorio contiene valores de ejemplo (WIFI_SSID = "XXXXXXXXX", WEB_PASS = "cambiame"), que deben reemplazarse antes de instalar el equipo.
flowchart LR
AUT([Autoridad local])
COM([Comunidad en sitio])
TEC([Técnico de mantenimiento])
subgraph SIS[AquaMonitor]
UC1((Consultar valor actual))
UC2((Consultar histórico y eventos))
UC3((Recibir notificación en tablero))
UC4((Silenciar alarma física))
UC5((Ver variables en la OLED))
UC6((Escuchar alarma sonora))
UC7((Autenticarse en el tablero))
UC8((Diagnosticar falla de sensor))
end
AUT --> UC1
AUT --> UC2
AUT --> UC3
AUT --> UC4
UC1 -. include .-> UC7
UC4 -. include .-> UC7
COM --> UC5
COM --> UC6
TEC --> UC8
TEC --> UC5
classDiagram
class Estado {
<<enumeration>>
EST_OK
EST_DESBORDE
EST_FUGA
EST_SEQUIA
EST_ERROR_SENSOR
}
class Datos {
float nivel, distCm
float temp, hum
float presion, tempBmp, presDelta
float uvV, uvIdx, evap
float tasaReal, tasaEsperada, perdidaExtra
int8_t tendencia
uint8_t catUV
uint8_t fallas
Estado estado
bool silenciada, presionCae
}
class Muestra {
unsigned long t
float nivel, temp, hum, uvIdx, evap, presion
Estado estado
}
class Evento {
unsigned long t
uint8_t codigo
}
class TareaSensado {
+tareaSensado(void*)
-leerUV(float&) bool
-medirDistanciaMediana(float) float
-calcularIndiceEvaporacion(t,h,uv) float
-calcularEstado(...) Estado
}
class GestorFallas {
+aplicarFalla(bit, activa, t)
+reportarFalla(bit, activa)
+registrarEvento(codigo, t)
}
class InterfazLocal {
+loop()
+dibujarPantalla(Datos)
+vigilarOLED(ahora)
}
class GestorWiFi {
+conectarWiFi() bool
+gestionarWiFi(ahora)
}
class ServidorWeb {
+configurarServidorWeb()
-acceso(req) bool
-ipEnSubred(ip) bool
}
Datos --> Estado
Muestra --> Estado
TareaSensado --> Datos : escribe (mutexDatos)
TareaSensado --> Muestra : historico[60]
TareaSensado --> GestorFallas
GestorFallas --> Evento : eventos[10]
InterfazLocal --> Datos : snapshot
InterfazLocal --> GestorFallas
GestorWiFi --> GestorFallas
ServidorWeb --> Datos : lee (mutexDatos)
ServidorWeb --> Muestra
ServidorWeb --> Evento
stateDiagram-v2
[*] --> OK
OK --> DESBORDE : nivel ≥ 95 %
DESBORDE --> OK : nivel < 90 %
OK --> FUGA : tasa < 0 y pérdida extra ≤ −2.5 %/h
FUGA --> OK : condición deja de cumplirse
OK --> SEQUIA : nivel ≤ 20 %, o nivel ≤ 35 % y evap ≥ 70 (55 si la presión cae)
SEQUIA --> OK : nivel > 25 % y sin riesgo por evaporación
OK --> ERROR_SENSOR : 5 ciclos sin eco válido
DESBORDE --> ERROR_SENSOR : 5 ciclos sin eco
FUGA --> ERROR_SENSOR : 5 ciclos sin eco
SEQUIA --> ERROR_SENSOR : 5 ciclos sin eco
ERROR_SENSOR --> OK : vuelve el eco (se reevalúan las reglas)
sequenceDiagram
actor U as Autoridad (navegador)
participant W as Servidor web (ESP32)
participant M as mutexDatos
participant T as Tarea de sensado
participant L as loop()
participant B as Buzzer
T->>M: toma mutex, escribe Datos (estado = SEQUIA)
L->>M: snapshot
L->>B: pitido intermitente
loop cada 4 s
U->>W: GET /data
W->>W: acceso() — subred y credenciales
alt fuera de la subred
W-->>U: 403
else sin credenciales válidas
W-->>U: 401
else autorizado
W->>M: copia Datos y eventos
W-->>U: 200 JSON
end
end
U->>W: POST /silenciar
W->>M: datos.silenciada = true
W-->>U: 200 ok=true
L->>M: snapshot (silenciada)
L->>B: apagado
flowchart TD
S([Inicio de ciclo]) --> D[Leer DHT22]
D --> DV{¿Válido?}
DV -- sí --> DA[Aplicar calibración, fallasDHT = 0]
DV -- no --> DF[fallasDHT++]
DA --> P
DF --> P
P[Tomar mutexI2C, verificar BMP180 en 0x77] --> PV{¿Responde y en rango?}
PV -- sí --> PA[Presión calibrada, fallasBmp = 0]
PV -- no --> PF[fallasBmp++, reinicializar al volver]
PA --> PW{¿Pasó la ventana de 3 h?}
PW -- sí --> PC[Calcular ΔP, presionCae si ΔP ≤ −2 hPa]
PW -- no --> U
PC --> U
PF --> U
U[Leer GUVA, 16 muestras] --> UV{¿Ruido, saturación o 0 mV?}
UV -- no --> UI[Índice UV]
UV -- sí --> UF[fallasUV++]
UI --> N
UF --> N
N[HC-SR04: 5 disparos, mediana] --> NV{¿≥ 3 válidas?}
NV -- sí --> NE[Nivel en porcentaje, EMA, tendencia, tasa]
NV -- no --> NF[fallasNivel++]
NE --> E
NF --> E
E[Índice de evaporación y pérdida esperada] --> MX[Tomar mutexDatos]
MX --> F[Actualizar bits de falla]
F --> C[calcularEstado - fusión]
C --> CE{¿Cambió el estado?}
CE -- sí --> EV[Registrar evento, rearmar alarma]
CE -- no --> H
EV --> H{¿Pasó 1 min?}
H -- sí --> HI[Guardar muestra en histórico]
H -- no --> R
HI --> R[Liberar mutex, imprimir en serie]
R --> W[Esperar 2 s] --> S