Consultar e mostrar — Arduino e IoT — semana 6 do 2o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 6 · Consultar e mostrar — Material de Apoio Arduino

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 BETWEEN e preparar as datas no formato que o MySQL entende.
  • Agregar por hora com GROUP BY HOUR e 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, mysql2 instalado 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
ParteO 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 horaas 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:

GuardarCustoO que você perde
cada leituramais espaco, tabela maiornada
media por horamenos espacoo 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.

  1. Escreva a consulta do enunciado antes de rodar: quais colunas, qual WHERE, qual GROUP BY, qual ORDER BY. Marque com o dedo o que e filtro e o que e agrupamento.
  2. 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?
  3. 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?
  4. Troque HOUR(dt_leitura) por DATE(dt_leitura) e rode. O que mudou no número de linhas? O que mudou na pergunta que a consulta responde?
  5. Troque BETWEEN ? AND ? por BETWEEN ? AND ? com o fim em 2026-09-30 (sem hora). Quantas linhas voltaram? Houve erro? Explique em uma frase por que a consulta "vazia" sem estar errada.
  6. Compare COUNT(*) e COUNT(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 X do 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 DELETE filtrado 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érioPontos
Consulta escrita antes de rodar, com filtro e agrupamento marcados2 pontos
Desenho do resultado feito, com o X na hora de maior distancia2 pontos
GROUP BY HOUR com AVG, MIN, MAX e COUNT funcionando3 pontos
ORDER BY presente na consulta, e a resposta de por que ele existe2 pontos
Item 4: a diferença entre agrupar por hora e por dia esta escrita2 pontos
Item 5: a explicacao do intervalo vazio sem erro esta escrita1 ponto

Erros comuns

ErroComo apareceCorrecao
Usar 30/09/2026 no BETWEENER_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-30Zero 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 horaO 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_cUma 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 inteiroAs 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 GET de 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 polling e por que o painel precisa buscar de novo.
  • Reconhecer o 404 de rota inexistente e o 200 com 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, mysql2 instalado 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.

OndeO que acontece
na rotareq.url diz qual pergunta; res.end devolve
no bancoWHERE, GROUP BY, AVG — tudo em SQL
no navegadorfetch 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.

  1. Escreva a resposta que a rota GET /dados deve devolver, no papel, com os valores de exemplo do seu ultimo resultado do dia 6 aula 1. Quantos itens tem em labels e em data?
  2. Crie a rota GET /dados com os parametros de placa e intervalo. Devolva labels e data, e devolva as duas listas com a mesma quantidade de itens.
  3. Crie a rota GET /atual que 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?
  4. Teste as três rotas no navegador: GET /dados, GET /atual e uma rota que não existe. Anote os três status e o corpo de cada.
  5. Monte a página com o grafico e fetch na 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?
  6. Adicione setInterval de 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?
  7. Quebre de propósito: apague o ORDER BY hora da 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:

  • labels com dois itens e data com dois itens. Contar e o item 2 da atividade. Se um dia eles vierem diferentes, o grafico desenha incompleto e sem avisar.
  • temp_c como texto ("24.50", e não 24.5). E o DECIMAL do banco voltando como string, e e por isso que o data do 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 38965 muda a cada execução, porque o exemplo usa listen(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érioPontos
Resposta esperada da rota desenhada, com a contagem de itens em cada lista2 pontos
GET /dados devolvendo labels e data do mesmo tamanho3 pontos
GET /atual com status escolhido e justificado2 pontos
As três rotas testadas, com status e corpo anotados2 pontos
Grafico desenhando, com o número de pontos conferido2 pontos
polling funcionando, e o custo em consultas por segundo estimado1 ponto

Erros comuns

ErroComo apareceCorrecao
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-TypeO 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 dadoItem 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.