diff --git a/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpClient.cs b/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpClient.cs new file mode 100644 index 0000000..fcc59b9 --- /dev/null +++ b/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpClient.cs @@ -0,0 +1,974 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronSockets; + +namespace PepperDash.Core +{ + /// + /// A class to handle secure TCP/IP communications with a server + /// + public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect + { + private const string SplusKey = "Uninitialized Secure Tcp _client"; + /// + /// 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; + + #region GenericSecureTcpIpClient Events & Delegates + + /// + /// + /// + //public event GenericSocketStatusChangeEventDelegate SocketStatusChange; + public event EventHandler ConnectionChange; + + /// + /// Auto reconnect evant handler + /// + public event EventHandler AutoReconnectTriggered; + + /// + /// Event for Receiving text. Once subscribed to this event the receive callback will start a thread that dequeues the messages and invokes the event on a new thread. + /// It is not recommended to use both the TextReceived event and the TextReceivedQueueInvoke event. + /// + public event EventHandler TextReceivedQueueInvoke; + + /// + /// For a client with a pre shared key, this will fire after the communication is established and the key exchange is complete. If you require + /// a key and subscribe to the socket change event and try to send data on a connection the data sent will interfere with the key exchange and disconnect. + /// + public event EventHandler ClientReadyForCommunications; + + #endregion + + + #region GenricTcpIpClient properties + + private string _hostname; + + /// + /// Address of server + /// + public string Hostname + { + get { return _hostname; } + set + { + _hostname = value; + if (_client != null) + { + _client.AddressClientConnectedTo = _hostname; + } + } + } + + /// + /// Port on server + /// + public int Port { get; set; } + + /// + /// S+ helper + /// + public ushort UPort + { + get { return Convert.ToUInt16(Port); } + set { Port = Convert.ToInt32(value); } + } + + /// + /// Defaults to 2000 + /// + public int BufferSize { get; set; } + + /// + /// Internal secure client + /// + private SecureTCPClient _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(); } } + + /// + /// Connection failure reason + /// + public string ConnectionFailure { get { return ClientStatus.ToString(); } } + + /// + /// bool to track if auto reconnect should be set on the socket + /// + 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; } + + /// + /// Flag Set only when the disconnect method is called. + /// + bool DisconnectCalledByUser; + + /// + /// + /// + public bool Connected + { + get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } + } + + // private Timer for auto reconnect + private CTimer RetryTimer; + + #endregion + + #region GenericSecureTcpIpClient properties + + /// + /// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class + /// + public bool SharedKeyRequired { get; set; } + + /// + /// S+ helper for requires shared key bool + /// + public ushort USharedKeyRequired + { + set + { + if (value == 1) + SharedKeyRequired = true; + else + SharedKeyRequired = false; + } + } + + /// + /// SharedKey is sent for varification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module + /// + public string SharedKey { get; set; } + + /// + /// flag to show the client is waiting for the server to send the shared key + /// + private bool WaitingForSharedKeyResponse { get; set; } + + /// + /// Semaphore on connect method + /// + bool IsTryingToConnect; + + /// + /// Bool showing if socket is ready for communication after shared key exchange + /// + public bool IsReadyForCommunication { get; set; } + + /// + /// S+ helper for IsReadyForCommunication + /// + public ushort UIsReadyForCommunication + { + get { return (ushort)(IsReadyForCommunication ? 1 : 0); } + } + + /// + /// Bool Heartbeat Enabled flag + /// + public bool HeartbeatEnabled { get; set; } + + /// + /// S+ helper for Heartbeat Enabled + /// + public ushort UHeartbeatEnabled + { + get { return (ushort)(HeartbeatEnabled ? 1 : 0); } + set { HeartbeatEnabled = value == 1; } + } + + /// + /// Heartbeat String + /// + public string HeartbeatString { get; set; } + //public int HeartbeatInterval = 50000; + + /// + /// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+ + /// + public int HeartbeatInterval { get; set; } + + /// + /// Simpl+ Heartbeat Analog value in seconds + /// + public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } } + + CTimer HeartbeatSendTimer; + CTimer HeartbeatAckTimer; + + // Used to force disconnection on a dead connect attempt + CTimer ConnectFailTimer; + CTimer WaitForSharedKey; + private int ConnectionCount; + + bool ProgramIsStopping; + + /// + /// Queue lock + /// + CCriticalSection DequeueLock = new CCriticalSection(); + + /// + /// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before + /// calling initialize. + /// + public int ReceiveQueueSize { get; set; } + + /// + /// Queue to temporarily store received messages with the source IP and Port info. Defaults to size 20. Use constructor or set queue size property before + /// calling initialize. + /// + private CrestronQueue MessageQueue; + + #endregion + + #region Constructors + + /// + /// Constructor + /// + /// + /// + /// + /// + public GenericSecureTcpIpClient(string key, string address, int port, int bufferSize) + : base(key) + { + StreamDebugging = new CommunicationStreamDebugging(key); + Hostname = address; + Port = port; + BufferSize = bufferSize; + AutoReconnectIntervalMs = 5000; + + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + } + + /// + /// Contstructor that sets all properties by calling the initialize method with a config object. + /// + /// + /// + public GenericSecureTcpIpClient(string key, TcpClientConfigObject clientConfigObject) + : base(key) + { + StreamDebugging = new CommunicationStreamDebugging(key); + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + AutoReconnectIntervalMs = 5000; + BufferSize = 2000; + + Initialize(clientConfigObject); + } + + /// + /// Default constructor for S+ + /// + public GenericSecureTcpIpClient() + : base(SplusKey) + { + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + AutoReconnectIntervalMs = 5000; + BufferSize = 2000; + } + + /// + /// Just to help S+ set the key + /// + public void Initialize(string key) + { + Key = key; + } + + /// + /// Initialize called by the constructor that accepts a client config object. Can be called later to reset properties of client. + /// + /// + public void Initialize(TcpClientConfigObject config) + { + if (config == null) + { + Debug.Console(0, this, "Could not initialize client with key: {0}", Key); + return; + } + try + { + Hostname = config.Control.TcpSshProperties.Address; + Port = config.Control.TcpSshProperties.Port > 0 && config.Control.TcpSshProperties.Port <= 65535 + ? config.Control.TcpSshProperties.Port + : 80; + + AutoReconnect = config.Control.TcpSshProperties.AutoReconnect; + AutoReconnectIntervalMs = config.Control.TcpSshProperties.AutoReconnectIntervalMs > 1000 + ? config.Control.TcpSshProperties.AutoReconnectIntervalMs + : 5000; + + SharedKey = config.SharedKey; + SharedKeyRequired = config.SharedKeyRequired; + + HeartbeatEnabled = config.HeartbeatRequired; + HeartbeatRequiredIntervalInSeconds = config.HeartbeatRequiredIntervalInSeconds > 0 + ? config.HeartbeatRequiredIntervalInSeconds + : (ushort)15; + + + HeartbeatString = string.IsNullOrEmpty(config.HeartbeatStringToMatch) + ? "heartbeat" + : config.HeartbeatStringToMatch; + + BufferSize = config.Control.TcpSshProperties.BufferSize > 2000 + ? config.Control.TcpSshProperties.BufferSize + : 2000; + + ReceiveQueueSize = config.ReceiveQueueSize > 20 + ? config.ReceiveQueueSize + : 20; + + MessageQueue = new CrestronQueue(ReceiveQueueSize); + } + catch (Exception ex) + { + Debug.Console(0, this, "Exception initializing client with key: {0}\rException: {1}", Key, ex); + } + } + + #endregion + + /// + /// Handles closing this up when the program shuts down + /// + void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing _client connection"); + ProgramIsStopping = true; + Disconnect(); + } + + } + + public override bool Deactivate() + { + if (_client != null) + { + _client.SocketStatusChange -= this.Client_SocketStatusChange; + DisconnectClient(); + } + return true; + } + + /// + /// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name. + /// + public void Connect() + { + ConnectionCount++; + Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount); + + + if (IsConnected) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring."); + return; + } + if (IsTryingToConnect) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring."); + return; + } + try + { + IsTryingToConnect = true; + if (RetryTimer != null) + { + RetryTimer.Stop(); + RetryTimer = null; + } + if (string.IsNullOrEmpty(Hostname)) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set"); + return; + } + if (Port < 1 || Port > 65535) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port"); + return; + } + if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set"); + return; + } + + // clean up previous client + if (_client != null) + { + Disconnect(); + } + DisconnectCalledByUser = false; + + _client = new SecureTCPClient(Hostname, Port, BufferSize); + _client.SocketStatusChange += Client_SocketStatusChange; + if (HeartbeatEnabled) + _client.SocketSendOrReceiveTimeOutInMs = (HeartbeatInterval * 5); + _client.AddressClientConnectedTo = Hostname; + _client.PortNumber = Port; + // SecureClient = c; + + //var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff"); + + ConnectFailTimer = new CTimer(o => + { + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount); + if (IsTryingToConnect) + { + IsTryingToConnect = false; + + //if (ConnectionHasHungCallback != null) + //{ + // ConnectionHasHungCallback(); + //} + //SecureClient.DisconnectFromServer(); + //CheckClosedAndTryReconnect(); + } + }, 30000); + + Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount); + _client.ConnectToServerAsync(o => + { + Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount); + + if (ConnectFailTimer != null) + { + ConnectFailTimer.Stop(); + } + IsTryingToConnect = false; + + if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + { + Debug.Console(2, this, "_client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient); + o.ReceiveDataAsync(Receive); + + if (SharedKeyRequired) + { + WaitingForSharedKeyResponse = true; + WaitForSharedKey = new CTimer(timer => + { + + Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication); + // Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus); + // This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup + o.DisconnectFromServer(); + //CheckClosedAndTryReconnect(); + //OnClientReadyForcommunications(false); // Should send false event + }, 15000); + } + else + { + //CLient connected and shared key is not required so just raise the ready for communication event. if Shared key + //required this is called by the shared key being negotiated + if (IsReadyForCommunication == false) + { + OnClientReadyForcommunications(true); // Key not required + } + } + } + else + { + Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus); + CheckClosedAndTryReconnect(); + } + }); + } + catch (Exception ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Error, "_client connection exception: {0}", ex.Message); + IsTryingToConnect = false; + CheckClosedAndTryReconnect(); + } + } + + /// + /// + /// + public void Disconnect() + { + Debug.Console(2, "Disconnect Called"); + + DisconnectCalledByUser = true; + + // stop trying reconnects, if we are + if (RetryTimer != null) + { + RetryTimer.Stop(); + RetryTimer = null; + } + + if (_client != null) + { + DisconnectClient(); + Debug.Console(1, this, "Disconnected"); + } + } + + /// + /// Does the actual disconnect business + /// + public void DisconnectClient() + { + if (_client == null) return; + + Debug.Console(1, this, "Disconnecting client"); + if (IsConnected) + _client.DisconnectFromServer(); + + // close up client. ALWAYS use this when disconnecting. + IsTryingToConnect = false; + + Debug.Console(2, this, "Disconnecting _client {0}", DisconnectCalledByUser ? ", Called by user" : ""); + _client.SocketStatusChange -= Client_SocketStatusChange; + _client.Dispose(); + _client = null; + + if (ConnectFailTimer == null) return; + ConnectFailTimer.Stop(); + ConnectFailTimer.Dispose(); + ConnectFailTimer = null; + } + + /// + /// Internal call to close up client. ALWAYS use this when disconnecting. + /// + //void Cleanup() + //{ + // IsTryingToConnect = false; + + // if (_client != null) + // { + // //SecureClient.DisconnectFromServer(); + // Debug.Console(2, this, "Disconnecting _client {0}", DisconnectCalledByUser ? ", Called by user" : ""); + // _client.SocketStatusChange -= Client_SocketStatusChange; + // _client.Dispose(); + // _client = null; + // } + // if (ConnectFailTimer != null) + // { + // ConnectFailTimer.Stop(); + // ConnectFailTimer.Dispose(); + // ConnectFailTimer = null; + // } + //} + + + #region Methods + + /// + /// Called from Connect failure or Socket Status change if + /// auto reconnect and socket disconnected (Not disconnected by user) + /// + void CheckClosedAndTryReconnect() + { + if (_client != null) + { + Debug.Console(2, this, "Cleaning up remotely closed/failed connection."); + Disconnect(); + } + if (!DisconnectCalledByUser && AutoReconnect) + { + var halfInterval = AutoReconnectIntervalMs / 2; + var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs; + Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime); + if (RetryTimer != null) + { + RetryTimer.Stop(); + RetryTimer = null; + } + if (AutoReconnectTriggered != null) + AutoReconnectTriggered(this, new EventArgs()); + RetryTimer = new CTimer(o => Connect(), rndTime); + } + } + + /// + /// Receive callback + /// + /// + /// + void Receive(SecureTCPClient client, int numBytes) + { + if (numBytes > 0) + { + string str = string.Empty; + var handler = TextReceivedQueueInvoke; + try + { + var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray(); + str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); + Debug.Console(2, this, "_client Received:\r--------\r{0}\r--------", str); + if (!string.IsNullOrEmpty(checkHeartbeat(str))) + { + + if (SharedKeyRequired && str == "SharedKey:") + { + Debug.Console(2, this, "Server asking for shared key, sending"); + SendText(SharedKey + "\n"); + } + else if (SharedKeyRequired && str == "Shared Key Match") + { + StopWaitForSharedKeyTimer(); + + + Debug.Console(2, this, "Shared key confirmed. Ready for communication"); + OnClientReadyForcommunications(true); // Successful key exchange + } + else + { + //var bytesHandler = BytesReceived; + //if (bytesHandler != null) + // bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); + var textHandler = TextReceived; + if (textHandler != null) + textHandler(this, new GenericCommMethodReceiveTextArgs(str)); + if (handler != null) + { + MessageQueue.TryToEnqueue(new GenericTcpServerCommMethodReceiveTextArgs(str)); + } + } + } + } + catch (Exception ex) + { + Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str); + } + if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + client.ReceiveDataAsync(Receive); + + //Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread. + if (handler != null) + { + var gotLock = DequeueLock.TryEnter(); + if (gotLock) + CrestronInvoke.BeginInvoke((o) => DequeueEvent()); + } + } + else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync + { + client.DisconnectFromServer(); + } + } + + /// + /// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently. + /// It will dequeue items as they are enqueued automatically. + /// + void DequeueEvent() + { + try + { + while (true) + { + // Pull from Queue and fire an event. Block indefinitely until an item can be removed, similar to a Gather. + var message = MessageQueue.Dequeue(); + var handler = TextReceivedQueueInvoke; + if (handler != null) + { + handler(this, message); + } + } + } + catch (Exception e) + { + Debug.Console(0, "DequeueEvent error: {0}\r", e); + } + // Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it. + if (DequeueLock != null) + { + DequeueLock.Leave(); + } + } + + void HeartbeatStart() + { + if (HeartbeatEnabled) + { + Debug.Console(2, this, "Starting Heartbeat"); + if (HeartbeatSendTimer == null) + { + + HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval); + } + if (HeartbeatAckTimer == null) + { + HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); + } + } + + } + void HeartbeatStop() + { + + if (HeartbeatSendTimer != null) + { + Debug.Console(2, this, "Stoping Heartbeat Send"); + HeartbeatSendTimer.Stop(); + HeartbeatSendTimer = null; + } + if (HeartbeatAckTimer != null) + { + Debug.Console(2, this, "Stoping Heartbeat Ack"); + HeartbeatAckTimer.Stop(); + HeartbeatAckTimer = null; + } + + } + void SendHeartbeat(object notused) + { + this.SendText(HeartbeatString); + Debug.Console(2, this, "Sending Heartbeat"); + + } + + //private method to check heartbeat requirements and start or reset timer + string checkHeartbeat(string received) + { + try + { + if (HeartbeatEnabled) + { + if (!string.IsNullOrEmpty(HeartbeatString)) + { + var remainingText = received.Replace(HeartbeatString, ""); + var noDelimiter = received.Trim(new char[] { '\r', '\n' }); + if (noDelimiter.Contains(HeartbeatString)) + { + if (HeartbeatAckTimer != null) + { + HeartbeatAckTimer.Reset(HeartbeatInterval * 2); + } + else + { + HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); + } + Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString); + return remainingText; + } + } + } + } + catch (Exception ex) + { + Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); + } + return received; + } + + + + void HeartbeatAckTimerFail(object o) + { + try + { + + if (IsConnected) + { + Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE"); + SendText("Heartbeat not received by server, closing connection"); + CheckClosedAndTryReconnect(); + } + + } + catch (Exception ex) + { + ErrorLog.Error("Heartbeat timeout Error on _client: {0}, {1}", Key, ex); + } + } + + /// + /// + /// + void StopWaitForSharedKeyTimer() + { + if (WaitForSharedKey != null) + { + WaitForSharedKey.Stop(); + WaitForSharedKey = null; + } + } + + /// + /// General send method + /// + public void SendText(string text) + { + if (!string.IsNullOrEmpty(text)) + { + try + { + var bytes = Encoding.GetEncoding(28591).GetBytes(text); + if (_client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + { + _client.SendDataAsync(bytes, bytes.Length, (c, n) => + { + // HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING????? + if (n <= 0) + { + Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key); + } + }); + } + } + catch (Exception ex) + { + Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text); + } + } + } + + /// + /// + /// + public void SendBytes(byte[] bytes) + { + if (bytes.Length > 0) + { + try + { + if (_client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + _client.SendData(bytes, bytes.Length); + } + catch (Exception ex) + { + Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message); + } + } + } + + /// + /// SocketStatusChange Callback + /// + /// + /// + void Client_SocketStatusChange(SecureTCPClient client, SocketStatus clientSocketStatus) + { + if (ProgramIsStopping) + { + ProgramIsStopping = false; + return; + } + try + { + Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus)); + + OnConnectionChange(); + // The client could be null or disposed by this time... + if (_client == null || _client.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) + { + HeartbeatStop(); + OnClientReadyForcommunications(false); // socket has gone low + CheckClosedAndTryReconnect(); + } + } + catch (Exception ex) + { + Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException); + } + } + + /// + /// Helper for ConnectionChange event + /// + void OnConnectionChange() + { + var handler = ConnectionChange; + if (handler == null) return; + + handler(this, new GenericSocketStatusChageEventArgs(this)); + } + + /// + /// Helper to fire ClientReadyForCommunications event + /// + void OnClientReadyForcommunications(bool isReady) + { + IsReadyForCommunication = isReady; + if (IsReadyForCommunication) + HeartbeatStart(); + + var handler = ClientReadyForCommunications; + if (handler == null) return; + + handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication)); + } + #endregion + } + +} \ No newline at end of file diff --git a/Pepperdash Core/Pepperdash Core/Comm/TcpClientConfigObject.cs b/Pepperdash Core/Pepperdash Core/Comm/TcpClientConfigObject.cs index a4fe8b2..c3b3bce 100644 --- a/Pepperdash Core/Pepperdash Core/Comm/TcpClientConfigObject.cs +++ b/Pepperdash Core/Pepperdash Core/Comm/TcpClientConfigObject.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Crestron.SimplSharp; -using PepperDash.Core; -using Newtonsoft.Json; +using Newtonsoft.Json; namespace PepperDash.Core { @@ -16,36 +10,50 @@ public class TcpClientConfigObject /// /// TcpSsh Properties /// + [JsonProperty("control")] public ControlPropertiesConfig Control { get; set; } + /// /// Bool value for secure. Currently not implemented in TCP sockets as they are not dynamic /// + [JsonProperty("secure")] public bool Secure { get; set; } + /// /// Require a shared key that both server and client negotiate. If negotiation fails server disconnects the client /// + [JsonProperty("sharedKeyRequired")] public bool SharedKeyRequired { get; set; } /// /// The shared key that must match on the server and client /// + [JsonProperty("sharedKey")] public string SharedKey { get; set; } + /// /// Require a heartbeat on the client/server connection that will cause the server/client to disconnect if the heartbeat is not received. /// heartbeats do not raise received events. /// + [JsonProperty("heartbeatRequired")] public bool HeartbeatRequired { get; set; } + /// /// The interval in seconds for the heartbeat from the client. If not received client is disconnected /// + [JsonProperty("heartbeatRequiredIntervalInSeconds")] public ushort HeartbeatRequiredIntervalInSeconds { get; set; } + /// /// HeartbeatString that will be checked against the message received. defaults to heartbeat if no string is provided. /// + [JsonProperty("heartbeatStringToMatch")] public string HeartbeatStringToMatch { get; set; } + /// /// Receive Queue size must be greater than 20 or defaults to 20 /// + [JsonProperty("receiveQueueSize")] public int ReceiveQueueSize { get; set; } } } \ No newline at end of file diff --git a/Pepperdash Core/Pepperdash Core/Comm/eControlMethods.cs b/Pepperdash Core/Pepperdash Core/Comm/eControlMethods.cs index 4e5d2eb..4936afd 100644 --- a/Pepperdash Core/Pepperdash Core/Comm/eControlMethods.cs +++ b/Pepperdash Core/Pepperdash Core/Comm/eControlMethods.cs @@ -11,6 +11,6 @@ namespace PepperDash.Core /// public enum eControlMethod { - None = 0, Com, IpId, IpidTcp, IR, Ssh, Tcpip, Telnet, Cresnet, Cec, Udp, Http, Https, Ws, Wss + None = 0, Com, IpId, IpidTcp, IR, Ssh, Tcpip, Telnet, Cresnet, Cec, Udp, Http, Https, Ws, Wss, SecureTcpIp } } \ No newline at end of file diff --git a/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj b/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj index aa8d2c2..86ae71d 100644 --- a/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj +++ b/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj @@ -71,6 +71,7 @@ +