Python code generator

This chapter describes the required steps for generating Python code with YAKINDU Statechart Tools. Furthermore, all components of the generated code will be described in detail and each configurable generator feature will be explained.


Table of content:

Statechart example model

We will use the following example to explain the code generation, the generated code and the integration with some application code. The example model describes a light switch with two states and the following behavior:

  • The on_button event turns the light on and, upon repeated pushing, turns the brightness higher until the latter’s maximum is reached.
  • The off_button event turns the light off.
  • When the light is on, it automatically turns off after 30 seconds.
  • Whenever a state is entered, an outgoing event is raised.

The ‚Python Code Generation’ example can be found in the example wizard:
File -> New -> Example... -> YAKINDU Statechart Examples -> Getting Started – Code Generation -> Python Code Generation

Statechart example model

Statechart example model

Generating Python code


Generating Python code from a statechart requires a generator file (.sgen). It must at least specify the yakindu::python generator, reference a statechart and define the targetProject and targetFolder in the Outlet feature. By specifying these attributes, Python state machine code can be generated.

Example:

GeneratorModel for yakindu::python {

	statechart LightSwitch {

		feature Outlet {
			targetProject = "com.yakindu.examples.python.lightswitch"
			targetFolder = "src-gen"
		}
	}
}

You can create a generator model with the YAKINDU Statechart generator model wizard by selecting File → New → Code generator model.

The code generation is performed automatically whenever the statechart or the generator file is modified. See also chapter Running a generator for more information.

Generated code files

Generated code files can be categorized into base and library files.

The base source file is the implementation of the state machine model as a Python class. It is generated into the folder defined by the targetFolder parameter. Its package is composed of the basePackage parameter and the statechart’s namespace. The file name is derived from the statechart name.

Library files are independent of the concrete state machine model. They are generated into the libraryTargetFolder , or if that one is not defined, into the targetFolder . Their package is yakindu if not otherwise defined by the libraryPackage parameter.

  • rx.py: Contains the Observer and Observable classes used for the observer mechanism to react on outgoing events. It is generated only if it is required.
  • timer_service.py: A default timer service implementation. It is only generated if enabled in the generator model.

The state machine class

The Python code generator translates the state machine model into a Python class.

The state machine class contains fundamental methods to enter and exit the state machine, as well as a method to execute a run-to-completion step. For the light switch example, these functions are generated as follows:

class LightSwitch:
	
	def __init__(self):
		""" Declares all necessary variables including list of states, histories etc. 
		"""
		self.user = LightSwitch.User(self)
		self.light = LightSwitch.Light(self)
		...
		# for timed statechart:
		self.timer_service = None
		...
	
	def enter(self):
		...
	
	def exit(self):
		...
	
	def run_cycle(self):
		...

  • Variables are initialized to their respective default values. If the statechart defines any initialized variables, these initializations are also done in the constructor.
  • 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. For event-driven statecharts, this method is called automatically when an event is received. For cycle-based statecharts, this methods needs to be called explicitly in the client code. See also chapter Execution schemes. Somewhat simplified, a run-to-completion cycle consists of the following steps:
    • Clear the list of outgoing events.
    • Check whether any events have occurred which are leading to a state change.
    • If a state change has to be done:
      • Make the present state inactive.
      • Execute exit actions of the present state.
      • Save history state, if necessary.
      • Execute transition actions, if any.
      • Execute entry actions of the new state.
      • Make the new state active.
    • Clear the list of incoming events.

Furthermore, the class defines methods to check whether the state machine is active, final or whether a specific state is active:

	def is_active(self):
		...
	
	def is_final(self):
		...
			
	def is_state_active(self, state):
		...

  • The is_active() method checks whether the state machine is active, i.e. at least one state of the 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.
  • The is_final() method checks whether the 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.
  • The is_state_active() method checks whether the specified state is active.

The state machine class allows to raise events and access variables which are defined in the statechart’s interface declarations. Each named statechart interface is reflected by an inner class. For the example model above, the two statechart interfaces user and light are transformed into the two classes User and Light:

class User:
	
	"""Implementation of scope User.
	"""
	
	def __init__(self, statemachine):
		self.on_button = None
		self.off_button = None
		self.statemachine = statemachine
	
	def raise_on_button(self):
		self.statemachine._LightSwitch__in_event_queue.put(self.raise_on_button_call)
		self.statemachine.run_cycle()
	
	def raise_on_button_call(self):
		self.on_button = True
	
	def raise_off_button(self):
		self.statemachine._LightSwitch__in_event_queue.put(self.raise_off_button_call)
		self.statemachine.run_cycle()
	
	def raise_off_button_call(self):
		self.off_button = True

class Light:

	"""Implementation of scope Light.
	"""
	
	def __init__(self, statemachine):
		self.brightness = None
		self.on = None
		self.on_observable = Observable()
		self.off = None
		self.off_observable = Observable()
		self.statemachine = statemachine
	
	def __raise_on(self):
		self.on = True
		
	def __raise_off(self):
		self.off = True
			

In this example code you can see the following:

  • Interface variables are reflected as class members (e.g. brightness) with corresponding visibility.
  • Incoming events are transformed into methods to raise such an event from the client code.
  • Outgoing events are transformed into observable objects to which the client code can subscribe.

A statechart can also define an unnamed interface. In this case, all the members and methods are defined directly in the state machine class.

Bringing it all together, the resulting class for the light switch example looks like the following. For documentation purposes we only show the public members here:

# Implementation of statechart light_switch.

from yakindu.rx import Observable
import queue

class LightSwitch:
	
	class State:
		(
			main_region_off,
			main_region_on,
			null_state
		) = range(3)
	
	class User:
		def __init__(self, statemachine):
			self.on_button = None
			self.off_button = None
			self.statemachine = statemachine
		
		def raise_on_button(self):
			...
	
		def raise_off_button(self):
			...
	
	class Light:
		def __init__(self, statemachine):
			self.brightness = None
			self.on = None
			self.on_observable = Observable()
			self.off = None
			self.off_observable = Observable()
			self.statemachine = statemachine
	
	
	def __init__(self):
		""" Declares all necessary variables including list of states, histories etc. 
		"""
		self.user = LightSwitch.User(self)
		self.light = LightSwitch.Light(self)
		...
		# for timed statechart:
		self.timer_service = None
		...
	
	def is_active(self):
		...
	
	def is_final(self):
		...
			
	def is_state_active(self, state):
		...
		
	def time_elapsed(self, event_id):
		...
	
	def raise_time_event(self, event_id):
		...
	
	def enter(self):
		...
	
	def exit(self):
		...
	
	def run_cycle(self):
		...


The following code snippet demonstrates how to use the state machine API:

class Main:
    def __init__(self):
        # Instantiates the state machine 
        self.sm = LightSwitch()

    def run(self):
        # Enters the state machine; from this point on the state machine is ready to react on incoming event 
        self.sm.enter()
        # Raises the on_button event in the state machine which causes the corresponding transition to be taken 
        self.sm.user.raise_on_button()
        # Gets the value of the brightness variable 
        brightness = self.sm.light.brightness
        # Exit the state machine
        self.sm.exit()

Observing outgoing events

There are basically two ways to access outgoing events, getters and observables. The desired option can be enabled in the generator model, see OutEventAPI .

The getter mechanism is straight forward and simply allows to check if an outgoing event is raised by calling an is_raised method that returns a boolean:

class LightSwitch:
    ...
    class Light:
         def is_raised_on(self):
            return self.on

        def is_raised_off(self):
            return self.off

The observable mechanism is more complex to set up, but it allows to get notified whenever the event is raised. Thus, the client code does not need to check the event status explicitly. The client code basically needs to set an Observer object to the out event’s Observable.

First of all, we need to specify the Observer:

# Observer with callback for the light.on event 
class LightOnObserver(Observer):
    def next(self):
        print("Light is on.")


Then, we need to instantiate the observer and subscribe it to the out event observable:

# Subscribes observers to the state machine's observables 
self.lightOnObserver = self.LightOnObserver()
self.sm.light.on_observable.subscribe(self.lightOnObserver)


With that code, observer’s next() function will be called whenever the out event on in interface light is raised by the state machine.

Serving operation callbacks

YAKINDU Statechart Tools supports operations that are executed by a state machine as actions, but are implemented by client-side code.
As a simple example a function myOp can be defined in the definition section of the LightSwitch example:

interface:
operation myOp()

Calling the operation myOp in the On state of the LightSwitch example generates following operation call:

def entry_action_main_region_on(self):
	self.sci_interface.operation_callback.my_op()

The operation callback must be set through the state machine’s API. At first, the Callback must be implemented:

class Callback
	def _init_(self):
		#empty constructor
		pass
	
	def my_op(self):
		print('Operation myOp has been called by the state machine')

After this, the callback must be set while before running the state machine. This could be realized like this:

class Main:
    def __init__(self):
        self.sm = LightSwitch()
        self.cb = Callback()

    def setup(self):
        self.sm.sci_interface.operation_callback = self.cb
        self.sm.enter()

Time-controlled state machines

State machines using timed triggers, for example after 10 s, are timed and need a timer service. The timer service needs to be set by the client code before entering the state machine.

Default timer service

The python code generator comes with a timer out of the box. To generate the default timer, the property DefaultTimer can be set in GeneralFeatures.

feature GeneralFeatures {
	DefaultTimer = true
}

Activating this feature provides a generated timer implementation ready to use for timed state machines. To use the timer, create an instance and hand it over using the state machine’s API.


from lightswitch.timer.sct_timer import Timer

class Main:
    def __init__(self):
        self.sm = LightSwitch()
        self.timer = Timer()

    def setup(self):
        self.sm.set_timer_service(self.timer)
        self.sm.enter()

Runtime template

The python code generator comes with a runtime service template. To generate it, set the RuntimeTemplate flag in GeneralFeatures.

feature GeneralFeatures {
	RuntimeTemplate = true
}

Activating the runtime template feature generates a default_runtime.py file, which supports basic functionalities to run a state machine. Custom code for use case depending projects can be added at the commented areas. Calling the state machine using the template ensures initializing and entering the state machine. After this, the state machine will be called in a while True loop:

import sys, os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from lightswitch.lightswitch_statemachine import LightSwitch

class LightSwitchRuntime:
	
	def __init__(self):
		self.sm = LightSwitch()
		# Enter custom init code here..
		
		
	"""
	Enter custom methods here..
	"""
	
	
	def setup(self):
		""" Get statemachine ready and enter it.
		"""
		self.sm.enter()
		
	def run(self):
		""" Include your interface actions here
		"""
		while True:
			# enter what you like to do
			self.sm.run_cycle()
		
	def shutdown(self):
		""" Unset timer and exit statemachine.
		"""
		print('State machine shuts down.')
		self.sm.exit()
		print('Bye!')
		
		
if __name__ == "__main__":
	sr = LightSwitchRuntime()
	sr.setup()
	sr.run()
	sr.shutdown()

Python code generator features

Beside the general code generator features, there are language specific generator features, which are listed in the following chapter.

Naming feature

The Naming feature allows the configuration of package names for the generated classes.

It is an optional feature and has the following parameters:

  • basePackage (String, optional): Package name for the generated statechart class. In case the statechart defines a namespace, it will be appended to this package name. In case of multiple statecharts referencing each other from different base packages, these need to be defined in the same generator model.
  • libraryPackage (String, optional): Package name for model-independent classes and interfaces.

Example:

feature Naming {
    basePackage = "org.ourproject.sct.impl"
    libraryPackage = "org.ourproject.sct.lib"
}

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:

  • DefaultTimer (Boolean, optional): Enables/disables the generation of a timer service implementation.
  • RuntimeTemplate (Boolean, optional): Enables/disables the generation of a runtime template that triggers the run cycle of a cycle-based state machine.

Example:

feature GeneralFeatures {
    DefaultTimer = true
    RuntimeTemplate = true
}

PyPackaging feature

Using the PyPackaging feature allows the user to specify a setup.py file, which can be used for packaging. The PyPackaging feature allows the configuration of:

  • CreateFiles (Boolean, optional): Specifies whether to generate a setup.py file.
  • Author (String, optional): Defines the author name.
  • Version (String, optional): Defines the version number.
  • ShortDescription (String, optional): Defines the description.
  • License (String, optional): Defines the license.
  • URL (String, optional): Defines the URL.

Example:

feature PyPackaging {
	CreateFiles = false
	Author = "admin"
	Version = "0.0.1"
	ShortDescription = "Some description"
	License = "WTFPL"
	URL = "www.your-homepage.com"
	}
}