diff --git a/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.cs b/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.cs
index 9131da63a6..4c1f766bfc 100644
--- a/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.cs
+++ b/Libraries/Opc.Ua.Server/NodeManager/CoreNodeManager.cs
@@ -714,6 +714,8 @@ namespace Opc.Ua.Server
// owned by this node manager.
nodeToRead.Processed = true;
+ DateTime readTime = DateTime.UtcNow;
+
// read the default value (also verifies that the attribute id is valid for the node).
ServiceResult error = node.Read(context, nodeToRead.AttributeId, value);
@@ -779,14 +781,16 @@ namespace Opc.Ua.Server
// Set SourceTimestamp if not already set by the node
if (value.SourceTimestamp == DateTime.MinValue)
{
- value.SourceTimestamp = DateTime.UtcNow;
+ value.SourceTimestamp = readTime;
}
- // Set ServerTimestamp to match SourceTimestamp for Value attributes
- // This ensures ServerTimestamp and SourceTimestamp are equal,
- // which is important for nodes like ServerStatus children where
- // the node's read callback sets a specific timestamp
- value.ServerTimestamp = value.SourceTimestamp;
+ // Set timestamps after ReadAttribute. ServerTimestamp reflects
+ // when the Server verified the value. A value whose
+ // SourceTimestamp was produced as part of this read (e.g.
+ // ServerStatus children) keeps ServerTimestamp aligned with
+ // SourceTimestamp; a stored/static value is verified now, so
+ // ServerTimestamp is stamped with the current read time.
+ value.ServerTimestamp = value.SourceTimestamp >= readTime ? value.SourceTimestamp : readTime;
}
}
}
diff --git a/Libraries/Opc.Ua.Server/NodeManager/CustomNodeManager.cs b/Libraries/Opc.Ua.Server/NodeManager/CustomNodeManager.cs
index cae0583bfc..0dd18bd972 100644
--- a/Libraries/Opc.Ua.Server/NodeManager/CustomNodeManager.cs
+++ b/Libraries/Opc.Ua.Server/NodeManager/CustomNodeManager.cs
@@ -1670,6 +1670,8 @@ namespace Opc.Ua.Server
continue;
}
+ DateTime readTime = DateTime.UtcNow;
+
// read the attribute value.
errors[ii] = handle.Node.ReadAttribute(
systemContext,
@@ -1678,24 +1680,24 @@ namespace Opc.Ua.Server
nodeToRead.DataEncoding,
value);
- // Set timestamps after ReadAttribute to ensure consistency
- // For Value attributes, match ServerTimestamp to SourceTimestamp
- // For other attributes, just ensure ServerTimestamp is set
+ // Set timestamps after ReadAttribute. ServerTimestamp reflects
+ // when the Server verified the value. A value whose
+ // SourceTimestamp was produced as part of this read (e.g.
+ // ServerStatus children) keeps ServerTimestamp aligned with
+ // SourceTimestamp; a stored/static value is verified now, so
+ // ServerTimestamp is stamped with the current read time.
if (nodeToRead.AttributeId == Attributes.Value)
{
if (value.SourceTimestamp == DateTime.MinValue)
{
- value.SourceTimestamp = DateTime.UtcNow;
+ value.SourceTimestamp = readTime;
}
- value.ServerTimestamp = value.SourceTimestamp;
+ value.ServerTimestamp = value.SourceTimestamp >= readTime ? value.SourceTimestamp : readTime;
}
- else
+ else if (value.ServerTimestamp == DateTime.MinValue)
{
// For non-value attributes, only ServerTimestamp is relevant
- if (value.ServerTimestamp == DateTime.MinValue)
- {
- value.ServerTimestamp = DateTime.UtcNow;
- }
+ value.ServerTimestamp = readTime;
}
#if DEBUG
if (nodeToRead.AttributeId == Attributes.Value)
diff --git a/Tests/Opc.Ua.Server.Tests/ReferenceServerTest.cs b/Tests/Opc.Ua.Server.Tests/ReferenceServerTest.cs
index fb05909cee..3ce3447ffa 100644
--- a/Tests/Opc.Ua.Server.Tests/ReferenceServerTest.cs
+++ b/Tests/Opc.Ua.Server.Tests/ReferenceServerTest.cs
@@ -543,6 +543,76 @@ namespace Opc.Ua.Server.Tests
"Array SourceTimestamp should be close to the second read time");
}
+ ///
+ /// Test that reading a static variable after a write returns a fresh ServerTimestamp.
+ ///
+ [Test]
+ [NonParallelizable]
+ public async Task ReadStaticVariableStampsFreshServerTimestampAfterWriteAsync()
+ {
+ ITelemetryContext telemetry = NUnitTelemetryContext.Create();
+ ILogger logger = telemetry.CreateLogger();
+
+ NodeId nodeId = ExpandedNodeId.ToNodeId(
+ CommonTestWorkers.NodeIdTestSetStatic[2],
+ m_server.CurrentInstance.NamespaceUris);
+ var nodesToRead = new ReadValueIdCollection
+ {
+ new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value }
+ };
+
+ // 1. Write a value to the static variable
+ var writeNodes = new WriteValueCollection
+ {
+ new WriteValue
+ {
+ NodeId = nodeId,
+ AttributeId = Attributes.Value,
+ Value = new DataValue { WrappedValue = new Variant(42) }
+ }
+ };
+
+ RequestHeader writeRequestHeader = m_requestHeader;
+ writeRequestHeader.Timestamp = DateTime.UtcNow;
+ WriteResponse writeResponse = await m_server.WriteAsync(
+ m_secureChannelContext,
+ writeRequestHeader,
+ writeNodes,
+ CancellationToken.None).ConfigureAwait(false);
+
+ logger.LogInformation("Write result: {StatusCode}", writeResponse.Results[0]);
+ NUnit.Framework.Assert.That(StatusCode.IsGood(writeResponse.Results[0]), Is.True, $"Write failed with: {writeResponse.Results[0]}");
+
+ // Wait a short delay so the subsequent read time is measurably greater than write time
+ await Task.Delay(500).ConfigureAwait(false);
+
+ // 2. Read the variable back
+ RequestHeader readRequestHeader = m_requestHeader;
+ readRequestHeader.Timestamp = DateTime.UtcNow;
+ DateTime timeBeforeRead = DateTime.UtcNow;
+
+ ReadResponse readResponse = await m_server.ReadAsync(
+ m_secureChannelContext,
+ readRequestHeader,
+ kMaxAge,
+ TimestampsToReturn.Both,
+ nodesToRead,
+ CancellationToken.None).ConfigureAwait(false);
+
+ NUnit.Framework.Assert.That(readResponse.Results.Count, Is.EqualTo(1));
+ DataValue readValue = readResponse.Results[0];
+ NUnit.Framework.Assert.That(StatusCode.IsGood(readValue.StatusCode), Is.True);
+
+ logger.LogInformation(
+ "After write read - SourceTimestamp: {SourceTimestamp}, ServerTimestamp: {ServerTimestamp}",
+ readValue.SourceTimestamp,
+ readValue.ServerTimestamp);
+
+ // ServerTimestamp should reflect when the server processed the read, not be stuck at write time
+ NUnit.Framework.Assert.That(readValue.ServerTimestamp, Is.GreaterThanOrEqualTo(timeBeforeRead.AddSeconds(-1)));
+ NUnit.Framework.Assert.That(readValue.ServerTimestamp, Is.GreaterThan(readValue.SourceTimestamp));
+ }
+
///
/// Update static Nodes, read modify write.
///