Projeto final do ano — Arduino e IoT — semana 12 do 3o trimestre
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
READMEque 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
MySQLdo projeto, ogit loge onpm 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ão | O que o professor procura | Defeito quando está errado |
|---|---|---|
a senha entra em hash com sal por usuário | dois usuários com a mesma senha têm hash diferente | a tabela de senha parece correta e é idêntica linha a linha |
o hash nunca volta para o cliente | o cadastro responde id e nome, e nada mais | a resposta de cadastro devolve o campo da senha |
| a sessão tem validade e destino | o logout invalida no servidor, não no navegador | o 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 roda | Custo | Quando acontece |
|---|---|---|
| uma vez no carregamento da página | uma consulta por sessão aberta | quando o usuário entra |
| a cada quadro da animação | sessenta consultas por segundo de tela | enquanto o gráfico se move |
| uma vez por página pedida | uma consulta por troca de página | quando 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?*
| Pergunta | O que a linha de log precisa ter | O que acontece sem isso |
|---|---|---|
| de qual placa veio o alerta | identificador do dispositivo | a investigação depende de alguém lembrar da ordem das abas |
| em que segundo do uptime | tempo desde o boot | "foi ontem" não localiza nada |
| com que versão de firmware | versão do firmware | a placa antiga e a nova se confundem no mesmo log |
| o que o servidor respondeu | status e motivo | o 403 vira "não funcionou" |
| qual requisição é esta | identificador de correlação | a 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:
- o que roda antes do programa? (instalação)
- o que o programa faz? (uso)
- 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.
- Escreva, no seu servidor, o cadastro de usuário e o
login, com a senha entrando emhashe sal por usuário. Confirme em dois arquivos que ohashdo mesmo texto muda entre dois usuários, e que a resposta docadastronão devolve o campo da senha. - Na sua placa, garanta que o
logmostra 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á. - 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 um403em uma frase. - 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.9e30.0. Anote o resultado ao lado do esperado e diga, em uma frase, se o limite é "acima de" ou "a partir de". - 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
SELECTacontece. Confirme que abrir duas abas não duplica a consulta a cada quadro. - 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
READMEe no painel. - Escreva o
logde observabilidade com identificador do dispositivo, tempo desde oboot, versão dofirmware,statusda resposta e identificador de correlação. Escolha uma pergunta concreta sobre o seu projeto e mostre que a linha delogresponde ela. - 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.
- 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ério | Pontos |
|---|---|
| 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 nenhum | 2 pontos |
As nove respostas de rota anotadas, com pelo menos um 403 explicado | 3 pontos |
| Regra de alerta com o limite como número, e os quatro casos da fronteira testados | 2 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 recebe | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
| 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 processo | o 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 protecao | o 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 painel | rota 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 divergem | o 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 comum | a 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 animacao | o 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ória | a 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 oral | o 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 cliente | o 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 testdo projeto, oMySQLem copia e ogit 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 faz | O que o teste faz |
|---|---|
| roda a regra com um valor | roda a regra com todos os valores que importam |
| mostra o resultado | compara com o esperado e diz que falhou |
| para no primeiro erro | mostra 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:
- dado que existia continua igual
- coluna nova tem valor para linha nova, sem adivinhacao
- 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.
| Porta | Quem manda | Defeito tipico do projeto |
|---|---|---|
| sensor | o dispositivo, com defeito | valor fora de faixa virando alerta falso |
| formulario | a pessoa na tela | campo sem escapar entrando no HTML |
| resposta de servidor | a outra maquina | corpo grande demais ou formato inesperado |
| cabecalho | a rede intermediária | token 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:
- roda o teste e ve se passa
- acha a regra de negocio e ve se o número esta no código
- acha onde o log e escrito e ve se tem segredo
- 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.
- 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.
- 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?
- 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.
- 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.
- 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.
- 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.
- 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ério | Pontos |
|---|---|
| Tabela com seis casos, incluindo o limite e o de erro, com resultado ao lado do esperado | 3 pontos |
| Item 2 respondido: o que o teste sem aquele caso deixa passar | 2 pontos |
| Teste negativo escrito, com a forma da recusa testada | 2 pontos |
| Migracao em duas etapas, com as duas compatibilidades conferidas | 3 pontos |
| Três comentarios de revisao escritos, com elogio especifico, duvida com linha e problema com arquivo | 2 pontos |
| Item 6 respondido: um defeito de seguranca nomeado, com arquivo e linha | 1 ponto |
Item 4 conferido também com o deploy do projeto, do build até o health check | 1 ponto |
| Autoavaliacao em três frases, cada uma com o motivo | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
| Teste com um caso so | expect(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 feliz | a 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 falhar | o 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 cheia | a 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 README | ninguem 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 desenvolvimento | a 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 obvio | o 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ódigo | projeto 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 aula | porta 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.
