826312befe
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.
214 lines
11 KiB
C#
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);
|
|
}
|
|
}
|