Hoy te mostraré cómo medir y guardar la temperatura y humedad del ambiente usando PHP, MySQL, un sensor de temperatura DHT22 y una tarjeta NodeMCU ESP8266. Lo que te voy a mostrar es un proyecto con estos componentes que te menciono, aunque siempre eres libre de reemplazarlos.
Al final vamos a tener un programa en donde la temperatura y humedad se van a registrar cada 30 segundos en una tabla de MySQL. Después vamos a tener una gráfica de línea en donde se mostrará la temperatura y humedad a lo largo del tiempo, por un rango de fechas.
El proceso va a ser sencillo. Primero vamos a programar la tarjeta para que lea el sensor cada determinado tiempo y envíe la humedad y temperatura a nuestro servidor con PHP.
Luego vamos a crear una aplicación web para consultar esos datos y mostrar una gráfica personalizada.
Artículos recomendados
Este post toma muchas cosas de otros artículos que he publicado aquí en el blog de Parzibyte. Te recomiendo que los leas, pues en ellos explico detalladamente cada parte.
Primero revisa cómo leer la temperatura y humedad usando el sensor DHT22, y después revisa cómo enviar valores a un servidor web usando la ESP8266 y el módulo WiFi que ya tiene integrado.
Si bien puedes continuar leyendo este artículo (ya que te dejaré el código completo al final) te recomiendo que revises esos posts en caso de que quieras estudiar más a fondo.
Circuito de ESP8266 y DHT22
Anteriormente hice un circuito pero el pin de conexión del sensor va al D8 de la tarjeta. Ahora lo he cambiado al D1, pues el D8 causa interferencia. Así que el circuito de este proyecto queda así:
Recuerda que tú puedes conectarlo como prefieras. Físicamente el mío quedó así:
Código de la tarjeta
Este código está escrito en C++, y puedes cargarlo a la tarjeta usando PlatformIO o el IDE de Arduino. Antes de usarlo, recuerda instalar las siguientes librerías:
- adafruit/DHT sensor library
- adafruit/Adafruit Unified Sensor
Después vemos el código. En este caso la NodeMCU ESP8266 se va a conectar a una red WiFi y desde ahí se conectará al servidor web con PHP, ya sea que el mismo sea local o esté en internet.
Lo único que tienes que hacer es cambiar el nombre de la red, contraseña y dirección del servidor. También eres libre de modificar el código como tú lo prefieras. Al final queda así:
/*
____ _____ _ _ _
| _ \ | __ \ (_) | | |
| |_) |_ _ | |__) |_ _ _ __ _____| |__ _ _| |_ ___
| _ <| | | | | ___/ _` | '__|_ / | '_ \| | | | __/ _ \
| |_) | |_| | | | | (_| | | / /| | |_) | |_| | || __/
|____/ \__, | |_| \__,_|_| /___|_|_.__/ \__, |\__\___|
__/ | __/ |
|___/ |___/
Blog: https://parzibyte.me/blog
Ayuda: https://parzibyte.me/blog/contrataciones-ayuda/
Contacto: https://parzibyte.me/blog/contacto/
Copyright (c) 2020 Luis Cabrera Benito
Licenciado bajo la licencia MIT
El texto de arriba debe ser incluido en cualquier redistribucion
*/
#include "DHT.h"
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
// Credentials to connect to the wifi network
const String SSID = "RED";
const String PASSWORD = "PASSWORD";
/*
The ip or server address. If you are on localhost, put your computer's IP (for example http://192.168.1.65)
If the server is online, put the server's domain for example https://parzibyte.me
*/
const String SERVER_ADDRESS = "http://192.168.1.82/esp8266_dht";
#define DHT_CONNECTION_PIN D1 // Pin connected to "out" pin in DHT sensor
#define SENSOR_TYPE DHT22
#define STATUS_LED 2
DHT sensor(DHT_CONNECTION_PIN, SENSOR_TYPE);
float humidity, temperature = 0;
int lastTimeRead = 0;
long readDataInterval = 30000; // Must be greater than 2 seconds (2000 miliseconds)
void setup()
{
// Led
pinMode(STATUS_LED, OUTPUT);
digitalWrite(STATUS_LED, LOW);
sensor.begin();
// Connect to wifi
WiFi.begin(SSID, PASSWORD);
while (WiFi.status() != WL_CONNECTED)
{
delay(1000);
}
digitalWrite(STATUS_LED, HIGH);
}
void indicateDhtError()
{
int x = 0;
for (x = 0; x < 5; x++)
{
digitalWrite(STATUS_LED, LOW);
delay(50);
digitalWrite(STATUS_LED, HIGH);
delay(50);
}
}
void indicateDhtSuccess()
{
digitalWrite(STATUS_LED, LOW);
delay(50);
digitalWrite(STATUS_LED, HIGH);
}
void loop()
{
if (WiFi.status() != WL_CONNECTED)
{
return;
}
// Read data if interval has been reached
if (lastTimeRead > readDataInterval)
{
humidity = sensor.readHumidity();
temperature = sensor.readTemperature();
// Check if data is ok
if (isnan(temperature) || isnan(humidity))
{
indicateDhtError();
lastTimeRead = 0;
return;
}
HTTPClient http;
// Setup data url
String full_url = SERVER_ADDRESS + "/save_data.php?temperature=" + temperature + "&humidity=" + humidity;
http.begin(full_url);
// Make request
int httpCode = http.GET();
if (httpCode > 0)
{
if (httpCode == HTTP_CODE_OK)
{
// Handle success
indicateDhtSuccess();
}
else
{
//Handle error
}
}
else
{
// Handle error
}
http.end(); //Close connection
lastTimeRead = 0;
}
// Here you can do more things...
delay(10);
lastTimeRead += 10;
}
El envío de datos está ocurriendo en la línea 105. Preparamos la URL y concatenamos los valores como parámetros GET para enviarlos al servidor.
La humedad y temperatura se van a enviar cada 30 segundos, aunque puedes modificar ese intervalo. Por cierto, así como lo indico en el circuito: el PIN de Data del DHT22 va conectado al D1 de la tarjeta.
También me tomé la libertad de indicar los estados de error y éxito usando el LED integrado. Cuando hay un error, el mismo va a parpadear 5 veces.
Y cuando todo vaya bien (es decir, la temperatura y humedad se enviaron al servidor web) va a parpadear una vez.
Ese es todo el código de la tarjeta. Como te dije anteriormente, puedes enviar esto a un servidor dentro de tu red local o a un servidor de internet.
Lado del servidor con PHP
En el servidor debemos conectarnos a la base de datos con MySQL y realizar las operaciones de guardado y lectura de los valores.
Lo primero que debes hacer cuando descargues el código es configurar el archivo env.php
basándote en el archivo env.example.php
pues desde ese archivo se van a leer las credenciales para acceder a la base de datos.
Personalmente tengo mi archivo así:
; <?php exit; ?>
MYSQL_DATABASE_NAME = "dht_log"
MYSQL_USER = "root"
MYSQL_PASSWORD = ""
Se está conectando a la base de datos llamada dht_log
. Es buen momento para mostrar el esquema de la base de datos, que solamente tiene una tabla:
CREATE TABLE IF NOT EXISTS dht_log(
id BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
date varchar(19) NOT NULL,
temperature DECIMAL(5, 2) NOT NULL,
humidity DECIMAL(5, 2) NOT NULL
);
Y finalmente veamos las funciones que vamos a usar. Tenemos algunas para leer variables del entorno del archivo que mencioné anteriormente, otras para obtener la conexión a la base de datos y otra para guardar o leer los datos.
Básicamente estoy usando PHP con PDO para guardar y leer desde MySQL.
<?php
function getSensorData($start, $end)
{
$db = getDatabase();
$statement = $db->prepare("SELECT date, temperature, humidity FROM dht_log WHERE date >= ? AND date <= ?");
$statement->execute([$start, $end]);
return $statement->fetchAll();
}
function saveDhtData($temperature, $humidity)
{
$db = getDatabase();
$currentDate = date("Y-m-d H:i:s");
$statement = $db->prepare("INSERT INTO dht_log(date, temperature, humidity) VALUES (?, ?, ?)");
return $statement->execute([$currentDate, $temperature, $humidity]);
}
function getVarFromEnvironmentVariables($key)
{
if (defined("_ENV_CACHE")) {
$vars = _ENV_CACHE;
} else {
$file = "env.php";
if (!file_exists($file)) {
throw new Exception("The environment file ($file) does not exists. Please create it");
}
$vars = parse_ini_file($file);
define("_ENV_CACHE", $vars);
}
if (isset($vars[$key])) {
return $vars[$key];
} else {
throw new Exception("The specified key (" . $key . ") does not exist in the environment file");
}
}
function getDatabase()
{
$password = getVarFromEnvironmentVariables("MYSQL_PASSWORD");
$user = getVarFromEnvironmentVariables("MYSQL_USER");
$dbName = getVarFromEnvironmentVariables("MYSQL_DATABASE_NAME");
$database = new PDO('mysql:host=localhost;dbname=' . $dbName, $user, $password);
$database->query("set names utf8;");
$database->setAttribute(PDO::ATTR_EMULATE_PREPARES, FALSE);
$database->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$database->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
return $database;
}
A través de todo el código vamos a ir invocando a estas funciones. Las estoy mostrando aquí de una vez para no explicarlas más adelante en cada apartado. Y no te preocupes, te dejaré el código completo al final del post.
Registrar temperatura y humedad con MySQL
En el código de la tarjeta se puede observar que se hace una petición al archivo save_data.php
. El contenido del mismo queda así:
<?php
if (!isset($_GET["temperature"]) || !isset($_GET["humidity"])) {
exit("temperature and humidity are required");
}
$temperature = $_GET["temperature"];
$humidity = $_GET["humidity"];
include_once "functions.php";
saveDhtData($temperature, $humidity);
exit("ok");
Lo único que hacemos es tomar los valores de la URL, incluir nuestro archivo de funciones (functions.php
, que ya vimos anteriormente) e invocar a la función saveDhtData
. Eso hará que la humedad y temperatura, junto con la fecha y hora actual, queden guardados en la base de datos.
Lado del cliente: gráfica de temperatura y humedad
Ahora del lado del cliente vamos a consultar al archivo get_data.php
al que le debemos pasar en la URL el rango de fechas en el que queremos consultar los datos. Este archivo nos va a devolver los datos como JSON en un arreglo.
<?php
if (!isset($_GET["start"]) || !isset($_GET["end"])) {
echo json_encode([]);
exit;
}
$start = $_GET["start"];
$end = $_GET["end"];
include_once "functions.php";
echo json_encode(getSensorData($start, $end));
Ahora en el lado del cliente con Vue y Chart.js graficamos los datos. Vue.js se va a encargar de hacer la petición HTTP al servidor con PHP usando fetch, y después va a decirle a Chart.js que refresque la gráfica:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Check DHT sensor log</title>
<link rel="stylesheet" href="./css/bulma.min.css">
</head>
<body>
<nav class="navbar is-warning" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="https://parzibyte.me/l/fW8zGd">
<img alt="" src="parzibyte.png" style="max-height: 80px;" />
</a>
<button class="navbar-burger is-warning button" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</button>
</div>
<div class="navbar-menu">
<div class="navbar-start">
<a href="index.php" class="navbar-item">
Chart
</a>
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<a target="_blank" rel="noreferrer" href="https://parzibyte.me/l/fW8zGd" class="button is-primary">
<strong>Support & Help</strong>
</a>
</div>
</div>
</div>
</div>
</nav>
<div id="app" class="section">
<div class="columns">
<div class="column is-one-third">
<p>Start</p>
<div class="field has-addons">
<div class="control">
<input @change="onDateOrTimeChanged" v-model="startDate" class="input" type="date">
</div>
<div class="control ml-2">
<input @change="onDateOrTimeChanged" v-model="startTime" class="input" type="time">
</div>
</div>
</div>
<div class="column is-one-third">
<p>End</p>
<div class="field has-addons">
<div class="control">
<input @change="onDateOrTimeChanged" v-model="endDate" class="input" type="date">
</div>
<div class="control ml-2">
<input @change="onDateOrTimeChanged" v-model="endTime" class="input" type="time">
</div>
</div>
</div>
</div>
<div class="columns">
<canvas id="chart"></canvas>
</div>
</div>
<script src="./js/Chart.bundle.min.js"></script>
<script src="./js/vue.min.js"></script>
<script src="./js/script.js"></script>
</body>
</html>
Cuando cualquiera de los campos tenga un cambio, se invoca a la función onDateOrTimeChanged
que a su vez va a obtener los datos enviando la fecha y hora de inicio y de fin. Todo esto pasará sin refrescar la página, usando AJAX.
El código JavaScript queda así:
new Vue({
el: "#app",
data: () => ({
startTime: "",
endTime: "",
startDate: "",
endDate: "",
chart: null,
}),
async mounted() {
this.startDate = this.getStartMonthDate();
this.endDate = this.getEndMonthDate();
this.endTime = this.getStartTime();
this.startTime = this.getStartTime();
await this.onDateOrTimeChanged();
},
methods: {
async onDateOrTimeChanged() {
const url = `http://localhost/esp8266_dht/get_data.php?start=${this.startDate.concat(" ", this.startTime)}&end=${this.endDate.concat(" ", this.endTime)}`;
const response = await fetch(url);
const log = await response.json();
const labels = log.map(d => {
return d.date;
});
const temperatureData = log.map(d => {
return d.temperature;
});
const humidityData = log.map(d => {
return d.humidity;
});
this.refreshChart(labels, temperatureData, humidityData);
},
getStartMonthDate() {
const d = new Date();
return this.formatDate(new Date(d.getFullYear(), d.getMonth(), 1));
},
getEndMonthDate() {
const d = new Date();
return this.formatDate(new Date(d.getFullYear(), d.getMonth() + 1, 0));
},
getStartTime() {
const d = new Date();
return this.formatTime(new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0));
},
formatDate(date) {
const month = date.getMonth() + 1;
const day = date.getDate();
return `${date.getFullYear()}-${this.padWithZero(month)}-${this.padWithZero(day)}`;
},
formatTime(date) {
const hours = date.getHours();
const minutes = date.getMinutes();
const seconds = date.getSeconds();
return `${this.padWithZero(hours)}:${this.padWithZero(minutes)}:${this.padWithZero(seconds)}`;
},
padWithZero(value) {
return (value < 10 ? "0" : "").concat(value);
},
refreshChart(labels, temperatureData, humidityData) {
if (this.chart) {
this.chart.destroy();
}
this.chart = new Chart(document.querySelector("#chart"), {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Temperature',
data: temperatureData,
backgroundColor: [
'rgba(255, 206, 86, 0.2)',
],
pointRadius: 1,
pointHoverRadius: 1
},
{
label: 'Humidity',
data: humidityData,
backgroundColor: [
'rgba(54, 162, 235, 0.2)',
],
pointRadius: 1,
pointHoverRadius: 1
}
]
},
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero: true
}
}],
xAxes: [{
display: false //this will remove all the x-axis grid lines
}]
},
}
});
},
},
});
La petición se hace en la línea 18. Y la gráfica (creada con chart.js) se está renderizando en la línea 59. En este caso tenemos dos datasets o conjunto de datos. Uno de ellos es la temperatura y el otro es la humedad.
Para el caso de las etiquetas o labels, todas son la fecha y hora en la que se registró la temperatura. Por cierto, si quieres registrar estos datos pero en adafruit, puedes mirar este post.
Como puedes ver, el usuario puede interactuar con el formulario y seleccionar un rango distinto para consultar la temperatura y humedad que la ESP8266 ya envió a nuestro servidor con PHP y MySQL.
Poniendo todo junto
El código completo lo dejaré, como siempre, en mi GitHub. Ahí vas a encontrar tanto el código de PHP como el de la tarjeta.
Si quieres usar mi código simplemente descárgalo y monta el proyecto en tu servidor Apache local o de internet. Configura el archivo de entorno, crea la base de datos e importa la tabla.
Después coloca la IP o dirección del mismo en la tarjeta ESP8266 así como el acceso a la red WiFi, carga el código, conecta el circuito y listo.
Muy bueno el tutorial, no conozco nada de php pero a fuerza quiero aprenderlo y este tutorial me sirvio
Muy bueno la app en Vue, estaria buno reflotar el proyecto pero usando React, se podra….sigue asi!!!!!
Gracias. Saludos