fix(dcl): treat OPC UA BadResourceUnavailable as transient Bad; Bad+null keeps last-known value (ScadaBridge#112)
OtOpcUa's out-of-process script runner publishes 0x80040000 with a null value. - OpcUaDataConnection: logs entry/recovery once per tag, names the symbolic status in read errors; never a disconnect (not in the reconnect status set). - InstanceActor: a Bad update with no value is a quality-only transition (keep value, quality Bad, stream publish, no script/alarm/waiter re-eval), matching the connection-drop rule. Bad with a value is unchanged. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -1223,6 +1223,23 @@ public class InstanceActor : ReceiveActor
|
||||
traced = true;
|
||||
}
|
||||
|
||||
// Bad quality WITHOUT a value (ScadaBridge#112) is a quality-only transition: the
|
||||
// source is saying "I cannot give you a value right now", not "the value is now
|
||||
// null". OPC UA servers commonly omit the value on a Bad status — OtOpcUa always
|
||||
// does, e.g. BadResourceUnavailable (0x80040000) from its out-of-process script
|
||||
// runner — and the DCL's own tag-resolution failure pushes Bad+null too. Treat it
|
||||
// exactly like a connection drop (HandleConnectionQualityChanged, DCL design
|
||||
// "Connection drop detection"): keep the last-known value, flip quality to Bad,
|
||||
// publish to the stream, and do NOT re-notify scripts/alarms or evaluate waiters —
|
||||
// nulling the stored value would fire value-change scripts with a null and make a
|
||||
// computed alarm re-evaluate on a value that never existed. A Bad update that DOES
|
||||
// carry a value still takes the normal path below.
|
||||
if (update.Quality == QualityCode.Bad && update.Value is null)
|
||||
{
|
||||
ApplyQualityOnlyBad(attrName, update.TagPath, update.Timestamp, traceSource, traced);
|
||||
continue;
|
||||
}
|
||||
|
||||
// A List-typed attribute coerces the incoming OPC UA array
|
||||
// (a CLR array/IEnumerable from the SDK) into a typed List<T>. On an
|
||||
// element-type mismatch we set the attribute's quality to Bad, log a
|
||||
@@ -1271,6 +1288,22 @@ public class InstanceActor : ReceiveActor
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks a data-sourced attribute Bad while keeping its last-known value (ScadaBridge#112):
|
||||
/// quality + timestamp move, the stored value does not, and only the site stream / debug
|
||||
/// view is told — mirroring <see cref="HandleConnectionQualityChanged"/>.
|
||||
/// </summary>
|
||||
private void ApplyQualityOnlyBad(
|
||||
string attrName, string tagPath, DateTimeOffset timestamp, Guid? traceSource, bool traced)
|
||||
{
|
||||
_attributeQualities[attrName] = "Bad";
|
||||
_attributeTimestamps[attrName] = timestamp;
|
||||
_attributes.TryGetValue(attrName, out var currentValue);
|
||||
_streamManager?.PublishAttributeValueChanged(new AttributeValueChanged(
|
||||
_instanceUniqueName, tagPath, attrName, currentValue, "Bad", timestamp,
|
||||
SourceExecutionId: traceSource, Trace: traced, SourceExecutionInferred: traced));
|
||||
}
|
||||
|
||||
/// <summary>Parses a <see cref="ResolvedAttribute.DataType"/> string, defaulting to
|
||||
/// <see cref="DataType.String"/> on an unrecognized value (never throws).</summary>
|
||||
private static DataType ParseDataType(string dataType) =>
|
||||
|
||||
Reference in New Issue
Block a user