Commands

Commands are parameter sets called Setup, Observe, and Wait. A command is created with the source of the command, given by a prefix, the name of the command, and an optional ObsId. Parameters are added to the command as needed. As the ESW design is developed, these command structures may evolve.

ObsId

An ObsID, or observation ID, indicates the observation the command is associated with. It can be constructed by creating an instance of ObsId.

Scala
sourceval obsId: ObsId = ObsId("2020A-001-123")
Java
sourcefinal ObsId obsId = ObsId.apply("2020A-001-123");

Source

The source of the command is given by the prefix, which should be the full name of the component sending the command. A prefix can be constructed with a string, but must start with a valid subsystem as in Subsystem. A component developer should supply a valid prefix string and the subsystem will be automatically parsed from it. An example of a valid string prefix is “nfiraos.ncc.trombone”.

See below examples:

Scala
source// using constructor, supplying subsystem and prefix both
val source1: Prefix = Prefix("nfiraos.ncc.trombone")

// just by supplying prefix
val source2: Prefix = Prefix("tcs.mobie.blue.filter")

// invalid prefix string which does not contain valid subsystem in the beginning will throw an exception,
// val badSource: Prefix = Prefix("abcdefgh")

// use implicit conversion to convert from String to Prefix
val source3: Prefix = Prefix("wfos.prog.cloudcover")
Java
source//using constructor, supplying subsystem and prefix both
Prefix source1 = Prefix.apply(JSubsystem.NFIRAOS, "ncc.trombone");

//just by supplying prefix
Prefix source2 = Prefix.apply(JSubsystem.TCS, "mobie.blue.filter");

//invalid prefix string which does not contain valid subsystem in the beginning will throw an exception,
// Prefix badSource = Prefix.apply("abcdefgh");

CommandName

Each command has a name given as a string. The CommandName object wraps the string name. The string should be continuous with no spaces.

Setup Command

This command is used to describe a goal that a system should match. The component developer is required to supply following arguments to create a Setup command.

  • Source: The source of the command is given by the prefix, which should be the full name of the component sending the command.
  • CommandName: a simple string name for the command (no spaces)
  • ObsId: an optional observation Id.
  • paramSet: Optional Set of Parameters. Default is empty.
Scala
source// keys
val k1: Key[Int]    = KeyType.IntKey.make("encoder")
val k2: Key[String] = KeyType.StringKey.make("stringThing")
val k2bad: Key[Int] = KeyType.IntKey.make("missingKey")
val k3: Key[Int]    = KeyType.IntKey.make("filter")
val k4: Key[Float]  = KeyType.FloatKey.make("correction")

// Source of the command is given by the prefix
// Source should be full name of the component sending the command.
val source: Prefix = Prefix("wfos.red.detector")

// parameters
val i1: Parameter[Int]    = k1.set(22)
val i2: Parameter[String] = k2.set("A")

// create Setup, add sequentially using add
val sc1: Setup = Setup(source, CommandName("move"), Some(obsId)).add(i1).add(i2)

// access keys
val k1Exists: Boolean = sc1.exists(k1) // true

// access parameters
val tryParam1: Try[Parameter[Int]] = Try(sc1(k1))    // success
val tryk2Bad: Try[Parameter[Int]]  = Try(sc1(k2bad)) // failure

// add more than one parameters, using madd
val sc2: Setup     = sc1.madd(k3.set(1, 2, 3, 4).withUnits(Units.day), k4.set(1.0f, 2.0f))
val paramSize: Int = sc2.size

// add binary payload
val byteKey1: Key[Byte] = ByteKey.make("byteKey1")
val byteKey2: Key[Byte] = ByteKey.make("byteKey2")
val bytes1: Array[Byte] = Array[Byte](10, 20)
val bytes2: Array[Byte] = Array[Byte](30, 40)

val b1: Parameter[Byte] = byteKey1.setAll(bytes1)
val b2: Parameter[Byte] = byteKey2.setAll(bytes2)

val sc3: Setup = Setup(source, CommandName("move"), Some(obsId), Set(b1, b2))

// remove a key
val sc4: Setup = sc3.remove(b1)

// list all keys
val allKeys: Set[String] = sc4.paramSet.map(_.keyName)
Java
source//keys
Key<Integer> k1 = JKeyType.IntKey().make("encoder", JUnits.encoder);
Key<String> k2 = JKeyType.StringKey().make("stringThing");
Key<Integer> k2bad = JKeyType.IntKey().make("missingKey");
Key<Integer> k3 = JKeyType.IntKey().make("filter");
Key<Float> k4 = JKeyType.FloatKey().make("correction");

Prefix source = Prefix.apply(JSubsystem.WFOS, "red.detector");

//parameters
Parameter<Integer> i1 = k1.set(22);
Parameter<String> i2 = k2.set("A");

//create setup, add sequentially using add
Setup sc1 = new Setup(source, new CommandName("move"), Optional.of(obsId)).add(i1).add(i2);

//access keys
boolean k1Exists = sc1.exists(k1); //true

//access parameters
Optional<Parameter<Integer>> optParam1 = sc1.jGet(k1); //present
Optional<Parameter<Integer>> optK2Bad = sc1.jGet(k2bad); //absent

//add more than one parameters, using madd
Setup sc2 = sc1.madd(k3.set(1, 2, 3, 4).withUnits(JUnits.day), k4.set(1.0f, 2.0f));
int paramSize = sc2.size();

//add binary payload
Key<Byte> byteKey1 = JKeyType.ByteKey().make("byteKey1");
Key<Byte> byteKey2 = JKeyType.ByteKey().make("byteKey2");
Byte[] bytes1 = {10, 20};
Byte[] bytes2 = {30, 40};

Parameter<Byte> b1 = byteKey1.setAll(bytes1);
Parameter<Byte> b2 = byteKey2.setAll(bytes2);

Setup sc3 = new Setup(source, new CommandName("move"), Optional.of(obsId)).add(b1).add(b2);

//remove a key
Setup sc4 = sc3.remove(b1);

//list all keys
java.util.List<String> allKeys = sc4.jParamSet().stream().map(Parameter::keyName).collect(Collectors.toList());

Observe Command

This command describes a science observation. It is intended to only be sent to Science Detector Assemblies and Sequencers.

Scala
source// keys
val k1: Key[Boolean] = KeyType.BooleanKey.make("repeat")
val k2: Key[Int]     = KeyType.IntKey.make("expTime")
val k2bad: Key[Int]  = KeyType.IntKey.make("missingKey")
val k3: Key[Int]     = KeyType.IntKey.make("filter")
val k4: Key[UTCTime] = KeyType.UTCTimeKey.make("creation-time")

// Source of the command is given by the prefix
// Source should be full name of the component sending the command.
val source: Prefix = Prefix("wfos.red.detector")

// parameters
val i1: Parameter[Boolean] = k1.set(true, false, true, false)
val i2: Parameter[Int]     = k2.set(1, 2, 3, 4)

// create Observe, add sequentially using add
val oc1: Observe = Observe(source, CommandName("move"), Some(obsId)).add(i1).add(i2)

// access parameters using apply method
val k1Param: Parameter[Boolean] = oc1.get(k1).get // true
val values: Array[Boolean]      = k1Param.values

// access parameters
val tryParam1: Try[Parameter[Boolean]] = Try(oc1(k1))    // success
val tryk2Bad: Try[Parameter[Int]]      = Try(oc1(k2bad)) // failure

// add more than one parameters, using madd
val oc2: Observe   = oc1.madd(k3.set(1, 2, 3, 4).withUnits(Units.day), k4.set(UTCTime.now()))
val paramSize: Int = oc2.size

// update existing key with set
val oc3: Observe = oc1.add(k2.set(5, 6, 7, 8))

// remove a key
val oc4: Observe = oc2.remove(k4)
Java
source//keys
Key<Boolean> k1 = JKeyType.BooleanKey().make("repeat");
Key<Integer> k2 = JKeyType.IntKey().make("expTime", JUnits.second);
Key<Integer> k2bad = JKeyType.IntKey().make("missingKey");
Key<Integer> k3 = JKeyType.IntKey().make("filter");
Key<UTCTime> k4 = JKeyType.UTCTimeKey().make("creation-time");

//Source of the command is given by the prefix
//Source should be full name of the component sending the command.
Prefix source = Prefix.apply(JSubsystem.WFOS, "red.detector");

//parameters
Boolean[] boolArray = {true, false, true, false};
Parameter<Boolean> i1 = k1.setAll(boolArray);
Parameter<Integer> i2 = k2.set(1, 2, 3, 4);

//create Observe, add sequentially using add
Observe oc1 = new Observe(source, new CommandName("move"), Optional.of(obsId)).add(i1).add(i2);

//access parameters
Optional<Parameter<Boolean>> k1Param = oc1.jGet(k1); //present
java.util.List<Boolean> values = k1Param.orElseThrow().jValues();

//access parameters
Optional<Parameter<ArrayData<Float>>> k2BadParam = oc1.jGet(k2bad.keyName(), JKeyType.FloatArrayKey());

//add more than one parameters, using madd
Observe oc2 = oc1.madd(k3.set(1, 2, 3, 4).withUnits(JUnits.day), k4.set(UTCTime.now()));
int paramSize = oc2.size();

//update existing key with set
Integer[] intArray = {5, 6, 7, 8};
Observe oc3 = oc1.add(k2.setAll(intArray));

//remove a key
Observe oc4 = oc2.remove(k4);

//list all keys
java.util.List<String> allKeys = oc4.jParamSet().stream().map(Parameter::keyName).collect(Collectors.toList());

Wait Command

This command causes a Sequencer to wait until notified. It can only be sent to Sequencers.

Scala
source// keys
val k1: Key[Boolean] = KeyType.BooleanKey.make("repeat")
val k2: Key[Int]     = KeyType.IntKey.make("expTime")
val k2bad: Key[Int]  = KeyType.IntKey.make("missingKey")
val k3: Key[Int]     = KeyType.IntKey.make("filter")
val k4: Key[UTCTime] = KeyType.UTCTimeKey.make("creation-time")

// parameters
val i1: Parameter[Boolean] = k1.set(true, false, true, false)
val i2: Parameter[Int]     = k2.set(1, 2, 3, 4)

// Source of the command is given by the prefix
// Source should be full name of the component sending the command.
val source: Prefix = Prefix("wfos.red.detector")

// create wait, add sequentially using add
val wc1: Wait = Wait(source, CommandName("move"), Some(obsId)).add(i1).add(i2)

// access params using get method
val k1Param: Option[Parameter[Boolean]] = wc1.get(k1)
val values: Array[Boolean]              = k1Param.map(_.values).getOrElse(Array.empty[Boolean])

// access parameters
val tryParam1: Try[Parameter[Boolean]] = Try(wc1(k1))    // success
val tryk2Bad: Try[Parameter[Int]]      = Try(wc1(k2bad)) // failure

// add more than one parameters, using madd
val wc2: Wait      = wc1.madd(k3.set(1, 2, 3, 4).withUnits(Units.day), k4.set(UTCTime.now()))
val paramSize: Int = wc2.size

// update existing key with set
val wc3: Wait = wc1.add(k2.set(5, 6, 7, 8))

// remove a key
val wc4: Wait = wc2.remove(k4)
Java
source//keys
Key<Boolean> k1 = JKeyType.BooleanKey().make("repeat");
Key<Integer> k2 = JKeyType.IntKey().make("expTime", JUnits.second);
Key<Integer> k2bad = JKeyType.IntKey().make("missingKey");
Key<Integer> k3 = JKeyType.IntKey().make("filter");
Key<UTCTime> k4 = JKeyType.UTCTimeKey().make("creation-time");

//Source of the command is given by the prefix
//Source should be full name of the component sending the command.
Prefix source = Prefix.apply(JSubsystem.WFOS, "red.detector");

//parameters
Boolean[] boolArray = {true, false, true, false};
Parameter<Boolean> i1 = k1.setAll(boolArray);
Parameter<Integer> i2 = k2.set(1, 2, 3, 4);

//create Wait, add sequentially using add
Wait wc1 = new Wait(source, new CommandName("move"), Optional.of(obsId)).add(i1).add(i2);

//access parameters using jGet
Optional<Parameter<Boolean>> k1Param = wc1.jGet(k1); //present
java.util.List<Boolean> values = k1Param.orElseThrow().jValues();

//access parameters
Optional<Parameter<ArrayData<Float>>> k2BadParam = wc1.jGet("absentKeyHere", JKeyType.FloatArrayKey());

//add more than one parameters, using madd
Wait wc2 = wc1.madd(k3.set(1, 2, 3, 4).withUnits(JUnits.day), k4.set(UTCTime.now()));
int paramSize = wc2.size();

//update existing key with set
Integer[] intArray = {5, 6, 7, 8};
Wait wc3 = wc1.add(k2.setAll(intArray));

//remove a key
Wait wc4 = wc2.remove(k4);

//list all keys
java.util.List<String> allKeys = wc4.jParamSet().stream().map(Parameter::keyName).collect(Collectors.toList());

JSON serialization

Commands can be serialized to JSON. The library has provided JsonSupport helper class and methods to serialize Setup, Observe and Wait commands.

Scala
sourceimport play.api.libs.json.{JsValue, Json}

// key
val k1: Key[MatrixData[Double]] = DoubleMatrixKey.make("myMatrix")
// values
val m1: MatrixData[Double] = MatrixData.fromArrays(
  Array(1.0, 2.0, 3.0),
  Array(4.1, 5.1, 6.1),
  Array(7.2, 8.2, 9.2)
)

// Source of the command is given by the prefix
// Source should be full name of the component sending the command.
val source: Prefix = Prefix("wfos.red.detector")

// parameter
val i1: Parameter[MatrixData[Double]] = k1.set(m1)

// commands
val sc: Setup   = Setup(source, CommandName("move"), Some(obsId)).add(i1)
val oc: Observe = Observe(source, CommandName("move"), Some(obsId)).add(i1)
val wc: Wait    = Wait(source, CommandName("move"), Some(obsId)).add(i1)

// json support - write
val scJson: JsValue = JsonSupport.writeSequenceCommand(sc)
val ocJson: JsValue = JsonSupport.writeSequenceCommand(oc)
val wcJson: JsValue = JsonSupport.writeSequenceCommand(wc)

// optionally prettify
val str: String = Json.prettyPrint(scJson)

// construct command from string
val scFromPrettyStr = JsonSupport.readSequenceCommand[Setup](Json.parse(str))

// json support - read
val sc1: Setup   = JsonSupport.readSequenceCommand[Setup](scJson)
val oc1: Observe = JsonSupport.readSequenceCommand[Observe](ocJson)
val wc1: Wait    = JsonSupport.readSequenceCommand[Wait](wcJson)
Java
source//key
Key<MatrixData<Double>> k1 = JKeyType.DoubleMatrixKey().make("myMatrix");

//values
Double[][] doubles = {{1.0, 2.0, 3.0}, {4.1, 5.1, 6.1}, {7.2, 8.2, 9.2}};
MatrixData<Double> m1 = MatrixData.fromArrays(doubles);

//parameter
Parameter<MatrixData<Double>> i1 = k1.set(m1);

//Source of the command is given by the prefix
//Source should be full name of the component sending the command.
Prefix source = Prefix.apply(JSubsystem.WFOS, "blue.filter");

//commands
Setup sc = new Setup(source, new CommandName("move"), Optional.of(obsId)).add(i1);
Observe oc = new Observe(source, new CommandName("move"), Optional.of(obsId)).add(i1);
Wait wc = new Wait(source, new CommandName("move"), Optional.of(obsId)).add(i1);

//json support - write
JsValue scJson = JavaJsonSupport.writeSequenceCommand(sc);
JsValue ocJson = JavaJsonSupport.writeSequenceCommand(oc);
JsValue wcJson = JavaJsonSupport.writeSequenceCommand(wc);

//optionally prettify
String str = Json.prettyPrint(scJson);

//construct command from string
Setup sc1 = JavaJsonSupport.readSequenceCommand(Json.parse(str));
Observe oc1 = JavaJsonSupport.readSequenceCommand(ocJson);
Wait wc1 = JavaJsonSupport.readSequenceCommand(wcJson);

Unique Key constraint

By design, a ParameterSet in a Setup, Observe, or Wait command is optimized to store only unique keys. When using add or madd methods on commands to add new parameters, if the parameter being added has a key which is already present in the paramSet, the already stored parameter will be replaced by the given parameter.

Note

If the Set is created by component developers and given directly while creating a command, then it will be the responsibility of component developers to maintain uniqueness with parameters based on key.

Here are some examples that illustrate this point:

Scala
source// keys
val encoderKey: Key[Int] = KeyType.IntKey.make("encoder")
val filterKey: Key[Int]  = KeyType.IntKey.make("filter")
val miscKey: Key[Int]    = KeyType.IntKey.make("misc.")

// Source of the command is given by the prefix
// Source should be full name of the component sending the command.
val source: Prefix = Prefix("wfos.red.detector")

// params
val encParam1: Parameter[Int] = encoderKey.set(1)
val encParam2: Parameter[Int] = encoderKey.set(2)
val encParam3: Parameter[Int] = encoderKey.set(3)

val filterParam1: Parameter[Int] = filterKey.set(1)
val filterParam2: Parameter[Int] = filterKey.set(2)
val filterParam3: Parameter[Int] = filterKey.set(3)

val miscParam1 = miscKey.set(100)

// Setup command with duplicate key via constructor
val setup: Setup =
  Setup(
    source,
    CommandName("move"),
    Some(obsId),
    Set(encParam1, encParam2, encParam3, filterParam1, filterParam2, filterParam3)
  )
// four duplicate keys are removed; now contains one Encoder and one Filter key
val uniqueKeys1 = setup.paramSet.toList.map(_.keyName)

// try adding duplicate keys via add + madd
val changedSetup: Setup = setup
  .add(encParam3)
  .madd(
    filterParam1,
    filterParam2,
    filterParam3
  )
// duplicate keys will not be added. Should contain one Encoder and one Filter key
val uniqueKeys2: List[String] = changedSetup.paramSet.toList.map(_.keyName)

// miscKey(unique) will be added; encoderKey(duplicate) will not be added
val finalSetUp: Setup = setup.madd(Set(miscParam1, encParam1))
// now contains encoderKey, filterKey, miscKey
val uniqueKeys3: List[String] = finalSetUp.paramSet.toList.map(_.keyName)
Java
source//keys
Key<Integer> encoderKey = JKeyType.IntKey().make("encoder", JUnits.encoder);
Key<Integer> filterKey = JKeyType.IntKey().make("filter");
Key<Integer> miscKey = JKeyType.IntKey().make("misc.");

//Source of the command is given by the prefix,
//Source should be full name of the component sending the command.
Prefix source = Prefix.apply(JSubsystem.WFOS, "blue.filter");

//params
Parameter<Integer> encParam1 = encoderKey.set(1);
Parameter<Integer> encParam2 = encoderKey.set(2);
Parameter<Integer> encParam3 = encoderKey.set(3);

Parameter<Integer> filterParam1 = filterKey.set(1);
Parameter<Integer> filterParam2 = filterKey.set(2);
Parameter<Integer> filterParam3 = filterKey.set(3);

Parameter<Integer> miscParam1 = miscKey.set(100);

//Setup command with duplicate key via madd
Setup setup = new Setup(source, new CommandName("move"), Optional.of(obsId)).madd(
        encParam1,
        encParam2,
        encParam3,
        filterParam1,
        filterParam2,
        filterParam3);
//four duplicate keys are removed; now contains one Encoder and one Filter key
Set<String> uniqueKeys1 = setup.jParamSet().stream().map(Parameter::keyName).collect(Collectors.toUnmodifiableSet());

//try adding duplicate keys via add + madd
Setup changedSetup = setup.add(encParam3).madd(filterParam1, filterParam2, filterParam3);
//duplicate keys will not be added. Should contain one Encoder and one Filter key
Set<String> uniqueKeys2 = changedSetup.jParamSet().stream().map(Parameter::keyName).collect(Collectors.toUnmodifiableSet());

//miscKey(unique) will be added; encoderKey(duplicate) will not be added
Setup finalSetUp = setup.madd(miscParam1, encParam1);
//now contains encoderKey, filterKey, miscKey
Set<String> uniqueKeys3 = finalSetUp.jParamSet().stream().map(Parameter::keyName).collect(Collectors.toUnmodifiableSet());

Cloning a Command

In order to track the completion of a command, every command that is sent must have a unique RunId. If you wish to resubmit a previously sent Setup, the cloneCommand method must be used prior to submission to create a new command from existing parameters, but with a new RunId.

Scala
sourceval setup  = Setup(source, commandName, Some(obsId)).madd(i1)
val setup2 = setup.cloneCommand

val observe  = Observe(source, commandName, Some(obsId)).madd(i1)
val observe2 = observe.cloneCommand

val wait  = Wait(source, commandName, Some(obsId)).madd(i1)
val wait2 = wait.cloneCommand
Java
sourceSetup setup = new Setup(source, commandName, Optional.of(obsId)).add(encoderParam);
Setup setup2 = setup.cloneCommand();

Observe observe = new Observe(source, commandName, Optional.empty()).add(encoderParam);
Observe observe2 = observe.cloneCommand();

Wait wait = new Wait(source, commandName, Optional.of(obsId)).add(encoderParam);
Wait wait2 = wait.cloneCommand();

Source Code for Examples