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 @@
+
+
+