Projeto final do primeiro trimestre — Arduino e IoT — semana 13 do 1o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 13 · Projeto final do primeiro trimestre — Material de Apoio Arduino

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 HTTPClient e tratar a falha sem travar a placa.

Material

  • 1 ESP32 DevKit V1 por aluno, com o cabo USB
  • 1 sensor DHT11 e 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:

BlocoO que entraPor que existe
1. CONFIGURACAOconst de pinos, tempos, rede e URLso unico lugar que o aluno troca
2. OBJETOSWebServer, DHT, tela, HTTPClientas ferramentas do programa
3. ESTADOo que muda enquanto rodaseparado do que e fixo
4 até 9uma função por responsabilidadeligarAccessPoint, lerSensor, mostrarNaTela...
10. SETUP E LOOPordem de execuçãoo 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:

  1. http.end() ou a proxima chamada falha. A conexão fica aberta e o socket acaba. E o erro que reaparece no segundo trimestre.
  2. Código 201 e sucesso, -1 e "ninguem respondeu". O -1 não e erro do código: e ausencia de servidor. E exatamente o estado da aula de hoje.
  3. O Content-Type no addHeader. Sem ele, o servidor do proximo trimestre recebe um texto que ele não sabe ler. O mesmo erro do charset do dia 12, do outro lado da conexão.

Atividade

Montagem:

  • DHT11: VCC no 3V3, GND no GND, DATA no GPIO4, resistor de 10 k entre DATA e VCC.
  • OLED I2C: VCC no 3V3, GND no GND, SDA no GPIO21, SCL no GPIO22.
  • LED de 3,3 V com resistor de 330 em serie, do GPIO2 para o + da protoboard.
  • Troque SSID e SENHA_WIFI para a rede da escola. URL_DO_SERVIDOR fica 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.

  1. Monte os três aparelhos. Confira a SDA no GPIO21 e a SCL no GPIO22 antes de ligar o USB.
  2. Troque SSID e SENHA_WIFI, grave e abra o monitor serial. Anote o endereco IP da sua placa.
  3. 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?
  4. 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.
  5. 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.
  6. Na página, abra /dados. Copie a resposta inteira no caderno. Ela tem as mesmas chaves que o POST que a placa tentou enviar? Alguma delas sobra?
  7. Espere uma janela de 30 segundos e leia o serial. O que o POST respondeu? A placa continuou funcionando depois da falha? Se parou, esse e o erro mais importante do projeto.
  8. Reescreva a sua versão do loop em 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 += " &deg;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érioPontos
DHT11 com pull-up de 10 k no GPIO4, OLED com SDA no GPIO21 e SCL no GPIO22, LED com resistor de 330 no GPIO23 pontos
Sketch organizado em blocos nomeados, com uma função por responsabilidade2 pontos
Leitura feita em uma unica função e guardada em variáveis de estado; página e tela leem o estado3 pontos
handleClient antes da leitura do sensor no loop, com os dois intervalos respeitados2 pontos
isnan verificado e estado "sem leitura" mostrado na tela e na página1 ponto
Item 6: JSON de /dados anotado e comparado com o corpo do POST1 ponto
Item 7: falha do POST lida e placa confirmada como viva depois dela1 ponto
Item 8: as quatro linhas do loop reescritas e ordenadas no caderno1 ponto

Erros comuns

ErroComo apareceCorrecao
SDA e SCL trocados no OLEDTela branca, e o serial segue normal"SDA e o dado no GPIO21, SCL e o relogio no GPIO22. Confira antes de ligar."
http.end() esquecidoO 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 servirPaginaO 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 sensorPá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 DHT11Tela 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 servidorA 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 diferentesO 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 velhaItem 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 += " &deg;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 DHT11 e 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 DHT11 e 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

ItemO que o programa fazO que o olho confirma
3. Displayenvia o comando de desenhoa tela mostra Revisao OK
4. Saída digitalacende e apaga o LEDo LED piscou mesmo
5. Redemostra o enderecoa 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 12Dia 13
Quem chamao navegadora placa
ClasseWebServerHTTPClient
MetodoGET (o link)POST (o dado)
Endereco/dados na placaURL_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: DHT11 no GPIO4 com pull-up de 10 k, OLED com SDA no GPIO21 e SCL no GPIO22, LED com resistor de 330 no GPIO2.
  • Troque SSID e SENHA_WIFI para a rede da escola. URL_DO_SERVIDOR fica 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.

  1. Grave o autoexame e rode na sua bancada. Anote no caderno, para os cinco itens, S ou N — mais o que você viu em cada N.
  2. 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.
  3. 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?
  4. Copie do serial a linha completa do POST e sublinhe o número que ele respondeu. Escreva em uma frase o que você precisa construir para esse número deixar de ser -1.
  5. 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)
  6. 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.
  7. 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érioPontos
Autoexame gravado e executado inteiro, com os cinco itens impressos no serial2 pontos
Os cinco itens marcados no caderno com S ou N e o que foi visto em cada N2 pontos
Item 2: frase do serial copiada e primeira verificação de montagem nomeada1 ponto
Item 4: linha do POST copiada, o -1 sublinhado e o que falta para virar 201 escrito2 pontos
Item 5: causa do item que reprovou escrita e o bloco do código identificado2 pontos
Item 6: aula mais errada do trimestre e o erro recorrente nomeados1 ponto

Erros comuns

ErroComo apareceCorrecao
Achar que acertos = testes significa que tudo funcionaAluno 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 loopO 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 sketchAluno 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.beginItem 2 e item 4 falham juntos"dht.begin() e pinMode ficam no setup, antes de qualquer teste."
Anotar "N" sem descrever o que viuItem 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 erradoAluno 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 6Caderno 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.