using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using Newtonsoft.Json;
namespace PepperDash.Core
{
///
/// A class to handle basic TCP/IP communications with a server
///
public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
{
private const string SplusKey = "Uninitialized TcpIpClient";
///
/// Object to enable stream debugging
///
public CommunicationStreamDebugging StreamDebugging { get; private set; }
///
/// Fires when data is received from the server and returns it as a Byte array
///
public event EventHandler BytesReceived;
///
/// Fires when data is received from the server and returns it as text
///
public event EventHandler TextReceived;
///
///
///
//public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
public event EventHandler ConnectionChange;
private string _hostname;
///
/// Address of server
///
public string Hostname
{
get
{
return _hostname;
}
set
{
_hostname = value;
if (_client != null)
{
_client.AddressClientConnectedTo = _hostname;
}
}
}
///
/// Gets or sets the Port
///
public int Port { get; set; }
///
/// Another damn S+ helper because S+ seems to treat large port nums as signed ints
/// which screws up things
///
public ushort UPort
{
get { return Convert.ToUInt16(Port); }
set { Port = Convert.ToInt32(value); }
}
///
/// Defaults to 2000
///
public int BufferSize { get; set; }
///
/// The actual client class
///
private TCPClient _client;
///
/// Bool showing if socket is connected
///
public bool IsConnected
{
get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
}
///
/// S+ helper for IsConnected
///
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
///
/// _client socket status Read only
///
public SocketStatus ClientStatus
{
get
{
return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus;
}
}
///
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
/// and IsConnected would be true when this == 2.
///
public ushort UStatus
{
get { return (ushort)ClientStatus; }
}
///
/// Status text shows the message associated with socket status
///
public string ClientStatusText { get { return ClientStatus.ToString(); } }
///
/// Ushort representation of client status
///
[Obsolete]
public ushort UClientStatus { get { return (ushort)ClientStatus; } }
///
/// Connection failure reason
///
public string ConnectionFailure { get { return ClientStatus.ToString(); } }
///
/// Gets or sets the AutoReconnect
///
public bool AutoReconnect { get; set; }
///
/// S+ helper for AutoReconnect
///
public ushort UAutoReconnect
{
get { return (ushort)(AutoReconnect ? 1 : 0); }
set { AutoReconnect = value == 1; }
}
///
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
///
public int AutoReconnectIntervalMs { get; set; }
///
/// Set only when the disconnect method is called
///
bool DisconnectCalledByUser;
///
///
///
public bool Connected
{
get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
}
//Lock object to prevent simulatneous connect/disconnect operations
private CCriticalSection connectLock = new CCriticalSection();
// private Timer for auto reconnect
private CTimer RetryTimer;
///
/// Constructor
///
/// unique string to differentiate between instances
///
///
///
public GenericTcpIpClient(string key, string address, int port, int bufferSize)
: base(key)
{
StreamDebugging = new CommunicationStreamDebugging(key);
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
Hostname = address;
Port = port;
BufferSize = bufferSize;
RetryTimer = new CTimer(o =>
{
Reconnect();
}, Timeout.Infinite);
}
///
/// Constructor
///
///
public GenericTcpIpClient(string key)
: base(key)
{
StreamDebugging = new CommunicationStreamDebugging(key);
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
BufferSize = 2000;
RetryTimer = new CTimer(o =>
{
Reconnect();
}, Timeout.Infinite);
}
///
/// Default constructor for S+
///
public GenericTcpIpClient()
: base(SplusKey)
{
StreamDebugging = new CommunicationStreamDebugging(SplusKey);
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
BufferSize = 2000;
RetryTimer = new CTimer(o =>
{
Reconnect();
}, Timeout.Infinite);
}
///
/// Initialize method
///
public void Initialize(string key)
{
Key = key;
}
///
/// Handles closing this up when the program shuts down
///
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping)
{
Debug.Console(1, this, "Program stopping. Closing connection");
Deactivate();
}
}
///
///
///
///
///
/// Deactivate method
///
public override bool Deactivate()
{
RetryTimer.Stop();
RetryTimer.Dispose();
if (_client != null)
{
_client.SocketStatusChange -= this.Client_SocketStatusChange;
DisconnectClient();
}
return true;
}
///
/// Connect method
///
public void Connect()
{
if (string.IsNullOrEmpty(Hostname))
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': No address set", Key);
return;
}
if (Port < 1 || Port > 65535)
{
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': Invalid port", Key);
return;
}
}
try
{
connectLock.Enter();
if (IsConnected)
{
Debug.Console(1, this, "Connection already connected. Exiting Connect()");
}
else
{
//Stop retry timer if running
RetryTimer.Stop();
_client = new TCPClient(Hostname, Port, BufferSize);
_client.SocketStatusChange -= Client_SocketStatusChange;
_client.SocketStatusChange += Client_SocketStatusChange;
DisconnectCalledByUser = false;
_client.ConnectToServerAsync(ConnectToServerCallback);
}
}
finally
{
connectLock.Leave();
}
}
private void Reconnect()
{
if (_client == null)
{
return;
}
try
{
connectLock.Enter();
if (IsConnected || DisconnectCalledByUser == true)
{
Debug.Console(1, this, "Reconnect no longer needed. Exiting Reconnect()");
}
else
{
Debug.Console(1, this, "Attempting reconnect now");
_client.ConnectToServerAsync(ConnectToServerCallback);
}
}
finally
{
connectLock.Leave();
}
}
///
/// Disconnect method
///
public void Disconnect()
{
try
{
connectLock.Enter();
DisconnectCalledByUser = true;
// Stop trying reconnects, if we are
RetryTimer.Stop();
DisconnectClient();
}
finally
{
connectLock.Leave();
}
}
///
/// DisconnectClient method
///
public void DisconnectClient()
{
if (_client != null)
{
Debug.Console(1, this, "Disconnecting client");
if (IsConnected)
_client.DisconnectFromServer();
}
}
///
/// Callback method for connection attempt
///
///
void ConnectToServerCallback(TCPClient c)
{
if (c.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{
Debug.Console(0, this, "Server connection result: {0}", c.ClientStatus);
WaitAndTryReconnect();
}
else
{
Debug.Console(1, this, "Server connection result: {0}", c.ClientStatus);
}
}
///
/// Disconnects, waits and attemtps to connect again
///
void WaitAndTryReconnect()
{
CrestronInvoke.BeginInvoke(o =>
{
try
{
connectLock.Enter();
if (!IsConnected && AutoReconnect && !DisconnectCalledByUser && _client != null)
{
DisconnectClient();
Debug.Console(1, this, "Attempting reconnect, status={0}", _client.ClientStatus);
RetryTimer.Reset(AutoReconnectIntervalMs);
}
}
finally
{
connectLock.Leave();
}
});
}
///
/// Recieves incoming data
///
///
///
void Receive(TCPClient client, int numBytes)
{
if (client != null)
{
if (numBytes > 0)
{
var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray();
var bytesHandler = BytesReceived;
if (bytesHandler != null)
{
if (StreamDebugging.RxStreamDebuggingIsEnabled)
{
Debug.Console(0, this, "Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
}
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
}
var textHandler = TextReceived;
if (textHandler != null)
{
var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
if (StreamDebugging.RxStreamDebuggingIsEnabled)
{
Debug.Console(0, this, "Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length);
}
textHandler(this, new GenericCommMethodReceiveTextArgs(str));
}
}
client.ReceiveDataAsync(Receive);
}
}
///
/// SendText method
///
public void SendText(string text)
{
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
// Check debug level before processing byte array
if (StreamDebugging.TxStreamDebuggingIsEnabled)
Debug.Console(0, this, "Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text));
if (_client != null)
_client.SendData(bytes, bytes.Length);
}
///
/// SendEscapedText method
///
public void SendEscapedText(string text)
{
var unescapedText = Regex.Replace(text, @"\\x([0-9a-fA-F][0-9a-fA-F])", s =>
{
var hex = s.Groups[1].Value;
return ((char)Convert.ToByte(hex, 16)).ToString();
});
SendText(unescapedText);
}
///
/// Sends Bytes to the server
///
///
///
/// SendBytes method
///
public void SendBytes(byte[] bytes)
{
if (StreamDebugging.TxStreamDebuggingIsEnabled)
Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
if (_client != null)
_client.SendData(bytes, bytes.Length);
}
///
/// Socket Status Change Handler
///
///
///
void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus)
{
if (clientSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{
Debug.Console(0, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
WaitAndTryReconnect();
}
else
{
Debug.Console(1, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
_client.ReceiveDataAsync(Receive);
}
var handler = ConnectionChange;
if (handler != null)
ConnectionChange(this, new GenericSocketStatusChageEventArgs(this));
}
}
///
/// Represents a TcpSshPropertiesConfig
///
public class TcpSshPropertiesConfig
{
///
/// Address to connect to
///
[JsonProperty(Required = Required.Always)]
public string Address { get; set; }
///
/// Port to connect to
///
[JsonProperty(Required = Required.Always)]
public int Port { get; set; }
///
/// Username credential
///
public string Username { get; set; }
///
/// Gets or sets the Password
///
public string Password { get; set; }
///
/// Defaults to 32768
///
public int BufferSize { get; set; }
///
/// Gets or sets the AutoReconnect
///
public bool AutoReconnect { get; set; }
///
/// Gets or sets the AutoReconnectIntervalMs
///
public int AutoReconnectIntervalMs { get; set; }
///
/// Default constructor
///
public TcpSshPropertiesConfig()
{
BufferSize = 32768;
AutoReconnect = true;
AutoReconnectIntervalMs = 5000;
Username = "";
Password = "";
}
}
}