La tarjeta NodeMCU ESP8266 es capaz de ejecutar un servidor web en donde podemos responder con HTML, JSON, etcétera.
Además, se puede conectar a una red WiFi y a su vez a internet. Por otro lado, puede conectarse a varios sensores como lo es el DHT22 que sirve para medir la temperatura y humedad.
Gracias a eso podemos crear un servidor web que nos va a decir la temperatura y humedad del ambiente usando el sensor DHT22. De este modo podremos conectarnos a la tarjeta usando su IP, y la misma nos va a mostrar (gracias al web server) la última lectura del sensor.
Lecturas recomendadas
Lo que vamos a ver aquí será, como en muchos casos dentro del blog de Parzibyte, una unión de varios artículos.
Vamos a recopilar la temperatura y humedad actual usando el sensor DHT22; después vamos a crear el diseño de la página web para el termómetro y finalmente vamos a embeber la página dentro de un servidor web en la ESP8266.
Así que en resumen será unir y adaptar esos tres artículos que cito. Si bien te dejaré el código completo al final del post, te recomiendo que los leas en caso de que no entiendas alguna cosa.
Preparando página web
Como te mencioné anteriormente, me basé en una plantilla que yo mismo diseñé. Sin embargo, le agregué el funcionamiento dinámico para mostrar la temperatura y humedad en tiempo real, refrescándola en un intervalo de 5 segundos.
Para hacer esto utilicé el framework Vue. Así que el código completo me quedó así:
<!DOCTYPE html>
<html lang='es'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>Sensor de temperatura - By Parzibyte</title>
<link rel='stylesheet' href='https://unpkg.com/bulma@0.9.1/css/bulma.min.css'>
<link rel='stylesheet' href='https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css'>
</head>
<body>
<section id='app' class='hero is-link is-fullheight'>
<div class='hero-body'>
<div class='container'>
<div class='columns has-text-centered'>
<div class='column'>
<h1 style='font-size: 2.5rem'>Termómetro</h1>
<i class='fa' :class='claseTermometro' style='font-size: 4rem;'></i>
</div>
</div>
<div class='columns'>
<div class='column has-text-centered'>
<h2 class='is-size-4 has-text-warning'>Temperatura</h2>
<h2 class='is-size-1'>{{temperatura}} °C</h2>
</div>
<div class='column has-text-centered'>
<h2 class='is-size-4 has-text-warning'>Humedad</h2>
<h2 class='is-size-1'>{{humedad}} %</h2>
</div>
</div>
<div class='columns'>
<div class='column'>
<p>Última lectura: Hace <strong class='has-text-white'>{{ultimaLectura}}</strong> segundo(s)</p>
<p class='is-size-5'><i class='fa fa-code'></i> con <i class='fa fa-heart has-text-danger'></i>
por <a target='_blank' class='has-text-warning'
href='https://parzibyte.me/blog'>Parzibyte</a></p>
</div>
</div>
</div>
</div>
</section>
<script src='https://unpkg.com/vue@2.6.12/dist/vue.min.js'>
</script>
<script>
const INTERVALO_REFRESCO = 5000;
new Vue({
el: '#app',
data: () => ({
ultimaLectura: 0,
temperatura: 0,
humedad: 0,
}),
mounted() {
this.refrescarDatos();
},
methods: {
async refrescarDatos() {
try {
const respuestaRaw = await fetch('./api');
const datos = await respuestaRaw.json();
this.ultimaLectura = datos.u;
this.temperatura = datos.t;
this.humedad = datos.h;
setTimeout(() => {
this.refrescarDatos();
}, INTERVALO_REFRESCO);
} catch (e) {
setTimeout(() => {
this.refrescarDatos();
}, INTERVALO_REFRESCO);
}
}
},
computed: {
claseTermometro() {
if (this.temperatura <= 5) {
return 'fa-thermometer-empty';
} else if (this.temperatura > 5 && this.temperatura <= 13) {
return 'fa-thermometer-quarter';
} else if (this.temperatura > 13 && this.temperatura <= 21) {
return 'fa-thermometer-half';
} else if (this.temperatura > 21 && this.temperatura <= 30) {
return 'fa-thermometer-three-quarters';
} else {
return 'fa-thermometer-full';
}
}
}
});
</script>
</body>
</html>
Lo que realmente cambia es el script y la importación de Vue. Básicamente estoy consumiendo la propia API que el servidor web de la ESP8266 crea. El endpoint es /api
.
El consumo se está realizando en la línea 58 dentro del método refrescarDatos
. Ya sea que la petición sea exitosa o no, volvemos a invocar a la función en un intervalo de 5 segundos. Así que en resumen estamos refrescando los valores cada 5 segundos.
Después, dentro de la plantilla tenemos la humedad, temperatura y última lectura exitosa. Además, como lo expliqué en el post del template en el que me estoy basando, mostraré un icono distinto para el termómetro dependiendo de la temperatura (línea 76).
Minificando página web
Fíjate en el código de la página web. No usa comillas dobles, solo usa comillas simples '
. Lo hice a propósito pues voy a copiar todo ese código dentro del código fuente de la tarjeta, y como estará en una línea entre comillas dobles, no causará conflictos.
Para minificar todo este HTML con el script, y así ahorrar memoria de la ESP8266 utilicé este minificador: https://www.willpeavy.com/tools/minifier/
De este modo, el resultado final fue el siguiente:
<!DOCTYPE html><html lang='es'><head> <meta charset='UTF-8'> <meta name='viewport' content='width=device-width, initial-scale=1.0'> <title>Sensor de temperatura - By Parzibyte</title> <link rel='stylesheet' href='https://unpkg.com/bulma@0.9.1/css/bulma.min.css'> <link rel='stylesheet' href='https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css'></head><body> <section id='app' class='hero is-link is-fullheight'> <div class='hero-body'> <div class='container'> <div class='columns has-text-centered'> <div class='column'> <h1 style='font-size: 2.5rem'>Termómetro</h1> <i class='fa' :class='claseTermometro' style='font-size: 4rem;'></i> </div></div><div class='columns'> <div class='column has-text-centered'> <h2 class='is-size-4 has-text-warning'>Temperatura</h2> <h2 class='is-size-1'>{{temperatura}}°C</h2> </div><div class='column has-text-centered'> <h2 class='is-size-4 has-text-warning'>Humedad</h2> <h2 class='is-size-1'>{{humedad}}%</h2> </div></div><div class='columns'> <div class='column'> <p>Última lectura: Hace <strong class='has-text-white'>{{ultimaLectura}}</strong> segundo(s)</p><p class='is-size-5'><i class='fa fa-code'></i> con <i class='fa fa-heart has-text-danger'></i> por <a target='_blank' class='has-text-warning' href='https://parzibyte.me/blog'>Parzibyte</a></p></div></div></div></div></section> <script src='https://unpkg.com/vue@2.6.12/dist/vue.min.js'> </script> <script>const INTERVALO_REFRESCO=5000; new Vue({el: '#app', data: ()=> ({ultimaLectura: 0, temperatura: 0, humedad: 0,}), mounted(){this.refrescarDatos();}, methods:{async refrescarDatos(){try{const respuestaRaw=await fetch('./api'); const datos=await respuestaRaw.json(); this.ultimaLectura=datos.u; this.temperatura=datos.t; this.humedad=datos.h; setTimeout(()=>{this.refrescarDatos();}, INTERVALO_REFRESCO);}catch (e){setTimeout(()=>{this.refrescarDatos();}, INTERVALO_REFRESCO);}}}, computed:{claseTermometro(){if (this.temperatura <=5){return 'fa-thermometer-empty';}else if (this.temperatura > 5 && this.temperatura <=13){return 'fa-thermometer-quarter';}else if (this.temperatura > 13 && this.temperatura <=21){return 'fa-thermometer-half';}else if (this.temperatura > 21 && this.temperatura <=30){return 'fa-thermometer-three-quarters';}else{return 'fa-thermometer-full';}}}}); </script></body></html>
Todo el código está minificado y en una sola línea. Lo que yo te recomiendo en caso de que lo modifiques es que primero pruebes todo el código con un servidor en tu computadora, y cuando todo esté probado, lo minifiques y transfieras a la tarjeta.
Circuito
El módulo de WiFi ya viene integrado en la tarjeta, por lo que únicamente necesitamos conectar el sensor. Así que el circuito es el mismo que he usado en mi otro post para monitorear la temperatura y humedad con PHP y MySQL.
Basta con decir que el pin de datos del DHT22 va al D1 de la tarjeta.
API para temperatura y humedad con ESP8266 y DHT22
Antes de mostrar el código completo quiero que diferencies entre las dos cosas que vamos a estar sirviendo aquí. Por un lado vamos a servir la página web que tiene todo el diseño, el termómetro, Vue, etcétera.
Pero esa página no tendrá los valores, ya que los mismos serán consumidos desde una API que vamos a crear en la misma tarjeta, misma que nos dará los datos en formato JSON.
La misma queda así:
// Nuestra pequeña API
void rutaJson()
{
// Calcular última lectura exitosa en segundos
unsigned long tiempoTranscurridoEnMilisegundos = millis() - ultimaLecturaExitosa;
int tiempoTranscurrido = tiempoTranscurridoEnMilisegundos / 1000;
// Búfer para escribir datos en JSON
char bufer[50];
// Crear la respuesta pasando las variables globales
// La salida será algo como:
// {"t":14.20,"h":79.20,"l":5.00}
sprintf(bufer, "{\"t\":%.2f,\"h\":%.2f,\"u\":%d}", temperatura, humedad, tiempoTranscurrido);
// Responder con ese JSON
servidor.send(200, "application/json", bufer);
}
Lo único que hacemos es leer la última temperatura, última humedad y también medir el tiempo de la última lectura. Después usamos sprintf para formatear el JSON y lo imprimimos.
Fíjate en que estamos indicando que el tipo de contenido es application/json
, y que de este modo podríamos consumir la API ya sea desde la propia tarjeta o desde otra aplicación.
Preparando servidor web
Es momento de mostrar el código completo del servidor web. Básicamente lo que hacemos es conectarnos a una red WiFi, configurar las rutas del servidor y leer la temperatura y humedad cada 5 segundos, pero sin dejar de escuchar las peticiones.
El código completo queda así:
/*
____ _____ _ _ _
| _ \ | __ \ (_) | | |
| |_) |_ _ | |__) |_ _ _ __ _____| |__ _ _| |_ ___
| _ <| | | | | ___/ _` | '__|_ / | '_ \| | | | __/ _ \
| |_) | |_| | | | | (_| | | / /| | |_) | |_| | || __/
|____/ \__, | |_| \__,_|_| /___|_|_.__/ \__, |\__\___|
__/ | __/ |
|___/ |___/
____________________________________
/ Si necesitas ayuda, contáctame en \
\ https://parzibyte.me /
------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Creado por Parzibyte (https://parzibyte.me).
------------------------------------------------------------------------------------------------
Si el código es útil para ti, puedes agradecerme siguiéndome: https://parzibyte.me/blog/sigueme/
Y compartiendo mi blog con tus amigos
También tengo canal de YouTube: https://www.youtube.com/channel/UCroP4BTWjfM0CkGB6AFUoBg?sub_confirmation=1
------------------------------------------------------------------------------------------------
*/
#include <ESP8266WiFiMulti.h>
#include <ESP8266WebServer.h>
#include "DHT.h"
#include <Arduino.h>
#define PIN_CONEXION_DHT D1
#define TIPO_SENSOR DHT22
#define LED_DE_ESTADO 2
ESP8266WiFiMulti wifiMulti;
ESP8266WebServer servidor(80);
DHT sensor(PIN_CONEXION_DHT, TIPO_SENSOR);
int ultimaVezLeido = 0;
long intervaloLectura = 5000; // Debería ser mayor que 2000
unsigned long ultimaLecturaExitosa = 0;
float humedad, temperatura = 0;
// Prototipos de funciones
void rutaRaiz();
void rutaNoEncontrada();
void rutaJson();
void indicarErrorDht();
void indicarExitoDht();
void setup(void)
{
pinMode(LED_DE_ESTADO, OUTPUT);
digitalWrite(LED_DE_ESTADO, LOW);
sensor.begin();
// Aquí puedes agregar varias redes. La tarjeta se conectará a la más cercana
wifiMulti.addAP("Red", "Contraseña");
// wifiMulti.addAP("Otra red", "Contraseña");
// Esperar conexión WiFi
while (wifiMulti.run() != WL_CONNECTED)
{
delay(250);
}
// Configurar rutas
servidor.on("/", rutaRaiz);
servidor.on("/api", rutaJson);
servidor.onNotFound(rutaNoEncontrada);
// Iniciar servidor
servidor.begin();
digitalWrite(LED_DE_ESTADO, HIGH);
}
void loop(void)
{
// Si el intervalo se ha alcanzado, leer la temperatura
if (ultimaVezLeido > intervaloLectura)
{
float nuevaHumedad = sensor.readHumidity();
float nuevaTemperatura = sensor.readTemperature();
// Si los datos son correctos, actualizamos las globales
if (isnan(nuevaTemperatura) || isnan(nuevaHumedad))
{
indicarErrorDht();
ultimaVezLeido = 0;
return;
}
ultimaLecturaExitosa = millis();
humedad = nuevaHumedad;
temperatura = nuevaTemperatura;
ultimaVezLeido = 0;
indicarExitoDht();
}
delay(1);
ultimaVezLeido += 1;
// Responder las solicitudes entrantes en caso de que haya
servidor.handleClient();
}
// Servir toda la página web, desde la misma se consultará a la API que está aquí mismo
void rutaRaiz()
{
servidor.send(200, "text/html", "<!DOCTYPE html><html lang='es'><head> <meta charset='UTF-8'> <meta name='viewport' content='width=device-width, initial-scale=1.0'> <title>Sensor de temperatura - By Parzibyte</title> <link rel='stylesheet' href='https://unpkg.com/bulma@0.9.1/css/bulma.min.css'> <link rel='stylesheet' href='https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css'></head><body> <section id='app' class='hero is-link is-fullheight'> <div class='hero-body'> <div class='container'> <div class='columns has-text-centered'> <div class='column'> <h1 style='font-size: 2.5rem'>Termómetro</h1> <i class='fa' :class='claseTermometro' style='font-size: 4rem;'></i> </div></div><div class='columns'> <div class='column has-text-centered'> <h2 class='is-size-4 has-text-warning'>Temperatura</h2> <h2 class='is-size-1'>{{temperatura}}°C</h2> </div><div class='column has-text-centered'> <h2 class='is-size-4 has-text-warning'>Humedad</h2> <h2 class='is-size-1'>{{humedad}}%</h2> </div></div><div class='columns'> <div class='column'> <p>Última lectura: Hace <strong class='has-text-white'>{{ultimaLectura}}</strong> segundo(s)</p><p class='is-size-5'><i class='fa fa-code'></i> con <i class='fa fa-heart has-text-danger'></i> por <a target='_blank' class='has-text-warning' href='https://parzibyte.me/blog'>Parzibyte</a></p></div></div></div></div></section> <script src='https://unpkg.com/vue@2.6.12/dist/vue.min.js'> </script> <script>const INTERVALO_REFRESCO=5000; new Vue({el: '#app', data: ()=> ({ultimaLectura: 0, temperatura: 0, humedad: 0,}), mounted(){this.refrescarDatos();}, methods:{async refrescarDatos(){try{const respuestaRaw=await fetch('./api'); const datos=await respuestaRaw.json(); this.ultimaLectura=datos.u; this.temperatura=datos.t; this.humedad=datos.h; setTimeout(()=>{this.refrescarDatos();}, INTERVALO_REFRESCO);}catch (e){setTimeout(()=>{this.refrescarDatos();}, INTERVALO_REFRESCO);}}}, computed:{claseTermometro(){if (this.temperatura <=5){return 'fa-thermometer-empty';}else if (this.temperatura > 5 && this.temperatura <=13){return 'fa-thermometer-quarter';}else if (this.temperatura > 13 && this.temperatura <=21){return 'fa-thermometer-half';}else if (this.temperatura > 21 && this.temperatura <=30){return 'fa-thermometer-three-quarters';}else{return 'fa-thermometer-full';}}}}); </script></body></html>");
}
// Manejador de 404
void rutaNoEncontrada()
{
servidor.send(404, "text/plain", "No encontrado");
}
// Nuestra pequeña API
void rutaJson()
{
// Calcular última lectura exitosa en segundos
unsigned long tiempoTranscurridoEnMilisegundos = millis() - ultimaLecturaExitosa;
int tiempoTranscurrido = tiempoTranscurridoEnMilisegundos / 1000;
// Búfer para escribir datos en JSON
char bufer[50];
// Crear la respuesta pasando las variables globales
// La salida será algo como:
// {"t":14.20,"h":79.20,"l":5.00}
sprintf(bufer, "{\"t\":%.2f,\"h\":%.2f,\"u\":%d}", temperatura, humedad, tiempoTranscurrido);
// Responder con ese JSON
servidor.send(200, "application/json", bufer);
}
/*
Patrones para parpadear LED
*/
void indicarErrorDht()
{
int x = 0;
for (x = 0; x < 5; x++)
{
digitalWrite(LED_DE_ESTADO, LOW);
delay(50);
digitalWrite(LED_DE_ESTADO, HIGH);
delay(50);
}
}
void indicarExitoDht()
{
digitalWrite(LED_DE_ESTADO, LOW);
delay(50);
digitalWrite(LED_DE_ESTADO, HIGH);
}
Así que al final tenemos dos rutas en el servidor. La primera sirve la página web, y la segunda sirve la API en donde muestra los datos del sensor DHT22. Después, en la página web vamos a consumir esta API cada 5 segundos.
Todo esto usando simplemente la NodeMCU ESP8266, el DHT22 y código de C++.
Código completo
Es momento de mostrarte el código completo. Recuerda que, como siempre, eres libre de descargarlo, modificarlo y compartirlo.
Puedes explorar el código en el repositorio de GitHub. También te dejo enlaces para leer más sobre la ESP8266.