Node com MySQL: o caminho fechado — Arduino e IoT — semana 5 do 2o trimestre
Semana 5 de 16· 2o trimestre · 01/05 a 04/09
Node com MySQL: o caminho fechado
A placa envia, o Node recebe, o banco guarda. Fim a fim.
Aula 1 — Conectar o Node no MySQL e gravar a leitura
Objetivos
- Conectar o Node no MySQL com
mysql2/promiseecreatePool, dizendo o que e um pool. - Ler credencial de variável de ambiente com
process.enve explicar por que ela não vai no código nem no git. - Gravar uma leitura com
pool.executee conferir a gravação com umSELECTpeloinsertId. - Reconhecer
ECONNREFUSEDeER_NO_SUCH_TABLEpeloerro.code, e dizer o que cada um significa. - Afirmar, com um exemplo, quem guarda a senha: a placa ou o Node.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1 protoboard de 830 pontos por dupla
- Computador com Node.js 20,
mysql2instalado e MySQL ou MariaDB 10.11 - Arquivo
.envcom as credenciais do banco de aula, e.gitignorejá configurado - Folha de papel por dupla, para o desenho de quem fala com quem
- Projetor, para o terminal do professor
Conceitos
createPool: por que um pool e não uma conexão
const pool = mysql.createPool({ host, user, password, database });
Uma conexão MySQL e cara de abrir: tem handshake, autenticação e negotiation. Abrir e fechar a cada leitura de 2 segundos seria desperdicio. O pool e um conjunto de conexões já abertas, que o Node empresta conforme a necessidade e devolve depois.
O ganho aparece na primeira requisicao e cresce depois: com pool, a consulta leva milissegundos; abrindo conexão toda vez, leva dezenas de milissegundos. No dia 7, quando a placa manda rapido demais, essa diferença aparece no tempo de resposta.
O await pool.end() no fim fecha o pool. Sem ele, o processo fica vivo e o terminal parece travado — o mesmo sintoma do servidor.close() do dia 2.
A credencial e o .env
host: process.env.DB_HOST,
password: process.env.DB_PASS,
O .env e um arquivo de texto com uma variável por linha, no formato NOME=valor. Ele não vai para o git, porque contem a senha do banco. O .gitignore e o que garante isso, e o dia 11 trata o assunto inteiro.
O que o material de aula pode mostrar e o nome da variável, nunca o valor. A distincao que o professor faz na lousa:
| Pode no material | Não pode no material |
|---|---|
DB_PASS | o valor que esta depois do = |
process.env.DB_PASS | a senha do banco da escola |
| "a senha vem do ambiente" | uma senha de exemplo que pareca real |
O Node 20 ou superior carrega o .env direto da linha de comando, com node --env-file=.env. O dotenv faz o mesmo a partir de dentro do código; as duas formas servem, e o dia 11 mostra quando usar cada uma.
O erro.code e o que ele diz
Em JavaScript, catch recebe um objeto de erro que tem duas propriedades uteis: message, que e texto para o humano, e code, que e o identificador que o programa pode comparar.
erro.code | O que aconteceu | O que fazer |
|---|---|---|
ECONNREFUSED | o banco não esta no endereco | suba o banco, confira host e porta |
ER_NO_SUCH_TABLE | conectou, mas a tabela não existe | rode o CREATE TABLE do dia 4 |
ER_ACCESS_DENIED_ERROR | usuario ou senha errados | confira o .env |
ER_DUP_ENTRY | violou chave unica | o dia 9 trata chave de idempotencia |
O erro mais caro do dia e o ER_NO_SUCH_TABLE, porque ele não parece erro de banco: parece erro de SQL, e o aluno reescreve a consulta cinco vezes antes de olhar o schema.
Atividade
Montagem: nenhuma. A placa fica com o circuito do dia 1. Ela não entra no WiFi hoje: o foco e o Node falando com o banco.
- Desenhe o caminho completo em três caixas: placa, Node, banco. Escreva em cada seta o que viaja: de placa para Node e de Node para banco. Marque, na seta Node para banco, onde esta a senha.
- Crie o pool com as quatro variáveis do
.env. RodeSELECT VERSION()e anote a versão que o banco respondeu (não escreva versão de memória no código). - Grave uma leitura e anote o
insertId. imediatamente depois, facaSELECTpor esseide confirme que a linha la esta. Otemp_cvoltou com quantas casas decimais? - Insira uma segunda leitura com
temp_cmenor que a primeira e facaSELECTde tudo semWHERE. O banco devolveu em ordem de insercao ou de temperatura? - Pare o banco e rode o mesmo script. Anote o
erro.codeexato que apareceu. Ele esta nomessage, nocode, ou nos dois? - Com o banco parado, deite a senha escrita no código em vez de ler do
.env. Compare o tempo que o Node leva para falhar nessa situacao e na anterior. O que a diferença de tempo diz sobre quem verifica a senha?
Nota: 11 pontos. Critério de fim: existe uma linha gravada e lida de volta com o insertId, e o item 5 tem o erro.code exato anotado.
Resolucao
O exemplo foi executado nesta maquina, com o banco de aula:
// Conectar o Node no MySQL e gravar uma leitura.
// A credencial NAO esta no codigo: vem de variavel de ambiente, que o
// arquivo .env carrega antes do programa comecar.
const mysql = require('mysql2/promise');
(async () => {
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASS,
database: process.env.DB_NAME,
dateStrings: true,
});
// O nome da variavel pode aparecer no material. O valor dela, nunca.
console.log('conectando em', process.env.DB_HOST + ':' + process.env.DB_PORT,
'| usuario', process.env.DB_USER, '| banco', process.env.DB_NAME);
const [versao] = await pool.query('SELECT VERSION() AS versao');
console.log('banco respondeu:', versao[0].versao);
const [r] = await pool.execute(
'INSERT INTO tb_leitura (id_placa, temp_c, umidade_pct, dt_leitura) VALUES (?, ?, ?, ?)',
['esp32-lab01', 26.8, 60, '2026-09-30 10:15:00']
);
console.log('linha gravada com id', r.insertId, '| affectedRows', r.affectedRows);
const [confere] = await pool.execute(
'SELECT id, temp_c FROM tb_leitura WHERE id = ?', [r.insertId]);
console.log('lendo de volta:', confere[0].id, confere[0].temp_c + ' C');
await pool.end();
})();
A saída real, medida nesta maquina:
conectando em 127.0.0.1:3306 | usuario materiais | banco materiais_teste banco respondeu: 10.11.14-MariaDB-0ubuntu0.24.04.1 linha gravada com id 4 | affectedRows 1 lendo de volta: 4 26.80 C
O professor não escreve 10.11.14-MariaDB no código. O script pergunta ao banco e imprime a resposta, e o valor que aparece na tela depende da maquina em que o aluno roda. Um material que afirma a versão esta mentindo para quem rodar em outro computador.
Os três números da ultima linha são os três critérios do dia: id existe, temp_c voltou com as duas casas do DECIMAL(5,2), e a linha e a mesma que foi gravada.
O sketch da placa responde ao item 1 — quem guarda a senha:
// Aula 1 do dia 5: o Node no MySQL, visto do lado da placa. // // O professor sobe o script de Node, mostra a linha gravada no banco, e esta // placa mostra o que ela precisa mandar para que essa linha exista. A placa // nao conhece o MySQL, e nao precisa: ela so preenche os campos do INSERT. #include <Arduino.h> const int PIN_SENSOR_ANALOGICO = 34; const int RESOLUCAO_ADC = 4095; const int FUSO_HORAS = -3; // As credenciais do banco NAO moram na placa, e o professor repete isso // tres vezes nesta aula. A placa fala HTTP com o Node; quem fala com o // MySQL e o Node, e e ele que guarda a senha. Se a senha estivesse aqui, // bastava alguém ler a placa para ter o banco. const char* ID_PLANCA = "esp32-lab01"; void setup() { Serial.begin(115200); delay(2000); Serial.println(); Serial.println("2o trimestre, dia 5, aula 1 - quem fala com quem"); Serial.println("================================================="); Serial.println(" placa --HTTP--> Node --SQL--> MySQL"); Serial.println(); Serial.println("a placa tem o IP do Node e nada mais."); Serial.println("o Node tem a senha do MySQL e nada mais."); Serial.println("quem segura a senha e o unico que fala com o banco."); Serial.println(); Serial.println("os cinco campos que a placa preenche, e o que cada um"); Serial.println("vira no INSERT do Node:"); Serial.println(" id_placa -> id_placa VARCHAR(32)"); Serial.println(" temp_c -> temp_c DECIMAL(5,2)"); Serial.println(" umidade_pct -> umidade_pct DECIMAL(5,2)"); Serial.println(" fuso -> (nao vira coluna: o Node soma o fuso)"); Serial.println(" dt_leitura -> dt_leitura DATETIME"); } void loop() { int bruto = analogRead(PIN_SENSOR_ANALOGICO); float temp_c = (bruto / 4095.0f) * 50.0f; int umidade_pct = 55 + (bruto % 12); // Este e o INSERT, escrito em C++. Os nomes dos campos sao os mesmos do // SQL do dia 4, e e por isso que os dois lados batem. char dt_leitura[24]; snprintf(dt_leitura, sizeof(dt_leitura), "2026-09-30 10:00:00"); Serial.println("-------------------------------------------------"); Serial.print("INSERT INTO tb_leitura (id_placa, temp_c, umidade_pct, dt_leitura)"); Serial.println(); Serial.print("VALUES ('"); Serial.print(ID_PLANCA); Serial.print("', "); Serial.print(temp_c, 1); Serial.print(", "); Serial.print(umidade_pct); Serial.print(", '"); Serial.print(dt_leitura); Serial.println("');"); Serial.println(); Serial.println("na placa isso vira POST /leitura, com JSON. No Node,"); Serial.println("o JSON vira parametro do mesmo INSERT. O dia 5 aula 2"); Serial.println("fecha o caminho e mostra a linha aparecendo no banco."); delay(2000); }
Por que assim e não de outro jeito. A frase "quem segura a senha e o unico que fala com o banco" esta no sketch e no texto. Não e repeticao: o sketch e o que fica gravado na placa do aluno, e ele vai volver a essa tela no dia 9, quando a autenticação entra. Um aluno que leu isso uma vez na lousa esquece; um aluno que tem a frase gravada na placa dela no ano inteiro.
O fuso aparece no sketch e não vira coluna. Isso e uma decisao, não um esquecimento: o fuso e propriedade da maquina que rodou o Node, e não do sensor. Guardar o fuso da placa no banco mistura duas referencias de tempo diferentes. O professor explica isso no dia 11, quando a maquina muda.
Criterios de correcao
| Critério | Pontos |
|---|---|
| Desenho com as três caixas, o conteudo de cada seta e o local da senha | 2 pontos |
| Pool criado com as quatro variáveis de ambiente, sem senha escrita no código | 2 pontos |
| Versão do banco obtida por consulta, e não escrita no código | 1 ponto |
Leitura gravada, insertId anotado e SELECT de confirmacao executado | 3 pontos |
Item 5: erro.code exato anotado, e dito de onde ele vem | 2 pontos |
| Item 6: o que o tempo de falha diz sobre quem verifica a senha | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
Escrever a senha direto no createPool | o createPool tem password: '...' escrito no arquivo | "A senha vem de process.env.DB_PASS. O valor não vai no código nem no git — e não por prudencia abstrata, mas porque o dia 9 mostra que ele vai sair da maquina." |
Escrever MySQL 8.0 no console.log | "Banco em uso: MySQL 8.0" | "Pergunte ao banco com SELECT VERSION() e imprima a resposta. A maquina do aluno pode ter outra versão, e o material não pode afirmar a dele." |
Criar o pool sem await pool.end() e achar que travou | "O script não volta pro prompt" | "O pool tem conexões abertas e o processo não morre. Feche com await pool.end() no fim." |
Confundir query com execute e passar parametros no query | "O ? não foi preenchido" | "query não usa parametro. Com parametro, e execute." |
Ler o erro.message e tentar decidir em cima dele | "A mensagem diz ECONNREFUSED mas eu comparo com string" | "A message e para o humano e muda de versão em versão. O code e estavel. Compare sempre com o code." |
Achar que o banco recusou por causa do dt_gravacao | "Inseri sem essa coluna e deu erro" | "A coluna tem DEFAULT CURRENT_TIMESTAMP. O erro de INSERT aqui e outro: leia o erro.code antes de supor." |
| Dizer que a placa precisa da senha do banco | Item 1 com a senha na seta da placa | "A placa fala HTTP. Ela nunca chega perto do banco. Quem fala SQL e o Node, e e o Node que tem a senha." |
Rodar o script sem o --env-file e ver undefined no host | "Conectando em undefined:undefined" | "Sem o .env carregado, process.env.DB_HOST e undefined. Rode com node --env-file=.env script.js." |
Desafio extra
Faca o script que mostra SELECT COUNT(*) de cada placa, agrupado por id_placa, ordenado da que tem mais leituras para a que tem menos. Depois apague todas as linhas de uma das placas e rode de novo. Escreva em uma frase o que a contagem por placa permite decidir que a contagem total não permite. A resposta: ela mostra se o sistema esta com todas as placas vivas ou se uma delas esta muda faz tempo — que e a primeira versão de um alerta, e o dia 13 transforma em painel.
A resolucao, compilada
// Aula 1 do dia 5: o Node no MySQL, visto do lado da placa. // // O professor sobe o script de Node, mostra a linha gravada no banco, e esta // placa mostra o que ela precisa mandar para que essa linha exista. A placa // nao conhece o MySQL, e nao precisa: ela so preenche os campos do INSERT. #include <Arduino.h> const int PIN_SENSOR_ANALOGICO = 34; const int RESOLUCAO_ADC = 4095; const int FUSO_HORAS = -3; // As credenciais do banco NAO moram na placa, e o professor repete isso // tres vezes nesta aula. A placa fala HTTP com o Node; quem fala com o // MySQL e o Node, e e ele que guarda a senha. Se a senha estivesse aqui, // bastava alguém ler a placa para ter o banco. const char* ID_PLANCA = "esp32-lab01"; void setup() { Serial.begin(115200); delay(2000); Serial.println(); Serial.println("2o trimestre, dia 5, aula 1 - quem fala com quem"); Serial.println("================================================="); Serial.println(" placa --HTTP--> Node --SQL--> MySQL"); Serial.println(); Serial.println("a placa tem o IP do Node e nada mais."); Serial.println("o Node tem a senha do MySQL e nada mais."); Serial.println("quem segura a senha e o unico que fala com o banco."); Serial.println(); Serial.println("os cinco campos que a placa preenche, e o que cada um"); Serial.println("vira no INSERT do Node:"); Serial.println(" id_placa -> id_placa VARCHAR(32)"); Serial.println(" temp_c -> temp_c DECIMAL(5,2)"); Serial.println(" umidade_pct -> umidade_pct DECIMAL(5,2)"); Serial.println(" fuso -> (nao vira coluna: o Node soma o fuso)"); Serial.println(" dt_leitura -> dt_leitura DATETIME"); } void loop() { int bruto = analogRead(PIN_SENSOR_ANALOGICO); float temp_c = (bruto / 4095.0f) * 50.0f; int umidade_pct = 55 + (bruto % 12); // Este e o INSERT, escrito em C++. Os nomes dos campos sao os mesmos do // SQL do dia 4, e e por isso que os dois lados batem. char dt_leitura[24]; snprintf(dt_leitura, sizeof(dt_leitura), "2026-09-30 10:00:00"); Serial.println("-------------------------------------------------"); Serial.print("INSERT INTO tb_leitura (id_placa, temp_c, umidade_pct, dt_leitura)"); Serial.println(); Serial.print("VALUES ('"); Serial.print(ID_PLANCA); Serial.print("', "); Serial.print(temp_c, 1); Serial.print(", "); Serial.print(umidade_pct); Serial.print(", '"); Serial.print(dt_leitura); Serial.println("');"); Serial.println(); Serial.println("na placa isso vira POST /leitura, com JSON. No Node,"); Serial.println("o JSON vira parametro do mesmo INSERT. O dia 5 aula 2"); Serial.println("fecha o caminho e mostra a linha aparecendo no banco."); delay(2000); }
Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia05 aula1.
Aula 2 — Receber do ESP32 e gravar no banco: ponta a ponta
Objetivos
- Fechar o caminho completo: placa envia, Node recebe, banco grava, e o
SELECTconfirma. - Escrever um log por etapa do caminho e usá-lo para dizer em que ponto o dado parou.
- Medir o tempo entre a leitura da placa e a gravação no banco, e explicar por que ele existe.
- Reconhecer o erro de nome de campo e de tipo, e dizer qual dos dois lados tem a culpa.
- Fazer o ciclo completo acontecer na frente da turma e dizer, em voz alta, o que apareceu no banco.
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 - Projetor, para o terminal do Node e o monitor serial ao mesmo tempo
- Folha de papel por dupla, para anotar o ciclo observado
Conceitos
O caminho fechado tem quatro provas
Uma leitura que percorreu o caminho inteiro deixa quatro rastros, em três lugares diferentes:
| # | Rastro | Onde aparece |
|---|---|---|
| 1 | o JSON que a placa montou | monitor serial |
| 2 | o status 201 que voltou | monitor serial |
| 3 | o log [3] gravada | terminal do Node |
| 4 | a linha com o id | tabela do banco |
O [3] e o que o professor.projects na tela: ele vem do console.log que esta entre o JSON.parse e o execute. Sem esse log, "o dado não chegou" e um palpite; com ele, o aluno olha e ve que chegou, foi interpretado, e parou antes do banco.
O try/catch do caminho inteiro
req.on('end', async () => {
try {
const leitura = JSON.parse(corpo);
const [r] = await pool.execute(sql, [/* ... */]);
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ id: r.insertId, gravado: true }));
} catch (erro) {
console.error('falha:', erro.code, erro.message);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ erro: 'nao consegui gravar' }));
}
});
O try existe para que um dado ruim não derrube o servidor inteiro. Sem ele, uma excepciona no JSON.parse sai do callback, o processo do Node morre, e a placa comeca a receber código de rede negativo — que e o dia 3 aula 2 de novo, agora por um motivo novo.
O 500 na resposta e o que a placa le no dia 3 aula 2 como "pode tentar de novo depois". O 400 seria "seu pedido esta errado, não adianta reenviar".
O atraso: a diferença entre as duas horas
Cada linha tem dois instantes: dt_leitura (a placa mediu) e dt_gravacao (o banco gravou). O banco calcula a diferença:
TIMESTAMPDIFF(SECOND, dt_leitura, dt_gravacao) AS atraso_s
Com o caminho saudavel, o atraso e o tempo de uma ida e volta pela rede local: alguns milissegundos, arredondado para segundos, dando 0. O que interessa não e o valor — e que ele existe e pode ser medido. Quando o dia 7 tornar a fila grande, esse número deixa de ser 0 e vira o indicador que mostra que o caminho esta atrasado.
Atividade
Montagem:
- Circuito do dia 1 (potenciometro no
GPIO34), sem peca nova. - Confirme que placa, computador do Node e banco estão todos alcancaveis. O IP do servidor e o do computador, não o do roteador.
- Suba o servidor, o banco e o sketch. Deixe rodando por 30 segundos. Anote quantas linhas apareceram no banco.
- Pare so o Node, sem mexer na placa. Espere 10 segundos e veja o serial. O código e positivo ou negativo? Em qual das quatro provas do caminho o rastro parou?
- Ligue o Node de novo. Quanto tempo o serial demora para voltar a mostrar
201? Esse tempo e o atraso do caminho? - Troque, no sketch, o nome do campo
temp_cparatemperatura. Grave e espere uma leitura. O que o terminal do Node mostra? Oerro.codee o que? Quem esta errado: a placa ou o Node? - Volte o nome para
temp_c, mas mande o valor de temperatura como texto:String("26.5")com aspas. O que o Node faz agora? O erro e o mesmo do item 4? Por que não? - Rode o
SELECTcomTIMESTAMPDIFF(SECOND, dt_leitura, dt_gravacao)para as ultimas 5 leituras. Escreva os valores. E zero? Se não for, o que você acha que houve entre a placa e o banco? - Escreva, no caderno, a frase que resume a aula: "o caminho fecha quando ______ e o primeiro sinal de que não fechou e ______".
Nota: 14 pontos. Critério de fim: o ciclo completo rodou e as quatro provas aparecem na tela ao mesmo tempo, e o item 4 foi feito de propósito, com o erro.code anotado.
Resolucao
O exemplo completo, executado nesta maquina. O POST de fetch imita, campo por campo, o que a placa faz com HTTPClient:
// O caminho fechado: a placa envia, o Node recebe, o banco guarda.
// O POST de fetch imita, campo por campo, o que a placa faz com HTTPClient.
const http = require('http');
const mysql = require('mysql2/promise');
const servidor = http.createServer((req, res) => {
if (req.method === 'POST' && req.url === '/leitura') {
let corpo = '';
req.on('data', (p) => { corpo += p.toString(); });
req.on('end', async () => {
// Log em CADA etapa. Sem isso, "nao gravou" e um palpite.
console.log('[1] POST /leitura chegou, corpo de', corpo.length, 'bytes');
const leitura = JSON.parse(corpo);
console.log('[2] JSON:', leitura);
const [r] = await pool.execute(
'INSERT INTO tb_leitura (id_placa, temp_c, umidade_pct, dt_leitura) VALUES (?, ?, ?, ?)',
[leitura.id_placa, leitura.temp_c, leitura.umidade_pct, leitura.dt_leitura]
);
console.log('[3] gravada no banco, id', r.insertId);
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ id: r.insertId, gravado: true }));
});
return;
}
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ erro: 'rota nao existe' }));
});
let pool;
(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 pool.query(CREATE TABLE IF NOT EXISTS tb_leitura (
id INT AUTO_INCREMENT, id_placa VARCHAR(32) NOT NULL,
temp_c DECIMAL(5,2) NOT NULL, umidade_pct DECIMAL(5,2) NOT NULL,
dt_leitura DATETIME NOT NULL, dt_gravacao DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id), INDEX ix_leitura_placa_data (id_placa, dt_leitura)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4);
await new Promise((r) => servidor.listen(0, '127.0.0.1', r));
const url = 'http://127.0.0.1:' + servidor.address().port + '/leitura';
console.log('servidor no ar:', url);
// Este e o papel que a placa faz: cliente HTTP falando POST.
const resposta = await fetch(url, { method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id_placa: 'esp32-lab01', temp_c: 28.3,
umidade_pct: 61, dt_leitura: '2026-09-30 10:20:00' }) });
console.log('[4] a placa recebeu status', resposta.status, await resposta.text());
const [n] = await pool.query('SELECT COUNT(*) AS total FROM tb_leitura');
console.log('[5] total de linhas em tb_leitura:', n[0].total);
servidor.close();
await pool.end();
})();
A saída real, medida nesta maquina:
servidor no ar: http://127.0.0.1:41681/leitura [1] POST /leitura chegou, corpo de 92 bytes [2] JSON: { id_placa: 'esp32-lab01', temp_c: 28.3, umidade_pct: 61, dt_leitura: '2026-09-30 10:20:00' } [3] gravada no banco, id 5 [4] a placa recebeu status 201 {"id":5,"gravado":true} [5] total de linhas em tb_leitura: 5
O 92 bytes do [1] e o mesmo tamanho que o professor mediu na lousa no dia 3. O id 5 do [3] e o mesmo que o {"id":5} do [4]: o banco gerou o id, o Node repassou, e a placa recebeu de volta o endereco do proprio dado. Esse eco e o que prova que o caminho fechou — a placa sabe onde o que ela mandou terminou.
O sketch da placa, que fecha o ciclo com a placa de verdade:
// Aula 2 do dia 5: ponta a ponta de verdade. A placa envia, o Node recebe, o // MySQL guarda. Este sketch e o cliente do sistema que o professor sobe na // frente da turma. // // O professor NAO le o codigo inteiro: ele sobe o servidor Node, roda este // sketch, e depois abre o banco e mostra a linha que apareceu. O ciclo // completo leva menos de 10 segundos, e e isso que a aula precisa provar. #include <Arduino.h> #include <WiFi.h> #include <HTTPClient.h> const int PIN_LED = 2; // Credenciais FICTICIAS: rede de teste do professor. const char* WIFI_SSID = "rede-teste-lab01"; const char* CHAVE_REDE = "ficticia-lab01"; // IP da maquina que roda o Node. Este e o unico endereco que a placa precisa. const char* IP_SERVIDOR = "192.168.0.10"; const int PORTA_SERVIDOR = 3000; const char* CAMINHO = "/leitura"; const long INTERVALO_MS = 6000; const unsigned long TIMEOUT_MS = 3000; WiFiClient cliente; HTTPClient http; unsigned long ultimo_envio = 0; int enviadas = 0; int perdidas = 0; void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(2000); Serial.println(); Serial.println("2o trimestre, dia 5, aula 2 - ponta a ponta"); Serial.println("-------------------------------------------------"); Serial.println("placa -> POST /leitura -> Node -> MySQL"); Serial.println("cada linha que aparece abaixo TEM uma linha no banco."); 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: o sketch ainda roda e mostra o motivo."); } } void loop() { unsigned long agora = millis(); if (agora - ultimo_envio < INTERVALO_MS) { return; } ultimo_envio = agora; if (WiFi.status() != WL_CONNECTED) { perdidas++; Serial.println("FALHA: sem rede, nada foi gravado."); return; } // A leitura e inventada de proposito: o sensor de verdade entra no dia 13. // O que importa aqui e que o JSON tem os MESMOS nomes de campo que o // INSERT do Node espera. Campo com nome diferente = INSERT quebrado. float temp_c = 25.0 + (enviadas % 5) * 0.9; int umidade_pct = 55 + (enviadas % 6); // 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:00", enviadas % 60); String json = String("{\"id_placa\":\"esp32-lab01\"") + ",\"temp_c\":" + String(temp_c, 1) + ",\"umidade_pct\":" + String(umidade_pct) + ",\"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); Serial.println("-------------------------------------------------"); Serial.print("JSON enviado: "); Serial.println(json); if (codigo == 201) { enviadas++; Serial.print("201 OK — "); Serial.print(enviadas); Serial.print(" gravadas, "); Serial.print(perdidas); Serial.println(" perdidas"); Serial.print("resposta do Node: "); Serial.println(http.getString()); } else { perdidas++; Serial.print("codigo "); Serial.print(codigo); Serial.println(" — nada foi gravado, e o motivo esta no log do Node"); } http.end(); }
Por que assim e não de outro jeito. O JSON.stringify no fetch do exemplo e o String da placa fazendo a mesma coisa pelos dois lados: transformar objeto em texto. O aluno que entendeu o sprintf da placa entende o JSON.stringify do Node, e são a mesma operacao com nomes diferentes.
O comentario do sketch sobre o sensor ser inventado tem razão pedagogica: se a placa lesse um sensor de verdade, o professor não conseguiria garantir que houve leitura nova, porque a temperatura pode não ter mudado em 6 segundos. Com o valor inventado e crescente, todo ciclo produz uma linha visivelmente diferente, e o professor ve o dado entrar sem depender do sensor.
Criterios de correcao
| Critério | Pontos |
|---|---|
| Ciclo completo rodando, com as quatro provas visiveis | 4 pontos |
| Item 2: o código e a prova em que o rastro parou, estão identificados | 2 pontos |
Item 3: o tempo de volta do 201 foi medido | 1 ponto |
Item 4: quebra proposital de nome de campo, com o erro.code anotado | 3 pontos |
| Item 5: a diferença entre nome errado e tipo errado esta explicada | 2 pontos |
Item 6: os valores de TIMESTAMPDIFF anotados, com interpretacao | 1 ponto |
| Item 7: as duas frases completadas | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
| Mudar o nome do campo e culpar a placa | "A placa manda errado" | "O campo temperatura não existe na tabela. O JSON precisa ter o nome da coluna. Ou você volta o nome, ou cria a coluna — e mudar schema no meio do caminho e o que o dia 9 faz." |
| Mandar o número como texto | "Mandei 26.5 com aspas e deu erro" | "Com aspas, o Node recebe a string 26.5, e o banco recebe texto numa coluna DECIMAL. O erro e diferente do item 4: la faltava a coluna, aqui o tipo esta errado." |
Não ter log entre o JSON.parse e o INSERT | "Não sei se chegou" | "Coloque um console.log depois de cada etapa. Sem ele, "parou" e um palpite, e a aula inteira vira adivinhacao." |
Sem try/catch, um dado ruim derruba o servidor | "Parou de funcionar depois de um erro" | "Sem try, a excecao sai do callback e o processo morre. Envolva o trecho, responda 500 e mantenha o servidor vivo." |
| Gravar e não conferir | Item 1 sem contagem de linhas | "Sem o SELECT de volta, você acredita que gravou. Grave, pegue o insertId, e leia esse id." |
Rodar o SELECT sem WHERE id = ? e dizer que gravou | "Tem varias linhas, então deu certo" | "Varias linhas e o estado antes do seu envio. Confira pelo insertId que o banco devolveu na hora do INSERT." |
Achar que dt_gravacao e a hora da leitura | Item 6 com atraso negativo | "Elas são instantes diferentes. dt_leitura veio no JSON da placa; dt_gravacao foi posto pelo banco. Se der negativo, a placa mandou uma hora no futuro." |
| Reiniciar a placa a cada leitura achando que "destravou" | "Toda vez que trava, eu aperto RESET" | "Reiniciar a placa não conserta Node parado nem banco fora. Descubra qual das três pecas caiu, olhando as quatro provas do caminho." |
Desafio extra
Adicione uma rota GET /contagem no servidor que devolve quantas linhas cada placa tem, e faca a placa pedir essa contagem a cada 10 envios, imprimindo o número no serial. Compare: o que a placa sabe da propria leitura e o que ela so descobre perguntando? A resposta que o professor espera: a placa sabe o que mandou e o id que recebeu, e nada do historico. Contar quantas leituras ela já mandou e um contador local, que ela mantem mesmo com o Node fora — e essa e a diferença entre contagem local e contagem no banco, que e o primeiro uso serio de dado local do dia 7.
A resolucao, compilada
// Aula 2 do dia 5: ponta a ponta de verdade. A placa envia, o Node recebe, o // MySQL guarda. Este sketch e o cliente do sistema que o professor sobe na // frente da turma. // // O professor NAO le o codigo inteiro: ele sobe o servidor Node, roda este // sketch, e depois abre o banco e mostra a linha que apareceu. O ciclo // completo leva menos de 10 segundos, e e isso que a aula precisa provar. #include <Arduino.h> #include <WiFi.h> #include <HTTPClient.h> const int PIN_LED = 2; // Credenciais FICTICIAS: rede de teste do professor. const char* WIFI_SSID = "rede-teste-lab01"; const char* CHAVE_REDE = "ficticia-lab01"; // IP da maquina que roda o Node. Este e o unico endereco que a placa precisa. const char* IP_SERVIDOR = "192.168.0.10"; const int PORTA_SERVIDOR = 3000; const char* CAMINHO = "/leitura"; const long INTERVALO_MS = 6000; const unsigned long TIMEOUT_MS = 3000; WiFiClient cliente; HTTPClient http; unsigned long ultimo_envio = 0; int enviadas = 0; int perdidas = 0; void setup() { pinMode(PIN_LED, OUTPUT); Serial.begin(115200); delay(2000); Serial.println(); Serial.println("2o trimestre, dia 5, aula 2 - ponta a ponta"); Serial.println("-------------------------------------------------"); Serial.println("placa -> POST /leitura -> Node -> MySQL"); Serial.println("cada linha que aparece abaixo TEM uma linha no banco."); 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: o sketch ainda roda e mostra o motivo."); } } void loop() { unsigned long agora = millis(); if (agora - ultimo_envio < INTERVALO_MS) { return; } ultimo_envio = agora; if (WiFi.status() != WL_CONNECTED) { perdidas++; Serial.println("FALHA: sem rede, nada foi gravado."); return; } // A leitura e inventada de proposito: o sensor de verdade entra no dia 13. // O que importa aqui e que o JSON tem os MESMOS nomes de campo que o // INSERT do Node espera. Campo com nome diferente = INSERT quebrado. float temp_c = 25.0 + (enviadas % 5) * 0.9; int umidade_pct = 55 + (enviadas % 6); // 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:00", enviadas % 60); String json = String("{\"id_placa\":\"esp32-lab01\"") + ",\"temp_c\":" + String(temp_c, 1) + ",\"umidade_pct\":" + String(umidade_pct) + ",\"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); Serial.println("-------------------------------------------------"); Serial.print("JSON enviado: "); Serial.println(json); if (codigo == 201) { enviadas++; Serial.print("201 OK — "); Serial.print(enviadas); Serial.print(" gravadas, "); Serial.print(perdidas); Serial.println(" perdidas"); Serial.print("resposta do Node: "); Serial.println(http.getString()); } else { perdidas++; Serial.print("codigo "); Serial.print(codigo); Serial.println(" — nada foi gravado, e o motivo esta no log do Node"); } http.end(); }
Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia05 aula2.
