Node com MySQL: o caminho fechado — Arduino e IoT — semana 5 do 2o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 5 · Node com MySQL: o caminho fechado — Material de Apoio Arduino

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/promise e createPool, dizendo o que e um pool.
  • Ler credencial de variável de ambiente com process.env e explicar por que ela não vai no código nem no git.
  • Gravar uma leitura com pool.execute e conferir a gravação com um SELECT pelo insertId.
  • Reconhecer ECONNREFUSED e ER_NO_SUCH_TABLE pelo erro.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, mysql2 instalado e MySQL ou MariaDB 10.11
  • Arquivo .env com as credenciais do banco de aula, e .gitignore já 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 materialNão pode no material
DB_PASSo valor que esta depois do =
process.env.DB_PASSa 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.codeO que aconteceuO que fazer
ECONNREFUSEDo banco não esta no enderecosuba o banco, confira host e porta
ER_NO_SUCH_TABLEconectou, mas a tabela não existerode o CREATE TABLE do dia 4
ER_ACCESS_DENIED_ERRORusuario ou senha erradosconfira o .env
ER_DUP_ENTRYviolou chave unicao 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.

  1. 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.
  2. Crie o pool com as quatro variáveis do .env. Rode SELECT VERSION() e anote a versão que o banco respondeu (não escreva versão de memória no código).
  3. Grave uma leitura e anote o insertId. imediatamente depois, faca SELECT por esse id e confirme que a linha la esta. O temp_c voltou com quantas casas decimais?
  4. Insira uma segunda leitura com temp_c menor que a primeira e faca SELECT de tudo sem WHERE. O banco devolveu em ordem de insercao ou de temperatura?
  5. Pare o banco e rode o mesmo script. Anote o erro.code exato que apareceu. Ele esta no message, no code, ou nos dois?
  6. 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érioPontos
Desenho com as três caixas, o conteudo de cada seta e o local da senha2 pontos
Pool criado com as quatro variáveis de ambiente, sem senha escrita no código2 pontos
Versão do banco obtida por consulta, e não escrita no código1 ponto
Leitura gravada, insertId anotado e SELECT de confirmacao executado3 pontos
Item 5: erro.code exato anotado, e dito de onde ele vem2 pontos
Item 6: o que o tempo de falha diz sobre quem verifica a senha1 ponto

Erros comuns

ErroComo apareceCorrecao
Escrever a senha direto no createPoolo 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 bancoItem 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 SELECT confirma.
  • 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, mysql2 instalado 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:

#RastroOnde aparece
1o JSON que a placa montoumonitor serial
2o status 201 que voltoumonitor serial
3o log [3] gravadaterminal do Node
4a linha com o idtabela 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.
  1. Suba o servidor, o banco e o sketch. Deixe rodando por 30 segundos. Anote quantas linhas apareceram no banco.
  2. 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?
  3. Ligue o Node de novo. Quanto tempo o serial demora para voltar a mostrar 201? Esse tempo e o atraso do caminho?
  4. Troque, no sketch, o nome do campo temp_c para temperatura. Grave e espere uma leitura. O que o terminal do Node mostra? O erro.code e o que? Quem esta errado: a placa ou o Node?
  5. 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?
  6. Rode o SELECT com TIMESTAMPDIFF(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?
  7. 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érioPontos
Ciclo completo rodando, com as quatro provas visiveis4 pontos
Item 2: o código e a prova em que o rastro parou, estão identificados2 pontos
Item 3: o tempo de volta do 201 foi medido1 ponto
Item 4: quebra proposital de nome de campo, com o erro.code anotado3 pontos
Item 5: a diferença entre nome errado e tipo errado esta explicada2 pontos
Item 6: os valores de TIMESTAMPDIFF anotados, com interpretacao1 ponto
Item 7: as duas frases completadas1 ponto

Erros comuns

ErroComo apareceCorrecao
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 conferirItem 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 leituraItem 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.