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)