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
#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
<!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
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:
- Conectar sensores mediante protocolo I2C
- Configurar librerías y inicializar sensores
- Implementar rutina de calibración automática
- Validar precisión con instrumentos de referencia
- Crear sistema de compensación por altitud
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:
- Configurar interrupciones para pulsos de viento
- Implementar filtros digitales anti-rebote
- Calcular velocidad promedio y ráfagas máximas
- Procesar dirección con compensación magnética
- Acumular precipitación con persistencia
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:
- Configurar QoS apropiado para datos críticos
- Implementar buffer local para conexión intermitente
- Crear sistema de alertas automáticas
- Integrar con plataformas meteorológicas
- Establecer autenticación segura
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:
- Chart.js para visualizaciones interactivas
- WebSocket para actualizaciones en tiempo real
- Progressive Web App (PWA) para móviles
- Machine Learning para predicciones básicas
- 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)
- Montaje de componentes en caja estanca con separación térmica
- Instalación de sensores con protección contra radiación
- Calibración de sensores con estándares de referencia
- Configuración de sistema de alimentación solar
- Pruebas de resistencia a condiciones ambientales
Fase 2: Software y Conectividad (Semana 2)
- Programación del firmware con FreeRTOS multitarea
- Implementación de protocolos de comunicación redundantes
- Configuración de base de datos local y remota
- Desarrollo de algoritmos de validación de datos
- Creación de sistema de alertas automáticas
Fase 3: Dashboard y Análisis (Semana 3)
- Desarrollo de interfaz web responsive
- Implementación de gráficos interactivos en tiempo real
- Integración con servicios meteorológicos externos
- Creación de reportes automatizados
- Implementación de machine learning básico para predicciones
Fase 4: Validación y Despliegue (Semana 4)
- Instalación en ubicación definitiva con certificación
- Comparación con estación meteorológica oficial
- Optimización de algoritmos según condiciones locales
- Documentación técnica completa
- 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
- World Meteorological Organization (WMO) - Estándares globales
- ISO 17714:2007 - Instrumentos meteorológicos
- NIST - Estándares de calibración para sensores
- SMN México - Servicio Meteorológico Nacional
Documentación Técnica ESP32
- ESP-IDF Documentation - Framework oficial
- Arduino Core for ESP32 - Librería Arduino
- ESP32 Datasheet - Especificaciones completas
Plataformas IoT y APIs Meteorológicas
- ThingsBoard - Plataforma IoT open source
- OpenWeatherMap API - Datos meteorológicos
- AWS IoT Core - Servicios de Amazon
- Azure IoT Hub - Plataforma Microsoft
- Google Cloud IoT - Servicios de Google
Herramientas de Desarrollo
- PlatformIO - IDE avanzado para IoT
- Arduino IDE - Entorno de desarrollo simple
- Chart.js - Librería de gráficos JavaScript
- MQTT.org - Protocolo de mensajería IoT
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