fca978de07
Sweep of 203 source files resolving CommentChecker findings: add <summary>/<param>/<returns>/<inheritdoc> where missing, and remove resolved task/issue tracking markers (Tests-NNN, Worker-NNN, Server-NNN, Task N) from code comments. Comment/doc-only — no logic changes. Server+Tests build clean under TreatWarningsAsErrors.
425 lines
18 KiB
C#
425 lines
18 KiB
C#
using System;
|
|
using System.Globalization;
|
|
using Google.Protobuf.WellKnownTypes;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
using ZB.MOM.WW.MxGateway.Worker.Conversion;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
|
|
|
/// <summary>Maps MXAccess COM events to protobuf MxEvent messages.</summary>
|
|
public sealed class MxAccessEventMapper
|
|
{
|
|
private readonly VariantConverter variantConverter;
|
|
private readonly MxStatusProxyConverter statusProxyConverter;
|
|
|
|
/// <summary>Initializes a new instance of the MxAccessEventMapper class with default converters.</summary>
|
|
public MxAccessEventMapper()
|
|
: this(new VariantConverter(), new MxStatusProxyConverter())
|
|
{
|
|
}
|
|
|
|
/// <summary>Initializes a new instance of the MxAccessEventMapper class with provided converters.</summary>
|
|
/// <param name="variantConverter">Converter for MXAccess variant values to MxValue protobuf messages.</param>
|
|
/// <param name="statusProxyConverter">Converter for MXAccess status arrays to MxStatusProxy protobuf messages.</param>
|
|
public MxAccessEventMapper(
|
|
VariantConverter variantConverter,
|
|
MxStatusProxyConverter statusProxyConverter)
|
|
{
|
|
this.variantConverter = variantConverter ?? throw new ArgumentNullException(nameof(variantConverter));
|
|
this.statusProxyConverter = statusProxyConverter ?? throw new ArgumentNullException(nameof(statusProxyConverter));
|
|
}
|
|
|
|
/// <summary>Creates an OnDataChange event from MXAccess COM event arguments.</summary>
|
|
/// <param name="sessionId">Identifier of the session.</param>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
|
/// <param name="value">Item value received from MXAccess.</param>
|
|
/// <param name="quality">Item quality code from MXAccess.</param>
|
|
/// <param name="timestamp">Item timestamp from MXAccess.</param>
|
|
/// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
|
|
/// <returns>The mapped OnDataChange event.</returns>
|
|
public MxEvent CreateOnDataChange(
|
|
string sessionId,
|
|
int serverHandle,
|
|
int itemHandle,
|
|
object? value,
|
|
int quality,
|
|
object? timestamp,
|
|
Array? statuses)
|
|
{
|
|
MxEvent mxEvent = CreateBaseEvent(
|
|
MxEventFamily.OnDataChange,
|
|
sessionId,
|
|
serverHandle,
|
|
itemHandle,
|
|
statuses);
|
|
mxEvent.Value = variantConverter.Convert(value);
|
|
mxEvent.Quality = quality;
|
|
ApplySourceTimestamp(mxEvent, timestamp);
|
|
mxEvent.OnDataChange = new OnDataChangeEvent();
|
|
|
|
return mxEvent;
|
|
}
|
|
|
|
/// <summary>Creates an OnWriteComplete event from MXAccess COM event arguments.</summary>
|
|
/// <param name="sessionId">Identifier of the session.</param>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
|
/// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
|
|
/// <returns>The mapped OnWriteComplete event.</returns>
|
|
public MxEvent CreateOnWriteComplete(
|
|
string sessionId,
|
|
int serverHandle,
|
|
int itemHandle,
|
|
Array? statuses)
|
|
{
|
|
MxEvent mxEvent = CreateBaseEvent(
|
|
MxEventFamily.OnWriteComplete,
|
|
sessionId,
|
|
serverHandle,
|
|
itemHandle,
|
|
statuses);
|
|
mxEvent.OnWriteComplete = new OnWriteCompleteEvent();
|
|
|
|
return mxEvent;
|
|
}
|
|
|
|
/// <summary>Creates an OperationComplete event from MXAccess COM event arguments.</summary>
|
|
/// <param name="sessionId">Identifier of the session.</param>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
|
/// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
|
|
/// <returns>The mapped OperationComplete event.</returns>
|
|
public MxEvent CreateOperationComplete(
|
|
string sessionId,
|
|
int serverHandle,
|
|
int itemHandle,
|
|
Array? statuses)
|
|
{
|
|
MxEvent mxEvent = CreateBaseEvent(
|
|
MxEventFamily.OperationComplete,
|
|
sessionId,
|
|
serverHandle,
|
|
itemHandle,
|
|
statuses);
|
|
mxEvent.OperationComplete = new OperationCompleteEvent();
|
|
|
|
return mxEvent;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an OnAlarmTransition event from MXAccess alarm-event arguments.
|
|
/// The worker's alarm path drives this method from
|
|
/// <see cref="MxAccessAlarmEventSink.EnqueueTransition"/> once
|
|
/// <see cref="AlarmDispatcher"/> decodes a transition raised by the
|
|
/// wnwrap-backed <see cref="WnWrapAlarmConsumer"/>.
|
|
/// </summary>
|
|
/// <param name="sessionId">Identifier of the session.</param>
|
|
/// <param name="alarmFullReference">Fully-qualified MxAccess alarm reference (e.g. "Tank01.Level.HiHi").</param>
|
|
/// <param name="sourceObjectReference">Galaxy-side source object reference; empty when not bound to a Galaxy object.</param>
|
|
/// <param name="alarmTypeName">MxAccess alarm-type qualifier (e.g. "AnalogLimitAlarm.HiHi").</param>
|
|
/// <param name="transitionKind">Discriminator: Raise / Acknowledge / Clear / Retrigger.</param>
|
|
/// <param name="severity">Raw MxAccess severity (kept on the native scale; lmxopcua maps to OPC UA 0-1000).</param>
|
|
/// <param name="originalRaiseTimestampUtc">When the alarm originally entered active; null on retrigger.</param>
|
|
/// <param name="transitionTimestampUtc">When this specific transition occurred.</param>
|
|
/// <param name="operatorUser">Operator principal recorded by MxAccess on Acknowledge transitions; empty on raise/clear.</param>
|
|
/// <param name="operatorComment">Operator-supplied comment recorded by MxAccess on Acknowledge transitions; empty on raise/clear.</param>
|
|
/// <param name="category">Alarm taxonomy bucket from the Galaxy template.</param>
|
|
/// <param name="description">Human-readable alarm description.</param>
|
|
/// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
|
|
/// <param name="degraded">
|
|
/// <see langword="true"/> when this transition was synthesized by the
|
|
/// subtag-provider fallback rather than the native alarmmgr path.
|
|
/// Defaults to <see langword="false"/> to preserve alarmmgr parity.
|
|
/// </param>
|
|
/// <param name="sourceProvider">
|
|
/// The alarm provider that sourced this transition. Defaults to
|
|
/// <see cref="AlarmProviderMode.Alarmmgr"/> for the native path.
|
|
/// </param>
|
|
/// <returns>The mapped OnAlarmTransition event.</returns>
|
|
public MxEvent CreateOnAlarmTransition(
|
|
string sessionId,
|
|
string alarmFullReference,
|
|
string sourceObjectReference,
|
|
string alarmTypeName,
|
|
AlarmTransitionKind transitionKind,
|
|
int severity,
|
|
DateTime? originalRaiseTimestampUtc,
|
|
DateTime transitionTimestampUtc,
|
|
string operatorUser,
|
|
string operatorComment,
|
|
string category,
|
|
string description,
|
|
Array? statuses,
|
|
bool degraded = false,
|
|
AlarmProviderMode sourceProvider = AlarmProviderMode.Alarmmgr)
|
|
{
|
|
MxEvent mxEvent = CreateBaseEvent(
|
|
MxEventFamily.OnAlarmTransition,
|
|
sessionId,
|
|
serverHandle: 0,
|
|
itemHandle: 0,
|
|
statuses);
|
|
|
|
OnAlarmTransitionEvent body = new()
|
|
{
|
|
AlarmFullReference = alarmFullReference ?? string.Empty,
|
|
SourceObjectReference = sourceObjectReference ?? string.Empty,
|
|
AlarmTypeName = alarmTypeName ?? string.Empty,
|
|
TransitionKind = transitionKind,
|
|
Severity = severity,
|
|
TransitionTimestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(
|
|
DateTime.SpecifyKind(transitionTimestampUtc, DateTimeKind.Utc)),
|
|
OperatorUser = operatorUser ?? string.Empty,
|
|
OperatorComment = operatorComment ?? string.Empty,
|
|
Category = category ?? string.Empty,
|
|
Description = description ?? string.Empty,
|
|
Degraded = degraded,
|
|
SourceProvider = sourceProvider,
|
|
};
|
|
if (originalRaiseTimestampUtc is { } orts)
|
|
{
|
|
body.OriginalRaiseTimestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(
|
|
DateTime.SpecifyKind(orts, DateTimeKind.Utc));
|
|
}
|
|
mxEvent.OnAlarmTransition = body;
|
|
return mxEvent;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an OnAlarmProviderModeChanged event from a
|
|
/// <see cref="FailoverAlarmConsumer"/> provider switch. The worker's
|
|
/// alarm path drives this when the failover composite switches between
|
|
/// the alarmmgr primary and the subtag standby, so connected gateway
|
|
/// clients can observe the degraded/recovered provider state.
|
|
/// </summary>
|
|
/// <param name="sessionId">Identifier of the session.</param>
|
|
/// <param name="mode">The provider mode now active after the switch.</param>
|
|
/// <param name="reason">Human-readable reason for the switch.</param>
|
|
/// <param name="hresult">The COM HRESULT that triggered a failover, or 0 for a clean failback.</param>
|
|
/// <param name="atUtc">The UTC instant the switch occurred.</param>
|
|
/// <returns>The mapped OnAlarmProviderModeChanged event.</returns>
|
|
public MxEvent CreateOnAlarmProviderModeChanged(
|
|
string sessionId,
|
|
AlarmProviderMode mode,
|
|
string reason,
|
|
int hresult,
|
|
DateTime atUtc)
|
|
{
|
|
MxEvent mxEvent = CreateBaseEvent(
|
|
MxEventFamily.OnAlarmProviderModeChanged,
|
|
sessionId,
|
|
serverHandle: 0,
|
|
itemHandle: 0,
|
|
statuses: null);
|
|
|
|
mxEvent.OnAlarmProviderModeChanged = new OnAlarmProviderModeChangedEvent
|
|
{
|
|
Mode = mode,
|
|
Reason = reason ?? string.Empty,
|
|
Hresult = hresult,
|
|
At = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(
|
|
DateTime.SpecifyKind(atUtc, DateTimeKind.Utc)),
|
|
};
|
|
return mxEvent;
|
|
}
|
|
|
|
/// <summary>Creates an OnBufferedDataChange event from MXAccess COM event arguments.</summary>
|
|
/// <param name="sessionId">Identifier of the session.</param>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
|
/// <param name="rawDataType">Raw MXAccess data type code for the buffered value.</param>
|
|
/// <param name="value">Item value received from MXAccess.</param>
|
|
/// <param name="quality">Array of quality values from MXAccess.</param>
|
|
/// <param name="timestamp">Array of timestamp values from MXAccess.</param>
|
|
/// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
|
|
/// <returns>The mapped OnBufferedDataChange event.</returns>
|
|
public MxEvent CreateOnBufferedDataChange(
|
|
string sessionId,
|
|
int serverHandle,
|
|
int itemHandle,
|
|
int rawDataType,
|
|
object? value,
|
|
object? quality,
|
|
object? timestamp,
|
|
Array? statuses)
|
|
{
|
|
MxDataType dataType = MapMxDataType(rawDataType);
|
|
MxEvent mxEvent = CreateBaseEvent(
|
|
MxEventFamily.OnBufferedDataChange,
|
|
sessionId,
|
|
serverHandle,
|
|
itemHandle,
|
|
statuses);
|
|
mxEvent.Value = variantConverter.Convert(value, dataType);
|
|
mxEvent.OnBufferedDataChange = new OnBufferedDataChangeEvent
|
|
{
|
|
DataType = dataType,
|
|
RawDataType = rawDataType,
|
|
QualityValues = ConvertBufferedArray(quality, MxDataType.Integer),
|
|
TimestampValues = ConvertBufferedArray(timestamp, MxDataType.Time),
|
|
};
|
|
|
|
return mxEvent;
|
|
}
|
|
|
|
/// <summary>Maps a raw MXAccess data type code to the MxDataType enum.</summary>
|
|
/// <param name="rawDataType">Raw MXAccess data type value to map.</param>
|
|
/// <returns>The corresponding MxDataType enum value.</returns>
|
|
public static MxDataType MapMxDataType(int rawDataType)
|
|
{
|
|
return rawDataType switch
|
|
{
|
|
-1 => MxDataType.Unknown,
|
|
0 => MxDataType.NoData,
|
|
1 => MxDataType.Boolean,
|
|
2 => MxDataType.Integer,
|
|
3 => MxDataType.Float,
|
|
4 => MxDataType.Double,
|
|
5 => MxDataType.String,
|
|
6 => MxDataType.Time,
|
|
7 => MxDataType.ElapsedTime,
|
|
8 => MxDataType.ReferenceType,
|
|
9 => MxDataType.StatusType,
|
|
10 => MxDataType.Enum,
|
|
11 => MxDataType.SecurityClassificationEnum,
|
|
12 => MxDataType.DataQualityType,
|
|
13 => MxDataType.QualifiedEnum,
|
|
14 => MxDataType.QualifiedStruct,
|
|
15 => MxDataType.InternationalizedString,
|
|
16 => MxDataType.BigString,
|
|
17 => MxDataType.End,
|
|
_ => MxDataType.Unknown,
|
|
};
|
|
}
|
|
|
|
private MxEvent CreateBaseEvent(
|
|
MxEventFamily family,
|
|
string sessionId,
|
|
int serverHandle,
|
|
int itemHandle,
|
|
Array? statuses)
|
|
{
|
|
MxEvent mxEvent = new()
|
|
{
|
|
Family = family,
|
|
SessionId = sessionId ?? string.Empty,
|
|
ServerHandle = serverHandle,
|
|
ItemHandle = itemHandle,
|
|
};
|
|
mxEvent.Statuses.Add(statusProxyConverter.ConvertMany(statuses));
|
|
|
|
return mxEvent;
|
|
}
|
|
|
|
private void ApplySourceTimestamp(
|
|
MxEvent mxEvent,
|
|
object? timestamp)
|
|
{
|
|
MxValue convertedTimestamp = variantConverter.Convert(timestamp, MxDataType.Time);
|
|
if (convertedTimestamp.KindCase == MxValue.KindOneofCase.TimestampValue)
|
|
{
|
|
mxEvent.SourceTimestamp = convertedTimestamp.TimestampValue;
|
|
return;
|
|
}
|
|
|
|
// MXAccess fires OnDataChange with pftItemTimeStamp marshaled as a
|
|
// VT_BSTR string (e.g. "3/26/2026 1:38:22.907 PM"), not a FILETIME or
|
|
// a VT_DATE — so the variant converter classifies it as a plain
|
|
// string and the timestamp would otherwise be dropped. Parse it here
|
|
// so the source timestamp still reaches MxEvent. MXAccess formats the
|
|
// string in the worker host's local time; see TryParseSourceTimestamp.
|
|
if (convertedTimestamp.KindCase == MxValue.KindOneofCase.StringValue
|
|
&& TryParseSourceTimestamp(convertedTimestamp.StringValue, out DateTime parsedUtc))
|
|
{
|
|
mxEvent.SourceTimestamp = Timestamp.FromDateTime(parsedUtc);
|
|
return;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(convertedTimestamp.RawDiagnostic))
|
|
{
|
|
mxEvent.RawStatus = string.IsNullOrWhiteSpace(mxEvent.RawStatus)
|
|
? convertedTimestamp.RawDiagnostic
|
|
: $"{mxEvent.RawStatus}; {convertedTimestamp.RawDiagnostic}";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses an MXAccess <c>OnDataChange</c> timestamp string into a UTC
|
|
/// <see cref="DateTime"/>. MXAccess delivers the value as a culture-
|
|
/// formatted string rather than a FILETIME or VT_DATE, and formats it
|
|
/// in the worker host's <em>local</em> time (verified empirically — a
|
|
/// fast-changing tag's timestamp lands the host's UTC offset behind
|
|
/// wall-clock UTC). The parsed value is therefore taken as local time
|
|
/// and converted to UTC. Tries the worker host's culture first
|
|
/// (MXAccess formats with the host locale), then the invariant culture.
|
|
/// </summary>
|
|
/// <param name="text">The MXAccess timestamp string.</param>
|
|
/// <param name="utc">The parsed UTC timestamp on success.</param>
|
|
/// <returns><see langword="true"/> when the string parsed successfully.</returns>
|
|
internal static bool TryParseSourceTimestamp(string? text, out DateTime utc)
|
|
{
|
|
utc = default;
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const DateTimeStyles styles = DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal;
|
|
if (DateTime.TryParse(text, CultureInfo.CurrentCulture, styles, out DateTime parsed)
|
|
|| DateTime.TryParse(text, CultureInfo.InvariantCulture, styles, out parsed))
|
|
{
|
|
utc = DateTime.SpecifyKind(parsed, DateTimeKind.Utc);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private MxArray ConvertBufferedArray(
|
|
object? value,
|
|
MxDataType expectedElementDataType)
|
|
{
|
|
if (value is Array array)
|
|
{
|
|
return variantConverter.ConvertArray(array, expectedElementDataType);
|
|
}
|
|
|
|
MxValue converted = variantConverter.Convert(value, expectedElementDataType);
|
|
if (converted.KindCase == MxValue.KindOneofCase.ArrayValue)
|
|
{
|
|
return converted.ArrayValue;
|
|
}
|
|
|
|
MxArray mxArray = new()
|
|
{
|
|
ElementDataType = converted.DataType,
|
|
VariantType = converted.VariantType,
|
|
RawElementDataType = converted.RawDataType,
|
|
RawDiagnostic = string.IsNullOrWhiteSpace(converted.RawDiagnostic)
|
|
? "Buffered MXAccess event argument was not a SAFEARRAY."
|
|
: converted.RawDiagnostic,
|
|
};
|
|
|
|
switch (converted.KindCase)
|
|
{
|
|
case MxValue.KindOneofCase.Int32Value:
|
|
mxArray.Int32Values = new Int32Array();
|
|
mxArray.Int32Values.Values.Add(converted.Int32Value);
|
|
break;
|
|
|
|
case MxValue.KindOneofCase.Int64Value:
|
|
mxArray.Int64Values = new Int64Array();
|
|
mxArray.Int64Values.Values.Add(converted.Int64Value);
|
|
break;
|
|
|
|
case MxValue.KindOneofCase.TimestampValue:
|
|
mxArray.TimestampValues = new TimestampArray();
|
|
mxArray.TimestampValues.Values.Add(converted.TimestampValue);
|
|
break;
|
|
}
|
|
|
|
return mxArray;
|
|
}
|
|
}
|