/* ======================================================================== * 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.Concurrent; using System.Threading; using System.Threading.Tasks; namespace Opc.Ua.Server { /// /// Manages the montioredItems for a NodeManager /// public class SamplingGroupMonitoredItemManager : IMonitoredItemManager { /// public SamplingGroupMonitoredItemManager( IAsyncNodeManager nodeManager, IServerInternal server, ApplicationConfiguration configuration) { m_samplingGroupManager = new SamplingGroupManager( server, nodeManager, (uint)configuration.ServerConfiguration!.MaxNotificationQueueSize, (uint)configuration.ServerConfiguration.MaxDurableNotificationQueueSize, configuration.ServerConfiguration.AvailableSamplingRates.ToArray()!); m_nodeManager = nodeManager; m_server = server; MonitoredNodes = []; MonitoredItems = new ConcurrentDictionary(); } /// public NodeIdDictionary MonitoredNodes { get; } /// public ConcurrentDictionary MonitoredItems { get; } /// public ISampledDataChangeMonitoredItem CreateMonitoredItem( IServerInternal server, IAsyncNodeManager nodeManager, ServerSystemContext context, NodeHandle handle, uint subscriptionId, double publishingInterval, DiagnosticsMasks diagnosticsMasks, TimestampsToReturn timestampsToReturn, MonitoredItemCreateRequest itemToCreate, Range euRange, MonitoringFilter filterToUse, double samplingInterval, uint revisedQueueSize, bool createDurable, MonitoredItemIdFactory monitoredItemIdFactory, Func addNodeToComponentCache) { // set min sampling interval if 0 if (samplingInterval.CompareTo(0.0) == 0) { samplingInterval = 1; } uint monitoredItemId = monitoredItemIdFactory.GetNextId(); // create monitored item. ISampledDataChangeMonitoredItem monitoredItem = m_samplingGroupManager.CreateMonitoredItem( context.OperationContext!, subscriptionId, publishingInterval, timestampsToReturn, monitoredItemId, handle, itemToCreate, euRange, samplingInterval, createDurable); // save the monitored item. MonitoredItems.AddOrUpdate( monitoredItemId, monitoredItem, (key, oldValue) => monitoredItem); return monitoredItem; } /// public void ApplyChanges() { // update all groups with any new items. m_samplingGroupManager.ApplyChanges(); } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// An overrideable version of the Dispose. /// protected virtual void Dispose(bool disposing) { if (disposing) { m_samplingGroupManager?.Dispose(); } } /// public StatusCode DeleteMonitoredItem( ServerSystemContext context, ISampledDataChangeMonitoredItem monitoredItem, NodeHandle handle) { // validate monitored item. if (!MonitoredItems.TryGetValue( monitoredItem.Id, out IMonitoredItem? existingMonitoredItem)) { return StatusCodes.BadMonitoredItemIdInvalid; } if (!ReferenceEquals(monitoredItem, existingMonitoredItem)) { return StatusCodes.BadMonitoredItemIdInvalid; } // remove item. m_samplingGroupManager.StopMonitoring(monitoredItem); // remove association with the group. MonitoredItems.TryRemove(monitoredItem.Id, out _); // delete successful. return StatusCodes.Good; } /// public ServiceResult? ModifyMonitoredItem( ServerSystemContext context, DiagnosticsMasks diagnosticsMasks, TimestampsToReturn timestampsToReturn, MonitoringFilter filterToUse, Range euRange, double samplingInterval, uint revisedQueueSize, ISampledDataChangeMonitoredItem monitoredItem, MonitoredItemModifyRequest itemToModify) { // validate monitored item. if (!MonitoredItems.TryGetValue( monitoredItem.Id, out IMonitoredItem? existingMonitoredItem)) { return StatusCodes.BadMonitoredItemIdInvalid; } if (!ReferenceEquals(monitoredItem, existingMonitoredItem)) { return StatusCodes.BadMonitoredItemIdInvalid; } return m_samplingGroupManager.ModifyMonitoredItem( context.OperationContext!, timestampsToReturn, monitoredItem, itemToModify, euRange); } /// public ValueTask<(ServiceResult, MonitoringMode?)> SetMonitoringModeAsync( ServerSystemContext context, ISampledDataChangeMonitoredItem monitoredItem, MonitoringMode monitoringMode, NodeHandle handle, CancellationToken cancellationToken = default) { if (!MonitoredItems.TryGetValue( monitoredItem.Id, out IMonitoredItem? existingMonitoredItem)) { return new ValueTask<(ServiceResult, MonitoringMode?)>((StatusCodes.BadMonitoredItemIdInvalid, null)); } if (!ReferenceEquals(monitoredItem, existingMonitoredItem)) { return new ValueTask<(ServiceResult, MonitoringMode?)>((StatusCodes.BadMonitoredItemIdInvalid, null)); } // update monitoring mode. MonitoringMode previousMode = monitoredItem.SetMonitoringMode(monitoringMode); // need to provide an immediate update after enabling. if (previousMode == MonitoringMode.Disabled && monitoringMode != MonitoringMode.Disabled) { var initialValue = new DataValue( Variant.Null, StatusCodes.BadWaitingForInitialData, DateTimeUtc.MinValue, DateTime.UtcNow); // read the initial value. if (monitoredItem.ManagerHandle is Node node) { ServiceResult error = node.Read( context, monitoredItem.AttributeId, ref initialValue); if (ServiceResult.IsBad(error)) { initialValue = initialValue .WithWrappedValue(default) .WithStatus(error.StatusCode); } } monitoredItem.QueueValue(initialValue, null); } return new ValueTask<(ServiceResult, MonitoringMode?)>((StatusCodes.Good, previousMode)); } /// public bool RestoreMonitoredItem( IServerInternal server, IAsyncNodeManager nodeManager, ServerSystemContext context, NodeHandle handle, IStoredMonitoredItem storedMonitoredItem, IUserIdentity savedOwnerIdentity, Func addNodeToComponentCache, out ISampledDataChangeMonitoredItem monitoredItem) { // create monitored item. monitoredItem = m_samplingGroupManager.RestoreMonitoredItem( handle, storedMonitoredItem, savedOwnerIdentity); // save monitored item. MonitoredItems.TryAdd(monitoredItem.Id, monitoredItem); return true; } /// public (MonitoredNode2?, ServiceResult) SubscribeToEvents( ServerSystemContext context, NodeState source, IEventMonitoredItem monitoredItem, bool unsubscribe) { MonitoredNode2? monitoredNode; // handle unsubscribe. if (unsubscribe) { // check for existing monitored node. if (!MonitoredNodes.TryGetValue(source.NodeId, out monitoredNode)) { return (null, StatusCodes.BadNodeIdUnknown); } monitoredNode.Remove(monitoredItem); MonitoredItems.TryRemove(monitoredItem.Id, out _); // check if node is no longer being monitored. if (!monitoredNode.HasMonitoredItems) { MonitoredNodes.Remove(source.NodeId); } return (monitoredNode, ServiceResult.Good); } // only objects or views can be subscribed to. if (source is not BaseObjectState instance || (instance.EventNotifier & EventNotifiers.SubscribeToEvents) == 0) { if (source is not ViewState view || (view.EventNotifier & EventNotifiers.SubscribeToEvents) == 0) { return (null, StatusCodes.BadNotSupported); } } // check for existing monitored node. if (!MonitoredNodes.TryGetValue(source.NodeId, out monitoredNode)) { MonitoredNodes[source.NodeId] = monitoredNode = new MonitoredNode2(m_nodeManager, m_server, source, IsMultiConsumerNode(source.NodeId)); } // remove existing monitored items with the same Id prior to insertion in order to avoid duplicates // this is necessary since the SubscribeToEvents method is called also from ModifyMonitoredItemsForEvents monitoredNode.EventMonitoredItems.TryRemove(monitoredItem.Id, out _); // this links the node to specified monitored item and ensures all events // reported by the node are added to the monitored item's queue. monitoredNode.Add(monitoredItem); MonitoredItems.TryAdd(monitoredItem.Id, monitoredItem); return (monitoredNode, ServiceResult.Good); } private bool IsMultiConsumerNode(NodeId nodeId) { return m_nodeManager.IsMultipleEventConsumerNode(nodeId); } private readonly IAsyncNodeManager m_nodeManager; private readonly IServerInternal m_server; private readonly SamplingGroupManager m_samplingGroupManager; } }