Erro: o caminho sempre quebra em algum lugar — Arduino e IoT — semana 8 do 2o trimestre
Semana 8 de 16· 2o trimestre · 01/05 a 04/09
Erro: o caminho sempre quebra em algum lugar
Validar, tratar e registrar. O trimestre deixa de ser sobre funcionar.
Aula 1 — Validar o dado que chega antes de gravar
Objetivos
- Rodar a mesma regra de validação dos dois lados do cabo e mostrar que ela devolve o mesmo veredito para o mesmo dado.
- Escrever a validação que separa campo obrigatório ausente, campo vazio, tipo errado e valor fora do limite plausível.
- Escolher o status de resposta (
400,422ou201) a partir do tipo de problema, e não do humor do professor. - Medir quantas das leituras que chegaram entraram no banco, e comparar com quantas chegaram.
- Dizer por que a regra roda na placa *e* no servidor, em vez de só num dos dois.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1
DHT11por dupla, no circuito do dia 8 do 1o trimestre, com resistor de pull-up - 1 protoboard de 830 pontos e 1 jogo de jumpers por dupla
- 1 computador com Node.js 20,
mysql2instalado e MySQL ou MariaDB 10.11 - Terminal com o
curlaberto na segunda aba - Projetor, para o monitor serial da placa e o terminal do Node juntos
Conceitos
Sujeira na entrada tem quatro formatos, e cada um pede um conserto diferente
O dado que chega do outro lado do cabo não chega "errado" de um jeito só. São quatro, e o professor escreve os quatro na lousa antes de qualquer código:
- campo faltando: o
JSONsaiu semumidade_pct. Não é problema de valor, é problema de forma. O corpo não é o que a rota prometeu. - campo vazio: a chave veio, com
""ounull. A forma está certa e o conteúdo não está. UmSELECTque soma esse campo devolveNULLe quebra a agregação do dia 6. - tipo errado:
temperatura_cchegou como texto,"quente". Compara string com número não dá erro em JavaScript: dáfalse, e a validação passa. Esse é o caso que derruba a turma. - valor absurdo: o número chegou, e é
900. O tipo está certo e o valor não é possível.
O que separa um grupo do outro é quem tem conserto. Campo faltando ou vazio tem conserto na placa (o JSON saiu mal montado). Tipo errado e valor absurdo têm conserto no sensor (o módulo devolveu aquilo). O status HTTP que a rota devolve diz qual dos dois problemas foi, e por isso o 400 existe separado do 422.
O item 2 é o mais enganoso dos quatro, e o professor escreve os dois tipos na lousa: campo faltando é undefined, campo vazio é string. O if (campo) do aluno passa nos dois casos, e o valor que entra no banco é "". A verificação é de tipo, não de presença.
O limite plausível não é o limite do sensor
O DHT11 tem uma faixa física, muito maior: a literatura do componente fala em cerca de -40 a 80 graus, e é isso que a placa usa de faixa. A escolha é dela, e o professor explica a diferença entre as duas:
| Faixa | Quem define | Para que serve |
|---|---|---|
| Faixa física do sensor | o fabricante | saber quando o componente está quebrado |
| Limite plausível | quem escreve a regra | decidir o que vale a pena gravar |
O termo em inglês para "valor que não faz sentido" é dado corrompido, e ele não é o mesmo que "dado quebrado". Dado corrompido é o dado que chegou inteiro e não presta: 900 graus, -5 de umidade, 140. O sensor está ligado, o cabo está bom, o JSON está bem formado, e mesmo assim o número é mentira. Nenhum desses é erro de transporte, e é por isso que a validação é a única defesa: na auditoria do WiFi não aparece nada, porque não houve nada a auditar.
Gravar -39,9 num DHT11 numa sala de aula não é erro do componente: é uma leitura que não describe a sala. Entrou no banco, e o dia 13 vai plotar -39,9 como se fosse temperatura de parede. Esse é o custo real de não validar: o gráfico mente, e mente com confiança.
O intervalo fica em constante, MIN_TEMP_C e MAX_TEMP_C, no topo do arquivo. A regra que muda de lugar a cada aula é a regra que alguém esquece de atualizar.
A palavra em inglês para essa faixa é range, e ela vale para os dois lados do cabo. Em Python e em JavaScript, range é o tipo que gera uma sequência de números, e o aluno vai usar range para percorrer as leituras do dia 8 e as séries de temperatura do dia 13. Aqui, range é outra coisa: é o limite plausível que o dado tem de respeitar, e o professor escreve as duas coisas na lousa porque o mesmo nome serve para as duas.
Quando o dado viola o range, a rota faz o rejeitar da leitura: o 422 com a lista de problemas, sem INSERT e sem linha na tabela. Rejeitar não é a mesma coisa que ignorar. Ignorar é quando o dado nem entra na fila de processamento; rejeitar é quando ele entra, é inspecionado, e a resposta volta dizendo por que ele não vira registro. O dia 9 aula 2 vai mostrar que a diferença é o que permite à placa decidir se tenta de novo ou se aceita que a leitura morreu.
A mesma regra nos dois lados, por um motivo operacional
Validar só na placa parece mais simples, e não é. O argumento que o professor escreve na lousa tem data: a placa pode receber firmware novo; o Node vai continuar recebendo dado de placa velha.
- Firmware velho gravado numa placa da turma anterior continua na rede até o professor lembrar de regravar.
- O servidor que recebe aquele dado é o mesmo que a turma vai usar, e o
INSERTé o mesmo. - A placa antiga não tem a regra nova, porque a regra nova nasceu depois que ela foi gravada.
Então a validação do Node não é redundância: é a única validação que vale para todo mundo, sempre. E a da placa existe por outro motivo, que é economia: se a placa descobre que a leitura é lixo antes de montar a requisição, ela não gasta socket, não gasta bateria e não ocupa o servidor com JSON que vai ser recusado. São duas justificativas diferentes, e o professor deixa claro qual é qual, porque o aluno que só_copy a validação do Node para a placa sem saber por quê vai removê-la na semana seguinte para "economizar tempo".
Resposta de erro é conteúdo, e recusar tem que ser visível
A rota que recusa devolve três coisas, e as três importam:
- o status, que decide se a placa tenta de novo (
500) ou aceita que o dado morreu (422), - a lista de problemas, que diz o que consertar e não só que errou,
- o identificador do dispositivo, que diz de qual placa é o problema.
O item do meio é o que o professor mais cobra. Um 422 sem lista de problemas é uma caixa preta: o aluno sabe que errou e não sabe o quê. Com a lista, ele sabe que o DHT11 devolveu 900 em vez de um número, e sabe que a culpa é do módulo, não do JSON.
E recusar precisa deixar rastro. A frase não gravar lixo tem duas metades: a tabela fica limpa, e existe uma linha de registro dizendo que chegou dado ruim, de qual dispositivo, com qual valor. A primeira metade se vê no SELECT; a segunda só aparece no dia 8 aula 2, e é a que transforma "perdemos seis leituras" em "a estacao-02 mandou -5 de umidade às 10:00:30".
Atividade
Montagem:
DHT11noGPIO4, com resistor de pull-up de 4,7 k ohm entre o pino de dados e3V3, como no dia 8 do 1o trimestre.- Servidor Node do dia 5 aula 2 rodando, com a tabela
tb_t2_leiturado dia 4.
- Grave na placa o sketch da resolução e abra o monitor serial a 115200. Anote, para cada um dos cinco casos, o status que a placa imprimiu e o motivo que ela nomeou.
- Rode o script do Node com as nove leituras da resolução. Preencha no caderno uma tabela com quatro colunas: caso, status devolvido, lista de problemas, entrou no banco ou não.
- Para cada uma das quatro formas de sujeira da entrada, escreva em uma frase onde o conserto acontece: na placa, no sensor ou no
JSON. - Escolha dois dos nove casos e troque a regra: faça o
LIMITEde temperatura caber de 5 a 60 e rode de novo. Anote o que mudou no status e na contagem de linhas gravadas. - Mande um
curlcom a chave de API correta e umtemperatura_cde900. Confirme o status e depois rode oSELECTpara mostrar que a linha não existe. - Mande o mesmo
curlsem a chave de API. Qual status volta agora, e por que é um número diferente do item 5? - Escreva, em uma frase, o que muda no banco se a validação for removida do Node e ficar só na placa.
Nota: 12 pontos. Critério de fim: a tabela do item 2 preenchida nas nove linhas, e o SELECT do item 5 mostrando que a leitura de 900 graus não virou linha.
Resolucao
Do lado do Node, a regra roda antes do INSERT e devolve a decisão, não o INSERT. Este script foi executado nesta máquina, contra MariaDB 10.11, no schema materiais_teste:
// dia 8 aula 1, lado do Node: a MESMA regra da placa, correndo antes do INSERT.
const mysql = require('mysql2/promise');
const LIMITE = {
temperatura_c: { min: -40, max: 80 },
umidade_pct: { min: 0, max: 100 },
};
const OBRIGATORIO = ['id_dispositivo', 'temperatura_c', 'umidade_pct'];
// Devolve { ok, status, problemas }. Zero problemas e gravavel.
function validarLeitura(entrada) {
const problemas = [];
for (const campo of OBRIGATORIO) {
if (!(campo in entrada)) problemas.push(${campo}: campo ausente);
else if (entrada[campo] === '' || entrada[campo] === null) {
problemas.push(${campo}: campo vazio);
}
}
for (const [campo, faixa] of Object.entries(LIMITE)) {
if (!(campo in entrada) || entrada[campo] === '' || entrada[campo] === null) continue;
if (typeof entrada[campo] !== 'number' || !Number.isFinite(entrada[campo])) {
problemas.push(${campo}: tipo errado, ${typeof entrada[campo]} "${entrada[campo]}");
continue;
}
if (entrada[campo] < faixa.min || entrada[campo] > faixa.max) {
problemas.push(${campo}: ${entrada[campo]} fora de ${faixa.min} a ${faixa.max});
}
}
if (problemas.length === 0) return { ok: true, status: 201, problemas };
// Campo ausente ou vazio e 400: o corpo nem e o que a rota promete.
// Campo presente e invalido e 422: a rota prometeu, veio errado.
const status = problemas.some((p) => p.includes('ausente') || p.includes('vazio')) ? 400 : 422;
return { ok: false, status, problemas };
}
const CHEGARAM = [
{ id_dispositivo: 'estacao-01', temperatura_c: 27.4, umidade_pct: 61, dt_leitura: '2026-09-30 10:00:00' },
{ id_dispositivo: 'estacao-01', temperatura_c: 34.8, umidade_pct: 55, dt_leitura: '2026-09-30 10:00:10' },
{ id_dispositivo: 'estacao-02', temperatura_c: 900, umidade_pct: 61, dt_leitura: '2026-09-30 10:00:20' },
{ id_dispositivo: 'estacao-02', temperatura_c: 27.0, umidade_pct: -5, dt_leitura: '2026-09-30 10:00:30' },
{ id_dispositivo: 'estacao-02', temperatura_c: 27.0, umidade_pct: 140, dt_leitura: '2026-09-30 10:00:40' },
{ id_dispositivo: 'estacao-02', temperatura_c: 'quente', umidade_pct: 61, dt_leitura: '2026-09-30 10:00:50' },
{ id_dispositivo: 'estacao-02', temperatura_c: 27.0, dt_leitura: '2026-09-30 10:01:00' },
{ id_dispositivo: 'estacao-02', temperatura_c: 27.0, umidade_pct: '', dt_leitura: '2026-09-30 10:01:10' },
{ id_dispositivo: 'estacao-02', temperatura_c: 28.1, umidade_pct: 58, dt_leitura: '2026-09-30 10:01:20' },
];
(async () => {
const pool = mysql.createPool({
host: process.env.DB_HOST, port: Number(process.env.DB_PORT),
user: process.env.DB_USER, password: process.env.DB_PASS,
database: process.env.DB_NAME, dateStrings: true,
});
// TRUNCATE e nao DELETE de proposito: o DELETE esvazia a tabela mas NAO
// zera o AUTO_INCREMENT, entao o id da proxima gravacao continua crescendo
// a cada execucao. Isso e o detalhe que a aula 2 do dia 12 usa para explicar
// por que um teste que espera insertId === 1 passa na primeira e falha na
// segunda. Aqui e a tabela de demonstracao, entao tanto faz.
await pool.query('TRUNCATE TABLE tb_t2_leitura');
let gravadas = 0, recusadas = 0;
for (const entrada of CHEGARAM) {
const v = validarLeitura(entrada);
const resumo = temp=${entrada.temperatura_c} umidade=${'umidade_pct' in entrada ? entrada.umidade_pct : '(sem campo)'};
if (v.ok) {
const [r] = await pool.execute(
INSERT INTO tb_t2_leitura (id_dispositivo, temperatura_c, umidade_pct, dt_leitura)
VALUES (?, ?, ?, ?),
[entrada.id_dispositivo, entrada.temperatura_c, entrada.umidade_pct, entrada.dt_leitura]);
gravadas++;
console.log(201 gravado | ${resumo} | id=${r.insertId});
} else {
recusadas++;
console.log(${v.status} recusado | ${resumo});
for (const p of v.problemas) console.log( -> ${p});
}
}
const [contagem] = await pool.query('SELECT COUNT(*) AS total FROM tb_t2_leitura');
console.log('');
console.log(chegaram ${CHEGARAM.length} | gravadas ${gravadas} | recusadas ${recusadas} +
| linhas na tabela ${contagem[0].total});
console.log('A contagem do INSERT e a contagem do SELECT batem: nada entrou sem passar na regra.');
const [porDispositivo] = await pool.query(
SELECT id_dispositivo, COUNT(*) AS leituras, MIN(temperatura_c) AS minima,
MAX(temperatura_c) AS maxima
FROM tb_t2_leitura
GROUP BY id_dispositivo ORDER BY id_dispositivo);
for (const l of porDispositivo) {
console.log( ${l.id_dispositivo}: ${l.leituras} leituras, de ${l.minima} a ${l.maxima} C);
}
console.log('');
console.log('estacao-02 mandou 6 das 9 leituras e gravei 1. A regra fez o filtro.');
console.log('Sem validacao, o banco teria 900, -5, 140, "quente" e dois vazios.');
console.log('Qualidade do dado do sistema: 3 de 9. E esse numero que se mede,');
console.log('nao a quantidade de leituras que a placa tentou enviar.');
await pool.end();
})();
A saída real, medida nesta máquina:
201 gravado | temp=27.4 umidade=61 | id=1 201 gravado | temp=34.8 umidade=55 | id=2 422 recusado | temp=900 umidade=61 -> temperatura_c: 900 fora de -40 a 80 422 recusado | temp=27 umidade=-5 -> umidade_pct: -5 fora de 0 a 100 422 recusado | temp=27 umidade=140 -> umidade_pct: 140 fora de 0 a 100 422 recusado | temp=quente umidade=61 -> temperatura_c: tipo errado, string "quente" 400 recusado | temp=27 umidade=(sem campo) -> umidade_pct: campo ausente 400 recusado | temp=27 umidade= -> umidade_pct: campo vazio 201 gravado | temp=28.1 umidade=58 | id=3 chegaram 9 | gravadas 3 | recusadas 6 | linhas na tabela 3 A contagem do INSERT e a contagem do SELECT batem: nada entrou sem passar na regra. estacao-01: 2 leituras, de 27.40 a 34.80 C estacao-02: 1 leituras, de 28.10 a 28.10 C estacao-02 mandou 6 das 9 leituras e gravei 1. A regra fez o filtro. Sem validacao, o banco teria 900, -5, 140, "quente" e dois vazios.
O sketch da placa, que é o lado esquerdo do mesmo cabo:
// dia 8, aula 1: a placa valida o dado ANTES de mandar. // // Por que a validaao esta aqui e nao so no Node: se a placa descobre que a // leitura e lixo, ela nem gasta a requisicao. E o professor mostra na lousa // que a MESMA regra roda dos dois lados — porque a placa pode mudar o // firmware, mas o Node vai continuar recebendo dado de placa velha. #include <Arduino.h> #include <DHT.h> // Pinos e tipos em constante no topo: e o que o criterio de correcao exige. const int PIN_DHT = 4; const uint8_t TIPO_SENSOR = DHT11; // Faixa plausivel para sensor de ambiente em sala de aula. Nao e o limite // fisico do DHT11: e o limite do QUE FAZ SENTIDO grav. E por isso que o // intervalo fica em constante, e nao um numero magico dentro do if. const float MIN_TEMP_C = -40.0; const float MAX_TEMP_C = 80.0; const float MIN_UMID_PCT = 0.0; const float MAX_UMID_PCT = 100.0; // Uma leitura simulada. A placa percorre estas cinco para mostrar que a // validacao pega as quatro formas de dado sujo que chegam do sensor. struct Leitura { const char* rotulo; float temperatura_c; float umidade_pct; bool mandouUmidade; // false: o JSON saiu sem o campo umidade_pct bool mandouTexto; // true: o JSON saiu com temperatura_c entre aspas }; DHT sensor(PIN_DHT, TIPO_SENSOR); // Monta o JSON que realmente vai no cabo. E este texto, e nao a struct, que // chega no Node — por isso o JSON e montado antes da validacao. String montarPayload(const Leitura& l) { String json = "{\"id_dispositivo\":\"estacao-01\""; if (l.mandouTexto) { // Sensor que devolveu texto em vez de numero: o JSON sai com aspas. json += ",\"temperatura_c\":\"quente\""; } else { json += ",\"temperatura_c\":"; json += l.temperatura_c; } if (l.mandouUmidade) { json += ",\"umidade_pct\":"; json += l.umidade_pct; } json += "}"; return json; } // Mesma regra que o Node roda no dia 8 aula 1. Retorna quantos problemas // achou: zero e leitura aceita. int validarLocal(const Leitura& l) { int problemas = 0; if (l.mandouTexto) { problemas++; // temperatura_c chegou como texto, nao como numero } else if (isnan(l.temperatura_c) || l.temperatura_c < MIN_TEMP_C || l.temperatura_c > MAX_TEMP_C) { problemas++; // fora da faixa plausivel: e o 900 graus do sensor quebrado } if (!l.mandouUmidade) { problemas++; // campo obrigatorio ausente } else if (l.umidade_pct < MIN_UMID_PCT || l.umidade_pct > MAX_UMID_PCT) { problemas++; // umidade negativa ou acima de 100 } return problemas; } void mostrarCaso(const Leitura& l) { int problemas = validarLocal(l); Serial.print(problemas == 0 ? "201 gravado | " : "422 recusado | "); Serial.print(l.rotulo); Serial.println(); Serial.print(" envio : "); Serial.println(montarPayload(l)); if (problemas == 0) { Serial.println(" regra : dentro da faixa, vai para o banco"); return; } // Nomeia cada problema. "Falhou" sozinho nao ajuda ninguem a consertar; // "temperatura_c fora de -40 a 80: 900" diz o que corrigir. if (l.mandouTexto) { Serial.println(" -> temperatura_c nao e numero"); } else if (isnan(l.temperatura_c) || l.temperatura_c < MIN_TEMP_C || l.temperatura_c > MAX_TEMP_C) { Serial.print(" -> temperatura_c fora de "); Serial.print(MIN_TEMP_C, 0); Serial.print(" a "); Serial.print(MAX_TEMP_C, 0); Serial.print(": "); Serial.println(l.temperatura_c); } if (!l.mandouUmidade) { Serial.println(" -> falta o campo umidade_pct"); } else if (l.umidade_pct < MIN_UMID_PCT || l.umidade_pct > MAX_UMID_PCT) { Serial.println(" -> umidade_pct fora de 0 a 100"); } } void setup() { Serial.begin(115200); Serial.println(); Serial.println("dia 8 aula 1 — validar antes de gravar"); Serial.println("-----------------------------------"); sensor.begin(); Serial.println("DHT11 no GPIO" + String(PIN_DHT) + " iniciado."); // Leitura real da bancada. Se o sensor nao esta na protoboard, o professor // mostra que o codigo trata o NaN em vez de mandar NaN para o Node. float t = sensor.readTemperature(); float u = sensor.readHumidity(); bool sensorPresente = !isnan(t) && !isnan(u); if (sensorPresente) { Serial.print("leitura da bancada: "); Serial.print(t, 1); Serial.print(" C / "); Serial.print(u, 0); Serial.println(" %"); } else { Serial.println("DHT11 nao respondeu (NaN). Usando o valor da aula: 27,4 C / 61 %."); } Leitura casos[5] = { {"leitura boa", sensorPresente ? t : 27.4, sensorPresente ? u : 61.0, true, false}, {"sensor falhou", 900.0, 61.0, true, false}, {"campo faltando", sensorPresente ? t : 27.4, 61.0, false, false}, {"tipo errado", 0.0, 61.0, true, true }, {"umidade vazia", sensorPresente ? t : 27.4, -5.0, true, false}, }; int aceitas = 0; for (int i = 0; i < 5; i++) { mostrarCaso(casos[i]); if (validarLocal(casos[i]) == 0) aceitas++; } Serial.println(); Serial.print("aceitas: "); Serial.print(aceitas); Serial.println(" de 5 — as outras quatro nao viram linha no banco"); Serial.println("o Node roda a MESMA regra de novo, do outro lado do cabo."); } void loop() { Serial.println(); Serial.println("--- nova rodada ---"); setup(); delay(10000); }
Por que assim e não de outro jeito. A placa monta o JSON antes de validar. A ordem parece invertida, e o comentário do arquivo diz por quê: quem decide a validade é o texto que vai no cabo, não a struct que o gerou. Um struct sempre tem os campos porque é tipada; uma String pode sair sem eles, e é a String que o Node recebe. Validar a struct e validar o que vai no cabo são coisas diferentes, e só a segunda protege o banco.
O isnan aparece duas vezes no mesmo if: na validação e na impressão do motivo. Sem ele, o caso do sensor ausente imprime temperatura_c fora de -40 a 80: nan, que é uma mensagem verdadeira e inútil. Com ele, o professor ganha o caso "sensor devolveu NaN" separado, que é o quarto formato de sujeira: o DHT11 sem resposta não devolve zero, devolve NaN, e NaN passa por qualquer comparação de faixa porque toda comparação com NaN dá falso. Esse é o bug silencioso que o dia 8 aula 2 vai nomear.
A placa classifica os quatro casos sujos como 422, e o Node separa 400 e 422. A diferença é deliberada e o professor assume na frente da turma: a placa não sabe se o corpo chegou completo, porque quem montou o corpo foi ela mesma. Ela sabe que montou mal, e isso é 422 do ponto de vista dela. Quem tem o corpo inteiro na mão e pode distinguir "veio sem a chave" de "veio com a chave e o valor errado" é o Node, e é ele que responde 400.
O TRUNCATE do script é diferente do DELETE do dia 7 aula 1, e a diferença é o que separa a aula 1 do dia 12 da aula 2. DELETE esvazia a tabela e deixa o AUTO_INCREMENT onde estava; TRUNCATE esvazia e zera. Por isso o id da primeira gravação da resolução é sempre 1, e o professor pode mandar o aluno rodar duas vezes e ver o mesmo número nas duas. É o efeito colateral que a aula 2 do dia 12 usa contra o teste que espera insertId === 1.
Criterios de correcao
| Critério | Pontos |
|---|---|
| Tabela do item 2 preenchida nas nove linhas, com status, lista de problemas e entrada ou não no banco | 3 pontos |
Os quatro formatos de sujeira nomeados, e o local do conserto (placa, sensor ou JSON) escrito para cada um | 2 pontos |
DHT11 no GPIO4 com pull-up, e MIN_TEMP_C e MAX_TEMP_C declarados em const no topo do arquivo | 2 pontos |
Item 4: o LIMIT estreitado e as duas contagens de linhas gravadas, antes e depois | 2 pontos |
Item 5: 422 no curl com 900 graus e SELECT confirmando que a linha não existe | 2 pontos |
Item 6: o 401 identificado como número diferente do 422, e a razão | 1 pontos |
Erros comuns
| Erro | Como aparece | Correção |
|---|---|---|
Validar depois do INSERT | "Inseri e depois vi que estava errado" | "A validação vem antes do INSERT. Depois de gravar, a linha já existe e o DELETE dela é remendo." |
| Comparar string com número sem avisar | "A validação passou e gravou a palavra quente" | "Em JavaScript, 'quente' < 80 dá false e o if não entra. Confira o tipo com typeof antes de comparar o valor." |
Confundir NaN com zero | "O sensor falhou e gravou 0" | "O DHT11 devolve NaN, e NaN passa por toda comparação de faixa. Teste com Number.isFinite ou isnan antes." |
| Usar o limite físico como limite plausível | "A sala nunca chega a 80, então nunca vai recusar" | "O limite plausível é menor que o do sensor. Trinta e dois graus numa sala de aula é dado demais, e o dia 13 usa isso como alerta." |
| Um único status para todo erro | "Tudo deu 422, não sei o que arrumar" | "Campo ausente ou vazio é 400: o corpo não é o que a rota promete. Valor errado é 422. São problemas de conserto diferente." |
| Validar só na placa | "Na minha placa nunca entrou lixo" | "A placa pode receber firmware velho, e o Node continua recebendo dado dela. A regra do Node vale para todas as placas, inclusive as que você não controla." |
| Recusar sem dizer qual campo | "O servidor disse 422 e eu não sei onde mexer" | "A resposta de erro tem que dizer o campo e o valor. Sem lista de problemas, o 422 é uma caixa preta." |
| Recusar e não registrar | "Perdi seis leituras e não sei quais foram" | "Cada recusa precisa de uma linha de log com o identificador do dispositivo. É o que o dia 8 aula 2 monta." |
Desafio extra
Escreva a função que devolve dois problemas em vez de um, e que aceite uma lista de leituras e devolve as que passaram, com o número de recusa por motivo. Depois rode a mesma lista com a regra duplicada (uma vez na placa, uma no Node) e conte quantas leituras são recusadas duas vezes — as que a placa já recusou nem chegam ao Node, então a resposta correta é zero. Se der diferente de zero, você tem duas regras que discordam, e descobre agora, antes do dia 12 escrever o teste que falha sozinho.
>A resolucao, compilada
// dia 8, aula 1: a placa valida o dado ANTES de mandar. // // Por que a validaao esta aqui e nao so no Node: se a placa descobre que a // leitura e lixo, ela nem gasta a requisicao. E o professor mostra na lousa // que a MESMA regra roda dos dois lados — porque a placa pode mudar o // firmware, mas o Node vai continuar recebendo dado de placa velha. #include <Arduino.h> #include <DHT.h> // Pinos e tipos em constante no topo: e o que o criterio de correcao exige. const int PIN_DHT = 4; const uint8_t TIPO_SENSOR = DHT11; // Faixa plausivel para sensor de ambiente em sala de aula. Nao e o limite // fisico do DHT11: e o limite do QUE FAZ SENTIDO grav. E por isso que o // intervalo fica em constante, e nao um numero magico dentro do if. const float MIN_TEMP_C = -40.0; const float MAX_TEMP_C = 80.0; const float MIN_UMID_PCT = 0.0; const float MAX_UMID_PCT = 100.0; // Uma leitura simulada. A placa percorre estas cinco para mostrar que a // validacao pega as quatro formas de dado sujo que chegam do sensor. struct Leitura { const char* rotulo; float temperatura_c; float umidade_pct; bool mandouUmidade; // false: o JSON saiu sem o campo umidade_pct bool mandouTexto; // true: o JSON saiu com temperatura_c entre aspas }; DHT sensor(PIN_DHT, TIPO_SENSOR); // Monta o JSON que realmente vai no cabo. E este texto, e nao a struct, que // chega no Node — por isso o JSON e montado antes da validacao. String montarPayload(const Leitura& l) { String json = "{\"id_dispositivo\":\"estacao-01\""; if (l.mandouTexto) { // Sensor que devolveu texto em vez de numero: o JSON sai com aspas. json += ",\"temperatura_c\":\"quente\""; } else { json += ",\"temperatura_c\":"; json += l.temperatura_c; } if (l.mandouUmidade) { json += ",\"umidade_pct\":"; json += l.umidade_pct; } json += "}"; return json; } // Mesma regra que o Node roda no dia 8 aula 1. Retorna quantos problemas // achou: zero e leitura aceita. int validarLocal(const Leitura& l) { int problemas = 0; if (l.mandouTexto) { problemas++; // temperatura_c chegou como texto, nao como numero } else if (isnan(l.temperatura_c) || l.temperatura_c < MIN_TEMP_C || l.temperatura_c > MAX_TEMP_C) { problemas++; // fora da faixa plausivel: e o 900 graus do sensor quebrado } if (!l.mandouUmidade) { problemas++; // campo obrigatorio ausente } else if (l.umidade_pct < MIN_UMID_PCT || l.umidade_pct > MAX_UMID_PCT) { problemas++; // umidade negativa ou acima de 100 } return problemas; } void mostrarCaso(const Leitura& l) { int problemas = validarLocal(l); Serial.print(problemas == 0 ? "201 gravado | " : "422 recusado | "); Serial.print(l.rotulo); Serial.println(); Serial.print(" envio : "); Serial.println(montarPayload(l)); if (problemas == 0) { Serial.println(" regra : dentro da faixa, vai para o banco"); return; } // Nomeia cada problema. "Falhou" sozinho nao ajuda ninguem a consertar; // "temperatura_c fora de -40 a 80: 900" diz o que corrigir. if (l.mandouTexto) { Serial.println(" -> temperatura_c nao e numero"); } else if (isnan(l.temperatura_c) || l.temperatura_c < MIN_TEMP_C || l.temperatura_c > MAX_TEMP_C) { Serial.print(" -> temperatura_c fora de "); Serial.print(MIN_TEMP_C, 0); Serial.print(" a "); Serial.print(MAX_TEMP_C, 0); Serial.print(": "); Serial.println(l.temperatura_c); } if (!l.mandouUmidade) { Serial.println(" -> falta o campo umidade_pct"); } else if (l.umidade_pct < MIN_UMID_PCT || l.umidade_pct > MAX_UMID_PCT) { Serial.println(" -> umidade_pct fora de 0 a 100"); } } void setup() { Serial.begin(115200); Serial.println(); Serial.println("dia 8 aula 1 — validar antes de gravar"); Serial.println("-----------------------------------"); sensor.begin(); Serial.println("DHT11 no GPIO" + String(PIN_DHT) + " iniciado."); // Leitura real da bancada. Se o sensor nao esta na protoboard, o professor // mostra que o codigo trata o NaN em vez de mandar NaN para o Node. float t = sensor.readTemperature(); float u = sensor.readHumidity(); bool sensorPresente = !isnan(t) && !isnan(u); if (sensorPresente) { Serial.print("leitura da bancada: "); Serial.print(t, 1); Serial.print(" C / "); Serial.print(u, 0); Serial.println(" %"); } else { Serial.println("DHT11 nao respondeu (NaN). Usando o valor da aula: 27,4 C / 61 %."); } Leitura casos[5] = { {"leitura boa", sensorPresente ? t : 27.4, sensorPresente ? u : 61.0, true, false}, {"sensor falhou", 900.0, 61.0, true, false}, {"campo faltando", sensorPresente ? t : 27.4, 61.0, false, false}, {"tipo errado", 0.0, 61.0, true, true }, {"umidade vazia", sensorPresente ? t : 27.4, -5.0, true, false}, }; int aceitas = 0; for (int i = 0; i < 5; i++) { mostrarCaso(casos[i]); if (validarLocal(casos[i]) == 0) aceitas++; } Serial.println(); Serial.print("aceitas: "); Serial.print(aceitas); Serial.println(" de 5 — as outras quatro nao viram linha no banco"); Serial.println("o Node roda a MESMA regra de novo, do outro lado do cabo."); } void loop() { Serial.println(); Serial.println("--- nova rodada ---"); setup(); delay(10000); }
Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia08 aula1.
Aula 2 — Tratamento de erro e log que responde pergunta
Objetivos
- Escrever o tratamento de erro de uma rota com
try,catchefinally, e dizer o que cada bloco garante. - Dar a mesma estrutura de cinco campos a toda linha de log, de forma que a linha trezentas tenha o mesmo formato da linha três.
- Escolher o nível de cada registro (
info,warn,error) pela consequência do evento, e não pela emoção de quem escreve. - Achar um erro no log filtrando pelo identificador, sem varrer as três centenas de linha na tela.
- Comparar uma rota que engole o erro com uma que registra e relança, e apontar o que o painel mostra em cada caso.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1 protoboard de 830 pontos e 1 jogo de jumpers por dupla
- 1 computador com Node.js 20,
mysql2instalado e MySQL ou MariaDB 10.11 - Tabela
tb_t2_logcriada comnivel,codigo,mensagem,id_dispositivoeid_requisicao - Folha de papel por dupla, para desenhar a assinatura de cinco campos do log
- Projetor, para o terminal do Node e o monitor serial da placa juntos
Conceitos
O que o tratamento de erro garante, bloco por bloco
O try sozinho não trata nada: ele apenas marca a região onde o erro pode acontecer. Quem trata é o catch, e o que garante a limpeza é o finally. O professor escreve os três na lousa com uma pergunta para cada um:
| Bloco | Pergunta que ele responde | O que acontece se você esquecer |
|---|---|---|
try | onde o erro pode acontecer? | o erro sobe e derruba o processo |
catch | o que fazer com o erro que chegou? | o processo morre; no dia 3 aula 2 isso é o "não travar" |
finally | o que fazer nos dois casos? | a conexão fica aberta, a linha de fechamento não sai |
O catch tem duas saídas possíveis, e a escolha é a aula inteira. Pode engolir o erro: registrar nada, devolver sucesso, seguir como se nada tivesse acontecido. Pode fazer o lançar erro, com throw, depois de registrar. A segunda é a que interessa, e a razão é simples: quem chamou a função precisa saber que ela falhou, para poder devolver 500 em vez de 201.
O erro inesperado é o que a linguagem não previa: o banco fora do ar, a coluna que alguém renomeou, a conexão que cai no meio do INSERT. O erro esperado é o 422 do dia 8 aula 1, que é regra de negócio e não vai para o catch. Separar os dois é o que impede que o catch do banco engula a validação: se os dois são tratados igual, a validação vira 500 e a placa tenta de novo para sempre, gravando lixo em cada tentativa.
O finally roda sempre, com sucesso ou com falha. É nele que fecha a conexão, libera o lock e grava a linha de encerramento do pedido. E é o lugar onde a esquisitidão aparece: o finally sozinho não sabe se o pedido deu certo, porque não tem acesso ao que aconteceu no try. Aqui ele grava com nível warn, e a leitura é "este pedido teve alguma coisa fora do normal". É o jeito barato de achar os pedidos que merecem investigação sem guardar uma flag no meio do caminho.
A assinatura de cinco campos, e por que ela não varia
O termo em inglês para tudo isso é logging, e o termo para a linha que você escreve é o log. Um sistema tem logging quando ele registra o que aconteceu, mesmo quando o resultado é sucesso. O que a aula separa é o erro em log (o erro registrado) do erro no painel (o erro que o usuário vê), e o que torna o primeiro útil é que ele responde a pergunta que o segundo não responde.
Todo o resto desta seção é construir o log com identificador: as cinco linhas que o filtro percorre, e o número que amarra cada grupo delas. A montagem é o log com contexto quando o campo de contexto é o do evento, e não o do pedido inteiro: a diferença entre escrever erro de banco e escrever erro de banco na coluna umidade_pct, valor 61, na placa estacao-01 é a diferença entre um log que decora e um log que prova.
Toda linha de log tem os mesmos cinco campos, sempre, sempre na mesma ordem:
- quando — o instante do evento
- que nível —
info,warnouerror - qual código — o nome curto do tipo de evento
- a mensagem — o que aconteceu, em português
- o contexto — os valores daquele evento específico
Uma função só de escrever log é o que garante isso. A regra é curta: nunca concatenar log à mão. Se o aluno escreve console.log('gravou ' + id) num lugar e console.log('erro em ' + tabela) em outro, ele não tem log, tem dois formatos diferentes que nenhum filtro aceita. O professor conta essa como a causa número um de "não achei o erro".
O nível de log responde a uma pergunta de consequência, e o professor dá as três definições:
| Nível | Quando usar | O que o professor faz com a linha |
|---|---|---|
info | o sistema funcionou como esperado | ignora |
warn | funcionou, e tem alguma coisa errada do lado | olha quando o número não bate |
error | o pedido falhou | procura por id_requisicao |
O erro de lógica da turma é pôr tudo como error, que transforma o filtro de nível em filter de wall e não acha nada. E o oposto: pôr tudo como info, que é mais barato de guardar e inútil quando quebra. A saída real da aula mede os três e mostra que info domina.
O identificador é a coluna que faz o log responder pergunta
O id_requisicao amarra as linhas do mesmo pedido. Sem ele, um E_BANCO e um E_OK ficam em linhas separadas e ninguém sabe se pertencem à mesma leitura. Com ele, a pergunta muda de forma: não é mais "o que deu errado", é achar o erro pelo identificador, e a consulta é esta: me mostra tudo do req00004.
E o id_requisicao não é decoração. Ele aparece na tabela de log como coluna indexada, e a consulta da aula é a mais barata do curso:
SELECT id_requisicao, nivel, codigo, mensagem, id_dispositivo
FROM tb_t2_log
WHERE id_requisicao = ?
ORDER BY id
O stack trace entra na mensagem do error como o número de linhas, não a pilha inteira. A pilha completa tem nomes de função que mudam a cada refatoração, e ninguém vai colar uma pilha de dez quadros em uma coluna de 160 caracteres. O que resolve o caso da aula é a mensagem do erro, que é estável: ER_DATA_TOO_LONG continua dizendo a mesma coisa depois de você mover a função de arquivo.
Engolir o erro é a falha que não aparece
Um catch que registra nada e devolve sucesso é o erro silencioso mais caro do curso, e o professor demonstra com a rota em vez de descrever. O sintoma éımples: a placa recebe 201, o painel fica verde, a contagem de leituras do painel e a contagem de leituras da placa divergem, e ninguém tem onde olhar.
O que torna o erro silencioso grave não é a falta de registro: é que ele se disfarça de sucesso. Uma exceção que derruba o processo é ruído, e ruído se conserta. Uma exceção engoleada é silêncio, e silêncio se confunde com saúde.
Por isso a regra é não engolir erro: o catch registra e relança. Quem chamou decide o status, e o status é o que a placa lê para decidir se tenta de novo.
O teste que o professor dá para reconhecer um log que engole: pegue um error conhecido, rode o código e procure a linha no log. Se o log não responder, o catch está vazio — por mais que ele exista no arquivo.
Rastreabilidade é os dois lados contando a mesma coisa
Rastreabilidade é a capacidade de seguir um evento pelos dois lados do cabo e ver que ele é o mesmo. Ela se prova com um número: a placa conta perdidas=1, o servidor conta um error, e os dois batem.
Se não batem, ou o log de um dos lados não está registrando, ou os dois estão contando coisas diferentes. Esse é o teste mais barato que existe e o único que pega erro de contagem: o SELECT de contagem total, comparado com o número que a placa imprime no serial. O dia 15 aula 1 vai usar exatamente essa conta para fechar o trimestre.
Atividade
Montagem:
DHT11noGPIO4, com resistor de pull-up de 4,7 k ohm entre o pino de dados e3V3.- Servidor Node do dia 5 aula 2 rodando, com as tabelas
tb_t2_leituraetb_t2_log.
- Grave na placa o sketch da resolução e abra o monitor serial a 115200. Anote o número de sequência
#de cada linha de log e o código de cada caso. - Escreva a assinatura de cinco campos do log no caderno, com o nome de cada campo e um exemplo de valor para cada um.
- Rode o script do Node. Preencha no caderno uma tabela com quatro linhas: caso, o que o painel mostra, o que o log tem, e o que você descobre quando vai procurar.
- Escolha um dos quatro casos do sketch da placa e escreva a linha de log que essa falha deveria ter gerado, com código de erro próprio. Depois confira: essa linha existe no log da sua placa? Se não existe, diga qual bloco do tratamento engoliu.
- No caso
[3]do sketch, o código engole o erro. Escreva, em duas linhas, o que você acrescentaria para que esse caso pare de ser invisível, e qual o custo em linhas de log de fazer isso em todo pedido. - Rode a consulta que filtra o log por
id_requisicaoe cole a saída no caderno. Quantas linhas tem? Elas são do mesmo pedido? - Adicione um quarto nível de log ao seu projeto, com nome e regra de uso próprios, e escreva em uma frase em que caso ele seria mais útil que
warn. - Grave a placa e rode o servidor, e confira: o número de leituras perdidas que a placa imprime bate com o número de
errorno log? Se não bate, qual dos dois lados está mentindo?
Nota: 12 pontos. Critério de fim: a saída do filtro por id_requisicao colada no caderno, e o número de error do servidor comparado com o número de perdidas da placa.
Resolucao
Do lado do Node, a mesma rota aparece duas vezes: uma que engole o erro e uma que registra e relança. Este script foi executado nesta máquina, contra MariaDB 10.11, no schema materiais_teste:
// dia 8 aula 2, lado do Node: um log que responde pergunta.
const mysql = require('mysql2/promise');
// ---------------------------------------------------------------------------
// NIVEL DE LOG: o numero que o professor filtra quando a tela tem 300 linhas.
// info = o sistema funcionando; warn = funcionando e algo da errado; error = o
// pedido falhou. Um log so com info e util so no dia em que nada quebra.
// ---------------------------------------------------------------------------
const NIVEL = { info: 0, warn: 1, error: 2 };
const NOME_DO_NIVEL = Object.fromEntries(Object.entries(NIVEL).map(([k, v]) => [v, k]));
// ---------------------------------------------------------------------------
// ID DE REQUISICAO: o numero que amarra as linhas do MESMO pedido.
// Sem ele, um error do banco e um info da gravacao ficam em linhas
// separadas do log, e ninguem consegue dizer se pertencem a mesma leitura.
// ---------------------------------------------------------------------------
let contador = 0;
function novoIdRequisicao() {
contador++;
return 'req' + String(contador).padStart(5, '0');
}
// A ASSINATURA do log e sempre a mesma: quem, quando, que nivel, qual codigo,
// a mensagem, e o contexto. Cinco campos, sempre os cinco, sempre na ordem.
async function registrar(pool, { nivel, codigo, mensagem, id_dispositivo, id_requisicao, contexto }) {
await pool.execute(
INSERT INTO tb_t2_log (dt_evento, nivel, codigo, mensagem, id_dispositivo, id_requisicao)
VALUES (NOW(3), ?, ?, ?, ?, ?),
[nivel, codigo, mensagem.slice(0, 160), id_dispositivo ?? null, id_requisicao ?? null]);
return { nivel, codigo, mensagem, id_requisicao, contexto };
}
// Um log que ENGOLE o erro: e o que a aula quer mostrar. O catch existe,
// o error nao aparece, e o pedido responde 201 como se nada tivesse dado
// errado. O professor conta quantas leituras sumiram sem ninguem saber.
async function postComErroEngolido(pool, entrada) {
try {
const id_requisicao = novoIdRequisicao();
await registrar(pool, { nivel: 'info', codigo: 'E_INICIO', mensagem: 'pedido recebido',
id_dispositivo: entrada.id_dispositivo, id_requisicao, contexto: '' });
if (entrada.id_dispositivo === 'estacao-02') {
throw new Error('ER_DATA_TOO_LONG: dados muito longos para a coluna');
}
const [r] = await pool.execute(
INSERT INTO tb_t2_leitura (id_dispositivo, temperatura_c, umidade_pct, dt_leitura)
VALUES (?, ?, ?, NOW()),
[entrada.id_dispositivo, entrada.temperatura_c, entrada.umidade_pct]);
await registrar(pool, { nivel: 'info', codigo: 'E_OK', mensagem: 'leitura gravada',
id_dispositivo: entrada.id_dispositivo, id_requisicao, contexto: id=${r.insertId} });
return { status: 201, id_requisicao };
} catch (e) {
// O catch existe e nao registra. Este e o erro silencioso: o codigo
// "segue como se nada tivesse acontecido", e a leitura e perdida sem
// deixar registro. So o contador de leituras da placa denuncia.
return { status: 201, id_requisicao: '(nenhum)' };
}
}
// A mesma rota, com try/catch/finally que de verdade registra e propaga.
async function postComLogCompleto(pool, entrada) {
const id_requisicao = novoIdRequisicao();
try {
await registrar(pool, { nivel: 'info', codigo: 'E_INICIO', mensagem: 'pedido recebido',
id_dispositivo: entrada.id_dispositivo, id_requisicao,
contexto: temp=${entrada.temperatura_c} umidade=${entrada.umidade_pct} });
if (entrada.id_dispositivo === 'estacao-02') {
throw new Error('ER_DATA_TOO_LONG: dados muito longos para a coluna');
}
const [r] = await pool.execute(
INSERT INTO tb_t2_leitura (id_dispositivo, temperatura_c, umidade_pct, dt_leitura)
VALUES (?, ?, ?, NOW()),
[entrada.id_dispositivo, entrada.temperatura_c, entrada.umidade_pct]);
await registrar(pool, { nivel: 'info', codigo: 'E_OK', mensagem: 'leitura gravada',
id_dispositivo: entrada.id_dispositivo, id_requisicao, contexto: id=${r.insertId} });
return { status: 201, id_requisicao };
} catch (e) {
await registrar(pool, { nivel: 'error', codigo: 'E_BANCO', mensagem: e.message,
id_dispositivo: entrada.id_dispositivo, id_requisicao,
contexto: stack=${e.stack.split('\n').length} linhas });
throw e; // quem chamou decide o status
} finally {
// O finally roda sempre, com sucesso ou com falha. E ele que garante
// a linha de fechamento do pedido, e o nivel warn diz o que o finally
// nao sabe: sozinho ele nao sabe se o pedido deu certo. Aqui ele e warn
// porque este finally so roda num pedido que pode falhar, e a leitura
// e: "este pedido teve alguma coisa fora do normal". E o jeito barato de
// achar os pedidos que precisam de investigacao sem guardar uma flag.
await registrar(pool, { nivel: 'warn', codigo: 'E_FIM', mensagem: 'pedido encerrado',
id_dispositivo: entrada.id_dispositivo, id_requisicao, contexto: '' });
}
}
(async () => {
const pool = mysql.createPool({
host: process.env.DB_HOST, port: Number(process.env.DB_PORT),
user: process.env.DB_USER, password: process.env.DB_PASS,
database: process.env.DB_NAME, dateStrings: true,
});
await pool.query('TRUNCATE TABLE tb_t2_log');
await pool.query('TRUNCATE TABLE tb_t2_leitura');
const LER = { id_dispositivo: 'estacao-01', temperatura_c: 27.4, umidade_pct: 61 };
const QUEBRADA = { id_dispositivo: 'estacao-02', temperatura_c: 28.1, umidade_pct: 58 };
// --- as deux versions, lado a lado
console.log('--- a rota que ENGOLE o erro ---');
const engolida = [];
engolida.push(await postComErroEngolido(pool, LER));
engolida.push(await postComErroEngolido(pool, QUEBRADA));
for (const r of engolida) console.log( respondeu ${r.status} (id_requisicao ${r.id_requisicao}));
const [sozinho] = await pool.query('SELECT COUNT(*) AS n FROM tb_t2_leitura WHERE id_dispositivo = ?',
[QUEBRADA.id_dispositivo]);
console.log( linhas da ${QUEBRADA.id_dispositivo} depois das duas respostas 201: ${sozinho[0].n});
console.log(' A placa recebeu 201 nas duas e nao tem nada no historico.');
console.log('');
console.log('--- a mesma rota, com log de verdade ---');
const completo = [];
completo.push(await postComLogCompleto(pool, LER));
try {
completo.push(await postComLogCompleto(pool, QUEBRADA));
} catch (e) {
completo.push({ status: 500, id_requisicao: 'req00004' });
console.log( respondeu ${500} (id_requisicao req00004));
}
// --- o que o painel mostra e o que o log responde
const [leituras] = await pool.query('SELECT COUNT(*) AS n FROM tb_t2_leitura');
console.log('');
console.log(leituras no banco depois das quatro respostas: ${leituras[0].n});
console.log('Todas da estacao-01. A estacao-02 respondeu 201 duas vezes e nao tem linha.');
// --- a consulta que responde a pergunta
console.log('');
console.log('--- "quando a estacao-02 perdeu a ultima leitura?" ---');
const [porDispositivo] = await pool.query(
SELECT id_dispositivo, MAX(dt_leitura) AS ultima, COUNT(*) AS linhas
FROM tb_t2_leitura GROUP BY id_dispositivo ORDER BY id_dispositivo);
for (const l of porDispositivo) {
console.log( ${l.id_dispositivo}: ultima em ${l.ultima}, ${l.linhas} linha(s));
}
console.log('A estacao-02 NAO tem linha: nao ha como descobrir pela tabela.');
console.log('So o log responde, e so se alguem tiver escrito nele.');
// --- filtrar pelo identificador
console.log('');
console.log('--- o log inteiro, filtrado por id_requisicao ---');
const [linhas] = await pool.query(
SELECT id_requisicao, nivel, codigo, mensagem, id_dispositivo
FROM tb_t2_log WHERE id_requisicao = ? ORDER BY id, ['req00004']);
for (const l of linhas) {
console.log( [${l.nivel.padEnd(5)}] ${l.codigo.padEnd(8)} ${l.id_dispositivo} | ${l.mensagem});
}
console.log('');
console.log('Tres linhas, um pedido. O E_BANCO diz o que e o E_FIM diz que acabou.');
// --- procurar pelo codigo
console.log('');
console.log('--- "quais pedidos falharam?" ---');
const [erros] = await pool.query(
SELECT codigo, COUNT(*) AS ocorrencias, COUNT(DISTINCT id_requisicao) AS pedidos
FROM tb_t2_log WHERE nivel = 'error' GROUP BY codigo);
for (const e of erros) {
console.log( ${e.codigo}: ${e.ocorrencias} linha(s) em ${e.pedidos} pedido(s));
}
// --- nivel de log conta
console.log('');
console.log('--- o que cada nivel custa ---');
const [niveis] = await pool.query(
'SELECT nivel, COUNT(*) AS linhas FROM tb_t2_log GROUP BY nivel ORDER BY FIELD(nivel, "info", "warn", "error")');
for (const n of niveis) {
console.log( ${n.nivel.padEnd(5)} ${n.linhas} linha(s));
}
console.log('');
console.log('Um log so com "info" ocupa menos e nao serve para nada quando quebra.');
// --- rastreabilidade
console.log('');
console.log('--- rastreabilidade: o mesmo erro, dois pontos de vista ---');
const [porDispositivoLog] = await pool.query(
SELECT id_dispositivo, nivel, COUNT(*) AS n
FROM tb_t2_log WHERE nivel <> 'info' GROUP BY id_dispositivo, nivel
ORDER BY id_dispositivo, nivel);
for (const l of porDispositivoLog) {
console.log( ${l.id_dispositivo} ${l.nivel}: ${l.n});
}
console.log('');
console.log('A placa conta "perdidas=1". O servidor conta "error=1".');
console.log('Os dois numeros batem porque os dois lados registraram.');
await pool.end();
})();
A saída real, medida nesta máquina:
--- a rota que ENGOLE o erro --- respondeu 201 (id_requisicao req00001) respondeu 201 (id_requisicao (nenhum)) linhas da estacao-02 depois das duas respostas 201: 0 A placa recebeu 201 nas duas e nao tem nada no historico. --- a mesma rota, com log de verdade --- respondeu 500 (id_requisicao req00004) leituras no banco depois das quatro respostas: 2 Todas da estacao-01. A estacao-02 respondeu 201 duas vezes e nao tem linha. --- "quando a estacao-02 perdeu a ultima leitura?" --- estacao-01: ultima em 2026-09-30 19:28:48, 2 linha(s) A estacao-02 NAO tem linha: nao ha como descobrir pela tabela. So o log responde, e so se alguem tiver escrito nele. --- o log inteiro, filtrado por id_requisicao --- [info ] E_INICIO estacao-02 | pedido recebido [error] E_BANCO estacao-02 | ER_DATA_TOO_LONG: dados muito longos para a coluna [warn ] E_FIM estacao-02 | pedido encerrado Tres linhas, um pedido. O E_BANCO diz o que e o E_FIM diz que acabou. --- "quais pedidos falharam?" --- E_BANCO: 1 linha(s) em 1 pedido(s) --- o que cada nivel custa --- info 6 linha(s) warn 2 linha(s) error 1 linha(s) Um log so com "info" ocupa menos e nao serve para nada quando quebra. --- rastreabilidade: o mesmo erro, dois pontos de vista --- estacao-01 warn: 1 estacao-02 warn: 1 estacao-02 error: 1 A placa conta "perdidas=1". O servidor conta "error=1". Os dois numeros batem porque os dois lados registraram.
O professor aponta quatro coisas nessa saída, e as quatro são medidas, não previstas:
estacao-01 warn: 1: a linha definallyaparece mesmo no pedido que deu certo. É o comportamento correto e o que o aluno mais estranha, porque ele esperava que ofinallyfosse só para o caminho do erro.info 6, warn 2, error 1: a proporção é o que o professor quer. Se os nove fosseminfo, o filtro porerrornão encontraria nada e o log não custaria menos, só perderia a função.respondido 201 (id_requisicao (nenhum)): a função que engole o erro nem chega a criar o identificador do segundo pedido, porque othrowacontece antes. A placa recebeu201e o log não tem nenhuma linha que relacione aquele201a alguma coisa. Esse detalhe é o que o professor usa no item 4 da atividade.ultima em 2026-09-30 19:28:48: a hora vem doNOW()do banco, não da placa. É odt_gravacaodo dia 4 aula 1, e o dia 14 aula 1 vem mostrar que quando a rede cai, essa hora engana.
O sketch da placa, que é o lado esquerdo do mesmo cabo:
// dia 8, aula 2: o log comecenta na placa. // // A placa nao tem sistema operacional, nao tem servidor de log, nao tem // disco. Se o POST falhar e o professor nao descobrir amanha, o dado // perdido e perdido para sempre. Por isso o log e escrito ANTES de enviar, // na propria placa, com carimbo de tempo e identificador do erro. // // E o que o aluno leva pro Node: no dia 11 ele escreve arquivo de log no // servidor, e o formato e o mesmo — data, nivel, codigo, mensagem. #include <Arduino.h> // Niveis: 0 = info, 1 = atencao, 2 = erro. O numero e o que o professor // filtra no monitor quando a tela tem 300 linhas. const int NIVEL_INFO = 0; const int NIVEL_ERRO = 2; // Identificador curto do erro. E a coluna que responde "quando aconteceu // isso?" — o professor filtra o serial por ele e acha a linha na hora. const char* COD_LIGACAO = "E_WIFI"; const char* COD_POST = "E_POST"; const char* COD_VALIDACAO = "E_VALID"; // Contador de sequencia: o numero do evento na vida da placa. Sem ele, dois // erros no mesmo segundo ficam com o mesmo carimbo e o professor nao sabe // qual veio primeiro. unsigned long sequencia = 0; // O que todo log precisa ter para responder pergunta: QUANDO, QUE NIVELEL, // QUAL ERRO e QUE CONTEXTO. O `id_dispositivo` e o que separa uma estacao // de outra quando as duas estao no mesmo serial monitor. const char* ID_DISPOSITIVO = "estacao-01"; // Monta o carimbo de tempo sem depender de NTP: a placa ainda nao tem hora // certa (o dia 14 aula 1 traz o RTC). Uso `millis` e o contador, e o // professor explica que o espaco do "agora" vai ser preenchido depois. String carimboTempo() { unsigned long ms = millis(); unsigned long segundos = ms / 1000UL; unsigned long minutos = segundos / 60UL; unsigned long horas = minutos / 60UL; char buf[24]; snprintf(buf, sizeof(buf), "%02lu:%02lu:%02lu.%03lu", horas, minutos, segundos % 60UL, ms % 1000UL); return String(buf); } // Uma linha de log. Cinco campos, sempre os cinco, sempre na mesma ordem: // quem, quando, que nivel, qual codigo, o que aconteceu. Uma funcao so de // escrever log e o que garante que a linha 800 tem o mesmo formato da 3. void registrar(int nivel, const char* codigo, const char* mensagem, const char* contexto) { sequencia++; Serial.print("["); Serial.print(carimboTempo()); Serial.print("] #"); Serial.print(sequencia); Serial.print(" "); Serial.print(nivel == NIVEL_ERRO ? "ERRO " : (nivel == NIVEL_INFO ? "INFO " : "AVISO ")); Serial.print(codigo); Serial.print(" | "); Serial.print(mensagem); Serial.print(" | ctx="); Serial.print(contexto); Serial.println(); } // Try/catch nao existe em C++ que nem em JavaScript: quem controla o erro // e o proprio programa, com `return`. A placa avisa, registra e volta a // trabalhar — em vez de travar esperando a rede que nao voltou. bool enviarParaServidor(const char* payload, float timeoutS) { Serial.print(" abrindo conexao, timeout "); Serial.print(timeoutS, 1); Serial.println(" s ..."); // O dia 3 aula 2 gravou a requisicao de verdade no HTTPClient. Aqui nao // ha servidor: a chamada e comentada de proposito, porque o ponto da aula // e o que acontece QUANDO ela falha, e o erro mais honesto de reproduzir // e "nenhuma rede disponivel". registrar(NIVEL_ERRO, COD_LIGACAO, "WiFi.begin nao associou em 4 s", "ssid=rede-da-escola status=WIFI_CONNECT_FAILED"); // returning e o `catch` do Node: a funcao diz que falhou e devolve o // controle para quem chamou decidir. return false; } void setup() { Serial.begin(115200); Serial.println(); Serial.println("dia 8 aula 2 — log comeca na placa"); Serial.println("==================================="); Serial.print("dispositivo: "); Serial.println(ID_DISPOSITIVO); String payload = "{\"id_dispositivo\":\"estacao-01\",\"temperatura_c\":27.4,\"umidade_pct\":61}"; // --- Caso 1: deu certo. Log de nivel INFO, curto, sem drama. Serial.println(); Serial.println("[1] envio valido"); if (enviarParaServidor(payload.c_str(), 8.0)) { registrar(NIVEL_INFO, "E_OK", "leitura gravada", "tentativa=1 http=201"); } // --- Caso 2: a placa ficou sem rede. Log de erro com o motivo e o que a // placa tentou, para o professor nao precisar deduzir. Serial.println(); Serial.println("[2] segunda tentativa, sem rede"); bool ok = enviarParaServidor(payload.c_str(), 8.0); if (!ok) { registrar(NIVEL_ERRO, COD_LIGACAO, "leitura nao gravada, buffer circular agotado", "tentativa=2 http=0 leituras_perdidas=1"); } // --- Caso 3: erro silencioso. E o que o professor QUER ver na lousa: // o codigo "engoliu" o erro e seguiu como se nada tivesse acontecido. Serial.println(); Serial.println("[3] o codigo que ENGOLE o erro"); bool ok3 = enviarParaServidor(payload.c_str(), 8.0); if (ok3) { registrar(NIVEL_INFO, "E_OK", "leitura gravada", "tentativa=3 http=201"); } Serial.println(" (acima: a placa NAO imprimiu nada sobre a falha. Para quem"); Serial.println(" so olha o painel, esta leitura simplesmente desapareceu.)"); // --- Caso 4: erro antes de sair da placa. Nem chegou a tentar a rede. Serial.println(); registrar(NIVEL_ERRO, COD_VALIDACAO, "leitura com temperatura fora de faixa", "id=estacao-01 temperatura_c=900"); Serial.println(); Serial.println("Um log responde pergunta. Teste: filtre por #3 ou por E_WIFI."); Serial.println("O que o painel mostra nao e o mesmo que o log responde."); } void loop() { Serial.println(); Serial.println("--- nova rodada ---"); setup(); delay(10000); }
Por que assim e não de outro jeito. O return false de enviarParaServidor é o catch do Node, e o comentário do arquivo diz isso. Em C++ não existe try, catch nem finally nessa forma: quem controla o erro é o próprio programa, com tipo de retorno. A função que pode falhar devolve bool, e quem chama decide o que fazer. Quem decide errado é quem escreve enviarParaServidor(...) e ignora o retorno, e é exatamente o caso [3] do setup.
O NIVEL_INFO e NIVEL_ERRO valem 0 e 2, com o 1 sobrando no meio. A escolha é deliberada: 1 fica disponível para o nível de atenção, e o professor explica que a ordem info abaixo de warn abaixo de error é o que permite filtrar por comparação numérica. Se os três fossem 0, 1 e 2 sem folga, bastaria inverter o eixo da tabela e o filtro por nível inverteria a tabela inteira.
O registrar recebe quatro parâmetros e não tem condicional. A placa é o lugar onde o if de "só registra se der erro" parece ganho, e o professor discorda: a linha de info que grava o que deu certo é o que permite ao professor dizer "às 10:04 gravou, às 10:04 falhou, e entre elas não aconteceu nada". Um log só com erro deixa buracos, e o buraco é indistinguível de "não aconteceu".
O contador sequencia é o id_requisicao da placa. Ele existe porque millis() tem resolução de milissegundos e dois eventos podem cair no mesmo milissegundo. O professor mostra isso Comparing o #1 e o #3 da saída: sem o contador, o filtro por tempo tem duas linhas com o mesmo carimbo e ninguém sabe qual veio primeiro.
O caso [3] é o ponto da aula e ele é o mais curto do arquivo: três linhas, e nenhuma delas registra a falha. O professor não comenta nada depois de rodar; deixa a turma achar, e a pergunta que ele faz é "onde está o erro?". Quando alguém achar que a resposta é "está no if (ok3) que nunca é verdadeiro", o professor aponta que o problema maior é o oposto: quando o if é verdadeiro e o envio falhou, o código grava E_OK e mente. Um if que só verifica o caminho feliz não tem tratamento de erro: tem sorte.
Criterios de correcao
| Critério | Pontos |
|---|---|
| Assinatura de cinco campos escrita no caderno, com exemplo de valor em cada campo | 2 pontos |
DHT11 no GPIO4 com pull-up, e pinos declarados em const no topo | 1 pontos |
| Tabela do item 3 preenchida nas quatro linhas, separando o que o painel mostra do que o log tem | 2 pontos |
| Linha de log proposta no item 4, com código de erro próprio, e o nome do bloco que engole a falha | 2 pontos |
| Item 5: as duas linhas do que acrescentar, e o custo em linhas de log por pedido | 2 pontos |
Saída do filtro por id_requisicao colada no caderno, com as três linhas do mesmo pedido | 2 pontos |
Item 8: comparação entre as perdidas da placa e os error do log, e o lado que está mentindo | 1 pontos |
Erros comuns
| Erro | Como aparece | Correção |
|---|---|---|
catch vazio com comentário "tratando o erro" | "Está no try/catch, então já está tratado" | "O catch que não registra e não relança é o erro silencioso: o pedido responde sucesso e o dado morre. Registre e lance o erro de novo." |
throw sem registrar antes | "Tem a exceção lá em cima" | "Se o catch só relança, ninguém sabe o que aconteceu: o log não tem a linha. Registre primeiro, lance depois." |
| Confundir erro esperado com erro inesperado | "A validação virou 500" | "O 422 do dia 8 aula 1 é regra de negócio e não vai para o catch. Se ele entra no catch e vira 500, a placa tenta de novo para sempre." |
| Concatenar log à mão em vez de usar a função | "Achei o erro, mas metade das linhas não tem data" | "Nunca monte log no console.log. Use a função de registro, para todo evento ter os mesmos campos na mesma ordem." |
Tudo com nível error | "Filtrei por error e apareceu o sistema inteiro" | "error é para o pedido que falhou. Informação do caminho feliz é info, e o que funcionou com um desvio é warn." |
Tudo com nível info | "O log está cheio e não achei nada" | "Log só de info ocupa menos e não serve para nada quando quebra. O nível é o que o filtro usa para achar." |
| Sem identificador de requisição | "Achei o erro do banco mas não sei de qual leitura era" | "Gere um identificador no começo do pedido e leve-o em todas as linhas. É ele que amarra o E_INICIO, o error e o E_FIM do mesmo pedido." |
| Mensagem do erro sem o código | "O banco caiu" | "O código é o que se filtra. ER_DATA_TOO_LONG continua valendo depois de você mover a função; o nome da função não." |
| Ignorar o retorno da função que pode falhar | "Chamei o envio e não conferi o resultado" | "Em C++ quem controla o erro é o tipo de retorno. Se a função devolve bool, conferiu o bool. Ignorar é o caso 3 do sketch." |
| Comparar o total do painel com o total do banco | "O painel diz 4 e o banco tem 3, o banco errou" | "O painel pode estar contando a tentativa. Compare com o log: a placa conta perdidas, o servidor conta error." |
| Confundir carimbo da placa com carimbo do banco | "A leitura chegou 20 minutos atrasada e o banco gravou agora" | "São dois instantes: dt_leitura é quando a placa mediu, dt_gravacao é quando o banco gravou. Com a rede caída, os dois divergem, e o dia 14 aula 1 resolve." |
Desafio extra
Escreva a função de registro com um sexto campo opcional, o tempo que o pedido levou, e meça o tempo com Date.now() em volta do try. Depois rode vinte pedidos e junte os dois lados: a placa registra o id_dispositivo e o número de tentativa, o servidor registra o id_requisicao e a duração. Escreva a consulta que devolve "qual placa tem mais pedidos acima de dois segundos", e diga em uma frase o que essa consulta responde que o painel não responde.
A resolucao, compilada
// dia 8, aula 2: o log comecenta na placa. // // A placa nao tem sistema operacional, nao tem servidor de log, nao tem // disco. Se o POST falhar e o professor nao descobrir amanha, o dado // perdido e perdido para sempre. Por isso o log e escrito ANTES de enviar, // na propria placa, com carimbo de tempo e identificador do erro. // // E o que o aluno leva pro Node: no dia 11 ele escreve arquivo de log no // servidor, e o formato e o mesmo — data, nivel, codigo, mensagem. #include <Arduino.h> // Niveis: 0 = info, 1 = atencao, 2 = erro. O numero e o que o professor // filtra no monitor quando a tela tem 300 linhas. const int NIVEL_INFO = 0; const int NIVEL_ERRO = 2; // Identificador curto do erro. E a coluna que responde "quando aconteceu // isso?" — o professor filtra o serial por ele e acha a linha na hora. const char* COD_LIGACAO = "E_WIFI"; const char* COD_POST = "E_POST"; const char* COD_VALIDACAO = "E_VALID"; // Contador de sequencia: o numero do evento na vida da placa. Sem ele, dois // erros no mesmo segundo ficam com o mesmo carimbo e o professor nao sabe // qual veio primeiro. unsigned long sequencia = 0; // O que todo log precisa ter para responder pergunta: QUANDO, QUE NIVELEL, // QUAL ERRO e QUE CONTEXTO. O `id_dispositivo` e o que separa uma estacao // de outra quando as duas estao no mesmo serial monitor. const char* ID_DISPOSITIVO = "estacao-01"; // Monta o carimbo de tempo sem depender de NTP: a placa ainda nao tem hora // certa (o dia 14 aula 1 traz o RTC). Uso `millis` e o contador, e o // professor explica que o espaco do "agora" vai ser preenchido depois. String carimboTempo() { unsigned long ms = millis(); unsigned long segundos = ms / 1000UL; unsigned long minutos = segundos / 60UL; unsigned long horas = minutos / 60UL; char buf[24]; snprintf(buf, sizeof(buf), "%02lu:%02lu:%02lu.%03lu", horas, minutos, segundos % 60UL, ms % 1000UL); return String(buf); } // Uma linha de log. Cinco campos, sempre os cinco, sempre na mesma ordem: // quem, quando, que nivel, qual codigo, o que aconteceu. Uma funcao so de // escrever log e o que garante que a linha 800 tem o mesmo formato da 3. void registrar(int nivel, const char* codigo, const char* mensagem, const char* contexto) { sequencia++; Serial.print("["); Serial.print(carimboTempo()); Serial.print("] #"); Serial.print(sequencia); Serial.print(" "); Serial.print(nivel == NIVEL_ERRO ? "ERRO " : (nivel == NIVEL_INFO ? "INFO " : "AVISO ")); Serial.print(codigo); Serial.print(" | "); Serial.print(mensagem); Serial.print(" | ctx="); Serial.print(contexto); Serial.println(); } // Try/catch nao existe em C++ que nem em JavaScript: quem controla o erro // e o proprio programa, com `return`. A placa avisa, registra e volta a // trabalhar — em vez de travar esperando a rede que nao voltou. bool enviarParaServidor(const char* payload, float timeoutS) { Serial.print(" abrindo conexao, timeout "); Serial.print(timeoutS, 1); Serial.println(" s ..."); // O dia 3 aula 2 gravou a requisicao de verdade no HTTPClient. Aqui nao // ha servidor: a chamada e comentada de proposito, porque o ponto da aula // e o que acontece QUANDO ela falha, e o erro mais honesto de reproduzir // e "nenhuma rede disponivel". registrar(NIVEL_ERRO, COD_LIGACAO, "WiFi.begin nao associou em 4 s", "ssid=rede-da-escola status=WIFI_CONNECT_FAILED"); // returning e o `catch` do Node: a funcao diz que falhou e devolve o // controle para quem chamou decidir. return false; } void setup() { Serial.begin(115200); Serial.println(); Serial.println("dia 8 aula 2 — log comeca na placa"); Serial.println("==================================="); Serial.print("dispositivo: "); Serial.println(ID_DISPOSITIVO); String payload = "{\"id_dispositivo\":\"estacao-01\",\"temperatura_c\":27.4,\"umidade_pct\":61}"; // --- Caso 1: deu certo. Log de nivel INFO, curto, sem drama. Serial.println(); Serial.println("[1] envio valido"); if (enviarParaServidor(payload.c_str(), 8.0)) { registrar(NIVEL_INFO, "E_OK", "leitura gravada", "tentativa=1 http=201"); } // --- Caso 2: a placa ficou sem rede. Log de erro com o motivo e o que a // placa tentou, para o professor nao precisar deduzir. Serial.println(); Serial.println("[2] segunda tentativa, sem rede"); bool ok = enviarParaServidor(payload.c_str(), 8.0); if (!ok) { registrar(NIVEL_ERRO, COD_LIGACAO, "leitura nao gravada, buffer circular agotado", "tentativa=2 http=0 leituras_perdidas=1"); } // --- Caso 3: erro silencioso. E o que o professor QUER ver na lousa: // o codigo "engoliu" o erro e seguiu como se nada tivesse acontecido. Serial.println(); Serial.println("[3] o codigo que ENGOLE o erro"); bool ok3 = enviarParaServidor(payload.c_str(), 8.0); if (ok3) { registrar(NIVEL_INFO, "E_OK", "leitura gravada", "tentativa=3 http=201"); } Serial.println(" (acima: a placa NAO imprimiu nada sobre a falha. Para quem"); Serial.println(" so olha o painel, esta leitura simplesmente desapareceu.)"); // --- Caso 4: erro antes de sair da placa. Nem chegou a tentar a rede. Serial.println(); registrar(NIVEL_ERRO, COD_VALIDACAO, "leitura com temperatura fora de faixa", "id=estacao-01 temperatura_c=900"); Serial.println(); Serial.println("Um log responde pergunta. Teste: filtre por #3 ou por E_WIFI."); Serial.println("O que o painel mostra nao e o mesmo que o log responde."); } void loop() { Serial.println(); Serial.println("--- nova rodada ---"); setup(); delay(10000); }
Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia08 aula2.
