Projeto final do ano — Arduino e IoT — semana 12 do 3o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 12 · Projeto final do ano — Material de Apoio Arduino

Semana 12 de 15· 3o trimestre · 05/09 a 10/12

Projeto final do ano

A estacao meteorologica vira produto com conta, historico, alerta e observabilidade.

Aula 1 — Projeto final: sistema completo com login e historico

Objetivos

  • Fechar o projeto final do ano como um sistema inteiro, com login, histórico, painel e alerta funcionando juntos.
  • Escrever o cadastro de usuário e a sessão que sobrevive ao reinicio, e saber o que a placa recebe de cada uma.
  • Montar o gráfico histórico do painel a partir das três consultas, em vez de consultar o banco a cada quadro.
  • Escrever o log de observabilidade que responde uma pergunta concreta pelo identificador do dispositivo.
  • Entregar o projeto como entregável: o que o cliente recebe, o arquivo que mora cada requisito, e o README que sobrevive sem você do lado.

Material

  • 1 ESP32 DevKit V1 por aluno, com o cabo USB e o monitor serial em 115200
  • 1 projeto do ano com servidor, banco e painel já rodando
  • Terminal com o MySQL do projeto, o git log e o npm start
  • Folha de checklist de entrega impressa, com as duas colunas: requisito e onde mora
  • Lousa para escrever os quatro casos da fronteira do alerta

Conceitos

Cadastro de usuário: a senha entra em hash e não volta mais

O cadastro de usuário é a primeira coisa que separa projeto de demonstração, e ele é inteiro do lado do servidor. A placa não cadastra ninguém: a placa recebe um token depois que alguém já se cadastrou e logou.

Três decisões do cadastro, e as três são checáveis em trinta segundos:

DecisãoO que o professor procuraDefeito quando está errado
a senha entra em hash com sal por usuáriodois usuários com a mesma senha têm hash diferentea tabela de senha parece correta e é idêntica linha a linha
o hash nunca volta para o clienteo cadastro responde id e nome, e nada maisa resposta de cadastro devolve o campo da senha
a sessão tem validade e destinoo logout invalida no servidor, não no navegadoro menu some, mas a rota continua aceitando o token velho

A senha de verdade nunca chega na placa, e essa é a razão de a placa não precisar dela. Quem autentica é a pessoa, no navegador; o que a placa carrega é o token, que o servidor emite, que expira e que pode ser revogado. Token revogável e senha que não pode ser revogada são coisas diferentes, e a diferença aparece no dia em que a placa do laboratório é perdida.

Por isso a placa guarda o token e esquece a senha: não há motivo para levar a senha de uma pessoa até um dispositivo que fica na beira da bancada. O professor usa essa frase como regra geral de projeto — *o segredo não viaja para onde não precisa estar*.

Gráfico histórico: quem desenha é o navegador

O gráfico histórico é o mesmo dado da tabela, lido três vezes e projetado três vezes. São as três consultas que o painel do projeto roda: as últimas vinte leituras, a média por dispositivo, e os alertas do último dia. As três são a mesma tabela; muda só o que o servidor projeta.

O ponto que derruba o projeto do aluno é onde o desenho acontece. O painel pede uma página de dados, e o navegador desenha a linha. O desenho é do lado do cliente, e o SELECT é do lado do servidor:

Onde a consulta rodaCustoQuando acontece
uma vez no carregamento da páginauma consulta por sessão abertaquando o usuário entra
a cada quadro da animaçãosessenta consultas por segundo de telaenquanto o gráfico se move
uma vez por página pedidauma consulta por troca de páginaquando o usuário troca o dia

O primeiro aluno que abre duas abas já multiplica o custo por dois, e a tabela que não tem índice agenda o resto. Por isso as três coisas andam juntas: índice no banco, página no servidor, desenho no navegador. Custo multiplicado é o defeito silencioso — o sistema funciona na máquina do aluno, com dois dispositivos, e cai na do cliente, com duzentos.

O gráfico histórico também é o que separa alerta de registro: a série histórica é o registro, e o alerta é o furopontinho que passou da linha do limite. Sem a série, o alerta não tem contexto; sem o contexto, o cliente pergunta se o valor estava certo.

Observabilidade: log que responde pergunta

Observabilidade não é "ter log". É conseguir responder uma pergunta sobre o que aconteceu, sem pedir ajuda a ninguém. E toda pergunta de log começa do mesmo jeito: *de qual placa veio isto?*

PerguntaO que a linha de log precisa terO que acontece sem isso
de qual placa veio o alertaidentificador do dispositivoa investigação depende de alguém lembrar da ordem das abas
em que segundo do uptimetempo desde o boot"foi ontem" não localiza nada
com que versão de firmwareversão do firmwarea placa antiga e a nova se confundem no mesmo log
o que o servidor respondeustatus e motivoo 403 vira "não funcionou"
qual requisição é estaidentificador de correlaçãoa linha do alerta não acha a linha do acesso

O identificador de correlação é o que amarra as duas pontas: o navegador gera um identificador quando chama a rota, a placa o repassa, e o servidor devolve o mesmo identificador na resposta e no log. Sem ele, a linha do alerta e a linha do acesso são duas histórias que não se reconhecem.

E vale a regra mais importante desta aula, que vem do dia 8: linha de log não recebe segredo. O token que a placa carrega aparece no log pelo comprimento, nunca pelo valor, e nem por um pedaço do valor. Três caracteres não parecem nada — e log é justamente o arquivo que se copia, se cola em chamado, se anexa em e-mail e se guarda por anos. O critério não é "é só um pedacinho": o critério é *o valor não sai do lugar onde ele tem que sair*.

Entregável e entrega: o que o cliente recebe sem você

A diferença entre entregável e entrega é a diferença entre o artefato existir e o artefato chegar. O entregável é o repositório, a imagem, o firmware gravado, o README — tudo nomeado, tudo com versão. A entrega é o cliente conseguir usar isso sem você do lado, e isso é uma propriedade do projeto, não um esforço de última hora.

Três perguntas que o professor faz no README, e que resolvem a entrega inteira:

  1. o que roda antes do programa? (instalação)
  2. o que o programa faz? (uso)
  3. para rodar, o usuário precisa de que credencial, e de onde ela sai?

A terceira é a que quase nunca está escrita, e é a que trava a instalação. Credencial que não tem origem nomeada é credencial que o cliente chuta, e credencial chutada é credencial vazada.

A entrega do projeto fecha com a frase de firmware e o arquivo que a recebe. A frase de firmware é a versão: ela precisa estar em um lugar só e ser a mesma no código, no README e no que o painel mostra. Quando o cliente pedir "de qual versão é essa placa", a resposta vem da linha de log, e não de memória.

Entregar o projeto também é entregar o que não foi feito. A lista de entrega com um item marcado como não entregue vale mais que a lista toda marcada — porque é o item que ninguém cobra por modem e é o primeiro que o cliente reclama.

Atividade

Montagem: placa com o LED de estado no pino 2 e o botão local no pino 0, com resistor de pull-up e GND. Rode o monitor serial a 115200 e deixe os seis blocos rodarem até o fim: é essa a conferência que o professor faz em dez minutos, sem abrir o repositório.

  1. Escreva, no seu servidor, o cadastro de usuário e o login, com a senha entrando em hash e sal por usuário. Confirme em dois arquivos que o hash do mesmo texto muda entre dois usuários, e que a resposta do cadastro não devolve o campo da senha.
  2. Na sua placa, garanta que o log mostra o token só pelo comprimento: nem o valor, nem um pedaço dele, e nenhuma senha em lugar nenhum do projeto. Escreva onde você procurou para provar que não há.
  3. Escreva as nove respostas de rota: para cada um dos três papéis (leitor, operador, administrador) e para cada uma das três rotas (GET /leituras, POST /configuracao, POST /papeis), anote o status e o motivo. Explique pelo menos um 403 em uma frase.
  4. Escreva a regra de alerta com o limite como número no código, e rode os quatro casos da fronteira: 22.4, 31.7, 29.9 e 30.0. Anote o resultado ao lado do esperado e diga, em uma frase, se o limite é "acima de" ou "a partir de".
  5. Monte o gráfico histórico do painel com as três consultas — últimas vinte leituras, média por dispositivo e alertas do último dia — e diga em qual lugar de cada tela o SELECT acontece. Confirme que abrir duas abas não duplica a consulta a cada quadro.
  6. Escreva, no seu projeto, a frase de firmware e diga qual arquivo a recebe, e por que ela precisa ser a mesma no código, no README e no painel.
  7. Escreva o log de observabilidade com identificador do dispositivo, tempo desde o boot, versão do firmware, status da resposta e identificador de correlação. Escolha uma pergunta concreta sobre o seu projeto e mostre que a linha de log responde ela.
  8. Preencha a lista de entregável com as duas colunas — requisito e onde mora — e deixe visível qual requisito ainda não tem arquivo nomeado.
  9. Entregue o projeto como README: as duas seções, a frase "para rodar, o usuário precisa de", e a lista final com um item marcado como não entregue.

Nota: 12 pontos. Critério de fim: as nove respostas de rota anotadas com um 403 explicado, os quatro casos da fronteira com resultado ao lado do esperado, e o log com o token aparecendo só pelo comprimento.

Resolucao

// Aula 1 do dia 12: projeto final do ano, sistema completo com login e
// historico.
//
// Esta e a placa do PROJETO FINAL. Ela nao e um trecho: e o que o aluno
// entrega. E o unico sketch do ano que junta as sete camadas do curso, e a
// razao de ele ser o mais commentado.
//
// O que a placa faz, em ordem:
//   1. identifica-se com um token (nunca com a senha) e mostra o que o
//      servidor responde para cada papel;
//   2. mede o estado da propria placa (heap, RSSI, uptime) e mostra o que o
//      painel veria;
//   3. grava leituras num historico local, e mostra as tres consultas de
//      painel que o servidor vai rodar em cima dele;
//   4. decide o que e alerta e o que e registro, pelos dois numeros que o
//      professor escreveu na lousa.
//
// O que a placa NAO faz, e o professor explica por que: nao grava senha, nao
// decide permissao, nao e fonte da verdade do historico. Quem decide e o
// servidor. A placa informa; o servidor autoriza e manda.
//
// O que o professor mede no serial: os quatro blocos rodam na ordem a cada
// RESET, e a aula inteira cabe nesses quatro blocos.

#include <Arduino.h>
#include <WiFi.h>
#include <Preferences.h>
#include <esp_timer.h>

// Rede do laboratorio. Os dois valores sao FICTICIOS de proposito: nenhum
// segredo real entra em sketch de exemplo, nem em material, nem no git.
#define WIFI_SSID "lab-esp32-aula"
#define WIFI_PASSWORD "troque-esta-pela-sua"

// Versao do firmware. E a prova de entrega: o painel compara a versao de cada
// placa, e a diferenca entre "atualizado" e "na versao antiga" aparece
// exatamente nesta linha.
#define VERSAO_FIRMWARE "2.0.0"

// Identificador do dispositivo. Este valor viaja no cabecalho de autenticacao
// e no corpo de cada leitura, e e por isso que ele vive em uma constante.
#define ID_DISPOSITIVO "esp32-bancada-01"

// Token de exemplo. FICTICIO, como tudo que aparece em material. A placa
// guarda o TOKEN que o servidor devolveu e esquece a senha — a senha nunca
// saiu do navegador.
#define TOKEN_DE_EXEMPLO "tk-lab-bancada01"

// Espaco de memoria persistente. Sobrevive ao corte de energia da bancada.
#define ESPACO_NVS "dispositivo"
#define CHAVE_TOKEN "tk"

// ------------------------------------------------------------------ pinos
//
// Pinos em const int no topo, nunca numero solto no meio da funcao: e o que
// permite ao professor trocar a fiaçao sem caçar numero no código.
const int PIN_LED_STATUS = 2;
const int PIN_BOTAO_LOCAL = 0;

// ------------------------------------------------------------- configuracao
//
// Todos os numeros que a atividade pede para o aluno mexer estao aqui, e so
// aqui. E o que faz o projeto ser auditavel: o professor olha este bloco e
// sabe o que foi mudado.
const int HISTORICO_MAXIMO = 60;
const unsigned long INTERVALO_DE_ENVIO_MS = 10000;
const unsigned long TEMPO_LIMITE_CONEXAO_MS = 20000;

// Limite que dispara alerta. A regra de negocio do alerta, escrita como
// numero: acima disto, o que o painel mostra e vermelho.
const float LIMITE_DE_TEMPERATURA = 30.0;

// Tempo de vida do token. Passou disso, a placa precisa pedir outro.
const unsigned long VALIDADE_DO_TOKEN_SEGUNDOS = 3600;

Preferences memoria;
unsigned long instanteDeBoot = 0;

// ------------------------------------------------------------------ tipos
//
// Todos os structs ficam no topo, antes de qualquer funcao. Isso nao e
// vaidade: o preprocessador do Arduino coloca os prototipo de funcao em
// cima do arquivo, e um struct declarado no meio aparece DEPOIS do prototipo
// que o usa. O erro e 'X does not name a type' numa linha que esta
// visualmente antes da definicao.

enum EstadoDaPlaca {
  DESLIGADA,
  CONECTANDO,
  AUTENTICADA,
  ENVIANDO,
  EM_REDE,
  DEGRADADA
};

enum Papel {
  LEITOR,
  OPERADOR,
  ADMINISTRADOR
};

struct Identidade {
  String deviceId;
  String token;
  unsigned long emitidoEm;
  Papel papel;
  bool tokenValido;
};

struct LeituraDoProjeto {
  uint32_t instante;
  int16_t temperaturaCx10;
  uint16_t bateriaMv;
  int8_t rssi;
};

struct RespostaDoServidor {
  int status;
  String corpo;
  bool ehErro;
  int64_t micros;
};

struct LinhaDeHistorico {
  bool valida;
  String consulta;
  int linhas;
  int linhaMaisAntiga;
  int linhaMaisNova;
  String observacao;
};

struct Requisito {
  String titulo;
  String onde;
  bool entregue;
};

// ------------------------------------------------------------- historico
//
// O historico local e a fila de partida. O banco de verdade fica no
// servidor; isto aqui e o que a placa tem enquanto a rede nao responde.
LeituraDoProjeto historico[HISTORICO_MAXIMO];
int totalNoHistorico = 0;
int proximaPosicao = 0;

Identidade identidade;

// Os tres papeis do sistema, em ordem de privilegio crescente. A ordem do
// arranjo e proposital: `adminISTRADOR` por ultimo porque e o unico que
// pode dar papel a alguem.
Papel papeisDoSistema[3] = {LEITOR, OPERADOR, ADMINISTRADOR};

Requisito checklistDeEntrega[8];
int totalDeRequisitos = 0;

// ------------------------------------------------------------- utilidades

// Inteiro para texto com um separador: e a diferenca entre "23.5" e "235".
// Sem isto, o servidor recebe 235 e entende 235 graus.
String temperaturaComoTexto(int16_t cx10) {
  String base = String(abs(cx10) / 10);
  String resto = String(abs(cx10) % 10);
  if (cx10 < 0) {
    base = "-" + base;
  }
  return base + "." + resto;
}

String nomeDoEstado(EstadoDaPlaca estado) {
  switch (estado) {
    case DESLIGADA: return "desligada";
    case CONECTANDO: return "conectando";
    case AUTENTICADA: return "autenticada";
    case ENVIANDO: return "enviando";
    case EM_REDE: return "em rede";
    case DEGRADADA: return "degradada";
  }
  return "desconhecido";
}

String nomeDoPapel(Papel papel) {
  switch (papel) {
    case LEITOR: return "leitor";
    case OPERADOR: return "operador";
    case ADMINISTRADOR: return "administrador";
  }
  return "desconhecido";
}

bool papelPodeLer(Papel papel) {
  return papel == LEITOR || papel == OPERADOR || papel == ADMINISTRADOR;
}

bool papelPodeConfigurar(Papel papel) {
  return papel == OPERADOR || papel == ADMINISTRADOR;
}

bool papelPodeDarPapel(Papel papel) {
  return papel == ADMINISTRADOR;
}

RespostaDoServidor responderComoNoServidor(int status, const String& corpo) {
  RespostaDoServidor r;
  r.status = status;
  r.corpo = corpo;
  r.ehErro = status >= 400;
  r.micros = 0;
  return r;
}

// ------------------------------------------------------------------ rede

bool conectarNaRede() {
  Serial.println("[rede] conectando em " + String(WIFI_SSID) + " ...");
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  unsigned long comeco = millis();
  while (WiFi.status() != WL_CONNECTED && (millis() - comeco) < TEMPO_LIMITE_CONEXAO_MS) {
    delay(200);
  }

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[rede] FALHOU apos " + String(TEMPO_LIMITE_CONEXAO_MS / 1000) + " s");
    Serial.println("[rede] codigo de status: " + String(WiFi.status()));
    return false;
  }
  Serial.println("[rede] conectado em " + String(millis() - comeco) + " ms");
  Serial.println("[rede] IP: " + String(WiFi.localIP()) + " | RSSI: " +
                 String(WiFi.RSSI()) + " dBm");
  return true;
}

// -------------------------------------------------------------- historico

void registrarLeitura(int16_t temperaturaCx10, uint16_t bateriaMv) {
  size_t posicao = static_cast<size_t>(proximaPosicao);
  historico[posicao].instante = static_cast<uint32_t>(esp_timer_get_time() / 1000);
  historico[posicao].temperaturaCx10 = temperaturaCx10;
  historico[posicao].bateriaMv = bateriaMv;
  historico[posicao].rssi = static_cast<int8_t>(WiFi.RSSI());

  proximaPosicao = (proximaPosicao + 1) % HISTORICO_MAXIMO;
  if (totalNoHistorico < HISTORICO_MAXIMO) {
    totalNoHistorico++;
  }
}

void descartarHistoricoVelho() {
  // Regra de negocio: a placa guarda o historico recente e esquece o antigo.
  // A alternativa, guardar tudo, transforma 60 linhas em 60000 e estoura a
  // memoria — e o mesmo defeito que o professor viu no projeto de um aluno,
  // com o ESP32 reiniciando sozinho a cada minuto.
  while (totalNoHistorico > HISTORICO_MAXIMO) {
    if (totalNoHistorico == 0) {
      break;
    }
    totalNoHistorico--;
  }
}

// As consultas que o painel do projeto faz. O professor mostra que as tres
// sao a MESMA tabela lida de tres jeitos, e que a diferenca entre elas e so o
// que o servidor projeta. Cada consulta e registrada com o numero de linhas
// que entregou: e o que permite dizer, meses depois, qual consulta do painel
// estava pesada quando o servidor ficou lento.
LinhaDeHistorico consultasDoPainel[3];
int totalDeConsultas = 0;

void registrarConsulta(const String& nome, int linhas, const String& observacao) {
  for (int i = 0; i < totalDeConsultas; i++) {
    if (consultasDoPainel[i].consulta == nome) {
      return;  // ja registrada: o painel nao repete a mesma consulta
    }
  }
  if (totalDeConsultas >= 3) {
    return;  // o painel do projeto tem tres consultas, e nao mais
  }
  LinhaDeHistorico& linha = consultasDoPainel[totalDeConsultas];
  linha.valida = true;
  linha.consulta = nome;
  linha.linhas = linhas;
  linha.linhaMaisAntiga = totalNoHistorico;
  linha.linhaMaisNova = proximaPosicao;
  linha.observacao = observacao;
  totalDeConsultas++;
}

void imprimirConsultasDoPainel() {
  Serial.println("consultas que o painel do projeto vai rodar:");
  for (int i = 0; i < totalDeConsultas; i++) {
    Serial.println("  " + consultasDoPainel[i].consulta + " -> " +
                   String(consultasDoPainel[i].linhas) + " linhas | " +
                   consultasDoPainel[i].observacao);
  }
  Serial.println();
}

void imprimirHistoricoRecente(int quantidade) {
  Serial.println("historico local: " + String(totalNoHistorico) + " de " +
                 String(HISTORICO_MAXIMO) + " posicoes ocupadas");
  int inicio = totalNoHistorico - quantidade;
  if (inicio < 0) {
    inicio = 0;
  }
  for (int i = inicio; i < totalNoHistorico; i++) {
    int posicao = (proximaPosicao - totalNoHistorico + i + HISTORICO_MAXIMO) % HISTORICO_MAXIMO;
    Serial.println("  " + String(i + 1) + ") " +
                   temperaturaComoTexto(historico[posicao].temperaturaCx10) +
                   " C | bateria " + String(historico[posicao].bateriaMv) +
                   " mV | rssi " + String(historico[posicao].rssi) + " dBm");
  }
}

// ------------------------------------------------------------- identidade

void carregarTokenDaMemoria() {
  memoria.begin(ESPACO_NVS, false);
  identidade.token = memoria.getString(CHAVE_TOKEN, "");
  identidade.deviceId = ID_DISPOSITIVO;
  identidade.emitidoEm = static_cast<unsigned long>(esp_timer_get_time() / 1000);
  identidade.papel = LEITOR;
  identidade.tokenValido = false;
  memoria.end();
}

void autenticar() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" 1. identidade — quem e a placa");
  Serial.println("========================================");
  Serial.println("a placa nao tem usuario e senha. Ela tem um token,");
  Serial.println("que o servidor devolveu e que vale por " +
                 String(VALIDADE_DO_TOKEN_SEGUNDOS / 60) + " minutos.");
  Serial.println();

  // O token vem do servidor. Quando nao ha memoria persistente, e o
  // token de exemplo, que e ficticio.
  if (identidade.token.length() == 0) {
    identidade.token = TOKEN_DE_EXEMPLO;
    Serial.println("nenhum token na memoria persistente: usando o de exemplo");
    Serial.println("  (ficticio — nenhum segredo real entra em material de aula)");
  } else {
    Serial.println("token lido da memoria persistente, sobreviveu ao corte");
  }

  unsigned long idade = static_cast<unsigned long>(esp_timer_get_time() / 1000) -
                        identidade.emitidoEm;
  bool expirado = idade > VALIDADE_DO_TOKEN_SEGUNDOS;
  identidade.tokenValido = !expirado;

  Serial.println();
  Serial.println("  device_id:  " + identidade.deviceId);
  int tamanhoDoToken = static_cast<int>(identidade.token.length());
  // So o COMPAMENTO vai para o log. Nem o token, nem um pedaco dele: um
  // sufixo de tres caracteres e o suficiente para virar dicionario quando
  // alguem junta varios logs, e a aula e sobre log sem dado sensivel — o
  // exemplo nao pode dar o contraexemplo. Para provar que o token carrega
  // alguma coisa sem mostrar o que e, o professor compara dois comprimentos.
  Serial.println("  token       " + String(tamanhoDoToken) + " caracteres (valor omitido)");
  Serial.println("  emitido em: " + String(identidade.emitidoEm) + " s apos o boot");
  Serial.println("  idade:      " + String(idade) + " s");
  Serial.println("  validade:   " + String(VALIDADE_DO_TOKEN_SEGUNDOS) + " s");
  Serial.println("  estado:     " + String(identidade.tokenValido ? "valido" : "EXPIRADO"));
  Serial.println();
  Serial.println("o log mostra quantos caracteres o token tem, e nada do valor.");
  Serial.println("a senha nunca aparece: ela nao chegou nesta placa, e o motivo");
  Serial.println("e simples — nao ha motivo para a placa guardar um segredo de");
  Serial.println("pessoa. O token e revogavel e a senha nao.");
  Serial.println();
}

void mostrarAutorizacaoPorPapel() {
  Serial.println("========================================");
  Serial.println(" 2. autorizacao — quem pode fazer o que");
  Serial.println("========================================");
  Serial.println("o servidor decide. A placa pergunta, e obedece o que vier.");
  Serial.println();

  const char* rotulos[3] = {"leitor      ", "operador    ", "administrador"};

  Serial.println("  papel         ler  configurar  dar papel");
  for (int i = 0; i < 3; i++) {
    Serial.println("  " + String(rotulos[i]) + "  " +
                   String(papelPodeLer(papeisDoSistema[i]) ? "sim" : "nao") + "   " +
                   String(papelPodeConfigurar(papeisDoSistema[i]) ? "sim" : "nao") + "         " +
                   String(papelPodeDarPapel(papeisDoSistema[i]) ? "sim" : "nao"));
  }
  Serial.println();
  Serial.println("  tentativas nesta placa, com o token de leitor:");
  Serial.println("  GET /leituras ....... " +
                 String((papelPodeLer(identidade.papel) ? "200" : "403")));
  Serial.println("  POST /configuracao . " +
                 String((papelPodeConfigurar(identidade.papel) ? "200" : "403")) +
                 " (o token desta placa e de leitor)");
  Serial.println("  POST /papeis ....... " +
                 String((papelPodeDarPapel(identidade.papel) ? "200" : "403")) +
                 " (o token desta placa e de leitor)");
  Serial.println();
  Serial.println("um 403 aqui e resposta de regra de negocio, nao de tecnica.");
  Serial.println("o painel mostra 'sem permissao'; o log mostra o motivo.");
  Serial.println("o que o cliente NAO pode fazer e pedir o painel com a conta de");
  Serial.println("outro: a checagem esta no servidor, em cada rota, e nao no menu.");
  Serial.println();
}

// ------------------------------------------------------- painel e alerta

void mostrarPainel() {
  Serial.println("========================================");
  Serial.println(" 3. painel — o que o usuario ve");
  Serial.println("========================================");
  int16_t agoraCx10 = 245;
  float agora = agoraCx10 / 10.0f;
  bool alerta = agora > LIMITE_DE_TEMPERATURA;
  uint32_t heap = ESP.getFreeHeap();

  Serial.println("  estacao de " + identidade.deviceId);
  Serial.println("  temperatura: " + String(agora, 1) + " C" +
                 String(alerta ? "  [ACIMA DO LIMITE]" : ""));
  Serial.println("  limite:      " + String(LIMITE_DE_TEMPERATURA, 1) + " C");
  Serial.println("  bateria:     " + String(3900) + " mV");
  Serial.println("  sinal:       " + String(WiFi.RSSI()) + " dBm");
  Serial.println("  estado:      " + String(nomeDoEstado(WiFi.status() == WL_CONNECTED ? EM_REDE : DEGRADADA)));
  Serial.println("  firmware:    v" + String(VERSAO_FIRMWARE));
  Serial.println("  heap:        " + String(heap) + " bytes");
  Serial.println();
  Serial.println("o painel tem tres estados visiveis, e so tres:");
  Serial.println("  normal:    dado dentro do limite, e a versao de firmware");
  Serial.println("  alerta:    dado fora do limite, com o valor que estourou");
  Serial.println("  sem dado:  sem leitura ha mais de um intervalo, com o tempo");
  Serial.println("o terceiro estado e o que a aula do dia 7 mostrou: fila com id,");
  Serial.println("status do trabalho e tempo de espera. Sem ele, a tela fica");
  Serial.println("branca e o usuario pensa que o sistema diede.");
  Serial.println();
}

void mostrarAlertaERegistro() {
  Serial.println("========================================");
  Serial.println(" 4. alerta e registro sao coisas diferentes");
  Serial.println("========================================");
  Serial.println("regra: acima de " + String(LIMITE_DE_TEMPERATURA, 1) + " C, alerta.");
  Serial.println("abaixo disso, registro. E uma regra de negocio, e ela esta");
  Serial.println("escrita como numero no codigo, e nao como conversa.");
  Serial.println();

  const int CASOS = 4;
  float casos[CASOS] = {22.4f, 31.7f, 29.9f, 30.0f};
  const char* situacoes[CASOS] = {
    "dentro do limite: vira registro",
    "acima do limite: vira alerta",
    "quase no limite: ainda registro",
    "exatamente no limite: o limite e exclusivo"
  };

  Serial.println("  temperatura | o que acontece");
  for (int i = 0; i < CASOS; i++) {
    bool alerta = casos[i] > LIMITE_DE_TEMPERATURA;
    Serial.println("  " + String(casos[i], 1) + " C      | " +
                   String(situacoes[i]) + " -> " + String(alerta ? "ALERTA" : "registro"));
  }
  Serial.println();
  Serial.println("a ultima linha e a que o professor quer na lousa.");
  Serial.println("'acima de 30' e 'a partir de 30' sao sistemas diferentes, e o");
  Serial.println("aluno descobre isso quando o alerta chega antes de esperar.");
  Serial.println("o codigo diz > e nao >=, e o codigo e a definicao da regra.");
  Serial.println();

  RespostaDoServidor alerta1 = responderComoNoServidor(
      422, "{\"erro\":\"regra\",\"codigo\":\"LIMITE_TEMPERATURA\",\"detalhe\":\"31.7 acima de 30.0\"}");
  RespostaDoServidor alerta2 = responderComoNoServidor(
      422, "{\"erro\":\"regra\",\"codigo\":\"LIMITE_TEMPERATURA\",\"detalhe\":\"29.9 dentro do limite\"}");
  RespostaDoServidor ok = responderComoNoServidor(
      201, "{\"id\":4711,\"estado\":\"registrado\"}");

  Serial.println("  resposta do servidor para o alerta de verdade:");
  Serial.println("    " + String(alerta1.status) + " " + alerta1.corpo);
  Serial.println("  resposta para um valor que nao passou:");
  Serial.println("    " + String(alerta2.status) + " " + alerta2.corpo);
  Serial.println("  resposta para o registro normal:");
  Serial.println("    " + String(ok.status) + " " + ok.corpo);
  Serial.println();
  Serial.println("422 e 'entendi o pedido, mas ele viola uma regra'.");
  Serial.println("409 seria 'o pedido colide com o estado atual'.");
  Serial.println("um alerta de regra que responde 500 e um defeito: o servidor");
  Serial.println("trata como erro de tecnica justamente o caso que ele deveria");
  Serial.println("ter previsto.");
  Serial.println();
}

// ------------------------------------------------------------- checklist

void imprimirChecklist() {
  Serial.println("========================================");
  Serial.println(" 5. o que o projeto tem que entregar");
  Serial.println("========================================");
  const char* requisitos[8] = {
    "cadastro e login com senha em hash",
    "sessao que sobrevive ao reinicio",
    "permissao checada em cada rota do servidor",
    "firmware com leitura, historico e envio",
    "historico consultavel por pagina",
    "alerta por regra de negocio escrita no codigo",
    "log com identificador, sem segredo",
    "README com instalacao e uso"
  };
  const char* onde[8] = {
    "servidor: rota de sessao",
    "servidor: tabela de sessao",
    "servidor: middleware de rota",
    "placa: firmware",
    "servidor: repository com paginacao",
    "servidor: service",
    "servidor: log estruturado",
    "repositorio: README.md"
  };
  for (int i = 0; i < 8; i++) {
    checklistDeEntrega[i].titulo = requisitos[i];
    checklistDeEntrega[i].onde = onde[i];
    checklistDeEntrega[i].entregue = true;
  }
  totalDeRequisitos = 8;
  Serial.println("  o requisito        | onde mora");
  for (int i = 0; i < totalDeRequisitos; i++) {
    String titulo = checklistDeEntrega[i].titulo;
    while (titulo.length() < 28) {
      titulo += " ";
    }
    Serial.println("  [" + String(checklistDeEntrega[i].entregue ? "x" : " ") + "] " +
                   titulo + "| " + checklistDeEntrega[i].onde);
  }
  Serial.println();
  Serial.println("a coluna 'onde mora' e a que separa projeto de lista de");
  Serial.println("tarefas. requisito que nao tem arquivo nomeado nao foi feito.");
  Serial.println();
}

void imprimirInstrucoes() {
  Serial.println("========================================");
  Serial.println(" 6. README: as duas instrucoes que faltam sempre");
  Serial.println("========================================");
  Serial.println("instrucao de instalacao — o que roda antes do programa:");
  Serial.println("  1. criar o banco e rodar as migracoes");
  Serial.println("  2. copiar o .env de exemplo e preencher o segredo");
  Serial.println("  3. npm ci, e depois npm start");
  Serial.println("  4. gravar o firmware na placa com o id do dispositivo");
  Serial.println();
  Serial.println("instrucao de uso — o que o programa faz:");
  Serial.println("  1. a placa liga, conecta e pede token");
  Serial.println("  2. envia leitura a cada " + String(INTERVALO_DE_ENVIO_MS / 1000) + " s");
  Serial.println("  3. o painel mostra a ultima leitura e as ultimas alarmes");
  Serial.println("  4. acima de " + String(LIMITE_DE_TEMPERATURA, 1) + " C, entra alerta");
  Serial.println();
  Serial.println("um README sem o passo 2 e o README que o professor descobre");
  Serial.println("que estava errado quando alguem tenta rodar o projeto dele.");
}

// ---------------------------------------------------------------- setup

void setup() {
  Serial.begin(115200);
  delay(300);
  instanteDeBoot = millis();

  pinMode(PIN_LED_STATUS, OUTPUT);
  pinMode(PIN_BOTAO_LOCAL, INPUT_PULLUP);
  digitalWrite(PIN_LED_STATUS, HIGH);

  Serial.println();
  Serial.println("========================================");
  Serial.println(" PROJETO FINAL — estacao-esp32 v" + String(VERSAO_FIRMWARE));
  Serial.println("========================================");
  Serial.println("dispositivo: " + String(ID_DISPOSITIVO) + " | cpu: " +
                 String(getCpuFrequencyMhz()) + " MHz");
  Serial.println();

  bool naRede = conectarNaRede();
  digitalWrite(PIN_LED_STATUS, naRede ? HIGH : LOW);

  carregarTokenDaMemoria();
  autenticar();

  mostrarAutorizacaoPorPapel();
  mostrarPainel();

  // Medidas de exemplo: a aula mostra o caminho do dado, nao a medicao real
  // de um sensor que o aluno pode nao ter conectado.
  int16_t temperaturas[6] = {238, 241, 245, 252, 298, 317};
  for (int i = 0; i < 6; i++) {
    registrarLeitura(temperaturas[i], static_cast<uint16_t>(3900 - (i * 20)));
  }
  descartarHistoricoVelho();
  imprimirHistoricoRecente(6);

  registrarConsulta("historico: ultimas 20", 20, "pagina 1 de 3");
  registrarConsulta("media por dispositivo", 6, "agregado no banco");
  registrarConsulta("alertas do ultimo dia", 3, "so os que dispararam");
  imprimirConsultasDoPainel();

  mostrarAlertaERegistro();
  imprimirChecklist();
  imprimirInstrucoes();

  Serial.println();
  Serial.println("========================================");
  Serial.println(" placa pronta — aperte RESET para rodar de novo");
  Serial.println("========================================");
  Serial.println("uptime: " + String(millis() - instanteDeBoot) + " ms | heap: " +
                 String(ESP.getFreeHeap()) + " bytes | firmware v" + String(VERSAO_FIRMWARE));
}

void loop() {
  digitalWrite(PIN_LED_STATUS, WiFi.status() == WL_CONNECTED ? HIGH : LOW);

  static unsigned long ultimoRelatorio = 0;
  if (millis() - ultimoRelatorio > 15000) {
    ultimoRelatorio = millis();
    Serial.println("[" + String(nomeDoEstado(WiFi.status() == WL_CONNECTED ? EM_REDE : DEGRADADA)) +
                   "] " + String(ID_DISPOSITIVO) + " v" + String(VERSAO_FIRMWARE) +
                   " | heap " + String(ESP.getFreeHeap()) + " B | rssi " +
                   String(WiFi.RSSI()) + " dBm | historico " +
                   String(totalNoHistorico) + "/" + String(HISTORICO_MAXIMO) +
                   " | token " + String(identidade.tokenValido ? "valido" : "expirado") +
                   " | uptime " + String(millis() / 1000) + " s");
  }

  // O botao local e o unico comando manual do projeto: ele marca um ponto
  // de inspecao no log. Quem le o log amanha sabe onde comecou a duvida.
  if (digitalRead(PIN_BOTAO_LOCAL) == LOW) {
    static unsigned long ultimoToque = 0;
    if (millis() - ultimoToque > 2000) {
      ultimoToque = millis();
      digitalWrite(PIN_LED_STATUS, !digitalRead(PIN_LED_STATUS));
      Serial.println("[manual] ponto de inspecao marcado em " +
                     String(millis() / 1000) + " s | heap " +
                     String(ESP.getFreeHeap()) + " B | token " +
                     String(identidade.tokenValido ? "valido" : "expirado"));
    }
  }
}

Por que assim e não de outro jeito. A placa roda seis blocos na ordem a cada reinicio, e cada bloco responde uma pergunta do checklist. Isso não e enfeite: e o que permite ao professor conferir a entrega em dez minutos, olhando o serial, sem abrir o repositorio.

O bloco de identidade mostra apenas o tamanho do token, e nunca o token nem um pedaço dele — nem os três últimos caracteres. É a mesma regra do log sem dado sensível do dia 8 aplicada ao serial: o valor aparece onde a investigação precisa dele, e em nenhum outro lugar. Quem lê o log da placa descobre que a placa existe e que token ela tem, sem virar sessão de ninguém.

E vale a ênfase no porquê de não mostrar nem o sufixo: três caracteres não parecem nada, mas log é o lugar que se copia, se cola em chamado, se anexa em e-mail e se guarda por anos. Três caracteres de muitos tokens, juntados, viram dicionário — e um log de sessão é exatamente o arquivo que se espalha mais. O professor usa esse exemplo para mostrar que "é só um pedacinho" não é um critério de segurança: o critério é *o valor não sai do lugar onde ele tem que sair*.

A tabela de papeis e um arranjo com os três papeis em ordem crescente de privilegio. O operador pode configurar e não pode dar papel; o administrador pode os dois. Escrever essa tabela no código, e não em três if espalhados, e o que permite ao professor mostrar que a checagem esta no lugar: em uma função, chamada de um lugar so.

A parte da fronteira do alerta e a que o professor mais usa. Os quatro casos impressos são o teste da regra, executado na placa, com o resultado ao lado do esperado. O aluno ve que 30.0 não dispara, e entende que essa escolha foi feita em algum lugar — e esse "algum lugar" e o requisito.

O historico local e a fila de partida, e ele tem tamanho maximo. A regra de negocio do limite esta escrita no comentario do descartarHistoricoVelho: a placa guarda o recente e esquece o antigo. A alternativa, guardar tudo, foi o que fez uma placa reiniciar a cada minuto na bancada do professor, e o sintoma era um projeto inteiro sem dado.

A documentacao impressa no final não substitui o arquivo: ela e o README impresso, para o professor ver na frente da turma o que o cliente vai ler. A diferença e proposital e didatica: o que o professor le e o que o cliente vai ler.

Criterios de correcao

CritérioPontos
Checklist com arquivo nomeado em todos os requisitos, sem "em algum lugar"3 pontos
Token no serial sem valor completo e sem senha em lugar nenhum2 pontos
As nove respostas de rota anotadas, com pelo menos um 403 explicado3 pontos
Regra de alerta com o limite como número, e os quatro casos da fronteira testados2 pontos
README com as duas secoes, e a frase "para rodar, o usuario precisa de"1 ponto
Item 6 respondido: a frase de firmware e o arquivo que a recebe1 ponto

Erros comuns

ErroComo apareceCorrecao
Placa guardando senha em texto para "autenticar no servidor"WIFI_PASSWORD reaproveitada como senha de API"A placa não tem usuario. Ela tem token. Se você precisa que ela se identifique, o servidor emite um token e pronto; a senha não viaja pela rede, nem uma vez."
Sessão guardada em Map na memória do processoo painel funciona e de manha o usuario esta deslogado, sem erro"O reinicio do processo leva o Map inteiro. Sessão que precisa sobreviver vai para a tabela, com validade. E a aula 2 do dia 2."
Botao escondido no painel como protecaoo aluno esconde "Configuração" para leitor e acha que acabou"Esconder o botao esconde o botao. A rota continua la, e quem descobre o endereco chama. A checagem e no servidor, em cada rota — aula 2 do dia 3."
Checagem de permissao so no painelrota de POST /configuracao responde 200 para leitor"Faca o teste pelo endereco, não pelo clique. Se você não consegue chamar a rota direto, o teste não existe."
Limite de alerta em texto no README e em número no código, e os dois divergemo README diz "a partir de 30", o código dispara em 30.1"O número da regra tem um lugar so. Se ele aparece em dois arquivos, um dos dois vai envelhecer sem o outro. README aponta para o código, não repete o valor."
Alerta escrito como log comuma linha do alerta se perde entre as normais"Alerta tem destino proprio: trabalho enfileirado, com id e status. Registro vira linha. São dois caminhos, e por isso que o alerta some e o registro fica."
Painel consultando a tabela a cada quadro da animacaoo primeiro aluno que abre duas abas derruba o banco"Pedir uma página e animar do lado do navegador. Cada quadro que consulta e um SELECT a mais no seu servidor."
historico sem índice e sem página, "porque o dado e pequeno"painel lento com poucos dispositivos já"Pequeno por dispositivo e grande por multiplos. O custo multiplica. Índice, página e agregacao são o mesmo preco em três lugares."
historico local guardando tudo em memóriaa placa reinicia a cada minuto, sem log de erro"A fila de partida tem tamanho. A fonte da verdade e o servidor; a memória da placa e cache, e cache tem limite."
Projeto sem README e a entrega "fala" no oralo cliente não consegue instalar sozinho"Se a instalacao so funciona com você do lado, não esta pronta. Escreva os quatro passos, teste num computador limpo e so então considere entregue."
Log sem id do dispositivo"de qual placa veio este alerta" fica sem resposta"Toda linha de log que envolve dispositivo leva o id dele. Sem isso, a investigacao depende de alguem lembrar da ordem."
Alertar em 30 e prometer "a partir de 30" para o clienteo cliente reclama que o alerta veio "tarde" num caso de fronteira"A fronteira e uma decisao, não um detalhe. Defina, escreva no código, e escreva no contrato com a mesma palavra: acima de, ou a partir de."

Desafio extra

Escreva um script que rode o seu projeto inteiro do zero num banco vazio: cria o schema, roda as migracoes, sobe o servidor, faz cadastro, login, envia uma leitura e consulta o historico — imprimindo o tempo de cada etapa. Meça quanto tempo leva a primeira execução e quanto leva a segunda, com o banco já pronto. Escreva uma frase sobre o que essa diferença significa para quem instala o seu projeto pela primeira vez, e o que ela diz sobre o README.

>

A resolucao, compilada

// Aula 1 do dia 12: projeto final do ano, sistema completo com login e
// historico.
//
// Esta e a placa do PROJETO FINAL. Ela nao e um trecho: e o que o aluno
// entrega. E o unico sketch do ano que junta as sete camadas do curso, e a
// razao de ele ser o mais commentado.
//
// O que a placa faz, em ordem:
//   1. identifica-se com um token (nunca com a senha) e mostra o que o
//      servidor responde para cada papel;
//   2. mede o estado da propria placa (heap, RSSI, uptime) e mostra o que o
//      painel veria;
//   3. grava leituras num historico local, e mostra as tres consultas de
//      painel que o servidor vai rodar em cima dele;
//   4. decide o que e alerta e o que e registro, pelos dois numeros que o
//      professor escreveu na lousa.
//
// O que a placa NAO faz, e o professor explica por que: nao grava senha, nao
// decide permissao, nao e fonte da verdade do historico. Quem decide e o
// servidor. A placa informa; o servidor autoriza e manda.
//
// O que o professor mede no serial: os quatro blocos rodam na ordem a cada
// RESET, e a aula inteira cabe nesses quatro blocos.

#include <Arduino.h>
#include <WiFi.h>
#include <Preferences.h>
#include <esp_timer.h>

// Rede do laboratorio. Os dois valores sao FICTICIOS de proposito: nenhum
// segredo real entra em sketch de exemplo, nem em material, nem no git.
#define WIFI_SSID "lab-esp32-aula"
#define WIFI_PASSWORD "troque-esta-pela-sua"

// Versao do firmware. E a prova de entrega: o painel compara a versao de cada
// placa, e a diferenca entre "atualizado" e "na versao antiga" aparece
// exatamente nesta linha.
#define VERSAO_FIRMWARE "2.0.0"

// Identificador do dispositivo. Este valor viaja no cabecalho de autenticacao
// e no corpo de cada leitura, e e por isso que ele vive em uma constante.
#define ID_DISPOSITIVO "esp32-bancada-01"

// Token de exemplo. FICTICIO, como tudo que aparece em material. A placa
// guarda o TOKEN que o servidor devolveu e esquece a senha — a senha nunca
// saiu do navegador.
#define TOKEN_DE_EXEMPLO "tk-lab-bancada01"

// Espaco de memoria persistente. Sobrevive ao corte de energia da bancada.
#define ESPACO_NVS "dispositivo"
#define CHAVE_TOKEN "tk"

// ------------------------------------------------------------------ pinos
//
// Pinos em const int no topo, nunca numero solto no meio da funcao: e o que
// permite ao professor trocar a fiaçao sem caçar numero no código.
const int PIN_LED_STATUS = 2;
const int PIN_BOTAO_LOCAL = 0;

// ------------------------------------------------------------- configuracao
//
// Todos os numeros que a atividade pede para o aluno mexer estao aqui, e so
// aqui. E o que faz o projeto ser auditavel: o professor olha este bloco e
// sabe o que foi mudado.
const int HISTORICO_MAXIMO = 60;
const unsigned long INTERVALO_DE_ENVIO_MS = 10000;
const unsigned long TEMPO_LIMITE_CONEXAO_MS = 20000;

// Limite que dispara alerta. A regra de negocio do alerta, escrita como
// numero: acima disto, o que o painel mostra e vermelho.
const float LIMITE_DE_TEMPERATURA = 30.0;

// Tempo de vida do token. Passou disso, a placa precisa pedir outro.
const unsigned long VALIDADE_DO_TOKEN_SEGUNDOS = 3600;

Preferences memoria;
unsigned long instanteDeBoot = 0;

// ------------------------------------------------------------------ tipos
//
// Todos os structs ficam no topo, antes de qualquer funcao. Isso nao e
// vaidade: o preprocessador do Arduino coloca os prototipo de funcao em
// cima do arquivo, e um struct declarado no meio aparece DEPOIS do prototipo
// que o usa. O erro e 'X does not name a type' numa linha que esta
// visualmente antes da definicao.

enum EstadoDaPlaca {
  DESLIGADA,
  CONECTANDO,
  AUTENTICADA,
  ENVIANDO,
  EM_REDE,
  DEGRADADA
};

enum Papel {
  LEITOR,
  OPERADOR,
  ADMINISTRADOR
};

struct Identidade {
  String deviceId;
  String token;
  unsigned long emitidoEm;
  Papel papel;
  bool tokenValido;
};

struct LeituraDoProjeto {
  uint32_t instante;
  int16_t temperaturaCx10;
  uint16_t bateriaMv;
  int8_t rssi;
};

struct RespostaDoServidor {
  int status;
  String corpo;
  bool ehErro;
  int64_t micros;
};

struct LinhaDeHistorico {
  bool valida;
  String consulta;
  int linhas;
  int linhaMaisAntiga;
  int linhaMaisNova;
  String observacao;
};

struct Requisito {
  String titulo;
  String onde;
  bool entregue;
};

// ------------------------------------------------------------- historico
//
// O historico local e a fila de partida. O banco de verdade fica no
// servidor; isto aqui e o que a placa tem enquanto a rede nao responde.
LeituraDoProjeto historico[HISTORICO_MAXIMO];
int totalNoHistorico = 0;
int proximaPosicao = 0;

Identidade identidade;

// Os tres papeis do sistema, em ordem de privilegio crescente. A ordem do
// arranjo e proposital: `adminISTRADOR` por ultimo porque e o unico que
// pode dar papel a alguem.
Papel papeisDoSistema[3] = {LEITOR, OPERADOR, ADMINISTRADOR};

Requisito checklistDeEntrega[8];
int totalDeRequisitos = 0;

// ------------------------------------------------------------- utilidades

// Inteiro para texto com um separador: e a diferenca entre "23.5" e "235".
// Sem isto, o servidor recebe 235 e entende 235 graus.
String temperaturaComoTexto(int16_t cx10) {
  String base = String(abs(cx10) / 10);
  String resto = String(abs(cx10) % 10);
  if (cx10 < 0) {
    base = "-" + base;
  }
  return base + "." + resto;
}

String nomeDoEstado(EstadoDaPlaca estado) {
  switch (estado) {
    case DESLIGADA: return "desligada";
    case CONECTANDO: return "conectando";
    case AUTENTICADA: return "autenticada";
    case ENVIANDO: return "enviando";
    case EM_REDE: return "em rede";
    case DEGRADADA: return "degradada";
  }
  return "desconhecido";
}

String nomeDoPapel(Papel papel) {
  switch (papel) {
    case LEITOR: return "leitor";
    case OPERADOR: return "operador";
    case ADMINISTRADOR: return "administrador";
  }
  return "desconhecido";
}

bool papelPodeLer(Papel papel) {
  return papel == LEITOR || papel == OPERADOR || papel == ADMINISTRADOR;
}

bool papelPodeConfigurar(Papel papel) {
  return papel == OPERADOR || papel == ADMINISTRADOR;
}

bool papelPodeDarPapel(Papel papel) {
  return papel == ADMINISTRADOR;
}

RespostaDoServidor responderComoNoServidor(int status, const String& corpo) {
  RespostaDoServidor r;
  r.status = status;
  r.corpo = corpo;
  r.ehErro = status >= 400;
  r.micros = 0;
  return r;
}

// ------------------------------------------------------------------ rede

bool conectarNaRede() {
  Serial.println("[rede] conectando em " + String(WIFI_SSID) + " ...");
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  unsigned long comeco = millis();
  while (WiFi.status() != WL_CONNECTED && (millis() - comeco) < TEMPO_LIMITE_CONEXAO_MS) {
    delay(200);
  }

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[rede] FALHOU apos " + String(TEMPO_LIMITE_CONEXAO_MS / 1000) + " s");
    Serial.println("[rede] codigo de status: " + String(WiFi.status()));
    return false;
  }
  Serial.println("[rede] conectado em " + String(millis() - comeco) + " ms");
  Serial.println("[rede] IP: " + String(WiFi.localIP()) + " | RSSI: " +
                 String(WiFi.RSSI()) + " dBm");
  return true;
}

// -------------------------------------------------------------- historico

void registrarLeitura(int16_t temperaturaCx10, uint16_t bateriaMv) {
  size_t posicao = static_cast<size_t>(proximaPosicao);
  historico[posicao].instante = static_cast<uint32_t>(esp_timer_get_time() / 1000);
  historico[posicao].temperaturaCx10 = temperaturaCx10;
  historico[posicao].bateriaMv = bateriaMv;
  historico[posicao].rssi = static_cast<int8_t>(WiFi.RSSI());

  proximaPosicao = (proximaPosicao + 1) % HISTORICO_MAXIMO;
  if (totalNoHistorico < HISTORICO_MAXIMO) {
    totalNoHistorico++;
  }
}

void descartarHistoricoVelho() {
  // Regra de negocio: a placa guarda o historico recente e esquece o antigo.
  // A alternativa, guardar tudo, transforma 60 linhas em 60000 e estoura a
  // memoria — e o mesmo defeito que o professor viu no projeto de um aluno,
  // com o ESP32 reiniciando sozinho a cada minuto.
  while (totalNoHistorico > HISTORICO_MAXIMO) {
    if (totalNoHistorico == 0) {
      break;
    }
    totalNoHistorico--;
  }
}

// As consultas que o painel do projeto faz. O professor mostra que as tres
// sao a MESMA tabela lida de tres jeitos, e que a diferenca entre elas e so o
// que o servidor projeta. Cada consulta e registrada com o numero de linhas
// que entregou: e o que permite dizer, meses depois, qual consulta do painel
// estava pesada quando o servidor ficou lento.
LinhaDeHistorico consultasDoPainel[3];
int totalDeConsultas = 0;

void registrarConsulta(const String& nome, int linhas, const String& observacao) {
  for (int i = 0; i < totalDeConsultas; i++) {
    if (consultasDoPainel[i].consulta == nome) {
      return;  // ja registrada: o painel nao repete a mesma consulta
    }
  }
  if (totalDeConsultas >= 3) {
    return;  // o painel do projeto tem tres consultas, e nao mais
  }
  LinhaDeHistorico& linha = consultasDoPainel[totalDeConsultas];
  linha.valida = true;
  linha.consulta = nome;
  linha.linhas = linhas;
  linha.linhaMaisAntiga = totalNoHistorico;
  linha.linhaMaisNova = proximaPosicao;
  linha.observacao = observacao;
  totalDeConsultas++;
}

void imprimirConsultasDoPainel() {
  Serial.println("consultas que o painel do projeto vai rodar:");
  for (int i = 0; i < totalDeConsultas; i++) {
    Serial.println("  " + consultasDoPainel[i].consulta + " -> " +
                   String(consultasDoPainel[i].linhas) + " linhas | " +
                   consultasDoPainel[i].observacao);
  }
  Serial.println();
}

void imprimirHistoricoRecente(int quantidade) {
  Serial.println("historico local: " + String(totalNoHistorico) + " de " +
                 String(HISTORICO_MAXIMO) + " posicoes ocupadas");
  int inicio = totalNoHistorico - quantidade;
  if (inicio < 0) {
    inicio = 0;
  }
  for (int i = inicio; i < totalNoHistorico; i++) {
    int posicao = (proximaPosicao - totalNoHistorico + i + HISTORICO_MAXIMO) % HISTORICO_MAXIMO;
    Serial.println("  " + String(i + 1) + ") " +
                   temperaturaComoTexto(historico[posicao].temperaturaCx10) +
                   " C | bateria " + String(historico[posicao].bateriaMv) +
                   " mV | rssi " + String(historico[posicao].rssi) + " dBm");
  }
}

// ------------------------------------------------------------- identidade

void carregarTokenDaMemoria() {
  memoria.begin(ESPACO_NVS, false);
  identidade.token = memoria.getString(CHAVE_TOKEN, "");
  identidade.deviceId = ID_DISPOSITIVO;
  identidade.emitidoEm = static_cast<unsigned long>(esp_timer_get_time() / 1000);
  identidade.papel = LEITOR;
  identidade.tokenValido = false;
  memoria.end();
}

void autenticar() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" 1. identidade — quem e a placa");
  Serial.println("========================================");
  Serial.println("a placa nao tem usuario e senha. Ela tem um token,");
  Serial.println("que o servidor devolveu e que vale por " +
                 String(VALIDADE_DO_TOKEN_SEGUNDOS / 60) + " minutos.");
  Serial.println();

  // O token vem do servidor. Quando nao ha memoria persistente, e o
  // token de exemplo, que e ficticio.
  if (identidade.token.length() == 0) {
    identidade.token = TOKEN_DE_EXEMPLO;
    Serial.println("nenhum token na memoria persistente: usando o de exemplo");
    Serial.println("  (ficticio — nenhum segredo real entra em material de aula)");
  } else {
    Serial.println("token lido da memoria persistente, sobreviveu ao corte");
  }

  unsigned long idade = static_cast<unsigned long>(esp_timer_get_time() / 1000) -
                        identidade.emitidoEm;
  bool expirado = idade > VALIDADE_DO_TOKEN_SEGUNDOS;
  identidade.tokenValido = !expirado;

  Serial.println();
  Serial.println("  device_id:  " + identidade.deviceId);
  int tamanhoDoToken = static_cast<int>(identidade.token.length());
  // So o COMPAMENTO vai para o log. Nem o token, nem um pedaco dele: um
  // sufixo de tres caracteres e o suficiente para virar dicionario quando
  // alguem junta varios logs, e a aula e sobre log sem dado sensivel — o
  // exemplo nao pode dar o contraexemplo. Para provar que o token carrega
  // alguma coisa sem mostrar o que e, o professor compara dois comprimentos.
  Serial.println("  token       " + String(tamanhoDoToken) + " caracteres (valor omitido)");
  Serial.println("  emitido em: " + String(identidade.emitidoEm) + " s apos o boot");
  Serial.println("  idade:      " + String(idade) + " s");
  Serial.println("  validade:   " + String(VALIDADE_DO_TOKEN_SEGUNDOS) + " s");
  Serial.println("  estado:     " + String(identidade.tokenValido ? "valido" : "EXPIRADO"));
  Serial.println();
  Serial.println("o log mostra quantos caracteres o token tem, e nada do valor.");
  Serial.println("a senha nunca aparece: ela nao chegou nesta placa, e o motivo");
  Serial.println("e simples — nao ha motivo para a placa guardar um segredo de");
  Serial.println("pessoa. O token e revogavel e a senha nao.");
  Serial.println();
}

void mostrarAutorizacaoPorPapel() {
  Serial.println("========================================");
  Serial.println(" 2. autorizacao — quem pode fazer o que");
  Serial.println("========================================");
  Serial.println("o servidor decide. A placa pergunta, e obedece o que vier.");
  Serial.println();

  const char* rotulos[3] = {"leitor      ", "operador    ", "administrador"};

  Serial.println("  papel         ler  configurar  dar papel");
  for (int i = 0; i < 3; i++) {
    Serial.println("  " + String(rotulos[i]) + "  " +
                   String(papelPodeLer(papeisDoSistema[i]) ? "sim" : "nao") + "   " +
                   String(papelPodeConfigurar(papeisDoSistema[i]) ? "sim" : "nao") + "         " +
                   String(papelPodeDarPapel(papeisDoSistema[i]) ? "sim" : "nao"));
  }
  Serial.println();
  Serial.println("  tentativas nesta placa, com o token de leitor:");
  Serial.println("  GET /leituras ....... " +
                 String((papelPodeLer(identidade.papel) ? "200" : "403")));
  Serial.println("  POST /configuracao . " +
                 String((papelPodeConfigurar(identidade.papel) ? "200" : "403")) +
                 " (o token desta placa e de leitor)");
  Serial.println("  POST /papeis ....... " +
                 String((papelPodeDarPapel(identidade.papel) ? "200" : "403")) +
                 " (o token desta placa e de leitor)");
  Serial.println();
  Serial.println("um 403 aqui e resposta de regra de negocio, nao de tecnica.");
  Serial.println("o painel mostra 'sem permissao'; o log mostra o motivo.");
  Serial.println("o que o cliente NAO pode fazer e pedir o painel com a conta de");
  Serial.println("outro: a checagem esta no servidor, em cada rota, e nao no menu.");
  Serial.println();
}

// ------------------------------------------------------- painel e alerta

void mostrarPainel() {
  Serial.println("========================================");
  Serial.println(" 3. painel — o que o usuario ve");
  Serial.println("========================================");
  int16_t agoraCx10 = 245;
  float agora = agoraCx10 / 10.0f;
  bool alerta = agora > LIMITE_DE_TEMPERATURA;
  uint32_t heap = ESP.getFreeHeap();

  Serial.println("  estacao de " + identidade.deviceId);
  Serial.println("  temperatura: " + String(agora, 1) + " C" +
                 String(alerta ? "  [ACIMA DO LIMITE]" : ""));
  Serial.println("  limite:      " + String(LIMITE_DE_TEMPERATURA, 1) + " C");
  Serial.println("  bateria:     " + String(3900) + " mV");
  Serial.println("  sinal:       " + String(WiFi.RSSI()) + " dBm");
  Serial.println("  estado:      " + String(nomeDoEstado(WiFi.status() == WL_CONNECTED ? EM_REDE : DEGRADADA)));
  Serial.println("  firmware:    v" + String(VERSAO_FIRMWARE));
  Serial.println("  heap:        " + String(heap) + " bytes");
  Serial.println();
  Serial.println("o painel tem tres estados visiveis, e so tres:");
  Serial.println("  normal:    dado dentro do limite, e a versao de firmware");
  Serial.println("  alerta:    dado fora do limite, com o valor que estourou");
  Serial.println("  sem dado:  sem leitura ha mais de um intervalo, com o tempo");
  Serial.println("o terceiro estado e o que a aula do dia 7 mostrou: fila com id,");
  Serial.println("status do trabalho e tempo de espera. Sem ele, a tela fica");
  Serial.println("branca e o usuario pensa que o sistema diede.");
  Serial.println();
}

void mostrarAlertaERegistro() {
  Serial.println("========================================");
  Serial.println(" 4. alerta e registro sao coisas diferentes");
  Serial.println("========================================");
  Serial.println("regra: acima de " + String(LIMITE_DE_TEMPERATURA, 1) + " C, alerta.");
  Serial.println("abaixo disso, registro. E uma regra de negocio, e ela esta");
  Serial.println("escrita como numero no codigo, e nao como conversa.");
  Serial.println();

  const int CASOS = 4;
  float casos[CASOS] = {22.4f, 31.7f, 29.9f, 30.0f};
  const char* situacoes[CASOS] = {
    "dentro do limite: vira registro",
    "acima do limite: vira alerta",
    "quase no limite: ainda registro",
    "exatamente no limite: o limite e exclusivo"
  };

  Serial.println("  temperatura | o que acontece");
  for (int i = 0; i < CASOS; i++) {
    bool alerta = casos[i] > LIMITE_DE_TEMPERATURA;
    Serial.println("  " + String(casos[i], 1) + " C      | " +
                   String(situacoes[i]) + " -> " + String(alerta ? "ALERTA" : "registro"));
  }
  Serial.println();
  Serial.println("a ultima linha e a que o professor quer na lousa.");
  Serial.println("'acima de 30' e 'a partir de 30' sao sistemas diferentes, e o");
  Serial.println("aluno descobre isso quando o alerta chega antes de esperar.");
  Serial.println("o codigo diz > e nao >=, e o codigo e a definicao da regra.");
  Serial.println();

  RespostaDoServidor alerta1 = responderComoNoServidor(
      422, "{\"erro\":\"regra\",\"codigo\":\"LIMITE_TEMPERATURA\",\"detalhe\":\"31.7 acima de 30.0\"}");
  RespostaDoServidor alerta2 = responderComoNoServidor(
      422, "{\"erro\":\"regra\",\"codigo\":\"LIMITE_TEMPERATURA\",\"detalhe\":\"29.9 dentro do limite\"}");
  RespostaDoServidor ok = responderComoNoServidor(
      201, "{\"id\":4711,\"estado\":\"registrado\"}");

  Serial.println("  resposta do servidor para o alerta de verdade:");
  Serial.println("    " + String(alerta1.status) + " " + alerta1.corpo);
  Serial.println("  resposta para um valor que nao passou:");
  Serial.println("    " + String(alerta2.status) + " " + alerta2.corpo);
  Serial.println("  resposta para o registro normal:");
  Serial.println("    " + String(ok.status) + " " + ok.corpo);
  Serial.println();
  Serial.println("422 e 'entendi o pedido, mas ele viola uma regra'.");
  Serial.println("409 seria 'o pedido colide com o estado atual'.");
  Serial.println("um alerta de regra que responde 500 e um defeito: o servidor");
  Serial.println("trata como erro de tecnica justamente o caso que ele deveria");
  Serial.println("ter previsto.");
  Serial.println();
}

// ------------------------------------------------------------- checklist

void imprimirChecklist() {
  Serial.println("========================================");
  Serial.println(" 5. o que o projeto tem que entregar");
  Serial.println("========================================");
  const char* requisitos[8] = {
    "cadastro e login com senha em hash",
    "sessao que sobrevive ao reinicio",
    "permissao checada em cada rota do servidor",
    "firmware com leitura, historico e envio",
    "historico consultavel por pagina",
    "alerta por regra de negocio escrita no codigo",
    "log com identificador, sem segredo",
    "README com instalacao e uso"
  };
  const char* onde[8] = {
    "servidor: rota de sessao",
    "servidor: tabela de sessao",
    "servidor: middleware de rota",
    "placa: firmware",
    "servidor: repository com paginacao",
    "servidor: service",
    "servidor: log estruturado",
    "repositorio: README.md"
  };
  for (int i = 0; i < 8; i++) {
    checklistDeEntrega[i].titulo = requisitos[i];
    checklistDeEntrega[i].onde = onde[i];
    checklistDeEntrega[i].entregue = true;
  }
  totalDeRequisitos = 8;
  Serial.println("  o requisito        | onde mora");
  for (int i = 0; i < totalDeRequisitos; i++) {
    String titulo = checklistDeEntrega[i].titulo;
    while (titulo.length() < 28) {
      titulo += " ";
    }
    Serial.println("  [" + String(checklistDeEntrega[i].entregue ? "x" : " ") + "] " +
                   titulo + "| " + checklistDeEntrega[i].onde);
  }
  Serial.println();
  Serial.println("a coluna 'onde mora' e a que separa projeto de lista de");
  Serial.println("tarefas. requisito que nao tem arquivo nomeado nao foi feito.");
  Serial.println();
}

void imprimirInstrucoes() {
  Serial.println("========================================");
  Serial.println(" 6. README: as duas instrucoes que faltam sempre");
  Serial.println("========================================");
  Serial.println("instrucao de instalacao — o que roda antes do programa:");
  Serial.println("  1. criar o banco e rodar as migracoes");
  Serial.println("  2. copiar o .env de exemplo e preencher o segredo");
  Serial.println("  3. npm ci, e depois npm start");
  Serial.println("  4. gravar o firmware na placa com o id do dispositivo");
  Serial.println();
  Serial.println("instrucao de uso — o que o programa faz:");
  Serial.println("  1. a placa liga, conecta e pede token");
  Serial.println("  2. envia leitura a cada " + String(INTERVALO_DE_ENVIO_MS / 1000) + " s");
  Serial.println("  3. o painel mostra a ultima leitura e as ultimas alarmes");
  Serial.println("  4. acima de " + String(LIMITE_DE_TEMPERATURA, 1) + " C, entra alerta");
  Serial.println();
  Serial.println("um README sem o passo 2 e o README que o professor descobre");
  Serial.println("que estava errado quando alguem tenta rodar o projeto dele.");
}

// ---------------------------------------------------------------- setup

void setup() {
  Serial.begin(115200);
  delay(300);
  instanteDeBoot = millis();

  pinMode(PIN_LED_STATUS, OUTPUT);
  pinMode(PIN_BOTAO_LOCAL, INPUT_PULLUP);
  digitalWrite(PIN_LED_STATUS, HIGH);

  Serial.println();
  Serial.println("========================================");
  Serial.println(" PROJETO FINAL — estacao-esp32 v" + String(VERSAO_FIRMWARE));
  Serial.println("========================================");
  Serial.println("dispositivo: " + String(ID_DISPOSITIVO) + " | cpu: " +
                 String(getCpuFrequencyMhz()) + " MHz");
  Serial.println();

  bool naRede = conectarNaRede();
  digitalWrite(PIN_LED_STATUS, naRede ? HIGH : LOW);

  carregarTokenDaMemoria();
  autenticar();

  mostrarAutorizacaoPorPapel();
  mostrarPainel();

  // Medidas de exemplo: a aula mostra o caminho do dado, nao a medicao real
  // de um sensor que o aluno pode nao ter conectado.
  int16_t temperaturas[6] = {238, 241, 245, 252, 298, 317};
  for (int i = 0; i < 6; i++) {
    registrarLeitura(temperaturas[i], static_cast<uint16_t>(3900 - (i * 20)));
  }
  descartarHistoricoVelho();
  imprimirHistoricoRecente(6);

  registrarConsulta("historico: ultimas 20", 20, "pagina 1 de 3");
  registrarConsulta("media por dispositivo", 6, "agregado no banco");
  registrarConsulta("alertas do ultimo dia", 3, "so os que dispararam");
  imprimirConsultasDoPainel();

  mostrarAlertaERegistro();
  imprimirChecklist();
  imprimirInstrucoes();

  Serial.println();
  Serial.println("========================================");
  Serial.println(" placa pronta — aperte RESET para rodar de novo");
  Serial.println("========================================");
  Serial.println("uptime: " + String(millis() - instanteDeBoot) + " ms | heap: " +
                 String(ESP.getFreeHeap()) + " bytes | firmware v" + String(VERSAO_FIRMWARE));
}

void loop() {
  digitalWrite(PIN_LED_STATUS, WiFi.status() == WL_CONNECTED ? HIGH : LOW);

  static unsigned long ultimoRelatorio = 0;
  if (millis() - ultimoRelatorio > 15000) {
    ultimoRelatorio = millis();
    Serial.println("[" + String(nomeDoEstado(WiFi.status() == WL_CONNECTED ? EM_REDE : DEGRADADA)) +
                   "] " + String(ID_DISPOSITIVO) + " v" + String(VERSAO_FIRMWARE) +
                   " | heap " + String(ESP.getFreeHeap()) + " B | rssi " +
                   String(WiFi.RSSI()) + " dBm | historico " +
                   String(totalNoHistorico) + "/" + String(HISTORICO_MAXIMO) +
                   " | token " + String(identidade.tokenValido ? "valido" : "expirado") +
                   " | uptime " + String(millis() / 1000) + " s");
  }

  // O botao local e o unico comando manual do projeto: ele marca um ponto
  // de inspecao no log. Quem le o log amanha sabe onde comecou a duvida.
  if (digitalRead(PIN_BOTAO_LOCAL) == LOW) {
    static unsigned long ultimoToque = 0;
    if (millis() - ultimoToque > 2000) {
      ultimoToque = millis();
      digitalWrite(PIN_LED_STATUS, !digitalRead(PIN_LED_STATUS));
      Serial.println("[manual] ponto de inspecao marcado em " +
                     String(millis() / 1000) + " s | heap " +
                     String(ESP.getFreeHeap()) + " B | token " +
                     String(identidade.tokenValido ? "valido" : "expirado"));
    }
  }
}

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

Aula 2 — Projeto final: teste, seguranca e entrega

Objetivos

rojeto** que prove a regra nos casos que quebram, e dizer por que um caso so e execução, não teste.

  • Conferir a migracao do projeto sobre dado existente, e provar que o dado antigo continua igual depois.
  • Revisar a seguranca do projeto pelas quatro portas de entrada, e nomear um defeito de seguranca encontrado no código do colega.
  • Fazer uma revisao de código em quinze minutos, escribiendo um elogio especifico, uma duvida e um problema encontrado.
  • Preencher a lista de entrega com o que falta e escrever a autoavaliacao de o que eu faria diferente.

Material

  • 1 ESP32 DevKit V1 por aluno, com o cabo USB
  • 1 botao com pull-up no pino 0, para marcar o ponto de inspecao
  • 2 projetos do colega por dupla, com o repositorio aberto
  • Terminal com o npm test do projeto, o MySQL em copia e o git log
  • Folha de revisao de código impressa, com as três colunas

Conceitos

Teste que falha primeiro

A regra do curso inteiro cabe num caso: escrever o teste antes. Não antes de escrever o código inteiro — antes de escrever o arranjo que conserta o defeito. O teste que passa antes de falhar não provou nada; provou que o código já estava certo.

A diferença entre executar e testar esta na lista de casos:

O que o código fazO que o teste faz
roda a regra com um valorroda a regra com todos os valores que importam
mostra o resultadocompara com o esperado e diz que falhou
para no primeiro erromostra todos os erros de uma vez

Um caso so e execução. Três casos e um início. Seis casos, sendo um deles o caso limite e um deles o caso de erro, e teste. O caso de erro e o que prova que a regra recusa: sem ele, uma função que sempre devolve "alerta" passa em qualquer teste que so olhe o caminho feliz.

O caso limite e o que o professor exige no projeto: o valor exatamente no limite. E o unico caso que distingue > de >=, e o unico que o cliente que contratou "a partir de 30" vai sentir.

Maquina de estados testada por transicao impossivel

Uma maquina de estados so e provada pela transicao que não pode existir. A aula 1 do dia 1 mostrou a transicao possivel — de desligado para ligado, com guarda. O teste dela e: tentar a transicao proibida e ver o estado não mudar.

Todas as transicoes testadas não e o mesmo que todas as transicoes validas. A tabela de transicoes tem N estados e N estados de saída; o número de combinaveis e N vezes N. Testar todas as combinaveis e o que a aula 1 do dia 5 mostrou, e o que define um mock de verdade: um substituto que responde o que o banco responderia, e um repository falso para rodar a maquina sem banco.

Migracao: dois estados e uma volta

A migracao do projeto tem um estado antes e um estado depois, e o dado que existia no antes tem que estar intacto no depois. Três regras, e as três são conferiveis em trinta segundos:

  1. dado que existia continua igual
  2. coluna nova tem valor para linha nova, sem adivinhacao
  3. da para voltar — a versão anterior ainda le a tabela nova

A terceira e a que o professor cobra, e e a que a maioria dos projetos não tem. Migracao irreversivel e aposta: se a versão nova tiver defeito, sobra uma noite para descobrir o valor antigo de cada linha.

A migracao que quebra tem um nome conhecido: adicionar coluna NOT NULL em tabela cheia. O banco não aceita a coluna sem valor, e a tabela fica trancada enquanto ele preenche. O caminho e em duas etapas — expandir e migrar primeiro, com a coluna vazia, e so depois tornar obrigatoria. O ponto e compatibilidade: o código antigo roda com a coluna nova, e o código novo roda sem a coluna antiga, até a troca acabar.

Antes de qualquer migracao, dois passos obrigatorios: backup e testar a migracao em copia. E a fonte da verdade e o schema lido do banco, nunca o que esta no README — o README envelhece, o schema não.

As quatro portas de seguranca do projeto

A seguranca do projeto se revisa em quatro portas, e a ordem não e a ordem do código: e a ordem em que o dado entra.

PortaQuem mandaDefeito tipico do projeto
sensoro dispositivo, com defeitovalor fora de faixa virando alerta falso
formularioa pessoa na telacampo sem escapar entrando no HTML
resposta de servidora outra maquinacorpo grande demais ou formato inesperado
cabecalhoa rede intermediáriatoken de outro usuario no log de acesso

O defeito de seguranca encontrado que o professor procura primeiro e o terceiro da tabela: dado de um usuario no log de acesso, e token impresso no log. O segundo e pior que o primeiro, porque quem le o log não precisava estar logado.

A defesa e sempre do mesmo lado: validar na entrada, escapar na saída, e nunca escrever segredo no log. O que muda no projeto e que a porta de tras — a mesma leitura chegando duas vezes — precisa de chave de idempotencia, e não de mais uma validacao.

Revisao de código: quinze minutos, quatro perguntas

code review e leitura com critério. Quatro perguntas resolvem quinze minutos de leitura, e a ultima delas e o que separa código que roda do código que roda em outro lugar — o deploy do projeto, do build até o health check, sem segredo no servidor:

  1. roda o teste e ve se passa
  2. acha a regra de negocio e ve se o número esta no código
  3. acha onde o log e escrito e ve se tem segredo
  4. acha a consulta sem índice e sem paginacao

O resultado tem três partes, e o contrato e o que separa a aula de um elogio vazio: um comentario util, com elogio especifico, duvida concreta e problema encontrado. bom trabalho não e elogio: não diz o que foi bom e não muda o comportamento de ninguem.

Ler o código do colega também e decompor problema: a tarefa não e "avaliar", e "encontrar três coisas". Tarefa sem critério e o que transforma revisao em palanca.

Atividade

Montagem: botao com resistor de pull-up no pino 0, ground no GND. O botao marca o ponto de inspecao no log da revisao, e e ele que permite ao professor achar a duvida do aluno depois.

  1. Escreva, no seu projeto, uma tabela de casos da sua regra de alerta com pelo menos seis linhas: caso normal, caso limite, caso de erro e três casos de borda. Rode cada um e anote o resultado ao lado do esperado.
  2. Rode o teste do seu projeto com um caso a menos que o que você escreveu. Ele continua passando? Se passou, o que isso diz sobre aquele caso?
  3. Crie o teste negativo da sua regra: o caso que a regra precisa recusar. Ele falha antes de você implementar a recusa? Se você já implementou, escreva de que forma a recusa foi testada.
  4. No seu projeto, leia o schema do banco e escreva a migracao que adiciona a coluna nova, em duas etapas. Confirme que a versão anterior do código ainda roda com a coluna nova, e que a versão nova roda sem a coluna antiga.
  5. Troque os projetos com o colega. Em quinze minutos, escreva três comentarios: um elogio especifico (diga o que e onde), uma duvida concreta (com a linha) e um problema encontrado (com o arquivo). Entregue os três.
  6. Passe pelas quatro portas de seguranca do projeto do colega. Nomeie um defeito de seguranca encontrado, com o arquivo e a linha. Se não houver nenhum, escreva onde você procurou.
  7. Preencha a nota final da sua lista de entrega e escreva a secao de autoavaliacao: o que eu faria diferente, em três frases com o motivo de cada uma.

Nota: 15 pontos. Critério de fim: a tabela de seis casos com resultado anotado, os três comentarios de revisao escritos, e a autoavaliacao com três frases.

Resolucao

// Aula 2 do dia 12: projeto final do ano, teste, seguranca e entrega.
//
// A aula 1 montou o produto. Esta aula e a aula do QUE FAZER ANTES DE
// ENTREGAR, e e a que separa projeto de trabalho de graduacao.
//
// A placa roda aqui quatro checagens, na ordem que o professor vai rodar a
// revisao de codigo:
//
//   1. TESTE: a regra de alerta e testada com os casos que quebram, nao so
//      com o caso que funciona. Uma maquina de estados so e provada pela
//      transicao que NAO pode existir.
//   2. MIGRACAO: o schema novo roda sobre o dado antigo, e o dado antigo
//      sobrevive. Mostrar a versao antes e depois e o que prova a migração.
//   3. SEGURANCA: a entrada nao confiavel entra pela porta de tras, e o que
//      vaza quando ela nao e tratada. Quatro portas, quatro respostas.
//   4. ENTREGA: a nota final nao e so o codigo. E o checklist, o README e a
//      autoavaliacao do que eu faria diferente.
//
// Nada aqui fala com servidor nenhum. A placa e o lugar onde a regra roda, e
// o lugar onde o teste prova que ela roda.

#include <Arduino.h>
#include <Preferences.h>
#include <esp_timer.h>

// Versao do firmware. E o que a revisao de codigo olha primeiro: duas
// versoes na mesma placa, sem registro, sao duas placas diferentes.
#define VERSAO_FIRMWARE "2.0.0"

// VerSAO do schema do servidor. A migracao do dia 12 leva o schema de 1 para
// 2; o que o professor quer ver e que a versao antiga continua legivel.
#define VERSAO_DO_SCHEMA 2

// Chave de idempotencia do trabalho enfileirado. A aula 7 mostrou que o
// mesmo envio pode chegar duas vezes; a chave e o que impede a duplicata.
#define CHAVE_DE_IDEMPOTENCIA "id-4711"

// ------------------------------------------------------------------ pinos
//
// Pinos em const int no topo, nunca numero solto no meio da funcao.
const int PIN_LED_STATUS = 2;
const int PIN_BOTAO_REVISAO = 0;

// ------------------------------------------------------------- configuracao

// Limite da regra de negocio. Mesma regra da aula 1, e o mesmo numero: mudar
// o numero em dois lugares e a forma classica de o sistema discordar de si.
const float LIMITE_DE_TEMPERATURA = 30.0;

// Tamanho do lote de teste. E o numero de casos que a revisao de codigo
// conta: regra com um caso so nao foi testada, foi executada.
const int TOTAL_DE_CASOS = 6;

// ------------------------------------------------------------------ tipos
//
// Structs antes de qualquer funcao: o preprocessador do Arduino coloca os
// prototipo em cima do arquivo, e um struct declarado no meio fica depois do
// prototipo que o usa.

struct CasoDeTeste {
  const char* nome;
  float entrada;
  bool deveDisparar;
  bool resultado;
  bool passou;
};

struct LinhaDoSchema {
  int versao;
  String colunaNova;
  String valorAntigo;
  String valorDepoisDaMigracao;
  bool dadoPreservado;
};

struct AchadoDeSeguranca {
  const char* porta;
  const char* entradaHostil;
  const char* semTratamento;
  const char* comTratamento;
  bool bloqueado;
};

struct ItemDaEntrega {
  const char* item;
  bool entregue;
  const char* ondeVerificar;
};

enum Cor { NORMAL, ALERTA, RECUSA };

// ------------------------------------------------------------------ estado

// O motor de teste. Uma maquina de estados pequena, com as tres cores que o
// painel do projeto mostra, e a funcao que decide qual delas aplicar.
bool aplicarRegraDeAlerta(float temperatura, Cor& cor) {
  if (temperatura > LIMITE_DE_TEMPERATURA) {
    cor = ALERTA;
    return true;
  }
  if (temperatura < 0.0) {
    cor = RECUSA;
    return false;
  }
  cor = NORMAL;
  return false;
}

String nomeDaCor(Cor cor) {
  switch (cor) {
    case NORMAL: return "normal";
    case ALERTA: return "alerta";
    case RECUSA: return "recusa";
  }
  return "desconhecida";
}

// ------------------------------------------------------------------ teste

// Os seis casos. Tres que passam por cima, dois que ficam dentro, e o
// ultimo que e o que o professor chama de "o caso que ninguem testa": o
// valor exatamente no limite. Sem ele, o `>` e o `>=` passam despercebidos.
CasoDeTeste casosDoProjeto[6];

// Contador de reprovados visivel no loop: se o teste falha na aula, o
// professor ve isso na linha de estado, sem precisar reler a tabela.
int totalDeReprovados = 0;

void montarCasos() {
  casosDoProjeto[0].nome = "dentro do limite";
  casosDoProjeto[0].entrada = 22.4;
  casosDoProjeto[0].deveDisparar = false;

  casosDoProjeto[1].nome = "bem acima do limite";
  casosDoProjeto[1].entrada = 34.9;
  casosDoProjeto[1].deveDisparar = true;

  casosDoProjeto[2].nome = "quase no limite, ainda nao";
  casosDoProjeto[2].entrada = 29.9;
  casosDoProjeto[2].deveDisparar = false;

  casosDoProjeto[3].nome = "exatamente no limite";
  casosDoProjeto[3].entrada = 30.0;
  casosDoProjeto[3].deveDisparar = false;

  casosDoProjeto[4].nome = "negativo: entrada invalida";
  casosDoProjeto[4].entrada = -5.0;
  casosDoProjeto[4].deveDisparar = false;

  casosDoProjeto[5].nome = "zero absoluto";
  casosDoProjeto[5].entrada = 0.0;
  casosDoProjeto[5].deveDisparar = false;
}

int executarTestes() {
  int aprovados = 0;
  int reprovados = 0;
  Cor cor = NORMAL;

  Serial.println("caso                     | entrada | esperado | obtido   | situacao");
  Serial.println("-------------------------|---------|----------|----------|----------");
  for (int i = 0; i < TOTAL_DE_CASOS; i++) {
    casosDoProjeto[i].resultado = aplicarRegraDeAlerta(casosDoProjeto[i].entrada, cor);
    casosDoProjeto[i].passou = casosDoProjeto[i].resultado == casosDoProjeto[i].deveDisparar;
    if (casosDoProjeto[i].passou) {
      aprovados++;
    } else {
      reprovados++;
    }
    String entrada = String(casosDoProjeto[i].entrada, 1);
    while (entrada.length() < 7) entrada += " ";
    Serial.println("  " + String(casosDoProjeto[i].nome) + " | " + entrada + " C | " +
                   String(casosDoProjeto[i].deveDisparar ? " alerta" : " normal") + "   | " +
                   String(casosDoProjeto[i].resultado ? " alerta" : " normal") + "   | " +
                   String(casosDoProjeto[i].passou ? "passou" : "REPROVOU"));
  }
  Serial.println();
  return reprovados;
}

void imprimirDiagnosticoDeTeste() {
  Serial.println("o caso 3 e o que segura o sistema inteiro:");
  Serial.println("  entrada 30.0, limite 30.0, resultado: " +
                 String(casosDoProjeto[3].resultado ? "alerta" : "normal"));
  Serial.println("  o codigo diz > e nao >=, entao 30.0 NAO dispara.");
  Serial.println("  se alguem trocar o > por >= no service e nao rodar o");
  Serial.println("  teste, o sistema so muda de comportamento na fronteira —");
  Serial.println("  e nenhum usuario interno reclama, so o cliente que contratou");
  Serial.println("  a regra com 'a partir de'.");
  Serial.println();
  Serial.println("regra com um caso so foi EXECUTADA. regra com seis foi TESTADA.");
  Serial.println("o caso negativo e o que prova que a regra recusa: sem ele,");
  Serial.println("uma funcao que sempre devolve 'alerta' passa em qualquer teste");
  Serial.println("que so olhe para o caminho feliz.");
  Serial.println();
}

// ------------------------------------------------------------- migracao

LinhaDoSchema historicoDoSchema[4];

void rodarMigracao() {
  Serial.println("migration: " + String(VERSAO_DO_SCHEMA - 1) + " -> " +
                 String(VERSAO_DO_SCHEMA) + ", sobre dado que ja existe");
  Serial.println();
  Serial.println("  estado  | coluna nova   | valor antigo | depois      | dado");
  Serial.println("  antes 1 | (nao existe) | 23.4 C      | 23.4 C      | preservado");
  Serial.println("  antes 1 | (nao existe) | 24.1 C      | 24.1 C      | preservado");
  Serial.println("  depois 2| alerta        | 23.4 C      | 23.4 C      | preservado");
  Serial.println("  depois 2| alerta        | 24.1 C      | 24.1 C      | preservado");
  Serial.println();

  historicoDoSchema[0].versao = VERSAO_DO_SCHEMA - 1;
  historicoDoSchema[0].colunaNova = "(nao existe)";
  historicoDoSchema[0].valorAntigo = "23.4 C";
  historicoDoSchema[0].valorDepoisDaMigracao = "23.4 C";
  historicoDoSchema[0].dadoPreservado = true;

  historicoDoSchema[1].versao = VERSAO_DO_SCHEMA;
  historicoDoSchema[1].colunaNova = "alerta";
  historicoDoSchema[1].valorAntigo = "23.4 C";
  historicoDoSchema[1].valorDepoisDaMigracao = "23.4 C";
  historicoDoSchema[1].dadoPreservado = true;

  historicoDoSchema[2].versao = VERSAO_DO_SCHEMA;
  historicoDoSchema[2].colunaNova = "alerta";
  historicoDoSchema[2].valorAntigo = "24.1 C";
  historicoDoSchema[2].valorDepoisDaMigracao = "24.1 C";
  historicoDoSchema[2].dadoPreservado = true;

  historicoDoSchema[3].versao = VERSAO_DO_SCHEMA;
  historicoDoSchema[3].colunaNova = "alerta";
  historicoDoSchema[3].valorAntigo = "(linha nova, sem valor antigo)";
  historicoDoSchema[3].valorDepoisDaMigracao = "falso";
  historicoDoSchema[3].dadoPreservado = true;

  Serial.println("tres regras que a migracao do projeto tem que cumprir:");
  Serial.println("  1. o dado que existia antes continua igual depois");
  Serial.println("  2. a coluna nova tem valor para a linha nova, sem adivinhacao");
  Serial.println("  3. da para voltar: a versao 1 ainda le a tabela da versao 2");
  Serial.println();
  Serial.println("o ponto 3 e o que o professor cobra. migracao sem volta e");
  Serial.println("aposta: se a versao nova tiver defeito, sobra uma noite para");
  Serial.println("descobrir o valor antigo de cada linha.");
  Serial.println();
  Serial.println("adicionar coluna NOT NULL em tabela cheia e a migration que");
  Serial.println("quebra producao: a tabela fica trancada enquanto ela preenche.");
  Serial.println("o caminho e em duas etapas: add nullable, preenche, so entao");
  Serial.println("torna NOT NULL. A aula 2 do dia 6 mostrou exatamente isso.");
  Serial.println();
}

// ------------------------------------------------------------ seguranca

AchadoDeSeguranca achados[4];

void revisarSeguranca() {
  achados[0].porta = "sensor DHT11";
  achados[0].entradaHostil = "valor 999.9 C, fora da faixa do sensor";
  achados[0].semTratamento = "vai para o banco e vira alerta de todo mundo";
  achados[0].comTratamento = "recusado na borda, com codigo de regra";
  achados[0].bloqueado = true;

  achados[1].porta = "campo do formulario";
  achados[1].entradaHostil = "<script> no campo de nome";
  achados[1].semTratamento = "executa no navegador de quem abriu o painel";
  achados[1].comTratamento = "escapa a saida, e textContent na tela";
  achados[1].bloqueado = true;

  achados[2].porta = "token no log";
  achados[2].entradaHostil = "Authorization: Bearer impresso no log de acesso";
  achados[2].semTratamento = "quem le o log tem o token do usuario";
  achados[2].comTratamento = "log mostra tamanho e ultimos tres caracteres";
  achados[2].bloqueado = true;

  achados[3].porta = "envio repetido da placa";
  achados[3].entradaHostil = "a mesma leitura chega duas vezes";
  achados[3].semTratamento = "o alerta dispara duas vezes para um evento";
  achados[3].comTratamento = "chave de idempotencia no cabecalho";
  achados[3].bloqueado = true;

  Serial.println("porta                    | entrada hostil | bloqueado");
  Serial.println("-------------------------|----------------|-----------");
  for (int i = 0; i < 4; i++) {
    String porta = achados[i].porta;
    while (porta.length() < 25) porta += " ";
    String hostil = achados[i].entradaHostil;
    while (hostil.length() < 14) hostil += " ";
    Serial.println("  " + porta + " | " + hostil + " | " +
                   String(achados[i].bloqueado ? "sim" : "NAO"));
  }
  Serial.println();

  Serial.println("as quatro portas, uma por vez:");
  for (int i = 0; i < 4; i++) {
    Serial.println("  " + String(achados[i].porta));
    Serial.println("    sem tratamento: " + String(achados[i].semTratamento));
    Serial.println("    com tratamento: " + String(achados[i].comTratamento));
  }
  Serial.println();

  Serial.println("a defense que faltava no projeto de dois alunos:");
  Serial.println("  chave de idempotencia do trabalho enfileirado: " +
                 String(CHAVE_DE_IDEMPOTENCIA));
  Serial.println("  senha do WiFi escrita no firmware, em texto, e enviada");
  Serial.println("  junto do binario. Quem tem o arquivo tem a rede.");
  Serial.println("  o caminho e provisionamento: a placa entra em modo Access");
  Serial.println("  Point sem senha conhecida, o celular entrega a rede, e a");
  Serial.println("  placa guarda na memoria persistente, fora do binario.");
  Serial.println();
}

// -------------------------------------------------------------- entrega

ItemDaEntrega itensDaEntrega[8];

void montarChecklist() {
  itensDaEntrega[0].item = "README com instalacao e uso";
  itensDaEntrega[0].entregue = true;
  itensDaEntrega[0].ondeVerificar = "README.md, secao Instalar e Rodar";

  itensDaEntrega[1].item = "teste que roda sem banco";
  itensDaEntrega[1].entregue = true;
  itensDaEntrega[1].ondeVerificar = "npm test, com saida esperada";

  itensDaEntrega[2].item = "migracao com versao e volta";
  itensDaEntrega[2].entregue = true;
  itensDaEntrega[2].ondeVerificar = "pasta de migracoes, dois arquivos por passo";

  itensDaEntrega[3].item = "log sem segredo";
  itensDaEntrega[3].entregue = true;
  itensDaEntrega[3].ondeVerificar = "grep por token no log de acesso";

  itensDaEntrega[4].item = "permissao checada no servidor";
  itensDaEntrega[4].entregue = true;
  itensDaEntrega[4].ondeVerificar = "cada rota, nao so o menu";

  itensDaEntrega[5].item = "chave de idempotencia no envio";
  itensDaEntrega[5].entregue = true;
  itensDaEntrega[5].ondeVerificar = "cabecalho da placa e tabela do servidor";

  itensDaEntrega[6].item = "instrucao de uso escrita para leigo";
  itensDaEntrega[6].entregue = false;
  itensDaEntrega[6].ondeVerificar = "ninguem de fora consegue rodar o passo 3";

  itensDaEntrega[7].item = "o que eu faria diferente";
  itensDaEntrega[7].entregue = false;
  itensDaEntrega[7].ondeVerificar = "secao do README chamada Decisoes";
}

void imprimirEntrega() {
  Serial.println("item                                    | entregue | onde verificar");
  Serial.println("----------------------------------------|----------|---------------------");
  int entregues = 0;
  for (int i = 0; i < 8; i++) {
    String item = itensDaEntrega[i].item;
    while (item.length() < 38) item += " ";
    String entregue = itensDaEntrega[i].entregue ? "sim" : "nao";
    while (entregue.length() < 8) entregue += " ";
    if (itensDaEntrega[i].entregue) entregues++;
    Serial.println("  " + item + " | " + entregue + " | " + itensDaEntrega[i].ondeVerificar);
  }
  Serial.println();
  Serial.println("entregues: " + String(entregues) + " de 8");
  Serial.println();
  Serial.println("os dois que faltaram sao os dois que nenhum professor cobra");
  Serial.println("por modem, e sao os dois que o cliente do projeto reclama:");
  Serial.println("  o item 6 e o primeiro 'nao consegui rodar' que chega por e-mail");
  Serial.println("  o item 7 e a unica prova de que voce aprendeu alguma coisa");
  Serial.println();
  Serial.println("code review: como ler o codigo do colega em 15 minutos");
  Serial.println("  1. roda o teste do colega e ve se passa");
  Serial.println("  2. procura a regra de negocio e ve se o numero esta no codigo");
  Serial.println("  3. procura onde o log e escrito e ve se tem segredo");
  Serial.println("  4. procura a consulta sem indice e sem paginacao");
  Serial.println("  5. escreve tres comentarios: um defeito, uma duvida, um elogio");
  Serial.println("  o item 5 e o que o professor exige: elogio especifico, nao elogio");
  Serial.println("  generico. 'bom trabalho' nao diz nada e nao ajuda ninguem.");
  Serial.println();
  Serial.println("defeito de seguranca que o professor procura primeiro:");
  Serial.println("  dado do outro usuario no log de acesso, e o token no log.");
  Serial.println("  o segundo e pior: quem le o log nao precisava estar logado.");
  Serial.println();
}

void imprimirAutoavaliacao() {
  Serial.println("nota final: o que entra na nota e o que nao entra");
  Serial.println("  entra: o que roda, o que tem teste, o que tem migracao");
  Serial.println("  entra: a seguranca das quatro portas");
  Serial.println("  entra: o README, porque entrega e comunicacao, nao so codigo");
  Serial.println("  nao entra: quantidade de linhas");
  Serial.println("  nao entra: quantas bibliotecas o projeto usa");
  Serial.println("  nao entra: se o codigo e bonito");
  Serial.println();
  Serial.println("o que eu faria diferente — tres respostas que o professor le:");
  Serial.println("  1. eu comecaria pelo teste da regra, e nao pela migration.");
  Serial.println("     eu escrevi a migration antes de saber o que a regra faz,");
  Serial.println("     e descobri o numero errado depois de rodar em copia.");
  Serial.println("  2. eu colocaria o limite num unico lugar, e nao em tres.");
  Serial.println("     o numero estava no service, no painel e no README, e um");
  Serial.println("     deles ficou com o valor velho.");
  Serial.println("  3. eu logaria o tempo de resposta de cada consulta desde o");
  Serial.println("     primeiro dia, e nao so quando o servidor ficou lento.");
  Serial.println();
  Serial.println("respostas assim valem mais que um projeto com tres vezes mais");
  Serial.println("funcionalidade: elas dizem que voce sabe onde o sistema quebra.");
}

// ---------------------------------------------------------------- setup

void setup() {
  Serial.begin(115200);
  delay(300);

  pinMode(PIN_LED_STATUS, OUTPUT);
  pinMode(PIN_BOTAO_REVISAO, INPUT_PULLUP);
  digitalWrite(PIN_LED_STATUS, HIGH);

  Serial.println();
  Serial.println("========================================");
  Serial.println(" projeto final — revisao e entrega v" + String(VERSAO_FIRMWARE));
  Serial.println("========================================");
  Serial.println("schema do servidor: versao " + String(VERSAO_DO_SCHEMA));
  Serial.println("heap: " + String(ESP.getFreeHeap()) + " bytes");
  Serial.println();

  Serial.println("========================================");
  Serial.println(" 1. teste — a regra prove nos casos que quebram");
  Serial.println("========================================");
  montarCasos();
  int reprovados = executarTestes();
  totalDeReprovados = reprovados;
  imprimirDiagnosticoDeTeste();

  Serial.println("  resultado: " + String(TOTAL_DE_CASOS - reprovados) + " de " +
                 String(TOTAL_DE_CASOS) + " casos passaram");
  Serial.println();

  Serial.println("========================================");
  Serial.println(" 2. migracao — o dado velho continua legivel");
  Serial.println("========================================");
  rodarMigracao();

  Serial.println("========================================");
  Serial.println(" 3. seguranca — as quatro portas do projeto");
  Serial.println("========================================");
  revisarSeguranca();

  Serial.println("========================================");
  Serial.println(" 4. entrega — o que mais conta na nota");
  Serial.println("========================================");
  montarChecklist();
  imprimirEntrega();
  imprimirAutoavaliacao();

  Serial.println("========================================");
  Serial.println(" revisao concluida");
  Serial.println("========================================");
  Serial.println("aperte o botao para a placa marcar o ponto de inspecao.");
}

void loop() {
  digitalWrite(PIN_LED_STATUS, !digitalRead(PIN_LED_STATUS));
  Serial.println("[revisao] viva | v" + String(VERSAO_FIRMWARE) + " | heap " +
                 String(ESP.getFreeHeap()) + " B | reprovados: " + String(totalDeReprovados) +
                 " | schema v" + String(VERSAO_DO_SCHEMA));
  delay(8000);

  if (digitalRead(PIN_BOTAO_REVISAO) == LOW) {
    Serial.println("[manual] ponto de inspecao marcado | heap " +
                   String(ESP.getFreeHeap()) + " B");
  }
}

Por que assim e não de outro jeito. A placa roda a revisao, e não o projeto. Isso e uma escolha de aula: o teste do projeto so roda de verdade no computador do aluno, com banco, com sessão e com o painel inteiro. Na placa, o que da para fazer e rodar a regra — a parte que decide — nos casos que importam, e mostrar o resultado ao lado do esperado.

aplicarRegraDeAlerta tem três saídas, e não duas: NORMAL, ALERTA e RECUSA. A recusa e o que separa um teste de execução de um teste de verdade, porque e o unico caminho que não passa pelo valor negativo. O caso negativo da resolucao e a entrada -5.0, que a regra recusa em vez de classificar.

A função montarCasos existe para o professor poder mostrar que o caso limite e uma linha de tabela, não um improviso. Os seis casos estão nomeados; o nome e o que permite apontar "esse aqui" na lousa sem ler o número em voz alta.

executarTestes devolve a contagem de reprovados e não para no primeiro erro. Parar no primeiro erro e o comportamento padrao de compilador e serve para o compilador; para teste de regra, mostrar todos os casos que falharam e o que permite corrigir os três de uma vez.

A secao de migracao mostra os quatro estados, com o dado antes e depois, e o professor aponta a coluna preservado quatro vezes seguidas. A palavra "rollback" aparece no texto porque e o termo que o cliente vai usar quando a migracao der errado; quem não conhece o nome da manobra de volta não consegue pedir a volta.

A revisao de seguranca monta as quatro portas e imprime as duas colunas — o que acontece sem tratamento e o que acontece com. A coluna do meio e a mais importante: e o defeito real, escrito como o usuario veria.

A lista de entrega tem dois itens marcados como não entregues, de propósito. O professor aponta que são exatamente os dois que ninguem cobra por modem e os dois que o cliente reclama: instrução de uso e o que eu faria diferente. Entrega e comunicacao, não so código.

Criterios de correcao

CritérioPontos
Tabela com seis casos, incluindo o limite e o de erro, com resultado ao lado do esperado3 pontos
Item 2 respondido: o que o teste sem aquele caso deixa passar2 pontos
Teste negativo escrito, com a forma da recusa testada2 pontos
Migracao em duas etapas, com as duas compatibilidades conferidas3 pontos
Três comentarios de revisao escritos, com elogio especifico, duvida com linha e problema com arquivo2 pontos
Item 6 respondido: um defeito de seguranca nomeado, com arquivo e linha1 ponto
Item 4 conferido também com o deploy do projeto, do build até o health check1 ponto
Autoavaliacao em três frases, cada uma com o motivo1 ponto

Erros comuns

ErroComo apareceCorrecao
Teste com um caso soexpect(temAlerta(23.5)).toBe(false) e pronto"Um caso e execução. Escreva o caso limite, o caso de erro e um de cada borda. Sem eles, a regra esta sem prova."
Teste que so olha o caminho feliza função que sempre devolve alerta passa"Inclua o caso negativo. E ele que prova que a regra recusa, e não so que ela aceita."
Corrigir o código antes de o teste falharo teste passa de primeira e ninguem sabe se ele testa algo"Primeiro o teste vermelho, depois o arranjo. Se passou de primeira, o teste não cobria o defeito — e isso e um defeito maior."
Migracao com NOT NULL em tabela cheiaa tabela trancada e o servidor fora"Duas etapas: coluna vazia, preenche, so então obrigatoria. E o que a aula 2 do dia 6 mostrou."
Migracao sem volta, porque "o schema novo e melhor"o cliente pede voltar e não ha caminho"Toda migracao precisa de caminho de volta. Se não tem, a versão antiga foi jogada fora cedo demais."
Rollback escrito como "desfaz e refaz" no READMEninguem sabe qual arquivo rodar para tras"O caminho de volta e um arquivo versionado, com o mesmo número da migracao. Ele esta no repositorio, junto do caminho de ida."
Testar a migracao so no banco de desenvolvimentoa producao tem dado que o desenvolvimento nunca viu"Copie a fonte da verdade: o dump de producao, não um dado inventado. Migracao que so roda em dado de teste roda em dado de teste."
Defeito de seguranca achado sendo o mais obvioo aluno aponta a SQL injection que já foi corrigida no dia 9"Procure o que sobrou: token no log, dado de um usuario no log de outro, e a porta de tras da leitura duplicada. A injecao já era."
Revisao de código elogiando generico"bom trabalho, gostei""Elogio especifico: 'a consulta do historico tem índice e página, e o painel pede 20 por vez'. Isso diz o que foi bom e o que deve ser repetido."
Revisao achando problema e não dando caminho"aqui tem um bug""Problema com caminho e util: 'a consulta varre a tabela; sugiro índice em dispositivo e instante'. Problema sem caminho e so quedacao."
Mock e repository falso skipped por serem "so teste"o teste so roda com banco de verdade"Sem substituto, o teste precisa de banco, de rede e de uma maquina preparada. Isso e o que faz a suite ser pulada. O duble e o que permite rodar em qualquer lugar."
nota final calculada so pelo códigoprojeto sem README recebe nota cheia"Entrega e comunicacao. Sem instrução de uso, o cliente não roda. Isso conta na nota final."
Autoavaliacao em frase generica"eu faria diferente a parte de banco""Três frases, cada uma com o motivo: o que, por que, e o que você faria em vez disso. 'Faria diferente' sem motivo e o mesmo frase com mais palavras."
Ensaio de deploy em cima da maquina da aulaporta ocupada e o aluno conclui que o erro e dele"Antes de subir, veja o que já esta rodando na maquina. O erro de porta ocupada e o melhor aluno: ele nomeia a causa."

Desafio extra

Escreva um regressao para o seu projeto: pegue um defeito já corrigido, apague a correcao de propósito, confirme que o teste falha, restaure a correcao e confirme que passa. Meça quanto tempo levou para o teste avisar. Depois repita o processo em três defeitos diferentes e escreva uma frase sobre qual deles o seu teste demorava mais para achar — e o que isso diz sobre a cobertura da sua suite.

>

A resolucao, compilada

// Aula 2 do dia 12: projeto final do ano, teste, seguranca e entrega.
//
// A aula 1 montou o produto. Esta aula e a aula do QUE FAZER ANTES DE
// ENTREGAR, e e a que separa projeto de trabalho de graduacao.
//
// A placa roda aqui quatro checagens, na ordem que o professor vai rodar a
// revisao de codigo:
//
//   1. TESTE: a regra de alerta e testada com os casos que quebram, nao so
//      com o caso que funciona. Uma maquina de estados so e provada pela
//      transicao que NAO pode existir.
//   2. MIGRACAO: o schema novo roda sobre o dado antigo, e o dado antigo
//      sobrevive. Mostrar a versao antes e depois e o que prova a migração.
//   3. SEGURANCA: a entrada nao confiavel entra pela porta de tras, e o que
//      vaza quando ela nao e tratada. Quatro portas, quatro respostas.
//   4. ENTREGA: a nota final nao e so o codigo. E o checklist, o README e a
//      autoavaliacao do que eu faria diferente.
//
// Nada aqui fala com servidor nenhum. A placa e o lugar onde a regra roda, e
// o lugar onde o teste prova que ela roda.

#include <Arduino.h>
#include <Preferences.h>
#include <esp_timer.h>

// Versao do firmware. E o que a revisao de codigo olha primeiro: duas
// versoes na mesma placa, sem registro, sao duas placas diferentes.
#define VERSAO_FIRMWARE "2.0.0"

// VerSAO do schema do servidor. A migracao do dia 12 leva o schema de 1 para
// 2; o que o professor quer ver e que a versao antiga continua legivel.
#define VERSAO_DO_SCHEMA 2

// Chave de idempotencia do trabalho enfileirado. A aula 7 mostrou que o
// mesmo envio pode chegar duas vezes; a chave e o que impede a duplicata.
#define CHAVE_DE_IDEMPOTENCIA "id-4711"

// ------------------------------------------------------------------ pinos
//
// Pinos em const int no topo, nunca numero solto no meio da funcao.
const int PIN_LED_STATUS = 2;
const int PIN_BOTAO_REVISAO = 0;

// ------------------------------------------------------------- configuracao

// Limite da regra de negocio. Mesma regra da aula 1, e o mesmo numero: mudar
// o numero em dois lugares e a forma classica de o sistema discordar de si.
const float LIMITE_DE_TEMPERATURA = 30.0;

// Tamanho do lote de teste. E o numero de casos que a revisao de codigo
// conta: regra com um caso so nao foi testada, foi executada.
const int TOTAL_DE_CASOS = 6;

// ------------------------------------------------------------------ tipos
//
// Structs antes de qualquer funcao: o preprocessador do Arduino coloca os
// prototipo em cima do arquivo, e um struct declarado no meio fica depois do
// prototipo que o usa.

struct CasoDeTeste {
  const char* nome;
  float entrada;
  bool deveDisparar;
  bool resultado;
  bool passou;
};

struct LinhaDoSchema {
  int versao;
  String colunaNova;
  String valorAntigo;
  String valorDepoisDaMigracao;
  bool dadoPreservado;
};

struct AchadoDeSeguranca {
  const char* porta;
  const char* entradaHostil;
  const char* semTratamento;
  const char* comTratamento;
  bool bloqueado;
};

struct ItemDaEntrega {
  const char* item;
  bool entregue;
  const char* ondeVerificar;
};

enum Cor { NORMAL, ALERTA, RECUSA };

// ------------------------------------------------------------------ estado

// O motor de teste. Uma maquina de estados pequena, com as tres cores que o
// painel do projeto mostra, e a funcao que decide qual delas aplicar.
bool aplicarRegraDeAlerta(float temperatura, Cor& cor) {
  if (temperatura > LIMITE_DE_TEMPERATURA) {
    cor = ALERTA;
    return true;
  }
  if (temperatura < 0.0) {
    cor = RECUSA;
    return false;
  }
  cor = NORMAL;
  return false;
}

String nomeDaCor(Cor cor) {
  switch (cor) {
    case NORMAL: return "normal";
    case ALERTA: return "alerta";
    case RECUSA: return "recusa";
  }
  return "desconhecida";
}

// ------------------------------------------------------------------ teste

// Os seis casos. Tres que passam por cima, dois que ficam dentro, e o
// ultimo que e o que o professor chama de "o caso que ninguem testa": o
// valor exatamente no limite. Sem ele, o `>` e o `>=` passam despercebidos.
CasoDeTeste casosDoProjeto[6];

// Contador de reprovados visivel no loop: se o teste falha na aula, o
// professor ve isso na linha de estado, sem precisar reler a tabela.
int totalDeReprovados = 0;

void montarCasos() {
  casosDoProjeto[0].nome = "dentro do limite";
  casosDoProjeto[0].entrada = 22.4;
  casosDoProjeto[0].deveDisparar = false;

  casosDoProjeto[1].nome = "bem acima do limite";
  casosDoProjeto[1].entrada = 34.9;
  casosDoProjeto[1].deveDisparar = true;

  casosDoProjeto[2].nome = "quase no limite, ainda nao";
  casosDoProjeto[2].entrada = 29.9;
  casosDoProjeto[2].deveDisparar = false;

  casosDoProjeto[3].nome = "exatamente no limite";
  casosDoProjeto[3].entrada = 30.0;
  casosDoProjeto[3].deveDisparar = false;

  casosDoProjeto[4].nome = "negativo: entrada invalida";
  casosDoProjeto[4].entrada = -5.0;
  casosDoProjeto[4].deveDisparar = false;

  casosDoProjeto[5].nome = "zero absoluto";
  casosDoProjeto[5].entrada = 0.0;
  casosDoProjeto[5].deveDisparar = false;
}

int executarTestes() {
  int aprovados = 0;
  int reprovados = 0;
  Cor cor = NORMAL;

  Serial.println("caso                     | entrada | esperado | obtido   | situacao");
  Serial.println("-------------------------|---------|----------|----------|----------");
  for (int i = 0; i < TOTAL_DE_CASOS; i++) {
    casosDoProjeto[i].resultado = aplicarRegraDeAlerta(casosDoProjeto[i].entrada, cor);
    casosDoProjeto[i].passou = casosDoProjeto[i].resultado == casosDoProjeto[i].deveDisparar;
    if (casosDoProjeto[i].passou) {
      aprovados++;
    } else {
      reprovados++;
    }
    String entrada = String(casosDoProjeto[i].entrada, 1);
    while (entrada.length() < 7) entrada += " ";
    Serial.println("  " + String(casosDoProjeto[i].nome) + " | " + entrada + " C | " +
                   String(casosDoProjeto[i].deveDisparar ? " alerta" : " normal") + "   | " +
                   String(casosDoProjeto[i].resultado ? " alerta" : " normal") + "   | " +
                   String(casosDoProjeto[i].passou ? "passou" : "REPROVOU"));
  }
  Serial.println();
  return reprovados;
}

void imprimirDiagnosticoDeTeste() {
  Serial.println("o caso 3 e o que segura o sistema inteiro:");
  Serial.println("  entrada 30.0, limite 30.0, resultado: " +
                 String(casosDoProjeto[3].resultado ? "alerta" : "normal"));
  Serial.println("  o codigo diz > e nao >=, entao 30.0 NAO dispara.");
  Serial.println("  se alguem trocar o > por >= no service e nao rodar o");
  Serial.println("  teste, o sistema so muda de comportamento na fronteira —");
  Serial.println("  e nenhum usuario interno reclama, so o cliente que contratou");
  Serial.println("  a regra com 'a partir de'.");
  Serial.println();
  Serial.println("regra com um caso so foi EXECUTADA. regra com seis foi TESTADA.");
  Serial.println("o caso negativo e o que prova que a regra recusa: sem ele,");
  Serial.println("uma funcao que sempre devolve 'alerta' passa em qualquer teste");
  Serial.println("que so olhe para o caminho feliz.");
  Serial.println();
}

// ------------------------------------------------------------- migracao

LinhaDoSchema historicoDoSchema[4];

void rodarMigracao() {
  Serial.println("migration: " + String(VERSAO_DO_SCHEMA - 1) + " -> " +
                 String(VERSAO_DO_SCHEMA) + ", sobre dado que ja existe");
  Serial.println();
  Serial.println("  estado  | coluna nova   | valor antigo | depois      | dado");
  Serial.println("  antes 1 | (nao existe) | 23.4 C      | 23.4 C      | preservado");
  Serial.println("  antes 1 | (nao existe) | 24.1 C      | 24.1 C      | preservado");
  Serial.println("  depois 2| alerta        | 23.4 C      | 23.4 C      | preservado");
  Serial.println("  depois 2| alerta        | 24.1 C      | 24.1 C      | preservado");
  Serial.println();

  historicoDoSchema[0].versao = VERSAO_DO_SCHEMA - 1;
  historicoDoSchema[0].colunaNova = "(nao existe)";
  historicoDoSchema[0].valorAntigo = "23.4 C";
  historicoDoSchema[0].valorDepoisDaMigracao = "23.4 C";
  historicoDoSchema[0].dadoPreservado = true;

  historicoDoSchema[1].versao = VERSAO_DO_SCHEMA;
  historicoDoSchema[1].colunaNova = "alerta";
  historicoDoSchema[1].valorAntigo = "23.4 C";
  historicoDoSchema[1].valorDepoisDaMigracao = "23.4 C";
  historicoDoSchema[1].dadoPreservado = true;

  historicoDoSchema[2].versao = VERSAO_DO_SCHEMA;
  historicoDoSchema[2].colunaNova = "alerta";
  historicoDoSchema[2].valorAntigo = "24.1 C";
  historicoDoSchema[2].valorDepoisDaMigracao = "24.1 C";
  historicoDoSchema[2].dadoPreservado = true;

  historicoDoSchema[3].versao = VERSAO_DO_SCHEMA;
  historicoDoSchema[3].colunaNova = "alerta";
  historicoDoSchema[3].valorAntigo = "(linha nova, sem valor antigo)";
  historicoDoSchema[3].valorDepoisDaMigracao = "falso";
  historicoDoSchema[3].dadoPreservado = true;

  Serial.println("tres regras que a migracao do projeto tem que cumprir:");
  Serial.println("  1. o dado que existia antes continua igual depois");
  Serial.println("  2. a coluna nova tem valor para a linha nova, sem adivinhacao");
  Serial.println("  3. da para voltar: a versao 1 ainda le a tabela da versao 2");
  Serial.println();
  Serial.println("o ponto 3 e o que o professor cobra. migracao sem volta e");
  Serial.println("aposta: se a versao nova tiver defeito, sobra uma noite para");
  Serial.println("descobrir o valor antigo de cada linha.");
  Serial.println();
  Serial.println("adicionar coluna NOT NULL em tabela cheia e a migration que");
  Serial.println("quebra producao: a tabela fica trancada enquanto ela preenche.");
  Serial.println("o caminho e em duas etapas: add nullable, preenche, so entao");
  Serial.println("torna NOT NULL. A aula 2 do dia 6 mostrou exatamente isso.");
  Serial.println();
}

// ------------------------------------------------------------ seguranca

AchadoDeSeguranca achados[4];

void revisarSeguranca() {
  achados[0].porta = "sensor DHT11";
  achados[0].entradaHostil = "valor 999.9 C, fora da faixa do sensor";
  achados[0].semTratamento = "vai para o banco e vira alerta de todo mundo";
  achados[0].comTratamento = "recusado na borda, com codigo de regra";
  achados[0].bloqueado = true;

  achados[1].porta = "campo do formulario";
  achados[1].entradaHostil = "<script> no campo de nome";
  achados[1].semTratamento = "executa no navegador de quem abriu o painel";
  achados[1].comTratamento = "escapa a saida, e textContent na tela";
  achados[1].bloqueado = true;

  achados[2].porta = "token no log";
  achados[2].entradaHostil = "Authorization: Bearer impresso no log de acesso";
  achados[2].semTratamento = "quem le o log tem o token do usuario";
  achados[2].comTratamento = "log mostra tamanho e ultimos tres caracteres";
  achados[2].bloqueado = true;

  achados[3].porta = "envio repetido da placa";
  achados[3].entradaHostil = "a mesma leitura chega duas vezes";
  achados[3].semTratamento = "o alerta dispara duas vezes para um evento";
  achados[3].comTratamento = "chave de idempotencia no cabecalho";
  achados[3].bloqueado = true;

  Serial.println("porta                    | entrada hostil | bloqueado");
  Serial.println("-------------------------|----------------|-----------");
  for (int i = 0; i < 4; i++) {
    String porta = achados[i].porta;
    while (porta.length() < 25) porta += " ";
    String hostil = achados[i].entradaHostil;
    while (hostil.length() < 14) hostil += " ";
    Serial.println("  " + porta + " | " + hostil + " | " +
                   String(achados[i].bloqueado ? "sim" : "NAO"));
  }
  Serial.println();

  Serial.println("as quatro portas, uma por vez:");
  for (int i = 0; i < 4; i++) {
    Serial.println("  " + String(achados[i].porta));
    Serial.println("    sem tratamento: " + String(achados[i].semTratamento));
    Serial.println("    com tratamento: " + String(achados[i].comTratamento));
  }
  Serial.println();

  Serial.println("a defense que faltava no projeto de dois alunos:");
  Serial.println("  chave de idempotencia do trabalho enfileirado: " +
                 String(CHAVE_DE_IDEMPOTENCIA));
  Serial.println("  senha do WiFi escrita no firmware, em texto, e enviada");
  Serial.println("  junto do binario. Quem tem o arquivo tem a rede.");
  Serial.println("  o caminho e provisionamento: a placa entra em modo Access");
  Serial.println("  Point sem senha conhecida, o celular entrega a rede, e a");
  Serial.println("  placa guarda na memoria persistente, fora do binario.");
  Serial.println();
}

// -------------------------------------------------------------- entrega

ItemDaEntrega itensDaEntrega[8];

void montarChecklist() {
  itensDaEntrega[0].item = "README com instalacao e uso";
  itensDaEntrega[0].entregue = true;
  itensDaEntrega[0].ondeVerificar = "README.md, secao Instalar e Rodar";

  itensDaEntrega[1].item = "teste que roda sem banco";
  itensDaEntrega[1].entregue = true;
  itensDaEntrega[1].ondeVerificar = "npm test, com saida esperada";

  itensDaEntrega[2].item = "migracao com versao e volta";
  itensDaEntrega[2].entregue = true;
  itensDaEntrega[2].ondeVerificar = "pasta de migracoes, dois arquivos por passo";

  itensDaEntrega[3].item = "log sem segredo";
  itensDaEntrega[3].entregue = true;
  itensDaEntrega[3].ondeVerificar = "grep por token no log de acesso";

  itensDaEntrega[4].item = "permissao checada no servidor";
  itensDaEntrega[4].entregue = true;
  itensDaEntrega[4].ondeVerificar = "cada rota, nao so o menu";

  itensDaEntrega[5].item = "chave de idempotencia no envio";
  itensDaEntrega[5].entregue = true;
  itensDaEntrega[5].ondeVerificar = "cabecalho da placa e tabela do servidor";

  itensDaEntrega[6].item = "instrucao de uso escrita para leigo";
  itensDaEntrega[6].entregue = false;
  itensDaEntrega[6].ondeVerificar = "ninguem de fora consegue rodar o passo 3";

  itensDaEntrega[7].item = "o que eu faria diferente";
  itensDaEntrega[7].entregue = false;
  itensDaEntrega[7].ondeVerificar = "secao do README chamada Decisoes";
}

void imprimirEntrega() {
  Serial.println("item                                    | entregue | onde verificar");
  Serial.println("----------------------------------------|----------|---------------------");
  int entregues = 0;
  for (int i = 0; i < 8; i++) {
    String item = itensDaEntrega[i].item;
    while (item.length() < 38) item += " ";
    String entregue = itensDaEntrega[i].entregue ? "sim" : "nao";
    while (entregue.length() < 8) entregue += " ";
    if (itensDaEntrega[i].entregue) entregues++;
    Serial.println("  " + item + " | " + entregue + " | " + itensDaEntrega[i].ondeVerificar);
  }
  Serial.println();
  Serial.println("entregues: " + String(entregues) + " de 8");
  Serial.println();
  Serial.println("os dois que faltaram sao os dois que nenhum professor cobra");
  Serial.println("por modem, e sao os dois que o cliente do projeto reclama:");
  Serial.println("  o item 6 e o primeiro 'nao consegui rodar' que chega por e-mail");
  Serial.println("  o item 7 e a unica prova de que voce aprendeu alguma coisa");
  Serial.println();
  Serial.println("code review: como ler o codigo do colega em 15 minutos");
  Serial.println("  1. roda o teste do colega e ve se passa");
  Serial.println("  2. procura a regra de negocio e ve se o numero esta no codigo");
  Serial.println("  3. procura onde o log e escrito e ve se tem segredo");
  Serial.println("  4. procura a consulta sem indice e sem paginacao");
  Serial.println("  5. escreve tres comentarios: um defeito, uma duvida, um elogio");
  Serial.println("  o item 5 e o que o professor exige: elogio especifico, nao elogio");
  Serial.println("  generico. 'bom trabalho' nao diz nada e nao ajuda ninguem.");
  Serial.println();
  Serial.println("defeito de seguranca que o professor procura primeiro:");
  Serial.println("  dado do outro usuario no log de acesso, e o token no log.");
  Serial.println("  o segundo e pior: quem le o log nao precisava estar logado.");
  Serial.println();
}

void imprimirAutoavaliacao() {
  Serial.println("nota final: o que entra na nota e o que nao entra");
  Serial.println("  entra: o que roda, o que tem teste, o que tem migracao");
  Serial.println("  entra: a seguranca das quatro portas");
  Serial.println("  entra: o README, porque entrega e comunicacao, nao so codigo");
  Serial.println("  nao entra: quantidade de linhas");
  Serial.println("  nao entra: quantas bibliotecas o projeto usa");
  Serial.println("  nao entra: se o codigo e bonito");
  Serial.println();
  Serial.println("o que eu faria diferente — tres respostas que o professor le:");
  Serial.println("  1. eu comecaria pelo teste da regra, e nao pela migration.");
  Serial.println("     eu escrevi a migration antes de saber o que a regra faz,");
  Serial.println("     e descobri o numero errado depois de rodar em copia.");
  Serial.println("  2. eu colocaria o limite num unico lugar, e nao em tres.");
  Serial.println("     o numero estava no service, no painel e no README, e um");
  Serial.println("     deles ficou com o valor velho.");
  Serial.println("  3. eu logaria o tempo de resposta de cada consulta desde o");
  Serial.println("     primeiro dia, e nao so quando o servidor ficou lento.");
  Serial.println();
  Serial.println("respostas assim valem mais que um projeto com tres vezes mais");
  Serial.println("funcionalidade: elas dizem que voce sabe onde o sistema quebra.");
}

// ---------------------------------------------------------------- setup

void setup() {
  Serial.begin(115200);
  delay(300);

  pinMode(PIN_LED_STATUS, OUTPUT);
  pinMode(PIN_BOTAO_REVISAO, INPUT_PULLUP);
  digitalWrite(PIN_LED_STATUS, HIGH);

  Serial.println();
  Serial.println("========================================");
  Serial.println(" projeto final — revisao e entrega v" + String(VERSAO_FIRMWARE));
  Serial.println("========================================");
  Serial.println("schema do servidor: versao " + String(VERSAO_DO_SCHEMA));
  Serial.println("heap: " + String(ESP.getFreeHeap()) + " bytes");
  Serial.println();

  Serial.println("========================================");
  Serial.println(" 1. teste — a regra prove nos casos que quebram");
  Serial.println("========================================");
  montarCasos();
  int reprovados = executarTestes();
  totalDeReprovados = reprovados;
  imprimirDiagnosticoDeTeste();

  Serial.println("  resultado: " + String(TOTAL_DE_CASOS - reprovados) + " de " +
                 String(TOTAL_DE_CASOS) + " casos passaram");
  Serial.println();

  Serial.println("========================================");
  Serial.println(" 2. migracao — o dado velho continua legivel");
  Serial.println("========================================");
  rodarMigracao();

  Serial.println("========================================");
  Serial.println(" 3. seguranca — as quatro portas do projeto");
  Serial.println("========================================");
  revisarSeguranca();

  Serial.println("========================================");
  Serial.println(" 4. entrega — o que mais conta na nota");
  Serial.println("========================================");
  montarChecklist();
  imprimirEntrega();
  imprimirAutoavaliacao();

  Serial.println("========================================");
  Serial.println(" revisao concluida");
  Serial.println("========================================");
  Serial.println("aperte o botao para a placa marcar o ponto de inspecao.");
}

void loop() {
  digitalWrite(PIN_LED_STATUS, !digitalRead(PIN_LED_STATUS));
  Serial.println("[revisao] viva | v" + String(VERSAO_FIRMWARE) + " | heap " +
                 String(ESP.getFreeHeap()) + " B | reprovados: " + String(totalDeReprovados) +
                 " | schema v" + String(VERSAO_DO_SCHEMA));
  delay(8000);

  if (digitalRead(PIN_BOTAO_REVISAO) == LOW) {
    Serial.println("[manual] ponto de inspecao marcado | heap " +
                   String(ESP.getFreeHeap()) + " B");
  }
}

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