WiFi e o primeiro dispositivo conectado — Arduino e IoT — semana 12 do 1o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 12 · WiFi e o primeiro dispositivo conectado — Material de Apoio Arduino

Semana 12 de 13· 1o trimestre · 01/02 a 30/04

WiFi e o primeiro dispositivo conectado

O salto do trimestre: sair da bancada e aparecer no mundo.

Aula 1 — Conectar no WiFi e servir uma página na placa

Objetivos

  • Trocar as constantes SSID e SENHA_WIFI pelos dados reais da rede e dizer por que a placa precisa delas.
  • Esperar a conexão com um contador de tentativas, em vez de um while que trava para sempre.
  • Publicar uma página web na placa com WebServer, server.on e server.handleClient.
  • Fazer a placa criar a propria rede com WiFi.softAP quando a rede da escola não esta disponivel.
  • Diagnosticar "o navegador não abre a página" separando problema de rede de problema de código.

Material

  • 1 ESP32 DevKit V1 por aluno, com o cabo USB
  • 1 protoboard de 830 pontos e 1 jogo de jumpers por aluno
  • Computador ou celular com navegador, na mesma rede da placa
  • Dados da rede WiFi da escola: nome e senha
  • Lista do kit impressa, uma por aluno

Conceitos

Modo estacao e modo access point

São os dois jeitos de o ESP32 estar numa rede, e escolher entre os dois e a decisao que torna esta aula possivel em qualquer escola.

ModoQuem cria a redeEndereco tipicoServe para
WIFI_STA (estacao)o roteador da escola192.168.0.107uso normal
WIFI_AP (access point)a propria placa192.168.4.1aula sem rede, sem senha, sem internet

No modo access point a placa vira o roteador: ela cria a rede, distribui endereco e responde as requisicoes. O computador precisa entrar nessa rede para falar com a placa, e não depende de nenhum outro aparelho. E por isso que a mesma aula funciona na escola A, com senha, e na escola B, sem senha nenhuma.

WiFi.begin e a espera obrigatoria

WiFi.begin(SSID, SENHA_WIFI) bloqueia a placa: a função comeca a procurar a rede e devolve o controle, mas o resto do programa so anda quando a placa já conectou. Conectar leva de um a dez segundos, e o professor precisa ver esse tempo passando.

O erro classico do aluno e o while sem contador:

while (WiFi.status() != WL_CONNECTED) {
  delay(500);
}

Com rede errada — e o caso normal, porque o aluno ainda não trocou a constante — esse laco não acaba nunca. A placa fica presa ali, sem atender o servidor e sem imprimir nada. A correcao e dar um limite:

for (int tentativa = 1; tentativa <= TENTATIVAS_MAX; tentativa++) {
  if (WiFi.status() == WL_CONNECTED) {
    return true;
  }
  delay(500);
}
return false;   // a funcao desiste e o professor decide o que fazer

A segunda parte importa tanto quanto o limite: quem desiste e a função, e o setup que chama. E por isso que o exemplo tem duas saídas — rede da escola, ou access point.

O endereco IP e a identidade da placa na rede

WiFi.localIP() e o endereco da placa dentro da rede. Ele muda a cada reinicio, porque o roteador empresta um endereco diferente. No access point ele e sempre 192.168.4.1.

ChamadaDevolve
WiFi.localIP()endereco IP da placa
WiFi.softAPIP()endereco da placa no modo access point
WiFi.SSID()nome da rede em que a placa esta conectada
WiFi.RSSI()forca do sinal em dBm, negativo, perto de zero e bom
WiFi.status()WL_CONNECTED quando ha conexão

E por isso que a ordem das linhas na setup importa: o server.begin() vem depois do WiFi.begin(). Servidor sem rede não tem endereco para escutar.

WebServer: três linhas e uma página

servidor.on("/", servirPagina);   // endereco que responde e o que ele responde
servidor.begin();                 // liga o servidor
// ...
servidor.handleClient();          // atende UMA requisicao por volta do loop

handleClient e obrigatorio no loop. Sem ele, a placa fica em silencio e o navegador da tempo esgotado depois de uns dez segundos — e o aluno conclui que a placa travou.

O server.onNotFound cobre o caso em que o aluno digita /pagina em vez de /: sem ele, o navegador recebe uma página vazia sem explicacao.

charset=utf-8 e o acento quebrado

O navegador recebe a página como bytes e precisa saber qual tabela usar para transformar esses bytes em letras. Sem dizer, ele assume a tabela antiga e cada acento vira dois caracteres:

servidor.send(200, "text/html; charset=utf-8", corpo);

E o erro mais chato do dia: o código esta perfeito, a página abre, e so o texto aparece com lixo. O aluno pensa que gravou o arquivo errado. A correcao cabe em seis caracteres no cabecalho.

Atividade

Montagem:

  • Nenhum circuito. Esta aula e so a placa, o cabo USB e o navegador.
  • O SSID e a SENHA_WIFI vao no código, não na montagem.

Nota: 12 pontos. Critério de fim: o monitor serial mostra um endereco IP e a página abre no navegador, em modo estacao ou em access point.

  1. Troque as duas constantes do topo do código: SSID e SENHA_WIFI. Nome da rede e senha da sua escola.
  2. Grave o sketch e abra o monitor serial a 115200. Anote quantos pontos . aparecem antes da conexão. Esse número e quanto tempo a placa esperou.
  3. Abra no navegador o endereco que apareceu no serial. Descreva o que você ve, em uma frase.
  4. Troque SSID para um nome que não existe (rede-que-nao-existe) e grave de novo. Cronometre quanto tempo a placa leva para desistir e subir o access point. O access point subiu? O que o serial mostrou?
  5. Conecte o celular na rede lab-esp32-01 e abra http://192.168.4.1. A página abriu? Se não abriu, o que você digitou?
  6. Volte o SSID para o valor correto. Escreva uma frase explicando por que o professor não escreve a senha da escola no material do curso.

Resolucao

O sketch tem quatro partes: esperar a conexão, criar a rede alternativa, montar a página e atender no loop.

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>

// --- Rede: TROQUE PELOS DADOS DA REDE DA SUA ESCOLA ------------------------
// Os dois valores abaixo sao FICTICIOS: nao existe uma rede "rede-da-escola"
// em lugar nenhum. E assim de proposito. Quando a conexao falha, a placa cria
// a propria rede (access point) e a aula continua funcionando na escola sem a
// senha do professor.
const char* SSID = "rede-da-escola";
const char* SENHA_WIFI = "senha-ficticia-123";

// Nome da rede que a placa cria quando nao encontra a rede da escola.
const char* NOME_DO_AP = "lab-esp32-01";

// Porta 80 e a porta padrao de pagina web. E por isso que o endereco no
// navegador nao precisa de ":80" escrito no final.
const int PORTA_HTTP = 80;

// WiFi.begin e BLOQUEANTE: enquanto a placa conecta, ela nao faz mais nada.
// 20 tentativas com 500 ms entre elas dao 10 segundos de espera maxima. O
// contador nao e enfeite: sem ele, um while travado para sempre quando o
// SSID nao existe — que e exatamente o caso deste exemplo.
const int TENTATIVAS_MAX = 20;
const int INTERVALO_ENTRE_TENTATIVAS = 500;

WebServer servidor(PORTA_HTTP);
bool conectouNaRede = false;
int pedidosRecebidos = 0;

// Espera a conexao contando as tentativas. Se o limite estourar, devolve false
// em vez de travar: quem chama decide o que fazer, e nao o while.
bool esperarConexao() {
  Serial.print("Procurando a rede ");
  Serial.print(SSID);
  Serial.println(" ...");

  WiFi.mode(WIFI_STA);
  WiFi.begin(SSID, SENHA_WIFI);

  for (int tentativa = 1; tentativa <= TENTATIVAS_MAX; tentativa++) {
    if (WiFi.status() == WL_CONNECTED) {
      Serial.print(" -> conectou na tentativa ");
      Serial.println(tentativa);
      return true;
    }
    Serial.print(".");
    delay(INTERVALO_ENTRE_TENTATIVAS);
  }

  Serial.println(" -> falhou");
  return false;
}

// Cria a propria rede quando nao ha rede da escola. A placa vira o roteador:
// o celular conecta nela e abre a pagina em 192.168.4.1.
void ligarAccessPoint() {
  WiFi.mode(WIFI_AP);
  WiFi.softAP(NOME_DO_AP);

  Serial.println("A placa virou a propria rede (modo access point).");
  Serial.print("Conecte o celular ou o computador na rede: ");
  Serial.println(NOME_DO_AP);
  Serial.print("E abra no navegador, sem senha: http://");
  Serial.println(WiFi.softAPIP());
}

// A pagina da aula. String concatena os valores: e assim que o HTML troca de
// numero sem o professor mexer no codigo.
void servirPagina() {
  pedidosRecebidos++;

  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 da bancada</title></head><body>";
  corpo += "<h1>Estacao da bancada</h1>";
  corpo += "<p>Esta pagina esta sendo servida pelo proprio ESP32.</p>";
  corpo += "<p>Rede: <b>";
  corpo += conectouNaRede ? SSID : NOME_DO_AP;
  corpo += "</b></p>";
  corpo += "<p>Endereco: <b>";
  corpo += WiFi.localIP().toString();
  corpo += "</b></p>";
  corpo += "<p>Requisicoes recebidas: <b>";
  corpo += pedidosRecebidos;
  corpo += "</b></p>";
  corpo += "<p>Liga ha <b>";
  corpo += millis() / 1000;
  corpo += " s</b></p>";
  corpo += "<p><a href=\"/\">Atualizar (F5)</a></p>";
  corpo += "</body></html>";

  // "charset=utf-8" no header e o que permite escrever "Estacao" com acento
  // dentro do HTML. Sem essa parte, o navegador le cada letra acentuada como
  // dois caracteres e a pagina aparece com um caracteres estranhos no lugar.
  servidor.sendHeader("Cache-Control", "no-cache");
  servidor.send(200, "text/html; charset=utf-8", corpo);
}

// Resposta de endereco que nao existe. Sem esta rota, o WebServer devolve uma
// pagina vazia e o aluno acha que a placa caiu.
void servirErro() {
  servidor.send(404, "text/plain; charset=utf-8",
                "Nao existe esta pagina. Tente http://");
  servidor.sendContent(WiFi.localIP().toString());
}

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println("Dia 12, aula 1 — WiFi e a primeira pagina na placa");
  Serial.println("---------------------------------------------");

  conectouNaRede = esperarConexao();

  if (conectouNaRede) {
    Serial.print("Rede da escola: ");
    Serial.println(WiFi.SSID());
    Serial.print("Forca do sinal (RSSI, em dBm): ");
    Serial.println(WiFi.RSSI());
    Serial.print("Abra no navegador: http://");
    Serial.println(WiFi.localIP());
  } else {
    ligarAccessPoint();
  }

  // Rota: endereco que responde e o que ele responde. Os dois sao declarados
  // antes de begin(), nunca depois.
  servidor.on("/", servirPagina);
  servidor.onNotFound(servirErro);
  servidor.begin();

  Serial.println("Servidor no ar. Aperte F5 na pagina para atualizar.");
}

void loop() {
  // handleClient e a unica coisa obrigatoria do loop quando ha servidor web.
  // Sem ela, a placa fica em silencio e o navegador da "tempo esgotado".
  servidor.handleClient();
}

Por que assim e não de outro jeito. A função esperarConexao devolve a resposta em vez de resolver o problema sozinha. A alternativa — a função decidir e trocar para access point por conta propria — parece mais curta e e pior: o aluno não consegue mais dizer em qual das duas redes esta, e quando a rede da escola cai no meio da aula, o sketch inteiro precisa ser reescrito para voltar ao estacao. Como a função devolve bool e o setup escolhe, cada caminho cabe em três linhas e o aluno le qual esta rodando no serial.

A página e montada com String e += em vez de um bloco gigante de server.send. Motivo: o contador de requisicoes precisa dentro do HTML. Com o texto fixo, o número nunca entraria na página; com String, o mesmo código serve para rede e para access point porque os dois nomes são variáveis.

O servidor.sendHeader("Cache-Control", "no-cache") resolve um problema que so aparece na segunda aula. Sem ele, o navegador guarda a página e o F5 mostra sempre a versão antiga — o aluno mexe no código, regrava, aperta F5 e conclui que a placa não mudou.

O que o professor fala enquanto o aluno espera. Os dez segundos de WiFi.begin não são tempo morto: e a unica vez no ano em que a placa fica parada sem fazer nada, e vale apontar que o Serial.print(".") dentro do laco e o unico lugar do curso onde imprimir e uma forma de mostrar progresso.

Criterios de correcao

CritérioPontos
SSID e SENHA_WIFI trocados pelos dados da rede, declarados em const char* no topo2 pontos
WiFi.begin seguido de espera com contador de tentativas, sem while infinito3 pontos
server.on e server.begin no setup, server.handleClient no loop2 pontos
Header text/html; charset=utf-8 no send da página2 pontos
Endereco IP anotado e item 3 respondido em uma frase1 ponto
Item 4: tempo de espera até o access point cronometrado e descrito1 ponto
Item 6: razão de não gravar a senha da escola no material1 ponto

Erros comuns

ErroComo apareceCorrecao
while (WiFi.status() != WL_CONNECTED) sem contadorA placa para depois dos pontos e não imprime mais nada, nem o access point"O while não tem fim quando a rede não existe. Troque por for com TENTATIVAS_MAX e faca a função devolver true ou false."
server.begin() antes do WiFi.beginSerial mostra que conectou, mas o navegador da tempo esgotado"O servidor precisa de endereco para escutar. Conecte primeiro, suba o servidor depois."
Esquecer o handleClient no loopA página abre uma vez no primeiro acesso e depois morre"handleClient atende UMA requisicao por volta. Sem ele no loop, a segunda página não vem."
text/html sem charset=utf-8Página abre, texto aparece com ç, ã no lugar dos acentos"O navegador precisa saber a tabela. Use text/html; charset=utf-8."
Conectar no access point e continuar na rede da escolaO celular não acha lab-esp32-01 na lista de redes"Sai da rede da escola antes de procurar a rede da placa. Duas redes com o mesmo celular não funciona."
Digitar 192.168.4.1 no modo estacao"Não posso acessar este site""Esse endereco so vale no access point. Em modo estacao o endereco e o que apareceu no serial."
Gravar a senha da escola no código e compartilhar o arquivoColega recebe o .ino com a senha dentro"A senha não vai no material nem no repositorio do curso. A variável SENHA_WIFI e o ponto de troca."
Colocar o WiFi.begin dentro do loopA placa reconecta a cada volta e a página some"WiFi.begin e uma vez, no setup. No loop so quem atende e o handleClient."

Desafio extra

A placa já e um roteador. Agora faca dela um roteador que também responde: acrescente uma segunda rota em servidor.on("/status", ...), que mostre quanto tempo a placa esta no ar em minutos, usando millis(). Depois escreva no monitor serial, a cada dez segundos, quantas requisicoes chegaram — e responda: quantas requisicoes o professor fez até agora? Para conferir, ele olha o contador da página.

>

A resolucao, compilada

// Dia 12, aula 1: conectar no WiFi e servir uma pagina na placa.
//
// Este e o primeiro sketch do curso que sai da bancada. Depois de gravar, o
// endereco que aparece no monitor serial e uma pagina de verdade, aberta no
// navegador de qualquer computador da mesma rede.

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>

// --- Rede: TROQUE PELOS DADOS DA REDE DA SUA ESCOLA ------------------------
// Os dois valores abaixo sao FICTICIOS: nao existe uma rede "rede-da-escola"
// em lugar nenhum. E assim de proposito. Quando a conexao falha, a placa cria
// a propria rede (access point) e a aula continua funcionando na escola sem a
// senha do professor.
const char* SSID = "rede-da-escola";
const char* SENHA_WIFI = "senha-ficticia-123";

// Nome da rede que a placa cria quando nao encontra a rede da escola.
const char* NOME_DO_AP = "lab-esp32-01";

// Porta 80 e a porta padrao de pagina web. E por isso que o endereco no
// navegador nao precisa de ":80" escrito no final.
const int PORTA_HTTP = 80;

// WiFi.begin e BLOQUEANTE: enquanto a placa conecta, ela nao faz mais nada.
// 20 tentativas com 500 ms entre elas dao 10 segundos de espera maxima. O
// contador nao e enfeite: sem ele, um while travado para sempre quando o
// SSID nao existe — que e exatamente o caso deste exemplo.
const int TENTATIVAS_MAX = 20;
const int INTERVALO_ENTRE_TENTATIVAS = 500;

WebServer servidor(PORTA_HTTP);
bool conectouNaRede = false;
int pedidosRecebidos = 0;

// Espera a conexao contando as tentativas. Se o limite estourar, devolve false
// em vez de travar: quem chama decide o que fazer, e nao o while.
bool esperarConexao() {
  Serial.print("Procurando a rede ");
  Serial.print(SSID);
  Serial.println(" ...");

  WiFi.mode(WIFI_STA);
  WiFi.begin(SSID, SENHA_WIFI);

  for (int tentativa = 1; tentativa <= TENTATIVAS_MAX; tentativa++) {
    if (WiFi.status() == WL_CONNECTED) {
      Serial.print(" -> conectou na tentativa ");
      Serial.println(tentativa);
      return true;
    }
    Serial.print(".");
    delay(INTERVALO_ENTRE_TENTATIVAS);
  }

  Serial.println(" -> falhou");
  return false;
}

// Cria a propria rede quando nao ha rede da escola. A placa vira o roteador:
// o celular conecta nela e abre a pagina em 192.168.4.1.
void ligarAccessPoint() {
  WiFi.mode(WIFI_AP);
  WiFi.softAP(NOME_DO_AP);

  Serial.println("A placa virou a propria rede (modo access point).");
  Serial.print("Conecte o celular ou o computador na rede: ");
  Serial.println(NOME_DO_AP);
  Serial.print("E abra no navegador, sem senha: http://");
  Serial.println(WiFi.softAPIP());
}

// A pagina da aula. String concatena os valores: e assim que o HTML troca de
// numero sem o professor mexer no codigo.
void servirPagina() {
  pedidosRecebidos++;

  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 da bancada</title></head><body>";
  corpo += "<h1>Estacao da bancada</h1>";
  corpo += "<p>Esta pagina esta sendo servida pelo proprio ESP32.</p>";
  corpo += "<p>Rede: <b>";
  corpo += conectouNaRede ? SSID : NOME_DO_AP;
  corpo += "</b></p>";
  corpo += "<p>Endereco: <b>";
  corpo += WiFi.localIP().toString();
  corpo += "</b></p>";
  corpo += "<p>Requisicoes recebidas: <b>";
  corpo += pedidosRecebidos;
  corpo += "</b></p>";
  corpo += "<p>Liga ha <b>";
  corpo += millis() / 1000;
  corpo += " s</b></p>";
  corpo += "<p><a href=\"/\">Atualizar (F5)</a></p>";
  corpo += "</body></html>";

  // "charset=utf-8" no header e o que permite escrever "Estacao" com acento
  // dentro do HTML. Sem essa parte, o navegador le cada letra acentuada como
  // dois caracteres e a pagina aparece com um caracteres estranhos no lugar.
  servidor.sendHeader("Cache-Control", "no-cache");
  servidor.send(200, "text/html; charset=utf-8", corpo);
}

// Resposta de endereco que nao existe. Sem esta rota, o WebServer devolve uma
// pagina vazia e o aluno acha que a placa caiu.
void servirErro() {
  servidor.send(404, "text/plain; charset=utf-8",
                "Nao existe esta pagina. Tente http://");
  servidor.sendContent(WiFi.localIP().toString());
}

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println("Dia 12, aula 1 — WiFi e a primeira pagina na placa");
  Serial.println("---------------------------------------------");

  conectouNaRede = esperarConexao();

  if (conectouNaRede) {
    Serial.print("Rede da escola: ");
    Serial.println(WiFi.SSID());
    Serial.print("Forca do sinal (RSSI, em dBm): ");
    Serial.println(WiFi.RSSI());
    Serial.print("Abra no navegador: http://");
    Serial.println(WiFi.localIP());
  } else {
    ligarAccessPoint();
  }

  // Rota: endereco que responde e o que ele responde. Os dois sao declarados
  // antes de begin(), nunca depois.
  servidor.on("/", servirPagina);
  servidor.onNotFound(servirErro);
  servidor.begin();

  Serial.println("Servidor no ar. Aperte F5 na pagina para atualizar.");
}

void loop() {
  // handleClient e a unica coisa obrigatoria do loop quando ha servidor web.
  // Sem ela, a placa fica em silencio e o navegador da "tempo esgotado".
  servidor.handleClient();
}

Sem saída de compilação gravada. Rode python3 validar.py -t 1 dia12 aula1.

Aula 2 — Servidor web com dado do sensor e comando da placa

Objetivos

  • Declarar mais rotas com server.on e explicar o que cada rota responde.
  • Montar uma página que mostra a leitura atual do DHT11 e recarrega sozinha.
  • Receber um comando do navegador e mudar o estado de um pino a partir dele.
  • Publicar a mesma informação em JSON numa rota separada e dizer para que serve.
  • Manter a leitura do sensor em um intervalo valido mesmo com um servidor web rodando.

Material

  • 1 ESP32 DevKit V1 por aluno, com o cabo USB
  • 1 sensor DHT11 e 1 resistor de 10 k por aluno
  • 1 protoboard de 830 pontos e 1 jogo de jumpers por aluno
  • 1 LED de 3,3 V e 1 resistor de 330 por aluno
  • Computador ou celular com navegador, na mesma rede da placa
  • Dados da rede WiFi da escola

Conceitos

Rota: endereco que responde e o que responde

servidor.on("/", servirPagina);       // a raiz responde a pagina
servidor.on("/liga", servirLiga);     // /liga acende o LED
servidor.on("/dados", servirDados);   // /dados devolve JSON
servidor.onNotFound(servirErro);      // qualquer outra coisa: 404

Toda rota recebe uma função, e a função decide o que responder. Quatro rotas, quatro funções. O que o professor quer que o aluno perceba e que o estado da placa esta num if la dentro da função da rota — o if não esta no loop, e o navegador que escolhe qual if roda.

O link como botao, sem JavaScript

<a href="/liga">Ligar LED</a> | <a href="/desliga">Desligar LED</a>

O link e um GET: o navegador pede a URL e mostra o que veio. Não existe formulario, não existe fetch, não existe setInterval. O F5 atualiza a página e a leitura nova aparece — que e o suficiente para provar que a placa esta viva na rede.

O aluno que perguntar "e para atualizar sozinho?" recebe a resposta honesta: hoje e com F5. Na aula 13, o meta refresh resolve sem JavaScript. Esse e o nivel de complicacao correto para o trimestre.

dados: a mesma informação, sem HTML

String json = "{";
json += "\"temperatura\":" + String(temperaturaC, 1) + ",";
json += "\"umidade\":" + String(umidade, 1) + ",";
json += "\"led\":" + String(ledAceso ? "true" : "false");
json += "}";
servidor.send(200, "application/json; charset=utf-8", json);
RotaTipoQuem usa
/text/htmlpessoa, no navegador
/dadosapplication/jsonprograma, no 2o trimestre

A distincao e a unica coisa que o professor precisa fazer o aluno escrever no caderno: página e para olho, JSON e para programa. O String(temperaturaC, 1) e o que garante uma casa decimal. Sem o 1, o JSON manda o float do processador inteiro, com quinze digitos, e o professor usa isso como piada.

NaN e o intervalo de 2 segundos

O DHT11 so aceita uma leitura a cada 2 segundos. Ler antes disso devolve NaN — que não e zero, e "não sei". Por isso o loop não le o sensor em toda volta:

unsigned long agora = millis();
if (agora - ultimaLeitura < INTERVALO_ENTRE_LEITURAS) {
  return;
}
ultimaLeitura = agora;

Esse return e a peca central da aula. O loop tem duas tarefas com ritmos diferentes: atender o servidor, sempre, e ler o sensor, de dois em dois segundos. O return faz a placa pular a leitura e voltar para o handleClient. O aluno que remove o return ve a página dying e o serial reclamando do DHT11 ao mesmo tempo.

A função aplicarEstadoDoLed existe pelo mesmo motivo: o digitalWrite so acontece dentro dela. Nem a rota /liga nem a rota /desliga tocam no pino — elas mudam a variável e chamam a função. Quem decide o estado do pino e sempre o mesmo lugar.

Atividade

Montagem:

  • DHT11: VCC no 3V3, GND no GND, DATA no GPIO4.
  • Resistor de 10 k entre DATA e VCC do sensor. Sem esse resistor a leitura volta nan.
  • LED de 3,3 V com resistor de 330 em serie, do GPIO2 para o + da protoboard.
  • Altere as constantes SSID e SENHA_WIFI para a rede da escola.

Nota: 13 pontos. Critério de fim: a página mostra temperatura e umidade, o link /liga acende o LED e o serial imprime a mudanca de estado.

  1. Monte o circuito do enunciado. Sem o resistor de 10 k, pare e confira antes de gravar.
  2. Grave o sketch e abra http://<endereco do serial>. A temperatura e a umidade aparecem? Anote os dois valores no caderno.
  3. Clique em Ligar LED. O que o monitor serial imprime? O LED acendeu ao mesmo tempo? Marque os dois horarios.
  4. Clique em Desligar LED. O serial imprime de novo? Se você olhar so o navegador, como saberia que o comando chegou?
  5. Abra http://<endereco do serial>/liga escrevendo a URL na mao, sem clicar no link. Funciona igual? O que isso prova sobre o link da página?
  6. Abra /dados. Escreva no caderno a resposta exata que apareceu. Quantas casas decimais tem a temperatura? Se você remover o , 1 do String, quantas casas aparecem?
  7. Repare no return do loop. Remova a linha do return e grave. Descreva o que o serial e a página fazem. Depois volte a linha.

Resolucao

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <DHT.h>

// --- Rede: TROQUE PELOS DADOS DA REDE DA SUA ESCOLA ------------------------
// Os dois valores abaixo sao FICTICIOS: nao existe uma rede "rede-da-escola".
// Sao eles que o aluno troca para a aula funcionar na escola. Se a conexao
// falhar, a placa cria a propria rede e a aula continua.
const char* SSID = "rede-da-escola";
const char* SENHA_WIFI = "senha-ficticia-123";

const char* NOME_DO_AP = "lab-esp32-02";

// --- Pinos -----------------------------------------------------------------
const int PIN_DHT = 4;      // data do DHT11
const int PIN_LED = 2;      // LED de estado, com resistor de 330 em serie

const int PORTA_HTTP = 80;
const int TENTATIVAS_MAX = 20;
const int INTERVALO_ENTRE_TENTATIVAS = 500;

// A DHT11 aceita no maximo uma leitura a cada 2 segundos. Ler mais rapido que
// isso devolve NaN — e o "erro" que o aluno ve quando esquece deste intervalo.
const long INTERVALO_ENTRE_LEITURAS = 2000;

WebServer servidor(PORTA_HTTP);
DHT sensorDht(PIN_DHT, DHT11);

bool conectouNaRede = false;
bool ledAceso = false;
unsigned long ultimaLeitura = 0;

// Ultima leitura valida, comecando em valores impossiveis. Assim o primeiro
// HTTP nao devolve zero: o aluno ve que o sensor ja foi lido antes.
float temperaturaC = -99.0;
float umidade = -99.0;
bool leituraValida = false;

// Internet nao e garantida na escola. A placa sobe como access point quando
// falha, e o endereco muda: 192.168.4.1 sem depender de roteador.
void ligarAccessPoint() {
  WiFi.mode(WIFI_AP);
  WiFi.softAP(NOME_DO_AP);
  Serial.println("Sem rede da escola: a placa subiu o proprio access point.");
  Serial.println("Conecte em lab-esp32-02 e abra http://192.168.4.1");
}

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(" — conectou na tentativa ");
      Serial.println(tentativa);
      return true;
    }
    Serial.print(".");
    delay(INTERVALO_ENTRE_TENTATIVAS);
  }
  Serial.println(" — nao conectou");
  return false;
}

// LED: funcao propria em vez de digitalWrite espalhado no codigo. O comando
// chega da web, mas quem decide o estado do pino e sempre esta funcao.
void aplicarEstadoDoLed() {
  digitalWrite(PIN_LED, ledAceso ? HIGH : LOW);
  Serial.print("LED agora: ");
  Serial.println(ledAceso ? "ligado" : "desligado");
}

// GET / — a pagina: mostra a leitura e traz o botao que manda o comando.
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 da bancada</title></head><body>";
  corpo += "<h1>Estacao da bancada</h1>";

  if (leituraValida) {
    corpo += "<p>Temperatura: <b>";
    corpo += temperaturaC;
    corpo += " &deg;C</b></p>";
    corpo += "<p>Umidade: <b>";
    corpo += umidade;
    corpo += " %</b></p>";
  } else {
    corpo += "<p>Aguardando a primeira leitura do sensor...</p>";
  }

  corpo += "<p>LED: <b>";
  corpo += ledAceso ? "ligado" : "desligado";
  corpo += "</b></p>";

  // O link vira GET em /liga. Nao ha formulario nem JavaScript: a coisa mais
  // simples que o navegador entende.
  corpo += "<p><a href=\"/liga\">Ligar LED</a> | ";
  corpo += "<a href=\"/desliga\">Desligar LED</a></p>";

  corpo += "<p><small>Atualizado a cada ";
  corpo += INTERVALO_ENTRE_LEITURAS / 1000;
  corpo += " s. Recarregue com F5.</small></p>";
  corpo += "</body></html>";

  servidor.sendHeader("Cache-Control", "no-cache");
  servidor.send(200, "text/html; charset=utf-8", corpo);
}

// GET /liga e GET /desliga — o comando chega pela URL, nao pelo codigo.
void servirLiga() {
  ledAceso = true;
  aplicarEstadoDoLed();
  servirPagina();   // volta para a pagina: o aluno ve o resultado do comando
}

void servirDesliga() {
  ledAceso = false;
  aplicarEstadoDoLed();
  servirPagina();
}

// GET /dados — a mesma informacao, sem HTML. E este endpoint que o 2o
// trimestre vai ler de um servidor Node.js: ele pede /dados e recebe JSON.
void servirDados() {
  if (!leituraValida) {
    servidor.send(503, "application/json; charset=utf-8",
                  "{\"erro\":\"sensor sem leitura\"}");
    return;
  }

  String json = "{";
  json += "\"temperatura\":" + String(temperaturaC, 1) + ",";
  json += "\"umidade\":" + String(umidade, 1) + ",";
  json += "\"led\":" + String(ledAceso ? "true" : "false") + ",";
  json += "\"dispositivo\":\"esp32-bancada\"";
  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 /");
}

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println("Dia 12, aula 2 — sensor e comando pela web");
  Serial.println("-----------------------------------------");

  pinMode(PIN_LED, OUTPUT);
  sensorDht.begin();
  conectouNaRede = esperarConexao();
  if (!conectouNaRede) {
    ligarAccessPoint();
  }

  servidor.on("/", servirPagina);
  servidor.on("/liga", servirLiga);
  servidor.on("/desliga", servirDesliga);
  servidor.on("/dados", servirDados);
  servidor.onNotFound(servirErro);
  servidor.begin();

  Serial.print("Endereco: http://");
  Serial.println(WiFi.localIP());
  Serial.println("Rotas: /  /liga  /desliga  /dados");
  Serial.println("Coloque a URL /liga no navegador e veja o serial responder.");
}

void loop() {
  servidor.handleClient();

  unsigned long agora = millis();
  if (agora - ultimaLeitura < INTERVALO_ENTRE_LEITURAS) {
    return;
  }
  ultimaLeitura = agora;

  // readTemperature e readHumidity: se a segunda leitura falha, a primeira
  // tambem. Um unico try para as duas evita metades a velhas na tela.
  float t = sensorDht.readTemperature();
  float h = sensorDht.readHumidity();

  if (isnan(t) || isnan(h)) {
    leituraValida = false;
    Serial.println("Falha na leitura do DHT11. Confira o pino 4 e o pull-up.");
    return;
  }

  temperaturaC = t;
  umidade = h;
  leituraValida = true;

  Serial.print("Temp: ");
  Serial.print(t, 1);
  Serial.print(" C  |  Umid: ");
  Serial.print(h, 1);
  Serial.println(" %");
}

Por que assim e não de outro jeito. As rotas chamam servirPagina() no fim, em vez de responderem com um texto curto. O aluno que responder "ligado" e esperar que a página atualize vai ficar parado olhando um "ligado" cinza na tela e concluir que o LED não acendeu. Como a função reenvia a página inteira, o botao volta para a página com o estado novo — e o efeito do comando fica visivel sem nenhum JavaScript. Essa escolha e a razão de o HTML ser uma função separada e não um texto repetido em cada rota.

O return do loop vem depois do handleClient e nunca antes. Se o aluno colocar o return como primeira linha, a placa nunca atende o navegador: as leituras nunca acontecem e o navegador da tempo esgotado. A ordem e o que a atividade do item 7 prova na pratica.

temperaturaC comeca em -99.0 e não em 0. Com zero, o primeiro /dados devolveria 0.0 e o aluno acharia que o sensor esta congelado. Com -99.0 mais a variável leituraValida, a rota responde 503 com uma mensagem clara até a primeira leitura chegar.

O que o professor fala na montagem. Com o DHT11 na protoboard e o resistor de 10 k ligado, o professor pede para três alunos da turma comprarem o ar e segurarem perto do sensor. A leitura muda na hora. Isso resolve em um minuto o item "o sensor não funciona", que não tem nada a ver com software.

Criterios de correcao

CritérioPontos
DHT11 com VCC no 3V3, GND no GND, DATA no GPIO4 e resistor de 10 k entre DATA e VCC2 pontos
pinMode(PIN_LED, OUTPUT) no setup antes de qualquer digitalWrite2 pontos
Quatro rotas declaradas antes do server.begin, cada uma com sua função2 pontos
handleClient no loop e leitura do sensor protegida pelo intervalo de 2 segundos3 pontos
isnan verificado e página mostrando estado "aguardando" em vez de zero1 ponto
Item 6: resposta JSON anotada e as casas decimais explicadas1 ponto
Item 7: efeito da remocao do return descrito e linha restaurada2 pontos

Erros comuns

ErroComo apareceCorrecao
Esquecer o resistor de 10 k do DHT11Serial repete "Falha na leitura do DHT11" e a página fica em "aguardando""O pino DATA precisa do pull-up. Resistor de 10 k entre DATA e VCC."
Ler o sensor em toda volta do loopnan na tela depois dos primeiros segundos"O DHT11 aceita uma leitura a cada 2 segundos. Volte o return que protege o intervalo."
return como primeira linha do loopPágina não abre e o navegador da tempo esgotado"handleClient vem antes do return. A placa precisa atender a cada volta."
digitalWrite escrito direto dentro da rotaFunciona, mas o estado do LED aparece errado depois de um F5"Mude a variável ledAceso e chame aplicarEstadoDoLed(). Quem mexe no pino e so ela."
String(temperaturaC) sem o 1O JSON mostra uma dezena de digitos em vez de um"String(valor, 1) corta em uma casa decimal. O JSON vai para outro programa, e número feio atrapalha."
Link escrito com # em vez de "/liga"Clicar no botao so rola a página para cima"O href e um endereco de verdade. Escreva href=\"/liga\"."
Esquecer o acento no charsetPágina abre com Estaao e umidade` quebrada"text/html; charset=utf-8 no send."
Colocar server.on depois do server.beginA rota nova da 404 e a página nunca aparecem"Todas as rotas vao antes do begin(). Depois do begin, a placa já esta escutando."

Desafio extra

Até agora o F5 atualiza a página. Troque o F5 por atualização automatica sem escrever JavaScript: acrescente <meta http-equiv="refresh" content="5"> no head do HTML. Meça o preco: a placa agora responde cinco vezes mais, e cada resposta gasta memória e tempo. Pergunte ao aluno quanto o contador de requisicoes cresce em um minuto antes e depois, e use a resposta para mostrar que "atualizar sozinho" também tem custo.

>

A resolucao, compilada

// Dia 12, aula 2: servidor web com dado do sensor e comando da placa.
//
// Esta e a aula em que a placa deixa de ser uma coisa que mostra e passa a ser
// uma coisa que responde. Um endpoint mostra a leitura, o outro muda o estado
// de uma saida. E assim que nasce a ideia de IoT do curso: sensor + rede.

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <DHT.h>

// --- Rede: TROQUE PELOS DADOS DA REDE DA SUA ESCOLA ------------------------
// Os dois valores abaixo sao FICTICIOS: nao existe uma rede "rede-da-escola".
// Sao eles que o aluno troca para a aula funcionar na escola. Se a conexao
// falhar, a placa cria a propria rede e a aula continua.
const char* SSID = "rede-da-escola";
const char* SENHA_WIFI = "senha-ficticia-123";

const char* NOME_DO_AP = "lab-esp32-02";

// --- Pinos -----------------------------------------------------------------
const int PIN_DHT = 4;      // data do DHT11
const int PIN_LED = 2;      // LED de estado, com resistor de 330 em serie

const int PORTA_HTTP = 80;
const int TENTATIVAS_MAX = 20;
const int INTERVALO_ENTRE_TENTATIVAS = 500;

// A DHT11 aceita no maximo uma leitura a cada 2 segundos. Ler mais rapido que
// isso devolve NaN — e o "erro" que o aluno ve quando esquece deste intervalo.
const long INTERVALO_ENTRE_LEITURAS = 2000;

WebServer servidor(PORTA_HTTP);
DHT sensorDht(PIN_DHT, DHT11);

bool conectouNaRede = false;
bool ledAceso = false;
unsigned long ultimaLeitura = 0;

// Ultima leitura valida, comecando em valores impossiveis. Assim o primeiro
// HTTP nao devolve zero: o aluno ve que o sensor ja foi lido antes.
float temperaturaC = -99.0;
float umidade = -99.0;
bool leituraValida = false;

// Internet nao e garantida na escola. A placa sobe como access point quando
// falha, e o endereco muda: 192.168.4.1 sem depender de roteador.
void ligarAccessPoint() {
  WiFi.mode(WIFI_AP);
  WiFi.softAP(NOME_DO_AP);
  Serial.println("Sem rede da escola: a placa subiu o proprio access point.");
  Serial.println("Conecte em lab-esp32-02 e abra http://192.168.4.1");
}

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(" — conectou na tentativa ");
      Serial.println(tentativa);
      return true;
    }
    Serial.print(".");
    delay(INTERVALO_ENTRE_TENTATIVAS);
  }
  Serial.println(" — nao conectou");
  return false;
}

// LED: funcao propria em vez de digitalWrite espalhado no codigo. O comando
// chega da web, mas quem decide o estado do pino e sempre esta funcao.
void aplicarEstadoDoLed() {
  digitalWrite(PIN_LED, ledAceso ? HIGH : LOW);
  Serial.print("LED agora: ");
  Serial.println(ledAceso ? "ligado" : "desligado");
}

// GET / — a pagina: mostra a leitura e traz o botao que manda o comando.
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 da bancada</title></head><body>";
  corpo += "<h1>Estacao da bancada</h1>";

  if (leituraValida) {
    corpo += "<p>Temperatura: <b>";
    corpo += temperaturaC;
    corpo += " &deg;C</b></p>";
    corpo += "<p>Umidade: <b>";
    corpo += umidade;
    corpo += " %</b></p>";
  } else {
    corpo += "<p>Aguardando a primeira leitura do sensor...</p>";
  }

  corpo += "<p>LED: <b>";
  corpo += ledAceso ? "ligado" : "desligado";
  corpo += "</b></p>";

  // O link vira GET em /liga. Nao ha formulario nem JavaScript: a coisa mais
  // simples que o navegador entende.
  corpo += "<p><a href=\"/liga\">Ligar LED</a> | ";
  corpo += "<a href=\"/desliga\">Desligar LED</a></p>";

  corpo += "<p><small>Atualizado a cada ";
  corpo += INTERVALO_ENTRE_LEITURAS / 1000;
  corpo += " s. Recarregue com F5.</small></p>";
  corpo += "</body></html>";

  servidor.sendHeader("Cache-Control", "no-cache");
  servidor.send(200, "text/html; charset=utf-8", corpo);
}

// GET /liga e GET /desliga — o comando chega pela URL, nao pelo codigo.
void servirLiga() {
  ledAceso = true;
  aplicarEstadoDoLed();
  servirPagina();   // volta para a pagina: o aluno ve o resultado do comando
}

void servirDesliga() {
  ledAceso = false;
  aplicarEstadoDoLed();
  servirPagina();
}

// GET /dados — a mesma informacao, sem HTML. E este endpoint que o 2o
// trimestre vai ler de um servidor Node.js: ele pede /dados e recebe JSON.
void servirDados() {
  if (!leituraValida) {
    servidor.send(503, "application/json; charset=utf-8",
                  "{\"erro\":\"sensor sem leitura\"}");
    return;
  }

  String json = "{";
  json += "\"temperatura\":" + String(temperaturaC, 1) + ",";
  json += "\"umidade\":" + String(umidade, 1) + ",";
  json += "\"led\":" + String(ledAceso ? "true" : "false") + ",";
  json += "\"dispositivo\":\"esp32-bancada\"";
  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 /");
}

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println("Dia 12, aula 2 — sensor e comando pela web");
  Serial.println("-----------------------------------------");

  pinMode(PIN_LED, OUTPUT);
  sensorDht.begin();
  conectouNaRede = esperarConexao();
  if (!conectouNaRede) {
    ligarAccessPoint();
  }

  servidor.on("/", servirPagina);
  servidor.on("/liga", servirLiga);
  servidor.on("/desliga", servirDesliga);
  servidor.on("/dados", servirDados);
  servidor.onNotFound(servirErro);
  servidor.begin();

  Serial.print("Endereco: http://");
  Serial.println(WiFi.localIP());
  Serial.println("Rotas: /  /liga  /desliga  /dados");
  Serial.println("Coloque a URL /liga no navegador e veja o serial responder.");
}

void loop() {
  servidor.handleClient();

  unsigned long agora = millis();
  if (agora - ultimaLeitura < INTERVALO_ENTRE_LEITURAS) {
    return;
  }
  ultimaLeitura = agora;

  // readTemperature e readHumidity: se a segunda leitura falha, a primeira
  // tambem. Um unico try para as duas evita metades a velhas na tela.
  float t = sensorDht.readTemperature();
  float h = sensorDht.readHumidity();

  if (isnan(t) || isnan(h)) {
    leituraValida = false;
    Serial.println("Falha na leitura do DHT11. Confira o pino 4 e o pull-up.");
    return;
  }

  temperaturaC = t;
  umidade = h;
  leituraValida = true;

  Serial.print("Temp: ");
  Serial.print(t, 1);
  Serial.print(" C  |  Umid: ");
  Serial.print(h, 1);
  Serial.println(" %");
}

Sem saída de compilação gravada. Rode python3 validar.py -t 1 dia12 aula2.