Erro: o caminho sempre quebra em algum lugar — Arduino e IoT — semana 8 do 2o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 8 · Erro: o caminho sempre quebra em algum lugar — Material de Apoio Arduino

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, 422 ou 201) 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 DHT11 por 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, mysql2 instalado e MySQL ou MariaDB 10.11
  • Terminal com o curl aberto 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:

  1. campo faltando: o JSON saiu sem umidade_pct. Não é problema de valor, é problema de forma. O corpo não é o que a rota prometeu.
  2. campo vazio: a chave veio, com "" ou null. A forma está certa e o conteúdo não está. Um SELECT que soma esse campo devolve NULL e quebra a agregação do dia 6.
  3. tipo errado: temperatura_c chegou 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.
  4. 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:

FaixaQuem definePara que serve
Faixa física do sensoro fabricantesaber quando o componente está quebrado
Limite plausívelquem escreve a regradecidir 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:

  • DHT11 no GPIO4, com resistor de pull-up de 4,7 k ohm entre o pino de dados e 3V3, como no dia 8 do 1o trimestre.
  • Servidor Node do dia 5 aula 2 rodando, com a tabela tb_t2_leitura do dia 4.
  1. 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.
  2. 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.
  3. 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.
  4. Escolha dois dos nove casos e troque a regra: faça o LIMITE de temperatura caber de 5 a 60 e rode de novo. Anote o que mudou no status e na contagem de linhas gravadas.
  5. Mande um curl com a chave de API correta e um temperatura_c de 900. Confirme o status e depois rode o SELECT para mostrar que a linha não existe.
  6. Mande o mesmo curl sem a chave de API. Qual status volta agora, e por que é um número diferente do item 5?
  7. 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érioPontos
Tabela do item 2 preenchida nas nove linhas, com status, lista de problemas e entrada ou não no banco3 pontos
Os quatro formatos de sujeira nomeados, e o local do conserto (placa, sensor ou JSON) escrito para cada um2 pontos
DHT11 no GPIO4 com pull-up, e MIN_TEMP_C e MAX_TEMP_C declarados em const no topo do arquivo2 pontos
Item 4: o LIMIT estreitado e as duas contagens de linhas gravadas, antes e depois2 pontos
Item 5: 422 no curl com 900 graus e SELECT confirmando que a linha não existe2 pontos
Item 6: o 401 identificado como número diferente do 422, e a razão1 pontos

Erros comuns

ErroComo apareceCorreçã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, catch e finally, 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, mysql2 instalado e MySQL ou MariaDB 10.11
  • Tabela tb_t2_log criada com nivel, codigo, mensagem, id_dispositivo e id_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:

BlocoPergunta que ele respondeO que acontece se você esquecer
tryonde o erro pode acontecer?o erro sobe e derruba o processo
catcho que fazer com o erro que chegou?o processo morre; no dia 3 aula 2 isso é o "não travar"
finallyo 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:

  1. quando — o instante do evento
  2. que nível — info, warn ou error
  3. qual código — o nome curto do tipo de evento
  4. a mensagem — o que aconteceu, em português
  5. 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ívelQuando usarO que o professor faz com a linha
infoo sistema funcionou como esperadoignora
warnfuncionou, e tem alguma coisa errada do ladoolha quando o número não bate
erroro pedido falhouprocura 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:

  • DHT11 no GPIO4, com resistor de pull-up de 4,7 k ohm entre o pino de dados e 3V3.
  • Servidor Node do dia 5 aula 2 rodando, com as tabelas tb_t2_leitura e tb_t2_log.
  1. 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.
  2. Escreva a assinatura de cinco campos do log no caderno, com o nome de cada campo e um exemplo de valor para cada um.
  3. 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.
  4. 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.
  5. 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.
  6. Rode a consulta que filtra o log por id_requisicao e cole a saída no caderno. Quantas linhas tem? Elas são do mesmo pedido?
  7. 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.
  8. 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 error no 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 de finally aparece mesmo no pedido que deu certo. É o comportamento correto e o que o aluno mais estranha, porque ele esperava que o finally fosse só para o caminho do erro.
  • info 6, warn 2, error 1: a proporção é o que o professor quer. Se os nove fossem info, o filtro por error nã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 o throw acontece antes. A placa recebeu 201 e o log não tem nenhuma linha que relacione aquele 201 a 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 do NOW() do banco, não da placa. É o dt_gravacao do 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érioPontos
Assinatura de cinco campos escrita no caderno, com exemplo de valor em cada campo2 pontos
DHT11 no GPIO4 com pull-up, e pinos declarados em const no topo1 pontos
Tabela do item 3 preenchida nas quatro linhas, separando o que o painel mostra do que o log tem2 pontos
Linha de log proposta no item 4, com código de erro próprio, e o nome do bloco que engole a falha2 pontos
Item 5: as duas linhas do que acrescentar, e o custo em linhas de log por pedido2 pontos
Saída do filtro por id_requisicao colada no caderno, com as três linhas do mesmo pedido2 pontos
Item 8: comparação entre as perdidas da placa e os error do log, e o lado que está mentindo1 pontos

Erros comuns

ErroComo apareceCorreçã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.