Implementing a protocol conversion with Kura Wires

A protocol conversion can be modeled in the Driver, Asset and Wires models as a data transfer between two different drivers.

This can be achieved by exploiting the rules for read, write and listen mode of the WireAsset to get some data from a driver and write it to another one.

This guide illustrates the general concepts required for implementing the conversion and shows how to address some common use cases.

A step by step example guide showing how to implement a conversion between the Modbus master and DNP3 slave drivers is available in ESF documentation

Prerequisites

This guide assumes basic knowledge about the Driver, Asset and Wires models, and how to use Assets within Kura Wires, the following guides should provide information on these topics:

Base graph topology

A unidirectional conversion can be implemented with the following basic graph topology:

Timer -> DriverAReadAsset -> DriverBWriteAsset

Timer is a timer component.
DriverAReadAsset is a WireAsset attached to the driver DriverA, DriverBWriteAsset is a WireAsset attached to the driver DriverB

DriverAReadAsset defines a channel with

  • name = myChannel
  • value.type = INTEGER
  • type = READ
  • listen = false
  • other parameters specific to DriverA.

DriverBWriteAsset defines a channel with

  • name = myChannel
  • value.type = INTEGER
  • type = READ_WRITE
  • listen = false
  • other parameters specific to DriverB.

Basing on the rules for read and write mode, when Timer ticks the following operations will happen:

  1. The envelope emitted by Timer is received by DriverAReadAsset, this will cause DriverA to read the value of the resource identified by myChannel, the result will be emitted by DriverAReadAsset as the following envelope:
  • WireEnvelope
    • WireRecord[0]
      • assetName: DriverAReadAsset (type = STRING)
      • myChannel: 78 (type = INTEGER)
      • myChannel_timestamp: 1597925188 (type = LONG)
  1. The envelope above is received by DriverBWriteAsset, this will cause DriverB to update the data point identified by myChannel with the value 78. This is due to the fact that the rule for write mode is respected, since DriverBWriteAsset defines a channel that is named myChannel with type = READ_WRITE and with the same value.type as the value of the myChannel property in the received envelope.

The example above sends the data obtained from DriverA to DriverB at every poll cycle of Timer.

If listen mode is used, the timer can be removed, obtaining the following topology:

DriverAReadAsset -> DriverBWriteAsset

In order to achieve this, the configuration of myChannel on DriverAReadAsset should be changed by setting listen = true.

In this case the transfer will occur only if and when DriverA generates an event for the data point identified by myChannel.

Listen mode is not supported by all drivers, the user should check the driver specific documentation to verify if this mode is supported.

To recap, the general rule for mapping the data point dataPointA reachable using DriverA and dataPointB reachable trough DriverB is the following:

  1. Create two assets, DriverAReadAsset and DriverBWriteAsset attached to the corresponding drivers

  2. create one channel per asset, with the same name and value.type.

  3. Configure the driver specific parameters of the channel on DriverAReadAsset so that it refers to dataPointA, set type to READ or READ_WRITE.

  4. Configure the driver specific parameters of the channel on DriverBWriteAsset so that it refers to dataPointB, set type to WRITE or READ_WRITE.

  5. Connect the two Assets in a Wire graph using the topology shown above.

Note: due to an implementation detail, the data points referenced in write mode on slave drivers usually must use READ_WRITE and not WRITE as type. This applies at least to the DNP3 and IEC60870 slave drivers and is mentioned in the specific documentation.

The scheme above creates an unidirectional mapping between two drivers (a data flow from DriverA to DriverB). Since Kura Wires only allows directed graphs another graph branch must be added in order to establish a bi-directional mapping.
This second branch would define channels in write mode on DriverA and channels in read mode on DriverB. This requires introducing two more Assets named for example DriverBReadAsset and DriverAWriteAsset, connected with the same topology and rules shown above:

Timer -> DriverBReadAsset -> DriverAWriteAsset

The resulting graph will therefore contain two branches:

TimerA -> DriverAReadAsset -> DriverBWriteAsset

TimerB -> DriverBReadAsset -> DriverAWriteAsset

Mapping semantics

The section above is quite abstract and does not make any assumption on the protocols and roles implemented by DriverA and DriverB. This is due to the fact that the protocol conversion rules using Kura Wires are largely independent from these aspects.

For example is possible to implement the following conversions:

Master driver <-> slave driver

For example if DriverA is the Modbus master driver and DriverB is the DNP3 slave driver, the rules above allow to expose the registers/coils on a Modbus slave as DNP3 data points.

For example a

Timer -> DriverAReadAsset -> DriverBWriteAsset

branch allows to read the values of registers/coils on a Modbus slave and copy them on DNP3 data points like analog inputs or binary inputs. These data points can then be queried by a DNP3 master SCADA through polling and/or using unsolicited reporting mode.

A

Timer -> DriverBReadAsset -> DriverAWriteAsset

branch allows for example to read the values of DNP3 analog outputs and binary outputs and copy them to Modbus registers or coils. Since the analog outputs and binary output can be changed by the SCADA, this allows it to change the value of the Modbus data points.

Since the DNP3 driver supports listen mode for reporting data point change events, the Timer can be removed on this branch to change the values of Modbus registers only when a command is received. The Timer can also be kept in order to implement a periodic write cycle, that can be more robust in case of Modbus communication problems.

The change above cannot be made on the Modbus -> DNP3 branch because the Modbus master driver does not support listen mode, but only polling.

Master driver <-> master driver

For example if DriverA is the Ethernet/IP for AllenBradley master driver and DriverB is the Modbus master driver, the rules above allow to map AllenBradley tags and Modbus registers/coils.

In this case the Timer cannot be removed on any branch since none of the two drivers support listen mode.

A

Timer -> DriverAReadAsset -> DriverBWriteAsset

branch can be used to periodically copy the data point values from the AllenBradley PLC tags to the Modbus registers/coils, while the

Timer -> DriverBReadAsset -> DriverAWriteAsset

branch sends the data in the other direction.

Slave driver <-> slave driver

The same rules allow to exchange data between two slave drivers. This allows for example to share data between a DNP3 master and an IEC60870 master.

Data processing

The simple graph topology above allows to transfer data point values unchanged between two drivers.
It is also possible to add other processing components between a read Asset and a write Asset.
An example of this component is the Javascript filter.
This component allows to write logic in the Javascript language to process the data contained in the wire envelopes, it allows to address several use cases, for example:

  • Generate computed data points

    • Compute the average of a data point over time
    • Create an output data point basing on the values of multiple input data points
    • Count the occurrences of particular conditions in input data.
  • Decode the JSON strings produced by some drivers to represent structured data and extract atomic data points out of them.

  • Implement an on-change cache. This allows to limit the number of write operations performed by the write Asset. See below for an example script.

  • Detect alert conditions basing on input data.

An example of a graph containing a Script Filter can be the following:

Timer -> DriverAReadAsset -> ScriptFilter -> DriverBWriteAsset

Driver thread decoupling

Connecting two Assets on the same Wire Graph branch can introduce coupling between the processing threads of the two drivers. This can cause timeouts occurring on one protocol end to propagate to the other.

For example a timeout on a Modbus Write might slow down DNP3 or IEC 60870 communication.

This is caused by the fact that the envelopes are delivered on a graph branch by a single thread synchronously. This thread is usually managed by the Timer component or it is a Driver internal thread in case of delivery of listen mode events.

A way to overcome this is to place a FIFO component between the read and write Assets:

Timer -> DriverAReadAsset -> FIFO -> DriverBWriteAsset

The FIFO component can be used to introduce a second thread that receives the envelopes from DriverAReadAsset and delivers them to DriverBWriteAsset asynchronously.

The graph above will use two threads, one managed by the Timer component, and another one managed by the FIFO component. In this case if the FIFO component blocks due to a communication timeout, the Timer thread will not be affected.

FIFO components and processing components can coexist on the same branch, for example:

Timer -> DriverAReadAsset -> FIFO -> Script -> DriverBWriteAsset

Communication error detection

It might be useful to introduce a computed data point on one protocol end that reports communication errors on the other one. For example this data point can be introduced in a Modbus to DNP3 conversion to report to the DNP3 master SCADA a communication failure in the communication with the Modbus slave.

A way for detecting errors originated from an Asset read is detecting the absence of a the channel value in the emitted envelopes.
If the Asset is triggered periodically using a Timer, if everything works correctly we can expect that values obtained from the Asset are emitted on the Wire Graph at every poll cycle. If this does not happen, then some error must have occurred.

This can be exploited to create a Javascript filter that detects delays in Asset reads, like the following:

// this script produces a boolean property with key defined by the STATUS_PROPERTY variable.
// STATUS_PROPERTY is true if a property with key TARGET_PROPERTY has been received
// by this component in the latest GRACE_INTERVAL_MS milliseconds, false otherwise.
// STATUS_PROPERTY will be STARTUP_VALUE for the first GRACE_INTERVAL_MS milliseconds after component activation
// or until TARGET_PROPERTY is received for the first time

// the property to monitor in received envelopes
var TARGET_PROPERTY = "register_value"
// the allowed interval before reporting a warning
var GRACE_INTERVAL_MS = 10000
// the emitted status property
var STATUS_PROPERTY = "comm_status"
// the status to emit immediately after activation
var STARTUP_VALUE = true

function initLastActivityTimestamp() {
  if (STARTUP_VALUE == true) {
    return new Date().getTime()
  } else {
    return 0
  }
}

lastActivityTimestamp = typeof(lastActivityTimestamp) === 'undefined' ? initLastActivityTimestamp() : lastActivityTimestamp

var record = input.records[0]

var now = new Date().getTime()

if (record[TARGET_PROPERTY]) {
  lastActivityTimestamp = now
}

var status = (now - lastActivityTimestamp) < GRACE_INTERVAL_MS

var outRecord = newWireRecord()
outRecord[STATUS_PROPERTY] = newBooleanValue(status)
output.add(outRecord)

This script requires an additional timer that triggers the status check, it can be connected with the following topology:

Performing writes on value change

The script below is an example that shows how to implement an on change cache using the Javascript filter. It can be extended for example to periodically emit all values using an additional Timer or to introduce some form of deadbanding.

var cache = typeof(cache) === "undefined" ? {} : cache

var updateCacheAndCheckChange = function (key, current) {
  var previous = cache[key]
  cache[key] = current

  if (
    previous === null
    || previous === undefined
    || previous.getType() !== current.getType()
    || previous.getValue() !== current.getValue()
  ) {
    return true
  }

  return false
}

var outputRecord = newWireRecord()
var somePropertyChanged = false

for (var recordIndex = 0; recordIndex < input.records.length; recordIndex++) {
  var inputRecord = input.records[recordIndex]

  for (var prop in inputRecord) {

    var propValue = inputRecord[prop]

    if (updateCacheAndCheckChange(prop, propValue)) {
      outputRecord[prop] = propValue
      somePropertyChanged = true
    }
  }
}

if (somePropertyChanged) {
  output.add(outputRecord)
}

It can be enabled by adding a Javascript filter containing it between a read Asset and a write Asset:

Timer -> DriverAReadAsset -> OnChangeFilter -> DriverBWriteAsset