rename: prefix gateway projects/namespaces with ZB.MOM.WW + sln→slnx
Apply the ZB.MOM.WW. prefix to all gateway-side projects, folders,
.csproj/.sln contents, C# namespaces, using directives, generated proto
C# (csharp_namespace + checked-in generated files), InternalsVisibleTo
attributes, project-name string literals (LoadProject, .sln lookups,
worker exe paths, staticwebassets manifest), and the install/script/doc
references that point at any of the above. Migrate the solution from
.sln to .slnx via `dotnet sln migrate` and delete the old file.
External-runtime identifiers are intentionally NOT prefixed so external
configuration keeps working:
- GatewayMetrics.cs MeterName ("MxGateway.Server")
- DashboardAuthenticationDefaults Scheme/Policy ("MxGateway.Dashboard")
- GatewayRequestLoggingMiddleware logger category ("MxGateway.Request")
- StaRuntime thread name ("MxGateway.Worker.STA")
- appsettings.json root section "MxGateway" + env-var prefix
MxGateway__... and secret-name MxGateway:ApiKeyPepper
- C:\ProgramData\MxGateway\ data dir paths
Also fixes two tests that were not rename-related but became visible
while validating the rename:
- WorkerLiveMxAccessSmokeTests.ShutDownAsync: cancellation that the
gateway service correctly maps to RpcException(Cancelled) per gRPC
convention was being misclassified as a stream fault. Added a sibling
catch on RpcException with StatusCode.Cancelled.
- IntegrationTestEnvironment.ResolveRepositoryRoot: extracted IsRepositoryRoot
and made it accept either a .git marker OR a .sln/.slnx next to src/
so the worker-exe walker works in non-git working copies.
clients/proto/proto-inputs.json's protoRoot updated to point at
src/ZB.MOM.WW.MxGateway.Contracts/Protos.
Verified by `dotnet build` and a full `dotnet test` of the .slnx with
MXGATEWAY_RUN_LIVE_{MXACCESS,LDAP,GALAXY}_TESTS=1:
Tests: 472/472 pass
Worker.Tests: 280/280 pass (4 dev-rig [Fact(Skip=...)] skipped)
IntegrationTests: 18/18 pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
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>
|
||||
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)
|
||||
{
|
||||
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,
|
||||
};
|
||||
if (originalRaiseTimestampUtc is { } orts)
|
||||
{
|
||||
body.OriginalRaiseTimestamp = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(
|
||||
DateTime.SpecifyKind(orts, DateTimeKind.Utc));
|
||||
}
|
||||
mxEvent.OnAlarmTransition = body;
|
||||
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>
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user