//******************************************************************************************************
// RemoteOutputAdapter.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:
// ----------------------------------------------------------------------------------------------------
// 06/01/2009 - J. Ritchie Carroll
// Generated original version of source code.
// 09/15/2009 - Stephen C. Will
// Added new header and license agreement.
// 09/22/2009 - Pinal C. Patel
// Re-wrote the adapter to utilize new components.
// 09/23/2009 - Pinal C. Patel
// Fixed the handling of socket disconnect.
// 03/04/2010 - Pinal C. Patel
// Added outputIsForArchive and throttleTransmission setting parameters for more control over
// the adapter.
// Switched to ManualResetEvent for waiting on historian acknowledgement for efficiency.
// 01/20/2011 - Pinal C. Patel
// Modified to use Settings for the ConnectionString property of historian socket.
// 12/13/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Threading;
using GSF;
using GSF.Communication;
using GSF.Diagnostics;
using GSF.Historian.Packets;
using GSF.Parsing;
using GSF.TimeSeries;
using GSF.TimeSeries.Adapters;
namespace HistorianAdapters
{
///
/// Represents an output adapter that publishes measurements to openHistorian for archival.
///
[Description("Remote v1.0 Historian Publisher: Forwards measurements to a remote 1.0 openHistorian for archival")]
public class RemoteOutputAdapter : OutputAdapterBase
{
#region [ Members ]
// Constants
private const int DefaultHistorianPort = 1003;
private const bool DefaultPayloadAware = true;
private const bool DefaultConserveBandwidth = true;
private const bool DefaultOutputIsForArchive = true;
private const bool DefaultThrottleTransmission = true;
private const int DefaultSamplesPerTransmission = 100000;
private const int PublisherWaitTime = 5000;
// Fields
private string m_server;
private int m_port;
private bool m_payloadAware;
private bool m_conserveBandwidth;
private bool m_outputIsForArchive;
private bool m_throttleTransmission;
private int m_samplesPerTransmission;
private readonly TcpClient m_historianPublisher;
private byte[] m_publisherBuffer;
private readonly ManualResetEvent m_publisherWaitHandle;
private Action m_publisherDelegate;
private bool m_publisherDisconnecting;
private long m_measurementsPublished;
private bool m_disposed;
#endregion
#region [ Constructors ]
///
/// Initializes a new instance of the class.
///
public RemoteOutputAdapter()
{
m_historianPublisher = new TcpClient();
m_publisherWaitHandle = new ManualResetEvent(false);
m_port = DefaultHistorianPort;
m_payloadAware = DefaultPayloadAware;
m_conserveBandwidth = DefaultConserveBandwidth;
m_outputIsForArchive = DefaultOutputIsForArchive;
m_throttleTransmission = DefaultThrottleTransmission;
m_samplesPerTransmission = DefaultSamplesPerTransmission;
}
#endregion
#region [ Properties ]
///
/// Gets or sets the host name for the server hosting the remote historian.
///
[ConnectionStringParameter,
Description("Define the host name of the remote historian.")]
public string Server
{
get
{
return m_server;
}
set
{
m_server = value;
}
}
///
/// Gets or sets the port on which the remote historian is listening.
///
[ConnectionStringParameter,
Description("Define the port on which the remote historian is listening."),
DefaultValue(1003)]
public int Port
{
get
{
return m_port;
}
set
{
m_port = value;
}
}
///
/// Gets or sets a boolean value indicating whether the payload
/// boundaries are to be preserved during transmission.
///
[ConnectionStringParameter,
Description("Define a value indicating whether to preserve payload boundaries during transmission."),
DefaultValue(true)]
public bool PayloadAware
{
get
{
return m_payloadAware;
}
set
{
m_payloadAware = value;
}
}
///
/// Gets or sets a boolean value that determines the packet
/// type to be used when sending data to the server.
///
[ConnectionStringParameter,
Description("Define a value indicating the packet type when sending data to the server."),
DefaultValue(true)]
public bool ConserveBandwidth
{
get
{
return m_conserveBandwidth;
}
set
{
m_conserveBandwidth = value;
}
}
///
/// Returns a flag that determines if measurements sent to this are destined for archival.
///
[ConnectionStringParameter,
Description("Define a value that determines whether the measurements are destined for archival."),
DefaultValue(true)]
public override bool OutputIsForArchive => m_outputIsForArchive;
///
/// Gets or sets a boolean value that determines whether to wait for
/// acknowledgment from the historian that the last set of points have
/// been received before attempting to send the next set of points.
///
[ConnectionStringParameter,
Description("Define a value that determines whether to wait for acknowledgment before sending more points."),
DefaultValue(true)]
public bool ThrottleTransmission
{
get
{
return m_throttleTransmission;
}
set
{
m_throttleTransmission = value;
}
}
///
/// Gets or sets an integer that indicates the maximum number
/// of points to be published to the historian at once.
///
[ConnectionStringParameter,
Description("Define the maximum number of points to be published at once."),
DefaultValue(100000)]
public int SamplesPerTransmission
{
get
{
return m_samplesPerTransmission;
}
set
{
m_samplesPerTransmission = value;
}
}
///
/// Gets flag that determines if this uses an asynchronous connection.
///
protected override bool UseAsyncConnect => true;
///
/// Returns the detailed status of the data output source.
///
public override string Status
{
get
{
StringBuilder status = new StringBuilder();
status.Append(base.Status);
status.AppendLine();
status.Append(m_historianPublisher.Status);
return status.ToString();
}
}
#endregion
#region [ Methods ]
///
/// Initializes this .
///
/// Server is missing from the .
public override void Initialize()
{
base.Initialize();
string errorMessage = "{0} is missing from Settings - Example: server=localhost;port=1003;payloadAware=True;conserveBandwidth=True;outputIsForArchive=True;throttleTransmission=True;samplesPerTransmission=100000";
Dictionary settings = Settings;
string setting;
// Validate settings.
if (!settings.TryGetValue("server", out m_server))
throw new ArgumentException(string.Format(errorMessage, "server"));
if (settings.TryGetValue("port", out setting))
m_port = int.Parse(setting);
else
settings.Add("port", m_port.ToString());
if (settings.TryGetValue("payloadaware", out setting))
m_payloadAware = setting.ParseBoolean();
if (settings.TryGetValue("conservebandwidth", out setting))
m_conserveBandwidth = setting.ParseBoolean();
if (settings.TryGetValue("outputisforarchive", out setting))
m_outputIsForArchive = setting.ParseBoolean();
if (settings.TryGetValue("throttletransmission", out setting))
m_throttleTransmission = setting.ParseBoolean();
if (settings.TryGetValue("samplespertransmission", out setting))
m_samplesPerTransmission = int.Parse(setting);
// Initialize publisher delegates.
if (m_conserveBandwidth)
{
m_publisherDelegate = TransmitPacketType101;
}
else
{
m_publisherDelegate = TransmitPacketType1;
m_publisherBuffer = new byte[m_samplesPerTransmission * PacketType1.FixedLength];
}
// Initialize publisher socket.
m_historianPublisher.ConnectionString = settings.JoinKeyValuePairs();
m_historianPublisher.PayloadAware = m_payloadAware;
m_historianPublisher.ConnectionAttempt += HistorianPublisher_ConnectionAttempt;
m_historianPublisher.ConnectionEstablished += HistorianPublisher_ConnectionEstablished;
m_historianPublisher.ConnectionTerminated += HistorianPublisher_ConnectionTerminated;
m_historianPublisher.SendDataException += HistorianPublisher_SendDataException;
m_historianPublisher.ReceiveDataComplete += HistorianPublisher_ReceiveDataComplete;
m_historianPublisher.ReceiveDataException += HistorianPublisher_ReceiveDataException;
m_historianPublisher.Initialize();
}
///
/// Gets a short one-line status of this .
///
/// Maximum length of the status message.
/// Text of the status message.
public override string GetShortStatus(int maxLength)
{
if (m_outputIsForArchive)
return $"Published {m_measurementsPublished} measurements for archival.".CenterText(maxLength);
return $"Published {m_measurementsPublished} measurements for processing.".CenterText(maxLength);
}
///
/// Releases the unmanaged resources used by this and optionally releases the managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected override void Dispose(bool disposing)
{
if (!m_disposed)
{
try
{
// This will be done regardless of whether the object is finalized or disposed.
if (disposing)
{
// This will be done only when the object is disposed by calling Dispose().
if (m_historianPublisher != null)
{
m_historianPublisher.ConnectionAttempt -= HistorianPublisher_ConnectionAttempt;
m_historianPublisher.ConnectionEstablished -= HistorianPublisher_ConnectionEstablished;
m_historianPublisher.ConnectionTerminated -= HistorianPublisher_ConnectionTerminated;
m_historianPublisher.SendDataException -= HistorianPublisher_SendDataException;
m_historianPublisher.ReceiveDataComplete -= HistorianPublisher_ReceiveDataComplete;
m_historianPublisher.ReceiveDataException -= HistorianPublisher_ReceiveDataException;
m_historianPublisher.Dispose();
}
m_publisherWaitHandle.Close();
}
}
finally
{
m_disposed = true; // Prevent duplicate dispose.
base.Dispose(disposing); // Call base class Dispose().
}
}
}
///
/// Attempts to connect to this .
///
protected override void AttemptConnection()
{
m_publisherDisconnecting = false;
m_historianPublisher.ConnectAsync();
}
///
/// Attempts to disconnect from this .
///
protected override void AttemptDisconnection()
{
m_publisherDisconnecting = true;
m_historianPublisher.Disconnect();
}
///
/// Publishes for archival.
///
/// Measurements to be archived.
/// Acknowledgment is not received from historian for published data.
protected override void ProcessMeasurements(IMeasurement[] measurements)
{
if (m_historianPublisher.CurrentState != ClientState.Connected)
throw new InvalidOperationException("Historian publisher socket is not connected");
try
{
for (int i = 0; i < measurements.Length; i += m_samplesPerTransmission)
{
// Wait for historian acknowledgment.
if (m_throttleTransmission)
{
if (!m_publisherWaitHandle.WaitOne(PublisherWaitTime))
throw new OperationCanceledException("Timeout waiting for acknowledgment from historian");
}
// Publish measurements to historian.
m_publisherWaitHandle.Reset();
m_publisherDelegate(measurements, i, (measurements.Length - i < m_samplesPerTransmission ? measurements.Length : i + m_samplesPerTransmission) - 1);
}
m_measurementsPublished += measurements.Length;
}
catch
{
m_publisherWaitHandle.Set();
throw;
}
}
private void HistorianPublisher_ConnectionAttempt(object sender, EventArgs e)
{
OnStatusMessage(MessageLevel.Info, "Attempting socket connection...");
}
private void HistorianPublisher_ConnectionEstablished(object sender, EventArgs e)
{
OnConnected();
m_publisherWaitHandle.Set();
}
private void HistorianPublisher_ConnectionTerminated(object sender, EventArgs e)
{
m_measurementsPublished = 0;
m_publisherWaitHandle.Reset();
if (!m_publisherDisconnecting)
Start();
}
private void HistorianPublisher_SendDataException(object sender, EventArgs e)
{
m_publisherWaitHandle.Set();
OnProcessException(MessageLevel.Warning, e.Argument);
}
private void HistorianPublisher_ReceiveDataComplete(object sender, EventArgs e)
{
// Check for acknowledgment from historian.
string reply = Encoding.ASCII.GetString(e.Argument1, 0, e.Argument2);
if (reply == "ACK")
m_publisherWaitHandle.Set();
}
private void HistorianPublisher_ReceiveDataException(object sender, EventArgs e)
{
m_publisherWaitHandle.Set();
OnProcessException(MessageLevel.Warning, e.Argument);
}
private void TransmitPacketType1(IMeasurement[] measurements, int startIndex, int endIndex)
{
int bufferIndex = 0;
for (int i = startIndex; i <= endIndex; i++)
bufferIndex += new PacketType1(measurements[i]).GenerateBinaryImage(m_publisherBuffer, bufferIndex);
m_historianPublisher.SendAsync(m_publisherBuffer, 0, bufferIndex);
}
private void TransmitPacketType101(IMeasurement[] measurements, int startIndex, int endIndex)
{
PacketType101 packet = new PacketType101();
for (int i = startIndex; i <= endIndex; i++)
packet.Data.Add(new PacketType101DataPoint(measurements[i]));
m_historianPublisher.SendAsync(packet.BinaryImage());
}
#endregion
}
}