Python code generator

State machines are a good approach to generate code for different languages based on the underlying model. This chapter describes the required steps for Python code generation 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:

Python example model

For documentation purpose this simple example will be used to explain the code generation, the generated code and the code output. The example model has two states:

  • The „on” button turns on the light and, upon repeated pushing, turn the brightness higher until the latter’s maximum is reached.
  • The „off” button turns off the light.

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

Python example model

Python example model

Generate Python code


Generating Python code from a statechart requires a generator file. It must at least specify the yakindu::python generator model, 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"
		}
	}
}

Generated files

Depending on the state machine and the configuration, python state machine code will be generated. Without any features the basic variant is splitted up into two parts. An interface and the state machine implementation.

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. According to the example, this is the generated class for the „user” interface:

"""Interfaces defined in the state chart model.

The interfaces defined in the state chart model are represented
as separate classes.

"""


class SCIUser:

	"""Implementation of scope sci_user.
	"""
	
	def __init__(self, statemachine):
		self.on_button = None
		self.off_button = None
		self.brightness = None
		self.statemachine = statemachine
	
	
	def set_brightness(self, value):
		self.brightness = value
	
	def get_brightness(self):
		return brightness
	
	def raise_on_button(self,):
		self.on_button = True
		self.statemachine.run_cycle()
	
	def raise_off_button(self,):
		self.off_button = True
		self.statemachine.run_cycle()

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

  • 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. 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.
# Implementation of statechart light_switch.

# implemented interfaces:
from .light_switch_interfaces import SCIUser



class LightSwitch:

	""" State Enum
	"""
	class State:
		(
			main_region_off,
			main_region_on,
			null_state
		) = range(3)
	

	"""
	
	Implementation of the state machine 'LightSwitch'.
	
	"""
	
	def __init__(self):
		""" Declares all necessary variables including list of states, histories etc. 
		"""
		self.sci_user = SCIUser(self)
		self.initialized = False
		self.state_vector = [None] * 1
		self.next_state_index = None
		# enumeration of all states:
		self.State =  LightSwitch.State
		self.is_executing = None
		
	def init(self):
		"""	Initializes the state machine by checking the timer, 
		initializing states and clearing events.
		"""
		self.initialized = True
		for state_index in range(1):
			self.state_vector[state_index] = self.State.null_state
		self.sci_user.brightness = 0
		self.is_executing = False
		
	
	def is_active(self):
		""" @see IStatemachine#is_active()
		"""
		return (self.state_vector[0] is not self.State.null_state)
	
	def is_final(self):
		"""Always returns 'false' since this state machine can never become final.
		@see IStatemachine#is_final()
		"""
		return False
			
	def is_state_active(self, state):
		""" Returns True if the given state is currently active otherwise false.
		"""
		s = state
		if s == self.State.main_region_off:
			return self.state_vector[0] == self.State.main_region_off
		elif s == self.State.main_region_on:
			return self.state_vector[0] == self.State.main_region_on
		else:
			return False
	
	def entry_action_main_region__off(self):
		self.sci_user.brightness = 0
		
	def enter_sequence_main_region__off_default(self):
		self.entry_action_main_region__off()
		self.next_state_index = 0
		self.state_vector[0] = self.State.main_region_off
		self.state_conf_vector_changed = True
		
	def enter_sequence_main_region__on_default(self):
		self.next_state_index = 0
		self.state_vector[0] = self.State.main_region_on
		self.state_conf_vector_changed = True
		
	def enter_sequence_main_region_default(self):
		self.react_main_region__entry__default()
		
	def exit_sequence_main_region__off(self):
		self.next_state_index = 0
		self.state_vector[0] = self.State.null_state
		
	def exit_sequence_main_region__on(self):
		self.next_state_index = 0
		self.state_vector[0] = self.State.null_state
		
	def exit_sequence_main_region(self):
		state = self.state_vector[0]
		if state == self.State.main_region_off:
			self.exit_sequence_main_region__off()
		elif state == self.State.main_region_on:
			self.exit_sequence_main_region__on()
		
	def react_main_region__entry__default(self):
		self.enter_sequence_main_region__off_default()
		
	def react(self):
		return False
	
	
	def main_region__off_react(self,  try_transition):
		did_transition = try_transition
		if try_transition:
			if self.react() == False:
				if self.sci_user.on_button:
					self.exit_sequence_main_region__off()
					self.sci_user.brightness = 1
					self.enter_sequence_main_region__on_default()
				else:
					did_transition = False
		return did_transition
	
	
	def main_region__on_react(self,  try_transition):
		did_transition = try_transition
		if try_transition:
			if self.react() == False:
				if self.sci_user.off_button:
					self.exit_sequence_main_region__on()
					self.enter_sequence_main_region__off_default()
				elif ((self.sci_user.on_button) and (self.sci_user.brightness < 10)):
					self.exit_sequence_main_region__on()
					self.sci_user.brightness = self.sci_user.brightness + 1
					self.enter_sequence_main_region__on_default()
				else:
					did_transition = False
		return did_transition
	
	
	def clear_in_events(self):
		self.sci_user.on_button = False
		self.sci_user.off_button = False
	
	
	def enter(self):
		if self.is_executing:
			return
		self.is_executing = True
		self.enter_sequence_main_region_default()
		self.is_executing = False
	
	
	def exit(self):
		if self.is_executing:
			return
		self.is_executing = True
		self.exit_sequence_main_region()
		self.is_executing = False
	
	
	def run_cycle(self):
		if self.is_executing:
			return
		self.is_executing = True
		self.next_state_index = 0
		while self.next_state_index < len(self.state_vector):
			if self.state_vector[self.next_state_index] == self.State.main_region_off :
				self.main_region__off_react(True)
			elif self.state_vector[self.next_state_index] == self.State.main_region_on :
				self.main_region__on_react(True)
			self.next_state_index += 1
		self.clear_in_events()
		self.is_executing = False

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.

DefaultTimer

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.

Example:

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

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"
	}
}