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; }
2 Comentários
Bom dia Fernando mano como eu acho essa biblioteca esp_task_wdt.h pra baixa
ResponderExcluirboa noite Fernando, eu precisava converter este timestamp para um formato data, mas n estou conseguindo de nenhuma forma, alguma dica?
ResponderExcluir