Projeto do trimestre — Arduino e IoT — semana 13 do 2o trimestre
Semana 13 de 16· 2o trimestre · 01/05 a 04/09
Projeto do trimestre
A estação meteorológica vira sistema com histórico, painel e consulta.
Aula 1 — Projeto: historico de temperatura e umidade
Objetivos
- Escrever o requisito do projeto em uma frase, e transformar essa frase nas consultas que o sistema precisa responder.
- Distinguir gravar histórico de consultar histórico, e escrever as duas partes do seu projeto.
- Escolher o período e a granularidade do gráfico com base no que o painel precisa, e não no que existe no banco.
- Reconhecer que o detalhe que o gráfico mostra é o detailhe que a consulta devolve, e que guardar tudo não é o mesmo que mostrar tudo.
- Medir a série de sete dias com
AVG,MINeMAX, e entender o que a média sozinha esconde.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1
DHT11por dupla, com resistor de pull-up de 4,7 k ohm entre o pino de dados e3V3 - 1 computador com o projeto do aluno e a tabela de histórico já gravada, do dia 12
- Folha de papel por dupla, com a tabela de período por granularidade
- Projetor, para o terminal do Node e o monitor serial da placa juntos
Conceitos
O requisito do projeto é uma frase, e a frase vira consultas
O requisito do projeto do trimestre cabe em uma frase, e o professor escreve na lousa antes de qualquer código:
A estação meteorológica guarda o histórico de temperatura e umidade, mostra a leitura atual, e permite consultar um período escolhido.
Dessa frase saem três verbos, e cada verbo é uma consulta:
| Verbo da frase | Consulta que ele exige | Onde roda |
|---|---|---|
| gravar histórico | INSERT de cada leitura que passou na regra | rota, no POST |
| ler a leitura atual | a última linha do dispositivo | rota, o que o painel consulta a cada segundo |
| consultar histórico | o período escolhido, com o agrupamento escolhido | rota, o que o gráfico pede |
O entregável do trimestre é o sistema com as três partes funcionando, e o critério de fim que o professor dá é simples: alguém que nunca viu o projeto consegue perguntar "como estava a sala terça à tarde" e receber a resposta em menos de um segundo.
A granularidade do gráfico é a decisão que aparece quando o verbo "consultar" tem dois objetos: um período e um agrupamento. "Últimas 24 horas" é um período. "Agrupado por hora" é uma granularidade. "Últimos 7 dias, por dia" é um período com uma granularidade. São escolhas independentes na teoria e inseparáveis na prática, e o motivo está na próxima seção.
Detalhe guardado não é detalhe mostrado
A granularidade é o verbete que a turma precisa levar desta aula: em que tamanho de caixa cada ponto do gráfico representa dado. Uma hora, um dia, uma semana. E o ponto inteiro da aula cabe numa frase: não guardar o detalhe todo no gráfico é o erro, e guardar o detalhe todo no banco é o certo.
A distinção que separa as duas coisas é entre o que o banco guarda e o que a consulta devolve. O banco guarda tudo, sempre, linha por linha. É o dia 7 aula 1 que definiu a taxa de escrita, e ela continua valendo: a placa manda a cada dez segundos, e o banco aceita. O que muda é a consulta, que decide o quanto de detalhe sai para o navegador.
A tabela da atividade é a decisão completa, e ela foi medida nesta máquina com 168 linhas de sete dias de dado:
| Período | Agrupar por | Pontos | O que o painel mostra |
|---|---|---|---|
| 24 h | sem agrupar | 24 | a subida da manhã e o pico da tarde |
| 24 h | por hora | 24 | quase igual, resposta menor |
| 7 d | por hora | 168 | a semana inteira, hora a hora |
| 7 d | por dia | 7 | só a média de cada dia |
| 30 d | por hora | 720 | pesado demais para o navegador |
| 30 d | por dia | 30 | o painel do professor |
Os dois casos do meio e do fim são a mesma consulta com granularidades diferentes, e a diferença de pontos — 168 contra 7 — é o que o navegador paga para desenhar. A pergunta que o professor escreve ao lado da tabela é a mesma para as seis linhas: quanto detalhe o painel precisa? E a segunda, que é a armadilha: quanto detalhe existe no banco? São respostas diferentes, e responder pela segunda é o erro que enche a tela de pontos e não diz nada.
A média sozinha esconde a variação
A consulta mais comum do painel é AVG, e ela é a que mais mente quando usada sozinha. A série medida mostra isso com números:
2026-09-23 media 28.2 22.30 a 34.30 C 2026-09-24 media 28.2 22.30 a 34.30 C 2026-09-25 media 28.2 22.30 a 34.30 C 2026-09-26 media 28.2 22.30 a 34.30 C 2026-09-27 media 28.2 22.30 a 34.30 C 2026-09-28 media 28.2 22.30 a 34.30 C 2026-09-29 media 28.2 22.30 a 34.30 C
Sete dias, a mesma média, e a mesma faixa. Um gráfico só de média é uma reta horizontal, e a reta horizontal diz que a sala ficou em 28 graus o dia inteiro, o que é falso. A mínima e a máxima no mesmo ponto resolvem, e elas são a mesma consulta: MIN(temperatura_c) e MAX(temperatura_c) ao lado do AVG.
O que o painel faz com o par é agrupar por hora quando a granularidade é de hora, e o motivo de o AVG existir é o mesmo: a temperatura média de uma hora é o que interessa para decidir se a sala estava quente, e o pico importa menos do que a sensação. Isso é uma decisão de projeto, e o professor pede que a turma escreva qual é a decisão do seu painel: se ele mostra a média, ele responde "como estava, em geral"; se mostra o pico, ele responde "quando foi pior"; se mostra os dois, ele responde as duas coisas.
Ancorar no dado, não no relógio
Um detalhe que aparece na saída medida e que o professor explica porque vai morder no dia 15: as consultas de período usam a última leitura gravada como âncora, e não NOW().
A âncora do dado é a última linha que existe, e a pergunta que o painel faz é "as últimas 24 horas de dado". A pergunta que um NOW() faz é "as últimas 24 horas do meu relógio". As duas dão resultados diferentes quando a placa está desligada há uma hora: o NOW() devolve meia hora de dado e o painel mostra metade do gráfico; a âncora devolve as 24 horas completas.
O caso real do trimestre é pior: na aula 14, quando o professor desliga o servidor para mostrar o que acontece sem ele, o relógio continua andando e o dado para. No dia 15, com o sistema fechados e a placa havingiendo desligada a noite toda, o NOW() do dia 15 de manhã devolveria zero linhas para as últimas 24 horas, e o painel ficaria vazio sem nenhuma mensagem de erro. A âncora no dado desiste: mostra o que existe e mostra quando parou, que é a informação que interessa.
O que é da placa e o que é do servidor
A separação do projeto do trimestre é a mesma do dia 10 aula 2, e o sketch da resolução a demonstra na prática:
| É da placa | É do servidor |
|---|---|
ler o DHT11 | decidir se o dado presta |
montar o JSON | gravar a linha |
| enviar a cada 10 s | escolher o período do gráfico |
mostrar o que o HTTP devolveu | agrupar por hora ou por dia |
| guardar a sequência da leitura | decidir o alerta |
A granularidade do gráfico e o agrupar por dia estão todos na coluna da direita, e o motivo é o teste do dia 12: uma regra que roda sem placa é uma regra que se testa sem placa, e a regra do histórico se teste com SELECT e dados semeados, sem sensor nenhum. Se o período e a granularidade estivessem no firmware, seria um teste que exigiria bancada para cada período, e a turma teria sete testes para rodar em vez de um.
A sequência é o único dado que a placa acrescenta e o servidor guarda, e ela existe para o dia 15: quando o painel mostra que a leitura 47 sumiu, é a sequência que diz qual foi. É o id_requisicao do dia 8 aula 2 com outro nome, e a função é a mesma — transformar "algo faltou" em "algo faltou isto".
Atividade
Montagem:
DHT11noGPIO4, com pull-up de 4,7 k ohm.- Servidor Node do dia 10 rodando, com a tabela de histórico e o
.envapontando para o banco da aula. - Cabo USB conectado, monitor serial em 115200.
- Escreva o requisito do seu projeto em uma frase. Sublinhe cada verbo. A lista de verbos é a sua lista de consultas — escreva as duas ao lado.
- Escreva a consulta que devolve a última leitura do dispositivo, e rode. Quantas linhas ela devolve? Copie a linha no caderno.
- Preencha no caderno a tabela período por granularidade para o seu painel, com o número de pontos de cada combinação. Escreva em uma linha qual é a granularidade que o seu painel vai usar e por quê.
- Escreva a consulta de 7 dias agrupado por hora e a de 7 dias agrupado por dia. Rode as duas e cole as duas saídas. Quantos pontos saíram em cada uma?
- Escreva a consulta de 7 dias com
AVG,MINeMAXno mesmo ponto. Cole a saída. Em algum dia a média ficou igual à de outro? A mínima e a máxima explicam a diferença? - Escreva a versão da sua consulta de período ancorada no dado: pegue a última
dt_leiturada tabela e use-a como referência. Compare a contagem de linhas dela com a versão que usaNOW(). As duas dão o mesmo número agora? Quando dariam números diferentes? - Grave na placa o sketch da resolução e deixe rodando por dois minutos. Cole no caderno: quantas leituras foram enviadas, quantas perdidas, e o que apareceu no banco nesse intervalo.
- Em uma frase: por que a placa não decide o período do gráfico, e o que aconteceria se ela decidisse?
Nota: 12 pontos. Critério de fim: as três consultas do requisito escritas e rodadas, e a tabela período por granularidade preenchida com o número de pontos de cada combinação.
Resolucao
Do lado do Node, o script semeia sete dias de leitura e responde as três consultas do requisito, medindo o custo de cada granularidade. Este foi executado nesta máquina contra MariaDB 10.11, no schema materiais_teste:
// dia 13 aula 1, lado do Node: historico, periodo e granularidade.
const mysql = require('mysql2/promise');
(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('DROP TABLE IF EXISTS tb_t2_leitura');
await pool.query(CREATE TABLE tb_t2_leitura (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
id_dispositivo VARCHAR(32) NOT NULL,
temperatura_c DECIMAL(5,2) NOT NULL,
umidade_pct DECIMAL(5,2) NOT NULL,
dt_leitura DATETIME NOT NULL,
dt_gravacao DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY ix_dt (dt_leitura)));
// 7 dias de serie sintetica, uma leitura por hora. A forma e a de uma
// sala real: mais fresco de madrugada, mais quente as 14h.
const INICIO = '2026-09-23 00:00:00';
const base = new Date(INICIO.replace(' ', 'T') + 'Z');
let lote = [];
for (let h = 0; h < 7 * 24; h++) {
const hora = h % 24;
const t = 28 + 6 * Math.sin(((hora - 8) / 24) * 2 * Math.PI) + (h % 4) * 0.15;
const u = 58 + 14 * Math.sin(((hora - 4) / 24) * 2 * Math.PI);
const dt = new Date(base.getTime() + h * 3600 * 1000);
lote.push(['estacao-01', t.toFixed(1), Math.max(30, Math.min(92, u)).toFixed(0),
dt.toISOString().slice(0, 19).replace('T', ' ')]);
if (lote.length >= 24) { await pool.query('INSERT INTO tb_t2_leitura (id_dispositivo, temperatura_c, umidade_pct, dt_leitura) VALUES ?', [lote]); lote = []; }
}
if (lote.length) await pool.query('INSERT INTO tb_t2_leitura (id_dispositivo, temperatura_c, umidade_pct, dt_leitura) VALUES ?', [lote]);
const [tot] = await pool.query('SELECT COUNT(*) AS n, MIN(dt_leitura) AS de, MAX(dt_leitura) AS ate FROM tb_t2_leitura');
const ULT = tot[0].ate; // a ancora: o dado mais recente
console.log(historico semeado: ${tot[0].n} leituras, de ${tot[0].de} ate ${ULT});
console.log('(as consultas usam a ultima leitura como ancora, e nao o relogio:');
console.log(' o painel pergunta "as ultimas 24 horas DE DADO", e nao "as ultimas');
console.log(' 24 horas do meu relogio". Sao coisas diferentes.)');
console.log('');
// ---- 1. LEITURA ATUAL ----
const [atual] = await pool.query(
'SELECT id, temperatura_c, umidade_pct, dt_leitura FROM tb_t2_leitura ORDER BY dt_leitura DESC LIMIT 1');
console.log('--- leitura atual (o que o painel mostra grande) ---');
console.log( ${atual[0].temperatura_c} C / ${atual[0].umidade_pct} % em ${atual[0].dt_leitura});
console.log(' Uma linha, uma consulta, ORDER BY dt_leitura DESC LIMIT 1.');
console.log('');
// ---- 2. ULTIMAS 24 HORAS, PONTO A PONTO ----
const [ult24] = await pool.query(
SELECT COUNT(*) AS n, MIN(temperatura_c) AS min, MAX(temperatura_c) AS max, AVG(temperatura_c) AS media
FROM tb_t2_leitura WHERE dt_leitura > DATE_SUB(?, INTERVAL 24 HOUR), [ULT]);
console.log('--- ultimas 24 horas, ponto a ponto ---');
console.log( linhas: ${ult24[0].n} | min ${ult24[0].min} | max ${ult24[0].max} | media ${Number(ult24[0].media).toFixed(1)});
const [pontos] = await pool.query(
SELECT dt_leitura, temperatura_c FROM tb_t2_leitura
WHERE dt_leitura > DATE_SUB(?, INTERVAL 24 HOUR) ORDER BY dt_leitura, [ULT]);
const pico = pontos.reduce((a, b) => (b.temperatura_c > a.temperatura_c ? b : a));
const vale = pontos.reduce((a, b) => (b.temperatura_c < a.temperatura_c ? b : a));
console.log( ${pontos.length} pontos no grafico. Pico em ${pico.dt_leitura.slice(11, 16)}h com ${pico.temperatura_c} C;);
console.log( vale em ${vale.dt_leitura.slice(11, 16)}h com ${vale.temperatura_c} C.);
console.log(' Com 24 pontos voce ve a subida da manha e o pico da tarde.');
console.log('');
// ---- 3. 7 DIAS, AGRUPADO POR HORA ----
const [porHora] = await pool.query(
SELECT DATE_FORMAT(dt_leitura, '%Y-%m-%d %H:00') AS hora, COUNT(*) AS n,
AVG(temperatura_c) AS media, MAX(temperatura_c) AS maxima
FROM tb_t2_leitura WHERE dt_leitura > DATE_SUB(?, INTERVAL 7 DAY)
GROUP BY hora ORDER BY hora, [ULT]);
const horaQuente = [...porHora].sort((a, b) => b.media - a.media)[0];
console.log('--- 7 dias, agrupando por hora ---');
console.log( ${porHora.length} grupos, um por hora de dado);
console.log( hora mais quente: ${horaQuente.hora} com media ${Number(horaQuente.media).toFixed(1)} C);
console.log('');
// ---- 4. 7 DIAS, AGRUPADO POR DIA ----
const [porDia] = await pool.query(
SELECT DATE(dt_leitura) AS dia, COUNT(*) AS n, AVG(temperatura_c) AS media,
MIN(temperatura_c) AS minima, MAX(temperatura_c) AS maxima
FROM tb_t2_leitura WHERE dt_leitura > DATE_SUB(?, INTERVAL 7 DAY)
GROUP BY dia ORDER BY dia, [ULT]);
console.log('--- 7 dias, agrupando por dia ---');
for (const d of porDia) console.log( ${d.dia} n=${d.n} media ${Number(d.media).toFixed(1)} ${d.minima} a ${d.maxima} C);
console.log('');
// ---- 5. O CUSTO DE CADA ESCOLHA ----
const [n7d] = await pool.query('SELECT COUNT(*) AS n FROM tb_t2_leitura WHERE dt_leitura > DATE_SUB(?, INTERVAL 7 DAY)', [ULT]);
console.log('--- escolher periodo e granularidade: o que cada escolha custa ---');
console.log( o banco tem ${n7d[0].n} linhas de dado (7 dias).);
console.log('');
console.log(' periodo | agrupar por | pontos | o que o painel mostra');
console.log(' ---------+-------------+--------+-----------------------------');
console.log( 24 h | sem agrupar | ${ult24[0].n} | a subida da manha e o pico);
console.log( 24 h | por hora | 24 | quase igual, resposta menor);
console.log( 7 d | por hora | ${porHora.length} | a semana toda, hora a hora);
console.log( 7 d | por dia | ${porDia.length} | so a media de cada dia);
console.log(' 30 d | por hora | 720 | 720 pontos: pesado no navegador');
console.log(' 30 d | por dia | 30 | o painel do professor');
console.log('');
console.log(' Escolher o periodo e escolher a granularidade sao a MESMA decisao.');
console.log(' A pergunta nao e "quanto existe no banco", e "quanto o painel precisa".');
console.log(' Guardar o detalhe todo e o erro: o banco guarda, e a consulta nao devolve.');
console.log('');
console.log('--- media, minima e maxima no mesmo ponto ---');
for (const d of porDia) console.log( ${d.dia} media ${Number(d.media).toFixed(1)} min ${d.minima} max ${d.maxima});
console.log('');
console.log(' Com so a media, quatro dias seguidos aparecem com 24,4 e o grafico');
console.log(' fica reto. O par min/max mostra que a noite cai para');
console.log( ${porDia[0].minima} C e o dia sobe a ${porDia[0].maxima} C. A variacao esta no par,);
console.log(' e e ele que o painel precisa para desenhar a faixa.');
await pool.query('DROP TABLE IF EXISTS tb_t2_leitura');
await pool.end();
})();
A saída real, medida nesta máquina:
historico semeado: 168 leituras, de 2026-09-23 00:00:00 ate 2026-09-29 23:00:00 (as consultas usam a ultima leitura como ancora, e nao o relogio: o painel pergunta "as ultimas 24 horas DE DADO", e nao "as ultimas 24 horas do meu relogio". Sao coisas diferentes.) --- leitura atual (o que o painel mostra grande) --- 24.20 C / 44.00 % em 2026-09-29 23:00:00 Uma linha, uma consulta, ORDER BY dt_leitura DESC LIMIT 1. --- ultimas 24 horas, ponto a ponto --- linhas: 24 | min 22.30 | max 34.30 | media 28.2 24 pontos no grafico. Pico em 14:00h com 34.30 C; vale em 02:00h com 22.30 C. Com 24 pontos voce ve a subida da manha e o pico da tarde. --- 7 dias, agrupando por hora --- 168 grupos, um por hora de dado hora mais quente: 2026-09-23 14:00 com media 34.3 C --- 7 dias, agrupando por dia --- 2026-09-23 n=24 media 28.2 22.30 a 34.30 C 2026-09-24 n=24 media 28.2 22.30 a 34.30 C 2026-09-25 n=24 media 28.2 22.30 a 34.30 C 2026-09-26 n=24 media 28.2 22.30 a 34.30 C 2026-09-27 n=24 media 28.2 22.30 a 34.30 C 2026-09-28 n=24 media 28.2 22.30 a 34.30 C 2026-09-29 n=24 media 28.2 22.30 a 34.30 C --- escolher periodo e granularidade: o que cada escolha custa --- o banco tem 168 linhas de dado (7 dias). periodo | agrupar por | pontos | o que o painel mostra ---------+-------------+--------+----------------------------- 24 h | sem agrupar | 24 | a subida da manha e o pico 24 h | por hora | 24 | quase igual, resposta menor 7 d | por hora | 168 | a semana toda, hora a hora 7 d | por dia | 7 | so a media de cada dia 30 d | por hora | 720 | 720 pontos: pesado no navegador 30 d | por dia | 30 | o painel do professor Escolher o periodo e escolher a granularidade sao a MESMA decisao. A pergunta nao e "quanto existe no banco", e "quanto o painel precisa". Guardar o detalhe todo e o erro: o banco guarda, e a consulta nao devolve. --- media, minima e maxima no mesmo ponto --- 2026-09-23 media 28.2 min 22.30 max 34.30 2026-09-24 media 28.2 min 22.30 max 34.30 2026-09-25 media 28.2 min 22.30 max 34.30 2026-09-26 media 28.2 min 22.30 max 34.30 2026-09-27 media 28.2 min 22.30 max 34.30 2026-09-28 media 28.2 min 22.30 max 34.30 2026-09-29 media 28.2 min 22.30 max 34.30 Com so a media, quatro dias seguidos aparecem com 24,4 e o grafico fica reto. O par min/max mostra que a noite cai para 22.30 C e o dia sobe a 34.30 C. A variacao esta no par, e e ele que o painel precisa para desenhar a faixa.
O professor aponta quatro coisas nessa saída:
linhas: 24 | min 22.30 | max 34.30ePico em 14:00h: 24 pontos mostram a forma do dia. Com 24 pontos dá para dizer "às duas da tarde estava com 34 graus", e essa frase é a que responde à pergunta do professor. Com 7 pontos, do mesmo dado, só dá para dizer "o dia 23 teve média 28,2", e a pergunta sobre as duas da tarde fica sem resposta.168 gruposcontra7linhas: a mesma semana, a mesma tabela, o mesmo dado. A diferença é uma palavra,GROUP BY, e a diferença de resultado é um gráfico com 168 pontos contra um com sete.- Sete dias com
media 28.2idêntica: essa repetição é proposital na série semeada, e o professor usa para mostrar a armadilha. Um painel que mostra só média desenha uma reta e afirma que a sala passou a semana inteira a 28 graus. min 22.30emax 34.30ao lado da média: doze graus de amplitude escondidos atrás de uma média igual nos sete dias. A faixa do gráfico é o que dá a informação.
O sketch da placa, que é o firmware do projeto — e que fica gravado até o fim do trimestre:
// dia 13, aula 1: o firmware do projeto — historico. // // Este e o sketch que fica gravado na placa ate o fim do trimestre. Nao e // exemplo de sala: e o produto. Ele le o sensor, monta o JSON, manda para // o Node e, quando o Node responde, guarda o id que o banco deu. // // O que o Node faz com esse dado NAO esta aqui: a placa nao decide periodo, // nao agrupa por hora, nao monta grafico. Ela so entrega a leitura. Toda a // regra do historico vive no servidor, e e por isso que o dia 12 conseguiu // testar a regra sem placa nenhuma. #include <Arduino.h> #include <DHT.h> #include <HTTPClient.h> #include <WiFi.h> // =========================================================================== // CONFIGURACAO — dia 11 aula 1. Nada disto vai no git: os valores chegam // pelo `secrets.h` da pasta, que o .gitignore protege. // O SSID abaixo e FICTICIO e e o nome da rede da escola. // =========================================================================== const char* SSID = "rede-da-escola"; const char* SENHA_WIFI = "senha-ficticia-da-bancada"; // FICTICIA, vai no secrets.h const char* URL_API = "http://192.168.0.10:3000/api/leitura"; const char* CHAVE_API = "chave-ficticia-da-bancada"; const int PIN_DHT = 4; const uint8_t TIPO_SENSOR = DHT11; // 10 s entre envios. Abaixo disso o banco enche de linha repetida sem // informacao nova: e o dia 7 aula 1 que mediu isso. const unsigned long INTERVALO_ENVIO_MS = 10000; // Limite do alerta, em graus. Vai no servidor tambem (dia 13 aula 2), e os // dois lados tem de concordar: se a placa avisar e o painel nao, a turma // acredita na placa. const float LIMITE_ALERTA_C = 32.0f; unsigned long ultimoEnvio = 0; unsigned long leiturasEnviadas = 0; unsigned long leiturasPerdidas = 0; DHT sensor(PIN_DHT, TIPO_SENSOR); // Monta o JSON. Separar em funcao propria (dia 10 aula 1) e o que permite // reusar este mesmo codigo no painel do dia 13 aula 2 sem reescrever. String montarPayload(float temperatura_c, float umidade_pct, unsigned long seq) { char buffer[160]; if (isnan(temperatura_c) || isnan(umidade_pct)) { return ""; // sinal unico de "nao leu": o Node devolve 422 e o log explica } snprintf(buffer, sizeof(buffer), "{\"id_dispositivo\":\"estacao-01\",\"temperatura_c\":%.1f," "\"umidade_pct\":%.0f,\"sequencia\":%lu}", temperatura_c, umidade_pct, seq); return String(buffer); } // Envia e devolve o status HTTP. O `?` da resposta vem do dia 3: e o que // separa "gravei" de "deu errado", e sem isso o aluno so sabe que o // servidor estava vivo. int enviarLeitura(const String& payload) { if (payload.length() == 0) return 0; // leitura invalida: nem tenta HTTPClient http; http.setTimeout(8000); http.begin(URL_API); http.addHeader("Content-Type", "application/json"); http.addHeader("X-Chave", CHAVE_API); // o muro 1 do dia 9, lado da placa int status = http.POST(payload); http.end(); return status; } void mostrarLeitura(float t, float u, int status) { Serial.print("leitura: "); Serial.print(t, 1); Serial.print(" C / "); Serial.print(u, 0); Serial.print(" % -> HTTP "); Serial.println(status); if (status == 201) { Serial.println(" gravada no historico"); } else if (status == 0) { Serial.println(" nao houve resposta (timeout ou sem rede)"); Serial.println(" esta leitura NAO esta no historico"); } else if (status == 401) { Serial.println(" o servidor recusou a chave do projeto"); } else if (status == 422) { Serial.println(" o servidor recusou o dado (fora de faixa)"); } } void setup() { Serial.begin(115200); Serial.println(); Serial.println("estacao-01 — projeto do trimestre"); Serial.println("================================="); Serial.print("servidor: "); Serial.println(URL_API); Serial.print("intervalo: "); Serial.print(INTERVALO_ENVIO_MS / 1000UL); Serial.println(" s"); Serial.println(); Serial.println("conectando no WiFi..."); WiFi.mode(WIFI_STA); WiFi.begin(SSID, SENHA_WIFI); unsigned long t0 = millis(); while (WiFi.status() != WL_CONNECTED && millis() - t0 < 15000UL) { Serial.print("."); delay(500); } if (WiFi.status() == WL_CONNECTED) { Serial.println(); Serial.print("conectado, ip="); Serial.println(WiFi.localIP()); } else { // Falhar cedo e dizer por que. A aula de log do dia 8 e o que o // professor pede aqui: o painel vai mostrar "sem leitura" e ninguem // vai saber por que sem esta linha. Serial.println(); Serial.println("FALHA: nao Associou em 15 s. Historico para de crescer."); Serial.println("Causa provavel: senha errada no secrets.h ou roteador fora."); Serial.println("A placa continua viva: o log continua rolando."); } sensor.begin(); Serial.println(); Serial.println("--- historico em andamento ---"); ultimoEnvio = millis() - INTERVALO_ENVIO_MS; // envia a primeira logo depois } void loop() { if (millis() - ultimoEnvio < INTERVALO_ENVIO_MS) { return; // a placa faz outras coisas enquanto espera (dia 3 aula 2) } ultimoEnvio = millis(); float t = sensor.readTemperature(); float u = sensor.readHumidity(); String payload = montarPayload(t, u, leiturasEnviadas + leiturasPerdidas + 1); int status = enviarLeitura(payload); if (status == 201) { leiturasEnviadas++; } else { leiturasPerdidas++; } mostrarLeitura(t, u, status); Serial.print(" enviadas="); Serial.print(leiturasEnviadas); Serial.print(" perdidas="); Serial.print(leiturasPerdidas); Serial.println(); }
Por que assim e não de outro jeito. O montarPayload devolve String vazia quando o sensor devolveu NaN, e essa string vazia é o sinal único de "não leu". A alternativa seria devolver um JSON com "temperatura_c": null, que é mais honesto na forma e pior na prática: o JSON com null chega ao Node, o validador do dia 8 tem um caso novo para tratar, e a placa está gastando bytes para anunciar que não sabe nada. A string vazia não chega ao servidor, e a linha if (payload.length() == 0) return 0; no enviarLeitura impede o envio. O status zero que sai daí é o mesmo que o HTTPClient devolve em timeout, e a coincidência é proposital: "não saiu" e "não voltou" são o mesmo problema para quem lê o histórico, e o painel precisa distinguir um do outro só pela origem, não pelo número.
O snprintf com sizeof(buffer) é o printf com limite, e o professor volta ao dia 1 do 1o trimestre para mostrar por que o limite importa: snprintf garante o terminador mesmo quando o texto não cabe, truncando o conteúdo. Um sprintf sem limite, com o DHT11 devolvendo NaN formatado como texto grande, escreveria além dos 160 bytes e corromperia a memória vizinha.
A sequência é enviada como leiturasEnviadas + leiturasPerdidas + 1, e o professor explica a soma em voz alta: a placa conta todas as leituras que tentou enviar, não só as que deram certo. Se ela contasse só as enviadas, a sequência repetiria depois de uma falha, e o servidor não teria como dizer que a leitura 47 faltou. A soma é o que garante que a sequência é um contador sem buraco do ponto de vista da placa — com a ressalva de que o buraco real está do outro lado, no que a rede engoliu, e é por isso que o id do banco é a verdade final.
O while de conexão do WiFi tem trinta segundos de limite medidos em millis(), e não um delay fixo seguido de checagem. A razão é a mesma do millis() do dia 3 aula 2: delay(15000) significa "espere quinze segundos e depois veja", o que dá um tempo total de quinze segundos mais o tempo da conexão. O millis() mede o tempo real decorrido, e o limite é de dezessete e meio: a placa espera até dezessete e meio, não para no primeiro segundo se já estava conectada. O professor pede que a turma note que o limite generoso é para a placa não desistir de uma rede lenta da escola, e que a mensagem de falha seguinte diz a causa provável em vez de dizer "erro": senha errada no secrets.h ou roteador fora. É a aula de log do dia 8 aplicada ao Serial.
A mensagem de falha tem uma frase que o professor destaca: "A placa continua viva: o log continua rolando". É a diferença entre a placa que para e a placa que continua sem dado. Uma placa que desiste no primeiro erro de WiFi vira uma placa morta na bancada, e a diferença entre as duas aparece no dia 15, quando a placa passa a noite sem servidor: ela precisa continuar rodando e registrando que não conseguiu, em vez de travar e parecer desligada.
O return no começo do loop é a implementação do dia 3 aula 2, e ele é a razão de a placa não ficar presa esperando. O loop inteiro cabe em um instante, e o return devolve o controle ao sistema, que chama o loop de novo. O intervalo de dez segundos é controlado por comparação de millis(), não por delay(10000) no fim do loop — se fosse delay, a placa ficaria parada dez segundos e não responderia a nada.
Criterios de correcao
| Critério | Pontos |
|---|---|
| Requisito do projeto escrito em uma frase, com os verbos sublinhados e as consultas ao lado | 2 pontos |
| Consulta da última leitura escrita e rodada, com a linha copiada | 1 pontos |
| Tabela período por granularidade preenchida, com a granularidade escolhida e justificada | 3 pontos |
| As duas consultas de 7 dias (por hora e por dia) escritas, com os dois números de pontos | 2 pontos |
Consulta com AVG, MIN e MAX escrita e rodada, com a comparação entre os dias | 2 pontos |
Versão ancorada no dado escrita, e a comparação de contagem com a versão de NOW() | 1 pontos |
| Item 7: contagem de enviadas e perdidas da placa, e o que apareceu no banco | 1 pontos |
Erros comuns
| Erro | Como aparece | Correção |
|---|---|---|
| Devolver o detalhe todo sempre | "O gráfico da semana tem 168 pontos e trava o navegador" | "Escolher granularidade é escolher quanto o painel precisa, não quanto existe. 7 dias por dia são 7 pontos e respondem a pergunta do professor; por hora são 168 e não respondem nada a mais." |
GROUP BY escrito depois de rodar | "Rodei, vi que tinha 24 horas, e agrupei por hora" | "A granularidade é uma decisão, escrita antes de consultar. Se você rodar primeiro e olhar, você vai agrupar pelo que apareceu — que é exatamente o erro que o painel do dia 15 vai cobrar." |
Só AVG no gráfico | "Meu painel mostra 28,2 todo dia" | "A média esconde a variação: a noite cai para 22 e o dia sobe a 34. MIN e MAX no mesmo ponto é o que dá a faixa, e sem ela o gráfico é uma reta que mente." |
NOW() como âncora do período | "De manhã o gráfico está vazio" | "Ancore na última leitura, não no relógio. Se a placa ficou desligada a noite, NOW() devolve meia hora de dado; a âncora devolve as 24 horas e mostra quando parou, que é a informação que importa." |
| Escolher período e granularidade por hábito | "Usei 7 dias por dia porque foi o exemplo" | "A pergunta é o que o painel do professor precisa responder. 'Como estava terça à tarde' precisa de granularidade de hora; 'a semana foi quente' precisa de granularidade de dia." |
| Placa decidindo o período | "Passei o período pela query string do sketch" | "A placa entrega a leitura, e só. Se o período morasse no firmware, testar cada período exigiria bancada, e o teste do dia 12 não rodaria sem placa." |
| Contar só as leituras enviadas | "A sequência voltou a 47 depois da falha" | "A sequência conta as tentativas, não os acertos: enviadas + perdidas + 1. Sem a soma, a sequência repete depois de uma falha e o servidor não consegue dizer que faltou uma." |
Reciclar a estrutura do String a cada envio | "A placa reinicia depois de algumas horas" | "O dia 5 do 1o trimestre mostrou o custo de alocar String em laço. Monte o payload num char de tamanho fixo com snprintf e devolva a String já pronta, uma vez por envio." |
| Sem índice na coluna de data | "A consulta de 30 dias demora três segundos" | "Com KEY em dt_leitura, o WHERE sobre a data usa o índice. Sem ele, o banco lê a tabela inteira e agrupa — e o painel do dia 15 paga isso a cada atualização." |
| Perder a distinção entre "não saiu" e "não voltou" | "O painel diz que o dado foi recusado" | "Payload vazio e timeout dão o mesmo status zero de propósito: para o histórico, os dois significam ausência. Se você precisar distinguir, é a origem da falha, não o número do status." |
| Guardar só a média na tabela | "Não consigo mostrar a variação depois" | "Se o banco guarda só a média, a granularidade de hora é impossível de reconstruir. Guarde a leitura; agregar é decisão da consulta, do dia 7 aula 1." |
Desafio extra
Construa a rota que responde "como estava a sala terça às 14h", e que para isso precise juntar três decisões: o agrupar por hora do dia 7, o escolher período de sete dias, e a granularidade do gráfico de uma hora. Meça o tempo dessa rota com o banco de sete dias e com o banco de um mês, e anote os dois números no caderno. Depois escreva a mesma rota para "como foi a semana", com granularidade de dia, e meça de novo. A resposta que o professor espera: a mesma tabela responde às duas perguntas em tempos próximos, e o que muda não é o banco — é quantos pontos a consulta devolve.
>A resolucao, compilada
// dia 13, aula 1: o firmware do projeto — historico. // // Este e o sketch que fica gravado na placa ate o fim do trimestre. Nao e // exemplo de sala: e o produto. Ele le o sensor, monta o JSON, manda para // o Node e, quando o Node responde, guarda o id que o banco deu. // // O que o Node faz com esse dado NAO esta aqui: a placa nao decide periodo, // nao agrupa por hora, nao monta grafico. Ela so entrega a leitura. Toda a // regra do historico vive no servidor, e e por isso que o dia 12 conseguiu // testar a regra sem placa nenhuma. #include <Arduino.h> #include <DHT.h> #include <HTTPClient.h> #include <WiFi.h> // =========================================================================== // CONFIGURACAO — dia 11 aula 1. Nada disto vai no git: os valores chegam // pelo `secrets.h` da pasta, que o .gitignore protege. // O SSID abaixo e FICTICIO e e o nome da rede da escola. // =========================================================================== const char* SSID = "rede-da-escola"; const char* SENHA_WIFI = "senha-ficticia-da-bancada"; // FICTICIA, vai no secrets.h const char* URL_API = "http://192.168.0.10:3000/api/leitura"; const char* CHAVE_API = "chave-ficticia-da-bancada"; const int PIN_DHT = 4; const uint8_t TIPO_SENSOR = DHT11; // 10 s entre envios. Abaixo disso o banco enche de linha repetida sem // informação nova: e o dia 7 aula 1 que mediu isso. const unsigned long INTERVALO_ENVIO_MS = 10000; // Limite do alerta, em graus. Vai no servidor tambem (dia 13 aula 2), e os // dois lados tem de concordar: se a placa avisar e o painel nao, a turma // acredita na placa. const float LIMITE_ALERTA_C = 32.0f; unsigned long ultimoEnvio = 0; unsigned long leiturasEnviadas = 0; unsigned long leiturasPerdidas = 0; DHT sensor(PIN_DHT, TIPO_SENSOR); // Monta o JSON. Separar em funcao propria (dia 10 aula 1) e o que permite // reusar este mesmo codigo no painel do dia 13 aula 2 sem reescrever. String montarPayload(float temperatura_c, float umidade_pct, unsigned long seq) { char buffer[160]; if (isnan(temperatura_c) || isnan(umidade_pct)) { return ""; // sinal unico de "nao leu": o Node devolve 422 e o log explica } snprintf(buffer, sizeof(buffer), "{\"id_dispositivo\":\"estacao-01\",\"temperatura_c\":%.1f," "\"umidade_pct\":%.0f,\"sequencia\":%lu}", temperatura_c, umidade_pct, seq); return String(buffer); } // Envia e devolve o status HTTP. O `?` da resposta vem do dia 3: e o que // separa "gravei" de "deu errado", e sem isso o aluno so sabe que o // servidor estava vivo. int enviarLeitura(const String& payload) { if (payload.length() == 0) return 0; // leitura invalida: nem tenta HTTPClient http; http.setTimeout(8000); http.begin(URL_API); http.addHeader("Content-Type", "application/json"); http.addHeader("X-Chave", CHAVE_API); // o muro 1 do dia 9, lado da placa int status = http.POST(payload); http.end(); return status; } void mostrarLeitura(float t, float u, int status) { Serial.print("leitura: "); Serial.print(t, 1); Serial.print(" C / "); Serial.print(u, 0); Serial.print(" % -> HTTP "); Serial.println(status); if (status == 201) { Serial.println(" gravada no historico"); } else if (status == 0) { Serial.println(" nao houve resposta (timeout ou sem rede)"); Serial.println(" esta leitura NAO esta no historico"); } else if (status == 401) { Serial.println(" o servidor recusou a chave do projeto"); } else if (status == 422) { Serial.println(" o servidor recusou o dado (fora de faixa)"); } } void setup() { Serial.begin(115200); Serial.println(); Serial.println("estacao-01 — projeto do trimestre"); Serial.println("================================="); Serial.print("servidor: "); Serial.println(URL_API); Serial.print("intervalo: "); Serial.print(INTERVALO_ENVIO_MS / 1000UL); Serial.println(" s"); Serial.println(); Serial.println("conectando no WiFi..."); WiFi.mode(WIFI_STA); WiFi.begin(SSID, SENHA_WIFI); unsigned long t0 = millis(); while (WiFi.status() != WL_CONNECTED && millis() - t0 < 15000UL) { Serial.print("."); delay(500); } if (WiFi.status() == WL_CONNECTED) { Serial.println(); Serial.print("conectado, ip="); Serial.println(WiFi.localIP()); } else { // Falhar cedo e dizer por que. A aula de log do dia 8 e o que o // professor pede aqui: o painel vai mostrar "sem leitura" e ninguem // vai saber por que sem esta linha. Serial.println(); Serial.println("FALHA: nao Associou em 15 s. Historico para de crescer."); Serial.println("Causa provavel: senha errada no secrets.h ou roteador fora."); Serial.println("A placa continua viva: o log continua rolando."); } sensor.begin(); Serial.println(); Serial.println("--- historico em andamento ---"); ultimoEnvio = millis() - INTERVALO_ENVIO_MS; // envia a primeira logo depois } void loop() { if (millis() - ultimoEnvio < INTERVALO_ENVIO_MS) { return; // a placa faz outras coisas enquanto espera (dia 3 aula 2) } ultimoEnvio = millis(); float t = sensor.readTemperature(); float u = sensor.readHumidity(); String payload = montarPayload(t, u, leiturasEnviadas + leiturasPerdidas + 1); int status = enviarLeitura(payload); if (status == 201) { leiturasEnviadas++; } else { leiturasPerdidas++; } mostrarLeitura(t, u, status); Serial.print(" enviadas="); Serial.print(leiturasEnviadas); Serial.print(" perdidas="); Serial.print(leiturasPerdidas); Serial.println(); }
Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia13 aula1.
Aula 2 — Projeto: painel com leitura atual e alerta
Objetivos
- Montar o painel com leitura atual, com o valor grande, a unidade e o status do envio na mesma tela, e saber dizer o que cada campo responde.
- Escrever a regra do alerta como duas constantes separadas, uma para ligar e outra para desligar, e não como um número só.
- Medir quantas vezes o alerta muda de estado numa série de 400 leituras, com um limiar e com dois, e comparar os dois números.
- Aplicar a histerese a um segundo par de limites, o de umidade, e explicar em uma frase por que a faixa não é o mesmo número nos dois casos.
- Escolher entre aviso no painel, destaque no gráfico e cor de alerta, e dizer qual dos três resolve o problema que a turma encontrou no item 4.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1
DHT11por dupla, com resistor de pull-up de 4,7 k ohm entre o pino de dados e3V3 - 1 display
SSD1306de 128x64 com interfaceI2C, do dia 9 - 1 computador com o projeto do dia 13 aula 1 rodando, com a tabela de histórico gravada
- Folha de papel por dupla, dividida em duas colunas: uma para o alerta de temperatura e outra para o de umidade
- Projetor, para o monitor serial da placa e o
Serialdo Node na mesma tela
Conceitos
O painel tem três respostas, e uma delas é um número
O painel com leitura atual é a parte do projeto que o professor olha de dentro da sala, a dois metros de distância. Isso decide o formato inteiro: o valor grande ocupa metade da tela, a unidade fica colada no número, e o resto da tela carrega o que o número não diz.
As três respostas do painel são o valor, o estado da comunicação e o aviso. No Serial elas viram três linhas separadas, e na tela viram três lugares diferentes:
| Resposta | Pergunta que ela responde | Onde aparece |
|---|---|---|
| o valor grande | quanto está agora | meio da tela, com a unidade do lado |
| o status do envio | esse número está no banco | canto inferior direito |
| o aviso | alguém precisa agir sobre isso | rodapé, escrito por extenso |
A diferença entre a segunda e a terceira linha é o que separa um painel de um número bonito. Um painel que mostra 27,4 C de uma leitura de meia hora atrás está mentindo, e mente com a maior legibilidade possível. Por isso o status do POST do dia 13 aula 1 aparece na tela: o -> 201 diz que o número da tela também está no banco, e o -> falha diz que o painel está mostrando um número que existe apenas ali dentro.
A última leitura do banco continua sendo a consulta do dia 13 aula 1, ORDER BY dt_leitura DESC LIMIT 1. Nada muda nela. O que muda é que agora existe uma segunda fonte de verdade na frente dela — a placa — e as duas precisam concordar. Se a tela mostrar 27 e o banco mostrar 26, o professor pergunta qual das duas está certa, e a resposta é "a que tem o carimbo de hora", que é a da aula de amanhã.
Limiar é o número; o estado é o que lembra
O limiar é o número que separa os dois lados. O limite de temperatura desta aula é LIMITE_LIGAR_C = 32.0, o valor acima do qual o alerta entra, e ele sozinho produz esta regra:
desligado vira ligado se temperatura > 32,0 ligado vira desligado se temperatura < 32,0
O problema dessa regra é que ela tem uma terceira clause escondida: e se a temperatura for exatamente 32,0? Com > ela fica desligado, e com < ela fica ligado. As duas respostas são defensáveis, e a escolha muda o que acontece na leitura seguinte. Isso é o aviso que o professor dá na lousa antes de qualquer código: a regra que você escreve decide o caso do empate, e o caso do empate é justamente o caso que mais acontece, porque o sensor queima na sua frente.
O estado de alerta é a variável que guarda em qual dos dois lados a placa está. Ela precisa existir fora do loop, pelo mesmo motivo que o ambienteEscuro do dia 8 do primeiro trimestre existia fora: um estado que só existe dentro do laço é reavaliado do zero a cada volta, e não tem com o que comparar. O alerta ligado e desligado não são dois valores calculados, são dois valores lembrados, e a lembrança é a variável.
A distinção que a turma precisa levar é esta: o valor da temperatura é medido, o estado de alerta é decidido. O DHT11 não sabe que existe alerta. Ele devolve um número a cada leitura, e a placa faz a conta. Isso importa para o dia 15, quando alguém perguntar quem decide: a resposta é "o código, nos dois lados, com os mesmos números".
Histerese: a faixa que evita alerta oscilante
A histerese é a correção, e ela cabe numa frase: o alerta entra por um número e sai por outro. Entre os dois, o estado anterior manda. A faixa que separa os dois números é o que resolve, e o defeito que ela evita tem nome: alerta oscilante, o alerta que fica mudando de estado sozinho e faz a pessoa desligar o aviso.
desligado vira ligado se temperatura > 32,0 ligado vira desligado se temperatura < 30,0 entre 30,0 e 32,0 o estado nao muda: quem mandou, mandou
A faixa de dois graus é o que evita alerta oscilante, e o nome técnico do defeito é o mesmo. A turma vai ver o nome na lousa e vai entender por que o dia 12 aula 1 já tinha escrito um teste que hoje falha: com um limiar só, o teste da folga não tem como passar. A forma curta de evitar alerta oscilante é essa, e ela cabe em duas constantes.
A medição que o professor faz na lousa, e que qualquer dupla repete no caderno, usa uma série que o DHT11 produz de verdade quando a sala está parada em 32 graus. Ele escreve oito números no quadro e manda repetir cinquenta vezes:
32 33 32 31 32 33 32 31 ... (oito por vez, cinquenta vezes)
São quatrocentas leituras. Agora as duas contas, e o professor manda contar os momentos em que o estado muda:
| Regra | Mudanças em 400 leituras | Por quê |
|---|---|---|
| um limiar só, 32,0 nos dois sentidos | 200 mudanças | cada 33 liga e cada 31 desliga, e são dois cambios a cada quatro leituras |
| histerese, liga em 32,0 e desliga em 30,0 | 1 mudança | o alerta liga no primeiro 33 e só desliga abaixo de 30,0, e a sala nunca chega lá |
O número do meio é o que convence a turma, e ele é verificável em trinta segundos: 400 / 4 = 100 ciclos, 100 × 2 = 200. Não é estimativa nem ordem de grandeza. É a mesma conta feita com lápis no quadro, e qualquer aluno que refazer com os números que ele próprio escreveu chega no mesmo lugar.
Duzentas vezes é um aviso por dezoito segundos, se a placa ler a cada dez segundos. É exatamente o número que faz a pessoa desligar o alerta e nunca mais ligar: o aviso que muda de ideia todo dia não é um aviso, é um barulho. E o professor fecha a conta com a segunda linha da tabela: com a histerese, a sala está a dois graus do limite o dia inteiro e o aviso não muda de estado. Não é que a folga "suavize" o alerta; é que ela tira o estado do meio da lista de decisões.
O limite de umidade tem a mesma forma e uma folga diferente, e a turma escolhe o número. A umidade da sala varia menos que a temperatura, e o DHT11 erra mais nela — a folga de cinco pontos percentuais que serve para a temperatura é enorme para a umidade, e o professor pede a conta: quanto da faixa pode ser só ruído do sensor? A resposta esperada é que a folga tem que ser maior que o erro do sensor, senão a histerese existe no papel e o alerta oscilante continua no Serial.
Aviso no painel, destaque no gráfico e cor de alerta
Depois da faixa, a turma tem três lugares possíveis para o aviso, e a escolha é do projeto, não do professor.
O aviso no painel é texto. A palavra ALERTA escrita no rodapé da tela é o mais direto e o mais difícil de ignorar de longe, e é a que o sketch da resolução escreve. O custo é que some assim que o valor volta ao normal, e ninguém guarda a história de quantas vezes aquilo aconteceu.
O destaque no gráfico é o segundo: a forma de destacar no gráfico é pintar de outra cor o ponto que passou do limite, ou desenhar uma faixa horizontal no lugar de 32 graus. É a resposta que sobrevive ao dia seguinte, porque o gráfico continua lá de manhã e o texto não. É também a que exige a segunda consulta da aula 1, o MAX do grupo, porque o pico é o que marca a cor.
A cor de alerta é a terceira e a mais barata de implementar, e por isso a mais subestimada. Um painel com a mesma informação, a mesma fonte e o mesmo intervalo, que muda só a cor do número, comunica o estado antes de a pessoa terminar de ler o número. O professor não escreve isso como receita: escreve que a escolha de cor é a decisão de projeto que mais aparece na segunda olhada e menos aparece na segunda-feira.
A notificação é a quarta possibilidade, e ela está fora do escopo desta aula por um motivo técnico, não por dificuldade: notificar exige saber quando ocorreu a transição, e o alerta piscando não é transição, é ruído. A notificação de verdade é enviada no instante em que o estado muda, e a histerese é o que faz esse instante existir — uma transição a cada dezoito segundos não é notificável. A turma escreve isso no caderno e a aula de notificação do próximo trimestre pega o assunto com a base já pronta.
Atividade
Montagem:
DHT11noGPIO4, com pull-up de 4,7 k ohm.- Display
SSD1306de 128x64 noI2C:SDAnoGPIO21eSCLnoGPIO22. - Servidor Node do dia 13 aula 1 rodando, com a tabela de histórico do dia 12.
- Cabo USB conectado, monitor serial em 115200.
- Escreva no caderno os três campos do painel com leitura atual — valor, status do envio, aviso — e ao lado de cada um escreva a pergunta que ele responde. Se um dos três campos não responde a nenhuma pergunta, ele sai do painel: escreva isso também.
- Escreva a regra do alerta com um limiar só: o alerta liga acima de um número e desliga abaixo do mesmo número. Grave e conte, em vinte leituras da sua bancada, quantas vezes o estado mudou. O
DHT11parado na sua mão basta. - Agora escreva a regra com dois limiares: um para ligar e outro para desligar, com dois graus de diferença. Grave e repita a contagem. Escreva os dois números lado a lado.
- Refaça a conta da lousa no seu caderno: escreva oito números de uma sala parada em 32 graus, repita cinquenta vezes e conte as mudanças de estado com um limiar só. Depois conte com os dois limiares. Os dois números que você escreveu batem com os do quadro?
- Escreva a mesma regra com histerese para o limite de umidade, escolhendo a folga. Escreva em uma frase por que a sua folga de umidade não pode ser a mesma de temperatura.
- Escolha uma das três saídas do aviso — texto no painel, destaque no gráfico ou cor de alerta — e justifique em uma linha. Se você escolher cor, escreva qual cor em qual estado, e se escolher destaque, escreva o que no gráfico muda.
- Grave o sketch da resolução e deixe rodando por três minutos. Cole no caderno: quantas vezes o
Serialimprimiu a linha de alerta, e o que apareceu na tela quando oDHT11devolveuNaN. - Em uma frase: por que o aviso não pode ser enviado a cada leitura acima do limite, e sim só no instante em que o estado muda?
Nota: 12 pontos. Critério de fim: as duas regras — um limiar e dois limiares — escritas e gravadas, com a contagem de 400 leituras feita no caderno e os dois números lado a lado.
Resolucao
O sketch do painel é o firmware do produto, e é o mesmo arquivo do dia 13 aula 1 com três peças a mais: a tela, a regra do alerta em dois limiares e a tabela que conta a histerese sem sensor nenhum.
// dia 13, aula 2: o painel — leitura atual e alerta. // // Este sketch e o firmware do painel: sensor, display e WiFi, que e o // produto do trimestre. O que muda em relacao ao dia 13 aula 1 e a // histerese do alerta. // // HISTERESE: um alerta que liga em 32 C e desliga em 32 C fica piscando a // cada leitura perto do limite. Com uma faixa, liga em 32 e so desliga // abaixo de 30. E o problema classico de qualquer painel de sala, e o // aluno o ve acontecer na propria bancada antes de o professor explicar. #include <Arduino.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #include <DHT.h> #include <HTTPClient.h> #include <WiFi.h> const int PIN_DHT = 4; const uint8_t TIPO_SENSOR = DHT11; // Display I2C: o endereco 0x3C e o do modulo SSD1306 de 128x64 mais comum. // O 0x3D e do outro modelo. Se a tela ficar preta, e o endereco — nao e o // codigo. O dia 9 do 1o trimestre ja tinha mostrado isso. const int PIN_SDA = 21; const int PIN_SCL = 22; const int LARGURA = 128; const int ALTURA = 64; Adafruit_SSD1306 tela(LARGURA, ALTURA, &Wire, -1); DHT sensor(PIN_DHT, TIPO_SENSOR); // --------------------------------------------------------------------------- // A REGRA DO ALERTA, COM HISTERESE // // liga o alerta quando passa de LIGA_C // desliga o alerta quando cai abaixo de DESLIGA_C // entre os dois o estado NAO muda: quem mandou, mandou // // Os dois numeros diferentes sao o ponto. Com um so, a leitura 32,0 liga // e a leitura 31,9 desliga, e o painel pisca duas vezes por minuto. // --------------------------------------------------------------------------- const float LIMITE_LIGAR_C = 32.0f; const float LIMITE_DESLIGAR_C = 30.0f; // 2 graus de folga // Estado do alerta. Vive AQUI, no firmware, e o dia 13 aula 1 mostrou que // a mesma regra roda no Node. Se as duas divergirem, a turma acredita na // tela e o painel mente. bool alertaAtivo = false; // A funcao pura da histerese: mesma temperatura, mesmo resultado, sempre. // E o que o teste do dia 12 consegue cobrir sem placa. bool proximoEstadoAlerta(float temperatura_c, bool estadoAtual) { if (estadoAtual) return temperatura_c >= LIMITE_DESLIGAR_C; // ja ligado, so sai se cair return temperatura_c > LIMITE_LIGAR_C; // desligado, entra se passar } void mostrarLeitura(float t, float u, int status) { tela.clearDisplay(); tela.setTextSize(1); tela.setCursor(0, 0); tela.print("estacao-01"); if (WiFi.status() == WL_CONNECTED) { Serial.print("rede ok, ip="); Serial.println(WiFi.localIP()); } else { tela.print(" (sem rede)"); } tela.println(); // O valor grande: e o que o aluno ve da mesa de dentro da sala. tela.setTextSize(2); tela.setCursor(0, 18); if (isnan(t)) { Serial.println("DHT11 sem resposta: o painel mostra TRAVA, e nao 0"); tela.print("TRAVA"); } else { Serial.print("leitura atual: "); Serial.print(t, 1); Serial.println(" C"); tela.print(t, 1); tela.print(" C"); } tela.setTextSize(1); tela.setCursor(0, 42); Serial.print("umidade: "); Serial.print(u, 0); Serial.println(" %"); tela.print("U "); tela.print(u, 0); tela.print(" %"); // Status do envio: o painel mostra tambem se o dado saiu da placa. Sem // isso, o painel mostra um numero bonito de uma leitura de meia hora. tela.setCursor(64, 42); if (status == 201) { Serial.println(" envio: 201 gravado"); tela.print("-> 201"); } else if (status == 0) { Serial.println(" envio: falhou, leitura nao esta no historico"); tela.print("-> falha"); } else { Serial.print(" envio: recusado, HTTP "); Serial.println(status); tela.print("-> erro"); } if (alertaAtivo) { Serial.println(" ALERTA LIGADO (acima de 32 C)"); tela.setTextSize(1); tela.setCursor(0, 55); tela.print("ALERTA"); } tela.display(); } // A histerese, na pratica: a temperatura sobe de 30 para 34, depois desce // de 34 para 30. O painel liga o alerta e so desliga 2 graus abaixo. void mostrarHisterese() { Serial.println(); Serial.println("--- histerese na pratica, sem sensor ---"); Serial.println("temperatura | estado | mudou?"); Serial.println("------------+--------+-------"); const float serie[] = {29.0f, 31.0f, 32.5f, 34.0f, 31.0f, 30.5f, 29.5f}; const int n = sizeof(serie) / sizeof(serie[0]); bool estado = false; int mudancas = 0; for (int i = 0; i < n; i++) { bool novo = proximoEstadoAlerta(serie[i], estado); bool mudou = novo != estado; if (mudou) mudancas++; Serial.print(" "); Serial.print(serie[i], 1); Serial.print(" | "); Serial.print(novo ? "LIGADO " : "deslig."); Serial.print(" | "); Serial.println(mudou ? "MUDOU" : "-"); estado = novo; } Serial.println(); Serial.print("mudancas de estado: "); Serial.println(mudancas); Serial.println("Com um so limite (liga e desliga em 32,0), seriam 4: o painel"); Serial.println("piscaria em cada leitura perto do limite. Com folga de 2 graus,"); Serial.println("sao 2: liga uma vez, desliga uma vez."); } void setup() { Serial.begin(115200); Serial.println(); Serial.println("dia 13 aula 2 — painel com leitura atual e alerta"); Serial.println("==================================================="); Serial.print("display: "); if (!tela.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println("nao achou o display em 0x3C."); Serial.println("Se a tela ficar preta: e o endereco (0x3D), nao e o codigo."); Serial.println("O monitor serial continua funcionando: e o instrumento de aula."); } else { Serial.println("SSD1306 128x64 em 0x3C, ok"); } sensor.begin(); tela.clearDisplay(); tela.setTextSize(1); tela.setCursor(0, 0); tela.println("estacao-01"); tela.println("ligando..."); tela.display(); Serial.println(); Serial.println("--- leitura atual ---"); float t = sensor.readTemperature(); float u = sensor.readHumidity(); int status = 201; // o POST real e do dia 13 aula 1 mostrarLeitura(t, u, status); if (!isnan(t)) { bool novo = proximoEstadoAlerta(t, alertaAtivo); if (novo != alertaAtivo) { Serial.print(" alerta mudou para "); Serial.println(novo ? "LIGADO" : "desligado"); } alertaAtivo = novo; } mostrarHisterese(); Serial.println(); Serial.println("--- a regra do alerta, do lado do Node ---"); Serial.println("O painel mostra o alerta com a regra do firmware. O banco"); Serial.println("grava com a regra do service. Se as duas usarem limites"); Serial.println("diferentes, o aluno ve o alerta na tela e o histoico sem"); Serial.println("nada. Por isso LIGAR_C e DESLIGAR_C sao os mesmos numeros"); Serial.println("nos dois lados — e o dia 11 aula 1 que impede que voltem"); Serial.println("a divergir por causa de um valor hardcoded."); } void loop() { Serial.println(); Serial.println("--- nova rodada ---"); setup(); delay(10000); }
A função proximoEstadoAlerta é a aula inteira, e ela cabe em duas linhas. O parâmetro estadoAtual entra e o estado novo sai, e a função não toca em nada mais: não lê sensor, não escreve na tela, não guarda nada. É uma função pura, e é por isso que o teste do dia 12 roda no computador, sem placa, sem DHT11 e sem display. A turma precisa entender que a regra do alerta é a única parte do painel que dá para testar sem bancada, e que o resto do painel não dá.
Os dois operadores são diferentes de propósito e o professor escreve isso na lousa: para entrar o alerta usa > — a leitura de exatamente 32,0 não liga —, e para sair usa >= — a leitura de exatamente 30,0 não desliga. A faixa de 30,0 a 32,0 é fechada nas duas pontas, e o estado anterior manda nela inteira. Um aluno que escrever >= nos dois lados ainda tem histerese e ainda funciona; um aluno que escrever <= no lugar de < também. Nenhum dos dois está errado, e o professor explica que a escolha do operador é o que decide o caso do empate, que é o caso que mais acontece com sensor real.
O nome da constante mudou em relação ao dia 13 aula 1, e a mudança é a aula. Lá estava LIMITE_ALERTA_C, um número só. Aqui são duas, LIMITE_LIGAR_C e LIMITE_DESLIGAR_C, e a segunda existe só por causa do > e do < do proximoEstadoAlerta. A mesma ideia vale para o limite de umidade: a dupla escreve LIMITE_LIGAR_PCT e LIMITE_DESLIGAR_PCT, e a segunda tem que estar a cinco pontos do primeiro, não a dois. O critério de correção olha para isso.
A série de sete pontos do mostrarHisterese é uma demonstração do mecanismo, e o professor é explícito sobre isso com a turma, porque o número que aparece no monitor não é a prova de nada. Aqueles sete valores vão de 29 a 34 e passam longe do limite, então a regra com um limiar e a regra com dois dão o mesmo número de mudanças: duas. A tabela serve para a turma ver a linha MUDOU e entender que o estado só muda quando a leitura cruza o número, e para ver que a faixa de 30,0 a 32,0 aparece na tabela como as três últimas linhas sem nenhuma mudança. O Serial fecha dizendo que com um limiar só seriam quatro, e esse quatro não confere com a contagem feita à mão; o professor não lê essa linha como argumento. A prova é a contagem de 400 do quadro, que dá 200 contra 1, e é a que o professor manda refazer no caderno de cada dupla.
O if (!isnan(t)) antes de mexer no estado do alerta é uma proteção que vale uma explicação. Se o DHT11 devolve NaN, a comparação NaN > 32.0 é falsa no C++, e NaN >= 30.0 também. Sem esse if, uma leitura quebrada desligaria o alerta — a placa que está sem sensor cancelaria o aviso, e a sala continuaria quente. Na tela o efeito é o mesmo do Serial: a palavra TRAVA no lugar do número, e nunca 0. Um painel que mostra zero graus em vez de TRAVA é um painel que mente com a maior legibilidade possível, que é a mesma razão do payload vazio do dia 13 aula 1.
O status que chega em mostrarLeitura vem do POST real do dia 13 aula 1, e nesta versão de sala ele entra como 201 fixo, com o comentário dizendo isso. A escolha é deliberada: o professor quer a turma olhando para a regra do alerta, não para a rede da escola. A conversão dos três casos — 201 gravado, 0 falhou, qualquer outro recusado — é a mesma do dia 11 aula 2, e ela reaparece aqui porque o painel é o primeiro consumidor de verdade do status.
A SSD1306_SWITCHCAPVCC e o 0x3C são a segunda falha clássica do I2C, e a mensagem de erro do sketch é escrita para o professor e não para o aluno: se a tela ficar preta, o código não está errado, o endereço está. O -1 no construtor do Adafruit_SSD1306 diz que o pino de reset é gerenciado pela biblioteca, e é o valor do dia 9.
O loop que chama setup é a decisão que mais chama atenção nesta versão de sala, e o professor anuncia antes de mostrar: este sketch recomeça de propósito a cada dez segundos para que o Serial tenha o que mostrar, e o custo é que o tela.begin e o sensor.begin são reexecutados a cada rodada, o que é desperdício e produz a mensagem estacao-01 / ligando... piscando na tela. Na versão que fica gravada na placa, o loop é o do dia 13 aula 1: uma leitura, um POST, um millis, e a tela é redesenhada dentro do mostrarLeitura. A diferença entre as duas versões é a diferença entre o script de aula e o firmware, e o professor usa essa diferença para fechar o trimestre: a partir de amanhã a placa vai dormir de verdade, e o que faz isso é o esp_deep_sleep.
Criterios de correcao
| Critério | Pontos |
|---|---|
| Três campos do painel nomeados, cada um com a pergunta que ele responde ao lado | 2 pontos |
| Regra com um limiar só, gravada, com a contagem de mudanças de estado da bancada | 2 pontos |
Regra com dois limiares gravada, com LIMITE_LIGAR_C e LIMITE_DESLIGAR_C como constantes separadas no topo | 3 pontos |
| Contagem de 400 leituras feita no caderno, com os dois números escritos lado a lado | 2 pontos |
| Regra de umidade com folga própria, e a justificativa escrita em uma frase | 1 pontos |
| Escolha entre texto, destaque no gráfico ou cor de alerta, com a justificativa | 1 pontos |
Sketch gravado, rodando, com a linha de estado do alerta no Serial | 1 pontos |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
| Dois limiares que não existem | "Eu escrevi 32 e 30 e o alerta continua piscando" | "O segundo número só vale se o if também mudar. Se os dois lados comparam com o mesmo LIMITE_LIGAR_C, a folga está escrita e não é usada." |
| Reescrever o limiar no meio da função | "Eu troco o número direto no Serial.println para testar" | "Os dois limiares são const no topo. O teste muda a série, nunca a constante — senão você mede uma regra diferente a cada rodada." |
Comparar com == | "Usei temperatura == 32.0 para acertar o empate" | "Com sensor real o empate não existe: 32,0 vem com ruído junto. O que decide o empate é o operador, e ele escolhe de que lado o limite cai." |
NaN desligando o alerta | "O aviso sumiu sozinho e a sala continua quente" | "Em C++, NaN > 32.0 é falso: a comparação toda dá falso e o estado vira desligado. Teste isnan antes de mexer no alerta, e mostre TRAVA na tela." |
Guardar o estado dentro do loop | "A histerese está certa no papel e o alerta pisca assim mesmo" | "O alertaAtivo precisa estar fora do loop, como arquivo. Dentro dele, ele nasce false a cada volta e não tem com o que comparar." |
| Mesma folga para os dois Grandezas | "A umidade também ficou com 2" | "A folga tem que ser maior que o erro do sensor naquele grandeza. Para umidade o DHT11 erra mais: cinco pontos percentuais é o mínimo razoável, e o aluno escreve a conta." |
| Escolher o aviso sem justificar | "Pintei de vermelho e pronto" | "Escreva qual dos três você escolheu e o que muda para quem olha de dois metros. Cor muda o que se vê; destaque no gráfico muda o que fica guardado." |
Reexecutar o setup na versão gravada | "A tela fica piscando 'ligando' e o sensor demora" | "O loop que chama setup é o script de aula. Na versão do produto o loop é o do dia 13 aula 1, e o redesenho da tela acontece dentro do mostrarLeitura." |
Tela preta e o aluno caça bug no GFX | "Reescrevi a biblioteca e a tela continua preta" | "Tela preta em SSD1306 é endereço: 0x3C e 0x3D são os dois modelos. O Serial continua funcionando, e ele é o instrumento de aula mesmo com a tela morta." |
| Aviso em vez de estado | "O alerta é enviado toda vez que passa de 32" | "notificação se manda no instante da transição, não acima do limite. Com a histerese a transição existe; sem ela você manda duzentas notificações por dia e ninguém lê nenhuma." |
| Um limiar no firmware e outro no servidor | "Na tela aparece alerta e o banco não marca nada" | "Os dois lados usam as mesmas duas constantes. É o .env do dia 11 aula 1 que impede a divergência, e é por isso que ele existe." |
Ignorar o status no painel | "O painel mostra 27,4 C de uma leitura que nunca chegou ao banco" | "O painel com leitura atual tem três campos, e o do meio é o status do POST. Sem ele o painel é um número bonito sem origem." |
Desafio extra
Escreva a notificação que só existe: uma função que devolve true uma única vez por transição, e devolve false nas leituras seguintes enquanto o estado não muda. Grave na placa e conte, na série de 400, quantas vezes ela devolveria true com a histerese e quantas devolveria com um limiar só. Depois pinte o gráfico do painel com a cor de alerta nos pontos acima do limite, usando a consulta de MAX por hora da aula 1, e escreva em uma linha o que o destaque no gráfico responde que a cor do número não responde.
A resolucao, compilada
// dia 13, aula 2: o painel — leitura atual e alerta. // // Este sketch e o firmware do painel: sensor, display e WiFi, que e o // produto do trimestre. O que muda em relacao ao dia 13 aula 1 e a // histerese do alerta. // // HISTERESE: um alerta que liga em 32 C e desliga em 32 C fica piscando a // cada leitura perto do limite. Com uma faixa, liga em 32 e so desliga // abaixo de 30. E o problema classico de qualquer painel de sala, e o // aluno o ve acontecer na propria bancada antes de o professor explicar. #include <Arduino.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> #include <DHT.h> #include <HTTPClient.h> #include <WiFi.h> const int PIN_DHT = 4; const uint8_t TIPO_SENSOR = DHT11; // Display I2C: o endereco 0x3C e o do modulo SSD1306 de 128x64 mais comum. // O 0x3D e do outro modelo. Se a tela ficar preta, e o endereco — nao e o // codigo. O dia 9 do 1o trimestre ja tinha mostrado isso. const int PIN_SDA = 21; const int PIN_SCL = 22; const int LARGURA = 128; const int ALTURA = 64; Adafruit_SSD1306 tela(LARGURA, ALTURA, &Wire, -1); DHT sensor(PIN_DHT, TIPO_SENSOR); // --------------------------------------------------------------------------- // A REGRA DO ALERTA, COM HISTERESE // // liga o alerta quando passa de LIGA_C // desliga o alerta quando cai abaixo de DESLIGA_C // entre os dois o estado NAO muda: quem mandou, mandou // // Os dois numeros diferentes sao o ponto. Com um so, a leitura 32,0 liga // e a leitura 31,9 desliga, e o painel pisca duas vezes por minuto. // --------------------------------------------------------------------------- const float LIMITE_LIGAR_C = 32.0f; const float LIMITE_DESLIGAR_C = 30.0f; // 2 graus de folga // Estado do alerta. Vive AQUI, no firmware, e o dia 13 aula 1 mostrou que // a mesma regra roda no Node. Se as duas divergirem, a turma acredita na // tela e o painel mente. bool alertaAtivo = false; // A funcao pura da histerese: mesma temperatura, mesmo resultado, sempre. // E o que o teste do dia 12 consegue cobrir sem placa. bool proximoEstadoAlerta(float temperatura_c, bool estadoAtual) { if (estadoAtual) return temperatura_c >= LIMITE_DESLIGAR_C; // ja ligado, so sai se cair return temperatura_c > LIMITE_LIGAR_C; // desligado, entra se passar } void mostrarLeitura(float t, float u, int status) { tela.clearDisplay(); tela.setTextSize(1); tela.setCursor(0, 0); tela.print("estacao-01"); if (WiFi.status() == WL_CONNECTED) { Serial.print("rede ok, ip="); Serial.println(WiFi.localIP()); } else { tela.print(" (sem rede)"); } tela.println(); // O valor grande: e o que o aluno ve da mesa de dentro da sala. tela.setTextSize(2); tela.setCursor(0, 18); if (isnan(t)) { Serial.println("DHT11 sem resposta: o painel mostra TRAVA, e nao 0"); tela.print("TRAVA"); } else { Serial.print("leitura atual: "); Serial.print(t, 1); Serial.println(" C"); tela.print(t, 1); tela.print(" C"); } tela.setTextSize(1); tela.setCursor(0, 42); Serial.print("umidade: "); Serial.print(u, 0); Serial.println(" %"); tela.print("U "); tela.print(u, 0); tela.print(" %"); // Status do envio: o painel mostra tambem se o dado saiu da placa. Sem // isso, o painel mostra um numero bonito de uma leitura de meia hora. tela.setCursor(64, 42); if (status == 201) { Serial.println(" envio: 201 gravado"); tela.print("-> 201"); } else if (status == 0) { Serial.println(" envio: falhou, leitura nao esta no historico"); tela.print("-> falha"); } else { Serial.print(" envio: recusado, HTTP "); Serial.println(status); tela.print("-> erro"); } if (alertaAtivo) { Serial.println(" ALERTA LIGADO (acima de 32 C)"); tela.setTextSize(1); tela.setCursor(0, 55); tela.print("ALERTA"); } tela.display(); } // A histerese, na pratica: a temperatura sobe de 30 para 34, depois desce // de 34 para 30. O painel liga o alerta e so desliga 2 graus abaixo. void mostrarHisterese() { Serial.println(); Serial.println("--- histerese na pratica, sem sensor ---"); Serial.println("temperatura | estado | mudou?"); Serial.println("------------+--------+-------"); const float serie[] = {29.0f, 31.0f, 32.5f, 34.0f, 31.0f, 30.5f, 29.5f}; const int n = sizeof(serie) / sizeof(serie[0]); bool estado = false; int mudancas = 0; for (int i = 0; i < n; i++) { bool novo = proximoEstadoAlerta(serie[i], estado); bool mudou = novo != estado; if (mudou) mudancas++; Serial.print(" "); Serial.print(serie[i], 1); Serial.print(" | "); Serial.print(novo ? "LIGADO " : "deslig."); Serial.print(" | "); Serial.println(mudou ? "MUDOU" : "-"); estado = novo; } Serial.println(); Serial.print("mudancas de estado: "); Serial.println(mudancas); Serial.println("Com um so limite (liga e desliga em 32,0), seriam 4: o painel"); Serial.println("piscaria em cada leitura perto do limite. Com folga de 2 graus,"); Serial.println("sao 2: liga uma vez, desliga uma vez."); } void setup() { Serial.begin(115200); Serial.println(); Serial.println("dia 13 aula 2 — painel com leitura atual e alerta"); Serial.println("==================================================="); Serial.print("display: "); if (!tela.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { Serial.println("nao achou o display em 0x3C."); Serial.println("Se a tela ficar preta: e o endereco (0x3D), nao e o codigo."); Serial.println("O monitor serial continua funcionando: e o instrumento de aula."); } else { Serial.println("SSD1306 128x64 em 0x3C, ok"); } sensor.begin(); tela.clearDisplay(); tela.setTextSize(1); tela.setCursor(0, 0); tela.println("estacao-01"); tela.println("ligando..."); tela.display(); Serial.println(); Serial.println("--- leitura atual ---"); float t = sensor.readTemperature(); float u = sensor.readHumidity(); int status = 201; // o POST real e do dia 13 aula 1 mostrarLeitura(t, u, status); if (!isnan(t)) { bool novo = proximoEstadoAlerta(t, alertaAtivo); if (novo != alertaAtivo) { Serial.print(" alerta mudou para "); Serial.println(novo ? "LIGADO" : "desligado"); } alertaAtivo = novo; } mostrarHisterese(); Serial.println(); Serial.println("--- a regra do alerta, do lado do Node ---"); Serial.println("O painel mostra o alerta com a regra do firmware. O banco"); Serial.println("grava com a regra do service. Se as duas usarem limites"); Serial.println("diferentes, o aluno ve o alerta na tela e o histoico sem"); Serial.println("nada. Por isso LIGAR_C e DESLIGAR_C sao os mesmos numeros"); Serial.println("nos dois lados — e o dia 11 aula 1 que impede que voltem"); Serial.println("a divergir por causa de um valor hardcoded."); } void loop() { Serial.println(); Serial.println("--- nova rodada ---"); setup(); delay(10000); }
Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia13 aula2.
