DNP3 Master Driver

The ESF DNP3 driver provides support for the DNP3 master protocol.

Features:

  • Support for Serial and TCP/IP.
  • Support for polled read operations, commands and unsolicited messaging.
  • Support for TLS
  • Support for UDP (starting from 2.1.0)

Driver global configuration

The configuration of this driver depends on the type of transport used (SERIAL or IP).

In case of IP transport, only IP address and port of the target device are required, in this case an unsecured channel will be used.

If the transport type is IP, the driver allows to use TLS to create a secure channel. TLS can be enabled by setting the ip.tls.enable parameter to true.

If TLS is enabled, the TLS related options must be properly specified.

In case of SERIAL transport, the path of the used serial port (usually /dev/ttyxxxx) and port configuration (speed, data bits, parity, stop bits) is required.

UDP support

Knows issues/limitations

Version 2.1.0 is affected by OpenDNP3 issue https://github.com/dnp3/opendnp3/issues/405.
This can cause connection problems and log flooding if the drivers receives UDP communication related ICMP errors.
Until the issue is fixed upstream, a possible workaround is to filter ICMP error messages using firewall.
This can be done adding the following iptables rules on the gateway, for example by adding them in /etc/sysconfig/iptables:

-A INPUT -p icmp -m icmp --icmp-type port-unreachable -j DROP
-A INPUT -p icmp -m icmp --icmp-type host-unreachable -j DROP

While this should prevent the issue, these rules will cause parsing failures in ESF 6.2.0 and earlier Firewall Service. This will cause the Firewall Web UI to stop working. This will be fixed in ESF 7.0.0.

In order to restore Firewall Web UI operation, the rules above can be removed from /etc/sysconfig/iptables and restart the gateway, this will also remove the workaround.

If possible, similar rules should be instead added to the peer so that it does not send back ICMP errors to the gateway.

The Master Driver currently supports at most one outstation per instance over UDP.

Using UDP on Docker will not work if the UDP port is exposed using the -p docker run argument due to port/address translation.

Configuration parameter changes

The following global configuration parameters have been changed/added in 2.1.0:

  • The UDP option has been added to the transport.type parameter. Select this option to enable UDP communication.

  • The new local.socket.address parameter must be set to the address for the local UDP socket. Setting this parameter to 0.0.0.0 will cause the driver to listen on all interfaces. This parameter is mandatory for UDP communication, it is otherwise ignored.

  • The new local.socket.port parameter must be set to the local UDP port used by the master. This is the source port that will be used by the master and the port on which it expects to receive messages from the outstation. This parameter is mandatory for UDP communication, it is otherwise ignored.

Channel Configuration

The DNP3 channel configuration is composed by the following parameters:

  • Dest. Address: The destination address of the remote outstation.

The meaning of the other parameters depends on the value of the Function parameter.

  • SCAN_ALL: Performs a read operation in all objects mode (Qualifier code 6). Mandatory configuration parameters:

    • SCAN Group: The object group
    • SCAN Variation: The variation, or 0 for all variations
  • SCAN_RANGE8: Performs a read operation in range-index mode using an 8-bit indexes (Qualifier code 0). Mandatory configuration parameters:

    • SCAN Group: The object group
    • SCAN Variation: The variation, or 0 for all variations
    • SCAN Idx From: The start index, must be in the [0,255] range.
    • SCAN Idx To: The end index, must be in the [0,255] range.
  • SCAN_RANGE16: Performs a read operation in range-index mode using an 16-bit indexes (Qualifier code 1). Mandatory configuration parameters:

    • SCAN Group: The object group
    • SCAN Variation: The variation, or 0 for all variations
    • SCAN Idx From: The start index, must be in the [0,65536] range.
    • SCAN Idx To: The end index, must be in the [0,65536] range.
  • SCAN_COUNT8: Performs a read operation in non ranged mode using an 8-bit indexes (Qualifier code 7). Mandatory configuration parameters:

    • SCAN Group: The object group
    • SCAN Variation: The variation, or 0 for all variations
    • SCAN Idx From: The count parameter, must be in the [0,255] range.
  • SCAN_COUNT16: Performs a read operation in non ranged mode using an 16-bit indexes (Qualifier code 8). Mandatory configuration parameters:

    • SCAN Group: The object group
    • SCAN Variation: The variation, or 0 for all variations
    • SCAN Idx From: The count parameter, must be in the [0,65536] range.
  • SCAN_INTEGRITY: Performs a read operation for all data in classes 0, 1, 2 and 3.

  • SCAN_EVENT_CLASSES: Performs a read operation for event data in classes 1, 2 and 3.

  • CMD_AOI32: Sends a command containing a 32 bit analog output block (Group 41 Variation 1). Mandatory configuration parameters:

    • CMD Index: The index of the data point.
    • CMD Selected: Determines whether to perform a select and operate request (true) or a direct operate request (false).
    • value.type: must be set to INTEGER.
  • CMD_AOI16: Sends a command containing a 16 bit analog output block (Group 41 Variation 2). Mandatory configuration parameters:

    • CMD Index: The index of the data point.
    • CMD Selected: Determines whether to perform a select and operate request (true) or a direct operate request (false).
    • value.type: must be set to INTEGER.
  • CMD_AOF32: Sends a command containing a 32 bit floating point output block (Group 41 Variation 3). Mandatory configuration parameters:

    • CMD Index: The index of the data point.
    • CMD Selected: Determines whether to perform a select and operate request (true) or a direct operate request (false).
    • value.type: must be set to FLOAT.
  • CMD_AOD64: Sends a command containing a 64 bit floating point output block (Group 41 Variation 4). Mandatory configuration parameters:

    • CMD Index: The index of the data point.
    • CMD Selected: Determines whether to perform a select and operate request (true) or a direct operate request (false).
    • value.type: must be set to DOUBLE.
  • CMD_CROB: Sends a command containing a control relay output block (Group 12 Variation 1). Mandatory configuration parameters:

    • CMD Index: The index of the data point.
    • CMD Selected: Determines whether to perform a select and operate request (true) or a direct operate request (false).
    • value.type: must be set to STRING.
  • LISTEN: Can be used to define a channel in listening mode, this value of the function parameter must be set if the listen flag is set to true. Channels of this type can be used for receiving data contained in unsolicited messages. Relevant configuration parameters:

    • SCAN Group: If specified, enables notification filtering basing on object group. In this case only the data points whose object group matches the specified group will be notified.
    • SCAN Variation: If specified, enables notification filtering basing on variation. In this case only the data points whose variation group matches the specified group will be notified.

Data representation

Data point value representation

The driver uses a JSON representation for returning the value of the data points received during requests or unsolicited messages. A data point object is represented as a JSON object with the following properties:

  • Index (number): reports the data point index.
  • Quality (number): reports the received quality value as an unsigned integer.
  • Timestamp (number): reports the timestamp in milliseconds since Unix Epoch.
  • Value (boolean, number or string): reports the received value. The value type depends on the DNP3 data point types
    • boolean: in case of a single binary value
    • string: in case of a double bit binary value, in this case the reported string will be one of the following:
      • INTERMEDIATE
      • DETERMINED_OFF
      • DETERMINED_ON
      • INDETERMINATE
    • number: for the other cases, if the value object represents a date, the value of this field will report the value in terms of milliseconds since Unix Epoch.

Example:

{
  "Index": 0,
  "Quality": 2,
  "Timestamp": 0,
  "Value": false
}

Sequence of data points representation

The contents of a sequence of data points relative to the same object header represented using a JSON object containing the following fields:

  • Type (string): A sting representing the data point type, possible values are:

  • AnalogInput

  • AnalogOutputStatus

  • BinaryInput

  • BinaryOutputStatus

  • Counter

  • DoubleBitBinaryInput

  • FrozenCounter

  • DNPTime

  • Group (integer): The object group of the received data points.

  • Variation (integer): The variation of the received data points.

  • Event (boolean): Reports if frame contains event data.

  • TimestampQuality (string): Reports the timestamp validity and synchronization status. Possible values are:

  • INVALID

  • SYNCHRONIZED

  • UNSYNCHRONIZED

  • Values (array): A JSON array reporting the data points.

Example:

{
  "Type":"BinaryInput",
  "Group":1,
  "Variation":2,
  "Event":false,
  "TimestampQuality":"INVALID",
  "Values":
  [
    {"Index":0,"Quality":2,"Timestamp":0,"Value":false},
    {"Index":1,"Quality":2,"Timestamp":0,"Value":false},
    {"Index":2,"Quality":2,"Timestamp":0,"Value":false},
    {"Index":3,"Quality":2,"Timestamp":0,"Value":false},
    {"Index":4,"Quality":2,"Timestamp":0,"Value":false},
    {"Index":5,"Quality":2,"Timestamp":0,"Value":false},
    {"Index":6,"Quality":2,"Timestamp":0,"Value":false},
    {"Index":7,"Quality":2,"Timestamp":0,"Value":false},
    {"Index":8,"Quality":2,"Timestamp":0,"Value":false},
    {"Index":9,"Quality":2,"Timestamp":0,"Value":false}
  ],
}

Response representation

A response to a driver read request will be represented as a JSON array containing a JSON object per object header:

Example:

[
  {
    "Type":"BinaryInput",
    "Group":1,
    "Variation":2,
    "Event":false,
    "TimestampQuality":"INVALID",
    "Values":
    [
      {"Index":0,"Quality":2,"Timestamp":0,"Value":false},
      {"Index":1,"Quality":2,"Timestamp":0,"Value":false},
      {"Index":2,"Quality":2,"Timestamp":0,"Value":false},
      {"Index":3,"Quality":2,"Timestamp":0,"Value":false},
      {"Index":4,"Quality":2,"Timestamp":0,"Value":false},
      {"Index":5,"Quality":2,"Timestamp":0,"Value":false},
      {"Index":6,"Quality":2,"Timestamp":0,"Value":false},
      {"Index":7,"Quality":2,"Timestamp":0,"Value":false},
      {"Index":8,"Quality":2,"Timestamp":0,"Value":false},
      {"Index":9,"Quality":2,"Timestamp":0,"Value":false}
    ],
  },
  {
    "Type":"DoubleBitBinaryInput",
    "Group":3,
    "Variation":2,
    "Event":false,
    "TimestampQuality":"INVALID",
    "Values":
    [
      {"Index":0,"Quality":2,"Timestamp":0,"Value":"INTERMEDIATE"},
      {"Index":1,"Quality":2,"Timestamp":0,"Value":"INTERMEDIATE"},
      {"Index":2,"Quality":2,"Timestamp":0,"Value":"INTERMEDIATE"},
      {"Index":3,"Quality":2,"Timestamp":0,"Value":"INTERMEDIATE"},
      {"Index":4,"Quality":2,"Timestamp":0,"Value":"INTERMEDIATE"},
      {"Index":5,"Quality":2,"Timestamp":0,"Value":"INTERMEDIATE"},
      {"Index":6,"Quality":2,"Timestamp":0,"Value":"INTERMEDIATE"},
      {"Index":7,"Quality":2,"Timestamp":0,"Value":"INTERMEDIATE"},
      {"Index":8,"Quality":2,"Timestamp":0,"Value":"INTERMEDIATE"},
      {"Index":9,"Quality":2,"Timestamp":0,"Value":"INTERMEDIATE"}
    ],
  }
]

Unsolicited responses representation

Unsolicited response can be retrieved using channels in listen mode.
The received data is provided as a JSON object having the same structure described in the Sequence of data points representation section.
Note that unlike the response representation presented above, the toplevel JSON element is an object instead of an array.

Example:

{
  "TimestampQuality": "INVALID",
  "Group": 2,
  "Variation": 1,
  "Event": true,
  "Values": [
    { "Index": 2, "Quality": 129, "Timestamp": 0, "Value": true}
  ]
}

Using the received values in Kura Wires

The Driver and Wires model support a limited set of data types, most of them are of numeric type and unstructured (arrays and complex data structures are not supported out of the box).
The DNP3 master driver uses the STRING type and the JSON format to overcome this limitation and represent the metadata associated with every data point value (index, quality, timestamp, type etc.).
This has the side effect of making the received data not directly consumable by wire components that expect simpler data types.
If the metadata is not required, it is possible to create a Javascript Filter component that can be attached to a DNP3 asset that converts the received data into simpler Wires values, reporting only the DNP3 data point value.
An example code for such script filter can be the following:

var processMessage = function (obj, record) {
  var type = obj.Type
  var dataPoints = obj.Values

  for (var i = 0; i<dataPoints.length; i++) {

    var dataPoint = dataPoints[i]
    var key = (type + '_' + dataPoint.Index).toString()
    var value = dataPoint.Value

    if (typeof value == 'number') {
      record[key] =  newDoubleValue(value)
    } else if (typeof value == 'string') {
      record[key] =  newStringValue(value)
    } else if (typeof value == 'boolean') {
      record[key] = newBooleanValue(value)
    }
  }
}

var processMessages = function (array, record) {
  for (var i = 0; i<array.length; i++) {
    var message = array[i]
    if (message instanceof Object) {
      processMessage(message, record)
    }
  }
}

var outputRecord = newWireRecord()

var inputRecords = input.records

for (var i = 0; i < inputRecords.length; i++) {
  var inputRecord = inputRecords[i]
  for (var key in inputRecord) {
    if (key == 'assetName') {
      continue
    }
    var value = inputRecord[key]
    if (value.getType() == STRING) {
      try {
        var obj = JSON.parse(value.getValue())
        if (Array.isArray(obj)) {
          processMessages(obj, outputRecord)
        } else {
          processMessage(obj, outputRecord)
        }
      } catch (err) {
        logger.warn('failed to process channel {}', key)
      }
    }
  }
}

output.add(outputRecord)

The attached script produces a single output wire record containing a property for each data point, the property key is the point Type from the Json representation concatenated with the point index by an underscore, the value will be the data point value as a Wires DOUBLE, BOOLEAN or STRING.
The script supports both channels in listen and read mode.

Creating a wire graph composed by the following components:

Timer -> DNP3Asset -> Script -> Logger

where

  • Timer is a Wires timer
  • DNP3Asset is an Asset attached to the DNP3Driver with a single channel that performs an integrity scan
  • Script is a ScriptFilter with the code above
  • Logger is a Logger in VERBOSE mode

produces an output similar the following one on the log (/var/log/kura.log):

INFO  o.e.k.i.w.l.Logger - Received WireEnvelope from org.eclipse.kura.wire.ScriptFilter-1579167946653-27
INFO  o.e.k.i.w.l.Logger - Record List content:
INFO  o.e.k.i.w.l.Logger -   Record content:
INFO  o.e.k.i.w.l.Logger -     Counter_2 : 0.0
INFO  o.e.k.i.w.l.Logger -     Counter_1 : 0.0
INFO  o.e.k.i.w.l.Logger -     Counter_0 : 0.0
INFO  o.e.k.i.w.l.Logger -     AnalogOutputStatus_0 : 0.0
INFO  o.e.k.i.w.l.Logger -     AnalogOutputStatus_2 : 0.0
INFO  o.e.k.i.w.l.Logger -     AnalogOutputStatus_1 : 0.0
INFO  o.e.k.i.w.l.Logger -     DoubleBitBinaryInput_2 : INTERMEDIATE
INFO  o.e.k.i.w.l.Logger -     DoubleBitBinaryInput_1 : INTERMEDIATE
INFO  o.e.k.i.w.l.Logger -     DoubleBitBinaryInput_0 : INTERMEDIATE
INFO  o.e.k.i.w.l.Logger -     FrozenCounter_0 : 0.0
INFO  o.e.k.i.w.l.Logger -     FrozenCounter_1 : 0.0
INFO  o.e.k.i.w.l.Logger -     FrozenCounter_2 : 0.0
INFO  o.e.k.i.w.l.Logger -     BinaryInput_0 : false
INFO  o.e.k.i.w.l.Logger -     BinaryInput_1 : false
INFO  o.e.k.i.w.l.Logger -     BinaryInput_2 : false
INFO  o.e.k.i.w.l.Logger -     BinaryOutputStatus_2 : false
INFO  o.e.k.i.w.l.Logger -     BinaryOutputStatus_1 : false
INFO  o.e.k.i.w.l.Logger -     BinaryOutputStatus_0 : false
INFO  o.e.k.i.w.l.Logger -     AnalogInput_0 : 0.0
INFO  o.e.k.i.w.l.Logger -     AnalogInput_1 : 0.0
INFO  o.e.k.i.w.l.Logger -     AnalogInput_2 : 0.0

In the example above the server contains 3 data points per mentioned type.

Command data representation

The values supplied for sending commands must be encoded as follows, depending on the value of the Function configuration parameter:

  • CMD_AOI32: an INTEGER value.

  • CMD_AOI16: an INTEGER value in the [-32768, 32767] range.

  • CMD_AOF32: a FLOAT value.

  • CMD_AOD64: a DOUBLE value.

  • CMD_CROB: a STRING value containing a JSON object representation of the CROB field, the JSON object must contain the following fields:

    • Count (number): the value of the count field.

    • OnTime (number): the value of the on time field.

    • OffTime (number): the value of the off time field.

    • ControlCode (string): the value of the control code field. The allowed values are the following:

      • NUL
      • NUL_CANCEL
      • PULSE_ON
      • PULSE_ON_CANCEL
      • PULSE_OFF
      • PULSE_OFF_CANCEL
      • LATCH_ON
      • LATCH_ON_CANCEL
      • LATCH_OFF
      • LATCH_OFF_CANCEL
      • CLOSE_PULSE_ON
      • CLOSE_PULSE_ON_CANCEL
      • TRIP_PULSE_ON
      • TRIP_PULSE_ON_CANCEL

      Example:

      {
        "Count": 1,
        "OnTime": 100,
        "OffTime": 100,
        "ControlCode":"CLOSE_PULSE_ON"
      }
      

Note about floating point values
The driver currently does not support denormal floating point values, it is recommended to avoid using such values if possible. Denormal floating point values received from the framework will be clamped to 0 by the driver.