.. _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`.