Robopupu.FSM API

Robopupu.FSM API provides a simple, easy to use, and light-weight API for implementing Finate-State-Machines (FSM) similar to UML Statemachines. A simple example of a such state machine is represented below as a UML statechart diagram:

simple_state_machine

The state machine above consists of the following elements:

  • States: A, B, C, D, B1, B2, and B3. State B is a composite state containing  substates B1, B2, and B3. State A is the initial state of the presented state machine, and state B1 is the initial substate of state B.
  • Initial points: The diagram contains two initial points: One for the top-level state machine and one for the state B.
  • History point: State B has a history point. If the state B is entered via this history point, the most recent substate is resumed.
  • Choice point: A transition point that has a condition determining which of the states, C or D is entered.
  • Entry point: When state B is entered via an entry point, substate B3 is always entered.
  • State Transitions: toB, toCorD, toB1, toB2, toB3, and toSelf.

FSM API Classes

The following class diagram depicts the classes needed for implemening the example state machine. Classe symbols with white background represent the FSM API classes. Classes with light blue background represent classes written by the developer. The State class with light yellow background is code generated by an annotation processor of the Robopupu Compiler from the TriggerEvents and ControllerContext interfaces. Code generation is provided to reduce the need for writing boiler plate code.

simple_statemachine_classes

FSM API consists of the following six Java types:

StateEngine

The core class of the API. StateEngine is essentially a kind of engine to construct hierarchical states where state transitions are triggered by event methods and where transitions may go through choice points, entry points, and history points. The most important methods for implementing the actual state classes are:

  • onEnter() to define code that is executed when a state is entered,
  • onExit() to define code that is executed when a state is exited, and
  • transitTo(Class) to define state transitions from one state to another.
State

The State class that is code generated by the Robopupu Compiler is extended from class StateEngine. All actual state classes are in turn extended from the generated State class. Most importantly this means that all the state classes written by the developer may override any of the trigger event  methods defined for the code generated State class. It should be noted that the name of the generated class is always “State”, but the package name is taken from the class that you extend from StateMachine.

StateMachine

An abstract base class for state machine implementations. A concrete implementation of StateMachine creates and starts an instance of the generated State class. StateMachine also initialises the instance of State by providing all the dependencies that the instance needs. In the example, there is only one such dependency to be initialised: An instance of Controller class.

StateEngineObserver

An observer interface for listening the events from the execution of a State instance. By default the StateMachine class implements this interface.

@StateMachineEvents

An annotation that is used to declare any interface that is to define methods to be used as event triggers in the actual state classes. In our state machine example, there is only one such interface, the interface TriggerEvents which defines trigger event methods. The annotation processor for the FSM API uses this annotation to trigger code generation of a State class.

@StateMachineContext

This annotation is used to declare an interface which defines the context for the generated State class. In the example, interface ControllerContext is annotated with @StateEngineObserver annotation. The annotation processor for the FSM API uses also this annotation.

StateEngine.Error

An enum that represents the possible error states that may occur in the execution of a StateEngine implementation.

Defining Trigger Events

As already mentioned above, the trigger events triggering state transitions in a state machine are implemented as interface methods. This is a significant difference compared to some existing state machine libraries, where trigger events are typically represented as primitive type literals, enum values, or object instances. The approach where interface methods can be used to trigger state transitions provides the following benefits:

  • Precise and easy debugging of trigger events: Every single state transition in a state machine can be precisely debugged by placing a breakpoint to an event method implementation that represents a particular trigger event to be debugged.
  • Trigger event parameters: Arbitrary parameters can be easily provided for state transitions as parameters to event methods. These parameters can be used by event actions or choice conditions written into event methods.
  • Advanced state machine constructs: Event methods may contain code that is used for implementing UML choice points and entry points. The code written for these transition points can be easily debugged.
  • Event actions: Beside entry and exit actions, a UML state machine may have event actions which are executed when events are received. Event actions can be easily implemented by writing them into event methods.The code written for event actions can be easily debugged.
  • Performance: Since the events that trigger state transitions are implemented simply as method invocations, the state machine implementations have a good performance.
  • Support for unit testing: Event methods and the interfaces used for declaring them can be utilised when writing unit tests for testing the state machine implementation.

The following code provides an example of an interface that defines the trigger event methods for our state machine example. Note that you may use any number of interfaces for declaring the trigger event methods. You just need to annotate your interface with the @StateMachineEvents annotation.

Note that you need to provide the class of your StateMachine implementation as an annotation parameter for the @StateMachineEvents annotation.


package com.robopupu.feature.fsm.statemachine;
import com.robopupu.api.fsm.StateMachineEvents;

@StateMachineEvents(SimpleStateMachine.class)
public interface TriggerEvents {
    void toB();
    void toCorD(int selector);
    void toB1();
    void toB2();
    void toB3();
    void toSelf();
}

 

Providing the Context for a State Machine

A state machine would not be very useful if it would not be able to control or manipulate something with the state entry, state exit, and event actions defined within it. The actual state objects need references to objects they control or manipulate in their actions. In FSM API, the context for a state machine is defined using another interface that is annotated with the @StateMachineContext annotation. In the given example we have the interface ControllerContext that declares a single setter method for injecting an instance of  Controller to the generated State. 

Note that you need to provide the class of your StateMachine implementation as a parameter for the @StateMachineContext annotation:


package com.robopupu.feature.fsm.statemachine;
import com.robopupu.api.fsm.StateMachineContext;

@StateMachineContext(SimpleStateMachine.class)
public interface ControllerContext {
    void setController(Controller controller);
}

An interface that defines the context for the generated State needs to declare a setter for each object type that is to be injected to the instance of State. The annotation processor for FSM API will then code generate the corresponding getter methods into the State class. These getter methods can be then called in the actual state classes derived from the State class.

For instance, the State_A class uses the method getController() of the State class to obtain a reference to the injected instance of Controller. State_A uses the obtained reference to invoke method Controller#onShowMessage(String):


package com.robopupu.feature.fsm.statemachine;

public class State_A extends State {
    public State_A() {
        super(State.class, null);
    }
    @Override
    public void toB() {
        transitTo(State_B.class);
    }
    @Override
    protected void onEnter() {
        getController().onShowMessage("Entered State A");
    }
    @Override
    protected void onExit() {
        getController().onShowMessage("Exited State A");
    }
}

 

Implementing State Classes

The State_A class above is an example of an actual state class derived from the generated State class. In fact, it is a requirement that all the actual state classes written by a developer must be extended from the very same generated State class. In addition, every state class needs to provide a public default constructor that calls the super constructor with the following two parameters:

  1. The parent state defined as a state class. In case of State_A, this parent state class is the generated State class.
  2. The initial substate defined as a state class. The State_A class does not have any initial state since it is not a composite state and therefore does not contain any substates. State_B does have substates, and is required to define the initial substate class.

State_A has entry actions implement in method onEnter(), and exit actions implemented in method onExit(). As shown in the state machine diagram, there is a state transition from state A to state B triggered by event toB. This trigger event is implemented with method toB(). The method uses the framework method StateEngine#transitTo(Class) to execute a transition from state A to state B. 

State is a composite state which has a history point and an entry point. State B is implemented as class State_B  as follows:


package com.robopupu.feature.fsm.statemachine;
public class State_B extends State {
    public State_B() {
        super(State.class, State_B1.class);
    }
    @Override
    public void toCorD(int selector) {
        if (selector == 1) {
            transitTo(State_C.class);
        } else {
            transitTo(State_D.class);
        }
    }
    @Override
    protected State enterEntryPoint(int entryPoint) {
        if (entryPoint == 3) {
            return transitTo(State_B3.class);
        }
        return null;
    }
    @Override
    protected void onEnter() {
        getController().onShowMessage("Entered State B");
    }
    @Override
    protected void onExit() {
        getController().onShowMessage("Exited State B");
    }
}

State B has an entry point, and because of that, it needs to override the framework method StateEngine#enterEntryPoint(int). Parameter entryPoint is an index for selecting the substate to be entered via the entry point. As the state diagram specifies, the entry point is always used to enter state B3. For this reason the trigger event toB which is implemented as State_D#toB() needs to be implemented as follows: The framework method StateEngine#transitTo(Class, int) is invoked using integer value 3 as  a second parameter:


...
public class State_D extends State {
    public State_D() {
        super(State.class, null);
    }
    @Override
    public void toB() {
        transitTo(State_B.class, 3);
    }
...

As depicted in state diagram, there is choice point in state transition from state B to either state C or D. The condition for a choice point can be written into the event method State_B#toCorD(int) which implements the trigger event toCorD. The method contains a condition that uses the int parameter to choose which of the states C or D is selected to be entered. However, that is just how the choice point condition happens to be implemented in this particular example.

Implementing a State Machine Component

When using the FSM API, a developer needs to provide a component that is extended from the framework class StateMachine. The purpose of this component is to encapsulate the logic needed for creating, initialising, and starting the StateEngine implementation that is in fact the code generated class State. Class SimpleStateMachine below provides an example how to implement a StateMachine:


package com.robopupu.feature.fsm.statemachine;
import com.robopupu.api.fsm.StateEngine;
import com.robopupu.api.fsm.StateMachine;

public class SimpleStateMachine extends StateMachine {
    private final Controller mController;

    public SimpleStateMachine(final Controller controller) {
        mController = controller;
    }
    @Override
    public void start() {
        start(State_A.class);
    }
    @Override
    protected void onStateEngineCreated(final StateEngine stateEngine) {
        ((State)stateEngine).setController(mController);
    }
}

As already described, the example state machine has only one object, an instance of Controller, that needs to be injected into the instance of State so that the actual states objects can obtain a reference for the Controller instance. An instance of Controller is first injected to SimpleStateMachine via its constructor. After an instance of the State class is created and started with the initial state State_A in method start() of SimpleStateMachine, the framework calls another framework method, observer method onStateEngineCreated(StateEngine). This observer method is the proper place to inject the instance of Controller into the State instance.

When to Use a State Machine

State machines can be utilised basically whenever there is any state dependent and/or event driven logic that can be encapsulated into form of an implemented StateMachine class. Logic represented as a state machine is easy to understand, maintain, and test. It is a recommend approach to start by drawing a UML statechart diagram for the state machine. Then the actual implementation of the state machine component using Robopupu.FSM API is pretty straightforward.