FSM Features

The FSM library has numerous capabilities that you would expect of a FSM library. To best point out the features of the FSM library, we will program the FSM logic for a simple CD player. The first step is performing Finite State Machine analysis to model the states that the CD player can be in and the events that the CD player will accept in a given state. Initially, we will make some simplifying assumptions to give us a two-state CD player (stopped, playing). The analysis leads to the states: stopped, playing, and events: Start, Stop. The UML state chart notation depicts the state logic for the two- state CD player:

Figure 1. UML State Chart Representation of CD Player Control Logic

Figure 1 illustrates a CD player state machine in UML State Chart notation. (A better overview of UML notation can be found here.) The FSM shows 4 states, (Initial state, STOPPED, PLAYING, Final state); 2 transitions (StartTransition, StopTransition) that are made up of two events (start, stop) with two actions (startAction, stopAction). The initial state and final state are called Pseudo States, since no real activity occurs. Transition to the Final state of an FSM corresponds to the FSM destruction. Within the Playing and Stopped states, internal transitions can be defined, which define internal actions, or activities, that are performed in response to events received while the object is in the state, without changing state. “Do” is a cyclic update activity performed when no external event has occurred.

Next, we take this FSM analysis and translate it into source code. FSM library does not automatically generate source from graphical representation at this time. So the FSM logic must be programmed. The following steps are performed to program the FSM logic.

1.      Define and implement new FSM class

a)     Derive a new class from CFiniteStateMachine

b)     Declare and implement event methods

c)     Declare and implement action and activity methods

d)     Setup FSM logic by defining transitions based on states, events and actions

2.      Declare and use instances of new FSM class

a)     Declare instance of FSM class

b)     Setup new FSM class

c)     Client invokes event methods

d)     Updating FSM:  If asynchronous model, updater causes FSM to services events, otherwise, FSM directly services events or services events in a background thread.

The FSM library is capable of some advanced functionality. The CD player example will be used to explore the following capabilities:

1.      Refine logic based on conditions, timeouts, triggers

2.      Parameterize Events

3.      Define execution model – asynchronous or automatic asynchronous or synchronous

4.      Nest FSM

5.      Customize State Behavior

6.      Trigger Events within FSM

7.      Monitor FSM behavior with Observer Pattern

Defining and implementing new FSM class

Derive a new class from CFiniteStateMachine

The first programming step is to declare a new FSM class derived from the CFiniteStateMachine class.

class CPlayer : public CFiniteStateMachine

 

For the CD player example, the derived class is CPlayer. To understand why the derived class was named CPlayer and not Player, see Hungarian notation URL for discussion on the naming convention popularized by Microsoft.

Declare and implement Event Methods

Next, declare and implement event methods that will signal state transitions. For the CD player example, the events “Start” and “Stop” are defined. Users of the CPlayer would invoke these methods to cause the CD player behavior to change.

class CPlayer : public CFiniteStateMachine

{

 public:

       void Start(){processEvent(“start”); }

       void Stop(){processEvent(CFSMEvent(“stop”, 10)); }

    . . .

};

 

The event methods define the service available to clients, while hiding the internal event-handling mechanism. Internally, the CFiniteStateMachine has the handleEvent or processEvent method to accept and queue new events. The processEvent method immediately services the events.  Queuing is not possible. The handleEvent method is more flexible and can either service the event immediately or allow queuing of the event depending on the FSM updating policy. FSM library has a policy mechanism to control the variety of options for operation and error handling, see FSM Policy mechanism for more details.

Several ways are possible to pass events when processing events.  For the Start event implementation, we merely allow a default CFSMEvent to be constructed from an ASCII string. For the Stop method implementation, we create a temporary CFSMEvent passing an ASCII text string name and priority to the constructor. The temporary event is copied by the processEvent and the copy will be inserted into the CFiniteStateMachine event queue with the given priority. By default, events are given a priority of 1 when no priority is assigned. Event priorities can range from 0…n with higher values corresponding to higher priorities. There are other approaches to defining the event illustrating how to pass data, but they will be covered later.  Other approaches to handling events are possible, and will be covered later.

Declare and implement Action and Activity Methods

We next implement action methods “startAction” and “stopAction” to associate behavior with these events. Each action method signature accepts a CFiniteStateMachine pointer as well as a CFSMEvent reference pointer.

class CPlayer : public CFiniteStateMachine

{

   . . .

 

   HRESULT startAction(CFiniteStateMachine * fsm, CFSMEvent * event)

    {

      AfxMessageBox("CD Player started");

      return S_OK;

    }

    HRESULT stopAction(CFiniteStateMachine * fsm, CFSMEvent * event)

    {

      AfxMessageBox("CD Player stopped");

      return S_OK;

    }

};

 

The action methods must adhere to the declared function type FSMObjectFunction signature defined below. This FSMObjectFunction action method signature returns an HRESULT, which is the Microsoft enumerated success and error status codes mechanism. Any enumerated name with S_, as in S_OK, S_TRUE, or S_FALSE, means that the function succeeded. Any name with E_ in it, which may be at the beginning as in E_FAIL or E_NOTIMPL means that the function failed.  See URL for more discussion on COM-based error handling.

 

typedef HRESULT (CFSMObject::* FSMObjectFunction)(CFiniteStateMachine * trans, CFSMEvent * event);

 

Purists would insist on error-handling exclusively through separate error states. However, there are shades of errors, and using the HRESULT mechanism to propagate errors offers flexibility to deal with incidental errors, such as a warning. See FSM Error Handling for more discussion on the problems of detecting and handling errors within the FSM library.

Setup of FSM state transition logic

The next step is to set up the state transition logic. This is done by implementing the  setupFSM method in the CPlayer class to define the state transitions allowed in the CPlayer. First, the method setFirstState is used to define the initial state of the FSM.  Then, the addTransition method is called repeated to adds state transitions for every event in a given state. This leads to the following code:

class CPlayer : public CFiniteStateMachine

{

       . . .

       void setupFSM(){

              setFirstState("STOPPED");

              addTransition("STOPPED",   

                     "start",   

                     "PLAYING", 

                     this, (FSMObjectFunction)  &CPlayer::startAction,

                     NULL);

              addTransition("PLAYING",   

                     "stop",   

                     "STOPPED", 

                     this, (FSMObjectFunction)  &CPlayer::stopAction,

                     NULL);

              addActivity("PLAYING", “do”,

                     this, (FSMObjectFunction) &CPlayer::playingActivity);        

       }

Here, the addTransition method adds the state transition for the event start in the STOPPED state as a state transition to the PLAYING state, which initially performs the startAction. Next, we add the state transition for the event stop in the PLAYING state as a transition to the STOPPED state that performs the stopAction. The setupFSM contains an addActivity method to add an internal action (associated with a state) to be performed every update of the FSM while in the PLAYING state, even when no external event has occurred. In a purely event-driven FSM, we could omit activities and perform actions only when an event has occurred, but in a control application cyclic updates must be performed, so a playingActivity will be periodically performed whenever the FSM is updated and in the PLAYING state.[1]  (To achieve purely event-driven behavior, the alternative to have an external timer generate an “update” event every cycle is possible.)

The addActivity method uses C++ pointers to class members to invoke actions. Pointers to class member can be declared as in the FSMObjectFunction, but require a pointer to the class instance (this) and a pointer to the class member (&CPlayer::stopAction).

                     this, (FSMObjectFunction)  &CPlayer::stopAction,

Actions could be declared like C callbacks, as pointers to static members, but this is not quite as flexibile – you can’t access class-specific variables without the “this” pointer anyway.

Actions can be shared and reused in a number of ways. Actions can be used from any other class instance derived from FSMObject besides the derived FSM class. This capability is useful for reusing or centralizing code throughout a program’s logic without copying the source or inheriting.

class CCentralizedActions: public FSMObject

{

public:

    HRESULT genericNonAction(CFiniteStateMachine * fsm, CFSMEvent * event)

    {

      AfxMessageBox(0, "Nothing Ventured Nothing Gained", "" ,  MB_OK)

      return S_OK;

    }

};

CCentralizedActions coreActions;

...

 

       coreActions, (FSMObjectFunction)  &CCentralizedActions::genericNonAction,

 

Declaring and using instances of new FSM Class

Now we are ready to illustrate the basic use of the newly implemented FSM class. 

Declare instance of FSM class

We declare the instance player of type CPlayer. Since CPlayer derives from CFiniteStateMachine, which is a generalization of RefCount, the RefCount method AddRef must be invoked when using stack-based (as in the example below) or heap-based (using the operator new) CFiniteStateMachine instances. Correspondingly, to free up any references in the CPlayer instance, the method releaseFSM is invoked, which clears any FSM setup and frees all events, states, actions and transitions. If the FSM reference count is not incremented, this will cause a problem later when the releaseFSM is invoked.  The use of AddRef and releaseFSM are part of the use of Smart Pointers within the CFiniteStateMachine class to handle events, states, actions and transition. (See Smart Pointers discussion for information on their use.)

main()

{      CPlayer player;

       player.AddRef();

       player1.setupFSM();

       player1.Start();

       …

       player.releaseFSM();

}

Setup FSM

Invoking the method setupFSM on the player instance builds the state transition table. The change of FSM personalities would be done by first invoking the releaseFSM() method and then invoking an dfifferent setupFSM method.

main()

{      CPlayer player;

       player.AddRef();

       player1.setupFSM();

       player1.Start();

       …

       player.releaseFSM();

}            

             

The setupFSM operation is explicit (i.e. not implicitly done in the CFiniteStateMachine) so that multiple control logics could be implemented within the same FSM and changed dynamically. It is unclear if changing FSM personalities is important, but the capability is there.

Client invokes event methods

Clients invoke methods to achieve behavior.  For example, to start the CD player, the Start method is invoked.  How the event is serviced depends on two things: how the event method was implemented and how FSM updating policy was defined.

main()

{      CPlayer player;

       player1.setupFSM();

       player1.Start();

       …

}

Depending on the event method implementation, an event can be serviced immediately (Synchronously), or it can be queued and handled now (Synchronously) or later (Asynchronously). If the event was defined to use the CFiniteStateMachine processEvent() method, then the event will be serviced immediately. If the event was defined to use the CFiniteStateMachine handleEvent() method, then the timing of the event service depends on the updating policy that has been defined for the  FSM.  The following section examines the FSM update policy.

FSM Updating Policy

The servicing of queued events in an FSM depends on defined policy. Assuming an event method was defined to use the CFiniteStateMachine processEvent() method,  the FSM can either be Synchronous or Asynchronous.  Three asynchronous execution approaches are also allowed: FSM_ASYNCHRONOUS, FSM_THREADED_ASYNCRHONOUS, FSM_TIMER_ASYNCRHONOUS.  In asynchronous FSM execution model, events are not serviced when they are received; they are queued and serviced later asynchronously. For the FSM_ASYNCHRONOUS case, the FSM is updated by an external service.  The other two approaches rely on CFiniteStateMachine mechanisms to spawn background threads that handle the updating transparently to the FSM client. For the FSM_ THREADED _ASYNCHRONOUS case, the FSM is updated periodically by a background thread created the FSM.  For the FSM_TIMER_ASYNCHRONOUS case, the FSM is updated periodically using a timer by a background thread created by the FSM. 

In the general FSM_ASYNCHRONOUS mode, an explicit call to updateFSM is required by an external client.  This mode of asynchronous execution is important to support multi-threaded priority-based execution. If events were serviced during the call from the client, the FSM would inherit the thread priority of the client. This would make performance harder to evaluate. Instead, in a complex application with multiple interacting FSM, all FSM should be updated under one scheduler, using some ordered execution scheme, which could be tied to a hardware interrupt. This can ensure that all FSM are updated in lock step.

To get this asynchronous external updating behavior, we use the getFSMPolicy() method to access the current FSM policy, and then modify the update technique using the CFMSPolicy enumeration:  CFSMPOLICY::FSM_ASYNCHRONOUS.  The timing of the updates is determined externally to the FSM.

main()

{      CPlayer player;

       player1.setupFSM();

       player.getFSMPolicy()->setFSMUpdateTechnique(CFSMPolicy::FSM_ASYNCHRONOUS);

       player1.Start();

       …

}

For the simple threaded FSM updating, we use the getFSMPolicy() method to access the current FSM policy to be asynchronous.  We then modify the update technique using the CFMSPolicy enumeration:  CFSMPOLICY::FSM_THREADED_ASYNCHRONOUS to get asynchronous threaded updating in a background thread defined by the CFiniteStateMachine, which is then responsible for invoking the updateFSM() method that causes the FSM to services events.  Similarly, to set the approximate time period of updates, the method setPeriod is invoked with the period in milliseconds. The period is only used as a waiting period using ::Sleep between updates.

main()

{      CPlayer player;

       player1.setupFSM();

       player.getFSMPolicy()->setFSMUpdateTechnique(CFSMPolicy::FSM_THREADED_ASYNCHRONOUS);

       player.getFSMPolicy()->setPeriod(10);

       player1.Start();

       …

}

For a more exact timing, the FSM_TIMED_ASYNCHRONOUS is used as above, but in this case, the period is determined by a more exacting hardware timer. This CFiniteStateMachine background threading uses an elapsed Windows hardware timer to trigger an update.

In either case, the FSM update is now run automatically in a background thread, with minimal additional programming. The methods startThread and stopThread methods are available to coordinate the FSM threaded updating. In addition, the methods suspendThread and resumeThread are available to temporarily change the status of the background FSM updating.

main()

{      CPlayer player;

       player1.setupFSM();

       player1.Start();

       tank.startThread(); // start FSM updating, in background thread

       player.Start();

       …

       player.Stop();

       …

       player.stopThread();  // stop FSM from updating, kill thread

}

 

FSM Library Advanced Capabilities

Advanced FSM programming capabilities are available to customize programming logic, including:

1.      Conditional State Transition Logic

2.      Events with parameters

3.      Nested FSM

4.      Overriding State Update Behavior

5.      State Triggers

This section will explore these topics using the CD player example.

Conditional State Transition Logic

A guardian or guard condition is an expression specifying a condition, which must be satisfied before a state transition is to take place. Guardians offer the opportunity to program conditional if/then/else style branching leaving a single state per a given event. In UML, the result of the guardian branching depends on whether the guard conditions are mutually exclusive, the construct may represent conditionality or concurrency. Currently, the FSM library does not allow concurrency through multiply active states – only one state can be active at a time.

As an example of a guard condition, we will modify the FSM logic by checking to see that the player contains a CD before playing. This corresponds to adding a guard condition containsCD() to the StartTransition before the transition can “fire.” This corresponds to the following UML state chart notation:

 

Without conditions, when the CFiniteStateMachine is in a given state and gets an event that matches one in its state transitions, it always executes the action, and changes to the next state. Now with conditions guarding the event from automatically triggering a transition while in a given state, the transition will occur only if the guardian condition is true. Guardian conditions provide a mechanism to refine transitions for the same event.  The start transition now is based on a condition:

       BOOL containsCD()

       {

              return (slot!=empty);

       }

Conditions must conform to a BOOL condition(); signature. This condition is now added to the state STOPPED start transition, by using the CFSMTransition addCondition method. In the method, we define conditions by supplying the following parameters to the CFSMCondition(CFSMObject * base, FSMConditionFunction condition, String name, bool inverter) constructor:

a)     the instance of the object - (this)

b)     a reference to the method pointer - (&CPlayer::containsCD)

c)     a name for the condition -   “ContainsCD”

d)     and a Boolean to indicate whether to invert the conditional value – false, no don’t invert.

The reason the invert flag is available is to be able to easily use the same condition method to map a complete truth space based on one condition method. The related source code to add the condition is:

void setupFSM(){

       FSMTransition * transition;

       FSMCondition * condition;

       condition = new CFSMCondition(this,

              (FSMConditionFunction) &CPlayer::containsCD),

              "ContainsCD",

              false);

              …

       transition = addTransition("STOPPED",   

                     "start",   

                     "PLAYING”, 

                     this, (FSMObjectFunction)  &CPlayer::startdAction,

                     NULL);

 

       transition->addCondition(condition);

The ability to add transition conditions both simplifies and complicates logic. It is now easier to refine transitions to more adequately meet behavior nuances. Now, we don’t need a different event for each transition, and we can incorporate more internal state information into the event handling. In this manner, we can lump a lot of state information under one state, and selectively choose how and when state transitions occur.

Complications arise because the same event can now be used to form multiple transitions out of the same state. This means that a single event could trigger multiple transitions when more than one of the event and guard condition pairs evaluates to true. (In UML, multiple events triggering results in concurrency – or multiply active states. This could be possible in the FSM library, was not facilitated.) Conversely, we might receive the same event, but never transition due to a repeatedly failing condition.  The multiple potential transitions per event now makes it harder to determine if the state space is covered (i.e., we can get into and out of all states) and whether the sum of the event conditions map the entire logic space.

The CFiniteStateMachine execution event-processing engine has adopted the following approach.  In a given state, the FSM engine checks all transitions in a state to find matching events and then evaluates the conditions associated with these events. However, it only executes the action associated with the first event and true condition transition it finds. As for mapping the entire conditional logic space per event, the programmer is on their own to insure logic correctness.

Events with parameters

Now let’s assume that we have a CD player that can start at any track position on the CD. This corresponds to defining a Start event method that accepts a desired starting track as an integer type. This track number can only trigger a start event, if the track number is less than last track number on the CD. (This of course could be detected by the state and reject starting.) This corresponds to the following UML state chart representation:


To pass the track number, the Start event signature is modified to accept the parameter:

void Start(int track=0)

{     

       processEvent(new CFSMEvent(“start”, 10, new int(track)));

}

This event data is passed through the FSM evaluation to the transition action that can then use it as part of its calculations.  In this case, the startAction uses the data to determine the track position on the CD.

HRESULT startAction(CFiniteStateMachine * fsm, CFSMEvent * event)

    {

       int track = *(int *) event->getParameterRef();

       MDI.start(track);

       return S_OK;

    }

 

In some cases, it may be necessary to pass a considerable amount of data. Suppose we want the CD player to provide programmable track play. Now, we must pass a track list to the playAction and the playingActivity.

In this case, we may find it easier to define a new event class that is a specialization of CFSMEvent.  We declare the class FSMTrackListEvent as derived from FSMEvent. We use the DECLARE_DYNAMIC operator to enable the MFC Run-time type information (RTTI) that allows the type of an object to be determined during program execution. We use the Standard Template Library (STL) vector to create track list.

class FSMTrackListEvent : public FSMEvent

{

  DECLARE_DYNAMIC( CFSMFailedEvent )

  typedef vector<int> tracks;

  FSMTrackListEvent(tracklist tracks);

};

When the startAction action receives the event, we use the RTTI facility to determine the event type that has been passed. This type determination is possible since all FSMObjects derive from CObject, which provides a member function IsKindOf. The argument to IsKindOf is a CRuntimeClass object, which you can get using the RUNTIME_CLASS macro with the name of the class. Using IsKindOf, we can determine if the event belongs to the FSMTrackListEvent or a generic FSMEvent class. If a track list event was passed, we save the track list. If not, we create a track list starting from the track given in the generic event.

class FSMTrackListEvent : public FSMEvent

{

  DECLARE_DYNAMIC( CFSMFailedEvent )

  typedef vector<int> tracklist;

  tracklist tracks;

  FSMTrackListEvent(tracklist tracks);

  int attrack;

HRESULT startAction(CFiniteStateMachine * fsm, CFSMEvent * event)

    {

       FSMTrackListEvent event;

       if(event->IsKindOf(RUNTIME_CLASS(FSMTrackListEvent)))

       {

              FSMTrackListEvent * trackevent= (FSMTrackListEvent *) event;

              tracks= trackevent->tracks;

       }

       else {

              int track = *(int *) event->getParameterRef();

              tracks.clear();

              for(int i=track; i<CD.max(); i++) tracks.push_back(i);

       }

       if(track.size()>0) MDI.start(track[0]);

       attrack=1;

       return S_OK

    }

};

The playingActivity uses the tracklist (either sequential or programmed) to sequence the remaining tracks through the CD player, until all tracks have been played.

HRESULT playingActivity()

    {

       if(MDI.Finished())

       {

         if(attrack<tracks.size())

               MDI.start(tracks[attrack++]);

         else queueEvent(“done”);

       }

       return S_OK;

    }

 

 

Nested FSM

So far, the conceptually simple “flat” FSM has been sufficient to handle the states in the CD player. A flat FSM is self-contained and contains no references to other FSM. Although the flat FSM offers a simple approach to programming the control logic where all states are at the same level of abstraction, its efficacy diminishes as complexity increases. Flat FSM can suffer from a combinatorial growth of state transitions, such as in the case where every normal state has a state transition to an error state in the FSM.

To simplify matters, the notion of hierarchical state or nested FSM is adopted as an integral part of the FSM library.  A hierarchical FSM allows a state within the FSM to be embodied by a lower-level FSM. The current state of such a hierarchical state can be described by a nesting of states where the FSM is simultaneously "in" all of these states, depending on the level of abstraction under consideration. States that are not nested FSM are called leaf states.

The hierarchical FSM can be used to simplify FSM state combinatorics by nesting all the states that have common transitions inside a higher-level running state.   Suppose the CD player can be paused while playing. This means that both the playing and paused states can be stopped. A new “Play” higher-level state can be created to simplify managing the stop transition out of these states. This new hierarchical Play state is represented in the following UML state chart:

The code to turn a flat FSM into a Hierarchical FSM involves declaring (or implementing) a nested FSM and then using this nested FSM as a state in the high-level FSM. There are two approaches to implementing the nested FSM state.

1)     Derive a separate FSM class. This method is advantageous if you want to reuse the nested FSM as a state in a number of different higher-level FSM. For the Play state, we define CPlayState derived from CFiniteStateMachine, which contains state transitions between the Playing and Paused states that are coded as a described previously:

class CPlayState : public CFiniteStateMachine

{

       CPlayState (){

              setName("PlayState");

       }

       void setupFSM(){

              setFirstState("PLAYING");

              addTransition("PLAYING",   

                     "pause",   

                     "PAUSED", 

                     this, (FSMObjectFunction) &CPlayState::pauseAction,

                     NULL);

              addTransition("PAUSED",   

                     "play",   

                     "PLAYING", 

                     this, (FSMObjectFunction) &CPlayState::resumedAction,

                     NULL);

              addActivity("PLAYING", “do”

                     this, (FSMObjectFunction) &CPlayer::playingActivity); 

       }

};

The second step involves creating an instance of the CPlayState as nested FSM and using it as a hierarchical state within the setupFSM method. Within setupFSM, the method addFSM is used to incorporate the playstate instance as a state. This allows any reference to the PLAY state to use the nested FSM instance. The internal CFiniteStateMachine engine will understand how to use the nested FSM as a hierarchical state.

class CPlayer : public CFiniteStateMachine

{

       CPlayState playstate;

       CPlayer (){

              setName("Player");

       }

       void setupFSM(){

              playstate->setName(“PLAY”); // set name to state name we want

              playstate->setup();

              setFirstState("STOPPED");

              addFSM(playstate); // add the nested FSM playstate

              addTransition("STOPPED",   

                     "play",   

                     "PLAY", 

                     this, (FSMObjectFunction) &CPlayState::playAction,

                     NULL);

              addTransition("PLAY",   

                     "stop",   

                     "STOPPED",

                     this, (FSMObjectFunction) &CPlayState::stopAction,

                     NULL);

       }

};

Declare and setup the nested FSM in the higher-level FSM. This involves combining the setup of both the lower and higher FSM into the higher-level FSM. This approach is advantageous for consolidating code.

class CPlayer : public CFiniteStateMachine

{

       CPlayState playstate;

       CPlayer (){

              setName("Player");

       }

       void setupFSM(){

              // First the lower level FSM

              playstate->setName(“PLAY”); // set name to state name we want

              playstate->setup();

              playstate->setFirstState("PLAYING");

              playstate->addTransition("PLAYING",   

                     "pause",   

                     "PAUSED", 

                     this, (FSMObjectFunction) &CPlayState::pauseAction,

                     NULL);

              playstate->addTransition("PAUSED",   

                     "play",   

                     "PLAYING", 

                     this, (FSMObjectFunction) &CPlayState::resumeAction,

                     NULL);

              playstate->addActivity("PLAYING", “do”

                      this, (FSMObjectFunction) & CPlayer::playingActivity);

              // Now the higher level FSM

              setFirstState("STOPPED");

              addFSM(playstate); // add the nested FSM playstate

              addTransition("STOPPED",   

                     "play",   

                     "PLAY", 

                     this, (FSMObjectFunction) &CPlayState::playAction,

                     NULL);

              addTransition("PLAY",   

                     "stop",   

                     "STOPPED",

                     this, (FSMObjectFunction) &CPlayState::stopAction,

                     NULL);

       }

};

 

Regardless of the approach to nested FSM implementation, using a CPlayer FSM instance is done as before: 

main()

{     

       CPlayer player;

       player.setupFSM();

       player.Start();

}

Now, upon service of Start event from the client, the CPlayer FSM transitions to the Play hierarchical state, which in turn transitions to the CPlayState FSM initial state, PLAYING.

Customizing State Behavior

Often, it is desirable to modify class functionality without modifying source code. The FSM library provides a couple ways to extend the functionality to state behavior, by either overriding the CFSMState base class methods or using hook functions.  Usually these extensions would be for adding diagnostics or logging capabilities not built into the basic state class. However, it is possible to integrate multiple classes as a state. For example, one could develop a separate “Command” class derived from CFSMState that integrates information from superior objects, subordinate objects and peer objects. The Command class could itself be a FSM.

Overriding State Update Behavior

Overriding base class virtual methods in a derived class is an essential object-oriented technique. It provides an easy way to extend or change functionality without messing with the original source code. Applying this technique to customize the state behavior offers much flexibility. Three CFSMState methods are virtual that allow overriding behavior:

1.    virtual HRESULT handleTransition(CFSMTransitionPtr transition, CFSMEventPtr event, CFSMStatePtr& nextState);

2.    virtual HRESULT updateState(CFSMEventPtr event, CFSMStatePtr& nextState);

3.    virtual HRESULT updateState(CFSMStatePtr newstate, CFSMStatePtr& nextState);

On its own, the CFSMState offers many options, such as logging and timing. However to easily change the logic, it is possible to override the handleTransition method and add application-specific functionality.

As an example, we will customize the state behavior of the CD Player so that we dump diagnostic information whenever we execute within a given state. The first step is to derive a new class CFSMDerivedState from CFSMState:

class CFSMDerivedState : public CFSMState

{

public:

       CFSMDerivedState(char * name) : CFSMState(name) {}

       void preUpdateProcess(CFSMEventPtr event)

       {

              printf("State %S Preprocess: Event %S\n",

                      (BSTR) getName(), (BSTR) event->getName());

       }

       void postUpdateProcess(CFSMStatePtr &nextState)

       {

              printf ("State %S Postprocess: Transition to State %S\n",

                      (BSTR) getName(),(BSTR) nextState->getName());

       }

 

       virtual HRESULT handleTransition(CFSMTransitionPtr transition,

               CFSMEventPtr event, CFSMStatePtr &nextState)

       {

              preUpdateProcess(event);

              HRESULT hr = CFSMState::handleTransition(transition, event, nextState);

              postUpdateProcess(nextState);

              if(FAILED(hr)) throw "Failed Updating State";

              return hr;

       }

};

typedef SmartPointer<CFSMDerivedState> CFSMDerivedStatePtr;

 

To use this new State class, we will modify the setup behavior to use the CFiniteStateMachine method addState that accepts a state. This will mean that the underlying FSM machine will not automatically create a new CFSMState from the STOPPED or PLAYING strings, but instead will use the CFSMDerivedState class instance.

void fsmSetup()

{

       fsm.setFirstState(L"STOPPED");

       ...

       fsm.addState((CFSMState *) new CFSMDerivedState("STOPPED "););

       fsm.addState((CFSMState *) new CFSMDerivedState("PLAYING "););

       fsm.addTransition("STOPPED",    

              "start", 

              "PLAYING", 

              new CFSMAction(this,

                     (FSMObjectFunction) &CPlayer:: playingAction, L" playingAction"));  

       ...

}

             

Of course, this state-based dumping of diagnostic can be done on a selective basis when developing software, so that only the state PLAYING could be monitored.

Simple Update Action State

The ability to install hook functions as pre or post updating actions is available and allows users to easily monitor the behavior of a state. This capability can be used for diagnostic purposes, or to integrate additional functionality into a specific state. Hook functions, installed using setPreUpdateAction or setPostUpdateAction, are called every time the CFSMState method updateState is invoked by a FiniteStateMachine. As a result, during the update process every state update triggers a call to the installed application’s hook function. 

Using the CD player example, one can install hooks by first getting the state instance for PLAYING, using the getState method within the FSM, and then setting the hook, using the state setPreUpdateAction or setPreUpdateAction method, to add a FSMvoidFunction type CFSMAction to be invoked every time the state is updated.

HRESULT prePlayUpdateAction()

{

   AfxMessageBox("CD Player preUpdate Action ");

   return S_OK;

}

CFSMStatePtr state= getState("PLAYING");

state->setPreUpdateAction(new CFSMAction(this,

              (FSMvoidFunction)  &CPlayer::prePlayUpdateAction,

               L"prePlayUpdateAction"));

state->setPostUpdateAction(new CFSMAction(this,

               (FSMvoidFunction)  &CPlayer::postPlayUpdateAction,

               L"postPlayUpdateAction"));

 

State Triggers

A CFMSTrigger specifies a mechanism for which Events are fired. (In UML, a trigger combines a Transition and an Event.) CFMSTrigger are added to states or FSM and are evaluated upon every state or FSM update. When the triggering mechanism is true, actions associated (such as issuing an new event) are executed. The types of triggers include:

1.      Condition – triggering mechanism based on a guardian condition evaluating to true, defined by type EVAL.

2.      Timeout  – triggering mechanism based  on a timer expiring, defined by type AFTER.

3.      Deadline – triggering mechanism based  on when the wall clock matches or exceeds a given deadline time, defined by type AFTER.

Below are examples of each of the triggers applied to the CD player. The EVAL trigger  illustrates that a CD Preview Timeout trigger that could be added to cause a “next” event to trigger a transition to the next track on the CD after a certain amount of time has elapsed. The AFTER trigger illustrates the invoking of a reminderAction to take out the trash after 1200000 milliseconds or 20 minutes. The WHEN trigger illustrates the use of a trigger to remind the listener to clean the CD head after one hour (to have the cleanest CD head around.)

         

   BOOL timeoutTrigger()

   {

          CFSMStatePtr state = getState("PLAYING");

          double elapsed = state->m_timing.elapsed();

          CLogging::Log.log("CD  Time Elapsed %f \n", elapsed);

          if((n>4) && (preview)) {

                 CLogging::Log.log("CD Preview Timeout trigger\n");

                 n=0;

                 handleEvent("next");

          }

          return S_OK;

   }

    // Add EVAL trigger to trigger state timeout.

      

       state->addPreTrigger(new CFSMTrigger(CFSMTrigger::EVAL,

               new CFSMCondition(this, (FSMConditionFunction)

               &CPlayer::timeoutTrigger)));

  

    // Add AFTER trigger to timeout to remind listener to take out the trash.

 

       state->addPreTrigger(new CFSMTrigger(CFSMTrigger::AFTER,

              1200000 /* milliseconds*/,

               new CFSMAction(this,

                     (FSMvoidFunction) &CPlayer::reminderAction,

                     L"reminderAction")));

                

    // Add WHEN trigger to timeout to remind listener to clean the CD head.

                             

       cleanreminder=state->addPreTrigger(new CFSMTrigger(CFSMTrigger::WHEN,

       CTime::GetCurrentTime() + CTimeSpan( 0, 1 /*hr*/, 0 /*min*/, 1 /*sec*/),

       new CFSMAction(this,

              (FSMvoidFunction) &CPlayer::cleanReminderAction,

               L"cleanReminderAction")));

 

 

 



[1] This activity corresponds to the “do” internal state action found in UML state chart notation.