Aprofundamento por escolha — Arduino e IoT — semana 14 do 3o trimestre
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
FreeRTOSe 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 daISR. - 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:
- a imagem nova vai para a
particaoque não esta rodando - a proxima vez que a placa reinicia, ela le a nova
- 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:
| Tarefa | Prioridade | Por que |
|---|---|---|
| rede | 5 | perder um pedido custa mais que atrasar uma leitura |
| sensor | 3 | atrasar 10 ms não custa nada |
| principal | 1 | o 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:
- orcamento de tempo: no ESP32, cerca de um microsegundo. Passou disso, o sistema reinicia.
- sem printing:
Serial.printdentro daISRtrava a placa. O trabalho de verdade vai para a tarefa. - so incremente e guarde: a
ISRincrementa 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
| Modo | Consumo tipico | Duracao com bateria de 2000 mAh |
|---|---|---|
| sempre acordada, com WiFi | cerca de 80 mA | cerca de 25 horas |
| acordada, so em repouso | cerca de 20 mA | cerca de 100 horas |
deep sleep | cerca de 10 microamperes | alguns 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.
- 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?
- Troque
VERSAO_FIRMWAREpara "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? - 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?
- Olhe os dois blocos de simulacao de evento: 20 pulsos e 50 pulsos. Quantas vezes a
ISRrodou em cada? Oloopestava parado? Escreva em uma frase por que a contagem bateu. - 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 odeep sleepdura 30 s, quantas vezes por hora a placa acorda? - Descomente a chamada de
deep sleepno fim doloop, grave e aperte o botao. O que a placa imprime ao acordar? O que sumiu e o que ficou? Se aISRestivesse 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ério | Pontos |
|---|---|
| Particao rodando e particao de destino anotadas, e a diferença explicada | 2 pontos |
| Item 2 respondido: o que a versão de firmware prova que o tamanho da imagem não | 2 pontos |
| Execuções e tempo das duas tarefas anotados, com a explicacao da prioridade | 3 pontos |
Contagem da ISR nos dois blocos, e a razão de a contagem bater | 2 pontos |
| Corrente medida nos dois modos, com a diferença em vezes e a conta por hora | 2 pontos |
| Item 6 respondido: o que a placa imprime ao acordar e o que sobrevive | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
esp_ota_get_next_update_partition usada como particao rodando | o 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 ISR | a 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 relogio | reinicio 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 ISR | aviso 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 retorno | a 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" tudo | a 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 tarefa | a 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 loop | a 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 watchdog | a 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áveis | a 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 alto | o 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 fixa | o 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,WebSockete mensagem retida não são a mesma coisa. - Montar a assinatura de
MQTTcom 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
Serialonde 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
PubSubClientaberto 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:
| Pergunta | O 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ópico | Quem assina | Retida |
|---|---|---|
estacao/esp32-bancada-01/leitura | o servidor que grava no banco | não |
estacao/esp32-bancada-01/estado | o painel que abre a qualquer hora | sim |
estacao/esp32-bancada-01/comando | ninguém nesta aula: é o painel falando | não |
estacao/+/estado | o 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ário | Sem retida | Com retida |
|---|---|---|
| assinante chega 5 min depois | não há resposta; o painel mostra desconhecido | o broker entrega o último estado ao entrar |
| o que a placa precisaria fazer | acordar, reconectar e responder | nada |
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:
| Mensagem | Chave | O que o consumidor fez |
|---|---|---|
| 1 | id-4711 | processou |
| 2 | id-4712 | processou |
| 3 | id-4712 | duplicado, descartado |
| 4 | id-4713 | processou |
| 5 | id-4714 | processou |
| 6 | id-4715 | fila 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:
| Caso | Transporte | Por que | Conexões por dispositivo |
|---|---|---|---|
| painel mostra leitura a cada 2 s | REST (pedido e resposta) | 3 pedidos por minuto, servidor leve, cache funciona | 3 |
| painel mostra alerta no instante que acontece | WebSocket (conexão aberta) | o servidor empurra; sem polling, sem conexão ociosa | 1 |
| painel só abre de manhã e fecha à noite | mensagem retida do broker | o assinante recebe o último estado ao abrir, sem pedir | 0 |
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.
- 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
comandotem assinante nesta aula? - 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?
- 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?
- 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.
- Preencha a tabela de transporte com os três casos do sketch e multiplique por 200. Quantas conexões o
RESTabre por minuto? Quantas conexões ficam abertas comWebSocket? Em qual dos dois o servidor gasta mais memória, e por quê? - 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ério | Pontos |
|---|---|
Quatro tópicos anotados com o assinante de cada um, e o comando sem assinante na aula | 2 pontos |
| Assinatura curinga escrita, com o número de linhas do painel que não muda de 3 para 300 placas | 2 pontos |
| Tempo de vida do pacote anotado, e o motivo de leitura não ser retida | 2 pontos |
| Cena da fila contada: mensagens, processadas, duplicadas por chave e perdidas por espaço | 3 pontos |
| Tabela de transporte preenchida e multiplicada por 200, com as duas contas de conexão | 2 pontos |
| Trilha escolhida com um número, e o problema que ela transfere nomeado | 1 ponto |
Erros comuns
| Erro | Como aparece | Correcao |
|---|---|---|
| Publicar e assinar no mesmo tópico | o 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ço | o 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 leitura | o 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 setServer | o 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 é barato | o 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 banco | a 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 reconectar | o 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.
