Quality Bad_OutOfService on DNP3 BO reads (newer driver)

Anyone familiar with this quality code for the newer DNP3 driver? Bad("Bad_OutOfService: The source of the data is not operational.")

I'm seeing it on all the binary outs I'm trying to read from a SEL RTAC and have no idea why - but there's nothing about this code in the driver doc.

It means the flags are indicating the point is Offline (Online bit in flags not set).

Ah, was it Bad_Offline with the legacy driver? I was probably overthinking it but there was just a different name.

Might have just been a generic "Bad" in the legacy driver.

Is there not a way to sent trip bits and close bits on the same device on this driver?

We're not understanding the relationship between BO device settings Trip Close Code and Op Type for two reasons:

  1. These settings are at a device level, implying we cannot use different bits on the same tag or on different tags on the same device?
  2. LatchOn and LatchOff are still pulses under the hood, so not understanding what Latch mode would actually do.

We are wanting to have the flexibility to pulse any of the following bits, and per tag:

  • Trip
  • Close
  • LatchOn
  • LatchOff
  • Pulse

If you need different per point settings you need to use the scripting functions instead.

edit: take care with the namespaces on these functions, system.dnp3 is used by the legacy driver, system.dnp by the new one.

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.

1 Like

@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?

Below is the RTAC BO tags configuration

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:

  1. *As expected, tcc = 0, opType = 0 gives an error since there's no applicable function code.
  2. †tcc = 0, opType = 2 gives an Ignition error, which makes sense because PULSE_OFF is, per my understanding, not an actual DNP function code.
  3. ‡Where tcc != 0 NUL, tcc overrides opType.
  4. 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.
  5. 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.

Results:

Trip Close Code tcc Operation Type opType RTAC Bit Received DNP Function Code
0=NUL 0=NUL *Ignition error
0=NUL 1=PULSE_ON .operPulse Pulse
0=NUL 2=PULSE_OFF †Ignition error
0=NUL 3=LATCH_ON .operLatchOn Latch On
0=NUL 4=LATCH_OFF .operLatchOff Latch Off
1=CLOSE 0=NUL .operClose Close
1=CLOSE 1=PULSE_ON ‡.operClose Close
1=CLOSE 2=PULSE_OFF ‡.operClose Close
1=CLOSE 3=LATCH_ON ‡.operClose Close
1=CLOSE 4=LATCH_OFF ‡.operClose Close
2=TRIP 0=NUL .operTrip Trip
2=TRIP 1=PULSE_ON ‡.operTrip Trip
2=TRIP 2=PULSE_OFF ‡.operTrip Trip
2=TRIP 3=LATCH_ON ‡.operTrip Trip
2=TRIP 4=LATCH_OFF ‡.operTrip Trip
3 Likes

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