A real-time SCADA reporting and alarm intelligence system built in Ignition Perspective, combining a frontend operator HMI with a backend Gateway Timer Script for continuous, session-independent alarm evaluation
This project implements a small but complete SCADA reporting solution in Ignition Maker Edition. It pairs an operator-facing Perspective view with a Gateway-scoped Timer Script ("Scientist 7 Alarm Intelligence") that continuously evaluates tank conditions and writes the most recent alarm into a memory tag for the UI to consume.
The architecture deliberately splits responsibility across two execution scopes: the Perspective view handles operator interaction and report rendering, while the Gateway Timer Script handles continuous tag evaluation and alarm classification. This is the same scope-separation pattern used in real production Ignition deployments.
Operators viewing raw tag values rarely get the context they need to make decisions quickly. A live tank level of "95" is meaningless without knowing whether that crosses an alarm threshold, whether the pump is running, and what the most recent alarm event was.
The engineering challenge was therefore:
Level: 95.0 Temp: 70.0 Pump: trueTankLevel and TankTemp via a batched system.tag.readBlocking() callPumpRunning and uses it as a guard — alarms are only evaluated when the pump is actually runninglevel > 80 → "High Tank Level", temp > 90 → "High Temperature"" | " as a separatorLastAlarmEvent value and only writes when it has changed, avoiding redundant tag writes every cycleChoosing the Gateway scope for alarm evaluation was deliberate. A Perspective Session script would only run while a client view is open, which means alarms would silently stop being evaluated as soon as the operator closed the page. Putting the logic in a Gateway Timer Script means the evaluation runs on the server every 2 seconds for as long as the Gateway is up, regardless of who is connected.
The Tag Browser confirms the live state at the moment of capture:
LastAlarmEvent = "High Tank Level", PumpRunning = true,
TankLevel = 95, TankTemp = 70. The alarm fired
correctly because the pump was running and the level exceeded the threshold of 80.
# Scientist 7 Alarm Intelligence
# Gateway Timer Script - Fixed Delay 2,000 ms, Shared threading
# Reads tank state, evaluates alarms, persists most recent event
tag_paths = [
"[default]TankLevel",
"[default]TankTemp"
]
values = system.tag.readBlocking(tag_paths)
level = values[0].value or 0
temp = values[1].value or 0
pump = system.tag.readBlocking(
["[default]PumpRunning"]
)[0].value or False
alarm_triggered = False
messages = []
# Only evaluate alarms if pump is running
if pump:
if level > 80:
alarm_triggered = True
messages.append("High Tank Level")
if temp > 90:
alarm_triggered = True
messages.append("High Temperature")
if alarm_triggered:
combined = " | ".join(messages)
last = system.tag.readBlocking(
["[default]LastAlarmEvent"]
)[0].value
# Only write if state has actually changed
if combined != last:
system.tag.writeBlocking(
["[default]LastAlarmEvent"],
[combined]
)
A few choices in this script reflect deliberate SCADA design thinking rather than just "make it work":
PumpRunning is true. This prevents nuisance alarms during
planned shutdowns — a tank sitting at 95% when the pump is off is
expected, not an emergency.
TankLevel and
TankTemp are read in a single readBlocking() call
rather than two separate calls, reducing round-trip overhead on every cycle.
combined != last). The
script only writes to LastAlarmEvent when the alarm state has
actually changed. Without this check, the Gateway would write the same value
every 2 seconds forever, polluting the historian and creating unnecessary
tag-change events for any subscribers.
" | " rather than overwriting one another.
The operator sees the full picture, not just the last one evaluated.
or 0, or False).
If a tag returns a null value during startup or a brief disconnect, the
script falls back to safe defaults rather than throwing and halting the timer.
I tested the system by toggling the pump and dragging the sliders to drive
TankLevel and TankTemp across their thresholds, then
watching the Tag Browser to confirm LastAlarmEvent updated as expected.
LastAlarmEvent updated to "High Tank Level"The Perspective view's "Generate Scientist Report" button rendered the formatted output to the Label, reading back the same tag values that the Gateway Timer Script was evaluating. This confirmed the full loop: operator input → tag system → Gateway evaluation → UI render.
The most important conceptual challenge was deciding where the alarm logic belonged. My first instinct was to put it on the Perspective button click event, since that's where the report gets generated. I had to step back and recognize that alarm evaluation cannot live in the client scope — otherwise alarms would only be detected when an operator happened to click the button. Moving it into a Gateway Timer Script was the correct architectural fix.
The second challenge was avoiding redundant writes. Without the
combined != last check, the script was writing the same alarm
string into LastAlarmEvent on every 2-second cycle, even when
nothing had changed. Adding the comparison made the writes meaningful and kept
the tag history clean.
system.tag.readBlocking() and system.tag.writeBlocking() for batched, reliable tag I/O