Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4044,7 +4044,6 @@ private DataItemState CreateDataItemVariable(
variable.Historizing = false;
variable.Value = TypeInfo.GetDefaultValue((uint)dataType, valueRank, Server.TypeTree);
variable.StatusCode = StatusCodes.Good;
variable.Timestamp = DateTime.UtcNow;

if (valueRank == ValueRanks.OneDimension)
{
Expand Down Expand Up @@ -4176,7 +4175,6 @@ private AnalogItemState CreateAnalogItemVariable(
TypeInfo.GetDefaultValue(dataType, valueRank, Server.TypeTree);

variable.StatusCode = StatusCodes.Good;
variable.Timestamp = DateTime.UtcNow;
// The latest UNECE version (Rev 11, published in 2015) is available here:
// http://www.opcfoundation.org/UA/EngineeringUnits/UNECE/rec20_latest_08052015.zip
variable.EngineeringUnits.Value = new EUInformation(
Expand Down Expand Up @@ -4233,7 +4231,6 @@ private TwoStateDiscreteState CreateTwoStateDiscreteItemVariable(
variable.Historizing = false;
variable.Value = (bool)GetNewValue(variable);
variable.StatusCode = StatusCodes.Good;
variable.Timestamp = DateTime.UtcNow;

variable.TrueState.Value = trueState;
variable.TrueState.AccessLevel = AccessLevels.CurrentReadOrWrite;
Expand Down Expand Up @@ -4277,7 +4274,6 @@ private MultiStateDiscreteState CreateMultiStateDiscreteItemVariable(
variable.Historizing = false;
variable.Value = (uint)0;
variable.StatusCode = StatusCodes.Good;
variable.Timestamp = DateTime.UtcNow;
variable.OnWriteValue = OnWriteDiscrete;

var strings = new LocalizedText[values.Length];
Expand Down Expand Up @@ -4338,7 +4334,6 @@ private MultiStateValueDiscreteState CreateMultiStateValueDiscreteItemVariable(
variable.Historizing = false;
variable.Value = (uint)0;
variable.StatusCode = StatusCodes.Good;
variable.Timestamp = DateTime.UtcNow;
variable.OnWriteValue = OnWriteValueDiscrete;

// there are two enumerations for this type:
Expand Down Expand Up @@ -4600,7 +4595,6 @@ private BaseDataVariableState CreateVariable(
};
variable.Value = GetNewValue(variable);
variable.StatusCode = StatusCodes.Good;
variable.Timestamp = DateTime.UtcNow;

if (valueRank == ValueRanks.OneDimension)
{
Expand Down
6 changes: 5 additions & 1 deletion Libraries/Opc.Ua.Client/Session/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3064,6 +3064,10 @@ private async Task OnSendKeepAliveAsync(
{
// This should not happen, but we fail gracefully anyway
}
catch (TaskCanceledException)
{
//expected exception type
}
catch (Exception e)
{
m_logger.LogError(
Expand Down Expand Up @@ -3512,7 +3516,7 @@ private void OnPublishComplete(
if (m_subscriptions.Count == 0)
{
// Publish responses with error should occur after deleting the last subscription.
m_logger.LogError(
m_logger.LogWarning(
"Publish #{RequestHandle}, Subscription count = 0, Error: {Message}",
requestHeader.RequestHandle,
e.Message);
Expand Down
7 changes: 5 additions & 2 deletions Stack/Opc.Ua.Types/State/BaseVariableState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1544,11 +1544,14 @@ protected override ServiceResult ReadValueAttribute(
// ensure a value timestamp exists.
if (m_timestamp == DateTime.MinValue)
{
m_timestamp = DateTime.UtcNow;
sourceTimestamp = DateTime.UtcNow;
}
else
{
sourceTimestamp = m_timestamp;
}

value = m_value;
sourceTimestamp = m_timestamp;
StatusCode statusCode = m_statusCode;

ServiceResult result = null;
Expand Down
4 changes: 3 additions & 1 deletion Tests/Opc.Ua.Client.Tests/ClientFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,9 @@ private void Session_KeepAlive(ISession session, KeepAliveEventArgs e)
{
// Ignore expected errors during test shutdown to reduce noise in CI logs
if (e.Status?.StatusCode == StatusCodes.BadServerHalted ||
e.Status?.StatusCode == StatusCodes.BadNoCommunication)
e.Status?.StatusCode == StatusCodes.BadNoCommunication||
e.Status?.StatusCode == StatusCodes.BadSecureChannelClosed ||
e.Status?.StatusCode == StatusCodes.BadRequestInterrupted)
{
return;
}
Expand Down
146 changes: 146 additions & 0 deletions Tests/Opc.Ua.Server.Tests/ReferenceServerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1181,5 +1181,151 @@ public async Task ProvisioningModeTestAsync()
// Clean up
await fixture.StopAsync().ConfigureAwait(false);
}

/// <summary>
/// Test that ReferenceNodeManager variables update their SourceTimestamp on read.
/// </summary>
[Test]
public async Task ReferenceNodeManagerVariablesUpdateTimestampOnReadAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ILogger logger = telemetry.CreateLogger<ReferenceServerTests>();

// Read a variable from the ReferenceNodeManager (namespace index 2)
var nodeId = new NodeId("Scalar_Static_Byte", 2);
var nodesToRead = new ReadValueIdCollection
{
new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value }
};

// First read
RequestHeader requestHeader = m_requestHeader;
requestHeader.Timestamp = DateTime.UtcNow;
DateTime timeBeforeFirstRead = DateTime.UtcNow;
ReadResponse firstReadResponse = await m_server.ReadAsync(
m_secureChannelContext,
requestHeader,
kMaxAge,
TimestampsToReturn.Both,
nodesToRead,
CancellationToken.None).ConfigureAwait(false);

Assert.IsNotNull(firstReadResponse);
Assert.IsNotNull(firstReadResponse.Results);
Assert.AreEqual(1, firstReadResponse.Results.Count);
DataValue firstValue = firstReadResponse.Results[0];
Assert.AreEqual(StatusCodes.Good, firstValue.StatusCode);
Assert.IsNotNull(firstValue.SourceTimestamp);
logger.LogInformation("First read - SourceTimestamp: {SourceTimestamp}, ServerTimestamp: {ServerTimestamp}",
firstValue.SourceTimestamp, firstValue.ServerTimestamp);

// Verify the timestamp is recent (not startup time)
Assert.GreaterOrEqual(firstValue.SourceTimestamp, timeBeforeFirstRead.AddSeconds(-1),
"SourceTimestamp should be close to the read time, not the server startup time");

// Wait a bit to ensure time difference
await Task.Delay(1500).ConfigureAwait(false);

// Second read
requestHeader.Timestamp = DateTime.UtcNow;
DateTime timeBeforeSecondRead = DateTime.UtcNow;
ReadResponse secondReadResponse = await m_server.ReadAsync(
m_secureChannelContext,
requestHeader,
kMaxAge,
TimestampsToReturn.Both,
nodesToRead,
CancellationToken.None).ConfigureAwait(false);

Assert.IsNotNull(secondReadResponse);
Assert.IsNotNull(secondReadResponse.Results);
Assert.AreEqual(1, secondReadResponse.Results.Count);
DataValue secondValue = secondReadResponse.Results[0];
Assert.AreEqual(StatusCodes.Good, secondValue.StatusCode);
Assert.IsNotNull(secondValue.SourceTimestamp);
logger.LogInformation("Second read - SourceTimestamp: {SourceTimestamp}, ServerTimestamp: {ServerTimestamp}",
secondValue.SourceTimestamp, secondValue.ServerTimestamp);

// Verify the second timestamp is more recent than the first
Assert.Greater(secondValue.SourceTimestamp, firstValue.SourceTimestamp,
"SourceTimestamp should be updated on each read");

// Verify the second timestamp is recent
Assert.GreaterOrEqual(secondValue.SourceTimestamp, timeBeforeSecondRead.AddSeconds(-1),
"SourceTimestamp should be close to the second read time");
}

/// <summary>
/// Test that ReferenceNodeManager array variables update their SourceTimestamp on read.
/// </summary>
[Test]
public async Task ReferenceNodeManagerArrayVariablesUpdateTimestampOnReadAsync()
{
ITelemetryContext telemetry = NUnitTelemetryContext.Create();
ILogger logger = telemetry.CreateLogger<ReferenceServerTests>();

// Read an array variable from the ReferenceNodeManager (namespace index 2)
var nodeId = new NodeId("Scalar_Static_Arrays_Byte", 2);
var nodesToRead = new ReadValueIdCollection
{
new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value }
};

// First read
RequestHeader requestHeader = m_requestHeader;
requestHeader.Timestamp = DateTime.UtcNow;
DateTime timeBeforeFirstRead = DateTime.UtcNow;
ReadResponse firstReadResponse = await m_server.ReadAsync(
m_secureChannelContext,
requestHeader,
kMaxAge,
TimestampsToReturn.Both,
nodesToRead,
CancellationToken.None).ConfigureAwait(false);

Assert.IsNotNull(firstReadResponse);
Assert.IsNotNull(firstReadResponse.Results);
Assert.AreEqual(1, firstReadResponse.Results.Count);
DataValue firstValue = firstReadResponse.Results[0];
Assert.AreEqual(StatusCodes.Good, firstValue.StatusCode);
Assert.IsNotNull(firstValue.SourceTimestamp);
logger.LogInformation("Array First read - SourceTimestamp: {SourceTimestamp}, ServerTimestamp: {ServerTimestamp}",
firstValue.SourceTimestamp, firstValue.ServerTimestamp);

// Verify the timestamp is recent (not startup time)
Assert.GreaterOrEqual(firstValue.SourceTimestamp, timeBeforeFirstRead.AddSeconds(-1),
"Array SourceTimestamp should be close to the read time, not the server startup time");

// Wait a bit to ensure time difference
await Task.Delay(1500).ConfigureAwait(false);

// Second read
requestHeader.Timestamp = DateTime.UtcNow;
DateTime timeBeforeSecondRead = DateTime.UtcNow;
ReadResponse secondReadResponse = await m_server.ReadAsync(
m_secureChannelContext,
requestHeader,
kMaxAge,
TimestampsToReturn.Both,
nodesToRead,
CancellationToken.None).ConfigureAwait(false);

Assert.IsNotNull(secondReadResponse);
Assert.IsNotNull(secondReadResponse.Results);
Assert.AreEqual(1, secondReadResponse.Results.Count);
DataValue secondValue = secondReadResponse.Results[0];
Assert.AreEqual(StatusCodes.Good, secondValue.StatusCode);
Assert.IsNotNull(secondValue.SourceTimestamp);
logger.LogInformation("Array Second read - SourceTimestamp: {SourceTimestamp}, ServerTimestamp: {ServerTimestamp}",
secondValue.SourceTimestamp, secondValue.ServerTimestamp);

// Verify the second timestamp is more recent than the first
Assert.Greater(secondValue.SourceTimestamp, firstValue.SourceTimestamp,
"Array SourceTimestamp should be updated on each read");

// Verify the second timestamp is recent
Assert.GreaterOrEqual(secondValue.SourceTimestamp, timeBeforeSecondRead.AddSeconds(-1),
"Array SourceTimestamp should be close to the second read time");
}
}
}
Loading