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:

RuleConstruct

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).

RuleConstruct

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).

Tree Member
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).

Fixed Rate Scheduler
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).

Fixed Delay Scheduler
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:

Type Assignment (Events)
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)

Rule - StartOrder - Variable/Event
 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)

Rule - TransferNewOrder - Event/Variable
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

Rule - File2MqttWithDB - Event/Commands
 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)

Rule - FileToMQTT - Lists
 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)

Rule with Logging
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.

CompileCode

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).

Code constructs - Loops
 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).

Code constructs - Conditions
 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).

Code constructs - Exception Handling
 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}