Nous devons implémenter une machine à états simple dans [~ # ~] c [~ # ~] .
Une déclaration de commutateur standard est-elle la meilleure voie à suivre?
Nous avons un état actuel (état) et un déclencheur pour la transition.
switch(state)
{
case STATE_1:
state = DoState1(transition);
break;
case STATE_2:
state = DoState2(transition);
break;
}
...
DoState2(int transition)
{
// Do State Work
...
if(transition == FROM_STATE_2) {
// New state when doing STATE 2 -> STATE 2
}
if(transition == FROM_STATE_1) {
// New State when moving STATE 1 -> STATE 2
}
return new_state;
}
Existe-t-il un meilleur moyen pour les machines à états simples
EDIT: Pour C++, je pense que la bibliothèque Boost Statechart pourrait être la solution. Cependant, cela n'aide pas avec C. Laissons-nous nous concentrer sur le cas d'utilisation de C.
Je préfère utiliser une approche basée sur les tables pour la plupart des machines d'état:
typedef enum { STATE_INITIAL, STATE_FOO, STATE_BAR, NUM_STATES } state_t;
typedef struct instance_data instance_data_t;
typedef state_t state_func_t( instance_data_t *data );
state_t do_state_initial( instance_data_t *data );
state_t do_state_foo( instance_data_t *data );
state_t do_state_bar( instance_data_t *data );
state_func_t* const state_table[ NUM_STATES ] = {
do_state_initial, do_state_foo, do_state_bar
};
state_t run_state( state_t cur_state, instance_data_t *data ) {
return state_table[ cur_state ]( data );
};
int main( void ) {
state_t cur_state = STATE_INITIAL;
instance_data_t data;
while ( 1 ) {
cur_state = run_state( cur_state, &data );
// do other program logic, run other state machines, etc
}
}
Cela peut bien sûr être étendu pour prendre en charge plusieurs machines d'état, etc. Les actions de transition peuvent également être adaptées:
typedef void transition_func_t( instance_data_t *data );
void do_initial_to_foo( instance_data_t *data );
void do_foo_to_bar( instance_data_t *data );
void do_bar_to_initial( instance_data_t *data );
void do_bar_to_foo( instance_data_t *data );
void do_bar_to_bar( instance_data_t *data );
transition_func_t * const transition_table[ NUM_STATES ][ NUM_STATES ] = {
{ NULL, do_initial_to_foo, NULL },
{ NULL, NULL, do_foo_to_bar },
{ do_bar_to_initial, do_bar_to_foo, do_bar_to_bar }
};
state_t run_state( state_t cur_state, instance_data_t *data ) {
state_t new_state = state_table[ cur_state ]( data );
transition_func_t *transition =
transition_table[ cur_state ][ new_state ];
if ( transition ) {
transition( data );
}
return new_state;
};
L'approche pilotée par table est plus facile à maintenir et à étendre, et plus simple à mapper en diagrammes d'état.
Vous avez peut-être vu ma réponse à une autre question C où j'ai mentionné FSM! Voici comment je le fais:
FSM {
STATE(x) {
...
NEXTSTATE(y);
}
STATE(y) {
...
if (x == 0)
NEXTSTATE(y);
else
NEXTSTATE(x);
}
}
Avec les macros suivantes définies
#define FSM
#define STATE(x) s_##x :
#define NEXTSTATE(x) goto s_##x
Cela peut être modifié pour convenir au cas particulier. Par exemple, vous pouvez avoir un fichier FSMFILE
que vous souhaitez gérer votre FSM, vous pouvez donc incorporer l'action de lecture du prochain caractère dans la macro elle-même:
#define FSM
#define STATE(x) s_##x : FSMCHR = fgetc(FSMFILE); sn_##x :
#define NEXTSTATE(x) goto s_##x
#define NEXTSTATE_NR(x) goto sn_##x
vous avez maintenant deux types de transitions: l’une passe à un état et lit un nouveau caractère, l’autre passe à un état sans consommer aucune entrée.
Vous pouvez également automatiser le traitement de EOF avec quelque chose comme:
#define STATE(x) s_##x : if ((FSMCHR = fgetc(FSMFILE) == EOF)\
goto sx_endfsm;\
sn_##x :
#define ENDFSM sx_endfsm:
L'avantage de cette approche est que vous pouvez traduire directement un diagramme d'états que vous dessinez en code fonctionnel et, inversement, vous pouvez facilement dessiner un diagramme d'états à partir du code.
Dans d’autres techniques de mise en œuvre de FSM, la structure des transitions est enterrée dans des structures de contrôle (while, if, switch ...) et contrôlée par une valeur de variable (typiquement une variable state
) et peut constituer une tâche complexe pour relier le diagramme de Nice à un code compliqué.
J'ai appris cette technique grâce à un article paru dans le grand magazine "Computer Language" qui, malheureusement, n'est plus publié.
J'ai aussi utilisé l'approche de la table. Cependant, il y a des frais généraux. Pourquoi stocker une deuxième liste de pointeurs? Une fonction en C sans le () est un pointeur const. Alors tu peux faire:
struct state;
typedef void (*state_func_t)( struct state* );
typedef struct state
{
state_func_t function;
// other stateful data
} state_t;
void do_state_initial( state_t* );
void do_state_foo( state_t* );
void do_state_bar( state_t* );
void run_state( state_t* i ) {
i->function(i);
};
int main( void ) {
state_t state = { do_state_initial };
while ( 1 ) {
run_state( state );
// do other program logic, run other state machines, etc
}
}
Bien sûr, en fonction de votre facteur de peur (sécurité vs vitesse), vous pouvez rechercher des indicateurs valides. Pour les machines d'état de plus de trois états environ, l'approche ci-dessus doit comporter moins d'instructions qu'une approche équivalente par commutateur ou par table. Vous pouvez même macro-iser comme:
#define RUN_STATE(state_ptr_) ((state_ptr_)->function(state_ptr_))
De plus, d'après l'exemple du PO, je pense qu'il faut simplifier la réflexion/la conception d'une machine à états. Je ne pense pas que l'état de transition devrait être utilisé pour la logique. Chaque fonction d’État devrait être en mesure de remplir son rôle sans connaître explicitement l’état ou les états passés. En gros, vous concevez comment passer de l'état dans lequel vous vous trouvez à un autre.
Enfin, ne démarrez pas la conception d'une machine à états basée sur des limites "fonctionnelles", utilisez des sous-fonctions pour cela. Divisez plutôt les états en fonction du moment où vous devrez attendre que quelque chose se passe avant de pouvoir continuer. Cela vous aidera à réduire le nombre de fois que vous devez exécuter la machine à états avant d'obtenir un résultat. Cela peut être important lorsque vous écrivez des fonctions d’E/S ou des gestionnaires d’interruption.
En outre, quelques avantages et inconvénients de la déclaration de commutateur classique:
Avantages:
Les inconvénients:
Notez les deux attributs à la fois pour et contre. Je pense que le changement offre la possibilité de trop partager entre les États et que l'interdépendance entre les États peut devenir ingérable. Toutefois, pour un petit nombre d’États, il se peut que ce soit le plus lisible et le plus facile à gérer.
Pour une machine d'état simple, utilisez simplement une instruction switch et un type enum pour votre état. Effectuez vos transitions à l'intérieur de l'instruction switch en fonction de votre saisie. Dans un programme réel, vous devez évidemment changer le "if (input)" pour vérifier vos points de transition. J'espère que cela t'aides.
typedef enum
{
STATE_1 = 0,
STATE_2,
STATE_3
} my_state_t;
my_state_t state = STATE_1;
void foo(char input)
{
...
switch(state)
{
case STATE_1:
if(input)
state = STATE_2;
break;
case STATE_2:
if(input)
state = STATE_3;
else
state = STATE_1;
break;
case STATE_3:
...
break;
}
...
}
il y a aussi le grille logique qui est plus facile à maintenir à mesure que la machine à états grossit
Dans ML Distilled de Martin Fowler , il déclare (sans jeu de mots) dans le chapitre 10 Schémas de machines d'état (l'emphase est mise à moi):
Un diagramme d'états peut être implémenté de trois manières principales: commutateur imbriqué , le modèle d'état et tables d'état .
Prenons un exemple simplifié des états de l'affichage d'un téléphone portable:
Fowler a donné un exemple de code C #, mais je l'ai adapté à mon exemple.
public void HandleEvent(PhoneEvent anEvent) {
switch (CurrentState) {
case PhoneState.ScreenOff:
switch (anEvent) {
case PhoneEvent.PressButton:
if (powerLow) { // guard condition
DisplayLowPowerMessage(); // action
// CurrentState = PhoneState.ScreenOff;
} else {
CurrentState = PhoneState.ScreenOn;
}
break;
case PhoneEvent.PlugPower:
CurrentState = PhoneState.ScreenCharging;
break;
}
break;
case PhoneState.ScreenOn:
switch (anEvent) {
case PhoneEvent.PressButton:
CurrentState = PhoneState.ScreenOff;
break;
case PhoneEvent.PlugPower:
CurrentState = PhoneState.ScreenCharging;
break;
}
break;
case PhoneState.ScreenCharging:
switch (anEvent) {
case PhoneEvent.UnplugPower:
CurrentState = PhoneState.ScreenOff;
break;
}
break;
}
}
Voici une implémentation de mon exemple avec le modèle d'état GoF:
En s'inspirant de Fowler, voici un tableau pour mon exemple:
État d'origine État cible État Action d'événement de garde --------------------------------- -------------------------------------------------- --- ScreenOff ScreenOff pressButton powerLow displayLowPowerMessage ScreenOff ScreenOn pressButton! PowerLow ScreenOn ScreenOff pressBouton ScreenOff ScreenCharging plugPower ScreenOn ScreenCharging plugPower .] ScreenCharging ScreenOff unplugPower
Le commutateur imbriqué conserve toute la logique au même endroit, mais le code peut être difficile à lire lorsqu'il y a beaucoup d'états et de transitions. C'est peut-être plus sûr et plus facile à valider que les autres approches (pas de polymorphisme ou d'interprétation).
L'implémentation du modèle d'état répartit potentiellement la logique sur plusieurs classes distinctes, ce qui peut compliquer sa compréhension. Par contre, les petites classes sont faciles à comprendre séparément. La conception est particulièrement fragile si vous modifiez le comportement en ajoutant ou en supprimant des transitions, car ce sont des méthodes dans la hiérarchie et que le code pourrait subir de nombreuses modifications. Si vous respectez le principe de conception des petites interfaces, vous constaterez que ce modèle ne fait pas si bien. Toutefois, si la machine à états est stable, ces modifications ne seront pas nécessaires.
L'approche par tables d'état nécessite l'écriture d'un interprète pour le contenu (cela pourrait être plus facile si vous avez une réflexion dans la langue que vous utilisez), ce qui pourrait nécessiter beaucoup de travail à faire à l'avance. Comme le souligne Fowler, si votre table est distincte de votre code, vous pouvez modifier le comportement de votre logiciel sans recompiler. Cela a toutefois des conséquences sur la sécurité; le logiciel se comporte en fonction du contenu d'un fichier externe.
Il existe également une approche d’interface fluide (également appelée langage interne spécifique au domaine), qui est probablement facilitée par les langages ayant fonctions de première classe . La bibliothèque sans état existe et ce blog montre un exemple simple avec du code. Une implémentation Java (pré Java8) est discutée. On m'a montré un exemple Python sur GitHub également.
J'ai trouvé une très bonne implémentation de Moore FSM en C dans le cours edx.org intitulé Embedded Systems - Shape the World. UTAustinX - UT.6.02x, chapitre 10, de Jonathan Valvano et Ramesh Yerraballi ....
struct State {
unsigned long Out; // 6-bit pattern to output
unsigned long Time; // delay in 10ms units
unsigned long Next[4]; // next state for inputs 0,1,2,3
};
typedef const struct State STyp;
//this example has 4 states, defining constants/symbols using #define
#define goN 0
#define waitN 1
#define goE 2
#define waitE 3
//this is the full FSM logic coded into one large array of output values, delays,
//and next states (indexed by values of the inputs)
STyp FSM[4]={
{0x21,3000,{goN,waitN,goN,waitN}},
{0x22, 500,{goE,goE,goE,goE}},
{0x0C,3000,{goE,goE,waitE,waitE}},
{0x14, 500,{goN,goN,goN,goN}}};
unsigned long currentState; // index to the current state
//super simple controller follows
int main(void){ volatile unsigned long delay;
//embedded micro-controller configuration omitteed [...]
currentState = goN;
while(1){
LIGHTS = FSM[currentState].Out; // set outputs lines (from FSM table)
SysTick_Wait10ms(FSM[currentState].Time);
currentState = FSM[currentState].Next[INPUT_SENSORS];
}
}
switch () est un moyen puissant et standard d'implémenter des machines à états en C, mais il peut réduire la maintenabilité si vous avez un grand nombre d'états. Une autre méthode courante consiste à utiliser des pointeurs de fonction pour stocker l'état suivant. Cet exemple simple implémente une bascule set/reset:
/* Implement each state as a function with the same prototype */
void state_one(int set, int reset);
void state_two(int set, int reset);
/* Store a pointer to the next state */
void (*next_state)(int set, int reset) = state_one;
/* Users should call next_state(set, reset). This could
also be wrapped by a real function that validated input
and dealt with output rather than calling the function
pointer directly. */
/* State one transitions to state one if set is true */
void state_one(int set, int reset) {
if(set)
next_state = state_two;
}
/* State two transitions to state one if reset is true */
void state_two(int set, int reset) {
if(reset)
next_state = state_one;
}
Pour les cas simples, vous pouvez changer votre méthode de style. Ce que j’ai trouvé qui fonctionne bien dans le passé, c’est aussi de gérer les transitions:
static int current_state; // should always hold current state -- and probably be an enum or something
void state_leave(int new_state) {
// do processing on what it means to enter the new state
// which might be dependent on the current state
}
void state_enter(int new_state) {
// do processing on what is means to leave the current atate
// might be dependent on the new state
current_state = new_state;
}
void state_process() {
// switch statement to handle current state
}
Je ne connais rien à la librairie boost, mais ce type d’approche est extrêmement simple, ne nécessite aucune dépendance externe et est facile à mettre en œuvre.
Un de mes modèles préférés est le modèle de conception d'état. Répondre ou se comporter différemment pour le même ensemble d'entrées.
L’un des problèmes liés à l’utilisation des instructions switch/case pour les machines à états est qu’au fur et à mesure que vous créez des états, le switch/les cas devient plus difficile à lire/à gérer, favorise le code spaghetti non organisé et devient de plus en plus difficile à modifier sans casser quelque chose. Je trouve que l'utilisation de modèles de conception m'aide à mieux organiser mes données, ce qui est le but de l'abstraction. Au lieu de concevoir votre code d'état en fonction de votre état d'origine, structurez-le de sorte qu'il enregistre l'état lorsque vous entrez un nouvel état. De cette façon, vous obtenez effectivement un enregistrement de votre état antérieur. J'aime la réponse de @ JoshPetit et ai poussé sa solution un peu plus loin, directement du livre de GoF:
stateCtxt.h:
#define STATE (void *)
typedef enum fsmSignal
{
eEnter =0,
eNormal,
eExit
}FsmSignalT;
typedef struct fsm
{
FsmSignalT signal;
// StateT is an enum that you can define any which way you want
StateT currentState;
}FsmT;
extern int STATECTXT_Init(void);
/* optionally allow client context to set the target state */
extern STATECTXT_Set(StateT stateID);
extern void STATECTXT_Handle(void *pvEvent);
stateCtxt.c:
#include "stateCtxt.h"
#include "statehandlers.h"
typedef STATE (*pfnStateT)(FsmSignalT signal, void *pvEvent);
static FsmT fsm;
static pfnStateT UsbState ;
int STATECTXT_Init(void)
{
UsbState = State1;
fsm.signal = eEnter;
// use an enum for better maintainability
fsm.currentState = '1';
(*UsbState)( &fsm, pvEvent);
return 0;
}
static void ChangeState( FsmT *pFsm, pfnStateT targetState )
{
// Check to see if the state has changed
if (targetState != NULL)
{
// Call current state's exit event
pFsm->signal = eExit;
STATE dummyState = (*UsbState)( pFsm, pvEvent);
// Update the State Machine structure
UsbState = targetState ;
// Call the new state's enter event
pFsm->signal = eEnter;
dummyState = (*UsbState)( pFsm, pvEvent);
}
}
void STATECTXT_Handle(void *pvEvent)
{
pfnStateT newState;
if (UsbState != NULL)
{
fsm.signal = eNormal;
newState = (*UsbState)( &fsm, pvEvent );
ChangeState( &fsm, newState );
}
}
void STATECTXT_Set(StateT stateID)
{
prevState = UsbState;
switch (stateID)
{
case '1':
ChangeState( State1 );
break;
case '2':
ChangeState( State2);
break;
case '3':
ChangeState( State3);
break;
}
}
statehandlers.h:
/* define state handlers */
extern STATE State1(void);
extern STATE State2(void);
extern STATE State3(void);
statehandlers.c:
#include "stateCtxt.h:"
/* Define behaviour to given set of inputs */
STATE State1(FsmT *fsm, void *pvEvent)
{
STATE nextState;
/* do some state specific behaviours
* here
*/
/* fsm->currentState currently contains the previous state
* just before it gets updated, so you can implement behaviours
* which depend on previous state here
*/
fsm->currentState = '1';
/* Now, specify the next state
* to transition to, or return null if you're still waiting for
* more stuff to process.
*/
switch (fsm->signal)
{
case eEnter:
nextState = State2;
break;
case eNormal:
nextState = null;
break;
case eExit:
nextState = State2;
break;
}
return nextState;
}
STATE State3(FsmT *fsm, void *pvEvent)
{
/* do some state specific behaviours
* here
*/
fsm->currentState = '2';
/* Now, specify the next state
* to transition to
*/
return State1;
}
STATE State2(FsmT *fsm, void *pvEvent)
{
/* do some state specific behaviours
* here
*/
fsm->currentState = '3';
/* Now, specify the next state
* to transition to
*/
return State3;
}
Pour la plupart des machines à états, en particulier Dans les machines à états finis, chaque état saura ce que devrait être son prochain état ainsi que les critères de transition vers son prochain état. Cela peut ne pas être le cas pour les conceptions en vrac, d'où la possibilité d'exposer l'API pour les états en transition. Si vous souhaitez davantage d'abstraction, chaque gestionnaire d'état peut être séparé dans son propre fichier, qui équivaut aux gestionnaires d'état concrets du livre GoF. Si votre conception est simple avec seulement quelques états, alors stateCtxt.c et statehandlers.c peuvent être combinés dans un seul fichier pour plus de simplicité.
Cet article est un bon exemple pour le modèle d'état (bien qu'il s'agisse de C++, pas spécifiquement de C).
Si vous pouvez mettre la main sur le livre " Head First Design Patterns ", l'explication et l'exemple sont très clairs.
Vous voudrez peut-être examiner le logiciel du générateur FSM libero . À partir d'un langage de description d'état et/ou d'un éditeur de diagramme d'état (Windows), vous pouvez générer du code pour les langages C, C++, Java et bien d'autres ... plus une documentation et des diagrammes sympathiques. Source et fichiers binaires à partir de iMatix
Pour compilateur qui supporte __COUNTER__
, vous pouvez les utiliser pour des mashines simples (mais volumineux).
#define START 0
#define END 1000
int run = 1;
state = START;
while(run)
{
switch (state)
{
case __COUNTER__:
//do something
state++;
break;
case __COUNTER__:
//do something
if (input)
state = END;
else
state++;
break;
.
.
.
case __COUNTER__:
//do something
if (input)
state = START;
else
state++;
break;
case __COUNTER__:
//do something
state++;
break;
case END:
//do something
run = 0;
state = START;
break;
default:
state++;
break;
}
}
L'avantage d'utiliser __COUNTER__
au lieu de numéros codés en dur, vous pouvez ajouter des états au milieu d’autres états, sans tout renuméroter à chaque fois. Si le compilateur ne supporte pas __COUNTER__
, de manière limitée, il est possible de l'utiliser avec précaution __LINE__
D'après mon expérience, l'utilisation de l'instruction 'switch' est le moyen standard de gérer plusieurs états possibles. Bien que je sois surpris que vous transmettiez une valeur de transition au traitement par état. Je pensais que l’intérêt d’une machine à états était que chaque état exécutait une seule action. Ensuite, l'action/entrée suivante détermine le nouvel état dans lequel effectuer la transition. Je me serais donc attendu à ce que chaque fonction de traitement d'état exécute immédiatement tout ce qui est fixé pour entrer dans un état puis décide ensuite si une transition est nécessaire vers un autre état.
Il existe un livre intitulé Statecharts pratiques en C/C++ . Cependant, c'est moyen trop lourd pour ce dont nous avons besoin.
Vous pouvez utiliser un framework de machine d'état UML minimaliste dans c. https://github.com/kiishor/UML-State-Machine-in-C
Il supporte à la fois la machine à états finie et hiérarchique. Il ne comporte que 3 API, 2 structures et 1 énumération.
La machine à états est représentée par state_machine_t
structure. C'est une structure abstraite qui peut être héritée pour créer une machine à états.
//! Abstract state machine structure
struct state_machine_t
{
uint32_t Event; //!< Pending Event for state machine
const state_t* State; //!< State of state machine.
};
L'état est représenté par le pointeur sur state_t
structure dans le cadre.
Si la structure est configurée pour une machine à états finis, alors state_t
contient,
typedef struct finite_state_t state_t;
// finite state structure
typedef struct finite_state_t{
state_handler Handler; //!< State handler function (function pointer)
state_handler Entry; //!< Entry action for state (function pointer)
state_handler Exit; //!< Exit action for state (function pointer)
}finite_state_t;
Le framework fournit une API dispatch_event
pour envoyer l'événement à la machine à états et à deux API pour la traversée d'états.
state_machine_result_t dispatch_event(state_machine_t* const pState_Machine[], uint32_t quantity);
state_machine_result_t switch_state(state_machine_t* const, const state_t*);
state_machine_result_t traverse_state(state_machine_t* const, const state_t*);
Pour plus de détails sur l'implémentation d'une machine à états hiérarchique, reportez-vous au référentiel GitHub.
exemples de code
https://github.com/kiishor/UML-State-Machine-in-C/blob/master/demo/simple_state_machine/readme.md
https://github.com/kiishor/UML-State-Machine-in-C/blob/master/demo/simple_state_machine_enhanced/readme.md
En C, pour les machines simples, c'est ce que je finis habituellement par utiliser.
Le FSM piloté par les événements est décrit par des objets d'état (FsmState) associés à une action (FsmAction) et des transitions (FsmEdge) définies par l'état et les événements en cours.
Il permet également de stocker et de transmettre à la fois les données FSM et les données utilisateur, afin de séparer les informations liées à FSM et à l'utilisateur et d'autoriser plusieurs instances du même FSM (c'est-à-dire en utilisant la même description mais en transmettant des données utilisateur différentes).
Les événements sont représentés par un type entier (FsmEvent). Les valeurs négatives sont réservées par l'implémentation pour permettre des événements communs spéciaux (Réinitialiser, Aucun, N'importe lequel, Vide, Terminer). Les événements non négatifs sont définis par l'utilisateur.
Pour plus de simplicité, les transitions sont répertoriées dans un tableau et la correspondance est tentée dans l'ordre du tableau, fournissant essentiellement des priorités de transition. Ils ont des fonctions de garde optionnelles. L’état suivant peut être indiqué directement dans la liste de transition ou par une fonction de saut, offrant ainsi plus de flexibilité permettant un comportement dynamique du FSM.
Dans les descriptions de transitions, un état actuel NULL correspond à tout état et un événement générique (Tout) correspond à tout événement. Quoi qu'il en soit, la valeur réelle de l'événement qui a déclenché la transition sera transmise aux fonctions de saut et de garde.
Pour les FSM complexes, la solution simple de matrice Edge peut devenir trop inefficace. Dans ce cas, une fonction de saut appropriée peut être mise en œuvre à l'aide du tableau Edge et des entrées d'événement converties en une matrice de transition ou en des listes de contiguïté d'état.
Les actions d'état sont censées être implémentées avec une fonction réentrante qui fait la distinction entre les opérations d'entrée à l'état (Entrée), de sortie d'état (Quitter) et en état (d'état). De cette manière, les informations d'état locales peuvent être encapsulées et préservées avec des variables de fonction statiques.
Normalement, les actions d'entrée et de sortie à l'état s'exécutent de manière non remarquable et ne renvoient aucun nouvel événement (Aucun). Sinon, le nouvel événement est intercepté et renvoyé immédiatement. Cela empêchera efficacement une transition au cas où cela se produirait lors de la sortie de l'état actuel.
La fonction d'étape FSM (fsmStep) exécute une étape unique du processus, en utilisant un nouvel événement pour déclencher une transition ou aucun événement (Aucun) pour exécuter l'action en état de l'état actuel. La fonction step renvoie un nouvel événement émis qui peut être géré ou réintroduit dans le FSM. ou None, Empty et End en cas d'absence d'événement, de transition non trouvée ou d'état final atteint, respectivement.
#ifndef FSM_H_
#define FSM_H_
#include <stdbool.h>
#include <stdint.h>
/** FSM enum type */
typedef enum
{
// Events and return values
fsm_User = 0, ///< User events start with this id
fsm_Reset = -1, ///< Reset event
fsm_None = -2, ///< No event
fsm_Any = -3, ///< Any event, used as a wildcard
fsm_Empty = -4, ///< No transition found for event
fsm_End = -5, ///< Final state event generated when FSM reaches end state, or stop processing when used in transition
// Action types
fsm_Enter = 0, ///< Entry action
fsm_State, ///< In-state action
fsm_Leave ///< Exit action
} fsm_e;
typedef int FsmEvent; ///< Type for events
typedef struct FsmState FsmState; ///< Type for states
typedef struct FsmEdge FsmEdge; ///< Type for edges (transitions)
/** State action functor
@param state Pointer to this state
@param type Action type (Enter/State/Leave)
@param frto Pointer to from(Enter)/to(Leave) state, NULL otherwise
@param data User data
@return Event id in case of a new triggered event, fsm_None otherwise
*/
typedef FsmEvent (*FsmAction)(FsmState *state, fsm_e type, FsmState *frto, void *data);
/** FSM state object */
struct FsmState
{
FsmAction action; ///< Per-state action
void *data; ///< Per-state data
};
/** State jump functor
@param Edge Pointer to this Edge
@param state Pointer to the actual current state
@param event Event id that triggered the transition
@param data User data
@return Pointer to the next state and NULL for end state
*/
typedef FsmState *(*FsmJump)(FsmEdge *Edge, FsmState *state, FsmEvent event, void *data);
/** Guard function
@param Edge Pointer to this Edge
@param state Pointer to the actual current state
@param event Event id that triggered the transition
@param data User data
@return Guard result
*/
typedef bool (*FsmGuard)(FsmEdge *Edge, FsmState *state, FsmEvent event, void *data);
/** FSM Edge transition */
struct FsmEdge
{
FsmState *state; ///< Matching current state pointer, or NULL to match any state
FsmEvent event; ///< Matching event id or fsm_Any for wildcard
void *next; ///< Next state pointer (FsmState *) or jump function (FsmJump)
FsmGuard guard; ///< Transition guard function
void *data; ///< Per-Edge data
};
typedef struct Fsm Fsm;
struct Fsm
{
FsmState *state; ///< Pointer to the state array
size_t states; ///< Number of states
void **stateData; ///< Pointer to user state data array
FsmEdge *Edge; ///< Pointer to the Edge transitions array
size_t edges; ///< Number of edges
void **edgeData; ///< Pointer to user Edge data array
FsmEvent event; ///< Current/last event
fsm_e type; ///< Current/last action type
FsmState *current; ///< Pointer to the current state
void *data; ///< Per-fsm data
};
#define fsm_INIT { 0 }
static inline FsmEvent
fsmStep(Fsm *f, FsmEvent e)
{
FsmState *cp = f->current; // Store current state
FsmEvent ne = fsm_None; // Next event
// User state data
void *us = (f->stateData && cp) ? f->stateData[cp - f->state] : NULL;
if (!cp && e == fsm_None)
e = fsm_Reset; // Inject reset into uninitialized FSM
f->event = e;
switch (e)
{
case fsm_Reset:
{
// Exit current state
if (cp && cp->action)
{
f->type = fsm_Leave, ne = cp->action(cp, fsm_Leave, f->state, us);
if (ne != fsm_None)
return ne; // Leave action emitted event
}
FsmState *ps = cp;
cp = f->current = f->state; // First state in array is entry state
if (!cp)
return fsm_End; // Null state machine
if (cp->action)
{
us = f->stateData ? f->stateData[0] : NULL;
f->type = fsm_Enter, ne = cp->action(cp, fsm_Enter, ps, us);
}
}
break;
case fsm_None: // No event, run in-state action
if (cp->action)
f->type = fsm_State, ne = cp->action(cp, fsm_State, NULL, us);
break;
default: // Process user transition event
ne = fsm_Empty; // Default return in case no transition is found
// Search transition in listing order
for (size_t i = 0; i < f->edges; ++i)
{
FsmEdge *ep = &f->Edge[i];
// Check for state match (null Edge state matches any state)
if (ep->state && ep->state != cp)
continue; // Not a match
// Check for stop processing filter
if (ep->event == fsm_End)
break;
ne = fsm_None; // Default return for a transition
// Check for event match
if (ep->event == e || ep->event == fsm_Any)
{
// User Edge data
void *ue = f->edgeData ? f->edgeData[i] : NULL;
// Check transition guard
if (!ep->guard || ep->guard(ep, cp, e, ue))
{
// Resolve next state pointer (NULL, new state pointer or jump function)
FsmState *np = (!ep->next) ? NULL
: ((FsmState *)(ep->next) >= f->state && (FsmState *)(ep->next) < (f->state + f->states)) ? (FsmState *)(ep->next)
: ((FsmJump)(ep->next))(ep, cp, e, ue);
if (cp->action)
{
f->type = fsm_Leave, ne = cp->action(cp, fsm_Leave, np, us);
if (ne != fsm_None)
return ne; // Leave action emitted event
}
if (!np) // Final state reached if next state is NULL
ne = fsm_End;
else if (np->action)
{
us = f->stateData ? f->stateData[np - f->state] : NULL;
f->type = fsm_Enter, ne = np->action(np, fsm_Enter, cp, us);
}
cp = np; // New state value
// Transition executed, stop
break;
}
}
}
}
f->current = cp; // Commit current state
return ne; // Last event value
}
static inline FsmEvent
fsmReset(Fsm *f)
{
return fsmStep(f, fsm_Reset);
}
#endif // FSM_H_
Vous trouverez ci-dessous un test très simple pour vous faire une idée de la définition et de l’utilisation de la mise en œuvre FSM. Il n'y a pas d'événements définis par l'utilisateur, seulement des données FSM (chaînes), la même action d'état pour chaque état et une fonction de saut partagée:
#include <stdio.h>
#include "fsm.h"
// State action example
static FsmEvent
state_action(FsmState *s, fsm_e t, FsmState *ft, void *us)
{
FsmEvent e = fsm_None; // State event
const char *q = "?";
switch (t)
{
case fsm_Enter:
q = "enter";
break;
case fsm_Leave:
q = "leave";
break;
default /* fsm_State */:
q = "state";
}
printf("%s %s\n", (const char *)s->data, q);
return e;
}
// States
FsmState fs[] =
{
{ state_action, "S0" },
{ state_action, "S1" },
{ state_action, "S2" },
};
// Transition jump example
static FsmState *
state_jump(FsmEdge *t, FsmState *s, FsmEvent e, void *ue)
{
if (s == &fs[0])
return &fs[1];
if (s == &fs[2])
return NULL; // End state
return NULL; // Trap
}
// Transition guard example
static bool
count_attempt(FsmEdge *t, FsmState *s, FsmEvent e, void *ue)
{
static int c = 0;
++c;
bool b = c == 3;
printf("guard is %s\n", b ? "true" : "false");
return b;
}
// Transitions
FsmEdge fe[] =
{
{ &fs[0], fsm_Any, state_jump }, // Using jump function, no guard
{ &fs[1], fsm_Any, &fs[2], count_attempt }, // Using direct state and guard
{ &fs[2], fsm_Any, state_jump }, // Using jump function, no guard
};
int
main(int argc, char **argv)
{
Fsm f = { fs, 3, NULL, fe, 3, NULL, };
fsmReset(&f);
do
{
fsmStep(&f, fsm_None);
} while (fsmStep(&f, fsm_Any) != fsm_End);
return 0;
}
En C++, considérons le modèle d'état .
Votre question s'apparente à "Existe-t-il un modèle d'implémentation de base de données typique"? La réponse dépend de ce que vous voulez réaliser? Si vous souhaitez implémenter une machine à états déterministe plus grande, vous pouvez utiliser un modèle et un générateur de machines à états. Des exemples peuvent être consultés sur www.StateSoft.org - SM Gallery. Janusz Dobrowolski