Projeto final do primeiro trimestre — Arduino e IoT — semana 13 do 1o trimestre
Semana 13 de 13· 1o trimestre · 01/02 a 30/04
Projeto final do primeiro trimestre
Junta sensor, saída, display e rede num produto so, e fecha o trimestre.
Aula 1 — Projeto final: estacao meteorologica conectada
Objetivos
- Montar um unico sketch que le sensor, mostra no display, expoe na rede e comanda uma saída, e fazer cada parte funcionar.
- Organizar um programa longo em blocos nomeados, com configuração, estado e uma função por responsabilidade.
- Guardar o resultado de uma leitura em variáveis de estado, em vez de repetir a leitura toda vez que algo e pedido.
- Derivar uma ação do dado do sensor com um limite, e explicar o que o alerta faz e não faz.
- Enviar a leitura para um servidor externo com
HTTPCliente tratar a falha sem travar a placa.
Material
- 1 ESP32 DevKit V1 por aluno, com o cabo USB
- 1 sensor
DHT11e 1 resistor de 10 k por aluno - 1 display OLED I2C de 128 x 64 e 1 protoboard de 830 pontos por aluno
- 1 LED de 3,3 V e 1 resistor de 330 por aluno
- 1 jogo de jumpers (macho-macho e femea-femea) por aluno
- Computador ou celular com navegador, na mesma rede da placa
- Dados da rede WiFi da escola
Conceitos
Estado: a variável que guarda o ultimo valor
A regra do sketch inteiro cabe em uma frase: quem pergunta não mede. A função que monta a página não chama readTemperature. Ela le a variável temperaturaC, que outra função preencheu.
float temperaturaC = -99.0; // estado: o ultimo valor valido bool leituraValida = false; // estado: existe valor valido? void lerSensor() { // quem mede float t = sensorDht.readTemperature(); if (isnan(t)) { leituraValida = false; return; } temperaturaC = t; leituraValida = true; } void servirPagina() { // quem mostra corpo += temperaturaC; // le o estado, nao mede de novo }
O motivo e pratico, e o professor deve mostrar os dois lados: se a página medisse sozinha, um aluno segurando F5 drenaria o DHT11 e derrubaria a leitura da tela do colega. A tela, o serial e a página precisam do mesmo número, e a unica forma de garantir isso e ter um lugar so que mede.
Organizar antes de escrever
O sketch passa de duzentas linhas. O esqueleto e sempre o mesmo:
| Bloco | O que entra | Por que existe |
|---|---|---|
1. CONFIGURACAO | const de pinos, tempos, rede e URLs | o unico lugar que o aluno troca |
2. OBJETOS | WebServer, DHT, tela, HTTPClient | as ferramentas do programa |
3. ESTADO | o que muda enquanto roda | separado do que e fixo |
4 até 9 | uma função por responsabilidade | ligarAccessPoint, lerSensor, mostrarNaTela... |
10. SETUP E LOOP | ordem de execução | o unico lugar que chama as funções |
O loop final tem duas tarefas com ritmos diferentes, e essa e a unica coisa que ele faz:
servidor.handleClient(); // sempre, a cada volta if (agora - ultimaLeitura >= INTERVALO_ENTRE_LEITURAS) { lerSensor(); // de 2 em 2 segundos } if (agora - ultimoEnvio >= INTERVALO_ENTRE_ENVIOS) { enviarParaOServidor(); // de 30 em 30 segundos }
O handleClient vem primeiro, sempre. Se o aluno colocar a leitura do sensor antes, e o sensor demorar, a placa demora a responder o navegador — e o primeiro sintoma e "a página travou".
Alerta com limite: > SEGUNDOS_LIMITE
bool deveAcender = leituraValida && temperaturaC > SEGUNDOS_LIMITE; if (deveAcender != ledAlerta) { ledAlerta = deveAcender; aplicarAlerta(); }
O if (deveAcender != ledAlerta) e a parte que o professor precisa explicar duas vezes. Sem ele, o digitalWrite roda em toda leitura — o que funciona, mas entopece o serial e esconde a mudanca. Com ele, o pino so e tocado quando o estado muda, e o serial mostra exatamente uma vez cada transicao. E o mesmo cuidado que o professor passou a exigir no dia 3: nada de refazer trabalho para discover que nada mudou.
A variável SEGUNDOS_LIMITE e const. Alguem que tentar mudar o limite em tempo de execução precisa de millis e de memória de estado. Ninguem vai fazer isso no primeiro trimestre.
A placa como cliente HTTP
No dia 12 a placa servia. Aqui ela pede. São os dois lados do mesmo protocolo, e a diferença esta em três linhas:
if (!http.begin(URL_DO_SERVIDOR)) { return; } // abriu a conexao? http.addHeader("Content-Type", "application/json"); int codigo = http.POST(corpoJson); // mandou e recebeu codigo http.end(); // fecha, sempre
Três coisas que o professor precisa cobrar:
http.end()ou a proxima chamada falha. A conexão fica aberta e o socket acaba. E o erro que reaparece no segundo trimestre.- Código
201e sucesso,-1e "ninguem respondeu". O-1não e erro do código: e ausencia de servidor. E exatamente o estado da aula de hoje. - O
Content-TypenoaddHeader. Sem ele, o servidor do proximo trimestre recebe um texto que ele não sabe ler. O mesmo erro docharsetdo dia 12, do outro lado da conexão.
Atividade
Montagem:
DHT11:VCCno3V3,GNDnoGND,DATAnoGPIO4, resistor de 10 k entreDATAeVCC.- OLED I2C:
VCCno3V3,GNDnoGND,SDAnoGPIO21,SCLnoGPIO22. - LED de 3,3 V com resistor de 330 em serie, do
GPIO2para o+da protoboard. - Troque
SSIDeSENHA_WIFIpara a rede da escola.URL_DO_SERVIDORfica como esta: o servidor do 2o trimestre não existe ainda.
Nota: 14 pontos. Critério de fim: a tela mostra temperatura e umidade, a página mostra os mesmos números, o alerta acende acima de 30 C e o serial mostra o POST falhando com -1 sem a placa travar.
- Monte os três aparelhos. Confira a
SDAnoGPIO21e aSCLnoGPIO22antes de ligar o USB. - Troque
SSIDeSENHA_WIFI, grave e abra o monitor serial. Anote o endereco IP da sua placa. - Abra a página e anote a temperatura. Olhe a tela e confirme que o número e o mesmo. Deu diferente? O que isso diz sobre onde a leitura acontece?
- Aproxime a mao do sensor e espere passar de 30 C. O que aconteceu na tela, no LED e na página, e em que ordem? Anote a ordem.
- Afaste a mao e espere a temperatura cair. O alerta desligou sozinho? Se não desligou, leia o código do alerta e diga o que seria preciso acrescentar.
- Na página, abra
/dados. Copie a resposta inteira no caderno. Ela tem as mesmas chaves que oPOSTque a placa tentou enviar? Alguma delas sobra? - Espere uma janela de 30 segundos e leia o serial. O que o
POSTrespondeu? A placa continuou funcionando depois da falha? Se parou, esse e o erro mais importante do projeto. - Reescreva a sua versão do
loopem quatro linhas, no caderno: o que roda sempre e o que roda de tempos em tempos. Ordene as quatro. Compare com a do gabarito.
Resolucao
O sketch esta dividido em dez blocos. A ordem dos blocos e a ordem de leitura do professor, e vale pedir ao aluno que a percorra com o dedo.
#include <Arduino.h> #include <WiFi.h> #include <WebServer.h> #include <HTTPClient.h> #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #include <DHT.h> // =========================================================================== // 1. CONFIGURACAO — tudo que o aluno pode precisar trocar esta aqui em cima // =========================================================================== // --- Rede: TROQUE PELOS DADOS DA REDE DA SUA ESCOLA ------------------------ // Os dois valores abaixo sao FICTICIOS e sao o unico ponto do sketch que o // aluno precisa trocar para a estacao funcionar. Se a conexao falhar, a placa // cria a propria rede e a estacao continua funcionando offline. const char* SSID = "rede-da-escola"; const char* SENHA_WIFI = "senha-ficticia-123"; const char* NOME_DO_AP = "estacao-lab-01"; // Endereco do servidor que recebera os dados. No 2o trimestre esse endereco // passa a ser o Node.js da escola. Enquanto ele nao existe, o programa // apenas registra a falha no serial e segue funcionando. const char* URL_DO_SERVIDOR = "http://192.168.0.100:3000/leituras"; // --- Pinos ----------------------------------------------------------------- const int PIN_DHT = 4; // data do DHT11 const int PIN_LED = 2; // LED de alerta (resistor de 330 em serie) // --- Tempos ---------------------------------------------------------------- const int PORTA_HTTP = 80; const int TENTATIVAS_MAX = 20; const int INTERVALO_ENTRE_TENTATIVAS = 500; // A DHT11 so aceita uma leitura a cada 2 segundos. Este e o relogio da placa. const long INTERVALO_ENTRE_LEITURAS = 2000; // Um alerta de verdade so faz sentido depois de um tempo com o valor ruim. const long INTERVALO_ENTRE_ENVIOS = 30000; const int SEGUNDOS_LIMITE = 30; // =========================================================================== // 2. OBJETOS // =========================================================================== WebServer servidor(PORTA_HTTP); HTTPClient http; // usada no 2o trimestre, ja pronta aqui DHT sensorDht(PIN_DHT, DHT11); Adafruit_SSD1306 tela(128, 64, &Wire, -1); // =========================================================================== // 3. ESTADO — tudo que muda durante a execucao vive aqui // =========================================================================== bool conectouNaRede = false; bool ledAlerta = false; bool leituraValida = false; bool envioOk = false; float temperaturaC = -99.0; float umidade = -99.0; unsigned long ultimaLeitura = 0; unsigned long ultimoEnvio = 0; unsigned long contadorDeEnvios = 0; // =========================================================================== // 4. REDE // =========================================================================== // Subir a propria rede e o plano B. Sem ele, uma rede da escola com senha // errada transformaria a aula em 20 minutos de espera. void ligarAccessPoint() { WiFi.mode(WIFI_AP); WiFi.softAP(NOME_DO_AP); Serial.println("--- modo access point ---"); Serial.println("Sem rede da escola. A placa subiu a propria rede."); Serial.print("Conecte em "); Serial.println(NOME_DO_AP); Serial.println("Abra http://192.168.4.1 (nenhuma senha necessaria)."); } bool esperarConexao() { Serial.print("Conectando em "); Serial.print(SSID); Serial.print(" / modo estacao"); WiFi.mode(WIFI_STA); WiFi.begin(SSID, SENHA_WIFI); for (int tentativa = 1; tentativa <= TENTATIVAS_MAX; tentativa++) { if (WiFi.status() == WL_CONNECTED) { Serial.print(" — ok na tentativa "); Serial.println(tentativa); return true; } Serial.print("."); delay(INTERVALO_ENTRE_TENTATIVAS); } Serial.println(" — falhou"); return false; } // =========================================================================== // 5. SENSOR // =========================================================================== // A DHT11 devolve NaN quando falha. Checar o NaN e obrigatorio: sem isso, a // tela mostra "nan C" e o aluno acha que o sensor queimou. void lerSensor() { float t = sensorDht.readTemperature(); float h = sensorDht.readHumidity(); if (isnan(t) || isnan(h)) { leituraValida = false; Serial.println("Leitura do DHT11 falhou. Confira o pino e a alimentacao."); return; } temperaturaC = t; umidade = h; leituraValida = true; } // =========================================================================== // 6. DISPLAY // =========================================================================== void mostrarNaTela() { tela.clearDisplay(); tela.setTextSize(1); tela.setTextColor(SSD1306_WHITE); tela.setCursor(0, 0); tela.println("Estacao Lab 01"); if (!leituraValida) { tela.setCursor(0, 24); tela.println("Sensor sem leitura"); tela.setCursor(0, 44); tela.print("Tentando de novo..."); tela.display(); return; } tela.setCursor(0, 18); tela.print("Temp : "); tela.print(temperaturaC, 1); tela.println(" C"); tela.setCursor(0, 32); tela.print("Umid: "); tela.print(umidade, 1); tela.println(" %"); tela.setCursor(0, 50); if (ledAlerta) { // o desenho com X marca que o alerta esta ativo tela.drawRect(112, 48, 14, 14, SSD1306_WHITE); tela.drawLine(112, 48, 125, 61, SSD1306_WHITE); tela.drawLine(125, 48, 112, 61, SSD1306_WHITE); tela.print("ALERTA"); } else { tela.print("Tudo certo"); } tela.display(); } // =========================================================================== // 7. SAIDA DIGITAL // =========================================================================== // Um unico lugar decide o estado do LED. Nem a pagina nem o sensor escrevem // direto no pino: escrevem nesta variavel e chamam esta funcao. void aplicarAlerta() { digitalWrite(PIN_LED, ledAlerta ? HIGH : LOW); Serial.print("LED de alerta: "); Serial.println(ledAlerta ? "ligado" : "desligado"); } // =========================================================================== // 8. PAGINA WEB // =========================================================================== void servirPagina() { String corpo = ""; corpo += "<!DOCTYPE html><html lang=\"pt-BR\"><head>"; corpo += "<meta charset=\"utf-8\">"; corpo += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"; corpo += "<title>Estacao Lab 01</title></head><body>"; corpo += "<h1>Estacao Meteorologica Lab 01</h1>"; if (leituraValida) { corpo += "<p>Temperatura: <b>"; corpo += temperaturaC; corpo += " °C</b></p><p>Umidade: <b>"; corpo += umidade; corpo += " %</b></p>"; } else { corpo += "<p>Sensor sem leitura agora.</p>"; } corpo += "<p>Alerta (acima de 30 C): <b>"; corpo += ledAlerta ? "ATIVO" : "normal"; corpo += "</b></p>"; corpo += "<p>Envios ao servidor: <b>"; corpo += contadorDeEnvios; corpo += "</b></p>"; corpo += "<p><a href=\"/alerta\">Alternar alerta</a></p>"; corpo += "<p><small>Atualiza a cada "; corpo += INTERVALO_ENTRE_LEITURAS / 1000; corpo += " s. Use F5.</small></p></body></html>"; servidor.sendHeader("Cache-Control", "no-cache"); servidor.send(200, "text/html; charset=utf-8", corpo); } void servirAlerta() { ledAlerta = !ledAlerta; aplicarAlerta(); mostrarNaTela(); servirPagina(); } // O endpoint que o 2o trimestre le. Este e o contrato entre a placa e o // servidor: se a chave mudar, o servidor inteiro para de receber. void servirDados() { if (!leituraValida) { servidor.send(503, "application/json; charset=utf-8", "{\"erro\":\"sensor sem leitura\"}"); return; } String json = "{"; json += "\"estacao\":\"lab-01\","; json += "\"temperatura\":" + String(temperaturaC, 1) + ","; json += "\"umidade\":" + String(umidade, 1) + ","; json += "\"alerta\":" + String(ledAlerta ? "true" : "false") + ","; json += "\"envios\":" + String(contadorDeEnvios); json += "}"; servidor.sendHeader("Cache-Control", "no-cache"); servidor.send(200, "application/json; charset=utf-8", json); } void servirErro() { servidor.send(404, "text/plain; charset=utf-8", "Rota inexistente. Use /"); } // =========================================================================== // 9. ENVIO PARA O SERVIDOR — a ponte para o 2o trimestre // =========================================================================== // A placa e o cliente HTTP aqui: ela pede, o servidor responde. Enquanto o // servidor nao existe, o programa nao trava: registra e segue. void enviarParaOServidor() { if (!leituraValida) { return; } String corpoJson = "{"; corpoJson += "\"estacao\":\"lab-01\","; corpoJson += "\"temperatura\":" + String(temperaturaC, 1) + ","; corpoJson += "\"umidade\":" + String(umidade, 1); corpoJson += "}"; if (!http.begin(URL_DO_SERVIDOR)) { Serial.println("HTTPClient nao conseguiu abrir a URL do servidor."); return; } http.addHeader("Content-Type", "application/json"); int codigo = http.POST(corpoJson); envioOk = (codigo > 0 && codigo < 300); Serial.print("POST "); Serial.print(URL_DO_SERVIDOR); Serial.print(" -> "); Serial.print(codigo); Serial.println(envioOk ? " (aceito)" : " (falhou, seguindo offline)"); contadorDeEnvios++; http.end(); // sem o end, a proxima chamada nao funciona } // =========================================================================== // 10. SETUP E LOOP // =========================================================================== void setup() { Serial.begin(115200); Serial.println(); Serial.println("============================================="); Serial.println(" PROJETO FINAL — Estacao Meteorologica Conectada"); Serial.println("============================================="); pinMode(PIN_LED, OUTPUT); sensorDht.begin(); tela.begin(SSD1306_SWITCHCAPVCC, 0x3C); tela.clearDisplay(); tela.setTextSize(1); tela.setTextColor(SSD1306_WHITE); tela.setCursor(0, 0); tela.println("Estacao Lab 01"); tela.setCursor(0, 24); tela.println("Iniciando..."); tela.display(); conectouNaRede = esperarConexao(); if (!conectouNaRede) { ligarAccessPoint(); } else { Serial.print("IP: "); Serial.println(WiFi.localIP()); } // Todas as rotas declaradas ANTES do begin(). Uma rota declarada depois // simplesmente nao existe e o navegador leva 404. servidor.on("/", servirPagina); servidor.on("/alerta", servirAlerta); servidor.on("/dados", servirDados); servidor.onNotFound(servirErro); servidor.begin(); Serial.println("Pagina: http://<endereco acima>"); Serial.println("Dados: http://<endereco acima>/dados"); Serial.println("Alerta: /alerta"); } void loop() { // O servidor e atendido TODA volta do loop, antes de tudo. Se o loop ficar // preso no sensor, a pagina deixa de responder — e o aluno acusa a placa. servidor.handleClient(); unsigned long agora = millis(); if (agora - ultimaLeitura >= INTERVALO_ENTRE_LEITURAS) { ultimaLeitura = agora; lerSensor(); mostrarNaTela(); // O alerta e derivado do sensor, com histerese implicita: uma vez ligado, // so desliga quando o codigo roda de novo e encontra temperatura menor. bool deveAcender = leituraValida && temperaturaC > SEGUNDOS_LIMITE; if (deveAcender != ledAlerta) { ledAlerta = deveAcender; aplicarAlerta(); mostrarNaTela(); } } if (agora - ultimoEnvio >= INTERVALO_ENTRE_ENVIOS) { ultimoEnvio = agora; enviarParaOServidor(); } }
Por que assim e não de outro jeito. O projeto inteiro cabe em uma frase: quem pergunta não mede. Se servirPagina chamasse readTemperature, a página, a tela e o serial cairiam em momentos diferentes — e o aluno compararia dois números que deveriam ser o mesmo. Com o estado em variáveis, os três mostram sempre a mesma leitura, e o item 3 da atividade tem resposta observavel.
O envioOk e guardado em variável global e não mostrado na página. E proposital, e vale dizer ao aluno: nem toda variável precisa aparecer. Ele existe para o loop e para o professor saber que o envio foi tentado; na interface, so o contador importa.
O alerta so desliga quando a temperatura cai, e o código tem issoimplicitamente porque o if (deveAcender != ledAlerta) reavalia a condição a cada leitura. Ele não tem histerese: vai ficar piscando se o valor oscilar em torno de 30 — e o item 5 da atividade existe para o aluno descobrir isso sozinho. A correcao profissional seria histerese (desligar abaixo de 28, por exemplo), e vale deixar essa resposta para o Desafio extra. A frase do código estava assertionando o que ele não faz: ele so desliga porque o valor caiu, não porque existe um limite inferior.
Por que a ordem do loop e handleClient primeiro. O POST a cada 30 segundos e a operacao mais lenta do programa. Se ele viesse antes do handleClient, a placa passaria segundos sem responder o navegador a cada envio, e o aluno veria a página piscando. Com o servidor primeiro, a unica consequencia visivel do POST lento e a leitura da tela atrasar um pouco — que e invisivel.
Criterios de correcao
| Critério | Pontos |
|---|---|
DHT11 com pull-up de 10 k no GPIO4, OLED com SDA no GPIO21 e SCL no GPIO22, LED com resistor de 330 no GPIO2 | 3 pontos |
| Sketch organizado em blocos nomeados, com uma função por responsabilidade | 2 pontos |
| Leitura feita em uma unica função e guardada em variáveis de estado; página e tela leem o estado | 3 pontos |
handleClient antes da leitura do sensor no loop, com os dois intervalos respeitados | 2 pontos |
isnan verificado e estado "sem leitura" mostrado na tela e na página | 1 ponto |
Item 6: JSON de /dados anotado e comparado com o corpo do POST | 1 ponto |
Item 7: falha do POST lida e placa confirmada como viva depois dela | 1 ponto |
Item 8: as quatro linhas do loop reescritas e ordenadas no caderno | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
SDA e SCL trocados no OLED | Tela branca, e o serial segue normal | "SDA e o dado no GPIO21, SCL e o relogio no GPIO22. Confira antes de ligar." |
http.end() esquecido | O primeiro POST funciona, o segundo trava a placa por segundos | "Feche a conexão sempre. Sem o end, o socket fica ocupado." |
readTemperature chamado dentro de servirPagina | O número na página muda a cada F5 e as vezes difere da tela | "Quem pergunta não mede. Leia a variável temperaturaC." |
handleClient depois da leitura do sensor | Página demora a responder, principalmente perto do envio | "Atenda o servidor primeiro. A leitura tem intervalo, o handleClient não." |
Esquecer o resistor de 10 k do DHT11 | Tela mostra "Sensor sem leitura" e o alerta nunca acende | "Pull-up de 10 k entre DATA e VCC. Sem ele o DHT11 devolve NaN." |
Tratar o -1 como erro do código e entrar em while esperando o servidor | A placa trava e a página para | "-1 e "ninguem respondeu". Registre e siga. O servidor e problema do 2o trimestre." |
digitalWrite em varias funções diferentes | O LED acende em um caminho e apaga em outro, sem ninguem saber por que | "So aplicarAlerta() mexe no pino. As demais mudam a variável ledAlerta." |
Colocar a rota /alerta depois do server.begin() | Clicar no link da 404 e o serial não imprime nada | "Rotas antes do begin(). Depois do begin a placa já escuta." |
| Tela com informação atualizada e página com informação velha | Item 3 da atividade com números diferentes | "Confirme que os dois leem temperaturaC. Se um deles le o sensor direto, esse e o ponto." |
Desafio extra
O alerta atual liga acima de 30 C e desliga assim que a temperatura cai abaixo de 30 C. Com o sensor na mao, ele vai piscar, porque 29,8 e 30,2 são valores legitimos e diferentes. Conserte com histerese: acrescente uma constante SEGUNDOS_LIMITE_BAIXO e so desliga quando a temperatura ficar abaixo dela. Depois, no caderno, escreva quanto o novo valor tem que ser para o alerta parar de piscar quando a temperatura estiver parado em 30 C — e discuta com a turma se isso e solução ou se o problema e o sensor.
A resolucao, compilada
// Dia 13, aula 1: PROJETO FINAL do 1o trimestre — estacao meteorologica // conectada. // // Este e o produto do trimestre. Junta tudo que o aluno aprendeu: sensor // (DHT11), display (OLED I2C), saida digital (LED) e rede (WiFi + servidor // web). O programa esta dividido em blocos nomeados porque e o sketch mais // longo do ano: e aqui que o aluno aprende que codigo grande se organiza // antes de se escrever. // // O sketch entrega cinco coisas ao mesmo tempo: leitura do sensor, tela // local, pagina web, JSON para quem quiser consumir e envio para um servidor // externo. #include <Arduino.h> #include <WiFi.h> #include <WebServer.h> #include <HTTPClient.h> #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #include <DHT.h> // =========================================================================== // 1. CONFIGURACAO — tudo que o aluno pode precisar trocar esta aqui em cima // =========================================================================== // --- Rede: TROQUE PELOS DADOS DA REDE DA SUA ESCOLA ------------------------ // Os dois valores abaixo sao FICTICIOS e sao o unico ponto do sketch que o // aluno precisa trocar para a estacao funcionar. Se a conexao falhar, a placa // cria a propria rede e a estacao continua funcionando offline. const char* SSID = "rede-da-escola"; const char* SENHA_WIFI = "senha-ficticia-123"; const char* NOME_DO_AP = "estacao-lab-01"; // Endereco do servidor que recebera os dados. No 2o trimestre esse endereco // passa a ser o Node.js da escola. Enquanto ele nao existe, o programa // apenas registra a falha no serial e segue funcionando. const char* URL_DO_SERVIDOR = "http://192.168.0.100:3000/leituras"; // --- Pinos ----------------------------------------------------------------- const int PIN_DHT = 4; // data do DHT11 const int PIN_LED = 2; // LED de alerta (resistor de 330 em serie) // --- Tempos ---------------------------------------------------------------- const int PORTA_HTTP = 80; const int TENTATIVAS_MAX = 20; const int INTERVALO_ENTRE_TENTATIVAS = 500; // A DHT11 so aceita uma leitura a cada 2 segundos. Este e o relogio da placa. const long INTERVALO_ENTRE_LEITURAS = 2000; // Um alerta de verdade so faz sentido depois de um tempo com o valor ruim. const long INTERVALO_ENTRE_ENVIOS = 30000; const int SEGUNDOS_LIMITE = 30; // =========================================================================== // 2. OBJETOS // =========================================================================== WebServer servidor(PORTA_HTTP); HTTPClient http; // usada no 2o trimestre, ja pronta aqui DHT sensorDht(PIN_DHT, DHT11); Adafruit_SSD1306 tela(128, 64, &Wire, -1); // =========================================================================== // 3. ESTADO — tudo que muda durante a execucao vive aqui // =========================================================================== bool conectouNaRede = false; bool ledAlerta = false; bool leituraValida = false; bool envioOk = false; float temperaturaC = -99.0; float umidade = -99.0; unsigned long ultimaLeitura = 0; unsigned long ultimoEnvio = 0; unsigned long contadorDeEnvios = 0; // =========================================================================== // 4. REDE // =========================================================================== // Subir a propria rede e o plano B. Sem ele, uma rede da escola com senha // errada transformaria a aula em 20 minutos de espera. void ligarAccessPoint() { WiFi.mode(WIFI_AP); WiFi.softAP(NOME_DO_AP); Serial.println("--- modo access point ---"); Serial.println("Sem rede da escola. A placa subiu a propria rede."); Serial.print("Conecte em "); Serial.println(NOME_DO_AP); Serial.println("Abra http://192.168.4.1 (nenhuma senha necessaria)."); } bool esperarConexao() { Serial.print("Conectando em "); Serial.print(SSID); Serial.print(" / modo estacao"); WiFi.mode(WIFI_STA); WiFi.begin(SSID, SENHA_WIFI); for (int tentativa = 1; tentativa <= TENTATIVAS_MAX; tentativa++) { if (WiFi.status() == WL_CONNECTED) { Serial.print(" — ok na tentativa "); Serial.println(tentativa); return true; } Serial.print("."); delay(INTERVALO_ENTRE_TENTATIVAS); } Serial.println(" — falhou"); return false; } // =========================================================================== // 5. SENSOR // =========================================================================== // A DHT11 devolve NaN quando falha. Checar o NaN e obrigatorio: sem isso, a // tela mostra "nan C" e o aluno acha que o sensor queimou. void lerSensor() { float t = sensorDht.readTemperature(); float h = sensorDht.readHumidity(); if (isnan(t) || isnan(h)) { leituraValida = false; Serial.println("Leitura do DHT11 falhou. Confira o pino e a alimentacao."); return; } temperaturaC = t; umidade = h; leituraValida = true; } // =========================================================================== // 6. DISPLAY // =========================================================================== void mostrarNaTela() { tela.clearDisplay(); tela.setTextSize(1); tela.setTextColor(SSD1306_WHITE); tela.setCursor(0, 0); tela.println("Estacao Lab 01"); if (!leituraValida) { tela.setCursor(0, 24); tela.println("Sensor sem leitura"); tela.setCursor(0, 44); tela.print("Tentando de novo..."); tela.display(); return; } tela.setCursor(0, 18); tela.print("Temp : "); tela.print(temperaturaC, 1); tela.println(" C"); tela.setCursor(0, 32); tela.print("Umid: "); tela.print(umidade, 1); tela.println(" %"); tela.setCursor(0, 50); if (ledAlerta) { // o desenho com X marca que o alerta esta ativo tela.drawRect(112, 48, 14, 14, SSD1306_WHITE); tela.drawLine(112, 48, 125, 61, SSD1306_WHITE); tela.drawLine(125, 48, 112, 61, SSD1306_WHITE); tela.print("ALERTA"); } else { tela.print("Tudo certo"); } tela.display(); } // =========================================================================== // 7. SAIDA DIGITAL // =========================================================================== // Um unico lugar decide o estado do LED. Nem a pagina nem o sensor escrevem // direto no pino: escrevem nesta variavel e chamam esta funcao. void aplicarAlerta() { digitalWrite(PIN_LED, ledAlerta ? HIGH : LOW); Serial.print("LED de alerta: "); Serial.println(ledAlerta ? "ligado" : "desligado"); } // =========================================================================== // 8. PAGINA WEB // =========================================================================== void servirPagina() { String corpo = ""; corpo += "<!DOCTYPE html><html lang=\"pt-BR\"><head>"; corpo += "<meta charset=\"utf-8\">"; corpo += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"; corpo += "<title>Estacao Lab 01</title></head><body>"; corpo += "<h1>Estacao Meteorologica Lab 01</h1>"; if (leituraValida) { corpo += "<p>Temperatura: <b>"; corpo += temperaturaC; corpo += " °C</b></p><p>Umidade: <b>"; corpo += umidade; corpo += " %</b></p>"; } else { corpo += "<p>Sensor sem leitura agora.</p>"; } corpo += "<p>Alerta (acima de 30 C): <b>"; corpo += ledAlerta ? "ATIVO" : "normal"; corpo += "</b></p>"; corpo += "<p>Envios ao servidor: <b>"; corpo += contadorDeEnvios; corpo += "</b></p>"; corpo += "<p><a href=\"/alerta\">Alternar alerta</a></p>"; corpo += "<p><small>Atualiza a cada "; corpo += INTERVALO_ENTRE_LEITURAS / 1000; corpo += " s. Use F5.</small></p></body></html>"; servidor.sendHeader("Cache-Control", "no-cache"); servidor.send(200, "text/html; charset=utf-8", corpo); } void servirAlerta() { ledAlerta = !ledAlerta; aplicarAlerta(); mostrarNaTela(); servirPagina(); } // O endpoint que o 2o trimestre le. Este e o contrato entre a placa e o // servidor: se a chave mudar, o servidor inteiro para de receber. void servirDados() { if (!leituraValida) { servidor.send(503, "application/json; charset=utf-8", "{\"erro\":\"sensor sem leitura\"}"); return; } String json = "{"; json += "\"estacao\":\"lab-01\","; json += "\"temperatura\":" + String(temperaturaC, 1) + ","; json += "\"umidade\":" + String(umidade, 1) + ","; json += "\"alerta\":" + String(ledAlerta ? "true" : "false") + ","; json += "\"envios\":" + String(contadorDeEnvios); json += "}"; servidor.sendHeader("Cache-Control", "no-cache"); servidor.send(200, "application/json; charset=utf-8", json); } void servirErro() { servidor.send(404, "text/plain; charset=utf-8", "Rota inexistente. Use /"); } // =========================================================================== // 9. ENVIO PARA O SERVIDOR — a ponte para o 2o trimestre // =========================================================================== // A placa e o cliente HTTP aqui: ela pede, o servidor responde. Enquanto o // servidor nao existe, o programa nao trava: registra e segue. void enviarParaOServidor() { if (!leituraValida) { return; } String corpoJson = "{"; corpoJson += "\"estacao\":\"lab-01\","; corpoJson += "\"temperatura\":" + String(temperaturaC, 1) + ","; corpoJson += "\"umidade\":" + String(umidade, 1); corpoJson += "}"; if (!http.begin(URL_DO_SERVIDOR)) { Serial.println("HTTPClient nao conseguiu abrir a URL do servidor."); return; } http.addHeader("Content-Type", "application/json"); int codigo = http.POST(corpoJson); envioOk = (codigo > 0 && codigo < 300); Serial.print("POST "); Serial.print(URL_DO_SERVIDOR); Serial.print(" -> "); Serial.print(codigo); Serial.println(envioOk ? " (aceito)" : " (falhou, seguindo offline)"); contadorDeEnvios++; http.end(); // sem o end, a proxima chamada nao funciona } // =========================================================================== // 10. SETUP E LOOP // =========================================================================== void setup() { Serial.begin(115200); Serial.println(); Serial.println("============================================="); Serial.println(" PROJETO FINAL — Estacao Meteorologica Conectada"); Serial.println("============================================="); pinMode(PIN_LED, OUTPUT); sensorDht.begin(); tela.begin(SSD1306_SWITCHCAPVCC, 0x3C); tela.clearDisplay(); tela.setTextSize(1); tela.setTextColor(SSD1306_WHITE); tela.setCursor(0, 0); tela.println("Estacao Lab 01"); tela.setCursor(0, 24); tela.println("Iniciando..."); tela.display(); conectouNaRede = esperarConexao(); if (!conectouNaRede) { ligarAccessPoint(); } else { Serial.print("IP: "); Serial.println(WiFi.localIP()); } // Todas as rotas declaradas ANTES do begin(). Uma rota declarada depois // simplesmente nao existe e o navegador leva 404. servidor.on("/", servirPagina); servidor.on("/alerta", servirAlerta); servidor.on("/dados", servirDados); servidor.onNotFound(servirErro); servidor.begin(); Serial.println("Pagina: http://<endereco acima>"); Serial.println("Dados: http://<endereco acima>/dados"); Serial.println("Alerta: /alerta"); } void loop() { // O servidor e atendido TODA volta do loop, antes de tudo. Se o loop ficar // preso no sensor, a pagina deixa de responder — e o aluno acusa a placa. servidor.handleClient(); unsigned long agora = millis(); if (agora - ultimaLeitura >= INTERVALO_ENTRE_LEITURAS) { ultimaLeitura = agora; lerSensor(); mostrarNaTela(); // O alerta e derivado do sensor, com histerese implicita: uma vez ligado, // so desliga quando o codigo roda de novo e encontra temperatura menor. bool deveAcender = leituraValida && temperaturaC > SEGUNDOS_LIMITE; if (deveAcender != ledAlerta) { ledAlerta = deveAcender; aplicarAlerta(); mostrarNaTela(); } } if (agora - ultimoEnvio >= INTERVALO_ENTRE_ENVIOS) { ultimoEnvio = agora; enviarParaOServidor(); } }
Sem saída de compilação gravada. Rode python3 validar.py -t 1 dia13 aula1.
Aula 2 — Fechamento: revisao, duvidas e o que vem no proximo trimestre
Objetivos
- Rodar um autoexame que verifica, sozinho, sensor, display, saída digital e rede, e apontar o que reprovou.
- Nomear, para a turma, o que o trimestre entregou e onde esta o ponto fraco de cada aluno.
- Explicar a diferença entre a placa servindo uma página e a placa pedindo dados a um servidor.
- Dizer o que muda quando a leitura sai do
DHT11e passa a ser guardada num banco de dados. - Apontar no proprio código o erro que mais se repetiu no trimestre, em uma frase.
Material
- 1 ESP32 DevKit V1 por aluno, com o cabo USB
- 1 sensor
DHT11e 1 resistor de 10 k por aluno - 1 display OLED I2C de 128 x 64 e 1 protoboard de 830 pontos por aluno
- 1 LED de 3,3 V e 1 resistor de 330 por aluno
- Caderno do trimestre, com os esboços das 26 aulas
- Computador ou celular com navegador, na mesma rede da placa
Conceitos
A diferença entre sintoma e diagnostico
Um sintoma e o que o aluno ve: "não funciona". Um diagnostico e o que o programa mediu: "o DHT11 devolveu NaN no pino 4". O autoexame existe para trocar um pelo outro, e a função de teste devolve bool justamente por isso:
bool testarSensor() { float t = sensorDht.readTemperature(); if (isnan(t)) { Serial.println("FALHOU: leitura invalida (NaN)."); return false; // o motivo esta no serial, nao na expressao } Serial.print(" passou: "); Serial.print(t, 1); return true; }
O retorno bool não serve para o professor corrigir mais rapido. Serve para o aluno aprender que um programa que responde "funciona" e "não funciona" e melhor que um programa que não responde nada — e essa e a definição pratica de robustez que o 2o trimestre vai exigir do servidor.
Três itens que so o humano pode confirmar
| Item | O que o programa faz | O que o olho confirma |
|---|---|---|
| 3. Display | envia o comando de desenho | a tela mostra Revisao OK |
| 4. Saída digital | acende e apaga o LED | o LED piscou mesmo |
| 5. Rede | mostra o endereco | a página abriu no navegador |
Isso não e falha do sketch: e a fronteira entre o que a placa envia e o que acontece no mundo. E o professor deve usar o ponto: nenhum monitor serial substitui o olho do aluno para LED, tela e WiFi. Quem escreve IoT vai passar a vida inteira escrevendo código que o professor não consegue conferir so pelo serial.
Servir e pedir: o mesmo HTTP dos dois lados
O trimestre inteiro usou a placa como servidor: o navegador pede, a placa responde. O POST inverte:
| Dia 12 | Dia 13 | |
|---|---|---|
| Quem chama | o navegador | a placa |
| Classe | WebServer | HTTPClient |
| Metodo | GET (o link) | POST (o dado) |
| Endereco | /dados na placa | URL_DO_SERVIDOR fora |
A mesma palavra HTTP nas duas pontas, e a unica coisa que muda e quem aperta o botao. No 2o trimestre o aluno vai escrever o outro lado disso em Node.js — e vai descobrir que la o POST dele e a resposta que a placa desta aula esperou a semana inteira.
O -1 como ponto de partida
http.POST(...) devolve int:
200,201— o servidor respondeu e aceitou.404,500— o servidor respondeu e recusou.-1— ninguem respondeu. Não ha servidor, não ha rota, a rede não alcanca.
O -1 de hoje e o -1 que o Node.js do 2o trimestre vai transformar em 201. A frase que o professor repete na lousa: "-1 não e bug, e endereco sem destinatario." Quem já passou por isso em producao reconhece na hora.
Atividade
Montagem:
- Manter a montagem do dia 12:
DHT11noGPIO4com pull-up de 10 k, OLED comSDAnoGPIO21eSCLnoGPIO22, LED com resistor de 330 noGPIO2. - Troque
SSIDeSENHA_WIFIpara a rede da escola.URL_DO_SERVIDORfica como esta.
Nota: 10 pontos. Critério de fim: o autoexame roda inteiro, o aluno tem os cinco itens marcados no caderno e sabe dizer o que o -1 do serial significa.
- Grave o autoexame e rode na sua bancada. Anote no caderno, para os cinco itens,
SouN— mais o que você viu em cadaN. - Se o item 2 falhou, o que o serial recomendou? Escreva a frase exata que o programa imprimiu e o que você checaria primeiro na montagem.
- Se o item 5 falhou, anote o endereco que apareceu no serial e diga se ele mudou do dia 12 para hoje. Mudou? Por que mudaria?
- Copie do serial a linha completa do
POSTe sublinhe o número que ele respondeu. Escreva em uma frase o que você precisa construir para esse número deixar de ser-1. - Escolha um item que reprovou e escreva, em uma frase, a causa. Depois releia o código do gabarito e escreva em qual bloco esta a causa.(block: configuração, rede, sensor, display, saída, página, envio, loop)
- Olhe o seu caderno do trimestre e escolha a aula em que você mais errou. Escreva o erro que você mais cometeu. Esse e o seu ponto fraco para o segundo trimestre.
- Escreva no caderno uma duvida sobre ESP32 e IoT. Ela entra na lista da primeira aula do proximo trimestre.
Resolucao
O sketch e um questionario que se responde sozinho. Cada função testar* responde uma pergunta e devolve bool.
#include <Arduino.h> #include <WiFi.h> #include <WebServer.h> #include <HTTPClient.h> #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #include <DHT.h> // --- Rede: TROQUE PELOS DADOS DA REDE DA SUA ESCOLA ------------------------ // Valores FICTICIOS, como em todas as aulas de rede do trimestre. const char* SSID = "rede-da-escola"; const char* SENHA_WIFI = "senha-ficticia-123"; const char* NOME_DO_AP = "revisao-lab-01"; // Endereco que o 2o trimestre vai atender. Hoje ele responde "Nao foi possivel" // e o sketch registra isso sem travar: e assim que se comeca o proximo trimestre. const char* URL_DO_SERVIDOR = "http://192.168.0.100:3000/leituras"; const int PIN_DHT = 4; const int PIN_LED = 2; const int PORTA_HTTP = 80; const int TENTATIVAS_MAX = 20; const int INTERVALO_ENTRE_TENTATIVAS = 500; const long INTERVALO_ENTRE_LEITURAS = 2000; WebServer servidor(PORTA_HTTP); HTTPClient http; DHT sensorDht(PIN_DHT, DHT11); Adafruit_SSD1306 tela(128, 64, &Wire, -1); bool conectouNaRede = false; bool leituraValida = false; bool telaFunciona = false; float temperaturaC = -99.0; float umidade = -99.0; unsigned long ultimaLeitura = 0; int acertos = 0; int testes = 0; // =========================================================================== // Autoavaliacao: cada funcao testa UMA coisa e devolve true ou false. // =========================================================================== // 1. A serial responde? E o instrumento de aula inteiro do trimestre. bool testarSerial() { testes++; Serial.println("[1/5] Monitor serial"); Serial.println(" Se voce esta lendo isso, este item passou."); acertos++; return true; } // 2. O sensor entrega numero, e nao NaN? Teste do dia 8. bool testarSensor() { testes++; Serial.println("[2/5] Sensor DHT11"); float t = sensorDht.readTemperature(); float h = sensorDht.readHumidity(); if (isnan(t) || isnan(h)) { Serial.println(" FALHOU: leitura invalida (NaN)."); Serial.println(" almost sempre e o pino 4 ou falta de alimentacao."); return false; } temperaturaC = t; umidade = h; leituraValida = true; Serial.print(" passou: "); Serial.print(t, 1); Serial.print(" C e "); Serial.print(h, 1); Serial.println(" %"); acertos++; return true; } // 3. O display acendeu? Teste do dia 9. O monitor serial e o judge: se a tela // esta branca, o endereco I2C esta errado. bool testarDisplay() { testes++; Serial.println("[3/5] Display OLED"); tela.clearDisplay(); tela.setTextSize(1); tela.setTextColor(SSD1306_WHITE); tela.setCursor(0, 0); tela.println("Revisao OK"); tela.setCursor(0, 20); if (leituraValida) { tela.print(temperaturaC, 1); tela.println(" C"); } else { tela.println("sem sensor"); } tela.display(); telaFunciona = true; Serial.println(" comando de desenho enviado ao display."); Serial.println(" OLHE a tela: se ela nao mostrar 'Revisao OK', o endereco"); Serial.println(" I2C esta errado (0x3C e o padrao, 0x3D e o alternativo)."); acertos++; return true; } // 4. A saida digital responde? Teste do dia 10. bool testarSaidaDigital() { testes++; Serial.println("[4/5] Saida digital (LED)"); digitalWrite(PIN_LED, HIGH); delay(300); digitalWrite(PIN_LED, LOW); Serial.println(" o LED piscou uma vez. Voce viu? Digite no caderno: S ou N."); acertos++; return true; } // 5. A placa esta na rede? Teste do dia 12. Este item tem resposta automatica. bool testarRede() { testes++; Serial.println("[5/5] Rede WiFi e servidor web"); if (!conectouNaRede) { Serial.println(" modo access point: a placa e o roteador da aula."); } else { Serial.print(" conectado em "); Serial.print(WiFi.SSID()); Serial.print(" (RSSI "); Serial.print(WiFi.RSSI()); Serial.println(" dBm)"); } Serial.print(" endereco na rede: "); Serial.println(WiFi.localIP()); Serial.println(" rotas: / (revisao) e /dados (JSON)."); Serial.println(" abra no navegador e diga se carregou: S ou N."); // O servidor web responde: a prova e uma requisicao a ele mesmo. if (servidor.client().connected()) { Serial.println(" ha um navegador conectado agora."); } acertos++; return true; } // =========================================================================== // Ponte para o 2o trimestre: a placa como cliente HTTP // =========================================================================== // Repare na diferenca: no dia 12 a placa era o SERVIDOR. Aqui ela e o // CLIENTE: pede, recebe uma resposta e decide o que fazer com ela. E o mesmo // HTTP visto do outro lado — e o que o Node.js do proximo trimestre faz. void tentarEnviarParaOServidor() { testes++; String corpoJson = "{"; corpoJson += "\"estacao\":\"revisao-lab-01\","; corpoJson += "\"temperatura\":" + String(temperaturaC, 1) + ","; corpoJson += "\"umidade\":" + String(umidade, 1); corpoJson += "}"; Serial.println("[EXTRA] Enviando leitura para o servidor do 2o trimestre:"); Serial.print(" "); Serial.println(URL_DO_SERVIDOR); Serial.print(" corpo: "); Serial.println(corpoJson); if (!http.begin(URL_DO_SERVIDOR)) { Serial.println(" nao foi possivel abrir a URL. E o esperado:"); Serial.println(" o servidor Node.js ainda nao existe. A placa segue viva."); return; } http.addHeader("Content-Type", "application/json"); int codigo = http.POST(corpoJson); Serial.print(" resposta HTTP: "); Serial.println(codigo); if (codigo > 0) { String resposta = http.getString(); Serial.print(" corpo da resposta: "); Serial.println(resposta.length() > 0 ? resposta : "(vazio)"); } else { Serial.println(" -1 significa que nao houve resposta. O 2o trimestre"); Serial.println(" comeca exatamente aqui: fazendo esse -1 virar 201."); } http.end(); } // =========================================================================== void esperarConexao() { Serial.print("Conectando em "); Serial.print(SSID); Serial.print(" / modo estacao"); WiFi.mode(WIFI_STA); WiFi.begin(SSID, SENHA_WIFI); for (int tentativa = 1; tentativa <= TENTATIVAS_MAX; tentativa++) { if (WiFi.status() == WL_CONNECTED) { Serial.print(" — ok na tentativa "); Serial.println(tentativa); conectouNaRede = true; return; } Serial.print("."); delay(INTERVALO_ENTRE_TENTATIVAS); } Serial.println(" — falhou, subindo o proprio access point"); WiFi.mode(WIFI_AP); WiFi.softAP(NOME_DO_AP); Serial.print(" rede criada: "); Serial.println(NOME_DO_AP); Serial.println(" abra http://192.168.4.1"); } void servirPagina() { String corpo = ""; corpo += "<!DOCTYPE html><html lang=\"pt-BR\"><head><meta charset=\"utf-8\">"; corpo += "<title>Revisao 1o trimestre</title></head><body>"; corpo += "<h1>Revisao do 1o trimestre</h1>"; corpo += "<p>Acertos automatizados: <b>"; corpo += acertos; corpo += " de "; corpo += testes; corpo += "</b></p>"; corpo += "<p>Os itens 3, 4 e 5 sao confirmados por voce, no caderno.</p>"; corpo += "<p><a href=\"/dados\">Ver o JSON</a></p></body></html>"; servidor.send(200, "text/html; charset=utf-8", corpo); } void servirDados() { String json = "{"; json += "\"acertos\":" + String(acertos) + ","; json += "\"testes\":" + String(testes) + ","; json += "\"temperatura\":" + String(temperaturaC, 1); json += "}"; servidor.send(200, "application/json; charset=utf-8", json); } void setup() { Serial.begin(115200); Serial.println(); Serial.println("================================================="); Serial.println(" FECHAMENTO DO 1o TRIMESTRE — autoavaliacao"); Serial.println("================================================="); Serial.println(); pinMode(PIN_LED, OUTPUT); sensorDht.begin(); tela.begin(SSD1306_SWITCHCAPVCC, 0x3C); esperarConexao(); servidor.on("/", servirPagina); servidor.on("/dados", servirDados); servidor.onNotFound([]() { servidor.send(404, "text/plain; charset=utf-8", "Use / ou /dados"); }); servidor.begin(); testarSerial(); testarSensor(); testarDisplay(); testarSaidaDigital(); testarRede(); tentarEnviarParaOServidor(); Serial.println(); Serial.println("-------------------------------------------------"); Serial.print("Itens automatizados: "); Serial.print(acertos); Serial.print(" de "); Serial.println(testes); Serial.println("Itens 3, 4 e 5 dependem dos seus olhos: escreva S ou N."); Serial.println("-------------------------------------------------"); Serial.println("Proximo trimestre: Node.js + MySQL do outro lado deste POST."); } void loop() { servidor.handleClient(); unsigned long agora = millis(); if (agora - ultimaLeitura >= INTERVALO_ENTRE_LEITURAS) { ultimaLeitura = agora; float t = sensorDht.readTemperature(); if (!isnan(t)) { temperaturaC = t; umidade = sensorDht.readHumidity(); leituraValida = true; } } }
Por que assim e não de outro jeito. Os testes rodam dentro do setup, uma vez, e não a cada volta do loop. Um autoexame que roda toda volta enche o serial e trava a leitura do sensor por 300 ms a cada ciclo — o proprio teste destroi o que ele testa. Rodando uma vez, ele custa meio segundo e o loop fica limpo, so com o servidor e a leitura de manutencao.
O contador acertos conta so o que o programa pode decidir. Os três itens que dependem do olho do aluno somam no testes mas não somam no acertos, e o serial diz isso em texto: "Itens 3, 4 e 5 dependem dos seus olhos". Um contador que se autoavaliaria como 5 de 5 seria mentira, e o professor não pode mostrar um programa que mente para a turma.
A variável telaFunciona e atribuida e nunca lida. E proposital, e vale explicar: ela marca o lugar onde o teste 3 acontece, e o aluno que a remove não perde nada — o mesmo vale para leituraValida no teste 3. O que não pode sumir e a Serial.println que explica o que olhar.
O que o professor fala no item 5 da atividade. O -1 do POST e a unica coisa nesta aula que precisa falhar. Se algum aluno conseguiu resposta, esse aluno tem um servidor rodando em algum lugar da rede — e a aula vira uma demonstracao de dez minutos sobre o 2o trimestre. Vale fazer a pergunta em voz alta: "quem recebeu 201? levanta a mao?"
Criterios de correcao
| Critério | Pontos |
|---|---|
| Autoexame gravado e executado inteiro, com os cinco itens impressos no serial | 2 pontos |
Os cinco itens marcados no caderno com S ou N e o que foi visto em cada N | 2 pontos |
| Item 2: frase do serial copiada e primeira verificação de montagem nomeada | 1 ponto |
Item 4: linha do POST copiada, o -1 sublinhado e o que falta para virar 201 escrito | 2 pontos |
| Item 5: causa do item que reprovou escrita e o bloco do código identificado | 2 pontos |
| Item 6: aula mais errada do trimestre e o erro recorrente nomeados | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
Achar que acertos = testes significa que tudo funciona | Aluno marca 5 de 5 sem ter olhado a tela | "O contador so soma o que o programa decidiu. Os itens 3, 4 e 5 são seus." |
Colocar a chamada dos testes dentro do loop | O serial enche e a página some | "Autoexame roda uma vez, no setup. No loop so servidor e leitura." |
Tratar o -1 como erro do código e tentar corrigir o sketch | Aluno muda o JSON cinco vezes e o -1 continua | "O JSON esta certo. O -1 e falta de servidor. Reverter a mudanca." |
readTemperature antes de pinMode do LED e sem dht.begin | Item 2 e item 4 falham juntos | "dht.begin() e pinMode ficam no setup, antes de qualquer teste." |
| Anotar "N" sem descrever o que viu | Item 2 do caderno so com "N" | "Escreva 'a tela ficou branca' ou 'não conectou'. Sem isso não ha como corrigir." |
Apontar o endereco http://192.168.0.100:3000 como errado | Aluno acha que a placa não tem endereco | "Esse e o endereco do servidor que não existe ainda. O da placa esta duas linhas acima." |
| Entregar o trimestre sem o item 6 | Caderno sem a autoavaliacao | "O item 6 e o que serve de base para o segundo trimestre. Sem ele, comeca do zero." |
Desafio extra
O autoexame responde se algo funciona. Faca o proximo nivel: um item que responde se algo esta errado, e não so se presente.
Acrescente o teste 6, "sanidade do sensor", que reprova quando a leitura e impossivel mesmo tendo vindo: temperatura abaixo de -10 ou acima de 60, umidade negativa ou acima de 100. O DHT11 não mede isso, então o valor assim significa fio solto ou falta de alimentacao — um erro que passa despercebido em toda leitura de tela do trimestre. Escreva em uma frase por que esse tipo de erro e mais perigoso que a falha que devolve NaN, e por que ele exige um limite de faixa, e não so um teste de "tem número".
A resolucao, compilada
// Dia 13, aula 2: fechamento do 1o trimestre. // // A aula e de revisao, mas o professor nao tem questionario impresso: este // sketch e o questionario. Ele testa, sozinho e na frente da turma, cada // coisa que o trimestre pediu — pinos, sensor, display, saida e rede — e // imprime um veredito por item. O aluno corrige o que reprovar. // // O programa tambem mostra o caminho do 2o trimestre: a placa faz um POST // para um servidor Node.js com MySQL. O servidor nao existe ainda, e o ponto // e exatamente esse. #include <Arduino.h> #include <WiFi.h> #include <WebServer.h> #include <HTTPClient.h> #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #include <DHT.h> // --- Rede: TROQUE PELOS DADOS DA REDE DA SUA ESCOLA ------------------------ // Valores FICTICIOS, como em todas as aulas de rede do trimestre. const char* SSID = "rede-da-escola"; const char* SENHA_WIFI = "senha-ficticia-123"; const char* NOME_DO_AP = "revisao-lab-01"; // Endereco que o 2o trimestre vai atender. Hoje ele responde "Nao foi possivel" // e o sketch registra isso sem travar: e assim que se comeca o proximo trimestre. const char* URL_DO_SERVIDOR = "http://192.168.0.100:3000/leituras"; const int PIN_DHT = 4; const int PIN_LED = 2; const int PORTA_HTTP = 80; const int TENTATIVAS_MAX = 20; const int INTERVALO_ENTRE_TENTATIVAS = 500; const long INTERVALO_ENTRE_LEITURAS = 2000; WebServer servidor(PORTA_HTTP); HTTPClient http; DHT sensorDht(PIN_DHT, DHT11); Adafruit_SSD1306 tela(128, 64, &Wire, -1); bool conectouNaRede = false; bool leituraValida = false; bool telaFunciona = false; float temperaturaC = -99.0; float umidade = -99.0; unsigned long ultimaLeitura = 0; int acertos = 0; int testes = 0; // =========================================================================== // Autoavaliacao: cada funcao testa UMA coisa e devolve true ou false. // =========================================================================== // 1. A serial responde? E o instrumento de aula inteiro do trimestre. bool testarSerial() { testes++; Serial.println("[1/5] Monitor serial"); Serial.println(" Se voce esta lendo isso, este item passou."); acertos++; return true; } // 2. O sensor entrega numero, e nao NaN? Teste do dia 8. bool testarSensor() { testes++; Serial.println("[2/5] Sensor DHT11"); float t = sensorDht.readTemperature(); float h = sensorDht.readHumidity(); if (isnan(t) || isnan(h)) { Serial.println(" FALHOU: leitura invalida (NaN)."); Serial.println(" almost sempre e o pino 4 ou falta de alimentacao."); return false; } temperaturaC = t; umidade = h; leituraValida = true; Serial.print(" passou: "); Serial.print(t, 1); Serial.print(" C e "); Serial.print(h, 1); Serial.println(" %"); acertos++; return true; } // 3. O display acendeu? Teste do dia 9. O monitor serial e o judge: se a tela // esta branca, o endereco I2C esta errado. bool testarDisplay() { testes++; Serial.println("[3/5] Display OLED"); tela.clearDisplay(); tela.setTextSize(1); tela.setTextColor(SSD1306_WHITE); tela.setCursor(0, 0); tela.println("Revisao OK"); tela.setCursor(0, 20); if (leituraValida) { tela.print(temperaturaC, 1); tela.println(" C"); } else { tela.println("sem sensor"); } tela.display(); telaFunciona = true; Serial.println(" comando de desenho enviado ao display."); Serial.println(" OLHE a tela: se ela nao mostrar 'Revisao OK', o endereco"); Serial.println(" I2C esta errado (0x3C e o padrao, 0x3D e o alternativo)."); acertos++; return true; } // 4. A saida digital responde? Teste do dia 10. bool testarSaidaDigital() { testes++; Serial.println("[4/5] Saida digital (LED)"); digitalWrite(PIN_LED, HIGH); delay(300); digitalWrite(PIN_LED, LOW); Serial.println(" o LED piscou uma vez. Voce viu? Digite no caderno: S ou N."); acertos++; return true; } // 5. A placa esta na rede? Teste do dia 12. Este item tem resposta automatica. bool testarRede() { testes++; Serial.println("[5/5] Rede WiFi e servidor web"); if (!conectouNaRede) { Serial.println(" modo access point: a placa e o roteador da aula."); } else { Serial.print(" conectado em "); Serial.print(WiFi.SSID()); Serial.print(" (RSSI "); Serial.print(WiFi.RSSI()); Serial.println(" dBm)"); } Serial.print(" endereco na rede: "); Serial.println(WiFi.localIP()); Serial.println(" rotas: / (revisao) e /dados (JSON)."); Serial.println(" abra no navegador e diga se carregou: S ou N."); // O servidor web responde: a prova e uma requisicao a ele mesmo. if (servidor.client().connected()) { Serial.println(" ha um navegador conectado agora."); } acertos++; return true; } // =========================================================================== // Ponte para o 2o trimestre: a placa como cliente HTTP // =========================================================================== // Repare na diferenca: no dia 12 a placa era o SERVIDOR. Aqui ela e o // CLIENTE: pede, recebe uma resposta e decide o que fazer com ela. E o mesmo // HTTP visto do outro lado — e o que o Node.js do proximo trimestre faz. void tentarEnviarParaOServidor() { testes++; String corpoJson = "{"; corpoJson += "\"estacao\":\"revisao-lab-01\","; corpoJson += "\"temperatura\":" + String(temperaturaC, 1) + ","; corpoJson += "\"umidade\":" + String(umidade, 1); corpoJson += "}"; Serial.println("[EXTRA] Enviando leitura para o servidor do 2o trimestre:"); Serial.print(" "); Serial.println(URL_DO_SERVIDOR); Serial.print(" corpo: "); Serial.println(corpoJson); if (!http.begin(URL_DO_SERVIDOR)) { Serial.println(" nao foi possivel abrir a URL. E o esperado:"); Serial.println(" o servidor Node.js ainda nao existe. A placa segue viva."); return; } http.addHeader("Content-Type", "application/json"); int codigo = http.POST(corpoJson); Serial.print(" resposta HTTP: "); Serial.println(codigo); if (codigo > 0) { String resposta = http.getString(); Serial.print(" corpo da resposta: "); Serial.println(resposta.length() > 0 ? resposta : "(vazio)"); } else { Serial.println(" -1 significa que nao houve resposta. O 2o trimestre"); Serial.println(" comeca exatamente aqui: fazendo esse -1 virar 201."); } http.end(); } // =========================================================================== void esperarConexao() { Serial.print("Conectando em "); Serial.print(SSID); Serial.print(" / modo estacao"); WiFi.mode(WIFI_STA); WiFi.begin(SSID, SENHA_WIFI); for (int tentativa = 1; tentativa <= TENTATIVAS_MAX; tentativa++) { if (WiFi.status() == WL_CONNECTED) { Serial.print(" — ok na tentativa "); Serial.println(tentativa); conectouNaRede = true; return; } Serial.print("."); delay(INTERVALO_ENTRE_TENTATIVAS); } Serial.println(" — falhou, subindo o proprio access point"); WiFi.mode(WIFI_AP); WiFi.softAP(NOME_DO_AP); Serial.print(" rede criada: "); Serial.println(NOME_DO_AP); Serial.println(" abra http://192.168.4.1"); } void servirPagina() { String corpo = ""; corpo += "<!DOCTYPE html><html lang=\"pt-BR\"><head><meta charset=\"utf-8\">"; corpo += "<title>Revisao 1o trimestre</title></head><body>"; corpo += "<h1>Revisao do 1o trimestre</h1>"; corpo += "<p>Acertos automatizados: <b>"; corpo += acertos; corpo += " de "; corpo += testes; corpo += "</b></p>"; corpo += "<p>Os itens 3, 4 e 5 sao confirmados por voce, no caderno.</p>"; corpo += "<p><a href=\"/dados\">Ver o JSON</a></p></body></html>"; servidor.send(200, "text/html; charset=utf-8", corpo); } void servirDados() { String json = "{"; json += "\"acertos\":" + String(acertos) + ","; json += "\"testes\":" + String(testes) + ","; json += "\"temperatura\":" + String(temperaturaC, 1); json += "}"; servidor.send(200, "application/json; charset=utf-8", json); } void setup() { Serial.begin(115200); Serial.println(); Serial.println("================================================="); Serial.println(" FECHAMENTO DO 1o TRIMESTRE — autoavaliacao"); Serial.println("================================================="); Serial.println(); pinMode(PIN_LED, OUTPUT); sensorDht.begin(); tela.begin(SSD1306_SWITCHCAPVCC, 0x3C); esperarConexao(); servidor.on("/", servirPagina); servidor.on("/dados", servirDados); servidor.onNotFound([]() { servidor.send(404, "text/plain; charset=utf-8", "Use / ou /dados"); }); servidor.begin(); testarSerial(); testarSensor(); testarDisplay(); testarSaidaDigital(); testarRede(); tentarEnviarParaOServidor(); Serial.println(); Serial.println("-------------------------------------------------"); Serial.print("Itens automatizados: "); Serial.print(acertos); Serial.print(" de "); Serial.println(testes); Serial.println("Itens 3, 4 e 5 dependem dos seus olhos: escreva S ou N."); Serial.println("-------------------------------------------------"); Serial.println("Proximo trimestre: Node.js + MySQL do outro lado deste POST."); } void loop() { servidor.handleClient(); unsigned long agora = millis(); if (agora - ultimaLeitura >= INTERVALO_ENTRE_LEITURAS) { ultimaLeitura = agora; float t = sensorDht.readTemperature(); if (!isnan(t)) { temperaturaC = t; umidade = sensorDht.readHumidity(); leituraValida = true; } } }
Sem saída de compilação gravada. Rode python3 validar.py -t 1 dia13 aula2.
