WiFi e o primeiro dispositivo conectado — Arduino e IoT — semana 12 do 1o trimestre
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
SSIDeSENHA_WIFIpelos 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
whileque trava para sempre. - Publicar uma página web na placa com
WebServer,server.oneserver.handleClient. - Fazer a placa criar a propria rede com
WiFi.softAPquando 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.
| Modo | Quem cria a rede | Endereco tipico | Serve para |
|---|---|---|---|
WIFI_STA (estacao) | o roteador da escola | 192.168.0.107 | uso normal |
WIFI_AP (access point) | a propria placa | 192.168.4.1 | aula 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.
| Chamada | Devolve |
|---|---|
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
SSIDe aSENHA_WIFIvao 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.
- Troque as duas constantes do topo do código:
SSIDeSENHA_WIFI. Nome da rede e senha da sua escola. - 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. - Abra no navegador o endereco que apareceu no serial. Descreva o que você ve, em uma frase.
- Troque
SSIDpara 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? - Conecte o celular na rede
lab-esp32-01e abrahttp://192.168.4.1. A página abriu? Se não abriu, o que você digitou? - Volte o
SSIDpara 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ério | Pontos |
|---|---|
SSID e SENHA_WIFI trocados pelos dados da rede, declarados em const char* no topo | 2 pontos |
WiFi.begin seguido de espera com contador de tentativas, sem while infinito | 3 pontos |
server.on e server.begin no setup, server.handleClient no loop | 2 pontos |
Header text/html; charset=utf-8 no send da página | 2 pontos |
| Endereco IP anotado e item 3 respondido em uma frase | 1 ponto |
| Item 4: tempo de espera até o access point cronometrado e descrito | 1 ponto |
| Item 6: razão de não gravar a senha da escola no material | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
while (WiFi.status() != WL_CONNECTED) sem contador | A 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.begin | Serial 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 loop | A 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-8 | Pá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 escola | O 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 arquivo | Colega 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 loop | A 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.one explicar o que cada rota responde. - Montar uma página que mostra a leitura atual do
DHT11e recarrega sozinha. - Receber um comando do navegador e mudar o estado de um pino a partir dele.
- Publicar a mesma informação em
JSONnuma 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
DHT11e 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);
| Rota | Tipo | Quem usa |
|---|---|---|
/ | text/html | pessoa, no navegador |
/dados | application/json | programa, 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:VCCno3V3,GNDnoGND,DATAnoGPIO4.- Resistor de 10 k entre
DATAeVCCdo sensor. Sem esse resistor a leitura voltanan. - LED de 3,3 V com resistor de 330 em serie, do
GPIO2para o+da protoboard. - Altere as constantes
SSIDeSENHA_WIFIpara 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.
- Monte o circuito do enunciado. Sem o resistor de 10 k, pare e confira antes de gravar.
- Grave o sketch e abra
http://<endereco do serial>. A temperatura e a umidade aparecem? Anote os dois valores no caderno. - Clique em Ligar LED. O que o monitor serial imprime? O LED acendeu ao mesmo tempo? Marque os dois horarios.
- Clique em Desligar LED. O serial imprime de novo? Se você olhar so o navegador, como saberia que o comando chegou?
- Abra
http://<endereco do serial>/ligaescrevendo a URL na mao, sem clicar no link. Funciona igual? O que isso prova sobre o link da página? - Abra
/dados. Escreva no caderno a resposta exata que apareceu. Quantas casas decimais tem a temperatura? Se você remover o, 1doString, quantas casas aparecem? - Repare no
returndoloop. Remova a linha doreturne 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 += " °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ério | Pontos |
|---|---|
DHT11 com VCC no 3V3, GND no GND, DATA no GPIO4 e resistor de 10 k entre DATA e VCC | 2 pontos |
pinMode(PIN_LED, OUTPUT) no setup antes de qualquer digitalWrite | 2 pontos |
Quatro rotas declaradas antes do server.begin, cada uma com sua função | 2 pontos |
handleClient no loop e leitura do sensor protegida pelo intervalo de 2 segundos | 3 pontos |
isnan verificado e página mostrando estado "aguardando" em vez de zero | 1 ponto |
| Item 6: resposta JSON anotada e as casas decimais explicadas | 1 ponto |
Item 7: efeito da remocao do return descrito e linha restaurada | 2 pontos |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
Esquecer o resistor de 10 k do DHT11 | Serial 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 loop | nan 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 loop | Pá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 rota | Funciona, 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 1 | O 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 charset | Página abre com Estaao e umidade` quebrada | "text/html; charset=utf-8 no send." |
Colocar server.on depois do server.begin | A 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 += " °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.
