A placa fala: JSON e WiFi do ESP32 — Arduino e IoT — semana 3 do 2o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 3 · A placa fala: JSON e WiFi do ESP32 — Material de Apoio Arduino

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 String e 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, com begin, addHeader e POST, e ler a resposta com getString.
  • Escolher o formato do número no JSON e justificar por que 26,5 quebraria o JSON.parse do 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:

EtapaComo o código falhaO que o serial mostra
RedeWiFi.status() != WL_CONNECTED"sem rede: envio pulado"
Enviohttp.POST devolve negativo"falha de rede, código -1"
Respostacodigo > 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.
  1. 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.
  2. Grave o sketch da resolucao. Abra o monitor serial e espere a placa conectar. Anote o IP que ela recebeu.
  3. Troque TEMPERATURA para 26.5 e grave. Troque para 26,5 (virgula) e grave. O que muda no monitor serial? E o que o Node recebe em cada caso?
  4. Adicione um campo novo ao JSON, umidade_pct, com o valor 58. Grave e confira se o Node imprimiu o campo novo no log.
  5. 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?
  6. 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érioPontos
JSON transcrito a mao no caderno, conferido caractere por caractere2 pontos
Placa conecta no WiFi e o IP anotado2 pontos
Item 3: a diferença entre ponto e virgula esta explicada2 pontos
Campo umidade_pct adicionado e visivel no log do Node2 pontos
Item 5: o código negativo foi identificado como falha de envio, e não de resposta2 pontos
Item 6: o tempo de volta do 201 foi medido e anotado2 pontos

Erros comuns

ErroComo apareceCorrecao
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 valorO 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 seguirA 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 limiteA 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 addHeaderNode 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 segundosA 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 significam WL_CONNECTED, WL_NO_SSID_AVAIL e WL_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.setTimeout e 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

SituacaoSintoma no serialO que aconteceu
Sem rede[1] SEM REDE e nada e enviadoa placa nem chegou a tentar
Sem servidor[2] SEM RESPOSTA, codigo -1o pedido saiu e ninguem respondeu
Servidor recusa[3] RESPOSTA RECUSADA, codigo 404alguem 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:

  1. contar as falhas seguidas;
  2. quando a falha vem, incrementar;
  3. quando o envio volta a dar certo, zerar o contador;
  4. 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.

  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.
  2. 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?
  3. Desligue o roteador por 15 segundos e religue. O que o WiFi.status() mostra? O que o código numerico diz sobre a causa?
  4. Troque WIFI_SSID para um nome que não existe. Grave. Compare o número do WiFi.status() com o do item 3. Os dois números são diferentes? O que isso permite concluir sobre o diagnostico?
  5. Mude TIMEOUT_MS de 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.
  6. 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érioPontos
Tabela das três situacoes preenchida com evidência do serial, não de memória3 pontos
Item 2: o código negativo foi identificado como falha de envio2 pontos
Item 3 e 4: os números de WiFi.status() comparados e a conclusao escrita3 pontos
TIMEOUT_MS declarado como constante e usado antes do begin2 pontos
Item 6: o contador de falhas foi observado após um 201 e a linha faltante identificada2 pontos

Erros comuns

ErroComo apareceCorrecao
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 setTimeoutA 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 beginA 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 sucessoItem 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 apertadoA 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.