Intervalos, atrasos e a placa que não responde — Arduino e IoT — semana 7 do 2o trimestre
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
DELETEcom filtro proprio, idempotente. - Medir a taxa de gravação real de um servidor com a placa mandando rapido demais.
- Explicar por que o
intervalode 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,
mysql2instalado 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
| Intervalo | Linhas por dia | Linhas em 7 dias |
|---|---|---|
| 1 s | 86 400 | 604 800 |
| 2 s | 43 200 | 302 400 |
| 10 s | 8 640 | 60 480 |
| 60 s | 1 440 | 10 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:
- quanto tempo guardar — 7 dias? 30? um mes?
- quando executar — a cada hora, no primeiro
INSERTdo dia, por agendamento? - o que apagar — por data, nunca por
id:idnã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.
- 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.
- Meça o tamanho real: rode a consulta de
information_schemae anoteTABLE_ROWS,DATA_LENGTHeINDEX_LENGTH. Quanto do espaco total e índice? - Grave a placa com
INTERVALO_CRITICO = 500e deixe rodando 60 segundos. Depois, com o Node parado, e conte quantas respostas201a placa recebeu. Compare com a taxa teorica de 2 por segundo. Qual foi a taxa real? - Explique em uma frase por que a taxa real foi menor. O que a placa estava fazendo enquanto o
500 mspassava? - Escreva a politica de retencao da sua placa: quantos dias guardar, e quando apagar. Depois escreva o
DELETEcorrespondente, com parametro. - Rode o
DELETEduas vezes seguidas. Quantas linhas foram apagadas na primeira, e quantas na segunda? Por que a segunda e zero? - 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 ve0e precisa entender por que. E o melhor item da atividade: umDELETEque apaga zero pode estar certo ou pode estar com o filtro errado, e so a conferida do filtro separa os dois casos. DIAS_QUE_QUEREMOSesta 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ério | Pontos |
|---|---|
| Tabela de linhas por dia para os quatro intervalos, com a escolha justificada por número | 3 pontos |
| Tamanho real da tabela medido, com a proporcao de índice anotada | 2 pontos |
| Taxa teorica e taxa real medidas e comparadas | 3 pontos |
| Politica de retencao escrita, com dias e momento do apagamento | 2 pontos |
DELETE com filtro proprio e parametro | 1 ponto |
| Item 6: as duas contagens de linhas apagadas, e a explicacao do zero | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
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 medir | Item 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
TIMESTAMPDIFFentre 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,
mysql2instalado 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 placadt_gravacao: quando oINSERTaconteceu, 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:
| id | dt_leitura | o que aconteceu |
|---|---|---|
| 27 | 10:00:00 | chegou em ordem |
| 28 | 10:01:00 | chegou em ordem |
| 29 | 09:59:00 | chegou por ultimo, e aconteceu antes |
Duas consultas com a mesma tabela, com resultados diferentes:
ORDER BY iddevolve 29, 28, 27: a ordem em que o sistema soube.ORDER BY dt_leituradevolve 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:
| Regra | Quem perde | Quando faz sentido |
|---|---|---|
| descarta a mais antiga | o primeiro dado | dado e serie historica, e perder o passado não muda a leitura atual |
| descarta a mais nova | o dado de agora | quando a leitura mais recente vale mais que a mais antiga |
| descarta tudo e espera | nada | quando 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.
- Insira três leituras fora de ordem: 10:00, 10:01, 09:59. Rode
ORDER BY ideORDER BY dt_leitura. Escreva as duas ordens e marque qual delas o grafico usa. - 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? - 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?
- Ligue o Node e veja as leituras atrasadas chegarem. O
dt_leituradelas ainda e o original, ou o Node colou a hora da chegada? Como você sabe olhando a linha? - Compare o que o serial da placa mostra com o que o
SELECTmostra do mesmo instante. O que o painel mostraria se usasseORDER BY id? Desenhe as duas curvas no papel, uma comORDER BY ide outra comORDER BY dt_leitura, e marque onde elas se cruzam. - 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?
- Troque o descarte "mais antiga" por "mais nova" e observe o que muda no
descartadase 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ério | Pontos |
|---|---|
| As duas ordens escritas e desenhadas, com a do grafico identificada | 3 pontos |
| Atrasos medidos e interpretados, com o da leitura atrasada identificado | 2 pontos |
| Item 3: leituras tentadas e leituras recebidas contadas durante o Node parado | 2 pontos |
Item 4: o dt_leitura preservado após a chegada atrasada, com evidência | 2 pontos |
| Regra de descarte escrita e conferida contra o código | 3 pontos |
| Item 7: a regra adotada para alerta, justificada pelo que o alerta precisa | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
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.
