.. _group-tutorial: Group tutorial -------------- Groups are containers used to modify the execution behavior of the Apps contained within them. They allow you to build up more complex workflows like looping and conditional branching. Each group defines one or more slots in which child apps and groups can be added. This chapter is a hands-on introduction to writing groups. At the end, you will have created a looping group, a branching group, 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 corresponding 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 state machine. .. code-block:: none . └── bundles └── group ├── manifest.json ├── resources └── sources └── Group.lf The ``Group.lf`` file will contain the code we create in this tutorial. Since you already know how to create an app we will start by creating a simple app called "Group", which will do nothing and succeed immediately. .. code-block:: lf 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. We will now transform this app into a group. The group will still not define any behavior. It will simply start the sequence of child states and terminate, once all child states have run or an error occurs in any of them. .. code-block:: lf 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. Each container represents a slot and specifies the path to the state, where apps shall be inserted (see :ref:`branching_group` for an example of a multi-slot group). * Add the states listed 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 add code which will set the `error_cause` to the `error_cause` of the failing app: .. code-block:: lf 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) }@ } } In order to forward the `error_cause` to the user, `childNames` is used to iterate over all children. It is a list containing 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. .. code-block:: lf 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. .. code-block:: lf 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: .. code-block:: lf Group { port Success child("head").port("done") port Error child("body").port("Error") clientData { name: "Group"; type: "group"; containers: [["body"]]; contextMenu: @{

}@; } ... } 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. .. _branching_group: 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: .. code-block:: lf 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``. .. code-block:: lf 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: .. code-block:: lf 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: @{

}@; } 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`: .. code-block:: lf 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 state `monitor_part` next to `body` and connect its port `violated` with the `Error` port of `Group`: .. code-block:: lf 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 could also connect the `violated` port 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: .. code-block:: lf Group { port Success child("body").port("Success") port Error child("body").port("Error") clientData { name: "Group"; type: "group"; containers: [["body"]]; components: @{ }@; } 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: .. code-block:: none . └── 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: .. code-block:: lf 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 .. code-block:: lf 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 .. code-block:: lf 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`: .. code-block:: lf 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: @{ }@; } 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: .. code-block:: html 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: .. code-block:: javascript 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 state machine would look like this: .. note:: This is the automatically generated code for the task and does NOT need to be implemented. .. code-block:: lf 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`.