diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session/UpstreamValueMapping.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session/UpstreamValueMapping.cs index 3e69b552..c00d5c92 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session/UpstreamValueMapping.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Session/UpstreamValueMapping.cs @@ -8,7 +8,12 @@ public static class UpstreamValueMapping /// Materializes scalar and one-dimensional array values without flattening matrices. /// The upstream value. /// The independent CLR payload. - public static object? ToClr(Variant value) => Detach(value.AsBoxedObject(Variant.BoxingBehavior.Legacy)); + public static object? ToClr(Variant value) + { + if (value.TypeInfo.IsMatrix) + throw new NotSupportedException("Matrix payloads are not supported by the OPC UA driver."); + return Detach(value.AsBoxedObject(Variant.BoxingBehavior.Legacy)); + } private static object? Detach(object? value) => value switch { diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs index c705200e..da89da04 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs @@ -548,7 +548,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit // G-2b: resolve any secret:-prefixed Password / UserCertificatePassword through the // shared secret store ONCE, here in the async connect flow, just before the credentials // are consumed. _options stays the raw config (with secret: refs) — only this - // connect-scoped local carries plaintext, and only for the duration of the connect. + // session identity carries the resolved credential while activation/signing needs it. // Resolving on every InitializeAsync (a full reconnect calls it) picks up rotations. // Both credential consumers — the Username password below and the Certificate password // in BuildCertificateIdentity — flow through BuildUserIdentity(resolvedOptions), so a @@ -1474,11 +1474,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit // Preserve the upstream StatusCode verbatim — including Bad codes per // §8's cascading-quality rule. Also preserve SourceTimestamp so downstream // clients can detect stale upstream data. - results[r] = new DataValueSnapshot( - Value: UpstreamValueMapping.ToClr(dv.WrappedValue), - StatusCode: dv.StatusCode.Code, - SourceTimestampUtc: dv.SourceTimestamp == DateTime.MinValue ? null : dv.SourceTimestamp.ToDateTime(), - ServerTimestampUtc: dv.ServerTimestamp == DateTime.MinValue ? now : dv.ServerTimestamp.ToDateTime()); + results[r] = ToSnapshot(dv, now); } RecordDeviceAnswered(generation, now); } @@ -2104,6 +2100,19 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit PublishNotification(handle, RefOf(item), dv); } + private static DataValueSnapshot ToSnapshot(DataValue value, DateTime now) + { + // Matrix payloads have no driver-domain contract. SDK 2's Legacy boxing materializes + // them into CLR matrices, so refuse that unsupported shape before boxing. 1D arrays + // retain their deployed pass-through behavior, including scalar-shaped authored tags. + var unsupportedShape = value.WrappedValue.TypeInfo.IsMatrix; + return new DataValueSnapshot( + Value: unsupportedShape ? null : UpstreamValueMapping.ToClr(value.WrappedValue), + StatusCode: unsupportedShape ? StatusCodes.BadTypeMismatch.Code : value.StatusCode.Code, + SourceTimestampUtc: value.SourceTimestamp == DateTimeUtc.MinValue ? null : value.SourceTimestamp.ToDateTime(), + ServerTimestampUtc: value.ServerTimestamp == DateTimeUtc.MinValue ? now : value.ServerTimestamp.ToDateTime()); + } + private static string RefOf(MonitoredItem item) => (item.Handle as string) ?? item.DisplayName ?? string.Empty; private void PublishNotification(OpcUaSubscriptionHandle handle, string fullRef, DataValue dv) @@ -2113,11 +2122,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit // a delivered notification is liveness evidence — the subscription path never calls ReadAsync, so // without this a busy subscription-only driver's LastSuccessfulRead stayed at its connect time. RecordLivenessEvidence(); - var snapshot = new DataValueSnapshot( - Value: UpstreamValueMapping.ToClr(dv.WrappedValue), - StatusCode: dv.StatusCode.Code, - SourceTimestampUtc: dv.SourceTimestamp == DateTime.MinValue ? null : dv.SourceTimestamp.ToDateTime(), - ServerTimestampUtc: dv.ServerTimestamp == DateTime.MinValue ? DateTime.UtcNow : dv.ServerTimestamp.ToDateTime()); + var snapshot = ToSnapshot(dv, DateTime.UtcNow); // Review of 3b88056f: the replay check and the publish are one critical section shared with the recovery and // trailing reads, so an older value can never be delivered after a newer one (see _deliveryLock). lock (_deliveryLock)