Fechamento do trimestre — Arduino e IoT — semana 15 do 2o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 15 · Fechamento do trimestre — Material de Apoio Arduino

Semana 15 de 16· 2o trimestre · 01/05 a 04/09

Fechamento do trimestre

Revisao, duvida por duvida, e o que muda no trimestre seguinte.

Aula 1 — Revisao do 2o trimestre e reparo do que ficou torto

Objetivos

  • Rodar o diagnóstico do caminho inteiro e apontar no monitor em qual das cinco etapas o seu projeto perde mais leitura.
  • Conferir, no seu próprio projeto, se a contagem de tentativas, gravações e perdas fecha — e dizer o que fazer quando não fecha.
  • Preencher a lista de checagem dos oito blocos do trimestre marcando OK, mais ou menos ou não fiz, e entregar ela no fim da aula.
  • Apontar um arquivo do seu projeto que você vai apagar até o fim do trimestre e outro que você vai manter.

Material

  • 1 ESP32 DevKit V1 por aluno, com o cabo USB
  • Computador com o projeto do trimestre aberto e o servidor parado
  • Folha de lista de checagem impressa, uma por aluno
  • 1 caneta por aluno
  • Projetor com o monitor serial da placa do professor

Conceitos

O caminho tem cinco etapas, e cada uma quebra de um jeito

O trimestre inteiro construiu uma cadeia. Ela não quebrou inteira: ela quebrou em algum lugar, e o lugar muda de aluno para aluno.

  1. Sensor: o DHT11 devolve zero quando falha, e zero é um número plausível. Sem limite de plausibilidade, o lixo entra.
  2. Montagem do JSON: a string sai com aspas erradas e o servidor devolve 400.
  3. Rede: a placa não acha o ponto de acesso, ou acha e não recebe resposta. É o timeout, não o código.
  4. Servidor: recebe e recusa com 422 porque o dado não passa na validação. O dado chegou inteiro e mesmo assim foi perdido.
  5. Banco: a consulta foi preparada, mas a tabela não existe ou a coluna mudou. É o 500.

A ordem importa na correção. Enquanto a etapa 1 não estiver boa, nenhuma das outras pode ser testada direito, porque o dado que chega no fim é o mesmo dado ruim.

Perdido e não tentado são coisas diferentes

Uma leitura que saiu da bancada e foi recusada pelo servidor foi tentada. Uma leitura que nunca saiu porque a placa reiniciou não foi tentada. As duas viram dado perdido, mas só uma delas consome tempo de rede.

O diagnóstico separa as duas porque o conserto é diferente: para recuperar a leitura recusada, o conserto é no servidor; para recuperar a leitura que nem saiu, o conserto é na placa. Somar tudo num número só esconde essa diferença.

As contas têm que fechar

A regra mais barata que existe em programa: o total é sempre a soma das partes. Tentativas são gravações mais perdas. Se o seu sistema grava 14 de 21 e o seu contador diz que perdeu 5, sobrou uma leitura que sumiu sem ser contada — e essa leitura é um erro de contagem, não do sistema.

Nenhum teste automatizado do curso descobre isso antes do professor olhar o número.

Dívida técnica é dívida, e conta nomeada

O que o aluno chama de "o meu código está meio bagunçado" tem nome e tem custo: uma função com quatro responsabilidades, uma senha escrita dentro do arquivo, um catch vazio. No fechamento do trimestre, o professor pede dois nomes, não uma opinião: o arquivo que você vai apagar e o arquivo que você vai manter. É o começo do trabalho do 3o trimestre.

Atividade

Montagem: nenhuma. A placa fica sem sensor nesta aula. O diagnóstico de hoje roda sozinho e é o que o professor projeta.

  1. Grave o sketch da resolução na sua placa e abra o monitor serial a 115200. Espere o relatório inteiro: são 21 ciclos, cerca de 40 segundos.
  2. Preencha o cabeçalho do seu relatório no caderno: quantas leituras o seu sistema gravou de verdade, e de quantas tentativas.
  3. Marque a tabela de perdas do seu lado com estes três números: leituras gravadas, leituras perdidas e a etapa que mais perdeu. A etapa que mais perdeu é a que a placa imprime em "comece por aqui".
  4. Rode a conferência de contas: ela fecha ou não fecha? Se não fecha, o erro é de contagem, e você vai corrigir isso antes de qualquer outra coisa.
  5. Preencha a lista de checagem dos oito blocos do trimestre, marcando OK, mais ou menos ou não fiz em cada linha:
  6. bloco 1, sensor e JSON na placa
  7. bloco 2, tratar erro de rede com timeout
  8. bloco 3, tabela e INSERT no MySQL
  9. bloco 4, ponta a ponta, da placa até a linha no banco
  10. bloco 5, consulta por período e agregação
  11. bloco 6, validar e recusar dado ruim
  12. bloco 7, log com identificador
  13. bloco 8, camadas, config por ambiente e um teste automático
  14. Escreva no verso da folha a lista do que você ainda não sabe fazer, em três linhas ou menos. Esta lista é o que abre a aula 2.

Nota: 10 pontos. Critério de fim: a lista de checagem preenchida nas oito linhas, com a etapa que mais perdeu escrita, e a lista do que ainda não sabe entregue. Critério de fim do professor: olha o envelope e sabe em quantos blocos a turma está forte e em quantos está fraca.

Resolucao

O sketch de hoje é a ferramenta de fechamento, não um exemplo de estilo. Ele roda o caminho inteiro simulado e determinístico — o sensor, a rede e o banco não estão ligados, e o comentário no topo avisa isso. Determinístico porque a tabela precisa ser igual na placa de todo mundo, senão a turma não compara a tela do colega com a sua.

Os contadores ficam fora do loop, declarados no topo. Um contador que nasce dentro do loop é a aula 2 de hoje inteira: ele zera a cada volta e o relatório não fecha.

// DIAGNOSTICO DO CAMINHO — dia 15, aula 1 (revisão do 2o trimestre).
//
// Este arquivo não é um exemplo de estilo. É a ferramenta que o aluno roda no
// fechamento do trimestre para ver ONDE o caminho dele está quebrando:
// quantas leituras foram gravadas, quantas foram perdidas e em quanto tempo.
//
// AVISO PARA O PROFESSOR: aqui não tem sensor ligado e não tem servidor. As
// leituras e as respostas de rede são SIMULADAS, e simuladas de forma
// DETERMINÍSTICA (divisão por número, nada de `random` sem semente). Isso é
// proposital: a tabela precisa ser exatamente a mesma na placa de todo mundo,
// senão a turma não consegue comparar a tela do colega com a sua.
//
// A aula 2 do dia 16 parte deste mesmo arquivo.

#include <Arduino.h>

// GPIO2 é o LED embutido na maioria dos DevKit. Aqui ele é o sinal de vida:
// se a luz parar de piscar, o programa travou — e não é o WiFi, e não é o banco.
const int PIN_LED = 2;

// Ritmo do diagnóstico. 21 ciclos em três janelas de sete: número pequeno o
// bastante para o professor ler a tabela inteira na lousa antes do intervalo.
const int INTERVALO_CICLO = 1500;
const int TOTAL_DE_CICLOS = 21;

// Limite de plausibilidade introduzido no dia 8. Fora desta faixa o dado é lixo
// e não entra no banco, mesmo que a placa "conseguiu" enviar.
const float TEMP_MINIMA = -10.0;
const float TEMP_MAXIMA = 60.0;

// Tamanho do buffer do JSON que a placa monta, igual ao dia 3.
const int TAMANHO_PAYLOAD = 96;

// --- Contadores do caminho. Ficam no topo, fora do `loop`, de propósito: um
// contador que nasce dentro do `loop` é a aula 2 do dia 15 inteira. ---
int ciclo = 0;
int leiturasTentadas = 0;
int leiturasGravadas = 0;
int falhasSemSinal = 0;
int falhasServidor = 0;
int falhasForaDoLimite = 0;
int tempoAcumulado = 0;
int tempoPiorCaso = 0;
int cicloPiorCaso = 0;

// Simula a leitura do DHT11. O valor 0 é um defeito real do sensor: quando a
// leitura falha, o DHT11 devolve zero em vez de erro. É por isso que existe
// limite de plausibilidade.
float temperaturaSimulada() {
  if (ciclo % 11 == 0) {
    return 0.0;
  }
  return 21.0 + ((ciclo % 9) * 2.0);
}

// Devolve o motivo da falha do ciclo, ou 0 quando o ciclo passa inteiro.
// A ordem importa e é o conteúdo da aula: o caminho quebra no PRIMEIRO ponto,
// não em todos ao mesmo tempo.
int motivoDaFalha(float temperatura) {
  if (temperatura < TEMP_MINIMA || temperatura > TEMP_MAXIMA) {
    return 3;  // 422: o dado chegou, mas era lixo
  }
  if (ciclo % 7 == 0) {
    return 1;  // a placa não achou o ponto de acesso
  }
  if (ciclo % 5 == 0) {
    return 2;  // 500: o servidor recebeu e não conseguiu gravar
  }
  return 0;
}

const char* nomeDoMotivo(int motivo) {
  if (motivo == 1) {
    return "FALHA sem sinal";
  }
  if (motivo == 2) {
    return "FALHA 500 servidor";
  }
  if (motivo == 3) {
    return "FALHA fora do limite";
  }
  return "gravada";
}

// Tempo de ida e volta da rede. Simulado, como todo o resto: o valor é
// determinístico para que o pior caso caia sempre no mesmo ciclo.
int tempoDeRedeSimulado() {
  return 90 + ((ciclo % 6) * 40);
}

// O JSON que a placa monta no dia 3. Montar a string é trabalho real e medido
// pelo `millis`; só a rede é simulada.
void montarPayload(float temperatura, char* destino) {
  snprintf(destino, TAMANHO_PAYLOAD,
           "{\"id\":\"placa-01\",\"temperatura\":%.1f}", temperatura);
}

// Recomeça a contagem para o aluno ver o relatório inteiro mais de uma vez
// sem precisar gravar o sketch de novo.
void zerarContadores() {
  ciclo = 0;
  leiturasTentadas = 0;
  leiturasGravadas = 0;
  falhasSemSinal = 0;
  falhasServidor = 0;
  falhasForaDoLimite = 0;
  tempoAcumulado = 0;
  tempoPiorCaso = 0;
  cicloPiorCaso = 0;
}

void mostrarResumo() {
  int perdas = TOTAL_DE_CICLOS - leiturasGravadas;
  int percentual = (TOTAL_DE_CICLOS > 0) ? (100 * leiturasGravadas / TOTAL_DE_CICLOS) : 0;
  int media = (leiturasGravadas > 0) ? (tempoAcumulado / leiturasGravadas) : 0;

  Serial.println();
  Serial.println("=== RESUMO DO CAMINHO ===");
  Serial.print("leituras tentadas .. ");
  Serial.println(leiturasTentadas);
  Serial.print("leituras gravadas ... ");
  Serial.println(leiturasGravadas);
  Serial.print("leituras perdidas ... ");
  Serial.print(perdas);
  Serial.print("  (");
  Serial.print(percentual);
  Serial.println(" % de aproveitamento)");
  Serial.print("tempo medio ........ ");
  Serial.print(media);
  Serial.println(" ms por leitura");
  Serial.print("pior caso ......... ");
  Serial.print(tempoPiorCaso);
  Serial.print(" ms, no ciclo ");
  Serial.println(cicloPiorCaso);
}

// A conferência que fecha o diagnóstico: se estas três contas não baterem, o
// erro é de CONTAGEM, e não do sistema. É esta linha que acha o defeito da
// aula 2.
void conferirContas() {
  int perdas = TOTAL_DE_CICLOS - leiturasGravadas;
  Serial.println();
  Serial.print("conferencia: ");
  Serial.print(leiturasTentadas);
  Serial.print(" = ");
  Serial.print(leiturasGravadas);
  Serial.print(" + ");
  Serial.print(perdas);
  Serial.println(" ?");
  if (leiturasTentadas != leiturasGravadas + perdas) {
    Serial.println("  >>> AS CONTAS NAO FECHAM. O erro esta na contagem.");
  } else {
    Serial.println("  >>> contas fecham: o caminho estainteiro, e o sistema.");
  }
}

// O diagnóstico que o aluno lê para decidir por onde começar a consertar.
void mostrarDiagnostico() {
  Serial.println();
  Serial.println("=== ONDE O CAMINHO QUEBROU ===");
  Serial.print("sensor e validacao . ");
  Serial.println(falhasForaDoLimite);
  Serial.print("sinal de rede ....... ");
  Serial.println(falhasSemSinal);
  Serial.print("servidor e banco .... ");
  Serial.println(falhasServidor);

  int maior = falhasSemSinal;
  const char* etapa = "sinal de rede";
  if (falhasServidor > maior) {
    maior = falhasServidor;
    etapa = "servidor e banco";
  }
  if (falhasForaDoLimite > maior) {
    maior = falhasForaDoLimite;
    etapa = "sensor e validacao";
  }

  Serial.println();
  if (maior == 0) {
    Serial.println("Nenhuma etapa perdeu leitura neste ciclo completo.");
    return;
  }
  Serial.print("Comece por aqui: ");
  Serial.print(etapa);
  Serial.print(", com ");
  Serial.print(maior);
  Serial.println(" perdas.");
}

void setup() {
  pinMode(PIN_LED, OUTPUT);

  Serial.begin(115200);
  Serial.println();
  Serial.println("DIAGNOSTICO DO CAMINHO — dia 15, aula 1");
  Serial.println("leituras e rede simuladas, deterministicas");
  Serial.println("---------------------------------------------------");
  Serial.println("ciclo | temperatura | resultado           | tempo");
}

void loop() {
  ciclo++;
  digitalWrite(PIN_LED, HIGH);  // sinal de vida: o ciclo começou

  float temperatura = temperaturaSimulada();
  int motivo = motivoDaFalha(temperatura);
  int rede = tempoDeRedeSimulado();

  char payload[TAMANHO_PAYLOAD];
  montarPayload(temperatura, payload);

  unsigned long inicio = millis();

  Serial.print(ciclo);
  Serial.print("     | ");
  Serial.print(temperatura, 1);
  Serial.print(" C       | ");

  if (motivo == 0) {
    leiturasTentadas++;
    leiturasGravadas++;
    Serial.print(nomeDoMotivo(motivo));
    // O payload só é mostrado no primeiro ciclo inteiro: serve de prova de que
    // a string que a placa manda é o JSON do dia 3, e polui se repetir 21 vezes.
    if (leiturasGravadas == 1) {
      Serial.print("  | ");
      Serial.print(payload);
    }
  } else {
    // A leitura também conta como tentativa: ela saiu da bancada, tentou
    // entrar e foi recusada. Perder é diferente de nem ter tentado.
    leiturasTentadas++;
    if (motivo == 1) {
      falhasSemSinal++;
    } else if (motivo == 2) {
      falhasServidor++;
    } else {
      falhasForaDoLimite++;
    }
    Serial.print(nomeDoMotivo(motivo));
  }

  int gasto = (int)(millis() - inicio);
  tempoAcumulado += gasto;
  if (gasto > tempoPiorCaso) {
    tempoPiorCaso = gasto;
    cicloPiorCaso = ciclo;
  }

  Serial.print("  | ");
  Serial.print(gasto);
  Serial.print(" ms + ");
  Serial.print(rede);
  Serial.println(" ms de rede");

  if (ciclo >= TOTAL_DE_CICLOS) {
    mostrarResumo();
    conferirContas();
    mostrarDiagnostico();
    Serial.println();
    Serial.println("O relatorio comeca de novo em alguns segundos.");
    zerarContadores();
    delay(5000);
  }

  delay(INTERVALO_CICLO);
  digitalWrite(PIN_LED, LOW);
}

Por que assim e não de outro jeito. O motivoDaFalha devolve um motivo, e não uma lista. Se a placa determinasse todas as falhas de uma vez, o relatório mostraria 8 falhas na rede e 6 no banco para o mesmo ciclo, e o aluno veria um número maior que o total de ciclos sem entender por quê. Devolver só o primeiro ponto quebrado é o comportamento do sistema real: o dado parou ali e não chegou às etapas seguintes.

O payload aparece só no primeiro ciclo que passou inteiro. Ele existe para provar que a string que a placa manda é o JSON do dia 3 — mostrar 14 vezes seguidas vira ruído, e o professor perde a turma na segunda.

A conferência de contas é uma função separada de propósito. Ela não serve para o relatório bonito, serve para falhar. É a única linha do sketch que existe para dizer que o número está errado, e por isso é a que vai aparecer quebrada na aula 2.

Por que a leitura que falha conta como tentativa. Se leiturasTentadas só aumentasse no caminho feliz, o total de tentativas seria menor que o total de ciclos, e a divisão do percentual ficaria sem sentido. Mais importante: é essa escolha que separa "recusado pelo servidor" de "nunca saiu da placa", e o aluno que não faz essa distinção gasta o resto do trimestre procurando erro de rede onde o problema é de validação.

Criterios de correcao

CritérioPontos
Diagnóstico gravado na placa e relatório completo lido até o fim (21 ciclos)2 pontos
Tabela de perdas preenchida com gravadas, perdidas e a etapa que mais perdeu3 pontos
Conferência de contas rodada e o resultado (fecha ou não fecha) anotado1 ponto
Lista de checagem com as oito linhas preenchidas e marcadas2 pontos
Lista do que ainda não sabe escrita no verso da folha2 pontos

Erros comuns

ErroComo apareceCorrecao
Subir só 20 segundos e ler o relatório pela metade"Chegou em 'ciclo 14' e parou de aparecer""São 21 ciclos, uns 40 segundos. Deixa rodar. Se travar no meio, olha se o LED ainda está piscando — e uma coisa de cada vez."
Ler só a linha de cima e dizer que está tudo bemRelatório com 14 gravadas e 7 perdidas, e o aluno relata "funciona""Funciona e não é a mesma coisa. Perdeu sete leituras: a próxima vez que o gráfico vier com buraco, é aqui que você olha."
Contar tentativa só no caminho feliz"Minhas tentativas deram 14 e minhas gravações deram 14""A leitura que o servidor recusou também saiu da bancada. Ela conta como tentativa. Por isso o total tem 21."
Pôr o contador dentro do loop ao refazer em casaO relatório zera a cada volta e nunca fecha"Contador nasce no topo, antes do setup. Se ele nasce dentro do loop, ele vale só para a última volta, e a sua conta nunca vai fechar."
Marcar a lista de checagem inteira como OKOito linhas com OK e zero linha marcada como pendência"Ponto que você fez uma vez e nunca mais não é OK, é mais ou menos. Se eu te pedir para rodar agora, você roda?"
Deixar a lista do que não sabe em brancoFolha entregue só com a frente preenchida"Essa lista é o que abre a aula de amanhã. Sem ela eu não sei o que a turma precisa, e vou perder o tempo da aula com o assunto que eu achei bonito."
Querer consertar tudo antes de medirAluno abre o arquivo e começa a editar sem saber a etapa"Meu conselho: primeiro o número. Se você não sabe onde perde, você não sabe o que consertar — e vai mexer em arquivo certo com conserto errado."
Comparar o resultado do colega com o seu e concluir que a placa dele está ruim"O dele deu menos leitura que o meu, a placa dele deve estar com defeito""A simulação é a mesma na placa de todo mundo, e o número sai igual. Se o seu deu diferente do meu, o que é diferente é o sistema, não a placa."

Desafio extra

A placa imprime "comece por aqui" com uma etapa. Troque o código para imprimir as duas piores etapas, em ordem, e justifique em duas linhas por que a ordem não pode ser qualquer uma. A resposta esperada: conserta-se o que perde mais primeiro só se as etapas forem independentes; quando uma etapa a montante alimenta a outra, consertar a pior conta não recupera nada, porque o dado nem chega lá. Para conferir, abra o codigo/t2/dia15/aula1.ino na pasta codigo/t2/dia15 e procure a função mostrarDiagnostico: as três comparações estão no fim dela.

>

A resolucao, compilada

// DIAGNOSTICO DO CAMINHO — dia 15, aula 1 (revisão do 2o trimestre).
//
// Este arquivo não é um exemplo de estilo. É a ferramenta que o aluno roda no
// fechamento do trimestre para ver ONDE o caminho dele está quebrando:
// quantas leituras foram gravadas, quantas foram perdidas e em quanto tempo.
//
// AVISO PARA O PROFESSOR: aqui não tem sensor ligado e não tem servidor. As
// leituras e as respostas de rede são SIMULADAS, e simuladas de forma
// DETERMINÍSTICA (divisão por número, nada de `random` sem semente). Isso é
// proposital: a tabela precisa ser exatamente a mesma na placa de todo mundo,
// senão a turma não consegue comparar a tela do colega com a sua.
//
// A aula 2 do dia 16 parte deste mesmo arquivo.

#include <Arduino.h>

// GPIO2 é o LED embutido na maioria dos DevKit. Aqui ele é o sinal de vida:
// se a luz parar de piscar, o programa travou — e não é o WiFi, e não é o banco.
const int PIN_LED = 2;

// Ritmo do diagnóstico. 21 ciclos em três janelas de sete: número pequeno o
// bastante para o professor ler a tabela inteira na lousa antes do intervalo.
const int INTERVALO_CICLO = 1500;
const int TOTAL_DE_CICLOS = 21;

// Limite de plausibilidade introduzido no dia 8. Fora desta faixa o dado é lixo
// e não entra no banco, mesmo que a placa "conseguiu" enviar.
const float TEMP_MINIMA = -10.0;
const float TEMP_MAXIMA = 60.0;

// Tamanho do buffer do JSON que a placa monta, igual ao dia 3.
const int TAMANHO_PAYLOAD = 96;

// --- Contadores do caminho. Ficam no topo, fora do `loop`, de propósito: um
// contador que nasce dentro do `loop` é a aula 2 do dia 15 inteira. ---
int ciclo = 0;
int leiturasTentadas = 0;
int leiturasGravadas = 0;
int falhasSemSinal = 0;
int falhasServidor = 0;
int falhasForaDoLimite = 0;
int tempoAcumulado = 0;
int tempoPiorCaso = 0;
int cicloPiorCaso = 0;

// Simula a leitura do DHT11. O valor 0 é um defeito real do sensor: quando a
// leitura falha, o DHT11 devolve zero em vez de erro. É por isso que existe
// limite de plausibilidade.
float temperaturaSimulada() {
  if (ciclo % 11 == 0) {
    return 0.0;
  }
  return 21.0 + ((ciclo % 9) * 2.0);
}

// Devolve o motivo da falha do ciclo, ou 0 quando o ciclo passa inteiro.
// A ordem importa e é o conteúdo da aula: o caminho quebra no PRIMEIRO ponto,
// não em todos ao mesmo tempo.
int motivoDaFalha(float temperatura) {
  if (temperatura < TEMP_MINIMA || temperatura > TEMP_MAXIMA) {
    return 3;  // 422: o dado chegou, mas era lixo
  }
  if (ciclo % 7 == 0) {
    return 1;  // a placa não achou o ponto de acesso
  }
  if (ciclo % 5 == 0) {
    return 2;  // 500: o servidor recebeu e não conseguiu gravar
  }
  return 0;
}

const char* nomeDoMotivo(int motivo) {
  if (motivo == 1) {
    return "FALHA sem sinal";
  }
  if (motivo == 2) {
    return "FALHA 500 servidor";
  }
  if (motivo == 3) {
    return "FALHA fora do limite";
  }
  return "gravada";
}

// Tempo de ida e volta da rede. Simulado, como todo o resto: o valor é
// determinístico para que o pior caso caia sempre no mesmo ciclo.
int tempoDeRedeSimulado() {
  return 90 + ((ciclo % 6) * 40);
}

// O JSON que a placa monta no dia 3. Montar a string é trabalho real e medido
// pelo `millis`; só a rede é simulada.
void montarPayload(float temperatura, char* destino) {
  snprintf(destino, TAMANHO_PAYLOAD,
           "{\"id\":\"placa-01\",\"temperatura\":%.1f}", temperatura);
}

// Recomeça a contagem para o aluno ver o relatório inteiro mais de uma vez
// sem precisar gravar o sketch de novo.
void zerarContadores() {
  ciclo = 0;
  leiturasTentadas = 0;
  leiturasGravadas = 0;
  falhasSemSinal = 0;
  falhasServidor = 0;
  falhasForaDoLimite = 0;
  tempoAcumulado = 0;
  tempoPiorCaso = 0;
  cicloPiorCaso = 0;
}

void mostrarResumo() {
  int perdas = TOTAL_DE_CICLOS - leiturasGravadas;
  int percentual = (TOTAL_DE_CICLOS > 0) ? (100 * leiturasGravadas / TOTAL_DE_CICLOS) : 0;
  int media = (leiturasGravadas > 0) ? (tempoAcumulado / leiturasGravadas) : 0;

  Serial.println();
  Serial.println("=== RESUMO DO CAMINHO ===");
  Serial.print("leituras tentadas .. ");
  Serial.println(leiturasTentadas);
  Serial.print("leituras gravadas ... ");
  Serial.println(leiturasGravadas);
  Serial.print("leituras perdidas ... ");
  Serial.print(perdas);
  Serial.print("  (");
  Serial.print(percentual);
  Serial.println(" % de aproveitamento)");
  Serial.print("tempo medio ........ ");
  Serial.print(media);
  Serial.println(" ms por leitura");
  Serial.print("pior caso ......... ");
  Serial.print(tempoPiorCaso);
  Serial.print(" ms, no ciclo ");
  Serial.println(cicloPiorCaso);
}

// A conferência que fecha o diagnóstico: se estas três contas não baterem, o
// erro é de CONTAGEM, e não do sistema. É esta linha que acha o defeito da
// aula 2.
void conferirContas() {
  int perdas = TOTAL_DE_CICLOS - leiturasGravadas;
  Serial.println();
  Serial.print("conferencia: ");
  Serial.print(leiturasTentadas);
  Serial.print(" = ");
  Serial.print(leiturasGravadas);
  Serial.print(" + ");
  Serial.print(perdas);
  Serial.println(" ?");
  if (leiturasTentadas != leiturasGravadas + perdas) {
    Serial.println("  >>> AS CONTAS NAO FECHAM. O erro esta na contagem.");
  } else {
    Serial.println("  >>> contas fecham: o caminho estainteiro, e o sistema.");
  }
}

// O diagnóstico que o aluno lê para decidir por onde começar a consertar.
void mostrarDiagnostico() {
  Serial.println();
  Serial.println("=== ONDE O CAMINHO QUEBROU ===");
  Serial.print("sensor e validacao . ");
  Serial.println(falhasForaDoLimite);
  Serial.print("sinal de rede ....... ");
  Serial.println(falhasSemSinal);
  Serial.print("servidor e banco .... ");
  Serial.println(falhasServidor);

  int maior = falhasSemSinal;
  const char* etapa = "sinal de rede";
  if (falhasServidor > maior) {
    maior = falhasServidor;
    etapa = "servidor e banco";
  }
  if (falhasForaDoLimite > maior) {
    maior = falhasForaDoLimite;
    etapa = "sensor e validacao";
  }

  Serial.println();
  if (maior == 0) {
    Serial.println("Nenhuma etapa perdeu leitura neste ciclo completo.");
    return;
  }
  Serial.print("Comece por aqui: ");
  Serial.print(etapa);
  Serial.print(", com ");
  Serial.print(maior);
  Serial.println(" perdas.");
}

void setup() {
  pinMode(PIN_LED, OUTPUT);

  Serial.begin(115200);
  Serial.println();
  Serial.println("DIAGNOSTICO DO CAMINHO — dia 15, aula 1");
  Serial.println("leituras e rede simuladas, deterministicas");
  Serial.println("---------------------------------------------------");
  Serial.println("ciclo | temperatura | resultado           | tempo");
}

void loop() {
  ciclo++;
  digitalWrite(PIN_LED, HIGH);  // sinal de vida: o ciclo começou

  float temperatura = temperaturaSimulada();
  int motivo = motivoDaFalha(temperatura);
  int rede = tempoDeRedeSimulado();

  char payload[TAMANHO_PAYLOAD];
  montarPayload(temperatura, payload);

  unsigned long inicio = millis();

  Serial.print(ciclo);
  Serial.print("     | ");
  Serial.print(temperatura, 1);
  Serial.print(" C       | ");

  if (motivo == 0) {
    leiturasTentadas++;
    leiturasGravadas++;
    Serial.print(nomeDoMotivo(motivo));
    // O payload só é mostrado no primeiro ciclo inteiro: serve de prova de que
    // a string que a placa manda é o JSON do dia 3, e polui se repetir 21 vezes.
    if (leiturasGravadas == 1) {
      Serial.print("  | ");
      Serial.print(payload);
    }
  } else {
    // A leitura também conta como tentativa: ela saiu da bancada, tentou
    // entrar e foi recusada. Perder é diferente de nem ter tentado.
    leiturasTentadas++;
    if (motivo == 1) {
      falhasSemSinal++;
    } else if (motivo == 2) {
      falhasServidor++;
    } else {
      falhasForaDoLimite++;
    }
    Serial.print(nomeDoMotivo(motivo));
  }

  int gasto = (int)(millis() - inicio);
  tempoAcumulado += gasto;
  if (gasto > tempoPiorCaso) {
    tempoPiorCaso = gasto;
    cicloPiorCaso = ciclo;
  }

  Serial.print("  | ");
  Serial.print(gasto);
  Serial.print(" ms + ");
  Serial.print(rede);
  Serial.println(" ms de rede");

  if (ciclo >= TOTAL_DE_CICLOS) {
    mostrarResumo();
    conferirContas();
    mostrarDiagnostico();
    Serial.println();
    Serial.println("O relatorio comeca de novo em alguns segundos.");
    zerarContadores();
    delay(5000);
  }

  delay(INTERVALO_CICLO);
  digitalWrite(PIN_LED, LOW);
}

Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia15 aula1.

Aula 2 — Aula de duvidas: o que a turma não conseguiu fazer

Objetivos

  • Reproduzir o seu erro na frente da turma, do começo ao fim, sem pular etapa nenhuma.
  • Reduzir o caso até sobrar um caminho só, uma variável e um erro.
  • Explicar para o colega o que você fez para achar a causa, em três frases, sem mostrar o código.
  • Registrar o erro no quadro de erros da turma, escrito como o aluno escreve e não como o professor fala.

Material

  • 1 ESP32 DevKit V1 por aluno, com o cabo USB
  • Computador com o projeto do trimestre aberto
  • Quadro branco limpo na frente da sala, com caneta
  • Projetor, para quem estiver com a placa sem resposta
  • Folha de listas da aula 1, uma por aluno

Conceitos

Depurar é reduzir, não procurar

O aluno acredita que depurar é "achar o erro". Não é. Depurar é tirar tudo até sobrar pouco: o caminho mínimo que ainda falha. Enquanto a placa, o servidor e o banco estão juntos na tela, existem dezenas de coisas que podem estar erradas, e o olho do aluno vai em todas elas ao mesmo tempo.

O caminho de redução que funciona nesta ordem:

  1. Qual etapa quebra? A placa imprime em qual ponto o dado parou. Isso corta cinco possibilidades para uma.
  2. Qual linha? O número da linha da resposta do servidor, ou a mensagem do erro do banco.
  3. Qual variável? O valor que entrou no lugar do que deveria ter entrado.
  4. Qual trecho? Meia dúzia de linhas que produzem esse valor errado.

Cada passo corta as possibilidades. Quem tenta pular direto do passo 1 para o passo 4 fica duas horas olhando o mesmo trecho.

O caso mínimo é o que se ensina

O caso mínimo é a menor explicação que ainda reproduz o erro. Se o aluno tem um projeto de trezentas linhas, o caso mínimo dele tem vinte. E essas vinte linhas são o que vai virar exemplo na próxima turma, no material e na aula de quem vier depois.

É por isso que o professor pede para reduzir em voz alta, na frente: o aluno que consegue reduzir o caso aprendeu, e o aluno que explica para o colega aprendeu duas vezes.

Explicar é o teste

O aluno diz "eu entendi" e não sabe explicar. Essa frase não vale nada. O teste é objetivo: explique para o colega o que você fez, sem mostrar o código, em três frases. Se o aluno precisa apontar a tela para explicar, ele não entendeu ainda.

Explicar para o outro é também o que transforma um erro individual em material de aula. O que a turma descobre hoje é o que entra no quadro e, depois, na próxima turma.

O erro é escrito como o aluno escreve

A lista de erros do quadro é escrita com as palavras do aluno: minha variavel ta dando null, o banco ta desconectado, ele fica travando. Reescrever isso em linguagem técnica esconde o motivo pelo qual o erro aconteceu, que foi o aluno não saber o que procurava.

O erro técnico é o mesmo para todo mundo. O que trava a turma é sempre a mesma coisa com quatro palavras diferentes.

Atividade

Montagem: a bancada fica como estiver. O projeto de cada aluno roda como está; nada se desmonta hoje, e a placa não precisa nem estar conectada quando a dúvida não for da placa.

  1. Escreva no quadro a sua dúvida em uma linha, com as suas palavras. Sem nome de função ainda.
  2. O professor escolhe uma das linhas e você reproduz o erro na frente da turma, do começo ao fim. Sem pular nenhuma etapa, mesmo a que você já sabe que não é a problema.
  3. A turma caça junto: cada um escreve um palpite e um número de linha no caderno, antes de o professor falar qualquer coisa.
  4. Reduza o caso com o professor até sobrar um caminho só: uma etapa, uma variável, um trecho de poucas linhas que ainda falha sozinho.
  5. Explique para o seu colega, em três frases e sem mostrar o código, o que você descobriu. Ele faz uma pergunta no fim e você responde.
  6. Escreva o seu caso no quadro: como o erro aparece na tela, e o que era a causa real.

Nota: 10 pontos. Critério de fim: o erro reproduzido na frente até a tela mostrar a falha, o caso mínimo escrito no quadro e a explicação de três frases dada ao colega. Quem só assistiu tira zero: esta aula não tem plateia, tem vez.

Resolucao

O sketch de hoje é a bancada de caça ao erro. A turma digita uma letra no monitor serial e a placa executa um pedaço do caminho — sensor, JSON, rede, servidor, banco — e para no ponto que o aluno mandou quebrar.

O que importa é que a placa não decide o que falha: a letra digitada decide. É assim que o caso mínimo funciona na prática — o erro é escolhido, não sorteado, e por isso a turma consegue ver o mesmo erro duas vezes na mesma aula, em placas diferentes.

// DIA 15, AULA 2 — a bancada de caça ao erro.
//
// Não é exemplo de estilo: é o programa que o professor usa enquanto o aluno
// reproduz o erro na frente. A turma digita uma letra no monitor serial e a
// placa executa UM pedaço do caminho — sensor, JSON, rede, servidor, banco.
// A letra que trava diz em qual etapa o caminho do aluno morre.
//
// AVISO PARA O PROFESSOR: não tem sensor, não tem servidor e não tem banco
// ligados. Cada etapa é simulada de forma determinística, e a etapa que
// "falha" é escolhida pela LETRA digitada, não pelo acaso. É assim que o
// caso mínimo funciona: uma variável, um caminho, um erro.

#include <Arduino.h>

// GPIO2 é o LED embutido. Ele pisca só na etapa pedida: assim a turma vê
// qual trecho do caminho rodou de verdade.
const int PIN_LED = 2;

// Espera entre as linhas digitadas. O aluno tem tempo de ler antes de a placa
// imprimir o relatório inteiro.
const int INTERVALO_ETAPA = 600;

const int TAMANHO_PAYLOAD = 96;

// Tempo simulado de cada trecho. Servem para o professor mostrar que o mesmo
// dado leva 30 ms numa etapa e 900 ms na seguinte, e que a lentidão tem nome.
const int TEMPO_NO_SENSOR = 30;
const int TEMPO_NO_SERIAL = 8;
const int TEMPO_NA_REDE = 420;
const int TEMPO_NO_SERVIDOR = 180;
const int TEMPO_NO_BANCO = 260;

// A etapa que vai falhar, controlada pela letra que o aluno digitar.
char etapaQuebrada = 'n';

float temperaturaDoCiclo() {
  // 21,5 C: valor estável, longe dos limites. Numa caça ao erro, o dado tem
  // de estar bom; se o dado também está ruim, são dois problemas ao mesmo
  // tempo e a turma não enxerga nenhum.
  return 21.5;
}

void piscar(int vezes) {
  for (int i = 0; i < vezes; i++) {
    digitalWrite(PIN_LED, HIGH);
    delay(120);
    digitalWrite(PIN_LED, LOW);
    delay(120);
  }
}

void cabecalho(char etapa, const char* nome) {
  Serial.println();
  Serial.print("--- ETAPA ");
  Serial.print(etapa);
  Serial.print(": ");
  Serial.println(nome);
  Serial.println("---------------------------------------------------");
}

// --- As cinco etapas. Cada uma e isolada de propósito: e isso que permite
// --- reproduzir o erro sem arrastar o resto do caminho junto. ---

// ETAPA S: a placa lê o sensor. É a única que existe de verdade no HW.
bool etapaSensor() {
  cabecalho('S', "sensor — leitura do DHT11");
  float temperatura = temperaturaDoCiclo();
  Serial.print("leitura bruta: ");
  Serial.print(temperatura, 1);
  Serial.println(" C");
  Serial.print("tempo no sensor: ");
  Serial.print(TEMPO_NO_SENSOR);
  Serial.println(" ms");
  Serial.print("status do sensor: ");
  Serial.println(temperatura == 0.0 ? "ERRO (devolveu zero)" : "ok");
  piscar(1);
  if (etapaQuebrada == 'S') {
    Serial.println(">>> QUEBROU AQUI: a leitura nao saiu do sensor.");
    return false;
  }
  Serial.println("etapa passou.");
  return true;
}

// ETAPA J: a placa monta o JSON. É o dia 3, sem mudar nada.
bool etapaJson() {
  cabecalho('J', "json — montar o corpo da requisição");
  char payload[TAMANHO_PAYLOAD];
  snprintf(payload, TAMANHO_PAYLOAD,
           "{\"id\":\"placa-01\",\"temperatura\":%.1f}", temperaturaDoCiclo());
  Serial.println("payload: ");
  Serial.println(payload);
  Serial.print("tamanho: ");
  Serial.print(strlen(payload));
  Serial.println(" bytes");
  Serial.print("tempo no serial: ");
  Serial.print(TEMPO_NO_SERIAL);
  Serial.println(" ms");
  piscar(1);
  if (etapaQuebrada == 'J') {
    Serial.println(">>> QUEBROU AQUI: o JSON saiu torto ou vazio.");
    return false;
  }
  Serial.println("etapa passou.");
  return true;
}

// ETAPA W: a placa procura o ponto de acesso e envia. É o dia 3 e o dia 4.
bool etapaRede() {
  cabecalho('W', "rede — procurar o ponto de acesso e enviar");
  Serial.println("SSID da sala: rede-da-sala   (nome ficticio, senha ficticia)");
  Serial.println("status: conectado, sinal -58 dBm");
  Serial.print("tempo gasto na rede: ");
  Serial.print(TEMPO_NA_REDE);
  Serial.println(" ms");
  piscar(2);
  if (etapaQuebrada == 'W') {
    Serial.println(">>> QUEBROU AQUI: a placa nao achou o ponto de acesso, ou o "
                   "servidor nao respondeu.");
    return false;
  }
  Serial.println("etapa passou.");
  return true;
}

// ETAPA V: o servidor recebe e decide. É o dia 8: valida antes de gravar.
bool etapaServidor() {
  cabecalho('V', "servidor — receber e validar");
  Serial.println("POST /leituras  ->  201 Created");
  Serial.print("tempo no servidor: ");
  Serial.print(TEMPO_NO_SERVIDOR);
  Serial.println(" ms");
  piscar(2);
  if (etapaQuebrada == 'V') {
    Serial.println(">>> QUEBROU AQUI: resposta 422, o dado foi recusado.");
    return false;
  }
  Serial.println("etapa passou.");
  return true;
}

// ETAPA B: o banco grava. É o dia 5 e o dia 6.
bool etapaBanco() {
  cabecalho('B', "banco — gravar a linha");
  Serial.println("INSERT INTO leituras (placa_id, temperatura) VALUES (...)");
  Serial.print("tempo no banco: ");
  Serial.print(TEMPO_NO_BANCO);
  Serial.println(" ms");
  Serial.println("linhas na tabela: 1284");
  Serial.print("indice em (placa_id, criada_em): ");
  Serial.println("usado");
  piscar(3);
  if (etapaQuebrada == 'B') {
    Serial.println(">>> QUEBROU AQUI: o banco recusou a gravacao.");
    return false;
  }
  Serial.println("etapa passou.");
  return true;
}

// Roda o caminho inteiro a partir de uma etapa. Quando o caminho morre, para
// ali e nao tenta as etapas seguintes: e o mesmo comportamento do sistema real,
// e por isso o relatorio nunca mente sobre onde foi o problema.
void rodarCaminho(char etapaInicial) {
  char inicio = etapaInicial;
  char etapa = inicio;

  Serial.println();
  Serial.print("rodando o caminho a partir da etapa ");
  Serial.print(inicio);
  Serial.print(", com a quebra forcada na etapa ");
  Serial.println(etapaQuebrada);

  while (true) {
    bool passou;
    if (etapa == 'S') {
      passou = etapaSensor();
    } else if (etapa == 'J') {
      passou = etapaJson();
    } else if (etapa == 'W') {
      passou = etapaRede();
    } else if (etapa == 'V') {
      passou = etapaServidor();
    } else if (etapa == 'B') {
      passou = etapaBanco();
    } else {
      cabecalho(etapa, "nenhuma etapa com essa letra");
      Serial.println("use S, J, W, V ou B.");
      return;
    }

    if (!passou) {
      Serial.println();
      Serial.println("RESUMO: o caminho morreu na etapa que voce pediu. "
                     "As etapas depois nunca chegaram a rodar.");
      Serial.println("E agora a turma corrige essa etapa e repete a letra.");
      return;
    }

    etapa++;
    if (etapa == 'X') {
      Serial.println();
      Serial.println("RESUMO: o caminho inteiro passou com a quebra forcada "
                     "em outra etapa.");
      Serial.println("Se o sistema do aluno falha e aqui passou, o defeito "
                     "esta no ambiente dele, e nao no caminho.");
      return;
    }
  }
}

void mostrarAjuda() {
  Serial.println();
  Serial.println("CACA AO ERRO — digite uma letra e aperte Enter:");
  Serial.println("  S = sensor        (DHT11)");
  Serial.println("  J = json          (corpo da requisicao)");
  Serial.println("  W = rede          (procurar a rede e enviar)");
  Serial.println("  V = servidor      (receber e validar)");
  Serial.println("  B = banco         (gravar a linha)");
  Serial.println("  T = caminho todo, sem forcar quebra");
  Serial.println("  Q = sair");
}

void setup() {
  pinMode(PIN_LED, OUTPUT);

  Serial.begin(115200);
  Serial.println();
  Serial.println("DIA 15, AULA 2 — cacando o erro na frente da turma");
  Serial.println("---------------------------------------------------");
  Serial.println("Simulado e deterministico: cada etapa e testada sozinha.");
  mostrarAjuda();
}

void loop() {
  if (!Serial.available()) {
    // Espera o comando sem `delay` grande: com `delay`, a placa não escuta o
    // teclado e o aluno acha que o monitor travou. Foi o defeito do dia 3 que
    // trocou `delay` por espera não bloqueante, e a razão de ele ter saído do
    // lugar fixo nas aulas seguintes.
    delay(50);
    return;
  }

  String comando = Serial.readStringUntil('\n');
  comando.trim();
  if (comando.length() == 0) {
    return;
  }

  char letra = (char)toupper(comando.charAt(0));
  etapaQuebrada = 'n';

  if (letra == 'Q') {
    Serial.println("Ate a proxima.");
    return;
  }

  if (letra == 'T') {
    etapaQuebrada = 'n';
    rodarCaminho('S');
    mostrarAjuda();
    return;
  }

  if (letra == 'S' || letra == 'J' || letra == 'W' || letra == 'V' || letra == 'B') {
    etapaQuebrada = letra;
    // Começa uma etapa antes da que foi pedida, para a turma ver o trecho
    // anterior funcionando e saber que o caso mínimo não é a placa inteira.
    char anterior = (letra == 'S') ? 'S' : (char)(letra - 1);
    rodarCaminho(anterior);
    mostrarAjuda();
    return;
  }

  cabecalho(letra, "comando desconhecido");
  mostrarAjuda();
}

Por que assim e não de outro jeito. Cada etapa é uma função separada que devolve true ou false, e o while caminha de uma para a outra. A alternativa óbvia seria um switch com o caminho inteiro dentro de cada caso, e ela é pior: quando a etapa V falha, um switch gigante ou continua rodando e imprime "banco ok" depois do erro — mentira na tela — ou o professor passa a aula inteira caçando o return certo.

A letra T existe para o caso do aluno cujo sistema funciona: o caminho inteiro passa, e a resposta é "o defeito está no ambiente dele, e não no caminho". Sem essa letra, a turma passa a aula inteira caçando erro em sistema que funciona.

O delay(50) no loop em vez de um delay grande no fim do laço é o que permite que a placa leia o teclado. É a mesma lição do dia 3, aplicada: a placa não pode passar a vida parada esperando.

Criterios de correcao

CritérioPontos
Erro reproduzido na frente da turma do começo ao fim, sem pular etapa3 pontos
Palpite escrito no caderno antes de qualquer fala do professor2 pontos
Caso mínimo no quadro: uma etapa, uma variável, um trecho3 pontos
Explicação de três frases ao colega, respondendo a uma pergunta2 pontos

Erros comuns

ATENÇÃO, PROFESSOR: esta seção começa vazia e é preenchida por Você depois da aula, com o que aconteceu de verdade. O quadro de erros montado no bloco de 6 minutos do roteiro é exatamente o conteúdo desta tabela. Escreva cada linha com as palavras do aluno, não com as suas — é assim que o erro fica encontrável por quem vai errar do mesmo jeito no ano que vem. Nenhuma linha desta tabela é de exemplo: as cinco que já estão são os erros de estrutura desta aula, e o resto é da turma.

ErroComo apareceCorrecao
Reproduzir "só o pedaço que eu acho que é o problema", pulando o restoO aluno reproduz o defeito, mas ninguém sabe se o resto do caminho também quebrava"Do começo. Se você pular a etapa da rede, eu não sei se o seu erro era de rede. Roda inteiro, mesmo a parte que você tem certeza que está boa."
Achar que o caso mínimo é a tela com o erroO aluno aponta a mensagem do navegador e diz "o caso é esse""A tela é onde o erro aparece, não é o caso. Reduz até sobrar um trecho de vinte linhas que falha sozinho, sem placa e sem banco. Esse é o caso."
Explicar com o dedo na tela"Aqui ó, ó, ó, ó" e o colega não entende nada"Sem apontar. Se você precisa apontar, você ainda não sabe o que está acontecendo. Explique a ideia, não a posição."
Trocar de erro no meio da explicaçãoO aluno começa falando de validação e termina falando de rede"Não. Primeiro termina esse. Se tem dois erros, a gente anota os dois no quadro e resolve um por aula."
"Não funciona aqui no meu computador" como resposta à caça ao erroO aluno recusa o caso mínimo porque no notebook dele funciona"Se no seu funciona e aqui falha, o que muda é o ambiente, e aí é outro problema. Mas a gente precisa do caso primeiro: roda aqui até falhar."

Desafio extra

Sem o professor: pegue a última linha que a turma escreveu no quadro e escreva o teste que pegaria aquele erro antes de ele acontecer. Um teste por erro, e ele tem que falhar quando você reintroduz o defeito de propósito. Se o teste passa com o defeito colado no código, o teste não está pegando o erro, e você descobriu isso na hora — que é a mesma hora em que o teste vai salvar o projeto no 3o trimestre.

>

A resolucao, compilada

// DIA 15, AULA 2 — a bancada de caça ao erro.
//
// Não é exemplo de estilo: é o programa que o professor usa enquanto o aluno
// reproduz o erro na frente. A turma digita uma letra no monitor serial e a
// placa executa UM pedaço do caminho — sensor, JSON, rede, servidor, banco.
// A letra que trava diz em qual etapa o caminho do aluno morre.
//
// AVISO PARA O PROFESSOR: não tem sensor, não tem servidor e não tem banco
// ligados. Cada etapa é simulada de forma determinística, e a etapa que
// "falha" é escolhida pela LETRA digitada, não pelo acaso. É assim que o
// caso mínimo funciona: uma variável, um caminho, um erro.

#include <Arduino.h>

// GPIO2 é o LED embutido. Ele pisca só na etapa pedida: assim a turma vê
// qual trecho do caminho rodou de verdade.
const int PIN_LED = 2;

// Espera entre as linhas digitadas. O aluno tem tempo de ler antes de a placa
// imprimir o relatório inteiro.
const int INTERVALO_ETAPA = 600;

const int TAMANHO_PAYLOAD = 96;

// Tempo simulado de cada trecho. Servem para o professor mostrar que o mesmo
// dado leva 30 ms numa etapa e 900 ms na seguinte, e que a lentidão tem nome.
const int TEMPO_NO_SENSOR = 30;
const int TEMPO_NO_SERIAL = 8;
const int TEMPO_NA_REDE = 420;
const int TEMPO_NO_SERVIDOR = 180;
const int TEMPO_NO_BANCO = 260;

// A etapa que vai falhar, controlada pela letra que o aluno digitar.
char etapaQuebrada = 'n';

float temperaturaDoCiclo() {
  // 21,5 C: valor estável, longe dos limites. Numa caça ao erro, o dado tem
  // de estar bom; se o dado também está ruim, são dois problemas ao mesmo
  // tempo e a turma não enxerga nenhum.
  return 21.5;
}

void piscar(int vezes) {
  for (int i = 0; i < vezes; i++) {
    digitalWrite(PIN_LED, HIGH);
    delay(120);
    digitalWrite(PIN_LED, LOW);
    delay(120);
  }
}

void cabecalho(char etapa, const char* nome) {
  Serial.println();
  Serial.print("--- ETAPA ");
  Serial.print(etapa);
  Serial.print(": ");
  Serial.println(nome);
  Serial.println("---------------------------------------------------");
}

// --- As cinco etapas. Cada uma e isolada de propósito: e isso que permite
// --- reproduzir o erro sem arrastar o resto do caminho junto. ---

// ETAPA S: a placa lê o sensor. É a única que existe de verdade no HW.
bool etapaSensor() {
  cabecalho('S', "sensor — leitura do DHT11");
  float temperatura = temperaturaDoCiclo();
  Serial.print("leitura bruta: ");
  Serial.print(temperatura, 1);
  Serial.println(" C");
  Serial.print("tempo no sensor: ");
  Serial.print(TEMPO_NO_SENSOR);
  Serial.println(" ms");
  Serial.print("status do sensor: ");
  Serial.println(temperatura == 0.0 ? "ERRO (devolveu zero)" : "ok");
  piscar(1);
  if (etapaQuebrada == 'S') {
    Serial.println(">>> QUEBROU AQUI: a leitura nao saiu do sensor.");
    return false;
  }
  Serial.println("etapa passou.");
  return true;
}

// ETAPA J: a placa monta o JSON. É o dia 3, sem mudar nada.
bool etapaJson() {
  cabecalho('J', "json — montar o corpo da requisição");
  char payload[TAMANHO_PAYLOAD];
  snprintf(payload, TAMANHO_PAYLOAD,
           "{\"id\":\"placa-01\",\"temperatura\":%.1f}", temperaturaDoCiclo());
  Serial.println("payload: ");
  Serial.println(payload);
  Serial.print("tamanho: ");
  Serial.print(strlen(payload));
  Serial.println(" bytes");
  Serial.print("tempo no serial: ");
  Serial.print(TEMPO_NO_SERIAL);
  Serial.println(" ms");
  piscar(1);
  if (etapaQuebrada == 'J') {
    Serial.println(">>> QUEBROU AQUI: o JSON saiu torto ou vazio.");
    return false;
  }
  Serial.println("etapa passou.");
  return true;
}

// ETAPA W: a placa procura o ponto de acesso e envia. É o dia 3 e o dia 4.
bool etapaRede() {
  cabecalho('W', "rede — procurar o ponto de acesso e enviar");
  Serial.println("SSID da sala: rede-da-sala   (nome ficticio, senha ficticia)");
  Serial.println("status: conectado, sinal -58 dBm");
  Serial.print("tempo gasto na rede: ");
  Serial.print(TEMPO_NA_REDE);
  Serial.println(" ms");
  piscar(2);
  if (etapaQuebrada == 'W') {
    Serial.println(">>> QUEBROU AQUI: a placa nao achou o ponto de acesso, ou o "
                   "servidor nao respondeu.");
    return false;
  }
  Serial.println("etapa passou.");
  return true;
}

// ETAPA V: o servidor recebe e decide. É o dia 8: valida antes de gravar.
bool etapaServidor() {
  cabecalho('V', "servidor — receber e validar");
  Serial.println("POST /leituras  ->  201 Created");
  Serial.print("tempo no servidor: ");
  Serial.print(TEMPO_NO_SERVIDOR);
  Serial.println(" ms");
  piscar(2);
  if (etapaQuebrada == 'V') {
    Serial.println(">>> QUEBROU AQUI: resposta 422, o dado foi recusado.");
    return false;
  }
  Serial.println("etapa passou.");
  return true;
}

// ETAPA B: o banco grava. É o dia 5 e o dia 6.
bool etapaBanco() {
  cabecalho('B', "banco — gravar a linha");
  Serial.println("INSERT INTO leituras (placa_id, temperatura) VALUES (...)");
  Serial.print("tempo no banco: ");
  Serial.print(TEMPO_NO_BANCO);
  Serial.println(" ms");
  Serial.println("linhas na tabela: 1284");
  Serial.print("indice em (placa_id, criada_em): ");
  Serial.println("usado");
  piscar(3);
  if (etapaQuebrada == 'B') {
    Serial.println(">>> QUEBROU AQUI: o banco recusou a gravacao.");
    return false;
  }
  Serial.println("etapa passou.");
  return true;
}

// Roda o caminho inteiro a partir de uma etapa. Quando o caminho morre, para
// ali e nao tenta as etapas seguintes: e o mesmo comportamento do sistema real,
// e por isso o relatorio nunca mente sobre onde foi o problema.
void rodarCaminho(char etapaInicial) {
  char inicio = etapaInicial;
  char etapa = inicio;

  Serial.println();
  Serial.print("rodando o caminho a partir da etapa ");
  Serial.print(inicio);
  Serial.print(", com a quebra forcada na etapa ");
  Serial.println(etapaQuebrada);

  while (true) {
    bool passou;
    if (etapa == 'S') {
      passou = etapaSensor();
    } else if (etapa == 'J') {
      passou = etapaJson();
    } else if (etapa == 'W') {
      passou = etapaRede();
    } else if (etapa == 'V') {
      passou = etapaServidor();
    } else if (etapa == 'B') {
      passou = etapaBanco();
    } else {
      cabecalho(etapa, "nenhuma etapa com essa letra");
      Serial.println("use S, J, W, V ou B.");
      return;
    }

    if (!passou) {
      Serial.println();
      Serial.println("RESUMO: o caminho morreu na etapa que voce pediu. "
                     "As etapas depois nunca chegaram a rodar.");
      Serial.println("E agora a turma corrige essa etapa e repete a letra.");
      return;
    }

    etapa++;
    if (etapa == 'X') {
      Serial.println();
      Serial.println("RESUMO: o caminho inteiro passou com a quebra forcada "
                     "em outra etapa.");
      Serial.println("Se o sistema do aluno falha e aqui passou, o defeito "
                     "esta no ambiente dele, e nao no caminho.");
      return;
    }
  }
}

void mostrarAjuda() {
  Serial.println();
  Serial.println("CACA AO ERRO — digite uma letra e aperte Enter:");
  Serial.println("  S = sensor        (DHT11)");
  Serial.println("  J = json          (corpo da requisicao)");
  Serial.println("  W = rede          (procurar a rede e enviar)");
  Serial.println("  V = servidor      (receber e validar)");
  Serial.println("  B = banco         (gravar a linha)");
  Serial.println("  T = caminho todo, sem forcar quebra");
  Serial.println("  Q = sair");
}

void setup() {
  pinMode(PIN_LED, OUTPUT);

  Serial.begin(115200);
  Serial.println();
  Serial.println("DIA 15, AULA 2 — cacando o erro na frente da turma");
  Serial.println("---------------------------------------------------");
  Serial.println("Simulado e deterministico: cada etapa e testada sozinha.");
  mostrarAjuda();
}

void loop() {
  if (!Serial.available()) {
    // Espera o comando sem `delay` grande: com `delay`, a placa não escuta o
    // teclado e o aluno acha que o monitor travou. Foi o defeito do dia 3 que
    // trocou `delay` por espera não bloqueante, e a razão de ele ter saído do
    // lugar fixo nas aulas seguintes.
    delay(50);
    return;
  }

  String comando = Serial.readStringUntil('\n');
  comando.trim();
  if (comando.length() == 0) {
    return;
  }

  char letra = (char)toupper(comando.charAt(0));
  etapaQuebrada = 'n';

  if (letra == 'Q') {
    Serial.println("Ate a proxima.");
    return;
  }

  if (letra == 'T') {
    etapaQuebrada = 'n';
    rodarCaminho('S');
    mostrarAjuda();
    return;
  }

  if (letra == 'S' || letra == 'J' || letra == 'W' || letra == 'V' || letra == 'B') {
    etapaQuebrada = letra;
    // Começa uma etapa antes da que foi pedida, para a turma ver o trecho
    // anterior funcionando e saber que o caso mínimo não é a placa inteira.
    char anterior = (letra == 'S') ? 'S' : (char)(letra - 1);
    rodarCaminho(anterior);
    mostrarAjuda();
    return;
  }

  cabecalho(letra, "comando desconhecido");
  mostrarAjuda();
}

Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia15 aula2.