Estruturar o código — Arduino e IoT — semana 10 do 2o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 10 · Estruturar o código — Material de Apoio Arduino

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 DHT11 por dupla, no circuito do dia 8 do 1o trimestre, com resistor de pull-up
  • 1 computador com Node.js 20, mysql2 instalado 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:

  1. qual método e qual caminho foi pedido?
  2. quem está falando, e a chave bate?
  3. o dado recebido faz sentido?
  4. como eu gravo isso no banco?
  5. 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çaArquivos que devem mudarSe mudou mais, o módulo está furado
trocar o sensor DHT11 por um BME280só o do sensorrota e validação
mudar a faixa de temperatura aceitávelsó o de configuraçãotudo
trocar o MySQL por outro bancosó o do bancorota, 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 placaNo Node
o arquivo éum .inoum .js
a divisão é// ---- modulo 1 ---- dentro do arquivoum arquivo por módulo
a ligação éo IDE concatena antes de compilarrequire ou import
o contrato éo nome da função e o tipo do retornoo nome do module.exports e a assinatura
quem carregao compiladoro 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:

  • DHT11 no GPIO4, com resistor de pull-up de 4,7 k ohm entre o pino de dados e 3V3.
  • Servidor Node do dia 5 aula 2 rodando, com a tabela tb_t2_leitura.
  1. 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.
  2. 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?
  3. Meça o acoplamento do seu arquivo antes de quebrar: conte quantas linhas citam pool.execute, quantas citam res.writeHead e quantas citam readTemperature. Escreva os três números no caderno.
  4. 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ó.
  5. 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ê?
  6. 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.
  7. 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.
  8. 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: o BME280 entra com duas casas decimais a mais que o DHT11, 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 transforma NaN em null sozinho, sem erro e sem aviso. O módulo da rota precisa checar Number.isFinite antes de formatar, porque se deixar o JSON.stringify resolver, o null vai para o Node e o dia 8 aula 1 devolve 400 de "campo vazio" em vez do 422 de "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 o package.json do 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érioPontos
Lista de perguntas do arquivo do projeto escrita, com as duas escolhidas para o primeiro módulo2 pontos
DHT11 no GPIO4 com pull-up, e pinos e tipos em const no topo1 pontos
Matriz de acoplamento da resolução desenhada no caderno, com a contagem de uns1 pontos
Os três números de acoplamento medidos no projeto antes da quebra2 pontos
Projeto quebrado em módulos, um por camada, cada um respondendo uma pergunta2 pontos
Os mesmos três números medidos depois, com a diferença explicada2 pontos
Estrutura de pastas desenhada, com a seta de dependência e nenhuma seta voltando1 pontos
Item 8: a frase sobre trocar o banco, com um arquivo do projeto que não mudaria1 pontos

Erros comuns

ErroComo apareceCorreçã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 422 e o 500 nascem 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 DHT11 por dupla, no circuito do dia 8 do 1o trimestre, com resistor de pull-up
  • 1 computador com Node.js 20, mysql2 instalado 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:

CamadaNome em inglêsPerguntaNão sabe
rotacontrollerqual método, qual caminho, qual status?o que é faixa, o que é SQL
regraserviceeste dado faz sentido? e o que eu faço com ele?o que é HTTP, o que é tabela
bancorepositoryqual 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:

StatusQuem decideO que aconteceu
422a regrao dado não faz sentido
500o bancoo dado fazia sentido e não deu para guardar
201os dois, nessa ordempassou 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:

  1. Trocar de banco custa uma classe nova. O dia 16 vai mostrar que o MySQL do 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.
  2. 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:

  • DHT11 no GPIO4, com resistor de pull-up de 4,7 k ohm entre o pino de dados e 3V3.
  • Servidor Node do dia 5 aula 2 rodando, com a tabela tb_t2_leitura.
  1. 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.
  2. 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.
  3. Escreva a função da sua rota que devolve 201, 422 e 500. Diga, para cada status, qual camada decidiu, e o que a sua função de banco devolve no lugar do id.
  4. Procure na sua rota qualquer if que 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?
  5. 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.
  6. 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.
  7. No item 6, quantas linhas ficaram no MySQL depois das duas execuções? Quantas ficaram no banco falso? O que a diferença diz sobre a injeção de dependência?
  8. Escreva, em duas linhas, o que muda no seu painel se você juntar aceitar e em_alerta num 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: o MIN e o MAX provam que os 900 e os NaN não entraram. A faixa que o professor escreveu na lousa aparece como número no banco, e o NaN aparece como ausência de linha, e não como 0.
  • 422 | sensor sem resposta, motivo sensor devolveu NaN: NaN não é 0 e 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 viu sucesso: false e devolveu 500. Essa é a assinatura da camada de banco: um booleano e um código, sem a causa.
  • gravadas no banco falso: 2 com linhas que sobraram no MySQL: 2: os quatro status saíram iguais nas duas execuções, e o MySQL ficou 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 qualquer push; 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érioPontos
Diagrama das três camadas desenhado, com a pergunta de cada uma e a seta de chamada2 pontos
DHT11 no GPIO4 com pull-up, e pinos e tipos em const no topo1 pontos
Tabela do item 2 preenchida nos quatro cenários, com quem decidiu e o que apareceu no banco2 pontos
Função de status da rota escrita, e a camada que decide cada um dos três status nomeada2 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 medido2 pontos
Banco falso escrito, e os quatro status comparados entre as duas execuções1 pontos
Item 8: as duas linhas sobre o booleano único, com a tela que fica sem informação1 pontos

Erros comuns

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