Come fare le cose passo dopo passo

5

Questa è la terza volta in cui ho dovuto scrivere software per controllare un modem cellulare. Per coloro che non hanno familiarità con il processo, hai una sequenza di passaggi che devi seguire. Ogni passaggio richiede una certa quantità di tempo e ci sono alcune risposte che dovresti ricevere in quel lasso di tempo. Ci sono anche alcune risposte che puoi ricevere in qualsiasi momento, indipendentemente dal passo in cui ti trovi. In base alla risposta, devi passare ad un'altra fase del processo. Se dovesse scadere, devi andare in una fase diversa. In alcuni casi, i passaggi vengono tentati più volte prima di passare a un altro stage.

Queste devono essere funzioni non bloccanti, che posso eseguire come attività su una singola macchina con thread. Quindi il programma principale chiamerebbe questo modemTask() poche centinaia di volte al secondo, controlla se deve fare qualcosa, quindi esegue una funzione se necessario ed esce.

In passato ho scritto questo come una semplice macchina di stato basata su switch , con fasi enumerate, un po 'come la seguente:

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

È difficile tenere traccia dell'intero flusso del sistema, l'inserimento di un passaggio aggiuntivo richiede modifiche ai passaggi prima e dopo, e sembra proprio che ci dovrebbe essere un modo più semplice. Il più grande che ho dovuto progettare aveva meno di 60 livelli, quindi non è ingestibile, ma non posso fare a meno di pensare che ci sia una strategia o uno schema migliore per questo tipo di lavoro.

Mentre uso un po 'di #define per la maggior parte dei timeout e dei tentativi, sarebbe un po' più bello se non fosse incorporato nella macchina a stati. Forse una struttura di qualche tipo potrebbe essere fatta per contenere ogni stato, ma poiché le risposte variano, sembra altrettanto complicato. La maggior parte dei passaggi avrà un semplice "OK", ma alcuni contengono lo stato e i dati sui quali si deve agire, in cui lo stage cambierà in base alla risposta esatta.

    
posta Adam Davis 07.05.2014 - 02:31
fonte

4 risposte

1

Le macchine a stati sono uno schema di progettazione comune nei sistemi embedded e sembra che tu abbia un tipico caso d'uso qui. Quello che puoi fare è semplicemente avere un ciclo infinito che consente allo stato corrente di gestire il messaggio in arrivo, passare a un nuovo stato se necessario, quindi attendere un po '.

Ecco una semplice prova in C ++. Nel mio codice, è necessario associare o registrare gli stati possibili in qualche modo, ma la cosa bella è che la parte veramente fissa (gestire l'evento, passare allo stato successivo se necessario) è veramente fissa nelle sottoclassi.

Inoltre, una buona vittoria delle macchine a stati è che puoi verificare facilmente la loro correttezza con una combinazione di revisione tra pari, lettura attenta del foglio dati e alcuni test di unità.

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;
}

Quindi, ottieni un loop principale che diventa qualcosa di simile a questo:

#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);
    }
}
    
risposta data 07.05.2014 - 14:02
fonte
0

Estratto sulla sua funzione, non stato. Gli Stati ricevono messaggi, fanno cose in risposta a quei messaggi e alterano lo stato di alcune variabili che sono persistenti negli stati.

I tuoi tentativi, timeout, stato successivo sono persistenti. Li inserivo in una sorta di gestore di tipo singleton, o semplicemente in alcune variabili statiche.

La funzionalità con lo stato è lasciata è cosa fare quando riceve un messaggio. Deve essere in grado di fare cose come cambiare le variabili statiche, eseguire funzioni basate su altre funzioni. Ecco un esempio a cui riesco a pensare, ma queste idee dovrebbero essere adattate a ciò che si desidera essere in grado di scrivere una volta che funzioni con tutti i diversi stati. Sono i loop di attesa? È il controllo per il comando di essere in una lista valida di comandi in base allo stato corrente? È in grado di tenere traccia delle sessioni?

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

Ora qualsiasi classe che crei può essere uno dei tuoi stati implementando questa interfaccia e aggiungendo un metodo Process (stringa) void alla 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;
    }
}
    
risposta data 07.05.2014 - 06:24
fonte
0

Sai come tutti dicono sempre che l'apprendimento di un linguaggio di programmazione funzionale può aiutarti in altre lingue, ma non sai mai esattamente come? Questo è uno di quei casi. A causa del modo in cui i linguaggi funzionali tracciano lo stato, hanno escogitato alcuni modi innovativi per eseguire attività di programmazione asincrone come le macchine a stati. Per usare questi pattern in C ++, probabilmente hai bisogno di C ++ 11 e / o boost.

Futures

I future sono un modo per specificare una sequenza di compiti asincroni, passando un valore da un passaggio all'altro, con percorsi diversi per il successo e l'insuccesso. Potresti estenderli per riprovare il comportamento che desideri. Una macchina di stato implementata usando i futures potrebbe sembrare qualcosa del tipo:

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

coroutine

Le Coroutine sono un modo per ordinare di interrompere una funzione nel mezzo e riprenderla dallo stesso punto in seguito, come quando viene ricevuta una risposta. È un tipo cooperativo di multitasking. Questo ti permette di specificare una funzione in un ordine più lineare.

powerOn()
yield
resetCmd()
yield
attachNetwork
yield

Attori

Gli attori sono un modo per fare passare il messaggio tra le attività in modo asincrono. Dovresti creare un attore per ogni fase della macchina a stati, quindi passare un messaggio al prossimo attore al momento opportuno. Questo è simile a quello che stai facendo ora con la variabile di stato, ma in un modo un po 'più strutturato.

Nessuna di queste idee può essere perfetta per la tua applicazione, ma spero che ti dia una spinta in una direzione per trovare una combinazione che potrebbe essere utile per te.

    
risposta data 07.05.2014 - 15:06
fonte
0

Se vuoi semplicemente passare dall'uso di una macchina basata su switch, ti consiglio una tecnica che ho imparato a scuola, che divide la macchina in 3 o 4 funzioni ortogonali:

  • Una funzione di determinazione dello stato successivo - questo richiederebbe lo stato corrente della macchina e il suo input corrente e determinerebbe quale dovrebbe essere il successivo stato della macchina, restituendo ciò (senza effetti collaterali)
  • Una funzione di output di transizione - questo dovrebbe esaminare quale transizione di stato è stata presa, insieme con l'input, e reagire ad essa (paragonabile all'output di una macchina Mealy, in esecuzione solo quando si verifica una transizione)
  • Una funzione di output di stato - questo dovrebbe guardare lo stato corrente e l'output per esso (paragonabile all'output di una macchina Moore, in esecuzione su ogni iterazione del loop della macchina)
  • Una funzione di transizione: chiama le altre tre funzioni e imposta lo stato della macchina

(Questo è copiato da la mia risposta qui - c'è un esempio di codice anche lì se vuoi un esempio.)

Tuttavia potresti considerare una macchina completamente OOP. Ci sono pacchetti software che ti permetteranno di creare diagrammi e generare il codice per loro, il che dovrebbe ridurre la noia di implementare una macchina a stati.

Se non sei sicuro di quale livello utilizzare, ho integrato con successo un approccio OOP dall'approccio della funzione.

Alcuni dei tuoi requisiti mi hanno fatto pensare a macchine di stato gerarchiche (potresti voler iniziare a leggere al precedente sottosezione), quindi potresti verificarlo.

    
risposta data 07.05.2014 - 15:11
fonte

Leggi altre domande sui tag