Módulo 12

Proyecto 1: Estación meteorológica IoT

Proyecto Final Integral

ESP32 Mecatrónica IoT UNAM

Introducción a las Estaciones Meteorológicas IoT

Las estaciones meteorológicas han evolucionado significativamente desde los primeros instrumentos mecánicos hasta los modernos sistemas digitales conectados. En la era del Internet de las Cosas (IoT), estas estaciones no solo recopilan datos ambientales, sino que los transmiten en tiempo real, permitiendo análisis avanzados y toma de decisiones automatizada.

El ESP32 representa una revolución en el desarrollo de estaciones meteorológicas personales y profesionales. Con su capacidad de conectividad Wi-Fi y Bluetooth integrada, procesamiento de doble núcleo y bajo consumo energético, es la plataforma ideal para desarrollar sistemas de monitoreo ambiental autónomos y conectados.

Las aplicaciones de las estaciones meteorológicas IoT abarcan múltiples sectores:

  • Agricultura de precisión: Monitoreo de microclimas para optimización de cultivos
  • Gestión urbana: Calidad del aire y condiciones ambientales en ciudades inteligentes
  • Investigación científica: Recolección de datos ambientales a largo plazo
  • Automatización residencial: Sistemas de climatización y riego inteligentes
  • Prevención de desastres: Alerta temprana de condiciones meteorológicas extremas

Arquitectura Técnica del Sistema

Sensores Meteorológicos Avanzados

La precisión en las mediciones meteorológicas depende crucialmente de la selección y calibración de sensores. Para nuestro proyecto, utilizamos un conjunto integral de sensores de alta precisión:

BME680 - Sensor Ambiental Multiparámetro
  • Temperatura: -40°C a +85°C (±0.5°C)
  • Humedad relativa: 0-100% RH (±3%)
  • Presión atmosférica: 300-1100 hPa (±0.12 hPa)
  • Calidad del aire: VOC y CO₂ equivalente
  • Comunicación: I2C/SPI
  • Consumo: 3.7 μA en standby
  • Compensación automática de temperatura
  • Algoritmo IAQ integrado

Sensores Complementarios

  • Anemómetro (AS5600): Medición de velocidad y dirección del viento con sensor magnético de efecto Hall
  • Pluviómetro (Reed Switch): Detección de precipitación por báscula basculante
  • Piranómetro (TSL2591): Medición de radiación solar con rango dinámico de 600M:1
  • Sensor UV (VEML6070): Índice ultravioleta con compensación de temperatura

Conectividad y Protocolos IoT

La transmisión de datos meteorológicos requiere protocolos eficientes y confiables:

Protocolos de Comunicación
  • MQTT: Telemetría ligera con QoS configurable
  • HTTP/HTTPS: APIs RESTful para servicios web
  • CoAP: Protocolo optimizado para dispositivos IoT
  • LoRaWAN: Comunicación de largo alcance y bajo consumo
Plataformas en la Nube
  • ThingsBoard: Plataforma IoT open-source
  • AWS IoT Core: Servicios gestionados de Amazon
  • Azure IoT Hub: Plataforma empresarial de Microsoft
  • Google Cloud IoT: Análisis avanzado con ML

Implementación del Sistema

Código Principal - Sistema de Adquisición de Datos

C++/Arduino
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_BME680.h>
#include <Adafruit_TSL2591.h>
#include <RTClib.h>
#include <SD.h>

// Configuración de red y MQTT
const char* ssid = "TU_RED_WIFI";
const char* password = "TU_PASSWORD";
const char* mqtt_server = "tu-broker-mqtt.com";
const int mqtt_port = 1883;
const char* mqtt_user = "usuario_mqtt";
const char* mqtt_password = "password_mqtt";

// Instancias de sensores
Adafruit_BME680 bme680; // Temperatura, humedad, presión, calidad del aire
Adafruit_TSL2591 tsl = Adafruit_TSL2591(2591); // Sensor de luz/radiación solar
RTC_DS3231 rtc; // Reloj en tiempo real para timestamps precisos

// Instancias de conectividad
WiFiClient espClient;
PubSubClient client(espClient);

// Estructura de datos meteorológicos
struct WeatherData {
  float temperature;      // °C
  float humidity;         // %
  float pressure;         // hPa
  float airQuality;       // IAQ Index
  float windSpeed;        // m/s
  float windDirection;    // grados
  float rainfall;         // mm
  float solarRadiation;   // W/m²
  float uvIndex;          // UV Index
  uint32_t timestamp;     // Unix timestamp
  String location;        // GPS coordinates
};

// Pines de sensores adicionales
#define WIND_SPEED_PIN 2
#define WIND_DIRECTION_PIN A0
#define RAIN_GAUGE_PIN 3
#define UV_SENSOR_PIN A1

// Variables globales
WeatherData currentWeather;
volatile int rainPulses = 0;
volatile unsigned long lastWindPulse = 0;
volatile int windPulses = 0;
unsigned long lastSensorRead = 0;
unsigned long lastDataSend = 0;
const unsigned long SENSOR_INTERVAL = 10000; // 10 segundos
const unsigned long SEND_INTERVAL = 60000;   // 1 minuto

void setup() {
  Serial.begin(115200);
  
  // Inicialización de pines
  pinMode(WIND_SPEED_PIN, INPUT_PULLUP);
  pinMode(RAIN_GAUGE_PIN, INPUT_PULLUP);
  
  // Interrupciones para sensores de viento y lluvia
  attachInterrupt(digitalPinToInterrupt(WIND_SPEED_PIN), windSpeedISR, FALLING);
  attachInterrupt(digitalPinToInterrupt(RAIN_GAUGE_PIN), rainGaugeISR, FALLING);
  
  // Inicialización de sensores I2C
  Wire.begin(21, 22); // SDA=21, SCL=22
  
  if (!bme680.begin()) {
    Serial.println("Error: No se pudo inicializar BME680");
    while(1);
  }
  
  if (!tsl.begin()) {
    Serial.println("Error: No se pudo inicializar TSL2591");
    while(1);
  }
  
  if (!rtc.begin()) {
    Serial.println("Error: No se pudo inicializar RTC");
    while(1);
  }
  
  // Configuración del BME680
  bme680.setTemperatureOversampling(BME680_OS_8X);
  bme680.setHumidityOversampling(BME680_OS_2X);
  bme680.setPressureOversampling(BME680_OS_4X);
  bme680.setIIRFilterSize(BME680_FILTER_SIZE_3);
  bme680.setGasHeater(320, 150); // 320°C por 150ms
  
  // Configuración del TSL2591
  tsl.setGain(TSL2591_GAIN_MED);
  tsl.setTiming(TSL2591_INTEGRATIONTIME_300MS);
  
  // Inicialización de Wi-Fi
  setupWiFi();
  
  // Configuración MQTT
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(mqttCallback);
  
  // Inicialización de tarjeta SD para logging local
  if (!SD.begin(5)) {
    Serial.println("Advertencia: Tarjeta SD no disponible");
  }
  
  Serial.println("Estación Meteorológica IoT - Sistema Iniciado");
  printSensorDetails();
}

void loop() {
  // Mantener conexión MQTT
  if (!client.connected()) {
    reconnectMQTT();
  }
  client.loop();
  
  // Lectura periódica de sensores
  if (millis() - lastSensorRead > SENSOR_INTERVAL) {
    readAllSensors();
    lastSensorRead = millis();
  }
  
  // Envío periódico de datos
  if (millis() - lastDataSend > SEND_INTERVAL) {
    sendDataToCloud();
    saveDataToSD();
    lastDataSend = millis();
  }
  
  delay(1000);
}

void readAllSensors() {
  // Lectura del BME680
  if (bme680.performReading()) {
    currentWeather.temperature = bme680.temperature;
    currentWeather.humidity = bme680.humidity;
    currentWeather.pressure = bme680.pressure / 100.0; // Pa a hPa
    currentWeather.airQuality = bme680.gas_resistance / 1000.0; // Aproximación IAQ
  }
  
  // Lectura del sensor de luz solar
  uint32_t lum = tsl.getFullLuminosity();
  uint16_t ir = lum >> 16;
  uint16_t full = lum & 0xFFFF;
  currentWeather.solarRadiation = tsl.calculateLux(full, ir) * 0.0079; // Conversión aproximada a W/m²
  
  // Lectura de sensores analógicos
  currentWeather.windDirection = readWindDirection();
  currentWeather.uvIndex = readUVIndex();
  
  // Cálculo de velocidad del viento (basado en interrupciones)
  currentWeather.windSpeed = calculateWindSpeed();
  
  // Cálculo de precipitación
  currentWeather.rainfall = rainPulses * 0.2794; // 0.2794 mm por pulse (estándar)
  
  // Timestamp
  DateTime now = rtc.now();
  currentWeather.timestamp = now.unixtime();
  
  // Mostrar lecturas en monitor serie
  printWeatherData();
}

float readWindDirection() {
  int sensorValue = analogRead(WIND_DIRECTION_PIN);
  float voltage = sensorValue * (3.3 / 4095.0);
  
  // Conversión de voltaje a grados (veleta con divisores resistivos)
  // Esta conversión debe calibrarse según el sensor específico
  float direction = voltage * (360.0 / 3.3);
  
  return direction;
}

float readUVIndex() {
  int sensorValue = analogRead(UV_SENSOR_PIN);
  float voltage = sensorValue * (3.3 / 4095.0);
  
  // Conversión específica del sensor VEML6070 o similar
  float uvIndex = voltage * 10.0; // Aproximación
  
  return constrain(uvIndex, 0, 15);
}

float calculateWindSpeed() {
  static unsigned long lastCalculation = 0;
  static int lastPulses = 0;
  
  unsigned long currentTime = millis();
  unsigned long timeInterval = currentTime - lastCalculation;
  
  if (timeInterval >= 1000) { // Cálculo cada segundo
    int currentPulses = windPulses;
    int pulseDifference = currentPulses - lastPulses;
    
    // Conversión: 1 pulso = 2.4 km/h = 0.667 m/s (según especificaciones del anemómetro)
    float speed = (pulseDifference * 0.667 * 1000) / timeInterval;
    
    lastPulses = currentPulses;
    lastCalculation = currentTime;
    
    return speed;
  }
  
  return currentWeather.windSpeed; // Mantener valor anterior
}

void sendDataToCloud() {
  // Crear JSON con datos meteorológicos
  DynamicJsonDocument doc(1024);
  doc["station_id"] = "ESP32_WS_001";
  doc["timestamp"] = currentWeather.timestamp;
  doc["temperature"] = currentWeather.temperature;
  doc["humidity"] = currentWeather.humidity;
  doc["pressure"] = currentWeather.pressure;
  doc["air_quality"] = currentWeather.airQuality;
  doc["wind_speed"] = currentWeather.windSpeed;
  doc["wind_direction"] = currentWeather.windDirection;
  doc["rainfall"] = currentWeather.rainfall;
  doc["solar_radiation"] = currentWeather.solarRadiation;
  doc["uv_index"] = currentWeather.uvIndex;
  
  // Geolocalización (si se dispone de GPS)
  doc["latitude"] = 19.3263; // UNAM, Ciudad de México
  doc["longitude"] = -99.1773;
  doc["altitude"] = 2240; // metros sobre nivel del mar
  
  String jsonString;
  serializeJson(doc, jsonString);
  
  // Envío por MQTT
  if (client.connected()) {
    client.publish("weather/data", jsonString.c_str());
    Serial.println("Datos enviados a broker MQTT");
  }
  
  // Envío por HTTP API (backup)
  sendHTTPData(jsonString);
}

void sendHTTPData(String jsonData) {
  WiFiClient httpClient;
  
  if (httpClient.connect("api.weatherstation.com", 80)) {
    httpClient.println("POST /api/v1/weather HTTP/1.1");
    httpClient.println("Host: api.weatherstation.com");
    httpClient.println("Content-Type: application/json");
    httpClient.println("Authorization: Bearer TU_API_TOKEN");
    httpClient.print("Content-Length: ");
    httpClient.println(jsonData.length());
    httpClient.println();
    httpClient.println(jsonData);
    
    // Leer respuesta
    String response = httpClient.readString();
    Serial.println("Respuesta HTTP: " + response);
    
    httpClient.stop();
  }
}

void saveDataToSD() {
  File dataFile = SD.open("/weather_log.csv", FILE_APPEND);
  
  if (dataFile) {
    // Formato CSV con timestamp
    DateTime now = rtc.now();
    dataFile.print(now.year()); dataFile.print("-");
    dataFile.print(now.month()); dataFile.print("-");
    dataFile.print(now.day()); dataFile.print(" ");
    dataFile.print(now.hour()); dataFile.print(":");
    dataFile.print(now.minute()); dataFile.print(":");
    dataFile.print(now.second()); dataFile.print(",");
    
    dataFile.print(currentWeather.temperature); dataFile.print(",");
    dataFile.print(currentWeather.humidity); dataFile.print(",");
    dataFile.print(currentWeather.pressure); dataFile.print(",");
    dataFile.print(currentWeather.airQuality); dataFile.print(",");
    dataFile.print(currentWeather.windSpeed); dataFile.print(",");
    dataFile.print(currentWeather.windDirection); dataFile.print(",");
    dataFile.print(currentWeather.rainfall); dataFile.print(",");
    dataFile.print(currentWeather.solarRadiation); dataFile.print(",");
    dataFile.println(currentWeather.uvIndex);
    
    dataFile.close();
    Serial.println("Datos guardados en SD");
  }
}

// Rutinas de interrupción
void IRAM_ATTR windSpeedISR() {
  unsigned long currentTime = micros();
  if (currentTime - lastWindPulse > 50000) { // Debounce de 50ms
    windPulses++;
    lastWindPulse = currentTime;
  }
}

void IRAM_ATTR rainGaugeISR() {
  static unsigned long lastRainPulse = 0;
  unsigned long currentTime = millis();
  
  if (currentTime - lastRainPulse > 100) { // Debounce de 100ms
    rainPulses++;
    lastRainPulse = currentTime;
  }
}

void setupWiFi() {
  WiFi.begin(ssid, password);
  Serial.print("Conectando a WiFi");
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.print(".");
  }
  
  Serial.println();
  Serial.println("WiFi conectado");
  Serial.print("Dirección IP: ");
  Serial.println(WiFi.localIP());
}

void reconnectMQTT() {
  while (!client.connected()) {
    Serial.print("Intentando conexión MQTT...");
    
    String clientId = "ESP32WeatherStation-";
    clientId += String(random(0xffff), HEX);
    
    if (client.connect(clientId.c_str(), mqtt_user, mqtt_password)) {
      Serial.println("Conectado al broker MQTT");
      client.subscribe("weather/commands");
    } else {
      Serial.print("Error, rc=");
      Serial.print(client.state());
      Serial.println(" Reintentando en 5 segundos");
      delay(5000);
    }
  }
}

void mqttCallback(char* topic, byte* payload, unsigned int length) {
  String message;
  for (int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  
  Serial.println("Comando recibido: " + message);
  
  // Procesar comandos remotos
  if (message == "RESET_RAIN") {
    rainPulses = 0;
    client.publish("weather/status", "Rain counter reset");
  } else if (message == "CALIBRATE") {
    calibrateSensors();
  } else if (message == "STATUS") {
    sendStatusReport();
  }
}

void calibrateSensors() {
  Serial.println("Iniciando calibración de sensores...");
  
  // Calibración específica para cada sensor
  // Temperatura: comparar con termómetro de referencia
  // Presión: ajustar según altitud conocida
  // Viento: calibrar con velocidades conocidas
  
  Serial.println("Calibración completada");
  client.publish("weather/status", "Sensor calibration completed");
}

void sendStatusReport() {
  DynamicJsonDocument statusDoc(512);
  statusDoc["uptime"] = millis();
  statusDoc["free_heap"] = ESP.getFreeHeap();
  statusDoc["wifi_rssi"] = WiFi.RSSI();
  statusDoc["last_reading"] = currentWeather.timestamp;
  
  String statusJson;
  serializeJson(statusDoc, statusJson);
  client.publish("weather/status", statusJson.c_str());
}

void printWeatherData() {
  Serial.println("=== LECTURA METEOROLÓGICA ===");
  Serial.printf("Temperatura: %.2f°C\n", currentWeather.temperature);
  Serial.printf("Humedad: %.2f%%\n", currentWeather.humidity);
  Serial.printf("Presión: %.2f hPa\n", currentWeather.pressure);
  Serial.printf("Calidad del aire: %.2f\n", currentWeather.airQuality);
  Serial.printf("Velocidad del viento: %.2f m/s\n", currentWeather.windSpeed);
  Serial.printf("Dirección del viento: %.2f°\n", currentWeather.windDirection);
  Serial.printf("Precipitación: %.2f mm\n", currentWeather.rainfall);
  Serial.printf("Radiación solar: %.2f W/m²\n", currentWeather.solarRadiation);
  Serial.printf("Índice UV: %.2f\n", currentWeather.uvIndex);
  Serial.printf("Timestamp: %u\n", currentWeather.timestamp);
  Serial.println("=============================");
}

void printSensorDetails() {
  Serial.println("=== DETALLES DE SENSORES ===");
  Serial.println("BME680 - Sensor Ambiental:");
  Serial.println("  - Temperatura: ±0.5°C");
  Serial.println("  - Humedad: ±3% RH");
  Serial.println("  - Presión: ±0.12 hPa");
  Serial.println("  - Calidad del aire: VOC");
  
  Serial.println("TSL2591 - Sensor de Luz:");
  Serial.println("  - Rango dinámico: 600M:1");
  Serial.println("  - Resolución: 16-bit");
  
  Serial.println("Sensores adicionales:");
  Serial.println("  - Anemómetro: Reed switch");
  Serial.println("  - Pluviómetro: Báscula basculante");
  Serial.println("  - Sensor UV: Analógico");
  Serial.println("============================");
}

Dashboard Web Interactivo

HTML/JavaScript
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Estación Meteorológica IoT - Dashboard</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/mqtt.min.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }
        
        body {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }
        
        .dashboard {
            max-width: 1400px;
            margin: 0 auto;
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
            gap: 20px;
        }
        
        .card {
            background: rgba(255, 255, 255, 0.95);
            border-radius: 15px;
            padding: 20px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
            backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.2);
        }
        
        .card h3 {
            color: #333;
            margin-bottom: 15px;
            display: flex;
            align-items: center;
            gap: 10px;
        }
        
        .metric {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 10px 0;
            border-bottom: 1px solid #eee;
        }
        
        .metric:last-child {
            border-bottom: none;
        }
        
        .metric-value {
            font-size: 1.2em;
            font-weight: bold;
            color: #667eea;
        }
        
        .chart-container {
            grid-column: span 2;
            height: 400px;
        }
        
        .status-indicator {
            width: 12px;
            height: 12px;
            border-radius: 50%;
            display: inline-block;
            margin-right: 8px;
        }
        
        .status-online {
            background: #4CAF50;
            box-shadow: 0 0 10px rgba(76, 175, 80, 0.5);
        }
        
        .status-offline {
            background: #f44336;
            box-shadow: 0 0 10px rgba(244, 67, 54, 0.5);
        }
        
        .alert {
            background: #ff9800;
            color: white;
            padding: 15px;
            border-radius: 10px;
            margin: 10px 0;
            display: none;
        }
        
        .weather-icon {
            font-size: 2em;
            margin-right: 15px;
        }
    </style>
</head>
<body>
    <div class="dashboard">
        <div class="card">
            <h3>🌡️ Temperatura</h3>
            <div class="metric">
                <span>Actual</span>
                <span class="metric-value" id="temperature">--°C</span>
            </div>
            <div class="metric">
                <span>Máxima (24h)</span>
                <span class="metric-value" id="temp-max">--°C</span>
            </div>
            <div class="metric">
                <span>Mínima (24h)</span>
                <span class="metric-value" id="temp-min">--°C</span>
            </div>
        </div>
        
        <div class="card">
            <h3>💧 Humedad</h3>
            <div class="metric">
                <span>Relativa</span>
                <span class="metric-value" id="humidity">--%</span>
            </div>
            <div class="metric">
                <span>Punto de rocío</span>
                <span class="metric-value" id="dewpoint">--°C</span>
            </div>
        </div>
        
        <div class="card">
            <h3>📊 Presión Atmosférica</h3>
            <div class="metric">
                <span>Actual</span>
                <span class="metric-value" id="pressure">-- hPa</span>
            </div>
            <div class="metric">
                <span>Tendencia</span>
                <span class="metric-value" id="pressure-trend">--</span>
            </div>
        </div>
        
        <div class="card">
            <h3>💨 Viento</h3>
            <div class="metric">
                <span>Velocidad</span>
                <span class="metric-value" id="wind-speed">-- m/s</span>
            </div>
            <div class="metric">
                <span>Dirección</span>
                <span class="metric-value" id="wind-direction">--°</span>
            </div>
            <div class="metric">
                <span>Ráfaga máx</span>
                <span class="metric-value" id="wind-gust">-- m/s</span>
            </div>
        </div>
        
        <div class="card">
            <h3>🌧️ Precipitación</h3>
            <div class="metric">
                <span>Actual</span>
                <span class="metric-value" id="rainfall">-- mm</span>
            </div>
            <div class="metric">
                <span>Últimas 24h</span>
                <span class="metric-value" id="rainfall-24h">-- mm</span>
            </div>
        </div>
        
        <div class="card">
            <h3>☀️ Radiación Solar</h3>
            <div class="metric">
                <span>Radiación</span>
                <span class="metric-value" id="solar-radiation">-- W/m²</span>
            </div>
            <div class="metric">
                <span>Índice UV</span>
                <span class="metric-value" id="uv-index">--</span>
            </div>
        </div>
        
        <div class="card">
            <h3>🏭 Calidad del Aire</h3>
            <div class="metric">
                <span>IAQ Index</span>
                <span class="metric-value" id="air-quality">--</span>
            </div>
            <div class="metric">
                <span>Estado</span>
                <span class="metric-value" id="air-status">--</span>
            </div>
        </div>
        
        <div class="card">
            <h3>🔗 Estado del Sistema</h3>
            <div class="metric">
                <span>Conexión</span>
                <span><span class="status-indicator status-offline" id="connection-status"></span> Desconectado</span>
            </div>
            <div class="metric">
                <span>Última actualización</span>
                <span class="metric-value" id="last-update">--</span>
            </div>
            <div class="metric">
                <span>Tiempo activo</span>
                <span class="metric-value" id="uptime">--</span>
            </div>
        </div>
        
        <div class="card chart-container">
            <h3>📈 Tendencias de Temperatura (24h)</h3>
            <canvas id="temperatureChart"></canvas>
        </div>
        
        <div class="card chart-container">
            <h3>📊 Presión Atmosférica (24h)</h3>
            <canvas id="pressureChart"></canvas>
        </div>
    </div>
    
    <div class="alert" id="weatherAlert">
        <strong>⚠️ Alerta Meteorológica</strong><br>
        <span id="alertMessage"></span>
    </div>

    <script>
        // Configuración MQTT
        const brokerUrl = 'wss://broker.hivemq.com:8884/mqtt';
        const client = mqtt.connect(brokerUrl);
        
        // Arrays para almacenar datos históricos
        let temperatureData = [];
        let pressureData = [];
        let timestamps = [];
        
        // Configuración de gráficos
        const temperatureChart = new Chart(document.getElementById('temperatureChart'), {
            type: 'line',
            data: {
                labels: timestamps,
                datasets: [{
                    label: 'Temperatura (°C)',
                    data: temperatureData,
                    borderColor: 'rgb(255, 99, 132)',
                    backgroundColor: 'rgba(255, 99, 132, 0.1)',
                    tension: 0.4
                }]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                scales: {
                    y: {
                        beginAtZero: false
                    }
                }
            }
        });
        
        const pressureChart = new Chart(document.getElementById('pressureChart'), {
            type: 'line',
            data: {
                labels: timestamps,
                datasets: [{
                    label: 'Presión (hPa)',
                    data: pressureData,
                    borderColor: 'rgb(54, 162, 235)',
                    backgroundColor: 'rgba(54, 162, 235, 0.1)',
                    tension: 0.4
                }]
            },
            options: {
                responsive: true,
                maintainAspectRatio: false,
                scales: {
                    y: {
                        beginAtZero: false
                    }
                }
            }
        });
        
        // Conexión MQTT
        client.on('connect', function () {
            console.log('Conectado al broker MQTT');
            document.getElementById('connection-status').className = 'status-indicator status-online';
            client.subscribe('weather/data');
            client.subscribe('weather/status');
        });
        
        client.on('message', function (topic, message) {
            try {
                const data = JSON.parse(message.toString());
                
                if (topic === 'weather/data') {
                    updateWeatherData(data);
                } else if (topic === 'weather/status') {
                    updateSystemStatus(data);
                }
            } catch (error) {
                console.error('Error parsing MQTT message:', error);
            }
        });
        
        function updateWeatherData(data) {
            // Actualizar valores en tiempo real
            document.getElementById('temperature').textContent = data.temperature.toFixed(1) + '°C';
            document.getElementById('humidity').textContent = data.humidity.toFixed(1) + '%';
            document.getElementById('pressure').textContent = data.pressure.toFixed(1) + ' hPa';
            document.getElementById('wind-speed').textContent = data.wind_speed.toFixed(1) + ' m/s';
            document.getElementById('wind-direction').textContent = data.wind_direction.toFixed(0) + '°';
            document.getElementById('rainfall').textContent = data.rainfall.toFixed(1) + ' mm';
            document.getElementById('solar-radiation').textContent = data.solar_radiation.toFixed(0) + ' W/m²';
            document.getElementById('uv-index').textContent = data.uv_index.toFixed(1);
            document.getElementById('air-quality').textContent = data.air_quality.toFixed(0);
            
            // Calcular punto de rocío
            const dewPoint = calculateDewPoint(data.temperature, data.humidity);
            document.getElementById('dewpoint').textContent = dewPoint.toFixed(1) + '°C';
            
            // Actualizar estado de calidad del aire
            const airStatus = getAirQualityStatus(data.air_quality);
            document.getElementById('air-status').textContent = airStatus;
            
            // Actualizar timestamp
            const lastUpdate = new Date().toLocaleTimeString();
            document.getElementById('last-update').textContent = lastUpdate;
            
            // Actualizar gráficos
            updateCharts(data);
            
            // Verificar alertas
            checkWeatherAlerts(data);
        }
        
        function updateSystemStatus(status) {
            if (status.uptime) {
                const uptime = Math.floor(status.uptime / 1000 / 60); // minutos
                document.getElementById('uptime').textContent = uptime + ' min';
            }
        }
        
        function calculateDewPoint(temp, humidity) {
            const a = 17.27;
            const b = 237.7;
            const alpha = ((a * temp) / (b + temp)) + Math.log(humidity / 100);
            return (b * alpha) / (a - alpha);
        }
        
        function getAirQualityStatus(iaq) {
            if (iaq <= 50) return 'Excelente';
            if (iaq <= 100) return 'Buena';
            if (iaq <= 150) return 'Moderada';
            if (iaq <= 200) return 'Mala';
            if (iaq <= 300) return 'Muy mala';
            return 'Peligrosa';
        }
        
        function updateCharts(data) {
            const now = new Date().toLocaleTimeString();
            
            // Limitar a 50 puntos de datos
            if (timestamps.length >= 50) {
                timestamps.shift();
                temperatureData.shift();
                pressureData.shift();
            }
            
            timestamps.push(now);
            temperatureData.push(data.temperature);
            pressureData.push(data.pressure);
            
            temperatureChart.update('none');
            pressureChart.update('none');
        }
        
        function checkWeatherAlerts(data) {
            const alerts = [];
            
            if (data.temperature > 35) {
                alerts.push('Temperatura extrema: ' + data.temperature.toFixed(1) + '°C');
            }
            
            if (data.wind_speed > 15) {
                alerts.push('Vientos fuertes: ' + data.wind_speed.toFixed(1) + ' m/s');
            }
            
            if (data.uv_index > 8) {
                alerts.push('Índice UV muy alto: ' + data.uv_index.toFixed(1));
            }
            
            if (data.air_quality > 200) {
                alerts.push('Calidad del aire mala: IAQ ' + data.air_quality.toFixed(0));
            }
            
            if (alerts.length > 0) {
                const alertDiv = document.getElementById('weatherAlert');
                const alertMessage = document.getElementById('alertMessage');
                alertMessage.innerHTML = alerts.join('<br>');
                alertDiv.style.display = 'block';
                
                // Ocultar alerta después de 10 segundos
                setTimeout(() => {
                    alertDiv.style.display = 'none';
                }, 10000);
            }
        }
        
        // Simular datos para demo (remover en implementación real)
        function simulateData() {
            const mockData = {
                temperature: 22 + Math.random() * 10,
                humidity: 50 + Math.random() * 30,
                pressure: 1013 + Math.random() * 20,
                wind_speed: Math.random() * 10,
                wind_direction: Math.random() * 360,
                rainfall: Math.random() * 5,
                solar_radiation: Math.random() * 1000,
                uv_index: Math.random() * 12,
                air_quality: 50 + Math.random() * 100,
                timestamp: Date.now()
            };
            
            updateWeatherData(mockData);
        }
        
        // Simular datos cada 5 segundos para demo
        setInterval(simulateData, 5000);
        simulateData(); // Llamada inicial
    </script>
</body>
</html>

Ejercicios Prácticos Progresivos

1

Configuración Básica de Sensores

Básico 30 min

Objetivo: Implementar la lectura básica de sensores meteorológicos con calibración inicial.

Materiales necesarios:
  • ESP32 DevKit V1
  • Sensor BME680 (temperatura, humedad, presión, calidad del aire)
  • Sensor TSL2591 (radiación solar)
  • Resistencias pull-up 4.7kΩ para I2C
  • Protoboard y cables de conexión
  • Fuente de alimentación 3.3V/5V
Tareas a realizar:
  1. Conectar sensores mediante protocolo I2C
  2. Configurar librerías y inicializar sensores
  3. Implementar rutina de calibración automática
  4. Validar precisión con instrumentos de referencia
  5. Crear sistema de compensación por altitud
2

Integración de Sensores Mecánicos

Intermedio 45 min

Objetivo: Agregar sensores mecánicos de viento y lluvia con procesamiento de señales por interrupciones.

Sensores mecánicos:
  • Anemómetro con sensor reed switch
  • Veleta con potenciómetro circular
  • Pluviómetro de báscula basculante
  • Sensor de dirección del viento magnético
Implementaciones técnicas:
  1. Configurar interrupciones para pulsos de viento
  2. Implementar filtros digitales anti-rebote
  3. Calcular velocidad promedio y ráfagas máximas
  4. Procesar dirección con compensación magnética
  5. Acumular precipitación con persistencia
3

Conectividad IoT y Transmisión de Datos

Intermedio 60 min

Objetivo: Establecer conectividad IoT con múltiples protocolos y redundancia de comunicación.

Protocolos de comunicación:
  • MQTT con broker local y en la nube
  • HTTP/HTTPS para APIs weather services
  • WebSocket para dashboard en tiempo real
  • NTP para sincronización temporal precisa
Características avanzadas:
  1. Configurar QoS apropiado para datos críticos
  2. Implementar buffer local para conexión intermitente
  3. Crear sistema de alertas automáticas
  4. Integrar con plataformas meteorológicas
  5. Establecer autenticación segura
4

Dashboard Profesional y Análisis

Avanzado 90 min

Objetivo: Desarrollar dashboard web interactivo con análisis meteorológico avanzado y predicciones.

Funcionalidades del dashboard:
  • Visualización en tiempo real con gráficos dinámicos
  • Análisis histórico y tendencias meteorológicas
  • Sistema de alertas configurables
  • Integración con APIs de pronóstico
  • Exportación de datos en múltiples formatos
Tecnologías utilizadas:
  1. Chart.js para visualizaciones interactivas
  2. WebSocket para actualizaciones en tiempo real
  3. Progressive Web App (PWA) para móviles
  4. Machine Learning para predicciones básicas
  5. Geolocalización y mapas meteorológicos

Proyecto Aplicado: Estación Meteorológica Profesional

Descripción del Proyecto

Desarrollo de una estación meteorológica profesional con capacidades IoT completas, diseñada para aplicaciones en agricultura de precisión, investigación ambiental y monitoreo urbano. El sistema integra sensores de alta precisión, conectividad redundante y análisis inteligente de datos.

Especificaciones Técnicas
  • Precisión de temperatura: ±0.3°C
  • Precisión de humedad: ±2% RH
  • Precisión de presión: ±0.1 hPa
  • Resolución de viento: 0.1 m/s
  • Resolución de lluvia: 0.2 mm
  • Frecuencia de muestreo: 10 segundos
  • Transmisión de datos: 1 minuto
  • Autonomía con batería: 30 días
Lista de Materiales
Componentes principales:
  • ESP32-WROOM-32 DevKit V1
  • Sensor BME680 (ambiente multi-parámetro)
  • Sensor TSL2591 (radiación solar)
  • Anemómetro profesional (Davis Instruments)
  • Pluviómetro de báscula (0.2mm resolución)
  • Módulo RTC DS3231 (precisión temporal)
  • Tarjeta MicroSD para logging local
  • Panel solar 10W + batería LiPo 6000mAh
  • Caja estanca IP65 con ventilación
  • Mástil meteorológico de 3 metros
Implementación Paso a Paso
Fase 1: Ensamblaje y Calibración (Semana 1)
  1. Montaje de componentes en caja estanca con separación térmica
  2. Instalación de sensores con protección contra radiación
  3. Calibración de sensores con estándares de referencia
  4. Configuración de sistema de alimentación solar
  5. Pruebas de resistencia a condiciones ambientales
Fase 2: Software y Conectividad (Semana 2)
  1. Programación del firmware con FreeRTOS multitarea
  2. Implementación de protocolos de comunicación redundantes
  3. Configuración de base de datos local y remota
  4. Desarrollo de algoritmos de validación de datos
  5. Creación de sistema de alertas automáticas
Fase 3: Dashboard y Análisis (Semana 3)
  1. Desarrollo de interfaz web responsive
  2. Implementación de gráficos interactivos en tiempo real
  3. Integración con servicios meteorológicos externos
  4. Creación de reportes automatizados
  5. Implementación de machine learning básico para predicciones
Fase 4: Validación y Despliegue (Semana 4)
  1. Instalación en ubicación definitiva con certificación
  2. Comparación con estación meteorológica oficial
  3. Optimización de algoritmos según condiciones locales
  4. Documentación técnica completa
  5. Entrenamiento de usuarios y mantenimiento preventivo

Troubleshooting y Solución de Problemas

Problemas Comunes de Sensores

Lecturas Erróneas de Temperatura

Síntomas: Temperatura significativamente diferente a referencias locales, fluctuaciones bruscas.

Causas posibles:

  • Exposición directa al sol o radiación térmica
  • Calentamiento interno por circuitos de potencia
  • Falta de ventilación adecuada en caja protectora
  • Sensor defectuoso o mal calibrado

Soluciones:

  • Instalar escudo de radiación (radiation shield) profesional
  • Separar físicamente sensores de circuitos de potencia
  • Implementar ventilación forzada o pasiva
  • Calibrar con termómetro de precisión certificado
  • Aplicar compensación por algoritmo si es necesario
Problemas de Conectividad IoT

Síntomas: Desconexiones frecuentes, pérdida de datos, timeouts en comunicación.

Soluciones implementadas:

  • Buffer local: Almacenar datos en SD cuando no hay conexión
  • Reconexión automática: Algoritmo exponential backoff para reintentos
  • Watchdog timer: Reinicio automático en caso de colgarse
  • Múltiples APN: Configuración de redes Wi-Fi de respaldo
  • Compresión de datos: Reducir payload para conexiones lentas
Gestión de Alimentación

Desafíos: Operación 24/7 con energía solar y backup de batería.

Estrategias de optimización:

  • Deep Sleep Mode: Reducir consumo entre lecturas
  • Regulación dinámica: Ajustar frecuencia de CPU según carga de trabajo
  • Sensores low-power: Configurar modos de bajo consumo
  • Carga inteligente: MPPT para máxima eficiencia solar
  • Monitoreo de batería: Alertas preventivas de bajo voltaje

Criterios de Evaluación

Precisión de Mediciones (30%)

  • Temperatura: Error < ±0.5°C comparado con referencia
  • Humedad: Error < ±3% RH en rango 20-80%
  • Presión: Error < ±0.2 hPa a nivel del mar
  • Viento: Error < 5% en velocidad, ±5° en dirección
  • Precipitación: Error < ±0.1mm por evento

Conectividad IoT (25%)

  • Disponibilidad: > 99% tiempo conectado en 24h
  • Latencia: < 10 segundos para datos críticos
  • Integridad: 0% pérdida de datos en condiciones normales
  • Redundancia: Múltiples protocolos funcionando

Calidad del Software (25%)

  • Código limpio: Comentarios, funciones modulares
  • Manejo de errores: Try-catch apropiados
  • Optimización: Uso eficiente de memoria y CPU
  • Configurabilidad: Parámetros ajustables

Dashboard y UX (20%)

  • Interfaz intuitiva: Usabilidad sin entrenamiento
  • Responsividad: Funciona en móviles y desktop
  • Visualización: Gráficos claros y informativos
  • Performance: Carga rápida, actualizaciones suaves
Criterios de Excelencia
🥉 Nivel Básico (70-79%)
  • Sensores básicos funcionando
  • Conectividad Wi-Fi estable
  • Dashboard simple operativo
  • Documentación básica
🥈 Nivel Intermedio (80-89%)
  • Sensores calibrados con precisión
  • Múltiples protocolos IoT
  • Dashboard con gráficos avanzados
  • Sistema de alertas funcionando
  • Logging local implementado
🥇 Nivel Avanzado (90-100%)
  • Precisión meteorológica profesional
  • Sistema totalmente redundante
  • AI/ML para predicciones
  • Integración con APIs externas
  • Instalación campo completa
  • Documentación técnica detallada

Referencias y Recursos Adicionales

Estándares Meteorológicos Internacionales
Documentación Técnica ESP32
Plataformas IoT y APIs Meteorológicas
Herramientas de Desarrollo
Literatura Científica
  • "Automatic Weather Stations: A Guide for the User" - WMO Technical Report
  • "IoT-Based Weather Monitoring System" - IEEE Conference Papers
  • "Precision Agriculture with IoT Sensors" - Journal of Agricultural Engineering
  • "Environmental Sensing with Low-Cost Hardware" - Sensors and Actuators Journal