/* ======================================================================== * Copyright (c) 2005-2020 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.Text; using System.Threading; using System.Security.Principal; using System.Globalization; using System.Threading.Tasks; namespace Opc.Ua.Server { /// /// An object which periodically reads the items and updates the cache. /// public class SamplingGroup : IDisposable { #region Constructors /// /// Creates a new instance of a sampling group. /// public SamplingGroup( IServerInternal server, INodeManager nodeManager, List samplingRates, OperationContext context, double samplingInterval) { if (server == null) throw new ArgumentNullException(nameof(server)); if (nodeManager == null) throw new ArgumentNullException(nameof(nodeManager)); if (samplingRates == null) throw new ArgumentNullException(nameof(samplingRates)); m_server = server; m_nodeManager = nodeManager; m_samplingRates = samplingRates; m_session = context.Session; m_diagnosticsMask = (DiagnosticsMasks)context.DiagnosticsMask & DiagnosticsMasks.OperationAll; m_samplingInterval = AdjustSamplingInterval(samplingInterval); m_itemsToAdd = new List(); m_itemsToRemove = new List(); m_items = new Dictionary(); // create a event to signal shutdown. m_shutdownEvent = new ManualResetEvent(true); } #endregion #region IDisposable Members /// /// Frees any unmanaged resources. /// public void Dispose() { Dispose(true); } /// /// An overrideable version of the Dispose. /// protected virtual void Dispose(bool disposing) { if (disposing) { lock (m_lock) { m_shutdownEvent.Set(); m_samplingRates.Clear(); } } } #endregion #region Public Methods /// /// Starts the sampling thread which periodically reads the items in the group. /// public void Startup() { lock (m_lock) { m_shutdownEvent.Reset(); Task.Run(() => { SampleMonitoredItems(m_samplingInterval); }); } } /// /// Stops the sampling thread. /// public void Shutdown() { lock (m_lock) { m_shutdownEvent.Set(); m_items.Clear(); } } /// /// Checks if the monitored item can be handled by the group. /// /// /// True if the item was added to the group. /// /// /// The ApplyChanges() method must be called to actually start sampling the item. /// public bool StartMonitoring(OperationContext context, ISampledDataChangeMonitoredItem monitoredItem) { lock (m_lock) { if (MeetsGroupCriteria(context, monitoredItem)) { m_itemsToAdd.Add(monitoredItem); monitoredItem.SetSamplingInterval(m_samplingInterval); return true; } return false; } } /// /// Checks if the monitored item can still be handled by the group. /// /// /// False if the item has be marked for removal from the group. /// /// /// The ApplyChanges() method must be called to actually stop sampling the item. /// public bool ModifyMonitoring(OperationContext context, ISampledDataChangeMonitoredItem monitoredItem) { lock (m_lock) { if (m_items.ContainsKey(monitoredItem.Id)) { if (MeetsGroupCriteria(context, monitoredItem)) { monitoredItem.SetSamplingInterval(m_samplingInterval); return true; } m_itemsToRemove.Add(monitoredItem); } return false; } } /// /// Stops monitoring the item. /// /// /// Returns true if the items was marked for removal from the group. /// public bool StopMonitoring(ISampledDataChangeMonitoredItem monitoredItem) { lock (m_lock) { if (m_items.ContainsKey(monitoredItem.Id)) { m_itemsToRemove.Add(monitoredItem); return true; } return false; } } /// /// Updates the group by apply any pending changes. /// /// /// Returns true if the group has no more items and can be dropped. /// public bool ApplyChanges() { lock (m_lock) { // add items. List itemsToSample = new List(); for (int ii = 0; ii < m_itemsToAdd.Count; ii++) { ISampledDataChangeMonitoredItem monitoredItem = m_itemsToAdd[ii]; if (!m_items.ContainsKey(monitoredItem.Id)) { m_items.Add(monitoredItem.Id, monitoredItem); if (monitoredItem.MonitoringMode != MonitoringMode.Disabled) { itemsToSample.Add(monitoredItem); } } } m_itemsToAdd.Clear(); // collect first sample. if (itemsToSample.Count > 0) { Task.Run(() => { DoSample(itemsToSample); }); } // remove items. for (int ii = 0; ii < m_itemsToRemove.Count; ii++) { m_items.Remove(m_itemsToRemove[ii].Id); } m_itemsToRemove.Clear(); // start the group if it is not running. if (m_items.Count > 0) { Startup(); } // stop the group if it is running. else if (m_items.Count == 0) { Shutdown(); } // can be shutdown if no items left. return m_items.Count == 0; } } #endregion #region Private Methods /// /// Checks if the item meets the group's criteria. /// private bool MeetsGroupCriteria(OperationContext context, ISampledDataChangeMonitoredItem monitoredItem) { // can only sample variables. if ((monitoredItem.MonitoredItemType & MonitoredItemTypeMask.DataChange) == 0) { return false; } // can't sample disabled items. if (monitoredItem.MonitoringMode == MonitoringMode.Disabled) { return false; } // check sampling interval. if (AdjustSamplingInterval(monitoredItem.SamplingInterval) != m_samplingInterval) { return false; } // compare session. if (context.SessionId != m_session.Id) { return false; } // check the diagnostics marks. if (m_diagnosticsMask != (context.DiagnosticsMask & DiagnosticsMasks.OperationAll)) { return false; } return true; } /// /// Ensures the requested sampling interval lines up with one of the supported sampling rates. /// private double AdjustSamplingInterval(double samplingInterval) { foreach (SamplingRateGroup samplingRate in m_samplingRates) { // groups are ordered by start rate. if (samplingInterval <= samplingRate.Start) { return samplingRate.Start; } // check if within range specfied by the group. double maxSamplingRate = samplingRate.Start; if (samplingRate.Increment > 0) { maxSamplingRate += samplingRate.Increment*samplingRate.Count; } if (samplingInterval > maxSamplingRate) { continue; } // find sampling rate within rate group. if (samplingInterval == maxSamplingRate) { return maxSamplingRate; } for (double ii = samplingRate.Start; ii <= maxSamplingRate; ii += samplingRate.Increment) { if (ii >= samplingInterval) { return ii; } } } return samplingInterval; } /// /// Periodically checks if the sessions have timed out. /// private void SampleMonitoredItems(object data) { try { //Utils.Trace("Server: {0} Thread Started.", Thread.CurrentThread.Name); int sleepCycle = Convert.ToInt32(data, CultureInfo.InvariantCulture); int timeToWait = sleepCycle; while (m_server.IsRunning) { DateTime start = DateTime.UtcNow; // wait till next sample. if (m_shutdownEvent.WaitOne(timeToWait)) { break; } // get current list of items to sample. List items = new List(); lock (m_lock) { uint disabledItemCount = 0; Dictionary.Enumerator enumerator = m_items.GetEnumerator(); while (enumerator.MoveNext()) { ISampledDataChangeMonitoredItem monitoredItem = enumerator.Current.Value; if (monitoredItem.MonitoringMode == MonitoringMode.Disabled) { disabledItemCount++; continue; } // check whether the item should be sampled. //if (!monitoredItem.SamplingIntervalExpired()) //{ // continue; //} items.Add(monitoredItem); } } // sample the values. DoSample(items); int delay = (int)(DateTime.UtcNow - start).TotalMilliseconds; timeToWait = sleepCycle; if (delay > sleepCycle) { timeToWait = 2*sleepCycle - delay; if (timeToWait < 0) { Utils.Trace("WARNING: SamplingGroup cannot sample fast enough. TimeToSample={0}ms, SamplingInterval={1}ms", delay, sleepCycle); timeToWait = sleepCycle; } } } //Utils.Trace("Server: {0} Thread Exited Normally.", Thread.CurrentThread.Name); } catch (Exception e) { Utils.Trace(e, "Server: SampleMonitoredItems Thread Exited Unexpectedly."); } } /// /// Samples the values of the items. /// private void DoSample(object state) { try { List items = state as List; // read values for all enabled items. if (items != null && items.Count > 0) { ReadValueIdCollection itemsToRead = new ReadValueIdCollection(items.Count); DataValueCollection values = new DataValueCollection(items.Count); List errors = new List(items.Count); // allocate space for results. for (int ii = 0; ii < items.Count; ii++) { ReadValueId readValueId = items[ii].GetReadValueId(); readValueId.Processed = false; itemsToRead.Add(readValueId); values.Add(null); errors.Add(null); } OperationContext context = new OperationContext(m_session, m_diagnosticsMask); // read values. m_nodeManager.Read( context, 0, itemsToRead, values, errors); // update monitored items. for (int ii = 0; ii < items.Count; ii++) { if (values[ii] == null) { values[ii] = new DataValue(StatusCodes.BadInternalError, DateTime.UtcNow); } items[ii].QueueValue(values[ii], errors[ii]); } } } catch (Exception e) { Utils.Trace(e, "Server: Unexpected error sampling values."); } } #endregion #region Private Fields private object m_lock = new object(); private IServerInternal m_server; private INodeManager m_nodeManager; private Session m_session; private DiagnosticsMasks m_diagnosticsMask; private double m_samplingInterval; private List m_itemsToAdd; private List m_itemsToRemove; private Dictionary m_items; private ManualResetEvent m_shutdownEvent; private List m_samplingRates; #endregion } }