Group Tutorial

This chapter is a hands-on introduction to writing Groups. At the end, you will have created a looping group, a multi-level group and a monitoring pattern as well as a group for providing parameters.

The Groups will be created step by step, and we will be adding the functionality in the following order:

  • Looping Group

  • Branching Group

  • Monitoring

  • Providing Parameters

Simple Group

Similar to creating an app we first create a new bundle which contains a statemachine.

.
└── bundles
  └── group
      ├── manifest.json
      ├── resources
      └── sources
         └── Group.lf

The Group.lf file will contain the code we create in this tutorial. Since you already should know how to create an app we will start by creating a simple app (called Group, because we will evolve it) which will do nothing and succeed immediately.

Group {
  port Success true
  port Error false

  clientData {
    name: "Group";
    type: "app";
  }

  resultType {
    string error_cause;
  }
}

This simple app can already be uploaded and used in Desk. A group state has a very similar interface to a normal app. Next, we will transform this app into a group. The group will still not define any behavior. It will simply start the sequence child states and will terminate, once all child states have run or an error occurs in any of them.

Group {
  port Success child("body").port("Success")
  port Error child("body").port("Error")

  clientData {
    name: "Group";
    type: "group";
    containers: [["body"]];
  }

  resultType {
    string error_cause;
  }

  --> body {
    port Success true
    port Error false

    resultType {
      string error_cause;
    }
  }
}

We have now created a group with one visible slot where apps can be inserted. Each inserted app is being placed into the state 'body'. Each app gets its "Success" port connected to the next state until the final inserted app. Its "Success" port will be connected to the "Success" port of 'body'. The error ports of all inserted apps will be connected to the "Error" port of 'body'.

In order to create a group from an app, we need to:
  • Change type: app to type: group

  • Add a field containers (Array[Path]) to clientData. This specifies the path of the state, where the apps shall be inserted. For each slot (see Branching example for an example of mulit-slot groups), an entry must be created.

  • Actually add the states named in containers. They basically need to fulfill the app interface, i.e have a Success and an Error port and name a string error_cause in their result.

We can also now add code which will set the error_cause to the error_cause of the failing app:

Group {
  port Success child("body").port("Success")
  port Error child("body").port("Error")

  clientData {
    name: "Group";
    type: "group";
    containers: [["body"]];
  }

  resultType {
    string error_cause;
  }

  exit @{
    if child("body").result ~= nil and child("body").result.error_cause ~= nil then
      setResult({error_cause = child("body").result.error_cause})
    end
  }@

  --> body {
    port Success true
    port Error false

    resultType {
      string error_cause;
    }

    exit @{
      result = { error_cause = nil }
      for i=1,#childNames do
        local c = child(childNames[i])
        if c.result ~= nil and c.result.error_cause ~= nil then
          result.error_cause = c.result.error_cause
        end
      end
      setResult(result)
    }@
  }
}

The Error port of body will be the union of the Error ports of all inserted children. The Success port will be the Success port of the last app in the group.

In order to collect all error_cause strings to communicate with the user, childNames is used to iterate all children. It is a list holding the names of all children.

If you install this bundle on your system you should see a new entry in the groups list. If you add this entry to a task, you can add apps into this group. The behavior of the apps should not have changed. If an app inside the group errors, the correct error messages should still be shown.

Feel free to modify color, icon and names as you would with Apps.

Looping

Now we will add functionality to the group. We can simply add states around our body state and wire them to and from body. So, to create a looping group we can simply add a head state, which decides if the sequence of apps shall run again.

Group {
  port Success child("head").port("done")
  port Error child("body").port("Error")

  clientData { ... }

  resultType { ... }

  exit @{ ... }@

  --> head {
    port continue true -> body
    port done false
  }

  body {
    port Success true -> head
    port Error false

    resultType { ... }

    exit @{ ... }@
  }
}

By simply adding head like this and having every successful completion of the apps re-invoke head we have created an infinite loop. If we now add a parameter describing the number of iterations, we can have the user decide how long the group will loop.

Group {
  port Success child("head").port("done")
  port Error child("body").port("Error")

  clientData { ... }

  parameterType {
    int nr_loops;
  }

  resultType { ... }

  exit @{ ... }@

  --> head {
    port continue variable < parameter -> body
    port done variable >= parameter

    parameterType int
    variableType int

    entry @{
      if variable == nil then
        setVariable(0)
      else
        setVariable(variable + 1)
      end
    }@
  } where parameter.nr_loops

  body { ... }
} where {
  nr_loops: 3;
}

Now we can add a context menu to make the number of loops configurable:

Group {
  port Success child("head").port("done")
  port Error child("body").port("Error")

  clientData {
    name: "Group";
    type: "group";
    containers: [["body"]];
    contextMenu: @{
      <step id="loops" name="Number of Loops" class="flex-column"
        label="Configure how often the sequence should be repeated.">
        <h2 data-bind="html: step.label"></h2>
        <arc-slider params="
          value: parameter('nr_loops'),
          unit: '',
          initial: 3,
          fullValue: 100,
          step: step
        "/>
      </step>
    }@;
  }

  ...
} where {
  nr_loops: 3;
}

If you upload this bundle now, you have a looping group, which will repeat the sequence of apps as often as you have configured.

Multiple Container - Branching Group

Next, we are going to create a group app with multiple branches. We are going to start from our simple, non-looping group with one container:

Group {
  port Success child("body").port("Success")
  port Error child("body").port("Error")

  clientData {
    name: "Group";
    type: "group";
    containers: [["body"]];
  }

  resultType {
    string error_cause;
  }

  --> body {
    port Success true
    port Error false

    resultType {
      string error_cause;
    }
  }
}

In order to add another container, we simply add a corresponding state and list it in the containers list in clientData.

Group {
  port Success child("first").port("Success") or child("second").port("Success")
  port Error child("first").port("Error") or child("second").port("Error")

  clientData {
    name: "Group";
    type: "group";
    containers: [["first"], ["second"]];
  }

  resultType {
    string error_cause;
  }

  --> choose {
    port first true -> first
    port second false -> second
  }

  first {
    port Success true
    port Error false

    resultType {
      string error_cause;
    }
  }

  second {
    port Success true
    port Error false

    resultType {
      string error_cause;
    }
  }
}

Now we have created a two-container group with an initial switch state choosing which container will be executed.

We can also add a parameter to decide if the first or second container will be executed:

Group {
  port Success child("first").port("Success") or child("second").port("Success")
  port Error child("first").port("Error") or child("second").port("Error")

  clientData {
    name: "Group";
    type: "group";
    containers: [["first"], ["second"]];
    contextMenu: @{
      <step id="choose" name="Container" class="flex-column"
        label="Configure which container should be executed.">
        <h2 data-bind="html: step.label"></h2>
        <checkbox-slider params="
          value: parameter('first'),
          unchecked: 'Lower',
          checked: 'Upper',
          step: step
        "></checkbox-slider>
      </step>
    }@;
  }

  parameterType {
    bool first;
  }

  resultType { ... }

  --> choose {
    port first parameter -> first
    port second not parameter -> second

    parameterType bool

  } where parameter.first

  first { ... }

  second { ... }
} where {
  first: nil;
}

By switching the checkbox-slider, you can now control which sequence is executed.

Monitoring

So far we have created groups which (possibly) modify the execution behavior of the sequence of apps inside the group. Another way of utilizing the group concept would be to create a monitor group, which would make sure a desired condition always holds for all apps in the group.

This can simply be done by adding a monitor state in parallel to the body state containing the sequence of apps. So, starting with the finished Group from the first section - Simple Group:

Group {
  port Success child("body").port("Success")
  port Error child("body").port("Error")

  clientData { .. }

  resultType { .. }

  exit @{ .. }@

  --> body {
    port Success true
    port Error false

    resultType { .. }

    exit @{ .. }@
  }
}

We can now add a monitoring state next to the body and connect the violation port with the error port as well:

Group {
  port Success child("body").port("Success")
  port Error child("body").port("Error") or child("monitor_part").port("violated")

  clientData { .. }

  resultType { .. }

  exit @{ .. }@

  --> barrier run {
    -> monitor_part
    -> body
  }

  monitor_part {
    port violated service("gripper").event("gripper_state") ~= nil and
                  not service("gripper").event("gripper_state").is_grasped
  }

  action child("monitor_part").port("violated") @{
    setResult({error_cause = "Monitor triggered - Part lost."})
  }@

  body {
    port Success true
    port Error false

    resultType { .. }

    exit @{ .. }@
  }
}

Similar to this we can also connect to the Success port, if the execution should continue after the condition is reached.

Providing Parameters to Apps

NOTE: This topic is for advanced users and requires a deeper understanding of context menu development.

Lastly we want to have a look at providing parameters to apps. A group can register its own components, which can then be used in apps inside the group. If the component has the same name as a default component (see Parameter Modification), an app inside the group will initially choose the component provided by the closest parent. This way, a group can change the teaching behavior of any existing app, if it uses the provided components.

In order to modify the teaching behavior of an app we can, for example, create our own custom robot-pose component to replace teaching by a text input field. To do this, we need to create 3 different parts, the group, the template and the parameter writing logic.

Writing the group itself requires to register the components in clientData:

Group {
  port Success child("body").port("Success")
  port Error child("body").port("Error")

  clientData {
    name: "Group";
    type: "group";
    containers: [["body"]];
    components: @{
      <component name="robot-pose generated-robot-pose" linkable>
        <template src="group/robot_pose.html"></template>
        <script src="group/robot_pose.js"></script>
      </component>
    }@;
  }

  parameterType {
    []string input;
  }

  resultType { .. }

  exit @{ .. }@

  --> body {
    port Success true
    port Error false

    resultType { .. }

    exit @{ .. }@
  }
} where {
  input: [];
}

We name the component robot-pose to override the standard teaching flow. We need to specify the files containing the markup (i.e. template) and the script and give them a name, with which they can be used in apps. We also define the parameterType as list of strings, where the inputs in the text field will be stored.

Our file system structure will look like this for this kind of group:

.
└── bundles
  └── group
      ├── manifest.json
      ├── resources
      │   └── robot_pose.html
      │   └── robot_pose.js
      └── sources
         └── Group.lf

Next, we will write the code that translates these input strings into poses which can be used as parameters in the apps:

Group {
  port Success child("body").port("Success")
  port Error child("body").port("Error")

  clientData { .. }


  parameterType {
    []string input;
  }

  resultType { .. }

  --> prepare_poses {
    port done result ~= nil -> body

    parameterType []string

    resultType {
      []{
        [][]{
          [16]float pose;
          []float joint_angles;
        } relative_trajectories;
        {
          [16]float pose;
          []float joint_angles;
        } pose;
      } poses;
    }

    entry @{
      setResult(nil)
    }@

    action service("pose_provider").event("poses") ~= nil @{
      local res = {}
      res.poses = {}
      for i, p in pairs(parameter) do
        for k,v in pairs(service("pose_provider").event("poses")) do
          if k == p then
            res.poses[i] = {pose = {pose = v}}
          end
        end
        res.poses[i].pose.joint_angles = {}
        res.poses[i].relative_trajectories = {}
      end
      setResult(res)
    }@
  } where parameter.input

  body {
    port Success true
    port Error false

    parameterType {
      []{
        [][]{
          [16]float pose;
          []float joint_angles;
        } relative_trajectories;
        {
          [16]float pose;
          []float joint_angles;
        } pose;
      } poses;
    }
    resultType { .. }

    exit @{ .. }@
  } where {
    poses: child("prepare_poses").result.poses;
  }

  exit @{ .. }@
} where {
  input: [];
}

So, first we create a state to fetch poses. These could be calculated, requested via an operation call or, like in this example, taken from a service event. We then give these poses into the body state as parameter. We need to make sure, that the body state gets a list of a parameterType, which is equal to the desired parameter we want to configure (in this case, the poses).

Note that the service could also simply be replaced by a dictionary, which contains poses related to pose names, i.e. replace

entry @{
  setResult(nil)
}@

action service("pose_provider").event("poses") ~= nil @{
  local res = {}
  res.poses = {}
  for i, p in pairs(parameter) do
    for k,v in pairs(service("pose_provider").event("poses")) do
      if k == p then
        res.poses[i] = {pose = {pose = v}}
      end
    end
    res.poses[i].pose.joint_angles = {}
    res.poses[i].relative_trajectories = {}
  end
  setResult(res)
}@

with

entry @{
  local poses = {["aa"] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -0.2, 0.1, 0.1, 1},
                 ["bb"] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0.1, 0.2, 0.4, 1}}
  local res = {}
  res.poses = {}
  for i, p in pairs(parameter) do
    for k,v in pairs(poses) do
      if k == p then
        res.poses[i] = {pose = {pose = v}}
      end
    end
    res.poses[i].pose.joint_angles = {}
    res.poses[i].relative_trajectories = {}
  end
  setResult(res)
}@

Since we now accumulate data in the group's parameter value for each taught app, we also add a garbage collection hook to clientData:

Group {
  port Success child("body").port("Success")
  port Error child("body").port("Error")

  clientData {
    name: "Group";
    type: "group";
    containers: [{ path: ["body"];
                   garbageCollect: { groupParameter: input;
                                     checkedContainerParameters: [poses];
                                   };
                }];
    components: @{
      <component name="robot-pose generated-robot-pose" linkable>
        <template src="input_group/robot_pose.html"></template>
        <script src="input_group/robot_pose.js"></script>
      </component>
    }@;
  }


  parameterType {
    []string input;
  }

  resultType { .. }

  --> prepare_poses {
    ...
  } where parameter.input

  body {
    ...
  } where {
    poses: child("prepare_poses").result.poses;
  }

  exit @{ .. }@
} where {
  input: [];
}

Hereby, we decide that our parameter input should be checked for references in the parameter value of the container state body in the parameter poses. So, if an app is removed from the group, this will make sure that the parameter value is also removed, if it is no longer used.

Next, we define the context menu. We start by defining the template, which for a simple text input field is simply:

<step id="input-pose">
  <input data-bind="
    value: pose
  ">
</step>

This is added to the file robot_pose.html which is placed in the resources directory of our bundle.

Now we just need to add the wiring logic. We need two distinct things to happen. Firstly, the string entered into the text field needs to end up in our groups parameter value. Secondly, the app needs to be parameterized, in this case with an access path into the containers parameter value, where we have put the actual pose values.

This can be done as follows:

var ko = require("knockout")
var _ = require("lodash")
var matrix = require("matrix")
var util = require("component_util")

function getParentAccess(targetPath, sourcePath, accessTail) {
  var numberOfParents = ko.unwrapDeep(targetPath).indices.length - ko.unwrapDeep(sourcePath).indices.length - 2
  return _.fill(Array(numberOfParents), "parent").concat(accessTail)
}

var RobotPose = module.exports = function(params, api, source) {
  if (!params.pose) {
    throw new Error("The parameter 'pose' is missing")
  } else if (!params.path) {
    throw new Error("The parameter 'path' is missing")
  }
  this.api = api
  this.source = source
  this.params = params
  this.pose = ko.pureComputed({
    read: function() {
      return this.config()
    },
    write: function(input) {
      var index = this.updateConfig(input)
      this.setTargetParameter(index)
    }
  }, this)
}

RobotPose.prototype.updateConfig = function(update) {
  var index = util.or(this.configIndex(), this.source.parameter("input")().length)
  this.source.parameter(["input", index])(update)
  return index
}

RobotPose.prototype.config = function() {
  return _.cloneDeep(this.source.parameter("input")()[this.configIndex()]) || ""
}

RobotPose.prototype.setTargetParameter = function(index) {
  var parentAccess = getParentAccess(this.params.path, this.source.path, ["parameter"])
  this.params.pose({ access: parentAccess.concat(["poses", index + 1, "pose"]) })
}

RobotPose.prototype.configIndex = function() {
  var access = getParentAccess(this.params.path, this.source.path, ["parameter", "input"])
  return this.params.pose() && this.params.pose().access[access.length] - 1
}

Here we can see that two things happen on write for the pose: First, the config is updated. This means, that the string entered into the text field is written into the parameter of the group. Next, the target parameter is written. The target parameter refers to the actual parameterization of the app. So inside the apps context menu we write the access path into the body. Note, that the index into the list of poses is the same as the index of the string in the groups parameter value. So, if we parameterize this, the resulting statemachine would look like this:

HINT: This is the automatically generated code for the task and does NOT need to be implemented.

task {
  port Success child("body").port("Success")
  port Error child("body").port("Error")

  exit @{ .. }@

  --> body {
    port Success child("group").port("Success")
    port Error child("group").port("Error") or false

    exit @{ .. }@

    --> input_group {
      port Success child("body").port("Success")
      port Error child("body").port("Error")

      exit @{ .. }@

      body {
        port Success child("pick").port("Success")
        port Error child("pick").port("Error") or false

        exit @{ .. }@

        --> pick {
          ...
        } where {
          approach : [parameter.poses[1].pose];
          pick_pose : parameter.poses[2].pose;
        }

        parameterType { .. }
        resultType { .. }
      } where {
        poses : child("prepare_poses").result.poses;
      }

      --> prepare_poses {
        ...
      } where parameter.input

      parameterType {
        []string input;
      }
      resultType { .. }
    } where {
      input : ["aa", "bb"];
    }

    parameterType { .. }
    resultType {
      string error_cause;
    }
  } where {
    poses : parameter.poses;
  }

} where {
  poses : [];
  speed : 0.5;
}

Here we can see nicely that the strings "aa" and "bb" have been entered into the groups parameter value. The pick app, which is the app we parameterized, has the access paths into the parameter of body with approach going to parameter.poses[1].pose and the actual pick pose to parameter.poses[2].pose.