Monitorización Ambiental de Málaga con ESP32 y Sensor BME680 Cómo Crear una Web de Datos en Tiempo Real

Monitorización Ambiental de Málaga con ESP32 y Sensor BME680: Cómo Crear una Web de Datos en Tiempo Real

Spread the love

El objetivo de este tutorial, Monitorización Ambiental Málaga ESP32 BME680 es: Informar y guiar a entusiastas de la tecnología, desarrolladores y aficionados a IoT sobre cómo crear una web interactiva para monitorizar datos ambientales en Málaga utilizando un ESP32 con el sensor BME680, desde el hardware hasta la presentación en línea.

Monitorización Ambiental de Málaga con ESP32 y Sensor BME680 Cómo Crear una Web de Datos en Tiempo Real
Sensor BME680

Monitorización Ambiental Málaga ESP32 BME680: Cómo Crear una Web de Datos en Tiempo Real

En un mundo cada vez más conectado, la monitorización ambiental se ha convertido en una herramienta clave para entender nuestro entorno. En este artículo, te mostramos cómo construir una web de monitorización ambiental en Málaga usando un ESP32 y el sensor BME680, capaz de medir temperatura, humedad, voltaje y presión atmosférica en tiempo real. Desde el hardware hasta la visualización en una página web moderna, te explicamos cada paso del proceso.


¿Qué es este proyecto?

El proyecto «Monitorización Ambiental de Málaga – ESP32 con Sensor BME680» utiliza un microcontrolador ESP32 conectado a un sensor BME680 para recopilar datos ambientales en Málaga. Estos datos se envían a un servidor, se almacenan en una base de datos y se presentan en una web interactiva con gauges y gráficos actualizados en tiempo real. Es ideal para aficionados a IoT, estudiantes de programación o cualquier persona interesada en el clima local.


Materiales necesarios

  1. Hardware:
    • ESP32: Un microcontrolador potente y económico con WiFi integrado.
    • Sensor BME680: Mide temperatura, humedad, presión atmosférica y calidad del aire.
    • Cables y fuente de alimentación (por ejemplo, batería o USB).
  2. Software y servidor:
    • Arduino IDE: Para programar el ESP32.
    • Node.js: Para crear un servidor que reciba datos del ESP32.
    • MariaDB: Base de datos para almacenar los datos.
    • Nginx: Servidor web para alojar la página.
    • PHP: Para procesar y servir los datos.
    • Chart.js y Canvas Gauges: Librerías para gráficos y gauges.
  3. Infraestructura:
    • Un servidor local o en la nube (por ejemplo, una Raspberry Pi o VPS).
    • Dominio (opcional, como vesko.duckdns.org).

Pasos para crear la web

1. Configurar el hardware

  • Conecta el sensor BME680 al ESP32 usando los pines I2C (SDA y SCL).
  • Programa el ESP32 con Arduino IDE para leer los datos del BME680 (temperatura, humedad, presión, voltaje de la batería) y enviarlos por WiFi a un servidor mediante HTTP POST.
  • Ejemplo de código básico:
#include <Wire.h>
#include <Adafruit_BME680.h>
#include <WiFi.h>
#include <HTTPClient.h>

Adafruit_BME680 bme;
const char* ssid = "tu_wifi";
const char* password = "tu_contraseña";
const char* server = "http://192.168.1.100:3001/data";

void setup() {
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) delay(500);
    bme.begin();
}

void loop() {
    float temp = bme.readTemperature();
    float hum = bme.readHumidity();
    float pres = bme.readPressure() / 100.0;
    HTTPClient http;
    http.begin(server);
    http.addHeader("Content-Type", "application/json");
    String json = "{\"temperatura\":" + String(temp) + ",\"humedad\":" + String(hum) + ",\"presion\":" + String(pres) + "}";
    http.POST(json);
    http.end();
    delay(5000);
}

2. Configurar el servidor backend

  • Usa Node.js para crear un servidor que reciba los datos del ESP32 y los guarde en una base de datos MariaDB.
  • Ejemplo de server.js
const express = require('express');
const mysql = require('mysql2');
const app = express();
app.use(express.json());

const db = mysql.createConnection({
    host: 'localhost',
    user: 'tu_usuario',
    password: 'tu_contraseña',
    database: 'esp32_data'
});

app.post('/data', (req, res) => {
    const { temperatura, humedad, presion } = req.body;
    db.query('INSERT INTO sensor_data (temperatura, humedad, presion) VALUES (?, ?, ?)', 
        [temperatura, humedad, presion], 
        (err) => {
            if (err) console.error(err);
            res.sendStatus(200);
        });
});

app.listen(3001, () => console.log('Server running on port 3001'));

3. Diseñar la base de datos

Monitorización Ambiental de Málaga con ESP32 y Sensor BME680 Cómo Crear una Web de Datos en Tiempo Real

Crea dos tablas en MariaDB:

sensor_data:Almacena datos brutos.

CREATE TABLE sensor_data (
    id INT AUTO_INCREMENT PRIMARY KEY,
    temperatura FLOAT,
    humedad FLOAT,
    voltaje FLOAT,
    presion FLOAT,
    fecha TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

sensor_media: Almacena promedios diarios, semanales, mensuales y anuales.

CREATE TABLE sensor_media (
    id INT AUTO_INCREMENT PRIMARY KEY,
    fecha DATE,
    periodo ENUM('diario', 'semanal', 'mensual', 'anual'),
    temperatura FLOAT,
    humedad FLOAT,
    voltaje FLOAT,
    presion FLOAT,
    UNIQUE KEY (fecha, periodo)
);

4. Procesar datos con PHP

Crea data.php para calcular medias y servir datos:

<?php
require_once 'conexion.php';
header('Content-Type: application/json');

function calcularMedias($conn) {
    $conn->query("INSERT IGNORE INTO sensor_media (fecha, periodo, temperatura, humedad, voltaje, presion)
                  SELECT CURDATE(), 'diario', AVG(temperatura), AVG(humedad), AVG(voltaje), AVG(presion)
                  FROM sensor_data WHERE DATE(fecha) = CURDATE() AND presion > 900 AND presion < 1100");
    // Similar para semanal, mensual, anual
}

if (isset($_GET['accion']) && $_GET['accion'] === 'obtener_medias') {
    calcularMedias($conn);
    $result = $conn->query("SELECT * FROM sensor_media ORDER BY fecha DESC LIMIT 30");
    $medias = [];
    while ($row = $result->fetch_assoc()) $medias[] = $row;
    echo json_encode($medias);
}
?>

5. Diseñar la web frontend

Usa HTML, CSS y JavaScript con Chart.js y Canvas Gauges para mostrar gauges y gráficos.

Ejemplo en index.php:

<canvas id="gauge_temp"></canvas>
<canvas id="grafico"></canvas>
<script src="js/canvas-gauges.min.js"></script>
<script src="js/chart.min.js"></script>
<script>
    const gaugeTemp = new RadialGauge({ renderTo: 'gauge_temp', minValue: -10, maxValue: 50 }).draw();
    fetch('/esp32_server/data.php?accion=obtener_medias')
        .then(response => response.json())
        .then(data => gaugeTemp.value = data[0].temperatura);
</script>

6. Configurar el servidor web con Nginx

Configura Nginx para servir la web desde /var/www/html:

server {
    server_name mi.dominio.org;
    root /var/www/html;
    location /html/ {
        try_files $uri $uri/ /esp32_server/index.php?$args;
    }
    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        include fastcgi_params;
    }
}

7. Desplegar y probar

Sube los archivos, reinicia Nginx (sudo systemctl restart nginx), y accede a https://mi.domino.org/.

Verifica que los datos se muestren en tiempo real.


Desafíos comunes

  • Datos inválidos: Filtra valores como presión = 0 en las consultas SQL.
  • Errores 404: Asegúrate de que las rutas de Nginx coincidan con la estructura de directorios.
  • Actualización en tiempo real: Usa setInterval en JavaScript para refrescar los datos cada pocos segundos.

Conclusión

Crear una web para la Monitorización Ambiental de Málaga con ESP32 y Sensor BME680 combina hardware, programación y diseño web. Con este enfoque, puedes monitorear el clima de Málaga en tiempo real y compartirlo con el mundo. ¿Listo para empezar tu propio proyecto IoT? ¡Déjanos tus preguntas en los comentarios!

Retoques finales del Monitorización Ambiental de Málaga con ESP32 y Sensor BME680:

Código ESP32:

#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME680.h>

#define SDA_PIN 13
#define SCL_PIN 14
#define BATTERY_PIN 34
#define SEALEVELPRESSURE_HPA (1013.25)

// Configuración de WiFi
const char* ssid = "TUWIFI";
const char* password = "PASSWIFI";
const char* serverName = "http://192.168.1.100:3001/api/datos";

Adafruit_BME680 bme;
LiquidCrystal_I2C lcd(0x27, 16, 2);

void conectarWiFi() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Conectando WiFi");
  Serial.print("Conectando a WiFi");
  WiFi.begin(ssid, password);

  int intentos = 0;
  while (WiFi.status() != WL_CONNECTED && intentos < 20) {
    delay(500);
    Serial.print(".");
    lcd.setCursor(intentos % 16, 1);
    lcd.print(".");
    intentos++;
  }

  lcd.clear();
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("\n✅ Conectado a WiFi");
    lcd.setCursor(0, 0);
    lcd.print("WiFi Conectado");
    delay(2000);
  } else {
    Serial.println("\n❌ No se pudo conectar a WiFi");
    lcd.setCursor(0, 0);
    lcd.print("Error WiFi");
    delay(2000);
  }
}

void setup() {
  Serial.begin(115200);
  Wire.begin(SDA_PIN, SCL_PIN);
  pinMode(BATTERY_PIN, INPUT);

  lcd.init();
  lcd.backlight();
  lcd.setCursor(0, 0);
  lcd.print("Iniciando...");

  if (!bme.begin(0x76)) {
    Serial.println("Error: No se encontró el BME680");
    lcd.setCursor(0, 1);
    lcd.print("Error BME680");
    while (1);
  }

  // Configurar parámetros del BME680
  bme.setTemperatureOversampling(BME680_OS_8X);
  bme.setHumidityOversampling(BME680_OS_2X);
  bme.setPressureOversampling(BME680_OS_4X);

  conectarWiFi();
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("🔄 Intentando reconectar...");
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Reconectando...");
    conectarWiFi();
  }

  if (!bme.performReading()) {
    Serial.println("Error al leer el BME680");
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Error BME680");
    delay(2000);
    return;
  }

  float temp = bme.temperature;
  float hum = bme.humidity;
  float pressure = bme.pressure / 100.0;
  int analogValue = analogRead(BATTERY_PIN);
  float voltage = (analogValue / 4095.0) * 3.3 * 2.79;

  // Verificar si los valores son válidos
  if (isnan(temp) || isnan(hum) || isnan(pressure) || isnan(voltage)) {
    Serial.println("❌ Error: Lecturas inválidas del BME680 o batería");
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Error Lectura");
    delay(2000);
    return;
  }

  // Mostrar en Serial
  Serial.printf("🌡️ Temp: %.2f °C | 💧 Hum: %.2f %%\n", temp, hum);
  Serial.printf("⬇️ Presión: %.2f hPa | ⚡ Voltaje: %.2f V\n", pressure, voltage);

  // Mostrar en LCD
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("T:" + String(temp, 1) + "C");
  lcd.setCursor(9, 0);
  lcd.print("H:" + String(hum, 1) + "%");
  lcd.setCursor(0, 1);
  lcd.print("P:" + String(pressure, 1) + "hPa");
  lcd.setCursor(10, 1);
  lcd.print("V:" + String(voltage, 1) + "V");

  // Enviar datos al servidor
  if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http;
    http.begin(serverName);
    http.addHeader("Content-Type", "application/json");

    String jsonData = "{\"temperatura\":" + String(temp) +
                      ",\"humedad\":" + String(hum) +
                      ",\"voltaje\":" + String(voltage) +
                      ",\"presion\":" + String(pressure) + "}";

    Serial.println("📡 Enviando datos: " + jsonData);
    int httpResponseCode = http.POST(jsonData);

    if (httpResponseCode > 0) {
      String response = http.getString();
      Serial.println("✅ Respuesta: " + response);
    } else {
      Serial.println("❌ Error en HTTP: " + String(httpResponseCode));
    }
    http.end();
  } else {
    Serial.println("⚠️ WiFi desconectado");
  }

  delay(5000);
}

index.php:

<?php
require_once 'conexion.php';

$periodo = isset($_GET['periodo']) ? $_GET['periodo'] : 'dia';
switch ($periodo) {
    case 'semana': $limit = "WHERE fecha >= DATE_SUB(NOW(), INTERVAL 7 DAY)"; break;
    case 'mes': $limit = "WHERE fecha >= DATE_SUB(NOW(), INTERVAL 1 MONTH)"; break;
    case 'año': $limit = "WHERE fecha >= DATE_SUB(NOW(), INTERVAL 1 YEAR)"; break;
    case 'dia': default: $limit = "WHERE fecha >= DATE_SUB(NOW(), INTERVAL 1 DAY)"; break;
}

$sql = "SELECT * FROM sensor_data $limit ORDER BY fecha DESC LIMIT 100";
$result = $conn->query($sql);

$data = [];
if ($result === false) {
    die("Error en la consulta: " . $conn->error);
} elseif ($result->num_rows > 0) {
    while ($row = $result->fetch_assoc()) {
        $row['temperatura'] = floatval($row['temperatura']);
        $row['humedad'] = floatval($row['humedad']);
        $row['voltaje'] = floatval($row['voltaje']);
        $row['presion'] = floatval($row['presion']); // Añadimos presión
        $data[] = $row;
    }
}
$conn->close();

$jsonData = json_encode($data);
?>

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="Monitorización de datos ambientales en Málaga con sensores ESP32. Temperatura, humedad, voltaje y presión atmosférica en tiempo real, con medias diarias, semanales, mensuales y anuales presentadas en un diseño inspirado en la ciudad.">
    <title>Datos del Sensor ESP32</title>
    <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
    <style>
        :root {
            --color-blue-dark: #395467;
            --color-gray: #212629;
            --color-gray-dark: #26323a;
            --color-white: #fff;
            --color-green: #25cd6b;
            --color-green-light: #a7db29;
            --color-yellow: #fbe500;
            --color-red: #e23131;
            --color-orange: #ed811c;
            --unit-spacing: 20px;
        }
        * { box-sizing: border-box; }
        body {
            font-family: 'Poppins', sans-serif;
            background-image: url('malaga.jpg');
            background-size: cover;
            background-position: center;
            background-repeat: no-repeat;
            color: #333;
            margin: 0;
            padding: 20px;
            padding-bottom: 90px;
            display: flex;
            flex-direction: column;
            align-items: center;
            min-height: 100vh;
        }
        h1 {
            color: #007BFF;
            margin-bottom: 20px;
            font-weight: bold;
            background-color: rgba(255, 255, 255, 0.7);
            padding: 10px 20px;
            border-radius: 10px;
            text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
        }
        .dashboard {
            display: flex;
            justify-content: center;
            gap: 30px;
            flex-wrap: wrap;
            margin-bottom: 20px;
        }
        .gauge-wrapper {
            text-align: center;
            transition: transform 0.3s ease;
        }
        .gauge-wrapper:hover { transform: scale(1.05); }
        .gauge-container {
            width: 220px;
            height: 220px;
            background-color: #fff;
            border-radius: 50%;
            box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
            padding: 10px;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        canvas.gauge { width: 200px !important; height: 200px !important; }
        .gauge-label {
            margin-top: 10px;
            font-size: 18px;
            font-weight: bold;
            background-color: rgba(255, 255, 255, 0.7);
            padding: 5px 10px;
            border-radius: 5px;
            text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
            color: #333;
        }
        .controls { margin: 20px 0; }
        .controls select {
            padding: 8px;
            font-size: 16px;
            border-radius: 5px;
            background-color: #007BFF;
            color: white;
            border: none;
            cursor: pointer;
            transition: background-color 0.3s ease;
        }
        .controls select:hover { background-color: #0056b3; }
        #grafico {
            max-width: 800px;
            width: 100%;
            background-color: rgba(255, 255, 255, 0.8);
            border-radius: 10px;
            box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
            padding: 20px;
        }
        .medias-container {
            max-width: 1200px;
            width: 100%;
            display: flex;
            flex-wrap: wrap;
            justify-content: center;
            gap: 20px;
            padding: 20px;
        }
        .media-item:nth-child(1) span { color: #007BFF; } /* Diario */
        .media-item:nth-child(2) span { color: #28A745; } /* Semanal */
        .media-item:nth-child(3) span { color: #FD7E14; } /* Mensual */
        .media-item:nth-child(4) span { color: #6F42C1; } /* Anual */
        .mes-card {
            background-color: rgba(255, 255, 255, 0.8);
            border-radius: 10px;
            box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
            padding: 15px;
            width: 300px;
            text-align: center;
            transition: transform 0.3s ease;
            animation: fadeIn 0.5s ease-in;
        }
        @keyframes fadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }
        .mes-card:hover { transform: scale(1.05); }
        .mes-header {
            margin-bottom: 10px;
        }
        .mes-header img {
            width: 200px;
            height: 200px;
            border-radius: 10px;
            object-fit: cover;
        }
        .mes-header h3 {
            margin: 10px 0 0;
            font-size: 20px;
            font-weight: 600;
            color: #333;
            text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
        }
        .media-item {
            margin: 10px 0;
            font-size: 14px;
            color: #555;
        }
        .media-item span {
            font-weight: bold;
        }
        @media (max-width: 768px) {
            .mes-card {
                width: 100%;
                max-width: 350px;
            }
        }
        .footer {
            position: fixed;
            bottom: 0;
            width: 100%;
            background-color: rgba(255, 255, 255, 0.8);
            padding: 15px;
            text-align: center;
            font-size: 14px;
            color: #333;
            text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
            box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
        }
        .footer a {
            text-decoration: none;
            color: #007BFF;
            transition: color 0.3s ease;
        }
        .footer a:hover { color: #0056b3; }
        .footer i { margin: 0 5px; color: #007BFF; }
    </style>
</head>
<body>
    <h1>Monitorización Ambiental de Málaga - ESP32 con Sensor BME680</h1>
    <div class="dashboard">
        <div class="gauge-wrapper">
            <canvas id="gauge_temp" class="gauge-container"></canvas>
            <div id="temp_label" class="gauge-label">Temperatura: -- °C</div>
        </div>
        <div class="gauge-wrapper">
            <canvas id="gauge_humidity" class="gauge-container"></canvas>
            <div id="humidity_label" class="gauge-label">Humedad: -- %</div>
        </div>
        <div class="gauge-wrapper">
            <canvas id="gauge_voltage" class="gauge-container"></canvas>
            <div id="voltage_label" class="gauge-label">Voltaje: -- V</div>
        </div>
        <div class="gauge-wrapper">
            <canvas id="gauge_pressure" class="gauge-container"></canvas>
            <div id="pressure_label" class="gauge-label">Presión: -- hPa</div>
        </div>
    </div>

    <canvas id="grafico"></canvas>
    <div id="medias-container" class="medias-container">
        <!-- Los datos se insertarán dinámicamente aquí -->
    </div>

    <footer class="footer">
        <p>
            <i class="fas fa-thermometer-half"></i> Datos Ambientales - Málaga 
            | <a href="mailto:veselin@veselin.es"><i class="fas fa-envelope"></i> Contacto</a> 
            | Desarrollado por Veselin Petrov 
            | <i class="fas fa-calendar-alt"></i> 2025
        </p>
    </footer>

    <script src="/esp32_server/js/canvas-gauges.min.js"></script>
    <script src="/esp32_server/js/chart.min.js"></script>
    <script>
        const gaugeTemp = new RadialGauge({
            renderTo: 'gauge_temp',
            width: 330, height: 330, units: '°C', title: 'Temperatura',
            minValue: -10, maxValue: 50,
            majorTicks: [-10, 0, 10, 20, 30, 40, 50], minorTicks: 5, strokeTicks: true,
            highlights: [
                { from: -10, to: 5, color: '#0000FF' }, { from: 5, to: 15, color: '#1E90FF' },
                { from: 15, to: 25, color: '#90EE90' }, { from: 25, to: 35, color: '#FDA400' },
                { from: 35, to: 50, color: '#FF0000' }
            ],
            value: 0, colorPlate: '#fff', colorNeedle: '#333', colorNeedleEnd: '#333',
            needleWidth: 4, animationDuration: 500, animationRule: 'elastic'
        }).draw();

        const gaugeHumidity = new RadialGauge({
            renderTo: 'gauge_humidity',
            width: 330, height: 330, units: '%', title: 'Humedad',
            minValue: 0, maxValue: 100,
            majorTicks: [0, 20, 40, 60, 80, 100], minorTicks: 5, strokeTicks: true,
            highlights: [
                { from: 0, to: 30, color: '#FF0000' }, { from: 30, to: 50, color: '#E0FFFF' },
                { from: 50, to: 75, color: '#87CEEB' }, { from: 75, to: 100, color: '#00008B' }
            ],
            value: 0, colorPlate: '#fff', colorNeedle: '#333', colorNeedleEnd: '#333',
            needleWidth: 4, animationDuration: 500, animationRule: 'elastic'
        }).draw();

        const gaugeVoltage = new RadialGauge({
            renderTo: 'gauge_voltage',
            width: 330, height: 330, units: 'V', title: 'Voltaje',
            minValue: 0, maxValue: 12,
            majorTicks: [0, 3, 6, 9, 12], minorTicks: 5, strokeTicks: true,
            highlights: [
                { from: 0, to: 6, color: '#FF0000' }, { from: 6, to: 8, color: '#FFFF00' },
                { from: 8, to: 10, color: '#00FF00' }, { from: 10, to: 12, color: '#32CD32' }
            ],
            value: 0, colorPlate: '#fff', colorNeedle: '#333', colorNeedleEnd: '#333',
            needleWidth: 4, animationDuration: 500, animationRule: 'elastic'
        }).draw();

        const gaugePressure = new RadialGauge({
            renderTo: 'gauge_pressure',
            width: 330, height: 330, units: 'hPa', title: 'Presión',
            minValue: 900, maxValue: 1100,
            majorTicks: [900, 950, 1000, 1050, 1100], minorTicks: 5, strokeTicks: true,
            highlights: [
                { from: 900, to: 980, color: '#FF4500' }, // Baja presión (tormentas)
                { from: 980, to: 1010, color: '#FFD700' }, // Normal baja
                { from: 1010, to: 1030, color: '#32CD32' }, // Normal alta
                { from: 1030, to: 1100, color: '#1E90FF' } // Alta presión (claro)
            ],
            value: 0, colorPlate: '#fff', colorNeedle: '#333', colorNeedleEnd: '#333',
            needleWidth: 4, animationDuration: 500, animationRule: 'elastic'
        }).draw();

        const ctx = document.getElementById('grafico').getContext('2d');
        const chart = new Chart(ctx, {
            type: 'line',
            data: {
                labels: [],
                datasets: [
                    { label: 'Temperatura (°C)', borderColor: 'red', data: [] },
                    { label: 'Humedad (%)', borderColor: 'blue', data: [] },
                    { label: 'Voltaje (V)', borderColor: 'green', data: [] },
                    { label: 'Presión (hPa)', borderColor: 'purple', data: [] }
                ]
            },
            options: {
                responsive: true,
                scales: {
                    x: { display: true, title: { display: true, text: 'Tiempo' } },
                    y: { display: true, title: { display: true, text: 'Valores' } }
                }
            }
        });

        const initialData = <?php echo $jsonData ?: '[]'; ?>;
        const tempLabel = document.getElementById('temp_label');
        const humidityLabel = document.getElementById('humidity_label');
        const voltageLabel = document.getElementById('voltage_label');
        const pressureLabel = document.getElementById('pressure_label');

        function actualizarDatos(data) {
            if (!data || data.length === 0) {
                console.error('No hay datos disponibles');
                return;
            }
            const ultimoDato = data[0];
            const temp = Number(ultimoDato.temperatura) || 0;
            const hum = Number(ultimoDato.humedad) || 0;
            const volt = Number(ultimoDato.voltaje) || 0;
            const press = Number(ultimoDato.presion) || 0;

            gaugeTemp.value = temp;
            tempLabel.textContent = `Temperatura: ${temp.toFixed(1)} °C`;
            gaugeHumidity.value = hum;
            humidityLabel.textContent = `Humedad: ${hum.toFixed(1)} %`;
            gaugeVoltage.value = volt;
            voltageLabel.textContent = `Voltaje: ${volt.toFixed(1)} V`;
            gaugePressure.value = press;
            pressureLabel.textContent = `Presión: ${press.toFixed(1)} hPa`;

            chart.data.labels = data.map(d => new Date(d.fecha).toLocaleTimeString());
            chart.data.datasets[0].data = data.map(d => Number(d.temperatura));
            chart.data.datasets[1].data = data.map(d => Number(d.humedad));
            chart.data.datasets[2].data = data.map(d => Number(d.voltaje));
            chart.data.datasets[3].data = data.map(d => Number(d.presion));
            chart.update();
        }

        if (initialData.length > 0) {
            actualizarDatos(initialData);
        }

        setInterval(() => {
            fetch('data.php?periodo=' + '<?php echo $periodo; ?>')
                .then(response => {
                    if (!response.ok) {
                        throw new Error('Error en la red');
                    }
                    return response.json();
                })
                .then(data => {
                    actualizarDatos(data);
                })
                .catch(error => console.error('Error al obtener los datos:', error));
        }, 5000);

        function cargarMedias() {
            fetch('data.php?accion=obtener_medias')
                .then(response => {
                    if (!response.ok) {
                        throw new Error('Error en la red');
                    }
                    return response.json();
                })
                .then(data => {
                    const container = document.getElementById('medias-container');
                    container.innerHTML = '';

                    if (data.length === 0) {
                        container.innerHTML = '<p style="text-align: center; color: #555;">Aún no hay datos recopilados</p>';
                        return;
                    }

                    const meses = {};
                    data.forEach(m => {
                        const fecha = new Date(m.fecha);
                        const mesNum = fecha.getMonth() + 1;
                        const mesNombre = fecha.toLocaleString('es-ES', { month: 'long' }).toUpperCase();
                        if (!meses[mesNum]) {
                            meses[mesNum] = { nombre: mesNombre, medias: [] };
                        }
                        meses[mesNum].medias.push(m);
                    });

                    Object.keys(meses).forEach(mesNum => {
                        const mes = meses[mesNum];
                        let html = `
                            <div class="mes-card">
                                <div class="mes-header">
                                    <img src="/esp32_server/images/mes_${mesNum}.webp" alt="${mes.nombre}">
                                    <h2>${mes.nombre}</h2>
                                </div>
                        `;

                        const diaria = mes.medias.find(m => m.periodo === 'diario') || { temperatura: '--', humedad: '--', voltaje: '--', presion: '--' };
                        const semanal = mes.medias.find(m => m.periodo === 'semanal') || { temperatura: '--', humedad: '--', voltaje: '--', presion: '--' };
                        const mensual = mes.medias.find(m => m.periodo === 'mensual') || { temperatura: '--', humedad: '--', voltaje: '--', presion: '--' };
                        const anual = mes.medias.find(m => m.periodo === 'anual') || { temperatura: '--', humedad: '--', voltaje: '--', presion: '--' };

                        html += `
                            <div class="media-item">Diaria: <span>
                            ${diaria.temperatura !== '--' ? Number(diaria.temperatura).toFixed(1) : '--'}°C, 
                            ${diaria.humedad !== '--' ? Number(diaria.humedad).toFixed(1) : '--'}%, 
                            ${diaria.voltaje !== '--' ? Number(diaria.voltaje).toFixed(1) : '--'}V, 
                            ${diaria.presion !== '--' ? Number(diaria.presion).toFixed(1) : '--'}hPa</span>
                            </div>
                            <div class="media-item">Semanal: <span>
                            ${semanal.temperatura !== '--' ? Number(semanal.temperatura).toFixed(1) : '--'}°C, 
                            ${semanal.humedad !== '--' ? Number(semanal.humedad).toFixed(1) : '--'}%, 
                            ${semanal.voltaje !== '--' ? Number(semanal.voltaje).toFixed(1) : '--'}V, 
                            ${semanal.presion !== '--' ? Number(semanal.presion).toFixed(1) : '--'}hPa</span>
                            </div>
                            <div class="media-item">Mensual: <span>
                            ${mensual.temperatura !== '--' ? Number(mensual.temperatura).toFixed(1) : '--'}°C, 
                            ${mensual.humedad !== '--' ? Number(mensual.humedad).toFixed(1) : '--'}%, 
                            ${mensual.voltaje !== '--' ? Number(mensual.voltaje).toFixed(1) : '--'}V, 
                            ${mensual.presion !== '--' ? Number(mensual.presion).toFixed(1) : '--'}hPa</span>
                            </div>
                            <div class="media-item">Anual: <span>
                            ${anual.temperatura !== '--' ? Number(anual.temperatura).toFixed(1) : '--'}°C, 
                            ${anual.humedad !== '--' ? Number(anual.humedad).toFixed(1) : '--'}%, 
                            ${anual.voltaje !== '--' ? Number(anual.voltaje).toFixed(1) : '--'}V, 
                            ${anual.presion !== '--' ? Number(anual.presion).toFixed(1) : '--'}hPa</span>
                            </div>
                        `;

                        html += `</div>`;
                        container.innerHTML += html;
                    });
                })
                .catch(error => console.error('Error cargando medias:', error));
        }

        cargarMedias();
        setInterval(cargarMedias, 60000);
    </script>
</body>
</html>

data.php:

<?php
header('Content-Type: application/json');
ini_set('display_errors', 1);
error_reporting(E_ALL);
require_once 'conexion.php';

if ($conn->connect_error) {
    die(json_encode(['error' => 'Error de conexión: ' . $conn->connect_error]));
}

$periodo = isset($_GET['periodo']) ? $_GET['periodo'] : 'dia';

switch ($periodo) {
    case 'semana':
        $limit = "WHERE fecha >= DATE_SUB(NOW(), INTERVAL 7 DAY)";
        break;
    case 'mes':
        $limit = "WHERE fecha >= DATE_SUB(NOW(), INTERVAL 1 MONTH)";
        break;
    case 'año':
        $limit = "WHERE fecha >= DATE_SUB(NOW(), INTERVAL 1 YEAR)";
        break;
    case 'dia':
    default:
        $limit = "WHERE fecha >= DATE_SUB(NOW(), INTERVAL 1 DAY)";
        break;
}

// Manejo predeterminado: datos de sensor_data
$sql = "SELECT * FROM sensor_data $limit ORDER BY fecha DESC LIMIT 100";
$result = $conn->query($sql);

$data = [];
if ($result && $result->num_rows > 0) {
    while ($row = $result->fetch_assoc()) {
        $data[] = $row;
    }
}

// Función para calcular medias
function calcularMedias($conn) {
    $errores = [];

    // Media diaria (últimas 24 horas)
    $sql_diaria = "INSERT IGNORE INTO sensor_media (fecha, periodo, temperatura, humedad, voltaje, presion)
                   SELECT CURDATE(), 'diario',
                          AVG(temperatura), AVG(humedad), AVG(voltaje), AVG(presion)
                   FROM sensor_data
                   WHERE fecha >= NOW() - INTERVAL 1 DAY
                   AND presion > 900 AND presion < 1100"; // Filtra presiones válidas
    if (!$conn->query($sql_diaria)) {
        $errores[] = "Error al insertar media diaria: " . $conn->error;
    }

    // Media semanal (últimos 7 días)
    $sql_semanal = "INSERT IGNORE INTO sensor_media (fecha, periodo, temperatura, humedad, voltaje, presion)
                    SELECT CURDATE(), 'semanal',
                           AVG(temperatura), AVG(humedad), AVG(voltaje), AVG(presion)
                    FROM sensor_data
                    WHERE fecha >= NOW() - INTERVAL 7 DAY
                    AND presion > 900 AND presion < 1100";
    if (!$conn->query($sql_semanal)) {
        $errores[] = "Error al insertar media semanal: " . $conn->error;
    }

    // Media mensual (últimos 30 días)
    $sql_mensual = "INSERT IGNORE INTO sensor_media (fecha, periodo, temperatura, humedad, voltaje, presion)
                    SELECT CURDATE(), 'mensual',
                           AVG(temperatura), AVG(humedad), AVG(voltaje), AVG(presion)
                    FROM sensor_data
                    WHERE fecha >= NOW() - INTERVAL 30 DAY
                    AND presion > 900 AND presion < 1100";
    if (!$conn->query($sql_mensual)) {
        $errores[] = "Error al insertar media mensual: " . $conn->error;
    }

    // Media anual (últimos 365 días)
    $sql_anual = "INSERT IGNORE INTO sensor_media (fecha, periodo, temperatura, humedad, voltaje, presion)
                  SELECT CURDATE(), 'anual',
                         AVG(temperatura), AVG(humedad), AVG(voltaje), AVG(presion)
                  FROM sensor_data
                  WHERE fecha >= NOW() - INTERVAL 1 YEAR
                  AND presion > 900 AND presion < 1100";
    if (!$conn->query($sql_anual)) {
        $errores[] = "Error al insertar media anual: " . $conn->error;
    }

    if (!empty($errores)) {
        error_log("Errores al calcular medias: " . implode("; ", $errores));
        return ["mensaje" => "Errores al calcular medias", "errores" => $errores];
    }
    return ["mensaje" => "Medias calculadas e insertadas con éxito"];
}

// Si se ejecuta desde CLI, calcular medias directamente
if (php_sapi_name() === 'cli') {
    $resultado = calcularMedias($conn);
    echo json_encode($resultado);
    exit;
}

// Si es una solicitud HTTP
if (isset($_GET['accion'])) {
    if ($_GET['accion'] === 'obtener_datos') {
        echo json_encode($data);
    } elseif ($_GET['accion'] === 'calcular_medias') {
        $resultado = calcularMedias($conn);
        echo json_encode($resultado);
    } elseif ($_GET['accion'] === 'obtener_medias') {
        $resultado = calcularMedias($conn);
        if (isset($resultado['errores'])) {
            echo json_encode(['error' => $resultado['errores']]);
            exit;
        }

        $sql = "SELECT fecha, periodo, temperatura, humedad, voltaje, presion 
                FROM sensor_media 
                ORDER BY fecha DESC, FIELD(periodo, 'diario', 'semanal', 'mensual', 'anual') 
                LIMIT 30";
        $result = $conn->query($sql);

        $medias = [];
        if ($result && $result->num_rows > 0) {
            while ($row = $result->fetch_assoc()) {
                $medias[] = $row;
            }
        }
        echo json_encode($medias);
    }
} else {
    echo json_encode($data);
}

$conn->close();
?>

server.js:

const express = require('express');
const mysql = require('mysql');
const bodyParser = require('body-parser');
const cors = require('cors');
const app = express();
const port = 3001;

// Configuración de MySQL
const db = mysql.createConnection({
    host: 'localhost',
    user: 'ddbbuser',
    password: 'ddbbpass',
    database: 'esp32_data'
});

db.connect((err) => {
    if (err) {
        console.error('Error al conectar a MySQL:', err);
        throw err;
    }
    console.log('Conectado a la base de datos MySQL');
});

// Middleware
app.use(bodyParser.json());
app.use(cors());

// Ruta para recibir datos del ESP32
app.post('/api/datos', (req, res) => {
    const { temperatura, humedad, voltaje, presion } = req.body;
    console.log('Datos recibidos:', req.body); // Para depuración

    // Verificar que los valores no sean undefined o null
    if (!temperatura || !humedad || !voltaje || !presion) {
        console.error('Error: Faltan datos en la solicitud');
        return res.status(400).send('Error: Faltan datos en la solicitud');
    }

    const query = 'INSERT INTO sensor_data (temperatura, humedad, voltaje, presion) VALUES (?, ?, ?, ?)';
    db.query(query, [temperatura, humedad, voltaje, presion], (err, result) => {
        if (err) {
            console.error('Error al ejecutar la consulta:', err);
            res.status(500).send(`Error al guardar los datos: ${err.sqlMessage || err.message}`);
        } else {
            console.log('Datos guardados, ID:', result.insertId);
            res.status(200).send('Datos guardados correctamente');
        }
    });
});

// Ruta para obtener los últimos 100 datos
app.get('/api/datos', (req, res) => {
    const query = 'SELECT * FROM sensor_data ORDER BY fecha DESC LIMIT 100';
    db.query(query, (err, results) => {
        if (err) {
            console.error('Error al obtener datos:', err);
            res.status(500).send('Error al obtener los datos');
        } else {
            res.status(200).json(results);
        }
    });
});

// Iniciar el servidor
app.listen(port, () => {
    console.log(`Servidor corriendo en http://192.168.1.100:${port}`);
});

Deja un comentario