HTTP: o idioma que o ESP32 fala — Arduino e IoT — semana 2 do 2o trimestre
Semana 2 de 16· 2o trimestre · 01/05 a 04/09
HTTP: o idioma que o ESP32 fala
Request, response, método e status. O que a placa precisa saber para não travar.
Aula 1 — O que e HTTP: metodo, caminho e código de status
Objetivos
- Nomear as quatro partes de uma requisicao HTTP: metodo, caminho, cabecalho e corpo.
- Escolher o metodo certo para cada operacao:
GETpara ler,POSTpara criar, e dizer por que a leitura não usaPOST. - Reconhecer um código de status pelo número e dizer o que a placa precisa fazer com cada um.
- Explicar o que o cabecalho
Content-Typefaz e por queJSON.parsefalha sem ele. - Montar, na placa, o texto exato de uma requisicao e comparar com o que o Node vai receber.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1 protoboard de 830 pontos por dupla
- Computador com Node.js 20 ou superior e
curldisponivel - Projetor, para o terminal do professor
- Folha de papel por aluno, para o mapeamento metodo/função/status
Conceitos
As quatro partes
Toda requisicao HTTP tem quatro partes, nesta ordem:
POST /leitura HTTP/1.1 Host: 192.168.0.10:3000 Content-Type: application/json Content-Length: 92 {"id_placa":"esp32-lab01","temp_c":26.5}
| Parte | Onde esta | Para que serve |
|---|---|---|
| Metodo | POST | o que você quer fazer com o recurso |
| Caminho | /leitura | em qual recurso |
| Cabecalho | Content-Type | como interpretar o que vem depois |
| Corpo | o JSON | os dados proprios ditos |
A ordem importa: o servidor le o metodo e o caminho para escolher a rota, e so depois olha o cabecalho para decidir como interpretar o corpo. Inverter essa ordem quebra a leitura.
Metodo: o verbo da operacao
| Metodo | Significado | Idempotente | No curso |
|---|---|---|---|
GET | ler, não muda nada | sim | dia 6, consultar dados |
POST | criar algo novo | não | dia 2 e dia 5, receber leitura |
PUT | substituir tudo | sim | não usado neste trimestre |
DELETE | apagar | sim | dia 7, limpar dado velho |
GET e idempotente: chamar dez vezes deixa o servidor no mesmo estado. POST não e: chamar dez vezes cria dez leituras. E por isso que a placa usa POST para enviar e nunca GET: mandar dez vezes precisa gerar dez linhas, e se fosse GET o servidor poderia muito bem ignorar a segunda.
Código de status: o que a placa precisa ler
O servidor responde com três digitos. O primeiro digito já diz a familia inteira:
| Familia | Significado | Exemplos | O que a placa faz |
|---|---|---|---|
| 2xx | deu certo | 200, 201 | considera gravado |
| 4xx | o pedido esta errado | 400, 404, 422 | não tenta de novo, arruma o dado |
| 5xx | o servidor falhou | 500, 503 | pode tentar de novo depois |
A distinção pratica que o aluno precisa ter na cabeça: 4xx não adianta tentar de novo, porque o problema esta no que a placa mandou. 5xx adianta, porque o problema esta do outro lado. O curl da abertura da aula mostra as duas: um 404 e um 500.
Content-Type: a frase que diz do que se trata
O cabecalho Content-Type: application/json diz ao servidor que o corpo e JSON. Sem ele, o servidor tem três opcoes: adivinhar, recusar, ou tentar interpretar como texto. O Node faz a primeira, e quando adivinha errado o JSON.parse lanca excecao.
O professor repete: Content-Type e o cabecalho que o dia 2 aula 2 cobra da placa, e o dia 3 aula 1 monta.
Atividade
Montagem: nenhuma. A placa não entra no WiFi hoje. O sketch monta a requisicao e mostra na tela, sem enviar.
- Escreva no caderno as quatro partes de uma requisicao, em ordem, com uma frase explicando para que serve cada uma.
- Preencha a tabela: para criar uma leitura nova, ler as ultimas 10 leituras e apagar leituras de ontem, qual metodo você usaria e por que. Depois circule na propria tabela as duas operacoes que não precisam de corpo.
- Para cada código de status abaixo, escreva o que a placa deve fazer: 200, 201, 404, 422, 500, -1.
- Grave o sketch da resolucao. Escreva no caderno, exatamente como aparece no monitor serial, o texto que a placa vai escrever na rede.
- Apague o
Content-Typedo texto que você escreveu no item 4 e explique em uma frase o que o servidor faz quando o recebe. - O professor mostra um
curlque devolveu 404 e outro que devolveu 500. Escreva a diferença entre "arrumar o código da placa" e "esperar e tentar de novo", e qual dos dois se aplica a cada caso.
Nota: 10 pontos. Critério de fim: a tabela de metodos esta preenchida com justificativa, os seis codigos de status tem resposta, e o item 4 esta transcrito sem erro.
Resolucao
O curl da abertura, rodado nesta maquina contra um endereco que não responde:
$ curl -X POST http://192.168.0.10:3000/leitura curl: (7) Failed to connect to 192.168.0.10 port 3000: Connection refused
O curl: (7) e o código de erro do proprio curl, e não um status HTTP. O aluno precisa distinguir as duas coisas: um status e uma resposta de um servidor, mesmo ruim; um erro de curl e "não houve resposta nenhuma". Essa distincao reaparece no dia 3 aula 2, no código negativo do HTTPClient.
O sketch de hoje não entra no WiFi e não envia. Ele monta o texto da requisicao e mostra na tela, porque o aluno precisa ver o que vai sair antes de a rede existir:
// Aula 1 do dia 2: o que e HTTP, visto de dentro da placa. // // HTTP e um protocolo de REGRAS, nao de biblioteca. Nao existe // HTTPClient.h no dia 2: ele so aparece na aula 2. Aqui o aluno ve que // uma requisicao e so um texto com forma combinada, e que o ESP32 sabe // montar esse texto com String e escrever pela rede. #include <Arduino.h> // O mesmo "caminho" que a placa vai usar contra o servidor Node. Mudar o // metodo e o caminho e mudar a frase que a placa fala. const char* METODO = "POST"; const char* CAMINHO = "/leitura"; const char* SERVIDOR = "192.168.0.10"; const int PORTA = 3000; // Cabecalho que o Node exige para fazer o JSON.parse sem reclamar. const char* CABECALHO_CHAVE = "Content-Type"; const char* CABECALHO_VALOR = "application/json"; // O que a placa vai mandar. Mesmo formato do JSON do dia 3. const char* CORPO = "{\"id_placa\":\"esp32-lab01\",\"temp_c\":26.5}"; void setup() { Serial.begin(115200); delay(2000); Serial.println(); Serial.println("2o trimestre, dia 2, aula 1 - a anatomia de uma requisicao"); Serial.println("================================================="); Serial.println("uma requisicao HTTP e um texto com forma combinada."); Serial.println("quatro partes, nesta ordem:"); Serial.println(" 1. metodo : o que voce quer fazer"); Serial.println(" 2. caminho : em qual recurso"); Serial.println(" 3. cabecalho : como interpretar o corpo"); Serial.println(" 4. corpo : os dados"); Serial.println(); // Monta a requisicao de verdade, so que NAO envia. A aula mostra o texto // antes de ele sair na rede, e essa diferenca e o que separa "erro meu" // de "erro do outro". String requisicao = String(METODO) + " " + CAMINHO + " HTTP/1.1"; String cabecalho = String(CABECALHO_CHAVE) + ": " + CABECALHO_VALOR; String endereco = String("servidor: ") + String(SERVIDOR) + ":" + String(PORTA); Serial.println("--- o que a placa vai escrever na rede ---"); Serial.println(requisicao); Serial.println(endereco); Serial.println(cabecalho); Serial.println(String("Content-Length: ") + String(strlen(CORPO))); Serial.println(); Serial.println(CORPO); Serial.println(); Serial.println("--- o que o servidor responde ---"); Serial.println("HTTP/1.1 201 Created"); Serial.println("Content-Type: application/json"); Serial.println("{\"id\":1,\"gravado\":true}"); Serial.println(); Serial.println("201 e resposta de CRIADO. 200 e de OK. 404 e de NAO ACHEI."); Serial.println("O codigo de status e a unica coisa que a placa precisa ler"); Serial.println("para saber se o dado entrou. A aula 2 do dia 3 le esse numero."); } void loop() { // Hoje a placa nao envia nada. Ela so lembra a frase, a cada volta, para // o professor poder apontar para o monitor durante a explicacao. Serial.println("--- (recapitulando, sem enviar) ---"); Serial.print(METODO); Serial.print(" "); Serial.print(CAMINHO); Serial.print(" -> servidor "); Serial.print(SERVIDOR); Serial.print(":"); Serial.println(PORTA); delay(4000); }
Por que assim e não de outro jeito. O sketch mostra o Content-Length calculado com strlen(CORPO), e não escrito a mao. Se o aluno mudar o corpo e esquecer de mudar o Content-Length, o servidor le um número de bytes diferente do que foi enviado, e a conexão quebra no meio do corpo — sem erro visivel do lado do Node. Mostrar o strlen e o que fecha essa porta.
O CORPO esta em const char* e não em String de propósito: e assim que o professor mostra que o JSON e texto, e que o strlen so existe porque e texto. No dia 3, a aula inverte: o JSON passa a ser String, porque ai ele precisa ser construido com valor dentro.
Criterios de correcao
| Critério | Pontos |
|---|---|
| As quatro partes da requisicao escritas em ordem, com a função de cada uma | 3 pontos |
| Tabela de metodos preenchida com justificativa, e as duas operacoes sem corpo circuladas | 3 pontos |
| Os seis codigos de status com a ação correta da placa | 2 pontos |
| Item 4: o texto da requisicao transcrito no caderno sem erro | 1 ponto |
| Item 5 e item 6 respondidos em uma frase cada | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
Usar GET para enviar a leitura | "Usei GET porque e para pegar informação" | "GET e para ler. E idempotente: se a placa mandar dez vezes, o servidor pode ignorar a segunda. Para gerar dez linhas, o verbo tem de ser POST." |
Escrever POST para consultar as ultimas leituras | A tabela de metodos toda com POST | "Consultar não cria nada, então e GET. Se você usar POST para ler, o dia 6 vai mostrar que cada consulta cria uma linha nova no log do servidor." |
| Achar que 404 significa "o servidor caiu" | Item 3 com 404 e "não gravou" | "404 e "não achei o caminho". O servidor esta de pe e respondeu. Ele so não conhece /leitura. Servidor caido e curl: (7), sem status nenhum." |
| Achar que 500 e culpa do código da placa | Item 3 com 500 e "arrumar o código" | "5xx e problema do servidor. O pedido chegou inteiro e o servidor falhou ao processar. O 4xx e o oposto: seu pedido que estava errado." |
Colocar o Content-Type depois do corpo | "Cabecalho vai embaixo" | "O servidor le o cabecalho para decidir como ler o que vem depois. Cabecalho embaixo do corpo não serve para nada: o corpo já chegou sem explicacao." |
Ler Content-Length como "o tamanho da resposta" | Item 4 com o número trocado | "Content-Length e quantos bytes o corpo que você mandou ocupa. A resposta tem o Content-Length dela, e e outro número." |
Desafio extra
Escreva, na placa, a requisicao que consulta as ultimas 10 leituras: qual metodo, qual caminho, quais cabecalhos, e o que vem no corpo. Depois escreva a requisicao que apaga as leituras de ontem. Em seguida, escreva qual das duas, segundo você, tem risco de ser repetida sem querer se a placa reiniciar no meio do envio, e por que. A resposta que o professor espera: as duas, porque POST e DELETE podem ser repetidos, e e por isso que o dia 7 precisa de idempotencia e de chave para deduplicar.
A resolucao, compilada
// Aula 1 do dia 2: o que e HTTP, visto de dentro da placa. // // HTTP e um protocolo de REGRAS, nao de biblioteca. Nao existe // HTTPClient.h no dia 2: ele so aparece na aula 2. Aqui o aluno ve que // uma requisicao e so um texto com forma combinada, e que o ESP32 sabe // montar esse texto com String e escrever pela rede. #include <Arduino.h> // O mesmo "caminho" que a placa vai usar contra o servidor Node. Mudar o // metodo e o caminho e mudar a frase que a placa fala. const char* METODO = "POST"; const char* CAMINHO = "/leitura"; const char* SERVIDOR = "192.168.0.10"; const int PORTA = 3000; // Cabecalho que o Node exige para fazer o JSON.parse sem reclamar. const char* CABECALHO_CHAVE = "Content-Type"; const char* CABECALHO_VALOR = "application/json"; // O que a placa vai mandar. Mesmo formato do JSON do dia 3. const char* CORPO = "{\"id_placa\":\"esp32-lab01\",\"temp_c\":26.5}"; void setup() { Serial.begin(115200); delay(2000); Serial.println(); Serial.println("2o trimestre, dia 2, aula 1 - a anatomia de uma requisicao"); Serial.println("================================================="); Serial.println("uma requisicao HTTP e um texto com forma combinada."); Serial.println("quatro partes, nesta ordem:"); Serial.println(" 1. metodo : o que voce quer fazer"); Serial.println(" 2. caminho : em qual recurso"); Serial.println(" 3. cabecalho : como interpretar o corpo"); Serial.println(" 4. corpo : os dados"); Serial.println(); // Monta a requisicao de verdade, so que NAO envia. A aula mostra o texto // antes de ele sair na rede, e essa diferenca e o que separa "erro meu" // de "erro do outro". String requisicao = String(METODO) + " " + CAMINHO + " HTTP/1.1"; String cabecalho = String(CABECALHO_CHAVE) + ": " + CABECALHO_VALOR; String endereco = String("servidor: ") + String(SERVIDOR) + ":" + String(PORTA); Serial.println("--- o que a placa vai escrever na rede ---"); Serial.println(requisicao); Serial.println(endereco); Serial.println(cabecalho); Serial.println(String("Content-Length: ") + String(strlen(CORPO))); Serial.println(); Serial.println(CORPO); Serial.println(); Serial.println("--- o que o servidor responde ---"); Serial.println("HTTP/1.1 201 Created"); Serial.println("Content-Type: application/json"); Serial.println("{\"id\":1,\"gravado\":true}"); Serial.println(); Serial.println("201 e resposta de CRIADO. 200 e de OK. 404 e de NAO ACHEI."); Serial.println("O codigo de status e a unica coisa que a placa precisa ler"); Serial.println("para saber se o dado entrou. A aula 2 do dia 3 le esse numero."); } void loop() { // Hoje a placa nao envia nada. Ela so lembra a frase, a cada volta, para // o professor poder apontar para o monitor durante a explicacao. Serial.println("--- (recapitulando, sem enviar) ---"); Serial.print(METODO); Serial.print(" "); Serial.print(CAMINHO); Serial.print(" -> servidor "); Serial.print(SERVIDOR); Serial.print(":"); Serial.println(PORTA); delay(4000); }
Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia02 aula1.
Aula 2 — Servidor HTTP em Node que recebe dado do ESP32
Objetivos
- Subir um servidor HTTP em Node com
http.createServerelisten, e dizer o que cada argumento significa. - Receber o corpo de um
POSTacumulando os pedacos emreq.on('data')e fechando emreq.on('end'). - Responder com
res.writeHeaderes.end, escolhendo o código de status certo para cada desfecho. - Reproduzir, e depois corrigir, o erro do
endfaltando, e explicar por que o servidor "fica carregando" sem ele. - Apontar, no log do servidor, em qual das quatro etapas o caminho parou.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1 protoboard de 830 pontos por dupla
- Computador com Node.js 20 ou superior
- Terminal com dois paineis: um para o servidor, um para o
curl - Projetor, para o terminal do professor
Conceitos
http.createServer e listen
const servidor = http.createServer((req, res) => { /* roda a cada pedido */ });
servidor.listen(3000);
O createServer não abre nada: ele so registra a função que sera chamada. O listen e que abre a porta e faz o processo ficar vivo. Um programa com createServer e sem listen imprime e morre, exatamente como o programa de ontem.
O detalhe que quebra a aula: listen(3000) com a porta fixa falha se a porta já estiver ocupada, e o aluno ve o erro EADDRINUSE e conclui que "o Node esta quebrado". Na aula, use listen(0) e pegue a porta com servidor.address().port, que e o que o exemplo faz.
O corpo chega em pedacos
Este e o coracao da aula. Uma requisicao com corpo não chega inteira: o Node entrega o corpo em eventos, e o programador tem de juntar:
let corpo = '';
req.on('data', (pedaco) => { corpo += pedaco.toString(); });
req.on('end', () => { /* aqui o corpo esta inteiro */ });
O req e um emissor de eventos: on('data') e chamado cada vez que chega um pedaco, e on('end') e chamado uma vez, quando acabou. Sem o on('end'), o trecho que responde nunca executa, e o pedido fica parado até a conexão morrer sozinha.
Para um JSON de 92 bytes isso quase nunca aparece, porque a resposta cabe em um pacote. Para um JSON de 200 KB, aparece sempre. O professor usa o curl com corpo pequeno e explica que o problema esta la, escondido, esperando a aula 13.
writeHead e end
Toda resposta tem duas partes: o status com cabecalhos, e o corpo.
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ id: 1, gravado: true }));
writeHead não envia nada ainda: ele memoriza. E end que fecha e envia. Um writeHead sem end deixa o navegador esperando para sempre — o mesmo sintoma do end faltando no req, e por isso o professor trata os dois como o mesmo defeito.
O Content-Type: application/json na resposta não e enfeite: e o que diz ao cliente que o corpo pode ser lido com JSON.parse e não como texto.
Atividade
Montagem: a placa não se conecta hoje. Ela fica gravada, com o sketch do dia 2 aula 1, so para o aluno ter a frase da requisicao na frente dos olhos.
- No terminal, crie o arquivo
servidor.jse digite o código da resolucao. Rode comnode servidor.jse não feche o terminal. - Em um segundo terminal, rode
curl -X POST http://127.0.0.1:3000/leitura -H "Content-Type: application/json" -d '{"id_placa":"lab01","temp_c":26.5}'. Anote o status e o corpo da resposta. - Confira no log do servidor: quantos bytes de corpo apareceram? O número bate com o tamanho do JSON que você mandou?
- Apague a linha do
req.on('end', ...)e rode ocurlde novo, com--max-time 5. O que ocurlimprime? Quanto tempo ele esperou? O que o log do servidor mostra? - Restaure a linha, troque o status de
201para404e mantenha o caminho/leitura. Rode ocurl. Quem esta errado agora, o cliente ou o servidor? Justifique com uma frase. - Adicione uma segunda rota,
GET /status, que responde200com um JSON de texto fixo. Teste comcurle depois no navegador, digitando o endereco na barra. O que muda entre os dois clientes? - Escreva, no caderno, o que você precisaria trocar no código para a placa conseguir falar com esse servidor. Quem fornece o endereco: a placa ou o servidor?
Nota: 12 pontos. Critério de fim: o servidor responde 201 ao POST /leitura, responde 200 ao GET /status, e o item 4 foi executado e anotado com o tempo de espera.
Resolucao
O exemplo foi executado nesta maquina com Node 24, e e o que o professor projeta:
// O servidor que recebe a leitura do ESP32.
// O ponto da aula: o corpo chega em PEDACOS, e so depois do evento 'end'
// que ele esta inteiro. Sem o 'end', o servidor nunca responde.
const http = require('http');
const servidor = http.createServer((req, res) => {
if (req.method === 'POST' && req.url === '/leitura') {
let corpo = ''; // acumulador: comeca vazio
req.on('data', (pedaco) => {
corpo += pedaco.toString(); // cada pedaco e acrescentado
});
req.on('end', () => {
console.log('corpo completo, tamanho', corpo.length);
const leitura = JSON.parse(corpo);
console.log('JSON interpretado:', leitura);
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));
const url = 'http://127.0.0.1:' + servidor.address().port + '/leitura';
console.log('servidor em', url);
// Este POST imita exatamente o que a placa faz com HTTPClient.
const resposta = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id_placa: 'esp32-lab01', temp_c: 27.4, umidade_pct: 58 }),
});
console.log('status', resposta.status, await resposta.text());
servidor.close();
})();
A saída real, medida nesta maquina:
servidor em http://127.0.0.1:33275/leitura corpo completo, tamanho 57 JSON interpretado: { id_placa: 'esp32-lab01', temp_c: 27.4, umidade_pct: 58 } status 201 {"id":1,"gravado":true}
O professor aponta dois números nessa saída: o 33275 da porta e o 57 do tamanho do corpo. A porta muda a cada execução porque o exemplo usa listen(0), que pede uma porta livre ao sistema. O 57 e fixo, e e o tamanho exato do JSON enviado — id_placa com 10 caracteres, temp_c com 4, umidade_pct com 2, mais as chaves e as virgulas. Se o aluno contar, ele chega no mesmo número, e ai o item 3 da atividade tem resposta.
O sketch da placa mostra o lado de dentro do HTTPClient, para o aluno ver que a biblioteca faz exatamente o que a aula 2 descreveu:
// Aula 2 do dia 2: agora a placa tem um destino. O servidor HTTP em Node que o // professor subiu e o servidor que o dia 2 aula 1 explicou. A placa fala // HTTP e o professor mostra o log do Node receiving cada POST. // // WiFi e um nome e uma senha FICTICIOS: a rede de teste do professor troca de // nome toda aula, e nenhum segredo real entra em material. O aluno troca // pelas credenciais da rede da escola. #include <Arduino.h> #include <WiFi.h> #include <HTTPClient.h> // GPIO2 e o LED embutido na maioria dos DevKit: serve de indicador de vida. const int PIN_LED = 2; // Credenciais da rede. FICTICIAS de proposito, com este comentario. const char* WIFI_SSID = "rede-teste-lab01"; const char* CHAVE_REDE = "ficticia-lab01"; // Endereco do PC que roda o Node. O IP e da maquina do professor, nunca do // roteador: o ESP32 conversa com o processo, nao com a internet. const char* IP_SERVIDOR = "192.168.0.10"; const int PORTA_SERVIDOR = 3000; const char* CAMINHO = "/leitura"; const long INTERVALO_MS = 5000; WiFiClient cliente; HTTPClient http; void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(2000); Serial.println(); Serial.println("2o trimestre, dia 2, aula 2 - a placa fala com o servidor"); Serial.println("-------------------------------------------------"); Serial.print("conectando em "); Serial.println(WIFI_SSID); WiFi.begin(WIFI_SSID, CHAVE_REDE); // Nao existe "esperar o wifi": a placa precisa checar o status num laco. // Esperar um tempo fixo e a causa numero um de "a placa nao enviou nada". int tentativas = 0; while (WiFi.status() != WL_CONNECTED && tentativas < 40) { delay(500); Serial.print("."); tentativas++; } Serial.println(); if (WiFi.status() == WL_CONNECTED) { Serial.print("conectado. IP da placa: "); Serial.println(WiFi.localIP()); digitalWrite(PIN_LED, HIGH); } else { Serial.println("FALHA: nao conectou. Verifique o nome da rede e a senha."); Serial.println("O codigo segue rodando: e o que a aula 3 aula 2 vai tratar."); } } void loop() { if (WiFi.status() != WL_CONNECTED) { Serial.println("sem rede: envio pulado, o loop continua"); delay(INTERVALO_MS); return; } digitalWrite(PIN_LED, LOW); // O corpo do POST. A String monta o JSON inteiro, chave por chave. // O primeiro operando da soma precisa ser String. Duas strings do C // ("abc" + "def") NAO concatenam em C++ — o compilador recusa, e o aluno // que tentar isso na lousa aprende o motivo. String json = String("{\"id_placa\":\"esp32-lab01\"") + ",\"temp_c_simulado\":26.5" + ",\"umidade_pct_simulado\":59" + ",\"dt_leitura\":\"2026-09-30 10:00:00\"}"; 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("POST "); Serial.print(url); Serial.println(); Serial.print("enviado: "); Serial.println(json); if (codigo > 0) { Serial.print("resposta do servidor, codigo "); Serial.print(codigo); Serial.print(" ("); Serial.print(codigo == 201 ? "criado" : "outro"); Serial.println(")"); Serial.print("corpo: "); Serial.println(http.getString()); } else { // O codigo NEGATIVO vem da propria pilha de rede, nao do servidor. Serial.print("falha de rede, codigo "); Serial.print(codigo); Serial.println(" (o servidor nao respondeu)"); } http.end(); // sempre: sem isso a conexao fica presa e a proxima falha digitalWrite(PIN_LED, HIGH); delay(INTERVALO_MS); }
Por que assim e não de outro jeito. O await new Promise(...) antes do fetch e obrigatorio: sem ele, o fetch dispara antes do servidor terminar de abrir a porta, e o exemplo falha de forma intermitente — a falha mais cara de depurar que existe. O servidor.close() no fim também e obrigatorio: sem ele, o processo fica vivo e o terminal parece travado.
O http.end() da placa e o mesmo cuidado pelo outro lado. Sem ele, cada envio deixa a conexão presa, e a placa trava na terceira ou na quarta tentativa com um código de rede negativo que não tem nada a ver com o servidor.
Criterios de correcao
| Critério | Pontos |
|---|---|
| Servidor sobe, responde e fica rodando no terminal | 2 pontos |
Corpo acumulado em req.on('data') e fechado em req.on('end') | 3 pontos |
POST /leitura responde 201 e GET /status responde 200 | 2 pontos |
Item 4 executado: o comportamento sem o end foi medido e anotado com o tempo | 3 pontos |
| Item 5 respondido: cliente ou servidor, com justificativa | 1 ponto |
| Item 7 respondido: quem fornece o endereco, e por que | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
Esquecer o req.on('end') | O curl fica carregando até dar timeout, e o log mostra o corpo chegando mas nenhuma resposta | "O corpo chegou inteiro, e você so avisou o Node de cada pedaco. Falta avisar que acabou. Toda vez que você junta pedacos, precisa do evento de fim." |
Chamar res.writeHead e esquecer o res.end | O navegador fica carregando, o status nem aparece | "writeHead so anota. E o end que fecha e envia. E o mesmo defeito do end do req, visto do outro lado." |
Usar listen(3000) e bater em EADDRINUSE | "Node quebrou" | "A porta 3000 já esta ocupada, por outro programa seu. Use listen(0) e pegue a porta com servidor.address().port." |
Fazer o fetch antes do listen resolver | Funciona as vezes e falha outras | "Você deu fetch antes do servidor estar pronto. Espere o listen resolver com await new Promise(...)." |
Comparar req.url com /leitura quando o cliente mandou /leitura?placa=lab01 | Cai no 404 com o cliente mandando a URL certa | "O req.url traz o caminho com a query. Compare com startsWith ou corte no ? antes de comparar." |
| Enviar o JSON sem o cabecalho e culpar o Node | "JSON.parse deu erro" | "Sem Content-Type: application/json o servidor não sabe o que e isso. O curl precisa de -H, e a placa precisa de http.addHeader." |
Achar que o 201 e "o Node gravou no banco" | Item 5 com essa resposta | "Hoje o Node respondeu 201 e não gravou nada: o banco entra no dia 4. Status 201 quer dizer que o servidor aceitou o pedido, e não que o dado foi salvo." |
Desafio extra
Faca o servidor aceitar dois corpos de tamanho bem diferente: um POST com 57 bytes de JSON e um com 200 KB de JSON. Conte quantas vezes o evento data dispara em cada caso, usando um contador no log. Depois escreva em uma frase por que o mesmo código funciona para os dois, e qual e o tamanho em que um corpo += pedaco sem join comecaria a doer. A resposta que o professor espera: o data dispara uma vez para corpo pequeno e muitas vezes para corpo grande, e o += em laco fica caro porque cada concatenacao cria uma string nova.
A resolucao, compilada
// Aula 2 do dia 2: agora a placa tem um destino. O servidor HTTP em Node que o // professor subiu e o servidor que o dia 2 aula 1 explicou. A placa fala // HTTP e o professor mostra o log do Node receiving cada POST. // // WiFi e um nome e uma senha FICTICIOS: a rede de teste do professor troca de // nome toda aula, e nenhum segredo real entra em material. O aluno troca // pelas credenciais da rede da escola. #include <Arduino.h> #include <WiFi.h> #include <HTTPClient.h> // GPIO2 e o LED embutido na maioria dos DevKit: serve de indicador de vida. const int PIN_LED = 2; // Credenciais da rede. FICTICIAS de proposito, com este comentario. const char* WIFI_SSID = "rede-teste-lab01"; const char* CHAVE_REDE = "ficticia-lab01"; // Endereco do PC que roda o Node. O IP e da maquina do professor, nunca do // roteador: o ESP32 conversa com o processo, nao com a internet. const char* IP_SERVIDOR = "192.168.0.10"; const int PORTA_SERVIDOR = 3000; const char* CAMINHO = "/leitura"; const long INTERVALO_MS = 5000; WiFiClient cliente; HTTPClient http; void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(2000); Serial.println(); Serial.println("2o trimestre, dia 2, aula 2 - a placa fala com o servidor"); Serial.println("-------------------------------------------------"); Serial.print("conectando em "); Serial.println(WIFI_SSID); WiFi.begin(WIFI_SSID, CHAVE_REDE); // Nao existe "esperar o wifi": a placa precisa checar o status num laco. // Esperar um tempo fixo e a causa numero um de "a placa nao enviou nada". int tentativas = 0; while (WiFi.status() != WL_CONNECTED && tentativas < 40) { delay(500); Serial.print("."); tentativas++; } Serial.println(); if (WiFi.status() == WL_CONNECTED) { Serial.print("conectado. IP da placa: "); Serial.println(WiFi.localIP()); digitalWrite(PIN_LED, HIGH); } else { Serial.println("FALHA: nao conectou. Verifique o nome da rede e a senha."); Serial.println("O codigo segue rodando: e o que a aula 3 aula 2 vai tratar."); } } void loop() { if (WiFi.status() != WL_CONNECTED) { Serial.println("sem rede: envio pulado, o loop continua"); delay(INTERVALO_MS); return; } digitalWrite(PIN_LED, LOW); // O corpo do POST. A String monta o JSON inteiro, chave por chave. // O primeiro operando da soma precisa ser String. Duas strings do C // ("abc" + "def") NAO concatenam em C++ — o compilador recusa, e o aluno // que tentar isso na lousa aprende o motivo. String json = String("{\"id_placa\":\"esp32-lab01\"") + ",\"temp_c_simulado\":26.5" + ",\"umidade_pct_simulado\":59" + ",\"dt_leitura\":\"2026-09-30 10:00:00\"}"; 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("POST "); Serial.print(url); Serial.println(); Serial.print("enviado: "); Serial.println(json); if (codigo > 0) { Serial.print("resposta do servidor, codigo "); Serial.print(codigo); Serial.print(" ("); Serial.print(codigo == 201 ? "criado" : "outro"); Serial.println(")"); Serial.print("corpo: "); Serial.println(http.getString()); } else { // O codigo NEGATIVO vem da propria pilha de rede, nao do servidor. Serial.print("falha de rede, codigo "); Serial.print(codigo); Serial.println(" (o servidor nao respondeu)"); } http.end(); // sempre: sem isso a conexao fica presa e a proxima falha digitalWrite(PIN_LED, HIGH); delay(INTERVALO_MS); }
Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia02 aula2.
