Estruturar o código — Arduino e IoT — semana 10 do 2o trimestre
Semana 10 de 16· 2o trimestre · 01/05 a 04/09
Estruturar o código
O arquivo unico que funcionava na aula 5 não cabe hoje.
Aula 1 — Módulos: separar o que cresceu
Objetivos
- Apontar, no seu projeto, as perguntas que hoje estão no mesmo arquivo, e dizer qual delas vira módulo.
- Separar um arquivo em módulos por separar por responsabilidade, nomeando a pergunta que cada módulo passa a responder.
- Medir o acoplamento entre módulos contando quem cita quem, e usar o número para dizer se a quebra aconteceu.
- Desenhar a estrutura de pastas do projeto e dizer o que entra em cada uma, sem exceções.
- Trocar o sensor no módulo do sensor e mostrar que nenhum outro arquivo mudou.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1
DHT11por dupla, no circuito do dia 8 do 1o trimestre, com resistor de pull-up - 1 computador com Node.js 20,
mysql2instalado e MySQL ou MariaDB 10.11 - Projeto do aluno aberto, com o arquivo único do dia 5 aula 2
- Folha de papel por dupla, para desenhar a estrutura de pastas
- Projetor, para o terminal do Node e o monitor serial da placa juntos
Conceitos
Separar por responsabilidade, e não por tamanho
O critério de quebra não é o tamanho do arquivo. A função grande é a que responde a várias perguntas, e ela precisa virar arquivo separado mesmo que tenha vinte linhas. Um arquivo de trinta linhas com quatro perguntas é pior que um arquivo de duzentas linhas com uma. O critério é a quantidade de perguntas que ele responde.
O termo para o arquivo separado é o módulo, e o termo em inglês é o mesmo: module é o arquivo, e module.exports é o que ele entrega. Em C++ não existe module, e a divisão equivalente é a classe, que é a outra forma de encapsular dado e comportamento num escopo próprio. O projeto do dia 12 vai precisar de uma: o objeto de configuração é a config como classe, com o valor dentro e o acesso por método. Hoje ela é um objeto literal, e a diferença é o dia 11 aula 1 que mostra.
O professor pede que a turma liste as perguntas do arquivo do dia 5, e são cinco:
- qual método e qual caminho foi pedido?
- quem está falando, e a chave bate?
- o dado recebido faz sentido?
- como eu gravo isso no banco?
- como eu abro o socket e fico esperando?
Cinco perguntas em um arquivo é um arquivo que ninguém entende por completo, porque para entender qualquer linha é preciso saber o comportamento das outras quatro. Isso é acoplamento: a dificuldade de mudar uma parte cresce com o número de partes que a tocam.
Separar por responsabilidade é responder cada pergunta em um módulo, e o nome do módulo é a pergunta. sensor responde "qual a temperatura agora". rota responde "como o dado viaja". banco responde "onde o dado fica". Quando o nome do arquivo não é uma pergunta, o módulo ainda não está desenhado.
Um módulo, uma pergunta, e o teste de 30 segundos
O teste que o professor aplica em cada módulo que a turma propõe é de 30 segundos e não usa nenhuma ferramenta:
Trocar o que esse módulo faz de fora muda algum outro arquivo?
Se a resposta é sim, a pergunta não estava isolada. Três exemplos que ele põe na lousa:
| Mudança | Arquivos que devem mudar | Se mudou mais, o módulo está furado |
|---|---|---|
trocar o sensor DHT11 por um BME280 | só o do sensor | rota e validação |
| mudar a faixa de temperatura aceitável | só o de configuração | tudo |
trocar o MySQL por outro banco | só o do banco | rota, validação e painel |
O terceiro caso é o que o professor mais usa, e ele responde à pergunta que a turma sempre faz: "mas a gente vai usar MySQL o trimestre inteiro". A resposta é que talvez não, e mesmo que sim, o dia que alguém pedir o sistema em outro banco vale uma tarde, não duas semanas. Separar por camadas é pagar essa tarde de uma vez.
Módulos em C++ e em Node são a mesma ideia
A placa e o servidor precisam da mesma quebra, e o aluno espera que sejam coisas diferentes. Não são.
| Na placa | No Node | |
|---|---|---|
| o arquivo é | um .ino | um .js |
| a divisão é | // ---- modulo 1 ---- dentro do arquivo | um arquivo por módulo |
| a ligação é | o IDE concatena antes de compilar | require ou import |
| o contrato é | o nome da função e o tipo do retorno | o nome do module.exports e a assinatura |
| quem carrega | o compilador | o node |
Em C++ a divisão é uma convenção do professor, porque o sketch é um arquivo só. Os blocos comentados no sketch da resolução são módulos de verdade no sentido de que cada um só responde uma pergunta, e o IDE os concatena na mesma ordem. No dia 14 aula 1, quando aparecer a biblioteca do BME280, o módulo do sensor ganha uma dependência nova e nenhum outro bloco muda.
require e import são as duas formas de fazer isso em JavaScript, e a aula usa require porque o projeto do dia 5 já usa. export é a forma de entregar o que o módulo tem, e um módulo que só tem export e nenhum import é um módulo que ninguém usa.
O objeto de configuração é o módulo mais subestimado
A config é o módulo que parece inútil e é o que mais impede erro. Ela tem só limites e nomes, sem nenhuma lógica:
- os limites de faixa do dia 8 aula 1,
- o
id_dispositivo, - e, no dia 11, os valores que vêm do ambiente.
O motivo de ela existir separado é que o limite de temperatura muda por ambiente e não por dia de aula. A bancada fica em trinta e dois graus; um pátio ao sol passa de quarenta. Quando a regra mora dentro da validação, mudar o limite é caçar a linha certa em um arquivo de duzentas linhas. Quando ela mora na config, é uma linha em um arquivo de seis, e o dia 11 aula 1 vai trocar essa linha sem tocar em mais nada.
O critério de coesão é o que separa config boa de config ruim: um módulo coeso é o que serve a uma só pergunta, mesmo que tenha um único item. Uma config com vinte campos sem relação entre eles não é coesa, e é o que o professor chama de "lixeira de constantes".
Dependência aponta para baixo, e nunca volta
A regra de dependência da estrutura de pastas é uma seta, e ela só aponta para um lado:
servidor -> rotas -> banco -> config
O servidor conhece as rotas. As rotas conhecem o banco. O banco conhece a config. E ninguém conhece o servidor. Quando a seta volta, a camada furou: a config que importa a rota é o sinal de que alguma decisão subiu de camada sem ter para onde voltar.
A injeção é o nome do que a estrutura permite. O módulo do banco recebe a conexão como parâmetro em vez de criar a própria, e aí o dia 12 consegue rodar o teste com um banco falso. O módulo de regra recebe a faixa como parâmetro em vez de ler a config, e aí o mesmo teste roda com outra faixa. Sem injeção, o teste precisa de banco de verdade, e o dia 12 aula 2 existe por causa disso.
A segunda metade da frase é o acoplamento, que é a dificuldade de mudar um módulo em função dos outros: quanto menos o módulo precisa dos demais para mudar, menos acoplado ele está. E o termo em inglês para a disciplina inteira vem do SOLID, do qual a aula usa hoje só o primeiro princípio, o princípio da responsabilidade única: um módulo, uma razão para mudar. O nome completo do princípio aparece no dia 10 aula 2, quando as três camadas aparecerem nomeadas, e o professor não escreve as cinco letras na lousa porque nenhuma delas serve para esta decisão.
E há uma segunda leitura de coesão que a turma erra sempre: coesão alta não é muitas coisas dentro do módulo. É o contrário. Módulo coeso é o que serve a uma pergunta, mesmo tendo um item só. A config da aula tem cinco campos e é o módulo mais coeso do projeto, porque os cinco respondem à mesma pergunta.
Atividade
Montagem:
DHT11noGPIO4, com resistor de pull-up de 4,7 k ohm entre o pino de dados e3V3.- Servidor Node do dia 5 aula 2 rodando, com a tabela
tb_t2_leitura.
- Abra o arquivo do seu projeto e escreva no caderno a lista de perguntas que ele responde. Quantas perguntas são? Marque as duas que você escolheria para o primeiro módulo.
- Rode o script da resolução e desenhe no caderno a matriz de acoplamento que ele imprime, com a ordem dos módulos anotada. Quantos umas existem?
- Meça o acoplamento do seu arquivo antes de quebrar: conte quantas linhas citam
pool.execute, quantas citamres.writeHeade quantas citamreadTemperature. Escreva os três números no caderno. - Quebre o seu arquivo em módulos: um para a
config, um para a regra, um para o banco, um para a rota. Um arquivo por camada, e cada um respondendo uma pergunta só. - Meça de novo, depois da quebra, e escreva os mesmos três números ao lado dos do item 3. Qual deles mudou, e por quê?
- Desenhe no caderno a estrutura de pastas do seu projeto: onde ficam as rotas, o banco e a
config. Marque com uma seta o sentido da dependência entre elas, e diga se alguma seta está voltando. - Troque o sensor no módulo do sensor do seu projeto e grave. Quantos arquivos você mudou? Se a resposta for mais de um, aponte o módulo que estava com duas perguntas.
- Escreva, em uma frase, o que muda na sua cabeça quando o módulo do banco for substituído por outro banco. Cite um arquivo do seu projeto que não teria que mudar.
Nota: 12 pontos. Critério de fim: os três números de acoplamento medidos antes e depois da quebra, com a diferença explicada, e a estrutura de pastas desenhada com a seta de dependência.
Resolucao
Do lado do Node, o script mede o acoplamento e troca o sensor sem tocar em mais nada. Este foi executado nesta máquina com Node.js 24:
// dia 10 aula 1, lado do Node: modulos e o teste do "e a mesma coisa?".
const path = require('path');
// ---------------------------------------------------------------------------
// MODULO config: so os limites e os nomes. Nao sabe o que e sensor, nem rota.
// Mudar "qual temperatura e erro" acontece aqui e em lugar nenhum mais.
// ---------------------------------------------------------------------------
const config = {
idDispositivo: 'estacao-01',
minTempC: -40,
maxTempC: 80,
minUmidadePct: 0,
maxUmidadePct: 100,
};
// ---------------------------------------------------------------------------
// MODULO sensor: so falar com o DHT11. Nao conhece rede, nem JSON, nem faixa.
// A funcao devolve NaN quando o sensor nao responde, e quem chama e que
// decide se isso e erro.
// ---------------------------------------------------------------------------
// As tres bancadas de teste: DHT11, BME280 (o dia 14 aula 1) e nada. O
// ponto e que o NUMERO muda e a ASSINATURA nao: mesma funcao, mesma
// pergunta, e a rota nem percebe quel sensor esta em uso.
const BANCADAS = {
'dht11': { t: 27.4, u: 61 },
'bme280': { t: 25.87, u: 58.2 },
'sem-sensor': { t: NaN, u: NaN },
};
function sensor_temperaturaC(bancada) {
return BANCADAS[bancada].t;
}
function sensor_umidadePct(bancada) {
return BANCADAS[bancada].u;
}
// ---------------------------------------------------------------------------
// MODULO rota: so montar o JSON. Nao sabe o tipo do sensor, nao sabe a
// senha do WiFi, nao sabe a faixa. So pede a leitura e usa a constante.
// ---------------------------------------------------------------------------
function rota_montarPayload(temperatura_c, umidade_pct) {
const id = config.idDispositivo;
const t = Number.isFinite(temperatura_c) ? temperatura_c : 'null';
const u = Number.isFinite(umidade_pct) ? umidade_pct : 'null';
return {"id_dispositivo":"${id}","temperatura_c":${t},"umidade_pct":${u}};
}
// ---------------------------------------------------------------------------
// O TESTE DO PROFESSOR: cada modulo responde a UMA pergunta?
// A funcao abaixo mede isso sem rodar nada: ela conta quantas perguntas de
// cada modulo aparecem no corpo do outro. Acoplamento alto e o sinal de que
// a quebra nao aconteceu, so mudou de lugar.
// ---------------------------------------------------------------------------
function contarReferencias(corpo, nomes) {
return corpo.split('\n').filter((l) => nomes.some((n) => l.includes(n))).length;
}
const PERGUNTAS = {
config: ['minTempC', 'maxTempC', 'minUmidadePct', 'maxUmidadePct', 'idDispositivo'],
sensor: ['readTemperature', 'readHumidity', 'DHT', 'analogRead'],
rota: ['montarPayload', 'JSON.stringify', 'id_dispositivo', 'temperatura_c'],
banco: ['INSERT', 'SELECT', 'pool.execute', 'tb_t2'],
http: ['createServer', 'res.writeHead', 'req.url', 'listen'],
};
const CORPOS = {
config: const config = {
idDispositivo: 'estacao-01',
minTempC: ${config.minTempC},
maxTempC: ${config.maxTempC},
minUmidadePct: ${config.minUmidadePct},
maxUmidadePct: ${config.maxUmidadePct},
};,
sensor: function sensor_temperaturaC() { return dht.readTemperature(); }
function sensor_umidadePct() { return dht.readHumidity(); },
rota: function rota_montarPayload(temperatura_c, umidade_pct) {
return JSON.stringify({ id_dispositivo: config.idDispositivo, temperatura_c, umidade_pct });
},
banco: await pool.execute('INSERT INTO tb_t2_leitura (...) VALUES (?, ?, ?)', [a, b, c]);,
http: const server = createServer((req, res) => { res.writeHead(200); res.end('{}'); });
server.listen(3000);,
};
console.log('--- a quebra: cada modulo responde a uma pergunta ---');
for (const nome of Object.keys(PERGUNTAS)) {
console.log( ${nome.padEnd(7)} -> ${PERGUNTAS[nome].join(', ')});
}
console.log('');
console.log(' config QUAL temperatura e erro?');
console.log(' sensor QUAL a temperatura agora?');
console.log(' rota COMO o dado viaja?');
console.log(' banco ONDE o dado fica?');
console.log(' http COMO o pedido entra?');
console.log('');
console.log('--- acoplamento medido: quem cita quem ---');
const nomes = Object.keys(PERGUNTAS);
console.log(' modulo que cita:');
for (const a of nomes) {
const linha = nomes.map((b) => {
if (a === b) return ' -- ';
return String(contarReferencias(CORPOS[a], PERGUNTAS[b])).padStart(5);
});
console.log( ${a.padEnd(7)} ${linha.join('')} (ordem: ${nomes.join(', ')}));
}
console.log('');
console.log('Cada modulo deve citar SO o config. Quem cita sensor E banco esta');
console.log('fazendo duas perguntas no mesmo arquivo, e nenhuma delas e testavel.');
console.log('');
console.log('--- o teste pratico: trocar o sensor muda uma linha? ---');
function comSensor(bancada) {
return {
t: sensor_temperaturaC(bancada),
u: sensor_umidadePct(bancada),
};
}
const comDht = comSensor('dht11');
const comBme = comSensor('bme280');
console.log( bancada com DHT11 : t=${comDht.t} u=${comDht.u});
console.log( bancada com BME280 : t=${comBme.t} u=${comBme.u} <- so o modulo sensor mudou);
console.log( payload da primeira : ${rota_montarPayload(comDht.t, comDht.u)});
console.log( payload da segunda : ${rota_montarPayload(comBme.t, comBme.u)});
console.log('');
console.log('Os numeros mudaram em dois digitos e a rota deu a mesma resposta.');
console.log('Se a rota tivesse o tipo do sensor dentro dela, o BME280 exigiria');
console.log('quatro casas decimais e a validacao do dia 8 teria outra faixa.');
console.log('Nada disso existe na rota: ela so formata o que recebeu.');
console.log('');
console.log('--- NaN: o modulo do sensor devolvendo, e a decisao de quem chama ---');
const sem = comSensor('sem-sensor');
console.log( sensor devolveu: t=${sem.t} u=${sem.u});
console.log( JSON.stringify({t: NaN}) = ${JSON.stringify({ t: NaN })});
console.log( rota_montarPayload(NaN) = ${rota_montarPayload(sem.t, sem.u)});
console.log('');
console.log('isnan e o comportamento CORRETO do modulo de sensor: ele nao decide');
console.log('que NaN e erro, so avisa. Quem decide e o modulo de validacao. Se o');
console.log('sensor devolvesse 0, o painel mostraria zero graus e ninguem saberia');
console.log('que a placa estava sem sensor — e o bug silencioso do dia 8.');
console.log('');
console.log('--- a estrutura de pastas do projeto ---');
for (const p of ['src', 'src/rotas', 'src/banco', 'src/config', 'src/servidor.js',
'test', 'test/rota.test.js', 'package.json', '.env', '.gitignore']) {
console.log( ${p});
}
console.log('');
console.log(' src/config -> um objeto so, sem logica. E o que o dia 11 troca');
console.log(' src/rotas -> so HTTP: metodo, caminho, status');
console.log(' src/banco -> so SQL: comando e parametro');
console.log(' src/servidor.js -> o require() de tudo, e nada mais');
console.log('');
console.log(' Dependencia de cima para baixo: servidor -> rotas -> banco -> config.');
console.log(' Volta nunca. Quando a rota importa o banco E o sensor, o desenho');
console.log(' parou de ser camadas e virou um arquivo com separacao de virgula.');
console.log( (cwd desta execucao: ${path.basename(process.cwd())}));
A saída real, medida nesta máquina:
--- a quebra: cada modulo responde a uma pergunta --- config -> minTempC, maxTempC, minUmidadePct, maxUmidadePct, idDispositivo sensor -> readTemperature, readHumidity, DHT, analogRead rota -> montarPayload, JSON.stringify, id_dispositivo, temperatura_c banco -> INSERT, SELECT, pool.execute, tb_t2 http -> createServer, res.writeHead, req.url, listen config QUAL temperatura e erro? sensor QUAL a temperatura agora? rota COMO o dado viaja? banco ONDE o dado fica? http COMO o pedido entra? --- acoplamento medido: quem cita quem --- modulo que cita: config -- 0 0 0 0 (ordem: config, sensor, rota, banco, http) sensor 0 -- 0 0 0 (ordem: config, sensor, rota, banco, http) rota 1 0 -- 0 0 (ordem: config, sensor, rota, banco, http) banco 0 0 0 -- 0 (ordem: config, sensor, rota, banco, http) http 0 0 0 0 -- (ordem: config, sensor, rota, banco, http) Cada modulo deve citar SO o config. Quem cita sensor E banco esta fazendo duas perguntas no mesmo arquivo, e nenhuma delas e testavel. --- o teste pratico: trocar o sensor muda uma linha? --- bancada com DHT11 : t=27.4 u=61 bancada com BME280 : t=25.87 u=58.2 <- so o modulo sensor mudou payload da primeira : {"id_dispositivo":"estacao-01","temperatura_c":27.4,"umidade_pct":61} payload da segunda : {"id_dispositivo":"estacao-01","temperatura_c":25.87,"umidade_pct":58.2} Os numeros mudaram em dois digitos e a rota deu a mesma resposta. Se a rota tivesse o tipo do sensor dentro dela, o BME280 exigiria quatro casas decimais e a validacao do dia 8 teria outra faixa. Nada disso existe na rota: ela so formata o que recebeu. --- NaN: o modulo do sensor devolvendo, e a decisao de quem chama --- sensor devolveu: t=NaN u=NaN JSON.stringify({t: NaN}) = {"t":null} rota_montarPayload(NaN) = {"id_dispositivo":"estacao-01","temperatura_c":null,"umidade_pct":null} isnan e o comportamento CORRETO do modulo de sensor: ele nao decide que NaN e erro, so avisa. Quem decide e o modulo de validacao. Se o sensor devolvesse 0, o painel mostraria zero graus e ninguem saberia que a placa estava sem sensor — e o bug silencioso do dia 8. --- a estrutura de pastas do projeto --- src src/rotas src/banco src/config src/servidor.js test test/rota.test.js package.json .env .gitignore src/config -> um objeto so, sem logica. E o que o dia 11 troca src/rotas -> so HTTP: metodo, caminho, status src/banco -> so SQL: comando e parametro src/servidor.js -> o require() de tudo, e nada mais Dependencia de cima para baixo: servidor -> rotas -> banco -> config. Volta nunca. Quando a rota importa o banco E o sensor, o desenho parou de ser camadas e virou um arquivo com separacao de virgula. (cwd desta execucao: aula)
O professor aponta quatro coisas nessa saída:
- a matriz com um e quatro zeros: a única referência que sobra é a rota que cita a
config, e ela é legítima. Todo o resto é zero, e é esse zero que o professor manda a turma mirar no próprio projeto. O número do rodapé é a ordem de leitura da matriz, e ele existe porque uma matriz de cinco por cinco impressa sem legenda é um desenho que ninguém sabe ler. temperatura_c":25.87: oBME280entra com duas casas decimais a mais que oDHT11, e a rota não muda. Se a rota tivesse o tipo do sensor dentro dela, ela teria de decidir quantas casas formatar, e essa decisão seria do sensor, não dela.JSON.stringify({t: NaN}) = {"t":null}: o JavaScript transformaNaNemnullsozinho, sem erro e sem aviso. O módulo da rota precisa checarNumber.isFiniteantes de formatar, porque se deixar oJSON.stringifyresolver, onullvai para o Node e o dia 8 aula 1 devolve400de "campo vazio" em vez do422de "sensor sem resposta". São dois status para o mesmo defeito físico, e o log perde a informação.(cwd desta execucao: aula): uma linha inútil no produto e útil na aula. O professor conta que ela existe para o aluno ver que o código roda em uma pasta de verdade, e que o nome dessa pasta é o mesmo que opackage.jsondo dia 11 vai descrever.
O sketch da placa, que é a mesma quebra do outro lado do cabo:
// dia 10, aula 1: a placa tambem precisa de modulos. // // O problema e o mesmo dos dois lados do cabo. No dia 5 o arquivo do // servidor cresceu ate fazer tudo: conectar no banco, montar a resposta, // checar o dado, gravar. Hoje a placa tem o mesmo problema com o payload. // // Este sketch mostra a quebra em tres "arquivos" declarados abaixo, cada um // com UMA responsabilidade. Em C++ a divisao de arquivos e uma Includes, // e o IDE concatena antes de compilar. No Node (dia 10 aula 1, lado // servidor) a mesma ideia vira `require('./modulo')`. #include <Arduino.h> #include <DHT.h> #include <WiFi.h> // --------------------------------------------------------------------------- // MODULO 1 — sensor.h: so falar com o DHT11. Nao conhece rede, nem JSON, // nem valida. Se o sensor trocar, so este modulo muda. // --------------------------------------------------------------------------- static const int MOD_SENSOR_PIN = 4; static const uint8_t MOD_SENSOR_TIPO = DHT11; // --------------------------------------------------------------------------- // MODULO 2 — config.h: so os limites e as constantes de negocio. // Mudar "qual temperatura e erro" acontece aqui e em lugar nenhum mais. // --------------------------------------------------------------------------- static const float CONF_MIN_TEMP_C = -40.0f; static const float CONF_MAX_TEMP_C = 80.0f; static const char* CONF_ID_DISPOSITIVO = "estacao-01"; // --------------------------------------------------------------------------- // MODULO 3 — wifi.h: so a rede. Nao sabe o que e temperatura. // // A regra que o aluno precisa levar: cada modulo responde a UMA pergunta. // "Qual a temperatura?" e uma pergunta. "A rede responde?" e outra. // Quando as duas estao na mesma funcao, nenhuma das duas esta testavel. // --------------------------------------------------------------------------- // ---- Implementacao do modulo 1 -------------------------------------------- static DHT g_sensor(MOD_SENSOR_PIN, MOD_SENSOR_TIPO); void sensor_iniciar() { g_sensor.begin(); } // Devolve NaN quando o sensor nao responde. Quem chama e que decide se isso // e erro: e o modulo de validacao, nao o modulo do sensor. float sensor_temperaturaC() { return g_sensor.readTemperature(); } float sensor_umidadePct() { return g_sensor.readHumidity(); } // ---- Implementacao do modulo 3 -------------------------------------------- void wifi_ligar() { Serial.print(" [wifi] iniciando, ssid="); Serial.println("rede-da-escola"); // nome de rede, pode ser mostrado Serial.println(" [wifi] senha nao e impressa em lugar nenhum"); } bool wifi_associado() { return false; // sem ponto de acesso nesta aula } // ---- O modulo que usa os outros: a rota ------------------------------------- // E o unico lugar que sabe montar o JSON. Ele NAO sabe o tipo do sensor, e // NAO sabe a senha do WiFi: so pede a leitura e usa a constante. String rota_montarPayload(float temperatura_c, float umidade_pct) { String json = "{\"id_dispositivo\":\""; json += CONF_ID_DISPOSITIVO; json += "\",\"temperatura_c\":"; if (isnan(temperatura_c)) { json += "null"; } else { json += temperatura_c; } json += ",\"umidade_pct\":"; if (isnan(umidade_pct)) { json += "null"; } else { json += umidade_pct; } json += "}"; return json; } void setup() { Serial.begin(115200); Serial.println(); Serial.println("dia 10 aula 1 — modulos: separar o que cresceu"); Serial.println("=============================================="); Serial.println(); Serial.println("O ARQUIVO CRESCEU. O QUE FICOU DENTRO DELE:"); Serial.println(" 1. ler o sensor"); Serial.println(" 2. decidir se o dado presta"); Serial.println(" 3. montar o JSON"); Serial.println(" 4. conectar na rede"); Serial.println(" 5. enviar"); Serial.println(" Quatro perguntas em um arquivo: nenhuma delas e testavel."); Serial.println(); Serial.println("A QUEBRA, E O QUE CADA MODULO RESPONDE"); Serial.println("------------------------------------"); Serial.println(" config.h -> QUAL temperatura e erro?"); Serial.println(" sensor -> QUAL a temperatura agora?"); Serial.println(" wifi -> A rede respondeu?"); Serial.println(" rota -> COMO o dado viaja?"); Serial.println(); Serial.println(" Em C++: um #include por modulo, e o IDE concatena."); Serial.println(" Em Node: um require('./modulo') por modulo, e o Node carrega."); Serial.println(" Mesma ideia: o nome do arquivo e o contrato."); Serial.println(); Serial.println("PROVANDO QUE A SEPARACAO FUNCIONA"); Serial.println("----------------------------------"); sensor_iniciar(); wifi_ligar(); float t = sensor_temperaturaC(); float u = sensor_umidadePct(); Serial.print(" [sensor] devolveu "); Serial.print(t, 1); Serial.print(" C / "); Serial.print(u, 0); Serial.println(" %"); if (isnan(t) || isnan(u)) { Serial.println(" [sensor] NaN: o DHT11 nao respondeu nesta bancada."); Serial.println(" [sensor] devolvendo NaN e o comportamento CORRETO: quem"); Serial.println(" decide que NaN e erro e o modulo de validacao."); } // Trocar o sensor e mexer em UMA linha. Este e o teste pratico da quebra: // o professor troca MOD_SENSOR_PIN de 4 para 2 e o resto nao muda. Serial.println(); Serial.print(" [rota] payload com o valor real: "); Serial.println(rota_montarPayload(t, u)); Serial.println(" [rota] payload com NaN (o sensor falhou): "); Serial.println(rota_montarPayload(isnan(t) ? NAN : 0.0f, isnan(u) ? NAN : 0.0f)); Serial.println(); Serial.println(" Trocar o sensor: uma linha em sensor. Mudar o limite de erro:"); Serial.println(" uma linha em config. Trocar o WiFi: so wifi. A rota nunca muda."); WiFi.mode(WIFI_OFF); Serial.println(); Serial.print(" [wifi] associado? "); Serial.println(wifi_associado() ? "sim" : "nao (esperado nesta aula)"); } void loop() { Serial.println(); Serial.println("--- nova rodada ---"); setup(); delay(10000); }
Por que assim e não de outro jeito. Os módulos da placa são funções static, e o static não é decoração: em C++, uma função com static em escopo de arquivo só existe naquele arquivo de verdade. É o mais próximo que o C++ tem de "privado", e ele garante que o módulo sensor não vaza para fora do sketch. O professor explica que no projeto real, com vários arquivos, o equivalente é o namespace, e que hoje a divisão é por convenção e não por compilador.
O sensor_temperaturaC devolve NaN e não zero, e o comentário do arquivo é explícito sobre isso. Devolver zero seria a decisão errada tomada dentro do módulo errado: o módulo do sensor não sabe se NaN é erro ou se o chamador vai tratar como tal. Zero é um número plausível e entra no banco; NaN é recusado pela validação. A placa e o Node concordam nisso porque os dois checam isnan no mesmo lugar da regra, e o dia 12 vai escrever o teste que garante que essa concordância não se perca.
A rota_montarPayload tem o isnan duas vezes, uma para cada campo, e o if inline em vez de um laço. A versão com laço seria mais curta e o professor rejeita: o laço precisaria de um vetor de campos com nome, e o nome do campo é texto que o JSON precisa, não dado. A versão repetida é mais longa e tem zero chance de errar o nome de um campo, porque o nome aparece escrito dentro do if que o trata.
O bloco da estrutura de módulos aparece antes do código, e não depois. Isso é escolha do autor do material e o professor assume na aula: o aluno lê a lista de perguntas antes de ver a implementação, e chegam ao código já sabendo o que cada parte faz. A alternativa, código primeiro e mapa depois, faz o aluno ler trezentas linhas antes de ter qualquer hipótese sobre o que está lendo.
Criterios de correcao
| Critério | Pontos |
|---|---|
| Lista de perguntas do arquivo do projeto escrita, com as duas escolhidas para o primeiro módulo | 2 pontos |
DHT11 no GPIO4 com pull-up, e pinos e tipos em const no topo | 1 pontos |
| Matriz de acoplamento da resolução desenhada no caderno, com a contagem de uns | 1 pontos |
| Os três números de acoplamento medidos no projeto antes da quebra | 2 pontos |
| Projeto quebrado em módulos, um por camada, cada um respondendo uma pergunta | 2 pontos |
| Os mesmos três números medidos depois, com a diferença explicada | 2 pontos |
| Estrutura de pastas desenhada, com a seta de dependência e nenhuma seta voltando | 1 pontos |
| Item 8: a frase sobre trocar o banco, com um arquivo do projeto que não mudaria | 1 pontos |
Erros comuns
| Erro | Como aparece | Correção |
|---|---|---|
| Quebrar por tamanho de arquivo | "O arquivo passou de duzentas linhas, então separei" | "Tamanho não é critério. Quantas perguntas o arquivo responde? Um arquivo de trinta linhas com quatro perguntas é pior que um de duzentas com uma." |
| Separar por pastas, não por responsabilidade | "Criei pastas sensor, rotas e banco" | "Pastas organizam o disco, e organizar pasta é a tarefa de quem monta a estrutura, não de quem separa responsabilidade. Pastas não separam responsabilidade. A pasta banco com uma função que valida faixa tem duas perguntas no mesmo lugar de sempre." |
| Módulo que chama todo mundo | "O módulo app importa os outros cinco" | "Um módulo que importa todos é o servidor, e ele não deveria fazer mais que require e listen. O que prova é o sentido da seta de dependência." |
| Dependência circular | "O banco importa a rota e a rota importa o banco" | "A seta voltou, e a camada furou. O banco não sabe que existe rota. Se um require dá erro de arquivo não encontrado, quase sempre é ciclo." |
| Colocar a faixa dentro do módulo do sensor | "Validei a temperatura no módulo do sensor" | "O sensor responde "qual a temperatura agora", não "a temperatura presta". Quem julga é a camada de regra, e é ela que o dia 12 vai testar sem sensor nenhum." |
Devolver 0 quando o sensor falha | "Quando o DHT11 não responde ele devolve 0 e eu gravo" | "Zero é um número plausível e entra no banco como temperatura da sala. Devolva NaN e deixe a validação recusar: NaN é o estado de "não sei", zero é o estado de "sei que são zero"." |
Deixar o JSON.stringify decidir o null | "Passei o NaN direto e o stringify resolveu" | "Ele resolve, e por isso é perigoso: vira null, e o dia 8 aula 1 devolve 400 de campo vazio em vez de 422 de sensor sem resposta. Cheque isnan antes de formatar." |
config virando lixeira de constantes | "Joguei todos os números que aparecem no projeto num config.js" | "Coesão é a pergunta do módulo. A config guarda o que muda por ambiente e o que é regra de negócio nomeada. Constant que pertence a um módulo fica no módulo." |
| Escolher o nome do módulo pelo assunto, não pela pergunta | "Criei temperatura.js e umidade.js" | "Pelo assunto, um DHT11 novo gera um módulo novo. Pela pergunta, DHT11, BME280 e qualquer outro sensor continuam no mesmo módulo sensor." |
| Módulo novo sem teste | "Criei o módulo mas não sei se roda" | "Um módulo que ninguém consegue exercitar sem ligar a placa é um módulo mal quebrado. O teste do dia 12 roda a camada de regra em quarenta milissegundos justamente porque ela não depende de nada." |
| Mover código para a pasta e achar que acabou | "Separei em pastas e a validação continua espalhada" | "A pasta não move a responsabilidade. Abra os arquivos e conte as perguntas de cada um. Se o número não caiu, a divisão é de disco, não de código." |
Esquecer de atualizar o require | "Renomei o arquivo e quebrou tudo" | "O require é o contrato: o nome do arquivo é parte da interface. Ou o package.json impede a quebra, ou o teste do dia 12 pega, e as duas coisas chegam antes do fim do trimestre." |
Desafio extra
Escreva o módulo de log do dia 8 aula 2 como módulo de verdade, com uma função só, e escreva em seguida o segundo módulo de log que ele substitui: o segundo formata a mensagem num arquivo e o primeiro formata no terminal. Rode os dois lado a lado com a mesma entrada e compare as duas linhas byte a byte. Se as duas não coincidirem, o formato do log não é um contrato ainda, e é o dia 11 aula 1 que vai transformar a lista de campos em variável de ambiente para travar esse contrato.
>A resolucao, compilada
// dia 10, aula 1: a placa tambem precisa de modulos. // // O problema e o mesmo dos dois lados do cabo. No dia 5 o arquivo do // servidor cresceu ate fazer tudo: conectar no banco, montar a resposta, // checar o dado, gravar. Hoje a placa tem o mesmo problema com o payload. // // Este sketch mostra a quebra em tres "arquivos" declarados abaixo, cada um // com UMA responsabilidade. Em C++ a divisao de arquivos e uma Includes, // e o IDE concatena antes de compilar. No Node (dia 10 aula 1, lado // servidor) a mesma ideia vira `require('./modulo')`. #include <Arduino.h> #include <DHT.h> #include <WiFi.h> // --------------------------------------------------------------------------- // MODULO 1 — sensor.h: so falar com o DHT11. Nao conhece rede, nem JSON, // nem valida. Se o sensor trocar, so este modulo muda. // --------------------------------------------------------------------------- static const int MOD_SENSOR_PIN = 4; static const uint8_t MOD_SENSOR_TIPO = DHT11; // --------------------------------------------------------------------------- // MODULO 2 — config.h: so os limites e as constantes de negocio. // Mudar "qual temperatura e erro" acontece aqui e em lugar nenhum mais. // --------------------------------------------------------------------------- static const float CONF_MIN_TEMP_C = -40.0f; static const float CONF_MAX_TEMP_C = 80.0f; static const char* CONF_ID_DISPOSITIVO = "estacao-01"; // --------------------------------------------------------------------------- // MODULO 3 — wifi.h: so a rede. Nao sabe o que e temperatura. // // A regra que o aluno precisa levar: cada modulo responde a UMA pergunta. // "Qual a temperatura?" e uma pergunta. "A rede responde?" e outra. // Quando as duas estao na mesma funcao, nenhuma das duas esta testavel. // --------------------------------------------------------------------------- // ---- Implementacao do modulo 1 -------------------------------------------- static DHT g_sensor(MOD_SENSOR_PIN, MOD_SENSOR_TIPO); void sensor_iniciar() { g_sensor.begin(); } // Devolve NaN quando o sensor nao responde. Quem chama e que decide se isso // e erro: e o modulo de validacao, nao o modulo do sensor. float sensor_temperaturaC() { return g_sensor.readTemperature(); } float sensor_umidadePct() { return g_sensor.readHumidity(); } // ---- Implementacao do modulo 3 -------------------------------------------- void wifi_ligar() { Serial.print(" [wifi] iniciando, ssid="); Serial.println("rede-da-escola"); // nome de rede, pode ser mostrado Serial.println(" [wifi] senha nao e impressa em lugar nenhum"); } bool wifi_associado() { return false; // sem ponto de acesso nesta aula } // ---- O modulo que usa os outros: a rota ------------------------------------- // E o unico lugar que sabe montar o JSON. Ele NAO sabe o tipo do sensor, e // NAO sabe a senha do WiFi: so pede a leitura e usa a constante. String rota_montarPayload(float temperatura_c, float umidade_pct) { String json = "{\"id_dispositivo\":\""; json += CONF_ID_DISPOSITIVO; json += "\",\"temperatura_c\":"; if (isnan(temperatura_c)) { json += "null"; } else { json += temperatura_c; } json += ",\"umidade_pct\":"; if (isnan(umidade_pct)) { json += "null"; } else { json += umidade_pct; } json += "}"; return json; } void setup() { Serial.begin(115200); Serial.println(); Serial.println("dia 10 aula 1 — modulos: separar o que cresceu"); Serial.println("=============================================="); Serial.println(); Serial.println("O ARQUIVO CRESCEU. O QUE FICOU DENTRO DELE:"); Serial.println(" 1. ler o sensor"); Serial.println(" 2. decidir se o dado presta"); Serial.println(" 3. montar o JSON"); Serial.println(" 4. conectar na rede"); Serial.println(" 5. enviar"); Serial.println(" Quatro perguntas em um arquivo: nenhuma delas e testavel."); Serial.println(); Serial.println("A QUEBRA, E O QUE CADA MODULO RESPONDE"); Serial.println("------------------------------------"); Serial.println(" config.h -> QUAL temperatura e erro?"); Serial.println(" sensor -> QUAL a temperatura agora?"); Serial.println(" wifi -> A rede respondeu?"); Serial.println(" rota -> COMO o dado viaja?"); Serial.println(); Serial.println(" Em C++: um #include por modulo, e o IDE concatena."); Serial.println(" Em Node: um require('./modulo') por modulo, e o Node carrega."); Serial.println(" Mesma ideia: o nome do arquivo e o contrato."); Serial.println(); Serial.println("PROVANDO QUE A SEPARACAO FUNCIONA"); Serial.println("----------------------------------"); sensor_iniciar(); wifi_ligar(); float t = sensor_temperaturaC(); float u = sensor_umidadePct(); Serial.print(" [sensor] devolveu "); Serial.print(t, 1); Serial.print(" C / "); Serial.print(u, 0); Serial.println(" %"); if (isnan(t) || isnan(u)) { Serial.println(" [sensor] NaN: o DHT11 nao respondeu nesta bancada."); Serial.println(" [sensor] devolvendo NaN e o comportamento CORRETO: quem"); Serial.println(" decide que NaN e erro e o modulo de validacao."); } // Trocar o sensor e mexer em UMA linha. Este e o teste pratico da quebra: // o professor troca MOD_SENSOR_PIN de 4 para 2 e o resto nao muda. Serial.println(); Serial.print(" [rota] payload com o valor real: "); Serial.println(rota_montarPayload(t, u)); Serial.println(" [rota] payload com NaN (o sensor falhou): "); Serial.println(rota_montarPayload(isnan(t) ? NAN : 0.0f, isnan(u) ? NAN : 0.0f)); Serial.println(); Serial.println(" Trocar o sensor: uma linha em sensor. Mudar o limite de erro:"); Serial.println(" uma linha em config. Trocar o WiFi: so wifi. A rota nunca muda."); WiFi.mode(WIFI_OFF); Serial.println(); Serial.print(" [wifi] associado? "); Serial.println(wifi_associado() ? "sim" : "nao (esperado nesta aula)"); } void loop() { Serial.println(); Serial.println("--- nova rodada ---"); setup(); delay(10000); }
Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia10 aula1.
Aula 2 — Camadas: rota, regra e banco
Objetivos
- Nomear as três camadas do caminho do dado, com a pergunta que cada uma responde, e o que cada uma não pode saber.
- Traduzir o veredito da regra em status HTTP, e explicar por que o
422e o500nascem em camadas diferentes. - Mostrar que o alerta e a gravação são duas respostas para o mesmo dado, e o que acontece quando se junta as duas.
- Trocar a camada de banco por uma implementação de mentira, sem mudar a rota nem a regra, e medir o que isso prova.
- Rodar a camada de regra sozinha, sem servidor e sem banco, e cronometrar.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1
DHT11por dupla, no circuito do dia 8 do 1o trimestre, com resistor de pull-up - 1 computador com Node.js 20,
mysql2instalado e MySQL ou MariaDB 10.11 - Projeto do aluno já quebrado em módulos, do fim da aula 1
- Folha de papel por dupla, para o diagrama das três camadas e a seta de chamada
- Projetor, para o terminal do Node e o monitor serial da placa juntos
Conceitos
Três camadas, três perguntas, uma seta
A separação em camadas é o desenho que o dia 10 aula 1 pediu e o professor nomeia hoje. São três, e cada uma tem nome, pergunta e o que não pode saber:
| Camada | Nome em inglês | Pergunta | Não sabe |
|---|---|---|---|
| rota | controller | qual método, qual caminho, qual status? | o que é faixa, o que é SQL |
| regra | service | este dado faz sentido? e o que eu faço com ele? | o que é HTTP, o que é tabela |
| banco | repository | qual comando, qual coluna, qual parâmetro? | o que é "temperatura alta" |
A rota só roteia: ela traduz pedido em resposta e nada mais. Existe lógica na rota quando aparece um if que compara temperatura com um número, e é o teste mais simples que o professor pede para a turma aplicar no próprio arquivo: se esse if existe, a camada do meio está furada, porque aquela comparação é regra morando no lugar errado.
Os três nomes em inglês são os que aparecem no require do projeto: a rota é o controller, a camada do meio é o serviço, e a camada de baixo é o repositório. A regra de negócio é o que o serviço responde, e o repositório devolve "gravei" ou "não gravei".
A regra de negócio é a camada do meio, e é a única que o teste do dia 12 consegue rodar sem nada em volta. Ela não recebe req nem res, não recebe conexão de banco: recebe dois números e devolve um veredito. É por isso que ela roda em milissegundos, e a medição do fim da aula é a prova.
O banco só grava: ele sabe SQL e não sabe o que é "temperatura alta". A prova de que ele não sabe é a assinatura da função de gravação, que devolve "gravei" ou "não gravei" e nunca devolve "o dado era ruim". Essa resposta é da regra, e se o banco devolvesse isso, a regra teria perdido o trabalho.
A seta de chamada e o que acontece quando ela vira ciclo
A seta aponta de cima para baixo, e o professor desenha na lousa com os nomes em português:
rota -> regra | +---> banco
A rota chama a regra e o banco. A regra não chama ninguém. O banco não chama ninguém. E a dependência injetada é o que permite desenhar isso: a rota *recebe* o banco como parâmetro em vez de criar a conexão ela mesma.
Injeção de dependência, em uma frase, é: a coisa que você precisa entra pela porta em vez de ser procurada dentro de casa. O banco entra pela porta porque quem chama é a rota, e a rota não deveria saber como se conecta em MySQL. Quando o banco é procurado dentro de casa, o createPool mora na rota, e a rota passa a saber MySQL — e a camada do meio vira decoração.
O que quebra quando a seta vira ciclo é sempre a mesma coisa, e o professor já viu acontecer em dois formatos: o require dá erro de módulo não encontrado, ou o programa entra em laço e estoura a pilha. O nome que o aluno procura no erro é dependência circular, e a correção é inverter uma seta: quem tem a pergunta chama quem tem a resposta.
422 é da regra, 500 é do banco
A tabela de tradução é o produto da aula, e ela cabe em três linhas:
| Status | Quem decide | O que aconteceu |
|---|---|---|
422 | a regra | o dado não faz sentido |
500 | o banco | o dado fazia sentido e não deu para guardar |
201 | os dois, nessa ordem | passou na regra e o banco gravou |
O motivo de essa separação ser uma assinatura de correção, e não uma preferência de estilo, é o log. Se a rota devolvesse 500 para o dado de 900 graus, o error do log diria que o banco caiu, e quem fosse investigar iria olhar servidor, rede e disco. O banco está de pé, com duas linhas dentro, e o problema era um DHT11 quebrado. Uma leitura de 27,4 que vira 900 não aconteceu na sala: aconteceu no fio, e nenhum log de rede mostra isso.
E o inverso acontece também: se a regra devolvesse 422 quando o banco falha, o log diria que o dado era ruim, e ninguém olharia o banco. Um 500 que se disfarça de 422 custa mais que um 500 honesto, porque aponta a equipe para o lugar errado.
Gravar e avisar são duas respostas
O ponto que o dia 13 vai cobrar caro está hoje, e o professor passa devagar: a camada de regra devolve duas respostas para o mesmo dado, e elas não podem ser a mesma resposta.
aceitar responde "este número presta?". em_alerta responde "este número é um número que eu quero avisar?". Um dado de 34,8 graus é as duas coisas: é válido e é quente demais. Um dado de 900 é só a primeira resposta, e é negativa.
O erro da turma é juntar as duas num booleano só, e o erro tem nome no dia 13: alerta oscilante. Com um booleano único, a regra fica "não, por isso não gravo", e o painel perde a leitura justamente no momento em que alguém precisa dela. Com os dois separados, o dado é gravado e marcado, e o dia 13 aula 2 usa o segundo para pintar o gráfico.
O que a separação compra: medir e trocar
A Separation em camadas não é estética, e o professor mede na frente da turma. Ele roda a mesma rota duas vezes: uma com o banco de verdade e outra com um banco falso que só guarda em memória. As duas produzem os mesmos quatro status, e o MySQL fica com as mesmas duas linhas de antes.
Isso prova duas coisas, e as duas são caras:
- Trocar de banco custa uma classe nova. O dia 16 vai mostrar que o
MySQLdo projeto vira outra coisa em produção, e a aula inteira que vem existe para que essa troca seja de um dia, não de um mês. - Testar sem banco custa nada. O banco falso é o que o teste do dia 12 aula 2 vai usar, e ele existe exatamente por esta razão: o teste precisa de um banco que ele possa apagar sem medo.
A segunda forma de dizer o mesmo, e a que a turma precisa escrever no caderno: separar responsabilidades é o que permite trocar a peça sem trocar o sistema. E o nome em inglês para essa disciplina vem do SOLID, o conjunto de cinco princípios que formaliza as três camadas que o dia 10 aula 1 desenhou. O SOLID aparece aqui pela primeira vez e o professor escreve o nome inteiro na lousa sem abrir as cinco letras, porque nenhuma das cinco serve para a decisão que a turma está tomando: a decisão é acoplamento baixo e coesão alta, e as duas são medidas, não declaradas.
A camada de regra tem uma propriedade a mais: ela não precisa de nenhum dos dois. Ela recebe dois números e devolve um veredito, e por isso o cronômetro do fim da aula mostra cem mil chamadas em menos de dez milissegundos. Quando o dia 12 escrever o teste unitário, ele vai rodar essa camada em uma máquina sem placa, sem rede e sem banco, e vai passar na primeira vez.
E a regra de testar sem banco tem um critério que o professor escreve na lousa e que vale para toda a camada: se a função precisa de banco para ser testada, ela está na camada errada. Não é que testar com banco é proibido; é que o teste sem banco é o que roda em todo push, e o teste com banco é o que roda uma vez por dia.
Atividade
Montagem:
DHT11noGPIO4, com resistor de pull-up de 4,7 k ohm entre o pino de dados e3V3.- Servidor Node do dia 5 aula 2 rodando, com a tabela
tb_t2_leitura.
- Desenhe no caderno as três camadas do caminho do dado, com a pergunta de cada uma e a seta de chamada. Marque na seta o que é passado por parâmetro.
- Rode o script da resolução. Preencha no caderno a tabela dos quatro cenários: status, quem decidiu, o motivo e o que apareceu no banco.
- Escreva a função da sua rota que devolve
201,422e500. Diga, para cada status, qual camada decidiu, e o que a sua função de banco devolve no lugar doid. - Procure na sua rota qualquer
ifque compare temperatura com um número. Escreva a linha exata que você encontrou, ou escreva "não existe" se não houver. Se existir, para qual camada ela vai? - Escreva a camada de regra do seu projeto como função pura: recebe dois números, devolve um veredito, e não chama nada. Meça quanto tempo leva para cem mil chamadas, e escreva o número.
- Escreva o banco falso: uma classe com o mesmo método de gravar que a sua camada de banco, mas guardando num vetor em memória. Rode a mesma rota com o banco de verdade e com o banco falso, e compare os quatro status.
- No item 6, quantas linhas ficaram no
MySQLdepois das duas execuções? Quantas ficaram no banco falso? O que a diferença diz sobre a injeção de dependência? - Escreva, em duas linhas, o que muda no seu painel se você juntar
aceitareem_alertanum booleano só. Cite a tela que fica sem informação.
Nota: 12 pontos. Critério de fim: a tabela do item 2 preenchida nos quatro cenários, e as duas execuções do item 6 com os status comparados e as contagens de linha.
Resolucao
Do lado do Node, a mesma rota roda contra o MySQL de verdade e contra um banco de mentira, sem mudar uma linha. Este foi executado nesta máquina, contra MariaDB 10.11, no schema materiais_teste:
// dia 10 aula 2, lado do Node: rota, regra e banco, medidos.
const mysql = require('mysql2/promise');
// ===========================================================================
// CAMADA 3 - BANCO (repository). Sabe SQL. Nao sabe o que e HTTP, nem o que
// e "temperatura alta". Trocar MySQL por outra coisa muda SO esta camada.
// ===========================================================================
// O que a camada de banco devolve: o id gravado, ou um erro de banco.
// Ela nunca devolve "o dado era ruim" — essa resposta e da camada de regra.
class BancoMySQL {
constructor(pool) { this.pool = pool; }
async gravarLeitura(l) {
const [r] = await this.pool.execute(
INSERT INTO tb_t2_leitura (id_dispositivo, temperatura_c, umidade_pct, dt_leitura)
VALUES (?, ?, ?, ?), [l.id_dispositivo, l.temperatura_c, l.umidade_pct, l.dt_leitura]);
return { sucesso: true, id: r.insertId, codigo_erro: null };
}
}
// O banco de mentira: a MESMA interface, sem MySQL. E o que permite o teste
// do dia 12 rodar a rota inteira sem banco nenhum.
class BancoFalso {
constructor() { this.gravadas = []; this.chamarBanco = false; }
async gravarLeitura(l) { this.gravadas.push(l); return { sucesso: true, id: this.gravadas.length, codigo_erro: null }; }
async falharAoGravar(l) { return { sucesso: false, id: 0, codigo_erro: 'E_BANCO' }; }
}
// ===========================================================================
// CAMADA 2 - REGRA (service). Sabe o que faz sentido. Nao sabe HTTP, nao
// sabe SQL. E a unica camada que o teste do dia 12 roda sem banco e sem
// servidor, e por isso ela nao recebe nada de fora: recebe numeros.
// ===========================================================================
const LIMITE_ALERTA_C = 32.0;
const MIN_TEMP_C = -40.0;
const MAX_TEMP_C = 80.0;
function regra_avaliar(temperatura_c, umidade_pct) {
const v = { aceitar: false, em_alerta: false, motivo: '' };
if (typeof temperatura_c !== 'number' || !Number.isFinite(temperatura_c)) {
v.motivo = 'sensor devolveu NaN'; return v;
}
if (temperatura_c < MIN_TEMP_C || temperatura_c > MAX_TEMP_C) {
v.motivo = 'temperatura fora da faixa plausivel'; return v;
}
if (typeof umidade_pct !== 'number' || !Number.isFinite(umidade_pct) ||
umidade_pct < 0 || umidade_pct > 100) {
v.motivo = 'umidade fora de 0 a 100'; return v;
}
v.aceitar = true;
// O alerta NAO impede a gravacao. Sao duas respostas para o mesmo dado:
// "isso e um numero valido" e "isso e um numero que eu quero avisar".
v.em_alerta = temperatura_c > LIMITE_ALERTA_C;
v.motivo = v.em_alerta ? 'aceito, acima do limite de alerta' : 'aceito, dentro do esperado';
return v;
}
// ===========================================================================
// CAMADA 1 - ROTA (controller). Sabe HTTP. Traduz voto em status. Nao
// valida e nao grava. Se o if de faixa aparecer aqui, a camada 2 esta
// furada.
// ===========================================================================
async function rota_responder(banco, cenario, t, u) {
const v = regra_avaliar(t, u);
let status;
if (!v.aceitar) {
status = 422; // dado nao serve: e a REGRA que decidiu
} else {
const r = await banco.gravarLeitura(
{ id_dispositivo: 'estacao-01', temperatura_c: t, umidade_pct: u, dt_leitura: '2026-09-30 10:00:00' });
status = r.sucesso ? 201 : 500; // dado servia, o banco nao deu conta
}
console.log(${status} | ${cenario});
console.log( regra : ${v.motivo});
console.log( alerta: ${v.em_alerta ? 'SIM' : 'nao'});
console.log('');
return status;
}
(async () => {
const pool = mysql.createPool({
host: process.env.DB_HOST, port: Number(process.env.DB_PORT),
user: process.env.DB_USER, password: process.env.DB_PASS,
database: process.env.DB_NAME, dateStrings: true,
});
await pool.query('TRUNCATE TABLE tb_t2_leitura');
const banco = new BancoMySQL(pool);
console.log('--- quatro requisicoes atravessando as tres camadas ---');
await rota_responder(banco, 'leitura normal', 27.4, 61);
await rota_responder(banco, 'leitura quente, acima do alerta', 34.8, 55);
await rota_responder(banco, 'dado impossivel', 900.0, 61);
await rota_responder(banco, 'sensor sem resposta', NaN, 61);
console.log('--- onde mora cada decisao ---');
const [l] = await pool.query('SELECT COUNT(*) AS n, MIN(temperatura_c) AS min, MAX(temperatura_c) AS max FROM tb_t2_leitura');
console.log( linhas gravadas: ${l[0].n} | de ${l[0].min} a ${l[0].max} C);
console.log(' 422 quem decide? a REGRA. Nao foi problema de banco.');
console.log(' 500 quem decide? o BANCO. O dado ja tinha passado.');
console.log(' 201 quem decide? os dois, nessa ordem.');
console.log('');
console.log(' As duas leituras recusadas NAO viraram linha. Se a rota devolvesse');
console.log(' 500 para o dado de 900 C, o log do servidor diria que o banco caiu,');
console.log(' e o banco esta de pe com duas linhas dentro.');
console.log('--- 500: a rota traduzindo uma falha que NAO e dela ---');
const quebrado = { gravarLeitura: async () => ({ sucesso: false, id: 0, codigo_erro: 'E_BANCO' }) };
await rota_responder(quebrado, 'banco fora, dado bom', 27.4, 61);
console.log('--- o alerta nao e o mesmo que a rejeicao ---');
const quente = regra_avaliar(34.8, 55.0);
const normal = regra_avaliar(27.4, 61.0);
const impossivel = regra_avaliar(900.0, 61.0);
console.log( 34,8 C -> aceitar=${quente.aceitar ? 'sim' : 'nao'}, alerta=${quente.em_alerta ? 'sim' : 'nao'});
console.log( 27,4 C -> aceitar=${normal.aceitar ? 'sim' : 'nao'}, alerta=${normal.em_alerta ? 'sim' : 'nao'});
console.log( 900 C -> aceitar=${impossivel.aceitar ? 'sim' : 'nao'}, alerta=${impossivel.em_alerta ? 'sim' : 'nao'});
console.log('');
console.log(' Gravar e avisar sao duas respostas para o MESMO dado.');
console.log(' Quem junta as duas num so booleano perde uma das duas, e o');
console.log(' painel do dia 13 some justamente quando precisa aparecer.');
console.log('--- o teste que prova a separacao: trocar de banco ---');
const falso = new BancoFalso();
await rota_responder(falso, 'leitura normal, banco falso', 27.4, 61);
await rota_responder(falso, 'leitura quente, banco falso', 34.8, 55);
await rota_responder(falso, 'dado impossivel, banco falso', 900.0, 61);
const [resto] = await pool.query('SELECT COUNT(*) AS n FROM tb_t2_leitura');
console.log( gravadas no banco falso: ${falso.gravadas.length});
console.log( linhas que sobraram no MySQL: ${resto[0].n} (as duas de antes, intactas));
console.log('');
console.log(' A rota e a regra nao mudaram UMA linha. So o objeto que entrou no');
console.log(' lugar do outro. Isso e injecao de dependencia, e e o que permite');
console.log(' testar a rota inteira em milissegundos.');
console.log('--- a regra sozinha, sem nada em volta ---');
const inicio = process.hrtime.bigint();
let aceitas = 0;
for (let i = 0; i < 100000; i++) if (regra_avaliar(27.4, 61).aceitar) aceitas++;
const ms = Number(process.hrtime.bigint() - inicio) / 1e6;
console.log( 100000 chamadas de regra_avaliar(27.4, 61) em ${ms.toFixed(1)} ms);
console.log( aceitas: ${aceitas} | banco tocado: ${falso.chamarBanco ? 'sim' : 'NAO'});
console.log(' A camada 2 nao sabe que HTTP e MySQL existem. E por isso que o');
console.log(' teste do dia 12 aula 1 roda nela em poucos milissegundos.');
await pool.query('TRUNCATE TABLE tb_t2_leitura');
await pool.end();
})();
A saída real, medida nesta máquina:
--- quatro requisicoes atravessando as tres camadas --- 201 | leitura normal regra : aceito, dentro do esperado alerta: nao 201 | leitura quente, acima do alerta regra : aceito, acima do limite de alerta alerta: SIM 422 | dado impossivel regra : temperatura fora da faixa plausivel alerta: nao 422 | sensor sem resposta regra : sensor devolveu NaN alerta: nao --- onde mora cada decisao --- linhas gravadas: 2 | de 27.40 a 34.80 C 422 quem decide? a REGRA. Nao foi problema de banco. 500 quem decide? o BANCO. O dado ja tinha passado. 201 quem decide? os dois, nessa ordem. As duas leituras recusadas NAO viraram linha. Se a rota devolvesse 500 para o dado de 900 C, o log do servidor diria que o banco caiu, e o banco esta de pe com duas linhas dentro. --- 500: a rota traduzindo uma falha que NAO e dela --- 500 | banco fora, dado bom regra : aceito, dentro do esperado alerta: nao --- o alerta nao e o mesmo que a rejeicao --- 34,8 C -> aceitar=sim, alerta=sim 27,4 C -> aceitar=sim, alerta=nao 900 C -> aceitar=nao, alerta=nao Gravar e avisar sao duas respostas para o MESMO dado. Quem junta as duas num so booleano perde uma das duas, e o painel do dia 13 some justamente quando precisa aparecer. --- o teste que prova a separacao: trocar de banco --- 201 | leitura normal, banco falso regra : aceito, dentro do esperado alerta: nao 201 | leitura quente, banco falso regra : aceito, acima do limite de alerta alerta: SIM 422 | dado impossivel, banco falso regra : temperatura fora da faixa plausivel alerta: nao gravadas no banco falso: 2 linhas que sobraram no MySQL: 2 (as duas de antes, intactas) A rota e a regra nao mudaram UMA linha. So o objeto que entrou no lugar do outro. Isso e injecao de dependencia, e e o que permite testar a rota inteira em milissegundos. --- a regra sozinha, sem nada em volta --- 100000 chamadas de regra_avaliar(27.4, 61) em 9.9 ms aceitas: 100000 | banco tocado: NAO A camada 2 nao sabe que HTTP e MySQL existem. E por isso que o teste do dia 12 aula 1 roda nela em poucos milissegundos.
O professor aponta cinco coisas nessa saída, e as cinco são medidas:
linhas gravadas: 2 | de 27.40 a 34.80 C: oMINe oMAXprovam que os900e osNaNnão entraram. A faixa que o professor escreveu na lousa aparece como número no banco, e oNaNaparece como ausência de linha, e não como0.422 | sensor sem resposta, motivosensor devolveu NaN:NaNnão é0e não é-999. O motivo nomeado no log é o que separa "banco caiu" de "sensor parou", e é o que o dia 15 aula 1 usa para fechar a conta de perdas.500 | banco fora, dado bom: a rota traduziu uma falha de banco sem nunca saber que foi falha de banco. Ela viusucesso: falsee devolveu500. Essa é a assinatura da camada de banco: um booleano e um código, sem a causa.gravadas no banco falso: 2comlinhas que sobraram no MySQL: 2: os quatro status saíram iguais nas duas execuções, e oMySQLficou exatamente como estava. A prova de que trocar de banco é uma classe nova, e não um redesenho.100000 chamadas em 9.9 ms: o número que o dia 12 vai usar. Uma camada que precisa de dez milissegundos para cem mil chamadas roda dentro de qualquerpush; uma camada que abre conexão de banco por chamada não roda em lugar nenhum.
O sketch da placa, que é a mesma separação do outro lado do cabo:
// dia 10, aula 2: rota, regra e banco. // // Tres camadas, tres perguntas, e a ordem importa: // // rota -> HTTP: qual metodo, qual caminho, qual status? // regra -> negocio: este dado faz sentido? e o que eu faco com ele? // banco -> SQL: qual comando, qual coluna, qual parametro? // // O teste do professor: apague o arquivo do banco e a regra continua // funcionando. Apague a regra e a rota quebra. Isso so acontece se as // camadas estao mesmo separadas. #include <Arduino.h> #include <DHT.h> #include <string.h> const int PIN_DHT = 4; const uint8_t TIPO_SENSOR = DHT11; const char* ID_DISPOSITIVO = "estacao-01"; // =========================================================================== // CAMADA 3 — BANCO // Sabe SQL. Nao sabe o que e HTTP, nem o que e "temperatura alta". // Trocar MySQL por outra coisa muda SO esta camada. // =========================================================================== // O que a camada de banco devolve: o id gravado, ou um erro de banco. // Ela nunca devolve "o dado era ruim" — essa resposta e da camada de regra. struct ResultadoBanco { bool sucesso; long id; char codigo_erro[16]; ResultadoBanco() : sucesso(false), id(0) { codigo_erro[0] = '\0'; } }; ResultadoBanco banco_gravar(float temperatura_c, float umidade_pct) { ResultadoBanco r; // O INSERT real usaria mysql2 no Node. Na placa nao ha MySQL: a placa // so fala HTTP. O que interessa aqui e a ASSINATURA: a camada devolve // "gravei / nao gravei", nunca "o dado era invalido". if (isnan(temperatura_c) || isnan(umidade_pct)) { strcpy(r.codigo_erro, "E_DADO_NULO"); return r; } r.sucesso = true; r.id = 1; return r; } // =========================================================================== // CAMADA 2 — REGRA (service) // Sabe o que faz sentido. Nao sabe HTTP, nao sabe SQL. // E a unica camada que o teste do dia 12 roda sem banco e sem servidor. // =========================================================================== const float LIMITE_ALERTA_C = 32.0f; const float MIN_TEMP_C = -40.0f; const float MAX_TEMP_C = 80.0f; struct VotoRegra { bool aceitar; bool em_alerta; char motivo[64]; VotoRegra() : aceitar(false), em_alerta(false) { motivo[0] = '\0'; } }; VotoRegra regra_avaliar(float temperatura_c, float umidade_pct) { VotoRegra v; if (isnan(temperatura_c)) { strcpy(v.motivo, "sensor devolveu NaN"); return v; } if (temperatura_c < MIN_TEMP_C || temperatura_c > MAX_TEMP_C) { strcpy(v.motivo, "temperatura fora da faixa plausivel"); return v; } if (isnan(umidade_pct) || umidade_pct < 0 || umidade_pct > 100) { strcpy(v.motivo, "umidade fora de 0 a 100"); return v; } v.aceitar = true; // O alerta NAO impede a gravacao. Sao duas respostas para o mesmo dado: // "isso e um numero valido" e "isso e um numero que eu quero avisar". // Confundir as duas e o erro que faz o painel sumir quando da alerta. v.em_alerta = temperatura_c > LIMITE_ALERTA_C; strcpy(v.motivo, v.em_alerta ? "aceito, acima do limite de alerta" : "aceito, dentro do esperado"); return v; } // =========================================================================== // CAMADA 1 — ROTA // Sabe HTTP. Traduz voto em status. Nao valida e nao grava. // Se o `if` de faixa aparecer aqui, a camada 2 esta furada. // =========================================================================== void rota_responder(const char* cenario, float t, float u) { VotoRegra v = regra_avaliar(t, u); const char* status; if (!v.aceitar) { status = "422"; // dado nao serve } else if (!banco_gravar(t, u).sucesso) { status = "500"; // dado servia, o banco nao deu conta } else { status = "201"; // gravado } Serial.print(status); Serial.print(" | "); Serial.println(cenario); Serial.print(" regra: "); Serial.println(v.motivo); Serial.print(" alerta: "); Serial.println(v.em_alerta ? "SIM" : "nao"); Serial.println(); } void setup() { Serial.begin(115200); Serial.println(); Serial.println("dia 10 aula 2 — camadas: rota, regra e banco"); Serial.println("============================================"); Serial.print("alerta a partir de "); Serial.print(LIMITE_ALERTA_C, 1); Serial.println(" C, com faixa aceita de -40 a 80 C"); Serial.println(); Serial.println("--- quatro requisicoes atravessando as tres camadas ---"); rota_responder("leitura normal", 27.4f, 61.0f); rota_responder("leitura quente, acima do alerta", 34.8f, 55.0f); rota_responder("dado impossivel", 900.0f, 61.0f); rota_responder("sensor sem resposta", NAN, 61.0f); Serial.println(); Serial.println("--- onde mora cada decisao ---"); Serial.println(" 422 quem decide? a REGRA. Nao foi problema de banco."); Serial.println(" 500 quem decide? o BANCO. O dado ja tinha passado."); Serial.println(" 201 quem decide? os dois, nessa ordem."); Serial.println(); Serial.println(" Se a rota devolvesse 500 para o dado de 900 C, o log do"); Serial.println(" servidor diria que o banco caiu. E o banco esta de pe."); Serial.println(); Serial.println("--- o teste que prova a separacao ---"); Serial.println(" regra_avaliar(34.8, 55) roda sem banco e sem rede."); Serial.println(" Por isso o teste do dia 12 aula 1 roda em 40 ms:"); Serial.println(" ele so chama a camada 2. O banco so aparece no dia 12 aula 2."); Serial.println(); Serial.println("--- o alerta nao e o mesmo que a rejeicao ---"); VotoRegra quente = regra_avaliar(34.8f, 55.0f); Serial.print(" 34,8 C -> aceito="); Serial.print(quente.aceitar ? "sim" : "nao"); Serial.print(", alerta="); Serial.println(quente.em_alerta ? "sim" : "nao"); Serial.println(" Gravar e avisar sao duas respostas para o MESMO dado."); Serial.println(" Quem junta as duas num so booleano perde uma das duas."); } void loop() { Serial.println(); Serial.println("--- nova rodada ---"); setup(); delay(10000); }
Por que assim e não de outro jeito. As três camadas do sketch estão na ordem inversa do desenho da lousa, e o professor assume isso na frente da turma. O banco_gravar vem antes de regra_avaliar só porque o C++ exige que a função esteja declarada antes de ser usada. A ordem de escrita não é a ordem de chamada, e a segunda é a que importa.
A VotoRegra e a ResultadoBanco são struct com construtor, e isso é o que impede o bug de campo não inicializado. VotoRegra() põe aceitar em falso, em_alerta em falso e motivo com o terminador na primeira posição. Sem o construtor, regra_avaliar que devolvesse pelo primeiro return deixaria motivo com lixo de memória, e o Serial.println imprimiria lixo. O dia 4 do 1o trimestre mostrou o strncpy e o terminador; aqui o mesmo problema aparece em C++, e a solução é o construtor.
O strcpy do motivo tem limite de destino e o professor aponta isso: motivo tem 64 bytes, e nenhum motivo da regra passa de trinta. O dia 1 do 1o trimestre mostrou o que acontece quando o texto é do tamanho exato do vetor: o terminador não cabe e o Serial.println lê memória vizinha. A regra aqui é a mesma: o strcpy vem sempre acompanhado de um limite verificado, e o professor pede que o aluno calcule o maior motivo possível antes de escrever o sizeof.
O banco_gravar recebe dois float e não a struct do sensor, e isso é injeção de dependência na sua forma mais simples: quem chama decide o que passa. A camada de banco não sabe de onde vieram os dois números, não conhece o DHT11 e não tem como descobrir. O mesmo vale para a regra. Se a regra recebesse a struct do sensor, ela passaria a saber o tipo do sensor, e o dia 14 aula 1 seria um problema de novo.
O NAN passado como argumento na quarta chamada é o teste mais honesto do arquivo. A placa não tem sensor sem resposta garantido na bancada, e o professor não quer depender disso para mostrar o caso: passando NAN na mão, o isnan da regra dispara sempre, e o aluno vê o 422 com motivo nomeado em qualquer bancada, sem sensor nenhum.
A rota tem dois if e nenhum número. É o critério de correção que o professor pede no item 4 da atividade, e ele é verificável em trinta segundos: se existe algum número no corpo da rota, a camada do meio está furada. No arquivo inteiro, os dois únicos números que aparecem na rota são as strings "422", "500" e "201", que são status, não valores de negócio.
Criterios de correcao
| Critério | Pontos |
|---|---|
| Diagrama das três camadas desenhado, com a pergunta de cada uma e a seta de chamada | 2 pontos |
DHT11 no GPIO4 com pull-up, e pinos e tipos em const no topo | 1 pontos |
| Tabela do item 2 preenchida nos quatro cenários, com quem decidiu e o que apareceu no banco | 2 pontos |
| Função de status da rota escrita, e a camada que decide cada um dos três status nomeada | 2 pontos |
Item 4: o if de faixa na rota encontrado e a camada de destino escrita, ou "não existe" | 1 pontos |
| Item 5: a camada de regra como função pura, e o tempo de cem mil chamadas medido | 2 pontos |
| Banco falso escrito, e os quatro status comparados entre as duas execuções | 1 pontos |
| Item 8: as duas linhas sobre o booleano único, com a tela que fica sem informação | 1 pontos |
Erros comuns
| Erro | Como aparece | Correção |
|---|---|---|
if de faixa dentro da rota | "A rota tem if (temp > 80) return 422" | "Esse if é regra morando no lugar errado. A rota pergunta à regra e traduz a resposta em status; o número da faixa mora na camada de regra, e ele não sabe o que é res.writeHead." |
| Banco devolvendo "dado inválido" | "O INSERT retornou que o dado estava errado" | "A camada de banco devolve "gravei" ou "não gravei". A resposta "o dado era ruim" é da regra: se o banco a desse, a regra viraria decoração e o 422 viraria 500." |
| Criar a conexão dentro da rota | "A rota faz createPool antes de gravar" | "Isso é MySQL dentro da rota. Receba o banco por parâmetro: aí trocar de banco é uma classe nova, e o teste do dia 12 roda sem banco nenhum." |
Unir aceitar e em_alerta num booleano | "Se está acima do limite, não gravo" | "Gravar e avisar são duas respostas. O dado de 34,8 é válido e quente: ele entra no histórico e acende o alerta. Juntar os dois faz o painel sumir quando a sala está quente." |
500 para qualquer falha | "Deu problema, então 500" | "O 500 é do banco. Dado fora de faixa é 422, e o log precisa dizer qual dos dois foi, senão quem investiga vai olhar servidor onde o problema é o DHT11." |
422 para falha de banco | "O execute deu erro, então dado inválido" | "O mesmo pelo avesso: o 422 manda o INVESTIGADOR para o sensor, e o sensor está perfeito. Um 422 que disfarça 500 custa mais que um 500 honesto." |
| Regra que chama o banco | "A regra grava e depois valida" | "Aí a seta vira e a regra precisa de banco para ser testada. Regra pura recebe números e devolve veredito; quem grava é a rota, que é quem tem o banco na mão." |
| Dependência circular | "O require dá erro de módulo não encontrado" | "Seta voltando. Quem tem a pergunta chama quem tem a resposta: a rota chama a regra, a rota chama o banco. Inverta uma seta e o erro some." |
Cronometrar em volta de await | "Rodei com o banco ligado e deu 4 segundos" | "Você cronometrou a ida e a volta da rede. O que o dia 12 mede é a função pura sozinha, e ela dá milissegundos porque não tem await dentro." |
| Banco falso com interface diferente | "Meu falso tem inserir e o meu tem gravar" | "A injeção só funciona se a interface for a mesma. O banco falso é a prova de que o contrato da camada está escrito; se as assinaturas divergem, a camada não tinha contrato." |
| Achatar as três camadas em três arquivos só | "Criei rota.js, regra.js e banco.js com o mesmo conteúdo" | "Três arquivos com o mesmo conteúdo é três nomes, não três camadas. Abra os três e conte as perguntas de cada um. O número tem que cair de um para um." |
Desafio extra
Adicione uma quarta camada de apresentação, que é a que o dia 13 vai precisar: a função que transforma o veredito da regra em linha de log, sem tocar no Serial e sem tocar no banco. Escreva as quatro camadas em quatro arquivos, com a seta de dependência desenhada, e depois troque o MySQL por um banco em memória e o Serial por um coletor de log em memória, sem mudar nenhuma das outras camadas. O que o teste final mede é quantas linhas você teve de tocar para fazer as duas trocas, e a resposta que o professor espera é zero.
A resolucao, compilada
// dia 10, aula 2: rota, regra e banco. // // Tres camadas, tres perguntas, e a ordem importa: // // rota -> HTTP: qual metodo, qual caminho, qual status? // regra -> negocio: este dado faz sentido? e o que eu faco com ele? // banco -> SQL: qual comando, qual coluna, qual parametro? // // O teste do professor: apague o arquivo do banco e a regra continua // funcionando. Apague a regra e a rota quebra. Isso so acontece se as // camadas estao mesmo separadas. #include <Arduino.h> #include <DHT.h> #include <string.h> const int PIN_DHT = 4; const uint8_t TIPO_SENSOR = DHT11; const char* ID_DISPOSITIVO = "estacao-01"; // =========================================================================== // CAMADA 3 — BANCO // Sabe SQL. Nao sabe o que e HTTP, nem o que e "temperatura alta". // Trocar MySQL por outra coisa muda SO esta camada. // =========================================================================== // O que a camada de banco devolve: o id gravado, ou um erro de banco. // Ela nunca devolve "o dado era ruim" — essa resposta e da camada de regra. struct ResultadoBanco { bool sucesso; long id; char codigo_erro[16]; ResultadoBanco() : sucesso(false), id(0) { codigo_erro[0] = '\0'; } }; struct VotoRegra { bool aceitar; bool em_alerta; char motivo[64]; VotoRegra() : aceitar(false), em_alerta(false) { motivo[0] = '\0'; } }; ResultadoBanco banco_gravar(float temperatura_c, float umidade_pct) { ResultadoBanco r; // O INSERT real usaria mysql2 no Node. Na placa nao ha MySQL: a placa // so fala HTTP. O que interessa aqui e a ASSINATURA: a camada devolve // "gravei / nao gravei", nunca "o dado era invalido". if (isnan(temperatura_c) || isnan(umidade_pct)) { strcpy(r.codigo_erro, "E_DADO_NULO"); return r; } r.sucesso = true; r.id = 1; return r; } // =========================================================================== // CAMADA 2 — REGRA (service) // Sabe o que faz sentido. Nao sabe HTTP, nao sabe SQL. // E a unica camada que o teste do dia 12 roda sem banco e sem servidor. // // A struct VotoRegra vem ANTES de qualquer funcao deste arquivo, e nao aqui // embaixo. Isso nao e estilo: o Arduino gera prototipo automatico de cada // funcao e insere no topo do arquivo, DEPOIS dos includes. Com `banco_gravar` // (linha 36) declarada antes desta struct, o prototipo de `regra_avaliar` // sobe para antes de `VotoRegra` existir e o build quebra com // "'VotoRegra' does not name a type" — mensagem que aponta a struct como // culpada quando a causa e a ordem das camadas. // =========================================================================== const float LIMITE_ALERTA_C = 32.0f; const float MIN_TEMP_C = -40.0f; const float MAX_TEMP_C = 80.0f; VotoRegra regra_avaliar(float temperatura_c, float umidade_pct) { VotoRegra v; if (isnan(temperatura_c)) { strcpy(v.motivo, "sensor devolveu NaN"); return v; } if (temperatura_c < MIN_TEMP_C || temperatura_c > MAX_TEMP_C) { strcpy(v.motivo, "temperatura fora da faixa plausivel"); return v; } if (isnan(umidade_pct) || umidade_pct < 0 || umidade_pct > 100) { strcpy(v.motivo, "umidade fora de 0 a 100"); return v; } v.aceitar = true; // O alerta NAO impede a gravacao. Sao duas respostas para o mesmo dado: // "isso e um numero valido" e "isso e um numero que eu quero avisar". // Confundir as duas e o erro que faz o painel sumir quando da alerta. v.em_alerta = temperatura_c > LIMITE_ALERTA_C; strcpy(v.motivo, v.em_alerta ? "aceito, acima do limite de alerta" : "aceito, dentro do esperado"); return v; } // =========================================================================== // CAMADA 1 — ROTA // Sabe HTTP. Traduz voto em status. Nao valida e nao grava. // Se o `if` de faixa aparecer aqui, a camada 2 esta furada. // =========================================================================== void rota_responder(const char* cenario, float t, float u) { VotoRegra v = regra_avaliar(t, u); const char* status; if (!v.aceitar) { status = "422"; // dado nao serve } else if (!banco_gravar(t, u).sucesso) { status = "500"; // dado servia, o banco nao deu conta } else { status = "201"; // gravado } Serial.print(status); Serial.print(" | "); Serial.println(cenario); Serial.print(" regra: "); Serial.println(v.motivo); Serial.print(" alerta: "); Serial.println(v.em_alerta ? "SIM" : "nao"); Serial.println(); } void setup() { Serial.begin(115200); Serial.println(); Serial.println("dia 10 aula 2 — camadas: rota, regra e banco"); Serial.println("============================================"); Serial.print("alerta a partir de "); Serial.print(LIMITE_ALERTA_C, 1); Serial.println(" C, com faixa aceita de -40 a 80 C"); Serial.println(); Serial.println("--- quatro requisicoes atravessando as tres camadas ---"); rota_responder("leitura normal", 27.4f, 61.0f); rota_responder("leitura quente, acima do alerta", 34.8f, 55.0f); rota_responder("dado impossivel", 900.0f, 61.0f); rota_responder("sensor sem resposta", NAN, 61.0f); Serial.println(); Serial.println("--- onde mora cada decisao ---"); Serial.println(" 422 quem decide? a REGRA. Nao foi problema de banco."); Serial.println(" 500 quem decide? o BANCO. O dado ja tinha passado."); Serial.println(" 201 quem decide? os dois, nessa ordem."); Serial.println(); Serial.println(" Se a rota devolvesse 500 para o dado de 900 C, o log do"); Serial.println(" servidor diria que o banco caiu. E o banco esta de pe."); Serial.println(); Serial.println("--- o teste que prova a separacao ---"); Serial.println(" regra_avaliar(34.8, 55) roda sem banco e sem rede."); Serial.println(" Por isso o teste do dia 12 aula 1 roda em 40 ms:"); Serial.println(" ele so chama a camada 2. O banco so aparece no dia 12 aula 2."); Serial.println(); Serial.println("--- o alerta nao e o mesmo que a rejeicao ---"); VotoRegra quente = regra_avaliar(34.8f, 55.0f); Serial.print(" 34,8 C -> aceito="); Serial.print(quente.aceitar ? "sim" : "nao"); Serial.print(", alerta="); Serial.println(quente.em_alerta ? "sim" : "nao"); Serial.println(" Gravar e avisar sao duas respostas para o MESMO dado."); Serial.println(" Quem junta as duas num so booleano perde uma das duas."); } void loop() { Serial.println(); Serial.println("--- nova rodada ---"); setup(); delay(10000); }
Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia10 aula2.
