SETUP del entorno de programación con Arduino IDE

Tras las instalación del Arduino IDE, es necesario indicarle al entorno que se va a trabajar con placas del fabricante Espressif, en concreto con la Wemos D1 mini Lite. Para ello:

Ir a Archivo, Preferencias, Gestor de URLs adicionales de tarjetas y añadir la línea:

  • http://arduino.esp8266.com/stable/package_esp8266com_index.json

Luego ir a Herramientas, Gestor de tarjetas y seleccionar la D1 mini Lite.

Librerías necesarias para el medidor de CO2

  • Thingspeak (incluido el código fuente).
  • Adafruit_SSD1306 (con todas sus dependencias).
  • Adafruit_NeoPixel.
  • DHT sensor library by Adafruit (con todas sus dependencias).
  • Incluir como librería .ZIP: WiFiManager-master by tzapu.

En caso de usar entorno Linux

Para poder utilizar el puerto USB asignado a la placa Arduino en Linux debemos agregar a nuestro usuario el grupo especial creado para el acceso al device tty asignado al puerto USB del Arduino,el grupo es el dialout de otra manera no podremos subir el código a la placa, obteniendo errores asociados a denegación de acceso al puerto por no tener los permisos requeridos.

Conectamos la placa y desde el IDE verificamos el puerto asignado Tools > Port, en mi caso el device es el /dev/tty/ACM0

verificamos el grupo asociado al dispositivo:

$ ls -all /dev/tty/ACM0
crw-rw—- 1 root dialout 166, 0 Feb 21 19:57 /dev/ttyACM0

agregamos a nuestro usuario al grupo dialout:

$ sudo usermod -a -G dialout $USER

debemos hacer logout para que el sistema tome los cambios y así poder acceder al puerto sin tener que levantar el IDE como usuario root.

 

Organización del código

El código actual funcionando con el servidor de Thingspeak tira de varias librerías incluidas de diferente forma, como se comentaba anteriormente:

  • WiFiManager-master.zip.
  • ThingSpeak.cpp y ThingSpeak.h como archivos de código y cabecera de C++. Prácticamente todo el código están en el archivo de cabecera, ligeramente modificado respecto a la versión original para apuntar al servidor del Politécnico. Esto está documentado en las primeras líneas del archivo.

Además cuenta con varios archivos que completan la funcionalidad:

  • medidor-v4-Thingspeak.ino. Este es el archivo principal de código, con amplia documentación interna.
  • secrets.h. Este es el archivo que contiene las claves para abrir los canales junto con las APIKEY, algo así como el login para poder enviar los datos.

 

Código principal para conectar con Thingspeak

A continuación se incluye el contenido de los archivos realizados para programar los medidores comenzando por la modificación del código fuente de la librería ThingSpeak.

 

ThingSpeak.h

// #define PRINT_DEBUG_MESSAGES

// #define PRINT_HTTP

#ifndef ThingSpeak_h

    #define ThingSpeak_h

    #define TS_VER "2.0.0"

    #include "Arduino.h"

    #include <Client.h>

// Servidor Thingspeak por defecto: "api.thingspeak.com" || Servidor del Politécnico:"thingspeak.politecnicomalaga.com"

// Puerto   Thingspeak por defecto: "80"                 || Puerto del Politécnico:  "8080"        

// Puerto HTTPS Thingspeak por defecto: "443"            || Puerto HTTPS del Politécnico:  "8443"

//Aquí hay que pegar lo que corresponda de los comentarios de arriba

    #define THINGSPEAK_URL "thingspeak.politecnicomalaga.com"              

    #define THINGSPEAK_PORT_NUMBER 8080                

    #define THINGSPEAK_HTTPS_PORT_NUMBER 8443

medidor-v4-Thingspeak.ino

Con comentarios dentro del propio código, esta es la fuente principal de la que tiran los medidores de CO2.

#include <SoftwareSerial.h>     //Librería para crear puerto serie software en otros pines

#include <Wire.h>               // Librería para comunicar la placa arduino con dispositivos que trabajan mediante el protocolo I2C/TWI

#include <Adafruit_SSD1306.h>   // Librería para controlar la OLED monócroma de Adafruit basada en los drivers SSD1306  https://github.com/adafruit/Adafruit_SSD1306

#include <Adafruit_GFX.h>       // Librería con para control de funciones gráficas de la OLED https://github.com/adafruit/Adafruit-GFX-Library

#include <Adafruit_NeoPixel.h>  // Librería para controlar el led RGB WS2812B

#include <DHT.h>                // Librería del sensor de temperatura y humedad DHT22 basado en el AM2302

#include <ESP8266WiFi.h>        // Librería de control de la WiFi para el ESP

#include <DNSServer.h>          // Librería para montar lo relativo al DNS en la conexión a la red

#include <WiFiManager.h>        // Librería para manejar la WiFi de forma sencilla con el móvil por ejemplo y no tener hardcoded los datos de la red https://github.com/tzapu/WiFiManager

#define TS_ENABLE_SSL           // Para hacer el envío seguro de datos

#include "ThingSpeak.h"         // Librería para subir los datos a la nube IoT de ThingSpeak.com

#include "secrets.h"            // Archivo añadido al proyecto para no tener las contraseñas en el mismo archivo de código. Con la WiFi sólo lo he usado al principio con una red fija

// Con las credenciales para subir a la nube IoT es necesario mantenerlo.

//PARÁMETROS SUSCEPTIBLES DE MODIFICARSE. Hay que tocar aquí en función de lo que hay en el archivo secrets.h y también al inicio de la librería ThingSpeak.h para escoger URL y puerto//

#define INDICE_MEDIDOR 0          // Mediante este valor elijo uno de los medidores cuyos datos están almacenados en el archivo  secrets.h

#define DELTA_CO2 0               // Marco un offset (+16 en abril de 2021) a sumar al mínimo de CO2 medido, ya que el sensor toma 400 como base y realmente la base es superior. Datos de https://gml.noaa.gov/ccgg/trends/global.html

#define FACTOR_CORRECCION 0       // Factor de offset a sumar para los medidores que se vean que difieren del valor correcto. Por ahora solo el medidor 4 tiene un offset de -90

#define MIN_CO2 400               // Marco el mínimo válido del sensor para filtrar datos erróneos

#define MAX_CO2 5000              // Marco el máximo válido del sensor para filtrar datos erróneos

#define NIVEL_1 500               // Primer nivel de CO2 a considerar

#define NIVEL_2 600               // Segundo nivel de CO2 a considerar

#define NIVEL_3 700               // Tercer nivel de CO2 a considerar

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Definir constantes

#define ANCHO_PANTALLA 128        // Ancho pantalla OLED

#define ALTO_PANTALLA 64          // Alto pantalla OLED

#define DHTTYPE DHT22             // DHT 22  (AM2302), AM2321

#define NUMPIXELS 1               // Modificamos este numero según los LEDs con los que contemos

#define UPDATE_TIME_LOCAL 5000    // Intervalo en ms para pedir datos a sensores

#define UPDATE_TIME_REMOTE 60000  // Intervalo en ms para enviar datos a la nube de Thingspeak

 

#define PINDHT D4           // Pin D4 al que conecto el sensor de temperatura y humedad (GPIO2)

#define PINLEDRGB D3        // Pin D3 al que conecto el DIN del primer led en la cadena (GPIO0)

#define PINBUZZER D8        // Pin D8 al que conecto el buzzer pasivo (GPIO15)

#define PINTXSENSOR D6      // Pin D6 al que conecto el RX del micro cruzado con el TX del sensor (GPIO12)

#define PINRXSENSOR D7      // Pin D7 al que conecto el TX del micro cruzado con el RX del sensor (GPIO13)

//El pin D1 (GPIO5) es SCL y el pin D2 (GPIO4) es SDA y van conectados a esas conexiones de la OLED. La encuentra por la dirección física I2C

unsigned long myChannelNumber = medidoresCanales[INDICE_MEDIDOR];   //canal a usar

const char * myWriteAPIKey = medidoresAPIKEY[INDICE_MEDIDOR];       //APIKEY del canal a usar

char ssid[] = SECRET_SSID;                              //mi SSID (nombre de la red)

char pass[] = SECRET_PASS;                              //el password de mi red

byte mac[6];                                            //la dirección física (MAC) del interfaz WiFi

unsigned long tiempo = 0;                                             // Variable para controlar el tiempo de ejecución

//WiFiClient client;                                                    // Cliente de WiFi

WiFiClientSecure client;                                                // Cliente seguro de WiFi

DHT dht(PINDHT, DHTTYPE);                                             // Objeto para refenciar al sensor de temperatura y humedad de la clase DHT

Adafruit_SSD1306 display(ANCHO_PANTALLA, ALTO_PANTALLA, &Wire, -1);   // Objeto para refenciar a la pantalla OLED de la clase Adafruit_SSD1306

Adafruit_NeoPixel pixels(NUMPIXELS, PINLEDRGB, NEO_GRB + NEO_KHZ800); // Objeto para refenciar al RGB de la clase Adafruit_NeoPixel

SoftwareSerial sensor(PINTXSENSOR, PINRXSENSOR);                      // Objeto para crear un puerto serie virtual de la clase SoftwareSerial

// Usamos el pin 12 (D6) para Rx del micro y el pin 13 (D7) para Tx del micro. Están cruzados con la UART del sensor

byte readCO2[] = {0xFE, 0X44, 0X00, 0X08, 0X02, 0X9F, 0X25};  //Command packet to read Co2 (see app note)

byte response[] = {0, 0, 0, 0, 0, 0, 0};                      //create an array to store the response

int valMultiplier = 1;                                        //multiplier for value. default is 1. set to 3 for K-30 3% and 10 for K-33 ICB

int nivelAlarma = 0;                  // Sirve para establecer el color del led RGB y el pitido del zumbador si se superan unos ciertos umbrales

WiFiManager wm;                       // Creamos una instancia de la clase WiFiManager para controlar el acceso a una red cualquiera.

 

void setup()

{

  Serial.begin(115200);

  delay(200);

  // Para conectar a la WiFi se puede hacer "a saco" con la clase WiFi o usando la librería WiFiManager (wm), que crea un portal en el que configurar el acceso

  // y se puede hacer desde el móvil por ejemplo

  WiFi.mode(WIFI_STA);                // Establecemos el modo STATION explícitamente, ya que el ESP lo pone por defecto a STA+AP

  //wm.resetSettings();               // Reseteo settings - limpiar credenciales para testeo; Si descomento esta línea habría que meter los datos de la WiFi cada vez que arranca

  wm.setConfigPortalBlocking(false);  // lo marcamos como no bloqueante; es decir, se puede configurar o no la WiFi, pero el resto sigue funcionando

  // Automaticamente se conecta usando las credenciales salvadas por defecto (si antes se había conectado)

  // Si la conexión falla arranca un AP con el nombre que se especifica

  // Si no salta el inicio de sesión hay que abrir en el navegador la IP 192.168.4.1

  // Si salta, estás directamente en el mismo punto. Ahí solo queda seleccionar tu red y meter tu contraseña

  // Esto sólo hay que hacerlo una vez, luego se queda guardado

  if (wm.autoConnect("Medidor 4.0.1")) {

    Serial.println("Conectado a la red");

  }

  else {

    Serial.println("Configportal running");

  }

  // Begin WiFi. Esto sería para una conexión fija y que no haya que configurarla

  //  WiFi.begin(ssid, pass);                       //Arranco WiFi

  // Connecting to WiFi...

  //  Serial.print("Connecting to ");

  //  Serial.println(ssid);

  client.setInsecure();                         //El cliente no verifica al servidor

  ThingSpeak.begin(client);                     //Arranco cliente de Thingspeak

  Serial.println("Iniciando pantalla OLED");    //Arranco OLED

  // Iniciar pantalla OLED en la dirección 0x3C

  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {

    Serial.println("No se encuentra la pantalla OLED");

    while (true);

  }

  sensor.begin(9600);     //Arranco sensor de CO2

  dht.begin();            //Arranco DHT22

  pixels.begin();         //Arranco led RGB

  tiempo = millis();      //Tomo referencia inicial de tiempo

}

void loop()

{

  wm.process();                     // Portal Web para gestionar la conexión a la red

  sendRequest(readCO2);             // Mando petición al sensor para medir CO2

  int valCO2 = getValue(response);  // Recojo la respuesta del sensor

  valCO2 += DELTA_CO2 + FACTOR_CORRECCION;              // Añado el offset necesario para tener datos más reales

  float h = dht.readHumidity();     // Lectura de humedad del sensor. Toma unos 250 ms

  float t = dht.readTemperature();  // Lectura de temperatura del sensor. Toma unos 250 ms

 

  valCO2 = (valCO2 > MAX_CO2) ? MAX_CO2 : valCO2;                     //Saturo el máximo del valor de CO2 para filtrar posibles errores en los que se va al fondo de escala del tipo de dato

  valCO2 = ((valCO2 > MIN_CO2 * 0.97) && (valCO2 < MIN_CO2)) ? MIN_CO2 : valCO2; //Saturo el mínimo del valor de CO2 para filtrar posibles errores (margen del sensor de +-50ppm / +-3%)

  actualizoLedRGB(valCO2);

  mostrarEnDisplay(valCO2, h, t);

  actualizoSonidosAlarma(valCO2,nivelAlarma);

  delay(UPDATE_TIME_LOCAL);

}


/*

   Función para mandar los datos al display OLED

*/

void mostrarEnDisplay(int nivelCO2, float hum, float tem)

{

  // Limpiar buffer

  display.clearDisplay();

  // Color del texto

  display.setTextColor(SSD1306_WHITE);

  // Tamaño del texto

  display.setTextSize(1);

  // Escribir texto

  if (!isnan(tem)) {

    // Posición del texto

    display.setCursor(0, 0); //distancia en pixeles empezando por la izquierda /  distancia en pixeles empezando por arriba

    display.print(tem);

    display.print(" C ");

  }

  if (!isnan(hum)) {

    display.setCursor(80, 0); //distancia en pixeles empezando por la izquierda /  distancia en pixeles empezando por arriba

    display.print(hum);

    display.println(" %");

  }

  // Tamaño del texto

  display.setTextSize(2);

  // Posición del texto

  display.setCursor(20, 16);

  // Actualiza datos en display si está dentro de los márgenes. El margen inferior tiene un 3% extra para cubrir un margen de error

  if ((nivelCO2 >= MIN_CO2) && (nivelCO2 < MAX_CO2))

  {

    display.print(nivelCO2);

    display.println(" ppm");

  }

  else

  {

    display.println("Midiendo");

  }

  // La MAC se puede escribir en cualquier caso por si tenemos un filtrado MAC en la red, aquí la veremos fácilmente

  display.setTextSize(1);

  display.setCursor(0, 56);

  display.print("MAC:");

  display.print(WiFi.macAddress());

  // Datos que dependen de que exista conexión

  if ((WiFi.status() == WL_CONNECTED))

  {

    // Conectado a la WiFi. Aquí ya puedo escribir la IP y mandar datos a la nube

    display.setCursor(8, 48);

    // Tamaño del texto

    display.setTextSize(1);

    display.print("IP: ");

    display.println(WiFi.localIP().toString());

    if ( (millis() - tiempo > UPDATE_TIME_REMOTE)             //sólo mando actualizaciones cada 30 segundos (para pruebas)

         && (nivelCO2 >= MIN_CO2) && (nivelCO2 < MAX_CO2) )   //sólo se mandan si está dentro de los mismos márgenes definidos para el display

    {

      subirInternet(nivelCO2, hum, tem);                      // subo los datos a la nube

    }

  }

 

  /* Esto era cuando había buzzer para controlar la información del dispositivo

  if (digitalRead(PINBUZZER)==HIGH)

  {

      mostrarInfoDispositivo();

  }

  */

 

  // Enviar a pantalla

  display.display();

}

/*

   Función para mostrar datos del dispositivo al cortocircuitar el pin 8 (el del buzzer) con Vcc

*/

void mostrarInfoDispositivo()

{

      // Limpiar buffer

      display.clearDisplay();

      // Color del texto

      display.setTextColor(SSD1306_WHITE);

      // Tamaño del texto

      display.setTextSize(1);

      display.setCursor(0, 0); //distancia en pixeles empezando por la izquierda /  distancia en pixeles empezando por arriba

      display.println("Info del dispositivo");

      display.println(medidoresNombres[INDICE_MEDIDOR]);

      display.print("U:"); display.print(THINGSPEAK_URL);display.print(": "); display.println(THINGSPEAK_PORT_NUMBER);

      display.print("Canal: "); display.println(medidoresCanales[INDICE_MEDIDOR]);

      display.print("KEY: "); display.println(medidoresAPIKEY[INDICE_MEDIDOR]);

      delay(3000);

}

 

/*

   Función para subir datos a la nube

*/

void subirInternet(int nivelCO2, float hum, float tem)

{

  if (myChannelNumber == 0)

  {

    Serial.println("Sólo muestra datos en local. No se sube nada");

  }

  else

  {

    Serial.println("Mando datos a la nube");

    tiempo = millis();

    // Carga los valores a enviar

    ThingSpeak.setField(1, nivelCO2);

    ThingSpeak.setField(2, tem);

    ThingSpeak.setField(3, hum);

    // Escribe todos los campos a la vez.

    int respuestaServidor = ThingSpeak.writeFields(myChannelNumber, myWriteAPIKey);

    if (respuestaServidor==200)

    {

      Serial.println("Datos en la nube correctamente");

    }

  }

}

/*

   Función para llamar al buzzer según el nivel de alarma (defcon) que haya

*/

void actualizoSonidosAlarma(int nivelCO2, int defcon)

{

  if ((nivelCO2 >= MIN_CO2) && (nivelCO2 < MAX_CO2))    //Suenan alarmas si está dentro de los márgenes lógicos, pero por encima del nivel deseado

  {

    switch (defcon) {

      case 2:

        tonoCorto();

        break;

      case 3:

        tonoLargo();

        break;

      default:

        break;

    }

  }

}

/*

   Función para actualizar el color del led RGB

*/

void actualizoLedRGB(int nivelCO2)

{

  pixels.clear();                                 // Al principio apagamos el(los) LED

  int indicePixel = 0;                            // Me fijo en el único que tengo

  int red = 0; int green = 0; int blue = 100;           //Inicialmente en azul

  if (nivelCO2 <= NIVEL_1)                              //verde

  {

    red = 0; green = 255; blue = 0;

    nivelAlarma = 0;

  }

  else if ((nivelCO2 > NIVEL_1) && (nivelCO2 <= NIVEL_2)) //amarillo

  {

    red = 150; green = 150; blue = 0;

    nivelAlarma = 1;

  }

  else if ((nivelCO2 > NIVEL_2) && (nivelCO2 <= NIVEL_3)) //naranja

  {

    red = 200; green = 150; blue = 0;

    nivelAlarma = 2;

  }

  else if (nivelCO2 > NIVEL_3)                          //rojo

  {

    red = 255; green = 0; blue = 0;

    nivelAlarma = 3;

  }

  pixels.setPixelColor(indicePixel, pixels.Color(red, green, blue));

  pixels.show();   // Mandamos todos los colores con la actualización hecha

}

/*

   Función que genera un par de tonos cortos

*/

void tonoCorto() {

  //generar tono de 440Hz durante 200 ms

  tone(PINBUZZER, 440);

  delay(200);

  //detener tono durante 100ms

  noTone(PINBUZZER);

  delay(100);

  //generar tono de 523Hz durante 300ms, y detenerlo durante 200ms.

  tone(PINBUZZER, 523, 300);

  delay(200);

}

/*

   Función que genera un par de tonos largos

*/

void tonoLargo() {

  //generar tono de 440Hz durante 1000 ms

  tone(PINBUZZER, 440);

  delay(1000);

  //detener tono durante 500ms

  noTone(PINBUZZER);

  delay(500);

  //generar tono de 523Hz durante 1000ms, y detenerlo durante 500ms.

  tone(PINBUZZER, 523, 1000);

  delay(500);

}

/*

   Función que realiza la petición de datos al sensor de CO2

*/

void sendRequest(byte packet[])

{

  while (!sensor.available())     //keep sending request until we start to get a response

  {

    sensor.write(readCO2, 7);

    delay(50);

  }

  int timeout = 0; //set a timeoute counter

  while (sensor.available() < 7 ) //Wait to get a 7 byte response

  {

    timeout++;

    if (timeout > 10)             //if it takes to long there was probably an error

    {

      while (sensor.available())  //flush whatever we have

        sensor.read();

      break;                      //exit and try again

    }

    delay(50);

  }

  for (int i = 0; i < 7; i++)

  {

    response[i] = sensor.read();

  }

}

/*

   Función que recibe los datos del sensor de CO2

*/

unsigned long getValue(byte packet[])

{

  int high = packet[3];                        //high byte for value is 4th byte in packet in the packet

  int low = packet[4];                         //low byte for value is 5th byte in the packet

  unsigned long val = high * 256 + low;        //Combine high byte and low byte with this formula to get value

  return val * valMultiplier;

}

 

secrets.h

En este archivo se meten todas las claves que NO deben ser públicas. Es por ello que no es exactamente el contenido del archivo, pero sí están todas las indicaciones para ponerlo a funcionar.

Se omiten los números de canal y las APIKEY correspondientes a cada canal sustituidas por “NUMERO_DE_CANAL_XX”, que debería ser un número y por “APIKEY_XX” que debería ser un código alfanumérico.

/*Tanto los nombres de los dispositivos como los canales y las APIKEYS quedan definidas en arrays para poder añadir nuevos dispositivos fácilmente.

 * Esto se configura en el archivo principal eligiendo el índice del medidor. Actualmente están definidos los siguientes:

 * 0: Medidor tipo que no manda datos a la nube

 * 1: Medidor usado como prototipo; más quemado que la pipa de un hippie, pero ahí está el tío

 * 2-4: Medidores 001, 002, 003 conectados al servidor externo de ThingSpeak.com

 * 5-10: Medidores 001-006 conectados al servidor propio del Politécnico Jesús Marín

 * Índice 5: Medidor 001-Polithing - AULA 10 ADMINISTRATIVO.

 * Índice 6: Medidor 002-Polithing - T2 ELECTRONICA.

 * Índice 7: Medidor 003-Polithing - AULA 5 INF.

 * Índice 8: Medidor 004-Polithing - AULA 25.

 * Índice 9: Medidor 005-Polithing - AULA TEORIA EDIF.

 * Índice 10: Medidor 006-Polithing - AULA PRAC EDIF - servidor externo - test prototipo.

 */

String medidoresNombres[]        = {"00X sin-nube",       "Prototipo serv-ext", "001 serv-ext",       "002 serv-ext",      "003 serv-ext-EDIFIC.",

                                    "001 serv-Polithing", "002 serv-Polithing", "003 serv-Polithing", "004 serv-Polithing","005 serv-Polithing",

                                    "006 serv-Polithing"};

unsigned long medidoresCanales[] = {NUMERO_DE_CANAL_0, NUMERO_DE_CANAL_1, NUMERO_DE_CANAL_2, NUMERO_DE_CANAL_3, NUMERO_DE_CANAL_4, NUMERO_DE_CANAL_5, NUMERO_DE_CANAL_6, NUMERO_DE_CANAL_7, NUMERO_DE_CANAL_8, NUMERO_DE_CANAL_9, NUMERO_DE_CANAL_10};

char * medidoresAPIKEY[]         = {"APIKEY_0", "APIKEY_1", "APIKEY_2", "APIKEY_3", "APIKEY_4", "APIKEY_5", "APIKEY_6", "APIKEY_7", "APIKEY_8", "APIKEY_9", "APIKEY_10"};

Con todo esto, más el esquema eléctrico correspondiente y la instalación del servidor ThingSpeak (o usando el servicio gratuito de la Web) es fácil reproducir los medidores realizados.

Esquema eléctrico

Esto aún necesita una revisión, porque el diseño es muy mejorable, pero el siguiente esquema indicado con etiquetas es totalmente funcional.

Con ese diseño, un posible conexionado de la PCB es como sigue: