To Tonya in her birthday
NOTE
JAVA IMPLEMENTATION OF HIERARCHICAL STATE MACHINE
INTRODUCTION
The article supposes reader knows and accepts the main principals of implementations of hierarchical state machine (HSM) that was described by Miro Samek in [1] (see details in www.quantum-leaps.com). Terminology introduced in this book is used here. Dr. Miro Samek describes implementation of HSM in C and C++, one of possible implementation in Java is given below. Some differences from original description of HSM [1] is introduced in this article:
1. The set of events used in original is split to external events (actions are coming into HSM from outside) and inner events that are controlling of HSM functioning. External events are being processed by event's dispatcher and directed to active state object of HSM the same way as in original. However, inner events used for transition operations in original are avoided here.
2. A state's objects of HSM are linked in state's graph (state's tree) that represents hierarchy of behavior inheritance. There is special super class
Path for State class for implementation of the state's tree. 3. Procedure of transition is changed and uses state's graph for moving from one state to other.
4. Entity of
Hsm class is changed too. The class Hsm has a super class State , so HSM is a one of a state, a root state, and behavior of this state is inherited the all other states of HSM. CLASS DIAGRAMM OF JAVA HIERARCHICAL STATE MACHINE
CLASSES DISCRIPTION
Before we start to discuss of structure of the classes, let see to the simple example of HSM from book [1] above mentioned.
The example of HSM showed on Fig. 2 can be represented as 3-D figure, like pyramid, consisted of state objects. A base of the pyramid is a root state (S0), higher states inherit property from lower ones. We can draw a diagram of behavioral inheritance of state as analogy of class diagram (see Fig. 3). Therefore, we have a graph - state tree of HSM that defines functionality of the object. An external events coming to the HSM are falling to the current state and then its are distributed to the lower state (parent state in the tree) along branch of state tree while the event will be absorbed by some state or will reach a root node of state tree. We can say that events fall to the state tree as a drop of rain.
In the same way, a transition from one state of HSM to other one have to move along branches of the tree. We always can find a path from any node to any node of tree, as proved in Graph theory. If both nodes (source and target) are placed on the same branch the path is very simple and if nodes are placed on different branches path consist of two parts: first, one is a segment from source node to a node that is common for both branches (least common ancestor - LCA). Second part is a segment from common node to target node. When a transition moves down to the root of tree, the moving is accompanied by execution of exit() method of the state that we leave at the top. And vice versa when a transaction moves up, the moving is accompanied by execution of enter() method of the state that we leave at the bottom.
Path CLASS In order to completely describe the state tree of HSM, each state has to keep reference to its parent node in the tree. However, for faster operating of HSM it is better to keep the array of references to all parent's nodes from the given node to the root of the tree. In this article
Path class implements this thesis. The class has fields: level and path[]. Level is a distance to the root node, path[] is array of a parent references to nodes from root to the given state object. Class constructor accepts as a parameter reference to the parent state of HSM, constructor without parameter creates root node of HSM (this is HSM itself). Code of the class methods is simple and clear without comments.State CLASS State class extends Path and has no own fields. However, the class has four abstract methods: enter(), exit(), fireEvent(), fireInit() those have to be implemented in a subclass. These methods are invoked by HSM during operating and interaction between states. Fig. 4 shows graphical representation of the method invocation for a state object.
enter() is called by HSM when one is coming to this state (or pass this state to its daughter state). exit() is called when HSM leaves this state (previously leave the all its daughter's states). fireEvent() is called by HSM's dispatcher when HSM is processing external event. When state object finish to process of the event it can return to dispatcher either null or reference to its parent. null is a signal to HSM that given state absorbs the event and does not need further to process the event. Parent reference is signal to deliver the event to parent state. fireInit() is called by HSM when the given state becomes a current state of HSM. After this method completes an initialization, the method returns either null or reference to some daughter state. null is a signal to HSM that initialization process is over, the reference is a signal for HSM to turn into daughter state and make it current state. Let see to
transition() method in details. There are three states involved in transition operation (see Fig. 5). First state is a current state of HSM in the moment. Second state is a source state that was reached during processing of an event and it activates this transition now. Third state is a target state that will be a current state after this transition. First, HSM has to change its state from current state to source state (from 'f' to 'e' in fig. 5). Note that source state is always lower (or equals) current state, because source state is a finite point of an event spreading and events always move from top to bottom of state tree. While HSM makes this moving, the states left by HSM execute
exit() methods. Then HSM has to move to target state. There are two alternatives. The first is when both nodes (source and target states) are placed on the same branch of the tree, HSM moves to target executes enter() (moving up) or exit() (moving down) methods of passing states. The second alternative is when source and target nodes belong different branches of the tree. In this case HSM moves down to nearest common node of both branches or LCA , ('a' node is a LCA) from 'e' via 'c' to 'a' in fig. 5 and executing exit() for each node. Then HSM moves up to the target node 'd' (executing enter() for each node). The rule is simple: when HSM leaves a state and goes down there is execution of exit() for the state. When HSM comes to a state from bottom (goes up) an execution of enter() for the state is needed. Hsm CLASS Hsm class extends class State and has currState field that keeps reference to the current state of state machine. init() method is invoked only one time before any operations with HSM. It sets state machine instance (a root of state tree) as a current state and executes enter() and setState() for one. Method setState() performs setting of HSM in current state (argument of the method) and then executes fireInit() for this state. If it returns reference to the daughter state, the one becomes a current and enter() is executed for it. This procedure is repeated for new state (moving up along state tree). Method
dispatchEvent() performs processing of an events coming to HSM. The first who accepts event is a current state and execute fireEvent() method. It returns reference to the parent state in the state tree. The processed event passes to next state down along tree. If the state absorbs event it returns null and processing of the event is over. Test CLASS Test class implements HSM that was described in [1], and has a little difference from original. 27 January 2004
REFERENCES
1. Samek, Miro. Practical Statecharts in C/C++. Quantum Programming for Embedded Systems. CMP Books, 2002, ISBN: 1-57820-110-1
CODE
You can download this code and use it without any restriction.
File Event.java
package statemachine.hsm; import java.util.EventObject; /** * This class represents event for Hierarchical State Machine (HSM) * @author Alexei Krasnopolski (krasnop@bellsouth.net) */ public class Event extends EventObject { private final int id; public Event( Object o, int id ) { super( o ); this.id = id; } public int getID() { return id; } } File Path.java
package statemachine.hsm; /** * This class represents a graph of state behavioral inheritance * * @author Alexei Krasnopolski (krasnop@bellsouth.net) */ class Path { /** * reference to the root node of HSM state tree. It is common for all states of HSM */ Hsm root; /** * Reference to the parent node for this node in tree hierarchy */ State parent; /** * Reference to the daughter node for this node in tree hierarchy. This helper field trails * moving from leaf nodes to root. This allows make moving back on the same way. */ State daughter; /** * Holds the index of a node's level in a tree of graph. Highest level equals 0 ( root node of * state machine ) */ int level; /** * Constructor creates instance of path for given instance of State (Path class is superclass of * State). It takes one parameter. The parameter is a reference to the parent node of the current * state in the state tree. */ protected Path( State parent ) { this.parent = parent; daughter = null; level = parent.level + 1; root = parent.root; } /** * Constructor without parameter uses for creates instance of root node. */ protected Path() { parent = null; daughter = null; level = 0; root = (Hsm)this; } /** * Get-method returns reference to the parent node of the instance. */ protected State getParent() { return parent; } /** * Method return parent node of state with <code>lvl</code> level from current path. Moving from * this node to <code>lvl</code>-level node is recording by setting <code>daughter</code> * fields for each processed node. If level of this node lows then <code>lvl</code> method * returns reference to the current instance. * * @param lvl * is level of node in the current path (branch) of SM tree * @return parent state of node lvl level */ State pullDownToLevel( int lvl ) { State tmp = (State)this; // starting position of tmp is this state State prnt; while( tmp.level > lvl ) // if leval of tmp > lvl (the end poin of moving) { // let's move if( (prnt = tmp.getParent()) == null) // does tmp is root? return null; prnt.daughter = tmp; // mark a way we are going: daughter will help us when we go opposit direction tmp = prnt; // next step } return tmp; } /** * Get-method returns reference to the root noode of the path which is common node for all * branches of state node tree of the HSM. */ Hsm getRoot() { return root; }; /** * Get-method returns reference to the level of the instance. */ int getLevel() { return level; }; } File State.java
package statemachine.hsm; /** * This class represents a state of state machine * * @author Alexei Krasnopolski (krasnop@bellsouth.net) */ abstract public class State extends Path { /** * Constructor creates instance of State and invokes a constructor of superclass */ public State( State parent ) { super( parent ); } /** * Constructor creates instance of root state (represents Hierarchical State Machine - HSM) */ public State() {} /** * The method moves HSM from current state to destination state. The given instance is a state * stimulated this transition. */ protected void transition( State destination ) { int sourceLevel = this.getLevel(); // level of source node of the transition // Moving along path of tree from current state to root of the tree. // Note <code>this</code> is reference to state instance of transition source // For each leaving state execute exit() method. State testedNode = root.getState(); // Adjust destination node: if it far away from root then current state, move it to current state level State destinationNode = destination.pullDownToLevel( testedNode.level ); while( testedNode != null ) { // if( testedNode.getLevel() <= sourceLevel ) // while do not reach of source node level skip a testing // { if( testedNode == destination && this == destination ) // self transition { testedNode.exit(); testedNode.enter(); destinationNode = destination; // needed? break; } if( testedNode == destinationNode ) break; // reach a common node for a current state and a destination state branches // } testedNode.exit(); // execute exit procedure destinationNode = destinationNode.pullDownToLevel( testedNode.level - 1 ); testedNode = testedNode.getParent(); } // Now move from common node to destination node in opposite direction. // Other direction is a reason to execute enter() methods for each state in the path while( destinationNode != destination ) { (destinationNode = destinationNode.daughter).enter(); } // Set SM in the destination state. Check and execute initial transition(s) for given state. root.setState( destination ); } // These abstract methods have to be implemented in subclasses. They are represents // behavior of the state. abstract public State fireInit(); abstract public void enter(); abstract public State fireEvent( Event e ); abstract public void exit(); } File Hsm.java
package statemachine.hsm; /** * This class represents a Hierarchical State Machine (HSM) * @author Alexei Krasnopolski (krasnop@bellsouth.net) */ abstract public class Hsm extends State { /** * Holds reference to current state of HSM */ private State currState; /** * Initializing of HSM. Moving from pseudostate (null) to initial state (this), * root of HSM's state tree. */ public void init() { enter(); setState( this ); } /** * Get-method for current state */ public State getState() { return currState; } /** * Set-method for current state. After setting currState field, we have to check * does have this state init transition? If it has HSM moves to up along branch of state tree. */ public void setState( State dest ) { currState = dest; State daughterNode; while( (daughterNode = currState.fireInit()) != null ) { (currState = daughterNode).enter(); } } /** * Dispatch method for external events. Pass the event to the states. Current state * is always first. After processing event HSM checks return value and in case null * the process is over. */ public void dispatchEvent(final Event e ) { State parentState = currState; Event ee = e; do { // System.out.println(">>> dispatch event " + e.getID() + "; state " + parentState ); parentState = parentState.fireEvent( e ); } while (parentState != null ); // while( (parentState = parentState.fireEvent( e )) != null ); } } File Test.java
package test; import java.io.*; import statemachine.hsm.Event; import statemachine.hsm.Hsm; import statemachine.hsm.State; /** * This class represents a test HSM implementation. * @author Alexei Krasnopolski (krasnop@bellsouth.net) */ public class Test extends Hsm { int myFoo; public void init() { System.out.print("Top-INIT;" ); myFoo = 0; super.init(); }; public State fireInit() { System.out.print("INIT>>s0;" ); return s1; } public State fireEvent( Event e ) { printMessage( e, "s0" ); switch( e.getID() ) { case 5 : transition( s211 ); return null; default : break; } return getParent(); }; public void enter() { System.out.print("ENTRY>>s0;" ); } public void exit() { System.out.print("EXIT<<s0;" ); }; Hsm s0 = this; // This is just alias of current instance State s1 = new State( s0 ) { public State fireEvent( Event e ) { printMessage( e, "s1" ); switch( e.getID() ) { case 1 : transition( s1 ); return null; case 2 : transition( s11 ); return null; case 3 : transition( s2 ); return null; case 4 : transition( s0 ); return null; case 6 : transition( s211 ); return null; default : break; } return getParent(); } public State fireInit() { System.out.print("INIT>>s1;" ); return s11; } public void enter() { System.out.print("ENTRY>>s1;" ); } public void exit() { System.out.print("EXIT<<s1;" ); } }; State s11 = new State( s1 ) { public State fireEvent( Event e ) { printMessage( e, "s11" ); switch( e.getID() ) { case 7 : transition( s211 ); return null; case 8 : System.out.print("myFoo="+myFoo + ";"); if ( myFoo == 1 ) { myFoo = 0; return null; } break; default : break; } return getParent(); } public State fireInit() { return null; } public void enter() { System.out.print("ENTRY>>s11;" ); } public void exit() { System.out.print("EXIT<<s11;" ); } }; State s2 = new State( s0 ) { public State fireEvent( Event e ) { printMessage( e, "s2" ); switch( e.getID() ) { case 3 : transition( s1 ); return null; case 6 : transition( s11 ); return null; default : break; } return getParent(); } public State fireInit() { System.out.print("INIT>>s2;" ); return s21; } public void enter() { System.out.print("ENTRY>>s2;" ); } public void exit() { System.out.print("EXIT<<s2;" ); } }; State s21 = new State( s2 ) { public State fireEvent( Event e ) { printMessage( e, "s21" ); switch( e.getID() ) { case 2 : transition( s211 ); return null; case 8 : System.out.print("myFoo=" + myFoo + ";"); if ( myFoo == 0 ) { myFoo = 1; transition( s21 ); return null; } break; default : break; } return getParent(); } public State fireInit() { System.out.print("INIT>>s21;" ); return s211; } public void enter() { System.out.print("ENTRY>>s21;" ); } public void exit() { System.out.print("EXIT<<s21;" ); } }; State s211 = new State( s21 ) { public State fireEvent( Event e ) { printMessage( e, "s211" ); switch( e.getID() ) { case 4 : transition( s21 ); return null; case 7 : transition( s0 ); return null; default : break; } return getParent(); } public State fireInit() { return null; } public void enter() { System.out.print("ENTRY>>s211;" ); } public void exit() { System.out.print("EXIT<<s211;" ); } }; public static void printMessage( Event e, String stateName ) { char c = 'A'; System.out.print( "Event-" + (char)(c + e.getID() - 1) + ">>" + stateName + ";" ); } public static void main(String[] args) { Test fsm = new Test(); fsm.init(); for(;;) { char c; try { System.out.print("\nSignal<-"); c = (char)System.in.read(); System.in.skip( 2 ); } catch( IOException ioe ) { System.out.print("\nIO error"); break; } if( c >= 'a' && c <= 'h' ) { fsm.dispatchEvent( new Event( fsm, c - 'a' + 1 ) ); } else break; }; System.out.print("\nThe Program Ends"); } } |