How to integrate PLC alarm array with Ignition native alarming?

Hi all,

I’m receiving alarms from a PLC as an array of around 580 Json structures over single OPC UA Tag.

Each array element contains fields like:

AlarmCode
Severity
TimestampActive
TimestampReset
Active
Source
Acknowledged
Priority

My requirement is to use Ignition’s native:

  • Alarm Status Table
  • Alarm Journal
  • Acknowledgement
  • Active/Clear history

The challenge is that the alarm array may be dynamic, so an alarm may not always stay at the same array index and might not be fixed number of structure inside the array .

For example:

Array[0] = AlarmCode 1001
Array[1] = AlarmCode 2001

Later, after 1001 clears:

Array[0] = AlarmCode 2001

So I don’t think configuring an Ignition alarm directly on Array[x].Active is safe.

This should be efficient in terms of performance and latency as there are 100+ assets carrying same array

**What would be the recommended design here?
**

Assuming all the alarms are predefined? Build them out in Ignition using a UDT for each. Then write a script that monitors the array tag from the PLC and makes the correct updates to the Alarm UDTs.

But better - fix the PLC so you don't have the problem of them moving.

Thanks!!:slightly_smiling_face:

All alarms are predefined, but the PLC array currently contains only active alarms and the indexes can change.

In that case, would you recommend creating one UDT instance per predefined alarm and using AlarmCode as the key in a Gateway script to update the corresponding UDT?

Would you monitor the whole array using a Tag Change Script, or periodically process it using a Gateway Timer Script?

And having 120+ machines how is it efficent to monitor a 580-structure array for each machine ?

Yes. One instance per alarm. And then yeah, whatever is the defining bit in the structure that is going to tell you which UDT instance to update..

But I think the more fun part - what happens when the alarm clears? Does it just cease to exist in the structure? or does a "Cleared" bit get set for a period of time? If it just disappears, you're going to have to have a way to track (maybe scanning all your UDT instances) to see what alarms are Active on the Ignition side that are no longer present in the PLC's struct.

Is the plc's structure front loaded? so you can loop thru it until you hit the first null? or are the alarms going to be randomly dispersed, so you have to scan the entire thing every time.

One thought on how I might try to handle all of it...
Have an On-Change script on each struct. (that way you're only looking at it when it actually changes, which probably isn't every second). Have that script parse the struct and pull out the important bits and write them into a new array that is condensed and setup in a way that makes it easy for an expression on each UDT Instance to watch it and detect if that instance's alarm is in the array and react accordingly.

You don't even necessarily need UDT instaces. A simple boolean expression tag could do the job. And would be relatively easy to script the creation of.

What determines that the alarm is active and organizes the array? Why not just configure alarms on the base alarm tag from the PLC and let Ignition handle the rest. I don't really see a reason that Ignition would need to read the array when the base alarm tags probably already exist.

It's not uncommon to run into migration projects where they do not want to edit the PLC program for the sake of recommissioning alarms & shutdowns so asking for changes is sometimes not an option. But I would dig through the PLC program first to see the mapping routine for the ring buffer as you likely can read the alarms directly as intermediary tags. That's the better solution I would recommend doing instead.

But if you do want to make this work as is - This topic was a fun concept for trialing some AI dev tools, attached is it's code solution. (untested but reading through it seems logical that it should work)

  1. UDT for your alarms:
    Parameter: AlarmCode
    Parameter: Description
    Expression: Code set to {AlarmCode}
    Memory (bool): Active
    Memory: Severity (match datatype to PLC for these 3)
    Memory: Priority
    Memory: Source
    Memory (DateTime): PlcTsActive
    Memory (Bool): PlcAcked
    Memory (DateTime): ResetTime

  2. Create your tags: Alarm1001 through Alarm9999

  3. Create the attached script as 'alarmBuffer' in the script library

  4. Browse the device connection and update the buffer_tag to point at at. Update TAG_ROOT to point at the directory for your alarm tags.

  5. Use the Script Console code first as a test bench, this will do validation checks on your pathing and device names as well as provide you with read times so you have an idea of how long it takes to execute

  6. When you actually want to run it then just create a scheduled task with the scheduled task code

  7. The code has a setting for map_refresh_ms, if it's 0 it will scan your UDT tags once and never again, if it's set to a time then it'll rebuild the map that often. For dev I'd create the first dozen alarm tags and test but once it's done and everything is built set it to 0 as it doesnt need to rebuild the index.

Should work but there's a bit of missing info. Like does an empty alarm come back as "", {}, null, or 0. If you do want to try it for fun, run the script console code first and reply with the data type, one raw element structure and I'll correct it to work.

Script console test code:

lib = alarmBuffer
qv = system.tag.readBlocking([lib.BUFFER_TAG])[0]
v = qv.value

# --- 1. What came in -----------------------------------------------------
print "Quality:", qv.quality
print "Python type:", type(v)
if v is not None and not isinstance(v, basestring):
    items = list(v)
    print "Element count:", len(items)
    print "Element type:", type(items[0]) if items else None
    print "\nFirst element raw:\n", unicode(items[0])[:500] if items else None
    print "\nLast element raw (shows what an empty slot looks like):\n", unicode(items[-1])[:300] if items else None
else:
    s = unicode(v)
    print "String length:", len(s)
    print "\nStart:\n", s[:500]
    print "\nEnd (check it isn't truncated mid-record):\n", s[-200:]

# --- 2. Parse as scan() would --------------------------------------------
current = lib._parse(v)
print "\nActive alarms parsed:", len(current)
for code, rec in current.items():
    print code, rec

alarmBuffer.py (7.5 KB)