Finite State Machines

Scripts have ability to define, include, and run Finite State Machine (FSM). FSM can transition between defined states and can be made reactive to Events and Commands.

Define a FSM

Create the FSM

To create an instance of an FSM, a helper method Fsm is provided as shown in example. This method takes following parameters:

  1. name of FSM
  2. initial state of the FSM
  3. block having states of the FSM
Kotlin
sourceval irisFsm: Fsm = Fsm(name = "iris-fsm", initState = "INIT") {
    // place to define all states of FSM
}

Define State

As mentioned above, the third parameter of Fsm method is a block which is the place to define all the states of the FSM. A method named state needs to be called with parameters name of the state and the block of actions to be performed in that state.

Kotlin
sourcestate("INIT") {
    // actions to be performed in this state
}
State names
  1. State names are case-insensitive.
  2. In case of multiple states with same name, the last one will be considered.

State Transition

To transition between states, the become method needs to be called with name of next state. This will change the state of the FSM to the next state and start executing it. An InvalidStateException will be thrown if the provided next state is not defined.

Kotlin
sourcebecome(state = "IN-PROGRESS")
Caution with Become

State transition should ideally be the last call in state or should be done with proper control flow so that become is not called multiple times.

Along with changing state, it is also possible to pass Params from the current state to the next state. Params can be given to become as the last argument, which will then be injected in the next state as a parameter.

In the case where state transition does not happen while executing a state, the FSM will stay in the same state and any re-evaluation of the FSM after that will execute the same state until a state transition happens. The reactive variables plays an important role in this as they are the way to re-evaluate the FSM state.

Kotlin
sourcestate("LOW") {
    on(temparature.first() < 20) {
        // do something but state transition does not happen
    }

    on(temparature.first() >= 20) {
        // do something and transit state
        become("HIGH")
    }
}

In the example above, the FSM is in LOW state. If the temperature is below 20, then there won’t be any state transition, and the FSM remain in the LOW state. A change in temperature after that will re-evaluate the “LOW” state again and if the temperature is greater than or equal to 20, then current state will change to HIGH. In the example temperature is an event based variable which enables re-evaluation of the current state on changes in temperature value.

Complete FSM

completeFsm marks the FSM as complete. Calling it will immediately stop execution of the FSM and next steps will be ignored. Therefore, it should be called at the end of a state.

Kotlin
sourcecompleteFsm()   // will complete the Fsm
// anything after this will not be executed

FSM Helper Constructs

The following are some useful FSM constructs.

  1. entry : executes the given block only when state transition happens from a different state

    Kotlin
    sourceentry {
        // do something
    }
  2. on : executes the given block if the given condition evaluates to true. This construct should be used for conditional execution of a task.

    Kotlin
    sourceon(temparature.first() < 20) {
        // do something but state transition does not happen
    }
    
    on(temparature.first() >= 20) {
        // do something and transit state
        become("HIGH")
    }
  3. after : executes the given block after the given duration

    Kotlin
    sourceafter(Duration.milliseconds(100)) {
        // do something
    }

Start FSM

After creating instance of FSM, it needs to be explicitly started by calling start on it. This will start executing the initial state of the FSM, which is provided while defining the instance.

Caution

Calling start more than once is not supported and will lead to unpredictable behaviour.

Kotlin
sourceirisFsm.start()

Wait for Completion

As an FSM has the ability to be complete itself, await can be called to wait for the FSM completion. Execution will be paused at the await statement until the FSM is marked complete.

Kotlin
sourceirisFsm.await()

Calling await before calling start will start the FSM internally and then wait for completion.

Reactive FSM

Reactive FSM means that changes of state can be tied to change in Events as well as Commands. An FSM can be made to react to change in Event and Command parameters with the help of Event based variables and Command flags. This reaction is called “re-evaluation”, which causes the code for the current state to be executed again. It is necessary to bind an FSM to reactive variables to achieve the reactive behavior.

Event-based variables

Event-based variables are the way to make an FSM react to CSW Events. They are linked to Events (or Parameters of Events) and are then bound to an FSM such that when the value of the linked Event (or Parameter) changes, the FSM is re-evaluated. Event-based variables can be used to share data between multiple sequencers using Events.

There are two types of Event-based variables.

EventVariable

An EventVariable will be tied to an Event published on the given EventKey. The example below shows creating an instance of an EventVariable, and the getEvent method which returns the latest event.

An EventVariable needs 2 parameters:

  • event key: specifies which Event to tie the variable to
  • duration: (optional) polling period for updating the value of the Event (Significance of duration parameter is explained below.)
Kotlin
sourceval eventVariable: EventVariable = EventVariable("ESW.IRIS_darkNight.temperature")

eventVariable.getEvent() // to get the latest Event

ParamVariable

A ParamVariable will be tied to a specific Parameter Key of an Event published on given EventKey The example below shows creating an instance of a ParamVariable and the usage of other helper methods.

A ParamVariable takes 4 parameters:

  • initial: initial value for the Parameter. The value of the parameter in the Event is updated when the ParamVariable is created.
  • event key: specifies the Event with the linked Parameter
  • param Key: specifies which Parameter to tie the variable to
  • duration: (optional) polling period for updating the value of the Parameter (Significance of duration parameter is explained below.)
Kotlin
sourceval paramVariable: ParamVariable<Int> = ParamVariable(0, "ESW.temperature.temp", tempKey)

paramVariable.getParam() // to get the current values of the parameter
paramVariable.first() // to get the first value from the values of the parameter
paramVariable.setParam(10, 11) // publishes the given values on event key

paramVariable.getEvent() // to get the latest Event

To make the FSM react to Event-based variables, we need to create an instance of the above event based variables and bind the FSM to it.

An FSM can be bound to multiple variables and vice versa.

Kotlin
sourceeventBasedVariable.bind(irisFsm)

Event-based variables have the ability to behave in one of two ways:

  • Subscribe to the Events getting published
  • Poll for a new event with a specified period

Subscribe to an Event

If the duration parameter of an Event-based variable is not specified, a subscription is made to the Event, and the value is updated (and the current state of the FSM is re-evaluated) whenever it is published.

The following example shows how to create Event Variables with the subscribing behavior and bind FSM to it.

Kotlin
source// ------------ EventVariable ---------------
val eventVariable: EventVariable = EventVariable("ESW.IRIS_darkNight.temperature")
eventVariable.bind(irisFsm)

// ------------ ParamVariable ---------------
val tempKey: Key<Int> = intKey("temperature")

val paramVariable: ParamVariable<Int> = ParamVariable(0, "ESW.temperature.temp", tempKey)
paramVariable.bind(irisFsm) // binds the FSM and event variable

Poll

If it is preferable to have the FSM re-evaluated at a constant periodic rate regardless of when new Events are published, polling behavior can be used by specifying the duration parameter when creating the Event-based variable. This can be useful when the publisher is too fast and there is no need respond so quickly to it.

The example code demonstrates this feature. The binding part is same as in previous example.

Kotlin
sourceval tempKey: Key<Int> = intKey("temperature")

// ------------ ParamVariable ---------------
val pollingParamVar: ParamVariable<Int> =
        ParamVariable(0, "ESW.temperature.temp", tempKey, Duration.seconds(2))

pollingParamVar.bind(irisFsm)

// ------------ EventVariable ---------------
val pollingEventVar = EventVariable("ESW.IRIS_darkNight.temperature", Duration.seconds(2))
pollingEventVar.bind(irisFsm)

CommandFlag

Command Flag acts as bridge that can be used to pass Parameters to an FSM from outside (i.e. via a Command Handler). A Command Flag can be defined in a scope accessible by a Command Handler and the FSM, and then be bound to the FSM. This causes the FSM to re-evaluate whenever the value of the Command Flag changes, which occurs when the set method is called in the Command Flag (which can be placed in a Command Handler, see example below).

A Command Flag can be bound to multiple FSMs, and multiple Command Flags can be bound to a single FSM. A Command Flag is limited to the scope of a single script. It does not have any effect on external scripts.

The following example shows how to create a CommandFlag, bind an FSM to it, and use the methods get and set to retrieve and set the value of parameters in the Command Flag.

Kotlin
sourceval flag = CommandFlag()
flag.bind(irisFsm) // bind the FSM and command flag

onSetup("setup-command") { command ->
    flag.set(command.params) // will set params and refreshes the bound FSMs with the new params
}

val params = flag.value() // extract the current params value in FSM
Note
  • Binding FSM to reactive variables can be done anytime in the lifecycle of FSM not only before starting it. Doing it after completion of FSM does not do anything.
  • Binding is necessary to achieve the reactive behavior.

Example FSM

In the below example, temparatureFsm demonstrates how to define and use FSM in the scripts. The Event-based variable is declared with the Event key esw.temperature.temp and parameter temperature, and the temperatureFsm is bound to it. The job of the temperatureFsm is to decide the state based on the temperature and publish it on the EventKey esw.temperatureFsm with the ParamKey state. The state is determined by comparing the “current temperature” (obtained from a ParamVariable) with the “temperatureLimit”, which defaults to 40, but can be updated using the Setup command “changeTemperatureLimit”.

THe logic of state change is:

condition state
temp == 30 FINISH
temp > tempLimit ERROR
else OK
Kotlin
source
// method to publish the state of the FSM val stateKey = stringKey("state") val tempFsmEvent = SystemEvent("esw.temperatureFsm", "state") suspend fun publishState(baseEvent: SystemEvent, state: String) = publishEvent(baseEvent.add(stateKey.set(state))) // temperature Fsm states val OK = "OK" val ERROR = "ERROR" val FINISHED = "FINISHED" // Event-based variable for current temperature val tempKey = longKey("temperature") val temperatureVar = ParamVariable(0, "esw.temperature.temp", tempKey) // CommandFlag, and method to get expected temperature from it val commandFlag = CommandFlag() fun getTemperatureLimit(defaultTemperatureLimit: Int): Int { val tempLimitParameter = commandFlag.value().get(intKey("temperatureLimit")) return if (tempLimitParameter.isDefined) tempLimitParameter.get().first else defaultTemperatureLimit } // key for parameter passed to Error state from Ok state val deltaKey = longKey("delta") // FSM definition val temperatureFsm = Fsm("TEMP", OK) { val initialTemperatureLimit = 40 // [[ 1 ]] state(OK) { val currentTemp = temperatureVar.first() // [[ 2 ]] val tempLimit = getTemperatureLimit(initialTemperatureLimit) entry { publishState(tempFsmEvent, OK) // [[ 3 ]] } on(currentTemp == 30L) { become(FINISHED) // [[ 4 ]] } on(currentTemp > tempLimit) { val deltaParam = deltaKey.set(currentTemp - tempLimit) become(ERROR, Params(setOf(deltaParam))) // [[ 5 ]] } on(currentTemp <= tempLimit) { info("temperature is below expected threshold", mapOf("limit" to tempLimit, "current" to currentTemp) ) } } state(ERROR) { params -> val tempLimit = getTemperatureLimit(initialTemperatureLimit) entry { info("temperature is above expected threshold", mapOf("limit" to tempLimit, "delta" to params(deltaKey).first) ) publishState(tempFsmEvent, ERROR) } on(temperatureVar.first() < tempLimit) { become(OK) } } state(FINISHED) { completeFsm() // [[ 6 ]] } } // bind reactives to FSM temperatureVar.bind(temperatureFsm) commandFlag.bind(temperatureFsm) // [[ 7 ]] // Command handlers onSetup("startFSM") { temperatureFsm.start() // [[ 8 ]] } onSetup("changeTemperatureLimit") { command -> commandFlag.set(command.params) // [[ 9 ]] } onSetup("waitForFSM") { temperatureFsm.await() // [[ 10 ]] info("FSM is no longer running.") }

Full example code is available here.

Key things in above example code are :

  • [[ 1 ]]: Shows top-level scope of the FSM which can used to declare variables in FSM’s scope and statements which should be executed while starting the FSM. Statements written here will be executed only once when the FSM starts.
  • [[ 2 ]]: The scope of the state. Statements written here will be executed on every evaluation of the state. So variables declared here will be reinitialized whenever state is re-evaluated. In the above case, tempLimit and currentTemp will be initialized every time the OK state is evaluated.
  • [[ 3 ]]: The code in the entry block is only executed when first transitioning to this state. Therefore, state will not be published repeatedly.
  • [[ 4 ]]: State transitions from OK state to FINISHED.
  • [[ 5 ]]: State transitions from OK state to ERROR with a Params set containing the delta temperature. The ERROR state shows how to consume Params in a state.
  • [[ 6 ]]: Marks the FSM complete. Re-evaluation or state transitions cannot happen after this is executed.
  • [[ 7 ]]: Shows the binding temperatureFsm to temperatureVar and commandFlag. After this point, a running FSM will re-evaluate whenever events are published on temperatureVar.
  • [[ 8 ]]: Starts evaluating the initial state of the FSM. Until this is called the code in the Fsm block only specifies the FSM functionality. However, note that the initialization code in the top-level scope of the FSM is executed (item [[ 1 ]]) on construction.
  • [[ 9 ]]: Updates the Params of the CommandFlag. In our example, we are using those params to specify the temperature limit.
  • [[ 10 ]]: Waits for completion of the FSM. In our example, the script execution will be blocked until the completeFsm method is called in [[ 6 ]], which occurs when switching to the FINISHED state. Any code after the await call will execute after the FSM is completed.

Example code also demos the use of the helper constructs like entry, on.