//******************************************************************************************************
// DataSubscriber.cs - Gbtc
//
// Copyright © 2012, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
// not use this file except in compliance with the License. You may obtain a copy of the License at:
//
// http://www.opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 08/20/2010 - J. Ritchie Carroll
// Generated original version of source code.
// 02/07/2012 - Mehulbhai Thakkar
// Modified SynchronizeMetadata to filter devices by original source and modified insert query
// to populate OriginalSource value. Added to flag to optionally avoid meta-data synchronization.
// 12/20/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml;
using GSF.Collections;
using GSF.Communication;
using GSF.Configuration;
using GSF.Data;
using GSF.Diagnostics;
using GSF.IO;
using GSF.Net.Security;
using GSF.Reflection;
using GSF.Security.Cryptography;
using GSF.Threading;
using GSF.TimeSeries.Adapters;
using GSF.TimeSeries.Data;
using GSF.TimeSeries.Statistics;
using GSF.TimeSeries.Transport.TSSC;
using GSF.Units;
using Random = GSF.Security.Cryptography.Random;
using TcpClient = GSF.Communication.TcpClient;
using UdpClient = GSF.Communication.UdpClient;
using System.Globalization;
#pragma warning disable 672
namespace GSF.TimeSeries.Transport
{
///
/// Represents a data subscribing client that will connect to a data publisher for a data subscription.
///
[Description("DataSubscriber: client that subscribes to a publishing server for a streaming data.")]
[EditorBrowsable(EditorBrowsableState.Advanced)] // Normally defined as an input device protocol
public class DataSubscriber : InputAdapterBase
{
#region [ Members ]
// Nested Types
// Local measurement concentrator
private class LocalConcentrator : ConcentratorBase
{
#region [ Members ]
// Fields
private DataSubscriber m_parent;
private bool m_disposed;
#endregion
#region [ Constructors ]
///
/// Creates a new local concentrator.
///
public LocalConcentrator(DataSubscriber parent)
{
m_parent = parent;
}
#endregion
#region [ Methods ]
///
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected override void Dispose(bool disposing)
{
if (!m_disposed)
{
try
{
if (disposing)
m_parent = null;
}
finally
{
m_disposed = true; // Prevent duplicate dispose.
base.Dispose(disposing); // Call base class Dispose().
}
}
}
///
/// Publish of time-aligned collection of values that arrived within the
/// concentrator's defined .
///
/// of measurements with the same timestamp that arrived within that are ready for processing.
/// Index of within a second ranging from zero to - 1.
protected override void PublishFrame(IFrame frame, int index)
{
// Publish locally sorted measurements
if ((object)m_parent != null)
m_parent.OnNewMeasurements(frame.Measurements.Values);
}
#endregion
}
private class SubscribedDevice : IDevice, IDisposable
{
#region [ Members ]
// Fields
private readonly string m_name;
private Guid m_statusFlagsID;
private Guid m_frequencyID;
private Guid m_deltaFrequencyID;
private long m_dataQualityErrors;
private long m_timeQualityErrors;
private long m_deviceErrors;
private long m_measurementsReceived;
private double m_measurementsExpected;
private long m_measurementsWithError;
private long m_measurementsDefined;
private bool m_disposed;
#endregion
#region [ Constructors ]
public SubscribedDevice(string name)
{
if ((object)name == null)
throw new ArgumentNullException(nameof(name));
m_name = name;
StatisticsEngine.Register(this, name, "Device", "PMU");
}
///
/// Releases the unmanaged resources before the object is reclaimed by .
///
~SubscribedDevice()
{
Unregister();
}
#endregion
#region [ Properties ]
public string Name => m_name;
public Guid StatusFlagsID
{
get
{
return m_statusFlagsID;
}
set
{
m_statusFlagsID = value;
}
}
public Guid FrequencyID
{
get
{
return m_frequencyID;
}
set
{
m_frequencyID = value;
}
}
public Guid DeltaFrequencyID
{
get
{
return m_deltaFrequencyID;
}
set
{
m_deltaFrequencyID = value;
}
}
public long DataQualityErrors
{
get
{
return Interlocked.Read(ref m_dataQualityErrors);
}
set
{
Interlocked.Exchange(ref m_dataQualityErrors, value);
}
}
public long TimeQualityErrors
{
get
{
return Interlocked.Read(ref m_timeQualityErrors);
}
set
{
Interlocked.Exchange(ref m_timeQualityErrors, value);
}
}
public long DeviceErrors
{
get
{
return Interlocked.Read(ref m_deviceErrors);
}
set
{
Interlocked.Exchange(ref m_deviceErrors, value);
}
}
public long MeasurementsReceived
{
get
{
return Interlocked.Read(ref m_measurementsReceived);
}
set
{
Interlocked.Exchange(ref m_measurementsReceived, value);
}
}
public long MeasurementsExpected
{
get
{
return (long)Interlocked.CompareExchange(ref m_measurementsExpected, 0.0D, 0.0D);
}
set
{
Interlocked.Exchange(ref m_measurementsExpected, value);
}
}
public long MeasurementsWithError
{
get
{
return Interlocked.Read(ref m_measurementsWithError);
}
set
{
Interlocked.Exchange(ref m_measurementsWithError, value);
}
}
public long MeasurementsDefined
{
get
{
return Interlocked.Read(ref m_measurementsDefined);
}
set
{
Interlocked.Exchange(ref m_measurementsDefined, value);
}
}
#endregion
#region [ Methods ]
public override bool Equals(object obj)
{
SubscribedDevice subscribedDevice = obj as SubscribedDevice;
return (object)subscribedDevice != null && m_name.Equals(subscribedDevice.m_name);
}
public override int GetHashCode()
{
return m_name.GetHashCode();
}
///
/// Releases all the resources used by the object.
///
public void Dispose()
{
Unregister();
GC.SuppressFinalize(this);
}
private void Unregister()
{
if (!m_disposed)
{
try
{
StatisticsEngine.Unregister(this);
}
finally
{
m_disposed = true; // Prevent duplicate dispose.
}
}
}
#endregion
}
///
/// EventArgs implementation for handling user commands.
///
public class UserCommandArgs : EventArgs
{
///
/// Creates a new instance of the class.
///
/// The code for the user command.
/// The code for the server's response.
/// Buffer containing the message from the server.
/// Index into the buffer used to skip the header.
/// The length of the message in the buffer, including the header.
public UserCommandArgs(ServerCommand command, ServerResponse response, byte[] buffer, int startIndex, int length)
{
Command = command;
Response = response;
Buffer = buffer;
StartIndex = startIndex;
Length = length;
}
///
/// Gets the code for the user command.
///
public ServerCommand Command { get; }
///
/// Gets the code for the server's response.
///
public ServerResponse Response { get; }
///
/// Gets the buffer containing the message from the server.
///
public byte[] Buffer { get; }
///
/// Gets the index into the buffer used to skip the header.
///
public int StartIndex { get; }
///
/// Gets the length of the message in the buffer, including the header.
///
public int Length { get; }
}
// Constants
///
/// Defines default value for .
///
public const OperationalModes DefaultOperationalModes = OperationalModes.CompressMetadata | OperationalModes.CompressSignalIndexCache | OperationalModes.ReceiveInternalMetadata | OperationalModes.UseCommonSerializationFormat;
///
/// Defines the default value for the property.
///
public const int DefaultMetadataSynchronizationTimeout = 0;
///
/// Defines the default value for the property.
///
public const bool DefaultUseTransactionForMetadata = true;
///
/// Default value for .
///
public const string DefaultLoggingPath = "ConfigurationCache";
///
/// Specifies the default value for the property.
///
public const int DefaultAllowedParsingExceptions = 10;
///
/// Specifies the default value for the property.
///
public const long DefaultParsingExceptionWindow = 50000000L; // 5 seconds
private const int EvenKey = 0; // Even key/IV index
private const int OddKey = 1; // Odd key/IV index
private const int KeyIndex = 0; // Index of cipher key component in keyIV array
private const int IVIndex = 1; // Index of initialization vector component in keyIV array
private const long MissingCacheWarningInterval = 20000000;
// Events
///
/// Occurs when client connection to the data publication server is established.
///
public event EventHandler ConnectionEstablished;
///
/// Occurs when client connection to the data publication server is terminated.
///
public event EventHandler ConnectionTerminated;
///
/// Occurs when client connection to the data publication server has successfully authenticated.
///
public event EventHandler ConnectionAuthenticated;
///
/// Occurs when client receives response from the server.
///
public event EventHandler> ReceivedServerResponse;
///
/// Occurs when client receives message from the server in response to a user command.
///
public event EventHandler ReceivedUserCommandResponse;
///
/// Occurs when client receives requested meta-data transmitted by data publication server.
///
public event EventHandler> MetaDataReceived;
///
/// Occurs when first measurement is transmitted by data publication server.
///
public event EventHandler> DataStartTime;
///
/// Indicates that processing for an input adapter (via temporal session) has completed.
///
///
/// This event is expected to only be raised when an input adapter has been designed to process
/// a finite amount of data, e.g., reading a historical range of data during temporal processing.
///
public new event EventHandler> ProcessingComplete;
///
/// Occurs when a notification has been received from the .
///
public event EventHandler> NotificationReceived;
///
/// Occurs when the server has sent a notification that its configuration has changed, this
/// can allow subscriber to request updated meta-data if desired.
///
public event EventHandler ServerConfigurationChanged;
///
/// Occurs when number of parsing exceptions exceed during .
///
public event EventHandler ExceededParsingExceptionThreshold;
// Fields
private volatile Dictionary> m_subscribedDevicesLookup;
private volatile List> m_statisticsHelpers;
private readonly LongSynchronizedOperation m_registerStatisticsOperation;
private IClient m_commandChannel;
private UdpClient m_dataChannel;
private bool m_useZeroMQChannel;
private LocalConcentrator m_localConcentrator;
private bool m_tsscResetRequested;
private TsscDecoder m_tsscDecoder;
private ushort m_tsscSequenceNumber;
private SharedTimer m_dataStreamMonitor;
private long m_commandChannelConnectionAttempts;
private long m_dataChannelConnectionAttempts;
private volatile SignalIndexCache m_remoteSignalIndexCache;
private volatile SignalIndexCache m_signalIndexCache;
private volatile long[] m_baseTimeOffsets;
private volatile int m_timeIndex;
private volatile byte[][][] m_keyIVs;
private volatile bool m_authenticated;
private volatile bool m_subscribed;
private volatile int m_lastBytesReceived;
private long m_monitoredBytesReceived;
private long m_totalBytesReceived;
private long m_lastMissingCacheWarning;
private Guid m_nodeID;
private int m_gatewayProtocolID;
private SecurityMode m_securityMode;
private bool m_synchronizedSubscription;
private bool m_useMillisecondResolution;
private bool m_requestNaNValueFilter;
private bool m_autoConnect;
private string m_metadataFilters;
private string m_sharedSecret;
private string m_authenticationID;
private string m_localCertificate;
private string m_remoteCertificate;
private SslPolicyErrors m_validPolicyErrors;
private X509ChainStatusFlags m_validChainFlags;
private bool m_checkCertificateRevocation;
private bool m_internal;
private bool m_includeTime;
private bool m_filterOutputMeasurements;
private bool m_autoSynchronizeMetadata;
private bool m_useTransactionForMetadata;
private bool m_useSourcePrefixNames;
private bool m_useLocalClockAsRealTime;
private bool m_metadataRefreshPending;
private int m_metadataSynchronizationTimeout;
private readonly LongSynchronizedOperation m_synchronizeMetadataOperation;
private volatile DataSet m_receivedMetadata;
private DataSet m_synchronizedMetadata;
private DateTime m_lastMetaDataRefreshTime;
private OperationalModes m_operationalModes;
private Encoding m_encoding;
private string m_loggingPath;
private RunTimeLog m_runTimeLog;
private bool m_bypassingStatistics;
private bool m_dataGapRecoveryEnabled;
private DataGapRecoverer m_dataGapRecoverer;
private int m_parsingExceptionCount;
private long m_lastParsingExceptionTime;
private int m_allowedParsingExceptions;
private Ticks m_parsingExceptionWindow;
private bool m_supportsRealTimeProcessing;
private bool m_supportsTemporalProcessing;
//private Ticks m_lastMeasurementCheck;
//private Ticks m_minimumMissingMeasurementThreshold = 5;
//private double m_transmissionDelayTimeAdjustment = 5.0;
private readonly List m_bufferBlockCache;
private uint m_expectedBufferBlockSequenceNumber;
private Ticks m_realTime;
private Ticks m_lastStatisticsHelperUpdate;
private SharedTimer m_subscribedDevicesTimer;
private long m_lifetimeMeasurements;
private long m_minimumMeasurementsPerSecond;
private long m_maximumMeasurementsPerSecond;
private long m_totalMeasurementsPerSecond;
private long m_measurementsPerSecondCount;
private long m_measurementsInSecond;
private long m_lastSecondsSinceEpoch;
private long m_lifetimeTotalLatency;
private long m_lifetimeMinimumLatency;
private long m_lifetimeMaximumLatency;
private long m_lifetimeLatencyMeasurements;
private long m_syncProgressTotalActions;
private long m_syncProgressActionsCount;
private long m_syncProgressUpdateInterval;
private long m_syncProgressLastMessage;
private bool m_disposed;
#endregion
#region [ Constructors ]
///
/// Creates a new .
///
public DataSubscriber()
{
m_registerStatisticsOperation = new LongSynchronizedOperation(HandleDeviceStatisticsRegistration)
{
IsBackground = true
};
m_synchronizeMetadataOperation = new LongSynchronizedOperation(SynchronizeMetadata)
{
IsBackground = true
};
m_encoding = Encoding.Unicode;
m_operationalModes = DefaultOperationalModes;
m_metadataSynchronizationTimeout = DefaultMetadataSynchronizationTimeout;
m_allowedParsingExceptions = DefaultAllowedParsingExceptions;
m_parsingExceptionWindow = DefaultParsingExceptionWindow;
string loggingPath = FilePath.GetDirectoryName(FilePath.GetAbsolutePath(DefaultLoggingPath));
if (Directory.Exists(loggingPath))
m_loggingPath = loggingPath;
// Default to not using transactions for meta-data on SQL server (helps avoid deadlocks)
try
{
using (AdoDataConnection database = new AdoDataConnection("systemSettings"))
{
m_useTransactionForMetadata = database.DatabaseType != DatabaseType.SQLServer;
}
}
catch
{
m_useTransactionForMetadata = DefaultUseTransactionForMetadata;
}
DataLossInterval = 10.0D;
m_bufferBlockCache = new List();
m_useLocalClockAsRealTime = true;
m_useSourcePrefixNames = true;
}
#endregion
#region [ Properties ]
///
/// Gets or sets the security mode used for communications over the command channel.
///
public SecurityMode SecurityMode
{
get
{
return m_securityMode;
}
set
{
m_securityMode = value;
}
}
///
/// Gets or sets flag that determines if requires subscribers to authenticate before making data requests.
///
public bool RequireAuthentication
{
get
{
return m_securityMode != SecurityMode.None;
}
set
{
m_securityMode = value ? SecurityMode.Gateway : SecurityMode.None;
}
}
///
/// Gets or sets flag that determines if ZeroMQ should be used for command channel communications.
///
public bool UseZeroMQChannel
{
get
{
return m_useZeroMQChannel;
}
set
{
m_useZeroMQChannel = value;
}
}
///
/// Gets or sets flag that determines if a should be used for connection.
///
public bool UseSimpleTcpClient { get; set; }
///
/// Gets or sets logging path to be used to be runtime and outage logs of the subscriber which are required for
/// automated data recovery.
///
///
/// Leave value blank for default path, i.e., installation folder. Can be a fully qualified path or a path that
/// is relative to the installation folder, e.g., a value of "ConfigurationCache" might resolve to
/// "C:\Program Files\MyTimeSeriespPp\ConfigurationCache\".
///
public string LoggingPath
{
get
{
return m_loggingPath;
}
set
{
if (!string.IsNullOrWhiteSpace(value))
{
string loggingPath = FilePath.GetDirectoryName(FilePath.GetAbsolutePath(value));
if (Directory.Exists(loggingPath))
value = loggingPath;
}
m_loggingPath = value;
}
}
///
/// Gets or sets flag that determines if should attempt to auto-connection to using defined connection settings.
///
public bool AutoConnect
{
get
{
return m_autoConnect;
}
set
{
m_autoConnect = value;
}
}
///
/// Gets or sets flag that determines if should
/// automatically request meta-data synchronization and synchronize publisher
/// meta-data with its own database configuration.
///
public bool AutoSynchronizeMetadata
{
get
{
return m_autoSynchronizeMetadata;
}
set
{
m_autoSynchronizeMetadata = value;
}
}
///
/// Gets flag that indicates whether the connection will be persisted
/// even while the adapter is offline in order to synchronize metadata.
///
public bool PersistConnectionForMetadata =>
!AutoStart && AutoSynchronizeMetadata && !this.TemporalConstraintIsDefined();
///
/// Gets or sets flag that determines if child devices associated with a subscription
/// should be prefixed with the subscription name and an exclamation point to ensure
/// device name uniqueness - recommended value is true.
///
public bool UseSourcePrefixNames
{
get
{
return m_useSourcePrefixNames;
}
set
{
m_useSourcePrefixNames = value;
}
}
///
/// Gets or sets requested meta-data filter expressions to be applied by before meta-data is sent.
///
///
/// Multiple meta-data filters, such filters for different data tables, should be separated by a semicolon. Specifying fields in the filter
/// expression that do not exist in the data publisher's current meta-data set could cause filter expressions to not be applied and possibly
/// result in no meta-data being received for the specified data table.
///
///
/// FILTER MeasurementDetail WHERE SignalType <> 'STAT'; FILTER PhasorDetail WHERE Phase = '+'
///
public string MetadataFilters
{
get
{
return m_metadataFilters;
}
set
{
m_metadataFilters = value;
}
}
///
/// Gets or sets flag that determines if a subscription is mutual, i.e., bi-directional pub/sub. In this mode one node will
/// be the owner and set Internal = True and the other node will be the renter and set Internal = False.
///
///
///
/// This flag is intended to be used in scenarios where a remote subscriber can add new measurements associated with a
/// source device, e.g., creating new calculated result measurements on a remote machine for load distribution that should
/// get associated with a device on the local machine, thus becoming part of the local measurement set.
///
///
/// For best results, both the owner and renter subscriptions should be reduced to needed measurements, i.e., renter should
/// only receive measurements needed for remote calculations and owner should only receive new calculated results. Note that
/// when used with a TLS-style subscription this can be accomplished by using the subscription UI screens that control the
/// measurement subscribed flag. For internal subscriptions, reduction of metadata and subscribed measurements will
/// need to be controlled via connection string with metadataFilters and outputMeasurements, respectively.
///
///
/// Setting to true will force to true
/// and to false.
///
///
public bool MutualSubscription { get; set; }
///
/// Gets or sets flag that informs publisher if base time-offsets can use millisecond resolution to conserve bandwidth.
///
[Obsolete("SubscriptionInfo object defines this parameter.", false)]
public bool UseMillisecondResolution
{
get
{
return m_useMillisecondResolution;
}
set
{
m_useMillisecondResolution = value;
}
}
///
/// Gets flag that determines whether the command channel is connected.
///
public bool CommandChannelConnected =>
(object)m_commandChannel != null &&
m_commandChannel.Enabled;
///
/// Gets flag that determines if this has successfully authenticated with the .
///
public bool Authenticated => m_authenticated;
///
/// Gets total data packet bytes received during this session.
///
public long TotalBytesReceived => m_totalBytesReceived;
///
/// Gets or sets data loss monitoring interval, in seconds. Set to zero to disable monitoring.
///
public double DataLossInterval
{
get
{
if ((object)m_dataStreamMonitor != null)
return m_dataStreamMonitor.Interval / 1000.0D;
return 0.0D;
}
set
{
if (value > 0.0D)
{
if ((object)m_dataStreamMonitor == null)
{
// Create data stream monitoring timer
m_dataStreamMonitor = Common.TimerScheduler.CreateTimer();
m_dataStreamMonitor.Elapsed += m_dataStreamMonitor_Elapsed;
m_dataStreamMonitor.AutoReset = true;
m_dataStreamMonitor.Enabled = false;
}
// Set user specified interval
m_dataStreamMonitor.Interval = (int)(value * 1000.0D);
}
else
{
// Disable data monitor
if ((object)m_dataStreamMonitor != null)
{
m_dataStreamMonitor.Elapsed -= m_dataStreamMonitor_Elapsed;
m_dataStreamMonitor.Dispose();
}
m_dataStreamMonitor = null;
}
}
}
///
/// Gets or sets a set of flags that define ways in
/// which the subscriber and publisher communicate.
///
public OperationalModes OperationalModes
{
get
{
return m_operationalModes;
}
set
{
OperationalEncoding operationalEncoding;
m_operationalModes = value;
operationalEncoding = (OperationalEncoding)(value & OperationalModes.EncodingMask);
m_encoding = GetCharacterEncoding(operationalEncoding);
}
}
///
/// Gets or sets the operational mode flag to compress meta-data.
///
public bool CompressMetadata
{
get
{
return m_operationalModes.HasFlag(OperationalModes.CompressMetadata);
}
set
{
if (value)
m_operationalModes |= OperationalModes.CompressMetadata;
else
m_operationalModes &= ~OperationalModes.CompressMetadata;
}
}
///
/// Gets or sets the operational mode flag to compress the signal index cache.
///
public bool CompressSignalIndexCache
{
get
{
return m_operationalModes.HasFlag(OperationalModes.CompressSignalIndexCache);
}
set
{
if (value)
m_operationalModes |= OperationalModes.CompressSignalIndexCache;
else
m_operationalModes &= ~OperationalModes.CompressSignalIndexCache;
}
}
///
/// Gets or sets the operational mode flag to compress data payloads.
///
public bool CompressPayload
{
get
{
return m_operationalModes.HasFlag(OperationalModes.CompressPayloadData);
}
set
{
if (value)
m_operationalModes |= OperationalModes.CompressPayloadData;
else
m_operationalModes &= ~OperationalModes.CompressPayloadData;
}
}
///
/// Gets or sets the operational mode flag to receive internal meta-data.
///
public bool ReceiveInternalMetadata
{
get
{
return m_operationalModes.HasFlag(OperationalModes.ReceiveInternalMetadata);
}
set
{
if (value)
m_operationalModes |= OperationalModes.ReceiveInternalMetadata;
else
m_operationalModes &= ~OperationalModes.ReceiveInternalMetadata;
}
}
///
/// Gets or sets the operational mode flag to receive external meta-data.
///
public bool ReceiveExternalMetadata
{
get
{
return m_operationalModes.HasFlag(OperationalModes.ReceiveExternalMetadata);
}
set
{
if (value)
m_operationalModes |= OperationalModes.ReceiveExternalMetadata;
else
m_operationalModes &= ~OperationalModes.ReceiveExternalMetadata;
}
}
///
/// Gets or sets the operational mode flag to use the common serialization format.
///
public bool UseCommonSerializationFormat
{
get
{
return m_operationalModes.HasFlag(OperationalModes.UseCommonSerializationFormat);
}
set
{
if (value)
m_operationalModes |= OperationalModes.UseCommonSerializationFormat;
else
m_operationalModes &= ~OperationalModes.UseCommonSerializationFormat;
}
}
///
/// Gets or sets the used by the subscriber and publisher.
///
public OperationalEncoding OperationalEncoding
{
get
{
return (OperationalEncoding)(m_operationalModes & OperationalModes.EncodingMask);
}
set
{
m_operationalModes &= ~OperationalModes.EncodingMask;
m_operationalModes |= (OperationalModes)value;
m_encoding = GetCharacterEncoding(value);
}
}
///
/// Gets or sets the used by the subscriber and publisher.
///
public CompressionModes CompressionModes
{
get
{
return (CompressionModes)(m_operationalModes & OperationalModes.CompressionModeMask);
}
set
{
m_operationalModes &= ~OperationalModes.CompressionModeMask;
m_operationalModes |= (OperationalModes)value;
if (value.HasFlag(CompressionModes.TSSC))
CompressPayload = true;
}
}
///
/// Gets the version number of the protocol in use by this subscriber.
///
public int Version => (int)(m_operationalModes & OperationalModes.VersionMask);
///
/// Gets the character encoding defined by the
/// of the communications stream.
///
public Encoding Encoding => m_encoding;
///
/// Gets flag indicating if this adapter supports real-time processing.
///
///
/// Setting this value to false indicates that the adapter should not be enabled unless it exists within a temporal session.
/// As an example, this flag can be used in a gateway system to set up two separate subscribers: one to the PDC for real-time
/// data streams and one to the historian for historical data streams. In this scenario, the assumption is that the PDC is
/// the data source for the historian, implying that only local data is destined for archival.
///
public bool SupportsRealTimeProcessing => m_supportsRealTimeProcessing;
///
/// Gets the flag indicating if this adapter supports temporal processing.
///
///
///
/// Although the data subscriber provisions support for temporal processing by receiving historical data from a remote source,
/// the adapter opens sockets and does not need to be engaged within an actual temporal , therefore
/// this method normally returns false to make sure the adapter doesn't get instantiated within a temporal session.
///
///
/// Setting this to true means that a subscriber will be initialized within a temporal session to provide historical
/// data from a remote source - this should only be enabled in cases where (1) there is no locally defined, e.g., in-process,
/// historian that can already provide historical data for temporal sessions, and (2) a remote subscriber should be allowed
/// to proxy temporal requests, e.g., those requested for data gap recovery, to an up-stream subscription. This is useful in
/// cases where a primary data subscriber that has data gap recovery enabled can also allow a remote subscription to proxy in
/// data gap recovery requests. It is recommended that remote data gap recovery request parameters be (1) either slightly
/// looser than those of local system to reduce the possibility of duplicated recovery sessions for the same data loss, or
/// (2) only enabled in the end-most system that most needs the recovered data, like a historian.
///
///
public override bool SupportsTemporalProcessing => m_supportsTemporalProcessing;
///
/// Gets or sets the desired processing interval, in milliseconds, for the adapter.
///
///
/// With the exception of the values of -1 and 0, this value specifies the desired processing interval for data, i.e.,
/// basically a delay, or timer interval, over which to process data. A value of -1 means to use the default processing
/// interval while a value of 0 means to process data as fast as possible.
///
public override int ProcessingInterval
{
get
{
return base.ProcessingInterval;
}
set
{
base.ProcessingInterval = value;
// Request server update the processing interval
SendServerCommand(ServerCommand.UpdateProcessingInterval, BigEndian.GetBytes(value));
}
}
///
/// Gets or sets the timeout used when executing database queries during meta-data synchronization.
///
public int MetadataSynchronizationTimeout
{
get
{
return m_metadataSynchronizationTimeout;
}
set
{
m_metadataSynchronizationTimeout = value;
}
}
///
/// Gets or sets flag that determines if meta-data synchronization should be performed within a transaction.
///
public bool UseTransactionForMetadata
{
get
{
return m_useTransactionForMetadata;
}
set
{
m_useTransactionForMetadata = value;
}
}
///
/// Gets or sets flag that determines whether to use the local clock when calculating statistics.
///
public bool UseLocalClockAsRealTime
{
get
{
return m_useLocalClockAsRealTime;
}
set
{
m_useLocalClockAsRealTime = value;
}
}
///
/// Gets or sets number of parsing exceptions allowed during before connection is reset.
///
public int AllowedParsingExceptions
{
get
{
return m_allowedParsingExceptions;
}
set
{
m_allowedParsingExceptions = value;
}
}
///
/// Gets or sets time duration, in , to monitor parsing exceptions.
///
public Ticks ParsingExceptionWindow
{
get
{
return m_parsingExceptionWindow;
}
set
{
m_parsingExceptionWindow = value;
}
}
///
/// Gets or sets based data source available to this .
///
public override DataSet DataSource
{
get
{
return base.DataSource;
}
set
{
base.DataSource = value;
m_registerStatisticsOperation.RunOnce();
bool outputMeasurementsUpdated = AutoConnect && UpdateOutputMeasurements();
// For automatic connections, when meta-data refresh is complete, update output measurements to see if any
// points for subscription have changed after re-application of filter expressions and if so, resubscribe
if (outputMeasurementsUpdated && Enabled && CommandChannelConnected)
{
OnStatusMessage(MessageLevel.Info, "Meta-data received from publisher modified measurement availability, adjusting active subscription...");
// Updating subscription will restart data stream monitor upon successful resubscribe
if (AutoStart)
SubscribeToOutputMeasurements(true);
}
if ((object)m_dataGapRecoverer != null)
m_dataGapRecoverer.DataSource = value;
}
}
///
/// Gets or sets output measurement keys that are requested by other adapters based on what adapter says it can provide.
///
public override MeasurementKey[] RequestedOutputMeasurementKeys
{
get
{
return base.RequestedOutputMeasurementKeys;
}
set
{
MeasurementKey[] oldKeys = base.RequestedOutputMeasurementKeys ?? new MeasurementKey[0];
MeasurementKey[] newKeys = value ?? new MeasurementKey[0];
HashSet oldKeySet = new HashSet(oldKeys);
base.RequestedOutputMeasurementKeys = value;
if (!AutoStart && Enabled && CommandChannelConnected && !oldKeySet.SetEquals(newKeys))
{
OnStatusMessage(MessageLevel.Info, "Requested measurements have changed, adjusting active subscription...");
SubscribeToOutputMeasurements(true);
}
}
}
///
/// Gets or sets output measurements that the will produce, if any.
///
public override IMeasurement[] OutputMeasurements
{
get
{
return base.OutputMeasurements;
}
set
{
base.OutputMeasurements = value;
if ((object)m_dataGapRecoverer != null)
m_dataGapRecoverer.FilterExpression = this.OutputMeasurementKeys().Select(key => key.SignalID.ToString()).ToDelimitedString(';');
}
}
///
/// Gets connection info for adapter, if any.
///
public override string ConnectionInfo
{
get
{
string commandChannelServerUri = m_commandChannel?.ServerUri;
string dataChannelServerUri = m_dataChannel?.ServerUri;
if (string.IsNullOrWhiteSpace(commandChannelServerUri) && string.IsNullOrWhiteSpace(dataChannelServerUri))
return null;
if (string.IsNullOrWhiteSpace(dataChannelServerUri))
return commandChannelServerUri;
if (string.IsNullOrWhiteSpace(commandChannelServerUri))
return dataChannelServerUri;
return $"{commandChannelServerUri} / {dataChannelServerUri}";
}
}
///
/// Gets the status of this .
///
///
/// Derived classes should provide current status information about the adapter for display purposes.
///
public override string Status
{
get
{
StringBuilder status = new StringBuilder();
status.AppendFormat(" Subscription mode: {0}", m_synchronizedSubscription ? "Remotely Synchronized" : (object)m_localConcentrator == null ? "Unsynchronized" : "Locally Synchronized");
status.AppendLine();
status.AppendFormat(" Authenticated: {0}", m_authenticated);
status.AppendLine();
status.AppendFormat(" Subscribed: {0}", m_subscribed);
status.AppendLine();
status.AppendFormat(" Security mode: {0}", SecurityMode);
status.AppendLine();
status.AppendFormat(" Compression modes: {0}", CompressionModes);
status.AppendLine();
status.AppendFormat(" Mutual Subscription: {0}{1}", MutualSubscription, MutualSubscription ? $" - system is {(m_internal ? "owner" : "renter")}" : "");
status.AppendLine();
status.AppendFormat(" Mark Received as Internal: {0}", m_internal);
status.AppendLine();
status.AppendFormat(" Receive Internal Metadata: {0}", ReceiveInternalMetadata);
status.AppendLine();
status.AppendFormat(" Receive External Metadata: {0}", ReceiveExternalMetadata);
status.AppendLine();
status.AppendFormat(" Total Bytes Received: {0:N0}", TotalBytesReceived);
status.AppendLine();
if ((object)m_dataChannel != null)
{
status.AppendFormat(" UDP Data packet security: {0}", (object)m_keyIVs == null ? "Unencrypted" : "Encrypted");
status.AppendLine();
}
status.AppendFormat(" Data monitor enabled: {0}", (object)m_dataStreamMonitor != null && m_dataStreamMonitor.Enabled);
status.AppendLine();
status.AppendFormat(" Logging path: {0}", FilePath.TrimFileName(m_loggingPath.ToNonNullNorWhiteSpace(FilePath.GetAbsolutePath("")), 51));
status.AppendLine();
if (DataLossInterval > 0.0D)
status.AppendFormat("No data reconnect interval: {0:0.000} seconds", DataLossInterval);
else
status.Append("No data reconnect interval: disabled");
status.AppendLine();
status.AppendFormat(" Data gap recovery mode: {0}", m_dataGapRecoveryEnabled ? "Enabled" : "Disabled");
status.AppendLine();
if (m_dataGapRecoveryEnabled && (object)m_dataGapRecoverer != null)
status.Append(m_dataGapRecoverer.Status);
if ((object)m_runTimeLog != null)
{
status.AppendLine();
status.AppendLine("Run-Time Log Status".CenterText(50));
status.AppendLine("-------------------".CenterText(50));
status.AppendFormat(m_runTimeLog.Status);
}
if ((object)m_dataChannel != null)
{
status.AppendLine();
status.AppendLine("Data Channel Status".CenterText(50));
status.AppendLine("-------------------".CenterText(50));
status.Append(m_dataChannel.Status);
}
if ((object)m_commandChannel != null)
{
status.AppendLine();
status.AppendLine("Command Channel Status".CenterText(50));
status.AppendLine("----------------------".CenterText(50));
status.Append(m_commandChannel.Status);
}
if ((object)m_localConcentrator != null)
{
status.AppendLine();
status.AppendLine("Local Concentrator Status".CenterText(50));
status.AppendLine("-------------------------".CenterText(50));
status.Append(m_localConcentrator.Status);
}
status.Append(base.Status);
return status.ToString();
}
}
///
/// Gets a flag that determines if this uses an asynchronous connection.
///
protected override bool UseAsyncConnect => true;
///
/// Gets or sets reference to data channel, attaching and/or detaching to events as needed.
///
protected UdpClient DataChannel
{
get
{
return m_dataChannel;
}
set
{
if ((object)m_dataChannel != null)
{
// Detach from events on existing data channel reference
m_dataChannel.ConnectionException -= m_dataChannel_ConnectionException;
m_dataChannel.ConnectionAttempt -= m_dataChannel_ConnectionAttempt;
m_dataChannel.ReceiveData -= m_dataChannel_ReceiveData;
m_dataChannel.ReceiveDataException -= m_dataChannel_ReceiveDataException;
if ((object)m_dataChannel != value)
m_dataChannel.Dispose();
}
// Assign new data channel reference
m_dataChannel = value;
if ((object)m_dataChannel != null)
{
// Attach to desired events on new data channel reference
m_dataChannel.ConnectionException += m_dataChannel_ConnectionException;
m_dataChannel.ConnectionAttempt += m_dataChannel_ConnectionAttempt;
m_dataChannel.ReceiveData += m_dataChannel_ReceiveData;
m_dataChannel.ReceiveDataException += m_dataChannel_ReceiveDataException;
}
}
}
///
/// Gets or sets reference to command channel, attaching and/or detaching to events as needed.
///
protected IClient CommandChannel
{
get
{
return m_commandChannel;
}
set
{
if ((object)m_commandChannel != null)
{
// Detach from events on existing command channel reference
m_commandChannel.ConnectionAttempt -= m_commandChannel_ConnectionAttempt;
m_commandChannel.ConnectionEstablished -= m_commandChannel_ConnectionEstablished;
m_commandChannel.ConnectionException -= m_commandChannel_ConnectionException;
m_commandChannel.ConnectionTerminated -= m_commandChannel_ConnectionTerminated;
m_commandChannel.ReceiveData -= m_commandChannel_ReceiveData;
m_commandChannel.ReceiveDataException -= m_commandChannel_ReceiveDataException;
m_commandChannel.SendDataException -= m_commandChannel_SendDataException;
if (m_commandChannel != value)
m_commandChannel.Dispose();
}
// Assign new command channel reference
m_commandChannel = value;
if ((object)m_commandChannel != null)
{
// Attach to desired events on new command channel reference
m_commandChannel.ConnectionAttempt += m_commandChannel_ConnectionAttempt;
m_commandChannel.ConnectionEstablished += m_commandChannel_ConnectionEstablished;
m_commandChannel.ConnectionException += m_commandChannel_ConnectionException;
m_commandChannel.ConnectionTerminated += m_commandChannel_ConnectionTerminated;
m_commandChannel.ReceiveData += m_commandChannel_ReceiveData;
m_commandChannel.ReceiveDataException += m_commandChannel_ReceiveDataException;
m_commandChannel.SendDataException += m_commandChannel_SendDataException;
}
}
}
///
/// Gets the total number of measurements processed through this data publisher over the lifetime of the subscriber.
///
public long LifetimeMeasurements => m_lifetimeMeasurements;
///
/// Gets the minimum value of the measurements per second calculation.
///
public long MinimumMeasurementsPerSecond => m_minimumMeasurementsPerSecond;
///
/// Gets the maximum value of the measurements per second calculation.
///
public long MaximumMeasurementsPerSecond => m_maximumMeasurementsPerSecond;
///
/// Gets the average value of the measurements per second calculation.
///
public long AverageMeasurementsPerSecond
{
get
{
if (m_measurementsPerSecondCount == 0L)
return 0L;
return m_totalMeasurementsPerSecond / m_measurementsPerSecondCount;
}
}
///
/// Gets the minimum latency calculated over the full lifetime of the subscriber.
///
public int LifetimeMinimumLatency => (int)Ticks.ToMilliseconds(m_lifetimeMinimumLatency);
///
/// Gets the maximum latency calculated over the full lifetime of the subscriber.
///
public int LifetimeMaximumLatency => (int)Ticks.ToMilliseconds(m_lifetimeMaximumLatency);
///
/// Gets the average latency calculated over the full lifetime of the subscriber.
///
public int LifetimeAverageLatency
{
get
{
if (m_lifetimeLatencyMeasurements == 0)
return -1;
return (int)Ticks.ToMilliseconds(m_lifetimeTotalLatency / m_lifetimeLatencyMeasurements);
}
}
///
/// Gets real-time as determined by either the local clock or the latest measurement received.
///
protected Ticks RealTime => m_useLocalClockAsRealTime ? (Ticks)DateTime.UtcNow.Ticks : m_realTime;
#endregion
#region [ Methods ]
///
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected override void Dispose(bool disposing)
{
if (!m_disposed)
{
try
{
if (disposing)
{
DataLossInterval = 0.0D;
CommandChannel = null;
DataChannel = null;
DisposeLocalConcentrator();
if ((object)m_dataGapRecoverer != null)
{
m_dataGapRecoverer.RecoveredMeasurements -= m_dataGapRecoverer_RecoveredMeasurements;
m_dataGapRecoverer.StatusMessage -= m_dataGapRecoverer_StatusMessage;
m_dataGapRecoverer.ProcessException -= m_dataGapRecoverer_ProcessException;
m_dataGapRecoverer.Dispose();
m_dataGapRecoverer = null;
}
if ((object)m_runTimeLog != null)
{
m_runTimeLog.ProcessException -= m_runTimeLog_ProcessException;
m_runTimeLog.Dispose();
m_runTimeLog = null;
}
if ((object)m_subscribedDevicesTimer != null)
{
m_subscribedDevicesTimer.Elapsed -= SubscribedDevicesTimer_Elapsed;
m_subscribedDevicesTimer.Dispose();
m_subscribedDevicesTimer = null;
}
}
}
finally
{
m_disposed = true; // Prevent duplicate dispose.
base.Dispose(disposing); // Call base class Dispose().
}
}
}
///
/// Initializes .
///
public override void Initialize()
{
base.Initialize();
Dictionary settings = Settings;
// Setup connection to data publishing server with or without authentication required
if (settings.TryGetValue("requireAuthentication", out string setting))
RequireAuthentication = setting.ParseBoolean();
// See if user has opted for different operational modes
if (settings.TryGetValue("operationalModes", out setting) && Enum.TryParse(setting, true, out OperationalModes operationalModes))
OperationalModes = operationalModes;
// Set the security mode if explicitly defined
if (!settings.TryGetValue("securityMode", out setting) || !Enum.TryParse(setting, true, out m_securityMode))
m_securityMode = SecurityMode.None;
// Apply gateway compression mode to operational mode flags
if (settings.TryGetValue("compressionModes", out setting) && Enum.TryParse(setting, true, out CompressionModes compressionModes))
CompressionModes = compressionModes;
// Check if output measurements should be filtered to only those belonging to the subscriber
m_filterOutputMeasurements = !settings.TryGetValue("filterOutputMeasurements", out setting) || setting.ParseBoolean();
// Check if the subscriber supports real-time and historical processing
m_supportsRealTimeProcessing = !settings.TryGetValue("supportsRealTimeProcessing", out setting) || setting.ParseBoolean();
m_supportsTemporalProcessing = settings.TryGetValue("supportsTemporalProcessing", out setting) && setting.ParseBoolean();
if (settings.TryGetValue("useSimpleTcpClient", out setting))
UseSimpleTcpClient = setting.ParseBoolean();
if (settings.TryGetValue("useZeroMQChannel", out setting))
m_useZeroMQChannel = setting.ParseBoolean();
// FUTURE: Remove this exception when CURVE is enabled in GSF ZeroMQ library
if (m_useZeroMQChannel && m_securityMode == SecurityMode.TLS)
throw new ArgumentException("CURVE security settings are not yet available for GSF ZeroMQ client channel.");
// Settings specific to Gateway security
if (m_securityMode == SecurityMode.Gateway)
{
if (!settings.TryGetValue("sharedSecret", out m_sharedSecret) || string.IsNullOrWhiteSpace(m_sharedSecret))
throw new ArgumentException("The \"sharedSecret\" setting must be defined when using Gateway security mode.");
if (!settings.TryGetValue("authenticationID", out m_authenticationID) || string.IsNullOrWhiteSpace(m_authenticationID))
throw new ArgumentException("The \"authenticationID\" setting must be defined when using Gateway security mode.");
}
// Settings specific to Transport Layer Security
if (m_securityMode == SecurityMode.TLS)
{
if (!settings.TryGetValue("localCertificate", out m_localCertificate) || !File.Exists(m_localCertificate))
m_localCertificate = GetLocalCertificate();
if (!settings.TryGetValue("remoteCertificate", out m_remoteCertificate) || !RemoteCertificateExists())
throw new ArgumentException("The \"remoteCertificate\" setting must be defined and certificate file must exist when using TLS security mode.");
if (!settings.TryGetValue("validPolicyErrors", out setting) || !Enum.TryParse(setting, out m_validPolicyErrors))
m_validPolicyErrors = SslPolicyErrors.None;
if (!settings.TryGetValue("validChainFlags", out setting) || !Enum.TryParse(setting, out m_validChainFlags))
m_validChainFlags = X509ChainStatusFlags.NoError;
if (settings.TryGetValue("checkCertificateRevocation", out setting) && !string.IsNullOrWhiteSpace(setting))
m_checkCertificateRevocation = setting.ParseBoolean();
else
m_checkCertificateRevocation = true;
}
// Check if measurements for this connection should be marked as "internal" - i.e., owned and allowed for proxy
if (settings.TryGetValue("internal", out setting))
m_internal = setting.ParseBoolean();
// Check if user has explicitly defined the ReceiveInternalMetadata flag
if (settings.TryGetValue("receiveInternalMetadata", out setting))
ReceiveInternalMetadata = setting.ParseBoolean();
// Check if user has explicitly defined the ReceiveExternalMetadata flag
if (settings.TryGetValue("receiveExternalMetadata", out setting))
ReceiveExternalMetadata = setting.ParseBoolean();
// Check if user has explicitly defined the MutualSubscription flag
if (settings.TryGetValue(nameof(MutualSubscription), out setting) && setting.ParseBoolean())
{
MutualSubscription = true;
ReceiveInternalMetadata = true;
ReceiveExternalMetadata = false;
m_filterOutputMeasurements = false;
}
// Check if user has defined a meta-data synchronization timeout
if (settings.TryGetValue("metadataSynchronizationTimeout", out setting) && int.TryParse(setting, out int metadataSynchronizationTimeout))
m_metadataSynchronizationTimeout = metadataSynchronizationTimeout;
// Check if user has defined a flag for using a transaction during meta-data synchronization
if (settings.TryGetValue("useTransactionForMetadata", out setting))
m_useTransactionForMetadata = setting.ParseBoolean();
// Check if user wants to request that publisher use millisecond resolution to conserve bandwidth
if (settings.TryGetValue("useMillisecondResolution", out setting))
m_useMillisecondResolution = setting.ParseBoolean();
// Check if user wants to request that publisher remove NaN from the data stream to conserve bandwidth
if (settings.TryGetValue("requestNaNValueFilter", out setting))
m_requestNaNValueFilter = setting.ParseBoolean();
// Check if user has defined any meta-data filter expressions
if (settings.TryGetValue("metadataFilters", out setting))
m_metadataFilters = setting;
// Define auto connect setting
if (settings.TryGetValue("autoConnect", out setting))
{
m_autoConnect = setting.ParseBoolean();
if (m_autoConnect)
m_autoSynchronizeMetadata = true;
}
// Define the maximum allowed exceptions before resetting the connection
if (settings.TryGetValue("allowedParsingExceptions", out setting))
m_allowedParsingExceptions = int.Parse(setting);
// Define the window of time over which parsing exceptions are tolerated
if (settings.TryGetValue("parsingExceptionWindow", out setting))
m_parsingExceptionWindow = Ticks.FromSeconds(double.Parse(setting));
// Check if synchronize meta-data is explicitly enabled or disabled
if (settings.TryGetValue("synchronizeMetadata", out setting))
m_autoSynchronizeMetadata = setting.ParseBoolean();
// Determine if source name prefixes should be applied during metadata synchronization
if (settings.TryGetValue("useSourcePrefixNames", out setting))
m_useSourcePrefixNames = setting.ParseBoolean();
// Define data loss interval
if (settings.TryGetValue("dataLossInterval", out setting) && double.TryParse(setting, out double interval))
DataLossInterval = interval;
// Define buffer size
if (!settings.TryGetValue("bufferSize", out setting) || !int.TryParse(setting, out int bufferSize))
bufferSize = ClientBase.DefaultReceiveBufferSize;
if (settings.TryGetValue("useLocalClockAsRealTime", out setting))
m_useLocalClockAsRealTime = setting.ParseBoolean();
if (m_autoConnect)
{
// Connect to local events when automatically engaging connection cycle
ConnectionAuthenticated += DataSubscriber_ConnectionAuthenticated;
MetaDataReceived += DataSubscriber_MetaDataReceived;
// Update output measurements to include "subscribed" points
UpdateOutputMeasurements(true);
}
else if (m_autoSynchronizeMetadata)
{
// Output measurements do not include "subscribed" points,
// but should still be filtered if applicable
TryFilterOutputMeasurements();
}
if (m_securityMode != SecurityMode.TLS)
{
if (m_useZeroMQChannel)
{
// Create a new ZeroMQ Dealer
ZeroMQClient commandChannel = new ZeroMQClient();
// Initialize default settings
commandChannel.PersistSettings = false;
commandChannel.MaxConnectionAttempts = 1;
commandChannel.ReceiveBufferSize = bufferSize;
commandChannel.SendBufferSize = bufferSize;
// Assign command channel client reference and attach to needed events
CommandChannel = commandChannel;
}
else if (UseSimpleTcpClient)
{
// Create a new simple TCP client
TcpSimpleClient commandChannel = new TcpSimpleClient();
// Initialize default settings
commandChannel.PayloadAware = true;
commandChannel.PersistSettings = false;
commandChannel.MaxConnectionAttempts = 1;
commandChannel.ReceiveBufferSize = bufferSize;
commandChannel.SendBufferSize = bufferSize;
commandChannel.NoDelay = true;
// Assign command channel client reference and attach to needed events
CommandChannel = commandChannel;
}
else
{
// Create a new TCP client
TcpClient commandChannel = new TcpClient();
// Initialize default settings
commandChannel.PayloadAware = true;
commandChannel.PersistSettings = false;
commandChannel.MaxConnectionAttempts = 1;
commandChannel.ReceiveBufferSize = bufferSize;
commandChannel.SendBufferSize = bufferSize;
commandChannel.NoDelay = true;
// Assign command channel client reference and attach to needed events
CommandChannel = commandChannel;
}
}
else
{
if (m_useZeroMQChannel)
{
// Create a new ZeroMQ Dealer with CURVE security enabled
ZeroMQClient commandChannel = new ZeroMQClient();
// Initialize default settings
commandChannel.PersistSettings = false;
commandChannel.MaxConnectionAttempts = 1;
commandChannel.ReceiveBufferSize = bufferSize;
commandChannel.SendBufferSize = bufferSize;
// FUTURE: Parse certificate and pass keys to ZeroMQClient for CURVE security
// Assign command channel client reference and attach to needed events
CommandChannel = commandChannel;
}
else
{
// Create a new TLS client and certificate checker
TlsClient commandChannel = new TlsClient();
SimpleCertificateChecker certificateChecker = new SimpleCertificateChecker();
// Set up certificate checker
certificateChecker.TrustedCertificates.Add(new X509Certificate2(FilePath.GetAbsolutePath(m_remoteCertificate)));
certificateChecker.ValidPolicyErrors = m_validPolicyErrors;
certificateChecker.ValidChainFlags = m_validChainFlags;
// Initialize default settings
commandChannel.PayloadAware = true;
commandChannel.PersistSettings = false;
commandChannel.MaxConnectionAttempts = 1;
commandChannel.CertificateFile = FilePath.GetAbsolutePath(m_localCertificate);
commandChannel.CheckCertificateRevocation = m_checkCertificateRevocation;
commandChannel.CertificateChecker = certificateChecker;
commandChannel.ReceiveBufferSize = bufferSize;
commandChannel.SendBufferSize = bufferSize;
commandChannel.NoDelay = true;
// Assign command channel client reference and attach to needed events
CommandChannel = commandChannel;
}
}
// Get proper connection string - either from specified command channel or from base connection string
if (settings.TryGetValue("commandChannel", out setting))
m_commandChannel.ConnectionString = setting;
else
m_commandChannel.ConnectionString = ConnectionString;
// Check for simplified compression setup flag
if (settings.TryGetValue("compression", out setting) && setting.ParseBoolean())
{
CompressionModes |= CompressionModes.TSSC | CompressionModes.GZip;
OperationalModes |= OperationalModes.CompressPayloadData | OperationalModes.CompressMetadata | OperationalModes.CompressSignalIndexCache | OperationalModes.UseCommonSerializationFormat;
}
// Get logging path, if any has been defined
if (settings.TryGetValue("loggingPath", out setting))
{
setting = FilePath.GetDirectoryName(FilePath.GetAbsolutePath(setting));
if (Directory.Exists(setting))
m_loggingPath = setting;
else
OnStatusMessage(MessageLevel.Info, $"Logging path \"{setting}\" not found, defaulting to \"{FilePath.GetAbsolutePath("")}\"...", flags: MessageFlags.UsageIssue);
}
// Initialize data gap recovery processing, if requested
if (settings.TryGetValue("dataGapRecovery", out setting))
{
// Make sure setting exists to allow user to by-pass phasor data source validation at startup
ConfigurationFile configFile = ConfigurationFile.Current;
CategorizedSettingsElementCollection systemSettings = configFile.Settings["systemSettings"];
CategorizedSettingsElement dataGapRecoveryEnabledSetting = systemSettings["DataGapRecoveryEnabled"];
// See if this node should process phasor source validation
if ((object)dataGapRecoveryEnabledSetting == null || dataGapRecoveryEnabledSetting.ValueAsBoolean())
{
// Example connection string for data gap recovery:
// dataGapRecovery={enabled=true; recoveryStartDelay=10.0; minimumRecoverySpan=0.0; maximumRecoverySpan=3600.0}
Dictionary dataGapSettings = setting.ParseKeyValuePairs();
if (dataGapSettings.TryGetValue("enabled", out setting) && setting.ParseBoolean())
{
// Remove dataGapRecovery connection setting from command channel connection string, if defined there.
// This will prevent any recursive data gap recovery operations from being established:
Dictionary connectionSettings = m_commandChannel.ConnectionString.ParseKeyValuePairs();
connectionSettings.Remove("dataGapRecovery");
connectionSettings.Remove("autoConnect");
connectionSettings.Remove("synchronizeMetadata");
connectionSettings.Remove("outputMeasurements");
// Note that the data gap recoverer will connect on the same command channel port as
// the real-time subscriber (TCP only)
m_dataGapRecoveryEnabled = true;
m_dataGapRecoverer = new DataGapRecoverer();
m_dataGapRecoverer.SourceConnectionName = Name;
m_dataGapRecoverer.DataSource = DataSource;
m_dataGapRecoverer.ConnectionString = string.Join("; ", $"autoConnect=false; synchronizeMetadata=false{(string.IsNullOrWhiteSpace(m_loggingPath) ? "" : "; loggingPath=" + m_loggingPath)}", dataGapSettings.JoinKeyValuePairs(), connectionSettings.JoinKeyValuePairs());
m_dataGapRecoverer.FilterExpression = this.OutputMeasurementKeys().Select(key => key.SignalID.ToString()).ToDelimitedString(';');
m_dataGapRecoverer.RecoveredMeasurements += m_dataGapRecoverer_RecoveredMeasurements;
m_dataGapRecoverer.StatusMessage += m_dataGapRecoverer_StatusMessage;
m_dataGapRecoverer.ProcessException += m_dataGapRecoverer_ProcessException;
m_dataGapRecoverer.Initialize();
}
else
{
m_dataGapRecoveryEnabled = false;
}
}
}
else
{
m_dataGapRecoveryEnabled = false;
}
if (!settings.TryGetValue("BypassStatistics", out setting) || !setting.ParseBoolean())
{
void statisticsCalculated(object sender, EventArgs args) => ResetMeasurementsPerSecondCounters();
StatisticsEngine.Register(this, "Subscriber", "SUB");
StatisticsEngine.Calculated += statisticsCalculated;
Disposed += (sender, args) => StatisticsEngine.Calculated -= statisticsCalculated;
}
else
{
m_bypassingStatistics = true;
}
if (PersistConnectionForMetadata)
m_commandChannel.ConnectAsync();
Initialized = true;
}
// Gets the path to the local certificate from the configuration file
private string GetLocalCertificate()
{
CategorizedSettingsElement localCertificateElement = ConfigurationFile.Current.Settings["systemSettings"]["LocalCertificate"];
string localCertificate = null;
if ((object)localCertificateElement != null)
localCertificate = localCertificateElement.Value;
if ((object)localCertificate == null || !File.Exists(FilePath.GetAbsolutePath(localCertificate)))
throw new InvalidOperationException("Unable to find local certificate. Local certificate file must exist when using TLS security mode.");
return localCertificate;
}
// Checks if the specified certificate exists
private bool RemoteCertificateExists()
{
string fullPath = FilePath.GetAbsolutePath(m_remoteCertificate);
CategorizedSettingsElement remoteCertificateElement;
if (!File.Exists(fullPath))
{
remoteCertificateElement = ConfigurationFile.Current.Settings["systemSettings"]["RemoteCertificatesPath"];
if ((object)remoteCertificateElement != null)
{
m_remoteCertificate = Path.Combine(remoteCertificateElement.Value, m_remoteCertificate);
fullPath = FilePath.GetAbsolutePath(m_remoteCertificate);
}
}
return File.Exists(fullPath);
}
// Initialize (or reinitialize) the output measurements associated with the data subscriber.
// Returns true if output measurements were updated, otherwise false if they remain the same.
private bool UpdateOutputMeasurements(bool initialCall = false)
{
IMeasurement[] originalOutputMeasurements = OutputMeasurements;
// Reapply output measurements if reinitializing - this way filter expressions and/or sourceIDs
// will be reapplied. This can be important after a meta-data refresh which may have added new
// measurements that could now be applicable as desired output measurements.
if (!initialCall)
{
if (Settings.TryGetValue("outputMeasurements", out string setting))
OutputMeasurements = ParseOutputMeasurements(DataSource, true, setting);
OutputSourceIDs = OutputSourceIDs;
}
// If active measurements are defined, attempt to defined desired subscription points from there
if ((m_securityMode == SecurityMode.TLS || m_filterOutputMeasurements) && (object)DataSource != null && DataSource.Tables.Contains("ActiveMeasurements"))
{
try
{
// Filter to points associated with this subscriber that have been requested for subscription, are enabled and not owned locally
DataRow[] filteredRows = DataSource.Tables["ActiveMeasurements"].Select("Subscribed <> 0");
List subscribedMeasurements = new List();
Guid signalID;
foreach (DataRow row in filteredRows)
{
// Create a new measurement for the provided field level information
Measurement measurement = new Measurement();
// Parse primary measurement identifier
signalID = row["SignalID"].ToNonNullString(Guid.Empty.ToString()).ConvertToType();
// Set measurement key if defined
MeasurementKey key = MeasurementKey.LookUpOrCreate(signalID, row["ID"].ToString());
measurement.Metadata = key.Metadata;
subscribedMeasurements.Add(measurement);
}
if (subscribedMeasurements.Count > 0)
{
// Combine subscribed output measurement with any existing output measurement and return unique set
if ((object)OutputMeasurements == null)
OutputMeasurements = subscribedMeasurements.ToArray();
else
OutputMeasurements = subscribedMeasurements.Concat(OutputMeasurements).Distinct().ToArray();
}
}
catch (Exception ex)
{
// Errors here may not be catastrophic, this simply limits the auto-assignment of input measurement keys desired for subscription
OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed to apply subscribed measurements to subscription filter: {ex.Message}", ex));
}
}
// Ensure that we are not attempting to subscribe to
// measurements that we know cannot be published
TryFilterOutputMeasurements();
// Determine if output measurements have changed
return originalOutputMeasurements.CompareTo(OutputMeasurements, false) != 0;
}
// When synchronizing meta-data, the publisher sends meta-data for all possible signals we can subscribe to.
// Here we check each signal defined in OutputMeasurements to determine whether that signal was defined in
// the published meta-data rather than blindly attempting to subscribe to all signals.
private void TryFilterOutputMeasurements()
{
if (!m_filterOutputMeasurements)
return;
IEnumerable measurementIDs;
ISet measurementIDSet;
Guid signalID = Guid.Empty;
try
{
if ((object)OutputMeasurements != null && (object)DataSource != null && DataSource.Tables.Contains("ActiveMeasurements"))
{
// Have to use a Convert expression for DeviceID column in Select function
// here since SQLite doesn't report data types for COALESCE based columns
measurementIDs = DataSource.Tables["ActiveMeasurements"]
.Select($"Convert(DeviceID, 'System.String') = '{ID}'")
.Where(row => Guid.TryParse(row["SignalID"].ToNonNullString(), out signalID))
.Select(row => signalID);
measurementIDSet = new HashSet(measurementIDs);
OutputMeasurements = OutputMeasurements.Where(measurement => measurementIDSet.Contains(measurement.ID)).ToArray();
}
}
catch (Exception ex)
{
OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Error when filtering output measurements by device ID: {ex.Message}", ex));
}
}
///
/// Authenticates subscriber to a data publisher.
///
/// Shared secret used to look up private crypto key and initialization vector.
/// Authentication ID that publisher will use to validate subscriber identity.
/// true if authentication transmission was successful; otherwise false.
public virtual bool Authenticate(string sharedSecret, string authenticationID)
{
if (!string.IsNullOrWhiteSpace(authenticationID))
{
try
{
using (BlockAllocatedMemoryStream buffer = new BlockAllocatedMemoryStream())
{
byte[] salt = new byte[DataPublisher.CipherSaltLength];
byte[] bytes;
// Generate some random prefix data to make sure auth key transmission is always unique
Random.GetBytes(salt);
// Get encoded bytes of authentication key
bytes = salt.Combine(m_encoding.GetBytes(authenticationID));
// Encrypt authentication key
bytes = bytes.Encrypt(sharedSecret, CipherStrength.Aes256);
// Write encoded authentication key length into buffer
buffer.Write(BigEndian.GetBytes(bytes.Length), 0, 4);
// Encode encrypted authentication key into buffer
buffer.Write(bytes, 0, bytes.Length);
// Send authentication command to server with associated command buffer
return SendServerCommand(ServerCommand.Authenticate, buffer.ToArray());
}
}
catch (Exception ex)
{
OnProcessException(MessageLevel.Error, new InvalidOperationException("Exception occurred while trying to authenticate publisher subscription: " + ex.Message, ex));
}
}
else
{
OnProcessException(MessageLevel.Error, new InvalidOperationException("Cannot authenticate subscription without a connection string."));
}
return false;
}
///
/// Subscribes (or re-subscribes) to a data publisher for a set of data points.
///
/// Configuration object that defines the subscription.
/// true if subscribe transmission was successful; otherwise false.
public bool Subscribe(SubscriptionInfo info)
{
SynchronizedSubscriptionInfo synchronizedSubscriptionInfo = info as SynchronizedSubscriptionInfo;
if ((object)synchronizedSubscriptionInfo != null)
return SynchronizedSubscribe(synchronizedSubscriptionInfo);
UnsynchronizedSubscriptionInfo unsynchronizedSubscriptionInfo = info as UnsynchronizedSubscriptionInfo;
if ((object)unsynchronizedSubscriptionInfo != null)
return UnsynchronizedSubscribe(unsynchronizedSubscriptionInfo);
throw new NotSupportedException("Type of subscription used is not supported");
}
///
/// Subscribes (or re-subscribes) to a data publisher for a synchronized set of data points.
///
/// Configuration object that defines the subscription.
/// true if subscribe transmission was successful; otherwise false.
public bool SynchronizedSubscribe(SynchronizedSubscriptionInfo info)
{
StringBuilder connectionString = new StringBuilder();
AssemblyInfo assemblyInfo = new AssemblyInfo(typeof(DataSubscriber).Assembly);
// Dispose of any previously established local concentrator
DisposeLocalConcentrator();
if (info.RemotelySynchronized)
{
connectionString.AppendFormat("framesPerSecond={0};", info.FramesPerSecond);
connectionString.AppendFormat("lagTime={0};", info.LagTime);
connectionString.AppendFormat("leadTime={0};", info.LeadTime);
connectionString.AppendFormat("includeTime=false;");
connectionString.AppendFormat("useLocalClockAsRealTime={0};", info.UseLocalClockAsRealTime);
connectionString.AppendFormat("ignoreBadTimestamps={0};", info.IgnoreBadTimestamps);
connectionString.AppendFormat("allowSortsByArrival={0};", info.AllowSortsByArrival);
connectionString.AppendFormat("timeResolution={0};", info.TimeResolution);
connectionString.AppendFormat("allowPreemptivePublishing={0};", info.AllowPreemptivePublishing);
connectionString.AppendFormat("requestNaNValueFilter={0};", info.RequestNaNValueFilter);
connectionString.AppendFormat("downsamplingMethod={0};", info.DownsamplingMethod);
connectionString.AppendFormat("processingInterval={0};", info.ProcessingInterval);
connectionString.AppendFormat("assemblyInfo={{source={0};version={1}.{2}.{3};buildDate={4:yyyy-MM-dd HH:mm:ss}}};", assemblyInfo.Name, assemblyInfo.Version.Major, assemblyInfo.Version.Minor, assemblyInfo.Version.Build, assemblyInfo.BuildDate);
if (!string.IsNullOrWhiteSpace(info.FilterExpression))
connectionString.AppendFormat("inputMeasurementKeys={{{0}}};", info.FilterExpression);
if (info.UdpDataChannel)
connectionString.AppendFormat("dataChannel={{localport={0}}};", info.DataChannelLocalPort);
if (!string.IsNullOrWhiteSpace(info.StartTime))
connectionString.AppendFormat("startTimeConstraint={0};", info.StartTime);
if (!string.IsNullOrWhiteSpace(info.StopTime))
connectionString.AppendFormat("stopTimeConstraint={0};", info.StopTime);
if (!string.IsNullOrWhiteSpace(info.ConstraintParameters))
connectionString.AppendFormat("timeConstraintParameters={0};", info.ConstraintParameters);
if (!string.IsNullOrWhiteSpace(info.ExtraConnectionStringParameters))
connectionString.AppendFormat("{0};", info.ExtraConnectionStringParameters);
return Subscribe(true, info.UseCompactMeasurementFormat, connectionString.ToString());
}
// Locally concentrated subscription simply uses an unsynchronized subscription and concentrates the
// measurements on the subscriber side
if (Subscribe(FromLocallySynchronizedInfo(info)))
{
// Establish a local concentrator to synchronize received measurements
LocalConcentrator localConcentrator = new LocalConcentrator(this);
localConcentrator.ProcessException += m_localConcentrator_ProcessException;
localConcentrator.FramesPerSecond = info.FramesPerSecond;
localConcentrator.LagTime = info.LagTime;
localConcentrator.LeadTime = info.LeadTime;
localConcentrator.UseLocalClockAsRealTime = info.UseLocalClockAsRealTime;
localConcentrator.IgnoreBadTimestamps = info.IgnoreBadTimestamps;
localConcentrator.AllowSortsByArrival = info.AllowSortsByArrival;
localConcentrator.TimeResolution = info.TimeResolution;
localConcentrator.AllowPreemptivePublishing = info.AllowPreemptivePublishing;
localConcentrator.DownsamplingMethod = info.DownsamplingMethod;
localConcentrator.UsePrecisionTimer = false;
// Parse time constraints, if defined
DateTime startTimeConstraint = !string.IsNullOrWhiteSpace(info.StartTime) ? ParseTimeTag(info.StartTime) : DateTime.MinValue;
DateTime stopTimeConstraint = !string.IsNullOrWhiteSpace(info.StopTime) ? ParseTimeTag(info.StopTime) : DateTime.MaxValue;
// When processing historical data, timestamps should not be evaluated for reasonability
if (startTimeConstraint != DateTime.MinValue || stopTimeConstraint != DateTime.MaxValue)
{
localConcentrator.PerformTimestampReasonabilityCheck = false;
localConcentrator.LeadTime = double.MaxValue;
}
// Assign alternate processing interval, if defined
if (info.ProcessingInterval != -1)
localConcentrator.ProcessingInterval = info.ProcessingInterval;
// Start local concentrator
localConcentrator.Start();
// Move concentrator to member variable
Interlocked.Exchange(ref m_localConcentrator, localConcentrator);
return true;
}
return false;
}
///
/// Subscribes (or re-subscribes) to a data publisher for an unsynchronized set of data points.
///
/// Configuration object that defines the subscription.
/// true if subscribe transmission was successful; otherwise false.
public bool UnsynchronizedSubscribe(UnsynchronizedSubscriptionInfo info)
{
// Dispose of any previously established local concentrator
DisposeLocalConcentrator();
StringBuilder connectionString = new StringBuilder();
AssemblyInfo assemblyInfo = new AssemblyInfo(typeof(DataSubscriber).Assembly);
connectionString.AppendFormat("trackLatestMeasurements={0};", info.Throttled);
connectionString.AppendFormat("publishInterval={0};", info.PublishInterval);
connectionString.AppendFormat("includeTime={0};", info.IncludeTime);
connectionString.AppendFormat("lagTime={0};", info.LagTime);
connectionString.AppendFormat("leadTime={0};", info.LeadTime);
connectionString.AppendFormat("useLocalClockAsRealTime={0};", info.UseLocalClockAsRealTime);
connectionString.AppendFormat("processingInterval={0};", info.ProcessingInterval);
connectionString.AppendFormat("useMillisecondResolution={0};", info.UseMillisecondResolution);
connectionString.AppendFormat("requestNaNValueFilter={0};", info.RequestNaNValueFilter);
connectionString.AppendFormat("assemblyInfo={{source={0};version={1}.{2}.{3};buildDate={4:yyyy-MM-dd HH:mm:ss}}};", assemblyInfo.Name, assemblyInfo.Version.Major, assemblyInfo.Version.Minor, assemblyInfo.Version.Build, assemblyInfo.BuildDate);
if (!string.IsNullOrWhiteSpace(info.FilterExpression))
connectionString.AppendFormat("inputMeasurementKeys={{{0}}};", info.FilterExpression);
if (info.UdpDataChannel)
connectionString.AppendFormat("dataChannel={{localport={0}}};", info.DataChannelLocalPort);
if (!string.IsNullOrWhiteSpace(info.StartTime))
connectionString.AppendFormat("startTimeConstraint={0};", info.StartTime);
if (!string.IsNullOrWhiteSpace(info.StopTime))
connectionString.AppendFormat("stopTimeConstraint={0};", info.StopTime);
if (!string.IsNullOrWhiteSpace(info.ConstraintParameters))
connectionString.AppendFormat("timeConstraintParameters={0};", info.ConstraintParameters);
if (!string.IsNullOrWhiteSpace(info.ExtraConnectionStringParameters))
connectionString.AppendFormat("{0};", info.ExtraConnectionStringParameters);
// Make sure not to monitor for data loss any faster than down-sample time on throttled connections - additionally
// you will want to make sure data stream monitor is twice lag-time to allow time for initial points to arrive.
if (info.Throttled && (object)m_dataStreamMonitor != null && m_dataStreamMonitor.Interval / 1000.0D < info.LagTime)
m_dataStreamMonitor.Interval = (int)(2.0D * info.LagTime * 1000.0D);
// Set millisecond resolution member variable for compact measurement parsing
m_useMillisecondResolution = info.UseMillisecondResolution;
return Subscribe(false, info.UseCompactMeasurementFormat, connectionString.ToString());
}
///
/// Subscribes (or re-subscribes) to a data publisher for a remotely synchronized set of data points.
///
/// Boolean value that determines if the compact measurement format should be used. Set to false for full fidelity measurement serialization; otherwise set to true for bandwidth conservation.
/// The desired number of data frames per second.
/// Allowed past time deviation tolerance, in seconds (can be sub-second).
/// Allowed future time deviation tolerance, in seconds (can be sub-second).
/// Filtering expression that defines the measurements that are being subscribed.
/// Desired UDP return data channel connection string to use for data packet transmission. Set to null to use TCP channel for data transmission.
/// Boolean value that determines whether or not to use the local clock time as real-time.
/// Boolean value that determines if bad timestamps (as determined by measurement's timestamp quality) should be ignored when sorting measurements.
/// Gets or sets flag that determines whether or not to allow incoming measurements with bad timestamps to be sorted by arrival time.
/// Gets or sets the maximum time resolution, in ticks, to use when sorting measurements by timestamps into their proper destination frame.
/// Gets or sets flag that allows system to preemptively publish frames assuming all expected measurements have arrived.
/// Gets the total number of down-sampled measurements processed by the concentrator.
/// Defines a relative or exact start time for the temporal constraint to use for historical playback.
/// Defines a relative or exact stop time for the temporal constraint to use for historical playback.
/// Defines any temporal parameters related to the constraint to use for historical playback.
/// Defines the desired processing interval milliseconds, i.e., historical play back speed, to use when temporal constraints are defined.
/// Comma separated list of wait handle names used to establish external event wait handles needed for inter-adapter synchronization.
/// Maximum wait time for external events, in milliseconds, before proceeding.
/// true if subscribe transmission was successful; otherwise false.
///
///
/// When the or temporal processing constraints are defined (i.e., not null), this
/// specifies the start and stop time over which the subscriber session will process data. Passing in null for the
/// and specifies the subscriber session will process data in standard, i.e., real-time, operation.
///
///
/// With the exception of the values of -1 and 0, the value specifies the desired historical playback data
/// processing interval in milliseconds. This is basically a delay, or timer interval, over which to process data. Setting this value to -1 means
/// to use the default processing interval while setting the value to 0 means to process data as fast as possible.
///
///
/// The and parameters can be specified in one of the
/// following formats:
///
///
/// Time Format
/// Format Description
///
/// -
/// 12-30-2000 23:59:59.033
/// Absolute date and time.
///
/// -
/// *
/// Evaluates to .
///
/// -
/// *-20s
/// Evaluates to 20 seconds before .
///
/// -
/// *-10m
/// Evaluates to 10 minutes before .
///
/// -
/// *-1h
/// Evaluates to 1 hour before .
///
/// -
/// *-1d
/// Evaluates to 1 day before .
///
///
///
///
[Obsolete("Preferred method uses SubscriptionInfo object to subscribe.", false)]
public virtual bool RemotelySynchronizedSubscribe(bool compactFormat, int framesPerSecond, double lagTime, double leadTime, string filterExpression, string dataChannel = null, bool useLocalClockAsRealTime = false, bool ignoreBadTimestamps = false, bool allowSortsByArrival = true, long timeResolution = Ticks.PerMillisecond, bool allowPreemptivePublishing = true, DownsamplingMethod downsamplingMethod = DownsamplingMethod.LastReceived, string startTime = null, string stopTime = null, string constraintParameters = null, int processingInterval = -1, string waitHandleNames = null, int waitHandleTimeout = 0)
{
// Dispose of any previously established local concentrator
DisposeLocalConcentrator();
StringBuilder connectionString = new StringBuilder();
AssemblyInfo assemblyInfo = new AssemblyInfo(typeof(DataSubscriber).Assembly);
connectionString.AppendFormat("framesPerSecond={0}; ", framesPerSecond);
connectionString.AppendFormat("lagTime={0}; ", lagTime);
connectionString.AppendFormat("leadTime={0}; ", leadTime);
connectionString.AppendFormat("inputMeasurementKeys={{{0}}}; ", filterExpression.ToNonNullString());
connectionString.AppendFormat("dataChannel={{{0}}}; ", dataChannel.ToNonNullString());
connectionString.AppendFormat("includeTime=false; ");
connectionString.AppendFormat("useLocalClockAsRealTime={0}; ", useLocalClockAsRealTime);
connectionString.AppendFormat("ignoreBadTimestamps={0}; ", ignoreBadTimestamps);
connectionString.AppendFormat("allowSortsByArrival={0}; ", allowSortsByArrival);
connectionString.AppendFormat("timeResolution={0}; ", (long)timeResolution);
connectionString.AppendFormat("allowPreemptivePublishing={0}; ", allowPreemptivePublishing);
connectionString.AppendFormat("downsamplingMethod={0}; ", downsamplingMethod.ToString());
connectionString.AppendFormat("startTimeConstraint={0}; ", startTime.ToNonNullString());
connectionString.AppendFormat("stopTimeConstraint={0}; ", stopTime.ToNonNullString());
connectionString.AppendFormat("timeConstraintParameters={0}; ", constraintParameters.ToNonNullString());
connectionString.AppendFormat("processingInterval={0}; ", processingInterval);
connectionString.AppendFormat("assemblyInfo={{source={0}; version={1}.{2}.{3}; buildDate={4:yyyy-MM-dd HH:mm:ss}}}", assemblyInfo.Name, assemblyInfo.Version.Major, assemblyInfo.Version.Minor, assemblyInfo.Version.Build, assemblyInfo.BuildDate);
if (!string.IsNullOrWhiteSpace(waitHandleNames))
{
connectionString.AppendFormat("; waitHandleNames={0}", waitHandleNames);
connectionString.AppendFormat("; waitHandleTimeout={0}", waitHandleTimeout);
}
return Subscribe(true, compactFormat, connectionString.ToString());
}
///
/// Subscribes (or re-subscribes) to a data publisher for a locally synchronized set of data points.
///
/// Boolean value that determines if the compact measurement format should be used. Set to false for full fidelity measurement serialization; otherwise set to true for bandwidth conservation.
/// The desired number of data frames per second.
/// Allowed past time deviation tolerance, in seconds (can be sub-second).
/// Allowed future time deviation tolerance, in seconds (can be sub-second).
/// Filtering expression that defines the measurements that are being subscribed.
/// Desired UDP return data channel connection string to use for data packet transmission. Set to null to use TCP channel for data transmission.
/// Boolean value that determines whether or not to use the local clock time as real-time.
/// Boolean value that determines if bad timestamps (as determined by measurement's timestamp quality) should be ignored when sorting measurements.
/// Gets or sets flag that determines whether or not to allow incoming measurements with bad timestamps to be sorted by arrival time.
/// Gets or sets the maximum time resolution, in ticks, to use when sorting measurements by timestamps into their proper destination frame.
/// Gets or sets flag that allows system to preemptively publish frames assuming all expected measurements have arrived.
/// Gets the total number of down-sampled measurements processed by the concentrator.
/// Defines a relative or exact start time for the temporal constraint to use for historical playback.
/// Defines a relative or exact stop time for the temporal constraint to use for historical playback.
/// Defines any temporal parameters related to the constraint to use for historical playback.
/// Defines the desired processing interval milliseconds, i.e., historical play back speed, to use when temporal constraints are defined.
/// Comma separated list of wait handle names used to establish external event wait handles needed for inter-adapter synchronization.
/// Maximum wait time for external events, in milliseconds, before proceeding.
/// true if subscribe transmission was successful; otherwise false.
///
///
/// When the or temporal processing constraints are defined (i.e., not null), this
/// specifies the start and stop time over which the subscriber session will process data. Passing in null for the
/// and specifies the subscriber session will process data in standard, i.e., real-time, operation.
///
///
/// With the exception of the values of -1 and 0, the value specifies the desired historical playback data
/// processing interval in milliseconds. This is basically a delay, or timer interval, over which to process data. Setting this value to -1 means
/// to use the default processing interval while setting the value to 0 means to process data as fast as possible.
///
///
/// The and parameters can be specified in one of the
/// following formats:
///
///
/// Time Format
/// Format Description
///
/// -
/// 12-30-2000 23:59:59.033
/// Absolute date and time.
///
/// -
/// *
/// Evaluates to .
///
/// -
/// *-20s
/// Evaluates to 20 seconds before .
///
/// -
/// *-10m
/// Evaluates to 10 minutes before .
///
/// -
/// *-1h
/// Evaluates to 1 hour before .
///
/// -
/// *-1d
/// Evaluates to 1 day before .
///
///
///
///
[Obsolete("Preferred method uses SubscriptionInfo object to subscribe.", false)]
public virtual bool LocallySynchronizedSubscribe(bool compactFormat, int framesPerSecond, double lagTime, double leadTime, string filterExpression, string dataChannel = null, bool useLocalClockAsRealTime = false, bool ignoreBadTimestamps = false, bool allowSortsByArrival = true, long timeResolution = Ticks.PerMillisecond, bool allowPreemptivePublishing = true, DownsamplingMethod downsamplingMethod = DownsamplingMethod.LastReceived, string startTime = null, string stopTime = null, string constraintParameters = null, int processingInterval = -1, string waitHandleNames = null, int waitHandleTimeout = 0)
{
// Dispose of any previously established local concentrator
DisposeLocalConcentrator();
// Establish a local concentrator to synchronize received measurements
m_localConcentrator = new LocalConcentrator(this);
m_localConcentrator.ProcessException += m_localConcentrator_ProcessException;
m_localConcentrator.FramesPerSecond = framesPerSecond;
m_localConcentrator.LagTime = lagTime;
m_localConcentrator.LeadTime = leadTime;
m_localConcentrator.UseLocalClockAsRealTime = useLocalClockAsRealTime;
m_localConcentrator.IgnoreBadTimestamps = ignoreBadTimestamps;
m_localConcentrator.AllowSortsByArrival = allowSortsByArrival;
m_localConcentrator.TimeResolution = timeResolution;
m_localConcentrator.AllowPreemptivePublishing = allowPreemptivePublishing;
m_localConcentrator.DownsamplingMethod = downsamplingMethod;
m_localConcentrator.UsePrecisionTimer = false;
// Parse time constraints, if defined
DateTime startTimeConstraint = !string.IsNullOrWhiteSpace(startTime) ? ParseTimeTag(startTime) : DateTime.MinValue;
DateTime stopTimeConstraint = !string.IsNullOrWhiteSpace(stopTime) ? ParseTimeTag(stopTime) : DateTime.MaxValue;
// When processing historical data, timestamps should not be evaluated for reasonability
if (startTimeConstraint != DateTime.MinValue || stopTimeConstraint != DateTime.MaxValue)
{
m_localConcentrator.PerformTimestampReasonabilityCheck = false;
m_localConcentrator.LeadTime = double.MaxValue;
}
// Assign alternate processing interval, if defined
if (processingInterval != -1)
m_localConcentrator.ProcessingInterval = processingInterval;
// Initiate unsynchronized subscribe
StringBuilder connectionString = new StringBuilder();
AssemblyInfo assemblyInfo = new AssemblyInfo(typeof(DataSubscriber).Assembly);
connectionString.AppendFormat("trackLatestMeasurements={0}; ", false);
connectionString.AppendFormat("inputMeasurementKeys={{{0}}}; ", filterExpression.ToNonNullString());
connectionString.AppendFormat("dataChannel={{{0}}}; ", dataChannel.ToNonNullString());
connectionString.AppendFormat("includeTime={0}; ", true);
connectionString.AppendFormat("lagTime={0}; ", 10.0D);
connectionString.AppendFormat("leadTime={0}; ", 5.0D);
connectionString.AppendFormat("useLocalClockAsRealTime={0}; ", false);
connectionString.AppendFormat("startTimeConstraint={0}; ", startTime.ToNonNullString());
connectionString.AppendFormat("stopTimeConstraint={0}; ", stopTime.ToNonNullString());
connectionString.AppendFormat("timeConstraintParameters={0}; ", constraintParameters.ToNonNullString());
connectionString.AppendFormat("processingInterval={0}; ", processingInterval);
connectionString.AppendFormat("useMillisecondResolution={0}; ", m_useMillisecondResolution);
connectionString.AppendFormat("assemblyInfo={{source={0}; version={1}.{2}.{3}; buildDate={4:yyyy-MM-dd HH:mm:ss}}}", assemblyInfo.Name, assemblyInfo.Version.Major, assemblyInfo.Version.Minor, assemblyInfo.Version.Build, assemblyInfo.BuildDate);
if (!string.IsNullOrWhiteSpace(waitHandleNames))
{
connectionString.AppendFormat("; waitHandleNames={0}", waitHandleNames);
connectionString.AppendFormat("; waitHandleTimeout={0}", waitHandleTimeout);
}
// Start subscription process
if (Subscribe(false, compactFormat, connectionString.ToString()))
{
// If subscription succeeds, start local concentrator
m_localConcentrator.Start();
return true;
}
return false;
}
///
/// Subscribes (or re-subscribes) to a data publisher for an unsynchronized set of data points.
///
/// Boolean value that determines if the compact measurement format should be used. Set to false for full fidelity measurement serialization; otherwise set to true for bandwidth conservation.
/// Boolean value that determines if data should be throttled at a set transmission interval or sent on change.
/// Filtering expression that defines the measurements that are being subscribed.
/// Desired UDP return data channel connection string to use for data packet transmission. Set to null to use TCP channel for data transmission.
/// Boolean value that determines if time is a necessary component in streaming data.
/// When is true, defines the data transmission speed in seconds (can be sub-second).
/// When is true, defines the allowed time deviation tolerance to real-time in seconds (can be sub-second).
/// When is true, defines boolean value that determines whether or not to use the local clock time as real-time. Set to false to use latest received measurement timestamp as real-time.
/// Defines a relative or exact start time for the temporal constraint to use for historical playback.
/// Defines a relative or exact stop time for the temporal constraint to use for historical playback.
/// Defines any temporal parameters related to the constraint to use for historical playback.
/// Defines the desired processing interval milliseconds, i.e., historical play back speed, to use when temporal constraints are defined.
/// Comma separated list of wait handle names used to establish external event wait handles needed for inter-adapter synchronization.
/// Maximum wait time for external events, in milliseconds, before proceeding.
/// true if subscribe transmission was successful; otherwise false.
///
///
/// When the or temporal processing constraints are defined (i.e., not null), this
/// specifies the start and stop time over which the subscriber session will process data. Passing in null for the
/// and specifies the subscriber session will process data in standard, i.e., real-time, operation.
///
///
/// With the exception of the values of -1 and 0, the value specifies the desired historical playback data
/// processing interval in milliseconds. This is basically a delay, or timer interval, over which to process data. Setting this value to -1 means
/// to use the default processing interval while setting the value to 0 means to process data as fast as possible.
///
///
/// The and parameters can be specified in one of the
/// following formats:
///
///
/// Time Format
/// Format Description
///
/// -
/// 12-30-2000 23:59:59.033
/// Absolute date and time.
///
/// -
/// *
/// Evaluates to .
///
/// -
/// *-20s
/// Evaluates to 20 seconds before .
///
/// -
/// *-10m
/// Evaluates to 10 minutes before .
///
/// -
/// *-1h
/// Evaluates to 1 hour before .
///
/// -
/// *-1d
/// Evaluates to 1 day before .
///
///
///
///
[Obsolete("Preferred method uses SubscriptionInfo object to subscribe.", false)]
public virtual bool UnsynchronizedSubscribe(bool compactFormat, bool throttled, string filterExpression, string dataChannel = null, bool includeTime = true, double lagTime = 10.0D, double leadTime = 5.0D, bool useLocalClockAsRealTime = false, string startTime = null, string stopTime = null, string constraintParameters = null, int processingInterval = -1, string waitHandleNames = null, int waitHandleTimeout = 0)
{
// Dispose of any previously established local concentrator
DisposeLocalConcentrator();
StringBuilder connectionString = new StringBuilder();
AssemblyInfo assemblyInfo = new AssemblyInfo(typeof(DataSubscriber).Assembly);
connectionString.AppendFormat("trackLatestMeasurements={0}; ", throttled);
connectionString.AppendFormat("inputMeasurementKeys={{{0}}}; ", filterExpression.ToNonNullString());
connectionString.AppendFormat("dataChannel={{{0}}}; ", dataChannel.ToNonNullString());
connectionString.AppendFormat("includeTime={0}; ", includeTime);
connectionString.AppendFormat("lagTime={0}; ", lagTime);
connectionString.AppendFormat("leadTime={0}; ", leadTime);
connectionString.AppendFormat("useLocalClockAsRealTime={0}; ", useLocalClockAsRealTime);
connectionString.AppendFormat("startTimeConstraint={0}; ", startTime.ToNonNullString());
connectionString.AppendFormat("stopTimeConstraint={0}; ", stopTime.ToNonNullString());
connectionString.AppendFormat("timeConstraintParameters={0}; ", constraintParameters.ToNonNullString());
connectionString.AppendFormat("processingInterval={0}; ", processingInterval);
connectionString.AppendFormat("useMillisecondResolution={0}; ", m_useMillisecondResolution);
connectionString.AppendFormat("requestNaNValueFilter={0}; ", m_requestNaNValueFilter);
connectionString.AppendFormat("assemblyInfo={{source={0}; version={1}.{2}.{3}; buildDate={4:yyyy-MM-dd HH:mm:ss}}}", assemblyInfo.Name, assemblyInfo.Version.Major, assemblyInfo.Version.Minor, assemblyInfo.Version.Build, assemblyInfo.BuildDate);
if (!string.IsNullOrWhiteSpace(waitHandleNames))
{
connectionString.AppendFormat("; waitHandleNames={0}", waitHandleNames);
connectionString.AppendFormat("; waitHandleTimeout={0}", waitHandleTimeout);
}
// Make sure not to monitor for data loss any faster than down-sample time on throttled connections - additionally
// you will want to make sure data stream monitor is twice lag-time to allow time for initial points to arrive.
if (throttled && (object)m_dataStreamMonitor != null && m_dataStreamMonitor.Interval / 1000.0D < lagTime)
m_dataStreamMonitor.Interval = (int)(2.0D * lagTime * 1000.0D);
return Subscribe(false, compactFormat, connectionString.ToString());
}
///
/// Subscribes (or re-subscribes) to a data publisher for a set of data points.
///
/// Boolean value that determines if subscription should be remotely synchronized - note that data publisher may not allow remote synchronization.
/// Boolean value that determines if the compact measurement format should be used. Set to false for full fidelity measurement serialization; otherwise set to true for bandwidth conservation.
/// Connection string that defines required and optional parameters for the subscription.
/// true if subscribe transmission was successful; otherwise false.
public virtual bool Subscribe(bool remotelySynchronized, bool compactFormat, string connectionString)
{
bool success = false;
if (!string.IsNullOrWhiteSpace(connectionString))
{
try
{
// Parse connection string to see if it contains a data channel definition
Dictionary settings = connectionString.ParseKeyValuePairs();
UdpClient dataChannel = null;
// Track specified time inclusion for later deserialization
if (settings.TryGetValue("includeTime", out string setting))
m_includeTime = setting.ParseBoolean();
else
m_includeTime = true;
settings.TryGetValue("dataChannel", out setting);
if (!string.IsNullOrWhiteSpace(setting))
{
if ((CompressionModes & CompressionModes.TSSC) > 0)
{
// TSSC is a stateful compression algorithm which will not reliably support UDP
OnStatusMessage(MessageLevel.Warning, "Cannot use TSSC compression mode with UDP - special compression mode disabled");
// Disable TSSC compression processing
CompressionModes &= ~CompressionModes.TSSC;
}
dataChannel = new UdpClient(setting);
dataChannel.ReceiveBufferSize = ushort.MaxValue;
dataChannel.MaxConnectionAttempts = -1;
dataChannel.ConnectAsync();
}
// Assign data channel client reference and attach to needed events
DataChannel = dataChannel;
// Setup subscription packet
using (BlockAllocatedMemoryStream buffer = new BlockAllocatedMemoryStream())
{
DataPacketFlags flags = DataPacketFlags.NoFlags;
byte[] bytes;
if (remotelySynchronized)
flags |= DataPacketFlags.Synchronized;
if (compactFormat)
flags |= DataPacketFlags.Compact;
// Write data packet flags into buffer
buffer.WriteByte((byte)flags);
// Get encoded bytes of connection string
bytes = m_encoding.GetBytes(connectionString);
// Write encoded connection string length into buffer
buffer.Write(BigEndian.GetBytes(bytes.Length), 0, 4);
// Encode connection string into buffer
buffer.Write(bytes, 0, bytes.Length);
// Cache subscribed synchronization state
m_synchronizedSubscription = remotelySynchronized;
// Send subscribe server command with associated command buffer
success = SendServerCommand(ServerCommand.Subscribe, buffer.ToArray());
}
}
catch (Exception ex)
{
OnProcessException(MessageLevel.Error, new InvalidOperationException("Exception occurred while trying to make publisher subscription: " + ex.Message, ex));
}
}
else
{
OnProcessException(MessageLevel.Error, new InvalidOperationException("Cannot make publisher subscription without a connection string."));
}
// Reset decompressor on successful resubscription
if (success)
{
m_tsscResetRequested = true;
}
return success;
}
///
/// Unsubscribes from a data publisher.
///
/// true if unsubscribe transmission was successful; otherwise false.
public virtual bool Unsubscribe()
{
// Send unsubscribe server command
return SendServerCommand(ServerCommand.Unsubscribe);
}
///
/// Returns the measurements signal IDs that were authorized after the last successful subscription request.
///
[AdapterCommand("Gets authorized signal IDs from last subscription request.", "Administrator", "Editor", "Viewer")]
public virtual Guid[] GetAuthorizedSignalIDs()
{
if ((object)m_signalIndexCache != null)
return m_signalIndexCache.AuthorizedSignalIDs;
return new Guid[0];
}
///
/// Returns the measurements signal IDs that were unauthorized after the last successful subscription request.
///
[AdapterCommand("Gets unauthorized signal IDs from last subscription request.", "Administrator", "Editor", "Viewer")]
public virtual Guid[] GetUnauthorizedSignalIDs()
{
if ((object)m_signalIndexCache != null)
return m_signalIndexCache.UnauthorizedSignalIDs;
return new Guid[0];
}
///
/// Resets the counters for the lifetime statistics without interrupting the adapter's operations.
///
[AdapterCommand("Resets the counters for the lifetime statistics without interrupting the adapter's operations.", "Administrator", "Editor")]
public virtual void ResetLifetimeCounters()
{
m_lifetimeMeasurements = 0L;
m_totalBytesReceived = 0L;
m_lifetimeTotalLatency = 0L;
m_lifetimeMinimumLatency = 0L;
m_lifetimeMaximumLatency = 0L;
m_lifetimeLatencyMeasurements = 0L;
}
///
/// Initiate a meta-data refresh.
///
[AdapterCommand("Initiates a meta-data refresh.", "Administrator", "Editor")]
public virtual void RefreshMetadata()
{
SendServerCommand(ServerCommand.MetaDataRefresh, m_metadataFilters);
}
///
/// Log a data gap for data gap recovery.
///
/// The string representing the data gap.
[AdapterCommand("Logs a data gap for data gap recovery.", "Administrator", "Editor")]
public virtual void LogDataGap(string timeString)
{
DateTimeOffset end = default(DateTimeOffset);
string[] split = timeString.Split(';');
if (!m_dataGapRecoveryEnabled)
throw new InvalidOperationException("Data gap recovery is not enabled.");
if (split.Length != 2)
throw new FormatException("Invalid format for time string - ex: 2014-03-27 02:10:47.566;2014-03-27 02:10:59.733");
string startTime = split[0];
string endTime = split[1];
bool parserSuccessful =
DateTimeOffset.TryParse(startTime, CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowInnerWhite, out DateTimeOffset start) &&
DateTimeOffset.TryParse(endTime, CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowInnerWhite, out end);
if (!parserSuccessful)
throw new FormatException("Invalid format for time string - ex: 2014-03-27 02:10:47.566;2014-03-27 02:10:59.733");
m_dataGapRecoverer?.LogDataGap(start, end, true);
}
///
/// Remove a data gap from data gap recovery.
///
/// The string representing the data gap.
[AdapterCommand("Removes a data gap from data gap recovery.", "Administrator", "Editor")]
public virtual string RemoveDataGap(string timeString)
{
DateTimeOffset end = default(DateTimeOffset);
string[] split = timeString.Split(';');
if (!m_dataGapRecoveryEnabled)
throw new InvalidOperationException("Data gap recovery is not enabled.");
if (split.Length != 2)
throw new FormatException("Invalid format for time string - ex: 2014-03-27 02:10:47.566;2014-03-27 02:10:59.733");
string startTime = split[0];
string endTime = split[1];
bool parserSuccessful =
DateTimeOffset.TryParse(startTime, CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowInnerWhite, out DateTimeOffset start) &&
DateTimeOffset.TryParse(endTime, CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AllowInnerWhite, out end);
if (!parserSuccessful)
throw new FormatException("Invalid format for time string - ex: 2014-03-27 02:10:47.566;2014-03-27 02:10:59.733");
if (m_dataGapRecoverer?.RemoveDataGap(start, end) ?? false)
return "Data gap successfully removed.";
else
return "Data gap not found.";
}
///
/// Displays the contents of the outage log.
///
/// The contents of the outage log.
[AdapterCommand("Displays data gaps queued for data gap recovery.", "Administrator", "Editor", "Viewer")]
public virtual string DumpOutageLog()
{
if (m_dataGapRecoveryEnabled && (object)m_dataGapRecoverer != null)
return Environment.NewLine + m_dataGapRecoverer.DumpOutageLog();
throw new InvalidOperationException("Data gap recovery not enabled");
}
///
/// Gets the status of the temporal used by the data gap recovery module.
///
/// Status of the temporal used by the data gap recovery module.
[AdapterCommand("Gets the status of the temporal subscription used by the data gap recovery module.", "Administrator", "Editor", "Viewer")]
public virtual string GetDataGapRecoverySubscriptionStatus()
{
if (m_dataGapRecoveryEnabled && (object)m_dataGapRecoverer != null)
return m_dataGapRecoverer.TemporalSubscriptionStatus;
return "Data gap recovery not enabled";
}
///
/// Spawn meta-data synchronization.
///
/// to use for synchronization.
///
/// This method makes sure only one meta-data synchronization happens at a time.
///
public void SynchronizeMetadata(DataSet metadata)
{
try
{
m_receivedMetadata = metadata;
m_synchronizeMetadataOperation.RunOnceAsync();
}
catch (Exception ex)
{
// Process exception for logging
OnProcessException(MessageLevel.Warning, new InvalidOperationException("Failed to queue meta-data synchronization: " + ex.Message, ex));
}
}
///
/// Sends a server command to the publisher connection with associated data.
///
/// to send.
/// String based command data to send to server.
/// true if transmission was successful; otherwise false.
public virtual bool SendServerCommand(ServerCommand commandCode, string message)
{
if (!string.IsNullOrWhiteSpace(message))
{
using (BlockAllocatedMemoryStream buffer = new BlockAllocatedMemoryStream())
{
byte[] bytes = m_encoding.GetBytes(message);
buffer.Write(BigEndian.GetBytes(bytes.Length), 0, 4);
buffer.Write(bytes, 0, bytes.Length);
return SendServerCommand(commandCode, buffer.ToArray());
}
}
return SendServerCommand(commandCode);
}
///
/// Sends a server command to the publisher connection.
///
/// to send.
/// Optional command data to send.
/// true if transmission was successful; otherwise false.
public virtual bool SendServerCommand(ServerCommand commandCode, byte[] data = null)
{
if ((object)m_commandChannel != null && m_commandChannel.CurrentState == ClientState.Connected)
{
try
{
using (BlockAllocatedMemoryStream commandPacket = new BlockAllocatedMemoryStream())
{
// Write command code into command packet
commandPacket.WriteByte((byte)commandCode);
// Write command buffer into command packet
if ((object)data != null && data.Length > 0)
commandPacket.Write(data, 0, data.Length);
// Send command packet to publisher
m_commandChannel.SendAsync(commandPacket.ToArray(), 0, (int)commandPacket.Length);
m_metadataRefreshPending = commandCode == ServerCommand.MetaDataRefresh;
}
return true;
}
catch (Exception ex)
{
OnProcessException(MessageLevel.Error, new InvalidOperationException($"Exception occurred while trying to send server command \"{commandCode}\" to publisher: {ex.Message}", ex));
}
}
else
OnProcessException(MessageLevel.Error, new InvalidOperationException($"Subscriber is currently unconnected. Cannot send server command \"{commandCode}\" to publisher."));
return false;
}
///
/// Attempts to connect to this .
///
protected override void AttemptConnection()
{
if (!this.TemporalConstraintIsDefined() && !m_supportsRealTimeProcessing)
return;
long now = m_useLocalClockAsRealTime ? DateTime.UtcNow.Ticks : 0L;
List> statisticsHelpers = m_statisticsHelpers;
m_registerStatisticsOperation.RunOnceAsync();
m_expectedBufferBlockSequenceNumber = 0u;
m_commandChannelConnectionAttempts = 0;
m_dataChannelConnectionAttempts = 0;
m_authenticated = m_securityMode == SecurityMode.TLS;
m_subscribed = false;
m_keyIVs = null;
m_totalBytesReceived = 0L;
m_monitoredBytesReceived = 0L;
m_lastBytesReceived = 0;
if (!PersistConnectionForMetadata)
m_commandChannel.ConnectAsync();
else
OnConnected();
if (PersistConnectionForMetadata && CommandChannelConnected)
{
// Attempt authentication if required, remaining steps will happen on successful authentication
if (m_securityMode == SecurityMode.Gateway)
Authenticate(m_sharedSecret, m_authenticationID);
else
SubscribeToOutputMeasurements(true);
}
if (m_useLocalClockAsRealTime && (object)m_subscribedDevicesTimer == null)
{
m_subscribedDevicesTimer = Common.TimerScheduler.CreateTimer(1000);
m_subscribedDevicesTimer.Elapsed += SubscribedDevicesTimer_Elapsed;
}
if ((object)statisticsHelpers != null)
{
m_realTime = 0L;
m_lastStatisticsHelperUpdate = 0L;
foreach (DeviceStatisticsHelper statisticsHelper in statisticsHelpers)
statisticsHelper.Reset(now);
}
if (m_useLocalClockAsRealTime)
m_subscribedDevicesTimer.Start();
}
///
/// Attempts to disconnect from this .
///
protected override void AttemptDisconnection()
{
// Unregister device statistics
m_registerStatisticsOperation.RunOnceAsync();
// Stop data stream monitor
if ((object)m_dataStreamMonitor != null)
m_dataStreamMonitor.Enabled = false;
// Disconnect command channel
if (!PersistConnectionForMetadata && (object)m_commandChannel != null)
m_commandChannel.Disconnect();
if ((object)m_subscribedDevicesTimer != null)
m_subscribedDevicesTimer.Stop();
m_metadataRefreshPending = false;
}
///
/// Gets a short one-line status of this .
///
/// Maximum length of the status message.
/// Text of the status message.
public override string GetShortStatus(int maxLength)
{
if ((object)m_commandChannel != null && m_commandChannel.CurrentState == ClientState.Connected)
return $"Subscriber is connected and receiving {(m_synchronizedSubscription ? "synchronized" : "unsynchronized")} data points".CenterText(maxLength);
return "Subscriber is not connected.".CenterText(maxLength);
}
///
/// Get message from string based response.
///
/// Response buffer.
/// Start index of response message.
/// Length of response message.
/// Decoded response string.
protected string InterpretResponseMessage(byte[] buffer, int startIndex, int length)
{
return m_encoding.GetString(buffer, startIndex, length);
}
// Restarts the subscriber.
private void Restart()
{
try
{
base.Start();
}
catch (Exception ex)
{
OnProcessException(MessageLevel.Warning, ex);
}
}
private void ProcessServerResponse(byte[] buffer, int length)
{
// Currently this work is done on the async socket completion thread, make sure work to be done is timely and if the response processing
// is coming in via the command channel and needs to send a command back to the server, it should be done on a separate thread...
if (buffer != null && length > 0)
{
try
{
Dictionary> subscribedDevicesLookup;
DeviceStatisticsHelper statisticsHelper;
ServerResponse responseCode = (ServerResponse)buffer[0];
ServerCommand commandCode = (ServerCommand)buffer[1];
int responseLength = BigEndian.ToInt32(buffer, 2);
int responseIndex = DataPublisher.ClientResponseHeaderSize;
byte[][][] keyIVs;
// Disconnect any established UDP data channel upon successful unsubscribe
if (commandCode == ServerCommand.Unsubscribe && responseCode == ServerResponse.Succeeded)
DataChannel = null;
if (!IsUserCommand(commandCode))
OnReceivedServerResponse(responseCode, commandCode);
else
OnReceivedUserCommandResponse(commandCode, responseCode, buffer, responseIndex, length);
switch (responseCode)
{
case ServerResponse.Succeeded:
switch (commandCode)
{
case ServerCommand.Authenticate:
OnStatusMessage(MessageLevel.Info, $"Success code received in response to server command \"{commandCode}\": {InterpretResponseMessage(buffer, responseIndex, responseLength)}");
m_authenticated = true;
OnConnectionAuthenticated();
break;
case ServerCommand.Subscribe:
OnStatusMessage(MessageLevel.Info, $"Success code received in response to server command \"{commandCode}\": {InterpretResponseMessage(buffer, responseIndex, responseLength)}");
m_subscribed = true;
break;
case ServerCommand.Unsubscribe:
OnStatusMessage(MessageLevel.Info, $"Success code received in response to server command \"{commandCode}\": {InterpretResponseMessage(buffer, responseIndex, responseLength)}");
m_subscribed = false;
if ((object)m_dataStreamMonitor != null)
m_dataStreamMonitor.Enabled = false;
break;
case ServerCommand.RotateCipherKeys:
OnStatusMessage(MessageLevel.Info, $"Success code received in response to server command \"{commandCode}\": {InterpretResponseMessage(buffer, responseIndex, responseLength)}");
break;
case ServerCommand.MetaDataRefresh:
OnStatusMessage(MessageLevel.Info, $"Success code received in response to server command \"{commandCode}\": latest meta-data received.");
OnMetaDataReceived(DeserializeMetadata(buffer.BlockCopy(responseIndex, responseLength)));
m_metadataRefreshPending = false;
break;
}
break;
case ServerResponse.Failed:
OnStatusMessage(MessageLevel.Info, $"Failure code received in response to server command \"{commandCode}\": {InterpretResponseMessage(buffer, responseIndex, responseLength)}");
if (commandCode == ServerCommand.MetaDataRefresh)
m_metadataRefreshPending = false;
break;
case ServerResponse.DataPacket:
long now = DateTime.UtcNow.Ticks;
// Deserialize data packet
List measurements = new List();
DataPacketFlags flags;
Ticks timestamp = 0;
int count;
if (m_totalBytesReceived == 0)
{
// At the point when data is being received, data monitor should be enabled
if ((object)m_dataStreamMonitor != null && !m_dataStreamMonitor.Enabled)
m_dataStreamMonitor.Enabled = true;
// Establish run-time log for subscriber
if (m_autoConnect || m_dataGapRecoveryEnabled)
{
if ((object)m_runTimeLog == null)
{
m_runTimeLog = new RunTimeLog();
m_runTimeLog.FileName = GetLoggingPath(Name + "_RunTimeLog.txt");
m_runTimeLog.ProcessException += m_runTimeLog_ProcessException;
m_runTimeLog.Initialize();
}
else
{
// Mark the start of any data transmissions
m_runTimeLog.StartTime = DateTimeOffset.UtcNow;
m_runTimeLog.Enabled = true;
}
}
// The duration between last disconnection and start of data transmissions
// represents a gap in data - if data gap recovery is enabled, we log
// this as a gap for recovery:
if (m_dataGapRecoveryEnabled && (object)m_dataGapRecoverer != null)
m_dataGapRecoverer.LogDataGap(m_runTimeLog.StopTime, DateTimeOffset.UtcNow);
}
// Track total data packet bytes received from any channel
m_totalBytesReceived += m_lastBytesReceived;
m_monitoredBytesReceived += m_lastBytesReceived;
// Get data packet flags
flags = (DataPacketFlags)buffer[responseIndex];
responseIndex++;
bool synchronizedMeasurements = (byte)(flags & DataPacketFlags.Synchronized) > 0;
bool compactMeasurementFormat = (byte)(flags & DataPacketFlags.Compact) > 0;
bool compressedPayload = (byte)(flags & DataPacketFlags.Compressed) > 0;
int cipherIndex = (flags & DataPacketFlags.CipherIndex) > 0 ? 1 : 0;
// Decrypt data packet payload if keys are available
if ((object)m_keyIVs != null)
{
// Get a local copy of volatile keyIVs reference since this can change at any time
keyIVs = m_keyIVs;
// Decrypt payload portion of data packet
buffer = Common.SymmetricAlgorithm.Decrypt(buffer, responseIndex, responseLength - 1, keyIVs[cipherIndex][0], keyIVs[cipherIndex][1]);
responseIndex = 0;
responseLength = buffer.Length;
}
// Synchronized packets contain a frame level timestamp
if (synchronizedMeasurements)
{
timestamp = BigEndian.ToInt64(buffer, responseIndex);
responseIndex += 8;
}
// Deserialize number of measurements that follow
count = BigEndian.ToInt32(buffer, responseIndex);
responseIndex += 4;
if (compressedPayload)
{
if (CompressionModes.HasFlag(CompressionModes.TSSC))
{
try
{
// Decompress TSSC serialized measurements from payload
ParseTSSCMeasurements(buffer, responseLength, ref responseIndex, measurements);
}
catch (Exception ex)
{
OnProcessException(MessageLevel.Error, new InvalidOperationException($"Decompression failure: (Decoded {measurements.Count} of {count} measurements)" + ex.Message, ex));
}
}
else
{
if ((object)m_signalIndexCache == null && m_lastMissingCacheWarning + MissingCacheWarningInterval < now)
{
// Warning message for missing signal index cache
if (m_lastMissingCacheWarning != 0L)
OnStatusMessage(MessageLevel.Error, "Signal index cache has not arrived. No compact measurements can be parsed.");
m_lastMissingCacheWarning = now;
}
else
{
try
{
// Decompress compact measurements from payload
measurements.AddRange(buffer.DecompressPayload(m_signalIndexCache, responseIndex, responseLength - responseIndex + DataPublisher.ClientResponseHeaderSize, count, m_includeTime, flags));
}
catch (Exception ex)
{
OnProcessException(MessageLevel.Error, new InvalidOperationException($"Decompression failure: (Decoded {measurements.Count} of {count} measurements)" + ex.Message, ex));
}
}
}
}
else
{
// Deserialize measurements
for (int i = 0; i < count; i++)
{
if (!compactMeasurementFormat)
{
// Deserialize full measurement format
SerializableMeasurement measurement = new SerializableMeasurement(m_encoding);
responseIndex += measurement.ParseBinaryImage(buffer, responseIndex, responseLength - responseIndex);
measurements.Add(measurement);
}
else if ((object)m_signalIndexCache != null)
{
// Deserialize compact measurement format
CompactMeasurement measurement = new CompactMeasurement(m_signalIndexCache, m_includeTime, m_baseTimeOffsets, m_timeIndex, m_useMillisecondResolution);
responseIndex += measurement.ParseBinaryImage(buffer, responseIndex, responseLength - responseIndex);
// Apply timestamp from frame if not included in transmission
if (!measurement.IncludeTime)
measurement.Timestamp = timestamp;
measurements.Add(measurement);
}
else if (m_lastMissingCacheWarning + MissingCacheWarningInterval < now)
{
// Warning message for missing signal index cache
if (m_lastMissingCacheWarning != 0L)
OnStatusMessage(MessageLevel.Error, "Signal index cache has not arrived. No compact measurements can be parsed.");
m_lastMissingCacheWarning = now;
}
}
}
// Calculate statistics
subscribedDevicesLookup = m_subscribedDevicesLookup;
statisticsHelper = null;
if ((object)subscribedDevicesLookup != null)
{
IEnumerable, IMeasurement>> deviceGroups = measurements
.Where(measurement => subscribedDevicesLookup.TryGetValue(measurement.ID, out statisticsHelper))
.Select(measurement => Tuple.Create(statisticsHelper, measurement))
.ToList()
.GroupBy(tuple => tuple.Item1, tuple => tuple.Item2);
foreach (IGrouping, IMeasurement> deviceGroup in deviceGroups)
{
statisticsHelper = deviceGroup.Key;
foreach (IGrouping frame in deviceGroup.GroupBy(measurement => measurement.Timestamp))
{
// Determine the number of measurements received with valid values
const MeasurementStateFlags ErrorFlags = MeasurementStateFlags.BadData | MeasurementStateFlags.BadTime | MeasurementStateFlags.SystemError;
bool hasError(MeasurementStateFlags stateFlags) => (stateFlags & ErrorFlags) != MeasurementStateFlags.Normal;
int measurementsReceived = frame.Count(measurement => !double.IsNaN(measurement.Value));
int measurementsWithError = frame.Count(measurement => !double.IsNaN(measurement.Value) && hasError(measurement.StateFlags));
IMeasurement statusFlags = null;
IMeasurement frequency = null;
IMeasurement deltaFrequency = null;
// Attempt to update real-time
if (!m_useLocalClockAsRealTime && frame.Key > m_realTime)
m_realTime = frame.Key;
// Search the frame for status flags, frequency, and delta frequency
foreach (IMeasurement measurement in frame)
{
if (measurement.ID == statisticsHelper.Device.StatusFlagsID)
statusFlags = measurement;
else if (measurement.ID == statisticsHelper.Device.FrequencyID)
frequency = measurement;
else if (measurement.ID == statisticsHelper.Device.DeltaFrequencyID)
deltaFrequency = measurement;
}
// If we are receiving status flags for this device,
// count the data quality, time quality, and device errors
if ((object)statusFlags != null)
{
uint commonStatusFlags = (uint)statusFlags.Value;
if ((commonStatusFlags & (uint)Bits.Bit19) > 0)
statisticsHelper.Device.DataQualityErrors++;
if ((commonStatusFlags & (uint)Bits.Bit18) > 0)
statisticsHelper.Device.TimeQualityErrors++;
if ((commonStatusFlags & (uint)Bits.Bit16) > 0)
statisticsHelper.Device.DeviceErrors++;
measurementsReceived--;
if (hasError(statusFlags.StateFlags))
measurementsWithError--;
}
// Zero is not a valid value for frequency.
// If frequency is zero, invalidate both frequency and delta frequency
if ((object)frequency != null)
{
if (!this.TemporalConstraintIsDefined())
statisticsHelper.MarkDeviceTimestamp(frequency.Timestamp);
if (frequency.Value == 0.0D)
{
if ((object)deltaFrequency != null && !double.IsNaN(deltaFrequency.Value))
measurementsReceived -= 2;
else
measurementsReceived--;
if (hasError(frequency.StateFlags))
{
if ((object)deltaFrequency != null && !double.IsNaN(deltaFrequency.Value))
measurementsWithError -= 2;
else
measurementsWithError--;
}
}
}
// Track the number of measurements received
statisticsHelper.AddToMeasurementsReceived(measurementsReceived);
statisticsHelper.AddToMeasurementsWithError(measurementsWithError);
}
}
}
// Provide new measurements to local concentrator, if defined, otherwise directly expose them to the consumer
if ((object)m_localConcentrator != null)
m_localConcentrator.SortMeasurements(measurements);
else
OnNewMeasurements(measurements);
// Gather statistics on received data
DateTime timeReceived = RealTime;
if (!m_useLocalClockAsRealTime && timeReceived.Ticks - m_lastStatisticsHelperUpdate > Ticks.PerSecond)
{
UpdateStatisticsHelpers();
m_lastStatisticsHelperUpdate = m_realTime;
}
m_lifetimeMeasurements += measurements.Count;
UpdateMeasurementsPerSecond(timeReceived, measurements.Count);
for (int x = 0; x < measurements.Count; x++)
{
long latency = timeReceived.Ticks - (long)measurements[x].Timestamp;
// Throw out latencies that exceed one hour as invalid
if (Math.Abs(latency) > Time.SecondsPerHour * Ticks.PerSecond)
continue;
if (m_lifetimeMinimumLatency > latency || m_lifetimeMinimumLatency == 0)
m_lifetimeMinimumLatency = latency;
if (m_lifetimeMaximumLatency < latency || m_lifetimeMaximumLatency == 0)
m_lifetimeMaximumLatency = latency;
m_lifetimeTotalLatency += latency;
m_lifetimeLatencyMeasurements++;
}
break;
case ServerResponse.BufferBlock:
// Buffer block received - wrap as a buffer block measurement and expose back to consumer
uint sequenceNumber = BigEndian.ToUInt32(buffer, responseIndex);
int cacheIndex = (int)(sequenceNumber - m_expectedBufferBlockSequenceNumber);
BufferBlockMeasurement bufferBlockMeasurement;
ushort signalIndex;
// Check if this buffer block has already been processed (e.g., mistaken retransmission due to timeout)
if (cacheIndex >= 0 && (cacheIndex >= m_bufferBlockCache.Count || (object)m_bufferBlockCache[cacheIndex] == null))
{
// Send confirmation that buffer block is received
SendServerCommand(ServerCommand.ConfirmBufferBlock, buffer.BlockCopy(responseIndex, 4));
// Get measurement key from signal index cache
signalIndex = BigEndian.ToUInt16(buffer, responseIndex + 4);
if (!m_signalIndexCache.Reference.TryGetValue(signalIndex, out MeasurementKey measurementKey))
throw new InvalidOperationException("Failed to find associated signal identification for runtime ID " + signalIndex);
// Skip the sequence number and signal index when creating the buffer block measurement
bufferBlockMeasurement = new BufferBlockMeasurement(buffer, responseIndex + 6, responseLength - 6)
{
Metadata = measurementKey.Metadata
};
// Determine if this is the next buffer block in the sequence
if (sequenceNumber == m_expectedBufferBlockSequenceNumber)
{
List bufferBlockMeasurements = new List();
int i;
// Add the buffer block measurement to the list of measurements to be published
bufferBlockMeasurements.Add(bufferBlockMeasurement);
m_expectedBufferBlockSequenceNumber++;
// Add cached buffer block measurements to the list of measurements to be published
for (i = 1; i < m_bufferBlockCache.Count; i++)
{
if ((object)m_bufferBlockCache[i] == null)
break;
bufferBlockMeasurements.Add(m_bufferBlockCache[i]);
m_expectedBufferBlockSequenceNumber++;
}
// Remove published measurements from the buffer block queue
if (m_bufferBlockCache.Count > 0)
m_bufferBlockCache.RemoveRange(0, i);
// Publish measurements
OnNewMeasurements(bufferBlockMeasurements);
}
else
{
// Ensure that the list has at least as many
// elements as it needs to cache this measurement
for (int i = m_bufferBlockCache.Count; i <= cacheIndex; i++)
m_bufferBlockCache.Add(null);
// Insert this buffer block into the proper location in the list
m_bufferBlockCache[cacheIndex] = bufferBlockMeasurement;
}
}
m_lifetimeMeasurements += 1;
UpdateMeasurementsPerSecond(DateTime.UtcNow, 1);
break;
case ServerResponse.DataStartTime:
// Raise data start time event
OnDataStartTime(BigEndian.ToInt64(buffer, responseIndex));
break;
case ServerResponse.ProcessingComplete:
// Raise input processing completed event
OnProcessingComplete(InterpretResponseMessage(buffer, responseIndex, responseLength));
break;
case ServerResponse.UpdateSignalIndexCache:
// Deserialize new signal index cache
m_remoteSignalIndexCache = DeserializeSignalIndexCache(buffer.BlockCopy(responseIndex, responseLength));
m_signalIndexCache = new SignalIndexCache(DataSource, m_remoteSignalIndexCache);
FixExpectedMeasurementCounts();
break;
case ServerResponse.UpdateBaseTimes:
// Get active time index
m_timeIndex = BigEndian.ToInt32(buffer, responseIndex);
responseIndex += 4;
// Deserialize new base time offsets
m_baseTimeOffsets = new[] { BigEndian.ToInt64(buffer, responseIndex), BigEndian.ToInt64(buffer, responseIndex + 8) };
break;
case ServerResponse.UpdateCipherKeys:
// Move past active cipher index (not currently used anywhere else)
responseIndex++;
// Extract remaining response
byte[] bytes = buffer.BlockCopy(responseIndex, responseLength - 1);
// Decrypt response payload if subscription is authenticated
if (m_authenticated)
bytes = bytes.Decrypt(m_sharedSecret, CipherStrength.Aes256);
// Deserialize new cipher keys
keyIVs = new byte[2][][];
keyIVs[EvenKey] = new byte[2][];
keyIVs[OddKey] = new byte[2][];
int index = 0;
int bufferLen;
// Read even key size
bufferLen = BigEndian.ToInt32(bytes, index);
index += 4;
// Read even key
keyIVs[EvenKey][KeyIndex] = new byte[bufferLen];
Buffer.BlockCopy(bytes, index, keyIVs[EvenKey][KeyIndex], 0, bufferLen);
index += bufferLen;
// Read even initialization vector size
bufferLen = BigEndian.ToInt32(bytes, index);
index += 4;
// Read even initialization vector
keyIVs[EvenKey][IVIndex] = new byte[bufferLen];
Buffer.BlockCopy(bytes, index, keyIVs[EvenKey][IVIndex], 0, bufferLen);
index += bufferLen;
// Read odd key size
bufferLen = BigEndian.ToInt32(bytes, index);
index += 4;
// Read odd key
keyIVs[OddKey][KeyIndex] = new byte[bufferLen];
Buffer.BlockCopy(bytes, index, keyIVs[OddKey][KeyIndex], 0, bufferLen);
index += bufferLen;
// Read odd initialization vector size
bufferLen = BigEndian.ToInt32(bytes, index);
index += 4;
// Read odd initialization vector
keyIVs[OddKey][IVIndex] = new byte[bufferLen];
Buffer.BlockCopy(bytes, index, keyIVs[OddKey][IVIndex], 0, bufferLen);
//index += bufferLen;
// Exchange keys
m_keyIVs = keyIVs;
OnStatusMessage(MessageLevel.Info, "Successfully established new cipher keys for data packet transmissions.");
break;
case ServerResponse.Notify:
// Skip the 4-byte hash
string message = m_encoding.GetString(buffer, responseIndex + 4, responseLength - 4);
// Display notification
OnStatusMessage(MessageLevel.Info, $"NOTIFICATION: {message}");
OnNotificationReceived(message);
// Send confirmation of receipt of the notification
SendServerCommand(ServerCommand.ConfirmNotification, buffer.BlockCopy(responseIndex, 4));
break;
case ServerResponse.ConfigurationChanged:
OnStatusMessage(MessageLevel.Info, "Received notification from publisher that configuration has changed.");
OnServerConfigurationChanged();
// Initiate meta-data refresh when publisher configuration has changed - we only do this
// for automatic connections since API style connections have to manually initiate a
// meta-data refresh. API style connection should attach to server configuration changed
// event and request meta-data refresh to complete automated cycle.
if (m_autoConnect && m_autoSynchronizeMetadata)
SendServerCommand(ServerCommand.MetaDataRefresh, m_metadataFilters);
break;
}
}
catch (Exception ex)
{
OnProcessException(MessageLevel.Error, new InvalidOperationException("Failed to process publisher response packet due to exception: " + ex.Message, ex));
}
}
}
private void ParseTSSCMeasurements(byte[] buffer, int responseLength, ref int responseIndex, List measurements)
{
// Use TSSC compression to decompress measurements
if ((object)m_tsscDecoder == null)
{
m_tsscDecoder = new TsscDecoder();
m_tsscSequenceNumber = 0;
}
if (buffer[responseIndex] != 85)
throw new Exception($"TSSC version not recognized: {buffer[responseIndex]}");
responseIndex++;
int sequenceNumber = BigEndian.ToUInt16(buffer, responseIndex);
responseIndex += 2;
if (sequenceNumber == 0)
{
OnStatusMessage(MessageLevel.Info, $"TSSC algorithm reset before sequence number: {m_tsscSequenceNumber}", "TSSC");
m_tsscDecoder = new TsscDecoder();
m_tsscSequenceNumber = 0;
m_tsscResetRequested = false;
}
if (m_tsscSequenceNumber != sequenceNumber)
{
if (!m_tsscResetRequested)
throw new Exception($"TSSC is out of sequence. Expecting: {m_tsscSequenceNumber}, Received: {sequenceNumber}");
// Ignore packets until the reset has occurred.
LogEventPublisher publisher = Log.RegisterEvent(MessageLevel.Debug, "TSSC", 0, MessageRate.EveryFewSeconds(1), 5);
publisher.ShouldRaiseMessageSupressionNotifications = false;
publisher.Publish($"TSSC is out of sequence. Expecting: {m_tsscSequenceNumber}, Received: {sequenceNumber}");
return;
}
m_tsscDecoder.SetBuffer(buffer, responseIndex, responseLength + DataPublisher.ClientResponseHeaderSize - responseIndex);
Measurement measurement;
MeasurementKey key = null;
while (m_tsscDecoder.TryGetMeasurement(out ushort id, out long time, out uint quality, out float value))
{
if (m_signalIndexCache?.Reference.TryGetValue(id, out key) ?? false)
{
measurement = new Measurement();
measurement.Metadata = key?.Metadata;
measurement.Timestamp = time;
measurement.StateFlags = (MeasurementStateFlags)quality;
measurement.Value = value;
measurements.Add(measurement);
}
}
m_tsscSequenceNumber++;
// Do not increment to 0 on roll-over
if (m_tsscSequenceNumber == 0)
m_tsscSequenceNumber = 1;
}
private bool IsUserCommand(ServerCommand command)
{
ServerCommand[] userCommands =
{
ServerCommand.UserCommand00,
ServerCommand.UserCommand01,
ServerCommand.UserCommand02,
ServerCommand.UserCommand03,
ServerCommand.UserCommand04,
ServerCommand.UserCommand05,
ServerCommand.UserCommand06,
ServerCommand.UserCommand07,
ServerCommand.UserCommand08,
ServerCommand.UserCommand09,
ServerCommand.UserCommand10,
ServerCommand.UserCommand11,
ServerCommand.UserCommand12,
ServerCommand.UserCommand13,
ServerCommand.UserCommand14,
ServerCommand.UserCommand15
};
return userCommands.Contains(command);
}
// Handles auto-connection subscription initialization
private void StartSubscription()
{
SubscribeToOutputMeasurements(!m_autoSynchronizeMetadata);
// Initiate meta-data refresh
if (m_autoSynchronizeMetadata && !this.TemporalConstraintIsDefined())
SendServerCommand(ServerCommand.MetaDataRefresh, m_metadataFilters);
}
private void SubscribeToOutputMeasurements(bool metaDataRefreshCompleted)
{
StringBuilder filterExpression = new StringBuilder();
string dataChannel = null;
string startTimeConstraint = null;
string stopTimeConstraint = null;
int processingInterval = -1;
// If TCP command channel is defined separately, then base connection string defines data channel
if (Settings.ContainsKey("commandChannel"))
dataChannel = ConnectionString;
if (this.TemporalConstraintIsDefined())
{
startTimeConstraint = StartTimeConstraint.ToString("yyyy-MM-dd HH:mm:ss.fffffff");
stopTimeConstraint = StopTimeConstraint.ToString("yyyy-MM-dd HH:mm:ss.fffffff");
processingInterval = ProcessingInterval;
}
MeasurementKey[] outputMeasurementKeys = AutoStart
? this.OutputMeasurementKeys()
: RequestedOutputMeasurementKeys;
if ((object)outputMeasurementKeys != null && outputMeasurementKeys.Length > 0)
{
foreach (MeasurementKey measurementKey in outputMeasurementKeys)
{
if (filterExpression.Length > 0)
filterExpression.Append(';');
// Subscribe by associated Guid...
filterExpression.Append(measurementKey.SignalID);
}
// Start unsynchronized subscription
#pragma warning disable 0618
UnsynchronizedSubscribe(true, false, filterExpression.ToString(), dataChannel, startTime: startTimeConstraint, stopTime: stopTimeConstraint, processingInterval: processingInterval);
}
else
{
Unsubscribe();
if (AutoStart && metaDataRefreshCompleted)
OnStatusMessage(MessageLevel.Error, "No measurements are currently defined for subscription.");
}
}
///
/// Handles meta-data synchronization to local system.
///
///
/// This function should only be initiated from call to to make
/// sure only one meta-data synchronization happens at once. Users can override this method to customize
/// process of meta-data synchronization.
///
protected virtual void SynchronizeMetadata()
{
bool dataMonitoringEnabled = false;
// TODO: This function is complex and very closely tied to the current time-series data schema - perhaps it should be moved outside this class and referenced
// TODO: as a delegate that can be assigned and called to allow other schemas as well. DataPublisher is already very flexible in what data it can deliver.
try
{
DataSet metadata = m_receivedMetadata;
// Only perform database synchronization if meta-data has changed since last update
if (!SynchronizedMetadataChanged(metadata))
return;
if ((object)metadata == null)
{
OnStatusMessage(MessageLevel.Error, "Meta-data synchronization was not performed, deserialized dataset was empty.");
return;
}
// Reset data stream monitor while meta-data synchronization is in progress
if ((object)m_dataStreamMonitor != null && m_dataStreamMonitor.Enabled)
{
m_dataStreamMonitor.Enabled = false;
dataMonitoringEnabled = true;
}
// Track total meta-data synchronization process time
Ticks startTime = DateTime.UtcNow.Ticks;
DateTime latestUpdateTime = DateTime.MinValue;
// Open the configuration database using settings found in the config file
using (AdoDataConnection database = new AdoDataConnection("systemSettings"))
using (IDbCommand command = database.Connection.CreateCommand())
{
IDbTransaction transaction = null;
if (m_useTransactionForMetadata)
transaction = database.Connection.BeginTransaction(database.DefaultIsolationLevel);
try
{
if ((object)transaction != null)
command.Transaction = transaction;
// Query the actual record ID based on the known run-time ID for this subscriber device
object sourceID = command.ExecuteScalar($"SELECT SourceID FROM Runtime WHERE ID = {ID} AND SourceTable='Device'", m_metadataSynchronizationTimeout);
if (sourceID == null || sourceID == DBNull.Value)
return;
int parentID = Convert.ToInt32(sourceID);
// Validate that the subscriber device is marked as a concentrator (we are about to associate children devices with it)
if (!command.ExecuteScalar($"SELECT IsConcentrator FROM Device WHERE ID = {parentID}", m_metadataSynchronizationTimeout).ToString().ParseBoolean())
command.ExecuteNonQuery($"UPDATE Device SET IsConcentrator = 1 WHERE ID = {parentID}", m_metadataSynchronizationTimeout);
// Get any historian associated with the subscriber device
object historianID = command.ExecuteScalar($"SELECT HistorianID FROM Device WHERE ID = {parentID}", m_metadataSynchronizationTimeout);
// Determine the active node ID - we cache this since this value won't change for the lifetime of this class
if (m_nodeID == Guid.Empty)
m_nodeID = Guid.Parse(command.ExecuteScalar($"SELECT NodeID FROM IaonInputAdapter WHERE ID = {(int)ID}", m_metadataSynchronizationTimeout).ToString());
// Determine the protocol record auto-inc ID value for the gateway transport protocol (GEP) - this value is also cached since it shouldn't change for the lifetime of this class
if (m_gatewayProtocolID == 0)
m_gatewayProtocolID = int.Parse(command.ExecuteScalar("SELECT ID FROM Protocol WHERE Acronym='GatewayTransport'", m_metadataSynchronizationTimeout).ToString());
// Ascertain total number of actions required for all meta-data synchronization so some level feed back can be provided on progress
InitSyncProgress(metadata.Tables.Cast().Select(dataTable => (long)dataTable.Rows.Count).Sum() + 3);
// Prefix all children devices with the name of the parent since the same device names could appear in different connections (helps keep device names unique)
string sourcePrefix = m_useSourcePrefixNames ? Name + "!" : "";
Dictionary deviceIDs = new Dictionary(StringComparer.OrdinalIgnoreCase);
DateTime updateTime;
string deviceAcronym, signalTypeAcronym;
decimal longitude, latitude;
decimal? location;
object originalSource;
int deviceID;
// Check to see if data for the "DeviceDetail" table was included in the meta-data
if (metadata.Tables.Contains("DeviceDetail"))
{
DataTable deviceDetail = metadata.Tables["DeviceDetail"];
DataRow[] deviceRows;
// Define SQL statement to query if this device is already defined (this should always be based on the unique guid-based device ID)
string deviceExistsSql = database.ParameterizedQueryString("SELECT COUNT(*) FROM Device WHERE UniqueID = {0}", "uniqueID");
// Define SQL statement to insert new device record
string insertDeviceSql = database.ParameterizedQueryString("INSERT INTO Device(NodeID, ParentID, HistorianID, Acronym, Name, ProtocolID, FramesPerSecond, OriginalSource, AccessID, Longitude, Latitude, ContactList, IsConcentrator, Enabled) " +
"VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, 0, 1)", "nodeID", "parentID", "historianID", "acronym", "name", "protocolID", "framesPerSecond", "originalSource", "accessID", "longitude", "latitude", "contactList");
// Define SQL statement to update device's guid-based unique ID after insert
string updateDeviceUniqueIDSql = database.ParameterizedQueryString("UPDATE Device SET UniqueID = {0} WHERE Acronym = {1}", "uniqueID", "acronym");
// Define SQL statement to query if a device can be safely updated
string deviceIsUpdatableSql = database.ParameterizedQueryString("SELECT COUNT(*) FROM Device WHERE UniqueID = {0} AND (ParentID <> {1} OR ParentID IS NULL)", "uniqueID", "parentID");
// Define SQL statement to update existing device record
string updateDeviceSql = database.ParameterizedQueryString("UPDATE Device SET Acronym = {0}, Name = {1}, OriginalSource = {2}, ProtocolID = {3}, FramesPerSecond = {4}, HistorianID = {5}, AccessID = {6}, Longitude = {7}, Latitude = {8}, ContactList = {9} WHERE UniqueID = {10}",
"acronym", "name", "originalSource", "protocolID", "framesPerSecond", "historianID", "accessID", "longitude", "latitude", "contactList", "uniqueID");
// Define SQL statement to retrieve device's auto-inc ID based on its unique guid-based ID
string queryDeviceIDSql = database.ParameterizedQueryString("SELECT ID FROM Device WHERE UniqueID = {0}", "uniqueID");
// Define SQL statement to retrieve all unique device ID's for the current parent to check for mismatches
string queryUniqueDeviceIDsSql = database.ParameterizedQueryString("SELECT UniqueID FROM Device WHERE ParentID = {0}", "parentID");
// Define SQL statement to remove device records that no longer exist in the meta-data
string deleteDeviceSql = database.ParameterizedQueryString("DELETE FROM Device WHERE UniqueID = {0}", "uniqueID");
// Determine which device rows should be synchronized based on operational mode flags
if (ReceiveInternalMetadata && ReceiveExternalMetadata || MutualSubscription)
deviceRows = deviceDetail.Select();
else if (ReceiveInternalMetadata)
deviceRows = deviceDetail.Select("OriginalSource IS NULL");
else if (ReceiveExternalMetadata)
deviceRows = deviceDetail.Select("OriginalSource IS NOT NULL");
else
deviceRows = new DataRow[0];
// Check existence of optional meta-data fields
DataColumnCollection deviceDetailColumns = deviceDetail.Columns;
bool accessIDFieldExists = deviceDetailColumns.Contains("AccessID");
bool longitudeFieldExists = deviceDetailColumns.Contains("Longitude");
bool latitudeFieldExists = deviceDetailColumns.Contains("Latitude");
bool companyAcronymFieldExists = deviceDetailColumns.Contains("CompanyAcronym");
bool protocolNameFieldExists = deviceDetailColumns.Contains("ProtocolName");
bool vendorAcronymFieldExists = deviceDetailColumns.Contains("VendorAcronym");
bool vendorDeviceNameFieldExists = deviceDetailColumns.Contains("VendorDeviceName");
bool interconnectionNameFieldExists = deviceDetailColumns.Contains("InterconnectionName");
bool updatedOnFieldExists = deviceDetailColumns.Contains("UpdatedOn");
// Older versions of GEP did not include the AccessID field, so this is treated as optional
int accessID = 0;
List uniqueIDs = deviceRows
.Select(deviceRow => Guid.Parse(deviceRow.Field