Configuração e ambiente — Arduino e IoT — semana 11 do 2o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 11 · Configuração e ambiente — Material de Apoio Arduino

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 .env do seu projeto com as variáveis que o servidor usa, e o .env.example que vai para o git.
  • Fazer o dotenv carregar 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, dotenv e mysql2 instalados
  • Arquivo .env do 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 mudaNa sua máquinaNa bancadaNo servidor final
a porta300031008080
o endereço do banco127.0.0.1o do professoro do deploy
o usuário do bancoo seuo da aulao da aplicação
o limite de alerta323240

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.
  1. 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.
  2. Escreva o .env do seu projeto com as variáveis que o servidor usa, e escreva o .env.example ao lado, com os mesmos nomes e nenhum valor. Confira: o example tem alguma senha? Não pode ter.
  3. 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?
  4. Escreva a função validarConfig do 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?
  5. Apague uma variável do seu .env e 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 do listen.
  6. 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 ssid da sua placa é nome ou valor?
  7. 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.
  8. 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 foi NaN, não zero. A validação pegou a ausência, mas o aluno precisa entender por que apareceu NaN e não 0: Number(undefined) é NaN, e é o dia 8 aula 1 que mostra que NaN passa por toda comparação. Sem a validação, o listen receberia NaN e a mensagem não diria nada sobre configuração.
  • porta 80 e porta 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 o listen falha. 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 caracteres e 19 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 o package.json da 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érioPontos
Lista de valores hardcoded do projeto escrita, com o que deve virar configuração2 pontos
.env e .env.example escritos com os mesmos nomes, e o example sem nenhum valor2 pontos
As três saídas de ambiente coladas no caderno, com a linha que muda em todos os três identificada1 pontos
validarConfig escrita e os cinco motivos colados, com o caso que a configuração atual deixaria passar3 pontos
Item 5: o servidor falha ao apagar a variável, e a mensagem cita o nome da variável2 pontos
Item 6: a placa gravada e anotado se o ssid da placa é nome ou valor1 pontos
Tabela dos três ambientes escrita, com as três colunas de valores1 pontos

Erros comuns

ErroComo apareceCorreçã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.json e 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 install de npm 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 que npm 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.json aberto 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:

ChavePara que serveVai para o git
nameo nome do projetosim
versiona versão do seu projeto, não das dependênciassim
scriptsos comandos curtos do projetosim
dependenciesos pacotes que o projeto precisasim

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:

ComandoLê o quêPode mudar a versão?
npm installa faixa do package.jsonsim, se houver versão nova dentro da faixa
npm cia versão exata do package-lock.jsonnã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:

  1. Tamanho. 4 MB e 362 arquivos contra 5 KB de lockfile. O git guarda cada versão de cada arquivo, e um node_modules versionado faz o repositório crescer sem limite.
  2. Recriabilidade. A pasta inteira sai de um comando. Não há informação nela que não esteja no package.json mais o package-lock.json.
  3. Ruído. O node_modules muda sozinho quando alguém instala. Se ele estivesse versionado, todo npm install geraria centenas de mudanças e o git diff deixaria 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.
  1. Escreva o package.json do seu projeto com name, version, scripts e dependencies. Qual chave guarda os pacotes de que o projeto precisa, e qual é a diferença entre o que está em scripts e o que está em dependencies?
  2. Rode npm install no seu projeto. Quanto tempo levou, quantos arquivos apareceram na pasta de instalados e quanto pesa o package-lock.json que apareceu junto? Copie os três números.
  3. Copie o projeto para outra pasta sem a pasta de instalados, rode npm ci e compare a versão instalada com a do item 2. As duas são iguais? Se não são, por que o npm install teria pegado outra?
  4. Explique em uma frase a diferença entre npm install e npm ci, incluindo o que cada um lê e o que cada um faz com a pasta de instalados antes de começar.
  5. 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.
  6. 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.
  7. 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?
  8. Em uma frase: por que npm ci funciona igual em trinta máquinas e npm install nã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 arquivos contra package-lock.json: 5.1 KB: a razão do .gitignore está 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 o package.json pedindo ^3.11.0: os dois números estão na tela ao mesmo tempo e eles contam a história da aula. O package.json aceitou uma versão bem mais nova porque a faixa permite; o lockfile registra qual foi. Amanhã, quando alguém rodar npm install numa 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, e npm ci construiu 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. O node_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érioPontos
package.json escrito com as quatro chaves, e a diferença entre scripts e dependencies explicada2 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 primeira2 pontos
Item 4: install e ci diferenciados em uma frase, com o que cada um lê e o que faz com a pasta de instalados2 pontos
scripts do projeto escritos e os três executados, com a saída colada2 pontos
Item 6: a instrução de instalação em três linhas, testada numa pasta limpa1 pontos
Lista do que versiona e do que não versiona, com o motivo de cada exclusão1 pontos

Erros comuns

ErroComo apareceCorreçã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.