Get the first changed tag from a folder filled with UDTs

I have a folder with n UDTs representing Alarms.

AlarmFolder
├ Alarm1
│  ├ ID
│  └ Active
├ Alarm2
│  ├ ID
│  └ Active
└ Alarm3
   ├ ID
   └ Active

Alarms can get activated at the same time, in random orders without any specific order.

Want to do something like this pseudo code:

for alarm in AlarmfFolder:
    if(alarm.Active):
        if(first_executed_of_folder):
            return alarm.ID

I have an idea but lookinf for ideas or feedback on how to it correctly.

Thanks!

could you not use an alarm journal to get the alarm history and return the alarm with the oldest timestamp for your range?

If you are wanting to look at active alarms you should be able to just look at the active timestamp to show when it went high and return off of that.

1 Like

I can't sadly. We had to create the UDT specially for our case. That's why I'm looking for a way of searching in it.

All UDTs are created for a specific use case..but can you maybe explain what it is you are trying to accomplish?

if you are wanting to script it out you could use system.alarm.queryStatus and parse through the info to compare the timestamps etc.

Edit:
After reading your questions over again the UDT structure is tags called active and ID correct?

You could browse and read the timestamps from the QualifiedValues and then do a compare.
Something like this:

alarms = system.tag.browse("[default]myInstance/Alarms")
alarmTagPaths = []
for a in alarms:
	alarmTagPaths.append("%s/Active" % a["fullPath"])

alarmValues = system.tag.readBlocking(alarmTagPaths)
for av in alarmValues:
	if av.value:
		##Do something with the timestamp: av.timestamp
2 Likes

This sounds like a 'first-out' indication. If so, there's a few ways to handle this... though, ideally, it's handled within the logic controller.

Edit: Decided to take a stab at a first-out setup...
In this example, I created 2 tags within their own folders, "Folder1/Tag1", and "Folder2/Tag2". Both are memory tags, boolean, configured to alarm when value == 1. I've manually entered a label on each of the alarms (though, one could certainly bind the label to a tag in the same folder via [.]ID).

I've also created 3 additional tags, within the parent folder to the above alarm tags:

  1. Name="sFirst_Out_Enum", Memory Tag, Integer. "s" prefix indicates that tag is written to by a script (my standard), additional insight in Meta Data (Documentation & Tooltip) indicating origin script.
  2. Name="binEnc_Alarms", Expression Tag, Integer w/ Expression (event driven).
  • Expression:
binEnc(
	// Create a binary encoded integer, where each bit represents
	// an alarm that is active and unacknowledged. Update to suit...
	{[.]Folder1/Tag1/Alarms/Alarm.IsActive} && !{[.]Folder1/Tag1/Alarms/Alarm.IsAcked},
	{[.]Folder2/Tag2/Alarms/Alarm.IsActive} && !{[.]Folder2/Tag2/Alarms/Alarm.IsAcked}
	// Add others as necessary...
)
  • This tag also with a script transform (search the forums for tag event scripts, then use your better judgement when deciding to add features. Bottom line: ensure entire script is very fast to execute). Tag Value Changed script which writes the "First-Out Enumeration" to aforementioned memory tag:
def valueChanged(tag, tagPath, previousValue, currentValue, initialChange, missedEvents):
	fo_path = "[.]sFirst_Out_Enum" # "s" prefix indicates 'value is written by script'.
	# If no alarms are active, output "All Clear" enumeration:
	if currentValue.value == 0:
		fo_enum = 0
		system.tag.writeBlocking([fo_path], [fo_enum])
	# If alarm transitions from 0 --> >0, capture and output first-out:
	elif previousValue.value == 0 and currentValue.value > 0:
		bin_alarm_string = format(currentValue.value, 'b') # Integer --> Binary e.g. 10 --> "1010"
		fo_enum = bin_alarm_string[::-1].find("1") + 1 # Reverse string, then return character index of first "1".
		system.tag.writeBlocking([fo_path], [fo_enum])
	# Previous value was NOT "All Clear", do nothing:
	else:
		pass
  1. Name="First_Out_Text", Expression Tag, String. Expression:
case({[.]sFirst_Out_Enum},
	0, "All Clear",
	// Add alarm labels in same order as binEnc_Alarms tag:
	1, {[.]Folder1/Tag1/Alarms/Alarm.Label},
	2, {[.]Folder2/Tag2/Alarms/Alarm.Label},
	// Add others as necessary...
	"Error" // Default value
)

This method should output a first-out enumeration & lookup text for each alarm that is configured. The only tags that need updated when alarms are added are the "binEnc_Alarms" expression and "First_Out_Text" expressions.
Not the most "hands-off" approach, but all logic is contained in a single folder. Can be useful for consistently named tags.

1 Like

This here:

I think I'm going to use this solution.
Is not the fastest and also not the best, but we have to start to test something so i'm going to use this for now.

Thank you!

Wow! I didn't even thought about this.

I'm going to write something similiar as Benjamin concept (because we have to get it running) and I promise I'll get back to it later on time.

However, a quick note. In the version I am (8.1.38) you can't use indirect bindings in tag change scripts.

I wasn't able to make it work in mine.

Another note, we have like 200-500 alarms per machine. Making a case that long or making the binEnc() that long won't be the best choice, right?

If you mean relative paths (denoted by [.]), you certainly can use these within a value change script!

Eww... definitely do not use my method for that many alarms! If you have any more than a handful, likely within a single UDT, then mine is probably not a good candidate. Was a fun exercise, nonetheless.

That said, I want to reiterate the preference for this logic to reside in a logic controller. If an "Emergency Shutdown" alarm will trigger a hundred other alarms, the likelihood of Ignition being able to capture 'what happened first' is very small.

2 Likes

Reliable "First Out" detection can only be done in the PLC, and only if the logic is carefully constructed in the proper order, so "First Out" is captured in a single logic scan.

Polling for alarm status does not preserve order of events.

Yeah my bad. You can use them.

I was using system.tag.query() and doesn't allow relative paths.

INFO   | jvm 1    | 2025/02/11 18:31:36 | java.util.concurrent.ExecutionException: java.lang.Exception: provider not found: .

Using [.].. here to get parent folder ↑