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.

Mapping Code - Rule Construct

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

Mapping Code - Rule Construct

Compiling

Compile the code for the selected rule by clicking the “Compile” button (1) and check for compilation errors before saving the rule.

Mapping Code - Rule Compiling

Logging

Logging can be added in the Rule implementation by calling - CommunicationLogger.log (line 5)

Rule with Logging
1
2
3
4
5
6
7
8
EquipmentModel.Alarm mapTo {variable =>
   MesModel.EquipmentAlarm.send(event => {
     Try {
       event.EquipmentId := EnterpriseModel.EquipmentName
       CommunicationLogger.log(variable, event)
     }
   })
}

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> =>

Tree Member as Trigger
1
2
3
4
5
6
EquipmentDataModel.ItemNr mapTo { variable =>
   Try {
     EquipmentDataModel.DemoData.Temperature := RestServerModel.DemoData.Temperature
     EquipmentDataModel.DemoData.Pressure := RestServerModel.DemoData.Pressure
   }
 }

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:

Start Rule
_trigger.Schedulers("<name of rule>").start()
Stop Rule
_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>)

Fixed Rate Scheduler
1
2
3
 _trigger.fixedRateScheduler("0/1 * * * * ? *") mapTo(() => {
   model1.StringVariable := model2.StringVariable
 })

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(() =>

Fixed Delay Scheduler
1
2
3
4
_trigger.fixedDelayScheduler(10, 60, SECONDS) mapTo(() => Try{
   EquipmentDataModel.DemoData.Temperature := RestServerModel.DemoData.Temperature
   EquipmentDataModel.DemoData.Pressure := RestServerModel.DemoData.Pressure
 })

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(() =>

Timeout Scheduler
1
2
3
4
_trigger.timeoutScheduler(60, SECONDS) mapTo(() => Try{
   EquipmentDataModel.DemoData.Temperature := RestServerModel.DemoData.Temperature
   EquipmentDataModel.DemoData.Pressure := RestServerModel.DemoData.Pressure
 })

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:

Mapping of two Events with the same type
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)

Rule - StartOrder - Variable/Event
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
EquipmentModel.Alarm mapTo {variable =>
   MesModel.EquipmentAlarm.send(event => {
     Try {
       event.EquipmentId := EnterpriseModel.EquipmentName
       event.OrderNr := EquipmentModel.CurrentOrder.OrderNr
       event.MaterialID := EquipmentModel.CurrentMaterialID
       event.AlarmInfo := EquipmentModel.AlarmInfo
       CommunicationLogger.log(variable, event)
     }
   })
}

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)

Rule - TransferNewOrder - Event/Variable
1
2
3
4
5
6
7
8
MesModel.TransferNewOrder mapTo { event =>
   Try {
     EquipmentModel.NewOrder.OrderNr := event.OrderNr
     EquipmentModel.NewOrder.MaterialNr := event.MaterialNr
     EquipmentModel.NewOrder.Quantity := event.Quantity
     EquipmentModel.NewMESOrderFlag := true
   }
 }

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.

Rule - File2MqttWithDB - Event/Commands
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
 file.FileEvent mapTo {event =>
   database.DatabaseCommand.execute(command => {
     Try {
       command.orderNr := event.orderNr
       command.materialNr := event.materialNr
       CommunicationLogger.log(event, command)
     }
   }, reply => {
     mqtt.MqttEvent.send(event1 => {
       Try {
         event1.Quality := event.quality
         event1.OrderNr := event.orderNr
         event1.MaterialNr := event.materialNr
         event1.Customer := reply.customer
         event1.Product := reply.product
         CommunicationLogger.log(reply, event1)
         }
     })
   })
 }

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.

Rule - Property/Variables
1
propertyNodeType := variableNodeType

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)

Rule - FileToMQTT - Lists
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
csv.FileEvent mapTo { event =>

     event.items.foreach { item =>
       mqtt.MqttEvent.send(event1 => {
         Try {
           val listItem = event1.DataList.newItem

           listItem.Timestamp := item.Timestamp
           listItem.Pressure := item.Alarmlevel

           CommunicationLogger.log(event, event1)
         }
       })
     }
 }

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

Code constructs - Loops
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.time.LocalDateTime
import java.time.Instant
import java.time.ZoneId

equipment.FileEvent mapTo { event =>
 val newImportDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(System.currentTimeMillis()), ZoneId.systemDefault())
 db.MainDatabaseEvent.send(event1 => Try {
   event1.ImportDateTime := newImportDate
   event1.StepId := event.StepId

   event.analysisData.items.foreach {

     case analysisDataType: ComplexCollectionVariableDefinition[AnalysisDataType] => {

       val analysisDataItem = event1.analysisDataTable.newItem

       analysisDataItem.name := analysisDataType.name
       analysisDataItem.length := analysisDataType.length
     }
   }
 })
}

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

Code constructs - Conditions
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
equipment.ActiveOrder.State mapTo { variable =>
 logger.info(s"Active order state: ${variable.value} - Processing Finished")
 if (variable.value == 3) {
   mes.NotifyOrderFinished.send(event => {
     Try{
       event.EquipmentId := equipment.EquipmentInformation.EquipmentType
       event.OrderNr := equipment.ActiveOrder.OrderInformation.OrderNo
       event.ProductNumber := equipment.ActiveOrder.OrderInformation.ProductNo
       event.QuantityOk := equipment.ActiveOrder.QuantityOk
       event.QuantityNOk := equipment.ActiveOrder.QuantityNOk
     }
   })
 }
}

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

Code constructs - Exception Handling
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
database.update mapTo { (updateCommand, reply) =>
 Try {
   api.AmorphAPI.send(event =>
     Try {

       event.id := updateCommand.Identifier.Id
       event.name := updateCommand.Identifier.Name

       updateCommand.Status.items.foreach(item => {
         val statusItem = event.status.newItem
         statusItem.index := item.Index
         statusItem.value := itemValue
       })
     }
   )
 }
}

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

Code constructs - Break()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def rule_r1(): Unit = {
  if(model1.myVariable.isEmpty()) {
    Break()
  }

 //... mapping code here ...

 model1.myVariable.mapTo {
   variable => model2.myVariable := variable
 }
}

Example 2: Breaking out of a loop if the iterator does not have a next element (line 4) and calling Break() (line 5).

Code constructs - Break()
1
2
3
4
5
6
7
8
9
def rule_r2(): Unit = {
  val iterator = model1.myList.items.iterator
  while(iterator.hasNext){
    if(iterator.next().isEmpty()){
      Break()
    }
  }
   // ... mapping code here ...
}