Files
UA-.NETStandard/samples/Quickstarts.Servers/SampleNodeManager/DataChangeMonitoredItem.cs
Marc Schier 84fa8e8906 CTT follow-up: monitored items, alarms, codecs, history, and model validation (#4002)
# Description

Follow-up to #3978 based on the latest OPC UA CTT aggregate, matrix,
Historical Access, Attribute, and Node Management runs.

## Aggregate run 16

- Ignore synthetic `BadBoundNotFound` raw-bound markers when feeding
aggregate calculators.
- Add direct and live historian regression coverage.

## Multi-dimensional Variant matrices

- Fix `Variant.Copy()` so rank-2 Variant matrices preserve their payload
and dimensions.
- Enforce valid Variant matrix dimensions consistently across Binary,
JSON, XML stream, and XML parser codecs.
- Encoders return `BadEncodingError`; decoders return `BadDecodingError`
for malformed matrix dimensions.
- Add cross-codec valid/null/empty/malformed tests and a batched
ReferenceServer Read regression.
- Correct the previous CTT issue attribution: the invalid
`Scalar_Static_Arrays2D_Variant` server encoding poisoned the batched
CTT Read.

## Node Management AddNodes

- Return `BadNodeIdRejected` for unsupported requested-NodeId
namespaces.
- Validate hierarchical ReferenceType source/target NodeClass
constraints.
- Document run-18 confirmation that all remaining 15 AddNodes errors are
CTT project/script configuration issues.

## Historical Access and Attribute Services run 18

- Reject whitespace in `NumericRange` syntax with
`BadIndexRangeInvalid`.
- Add HistoryRead tests for operation-level invalid syntax and
per-DataValue `BadIndexRangeNoData`.
- Document CTT defects:
- `012.js` expects `BadIndexRangeNoData` at the operation level instead
of on returned DataValues;
  - `Err-012.js` uses a non-historizing node for an access-denied test;
- generic Attribute array helpers omit NodeId[] and StatusCode[]
conversion paths.

## Base Information model validation run 19

- Run the CTT main session as `sysadmin` over Basic256Sha256
SignAndEncrypt so SecurityAdmin RolePermissions and EncryptionRequired
restrictions are both satisfied without weakening the server model.
- Preserve predefined well-known NodeIds while making generated dynamic
type and optional-child factories allocate compatible per-instance
NodeIds, remap internal references, and update references from the
owning subtree.
- Materialize dynamic RoleType properties through generated helpers and
keep role-child string NodeIds disjoint from numeric RoleManager root
IDs.
- Add net48/net10 regressions for generator output, NodeId
allocation/reference remapping, DI mandatory children, dynamic roles,
and low-privilege versus SecurityAdmin Browse visibility.
- Document CTT defects for permission-unaware mandatory-child
validation, the UA 1.04 reference model used against a UA 1.05 server,
stale ServerDiagnostics cache, and the `ConformanceUnits` scalar/array
mismatch.
## Related Issues

- Follow-up to #3960 and #3978.

## Checklist

- [x] I have signed the CLA and read the CONTRIBUTING document.
- [x] Tests prove the fixes and improve coverage.
- [x] Required CTT documentation is updated.
- [x] Focused builds/tests introduce no warnings.
- [x] Affected tests pass on .NET Framework and .NET 10.
- [ ] CI and CodeQL are green.
- [ ] All PR feedback is addressed.



## Latest CTT full run and A&C run 20

- Fix Monitor Value Change V2 `036.js`: an ambiguous
`DataValue(StatusCode)` call encoded `BadIndexRangeNoData` as a Good
value.
- Prove all 19 configured matrix monitored items notify when the
selected value actually changes; document the independent `042.js`
script defects.
- Rebase dynamic `CertificateExpired` / `TrustListOutOfDate` alarm roots
and descendants, restore standard type declarations and ModellingRules,
and expose the cross-node-manager references.
- Deliver method-triggered events to every authorized Session, fixing
the two-Session Confirm scenario.
- Preserve locale-only `LocalizedText` values consistently across
Binary, JSON, and XML and align Acknowledge/Confirm comment behavior and
tests.
- Document the remaining proven CTT defects for TransactionDiagnostics,
SemanticChange, History `013`, PolicyId scope, A&C, durable
subscriptions, and Subscription Minimum events.

## Base synchronization

Merged `master` through `007cfaf90` (`#4010`, repository layout
consolidation) and retained the matrix regression tests at their new
`tests/` locations.

## Latest validation

- Focused merged-tree tests pass on net10 and net48.
- `Opc.Ua.Server` and `Quickstarts.Servers` Release builds complete with
0 warnings and 0 errors on net10 and net48.
- The existing
`DisableEnableViaTypeAndInstanceMethodNoEventsDuringDisabledAsync`
timing test took its documented inconclusive timeout path after the
layout merge; no test failure was reported.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <romanett98@gmail.com>
2026-07-18 05:01:03 +02:00

929 lines
30 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.Extensions.Logging;
using Opc.Ua.Server;
namespace Opc.Ua.Sample
{
/// <summary>
/// Provides a basic monitored item implementation which does not support queuing.
/// </summary>
public class DataChangeMonitoredItem : IDataChangeMonitoredItem2
{
/// <summary>
/// Constructs a new instance.
/// </summary>
public DataChangeMonitoredItem(
MonitoredNode source,
uint id,
uint attributeId,
NumericRange indexRange,
QualifiedName dataEncoding,
DiagnosticsMasks diagnosticsMasks,
TimestampsToReturn timestampsToReturn,
MonitoringMode monitoringMode,
uint clientHandle,
double samplingInterval,
bool alwaysReportUpdates)
{
m_source = source;
Id = id;
AttributeId = attributeId;
IndexRange = indexRange;
DataEncoding = dataEncoding;
m_timestampsToReturn = timestampsToReturn;
m_diagnosticsMasks = diagnosticsMasks;
MonitoringMode = monitoringMode;
ClientHandle = clientHandle;
m_samplingInterval = samplingInterval;
m_nextSampleTime = DateTime.UtcNow.Ticks;
m_readyToPublish = false;
m_readyToTrigger = false;
m_resendData = false;
AlwaysReportUpdates = alwaysReportUpdates;
NodeId = source.Node.NodeId;
}
/// <summary>
/// Constructs a new instance.
/// </summary>
public DataChangeMonitoredItem(
IMonitoredItemQueueFactory monitoredItemQueueFactory,
MonitoredNode source,
uint id,
uint attributeId,
NumericRange indexRange,
QualifiedName dataEncoding,
DiagnosticsMasks diagnosticsMasks,
TimestampsToReturn timestampsToReturn,
MonitoringMode monitoringMode,
uint clientHandle,
double samplingInterval,
uint queueSize,
bool discardOldest,
DataChangeFilter? filter,
Range? range,
bool alwaysReportUpdates)
{
m_source = source;
m_monitoredItemQueueFactory = monitoredItemQueueFactory;
Id = id;
AttributeId = attributeId;
IndexRange = indexRange;
DataEncoding = dataEncoding;
m_timestampsToReturn = timestampsToReturn;
m_diagnosticsMasks = diagnosticsMasks;
MonitoringMode = monitoringMode;
ClientHandle = clientHandle;
m_samplingInterval = samplingInterval;
m_nextSampleTime = DateTime.UtcNow.Ticks;
m_readyToPublish = false;
m_readyToTrigger = false;
m_resendData = false;
m_queue = null;
m_queueSize = queueSize;
DataChangeFilter = filter;
m_range = 0;
AlwaysReportUpdates = alwaysReportUpdates;
NodeId = source.Node.NodeId;
if (range != null)
{
m_range = range.High - range.Low;
}
if (queueSize > 1)
{
m_queue = new DataChangeQueueHandler(
id,
false,
m_monitoredItemQueueFactory,
source.Server.Telemetry);
m_queue.SetQueueSize(queueSize, discardOldest, diagnosticsMasks);
m_queue.SetSamplingInterval(samplingInterval);
}
}
/// <summary>
/// Constructs a new instance from a template.
/// </summary>
public DataChangeMonitoredItem(
ISubscriptionStore subscriptionStore,
IMonitoredItemQueueFactory monitoredItemQueueFactory,
MonitoredNode source,
IStoredMonitoredItem storedMonitoredItem)
{
m_source = source;
m_monitoredItemQueueFactory = monitoredItemQueueFactory;
Id = storedMonitoredItem.Id;
AttributeId = storedMonitoredItem.AttributeId;
IndexRange = storedMonitoredItem.ParsedIndexRange;
DataEncoding = storedMonitoredItem.Encoding;
m_timestampsToReturn = storedMonitoredItem.TimestampsToReturn;
m_diagnosticsMasks = storedMonitoredItem.DiagnosticsMasks;
MonitoringMode = storedMonitoredItem.MonitoringMode;
ClientHandle = storedMonitoredItem.ClientHandle;
m_samplingInterval = storedMonitoredItem.SamplingInterval;
m_nextSampleTime = DateTime.UtcNow.Ticks;
m_readyToPublish = false;
m_readyToTrigger = false;
m_resendData = false;
m_queue = null;
m_queueSize = storedMonitoredItem.QueueSize;
DataChangeFilter = storedMonitoredItem.FilterToUse as DataChangeFilter;
m_range = storedMonitoredItem.Range;
AlwaysReportUpdates = storedMonitoredItem.AlwaysReportUpdates;
m_lastValue = storedMonitoredItem.LastValue;
m_lastError = storedMonitoredItem.LastError;
NodeId = storedMonitoredItem.NodeId;
if (storedMonitoredItem.QueueSize > 1)
{
IDataChangeMonitoredItemQueue queue = subscriptionStore
.RestoreDataChangeMonitoredItemQueue(
storedMonitoredItem.Id);
if (queue != null)
{
m_queue = new DataChangeQueueHandler(
queue,
storedMonitoredItem.DiscardOldest,
storedMonitoredItem.SamplingInterval,
source.Server.Telemetry);
}
else
{
m_queue = new DataChangeQueueHandler(
storedMonitoredItem.Id,
false,
m_monitoredItemQueueFactory,
source.Server.Telemetry);
m_queue.SetQueueSize(
storedMonitoredItem.QueueSize,
storedMonitoredItem.DiscardOldest,
storedMonitoredItem.DiagnosticsMasks);
m_queue.SetSamplingInterval(storedMonitoredItem.SamplingInterval);
}
}
}
/// <summary>
/// Gets the id for the attribute being monitored.
/// </summary>
public uint AttributeId { get; }
/// <summary>
/// Gets the index range used to selected a subset of the value.
/// </summary>
public NumericRange IndexRange { get; }
/// <summary>
/// Gets the data encoding to use when returning the value.
/// </summary>
public QualifiedName DataEncoding { get; }
/// <summary>
/// Whether the monitored item should report a value without checking if it was changed.
/// </summary>
public bool AlwaysReportUpdates { get; set; }
/// <summary>
/// The number of milliseconds until the next sample.
/// </summary>
public int TimeToNextSample
{
get
{
lock (m_lock)
{
if (MonitoringMode == MonitoringMode.Disabled)
{
return int.MaxValue;
}
DateTime now = DateTime.UtcNow;
if (m_nextSampleTime <= now.Ticks)
{
return 0;
}
return (int)((m_nextSampleTime - now.Ticks) / TimeSpan.TicksPerMillisecond);
}
}
}
/// <summary>
/// The monitoring mode.
/// </summary>
public MonitoringMode MonitoringMode { get; private set; }
/// <summary>
/// The sampling interval.
/// </summary>
public double SamplingInterval
{
get
{
lock (m_lock)
{
return m_samplingInterval;
}
}
}
/// <summary>
/// Modifies the monitored item parameters,
/// </summary>
public ServiceResult Modify(
DiagnosticsMasks diagnosticsMasks,
TimestampsToReturn timestampsToReturn,
uint clientHandle,
double samplingInterval)
{
return Modify(
diagnosticsMasks,
timestampsToReturn,
clientHandle,
samplingInterval,
0,
false,
filter: null,
range: null);
}
/// <summary>
/// Modifies the monitored item parameters,
/// </summary>
public ServiceResult Modify(
DiagnosticsMasks diagnosticsMasks,
TimestampsToReturn timestampsToReturn,
uint clientHandle,
double samplingInterval,
uint queueSize,
bool discardOldest,
DataChangeFilter? filter,
Range? range)
{
lock (m_lock)
{
m_diagnosticsMasks = diagnosticsMasks;
m_timestampsToReturn = timestampsToReturn;
ClientHandle = clientHandle;
m_queueSize = queueSize;
// subtract the previous sampling interval.
long oldSamplingInterval = (long)(m_samplingInterval *
TimeSpan.TicksPerMillisecond);
if (oldSamplingInterval < m_nextSampleTime)
{
m_nextSampleTime -= oldSamplingInterval;
}
m_samplingInterval = samplingInterval;
// calculate the next sampling interval.
long newSamplingInterval = (long)(m_samplingInterval *
TimeSpan.TicksPerMillisecond);
if (m_samplingInterval > 0)
{
m_nextSampleTime += newSamplingInterval;
}
else
{
m_nextSampleTime = 0;
}
// update the filter and the range.
DataChangeFilter = filter;
m_range = 0;
if (range != null)
{
m_range = range.High - range.Low;
}
// update the queue size.
if (queueSize > 1)
{
m_queue ??= new DataChangeQueueHandler(
Id,
false,
m_monitoredItemQueueFactory!,
m_source.Server.Telemetry);
m_queue.SetQueueSize(
queueSize,
discardOldest,
diagnosticsMasks);
m_queue.SetSamplingInterval(samplingInterval);
}
else
{
m_queue = null;
}
return ServiceResult.Good;
}
}
/// <summary>
/// Called when the attribute being monitored changed. Reads and queues the value.
/// </summary>
public void ValueChanged(ISystemContext context)
{
var value = new DataValue();
ServiceResult error = m_source.Node
.ReadAttribute(context, AttributeId, default, default, ref value);
if (ServiceResult.IsBad(error))
{
value = DataValue.FromStatusCode(error.StatusCode);
}
value = value.WithServerTimestamp(DateTimeUtc.Now);
QueueValue(value, error, false);
}
/// <summary>
/// The node manager for the monitored item.
/// </summary>
public IAsyncNodeManager NodeManager => m_source.NodeManager;
/// <summary>
/// The session for the monitored item.
/// </summary>
public ISession Session
{
get
{
ISubscription subscription = SubscriptionCallback;
if (subscription != null)
{
return subscription.Session;
}
return null!;
}
}
/// <summary>
/// The monitored items owner identity.
/// </summary>
public IUserIdentity EffectiveIdentity
{
get
{
ISubscription subscription = SubscriptionCallback;
return (subscription?.EffectiveIdentity)!;
}
}
/// <summary>
/// The identifier for the subscription that the monitored item belongs to.
/// </summary>
public uint SubscriptionId
{
get
{
ISubscription subscription = SubscriptionCallback;
if (subscription != null)
{
return subscription.Id;
}
return 0;
}
}
/// <summary>
/// The unique identifier for the monitored item.
/// </summary>
public uint Id { get; }
/// <summary>
/// The client handle.
/// </summary>
public uint ClientHandle { get; private set; }
/// <summary>
/// The callback to use to notify the subscription when values are ready to publish.
/// </summary>
public ISubscription SubscriptionCallback { get; set; } = null!;
/// <summary>
/// The handle assigned to the monitored item by the node manager.
/// </summary>
public object ManagerHandle => m_source;
/// <summary>
/// The type of monitor item.
/// </summary>
public int MonitoredItemType => MonitoredItemTypeMask.DataChange;
/// <summary>
/// Returns true if the item is ready to publish.
/// </summary>
public bool IsReadyToPublish
{
get
{
lock (m_lock)
{
// check if not ready to publish.
if (!m_readyToPublish)
{
return false;
}
// check if monitoring was turned off.
if (MonitoringMode != MonitoringMode.Reporting)
{
return false;
}
// re-queue if too little time has passed since the last publish.
long now = DateTime.UtcNow.Ticks;
return m_nextSampleTime <= now;
}
}
}
/// <summary>
/// Gets or Sets a value indicating whether the item is ready to trigger in case it has some linked items.
/// </summary>
public bool IsReadyToTrigger
{
get
{
lock (m_lock)
{
// only allow to trigger if sampling or reporting.
if (MonitoringMode == MonitoringMode.Disabled)
{
return false;
}
return m_readyToTrigger;
}
}
set
{
lock (m_lock)
{
m_readyToTrigger = value;
}
}
}
/// <inheritdoc/>
public bool IsResendData
{
get
{
lock (m_lock)
{
return m_resendData;
}
}
}
/// <summary>
/// Returns the results for the create request.
/// </summary>
public ServiceResult GetCreateResult(out MonitoredItemCreateResult result)
{
lock (m_lock)
{
result = new MonitoredItemCreateResult
{
MonitoredItemId = Id,
StatusCode = StatusCodes.Good,
RevisedSamplingInterval = m_samplingInterval,
RevisedQueueSize = 0,
FilterResult = default
};
if (m_queue != null)
{
result.RevisedQueueSize = m_queueSize;
}
return ServiceResult.Good;
}
}
/// <summary>
/// Returns the results for the modify request.
/// </summary>
public ServiceResult GetModifyResult(out MonitoredItemModifyResult result)
{
lock (m_lock)
{
result = new MonitoredItemModifyResult
{
StatusCode = StatusCodes.Good,
RevisedSamplingInterval = m_samplingInterval,
RevisedQueueSize = 0,
FilterResult = default
};
if (m_queue != null)
{
result.RevisedQueueSize = m_queueSize;
}
return ServiceResult.Good;
}
}
/// <inheritdoc/>
public void SetupResendDataTrigger()
{
lock (m_lock)
{
if (MonitoringMode == MonitoringMode.Reporting)
{
m_resendData = true;
}
}
}
/// <inheritdoc/>
public IStoredMonitoredItem ToStorableMonitoredItem()
{
return new StoredMonitoredItem
{
SamplingInterval = m_samplingInterval,
SubscriptionId = SubscriptionCallback.Id,
QueueSize = m_queueSize,
AlwaysReportUpdates = AlwaysReportUpdates,
AttributeId = AttributeId,
ClientHandle = ClientHandle,
DiagnosticsMasks = m_diagnosticsMasks,
IsDurable = false,
Encoding = DataEncoding,
FilterToUse = DataChangeFilter!,
Id = Id,
LastError = m_lastError!,
LastValue = m_lastValue,
MonitoringMode = MonitoringMode,
NodeId = m_source.Node.NodeId,
OriginalFilter = DataChangeFilter!,
Range = m_range,
TimestampsToReturn = m_timestampsToReturn,
ParsedIndexRange = IndexRange
};
}
/// <inheritdoc/>
public void QueueValue(in DataValue value, ServiceResult? error)
{
QueueValue(in value, error, false);
}
/// <inheritdoc/>
public void QueueValue(in DataValue value, ServiceResult? error, bool ignoreFilters)
{
lock (m_lock)
{
// check if value has changed.
if (!AlwaysReportUpdates &&
!ignoreFilters &&
!MonitoredItem.ValueChanged(
value,
error!,
m_lastValue,
m_lastError!,
DataChangeFilter!,
m_range))
{
return;
}
DataValue current = value;
// make a shallow copy of the value.
if (!current.IsNull)
{
current = new DataValue(
current.WrappedValue,
current.StatusCode,
current.SourceTimestamp,
current.ServerTimestamp,
current.SourcePicoseconds,
current.ServerPicoseconds);
// ensure the data value matches the error status code.
if (error != null && error.StatusCode.Code != 0)
{
current = current.WithStatus(error.StatusCode);
}
}
m_lastValue = current;
m_lastError = error;
// queue value.
m_queue?.QueueValue(current, error!);
// flag the item as ready to publish.
m_readyToPublish = true;
m_readyToTrigger = true;
}
}
/// <summary>
/// Sets a flag indicating that the semantics for the monitored node have changed.
/// </summary>
/// <remarks>
/// The StatusCode for next value reported by the monitored item will have the SemanticsChanged bit set.
/// </remarks>
public void SetSemanticsChanged()
{
lock (m_lock)
{
m_semanticsChanged = true;
}
}
/// <summary>
/// Sets a flag indicating that the structure of the monitored node has changed.
/// </summary>
/// <remarks>
/// The StatusCode for next value reported by the monitored item will have the StructureChanged bit set.
/// </remarks>
public void SetStructureChanged()
{
lock (m_lock)
{
m_structureChanged = true;
}
}
/// <summary>
/// Changes the monitoring mode.
/// </summary>
public MonitoringMode SetMonitoringMode(MonitoringMode monitoringMode)
{
lock (m_lock)
{
MonitoringMode previousMode = MonitoringMode;
if (previousMode == monitoringMode)
{
return previousMode;
}
if (previousMode == MonitoringMode.Disabled)
{
m_nextSampleTime = DateTime.UtcNow.Ticks;
m_lastError = null;
m_lastValue = DataValue.Null;
}
MonitoringMode = monitoringMode;
if (monitoringMode == MonitoringMode.Disabled)
{
m_readyToPublish = false;
m_readyToTrigger = false;
}
return previousMode;
}
}
/// <summary>
/// No filters supported.
/// </summary>
public DataChangeFilter? DataChangeFilter { get; private set; }
public bool IsDurable => false;
public NodeId NodeId { get; }
/// <summary>
/// Increments the sample time to the next interval.
/// </summary>
private void IncrementSampleTime()
{
// update next sample time.
long now = DateTime.UtcNow.Ticks;
long samplingInterval = (long)(m_samplingInterval * TimeSpan.TicksPerMillisecond);
if (m_nextSampleTime > 0)
{
long delta = now - m_nextSampleTime;
if (samplingInterval > 0 && delta >= 0)
{
m_nextSampleTime += ((delta / samplingInterval) + 1) * samplingInterval;
}
}
// set sampling time based on current time.
else
{
m_nextSampleTime = now + samplingInterval;
}
}
/// <summary>
/// Called by the subscription to publish any notification.
/// </summary>
public bool Publish(
OperationContext context,
Queue<MonitoredItemNotification> notifications,
Queue<DiagnosticInfo> diagnostics,
uint maxNotificationsPerPublish,
ILogger logger)
{
lock (m_lock)
{
// check if not ready to publish.
if (!IsReadyToPublish)
{
if (!m_resendData)
{
return false;
}
}
else
{
// update sample time.
IncrementSampleTime();
}
// check if queuing is enabled.
if (m_queue != null && (!m_resendData || m_queue.ItemsInQueue != 0))
{
uint notificationCount = 0;
while (
notificationCount < maxNotificationsPerPublish &&
m_queue.PublishSingleValue(out DataValue value, out ServiceResult error))
{
Publish(context, value, error, notifications, diagnostics, logger);
notificationCount++;
if (m_resendData)
{
break;
}
}
}
else
{
Publish(context, m_lastValue, m_lastError, notifications, diagnostics, logger);
}
bool moreValuesToPublish = m_queue?.ItemsInQueue > 0;
// update flags
m_readyToPublish = moreValuesToPublish;
m_readyToTrigger = moreValuesToPublish;
m_resendData = false;
return moreValuesToPublish;
}
}
/// <summary>
/// Publishes a value.
/// </summary>
private void Publish(
OperationContext context,
in DataValue value,
ServiceResult? error,
Queue<MonitoredItemNotification> notifications,
Queue<DiagnosticInfo> diagnostics,
ILogger logger)
{
DataValue current = value;
// set semantics changed bit.
if (m_semanticsChanged)
{
if (!current.IsNull)
{
current = current.WithStatus(current.StatusCode.SetSemanticsChanged(true));
}
m_semanticsChanged = false;
}
// set structure changed bit.
if (m_structureChanged)
{
if (!current.IsNull)
{
current = current.WithStatus(current.StatusCode.SetStructureChanged(true));
}
m_structureChanged = false;
}
// copy data value.
var item = new MonitoredItemNotification { ClientHandle = ClientHandle, Value = current };
// apply timestamp filter.
if (m_timestampsToReturn is not TimestampsToReturn.Server and not TimestampsToReturn.Both)
{
item.Value = item.Value.WithServerTimestamp(DateTimeUtc.MinValue);
}
if (m_timestampsToReturn is not TimestampsToReturn.Source and not TimestampsToReturn.Both)
{
item.Value = item.Value.WithSourceTimestamp(DateTimeUtc.MinValue);
}
notifications.Enqueue(item);
// update diagnostic info.
DiagnosticInfo? diagnosticInfo = null;
if (m_lastError != null && (m_diagnosticsMasks & DiagnosticsMasks.OperationAll) != 0)
{
diagnosticInfo = ServerUtils.CreateDiagnosticInfo(
m_source.Server,
context,
m_lastError,
logger);
}
diagnostics.Enqueue(diagnosticInfo!);
}
/// <inheritdoc/>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// An overrideable version of the Dispose.
/// </summary>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
m_queue?.Dispose();
}
}
private readonly Lock m_lock = new();
private readonly IMonitoredItemQueueFactory? m_monitoredItemQueueFactory;
private readonly MonitoredNode m_source;
private DataValue m_lastValue;
private ServiceResult? m_lastError;
private TimestampsToReturn m_timestampsToReturn;
private DiagnosticsMasks m_diagnosticsMasks;
private double m_samplingInterval;
private DataChangeQueueHandler? m_queue;
private uint m_queueSize;
private double m_range;
private long m_nextSampleTime;
private bool m_readyToPublish;
private bool m_readyToTrigger;
private bool m_semanticsChanged;
private bool m_structureChanged;
private bool m_resendData;
}
}