Command Service for Assemblies and HCDs

A Sequencer script can send commands to Assemblies and HCDs. This section describes the Command Service DSL that is a wrapper for the CSW Command Service module for sending commands to Assemblies or HCDs within scripts. You can refer to detailed documentation of the Command Service provided by CSW here.

The DSL provides a way to define an Assembly or HCD as an object. This object encapsulates the Location Service and Command Service of CSW to provide a higher level DSL for script usage. This DSL exposes following APIs:

A Sequencer can also send Sequences to other Sequencers. See here for more information on sending Sequences to Sequencers.

Assembly

The Assembly DSL method creates a Command Service entity for an Assembly with the provided Prefix that can be used to send commands from a script, such as sending Setups or Observes or lifecycle methods e.g. goOnline, goOffline, lock Assembly etc. This DSL method provides a default timeout which will be used for commands like submitAndWait, queryFinal etc, but also allows adding an Assembly-specific default timeout. The built-in default timeout is 10 seconds. Commands requiring a timeout also allow command-specific timeouts.

Assembly takes the following parameters:

  • prefix: Prefix of the Assembly as defined in the Assembly’s model file
  • defaultTimeout: optional command response timeout to be used when not explicitly provided for command
Kotlin
sourceval galilAssembly = Assembly(WFOS, "FilterWheel")

val galilAssembly2 = Assembly(WFOS, "FilterWheel", defaultTimeout = Duration.seconds(20))

HCD

The HCD DSL method creates a Command Service DSL entity for an HCD with the provided Prefix that can be used to send commands from a script, such as sending Setups or Observes or lifecycle methods e.g. goOnline, goOffline, lock HCD etc. This DSL method provides a default timeout which will be used for commands like submitAndWait, queryFinal etc., but also allows adding an HCD-specific default timeout. The built-in timeout is 10 seconds. Commands requiring a timeout also allow command-specific timeouts.

HCD takes the following parameters:

  • prefix: - Prefix of HCD as defined in the HCD’s model file
  • defaultTimeout: - optional command response timeout to be used when not explicitly provided for command
Kotlin
sourceval filterWheelHcd = Hcd(WFOS, "GalilHcd1")

val filterWheelHcd2 = Hcd(WFOS, "GalilHcd1", defaultTimeout = Duration.seconds(20))
Resolving a Component with Location Service

Since all the components in the TMT architecture are dynamic in nature, which implies they can be shutdown and spawned dynamically on some other location, the Assembly/HCD is resolved each time the Command Service DSL is used. It is possible to create an Assembly or HCD entity for a non-existent component, but a command to the component will fail because the component is resolved when the command is sent.

Command Service DSL

The Command Service API provided by the DSL for use in scripts is similar to the CommandService API provided by Scala or Java. The big difference is that the DSL does not return a Future. In the scripting language, even though the commands are asynchronous, each completes and returns its value, such as a SubmitResponse, not a Future[SubmitResponse]. This section describes each of the available DSL methods.

Submit

The submit method of the DSL allows sending a Setup or Observe command to an Assembly/HCD. submit returns a positive SubmitResponse which can be Completed or Started. A very short command may quickly return Completed. A command that starts actions that are long-running returns Started.

submit or submitAndWait?

submit is similar to submitAndWait in that both send a command to another component. submit is the right choice when the command starts long-running actions, and you need to take additional actions before the command completes. A successful long-running command returns a Started response that includes a runId, which can be used with query or queryFinal to wait for the commands final response at a later time.

submitAndWait combines submit and queryFinal as a shortcut when you only need to wait for all started actions to complete before taking the next script step.

The following example shows a submit to the Galil Assembly that is going to take a long time.

Kotlin
sourceval parameters = intKey("target").set(100)
val galilCommand = Setup("ESW.IRIS_darkNight", "moveWheel", command.obsId).add(parameters)
val startedResponse = galilAssembly.submit(galilCommand)

Error Handling in Scripts

In most cases errors encountered in the execution of a script will likely cause the command (and therefore, Sequence) to fail. Most of the time, not much can be done when an error occurs other than to report the error that occurred. In some cases, it is possible some remediation can be performed, but it is likely the Sequence would need to run again. For this reason, the error handling of commands in a script has been simplified such that errors from the Command Service DSL calls are captured and delivered to error handlers specific to a single sequence command handler, or global to the entire script. In this way, such error handling does not need to be repeated throughout the script for each command sent.

To add an error handler to a command handler, extend the command handler block with a .onError block. The SubmitResponse error is captured in a ScriptError type and passed into the block. This type contains a reason String explaining what went wrong. If the command handler does not have an onError block, the global error handler will be called. See the page on Script Handlers for more information. After this block is called, the command sending the sequence terminates with an Error status.

Because of this mechanism, a submit (and other Command Service API calls) always returns a positive SubmitResponse. For submit, the two possible responses are Started and Completed. They can be handled using the .onStarted and .onCompleted methods, respectively. These methods allow you to specify a block of code to be called in each of those cases. Alternatively, a Kotlin when can be used to perform pattern matching on the result. An example of both are shown below, along with an example of an onError handler for the sequence command handler. Since there are only two positive options, forming an if statement using the isStarted call on the SubmitResponse is convenient in many cases.

Kotlin
sourceonSetup("submit-error-handling") { command ->

    /* =========== Scenario-1 (default) ============
     * if submit returns negative response (which is considered as error by default)
     * then current execution flow breaks and onError command handler gets invoked
     * Hence, only Started (in case of long-running command) or Completed (in case of short running command) response is returned
     */
    val parameters = intKey("target").set(100)
    val galilCommand = Setup("ESW.IRIS_darkNight", "moveWheel", command.obsId).add(parameters)
    val positiveSubmitResponse: CommandResponse.SubmitResponse = galilAssembly.submit(galilCommand)

    //  First approach - using custom dsl (this is an alternative to kotlin pattern match using when)
    positiveSubmitResponse
            .onStarted { startedRes ->
                val completedResponse = galilAssembly.queryFinal(startedRes.runId())
                info("command completed with result: ${completedResponse.result}")
            }
            .onCompleted { completed ->
                info("command with ${completed.runId()} is completed with result: ${completed.result}")
            }

    // Second approach - using kotlin pattern matching
    when (positiveSubmitResponse) {
        is CommandResponse.Started -> {
            val completedResponse = galilAssembly.queryFinal(positiveSubmitResponse.runId())
            info("command completed with response: $completedResponse")
        }
        is CommandResponse.Completed -> info("command with ${positiveSubmitResponse.runId()} is completed")
    }

    // Third approach - use the isStarted value
    if (positiveSubmitResponse.isStarted) {
        val completedResponse = galilAssembly.queryFinal(positiveSubmitResponse.runId())
        info("command completed with result: ${completedResponse.result}")
    } else {
        info("command with ${positiveSubmitResponse.runId()} is completed with result: ${positiveSubmitResponse.result}")
    }

}.onError { err ->
    // onError is called when submit command to galil assembly fails
    error(err.reason)
}

If you desire to handle errors manually on a per-command basis, the resumeOnError flag can be used. If this flag is set to true, then script execution continues, and action is taken based on custom logic in the script using an .onFailed method. You can still choose to terminate the Sequence using the onFailedTerminate utility. This will cause similar behavior as when the flag is not set by calling the onError or onGlobalError blocks and terminating the sequence, if the SubmitResponse is some kind of error.

Kotlin
sourceonSetup("submit-error-handling-resume") { command ->
    /* =========== Scenario-2 (resumeOnError = true) ============
     * if submit returns negative response
     * then current execution flow will continue because resumeOnError = true
     * Here, all the possible SubmitResponses are expected to be returned
     */
    val parameters = intKey("target").set(100)
    val galilCommand = Setup("ESW.iris_darkNight", "moveWheel", command.obsId).add(parameters)
    val submitResponse: CommandResponse.SubmitResponse = galilAssembly.submit(galilCommand, resumeOnError = true)

    //  First approach - using custom dsl (this is an alternative to kotlin pattern match using when)
    submitResponse
            .onStarted { startedRes ->
                val completedResponse = galilAssembly.queryFinal(startedRes.runId())
                info("command completed with result: ${completedResponse.result}")
            }
            .onCompleted { completed ->
                info("command with ${completed.runId()} is completed with result: ${completed.result}")
            }
            .onFailed { negativeResponse ->
                error("command with ${negativeResponse.runId()} is failed with result: $negativeResponse")

            }

    // Script writer can still choose to terminate sequence in case of negative response
    submitResponse.onFailedTerminate()
}

SubmitAndWait

The submitAndWait DSL method combines submit and queryFinal allowing you to submit a command to an Assembly/HCD and wait for the final response. A timeout can be specified if needed, indicating the time submitAndWait will wait to receive the final SubmitResponse. If this time expires, the command will timeout, breaking script execution flow, and the Sequence is terminated with failure. If timeout is not provided explicitly, then the timeout provided while creating the instance of Assembly or HCD is used as default timeout. This command follows the same error handling semantics as submit as described above.

Kotlin
sourceval parameters = intKey("target").set(100)
val galilCommand = Setup("ESW.IRIS_darkNight", "moveWheel", command.obsId).add(parameters)
galilAssembly.submitAndWait(galilCommand, timeout = Duration.seconds(20))
Do I Need a Result Variable?

Note that this example does not save the submitAndWait result. If a submitAndWait does not return a result, and since the submitAndWait returns only after the actions are completed, and errors are handled elsewhere, there is not much reason to bother with the result. If it is the case where the command returns a result in the Completed, save the returned Completed value and retrieve the result.

Query

The query DSL method allows you to check the status of a submit command that has returned a Started response . The Started response contains a runId that can be used to identify the command to query. The query command returns immediately while queryFinal waits for the final response. Therefore, query can be used to poll for the final response. Note that if the runId is not present or has been removed from the CRM, the response returned is an Invalid response with an IdNotAvailableIssue. This command follows the same error handling semantics as submit as described above.

Kotlin
sourceval response = galilAssembly.submit(command)
val queryResponse = galilAssembly.query(response.runId())

QueryFinal

The queryFinal DSL method allows querying for the final response of a submit command that has returned a Started response . The Started response contains a runId that can be used to identify the command. A timeout can be specified, indicating how long queryFinal wait for getting the final SubmitResponse. If this time expires, the command will timeout, breaking script execution flow, and the sequence is terminated with failure. If timeout is not provided explicitly, then the timeout provided while creating instance of Assembly/HCD is used. Note that if the runId is not present or has been removed from the CRM, the response returned is an Invalid response with an IdNotAvailableIssue. This command follows the same error handling semantics as submit as described above.

Kotlin
sourceval startedResponse = galilAssembly.submit(galilCommand)
val finalResponse = galilAssembly.queryFinal(startedResponse.runId())

SubscribeCurrentState

This DSL allows subscribing to the current state data of the Assembly/HCD. You can provide a list of state names to subscribe to. If not provided, all current state values are subscribed to. This DSL takes a callback (or lambda), which is called whenever the Assembly/HCD publishes an item in the list of subscribed values.

Kotlin
sourcegalilAssembly.subscribeCurrentState(StateName("stateName1")) { currentState ->
    // do something with currentState matching provided state name
    println("current state : $currentState")
}

Going To online/offline Mode

A Sequencer can command an Assembly or HCD to go to the online or offline state. This is a wrapper for putting another Assembly/HCD into Online or Offline mode. When an Assembly/HCD receives this command, its respective handlers are called. The detailed documentation of Online/Offline handlers for Assembly/HCD can be found here

goOffline

A declared Assembly/HCD includes a DSL command puts an Assembly/HCD into Offline mode. goOffline can be called from anywhere in script. This results in the triggering of the onGoOffline handler in the component. The following example shows a Sequencer sending the goOffline command to a downstream “Galil Assembly” when it receives a goOffline command.

Kotlin
sourceonGoOffline {
    // do some actions to go offline

    galilAssembly.goOffline()

}

goOnline

A declared Assembly/HCD includes a DSL command to put an Assembly/HCD into Online mode. goOnline can be called from anywhere in the script. This results in the triggering of the onGoOnline handler in the component. The following example shows a Sequencer sending the goOnline command to a downstream “Galil Assembly” when it receives a goOnline command.

Kotlin
sourceonGoOnline {
    // do some actions to go online
    galilAssembly.goOnline()
}

Operations Mode and Diagnostic Mode

A Sequencer can place an Assembly or HCD in a diagnostic technical data mode. There are two methods in the Assembly/HCD Command Service DSL related to technical data collection.

diagnosticMode

The diagnosticMode DSL method puts an Assembly/HCD into Diagnostic data mode based on a hint at the specified startTime. diagnosticMode can be called from anywhere in script. The hint is specified by the component. Not all components have diagnostic modes for technical data. The following example shows a Sequencer sending the diagnosticMode command to a downstream “Galil Assembly” when it receives a diagnosticMode command.

Kotlin
sourceonDiagnosticMode { startTime, hint ->
    // do some actions to go to diagnostic mode based on hint
    galilAssembly.diagnosticMode(startTime, hint)
}

operationsMode

This operationsMode DSL method returns an Assembly/HCD to Operations mode, the normal running mode. operationsMode can be called from anywhere in script. The following example shows a Sequencer sending the operationsMode command to a downstream “Galil Assembly” when it receives an operationsMode command.

Kotlin
sourceonOperationsMode {
    // do some actions to go to operations mode
    galilAssembly.operationsMode()
}

Locking and Unlocking Assemblies and HCDs

A Sequencer script can lock and unlock individual Assemblies and HCDs. When a Sequencer locks a component, it is the only component that can send commands to the component that will be accepted.

lock

This Command Service DSL method locks an Assembly/HCD from a Sequencer script for the specified duration. When you lock an Assembly/HCD, the Sequencer sending the lock command is designated as the source, which is the only component that can send commands to the locked component while locked. This DSL returns a LockingResponse which can be LockAcquired in the successful scenario or AcquiringLockFailed in case of failure. This DSL also provides callbacks for onLockAboutToExpire and, onLockExpired where script writer can write custom logic. These callbacks are thread safe.

Kotlin
sourcegalilAssembly.lock(
        leaseDuration = Duration.seconds(20),
        onLockAboutToExpire = {
            // do something when lock is about to expire
            publishEvent(SystemEvent("ESW.test", "TCS.lock.about.to.expire"))
        },
        onLockExpired = {
            // do something when lock expired
            publishEvent(SystemEvent("ESW.test", "TCS.lock.expired"))
        }
)

unlock

This Command Service DSL method unlocks an Assembly/HCD from a Sequencer script. Only the Sequencer that locked the Assembly/HCD can unlock it. This DSL returns a LockingResponse which can be LockReleased or LockAlreadyReleased in the successful scenario or ReleasingLockFailed in case of failure.

Kotlin
sourcegalilAssembly.unlock()

Source code for examples