Bad Quality Isn't Zero: Handling SCADA Tag Quality Without Lying to the Operator
OPC DA quality words, OPC UA StatusCodes, DNP3 flags — and how to display, alarm and historize them so a dead signal never reads as a valid value.
A flow meter dropped off a Modbus link during a night shift. The gateway timed out, the driver wrote 0 into the register, and the HMI showed 0.0 m³/h. The operator read that as "pump isn't moving product," opened a bypass, and spilled into a sump. The transmitter was fine the whole time — the number on the screen was a comms failure dressed up as a process value.
That is the entire problem with tag quality in one incident. The value and the quality are two separate signals, and the moment you throw the quality away you are asking the operator to trust a number that the system itself does not trust.
Quality lives in the protocol, not just in your head
Every serious SCADA protocol carries a quality field alongside the value. If you know where it lives, you stop guessing.
OPC DA packs quality into a 16-bit word, but only the top byte matters in practice. Mask with 0xC0: 0xC0 is Good, 0x40 is Uncertain, 0x00 is Bad. The substatus nibble tells you why — 0x18 is a comms failure, 0x0C device failure, 0x1C a last-known-value. So 0xD8 isn't a random number, it's Good with a "local override" hint. A lot of integrations look at the value and never mask the quality word at all.
OPC UA replaced that with a 32-bit StatusCode (IEC 62541-4, Part 8 for Data Access). The top two bits are the severity: 0x00000000 Good, 0x40000000 Uncertain, 0x80000000 Bad. You rarely need the whole table — code & 0xC0000000 tells you which bucket you're in. The named codes are worth knowing because they show up in logs: Bad_NoCommunication (0x80310000), Bad_NotConnected (0x808A0000), Bad_OutOfService (0x808D0000), Uncertain_LastUsableValue (0x40900000), Bad_SensorFailure (0x80510000). There are also limit bits (bits 8–9) that say the value is pegged at a range limit — useful for a saturated analog that is technically "good" but stuck.
DNP3 (IEEE 1815) doesn't send a quality code, it sends a flags octet per point: ONLINE (bit 0), RESTART, COMM_LOST, REMOTE_FORCED, LOCAL_FORCED, and for analogs OVER_RANGE and REFERENCE_ERR. ONLINE = 1 is your "good." If COMM_LOST is set, the outstation is telling you this point is stale — read it and mark the tag, don't ignore it.
Modbus carries nothing. A holding register is 16 bits of value and that's it. Quality on a Modbus tag is entirely synthesized by the driver from timeouts and exception responses — which is exactly why the flow-meter story above happens on Modbus and not on DNP3. If you run Modbus, the driver's timeout and "on-failure" behavior is your quality model, so go look at what it's configured to do.
The states operators actually need to tell apart
Platforms name these differently, but collapsing them all into one gray color is the real mistake. A disconnected transmitter, a point you deliberately bypassed, and a calculation missing an input all need different operator responses.
| State | Where it comes from | What the screen should do |
|---|---|---|
| Good | Fresh value, expected source | Display normally |
| Bad comms | Driver / device / route down | Bad-quality mark, suspend process-alarm logic |
| Stale | Last value older than the max age | Show last value + age, mark it old |
| Uncertain | Connected but value questionable | Warning styling + reason |
| Substituted / forced | Manual, simulation, or LOCAL_FORCED | Show clearly that it's not the field value |
| Out of service | Intentionally removed | Suppress nuisance alarms only if approved and logged |
| Calc bad | An input or the calc failed | Show as unavailable, never as 0 |
Never map bad quality to a value
Mapping bad quality to 0, false, or "" makes the screen look clean and quietly deletes the diagnostic. The failures are always the same shape:
- A dead flow meter reads zero flow and drives the wrong response (the story above).
- A valve with lost feedback looks closed.
- A lost temperature tag trips a false low alarm.
- A derived "running" tag flips because one input stopped updating.
- A historian trend draws a clean step to zero instead of a gap.
If the downstream system genuinely can't carry quality — a flat Modbus map to a third-party MES is the usual case — add an explicit companion tag: P101_FLOW_Q next to P101_FLOW. It's ugly. It's still safer than shipping a number you know is wrong.
HMI: keep the number, flag the trust
Keep the last value on screen — operators need something to reason about — but make it obvious the value isn't fit for a control decision. Ignition draws a diagonal-hatch overlay on any component whose tag quality isn't Good; other platforms use a border or an X. Whatever you use, don't reach for a color that collides with your alarm palette.
A pump faceplate showing Stopped when the run feedback is bad quality is lying twice. It should read Run feedback unavailable and still show whether the command output is energized — because "I told it to run and can't confirm" is a completely different situation from "it's stopped." For any critical analog, put the age of the last update on the faceplate; a value that's 40 minutes old should say so.
Alarms: don't judge a limit on data you don't trust
One global rule for bad quality causes either alarm floods or hidden risk. Split it by alarm class.
Analog process alarms: suppress high/low limit evaluation while input quality is bad. You cannot honestly say a temperature is "HIGH" from a value the driver flagged Bad_NoCommunication. Raise a separate instrument/data-quality alarm if losing that point matters, log the moment evaluation was suspended, and re-arm gently on recovery so you don't dump a burst of stale transitions the instant comms return.
Discrete alarms and interlocks: never read unknown feedback as the safe state. A permissive built on a bad-quality input should fail conservative and show the reason. And roll comms failures up — PLC rack 2 comms lost is one actionable alarm; three hundred individual tag bad alarms from the same dead switch are noise that buries it.
Historian and reports
If the historian stores value without status, a comms outage becomes a smooth interpolated line through the gap, and six months later someone builds a compliance report on top of it. Store quality with every sample, and make the trend render bad-quality spans as gaps or shaded regions, not connected line. For totals and averages, decide up front whether bad-quality periods are excluded, estimated, or reported separately — never silently averaged as real production.
Calculated tags are where quality quietly dies, because the calc returns one number and the status falls off. Make the rule explicit: a tank-mass calc with a bad level or density reads calculation unavailable, not 0 kg. A line "Available" tag with a missing MCC status reads bad-with-reason, not false. If the platform propagates quality, use it; if not, carry a status tag next to every derived value.
The five-minute test that pays for itself
Before handover, do this with real devices, not simulation tags:
- Pull one PLC comms path and watch the tag quality flip on the HMI.
- Confirm you get a data-quality alarm, not a wall of false process alarms.
- Check the trend shows the outage as a gap or bad segment.
- Restore comms and verify timestamps, quality, and alarm states come back clean — no stale-transition burst.
- Force a substituted value and confirm it's visibly marked on screens and reports.
- Break one input to a calculated tag and confirm the result doesn't read normal.
- Read the event log afterward — is there enough there to explain the outage next week?
This test is cheap and it routinely exposes the real bugs: a driver timeout set too long, a scan class that never re-reads, a "map failure to zero" left in from bench testing. Better to find those on a commissioning afternoon than the night an operator opens a bypass on a value that was never real.