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