I can understand DNP3 writes may be tricky since there's not one way to simply write a value to a tag. The directOperateBinary looks like it will solve that problem, but introduces another problem for deriving the device name and index rather than accepting tagPath.
Yeah, you'll have to figure that out. There's zero correlation between a tag path and whatever the point index might be (unless you create one by convention).
Direct- or Select-And- Operate in DNP3 is a complex operation, it's not simply a write. There's some fidelity loss when you map it to something conceptually simpler like the OPC UA write service. The operations require many parameters and there's no way to provide those via the write service of another protocol, which is why there are some defaults to make it work.
The proper way to map DNP3 operations to OPC UA would be via OPC UA methods, and that's what we would have done if we were a more generic/standalone server, but since the primary consumer is Ignition itself we provide the scripting functions instead, which are basically the same thing but easier to use than OPC UA methods.
@msteele Hi, I'm having the exact same issue with BinaryOutput, AnalogueOutput DNP3 tags having Bad("Bad_OutOfService: The source of the data is not operational.") quality. And I'm also reading them from a SEL RTAC using the new DNP3 driver.
Just wondering what did you do at the end to get them working please?
In our tag processor, we have two line items for each tag.
To make the data readable and get rid of the error you mention, we map the .status of BOs in our Ignition_DNP tag map.
E.g. destination tag Ignition_DNP.BO_TripBreaker_OurBreaker as datatype SPS
to source expression OurRelay_SEL.OurBreaker_52a_closed as datatype SPS.
We also have to map the trip signal from Ignition to the appropriate trip signal for a breaker or control.
E.g. destination tag OurRelay_SEL.RB1_TripBreaker_OurBreaker.operPulse as datatype OPERSPC to source expression Ignition_DNP.BO_TripBreaker_OurBreaker.operTrip as datatype OPERSPC.
We finally got around to testing system.dnp.directOperateBinary() to a SEL RTAC's DNPC 61131 data structure. Our interpretation of DNP3 is that there are 5 function codes (Trip, Close, Pulse, Latch On, Latch Off) so the point of this exercise was to map out which combination of tcc and opType produce these five function codes over the wire.
Observations:
*As expected, tcc = 0, opType = 0 gives an error since there's no applicable function code.
†tcc = 0, opType = 2 gives an Ignition error, which makes sense because PULSE_OFF is, per my understanding, not an actual DNP function code.
‡Where tcc != 0 NUL, tcc overrides opType.
Additional arguments used in our test: count = 1, onTime = 1, offTime = 1. Our understanding is the time arguments should have no effect on RTAC response.
As mentioned upthread, if you want to poll a tag value via your BO, you need to map something to the .status SPS object in your RTAC. This object is a sibling to the five .oper* objects representing the DNP3 function codes.
Some helper functions for DNP3 BO and AO writes. getDNP3Index presumes your addressing uses the standard syntax, e.g. BinaryOutput0
# DNP3 Control + conversion of tagPath to inputs needed for system.dnp.* functions
# Compile the patterns once, because "dramatically faster"
devNamePattern = re.compile(r'\[(.*?)\]')
devIndexPattern = re.compile(r'(\d+)$')
def getDnp3Device(tagPath):
"""Extracts the device name for the given tag path."""
# Since opcItemPath is a tag property, no timeout is needed.
opcPath = system.tag.readBlocking(tagPath + '.OpcItemPath')[0].value
match = devNamePattern.search(opcPath)
if match:
return match.group(1)
return None
#
def getDnp3Index(tagPath):
"""Extracts the DNP3 index from the """
opcPath = system.tag.readBlocking(tagPath + '.OpcItemPath')[0].value
match = devIndexPattern.search(opcPath)
if match:
return match.group(1)
return None
#
# Map integers representing DNP3 BO commands to tcc, opType
funcCodesMap = {
0: (2, 0), # Trip
1: (1, 0), # Close
2: (0, 1), # Pulse
3: (0, 2), # Latch On
4: (0, 3), # Latch Off
}
# Binary operate.
def doWriteDnp3Bo(tagPath, dnp3FuncCode):
"""Wrapper for `system.dnp.directOperateBinary` func call for new DNP3 devices.
dnp3FuncCodes:
0=Trip
1=Close
2=Pulse
3=Latch On
4=Latch Off
"""
deviceName = control.getDnp3Device(tagPath)
index = control.getDnp3Index(tagPath)
tcc, opType = funcCodesMap[dnp3FuncCode]
# system.dnp.directOperateBinary(deviceName, index, tcc, opType, count, onTime, offTime)
system.dnp.directOperateBinary(deviceName, index, tcc, opType, 1, 1, 1)
#
# Analog operate. Default variation is float.
def doWriteDnp3Ao(tagPath, value, variation=3):
"""Wrapper for `system.dnp.directOperateAnalog` func call for new DNP3 devices. Default variation is float."""
deviceName = control.getDnp3Device(tagPath)
index = control.getDnp3Index(tagPath)
system.dnp.directOperateAnalog(deviceName, variation, index, value)
#