Regra de negocio — Arduino e IoT — semana 1 do 3o trimestre
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
recusarmanual - 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écnica | Erro de negócio | |
|---|---|---|
| Quem produz | a máquina falhando | uma condição escrita no código |
| Como aparece | exceção, 500, valor impossível | recusa com resposta formatada |
| Quem corrige | o técnico | quem define a regra |
| Onde se olha | o log e a máquina | o requisito e a validação |
| Repete se não corrigir | sim, a máquina volta a falhar | não, a regra continua valendo |
| Mensagem | técnica, com detalhe | para 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:
LEDda placa noGPIO2, com o resistor de 330 ohm e o jumper para oGND.- Botão da bancada no
GPIO4comINPUT_PULLUP, para a demonstração de recusa manual. - Cabo USB conectado, monitor serial em 115200.
- O sensor desta aula é simulado no sketch: nenhum
DHT11conectado, porque a aula é sobre a decisão e não sobre a leitura.
- 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.
- 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.
- 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.
- 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.
- Rode de novo com
CICLOS_ATE_FALHARtrocado 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. - Troque a condição do sensor de
ciclo >= CICLOS_ATE_FALHARparaciclo == CICLOS_ATE_FALHARe 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. - 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.
- 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ério | Pontos |
|---|---|
| Tabela de duas colunas preenchida com os quatro exemplos e o responsável por cada um | 2 pontos |
| Seis primeiras e três últimas linhas copiadas no caderno, com o horário simulado | 1 ponto |
| Os três números de resposta contados em dois minutos de monitor | 2 pontos |
| A linha do 422 localizada, ou a explicação com as duas linhas de código que a impedem | 2 pontos |
Item 5: o 422 aparece com CICLOS_ATE_FALHAR igual a 30, com o horário anotado | 1 ponto |
Item 6: os três números de novo depois da troca para ==, com o efeito no diagnóstico | 1 ponto |
| Tabela do servidor com 422, 503 e 409 e três situações diferentes | 1 ponto |
Erros comuns
| Erro | Como aparece | Correção |
|---|---|---|
| Confundir os dois e devolver 500 para tudo | O 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 invertida | if (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 zero | O 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ário | Só 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 painel | O 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 gravou | O 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:
LEDda placa noGPIO2, com o resistor de 330 ohm e o jumper para oGND.- Botao da bancada no
GPIO4comINPUT_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.
- 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.
- 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.
- 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.
- 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.
- 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.
- Sem mudar o hardware, mude a
structda tabela para três colunas, comEstado de origem,EventoeEstado 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? - 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
enumfosse trocado por cinco variáveis booleanas, e qual das duas frases ficaria falsa. - 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ério | Pontos |
|---|---|
| Diagrama da máquina desenhado no papel, com os cinco estados e o evento de cada seta | 2 pontos |
| Tabela de transições preenchida no papel, com o número de linhas anotado | 1 ponto |
| Nove passos copiados com o estado de chegada e o resultado de cada um | 2 pontos |
| Os dois números de aceitos e recusados anotados, com a comparação com o palpite | 2 pontos |
| Tabela de sete linhas copiada e as quatro divergências marcadas | 2 pontos |
| Item 6: os dois números de novo depois de corrigir a tabela para três colunas | 1 ponto |
Erros comuns
| Erro | Como aparece | Correção |
|---|---|---|
| Achar que a máquina está funcionando porque o monitor não reclamou | O 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 enum | bool 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 ficou | O 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 tabela | if (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 estado | O 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 enum | String 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.
