Como levar as coisas passo a passo

5

Esta é a terceira vez que eu tive que escrever software para controlar um modem celular. Para aqueles que não estão familiarizados com o processo, você tem uma seqüência de etapas que você deve seguir. Cada etapa leva um certo tempo, e há algumas respostas que você deve receber nesse período de tempo. Há também algumas respostas que você pode receber a qualquer momento, independentemente da etapa em que esteja. Com base na resposta, você precisa ir para outro estágio do processo. Se acabar, você tem que ir para um estágio diferente. Em alguns casos, as etapas são tentadas várias vezes antes de passar para outro estágio.

Estas têm que ser funções sem bloqueio, que eu posso executar como uma tarefa em uma única máquina com thread. Assim, o programa principal chamaria isso de modemTask() algumas centenas de vezes por segundo, ele verifica se precisa fazer alguma coisa, executa uma função, se necessário, e sai.

No passado escrevi isso como uma simples máquina de estados baseada em switch , com estágios enumerados, algo como o seguinte:

switch(stage){
   case Power:
      powerOn();                 // Turn the modem on
      nextstage = ResetCmd;      // Go perform a reset
      attemptsLeft = 5;          // Send the reset command up to five times
      break;
   case ResetCmd;
      modem.write("ATZ\n");      // ATZ - reset
      attemptsLeft--;            // Use one of our attempts
      nextstage = ResetReply;    // Next wait for a response (should be OK)
      timeout = millis() + 5000; // Wait for up to 5 seconds each attempt
      break;
   case ResetReply;
      if(receivedResponse() == OK)     // Success
      {
         nextStage = NetworkAttachCmd; // Attach to the cellular network
      } elseif(receivedResponse() == ERROR || timeout < millis())
      {        // If we get an error or timeout, reattempt if we can, power on if we can't
         if(attemptsLeft > 0)
         {
            nextStage = ResetCmd;
         } else {
            nextStage = Power;
         }
      } 
      break;
   case NetworkAttachCmd:
   ...
}
stage = nextstage;               // Assign stage indirectly for debug purposes - nice to know where we came from at this point

É difícil acompanhar todo o fluxo do sistema, inserindo uma etapa adicional requer alterações nas etapas antes e depois, e parece que deve haver uma maneira mais fácil. O maior que eu tive que projetar tinha menos de 60 estágios, então não é incontrolável, mas não posso deixar de pensar que há uma estratégia ou um padrão melhor para esse tipo de trabalho.

Embora eu use alguns #define para a maioria dos tempos limite e tentativas, seria um pouco melhor se isso não fosse incorporado na máquina de estado. Talvez uma estrutura de algum tipo pudesse ser feita para manter cada estado, mas, como as respostas variam, parece tão complicado. A maioria das etapas terá um simples "OK", mas algumas conterão status e dados que precisam ser executados, onde o estágio será alterado com base na resposta exata.

    
por Adam Davis 07.05.2014 / 02:31
fonte

4 respostas

1

As máquinas de estado são um padrão de design comum em sistemas embarcados, e parece que você tem um caso de uso típico aqui. O que você pode fazer é simplesmente ter um loop infinito que permite que o estado atual manipule a mensagem recebida, faça a transição para um novo estado, se necessário, e espere um pouco.

Aqui está uma simples tentativa de C ++. No meu código, você precisa vincular ou registrar os possíveis estados de alguma forma, mas o interessante é que a parte realmente fixa (manipular evento, alternar para o próximo estado, se necessário) é realmente corrigida em subclasses.

Além disso, uma ótima vitória das máquinas de estado é que você pode verificar sua correção facilmente com uma combinação de revisão por pares, leitura cuidadosa de folhas de dados e alguns testes de unidade.

class State {
public:
    // Forgetting constructors, etc.

    // Assume event is an integer, could be tuned to your case
    State *handleEvent(int event) {
        switch (event) {
           case EVENT1:
               this->doAction1();
               break;
            // etc.
        }
        State *nextState = this->nextStateForEvent(event);
        return nextState;
    }

    private:
        // Force each subclass (= posisble state) to re-implement all these
        virtual void doAction1() = 0;
        // etc.

        virtual State *nextStateForEvent(int event) = 0;
}

Em seguida, você obtém um loop principal que se torna algo assim:

#define DEFAULT_STATE 0

volatile int event = 0;

int main()
{
    // Create the states beforehand
    State *allStates = populate_the_possible_states;
    State *currentState = allStates[DEFAULT_STATE];

    while (1) {
        currentState = currentState->handleEvent(event);
        sleep(someMilliseconds);
    }
}
    
por 07.05.2014 / 14:02
fonte
0

Resumo em sua função, não em estado. Os estados recebem mensagens, fazem coisas em resposta a essas mensagens e alteram o estado de algumas variáveis que persistem nos estados.

Suas tentativas, tempo limite e o próximo estado persistem. Eu coloquei isso em algum tipo de gerenciador de tipos singleton, ou simplesmente algumas variáveis estáticas.

A funcionalidade com o estado é deixada é o que fazer quando recebe uma mensagem. Ele precisa ser capaz de fazer coisas como alterar variáveis estáticas, executar funções com base em outras funções. Aqui está um exemplo que eu posso pensar, mas essas idéias devem ser adaptadas para o que você quer ser capaz de escrever uma vez que funciona com todos os seus diferentes estados. São os loops de espera? É a verificação de que o comando esteja em uma lista válida de comandos com base no estado atual? Está sendo capaz de acompanhar as sessões?

public interface IState {
    void Process(string message);
}

Agora, qualquer classe que você criar pode ser um dos seus estados implementando essa interface e adicionando um método de processo (string) vazio à classe.

public class PowerOn : IState {
    public void Process(string message) {
        powerOn();
        StateManager.NextStage = new ResetCmd();
        StateManager.AttemptsLeft = 5;
    }
}

public class ResetCmd : IState {
    public void Process(string message) {
        modem.write("ATZ\n");
        StateManager.AttemptsLeft--;
        StateManager.NextStage = new ResetReply();
        StateManager.Timeout = millis() + 5000;
    }
}
    
por 07.05.2014 / 06:24
fonte
0

Você sabe como todo mundo está sempre dizendo que aprender uma linguagem de programação funcional pode ajudá-lo em outros idiomas, mas você nunca sabe exatamente como? Este é um daqueles casos. Devido à forma como as linguagens funcionais rastreiam o estado, elas criaram algumas maneiras inovadoras de executar tarefas de programação assíncrona, como máquinas de estado. Para usar esses padrões em C ++, você provavelmente precisará do C ++ 11 e / ou boost.

Futuros

Os futuros são uma maneira de especificar uma sequência de tarefas assíncronas, passando um valor de passo a passo, com caminhos diferentes para sucesso e falha. Você pode estendê-los para o comportamento de repetição desejado. Uma máquina de estado implementada usando futuros pode ser algo como:

retryFuture(5)
.then(powerOn)
.then(resetCmd)
.then(attachNetwork)
.timeout(5000)
.failure(printFailureToScreen)

Coroutines

Coroutines são uma maneira de interromper uma função no meio e retomá-la a partir do mesmo ponto, como quando uma resposta é recebida. É um tipo cooperativo de multitarefa. Isso permite que você especifique uma função em uma ordem mais linear.

powerOn()
yield
resetCmd()
yield
attachNetwork
yield

Atores

Atores são uma maneira de passar mensagens entre tarefas de maneira assíncrona. Você configuraria um ator para executar cada etapa da máquina de estado e passaria uma mensagem para o próximo ator no momento apropriado. Isso é semelhante ao que você está fazendo agora com a variável de estado, mas de uma maneira um pouco mais estruturada.

Nenhuma destas ideias pode ser perfeita para a sua candidatura, mas esperamos que lhe dê um impulso na direção de encontrar uma combinação que possa ser útil para você.

    
por 07.05.2014 / 15:06
fonte
0

Se você quer apenas usar uma máquina baseada em switches, eu recomendo uma técnica que eu aprendi na escola, que está dividindo a máquina em 3 ou 4 funções ortogonais:

  • Uma função de determinação de estado seguinte - isso levaria o estado atual da máquina e sua entrada atual e determinaria qual seria o próximo estado da máquina, retornando isso (sem efeitos colaterais)
  • Uma função de saída de transição - isso examinaria qual transição de estado estava sendo tomada, junto com a entrada e reagiria a ela (comparável à saída de uma máquina Mealy, sendo executada somente quando ocorre uma transição)
  • Uma função de saída de estado - isso examinaria o estado atual e a saída para ele (comparável a uma saída da máquina Moore, sendo executada em cada iteração do loop da máquina)
  • Uma função de transição - isso chamaria as outras três funções e definiria o estado da máquina

(Isso é copiado de minha resposta aqui - há um exemplo de código também se você quiser um exemplo.)

Você pode considerar uma máquina OOP totalmente embora. Existem pacotes de software que permitem diagramar máquinas e gerar o código para elas, o que deve reduzir o tédio da implementação de uma máquina de estados.

Se você não tiver certeza de qual nível seguir, eu obtive sucesso em uma abordagem OOP a partir da abordagem de função.

Algumas de suas exigências me fizeram pensar em máquinas de estado hierárquicas (você pode querer começar a ler no anterior sub-seção), então você pode verificar isso.

    
por 07.05.2014 / 15:11
fonte