Montaje y código

De pruebas y errores. Una pequeña introducción

Tenía mis cachivaches funcionando tan alegremente cuando se sucedieron los siguientes tres problemas que me obligaron a rehacer completamente la programación:

    1. El montaje no funcionaba si no se conecta a la WiFi. Esto en sí no era un problema, estaba diseñado así, pero había pedido el favor de comparar las lecturas obtenidas con un sensor “de verdad”, de los pata negra, y claro, en cuanto se intentó conectar el chisme fuera del ámbito de la red WiFi en la que estaba configurado, no se pudo.
    2. Uno de los sensores perdió conexión a la WiFi y no se volvió a reconectar. Esto ya es un problema algo más grave, porque no sólo perdía la conexión, sino que dejaba de mostrar los datos. En principio el script estaba preparado para reconectarse, pero no lo hacía, así que vuelta a buscar por la red.
    3. Contrastando lecturas con el sensor “pata negra”, las medidas mostraban un margen de error creciente conforme más alta era la lectura del CO2, yendo desde un 10% en niveles bajos, en el entorno de los cuatrocientos y pico, hasta un 20% en lecturas de tan solo setecientos y pico. Esto obviamente era una barbaridad. En ese momento se estaba tomando la lectura del sensor mediante la señal PWM, lo que me hizo sospechar que en el proceso de conversión y lectura pudiera generarse este error, al menos en parte, pero el que aumentara de forma no lineal sino porcentualmente el error conforme subiera la señal daba que pensar. Todavía no he podido verificar esta sospecha, pero por si acaso he modificado el código para tomar los datos del sensor por UART.

La parte buena de todo esto es que los tres problemas se sucedieron casi a la vez, en la misma semana, así que se pudieron afrontar todos al mismo tiempo ¿cómo? Pues rehaciendo la programación completamente, por eso esta entrada ha tardado algo más de la cuenta.

Y es que, ya puestos, me pareció buena idea que si el ESP32 tenía dos núcleos se podían utilizar ambos para realizar tareas distintas. Se podía, con un núcleo, tomar la lectura de los sensores y mostrar los datos por la pantalla, y el otro dedicarlo a todo lo demás, que por el momento sólo es la conexión WiFi y subir los datos a la nube.

Dicho así es fácil, el problema era que no tenía ni idea de cómo hacerlo y hubo que documentarse “un poco”.

Pero vamos ya directamente al montaje y programación del engendro

Esquema y código.

Funcionamiento

    • Cada seis segundos mostrará por la pantalla los datos de temperatura, humedad y CO2
    • Cada cinco minutos subirá a ThingSpeak la media de las diez últimas lecturas de los mencionados datos.
    • Si el nivel de CO2 supera las 700 ppm parpadeará un led azul
    • Si el nivel de CO2 supera las 900 ppm parpadearán un led azul y uno rojo alternativamente
    • Al inicio, el montaje se intentará conectar a la WiFi, mientras lo intenta parpadeará el led azul de la placa.
    • Intentará establecer conexión durante un par de minutos, en el momento que lo consiga el led azul pasará a estar fijo, si transcurrido ese tiempo no ha conseguido conexión, el led azul se apagará y no se volverá a intentar una conexión, habría que resetear o apagar y encender.
    • Si una vez establecida la conexión esta se pierde, el sistema intentará reconectarse. Lo veremos porque el led azul de la placa parpadea.

Montaje

ADVERTENCIAS PREVIAS IMPORTANTES

Vamos primero con un descargo de responsabilidades: No me hago responsable de los daños que se puedan derivar por el montaje descrito a continuación o el uso del mismo. Que cada cual actúe bajo su propia responsabilidad.

Por favor, mucha atención a lo siguiente antes de hacer nada porque, aunque a algunos les parecerá de Perogrullo, es muy importante:

El montaje propuesto se alimenta con una fuente de 5v y a través del pin Vin y esto requiere de una cierta prudencia. Con 500 mA son suficientes para alimentar este circuito. Si mis cálculos son correctos, la suma de los consumos máximos de cada componente no excede los 430 mA. Por supuesto, nada nos impide usar alimentadores de 1 ó 2 amperios, pero 500 mA es lo mínimo y es suficiente.

Como hemos dicho, alimentaremos el montaje a través del pin Vin para alimentar también directamente desde la fuente al sensor. Esto es así porque he comprobado que si alimentamos a la placa por el puerto micro USB la salida Vin proporciona, en mi caso, 4,5 V, lo que es muy justo para el modelo MH-Z19B, e insuficiente, según el datasheet, para el MH-Z19C, aunque funciona, pero no me fío, puede que algunos de los fallos que experimentara en las pruebas fueran por alimentar el montaje así. No obstante, alimentar el sistema por el PIN V in exige ciertas precauciones.

Mucha precaución: Desconectar completamente el montaje antes de conectarlo al puerto USB del ordenador para programarlo. Ojo con esto, porque nos podemos cargar la placa base del ordenador y liarla parda. Así que repito: HAY QUE DESCONECTAR COMPLETAMENTE EL MONTAJE DE LA FUENTE DE ALIMENTACIÓN ANTES DE CONECTARLO AL ORDENADOR PARA PROGRAMAR LA PLACA. Y a riesgo de ser mu pesao, vuelvo a insistir: Siempre que queramos conectar el montaje al ordenador lo tendremos que conectar por el puerto micro USB de la placa, obviamente, así lo programaremos y podremos realizar otras pruebas, podremos usar el monitor serie del IDE de Arduino, etc. Pues bien, en este contexto, jamás, jamás ha de estar conectado a la fuente de alimentación puesto que ya está recibiendo la alimentación por el puerto USB del ordenador y podríamos armar un cortocircuito de consecuencias bastante desagradables. Espero que este punto haya quedado suficientemente claro.

Ojo con las conexiones: Hay diferentes placas con el ESP32 y tienen patillajes diferentes. En este montaje estoy usando unas con 30 pines, pero hay otras en las que el número y orden de los pines cambia, así que no es cuestión de “conectar el quinto por arriba a la derecha con el…” no, ni hablar. Hay que fijarse en el pinout de la placa que podamos tener y buscar la correspondencia de pines.

Con los DHT22 pasa algo parecido: según el modelo puede cambiar el orden de los pines, así que tampoco se trata de “este al del medio y estotro al del…” Pues no, tampoco. Hay que prestar atención a la serigrafía de los pines para realizar las conexiones adecuadamente.

Dos modelos de DHT22 en los que las patillas llevan diferente orden

Esquema de montaje

Esquema de montaje. Pincha sobre la imagen para abrirla a tamaño completo en una nueva ventana.

Alimentamos, como hemos dicho anteriormente, por el pin Vin y a la vez alimentamos directamente el sensor. El positivo del alimentador, +5V, está en este caso coloreado de rojo y blanco, para diferenciarlo del +3V que sale de la placa y que alimentará a la pantalla y al DHT22.

Hemos usado un led rojo y uno azul. Para el rojo pondremos una resistencia de 100Ω, para el azul no hace falta resistencia. Si queremos usar otros colores deberemos informarnos de su tensión de funcionamiento y colocar la resistencia que corresponda.

Conexiones

Sugiero pinchar en el esquema y ampliarlo para poder ver bien las conexiones. Estas van así:

MH-Z19B: Positivo a Vin de la tarjeta y GND a GND de la tarjeta. Alimentaremos por estos pines a la tarjeta y a la vez al sensor directamente. Para la lectura de datos vamos a usar el Serial 2 de la tarjeta, así que el pin TX del sensor lo conectamos al pin RX2 de la tarjeta, GPIO 16, y el pin RX del sensor al TX2 de la tarjeta, GPIO 17

DHT22: El pin de datos del sensor lo conectaremos al GPIO 4 de la tarjeta. El pin Vcc lo conectaremos a la salida de 3V de la tarjeta y GND a GND

Pantalla: La alimentaremos igualmente a 3V, así que VCC a +3V y GND a GND, como antes. El SDA de la pantalla lo conectaremos al GPIO 21 y el SCK al GPIO 22, que por cierto, no están consecutivos, ojo.

Diodos: Las patas cortas son los cátodos y las conectaremos ambas a GND. Las largas, ánodos, las conectaremos: La del led azul al GPIO 32 y la del led rojo al GPIO 33. Como el led azul funciona a 3.6 V no necesita resistencia, sin embargo el led rojo funciona a 1.6V y necesita que entre el ánodo y el GPIO pongamos una resistencia de 100Ω (Bueno, en realidad creo que son unos 90Ω, 100 era la que tenía a mano y funciona. También la resistencia puede conectarse al negativo, el caso es que el circuito tenga el diodo y la resistencia en serie).

Preparando el IDE de Arduino

Vamos a usar el IDE de Arduino para grabar el ESP32. La forma de instalarlo es muy fácil y por si acaso, el link y alguna cosilla más están aquí:

Preparando la placa Arduino

Además de tener instalado ese IDE necesitaremos seleccionar la placa e instalar alguna otra librería que vamos a necesitar. Los pasos son los siguientes:

    • Seleccionar la placa ESP32
    • Instalar la librería DHTesp.h
    • Instalar la librería Adafruit_SSD1306.h
    • Instalar la librería ThingSpeak.h
    • Instalar la librería ErriezMHZ19B.h

Para Instalar la tarjeta ESP32 en el IDE de Arduino tendremos que ir a “Archivo” >> “Preferencias” y en “Gestor de URLs Adicionales de Tarjetas” añadir lo siguiente:

https://dl.espressif.com/dl/package_esp32_index.json

Tras esto, ya podemos ir a “Herramientas” >> “Placa” y en “Gestor de Tarjetas” escribir ESP32 y seleccionarla:

Tras esto ya podemos seleccionar la tarjeta ESP32. En mi caso, la programación la he realizado seleccionando ESP32 Dev Module:

Vamos ahora con la librería para el DHT22. En este caso iremos a “Programa” >> “Incluir Librería” y en “Administrar Bibliotecas” haremos una búsqueda poniendo dhtesp. El autor de la librería es Beegee-Tokyo y es una librería preparada para el ESP32. Es la que he usado y funciona perfectamente. El código fuente y documentación está aquí:

Librería DHTesp de Beegee-Tokyo

Instalamos la pantalla. En este caso vamos igualmente a “Adminstrar Bibliotecas” y en la búsqueda ponemos ssd1306 En este caso la librería a instalar es la de Adafruit:

Librería SSD1306 de Adafruit

Muy probablemente nos diga que necesita instalar otras librerías adicionales. Decimos que instale todas:

Instalar librerías adicionales

Ahora instalamos la librería de ThingSpeak. En este caso la instalaremos desde zip, así que primero habrá que descargarla. Lo hacemos desde esta dirección:

Para descargar, pinchamos en “Code” y luego en “Download ZIP”

No hay que descomprimir el fichero, simplemente, una vez descargado, vamos a “Incluir Librería” pero en este caso seleccionamos “Añadir biblioteca ZIP”, buscamos el fichero que acabamos de descargar y lo seleccionamos:

Listo, ya tenemos la librería preparada para usarla en nuestros desarrollos.

Y por último vamos con una librería para tomar los datos del MH-Z19B. Podríamos tomar los datos nosotros mismos, y la verdad es que para solo tomar las lecturas del CO2 estamos metiendo un montón de código de más, pero es que esta librería hace casi de todo, incluso con instrucciones no documentadas, que por supuesto habrán de usarse bajo el criterio y responsabilidad de cada uno, y tengo un par de ideas que llevar adelante y esta librería me facilita muchísimo el trabajo:

A mí me ha parecido una joya. La he probado con un MH-Z19C y también funciona. Por cierto, hablaré de este modelo de sensor en otra entrada porque lo que he visto me ha gustado bastante, más que el “B”.

Bien, pues para instalar esta librería procedemos igual que con la de ThingSpeak, acudimos a la dirección mencionada, bajamos el Zip y lo instalamos:

Para los atrevidos, si pinchamos en “master” accederemos a otras ramas. Una de ellas es bastante interesante porque puede darnos la temperatura que está midiendo el sensor. Lo he probado con un MH-Z19B y funciona bien, así que quien no necesite los datos de humedad y tenga un “B” (desconozco “aún” si funciona con otros modelos) puede sacar el nivel de CO2 y la temperatura sólo con el sensor.

Rama que implementa la posibilidad de tomar los datos de temperatura del sensor

El código

Para hacerlo funcionar necesitaremos introducir cuatro datos:

    • Nombre de la WiFi
    • Password
    • Número de canal de ThingSpeak
    • Write API Key del canal

Estos cuatro datos se encuentran al principio del código: “AAAAA”, “BBBBB”, 11111 y “CCCCC” respectivamente. Han de introducirse todos con comillas excepto el número del canal, que va sin comillas. Tras colocar esos datos, se salva, se graba la tarjeta y a funcionar.

Pero vuelvo a insistir en lo de antes: Mucha precaución y cuando grabemos la tarjeta hay que asegurarse de que el montaje está completamente desconectado de cualquier fuente de alimentación. Y viceversa, cuando vayamos a conectarlo con un alimentador, hay que asegurarse bien de que está completamente desconectado del ordenador.

Aquí está todo el código para poder echarle un ojo. Más abajo se puede descargar en formato .ino para meterlo al IDE de Arduino o editarlo al gusto.

// PROYECTO DE MONITORIZACIÓN DE CONCENTRACIÓN DE
// CO2, TEMPERATURA Y HUMEDAD
// CON ENVÍO DE DATOS A THINGSPEAK

// Placa SP-Cow ESP32-WROOM-32
// IDE Arduino: ESP32 Dev Module

// Sensor de CO2: MH-Z19B - MH-Z14A - MH-Z19C
// Sensor de temperatura y humedad: DHT22
// Pantalla OLED monocromo: GM009605 v4

// Versión 2.0

// https://wampirius.com


#include <SPI.h> // Master In Slave Out
#include <Wire.h> // Comunicación I2C
#include <Adafruit_GFX.h> // Pantalla OLED
#include <Adafruit_SSD1306.h> // Pantalla OLED
#include <DHTesp.h> // Sensor de temperatura y humedad
#include <WiFi.h> // ESP32 WiFi
#include <ThingSpeak.h> // Librería ThingSpeak
#include <ErriezMHZ19B.h> // Librería para pedir y obtener datos del sensor
// Librerías FreeRTOS para separar tareas, etc.
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"

// La mayoría de los códigos que comprende la librería Erriez son compatibles entre los tres sensores enumerados al principio.
// Consultar los datasheet para comprobar la compatibilidad
// Tal como dice su autor: Usar la librería con los códigos no documentados bajo la responsabilidad de cada cual
// https://github.com/Erriez/ErriezMHZ19B

// Pin sensor DHT
#define DHT_IN 4

#define MHZ19B_RX_PIN        16 // Pin serie RX
#define MHZ19B_TX_PIN        17 // Pin serie TX
#define mhzSerial           Serial2 // Usamos tercer hardware serial   

// Led exceso CO2
#define LED_AZUL 32

// Led gran exceso CO2
#define LED_ROJO 33

// Usaremos el led de la placa para el estado de la WiFi
#define LED_BUILTIN 2

// **** INTRODUCIR LOS DATOS DE WIFI Y THINGSPEAK ****
// AAAAA: Nombre de la WiFi
// BBBBB: Password de la Wifi
// 11111: Número del canal de ThingSpeak
// CCCCC: Write API Key del canal de ThingSpeak
// Los valores AAAAA, BBBBB y CCCCC han de ir entre comillas dobles, el valor 11111 ha de ir sin comillas.

// WIFI
const char* ssid = "AAAAA"; // SSID WiFi
const char* password = "BBBBB"; // Password WiFi

WiFiClient client; // Initializa la biblioteca Client

// Datos ThingSpeak
unsigned long myChannelNumber = 11111; // Canal Thingspeak
const char* myWriteAPIKey = "CCCCC"; // ThingSpeak Write API Key

// Datos pantalla
#define ANCHO_PANTALLA 128 // ancho pantalla OLED
#define ALTO_PANTALLA 64 // alto pantalla OLED

// Sensor DHT22
DHTesp dht;

// Create MHZ19B object
ErriezMHZ19B mhz19b(&mhzSerial);

// Semáforo mutex para intercambiar valores entre tareas
SemaphoreHandle_t mutex_v; 

// Variables globales para pasar los datos
float global_temperatura = 0;
float global_humedad = 0;
int global_co2 = 0;

// Objeto de la clase Adafruit_SSD1306
Adafruit_SSD1306 display(ANCHO_PANTALLA, ALTO_PANTALLA, &Wire, -1);

// Creamos las tareas para cada núcleo
TaskHandle_t Task0; //Tarea para el núcleo 0
TaskHandle_t Task1; //Tarea para el núcleo 1 -núcleo por defecto-

void setup() {

  // Initialize serial port to print diagnostics and CO2 output
    mhzSerial.begin(9600, SERIAL_8N1, MHZ19B_RX_PIN, MHZ19B_TX_PIN);

  // Configuramos el pin para el led de la placa
    pinMode(LED_BUILTIN, OUTPUT);

  // Configuramos el pin para el led azul de aviso
    pinMode(LED_AZUL, OUTPUT);

  // Configuramos el pin para el led rojo de mala calidad del aire
    pinMode(LED_ROJO, OUTPUT);

  // Sensor de temperatura
    dht.setup(DHT_IN, DHTesp::DHT22);

  // Iniciar pantalla OLED en la dirección 0x3C
    display.begin(SSD1306_SWITCHCAPVCC, 0x3C);

  // Limpiar buffer
    display.clearDisplay();

  // Tamaño del texto
    display.setTextSize(2);

  // Color del texto
    display.setTextColor(SSD1306_WHITE);

  // Enviar a pantalla
    display.display();

  // Creamos el semáforo
    mutex_v = xSemaphoreCreateMutex();

  // Creamos la tarea para el Core 0
    xTaskCreatePinnedToCore(
                    Task0code,   // Task function.
                    "Task0",     // name of task.
                    10000,       // Stack size of task
                    NULL,        // parameter of the task
                    1,           // priority of the task
                    &Task0,      // Task handle to keep track of created task
                    0);          // pin task to core 0                  

  // Creamos la tarea para el Core 1 -núcleo por defecto-
    xTaskCreatePinnedToCore(
                    Task1code,   // Task function.
                    "Task1",     // name of task.
                    10000,       // Stack size of task
                    NULL,        // parameter of the task
                    2,           // priority of the task
                    &Task1,      // Task handle to keep track of created task
                    1);          // pin task to core 0  

}

// Task0: En esta tarea llevaremos lo relativo a la Wifi
void Task0code(void *parameter){

  // Variables a usar para controlar la conexión y la subida de datos
  int conexion =0; // Lo usaremos para mostrar los intentos de conexión a la WiFi
  unsigned long subidaIntervalo = 300000; // Tiempo en milisegundos para subir los datos a ThingSpeak: 5 minutos (300000)
  unsigned long tiempo; // Variable en la que almacenaremos millis()
  unsigned long subidaUltima = 0; // Variable en la que almacenaremos el valor en milisegundos de la última subida

  //Variables en las que cargaremos los datos:
  float enviar_temperatura;
  float enviar_humedad;
  int enviar_co2;

  // Conexión WiFi
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  ThingSpeak.begin(client); // Inicia ThingSpeak

  // Esperamos conexión con WiFi
  while (WiFi.status() != WL_CONNECTED) 
  {
    // Usaremos el led de la placa para verificar la conexión
  	conexion++;
  	digitalWrite(LED_BUILTIN, HIGH);
  	vTaskDelay(200 / portTICK_PERIOD_MS);
  	digitalWrite(LED_BUILTIN,LOW);
  	vTaskDelay(300 / portTICK_PERIOD_MS);
    // Si al cabo de dos minutos no hemos conseguido conexión, apagamos el led de la placa y trabajaremos sin WiFi matando esta tarea
  	if(conexion > 240){
  		digitalWrite(LED_BUILTIN, LOW);
  		vTaskDelete(Task0);
  	}
  }
  // Cuando conectemos, encendemos el led
  Serial.println(WiFi.localIP());
  digitalWrite(LED_BUILTIN, HIGH);

  // Loop para ejecutar la subida de datos a la nube
  for(;;){

    // Subimos datos a ThingSpeak cada 5 minutos
    // Anotamos el tiempo actual
  	tiempo = millis();
    // Transcurrido el tiempo, llamamos al semáforo, cargamos los valores y efectuamos la subida
  	if((tiempo - subidaUltima) > subidaIntervalo){
  		
  		xSemaphoreTake(mutex_v, portMAX_DELAY);
  		enviar_temperatura = global_temperatura;
  		enviar_humedad = global_humedad;
  		enviar_co2 = global_co2;
  		xSemaphoreGive(mutex_v);
  		
  		ThingSpeak.setField(1, enviar_temperatura);
  		ThingSpeak.setField(2, enviar_humedad);
  		ThingSpeak.setField(3, enviar_co2);

      // Comprobamos la conexión WiFi
  		if (WiFi.status() != WL_CONNECTED) 
  		{
  			digitalWrite(LED_BUILTIN,LOW);
  			conexion = 0;
  			WiFi.disconnect();
  			vTaskDelay(200 / portTICK_PERIOD_MS);
  			WiFi.reconnect();
  			
        // Esperamos dos minutos a conseguir conexión y si no ejecutamos de nuevo el loop sin subir los datos para actualizar estos.
  			while ((WiFi.status() != WL_CONNECTED) && (conexion < 240))
  			{
            // Usaremos el led de la placa para verificar la conexión
  				conexion++;
  				digitalWrite(LED_BUILTIN, HIGH);
  				vTaskDelay(200 / portTICK_PERIOD_MS);
  				digitalWrite(LED_BUILTIN,LOW);
  				vTaskDelay(300 / portTICK_PERIOD_MS);
  				Serial.println(conexion);
  			}
  		}

      // Volvemos a comprobar si estamos conectados y en caso afirmativo subimos los datos y si no volvemos a ejecutar el bucle
  		if (WiFi.status() == WL_CONNECTED) 
  		{
  			digitalWrite(LED_BUILTIN, HIGH);
        // Subimos los datos a ThingSpeak
  			int x = ThingSpeak.writeFields(myChannelNumber, myWriteAPIKey);
  			if (x == 200) {
          // Si la subida ha tenido éxito anotamos el tiempo
  				subidaUltima = millis();
  			}
  		}
  	}

    // Este delay es necesario o se produce un error watchdog 
  	vTaskDelay(10 / portTICK_PERIOD_MS);

  }

}

// Task1: En esta tarea llevaremos lo relativo a toma de datos de los sensores
void Task1code(void *parameter){

  // Arrays y variables para almacenar 10 valores obtenidos por los sensores y sacar la media
    float serie_tem[10] = {0,0,0,0,0,0,0,0,0,0};
    float serie_hum[10] = {0,0,0,0,0,0,0,0,0,0};
    int serie_co2[10] = {0,0,0,0,0,0,0,0,0,0};
    float total_tem = 0;
    float total_hum = 0;
    int total_co2 = 0;
    float media_tem = 0;
    float media_hum = 0;
    int media_co2 = 0;
    int serie_contador = 0;

  // Con las siguientes variables controlaremos si hay algún error en las lecturas.
    char* hum;
    char* tem;
    char* aire;

  // Precalentamos durante 3 minutos el sensor
    int precalporcentaje = 0;
    int precalcontador = 0;
    while (mhz19b.isWarmingUp()) {
        //Serial.println(F("Warming up..."));
        display.clearDisplay();
        display.setCursor(0, 0);
        display.print("Calentando");
        display.setCursor(0, 30);
        display.print("Sensor ");
        display.print(precalporcentaje);
        display.print("%"); 
        display.display();
        precalcontador++;
        if(precalcontador==9){
            precalporcentaje+=5;
            precalcontador=0;
        }
        vTaskDelay(1000 / portTICK_PERIOD_MS);

    };


  // Loop para mostrar los datos por pantalla
    for(;;){

    // CO2
        int co2;
      // Minimum interval between CO2 reads is required
        if (mhz19b.isReady()) {
          // Read CO2 concentration
            co2 = mhz19b.readCO2();
        }

    // Temperatura y humedad
        float h = dht.getHumidity();
        float t = dht.getTemperature();

    // Comprobamos si hemos recibido datos o marcamos error
        if( co2 < 0){
            aire = "ERROR";
        }
        else{
            aire = "OK";
        }     
        if(isnan(h)){
            hum = "ERROR";
        }
        else{
            hum = "OK";
        }
        if(isnan(t)){
            tem = "ERROR";
        }
        else{
            tem = "OK";
        }

    // Mostramos los datos que se van tomando o si se produce algún error
        display.clearDisplay();
        display.setCursor(0, 0);
        display.print("Tmp: ");
        if( tem == "OK"){
            display.print(t);
        }else{
            display.print("ERROR");
        }
        display.setCursor(0, 20);
        display.print("Hum: ");
        if( hum == "OK"){
            display.print(h);
        }else{
            display.print("ERROR");
        }
        display.setCursor(0, 40);
        display.print("CO2: ");
        if( aire == "OK"){
            display.print(co2);
        }else{
            display.print("ERROR");
        }    
        display.display();

    // Calculamos la media de las lecturas, que será lo que subamos a ThingSpeak
    // Comprobamos que la primera lectura tenga datos correctos, si no, esperamos a la siguiente
        if ((serie_contador == 0) && (serie_co2[0] == 0)){
            if ((tem == "OK") && (hum == "OK") && (aire == "OK")){
                serie_tem[serie_contador] = t;
                serie_hum[serie_contador] = h;
                serie_co2[serie_contador] = co2;
                serie_contador++;
            }
        }else{
      //Si no es la primera lectura, cargamos los valores que tengamos, si falla alguno cargaremos el anterior
            int serie_anterior = serie_contador - 1;
            if(serie_contador == 10){
                serie_contador = 0;
            }
            if (tem == "OK"){
                serie_tem[serie_contador] = t;
            }else{
                serie_tem[serie_contador] = serie_tem[serie_anterior];
            }
            if (hum == "OK"){
                serie_hum[serie_contador] = h;
            }else{
                serie_hum[serie_contador] = serie_hum[serie_anterior];
            }
            if (aire == "OK"){
                serie_co2[serie_contador] = co2;
            }else{
                serie_co2[serie_contador] = serie_co2[serie_anterior];
            }
            serie_contador++;
        }

    // Almacenamos la media de las 10 últimas lecturas
    // Si tenemos un mínimo de 10 lecturas sacamos la media, en caso contrario tomamos el último valor
        if(serie_co2[9] > 0){

            for (int indice_datos = 0; indice_datos < 10; indice_datos++){
                total_tem += serie_tem[indice_datos];
                total_hum += serie_hum[indice_datos];
                total_co2 += serie_co2[indice_datos];   
            }
            
            media_tem = total_tem/10;
            media_hum = total_hum/10;
            media_co2 = ((total_co2/10)+0.5);
      // Reseteamos los totales
            total_tem = 0;
            total_hum = 0;
            total_co2 = 0;

        }else{
            if(tem == "OK"){
                media_tem = t;
            }
            if(hum == "OK"){
                media_hum = h;
            }
            if(aire == "OK"){
                media_co2 = co2;
            }
        }

    // Final del loop: Cargar las variables a subir a ThingSpeak en variables globales
    // Llamamos al semáforo, cargamos los valores a las variables globales y liberamos el semáforo
        xSemaphoreTake(mutex_v, portMAX_DELAY);
        global_temperatura = media_tem;
        global_humedad = media_hum;
        global_co2 = media_co2;
        xSemaphoreGive(mutex_v);


        // Esperamos 6 segundos y volvemos a tomar datos
        // (La librería Erriez establece un intervalo mínimo de 5 segundos para pedir datos al sensor)


        // *** LEDS ***
        // Si no queremos usar los leds, descomentar la siguiente línea y comentar el fragmento CONTROL LEDS

        //vTaskDelay(6000 / portTICK_PERIOD_MS);

    // *** CONTROL LEDS  ***
        if (co2<700){
            vTaskDelay(6000 / portTICK_PERIOD_MS);
        }else if (co2<900){
            for (int t=0; t<6; t++){
                digitalWrite(LED_AZUL,HIGH);
                vTaskDelay(200 / portTICK_PERIOD_MS);
                digitalWrite(LED_AZUL,LOW);
                vTaskDelay(200 / portTICK_PERIOD_MS);
                digitalWrite(LED_AZUL,HIGH);
                vTaskDelay(200 / portTICK_PERIOD_MS);
                digitalWrite(LED_AZUL,LOW);
                vTaskDelay(400 / portTICK_PERIOD_MS);
            }
        }else{
            for (int t=0; t<6; t++){
                digitalWrite(LED_AZUL,HIGH);
                vTaskDelay(500 / portTICK_PERIOD_MS);
                digitalWrite(LED_AZUL,LOW);
                digitalWrite(LED_ROJO,HIGH);
                vTaskDelay(500 / portTICK_PERIOD_MS);
                digitalWrite(LED_ROJO,LOW);
            }
        }
    // *** FIN CONTROL LEDS ***

    }
}

// Si lo programamos con el IDE de Arduino, tenemos que meter un loop vacío o dará error
void loop(){

}

 

Aquí está para su descarga. Hay que descomprimir y colocar la carpeta y el archivo donde tengamos nuestros desarrollos de Arduino, abrirlo y colocar los cuatro datos relativos a la WiFi y al canal de ThingSpeak.

Con esto ya se puede realizar el montaje, programar la tarjeta y ponerlo en funcionamiento. Vamos a ello:

Sin conectar todavía nada podemos comprobar si el código se compila bien y sin errores:

Bueno, pues el primer paso está superado. Ahora conectamos la tarjeta con el ESP32 para programarla. Si es la primera vez el sistema realizará la instalación necesaria para asignar un puerto COM. En mi portátil me ha tocado en suerte el COM 3:

El siguiente paso es configurar la velocidad. El ESP32 habla rapidito, así que podemos configurar la velocidad en 115200 tanto en el sistema como en el IDE:

Y con esto ya tenemos todo para grabar la tarjeta, pero un pequeño apunte: En algunas tarjetas es tan sólo dar a “subir” y listo, pero en otras será necesario pulsar el botón “BOOT” y mantenerlo hasta que comience la grabación de datos. También tengo por aquí alguna especialmente dura de mollera, en la que en ocasiones hay que insistir o pulsar el otro botón para forzar un reinicio e inmediatamente pulsar boot.

Botón BOOT: En algunas placas habrá que pulsarlo cuando el IDE comience a “intentar” subir el código tras realizar la compilación y hasta que veamos que en efecto comienza a subirse.

Y ya está. Con todo esto, a funcionar.

Ahora, por si no han llegado todavía las piezas compradas en el “lejano oriente”, vamos a comentar un poco el código, por matar el rato…

Comentarios al código

Bien, como ya he comentado al inicio de esta entrada, al principio programé el engendro “a lo Arduino”: un setup, un void y a funcionar. Y funcionaba.

Paralelamente seguí documentándome sobre el ESP32, ya que para mí era completamente nuevo, y me topé con este vídeo:

https://youtu.be/l3LR_ljKEPQ

Así que cuando surgieron los problemas y vi que tenía que retocar el código, pensé que el camino correcto era usar los dos núcleos para separar las tareas. Encontré más información al respecto aquí:

https://savjee.be/2020/01/multitasking-esp32-arduino-freertos/

En las líneas 90 y 91 se creaan las tareas
Estas se definen en el setup utilizando los nombres que les hemos dado antes
Y después cada tarea tiene una primera parte que, por llamarlo de algún modo, sería el equivalente al setup si estuviéramos programando como normalmente hacemos en Arduino
Y luego establecemos un bucle infinito. Aquí lo hemos hecho con for(;;) pero podría hacerse con while (1) o while (true), etc. A gustos. El caso es que este bucle sería el equivalente al habitual loop de Arduino

En cuanto al problema de la WiFi, de tres montajes que puse en funcionamiento sólo uno experimentó problemas, y la programación era la misma. Buscando por la red me quedé con la simple solución que aparece aquí:

https://randomnerdtutorials.com/solved-reconnect-esp32-to-wifi/

Definimos la conexión WiFi en la tarea correspondiente
Y cuando la necesitamos comprobamos su estado y si no estamos conectados desconectamos y reconectamos.

Ni siquiera eché mano de los eventos, simplemente cuando hay que subir datos se comprueba el estado de la WiFi y si es preciso se reconecta. Y parece que ahora funciona. Probé con un montaje y le puteé hasta la saciedad en casa, primero a idea, apagando y encendiendo el router, y después de forma involuntaria, ya que tuve a los tres sensores en la ventana un día entero para que se “autocalibraran” y como allí la señal de la Wifi llegaba llorando, pues iba viendo cómo a ratos parpadeaba un led u otro.

Bien, pues ya tenía una tarea para la lectura de los sensores y mostrar los datos por pantalla y otra para subir estos datos por WiFi y lo que se me ocurriera más adelante. Para pasar los datos de una tarea a otra era lógico: había que usar variables globales.

El vídeo mencionado anteriormente tiene una continuación:

https://youtu.be/_JzfJi2z1CY

Sin embargo, la forma de resolver el problema de que podamos acceder a la vez a la variable global desde las dos tareas no me convence cómo está resuelto en ese vídeo. Vamos a ver, una tarea es la que se encarga de las lecturas y la que tiene que escribir en las variables globales los datos para que estén disponibles cuando la otra tarea tenga que acceder a esas variables para leer los datos y subirlos a la nube. El proceso descrito en el vídeo no garantiza que en un momento dado podamos intentar leer y escribir a la vez en la misma variable y eso no sé qué consecuencias podría tener, pero de pasar algo, seguro que no pasaba nada bueno.

Pero ojo, que esto no quite ni un ápice al mérito de esos vídeos. Gracias a ellos aprendí bastante de cómo poder asignar tareas distintas a cada núcleo del ESP32. Y encima en nuestro idioma, es muy de agradecer.

Buscando y buscando, encontré que la solución era usar “semáforos”. Una buena explicación está aquí:

https://circuitdigest.com/microcontroller-projects/arduino-freertos-tutorial-using-semaphore-and-mutex-in-freertos-with-arduino

Básicamente consiste en lo siguiente: Definimos un semáforo y cuando una tarea lo llama lo pone en rojo, hace lo que sea, y libera al semáforo poniéndolo de nuevo en verde. Si mientras está el semáforo en rojo es llamado por otra tarea, esta se para hasta que encuentra la luz verde, momento en el que toma el semáforo poniéndolo en rojo, realiza lo que tenga que hacer y lo devuelve poniéndolo de nuevo en verde.

Uso de los semáforos: Primero lo creamos
En una tarea lo usamos para guardar los datos en las variables globales
Y en la otra tarea lo usamos para leer los datos de las variables globales

El concepto de semáforo hace que sea muy fácil de entender, ya que todos sabemos cómo funciona un semáforo en la vida real. Así, con una tarea llamamos al semáforo, escribimos en las variables globales y liberamos el semáforo; y con la otra lo mismo para la lectura de esas variables. Problema resuelto.

Aún quedaban por resolver un par de problemas. Con este nuevo script apareció un error: “task watchdog got triggered” creo recordar, que hacía que la placa se reiniciara una y otra vez. Buscando y buscando encontré un “apaño”, que no solución, aquí:

https://github.com/espressif/arduino-esp32/issues/595

En ese hilo se dan varias soluciones. La que ha funcionado es poner un delay de 10 milisegundos al final de la tarea del núcleo 0. No me parece que eso suene a solución, aunque en este desarrollo funciona, todavía tengo pendiente el documentarme más sobre el particular.

O ponemos este delay o se cabrea al perro (aunque no me parece que sea una solución sino un apaño, pero en este script funciona)

Por si acaso, aunque se está ejecutando una tarea, y sólo una, en cada núcleo, pensé que no sobraba el establecer prioridades diferentes a cada una de ellas. No sé si esto hará algo o no, pero no sobra:

Task0 con prioridad 1 y Task1 con prioridad 2. Contra más alto es el número mayor es la prioridad.

Otro de los problemas era que al estar en el IDE de Arduino debemos de poner un loop aunque esté vacío, o nos dará error. Tampoco es algo que me apasione, pero mientras estemos usando este IDE es lo que toca.

Esto del final tampoco me apasiona, pero con el IDE de Arduino es necesario

En cuanto a la toma de datos del sensor, como ya he comentado al principio, inicialmente lo hacía leyendo la señal PWM y estableciendo una simple regla de tres. Como sospecho, y sólo sospecho porque aún no tengo evidencia que lo demuestre, que este método no es del todo fiable, decidí hacer las cosas bien y leer los datos mediante UART.

Tenía dos opciones, o programar unas cuantas líneas o buscar una librería ya hecha. Y encontré esta:

https://github.com/Erriez/ErriezMHZ19B

Estuve haciendo unas cuantas pruebas con la variante que lee la temperatura y funciona de maravilla. Como llevo pensado incorporar alguna funcionalidad más al montaje, me decidí por esta librería que facilita muchísimo la faena.

La única diferencia es que en el desarrollo que he presentado prescindo de SoftwareSerial, que es el usado en los ejemplos de esta librería. Directamente tenemos un Serial 2 sin usar en la tarjeta ESP32, así que hacemos un par de mínimos cambios para usar la librería de Erriez sin necesidad de usar la de SoftwareSerial. Lo vemos a continuación.

En este link tenemos un ejemplo de cómo el autor usa su librería:

https://github.com/Erriez/ErriezMHZ19B/blob/master/examples/ErriezMHZ19BGettingStarted/ErriezMHZ19BGettingStarted.ino

Para evitar el uso de SoftwareSerial hemos realizado los siguientes cambios (ponemos primero en blanco las capturas del código del autor de la librería y debajo los cambios efectuados):

Asignamos los pines correspondientes al Serial 2

 

El objeto lo creamos igual

 

A la hora de inicializar el puerto definimos su funcionamiento.

Y listo, ya tenemos la librería de Erriez lista para usar por el Serial 2 del ESP32.

En cuanto a la toma de datos, esta librería de Erriez establece un intervalo mínimo de 5 segundos para pedir los datos al sensor, en el script propuesto nosotros establecemos las lecturas cada 6 segundos. Además, para subir los datos a ThingSpeak se han creado unos arrays que almacenan las últimas 10 lecturas y la variable global almacena la media de esas lecturas, de forma que a ThingSpeak mandamos la media de lecturas del último minuto.

Cada vez que se toman las lecturas se almacena el dato en el Array, así que, ante una posible desconexión, siempre tendremos disponible el dato de la media del último minuto. Esto es así porque ThingSpeak determina cuándo se ha subido un dato, no mandamos nosotros el dato de fecha y hora. Si en lugar de ThingSpeak subiéramos los datos a una web propia en la que creáramos las gráficas con los datos de tiempo aportados por nosotros podríamos realizar la programación de forma distinta, para que no se perdiera ningún dato y la gráfica no se viera afectada, pero en ThingSpeak no es posible (que yo sepa).

Y bueno, creo que esta entrada ya ha quedado lo suficientemente larga como para proporcionar un plácido sopor al más insomne, así que ponemos el punto y aparte, que no el final, porque tengo en el tintero alguna cosilla curiosa respecto a la auto calibración de los sensores y alguna idea más para la versión 3.0 que será objeto de siguientes entradas.

Pero antes de acabar, aunque me he asegurado de que el código funciona correctamente y está requeteprobado, seguro, seguro, que hay formas más eficientes de llevarlo a cabo. Lo mostrado anteriormente no es más que lo aprendido durante estas últimas semanas, mucha prueba y error, pero al fin y al cabo lo aprendido sobre la marcha y que seguro es mejorable, aunque sin embargo…

Hasta la próxima!

2 respuestas a «Montaje y código»

  1. Muy bueno el tuto. Finalmente localicé uno que hiciera referencia a algun proyecto sobre este sensor. Me ha costado un montón hacerlo funcionar, pero finalmente lo conseguí. Pero me encuentro con algunos problemas:

    1.- Para que me funcione, he tenido que conectar RX del sensor con RX de la ESP32. Pero de tanto en tanto, según cargo el sketch, me da error en la lectura. Entonces cruzo RX y TX, y da lectura.
    Otras veces, también después de cargar el sketch, me da una lectura estratosférica, del orden de 1200, 5000, etc, y los dos LED externos parpadean alternativamente (el LED azul de la placa no se ha encendido nunca, pero ThingSpeak recibe datos, no se li correctos, pero algo recibe.

    2.- Ahora, la lectura es de 401 y LEDs apagados. El valor que aparece en ThingSpeak, es de 29.6. A que se debe esta diferencia? Deberían señalar el mismo, no?

    Podrías indicarme sobre qué valores deberían moverse los valores en OLED y en Thing?

    Estaré muy agradecido si me respondes y me puedes aclarar estas dudas.

    La web es una pasada y voy a seguirte con otros proyectos que has publicado.

    Gracias por la atención y la ayuda

    Salu2 cordiales

    1. Hola Jaume

      ¡Muchas gracias por tu comentario sobre la web! 🙂

      Respecto al montaje que has realizado, vamos por partes, a ver si te puedo echar una mano:

      Una cosa está clara: RX tiene que ir a TX y viceversa. TX es transmisión y RX recepción, eso tiene que ser así. Yo, uno de los sensores que tenemos en funcionamiento de vez en cuando le pasan estas cosas, que cuando le da la gana da error. Afortunadamente tenía tres montajes y cambiando piezas pude determinar que era el sensor, que está “medio” cascao. Espero que no sea tu caso. No lo descartes, pero no lo tires aún a la basura.

      ¿Es un MH-Z19B o un “C”? Si es un “C”, la fuente de alimentación tiene que ir muy muy fina, o hará cosas raras. En el siguiente link tienes información, al final, y un enlace para un estabilizador.

      https://emariete.com/sensor-co2-mh-z19b/

      Es muy posible que conocieras ya el blog que te menciono, pero si no, no te lo pierdas, es una pasada ¡Ese tipo sí que sabe!

      ¿Alimentas el sensor y la placa directamente por los pines o por el puerto microusb? Ya me dirás. Y sobre todo mucho ojo con eso, no te despistes y lo conectes por el usb al ordenador y tengas a la vez el alimentador conectado que se puede liar muy parda. Ojo.

      Lecturas de 1200, o más, son posibles según dónde lo tengas. Por ejemplo: hay armarios roperos más grandes que el cuarto donde me encuentro, pues bien, en invierno, con la ventana cerrada, subirlo por encima de ese valor no era nada difícil, aunque no a 5000 salvo que le “torturara” a idea.

      Que en la pantalla aparezca 401 y en ThingSpeak 29.6 sí que no tiene ningún sentido salvo que a ThingSpeak le esté llegando otro valor, como la temperatura ¿has creado los tres campos en ThingSpeak? El sketch está preparado para enviar al campo 1 la temperatura, al campo 2 la humedad y al 3 el CO2. A ThingSpeak le llega el valor medio de las últimas 10 lecturas, por lo que, por norma general, será muy parecido a lo que veas en la pantallita.

      Y ya, por último, asegúrate de que las patillas de tu placa coinciden con el esquema, que hay varios modelos de placas con el ESP32 y en algunas cambian. Te comento esto último porque me parece extraño que no se encienda el led azul de la placa…

      En fin, es un poco difícil saber exactamente qué está fallando, pero espero que al menos algo de lo dicho te haya podido dar alguna pista.

      Ya me dirás si hay suerte… Y si no, aquí seguiremos para seguir buscando 😉

      Un cordial saludo.

Deja una respuesta

No está permitida la inserción de ningún dato de carácter personal (mail, tef...). Cualquier comentario que los contenga será editado o eliminado.

Por favor, si dejas un comentario pon cualquier nombre para poderme dirigir a ti pero que no te identifique

El comentario es totalmente anónimo. No se almacena la IP