feat(dcl): populate obtainable NativeAlarmTransition fields from OPC UA and MxGateway (#27, M2.13)

OPC UA (RealOpcUaClient):
- Append 5 new SelectClauses at indices 13–17 (never renumber 0–12):
  - 13: AlarmConditionType/ActiveState/TransitionTime → OriginalRaiseTime
  - 14–17: LimitAlarmType HighHighLimit/HighLimit/LowLimit/LowLowLimit → LimitValue
- New OpcUaAlarmMapper.PickLimitValue helper: first non-null in HiHi→Hi→Lo→LoLo
  priority order, InvariantCulture-formatted; empty string for non-limit alarm types.
- HandleAlarmEvent reads new indices with fields.Count > N guards; hard minimum (6)
  unchanged so base ConditionType events still process without the limit fields.
- Document unavailable-by-protocol fields (Category, Description, OperatorUser,
  CurrentValue) inline in BuildAlarmEventFilter and HandleAlarmEvent.

MxGateway (MxGatewayAlarmMapper):
- MapTransition: CurrentValue and LimitValue now populated via MxValueToString
  (uses MxValueExtensions.ToClrValue + InvariantCulture) from OnAlarmTransitionEvent
  proto fields current_value/limit_value.
- MapSnapshot: same — populated from ActiveAlarmSnapshot.current_value/limit_value.
- MxValueToString helper (internal): null-safe MxValue → string conversion.

Tests (17 new, 40 total pass):
- OpcUaAlarmMapperTests: PickLimitValue priority, InvariantCulture, all-null case.
- MxGatewayAlarmMapperTests: CurrentValue/LimitValue populate from double/string
  MxValue; absent fields yield empty strings.
- RealOpcUaClientAlarmFilterTests: index alignment assertions (count=18, per-index
  TypeDefinitionId+BrowsePath), regression guard on existing indices 0–12.
This commit is contained in:
Joseph Doherty
2026-06-16 06:37:19 -04:00
parent e2b31a9fd2
commit 722b8663c1
8 changed files with 338 additions and 9 deletions
@@ -65,4 +65,40 @@ public static class OpcUaAlarmMapper
null or "Unshelved" => AlarmShelveState.Unshelved,
_ => AlarmShelveState.OneShotShelved
};
/// <summary>
/// Picks a representative display-only limit value from the four standard
/// <c>LimitAlarmType</c> set-point fields (HighHighLimit, HighLimit, LowLimit,
/// LowLowLimit) returned by the OPC UA event SelectClause.
///
/// <para>
/// The fields are absent (null raw value) on non-limit alarm types (discrete,
/// off-normal, etc.). When present, the first non-null value is returned in
/// priority order: HighHigh → High → Low → LowLow. The caller may use
/// <c>AlarmTypeName</c> or <c>ConditionName</c> to determine which specific
/// limit is active; this method intentionally returns the coarsest useful value
/// for the common single-limit case without requiring callers to understand the
/// OPC UA limit hierarchy.
/// </para>
/// </summary>
/// <param name="highHighRaw">Raw HighHighLimit field value (null when absent).</param>
/// <param name="highRaw">Raw HighLimit field value (null when absent).</param>
/// <param name="lowRaw">Raw LowLimit field value (null when absent).</param>
/// <param name="lowLowRaw">Raw LowLowLimit field value (null when absent).</param>
/// <returns>
/// A formatted string representation of the first non-null limit value, or an
/// empty string when all four fields are absent (non-limit alarm type).
/// </returns>
public static string PickLimitValue(object? highHighRaw, object? highRaw, object? lowRaw, object? lowLowRaw)
{
// Standard OPC UA LimitAlarmType limit values are numeric (Double/Float/Int).
// Convert with InvariantCulture so the decimal separator is always '.' regardless
// of the server's locale.
foreach (var raw in new[] { highHighRaw, highRaw, lowRaw, lowLowRaw })
{
if (raw is not null)
return Convert.ToString(raw, System.Globalization.CultureInfo.InvariantCulture) ?? "";
}
return "";
}
}