//******************************************************************************************************
// MultiProtocolFrameParser.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:
// ----------------------------------------------------------------------------------------------------
// 03/16/2006 - J. Ritchie Carroll
// Initial version of source generated.
// 06/26/2006 - Pinal C. Patel
// Changed out the socket code with TcpClient and UdpClient components from GSF.Communication.
// 01/31/2007 - J. Ritchie Carroll
// Added TCP "server" support to allow listening connections from devices that act as data
// clients, e.g., F-NET devices.
// 05/23/2007 - Pinal C. Patel
// Added member variable 'm_clientConnectionAttempts' to track the number of attempts made for
// connecting to the server since this information is no longer provided by the event raised by
// any of the Communication Client components.
// 07/05/2007 - J. Ritchie Carroll
// Wrapped all event raising for frame parsing in Try/Catch so that any exceptions thrown in
// consumer event handlers won't have a negative effect on continuous data parsing - exceptions
// in consumer event handlers are duly noted and raised through the ParsingException event.
// 09/28/2007 - J. Ritchie Carroll
// Implemented new disconnect overload on communications client that allows timeout on socket
// close to fix an issue related non-responsive threads that "lock-up" after sending connection
// commands that attempt to close the socket for remotely connected devices.
// 12/14/2007 - J. Ritchie Carroll
// Implemented simulated timestamp injection for published frames to allow for real-time
// data simulations from archived sample data.
// 10/28/2008 - J. Ritchie Carroll
// Added support for SEL's UDP_T and UDP_U protocol implementations (UDP_S was already supported),
// implementation handled by allowing definition of a "CommandChannel" in the connection string.
// 04/27/2009 - J. Ritchie Carroll
// Added support for SEL Fast Message protocol.
// 02/12/2010 - Pinal C. Patel
// Modified to start the IFrameParser object in InitializeFrameParser() instead of Start().
// 03/20/2010 - J. Ritchie Carroll
// Added property "SkipDisableRealTimeData" to allow consumer to bypass sending the command to
// turn off the real-time data stream when automatically starting the data parsing sequence. This
// is useful when using UDP multicast that may have many listeners, in these cases you don't want
// to disable the stream on startup or shutdown since other applications may be subscribed to the
// real-time stream.
// 03/21/2010 - J. Ritchie Carroll
// Added parsing exception threshold settings and consumer event to handle situation.
// 06/13/2010 - J. Ritchie Carroll
// Added several more run-time statistics to the frame parser (e.g., missing frames, CRC errors).
// 08/10/2010 - J. Ritchie Carroll
// Added code to handle high-resolution input timing to support accurate input simulations.
// 05/06/2010- Jian (Ryan) Zuo
// Updated to exclude non-data frames from frame counts and injected waiting periods.
// 05/19/2011 - Ritchie
// Added DST file support.
// 12/17/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Ports;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using GSF.Communication;
using GSF.Diagnostics;
using GSF.IO;
using GSF.Parsing;
using GSF.PhasorProtocols.IEEEC37_118;
using GSF.PhasorProtocols.Macrodyne;
using GSF.PhasorProtocols.SelFastMessage;
using GSF.Threading;
using GSF.TimeSeries;
using GSF.Units;
using GSF.Units.EE;
using TcpClient = GSF.Communication.TcpClient;
using UdpClient = GSF.Communication.UdpClient;
// Ignore warnings about unused events that are required by IClient
#pragma warning disable 67
namespace GSF.PhasorProtocols
{
#region [ Enumerations ]
///
/// Phasor data protocols enumeration.
///
[Serializable]
public enum PhasorProtocol
{
///
/// IEEE C37.118.2-2011 protocol.
///
IEEEC37_118V2,
///
/// IEEE C37.118-2005 protocol.
///
IEEEC37_118V1,
///
/// IEEE C37.118, draft 6 protocol.
///
IEEEC37_118D6,
///
/// IEEE 1344-1995 protocol.
///
IEEE1344,
///
/// BPA PDCstream protocol.
///
BPAPDCstream,
///
/// UTK F-NET protocol.
///
FNET,
///
/// SEL Fast Message protocol.
///
SelFastMessage,
///
/// Macrodyne protocol.
///
Macrodyne,
///
/// IEC 61850-90-5 protocol.
///
IEC61850_90_5
}
#endregion
///
/// Protocol independent frame parser.
///
///
/// This class takes all protocol frame parsing implementations and reduces them to a single simple-to-use class exposing all
/// data through abstract interfaces (e.g., IConfigurationFrame, IDataFrame, etc.) - this way new protocol implementations can
/// be added without adversely affecting consuming code. Additionally, this class implements a variety of transport options
/// (e.g., TCP, UDP, Serial, etc.) and hides the complexities of this connectivity and internally pushes all data received from
/// the selected transport protocol to the selected phasor parsing protocol.
///
public sealed class MultiProtocolFrameParser : IFrameParser
{
#region [ Members ]
// Nested Types
///
/// Creates an instance of a TCP server that can be shared for multiple devices (e.g., an F-NET server)
///
private sealed class SharedTcpServerReference : IServer
{
#region [ Members ]
// Events
///
/// Occurs when the server is started.
///
public event EventHandler ServerStarted;
///
/// Occurs when the server is stopped.
///
public event EventHandler ServerStopped;
///
/// Occurs when a client connects to the server.
///
///
/// is the ID of the client that connected to the server.
///
public event EventHandler> ClientConnected;
///
/// Occurs when a client disconnects from the server.
///
///
/// is the ID of the client that disconnected from the server.
///
public event EventHandler> ClientDisconnected;
///
/// Occurs when an exception is encountered while a client is connecting.
///
///
/// is the encountered when connecting to the client.
///
public event EventHandler> ClientConnectingException;
///
/// Occurs when data is being sent to a client.
///
///
/// is the ID of the client to which the data is being sent.
///
public event EventHandler> SendClientDataStart;
///
/// Occurs when data has been sent to a client.
///
///
/// is the ID of the client to which the data has been sent.
///
public event EventHandler> SendClientDataComplete;
///
/// Occurs when an is encountered when sending data to a client.
///
///
/// is the ID of the client to which the data was being sent.
/// is the encountered when sending data to a client.
///
public event EventHandler> SendClientDataException;
///
/// Occurs when unprocessed data has been received from a client.
///
///
///
/// This event can be used to receive a notification that client data has arrived. The method can then be used
/// to copy data to an existing buffer. In many cases it will be optimal to use an existing buffer instead of subscribing to the
/// event, however, the data that is available after calling the method
/// will be the original unprocessed data received by the client, i.e., not optionally decrypted or decompressed data.
///
///
/// is the ID of the client from which data is received.
/// is the number of bytes received in the buffer from the client.
///
///
public event EventHandler> ReceiveClientData;
///
/// Occurs when data received from a client has been processed and is ready for consumption.
///
///
/// is the ID of the client from which data is received.
/// is a new buffer containing post-processed data received from the client starting at index zero.
/// is the number of post-processed bytes received in the buffer from the client.
///
public event EventHandler> ReceiveClientDataComplete;
///
/// Occurs when an is encountered when receiving data from a client.
///
///
/// is the ID of the client from which the data was being received.
/// is the encountered when receiving data from a client.
///
public event EventHandler> ReceiveClientDataException;
///
/// Occurs when an is encountered in a user-defined function via an event dispatch.
///
///
/// is the thrown by the user-defined function.
///
public event EventHandler> UnhandledUserException;
///
/// Raised after the source object has been properly disposed.
///
public event EventHandler Disposed;
// Fields
private TcpServer m_tcpServer;
private IPAddress m_remoteAddress;
private Guid m_clientID;
#endregion
#region [ Properties ]
///
/// Gets the name of the object providing status information.
///
public string Name => m_tcpServer?.Name;
///
/// Gets the current status details about object providing status information.
///
public string Status => m_tcpServer?.Status ?? string.Empty;
///
/// Gets or sets the data required by the server to initialize.
///
public string ConfigurationString { get; set; }
///
/// Gets or sets the maximum number of clients that can connect to the server.
///
public int MaxClientConnections
{
get => m_tcpServer?.MaxClientConnections ?? ServerBase.DefaultMaxClientConnections;
set { } // Ignore this setting since we need to share
}
///
/// Gets or sets the size of the buffer used by the client for receiving data from the server.
///
public int SendBufferSize
{
get => m_tcpServer?.SendBufferSize ?? ServerBase.DefaultSendBufferSize;
set { } // Ignore this setting since we need to share
}
///
/// Gets or sets the size of the buffer used by the server for receiving data from the clients.
///
public int ReceiveBufferSize { get; set; }
///
/// Gets or sets the to be used for the text sent to the connected clients.
///
public Encoding TextEncoding
{
get => m_tcpServer?.TextEncoding;
set { } // Ignore this setting since we need to share
}
///
/// Gets the current .
///
public ServerState CurrentState => m_tcpServer?.CurrentState ?? ServerState.NotRunning;
///
/// Gets the used by the server for the transportation of data with the clients.
///
public TransportProtocol TransportProtocol => TransportProtocol.Tcp;
///
/// Gets the server's ID.
///
public Guid ServerID => m_tcpServer?.ServerID ?? Guid.Empty;
///
/// Gets the IDs of clients connected to the server.
///
public Guid[] ClientIDs => m_tcpServer?.ClientIDs ?? Array.Empty();
///
/// Gets the for which the server has been running.
///
public Time RunTime => m_tcpServer?.RunTime ?? 0.0D;
///
/// Gets or sets a boolean value that indicates whether the object is enabled.
///
public bool Enabled
{
get => m_tcpServer?.Enabled ?? false;
// ReSharper disable once ValueParameterNotUsed
set
{
// value parameter not used here intentionally
if (m_tcpServer is not null)
m_tcpServer.Enabled = true;
}
}
///
/// Gets a flag that indicates whether the object has been disposed.
///
public bool IsDisposed { get; private set; }
#endregion
#region [ Methods ]
///
/// Initializes the state of the object.
///
public void Initialize()
{
Dictionary settings = ConfigurationString.ParseKeyValuePairs();
if (!settings.TryGetValue("receiveFrom", out string receiveFromSetting))
return;
IPStack ipStack = Transport.GetInterfaceIPStack(settings);
m_remoteAddress = Transport.CreateEndPoint(receiveFromSetting, 0, ipStack).Address;
}
///
/// Starts the server.
///
public void Start() => GetSharedServer();
///
/// Stops the server.
///
public void Stop() => ReturnSharedServer();
///
/// Disconnects a connected client.
///
/// ID of the client to be disconnected.
public void DisconnectOne(Guid clientID)
{
if (clientID == m_clientID)
m_tcpServer?.DisconnectOne(clientID);
}
///
/// Disconnects all of the connected clients.
///
public void DisconnectAll() => m_tcpServer?.DisconnectOne(m_clientID);
///
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
///
public void Dispose()
{
if (IsDisposed)
return;
try
{
ReturnSharedServer();
}
finally
{
IsDisposed = true;
}
}
///
/// Sends data to the specified client synchronously.
///
/// ID of the client to which the data is to be sent.
/// The buffer that contains the binary data to be sent.
/// The zero-based position in the at which to begin sending data.
/// The number of bytes to be sent from starting at the .
public void SendTo(Guid clientID, byte[] data, int offset, int length) => SendToAsync(m_clientID, data, offset, length).WaitOne();
///
/// Sends data to all of the connected clients synchronously.
///
/// The buffer that contains the binary data to be sent.
/// The zero-based position in the at which to begin sending data.
/// The number of bytes to be sent from starting at the .
public void Multicast(byte[] data, int offset, int length) => WaitHandle.WaitAll(MulticastAsync(data, offset, length));
///
/// Sends data to the specified client asynchronously.
///
/// ID of the client to which the data is to be sent.
/// The buffer that contains the binary data to be sent.
/// The zero-based position in the at which to begin sending data.
/// The number of bytes to be sent from starting at the .
/// for the asynchronous operation.
public WaitHandle SendToAsync(Guid clientID, byte[] data, int offset, int length) => m_tcpServer?.SendToAsync(clientID, data, offset, length) ?? new ManualResetEvent(true);
///
/// Sends data to all of the connected clients asynchronously.
///
/// The buffer that contains the binary data to be sent.
/// The zero-based position in the at which to begin sending data.
/// The number of bytes to be sent from starting at the .
/// Array of for the asynchronous operation.
public WaitHandle[] MulticastAsync(byte[] data, int offset, int length) => m_tcpServer is null ? Array.Empty() : new[] { m_tcpServer.SendToAsync(m_clientID, data, offset, length) };
///
/// Reads a number of bytes from the current received data buffer and writes those bytes into a byte array at the specified offset.
///
/// ID of the client from which data buffer should be read.
/// Destination buffer used to hold copied bytes.
/// 0-based starting index into destination to begin writing data.
/// The number of bytes to read from current received data buffer and write into .
/// The number of bytes read.
///
/// This function should only be called from within the event handler. Calling this method outside this
/// event will have unexpected results.
///
public int Read(Guid clientID, byte[] buffer, int startIndex, int length) => m_tcpServer?.Read(clientID, buffer, startIndex, length) ?? 0;
///
/// Gets a reference to the shared server listening
/// on this server's local end point.
///
/// A reference to a shared server.
private void GetSharedServer()
{
const string ConfigurationMismatchError = "Configuration mismatch detected between parsers using shared TCP server: {0}";
lock (s_sharedServers)
{
EndPoint localEndPoint = GetLocalEndPoint();
bool sharing = s_sharedServers.TryGetValue(localEndPoint, out TcpServer sharedServer);
if (sharing)
{
// Validate settings to ensure that they match
if (sharedServer.ReceiveBufferSize != ReceiveBufferSize)
throw new InvalidOperationException(string.Format(ConfigurationMismatchError, "Receive buffer size"));
}
else
{
// Create new client and add it to the shared list
sharedServer = new TcpServer();
s_sharedServers.Add(localEndPoint, sharedServer);
s_sharedReferenceCount.Add(localEndPoint, 0);
}
// Set the TCP server member variable ASAP to guarantee
// that it is set before callbacks can be triggered
m_tcpServer = sharedServer;
// Attach to event handlers
sharedServer.ClientConnected += SharedServer_ClientConnected;
sharedServer.ClientDisconnected += SharedServer_ClientDisconnected;
sharedServer.ClientConnectingException += SharedServer_ClientConnectingException;
sharedServer.ReceiveClientData += SharedServer_ReceiveClientData;
sharedServer.ReceiveClientDataException += SharedServer_ReceiveClientDataException;
sharedServer.SendClientDataException += SharedServer_SendClientDataException;
sharedServer.ServerStarted += SharedServer_ServerStarted;
sharedServer.ServerStopped += SharedServer_ServerStopped;
sharedServer.UnhandledUserException += SharedServer_UnhandledUserException;
if (!sharing)
{
// Initialize settings and connect
sharedServer.ConfigurationString = ConfigurationString;
sharedServer.ReceiveBufferSize = ReceiveBufferSize;
sharedServer.Start();
}
// Increment reference count
s_sharedReferenceCount[localEndPoint]++;
}
}
///
/// Releases a reference to this server's shared server,
/// and disposes of the shared server if nobody is using it.
///
private void ReturnSharedServer()
{
lock (s_sharedServers)
{
if (m_tcpServer is null)
return;
// Detach from event handlers
m_tcpServer.ClientConnected -= SharedServer_ClientConnected;
m_tcpServer.ClientDisconnected -= SharedServer_ClientDisconnected;
m_tcpServer.ReceiveClientData -= SharedServer_ReceiveClientData;
m_tcpServer.ReceiveClientDataException -= SharedServer_ReceiveClientDataException;
m_tcpServer.SendClientDataException -= SharedServer_SendClientDataException;
m_tcpServer.ServerStarted -= SharedServer_ServerStarted;
m_tcpServer.ServerStopped -= SharedServer_ServerStopped;
m_tcpServer.UnhandledUserException -= SharedServer_UnhandledUserException;
// Decrement reference count
EndPoint localEndPoint = GetLocalEndPoint();
s_sharedReferenceCount[localEndPoint]--;
if (s_sharedReferenceCount[localEndPoint] == 0)
{
// No more references to TCP server
// exist so dispose of it
m_tcpServer.Stop();
m_tcpServer.Dispose();
s_sharedServers.Remove(localEndPoint);
s_sharedReferenceCount.Remove(localEndPoint);
}
// Release reference to TCP server
m_tcpServer = null;
}
}
///
/// Terminates the server as quickly as possible and
/// removes it from the collection of shared servers.
///
// ReSharper disable once UnusedMember.Local
private void TerminateSharedClient()
{
lock (s_sharedServers)
{
EndPoint localEndPoint = GetLocalEndPoint();
if (s_sharedServers.TryGetValue(localEndPoint, out TcpServer sharedServer))
{
// If the wrapped server and the shared server are the same,
// no one has terminated the shared server yet
if (m_tcpServer == sharedServer)
{
// Disconnect and dispose of the old server
m_tcpServer.Stop();
m_tcpServer.Dispose();
// Remove the old server from the
// collection of shared servers
localEndPoint = GetLocalEndPoint();
s_sharedServers.Remove(localEndPoint);
s_sharedReferenceCount.Remove(localEndPoint);
}
}
m_tcpServer = null;
}
}
///
/// Determines the local end point this server intends
/// to listen on via configuration string properties.
///
/// The local end point.
private EndPoint GetLocalEndPoint()
{
Dictionary settings = ConfigurationString.ParseKeyValuePairs();
IPStack ipStack = Transport.GetInterfaceIPStack(settings);
if (!settings.TryGetValue("interface", out string localInterface))
localInterface = string.Empty;
if (!settings.TryGetValue("port", out string localPortSetting))
throw new InvalidOperationException($"Local port property missing from connection string: {ConfigurationString}");
if (!int.TryParse(localPortSetting, out int localPort))
throw new InvalidOperationException($"Unable to parse local port from \"{localPortSetting}\".");
return Transport.CreateEndPoint(localInterface, localPort, ipStack);
}
// Shared server client connected handler.
// Forwards event to users attached to this server.
private void SharedServer_ClientConnected(object sender, EventArgs e)
{
Guid clientID = e.Argument;
if (!m_tcpServer.TryGetClient(clientID, out TransportProvider client))
return;
if (client.Provider.RemoteEndPoint is not IPEndPoint remoteEndPoint)
return;
if (!remoteEndPoint.Address.Equals(m_remoteAddress))
return;
m_clientID = clientID;
ClientConnected?.Invoke(this, e);
}
// Shared server client disconnected handler.
// Forwards event to users attached to this server.
private void SharedServer_ClientDisconnected(object sender, EventArgs e)
{
if (e.Argument == m_clientID)
ClientDisconnected?.Invoke(this, e);
}
// Shared server client connecting exception handler.
// Forwards event to users attached to this server.
private void SharedServer_ClientConnectingException(object sender, EventArgs e) => ClientConnectingException?.Invoke(this, e);
// Shared server receive client data handler.
// Forwards event to users attached to this server.
private void SharedServer_ReceiveClientData(object sender, EventArgs e)
{
if (e.Argument1 == m_clientID)
ReceiveClientData?.Invoke(this, e);
}
// Shared server receive client data exception handler.
// Forwards event to users attached to this server.
private void SharedServer_ReceiveClientDataException(object sender, EventArgs e)
{
if (e.Argument1 == m_clientID)
ReceiveClientDataException?.Invoke(this, e);
}
// Shared server send client data exception handler.
// Forwards event to users attached to this server.
private void SharedServer_SendClientDataException(object sender, EventArgs e)
{
if (e.Argument1 == m_clientID)
SendClientDataException?.Invoke(this, e);
}
// Shared server started handler.
// Forwards event to users attached to this server.
private void SharedServer_ServerStarted(object sender, EventArgs e) => ServerStarted?.Invoke(this, e);
// Shared server stopped handler.
// Forwards event to users attached to this server.
private void SharedServer_ServerStopped(object sender, EventArgs e) => ServerStopped?.Invoke(this, e);
// Shared server unhandled user exception handler.
// Forwards event to users attached to this server.
private void SharedServer_UnhandledUserException(object sender, EventArgs e) => UnhandledUserException?.Invoke(this, e);
#endregion
#region [ Static ]
// Static Fields
private static readonly Dictionary s_sharedServers = new();
private static readonly Dictionary s_sharedReferenceCount = new();
#endregion
}
///
/// Shared UDP client reference.
///
///
/// This class is used to create multiple IClient instances which share a single UDP client.
/// One shared UDP client instance will be created per local end point.
///
private sealed class SharedUdpClientReference : IClient
{
#region [ Members ]
// Events
public event EventHandler ConnectionAttempt;
public event EventHandler ConnectionEstablished;
public event EventHandler ConnectionTerminated;
public event EventHandler> ConnectionException;
public event EventHandler SendDataStart;
public event EventHandler SendDataComplete;
public event EventHandler> SendDataException;
public event EventHandler> ReceiveData;
public event EventHandler> ReceiveDataComplete;
public event EventHandler> ReceiveDataFrom;
public event EventHandler> ReceiveDataException;
public event EventHandler> UnhandledUserException;
public event EventHandler Disposed;
// Fields
private UdpClient m_udpClient;
private EndPoint m_sendDestination;
private IPAddress m_multicastServerAddress;
private IPAddress m_multicastSourceAddress;
private bool m_receivePacketInfo;
#endregion
#region [ Properties ]
///
/// Gets or sets the data required by the client to connect to the server.
///
public string ConnectionString { get; set; }
///
/// Gets the for which the client has been connected to the server.
///
public Time ConnectionTime => m_udpClient?.ConnectionTime ?? 0.0D;
///
/// Gets the current .
///
public ClientState CurrentState => m_udpClient?.CurrentState ?? ClientState.Disconnected;
///
/// Gets or sets the maximum number of times the client will attempt to connect to the server.
///
/// Set to -1 for infinite connection attempts.
public int MaxConnectionAttempts { get; set; }
///
/// Gets or sets the size of the buffer used by the client for receiving data from the server.
///
/// The value being assigned is either zero or negative.
public int ReceiveBufferSize { get; set; }
///
/// Gets or sets the size of the buffer used by the client for sending data to the server.
///
/// The value being assigned is either zero or negative.
public int SendBufferSize
{
get => m_udpClient?.SendBufferSize ?? 0;
set { }
}
///
/// Gets the server URI.
///
public string ServerUri => m_udpClient?.ServerUri;
///
/// Gets the current server index, when multiple server end points are defined.
///
public int ServerIndex => 0;
///
/// Gets or sets the to be used for the text sent to the server.
///
public Encoding TextEncoding
{
get => m_udpClient?.TextEncoding;
set { }
}
///
/// Gets the used by the client for the transportation of data with the server.
///
public TransportProtocol TransportProtocol => TransportProtocol.Udp;
///
/// Gets or sets a boolean value that indicates whether the client is currently enabled.
///
///
/// Setting to true will start connection cycle for the client if it
/// is not connected, setting to false will disconnect the client if it is connected.
///
public bool Enabled
{
get => m_udpClient?.Enabled ?? false;
set
{
if (m_udpClient is not null)
m_udpClient.Enabled = value;
}
}
///
/// Gets a flag that indicates whether the object has been disposed.
///
public bool IsDisposed { get; private set; }
///
/// Gets the unique identifier of the client.
///
public string Name => m_udpClient?.Name;
///
/// Gets or sets the flag that determines whether the UDP client
/// should attempt to receive packet info when receiving data
/// from the socket.
///
public bool ReceivePacketInfo
{
// ReSharper disable once UnusedMember.Local
get => m_udpClient?.ReceivePacketInfo ?? m_receivePacketInfo;
set
{
if (m_udpClient is not null)
m_udpClient.ReceivePacketInfo = value;
m_receivePacketInfo = value;
}
}
///
/// Gets the for the client connection.
///
public TransportStatistics Statistics => m_udpClient.Statistics;
///
/// Gets the descriptive status of the client.
///
public string Status => m_udpClient?.Status ?? string.Empty;
#endregion
#region [ Methods ]
///
/// Connects the client to the server synchronously.
///
public void Connect() => ConnectAsync();
///
/// Connects the client to the server asynchronously.
///
/// Server property in is invalid.
/// Attempt is made to connect the client when it is not disconnected.
/// for the asynchronous operation.
///
/// Derived classes are expected to override this method with protocol specific connection operations. Call the base class
/// method to obtain an operational wait handle if protocol connection operation doesn't provide one already.
///
public WaitHandle ConnectAsync()
{
Dictionary settings = ConnectionString.ParseKeyValuePairs();
// Set up destination used for send operations
if (settings.TryGetValue("server", out string serverSetting))
{
if (settings.TryGetValue("remotePort", out string remotePortSetting))
serverSetting = $"{serverSetting}:{remotePortSetting}";
Match endPointMatch = Regex.Match(serverSetting, Transport.EndpointFormatRegex);
if (int.TryParse(endPointMatch.Groups["port"].Value, out int remotePort))
{
IPStack ipStack = Transport.GetInterfaceIPStack(settings);
IPEndPoint sendDestination = Transport.CreateEndPoint(endPointMatch.Groups["host"].Value, remotePort, ipStack);
m_sendDestination = sendDestination;
if (Transport.IsMulticastIP(sendDestination.Address))
{
m_multicastServerAddress = sendDestination.Address;
if (settings.TryGetValue("multicastSource", out string multicastSourceSetting))
m_multicastSourceAddress = IPAddress.Parse(multicastSourceSetting);
}
}
}
GetSharedClient();
return null;
}
///
/// When overridden in a derived class, disconnects client from the server synchronously.
///
public void Disconnect()
{
if (m_multicastServerAddress is not null)
m_udpClient.DropMulticastMembership(m_multicastServerAddress, m_multicastSourceAddress);
ReturnSharedClient();
}
///
/// Reads a number of bytes from the current received data buffer and writes those bytes into a byte array at the specified offset.
///
/// Destination buffer used to hold copied bytes.
/// 0-based starting index into destination to begin writing data.
/// The number of bytes to read from current received data buffer and write into .
/// The number of bytes read.
///
/// This function should only be called from within the event handler. Calling this method outside
/// this event will have unexpected results.
///
/// No received data buffer has been defined to read.
/// is null.
///
/// or is less than 0 -or-
/// and will exceed length.
///
public int Read(byte[] buffer, int startIndex, int length) => m_udpClient?.Read(buffer, startIndex, length) ?? 0;
///
/// Requests that the client attempt to move to the next .
///
/// true if request succeeded; otherwise, false.
public bool RequestNextServerIndex() => false;
///
/// Sends data to the server synchronously.
///
/// The buffer that contains the binary data to be sent.
/// The zero-based position in the at which to begin sending data.
/// The number of bytes to be sent from starting at the .
public void Send(byte[] data, int offset, int length) => SendAsync(data, offset, length).WaitOne();
///
/// Sends data to the server asynchronously.
///
/// The buffer that contains the binary data to be sent.
/// The zero-based position in the at which to begin sending data.
/// The number of bytes to be sent from starting at the .
/// for the asynchronous operation.
public WaitHandle SendAsync(byte[] data, int offset, int length) => m_sendDestination is null ? new ManualResetEvent(true) : m_udpClient.SendDataToAsync(data, offset, length, m_sendDestination);
///
/// Initializes the client.
///
///
/// is to be called by user-code directly only if the client is not consumed through the designer surface of the IDE.
///
public void Initialize() { }
///
/// Releases the unmanaged resources used by the client and optionally releases the managed resources.
///
public void Dispose()
{
if (IsDisposed)
return;
try
{
ReturnSharedClient();
}
finally
{
IsDisposed = true;
}
}
///
/// Gets a reference to the shared client listening
/// on this client's local end point.
///
/// A reference to a shared client.
private void GetSharedClient()
{
const string ConfigurationMismatchError = "Configuration mismatch detected between parsers using shared UDP client: {0}";
bool sharing;
UdpClient sharedClient;
lock (s_sharedClients)
{
EndPoint localEndPoint = GetLocalEndPoint();
sharing = s_sharedClients.TryGetValue(localEndPoint, out sharedClient);
if (sharing)
{
// Validate settings to ensure that they match
if (sharedClient.ReceiveBufferSize != ReceiveBufferSize)
throw new InvalidOperationException(string.Format(ConfigurationMismatchError, "Receive buffer size"));
if (sharedClient.MaxConnectionAttempts != MaxConnectionAttempts)
throw new InvalidOperationException(string.Format(ConfigurationMismatchError, "Max connection attempts"));
}
else
{
// Create new client and add it to the shared list
sharedClient = new UdpClient();
s_sharedClients.Add(localEndPoint, sharedClient);
s_sharedReferenceCount.Add(localEndPoint, 0);
}
// Set the UDP client member variable ASAP to guarantee
// that it is set before callbacks can be triggered
m_udpClient = sharedClient;
// Attach to event handlers
sharedClient.ConnectionAttempt += SharedClient_ConnectionAttempt;
sharedClient.ConnectionEstablished += SharedClient_ConnectionEstablished;
sharedClient.ConnectionException += SharedClient_ConnectionException;
sharedClient.ConnectionTerminated += SharedClient_ConnectionTerminated;
sharedClient.ReceiveDataException += SharedClient_ReceiveDataException;
sharedClient.ReceiveDataFrom += SharedClient_ReceiveDataFrom;
sharedClient.SendDataException += SharedClient_SendDataException;
sharedClient.UnhandledUserException += SharedClient_UnhandledUserException;
// Make sure to set packet info flag if needed since
// other SharedUdpClientReferences may not need it
if (m_receivePacketInfo)
sharedClient.ReceivePacketInfo = true;
if (!sharing)
{
// Initialize settings and connect
sharedClient.ConnectionString = ConnectionString;
sharedClient.ReceiveBufferSize = ReceiveBufferSize;
sharedClient.MaxConnectionAttempts = MaxConnectionAttempts;
sharedClient.ConnectAsync();
}
// Increment reference count
s_sharedReferenceCount[localEndPoint]++;
}
if (sharing && sharedClient.CurrentState == ClientState.Connected)
OnConnectionEstablished();
}
///
/// Releases a reference to this client's shared client,
/// and disposes of the shared client if nobody is using it.
///
private void ReturnSharedClient()
{
lock (s_sharedClients)
{
if (m_udpClient is null)
return;
// Detach from event handlers
m_udpClient.ConnectionAttempt -= SharedClient_ConnectionAttempt;
m_udpClient.ConnectionEstablished -= SharedClient_ConnectionEstablished;
m_udpClient.ConnectionException -= SharedClient_ConnectionException;
m_udpClient.ConnectionTerminated -= SharedClient_ConnectionTerminated;
m_udpClient.ReceiveDataException -= SharedClient_ReceiveDataException;
m_udpClient.ReceiveDataFrom -= SharedClient_ReceiveDataFrom;
m_udpClient.SendDataException -= SharedClient_SendDataException;
// Decrement reference count
EndPoint localEndPoint = GetLocalEndPoint();
s_sharedReferenceCount[localEndPoint]--;
if (s_sharedReferenceCount[localEndPoint] == 0)
{
// No more references to UDP client
// exist so dispose of it
m_udpClient.Disconnect();
m_udpClient.Dispose();
s_sharedClients.Remove(localEndPoint);
s_sharedReferenceCount.Remove(localEndPoint);
}
// Release reference to UDP client
m_udpClient = null;
}
}
///
/// Terminates the client as quickly as possible and
/// removes it from the collection of shared clients.
///
private void TerminateSharedClient()
{
lock (s_sharedClients)
{
EndPoint localEndPoint = GetLocalEndPoint();
if (s_sharedClients.TryGetValue(localEndPoint, out UdpClient sharedClient))
{
// If the wrapped client and the shared client are the same,
// no one has terminated the shared client yet
if (m_udpClient == sharedClient)
{
// Disconnect and dispose of the old client
m_udpClient.Disconnect();
m_udpClient.Dispose();
// Remove the old client from the
// collection of shared clients
localEndPoint = GetLocalEndPoint();
s_sharedClients.Remove(localEndPoint);
s_sharedReferenceCount.Remove(localEndPoint);
}
}
m_udpClient = null;
}
}
///
/// Determines the local end point this client intends
/// to listen on via connection string properties.
///
/// The local end point.
private EndPoint GetLocalEndPoint()
{
Dictionary settings = ConnectionString.ParseKeyValuePairs();
IPStack ipStack = Transport.GetInterfaceIPStack(settings);
if (!settings.TryGetValue("interface", out string localInterface))
localInterface = string.Empty;
if (!settings.TryGetValue("localport", out string localPortSetting) && !settings.TryGetValue("port", out localPortSetting))
throw new InvalidOperationException($"Local port property missing from connection string: {ConnectionString}");
if (!int.TryParse(localPortSetting, out int localPort))
throw new InvalidOperationException($"Unable to parse local port from \"{localPortSetting}\".");
return Transport.CreateEndPoint(localInterface, localPort, ipStack);
}
// Triggers the ConnectionEstablished event.
private void OnConnectionEstablished()
{
if (m_multicastServerAddress is not null)
m_udpClient.AddMulticastMembership(m_multicastServerAddress, m_multicastSourceAddress);
ConnectionEstablished?.Invoke(this, EventArgs.Empty);
}
// Shared client connection attempt handler.
// Forwards event to users attached to this client.
private void SharedClient_ConnectionAttempt(object sender, EventArgs e) => ConnectionAttempt?.Invoke(this, e);
// Shared client connection established handler.
// Forwards event to users attached to this client.
private void SharedClient_ConnectionEstablished(object sender, EventArgs e) => OnConnectionEstablished();
// Shared client connection exception handler.
// Forwards event to users attached to this client.
private void SharedClient_ConnectionException(object sender, EventArgs e)
{
// Terminate before propagating the exception to
// ensure that subsequent calls to ReturnSharedClient
// and GetSharedClient will work properly
TerminateSharedClient();
ConnectionException?.Invoke(this, e);
}
// Shared client connection terminated handler.
// Forwards event to users attached to this client.
private void SharedClient_ConnectionTerminated(object sender, EventArgs e) => ConnectionTerminated?.Invoke(this, e);
// Shared client receive data exception handler.
// Forwards event to users attached to this client.
private void SharedClient_ReceiveDataException(object sender, EventArgs e)
{
// Terminate before propagating the exception to
// ensure that subsequent calls to ReturnSharedClient
// and GetSharedClient will work properly
TerminateSharedClient();
ReceiveDataException?.Invoke(this, e);
}
// Shared client receive data from handler.
// Forwards event to users attached to this client.
private void SharedClient_ReceiveDataFrom(object sender, EventArgs e) => ReceiveDataFrom?.Invoke(this, e);
// Shared client send data handler.
// Forwards event to users attached to this client.
private void SharedClient_SendDataException(object sender, EventArgs e)
{
// Terminate before propagating the exception to
// ensure that subsequent calls to ReturnSharedClient
// and GetSharedClient will work properly
TerminateSharedClient();
SendDataException?.Invoke(this, e);
}
// Shared client unhandled user exception handler.
// Forwards event to users attached to this client.
private void SharedClient_UnhandledUserException(object sender, EventArgs e) => UnhandledUserException?.Invoke(this, e);
#endregion
#region [ Static ]
// Static Fields
private static readonly Dictionary s_sharedClients = new();
private static readonly Dictionary s_sharedReferenceCount = new();
#endregion
}
// Constants
///
/// Specifies the default value for the property.
///
public const int DefaultBufferSize = 262144; // 256K
///
/// Specifies the default value for the property.
///
public const int DefaultDefinedFrameRate = 30;
///
/// Specifies the default value for the property.
///
public const int DefaultMaximumConnectionAttempts = -1;
///
/// Specifies the default value for the property.
///
public const bool DefaultAutoStartDataParsingSequence = true;
///
/// Specifies the default value for the property.
///
public const bool DefaultSkipDisableRealTimeData = false;
///
/// Specifies the default value for the property.
///
public const bool DefaultDisableRealTimeDataOnStop = true;
///
/// 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
///
/// Specifies the default value for the property.
///
public const bool DefaultTrustHeaderLength = true;
///
/// Specifies the default value for the property.
///
public const bool DefaultKeepCommandChannelOpen = true;
// Events
///
/// Occurs when any has been received.
///
///
/// is the that was received.
/// is the flag to determine if frame publication should continue.
///
public event EventHandler> ReceivedChannelFrame;
///
/// Occurs when a has been received.
///
///
/// is the that was received.
///
public event EventHandler> ReceivedCommandFrame;
///
/// Occurs when a has been received.
///
///
/// is the that was received.
///
public event EventHandler> ReceivedConfigurationFrame;
///
/// Occurs when a has been received.
///
///
/// is the that was received.
///
public event EventHandler> ReceivedDataFrame;
///
/// Occurs when a has been received.
///
///
/// is the that was received.
///
public event EventHandler> ReceivedHeaderFrame;
///
/// Occurs when an undetermined has been received.
///
///
/// is the undetermined that was received.
///
public event EventHandler> ReceivedUndeterminedFrame;
///
/// Occurs when a frame image has been received.
///
///
/// is the of the frame buffer image that was received.
/// is the length of the frame image that was received.
///
public event EventHandler> ReceivedFrameImage;
///
/// Occurs when a frame buffer image has been received.
///
///
///
/// is the of the frame buffer image that was received.
/// is the buffer that contains the frame image that was received.
/// is the offset into the buffer that contains the frame image that was received.
/// is the length of data in the buffer that contains the frame image that was received.
///
///
/// Consumers should use the more efficient event if the buffer is not needed.
///
///
public event EventHandler> ReceivedFrameBufferImage;
///
/// Occurs when a device sends a notification that its configuration has changed.
///
public event EventHandler ConfigurationChanged;
///
/// Occurs when an is encountered while parsing the data stream.
///
///
/// is the encountered while parsing the data stream.
///
public event EventHandler> ParsingException;
///
/// Occurs when buffer parsing has completed.
///
public event EventHandler BufferParsed;
///
/// Occurs when number of parsing exceptions exceed during .
///
public event EventHandler ExceededParsingExceptionThreshold;
///
/// Occurs when a is sent to a device.
///
///
/// is a reference to the that was sent to the device.
///
public event EventHandler> SentCommandFrame;
///
/// Occurs when an is encountered during connection attempt to a device.
///
///
/// is the exception that occurred during the connection attempt.
/// is the number of connections attempted so far.
///
public event EventHandler> ConnectionException;
///
/// Occurs when is attempting connection to a device.
///
public event EventHandler ConnectionAttempt;
///
/// Occurs when has established a connection to a device.
///
public event EventHandler ConnectionEstablished;
///
/// Occurs when device connection has been terminated.
///
public event EventHandler ConnectionTerminated;
///
/// Occurs when the is setup as a listening connection and server connection has been started.
///
public event EventHandler ServerStarted;
///
/// Occurs when the is setup as a listening connection and server connection has been stopped.
///
public event EventHandler ServerStopped;
///
/// Occurs when the of the associated connection is updated.
///
public event EventHandler ServerIndexUpdated;
// Fields
private PhasorProtocol m_phasorProtocol;
private TransportProtocol m_transportProtocol;
private string m_connectionString;
private int m_maximumConnectionAttempts;
private bool m_autoRepeatCapturedPlayback;
private int m_bufferSize;
private IFrameParser m_frameParser;
private IClient m_dataChannel;
private IServer m_serverBasedDataChannel;
private IClient m_commandChannel;
private IPAddress m_receiveFromAddress;
private IPAddress m_multicastServerAddress;
private PrecisionInputTimer m_inputTimer;
private ShortSynchronizedOperation m_readNextBuffer;
private SharedTimer m_rateCalcTimer;
private IConfigurationFrame m_configurationFrame;
private CheckSumValidationFrameTypes m_checkSumValidationFrameTypes;
private long m_dataStreamStartTime;
private long m_missingFramesOverflow;
private long m_lastFrameReceivedTime;
private int m_frameRateTotal;
private int m_byteRateTotal;
private int m_parsingExceptionCount;
private long m_lastParsingExceptionTime;
private int m_definedFrameRate;
private bool m_initiatingDataStream;
private long m_initialBytesReceived;
private bool m_initiatingSerialConnection;
private IConnectionParameters m_connectionParameters;
private int m_connectionAttempts;
private int m_serverIndex;
private bool m_enabled;
private bool m_disposed;
#endregion
#region [ Constructors ]
///
/// Creates a new using the default settings.
///
public MultiProtocolFrameParser()
{
m_connectionString = "server=127.0.0.1:4712";
m_bufferSize = DefaultBufferSize;
m_maximumConnectionAttempts = DefaultMaximumConnectionAttempts;
m_phasorProtocol = PhasorProtocol.IEEEC37_118V1;
m_transportProtocol = TransportProtocol.Tcp;
m_checkSumValidationFrameTypes = CheckSumValidationFrameTypes.AllFrames;
// Set default frame rate, this calculates milliseconds for each frame
DefinedFrameRate = DefaultDefinedFrameRate;
m_rateCalcTimer = TimerScheduler.CreateTimer();
m_rateCalcTimer.Elapsed += m_rateCalcTimer_Elapsed;
m_rateCalcTimer.Interval = 5000;
m_rateCalcTimer.AutoReset = true;
m_rateCalcTimer.Enabled = false;
// Set minimum timer resolution to one millisecond to improve timer accuracy
PrecisionTimer.SetMinimumTimerResolution(1);
}
///
/// Releases the unmanaged resources before the object is reclaimed by .
///
~MultiProtocolFrameParser() => Dispose(false);
#endregion
#region [ Properties ]
///
/// Gets or sets to use with this .
///
public PhasorProtocol PhasorProtocol
{
get => m_phasorProtocol;
set
{
m_phasorProtocol = value;
DeviceSupportsCommands = DeriveCommandSupport();
// Setup protocol specific connection parameters, for those protocols that have them...
m_connectionParameters = value switch
{
PhasorProtocol.BPAPDCstream => new BPAPDCstream.ConnectionParameters(),
PhasorProtocol.FNET => new FNET.ConnectionParameters(),
PhasorProtocol.SelFastMessage => new SelFastMessage.ConnectionParameters(),
PhasorProtocol.IEC61850_90_5 => new IEC61850_90_5.ConnectionParameters(),
PhasorProtocol.Macrodyne => new Macrodyne.ConnectionParameters(),
_ => null,
};
}
}
///
/// Gets or sets to use with this .
///
public TransportProtocol TransportProtocol
{
get => m_transportProtocol;
set
{
m_transportProtocol = value;
DeviceSupportsCommands = DeriveCommandSupport();
// File based input connections are handled more carefully
if (m_transportProtocol != TransportProtocol.File)
return;
if (m_maximumConnectionAttempts < 1)
m_maximumConnectionAttempts = 1;
}
}
///
/// Gets or sets the key/value pair based connection information required by the to connect to a device.
///
public string ConnectionString
{
get => m_connectionString;
set
{
m_connectionString = value;
// Parse connection string to see if a phasor or transport protocol was assigned
Dictionary settings = m_connectionString.ParseKeyValuePairs();
if (settings.TryGetValue(nameof(PhasorProtocol), out string setting))
PhasorProtocol = (PhasorProtocol)Enum.Parse(typeof(PhasorProtocol), setting, true);
if (settings.TryGetValue(nameof(TransportProtocol), out setting) || settings.TryGetValue("protocol", out setting))
TransportProtocol = (TransportProtocol)Enum.Parse(typeof(TransportProtocol), setting, true);
if (settings.TryGetValue(nameof(KeepCommandChannelOpen), out setting))
KeepCommandChannelOpen = setting.ParseBoolean();
if (settings.TryGetValue(nameof(BufferSize), out setting) && int.TryParse(setting, out int bufferSize) && bufferSize >= 1)
BufferSize = bufferSize;
if (settings.TryGetValue("checkSumValidationFrameTypes", out setting))
{
if (!Enum.TryParse(setting, true, out m_checkSumValidationFrameTypes))
m_checkSumValidationFrameTypes = CheckSumValidationFrameTypes.AllFrames;
}
if (settings.TryGetValue(nameof(TrustHeaderLength), out setting))
TrustHeaderLength = setting.ParseBoolean();
DeviceSupportsCommands = settings.TryGetValue(nameof(DeviceSupportsCommands), out setting) ?
setting.ParseBoolean() :
DeriveCommandSupport();
}
}
///
/// Gets or sets flag that determines whether to keep the command channel open after the initial startup sequence.
/// Defaults to .
///
public bool KeepCommandChannelOpen { get; set; } = DefaultKeepCommandChannelOpen;
///
/// Gets or sets flag that determines if a device supports commands.
///
///
/// This property is automatically derived based on the selected ,
/// and , but can be overridden if the consumer already knows that a device supports commands.
///
public bool DeviceSupportsCommands { get; set; }
///
/// Gets or sets the device identification code often needed to establish a connection.
///
///
/// Many devices validate this ID when sending commands, so it may need to be correct in order to start parsing sequence.
///
public ushort DeviceID { get; set; } = 1;
///
/// Gets or sets the size of the buffer used by the for sending and receiving data from a device.
///
/// The value specified is either zero or negative.
public int BufferSize
{
get => m_bufferSize;
set
{
if (value < 1)
throw new ArgumentException("Value cannot be zero or negative.");
m_bufferSize = value;
}
}
///
/// Gets or sets the maximum number of times the will attempt to connect to a device.
/// Defaults to .
///
/// Set to -1 for infinite connection attempts.
public int MaximumConnectionAttempts
{
get => m_maximumConnectionAttempts;
set
{
m_maximumConnectionAttempts = value;
// All values below zero are assumed to mean infinite connection attempts
if (m_maximumConnectionAttempts < 1)
m_maximumConnectionAttempts = -1;
// We don't allow maximum connection attempts set to infinite if using file based source since file based
// connection errors are like "file not found", "invalid path", etc. These connection exceptions are returned
// so quickly that they will queue up much faster than they will be reported.
if (m_maximumConnectionAttempts < 1 && m_transportProtocol == TransportProtocol.File)
m_maximumConnectionAttempts = 1;
}
}
///
/// Gets or sets flag to automatically send the ConfigFrame2 and EnableRealTimeData command frames used to start a typical data parsing sequence.
/// Defaults to .
///
///
/// For devices that support IEEE commands, setting this property to true will automatically start the data parsing sequence.
///
public bool AutoStartDataParsingSequence { get; set; } = DefaultAutoStartDataParsingSequence;
///
/// Gets or sets flag to skip automatic disabling of the real-time data stream on shutdown or startup.
/// Defaults to .
///
///
/// This flag may important when using UDP multicast with several subscribed clients.
///
public bool SkipDisableRealTimeData { get; set; } = DefaultSkipDisableRealTimeData;
///
/// Gets or sets flag to disable real-time data on stop.
/// Defaults to .
///
///
/// If false, disable real-time command will not be sent when parser is stopped regardless
/// of value.
///
public bool DisableRealTimeDataOnStop { get; set; } = DefaultDisableRealTimeDataOnStop;
///
/// Gets or sets number of parsing exceptions allowed during before connection is reset.
/// Defaults to .
///
public int AllowedParsingExceptions { get; set; } = DefaultAllowedParsingExceptions;
///
/// Gets or sets time duration, in , to monitor parsing exceptions.
/// Defaults to .
///
public Ticks ParsingExceptionWindow { get; set; } = DefaultParsingExceptionWindow;
///
/// Gets or sets a descriptive name for a device connection.
///
public string SourceName { get; set; }
///
/// Gets or sets flag that determines if a high-resolution precision timer should be used for file based input.
///
///
/// Useful when input frames need be accurately time-aligned to the local clock to better simulate
/// an input device and calculate downstream latencies.
/// This is only applicable when connection is made to a file for replay purposes.
///
public bool UseHighResolutionInputTimer
{
get => m_inputTimer is not null;
set
{
// Note that a 1-ms timer and debug mode don't mix, so the high-resolution timer is disabled while debugging
if (value && m_inputTimer is null && !Debugger.IsAttached)
m_inputTimer = PrecisionInputTimer.Attach(m_definedFrameRate, OnParsingException);
else if (!value && m_inputTimer is not null)
PrecisionInputTimer.Detach(ref m_inputTimer);
}
}
///
/// Gets or sets desired frame rate to use for maintaining captured frame replay timing.
/// Defaults to .
///
///
/// This is only applicable when connection is made to a file for replay purposes.
///
public int DefinedFrameRate
{
get => m_definedFrameRate;
set
{
if (m_definedFrameRate != value)
{
bool timerActive = UseHighResolutionInputTimer;
// Deactivate timer before changing defined frame rate
if (timerActive)
UseHighResolutionInputTimer = false;
m_definedFrameRate = value;
// Reactivate timer if it was active
if (timerActive)
UseHighResolutionInputTimer = true;
}
}
}
///
/// Gets or sets the replay start time to use for file based input. File read will begin when encountered
/// frame timestamps are greater than or equal to specified .
///
///
/// This is only applicable when connection is made to a file for replay purposes.
///
public DateTime ReplayStartTime { get; set; } = DateTime.MinValue;
///
/// Gets or sets the replay stop time to use for file based input. File read will end when encountered
/// frame timestamps are greater than or equal to specified .
///
///
/// This is only applicable when connection is made to a file for replay purposes.
///
public DateTime ReplayStopTime { get; set; } = DateTime.MaxValue;
///
/// Gets or sets flag indicating whether or not to inject local system time into parsed data frames.
///
///
/// When connection is made to a file for replay purposes or consumer doesn't trust remote clock source, this flag
/// can be set to true replace all frame timestamps with a UTC timestamp derived from the local system clock.
///
public bool InjectSimulatedTimestamp { get; set; }
///
/// Gets or sets a flag that determines if a file used for replaying data should be restarted at the beginning once it has been completed.
///
///
/// This is only applicable when connection is made to a file for replay purposes.
///
public bool AutoRepeatCapturedPlayback
{
get => m_autoRepeatCapturedPlayback;
set
{
m_autoRepeatCapturedPlayback = value;
if (m_transportProtocol != TransportProtocol.File)
return;
FileClient fileClient = m_dataChannel as FileClient;
if (fileClient is not null)
fileClient.AutoRepeat = value;
}
}
///
/// Gets or sets flag that determines if client should disconnect when end of file has been reached.
///
///
/// This is only applicable when connection is made to a file for replay purposes.
///
public bool DisconnectAtEOF { get; set; }
///
/// Gets or sets current used for parsing 's encountered in the data stream from a device.
///
///
/// If a has been parsed, this will return a reference to the parsed frame. Consumer can manually assign a
/// to start parsing data if one has not been encountered in the stream.
///
public IConfigurationFrame ConfigurationFrame
{
get => m_configurationFrame;
set
{
m_configurationFrame = value;
// Pass new config frame onto appropriate parser, casting into appropriate protocol if needed...
if (m_frameParser is not null)
m_frameParser.ConfigurationFrame = value;
}
}
///
/// Gets or sets flags that determine if check-sums for specified frames should be validated.
///
///
/// It is expected that this will normally be set to .
///
public CheckSumValidationFrameTypes CheckSumValidationFrameTypes
{
get => m_checkSumValidationFrameTypes;
set => m_checkSumValidationFrameTypes = value;
}
///
/// Gets or sets flag that determines if header lengths should be trusted over parsed byte count.
/// Defaults to .
///
///
/// It is expected that this will normally be left as true.
///
public bool TrustHeaderLength { get; set; } = DefaultTrustHeaderLength;
///
/// Gets the number of redundant frames in each packet.
///
///
/// This value is used when calculating statistics. It is assumed that for each
/// frame that is received, that frame will be included in the next n
/// packets, where n is the number of redundant frames per packet.
///
public int RedundantFramesPerPacket => m_frameParser?.RedundantFramesPerPacket ?? 0;
///
/// Gets a flag that determines if the currently selected is an IEEE standard protocol.
///
public bool IsIEEEProtocol => m_phasorProtocol == PhasorProtocol.IEEEC37_118V2 ||
m_phasorProtocol == PhasorProtocol.IEEEC37_118V1 ||
m_phasorProtocol == PhasorProtocol.IEEEC37_118D6 ||
m_phasorProtocol == PhasorProtocol.IEEE1344;
///
/// Gets a flag that determines if the currently selected is connected.
///
public bool IsConnected
{
get
{
if (m_commandChannel is not null && KeepCommandChannelOpen)
return m_commandChannel.CurrentState == ClientState.Connected;
if (m_dataChannel is not null)
return m_dataChannel.CurrentState == ClientState.Connected;
if (m_serverBasedDataChannel is not null)
return m_serverBasedDataChannel.ClientIDs.Length > 0;
return false;
}
}
///
/// Gets the current server index, when multiple server end points are defined.
///
public int ServerIndex
{
get => m_serverIndex;
private set
{
if (m_serverIndex == value)
return;
m_serverIndex = value;
ServerIndexUpdated?.Invoke(this, EventArgs.Empty);
}
}
///
/// Gets a string representing connectivity information.
///
public string ConnectionInfo
{
get
{
if (m_serverBasedDataChannel is not null)
return $"{m_serverBasedDataChannel.TransportProtocol} Server";
string commandChannelServerUri = CommandChannelServerUri;
string dataChannelServerUri = DataChannelServerUri;
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 server URI of the command channel connection, or null if not connected.
///
public string CommandChannelServerUri => m_commandChannel?.ServerUri;
///
/// Gets the server URI of the data channel connection, or null if not connected.
///
public string DataChannelServerUri => m_dataChannel?.ServerUri;
///
/// Gets flag that determines if the connection type is multicast.
///
public bool ConnectionIsMulticast
{
get
{
// Multicast will only be for UDP style connections
if (m_transportProtocol != TransportProtocol.Udp)
return false;
if (string.IsNullOrWhiteSpace(m_connectionString))
return false;
Dictionary settings = m_connectionString.ParseKeyValuePairs();
if (!settings.TryGetValue("server", out string server))
return false;
if (!settings.ContainsKey("remotePort"))
{
Match endPoint = Regex.Match(server, Transport.EndpointFormatRegex);
server = endPoint.Groups["host"].Value;
}
return IPAddress.TryParse(server, out IPAddress serverAddress) && Transport.IsMulticastIP(serverAddress);
}
}
///
/// Gets flag that determines if the connection type is a TCP server.
///
public bool ConnectionIsListener
{
get
{
Dictionary settings;
// Listener setting is only valid for TCP data channels
if (m_transportProtocol != TransportProtocol.Tcp)
return false;
if (string.IsNullOrWhiteSpace(m_connectionString))
return false;
settings = m_connectionString.ParseKeyValuePairs();
return settings.TryGetValue("isListener", out string setting) && setting.ParseBoolean();
}
}
///
/// Gets total time connection has been active.
///
public Time ConnectionTime
{
get
{
if (m_commandChannel is not null)
return m_commandChannel.ConnectionTime;
if (m_dataChannel is not null)
return m_dataChannel.ConnectionTime;
return m_serverBasedDataChannel?.RunTime ?? 0.0D;
}
}
///
/// Gets or sets a boolean value that indicates whether the is currently enabled.
///
///
/// Setting to true will start the if it is not started,
/// setting to false will stop the if it is started.
///
public bool Enabled
{
get => m_enabled;
set
{
if (value && !m_enabled)
Start();
else if (!value && m_enabled)
Stop();
}
}
///
/// Gets the total number of buffers that are currently queued for processing, if any.
///
public int QueuedBuffers => m_frameParser?.QueuedBuffers ?? 0;
///
/// Gets the total number of frames that are currently queued for publication, if any.
///
public int QueuedOutputs => m_frameParser?.QueuedOutputs ?? 0;
///
/// Gets a boolean value that determines if data channel is defined as a server based connection.
///
public bool DataChannelIsServerBased
{
get
{
if (m_dataChannel is not null)
return false;
if (m_serverBasedDataChannel is not null)
return true;
if (string.IsNullOrWhiteSpace(m_connectionString))
return false;
// No connection is currently active, see if connection string defines a server based connection
Dictionary settings = m_connectionString.ParseKeyValuePairs();
return settings.TryGetValue("isListener", out string setting) && setting.ParseBoolean();
}
}
///
/// Gets total number of frames that have been received from a device so far.
///
public long TotalFramesReceived { get; private set; }
///
/// Gets total number of bytes that have been received from a device so far.
///
public long TotalBytesReceived { get; private set; }
///
/// Gets total number of frames that were missing from device so far.
///
public long TotalMissingFrames { get; private set; }
///
/// Gets total number of CRC exceptions encountered from device so far.
///
public long TotalCrcExceptions { get; private set; }
///
/// Gets the configured frame rate as reported by the connected device.
///
public int ConfiguredFrameRate { get; private set; }
///
/// Gets the calculated frame rate (i.e., frames per second) based on data received from device connection.
///
public double CalculatedFrameRate { get; private set; }
///
/// Gets the calculated byte rate (i.e., bytes per second) based on data received from device connection.
///
public double ByteRate { get; private set; }
///
/// Gets the calculated bit rate (i.e., bits per second (bps)) based on data received from device connection.
///
public double BitRate => ByteRate * 8.0D;
///
/// Gets the calculated megabits per second (Mbps) rate based on data received from device connection.
///
public double MegaBitRate => BitRate / SI2.Mega;
///
/// Gets a descriptive name for a device connection that includes , if provided.
///
public string Name
{
get
{
if (string.IsNullOrWhiteSpace(SourceName))
return $"ID {DeviceID} using {m_phasorProtocol.GetFormattedProtocolName()} over {m_transportProtocol}";
return $"{SourceName} ({DeviceID})";
}
}
///
/// Gets current descriptive status of the .
///
public string Status
{
get
{
StringBuilder status = new();
status.AppendLine($" Device Connection ID: {DeviceID}");
status.AppendLine($" Phasor protocol: {m_phasorProtocol.GetFormattedProtocolName()}");
status.AppendLine($" Connection type: {ConnectionType}{(ConnectionIsMulticast ? " - Multicast" : "")}");
status.AppendLine($" Source Connection: {ConnectionInfo.TruncateRight(51)}");
status.AppendLine($" Buffer size: {m_bufferSize:N0}");
status.AppendLine($" Total frames received: {TotalFramesReceived:N0}");
status.AppendLine($" Total missing frames: {TotalMissingFrames:N0}");
status.AppendLine($" Total CRC exceptions: {TotalCrcExceptions:N0}");
status.AppendLine($" Calculated frame rate: {CalculatedFrameRate:0.00} frames/sec");
status.AppendLine($" Calculated data rate: {ByteRate:0.0} bytes/sec, {MegaBitRate:0.0000} Mbps");
status.AppendLine($"Allowed parsing exceptions: {AllowedParsingExceptions:N0}");
status.AppendLine($" Parsing exception window: {ParsingExceptionWindow.ToSeconds():0.00} seconds");
status.AppendLine($"Using simulated timestamps: {(InjectSimulatedTimestamp ? "Yes" : "No")}");
if (m_transportProtocol == TransportProtocol.File)
{
status.AppendLine($" Defined input frame rate: {m_definedFrameRate:N0} frames/sec");
status.AppendLine($" Precision input timer: {(UseHighResolutionInputTimer ? "Enabled" : "Offline")}");
status.AppendLine($" Auto-repeat playback: {AutoRepeatCapturedPlayback}");
status.AppendLine($" Disconnect at EOF: {DisconnectAtEOF}");
if (m_inputTimer is not null)
status.AppendLine($" Timer resynchronizations: {m_inputTimer.Resynchronizations:N0}");
if (ReplayStartTime > DateTime.MinValue || ReplayStopTime < DateTime.MaxValue)
{
status.AppendLine($" Replay start time: {ReplayStartTime:yyyy-MM-dd HH:mm:ss.fff}");
status.AppendLine($" Replay stop time: {ReplayStopTime:yyyy-MM-dd HH:mm:ss.fff}");
}
}
if (m_frameParser is not null)
status.Append(m_frameParser.Status);
if (m_dataChannel is not null)
status.Append(m_dataChannel.Status);
if (m_serverBasedDataChannel is not null)
status.Append(m_serverBasedDataChannel.Status);
if (m_commandChannel is not null)
status.Append(m_commandChannel.Status);
return status.ToString();
}
}
///
/// Gets the connection type (Active, Passive or Hybrid) based on defined channels and transport selections.
///
public string ConnectionType
{
get
{
return m_transportProtocol switch
{
TransportProtocol.Tcp or TransportProtocol.Serial => "Active",
TransportProtocol.Udp or TransportProtocol.File => m_commandChannel is null ? "Passive" : "Hybrid",
_ => "Undetermined",
};
}
}
///
/// Gets or sets any connection specific that may be applicable for the current .
///
public IConnectionParameters ConnectionParameters
{
get => m_connectionParameters;
set
{
m_connectionParameters = value;
// Pass new connection parameters along to derived frame parser if instantiated
if (m_frameParser is not null)
m_frameParser.ConnectionParameters = value;
}
}
#endregion
#region [ Methods ]
///
/// Releases all the resources used by the object.
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// 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.
private void Dispose(bool disposing)
{
if (m_disposed)
return;
try
{
if (!disposing)
return;
Stop();
PrecisionInputTimer.Detach(ref m_inputTimer);
if (m_rateCalcTimer is not null)
{
m_rateCalcTimer.Elapsed -= m_rateCalcTimer_Elapsed;
m_rateCalcTimer.Dispose();
}
m_rateCalcTimer = null;
// Clear minimum timer resolution.
PrecisionTimer.ClearMinimumTimerResolution(1);
}
finally
{
m_disposed = true; // Prevent duplicate dispose.
}
}
///
/// Starts the .
///
public void Start()
{
IConfigurationFrame configurationFrame = m_configurationFrame;
// Stop parser if it is already running - thus calling start after already started will have the effect
// of "restarting" the parsing engine...
Stop();
// Reset statistics...
TotalFramesReceived = 0L;
TotalMissingFrames = 0L;
TotalCrcExceptions = 0L;
m_frameRateTotal = 0;
m_byteRateTotal = 0;
TotalBytesReceived = 0L;
CalculatedFrameRate = 0.0D;
ByteRate = 0.0D;
m_lastParsingExceptionTime = 0L;
m_parsingExceptionCount = 0;
try
{
// Parse connection string to check for special parameters
Dictionary settings = m_connectionString.ParseKeyValuePairs();
// Reset connection attempt counter
m_connectionAttempts = 0;
// Validate that the high-precision input timer is necessary
if (m_transportProtocol != TransportProtocol.File && UseHighResolutionInputTimer)
UseHighResolutionInputTimer = false;
// Establish protocol specific frame parser
InitializeFrameParser(settings);
if (configurationFrame is not null)
ConfigurationFrame = configurationFrame;
if (settings.TryGetValue("commandChannel", out string setting))
{
// Establish command channel connection, if defined...
InitializeCommandChannel(setting);
}
else
{
// Establish data channel connection - must be defined.
InitializeDataChannel(settings);
}
m_rateCalcTimer.Enabled = true;
m_enabled = true;
}
catch (SocketException ex)
{
Stop();
// Check for common error when using an IPv4 address on an IPv6 stack
if (ex.ErrorCode == 10014)
OnConnectionException(new InvalidOperationException($"Bad IP address format in \"{m_connectionString}\": {ex.Message}\r\n\r\nUse a DNS name or an IPv6 formatted IP address (e.g., ::1); otherwise, force IPv4 mode.", ex), 1);
else
OnConnectionException(new InvalidOperationException($"{ex.Message} in \"{m_connectionString}\"", ex), 1);
}
catch (Exception ex)
{
Stop();
OnConnectionException(new InvalidOperationException($"{ex.Message} in \"{m_connectionString}\"", ex), 1);
}
}
///
/// Attempts to initialize the protocol specific frame parser.
///
/// true if frame parser was successfully initialized; otherwise false.
///
/// Starting the multi-protocol frame parser will automatically initialize the frame parsers so calling
/// this method then will be unnecessary, however, if you are using this class just to edit custom
/// connection parameters then initializing the will be necessary.
///
public bool TryInitializeFrameParser()
{
bool success = true;
try
{
if (m_frameParser is not null)
Stop();
InitializeFrameParser(m_connectionString.ParseKeyValuePairs());
}
catch
{
success = false;
}
return success;
}
///
/// Initialize frame parser.
///
/// Key/value pairs dictionary parsed from connection string.
private void InitializeFrameParser(Dictionary settings)
{
string setting;
// Instantiate protocol specific frame parser
switch (m_phasorProtocol)
{
case PhasorProtocol.IEEEC37_118V2:
m_frameParser = new IEEEC37_118.FrameParser(m_checkSumValidationFrameTypes, TrustHeaderLength, DraftRevision.Std2011);
break;
case PhasorProtocol.IEEEC37_118V1:
case PhasorProtocol.IEEEC37_118D6:
m_frameParser = new IEEEC37_118.FrameParser(m_checkSumValidationFrameTypes, TrustHeaderLength, m_phasorProtocol == PhasorProtocol.IEEEC37_118D6 ? DraftRevision.Draft6 : DraftRevision.Std2005);
break;
case PhasorProtocol.IEEE1344:
m_frameParser = new IEEE1344.FrameParser(m_checkSumValidationFrameTypes, TrustHeaderLength);
break;
case PhasorProtocol.IEC61850_90_5:
m_frameParser = new IEC61850_90_5.FrameParser(m_checkSumValidationFrameTypes, TrustHeaderLength);
// Check for IEC 61850-90-5 protocol specific parameters in connection string
if (m_connectionParameters is IEC61850_90_5.ConnectionParameters iecParameters)
{
if (settings.TryGetValue("useETRConfiguration", out setting))
iecParameters.UseETRConfiguration = setting.ParseBoolean();
if (settings.TryGetValue("guessConfiguration", out setting))
iecParameters.GuessConfiguration = setting.ParseBoolean();
if (settings.TryGetValue("parseRedundantASDUs", out setting))
iecParameters.ParseRedundantASDUs = setting.ParseBoolean();
if (settings.TryGetValue("ignoreSignatureValidationFailures", out setting))
iecParameters.IgnoreSignatureValidationFailures = setting.ParseBoolean();
if (settings.TryGetValue("ignoreSampleSizeValidationFailures", out setting))
iecParameters.IgnoreSampleSizeValidationFailures = setting.ParseBoolean();
}
break;
case PhasorProtocol.BPAPDCstream:
m_frameParser = new BPAPDCstream.FrameParser(m_checkSumValidationFrameTypes, TrustHeaderLength);
// Check for BPA PDCstream protocol specific parameters in connection string
if (m_connectionParameters is BPAPDCstream.ConnectionParameters bpaPdcParameters)
{
// INI file name setting is required
if (settings.TryGetValue("iniFileName", out setting))
bpaPdcParameters.ConfigurationFileName = FilePath.GetAbsolutePath(setting);
else if (string.IsNullOrWhiteSpace(bpaPdcParameters.ConfigurationFileName))
throw new ArgumentException("BPA PDCstream INI filename setting (e.g., \"iniFileName=DEVICE_PDC.ini\") was not found. This setting is required for BPA PDCstream protocol connections - frame parser initialization terminated.");
if (settings.TryGetValue("refreshConfigFileOnChange", out setting))
bpaPdcParameters.RefreshConfigurationFileOnChange = setting.ParseBoolean();
if (settings.TryGetValue("parseWordCountFromByte", out setting))
bpaPdcParameters.ParseWordCountFromByte = setting.ParseBoolean();
if (settings.TryGetValue("usePhasorDataFileFormat", out setting))
bpaPdcParameters.UsePhasorDataFileFormat = setting.ParseBoolean();
}
break;
case PhasorProtocol.FNET:
m_frameParser = new FNET.FrameParser(m_checkSumValidationFrameTypes, TrustHeaderLength);
// Check for F-NET protocol specific parameters in connection string
if (m_connectionParameters is FNET.ConnectionParameters fnetParameters)
{
if (settings.TryGetValue("timeOffset", out setting))
fnetParameters.TimeOffset = long.Parse(setting);
if (settings.TryGetValue("stationName", out setting))
fnetParameters.StationName = setting;
if (settings.TryGetValue("frameRate", out setting))
fnetParameters.FrameRate = ushort.Parse(setting);
if (settings.TryGetValue("nominalFrequency", out setting))
fnetParameters.NominalFrequency = (LineFrequency)int.Parse(setting);
}
break;
case PhasorProtocol.SelFastMessage:
m_frameParser = new SelFastMessage.FrameParser(m_checkSumValidationFrameTypes, TrustHeaderLength);
// Check for SEL Fast Message protocol specific parameters in connection string
if (m_connectionParameters is SelFastMessage.ConnectionParameters selParameters)
{
if (settings.TryGetValue("messagePeriod", out setting))
selParameters.MessagePeriod = (MessagePeriod)Enum.Parse(typeof(MessagePeriod), setting, true);
}
break;
case PhasorProtocol.Macrodyne:
m_frameParser = new Macrodyne.FrameParser(m_checkSumValidationFrameTypes, TrustHeaderLength);
// Check for Macrodyne protocol specific parameters in connection string
if (m_connectionParameters is Macrodyne.ConnectionParameters macrodyneParameters)
{
if (settings.TryGetValue("protocolVersion", out setting) && Enum.TryParse(setting, true, out ProtocolVersion protocolVersion))
macrodyneParameters.ProtocolVersion = protocolVersion;
// INI file name setting is required for 1690G protocol
if (settings.TryGetValue("iniFileName", out setting))
macrodyneParameters.ConfigurationFileName = FilePath.GetAbsolutePath(setting);
else if (macrodyneParameters.ProtocolVersion == ProtocolVersion.G && string.IsNullOrWhiteSpace(macrodyneParameters.ConfigurationFileName))
throw new ArgumentException("Macrodyne INI filename setting (e.g., \"iniFileName=DEVICE_PDC.ini\") was not found. This setting is required for 1690G devices - frame parser initialization terminated.");
// Device label setting is required for 1690G protocol
if (settings.TryGetValue("deviceLabel", out setting))
macrodyneParameters.DeviceLabel = setting;
else if (macrodyneParameters.ProtocolVersion == ProtocolVersion.G && string.IsNullOrWhiteSpace(macrodyneParameters.DeviceLabel))
throw new ArgumentException("Macrodyne device label setting (e.g., \"deviceLabel=DEVICE1\") was not found. This setting is required for 1690G devices - frame parser initialization terminated.");
if (settings.TryGetValue("refreshConfigFileOnChange", out setting))
macrodyneParameters.RefreshConfigurationFileOnChange = setting.ParseBoolean();
}
break;
default:
throw new InvalidOperationException($"Phasor protocol \"{m_phasorProtocol}\" is not recognized, failed to initialize frame parser");
}
// Assign frame parser properties
m_frameParser.ConnectionParameters = m_connectionParameters;
// Setup event handlers
m_frameParser.ReceivedChannelFrame += m_frameParser_ReceivedChannelFrame;
m_frameParser.ReceivedCommandFrame += m_frameParser_ReceivedCommandFrame;
m_frameParser.ReceivedConfigurationFrame += m_frameParser_ReceivedConfigurationFrame;
m_frameParser.ReceivedDataFrame += m_frameParser_ReceivedDataFrame;
m_frameParser.ReceivedHeaderFrame += m_frameParser_ReceivedHeaderFrame;
m_frameParser.ReceivedUndeterminedFrame += m_frameParser_ReceivedUndeterminedFrame;
m_frameParser.ReceivedFrameImage += m_frameParser_ReceivedFrameImage;
m_frameParser.ConfigurationChanged += m_frameParser_ConfigurationChanged;
m_frameParser.ParsingException += m_frameParser_ParsingException;
m_frameParser.BufferParsed += m_frameParser_BufferParsed;
// Only attach to this event if consumer needs buffer image (i.e., has attached to our event)
if (ReceivedFrameBufferImage is not null)
m_frameParser.ReceivedFrameBufferImage += m_frameParser_ReceivedFrameBufferImage;
// Start parsing engine
m_frameParser.Start();
}
///
/// Initialize command channel.
///
/// Command channel connection string.
private void InitializeCommandChannel(string connectionString)
{
// Parse command channel connection settings
TransportProtocol transportProtocol;
Dictionary settings = connectionString.ParseKeyValuePairs();
// Verify user did not attempt to setup command channel as a TCP server
if (settings.ContainsKey("isListener") && settings["isListener"].ParseBoolean())
throw new ArgumentException("Command channel cannot be setup as a TCP server.");
// Determine what transport protocol user selected
if (settings.TryGetValue("transportProtocol", out string setting) || settings.TryGetValue("protocol", out setting))
{
transportProtocol = (TransportProtocol)Enum.Parse(typeof(TransportProtocol), setting, true);
// The communications engine only recognizes the transport protocol key as "protocol"
connectionString = connectionString.ReplaceCaseInsensitive("transportProtocol", "protocol");
}
else
{
throw new ArgumentException("No transport protocol was specified for command channel. For example: \"transportProtocol=Tcp\".");
}
// Validate command channel transport protocol selection
if (transportProtocol != TransportProtocol.Tcp && transportProtocol != TransportProtocol.Serial && transportProtocol != TransportProtocol.File)
throw new ArgumentException("Command channel transport protocol can only be defined as TCP, Serial or File");
// Instantiate command channel based on defined transport layer
m_commandChannel = ClientBase.Create(connectionString);
// Setup event handlers
m_commandChannel.ConnectionEstablished += m_commandChannel_ConnectionEstablished;
m_commandChannel.ConnectionAttempt += m_commandChannel_ConnectionAttempt;
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;
m_commandChannel.UnhandledUserException += m_commandChannel_UnhandledUserException;
// Restore last server index so that next connection attempt will be for the next configured server
if (m_commandChannel is ClientBase clientBase)
clientBase.ServerIndex = ServerIndex;
// Attempt connection to device over command channel
m_commandChannel.ReceiveBufferSize = m_bufferSize;
m_commandChannel.MaxConnectionAttempts = m_maximumConnectionAttempts;
m_commandChannel.ConnectAsync();
}
///
/// Initialize data channel.
///
/// Key/value pairs dictionary parsed from connection string.
private void InitializeDataChannel(Dictionary settings)
{
// Instantiate selected transport layer
switch (m_transportProtocol)
{
case TransportProtocol.Tcp:
// The TCP transport may be set up as a server or as a client, we distinguish
// this simply by deriving the value of an added key/value pair in the
// connection string called "IsListener"
if (settings.TryGetValue("isListener", out string setting))
{
if (setting.ParseBoolean())
m_serverBasedDataChannel = settings.ContainsKey("receiveFrom") ? (IServer)new SharedTcpServerReference() : new TcpServer();
else
m_dataChannel = new TcpClient();
}
else
{
// If the key doesn't exist, we assume it's a client connection
m_dataChannel = new TcpClient();
}
break;
case TransportProtocol.Udp:
InitializeUdpDataChannel(settings);
break;
case TransportProtocol.Serial:
m_dataChannel = new SerialClient();
m_initiatingSerialConnection = true;
break;
case TransportProtocol.File:
m_dataChannel = new FileClient();
// For file based playback, we allow the option of auto-repeat
FileClient fileClient = (FileClient)m_dataChannel;
fileClient.FileOpenMode = FileMode.Open;
fileClient.FileAccessMode = FileAccess.Read;
fileClient.FileShareMode = FileShare.Read;
fileClient.ReceiveOnDemand = true;
fileClient.ReceiveBufferSize = ushort.MaxValue;
fileClient.AutoRepeat = AutoRepeatCapturedPlayback;
fileClient.DisconnectAtEOF = DisconnectAtEOF;
// Setup synchronized read operation for file client operations
m_readNextBuffer = new ShortSynchronizedOperation(ReadNextFileBuffer, ex => OnParsingException(new InvalidOperationException($"Encountered an exception while reading file data: {ex.Message}", ex)));
break;
default:
throw new InvalidOperationException($"Transport protocol \"{m_transportProtocol}\" is not recognized, failed to initialize data channel");
}
// Handle primary data connection, this *must* be defined...
if (m_dataChannel is not null)
{
// Setup event handlers
m_dataChannel.ConnectionEstablished += m_dataChannel_ConnectionEstablished;
m_dataChannel.ConnectionAttempt += m_dataChannel_ConnectionAttempt;
m_dataChannel.ConnectionException += m_dataChannel_ConnectionException;
m_dataChannel.ConnectionTerminated += m_dataChannel_ConnectionTerminated;
m_dataChannel.ReceiveData += m_dataChannel_ReceiveData;
m_dataChannel.ReceiveDataException += m_dataChannel_ReceiveDataException;
m_dataChannel.SendDataException += m_dataChannel_SendDataException;
m_dataChannel.UnhandledUserException += m_dataChannel_UnhandledUserException;
// Restore last server index so that next connection attempt will be for the next configured server
if (m_dataChannel is ClientBase clientBase)
clientBase.ServerIndex = ServerIndex;
// Attempt connection to device
m_dataChannel.ReceiveBufferSize = m_bufferSize;
m_dataChannel.ConnectionString = m_connectionString;
m_dataChannel.MaxConnectionAttempts = m_maximumConnectionAttempts;
m_dataChannel.ConnectAsync();
}
else if (m_serverBasedDataChannel is not null)
{
// Setup event handlers
m_serverBasedDataChannel.ClientConnected += m_serverBasedDataChannel_ClientConnected;
m_serverBasedDataChannel.ClientDisconnected += m_serverBasedDataChannel_ClientDisconnected;
m_serverBasedDataChannel.ClientConnectingException += m_serverBasedDataChannel_ClientConnectingException;
m_serverBasedDataChannel.ServerStarted += m_serverBasedDataChannel_ServerStarted;
m_serverBasedDataChannel.ServerStopped += m_serverBasedDataChannel_ServerStopped;
m_serverBasedDataChannel.ReceiveClientData += m_serverBasedDataChannel_ReceiveClientData;
m_serverBasedDataChannel.ReceiveClientDataException += m_serverBasedDataChannel_ReceiveClientDataException;
m_serverBasedDataChannel.SendClientDataException += m_serverBasedDataChannel_SendClientDataException;
m_serverBasedDataChannel.UnhandledUserException += m_serverBasedDataChannel_UnhandledUserException;
// Listen for device connection
m_serverBasedDataChannel.ReceiveBufferSize = m_bufferSize;
m_serverBasedDataChannel.ConfigurationString = m_connectionString;
m_serverBasedDataChannel.MaxClientConnections = 1;
m_serverBasedDataChannel.Start();
}
else
{
throw new InvalidOperationException("No data channel was initialized, cannot start frame parser");
}
}
private void InitializeUdpDataChannel(Dictionary settings)
{
m_multicastServerAddress = null;
if (!settings.TryGetValue("receiveFrom", out string receiveFromSetting))
{
m_dataChannel = new UdpClient();
}
else
{
IPStack ipStack = Transport.GetInterfaceIPStack(settings);
m_receiveFromAddress = Transport.CreateEndPoint(receiveFromSetting, 0, ipStack).Address;
// Set up data channel
SharedUdpClientReference udpRef = new();
udpRef.ReceiveDataFrom += m_dataChannel_ReceiveDataFrom;
m_dataChannel = udpRef;
if (!settings.TryGetValue("server", out string serverSetting))
return;
if (settings.TryGetValue("remotePort", out string remotePortSetting))
serverSetting = $"{serverSetting}:{remotePortSetting}";
Match endPointMatch = Regex.Match(serverSetting, Transport.EndpointFormatRegex);
if (IPAddress.TryParse(endPointMatch.Groups["host"].Value, out IPAddress serverAddress) && Transport.IsMulticastIP(serverAddress))
{
m_multicastServerAddress = serverAddress;
udpRef.ReceivePacketInfo = true;
}
}
}
///
/// Stops the .
///
public void Stop()
{
m_enabled = false;
m_rateCalcTimer.Enabled = false;
m_configurationFrame = null;
// Make sure data stream is disabled
if (!SkipDisableRealTimeData && DisableRealTimeDataOnStop)
{
WaitHandle commandWaitHandle = SendDeviceCommand(DeviceCommand.DisableRealTimeData);
commandWaitHandle?.WaitOne(1000);
}
if (m_dataChannel is not null)
{
try
{
m_dataChannel.Disconnect();
}
catch (Exception ex)
{
OnParsingException(new ConnectionException($"Failed to properly disconnect data channel: {ex.Message}", ex));
}
finally
{
m_dataChannel.ConnectionEstablished -= m_dataChannel_ConnectionEstablished;
m_dataChannel.ConnectionAttempt -= m_dataChannel_ConnectionAttempt;
m_dataChannel.ConnectionException -= m_dataChannel_ConnectionException;
m_dataChannel.ConnectionTerminated -= m_dataChannel_ConnectionTerminated;
m_dataChannel.ReceiveData -= m_dataChannel_ReceiveData;
m_dataChannel.ReceiveDataException -= m_dataChannel_ReceiveDataException;
m_dataChannel.SendDataException -= m_dataChannel_SendDataException;
m_dataChannel.UnhandledUserException -= m_dataChannel_UnhandledUserException;
m_dataChannel.Dispose();
}
m_dataChannel = null;
}
m_readNextBuffer = null;
if (m_serverBasedDataChannel is not null)
{
try
{
m_serverBasedDataChannel.DisconnectAll();
}
catch (Exception ex)
{
OnParsingException(new ConnectionException($"Failed to properly disconnect server based data channel: {ex.Message}", ex));
}
finally
{
m_serverBasedDataChannel.ClientConnected -= m_serverBasedDataChannel_ClientConnected;
m_serverBasedDataChannel.ClientDisconnected -= m_serverBasedDataChannel_ClientDisconnected;
m_serverBasedDataChannel.ClientConnectingException -= m_serverBasedDataChannel_ClientConnectingException;
m_serverBasedDataChannel.ServerStarted -= m_serverBasedDataChannel_ServerStarted;
m_serverBasedDataChannel.ServerStopped -= m_serverBasedDataChannel_ServerStopped;
m_serverBasedDataChannel.ReceiveClientData -= m_serverBasedDataChannel_ReceiveClientData;
m_serverBasedDataChannel.ReceiveClientDataException -= m_serverBasedDataChannel_ReceiveClientDataException;
m_serverBasedDataChannel.SendClientDataException -= m_serverBasedDataChannel_SendClientDataException;
m_serverBasedDataChannel.UnhandledUserException -= m_serverBasedDataChannel_UnhandledUserException;
m_serverBasedDataChannel.Dispose();
}
m_serverBasedDataChannel = null;
}
if (m_commandChannel is not null)
{
try
{
m_commandChannel.Disconnect();
}
catch (Exception ex)
{
OnParsingException(new ConnectionException($"Failed to properly disconnect command channel: {ex.Message}", ex));
}
finally
{
m_commandChannel.ConnectionEstablished -= m_commandChannel_ConnectionEstablished;
m_commandChannel.ConnectionAttempt -= m_commandChannel_ConnectionAttempt;
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;
m_commandChannel.UnhandledUserException -= m_commandChannel_UnhandledUserException;
m_commandChannel.Dispose();
}
m_commandChannel = null;
}
if (m_frameParser is not null)
{
try
{
m_frameParser.Stop();
}
catch (Exception ex)
{
OnParsingException(ex, "Failed to properly stop frame parser: {0}", ex.Message);
}
finally
{
m_frameParser.ReceivedChannelFrame -= m_frameParser_ReceivedChannelFrame;
m_frameParser.ReceivedCommandFrame -= m_frameParser_ReceivedCommandFrame;
m_frameParser.ReceivedConfigurationFrame -= m_frameParser_ReceivedConfigurationFrame;
m_frameParser.ReceivedDataFrame -= m_frameParser_ReceivedDataFrame;
m_frameParser.ReceivedHeaderFrame -= m_frameParser_ReceivedHeaderFrame;
m_frameParser.ReceivedUndeterminedFrame -= m_frameParser_ReceivedUndeterminedFrame;
m_frameParser.ReceivedFrameImage -= m_frameParser_ReceivedFrameImage;
m_frameParser.ConfigurationChanged -= m_frameParser_ConfigurationChanged;
m_frameParser.ParsingException -= m_frameParser_ParsingException;
m_frameParser.BufferParsed -= m_frameParser_BufferParsed;
if (ReceivedFrameBufferImage is not null)
m_frameParser.ReceivedFrameBufferImage -= m_frameParser_ReceivedFrameBufferImage;
m_frameParser.Dispose();
}
m_frameParser = null;
}
}
///
/// Requests that the client attempt to move to the next .
///
/// true if request succeeded; otherwise, false.
///
/// Return value will only be true if changed.
///
public bool RequestNextServerIndex()
{
if (!IsConnected)
return false;
if (m_commandChannel is not null)
{
if (m_commandChannel.RequestNextServerIndex())
ServerIndex = m_commandChannel.ServerIndex;
}
if (m_dataChannel is not null)
{
if (m_dataChannel.RequestNextServerIndex())
ServerIndex = m_dataChannel.ServerIndex;
}
return false;
}
///
/// Sends the specified raw command to the remote device.
///
/// to send to the remote device.
///
/// Command will only be sent if is true and .
///
/// A for send operation.
public WaitHandle SendRawDeviceCommand(ushort rawCommand) => SendDeviceCommand((DeviceCommand)rawCommand);
///
/// Sends the specified to the remote device.
///
/// to send to the remote device.
///
/// Command will only be sent if is true and .
///
/// A for send operation.
///
/// This function will not throw an exception. Any send exceptions will be logged to event. Consumer
/// can assume function failed if returned is null, which will be the case if device does not support
/// commands or no channel is available for publication.
///
public WaitHandle SendDeviceCommand(DeviceCommand command)
{
WaitHandle handle = null;
try
{
if (DeviceSupportsCommands && (m_dataChannel is not null || m_serverBasedDataChannel is not null || m_commandChannel is not null))
{
ICommandFrame commandFrame;
// Only IEEE C37.118-2011 supports requests for config frame 3, for any other protocol,
// downgrade frame request to config frame 2
if (command == DeviceCommand.SendConfigurationFrame3 && m_phasorProtocol != PhasorProtocol.IEEEC37_118V2)
command = DeviceCommand.SendConfigurationFrame2;
// Only the IEEE, SEL Fast Message and Macrodyne protocols support commands
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
switch (m_phasorProtocol)
{
case PhasorProtocol.IEEEC37_118V2:
case PhasorProtocol.IEEEC37_118V1:
case PhasorProtocol.IEEEC37_118D6:
commandFrame = new IEEEC37_118.CommandFrame(DeviceID, command, 1);
break;
case PhasorProtocol.IEEE1344:
commandFrame = new IEEE1344.CommandFrame(DeviceID, command);
break;
case PhasorProtocol.IEC61850_90_5:
commandFrame = new IEC61850_90_5.CommandFrame(DeviceID, command, 1);
break;
case PhasorProtocol.SelFastMessage:
// Get defined message period
MessagePeriod messagePeriod = MessagePeriod.DefaultRate;
if (m_connectionParameters is SelFastMessage.ConnectionParameters connectionParameters)
messagePeriod = connectionParameters.MessagePeriod;
commandFrame = new SelFastMessage.CommandFrame(command, messagePeriod);
break;
case PhasorProtocol.Macrodyne:
commandFrame = new Macrodyne.CommandFrame(command);
break;
default:
commandFrame = null;
break;
}
if (commandFrame is not null)
handle = SendDeviceCommand(commandFrame);
}
}
catch (Exception ex)
{
OnParsingException(new ConnectionException($"Failed to send device command \"{command}\": {ex.Message}", ex));
}
return handle;
}
///
/// Sends the specified to the remote device.
///
/// Command frame to send.
/// A for send operation.
/// is null.
/// Device does not support commands - or - No channel open for command publication.
public WaitHandle SendDeviceCommand(ICommandFrame commandFrame)
{
if (commandFrame is null)
throw new ArgumentNullException(nameof(commandFrame));
if (!DeviceSupportsCommands)
throw new InvalidOperationException("Device does not support commands");
if (m_dataChannel is null && m_serverBasedDataChannel is null && m_commandChannel is null)
throw new InvalidOperationException("No channel open for command publication");
WaitHandle handle = null;
byte[] buffer = commandFrame.BinaryImage();
// Send command over appropriate communications channel - command channel, if defined,
// will take precedence over other communications channels for command traffic...
if (m_commandChannel is not null && m_commandChannel.CurrentState == ClientState.Connected)
{
handle = m_commandChannel.SendAsync(buffer, 0, buffer.Length);
}
else if (m_dataChannel is not null && m_dataChannel.CurrentState == ClientState.Connected)
{
handle = m_dataChannel.SendAsync(buffer, 0, buffer.Length);
}
else if (m_serverBasedDataChannel is not null && m_serverBasedDataChannel.CurrentState == ServerState.Running)
{
WaitHandle[] handles = m_serverBasedDataChannel.MulticastAsync(buffer, 0, buffer.Length);
if (handles is not null && handles.Length > 0)
handle = handles[0];
}
SentCommandFrame?.Invoke(this, new EventArgs(commandFrame));
return handle;
}
///
/// Writes data directly to the frame parsing engine buffer.
///
///
/// This method is public to allow consumer to "manually send extra data" to the parsing engine to be parsed, if desired.
///
/// Buffer containing data to be parsed.
/// Offset into buffer where data begins.
/// Length of data in buffer to be parsed.
public void Write(byte[] buffer, int offset, int count) =>
Parse(SourceChannel.Other, buffer, offset, count);
///
/// Writes a sequence of bytes onto the stream for parsing.
///
/// Defines the source channel for the data.
/// An array of bytes. This method copies count bytes from buffer to the current stream.
/// The zero-based byte offset in buffer at which to begin copying bytes to the current stream.
/// The number of bytes to be written to the current stream.
internal void Parse(SourceChannel source, byte[] buffer, int offset, int count)
{
// Pass data from communications client into protocol specific frame parser
m_frameParser.Parse(source, buffer, offset, count);
m_byteRateTotal += count;
if (m_initiatingDataStream)
m_initialBytesReceived += count;
}
void IFrameParser.Parse(SourceChannel source, byte[] buffer, int offset, int count) =>
Parse(source, buffer, offset, count);
///
/// Resets the value for the statistic.
///
public void ResetTotalBytesReceived() => TotalBytesReceived = 0L;
///
/// Raises the event.
///
/// Exception to send to event.
private void OnParsingException(Exception ex)
{
if (ex is not ThreadAbortException && ex is not ObjectDisposedException)
ParsingException?.Invoke(this, new EventArgs(ex));
if (DateTime.UtcNow.Ticks - m_lastParsingExceptionTime > ParsingExceptionWindow)
{
// Exception window has passed since last exception, so we reset counters
m_lastParsingExceptionTime = DateTime.UtcNow.Ticks;
m_parsingExceptionCount = 0;
}
m_parsingExceptionCount++;
if (m_parsingExceptionCount <= AllowedParsingExceptions)
return;
try
{
// When the parsing exception threshold has been exceeded, connection is stopped
Stop();
}
finally
{
// Notify consumer of parsing exception threshold deviation
OnExceededParsingExceptionThreshold();
m_lastParsingExceptionTime = 0;
m_parsingExceptionCount = 0;
}
}
///
/// Raises the event.
///
/// Actual exception to send as inner exception to event.
/// Message of new exception to send to event.
/// Arguments of message of new exception to send to event.
private void OnParsingException(Exception innerException, string message, params object[] args)
{
if (innerException is not ThreadAbortException && innerException is not ObjectDisposedException)
OnParsingException(new Exception(string.Format(message, args), innerException));
}
///
/// Raises the event.
///
private void OnExceededParsingExceptionThreshold() => ExceededParsingExceptionThreshold?.Invoke(this, EventArgs.Empty);
///
/// Raises the event.
///
/// Exception to raise.
/// Number of connection attempts to report.
private void OnConnectionException(Exception ex, int connectionAttempts)
{
if (ex is not ThreadAbortException && ex is not ObjectDisposedException)
ConnectionException?.Invoke(this, new EventArgs(ex, connectionAttempts));
}
///
/// Derives a flag based on settings that determines if the current connection supports device commands.
///
/// Derived flag that determines if the current connection supports device commands.
private bool DeriveCommandSupport()
{
// Command support is based on phasor protocol, transport protocol and connection style
if (IsIEEEProtocol || m_phasorProtocol == PhasorProtocol.IEC61850_90_5 || m_phasorProtocol == PhasorProtocol.SelFastMessage || m_phasorProtocol == PhasorProtocol.Macrodyne)
{
// IEEE protocols using TCP or Serial connection support device commands
if (m_transportProtocol == TransportProtocol.Tcp || m_transportProtocol == TransportProtocol.Serial)
return true;
if (string.IsNullOrWhiteSpace(m_connectionString))
return false;
Dictionary settings = m_connectionString.ParseKeyValuePairs();
// A defined command channel inherently means commands are supported
if (settings.ContainsKey("commandChannel"))
return true;
if (m_transportProtocol != TransportProtocol.Udp)
return false;
// IEEE protocols "can" use UDP connection to support devices commands, but only
// when remote device acts as a UDP listener (i.e., a "server" connection)
// and remote device is not a multicast end point
if (!settings.TryGetValue("server", out string server))
return false;
if (!settings.ContainsKey("remotePort"))
{
Match endPoint = Regex.Match(server, Transport.EndpointFormatRegex);
server = endPoint.Groups["host"].Value;
}
if (IPAddress.TryParse(server, out IPAddress serverAddress))
return !Transport.IsMulticastIP(serverAddress);
}
return false;
}
// Starts data parsing sequence.
private void StartDataParsingSequence()
{
try
{
// Attempt to stop real-time data, waiting a maximum of three seconds for this activity
if (!SkipDisableRealTimeData && m_phasorProtocol != PhasorProtocol.IEC61850_90_5)
{
// Some devices will only send a config frame once data streaming has been disabled, so
// we use this code to disable real-time data and wait for data to stop streaming...
int attempts = 0;
// Make sure data stream is disabled
SendDeviceCommand(DeviceCommand.DisableRealTimeData);
Thread.Sleep(1000);
// Wait for real-time data stream to cease for up to two seconds
while (m_initialBytesReceived > 0)
{
m_initialBytesReceived = 0;
Thread.Sleep(100);
attempts++;
if (attempts >= 20)
break;
}
}
m_initiatingDataStream = false;
// Request configuration frame once real-time data has been disabled. Note that data stream
// will be enabled when we receive a configuration frame.
switch (m_phasorProtocol)
{
case PhasorProtocol.SelFastMessage:
// SEL Fast Message doesn't define a binary configuration frame so we skip
// requesting one and jump straight to enabling the data stream.
SendDeviceCommand(DeviceCommand.EnableRealTimeData);
break;
case PhasorProtocol.Macrodyne:
// We collect the station name (i.e. the unit ID via 0xBB 0x48)) from the Macrodyne
// protocol interpreted as a header frame before we get the configuration frame
bool sendCommand = true;
if (m_connectionParameters is Macrodyne.ConnectionParameters parameters)
sendCommand = parameters.ProtocolVersion != ProtocolVersion.G;
if (sendCommand)
SendDeviceCommand(DeviceCommand.SendHeaderFrame);
break;
default:
// Otherwise we just request the configuration frame
SendDeviceCommand(DeviceCommand.SendConfigurationFrame3);
break;
}
}
catch (Exception ex)
{
OnParsingException(ex);
}
}
// Calculate frame and data rates
private void m_rateCalcTimer_Elapsed(object sender, EventArgs e)
{
double time = Ticks.ToSeconds(DateTime.UtcNow.Ticks - m_dataStreamStartTime);
CalculatedFrameRate = m_frameRateTotal / time;
ByteRate = m_byteRateTotal / time;
// Since rate calculation timer is not precise, the missing frames calculation can be calculated out
// of sequence with the total frames. If there is a negative balance, we cache the value so it can
// be applied to the next calculation to keep calculation more accurate.
long missingFrames = (long)(ConfiguredFrameRate * m_rateCalcTimer.Interval * SI.Milli * (RedundantFramesPerPacket + 1)) - m_frameRateTotal;
if (missingFrames > 0)
{
TotalMissingFrames += missingFrames + m_missingFramesOverflow;
m_missingFramesOverflow = 0;
}
else
{
m_missingFramesOverflow = missingFrames;
}
TotalFramesReceived += m_frameRateTotal;
TotalBytesReceived += m_byteRateTotal;
m_frameRateTotal = 0;
m_byteRateTotal = 0;
m_dataStreamStartTime = DateTime.UtcNow.Ticks;
}
// Handles needed start-up actions once a client is connected
private void ClientConnectedHandler()
{
try
{
ConnectionEstablished?.Invoke(this, EventArgs.Empty);
if (!DeviceSupportsCommands || !AutoStartDataParsingSequence)
return;
m_initialBytesReceived = 0L;
m_initiatingDataStream = true;
// Begin data parsing sequence to handle reception of configuration frame
Thread startDataParsingSequenceThread = new(StartDataParsingSequence)
{
IsBackground = true
};
startDataParsingSequenceThread.Start();
}
catch (Exception ex)
{
OnParsingException(ex);
}
}
#region [ Data Channel Event Handlers ]
private void m_dataChannel_ReceiveDataFrom(object sender, EventArgs e)
{
int length = e.Argument3;
if (e.Argument1 is not IPEndPoint remoteEndPoint)
return;
if (!remoteEndPoint.Address.Equals(m_receiveFromAddress))
return;
IPAddress destinationAddress = e.Argument2.Address;
if (destinationAddress is not null && m_multicastServerAddress is not null && !destinationAddress.Equals(m_multicastServerAddress))
return;
byte[] buffer = new byte[length];
length = m_dataChannel.Read(buffer, 0, length);
Parse(SourceChannel.Data, buffer, 0, length);
}
private void m_dataChannel_ReceiveData(object sender, EventArgs e)
{
int length = e.Argument;
byte[] buffer = new byte[length];
length = m_dataChannel.Read(buffer, 0, length);
Parse(SourceChannel.Data, buffer, 0, length);
}
private void m_dataChannel_ConnectionEstablished(object sender, EventArgs e)
{
// Handle client connection from data channel
ClientConnectedHandler();
try
{
// Start reading file data
if (m_transportProtocol == TransportProtocol.File)
m_readNextBuffer?.RunOnceAsync();
}
catch (Exception ex)
{
// Process exception for logging
OnParsingException(new InvalidOperationException($"Failed to queue file read operation due to exception: {ex.Message}", ex));
}
}
private void m_dataChannel_ConnectionAttempt(object sender, EventArgs e)
{
m_connectionAttempts++;
ConnectionAttempt?.Invoke(this, EventArgs.Empty);
}
private void m_dataChannel_ConnectionException(object sender, EventArgs e)
{
Exception ex = e.Argument;
if (ex is not NullReferenceException && ex is not ObjectDisposedException)
OnConnectionException(ex, m_connectionAttempts);
// Next server index is selected when a connection exception occurs
if (sender is ClientBase clientBase)
ServerIndex = clientBase.ServerIndex;
}
private void m_dataChannel_ConnectionTerminated(object sender, EventArgs e) => ConnectionTerminated?.Invoke(this, EventArgs.Empty);
private void m_dataChannel_SendDataException(object sender, EventArgs e)
{
Exception ex = e.Argument;
if (ex is not NullReferenceException && ex is not ObjectDisposedException)
OnParsingException(new ConnectionException($"Data channel send exception: {ex.Message}", ex));
}
private void m_dataChannel_ReceiveDataException(object sender, EventArgs e)
{
Exception ex = e.Argument;
// For some serially connected devices, a frame exception on initial connection is very common - so we ignore this during startup
if (m_initiatingSerialConnection && ex is SerialException serialEx && serialEx.SerialError == SerialError.Frame)
{
m_initiatingSerialConnection = false;
return;
}
if (ex is not NullReferenceException && ex is not ObjectDisposedException)
OnParsingException(new ConnectionException($"Data channel receive exception: {ex.Message}", ex));
}
private void m_dataChannel_UnhandledUserException(object sender, EventArgs e)
{
Exception ex = e.Argument;
if (ex is not NullReferenceException && ex is not ObjectDisposedException)
OnParsingException(ex, "Data channel user unhandled exception: {0}", ex.Message);
}
#endregion
#region [ Server Based Data Channel Event Handlers ]
private void m_serverBasedDataChannel_ReceiveClientData(object sender, EventArgs e)
{
Guid clientID = e.Argument1;
int length = e.Argument2;
byte[] buffer = new byte[length];
length = m_serverBasedDataChannel.Read(clientID, buffer, 0, length);
Parse(SourceChannel.Data, buffer, 0, length);
}
private void m_serverBasedDataChannel_ClientConnected(object sender, EventArgs e) => ClientConnectedHandler();
private void m_serverBasedDataChannel_ClientDisconnected(object sender, EventArgs e) => ConnectionTerminated?.Invoke(this, EventArgs.Empty);
private void m_serverBasedDataChannel_ClientConnectingException(object sender, EventArgs e)
{
Exception ex = e.Argument;
if (ex is not NullReferenceException && ex is not ObjectDisposedException)
OnParsingException(new ConnectionException($"Server based data channel send exception: {ex.Message}", ex));
}
private void m_serverBasedDataChannel_ServerStarted(object sender, EventArgs e) => ServerStarted?.Invoke(this, EventArgs.Empty);
private void m_serverBasedDataChannel_ServerStopped(object sender, EventArgs e) => ServerStopped?.Invoke(this, EventArgs.Empty);
private void m_serverBasedDataChannel_SendClientDataException(object sender, EventArgs e)
{
Exception ex = e.Argument2;
if (ex is not NullReferenceException && ex is not ObjectDisposedException)
OnParsingException(new ConnectionException($"Server based data channel send exception: {ex.Message}", ex));
}
private void m_serverBasedDataChannel_ReceiveClientDataException(object sender, EventArgs e)
{
Exception ex = e.Argument2;
if (ex is not NullReferenceException && ex is not ObjectDisposedException)
OnParsingException(new ConnectionException($"Server based data channel receive exception: {ex.Message}", ex));
}
private void m_serverBasedDataChannel_UnhandledUserException(object sender, EventArgs e)
{
Exception ex = e.Argument;
if (ex is not NullReferenceException && ex is not ObjectDisposedException)
OnParsingException(ex, "Server based data channel user unhandled exception: {0}", ex.Message);
}
#endregion
#region [ Command Channel Event Handlers ]
private void m_commandChannel_ReceiveData(object sender, EventArgs e)
{
int length = e.Argument;
byte[] buffer = new byte[length];
length = m_commandChannel.Read(buffer, 0, length);
Parse(SourceChannel.Command, buffer, 0, length);
}
private void m_commandChannel_ConnectionEstablished(object sender, EventArgs e)
{
try
{
// We'll start data channel once command channel has been established...
InitializeDataChannel(m_connectionString.ParseKeyValuePairs());
}
catch (Exception ex)
{
OnConnectionException(ex, m_connectionAttempts);
}
}
private void m_commandChannel_ConnectionAttempt(object sender, EventArgs e)
{
m_connectionAttempts++;
ConnectionAttempt?.Invoke(this, EventArgs.Empty);
}
private void m_commandChannel_ConnectionException(object sender, EventArgs e)
{
Exception ex = e.Argument;
if (ex is not NullReferenceException && ex is not ObjectDisposedException)
OnConnectionException(ex, m_connectionAttempts);
// Next server index is selected when a connection exception occurs
if (sender is ClientBase clientBase)
ServerIndex = clientBase.ServerIndex;
}
private void m_commandChannel_ConnectionTerminated(object sender, EventArgs e)
{
if (!KeepCommandChannelOpen)
return;
ConnectionTerminated?.Invoke(this, EventArgs.Empty);
}
private void m_commandChannel_SendDataException(object sender, EventArgs e)
{
Exception ex = e.Argument;
if (ex is not NullReferenceException && ex is not ObjectDisposedException)
OnParsingException(new ConnectionException($"Command channel send exception: {ex.Message}", ex));
}
private void m_commandChannel_ReceiveDataException(object sender, EventArgs e)
{
Exception ex = e.Argument;
if (ex is not NullReferenceException && ex is not ObjectDisposedException)
OnParsingException(new ConnectionException($"Command channel receive exception: {ex.Message}", ex));
}
private void m_commandChannel_UnhandledUserException(object sender, EventArgs e)
{
Exception ex = e.Argument;
if (ex is not NullReferenceException && ex is not ObjectDisposedException)
OnParsingException(ex, "Command channel user unhandled exception: {0}", ex.Message);
}
#endregion
#region [ Frame Parser Event Handlers ]
private void MaintainCapturedFrameReplayTiming(IChannelFrame sourceFrame)
{
long simulatedTimestamp = 0L;
if (m_inputTimer is null)
{
if (m_lastFrameReceivedTime > 0L)
{
// To maintain timing on "frames per second", we wait for defined frame rate interval
double sleepTime = 1.0D / m_definedFrameRate - (DateTime.UtcNow.Ticks - m_lastFrameReceivedTime) / (double)Ticks.PerSecond;
// Thread sleep time is a minimum suggested sleep time depending on system activity, when not using high-resolution
// input timer we assume getting close is good enough
if (sleepTime > 0.0D)
Thread.Sleep((int)(sleepTime * 1000.0D));
}
m_lastFrameReceivedTime = DateTime.UtcNow.Ticks;
if (InjectSimulatedTimestamp)
simulatedTimestamp = Ticks.AlignToMillisecondDistribution(m_lastFrameReceivedTime, m_definedFrameRate);
}
else
{
// When high resolution input timing is requested, we only need to wait for the next signal...
m_inputTimer.FrameWaitHandle.Wait();
// Input timer can be disabled while thread is waiting, so we make sure it is not null
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (m_inputTimer is not null)
simulatedTimestamp = m_inputTimer.LastFrameTime;
}
// If injecting a simulated timestamp, use the last received time
if (InjectSimulatedTimestamp)
sourceFrame.Timestamp = simulatedTimestamp;
}
private void m_frameParser_ReceivedChannelFrame(object sender, EventArgs e)
{
m_frameRateTotal++;
// We don't stop parsing for exceptions thrown in consumer event handlers
try
{
if (ReceivedChannelFrame is not null)
{
ReceivedChannelFrame(this, e);
if (!e.Argument2)
return;
}
IChannelFrame sourceFrame = e.Argument1;
if (m_transportProtocol == TransportProtocol.File)
{
DateTime timestamp = sourceFrame.Timestamp;
if (timestamp >= ReplayStartTime && timestamp < ReplayStopTime)
MaintainCapturedFrameReplayTiming(sourceFrame);
else
e.Argument2 = false; // Do not publish frame
// Keep reading file data
if (QueuedOutputs < 2 && QueuedBuffers < 10)
m_readNextBuffer?.RunOnceAsync();
}
else if (InjectSimulatedTimestamp)
{
sourceFrame.Timestamp = DateTime.UtcNow.Ticks;
}
}
catch (Exception ex)
{
OnParsingException(ex, "MultiProtocolFrameParser \"ReceivedChannelFrame\" consumer event handler exception: {0}", ex.Message);
}
}
private void m_frameParser_ReceivedCommandFrame(object sender, EventArgs e)
{
// We don't stop parsing for exceptions thrown in consumer event handlers
try
{
ReceivedCommandFrame?.Invoke(this, e);
}
catch (Exception ex)
{
OnParsingException(ex, "MultiProtocolFrameParser \"ReceivedCommandFrame\" consumer event handler exception: {0}", ex.Message);
}
}
private void m_frameParser_ReceivedConfigurationFrame(object sender, EventArgs e)
{
// We automatically request enabling of real-time data upon reception of config frame if requested. Note that SEL Fast Message will
// have already been enabled at this point so we don't duplicate request for enabling real-time data stream
if (m_configurationFrame is null && DeviceSupportsCommands && AutoStartDataParsingSequence && m_phasorProtocol != PhasorProtocol.SelFastMessage && m_phasorProtocol != PhasorProtocol.IEC61850_90_5)
SendDeviceCommand(DeviceCommand.EnableRealTimeData);
m_configurationFrame = e.Argument;
// We don't stop parsing for exceptions thrown in consumer event handlers
try
{
ReceivedConfigurationFrame?.Invoke(this, e);
if (m_configurationFrame is not null)
ConfiguredFrameRate = m_configurationFrame.FrameRate;
}
catch (Exception ex)
{
OnParsingException(ex, "MultiProtocolFrameParser \"ReceivedConfigurationFrame\" consumer event handler exception: {0}", ex.Message);
}
// If user has requested to not keep the command channel open, disconnect it once the system has received a configuration frame
if (!KeepCommandChannelOpen && m_commandChannel is not null && m_commandChannel.CurrentState == ClientState.Connected)
m_commandChannel.Disconnect();
}
private void m_frameParser_ReceivedDataFrame(object sender, EventArgs e)
{
// We don't stop parsing for exceptions thrown in consumer event handlers
try
{
ReceivedDataFrame?.Invoke(this, e);
}
catch (Exception ex)
{
OnParsingException(ex, "MultiProtocolFrameParser \"ReceivedDataFrame\" consumer event handler exception: {0}", ex.Message);
}
}
private void m_frameParser_ReceivedHeaderFrame(object sender, EventArgs e)
{
// Macrodyne receives header frame which contains station name before configuration frame (this gets online data format: 0xBB 0x24)
if (m_configurationFrame is null && m_phasorProtocol == PhasorProtocol.Macrodyne)
SendDeviceCommand(DeviceCommand.SendConfigurationFrame2);
// We don't stop parsing for exceptions thrown in consumer event handlers
try
{
ReceivedHeaderFrame?.Invoke(this, e);
}
catch (Exception ex)
{
OnParsingException(ex, "MultiProtocolFrameParser \"ReceivedHeaderFrame\" consumer event handler exception: {0}", ex.Message);
}
}
private void m_frameParser_ReceivedUndeterminedFrame(object sender, EventArgs e)
{
// We don't stop parsing for exceptions thrown in consumer event handlers
try
{
ReceivedUndeterminedFrame?.Invoke(this, e);
}
catch (Exception ex)
{
OnParsingException(ex, "MultiProtocolFrameParser \"ReceivedUndeterminedFrame\" consumer event handler exception: {0}", ex.Message);
}
}
private void m_frameParser_ReceivedFrameImage(object sender, EventArgs e)
{
// We don't stop parsing for exceptions thrown in consumer event handlers
try
{
ReceivedFrameImage?.Invoke(this, e);
}
catch (Exception ex)
{
OnParsingException(ex, "MultiProtocolFrameParser \"ReceivedFrameImage\" consumer event handler exception: {0}", ex.Message);
}
}
private void m_frameParser_ReceivedFrameBufferImage(object sender, EventArgs e)
{
// We don't stop parsing for exceptions thrown in consumer event handlers
try
{
ReceivedFrameBufferImage?.Invoke(this, e);
}
catch (Exception ex)
{
OnParsingException(ex, "MultiProtocolFrameParser \"ReceivedFrameBufferImage\" consumer event handler exception: {0}", ex.Message);
}
}
private void m_frameParser_ConfigurationChanged(object sender, EventArgs e)
{
// We don't stop parsing for exceptions thrown in consumer event handlers
try
{
ConfigurationChanged?.Invoke(this, e);
}
catch (Exception ex)
{
OnParsingException(ex, "MultiProtocolFrameParser \"ConfigurationChanged\" consumer event handler exception: {0}", ex.Message);
}
}
private void m_frameParser_ParsingException(object sender, EventArgs e)
{
Exception ex = e.Argument;
if (ex is CrcException)
TotalCrcExceptions++;
OnParsingException(ex);
}
private void m_frameParser_BufferParsed(object sender, EventArgs e)
{
// We don't stop parsing for exceptions thrown in consumer event handlers
try
{
BufferParsed?.Invoke(this, EventArgs.Empty);
}
catch (Exception ex)
{
OnParsingException(ex, "MultiProtocolFrameParser \"BufferParsed\" consumer event handler exception: {0}", ex.Message);
}
}
private void ReadNextFileBuffer()
{
if (m_dataChannel is not FileClient fileClient)
return;
if (fileClient.CurrentState == ClientState.Connected)
fileClient.ReadNextBuffer();
}
#endregion
#endregion
#region [ Static ]
private static readonly SharedTimerScheduler TimerScheduler;
static MultiProtocolFrameParser()
{
using (Logger.AppendStackMessages("Owner", "MultiProtocolFrameParser"))
TimerScheduler = new SharedTimerScheduler();
}
#endregion
}
}