Generating state machine code

Ultimately, you will need your state machine not only as a nice graphical statechart model, but rather in the form of executable code that you can integrate with your software project as a component. You will want to send events to the state machine, receive events from it, retrieve information regarding the currently active state (or states), set and get state machine variables, and have the state machine invoke specific behaviour that is external to it.

YAKINDU Statechart Tools comes with a couple of code generators that save you from the time-consuming and error-prone task of coding your state machine manually. The YAKINDU code generators transform your statechart into source code of a target programming language in an instant. The generated code is always correct, at least in the sense that it is a true one-to-one mapping of your statechart – and at least unless a code generator is buggy itself.

Use SCTUnit in order to test whether your state machine is also correct in the sense that it does what it is intended to do.

Currently YAKINDU Statechart Tools support code generation for the following programming languages:

  • C
  • C++
  • Java
  • TypeScript

If these code generators don’t suit your needs, you are free to develop your very own custom code generator.

General concepts of the state machine code

Execution schemes

The generated state machine code implements one of two execution schemes:

  • In the cycle-based execution scheme, the state machine will first collect events and later process them in a run-to-completion step. Run-to-completion steps are typically executed in regular time intervals.
  • In the event-driven execution scheme, the state machine becomes active each time an event arrives and processes it right away in a run-to-completion step.

You can select the execution scheme via the @CycleBased or @EventDriven annotations. Write the appropriate annotation to the top of your statechart’s definition section, see sections "@CycleBased" and "@EventDriven".

Cycle-based execution scheme

In the cycle-based execution scheme, each run cycle consists of two different phases:

  1. In the first phase, incoming events and time events are collected.
  2. In the second phase, runCycle() is executed and processes the events collected in the first phase.

This approach allows for explicitly processing several events at the same time as well as for checking combinations of events, e.g. a guard condition like [eventA && eventB]. This is very useful for systems that are close to hardware and input signals. Basically it is the IPO (input processing output) model.

Example: The sample client code below first raises events eventA and eventB in the state machine. These events are lingering in the state machine until the client code calls runCycle() to actually process the events.

sc_raiseEventA(handle);
sc_raiseEventB(handle);
…
sc_runCycle(handle);

Please note: In the cycle-based execution scheme, an event that has been raised internally using the raise statement is visible „downstream” in the run cycle only, i.e. in regions and the states therein that have not yet been processed in the current run cycle. Everything that is „upstream” in the run cycle cannot „see” this event. This is semantically different from the event-driven execution scheme. Read more on this in section "Raising and processing an event".

Event-driven execution scheme

In the event-driven execution scheme, each incoming event or time event immediately triggers the execution of a run-to-completion step. As a consequence, the state machine will never process two events simultaneously, and a guard condition like [eventA && eventB] will never become true.

Example: The sample client code below raises the events eventA and eventB. The state machine processes each event immediately as it arrives, triggered by the respective raise…() method. Thus the client code does not need to call runCycle().

sc_raiseEventA(handle);
sc_raiseEventB(handle);

Please note: In the event-driven execution scheme, an event that has been raised internally using the raise statement is not immediately acted upon by active states „downstream” in the event cycle, because only a single event can be processed at a time, which is the event that caused the run cycle to execute in the first place. The internally-raised event will trigger its own run-to-completion step subsequently, and it will thus be visible to all active states. This is semantically different from the cycle-based execution scheme. Read more on this in section "Raising and processing an event".

Responsibilities of generated code

The generated source code supports a basic statechart execution model that can be used to implement different variants. It is important to understand the responsibilities of the generated code, i.e. what it cares about and what it doesn’t. The remaining issues are out of scope and must be dealt with in the client code the developer has to provide.

The generated code basically takes care about the following:

  • It provides the interfaces to access state machine variables and events.
  • It initializes the state machine.
  • It implements the execution semantics within a run-to-completion (RTC) step. This is everything that is happening within the runCycle() function.

There are some predefined constraints:

  • The implementation is not thread-safe. Therefore the runCycle() method must never be called in parallel by different threads.
  • All events for a run-to-completion step will be consumed.

Out of scope are:

  • Scheduling: The code generator cannot make any general assumptions about scheduling mechanisms and constraints.
  • Timers: The code generator does not know how the system handles times. If any specific time-dependend behavior is required, the client code has to contribute it.
  • Event handling: The code generator cannot know and does not prescribe how events are entering the system. Therefore deciding when events will be delivered to the state machine is an outside (client code) responsibility.
  • Concurrency: The client code has to care about any thread synchronization.
  • Memory management: The generated state machine code will never allocate any memory. Allocating the memory needed for the statechart structure is a responsibility of the client code.

All these things are out of the generated code’s scope, since the code generators coming with YAKINDU Statechart Tools cannot provide tailored solutions without knowing any details about a respective specific environment, like runtime system, libraries, etc.

In order to have a better code generator support for specific usage patterns, you would have to implement corresponding code generator extensions.

Thread-safe execution

In order to circumvent the missing thread-safety of the runCycle() method, the Java code generator has an option for generating a so-called runnable wrapper, providing event queueing and multi-threading support. For details, see section "RunnableWrapper". It is easy to implement this as a general solution for Java, since the programming language provides corresponding standard features that by definition are available everywhere by can simply be used.

The C programming language, however, does not have any standardized threading features. Therefore a general solution cannot be given. Specific solutions for specific threading libraries would require the implementation of a suitable code generator extension.

Setting up a code generator project

In order to deploy a code generator and actually generate code, you have to set up a code generator project. This is described in the following subsections.

For configuring the code generation process, YAKINDU Statechart Tools uses a textual generator model called SGen. It can be created either by using the YAKINDU Statechart Generator Model wizard or by creating a text file with the filename extension .sgen.

To create a generator model using the wizard, proceed as follows:

  1. In the project explorer view, right-click on your project. The context meu opens.
  2. In the context menu, select New → Code generator model.
  3. The New YAKINDU SGen model wizard opens.
  4. Select an appropriate folder and specify the filename of your SGen model. The filename must have the extension .sgen.
  5. Click on Next >.
  6. From the Generator drop-down menu, choose the code generator you want to use, e.g. YAKINDU SCT Java code generator.
  7. Check the statechart(s) to generate code from.
  8. Click on Finish.

Selecting code generator and statecharts

Selecting code generator and statecharts

The result is the specified .sgen file. It has the following format:

GeneratorModel for [GeneratorId] {
	statechart [StatechartReference] {
		feature [Feature] {
			[ParameterName] = [ParameterValue]
		}
	}
}

The [GeneratorId] represents the unique ID of the chosen generator. Currently, YAKINDU Statechart Tools supports the following code generators out of the box:

  • yakindu::java – Generator ID for the Java code generator
  • yakindu::c – Generator ID for the C code generator
  • yakindu::cpp – Generator ID for the C++ code generator
  • yakindu::generic – Generator ID for custom Java-based code generators
  • sctunit::c – Generator ID for SCTUnit/C code generator

A single generator model may contain multiple statechart … { … } blocks and thus specify the code generation for multiple statecharts. For each statechart, the generator process can be configured with Features. Each feature has one or more parameters. These parameters can be configured with ParameterName = ParameterValue.

Running a generator

Provided you have created the generator model, proceed as follows:

  1. In the project explorer view, right-click on the .sgen file containing the generator model. The context menu opens.
  2. In the context menu, select Generate Code Artifacts.
  3. The generator is executed.

The source files the generator produces depend on the generator itself. Each generator makes use of its specific target language and the latter’s features and best practices. For example, the Java generator generates Java interfaces and classes, and the C and C++ generators generate .h header files and .c source files. The location of the generated sources can be changed in the generator model. The respective options are explained in the following section.

The generator model is executed by a so-called Eclipse builder. Thus, the artifacts are generated automatically whenever the statechart is modified, as long as Project → Build Automatically is checked. If you want or need to execute your generator model manually, select Generate Statechart Artifacts from the package explorer's context menu.

Configuring a generator

As mentioned above, all generators can be set up and customized by a generator model. The following screenshot shows a sample configuration for the Java code generator.

SGen generator model with default values

SGen generator model with default values

The following sections describe core features, which are available for all code generators. Individual code generators usually support specific additional features; please see the respective code generator documentation for details.

Outlet feature

The Outlet feature specifies target project and target folder for the generated artifacts. It is a required feature and has the following parameters:

  • targetProject (String, required): The project to write the generated artifacts to.
  • targetFolder (String, required): The folder within the target folder to write the generated artifacts to. If a library target folder (see below) is specified, only the model-dependent artifacts are generated in the target folder. All artifacts in this folder will be overwritten during re-generation.
  • libraryTargetFolder (String, optional): The folder to write the model-independent artifacts to. If this parameter is not specified, these artifacts will be generated in the target folder (see above). All artifacts in this folder will be preserved during re-generation. Manual modifications of these artifacts will not be overwritten upon re-generation.
  • apiTargetFolder (String, optional): The folder to write API code to, e.g. interfaces or header files. If this parameter is not specified, these artifacts will be generated in the target folder (see above).
  • skipLibraryFiles (Boolean, optional): If you wish to exclude the static files from the code generation, i.e. those that are put into the libraryTargetFolder, you can set this value to true. If the value is false or not specified at all, the files are generated as usual. Currently supported for the Java, C and C++ generators.

Example:

feature Outlet {
    targetProject = "SampleProject"
    targetFolder = "src-gen"
    libraryTargetFolder = "src"
    apiTargetFolder = "api-gen"
}

LicenseHeader feature

The LicenseHeader feature specifies the license text to be added as a header to the generated artifacts. It is an optional feature and has the following parameter:

  • licenseText (String, required): License text to be added as a file header

Example:

feature LicenseHeader {
    licenseText = "Copyright (c) 2017 committers of YAKINDU and others."
}

FunctionInlining feature

The FunctionInlining feature enables the inlining of expressions instead of generating separate functions or methods. This might reduce the readability of the generated code, but increases performance, because less operation calls are necessary. This is an optional feature and has the following parameters:

  • inlineReactions (Boolean, optional): inlines the expression for reactions
  • inlineEntryActions (Boolean, optional): inlines the expression for entry actions
  • inlineExitActions (Boolean, optional): inlines the expression for exit actions
  • inlineEnterSequences (Boolean, optional): inlines the expression for state-entering sequences
  • inlineExitSequences (Boolean, optional): inlines the expression for state-exiting sequences
  • inlineChoices (Boolean, optional): inlines the expression for choices
  • inlineEnterRegion (Boolean, optional): inlines the expression for region-entering sequences
  • inlineExitRegion (Boolean, optional): inlines the expression for region-exiting sequences
  • inlineEntries (Boolean, optional): inlines the expression for entries

ALl parameters of the FunctionInlining feature are false by default.

Example:

feature FunctionInlining {
    inlineChoices = false
    inlineEnterRegion = true
    inlineEntries = true
}

Debug feature

The Debug feature dumps the execution model to the target folder as an XMI model. It is an optional feature and has the following parameter:

  • dumpSexec (Boolean, required): dumps the execution model as XMI model (default: false)

Example:

feature Debug {
    dumpSexec = true
}

Using properties and expressions in generator models

An SGen generator model may assign values to properties and later use these properties in expressions. The following sample generator model uses the var keyword to declare the properties projectName, version, isBeta, and generateTimerServce with their respective types and default values. The model then uses these values in feature clauses by referring to the properties:

GeneratorModel for yakindu::java {

    var projectName : string = "light_switch"
    var version : string = "1.0"
    var isBeta : boolean = true
    var generateTimerServce : boolean = true

    statechart myStateAutomaton {

        feature Outlet {
            targetProject = projectName
            targetFolder = "src-gen/" + version + (isBeta ? "beta" : "")
            libraryTargetFolder = "src"
        }

        feature GeneralFeatures {
            TimerService = generateTimerServce
        }

    }
}

The syntax rules for expressions in generator models are the same as for statechart language expressions, see section "Expressions". However, expressions are constrained to a subset that semantically makes sense in the context of a generator model. That means that, for example, while you can use the + operator to concatenate strings, you cannot use statechart-related constructs like the after keyword or the active() built-in function.

In the properties definition section near the top of a generator model, a value assigned to a property must be given as a literal. More complex expressions are not supported there.

The values assigned to properties are default values only. That means, you can override them by different values when actually executing a generation model by way of running the headless code generator.

For example, the command to call the headless generator might look like this:

scc -m myGenmodel.sct -v version=2.0;isBeta=false

The name/value pairs specified by the -v option would override the corresponding default values of the properties in the generator model. In this case, the properties version and isBeta would be set to the values „2.0” and „false”, respectively. The variables projectName and generateTimerServce would maintain their default values as specified in the SGen file.

C code generator

C code generator features

IdentifierSettings feature

The IdentifierSettings feature allows the configuration of module names and identifier character length:

  • moduleName (String, optional): Name for header and implementation. Default: statechart name.
  • statemachinePrefix (Boolean, optional): Prefix that is prepended to function, state, and type names.
  • maxIdentifierLength (Integer, optional): Maximum number of characters of an identifier. Default: 31 characters, which is complying with the ANSI C99 standard.
  • separator (String, optional): Character to replace whitespace and otherwise illegal characters in names.

Example:

feature IdentifierSettings {
    moduleName = "MyStatechart"
    statemachinePrefix = "myStatechart"
    maxIdentifierLength = 31
    separator = "_"
}

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
}

Includes feature

The Includes feature allows to change how include statements are generated:

  • useRelativePaths (Boolean, optional): If this parameter is set to true, relative paths are calculated for include statements, otherwise simple includes are used. Default: true.

Example:

feature Includes {
    useRelativePaths = false
}

Specification of C code

In order to describe the API specifications of the code generated by the YAKINDU C 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

You will find the generated code in the src-gen folder of the traffic light example.

The C code generator generates two or three header files, depending on the statechart.

  • sc_types.h: Contains type definitions used by the statechart. Since the contents of this file is always the same for all statecharts, it will be generated only if it does not yet exist. And since it will never be overwritten, you can change or amend the definitions made there. For example, you might wish to adapt the types to better match your target platform.
  • Statechart.h: Contains the basic declarations for everything needed in the underlying statechart’s code. The file is named after the statechart, i.e. if the statechart’s name is TrafficLight, this file’s name will be TrafficLight.h. It is always generated.
  • StatechartRequired.h: Contains declarations of functions the developer has to implement. The file is named after the statechart, i.e. if the statechart’s name is TrafficLight, this file’s name will be TrafficLightRequired.h. It is generated only if one or more of the following conditions are fulfilled:
    • The statechart declares operations.
    • The statechart uses timing features.
    • The code generator model activates the Tracing feature.
The first one is sc_types.h:

#ifndef SC_TYPES_H_
#define SC_TYPES_H_

#ifdef __cplusplus
extern "C"
{
#endif 

#include <stdint.h>
#include <stdbool.h>

#define sc_string		char*
 
typedef bool			sc_boolean;
typedef int_fast16_t	sc_short;
typedef uint_fast16_t	sc_ushort;
typedef int32_t			sc_integer;
typedef uint32_t		sc_uinteger;

typedef double			sc_real;

typedef void*			sc_eventid;

typedef intptr_t		sc_intptr_t;

#ifdef __cplusplus
}
#endif

#ifndef null
	#ifdef __cplusplus
		#define null 0
	#else
		#define null ((void *)0)
	#endif
#endif

#define bool_true true
#define bool_false false

#endif /* SC_TYPES_H_ */

The header file contains some basic definitions for C++ compiler compatibility and some typedefs to map the YAKINDU statechart types to C types.

The next header file is named after the statechart. In case of the traffic light example it is called TrafficLight.h:



#ifndef TRAFFICLIGHTWAITING_H_
#define TRAFFICLIGHTWAITING_H_

#include "../src/sc_types.h"
		
#ifdef __cplusplus
extern "C" { 
#endif 

/*! \file Header of the state machine 'TrafficLightWaiting'.
*/


/*! Enumeration of all states */ 
typedef enum
{
	TrafficLightWaiting_last_state,
	TrafficLightWaiting_main_region_on,
	TrafficLightWaiting_main_region_on_r1_StreetGreen,
	TrafficLightWaiting_main_region_on_r1_PedWaiting,
	TrafficLightWaiting_main_region_on_r1_PedWaiting_r1_waitOn,
	TrafficLightWaiting_main_region_on_r1_PedWaiting_r1_waitOff,
	TrafficLightWaiting_main_region_on_r1_StreetAttention,
	TrafficLightWaiting_main_region_on_r1_StreetRed,
	TrafficLightWaiting_main_region_on_r1_PedestrianGreen,
	TrafficLightWaiting_main_region_on_r1_PedestrianRed,
	TrafficLightWaiting_main_region_on_r1_StreetPrepare,
	TrafficLightWaiting_main_region_off,
	TrafficLightWaiting_main_region_off_r1_YellowOn,
	TrafficLightWaiting_main_region_off_r1_YellowOff
} TrafficLightWaitingStates;

/*! Type definition of the data structure for the TrafficLightWaitingIfaceTrafficLight interface scope. */
typedef struct
{
	sc_boolean red;
	sc_boolean yellow;
	sc_boolean green;
} TrafficLightWaitingIfaceTrafficLight;

/*! Type definition of the data structure for the TrafficLightWaitingIfacePedestrian interface scope. */
typedef struct
{
	sc_boolean request;
	sc_boolean red;
	sc_boolean green;
} TrafficLightWaitingIfacePedestrian;

/*! Type definition of the data structure for the TrafficLightWaitingIface interface scope. */
typedef struct
{
	sc_boolean pedestrianRequest_raised;
	sc_boolean onOff_raised;
} TrafficLightWaitingIface;

/*! Type definition of the data structure for the TrafficLightWaitingTimeEvents interface scope. */
typedef struct
{
	sc_boolean trafficLightWaiting_main_region_on_r1_PedWaiting_tev0_raised;
	sc_boolean trafficLightWaiting_main_region_on_r1_PedWaiting_r1_waitOn_tev0_raised;
	sc_boolean trafficLightWaiting_main_region_on_r1_PedWaiting_r1_waitOff_tev0_raised;
	sc_boolean trafficLightWaiting_main_region_on_r1_StreetAttention_tev0_raised;
	sc_boolean trafficLightWaiting_main_region_on_r1_StreetRed_tev0_raised;
	sc_boolean trafficLightWaiting_main_region_on_r1_PedestrianGreen_tev0_raised;
	sc_boolean trafficLightWaiting_main_region_on_r1_PedestrianRed_tev0_raised;
	sc_boolean trafficLightWaiting_main_region_on_r1_StreetPrepare_tev0_raised;
	sc_boolean trafficLightWaiting_main_region_off_r1_YellowOn_tev0_raised;
	sc_boolean trafficLightWaiting_main_region_off_r1_YellowOff_tev0_raised;
} TrafficLightWaitingTimeEvents;


/*! Define dimension of the state configuration vector for orthogonal states. */
#define TRAFFICLIGHTWAITING_MAX_ORTHOGONAL_STATES 1

/*! Define maximum number of time events that can be active at once */
#define TRAFFICLIGHTWAITING_MAX_ACTIVE_TIME_EVENTS 2

/*! Define indices of states in the StateConfVector */
#define SCVI_TRAFFICLIGHTWAITING_MAIN_REGION_ON 0
#define SCVI_TRAFFICLIGHTWAITING_MAIN_REGION_ON_R1_STREETGREEN 0
#define SCVI_TRAFFICLIGHTWAITING_MAIN_REGION_ON_R1_PEDWAITING 0
#define SCVI_TRAFFICLIGHTWAITING_MAIN_REGION_ON_R1_PEDWAITING_R1_WAITON 0
#define SCVI_TRAFFICLIGHTWAITING_MAIN_REGION_ON_R1_PEDWAITING_R1_WAITOFF 0
#define SCVI_TRAFFICLIGHTWAITING_MAIN_REGION_ON_R1_STREETATTENTION 0
#define SCVI_TRAFFICLIGHTWAITING_MAIN_REGION_ON_R1_STREETRED 0
#define SCVI_TRAFFICLIGHTWAITING_MAIN_REGION_ON_R1_PEDESTRIANGREEN 0
#define SCVI_TRAFFICLIGHTWAITING_MAIN_REGION_ON_R1_PEDESTRIANRED 0
#define SCVI_TRAFFICLIGHTWAITING_MAIN_REGION_ON_R1_STREETPREPARE 0
#define SCVI_TRAFFICLIGHTWAITING_MAIN_REGION_OFF 0
#define SCVI_TRAFFICLIGHTWAITING_MAIN_REGION_OFF_R1_YELLOWON 0
#define SCVI_TRAFFICLIGHTWAITING_MAIN_REGION_OFF_R1_YELLOWOFF 0

/*! 
 * Type definition of the data structure for the TrafficLightWaiting state machine.
 * This data structure has to be allocated by the client code. 
 */
typedef struct
{
	TrafficLightWaitingStates stateConfVector[TRAFFICLIGHTWAITING_MAX_ORTHOGONAL_STATES];
	sc_ushort stateConfVectorPosition; 
	
	TrafficLightWaitingIfaceTrafficLight ifaceTrafficLight;
	TrafficLightWaitingIfacePedestrian ifacePedestrian;
	TrafficLightWaitingIface iface;
	TrafficLightWaitingTimeEvents timeEvents;
} TrafficLightWaiting;


/*! Initializes the TrafficLightWaiting state machine data structures. Must be called before first usage.*/
extern void trafficLightWaiting_init(TrafficLightWaiting* handle);

/*! Activates the state machine */
extern void trafficLightWaiting_enter(TrafficLightWaiting* handle);

/*! Deactivates the state machine */
extern void trafficLightWaiting_exit(TrafficLightWaiting* handle);

/*! Performs a 'run to completion' step. */
extern void trafficLightWaiting_runCycle(TrafficLightWaiting* handle);

/*! Raises a time event. */
extern void trafficLightWaiting_raiseTimeEvent(const TrafficLightWaiting* handle, sc_eventid evid);

/*! Gets the value of the variable 'red' that is defined in the interface scope 'TrafficLight'. */ 
extern sc_boolean trafficLightWaitingIfaceTrafficLight_get_red(const TrafficLightWaiting* handle);
/*! Sets the value of the variable 'red' that is defined in the interface scope 'TrafficLight'. */ 
extern void trafficLightWaitingIfaceTrafficLight_set_red(TrafficLightWaiting* handle, sc_boolean value);
/*! Gets the value of the variable 'yellow' that is defined in the interface scope 'TrafficLight'. */ 
extern sc_boolean trafficLightWaitingIfaceTrafficLight_get_yellow(const TrafficLightWaiting* handle);
/*! Sets the value of the variable 'yellow' that is defined in the interface scope 'TrafficLight'. */ 
extern void trafficLightWaitingIfaceTrafficLight_set_yellow(TrafficLightWaiting* handle, sc_boolean value);
/*! Gets the value of the variable 'green' that is defined in the interface scope 'TrafficLight'. */ 
extern sc_boolean trafficLightWaitingIfaceTrafficLight_get_green(const TrafficLightWaiting* handle);
/*! Sets the value of the variable 'green' that is defined in the interface scope 'TrafficLight'. */ 
extern void trafficLightWaitingIfaceTrafficLight_set_green(TrafficLightWaiting* handle, sc_boolean value);
/*! Gets the value of the variable 'request' that is defined in the interface scope 'Pedestrian'. */ 
extern sc_boolean trafficLightWaitingIfacePedestrian_get_request(const TrafficLightWaiting* handle);
/*! Sets the value of the variable 'request' that is defined in the interface scope 'Pedestrian'. */ 
extern void trafficLightWaitingIfacePedestrian_set_request(TrafficLightWaiting* handle, sc_boolean value);
/*! Gets the value of the variable 'red' that is defined in the interface scope 'Pedestrian'. */ 
extern sc_boolean trafficLightWaitingIfacePedestrian_get_red(const TrafficLightWaiting* handle);
/*! Sets the value of the variable 'red' that is defined in the interface scope 'Pedestrian'. */ 
extern void trafficLightWaitingIfacePedestrian_set_red(TrafficLightWaiting* handle, sc_boolean value);
/*! Gets the value of the variable 'green' that is defined in the interface scope 'Pedestrian'. */ 
extern sc_boolean trafficLightWaitingIfacePedestrian_get_green(const TrafficLightWaiting* handle);
/*! Sets the value of the variable 'green' that is defined in the interface scope 'Pedestrian'. */ 
extern void trafficLightWaitingIfacePedestrian_set_green(TrafficLightWaiting* handle, sc_boolean value);
/*! Raises the in event 'pedestrianRequest' that is defined in the default interface scope. */ 
extern void trafficLightWaitingIface_raise_pedestrianRequest(TrafficLightWaiting* handle);

/*! Raises the in event 'onOff' that is defined in the default interface scope. */ 
extern void trafficLightWaitingIface_raise_onOff(TrafficLightWaiting* handle);


/*!
 * Checks whether the state machine is active (until 2.4.1 this method was used for states).
 * A state machine is active if it was entered. It is inactive if it has not been entered at all or if it has been exited.
 */
extern sc_boolean trafficLightWaiting_isActive(const TrafficLightWaiting* handle);

/*!
 * Checks if all active states are final. 
 * If there are no active states then the state machine is considered being inactive. In this case this method returns false.
 */
extern sc_boolean trafficLightWaiting_isFinal(const TrafficLightWaiting* handle);

/*! Checks if the specified state is active (until 2.4.1 the used method for states was called isActive()). */
extern sc_boolean trafficLightWaiting_isStateActive(const TrafficLightWaiting* handle, TrafficLightWaitingStates state);



#ifdef __cplusplus
}
#endif 

#endif /* TRAFFICLIGHTWAITING_H_ */


Within this header file an enum containing the state names is defined as well as data structures for each of the statechart’s interfaces. Additionally a structure for the statechart’s time events is defined. The interfaces' and time events' data structures are nested into the parent structure TrafficLight. The client has to allocate memory for this structure. It is a common parameter of most functions the generated state machine code defines. In the subsequent text, this structure is called the statechart data structure.

Defines and constants in the statechart header file

The header file Statechart.h contains several #define statements for constants that are explained below, based on the TrafficLight example and the header file shown above.

  • #define TRAFFICLIGHTWAITING_MAX_ORTHOGONAL_STATES 1: The orthogonality of this statechart, that is, the maximum number of states that will be active at the same time. The state configuration vector manages the active states. It has this many entries.
  • #define TRAFFICLIGHTWAITING_MAX_ACTIVE_TIME_EVENTS 2: The maximum number of time events this statechart will have to wait for simultaneously. When writing a timer service, you can use this constant to plan for the amount of memory you will have to allocate for timers. (Take a look at the traffic light statechart. Can you find the state in which two time events will be active at the same time?)
  • #define SCVI_TRAFFICLIGHTWAITING_MAIN_REGION_ON 0 (and so on): The index of the state in the state configuration vector. Since this example does not have any orthogonal regions, all indices are 0. If states are orthogonal to each other, their index will be different.
Fundamental statechart functions

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

In the header file, the function names are made up of the state machine name followed by the name of the respective functionality. For the traffic light example, these functions are generated as follows:

/*! Initializes the TrafficLight state machine data structures. Must be called before first usage.*/
extern void trafficLight_init(TrafficLight* handle);

/*! Activates the state machine */
extern void trafficLight_enter(TrafficLight* handle);

/*! Deactivates the state machine */
extern void trafficLight_exit(TrafficLight* handle);

/*! Performs a 'run to completion' step. */
extern void trafficLight_runCycle(TrafficLight* handle);

  • The init() function 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() function.

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

  • The exit() function 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() function 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.

Accessing variables and events

The client code can read and write state machine variables and raise state machine events. The getters and setters for each variable and event are also contained in the header file. The function names are matching the following pattern:

statechart_name Iface interface_name _ [ set | get | raise ] _ [ variable_name|_event_name_]

For example, the getter of the red variable of the Pedestrian interface is named trafficLightIfacePedestrian_get_red(TrafficLight* handle)

Time-controlled state machines

If a statechart uses timing functionality or external operations, or tracing is activated in the generator model, an additional header file is generated. Its name matches the following pattern:

statechart_name Required.h

This header file declares functions the client code has to implement externally.

Example

The traffic light example uses timing functionality, namely after clauses. To support time-controlled behavior, the additional header file TrafficLightRequired.h is generated. Note the two functions trafficLight_setTimer and trafficLight_unsetTimer..



#ifndef TRAFFICLIGHTWAITINGREQUIRED_H_
#define TRAFFICLIGHTWAITINGREQUIRED_H_

#include "../src/sc_types.h"
#include "TrafficLightWaiting.h"

#ifdef __cplusplus
extern "C"
{
#endif 

/*! \file This header defines prototypes for all functions that are required by the state machine implementation.

This is a state machine uses time events which require access to a timing service. Thus the function prototypes:
	- trafficLightWaiting_setTimer and
	- trafficLightWaiting_unsetTimer
are defined.

These functions will be called during a 'run to completion step' (runCycle) of the statechart. 
There are some constraints that have to be considered for the implementation of these functions:
	- never call the statechart API functions from within these functions.
	- make sure that the execution time is as short as possible.
 
*/




/*!
 * This is a timed state machine that requires timer services
 */ 

/*! This function has to set up timers for the time events that are required by the state machine. */
/*! 
	This function will be called for each time event that is relevant for a state when a state will be entered.
	\param evid An unique identifier of the event.
	\time_ms The time in milli seconds
	\periodic Indicates the the time event must be raised periodically until the timer is unset 
*/
extern void trafficLightWaiting_setTimer(TrafficLightWaiting* handle, const sc_eventid evid, const sc_integer time_ms, const sc_boolean periodic);

/*! This function has to unset timers for the time events that are required by the state machine. */
/*! 
	This function will be called for each time event taht is relevant for a state when a state will be left.
	\param evid An unique identifier of the event.
*/
extern void trafficLightWaiting_unsetTimer(TrafficLightWaiting* handle, const sc_eventid evid);



#ifdef __cplusplus
}
#endif 

#endif /* TRAFFICLIGHTWAITINGREQUIRED_H_ */
/

Basically the proper time handling has to be implemented by the developer, because timer functions generally depend on the hardware target used. So for each hardware target the client code must provide a function to set a timer and another function to unset it. These functions have to be implemented externally and have to be linked to the generated code.

The following functions are dealing with timing functionality:

Function setTimer

A state machine calls the setTimer() function – short for the function’s full name like e.g. void trafficLight_setTimer(TrafficLight* handle, const sc_eventid evid, const sc_integer time_ms, const sc_boolean periodic) – to tell the timer service that it has to start a timer for the given time event identifier and raise it after the period of time specified by the time_ms parameter has expired. It is important to only start a timer thread or a hardware timer interrupt within the setTimer() function and avoid any time-consuming operations like extensive computations, sleeping or waiting. Never call the statechart API functions from within these functions! Otherwise the state machine execution might hang within the timer service or might not show the expected runtime behavior.

In order to have the timer service raise the time event periodically, the parameter periodic must be true.

Function unsetTimer

The state machine calls the function trafficLight_unsetTimer(TrafficLight* handle, const sc_eventid evid) to notify the timer service to unset the timer for the given event ID.

Function raiseTimeEvent

In order to notify the state machine about the occurence of a time event after a period of time has expired, the raiseTimeEvent() function – defined in the header file of the state machine – needs to be called on the state machine. In the case of the traffic light example it is named trafficLight_raiseTimeEvent(const TrafficLight* handle, sc_eventid evid) (in file TrafficLight.h).

The time event is recognized by the state machine and will be processed during the next run cycle.

You can conclude that in order to process the time events raised by the timing service without too much latency, the runtime environment has to call the state machine’s runCycle() function 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() function only once per 1000 ms, the event will quite likely not be processed at the correct points in time.

Operation callbacks

YAKINDU Statechart Tools support client code operations that can be called by a state machine and that are executed as actions. These operations have to be implemented in order to make a statechart executable. 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 in DefaultSMRequired.h:


#ifndef DEFAULTSMREQUIRED_H_
#define DEFAULTSMREQUIRED_H_

#include "sc_types.h"
#include "DefaultSM.h"

#ifdef __cplusplus
extern "C"
{
#endif 

/*! \file This header defines prototypes for all functions that are required by the state machine implementation.

This state machine makes use of operations declared in the state machines interface or internal scopes. Thus the function prototypes:
	- defaultSMIfaceSample_myOperation
are defined.

These functions will be called during a 'run to completion step' (runCycle) of the statechart. 
There are some constraints that have to be considered for the implementation of these functions:
	- never call the statechart API functions from within these functions.
	- make sure that the execution time is as short as possible.
 
*/
extern sc_integer defaultSMIfaceSample_myOperation(DefaultSM* handle, const sc_integer p1, const sc_boolean p2);




#ifdef __cplusplus
}
#endif 

#endif /* DEFAULTSMREQUIRED_H_ */

An additional external function declaration sc_integer defaultSMIfaceSample_myOperation(DefaultSM* handle, const sc_integer p1, const sc_boolean p2) has been generated. This function has to be implemented and linked with the generated code, so that the state machine can use it.

Integrating generated code

To get a clue how to integrate a generated C state machine with your project, have a look at this main.c file and its main() function. Please note that the trafficLight_setTimer and trafficLight_unsetTimer functions are implemented here as well:

#include "org_yakindu_sct_examples_c_trafficlight.h"

#include "src-gen/sc_types.h"
#include "src-gen/TrafficLight.h"
#include "statemachine/TrafficLightTimer.h"
#include "statemachine/TrafficLightRunner.h"

TrafficLightTimer *timer;

int main(int argc, char *argv[]) {
    TrafficLight handle;
    trafficLight_init(&handle);
    timer = new TrafficLightTimer(&handle);
    trafficLight_enter(&handle);
    QApplication a(argc, argv);
    TrafficLightRunner *runner = new TrafficLightRunner(&handle, 100);
    org_yakindu_sct_examples_c_trafficlight w(0, runner);
    w.show();
    int ret = a.exec();
    return ret;
}

void trafficLight_setTimer(const sc_eventid evid,
        const sc_integer time_ms, const sc_boolean periodic) {
    timer->setTimer(evid, time_ms, periodic);
}

void trafficLight_unsetTimer(const sc_eventid evid) {
    timer->unsetTimer(evid);
}

First an instance of the statechart data structure is created and initialized by the trafficLight_init(&handle) function. The next step instantiates the timer. The class TrafficLightTimer represents an implementation of a timer service and uses the timer functionality of the Qt framework. The TrafficLightRunner is a runtime service that executes a run-to-completion step of the state machine every 100 ms. The runner class and the GUI are wired in the C++ class org_yakindu_sct_examples_c_trafficlight:

#include "org_yakindu_sct_examples_c_trafficlight.h"

org_yakindu_sct_examples_c_trafficlight::org_yakindu_sct_examples_c_trafficlight(
        QWidget *parent, TrafficLightRunner *runner) :
        QMainWindow(parent) {

    ui.setupUi(this);
    crossing = new CrossingWidget(this);

    trafficLight = new TrafficLightWidget(crossing);
    trafficLight->setGeometry(275, 75, 30, 90);

    pedestrianLight = new PedestrianLightWidget(crossing);
    pedestrianLight->setGeometry(50, 10, 70, 20);

    connect(runner, SIGNAL(cycleDone(TrafficLight*)), this, SLOT(update(TrafficLight*)));

    pedestrianReq = new QPushButton("pedestrian request", this);
    pedestrianReq->setGeometry(1, 365, 150, 30);
    connect(pedestrianReq, SIGNAL(released()), runner, SLOT(raisePedestrianRequest()));


    off = new QPushButton("off / on", this);
    off->setGeometry(249, 365, 150, 30);
    connect(off, SIGNAL(released()), runner, SLOT(raiseOnOff()));
}

void org_yakindu_sct_examples_c_trafficlight::update(
        TrafficLight *handle) {
    trafficLight->setSignals(handle-&gt;ifaceTrafficLight.red,
            handle->ifaceTrafficLight.yellow, handle->ifaceTrafficLight.green);
    pedestrianLight->setSignals(handle->ifacePedestrian.request,
            handle->ifacePedestrian.red, handle->ifacePedestrian.green);
    QMainWindow::update();
}

org_yakindu_sct_examples_c_trafficlight::~org_yakindu_sct_examples_c_trafficlight() {

}





C++ code generator

C++ code generator features

The C++ generator features are the same a the C generator features, plus the following ones.

GeneratorOptions feature

The GeneratorOptions feature allows to change the behavior of the C++ generator:

  • innerFunctionVisibility (String, optional): This parameter is used to change the visibility of inner functions and variables. By default private visibility is used. It can be changed to protected to allow function overriding for a class which inherits from the generated state machine base class.
  • staticOperationCallback (Boolean, optional): If this parameter is set to true, the callback function declaration for statechart operations is static and the functions are called statically by the state machine code.

Example:

feature GeneratorOptions {
    innerFunctionVisibility = "protected"
    staticOperationCallback = true
}

Includes feature

The Includes feature allows to change the way include statements are generated:

  • useRelativePaths (Boolean, optional): If this parameter is set to true, relative paths are calculated for include statements, otherwise simple includes are used. Default value is true.

Example:

feature Includes {
    useRelativePaths = false
}

Specification of C++ code

In order to describe the API specifications of the code generated by the YAKINDU C++ 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

You will find the generated code in the src-gen folder of the traffic light example.

The StatemachineInterface.h header file defines the fundamental state machine interface methods. This file also contains the definition of the abstract class StatemachineInterface which contains pure virtual functions only. It is needed by each particular state machine and is independend from concrete ones.

Statemachine class

The state machine source code is generated as a C++ class with the same name as the statechart. For example, if the statechart is named DefaultSM the C++ class will also be called DefaultSM, and it will be generated in the source code file DefaultSM.cpp.

Abstract class StatemachineInterface

Each generated state machine implements the interface StatemachineInterface:


#ifndef STATEMACHINEINTERFACE_H_
#define STATEMACHINEINTERFACE_H_

/*
 * Basic interface for state machines.
 */
class StatemachineInterface
{
	public:
	
		virtual ~StatemachineInterface() = 0;
		
		/*
		* Initializes the state machine. Used to initialize internal variables etc.
		*/
		virtual void init() = 0;
	
		/*
		* Enters the state machine. Sets the state machine into a defined state.
		*/
		virtual void enter() = 0;
	
		/*
		* Exits the state machine. Leaves the state machine with a defined state.
		*/
		virtual void exit() = 0;
	
		/*
		* Start a run-to-completion cycle.
		*/
		virtual void runCycle() = 0;
		
		/*
		* 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.
	 	*/	
		virtual	sc_boolean isActive() = 0;
		
		/*
		* Checks if all active states are final. 
	 	* If there are no active states then the state machine is considered being inactive. In this case this method returns false.
	 	*/
		virtual sc_boolean isFinal() = 0;
};

inline StatemachineInterface::~StatemachineInterface() {}

#endif /* STATEMACHINEINTERFACE_H_ */


Fundamental statechart methods

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

The StatemachineInterface interface specifies the four functions init(), enter(), exit() and runCycle().

  • The init() function 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() function.

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

  • The exit() function 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() function 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 abstract classes TimedStatemachineInterface and TimerInterface are generated. Also, the main statechart class has two constants: timeEventsCount and parallelTimeEventsCount, which are the overall number of time events in the statechart and the maximum number of time events that can be active simultaneously. For example, time events in states within the same region can never be active in parallel, while time events in a parent state can be active together with the time events of child states. You can use these constants in a timer service to allocate sufficient memory for the timers.

The TimedStatemachineInterface interface extends the generated state machine by a TimerInterface data member. The client code must provide an implementation of that interface.

TimedStatemachineInterface also specifies the callback function raiseTimeEvent(sc_eventid event), enabling the timer service to raise time events.


#ifndef TIMEDSTATEMACHINEINTERFACE_H_
#define TIMEDSTATEMACHINEINTERFACE_H_

#include "sc_types.h"
#include "TimerInterface.h"

/*
* Interface for state machines which use timed event triggers.
*/
class TimedStatemachineInterface {
	public:
	
		virtual ~TimedStatemachineInterface() = 0;
		
		/*
		* Set the ITimerService for the state machine. It must be set
		* externally on a timed state machine before a run cycle can be correct
		* executed.
		*/
		virtual void setTimer(TimerInterface* timer) = 0;
		
		/*
		* Returns the currently used timer service.
		*/
		virtual TimerInterface* getTimer() = 0;
		
		/*
		* Callback method if a time event occurred.
		*/
		virtual void raiseTimeEvent(sc_eventid event) = 0;
};

inline TimedStatemachineInterface::~TimedStatemachineInterface() {}

#endif /* TIMEDSTATEMACHINEINTERFACE_H_ */

Basically the proper time handling has to be implemented by the developer, because timer functions generally depend on the hardware target used. So for each hardware target a timer service class implementing the TimerInterface interface has to be developed.

Let’s have a look at the TimerInterface interface:


#ifndef TIMERINTERFACE_H_
#define TIMERINTERFACE_H_

#include "sc_types.h"

//forward declaration of TimedStatemachineInterface to avoid cyclic dependency
class TimedStatemachineInterface;

/*
 * Basic interface for state machines.
 */
class TimerInterface
{
	public:
		
		virtual ~TimerInterface() = 0;
	
		/*
		 * Starts the timing for a time event.
		 */ 
		virtual void setTimer(TimedStatemachineInterface* statemachine, sc_eventid event, sc_integer time, sc_boolean isPeriodic) = 0;
		
		/*
		 * Unsets the given time event.
		 */
		virtual void unsetTimer(TimedStatemachineInterface* statemachine, sc_eventid event) = 0;
	
		/*
		 * Cancel timer service. Use this to end possible timing threads and free
		 * memory resources.
		 */
		virtual void cancel() = 0;
};

inline TimerInterface::~TimerInterface() {}

#endif /* TIMERINTERFACE_H_ */

The TimerInterface interface defines the following functions dealing with timing functionality:

Function setTimer

A state machine calls the setTimer(TimedStatemachineInterface* statemachine, sc_eventid event, sc_integer time, sc_boolean isPeriodic) function to tell the timer service that it has to start a timer for the given time event and raise it after the period of time specified by the time parameter has expired. It is important to only start a timer thread or a hardware timer interrupt within the setTimer() function and to avoid any time-consuming operations like extensive computations, Thread.sleep(…), or waiting. Otherwise the state machine execution might hang within the timer service or might not show the expected runtime behavior.

In order to have the timer service raise the time event periodically, the parameter isPeriodic must be true.

Function unsetTimer

The state machine calls the function unsetTimer(TimedStatemachineInterface* statemachine, sc_eventid event) to notify the timer service to unset the timer for the given event ID.

Function raiseTimeEvent

In order to notify the state machine about the occurence of a time event after a period of time has expired, the function raiseTimeEvent(sc_eventid event) must be called on the state machine. For this purpose, the state machine must implement the TimedStatemachineInterface interface.

The time event is recognized by the state machine and will be processed during the next run cycle.

You can conclude that in order to process the time events raised by the timing service without too much latency, the runtime environment has to call the state machine’s runCycle() function 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() function with a frequency of once per 1000 ms only, the event will quite likely not be processed at the correct points in time.

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 C++ code, these interfaces can be found as internal subclasses of the main state machine class. This outer class' name is derived from the statechart’s name while the internal subclasses' names are derived from the respective names of the statechart interfaces.

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

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

The generated interface code is shown below in the inner class SCI_Sample. Since the statechart’s name is DefaultSM the state machine class' name is also DefaultSM, to be found in the DefaultSM.h file:


#ifndef DEFAULTSM_H_
#define DEFAULTSM_H_

#include "sc_types.h"
#include "StatemachineInterface.h"

/*! \file Header of the state machine 'DefaultSM'.
*/

class DefaultSM : public StatemachineInterface
{
	
	public:
		
		DefaultSM();
		
		~DefaultSM();
		
		/*! Enumeration of all states */ 
		typedef enum
		{
			main_region_MyState,
			DefaultSM_last_state
		} DefaultSMStates;
		
		//! Inner class for Sample interface scope.
		class SCI_Sample
		{
			
			public:
				/*! Gets the value of the variable 'a' that is defined in the interface scope 'Sample'. */ 
				sc_boolean get_a();
				
				/*! Sets the value of the variable 'a' that is defined in the interface scope 'Sample'. */ 
				void set_a(sc_boolean value);
				
				/*! Raises the in event 'evA' that is defined in the interface scope 'Sample'. */ 
				void raise_evA(sc_boolean value);
				
				/*! Checks if the out event 'evB' that is defined in the interface scope 'Sample' has been raised. */ 
				sc_boolean isRaised_evB();
				
				/*! Gets the value of the out event 'evB' that is defined in the interface scope 'Sample'. */ 
				sc_integer get_evB_value();
				
				
			private:
				friend class DefaultSM;
				sc_boolean a;
				sc_boolean evA_raised;
				sc_boolean evA_value;
				sc_boolean evB_raised;
				sc_integer evB_value;
		};
				
		
		/*! Returns an instance of the interface class 'SCI_Sample'. */
		SCI_Sample* getSCI_Sample();
		
		
		void init();
		
		void enter();
		
		void exit();
		
		void runCycle();
		
		/*!
		* Checks if the state machine is active (until 2.4.1 this method was used for states).
		* 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.
		*/
		sc_boolean isActive();
		
		
		/*!
		* Checks if all active states are final. 
		* If there are no active states then the state machine is considered being inactive. In this case this method returns false.
		*/
		sc_boolean isFinal();
		
		
		/*! Checks if the specified state is active (until 2.4.1 the used method for states was calles isActive()). */
		sc_boolean isStateActive(DefaultSMStates state);
	
	private:
	
	
		//! the maximum number of orthogonal states defines the dimension of the state configuration vector.
		static const sc_integer maxOrthogonalStates = 1;
		
		
		DefaultSMStates stateConfVector[maxOrthogonalStates];
		
		sc_ushort stateConfVectorPosition;
		
		SCI_Sample ifaceSample;
		
		// prototypes of all internal functions
		
		void enseq_main_region_MyState_default();
		void enseq_main_region_default();
		void exseq_main_region_MyState();
		void exseq_main_region();
		void react_main_region_MyState();
		void react_main_region__entry_Default();
		void clearInEvents();
		void clearOutEvents();
		
};
#endif /* DEFAULTSM_H_ */

A statechart interface is generated as an internal subclass within the state machine class. The subclass' 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 C++ subclass SCInterface.

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

For an outgoing event evB:integer the functions isRaised_evB() and get_evB_value() are generated. The former can be used to determine whether the event has already 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 functions, here sc_boolean get_a() and void set_a(sc_boolean value).

The code generator also creates appropriately named getter functions in the enclosing class which can be used to acquire the inner classes, here: SCI_Sample* getSCI_Sample().

The nesting class is the generated state machine source code. It holds instances of the nested interface class implementations and provides them via getter functions. Have a look at (some snippets of) the source code generated for the Sample statechart interface in the source file DefaultSM.cpp:

void DefaultSM::SCI_Sample::raise_evA(sc_boolean value)
{
	evA_value = value;
	evA_raised = true;
}

sc_boolean DefaultSM::SCI_Sample::isRaised_evB()
{
	return evB_raised;
}

sc_integer DefaultSM::SCI_Sample::get_evB_value()
{
	return evB_value;
}


sc_boolean DefaultSM::SCI_Sample::get_a()
{
	return a;
}

void DefaultSM::SCI_Sample::set_a(sc_boolean value)
{
	a = value;
}

The value of an event can be accessed only if the event has been processed in a run-to-completion step. Otherwise the event could contain an illegal value.

Operation callbacks

YAKINDU Statechart Tools support client code operations that can be used by a state machine and are executed as as actions. These operations have to be implemented in the client code in order to make a statechart executable. 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 additionally generated code for operation support in the DefaultSM.h header file:

				//! Inner class for Sample interface scope operation callbacks.
				class SCI_Sample_OCB
				{
					public:
						virtual ~SCI_Sample_OCB() = 0;
						
						virtual sc_integer myOperation(sc_integer p1, sc_boolean p2) = 0;
				};
				
				/*! Set the working instance of the operation callback interface 'SCI_Sample_OCB'. */
				void setSCI_Sample_OCB(SCI_Sample_OCB* operationCallback);

An additional interface SCI_Sample_OCB with the pure virtual function sc_integer myOperation(sc_integer p1, sc_boolean p2) has been generated. This interface has to be implemented, and an instance of the implementing class has to be provided to the state machine via the setSCI_Sample_OCB(SCI_Sample_OCB* operationCallback) function, so that the state machine can use it.

#include "DefaultSM.h"

sc_integer DefaultSM::SCI_Sample_OCB::myOperation(sc_integer p1, sc_boolean p2) {
    // Your operation code should be placed here;
    return 0;
}

int main(int argc, char *argv[]) {
    DefaultSM *defaultSM = new DefaultSM();
    SCI_Sample_OCB *sci_Sample_OCB = new SCI_Sample_OCB();

    defaultSM->setSCI_Sample_OCB(sci_Sample_OCB);

    defaultSM->init();
    defaultSM->enter();
    defaultSM->runCycle();
}





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

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

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

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.

Example:

feature GeneralFeatures {
    InterfaceObserverSupport = true
    RuntimeService = true
    TimerService = 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();
}



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

Custom code generators

Although YAKINDU Statechart Tools are shipped with powerful code generators for C, C++, Java, and TypeScript, it may be necessary to create a custom code generator to support a variety of use cases. One reason for a custom code generator could be to support additional programming languages – in this case we would be happy to receive a contribution! – or to generate code for an existing framework.

Prerequisits

Implementing a custom code generator is no trivial task. Before you get started, you should have a basic understanding about the Eclipse Modeling Framework that we are using to structure our data model. Furthermore, we highly recommend to use Xtend as your code generator’s template language, although plain old Java is still supported. Xtend provides some great features like Template Expressions, Lambdas, and Polymorphic Method Invocation, boosting readability and productivity.

Creating a new custom code generator project

Creating custom code generators is a built-in concept in YAKINDU Statechart Tools. You can develop custom code generators directly within your project workspace. You can choose between Xtend2 (preferred) or Java for implementing the code generator templates.

To set up a new (Xtend2/Java) generator project, select File → New → Other... → YAKINDU SCT → Xtend2/Java Generator Project_ and click Next.

Creating an Xtend2 generator project

Creating an Xtend2 generator project

The wizard asks for a Project name and the name of the Generator class, which has to be the fully-qualified class name. If you check the Use Xtend checkbox, the generator class will initially be created as an Xtend class. Otherwise, Java will be used for the generator.

The check box Configure for Plugin Export adds all required extension point registrations to the new project for exporting it as a plugin. The generator model can refer to the new generator plugin via its unique Generator ID. If you want to contribute custom generator features for your code generator, check the Create Feature Library check box. Click Finish to close the wizard.

Created generator project

Created generator project

Voilà! The wizard creates a new generator project for you with a structure as shown above. The file CustomGenerator.xtend contains a simple default code generator that simply prints the name of the statechart and all of its states to the target file.

Executing the custom code generator

To test your custom code generator, create a new project containing a YAKINDU Statechart Tools model as described in section "Editing statecharts".

After that, create a new generator model as described in section "Configuring a generator". Select Custom Xtend/Java based code generator as the generator to use. As you may have noticed, the generator model for the yakindu::generic generator contains an additional feature called Generator. This is where you can specify the name of your custom generator project and the fully-qualified generator class name as shown below.

GeneratorModel for yakindu::generic {
	statechart MyStatechart {
		feature Outlet {
			targetProject = "SCTExample"
			targetFolder = "src-gen"
		}
		feature Generator {
			generatorProject = "MyCustomGenerator"
			generatorClass = "org.yakindu.CustomGenerator"
		}
	}
}

When you right click the .sgen file and select Generate Statechart Artifacts from the context menu, the generator is executed and creates a new file src-gen/MyStatechart.txt with the following contents:

The name of the Statemachine is 'MyStatechart'
The Statemachine has the following states:
main_region.A
main_region.B

Congratulations, you successfully created a custom generator project! Add

The Statemachine contains «flow.states.size» states

to the CustomGenerator.xtend file and regenerate. The MyStatechart.txt file will be updated immediately, a very powerful feature of YAKINDU Statechart Tools. You can develop your code generator at runtime with zero turnaround time. Just click Generate and see the results.

SGen features for custom code generators

Generator feature

The Generator feature allows to configure a custom code generator located in the workspace and written in Java or in another JVM language.

It is a required feature and has the following parameters:

  • generatorProject (String, required): Name of the generator project
  • generatorClass (String, required): Fully-qualified name of the code generator class

Example:

feature Generator {
    generatorProject = "org.yakindu.sct.mygenerator"
    generatorClass = "org.yakindu.sct.MyGenerator"
}

Different meta models for different use cases

The SGraph meta model
The SGraph meta model defines the structural aspects of the statechart model and is similiar to the state machine model defined by the Unified Modeling Language (UML). The diagram below shows a simplified version:

Simplified SGraph meta model

Simplified SGraph meta model

  • Statechart extends CompositeElement, therefore it contains 0..* Regions. It is the root element of the model.
  • CompositeElement is an abstract type that contains Regions. Known subclasses are Statechart and State.
  • Region contains 1..* Vertices.
  • Vertex is an abstract type representing nodes in the SGraph tree. Vertices contain outgoing Transitions.
  • RegularState is an abstract type derived from Vertex. It has no additional features, but it is the common base type for State and FinalState.
  • State is derived from RegularState and CompositeElement, thus it may contain Regions and Transitions.
  • FinalState is derived from RegularState. It indicates the completion of its containing Region.
  • PseudoState is an abstract type derived from Vertex. It has no additional features, but is the common base type for Choice, Entry, Exit and Synchronization.
  • Choice is a Pseudostate with the additional attribute kind, that determines whether the element has static or dynamic execution semantics.
  • Entry is a Pseudostate with the additional attribute kind. An Entry may be of the kind Initial, ShallowHistory or DeepHistory.
  • Exit is a Pseudostate. It is defined as the point where a region is left.
  • Synchronization is a Pseudostate. It is equivalent to a Fork and to a Join.
  • Transition is defined as a directed relationship between two vertices.