Files
lmxopcua/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect/MTConnectConditionAlarmMapper.cs
T
Joseph Doherty 826312befe docs: fill missing XML docs and strip tracking IDs from src comments
Clears CommentChecker findings in src/ (3,280 -> 209, the remainder being false
positives like PLC-5/IEEE-754/M0.0) by adding missing summaries/params/returns,
normalising inheritdoc usage, and removing Gitea/review tracking IDs while keeping
the rationale they annotated. Comment-only: comment-stripped sources are identical
to the parent commit.
2026-10-15 06:14:58 -04:00

214 lines
11 KiB
C#

using System.Text;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect;
/// <summary>
/// What the server was last TOLD about one condition: the last alarm statement it was given
/// (active or Normal — never Unavailable) and whether its Quality is currently annotated Bad.
/// </summary>
/// <param name="Statement">The last statement delivered; <c>null</c> when only an Unavailable was ever seen.</param>
/// <param name="QualityBad">Whether the condition's Quality was last annotated <c>BadNoCommunication</c>.</param>
internal sealed record MTConnectToldCondition(MTConnectConditionState? Statement, bool QualityBad);
/// <summary>
/// The MTConnect CONDITION → OPC UA Part 9 mapping, as one pure function: given what the server was
/// last told about a condition and what the Agent says now, the single <see cref="AlarmEventArgs"/> to
/// raise — or none — and what the server has then been told.
/// </summary>
/// <remarks>
/// <list type="table">
/// <listheader><term>Agent level</term><description>Part 9</description></listheader>
/// <item><term>Normal</term><description>inactive (return to normal), quality Good</description></item>
/// <item><term>Warning</term><description>active, severity 500 (<see cref="AlarmSeverity.Medium"/>)</description></item>
/// <item><term>Fault</term><description>active, severity 800 (<see cref="AlarmSeverity.High"/> bucket)</description></item>
/// <item><term>Unavailable</term><description>state UNCHANGED, quality <c>BadNoCommunication</c></description></item>
/// </list>
/// <para>
/// <b>Unavailable is comms loss one hop further out, not a return to normal.</b> A cppagent marks every
/// DataItem of an adapter UNAVAILABLE when the adapter socket drops, so it is treated exactly like the
/// host's own transport loss: an active condition STAYS active (and keeps its ack), only its
/// Quality goes Bad, as a quality-only annotation (<see cref="AlarmEventArgs.IsQualityOnly"/>, no
/// <c>/alerts</c> row). When data returns: the same statement ⇒ quality-only Good; Normal ⇒ Clear; a
/// different active statement ⇒ Retrigger.
/// </para>
/// <para>
/// Otherwise: inactive → active is a <see cref="AlarmTransitionKind.Raise"/>; active → a DIFFERENT active
/// statement (Warning → Fault, a new winning <c>nativeCode</c>) is a
/// <see cref="AlarmTransitionKind.Retrigger"/> — active and unacknowledged again with the new severity
/// and message (so clearing the worst of several codes re-raises the next one down, un-acked, by
/// design: it is a statement the operator has not seen); active → Normal is a
/// <see cref="AlarmTransitionKind.Clear"/>. An unchanged statement
/// (<see cref="MTConnectConditionState.SameAlarmAs"/> — text excluded) emits nothing, which keeps a
/// reconnect's replayed <c>/current</c> from re-raising, and so un-acknowledging, an acked alarm; a
/// text-only change likewise updates nothing, so the message shown stays the statement's first text.
/// </para>
/// <para>
/// <b>Resync.</b> On a (re)subscribe an unchanged Bad quality is re-asserted, because the host
/// re-annotates every condition Good when the driver reconnects and nothing else would put the
/// per-condition Bad back.
/// </para>
/// </remarks>
internal static class MTConnectConditionAlarmMapper
{
/// <summary>OPC UA severity of an active <c>Warning</c>.</summary>
internal const int WarningSeverity = 500;
/// <summary>OPC UA severity of an active <c>Fault</c>.</summary>
internal const int FaultSeverity = 800;
/// <summary>Canonical <c>Opc.Ua.StatusCodes.Good</c>.</summary>
private const uint Good = 0x00000000u;
/// <summary>Canonical <c>Opc.Ua.StatusCodes.BadNoCommunication</c> — the Agent has no state for the item.</summary>
private const uint BadNoCommunication = 0x80310000u;
/// <summary>
/// The event moving the server from <paramref name="told"/> to <paramref name="current"/> (or <c>null</c>
/// when nothing needs to be said), and the new told record.
/// </summary>
/// <param name="told">What the server was last told for this condition; <c>null</c> when nothing yet.</param>
/// <param name="current">The Agent's current statement.</param>
/// <param name="conditionId">The condition's ConditionId — the alarm-bearing raw tag's RawPath.</param>
/// <param name="dataItemId">The Agent DataItem id (carried as <see cref="AlarmEventArgs.SourceNodeId"/>).</param>
/// <param name="handle">The alarm subscription the event belongs to.</param>
/// <param name="resync">True on a (re)subscribe: re-assert an unchanged Bad quality.</param>
/// <returns>The event to raise (or <c>null</c>) and what the server has been told afterwards.</returns>
public static (AlarmEventArgs? Event, MTConnectToldCondition Told) Transition(
MTConnectToldCondition? told,
MTConnectConditionState current,
string conditionId,
string dataItemId,
IAlarmSubscriptionHandle handle,
bool resync)
{
ArgumentNullException.ThrowIfNull(current);
var statement = told?.Statement;
var bad = told?.QualityBad ?? false;
if (current.Level == MTConnectConditionLevel.Unavailable)
{
// State untouched; only the quality moves (or is re-asserted on a resync).
var next = new MTConnectToldCondition(statement, QualityBad: true);
return !bad || resync
? (QualityOnly(statement ?? current, current, conditionId, dataItemId, handle, BadNoCommunication), next)
: (null, next);
}
var told2 = new MTConnectToldCondition(current, QualityBad: false);
if (statement is not null && statement.SameAlarmAs(current))
{
// Unchanged statement: keep the FIRST text the server was shown. Only a Bad quality is lifted.
var kept = told2 with { Statement = statement };
return bad
? (QualityOnly(statement, current, conditionId, dataItemId, handle, Good), kept)
: (null, kept);
}
var wasActive = statement?.IsActive == true;
if (current.IsActive)
{
return (Build(current, statement, conditionId, dataItemId, handle,
wasActive ? AlarmTransitionKind.Retrigger : AlarmTransitionKind.Raise), told2);
}
if (wasActive)
{
return (Build(current, statement, conditionId, dataItemId, handle, AlarmTransitionKind.Clear), told2);
}
// Normal after nothing / after Normal: nothing to say about the state — the host's connectivity
// annotation confirms a materialised condition Good — except lifting a Bad this driver put there.
return bad
? (QualityOnly(current, current, conditionId, dataItemId, handle, Good), told2)
: (null, told2);
}
/// <summary>The Part 9 severity of an active level.</summary>
/// <param name="level">An active level.</param>
/// <returns>The Fault or Warning severity.</returns>
internal static int SeverityOf(MTConnectConditionLevel level) =>
level == MTConnectConditionLevel.Fault ? FaultSeverity : WarningSeverity;
/// <summary>
/// The condition's message: its text content, else <c>"{type} {level}"</c>; followed by whichever of
/// <c>nativeCode</c> / <c>nativeSeverity</c> / <c>qualifier</c> the Agent supplied — the Part 9 event
/// model has no slot for them, and an operator needs the native code to look the alarm up.
/// </summary>
/// <param name="state">The state to describe.</param>
/// <returns>The alarm message text.</returns>
internal static string MessageOf(MTConnectConditionState state)
{
var detail = state.Detail;
var level = state.Level == MTConnectConditionLevel.Unavailable ? "UNAVAILABLE" : state.Level.ToString();
var builder = new StringBuilder(detail.Text ?? $"{detail.ConditionType ?? "CONDITION"} {level}");
var extras = new List<string>(3);
if (detail.NativeCode is not null) extras.Add($"nativeCode={detail.NativeCode}");
if (detail.NativeSeverity is not null) extras.Add($"nativeSeverity={detail.NativeSeverity}");
if (detail.Qualifier is not null) extras.Add($"qualifier={detail.Qualifier}");
if (extras.Count > 0) builder.Append(" [").AppendJoin(", ", extras).Append(']');
return builder.ToString();
}
/// <summary>
/// A quality-only annotation. The host writes ONLY the condition's Quality for it (never message,
/// severity or ack state), so the message/severity carried are informational: those of the statement the
/// server holds.
/// </summary>
private static AlarmEventArgs QualityOnly(
MTConnectConditionState shown,
MTConnectConditionState current,
string conditionId,
string dataItemId,
IAlarmSubscriptionHandle handle,
uint quality)
{
var severity = shown.IsActive ? SeverityOf(shown.Level) : WarningSeverity;
return new AlarmEventArgs(
SubscriptionHandle: handle,
SourceNodeId: dataItemId,
ConditionId: conditionId,
AlarmType: shown.Detail.ConditionType ?? "CONDITION",
Message: MessageOf(shown),
Severity: severity >= FaultSeverity ? AlarmSeverity.High : AlarmSeverity.Medium,
SourceTimestampUtc: current.TimestampUtc,
Kind: AlarmTransitionKind.Unspecified,
OpcUaSeverity: severity,
QualityStatusCode: quality);
}
private static AlarmEventArgs Build(
MTConnectConditionState current,
MTConnectConditionState? previous,
string conditionId,
string dataItemId,
IAlarmSubscriptionHandle handle,
AlarmTransitionKind kind)
{
// A clear keeps the severity of the alarm it clears, so it does not visibly re-grade the condition.
var severity = current.IsActive
? SeverityOf(current.Level)
: previous?.IsActive == true ? SeverityOf(previous.Level) : WarningSeverity;
return new AlarmEventArgs(
SubscriptionHandle: handle,
SourceNodeId: dataItemId,
ConditionId: conditionId,
AlarmType: current.Detail.ConditionType ?? "CONDITION",
Message: MessageOf(current),
Severity: severity >= FaultSeverity ? AlarmSeverity.High : AlarmSeverity.Medium,
SourceTimestampUtc: current.TimestampUtc,
Kind: kind,
OpcUaSeverity: severity,
// A real transition only ever happens on live data, so it also lifts any Bad annotation.
QualityStatusCode: Good);
}
}