Setup Input/Output

This section describes how commands are encoded as Setups and how they are unpacked and transformed into commands that go to the individual segments.

Input: Setup Description

The Segment Assembly receives CSW Setups, which may come from a variety of sources. In this project we demonstrate Setups from test code and using esw-shell (in a later section on testing). Input describes how Setups are created.

The strategy is to build a library of functions that make it relatively easy to construct Setups. The API is based on the previously referenced document. In this document each command is documented with a set of parameters that are required or optional.

The set of parameters for each command is generally different, although some share parameters. For instance, many commands include a selection of actuators. Each command also has a common parameter that indicates if the command should go to one specified segment or to all segments.

The creation of Setups is implemented in a separate project with only the code needed for the job so that a JAR file can be created that can be loaded into esw-shell. The lscsCommands JAR only depends on CSW libraries, which are included in esw-shell; therefore, scripts can be written using the library functions.

Implementing Commands

A representative subset of the commands have been implemented. The plan was to do them all, but the documentation gets less reliable towards the end and is missing command examples for a few important commands.

There is a subproject called lscsCommands. This project contains all the code to create Assembly Setups and to extract and convert an Assembly Setup to a segment command string.

Under the package m1cs.segments.segcommands there is a file for each implemented command that is the name of the command. There are currently 10 commands implemented. Examples are: ACTUATOR, CFG_ACT_OFFLD, etc.

In each command file there is an object with the same name (i.e. ACTUATOR). An example is the Actuator command shown below, which includes all the features of the command implementation. Within the object is all the code to create a Setup and to extract a segment command. All commands are constructed the same way.

Scala
sourceobject ACTUATOR {
  import Common.*

  // ACTUATOR command
  val COMMAND_NAME: CommandName = CommandName("ACTUATOR")

  object ActuatorModes extends Enumeration {
    type ActuatorMode = Value

    val OFF: Value       = Value(1, "OFF")
    val TRACK: Value     = Value(2, "TRACK")
    val SLEW: Value      = Value(3, "SLEW")
    val CALIBRATE: Value = Value(4, "CALIBRATE")
  }

  import ActuatorModes.*

  val actuatorChoices: Choices    = Choices.from(OFF.toString, TRACK.toString, SLEW.toString, CALIBRATE.toString)
  val actuatorModeKey: GChoiceKey = ChoiceKey.make("MODE", actuatorChoices)
  val targetKey: Key[Float]       = KeyType.FloatKey.make("TARGET")

  case class toActuator(prefix: Prefix, actId: Set[Int]) extends BaseCommand[toActuator](prefix, COMMAND_NAME) {

    setup = Common.addActuators(setup, actId)

    def withMode(mode: ActuatorMode): toActuator = {
      setup = setup.add(actuatorModeKey.set(Choice(mode.toString)))
      this
    }

    def withTarget(target: Double): toActuator = {
      setup = setup.add(targetKey.set(target.toFloat))
      this
    }

    // Make a copy -- do any checks here
    override def asSetup: Setup = {
      val mode   = setup.get(actuatorModeKey)
      val target = setup.get(targetKey)

      // Check that there is at least one
      require(mode.isDefined || target.isDefined, "Actuator must have either a mode or target or both.")

      // Should require a segment set
      Setup(setup.source, setup.commandName, setup.maybeObsId, setup.paramSet)
    }
  }

  /**
   * Returns a formatted ACTUATOR command from a [Setup]
   *
   * @param setup Setup created with toActuator
   * @return String command ready to send
   */
  def toCommand(setup: Setup): String = {
    require(setup.commandName == COMMAND_NAME, s"The provided Setup is not a: $COMMAND_NAME")
    val actId        = setup(actuatorIdKey)
    val modeExists   = setup.exists(actuatorModeKey)
    val targetExists = setup.exists(targetKey)
    require(targetExists || modeExists, "ACTUATOR requires either a mode or a target or both.")
    val actIdVal = if (actId.size == 3) "ALL" else valuesToString(actId.values)
    val sb       = new StringBuilder(s"${setup.commandName.name} ACT_ID=$actIdVal")
    if (modeExists) sb ++= s", MODE=${setup(actuatorModeKey).head.name}"
    if (targetExists) sb ++= s", TARGET=${setup(targetKey).head}"
    sb.result()
  }
}

At the top of the object common code is imported. Following this is the name of the command, which is again, the name of the file.

Note

In this example, I decided that there would be a unique Setup for each command. So the CommandName is the name of the segment command. An alternative would be to have a single Setup type and include a parameter called CommandName. There are pros/cons of each approach. For the HCD, I selected the second approach.

Note

Each command is implemented as a case class with parameters that are the command’s required parameters. There is a base class for all commands that includes a Setup and support for sending the command to one or all the segments with a segmentId key.

In this case, ACTUATOR includes the prefix of the sender, and a Set of integers indicating the actuators to influence. Examples are Set(1), Set(1,3). To indicate all actuators you can say Set(1,2,3) or ALL_ACTUATORS, which is an alias for Set(1,2,3). If the source is prefix: M1CS.client, the minimal Actuator command is:

import csw.prefix.models.Prefix
import m1cs.segments.segcommands.Common.*
import m1cs.segments.segcommands.ACTUATOR.*
import m1cs.segments.segcommands.ACTUATOR.ActuatorModes.*

 
val prefix=Prefix("M1CS.client")
toActuator(prefix, Set(1))

This command is somewhat meaningless, because to be a correct ACTUATOR command it must have at least one of the optional parameters (see below).

Optional Parameters

The value returned by toActuator is a toActuator instance. Optional values are added using “with” methods using a fluid-style API so options can be added as needed. For example to add the optional actuator mode and target, the following are all possible:

toActuator(prefix, Set(1)).withMode(SLEW)

toActuator(prefix, Set(1)).withTarget(22.3)

This last example shows the optional parameters can be combined as needed:

toActuator(prefix, ALL_ACTUATORS).withMode(SLEW).withTarget(22.3)

The case classes include with methods to add optional parameters as in:

def withMode(mode: ActuatorMode): toActuator = {
  setup = setup.add(actuatorModeKey.set(Choice(mode.toString)))
  this
}

def withTarget(target: Double): toActuator = {
  setup = setup.add(targetKey.set(target.toFloat))
  this
}

Each method returns this, which is in this case an toActuator instance, allowing the fluid style. This is a reasonable way to support optional parameters in a typeable API.

Choice Parameters

There are quite a few choice parameters. I’ve implemented them as enumerations as shown below for ActuatorMode:

import csw.params.core.models.{Choice, Choices}
import csw.params.core.generics.KeyType.ChoiceKey
import csw.params.core.generics.{GChoiceKey, Key, KeyType}

object ActuatorModes extends Enumeration {
    type ActuatorMode = Value

    val OFF: Value       = Value(1, "OFF")
    val TRACK: Value     = Value(2, "TRACK")
    val SLEW: Value      = Value(3, "SLEW")
    val CALIBRATE: Value = Value(4, "CALIBRATE")
  }

val actuatorChoices: Choices    = Choices.from(OFF.toString, TRACK.toString, SLEW.toString, CALIBRATE.toString)
val actuatorModeKey: GChoiceKey = ChoiceKey.make("MODE", actuatorChoices)
val targetKey: Key[Float]       = KeyType.FloatKey.make("TARGET")

The choice values must be imported as shown. This is also true when the API is used externally (i.e., not inside the command implementation). Each enumeration is supported with a GChoiceKey and the Choices are made up of the enumeration values as Strings.

The last line shows that there is a Float key for the TARGET value. Note that the API takes a Double, not a Float. This is because in Scala (and Java) you must add an f to make a value a Float, which is annoying. The conversion from a Double to a Float is done inside the code to make it a little more friendly to typing.

Conversion to Setup

After creating a command instance like toActutator, it can then be converted to a Setup for submission to the Segment Assembly. Each command includes a method called asSetup that returns a Setup. This is the method that verifies that all the information has been entered that is required.

override def asSetup: Setup = {
  val mode   = setup.get(actuatorModeKey)
  val target = setup.get(targetKey)

  // Check that there is at least one
  require(mode.isDefined || target.isDefined, "Actuator must have either a mode or target or both.")

  // Should require a segment set
  Setup(setup.source, setup.commandName, setup.maybeObsId, setup.paramSet)
}

In the ACTUATOR command, when calling asSetup a check is done to verify that at least the mode or target is included. If neither of these parameters is provided, an exception is thrown. If it is good, a copy of the internal Setup is returned.

Warning

Many of the commands in the documentation have optional commands, but there is no information on what combinations are legal or which ones must really be provided as in the above ACTUATOR case. This can be fixed in the same way as the above was done once the documentation is improved.

In summary, to create a Setup to send to the Segment Assembly for the ACTUATOR command, the following is an example and the contents of the Setup:

val setup = toActuator(prefix, ALL_ACTUATORS).withMode(SLEW).withTarget(22.3).asSetup
// setup: csw.params.commands.Setup = Setup(
//   source = Prefix(subsystem = M1CS, componentName = "client"),
//   commandName = CommandName(name = "ACTUATOR"),
//   maybeObsId = None,
//   paramSet = Set(
//     Parameter(
//       keyName = "SegmentId",
//       keyType = StringKey,
//       items = ArraySeq("ALL"),
//       units = none
//     ),
//     Parameter(
//       keyName = "ACT_ID",
//       keyType = IntKey,
//       items = ArraySeq(1, 2, 3),
//       units = none
//     ),
//     Parameter(
//       keyName = "MODE",
//       keyType = ChoiceKey,
//       items = ArraySeq(Choice(name = "SLEW")),
//       units = none
//     ),
//     Parameter(
//       keyName = "TARGET",
//       keyType = FloatKey,
//       items = ArraySeq(22.3F),
//       units = none
//     )
//   )
// )

Output: Converting a Setup to a Segment Command

In the implementation approach mentioned on the overview page, the strategy is that the Assembly Segment receives the assembly Setup and converts it to an HCD Setup. The HCD Setup is a single command that has an argument that is the command that is sent to the segment as a String.

The output then is to extract the segment command from the Assembly Setup. Each command also implements a method called toCommand, which uses the parameters of the Setup to create a well-formed command. The following is the toCommand method for the ACTUATOR command.

import csw.params.commands.{CommandName, Setup}

 /**
   * Returns a formatted ACTUATOR command from a [Setup]
   *
   * @param setup Setup created with toActuator
   * @return String command ready to send
   */
  def toCommand(setup: Setup): String = {
    require(setup.commandName == COMMAND_NAME, s"The provided Setup is not a: $COMMAND_NAME")
    val actId        = setup(actuatorIdKey)
    val modeExists   = setup.exists(actuatorModeKey)
    val targetExists = setup.exists(targetKey)
    val actIdVal     = if (actId.size == 3) "ALL" else valuesToString(actId.values)
    val sb           = new StringBuilder(s"${setup.commandName.name} ACT_ID=$actIdVal")
    if (modeExists) sb ++= s", MODE=${setup(actuatorModeKey).head.name}"
    if (targetExists) sb ++= s", TARGET=${setup(targetKey).head}"
    sb.result()
  }

To recap, a command includes required parameters and optional parameters. The above shows extracting the values for actuator, which is required. It creates a Boolean to check to see if actuator mode and/or target exists by checking for actuatorModeKey and targetKey.

Then a StringBuilder is created that creates a String for the command. First, the command is extracted using the Scala String interpolator syntax (${parameter}). The line starting with actIdVal checks to see whether there is a subset or all of the actuators. The valuesToString method formats a proper value for the command (as in (1,2)). Finally, if the mode and target exist, parameters are added to the String for each.

The example below is shows the output command that goes with the created Assembly Setup.

toCommand(toActuator(prefix, ALL_ACTUATORS).withMode(TRACK).withTarget(22.34).asSetup)
// res4: String = "ACTUATOR ACT_ID=ALL, MODE=TRACK, TARGET=22.34"

Segment Destination

Each command requires a segment location, but the location does not appear in the output command. The Setup contains a parameter for the segment destination. The following is the printed value of an ACTUATOR Setup for the example above.

toActuator(prefix, ALL_ACTUATORS).withMode(TRACK).withTarget(22.34).asSetup
// res5: Setup = Setup(
//   source = Prefix(subsystem = M1CS, componentName = "client"),
//   commandName = CommandName(name = "ACTUATOR"),
//   maybeObsId = None,
//   paramSet = Set(
//     Parameter(
//       keyName = "SegmentId",
//       keyType = StringKey,
//       items = ArraySeq("ALL"),
//       units = none
//     ),
//     Parameter(
//       keyName = "ACT_ID",
//       keyType = IntKey,
//       items = ArraySeq(1, 2, 3),
//       units = none
//     ),
//     Parameter(
//       keyName = "MODE",
//       keyType = ChoiceKey,
//       items = ArraySeq(Choice(name = "TRACK")),
//       units = none
//     ),
//     Parameter(
//       keyName = "TARGET",
//       keyType = FloatKey,
//       items = ArraySeq(22.34F),
//       units = none
//     )
//   )
// )

There is an extra SegmentId parameter with the value ALL, indicating the command will be sent to all segments. This parameter is used by the HCD to do the right thing. The SegmentId parameter is handled within the command base class. By default, a command goes to all segments.

The base class also provides two methods called toAll, and toSegment to add a destination to any command as shown here:

to = toActuator(prefix, Set(1, 3)).withMode(SLEW).toSegment(SegmentId("B22"))

or

to = toActuator(prefix, Set(1, 3)).withMode(SLEW).toAll

A SegmentId instance must be created to send to a specific segment. The SegmentId type verifies that the segment sector is A-F and segment number is 1-82. An exception is thrown if not true.

Testing Commands

Tests exist for each command to verify that it is working properly. There is one file called SegmentCommandsTests in the lscsCommands test area. Each command has similar tests. The following shows the tests for Actuator.

Scala
sourcetest("To From ACTUATOR") {
  import m1cs.segments.segcommands.ACTUATOR.*
  import m1cs.segments.segcommands.ACTUATOR.ActuatorModes.*

  var to = toActuator(prefix, Set(1, 3)).withMode(TRACK)
  // Verify segmentId is all by default
  to.asSetup(segmentIdKey).head shouldBe ALL_SEGMENTS

  // Verify override works
  to = toActuator(prefix, Set(1, 3)).withMode(TRACK).toSegment(SegmentId("A22"))
  to.asSetup(segmentIdKey).head shouldBe "A22"

  // Only 2 actuators
  to = toActuator(prefix, Set(1, 3)).withMode(TRACK)
  ACTUATOR.toCommand(to.asSetup) shouldBe s"${COMMAND_NAME.name} ACT_ID=(1,3), MODE=TRACK"

  to = toActuator(prefix, Set(1, 3)).withTarget(22.34)
  ACTUATOR.toCommand(to.asSetup) shouldBe s"${COMMAND_NAME.name} ACT_ID=(1,3), TARGET=22.34"

  to = toActuator(prefix, Set(1, 3)).withMode(TRACK).withTarget(target = 22.34)
  ACTUATOR.toCommand(to.asSetup) shouldBe s"${COMMAND_NAME.name} ACT_ID=(1,3), MODE=TRACK, TARGET=22.34"

  // Verify All
  to = toActuator(prefix, ALL_ACTUATORS).withMode(TRACK).withTarget(22.34)
  ACTUATOR.toCommand(to.asSetup) shouldBe s"${COMMAND_NAME.name} ACT_ID=ALL, MODE=TRACK, TARGET=22.34"

  // Check for no optional
  assertThrows[IllegalArgumentException] {
    toActuator(prefix, Set(1, 2, 3)).asSetup
  }

  // Check for too big set
  assertThrows[IllegalArgumentException] {
    toActuator(prefix, Set(1, 2, 3, 4))
  }

  // Check for empty set
  assertThrows[IllegalArgumentException] {
    toActuator(prefix, Set())
  }

  // Check for out of range ID
  assertThrows[IllegalArgumentException] {
    toActuator(prefix, Set(1, 2, 4))
  }
}

The tests create Setups with varied parameters and test that the output command is correct. Following are tests to verify that exceptions are thrown for bad conditions. For instance, note the test for the lack of an optional parameter.

 // Check for no options
assertThrows[IllegalArgumentException] {
  toActuator(prefix, Set(1, 2, 3)).asSetup
}

This verifies that an exception is thrown if neither withMode nor withTarget is included.

Input/Output Summary

Each command is created in the same way. A file is created in the m1cs.segments.segcommands package with the name of the command. A case class is created for the command with the required parameters. With methods are added for each optional parameter.

A asSetup method is included to check the Setup for consistency or constraints. A toCommand function is included to use the Setup parameters and output a segment command.

The source code for this page can be found here.