Desarollo de código para interacción IoT - andrerossellm/TC1004B.501_E8 GitHub Wiki

Contenido:

Para leer y escribir datos en Firebase, se desarrollaron diversos códigos que implementan la interacción con la base de datos en distintas plataformas:

Implementación de Python

Este programa puede acceder a la base de datos, imprimir en consola toda la información que contiene e indicar si detecta que el valor del campo numero es inválido, es decir, si es mayor de nueve; si este es el caso, realiza una señalización en el campo de usuario, cuyo valor reemplaza por "elemento mayor a 9".
El código desarrollado en Python hace uso de la librería Pyrebase, la cual provee una interfaz de integración simple para interactuar con las características de nuestro proyecto. Utilizamos el método de database() para obtener la base de datos en tiempo real y una vez que tenemos un objeto nativo de Python, podemos loopear a través de los elementos, aplicando los métodos key() y val() para obtener las etiquetas y los valores almacenados en la base de datos. Se incluyen una serie de validaciones para realizar impresiones con unidades y formato acorde al dato representado. El código fuente está disponible en los archivos de la repo. El programa no recibe ninguna entrada del usuario, sus salidas son impresiones directamente en pantalla y se espera que imprima un total de 8 líneas de información, además de una opcional, la cual informa al usuario que se hizo una modificación a la base de datos cuando se detectó un numero mayor a 9.

Las siguientes líneas acceden a la API de Firebase y generan una copia local de la información almacenada en la base de datos. Este detalle es importante porque cualquier cambio interno o externo a la información de la base no se ve inmediatamente reflejado en la copia local hasta que esta se actualiza respecto de la disponible en el servidor.

import pyrebase 
config = {  
"apiKey" : "AIzaSyC6y5rPcMl6RU7kMSaSRhd4m6ccrF0siNs",
"authDomain" : "pipin-ca62a.firebaseapp.com",
"databaseURL": "https://pipin-ca62a-default-rtdb.firebaseio.com/",
"projectId" : "pipin-ca62a",
"storageBucket" : "pipin-ca62a.appspot.com",
"messagingSenderId" : "272936523214",
"appId" : "1:272936523214:web:87a26d7bdd965f2bd3479f",
"measurementId" : "G-9YW4HNKKNK"
}

firebase = pyrebase.initialize_app(config) 
db = firebase.database() 
info = db.child("readings").get()

Utilizando un estatuto de repetición podemos iterar a través de todos los elementos guardados en la base de datos:

for item in info.each():

donde item cambiará en cada iteración, representando cada uno de los elementos disponibles, de ahí el método each(), que los enlista todos.

En el código se incluyen varios estatutos de decisión if para determinar qué variable física se está accesando e imprimir las unidades adecuadas para la misma; todas siguen el mismo algoritmo general:

  1. Detectar magnitud física
  2. Imprimir plantilla: "Elemento nombre = valor unidad

Para esto se utilizan los métodos key(), que devuelve el nombre de la etiqueta del elemento actual en la iteración, y val(), que da su valor.

Las únicas excepciones a este algoritmo suceden cuando:

  • Se detecta un acceso al campo usuario, en cuyo caso la impresión varía ligeramente en el sentido de que no necesita unidades:
if(item.key()=="usuario"):
        print(f' Elemento {item.key()} = {item.val()}')
  • El campo numero, que debe de validar que el campo no contenga un valor mayor a 9, si esto sucede, entonces reemplaza el valor del campo usuario por la cadena "elemento mayor a 9". Esto se hace mediante el método update() como sigue:
    elif(item.key()=="numero"): #caso especifico para el dato _numero_
        if(int(item.val())>9):
            db.child("readings").update({"usuario":"elemento mayor a 9"}) #hace el cambio en campo usuario
            flag=True;
            print(" Numero mayor que 9... ")
        else:
            print(f' Numero = {item.val()}')   
  • Los campos isFlame y movement que no tienen un valor análogo y cuyo estado booleano es interpretado por el programa para desplegar una impresión más amigable;
    elif("isFlame" == item.key()): #caso para el booleano que indica si hay flama
        if(item.val()==False):
            print(" No hay presencia de flama")
        else:
            print(" Hay presencia de flama")
    elif("movement"==item.key()): #caso para booleano de movimiento
        if(item.val()==False):
            print(" No hay presencia de movimiento")
        else:
            print(" Hay presencia de movimiento")

Dentro del programa se maneja una flag de valor binario que sólo se activa si se entra al if que monitorea el valor del campo numero, lo cual quiere decir que se actualizará el valor del campo usuario. El valor de la flag, si está se levantó, se revisa al final del programa para advertir que habrá un cambio sobre el valor del campo mencionado:

if(flag==True):
    print("Campo de usuario se actualizará a 'elemento mayor a 9'") #en proxima impresión el cambio se reflejará, en esta no porque toda la información se descargó antes de la modificación

Implementación de Arduino

Dadas las instrucciones correspondientes a la implementación de la base de datos con la placa de desarrollo ESP32, se deberá: Escribir en una base de datos de Firebase la lectura de cinco variables físicas diferentes, usando el ESP32. Además, que muestre en un display de siete segmentos el valor del elemento numero. Sí el elemento en numero es mayor a nueve, el display de siete segmentos debe marcar “-”. El código final de la implementación puede consultarse en esta liga.

Las librerías a utilizar se declaran en el header del documento .ino de la siguiente manera:

#include <esp_wpa2.h>
#include <WiFi.h>
#include <Firebase_ESP_Client.h>  
#include <addons/TokenHelper.h>
#include "Arduino.h"
#include "addons/RTDBHelper.h"
#include "DHT.h"

Ya que el ESP-32 necesita conectarse a una red WiFi para actualizar las variables físicas en la base de datos estaremos empleando la librería WiFi.h y Firebase_ESP_Client.h . Así mismo, los addons TokenHelper.h y RTDBHelper.h se incluyen para facilitar el proceso de establecer conexión con la base de datos. Para hacer uso del sensor DHT11 debemos incluir la librería DHT.h.

En las siguientes líneas se muestran las conexiones establecidas entre los sensores y la placa de desarrollo junto con un identificador:

#define TRIG_PIN 23     // Pin input para el sensor ultrasonico (TRIGGER)
#define ECHO_PIN 22     // Pin input para el sensor ultrasonico (ECHO)
#define FLAMA_PIN 17    // Pin input para el puerto DO . Sensor flama
#define S_MOV_PIN 19    // Pin input para el sensor de movimiento
#define DHT_PIN 4       // Pin input para el sensor DHT11

Para la implementación del display de 7 segmentos en el que desplegaremos la variable número de la base de datos, es necesario declarar los pines de la placa de desarrollo correspondientes a los segmentos que se iluminarán. En las siguientes líneas se declara el arreglo de números sobre el que se iterará para definir los pines en modo OUTPUT en la función setup() y los arreglos correspondientes a los valores de pin para formar el número capturado desde la base de datos.

//Se declaran los pines a usar para el display
int LEDs[] = {25,16,5,18,21,3,1};
//25 g, 16 f, 5 e, 18 d, 3 c, 21 b, 1 a

// Se declaran los arreglos que forman los dígitos
int zero[] = {0, 1, 1, 1, 1, 1, 1};   // cero
int one[] = {0, 0, 0, 0, 1, 1, 0};   // uno
int two[] = {1, 0, 1, 1, 0, 1, 1};   // dos
int three[] = {1, 0, 0, 1, 1, 1, 1};   // tres
int four[] = {1, 1, 0, 0, 1, 1, 0};   // cuatro 
int five[] = {1, 1, 0, 1, 1, 0, 1};   // cinco
int six[] = {1, 1, 1, 1, 1, 0, 1};   // seis
int seven[] = {0, 0, 0, 0, 1, 1, 1};   // siete
int eight[] = {1, 1, 1, 1, 1, 1, 1}; // ocho
int nine[] = {1, 1, 0, 1, 1, 1, 1};   // nueve
int no_number[] = {1, 0, 0, 0, 0, 0, 0};   // numero mayor a nueve

Se declaran las variables designadas para almacenar las lecturas de los sensores:

//Variables sensor de movimiento
int pinStateCurrent   = LOW;  // Estado actual de la lectura del pin
int pinStatePrevious  = LOW;  // Estado anterior de la lectura del pin

//Variables
int isFlame = HIGH;             // Sensor flama. HIGH significa NO FLAME
bool flama;                     // Variable booleana para validar la presencia de flama
bool movimiento;                // Variable booleana para validar la presencia de movimiento
float duration_us, distance_cm; // Sensor distancia
float h;                        // Sensor DHT
float t;                        // Sensor DHT
float f;                        // Sensor DHT
String numero;                  // String de Firebase /readings/numero
int num;                        // valor de numero.toInt()

Para poder establecer conexión con la red declarada y poder publicar las mediciones obtenidas a la base de datos se requiere de las siguientes credenciales:

  • Credenciales para conectarse a la red bajo protocolo WPA2-Personal
const char* ssid = "LAPTOP-Luis";
const char* password = "lf0602hf";
  • Credenciales para el escritura de datos en Firebase
#define USER_EMAIL "[email protected]"
#define USER_PASSWORD "a01735939"
  • API Key de la DB en Firebase
#define API_KEY "AIzaSyC6y5rPcMl6RU7kMSaSRhd4m6ccrF0siNs"//AIzaSyAjjTHMIV0y394tayvijhU-aVVcKdkIZxU
  • Definimos el URL de la base de datos
#define DATABASE_URL "https://pipin-ca62a-default-rtdb.firebaseio.com/"
  • Definimos Firebase Data object
FirebaseData fbdo;

FirebaseAuth auth;
FirebaseConfig config;

unsigned long sendDataPrevMillis = 0;
int intValue;
float floatValue;
// Valor booleano para evaluar el inicio de sesion exitoso
bool signupOK = false;

Previo a la ejecución de la función loop(), la función setup() se encarga de inicializar las variables, establecer las modalidades de los pines y empezar a utilizar las librerías declaradas.

void setup() {
  Serial.begin(115200);       // Se incializa el puerto serial con tasa de informacion en 115200 bits/segundos
  dht.begin();                // Se inicializa la lectura del sensor DHT11
  delay(10);                  // Delay

  // Se configura TRING_PIN como output
  pinMode(TRIG_PIN, OUTPUT);
  // Se configura TRING_PIN como input
  pinMode(ECHO_PIN, INPUT);

  // Se inicializan los pines del display como salida 
  for (int i = 0; i<7; i++) pinMode(LEDs[i], OUTPUT);
  • Se desplegará información sobre la red WiFi a la que se conectará, así como la dirección MAC. Mientras no se haya establecido la conexión se mostrará una serie de "." en el Output del monitor serial.
  Serial.println();
  Serial.print("Connecting to ");
  // Se imprime el ssid de la red declarada
  Serial.println(ssid);
  // Se imprime el direccion MAC de la red declarada
  Serial.print("MAC >> ");
  Serial.println(WiFi.macAddress());
  Serial.printf("Connecting to WiFi: %s ", ssid);
  // Se inicializa la conexion a Internet con las credenciales declaradas
  WiFi.begin(ssid, password);
  
  while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      Serial.print(".");
  }
  • Una vez se haya establecido conexión con la red se configura la conexión con la base de datos a partir de las credenciales declaradas en el código.
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  // Se asigna la variable API_KEY a config.api_key
  config.api_key = API_KEY;
  // Se asigna la variable DATABASE_URL a config.database_url
  config.database_url = DATABASE_URL;
  // Se verifica que el loggeo con la DB haya sido exitoso
  if (Firebase.signUp(&config, &auth, "", "")){
    Serial.println("ok");
    signupOK = true;
  }
  else{
    Serial.printf("%s\n", config.signer.signupError.message.c_str());
  }
  /* Assign the callback function for the long running token generation task */
  config.token_status_callback = tokenStatusCallback; //see addons/TokenHelper.h
  // Se inicializa Firebase con las credenciales declaradas anteriormente
  Firebase.begin(&config, &auth);
  Firebase.reconnectWiFi(true);
}
  • La función segment_display(int) se encarga de configurar los pines designados al display de 7 segmentos de manera que se muestre el dato parámetro siempre y cuando se trate de un número entre 0 y 9. En cualquier otro caso el display desplegará el segmento de en medio: "-".
void segment_display(unsigned char valor){
    switch(valor){
        case 0:
            for (int i = 0; i<7; i++) digitalWrite(LEDs[i], zero[i]); break;
        case 1:
            for (int i = 0; i<7; i++) digitalWrite(LEDs[i], one[i]); break;
        case 2:
            for (int i = 0; i<7; i++) digitalWrite(LEDs[i], two[i]); break;
        case 3:
            for (int i = 0; i<7; i++) digitalWrite(LEDs[i], three[i]); break;
        case 4:
            for (int i = 0; i<7; i++) digitalWrite(LEDs[i], four[i]); break;
        case 5:
            for (int i = 0; i<7; i++) digitalWrite(LEDs[i], five[i]); break;
        case 6:
            for (int i = 0; i<7; i++) digitalWrite(LEDs[i], six[i]); break;
        case 7:
            for (int i = 0; i<7; i++) digitalWrite(LEDs[i], seven[i]); break;
        case 8:
            for (int i = 0; i<7; i++) digitalWrite(LEDs[i], eight[i]); break;
        case 9:
            for (int i = 0; i<7; i++) digitalWrite(LEDs[i], nine[i]); break; 
        default:
            for (int i = 0; i<7; i++) digitalWrite(LEDs[i], no_number[i]); break;          
    }
}

Para la obtención de mediciones definimos varias funciones a ser llamadas en la función loop() del archivo .ino . Primeramente, las mediciones referentes a la temperatura y humedad se llevan a cabo en la función sensorTempHum().

// Se define el tipo de sensor a utilizar
#define DHTTYPE DHT11  
DHT dht(DHT_PIN, DHTTYPE);

void sensorTempHum(){
  // Se lee humedad
  h = dht.readHumidity();
  // Se lee temperatura en grados Celsius, magnitud predeterminada
  t = dht.readTemperature();
  // Se lee temperatura en grados Fahrenheit, parametro true
  f = dht.readTemperature(true);
}

De esta misma manera es que decidimos modularizar el código y desarrollamos las siguientes funciones:

  • Función para validar la presencia de llama en cercanía del sensor dedicado:
void sensorFlama(){
  isFlame = digitalRead(FLAMA_PIN);   // Se le asigna a isFlame el valor recibido por el sensor de flama
  if(isFlame==HIGH){
    flama=true;
  }
  else{
    flama=false;
  }
}
  • Función para medir distancia con el sensor ultrasónico:
void sensorDistancia(){
  digitalWrite(TRIG_PIN, HIGH);
  // Genera un delay de 10 microsegundos para la medicion de la distancia
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);

  // Mide la duracion del pulso generado por el ECHO_PIN
  duration_us = pulseIn(ECHO_PIN, HIGH);

  // Se calcula la distancia mediante una conversion
  distance_cm = 0.017 * duration_us;
}
  • Función para validar la existencia de objetos en movimiento en cercanía del sensor dedicado:
void sensorMovimiento(){
  pinStatePrevious = pinStateCurrent;         // Se actualiza el estado anterior de la variable movimiento
  pinStateCurrent = digitalRead(S_MOV_PIN);   // Se actualiza el estado presente de la variable movimiento
  // En el caso de que la medicion anterior sea LOW y la actual HIGH, hay movimiento
  if (pinStatePrevious == LOW && pinStateCurrent == HIGH) {
    movimiento=true;
  }
  else if (pinStatePrevious == HIGH && pinStateCurrent == LOW) {
    movimiento=false;
  }
  // En el caso de que la medicion anterior sea HIGH y la actual LOW, no hay movimiento
}

Por último, se presenta la funcion loop(). En esta función las primeras instrucciones son tomar las mediciones correspondientes y posterior a ello empieza a actualizarlas en la base de datos: Todo lo descrito a continuación pertenece a la función loop().

  • Se llaman a la funciones dedicadas a las mediciones.
void loop{                  // Aquí empieza loop()
  delay(2000);
  sensorTempHum();
  sensorFlama();
  sensorDistancia();
  sensorMovimiento();
  • Se valida el establecimiento de la conexion WiFi y el logging-in a la base de datos haya sido exitoso.
  if (Firebase.ready() && signupOK && (millis() - sendDataPrevMillis > 15000 || sendDataPrevMillis == 0)) {
    sendDataPrevMillis = millis();
    // Para todas las escrituras de variables, se valida que la escritura haya sido exitosa
    // En caso de que no se pueda escribir la lectura, se imprime la razon del error
    // Se manda la variable t a la database en path readings/temperaturaC
  • Se validan la escritura de las mediciones en la base de datos. Para capturar la temperatura en grados Celsius:
    if (Firebase.RTDB.setFloat(&fbdo, "readings/temperaturaC", t)){
      Serial.println("PASSED");
    }
    else {
      Serial.println("FAILED");
      Serial.println("REASON: " + fbdo.errorReason());
    }
  • Se valida la captura de la variable f a la database en path readings/temperaturaF:
    if (Firebase.RTDB.setFloat(&fbdo, "readings/temperaturaF", f)){
      Serial.println("PASSED");
    }
    else {
      Serial.println("FAILED");
      Serial.println("REASON: " + fbdo.errorReason());
    }
  • Se valida la captura de la variable h a la database en path readings/humedad:
    if (Firebase.RTDB.setFloat(&fbdo, "readings/humedad", h)){
      Serial.println("PASSED");
    }
    else {
      Serial.println("FAILED");
      Serial.println("REASON: " + fbdo.errorReason());
    }
  • Se valida la captura de la variable flama a la database en path readings/isFlame:
    //Se manda la variable flama a la database en path readings/isFlame
    if (Firebase.RTDB.setBool(&fbdo, "readings/isFlame", flama)){
      Serial.println("PASSED");
    }
    else {
      Serial.println("FAILED");
      Serial.println("REASON: " + fbdo.errorReason());
    }
  • Se valida la captura de la variable movimiento a la database en path readings/movement:
    //Se manda la variable movimiento a la database en path readings/movement
    if (Firebase.RTDB.setBool(&fbdo, "readings/movement", movimiento)){
      Serial.println("PASSED");
    }
    else {
      Serial.println("FAILED");
      Serial.println("REASON: " + fbdo.errorReason());
    }
  • Se valida la captura de la variable distance_cm a la database en path readings/distance:
    // Se manda la variable distance_cm a la database en path readings/distance
    if (Firebase.RTDB.setFloat(&fbdo, "readings/distance", distance_cm)){
      Serial.println("PASSED");
    }
    else {
      Serial.println("FAILED");
      Serial.println("REASON: " + fbdo.errorReason());
    }
  • Se valida la lectura de la variable numero de la database en path readings/numero y se llama a la función segment_display con el número leído como parámetro.
    if (Firebase.RTDB.getString(&fbdo, "/readings/numero")){
      // Se lee la variable en /readings/numero
      numero = fbdo.intData();
      num = numero.toInt();
    }
    segment_display(num);
  }
}                           // Aquí termina loop()

Implementación de App Inventor

La aplicación desarrollada permite al usuario consultar las mediciones desde la base de datos y actualizar las variables Numero y Usuario. Para la creación de esta se utilizó el entorno de desarrollo de software por bloques App Inventor.

Esta cuenta con dos campos: Usuario y Numero, en los que el usuario ingresará datos de tipo String e Int, respectivamente. Al momento en el que el usuario presione el botón etiquetado como Enviar, la plataforma actualizará estas variables en la base de datos. La programación necesaria para esta función vincula los elementos propios de la aplicación y las variables en la base de datos.

Adicionalmente, la aplicación despliega las magnitudes que recibe la base de datos. Para esto vinculamos elementos de tipo Etiqueta con las variables almacenadas en la base de datos. Los bloques necesarios para dicha función son los siguientes.

⚠️ **GitHub.com Fallback** ⚠️