Consultar e mostrar — Arduino e IoT — semana 6 do 2o trimestre
Semana 6 de 16· 2o trimestre · 01/05 a 04/09
Consultar e mostrar
O dado entrou. Agora ele precisa responder pergunta.
Aula 1 — Consultar por período e agregar
Objetivos
- Consultar um intervalo de tempo com
BETWEENe preparar as datas no formato que o MySQL entende. - Agregar por hora com
GROUP BY HOURe calcular media, minima e maxima no proprio banco. - Escolher o nivel de detalhe de uma serie temporal e justificar a escolha com um número.
- Explicar por que o detalhe e guardado e a agregacao e calculada, e não o contrario.
- Ler um resultado agregado e dizer, em uma frase, o que ele permite concluir sobre o período.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1 protoboard de 830 pontos por dupla
- Computador com Node.js 20,
mysql2instalado e MySQL ou MariaDB 10.11 - Folha de papel por dupla, para desenhar a serie temporal a mao
- Projetor, para o terminal do professor
- Quadro, para o professor desenhar o grafico de barras com as medias
Conceitos
Filtrar por período: BETWEEN e o formato da data
WHERE dt_leitura BETWEEN ? AND ?
O BETWEEN e inclusivo: pega as duas pontas. O detalhe que trava a aula esta no formato: o MySQL espera 2026-09-30 08:00:00 (ou 2026-09-30 08:00), e não 30/09/2026 08:00.
Pior: se você mandar 2026-09-30 no fim, o intervalo vira ate 2026-09-30 00:00:00, e a consulta devolve zero linhas sem erro nenhum. O aluno ve "esta vazio" e conclui que "não tem dado nesse dia", quando o dado existe e esta depois da meia-noite.
O professor escreve a regra na lousa e a repete no dia 7: o fim de um intervalo de dia precisa ser 23:59:59.
Agregar: quem faz a conta
SELECT HOUR(dt_leitura) AS hora,
COUNT(*) AS leituras,
ROUND(AVG(temp_c), 2) AS temp_media,
MIN(temp_c) AS temp_min,
MAX(temp_c) AS temp_max
FROM tb_leitura
WHERE id_placa = ? AND dt_leitura BETWEEN ? AND ?
GROUP BY HOUR(dt_leitura)
ORDER BY hora
| Parte | O que devolve |
|---|---|
COUNT(*) | quantas leituras cairam naquela hora |
AVG(temp_c) | media das leituras daquela hora |
MIN(temp_c) | menor valor da hora |
MAX(temp_c) | maior valor da hora |
GROUP BY HOUR(dt_leitura) | uma linha por hora |
ORDER BY hora | as horas em ordem cronologica |
O ORDER BY não e enfeite: sem ele, o banco garante nenhuma ordem, e o grafico desenha os pontos embaralhados. O professor já mostrou isso no dia 4, e o aluno que esquece o ORDER BY ve o eixo X bagunçado e não sabe por que.
HOUR() funciona porque dt_leitura e um DATETIME: o banco separa a parte da hora. Isso não funcionaria com uma coluna de texto, que e a razão de o dia 4 aula 1 pedir DATETIME e não VARCHAR.
COUNT(*) diferente de COUNT(temp_c)
COUNT(*) conta linhas. COUNT(temp_c) conta linhas onde temp_c não e nulo. Enquanto a coluna for NOT NULL, os dois dao o mesmo número.
A diferença importa quando o dia 8 comecar a rejeitar leitura incompleta: uma leitura sem temp_c entra na tabela e some da contagem, e o total da tabela e maior que o total da serie. O professor menciona e deixa para o dia 8.
Detalhe guardado, agregacao calculada
A decisao de projeto do dia, e ela vale mais do que qualquer sintaxe:
| Guardar | Custo | O que você perde |
|---|---|---|
| cada leitura | mais espaco, tabela maior | nada |
| media por hora | menos espaco | o pico, a minima, e a granularidade fina |
Como guardar o detalhe e sempre reversivel e guardar a agregacao não e, o curso guarda o detalhe. A media por hora e calculada na hora da consulta, a partir do detalhe, e não e uma tabela paralela.
Atividade
Montagem: nenhuma. A placa fica com o circuito do dia 1 e mostra a leitura bruta.
- Escreva a consulta do enunciado antes de rodar: quais colunas, qual
WHERE, qualGROUP BY, qualORDER BY. Marque com o dedo o que e filtro e o que e agrupamento. - Rode a consulta e desenhe no papel o resultado: uma coluna por hora, uma linha com a media, e as colunas de minimo e maximo. Marque com um X a hora em que a media esta mais longe do maximo. O que esse X diz sobre o período?
- Repita a consulta mudando o intervalo para uma hora so. O que o resultado passa a ser? Um grafico de quantos pontos fica na tela?
- Troque
HOUR(dt_leitura)porDATE(dt_leitura)e rode. O que mudou no número de linhas? O que mudou na pergunta que a consulta responde? - Troque
BETWEEN ? AND ?porBETWEEN ? AND ?com o fim em2026-09-30(sem hora). Quantas linhas voltaram? Houve erro? Explique em uma frase por que a consulta "vazia" sem estar errada. - Compare
COUNT(*)eCOUNT(temp_c)na sua consulta. Os números bateram? Escreva a condição de tabela que faria eles não baterem.
Nota: 12 pontos. Critério de fim: a consulta agregada roda, o desenho esta feito com o X marcado, e os itens 4 e 5 tem resposta escrita.
Resolucao
O exemplo foi executado nesta maquina, duas vezes seguidas, com a mesma saída nos dois casos — o DELETE filtrado no comeco e o que garante isso:
// Consultar por periodo e agregar por hora. O dado ja esta no banco.
// A resposta e uma serie temporal: hora no eixo, media no valor.
const mysql = require('mysql2/promise');
const JANELA = ['2026-09-30 08:00:00', '2026-09-30 09:59:59'];
(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 });
// Idempotencia: limpa SO a janela que este exemplo vai usar. Linhas de
// outros exemplos nao entram na conta, e rodar duas vezes da o mesmo numero.
await pool.execute('DELETE FROM tb_leitura WHERE dt_leitura BETWEEN ? AND ?', JANELA);
const linhas = [
['esp32-lab01', 25.0, 60, '2026-09-30 08:05:00'],
['esp32-lab01', 27.5, 58, '2026-09-30 08:07:00'],
['esp32-lab01', 29.0, 55, '2026-09-30 08:55:00'],
['esp32-lab01', 31.2, 50, '2026-09-30 09:10:00'],
['esp32-lab01', 26.0, 62, '2026-09-30 09:44:00'],
['esp32-lab01', 24.5, 65, '2026-09-30 09:58:00'],
];
for (const l of linhas) {
await pool.execute('INSERT INTO tb_leitura (id_placa, temp_c, umidade_pct, dt_leitura) VALUES (?,?,?,?)', l);
}
// GROUP BY HOUR: quem agrupa e o banco, e nao o Node. Ele sabe contar.
const [por_hora] = await pool.execute(
SELECT HOUR(dt_leitura) AS hora,
COUNT(*) AS leituras,
ROUND(AVG(temp_c), 2) AS temp_media,
MIN(temp_c) AS temp_min,
MAX(temp_c) AS temp_max
FROM tb_leitura
WHERE id_placa = ? AND dt_leitura BETWEEN ? AND ?
GROUP BY HOUR(dt_leitura)
ORDER BY hora, ['esp32-lab01', ...JANELA]);
console.log('hora | leituras | media | min | max');
for (const h of por_hora) {
console.log(String(h.hora).padStart(4), '|', String(h.leituras).padStart(8), '|',
String(h.temp_media).padStart(5), '|', String(h.temp_min).padStart(5), '|', String(h.temp_max).padStart(5));
}
const [geral] = await pool.execute(
'SELECT ROUND(AVG(temp_c),2) AS media, COUNT(*) AS total FROM tb_leitura WHERE id_placa = ?',
['esp32-lab01']);
console.log('media geral da placa:', geral[0].media, 'C em', geral[0].total, 'leituras gravadas no total');
await pool.end();
})();
A saída real, identica nas duas execuções:
hora | leituras | media | min | max 8 | 3 | 27.17 | 25.00 | 29.00 9 | 3 | 27.23 | 24.50 | 31.20 media geral da placa: 27.78 C em 10 leituras gravadas no total
Os três números que o professor aponta nessa saída:
- 3 e 3: as duas horas tem a mesma quantidade de leituras, porque o exemplo semeia três em cada. Não e coincidencia do dado, e do exemplo.
- 27.17 contra 27.23: as medias das duas horas são quase iguais, e o
Xdo item 2 fica na hora 9, onde a distancia entre a media (27.23) e o maximo (31.20) e a maior. - 10 no total e 6 na janela: a tabela tem quatro linhas de outros exemplos. E por isso que o
DELETEfiltrado esta no comeco: sem ele, o total cresceria a cada execução e o número da ultima linha seria o que impressiona o aluno errado.
A razão do ROUND(..., 2): sem ele, AVG devolve muitas casas decimais (27.1666666), e o grafico mostra uma linha com quinze casas. Arredondar e para o olho humano; para calculo, o dia 9 mostra que o certo e guardar o valor cheio.
O sketch da placa mostra a agregacao feita a mao, para o aluno ver a conta que o banco faz:
// Aula 1 do dia 6: granularidade. A media por hora e uma decisao, nao um // default. // // A placa tira uma leitura a cada 2 segundos, o que e 30 leituras por hora. // A consulta do dia so pede a media. A placa nao faz a media: ela entrega // o detalhe e quem decide o nivel de detalhe e a consulta, no Node. #include <Arduino.h> const int PIN_SENSOR_ANALOGICO = 34; const int TOTAL_DE_CICLOS = 12; const int RESOLUCAO_ADC = 4095; float soma_hora = 0.0; int quantidade_hora = 0; int ciclo = 0; unsigned long inicio_hora = 0; unsigned long inicio = 0; void setup() { Serial.begin(115200); delay(2000); inicio = millis(); inicio_hora = millis(); Serial.println(); Serial.println("2o trimestre, dia 6, aula 1 - media por hora"); Serial.println("================================================="); Serial.println("a placa entrega o detalhe; quem agrega e a consulta."); Serial.println("esta tela mostra 12 leituras cruas e a media do bloco."); Serial.println(); Serial.println("ciclo | bruto | temp_c"); Serial.println("------+-------+-------"); } void loop() { if (ciclo < TOTAL_DE_CICLOS) { int bruto = analogRead(PIN_SENSOR_ANALOGICO); float temp_c = (bruto / 4095.0f) * 50.0f; ciclo++; soma_hora += temp_c; quantidade_hora++; Serial.print(" "); Serial.print(ciclo); for (int e = strlen("0") + 1; e < 5; e++) Serial.print(" "); Serial.print(" | "); Serial.print(bruto); for (int e = 1; e < 5; e++) Serial.print(" "); Serial.print(" | "); Serial.print(temp_c, 1); for (int e = 1; e < 5; e++) Serial.print(" "); Serial.println(" C"); } // A agregacao real acontece no MySQL, com AVG() e GROUP BY HOUR. // Aqui ela e feita na mao so para o aluno ver a conta que o banco faz. if (quantidade_hora > 0) { Serial.print("media de "); Serial.print(quantidade_hora); Serial.print(" leituras: "); Serial.print(soma_hora / quantidade_hora, 2); Serial.println(" C (no banco: AVG(temp_c))"); } if (ciclo >= TOTAL_DE_CICLOS) { Serial.println(); Serial.print("tempo gasto em "); Serial.print((millis() - inicio) / 1000); Serial.println(" s"); Serial.println("cada bloco de 30 leituras vira UM ponto no grafico."); Serial.println("detalhe guardado: 30 linhas no banco. ponto no grafico: 1."); ciclo = 0; soma_hora = 0.0; quantidade_hora = 0; inicio = millis(); Serial.println("-------------------------------------------------"); } delay(1500); }
Por que assim e não de outro jeito. A media esta calculada na placa justamente para mostrar que ela pode ser calculada nos dois lados — e que ela não e. A frase do rodape, "detalhe guardado: 30 linhas no banco, ponto no grafico: 1", e a conta que resolve a discussao: o grafico mostra um ponto, mas o banco guarda trinta.
A media impressa usa duas casas (soma / quantidade, 2), e a soma e acumulada em float. Com int, a soma seria truncada a cada ciclo e a media sairia errada em 0,5 — o mesmo tipo de bug do map() com tipo errado que o primeiro trimestre perseguia.
Criterios de correcao
| Critério | Pontos |
|---|---|
| Consulta escrita antes de rodar, com filtro e agrupamento marcados | 2 pontos |
| Desenho do resultado feito, com o X na hora de maior distancia | 2 pontos |
GROUP BY HOUR com AVG, MIN, MAX e COUNT funcionando | 3 pontos |
ORDER BY presente na consulta, e a resposta de por que ele existe | 2 pontos |
| Item 4: a diferença entre agrupar por hora e por dia esta escrita | 2 pontos |
| Item 5: a explicacao do intervalo vazio sem erro esta escrita | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
Usar 30/09/2026 no BETWEEN | ER_WRONG_VALUE ou intervalo vazio | "O MySQL espera AAAA-MM-DD HH:MM:SS. O formato brasileiro ele le como texto e não como data." |
Terminar o intervalo em 2026-09-30 | Zero linhas, sem erro | "Sem hora, o MySQL entende 00:00:00. O BETWEEN e até a meia-noite, e todas as leituras do dia ficam de fora." |
Esquecer o ORDER BY hora | O grafico sai com os pontos fora de ordem | "ORDER BY não muda o conjunto de linhas, so a ordem. Sem ele o banco pode devolver qualquer ordem, e o eixo X do grafico fica bagunçado." |
Tentar GROUP BY temp_c | Uma linha por leitura | "GROUP BY agrupa pelo valor da coluna. Agrupar por temperatura não agrega o tempo: agrupa leituras de mesma temperatura. O que você quer e agrupar por hora." |
Escrever AVG(dt_leitura) | "Media de data" sem sentido | "AVG so faz sentido em coluna numerica. Para data, o que existe e TIMESTAMPDIFF, que o dia 7 usa para o atraso." |
| Fazer a media em JavaScript | "Trouxe 30 mil linhas para o Node" | "Isso funciona e e lento. O banco le a tabela uma vez e devolve duas linhas. Aggregate com GROUP BY e o Node so recebe o resultado." |
Achar que MIN e MAX são do período inteiro | As duas horas com o mesmo minimo | "MIN e MAX são calculados dentro de cada grupo. Com GROUP BY HOUR, são o minimo e o maximo daquela hora. O minimo do período inteiro e outra consulta, sem GROUP BY." |
Usar COUNT(temp_c) e esperar mais linhas | "Veio menos que o total" | "Conta as linhas com temp_c diferente de nulo. Se alguma leitura entrou sem temperatura, ela some da contagem. COUNT(*) conta tudo." |
Desafio extra
Escreva a consulta que responde "a hora mais quente das ultimas 24 horas, e quanto Above a media da media ficou". Isso exige duas agregacoes: uma para achar o maximo das medias, outra para a media geral. Depois escreva em uma frase por que isso não e a mesma coisa que perguntar qual foi a leitura mais quente. A resposta que o professor espera: "hora mais quente pela media" e "hora mais quente por um unico pico" são perguntas diferentes, e a segunda so responde quem consultou o detalhe — e e por isso que o detalhe guardado nunca vira agregado guardado.
>A resolucao, compilada
// Aula 1 do dia 6: granularidade. A media por hora e uma decisao, nao um // default. // // A placa tira uma leitura a cada 2 segundos, o que e 30 leituras por hora. // A consulta do dia so pede a media. A placa nao faz a media: ela entrega // o detalhe e quem decide o nivel de detalhe e a consulta, no Node. #include <Arduino.h> const int PIN_SENSOR_ANALOGICO = 34; const int TOTAL_DE_CICLOS = 12; const int RESOLUCAO_ADC = 4095; float soma_hora = 0.0; int quantidade_hora = 0; int ciclo = 0; unsigned long inicio_hora = 0; unsigned long inicio = 0; void setup() { Serial.begin(115200); delay(2000); inicio = millis(); inicio_hora = millis(); Serial.println(); Serial.println("2o trimestre, dia 6, aula 1 - media por hora"); Serial.println("================================================="); Serial.println("a placa entrega o detalhe; quem agrega e a consulta."); Serial.println("esta tela mostra 12 leituras cruas e a media do bloco."); Serial.println(); Serial.println("ciclo | bruto | temp_c"); Serial.println("------+-------+-------"); } void loop() { if (ciclo < TOTAL_DE_CICLOS) { int bruto = analogRead(PIN_SENSOR_ANALOGICO); float temp_c = (bruto / 4095.0f) * 50.0f; ciclo++; soma_hora += temp_c; quantidade_hora++; Serial.print(" "); Serial.print(ciclo); for (int e = strlen("0") + 1; e < 5; e++) Serial.print(" "); Serial.print(" | "); Serial.print(bruto); for (int e = 1; e < 5; e++) Serial.print(" "); Serial.print(" | "); Serial.print(temp_c, 1); for (int e = 1; e < 5; e++) Serial.print(" "); Serial.println(" C"); } // A agregacao real acontece no MySQL, com AVG() e GROUP BY HOUR. // Aqui ela e feita na mao so para o aluno ver a conta que o banco faz. if (quantidade_hora > 0) { Serial.print("media de "); Serial.print(quantidade_hora); Serial.print(" leituras: "); Serial.print(soma_hora / quantidade_hora, 2); Serial.println(" C (no banco: AVG(temp_c))"); } if (ciclo >= TOTAL_DE_CICLOS) { Serial.println(); Serial.print("tempo gasto em "); Serial.print((millis() - inicio) / 1000); Serial.println(" s"); Serial.println("cada bloco de 30 leituras vira UM ponto no grafico."); Serial.println("detalhe guardado: 30 linhas no banco. ponto no grafico: 1."); ciclo = 0; soma_hora = 0.0; quantidade_hora = 0; inicio = millis(); Serial.println("-------------------------------------------------"); } delay(1500); }
Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia06 aula1.
Aula 2 — Servir os dados: API de leitura e painel simples
Objetivos
- Servir os dados do banco em rotas
GETde JSON, e dizer por que a rota consulta e devolve. - Devolver a serie no formato que o navegador usa: uma lista de rotulos e uma lista de valores de mesmo tamanho.
- Montar a página do painel e consumir a API com
fetch, e ver o grafico com dado de verdade. - Explicar o que e
pollinge por que o painel precisa buscar de novo. - Reconhecer o
404de rota inexistente e o200com corpo vazio, e dizer a diferença.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1 protoboard de 830 pontos por dupla
- Computador com Node.js 20,
mysql2instalado e MySQL ou MariaDB 10.11 - Navegador com acesso a internet, para o professor carregar o grafico
- Projetor, para o navegador e o terminal ao lado
- Folha de papel por dupla, para desenhar as duas listas do grafico
Conceitos
A rota consulta, o banco sabe
A rota tem uma unica responsabilidade: receber o pedido, perguntar ao banco, e devolver JSON. Ela não calcula media, não filtra no JavaScript, e não formata número.
| Onde | O que acontece |
|---|---|
| na rota | req.url diz qual pergunta; res.end devolve |
| no banco | WHERE, GROUP BY, AVG — tudo em SQL |
| no navegador | fetch le JSON e desenha |
Quando a lógica entra na rota, dois problemas aparecem: a mesma media e reimplementada em cada rota, e a rota fica impossivel de testar sem banco. O dia 10 desfaz isso com camadas; hoje o professor so aponta o caminho.
O formato do grafico: duas listas do mesmo tamanho
O Chart.js e quase toda biblioteca de grafico do navegador espera duas listas paralelas:
{ "labels": ["08h", "09h"], "data": ["27.17", "27.64"] }
Se labels tiver três itens e data dois, o grafico desenha dois pontos e ignora o terceiro, sem erro. Esse e o bug silencioso mais comum do dia: o grafico aparece, so que incompleto.
A rota precisa montar as duas listas a partir do resultado agregado. O professor escreve o map na lousa e explica que ele e o "para cada linha, me devolve um item" — exatamente o for do 1o trimestre, com outra forma.
Um detalhe do exemplo: temp_media volta como string do MySQL quando vem de ROUND sobre DECIMAL, e o Chart.js aceita, mas o eixo Y fica com decimais longos. O professor mostra os dois e deixa a escolha para o aluno.
polling: perguntar de novo
setInterval(() => { buscarDados(); }, 5000);
Não existe WebSocket nesta aula, e a razão e pedagogica: o curso ainda não mostrou conexão persistente, e polling resolve o caso de uso com três linhas. O dia 9 mostra que abrir conexão com a placa e outra historia.
O polling tem um custo que o professor nomeia: cada busca e uma consulta ao banco. Com 20 placas no painel e busca de 5 em 5 segundos, são 4 consultas por segundo, mesmo sem nenhum dado novo. E por isso que o dia 7 vem logo depois.
Atividade
Montagem: nenhuma. A placa fica com o circuito do dia 1.
- Escreva a resposta que a rota
GET /dadosdeve devolver, no papel, com os valores de exemplo do seu ultimo resultado do dia 6 aula 1. Quantos itens tem emlabelse emdata? - Crie a rota
GET /dadoscom os parametros de placa e intervalo. Devolvalabelsedata, e devolva as duas listas com a mesma quantidade de itens. - Crie a rota
GET /atualque devolve so a ultima leitura (ORDER BY id DESC LIMIT ?). O que ela devolve quando não ha nenhuma leitura? Escolha o status: 200 com corpo vazio, ou 404? - Teste as três rotas no navegador:
GET /dados,GET /atuale uma rota que não existe. Anote os três status e o corpo de cada. - Monte a página com o grafico e
fetchna rota/dados. Ve o grafico desenhado com quantos pontos? O número de pontos bate com o número de horas do dia 6 aula 1? - Adicione
setIntervalde 5 segundos e veja o grafico se atualizar. A consulta ao banco roda a cada 5 segundos mesmo com o dado parado? Como você sabe? - Quebre de propósito: apague o
ORDER BY horada consulta. O grafico muda? O que o grafico mostra que a consulta já não garante?
Nota: 12 pontos. Critério de fim: o grafico desenha com as duas listas do mesmo tamanho, as três rotas foram testadas com status anotado, e o item 7 tem resposta escrita.
Resolucao
O exemplo foi executado nesta maquina, com as três rotas testadas:
// A API de leitura que alimenta o painel. Duas rotas:
// GET /dados devolve a serie para o grafico, GET /atual devolve a ultima.
const http = require('http');
const mysql = require('mysql2/promise');
let pool;
const servidor = http.createServer(async (req, res) => {
const responder = (status, objeto) => {
res.writeHead(status, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(objeto));
};
if (req.method === 'GET' && req.url.startsWith('/dados')) {
const [serie] = await pool.execute(
SELECT HOUR(dt_leitura) AS hora, ROUND(AVG(temp_c),2) AS temp_media
FROM tb_leitura WHERE id_placa = ? AND dt_leitura BETWEEN ? AND ?
GROUP BY HOUR(dt_leitura) ORDER BY hora,
['esp32-lab01', '2026-09-30 08:00:00', '2026-09-30 09:59:59']);
// O browser precisa da chave "labels" e "data" e uma chave "hora"/"temp_media"
// seria duas vezes mais codigo no painel. O servidor traduz.
const rotulos = serie.map((h) => String(h.hora).padStart(2, '0') + 'h');
const valores = serie.map((h) => h.temp_media);
return responder(200, { labels: rotulos, data: valores });
}
if (req.method === 'GET' && req.url.startsWith('/atual')) {
const [ultima] = await pool.execute(
'SELECT id, temp_c, umidade_pct, dt_leitura FROM tb_leitura WHERE id_placa = ? ORDER BY id DESC LIMIT ?',
['esp32-lab01', 1]);
return responder(200, ultima[0] || { erro: 'nenhuma leitura ainda' });
}
responder(404, { erro: 'rota nao existe' });
});
(async () => {
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 new Promise((r) => servidor.listen(0, '127.0.0.1', r));
const base = 'http://127.0.0.1:' + servidor.address().port;
console.log('API no ar em', base);
const serie = await fetch(base + '/dados?placa=esp32-lab01');
console.log('/dados ->', serie.status, await serie.text());
const atual = await fetch(base + '/atual');
console.log('/atual ->', atual.status, await atual.text());
const erro = await fetch(base + '/nao-existe');
console.log('/nao-existe ->', erro.status, await erro.text());
servidor.close();
await pool.end();
})();
A saída real, medida nesta maquina:
API no ar em http://127.0.0.1:38965 /dados -> 200 {"labels":["08h","09h"],"data":["27.17","27.64"]} /atual -> 200 {"id":11,"temp_c":"24.50","umidade_pct":"65.00","dt_leitura":"2026-09-30 09:58:00"} /nao-existe -> 404 {"erro":"rota nao existe"}
O professor aponta três coisas dessa saída:
labelscom dois itens edatacom dois itens. Contar e o item 2 da atividade. Se um dia eles vierem diferentes, o grafico desenha incompleto e sem avisar.temp_ccomo texto ("24.50", e não24.5). E oDECIMALdo banco voltando como string, e e por isso que odatado grafico também vem com"27.17"entre aspas. Funciona no grafico; quebra no dia 9, quando o painel precisar calcular a media no navegador.- A porta
38965muda a cada execução, porque o exemplo usalisten(0). O professor avisa: se você rodar e o número for outro, o exemplo esta certo.
A página do painel, para o professor colar num arquivo e abrir no navegador:
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<title>Painel de temperatura</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body { font-family: system-ui, sans-serif; margin: 2rem; }
#grafico { max-width: 640px; }
</style>
</head>
<body>
<h1>Temperatura por hora</h1>
<canvas id="grafico" width="640" height="320"></canvas>
<p id="atual">leitura atual: carregando...</p>
<script>
const BASE = 'http://127.0.0.1:3000';
const grafico = new Chart(document.getElementById('grafico'), {
type: 'line',
data: { labels: [], datasets: [{ label: 'temperatura C', data: [] }] },
});
async function buscarDados() {
// A rota devolve labels e data do mesmo tamanho. Se voltarem
// diferentes, o grafico desenha incompleto e nao avisa.
const serie = await fetch(BASE + '/dados?placa=esp32-lab01').then((r) => r.json());
grafico.data.labels = serie.labels;
grafico.data.datasets[0].data = serie.data;
grafico.update();
const atual = await fetch(BASE + '/atual').then((r) => r.json());
document.getElementById('atual').textContent =
'leitura atual: ' + atual.temp_c + ' C as ' + atual.dt_leitura;
}
buscarDados();
// polling: pergunta de novo a cada 5 s, sem recarregar a pagina.
setInterval(buscarDados, 5000);
</script>
</body>
</html>
Por que assim e não de outro jeito. A rota monta as duas listas de uma vez, a partir do mesmo serie. Montar labels numa consulta e data em outra seria duas chances de errar o ORDER BY, e o grafico mostraria rotulos que não batem com os valores.
O startsWith('/dados') em vez de === '/dados' resolve o problema da query string: o navegador pede /dados?placa=esp32-lab01, e o === compararia com a query inteira e cairia no 404. O dia 2 aula 2 já mostrou isso, e o aluno que esqueceu vai esquecer de novo.
O sketch da placa mostra o JSON que a API devolve, montado do lado do sensor:
// Aula 2 do dia 6: o que o painel pede ao servidor. // // O grafico do navegador precisa de duas listas de mesmo tamanho: os rotulos // do eixo X e os valores do eixo Y. Esta placa monta essas duas listas // exatamente no formato que o Node devolve, para o aluno ver de onde vem o // {"labels": [...], "data": [...]}. #include <Arduino.h> const int PIN_SENSOR_ANALOGICO = 34; const int TOTAL_DE_HORAS = 4; const int RESOLUCAO_ADC = 4095; float temp_hora[TOTAL_DE_HORAS]; int preenchidas = 0; int ciclo = 0; void setup() { Serial.begin(115200); delay(2000); Serial.println(); Serial.println("2o trimestre, dia 6, aula 2 - o JSON que o painel consome"); Serial.println("================================================="); Serial.println("o Node responde exatamente com estes dois campos:"); Serial.println(" labels: rotulos do eixo X, em string"); Serial.println(" data : valores do eixo Y, em numero"); Serial.println("as duas listas precisam do MESMO tamanho."); Serial.println(); } void loop() { // Uma leitura por volta. Depois de TOTAL_DE_HORAS, o JSON fecha. if (preenchidas < TOTAL_DE_HORAS) { int bruto = analogRead(PIN_SENSOR_ANALOGICO); temp_hora[preenchidas] = (bruto / 4095.0f) * 50.0f; preenchidas++; ciclo++; } // Monta o JSON que o Node vai devolver. Repare no size(2): e o que // garante que labels e data crescem juntas. String labels = "["; String data = "["; for (int i = 0; i < preenchidas; i++) { if (i > 0) { labels += ","; data += ","; } labels += "\"" + String(8 + i) + "h\""; data += String(temp_hora[i], 2); } labels += "]"; data += "]"; Serial.println("-------------------------------------------------"); Serial.print("GET /dados -> HTTP/1.1 200 OK"); Serial.println(); Serial.println(String("{\"labels\":") + labels + ",\"data\":" + data + "}"); if (preenchidas >= TOTAL_DE_HORAS) { Serial.println(); Serial.print("leitura atual (GET /atual) -> "); Serial.print(temp_hora[TOTAL_DE_HORAS - 1], 2); Serial.println(" C"); Serial.println(); Serial.println("o grafico le duas vezes: uma para os rotulos e outra"); Serial.println("para os valores. Sem a segunda lista, o eixo Y fica vazio."); preenchidas = 0; ciclo = 0; } delay(1500); }
Por que assim e não de outro jeito. As duas listas são montadas no mesmo for, com o if (i > 0) isolando a virgula. E a forma de garantir o item 2 da atividade: não ha como labels ficar maior que data, porque os dois crescem juntos dentro do mesmo laco.
O professor escreve essa construção na lousa e pede a mesma coisa em JavaScript, no map da rota. O aluno ve que a mesma ideia — "para cada linha, um item" — tem duas sintaxes porque duas linguagens, e não porque o problema e diferente.
Criterios de correcao
| Critério | Pontos |
|---|---|
| Resposta esperada da rota desenhada, com a contagem de itens em cada lista | 2 pontos |
GET /dados devolvendo labels e data do mesmo tamanho | 3 pontos |
GET /atual com status escolhido e justificado | 2 pontos |
| As três rotas testadas, com status e corpo anotados | 2 pontos |
| Grafico desenhando, com o número de pontos conferido | 2 pontos |
polling funcionando, e o custo em consultas por segundo estimado | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
labels com 3 itens e data com 2 | "O grafico ficou torto" | "A biblioteca para quando a lista acaba, e não avisa. Conte os dois com o console antes de desenhar: console.log(s.labels.length, s.data.length)." |
Comparar req.url === '/dados' | Cai no 404 mesmo com a rota certa | "O navegador manda /dados?placa=.... Use startsWith, ou corte o ? antes de comparar." |
| Filtrar e agregar em JavaScript | "Trouxe 5000 linhas e somei no Node" | "A consulta já devolve duas linhas. Faca o GROUP BY no SQL e o Node so recebe o resultado pronto." |
Esquecer res.writeHead com o Content-Type | O navegador mostra JSON cru em vez de grafico | "Sem o Content-Type: application/json, o fetch pode receber texto, e res.json() quebra." |
Achar que polling e so para o professor ver | "Não precisa buscar de novo, o dado não mudou" | "O painel precisa saber que o dado mudou. Se você não perguntar, você não sabe. E cada busca e uma consulta — por isso o dia 7 vem agora." |
GET /atual devolvendo 200 com lista vazia quando não ha dado | Item 3 sem justificativa | "Depende do que o painel precisa. Se precisa tratar "sem leitura ainda", um 200 com { erro: ... } funciona; se a ausencia e erro, 404. Escreva qual dos dois você escolheu e por que." |
Rodar o grafico sem grafico.update() | "Mudei o dado e o grafico não mudou" | "Alterar data não redesenha. Chame update() depois de atribuir as listas." |
Fechar o servidor sem await pool.end() | "O terminal não volta" | "O pool tem conexões abertas. Feche os dois: servidor.close() e await pool.end()." |
Desafio extra
Faca o painel buscar duas placas e desenhar as duas no mesmo grafico, com duas linhas. Você vai precisar de duas consultas e de um datasets com dois itens. Depois escreva em uma frase o que muda no setInterval: quantas consultas por segundo o navegador faz agora. A resposta: dobra, porque cada buscarDados faz uma consulta por placa. Com 20 placas e busca de 5 s, são 4 consultas por segundo parado — e o dia 7 mostra que o custo maior esta em gravar, não em consultar.
A resolucao, compilada
// Aula 2 do dia 6: o que o painel pede ao servidor. // // O grafico do navegador precisa de duas listas de mesmo tamanho: os rotulos // do eixo X e os valores do eixo Y. Esta placa monta essas duas listas // exatamente no formato que o Node devolve, para o aluno ver de onde vem o // {"labels": [...], "data": [...]}. #include <Arduino.h> const int PIN_SENSOR_ANALOGICO = 34; const int TOTAL_DE_HORAS = 4; const int RESOLUCAO_ADC = 4095; float temp_hora[TOTAL_DE_HORAS]; int preenchidas = 0; int ciclo = 0; void setup() { Serial.begin(115200); delay(2000); Serial.println(); Serial.println("2o trimestre, dia 6, aula 2 - o JSON que o painel consome"); Serial.println("================================================="); Serial.println("o Node responde exatamente com estes dois campos:"); Serial.println(" labels: rotulos do eixo X, em string"); Serial.println(" data : valores do eixo Y, em numero"); Serial.println("as duas listas precisam do MESMO tamanho."); Serial.println(); } void loop() { // Uma leitura por volta. Depois de TOTAL_DE_HORAS, o JSON fecha. if (preenchidas < TOTAL_DE_HORAS) { int bruto = analogRead(PIN_SENSOR_ANALOGICO); temp_hora[preenchidas] = (bruto / 4095.0f) * 50.0f; preenchidas++; ciclo++; } // Monta o JSON que o Node vai devolver. Repare no size(2): e o que // garante que labels e data crescem juntas. String labels = "["; String data = "["; for (int i = 0; i < preenchidas; i++) { if (i > 0) { labels += ","; data += ","; } labels += "\"" + String(8 + i) + "h\""; data += String(temp_hora[i], 2); } labels += "]"; data += "]"; Serial.println("-------------------------------------------------"); Serial.print("GET /dados -> HTTP/1.1 200 OK"); Serial.println(); Serial.println(String("{\"labels\":") + labels + ",\"data\":" + data + "}"); if (preenchidas >= TOTAL_DE_HORAS) { Serial.println(); Serial.print("leitura atual (GET /atual) -> "); Serial.print(temp_hora[TOTAL_DE_HORAS - 1], 2); Serial.println(" C"); Serial.println(); Serial.println("o grafico le duas vezes: uma para os rotulos e outra"); Serial.println("para os valores. Sem a segunda lista, o eixo Y fica vazio."); preenchidas = 0; ciclo = 0; } delay(1500); }
Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia06 aula2.
