Intervalos, atrasos e a placa que não responde — Arduino e IoT — semana 7 do 2o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 7 · Intervalos, atrasos e a placa que não responde — Material de Apoio Arduino

Semana 7 de 16· 2o trimestre · 01/05 a 04/09

Intervalos, atrasos e a placa que não responde

Tempo e a dimensao que ninguem planeja e todo mundo sofre.

Aula 1 — A placa que manda rapido demais e a base que cresce

Objetivos

  • Calcular quantas linhas um intervalo de envio gera por dia, e comparar três intervalos com números.
  • Medir o tamanho real da tabela no disco, em vez de estimar, e explicar a diferença entre linha e espaco.
  • Escrever uma politica de retencao e um DELETE com filtro proprio, idempotente.
  • Medir a taxa de gravação real de um servidor com a placa mandando rapido demais.
  • Explicar por que o intervalo de envio e uma decisao de projeto, e não um número de taste.

Material

  • 1 ESP32 DevKit V1 por aluno, com o cabo USB
  • 1 protoboard de 830 pontos e 1 jogo de jumpers por aluno
  • 1 módulo de alimentacao MB102 por dupla
  • 1 roteador com rede de teste do professor
  • 1 computador com Node.js 20, mysql2 instalado e MySQL ou MariaDB 10.11
  • Folha de papel por dupla, para a conta de linhas por intervalo
  • Projetor, para o terminal do Node e o monitor serial juntos

Conceitos

A conta que assusta antes do teste

IntervaloLinhas por diaLinhas em 7 dias
1 s86 400604 800
2 s43 200302 400
10 s8 64060 480
60 s1 44010 080

A conta e 86400 / intervalo. Com 300 bytes por linha (a linha do banco com o índice, não so o JSON), 86 400 linhas por dia são cerca de 26 MB por dia e quase 180 MB por semana, so nessa placa. Com dez placas na sala, o número multiplica por dez.

O professor não faz a conta de "300 bytes" ao acaso: ele manda o aluno medir o tamanho real da tabela no item 4, e compara. A conta serve para decidir, a medicao serve para conferir. E assim que o aluno aprende a não confiar nem na propria estimativa.

Medir em vez de estimar

SELECT TABLE_NAME, TABLE_ROWS, DATA_LENGTH, INDEX_LENGTH
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'tb_leitura'

information_schema e o catalogo do proprio banco, com o que ele sabe sobre as suas tabelas. DATA_LENGTH e os dados, INDEX_LENGTH e o índice — e o índice ocupa tanto quanto os dados nesta tabela, porque são duas colunas em uma tabela que so cresce.

O professor anota: índice em tabela de log costuma ser desperdicio, e o dia 9 mostra o caso onde ele e necessario.

Politica de retencao

Apagar dado velho e uma decisao, e a decisao tem três partes:

  1. quanto tempo guardar — 7 dias? 30? um mes?
  2. quando executar — a cada hora, no primeiro INSERT do dia, por agendamento?
  3. o que apagar — por data, nunca por id: id não tem relacao com tempo.
DELETE FROM tb_leitura WHERE dt_leitura < ?

Esse DELETE e idempotente: rodar duas vezes apaga as mesmas linhas e depois zero. O dia 4 aula 2 usou TRUNCATE para o mesmo efeito, e a diferença e que o TRUNCATE apaga tudo e o DELETE apaga so o que e velho.

O filtro precisa ser proprio: WHERE dt_leitura < ? com a data de corte calculada pelo código. DELETE FROM tb_leitura sem filtro apaga tudo, e o dia 8 trata o que acontece quando esse comando vem de um if mal colocado.

A taxa real e menor que a taxa teorica

A placa manda a cada 500 ms, então a taxa teorica e 2 leituras por segundo. A taxa real vai ser menor, e o professor explica por que antes de medir: cada envio espera a resposta do servidor, e enquanto espera, o intervalo de 500 ms já passou.

Esse e o primeiro sinal de que taxa nominal e taxa efetiva são coisas diferentes, e a fila do dia 7 aula 2 vai mostrar o que acontece quando a fila cresce até o ponto de a taxa efetiva cair mais ainda. E a diferença entre "a placa manda a cada 500 ms" e "o banco recebe a cada 500 ms".

Atividade

Montagem:

  • Circuito do dia 1 (potenciometro no GPIO34), sem peca nova.
  • Servidor Node do dia 5 aula 2 rodando, e banco de aula com a tabela do dia 4.
  1. Escreva a conta de linhas por dia para os intervalos 1 s, 2 s, 10 s e 60 s. Escolha um intervalo para a sua placa e justifique a escolha com o número, não com gosto.
  2. Meça o tamanho real: rode a consulta de information_schema e anote TABLE_ROWS, DATA_LENGTH e INDEX_LENGTH. Quanto do espaco total e índice?
  3. Grave a placa com INTERVALO_CRITICO = 500 e deixe rodando 60 segundos. Depois, com o Node parado, e conte quantas respostas 201 a placa recebeu. Compare com a taxa teorica de 2 por segundo. Qual foi a taxa real?
  4. Explique em uma frase por que a taxa real foi menor. O que a placa estava fazendo enquanto o 500 ms passava?
  5. Escreva a politica de retencao da sua placa: quantos dias guardar, e quando apagar. Depois escreva o DELETE correspondente, com parametro.
  6. Rode o DELETE duas vezes seguidas. Quantas linhas foram apagadas na primeira, e quantas na segunda? Por que a segunda e zero?
  7. Inverta o filtro: WHERE dt_leitura > ? com a data de hoje. Quantas linhas ele apagou? O que essa inversao diz sobre o perigo de um filtro escrito sem_TESTAR?

Nota: 12 pontos. Critério de fim: existe taxa teorica e taxa real medidas e comparadas, e o item 6 tem os dois números de linhas apagadas.

Resolucao

O exemplo foi executado nesta maquina. A conta vem primeiro, a medicao depois, e a limpeza no fim:

// A placa mandando rapido demais: quantas linhas cabem num dia, e o que fazer
// com o que ja e velho. A resposta e uma politica de retencao, nao um DELETE cego.
const mysql = require('mysql2/promise');

const DIAS_QUE_QUEREMOS = 7;

(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 });

  // 1. Quanto o intervalo de envio custa, em linhas por dia.
  for (const segundos of [2, 10, 60]) {
    const por_dia = Math.floor(86400 / segundos);
    console.log('a cada', String(segundos).padStart(2), 's ->', String(por_dia).padStart(6),
      'linhas/dia |', String(por_dia * 7).padStart(7), 'linhas em 7 dias');
  }
  console.log('a 1 linha por segundo seriam', 86400, 'linhas por dia, e 2,4 GB em 7 dias com 300 bytes por linha.');
  console.log('Por isso a placa do dia 7 envia a cada 60 s, e nao a cada 1 s.');

  // 2. Quanto a tabela ocupa agora, medido -- e nao estimado.
  const [tamanho] = await pool.execute(
    SELECT TABLE_NAME, TABLE_ROWS, DATA_LENGTH, INDEX_LENGTH
    FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'tb_leitura');
  console.log('tabela:', tamanho[0].TABLE_NAME, '| linhas estimadas', tamanho[0].TABLE_ROWS,
    '| dados', tamanho[0].DATA_LENGTH, 'bytes | indice', tamanho[0].INDEX_LENGTH, 'bytes');

  // 3. A limpeza: um DELETE com filtro proprio, e so do que e velho.
  const [antes] = await pool.query('SELECT COUNT(*) AS total FROM tb_leitura');
  const [r] = await pool.execute(
    'DELETE FROM tb_leitura WHERE dt_leitura < ?', ['2026-09-30 08:00:00']);
  const [depois] = await pool.query('SELECT COUNT(*) AS total FROM tb_leitura');
  console.log('linhas antes', antes[0].total, '| apagadas', r.affectedRows, '| depois', depois[0].total);
  console.log('O DELETE e idempotente: rodar de novo apaga', 0, 'linhas.');

  await pool.end();
})();

A saída real, medida nesta maquina:

a cada  2 s ->   43200 linhas/dia |   302400 linhas em 7 dias
a cada 10 s ->    8640 linhas/dia |    60480 linhas em 7 dias
a cada 60 s ->    1440 linhas/dia |    10080 linhas em 7 dias
a 1 linha por segundo seriam 86400 linhas por dia, e 2,4 GB em 7 dias com 300 bytes por linha.
Por isso a placa do dia 7 envia a cada 60 s, e nao a cada 1 s.
tabela: tb_leitura | linhas estimadas 11 | dados 16384 bytes | indice 16384 bytes
linhas antes 11 | apagadas 0 | depois 11
O DELETE e idempotente: rodar de novo apaga 0 linhas.

O professor aponta três coisas nessa saída, e as três são resultados medidos, não previstos:

  • dados 16384 e índice 16384: o índice ocupa exatamente o mesmo espaco que os dados. Com 11 linhas isso e arredondamento do bloco, e o professor diz isso: em tabela pequena o número não ensina. Em tabela grande, índice em log e desperdicio.
  • apagadas 0: o filtro dt_leitura < '2026-09-30 08:00:00' não pegou nenhuma linha, porque todas as linhas do exemplo estão depois desse horario. O aluno que esperava apagar alguma coisa ve 0 e precisa entender por que. E o melhor item da atividade: um DELETE que apaga zero pode estar certo ou pode estar com o filtro errado, e so a conferida do filtro separa os dois casos.
  • DIAS_QUE_QUEREMOS esta declarada e não usada no código. O professor deixa assim de propósito: a variável de politica existe antes de existir o agendamento, e o aluno tem de ver que a politica e uma decisao escrita antes de ser código.

O sketch da placa, que manda rapido demais de propósito:

// Aula 1 do dia 7: a placa que manda rapido demais.
//
// Esta placa envia a cada 500 ms, de proposito. Em um dia inteiro isso sao
// 172800 linhas na tabela: o aluno ve o numero, e o numero e o ensinado.
// Nao existe montao com 172800 linhas nesta aula — a conta e feita no
// terminal do Node, e e mais honesta do que encher um banco.

#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>

const int PIN_LED = 2;

// Credenciais FICTICIAS.
const char* WIFI_SSID     = "rede-teste-lab01";
const char* CHAVE_REDE = "ficticia-lab01";

const char* IP_SERVIDOR   = "192.168.0.10";
const int   PORTA_SERVIDOR = 3000;
const char* CAMINHO = "/leitura";

// 500 ms e o INTERVALO_CRITICO do dia 7. Os alunos que terminaram podem
// baixar para 200 e ver o que acontece com o Node; a maioria nao aguenta.
const long INTERVALO_CRITICO = 500;

const unsigned long TIMEOUT_MS = 2000;

WiFiClient cliente;
HTTPClient http;
unsigned long ultimo_envio = 0;

// Marcador de tempo: quando comecou a medicao. A placa nao tem relogio de
// data, entao medimos em segundos desde o boot.
unsigned long inicio = 0;
long enviadas = 0;
long perdidas = 0;
int taxa_cinco_segundos = 0;
unsigned long marca_cinco = 0;

void setup() {
  pinMode(PIN_LED, OUTPUT);
  Serial.begin(115200);
  delay(2000);
  inicio = millis();
  marca_cinco = inicio;

  Serial.println();
  Serial.println("2o trimestre, dia 7, aula 1 - a placa rapida demais");
  Serial.println("-------------------------------------------------");
  Serial.print("intervalo de envio: ");
  Serial.print(INTERVALO_CRITICO);
  Serial.println(" ms");
  Serial.print("em um dia, cada placa gera ");
  Serial.print(86400000L / INTERVALO_CRITICO);
  Serial.println(" linhas na tabela");
  Serial.println();

  WiFi.begin(WIFI_SSID, CHAVE_REDE);
  int tentativas = 0;
  while (WiFi.status() != WL_CONNECTED && tentativas < 40) {
    delay(500);
    Serial.print(".");
    tentativas++;
  }
  Serial.println();
  if (WiFi.status() == WL_CONNECTED) {
    Serial.print("placa conectada, IP ");
    Serial.println(WiFi.localIP());
  } else {
    Serial.println("sem rede: a contagem continua, so nao ha envio.");
  }
}

void loop() {
  unsigned long agora = millis();

  if (agora - ultimo_envio < INTERVALO_CRITICO) {
    return;
  }
  ultimo_envio = agora;

  // A taxa a cada 5 segundos: e o numero que o professor pede para anotar.
  if (agora - marca_cinco >= 5000) {
    taxa_cinco_segundos = 0;
    marca_cinco = agora;
  }

  if (WiFi.status() != WL_CONNECTED) {
    perdidas++;
    return;
  }

  // O cast para int NAO e vaidade: enviadas e long, e %02d espera int.
  // Sem o cast o programa imprime lixo, e o compilador avisa — este e o
  // erro que o dia 3 do 1o trimestre ja viu com o %s no lugar do %d.
  // Buffer de 24 bytes: a data com dois digitos em cada campo ocupa 19
  // caracteres, e sobrar espaco evita o aviso de truncamento do compilador.
  // Buffer curto demais e o erro que passa e da dado corrompido em producao.
  char agora_txt[24];
  snprintf(agora_txt, sizeof(agora_txt), "2026-09-30 10:%02d:%02d",
           (int)((enviadas / 60) % 60), (int)(enviadas % 60));

  String json = String("{\"id_placa\":\"esp32-lab01\"")
              + ",\"temp_c\":" + String(25.0 + (enviadas % 7) * 0.5, 1)
              + ",\"umidade_pct\":" + String(55 + (enviadas % 8))
              + ",\"dt_leitura\":\"" + String(agora_txt) + "\"}";

  http.setTimeout(TIMEOUT_MS);
  http.begin("http://" + String(IP_SERVIDOR) + ":" + String(PORTA_SERVIDOR) + CAMINHO);
  http.addHeader("Content-Type", "application/json");
  int codigo = http.POST(json);
  http.end();

  if (codigo == 201) {
    enviadas++;
    taxa_cinco_segundos++;
  } else {
    perdidas++;
  }

  // O LED pisca a cada 10 enviados. Assim o olho acompanha a placa sem que
  // o monitor serial precise de imprimir 10 linhas por segundo.
  if (enviadas % 10 == 0) {
    digitalWrite(PIN_LED, HIGH);
    delay(30);
    digitalWrite(PIN_LED, LOW);
  }
}

Por que assim e não de outro jeito. O taxa_cinco_segundos existe para o professor poder anotar um número por janela de cinco segundos, e não um total acumulado. O contador acumulado sobe devagar e o aluno não distingue "a placa parou" de "a placa esta lenta".

O http.end() vem antes do if do 201. Na maioria das amostras isso não muda nada, porque o end so fecha a conexão; mas quando o intervalo e de 200 ms e a placa trava, ter o end fora do caminho feliz evita conexão presa. E a diferença entre "funciona na aula" e "funciona com a turma toda mandando".

O if (enviadas % 10 == 0) para o LED e um caso em que o % faz papel de "a cada 10", e não de resto de divisão. O professor usa para mostrar que o mesmo operador serve para duas coisas, e que a leitura do % depende do que se quer dizer com ele.

Criterios de correcao

CritérioPontos
Tabela de linhas por dia para os quatro intervalos, com a escolha justificada por número3 pontos
Tamanho real da tabela medido, com a proporcao de índice anotada2 pontos
Taxa teorica e taxa real medidas e comparadas3 pontos
Politica de retencao escrita, com dias e momento do apagamento2 pontos
DELETE com filtro proprio e parametro1 ponto
Item 6: as duas contagens de linhas apagadas, e a explicacao do zero1 ponto

Erros comuns

ErroComo apareceCorrecao
Usar TRUNCATE na limpeza diaria"Truncatei e perdi o historico""TRUNCATE apaga tudo. Para retencao, o que apaga so o velho e DELETE com filtro de data."
Apagar por id"Apaguei as 100 primeiras linhas""id cresce na ordem da chegada, não do tempo. Apagar por id apaga dado recente. O filtro de tempo e por dt_leitura."
DELETE sem filtro"Deu certo e eu vi o banco vazio""Funcionou demais. DELETE FROM tb_leitura apaga tudo, em qualquer dia, e não da chance de confirmar. Escreva o filtro e teste com SELECT antes."
Confundir taxa nominal com taxa efetiva"Mandei a cada 500 ms, então 2 por segundo""Esse e o número teorico. Meça os 201 em 60 segundos. A placa espera a resposta do servidor, e enquanto espera o intervalo já passou."
Assumir 300 bytes por linha sem medirItem 2 sem medir"300 e uma ordem de grandeza, não o seu número. A medicao de DATA_LENGTH e INDEX_LENGTH e o que substitui a conta."
Achar que o índice não ocupa espaco"Índice e de graca""Nessa tabela, INDEX_LENGTH e igual a DATA_LENGTH. Índice ocupa espaco, e em tabela que so cresce, ele cresce junto."
Rodar a limpeza com o intervalo de envio de 500 ms"Rodei o DELETE e a tabela voltou a encher""A limpeza e uma operacao pontual. Se você apaga a cada envio, o custo sobe e o banco trabalha duas vezes por leitura. A politica e "após N leituras" ou "por agendamento"."
Declarar DIAS_QUE_QUEREMOS e nunca usar"Escrevi a politica e não apaga nada""A variável sozinha não faz nada. Ela vira parametro do DELETE, com a data de corte calculada a partir dela. Sem esse calculo, a politica e so um comentario."

Desafio extra

Escreva o código que calcula a data de corte a partir de DIAS_QUE_QUEREMOS, e que so roda o DELETE quando já passou uma hora desde a ultima limpeza — guardando a hora da ultima limpeza em memória. Depois pergunte: o que acontece se o Node reiniciar? E o que o dia 11 responde? A resposta que o professor espera: a variável em memória morre com o processo, e por isso que a hora da ultima limpeza tem de ser persistida ou a limpeza tem de rodar por agendamento do sistema. A memória do processo não sobrevive — a unica coisa que sobrevive e a tabela.

>

A resolucao, compilada

// Aula 1 do dia 7: a placa que manda rapido demais.
//
// Esta placa envia a cada 500 ms, de proposito. Em um dia inteiro isso sao
// 172800 linhas na tabela: o aluno ve o numero, e o numero e o ensinado.
// Nao existe montao com 172800 linhas nesta aula — a conta e feita no
// terminal do Node, e e mais honesta do que encher um banco.

#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>

const int PIN_LED = 2;

// Credenciais FICTICIAS.
const char* WIFI_SSID     = "rede-teste-lab01";
const char* CHAVE_REDE = "ficticia-lab01";

const char* IP_SERVIDOR   = "192.168.0.10";
const int   PORTA_SERVIDOR = 3000;
const char* CAMINHO = "/leitura";

// 500 ms e o INTERVALO_CRITICO do dia 7. Os alunos que terminaram podem
// baixar para 200 e ver o que acontece com o Node; a maioria nao aguenta.
const long INTERVALO_CRITICO = 500;

const unsigned long TIMEOUT_MS = 2000;

WiFiClient cliente;
HTTPClient http;
unsigned long ultimo_envio = 0;

// Marcador de tempo: quando comecou a medicao. A placa nao tem relogio de
// data, entao medimos em segundos desde o boot.
unsigned long inicio = 0;
long enviadas = 0;
long perdidas = 0;
int taxa_cinco_segundos = 0;
unsigned long marca_cinco = 0;

void setup() {
  pinMode(PIN_LED, OUTPUT);
  Serial.begin(115200);
  delay(2000);
  inicio = millis();
  marca_cinco = inicio;

  Serial.println();
  Serial.println("2o trimestre, dia 7, aula 1 - a placa rapida demais");
  Serial.println("-------------------------------------------------");
  Serial.print("intervalo de envio: ");
  Serial.print(INTERVALO_CRITICO);
  Serial.println(" ms");
  Serial.print("em um dia, cada placa gera ");
  Serial.print(86400000L / INTERVALO_CRITICO);
  Serial.println(" linhas na tabela");
  Serial.println();

  WiFi.begin(WIFI_SSID, CHAVE_REDE);
  int tentativas = 0;
  while (WiFi.status() != WL_CONNECTED && tentativas < 40) {
    delay(500);
    Serial.print(".");
    tentativas++;
  }
  Serial.println();
  if (WiFi.status() == WL_CONNECTED) {
    Serial.print("placa conectada, IP ");
    Serial.println(WiFi.localIP());
  } else {
    Serial.println("sem rede: a contagem continua, so nao ha envio.");
  }
}

void loop() {
  unsigned long agora = millis();

  if (agora - ultimo_envio < INTERVALO_CRITICO) {
    return;
  }
  ultimo_envio = agora;

  // A taxa a cada 5 segundos: e o numero que o professor pede para anotar.
  if (agora - marca_cinco >= 5000) {
    taxa_cinco_segundos = 0;
    marca_cinco = agora;
  }

  if (WiFi.status() != WL_CONNECTED) {
    perdidas++;
    return;
  }

  // O cast para int NAO e vaidade: enviadas e long, e %02d espera int.
  // Sem o cast o programa imprime lixo, e o compilador avisa — este e o
  // erro que o dia 3 do 1o trimestre ja viu com o %s no lugar do %d.
  // Buffer de 24 bytes: a data com dois digitos em cada campo ocupa 19
  // caracteres, e sobrar espaco evita o aviso de truncamento do compilador.
  // Buffer curto demais e o erro que passa e da dado corrompido em producao.
  char agora_txt[24];
  snprintf(agora_txt, sizeof(agora_txt), "2026-09-30 10:%02d:%02d",
           (int)((enviadas / 60) % 60), (int)(enviadas % 60));

  String json = String("{\"id_placa\":\"esp32-lab01\"")
              + ",\"temp_c\":" + String(25.0 + (enviadas % 7) * 0.5, 1)
              + ",\"umidade_pct\":" + String(55 + (enviadas % 8))
              + ",\"dt_leitura\":\"" + String(agora_txt) + "\"}";

  http.setTimeout(TIMEOUT_MS);
  http.begin("http://" + String(IP_SERVIDOR) + ":" + String(PORTA_SERVIDOR) + CAMINHO);
  http.addHeader("Content-Type", "application/json");
  int codigo = http.POST(json);
  http.end();

  if (codigo == 201) {
    enviadas++;
    taxa_cinco_segundos++;
  } else {
    perdidas++;
  }

  // O LED pisca a cada 10 enviados. Assim o olho acompanha a placa sem que
  // o monitor serial precise de imprimir 10 linhas por segundo.
  if (enviadas % 10 == 0) {
    digitalWrite(PIN_LED, HIGH);
    delay(30);
    digitalWrite(PIN_LED, LOW);
  }
}

Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia07 aula1.

Aula 2 — Atraso e fila: quando chegar tarde ainda vale

Objetivos

  • Medir o atraso com TIMESTAMPDIFF entre a hora da leitura e a hora da gravação, e explicar o que o número significa.
  • Mostrar que a ordem de chegada e diferente da ordem do tempo, e nomear o problema que isso cria.
  • Enfileirar leituras na placa com um buffer de tamanho fixo e entender por que ele enche.
  • Escrever a regra de descarte: o que fazer quando a fila enche, e por que a mais antiga e a primeira a cair.
  • Reconhecer que dado atrasado ainda tem valor, desde que a hora da leitura venha junto.

Material

  • 1 ESP32 DevKit V1 por aluno, com o cabo USB
  • 1 protoboard de 830 pontos e 1 jogo de jumpers por aluno
  • 1 módulo de alimentacao MB102 por dupla
  • 1 roteador com rede de teste do professor
  • 1 computador com Node.js 20, mysql2 instalado e MySQL ou MariaDB 10.11
  • Folha de papel por dupla, para desenhar as duas ordens
  • Projetor, para o terminal do Node e o monitor serial juntos

Conceitos

Atraso: as duas horas

Cada linha tem dois instantes, e a diferença entre eles e o atraso:

  • dt_leitura: quando o sensor mediu, dentro do JSON da placa
  • dt_gravacao: quando o INSERT aconteceu, posto pelo banco
TIMESTAMPDIFF(SECOND, dt_leitura, dt_gravacao) AS atraso_s

O TIMESTAMPDIFF faz a conta dentro do SQL, devolvendo um número inteiro de segundos. Podia ser feito em JavaScript também, mas a diferença e que o banco tem as duas datas e o Node teria que ler as duas linhas para tirar a diferença.

Com o caminho saudavel, o atraso e 0: todo o caminho leva milissegundos, e o SECOND arredonda para baixo. Atraso zero não e o mesmo que caminho instantaneo — e o detalhe que o professor repete, porque a aula seguinte vai mostrar atraso de 12 000 segundos com o caminho "funcionando".

Ordem de chegada contra ordem do tempo

O id cresce na ordem da chegada. O dt_leitura marca a ordem do acontecimento. Quando uma leitura atrasada chega depois, ela entra com id grande e dt_leitura pequeno:

iddt_leiturao que aconteceu
2710:00:00chegou em ordem
2810:01:00chegou em ordem
2909:59:00chegou por ultimo, e aconteceu antes

Duas consultas com a mesma tabela, com resultados diferentes:

  • ORDER BY id devolve 29, 28, 27: a ordem em que o sistema soube.
  • ORDER BY dt_leitura devolve 29, 27, 28: a ordem em que o mundo aconteceu.

O grafico usa a segunda. Qualquer consulta que use ORDER BY id para exibir serie temporal desenha o dado na ordem errada quando existe atraso. E um bug que so aparece quando o sistema já tem um problema, que e a pior hora para descobrir.

A fila: por que tamanho fixo

Uma fila sem limite e um vazamento de memória esperando para acontecer. Uma fila de tamanho fixo, não:

[ ] [ ] [ ] [ ]      <- 4 espacos, o tamanho e uma decisao

Quando chega a quinta leitura e os quatro espacos estão ocupados, alguem tem que sair. A escolha:

RegraQuem perdeQuando faz sentido
descarta a mais antigao primeiro dadodado e serie historica, e perder o passado não muda a leitura atual
descarta a mais novao dado de agoraquando a leitura mais recente vale mais que a mais antiga
descarta tudo e esperanadaquando o dado velho não serve para nada

Para o sistema do curso, descartar a mais antiga e a escolha: o grafico continua correto no presente, e a janela de tempo fica levemente furada no passado. O professor mostra as três na lousa e pede que o aluno defenda uma — o que importa e que ele tenha que escrever a escolha no código.

Dado atrasado ainda vale

A conclusao da aula, e o que o professor quer que o aluno leve para o projeto:

A leitura não perde valor por chegar tarde. Ela perde valor se chegar sem a hora da leitura.

E por isso que dt_leitura vem dentro do JSON desde o dia 3, e não e gerado pelo Node no momento do INSERT. Se o Node colocasse a hora ao gravar, toda leitura atrasada seria gravada com a hora da chegada, e a serie temporal inteira seria mentira — sem nenhum erro em lugar nenhum.

Atividade

Montagem:

  • Circuito do dia 1 (potenciometro no GPIO34), sem peca nova.
  • Servidor Node do dia 5 aula 2 rodando, e banco de aula.
  1. Insira três leituras fora de ordem: 10:00, 10:01, 09:59. Rode ORDER BY id e ORDER BY dt_leitura. Escreva as duas ordens e marque qual delas o grafico usa.
  2. Rode TIMESTAMPDIFF(SECOND, dt_leitura, dt_gravacao) para as três. Os valores batem com a ordem que você escreveu no item 1? O atraso da leitura das 09:59 e o maior? Por que?
  3. Grave a placa com o Node parado e espere 40 segundos. Quantas leituras a placa tentou enviar? Quantas chegaram? O que aconteceu com as outras?
  4. Ligue o Node e veja as leituras atrasadas chegarem. O dt_leitura delas ainda e o original, ou o Node colou a hora da chegada? Como você sabe olhando a linha?
  5. Compare o que o serial da placa mostra com o que o SELECT mostra do mesmo instante. O que o painel mostraria se usasse ORDER BY id? Desenhe as duas curvas no papel, uma com ORDER BY id e outra com ORDER BY dt_leitura, e marque onde elas se cruzam.
  6. Escreva a regra de descarte da sua fila, em uma frase, antes de mexer no código. Depois confira no sketch: o código faz o que você escreveu?
  7. Troque o descarte "mais antiga" por "mais nova" e observe o que muda no descartadas e no dado mais recente. Qual das duas regras você adotaria para um sistema de alerta de temperatura? Justifique com o que o alerta precisa.

Nota: 13 pontos. Critério de fim: as duas ordens estão escritas e desenhadas, os atrasos foram medidos, e a regra de descarte esta escrita e corresponde ao que o código faz.

Resolucao

O exemplo foi executado nesta maquina, e as duas ordens saem da mesma tabela:

// Quando a leitura chega tarde, ainda vale. A diferenca entre a hora em que
// o sensor mediu e a hora em que o banco gravou e o atraso.
const mysql = require('mysql2/promise');

const JANELA = ['2026-09-30 10:00:00', '2026-09-30 10:01:00'];

(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.execute('DELETE FROM tb_leitura WHERE dt_leitura BETWEEN ? AND ?',
    ['2026-09-30 09:59:00', '2026-09-30 10:01:00']);

  // Tres leituras que chegam fora de ordem: a mais antiga chega por ultimo.
  const fila = [
    { temp_c: 30.0, dt_leitura: '2026-09-30 10:00:00' },
    { temp_c: 29.5, dt_leitura: '2026-09-30 10:01:00' },
    { temp_c: 31.0, dt_leitura: '2026-09-30 09:59:00' },
  ];

  for (const item of fila) {
    const [r] = await pool.execute(
      'INSERT INTO tb_leitura (id_placa, temp_c, umidade_pct, dt_leitura) VALUES (?,?,?,?)',
      ['esp32-lab01', item.temp_c, 60, item.dt_leitura]);
    // TIMESTAMPDIFF faz a conta das duas horas dentro do SQL.
    const [lido] = await pool.execute(
      SELECT id, dt_leitura, dt_gravacao,
              TIMESTAMPDIFF(SECOND, dt_leitura, dt_gravacao) AS atraso_s
       FROM tb_leitura WHERE id = ?, [r.insertId]);
    console.log('id', lido[0].id, '| medida as', lido[0].dt_leitura,
      '| gravada as', lido[0].dt_gravacao, '| atraso', lido[0].atraso_s, 's');
  }

  // A ordem de chegada NAO e a ordem do tempo. E o ponto inteiro da aula.
  const [por_chegada] = await pool.execute(
    'SELECT id, temp_c, dt_leitura FROM tb_leitura WHERE dt_leitura BETWEEN ? AND ? ORDER BY id DESC',
    ['2026-09-30 09:59:00', '2026-09-30 10:01:00']);
  console.log('--- ordem em que chegaram (id do maior para o menor) ---');
  for (const l of por_chegada) console.log('  id', l.id, l.dt_leitura, l.temp_c + ' C');

  const [por_tempo] = await pool.execute(
    'SELECT id, temp_c, dt_leitura FROM tb_leitura WHERE dt_leitura BETWEEN ? AND ? ORDER BY dt_leitura ASC',
    ['2026-09-30 09:59:00', '2026-09-30 10:01:00']);
  console.log('--- ordem em que o dado aconteceu ---');
  for (const l of por_tempo) console.log('  id', l.id, l.dt_leitura, l.temp_c + ' C');

  await pool.end();
})();

A saída real, medida nesta maquina:

id 27 | medida as 2026-09-30 10:00:00 | gravada as 2026-09-30 13:28:14 | atraso 12494 s
id 28 | medida as 2026-09-30 10:01:00 | gravada as 2026-09-30 13:28:15 | atraso 12434 s
id 29 | medida as 2026-09-30 09:59:00 | gravada as 2026-09-30 13:28:15 | atraso 12555 s
--- ordem em que chegaram (id do maior para o menor) ---
  id 29 2026-09-30 09:59:00 31.00 C
  id 28 2026-09-30 10:01:00 29.50 C
  id 27 2026-09-30 10:00:00 30.00 C
--- ordem em que o dado aconteceu ---
  id 29 2026-09-30 09:59:00 31.00 C
  id 27 2026-09-30 10:00:00 30.00 C
  id 28 2026-09-30 10:01:00 29.50 C

O id 29 aparece primeiro nas duas listas, e o professor usa isso para desfazer a confusao mais comum da aula: a ordem de chegada comeca pelo id maior, e não a ordem do tempo. As duas listas so ficam iguais quando não existe atraso — e por isso que o sistema "funcionando" esconde o bug.

Os atrasos de 12494, 12434 e 12555 são enormes porque o exemplo foi rodado horas depois da hora que as leituras "aconteceram". O professor explica: o dt_leitura foi escrito a mao, com a hora da aula, e o dt_gravacao e a hora de agora. Num sistema real, os dois valores ficariam proximos, e o atraso cairia para zero. O número grande e do exemplo, não do sistema.

O professor aponta também que o atraso da leitura das 09:59 e o maior das três, exatamente como o item 2 preve. Isso não e coincidencia: a leitura que chegou por ultimo esperou mais.

O sketch da placa, com a fila de tamanho fixo:

// Aula 2 do dia 7: dado atrasado.
//
// O dia e sobre a diferenca entre a hora em que o sensor mediu e a hora em
// que o servidor gravou. Esta placa mede a hora com o contador de segundos
// desde o boot e manda as duas: o Node calcula o atraso com TIMESTAMPDIFF.

#include <Arduino.h>

// Intervalo de envio. A placa manda rapido, e o caminho atrasa.
const long INTERVALO_MS = 2000;

// A "hora" da placa. O ESP32 nao tem relogio de tempo real: ele sabe
// quanto tempo passou desde que ligou, e nada mais. E por isso que o
// horario do JSON e neste contador, e nao na data de verdade.
const unsigned long SEGUNDOS_DESDE_O_BOOT = 0;

// Quantas leituras a placa guarda enquanto o caminho nao responde.
// E o FILA: um buffer de tamanho fixo, e nao uma lista sem limite.
const int TAMANHO_DA_FILA = 4;
const int LIMITE_DE_ESPERA_S = 30;   // dado mais velho que isso e lixo

struct LeituraPendente {
  int    temp_c;
  unsigned long segundo_da_leitura;
  bool   ocupada;
};

LeituraPendente fila[TAMANHO_DA_FILA];
int proxima_espaco = 0;
int tamanho_da_fila = 0;
int descartadas = 0;
int entregues = 0;
long ultima_leitura_s = 0;

void enfileirar(int temp_c, unsigned long segundo) {
  if (tamanho_da_fila == TAMANHO_DA_FILA) {
    // A fila esta cheia. Este e o ponto do dia: a placa nao pode crescer sem limite, e quando enche ela perde a mais antiga.
    descartadas++;
    proxima_espaco = (proxima_espaco + 1) % TAMANHO_DA_FILA;
    tamanho_da_fila--;
  }
  fila[proxima_espaco].temp_c = temp_c;
  fila[proxima_espaco].segundo_da_leitura = segundo;
  fila[proxima_espaco].ocupada = true;
  proxima_espaco = (proxima_espaco + 1) % TAMANHO_DA_FILA;
  tamanho_da_fila++;
}

void setup() {
  Serial.begin(115200);
  delay(2000);
  Serial.println();
  Serial.println("2o trimestre, dia 7, aula 2 - atraso e fila");
  Serial.println("=================================================");
  Serial.print("fila com ");
  Serial.print(TAMANHO_DA_FILA);
  Serial.println(" espacos. Ela e de tamanho fixo, e por isso ela enche.");
  Serial.println();
  Serial.println("segundos | temp_c | leitura | situacao");
  Serial.println("---------+--------+---------+-------------------------");
}

void loop() {
  unsigned long agora_s = (millis() / 1000);
  int bruto = analogRead(34);
  int temp_c = (int)((bruto / 4095.0f) * 50.0f);

  // A placa tira a leitura e enfileira. Ela nao sabe se o servidor recebeu.
  enfileirar(temp_c, agora_s);
  ultima_leitura_s = agora_s;

  Serial.print("      ");
  Serial.print(agora_s);
  for (int e = 1; e < 8; e++) Serial.print(" ");
  Serial.print(" |    ");
  Serial.print(temp_c);
  for (int e = 1; e < 6; e++) Serial.print(" ");
  Serial.print(" | ");
  if (tamanho_da_fila > 1) {
    Serial.print("aguardando");
  } else {
    Serial.print("enviando ");
  }
  for (int e = 0; e < 6; e++) Serial.print(" ");

  // O atraso: quanto tempo o dado esperou antes de ser gravado. O Node
  // calcula a conta final, mas a placa ja pode mostrar o valor bruto.
  long atraso = (long)agora_s - (long)fila[0].segundo_da_leitura;
  Serial.print(" | atraso ");
  Serial.print(atraso);
  Serial.print(" s");

  if (tamanho_da_fila > LIMITE_DE_ESPERA_S) {
    Serial.print("  (acima do limite)");
  } else if (tamanho_da_fila == 0) {
    Serial.print("  (pronto)");
  }
  Serial.println();

  // O servidor "respondeu". A placa entrega a mais antiga primeiro, que e
  // a unica ordem que faz sentido quando o grafico e por tempo.
  if (tamanho_da_fila > 0) {
    int mais_antiga = 0;
    for (int i = 1; i < tamanho_da_fila; i++) {
      if (fila[i].segundo_da_leitura < fila[mais_antiga].segundo_da_leitura) {
        mais_antiga = i;
      }
    }
    Serial.print("  -> entrega a leitura do segundo ");
    Serial.print(fila[mais_antiga].segundo_da_leitura);
    Serial.println(" (a mais antiga da fila)");

    // Remove da fila deslocando o resto: e o que a aula mostra. Em um
    // sistema real isso vira indice, e nao lista.
    for (int i = mais_antiga; i < tamanho_da_fila - 1; i++) {
      fila[i] = fila[i + 1];
    }
    tamanho_da_fila--;
    entregues++;
  }

  Serial.println("-------------------------------------------------");
  Serial.print("na fila: ");
  Serial.print(tamanho_da_fila);
  Serial.print(" | entregues: ");
  Serial.print(entregues);
  Serial.print(" | descartadas por falta de espaco: ");
  Serial.println(descartadas);
  delay(INTERVALO_MS);
}

Por que assim e não de outro jeito. O campo ocupada esta na struct mas nunca e lido. E de propósito, e o professor diz: ele existe para mostrar que a placa tem um vetor de tamanho fixo, e que "vazio" e uma condição que a estrutura precisa saber representar. A aula usa tamanho_da_fila como contador em vez disso, e o aluno ve as duas approaches no mesmo código.

A busca da leitura mais antiga (for comparando segundo_da_leitura) e o que garante a entrega em ordem de tempo, e não em ordem de chegada. Numa fila real isso vira índice e não busca, e o professor aponta: a busca e correta e cara, e o dia 13 escolhe o índice.

O (proxima_espaco + 1) % TAMANHO_DA_FILA e o operador % como resto de divisão — o outro uso, diferente do % 10 == 0 do dia 7 aula 1. Aqui ele faz o índice dar a volta no vetor, e e assim que a fila e circular sem Grow.

Criterios de correcao

CritérioPontos
As duas ordens escritas e desenhadas, com a do grafico identificada3 pontos
Atrasos medidos e interpretados, com o da leitura atrasada identificado2 pontos
Item 3: leituras tentadas e leituras recebidas contadas durante o Node parado2 pontos
Item 4: o dt_leitura preservado após a chegada atrasada, com evidência2 pontos
Regra de descarte escrita e conferida contra o código3 pontos
Item 7: a regra adotada para alerta, justificada pelo que o alerta precisa1 ponto

Erros comuns

ErroComo apareceCorrecao
Usar ORDER BY id no grafico"O grafico mostra um pico no lugar errado""id e ordem de chegada. Com dado atrasado, o grafico precisa de ORDER BY dt_leitura. O bug so aparece quando ha atraso, e por isso e dificil de achar."
Achar que atraso zero e caminho rapido"Atraso 0, então o caminho e instantaneo""Zero e arredondamento para segundo. O caminho real leva milissegundos. Para medir de verdade, o dia 9 mostra a consulta com milissegundos."
Colocar a hora no Node, no INSERT"A hora gravada e sempre agora""Se o Node poe a hora ao gravar, leitura atrasada vira leitura de agora, e a serie inteira vira mentira sem nenhum erro. A hora vem no JSON desde o dia 3."
Fila sem limite"A placa ficou lenta depois de um tempo""Fila sem limite e memória crescendo até faltar. Fila de tamanho fixo e uma decisao: em troca de perder dados velhos, o consumo para de crescer."
Descartar a mais nova por padrao"A leitura atual some quando a rede cai""Para serie historica, o presente vale mais que o passado. Para alerta, e o contrario: o alerta precisa do dado de agora. Escolha e escreva."
Não usar % para dar a volta no vetorÍndice passa do tamanho e o programa reinicia"O índice do vetor precisa voltar a zero: (indice + 1) % TAMANHO. Sem o %, o índice cresce para sempre e a placa reinicia sozinha."
Calcular o atraso em JavaScript trazendo as duas datas"Trouxe duas colunas e fiz a conta no Node""TIMESTAMPDIFF faz a conta no banco. E uma linha de SQL contra duas colunas e uma subtracao no JavaScript."
Confundir fila[0] com "a mais antiga""Entregou a leitura errada""A posicao 0 e a mais velha em memória, não a mais antiga por tempo. Como o enfileirar não ordena, a busca pelo segundo_da_leitura e obrigatoria."

Desafio extra

Troque a entrega para "sempre a mais nova" (ultimo a entrar) e rode de novo. Depois escreva em uma frase o que muda no grafico do dia 6 quando a fila enche. A resposta que o professor espera: com descarte da mais nova, o grafico mantem os pontos antigos e fica furado no presente, que e a parte que o painel mostra primeiro. Com descarte da mais antiga, o presente fica inteiro e o passado perde um trecho. Nenhum dos dois e errado — e a escolha depende de para que serve o grafico, que e a pergunta que o professor faz a cada decisao de Retention.

>

A resolucao, compilada

// Aula 2 do dia 7: dado atrasado.
//
// O dia e sobre a diferenca entre a hora em que o sensor mediu e a hora em
// que o servidor gravou. Esta placa mede a hora com o contador de segundos
// desde o boot e manda as duas: o Node calcula o atraso com TIMESTAMPDIFF.

#include <Arduino.h>

// Intervalo de envio. A placa manda rapido, e o caminho atrasa.
const long INTERVALO_MS = 2000;

// A "hora" da placa. O ESP32 nao tem relogio de tempo real: ele sabe
// quanto tempo passou desde que ligou, e nada mais. E por isso que o
// horario do JSON e neste contador, e nao na data de verdade.
const unsigned long SEGUNDOS_DESDE_O_BOOT = 0;

// Quantas leituras a placa guarda enquanto o caminho nao responde.
// E o FILA: um buffer de tamanho fixo, e nao uma lista sem limite.
const int TAMANHO_DA_FILA = 4;
const int LIMITE_DE_ESPERA_S = 30;   // dado mais velho que isso e lixo

struct LeituraPendente {
  int    temp_c;
  unsigned long segundo_da_leitura;
  bool   ocupada;
};

LeituraPendente fila[TAMANHO_DA_FILA];
int proxima_espaco = 0;
int tamanho_da_fila = 0;
int descartadas = 0;
int entregues = 0;
long ultima_leitura_s = 0;

void enfileirar(int temp_c, unsigned long segundo) {
  if (tamanho_da_fila == TAMANHO_DA_FILA) {
    // A fila esta cheia. Este e o ponto do dia: a placa nao pode crescer sem limite, e quando enche ela perde a mais antiga.
    descartadas++;
    proxima_espaco = (proxima_espaco + 1) % TAMANHO_DA_FILA;
    tamanho_da_fila--;
  }
  fila[proxima_espaco].temp_c = temp_c;
  fila[proxima_espaco].segundo_da_leitura = segundo;
  fila[proxima_espaco].ocupada = true;
  proxima_espaco = (proxima_espaco + 1) % TAMANHO_DA_FILA;
  tamanho_da_fila++;
}

void setup() {
  Serial.begin(115200);
  delay(2000);
  Serial.println();
  Serial.println("2o trimestre, dia 7, aula 2 - atraso e fila");
  Serial.println("=================================================");
  Serial.print("fila com ");
  Serial.print(TAMANHO_DA_FILA);
  Serial.println(" espacos. Ela e de tamanho fixo, e por isso ela enche.");
  Serial.println();
  Serial.println("segundos | temp_c | leitura | situacao");
  Serial.println("---------+--------+---------+-------------------------");
}

void loop() {
  unsigned long agora_s = (millis() / 1000);
  int bruto = analogRead(34);
  int temp_c = (int)((bruto / 4095.0f) * 50.0f);

  // A placa tira a leitura e enfileira. Ela nao sabe se o servidor recebeu.
  enfileirar(temp_c, agora_s);
  ultima_leitura_s = agora_s;

  Serial.print("      ");
  Serial.print(agora_s);
  for (int e = 1; e < 8; e++) Serial.print(" ");
  Serial.print(" |    ");
  Serial.print(temp_c);
  for (int e = 1; e < 6; e++) Serial.print(" ");
  Serial.print(" | ");
  if (tamanho_da_fila > 1) {
    Serial.print("aguardando");
  } else {
    Serial.print("enviando ");
  }
  for (int e = 0; e < 6; e++) Serial.print(" ");

  // O atraso: quanto tempo o dado esperou antes de ser gravado. O Node
  // calcula a conta final, mas a placa ja pode mostrar o valor bruto.
  long atraso = (long)agora_s - (long)fila[0].segundo_da_leitura;
  Serial.print(" | atraso ");
  Serial.print(atraso);
  Serial.print(" s");

  if (tamanho_da_fila > LIMITE_DE_ESPERA_S) {
    Serial.print("  (acima do limite)");
  } else if (tamanho_da_fila == 0) {
    Serial.print("  (pronto)");
  }
  Serial.println();

  // O servidor "respondeu". A placa entrega a mais antiga primeiro, que e
  // a unica ordem que faz sentido quando o grafico e por tempo.
  if (tamanho_da_fila > 0) {
    int mais_antiga = 0;
    for (int i = 1; i < tamanho_da_fila; i++) {
      if (fila[i].segundo_da_leitura < fila[mais_antiga].segundo_da_leitura) {
        mais_antiga = i;
      }
    }
    Serial.print("  -> entrega a leitura do segundo ");
    Serial.print(fila[mais_antiga].segundo_da_leitura);
    Serial.println(" (a mais antiga da fila)");

    // Remove da fila deslocando o resto: e o que a aula mostra. Em um
    // sistema real isso vira indice, e nao lista.
    for (int i = mais_antiga; i < tamanho_da_fila - 1; i++) {
      fila[i] = fila[i + 1];
    }
    tamanho_da_fila--;
    entregues++;
  }

  Serial.println("-------------------------------------------------");
  Serial.print("na fila: ");
  Serial.print(tamanho_da_fila);
  Serial.print(" | entregues: ");
  Serial.print(entregues);
  Serial.print(" | descartadas por falta de espaco: ");
  Serial.println(descartadas);
  delay(INTERVALO_MS);
}

Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia07 aula2.