Kevin Marques ★

Arduino #1 | Estrutura de Projetos & Máquinas de Estado

· Kevin Marques

Hoje a ideia desse artigo é ser curto mesmo, isso porque eu só quero compartilhar uma coisa que aconteceu comigo hoje e me deixou até que bem feliz. Eu estive arrumando o meu quarto e me deparei com o meu Arduino Xing Ling acumulando poeira na minha gaveta de eletrônicos, aí lembrei de um projeto antigo que já cheguei a fazer nele – um relógio Pomodoro1 – e pensei: “Meh… Por que não tento fazer direito dessa vez?”.

Daí, nessa brincadeira, caiu a ficha do que realmente é uma máquina de estado, e percebi que meio que sempre usei, ao menos em projetos Arduino, onde tudo é meio bare metal, mesmo sem perceber. Lógico que pensei algo como “Putz! cheguei à mesma conclusão de Turing sozinho!”, mas minha animação desapareceu quando percebi que eu só era mais um nerdola num quarto vazio com acesso à internet, jamais pensaria nisso por conta própria no século 19. Enfim.

Contexto

Eu tenho pelo menos uns dois posts nesse site que comento sobre um dos “problemas” que o Arduino têm: ele é muito limitado (sem surpresas, eu sei).

Mas, apesar de relacionado, o problema que eu quero abordar aqui é outro. Quase nenhum projeto que vejo por aí é padronizado de alguma forma; se o projeto não for simplesmente um arquivo .ino gigante, é quase certo que ele será super complicado de acompanhar.

Enquanto estava escrevendo o Makefile (sim, sou usuário de arduino-cli), pensei um pouco como eu iria organizar o projeto, a princípio tava querendo fazer uma parada mais rápida, mas decidi organizar tudo de um jeito fácil de testar no futuro ou de mudar o comportamento geral do código. Mas com intenção nenhuma, só queria tentar acertar dessa vez.

Soluções

Bem, acho que o meu primeiro instinto foi o mesmo que muitos teriam com esse problema: tornar o código super modular.

Comecei com o código que controla o que o meu Arduino deve fazer quando um botão for clicado. Criei uma classe pra isso, só que ela já estava ficando grande demais, então botei ela dentro da pasta src/ – sei que não é uma boa porque o Arduino IDE (1 e 2) delegam o conteúdo dessa pasta, o desenvolvedor só tem acesso aos arquivos .ino.

Meu LSP começou a reclamar que não estava achando a biblioteca Arduino.h no meu sistema, o que faz sentido, mas não tava com paciência de configurar o clangd pra procurar pelos arquivos do Arduino, muitos deles também tem umas dependências estranhas, então meio que não tem muito o que fazer.

— Por que não simplesmente não usar a Arduino.h então?

— Acho que posso fazer só o pomoduino.ino (nome do arquivo principal do projeto) enviar o estado do botão…

— Ah! Eu posso até fazer um setter que usa uma função anônima que essa classe vai executar quando ele detectar o clique. Vai ser legal.

Nesse momento eu me toquei nas implicações que pensei.

— …

— Pera…

Benefícios

Deixa eu tentar explicar porque esse pensamento foi importante pra mim.

Uma das coisas que mais me irrita na hora de escrever C++ pra Arduino é que não é uma tarefa muito fácil escrever testes em Arduino, porque o código vai acabar dependendo de uma placa pra realizar os testes (nada de CI/CD, nem com máquina virtual consegui no passado). Esse é o primeiro problema que escrever uma src/ independente da Arduino.h resolve, uma vez que ela é só C++ puro, eu posso chamar as funções dela e compilar com G++ ou clang++ pra fazer os testes que tanto queria.

E o segundo problema que isso resolve é que agora, magicamente, o código fica legível. O arquivo principal só ficará encarregado de criar uma instância dessa máquina de estado, de atualizar o estado dessa máquina e ler o estado dela, e baseado nesse estado o programa vai executar X ou Y.

E o bônus: toda lógica complicada de tornar todo o código assíncrono2 também é delegada aos arquivos principais.

Usando uma Máquina de Estado

Mas chega de conversa, quero mostrar o código logo. A primeira máquina que fiz pra esse projeto foi a do botão, como já falei, depois fiz outra pro pomodoro e outra pros LEDs/buzzer que chamei de flicker, essa que eu vou mostrar o código.

 1#define FLICKER_STATE_OFF 0
 2#define FLICKER_STATE_ON 1
 3#define FLICKER_STATE_FLICK 2
 4
 5class flicker_o {
 6public:
 7  flicker_o(const unsigned long);
 8
 9  void refresh(unsigned long);
10  void turn_on(void);
11  void turn_off(void);
12  void turn_flick(void);
13
14  //unsigned char get_state(void);
15  //unsigned long get_frequency(void);
16  bool is_on(void);
17
18private:
19  unsigned char _state = FLICKER_STATE_OFF;
20  unsigned long _frequency;
21  unsigned long _pc = 0;
22  bool _is_on = false;
23};

Comentei algumas funções aqui porque elas não vão ser úteis pra explicação, no código original mesmo eu só coloquei elas pra poder loggar o estado da máquina no monitor serial, mas isso é detalhe só.

Não acho que seja muito complicado de entender no geral. O método refresh deve ser rodado no loop(), ele que vai atualizar o estado (_state) da máquina, que pode só assumir os estados nas constantes acima.

E o _is_on? Essa é a variável que vai ficar acesa (true) quando a máquina estiver em FLICKER_STATE_ON, desligada (false) quando estiver em FLICKER_STATE_ON e oscilando entre ligado e desligado quando estiver no estado FLICKER_STATE_FLICK. Ai esse último estado vai depender do timer da placa – o que explica o unsigned long em refresh() – e de uma frequência – o que explica o _frequency.

 1/**
 2 * Constructor da máquina.
 3 */
 4flicker_o::flicker_o(const unsigned long frequency) {
 5  _frequency = frequency;
 6}
 7
 8/**
 9 * Atualiza o estado do LED (acontece durante clock).
10 */
11void flicker_o::refresh(unsigned long timer) {
12  switch _state {
13  case FLICKER_STATE_OFF:
14    _is_on = false;
15    break;
16
17  case FLICKER_STATE_ON:
18    _is_on = true;
19    break;
20
21  case FLICKER_STATE_FLICK:
22    if (timer - _pc > _frequency) {
23      _is_on = not _is_on;
24      _pc = timer;
25    }
26
27    break;
28  }
29}
30
31// Métodos pra mudar o estado da máquina.
32
33void flicker_o::turn_on(void) {
34  _state = FLICKER_STATE_ON;
35}
36
37void flicker_o::turn_off(void) {
38  _state = FLICKER_STATE_OFF;
39}
40
41void flicker_o::turn_flick(void) {
42  _state = FLICKER_STATE_FLICK;
43}

E caso esteja se perguntado, você usaria essa máquina de estado assim:

 1flicker_o flicker(250);
 2
 3void setup(void) {
 4  pinMode(13, OUTPUT);
 5
 6  flicker.turn_flick();
 7}
 8
 9void loop(void) {
10  flicker.refresh(millis());
11
12  digitalWrite(13, flicker.is_on());
13}

Note que o snippet acima é o único que usa funções da Arduino.h, a máquina de estado é feita com C++ puro, e facilmente testável só com o ferramental da própria linguagem.

Esse código já inicia a máquina de estado no modo flick, que faz o valor do _is_on ficar oscilando entre true ou false, mas a ideia é que o código principal faria mais coisas, e dependendo das condições ele mudaria o estado dessa máquina.

Agora que parei pra pensar, o próprio código original poderia ser facilmente uma máquina de estado também! Acredito que o código ficaria muito mais fácil de acompanhar com essa estratégia aí também.

O que Aprendi

Assim que comecei a ter uns flashbacks dos vídeos do Fábio Akita, perguntei pra alguns amigos se isso era realmente uma máquina de estado, depois pesquisei certinho o que era, quando confirmaram minha teoria. Pra ser mais específico, o que eu criei foi uma máquina de estado finita determinística, também chamado de autômato finito determinístico (DFA).

Não me aprofundei muito nos detalhes do assunto, mas acho que agora eu finalmente entendi o que torna um sistema turing complete de fato.

Se eu fosse fazer de novo, com certeza faria diferente, usaria um enum class, usaria um simples set_state() ao invés de ter métodos separados, etc.. Mas pra um projeto de meia hora acho que dá pro gasto.

Idealizações de Projetos Futuros

Depois que finalizei o projeto e comecei a usar de fato esse alarme caseiro, lembrei de uma ideia antiga que rendeu alguns repositórios no meu perfil do Github.

Há um tempo atrás, eu tava estava querendo muito dar um jeito de fazer alguma coisa, biblioteca, ou o que for, pra manipular múltiplos servo motores ao mesmo tempo. Não só isso, mas manipular vários servomotores de forma simples.

Até cheguei a criar uma biblioteca (coberta de testes, aliás) que realmente funciona, mas ainda assim ela parece super estranha, e a sintaxe pra escrever o “script” – meio que essa biblioteca cria uma linguagem própria pra resolver o problema em específico – não está muito amigável. O meu maior erro foi tentar fazer a biblioteca depender da Servo.h, agora me ocorreu que não precisava dela.

Não comecei nada sério ainda, estou apenas idealizando. Arduino é uma coisa que saiu do meu hiperfoco faz um tempo já, duvido que vou voltar a me interessar tão cedo pelo assunto. Mas pra não desperdiçar o hype, eu escrevi, mais ou menos, a API que essa minha biblioteca vai ter:

 1#pragma once
 2
 3namespace pservo {
 4  enum class state : unsigned char {
 5    STAND_BY,
 6    INITIALIZED,
 7    MOVING,
 8    WAITING,
 9    DONE,
10    PAUSED,
11    ERROR_UNINITIALIZED,
12    // [...]
13  };
14
15  namespace defaults {
16    unsigned char const MIN = 0;
17    unsigned char const MAX = 180;
18    unsigned char const DELAY = 1;
19  };
20
21  typedef struct props {
22    state state;
23    bool is_status_ok;
24
25    unsigned char min;
26    unsigned char max;
27    bool is_resetable;
28
29    unsigned char curr_action;
30    unsigned char actions_count;
31    unsigned char pos;
32    unsigned short delay;
33  } props;
34
35  class pservo {
36  public:
37    pservo(unsigned char const, unsigned char const, bool);
38    pservo() {}
39
40    void refresh(unsigned long const *);
41    unsigned char get_pos(void);
42
43    pservo *begin(void);
44    pservo *wait(bool);
45    pservo *move(unsigned char const);
46    pservo *move(unsigned char const, unsigned short const);
47    pservo *halt(void);
48
49    props get_props(void);
50    state get_state(void);
51    bool is_status_ok(void);
52
53  private:
54    state _state = state::STAND_BY;
55    bool _is_status_ok = true;
56
57    unsigned long _pc = 0;
58    unsigned long *_timer = nullptr;
59
60    unsigned char _min = defaults::MIN;
61    unsigned char _max = defaults::MAX;
62    bool _is_resetable = false;
63
64    unsigned char _curr_action = 0;
65    unsigned char _actions_count = 0;
66    unsigned char _pos = 0;
67    unsigned short _delay = defaults::DELAY;
68  };
69
70  void log_state(char *&, pservo *);
71};

Não quero gastar muito tempo explicando, mas – de forma resumida – o usuário iria definir o padrão de movimento do servo dentro do loop(), ai o primeiro loop seria pra setar a quantidade de movimentos/pausas e os outros seria pra atualizar o estado da máquina.

Assim essa máquina guardaria o estado de movimento e qual a posição atual que o servo deve estar a cada movimento. Assim o usuário teria uma interface super simples de usar.

Certo que é mais fácil falar do que fazer, mas eu tenho certeza que o que eu disse é totalmente possível. Só preciso dedicar um tempinho pra começar a implementar o corpo desses métodos que a solução vem.

Estrutura Básica pra Novos Projetos

Isso tudo me fez pensar em como eu iria organizar qualquer tipo de projeto. As conclusões que cheguei foram bem simples até.

Primeiro de tudo, não acho que seja uma boa depender de uma pasta src/ como fiz com o meu projeto. Quem usa o Arduino IDE não vai conseguir abrir esses arquivos com facilidade. Mas de vez em quando vem a calhar. Enfim, o mais importante é que o código dessa pasta não dependa da biblioteca Arduino.h, pra facilitar os testes e pra isolar a lógica bruta do sketch.

Já os arquivos .ino ficam na raiz do sketch. na minha cabeça faz sentido usar o nome do sketch como prefixo pros nomes dos outros arquivos de helpers, onde eu imagino que ficaria as funções que interagem com a placa. A ideia é deixar o arquivo principal só com o setup() e loop().

Aí a raiz, a raiz mesmo, do projeto ficaria com as informações do repositório (com README.md pra documentações, Makefile pra buildar o projeto, etc.). A pasta com o sketch ficaria dentro dessa raiz.

No caso de bibliotecas, a pasta src/ poderia ficar na raiz do projeto mesmo, e haveria uma pasta chamada examples/ com vários sketches que usam a biblioteca. Também acho que seria bom se a biblioteca não dependesse interamente do Arduino, pra ficar fácil de testar na máquina do desenvolvedor sem depender da placa.

No final, ficaria algo similar a isso:

Mais Informações

Repositório do projeto desse blog:

Acabei descobrindo que máquinas de estado são muito mais comuns do que imaginei, eu mesmo até fiz alguns projetos que usam uma estratégia parecida sem perceber. Por exemplo, esse semáforo “inteligente” que criei pra um projeto da escola:

As bibliotecas que criei pra controlar os servos de forma assíncrona:

Vídeos que foram super úteis pra eu entender a teoria:


  1. A essa altura você deve saber, mas caso não saiba, é só um relgógio que conta 25 min pra trabalho/estudo e 5 min pra descanço, o que ajuda muito quem sofre com problemas de concentração como eu. Mais informações aqui, se precisar. ↩︎

  2. Veja esse post antigo que fiz, ainda acredito que ele seja útil até hoje. ↩︎

#Programming   #Learning  

comments powered by Disqus