Code-based Rules¶
The main target of SMARTUNIFIER is to build up the connectivity between systems. Sometimes integrations become more complex and it might require to build up Rules via the code editor using the Scala programming language. SMARTUNIFIER extends the Scala programming language with addition operators and methods to simplify the realization of data transfer between Information Models.
Similar to Mappings via drag and drop, there is no knowledge of the underlying communication protocol (e.g., MQTT, OPCUA, etc.) needed. Protocols are hidden behind the corresponding Information Models. The parameter values of an Information Model are stored in the objects of type VariableDefinition[T] or PropertyDefinition[T]. These contain additional information and methods rather than just the parameter values. They also provide methods to listen for changes and conversion between variable types.
Basics¶
Rule construct¶
A rule always starts with a Trigger (1). The trigger can represent an element of the Information Model, such as a Variable, Event, Command, or it can be time-based.
After the trigger, call mapTo (2) and define the function body by adding curly braces (3).
Depending on the trigger, declare the TriggerInstance (4). Use naming that corresponds to the type of the trigger.
The Source (5) is the content of the TriggerInstance. For example, if the trigger is a Variable, then the Source is an instance of that Variable.
To assign the Source to the Target, use the := operator (6).
The Target can be any variable you want to map to (7).
Compiling¶
Compile the code for the selected rule by clicking the “Compile” button (1) and check for compilation errors before saving the rule.
Logging¶
Logging can be added in the Rule implementation by calling - CommunicationLogger.log (line 5)
1 2 3 4 5 6 7 8 |
|
Trigger Types¶
Tree Member¶
The following Information Model elements can be used as a trigger: Variables, Events, Commands. The snippet below shows how the trigger is defined:
<Information Model>.<Element from the Information Model> mapTo { <Element type> =>
1 2 3 4 5 6 |
|
Schedulers¶
With schedulers, you can execute Rules at specified times or intervals. You can choose from the following scheduler types:
Typically, the scheduler is started automatically and executes the rule once the instance is started, and used channels are in the “Connected” state.
However, you can manually trigger the execution and termination of a rule by using the start/stop function of the scheduler:
_trigger.Schedulers("<name of rule>").start()
_trigger.Schedulers("<name of rule>").stop()
Fixed Rate Scheduler¶
Rules can be scheduled to run continuously at a fixed rate. Instead of defining an element of the Information Model as a trigger, the fixedRateScheduler method can be used. The snippet below shows how the fixed rate scheduler is defined:
_trigger.fixedRateScheduler(<Cron Expression>)
1 2 3 |
|
Fixed Delay Scheduler¶
Rules can be scheduled to run at a fixed rate with an initial delay. The snippet below shows how the fixed delay scheduler is defined:
_trigger.fixedDelayScheduler(<Initial Delay>, <Period>, <Unit>) mapTo(() =>
1 2 3 4 |
|
Timeout Scheduler¶
Rules can be scheduled to run after a specific timeout. The snippet below demonstrates how the timeout scheduler is defined:
_trigger.timeoutScheduler(<Delay>, <Unit>) mapTo(() =>
1 2 3 4 |
|
Target-to-Source Mapping¶
Node Types Sharing the Same Custom Data Type¶
When the target and source Node Types in the Information Model are both of the same Custom Data Type, the Mapping can be simplified:
event1 := event2
The two Node Types have to be of the same kind e.g., both are Events or both are Variables.
Node Types with different Custom Data Type¶
The examples demonstrate how to map values between source and target variables.
Variables to Events¶
This mapping is utilized when static data needs to be transformed into an Event. This is often the case when data originates from a variable-based data server (such as OPC UA server, Modbus, Iso-On-TCP) and is required to be mapped to an event or message-based target system (like MQTT, Kafka, Databases, etc.).
The example below illustrates the mapping of variables from the EnterpriseModel and the EquipmentModel to an Event within the MesModel:
Trigger: EquipmentModel.Alarm (line 1)
TriggerInstance of EquipmentModel.Alarm: variable (line 1)
Invoke the send method on the EquipmentAlarm Event (line 2) and define the TriggerInstance as event (line 2)
Variable assignment is performed using the assignment operator :=. Both target and source are specified by entering the path of the variables in the Information Model, for example, event.EquipmentId and EnterpriseModel.EquipmentName (line 4)
1 2 3 4 5 6 7 8 9 10 11 |
|
Event to Variables¶
This mapping is utilized when dealing with event-driven data that needs to be mapped to variables. This scenario often occurs when data originates from an event or message-based system (e.g., MQTT, Kafka, Databases, etc.) and needs to be mapped to a variable-based data server (such as OPC UA server, Modbus, Iso-On-TCP).
The example below outlines the mapping of values from the TransferNewOrder Event in the MesModel into variables within the EquipmentModel:
The Trigger is specified by entering the path of the Event MesModel.TransferNewOrder (line 1). Since an Event is utilized as the Trigger, the TriggerInstance is appropriately named event (line 1).
In the function body, the Complex Variable NewOrder and the Simple Variable NewMESOrderFlag are provided with data from the MesModel’s TransferNewOrder Event.
Targets are specified by entering the path of the variables, such as EquipmentModel.NewOrder.OrderNr (line 3).
To assign values to OrderNr, MaterialNr and Quantity of the Complex Variable NewOrder, enter the TriggerInstance event followed by the variable name from the TransferNewOrder Event, e.g., event.OrderNr (line 3).
In this case it is also possible to assign the variable NewMesOrderFlag a Boolean value like true (line 6)
1 2 3 4 5 6 7 8 |
|
Event to Commands¶
This mapping is employed when dealing with event-driven data that needs to be mapped to a Command. This scenario may arise when incoming event or message-driven data should be enriched with data from another system (such as a database or a REST server) before being further mapped to another event-driven message.
The following scenario describes a rule that maps incoming data from a file to MQTT. When the FileEvent is triggered, the rule first executes the DatabaseCommand to retrieve data from a database (the result of the reply can be accessed directly afterward):
Trigger is specified by entering the path of the Event file.FileEvent (line 1). Since an Event serves as the Trigger, the TriggerInstance should be named event (line 1)
Within the function body, execute a Command. The execution of a Command is specified by entering the path of the Command and calling the execute function at the end of the path (line 2). The TriggerInstance is named command (line 4).
Lines 4-6 illustrate the first part of the Command execution, where values from the source model are assigned to the Command Parameters.
Every Command includes a Reply, which necessitates defining the reply section (line 8).
After retrieving data from the database, send out the data over MQTT. In the reply function body, specify the path of the MqttEvent. Since this is the second Event, the TriggerInstance can be named event1 (line 10).
Within the reply function body, assign values from the FileEvent (lines 11-13) as well as from the Reply (lines 14-15) to the MqttEvent.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Properties to Variables¶
When a Property serves as the source and a Variable as the target, the mapping is straightforward: the Property is assigned to the Variable using the assignment operator :=. This approach may be utilized when dealing with an XML structure that includes XML-Attributes, which are modeled as Properties in the Information Model, while the target system expects the data to be presented as Variables.
1 |
|
Mapping including Lists¶
If there are Lists structures within an Information Model that need to be mapped to another Information Model, it is necessary to iterate through the list items using a foreach loop.
The following scenario describes a Rule that maps incoming data from a file to MQTT. The MQTT Model contains a List called DataList.
Initialize a variable named listItem reference a newItem in the DataList (line 6)
Then, assign the value from the file event to this variable listItem (line 8)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Note
Lists can only be mapped in the code view.
SMARTUNIFIER Code Constructs¶
Rules are written in the Scala programming language. SMARTUNIFIER also includes custom code constructs that can be used within mappings, allowing for operations such as type conversions directly at the variable level.
Converters¶
If the variables to be mapped to each other are not of the same data type, use the provided type converters. Converters can be used on Information Model nodes such as Variables and on Properties .
Method |
Description |
Example |
---|---|---|
toBoolean(definition: TVariableDefinition[T]) |
Converts a variable to a Boolean |
toBoolean(m1.IntVariable) |
toBoolean(definition: TPropertyDefinition [T]) |
Either the literal true or the literal false |
‘’ |
toByte (definition: TVariableDefinition[T]) |
Conversion of a variable to an Byte |
toByte(m1.IntVariable) |
toByte (definition: TPropertyDefinition [T]) |
8 bit signed value. Range from -128 to 127 |
‘’ |
toShort (definition: TVariableDefinition[T]) |
Conversion of a variable to a Short |
toShort(m1.IntVariable) |
toShort (definition: TPropertyDefinition [T]) |
16 bit signed value. Range -32768 to 32767 |
‘’ |
toInt (definition: TVariableDefinition[T]) |
Conversion of a variable to an Integer |
toInt(m1.StringVariable) |
toInt (definition: TPropertyDefinition [T]) |
32 bit signed value. Range -2147483648 to 2147483647 |
‘’ |
toLong (definition: TVariableDefinition[T]) |
Conversion of a variable to a Long |
toLong(m1.IntVariable) |
toLong (definition: TPropertyDefinition [T]) |
64 bit signed value. Range -9223372036854775808 to 9223372036854775807 |
‘’ |
toFloat(definition: TVariableDefinition[T]) |
Conversion of a variable to a Float |
toFloat(m1.IntVariable) |
toFloat (definition: TPropertyDefinition [T]) |
32 bit IEEE 754 single-precision float |
‘’ |
toDouble(definition: TVariableDefinition[T]) |
Conversion of a variable to a Double |
toDouble(m1.IntVariable) |
toDouble (definition: TPropertyDefinition [T]) |
64 bit IEEE 754 double-precision float |
‘’ |
toStr(definition: TVariableDefinition[T]) |
Conversion of a variable to a String |
toStr(m1.IntVariable) |
toStr (definition: TPropertyDefinition [T]) |
A sequence of Chars |
‘’ |
Math Operators¶
Math Operator methods can be utilized to perform calculations, such as addition, subtraction, multiplication, and division. If there’s a need to perform calculations on the values of a variable within the mapping before sending data to the target system, the following methods can be employed:
Method |
Description |
Example |
---|---|---|
add(Option[T],Double) |
Addition of a variable with a numeric data type and a Double value |
add(model.IntVariable, 2) |
sub(Option[T],Double) |
Subtraction of a variable with a numeric data type and a Double value |
sub(model.IntVariable, 2.5) |
mult(Option[T],Double) |
Multiplication of a variable with a numeric data type and a Double value |
mult(model.IntVariable, 3) |
div(Option[T],Double) |
Division of a variable with a numeric data type and a Double value |
div(model.IntVariable, 3.5) |
String Operators¶
String Operator methods can be utilized to perform String manipulation:
Method |
Description |
Example |
---|---|---|
trim(Option[String]) |
Removes leading and trailing whitespace from a string. |
trim(variable) |
toLowerCase(Option[String]) |
Converts all characters in a string to lower case. |
toLowerCase(variable) |
toUpperCase(Option[String]) |
Converts all characters in a string to upper case. |
toUpperCase(variable) |
strip(Option[String]) |
Similar to trim, removes leading and trailing whitespace from a string. |
strip(variable) |
matches(Option[String], Option[String]) |
Checks if the string matches the given regular expression. |
matches(variable, testString.r.regex) |
replace(Option[String], Option[String], Option[String]) |
Replaces first occurrence of a substring within the string with the specified replacement. |
replace(variable, “T”, “X”) |
replaceAll(Option[String], Option[String], Option[String]) |
Replaces all occurrences of a substring within the string with the specified replacement. |
replaceAll(variable, “e”, “i”) |
substring(Option[String], Int) |
Extracts a substring from the string starting at the specified index. |
substring(variable, 3) |
concat(Option[String], Option[String]) |
Concatenates two strings together. |
concat(variable, variable) |
Helpers¶
Helpers are methods that can be used to simplify the mapping process. They can be used to compare the value of a variable with a given value, or to map child variables from one complex variable to another.
Method |
Description |
Example |
---|---|---|
equals(TVariableDefinition[T],Any) |
Compares the value of a variable with a given value |
equals(m1.StringVariable, “Foo”) |
formatDateTime(Option[java.time.OffsetDateTime], DateTimeFormatter) |
Applies a DateTimeFormatter to a DateTime |
formatDateTime(parseDateTime(m1.Timestamp, “HH:mm:ss”) |
parseDateTime(Option[String]) |
Parses a string to a DateTime |
parseDateTime(m1.Timestamp) |
mapAndAssignChildren(TComplexVariableDefinition[T],TComplexVariableDefinition[T]) |
Maps child variables from one complex variable to another mapAndAssignChildren(<source>, <target>) |
mapAndAssignChildren(m1.ComplexVariableDepth1, m2.ComplexVariableDepth1) |
Loops (foreach)¶
In some use cases, it may be necessary to iterate through a collection if the Information Model contains a list or an array. In this case, call the items method on the list element of the Information Model, followed by foreach (line 13).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Conditions (If - statements)¶
Within a rule, it’s possible to implement conditions using Scala’s conditional expressions. If statements can be used to test a condition before executing the subsequent block. For example, this can be utilized to check if a certain condition is met before executing an event (line 3).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Exception Handling (Try/Catch)¶
Exception Handling is an integral part of the SMARTUNIFIER mapping logic. The mapTo and send callbacks expect a return value of the Scala type Try. If an exception occurs in one of the rules, SMARTUNIFIER logs the exception and displays a notification in the manager. Supported Communication Channels take further actions once an exception has occurred. For example, the File Reader Channel moves a file that initially triggered a rule into an error folder.
In the example below, a Try block is placed after each command and event call (lines 2 and 4).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Breaking out of Rules¶
You can break out of a rule by calling the Break() method in your code. Any code defined after the Break() method will not be executed.
Example 1: The Break() method can be used to stop the execution of the code if, for example, a variable value is not present (lines 2-4) but is needed later (line 9).
1 2 3 4 5 6 7 8 9 10 11 |
|
Example 2: Breaking out of a loop if the iterator does not have a next element (line 4) and calling Break() (line 5).
1 2 3 4 5 6 7 8 9 |
|