diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AdvertisedAggregateFunctions.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AdvertisedAggregateFunctions.cs index be8a6e8f..4751e15e 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AdvertisedAggregateFunctions.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AdvertisedAggregateFunctions.cs @@ -30,8 +30,8 @@ internal static class AdvertisedAggregateFunctions private static readonly object SyncGate = new(); /// Every standard aggregate-function NodeId the SDK's nodeset knows (ObjectIds.AggregateFunction_*). private static readonly NodeId[] StandardAggregateIds = typeof(ObjectIds) - .GetFields(BindingFlags.Public | BindingFlags.Static) - .Where(f => f.FieldType == typeof(NodeId) && f.Name.StartsWith("AggregateFunction_", StringComparison.Ordinal)) + .GetProperties(BindingFlags.Public | BindingFlags.Static) + .Where(f => f.PropertyType == typeof(NodeId) && f.Name.StartsWith("AggregateFunction_", StringComparison.Ordinal)) .Select(f => (NodeId)f.GetValue(null)!) .ToArray(); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs index 91d0a137..1dfad616 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs @@ -1522,6 +1522,10 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 var parent = ResolveParentFolder(equipmentNodeId, realm); AlarmConditionState alarm = CreateAlarmConditionOfType(alarmType, parent); + // SDK 2.0 creates optional confirmation nodes only when requested explicitly. + // Both are part of the application's existing Part 9 method/state contract. + alarm.ConfirmedState = new TwoStateVariableState(alarm); + alarm.Confirm = new AddCommentMethodState(alarm); alarm.SymbolicName = displayName; // HasComponent so the parent folder "owns" the condition (matches the T13 notes' pattern). alarm.ReferenceTypeId = ReferenceTypeIds.HasComponent; @@ -5582,14 +5586,14 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 // same HistoryRead authorization as a raw read (above); a backend with no trail ⇒ unsupported. work.Add(() => ServeModifiedPagedAsync( handle, session, nodesToRead, results, errors, - details.StartTime.ToDateTime(), details.EndTime.ToDateTime(), details.NumValuesPerNode, + SdkValueBoundary.ToClrTimestamp(details.StartTime), SdkValueBoundary.ToClrTimestamp(details.EndTime), details.NumValuesPerNode, ct)); continue; } work.Add(() => ServeRawPagedAsync( handle, session, nodesToRead, results, errors, - details.StartTime.ToDateTime(), details.EndTime.ToDateTime(), details.NumValuesPerNode, + SdkValueBoundary.ToClrTimestamp(details.StartTime), SdkValueBoundary.ToClrTimestamp(details.EndTime), details.NumValuesPerNode, ct, details.ReturnBounds && HonorReturnBounds)); } @@ -5649,8 +5653,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 var agg = aggregate.Value; work.Add(() => ServeNodeAsync(handle, results, errors, (source, tagname, token) => source.ReadProcessedAsync( tagname, - details.StartTime.ToDateTime(), - details.EndTime.ToDateTime(), + SdkValueBoundary.ToClrTimestamp(details.StartTime), + SdkValueBoundary.ToClrTimestamp(details.EndTime), interval, agg, token), ct)); @@ -5827,8 +5831,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 var source = _historianDataSource; try { - var startUtc = details.StartTime.ToDateTime(); - var endUtc = details.EndTime.ToDateTime(); + var startUtc = SdkValueBoundary.ToClrTimestamp(details.StartTime); + var endUtc = SdkValueBoundary.ToClrTimestamp(details.EndTime); var numValuesPerNode = details.NumValuesPerNode; var boundarySkip = 0; var walkHint = default(ReverseWalkHint); // a walked-back read's width + proven-empty stretch @@ -6167,8 +6171,8 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 // Deleting the modified-value trail itself (Part 11 IsDeleteModified) is not supported: the trail is the // audit record of every change and is aged out only by retention. if (details.IsDeleteModified) return null; - var start = details.StartTime.ToDateTime(); - var end = details.EndTime.ToDateTime(); + var start = SdkValueBoundary.ToClrTimestamp(details.StartTime); + var end = SdkValueBoundary.ToClrTimestamp(details.EndTime); // Part 11 requires both bounds of a raw delete; DateTime.MinValue is the wire's "unspecified". Refuse // it here rather than let a backend read MinValue as "from the beginning of time". if (start == DateTime.MinValue || end == DateTime.MinValue) @@ -6531,7 +6535,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 if (string.IsNullOrWhiteSpace(typeName)) return ObjectTypeIds.AlarmConditionType; return s_eventTypesByName.GetOrAdd(typeName, name => { - var field = typeof(ObjectTypeIds).GetField(name.EndsWith("Type", StringComparison.Ordinal) ? name : name + "Type", + var field = typeof(ObjectTypeIds).GetProperty(name.EndsWith("Type", StringComparison.Ordinal) ? name : name + "Type", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); return field?.GetValue(null) is NodeId type && Server.TypeTree.IsTypeOf(type, ObjectTypeIds.BaseEventType) ? type @@ -6838,7 +6842,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 /// synthesised time-based: /// /// Fresh read (no inbound continuation point): read the window from - /// details.StartTime.ToDateTime() to capped at + /// SdkValueBoundary.ToClrTimestamp(details.StartTime) to capped at /// + 1 — one lookahead row. If the lookahead row came /// back, return the first N, store a resume cursor and emit a continuation point; otherwise the page is /// the last and carries none (so a total that is an exact multiple of N ends on a full page, not on an diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkValueBoundary.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkValueBoundary.cs index 5e5dae95..381b7877 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkValueBoundary.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkValueBoundary.cs @@ -21,6 +21,10 @@ internal static class SdkValueBoundary throw new ArgumentException("Unsupported OPC UA value type.", nameof(value)); } + /// Map the UA zero timestamp to the application's existing unspecified-bound sentinel. + internal static DateTime ToClrTimestamp(DateTimeUtc value) => value == DateTimeUtc.MinValue + ? DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc) : value.ToDateTime(); + /// Restore existing CLR payload types and detach mutable arrays. internal static object? ToClr(Variant value) => value.AsBoxedObject(Variant.BoxingBehavior.Legacy) switch { diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/ZB.MOM.WW.OtOpcUa.OpcUaServer.csproj b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/ZB.MOM.WW.OtOpcUa.OpcUaServer.csproj index 0e24ce27..785c7222 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/ZB.MOM.WW.OtOpcUa.OpcUaServer.csproj +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/ZB.MOM.WW.OtOpcUa.OpcUaServer.csproj @@ -23,6 +23,9 @@ + + +