Aprofundamento por escolha — Arduino e IoT — semana 14 do 3o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 14 · Aprofundamento por escolha — Material de Apoio Arduino

Semana 14 de 15· 3o trimestre · 05/09 a 10/12

Aprofundamento por escolha

Cada aluno escolhe um caminho e aprofunda. De onde vem a duvida real.

Aula 1 — Caminho A: mais firmware

Objetivos

firmware: como uma placa em campo recebe firmware novo pela rede, sem cabo, e o que acontece quando a imagem nova falha.

  • Criar duas tarefas com FreeRTOS e mostrar que a latencia de uma não afeta a da outra.
  • Configurar uma interrupcao e medir que o código roda fora do loop, e explicar o limite de tempo da ISR.
  • Medir o consumo de energia da placa acordada e em deep sleep avancado, e dizer o que ela perde ao dormir.
  • Configurar o watchdog timer e explicar que a placa se reinicia sozinha quando trava.

Material

  • 1 ESP32 DevKit V1 por aluno, com o cabo USB
  • 1 botao com pull-up no pino 0, para entrar em sono profundo
  • 1 sensor ou jumper no pino 4, para gerar o evento da interrupcao
  • Monitor serial a 115200, que fica aberto durante a aula inteira
  • 1 multímetro em serie, para a conta de consumo

Conceitos

OTA: por que a flash tem duas copias

Uma placa em campo não aceita cabo USB. O caminho de atualizar firmware pela rede existe, e o nome e OTA, de *over the air* — atualiza o firmware pela rede.

A razão de existirem duas particoes não e redundancia, e seguranca: sem a copia antiga, uma imagem com defeito deixa a placa sem firmware. O espaco de flash e o orcamento de tudo que cabe nela. A sequência de particao e sempre a mesma:

  1. a imagem nova vai para a particao que não esta rodando
  2. a proxima vez que a placa reinicia, ela le a nova
  3. se a nova deu erro, o boot volta para a antiga sozinha

O item 3 e o que separa OTA de risco. A imagem antiga so desaparece quando a nova e confirmada como funcional. E o preco: a particao do programa e metade da flash, então firmware grande demais deixa de caber e o build passa a falhar com erro de espaco — um erro que so aparece quando o firmware já cresceu.

As duas chamadas que a aula usa tem nomes parecidos e não são a mesma coisa: esp_ota_get_running_partition responde de qual particao eu vim, e esp_ota_get_next_update_partition responde para onde vai a proxima imagem. Trocar uma pela outra produz um sistema que atualiza a particao que já estava rodando.

O WiFi provisioning entra aqui como o outro lado do mesmo problema: a senha do WiFi não pode estar no binario, porque quem tem o arquivo de firmware tem a rede da sua casa. A placa entra em modo Access Point sem senha conhecida, o celular entrega a rede, e a placa grava na memória persistente. E a mesma lógica do token, aplicada ao WiFi.

FreeRTOS: duas tarefas, dois núcleos

Até agora o programa tinha um loop. Quando a aplicacao cresce, uma so função comeca a fazer demais: esperar a rede, medir o sensor, atender o pedido. E a espera da rede trava a medicao, e a medicao atrasa o pedido.

Uma tarefa resolve isso, com a prioridade decidindo quem roda primeiro:

TarefaPrioridadePor que
rede5perder um pedido custa mais que atrasar uma leitura
sensor3atrasar 10 ms não custa nada
principal1o loop que você conhece

A palavra "simultanea" precisa de cuidado: são dois núcleos, e cada núcleo roda uma tarefa por vez. Duas tarefas no mesmo núcleo se alternam por tempo, e ai a prioridade importa de verdade — a de maior número roda primeiro e pode adiar a outra.

O que a aula mede e o ponto: as duas tarefas executam no mesmo intervalo de um segundo, e o tempo de cada uma e o mesmo. Em um loop unico, o tempo da rede teria ENSERIDO o sensor terminar antes. Essa e a latencia isolada, e e o motivo de a tarefa existir.

O semaforo e o que vem quando duas tarefas compartilham um recurso: sensor de I2C, memória, conexão. A aula não usa porque o exemplo do dia não tem recurso compartilhado, e o professor explica por que não forçou: semaforo sem recurso compartilhado e sintaxe sem motivo.

Interrupcao: a ISR e o código fora do loop

A interrupcao resolve um caso especifico: o evento tem que ser respondido agora, e não no proximo ciclo do loop. Encoder, sensor ultrassom, botao com pressão longa, pulso de encoder — todos esses precisam de resposta imediata.

O ISR e a função que roda no evento, e ela tem três restricoes que o professor dita:

  1. orcamento de tempo: no ESP32, cerca de um microsegundo. Passou disso, o sistema reinicia.
  2. sem printing: Serial.print dentro da ISR trava a placa. O trabalho de verdade vai para a tarefa.
  3. so incremente e guarde: a ISR incrementa contador, grava o valor, e sai.

O que o aluno ve na pratica: a placa dispara 50 eventos, e o loop estava ocupado imprimindo. A ISR contou os 50. Em um loop sem interrupcao, esses 50 eventos seriam vistos so no proximo ciclo, e o atraso seria o tempo inteiro do loop.

O limite de um microsegundo e o que separa "usar ISR" de "criar problema". A regra pratica do professor: se a ISR precisa de mais que um relogio, ela e uma tarefa e não uma ISR.

Energia: o que deep sleep compra e o que custa

ModoConsumo tipicoDuracao com bateria de 2000 mAh
sempre acordada, com WiFicerca de 80 mAcerca de 25 horas
acordada, so em repousocerca de 20 mAcerca de 100 horas
deep sleepcerca de 10 microamperesalguns meses

A conta e divisão de capacidade por consumo, e ela e simples de propósito: o que a tabela revela não e o modelo do sensor, e a decisao de projeto de quantas vezes por segundo a placa acorda.

O quinto núcleo e o que vem depois do sono: o temporizador que dispara a medicao sozinho, independente do loop. Ele e o que garante o período quando o loop trava, e a razão de a tarefa do sensor da resolucao contar execuções em vez de so ler uma vez.

O preco do sono e duplo, e o professor escreve os dois:

  • a placa perde a conexão ao dormir, e reconecta do zero ao acordar;
  • o servidor ve o dispositivo sumir entre os envios, e o painel precisa tratar "sem dado ha 40 segundos" como normal.

Sem a segunda regra, o painel mostra queda de dispositivo a cada ciclo, e o aluno passa o tempo apagando alerta falso. E a diferença entre um projeto com sono e um projeto que so sabe gritar.

deep sleep não e um estado do loop: e desligamento. A função que entra nele não volta, e o código recomeca do setup, com a RAM apagada. O que sobrevive e o que esta na flash ou na memória persistente — token, id e intervalo de envio são exatamente os três valores que precisam sobreviver, e por isso vivem fora do loop.

watchdog: a placa que se vigia

O watchdog e um relogio que corre em segundo plano e que o código precisa alimentar. Se o código travar e parar de alimentar, o relogio estoura e o sistema reinicia sozinho.

E a defesa contra o pior defeito de firmware de dispositivo: o travamento silencioso. Sem watchdog, uma placa que trava e uma placa morta, e o dono descobre semanas depois. Com watchdog, ela volta sozinha, e o log de boot do proximo ciclo mostra que reiniciou.

A configuração da aula e de dez segundos, com disparo de pânico. Dez segundos e generoso para firmware que dorme: se a placa esta em deep sleep, ela não alimenta o watchdog, e o tempo de sono precisa ser menor que o limite — ou o relogio precisa ser pausado para o sono.

A combinacao que o professor fecha: watchdog para a placa acordada, tempo limite em cada espera, e log no boot. As três juntas transformam "a placa morreu" em "a placa reiniciou as 3h14, e o log diz por que".

Atividade

Montagem: botao com pull-up no pino 0, jumper de saída no pino 4 para gerar o evento do sensor, ground no GND. O botao e o que coloca a placa em sono profundo; o pino 4 e o que dispara a interrupcao.

  1. Grave o sketch da resolucao e rode no serial. Anote a particao rodando e a particao de destino da proxima imagem. As duas são diferentes?
  2. Troque VERSAO_FIRMWARE para "2.1.0", grave de novo e veja o que muda no log de boot. O que o número de versão responde que o tamanho da imagem não responde?
  3. Anote o número de execuções e o ultimo tempo de cada uma das duas tarefas. As duas rodaram no mesmo intervalo de um segundo? Se você tirar a prioridade da rede, o que muda?
  4. Olhe os dois blocos de simulacao de evento: 20 pulsos e 50 pulsos. Quantas vezes a ISR rodou em cada? O loop estava parado? Escreva em uma frase por que a contagem bateu.
  5. Com o multímetro em serie, anote a corrente com o WiFi ligado e depois em deep sleep. Quantas vezes a primeira e maior? Se o deep sleep dura 30 s, quantas vezes por hora a placa acorda?
  6. Descomente a chamada de deep sleep no fim do loop, grave e aperte o botao. O que a placa imprime ao acordar? O que sumiu e o que ficou? Se a ISR estivesse rodando durante o sono, o que aconteceria?

Nota: 12 pontos. Critério de fim: as duas particoes anotadas e distintas, e a conta de consumo feita com o número do seu multímetro.

Resolucao

// Aula 1 do dia 14: Caminho A, mais firmware.
//
// Este e o caminho de quem terminou o ano e quer ir fundo na placa. A aula
// nao introduz novidade do zero: ela junta o que o curso ja usou e mostra o
// que a placa faz quando a aplicacao deixa de ser um unico `loop`.
//
// Quatro nucleos, na ordem em que aparecem quando a aplicacao cresce:
//
//   1. ATUALIZAR SEM CABO (OTA): placa em campo nao aceita cabo USB. O
//      firmware se recebe pela rede, e isso tem preco: duas particoes, uma
//      imagem, e um boot que so troca de particao se a imagem nova funcionar.
//   2. TAREFAS (FreeRTOS): enquanto uma tarefa espera a rede, outra mede.
//      Concorrencia e o que sai de um `loop` unico quando a latencia conta.
//   3. INTERRUPCAO (ISR): o evento chega, o codigo roda fora do `loop`.
//      E o que separa "reagir em 5 ms" de "reagir no proximo ciclo".
//   4. ENERGIA E GUARDIAN: o que a placa consome acordada, o que consome
//      dormindo, e o `watchdog` que reinicia a placa quando ela trava.
//
// Nada aqui fala com servidor. A placa sozinha e o laboratorio, e o serial
// e o unico instrumento: cada nucleo imprime o numero que responde a
// pergunta da secao.

#include <Arduino.h>
#include <WiFi.h>
#include <esp_ota_ops.h>
#include <esp_partition.h>
#include <esp_sleep.h>
#include <esp_task.h>
#include <esp_timer.h>
#include <esp_task_wdt.h>

// Rede do laboratorio. Os dois valores sao FICTICIOS de proposito: nenhum
// segredo real entra em sketch de exemplo, nem em material, nem no git.
#define WIFI_SSID "lab-esp32-aula"
#define WIFI_PASSWORD "troque-esta-pela-sua"

// Versao do firmware. E a prova de que a imagem nova entrou: o numero muda,
// o log muda, e o professor ve sem tirar a placa da bancada.
#define VERSAO_FIRMWARE "2.0.0"

#define ID_DISPOSITIVO "esp32-bancada-01"

// ------------------------------------------------------------------ pinos

const int PIN_LED_STATUS = 2;
const int PIN_BOTAO_INTERRUPCAO = 0;
const int PIN_SENSOR_INTERROMPIDO = 4;

// ------------------------------------------------- 1. atualizacao sem cabo

// Tamanho da flash do ESP32: 4 MB. E o orcamento de tudo que cabe nela.
const unsigned long TAMANHO_FLASH = 4UL * 1024UL * 1024UL;

// Tempo limite de conexao. Todo laco de espera precisa de um.
const unsigned long TEMPO_LIMITE_CONEXAO_MS = 20000;

// ------------------------------------------------------ 2. tarefas

// Prioridade das tarefas: numero maior roda primeiro. A tarefa de rede
// (atender) e prioritaria sobre a tarefa de sensor, porque perder um pedido
// HTTP custa mais que atrasar uma leitura em 10 ms.
const unsigned int PRIORIDADE_DA_REDE = 5;
const unsigned int PRIORIDADE_DO_SENSOR = 3;

// Periodo do temporizador que mede o sensor. Um timer que dispara sozinho e
// o que garante o periodo mesmo quando o `loop` trava.
const unsigned long PERIODO_DO_SENSOR_US = 1000000;

// ---------------------------------------------------------- 3. interrupcao

// Tempo maximo que uma ISR pode rodar antes de travar o sistema: 1 micropor
// segundo no Xtensa. Tudo que a interrupcao faz tem de caber nesse tempo.
const unsigned long DURACAO_MAXIMA_DA_ISR = 1;

// Numero de pulsos que a ISR vai contar. A aula 4 usou encoder; aqui a
// interrupcao conta evento, que e a forma mais comum no mundo real.
volatile unsigned int pulsosNaInterrupcao = 0;

// ------------------------------------------------------------ 4. energia

// Consumo medio do ESP32 em cada modo, em microamperes. Os numeros sao de
// tabela do modulo e servem para a conta da aula, nao para orcamento.
const float CONSUMO_SEMPRE_ACORDADO = 80000.0;
const float CONSUMO_SEM_SOMENTE_RF = 20000.0;
const float CONSUMO_EM_SONO_PROFUNDO = 10.0;

// Tempo que a placa dorme entre um envio e o seguinte. 30 s e um numero
// teaching: da algumas horas de bateria e ainda da tempo de o servidor ver
// a placa viva.
const unsigned long SONO_ENTRE_ENVIOS_US = 30000000ULL;

// ------------------------------------------------------------------ tipos

struct Particao {
  const char* rotulo;
  const char* tipo;
  unsigned int tamanho;
  const char* papel;
};

struct Tarefa {
  const char* nome;
  unsigned int prioridade;
  unsigned long execucoes;
  unsigned long ultimoMicros;
};

struct ComparacaoDeConsumo {
  const char* modo;
  float consumoMicroAmpere;
  float horasDeBateria;
};

// ------------------------------------------------------------------ rede

bool conectarNaRede() {
  Serial.println("[rede] conectando em " + String(WIFI_SSID) + " ...");
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  unsigned long comeco = millis();
  while (WiFi.status() != WL_CONNECTED && (millis() - comeco) < TEMPO_LIMITE_CONEXAO_MS) {
    delay(200);
  }

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[rede] FALHOU apos " + String(TEMPO_LIMITE_CONEXAO_MS / 1000) + " s");
    return false;
  }
  Serial.println("[rede] conectado em " + String(millis() - comeco) + " ms | IP " +
                 String(WiFi.localIP()));
  return true;
}

// ------------------------------------------------- 1. atualizacao sem cabo

// Le a tabela de particoes e mostra o que a placa tem gravado. E a primeira
// coisa que se imprime numa investigacao de "a placa atualizou?": se a
// particao ativa nao for `ota_0`, a imagem nova nao subiu.
void mostrarParticoes() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" 1. OTA — atualizar o firmware pela rede");
  Serial.println("========================================");
  Serial.println("a flash de " + String(TAMANHO_FLASH / (1024UL * 1024UL)) +
                 " MB guarda duas copias do programa:");
  Serial.println();

  Particao particoes[3];
  particoes[0].rotulo = "ota_0";
  particoes[0].tipo = "app";
  particoes[0].tamanho = 1966080;
  particoes[0].papel = "roda agora (a imagem ativa)";

  particoes[1].rotulo = "ota_1";
  particoes[1].tipo = "app";
  particoes[1].tamanho = 1966080;
  particoes[1].papel = "recebe a imagem nova";

  particoes[2].rotulo = "nvs";
  particoes[2].tipo = "dados";
  particoes[2].tamanho = 24576;
  particoes[2].papel = "token, id, memoria persistente";

  for (int i = 0; i < 3; i++) {
    Serial.println("  " + String(particoes[i].rotulo) + " | " + String(particoes[i].tipo) +
                   " | " + String(particoes[i].tamanho / 1024) + " KB | " +
                   String(particoes[i].papel));
  }
  Serial.println();

  // As duas APIs que respondem "de qual particao eu vim?" e "para onde vai
  // a proxima imagem?". E aqui que o nome certo importa: existe uma funcao
  // `esp_ota_get_running_partition` e uma `esp_ota_get_next_update_partition`,
  // e elas nao sao a mesma coisa.
  const esp_partition_t* rodando = esp_ota_get_running_partition();
  const esp_partition_t* proxima = esp_ota_get_next_update_partition(NULL);

  if (rodando != NULL) {
    Serial.println("particao rodando agora: " + String(rodando->label) + " (" +
                   String(rodando->size / 1024) + " KB)");
  } else {
    Serial.println("particao rodando agora: desconhecida");
  }
  if (proxima != NULL) {
    Serial.println("proxima imagem vai para: " + String(proxima->label) + " (" +
                   String(proxima->size / 1024) + " KB)");
  } else {
    Serial.println("proxima imagem: sem particao de destino livre");
  }
  Serial.println();
  Serial.println("como uma placa em campo atualiza, sem cabo:");
  Serial.println("  1. a imagem nova vai para a particao que NAO esta rodando");
  Serial.println("  2. a proxima vez que a placa reinicia, ela le a nova");
  Serial.println("  3. se a nova deu erro, o boot volta para a antiga sozinha");
  Serial.println();
  Serial.println("o item 3 e o que separa OTA de risco. A imagem antiga so");
  Serial.println("some quando a nova e confirmada como funcional. Gravar a");
  Serial.println("imagem por cima da que roda e o que transforma uma atualizacao");
  Serial.println("em tijolo no meio do poste.");
  Serial.println();
  Serial.println("o custo: a particao do programa e metade da flash. Firmware");
  Serial.println("grande demais deixa de caber em uma particao, e ai o build");
  Serial.println("passa a falhar com erro de espaco.");
  Serial.println();
  Serial.println("provisionamento: rede e senha fora do binario. A placa entra em");
  Serial.println("modo Access Point sem senha conhecida, o celular entrega a");
  Serial.println("rede, e a placa grava na memoria persistente. Quem tem o");
  Serial.println("arquivo de firmware nao tem a rede da sua casa.");
  Serial.println();
}

// ------------------------------------------------------ 2. tarefas

Tarefa tarefas[3];
int totalDeTarefas = 0;

// A tarefa da rede: enquanto ela espera conexao, a outra roda. E a diferenca
// entre "atender um pedido" e "responder no proximo loop".
void tarefaDaRede(void* param) {
  (void)param;
  int indice = 0;
  for (int i = 0; i < totalDeTarefas; i++) {
    if (String(tarefas[i].nome) == "rede") {
      indice = i;
    }
  }
  for (int volta = 0; volta < 3; volta++) {
    int64_t inicio = esp_timer_get_time();
    tarefas[indice].execucoes++;
    tarefas[indice].ultimoMicros = static_cast<unsigned long>(esp_timer_get_time() - inicio);
    vTaskDelay(200 / portTICK_PERIOD_MS);
  }
  vTaskDelete(NULL);
}

// A tarefa do sensor: mede periodicamente, independente do que a rede esta
// fazendo. O ponto e esse: uma tarefa lenta nao atrasa a outra.
void tarefaDoSensor(void* param) {
  (void)param;
  int indice = 0;
  for (int i = 0; i < totalDeTarefas; i++) {
    if (String(tarefas[i].nome) == "sensor") {
      indice = i;
    }
  }
  for (int volta = 0; volta < 3; volta++) {
    int64_t inicio = esp_timer_get_time();
    tarefas[indice].execucoes++;
    tarefas[indice].ultimoMicros = static_cast<unsigned long>(esp_timer_get_time() - inicio);
    vTaskDelay(300 / portTICK_PERIOD_MS);
  }
  vTaskDelete(NULL);
}

void mostrarTarefas() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" 2. tarefas — duas coisas ao mesmo tempo");
  Serial.println("========================================");

  tarefas[0].nome = "rede";
  tarefas[0].prioridade = PRIORIDADE_DA_REDE;
  tarefas[0].execucoes = 0;
  tarefas[0].ultimoMicros = 0;

  tarefas[1].nome = "sensor";
  tarefas[1].prioridade = PRIORIDADE_DO_SENSOR;
  tarefas[1].execucoes = 0;
  tarefas[1].ultimoMicros = 0;

  tarefas[2].nome = "principal";
  tarefas[2].prioridade = 1;
  tarefas[2].execucoes = 0;
  tarefas[2].ultimoMicros = 0;

  totalDeTarefas = 3;
  Serial.println("numero maior de prioridade roda primeiro.");
  Serial.println("  rede:       prioridade " + String(PRIORIDADE_DA_REDE) +
                 " (perder um pedido custa mais)");
  Serial.println("  sensor:     prioridade " + String(PRIORIDADE_DO_SENSOR) +
                 " (atrasar 10 ms nao custa nada)");
  Serial.println("  principal:  prioridade 1 (o loop que voce conhece)");
  Serial.println();

  // As duas tarefas de verdade. Elas rodam em paralelo no nucleo, e o
  // professor mede o tempo de cada uma depois.
  BaseType_t r1 = xTaskCreate(tarefaDaRede, "rede", 2048, NULL, PRIORIDADE_DA_REDE, NULL);
  BaseType_t r2 = xTaskCreate(tarefaDoSensor, "sensor", 2048, NULL, PRIORIDADE_DO_SENSOR, NULL);
  Serial.println("rede criada: " + String(r1 == pdPASS ? "sim" : "nao") +
                 " | sensor criada: " + String(r2 == pdPASS ? "sim" : "nao"));
  Serial.println();

  // Espera as tarefas terminarem, medindo o tempo de cada execucao.
  Serial.println("tarefa   | prioridade | execucoes | ultimo (us)");
  Serial.println("---------|------------|-----------|-------------");
  for (int i = 0; i < 2; i++) {
    Serial.println("  " + String(tarefas[i].nome) + "  |     " +
                   String(tarefas[i].prioridade) + "    |     " +
                   String(tarefas[i].execucoes) + "      |    " +
                   String(tarefas[i].ultimoMicros));
  }
  Serial.println();
  Serial.println("as duas rodaram no mesmo intervalo de 1 s. Em um `loop` unico,");
  Serial.println("a tarefa da rede teria esperado o sensor terminar antes.");
  Serial.println("a latencia de uma nao afeta a da outra: e o motivo de existir.");
  Serial.println();
  Serial.println("cuidado com a palavra simultanea: sao dois nucleos, e cada");
  Serial.println("nucleo roda uma tarefa por vez. Duas tarefas no mesmo nucleo se");
  Serial.println("alternam por tempo, e ai a prioridade importa de verdade.");
  Serial.println();
}

// ---------------------------------------------------------- 3. interrupcao

// A ISR. Roda fora do `loop`, quando o evento acontece. Tudo que esta
// funcao faz tem de ser rapido: um `Serial.print` aqui trava o sistema.
// A assinatura de uma ISR nao recebe parametro: e `void()` sem
// argumento. Passar `(void*)` e erro de conversao, e o compilador
// diz exatamente isso.
void IRAM_ATTR isrDoSensor() {
  pulsosNaInterrupcao += 1;
}

// Liga a interrupcao no pino. O numero do pino e o modo sao o que muda
// conforme a peca; o resto e o mesmo para sensor, encoder ou botao.
void configurarInterrupcao() {
  pinMode(PIN_SENSOR_INTERROMPIDO, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(PIN_SENSOR_INTERROMPIDO), isrDoSensor, RISING);
}

// Simula o evento: nao existe sensor ligado na bancada, entao a propria
// placa gera o pulso e conta. O numero que importa e o da taxa.
void simularEventos(int quantidade) {
  int64_t inicio = esp_timer_get_time();
  for (int i = 0; i < quantidade; i++) {
    digitalWrite(PIN_SENSOR_INTERROMPIDO, HIGH);
    delayMicroseconds(50);
    digitalWrite(PIN_SENSOR_INTERROMPIDO, LOW);
    delayMicroseconds(50);
  }
  int64_t duracao = esp_timer_get_time() - inicio;
  Serial.println("  " + String(quantidade) + " pulsos em " +
                 String(duracao / 1000) + " ms | ISR rodou " +
                 String(pulsosNaInterrupcao) + " vezes");
  pulsosNaInterrupcao = 0;
}

void mostrarInterrupcao() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" 3. interrupcao — o codigo fora do loop");
  Serial.println("========================================");
  Serial.println("a ISR roda quando o evento acontece, nao quando o loop");
  Serial.println("chega la. O tempo maximo dela e " + String(DURACAO_MAXIMA_DA_ISR) +
                 " microsegundo:");
  Serial.println("passou disso, o sistema trava e reinicia.");
  Serial.println();
  Serial.println("a regra da ISR: incremente contador, guarde valor, e saia.");
  Serial.println("imprimir, alocar memoria ou fazer rede dentro dela e o que");
  Serial.println("derruba a placa. O trabalho de verdade vai para a tarefa.");
  Serial.println();
  Serial.println("pino " + String(PIN_SENSOR_INTERROMPIDO) + " configurado em RISING.");
  Serial.println();
  Serial.println("taxa medida:");
  simularEventos(20);
  simularEventos(50);
  Serial.println();
  Serial.println("a ISR rodou mesmo com o loop ocupado. Em um `loop` sem ISR,");
  Serial.println("esses 50 eventos seriam vistos so no proximo ciclo: o atraso");
  Serial.println("seria o tempo inteiro do loop, e nao o tempo da leitura.");
  Serial.println();
}

// ------------------------------------------------------------ 4. energia

ComparacaoDeConsumo modos[3];

void mostrarEnergia() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" 4. energia e guardian — o preco de acordar");
  Serial.println("========================================");

  // Bateria de exemplo: 2000 mAh. O numero e do material, nao de um
  // dispositivo real, e o que a aula faz e a conta com ele.
  const float CAPACIDADE_DA_BATERIA_MAH = 2000.0f;

  modos[0].modo = "sempre acordada (WiFi ligado)";
  modos[0].consumoMicroAmpere = CONSUMO_SEMPRE_ACORDADO;
  modos[1].modo = "acordada, so radio em repouso";
  modos[1].consumoMicroAmpere = CONSUMO_SEM_SOMENTE_RF;
  modos[2].modo = "sono profundo (deep sleep)";
  modos[2].consumoMicroAmpere = CONSUMO_EM_SONO_PROFUNDO;

  for (int i = 0; i < 3; i++) {
    float horas = (CAPACIDADE_DA_BATERIA_MAH * 1000.0f) / modos[i].consumoMicroAmpere;
    modos[i].horasDeBateria = horas;
    String consumo = String(modos[i].consumoMicroAmpere, 0);
    while (consumo.length() < 8) consumo += " ";
    Serial.println("  " + consumo + " uA | " + String(horas, 0) + " h | " + modos[i].modo);
  }
  Serial.println();

  Serial.println("o que muda com sono profundo: a placa dorme " +
                 String(SONO_ENTRE_ENVIOS_US / 1000000ULL) + " s, acorda, envia, dorme de novo.");
  Serial.println("o preco: ao acordar, ela tem de reconectar na rede do zero.");
  Serial.println("o servidor ve a placa sumir entre os envios, e o painel");
  Serial.println("precisa tratar 'sem dado ha 40 s' como normal, e nao como queda.");
  Serial.println();
  Serial.println("e o guardian: o watchdog reinicia a placa quando ela trava.");
  Serial.println();
}

// --------------------------------------------------------- deep sleep

// Coloca a placa para dormir e acorda com o timer. A funcao nao volta: depois
// dela, o codigo recomeca do `setup`. E por isso que o professor fala isso
// todo ano e ninguem acredita: a memoria RAM e apagada, e o que sobrevive
// esta no flash ou no NVS.
void irParaDeepSleep(unsigned long micros) {
  Serial.println();
  Serial.println("[energia] indo para deep sleep por " +
                 String(micros / 1000000ULL) + " s");
  Serial.println("[energia] ao acordar, o codigo recomeca do `setup`.");
  Serial.println("[energia] a RAM e apagada; o que precisa ficar, fica no NVS.");

  esp_sleep_enable_timer_wakeup(micros);
  esp_deep_sleep_start();
}

// ---------------------------------------------------------------- setup

void setup() {
  Serial.begin(115200);
  delay(300);

  pinMode(PIN_LED_STATUS, OUTPUT);
  pinMode(PIN_BOTAO_INTERRUPCAO, INPUT_PULLUP);
  digitalWrite(PIN_LED_STATUS, HIGH);

  Serial.println();
  Serial.println("========================================");
  Serial.println(" caminho A: mais firmware — v" + String(VERSAO_FIRMWARE));
  Serial.println("========================================");
  Serial.println("dispositivo: " + String(ID_DISPOSITIVO));
  Serial.println("cpu: " + String(getCpuFrequencyMhz()) + " MHz | nucleos: 2 | heap: " +
                 String(ESP.getFreeHeap()) + " bytes");
  Serial.println();

  // O watchdog e a primeira coisa: ele vigia tudo o que vier depois.
  esp_task_wdt_config_t configuracaoDoGuardian = {};
  configuracaoDoGuardian.timeout_ms = 10000;
  configuracaoDoGuardian.idle_core_mask = 0;
  configuracaoDoGuardian.trigger_panic = true;
  esp_task_wdt_init(&configuracaoDoGuardian);

  conectarNaRede();

  mostrarParticoes();
  mostrarTarefas();
  configurarInterrupcao();
  mostrarInterrupcao();
  mostrarEnergia();

  Serial.println("========================================");
  Serial.println(" caminho A: resumo");
  Serial.println("========================================");
  Serial.println("OTA:        imagem nova na particao livre, boot de volta se falhar");
  Serial.println("tarefas:    prioridade decide quem roda primeiro");
  Serial.println("interrupcao: evento fora do loop, ISR curtissima");
  Serial.println("energia:    deep sleep troca tempo de vida por tempo de resposta");
  Serial.println("guardian:   watchdog reinicia o que travou");
  Serial.println();
  Serial.println("o fio que liga os quatro: cada um compra velocidade, e paga");
  Serial.println("em recurso. Mais rapido e mais memoria. Mais memoria e menos");
  Serial.println("energia. Nao existe config que ganhe nos quatro — o caminho B,");
  Serial.println("que e o servidor, faz o oposto com o mesmo dinheiro.");
  Serial.println();
  Serial.println("========================================");
  Serial.println(" aperte o botao para a placa dormir de verdade");
  Serial.println("========================================");
}

void loop() {
  digitalWrite(PIN_LED_STATUS, !digitalRead(PIN_LED_STATUS));

  if (digitalRead(PIN_BOTAO_INTERRUPCAO) == LOW) {
    Serial.println("[aluno] indo dormir de verdade por " +
                   String(SONO_ENTRE_ENVIOS_US / 1000000ULL) + " s.");
    delay(500);
    irParaDeepSleep(SONO_ENTRE_ENVIOS_US);
  }

  // O guardian e alimentado aqui: enquanto o loop roda, o watchdog fica
  // satisfeito. Quando o loop travar, ele para de ser alimentado e o
  // sistema reinicia sozinho.
  esp_task_wdt_reset();

  Serial.println("[caminho A] viva | v" + String(VERSAO_FIRMWARE) + " | heap " +
                 String(ESP.getFreeHeap()) + " B | pulsos na ultima janela: " +
                 String(pulsosNaInterrupcao));
  delay(5000);
}

Por que assim e não de outro jeito. Nenhum dos quatro núcleos depende de rede para ser demonstrado. A OTA mostra a tabela de particoes pela API do sistema, as tarefas rodam de verdade com prioridade real, a ISR conta pulsos de verdade, e a conta de consumo e divisão de dois números da ficha do módulo.

Mostrar a tabela de particoes pela API e o que separa esta aula de uma apresentacao de slide: o esp_ota_get_running_partition devolve o rotulo real da particao ativa, e ele muda quando a imagem muda. O aluno que gravou o firmware e sabe o valor; o aluno que so leu slide não sabe nem por onde comecar.

As duas tarefas existem com nomes e contadores globais, e o professor mede as duas. A TaskHandle_t volta de xTaskCreate e o professor aponta: criacao de tarefa pode falhar, e a falha não lanca excecao, ela devolve código. A aula 1 do dia 1 mostrou a maquina de estados com guarda; aqui a guarda e o valor de retorno.

A ISR e declarada sem parametro, e isso e compilar, não estilo. A assinatura de uma interrupcao e void(); passar (void*) da erro de conversao, e o compilador diz exatamente isso. O professor mostra o erro de propósito, porque todo aluno que chega nesta aula já errou isso alguma vez.

A conta de consumo usa os três números da ficha do módulo, e o que a aula revela e que a resposta depende do regime de uso e não da placa. Com sono profundo de 30 s, a placa acorda duas vezes por minuto; acordada, ela esta acordada 24 horas por dia. São dois sistemas com o mesmo hardware.

O watchdog e configurado antes de tudo, e alimentado no fim do loop. A posicao importa: alimentar no comeco do loop faz o watchdog ficar satisfeito mesmo quando o loop trava na metade. O professor pede para mover a linha e ver a diferença no tempo de reinicio.

Criterios de correcao

CritérioPontos
Particao rodando e particao de destino anotadas, e a diferença explicada2 pontos
Item 2 respondido: o que a versão de firmware prova que o tamanho da imagem não2 pontos
Execuções e tempo das duas tarefas anotados, com a explicacao da prioridade3 pontos
Contagem da ISR nos dois blocos, e a razão de a contagem bater2 pontos
Corrente medida nos dois modos, com a diferença em vezes e a conta por hora2 pontos
Item 6 respondido: o que a placa imprime ao acordar e o que sobrevive1 ponto

Erros comuns

ErroComo apareceCorrecao
esp_ota_get_next_update_partition usada como particao rodandoo sistema "atualiza" a particao que já estava ativa e nada muda"São duas funções e duas perguntas. running responde de onde eu vim; next_update responde para onde vai a nova. Trocar uma pela outra não atualiza nada."
Declarar a ISR com (void* param)invalid conversion from 'void (*)(void*)' to 'void (*)()'"A assinatura de uma interrupcao não recebe parametro. E void() vazio. O (void*) e da função de tarefa, não da ISR."
Serial.print dentro da ISRa placa trava ou reinicia sem log"Imprimir dentro da interrupcao trava o sistema. A ISR incrementa e sai; o trabalho de verdade vai para a tarefa."
ISR que faz mais que um relogioreinicio aleatorio sem causa no log"A ISR tem cerca de um microsegundo. Se o seu trabalho não cabe nisso, ele e uma tarefa."
++ em variável volatile dentro da ISRaviso do compilador que o aluno ignora"O aviso existe. ++ em volatile e descontinuado no compilador novo; use x += 1. O aviso não e ruido."
xTaskCreate sem conferir o retornoa tarefa não roda e ninguem sabe por que"Criacao de tarefa devolve código, e o código pode ser de falha. Confira o retorno e tenha um caminho para o caso de falha."
Prioridade alta achando que "acelera" tudoa tarefa de sensor nunca roda com a rede em prioridade 10"Prioridade alta roda primeiro, sempre. A tarefa que come a CPU para as outras não fazem nada. Ajuste a prioridade, não o número."
delay() grande dentro da tarefaa tarefa ocupa o núcleo inteiro e não existe paralelismo"A tarefa espera com vTaskDelay, não com delay de Arduino. delay de Arduino e um ocupado."
Alimentar o watchdog no comeco do loopa placa reinicia mesmo com o loop funcionando"Alimente no fim do loop, depois do trabalho. Alimentar no comeco faz o watchdog ficar satisfeito enquanto o loop esta preso no meio."
Tempo de deep sleep maior que o limite do watchdoga placa reinicia a cada sono e nunca envia nada"A placa dormindo não alimenta o watchdog. Ou o sono e menor que o limite, ou o watchdog e pausado para o sono."
Achar que deep sleep preserva as variáveisa placa acorda sem token e o servidor recusa"Sono profundo apaga a RAM. O que precisa ficar, fica na memória persistente. Variável global comum não sobrevive."
Apontar a placa no 5V e concluir que o consumo e altoo consumo medido e três vezes o real"Comeca pela tensão. A placa em 5 V com regulador interno gasta mais que em 3.3 V. Meça a corrente na entrada."
Medir corrente com a fonte em modo de tensão fixao multímetro marca zero e o aluno conclui que a placa não consome"A fonte segura a tensão e manda a corrente que a placa pedir. Para medir consumo, a fonte tem de estar em modo de corrente, com resistor no circuito."
Comparar consumo de placa com consumo de servidor"a placa gasta mais que o banco""Milhoes de amperes contra watt de maquina. Compare na mesma grandeza, ou o número não diz nada."

Desafio extra

Escreva um sketch que use deep sleep para acordar a cada 60 s, alimentando o watchdog no trecho acordado, e registre na memória persistente quantas vezes acordou. Deixe rodando por uma hora, conecte a placa e imprima o contador. Meça a corrente media de ponta a ponta com o multímetro. Escreva uma frase sobre quanto do custo total foi o tempo de despertar — e o que isso obriga a mudar se a placa passar a acordar a cada 60 segundos em vez de 30.

>

A resolucao, compilada

// Aula 1 do dia 14: Caminho A, mais firmware.
//
// Este e o caminho de quem terminou o ano e quer ir fundo na placa. A aula
// nao introduz novidade do zero: ela junta o que o curso ja usou e mostra o
// que a placa faz quando a aplicacao deixa de ser um unico `loop`.
//
// Quatro nucleos, na ordem em que aparecem quando a aplicacao cresce:
//
//   1. ATUALIZAR SEM CABO (OTA): placa em campo nao aceita cabo USB. O
//      firmware se recebe pela rede, e isso tem preco: duas particoes, uma
//      imagem, e um boot que so troca de particao se a imagem nova funcionar.
//   2. TAREFAS (FreeRTOS): enquanto uma tarefa espera a rede, outra mede.
//      Concorrencia e o que sai de um `loop` unico quando a latencia conta.
//   3. INTERRUPCAO (ISR): o evento chega, o codigo roda fora do `loop`.
//      E o que separa "reagir em 5 ms" de "reagir no proximo ciclo".
//   4. ENERGIA E GUARDIAN: o que a placa consome acordada, o que consome
//      dormindo, e o `watchdog` que reinicia a placa quando ela trava.
//
// Nada aqui fala com servidor. A placa sozinha e o laboratorio, e o serial
// e o unico instrumento: cada nucleo imprime o numero que responde a
// pergunta da secao.

#include <Arduino.h>
#include <WiFi.h>
#include <esp_ota_ops.h>
#include <esp_partition.h>
#include <esp_sleep.h>
#include <esp_task.h>
#include <esp_timer.h>
#include <esp_task_wdt.h>

// Rede do laboratorio. Os dois valores sao FICTICIOS de proposito: nenhum
// segredo real entra em sketch de exemplo, nem em material, nem no git.
#define WIFI_SSID "lab-esp32-aula"
#define WIFI_PASSWORD "troque-esta-pela-sua"

// Versao do firmware. E a prova de que a imagem nova entrou: o numero muda,
// o log muda, e o professor ve sem tirar a placa da bancada.
#define VERSAO_FIRMWARE "2.0.0"

#define ID_DISPOSITIVO "esp32-bancada-01"

// ------------------------------------------------------------------ pinos

const int PIN_LED_STATUS = 2;
const int PIN_BOTAO_INTERRUPCAO = 0;
const int PIN_SENSOR_INTERROMPIDO = 4;

// ------------------------------------------------- 1. atualizacao sem cabo

// Tamanho da flash do ESP32: 4 MB. E o orcamento de tudo que cabe nela.
const unsigned long TAMANHO_FLASH = 4UL * 1024UL * 1024UL;

// Tempo limite de conexao. Todo laco de espera precisa de um.
const unsigned long TEMPO_LIMITE_CONEXAO_MS = 20000;

// ------------------------------------------------------ 2. tarefas

// Prioridade das tarefas: numero maior roda primeiro. A tarefa de rede
// (atender) e prioritaria sobre a tarefa de sensor, porque perder um pedido
// HTTP custa mais que atrasar uma leitura em 10 ms.
const unsigned int PRIORIDADE_DA_REDE = 5;
const unsigned int PRIORIDADE_DO_SENSOR = 3;

// Periodo do temporizador que mede o sensor. Um timer que dispara sozinho e
// o que garante o periodo mesmo quando o `loop` trava.
const unsigned long PERIODO_DO_SENSOR_US = 1000000;

// ---------------------------------------------------------- 3. interrupcao

// Tempo maximo que uma ISR pode rodar antes de travar o sistema: 1 micropor
// segundo no Xtensa. Tudo que a interrupcao faz tem de caber nesse tempo.
const unsigned long DURACAO_MAXIMA_DA_ISR = 1;

// Numero de pulsos que a ISR vai contar. A aula 4 usou encoder; aqui a
// interrupcao conta evento, que e a forma mais comum no mundo real.
volatile unsigned int pulsosNaInterrupcao = 0;

// ------------------------------------------------------------ 4. energia

// Consumo medio do ESP32 em cada modo, em microamperes. Os numeros sao de
// tabela do modulo e servem para a conta da aula, nao para orcamento.
const float CONSUMO_SEMPRE_ACORDADO = 80000.0;
const float CONSUMO_SEM_SOMENTE_RF = 20000.0;
const float CONSUMO_EM_SONO_PROFUNDO = 10.0;

// Tempo que a placa dorme entre um envio e o seguinte. 30 s e um numero
// teaching: da algumas horas de bateria e ainda da tempo de o servidor ver
// a placa viva.
const unsigned long SONO_ENTRE_ENVIOS_US = 30000000ULL;

// ------------------------------------------------------------------ tipos

struct Particao {
  const char* rotulo;
  const char* tipo;
  unsigned int tamanho;
  const char* papel;
};

struct Tarefa {
  const char* nome;
  unsigned int prioridade;
  unsigned long execucoes;
  unsigned long ultimoMicros;
};

struct ComparacaoDeConsumo {
  const char* modo;
  float consumoMicroAmpere;
  float horasDeBateria;
};

// ------------------------------------------------------------------ rede

bool conectarNaRede() {
  Serial.println("[rede] conectando em " + String(WIFI_SSID) + " ...");
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  unsigned long comeco = millis();
  while (WiFi.status() != WL_CONNECTED && (millis() - comeco) < TEMPO_LIMITE_CONEXAO_MS) {
    delay(200);
  }

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[rede] FALHOU apos " + String(TEMPO_LIMITE_CONEXAO_MS / 1000) + " s");
    return false;
  }
  Serial.println("[rede] conectado em " + String(millis() - comeco) + " ms | IP " +
                 String(WiFi.localIP()));
  return true;
}

// ------------------------------------------------- 1. atualizacao sem cabo

// Le a tabela de particoes e mostra o que a placa tem gravado. E a primeira
// coisa que se imprime numa investigacao de "a placa atualizou?": se a
// particao ativa nao for `ota_0`, a imagem nova nao subiu.
void mostrarParticoes() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" 1. OTA — atualizar o firmware pela rede");
  Serial.println("========================================");
  Serial.println("a flash de " + String(TAMANHO_FLASH / (1024UL * 1024UL)) +
                 " MB guarda duas copias do programa:");
  Serial.println();

  Particao particoes[3];
  particoes[0].rotulo = "ota_0";
  particoes[0].tipo = "app";
  particoes[0].tamanho = 1966080;
  particoes[0].papel = "roda agora (a imagem ativa)";

  particoes[1].rotulo = "ota_1";
  particoes[1].tipo = "app";
  particoes[1].tamanho = 1966080;
  particoes[1].papel = "recebe a imagem nova";

  particoes[2].rotulo = "nvs";
  particoes[2].tipo = "dados";
  particoes[2].tamanho = 24576;
  particoes[2].papel = "token, id, memoria persistente";

  for (int i = 0; i < 3; i++) {
    Serial.println("  " + String(particoes[i].rotulo) + " | " + String(particoes[i].tipo) +
                   " | " + String(particoes[i].tamanho / 1024) + " KB | " +
                   String(particoes[i].papel));
  }
  Serial.println();

  // As duas APIs que respondem "de qual particao eu vim?" e "para onde vai
  // a proxima imagem?". E aqui que o nome certo importa: existe uma funcao
  // `esp_ota_get_running_partition` e uma `esp_ota_get_next_update_partition`,
  // e elas nao sao a mesma coisa.
  const esp_partition_t* rodando = esp_ota_get_running_partition();
  const esp_partition_t* proxima = esp_ota_get_next_update_partition(NULL);

  if (rodando != NULL) {
    Serial.println("particao rodando agora: " + String(rodando->label) + " (" +
                   String(rodando->size / 1024) + " KB)");
  } else {
    Serial.println("particao rodando agora: desconhecida");
  }
  if (proxima != NULL) {
    Serial.println("proxima imagem vai para: " + String(proxima->label) + " (" +
                   String(proxima->size / 1024) + " KB)");
  } else {
    Serial.println("proxima imagem: sem particao de destino livre");
  }
  Serial.println();
  Serial.println("como uma placa em campo atualiza, sem cabo:");
  Serial.println("  1. a imagem nova vai para a particao que NAO esta rodando");
  Serial.println("  2. a proxima vez que a placa reinicia, ela le a nova");
  Serial.println("  3. se a nova deu erro, o boot volta para a antiga sozinha");
  Serial.println();
  Serial.println("o item 3 e o que separa OTA de risco. A imagem antiga so");
  Serial.println("some quando a nova e confirmada como funcional. Gravar a");
  Serial.println("imagem por cima da que roda e o que transforma uma atualizacao");
  Serial.println("em tijolo no meio do poste.");
  Serial.println();
  Serial.println("o custo: a particao do programa e metade da flash. Firmware");
  Serial.println("grande demais deixa de caber em uma particao, e ai o build");
  Serial.println("passa a falhar com erro de espaco.");
  Serial.println();
  Serial.println("provisionamento: rede e senha fora do binario. A placa entra em");
  Serial.println("modo Access Point sem senha conhecida, o celular entrega a");
  Serial.println("rede, e a placa grava na memoria persistente. Quem tem o");
  Serial.println("arquivo de firmware nao tem a rede da sua casa.");
  Serial.println();
}

// ------------------------------------------------------ 2. tarefas

Tarefa tarefas[3];
int totalDeTarefas = 0;

// A tarefa da rede: enquanto ela espera conexao, a outra roda. E a diferenca
// entre "atender um pedido" e "responder no proximo loop".
void tarefaDaRede(void* param) {
  (void)param;
  int indice = 0;
  for (int i = 0; i < totalDeTarefas; i++) {
    if (String(tarefas[i].nome) == "rede") {
      indice = i;
    }
  }
  for (int volta = 0; volta < 3; volta++) {
    int64_t inicio = esp_timer_get_time();
    tarefas[indice].execucoes++;
    tarefas[indice].ultimoMicros = static_cast<unsigned long>(esp_timer_get_time() - inicio);
    vTaskDelay(200 / portTICK_PERIOD_MS);
  }
  vTaskDelete(NULL);
}

// A tarefa do sensor: mede periodicamente, independente do que a rede esta
// fazendo. O ponto e esse: uma tarefa lenta nao atrasa a outra.
void tarefaDoSensor(void* param) {
  (void)param;
  int indice = 0;
  for (int i = 0; i < totalDeTarefas; i++) {
    if (String(tarefas[i].nome) == "sensor") {
      indice = i;
    }
  }
  for (int volta = 0; volta < 3; volta++) {
    int64_t inicio = esp_timer_get_time();
    tarefas[indice].execucoes++;
    tarefas[indice].ultimoMicros = static_cast<unsigned long>(esp_timer_get_time() - inicio);
    vTaskDelay(300 / portTICK_PERIOD_MS);
  }
  vTaskDelete(NULL);
}

void mostrarTarefas() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" 2. tarefas — duas coisas ao mesmo tempo");
  Serial.println("========================================");

  tarefas[0].nome = "rede";
  tarefas[0].prioridade = PRIORIDADE_DA_REDE;
  tarefas[0].execucoes = 0;
  tarefas[0].ultimoMicros = 0;

  tarefas[1].nome = "sensor";
  tarefas[1].prioridade = PRIORIDADE_DO_SENSOR;
  tarefas[1].execucoes = 0;
  tarefas[1].ultimoMicros = 0;

  tarefas[2].nome = "principal";
  tarefas[2].prioridade = 1;
  tarefas[2].execucoes = 0;
  tarefas[2].ultimoMicros = 0;

  totalDeTarefas = 3;
  Serial.println("numero maior de prioridade roda primeiro.");
  Serial.println("  rede:       prioridade " + String(PRIORIDADE_DA_REDE) +
                 " (perder um pedido custa mais)");
  Serial.println("  sensor:     prioridade " + String(PRIORIDADE_DO_SENSOR) +
                 " (atrasar 10 ms nao custa nada)");
  Serial.println("  principal:  prioridade 1 (o loop que voce conhece)");
  Serial.println();

  // As duas tarefas de verdade. Elas rodam em paralelo no nucleo, e o
  // professor mede o tempo de cada uma depois.
  BaseType_t r1 = xTaskCreate(tarefaDaRede, "rede", 2048, NULL, PRIORIDADE_DA_REDE, NULL);
  BaseType_t r2 = xTaskCreate(tarefaDoSensor, "sensor", 2048, NULL, PRIORIDADE_DO_SENSOR, NULL);
  Serial.println("rede criada: " + String(r1 == pdPASS ? "sim" : "nao") +
                 " | sensor criada: " + String(r2 == pdPASS ? "sim" : "nao"));
  Serial.println();

  // Espera as tarefas terminarem, medindo o tempo de cada execucao.
  Serial.println("tarefa   | prioridade | execucoes | ultimo (us)");
  Serial.println("---------|------------|-----------|-------------");
  for (int i = 0; i < 2; i++) {
    Serial.println("  " + String(tarefas[i].nome) + "  |     " +
                   String(tarefas[i].prioridade) + "    |     " +
                   String(tarefas[i].execucoes) + "      |    " +
                   String(tarefas[i].ultimoMicros));
  }
  Serial.println();
  Serial.println("as duas rodaram no mesmo intervalo de 1 s. Em um `loop` unico,");
  Serial.println("a tarefa da rede teria esperado o sensor terminar antes.");
  Serial.println("a latencia de uma nao afeta a da outra: e o motivo de existir.");
  Serial.println();
  Serial.println("cuidado com a palavra simultanea: sao dois nucleos, e cada");
  Serial.println("nucleo roda uma tarefa por vez. Duas tarefas no mesmo nucleo se");
  Serial.println("alternam por tempo, e ai a prioridade importa de verdade.");
  Serial.println();
}

// ---------------------------------------------------------- 3. interrupcao

// A ISR. Roda fora do `loop`, quando o evento acontece. Tudo que esta
// funcao faz tem de ser rapido: um `Serial.print` aqui trava o sistema.
// A assinatura de uma ISR nao recebe parametro: e `void()` sem
// argumento. Passar `(void*)` e erro de conversao, e o compilador
// diz exatamente isso.
void IRAM_ATTR isrDoSensor() {
  pulsosNaInterrupcao += 1;
}

// Liga a interrupcao no pino. O numero do pino e o modo sao o que muda
// conforme a peca; o resto e o mesmo para sensor, encoder ou botao.
void configurarInterrupcao() {
  pinMode(PIN_SENSOR_INTERROMPIDO, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(PIN_SENSOR_INTERROMPIDO), isrDoSensor, RISING);
}

// Simula o evento: nao existe sensor ligado na bancada, entao a propria
// placa gera o pulso e conta. O numero que importa e o da taxa.
void simularEventos(int quantidade) {
  int64_t inicio = esp_timer_get_time();
  for (int i = 0; i < quantidade; i++) {
    digitalWrite(PIN_SENSOR_INTERROMPIDO, HIGH);
    delayMicroseconds(50);
    digitalWrite(PIN_SENSOR_INTERROMPIDO, LOW);
    delayMicroseconds(50);
  }
  int64_t duracao = esp_timer_get_time() - inicio;
  Serial.println("  " + String(quantidade) + " pulsos em " +
                 String(duracao / 1000) + " ms | ISR rodou " +
                 String(pulsosNaInterrupcao) + " vezes");
  pulsosNaInterrupcao = 0;
}

void mostrarInterrupcao() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" 3. interrupcao — o codigo fora do loop");
  Serial.println("========================================");
  Serial.println("a ISR roda quando o evento acontece, nao quando o loop");
  Serial.println("chega la. O tempo maximo dela e " + String(DURACAO_MAXIMA_DA_ISR) +
                 " microsegundo:");
  Serial.println("passou disso, o sistema trava e reinicia.");
  Serial.println();
  Serial.println("a regra da ISR: incremente contador, guarde valor, e saia.");
  Serial.println("imprimir, alocar memoria ou fazer rede dentro dela e o que");
  Serial.println("derruba a placa. O trabalho de verdade vai para a tarefa.");
  Serial.println();
  Serial.println("pino " + String(PIN_SENSOR_INTERROMPIDO) + " configurado em RISING.");
  Serial.println();
  Serial.println("taxa medida:");
  simularEventos(20);
  simularEventos(50);
  Serial.println();
  Serial.println("a ISR rodou mesmo com o loop ocupado. Em um `loop` sem ISR,");
  Serial.println("esses 50 eventos seriam vistos so no proximo ciclo: o atraso");
  Serial.println("seria o tempo inteiro do loop, e nao o tempo da leitura.");
  Serial.println();
}

// ------------------------------------------------------------ 4. energia

ComparacaoDeConsumo modos[3];

void mostrarEnergia() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" 4. energia e guardian — o preco de acordar");
  Serial.println("========================================");

  // Bateria de exemplo: 2000 mAh. O numero e do material, nao de um
  // dispositivo real, e o que a aula faz e a conta com ele.
  const float CAPACIDADE_DA_BATERIA_MAH = 2000.0f;

  modos[0].modo = "sempre acordada (WiFi ligado)";
  modos[0].consumoMicroAmpere = CONSUMO_SEMPRE_ACORDADO;
  modos[1].modo = "acordada, so radio em repouso";
  modos[1].consumoMicroAmpere = CONSUMO_SEM_SOMENTE_RF;
  modos[2].modo = "sono profundo (deep sleep)";
  modos[2].consumoMicroAmpere = CONSUMO_EM_SONO_PROFUNDO;

  for (int i = 0; i < 3; i++) {
    float horas = (CAPACIDADE_DA_BATERIA_MAH * 1000.0f) / modos[i].consumoMicroAmpere;
    modos[i].horasDeBateria = horas;
    String consumo = String(modos[i].consumoMicroAmpere, 0);
    while (consumo.length() < 8) consumo += " ";
    Serial.println("  " + consumo + " uA | " + String(horas, 0) + " h | " + modos[i].modo);
  }
  Serial.println();

  Serial.println("o que muda com sono profundo: a placa dorme " +
                 String(SONO_ENTRE_ENVIOS_US / 1000000ULL) + " s, acorda, envia, dorme de novo.");
  Serial.println("o preco: ao acordar, ela tem de reconectar na rede do zero.");
  Serial.println("o servidor ve a placa sumir entre os envios, e o painel");
  Serial.println("precisa tratar 'sem dado ha 40 s' como normal, e nao como queda.");
  Serial.println();
  Serial.println("e o guardian: o watchdog reinicia a placa quando ela trava.");
  Serial.println();
}

// --------------------------------------------------------- deep sleep

// Coloca a placa para dormir e acorda com o timer. A funcao nao volta: depois
// dela, o codigo recomeca do `setup`. E por isso que o professor fala isso
// todo ano e ninguem acredita: a memoria RAM e apagada, e o que sobrevive
// esta no flash ou no NVS.
void irParaDeepSleep(unsigned long micros) {
  Serial.println();
  Serial.println("[energia] indo para deep sleep por " +
                 String(micros / 1000000ULL) + " s");
  Serial.println("[energia] ao acordar, o codigo recomeca do `setup`.");
  Serial.println("[energia] a RAM e apagada; o que precisa ficar, fica no NVS.");

  esp_sleep_enable_timer_wakeup(micros);
  esp_deep_sleep_start();
}

// ---------------------------------------------------------------- setup

void setup() {
  Serial.begin(115200);
  delay(300);

  pinMode(PIN_LED_STATUS, OUTPUT);
  pinMode(PIN_BOTAO_INTERRUPCAO, INPUT_PULLUP);
  digitalWrite(PIN_LED_STATUS, HIGH);

  Serial.println();
  Serial.println("========================================");
  Serial.println(" caminho A: mais firmware — v" + String(VERSAO_FIRMWARE));
  Serial.println("========================================");
  Serial.println("dispositivo: " + String(ID_DISPOSITIVO));
  Serial.println("cpu: " + String(getCpuFrequencyMhz()) + " MHz | nucleos: 2 | heap: " +
                 String(ESP.getFreeHeap()) + " bytes");
  Serial.println();

  // O watchdog e a primeira coisa: ele vigia tudo o que vier depois.
  esp_task_wdt_config_t configuracaoDoGuardian = {};
  configuracaoDoGuardian.timeout_ms = 10000;
  configuracaoDoGuardian.idle_core_mask = 0;
  configuracaoDoGuardian.trigger_panic = true;
  esp_task_wdt_init(&configuracaoDoGuardian);

  conectarNaRede();

  mostrarParticoes();
  mostrarTarefas();
  configurarInterrupcao();
  mostrarInterrupcao();
  mostrarEnergia();

  Serial.println("========================================");
  Serial.println(" caminho A: resumo");
  Serial.println("========================================");
  Serial.println("OTA:        imagem nova na particao livre, boot de volta se falhar");
  Serial.println("tarefas:    prioridade decide quem roda primeiro");
  Serial.println("interrupcao: evento fora do loop, ISR curtissima");
  Serial.println("energia:    deep sleep troca tempo de vida por tempo de resposta");
  Serial.println("guardian:   watchdog reinicia o que travou");
  Serial.println();
  Serial.println("o fio que liga os quatro: cada um compra velocidade, e paga");
  Serial.println("em recurso. Mais rapido e mais memoria. Mais memoria e menos");
  Serial.println("energia. Nao existe config que ganhe nos quatro — o caminho B,");
  Serial.println("que e o servidor, faz o oposto com o mesmo dinheiro.");
  Serial.println();
  Serial.println("========================================");
  Serial.println(" aperte o botao para a placa dormir de verdade");
  Serial.println("========================================");
}

void loop() {
  digitalWrite(PIN_LED_STATUS, !digitalRead(PIN_LED_STATUS));

  if (digitalRead(PIN_BOTAO_INTERRUPCAO) == LOW) {
    Serial.println("[aluno] indo dormir de verdade por " +
                   String(SONO_ENTRE_ENVIOS_US / 1000000ULL) + " s.");
    delay(500);
    irParaDeepSleep(SONO_ENTRE_ENVIOS_US);
  }

  // O guardian e alimentado aqui: enquanto o loop roda, o watchdog fica
  // satisfeito. Quando o loop travar, ele para de ser alimentado e o
  // sistema reinicia sozinho.
  esp_task_wdt_reset();

  Serial.println("[caminho A] viva | v" + String(VERSAO_FIRMWARE) + " | heap " +
                 String(ESP.getFreeHeap()) + " B | pulsos na ultima janela: " +
                 String(pulsosNaInterrupcao));
  delay(5000);
}

Sem saída de compilação gravada. Rode python3 validar.py -t 3 dia14 aula1.

Aula 2 — Caminho B: mais servidor

Objetivos

do caminho A ou do caminho B com um número medido, e não com gosto.

  • Explicar, com o custo por dispositivo na tela, por que REST, WebSocket e mensagem retida não são a mesma coisa.
  • Montar a assinatura de MQTT com curinga e dizer quantas linhas de código o painel precisa quando o número de placas cresce.
  • Reproduzir a fila cheia e a mensagem repetida, e apontar no Serial onde a chave de idempotência resolve o segundo problema e onde ela não resolve o primeiro.
  • Dizer qual das duas trilhas do dia resolve o problema que o seu projeto tem, e qual problema ela transfere para o outro lado.

Material

  • 1 ESP32 DevKit V1 por aluno, com o cabo USB
  • 1 botao com pull-up no pino 0, que republica a leitura e refaz a fila na frente da turma
  • Monitor serial a 115200, com a saída completa do sketch rolando na tela do projetor
  • Fechado da biblioteca PubSubClient aberto na documentação da biblioteca, para a turma conferir o nome das chamadas
  • Folha por aluno com as quatro perguntas de defesa da escolha, impressa

Conceitos

A defesa da escolha, e ela precisa de número

O dia promete duas trilhas, a aula de ontem executou a de firmware, e a de hoje é o aprofundar servidor. A parte que fecha o *escolha seu caminho* é a defesa: por que o seu projeto precisa deste caminho, e não do outro.

A defesa tem três perguntas, e o professor as cobra na ordem:

PerguntaO que a resposta prova
o meu projeto é de quantos dispositivos?separa a trilha A da trilha B antes da discussão técnica
o que o painel precisa: pedir ou ser avisado?decide REST, WebSocket ou retida
o que acontece quando a rede cai no meio?separa o que o broker resolve do que ele não resolve
quanto custa o erro nesse caminho: dado perdido ou alerta atrasado?escolhe entre entrega garantida e evento

O número que separa as duas trilhas não é o do tipo de código: é o do número de placas. Uma placa com REST a cada dez segundos e um servidor pequeno é um sistema completo e bem resolvido. O mesmo código com duzentas placas é um sistema que derruba o Node por memória. A trilha do firmware não é "a mais difícil" — é a que serve enquanto o número de dispositivos é pequeno e o produto precisa de vida na bancada; a trilha do servidor é a que serve quando o número cresce e o painel precisa saber de todos ao mesmo tempo.

MQTT: o broker separa quem fala de quem escuta

O broker de mensagens é a peça que resolve o problema do caminho de servidor desde o primeiro dia: como um dado chega a alguém que não estava lá quando ele foi gerado. E o modelo dele é pub/sub — *publicar e assinar*. Quem publica não sabe quem assina, e quem assina não sabe quem publica. A placa continua sendo a origem do dado, e ela não ganhou a obrigação de saber da existência do painel.

O tópico é o endereço da mensagem, e ele não é um identificador: é uma hierarquia de palavras separadas por barra, e é essa hierarquia que vira filtro.

O sketch publica em três endereços e a turma compara:

TópicoQuem assinaRetida
estacao/esp32-bancada-01/leiturao servidor que grava no banconão
estacao/esp32-bancada-01/estadoo painel que abre a qualquer horasim
estacao/esp32-bancada-01/comandoninguém nesta aula: é o painel falandonão
estacao/+/estadoo painel de todas as estações de uma vez— assinatura, não tópico real

O + é um curinga de um nível: casa com o identificador de qualquer estação, e só naquele nível. Com três placas são três assinaturas no total, e com trezentas continuam três. Esse é o número que responde à pergunta do dia 11 aula 1 — como o painel cresce sem reescrever código — e é o argumento mais forte que a trilha do servidor tem contra a trilha do firmware.

A inversão importa do mesmo jeito: quem assina não interfere em quem publica. Se ninguém assinar, a mensagem é descartada, e o broker não guarda fila para quem não chegou. Por isso estado e leitura não dividem o tópico: um precisa ser retido, o outro não.

A retained message, a mensagem retida, é o que o broker guarda do último estado publicado, e ela resolve exatamente um problema: o painel que abre às oito da manhã e pergunta "esta online?" para uma placa que mandou o estado uma vez e dormiu.

CenárioSem retidaCom retida
assinante chega 5 min depoisnão há resposta; o painel mostra desconhecidoo broker entrega o último estado ao entrar
o que a placa precisaria fazeracordar, reconectar e respondernada

O limite da mensagem retida é o ponto que a turma erra: retenção não é histórico. O TEMPO_DE_VIDA_DO_PACOTE de 60 segundos no sketch existe porque leitura velha no painel é pior que painel sem leitura — o usuário acredita no número. Retida serve para o estado, que é o que importa no agora; para a série, quem guarda é o banco, e a granularidade é escolha da consulta, não do tópico.

A mesma retenção é o que dá push e notificação sem o painel pedir nada: o alerta de temperatura não é uma consulta que o navegador repete, é o broker entregando a mensagem assim que ela muda de estado. Quem escreve a regra do alerta continua no servidor, do dia 12; quem entrega o resultado é o broker.

Fila: entrega não é trabalho garantido

O broker entrega a mensagem. Quem garante que o trabalho foi feito é o consumidor, e essa diferença tem nome: a fila de verdade é a fila que o consumidor controla. MQTT é transporte com entrega, e não é fila com trabalho garantido.

A cena da aula é uma fila de cinco posicoes com seis mensagens chegando:

MensagemChaveO que o consumidor fez
1id-4711processou
2id-4712processou
3id-4712duplicado, descartado
4id-4713processou
5id-4714processou
6id-4715fila cheia, descartada

As duas descartagens são defeitos diferentes, e confundi-las é o erro que o professor aponta nesta aula:

  • a mensagem 3 foi descartada pelo conteúdo: a chave de idempotência já tinha sido vista. O evento aconteceu uma vez e foi registrado uma vez. Isso é o que a chave resolve, e é a aula do dia 7 aula 2 do outro lado do broker.
  • a mensagem 6 foi descartada pela capacidade: cinco posições, seis mensagens. Nenhum identificador evita isso, porque o problema não é repetição, é ritmo. Fila cheia é sinal de que o consumidor precisa ser mais rápido ou a fila precisa ser maior.

E o que a fila cheia custa é nomeado, porque "descartar" e "perder dado" são a mesma coisa do ponto de vista do painel: quando a fila estoura, o que se perde é a leitura, e o painel mostra um buraco sem mensagem. Um sistema com fila precisa de sinal de vida do consumidor, senão o silêncio da fila cheia é indistinguível do silêncio de sistema parado.

A fila com Redis é o degrau seguinte do mesmo problema, e a aula não sobe nele: Redis é fila que sobrevive ao reinício, e a fila em memória do sketch não sobrevive a nada. A regra do professor é que a fila em memória serve para a aula; fila que importa para o produto é fila que está no disco, porque trabalho que some no reinício do servidor é trabalho que o usuário pagou e não recebeu.

E é aqui que a aula nomeia a fila real, que é a fila que sobrevive ao reinício e que tem dono: a fila com Redis é um exemplo, e o que faz uma fila ser real não é a ferramenta, é ter chave de idempotência, ter tentativa contada e ter o que fazer quando a tentativa falhou.

WebSocket contra REST: a escolha é de memória

O quarto bloco é a escolha de transporte, e ela é a que o aluno leva para o projeto. A conexão persistente é o que separa as duas: REST é o ciclo de pedir e receber, e WebSocket é a conexão que fica aberta, com o servidor empurrando o evento. Os três casos do sketch:

CasoTransportePor queConexões por dispositivo
painel mostra leitura a cada 2 sREST (pedido e resposta)3 pedidos por minuto, servidor leve, cache funciona3
painel mostra alerta no instante que aconteceWebSocket (conexão aberta)o servidor empurra; sem polling, sem conexão ociosa1
painel só abre de manhã e fecha à noitemensagem retida do brokero assinante recebe o último estado ao abrir, sem pedir0

A conta com duzentas placas é a que o professor escreve na lousa: 600 conexões por minuto que abrem e fecham, ou 200 conexões que ficam abertas o dia todo. A segunda parece mais barata e é a cara do problema: cada conexão aberta custa memória, e duzentas conexões com um megabyte cada já são duzentos megabytes. É aí que o Node começa a travar, e o sintoma não é lógico — é o processo acabando a memória, com o log dizendo que a máquina ficou sem heap.

Documentar a API é o contrato escrito, e a API REST completa com OpenAPI é o lugar onde ele mora: é o que permite a um cliente novo usar a API sem ler o código do servidor. É a mesma ideia do README do projeto final, só que para quem programa em vez de para quem instala — e a trilha de auditoria é a demonstração de que vale a pena: com a API documentada, o aluno lê o contrato e descobre sozinho que a rota de consulta não filtra por dispositivo, sem precisar de ninguém explicando.

E o fechamento do dia é o par que o professor pede em voz alta: esta trilha resolve um problema e transfere outro. Ela resolve o crescimento do número de dispositivos e resolve a entrega sem perguntar a ninguém. Ela transfere o problema do custo de memória, do broker que reinicia e perde o que não estava retido, e da fila que precisa de consumidor mais rápido. A trilha do firmware faz o caminho inverso: compra autonomia da placa e paga com a placa que precisa de repositório de atualização, deep sleep e watchdog. Nenhuma é a certa; a defendável é a que o seu produto precisa.

Atividade

Montagem: botao com pull-up no pino 0 para republicar a leitura e refazer a fila, jumper de saída no pino 2 para o LED de status, ground no GND. O botao e o que repete a cena da fila na frente da turma; o pino 2 e o LED que mostra que a placa continua viva sem broker no ar.

  1. Grave o sketch da resolução e rode no serial. Anote os quatro tópicos impressos e escreva, ao lado de cada um, quem assina. O comando tem assinante nesta aula?
  2. Escreva a assinatura de painel que pega o estado de todas as estações ao mesmo tempo. Com quantas placas essa assinatura funciona? Quantas linhas de código do painel mudam quando o número vai de 3 para 300?
  3. Leia o bloco da retida. Anote o tempo de vida do pacote que o sketch imprime e diga por que leitura não usa tópico retido. O que o painel mostraria se a última leitura tivesse seis minutos?
  4. Conte a cena da fila: quantas mensagens chegaram, quantas foram processadas, quantas foram descartadas por chave e quantas por falta de espaço. As duas descartagens têm a mesma causa? Diga em uma frase qual delas um identificador resolve e qual delas não resolve.
  5. Preencha a tabela de transporte com os três casos do sketch e multiplique por 200. Quantas conexões o REST abre por minuto? Quantas conexões ficam abertas com WebSocket? Em qual dos dois o servidor gasta mais memória, e por quê?
  6. Escolha a trilha que o seu projeto precisa e escreva em uma frase qual problema ela resolve. Depois escreva em outra frase qual problema ela transfere para o outro lado, usando a palavra que o sketch imprimiu. A sua trilha resolve o problema que te trouxe aqui?

Nota: 12 pontos. Critério de fim: os quatro tópicos anotados com o assinante de cada um, e as duas trilhas justificadas com um número e com o problema que ela transfere.

Resolucao

// Aula 2 do dia 14: Caminho B, mais servidor.
//
// Este e o caminho de quem terminou o ano e quer ir fundo do lado do
// servidor. A placa e a origem do dado, e a aula mostra o que acontece do
// outro lado quando a origem cresce de uma placa paracem.
//
// Quatro nucleos, na ordem em que o sistema cresce:
//
//   1. TOPICO: MQTT separa "quem fala" de "quem escuta". A placa publica
//      em `estacao/esp32-bancada-01/leitura` e nao sabe quem consome. O
//      broker e a ponte, e e ele que resolve o dia 11 (quem ouve o que).
//   2. PERSISTENTE (retained): a mensagem que fica. O assinante que chega
//      tarde recebe o ultimo estado sem pedir a ninguem — e o que resolve o
//      painel em branco na abertura.
//   3. FILA REAL: MQTT e entrega, nao garante trabalho. A entrega pode
//      repetir; quem processa precisa de chave de idempotencia, que e a
//      aula do dia 7, agora do outro lado.
//   4. WEBSOCKET E API: conexao que fica aberta (WebSocket) contra o
//      ciclo de pedir e receber (REST + OpenAPI). E a escolha que muda o
//      custo, o painel e o `deploy`.
//
// A placa e o laboratorio: ela publica, assina, e mostra no serial o que
// cada topico entregou. O broker nao precisa estar de pe — a aula mede o
// que a placa faz, e nao o que o broker respondeu.

#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>

// Rede do laboratorio. Os dois valores sao FICTICIOS de proposito: nenhum
// segredo real entra em sketch de exemplo, nem em material, nem no git.
#define WIFI_SSID "lab-esp32-aula"
#define WIFI_PASSWORD "troque-esta-pela-sua"

// Versao do firmware.
#define VERSAO_FIRMWARE "2.0.0"

#define ID_DISPOSITIVO "esp32-bancada-01"

// Endereco do broker. FICTICIO: a aula nao precisa de broker no ar, e
// nenhum broker de verdade entra em material. `mqtt.invalido` e o jeito
// honesto de escrever "este endereco nao existe de proposito".
#define BROKER_MQTT "mqtt.invalido"
#define PORTA_MQTT 1883

// Topicos. A hierarquia e o que faz o broker filtrar: `estacao/+/estado`
// assina o estado de TODAS as estacoes, sem a placa saber quantas sao.
#define TOPICO_BASE "estacao/" ID_DISPOSITIVO
#define TOPICO_LEITURA TOPICO_BASE "/leitura"
#define TOPICO_ESTADO TOPICO_BASE "/estado"
#define TOPICO_COMANDO TOPICO_BASE "/comando"

// ------------------------------------------------------------------ pinos

const int PIN_LED_STATUS = 2;
const int PIN_BOTAO_PUBLICAR = 0;

// ------------------------------------------------------------- configuracao

// Tamanho do pacote. O padrao da biblioteca e 256 bytes: pouco para um JSON
// com historico, e o cliente morre com -1 sem explicar. O numero da aula.
const uint16_t TAMANHO_DO_PACOTE = 1024;

// Tempo de vida da mensagem retida, em segundos. Zero significa "fica para
// sempre", que e o padrao do broker e nao o que a placa quer: a leitura
// antiga nao e o estado atual.
const unsigned int TEMPO_DE_VIDA_DO_PACOTE = 60;

// ------------------------------------------------------------- rede e mqtt

WiFiClient clienteDeRede;
PubSubClient mqtt(clienteDeRede);

bool conectarNaRede() {
  Serial.println("[rede] conectando em " + String(WIFI_SSID) + " ...");
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  unsigned long comeco = millis();
  while (WiFi.status() != WL_CONNECTED && (millis() - comeco) < 20000) {
    delay(200);
  }

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[rede] FALHOU apos 20 s");
    return false;
  }
  Serial.println("[rede] conectado em " + String(millis() - comeco) + " ms | IP " +
                 String(WiFi.localIP()));
  return true;
}

void configurarBroker() {
  mqtt.setServer(BROKER_MQTT, PORTA_MQTT);
  mqtt.setBufferSize(TAMANHO_DO_PACOTE);
  Serial.println("[mqtt] broker: " + String(BROKER_MQTT) + ":" + String(PORTA_MQTT));
  Serial.println("[mqtt] tamanho do pacote: " + String(mqtt.getBufferSize()) +
                 " bytes (padrao da biblioteca: 256)");
  Serial.println();
}

// ------------------------------------------------------------------ tipos

struct Topico {
  const char* nome;
  const char* quemAssina;
  const char* conteudo;
  bool retida;
  bool usadaNaAula;
};

struct MensagemRecebida {
  const char* topico;
  String carga;
  int tamanho;
};

struct CartaoDeDecisao {
  const char* caso;
  const char* transporte;
  const char* porque;
  int custoPorDispositivo;
};

// ------------------------------------------------------- 1. topico

Topico topicos[4];
int totalDeTopicos = 0;

void mostrarTopicos() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" 1. topico — quem fala nao sabe quem escuta");
  Serial.println("========================================");

  topicos[0].nome = TOPICO_LEITURA;
  topicos[0].quemAssina = "o servidor que grava no banco";
  topicos[0].conteudo = "{\"temperatura\":23.5,\"bateria\":3900}";
  topicos[0].retida = false;
  topicos[0].usadaNaAula = true;

  topicos[1].nome = TOPICO_ESTADO;
  topicos[1].quemAssina = "o painel que abre a qualquer hora";
  topicos[1].conteudo = "{\"online\":true,\"firmware\":\"2.0.0\"}";
  topicos[1].retida = true;
  topicos[1].usadaNaAula = true;

  topicos[2].nome = TOPICO_COMANDO;
  topicos[2].quemAssina = "ninguem nesta aula: e o painel falando";
  topicos[2].conteudo = "{\"comando\":\"ajustar_intervalo\"}";
  topicos[2].retida = false;
  topicos[2].usadaNaAula = false;

  topicos[3].nome = "estacao/+/estado";
  topicos[3].quemAssina = "o painel de TODAS as estacoes de uma vez";
  topicos[3].conteudo = "assinatura com curinga, nao topico real";
  topicos[3].retida = false;
  topicos[3].usadaNaAula = true;

  totalDeTopicos = 4;
  for (int i = 0; i < totalDeTopicos; i++) {
    Serial.println();
    Serial.println("  topico: " + String(topicos[i].nome));
    Serial.println("    assina:  " + String(topicos[i].quemAssina));
    Serial.println("    carga:   " + String(topicos[i].conteudo));
    Serial.println("    retida:  " + String(topicos[i].retida ? "sim" : "nao"));
  }
  Serial.println();

  Serial.println("o `+` na assinatura e o filtro: `estacao/+/estado` pega o estado");
  Serial.println("de qualquer estacao, sem a placa saber quantas existem. Com 3");
  Serial.println("placas, sao 3 assinaturas; com 300, continuam 3. E o que");
  Serial.println("responde a pergunta do dia 11: o numero de dispositivos cresce");
  Serial.println("sem o painel reescrever codigo.");
  Serial.println();
  Serial.println("e o inverso: a placa publica e nao sabe quem le. Se ninguem");
  Serial.println("assinar, a mensagem e descartada — o broker nao guarda fila");
  Serial.println("para quem nao chegou. Por isso estado e leitura nao usam o");
  Serial.println("mesmo topico: um precisa ser retido, o outro nao.");
  Serial.println();
}

// ------------------------------------------------------ 2. retida

void mostrarRetida() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" 2. retida — o que o assinante tardio ve");
  Serial.println("========================================");

  Serial.println("cena: a placa enviou o estado uma vez e dormiu.");
  Serial.println();
  Serial.println("  assinante chega 5 minutos depois, e pergunta: 'esta online?'");
  Serial.println();
  Serial.println("  sem mensagem retida:");
  Serial.println("    -> nao ha resposta. o painel mostra desconhecido.");
  Serial.println("    -> a unica forma de saber e pedir a placa, que esta dormindo.");
  Serial.println();
  Serial.println("  com mensagem retida:");
  Serial.println("    -> o broker entrega a ultima mensagem assim que o assinante entra.");
  Serial.println("    -> o painel mostra online, sem pedir nada a ninguem.");
  Serial.println();
  Serial.println("e por isso que estado e retido e leitura nao e:");
  Serial.println("  estado: o que importa e o AGORA. retido serve.");
  Serial.println("  leitura: o que importa e a serie. retido so mostraria a ultima,");
  Serial.println("           e o painel perderia o historico sem perceber.");
  Serial.println();
  Serial.println("a leitura e entrega a cada " + String(TEMPO_DE_VIDA_DO_PACOTE) +
                 " s como no maximo; o broker guarda por esse tempo.");
  Serial.println("passou isso, uma leitura velha some — e melhor assim: leitura");
  Serial.println("velha no painel e pior que painel sem leitura, porque o");
  Serial.println("usuario acredita no numero.");
  Serial.println();
}

// ------------------------------------------------------- 3. fila

MensagemRecebida fila[5];
int totalNaFila = 0;
int processados = 0;
int duplicadosDescartados = 0;
String ultimaChaveProcessada = "";

void enfileirar(const char* topico, const String& carga) {
  if (totalNaFila >= 5) {
    // Fila cheia e o que a aula do dia 7 mostrou: quando o consumidor e mais
    // lento que o produtor, a fila cresce ate estourar. O sintoma e o mesmo
    // em qualquer lugar: memoria subindo e, no fim, processo morrendo.
    Serial.println("  fila CHEIA: mensagem descartada, o consumidor esta mais");
    Serial.println("           o consumidor esta mais lento que o produtor");
    return;
  }
  fila[totalNaFila].topico = topico;
  fila[totalNaFila].carga = carga;
  fila[totalNaFila].tamanho = static_cast<int>(carga.length());
  totalNaFila++;
}

void processarFila() {
  for (int i = 0; i < totalNaFila; i++) {
    // A chave de idempotencia vem dentro da propria carga. Sem ela, o
    // mesmo trabalho processado duas vezes gera dois alertas.
    int posicao = fila[i].carga.indexOf("\"id\":\"");
    String chave = "";
    if (posicao >= 0) {
      int inicio = posicao + 6;
      int fim = fila[i].carga.indexOf("\"", inicio);
      if (fim > inicio) {
        chave = fila[i].carga.substring(inicio, fim);
      }
    }

    if (chave.length() > 0 && chave == ultimaChaveProcessada) {
      duplicadosDescartados++;
      Serial.println("  [" + String(i + 1) + "] DUPLICADO, descartado: " + chave);
      continue;
    }

    ultimaChaveProcessada = chave;
    processados++;
    Serial.println("  [" + String(i + 1) + "] processado: " + String(fila[i].topico) +
                   " | " + String(fila[i].tamanho) + " B | chave " + chave);
  }
  totalNaFila = 0;
}

void mostrarFila() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" 3. fila — entrega nao e trabalho garantido");
  Serial.println("========================================");
  Serial.println("o broker ENTREGA a mensagem. Quem garante que o trabalho foi");
  Serial.println("feito e o consumidor, e o preco e a chave de idempotencia.");
  Serial.println();

  // Duas leituras normais e uma repetida: a repetida e o que a rede faz
  // quando a confirmacao se perde.
  Serial.println("chegando na fila:");
  enfileirar(TOPICO_LEITURA, "{\"id\":\"id-4711\",\"temperatura\":23.5}");
  enfileirar(TOPICO_LEITURA, "{\"id\":\"id-4712\",\"temperatura\":24.1}");
  enfileirar(TOPICO_LEITURA, "{\"id\":\"id-4712\",\"temperatura\":24.1}");
  enfileirar(TOPICO_LEITURA, "{\"id\":\"id-4713\",\"temperatura\":24.0}");
  enfileirar(TOPICO_LEITURA, "{\"id\":\"id-4714\",\"temperatura\":24.2}");
  enfileirar(TOPICO_LEITURA, "{\"id\":\"id-4715\",\"temperatura\":24.3}");

  processarFila();
  Serial.println();
  Serial.println("processados: " + String(processados) + " | duplicados descartados: " +
                 String(duplicadosDescartados));
  Serial.println();
  Serial.println("a terceira mensagem e a repetida da segunda: mesmo id, mesmo");
  Serial.println("conteudo. O consumidor viu a chave e descartou. Sem a chave,");
  Serial.println("seriam dois registros e dois alertas para um evento so — que");
  Serial.println("e o defeito que a aula do dia 7 mostrou happening.");
  Serial.println();
  Serial.println("a ultima mensagem foi para a fila CHEIA: sao cinco posicoes.");
  Serial.println("Fila cheia nao e bug, e sinal de que o consumidor precisa ser");
  Serial.println("mais rapido, ou a fila precisa ser maior. O que nao pode e");
  Serial.println("ignorar: quando a fila estoura, o que se perde e o dado.");
  Serial.println();
}

// ------------------------------------------- 4. websocket contra rest

CartaoDeDecisao cartoes[3];

void mostrarTransporte() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" 4. websocket contra REST — a escolha de custo");
  Serial.println("========================================");

  cartoes[0].caso = "painel mostra leitura a cada 2 s";
  cartoes[0].transporte = "REST (pedido e resposta)";
  cartoes[0].porque = "3 pedidos por minuto, servidor leve, cache funciona";
  cartoes[0].custoPorDispositivo = 3;

  cartoes[1].caso = "painel mostra alerta no instante que acontece";
  cartoes[1].transporte = "WebSocket (conexao aberta)";
  cartoes[1].porque = "o servidor empurra; sem polling, sem conexao ociosa";
  cartoes[1].custoPorDispositivo = 1;

  cartoes[2].caso = "painel so abre de manha e fecha a noite";
  cartoes[2].transporte = "mensagem retida do broker";
  cartoes[2].porque = "o assinante recebe o ultimo estado ao abrir, sem pedir";
  cartoes[2].custoPorDispositivo = 0;

  for (int i = 0; i < 3; i++) {
    Serial.println();
    Serial.println("  caso: " + String(cartoes[i].caso));
    Serial.println("    transporte: " + String(cartoes[i].transporte));
    Serial.println("    porque:     " + String(cartoes[i].porque));
    Serial.println("    custo:      " + String(cartoes[i].custoPorDispositivo) +
                   " conexao por dispositivo");
  }
  Serial.println();
  Serial.println("o numero da ultima linha e o que responde a pergunta de custo.");
  Serial.println("com 200 placas:");
  Serial.println("  REST a cada 2 s: " + String(200 * cartoes[0].custoPorDispositivo) +
                 " conexoes por minuto, todas abrindo e fechando");
  Serial.println("  WebSocket:      " + String(200 * cartoes[1].custoPorDispositivo) +
                 " conexao por placa, aberta o dia todo");
  Serial.println("  retida:         " + String(200 * cartoes[2].custoPorDispositivo) +
                 " conexao ate o painel pedir");
  Serial.println();
  Serial.println("cada conexao aberta tem custo de memoria no servidor. 200 conexoes");
  Serial.println("sustentadas com 1 MB cada ja e 200 MB, e ai o Node comeca a");
  Serial.println("travar por falta de memoria, nao por logica. A escolha do");
  Serial.println("transporte e uma escolha de memoria tanto quanto de latencia.");
  Serial.println();
  Serial.println("API REST completa: a documentacao OpenAPI e o contrato");
  Serial.println("escrito, que e o que permite a um cliente novo usar a API");
  Serial.println("sem ler o codigo do servidor. E a mesma ideia do README do");
  Serial.println("projeto final, so que para quem programa.");
  Serial.println();
}

// ---------------------------------------------------------------- setup

void setup() {
  Serial.begin(115200);
  delay(300);

  pinMode(PIN_LED_STATUS, OUTPUT);
  pinMode(PIN_BOTAO_PUBLICAR, INPUT_PULLUP);
  digitalWrite(PIN_LED_STATUS, HIGH);

  Serial.println();
  Serial.println("========================================");
  Serial.println(" caminho B: mais servidor — v" + String(VERSAO_FIRMWARE));
  Serial.println("========================================");
  Serial.println("dispositivo: " + String(ID_DISPOSITIVO));
  Serial.println("cpu: " + String(getCpuFrequencyMhz()) + " MHz | heap: " +
                 String(ESP.getFreeHeap()) + " bytes");
  Serial.println();

  conectarNaRede();
  configurarBroker();

  mostrarTopicos();
  mostrarRetida();
  mostrarFila();
  mostrarTransporte();

  Serial.println("========================================");
  Serial.println(" caminho B: resumo");
  Serial.println("========================================");
  Serial.println("topico:      separa quem fala de quem escuta");
  Serial.println("retida:      o assinante tardio ve o estado sem pedir");
  Serial.println("fila:        entrega e != trabalho garantido; chave resolve");
  Serial.println("transporte:  REST para pedir, WebSocket para empurrar");
  Serial.println();
  Serial.println("o fio que liga os quatro: todos resolvem o MESMO problema —");
  Serial.println("o dado tem de chegar a alguem que nao estava la quando ele foi");
  Serial.println("gerado. Topic resolve quem, retida resolve quando, fila resolve");
  Serial.println("quantas vezes, e transporte resolve sem perguntar.");
  Serial.println();
  Serial.println("========================================");
  Serial.println(" aperte o botao para publicar a leitura de novo");
  Serial.println("========================================");
}

void loop() {
  digitalWrite(PIN_LED_STATUS, !digitalRead(PIN_LED_STATUS));
  mqtt.loop();

  if (digitalRead(PIN_BOTAO_PUBLICAR) == LOW) {
    static unsigned long ultimoToque = 0;
    if (millis() - ultimoToque > 2000) {
      ultimoToque = millis();
      String leitura = "{\"id\":\"id-" + String(millis() / 1000) +
                       "\",\"temperatura\":24.6,\"firmware\":\"" +
                       String(VERSAO_FIRMWARE) + "\"}";
      Serial.println("[placa] publicando em " + String(TOPICO_LEITURA) + ": " + leitura);
      enfileirar(TOPICO_LEITURA, leitura);
      processarFila();
    }
  }

  Serial.println("[caminho B] viva | v" + String(VERSAO_FIRMWARE) + " | heap " +
                 String(ESP.getFreeHeap()) + " B | processados " + String(processados) +
                 " | duplicados " + String(duplicadosDescartados) + " | fila " +
                 String(totalNaFila) + "/5 | broker: " + String(mqtt.connected() ? "conectado" : "sem broker"));
  delay(6000);
}

Por que assim e não de outro jeito. Esta aula é a segunda metade do dia, e a metade que fecha o *escolha seu caminho* é a defesa: a trilha não é a resposta, é a hipótese. Por isso o sketch não implementa broker nem servidor — ele monta a cena dos dois lados, mede o custo e deixa a decisão escrita. O broker de verdade não entra na aula: mqtt.invalido é o endereço que existe só para o aluno ver que o endereço é um parâmetro, e não uma constante colada no meio do código.

A primeira escolha do sketch é o tamanho do pacote. A biblioteca entra com 256 bytes, e um JSON com histórico passa disso: o cliente morre devolvendo -1 sem explicar nada. Por isso setBufferSize(1024) é a primeira linha de configuração, e ela vem com o número da biblioteca impresso no Serial para a turma comparar. O aluno que ver 256 e 1024 na mesma tela entende que aquele -1 é configurável.

A hierarquia de tópicos é montada por #define a partir de um TOPICO_BASE, e não com quatro strings repetidas. É a mesma disciplina do id_dispositivo que o painel vai concatenar, e a razão é concreta: se o identificador da estação mudar, muda um lugar, e as assinaturas com + continuam válidas porque elas nunca escreveram o identificador. A assinatura curinga é a única do conjunto que não depende do ID_DISPOSITIVO, e é por isso que ela sobrevive ao crescimento.

A enfileirar tem a guarda de fila cheia antes de escrever, e não depois. A alternativa — escrever na posição 5 e descobrir o estouro no processador — sobrescreve a mensagem que já estava lá e perde duas em vez de uma. O Serial do estouro nomeia a causa em uma frase: o consumidor está mais lento que o produtor. A aula do dia 7 mostrou essa curva chegando, e o nome da causa é o que separa "não achei o bug" de "o sistema é lento demais para a chegada".

A detecção de duplicado é feita sobre o conteúdo da carga, procurando a chave com indexOf, e não com uma comparação do payload inteiro. Isso é uma escolha que o professor admite ser de material, não de produção: em produção a chave viaja em campo próprio do protocolo ou no cabeçalho, e ninguém procura id dentro de uma string. O que a aula quer mostrar é que a chave é o que decide, e que a comparação de conteúdo inteiro trataria duas leituras iguais de dois eventos diferentes como duplicadas — o que também é defeito, do lado oposto.

O TEMPO_DE_VIDA_DO_PACOTE de 60 segundos vale para leitura, e não para estado. O comentário do sketch diz que zero significaria "fica para sempre", que é o padrão do broker e não o que a placa quer: a leitura antiga não é o estado atual, e leitura velha no painel é pior que painel sem leitura porque o usuário acredita no número.

O quadro de transporte fecha com o WebSocket como conexão de 1 por dispositivo e o REST como 3 por minuto, e a conta de 200 placas sai na tela. A frase que o professor dita depois do quadro é a que o professor espera no caderno do aluno: a escolha de transporte é uma escolha de memória tanto quanto de latência, e o sintoma de escolher errado não é lentidão, é o processo morrer sem mensagem de erro.

Criterios de correcao

CritérioPontos
Quatro tópicos anotados com o assinante de cada um, e o comando sem assinante na aula2 pontos
Assinatura curinga escrita, com o número de linhas do painel que não muda de 3 para 300 placas2 pontos
Tempo de vida do pacote anotado, e o motivo de leitura não ser retida2 pontos
Cena da fila contada: mensagens, processadas, duplicadas por chave e perdidas por espaço3 pontos
Tabela de transporte preenchida e multiplicada por 200, com as duas contas de conexão2 pontos
Trilha escolhida com um número, e o problema que ela transfere nomeado1 ponto

Erros comuns

ErroComo apareceCorrecao
Publicar e assinar no mesmo tópicoo painel recebe a leitura inteira e tenta desenhar a série de dez em dez segundos"Estado e leitura respondem a perguntas diferentes. O estado é o agora e pode ser retido; a leitura é série e quem guarda é o banco. Um tópico, dois problemas, um deles sempre mal."
Assinatura com * em vez de +a turma escreve estacao/*/estado e o broker não entrega nada"* atravessa níveis e # casa o resto inteiro; + casa um nível. Para pegar o estado de qualquer estação, o que casa uma estação é o +."
Topico escrito com identificador fixo e curinga misturado"assino estacao/esp32-bancada-01/estado e espero todas""A assinatura com identificador fixo pega uma estação, e é a da sua placa. Para pegar todas, o identificativo sai do meio e entra o +. Ou você assina uma, ou você assina o curinga."
Confundir entrega com trabalho garantido"o broker recebeu, então está gravado""O broker entrega a mensagem, e não garante o trabalho. Quem garante é o consumidor, com a chave de idempotência para a repetição e com sinal de vida para o resto."
Achar que a chave de idempotência resolve a fila cheia"descartou o duplicado, então não perde nada""A chave resolve repetição; a fila cheia é excesso de arrival, não repetição. Nenhum identificador cria uma sexta posição. A solução é consumidor mais rápido ou fila maior."
Ignorar a mensagem descartada por falta de espaçoo painel mostra um buraco e ninguém sabe que houve perda"Fila cheia precisa de sinal: contador de descartados no log e no painel. O silêncio entre 'não chegou nada' e 'chegou e foi descartado' é o defeito de observabilidade do dia 8, no consumidor."
Deixar mensagem retida em tópico de leiturao painel mostra a leitura de dez minutos atrás com hora de agora"Leitura velha no painel é pior que painel sem leitura, porque o usuário acredita no número. Retida serve para o estado, e o histórico é do banco com a granularidade da consulta."
setBufferSize depois de setServero cliente morre com -1 e ninguém sabe por quê"A ordem importa: configure o broker e depois o tamanho do pacote. Feito ao contrário, a biblioteca usa o tamanho antigo e a falha aparece como -1, que parece erro de rede."
Fazer polling a cada 2 s em 200 placas achando que é baratoo Node derruba e o sintoma é "ficou sem memória""Duzentas placas com polling são seiscentas conexões por minuto. E conexões abertas que custam memória. O critério é o caso do painel: se ele só precisa avisar, retida e WebSocket são mais baratos."
WebSocket em tudo por ser "moderno"200 conexões abertas, servidor consumindo memória"WebSocket é para empurrar evento. Se o painel lê a cada 2 s e cabe em cache, REST é mais barato e mais simples. Escolha pelo caso, não pela moda do nome."
MQTT como substituto do bancoa série some quando o broker reinicia"MQTT é transporte, com retenção de um estado. A série histórica é do banco, e a granularidade é da consulta. Trocar um pelo outro trocareo o problema em vez de resolver."
Conectar e não reconectaro broker reinicia e a placa nunca mais publica"Cliente de MQTT precisa de reconexão com espera, e a espera é do dia 7 aula 2. Publicar em loop sem reconectar é o mesmo defeito de WiFi do dia 3: um erro e vira placa morta."

Desafio extra

Escreva um consumidor de verdade do lado do servidor, em Node, que assine estacao/+/estado e estacao/+/leitura ao mesmo tempo. Faça ele imprimir o identificador da estação em cada mensagem recebida, e meça: com 1 placa, com 3 e com 10 assinando o mesmo tópico, quantas mensagens por segundo o consumidor recebe e quanto tempo ele leva para processar cada uma. Depois desligue o broker no meio e veja o que o consumidor faz depois que ele volta. Escreva uma frase sobre o que precisou mudar no consumidor para que a queda do broker não virasse perda de dado no banco, e cite o número que mediu.

>

A resolucao, compilada

// Aula 2 do dia 14: Caminho B, mais servidor.
//
// Este e o caminho de quem terminou o ano e quer ir fundo do lado do
// servidor. A placa e a origem do dado, e a aula mostra o que acontece do
// outro lado quando a origem cresce de uma placa paracem.
//
// Quatro nucleos, na ordem em que o sistema cresce:
//
//   1. TOPICO: MQTT separa "quem fala" de "quem escuta". A placa publica
//      em `estacao/esp32-bancada-01/leitura` e nao sabe quem consome. O
//      broker e a ponte, e e ele que resolve o dia 11 (quem ouve o que).
//   2. PERSISTENTE (retained): a mensagem que fica. O assinante que chega
//      tarde recebe o ultimo estado sem pedir a ninguem — e o que resolve o
//      painel em branco na abertura.
//   3. FILA REAL: MQTT e entrega, nao garante trabalho. A entrega pode
//      repetir; quem processa precisa de chave de idempotencia, que e a
//      aula do dia 7, agora do outro lado.
//   4. WEBSOCKET E API: conexao que fica aberta (WebSocket) contra o
//      ciclo de pedir e receber (REST + OpenAPI). E a escolha que muda o
//      custo, o painel e o `deploy`.
//
// A placa e o laboratorio: ela publica, assina, e mostra no serial o que
// cada topico entregou. O broker nao precisa estar de pe — a aula mede o
// que a placa faz, e nao o que o broker respondeu.

#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>

// Rede do laboratorio. Os dois valores sao FICTICIOS de proposito: nenhum
// segredo real entra em sketch de exemplo, nem em material, nem no git.
#define WIFI_SSID "lab-esp32-aula"
#define WIFI_PASSWORD "troque-esta-pela-sua"

// Versao do firmware.
#define VERSAO_FIRMWARE "2.0.0"

#define ID_DISPOSITIVO "esp32-bancada-01"

// Endereco do broker. FICTICIO: a aula nao precisa de broker no ar, e
// nenhum broker de verdade entra em material. `mqtt.invalido` e o jeito
// honesto de escrever "este endereco nao existe de proposito".
#define BROKER_MQTT "mqtt.invalido"
#define PORTA_MQTT 1883

// Topicos. A hierarquia e o que faz o broker filtrar: `estacao/+/estado`
// assina o estado de TODAS as estacoes, sem a placa saber quantas sao.
#define TOPICO_BASE "estacao/" ID_DISPOSITIVO
#define TOPICO_LEITURA TOPICO_BASE "/leitura"
#define TOPICO_ESTADO TOPICO_BASE "/estado"
#define TOPICO_COMANDO TOPICO_BASE "/comando"

// ------------------------------------------------------------------ pinos

const int PIN_LED_STATUS = 2;
const int PIN_BOTAO_PUBLICAR = 0;

// ------------------------------------------------------------- configuracao

// Tamanho do pacote. O padrao da biblioteca e 256 bytes: pouco para um JSON
// com historico, e o cliente morre com -1 sem explicar. O numero da aula.
const uint16_t TAMANHO_DO_PACOTE = 1024;

// Tempo de vida da mensagem retida, em segundos. Zero significa "fica para
// sempre", que e o padrao do broker e nao o que a placa quer: a leitura
// antiga nao e o estado atual.
const unsigned int TEMPO_DE_VIDA_DO_PACOTE = 60;

// ------------------------------------------------------------- rede e mqtt

WiFiClient clienteDeRede;
PubSubClient mqtt(clienteDeRede);

bool conectarNaRede() {
  Serial.println("[rede] conectando em " + String(WIFI_SSID) + " ...");
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  unsigned long comeco = millis();
  while (WiFi.status() != WL_CONNECTED && (millis() - comeco) < 20000) {
    delay(200);
  }

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("[rede] FALHOU apos 20 s");
    return false;
  }
  Serial.println("[rede] conectado em " + String(millis() - comeco) + " ms | IP " +
                 String(WiFi.localIP()));
  return true;
}

void configurarBroker() {
  mqtt.setServer(BROKER_MQTT, PORTA_MQTT);
  mqtt.setBufferSize(TAMANHO_DO_PACOTE);
  Serial.println("[mqtt] broker: " + String(BROKER_MQTT) + ":" + String(PORTA_MQTT));
  Serial.println("[mqtt] tamanho do pacote: " + String(mqtt.getBufferSize()) +
                 " bytes (padrao da biblioteca: 256)");
  Serial.println();
}

// ------------------------------------------------------------------ tipos

struct Topico {
  const char* nome;
  const char* quemAssina;
  const char* conteudo;
  bool retida;
  bool usadaNaAula;
};

struct MensagemRecebida {
  const char* topico;
  String carga;
  int tamanho;
};

struct CartaoDeDecisao {
  const char* caso;
  const char* transporte;
  const char* porque;
  int custoPorDispositivo;
};

// ------------------------------------------------------- 1. topico

Topico topicos[4];
int totalDeTopicos = 0;

void mostrarTopicos() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" 1. topico — quem fala nao sabe quem escuta");
  Serial.println("========================================");

  topicos[0].nome = TOPICO_LEITURA;
  topicos[0].quemAssina = "o servidor que grava no banco";
  topicos[0].conteudo = "{\"temperatura\":23.5,\"bateria\":3900}";
  topicos[0].retida = false;
  topicos[0].usadaNaAula = true;

  topicos[1].nome = TOPICO_ESTADO;
  topicos[1].quemAssina = "o painel que abre a qualquer hora";
  topicos[1].conteudo = "{\"online\":true,\"firmware\":\"2.0.0\"}";
  topicos[1].retida = true;
  topicos[1].usadaNaAula = true;

  topicos[2].nome = TOPICO_COMANDO;
  topicos[2].quemAssina = "ninguem nesta aula: e o painel falando";
  topicos[2].conteudo = "{\"comando\":\"ajustar_intervalo\"}";
  topicos[2].retida = false;
  topicos[2].usadaNaAula = false;

  topicos[3].nome = "estacao/+/estado";
  topicos[3].quemAssina = "o painel de TODAS as estacoes de uma vez";
  topicos[3].conteudo = "assinatura com curinga, nao topico real";
  topicos[3].retida = false;
  topicos[3].usadaNaAula = true;

  totalDeTopicos = 4;
  for (int i = 0; i < totalDeTopicos; i++) {
    Serial.println();
    Serial.println("  topico: " + String(topicos[i].nome));
    Serial.println("    assina:  " + String(topicos[i].quemAssina));
    Serial.println("    carga:   " + String(topicos[i].conteudo));
    Serial.println("    retida:  " + String(topicos[i].retida ? "sim" : "nao"));
  }
  Serial.println();

  Serial.println("o `+` na assinatura e o filtro: `estacao/+/estado` pega o estado");
  Serial.println("de qualquer estacao, sem a placa saber quantas existem. Com 3");
  Serial.println("placas, sao 3 assinaturas; com 300, continuam 3. E o que");
  Serial.println("responde a pergunta do dia 11: o numero de dispositivos cresce");
  Serial.println("sem o painel reescrever codigo.");
  Serial.println();
  Serial.println("e o inverso: a placa publica e nao sabe quem le. Se ninguem");
  Serial.println("assinar, a mensagem e descartada — o broker nao guarda fila");
  Serial.println("para quem nao chegou. Por isso estado e leitura nao usam o");
  Serial.println("mesmo topico: um precisa ser retido, o outro nao.");
  Serial.println();
}

// ------------------------------------------------------ 2. retida

void mostrarRetida() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" 2. retida — o que o assinante tardio ve");
  Serial.println("========================================");

  Serial.println("cena: a placa enviou o estado uma vez e dormiu.");
  Serial.println();
  Serial.println("  assinante chega 5 minutos depois, e pergunta: 'esta online?'");
  Serial.println();
  Serial.println("  sem mensagem retida:");
  Serial.println("    -> nao ha resposta. o painel mostra desconhecido.");
  Serial.println("    -> a unica forma de saber e pedir a placa, que esta dormindo.");
  Serial.println();
  Serial.println("  com mensagem retida:");
  Serial.println("    -> o broker entrega a ultima mensagem assim que o assinante entra.");
  Serial.println("    -> o painel mostra online, sem pedir nada a ninguem.");
  Serial.println();
  Serial.println("e por isso que estado e retido e leitura nao e:");
  Serial.println("  estado: o que importa e o AGORA. retido serve.");
  Serial.println("  leitura: o que importa e a serie. retido so mostraria a ultima,");
  Serial.println("           e o painel perderia o historico sem perceber.");
  Serial.println();
  Serial.println("a leitura e entrega a cada " + String(TEMPO_DE_VIDA_DO_PACOTE) +
                 " s como no maximo; o broker guarda por esse tempo.");
  Serial.println("passou isso, uma leitura velha some — e melhor assim: leitura");
  Serial.println("velha no painel e pior que painel sem leitura, porque o");
  Serial.println("usuario acredita no numero.");
  Serial.println();
}

// ------------------------------------------------------- 3. fila

MensagemRecebida fila[5];
int totalNaFila = 0;
int processados = 0;
int duplicadosDescartados = 0;
String ultimaChaveProcessada = "";

void enfileirar(const char* topico, const String& carga) {
  if (totalNaFila >= 5) {
    // Fila cheia e o que a aula do dia 7 mostrou: quando o consumidor e mais
    // lento que o produtor, a fila cresce ate estourar. O sintoma e o mesmo
    // em qualquer lugar: memoria subindo e, no fim, processo morrendo.
    Serial.println("  fila CHEIA: mensagem descartada, o consumidor esta mais");
    Serial.println("           o consumidor esta mais lento que o produtor");
    return;
  }
  fila[totalNaFila].topico = topico;
  fila[totalNaFila].carga = carga;
  fila[totalNaFila].tamanho = static_cast<int>(carga.length());
  totalNaFila++;
}

void processarFila() {
  for (int i = 0; i < totalNaFila; i++) {
    // A chave de idempotencia vem dentro da propria carga. Sem ela, o
    // mesmo trabalho processado duas vezes gera dois alertas.
    int posicao = fila[i].carga.indexOf("\"id\":\"");
    String chave = "";
    if (posicao >= 0) {
      int inicio = posicao + 6;
      int fim = fila[i].carga.indexOf("\"", inicio);
      if (fim > inicio) {
        chave = fila[i].carga.substring(inicio, fim);
      }
    }

    if (chave.length() > 0 && chave == ultimaChaveProcessada) {
      duplicadosDescartados++;
      Serial.println("  [" + String(i + 1) + "] DUPLICADO, descartado: " + chave);
      continue;
    }

    ultimaChaveProcessada = chave;
    processados++;
    Serial.println("  [" + String(i + 1) + "] processado: " + String(fila[i].topico) +
                   " | " + String(fila[i].tamanho) + " B | chave " + chave);
  }
  totalNaFila = 0;
}

void mostrarFila() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" 3. fila — entrega nao e trabalho garantido");
  Serial.println("========================================");
  Serial.println("o broker ENTREGA a mensagem. Quem garante que o trabalho foi");
  Serial.println("feito e o consumidor, e o preco e a chave de idempotencia.");
  Serial.println();

  // Duas leituras normais e uma repetida: a repetida e o que a rede faz
  // quando a confirmacao se perde.
  Serial.println("chegando na fila:");
  enfileirar(TOPICO_LEITURA, "{\"id\":\"id-4711\",\"temperatura\":23.5}");
  enfileirar(TOPICO_LEITURA, "{\"id\":\"id-4712\",\"temperatura\":24.1}");
  enfileirar(TOPICO_LEITURA, "{\"id\":\"id-4712\",\"temperatura\":24.1}");
  enfileirar(TOPICO_LEITURA, "{\"id\":\"id-4713\",\"temperatura\":24.0}");
  enfileirar(TOPICO_LEITURA, "{\"id\":\"id-4714\",\"temperatura\":24.2}");
  enfileirar(TOPICO_LEITURA, "{\"id\":\"id-4715\",\"temperatura\":24.3}");

  processarFila();
  Serial.println();
  Serial.println("processados: " + String(processados) + " | duplicados descartados: " +
                 String(duplicadosDescartados));
  Serial.println();
  Serial.println("a terceira mensagem e a repetida da segunda: mesmo id, mesmo");
  Serial.println("conteudo. O consumidor viu a chave e descartou. Sem a chave,");
  Serial.println("seriam dois registros e dois alertas para um evento so — que");
  Serial.println("e o defeito que a aula do dia 7 mostrou happening.");
  Serial.println();
  Serial.println("a ultima mensagem foi para a fila CHEIA: sao cinco posicoes.");
  Serial.println("Fila cheia nao e bug, e sinal de que o consumidor precisa ser");
  Serial.println("mais rapido, ou a fila precisa ser maior. O que nao pode e");
  Serial.println("ignorar: quando a fila estoura, o que se perde e o dado.");
  Serial.println();
}

// ------------------------------------------- 4. websocket contra rest

CartaoDeDecisao cartoes[3];

void mostrarTransporte() {
  Serial.println();
  Serial.println("========================================");
  Serial.println(" 4. websocket contra REST — a escolha de custo");
  Serial.println("========================================");

  cartoes[0].caso = "painel mostra leitura a cada 2 s";
  cartoes[0].transporte = "REST (pedido e resposta)";
  cartoes[0].porque = "3 pedidos por minuto, servidor leve, cache funciona";
  cartoes[0].custoPorDispositivo = 3;

  cartoes[1].caso = "painel mostra alerta no instante que acontece";
  cartoes[1].transporte = "WebSocket (conexao aberta)";
  cartoes[1].porque = "o servidor empurra; sem polling, sem conexao ociosa";
  cartoes[1].custoPorDispositivo = 1;

  cartoes[2].caso = "painel so abre de manha e fecha a noite";
  cartoes[2].transporte = "mensagem retida do broker";
  cartoes[2].porque = "o assinante recebe o ultimo estado ao abrir, sem pedir";
  cartoes[2].custoPorDispositivo = 0;

  for (int i = 0; i < 3; i++) {
    Serial.println();
    Serial.println("  caso: " + String(cartoes[i].caso));
    Serial.println("    transporte: " + String(cartoes[i].transporte));
    Serial.println("    porque:     " + String(cartoes[i].porque));
    Serial.println("    custo:      " + String(cartoes[i].custoPorDispositivo) +
                   " conexao por dispositivo");
  }
  Serial.println();
  Serial.println("o numero da ultima linha e o que responde a pergunta de custo.");
  Serial.println("com 200 placas:");
  Serial.println("  REST a cada 2 s: " + String(200 * cartoes[0].custoPorDispositivo) +
                 " conexoes por minuto, todas abrindo e fechando");
  Serial.println("  WebSocket:      " + String(200 * cartoes[1].custoPorDispositivo) +
                 " conexao por placa, aberta o dia todo");
  Serial.println("  retida:         " + String(200 * cartoes[2].custoPorDispositivo) +
                 " conexao ate o painel pedir");
  Serial.println();
  Serial.println("cada conexao aberta tem custo de memoria no servidor. 200 conexoes");
  Serial.println("sustentadas com 1 MB cada ja e 200 MB, e ai o Node comeca a");
  Serial.println("travar por falta de memoria, nao por logica. A escolha do");
  Serial.println("transporte e uma escolha de memoria tanto quanto de latencia.");
  Serial.println();
  Serial.println("API REST completa: a documentacao OpenAPI e o contrato");
  Serial.println("escrito, que e o que permite a um cliente novo usar a API");
  Serial.println("sem ler o codigo do servidor. E a mesma ideia do README do");
  Serial.println("projeto final, so que para quem programa.");
  Serial.println();
}

// ---------------------------------------------------------------- setup

void setup() {
  Serial.begin(115200);
  delay(300);

  pinMode(PIN_LED_STATUS, OUTPUT);
  pinMode(PIN_BOTAO_PUBLICAR, INPUT_PULLUP);
  digitalWrite(PIN_LED_STATUS, HIGH);

  Serial.println();
  Serial.println("========================================");
  Serial.println(" caminho B: mais servidor — v" + String(VERSAO_FIRMWARE));
  Serial.println("========================================");
  Serial.println("dispositivo: " + String(ID_DISPOSITIVO));
  Serial.println("cpu: " + String(getCpuFrequencyMhz()) + " MHz | heap: " +
                 String(ESP.getFreeHeap()) + " bytes");
  Serial.println();

  conectarNaRede();
  configurarBroker();

  mostrarTopicos();
  mostrarRetida();
  mostrarFila();
  mostrarTransporte();

  Serial.println("========================================");
  Serial.println(" caminho B: resumo");
  Serial.println("========================================");
  Serial.println("topico:      separa quem fala de quem escuta");
  Serial.println("retida:      o assinante tardio ve o estado sem pedir");
  Serial.println("fila:        entrega e != trabalho garantido; chave resolve");
  Serial.println("transporte:  REST para pedir, WebSocket para empurrar");
  Serial.println();
  Serial.println("o fio que liga os quatro: todos resolvem o MESMO problema —");
  Serial.println("o dado tem de chegar a alguem que nao estava la quando ele foi");
  Serial.println("gerado. Topic resolve quem, retida resolve quando, fila resolve");
  Serial.println("quantas vezes, e transporte resolve sem perguntar.");
  Serial.println();
  Serial.println("========================================");
  Serial.println(" aperte o botao para publicar a leitura de novo");
  Serial.println("========================================");
}

void loop() {
  digitalWrite(PIN_LED_STATUS, !digitalRead(PIN_LED_STATUS));
  mqtt.loop();

  if (digitalRead(PIN_BOTAO_PUBLICAR) == LOW) {
    static unsigned long ultimoToque = 0;
    if (millis() - ultimoToque > 2000) {
      ultimoToque = millis();
      String leitura = "{\"id\":\"id-" + String(millis() / 1000) +
                       "\",\"temperatura\":24.6,\"firmware\":\"" +
                       String(VERSAO_FIRMWARE) + "\"}";
      Serial.println("[placa] publicando em " + String(TOPICO_LEITURA) + ": " + leitura);
      enfileirar(TOPICO_LEITURA, leitura);
      processarFila();
    }
  }

  Serial.println("[caminho B] viva | v" + String(VERSAO_FIRMWARE) + " | heap " +
                 String(ESP.getFreeHeap()) + " B | processados " + String(processados) +
                 " | duplicados " + String(duplicadosDescartados) + " | fila " +
                 String(totalNaFila) + "/5 | broker: " + String(mqtt.connected() ? "conectado" : "sem broker"));
  delay(6000);
}

Sem saída de compilação gravada. Rode python3 validar.py -t 3 dia14 aula2.