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.
Note
Make sure to select Single Sule if you want to build up a rule using the code editor!
Basics - Rule construct
A Rule is always starting with a Trigger (1). The Trigger can represent a Variable, an Event or a Command; within one of the selected Information Models. After the trigger call mapTo (2) and define the function body by adding curly braces (3). Depending on the Trigger declare the TriggerInstance (4). Depending on the type of the Trigger use the naming accordingly:
The Source (5) is the content of the TriggerInstance (e.g., In case the Trigger is a Variable, then is the Source an Instance of that Variable) In order to assign the Source to the Target, add the := operator (6). The Target can be any variable you want to map to (7).
Trigger Types
Tree Member
Represents a basic Rule that uses as Trigger an Information Model element: Variables, Events or Commands.
Therefor define the Trigger as the following: <Information Model>.<Element from the Information Model> mapTo { <Element type> =>
(line 1).
1EquipmentDataModel.ItemNr mapTo { variable =>
2 Try {
3 EquipmentDataModel.DemoData.Temperature := RestServerModel.DemoData.Temperature
4 EquipmentDataModel.DemoData.Pressure := RestServerModel.DemoData.Pressure
5 }
6 }
Fixed Rate Scheduler
Rules can be scheduled to run continuously at a fixed rate. Instead of having an element of the Information Model defined as a Trigger the fixedRateScheduler method can be used.
Therefor define the Trigger as the following: _trigger.fixedRateScheduler(<Cron Expression>)
(line 2).
1def rule_ScheduleNode(): Unit = {
2 _trigger.fixedRateScheduler("0/1 * * * * ? *") mapTo(() => {
3 model1.StringVariable := model2.StringVariable
4 })
5 }
Fixed Delay Scheduler
Rules can be scheduled to run with a specific delay.
Therefor define the Trigger as the following: _trigger.fixedDelayScheduler(<Initial Delay>, <Period>, <Unit>) mapTo(() =>
(line 1).
1_trigger.fixedDelayScheduler(10, 60, SECONDS) mapTo(() => Try{
2 EquipmentDataModel.DemoData.Temperature := RestServerModel.DemoData.Temperature
3 EquipmentDataModel.DemoData.Pressure := RestServerModel.DemoData.Pressure
4 })
Target < > Source Assignment
Assignments of Same Type
When both target and source nodes are of the same data type the assignment of variables can be shorten:
1event1 := event2
Assignments of Different Type
The following examples illustrate how to implement assignments between source and target variables.
Source: Variable to Target: Event
This Mapping is used when dealing with static data that should be transformed to an Event. This might be the case when data is coming from a variable based data server (e.g., OPC UA server, Modbus, Iso-On-TCP) and needs to be mapped to an event or message-based target system (e.g., MQTT, Kafka, Databases, etc.).
The following example shows the mapping of variables from the EnterpriseModel and the EquipmentModel to an Event located in the MesModel:
Trigger: EquipmentModel.Alarm (line 1)
TriggerInstance of EquipmentModel.Alarm: variable (line 1)
Call send method on the EquipmentAlarm Event (line 2) and define the TriggerInstance: event (line 2)
The assignment of variables is done using the assignment operator :=. Both target and source are defined by entering the path of the variables in the Information Model e.g., event.EquipmentId and EnterpriseModel.EquipmentName (line 4)
1EquipmentModel.Alarm mapTo {variable =>
2 MesModel.EquipmentAlarm.send(event => {
3 Try {
4 event.EquipmentId := EnterpriseModel.EquipmentName
5 event.OrderNr := EquipmentModel.CurrentOrder.OrderNr
6 event.MaterialID := EquipmentModel.CurrentMaterialID
7 event.AlarmInfo := EquipmentModel.AlarmInfo
8 CommunicationLogger.log(variable, event)
9 }
10 })
11}
Source: Event to Target: Variable
This Mapping is used when dealing with event driven data that should be mapped to variables. This might be the case when data is coming from an event or message-based system (e.g., MQTT, Kafka, Databases, etc.) and needs to be mapped to a variable based data server (e.g., OPC UA server, Modbus, Iso-On-TCP).
The following example describes the mapping of values inside the TransferNewOrder Event from the MesModel into variables from the EquipmentModel:
The Trigger is defined by entering the path of the Event MesModel.TransferNewOrder (line 1). Since an Event is used as Trigger, the TriggerInstance is named accordingly event (line 1)
In the function body provide the Complex Variable NewOrder and the Simple Variable NewMESOrderFlag with data from the MesModels TransferNewOrder Event
Targets are defined by entering the path of the variables like EquipmentModel.NewOrder.OrderNr (line 3)
In order to assign values to OrderNr, MaterialNr and Quantity of the Complex Variable NewOrder, enter the TriggerInstance event followed by the variable name of the TransferNewOrder Event event.OrderNr (line 3)
In this case it is also possible to provide the variable NewMesOrderFlag with a Boolean like true (line 6)
1MesModel.TransferNewOrder mapTo { event =>
2 Try {
3 EquipmentModel.NewOrder.OrderNr := event.OrderNr
4 EquipmentModel.NewOrder.MaterialNr := event.MaterialNr
5 EquipmentModel.NewOrder.Quantity := event.Quantity
6 EquipmentModel.NewMESOrderFlag := true
7 }
8 }
Source: Event to Target: Command
This Mapping is used when dealing with event driven data that should be mapped to a Command. This might be the case when incoming event or message driven data should be enriched with data from another system (e.g., a database, a REST server, etc.) and then further mapped to another event driven message.
The following scenario describes a Rule mapping incoming data from a file to MQTT. When the FileEvent is triggered, the rule executes first the DatabaseCommand to retrieve data from a database (after the request is executed, the result of the reply can be accessed directly):
Trigger is defined by entering the path of the Event file.FileEvent (line 1). Since an Event is used as Trigger, the TriggerInstance should be named accordingly event (line 1)
Inside the function body execute a Command. The execution of a Command is defined by entering the path of the Command. At the end of the path, call the execute function (line 2). The TriggerInstance is named accordingly command (line 4)
The lines 4-6 show the first part of the Command. Here are assigned values from the source model to the Command Parameters
Since every Command has a Reply, we need to define the reply section (line 8)
In this case send out the data over MQTT after the data is retrieved from the database. In the reply function body, enter the path of the MqttEvent. Since this is the 2nd Event, the TriggerInstance can be named event1 (line 1)
Inside the function body assign values from the FileEvent (line 11-13) as well as from the Reply (line 14-15) to the MqttEvent
1 file.FileEvent mapTo {event =>
2 database.DatabaseCommand.execute(command => {
3 Try {
4 command.orderNr := event.orderNr
5 command.materialNr := event.materialNr
6 CommunicationLogger.log(event, command)
7 }
8 }, reply => {
9 mqtt.MqttEvent.send(event1 => {
10 Try {
11 event1.Quality := event.quality
12 event1.OrderNr := event.orderNr
13 event1.MaterialNr := event.materialNr
14 event1.Customer := reply.customer
15 event1.Product := reply.product
16 CommunicationLogger.log(reply, event1)
17 }
18 })
19 })
20 }
Mapping with Lists
If there are Lists structures within an Information Model that is going to be mapped to another Information Model, it is required to step through the list items using a foreach.
The following scenario describes a Rule that is mapping incoming data from a file to MQTT. The MQTT Model contains a List called DataList.
Create a variable listItem that holds a reference of a newItem in the DataList (line 6)
Call the variable from the listItem and assign the value from the file event (line 8)
1csv.FileEvent mapTo { event =>
2
3 event.items.foreach { item =>
4 mqtt.MqttEvent.send(event1 => {
5 Try {
6 val listItem = event1.DataList.newItem
7
8 listItem.Timestamp := item.Timestamp
9 listItem.Pressure := item.Alarmlevel
10
11 CommunicationLogger.log(event, event1)
12 }
13 })
14 }
15 }
Note
Lists can only be mapped in the code view.
Logging
Logging can be added in the Rule implementation by calling - CommunicationLogger.log (line 5)
1EquipmentModel.Alarm mapTo {variable =>
2 MesModel.EquipmentAlarm.send(event => {
3 Try {
4 event.EquipmentId := EnterpriseModel.EquipmentName
5 CommunicationLogger.log(variable, event)
6 }
7 })
8}
Compiling
You can compile the code for the selected Rule by clicking the “Compile” button (1) and check for compilation errors before saving the Rule.
SMARTUNIFIER Code Constructs
Rules are written in the Scala programming language. SMARTUNIFIER comes also with some custom code constructs that can be used within the Mapping which allow e.g., to do type conversions directly on the variable level.
Conversions
If both variables that are going to be mapped to each other are not of the same data type, use the provided type conversions. Conversions 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. -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 an String |
toStr(m1.IntVariable) |
toStr (definition: TPropertyDefinition [T]) |
A sequence of Chars |
|
Use the functional examples in the manual. e.g. toBoolean(event.MyValue) |
Operators
Operator methods can be used to implement calculations such as additions, subtractions, multiplications, and divisions. If it is required to make some calculations on the values of a variable within the mapping before sending data to the target system, the following methods can be used:
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) |
Loops (foreach)
In some use cases it might be required to iterate through a collection if the Information Model contains a list or an array. In this case, call the items method on the lists element of the Information Model followed by foreach (line 13).
1import java.time.LocalDateTime
2import java.time.Instant
3import java.time.ZoneId
4
5equipment.FileEvent mapTo { event =>
6 val newImportDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(System.currentTimeMillis()), ZoneId.systemDefault())
7 db.MainDatabaseEvent.send(event1 => Try {
8 event1.ImportDateTime := newImportDate
9 event1.StepId := event.StepId
10
11 event.analysisData.items.foreach {
12
13 case analysisDataType: ComplexCollectionVariableDefinition[AnalysisDataType] => {
14
15 val analysisDataItem = event1.analysisDataTable.newItem
16
17 analysisDataItem.name := analysisDataType.name
18 analysisDataItem.length := analysisDataType.length
19 }
20 }
21 })
22}
Conditions (If - statements)
Within a Rule it is possible to implement conditions using Scala’s conditional expressions. If statements can be used to test a condition before executing the block below. This can be used for example to check when a certain condition is met to execute an event (line 3).
1equipment.ActiveOrder.State mapTo { variable =>
2 logger.info(s"Active order state: ${variable.value} - Processing Finished")
3 if (variable.value == 3) {
4 mes.NotifyOrderFinished.send(event => {
5 Try{
6 event.EquipmentId := equipment.EquipmentInformation.EquipmentType
7 event.OrderNr := equipment.ActiveOrder.OrderInformation.OrderNo
8 event.ProductNumber := equipment.ActiveOrder.OrderInformation.ProductNo
9 event.QuantityOk := equipment.ActiveOrder.QuantityOk
10 event.QuantityNOk := equipment.ActiveOrder.QuantityNOk
11 }
12 })
13 }
14}
Exception Handling (Try/Catch)
Exception Handling is an integrated part of the SMARTUNIFIER mapping logic. The mapTo and send callbacks expect a return value of the Scala type Try. In case of an exception happening in one of the rules, the SMARTUNIFIER logs the exception and shows a notification in the manager. Supported Communication Channels execute further actions once an exception occurred E.g., the File Reader Channel moves a file that initially triggered a Rule into an error folder.
In the example below, the Try block is placed after each command and event call (line 2 and 4).
1database.update mapTo { (updateCommand, reply) =>
2 Try {
3 api.AmorphAPI.send(event =>
4 Try {
5
6 event.id := updateCommand.Identifier.Id
7 event.name := updateCommand.Identifier.Name
8
9 updateCommand.Status.items.foreach(item => {
10 val statusItem = event.status.newItem
11 statusItem.index := item.Index
12 statusItem.value := itemValue
13 })
14 }
15 )
16 }
17}