Regra de negocio — Arduino e IoT — semana 1 do 3o trimestre

Informatica · Conteudo · publicado em 02/10/2026
Semana 1 · Regra de negocio — Material de Apoio Arduino

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

Regra de negocio

O que o sistema não pode fazer, escrito como código e não como conversa.

Aula 1 — Erro de negocio contra erro de tecnica

Objetivos

  • Classificar um erro de regra de negócio e u// Aula 1 do contra erro de tecnica.

// // A placa e o lugar onde a regra de negocio aparece primeiro: um dispositivo // so aceita leitura dentro do horario de operacao, e um sensor quebrado e um // problema diferente. O sketch separa os dois e mostra que cada um leva uma // mensagem diferente para quem le.

#include <Arduino.h>

// Pino do LED da placa. Serve de sinal de vida: aceso enquanto o programa roda. const int PIN_LED = 2;

// REGRA DE NEGOCIO: a estacao so aceita leitura entre 6h e 22h. // São duas constantes porque a regra muda; a lógica não muda junto. const int HORA_Início = 6; const int HORA_FIM = 22;

// Ritmo da simulacao. 4000 ms e so para o monitor não virar parede de texto. const unsigned long INTERVALO_Leitura_MS = 4000;

// Cada ciclo avanca o relogio simulado em 37 minutos: 24 ciclos dão as 24 horas. const int MINUTOS_POR_CICLO = 37;

// Depois de N leituras o sensor "quebra". E a falha TECNICA: nada a ver com a // regra de horario. const int CICLOS_Até_FALHAR = 6;

// Faixa que o sensor considera leitura confiavel. Fora dela, o valor e ruido. const int Leitura_MINIMA = 10; const int Leitura_MAXIMA = 90;

// Resultado da avaliacao. Os dois ultimos campos são o ponto da aula: a mesma // falha pode ter duas mensagens, porque são para destinatarios diferentes. struct Avaliacao { bool aceita; // a leitura entra no historico? const char* código; // 100, 422, 503: código que o servidor devolve const char* tipo; // "OK", "REGRA" ou "TECNICA" const char* paraUsuario; // o que o painel mostra const char* paraTecnico; // o que o log guarda };

// Relogio simulado, em minutos desde a meia-noite. Não ha RTC na bancada. int minutoDoDia = HORA_Início * 60; int ciclo = 0;

// Sensor simulado: valor que sobe e desce, mais a falha programada. int lerSensorSimulado() { int base = 40 + (ciclo % 5) * 8; if (ciclo >= CICLOS_Até_FALHAR) { // Valor impossivel: e assim que sensor quebrado aparece na pratica. return -1; } return base; }

// A REGRA. Recebe os fatos crus e devolve a decisao. Não imprime nada, não // chama Serial, não sabe que existe LED: e por isso que da para testar. Avaliacao avaliarLeitura(int hora, int leitura) { Avaliacao r;

// 1. TECNICA primeiro: sem leitura confiavel, não ha nada para decidir. if (leitura < Leitura_MINIMA || leitura > Leitura_MAXIMA) { r.aceita = false; r.código = "503"; r.tipo = "TECNICA"; r.paraUsuario = "Estacao temporariamente indisponivel."; r.paraTecnico = "leitura fora da faixa do sensor"; return r; }

// 2. REGRA DE NEGOCIO: o horario da estacao. Duas condições de borda que o // aluno erra: 6h entra, 22h não entra, e 22:30 já passou do limite. if (hora < HORA_Início || hora >= HORA_FIM) { r.aceita = false; r.código = "422"; r.tipo = "REGRA"; r.paraUsuario = "Fora do horario de operacao."; r.paraTecnico = "leitura recusada por horario"; return r; }

// 3. Ha caminho feliz. r.aceita = true; r.código = "100"; r.tipo = "OK"; r.paraUsuario = "Leitura registrada."; r.paraTecnico = "leitura aceita"; return r; }

// Impressao do resultado. Fica separada da regra de propósito: e a camada que // fala com o serial, e ela não decide nada. void mostrarAvaliacao(int hora, int leitura, const Avaliacao& r) { Serial.printf("%02d:%02d leitura %3d -> %s código %s\n", hora, minutoDoDia % 60, leitura, r.tipo, r.código); Serial.print(" usuario : "); Serial.println(r.paraUsuario); Serial.print(" tecnico : "); Serial.println(r.paraTecnico); }

void setup() { pinMode(PIN_LED, OUTPUT);

Serial.begin(115200); Serial.println(); Serial.println("Dia 01 aula 1 — erro de negocio contra erro de tecnica"); Serial.printf("Regra: so aceita leitura das %02dh as %02dh.\n", HORA_Início, HORA_FIM); Serial.printf("Falha tecnica do sensor a partir do ciclo %d.\n\n", CICLOS_Até_FALHAR); Serial.println("hora leitura resultado código"); }

void loop() { digitalWrite(PIN_LED, HIGH);

int leitura = lerSensorSimulado(); int hora = minutoDoDia / 60; Avaliacao r = avaliarLeitura(hora, leitura);

mostrarAvaliacao(hora, leitura, r); if (r.aceita) { Serial.printf(" historico: gravada a temperatura %d C\n\n", leitura); } else { Serial.println(" historico: nada gravado"); Serial.println(); }

// Um ciclo por linha de tempo simulada. 37 minutos por volta: a regra do // horario aparece varias vezes em uma tela de monitor. minutoDoDia = (minutoDoDia + MINUTOS_POR_CICLO) % (24 * 60); ciclo++;

digitalWrite(PIN_LED, LOW); delay(INTERVALO_Leitura_MS); } ncreto da bancada, e escrever a mensagem que cada um produz.

  • Escrever a validação de regra de uma estação que só aceita leitura entre 6h e 22h, e dizer qual linha do código faz cada recusa.
  • Decidir o código de resposta que a placa deve mandar ao servidor em cada caso, e justificar por que erro de negócio não é erro de técnica.
  • Separar a mensagem que vai para o painel da mensagem que vai para o log, e explicar por que as duas não podem ser a mesma frase.
  • Medir o que sai no monitor serial, comparar com o que o comentário do sketch promete, e apontar a linha onde os dois divergem.

Material

  • 1 ESP32 DevKit V1 por dupla, com o cabo USB
  • 1 LED da placa no GPIO2, com o resistor de 330 ohm da bancada
  • 1 botão da bancada, para a demonstração do recusar manual
  • 1 computador com o monitor serial aberto a 115200
  • Folha de papel por dupla, com a tabela de erro de negócio contra erro de técnica
  • Projetor, para o monitor serial da placa e o terminal do servidor lado a lado

Conceitos

Erro de negócio e erro de técnica são dois defeitos com responsáveis diferentes

O ponto de partida da aula é uma pergunta que o professor faz com frequência e que quase ninguém responde direito: o que o sistema fez de errado? A pergunta parece uma, e a resposta certa são duas, porque existem dois defeitos que não se corrigem da mesma forma.

O erro de técnica é o caminho que quebrou. A porta não abriu, o sensor devolveu um número impossível, o banco recusou a conexão, o roteador caiu. Ninguém escreveu uma regra para isso acontecer: acontece com o melhor código do mundo quando a máquina falha. A correção é técnica — trocar a peça, reconectar, aumentar o tempo limite, volver a tentar. O sinal no sistema é sempre o mesmo: uma exceção, um 500, um null onde deveria haver um número, uma exceção que ninguém previu porque ninguém previu nada.

O erro de negócio é o caminho inteiro funcionando e a resposta ainda assim ser não. A requisição chegou, o banco respondeu, o HTTP foi honesto — e o sistema diz que não pode. Isso só acontece porque alguém escreveu uma condição. A estação só opera das 6h às 22h; o limite de envio por minuto foi ultrapassado; a conta está vencida; o dispositivo já foi configurado com outro nome. A correção é de conversa antes de ser de código: alguém precisa decidir que a regra existe, e essa decisão é o requisito.

A distinção importa porque as duas coisas exigem ações opostas do professor e do aluno. Erro de técnica pede diagnóstico: qual máquina, em que linha, desde quando. Erro de negócio pede leitura de requisito: qual frase do sistema foi violada, e por que a frase existe. O primeiro se resolve no log e no multímetro. O segundo se resolve na conversa com quem pediu o sistema.

Erro de técnicaErro de negócio
Quem produza máquina falhandouma condição escrita no código
Como apareceexceção, 500, valor impossívelrecusa com resposta formatada
Quem corrigeo técnicoquem define a regra
Onde se olhao log e a máquinao requisito e a validação
Repete se não corrigirsim, a máquina volta a falharnão, a regra continua valendo
Mensagemtécnica, com detalhepara o usuário, sem detalhe interno

A última linha da tabela é a que a turma esquece, e ela é o objeto da aula 2 deste mesmo dia.

O segundo par de termos responde a pergunta seguinte, e ela é a que separa a regra da máquina: nem todo erro é defeito. Existe o erro esperado, que é o sistema recusando algo que ele mesmo definiu como inválido, e existe o erro inesperado, que é o sistema tentando fazer algo e fracassando. A diferença é se alguém pensou no caso antes.

Um erro esperado tem um caminho de saída escrito: qual resposta, qual código, qual registro, qual aviso. O aluno do 22:05 está dentro do sistema funcionando: a estação não opera de noite, o sistema diz que não opera de noite, e isso é comportamento, não defeito. O caminho de saída é limpo porque alguém escreveu o que fazer.

Um erro inesperado é o que o sistema não sabia que podia acontecer: o sensor devolveu -1, o banco fechou a conexão, a variável veio nula. Não há caminho de saída porque não havia previsão. E aqui está o ponto que o professor escreve na lousa: um erro inesperado que vira erro esperado é um ganho do projeto, e a aula de hoje é o primeiro passo dessa promoção. Daqui a duas semanas, a validação de regra vai tratar o -1 antes que ele chegue no banco, e o -1 deixa de ser inesperado.

O erro clássico de aluno é tratar os dois do mesmo jeito, com um catch genérico que devolve sempre a mesma frase. O resultado é um sistema que mente de forma consistente: devolve "erro interno" para o usuário que mandou um dado fora de faixa, e o log registra "erro interno" também, que não diz nada. A turma precisa ver esse padrão na primeira aula, porque ele reaparece em todas as aulas do trimestre.

Códigos de negócio e de erro: o número que o servidor e a placa combinam antes de existir

Uma regra de negócio é uma condição escrita no código, e o código de negócio é o identificador que o servidor devolve quando a recusa vem dela. Ele existe para que a placa, o painel e o log falem a mesma língua sem depender de texto humano. O código de erro técnico diz o que quebrou; o código de negócio diz qual regra de negócio do sistema foi violada. Os dois são números, e o que os separa é quem tem de agir depois de lê-los.

Os três que esta aula usa e que o aluno precisa saber de cor:

  • 422: o dado chegou inteiro e compreensível, e mesmo assim a regra o recusa. É a resposta da leitura fora de horário, do valor fora da faixa de confiança que o sensor não deveria ter enviado, e do limite de frequência ultrapassado. A regra do 422 é uma frase: o pedido faz sentido e não pode ser atendido agora.
  • 409: o pedido faz sentido e não pode ser atendido porque conflita com o estado atual do sistema. Duas placas gravando a mesma configuração, um dispositivo já cadastrado com aquele identificador, uma migração aplicada duas vezes. O 409 é o conflito, e ele é a resposta natural para o problema que a aula 1 do dia 2 vai resolver com máquina de estados.
  • 503: o caminho existe e o pedido é válido, mas o serviço não consegue atender agora. É o erro técnico que ainda não virou erro de negócio: sensor fora de faixa, banco indisponível, memória esgotada.

O critério de escrita é o que o professor pede em voz alta: o número vem da natureza da recusa, não da camada que encontrou o problema. Um valor fora de faixa que o sensor do banco já tinha recusado e nunca chegou ao servidor é 422 do ponto de vista da regra, mesmo que quem descobriu foi a consulta. Se o número depende de quem olhou, o sistema tem duas versões da verdade.

E há o caso que o professor chama de status HTTP errado: o caminho está de pé, a requisição foi entendida, e a resposta diz que deu certo quando nada foi gravado. Esse é o pior dos três, porque o painel mostra leitura onde não existe leitura, e a turma só descobre o problema quando a régua histórica faz sentido demais. O critério para nunca acontecer é simples e vale para a placa: não grave, não diga 200. Se a leitura não entrou, a resposta que importa é a de erro, e a placa precisa imprimi-la.

Regra implícita e regra explícita: a regra que ninguém escreveu

A regra implícita é a regra que todo mundo cumpre e ninguém escreveu. A da estação deste curso é o exemplo: a turma não sabia que a estação não mede de noite, porque "de noite o ar está parado e não muda". Essa frase nunca entrou em nenhum arquivo, e mesmo assim é a regra que mais vezes aparece na conversa do projeto.

A regra explícita é a mesma regra escrita, com a condição no código, o código de resposta e a mensagem. A validação de regra é o ponto do código onde a regra implícita vira explícita, e é uma linha — neste caso, o if que compara a hora com o início e o fim.

A distinção não é troubledora: é a diferença entre um sistema que muda de comportamento quando alguém entra na conversa e um sistema que muda de comportamento quando alguém edita um arquivo. O professor conta o custo do outro lado com um número do projeto do ano: cada regra implícita que vira explícita é uma linha a menos de adivinhação e uma linha a mais de teste, e o teste da regra é o que impede a voltada.

A mesma falha, duas mensagens, e por que a segunda não vaza detalhe

A última ideia da aula é a que aparece no sketch e que vale para todo o resto do curso. Uma falha tem dois destinatários, e para cada um a mensagem é diferente.

A mensagem para o usuário é o que o painel mostra. Ela responde o que a pessoa pode fazer, e nada além disso. "Fora do horário de operação" diz o que aconteceu e diz que há um horário. "Estação temporariamente indisponível" diz que há algo a esperar, sem dizer o quê. A regra é negativa, e o professor escreve ela inteira na lousa: não vazar detalhe interno. Isso vale para a mensagem para o usuário de qualquer tela do sistema, e vale também para a mensagem técnica quando ela sai para fora do log. Uma mensagem que vaza é pior que uma mensagem ausente, porque ela faz o usuário parar e procurar o que a mensagem disse. Detalhe interno é nome de tabela, nome de arquivo, linha de código, endereço de memória, versão de biblioteca, nome de host e valor de um campo que o usuário não enviou. Nada disso vai para o painel.

A mensagem técnica é o que o log guarda. Aqui o inverso vale: ela precisa dizer o suficiente para o diagnóstico. "leitura fora da faixa do sensor" diz o que aconteceu; o sketch completo acrescenta o valor, a hora e o ciclo, que é o que permite achar quando começou.

O erro clássico é mandar a mensagem técnica para o usuário, e ele aparece na tela do painel com o nome da tabela dentro. O segundo erro, mais sutil, é mandar a mensagem para o usuário para o log: "Estação temporariamente indisponível" no arquivo de log não diz nada, e na semana em que o sensor estiver com mau contato, o professor vai ter mil linhas idênticas e nenhuma causa.

A mensagem técnica e a mensagem para o usuário vivem no mesmo struct no sketch desta aula, e é por isso que o código funciona: a decisão de aceitar ou recusar acontece uma vez, e as duas frases saem da mesma decisão. Quando a decisão e a frase se separam, aparece o caso em que o sistema recusa e o painel acha que gravou.

Atividade

Montagem:

  • LED da placa no GPIO2, com o resistor de 330 ohm e o jumper para o GND.
  • Botão da bancada no GPIO4 com INPUT_PULLUP, para a demonstração de recusa manual.
  • Cabo USB conectado, monitor serial em 115200.
  • O sensor desta aula é simulado no sketch: nenhum DHT11 conectado, porque a aula é sobre a decisão e não sobre a leitura.
  1. Escreva na folha duas colunas com o cabeçalho "erro de negócio" e "erro de técnica". Nas duas colunas, escreva dois exemplos da sua bancada e dois exemplos do servidor do seu projeto. Embaixo de cada exemplo, escreva quem corrige.
  2. Grave o sketch da resolução e deixe rodando no monitor serial por pelo menos dois minutos. Copie no caderno as seis primeiras linhas e as três últimas, com o horário simulado de cada uma.
  3. Conte, no papel, quantas linhas com 100, quantas com 422 e quantas com 503 aparecem em dois minutos. Escreva o número de cada uma. Se uma das três for zero, escreva por quê você acha que é zero.
  4. Procure no monitor serial a linha com 422. Marque no caderno o que você encontrou. Se não encontrou, escreva em qual linha do código está a condição que deveria produzir essa resposta, e em qual linha do código está a condição que a impede de ser alcançada.
  5. Rode de novo com CICLOS_ATE_FALHAR trocado por um valor igual a 30, ou seja, uma falha que só acontece no fim. Agora o 422 aparece? Copie a primeira linha que aparece com 422, com o horário simulado dela, e explique por que ela só aparece agora.
  6. Troque a condição do sensor de ciclo >= CICLOS_ATE_FALHAR para ciclo == CICLOS_ATE_FALHAR e rode de novo. Escreva os três números de novo (100, 422, 503) e diga, em uma frase, o que a troca mudou no papel do diagnóstico.
  7. Escreva no caderno a tabela do servidor com três linhas: a linha do 422, a linha do 503 e a linha do 409. Em cada linha, escreva uma situação real do seu projeto. Nenhuma das três pode ter a mesma situação.
  8. Escreva as duas frases de uma falha só: a que vai para o painel e a que vai para o log. Depois escreva uma terceira situação em que a sua frase de painel ainda está vazando detalhe interno, e corrija a frase.

Nota: 10 pontos. Critério de fim: os três números de resposta contados no monitor serial em dois minutos e anotados, e a primeira linha com 422 localizada — ou a explicação de por que ela não existe no código que está rodando.

Resolucao

O sketch da aula está em codigo/t3/dia01/aula1.ino.

// Aula 1 do 3o trimestre: erro de negocio contra erro de tecnica.
//
// A placa e o lugar onde a regra de negocio aparece primeiro: um dispositivo
// so aceita leitura dentro do horario de operacao, e um sensor quebrado e um
// problema diferente. O sketch separa os dois e mostra que cada um leva uma
// mensagem diferente para quem le.

#include <Arduino.h>

// Pino do LED da placa. Serve de sinal de vida: aceso enquanto o programa roda.
const int PIN_LED = 2;

// REGRA DE NEGOCIO: a estacao so aceita leitura entre 6h e 22h.
// Sao duas constantes porque a regra muda; a logica nao muda junto.
const int HORA_INICIO = 6;
const int HORA_FIM = 22;

// Ritmo da simulacao. 4000 ms e so para o monitor nao virar parede de texto.
const unsigned long INTERVALO_LEITURA_MS = 4000;

// Cada ciclo avanca o relogio simulado em 37 minutos: 24 ciclos dão as 24 horas.
const int MINUTOS_POR_CICLO = 37;

// Depois de N leituras o sensor "quebra". E a falha TECNICA: nada a ver com a
// regra de horario.
const int CICLOS_ATE_FALHAR = 6;

// Faixa que o sensor considera leitura confiavel. Fora dela, o valor e ruido.
const int LEITURA_MINIMA = 10;
const int LEITURA_MAXIMA = 90;

// Resultado da avaliacao. Os dois ultimos campos sao o ponto da aula: a mesma
// falha pode ter duas mensagens, porque sao para destinatarios diferentes.
struct Avaliacao {
  bool aceita;            // a leitura entra no historico?
  const char* codigo;     // 100, 422, 503: codigo que o servidor devolve
  const char* tipo;       // "OK", "REGRA" ou "TECNICA"
  const char* paraUsuario;   // o que o painel mostra
  const char* paraTecnico;   // o que o log guarda
};

// Relogio simulado, em minutos desde a meia-noite. Nao ha RTC na bancada.
int minutoDoDia = HORA_INICIO * 60;
int ciclo = 0;

// Sensor simulado: valor que sobe e desce, mais a falha programada.
int lerSensorSimulado() {
  int base = 40 + (ciclo % 5) * 8;
  if (ciclo >= CICLOS_ATE_FALHAR) {
    // Valor impossivel: e assim que sensor quebrado aparece na pratica.
    return -1;
  }
  return base;
}

// A REGRA. Recebe os fatos crus e devolve a decisao. Nao imprime nada, nao
// chama Serial, nao sabe que existe LED: e por isso que da para testar.
Avaliacao avaliarLeitura(int hora, int leitura) {
  Avaliacao r;

  // 1. TECNICA primeiro: sem leitura confiavel, nao ha nada para decidir.
  if (leitura < LEITURA_MINIMA || leitura > LEITURA_MAXIMA) {
    r.aceita = false;
    r.codigo = "503";
    r.tipo = "TECNICA";
    r.paraUsuario = "Estacao temporariamente indisponivel.";
    r.paraTecnico = "leitura fora da faixa do sensor";
    return r;
  }

  // 2. REGRA DE NEGOCIO: o horario da estacao. Duas condicoes de borda que o
  //    aluno erra: 6h entra, 22h nao entra, e 22:30 ja passou do limite.
  if (hora < HORA_INICIO || hora >= HORA_FIM) {
    r.aceita = false;
    r.codigo = "422";
    r.tipo = "REGRA";
    r.paraUsuario = "Fora do horario de operacao.";
    r.paraTecnico = "leitura recusada por horario";
    return r;
  }

  // 3. Ha caminho feliz.
  r.aceita = true;
  r.codigo = "100";
  r.tipo = "OK";
  r.paraUsuario = "Leitura registrada.";
  r.paraTecnico = "leitura aceita";
  return r;
}

// Impressao do resultado. Fica separada da regra de proposito: e a camada que
// fala com o serial, e ela nao decide nada.
void mostrarAvaliacao(int hora, int leitura, const Avaliacao& r) {
  Serial.printf("%02d:%02d  leitura %3d  ->  %s  codigo %s\n",
                hora, minutoDoDia % 60, leitura, r.tipo, r.codigo);
  Serial.print("    usuario : ");
  Serial.println(r.paraUsuario);
  Serial.print("    tecnico : ");
  Serial.println(r.paraTecnico);
}

void setup() {
  pinMode(PIN_LED, OUTPUT);

  Serial.begin(115200);
  Serial.println();
  Serial.println("Dia 01 aula 1 — erro de negocio contra erro de tecnica");
  Serial.printf("Regra: so aceita leitura das %02dh as %02dh.\n",
                HORA_INICIO, HORA_FIM);
  Serial.printf("Falha tecnica do sensor a partir do ciclo %d.\n\n",
                CICLOS_ATE_FALHAR);
  Serial.println("hora   leitura  resultado  codigo");
}

void loop() {
  digitalWrite(PIN_LED, HIGH);

  int leitura = lerSensorSimulado();
  int hora = minutoDoDia / 60;
  Avaliacao r = avaliarLeitura(hora, leitura);

  mostrarAvaliacao(hora, leitura, r);
  if (r.aceita) {
    Serial.printf("    historico: gravada a temperatura %d C\n\n", leitura);
  } else {
    Serial.println("    historico: nada gravado");
    Serial.println();
  }

  // Um ciclo por linha de tempo simulada. 37 minutos por volta: a regra do
  // horario aparece varias vezes em uma tela de monitor.
  minutoDoDia = (minutoDoDia + MINUTOS_POR_CICLO) % (24 * 60);
  ciclo++;

  digitalWrite(PIN_LED, LOW);
  delay(INTERVALO_LEITURA_MS);
}

Por que assim e não de outro jeito. A regra mora inteira dentro de avaliarLeitura, e a função não imprime nada, não chama o monitor e não conhece o LED. Ela recebe dois inteiros e devolve um struct Avaliacao com cinco campos. Essa separação é o que vai permitir testar a regra no dia 5 sem placa nenhuma, e é por isso que o professor insiste nela desde a primeira semana do trimestre: uma regra que escreve no monitor só pode ser testada com monitor, com placa e com a bancada ocupada.

A ordem dos dois if é a parte que o professor pede para ler em voz alta, e ela não é estética. O técnico vem primeiro porque não existe decisão de negócio sobre um dado que não foi lido. Se o 422 viesse antes, uma leitura de -1 às 21h seria recusada por horário, e o log diria que a estação estava fechada quando na verdade o sensor morreu. A ordem certa é: primeiro o fato, depois a regra.

O struct Avaliacao tem dois pares de mensagens, e não é excesso. aceita e codigo respondem para a máquina, que decide se grava. paraUsuario e paraTecnico respondem para gente, e cada uma tem o destinatário certo. O campo tipo com os valores "OK", "REGRA" e "TECNICA" é o que permite ao professor filtrar o monitor com uma busca só, e é o ancestral do campo de nível de severidade que a aula 1 do dia 8 vai pedir.

O desvio entre o comentário e o que sai no monitor, medido nesta placa. O sketch que acompanha esta aula tem uma divergência entre o que o comentário promete e o que o professor vê, e essa divergência é a melhor parte da aula. O comentário do sensor promete a falha técnica "a partir do ciclo 6", e ela acontece mesmo: às 09:42 do dia simulado, no sétimo ciclo. O comentário da regra promete também as duas condições de borda do horário — "6h entra, 22h não entra" — e o monitor não mostra uma única linha de 422. Medido com o código como está: as seis primeiras leituras saem com 100, e do sétimo ciclo em diante todas as outras saem com 503, porque o lerSensorSimulado devolve -1 para todo ciclo a partir do sexto e o primeiro horário proibido do dia simulado só chega no ciclo 27, às 22:02, vinte minutos depois de o sensor ter quebrado. Em dois minutos de monitor são 31 ciclos: 6 com 100 e 25 com 503, e nenhuma com 422.

A causa não é a regra, que está escrita e está correta. A causa é a ordem natural dos dois eventos na simulação: a falha técnica vem antes do horário proibido, e como a falha técnica não se conserta, ela come o resto do dia simulado. Na sua bancada isso tem nome, e o nome é falha em cascata: um defeito no começo do caminho esconde todos os defeitos do meio. Por isso o item 4 da atividade existe — o aluno precisa achar a linha do if do horário e apontar a linha que a impede de ser alcançada, e não concluir que a regra não foi implementada.

A correção cabe em um caractere e o professor faz na frente da turma: trocar ciclo >= CICLOS_ATE_FALHAR por ciclo == CICLOS_ATE_FALHAR. Com a falha técnica durando um ciclo só, o mesmo sketch passa a mostrar as três famílias de resposta: 25 leituras com 100, uma com 503 às 09:42, e seis com 422 — a primeira às 22:02, e mais uma às 00:30 do dia seguinte, quando o relógio simulado dá a volta e a hora 0 continua fora da janela. Essa última linha é a surpresa da aula, e ela é o argumento mais forte que existe contra a frase "não posso ligar agora" como sinônimo de "é de noite": a regra não diz que é noite, a regra diz que está entre 6 e 22, e 00:30 está fora.

O Serial.printf do mostrarAvaliacao imprime o horário com dois dígitos e a leitura com três, alinhando as colunas na tela do monitor. O alinhamento não é estética: com cinco leituras de larguras diferentes, o professor não consegue ler a tabela de relance e perde tempo conferindo linha por linha o que era para ver de imediato.

O digitalWrite do LED dentro do loop acende e apaga a cada ciclo, e isso faz da luz um indicador de que o programa está rodando. Se a luz parar de piscar e a última linha do monitor for a de 09:05, a placa travou naquele ponto — e essa é a informação que o monitor serial entrega e que o olho não entrega sozinho.

Criterios de correcao

CritérioPontos
Tabela de duas colunas preenchida com os quatro exemplos e o responsável por cada um2 pontos
Seis primeiras e três últimas linhas copiadas no caderno, com o horário simulado1 ponto
Os três números de resposta contados em dois minutos de monitor2 pontos
A linha do 422 localizada, ou a explicação com as duas linhas de código que a impedem2 pontos
Item 5: o 422 aparece com CICLOS_ATE_FALHAR igual a 30, com o horário anotado1 ponto
Item 6: os três números de novo depois da troca para ==, com o efeito no diagnóstico1 ponto
Tabela do servidor com 422, 503 e 409 e três situações diferentes1 ponto

Erros comuns

ErroComo apareceCorreção
Confundir os dois e devolver 500 para tudoO painel mostra "erro interno" quando a estação estava fechada"O 500 é do caminho quebrado. Recusa de regra é 422, e o número vem da natureza da recusa, não da camada que encontrou o problema."
Botar a regra depois da checagem técnica invertidaif (hora < 6) ... else if (leitura < 0) ..."Leia o fato antes de decidir. Não existe decisão de negócio sobre um dado que não foi lido: às 21h, um -1 é sensor morto, não estação fechada."
Tratar -1 como zeroO sketch grava -1 no histórico e o gráfico desce para menos um grau"Fora da faixa de confiança não é zero, é ausência. Zero é uma temperatura medida; -1 é um sensor que não respondeu, e são coisas que precisam de caminhos diferentes."
Usar > no lugar de >= no fim do horárioÀs 22:00 exata a leitura passa, e o aluno não entende por que 22:01 passou"A regra é 'das 6h às 22h'. Escreva o fim como limite, e 22h é o primeiro minuto que já passou do horário. Escreva os dois limites e teste os dois."
&& no lugar do OU na condição do horárioSó recusa quando a hora é ao mesmo tempo cedo e tarde, ou seja, nunca"Fora do horário é qualquer uma das duas condições, não as duas. Escreva a frase antes do operador: 'é antes das 6 ou depois das 22'."
Mensagem técnica na tela do painelO painel mostra "leitura fora da faixa do sensor""Duas mensagens, dois destinatários. No painel entra o que a pessoa pode fazer; o detalhe fica no log. Se a frase do log vaza para a tela, ela não serve para nenhum dos dois."
Devolver 200 quando não gravouO painel mostra leitura e a régua histórica não cresce"O status HTTP errado é o pior dos três, porque o painel passa a mentir comootro dia bom. Se não gravou, a resposta é de erro — e a placa precisa imprimir que erro foi."
Regra implícita escrita só na conversa"De noite a estação não mede mesmo, sempre foi assim""Regra que ninguém escreveu é regra que ninguém testa. Se a frase 'não posso ligar agora' vale, ela vira uma condição no código, com o horário como constante e o caso de teste da meia-noite."
Achar que 422 significa "dado inválido de formato"O aluno usa 422 quando o JSON não parseia"422 é o pedido compreendido e recusado pela regra. Dado que nem chegou a ser lido é 400. Separar os dois é o que permite saber se o problema é do cliente ou da regra."

Desafio extra

A régua do horário desta aula só tem dois limites: 6 e 22. Escreva a versão com três limites, no formato de um horário que abre e outro que fecha, e com uma lista de exceções: o intervalo de almoço, quando a estação não mede, e um dia da semana em que o horário é maior. Meça quantos pontos o monitor mostra em dois minutos com os três limites, e escreva uma frase por intervalo dizendo qual linha do seu código decide aquele caso. Depois troque o 422 por um código novo — 419, por exemplo — e escreva no papel o que quebra do outro lado quando um cliente antigo não conhece o número. A pergunta que o professor espera de resposta: quem decide o número do código, e o que acontece com as placas que já estão na rua quando esse número muda.

>

A resolucao, compilada

// Aula 1 do 3o trimestre: erro de negocio contra erro de tecnica.
//
// A placa e o lugar onde a regra de negocio aparece primeiro: um dispositivo
// so aceita leitura dentro do horario de operacao, e um sensor quebrado e um
// problema diferente. O sketch separa os dois e mostra que cada um leva uma
// mensagem diferente para quem le.

#include <Arduino.h>

// Pino do LED da placa. Serve de sinal de vida: aceso enquanto o programa roda.
const int PIN_LED = 2;

// REGRA DE NEGOCIO: a estacao so aceita leitura entre 6h e 22h.
// Sao duas constantes porque a regra muda; a logica nao muda junto.
const int HORA_INICIO = 6;
const int HORA_FIM = 22;

// Ritmo da simulacao. 4000 ms e so para o monitor nao virar parede de texto.
const unsigned long INTERVALO_LEITURA_MS = 4000;

// Cada ciclo avanca o relogio simulado em 37 minutos: 24 ciclos dão as 24 horas.
const int MINUTOS_POR_CICLO = 37;

// Depois de N leituras o sensor "quebra". E a falha TECNICA: nada a ver com a
// regra de horario.
const int CICLOS_ATE_FALHAR = 6;

// Faixa que o sensor considera leitura confiavel. Fora dela, o valor e ruido.
const int LEITURA_MINIMA = 10;
const int LEITURA_MAXIMA = 90;

// Resultado da avaliacao. Os dois ultimos campos sao o ponto da aula: a mesma
// falha pode ter duas mensagens, porque sao para destinatarios diferentes.
struct Avaliacao {
  bool aceita;            // a leitura entra no historico?
  const char* codigo;     // 100, 422, 503: codigo que o servidor devolve
  const char* tipo;       // "OK", "REGRA" ou "TECNICA"
  const char* paraUsuario;   // o que o painel mostra
  const char* paraTecnico;   // o que o log guarda
};

// Relogio simulado, em minutos desde a meia-noite. Nao ha RTC na bancada.
int minutoDoDia = HORA_INICIO * 60;
int ciclo = 0;

// Sensor simulado: valor que sobe e desce, mais a falha programada.
int lerSensorSimulado() {
  int base = 40 + (ciclo % 5) * 8;
  if (ciclo >= CICLOS_ATE_FALHAR) {
    // Valor impossivel: e assim que sensor quebrado aparece na pratica.
    return -1;
  }
  return base;
}

// A REGRA. Recebe os fatos crus e devolve a decisao. Nao imprime nada, nao
// chama Serial, nao sabe que existe LED: e por isso que da para testar.
Avaliacao avaliarLeitura(int hora, int leitura) {
  Avaliacao r;

  // 1. TECNICA primeiro: sem leitura confiavel, nao ha nada para decidir.
  if (leitura < LEITURA_MINIMA || leitura > LEITURA_MAXIMA) {
    r.aceita = false;
    r.codigo = "503";
    r.tipo = "TECNICA";
    r.paraUsuario = "Estacao temporariamente indisponivel.";
    r.paraTecnico = "leitura fora da faixa do sensor";
    return r;
  }

  // 2. REGRA DE NEGOCIO: o horario da estacao. Duas condicoes de borda que o
  //    aluno erra: 6h entra, 22h nao entra, e 22:30 ja passou do limite.
  if (hora < HORA_INICIO || hora >= HORA_FIM) {
    r.aceita = false;
    r.codigo = "422";
    r.tipo = "REGRA";
    r.paraUsuario = "Fora do horario de operacao.";
    r.paraTecnico = "leitura recusada por horario";
    return r;
  }

  // 3. Ha caminho feliz.
  r.aceita = true;
  r.codigo = "100";
  r.tipo = "OK";
  r.paraUsuario = "Leitura registrada.";
  r.paraTecnico = "leitura aceita";
  return r;
}

// Impressao do resultado. Fica separada da regra de proposito: e a camada que
// fala com o serial, e ela nao decide nada.
void mostrarAvaliacao(int hora, int leitura, const Avaliacao& r) {
  Serial.printf("%02d:%02d  leitura %3d  ->  %s  codigo %s\n",
                hora, minutoDoDia % 60, leitura, r.tipo, r.codigo);
  Serial.print("    usuario : ");
  Serial.println(r.paraUsuario);
  Serial.print("    tecnico : ");
  Serial.println(r.paraTecnico);
}

void setup() {
  pinMode(PIN_LED, OUTPUT);

  Serial.begin(115200);
  Serial.println();
  Serial.println("Dia 01 aula 1 — erro de negocio contra erro de tecnica");
  Serial.printf("Regra: so aceita leitura das %02dh as %02dh.\n",
                HORA_INICIO, HORA_FIM);
  Serial.printf("Falha tecnica do sensor a partir do ciclo %d.\n\n",
                CICLOS_ATE_FALHAR);
  Serial.println("hora   leitura  resultado  codigo");
}

void loop() {
  digitalWrite(PIN_LED, HIGH);

  int leitura = lerSensorSimulado();
  int hora = minutoDoDia / 60;
  Avaliacao r = avaliarLeitura(hora, leitura);

  mostrarAvaliacao(hora, leitura, r);
  if (r.aceita) {
    Serial.printf("    historico: gravada a temperatura %d C\n\n", leitura);
  } else {
    Serial.println("    historico: nada gravado");
    Serial.println();
  }

  // Um ciclo por linha de tempo simulada. 37 minutos por volta: a regra do
  // horario aparece varias vezes em uma tela de monitor.
  minutoDoDia = (minutoDoDia + MINUTOS_POR_CICLO) % (24 * 60);
  ciclo++;

  digitalWrite(PIN_LED, LOW);
  delay(INTERVALO_LEITURA_MS);
}

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

Aula 2 — Maquina de estados: o sistema que so aceita transicoes validas

Objetivos

  • Escrever a tabela de transições de um dispositiritos, e rodar essa tabela na placa.
  • Distinguir estado de variável booleana, e mostrar na tela um estado que não deveria existir: desligado e ligando ao mesmo tempo.
  • Registrar o estado atual como uma variável só, e explicar por que isso é o que garante a invariante.
  • Prever, antes de gravar, quais dos nove eventos do roteiro vão ser recusados, e conferir a previsão com o monitor serial.
  • Ler a saída do sketch desta aula e apontar a linha onde ela diverge do que o comentário do próprio código promete.

Material

  • 1 ESP32 DevKit V1 por dupla, com o cabo USB
  • 1 LED da placa no GPIO2, com o resistor de 330 ohm da bancada
  • 1 botao da bancada no GPIO4, para o aluno tentar forçar um evento na mao
  • Folha de papel por dupla, com a tabela de transições desenhada antes de gravar
  • 1 computador com o monitor serial aberto a 115200
  • Projetor, para a tabela de transições impressa pelo sketch ao lado do diagrama da lousa

Conceitos

Máquina de estados: o sistema que só aceita transições válidas

Uma máquina de estados é um modelo que descreve um sistema por onde ele está e por onde ele pode passar. E o nome técnico, mas a ideia é da vida real: uma porta tem quatro estados — fechada destrancada, fechada trancada, aberta destrancada, aberta trancada — e não é qualquer combinação dos quatro que existe. "Aberta e trancada" é estado que a porta não tem. O modelo inteiro está nas posições possíveis e nos caminhos entre elas.

O nome completo é máquina de estados finita, ou FSM na sigla que aparece em quase toda documentação de firmware, e cada palavra significa alguma coisa. É finita porque o número de estados é pequeno e conhecido de antemão: cinco aqui. Esse é o motivo de a técnica valer em firmware: o professor consegue imprimir todos os estados possíveis e mostrar que o sistema nunca saiu deles, e o que não está na lista simplesmente não acontece. Um dispositivo IoT tem essa propriedade por natureza — ele está ligado ou desligado, conectando ou conectado, enviando ou esperando — e a aula é sobre escrever isso antes de-programar.

O estado é a posição atual, e o estado atual é a variável que guarda essa posição — a regra de ouro da aula é que ela é uma só. O estado inicial é onde a máquina começa, e aqui é DESLIGADO, porque uma placa que acabou de ligar não deveria se anunciar como operando antes de fazer nada. O estado final é o estado em que não há mais saída, e a máquina deste dia não tem nenhum: de qualquer estado é sempre possível chegar em ERRO, e de ERRO é sempre possível sair. Máquina com estado final é a que representa um processo que acaba, e o exemplo do curso é o trabalho demorado do dia 7, que começa, processa e termina.

O evento é o que chega de fora e pode mudar a posição: o botão apertado, o WiFi confirmando, o sensor fora de faixa. A transição é o par estado mais evento, e uma transição válida é a que está escrita na tabela. O diagrama de estados é o desenho dessas setas: estado, evento e destino. Tudo o que não está na tabela é uma transição impossível, e a máquina precisa recusar com mensagem em vez de fazer qualquer outra coisa.

A escolha mais óbvia, e que parece mais simples, é substituir o enum por cinco variáveis: desligado, ligando, ligado, desligando, erro, todas com true ou false. Compila, ocupa menos ou igual, e parece mais simples. E está errado, e o motivo é uma propriedade que os booleanos não conseguem ter.

A propriedade chama-se invariante: uma afirmação que é verdade em todo momento da execução. A invariante desta máquina é "o dispositivo está em exatamente um dos cinco estados". Com um enum, ela é garantida pela própria forma: o valor é um só. Com cinco booleanos, ela é possível de violar — e o professor passa dez minutos construindo a violação na lousa, porque essa é a melhor demonstração da aula:

desligado = false
ligando   = true      <- ligando, sem nunca ter ligado
ligado    = true      <- ligado e ligando ao mesmo tempo

Nada impede esse código. O compilador aceita. E o efeito de um estado impossível não é um estado impossível: é um sistema que age como se estivesse nos dois lugares ao mesmo tempo. Se ligando acende o LED de advertência e ligado acende o de operação, o painel mostra dois estados contraditórios, e ninguém sabe qual dos dois decide o próximo comando.

A segunda metade do problema é mais concreta: com cinco booleanos, cada decisão vira uma combinação. "Se ligando for verdadeiro, já pode" vira "se ligando ou (ligado e desligando) for verdadeiro", e a combinação cresce com o número de estados. Com um enum e uma tabela, a decisão é uma busca na tabela, e a busca é a mesma para os cinco estados. É por isso que a máquina de estados é uma técnica, e não um estilo de escrever.

A tabela de transições, o que o código recusa e por que booleanos não bastam

A tabela de transições é o lugar único onde a regra está escrita. Cada linha tem um estado de origem, um evento e um estado de destino, e nada mais. Se uma regra precisa estar em dois lugares, ela não está em um lugar só, e é por isso que a tabela existe: ela é o contrato entre o evento que chega e o estado que muda.

O que o sketch desta aula mostra, e que o professor comenta com a turma, é o comportamento da recusa. Quando o evento chega e não há linha na tabela com aquele estado e aquele evento juntos, a máquina não faz nada e imprime a recusa com o nome do estado e o nome do evento. Isso é o que permite a um sistema dizer "não" de um jeito padronizado em vez de adivinhar. As duas frases que o professor destaca na tela são a segunda linha da recusa e a linha do estado: a primeira diz que a transição é impossível, a segunda diz onde a máquina ficou. Uma recusa sem o estado depois dela é inútil, porque o diagnóstico precisa dos dois.

O conceito de não misturar estado vem daqui e vale para o resto do curso: a tabela não sabe de sensor, não sabe de HTTP e não sabe de banco. Ela recebe o evento e devolve o próximo estado. Se a tabela precisar saber se o servidor respondeu para decidir se o dispositivo está ligado, o modelo já quebrou — e o sintoma aparece como um estado que muda por motivo errado.

A tabela tem sete linhas nesta aula, e sete é o número certo para o que o dispositivo faz. Uma linha a mais e o dispositivo pode pular de DESLIGADO para LIGADO sem passar por LIGANDO, e aí a luz de advertência que existe para avisar que algo está acontecendo nunca acende. Uma linha a menos e a falha que acontece na partida deixa o dispositivo preso sem saída. O professor pede que a turma justifique cada linha das sete em uma frase, e a pergunta que o professor faz ao lado é: "o que quebra se eu apagar esta linha?".

Desvio medido entre o comentário e a saída do sketch

Esta parte é o centro da aula, e ela existe porque o sketch que acompanha a aula tem um defeito de verdade. O professor mediu rodando o código, e o que sai no monitor serial não é o que os comentários prometem.

Os comentários de cada linha da tabela prometem o destino de cada transição: DESLIGADO mais ligar vai para ligando, LIGANDO mais ligado confirmado vai para ligado, e assim por diante. Essa é a intenção, e ela está escrita com todas as letras no arquivo. O problema é que a estrutura TRANSICAO guarda duas colunas, estado de origem e evento, e o código de aplicação lê TRANSICAO[indice][1] como se fosse o destino. Como a coluna 1 é o evento, o destino que a máquina realmente assume é o próprio número do evento.

O efeito é visível e mensurável. A tabela que o setup imprime, com o destino saindo da coluna errada, mostra estas sete linhas:

DESLIGADO    --ligar               --> DESLIGADO
LIGANDO      --ligado confirmado   --> LIGANDO
LIGADO       --desligar            --> LIGADO
DESLIGANDO   --desligado confirmado--> DESLIGANDO
LIGADO       --falha               --> ERRO
ERRO         --desligar            --> LIGADO
DESLIGADO    --falha               --> ERRO

Quatro das sete linhas impressas dizem que o destino é igual ao estado de origem. E no roteiro de nove eventos, o efeito é que cinco dos nove passos são recusados e só três passam, quando a intenção era o contrário: dois recusados e sete aceitos. A sequência real medida é esta: o passo 1 manda ligar em DESLIGADO e a máquina "vai" para DESLIGADO; o passo 2, ligar de novo, é aceito em vez de recusado; o passo 3, ligado confirmado, é recusado, porque a máquina nunca saiu de DESLIGADO e aquela transição exige estar em LIGANDO; o passo 4, desligar, também é recusado pelo mesmo motivo; o passo 7, falha, leva a DESLIGADO para ERRO — a única transição de falha que funciona por acaso, porque o evento falha tem o mesmo número do destino ERRO; e o passo 9, desligar em ERRO, leva a máquina para LIGADO.

Vale a pena que a turma veja o detalhe do passo 7, porque ele é a assinatura exata do defeito: EV_FALHA vale 4 e ERRO vale 4. A transição funciona não porque a tabela está certa, e porque o número do evento coincide com o número do estado de destino. Um defeito que funciona por coincidência de numeração é o tipo de defeito que sobrevive a anos de produção, e o professor usa essa frase na lousa.

A correção cabe em uma linha e o professor a faz na frente da turma: a tabela passa a ter três colunas, Estado de origem, Evento e Estado de destino, e o código passa a ler TRANSICAO[indice][2]. Feito isso, os sete comentários batem com as sete linhas impressas, e o roteiro passa a recusar exatamente os dois eventos que ele deveria recusar — o segundo ligar e o ligado confirmado fora de ordem. O item 5 da atividade existe para o aluno prever esse número antes de gravar, e é a previsão que diz se ele entendeu a tabela ou só decorou a saída.

Estado como variável, e o LED como espelho

A última parte da aula é o que a variável de estado compra na prática: uma linha que mostra o estado inteiro do dispositivo em qualquer instante, e uma luz que o professor pode ler de longe.

O LED do sketch é aceso quando o estado é ligado e apagado em todos os outros quatro casos. Isso é uma consequência direta de usar uma variável só: a condição da luz é uma comparação, e não uma conjunção de cinco flags. Se a luz estiver acesa, o professor sabe, sem ler o monitor, que o dispositivo está operando; se estiver apagada, ele sabe que há quatro possibilidades e precisa do monitor para distinguir quais — e é essa necessidade que a máquina resolve, porque o nome do estado está impresso.

A mesma linha de luz é o instrumento de diagnóstico da placa que travou. Se a luz parou de piscar entre dois passos do roteiro, o estado travou e o monitor mostra qual linha ficou por último. A regra da aula inteira cabe nessa frase: o estado é a variável que responde "em que ponto estou", e tudo o mais que a placa faz é função dele.

Atividade

Montagem:

  • LED da placa no GPIO2, com o resistor de 330 ohm e o jumper para o GND.
  • Botao da bancada no GPIO4 com INPUT_PULLUP, para a demonstração de forçar um evento.
  • Cabo USB conectado, monitor serial em 115200.
  • Nenhum sensor nesta aula: a máquina de estados se prova sozinha, sem a incerteza do ambiente.
  1. Desenhe no papel a máquina de estados do dispositivo: os cinco estados em círculo e as setas de transição. Escreva ao lado de cada seta o evento que a causa. Não olhe o sketch antes de terminar o desenho.
  2. Preencha no papel a tabela de transições com duas colunas, evento e estado de destino. Conte quantas linhas você escreveu e anote o número. Esse é o seu palpite; o item 5 confere.
  3. Grave o sketch da resolução e rode no monitor serial por pelo menos quarenta segundos, o que dá para os nove passos e mais um reinício do roteiro. Copie no caderno as nove linhas que dizem o nome do evento, o nome do estado onde ele chegou e a palavra que veio depois da seta.
  4. Conte e anote: quantos dos nove passos foram aceitos e quantos foram recusados. Escreva o número dos dois. Agora compare com o seu palpite do item 2 e diga em uma frase a diferença.
  5. Copie no caderno a tabela de sete linhas que o sketch imprime no começo, com os três campos de cada linha. Depois escreva ao lado de cada linha o que o comentário da tabela no código promete. Quantas linhas divergem? Marque as quatro que o professor apontou na aula.
  6. Sem mudar o hardware, mude a struct da tabela para três colunas, com Estado de origem, Evento e Estado de destino, e mude a leitura para a terceira coluna. Grave e rode de novo. Escreva os dois números de novo: aceitos e recusados. Quantos comentários agora batem com a linha impressa?
  7. Escreva em uma frase o invariante desta máquina, e escreva ao lado qual linha do código garante que ele vale. Depois escreva o que aconteceria se o mesmo enum fosse trocado por cinco variáveis booleanas, e qual das duas frases ficaria falsa.
  8. No papel, escreva a resposta para a pergunta que o professor faz no fim da aula: quantos eventos existem, quantas transições estão escritas, e o que o dispositivo faz quando chega um evento que não está em nenhuma delas.

Nota: 10 pontos. Critério de fim: os nove passos do roteiro copiados com o estado de chegada e o resultado, e os dois números de aceitos e recusados anotados — o do código como está e o do código corrigido.

Resolucao

O sketch da aula está em codigo/t3/dia01/aula2.ino.

// Aula 2 do 3o trimestre: maquina de estados do dispositivo.
//
// O dispositivo nao tem um monte de booleanos: ele tem UM estado, e so sai
// dele por transicoes escritas antes. Qualquer coisa fora da tabela e recusada
// com mensagem, e e essa recusa que a aula quer mostrar.

#include <Arduino.h>

// Pino do LED da placa.
const int PIN_LED = 2;

// Intervalo entre um passo e o outro da simulacao.
const unsigned long PASSO_MS = 2500;

// Os estados do dispositivo. Um enum, nao cinco booleanos: com cinco
// booleanos da para estar desligado e ligando ao mesmo tempo, que e estado
// que nao existe.
enum Estado {
  DESLIGADO,
  LIGANDO,
  LIGADO,
  DESLIGANDO,
  ERRO
};

// Os eventos que chega de fora: botao, WiFi, sensor.
enum Evento {
  EV_LIGAR,
  EV_LIGADO_CONFIRMADO,
  EV_DESLIGAR,
  EV_DESLIGADO_CONFIRMADO,
  EV_FALHA
};

// Estado atual. Uma variavel so: e isso que garante o invariante.
Estado estadoAtual = DESLIGADO;

// Nome do estado, para o monitor. Funcao em vez de array: o compilador
// complains se o enum ganhar um valor e o array ficar curto.
const char* nomeEstado(Estado e) {
  switch (e) {
    case DESLIGADO:   return "DESLIGADO";
    case LIGANDO:     return "LIGANDO";
    case LIGADO:      return "LIGADO";
    case DESLIGANDO:  return "DESLIGANDO";
    case ERRO:        return "ERRO";
  }
  return "DESCONHECIDO";
}

const char* nomeEvento(Evento ev) {
  switch (ev) {
    case EV_LIGAR:                return "ligar";
    case EV_LIGADO_CONFIRMADO:    return "ligado confirmado";
    case EV_DESLIGAR:             return "desligar";
    case EV_DESLIGADO_CONFIRMADO: return "desligado confirmado";
    case EV_FALHA:                return "falha";
  }
  return "evento desconhecido";
}

// A TABELA DE TRANSICOES. A regra esta escrita aqui, e so aqui: dois estados
// em cada linha, e a chave e o evento.
//
// As colunas sao `int`, e nao `Estado`, de proposito: e a tabela COMO ESTA que
// a aula mede. Com `Estado` nas duas colunas o proprio codigo nem compila, e o
// aluno nunca chega na parte que importa, que e o defeito que PASSA no teste.
// Lendo o destino da segunda coluna — que e o evento — a tabela fica correta
// por coincidencia de numeracao em duas linhas e errada nas outras. Ver o
// item 6 da atividade: a correcao e dar uma terceira coluna a tabela.
const int TRANSICAO[][2] = {
  {DESLIGADO,  EV_LIGAR},              // desligado -> ligando
  {LIGANDO,    EV_LIGADO_CONFIRMADO},  // ligando   -> ligado
  {LIGADO,     EV_DESLIGAR},           // ligado    -> desligando
  {DESLIGANDO, EV_DESLIGADO_CONFIRMADO},// desligando-> desligado
  {LIGADO,     EV_FALHA},              // ligado    -> erro
  {ERRO,       EV_DESLIGAR},           // erro      -> desligando
  {DESLIGADO,  EV_FALHA}               // desligado -> erro (falha na partida)
};

const int TOTAL_TRANSICOES = 7;

// Procura a transicao. Devolve -1 quando o evento nao existe na linha atual:
// e isso, e nao um if solto, que recusa a transicao impossivel.
int procurarTransicao(Estado de, Evento ev) {
  for (int i = 0; i < TOTAL_TRANSICOES; i++) {
    if (TRANSICAO[i][0] == de && TRANSICAO[i][1] == ev) {
      return i;
    }
  }
  return -1;
}

// Aplica o evento. Devolve true se aceitou, false se recusou.
bool aplicarEvento(Evento ev) {
  int indice = procurarTransicao(estadoAtual, ev);

  Serial.print("  evento \"");
  Serial.print(nomeEvento(ev));
  Serial.print("\" no estado ");
  Serial.print(nomeEstado(estadoAtual));

  if (indice < 0) {
    Serial.println("  ->  RECUSADO");
    Serial.println("      transicao impossivel: este evento nao existe na tabela");
    return false;
  }

  Estado destino = static_cast<Estado>(TRANSICAO[indice][1]);   // colunas 0 = chave, 1 = destino lido da coluna do evento
  Serial.print("  ->  ");
  Serial.println(nomeEstado(destino));
  estadoAtual = destino;
  return true;
}

// Sequencia que a placa executa, incluindo os dois eventos proibidos.
const Evento ROTEIRO[] = {
  EV_LIGAR,                // ok
  EV_LIGAR,                // RECUSADO: ja esta ligando
  EV_LIGADO_CONFIRMADO,    // ok
  EV_DESLIGAR,            // ok
  EV_LIGADO_CONFIRMADO,    // RECUSADO: nao esta mais ligado
  EV_DESLIGADO_CONFIRMADO, // ok
  EV_FALHA,                // ok: desligado -> erro
  EV_LIGADO_CONFIRMADO,    // RECUSADO: em erro so sai por desligar
  EV_DESLIGAR              // ok: erro -> desligando
};

const int TOTAL_PASSOS = 9;
int passo = 0;

void setup() {
  pinMode(PIN_LED, OUTPUT);

  Serial.begin(115200);
  Serial.println();
  Serial.println("Dia 01 aula 2 — maquina de estados do dispositivo");
  Serial.printf("Transicoes validas na tabela: %d\n", TOTAL_TRANSICOES);
  Serial.println("Qualquer evento fora da tabela e recusado.");
  Serial.println();

  Serial.println("Tabela de transicoes:");
  for (int i = 0; i < TOTAL_TRANSICOES; i++) {
    Serial.printf("  %-12s --%-20s--> %s\n",
                  nomeEstado(static_cast<Estado>(TRANSICAO[i][0])),
                  nomeEvento(static_cast<Evento>(TRANSICAO[i][1])),
                  nomeEstado(static_cast<Estado>(TRANSICAO[i][1])));
  }
  Serial.println();
  Serial.println("--- roteiro da placa ---");
}

void loop() {
  // O LED espelha o estado: aceso so quando o dispositivo esta ligado.
  digitalWrite(PIN_LED, estadoAtual == LIGADO ? HIGH : LOW);

  Serial.printf("passo %d de %d\n", passo + 1, TOTAL_PASSOS);
  aplicarEvento(ROTEIRO[passo]);
  Serial.print("  estado agora: ");
  Serial.println(nomeEstado(estadoAtual));
  Serial.println();

  passo++;
  if (passo >= TOTAL_PASSOS) {
    passo = 0;
    Serial.println("(roteiro recomeca)");
    Serial.println();
  }

  delay(PASSO_MS);
}

Por que assim e não de outro jeito. Os dois enum, de estado e de evento, existem porque a alternativa — dois inteiros com números soltos — produz exatamente o defeito que esta aula ensina a ver. Com enum, o valor errado de um evento é um identificador que não existe, e o compilador avisa. Com int, ele compila e a máquina aceita uma transição que ninguém escreveu.

As funções nomeEstado e nomeEvento são switch e não índices de arranjo. A razão está no comentário do arquivo: se o enum ganhar um valor novo e alguém esquecer de acrescentar no arranjo, o acesso sai da memória e o monitor imprime lixo. Com switch, o compilador avisa que falta um case, e o aviso chega antes de a placa ir para a bancada. É a mesma razão do pinMode declarado no topo que o curso pede desde o primeiro dia: o que o compilador pega é defeito que não chega na bancada.

A procuraTransicao é uma busca linear na tabela e devolve -1 quando não acha. Esse -1 é o único lugar do arquivo onde a transição impossível é decidida, e é por isso que a recusa tem sempre a mesma frase. Se a decisão estivesse espalhada em if dentro do loop, cada recusa teria uma redação e o log do dia 8 não teria o que casar.

O ROTEIRO de nove eventos é a parte mais importante do sketch, e o professor explica isso antes de rodar: dois dos eventos estão ali de propósito para serem recusados. O segundo ligar chega quando a máquina já está ligando, e o ligado confirmado chega quando ela já não está mais em LIGANDO. Um roteiro em que tudo dá certo não prova tabela de transições; prova que a tabela não foi consultada. O teste de uma máquina de estados é justamente o caminho que não existe, e o roteiro tem esse caminho.

O delay de 2500 ms entre os passos existe para o professor ler a tela em frente à turma. Com o PASSO_MS menor, os nove passos passam em menos de dez segundos e ninguém acompanha. O valor é escolha de aula e não de projeto, e o professor diz isso para o aluno não tratar os 2500 ms como constante técnica.

O desvio medido, e a divergência que o aluno vai encontrar. Rodando o sketch como está, a tabela de sete linhas que o setup imprime tem o destino lido da segunda coluna, e a segunda coluna é o evento. O resultado medido é a tabela mostrada na seção de Conceitos, com quatro das sete linhas mostrando o destino igual à origem. No roteiro de nove passos, a máquina aceita três e recusa cinco: os passos 3, 4, 5, 6 e 8 são recusados, e os passos 1, 7 e 9 passam. Duas dessas aceitações são erradas — o passo 1 deveria ir para LIGANDO e fica em DESLIGADO, e o passo 2 deveria ser recusado e é aceito.

O detalhe que fecha o diagnóstico está na transição de falha. EV_FALHA tem o valor 4 na declaração do enum e ERRO tem o valor 4 também, então a linha {LIGADO, EV_FALHA} funciona por coincidência de numeração e não por estar certa. O professor escreve isso na lousa porque é o melhor argumento da aula contra confiar no teste que passou: um defeito que só se manifesta quando os números de dois enum não coincidem fica em produção até o dia em que alguém insere um estado novo no meio.

A correção é de uma linha estrutural e de uma linha de leitura. A tabela passa a ter três colunas, e o destino é lido da terceira. Depois da correção, o roteiro se comporta como os comentários prometem do começo ao fim: sete passos aceitos, dois recusados, e a transição de falha funcionando por estar certa e não por acaso. O aluno que faz o item 6 da atividade chega nesse número sozinho, e é o número que ele precisa escrever no caderno antes de ir embora.

O LED acende só em ligado, e essa linha é a que fecha a aula na bancada: com a tabela corrigida, o LED acende no passo 3 e apaga no passo 4, e o professor conta acesa e apagada no meio da explicação sem olhar o monitor.

Criterios de correcao

CritérioPontos
Diagrama da máquina desenhado no papel, com os cinco estados e o evento de cada seta2 pontos
Tabela de transições preenchida no papel, com o número de linhas anotado1 ponto
Nove passos copiados com o estado de chegada e o resultado de cada um2 pontos
Os dois números de aceitos e recusados anotados, com a comparação com o palpite2 pontos
Tabela de sete linhas copiada e as quatro divergências marcadas2 pontos
Item 6: os dois números de novo depois de corrigir a tabela para três colunas1 ponto

Erros comuns

ErroComo apareceCorreção
Achar que a máquina está funcionando porque o monitor não reclamouO aluno viu as cinco recusas e concluiu que era o esperado"Recusa é informação, não erro. A tabela que não recusa nada não é uma tabela, é um else que aceita tudo. Conte: o roteiro tem dois eventos proibidos de propósito."
Cinco variáveis booleanas em vez do enumbool desligado, ligando, ligado, desligando, erro; e nenhuma delas parada"O problema não é o tamanho: é que cinco true podem ser verdadeiros ao mesmo tempo, e 'ligado e ligando' é estado que o dispositivo não tem. Uma variável só, e o invariante vem de graça."
Tabela de transições com duas colunas{DESLIGADO, EV_LIGAR} e o destino lido do mesmo lugar do evento"A tabela precisa de três colunas: origem, evento, destino. Com duas, o destino lido é o evento, e as transições funcionam só quando o número do evento bate com o número do estado."
Recusa sem dizer o estado em que ficouO código imprime "evento inválido" e o aluno não sabe onde a máquina parou"A recusa precisa dos dois: o evento que chegou e o estado que não mudou. Sem o estado, o log do dia 8 não tem como casar a tentativa com a máquina."
Confiar no teste que passou"A transição de falha funcionou, então a tabela está certa""Ela funcionou porque EV_FALHA e ERRO têm o mesmo número. Confira o outro lado: no roteiro, três dos nove passos deviam passar e não passam. Um teste verde prova que o caminho testado é verde."
Escolher o destino com if em vez de tabelaif (estado == DESLIGADO && ev == EV_LIGAR) estado = LIGANDO; repetido cinco vezes"A tabela é o lugar único da regra. Cinco if são cinco lugares, e a próxima transição que alguém esquecer é a próxima linha que ninguém encontra."
Declarar LIGANDO e DESLIGANDO como se fossem o mesmo estadoO LED de advertência acende junto com o de operação"Ligar e desligar são caminhos, e o caminho importa: durante LIGANDO o dispositivo ainda não está operando, e o painel precisa saber que está no meio."
Colocar o estado como String em vez de enumString estadoAtual = "ligado"; e comparação com =="Comparar String com == compara o conteúdo e é mais lento, e o compilador não avisa quando o valor digitado não é um estado. enum dá o aviso de graça e ocupa um inteiro."

Desafio extra

Acrescente um sexto estado ao dispositivo, chamado ATUALIZANDO, e escreva as três transições que ele precisa: de onde se entra, o que sai e qual evento dispara a entrada. Escolha o número dele de propósito — coloque no fim do enum, depois de ERRO, e não no meio — e meça o que acontece com a transição de falha depois dessa inserção. Em seguida, corrija a tabela para três colunas e rode o roteiro inteiro de novo, contando quantos passos são aceitos em cada uma das duas versões. A pergunta que o professor espera de resposta, e que vale para o resto do ano: qual das duas colunas da tabela você escolheu ler, e o que exatamente o compilador deixou passar?

>

A resolucao, compilada

// Aula 2 do 3o trimestre: maquina de estados do dispositivo.
//
// O dispositivo nao tem um monte de booleanos: ele tem UM estado, e so sai
// dele por transicoes escritas antes. Qualquer coisa fora da tabela e recusada
// com mensagem, e e essa recusa que a aula quer mostrar.

#include <Arduino.h>

// Pino do LED da placa.
const int PIN_LED = 2;

// Intervalo entre um passo e o outro da simulacao.
const unsigned long PASSO_MS = 2500;

// Os estados do dispositivo. Um enum, nao cinco booleanos: com cinco
// booleanos da para estar desligado e ligando ao mesmo tempo, que e estado
// que nao existe.
enum Estado {
  DESLIGADO,
  LIGANDO,
  LIGADO,
  DESLIGANDO,
  ERRO
};

// Os eventos que chega de fora: botao, WiFi, sensor.
enum Evento {
  EV_LIGAR,
  EV_LIGADO_CONFIRMADO,
  EV_DESLIGAR,
  EV_DESLIGADO_CONFIRMADO,
  EV_FALHA
};

// Estado atual. Uma variavel so: e isso que garante o invariante.
Estado estadoAtual = DESLIGADO;

// Nome do estado, para o monitor. Funcao em vez de array: o compilador
// complains se o enum ganhar um valor e o array ficar curto.
const char* nomeEstado(Estado e) {
  switch (e) {
    case DESLIGADO:   return "DESLIGADO";
    case LIGANDO:     return "LIGANDO";
    case LIGADO:      return "LIGADO";
    case DESLIGANDO:  return "DESLIGANDO";
    case ERRO:        return "ERRO";
  }
  return "DESCONHECIDO";
}

const char* nomeEvento(Evento ev) {
  switch (ev) {
    case EV_LIGAR:                return "ligar";
    case EV_LIGADO_CONFIRMADO:    return "ligado confirmado";
    case EV_DESLIGAR:             return "desligar";
    case EV_DESLIGADO_CONFIRMADO: return "desligado confirmado";
    case EV_FALHA:                return "falha";
  }
  return "evento desconhecido";
}

// A TABELA DE TRANSICOES. A regra esta escrita aqui, e so aqui: dois estados
// em cada linha, e a chave e o evento.
//
// As colunas sao `int`, e nao `Estado`, de proposito: e a tabela COMO ESTA que
// a aula mede. Com `Estado` nas duas colunas o proprio codigo nem compila, e o
// aluno nunca chega na parte que importa, que e o defeito que PASSA no teste.
// Lendo o destino da segunda coluna — que e o evento — a tabela fica correta
// por coincidencia de numeracao em duas linhas e errada nas outras. Ver o
// item 6 da atividade: a correcao e dar uma terceira coluna a tabela.
const int TRANSICAO[][2] = {
  {DESLIGADO,  EV_LIGAR},              // desligado -> ligando
  {LIGANDO,    EV_LIGADO_CONFIRMADO},  // ligando   -> ligado
  {LIGADO,     EV_DESLIGAR},           // ligado    -> desligando
  {DESLIGANDO, EV_DESLIGADO_CONFIRMADO},// desligando-> desligado
  {LIGADO,     EV_FALHA},              // ligado    -> erro
  {ERRO,       EV_DESLIGAR},           // erro      -> desligando
  {DESLIGADO,  EV_FALHA}               // desligado -> erro (falha na partida)
};

const int TOTAL_TRANSICOES = 7;

// Procura a transicao. Devolve -1 quando o evento nao existe na linha atual:
// e isso, e nao um if solto, que recusa a transicao impossivel.
int procurarTransicao(Estado de, Evento ev) {
  for (int i = 0; i < TOTAL_TRANSICOES; i++) {
    if (TRANSICAO[i][0] == de && TRANSICAO[i][1] == ev) {
      return i;
    }
  }
  return -1;
}

// Aplica o evento. Devolve true se aceitou, false se recusou.
bool aplicarEvento(Evento ev) {
  int indice = procurarTransicao(estadoAtual, ev);

  Serial.print("  evento \"");
  Serial.print(nomeEvento(ev));
  Serial.print("\" no estado ");
  Serial.print(nomeEstado(estadoAtual));

  if (indice < 0) {
    Serial.println("  ->  RECUSADO");
    Serial.println("      transicao impossivel: este evento nao existe na tabela");
    return false;
  }

  Estado destino = static_cast<Estado>(TRANSICAO[indice][1]);   // colunas 0 = chave, 1 = destino lido da coluna do evento
  Serial.print("  ->  ");
  Serial.println(nomeEstado(destino));
  estadoAtual = destino;
  return true;
}

// Sequencia que a placa executa, incluindo os dois eventos proibidos.
const Evento ROTEIRO[] = {
  EV_LIGAR,                // ok
  EV_LIGAR,                // RECUSADO: ja esta ligando
  EV_LIGADO_CONFIRMADO,    // ok
  EV_DESLIGAR,            // ok
  EV_LIGADO_CONFIRMADO,    // RECUSADO: nao esta mais ligado
  EV_DESLIGADO_CONFIRMADO, // ok
  EV_FALHA,                // ok: desligado -> erro
  EV_LIGADO_CONFIRMADO,    // RECUSADO: em erro so sai por desligar
  EV_DESLIGAR              // ok: erro -> desligando
};

const int TOTAL_PASSOS = 9;
int passo = 0;

void setup() {
  pinMode(PIN_LED, OUTPUT);

  Serial.begin(115200);
  Serial.println();
  Serial.println("Dia 01 aula 2 — maquina de estados do dispositivo");
  Serial.printf("Transicoes validas na tabela: %d\n", TOTAL_TRANSICOES);
  Serial.println("Qualquer evento fora da tabela e recusado.");
  Serial.println();

  Serial.println("Tabela de transicoes:");
  for (int i = 0; i < TOTAL_TRANSICOES; i++) {
    Serial.printf("  %-12s --%-20s--> %s\n",
                  nomeEstado(static_cast<Estado>(TRANSICAO[i][0])),
                  nomeEvento(static_cast<Evento>(TRANSICAO[i][1])),
                  nomeEstado(static_cast<Estado>(TRANSICAO[i][1])));
  }
  Serial.println();
  Serial.println("--- roteiro da placa ---");
}

void loop() {
  // O LED espelha o estado: aceso so quando o dispositivo esta ligado.
  digitalWrite(PIN_LED, estadoAtual == LIGADO ? HIGH : LOW);

  Serial.printf("passo %d de %d\n", passo + 1, TOTAL_PASSOS);
  aplicarEvento(ROTEIRO[passo]);
  Serial.print("  estado agora: ");
  Serial.println(nomeEstado(estadoAtual));
  Serial.println();

  passo++;
  if (passo >= TOTAL_PASSOS) {
    passo = 0;
    Serial.println("(roteiro recomeca)");
    Serial.println();
  }

  delay(PASSO_MS);
}

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