MySQL: o banco que guarda — Arduino e IoT — semana 4 do 2o trimestre
Semana 4 de 16· 2o trimestre · 01/05 a 04/09
MySQL: o banco que guarda
Schema, tipos, INSERT e SELECT. A primeira tabela que responde uma pergunta.
Aula 1 — Modelar a tabela de leitura e criar o schema
Objetivos
- Modelar a tabela de leitura, dizendo para que serve cada coluna e que tipo ela precisa.
- Criar a tabela com
CREATE TABLE, incluindo chave primaria e índice, e ler de volta o schema criado. - Justificar por que
DECIMALe nãoFLOATpara temperatura, e o que muda na pratica. - Explicar a diferença entre
dt_leituraedt_gravacao, e por que as duas colunas precisam existir. - Apontar, no
DESCRIBE, o que o banco fez com cada tipo que o aluno pediu.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1 protoboard de 830 pontos por dupla
- Computador com Node.js 20, MySQL ou MariaDB 10.11, e um cliente de banco
- Arquivo
.envcom a credencial do banco de aula, com os nomes das variáveis na lousa - Folha de papel por dupla, para o desenho do schema
- Projetor, para o terminal do professor
Conceitos
Uma linha por leitura, e não uma linha por dia
A decisao de modelagem mais importante do dia: a tabela guarda uma linha por leitura, com data e hora. Não uma linha por hora, não uma linha por media.
O motivo e a pergunta que o banco precisa responder no dia 6. Se você guarda a media por hora, você perde o pico da temperatura. Se você guarda cada leitura, você consegue calcular a media por hora na hora que quiser, e ainda mostra a minima e a maxima. Guardar o detalhe custa mais espaco e nunca custa informação; guardar a agregacao economiza espaco e para sempre.
As sete colunas
| Coluna | Tipo | Por que este tipo |
|---|---|---|
id | INT AUTO_INCREMENT | chave primaria, o banco gera |
id_placa | VARCHAR(32) | texto, porque tem letras |
temp_c | DECIMAL(5,2) | até 999,99 com duas casas |
umidade_pct | DECIMAL(5,2) | mesmo formato do percentual |
dt_leitura | DATETIME | quando a placa mediu |
dt_gravacao | DATETIME DEFAULT CURRENT_TIMESTAMP | quando o banco gravou |
| índice | INDEX (id_placa, dt_leitura) | para o dia 6 |
DECIMAL contra FLOAT
FLOAT guarda o número em notacao cientifica binaria, e 26,5 não e exatamente representavel nessa base: o banco guarda o valor mais proximo, e a leitura volta com uma diferença na terceira ou quarta casa.
DECIMAL(5,2) guarda o número como decimal, com duas casas exatas. O que entra e o que sai, sempre. Para temperatura, umidade, e qualquer medida fisica, DECIMAL e a escolha certa. O professor mostra os dois lado a lado no terminal, e o aluno ve a diferença em 0.01.
dt_leitura contra dt_gravacao
São dois instantes diferentes e o banco precisa dos dois:
dt_leiturae o momento em que o sensor mediu, e vem dentro do JSON, enviado pela placa.dt_gravacaoe o momento em que oINSERTaconteceu, e e preenchido pelo proprio banco.
Quando a placa esta lenta, chega tarde ou a fila cresce, os dois valores se separam. No dia 7 o professor mede essa separacao com TIMESTAMPDIFF e mostra que a diferença entre os dois e o atraso do caminho. Se so existisse uma coluna de hora, essa medida seria impossivel.
Atividade
Montagem: nenhuma. A placa fica com o circuito do dia 1 e mostra a leitura, para o aluno ter de onde vem o valor que entra na tabela.
- Desenhe no papel a tabela com sete colunas. Para cada uma, escreva o tipo e uma frase dizendo para que serve.
- Escreva o
CREATE TABLEcompleto, com chave primaria e índice. Nomeie a tabelatb_leiturae as colunas em portugues com sublinhado. - Crie a tabela no banco de aula e rode o
DESCRIBE. - Comparar, lado a lado, o que você pediu e o que o banco devolveu para cada coluna. Escreva duas linhas de diferença que você encontrou.
- Troque
DECIMAL(5,2)porFLOATem uma tabela de teste, insira26,5e le de volta. O que mudou? Escreva o número exato que voltou. - Explique em uma frase: se a tabela guardasse so a media por hora, o que o dia 6 não seria capaz de mostrar?
- Rode
SHOW CREATE TABLE tb_leiturae guarde a saída no caderno. Essa e a definição real da tabela, e não o que você digitou.
Nota: 11 pontos. Critério de fim: a tabela existe no banco, o DESCRIBE esta anotado e os itens 4 e 5 tem as diferancas escritas com números.
Resolucao
O exemplo que cria o schema e le de volta, executado nesta maquina:
// Criar o schema da tabela de leitura. A tabela e a unica coisa que
// sobrevive ao fim do processo Node -- e por isso que ela precisa de nome.
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,
});
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
);
console.log('tb_leitura criada ou ja existia');
// DESCRIBE e o "print" do schema: devolve as colunas como o banco as guardou.
const [colunas] = await pool.query('DESCRIBE tb_leitura');
for (const c of colunas) {
console.log(' ', c.Field.padEnd(12), c.Type.padEnd(12), c.Null === 'NO' ? 'obrigatorio' : 'opcional', c.Key || '');
}
await pool.end();
})();
A saída real do DESCRIBE, medida nesta maquina contra o MariaDB 10.11:
tb_leitura criada ou ja existia id int(11) obrigatorio PRI id_placa varchar(32) obrigatorio MUL temp_c decimal(5,2) obrigatorio umidade_pct decimal(5,2) obrigatorio dt_leitura datetime obrigatorio dt_gravacao datetime obrigatorio
O professor aponta três coisas nessa saída: o MUL na id_placa e o índice funcionando, o PRI na id e a chave primaria, e o decimal(5,2) minusculo, porque o banco normaliza tudo para minusculo. O aluno que escreveu TEMP_C descobre aqui que o banco não fez distincao, e e por isso que o padrao do curso e minusculo com sublinhado.
O IF NOT EXISTS e o que permite rodar o exemplo duas vezes sem erro, e o DEFAULT CURRENT_TIMESTAMP e o que dispensa o dt_gravacao no INSERT do dia 4 aula 2: o banco poe a hora sozinho.
O script todo esta em crase, e não em aspas simples. SQL com varias linhas não cabe em uma string de aspas simples em JavaScript: a aspa simples fecha no primeiro espaço+quebra, e o Node lanca SyntaxError: Invalid or unexpected token. O aluno vai encontrar esse erro sozinho quando tentar.
O sketch da placa mostra o lado de dentro do schema: cada campo do registro tem um nome e um tipo, e e a correspondencia entre esses nomes e as colunas do banco que o INSERT do Node vai depender.
// Aula 1 do dia 4: o dado existe, e precisa de endereco. // // Do lado do Node, a tabela tb_leitura tem sete colunas. Esta placa nao tem // como ver essa tabela, mas ela tem o mesmo trabalho a fazer: transformar um // numero solto em um registro com nome, tipo e espaco reservado. As duas // colunas do JSON batem com as colunas do banco, e essa correspondencia e // o tema do dia inteiro. #include <Arduino.h> const int PIN_SENSOR_ANALOGICO = 34; // As mesmas colunas do schema, na mesma ordem do INSERT do Node. const int TOTAL_DE_COLUNAS = 5; const char* NOMES[] = { "id_placa", // VARCHAR(32) - quem mandou "temp_c", // DECIMAL(5,2) - quanto valia "umidade_pct", // DECIMAL(5,2) - quanto valia "fuso", // INT - deslocamento do relogio "dt_leitura" // DATETIME - quando a placa mediu }; // Larguras de coluna na saida do monitor. ASCII nao tem caractere de // alinhamento automatico, entao o alinhamento e um array. const int LARGURAS[] = {12, 10, 14, 6, 21}; const int RESOLUCAO_ADC = 4095; const int FUSO_HORAS = -3; void imprimir_cabecalho() { Serial.println("registro que a placa vai mandar, campo por campo:"); Serial.println(); for (int i = 0; i < TOTAL_DE_COLUNAS; i++) { Serial.print(NOMES[i]); for (int e = strlen(NOMES[i]); e < LARGURAS[i]; e++) { Serial.print(" "); } } Serial.println(); } // String nao pode ser declarado como vetor de referencias: isso nao // compila em ESP32. O vetor de String e o jeito certo de passar uma // lista de texto para uma funcao. void imprimir_linha(String campos[], int quantos) { for (int i = 0; i < quantos; i++) { Serial.print(campos[i]); for (int e = strlen(campos[i].c_str()); e < LARGURAS[i]; e++) { Serial.print(" "); } } Serial.println(); } void setup() { Serial.begin(115200); delay(2000); Serial.println(); Serial.println("2o trimestre, dia 4, aula 1 - cada campo tem tipo"); Serial.println("================================================="); Serial.println("do lado do Node, o schema e:"); Serial.println(" id, id_placa, temp_c, umidade_pct, dt_leitura, dt_gravacao"); Serial.println(); Serial.println("a placa so precisa preencher cinco deles. O resto e do banco."); Serial.println(); imprimir_cabecalho(); } void loop() { int bruto = analogRead(PIN_SENSOR_ANALOGICO); float temp_c = (bruto / 4095.0f) * 50.0f; // Mesmo formato do JSON do dia 3: chave, dois pontos, valor. String campos[TOTAL_DE_COLUNAS]; campos[0] = "esp32-lab01"; campos[1] = String(temp_c, 1); campos[2] = String(55 + (bruto % 10)); campos[3] = String(FUSO_HORAS); campos[4] = "2026-09-30 10:00:00"; imprimir_linha(campos, TOTAL_DE_COLUNAS); Serial.println(); Serial.println("cinco campos, cinco tipos no banco. Nome que nao bate,"); Serial.println("o INSERT do Node quebra — e o erro do dia 5."); delay(2000); }
Criterios de correcao
| Critério | Pontos |
|---|---|
| Sete colunas desenhadas, com tipo e função | 3 pontos |
CREATE TABLE com chave primaria e índice, tudo em minusculo | 2 pontos |
DESCRIBE executado e anotado | 1 ponto |
| Item 4: duas diferenças entre o pedido e o devolvido, com o que o banco fez | 2 pontos |
Item 5: o número devolvido pelo FLOAT esta escrito | 2 pontos |
| Item 6: o que a media por hora faria perder esta em uma frase | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
Criar a tabela com o CREATE TABLE em aspas simples e varias linhas | SyntaxError: Invalid or unexpected token | "SQL de varias linhas vai entre crases, em JavaScript. As aspas simples fecham no primeiro espaco seguido de quebra de linha." |
Nome de coluna em portugues com maiuscula, tipo Temperatura | "Usei o mesmo nome do desenho" | "Minusculo e sublinhado: temp_c. O banco normaliza para minusculo, e quando o INSERT do dia 4 aula 2 não bater, o erro e nome de coluna." |
Usar FLOAT porque "todo mundo usa" | Item 5 sem diferença observada | "FLOAT guarda em notacao binaria e 26,5 não existe exatamente nessa base. DECIMAL(5,2) guarda o decimal. Para medida fisica, DECIMAL sempre." |
Criar so uma coluna de hora, dt_hora | Schema com seis colunas em vez de sete | "São dois instantes: quando a placa mediu e quando o banco gravou. Com uma coluna so, o dia 7 não consegue medir o atraso." |
| Esquecer o índice e descobrir so no dia 6 | Consulta lenta na aula 6 | "O índice não muda o resultado, muda o tempo. O dia 6 faz consulta por placa e por intervalo, e são exatamente as duas colunas do índice." |
Usar VARCHAR para temperatura | temp_c como texto | "Número em texto não soma, não tem media e ocupa mais espaco. DECIMAL guarda número e aceita calculo." |
Deixar a tabela sem PRIMARY KEY | "Criei a tabela, mas não tem id" | "Sem chave primaria não ha AUTO_INCREMENT e não ha linha unica. Toda tabela de leitura precisa de id." |
| Apagar a tabela toda a cada aula | DROP TABLE no comeco do script | "Não use DROP. Use CREATE TABLE IF NOT EXISTS, e quando precisar zerar, use TRUNCATE. DROP apaga a estrutura, e o dia 5 depende dela." |
Desafio extra
Crie uma segunda tabela, tb_leitura_hora, com uma linha por hora e por placa, com id_placa, dt_hora, temp_media, temp_min e temp_max. Não insira nada nela: apenas a estrutura. Depois escreva em uma frase o que essa tabela resolve e o que ela custa. A resposta que o professor espera: ela responde "como foi a tarde de hoje" em uma consulta rapida, e custa o trabalho de manter a agregacao atualizada a cada leitura — trabalho que o dia 6 mostra que o banco faz sozinho com GROUP BY, e que e o motivo de você provavelmente não precisar dela.
A resolucao, compilada
// Aula 1 do dia 4: o dado existe, e precisa de endereco. // // Do lado do Node, a tabela tb_leitura tem sete colunas. Esta placa nao tem // como ver essa tabela, mas ela tem o mesmo trabalho a fazer: transformar um // numero solto em um registro com nome, tipo e espaco reservado. As duas // colunas do JSON batem com as colunas do banco, e essa correspondencia e // o tema do dia inteiro. #include <Arduino.h> const int PIN_SENSOR_ANALOGICO = 34; // As mesmas colunas do schema, na mesma ordem do INSERT do Node. const int TOTAL_DE_COLUNAS = 5; const char* NOMES[] = { "id_placa", // VARCHAR(32) - quem mandou "temp_c", // DECIMAL(5,2) - quanto valia "umidade_pct", // DECIMAL(5,2) - quanto valia "fuso", // INT - deslocamento do relogio "dt_leitura" // DATETIME - quando a placa mediu }; // Larguras de coluna na saida do monitor. ASCII nao tem caractere de // alinhamento automatico, entao o alinhamento e um array. const int LARGURAS[] = {12, 10, 14, 6, 21}; const int RESOLUCAO_ADC = 4095; const int FUSO_HORAS = -3; void imprimir_cabecalho() { Serial.println("registro que a placa vai mandar, campo por campo:"); Serial.println(); for (int i = 0; i < TOTAL_DE_COLUNAS; i++) { Serial.print(NOMES[i]); for (int e = strlen(NOMES[i]); e < LARGURAS[i]; e++) { Serial.print(" "); } } Serial.println(); } // String nao pode ser declarado como vetor de referencias: isso nao // compila em ESP32. O vetor de String e o jeito certo de passar uma // lista de texto para uma funcao. void imprimir_linha(String campos[], int quantos) { for (int i = 0; i < quantos; i++) { Serial.print(campos[i]); for (int e = strlen(campos[i].c_str()); e < LARGURAS[i]; e++) { Serial.print(" "); } } Serial.println(); } void setup() { Serial.begin(115200); delay(2000); Serial.println(); Serial.println("2o trimestre, dia 4, aula 1 - cada campo tem tipo"); Serial.println("================================================="); Serial.println("do lado do Node, o schema e:"); Serial.println(" id, id_placa, temp_c, umidade_pct, dt_leitura, dt_gravacao"); Serial.println(); Serial.println("a placa so precisa preencher cinco deles. O resto e do banco."); Serial.println(); imprimir_cabecalho(); } void loop() { int bruto = analogRead(PIN_SENSOR_ANALOGICO); float temp_c = (bruto / 4095.0f) * 50.0f; // Mesmo formato do JSON do dia 3: chave, dois pontos, valor. String campos[TOTAL_DE_COLUNAS]; campos[0] = "esp32-lab01"; campos[1] = String(temp_c, 1); campos[2] = String(55 + (bruto % 10)); campos[3] = String(FUSO_HORAS); campos[4] = "2026-09-30 10:00:00"; imprimir_linha(campos, TOTAL_DE_COLUNAS); Serial.println(); Serial.println("cinco campos, cinco tipos no banco. Nome que nao bate,"); Serial.println("o INSERT do Node quebra — e o erro do dia 5."); delay(2000); }
Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia04 aula1.
Aula 2 — Inserir e consultar: INSERT e SELECT na pratica
Objetivos
- Inserir uma leitura com
INSERTe parametro, e ler oinsertIdque o banco devolve. - Consultar com
SELECT, filtrando comWHEREe ordenando comORDER BYeLIMIT. - Passar valor por parametro
?em vez de concatenar, e dizer por que isso e obrigatorio. - Ler a diferença entre
executeequerynomysql2e escolher a certa em cada caso. - Reproduzir, e explicar, o deslocamento de fuso que o Node faz com a coluna
DATETIME.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1 protoboard de 830 pontos por dupla
- Computador com Node.js 20 e MySQL ou MariaDB 10.11
- Arquivo
.envcom a credencial do banco de aula - Folha de papel por dupla, para anotar as consultas e o que elas devolveram
- Projetor, para o terminal do professor
Conceitos
INSERT e o insertId
INSERT INTO tb_leitura (id_placa, temp_c, umidade_pct, dt_leitura)
VALUES (?, ?, ?, ?)
Quatro ?, quatro valores, na mesma ordem. O banco devolve um objeto com insertId (o id que ele gerou) e affectedRows (quantas linhas mudaram).
O dt_gravacao não aparece no INSERT de propósito: ele tem DEFAULT CURRENT_TIMESTAMP, e o banco poe a hora. Se você mandar dt_gravacao do Node, a hora gravada passa a ser a hora do servidor Node, e não a hora do banco — o que parece a mesma coisa e não e, e volta no dia 11 quando o Node roda em outra maquina, em outro fuso.
Parametro contra concatenacao
// ERRADO: a string do SQL muda de forma
const sql = "SELECT * FROM tb_leitura WHERE id_placa = '" + placa + "'";
// CERTO: o SQL tem forma fixa, o valor entra separado
const [linhas] = await pool.execute(sql, [placa]);
O segundo trecho não e "mais seguro" por estilo: e que o SQL não muda de forma. O banco le a frase, decide o plano de execução, e substitui o ponto de interrogacao pelo valor. A concatenacao muda a frase, e o banco tem que decidir de novo — e qualquer aspa que o aluno coloque no valor fecha a string SQL e deixa o aluno executar um comando que não era para executar.
O curso de JavaScript do 1o trimestre já mostrou isso no innerHTML do document. Aqui o nome e outro (SQL injection, dia 9), e o resultado e o mesmo.
execute contra query
| Metodo | Quando usar | Devolve |
|---|---|---|
pool.execute(sql, [params]) | com parametro, o dia inteiro | prepared statement, mais rapido |
pool.query(sql) | sem parametro: CREATE, DESCRIBE, SHOW | sem prepared statement |
O aluno que usar execute sem lista de parametros recebe o mesmo resultado, e não entende por que a aula insiste em query para o CREATE. O motivo e desempenho: sem parametro não ha o que preparar, e preparar uma frase sem parametro e trabalho jogado fora.
O fuso: o erro que o Node causa
Este e o item mais importante do dia, e so aparece quando o aluno usa DATETIME:
Com mysql2 no padrao, DATETIME volta como um objeto Date em JavaScript. O Date guarda o instante em UTC, e o MySQL guardou as 9 h locais. O resultado e que a hora volta deslocada, e o aluno ve 09:00 virar 07:00Z.
A correcao esta na configuração do pool:
mysql.createPool({ /* ... */ dateStrings: true });
Com dateStrings: true, o banco devolve o texto 2026-09-30 09:00:00, sem conversao nenhuma, e o que foi guardado e o que aparece. Para um sistema que so grava e relé, e a escolha certa.
Atividade
Montagem: nenhuma. A placa fica com o circuito do dia 1.
- Escreva o
INSERTcom quatro?e a lista de valores na ordem certa. Confira: qual?e odt_leitura? - Insira três leituras da sua placa e duas de outra placa ficticia. Anote o
insertIdde cada uma. Os ids pulam algum número? Por que? - Consulte todas as linhas com
ORDER BY id. Depois consulte so as da sua placa comWHERE. As duas consultas tem resultados diferentes? Quantas linhas cada uma devolveu? - Troque
ORDER BY idporORDER BY dt_leitura DESCe repita. O que mudou na ordem? ComORDER BY id DESC, as leituras aparecem na ordem em que chegaram ou na ordem em que aconteceram? Defina a diferença em uma frase. - Adicione
LIMIT 2e explique em uma frase o que ele faz. Ele limita a quantidade de linhas ou o intervalo de tempo? - Inserir
26,5com virgula e ler de volta. O que aconteceu com o número? Tente26.5com ponto e le de volta. Qual das duas formas funciona? - Adicione
dateStrings: trueno pool e rode a consulta de novo. A hora mudou? Escreva a hora que aparece antes e depois da mudanca.
Nota: 12 pontos. Critério de fim: existem pelo menos cinco linhas, com duas placas diferentes, e os itens 4, 5 e 7 tem resposta escrita com os números observados.
Resolucao
O exemplo foi executado duas vezes seguidas nesta maquina, e as duas execuções deram exatamente a mesma saída — e o que prova que o TRUNCATE no comeco cumpre o papel:
// INSERT e SELECT na pratica. O exemplo e idempotente: TRUNCATE no comeco,
// entao rodar duas vezes da o mesmo resultado.
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, // devolve DATETIME como texto, sem deslocar o fuso
});
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 pool.query('TRUNCATE TABLE tb_leitura');
// INSERT de tres leituras, com o ? preenchido pelo array de valores.
// O primeiro ? e id_placa, o segundo temp_c, o terceiro umidade_pct.
const sql = 'INSERT INTO tb_leitura (id_placa, temp_c, umidade_pct, dt_leitura) VALUES (?, ?, ?, ?)';
const linhas = [
['esp32-lab01', 27.40, 58, '2026-09-30 09:00:00'],
['esp32-lab01', 29.10, 55, '2026-09-30 09:05:00'],
['esp32-lab02', 22.80, 71, '2026-09-30 09:10:00'],
];
for (const linha of linhas) {
const [r] = await pool.execute(sql, linha);
console.log('id gerado', r.insertId, '->', linha[1] + ' C');
}
const [todas] = await pool.execute(
'SELECT id, id_placa, temp_c, umidade_pct, dt_leitura FROM tb_leitura ORDER BY id');
console.log('--- todas as linhas, em ordem de chegada ---');
for (const t of todas) {
console.log(' ', t.id, t.id_placa, t.temp_c + ' C', t.umidade_pct + ' %', t.dt_leitura.slice(11, 19));
}
// WHERE com parametro: nunca concatenar o valor na string.
const [uma] = await pool.execute(
'SELECT COUNT(*) AS total FROM tb_leitura WHERE id_placa = ?', ['esp32-lab01']);
console.log('leituras de esp32-lab01:', uma[0].total);
const [limite] = await pool.execute(
'SELECT * FROM tb_leitura ORDER BY id DESC LIMIT ?', [1]);
console.log('ultima leitura inserida:', limite[0].id, limite[0].temp_c + ' C');
await pool.end();
})();
A saída real, nas duas execuções:
id gerado 1 -> 27.4 C id gerado 2 -> 29.1 C id gerado 3 -> 22.8 C --- todas as linhas, em ordem de chegada --- 1 esp32-lab01 27.40 C 58.00 % 09:00:00 2 esp32-lab01 29.10 C 55.00 % 09:05:00 3 esp32-lab02 22.80 C 71.00 % 09:10:00 leituras de esp32-lab01: 2 ultima leitura inserida: 3 22.80 C
O professor aponta duas coisas nessa saída. A primeira: a coluna dt_leitura mostra 09:10:00, que e exatamente o que foi inserido — e so aparece isso porque o pool foi criado com dateStrings: true. A segunda: a coluna dt_gravacao não aparece no SELECT, mas existe na linha, preenchida pelo banco no momento do INSERT, com o horario da maquina onde o Node roda. Os dois instantes estão ali, diferentes, e o dia 7 mede o quanto diferentes com TIMESTAMPDIFF.
Note também que temp_c volta como 27.40 e não 27.4: e o DECIMAL(5,2) devolvendo as duas casas que você pediu. O aluno que esperava 27.4 esta vendo o tipo funcionando.
O sketch da placa mostra INSERT e SELECT sem MySQL, num vetor de memória, para o aluno ver as duas operacoes antes de ver a versão com banco:
// Aula 2 do dia 4: INSERT e SELECT do lado da placa. // // Do lado do Node, o INSERT escreve na tb_leitura e o SELECT le de volta. // Esta placa mostra as duas operacoes no mesmo formato, sem MySQL: e a // mesma ideia de "guardar e depois buscar" que o dia 4 executa no banco. #include <Arduino.h> const int PIN_SENSOR_ANALOGICO = 34; const int TOTAL_DE_LEITURAS = 5; const int RESOLUCAO_ADC = 4095; // Estrutura de uma linha da tabela. E a mesma do banco, em C++: um tipo // para o id, um tipo para cada medida e um texto para a data. struct Leitura { int id; char id_placa[10]; float temp_c; int umidade_pct; char dt_leitura[20]; }; // O "banco" da placa: um vetor na memoria. Some quando a energia acaba, // e e exatamente por isso que o MySQL existe. Leitura tb_leitura[TOTAL_DE_LEITURAS]; int total_gravado = 0; int proximo_id = 1; // INSERT: acrescenta uma linha e devolve o id que o banco gerou. int inserir(const char* placa, float temp_c, int umidade_pct, const char* quando) { if (total_gravado >= TOTAL_DE_LEITURAS) { return -1; // tabela cheia: e o mesmo -1 que a aula 2 do dia 3 viu } Leitura& nova = tb_leitura[total_gravado]; nova.id = proximo_id++; strncpy(nova.id_placa, placa, sizeof(nova.id_placa) - 1); nova.id_placa[sizeof(nova.id_placa) - 1] = '\0'; // o \0 e obrigatorio nova.temp_c = temp_c; nova.umidade_pct = umidade_pct; strncpy(nova.dt_leitura, quando, sizeof(nova.dt_leitura) - 1); nova.dt_leitura[sizeof(nova.dt_leitura) - 1] = '\0'; return nova.id; } void setup() { Serial.begin(115200); delay(2000); Serial.println(); Serial.println("2o trimestre, dia 4, aula 2 - INSERT e SELECT"); Serial.println("================================================="); Serial.println("o vetor abaixo e a tb_leitura: 5 linhas, como o banco."); Serial.println(); Serial.println("id | placa | temp_c | umid | dt_leitura"); Serial.println("---+-----------+--------+------+-----------------------------"); } void loop() { if (total_gravado < TOTAL_DE_LEITURAS) { int bruto = analogRead(PIN_SENSOR_ANALOGICO); float temp_c = (bruto / 4095.0f) * 50.0f; int umidade_pct = 55 + (total_gravado % 6); char quando[20]; snprintf(quando, sizeof(quando), "2026-09-30 10:0%d:00", total_gravado); // INSERT int id = inserir("esp32-lab01", temp_c, umidade_pct, quando); if (id < 0) { Serial.println("INSERT recusado: a tabela esta cheia (5 linhas)."); Serial.println("no dia 7 a tabela cresce ate 172800 linhas por dia."); } } // SELECT: imprime a tabela inteira, do id mais baixo para o mais alto. // ORDER BY id e essa ordem. No dia 7 a ordem muda, e muda de proposito. for (int i = 0; i < total_gravado; i++) { Serial.print(" "); Serial.print(tb_leitura[i].id); Serial.print(" | "); Serial.print(tb_leitura[i].id_placa); for (int e = strlen(tb_leitura[i].id_placa); e < 9; e++) Serial.print(" "); Serial.print(" | "); Serial.print(tb_leitura[i].temp_c, 1); for (int e = 1; e < 6; e++) Serial.print(" "); Serial.print(" | "); Serial.print(tb_leitura[i].umidade_pct); for (int e = 1; e < 4; e++) Serial.print(" "); Serial.print(" | "); Serial.println(tb_leitura[i].dt_leitura); } if (total_gravado >= TOTAL_DE_LEITURAS) { Serial.println("a tabela esta cheia. DELETE o dia 7, aqui seria so"); Serial.println("reduzir TOTAL_DE_LEITURAS para zero e recarregar a placa."); } else { Serial.println("..."); } delay(1500); }
Por que assim e não de outro jeito. O strncpy não garante terminador quando o texto e do tamanho exato do vetor. Este código resolve isso com sizeof(campo) - 1 no terceiro argumento: o strncpy copia um caractere a menos que o tamanho do vetor e deixa a ultima posicao livre. Ha outra forma comum, escrever o '\x00' na ultima posicao depois da copia, e as duas funcionam — mas não as duas ao mesmo tempo, e o aluno que mistura as duas recebe um aviso do compilador. Sem terminador, o proximo Serial.print procura o fim da string e le a memória seguinte, e o monitor mostra lixo. O professor já mostrou esse bug no dia 2 do 1o trimestre, e ele volta porque e o mesmo código.
O TOTAL_DE_LEITURAS = 5 existe para o aluno ver a tabela cheia e receber o -1. Sem limite, o vetor estouraria e a placa reiniciaria sozinha — que e o pior sintoma possivel, porque parece que o WiFi caiu. O dia 7 volta a esse -1 com a fila de tamanho fixo.
Criterios de correcao
| Critério | Pontos |
|---|---|
INSERT com quatro ? e a ordem dos valores conferida | 2 pontos |
Pelo menos cinco linhas inseridas, com duas placas diferentes, insertId anotado | 3 pontos |
WHERE filtrando por placa e LIMIT limitando quantidade | 2 pontos |
| Item 4: a diferença entre ordem de chegada e ordem de acontecimento esta escrita | 2 pontos |
| Item 6: a forma que funciona esta identificada, com o número observado | 2 pontos |
dateStrings: true no pool, com a hora antes e depois anotada | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
| Concatenar o valor no SQL | SELECT * FROM tb_leitura WHERE id_placa = ' + placa | "O ? existe para isso. Troque por WHERE id_placa = ? e passe a placa no array. E o dia 9 trata por que isso e seguranca, não estilo." |
Usar virgula no INSERT | "O banco leu 26 e 5 como dois campos" | "Em SQL o decimal e ponto. A virgula e separador de coluna." |
Não usar dateStrings e estranhar a hora | Leitura inserida as 9 h, consulta devolve 7 h | "O mysql2 converte DATETIME em Date, e Date e UTC. Ponha dateStrings: true no pool e a hora volta como foi guardada." |
Esquecer o TRUNCATE e rodar duas vezes | "insertId comecou em 7, não em 1" | "O AUTO_INCREMENT não anda para tras. Use TRUNCATE no comeco, ou DELETE com filtro, para o exemplo ser repetivel." |
Mandar dt_gravacao no INSERT | Coluna duplicada ou valor diferente do esperado | "Ela tem DEFAULT CURRENT_TIMESTAMP. Se você mandar, perde a hora do banco e passa a gravar a hora do Node. Deixe o banco cuidar." |
Achar que ORDER BY id e ordem de tempo | Item 4 com resposta trocada | "id cresce na ordem da chegada. Se uma leitura antiga chegar depois, ela fica com id grande e dt_leitura pequena. O dia 7 mostra exatamente esse caso." |
Usar LIMIT 5 esperando 5 leituras do intervalo | "Pedeu 5 e veio leitura de ontem" | "LIMIT limita quantidade, não tempo. Para tempo, e o dia 6, com BETWEEN." |
SELECT * e reclamar das colunas extras | "Veio id e dt_gravacao e eu não pedi" | "* e "todas". Nomeie as colunas que você usa: SELECT id, temp_c FROM .... Além de ficar claro, economiza trafego." |
Desafio extra
Escreva uma consulta que devolva a ultima leitura de cada placa, e não apenas a ultima leitura do banco. Depois explique em uma frase por que ORDER BY id DESC LIMIT 1 não faz isso. A resposta que o professor espera: ele devolve a ultima leitura de qualquer placa, e para ter uma por placa o filtro precisa vir antes do LIMIT — ou a consulta precisa de GROUP BY id_placa com MAX(id), que e exatamente a mesma combinacao do dia 6.
A resolucao, compilada
// Aula 2 do dia 4: INSERT e SELECT do lado da placa. // // Do lado do Node, o INSERT escreve na tb_leitura e o SELECT le de volta. // Esta placa mostra as duas operacoes no mesmo formato, sem MySQL: e a // mesma ideia de "guardar e depois buscar" que o dia 4 executa no banco. #include <Arduino.h> const int PIN_SENSOR_ANALOGICO = 34; const int TOTAL_DE_LEITURAS = 5; const int RESOLUCAO_ADC = 4095; // Estrutura de uma linha da tabela. E a mesma do banco, em C++: um tipo // para o id, um tipo para cada medida e um texto para a data. struct Leitura { int id; char id_placa[10]; float temp_c; int umidade_pct; char dt_leitura[20]; }; // O "banco" da placa: um vetor na memoria. Some quando a energia acaba, // e e exatamente por isso que o MySQL existe. Leitura tb_leitura[TOTAL_DE_LEITURAS]; int total_gravado = 0; int proximo_id = 1; // INSERT: acrescenta uma linha e devolve o id que o banco gerou. int inserir(const char* placa, float temp_c, int umidade_pct, const char* quando) { if (total_gravado >= TOTAL_DE_LEITURAS) { return -1; // tabela cheia: e o mesmo -1 que a aula 2 do dia 3 viu } Leitura& nova = tb_leitura[total_gravado]; nova.id = proximo_id++; strncpy(nova.id_placa, placa, sizeof(nova.id_placa) - 1); nova.id_placa[sizeof(nova.id_placa) - 1] = '\0'; // o \0 e obrigatorio nova.temp_c = temp_c; nova.umidade_pct = umidade_pct; strncpy(nova.dt_leitura, quando, sizeof(nova.dt_leitura) - 1); nova.dt_leitura[sizeof(nova.dt_leitura) - 1] = '\0'; return nova.id; } void setup() { Serial.begin(115200); delay(2000); Serial.println(); Serial.println("2o trimestre, dia 4, aula 2 - INSERT e SELECT"); Serial.println("================================================="); Serial.println("o vetor abaixo e a tb_leitura: 5 linhas, como o banco."); Serial.println(); Serial.println("id | placa | temp_c | umid | dt_leitura"); Serial.println("---+-----------+--------+------+-----------------------------"); } void loop() { if (total_gravado < TOTAL_DE_LEITURAS) { int bruto = analogRead(PIN_SENSOR_ANALOGICO); float temp_c = (bruto / 4095.0f) * 50.0f; int umidade_pct = 55 + (total_gravado % 6); char quando[20]; snprintf(quando, sizeof(quando), "2026-09-30 10:0%d:00", total_gravado); // INSERT int id = inserir("esp32-lab01", temp_c, umidade_pct, quando); if (id < 0) { Serial.println("INSERT recusado: a tabela esta cheia (5 linhas)."); Serial.println("no dia 7 a tabela cresce ate 172800 linhas por dia."); } } // SELECT: imprime a tabela inteira, do id mais baixo para o mais alto. // ORDER BY id e essa ordem. No dia 7 a ordem muda, e muda de proposito. for (int i = 0; i < total_gravado; i++) { Serial.print(" "); Serial.print(tb_leitura[i].id); Serial.print(" | "); Serial.print(tb_leitura[i].id_placa); for (int e = strlen(tb_leitura[i].id_placa); e < 9; e++) Serial.print(" "); Serial.print(" | "); Serial.print(tb_leitura[i].temp_c, 1); for (int e = 1; e < 6; e++) Serial.print(" "); Serial.print(" | "); Serial.print(tb_leitura[i].umidade_pct); for (int e = 1; e < 4; e++) Serial.print(" "); Serial.print(" | "); Serial.println(tb_leitura[i].dt_leitura); } if (total_gravado >= TOTAL_DE_LEITURAS) { Serial.println("a tabela esta cheia. DELETE o dia 7, aqui seria so"); Serial.println("reduzir TOTAL_DE_LEITURAS para zero e recarregar a placa."); } else { Serial.println("..."); } delay(1500); }
Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia04 aula2.
