Skip to content

07. Diseno de software

federicovaldez14 edited this page Oct 2, 2026 · 8 revisions

7. Diseño de software

7.1 Estructura del software — v3.2

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)

7.2 Librerías utilizadas

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

7.3 Parámetros principales

// 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    3000UL

Las 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.

7.4 Adquisición y filtrado del nivel

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

7.5 Tendencia y tasa de cambio

  • 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) / horas en %/h. Hasta completar la primera ventana la tasa no es válida y no se evalúa FUGA.

7.6 Índice de evaporación y presión

  • 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.

7.7 Máquina de estados

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

7.8 Detección de fallas por componente

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.

7.9 Servidor web local y tablero de control

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 (ERR si 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.

7.10 Sincronización mediante mutex

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.

7.11 Manejo de WiFi y credenciales

  • 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 registra FALLA_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.

7.12 Diagramas UML

7.12.1 Diagrama de casos de uso

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
Loading

7.12.2 Diagrama de clases (estructuras y módulos)

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
Loading

7.12.3 Diagrama de estados (lógica de fusión)

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)
Loading

7.12.4 Diagrama de secuencia — consulta y silenciado desde el tablero

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
Loading

7.12.5 Diagrama de actividad — un ciclo de la tarea de sensado

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
Loading

Clone this wiki locally