Files
mxaccessgw/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessEventMapper.cs
T
Joseph Doherty fca978de07 docs(src): add missing XML docs and strip tracking-ID comments
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.
2026-07-07 14:09:49 -04:00

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;
}
}