Command based workflow and template schema enrichment
This tutorial shows how to create a command with arguments which will trigger a workflow. It will also show us, how to enrich a instantiated schema (like a device) with a template schema (which can contain any capability) by using a scripted rule.
Preconditions
- HumanOS IoT Designer installed on development client (Check Installation Manual)
Step 1: Create a IoT Project with OPC-UA Server
-
Create the project
Step 2: Create two simple Hostcontrol device templates
-
Add the the device one by one:
-
Add a property to one of your templates:
-
Assign them to the
default
target:
Step 3: Create the datamodels
Create the scripting agent
-
If a script is to be executed, there must be at least one scripting agent which can be added like this:
Content:
{
"Id": "{C5597B6F-AB75-4F6C-BECC-BEC361314ACB}",
"Name": "Objects.Schema",
"Agents": [
{
"Id": "{34779630-197D-4924-8C27-210D462930B7}",
"Name": "ScriptingAgent",
"Type": "HosAgent",
"Skills": [ "54C4FA92-F385-4644-B929-5650310B4930" ]
}
],
"Skills": [
{
"Id": "54C4FA92-F385-4644-B929-5650310B4930",
"Name": "Objects.ScriptingSkill"
}
]
}
Create the schema and its scripts
-
Add a schema. This will be the "template" we will use for injecting it to the devices, enriching their capabilities.
In the schema we will use the following code, defining a simple workflow and a command. The command will also have an argument of type System.String
which we will pipe to the workflow which himself also takes that one argument of the same type.
{
"Id": "{A7FAA92A-99FC-4D38-BC68-E4915F333505}",
"Name": "InjectionSchema",
"PersistanceMode": "Volatile",
"Commands": [
{
"Name": "ExecuteWorkflowCommand",
"Type": "CSharpScriptCommandNode",
"Arguments": [
{
"Name": "Arg1",
"Type": "Input",
"DataType": "System.String"
}
],
"Properties": [
{
"Name": "ScriptFileName",
"Value": "ExecuteWorkflow.cs"
}
]
}
],
"Workflows": [
{
"Arguments": [
{
"Name": "Arg1",
"DataType": "System.String",
"Type": "Input"
}
],
"Name": "Workflow",
"Nodes": [
{
"Name": "Start",
"Type": "InitialNode"
},
{
"Name": "Fork1",
"Type": "ForkNode"
},
{
"Name": "Activity_One",
"Type": "GenericActivity",
"AutoReset": true,
"ActorSkill": "54C4FA92-F385-4644-B929-5650310B4930",
"Operations": [
{
"Name": "Operation",
"Type": "CSharpScriptOperation",
"Instruction": "Operation_One.cs"
}
]
},
{
"Name": "ExceptionHandler",
"Type": "ExceptionHandler",
"Properties": [
{
"Name": "ExceptionTypeName",
"Value": "System.Exception"
}
]
},
{
"Name": "TerminateActivity",
"Type": "GenericActivity",
"AutoReset": true,
"ActorSkill": "54C4FA92-F385-4644-B929-5650310B4930",
"Operations": [
{
"Name": "Operation1",
"Type": "CSharpScriptOperation",
"Instruction": "TTerminateHumanOS.cs"
}
]
},
{
"Name": "End",
"Type": "ActivityFinalNode"
}
],
"ControlFlows": [
{
"Name": "Flow_Start_Fork1",
"Node1RefName": "Start",
"Node2RefName": "Fork1"
},
{
"Name": "Flow_Fork1_Activity_One",
"Node1RefName": "Fork1",
"Node2RefName": "Activity_One"
},
{
"Name": "Flow_ExceptionHandler_TerminateActivity",
"Node1RefName": "ExceptionHandler",
"Node2RefName": "TerminateActivity"
},
{
"Name": "Flow_TerminateActivity_End",
"Node1RefName": "TerminateActivity",
"Node2RefName": "End"
}
]
}
]
}
-
Add a scripted rule which will trigger every time when a device appears. This will do the work of injecting/enriching each device which has the property
InjectEnabled
with our workflow and command:
Content:
{
"Id": "{F8D52C3E-0566-46F3-A41F-E99C06031F56}",
"Name": "Rules.Schema",
"PersistanceMode": "Volatile",
"Rules": [
{
"Name": "InjectToDevices",
"Type": "GenericRule",
"TriggerEvent": "OnObjectAppeared",
"TriggerCondition": "Context.Node is IGroupRelation",
"Action": {
"Type": "ScriptFile",
"Arguments": [
{
"Name": "Node",
"Predicate": "Node.hasProperty('InjectEnabled')"
}
],
"ScriptFile": "InjectToDevices.cs"
}
}
]
}
Step 3: Write C# Script
Now we can write our scripts which the rule, the workflow and the command.
Note the names of the scripts in the datamodel files we added before and make sure you name them accordingly.
-
Add and code the command script. Command Scripts which are not explicitly bound to a device must be added under the global scripts section.
Content:
/*****************************************************************************
* Copyright (C) by CyberTech Engineering 2022 – www.cybertech.swiss *
*****************************************************************************
* Project: HumanOS (R)
* Date : 2022
*****************************************************************************
* License: *
* This library is protected software; you are not allowed to redistribute *
* whole or part of it to other companies or external persons without the *
* authorization of the CEO CyberTech Engineering GmbH. *
*****************************************************************************/
using CyberTech;
using HumanOS.Kernel.DataModel;
using HumanOS.Kernel.DataModel.Space;
using HumanOS.Kernel.Processing;
using HumanOS.Kernel.Script;
using HumanOS.Kernel.Utils;
using HumanOS.Kernel.Workflow;
using HumanOS.Kernel.Workflow.Activity;
using System;
using System.Collections.Generic;
using System.Linq;
namespace HumanOS.IoT.Designer.Library.Scripts
{
/// <summary>
/// Examble of a command script
/// </summary>
public class Blank_PeMiLCommandScriptObject : TAbstractCommandScriptObject
{
///<see cref="TAbstractCommandScriptObject"/>
public override TCommandResult executeCommand(ICommandNode CommandNode, Dictionary<string, object> dicInputArguments, Dictionary<string, object> dicOutputArguments)
{
TCommandResult Retval = new TCommandResult();
try
{
if (!TCommandHelper.tryGetArgument<string>(dicInputArguments, "Arg1", out string strArg1))
{
throw new ArgumentException($"Missing or invalid type of input argument 'Arg1'.");
}
string strWorkflowName = "Workflow";
Retval.setErrorInfo(EProcessingState.GoodInProgress);
INodeSpace NodeSpace = (INodeSpace)CommandNode.Relations.First(n => n is INodeSpace);
foreach (INode Device in NodeSpace.queryNodesLocally(n => n.Name != ""))
{
IGroupRelation nDevice = Device as IGroupRelation;
if (nDevice != null && nDevice.hasProperty("DriverId"))
{
bool bResult = TObject.castAndExecute<IWorkflow>(nDevice.queryNodeLocally(n => n.Name == strWorkflowName), (o) =>
{
if (o.State == EActivityState.Running)
{
throw new ArgumentException($"Cannot execute the action, device is busy, try again later.");
}
else
{
TCommandCallContext Ccc = o.createCallContext();
Ccc.setInputArgumentValue<string>("Arg1", strArg1);
TCommandResult Res = o.execute(Ccc);
if (Res.State != EProcessingState.Good)
{
throw new ArgumentException($"Unable to start workflow for device '{nDevice.Name}'.", o.Context.ErrorInfo);
}
}
});
if (!bResult)
{
Logger.writeWarning($"Did not find workflow with name '{strWorkflowName}' in node '{nDevice.Name}'.");
}
}
}
}
catch (Exception Exc)
{
Retval.setErrorInfo(EProcessingState.BadProcessing, Exc);
}
return Retval;
}
}
} -
Add and code the workflow script and workflow exception handling script. Take a look at the workflow script:
Content:
/*****************************************************************************
* Copyright (C) by CyberTech Engineering 2022 – www.cybertech.swiss *
*****************************************************************************
* Project: HumanOS (R)
* Date : 2022
*****************************************************************************
* License: *
* This library is protected software; you are not allowed to redistribute *
* whole or part of it to other companies or external persons without the *
* authorization of the CEO CyberTech Engineering GmbH. *
*****************************************************************************/
using HumanOS.Kernel;
using HumanOS.Kernel.DataModel;
using HumanOS.Kernel.Workflow.Activity;
using HumanOS.Kernel.Workflow.Instruction;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace HumanOS.IoT.Designer.Library.Scripts
{
/// <summary>
/// Example of a workflow operation script
/// </summary>
public class Blank_PeMiLOperationScriptObject : TAbstractOperationScriptObject
{
///<see cref="TAbstractOperationScriptObject"/>
public override Task runAsync(IKernelAccess Kernel, IActivity Activity, CancellationTokenSource CancellationToken)
{
try
{
if (!Activity.Context.hasValue("Arg1"))
{
throw new ArgumentException($"Missing argument 'Arg1', cannot start workflow.");
}
Logger.writeInfo($"Hello from workflow, with greetings text '{Activity.Context.getValue<string>("Arg1")}'");
}
catch (Exception Exc)
{
Logger.writeError($"Error occured while executing workflow.", Exc);
}
return Task.CompletedTask;
}
}
}And then the exception handling script:
Content:
/*****************************************************************************
* Copyright (C) by CyberTech Engineering 2022 – www.cybertech.swiss *
*****************************************************************************
* Project: HumanOS (R)
* Date : 2022
*****************************************************************************
* License: *
* This library is protected software; you are not allowed to redistribute *
* whole or part of it to other companies or external persons without the *
* authorization of the CEO CyberTech Engineering GmbH. *
*****************************************************************************/
using HumanOS.Kernel;
using HumanOS.Kernel.Workflow.Activity;
using HumanOS.Kernel.Workflow.Instruction;
using System.Threading;
using System.Threading.Tasks;
namespace HumanOS.IoT.Designer.Library.Scripts
{
/// <summary>
/// Example of a workflow operation script
/// </summary>
public class Blank_PeMiLOperationScriptObject : TAbstractOperationScriptObject
{
///<see cref="TAbstractOperationScriptObject"/>
public override Task runAsync(IKernelAccess Kernel, IActivity Activity, CancellationTokenSource CancellationToken)
{
Kernel.postFatalError(Activity.Context.ErrorInfo);
return Task.CompletedTask;
}
}
} -
Add and code the injection rule script.
Content:
/*****************************************************************************
* Copyright (C) by CyberTech Engineering 2022 – www.cybertech.swiss *
*****************************************************************************
* Project: HumanOS (R)
* Date : 2022
*****************************************************************************
* License: *
* This library is protected software; you are not allowed to redistribute *
* whole or part of it to other companies or external persons without the *
* authorization of the CEO CyberTech Engineering GmbH. *
*****************************************************************************/
using System;
using System.Collections.Generic;
using System.Linq;
using HumanOS.Kernel;
using HumanOS.Kernel.DataModel;
using HumanOS.Kernel.DataModel.Rules;
namespace HumanOS.IoT.Designer.Library.Scripts
{
/// <summary>
/// Example of a script for rules
/// </summary>
public class Blank_PeMiLRuleScriptObject : TAbstractRuleScriptObject
{
///<see cref="TAbstractRuleScriptObject"/>
public override void execute(IKernelAccess Kernel, IVariablePool Pool)
{
IGroupRelation nDevice = null;
Guid WorkflowSchemaId = Guid.Parse("A7FAA92A-99FC-4D38-BC68-E4915F333505"); //Id of injection schema
try
{
nDevice = Pool.getValue<IGroupRelation>("Node");
if (nDevice == null)
{
throw new ArgumentException($"Device not found, cannot map workflow schema to empty device.");
}
IEnumerable<INode> SchemaNodes = Kernel.NodeFactory.createNodesFromSchema(WorkflowSchemaId, nDevice.GlobalId);
if (SchemaNodes.Count() == 0)
{
Logger.writeWarning($"Schema with id '{WorkflowSchemaId}' returned no nodes, nothing will be injected.");
}
foreach (INode SchemaNode in SchemaNodes)
{
try
{
Kernel.NodeSpace.addNodeToGroup(nDevice.GlobalId, SchemaNode.GlobalId);
}
catch (Exception Exc)
{
Logger.writeError($"Failed to add the node '{SchemaNode.Name}' to device '{nDevice.Name}'.", Exc);
}
}
Logger.writeInfo($"Injection done successfully for device '{nDevice.Name}'.");
}
catch (Exception Exc)
{
Logger.writeError($"Failed to add the workflow schema with id '{WorkflowSchemaId}' to the device node '{nDevice?.Name}'.", Exc);
}
}
}
}
Step 4: Test it with an OPC-UA Client
-
Startup the HumanOS IoT Gateway.
-
Connect with your favorite OPC-UA Client.
-
Execute the command which will trigger our workflow on any devices with the property
InjectEnabled
. -
Check the command line and sport your output.
2023-09-08 19:32:40.392 [WRN] ExecuteWorkflowCommand: Did not find workflow with name 'Workflow' in node 'TestHost System 2'.
2023-09-08 19:32:40.470 [INF] Operation: Hello from workflow, with greetings text 'Hi'There is a message saying that one device is excluded as it does not have the property
InjectEnabled
The other device executes the workflow as designed. Congrats, you have now learnt how to link commands and workflows, as well as injecting schemas into devices using rules.