A placa fala: JSON e WiFi do ESP32 — Arduino e IoT — semana 3 do 2o trimestre
Semana 3 de 16· 2o trimestre · 01/05 a 04/09
A placa fala: JSON e WiFi do ESP32
A placa monta o JSON, envia e trata o que volta.
Aula 1 — Montar e enviar JSON a partir do ESP32
Objetivos
- Montar um JSON valido dentro do ESP32 usando
Stringe concatenacao, e explicar por que a aspa do JSON e a aspa do C++ ao mesmo tempo. - Conectar a placa no WiFi e dizer por que esperar um tempo fixo não substitui checar o status.
- Enviar o JSON com
HTTPClient, combegin,addHeaderePOST, e ler a resposta comgetString. - Escolher o formato do número no JSON e justificar por que
26,5quebraria oJSON.parsedo Node. - Apontar, no monitor serial, qual das três etapas da placa falhou: rede, envio ou resposta.
Material
- 1 ESP32 DevKit V1 por aluno, com o cabo USB
- 1 protoboard de 830 pontos e 1 jogo de jumpers por aluno
- 1 módulo de alimentacao MB102 por dupla
- 1 roteador com rede de teste do professor, nome e senha anotados na lousa
- 1 computador com Node.js 20 ou superior, rodando o servidor do dia 2
- Projetor, para o terminal do Node e para o monitor serial ao mesmo tempo
Conceitos
JSON montado na mao, e por que
O ESP32 não tem biblioteca de JSON no core. Existe a ArduinoJson, que resolve isso bem, mas ela não vem instalada, e o trimestre mostra primeiro o jeito na mao — porque e o que a placa faz quando o servidor cai e a aula do dia 3 aula 2 mostra que ele continua funcionando assim.
O JSON e um texto. As regras de texto em C++ são:
// Aula 1 do dia 3: montar e enviar JSON a partir do ESP32. // // O ponto da aula e que JSON nao tem biblioteca no core do ESP32. O // ArduinoJson existe, mas nao vem instalado, e o trimestre mostra primeiro // o jeito na mao, porque e o que a placa faz quando o servidor cai. #include <Arduino.h> #include <WiFi.h> #include <HTTPClient.h> // GPIO2 e o LED embutido na maioria dos DevKit: indicador de vida. const int PIN_LED = 2; // Credenciais FICTICIAS. Nenhum segredo real entra em material de aula. const char* WIFI_SSID = "rede-teste-lab01"; const char* CHAVE_REDE = "ficticia-lab01"; // IP da maquina que roda o Node, nao o do roteador. const char* IP_SERVIDOR = "192.168.0.10"; const int PORTA_SERVIDOR = 3000; const char* CAMINHO = "/leitura"; const long INTERVALO_MS = 5000; // Deslocamento do fuso, em horas. A placa nao tem relogio de tempo real, e o // horario dela so fica certo se o professor acertar este numero. O dia 14 // mostra o arranjo serio: um modulo RTC. const int FUSO_HORAS = -3; WiFiClient cliente; HTTPClient http; unsigned long ultimo_envio = 0; int leituras_enviadas = 0; void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(2000); Serial.println(); Serial.println("2o trimestre, dia 3, aula 1 - JSON saindo da placa"); Serial.println("-------------------------------------------------"); Serial.print("conectando em "); Serial.println(WIFI_SSID); WiFi.begin(WIFI_SSID, CHAVE_REDE); int tentativas = 0; while (WiFi.status() != WL_CONNECTED && tentativas < 40) { delay(500); Serial.print("."); tentativas++; } Serial.println(); if (WiFi.status() != WL_CONNECTED) { Serial.println("FALHA na rede. O codigo continua, e o que a aula 2 vai tratar."); return; } Serial.print("conectado. IP da placa: "); Serial.println(WiFi.localIP()); } void loop() { // Temporizador com millis(), e nao delay(): a aula 1 do dia 7 vai mostrar // que delay travaria o programa e nao deixaria a placa checar o WiFi. unsigned long agora = millis(); if (agora - ultimo_envio < INTERVALO_MS) { return; } ultimo_envio = agora; if (WiFi.status() != WL_CONNECTED) { Serial.println("sem rede: envio pulado"); return; } // Montagem do JSON NA MAO. Repare em tres coisas: // 1. a aspa do JSON precisa de barra: \" abre e fecha uma chave; // 2. o valor numerico vem de String(float, casas) — sem isso viraria // 26.5000004768 e o professor explica por que; // 3. o separador virgula entre um campo e o outro nao pode sobrar nem // faltar, senao o JSON.parse do Node lanca excecao. float temp_c = 26.5 + (leituras_enviadas % 3) * 0.7; int umidade_pct = 58 + (leituras_enviadas % 4); // Buffer de 24 bytes: a data com dois digitos em cada campo ocupa 19 // caracteres, e sobrar espaco evita o aviso de truncamento do compilador. // Buffer curto demais e o erro que passa e da dado corrompido em producao. char agora_txt[24]; snprintf(agora_txt, sizeof(agora_txt), "%02d:%02d:%02d", (leituras_enviadas % 24), leituras_enviadas % 60, 0); String json = String("{\"id_placa\":\"esp32-lab01\"") + ",\"temp_c\":" + String(temp_c, 1) + ",\"umidade_pct\":" + String(umidade_pct) + ",\"fuso\":" + String(FUSO_HORAS) + ",\"dt_leitura\":\"" + String(agora_txt) + "\"}"; digitalWrite(PIN_LED, HIGH); String url = "http://" + String(IP_SERVIDOR) + ":" + String(PORTA_SERVIDOR) + CAMINHO; http.begin(url); http.addHeader("Content-Type", "application/json"); int codigo = http.POST(json); Serial.println("-------------------------------------------------"); Serial.print("enviado: "); Serial.println(json); if (codigo == 201) { leituras_enviadas++; Serial.print("OK 201 — gravada. total enviado: "); Serial.println(leituras_enviadas); } else { Serial.print("resposta inesperada, codigo "); Serial.println(codigo); } http.end(); digitalWrite(PIN_LED, LOW); }
A barra invertida antes das aspas e o unico caracter da linha que o aluno erra. O professor passa pela tela e pergunta "o que essa barra faz?" até todo mundo responder antes de seguir.
Concatenar com valor dentro
O segundo cuidado e o ponto decimal. A String "26.5" e texto; a String "26,5" e texto que o Node não entende como número, e o JSON.parse falha com erro de sintaxe.
String json = String("{"temp_c":") + String(26.5, 1) + "}"; // o nome da chave o valor a chave fecha
O String(26.5, 1) e o que controla as casas decimais. Sem o segundo argumento, o ESP32 imprime 26.5000004768, que e um número legitimo de ponto flutuante e completamente inutil para o banco. O dia 3 do 1o trimestre já mostrou esse tipo de erro com %s no lugar de %d; aqui ele volta em outra forma.
WiFi.begin e a espera correta
WiFi.begin(ssid, senha); while (WiFi.status() != WL_CONNECTED) { delay(500); }
O while com delay e a forma errada de esperar: ela trava a placa por tempo indeterminado, e se o roteador estiver desligado o programa nunca chega na proxima linha. O if dentro e a forma certa: a placa tenta por um tempo limitado, verifica, e se não conectou ela segue rodando em modo degradado.
O professor mostra WiFi.status() com o código numerico e o nome ao lado: WL_CONNECTED, WL_NO_SSID_AVAIL (a rede não existe), WL_CONNECT_FAILED (senha errada). O aluno que ver o número 4 sabe que a senha esta errada, e o que ver 1 sabe que o nome da rede esta errado. E o diagnostico que funciona sem depurador.
As três etapas que podem falhar
O sketch tem que permitir distinguir:
| Etapa | Como o código falha | O que o serial mostra |
|---|---|---|
| Rede | WiFi.status() != WL_CONNECTED | "sem rede: envio pulado" |
| Envio | http.POST devolve negativo | "falha de rede, código -1" |
| Resposta | codigo > 0 mas diferente de 201 | "resposta inesperada, código 404" |
O return logo após checar a rede e o que mantem as três etapas separadas. Sem ele, a placa tenta POST sem rede e recebe um código negativo, e o aluno conclui que "o servidor esta fora" quando o problema era o WiFi.
Atividade
Montagem:
- Nenhuma peca nova. A placa usa o circuito do dia 1 (potenciometro no
GPIO34). - Confirme que o computador do professor e o da dupla estão na mesma rede do roteador, e que o firewall do Node aceita a porta 3000.
- Escreva no caderno, a mao, o JSON que a placa vai enviar, com as aspas, chaves e virgulas. Confira caractere por caractere contra o sketch.
- Grave o sketch da resolucao. Abra o monitor serial e espere a placa conectar. Anote o IP que ela recebeu.
- Troque
TEMPERATURApara26.5e grave. Troque para26,5(virgula) e grave. O que muda no monitor serial? E o que o Node recebe em cada caso? - Adicione um campo novo ao JSON,
umidade_pct, com o valor58. Grave e confira se o Node imprimiu o campo novo no log. - Com o Node parado, mantenha a placa gravada e veja o que o serial mostra. O código impresso e positivo ou negativo? O que isso diz sobre quem falhou?
- Ligue o Node de novo, com a placa já rodando, e veja em quanto tempo o serial volta a mostrar
201. Esse tempo e o tempo que a placa leva para descobrir que o caminho voltou. Escreva esse tempo.
Nota: 12 pontos. Critério de fim: a placa envia, o Node responde 201, e os itens 3, 5 e 6 tem resposta escrita no caderno.
Resolucao
O exemplo do servidor Node que roda na outra ponta e o mesmo do dia 2 aula 2, com o log de cada etapa:
// Log de cada etapa do caminho, do lado do servidor. O professor aponta
// para estas linhas enquanto a placa envia, e o aluno acompanha a ordem:
// chegou, foi interpretado, foi gravado.
const http = require('http');
const servidor = http.createServer((req, res) => {
if (req.method === 'POST' && req.url === '/leitura') {
let corpo = '';
req.on('data', (pedaco) => { corpo += pedaco.toString(); });
req.on('end', () => {
console.log('[servidor] corpo de', corpo.length, 'bytes');
console.log('[servidor] JSON:', JSON.parse(corpo));
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ id: 1, gravado: true }));
});
return;
}
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ erro: 'rota nao existe' }));
});
(async () => {
await new Promise((r) => servidor.listen(0, '127.0.0.1', r));
console.log('[servidor] no ar em http://127.0.0.1:' + servidor.address().port + '/leitura');
// O servidor fica no ar esperando a placa: e o que o diferencia do
// programa do dia 1, que imprimia e morria.
process.on('SIGINT', () => { servidor.close(); console.log('[servidor] encerrado'); });
})();
O process.on('SIGINT') existe para o aluno poder fechar o servidor com Ctrl+C sem deixar o processo pendurado. Sem ele, o Ctrl+C funciona, mas o log não mostra a saída limpa, e o aluno não sabe se o processo morreu.
O sketch da placa, que monta o JSON e envia:
// Aula 1 do dia 3: montar e enviar JSON a partir do ESP32. // // O ponto da aula e que JSON nao tem biblioteca no core do ESP32. O // ArduinoJson existe, mas nao vem instalado, e o trimestre mostra primeiro // o jeito na mao, porque e o que a placa faz quando o servidor cai. #include <Arduino.h> #include <WiFi.h> #include <HTTPClient.h> // GPIO2 e o LED embutido na maioria dos DevKit: indicador de vida. const int PIN_LED = 2; // Credenciais FICTICIAS. Nenhum segredo real entra em material de aula. const char* WIFI_SSID = "rede-teste-lab01"; const char* CHAVE_REDE = "ficticia-lab01"; // IP da maquina que roda o Node, nao o do roteador. const char* IP_SERVIDOR = "192.168.0.10"; const int PORTA_SERVIDOR = 3000; const char* CAMINHO = "/leitura"; const long INTERVALO_MS = 5000; // Deslocamento do fuso, em horas. A placa nao tem relogio de tempo real, e o // horario dela so fica certo se o professor acertar este numero. O dia 14 // mostra o arranjo serio: um modulo RTC. const int FUSO_HORAS = -3; WiFiClient cliente; HTTPClient http; unsigned long ultimo_envio = 0; int leituras_enviadas = 0; void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(2000); Serial.println(); Serial.println("2o trimestre, dia 3, aula 1 - JSON saindo da placa"); Serial.println("-------------------------------------------------"); Serial.print("conectando em "); Serial.println(WIFI_SSID); WiFi.begin(WIFI_SSID, CHAVE_REDE); int tentativas = 0; while (WiFi.status() != WL_CONNECTED && tentativas < 40) { delay(500); Serial.print("."); tentativas++; } Serial.println(); if (WiFi.status() != WL_CONNECTED) { Serial.println("FALHA na rede. O codigo continua, e o que a aula 2 vai tratar."); return; } Serial.print("conectado. IP da placa: "); Serial.println(WiFi.localIP()); } void loop() { // Temporizador com millis(), e nao delay(): a aula 1 do dia 7 vai mostrar // que delay travaria o programa e nao deixaria a placa checar o WiFi. unsigned long agora = millis(); if (agora - ultimo_envio < INTERVALO_MS) { return; } ultimo_envio = agora; if (WiFi.status() != WL_CONNECTED) { Serial.println("sem rede: envio pulado"); return; } // Montagem do JSON NA MAO. Repare em tres coisas: // 1. a aspa do JSON precisa de barra: \\" abre e fecha uma chave; // 2. o valor numerico vem de String(float, casas) — sem isso viraria // 26.5000004768 e o professor explica por que; // 3. o separador virgula entre um campo e o outro nao pode sobrar nem // faltar, senao o JSON.parse do Node lanca excecao. float temp_c = 26.5 + (leituras_enviadas % 3) * 0.7; int umidade_pct = 58 + (leituras_enviadas % 4); // Buffer de 24 bytes: a data com dois digitos em cada campo ocupa 19 // caracteres, e sobrar espaco evita o aviso de truncamento do compilador. // Buffer curto demais e o erro que passa e da dado corrompido em producao. char agora_txt[24]; snprintf(agora_txt, sizeof(agora_txt), "%02d:%02d:%02d", (leituras_enviadas % 24), leituras_enviadas % 60, 0); String json = String("{\"id_placa\":\"esp32-lab01\"") + ",\"temp_c\":" + String(temp_c, 1) + ",\"umidade_pct\":" + String(umidade_pct) + ",\"fuso\":" + String(FUSO_HORAS) + ",\"dt_leitura\":\"" + String(agora_txt) + "\"}"; digitalWrite(PIN_LED, HIGH); String url = "http://" + String(IP_SERVIDOR) + ":" + String(PORTA_SERVIDOR) + CAMINHO; http.begin(url); http.addHeader("Content-Type", "application/json"); int codigo = http.POST(json); Serial.println("-------------------------------------------------"); Serial.print("enviado: "); Serial.println(json); if (codigo == 201) { leituras_enviadas++; Serial.print("OK 201 — gravada. total enviado: "); Serial.println(leituras_enviadas); } else { Serial.print("resposta inesperada, codigo "); Serial.println(codigo); } http.end(); digitalWrite(PIN_LED, LOW); }
Por que assim e não de outro jeito. O if (WiFi.status() != WL_CONNECTED) esta duas vezes: uma no setup, para avisar que a placa não conectou, e outra no loop, porque a rede pode cair depois. O aluno que so verifica no setup tem um programa que funciona a aula inteira e falha na segunda.
A leitura da dt_leitura usa snprintf com %02d, e não String com +: com + seria preciso converter cada número com String() e code pior. O snprintf com buffer dimensionado e a forma que o professor usa desde o dia 7 do 1o trimestre, e ele já viu o aviso de truncamento quando o buffer era pequeno demais.
Criterios de correcao
| Critério | Pontos |
|---|---|
| JSON transcrito a mao no caderno, conferido caractere por caractere | 2 pontos |
| Placa conecta no WiFi e o IP anotado | 2 pontos |
| Item 3: a diferença entre ponto e virgula esta explicada | 2 pontos |
Campo umidade_pct adicionado e visivel no log do Node | 2 pontos |
| Item 5: o código negativo foi identificado como falha de envio, e não de resposta | 2 pontos |
Item 6: o tempo de volta do 201 foi medido e anotado | 2 pontos |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
Escrever String json = "{"temp_c":26.5}"; | Erro de compilacao com expected ' antes de temp_c | "Toda aspa que existe dentro do texto do JSON precisa de \\ antes. Sem isso o C++ acha que o texto acabou ali e tenta ler temp_c como código." |
| Usar virgula decimal no valor | O Node lanca SyntaxError no JSON.parse e o serial mostra o JSON com ponto | "JSON e padrao english: separador de decimal e ponto. Virgula e separador de campo. A placa já imprime ponto, e o Node espera ponto." |
WiFi.begin seguido de delay(5000) fixo e seguir | A placa manda o primeiro POST e falha, depois funciona | "Cinco segundos e palpite. Cheque WiFi.status() em laco, com um limite de tentativas, e so depois de WL_CONNECTED mande o primeiro dado." |
while (WiFi.status() != WL_CONNECTED) sem limite | A placa trava e não imprime nada, nem "FALHA" | "Esse laco so acaba quando a placa conecta. Com o roteador desligado, ele nunca acaba. Ponha um contador de tentativas e saia mesmo assim." |
http.begin sem addHeader | Node responde 400 ou o JSON.parse falha | "Content-Type: application/json diz ao servidor que o corpo e JSON. Sem ele, o Node adivinha — e adivinha errado." |
Esquecer http.end() | A terceira ou quarta leitura falha com código negativo | "end fecha a conexão. Sem ele, as conexões vao se acumulando e a placa esgota as abas de rede. O dia 3 aula 2 mostra o sintoma." |
| Escrever o IP da placa no código do Node | "O Node precisa saber o IP da placa" | "E o contrario. O Node fica parado ouvindo numa porta, e a placa procura o Node. O Node nunca descobre o IP de ninguem." |
Ficar chamando WiFi.reconnect() a cada 5 segundos | A placa nunca sai do estado de conectando | "Reconnect a cada 5 s e reconnect em loop, e as coisas se brigam. Uma placa que perdeu a rede espera o intervalo dela e tenta de novo, como a aula 2 mostra." |
Desafio extra
Monte o JSON com cinco campos em vez de três, incluindo um campo de texto com espaco, como "id_placa": "esp32 lab 01". Verifique se o Node interpretou o espaco corretamente. Depois escreva em uma frase o que aconteceria se o texto tivesse uma aspa dentro, e o que o professor teria de fazer para consertar sem mudar o JSON. A resposta que o professor espera: a aspa interna fecha a chave cedo e o JSON fica invalido; a correcao e escapar a aspa com barra no lado da placa, e não no lado do Node.
A resolucao, compilada
// Aula 1 do dia 3: montar e enviar JSON a partir do ESP32. // // O ponto da aula e que JSON nao tem biblioteca no core do ESP32. O // ArduinoJson existe, mas nao vem instalado, e o trimestre mostra primeiro // o jeito na mao, porque e o que a placa faz quando o servidor cai. #include <Arduino.h> #include <WiFi.h> #include <HTTPClient.h> // GPIO2 e o LED embutido na maioria dos DevKit: indicador de vida. const int PIN_LED = 2; // Credenciais FICTICIAS. Nenhum segredo real entra em material de aula. const char* WIFI_SSID = "rede-teste-lab01"; const char* CHAVE_REDE = "ficticia-lab01"; // IP da maquina que roda o Node, nao o do roteador. const char* IP_SERVIDOR = "192.168.0.10"; const int PORTA_SERVIDOR = 3000; const char* CAMINHO = "/leitura"; const long INTERVALO_MS = 5000; // Deslocamento do fuso, em horas. A placa nao tem relogio de tempo real, e o // horario dela so fica certo se o professor acertar este numero. O dia 14 // mostra o arranjo serio: um modulo RTC. const int FUSO_HORAS = -3; WiFiClient cliente; HTTPClient http; unsigned long ultimo_envio = 0; int leituras_enviadas = 0; void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(2000); Serial.println(); Serial.println("2o trimestre, dia 3, aula 1 - JSON saindo da placa"); Serial.println("-------------------------------------------------"); Serial.print("conectando em "); Serial.println(WIFI_SSID); WiFi.begin(WIFI_SSID, CHAVE_REDE); int tentativas = 0; while (WiFi.status() != WL_CONNECTED && tentativas < 40) { delay(500); Serial.print("."); tentativas++; } Serial.println(); if (WiFi.status() != WL_CONNECTED) { Serial.println("FALHA na rede. O codigo continua, e o que a aula 2 vai tratar."); return; } Serial.print("conectado. IP da placa: "); Serial.println(WiFi.localIP()); } void loop() { // Temporizador com millis(), e nao delay(): a aula 1 do dia 7 vai mostrar // que delay travaria o programa e nao deixaria a placa checar o WiFi. unsigned long agora = millis(); if (agora - ultimo_envio < INTERVALO_MS) { return; } ultimo_envio = agora; if (WiFi.status() != WL_CONNECTED) { Serial.println("sem rede: envio pulado"); return; } // Montagem do JSON NA MAO. Repare em tres coisas: // 1. a aspa do JSON precisa de barra: \\" abre e fecha uma chave; // 2. o valor numerico vem de String(float, casas) — sem isso viraria // 26.5000004768 e o professor explica por que; // 3. o separador virgula entre um campo e o outro nao pode sobrar nem // faltar, senao o JSON.parse do Node lanca excecao. float temp_c = 26.5 + (leituras_enviadas % 3) * 0.7; int umidade_pct = 58 + (leituras_enviadas % 4); // Buffer de 24 bytes: a data com dois digitos em cada campo ocupa 19 // caracteres, e sobrar espaco evita o aviso de truncamento do compilador. // Buffer curto demais e o erro que passa e da dado corrompido em producao. char agora_txt[24]; snprintf(agora_txt, sizeof(agora_txt), "%02d:%02d:%02d", (leituras_enviadas % 24), leituras_enviadas % 60, 0); String json = String("{\"id_placa\":\"esp32-lab01\"") + ",\"temp_c\":" + String(temp_c, 1) + ",\"umidade_pct\":" + String(umidade_pct) + ",\"fuso\":" + String(FUSO_HORAS) + ",\"dt_leitura\":\"" + String(agora_txt) + "\"}"; digitalWrite(PIN_LED, HIGH); String url = "http://" + String(IP_SERVIDOR) + ":" + String(PORTA_SERVIDOR) + CAMINHO; http.begin(url); http.addHeader("Content-Type", "application/json"); int codigo = http.POST(json); Serial.println("-------------------------------------------------"); Serial.print("enviado: "); Serial.println(json); if (codigo == 201) { leituras_enviadas++; Serial.print("OK 201 — gravada. total enviado: "); Serial.println(leituras_enviadas); } else { Serial.print("resposta inesperada, codigo "); Serial.println(codigo); } http.end(); digitalWrite(PIN_LED, LOW); }
Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia03 aula1.
Aula 2 — Tratar erro de rede na placa: sem sinal, sem servidor, com timeout
Objetivos
- Fazer a placa sobreviver a três situacoes de erro: sem rede, sem servidor e sem resposta no tempo.
- Ler
WiFi.status()e nomear o que significamWL_CONNECTED,WL_NO_SSID_AVAILeWL_CONNECT_FAILED. - Separar, no código, falha de envio (código negativo) de recusa do servidor (código positivo diferente de 201).
- Explicar para que serve
http.setTimeoute o que acontece sem ele. - Escrever um contador de falhas seguidas e explicar por que ele existe, em vez de reenviar para sempre.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1 protoboard de 830 pontos por dupla
- 1 roteador com rede de teste do professor
- 1 computador com Node.js 20 ou superior
- Folha de papel por aluno, para a tabela de três situacoes
- Projetor, para o terminal do Node e o monitor serial juntos
Conceitos
As três situacoes, e o código de cada uma
| Situacao | Sintoma no serial | O que aconteceu |
|---|---|---|
| Sem rede | [1] SEM REDE e nada e enviado | a placa nem chegou a tentar |
| Sem servidor | [2] SEM RESPOSTA, codigo -1 | o pedido saiu e ninguem respondeu |
| Servidor recusa | [3] RESPOSTA RECUSADA, codigo 404 | alguem respondeu e disse não |
A distincao do meio e a que o professor mais insiste: o código negativo vem da pilha de rede do ESP32, não do servidor. Ninguem no servidor disse nada. O HTTPClient tentou abrir um socket, o socket falhou, e o número negativo e o código do erro de socket. O aluno que le "código -1" como "o servidor respondeu não" vai passar meia hora olhando um Node que esta perfeito.
http.setTimeout: o tempo maximo de espera
Sem setTimeout, o http.POST pode ficar esperando indefinidamente. Com o roteador desligado mas a rede ainda "parecendo" conectada, a placa trava por minutos em uma chamada — e durante esse tempo o loop para de rodar, o que significa que a placa parou de responder a tudo, inclusive ao botao e ao LED.
http.setTimeout(TIMEOUT_MS); // antes do begin
O professor faz a conta na lousa: com TIMEOUT_MS = 3000 e intervalo de 5000 ms, a placa pode perder no maximo 3 segundos de 5. Com o padrao, que e muito maior, ela pode perder a aula inteira.
Contador de falhas: por que existe
Reenviar para sempre e o erro de projeto mais caro deste trimestre: consome bateria, derruba a rede e esconde o problema real. O padrao correto e:
- contar as falhas seguidas;
- quando a falha vem, incrementar;
- quando o envio volta a dar certo, zerar o contador;
- a partir de um limite, não insistir no mesmo ritmo.
O if (codigo == 201) { falhas_seguidas = 0; } e a linha que o aluno sempre esquece. Sem ela, um contador que já chegou a três nunca mais volta a zero, e a placa fica em "modo degradado" para sempre mesmo depois do servidor voltar. Esse e o bug de verdade que o professor quero que o aluno encontre sozinho: ele liga o Node, ve que voltou a funcionar, e o contador continua em três.
Atividade
Montagem: nenhuma peca nova. A placa usa o circuito do dia 1 e o WiFi do dia 3 aula 1.
- Preencha a tabela de três situacoes: nome da situacao, o que o serial mostra, e o que o professor precisa fazer para provocar. Deixe a ultima coluna em branco: você preenche depois de provar.
- Grave o sketch da resolucao. Com o Node parado, espere 20 segundos e anote o que aparece no serial. O código e positivo ou negativo?
- Desligue o roteador por 15 segundos e religue. O que o
WiFi.status()mostra? O que o código numerico diz sobre a causa? - Troque
WIFI_SSIDpara um nome que não existe. Grave. Compare o número doWiFi.status()com o do item 3. Os dois números são diferentes? O que isso permite concluir sobre o diagnostico? - Mude
TIMEOUT_MSde 3000 para 10000, com o Node parado. Meça o tempo que a placa leva para mostrar a falha. Multiplique por 2 e explique em uma frase por que esse tempo e o pior número do código. - Com o Node funcionando, observe o contador de falhas: ele volta a zero depois de um
201? Se não, escreva qual linha do código esta faltando e o que acontece na pratica.
Nota: 12 pontos. Critério de fim: as três situacoes foram provadas na sua placa, com o serial como evidência, e o item 6 tem resposta com a linha do código identificada.
Resolucao
O sketch da placa, com as três situacoes separadas e nomeadas:
// Aula 2 do dia 3: o que a placa faz quando o caminho nao existe. // // Tres situacoes, e a aula nao e sobre a placa funcionar: e sobre a placa // continuar ALIVE. Um ESP32 que trava esperando a rede morre no seu // programa de forma silenciosa, e o professor descobre no fim do semestre. #include <Arduino.h> #include <WiFi.h> #include <HTTPClient.h> const int PIN_LED = 2; // Credenciais FICTICIAS. const char* WIFI_SSID = "rede-fantasma"; const char* CHAVE_REDE = "ficticia-lab01"; // Servidor de proposito IREACHIVEL, para o aluno ver o codigo negativo. // Na aula real o professor troca pelo IP que responde. const char* IP_SERVIDOR = "192.168.0.99"; const int PORTA_SERVIDOR = 3000; const char* CAMINHO = "/leitura"; // Tempo MAXIMO de espera pelo servidor. Passou disso, desiste. Este numero // e o timeout: sem ele, a placa trava esperando resposta que nunca vem. const unsigned long TIMEOUT_MS = 3000; // Quanto tempo a placa espera antes de TENTAR DE NOVO, entre uma falha e // outra. Tentar 60 vezes por segundo derruba a bateria e a rede. const long ESPERA_ENTRE_TENTATIVAS_MS = 5000; const int MAX_TENTATIVAS_CONSECUTIVAS = 3; WiFiClient cliente; HTTPClient http; unsigned long ultimo_envio = 0; int falhas_seguidas = 0; int total_ok = 0; int total_falhas = 0; void mostrar_rede() { Serial.print(" WiFi.status() = "); Serial.print(WiFi.status()); switch (WiFi.status()) { case WL_CONNECTED: Serial.println(" (WL_CONNECTED) conectado"); break; case WL_NO_SSID_AVAIL: Serial.println(" (WL_NO_SSID_AVAIL) rede nao existe"); break; case WL_CONNECT_FAILED: Serial.println(" (WL_CONNECT_FAILED) senha recusada"); break; case WL_IDLE_STATUS: Serial.println(" (WL_IDLE_STATUS) ainda tentando"); break; case WL_DISCONNECTED: Serial.println(" (WL_DISCONNECTED) desconectado"); break; default: Serial.println(" (outro)"); break; } } void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(2000); Serial.println(); Serial.println("2o trimestre, dia 3, aula 2 - quando o caminho quebra"); Serial.println("-------------------------------------------------"); Serial.println("Placa alive: o programa roda mesmo com tudo errado."); Serial.println(); WiFi.begin(WIFI_SSID, CHAVE_REDE); int tentativas = 0; while (WiFi.status() != WL_CONNECTED && tentativas < 20) { delay(500); Serial.print("."); tentativas++; Serial.println(); } mostrar_rede(); } void loop() { unsigned long agora = millis(); if (agora - ultimo_envio < ESPERA_ENTRE_TENTATIVAS_MS) { return; } ultimo_envio = agora; // SITUACAO 1: sem rede. Nao ha envio, mas o loop volta e a placa respira. if (WiFi.status() != WL_CONNECTED) { falhas_seguidas++; total_falhas++; Serial.println("[1] SEM REDE — envio pulado, contador de falhas em"); Serial.print(" "); Serial.println(falhas_seguidas); // Pisca rapido: o sinal de que a placa esta viva mas sem caminho. digitalWrite(PIN_LED, HIGH); delay(80); digitalWrite(PIN_LED, LOW); if (falhas_seguidas == MAX_TENTATIVAS_CONSECUTIVAS) { Serial.println(" tres falhas seguidas: a placa nao vai insistir, "); Serial.println(" ela espera o intervalo e tenta de novo mais devagar."); } return; } digitalWrite(PIN_LED, HIGH); String url = "http://" + String(IP_SERVIDOR) + ":" + String(PORTA_SERVIDOR) + CAMINHO; http.setTimeout(TIMEOUT_MS); // o timeout mora AQUI, antes do begin http.setReuse(true); http.begin(url); http.addHeader("Content-Type", "application/json"); int codigo = http.POST("{\"id_placa\":\"esp32-lab01\",\"temp_c\":26.5}"); // SITUACAO 2: codigo NEGATIVO. A resposta veio da pilha de rede, nao do // servidor: nao chegou nada, ou a rede sumiu no meio do caminho. // SITUACAO 3: codigo POSITIVO mas diferente de 201. O servidor respondeu // e disse que nao gravou. Sao problemas diferentes, com pistas diferentes. if (codigo > 0) { if (codigo == 201) { falhas_seguidas = 0; // deu certo: zera o contador total_ok++; Serial.print("[3] RESPOSTA OK 201 — gravada ("); Serial.print(total_ok); Serial.println(" ok no total)"); } else { falhas_seguidas++; total_falhas++; Serial.print("[3] RESPOSTA RECUSADA, codigo "); Serial.print(codigo); Serial.print(" — "); Serial.println(http.getString()); } Serial.print(" corpo: "); Serial.println(http.getString()); } else { falhas_seguidas++; total_falhas++; Serial.print("[2] SEM RESPOSTA, codigo "); Serial.print(codigo); Serial.println(" — o servidor nao respondeu no tempo do timeout"); } http.end(); digitalWrite(PIN_LED, LOW); }
Por que assim e não de outro jeito. A função mostrar_rede() existe so para o professor e o aluno lerem o mesmo número com o mesmo nome ao lado. O switch não esta ali por enfeite: e o que transforma 4 em "senha recusada", e essa traducao e o que economiza dez minutos em cada diagnostico do semestre.
O return do loop aparece duas vezes e nos dois casos e a saída do caminho. Sem ele, uma falha de rede cairia no bloco do http.POST e tentaria enviar sem conexão, produzindo um código negativo falso — o aluno veria "falha de rede" quando o problema era so o WiFi.
O if (codigo > 0) externo e o que separa as situacoes 2 e 3, e o if (codigo == 201) interno e o que decide se o contador zera. São duas perguntas diferentes: "alguem respondeu?" e "respondeu bem?". O código do dia 8 organiza isso em try/catch, e o aluno vai reconhecer este if la dentro.
A saída real da placa, medida nesta maquina com o IP do servidor apontando para onde não existe:
[2] SEM RESPOSTA, codigo -1 — o servidor nao respondeu no tempo do timeout
Criterios de correcao
| Critério | Pontos |
|---|---|
| Tabela das três situacoes preenchida com evidência do serial, não de memória | 3 pontos |
| Item 2: o código negativo foi identificado como falha de envio | 2 pontos |
Item 3 e 4: os números de WiFi.status() comparados e a conclusao escrita | 3 pontos |
TIMEOUT_MS declarado como constante e usado antes do begin | 2 pontos |
Item 6: o contador de falhas foi observado após um 201 e a linha faltante identificada | 2 pontos |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
| Tratar código negativo como resposta do servidor | "O servidor respondeu -1" | "Código negativo não vem do servidor. Ninguem no Node recebeu o seu pedido. O erro e de socket, na rede. Olhe o terminal do Node: se não tem nada la, quem falhou foi a rede." |
| Tratar 404 como "sem resposta" | Item 2 com 404 na coluna errada | "404 e resposta: o servidor falou. Ele so não conhece o caminho. Sem resposta e código negativo." |
Não usar setTimeout | A placa trava por muito tempo e o professor já perguntou "ainda ta gravando?" | "Sem timeout, o POST espera até a rede desistir. Durante essa espera o loop para. A placa fica viva, mas parada. Declare o tempo e chame setTimeout antes do begin." |
Colocar o setTimeout depois do begin | A placa trava do mesmo jeito | "setTimeout configura o cliente, então tem de vir antes de begin criar a conexão. Depois do begin e tarde: o begin já abriu o socket." |
| Esquecer de zerar o contador no sucesso | Item 6 com o contador preso em três mesmo depois do 201 | "Você conta falha e nunca desconta. Toda vez que o envio volta a dar certo, zere o contador. Sem isso a placa fica achando que ainda esta falhando depois de voltar a funcionar." |
Chamar WiFi.reconnect() em laco apertado | A placa gasta toda a bateria e nunca conecta | "reconnect em laco e reconnect constante. Uma placa que perdeu a rede espera o intervalo dela e tenta. O ESPERA_ENTRE_TENTATIVAS_MS existe por causa disso." |
| Achar que sem WiFi a placa "para" | "A placa desligou sozinha" | "Ela continua rodando. Repare no LED piscando rapido e no serial repetindo. Placa parada e placa viva sem caminho são coisas bem diferentes, e so a segunda aparece nesta aula." |
| Usar o mesmo código para 4xx e 5xx no reenvio | "Vou reenviar até dar certo" | "Com 4xx, reenviar não resolve: o pedido esta errado e vai chegar errado de novo. Com 5xx, reenviar faz sentido. O dia 8 separa os dois com try/catch." |
Desafio extra
Troque ESPERA_ENTRE_TENTATIVAS_MS de 5000 para algo crescente: 5000 na primeira falha, 10000 na segunda, 15000 na terceira. Isso se chama *backoff*, e e a tecnica que o professor quer que o aluno reconheca. Meça quanto tempo a placa leva para tentar de novo depois de cada falha, e escreva em uma frase por que esperar cada vez mais e melhor do que esperar sempre o mesmo tempo. A resposta: quando o problema e o outro lado, insistir no mesmo ritmo não faz o outro lado voltar mais rapido — so gasta a sua bateria e piora a fila.
A resolucao, compilada
// Aula 2 do dia 3: o que a placa faz quando o caminho nao existe. // // Tres situacoes, e a aula nao e sobre a placa funcionar: e sobre a placa // continuar ALIVE. Um ESP32 que trava esperando a rede morre no seu // programa de forma silenciosa, e o professor descobre no fim do semestre. #include <Arduino.h> #include <WiFi.h> #include <HTTPClient.h> const int PIN_LED = 2; // Credenciais FICTICIAS. const char* WIFI_SSID = "rede-fantasma"; const char* CHAVE_REDE = "ficticia-lab01"; // Servidor de proposito IREACHIVEL, para o aluno ver o codigo negativo. // Na aula real o professor troca pelo IP que responde. const char* IP_SERVIDOR = "192.168.0.99"; const int PORTA_SERVIDOR = 3000; const char* CAMINHO = "/leitura"; // Tempo MAXIMO de espera pelo servidor. Passou disso, desiste. Este numero // e o timeout: sem ele, a placa trava esperando resposta que nunca vem. const unsigned long TIMEOUT_MS = 3000; // Quanto tempo a placa espera antes de TENTAR DE NOVO, entre uma falha e // outra. Tentar 60 vezes por segundo derruba a bateria e a rede. const long ESPERA_ENTRE_TENTATIVAS_MS = 5000; const int MAX_TENTATIVAS_CONSECUTIVAS = 3; WiFiClient cliente; HTTPClient http; unsigned long ultimo_envio = 0; int falhas_seguidas = 0; int total_ok = 0; int total_falhas = 0; void mostrar_rede() { Serial.print(" WiFi.status() = "); Serial.print(WiFi.status()); switch (WiFi.status()) { case WL_CONNECTED: Serial.println(" (WL_CONNECTED) conectado"); break; case WL_NO_SSID_AVAIL: Serial.println(" (WL_NO_SSID_AVAIL) rede nao existe"); break; case WL_CONNECT_FAILED: Serial.println(" (WL_CONNECT_FAILED) senha recusada"); break; case WL_IDLE_STATUS: Serial.println(" (WL_IDLE_STATUS) ainda tentando"); break; case WL_DISCONNECTED: Serial.println(" (WL_DISCONNECTED) desconectado"); break; default: Serial.println(" (outro)"); break; } } void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(2000); Serial.println(); Serial.println("2o trimestre, dia 3, aula 2 - quando o caminho quebra"); Serial.println("-------------------------------------------------"); Serial.println("Placa alive: o programa roda mesmo com tudo errado."); Serial.println(); WiFi.begin(WIFI_SSID, CHAVE_REDE); int tentativas = 0; while (WiFi.status() != WL_CONNECTED && tentativas < 20) { delay(500); Serial.print("."); tentativas++; Serial.println(); } mostrar_rede(); } void loop() { unsigned long agora = millis(); if (agora - ultimo_envio < ESPERA_ENTRE_TENTATIVAS_MS) { return; } ultimo_envio = agora; // SITUACAO 1: sem rede. Nao ha envio, mas o loop volta e a placa respira. if (WiFi.status() != WL_CONNECTED) { falhas_seguidas++; total_falhas++; Serial.println("[1] SEM REDE — envio pulado, contador de falhas em"); Serial.print(" "); Serial.println(falhas_seguidas); // Pisca rapido: o sinal de que a placa esta viva mas sem caminho. digitalWrite(PIN_LED, HIGH); delay(80); digitalWrite(PIN_LED, LOW); if (falhas_seguidas == MAX_TENTATIVAS_CONSECUTIVAS) { Serial.println(" tres falhas seguidas: a placa nao vai insistir, "); Serial.println(" ela espera o intervalo e tenta de novo mais devagar."); } return; } digitalWrite(PIN_LED, HIGH); String url = "http://" + String(IP_SERVIDOR) + ":" + String(PORTA_SERVIDOR) + CAMINHO; http.setTimeout(TIMEOUT_MS); // o timeout mora AQUI, antes do begin http.setReuse(true); http.begin(url); http.addHeader("Content-Type", "application/json"); int codigo = http.POST("{\"id_placa\":\"esp32-lab01\",\"temp_c\":26.5}"); // SITUACAO 2: codigo NEGATIVO. A resposta veio da pilha de rede, nao do // servidor: nao chegou nada, ou a rede sumiu no meio do caminho. // SITUACAO 3: codigo POSITIVO mas diferente de 201. O servidor respondeu // e disse que nao gravou. Sao problemas diferentes, com pistas diferentes. if (codigo > 0) { if (codigo == 201) { falhas_seguidas = 0; // deu certo: zera o contador total_ok++; Serial.print("[3] RESPOSTA OK 201 — gravada ("); Serial.print(total_ok); Serial.println(" ok no total)"); } else { falhas_seguidas++; total_falhas++; Serial.print("[3] RESPOSTA RECUSADA, codigo "); Serial.print(codigo); Serial.print(" — "); Serial.println(http.getString()); } Serial.print(" corpo: "); Serial.println(http.getString()); } else { falhas_seguidas++; total_falhas++; Serial.print("[2] SEM RESPOSTA, codigo "); Serial.print(codigo); Serial.println(" — o servidor nao respondeu no tempo do timeout"); } http.end(); digitalWrite(PIN_LED, LOW); }
Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia03 aula2.
