electrónica

Mostrar temperatura en servidor web de ESP8266

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.

Termómetro digital – Mostrar temperatura y humedad en sensor DHT22 con servidor web

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.

ESP8266 con DHT22 para enviar temperatura y humedad a PHP

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.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Entradas recientes

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

2 días hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

2 días hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

2 días hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

2 días hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

2 días hace

Solución: Apache – Server unable to read htaccess file

Ayer estaba editando unos archivos que son servidos con el servidor Apache y al visitarlos…

3 días hace

Esta web usa cookies.