Click here to Skip to main content
15,894,720 members
Articles / Programming Languages / C#
Article

StateProto - Interacting State Machines

Rate me:
Please Sign up or sign in to vote.
4.33/5 (8 votes)
22 Jul 2006CPOL12 min read 32.2K   726   29  
Drawing state diagrams, and C# code generation for the modified QF4Net.

StateProtoNew

Contents

Introduction

In the last article on stateProto, I introduced the mechanisms needed to run one or more instances of a state machine. I also showed some basic animation of the state machine. In this article, the question is - how does one enable different state machine instances to communicate with each other (and possibly with the outside world)?

The sample

While trying to think of a reasonably simple but interesting sample to put together for this article, the ideas always seemed to gravitate towards some physical process - including things like coin operated turnstiles, paid for parking, brake system operation, human-elevator interaction, etcetera. The problem is that these can turn into complex and boring samples.

I decided that as a South African, I'll think about it further while putting some meat on the braai. To light the braai, I bought a small lighter, and this led me to thinking about the various interactions that results in the production of a flame. The simplest model I could come up with includes the following domain objects:

  1. the valve that supplies the fuel,
  2. air which provides the oxygen source,
  3. the resulting fuel/air mixture
  4. and the flint which provides the spark to ignite the fuel/air mixture.

TopOfSampleApp1

Controls of the demo

Ports - The means of communication

FuelMixtureHsm

Fuel mixture HSM (see ports on the left hand side)

State machines can indicate sources and sinks of events with the use of ports. A port is a placeholder to which another state machine can be attached that will enable the two state machines to interact.

FuelMixtureHsm_FlintPort

Flint port of FuelMixture HSM

One state machine can send a message to another state machine via the port that represents the target state machine, via the following expression: ^TargetStateMachinePort.SignalName(ArgumentToTheSignal). For example, when the Flint state machine wants to send a message to the FuelMixture state machine, then it sends ^FuelMixture.Ignite() (note that the ^ means send, FuelMixture is the port, and Ignite is the signal to be sent - without any arguments).

FlintHsm_FuelMixturePort

Flint HSM: notice the FuelMixture port. Also, in the Sparking state, notice the ^FuelMixture.Ignite() in action.

If you do not understand the terms State, Signal, and Action, then please refer to Miro Samek's article referenced below.

Ports work as complementary pairs between state machines - if StateMachine Flint wants to send a message to StateMachine FuelMixture, then Flint might have a FuelMixture port and FuelMixture might have a Flint port. It is important to note that the actual naming of the ports is not really of importance here - as we could have called the Flint port the IgnitionSource port instead.

C#
1      class Flint : LQHsm {
2          IQPort FuelMixture;
3      }
4      class FuelMixture : LQHsm {
5          IQPort Flint;
6      }
7

What is useful about this pattern of using ports as intermediaries is that we can swop out one Flint implementation for another without FuelMixture being any the wiser. Also, at a later stage, the two state machines could be running in separate processes or even on different machines, and neither would be the wiser (assuming latency is not an issue). While not necessarily very useful for Flint and FuelMixture - the extensibility of this mechanism will be of use to us in the future.

A port is a concrete implementation of an IQPort interface. This interface is defined as follows:

C#
 1      /// <summary>
 2      /// IQPort - single state machine accessor.
 3      /// </summary>

 4      public interface IQPort
 5      {
 6          string Name { get; }
 7          void Send (IQEvent ev);
 8          event QEventHandler QEvents;
 9
10          void Receive (IQPort fromPort, IQEvent ev);
11      }
12
13      public delegate void QEventHandler (IQPort port, IQEvent ev);

This interface can be split into two behavioural components.

  • The eventing portion that is used by the sending state machine. Send is called which results in the QEvents event sink being called - any code that registers with QEvents will then receive the event being sent.
  • C#
    1          void Send (IQEvent ev);
    2          event QEventHandler QEvents;
    
  • It just so happens that the Receive method fits the call signature of the QEventHandler delegate perfectly. All that is necessary is for the state machine on the other side to register its complementary port's Receive method to the sender's QEvents sink.
  • C#
    1          void Receive (IQPort fromPort, IQEvent ev);
    

Thus, for Flint and FuelMixture above, we can link the two up as follows:

C#
 1      Flint flint = new Flint("flint1", lifeCycleManager);
 2      FuelMixture fuelMixture = new FuelMixture("mix1", lifeCycleManager);
 3
 4      // Setup flint to be able to call FuelMixture.Send(ev1)
 5      // This will result in fuelMixture's Flint.Receive() being called
 6      // with the message ev1.

 7      flint.FuelMixture.QEvents += new QEventHandler(fuelMixture.Flint.Receive);
 8
 9      // Setup fuelMixture to be able to call Flint.Send(ev2).
10      // This will result in flint.FuelMixture.Receive() being called
11      // with the message ev2.

12      fuelMixture.Flint.QEvents += new QEventHandler(flint.FuelMixture.Receive);
13
14      // activate the two state machines

15      flint.init();
16      fuelMixture.init();

As soon as Receive is called, the message gets placed on the receiving port's owning state machine's queue - with a message that is qualified with the source information. It is even possible to use ports for sending events to and receiving events from some external (non state machine) actor in the system.

Sending a message from one State Machine to another

The simplest way to send a message from one HSM to another is to get a direct reference to the other HSM and call its AsyncDispatch(ev) method. The problem with this approach is modularity. Direct access to the second HSM might cause the developer to attempt to directly call exposed properties, etc., on this instance. A port prevents such mishaps. Also, a direct HSM reference would, at a later stage, make it more difficult to isolate the interacting state machines by placing each into their own application domains (or even into separate process spaces on different machines).

The answer is a simple interface defined by the port which takes on the role of a pipe for data transmission. To keep things simple, each individual port supports data flow in one direction only.

The lighter

Starting up

EnabledCreateButton

Enabled Create button

On starting the sample application - you will find yourself presented with a simple GUI with one enabled button "Create Ligther Elements".

Creating

On clicking this button - it will become disabled - and all the other event input buttons will subsequently be enabled.

ElementsCreated

Event input buttons are all enabled

The elements of the lighter scenario will be in their default startup states. The image on the right hand side will be that of an unlit lighter.

StartupStateOfElementsOfLighterScenario

Default startup state of elements

You will notice that the states in the diagram all start with S_. This is, in fact, the method name in the generated code. I will leave this out in the text as I discuss these state names.

  • FuelMixture - Active_NoFuel /fuel=0 /air=0 -- Fuel/Air Mixture is in the active state but no fuel is present.
  • Air Flow - Still/0 -- The air is currently still with a flow rate of 0 m/s.
  • Valve - Closed/0 -- The valve is in a closed position with a resulting 0 flow rate.
  • Flint - NoSparks -- The flint is not being struck, and as a result, has no sparks.

Some things to note (although this is probably stating the obvious):

  1. Even if the flint were to be spun and thus creating sparks - there is no fuel mixture - so no flame will result.
  2. If the Valve Switch was pressed - fuel would start flowing with a default flow rate of 10m/s (the middle fuel flow rate in this app).
  3. If the air flow rate is faster than the fuel flow rate, then the fuel would be dissipated and no flame would result if sparks were to fly.
  4. If a flame was burning and the air flow got too fast or the fuel flow rate is reduced, then the flame would go out.
  5. If the air pressure were increased, then the air flow would reach "gusty" conditions. This would mean that the flow rate would, on average, be higher than mere "drafty" conditions.
    • Also, ever so often, a gust of higher intensity would come by.
    • So, even if the fuel air mix might be surviving the faster gustier flow rates - it might not survive these interim turbulent gusts, and would be temporarily depleted of fuel.
    • Very irritating conditions to try to light a braai :-).

Opening the valve

Pressing the valve open, "Press Valve Switch" will change the states to look as follows:

Highlighted elements have changed state.

  • FuelMixture - Active_FuelSupplied_Mixed /fuel=10 /air=0 -- Fuel/Air Mixture is in the active state, fuel is being supplied and the mixture is fuel rich (i.e., mixed).
  • Air Flow - Still/0.
  • Valve - Open/10 -- The valve has been opened and the fuel is flowing out at 10m/s.
  • Flint - NoSparks.

Striking the flint now will result in the following change:

  • FuelMixture - Active_FuelSupplied_Burning /fuel=10 /air=0 -- Fuel/Air Mixture is in the active state, fuel is being supplied and we have a flame!
  • Air Flow - Still/0.
  • Valve - Open/10.
  • Flint - NoSparks -- the flint enters the S_Sparking state, and then automatically times out after a random interval, returning to the S_NoSparks state. In fact, the first time I struck the flint, it timed out so fast that it did not even spark. "Sparking" is also initiated by a timer (with an every expression) which could happen a number of times while the flint is in the Sparking state.

LighterHasFlame1

Lighter will remain stable in flame for as long as the valve is open and the air is still.

Less stable conditions...

Hitting the "Increase Air Flow" results in a less stable flame. It will move about in the drafty air - but because the initial fuel flow started at 10m/s - the flame will not go out. Decreasing the fuel flow rate "Decrease Fuel Flow" twice results in a situation where the flame could be blown out by the drafty conditions.

  • FuelMixture - Active_FuelSupplied_Mixed or Active_NoFuel /fuel=8 /air=8 -- The previously burning mixture could have its flame extinguished by the moving air.
  • Air Flow - Moving_Draft/8 -- Even though I show /8 here - this speed is continuously (every second or two) changing.
  • Valve - Open/8 -- Decrease Fuel Flow was clicked twice, which took the flow rate to 8m/s.
  • Flint - NoSparks.

If you were to strike the flint while the FuelMixture was "Mixed" - you might stand a chance that the mixture takes flame - but could soon be extinguished again. Increasing the fuel flow rate would increase your chances at getting a stable flame again.

Even less stable conditions would result if the air flow were to increase to gusty conditions. This implies a higher speed air flow rate. The air flow rate (like its drafty counterpart) will also change randomly every 1 to 2 seconds. But, this higher flow rate is not the only nuance of this air flow state.

Ever so often - a higher (but short lived) turbulent gust would come along, which could temporarily deplete the fuel, but because the valve is still open, and if the normal air flow rate is less than the fuel flow rate, the fuel would be replenished a second or two later. This depletion scenario will have the following states:

  • FuelMixture - Active_FuelSupplied_Mixed or Active_NoFuel /fuel=8 /air=12 /lastgust=0 -- The previously burning mixture could have its flame extinguished by the moving air.
    • Notice the /lastgust=0 which will indicate the value of the last gust of air.
    • At a fuel rate of 8, the mixture would indicate Active_NoFuel.
    • At a fuel rate of more than the current air rate, the mixture would indicate Active_FuelSupplied_Mixed.
    • Note that /lastgust would only update if the fuel flow rate is more than the air flow rate at the time of the gust.
  • Air Flow - Moving_Gust/12 -- The /12 flow rate could go as high as 14. But the interim "gusts" could go up to 18.
  • Valve - Open/8 -- Decrease Fuel Flow was clicked twice, which took the flow rate to 8m/s.
  • Flint - NoSparks.

Moving the fuel flow rate to 20 and striking the flint would result in a flame that will keep on going in this sample (there is no countdown of the fuel amount at the moment).

The frame

The LighterFrame is the class in which the four interacting state machines are created, and through which the user/GUI can interact with them.

The LighterFrame diagram below shows this encapsulation of the state machines within the frame. It also shows the port relations between the various state machines and the direction of signal flow. For example - the "FuelMixture" port on the "Flint" state machine is related to the "Flint" port on the "FuelMixture" state machine via port relation P-1.

LighterFrame

The LighterFrame is a containing class for interaction between the user/GUI and the state machines.

The port relations U-1 and U-2 do not really exist as two ports that are linked via their QEvents. The "User port in the "Valve" state machine, for example, allows us to model a user's interaction with the state machine - while on the user sending side - we call the Receive method on the Valve's User port directly, as show in line 3:

C#
1      public void PressValve()
2      {
3          _Valve.User.Receive (null, new QEvent (ValveSignals.Press));
4      }

The advantage of this is that the generated code is fully qualified within the Valve state machine:

C#
 1      protected virtual QState S_Closed (IQEvent ev){
 2
 3          // ........... other code removed .............

 4
 5          case QualifiedValveSignals.User_Press: {
 6              LogStateEvent (StateLogType.EventTransition, s_Closed,
                               s_Open, "User.Press",
                               "User.Press");
 7              TransitionTo (s_Open, s_trans_User_Press_Closed_2_Open);
 8              return null;
 9          }  // User.Press

10
11          // .......... other code removed ..............

12
13      } // S_Closed

Notice the QualifiedValveSignals.User_Press which means that the state machine is reacting to a more specific event than just Press, but is taking the signal source "User" into account as well.

Valve_User_Press

Valve reacts to the Press event from within the Closed state only if the source is the User port.

On the other hand...

On the other hand - the Air state machine also needs user input to increase and decrease the air speed. It, however, does not have a User port. In order to send it the PressureIncrease signal - the frame simply calls the SigPressureIncrease directly.

C#
1      public void IncreaseAirFlow()
2      {
3          _Air.SigPressureIncrease (null);
4      }

AirHsm1

Air state machine will react to PressureIncrease and PressureDecrease from any source.

Though simpler - this mechanism would have to change if we wanted to simulate the user by adding a "User" state machine into the system. Even so, I showed both methods just for illustration - it is more important that the four state machines interact via their ports. The user to state machine interaction can be done either way.

Summary

Ports make interactions between related state machines cleaner and simpler, while allowing for future enhancements such as process space isolation of individual state machines.

Using ports also means that I can swap out one implementation of the Valve state machine with another - as long as the message protocol is the same.

I tried to choose a fairly simple interaction scenario - but as I developed it - I found that the actual interaction scenarios can become quite complex as each individual element adds variability to their behaviour.

Coming up

  1. Saving the State Machine for later rehydration.

References

History

Third article for the StateProto Beta release showing how state machines send messages to each other.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
South Africa South Africa
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --