Este tutorial te muestra cómo construir una estación meteorológica con servidor web usando el ESP32 para mostrar las lecturas del sensor ambiental BME680: gas (calidad del aire), temperatura, humedad y presión. Las lecturas se actualizan automáticamente en el servidor web usando Server-Sent Events (SSE). El ESP32 se programará usando Arduino IDE.
Para construir el servidor web usaremos la libería ESP Async Web Server, que proporciona una forma sencilla de construir un servidor web asíncrono.
¿Qué es el sensor ambiental BME680?
El BME680 es un sensor ambiental que combina sensores de gas, temperatura, humedad y presión. El sensor de gas puede detectar una amplia gama de gases como compuestos orgánicos volátiles (VOC). Por esta razón, el BME680 se puede usar en el control de calidad del aire en interiores.
El BME680 contiene un sensor MOX (Metal-oxide) que detecta VOCs en el aire. Este sensor te da una idea cualitativa de la suma de VOCs/contaminantes en el aire circundante. Como señal cruda, el BME680 emite valores de resistencia. Estos valores cambian debido a variaciones en las concentraciones de VOC:
Mayor concentración de VOCs → menor resistencia. Menor concentración de VOCs → mayor resistencia.
Para más información sobre el BME680, consulta nuestra guía de introducción específica.
Componentes necesarios
Para completar este tutorial necesitas: un módulo sensor BME680, una placa ESP32, una protoboard y cables jumper.
Esquema de conexión: ESP32 con BME680
El BME680 puede comunicarse usando protocolos de comunicación I2C o SPI. En este tutorial, usaremos protocolo de comunicación I2C. Conecta el BME680 al ESP32 usando los pines I2C por defecto (GPIO 21 para SDA, GPIO 22 para SCL).
Preparar Arduino IDE
Vamos a programar la placa ESP32 usando Arduino IDE. Así que asegúrate de tener el complemento ESP32 instalado. También necesitas instalar las siguientes librerías: la libería Adafruit_BME680, la libería Adafruit_Sensor, la libería ESPAsyncWebServer de ESP32Async y la libería AsyncTCP de ESP32Async.
Puedes instalar las librerías a través del Gestor de Librerías de Arduino. Ve a Programa > Incluir Librería > Gestionar Librerías y busca los nombres de las librerías para instalarlas.
Código completo del servidor web con BME680
Abre tu Arduino IDE y copia el siguiente código. Para que funcione, necesitas insertar tus credenciales de red: SSID y contraseña:
/*
Rui Santos & Sara Santos - Random Nerd Tutorials
Detalles completos del proyecto en https://RandomNerdTutorials.com/esp32-bme680-sensor-arduino/
*/
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include "Adafruit_BME680.h"
#include <WiFi.h>
#include "ESPAsyncWebServer.h"
// Reemplaza con las credenciales de tu red
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
//Descomenta si usas SPI
/*#define BME_SCK 18
#define BME_MISO 19
#define BME_MOSI 23
#define BME_CS 5*/
Adafruit_BME680 bme; // I2C
//Adafruit_BME680 bme(BME_CS); // hardware SPI
//Adafruit_BME680 bme(BME_CS, BME_MOSI, BME_MISO, BME_SCK);
float temperature;
float humidity;
float pressure;
float gasResistance;
AsyncWebServer server(80);
AsyncEventSource events("/events");
unsigned long lastTime = 0;
unsigned long timerDelay = 30000; // temporizador de envío de lecturas
void getBME680Readings(){
// Indica al BME680 que comience la medición.
unsigned long endTime = bme.beginReading();
if (endTime == 0) {
Serial.println(F("Failed to begin reading :("));
return;
}
if (!bme.endReading()) {
Serial.println(F("Failed to complete reading :("));
return;
}
temperature = bme.temperature;
pressure = bme.pressure / 100.0;
humidity = bme.humidity;
gasResistance = bme.gas_resistance / 1000.0;
}
String processor(const String& var){
getBME680Readings();
//Serial.println(var);
if(var == "TEMPERATURE"){
return String(temperature);
}
else if(var == "HUMIDITY"){
return String(humidity);
}
else if(var == "PRESSURE"){
return String(pressure);
}
else if(var == "GAS"){
return String(gasResistance);
}
return "0";
}
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
<title>BME680 Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.2/css/all.css" integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
<link rel="icon" href="data:,">
<style>
html {font-family: Arial; display: inline-block; text-align: center;}
p { font-size: 1.2rem;}
body { margin: 0;}
.topnav { overflow: hidden; background-color: #4B1D3F; color: white; font-size: 1.7rem; }
.content { padding: 20px; }
.card { background-color: white; box-shadow: 2px 2px 12px 1px rgba(140,140,140,.5); }
.cards { max-width: 700px; margin: 0 auto; display: grid; grid-gap: 2rem; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
.reading { font-size: 2.8rem; }
.card.temperature { color: #0e7c7b; }
.card.humidity { color: #17bebb; }
.card.pressure { color: #3fca6b; }
.card.gas { color: #d62246; }
</style>
</head>
<body>
<div class="topnav">
<h3>BME680 WEB SERVER</h3>
</div>
<div class="content">
<div class="cards">
<div class="card temperature">
<h4><i class="fas fa-thermometer-half"></i> TEMPERATURE</h4><p><span class="reading"><span id="temp">%TEMPERATURE%</span> °C</span></p>
</div>
<div class="card humidity">
<h4><i class="fas fa-tint"></i> HUMIDITY</h4><p><span class="reading"><span id="hum">%HUMIDITY%</span> %</span></p>
</div>
<div class="card pressure">
<h4><i class="fas fa-angle-double-down"></i> PRESSURE</h4><p><span class="reading"><span id="pres">%PRESSURE%</span> hPa</span></p>
</div>
<div class="card gas">
<h4><i class="fas fa-wind"></i> GAS</h4><p><span class="reading"><span id="gas">%GAS%</span> KΩ</span></p>
</div>
</div>
</div>
<script>
if (!!window.EventSource) {
var source = new EventSource('/events');
source.addEventListener('open', function(e) {
console.log("Events Connected");
}, false);
source.addEventListener('error', function(e) {
if (e.target.readyState != EventSource.OPEN) {
console.log("Events Disconnected");
}
}, false);
source.addEventListener('message', function(e) {
console.log("message", e.data);
}, false);
source.addEventListener('temperature', function(e) {
console.log("temperature", e.data);
document.getElementById("temp").innerHTML = e.data;
}, false);
source.addEventListener('humidity', function(e) {
console.log("humidity", e.data);
document.getElementById("hum").innerHTML = e.data;
}, false);
source.addEventListener('pressure', function(e) {
console.log("pressure", e.data);
document.getElementById("pres").innerHTML = e.data;
}, false);
source.addEventListener('gas', function(e) {
console.log("gas", e.data);
document.getElementById("gas").innerHTML = e.data;
}, false);
}
</script>
</body>
</html>)rawliteral";
void setup() {
Serial.begin(115200);
// Configura el dispositivo como Station y Soft Access Point simultáneamente
WiFi.mode(WIFI_AP_STA);
// Configura el dispositivo como Wi-Fi Station
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Setting as a Wi-Fi Station..");
}
Serial.print("Station IP Address: ");
Serial.println(WiFi.localIP());
Serial.println();
// Inicia el sensor BME680
if (!bme.begin()) {
Serial.println(F("Could not find a valid BME680 sensor, check wiring!"));
while (1);
}
// Configura el sobremuestreo y la inicialización del filtro
bme.setTemperatureOversampling(BME680_OS_8X);
bme.setHumidityOversampling(BME680_OS_2X);
bme.setPressureOversampling(BME680_OS_4X);
bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
bme.setGasHeater(320, 150); // 320*C durante 150 ms
// Maneja el servidor web
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(200, "text/html", index_html, processor);
});
// Maneja los eventos del servidor web
events.onConnect([](AsyncEventSourceClient *client){
if(client->lastId()){
Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
}
// envía evento con mensaje "hello!", id actual en millis
// y establece el delay de reconexión a 10 segundos
client->send("hello!", NULL, millis(), 10000);
});
server.addHandler(&events);
server.begin();
}
void loop() {
if ((millis() - lastTime) > timerDelay) {
getBME680Readings();
Serial.printf("Temperature = %.2f ºC \n", temperature);
Serial.printf("Humidity = %.2f % \n", humidity);
Serial.printf("Pressure = %.2f hPa \n", pressure);
Serial.printf("Gas Resistance = %.2f KOhm \n", gasResistance);
Serial.println();
// Envía eventos al servidor web con las lecturas del sensor
events.send("ping",NULL,millis());
events.send(String(temperature).c_str(),"temperature",millis());
events.send(String(humidity).c_str(),"humidity",millis());
events.send(String(pressure).c_str(),"pressure",millis());
events.send(String(gasResistance).c_str(),"gas",millis());
lastTime = millis();
}
}
Inserta las credenciales de tu red en las siguientes variables y el código funcionará inmediatamente:
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";
Cómo funciona el código paso a paso
Incluir las librerías
Empieza incluyendo las librerías necesarias. La libería Wire es necesaria para el protocolo de comunicación I2C. También incluimos la libería SPI si quieres usar comunicación SPI en su lugar. Las liberías Adafruit_Sensor y Adafruit_BME680 son necesarias para interactuar con el sensor BME680. Las liberías WiFi y ESPAsyncWebServer se usan para crear el servidor web.
Credenciales de red
Se insertan las credenciales de red en las variables correspondientes, para que el ESP32 pueda conectarse a tu red local usando Wi-Fi.
Comunicación I2C
Se crea un objeto Adafruit_BME680 llamado bme en los pines I2C por defecto del ESP32. Si quieres usar comunicación SPI en su lugar, necesitas definir los pines SPI del ESP32. Y después, crear un objeto Adafruit_BME680 usando esos pines.
Declarar variables
Las variables temperature, humidity, pressure y gasResistance de tipo float se usarán para almacenar las lecturas del sensor BME680. Las variables lastTime y timerDelay se usarán para actualizar las lecturas del sensor cada X número de segundos. Como ejemplo, obtendremos nuevas lecturas cada 30 segundos (30000 milisegundos). Puedes cambiar ese tiempo de delay en la variable timerDelay.
Se crea un Async Web Server en el puerto 80.
Crear el event source
Para mostrar automáticamente la información en el servidor web cuando llegue una nueva lectura, usaremos Server-Sent Events (SSE). La siguiente línea crea un nuevo event source en /events:
AsyncEventSource events("/events");
Server-Sent Events permite que una página web (cliente) reciba actualizaciones de un servidor. Usaremos esto para mostrar automáticamente nuevas lecturas en la página del servidor web cuando haya nuevas lecturas del BME680 disponibles.
Importante: Server-Sent Events no son compatibles con Internet Explorer.
Obtener las lecturas del BME680
La función getBME680Readings() obtiene las lecturas de gas, temperatura, humedad y presión del sensor BME680 y las guarda en las variables gasResistance, temperature, humidity y pressure. La función inicia la medición con bme.beginReading() y la completa con bme.endReading(), comprobando errores en cada paso.
La función processor()
La función processor() reemplaza cualquier placeholder del texto HTML que se usa para construir la página web con las lecturas actuales del sensor. Esto nos permite mostrar las lecturas actuales del sensor en la página web cuando la visitas por primera vez. De lo contrario, verías un espacio en blanco hasta que hubiera nuevas lecturas disponibles (lo que puede tardar un tiempo según el delay definido en el código).
Construir la página web
La variable index_html contiene todo el HTML, CSS y JavaScript necesarios para construir la página web. No entraremos en detalle sobre cómo funciona el HTML y CSS; solo veremos cómo manejar los eventos enviados por el servidor.
Veamos la línea que muestra la temperatura:
<h4><i class="fas fa-thermometer-half"></i> TEMPERATURE</h4><p><span class="reading"><span id="temp">%TEMPERATURE%</span> °C</span></p>
Puedes ver que el placeholder %TEMPERATURE% está rodeado por las etiquetas <span id="temp"> y </span>. El atributo id de HTML se usa para especificar un identificador único para un elemento HTML. Se usa para apuntar a un estilo específico o puede ser usado por JavaScript para acceder y manipular el elemento con ese id específico. Eso es lo que vamos a hacer.
Por ejemplo, cuando el servidor web recibe un nuevo evento con la última lectura de temperatura, actualizaremos el elemento HTML con el id temp con la nueva lectura. Un proceso similar se hace para actualizar las otras lecturas.
Manejar eventos
Se crea un nuevo objeto EventSource y se especifica la URL de la página que envía las actualizaciones. En nuestro caso, es /events:
if (!!window.EventSource) {
var source = new EventSource('/events');
Una vez que has instanciado un event source, puedes empezar a escuchar mensajes del servidor con addEventListener(). Estos son los event listeners por defecto. Después, se añade el event listener para temperature:
source.addEventListener('temperature', function(e) {
Cuando hay una nueva lectura de temperatura disponible, el ESP32 envía un evento (temperature) al cliente. Las siguientes líneas manejan qué ocurre cuando el navegador recibe ese evento:
console.log("temperature", e.data);
document.getElementById("temp").innerHTML = e.data;
Básicamente, imprime las nuevas lecturas en la consola del navegador y coloca los datos recibidos en el elemento con el id correspondiente (temp) en la página web. Un procesador similar se hace para humedad, presión y resistencia de gas.
La función setup()
En el setup(), se inicializa el Monitor Serie. Se conecta el ESP32 a tu red local y se imprime la dirección IP del ESP32. Después, se inicializa el sensor BME680. Se configura el sobremuestreo y la inicialización del filtro para obtener la mejor calidad de lectura:
bme.setTemperatureOversampling(BME680_OS_8X);
bme.setHumidityOversampling(BME680_OS_2X);
bme.setPressureOversampling(BME680_OS_4X);
bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
bme.setGasHeater(320, 150); // 320*C durante 150 ms
Manejar peticiones
Cuando accedes a la dirección IP del ESP32 en la URL raíz /, se envía el texto que está almacenado en la variable index_html para construir la página web y se pasa el processor como argumento, para que todos los placeholders sean reemplazados con las últimas lecturas del sensor.
Event source del servidor
Se configura el event source en el servidor. La función onConnect se ejecuta cada vez que un nuevo cliente se conecta al stream de eventos. Enviamos un mensaje de bienvenida “hello!” con un identificador de mensaje y un delay de reconexión de 10 segundos. Finalmente, se inicia el servidor.
La función loop()
En el loop(), se obtienen nuevas lecturas del sensor. Se imprimen las nuevas lecturas en el Monitor Serie. Finalmente, se envían eventos al navegador con las lecturas más recientes del sensor para actualizar la página web:
events.send("ping",NULL,millis());
events.send(String(temperature).c_str(),"temperature",millis());
events.send(String(humidity).c_str(),"humidity",millis());
events.send(String(pressure).c_str(),"pressure",millis());
events.send(String(gasResistance).c_str(),"gas",millis());
Subir el código
Ahora, sube el código a tu ESP32. Asegúrate de tener la placa y el puerto COM correctos seleccionados. Después de subir, abre el Monitor Serie a 115200 baudios. Pulsa el botón RST/EN integrado en la placa. La dirección IP del ESP32 debería imprimirse en el monitor serie.
Demostración
Abre un navegador en tu red local y escribe la dirección IP del ESP32. Deberías obtener acceso al servidor web del ESP32 con las últimas lecturas del BME680. Las lecturas se actualizan automáticamente usando Server-Sent Events.
Conclusión: lo que has aprendido
En este tutorial has aprendido cómo construir una estación meteorológica con servidor web asíncrono usando el ESP32 para mostrar las lecturas del sensor BME680 (gas/calidad del aire, temperatura, humedad y presión), y cómo actualizar las lecturas automáticamente en la página web usando Server-Sent Events.
El BME680 es mucho más que un simple sensor ambiental: es una puerta de entrada a la monitorización de calidad del aire interior. Combinado con el servidor web asíncrono y SSE que has aprendido a construir, tienes una estación meteorológica completa y profesional capaz de monitorizar en tiempo real las condiciones ambientales de cualquier espacio. Añade el ESP32-CAM y una tarjeta microSD, y podrás construir sistemas de monitorización ambiental autónomos que registren datos durante semanas o meses. La combinación BME680 + ESP32 + SSE es una de las más potentes y útiles del ecosistema maker, aplicable tanto a proyectos domésticos como a soluciones de monitorización industrial.
Server-Sent Events es la tecnología perfecta para este tipo de proyectos unidireccionales, donde el servidor empuja los datos al cliente sin que este necesite pedirlos continuamente. Menos compleja que WebSocket, más eficiente que el polling tradicional, SSE es la opción natural cuando solo necesitas que la página web se actualice automáticamente sin intervención del usuario.
Descubre más desde CIBERED
Suscríbete y recibe las últimas entradas en tu correo electrónico.
