banner

AppFernandoK Monitor gráfico com Firebase e HTU21D



Mais um trabalho desenvolvido com o meu AppFernandoK, desta vez na versão 2.0. Quero te apresentar um exemplo de envio de histórico de dados para o Firebase, bem como a utilização do recurso de gráfico do aplicativo. Neste vídeo eu utilizei também o HTU21D e o ESP32 LoRa.






Recursos usados

  • ESP32 LoRa WiFi Heltec
  • Sensor de temperatura e umidade HTU21D
  • Smartphone com AppFernandoK
  • Protoboard
  • Jumpers





Montagem







APP Botões

Os Botões utilizados são:
  • Climate: Obtém os valores de temperatura e variável
  • Delete Var: Deleta todas as variáveis do path Commands, usado caso aconteça algum engano na inserção de uma variável, seja no nome ou no valor.
  • CMDs path: Seta o caminho do Firebase que o App trabalhará, neste caso é o path /Commands.
  • History path: Seta o caminho do Firebase que o App trabalhará, neste caso é o path /History.
  • Graph: Cria um gráfico, mais detalhes nos próximos slides





APP Utilizando o gráfico

Os comandos de criação de gráficos são:

G.name <NOME_VARIAVEL>
Define a variável usada no gráfico

G.path <CAMINHO_VARIAVEL>
Define o caminho em que a variável se encontra

G.range <QUANTIDADE_VALORES>
Define a quantidade de valores exibidos no gráfico

G.latest <ULTIMOS_VALORES>
Define os últimos valores obtidos da base de dados

G.color <COR_LINHA>
Define a cor da linha

G.plot
Cria o gráfico


Exemplo de criação de gráfico para a variável de temperatura:
G.name T
G.path /History
G.range 100
G.latest 100
G.color red
G.plot

Adicionamos um botão chamado Graph para executar este script.


Após executarmos o comando G.plot, a mensagem Swipe to the side será exibida, indicando que podemos visualizar o gráfico deslizando a tela da direita para a esquerda.






Código - Configurações

Bibliotecas necessárias

Sensor HTU21D:


Firebase IOXhop_FirebaseESP32:


Display SSD1306:


Biblioteca ArduinoJson
A biblioteca do Firebase para o ESP32 requer que a biblioteca ArduinoJson esteja instalada e que sua versão seja a V5.13.3, segundo sua documentação.



Instalação da biblioteca ArduinoJson
Na sua Arduino IDE vá em:
Sketch -> Incluir biblioteca -> Gerenciar bibliotecas
Pesquise por: arduinojson
Procure pela biblioteca descrita como: ArduinoJson by Benoit Blanchon


Selecione a versão 5.13.3


E clique em instalar


Nós implementamos uma biblioteca chamada DisplayOLED  e estará disponível para download junto ao projeto

Instalação da biblioteca DisplayOLED
Mova a pasta DisplayOLED para a pasta: C:\Users\[SEU NOME DE USUARIO]\Documents\Arduino\libraries






Fluxograma


Obs. A Task Handle Clients deve estar no mesmo core que o loop pois ambos utilizam o display.
A função Task Callback do Firebase é criada pela biblioteca IOXhop_FirebaseESP32 no core 1.




Tipos de variáveis:
variablesFirebase não são comandos. São usadas somente para exibição.
temporaryVariables são comandos válidos, porém temporários.





Código ESP32

Declarações e variáveis

#include <Wire.h> // Lib necessária para comunicação i2c
#include <SparkFunHTU21D.h> // Lib do sensor HTU21D
#include "esp_task_wdt.h" // Lib com as funções de task 
#include <DisplayOLED.h> // Lib com as funções de display
#include <IOXhop_FirebaseESP32.h> // Lib Firebase
#include <ArduinoJson.h> // Lib para a manipulação de Json

#define FIREBASE_HOST "meuprojeto.firebaseio.com" // URL da base de dados fornecido pelo Firebase para a conexão http
#define FIREBASE_AUTH "" // Autenticação

// Dados WiFi
const char* ssid     = "SSID"; // Coloque o nome da sua rede wifi aqui
const char* password = "PASSWORD"; // Coloque a sua senha wifi aqui

const IPAddress IP = IPAddress(111,111,1,11); // IP 
const IPAddress GATEWAY = IPAddress(111,111,1,1); // Gateway
const IPAddress SUBNET = IPAddress(255,255,255,0); // Máscara

// O DNS Public da Google representa dois endereços IPv4 – 8.8.8.8 e 8.8.4.4
// Devemos setar os dois no wificonfig para evitar problemas de DNS da lib do Firebase
const IPAddress PRIMARY_DNS(8, 8, 8, 8);   
const IPAddress SECONDARY_DNS(8, 8, 4, 4); 

const int port = 80; // Porta

// Objeto WiFi Server, o ESP será o servidor
WiFiServer server(port); 

// Vetor com os clientes que se conectarão no ESP
std::vector<WiFiClient> clients; 

// Pinos do sensor (i2c - canal 1)
#define SDA 21
#define SCL 22

// Objeto referente ao sensor
HTU21D htudSensor;

// Objeto de conexão (wire) do sensor (Obs. O display usa o canal 0 e o sensor usa o canal 1)
TwoWire wireSensor = TwoWire(1);

// Objeto do display
DisplayOLED display;

// Variáveis de contagem de tempo (millis) da função loop
long millisRefLoop, millisRefVerifyPath;
bool updateMillisRefLoop = false, updateMillisVerifyPath = false;

// Variáveis que guardam os valores de temperatura e umidade
String temp, humd;

// Comandos existentes nessa aplicação
// "temporaryVariables" guardam os comandos temporários, que são excluídos logo após sua execução
// "variablesFirebase" guardam as variáveis
const int SIZE_TEMP_VAR = 3, SIZE_VAR = 3;

String temporaryVariables[SIZE_TEMP_VAR] = {"ALERT", "CLIMATE", "DELETEVAR"};
String variablesFirebase[SIZE_VAR] = {"T", "H", "INIT"};

// Caminho do Firebase aonde os comandos serão criados
const String PATH_COMMANDS = "/Commands"; 

// Tempo aguardado entre os envios de valores do sensor para o firebase
#define SEND_DATA_INTERVAL 1000


Setup




void setup()
{  
  // Inicializamos o watchdog com 10 segundos de timeout
  esp_task_wdt_init(15, true);

  Serial.begin(115200);
  Serial.println("Starting...");

  // Iniciamos o sensor de temperatura e umidade
  htu21Begin();

  // Iniciamos o display
  if(!display.begin())
  {
    // Se não deu certo, exibimos falha de display na serial
    Serial.println("Display failed!");
    // E deixamos em loop infinito
    while(1);
  }
  // Exibimos no display
  display.println("Display ok", CLEAR);
  display.showDisplay();
  
  // Iniciamos o server, deve ser iniciado antes do firebaseBegin!
  serverBegin();




// Criamos as tasks de conexão por socket
  createTasksSocket();

  // Iniciamos a função callback do Firebase
  firebaseBegin();

  // Iniciamos a task de envios de valores do sensor para o Firebase
  xTaskCreatePinnedToCore(sendSensorData, "sendSensorData", 10000, NULL, 1, NULL, 0);
}





Loop



void loop()
{  
  // A cada 3s executamos o loop
  if(timeout(3000, &millisRefLoop, &updateMillisRefLoop))
  {
    // Exibimos os clientes conectados
    Serial.println("Socket Clients: "+String(clients.size()));    
    display.println("Socket Clients: "+String(clients.size()), CLEAR);

    // Efetuamos a leitura do sensor
    readClimate();

    // Exibimos os valores formatados no display
    if(temp != "")
      display.println("T: " + temp + " C", NOT_CLEAR);
    else
      display.println("T: -", NOT_CLEAR);

    if(humd != "")
      display.println("H: " + humd + "%", NOT_CLEAR);
    else
      display.println("H: -", NOT_CLEAR);

    // Devemos chamar a função "showDisplay" para que os valores realmente sejam exibidos no display
    display.showDisplay();    
  }

  delay(10);
}



CreateTasksSocket




// Função que cria 3 tasks
void createTasksSocket()
{
  // Criamos a task que insere os novos clientes no vector
  xTaskCreatePinnedToCore(taskNewClients, "taskNewClients", 10000, NULL, 1, NULL, 0);

  // Criamos a task que recebe e executa os comandos dos clients conectados  
  xTaskCreatePinnedToCore(handleClients, "handleClients", 10000, NULL, 1, NULL, 1); 

  // IMPORTANTE: DEIXAR AS TASKS QUE UTILIZAM O DISPLAY NO MESMO CORE QUE O LOOP (CORE 1)
}



TaskNewClients



// Task que verifica se novos clientes socket se conectaram
void taskNewClients(void *p)
{
  // Objeto WiFiClient que receberá o novo cliente conectado
  WiFiClient newClient;
  
  // Adicionamos a tarefa na lista de monitoramento do watchdog
  esp_task_wdt_add(NULL);

  while(true)
  { 
    // Resetamos o watchdog
    esp_task_wdt_reset();
    delay(1); 

    // Se existir um novo client atribuimos para a variável
    newClient = server.available(); 
    
    // Se o client for diferente de nulo
    if(newClient)    
    {      
      // Inserimos no vector
      clients.push_back(newClient);
      // Exibimos na serial indicando novo client e a quantidade atual de clients
      Serial.println("New client! size:"+String(clients.size()));
    }   
  }
}



HandleClients


// Função que verifica se um cliente enviou um comando (comunicação por socket)
void handleClients(void *p)
{
  String cmd, response, msgDisplay;
  // Adicionamos a tarefa na lista de monitoramento do watchdog
  esp_task_wdt_add(NULL);
  
  while(true)
  { 
    // Atualizamos as conexões atuais via socket
    refreshConnections();
    
    // Percorremos o vetor de clientes socket
    for(int i=0; i<clients.size(); i++)
    {      
      // Se existem dados a serem lidos de um client
      if(clients[i].available())
      {
        // Obtemos o comando
        cmd = clients[i].readStringUntil('\n');
        
        // Exibimos na serial e no display
        printMsg("Received from cli:", CLEAR);  
        printMsg(cmd, NOT_CLEAR);






// Executamos o comando e recebemos sua respectiva resposta
        response = executeCommandFromSocket(cmd);
        // Enviamos para o app a resposta
        clients[i].print(response); 
        
        // Setamos a variável de contagem de tempo para podermos visualizar a exibição no display
        updateMillisRefLoop = true;

        // Se for um comando inválido, exibimos no display "Invalid command", se for válido exibimos "OK"
        if(response.indexOf("Invalid command")>=0)
          msgDisplay = "Invalid command";
        else
          msgDisplay = "OK";
        // Exibição display
        printMsg(msgDisplay, NOT_CLEAR);
      }   
      // Resetamos o watchdog
      esp_task_wdt_reset(); 
    }
    esp_task_wdt_reset();
    // Esperamos 1s para que a função loop possa ser executada
    delay(1); 
  }
}



sendSensorData – variáveis

// Função que envia constantemente os valores do sensor para o Firebase no formato json
void sendSensorData(void *p)
{
  // Criamos dois objetos do tipo StaticJsonBuffer, com seus respectivos tamanhos
  // Buffer do json que será enviado (Json de envio)
  StaticJsonBuffer<150> jsonBufferSensor; 
  // Buffer do json de timestamp (Json auxiliar - será concatenado com o json de envio)
  StaticJsonBuffer<50> jsonBufferTimestamp; // Ex: "timestamp":{".sv": "timestamp"}
  // Variável usada para contagem de tempo
  long prevMillis;

  // Adicionamos a tarefa na lista de monitoramento do watchdog
  esp_task_wdt_add(NULL);

  // Criamos o objeto json referente ao timestamp (Json auxiliar)
  JsonObject& timestamp = jsonBufferTimestamp.createObject();
  // Inserimos o atributo de timestamp  "timestamp":{".sv": "timestamp"} no objeto json
  timestamp[".sv"] = "timestamp";




SendSensorData


while(true)
  {     
    if(timeout(1000, &millisRefVerifyPath, &updateMillisVerifyPath))
      verifyCommandsFolder();

    // Resetamos o watchdog
    esp_task_wdt_reset(); 
    // Esperamos um tempo para que as outras funções do core 0 sejam executadas
    delay(1);  

    // Se algum dos valores estiver vazio, não enviamos nada para o Firebase
    if(temp != "" && humd != "")
    {
      // Criamos os objetos json
      JsonObject& sensorData = jsonBufferSensor.createObject();  

      // Inserimos os atributos de temperatura e umidade no json sensorData
      sensorData["T"] = temp;    
      sensorData["H"] = humd;

      // Inserimos o "pedaço" de json referente ao timestamp no final do json sensorData
      sensorData["timestamp"] = timestamp;
      // Enviamos o json sensorData para o Firebase na pasta History
      Firebase.push("/History", sensorData);
    }



sendSensorData – Json

O JSON enviado pelo ESP32 possui os seguintes dados:
ID:
Com o comando Firebase.push, o Firebase cria uma pasta com um ID sequencial aonde os dados serão armazenados. Esse ID pode ser convertido para um timestamp usando um algoritmo de conversão.
H:
Valor referente a umidade
T:
Valor referente a temperatura
timestamp:
Valor referente ao tempo de envio. Esse valor é gerado pelo Firebase. O Json usado gerarmos o timestamp do lado do servidor (Firebase) é:
"timestamp":{".sv": "timestamp"}

Visualização do banco de dados no Firebase




SendSensorData


 // Aguardamos um tempo
    prevMillis = millis();
    while(prevMillis + SEND_DATA_INTERVAL > millis())
    {
      // Resetamos o watchdog
      esp_task_wdt_reset(); 
       // Esperamos um tempo para que as outras funções do core 0 sejam executadas
      delay(100);
    }
    // Limpamos o buffer do json
    jsonBufferSensor.clear();
  }
}



CallbackFirebase



// Função que inicia a conexão com o Firebase
void firebaseBegin()
{
  Firebase.begin(FIREBASE_HOST, FIREBASE_AUTH);  
  
  // Callback executado a cada alteração do firebase
  Firebase.stream(PATH_COMMANDS, [](FirebaseStream stream) 
  {
    String response, msgDisplay, path, value;
    // Se o evento que vem do callback é de alteração "put"
    if(stream.getEvent() == "put") 
    {     
      // Obtemos os valores de path e value
      path = stream.getPath();
      value = stream.getDataString();      
      
      // Verificamos:
      // Se não entrou pela primeira vez (o Firebase envia um json {} com todas as variáveis)
      // Se a variável possui um valor válido
      // Se o path não é "History", usado pela nossa task de envios constantes (histórico de envios)
      // Se todas condições forem verdadeiras, então executaremos o comando
      if(path.indexOf("History") < 0 && !value.equals("null") && value.indexOf("{") < 0 && value.indexOf("}") < 0 && !value.equals(""))
      {    





 // Verificamos se o valor recebido não é do tipo "variablesFirebase", pois estes não são comandos, apenas variáveis de exibição
        if(!vectorContains(path, variablesFirebase, SIZE_VAR))
        {
          // Montamos a mensagem que será exibida no display
          msgDisplay = path;
          msgDisplay.replace("/","");
          
          // Setamos a variável de contagem de tempo para podermos visualizar as exibições no display
          updateMillisRefLoop = true;                    
          display.println("Received from cli\n" + msgDisplay, CLEAR); 
          display.showDisplay();  

          // Executamos o comando recebido
          response = executeCommandFromFirebase(path);    

          // Setamos novamente a variável de contagem de tempo
          updateMillisRefLoop = true;

          // Exibimos a resposta correspondente ao comando recebido no display
          display.println(msgDisplay+": "+response, CLEAR);
          display.showDisplay();
        } 
      }
    }
  });
}



Funções Setup – hut21begin e serverBegin


// Função que inicializa o sensor de temperatura e umidade HTU21D
void htu21Begin()
{
  wireSensor.begin(SDA, SCL, 400000);
  htudSensor.begin(wireSensor);
}


// Conectamos no WiFi e iniciamos o servidor
void serverBegin()
{ 
  delay(1000);
  // Iniciamos o WiFi
  WiFi.begin(ssid, password);  
  // Exibimos na serial e no display
  printMsg("Wifi Connecting", CLEAR);  

  // Enquanto não estiver conectado exibimos um ponto
  while (WiFi.status() != WL_CONNECTED)
  {
    printMsg(".", NOT_CLEAR, false);    
    esp_task_wdt_reset();
    delay(1000);
  }



Funções Setup - serverBegin


// Configuramos o WiFi com o IP definido anteriormente 
  if (!WiFi.config(IP, GATEWAY, SUBNET, PRIMARY_DNS, SECONDARY_DNS)) 
  {
    printMsg("STA config failed", CLEAR);
    while(1);
  }

  // Exibimos na serial e no display a mensagem OK
  printMsg("OK", CLEAR);   
  // Printamos o IP (debug)
  // SerialPrintln(WiFi.localIP());
  
  // Iniciamos o servidor
  server.begin(port);
}



Funções Loop - readClimate


// Função que lê os valores do sensor
void readClimate()
{
  // Retorna 999 caso dê erro de leitura
  float h = htudSensor.readHumidity();
  float t = htudSensor.readTemperature();

  if(t < 900)
    temp = String(t);
  else
    temp = "";
  
  if(h < 900) 
    humd = String(h);
  else
    humd = "";
}



Funções callbackFirebase - executeCommandFromFirebase


// Função que executa um comando que veio via Firebase
String executeCommandFromFirebase(String cmd)
{  
  String cmdUpperCase, response = "OK";

  if(cmd.charAt(0) == '/')
    cmd = cmd.substring(1);

  // Utilizamos para comparação o comando em maiúsculo em uma outra variável
  // O a variável "cmd", que guarda o comando da forma em que o usuário nos enviou, é usada para podermos excluí-la no Firebase, após sua execução, caso ela seja do tipo "temporaryVariables"
  cmdUpperCase = cmd;
  cmdUpperCase.toUpperCase();

  Serial.println(cmdUpperCase);

  // Verificamos o comando e executamos uma ação
  if(cmdUpperCase.equals("CLIMATE"))
  {    
    setClimate();
   /* Firebase.setString(PATH_COMMANDS + "/T", temp);
    Firebase.setString(PATH_COMMANDS + "/H", humd);*/
  }
else
  if(cmdUpperCase.equals("DELETEVAR"))
  {
    Firebase.remove(PATH_COMMANDS);
    //resetFireBase();
  }    
  else
  {
    response = "Invalid command";    
    Firebase.setString(PATH_COMMANDS + "/Alert", "Invalid command");    
  }

  // Se for um comando temporário, excluímos este comando
  if(vectorContains(cmdUpperCase, temporaryVariables, SIZE_TEMP_VAR))
  {
    // Debug
    Serial.println("executeCommandFromFirebase remoção: " + PATH_COMMANDS + "/" + cmd);
    // Exclusão
    Firebase.remove(PATH_COMMANDS + "/" + cmd);
  }   

  return response; 
}



Funções handleClients - executeCommandFromSocket


// Função que executa um comando que veio via socket
String executeCommandFromSocket(String cmd)
{
  String response;

  // Retiramos o valor atribuído para o comando, obtendo somente o nome do comando
  cmd = cmd.substring(0, cmd.indexOf(" "));

  // Deixamos em maiúsculo
  cmd.toUpperCase();

  // Se for igual a CLIMATE
  if(cmd.equals("CLIMATE"))
  {
    // Montamos a resposta com a temperatura e umidade que será exibida no aplicativo
    response = "T: " + temp;

    if(!temp.equals("-"))
      response += " C";

    response += "\n H: " + humd;

    if(!humd.equals("-"))
      response += " %";   
  }
  else
    // Se não for um comando válido, deixamos o indicador "aviso: " no início da mensagem, assim o aplicativo pintará a mensagem de amarelo
    response = "aviso: Invalid command";

  return response;
}



Funções handleClients - refreshConnections


// Função que verifica se um ou mais clients se desconectaram do server e, se sim, estes clientes serão retirados do vector (comunicação por socket)
void refreshConnections()
{
  // Flag que indica se pelo menos um client se desconectou
  bool flag = false;
  // Vetor que receberá apenas os cliente conectados
  std::vector<WiFiClient> newVector;

  // Percorremos o vector
  for(int i=0; i<clients.size(); i++)
  {
    // Verificamos se o client está desconectado
    if(!clients[i].connected())
    {
      // Exibimos na serial que um client se desconectou e a posição em que ele está no vector (debug)
      Serial.println("Client disconnected! ["+String(i)+"]");
      // Desconectamos o client
      clients[i].stop();      
      // Setamos a flag como true indicando que o vector foi alterado
      flag = true;          
    }
    else
      newVector.push_back(clients[i]); // Se o client está conectado, adicionamos no newVector
  }  
  // Se pelo menos um client se desconectou, atribuimos ao vector "clients" os clients de "newVector"
  if(flag)
    clients = newVector;
}



Funções sendSensorData - verifyCommandsFolder


// Função que verifica se o caminho PATH_COMMANDS existe e cria se não existir
void verifyCommandsFolder()
{  
  String allData;

  // Listamos todas as variáveis do path de comandos e atribuimos para a variável allData no formato json
  Firebase.get(PATH_COMMANDS, allData);  

  // Verificamos se o caminho existe, se não, criamos ele
  if(allData.indexOf("\"") < 0)
    Firebase.setString(PATH_COMMANDS+"/init", "1");
}




Funções auxiliares – printMsg


// Função que exibe na serial e no display uma mensagem
void printMsg(String msg, bool clear, bool newLine = true)
{
  if(newLine)
  {
    Serial.println(msg);    
    display.println(msg, clear);
  }
  else
  {
    Serial.print(msg);    
    display.print(msg, clear);    
  }  
  display.showDisplay();
}



Funções auxiliares – timeout

// Função usada para aguardar um tempo comparando o valores de millis() sem utilizar a função "delay"
bool timeout(const int DELAY, long *millisRef, bool *updateMillisRef)
{
  if(*updateMillisRef)
  {
    *millisRef = millis();
    *updateMillisRef = false;
  }  

  if((*millisRef + DELAY) < millis())
  {
    *updateMillisRef = true;
    return true;
  }

  return false;
}




FAÇA O DOWNLOAD DOS ARQUIVOS:




Nenhum comentário:

Tecnologia do Blogger.