Tutorial.md 15 KB

Wayne.Lib.StateEngine Tutorial

Modelling the Microwave oven

We are going to design a Microwave oven simulation, and by that we are going to use the State Engine to implement the behavior.

interface IMicrowaveOven
{
    bool DoorIsOpen{get;}
    void LightOn();
    void LightOff();
    void HeatingOn();
    void HeatingOfff();
}

It can send the following events when something changes:

DoorOpened
DoorClosed
OnButtonPressed

We want to model a very simple microwave oven without timer.

  • When the door is open, or the heating is running, the light should be on.
  • When the door is closed and we press the on-button we should heat.
  • If the door opens during heating, the heating should be stopped.

We can identify four states:

  • InitialState
  • IdleState (Door is closed, and not heating)
  • DoorIsOpenState (Door is open)
  • HeatingState (Door is closed and the heating is on)

Now we are modeling the transitions:

State Event Next state
InitState --- IdleState
IdleState DoorOpen DoorIsOpenState
IdleState OnButtonPressed HeatingState
DoorIsOpenState DoorClosed IdleState
HeatingState DoorOpen DoorIsOpenState

We are now going to translate this into a state-transition table, where the transitions are interpretations of the events in each state. Transitions are also used when we are interpreting the startup of the state machine. When we start the microwave oven, we don't know if the door is open or not, so we have to check that, and from Initstate determine if we should go to IdleState or to DoorIsOpenState.

State Event Interpreted into Transition by state Next state
Initstate --- DoorIsOpenTransition DoorIsOpenState
A --- DoorIsClosedTransition IdleState
IdleState DoorOpen DoorIsOpenTransition DoorIsOpenState
IdleState OnButtonPressed StartCookingTransition HeatingState
DoorIsOpenState DoorClosed DoorIsClosedTransition IdleState
HeatingState DoorOpen StopCookingTransition DoorIsOpenState

The Event is not needed for the state transition table, only the interpretations of it.

Writing the code

We create an enumeration to identify the Events

public enum MicroEvent
{
    DoorOpen,
    DoorClosed,
    ButtonPressed,
    Timer,
}

And a transition definition, also an enumeration

public enum MicroTransition
{
    Init,
    DoorIsOpenTransition,
    DoorIsClosedTransition,
    StartCookingTransition,
    StopCookingTransition,
    ToggleHeater
}

Any object such as a number or a string can be used as identifier for an event or transition type, but enumerations are strongly recommended.

We have an interface to the object that the state should be working with

public interface IMicrowaveOven
{
    event EventHandler OnDoorOpen;
    event EventHandler OnDoorClosed;
    event EventHandler OnButtonPressed;

    bool DoorIsOpen { get;}
    void TurnOnLight();
    void TurnOffLight();
    void TurnOnHeater();
    void TurnOffHeater();
}

Next, we create the state objects.

First, InitState, which should just determine if the door is open or not, and make different transitions depending on that.

public class InitState : InitialState
{
    // The Microwave oven to manipulate
    IMicrowaveOven oven;

    //Constructor receiving oven object from factory
    public InitState(IMicrowaveOven oven)
    {
        this.oven = oven;
    }

    //Must transition directly.
    protected override Transition CreatePseudoStateTransition(StateEntry entry)
    {
        if (oven.DoorIsOpen)
            return new Transition(this, MicroTransition.DoorIsOpenTransition);
        else
            return new Transition(this, MicroTransition.DoorIsClosedTransition);
    }
}

Next, we write the Idle State. Note that we now have a HandleEvent method that receives events. If other event than those expected in this state arrives, we just ignore them. The state does also contain a reference to the microwave oven object that we need to use. This is passed in to the constructor. The calls to RemovePendingEventsOfType method in Enter() is to prevent button press-events that has been created when the door is open. Otherwise they will be queued and sent to Handle event.

class IdleState : State
{
    // The Microwave oven to manipulate
    IMicrowaveOven oven;

    //Constructor receiving oven object from factory
    public IdleState(IMicrowaveOven oven)
    {
        this.oven = oven;
    }

    protected override void Enter(StateEntry stateEntry, ref Transition transition)
    {
        base.Enter(stateEntry, ref transition);
        RemovePendingEventsOfType(MicroEvent.ButtonPressed);
        if (oven.DoorIsOpen)
            transition = new Transition(this, MicroTransition.DoorIsOpenTransition);
    }

    protected override void HandleEvent(StateEngineEvent stateEngineEvent, ref Transition transition)
    {
        if (stateEngineEvent.Type is MicroEvent)
        {
            switch ((MicroEvent)stateEngineEvent.Type)
            {
                case MicroEvent.DoorOpen:
                    {
                        stateEngineEvent.Handled = true;
                        transition = new Transition(this, MicroTransition.DoorIsOpenTransition);
                        break;
                    }
                case MicroEvent.ButtonPressed:
                    {
                        stateEngineEvent.Handled = true;
                        transition = new Transition(this, MicroTransition.StartCookingTransition);
                        break;
                    }
            }
        }
    }

}

The DoorIsOpenState must execute code for both enter and exit of the state in order to turn the light on and off.


public class DoorIsOpenState : State
{
    // The Microwave oven to manipulate
    IMicrowaveOven oven;

    public DoorIsOpenState(IMicrowaveOven oven)
    {
        this.oven = oven;
    }

    //Code run when entering state. Turning on the light.
    protected override void Enter(StateEntry Entry, ref Transition transition)
    {
        oven.TurnOnLight();
    }

    //Handling only one event, door closed, and then we transition.
    protected override void HandleEvent(StateEngineEvent stateEngineEvent, ref Transition transition)
    {
        if (stateEngineEvent.Type.Equals(MicroEvent.DoorClosed))
        {
            stateEngineEvent.Handled = true;
            transition = new Transition(this, MicroTransition.DoorIsClosedTransition);
        }
    }

    //Code run when exiting state. Turning off the light.
    protected override void Exit()
    {
        oven.TurnOffLight();
    }
}

The heating state is much the same, but both light and heater should be turned on.


public class DoorIsOpenState : State
{
    // The Microwave oven to manipulate
    IMicrowaveOven oven;

    public DoorIsOpenState(IMicrowaveOven oven)
    {
        this.oven = oven;
    }

    //Code run when entering state. Turning on the light.
    protected override void Enter(StateEntry Entry, ref Transition transition)
    {
        oven.TurnOnLight();
    }

    //Handling only one event, door closed, and then we transition.
    protected override void HandleEvent(StateEngineEvent stateEngineEvent, ref Transition transition)
    {
        if (stateEngineEvent.Type.Equals(MicroEvent.DoorClosed))
        {
            stateEngineEvent.Handled = true;
            transition = new Transition(this, MicroTransition.DoorIsClosedTransition);
        }
    }

    //Code run when exiting state. Turning off the light.
    protected override void Exit()
    {
        oven.TurnOffLight();
    }
}

Now we need a state factory that we can associate with the state machine so the states can be created correctly.


public class MicroStateFactory : IStateFactory
{
    private IMicrowaveOven oven;

    public MicroStateFactory(IMicrowaveOven oven)
    {
        this.oven = oven;
    }

    public State CreateState(string stateName)
    {
        //Root states 
        if (stateName == typeof(InitState).FullName)
            return new InitState(oven);
        else if (stateName == typeof(IdleState).FullName)
            return new IdleState(oven);
        else if (stateName == typeof(DoorIsOpenState).FullName)
            return new DoorIsOpenState(oven);
        else if (stateName == typeof(HeatingState).FullName)
            return new HeatingState(oven);
        else
            return null;
    }

    public string Name
    {
        get { return "MicroStateFactory"; }
    }
}

Now all we need is to assemble everything, and create a state-transition configuration. We create a class called Configuration that handles the setup of the state transition table here to illustrate it. It does also configure the state-transition table.

static class Configuration
{
 public static void Config(StateTransitionLookup lookup)
 {
   //InitState transitions
   lookup.AddTransition<InitState, IdleState>(MicroTransition.DoorIsClosedTransition);
   lookup.AddTransition<InitState, DoorIsOpenState>(MicroTransition.DoorIsOpenTransition);

   //IdleState transitions
   lookup.AddTransition<IdleState, DoorIsOpenState>(MicroTransition.DoorIsOpenTransition);
   lookup.AddTransition<IdleState, Heating.CompositeState>(MicroTransition.StartCookingTransition);

   //DoorIsOpenState transitions
   lookup.AddTransition<DoorIsOpenState, IdleState>(MicroTransition.DoorIsClosedTransition);

   //HeatingState transitions
   lookup.AddTransition<HeatingState, IdleState>(MicroTransition.StopCookingTransition);
 }
}

Exchanging HeatingState with a compositestate.

A microwave oven does usually not heat all the time, rather it oscillates between heating and resting with a given time interval. We want to extend the Microwave state machine so it can reflect such functionality. We create a new state called Heating.Composite (Heating is an additional namespace). It handles turning on light when entering and turning it right off when exiting. The control over the heater is left to the sub states. The composite state must add a state factory and configure its state for itself.

class CompositeState : Wayne.Lib.StateEngine.CompositeState
{
    // The Microwave oven to manipulate
    IMicrowaveOven oven;

    //Constructor receiving oven object from factory
    public CompositeState(IMicrowaveOven oven)
    {
        this.oven = oven;
        StateMachine.AddStateFactory(new MicroStateFactory(oven));
        Configuration.Config(StateMachine.StateTransitionLookup);
    }

    protected override void Enter(StateEntry stateEntry, ref Transition transition)
    {
        base.Enter(stateEntry, ref transition);
        oven.TurnOnLight();
    }

    public override void UnhandledEvent(StateEngineEvent stateEngineEvent, ref Transition transition)
    {
        base.UnhandledEvent(stateEngineEvent, ref transition);
        if (stateEngineEvent.Type.Equals(MicroEvent.DoorOpen))
        {
            stateEngineEvent.Handled = true;
            transition = new Transition(this, MicroTransition.StopCookingTransition);
        }
    }

    protected override void Exit()
    {
        base.Exit();
        oven.TurnOffLight();
        oven.TurnOffHeater(); // Turn off heater.
    }
}

We create one state for On and one for Off.

class OnState : State
{
    // The Microwave oven to manipulate
    IMicrowaveOven oven;

    //Constructor receiving oven object from factory
    public OnState(IMicrowaveOven oven)
    {
        this.oven = oven;
    }

    protected override void Enter(StateEntry stateEntry, ref Transition transition)
    {
        base.Enter(stateEntry, ref transition);
        oven.TurnOnHeater();
        ActivateTimer(new Timer(this, MicroEvent.Timer, 5000, null));
    }

    protected override void HandleEvent(StateEngineEvent stateEngineEvent, 
ref Transition transition)
    {
        base.HandleEvent(stateEngineEvent, ref transition);

        if (stateEngineEvent.Type.Equals(MicroEvent.Timer))
        {
            stateEngineEvent.Handled = true;
            transition = new Transition(this, MicroTransition.ToggleHeater);
        }
    }
}

class OffState : State
{
    // The Microwave oven to manipulate
    IMicrowaveOven oven;

    //Constructor receiving oven object from factory
    public OffState(IMicrowaveOven oven)
    {
        this.oven = oven;
    }

    protected override void Enter(StateEntry stateEntry, ref Transition transition)
    {
        base.Enter(stateEntry, ref transition);
        oven.TurnOffHeater();
        ActivateTimer(new Timer(this, MicroEvent.Timer, 2000, null));
    }

    protected override void HandleEvent(StateEngineEvent stateEngineEvent, 
ref Transition transition)
    {
        base.HandleEvent(stateEngineEvent, ref transition);

        if (stateEngineEvent.Type.Equals(MicroEvent.Timer))
        {
            stateEngineEvent.Handled = true;
            transition = new Transition(this, MicroTransition.ToggleHeater);
        }
    }
}

These are configured in the configuration for the composite state.

static class Configuration
{
    public static void Config(StateTransitionLookup sl)
    {
        sl.AddTransition<InitState, OnState>(MicroTransition.Init);
        sl.AddTransition<OnState, OffState>(MicroTransition.ToggleHeater);
        sl.AddTransition<OffState, OnState>(MicroTransition.ToggleHeater);
    }
}

We also modify the Configuration for the root state machine, so we reference the Heating.Composite state instead of HeatingState.

public static void Config(StateTransitionLookup lookup)
{
    //InitState transitions
    lookup.AddTransition<InitState, IdleState>(MicroTransition.DoorIsClosedTransition);
    lookup.AddTransition<InitState, DoorIsOpenState>(MicroTransition.DoorIsOpenTransition);

    //IdleState transitions
    lookup.AddTransition<IdleState, DoorIsOpenState>(MicroTransition.DoorIsOpenTransition);
    lookup.AddTransition<IdleState, Heating.CompositeState>(MicroTransition.StartCookingTransition);

    //DoorIsOpenState transitions
    lookup.AddTransition<DoorIsOpenState, IdleState>(MicroTransition.DoorIsClosedTransition);

    //HeatingState transitions
    lookup.AddTransition<Heating.CompositeState, IdleState>(MicroTransition.StopCookingTransition);
}

The State factory must also be extended so it can create the new states Heating.Composite, Heating.OnState and Heating.OffState.

Now the microwave oven will oscillate during heating between heating 5 seconds, and resting 2 seconds.