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.
.
└── 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.
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.
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
totype: 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 Multiple container - branching group for an example of a multi-slot group).Add the states listed in
containers
. They basically need to fulfill theapp
interface, i.e have aSuccess
and anError
port and name astring error_cause
in their result.
We can also 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)
}@
}
}
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.
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 state monitor_part next to body and connect its port violated with the Error port of Group:
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:
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">
<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">
<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 state machine would look like this:
Note
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.