Java code generator

Java code generator features

Naming feature

The Naming feature allows the configuration of package names for the generated classes (and interfaces) as well as a prefix and/or a suffix for their names.

It is an optional feature and has the following parameters:

  • basePackage (String, required): Package name for the generated Java classes
  • implementationSuffix (String, optional): Name suffix for the implementing classes

Example:

feature Naming {
    basePackage = "org.ourproject.sct"
    implementationSuffix = "Impl"
}

SynchronizedWrapper feature

This feature is deprecated. Use GeneralFeatures { synchronized=true} instead.

The SynchronizedWrapper feature generates a synchronized wrapper for the state machine. This is an additional Java class providing thread-safe access to the generated state machine implementation. As the wrapper adds thread safety any number of client threads may call the state machine.

It is an optional feature and has the following parameters:

  • namePrefix (String, optional): Name prefix for the generated wrapper class
  • nameSuffix (String, optional): Name suffix for the generated wrapper class

Example:

feature SynchronizedWrapper {
    namePrefix = "Synchronized"
    nameSuffix = "Wrapper"
}

RunnableWrapper feature

This feature is deprecated. Use GeneralFeatures { Runnable=true} instead.

The RunnableWrapper feature generates a runnable wrapper for the state machine. This is an additional Java class providing a thread-safe wrapper for the generated state machine implementation. In addition to the specific state machine interface it implements the Runnable interface and can be executed in a thread. It implements an event queue and event-driven execution semantics. As the wrapper adds thread safety any number of client threads may call the state machine.

It is an optional feature and has the following parameters:

  • namePrefix (String, optional): Name prefix for the generated wrapper class
  • nameSuffix (String, optional): Name suffix for the generated wrapper class

Instead of using the RunnableWrapper, you can also use the option InEventQueue if the statemachine uses the eventdriven execution model.

Example:

feature RunnableWrapper {
    namePrefix = "Runnable"
    nameSuffix = "Wrapper"
}

GeneralFeatures feature

The GeneralFeatures feature allows to configure additional services to be generated along with the state machine. Per default, all parameters are false, meaning to disable the corresponding features, respectively.

GeneralFeatures is an optional feature and has the following parameters:

  • InterfaceObserverSupport (Boolean, optional): Enables/disables the generation of listener interfaces for the state machine.
  • RuntimeService (Boolean, optional): Enables/disables the generation of a runtime service that triggers the run cycle of a cycle-based state machine.
  • TimerService (Boolean, optional): Enables/disables the generation of a timer service implementation using java.util.Timer.
  • InEventQueue (Boolean, optional): Enables queuing of in-events in the statemachine (for event-driven execution). You can use this instead of the RunnableWrapper .
  • synchronized (Boolean, optional): Sets all methods where it is appropriate to „synchronized”. This replaces the SynchronizedWrapper .
  • Runnable (Boolean, optional): Makes the statemachine implement java.lang.Runnable. Works only for event-driven statemachines. This implies synchronized and InEventQueue. This replaces the RunnableWrapper .

Example:

feature GeneralFeatures {
    InterfaceObserverSupport = true
    RuntimeService = true
    TimerService = true
}

Tracing feature

The Tracing feature enables the generation of tracing callback functions:

  • enterState (boolean, optional): Specifies whether to generate a callback function that is used to notify about state-entering events.
  • exitState (boolean, optional): Specifies whether to generate a callback that is used to notify about state-exiting events.

Example:

feature Tracing {
    enterState = true
    exitState  = true
}

Specification of Java code

In order to describe the API specifications of the code generated by the YAKINDU Java generator, the explanations below are using the Traffic Light sample state machine, see figure "The Traffic Light sample statechart model" . It models a pedestrian crossing with push-button operated traffic lights („pelican crossing”).


The _Traffic Light_ sample statechart model

The Traffic Light sample statechart model

Generated code files

Generally you will find generated code at the places specified in the SGen model, see section "Outlet" for details.

In the case of the traffic light example, you will find the generated code in the src-gen folder.

The package org.yakindu.sct.examples.trafficlight.cyclebased contains the general state machine interfaces and classes. They are needed by each particular state machine and are independend from concrete ones.

The state machine interface

Each generated state machine implements the IStatemachine interface:

package org.yakindu.sct.examples.trafficlight;

/**
 * Basic interface for state machines.
 */
public interface IStatemachine {

	/**
	 * Initializes the state machine. Used to initialize internal variables etc.
	 */
	public void init();

	/**
	 * Enters the state machine. Sets the state machine into a defined state.
	 */
	public void enter();

	/**
	 * Exits the state machine. Leaves the state machine with a defined state.
	 */
	public void exit();

	/**
	 * Checks whether the state machine is active. 
	 * A state machine is active if it has been entered. It is inactive if it has not been entered at all or if it has been exited.
	 */
	public boolean isActive();

	/**
	 * Checks whether all active states are final. 
	 * If there are no active states then the state machine is considered being incative. In this case this method returns <code>false</code>.
	 */
	public boolean isFinal();

	/**
	* Start a run-to-completion cycle.
	*/
	public void runCycle();
}


Fundamental statechart methods

The generated code contains fundamental methods to initialize, enter, and exit a state machine, as well as a method to execute a run-to-completion step.

The IStatemachine interface specifies the four methods init(), enter(), exit(), and runCycle().

  • The init() method is used to initialize the internal objects of the state machine right after its instantiation. Variables are initialized to their respective default values. If the statechart defines any initialized variables, these initializations are also done in the init() method.

  • The enter() method must be called to enter the state machine. It brings the state machine to a well-defined state.

  • The exit() method is used to leave a state machine statefully. If for example a history state is used in one of the top regions, the last active state is stored and the state machine is left via exit(). Re-entering it via enter() continues to work with the saved state.

  • The runCycle() method is used to trigger a run-to-completion step in which the state machine evaluates arising events and computes possible state changes. Somewhat simplified, a run-to-completion cycle consists of the following steps:

    1. Clear the list of outgoing events.

    2. Check whether any events have occurred which are leading to a state change.

    3. If a state change has to be done:

      1. Make the present state inactive.

      2. Execute exit actions of the present state.

      3. Save history state, if necessary.

      4. Execute transition actions, if any.

      5. Execute entry actions of the new state.

      6. Make the new state active.

    4. Clear the list of incoming events.

Time-controlled state machines

If a statechart uses timing functionality, additional classes are generated.

The traffic light example uses timing funtionality, namely after clauses. To support time-controlled behavior, the interfaces ITimerCallback and ITimer are generated. Like IStatemachine, they are independend of any particular state machine and are generated in the libraryTargetFolder directory, if specified. See section "Outlet" for details.

The generated state machine class implements the ITimerCallback and has a property timer of type ITimer. The client code must provide an ITimer implementation to the state machine by calling the latter’s setTimer() method.

Here’s an example showing how to create a new instance of the state machine (here: class MyTimedStatemachine), create a new instance of a timer (here: class MyTimer), set the latter on the former, and start the state machine by entering it:

MyTimedStatemachine sm = new MyTimedStatemachine();
sm.setTimer(new MyTimer());
sm.enter(); // Enter the state machine

Timer functions generally depend on the hardware target used, therefore the proper time handling has to be implemented by the developer. In principle, for each hardware target a dedicated timer service class implementing the ITimer interface has to be developed.

Default timer implementation

However, upon request the Java code generator can create a default implementation of the ITimer interface, and in many cases it will be sufficient. This implementation is based on java.util.Timer and java.util.TimerTask and should be compatible with the Oracle JVM or the OpenJDK JVM.

To generate the default timer service class, set the TimerService feature in the SGen model to true. Example:

GeneratorModel for yakindu::java {

    statechart MyStateMachine {

        /* … */

        feature GeneralFeatures {
            TimerService = true
        }

    }
}

The generated class is named TimerService and looks like this:

package org.yakindu.sct.examples.trafficlight;

import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Default timer service implementation.
 *
 */
public class TimerService implements ITimer {

	private final Timer timer = new Timer();
	
	private final List<TimeEventTask> timerTaskList = new ArrayList<TimeEventTask>();
	
	private final Lock lock = new ReentrantLock();
	
	/**
	 * Timer task that reflects a time event. It's internally used by
	 * {@link TimerService}.
	 *
	 */
	private class TimeEventTask extends TimerTask {
	
		private ITimerCallback callback;
	
		int eventID;
	
		/**
		 * Constructor for a time event.
		 *
		 * @param callback
		 *            : Set to {@code true} if event should be repeated
		 *            periodically.
		 *
		 * @param eventID
		 *            : Index position within the state machine's timeEvent
		 *            array.
		 */
		public TimeEventTask(ITimerCallback callback, int eventID) {
			this.callback = callback;
			this.eventID = eventID;
		}
	
		public void run() {
			callback.timeElapsed(eventID);
		}
	
		public boolean equals(Object obj) {
			if (obj instanceof TimeEventTask) {
				return ((TimeEventTask) obj).callback.equals(callback)
						&& ((TimeEventTask) obj).eventID == eventID;
			}
			return super.equals(obj);
		}
	}
	
	public void setTimer(final ITimerCallback callback, final int eventID,
			long time, boolean isPeriodic) {
	
		// Create a new TimerTask for given event and store it.
		TimeEventTask timerTask = new TimeEventTask(callback, eventID);
		lock.lock();
		timerTaskList.add(timerTask);
	
		// start scheduling the timer
		if (isPeriodic) {
			timer.scheduleAtFixedRate(timerTask, time, time);
		} else {
			timer.schedule(timerTask, time);
		}
		lock.unlock();
	}
	
	public void unsetTimer(ITimerCallback callback, int eventID) {
		lock.lock();
		int index = timerTaskList.indexOf(new TimeEventTask(callback, eventID));
		if (index != -1) {
			timerTaskList.get(index).cancel();
			timer.purge();
			timerTaskList.remove(index);
		}
		lock.unlock();
	}
	
	/**
	 * Cancel timer service. Use this to end possible timing threads and free
	 * memory resources.
	 */
	public void cancel() {
		lock.lock();
		timer.cancel();
		timer.purge();
		lock.unlock();
	}
}


Timer service

A timer service must implement the ITimer interface and must be able to maintain a number of time events and the timers associated with them. A time event is identified by a numeric ID.

If suitable, an application can use the default timer service class TimerService, see section "Default timer implementation" for details.

The ITimer interface looks like this:

package org.yakindu.sct.examples.trafficlight;

/**
 * Interface a timer has to implement. Use to implement your own timer
 * service.
 * 
 */
public interface ITimer {

	/**
	 * Starts the timing for a given time event id.
	 * 
	 * @param callback
	 * 			  : The target callback where the time event has to be raised.
	 * 
	 * @param eventID
	 *            : The eventID the timer should use if timed out.
	 *            
	 * @param time
	 *            : Time in milliseconds after the given time event should be
	 *            triggered
	 *            
	 * @param isPeriodic
	 * 			  : Set to true if the time event should be triggered periodically
	 */
	public void setTimer(ITimerCallback callback, int eventID, long time, boolean isPeriodic);

	/**
	 * Unset a time event.
	 * 
	 * @param callback
	 * 			: The target callback for which the time event has to be unset.
	 * 
	 * @param eventID
	 * 			: The time event id.
	 */
	public void unsetTimer(ITimerCallback callback, int eventID);
}


Method setTimer

A state machine calls the setTimer(ITimerCallback callback, int eventID, long time, boolean isPeriodic) method to tell the timer service that it has to start a timer for the given eventID. The time parameter specifies the number of milliseconds until the timer expires. When this period of time has elapsed, the timer service must raise the time event by calling the method public void timeElapsed(int eventID) on the ITimerCallback specified by the callback parameter, i.e., usually the state machine.

It is important to keep the execution of the setTimer() method short and use it only to start a timer thread, a hardware timer interrupt, or the like. Avoid any time-consuming operations like extensive computations, Thread.sleep(…), waiting, etc. Otherwise the state machine execution might hang within the timer service or might not show the expected runtime behavior.

If the parameter isPeriodic is false, the timer service raises the time event only once. If isPeriodic is true, the timer service raises the time event every time milliseconds.

Method unsetTimer

If the state machine calls the unsetTimer(ITimerCallback callback, int eventID) method the timer service must unset the timer for the given eventID, i.e., the time event will not be raised.

Raising time events on a state machine

If a statechart is using time events, the generated Java state machine class not only implements the IStatemachine interface, but it also implements the ITimerCallback interface. ITimerCallback is defined as follows. It specifies a single method: public void timeElapsed(int eventID).

package org.yakindu.sct.examples.trafficlight;

/**
* Interface for state machines which use timed event triggers.
*/
public interface ITimerCallback {
	
	/**
	* Callback method if a time event occurred.
	* 
	* @param eventID
	* 			:The id of the occurred event.
	*/
	public void timeElapsed(int eventID);
}


Method timeElapsed

It is the timer service’s responsibility to actually raise a time event on a state machine. To do so, the timer service calls the state machine’s timeElapsed() method and supplies the time event’s eventID as a parameter. The state machine recognizes the time event and will process it during the next run cycle.

You can conclude that in order to process time events without too much latency, the runtime environment has to call the state machine’s runCycle() method as frequently as needed. Consider for example a time event which is raised by the timer service after 500 ms. However, if the runtime environment calls the state machine’s runCycle() method with a frequency of once per 1000 ms only, the event will quite likely not be processed at the correct points in time.

Runtime service

The RuntimeService class maintains all state machines that are expected to execute run-to-completion steps periodically. A client application can retrieve the RuntimeService singleton using RuntimeService.getInstance(). It can then pause, resume or cancel all state machines that are poised to run at a specified intervall.

Please note: To enable this feature, set the RuntimeService parameter of GeneralFeatures to true, see Java: General Features.

Accessing interfaces, variables and events

The client code can read and write state machine variables and raise state machine events. In a YAKINDU statechart, variables and events are contained in so-called interfaces. There can be at most one default, unnamed interface plus zero or more named interfaces. In the generated Java code, these interfaces can be found as inner interface of the interface specifying the state machine. The outer interface’s name is derived from the statechart’s name while the inner interfaces' names are derived from the respective names of the statechart interfaces.

Let’s have a look at the following sample statechart interface declaration:

interface Sample:
    var a:boolean
    in event evA:boolean
    out event evB:integer

The generated interface code looks like this:

package org.yakindu.scr.defaultsm;
import org.yakindu.scr.IStatemachine;

public interface IDefaultSMStatemachine extends IStatemachine {
	public interface SCISample {
		public void raiseEvA(boolean value);
		public boolean isRaisedEvB();
		public long getEvBValue();
		public boolean getA();
		public void setA(boolean value);

	}

	public SCISample getSCISample();

}

A statechart interface is generated as an inner Java interface within the state machine interface. The Java interface’s name is derived from the statechart interface’s name by prepending the string SCI.

A special case is the unnamed statechart interface: It is generated as the Java interface SCInterface.

An incoming event evA:boolean is generated as the raise method raiseEvA(boolean value). Since the event is of type boolean the method has a boolean parameter.

For an outgoing event evB:integer the methods boolean isRaisedEvB() and long getEvBValue() are generated. The former can be used to determine whether the event has been raised by the state machine or not. The latter serves to query the value of the event.

For variables, the code generator creates getter and setter methods, here boolean getA() and void setA(boolean value).

The code generator also creates appropriately-named getter methods in the enclosing interface, which can be used to acquire the nested interfaces, here: SCISample getSCISample().

The nesting interface is implemented by the generated state machine source code. Each nested interface is implemented as an internal class of the state machine class. The latter holds instances of the nested interface implementations and provides them via getter methods. Have a look at the source code generated for the Sample interface:

package org.yakindu.scr.defaultsm;

public class DefaultSMStatemachine implements IDefaultSMStatemachine {

	protected class SCISampleImpl implements SCISample {

		private boolean evA;

		private boolean evAValue;

		public void raiseEvA(boolean value) {
			evA = true;
			evAValue = value;
		}

		protected boolean getEvAValue() {
			if (!evA)
				throw new IllegalStateException("Illegal event value access. Event EvA is not raised!");
			return evAValue;
		}

		private boolean evB;

		private long evBValue;

		public boolean isRaisedEvB() {
			return evB;
		}

		protected void raiseEvB(long value) {
			evB = true;
			evBValue = value;
		}

		public long getEvBValue() {
			if (!evB)
				throw new IllegalStateException("Illegal event value access. Event EvB is not raised!");
			return evBValue;
		}

		private boolean a;

		public boolean getA() {
			return a;
		}

		public void setA(boolean value) {
			this.a = value;
		}

		protected void clearEvents() {
			evA = false;
		}

		protected void clearOutEvents() {
			evB = false;
		}
	}

	protected SCISampleImpl sCISample;

	private boolean initialized = false;

	public enum State {
		main_region_MyState, $NullState$
	};

	private final State[] stateVector = new State[1];

	private int nextStateIndex;

	public DefaultSMStatemachine() {

		sCISample = new SCISampleImpl();
	}

	public void init() {
		this.initialized = true;
		for (int i = 0; i < 1; i++) {
			stateVector[i] = State.$NullState$;
		}

		clearEvents();
		clearOutEvents();

		sCISample.setA(false);
	}

	public void enter() {
		if (!initialized)
			throw new IllegalStateException(
					"The state machine needs to be initialized first by calling the init() function.");

		enterSequence_main_region_default();
	}

	public void exit() {
		exitSequence_main_region();
	}

	/**
	 * @see IStatemachine#isActive()
	 */
	public boolean isActive() {

		return stateVector[0] != State.$NullState$;
	}

	/** 
	* Always returns 'false' since this state machine can never become final.
	*
	 * @see IStatemachine#isFinal() 
	 */
	public boolean isFinal() {
		return false;
	}

	/**
	* This method resets the incoming events (time events included).
	*/
	protected void clearEvents() {
		sCISample.clearEvents();

	}

	/**
	* This method resets the outgoing events.
	*/
	protected void clearOutEvents() {
		sCISample.clearOutEvents();
	}

	/**
	* Returns true if the given state is currently active otherwise false.
	*/
	public boolean isStateActive(State state) {
		switch (state) {
			case main_region_MyState :
				return stateVector[0] == State.main_region_MyState;
			default :
				return false;
		}
	}

	public SCISample getSCISample() {
		return sCISample;
	}

	/* 'default' enter sequence for state MyState */
	private void enterSequence_main_region_MyState_default() {
		nextStateIndex = 0;
		stateVector[0] = State.main_region_MyState;
	}

	/* 'default' enter sequence for region main region */
	private void enterSequence_main_region_default() {
		react_main_region__entry_Default();
	}

	/* Default exit sequence for state MyState */
	private void exitSequence_main_region_MyState() {
		nextStateIndex = 0;
		stateVector[0] = State.$NullState$;
	}

	/* Default exit sequence for region main region */
	private void exitSequence_main_region() {
		switch (stateVector[0]) {
			case main_region_MyState :
				exitSequence_main_region_MyState();
				break;

			default :
				break;
		}
	}

	/* The reactions of state MyState. */
	private void react_main_region_MyState() {
	}

	/* Default react sequence for initial entry  */
	private void react_main_region__entry_Default() {
		enterSequence_main_region_MyState_default();
	}

	public void runCycle() {
		if (!initialized)
			throw new IllegalStateException(
					"The state machine needs to be initialized first by calling the init() function.");

		clearOutEvents();

		for (nextStateIndex = 0; nextStateIndex < stateVector.length; nextStateIndex++) {

			switch (stateVector[nextStateIndex]) {
				case main_region_MyState :
					react_main_region_MyState();
					break;
				default :
					// $NullState$
			}
		}

		clearEvents();
	}
}

The value of an event can be accessed only if the event has been processed in a run-to-completion step. Otherwise an IllegalStateException will be thrown.

Interface observers

If the general feature InterfaceObserverSupport is enabled in the SGen model, the generated interfaces will support the registration of observers.

Enabling the InterfaceObserverSupport feature looks like this in the .sgen file:

feature GeneralFeatures {
    InterfaceObserverSupport = true
}

Now the generated code has additional features:

package org.yakindu.scr.defaultsm;
import java.util.List;
import org.yakindu.scr.IStatemachine;

public interface IDefaultSMStatemachine extends IStatemachine {
	public interface SCISample {
		public void raiseEvA(boolean value);
		public boolean isRaisedEvB();
		public long getEvBValue();
		public boolean getA();
		public void setA(boolean value);
		public List<SCISampleListener> getListeners();

	}

	public interface SCISampleListener {
		public void onEvBRaised(long value);
	}

	public SCISample getSCISample();

}

An additional listener interface is generated, here SCISampleListener. It contains a callback method for each outgoing event. Here it is a single one: void onEvBRaised(long value).

The client code has to provide an implementation of the listener interface. A listener method gets called by the state machine when it raises an outgoing event.

To register or unregister a listener, use the getListeners() method of the nesting interface. This method returns a java.util.List parameterized with the appropriate listener type. Initially this list is empty. Add or remove listeners as needed.

A callback method specified by the listener interface should complete its operations quickly, because otherwise the state machine execution might be delayed for too long, potentially leading to unexpected runtime behavior.

Operation callbacks

YAKINDU Statechart Tools support operations that are executed by a state machine as actions, but are implemented by client-side code. The figure below shows a sample statechart using an operation:

Specifying an operation callback in the model

Specifying an operation callback in the model

Let’s have a look at the generated code:

package org.yakindu.scr.defaultsm;
import java.util.List;
import org.yakindu.scr.IStatemachine;

public interface IDefaultSMStatemachine extends IStatemachine {
	public interface SCISample {
		public void raiseEvA(boolean value);
		public boolean isRaisedEvB();
		public long getEvBValue();
		public boolean getA();
		public void setA(boolean value);
		public List<SCISampleListener> getListeners();

		public void setSCISampleOperationCallback(SCISampleOperationCallback operationCallback);
	}

	public interface SCISampleListener {
		public void onEvBRaised(long value);
	}

	public interface SCISampleOperationCallback {
		public long myOperation(long p1, boolean p2);
	}

	public SCISample getSCISample();

}

An additional interface SCISampleOperationCallback specifying the method public long myOperation(long p1, boolean p2) has been generated. The client code has to

  • provide an implementation of this interface and
  • pass an instance of it to the state machine via the setSCISampleOperationCallback(SCISampleOperationCallback operationCallback) method.

Here’s some sample code that passes an implementation of the operation to a state machine, and then executes the latter:

public static void main(String[] args) {
    DefaultSMStatemachine statemachine = new DefaultSMStatemachine();

    SCISampleOperationCallback callback = new SCISampleOperationCallback() {

        @Override
        public long myOperation(long p1, boolean p2) {
            // Your operation code should be placed here;
            return 0;
        }
    };

    statemachine.getSCISample().setSCISampleOperationCallback(callback);

    statemachine.init();
    statemachine.enter();
    statemachine.runCycle();
}

Trace-observed state machine

By using the tracing feature the execution of the state machine can be observed. In detail, entered and exited states can be traced. Therefore, additional operation callbacks onStateEntered and onStateExited are generated in the ITracingListener interface:

/**
 * Tracing interface for state machines.
 * Must be implemented if tracing feature is used.
 * Multiple listeners can be added to and removed from the state machine.
 */
public interface ITracingListener<T> {
	
	/**
	 * OnStateEntered will be called if any state has been entered.
	 */
	void onStateEntered(T state);
	
	/**
	* OnStateExited will be called if any state has been exited.
	*/
	void onStateExited(T state);
}

With additional generated API in the generated state machine, listeners can be added to and removed from the state machine. Therefore, an implementation of the ITracingListener interface is required. A minimum implementation could be done like this:

public class TracingImpl<T> implements ITracingListener<T>{
	@Override
	public void onStateEntered(T state) {}
	@Override
	public void onStateExited(T state) {}
}

If the tracing feature is enabled, one or more listeners can be added:

public class StatemachineRunner {
	public void runStatemachine() {
		SomeStatemachine sm = new SomeStatemachine();
		
		// create multiple observer
		TracingImpl<State> traceObserver1 = new TracingImpl<State>();
		TracingImpl<State> traceObserver2 = new TracingImpl<State>();
		
		// add observer to the statemachine
		sm.addTraceObserver(traceObserver1);
		sm.addTraceObserver(traceObserver2);
		
		sm.init();
		sm.enter();
		
		// remove observer if it is not needed anymore
		sm.removeTraceObserver(traceObserver2);
		
		sm.runCycle();
	}
}


Integrating generated code

To get a clue how to integrate a generated Java state machine with your project, have a look at the CrossingDemoCycleBased class and its abstract superclass CrossingDemoBase. The main() method is in CrossingDemoCycleBased:

public static void main(String[] args) {

    new CrossingDemoCycleBased().runTrafficLight();
}

A new instance of the class is created and the method runTrafficLight() is called. This method can be found in the superclass:

public void runTrafficLight() {

        setUpAndRunStatemachine();
        createUIContent();

        shell.open();
        while (!shell.isDisposed()) {
            // update traffic lights
            readStatemachineOutput();

            crossing.repaint();

            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }

        tearDownStatemachine();
}

This method sets up the state machine and creates the GUI content. In a while loop it reads the content of the state machine and repaints the GUI. If the user exits the GUI shell, the loop terminates and the state machine is torn down. The really interesting methods are setUpAndRunStatemachine(), readStatemachineOutput(), and tearDownStatemachine():

protected void setUpAndRunStatemachine() {

    statemachine = new TrafficLightStatemachine();
    statemachine.setTimerService(new TimerService());
    statemachine.init();
    statemachine.enter();

    RuntimeService.getInstance().registerStatemachine(statemachine, 100);
}

First a new instance of the generated state machine is created. Since the traffic light statechart uses timing clauses, it is provided with a timer service, here with the default implementation of the ITimerService interface. In the next steps the state machine is initialized and entered. After the enter() method has been executed, the machine is in a defined state.

Finally the state machine is passed to the runtime service. This service executes the runCycle() method of the state machine every 100 ms, that is the state machine executes a run-to-completion step every 100 ms.

protected void readStatemachineOutput() {
    trafficLightFigure.setRed(statemachine.getSCITrafficLight()
            .getRed());
    trafficLightFigure.setYellow(statemachine.getSCITrafficLight()
            .getYellow());
    trafficLightFigure.setGreen(statemachine.getSCITrafficLight()
            .getGreen());
        pedestrianLightFigure.setWhite(statemachine.getSCIPedestrian()
            .getRequest());
    pedestrianLightFigure.setRed(statemachine.getSCIPedestrian()
            .getRed());
    pedestrianLightFigure.setGreen(statemachine.getSCIPedestrian()
            .getGreen());
}

The generated code contains getters and setters for each variable and event. So it’s easy to read values from or write values to a state machine, raise events, or ask the state machine whether outgoing events have been raised during the last run-to-completion step. Within the readStatemachineOutput() method, these methods are used to get the lights values from the state machine and set them to the UI elements. The methods pedestrianRequestButtonClicked() and onOffButtonClicked() raise some events.

Hint: When outgoing events are raised within the state machine, they remain active until the next run-to-completion step is started.

@Override
protected void tearDownStatemachine() {
    // End TimerHandler and RuntimeService.
    statemachine.getTimerService().cancel();
    RuntimeService.getInstance().cancelTimer();
}

If the UI thread has been terminated by the user, the state machine will be shut down. It is necessary to explicitly end the timer service. Finally the runtime service is cancelled.

Simulating operations with custom Java code

To simulate a model with operations it is possible to use custom Java code that mocks the desired behavior or even to simulate against an existing Java backend. For this purpose it is required to provide one or more custom Java classes having a method with a matching signature.

A statechart model with an operation

A statechart model with an operation

To simulate the statechart above, a new Java class must be created matching the method signature defined in the statechart. This class must be placed onto the classpath of the statecharts project.

YAKINDU Statechart Tools' default types are mapped to Java types as follows:

YSCT type Java type
integer long
real double
boolean boolean
string String
void void
package example;
public class Calculator {
   public long add(long param1, long param2) {
      return param1 + param2;
   }
}

This custom class can be passed to Eclipse’s run configuration as an Operation Class, see the figure below. It is possible to specify multiple Java classes, separated by comma.

When the simulation is executed, the variable result gets the value 2.

Configuring an operations class

Configuring an operations class