/* ========================================================================
* 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
}
}