Defining Script

There are 3 variations of Sequencer Scripts. These variations are based the way the Script gets executed. The variations are:

  • Regular Script
  • Finite State Machine Script (FSM Script)
  • Reusable Script

Regular Script

Regular script is like a collection of script handlers which executes the handlers of requested action or command. To define a regular script, a function named script needs to be invoked with a block which contains the logic of the script. The below example shows way to declare the script.

Kotlin
import esw.ocs.dsl.core.script

script {
    // place to add Sequencer Script logic
}

The logic can be divided into 2 parts:

  • Top-level statements (initialisation logic) : Executed while loading (initialising) the script.
  • Script Handlers: Executed when a command to execute a particular handler is received.

Script handlers are defined to process Sequence of Commands or to perform actions like Going online or offline, starting Diagnostic mode etc. Documentation of handlers can be found here. Handlers will be executed whenever there is need to execute Sequence or to perform any action on Sequencer.

Everything except Script Handlers are considered as Top-level statements and will be executed while loading the script. This is the place to declare the Script specific variables and tasks to be executed at initialisation of the Script.

Kotlin
script {
    info("Loading DarkNight script")

    val tromboneTemperatureAlarm =
            Key.AlarmKey(Prefix(NFIRAOS, "trombone"), "tromboneMotorTemperatureAlarm")

    loopAsync(1.seconds) {
        setSeverity(tromboneTemperatureAlarm, getSeverity())
    }

    onSetup("basic-setup") { command ->

        val intKey = intKey("angle")
        val angle = command.parameter(intKey).head()!!

        info("moving motor by : $angle")
        moveMotor(angle)
        info("motor moved to required position")
    }

    onObserve("start-observation") {
        info("opening the primary shutter to start observation")

        val openingStatusKey = stringKey("status").set("open")
        publishEvent(ObserveEvent("IRIS.primary_shutter", "current-status", openingStatusKey))

        openPrimaryShutter()
    }

}

The example mainly demos:

Finite State Machine Script (FSM Script)

FSM script is a way of writing Sequencer Script as Finite State Machines (FSM), where execution of Script Handler is dependent on the Current State of the Sequencer Script.

To define FSM Script a function FSMScript needs to be called with the initial state to start script with, and the block containing Script logic. The block contains initialisation logic and different states.

In FSM Script, Script handlers can be defined in two scopes :

  • Default scope - top-level scope of the Script
  • State scope - scope of a specific state.

The below code shows how to declare FSM Script and States. It also shows the scopes where handlers can be added.

Kotlin
import esw.ocs.dsl.core.FsmScript

FsmScript("INIT") {

    // Default scope
    // place for Script variable declarations and initialisation statements

    state("INIT") { params ->
        // Scope of INIT state
        // handlers of INTI state
    }

    state("IN-PROGRESS") {
        // Scope of IN-PROGRESS state
        // handlers of IN-PROGRESS state
    }

}

Initialisation of the Script takes place by executing the top-level statements, and then executing the initial state. The top-level scope is the place to declare variables which can be used in Script.

While defining handlers there is restriction about Command handlers that they can only be tied to State scope. Other Script handlers except the Command handlers can be tied both scopes of FSM script.

To execute any action, corresponding handlers in current State scope will be executed first and then handlers in Default scope will be executed. In case of a Command Sequence, if the current state does not handle Command which is being executed, the Sequence will be completed with Error with reason UnhandledCommandException.

For state transition, become needs to called from the current state with next state. It will start evaluating the next state, and will execute further actions on the next state. If the next state is not defined in Script, then an exception will be thrown saying No state declaration found for state.

It is also possible to pass Params from current state to the next state by passing them as last argument to the become function. The passed Params will be available as a function parameter while defining any State.

In below example, [[ 1 ]] shows use of become to change state. where [[ 2 ]] shows how to pass Params while changing state. The ON state shows how to consumes the Params.

Kotlin
state("ON") { params ->

    onSetup("turn-off") {
        turnOffLight()
        become("OFF")                           // [[ 1 ]]
    }
}

state("OFF") {

    onSetup("turn-on") { command ->
        turnOnLight()
        become("ON", command.params)           // [[ 2 ]]
    }
}

The State scope can have top-level statements and Script handlers. The State’s top-level statements will be executed when state transition happens. So invoking become will initialise the next state which includes calling the top-level statements. The State top-level can be used to declare variables limited to State scope which will last till state transition. After that, state will be cleared and next time it will be initialised again to default values.

Kotlin
state("SETTING-UP") { params ->

    val initialPos = params[intKey("current-position")].get().head()
    var moved = false

    onSetup("move") { command ->
        val angle = command.params[intKey("angle")].get().head()
        moveBy(angle)
        moved = true

        info("moved from : $initialPos by angle : $angle")

        become("READY")
    }

    onGoOffline {
        stopSetup()
        info("Going in offline mode")
    }

}

In the example, initialPos and moved demos declaring State scoped variables. Whenever state transition happens to some other state and back to SETTING-UP state, these variables will be reinitialised to its default values as defined in code. Transition to self will not reinitialise variables.

Reusable Script

Reusable Scripts make it possible to write the common logic which needs to shared across multiple scripts. This can be used to create small building blocks for building Sequencer Scripts.

They are same as the Regular Script except they cannot be directly loaded into a Sequence Component, and can only be loaded into other Sequencer Scripts.

The common logic consists of Script handlers and the top-level statements(initialisation logic). The top-level statements will be executed while loading the script. Script handlers will be added to the corresponding handlers of the script loading it.

Following code declares a Reusable Script with Observe Command Handler.

Kotlin
import esw.ocs.dsl.core.reusableScript

val startObservationScript = reusableScript {
    onObserve("start-observation") {
        info("opening the primary shutter to start observation")

        val openingStatusKey = stringKey("status").set("open")
        publishEvent(ObserveEvent("IRIS.primary_shutter", "current-status", openingStatusKey))

        openPrimaryShutter()
    }

}

Loading in Regular Script

To use Reusable Scripts, the Regular script needs to call function called loadScript with the instance of Reusable Script. Calling loadScript will initialise the Reusable Script and then combine handlers of both scripts.

Kotlin
script {

    loadScripts(startObservationScript)

}

Loading in FSM Script

A Reusable Script cannot be directly imported at top-level of FSM script. It can only be imported in a particular State of the FSM script. Loaded script is limited to that particular State. Below example loading script into a State.

Kotlin
state("INIT") { params ->

    loadScripts(startObservationScript)

}