Configuração e ambiente — Arduino e IoT — semana 11 do 2o trimestre
Semana 11 de 16· 2o trimestre · 01/05 a 04/09
Configuração e ambiente
O mesmo código rodando na sua maquina e na maquina da aula.
Aula 1 — Configuração por ambiente e variáveis de ambiente
Objetivos
- Escrever a diferença entre máquinas como configuração por ambiente, e apontar no seu projeto os três valores que hardcode hoje.
- Montar o
.envdo seu projeto com as variáveis que o servidor usa, e o.env.exampleque vai para o git. - Fazer o
dotenvcarregar o.env, e explicar por que a leitura do arquivo acontece no módulo de configuração e não na rota. - Validar a configuração e fazer o servidor falhar cedo, antes de abrir porta e antes de perder leitura.
- Explicar por que a placa não tem variável de ambiente, e o que faz o papel dela na hora da compilação.
Material
- 1 ESP32 DevKit V1 por dupla, com o cabo USB
- 1 computador com Node.js 20,
dotenvemysql2instalados - Arquivo
.envdo projeto do aluno, criado no computador da dupla - Folha de papel por dupla, com a tabela dos três ambientes do trimestre
- Projetor, para o terminal do Node e o monitor serial da placa juntos
Conceitos
Configuração por ambiente é a lista do que muda entre máquinas
O mesmo código roda na máquina do aluno, na bancada da aula e no servidor final. O que muda são três coisas, e o professor escreve as três com os nomes em português no cabeçalho da tabela da atividade:
| O que muda | Na sua máquina | Na bancada | No servidor final |
|---|---|---|---|
| a porta | 3000 | 3100 | 8080 |
| o endereço do banco | 127.0.0.1 | o do professor | o do deploy |
| o usuário do banco | o seu | o da aula | o da aplicação |
| o limite de alerta | 32 | 32 | 40 |
O hardcode é o oposto disso: é o número escrito dentro do arquivo de código. Ele funciona na máquina onde foi escrito e quebra em todas as outras, e o pior do hardcode é que o código está certo. Não há erro de sintaxe, não há aviso, não há undefined. A placa conecta e não recebe resposta, e o aluno passa meia hora olhando o sketch antes de olhar a porta.
Config por ambiente é a lista do que muda, escrita num lugar só, lida num lugar só. O config do dia 10 aula 1 é o módulo que lê essa lista, e hoje ele passa a ler de um arquivo em vez de ter os valores dentro de si. É a resposta ao não hardcode: não hardcode é não escrever valor de máquina dentro do arquivo de código, e o motivo não é estilo, é que o valor muda e o arquivo não.
A variável de ambiente e o dotenv
Uma variável de ambiente é uma variável que pertence ao processo, não ao arquivo. Ela é criada fora do código, pelo sistema operacional ou pelo shell que lançou o processo, e o código só a lê. No Node, a leitura é process.env, e a variável é uma chave desse objeto gigante que o sistema inteiro partilha.
O problema é que a variável de ambiente não tem lugar na pasta do projeto: quem cria uma variável de ambiente na sua sessão do terminal é a sua sessão do terminal, e ela morre quando a sessão fecha. Para o projeto inteiro, incluindo a turma, o que existe é o arquivo .env: um arquivo de texto com NOME=valor por linha, que ninguém versiona e que a biblioteca dotenv lê no começo do processo.
O .env.example é o arquivo irmão que vai para o git, e ele tem os nomes e comentários, sem nenhum valor. É o que o colega clona e usa para saber o que preencher. A regra do dia 9 aula 1 aplicada a arquivos: o .env tem valor e não vai para o git; o .env.example tem nome e vai.
A leitura do .env acontece no módulo de configuração, e só lá. Ela não acontece na rota e não acontece no banco. Se cada módulo fizer require('dotenv').config(), cada módulo depende de arquivo de disco, e o teste do dia 12 precisa criar arquivo para rodar. Uma leitura, no topo, é o que mantém a camada de regra testável sem disco.
A diferença entre máquinas tem nome, e o nome está no arquivo
O motivo de dar nome a cada valor configurável não é organize, é diagnóstico. Quando o servidor sobe e a placa não conecta, o primeiro que olha é a tabela de configuração, e a tabela tem a coluna "valor desta máquina". Sem nome, o valor é um número solto e ninguém sabe qual máquina ele era.
O professor pede que a turma escreva a tabela dos três ambientes na parede da sala, e essa parede é o artefato mais útil do dia. No dia 15 aula 1, quando o sistema estiver fechado, a tabela é o que o aluno consulta antes de culpar o banco.
Falhar cedo, e o custo de não falhar
A validação de configuração é a função que roda antes de abrir a porta, e o que ela protege não é o servidor: é o tempo de descoberta.
Subir com a porta errada não dá erro. Dá servidor rodando, sem ninguém conectado, e o primeiro sinal de que algo está errado é o painel vazio no minuto vinte e cinco, quando a primeira leitura já foi perdida. Subir com o banco errado dá ER_ACCESS_DENIED no meio do primeiro pedido, que é melhor. Subir com a chave vazia dá 401 em toda requisição, que é o melhor dos três, porque o 401 é nomeado.
A configuração ausente tem um caso que é o pior de todos e que o professor conta na lousa: a variável não existe e o código faz Number(undefined). O resultado é NaN, e NaN na porta faz o listen falhar com uma mensagem que não menciona configuração. A validação transforma "não posso abrir a porta NaN" em "PORT ausente, e o que configure foi isto".
Erro de config é o nome que a turma dá a essa família de problemas, e ele tem uma característica que nenhum outro erro do trimestre tem: não depende de dado, de placa ou de rede. Erro de config é erro antes de qualquer coisa rodar, e por isso ele é o único que dá para testar de cabeça, sem bancada, no minuto um da aula.
A configuração ausente é o caso geral, e ele tem dois nomes: variável que não existe e variável que existe com valor vazio. PORT= no arquivo dá string vazia, e Number('') dá 0, que é uma porta válida na faixa do código e uma porta que exige privilégio. A validação do material trata os dois como ausência, e o motivo está no c.chave.length < 8: string vazia tem comprimento zero, e comprimento zero nunca é credencial.
O que o professor escreve como regra: a validação roda antes de abrir a porta, e o servidor não sobe com a configuração inválida. Um servidor que sobe errado custa mais caro que um servidor que não sobe, porque o segundo dá mensagem e o primeiro dá silêncio.
A placa não tem variável de ambiente, e isso não é problema
Numa placa que só tem o firmware gravado não existe process.env. Não existe porque o conceito é de sistema operacional, e o ESP32 roda sem sistema operacional: o firmware começa a executar e não há ninguém para criar variáveis de ambiente.
O que existe é a cadeia de configuração no momento da compilação, e ela é a mesma coisa com outro nome. O valor entra pelo -D do compilador, ou pelo #define de um arquivo de cabeçalho da pasta, e o que entra vai para dentro do binário. A consequência é a mesma do dia 9 aula 1, e o professor repete: o valor da senha está dentro do binário gravado na placa, e quem tiver a placa tem a senha.
A diferença que importa é quem decide o valor. No Node, quem decide é o arquivo .env que cada pessoa tem na sua máquina. Na placa, quem decide é o platformio.ini do projeto, e o professor troca de perfil trocando uma linha dele. O firmware do aluno não muda em uma linha sequer, e a placa manda para outro endereço e outro intervalo.
E é por isso que o dia 11 tem esta ordem: primeiro a configuração, e só na aula 2 o package.json, que é a configuração do próprio projeto de software.
Atividade
Montagem:
- Nenhuma montagem nova. A placa fica sem sensor e sem rede nesta aula.
- Computador com o projeto do aluno aberto e com o servidor do dia 10 parado.
- Abra o seu projeto e liste os valores que estão escritos dentro do código: porta, endereço do banco, usuário, senha, chave, limite de alerta. Escreva no caderno os que deveriam ser configuração por ambiente.
- Escreva o
.envdo seu projeto com as variáveis que o servidor usa, e escreva o.env.exampleao lado, com os mesmos nomes e nenhum valor. Confira: o example tem alguma senha? Não pode ter. - Rode o script da resolução e copie para o caderno as três linhas de saída dos três ambientes. Qual é a única linha que muda em todos os três? A sua resposta é a mesma?
- Escreva a função
validarConfigdo seu projeto e rode os cinco casos inválidos da resolução. Cole os cinco motivos. Qual deles é o que a sua configuração atual deixaria passar? - Apague uma variável do seu
.enve rode o servidor. Ele sobe ou falha? A mensagem diz o nome da variável ausente ou diz outra coisa? Se diz outra coisa, a validação não está antes dolisten. - Grave na placa o sketch da resolução e leia a saída. Anote o que o professor gravou na lousa: qual nome vai para o git e qual não. O
ssidda sua placa é nome ou valor? - Escreva no caderno a tabela dos três ambientes do trimestre, com as três colunas de valores. Cole essa tabela na parede da sala.
- Em uma frase: o que muda no seu projeto se o professor mandar o sistema rodar em outra porta e outro banco, e você não puder editar o código?
Nota: 12 pontos. Critério de fim: o .env e o .env.example escritos com os mesmos nomes e o example sem nenhum valor, e os cinco motivos de configuração inválida colados no caderno.
Resolucao
Do lado do Node, o script roda o mesmo código com três arquivos de ambiente e depois com cinco configurações quebradas. Este foi executado nesta máquina com Node.js 24:
// dia 11 aula 1, lado do Node: tres maquinas, um codigo.
const fs = require('fs');
const path = require('path');
const os = require('os');
// ---------------------------------------------------------------------------
// O QUE ISTO FAZ: cria tres arquivos de ambiente, um para cada maquina do
// trimestre, e roda o MESMO codigo com cada um deles. A saida e a prova de
// que config nao e hardcode: o codigo nao muda, o arquivo muda.
// ---------------------------------------------------------------------------
const PASTA = '/tmp/config-da-aula';
fs.rmSync(PASTA, { recursive: true, force: true });
fs.mkdirSync(PASTA, { recursive: true });
// O .env.example que vai para o git: nome da variavel e comentario, NENHUM valor.
fs.writeFileSync(path.join(PASTA, '.env.example'),
# Copie este arquivo para .env e preencha. O .env NAO vai para o git.
# Estes nomes vao para o git. Estes valores, nao.
PORT=3000
HOST=127.0.0.1
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=aula
DB_NAME=materiais_teste
CHAVE_API=valor-ficticio-da-aula-11
LIMITE_ALERTA_C=32
);
const AMBIENTES = {
'sua-maquina': { PORT: 3000, HOST: '127.0.0.1', DB_HOST: '127.0.0.1', DB_PORT: 3306, DB_USER: 'aula', DB_NAME: 'materiais_teste', CHAVE_API: 'valor-ficticio-maq-1', LIMITE_ALERTA_C: 32 },
'bancada-da-aula': { PORT: 3100, HOST: '127.0.0.1', DB_HOST: '127.0.0.1', DB_PORT: 3306, DB_USER: 'aula', DB_NAME: 'materiais_producao', CHAVE_API: 'valor-ficticio-aula', LIMITE_ALERTA_C: 32 },
'servidor-final': { PORT: 8080, HOST: '0.0.0.0', DB_HOST: '10.0.0.20', DB_PORT: 3306, DB_USER: 'app', DB_NAME: 'materiais_producao', CHAVE_API: 'valor-ficticio-prod', LIMITE_ALERTA_C: 40 },
};
// O CODIGO. Um so. Roda com o que o ambiente dar.
function carregarConfig(env) {
return {
porta: Number(env.PORT),
host: env.HOST,
dbHost: env.DB_HOST,
dbPort: Number(env.DB_PORT),
dbUser: env.DB_USER,
dbName: env.DB_NAME,
chave: env.CHAVE_API,
limite: Number(env.LIMITE_ALERTA_C),
};
}
// FALHAR CEDO: o que acontece quando a configuracao esta errada ou ausente.
function validarConfig(c) {
const problemas = [];
if (!Number.isInteger(c.porta) || c.porta < 1 || c.porta > 65535) {
problemas.push(PORT fora de 1 a 65535: "${c.porta}");
}
if (c.porta < 1024) problemas.push(PORT abaixo de 1024 precisa de privilegio: ${c.porta});
if (!c.dbHost) problemas.push('DB_HOST ausente');
if (!c.dbName) problemas.push('DB_NAME ausente');
if (!c.chave || c.chave.length < 8) problemas.push('CHAVE_API ausente ou curta demais');
if (c.limite < -40 || c.limite > 80) problemas.push(LIMITE_ALERTA_C fora de -40 a 80: ${c.limite});
return problemas;
}
const CABECALHO = 'porta | host | banco | limite alerta | chave';
function mostrar(c, ambiente) {
console.log( [${ambiente}]);
console.log( url local : http://${c.host}:${c.porta});
console.log( banco : ${c.dbUser}@${c.dbHost}:${c.dbPort}/${c.dbName});
console.log( limite alerta : ${c.limite} C);
console.log( chave api : ${c.chave.length} caracteres (valor nunca impresso));
const problemas = validarConfig(c);
console.log(problemas.length === 0
? ' validacao : ok, o sobe'
: validacao : FALHOU -> ${problemas.join('; ')});
}
console.log('--- o mesmo codigo, tres ambientes ---');
for (const [nome, env] of Object.entries(AMBIENTES)) {
mostrar(carregarConfig(env), nome);
}
console.log('');
console.log('--- hardcode seria isto ---');
const HARDCODE = { porta: 3000, dbHost: '127.0.0.1', dbName: 'materiais_teste' };
console.log( const PORT = ${HARDCODE.porta};);
console.log( const DB_HOST = '${HARDCODE.dbHost}';);
console.log( const DB_NAME = '${HARDCODE.dbName}';);
console.log('');
console.log('Com hardcode, a bancada da aula precisa do servidor na porta 3000');
console.log('com o banco materiais_teste. Se o servidor da aula roda em 3100 com');
console.log('materiais_producao, a placa conecta e recebe erro de porta, e ninguem');
console.log('descobre olhando o codigo: ele esta certo.');
console.log('');
console.log('--- configuracao ausente: falhar cedo ---');
const INCOMPLETOS = [
['sem porta', { HOST: '127.0.0.1', DB_HOST: '127.0.0.1', DB_NAME: 'materiais_teste', CHAVE_API: 'valor-ficticio-11', LIMITE_ALERTA_C: 32 }],
['porta 80', { PORT: 80, HOST: '127.0.0.1', DB_HOST: '127.0.0.1', DB_NAME: 'materiais_teste', CHAVE_API: 'valor-ficticio-11', LIMITE_ALERTA_C: 32 }],
['sem db_host', { PORT: 3000, HOST: '127.0.0.1', DB_PORT: 3306, DB_NAME: 'materiais_teste', CHAVE_API: 'valor-ficticio-11', LIMITE_ALERTA_C: 32 }],
['chave vazia', { PORT: 3000, HOST: '127.0.0.1', DB_HOST: '127.0.0.1', DB_PORT: 3306, DB_NAME: 'materiais_teste', CHAVE_API: '', LIMITE_ALERTA_C: 32 }],
['limite absurdo', { PORT: 3000, HOST: '127.0.0.1', DB_HOST: '127.0.0.1', DB_PORT: 3306, DB_NAME: 'materiais_teste', CHAVE_API: 'valor-ficticio-11', LIMITE_ALERTA_C: 900 }],
];
for (const [rotulo, env] of INCOMPLETOS) {
const problemas = validarConfig(carregarConfig(env));
console.log( ${rotulo.padEnd(16)} -> ${problemas.length === 0 ? 'sobe' : problemas.join('; ')});
}
console.log('');
console.log('O servidor nao sobe em nenhum dos cinco casos. Subir com configuracao');
console.log('errada custa mais caro: o primeiro sinal de que deu errado e o painel');
console.log('vazio, no minuto 25, depois de a primeira leitura ja ter sido perdida.');
console.log('');
console.log('--- a placa: cadeia de configuracao no momento da compilacao ---');
console.log(' Nao existe variavel de ambiente numa placa que so tem firmware.');
console.log(' O que existe e o -D da compilacao, e ele entra no binario.');
console.log(' pio run -e bancada -t upload -> -D PERFIL="bancada" -D INTERVALO_ENVIO_MS=10000');
console.log(' pio run -e prova -t upload -> -D PERFIL="prova" -D INTERVALO_ENVIO_MS=60000');
console.log('');
console.log(' A regra e a mesma: quem compila decide le valor. O professor troca');
console.log(' de perfil trocando uma linha do platformio.ini, e o firmware do');
console.log(' aluno nao muda em uma linha sequer.');
console.log( (host: ${os.hostname()}, ${process.platform} ${process.arch}, node ${process.version}));
fs.rmSync(PASTA, { recursive: true, force: true });
A saída real, medida nesta máquina:
--- o mesmo codigo, tres ambientes --- [sua-maquina] url local : http://127.0.0.1:3000 banco : aula@127.0.0.1:3306/materiais_teste limite alerta : 32 C chave api : 20 caracteres (valor nunca impresso) validacao : ok, o sobe [bancada-da-aula] url local : http://127.0.0.1:3100 banco : aula@127.0.0.1:3306/materiais_producao limite alerta : 32 C chave api : 19 caracteres (valor nunca impresso) validacao : ok, o sobe [servidor-final] url local : http://0.0.0.0:8080 banco : app@10.0.0.20:3306/materiais_producao limite alerta : 40 C chave api : 19 caracteres (valor nunca impresso) validacao : ok, o sobe --- hardcode seria isto --- const PORT = 3000; const DB_HOST = '127.0.0.1'; const DB_NAME = 'materiais_teste'; Com hardcode, a bancada da aula precisa do servidor na porta 3000 com o banco materiais_teste. Se o servidor da aula roda em 3100 com materiais_producao, a placa conecta e recebe erro de porta, e ninguem descobre olhando o codigo: ele esta certo. --- configuracao ausente: falhar cedo --- sem porta -> PORT fora de 1 a 65535: "NaN" porta 80 -> PORT abaixo de 1024 precisa de privilegio: 80 sem db_host -> DB_HOST ausente chave vazia -> CHAVE_API ausente ou curta demais limite absurdo -> LIMITE_ALERTA_C fora de -40 a 80: 900 O servidor nao sobe em nenhum dos cinco casos. Subir com configuracao errada custa mais caro: o primeiro sinal de que deu errado e o painel vazio, no minuto 25, depois de a primeira leitura ja ter sido perdida. --- a placa: cadeia de configuracao no momento da compilacao --- Nao existe variavel de ambiente numa placa que so tem firmware. O que existe e o -D da compilacao, e ele entra no binario. pio run -e bancada -t upload -> -D PERFIL="bancada" -D INTERVALO_ENVIO_MS=10000 pio run -e prova -t upload -> -D PERFIL="prova" -D INTERVALO_ENVIO_MS=60000 A regra e a mesma: quem compila decide o valor. O professor troca de perfil trocando uma linha do platformio.ini, e o firmware do aluno nao muda em uma linha sequer. (host: vmi3339533, linux x64, node v24.21.0)
O professor aponta quatro coisas nessa saída:
PORT fora de 1 a 65535: "NaN": o primeiro caso é variável ausente, e o número que apareceu foiNaN, não zero. A validação pegou a ausência, mas o aluno precisa entender por que apareceuNaNe não0:Number(undefined)éNaN, e é o dia 8 aula 1 que mostra queNaNpassa por toda comparação. Sem a validação, olistenreceberiaNaNe a mensagem não diria nada sobre configuração.porta 80eporta 3100: os dois aparecem, e os dois são números válidos. Uma é problema da máquina (porta ocupada), a outra é problema de privilégio. A validação pega a segunda e não a primeira, porque a primeira só aparece quando olistenfalha. O professor usa isso para mostrar que a validação de configuração não substitui o log de subida: as duas coisas são necessárias.chave api: 20 caracterese19 caracteres: três ambientes, três comprimentos diferentes, e nenhum valor na tela. É o mecanismo do dia 9 aula 1 funcionando: dá para conferir se o arquivo foi lido e se a chave chegou inteira, sem nunca mostrar a chave.(host: vmi3339533, linux x64, node v24.21.0): a linha que parece inútil e é a que o dia 16 vai precisar. Quando o sistema rodar em outra máquina, o primeiro sintoma de "o código funciona aqui e não lá" é a versão do Node. A configuração por ambiente não inclui a versão do Node, e opackage.jsonda aula 2 é a resposta parcial para isso.
O sketch da placa, que é a mesma cadeia do outro lado:
// dia 11, aula 1: o mesmo firmware, duas maquinas. // // Configuracao hardcoded e a razao do projeto funcionar na bancada do // aluno e quebrar na maquina do professor: porta 3000 ocupada, endereco // do banco diferente, caminho do arquivo de log diferente. // // Nao existe variavel de ambiente numa placa. O que existe e a cadeia de // CONFIGURACAO, e ela e montada no momento da compilacao: quem compila // decide o valor. Por isso a placa tem dois perfis de configuracao — um de // bancada e um de prova — e os dois entram no mesmo binario sem que o // professor precise editar o codigo na frente da turma. #include <Arduino.h> // =========================================================================== // PERFIL DE CONFIGURACAO // Trocar de perfil e o que o professor faz na aula: uma linha do `-D` na // hora de compilar. O codigo abaixo nao muda em uma linha sequer. // =========================================================================== #ifndef PERFIL #define PERFIL "bancada" #endif // Cada chave e lida de uma "variavel de ambiente" da compilacao. O valor // chega pelo `-D` e nunca fica no arquivo versionado. #ifndef URL_API #define URL_API "http://192.168.0.10:3000/api/leitura" #endif #ifndef INTERVALO_ENVIO_MS #define INTERVALO_ENVIO_MS 10000 #endif #ifndef CHAVE_API #define CHAVE_API "chave-ficticia-da-aula-11" #endif // Faixa de alerta. Depende do AMBIENTE fisico: a bancada fica em 32 C, // o patio ao sol passa de 40. A mesma regra, numeros diferentes. #ifndef LIMITE_ALERTA_C #define LIMITE_ALERTA_C 32.0f #endif struct Config { const char* perfil; const char* url; unsigned long intervalo_ms; const char* chave; float limite_alerta_c; }; // A unica funcao que decide qual valor vale. Todo o resto do firmware le // `cfg`, e nunca as macros. E por isso que a troca de ambiente cabe em // uma funcao e nao em trinta if espalhados pelo codigo. Config carregarConfig() { Config c; c.perfil = PERFIL; c.url = URL_API; c.intervalo_ms = INTERVALO_ENVIO_MS; c.chave = CHAVE_API; c.limite_alerta_c = LIMITE_ALERTA_C; return c; } // FALHAR CEDO: o que a placa faz quando a configuracao esta errada ou // ausente. Comparar a hora do erro e o custo de descobrir a diferenca: // com esta funcao, o erro aparece no primeiro segundo da aula; sem ela, // aparece no minuto 25, quando a primeira leitura ja foi perdida. bool configValida(const Config& c) { if (c.url == nullptr || strlen(c.url) == 0) return false; if (c.url[0] != 'h') return false; // tem de comecar com http if (c.intervalo_ms < 1000UL) return false; // abaixo disso e flood if (c.limite_alerta_c < -40.0f || c.limite_alerta_c > 80.0f) return false; return true; } void mostrarConfig(const Config& c) { Serial.print("perfil : "); Serial.println(c.perfil); Serial.print("url da api : "); Serial.println(c.url); Serial.print("intervalo : "); Serial.print(c.intervalo_ms / 1000UL); Serial.println(" s"); Serial.print("limite alerta : "); Serial.print(c.limite_alerta_c, 1); Serial.println(" C"); // A chave mostra SO o tamanho. Ver dia 9 aula 1. Serial.print("chave api : "); Serial.print(strlen(c.chave)); Serial.println(" caracteres (valor nunca impresso)"); } void setup() { Serial.begin(115200); Serial.println(); Serial.println("dia 11 aula 1 — configuracao por ambiente"); Serial.println("=========================================="); Serial.println("Compilando com: -DPERFIL=\"bancada\""); Serial.println("Nenhum destes valores esta neste arquivo. Todos"); Serial.println("chegaram de fora, na hora da compilacao."); Config cfg = carregarConfig(); mostrarConfig(cfg); Serial.println(); if (configValida(cfg)) { Serial.println("configuracao valida: o firmware sobe."); } else { Serial.println("configuracao INVALIDA: o firmware NAO sobe."); Serial.println("E melhor assim do que subir e perder leitura em silencio."); } Serial.println(); Serial.println("--- o MESMO codigo em outro ambiente ---"); Serial.println("Recompile com -DINTERVALO_ENVIO_MS=60000 e -DLIMITE_ALERTA_C=40.0"); Serial.println("e a placa passa a enviar a cada minuto, com alerta a 40 C."); Serial.println("O codigo nao mudou. O que mudou e QUEM mandou os valores."); Serial.println(); // As tres maquinas do trimestre, lado a lado. Esta e a tabela que o // professor desenha na lousa e que o aluno vai colar na parede. Serial.println("--- os tres ambientes do mesmo projeto ---"); Serial.println(" sua maquina : porta 3000, banco 127.0.0.1"); Serial.println(" bancada da aula: outra porta, o banco da maquina do professor"); Serial.println(" servidor final : porta e banco do deploy"); Serial.println(" tres .env, um codigo. Sem hardcode, e a diferenca e so o arquivo."); Serial.println(); Serial.println("--- o que NUNCA entra no arquivo ---"); Serial.println(" senha de WiFi, chave de API, senha do banco, host do deploy."); Serial.println(" Motivo: o arquivo vai para o git, e o historico nao tem volta."); Serial.println(" Nome da variavel vai. Valor nao."); } void loop() { Serial.println(); Serial.println("--- nova rodada ---"); setup(); delay(10000); }
Por que assim e não de outro jeito. O carregarConfig existe como função e não como bloco de código solto, e a razão está no comentário do arquivo: todo o resto do firmware lê cfg, e nunca as macros. Se o firmware lesse LIMITE_ALERTA_C direto em seis lugares, trocar o limite seria editar seis linhas; lendo cfg.limite_alerta_c, é uma linha. É a mesma regra do config do dia 10 aula 1, aplicada agora aos valores que mudam por máquina.
O configValida tem um teste que parece arbitrário e não é: c.url[0] != 'h'. Ele existe porque o erro de digitação mais comum em variável de ambiente é trocar HOST por HOSTNAME e acabar com uma string que não é URL, e o listen não reclama disso. O professor explica que a validação de configuração tem duas famílias: a que verifica formato (a URL começa com http, a porta é número, o limite está na faixa) e a que verifica presença (a variável existe). A primeira pega erro de digitação, e é a que a turma mais esquece.
O #ifndef em cada #define é o que permite o valor padrão, e é a diferença entre "configuração por ambiente" e "configuração obrigatória". Com o #ifndef presente, o sketch compila sem ninguém configurar nada, e roda com o padrão fictício. Sem o #ifndef, apagar o -D da compilação dá erro de compilação, que é o modo mais duro de falhar cedo. O professor explica que o material usa o padrão porque a aula roda em trinta bancadas e o valor padrão é o que garante que a placa liga e mostra alguma coisa no Serial mesmo sem configuração.
A struct Config é passada por referência constante em configValida e por valor em mostrarConfig. A diferença é deliberada: quem valida não muda a configuração, então recebe referência constante, e é o compilador que impede alguém de escrever dentro dela por engano. Quem mostra também não muda, e poderia receber referência; o valor é o caso mais simples e o professor não cria polêmica sobre isso.
O showConfig imprime o tamanho da chave e nunca o valor, e o comentário aponta para o dia 9 aula 1. Essa é a regra que atravessa o trimestre inteiro: a credencial é verificável sem ser impressa, e qualquer código que imprima valor de credencial está errado mesmo que funcione.
Criterios de correcao
| Critério | Pontos |
|---|---|
| Lista de valores hardcoded do projeto escrita, com o que deve virar configuração | 2 pontos |
.env e .env.example escritos com os mesmos nomes, e o example sem nenhum valor | 2 pontos |
| As três saídas de ambiente coladas no caderno, com a linha que muda em todos os três identificada | 1 pontos |
validarConfig escrita e os cinco motivos colados, com o caso que a configuração atual deixaria passar | 3 pontos |
| Item 5: o servidor falha ao apagar a variável, e a mensagem cita o nome da variável | 2 pontos |
Item 6: a placa gravada e anotado se o ssid da placa é nome ou valor | 1 pontos |
| Tabela dos três ambientes escrita, com as três colunas de valores | 1 pontos |
Erros comuns
| Erro | Como aparece | Correção |
|---|---|---|
| Porta escrita dentro do arquivo | "A placa procura o servidor na 3000" | "A porta é configuração por ambiente. No .env, na sua máquina, na bancada e no servidor são três valores diferentes, e o código é o mesmo." |
Number(undefined) sem validação | "O servidor disse que não pode ouvir na porta NaN" | "NaN na porta é variável ausente. A validação de configuração roda antes do listen e diz "PORT ausente", que é a mensagem que resolve." |
.env.example com senha | "Preenchi o example com os dados do banco" | "O example vai para o git. Ele tem nome e comentário, nenhum valor. Quem preenche é quem clona, na máquina dele, com a senha que o professor deu." |
require('dotenv').config() em cada módulo | "Coloquei a chamada em cada arquivo" | "Uma leitura só, no módulo de configuração. Se cada módulo lê disco, o teste do dia 12 precisa criar arquivo para rodar, e a camada de regra deixa de ser pura." |
Validar depois do listen | "Subo o servidor e só depois vejo se a porta presta" | "Falhar cedo é antes. O custo de subir errado não é o erro: é o painel vazio no minuto vinte e cinco, com a primeira leitura já perdida." |
| Validar só presença, não formato | "A variável existe, o servidor sobe" | "Presença é metade. O erro de digitação mais comum é trocar o nome da variável, e a string errada existe. Verifique o formato também: a URL começa com http, a porta é número, o limite está na faixa." |
Colocar a configuração no .ino da placa | "Passei a URL do servidor com -D na hora de gravar" | "Isso funciona e é a placa mesma. O que não pode é o valor padrão ficar no arquivo versionado com valor de verdade: o padrão do material é fictício e está comentado como tal." |
| Achar que variável de ambiente protege o valor | "A senha está em variável de ambiente, então está segura" | "Variável de ambiente é um lugar para o valor, não um lugar seguro. Quem tem acesso ao processo lê todas. Ela serve para trocar valor entre máquinas, não para escondê-lo." |
| Não validar o limite de alerta | "O alerta está em 900 e ninguém reclamou" | "O limite é a única configuração que a placa usa para decidir se um dado é estranho, e um limite de 900 não recusa nada. Ele tem a mesma validação de faixa do dia 8." |
Copiar o .env do colega | "Peguei o arquivo dele e mudei a porta" | "O arquivo do colega tem o banco, o usuário e a chave *dele*. O que se copia é o .env.example, e o que se preenche é o seu." |
| Uma variável para dois ambientes | "Usei HOST para o servidor e para o banco" | "Duas coisas, dois nomes. HOST é onde o servidor ouve, DB_HOST é onde está o banco. Quando o banco muda de máquina, é só a segunda que muda." |
Desafio extra
Escreva a validação de configuração que também confere a coerência entre os valores, e não só a existência de cada um: a porta não pode ser a mesma que a do banco mais um, o HOST não pode ser 0.0.0.0 numa máquina de aluno, e o DB_NAME não pode ser o de produção quando HOST é 127.0.0.1. Rode com os três ambientes da resolução e depois com uma configuração que viola cada uma das três regras. A resposta que o professor espera: a validação de presença pega o que está faltando, e a de coerência pega o que está presente e não faz sentido junto.
A resolucao, compilada
// dia 11, aula 1: o mesmo firmware, duas maquinas. // // Configuracao hardcoded e a razao do projeto funcionar na bancada do // aluno e quebrar na maquina do professor: porta 3000 ocupada, endereco // do banco diferente, caminho do arquivo de log diferente. // // Nao existe variavel de ambiente numa placa. O que existe e a cadeia de // CONFIGURACAO, e ela e montada no momento da compilacao: quem compila // decide o valor. Por isso a placa tem dois perfis de configuracao — um de // bancada e um de prova — e os dois entram no mesmo binario sem que o // professor precise editar o codigo na frente da turma. #include <Arduino.h> // =========================================================================== // PERFIL DE CONFIGURACAO // Trocar de perfil e o que o professor faz na aula: uma linha do `-D` na // hora de compilar. O codigo abaixo nao muda em uma linha sequer. // =========================================================================== #ifndef PERFIL #define PERFIL "bancada" #endif // Cada chave e lida de uma "variavel de ambiente" da compilacao. O valor // chega pelo `-D` e nunca fica no arquivo versionado. #ifndef URL_API #define URL_API "http://192.168.0.10:3000/api/leitura" #endif #ifndef INTERVALO_ENVIO_MS #define INTERVALO_ENVIO_MS 10000 #endif #ifndef CHAVE_API #define CHAVE_API "chave-ficticia-da-aula-11" #endif // Faixa de alerta. Depende do AMBIENTE fisico: a bancada fica em 32 C, // o patio ao sol passa de 40. A mesma regra, numeros diferentes. #ifndef LIMITE_ALERTA_C #define LIMITE_ALERTA_C 32.0f #endif struct Config { const char* perfil; const char* url; unsigned long intervalo_ms; const char* chave; float limite_alerta_c; }; // A unica funcao que decide qual valor vale. Todo o resto do firmware le // `cfg`, e nunca as macros. E por isso que a troca de ambiente cabe em // uma funcao e nao em trinta if espalhados pelo codigo. Config carregarConfig() { Config c; c.perfil = PERFIL; c.url = URL_API; c.intervalo_ms = INTERVALO_ENVIO_MS; c.chave = CHAVE_API; c.limite_alerta_c = LIMITE_ALERTA_C; return c; } // FALHAR CEDO: o que a placa faz quando a configuracao esta errada ou // ausente. Comparar a hora do erro e o custo de descobrir a diferenca: // com esta funcao, o erro aparece no primeiro segundo da aula; sem ela, // aparece no minuto 25, quando a primeira leitura ja foi perdida. bool configValida(const Config& c) { if (c.url == nullptr || strlen(c.url) == 0) return false; if (c.url[0] != 'h') return false; // tem de comecar com http if (c.intervalo_ms < 1000UL) return false; // abaixo disso e flood if (c.limite_alerta_c < -40.0f || c.limite_alerta_c > 80.0f) return false; return true; } void mostrarConfig(const Config& c) { Serial.print("perfil : "); Serial.println(c.perfil); Serial.print("url da api : "); Serial.println(c.url); Serial.print("intervalo : "); Serial.print(c.intervalo_ms / 1000UL); Serial.println(" s"); Serial.print("limite alerta : "); Serial.print(c.limite_alerta_c, 1); Serial.println(" C"); // A chave mostra SO o tamanho. Ver dia 9 aula 1. Serial.print("chave api : "); Serial.print(strlen(c.chave)); Serial.println(" caracteres (valor nunca impresso)"); } void setup() { Serial.begin(115200); Serial.println(); Serial.println("dia 11 aula 1 — configuracao por ambiente"); Serial.println("=========================================="); Serial.println("Compilando com: -DPERFIL=\"bancada\""); Serial.println("Nenhum destes valores esta neste arquivo. Todos"); Serial.println("chegaram de fora, na hora da compilacao."); Config cfg = carregarConfig(); mostrarConfig(cfg); Serial.println(); if (configValida(cfg)) { Serial.println("configuracao valida: o firmware sobe."); } else { Serial.println("configuracao INVALIDA: o firmware NAO sobe."); Serial.println("E melhor assim do que subir e perder leitura em silencio."); } Serial.println(); Serial.println("--- o MESMO codigo em outro ambiente ---"); Serial.println("Recompile com -DINTERVALO_ENVIO_MS=60000 e -DLIMITE_ALERTA_C=40.0"); Serial.println("e a placa passa a enviar a cada minuto, com alerta a 40 C."); Serial.println("O codigo nao mudou. O que mudou e QUEM mandou os valores."); Serial.println(); // As tres maquinas do trimestre, lado a lado. Esta e a tabela que o // professor desenha na lousa e que o aluno vai colar na parede. Serial.println("--- os tres ambientes do mesmo projeto ---"); Serial.println(" sua maquina : porta 3000, banco 127.0.0.1"); Serial.println(" bancada da aula: outra porta, o banco da maquina do professor"); Serial.println(" servidor final : porta e banco do deploy"); Serial.println(" tres .env, um codigo. Sem hardcode, e a diferenca e so o arquivo."); Serial.println(); Serial.println("--- o que NUNCA entra no arquivo ---"); Serial.println(" senha de WiFi, chave de API, senha do banco, host do deploy."); Serial.println(" Motivo: o arquivo vai para o git, e o historico nao tem volta."); Serial.println(" Nome da variavel vai. Valor nao."); } void loop() { Serial.println(); Serial.println("--- nova rodada ---"); setup(); delay(10000); }
Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia11 aula1.
Aula 2 — Scripts, npm e o projeto que roda com um comando
Objetivos
- Explicar para que serve o
package.jsone por que ele é o arquivo que vai para o git enquanto a pasta de instalados não vai. - Instalar o projeto do zero numa segunda pasta, e mostrar que o resultado é idêntico ao da primeira máquina.
- Distinguir
npm installdenpm ci, e dizer em uma frase o que cada um lê e o que cada um pode mudar. - Criar os
scripts do projeto, e entender quenpm starté um nome, não um comando novo. - Escrever a instrução de instalação do próprio projeto em três linhas, e testá-la numa pasta limpa.
Material
- 1 computador com Node.js 20 e
npm, por dupla - 1 terminal por dupla, com acesso à internet da escola
- Projeto do aluno versionado no git, do fim da aula 1
- Folha de papel por dupla, para a lista de "o que vai e o que não vai para o git"
- Projetor, para o
package.jsonaberto e o terminal lado a lado
Conceitos
O package.json é a identidade do projeto
O package.json é um arquivo de texto com objeto dentro, e ele é a identidade do projeto para qualquer ferramenta que precisar falar com ele. Ele tem quatro pedaços que importam nesta aula, e o professor abre o arquivo no projetor e aponta um por um:
| Chave | Para que serve | Vai para o git |
|---|---|---|
name | o nome do projeto | sim |
version | a versão do seu projeto, não das dependências | sim |
scripts | os comandos curtos do projeto | sim |
dependencies | os pacotes que o projeto precisa | sim |
A dependência é um pacote que o seu projeto usa e não escreveu. mysql2 fala MySQL para o Node, e o dotenv lê o .env. Ambos são dependência: o seu projeto depende deles, e eles não fazem parte do seu código.
Instalar pacote é o que o comando npm install seguido do nome do pacote faz, e ele tem dois efeitos que a turma precisa separar: ele baixa o pacote e ele reescreve o package.json, acrescentando o nome na lista de dependências. O efeito de reescrever o arquivo é o que costuma assustar no primeiro dia, e a razão de não assustar é que ele só acrescenta: a faixa vai para dentro do arquivo que já vai para o git, e é esse caminho que a instalação do dia 16 vai usar.
O que o package.json não tem, e essa é a metade da aula, é a versão exata do mysql2. Ele tem "mysql2": "^3.11.0", e o sinal ^ significa "a partir da 3.11.0, sem passar da 3.x". Isso é uma faixa, não uma versão. O package.json diz o que o projeto aceita, e não o que está rodando na sua máquina.
O lockfile é quem fixa, e a faixa é quem aceita
O lockfile é o arquivo que resolve essa distância, e o nome do arquivo é package-lock.json. Ele faz duas coisas que o package.json não faz: grava a versão exata de cada dependência e grava o hash de cada pacote baixado.
O hash é o resumo do conteúdo, e é o que permite ao npm dizer "esse arquivo não é o que eu esperava" se alguém trocar o conteúdo no meio do caminho. O professor explica em uma frase: o lockfile não é só a lista do que instalar, é a prova de que foi instalado exatamente aquilo.
A distinção que a turma precisa levar para o caderno é de uma linha cada, e o professor escreve as duas na lousa:
| Comando | Lê o quê | Pode mudar a versão? |
|---|---|---|
npm install | a faixa do package.json | sim, se houver versão nova dentro da faixa |
npm ci | a versão exata do package-lock.json | não, nunca |
npm ci significa "instalar do jeito que está travado", e ele apaga o node_modules antes de instalar, para garantir que não sobrou nada da instalação anterior. Essa é a diferença que o professor aponta como a mais importante na prática: npm install pode acumular coisas, e npm ci recomeça do zero todas as vezes.
O que o package.json e o package-lock.json vai para o git, e a razão é o tamanho. O que vai é poucos kilobytes; o que fica de fora são os 4 MB e 362 arquivos do node_modules, e a conta está medida na resolução. Não é que o node_modules seja feio: é que ele é recriável, e o git guarda a diferença entre versões, não o estado de uma pasta inteira.
node_modules não é versionado, e o motivo não é o tamanho
A pasta node_modules é o que o npm cria ao instalar. A lista do que não versionar começa por não versionar node_modules, e essa linha entra no .gitignore antes de qualquer outra coisa. São três razões, e o professor dá as três:
- Tamanho. 4 MB e 362 arquivos contra 5 KB de lockfile. O git guarda cada versão de cada arquivo, e um
node_modulesversionado faz o repositório crescer sem limite. - Recriabilidade. A pasta inteira sai de um comando. Não há informação nela que não esteja no
package.jsonmais opackage-lock.json. - Ruído. O
node_modulesmuda sozinho quando alguém instala. Se ele estivesse versionado, todonpm installgeraria centenas de mudanças e ogit diffdeixaria de servir para alguma coisa.
A lista do que não versionar tem um segundo item que já apareceu no dia 9 aula 1 e que o professor não deixa passar: o .env vai na mesma linha do node_modules, no mesmo .gitignore, e pelo motivo oposto. O node_modules é descartável e grande; o .env é pequeno e insubstituível, porque o valor do servidor final não está em nenhum outro lugar.
O script é um nome para um comando longo
Um script é uma entrada do objeto scripts que associate um nome a uma linha de comando. O exemplo do material é npm start, que roda node src/servidor.js, e npm test, que roda node --test.
O ganho do script não é o atalho, e o professor insiste nisso porque é o ponto que a turma costuma entender errado. Se fosse só atalho, ninguém usaria: digitar o comando inteiro é mais rápido do que lembrar o atalho. O ganho é que o comando fica gravado no arquivo do projeto, e não na memória de quem roda. O colega clona o repositório e precisa saber o nome, não o caminho, não o argumento e não a ordem.
É a mesma coisa que o Makefile faz em C e em C++, e o professor conecta: make é um script com nome, e quem usa make nunca digita o gcc inteiro. A placa tem a mesma ideia do outro lado, e o sketch da resolução mostra o platformio.ini com os comandos comentados.
Do lado da placa: platformio.ini e platformio.lock
Na placa não existe package.json nem node_modules, e a troca de nome não troca o conceito. O que monta o firmware é o platformio.ini, e o pio run -e bancada -t upload é o equivalente do npm start: um comando curto que aponta para um longo que está gravado no arquivo do projeto.
Os perfil de compilação são as entradas [env:esp32dev], [env:bancada] e [env:prova] do arquivo, e o -e escolhe qual deles. O extends é o mesmo mecanismo de herança que o extends da aula 10, e ele existe pelo mesmo motivo: o perfil de bancada e o de prova só mudam duas linhas cada, e escrever tudo de novo seria copiar a parte que não muda.
O lockfile da placa tem nome e papel próprios: o platformio.lock. Ele trava a versão do framework e do conjunto de ferramentas, e é por isso que dois alunos que compilam na mesma semana têm o mesmo firmware. Sem ele, o pio run do dia 15 baixaria a versão mais nova do framework, e o sketch que funcionava ontem pararia hoje sem que ninguém tivesse mexido em nada.
A instrução de instalação é o entregável real
A instrução de instalação é o texto que diz a alguém que nunca viu o projeto como rodá-lo, e ela é o teste final do dia. Com o package.json e o package-lock.json versionados, ela cabe em três linhas:
git clone <repositorio> cd estacao-meteorologica npm ci && cp .env.example .env && npm start
A terceira linha é a que o dia 9 aula 1 preparou: npm ci instala, e cp .env.example .env cria o arquivo local a partir do example que não tem segredo nenhum. Sem o .env.example versionado, essa linha não existe, e o primeiro erro do colega é "faltou o arquivo de configuração" em vez de "não tenho a chave".
O professor pede que a turma teste a instrução de verdade: apaga a pasta, cloneia de novo em outra máquina, ou em outro lugar da mesma máquina, e executa as três linhas. Se alguma linha precisar de explicação, a linha está longa demais ou a instrução está incompleta. É esse teste que o dia 15 aula 1 vai repetir com a sala inteira.
Atividade
Montagem:
- Nenhuma montagem nova. A placa fica sem sensor e sem rede nesta aula.
- Computador com o projeto versionado e a internet da escola.
- Escreva o
package.jsondo seu projeto comname,version,scriptsedependencies. Qual chave guarda os pacotes de que o projeto precisa, e qual é a diferença entre o que está emscriptse o que está emdependencies? - Rode
npm installno seu projeto. Quanto tempo levou, quantos arquivos apareceram na pasta de instalados e quanto pesa opackage-lock.jsonque apareceu junto? Copie os três números. - Copie o projeto para outra pasta sem a pasta de instalados, rode
npm cie compare a versão instalada com a do item 2. As duas são iguais? Se não são, por que onpm installteria pegado outra? - Explique em uma frase a diferença entre
npm installenpm ci, incluindo o que cada um lê e o que cada um faz com a pasta de instalados antes de começar. - Escreva os
scripts do seu projeto: um para subir o servidor, um para rodar os testes e um para instalar do zero. Rode cada um e cole o que saiu. - Escreva a instrução de instalação do seu projeto em três linhas, incluindo a cópia do
.env.example. Teste de verdade: apague uma pasta temporária, copie o projeto lá sem a pasta de instalados, e execute as três linhas. - Escreva a lista do que vai e do que não vai para o git no seu projeto, e diga o motivo de cada item da lista que não vai. Quantos itens dessa lista já apareceram em aulas anteriores deste trimestre?
- Em uma frase: por que
npm cifunciona igual em trinta máquinas enpm installnão? A resposta tem que mencionar o arquivo, e não o comando.
Nota: 12 pontos. Critério de fim: a instrução de instalação em três linhas, testada numa pasta limpa, e a lista do que versiona e do que não versiona.
Resolucao
Do lado do Node, o script cria um projeto, instala numa máquina, copia para outra e instala de novo com npm ci. Este foi executado nesta máquina com Node.js 24 e npm 11:
// dia 11 aula 2, lado do Node: instalar do zero, duas maquinas.
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const os = require('os');
const RAIZ = '/root/.hermes/cache/scratch/aula/projeto-aluno';
const sh = (c, cwd) => execSync(c, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
console.log(node ${process.version} | npm ${sh('npm --version', os.tmpdir()).trim()} | ${process.platform} ${process.arch});
function tamanho(p) {
let total = 0, arquivos = 0;
const anda = (d) => {
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
const f = path.join(d, e.name);
if (e.isDirectory()) anda(f);
else { total += fs.statSync(f).size; arquivos++; }
}
};
if (fs.existsSync(p)) anda(p);
return { mb: (total / 1048576).toFixed(1), arquivos };
}
// ---------------------------------------------------------------------------
// 1. O QUE ESTA NO package.json
// ---------------------------------------------------------------------------
fs.rmSync(RAIZ, { recursive: true, force: true });
fs.mkdirSync(path.join(RAIZ, 'src'), { recursive: true });
fs.writeFileSync(path.join(RAIZ, 'package.json'), JSON.stringify({
name: 'estacao-meteorologica',
version: '1.0.0',
private: true,
main: 'src/servidor.js',
scripts: { start: 'node src/servidor.js', test: 'node --test' },
dependencies: { mysql2: '^3.11.0' },
}, null, 2) + '\n');
fs.writeFileSync(path.join(RAIZ, 'src/servidor.js'), "console.log('servidor no ar');\n");
fs.writeFileSync(path.join(RAIZ, '.gitignore'), 'node_modules/\n.env\n');
console.log('--- o package.json que vai para o git ---');
console.log(fs.readFileSync(path.join(RAIZ, 'package.json'), 'utf-8').trimEnd());
console.log('');
console.log(' dependencies -> o NOME e a faixa ("^3.11.0"): "serve deste ponto em diante".');
console.log(' scripts -> o comando curto: npm start, npm test.');
console.log(' O que NAO esta aqui: la versao exata. Para isso existe o lockfile.');
console.log('');
console.log('--- npm install: a primeira maquina, do zero ---');
sh('npm install --no-audit --no-fund 2>&1', RAIZ);
const lock = path.join(RAIZ, 'package-lock.json');
const lockJson = JSON.parse(fs.readFileSync(lock, 'utf-8'));
const nm = tamanho(path.join(RAIZ, 'node_modules'));
console.log(' (saida do npm suprimida: o que interessa e o tamanho)');
console.log( node_modules: ${nm.mb} MB em ${nm.arquivos} arquivos);
console.log( package-lock.json: ${(fs.statSync(lock).size / 1024).toFixed(1)} KB);
console.log( versao travada de mysql2 no lock: ${lockJson.packages['node_modules/mysql2'].version});
console.log( Integrity (hash) gravada no lock: ${lockJson.packages['node_modules/mysql2'].integrity.slice(0, 45)}...);
console.log('');
console.log( ${((fs.statSync(path.join(RAIZ, 'package.json')).size + fs.statSync(lock).size / 1024) / 1048576 * 1024).toFixed(1)} MB de declaracao contra ${nm.mb} MB de node_modules.);
console.log(' E por isso que node_modules NAO vai para o git.');
console.log('');
console.log('--- npm ci: a SEGUNDA maquina, a de um colega ---');
{
const outra = '/root/.hermes/cache/scratch/aula/projeto-aluno-outra';
fs.rmSync(outra, { recursive: true, force: true });
fs.cpSync(RAIZ, outra, { recursive: true, filter: (s) => !s.includes('node_modules') });
console.log(' A pasta copiada tem package.json e package-lock.json, e nada mais.');
console.log( node_modules presente antes? ${fs.existsSync(path.join(outra, 'node_modules'))});
sh('npm ci --no-audit --no-fund 2>&1', outra);
const lock2 = JSON.parse(fs.readFileSync(path.join(outra, 'package-lock.json'), 'utf-8'));
const nm2 = tamanho(path.join(outra, 'node_modules'));
console.log( apos npm ci: ${nm2.mb} MB em ${nm2.arquivos} arquivos);
console.log( mysql2 instalado: ${lock2.packages['node_modules/mysql2'].version} (identico));
console.log('');
console.log(' npm ci le a versao do lock e instala ESSA, ignorando a faixa.');
console.log(' npm install respeita a faixa e pode atualizar. E a diferenca.');
fs.rmSync(outra, { recursive: true, force: true });
}
console.log('');
console.log('--- npm run: o script e um atalho, nao um comando novo ---');
console.log(' npm start equivale a: node src/servidor.js');
console.log(' npm test equivale a: node --test');
console.log('');
console.log(' E o ganho nao e o atalho. E que o comando esta no arquivo do');
console.log(' projeto: o colega nao precisa saber onde esta o arquivo, nem com');
console.log(' que argumento. Ele precisa saber o nome.');
console.log('');
console.log('--- a instrucao de instalacao cabe em tres linhas ---');
console.log(' git clone <repositorio>');
console.log(' cd estacao-meteorologica');
console.log(' npm ci && cp .env.example .env && npm start');
console.log('');
console.log(' Com o lockfile, essas tres linhas funcionam em qualquer maquina.');
console.log(' Sem o lockfile, elas funcionam ate o dia em que uma dependencia');
console.log(' publica versao nova. E o dia 15 aula 1 vai ser esse dia.');
console.log('');
console.log('--- o que o git veria ---');
const versionados = ['package.json', 'package-lock.json', '.gitignore', 'src/servidor.js'];
const ignorados = ['node_modules/', '.env'];
for (const v of versionados) console.log( versiona ${v.padEnd(24)} ${(fs.statSync(path.join(RAIZ, v)).size / 1024).toFixed(1)} KB);
for (const v of ignorados) console.log( ignora ${v.padEnd(24)} ${v === 'node_modules/' ? nm.mb + ' MB' : 'credencial'});
fs.rmSync(RAIZ, { recursive: true, force: true });
A saída real, medida nesta máquina:
node v24.21.0 | npm 11.19.0 | linux x64 --- o package.json que vai para o git --- { "name": "estacao-meteorologica", "version": "1.0.0", "private": true, "main": "src/servidor.js", "scripts": { "start": "node src/servidor.js", "test": "node --test" }, "dependencies": { "mysql2": "^3.11.0" } } dependencies -> o NOME e a faixa ("^3.11.0"): "serve deste ponto em diante". scripts -> o comando curto: npm start, npm test. O que NAO esta aqui: a versao exata. Para isso existe o lockfile. --- npm install: a primeira maquina, do zero --- (saida do npm suprimida: o que interessa e o tamanho) node_modules: 4.0 MB em 362 arquivos package-lock.json: 5.1 KB versao travada de mysql2 no lock: 3.24.5 Integrity (hash) gravada no lock: sha512-X6Ujsr2QSkkLpkQGjxzpKRAPn9nu4axpR63ntB... 0.2 MB de declaracao contra 4.0 MB de node_modules. E por isso que node_modules NAO vai para o git. --- npm ci: a SEGUNDA maquina, a de um colega --- A pasta copiada tem package.json e package-lock.json, e nada mais. node_modules presente antes? false apos npm ci: 4.0 MB em 362 arquivos mysql2 instalado: 3.24.5 (identico) npm ci le a versao do lock e instala ESSA, ignorando a faixa. npm install respeita a faixa e pode atualizar. E a diferenca. --- npm run: o script e um atalho, nao um comando novo --- npm start equivale a: node src/servidor.js npm test equivale a: node --test E o ganho nao e o atalho. E que o comando esta no arquivo do projeto: o colega nao precisa saber onde esta o arquivo, nem com que argumento. Ele precisa saber o nome. --- a instrucao de instalacao cabe em tres linhas --- git clone <repositorio> cd estacao-meteorologica npm ci && cp .env.example .env && npm start Com o lockfile, essas tres linhas funcionam em qualquer maquina. Sem o lockfile, elas funcionam ate o dia em que uma dependencia publica versao nova. E o dia 15 aula 1 vai ser esse dia. --- o que o git veria --- versiona package.json 0.2 KB versiona package-lock.json 5.1 KB versiona .gitignore 0.0 KB versiona src/servidor.js 0.0 KB ignora node_modules/ 4.0 MB ignora .env credencial
O professor aponta quatro coisas nessa saída:
node_modules: 4.0 MB em 362 arquivoscontrapackage-lock.json: 5.1 KB: a razão do.gitignoreestá nessnumber. O git guarda o histórico inteiro, e versionar 362 arquivos de biblioteca que mudam sozinhas faz o repositório inchar sem nenhum ganho. O que vai para o git é a instrução de como recriar a pasta, não a pasta.versao travada de mysql2 no lock: 3.24.5, com opackage.jsonpedindo^3.11.0: os dois números estão na tela ao mesmo tempo e eles contam a história da aula. Opackage.jsonaceitou uma versão bem mais nova porque a faixa permite; o lockfile registra qual foi. Amanhã, quando alguém rodarnpm installnuma máquina nova, ele vai pegar a 3.24.5, e não a 3.11.0, e a diferença entre "instalei o que o projeto pediu" e "instalei o que está travado" aparece.node_modules presente antes? false: a segunda máquina não tinha nada, enpm ciconstruiu os 4 MB do zero. É o que o professor quer dizer com "instalar do zero": não é reinstalar em cima, é construir de novo a partir de duas instruções de texto.ignora .env credencial: a última linha é a mais importante da tabela, e ela liga esta aula com a aula 9. Onode_modulesé descartável e grande; o.envé pequeno e insubstituível. Os dois estão na mesma linha do.gitignore, e por motivos opostos.
O sketch da placa, que é a mesma ideia do outro lado do cabo:
// dia 11, aula 2: o projeto que roda com um comando. // // Na aula 5 o aluno rodava o servidor digitando o comando inteiro: node // servidor.js. Isso quebra na primeira maquina que nao tem a mesma pasta, // a mesma versao do Node e o mesmo `node_modules`. O `package.json` resolve // as duas coisas: fixa a versao e guarda o comando. // // O script do ESP32 e a mesma ideia do outro lado: `placa-monitorar` nao // e um nome, e o conteudo de um bloco de codigo que o professor roda. // O aluno ve aqui que os dois lados do projeto tem um "um comando so". #include <Arduino.h> #include <DHT.h> const int PIN_DHT = 4; const uint8_t TIPO_SENSOR = DHT11; // =========================================================================== // O QUE UM "SCRIPT" E, DO LADO DA PLACA // // No Node, `npm start` le o package.json e roda a linha que esta escrita // la. Na placa nao existe package.json: quem monta o firmware e o // platformio.ini, e o `upload` dele e o equivalente do `npm start`. // Abaixo esta o conteudo desse arquivo, comentado, porque o aluno precisa // ver que a troca e de nome e nao de conceito. // =========================================================================== // // [env:esp32dev] // platform = espressif32 // board = esp32dev // framework = arduino // monitor_speed = 115200 // // [env:bancada] // extends = env:esp32dev // build_flags = // -D PERFIL=\"bancada\" // -D INTERVALO_ENVIO_MS=10000 // // [env:prova] // extends = env:esp32dev // build_flags = // -D PERFIL=\"prova\" // -D INTERVALO_ENVIO_MS=60000 // // Comandos: // pio run -e bancada -t upload compila e grava o perfil de bancada // pio run -e prova -t upload compila e grava o perfil de prova // pio device monitor -b 115200 abre o monitor serial // Os valores default, iguais aos do perfil de bancada. Servem para o // sketch rodar mesmo sem platformio: e o mesmo papel do default do npm. #ifndef INTERVALO_ENVIO_MS #define INTERVALO_ENVIO_MS 10000 #endif #ifndef PERFIL #define PERFIL "bancada" #endif DHT sensor(PIN_DHT, TIPO_SENSOR); // Simula o que o `pio run -t upload` faria: gravar e reiniciar. Aqui ele // so escreve no serial, porque o professor nao quer gravar 14 vezes. void simularUpload(const char* perfil) { Serial.print("[pio] escrevendo firmware do perfil "); Serial.print(perfil); Serial.println(" na placa..."); Serial.println("[pio] 100% (336896 bytes) — reiniciando a placa"); Serial.println(); } // Simula o que `npm ci` faz: instala o que esta no lockfile, na versao // travada. Na placa nao ha node_modules, mas o lockfile existe igual: // e o platformio.lock que o `pio run` gera. void simularInstalacao() { Serial.println("[pio] dependencias do platformio.lock:"); Serial.println(" framework-arduinoespressif32 @ 3.20017.241212"); Serial.println(" toolchain-xtensa-esp32 @ 8.4.0+2021r2"); Serial.println("[pio] nada instalado do zero: as versoes estão travadas no lock."); Serial.println(); } void setup() { Serial.begin(115200); Serial.println(); Serial.println("dia 11 aula 2 — scripts, npm e um comando so"); Serial.println("============================================="); simularInstalacao(); simularUpload(PERFIL); Serial.print("[script] intervalo do perfil "); Serial.print(PERFIL); Serial.print(": "); Serial.print(INTERVALO_ENVIO_MS / 1000UL); Serial.println(" s"); Serial.println(); Serial.println("--- O MESMO PROJETO, DOIS LADOS ---"); Serial.println(); Serial.println(" Node, package.json:"); Serial.println(" \"scripts\": { \"start\": \"node src/servidor.js\","); Serial.println(" \"test\": \"node --test\" }"); Serial.println(" npm start -> sobe o servidor"); Serial.println(" npm test -> roda a suite"); Serial.println(" npm ci -> instala do lockfile, versao travada"); Serial.println(); Serial.println(" Placa, platformio.ini:"); Serial.println(" pio run -e bancada -t upload"); Serial.println(" pio run -e prova -t upload"); Serial.println(" pio device monitor -b 115200"); Serial.println(); Serial.println(" Mesma ideia: o comando curto aponta para o longo, e o longo"); Serial.println(" fica gravado no arquivo do projeto, nao na memoria do professor."); Serial.println(); Serial.println("--- O QUE NAO VAI PARA O GIT ---"); Serial.println(" node_modules/ -> 40 MB, recriavel com npm ci"); Serial.println(" .env -> credencial (dia 9 aula 1)"); Serial.println(" .pio/build/ -> o binario compilado da placa"); Serial.println(); Serial.println(" O QUE VAI:"); Serial.println(" package.json -> as dependencias e os scripts"); Serial.println(" package-lock.json-> a versao exata de cada uma"); Serial.println(" platformio.ini -> os perfis de build"); Serial.println(" .gitignore -> a lista do que fica de fora"); Serial.println(); Serial.println(" Sem o lockfile, dois alunos instalam versoes diferentes e"); Serial.println(" o projeto funciona na maquina de um e quebra na do outro."); sensor.begin(); Serial.println(); Serial.print("[sensor] leitura da bancada: "); float t = sensor.readTemperature(); if (isnan(t)) { Serial.println("DHT11 nao respondeu (NaN) — o projeto segue e grava o erro."); } else { Serial.print(t, 1); Serial.println(" C"); } } void loop() { Serial.println(); Serial.println("--- nova rodada ---"); setup(); delay(10000); }
Por que assim e não de outro jeito. O sketch não executa o pio run: ele imprime o que o pio run faria. A razão é de aula, e o professor assume: gravar catorze vezes em quarenta minutos gasta o tempo que a turma precisa para pensar, e o que está em discussão é o conceito de "um comando só", não o tempo de compilação. A função simularUpload recebe o nome do perfil como parâmetro e escreve duas linhas, e a informação que a turma precisa — que o mesmo comando com -e diferente produz firmware diferente — aparece na tela em menos de um segundo.
Os dois #ifndef no começo do arquivo são o valor padrão que o comentário promises, e eles existem pelo mesmo motivo do .env.example no lado do Node: o sketch tem de compilar e rodar mesmo sem ninguém configurar nada. Sem os #ifndef, apagar o -D da linha de comando daria erro de compilação. O professor explica que a escolha do material é o valor padrão porque a aula roda em trinta bancadas, e a última escolha seria deixar metade das placas sem ligar.
O platformio.ini está comentado dentro do sketch, e essa é uma decisão deliberada que o professor justifica: o arquivo é Configuration e configuração não é código, então ele não compila, mas o aluno precisa ver o arquivo inteiro na mesma tela em que vê o sketch. É a mesma razão pela qual o package.json aparece impresso na saída do script do Node: a peça de configuração é objeto de aula, e escondê-la atrás de um comando deixa a turma sem o objeto.
O isnan na leitura do DHT11 é o dia 8 aula 1 aparecendo no lugar esperado, e não é enfeite: o pio run ter sucesso não significa que o sensor respondeu. São duas coisas diferentes, uma é o build e a outra é o dado, e o professor passa na lousa a lista do que um script garante: o comando roda, a versão é a travada, o firmware é gravado. O que ele não garante é que a temperatura seja um número.
Criterios de correcao
| Critério | Pontos |
|---|---|
package.json escrito com as quatro chaves, e a diferença entre scripts e dependencies explicada | 2 pontos |
Item 2: as três medidas do npm install coladas (tempo, arquivos, tamanho do lockfile) | 2 pontos |
| Item 3: a versão instalada pela segunda máquina comparada com a da primeira | 2 pontos |
Item 4: install e ci diferenciados em uma frase, com o que cada um lê e o que faz com a pasta de instalados | 2 pontos |
scripts do projeto escritos e os três executados, com a saída colada | 2 pontos |
| Item 6: a instrução de instalação em três linhas, testada numa pasta limpa | 1 pontos |
| Lista do que versiona e do que não versiona, com o motivo de cada exclusão | 1 pontos |
Erros comuns
| Erro | Como aparece | Correção |
|---|---|---|
Versionar node_modules | "Commitei a pasta toda para não perder" | "Ela não se perde: sai de um comando. São 4 MB e 362 arquivos de biblioteca que mudam sozinhas. O que vai para o git é a instrução de como recriá-la." |
.env fora do .gitignore | "O .env está junto do package.json" | "O node_modules é descartável; o .env é insubstituível. Os dois ficam de fora, e por motivos opostos. Releia a aula 9 antes do git push." |
npm install em vez de npm ci na instrução | "Escrevi npm install porque é mais curto" | "Mais curto e mais perigoso: o install respeita a faixa e pode pegar versão nova. A instrução de instalação de um projeto usa ci, e o install fica para o desenvolvimento do dia a dia." |
Achar que ^3.11.0 fixa a versão | "Coloquei a faixa, então está travado" | "A faixa é "a partir da 3.11". Quem trava é o package-lock.json, com a versão exata e o hash. As duas coisas juntas é que dão reprodutibilidade." |
script apontando para caminho errado | "O npm start dá erro de módulo não encontrado" | "O caminho no script é relativo à raiz do projeto, não à pasta do package.json. Escreva node src/servidor.js, e não node servidor.js." |
Esquecer o .env.example na instrução | "O colega reclamou que faltou o arquivo de configuração" | "A terceira linha é npm ci && cp .env.example .env && npm start. O cp é o que fecha: sem ele o primeiro erro do colega não é o erro dele." |
package.json com script que abre banco | "\"start\": \"node src/servidor.js --db=materiais_teste\"" | "O nome da máquina não vai no arquivo do projeto: vai no .env. O script é o mesmo nas três máquinas, e a configuração é que muda." |
Ignorar o package-lock.json no commit | "Não versionei o lock, é só um arquivo gerado" | "É justamente por ele ser gerado que precisa ser versionado. Sem o lock no repositório, o npm ci do colega não tem contra o que instalar a versão certa." |
node_modules com restos e duvidar do teste | "Rodei o npm install duas vezes e deu versão diferente" | "Isso é o install acumulando. É por isso que npm ci apaga a pasta antes de instalar: para o resultado não depender do que já estava lá." |
Colocar a versão do Node no package.json e achar que resolve | "Declarei a versão do Node e o problema sumiu" | "O campo de versão do Node é dica, não trava: o npm avisa e segue. Para travar de verdade, o package.json ajuda e o time de instalação, não, resolve." |
Esquecer o .pio/build no .gitignore | "Commitei o binário da placa" | "O binário compilado é saída de build, e sai de novo com um comando. Ele vai na lista com o node_modules, pelo mesmo motivo: recriável." |
Desafio extra
Escreva a instrução de instalação completa do seu projeto, incluindo a parte que não é npm: o que a pessoa faz com a placa depois de instalar o servidor, em que pino o DHT11 fica, qual biblioteca a plataforma precisa e o que fazer se o DHT11 devolver NaN na primeira leitura. Teste a instrução inteira num computador que não seja o seu: alguém que nunca viu o projeto tem de conseguir rodar e ver uma linha de log. O que o professor mede no fim é quantas perguntas essa pessoa precisou fazer, e a resposta esperada é zero.
A resolucao, compilada
// dia 11, aula 2: o projeto que roda com um comando. // // Na aula 5 o aluno rodava o servidor digitando o comando inteiro: node // servidor.js. Isso quebra na primeira maquina que nao tem a mesma pasta, // a mesma versao do Node e o mesmo `node_modules`. O `package.json` resolve // as duas coisas: fixa a versao e guarda o comando. // // O script do ESP32 e a mesma ideia do outro lado: `placa-monitorar` nao // e um nome, e o conteudo de um bloco de codigo que o professor roda. // O aluno ve aqui que os dois lados do projeto tem um "um comando so". #include <Arduino.h> #include <DHT.h> const int PIN_DHT = 4; const uint8_t TIPO_SENSOR = DHT11; // =========================================================================== // O QUE UM "SCRIPT" E, DO LADO DA PLACA // // No Node, `npm start` le o package.json e roda a linha que esta escrita // la. Na placa nao existe package.json: quem monta o firmware e o // platformio.ini, e o `upload` dele e o equivalente do `npm start`. // Abaixo esta o conteudo desse arquivo, comentado, porque o aluno precisa // ver que a troca e de nome e nao de conceito. // =========================================================================== // // [env:esp32dev] // platform = espressif32 // board = esp32dev // framework = arduino // monitor_speed = 115200 // // [env:bancada] // extends = env:esp32dev // build_flags = // -D PERFIL=\"bancada\" // -D INTERVALO_ENVIO_MS=10000 // // [env:prova] // extends = env:esp32dev // build_flags = // -D PERFIL=\"prova\" // -D INTERVALO_ENVIO_MS=60000 // // Comandos: // pio run -e bancada -t upload compila e grava o perfil de bancada // pio run -e prova -t upload compila e grava o perfil de prova // pio device monitor -b 115200 abre o monitor serial // Os valores default, iguais aos do perfil de bancada. Servem para o // sketch rodar mesmo sem platformio: e o mesmo papel do default do npm. #ifndef INTERVALO_ENVIO_MS #define INTERVALO_ENVIO_MS 10000 #endif #ifndef PERFIL #define PERFIL "bancada" #endif DHT sensor(PIN_DHT, TIPO_SENSOR); // Simula o que o `pio run -t upload` faria: gravar e reiniciar. Aqui ele // so escreve no serial, porque o professor nao quer gravar 14 vezes. void simularUpload(const char* perfil) { Serial.print("[pio] escrevendo firmware do perfil "); Serial.print(perfil); Serial.println(" na placa..."); Serial.println("[pio] 100% (336896 bytes) — reiniciando a placa"); Serial.println(); } // Simula o que `npm ci` faz: instala o que esta no lockfile, na versao // travada. Na placa nao ha node_modules, mas o lockfile existe igual: // e o platformio.lock que o `pio run` gera. void simularInstalacao() { Serial.println("[pio] dependencias do platformio.lock:"); Serial.println(" framework-arduinoespressif32 @ 3.20017.241212"); Serial.println(" toolchain-xtensa-esp32 @ 8.4.0+2021r2"); Serial.println("[pio] nada instalado do zero: as versoes estão travadas no lock."); Serial.println(); } void setup() { Serial.begin(115200); Serial.println(); Serial.println("dia 11 aula 2 — scripts, npm e um comando so"); Serial.println("============================================="); simularInstalacao(); simularUpload(PERFIL); Serial.print("[script] intervalo do perfil "); Serial.print(PERFIL); Serial.print(": "); Serial.print(INTERVALO_ENVIO_MS / 1000UL); Serial.println(" s"); Serial.println(); Serial.println("--- O MESMO PROJETO, DOIS LADOS ---"); Serial.println(); Serial.println(" Node, package.json:"); Serial.println(" \"scripts\": { \"start\": \"node src/servidor.js\","); Serial.println(" \"test\": \"node --test\" }"); Serial.println(" npm start -> sobe o servidor"); Serial.println(" npm test -> roda a suite"); Serial.println(" npm ci -> instala do lockfile, versao travada"); Serial.println(); Serial.println(" Placa, platformio.ini:"); Serial.println(" pio run -e bancada -t upload"); Serial.println(" pio run -e prova -t upload"); Serial.println(" pio device monitor -b 115200"); Serial.println(); Serial.println(" Mesma ideia: o comando curto aponta para o longo, e o longo"); Serial.println(" fica gravado no arquivo do projeto, nao na memoria do professor."); Serial.println(); Serial.println("--- O QUE NAO VAI PARA O GIT ---"); Serial.println(" node_modules/ -> 40 MB, recriavel com npm ci"); Serial.println(" .env -> credencial (dia 9 aula 1)"); Serial.println(" .pio/build/ -> o binario compilado da placa"); Serial.println(); Serial.println(" O QUE VAI:"); Serial.println(" package.json -> as dependencias e os scripts"); Serial.println(" package-lock.json-> a versao exata de cada uma"); Serial.println(" platformio.ini -> os perfis de build"); Serial.println(" .gitignore -> a lista do que fica de fora"); Serial.println(); Serial.println(" Sem o lockfile, dois alunos instalam versoes diferentes e"); Serial.println(" o projeto funciona na maquina de um e quebra na do outro."); sensor.begin(); Serial.println(); Serial.print("[sensor] leitura da bancada: "); float t = sensor.readTemperature(); if (isnan(t)) { Serial.println("DHT11 nao respondeu (NaN) — o projeto segue e grava o erro."); } else { Serial.print(t, 1); Serial.println(" C"); } } void loop() { Serial.println(); Serial.println("--- nova rodada ---"); setup(); delay(10000); }
Sem saída de compilação gravada. Rode python3 validar.py -t 2 dia11 aula2.
