fix: update solution filename and folder paths for workflow expectations.

This commit is contained in:
Jonathan Arndt
2025-05-05 16:46:17 -07:00
parent 083b935d71
commit d84eca0cc4
83 changed files with 12323 additions and 12342 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Crestron.SimplSharp;
using PepperDash.Core;
namespace PepperDash.Core
{
/// <summary>
/// Defines the string event handler for line events on the gather
/// </summary>
/// <param name="text"></param>
public delegate void LineReceivedHandler(string text);
/// <summary>
/// Attaches to IBasicCommunication as a text gather
/// </summary>
public class CommunicationGather
{
/// <summary>
/// Event that fires when a line is received from the IBasicCommunication source.
/// The event merely contains the text, not an EventArgs type class.
/// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> LineReceived;
/// <summary>
/// The communication port that this gathers on
/// </summary>
public ICommunicationReceiver Port { get; private set; }
/// <summary>
/// Default false. If true, the delimiter will be included in the line output
/// events
/// </summary>
public bool IncludeDelimiter { get; set; }
/// <summary>
/// For receive buffer
/// </summary>
StringBuilder ReceiveBuffer = new StringBuilder();
/// <summary>
/// Delimiter, like it says!
/// </summary>
char Delimiter;
string[] StringDelimiters;
/// <summary>
/// Constructor for using a char delimiter
/// </summary>
/// <param name="port"></param>
/// <param name="delimiter"></param>
public CommunicationGather(ICommunicationReceiver port, char delimiter)
{
Port = port;
Delimiter = delimiter;
port.TextReceived += new EventHandler<GenericCommMethodReceiveTextArgs>(Port_TextReceived);
}
/// <summary>
/// Constructor for using a single string delimiter
/// </summary>
/// <param name="port"></param>
/// <param name="delimiter"></param>
public CommunicationGather(ICommunicationReceiver port, string delimiter)
:this(port, new string[] { delimiter} )
{
}
/// <summary>
/// Constructor for using an array of string delimiters
/// </summary>
/// <param name="port"></param>
/// <param name="delimiters"></param>
public CommunicationGather(ICommunicationReceiver port, string[] delimiters)
{
Port = port;
StringDelimiters = delimiters;
port.TextReceived += Port_TextReceivedStringDelimiter;
}
/// <summary>
/// Disconnects this gather from the Port's TextReceived event. This will not fire LineReceived
/// after the this call.
/// </summary>
public void Stop()
{
Port.TextReceived -= Port_TextReceived;
Port.TextReceived -= Port_TextReceivedStringDelimiter;
}
/// <summary>
/// Handler for raw data coming from port
/// </summary>
void Port_TextReceived(object sender, GenericCommMethodReceiveTextArgs args)
{
var handler = LineReceived;
if (handler != null)
{
ReceiveBuffer.Append(args.Text);
var str = ReceiveBuffer.ToString();
var lines = str.Split(Delimiter);
if (lines.Length > 0)
{
for (int i = 0; i < lines.Length - 1; i++)
{
string strToSend = null;
if (IncludeDelimiter)
strToSend = lines[i] + Delimiter;
else
strToSend = lines[i];
handler(this, new GenericCommMethodReceiveTextArgs(strToSend));
}
ReceiveBuffer = new StringBuilder(lines[lines.Length - 1]);
}
}
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
void Port_TextReceivedStringDelimiter(object sender, GenericCommMethodReceiveTextArgs args)
{
var handler = LineReceived;
if (handler != null)
{
// Receive buffer should either be empty or not contain the delimiter
// If the line does not have a delimiter, append the
ReceiveBuffer.Append(args.Text);
var str = ReceiveBuffer.ToString();
// Case: Receiving DEVICE get version\x0d\0x0a+OK "value":"1234"\x0d\x0a
// RX: DEV
// Split: (1) "DEV"
// RX: I
// Split: (1) "DEVI"
// RX: CE get version
// Split: (1) "DEVICE get version"
// RX: \x0d\x0a+OK "value":"1234"\x0d\x0a
// Split: (2) DEVICE get version, +OK "value":"1234"
// Iterate the delimiters and fire an event for any matching delimiter
foreach (var delimiter in StringDelimiters)
{
var lines = Regex.Split(str, delimiter);
if (lines.Length == 1)
continue;
for (int i = 0; i < lines.Length - 1; i++)
{
string strToSend = null;
if (IncludeDelimiter)
strToSend = lines[i] + delimiter;
else
strToSend = lines[i];
handler(this, new GenericCommMethodReceiveTextArgs(strToSend, delimiter));
}
ReceiveBuffer = new StringBuilder(lines[lines.Length - 1]);
}
}
}
/// <summary>
/// Deconstructor. Disconnects from port TextReceived events.
/// </summary>
~CommunicationGather()
{
Stop();
}
}
}

View File

@@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using PepperDash.Core;
namespace PepperDash.Core
{
/// <summary>
/// Controls the ability to disable/enable debugging of TX/RX data sent to/from a device with a built in timer to disable
/// </summary>
public class CommunicationStreamDebugging
{
/// <summary>
/// Device Key that this instance configures
/// </summary>
public string ParentDeviceKey { get; private set; }
/// <summary>
/// Timer to disable automatically if not manually disabled
/// </summary>
private CTimer DebugExpiryPeriod;
/// <summary>
/// The current debug setting
/// </summary>
public eStreamDebuggingSetting DebugSetting { get; private set; }
private uint _DebugTimeoutInMs;
private const uint _DefaultDebugTimeoutMin = 30;
/// <summary>
/// Timeout in Minutes
/// </summary>
public uint DebugTimeoutMinutes
{
get
{
return _DebugTimeoutInMs/60000;
}
}
/// <summary>
/// Indicates that receive stream debugging is enabled
/// </summary>
public bool RxStreamDebuggingIsEnabled{ get; private set; }
/// <summary>
/// Indicates that transmit stream debugging is enabled
/// </summary>
public bool TxStreamDebuggingIsEnabled { get; private set; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="parentDeviceKey"></param>
public CommunicationStreamDebugging(string parentDeviceKey)
{
ParentDeviceKey = parentDeviceKey;
}
/// <summary>
/// Sets the debugging setting and if not setting to off, assumes the default of 30 mintues
/// </summary>
/// <param name="setting"></param>
public void SetDebuggingWithDefaultTimeout(eStreamDebuggingSetting setting)
{
if (setting == eStreamDebuggingSetting.Off)
{
DisableDebugging();
return;
}
SetDebuggingWithSpecificTimeout(setting, _DefaultDebugTimeoutMin);
}
/// <summary>
/// Sets the debugging setting for the specified number of minutes
/// </summary>
/// <param name="setting"></param>
/// <param name="minutes"></param>
public void SetDebuggingWithSpecificTimeout(eStreamDebuggingSetting setting, uint minutes)
{
if (setting == eStreamDebuggingSetting.Off)
{
DisableDebugging();
return;
}
_DebugTimeoutInMs = minutes * 60000;
StopDebugTimer();
DebugExpiryPeriod = new CTimer((o) => DisableDebugging(), _DebugTimeoutInMs);
if ((setting & eStreamDebuggingSetting.Rx) == eStreamDebuggingSetting.Rx)
RxStreamDebuggingIsEnabled = true;
if ((setting & eStreamDebuggingSetting.Tx) == eStreamDebuggingSetting.Tx)
TxStreamDebuggingIsEnabled = true;
Debug.SetDeviceDebugSettings(ParentDeviceKey, setting);
}
/// <summary>
/// Disabled debugging
/// </summary>
private void DisableDebugging()
{
StopDebugTimer();
Debug.SetDeviceDebugSettings(ParentDeviceKey, eStreamDebuggingSetting.Off);
}
private void StopDebugTimer()
{
RxStreamDebuggingIsEnabled = false;
TxStreamDebuggingIsEnabled = false;
if (DebugExpiryPeriod == null)
{
return;
}
DebugExpiryPeriod.Stop();
DebugExpiryPeriod.Dispose();
DebugExpiryPeriod = null;
}
}
/// <summary>
/// The available settings for stream debugging
/// </summary>
[Flags]
public enum eStreamDebuggingSetting
{
/// <summary>
/// Debug off
/// </summary>
Off = 0,
/// <summary>
/// Debug received data
/// </summary>
Rx = 1,
/// <summary>
/// Debug transmitted data
/// </summary>
Tx = 2,
/// <summary>
/// Debug both received and transmitted data
/// </summary>
Both = Rx | Tx
}
/// <summary>
/// The available settings for stream debugging response types
/// </summary>
[Flags]
public enum eStreamDebuggingDataTypeSettings
{
/// <summary>
/// Debug data in byte format
/// </summary>
Bytes = 0,
/// <summary>
/// Debug data in text format
/// </summary>
Text = 1,
/// <summary>
/// Debug data in both byte and text formats
/// </summary>
Both = Bytes | Text,
}
}

View File

@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Newtonsoft.Json;
namespace PepperDash.Core
{
/// <summary>
/// Config properties that indicate how to communicate with a device for control
/// </summary>
public class ControlPropertiesConfig
{
/// <summary>
/// The method of control
/// </summary>
public eControlMethod Method { get; set; }
/// <summary>
/// The key of the device that contains the control port
/// </summary>
public string ControlPortDevKey { get; set; }
/// <summary>
/// The number of the control port on the device specified by ControlPortDevKey
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)] // In case "null" is present in config on this value
public uint ControlPortNumber { get; set; }
/// <summary>
/// The name of the control port on the device specified by ControlPortDevKey
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)] // In case "null" is present in config on this value
public string ControlPortName { get; set; }
/// <summary>
/// Properties for ethernet based communications
/// </summary>
public TcpSshPropertiesConfig TcpSshProperties { get; set; }
/// <summary>
/// The filename and path for the IR file
/// </summary>
public string IrFile { get; set; }
/// <summary>
/// The IpId of a Crestron device
/// </summary>
public string IpId { get; set; }
/// <summary>
/// Readonly uint representation of the IpId
/// </summary>
[JsonIgnore]
public uint IpIdInt { get { return Convert.ToUInt32(IpId, 16); } }
/// <summary>
/// Char indicating end of line
/// </summary>
public char EndOfLineChar { get; set; }
/// <summary>
/// Defaults to Environment.NewLine;
/// </summary>
public string EndOfLineString { get; set; }
/// <summary>
/// Indicates
/// </summary>
public string DeviceReadyResponsePattern { get; set; }
/// <summary>
/// Used when communcating to programs running in VC-4
/// </summary>
public string RoomId { get; set; }
/// <summary>
/// Constructor
/// </summary>
public ControlPropertiesConfig()
{
EndOfLineString = CrestronEnvironment.NewLine;
}
}
}

View File

@@ -0,0 +1,684 @@
/*PepperDash Technology Corp.
JAG
Copyright: 2017
------------------------------------
***Notice of Ownership and Copyright***
The material in which this notice appears is the property of PepperDash Technology Corporation,
which claims copyright under the laws of the United States of America in the entire body of material
and in all parts thereof, regardless of the use to which it is being put. Any use, in whole or in part,
of this material by another party without the express written permission of PepperDash Technology Corporation is prohibited.
PepperDash Technology Corporation reserves all rights under applicable laws.
------------------------------------ */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core;
namespace DynamicTCP
{
public class DynamicTCPServer : Device
{
#region Events
/// <summary>
/// Event for Receiving text
/// </summary>
public event EventHandler<CopyCoreForSimplpGenericCommMethodReceiveTextArgs> TextReceived;
/// <summary>
/// Event for client connection socket status change
/// </summary>
public event EventHandler<DynamicTCPSocketStatusChangeEventArgs> ClientConnectionChange;
/// <summary>
/// Event for Server State Change
/// </summary>
public event EventHandler<DynamicTCPServerStateChangedEventArgs> ServerStateChange;
#endregion
#region Properties/Variables
/// <summary>
/// Secure or unsecure TCP server. Defaults to Unsecure or standard TCP server without SSL
/// </summary>
public bool Secure { get; set; }
/// <summary>
/// S+ Helper for Secure bool. Parameter in SIMPL+ so there is no get, one way set from simpl+ Param to property in func main of SIMPL+
/// </summary>
public ushort USecure
{
set
{
if (value == 1)
Secure = true;
else if (value == 0)
Secure = false;
}
}
/// <summary>
/// Text representation of the Socket Status enum values for the server
/// </summary>
public string Status
{
get
{
if (Secure ? SecureServer != null : UnsecureServer != null)
return Secure ? SecureServer.State.ToString() : UnsecureServer.State.ToString();
else
return "";
}
}
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsConnected
{
get
{
return (Secure ? SecureServer != null : UnsecureServer != null) &&
(Secure ? SecureServer.State == ServerState.SERVER_CONNECTED : UnsecureServer.State == ServerState.SERVER_CONNECTED);
}
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsListening
{
get { return (Secure ? SecureServer != null : UnsecureServer != null) &&
(Secure ? SecureServer.State == ServerState.SERVER_LISTENING : UnsecureServer.State == ServerState.SERVER_LISTENING); }
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsListening
{
get { return (ushort)(IsListening ? 1 : 0); }
}
public ushort MaxClients { get; set; } // should be set by parameter in SIMPL+ in the MAIN method, Should not ever need to be configurable
/// <summary>
/// Number of clients currently connected.
/// </summary>
public ushort NumberOfClientsConnected
{
get
{
if (Secure ? SecureServer != null : UnsecureServer != null)
return Secure ? (ushort)SecureServer.NumberOfClientsConnected : (ushort)UnsecureServer.NumberOfClientsConnected;
return 0;
}
}
/// <summary>
/// Port Server should listen on
/// </summary>
public int Port { get; set; }
/// <summary>
/// S+ helper for Port
/// </summary>
public ushort UPort
{
get { return Convert.ToUInt16(Port); }
set { Port = Convert.ToInt32(value); }
}
/// <summary>
/// Bool to show whether the server requires a preshared key. Must be set the same in the client, and if true shared keys must be identical on server/client
/// </summary>
public bool SharedKeyRequired { get; set; }
/// <summary>
/// S+ helper for requires shared key bool
/// </summary>
public ushort USharedKeyRequired
{
set
{
if (value == 1)
SharedKeyRequired = true;
else
SharedKeyRequired = false;
}
}
/// <summary>
/// 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.
/// If SharedKey changes while server is listening or clients are connected, disconnect and stop listening will be called
/// </summary>
public string SharedKey { get; set; }
/// <summary>
/// Heartbeat Required bool sets whether server disconnects client if heartbeat is not received
/// </summary>
public bool HeartbeatRequired { get; set; }
/// <summary>
/// S+ Helper for Heartbeat Required
/// </summary>
public ushort UHeartbeatRequired
{
set
{
if (value == 1)
HeartbeatRequired = true;
else
HeartbeatRequired = false;
}
}
/// <summary>
/// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+
/// </summary>
public int HeartbeatRequiredIntervalMs { get; set; }
/// <summary>
/// Simpl+ Heartbeat Analog value in seconds
/// </summary>
public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatRequiredIntervalMs = (value * 1000); } }
/// <summary>
/// String to Match for heartbeat. If null or empty any string will reset heartbeat timer
/// </summary>
public string HeartbeatStringToMatch { get; set; }
//private timers for Heartbeats per client
Dictionary<uint, CTimer> HeartbeatTimerDictionary = new Dictionary<uint, CTimer>();
//flags to show the secure server is waiting for client at index to send the shared key
List<uint> WaitingForSharedKey = new List<uint>();
//Store the connected client indexes
List<uint> ConnectedClientsIndexes = new List<uint>();
/// <summary>
/// Defaults to 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Private flag to note that the server has stopped intentionally
/// </summary>
private bool ServerStopped { get; set; }
//Servers
SecureTCPServer SecureServer;
TCPServer UnsecureServer;
#endregion
#region Constructors
/// <summary>
/// constructor
/// </summary>
public DynamicTCPServer()
: base("Uninitialized Dynamic TCP Server")
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
BufferSize = 2000;
Secure = false;
}
#endregion
#region Methods - Server Actions
/// <summary>
/// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+
/// </summary>
/// <param name="key"></param>
public void Initialize(string key)
{
Key = key;
}
/// <summary>
/// Start listening on the specified port
/// </summary>
public void Listen()
{
try
{
if (Port < 1 || Port > 65535)
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericSecureTcpClient '{0}': Invalid port", Key);
ErrorLog.Warn(string.Format("GenericSecureTcpClient '{0}': Invalid port", Key));
return;
}
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericSecureTcpClient '{0}': No Shared Key set", Key);
ErrorLog.Warn(string.Format("GenericSecureTcpClient '{0}': No Shared Key set", Key));
return;
}
if (IsListening)
return;
if (Secure)
{
SecureServer = new SecureTCPServer(Port, MaxClients);
SecureServer.SocketStatusChange += new SecureTCPServerSocketStatusChangeEventHandler(SecureServer_SocketStatusChange);
ServerStopped = false;
SecureServer.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback);
onServerStateChange();
Debug.Console(2, "Secure Server Status: {0}, Socket Status: {1}\r\n", SecureServer.State.ToString(), SecureServer.ServerSocketStatus);
}
else
{
UnsecureServer = new TCPServer(Port, MaxClients);
UnsecureServer.SocketStatusChange += new TCPServerSocketStatusChangeEventHandler(UnsecureServer_SocketStatusChange);
ServerStopped = false;
UnsecureServer.WaitForConnectionAsync(IPAddress.Any, UnsecureConnectCallback);
onServerStateChange();
Debug.Console(2, "Unsecure Server Status: {0}, Socket Status: {1}\r\n", UnsecureServer.State.ToString(), UnsecureServer.ServerSocketStatus);
}
}
catch (Exception ex)
{
ErrorLog.Error("Error with Dynamic Server: {0}", ex.ToString());
}
}
/// <summary>
/// Stop Listeneing
/// </summary>
public void StopListening()
{
Debug.Console(2, "Stopping Listener");
if (SecureServer != null)
SecureServer.Stop();
if (UnsecureServer != null)
UnsecureServer.Stop();
ServerStopped = true;
onServerStateChange();
}
/// <summary>
/// Disconnect All Clients
/// </summary>
public void DisconnectAllClients()
{
Debug.Console(2, "Disconnecting All Clients");
if (SecureServer != null)
SecureServer.DisconnectAll();
if (UnsecureServer != null)
UnsecureServer.DisconnectAll();
onConnectionChange();
onServerStateChange(); //State shows both listening and connected
}
/// <summary>
/// Broadcast text from server to all connected clients
/// </summary>
/// <param name="text"></param>
public void BroadcastText(string text)
{
if (ConnectedClientsIndexes.Count > 0)
{
byte[] b = Encoding.GetEncoding(28591).GetBytes(text);
if (Secure)
foreach (uint i in ConnectedClientsIndexes)
SecureServer.SendDataAsync(i, b, b.Length, SecureSendDataAsyncCallback);
else
foreach (uint i in ConnectedClientsIndexes)
UnsecureServer.SendDataAsync(i, b, b.Length, UnsecureSendDataAsyncCallback);
}
}
/// <summary>
/// Not sure this is useful in library, maybe Pro??
/// </summary>
/// <param name="text"></param>
/// <param name="clientIndex"></param>
public void SendTextToClient(string text, uint clientIndex)
{
byte[] b = Encoding.GetEncoding(28591).GetBytes(text);
if (Secure)
SecureServer.SendDataAsync(clientIndex, b, b.Length, SecureSendDataAsyncCallback);
else
UnsecureServer.SendDataAsync(clientIndex, b, b.Length, UnsecureSendDataAsyncCallback);
}
//private method to check heartbeat requirements and start or reset timer
void checkHeartbeat(uint clientIndex, string received)
{
if (HeartbeatRequired)
{
if (!string.IsNullOrEmpty(HeartbeatStringToMatch))
{
if (received == HeartbeatStringToMatch)
{
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs);
else
{
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
}
}
}
else
{
if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs);
else
{
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
}
}
}
}
#endregion
#region Methods - HeartbeatTimer Callback
void HeartbeatTimer_CallbackFunction(object o)
{
uint clientIndex = (uint)o;
string address = Secure ? SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex) :
UnsecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
ErrorLog.Error("Heartbeat not received for Client at IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE", address);
Debug.Console(2, "Heartbeat not received for Client at IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE", address);
SendTextToClient("Heartbeat not received by server, closing connection", clientIndex);
if (Secure)
SecureServer.Disconnect(clientIndex);
else
UnsecureServer.Disconnect(clientIndex);
HeartbeatTimerDictionary.Remove(clientIndex);
}
#endregion
#region Methods - Socket Status Changed Callbacks
/// <summary>
/// Secure Server Socket Status Changed Callback
/// </summary>
/// <param name="mySecureTCPServer"></param>
/// <param name="clientIndex"></param>
/// <param name="serverSocketStatus"></param>
void SecureServer_SocketStatusChange(SecureTCPServer server, uint clientIndex, SocketStatus serverSocketStatus)
{
Debug.Console(2, "Client at {0} ServerSocketStatus {1}",
server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), serverSocketStatus.ToString());
if (server.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
{
if (SharedKeyRequired && !WaitingForSharedKey.Contains(clientIndex))
WaitingForSharedKey.Add(clientIndex);
if (!ConnectedClientsIndexes.Contains(clientIndex))
ConnectedClientsIndexes.Add(clientIndex);
}
else
{
if (ConnectedClientsIndexes.Contains(clientIndex))
ConnectedClientsIndexes.Remove(clientIndex);
if (HeartbeatRequired && HeartbeatTimerDictionary.ContainsKey(clientIndex))
HeartbeatTimerDictionary.Remove(clientIndex);
}
if(SecureServer.ServerSocketStatus.ToString() != Status)
onConnectionChange();
}
/// <summary>
/// TCP Server (Unsecure) Socket Status Change Callback
/// </summary>
/// <param name="mySecureTCPServer"></param>
/// <param name="clientIndex"></param>
/// <param name="serverSocketStatus"></param>
void UnsecureServer_SocketStatusChange(TCPServer server, uint clientIndex, SocketStatus serverSocketStatus)
{
Debug.Console(2, "Client at {0} ServerSocketStatus {1}",
server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), serverSocketStatus.ToString());
if (server.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
{
if (SharedKeyRequired && !WaitingForSharedKey.Contains(clientIndex))
WaitingForSharedKey.Add(clientIndex);
if (!ConnectedClientsIndexes.Contains(clientIndex))
ConnectedClientsIndexes.Add(clientIndex);
}
else
{
if (ConnectedClientsIndexes.Contains(clientIndex))
ConnectedClientsIndexes.Remove(clientIndex);
if (HeartbeatRequired && HeartbeatTimerDictionary.ContainsKey(clientIndex))
HeartbeatTimerDictionary.Remove(clientIndex);
}
if (UnsecureServer.ServerSocketStatus.ToString() != Status)
onConnectionChange();
}
#endregion
#region Methods Connected Callbacks
/// <summary>
/// Secure TCP Client Connected to Secure Server Callback
/// </summary>
/// <param name="mySecureTCPServer"></param>
/// <param name="clientIndex"></param>
void SecureConnectCallback(SecureTCPServer mySecureTCPServer, uint clientIndex)
{
if (mySecureTCPServer.ClientConnected(clientIndex))
{
if (SharedKeyRequired)
{
byte[] b = Encoding.GetEncoding(28591).GetBytes(SharedKey + "\n");
mySecureTCPServer.SendDataAsync(clientIndex, b, b.Length, SecureSendDataAsyncCallback);
Debug.Console(2, "Sent Shared Key to client at {0}", mySecureTCPServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
}
if (HeartbeatRequired)
{
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
}
mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback);
if (mySecureTCPServer.State != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped)
mySecureTCPServer.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback);
}
}
/// <summary>
/// Unsecure TCP Client Connected to Unsecure Server Callback
/// </summary>
/// <param name="myTCPServer"></param>
/// <param name="clientIndex"></param>
void UnsecureConnectCallback(TCPServer myTCPServer, uint clientIndex)
{
if (myTCPServer.ClientConnected(clientIndex))
{
if (SharedKeyRequired)
{
byte[] b = Encoding.GetEncoding(28591).GetBytes(SharedKey + "\n");
myTCPServer.SendDataAsync(clientIndex, b, b.Length, UnsecureSendDataAsyncCallback);
Debug.Console(2, "Sent Shared Key to client at {0}", myTCPServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
}
if (HeartbeatRequired)
{
CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
}
myTCPServer.ReceiveDataAsync(clientIndex, UnsecureReceivedDataAsyncCallback);
if (myTCPServer.State != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped)
myTCPServer.WaitForConnectionAsync(IPAddress.Any, UnsecureConnectCallback);
}
if (myTCPServer.State != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped)
myTCPServer.WaitForConnectionAsync(IPAddress.Any, UnsecureConnectCallback);
}
#endregion
#region Methods - Send/Receive Callbacks
/// <summary>
/// Secure Send Data Async Callback
/// </summary>
/// <param name="mySecureTCPServer"></param>
/// <param name="clientIndex"></param>
/// <param name="numberOfBytesSent"></param>
void SecureSendDataAsyncCallback(SecureTCPServer mySecureTCPServer, uint clientIndex, int numberOfBytesSent)
{
//Seems there is nothing to do here
}
/// <summary>
/// Unsecure Send Data Asyc Callback
/// </summary>
/// <param name="myTCPServer"></param>
/// <param name="clientIndex"></param>
/// <param name="numberOfBytesSent"></param>
void UnsecureSendDataAsyncCallback(TCPServer myTCPServer, uint clientIndex, int numberOfBytesSent)
{
//Seems there is nothing to do here
}
/// <summary>
/// Secure Received Data Async Callback
/// </summary>
/// <param name="mySecureTCPServer"></param>
/// <param name="clientIndex"></param>
/// <param name="numberOfBytesReceived"></param>
void SecureReceivedDataAsyncCallback(SecureTCPServer mySecureTCPServer, uint clientIndex, int numberOfBytesReceived)
{
if (numberOfBytesReceived > 0)
{
string received = "Nothing";
byte[] bytes = mySecureTCPServer.GetIncomingDataBufferForSpecificClient(clientIndex);
received = System.Text.Encoding.GetEncoding(28591).GetString(bytes, 0, numberOfBytesReceived);
if (WaitingForSharedKey.Contains(clientIndex))
{
received = received.Replace("\r", "");
received = received.Replace("\n", "");
if (received != SharedKey)
{
byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting");
Debug.Console(2, "Client at index {0} Shared key did not match the server, disconnecting client", clientIndex);
ErrorLog.Error("Client at index {0} Shared key did not match the server, disconnecting client", clientIndex);
mySecureTCPServer.SendDataAsync(clientIndex, b, b.Length, null);
mySecureTCPServer.Disconnect(clientIndex);
}
if (mySecureTCPServer.NumberOfClientsConnected > 0)
mySecureTCPServer.ReceiveDataAsync(SecureReceivedDataAsyncCallback);
WaitingForSharedKey.Remove(clientIndex);
byte[] skResponse = Encoding.GetEncoding(28591).GetBytes("Shared Key Match, Connected and ready for communication");
mySecureTCPServer.SendDataAsync(clientIndex, skResponse, skResponse.Length, null);
mySecureTCPServer.ReceiveDataAsync(SecureReceivedDataAsyncCallback);
}
else
{
mySecureTCPServer.ReceiveDataAsync(SecureReceivedDataAsyncCallback);
Debug.Console(2, "Secure Server Listening on Port: {0}, client IP: {1}, NumberOfBytesReceived: {2}, Received: {3}\r\n",
mySecureTCPServer.PortNumber, mySecureTCPServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), numberOfBytesReceived, received);
onTextReceived(received);
}
checkHeartbeat(clientIndex, received);
}
if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback);
}
/// <summary>
/// Unsecure Received Data Async Callback
/// </summary>
/// <param name="myTCPServer"></param>
/// <param name="clientIndex"></param>
/// <param name="numberOfBytesReceived"></param>
void UnsecureReceivedDataAsyncCallback(TCPServer myTCPServer, uint clientIndex, int numberOfBytesReceived)
{
if (numberOfBytesReceived > 0)
{
string received = "Nothing";
byte[] bytes = myTCPServer.GetIncomingDataBufferForSpecificClient(clientIndex);
received = System.Text.Encoding.GetEncoding(28591).GetString(bytes, 0, numberOfBytesReceived);
if (WaitingForSharedKey.Contains(clientIndex))
{
received = received.Replace("\r", "");
received = received.Replace("\n", "");
if (received != SharedKey)
{
byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting");
Debug.Console(2, "Client at index {0} Shared key did not match the server, disconnecting client", clientIndex);
ErrorLog.Error("Client at index {0} Shared key did not match the server, disconnecting client", clientIndex);
myTCPServer.SendDataAsync(clientIndex, b, b.Length, null);
myTCPServer.Disconnect(clientIndex);
}
if (myTCPServer.NumberOfClientsConnected > 0)
myTCPServer.ReceiveDataAsync(UnsecureReceivedDataAsyncCallback);
WaitingForSharedKey.Remove(clientIndex);
byte[] skResponse = Encoding.GetEncoding(28591).GetBytes("Shared Key Match, Connected and ready for communication");
myTCPServer.SendDataAsync(clientIndex, skResponse, skResponse.Length, null);
myTCPServer.ReceiveDataAsync(UnsecureReceivedDataAsyncCallback);
}
else
{
myTCPServer.ReceiveDataAsync(UnsecureReceivedDataAsyncCallback);
Debug.Console(2, "Secure Server Listening on Port: {0}, client IP: {1}, NumberOfBytesReceived: {2}, Received: {3}\r\n",
myTCPServer.PortNumber, myTCPServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex), numberOfBytesReceived, received);
onTextReceived(received);
}
checkHeartbeat(clientIndex, received);
}
if (myTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
myTCPServer.ReceiveDataAsync(clientIndex, UnsecureReceivedDataAsyncCallback);
}
#endregion
#region Methods - EventHelpers/Callbacks
//Private Helper method to call the Connection Change Event
void onConnectionChange()
{
var handler = ClientConnectionChange;
if (handler != null)
{
if (Secure)
handler(this, new DynamicTCPSocketStatusChangeEventArgs(SecureServer, Secure));
else
handler(this, new DynamicTCPSocketStatusChangeEventArgs(UnsecureServer, Secure));
}
}
//Private Helper Method to call the Text Received Event
void onTextReceived(string text)
{
var handler = TextReceived;
if (handler != null)
handler(this, new CopyCoreForSimplpGenericCommMethodReceiveTextArgs(text));
}
//Private Helper Method to call the Server State Change Event
void onServerStateChange()
{
var handler = ServerStateChange;
if(handler != null)
{
if(Secure)
handler(this, new DynamicTCPServerStateChangedEventArgs(SecureServer, Secure));
else
handler(this, new DynamicTCPServerStateChangedEventArgs(UnsecureServer, Secure));
}
}
//Private Event Handler method to handle the closing of connections when the program stops
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping)
{
Debug.Console(1, this, "Program stopping. Closing server");
DisconnectAllClients();
StopListening();
}
}
#endregion
}
}

251
src/Comm/EventArgs.cs Normal file
View File

@@ -0,0 +1,251 @@
/*PepperDash Technology Corp.
Copyright: 2017
------------------------------------
***Notice of Ownership and Copyright***
The material in which this notice appears is the property of PepperDash Technology Corporation,
which claims copyright under the laws of the United States of America in the entire body of material
and in all parts thereof, regardless of the use to which it is being put. Any use, in whole or in part,
of this material by another party without the express written permission of PepperDash Technology Corporation is prohibited.
PepperDash Technology Corporation reserves all rights under applicable laws.
------------------------------------ */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
namespace PepperDash.Core
{
/// <summary>
/// Delegate for notifying of socket status changes
/// </summary>
/// <param name="client"></param>
public delegate void GenericSocketStatusChangeEventDelegate(ISocketStatus client);
/// <summary>
/// EventArgs class for socket status changes
/// </summary>
public class GenericSocketStatusChageEventArgs : EventArgs
{
/// <summary>
///
/// </summary>
public ISocketStatus Client { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="client"></param>
public GenericSocketStatusChageEventArgs(ISocketStatus client)
{
Client = client;
}
/// <summary>
/// S+ Constructor
/// </summary>
public GenericSocketStatusChageEventArgs() { }
}
/// <summary>
/// Delegate for notifying of TCP Server state changes
/// </summary>
/// <param name="state"></param>
public delegate void GenericTcpServerStateChangedEventDelegate(ServerState state);
/// <summary>
/// EventArgs class for TCP Server state changes
/// </summary>
public class GenericTcpServerStateChangedEventArgs : EventArgs
{
/// <summary>
///
/// </summary>
public ServerState State { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="state"></param>
public GenericTcpServerStateChangedEventArgs(ServerState state)
{
State = state;
}
/// <summary>
/// S+ Constructor
/// </summary>
public GenericTcpServerStateChangedEventArgs() { }
}
/// <summary>
/// Delegate for TCP Server socket status changes
/// </summary>
/// <param name="socket"></param>
/// <param name="clientIndex"></param>
/// <param name="clientStatus"></param>
public delegate void GenericTcpServerSocketStatusChangeEventDelegate(object socket, uint clientIndex, SocketStatus clientStatus);
/// <summary>
/// EventArgs for TCP server socket status changes
/// </summary>
public class GenericTcpServerSocketStatusChangeEventArgs : EventArgs
{
/// <summary>
///
/// </summary>
public object Socket { get; private set; }
/// <summary>
///
/// </summary>
public uint ReceivedFromClientIndex { get; private set; }
/// <summary>
///
/// </summary>
public SocketStatus ClientStatus { get; set; }
/// <summary>
///
/// </summary>
/// <param name="socket"></param>
/// <param name="clientStatus"></param>
public GenericTcpServerSocketStatusChangeEventArgs(object socket, SocketStatus clientStatus)
{
Socket = socket;
ClientStatus = clientStatus;
}
/// <summary>
///
/// </summary>
/// <param name="socket"></param>
/// <param name="clientIndex"></param>
/// <param name="clientStatus"></param>
public GenericTcpServerSocketStatusChangeEventArgs(object socket, uint clientIndex, SocketStatus clientStatus)
{
Socket = socket;
ReceivedFromClientIndex = clientIndex;
ClientStatus = clientStatus;
}
/// <summary>
/// S+ Constructor
/// </summary>
public GenericTcpServerSocketStatusChangeEventArgs() { }
}
/// <summary>
/// EventArgs for TCP server com method receive text
/// </summary>
public class GenericTcpServerCommMethodReceiveTextArgs : EventArgs
{
/// <summary>
///
/// </summary>
public uint ReceivedFromClientIndex { get; private set; }
/// <summary>
///
/// </summary>
public ushort ReceivedFromClientIndexShort
{
get
{
return (ushort)ReceivedFromClientIndex;
}
}
/// <summary>
///
/// </summary>
public string Text { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="text"></param>
public GenericTcpServerCommMethodReceiveTextArgs(string text)
{
Text = text;
}
/// <summary>
///
/// </summary>
/// <param name="text"></param>
/// <param name="clientIndex"></param>
public GenericTcpServerCommMethodReceiveTextArgs(string text, uint clientIndex)
{
Text = text;
ReceivedFromClientIndex = clientIndex;
}
/// <summary>
/// S+ Constructor
/// </summary>
public GenericTcpServerCommMethodReceiveTextArgs() { }
}
/// <summary>
/// EventArgs for TCP server client ready for communication
/// </summary>
public class GenericTcpServerClientReadyForcommunicationsEventArgs : EventArgs
{
/// <summary>
///
/// </summary>
public bool IsReady;
/// <summary>
///
/// </summary>
/// <param name="isReady"></param>
public GenericTcpServerClientReadyForcommunicationsEventArgs(bool isReady)
{
IsReady = isReady;
}
/// <summary>
/// S+ Constructor
/// </summary>
public GenericTcpServerClientReadyForcommunicationsEventArgs() { }
}
/// <summary>
/// EventArgs for UDP connected
/// </summary>
public class GenericUdpConnectedEventArgs : EventArgs
{
/// <summary>
///
/// </summary>
public ushort UConnected;
/// <summary>
///
/// </summary>
public bool Connected;
/// <summary>
/// Constructor
/// </summary>
public GenericUdpConnectedEventArgs() { }
/// <summary>
///
/// </summary>
/// <param name="uconnected"></param>
public GenericUdpConnectedEventArgs(ushort uconnected)
{
UConnected = uconnected;
}
/// <summary>
///
/// </summary>
/// <param name="connected"></param>
public GenericUdpConnectedEventArgs(bool connected)
{
Connected = connected;
}
}
}

View File

@@ -0,0 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core
{
/// <summary>
/// Background class that manages debug features for sockets
/// </summary>
public static class CommStatic
{
static List<ISocketStatus> Sockets = new List<ISocketStatus>();
/// <summary>
/// Sets up the backing class. Adds console commands for S#Pro programs
/// </summary>
static CommStatic()
{
if (CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro)
{
CrestronConsole.AddNewConsoleCommand(SocketCommand, "socket", "socket commands: list, send, connect, disco",
ConsoleAccessLevelEnum.AccessOperator);
}
}
static void SocketCommand(string s)
{
// 0 1 2
//socket command number/key/all param
//socket list
//socket send 4 ver -v\n
if (string.IsNullOrEmpty(s))
return;
var tokens = s.Split(' ');
if (tokens.Length == 0)
return;
var command = tokens[0].ToLower();
if(command == "connect")
{
}
else if(command == "disco")
{
}
else if(command =="list")
{
CrestronConsole.ConsoleCommandResponse("{0} sockets", Sockets.Count);
if(Sockets.Count == 0)
return;
// get the longest key name, for formatting
var longestLength = Sockets.Aggregate("",
(max, cur) => max.Length > cur.Key.Length ? max : cur.Key).Length;
for(int i = 0; i < Sockets.Count; i++)
{
var sock = Sockets[i];
CrestronConsole.ConsoleCommandResponse("{0} {1} {2} {3}",
i, sock.Key, GetSocketType(sock), sock.ClientStatus);
}
}
else if(command == "send")
{
}
}
/// <summary>
/// Helper for socket list, to show types
/// </summary>
static string GetSocketType(ISocketStatus sock)
{
if (sock is GenericSshClient)
return "SSH";
else if (sock is GenericTcpIpClient)
return "TCP-IP";
else
return "?";
}
/// <summary>
///
/// </summary>
/// <param name="socket"></param>
public static void AddSocket(ISocketStatus socket)
{
if(!Sockets.Contains(socket))
Sockets.Add(socket);
}
/// <summary>
///
/// </summary>
/// <param name="socket"></param>
public static void RemoveSocket(ISocketStatus socket)
{
if (Sockets.Contains(socket))
Sockets.Remove(socket);
}
}
}

View File

@@ -0,0 +1,314 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using Crestron.SimplSharp.Net.Http;
namespace PepperDash.Core
{
/// <summary>
/// Client for communicating with an HTTP Server Side Event pattern
/// </summary>
public class GenericHttpSseClient : ICommunicationReceiver
{
/// <summary>
/// Notifies when bytes have been received
/// </summary>
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Notifies when text has been received
/// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
/// <summary>
/// Indicates connection status
/// </summary>
public bool IsConnected
{
get;
private set;
}
/// <summary>
/// Unique identifier for the instance
/// </summary>
public string Key
{
get;
private set;
}
/// <summary>
/// Name for the instance
/// </summary>
public string Name
{
get;
private set;
}
/// <summary>
/// URL of the server
/// </summary>
public string Url { get; set; }
HttpClient Client;
HttpClientRequest Request;
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="name"></param>
public GenericHttpSseClient(string key, string name)
{
Key = key;
Name = name;
}
/// <summary>
/// Connects to the server. Requires Url to be set first.
/// </summary>
public void Connect()
{
InitiateConnection(Url);
}
/// <summary>
/// Disconnects from the server
/// </summary>
public void Disconnect()
{
CloseConnection(null);
}
/// <summary>
/// Initiates connection to the server
/// </summary>
/// <param name="url"></param>
public void InitiateConnection(string url)
{
CrestronInvoke.BeginInvoke(o =>
{
try
{
if(string.IsNullOrEmpty(url))
{
Debug.Console(0, this, "Error connecting to Server. No URL specified");
return;
}
Client = new HttpClient();
Request = new HttpClientRequest();
Client.Verbose = true;
Client.KeepAlive = true;
Request.Url.Parse(url);
Request.RequestType = RequestType.Get;
Request.Header.SetHeaderValue("Accept", "text/event-stream");
// In order to get a handle on the response stream, we have to get
// the request stream first. Boo
Client.BeginGetRequestStream(GetRequestStreamCallback, Request, null);
CrestronConsole.PrintLine("Request made!");
}
catch (Exception e)
{
ErrorLog.Notice("Exception occured in AsyncWebPostHttps(): " + e.ToString());
}
});
}
/// <summary>
/// Closes the connection to the server
/// </summary>
/// <param name="s"></param>
public void CloseConnection(string s)
{
if (Client != null)
{
Client.Abort();
IsConnected = false;
Debug.Console(1, this, "Client Disconnected");
}
}
private void GetRequestStreamCallback(HttpClientRequest request, HTTP_CALLBACK_ERROR error, object status)
{
try
{
// End the the async request operation and return the data stream
Stream requestStream = request.ThisClient.EndGetRequestStream(request, null);
// If this were something other than a GET we could write to the stream here
// Closing makes the request happen
requestStream.Close();
// Get a handle on the response stream.
request.ThisClient.BeginGetResponseStream(GetResponseStreamCallback, request, status);
}
catch (Exception e)
{
ErrorLog.Notice("Exception occured in GetSecureRequestStreamCallback(): " + e.ToString());
}
}
/// <summary>
///
/// </summary>
/// <param name="request"></param>
/// <param name="error"></param>
/// <param name="status"></param>
private void GetResponseStreamCallback(HttpClientRequest request, HTTP_CALLBACK_ERROR error, object status)
{
try
{
// This closes up the GetResponseStream async
var response = request.ThisClient.EndGetResponseStream(request);
response.DataConnection.OnBytesReceived += new EventHandler(DataConnection_OnBytesReceived);
IsConnected = true;
Debug.Console(1, this, "Client Disconnected");
Stream streamResponse = response.ContentStream;
// Object containing various states to be passed back to async callback below
RequestState asyncState = new RequestState();
asyncState.Request = request;
asyncState.Response = response;
asyncState.StreamResponse = streamResponse;
asyncState.HttpClient = request.ThisClient;
// This processes the ongoing data stream
Crestron.SimplSharp.CrestronIO.IAsyncResult asyncResult = null;
do
{
asyncResult = streamResponse.BeginRead(asyncState.BufferRead, 0, RequestState.BUFFER_SIZE,
new Crestron.SimplSharp.CrestronIO.AsyncCallback(ReadCallBack), asyncState);
}
while (asyncResult.CompletedSynchronously && !asyncState.Done);
//Console.WriteLine("\r\nExit Response Callback\r\n");
}
catch (Exception e)
{
ErrorLog.Notice("Exception occured in GetSecureRequestStreamCallback(): " + e.ToString());
}
}
void DataConnection_OnBytesReceived(object sender, EventArgs e)
{
Debug.Console(1, this, "DataConnection OnBytesReceived Fired");
}
private void ReadCallBack(Crestron.SimplSharp.CrestronIO.IAsyncResult asyncResult)
{
//we are getting back everything here, so cast the state from the call
RequestState requestState = asyncResult.AsyncState as RequestState;
Stream responseStream = requestState.StreamResponse;
int read = responseStream.EndRead(asyncResult);
// Read the HTML page and then print it to the console.
if (read > 0)
{
var bytes = requestState.BufferRead;
var bytesHandler = BytesReceived;
if (bytesHandler != null)
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
var textHandler = TextReceived;
if (textHandler != null)
{
var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
textHandler(this, new GenericCommMethodReceiveTextArgs(str));
}
//requestState.RequestData.Append(Encoding.ASCII.GetString(requestState.BufferRead, 0, read));
//CrestronConsole.PrintLine(requestState.RequestData.ToString());
//clear the byte array buffer used.
Array.Clear(requestState.BufferRead, 0, requestState.BufferRead.Length);
if (asyncResult.CompletedSynchronously)
{
return;
}
Crestron.SimplSharp.CrestronIO.IAsyncResult asynchronousResult;
do
{
asynchronousResult = responseStream.BeginRead(requestState.BufferRead, 0, RequestState.BUFFER_SIZE,
new Crestron.SimplSharp.CrestronIO.AsyncCallback(ReadCallBack), requestState);
}
while (asynchronousResult.CompletedSynchronously && !requestState.Done);
}
else
{
requestState.Done = true;
}
}
}
/// <summary>
/// Stores the state of the request
/// </summary>
public class RequestState
{
/// <summary>
///
/// </summary>
public const int BUFFER_SIZE = 10000;
/// <summary>
///
/// </summary>
public byte[] BufferRead;
/// <summary>
///
/// </summary>
public HttpClient HttpClient;
/// <summary>
///
/// </summary>
public HttpClientRequest Request;
/// <summary>
///
/// </summary>
public HttpClientResponse Response;
/// <summary>
///
/// </summary>
public Stream StreamResponse;
/// <summary>
///
/// </summary>
public bool Done;
/// <summary>
/// Constructor
/// </summary>
public RequestState()
{
BufferRead = new byte[BUFFER_SIZE];
HttpClient = null;
Request = null;
Response = null;
StreamResponse = null;
Done = false;
}
}
/// <summary>
/// Waithandle for main thread.
/// </summary>
public class StreamAsyncTest
{
/// <summary>
///
/// </summary>
public CEvent wait_for_response = new CEvent(true, false);
}
}

View File

@@ -0,0 +1,954 @@
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
{
/// <summary>
/// A class to handle secure TCP/IP communications with a server
/// </summary>
public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
{
private const string SplusKey = "Uninitialized Secure Tcp _client";
/// <summary>
/// Stream debugging
/// </summary>
public CommunicationStreamDebugging StreamDebugging { get; private set; }
/// <summary>
/// Fires when data is received from the server and returns it as a Byte array
/// </summary>
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Fires when data is received from the server and returns it as text
/// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
#region GenericSecureTcpIpClient Events & Delegates
/// <summary>
///
/// </summary>
//public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
/// <summary>
/// Auto reconnect evant handler
/// </summary>
public event EventHandler AutoReconnectTriggered;
/// <summary>
/// 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.
/// </summary>
public event EventHandler<GenericTcpServerCommMethodReceiveTextArgs> TextReceivedQueueInvoke;
/// <summary>
/// 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.
/// </summary>
public event EventHandler<GenericTcpServerClientReadyForcommunicationsEventArgs> ClientReadyForCommunications;
#endregion
#region GenricTcpIpClient properties
private string _hostname;
/// <summary>
/// Address of server
/// </summary>
public string Hostname
{
get { return _hostname; }
set
{
_hostname = value;
if (_client != null)
{
_client.AddressClientConnectedTo = _hostname;
}
}
}
/// <summary>
/// Port on server
/// </summary>
public int Port { get; set; }
/// <summary>
/// S+ helper
/// </summary>
public ushort UPort
{
get { return Convert.ToUInt16(Port); }
set { Port = Convert.ToInt32(value); }
}
/// <summary>
/// Defaults to 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Internal secure client
/// </summary>
private SecureTCPClient _client;
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsConnected
{
get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
/// _client socket status Read only
/// </summary>
public SocketStatus ClientStatus
{
get
{
return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus;
}
}
/// <summary>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
/// and IsConnected would be true when this == 2.
/// </summary>
public ushort UStatus
{
get { return (ushort)ClientStatus; }
}
/// <summary>
/// Status text shows the message associated with socket status
/// </summary>
public string ClientStatusText { get { return ClientStatus.ToString(); } }
/// <summary>
/// Connection failure reason
/// </summary>
public string ConnectionFailure { get { return ClientStatus.ToString(); } }
/// <summary>
/// bool to track if auto reconnect should be set on the socket
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// S+ helper for AutoReconnect
/// </summary>
public ushort UAutoReconnect
{
get { return (ushort)(AutoReconnect ? 1 : 0); }
set { AutoReconnect = value == 1; }
}
/// <summary>
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// Flag Set only when the disconnect method is called.
/// </summary>
bool DisconnectCalledByUser;
/// <summary>
///
/// </summary>
public bool Connected
{
get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
}
// private Timer for auto reconnect
private CTimer RetryTimer;
#endregion
#region GenericSecureTcpIpClient properties
/// <summary>
/// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class
/// </summary>
public bool SharedKeyRequired { get; set; }
/// <summary>
/// S+ helper for requires shared key bool
/// </summary>
public ushort USharedKeyRequired
{
set
{
if (value == 1)
SharedKeyRequired = true;
else
SharedKeyRequired = false;
}
}
/// <summary>
/// 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
/// </summary>
public string SharedKey { get; set; }
/// <summary>
/// flag to show the client is waiting for the server to send the shared key
/// </summary>
private bool WaitingForSharedKeyResponse { get; set; }
/// <summary>
/// Semaphore on connect method
/// </summary>
bool IsTryingToConnect;
/// <summary>
/// Bool showing if socket is ready for communication after shared key exchange
/// </summary>
public bool IsReadyForCommunication { get; set; }
/// <summary>
/// S+ helper for IsReadyForCommunication
/// </summary>
public ushort UIsReadyForCommunication
{
get { return (ushort)(IsReadyForCommunication ? 1 : 0); }
}
/// <summary>
/// Bool Heartbeat Enabled flag
/// </summary>
public bool HeartbeatEnabled { get; set; }
/// <summary>
/// S+ helper for Heartbeat Enabled
/// </summary>
public ushort UHeartbeatEnabled
{
get { return (ushort)(HeartbeatEnabled ? 1 : 0); }
set { HeartbeatEnabled = value == 1; }
}
/// <summary>
/// Heartbeat String
/// </summary>
public string HeartbeatString { get; set; }
//public int HeartbeatInterval = 50000;
/// <summary>
/// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+
/// </summary>
public int HeartbeatInterval { get; set; }
/// <summary>
/// Simpl+ Heartbeat Analog value in seconds
/// </summary>
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;
/// <summary>
/// Queue lock
/// </summary>
CCriticalSection DequeueLock = new CCriticalSection();
/// <summary>
/// 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.
/// </summary>
public int ReceiveQueueSize { get; set; }
/// <summary>
/// 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.
/// </summary>
private CrestronQueue<GenericTcpServerCommMethodReceiveTextArgs> MessageQueue;
#endregion
#region Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="bufferSize"></param>
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);
}
/// <summary>
/// Contstructor that sets all properties by calling the initialize method with a config object.
/// </summary>
/// <param name="key"></param>
/// <param name="clientConfigObject"></param>
public GenericSecureTcpIpClient(string key, TcpClientConfigObject clientConfigObject)
: base(key)
{
StreamDebugging = new CommunicationStreamDebugging(key);
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
BufferSize = 2000;
Initialize(clientConfigObject);
}
/// <summary>
/// Default constructor for S+
/// </summary>
public GenericSecureTcpIpClient()
: base(SplusKey)
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
BufferSize = 2000;
}
/// <summary>
/// Just to help S+ set the key
/// </summary>
public void Initialize(string key)
{
Key = key;
}
/// <summary>
/// Initialize called by the constructor that accepts a client config object. Can be called later to reset properties of client.
/// </summary>
/// <param name="config"></param>
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<GenericTcpServerCommMethodReceiveTextArgs>(ReceiveQueueSize);
}
catch (Exception ex)
{
Debug.Console(0, this, "Exception initializing client with key: {0}\rException: {1}", Key, ex);
}
}
#endregion
/// <summary>
/// Handles closing this up when the program shuts down
/// </summary>
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();
}
}
/// <summary>
/// Deactivate the client
/// </summary>
/// <returns></returns>
public override bool Deactivate()
{
if (_client != null)
{
_client.SocketStatusChange -= this.Client_SocketStatusChange;
DisconnectClient();
}
return true;
}
/// <summary>
/// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name.
/// </summary>
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();
}
}
/// <summary>
///
/// </summary>
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");
}
}
/// <summary>
/// Does the actual disconnect business
/// </summary>
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;
}
#region Methods
/// <summary>
/// Called from Connect failure or Socket Status change if
/// auto reconnect and socket disconnected (Not disconnected by user)
/// </summary>
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);
}
}
/// <summary>
/// Receive callback
/// </summary>
/// <param name="client"></param>
/// <param name="numBytes"></param>
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();
}
}
/// <summary>
/// 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.
/// </summary>
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);
}
}
/// <summary>
///
/// </summary>
void StopWaitForSharedKeyTimer()
{
if (WaitForSharedKey != null)
{
WaitForSharedKey.Stop();
WaitForSharedKey = null;
}
}
/// <summary>
/// General send method
/// </summary>
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);
}
}
}
/// <summary>
///
/// </summary>
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);
}
}
}
/// <summary>
/// SocketStatusChange Callback
/// </summary>
/// <param name="client"></param>
/// <param name="clientSocketStatus"></param>
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);
}
}
/// <summary>
/// Helper for ConnectionChange event
/// </summary>
void OnConnectionChange()
{
var handler = ConnectionChange;
if (handler == null) return;
handler(this, new GenericSocketStatusChageEventArgs(this));
}
/// <summary>
/// Helper to fire ClientReadyForCommunications event
/// </summary>
void OnClientReadyForcommunications(bool isReady)
{
IsReadyForCommunication = isReady;
if (IsReadyForCommunication)
HeartbeatStart();
var handler = ClientReadyForCommunications;
if (handler == null) return;
handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
}
#endregion
}
}

View File

@@ -0,0 +1,908 @@
/*PepperDash Technology Corp.
JAG
Copyright: 2017
------------------------------------
***Notice of Ownership and Copyright***
The material in which this notice appears is the property of PepperDash Technology Corporation,
which claims copyright under the laws of the United States of America in the entire body of material
and in all parts thereof, regardless of the use to which it is being put. Any use, in whole or in part,
of this material by another party without the express written permission of PepperDash Technology Corporation is prohibited.
PepperDash Technology Corporation reserves all rights under applicable laws.
------------------------------------ */
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
{
/// <summary>
/// Generic secure TCP/IP client for server
/// </summary>
public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
{
/// <summary>
/// Band aid delegate for choked server
/// </summary>
internal delegate void ConnectionHasHungCallbackDelegate();
#region Events
//public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Notifies of text received
/// </summary>
public event EventHandler<GenericTcpServerCommMethodReceiveTextArgs> TextReceived;
/// <summary>
/// Notifies of auto reconnect sequence triggered
/// </summary>
public event EventHandler AutoReconnectTriggered;
/// <summary>
/// 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.
/// </summary>
public event EventHandler<GenericTcpServerCommMethodReceiveTextArgs> TextReceivedQueueInvoke;
/// <summary>
/// Notifies of socket status change
/// </summary>
public event EventHandler<GenericTcpServerSocketStatusChangeEventArgs> ConnectionChange;
/// <summary>
/// This is something of a band-aid callback. If the client times out during the connection process, because the server
/// is stuck, this will fire. It is intended to be used by the Server class monitor client, to help
/// keep a watch on the server and reset it if necessary.
/// </summary>
internal ConnectionHasHungCallbackDelegate ConnectionHasHungCallback;
/// <summary>
/// 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.
/// </summary>
public event EventHandler<GenericTcpServerClientReadyForcommunicationsEventArgs> ClientReadyForCommunications;
#endregion
#region Properties & Variables
/// <summary>
/// Address of server
/// </summary>
public string Hostname { get; set; }
/// <summary>
/// Port on server
/// </summary>
public int Port { get; set; }
/// <summary>
/// S+ helper
/// </summary>
public ushort UPort
{
get { return Convert.ToUInt16(Port); }
set { Port = Convert.ToInt32(value); }
}
/// <summary>
/// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class
/// </summary>
public bool SharedKeyRequired { get; set; }
/// <summary>
/// S+ helper for requires shared key bool
/// </summary>
public ushort USharedKeyRequired
{
set
{
if (value == 1)
SharedKeyRequired = true;
else
SharedKeyRequired = false;
}
}
/// <summary>
/// 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
/// </summary>
public string SharedKey { get; set; }
/// <summary>
/// flag to show the client is waiting for the server to send the shared key
/// </summary>
private bool WaitingForSharedKeyResponse { get; set; }
/// <summary>
/// Defaults to 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Semaphore on connect method
/// </summary>
bool IsTryingToConnect;
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsConnected
{
get
{
if (Client != null)
return Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED;
else
return false;
}
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
/// Bool showing if socket is ready for communication after shared key exchange
/// </summary>
public bool IsReadyForCommunication { get; set; }
/// <summary>
/// S+ helper for IsReadyForCommunication
/// </summary>
public ushort UIsReadyForCommunication
{
get { return (ushort)(IsReadyForCommunication ? 1 : 0); }
}
/// <summary>
/// Client socket status Read only
/// </summary>
public SocketStatus ClientStatus
{
get
{
if (Client != null)
return Client.ClientStatus;
else
return SocketStatus.SOCKET_STATUS_NO_CONNECT;
}
}
/// <summary>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
/// and IsConnected would be true when this == 2.
/// </summary>
public ushort UStatus
{
get { return (ushort)ClientStatus; }
}
/// <summary>
/// Status text shows the message associated with socket status
/// </summary>
public string ClientStatusText { get { return ClientStatus.ToString(); } }
/// <summary>
/// bool to track if auto reconnect should be set on the socket
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// S+ helper for AutoReconnect
/// </summary>
public ushort UAutoReconnect
{
get { return (ushort)(AutoReconnect ? 1 : 0); }
set { AutoReconnect = value == 1; }
}
/// <summary>
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// Flag Set only when the disconnect method is called.
/// </summary>
bool DisconnectCalledByUser;
/// <summary>
/// private Timer for auto reconnect
/// </summary>
CTimer RetryTimer;
/// <summary>
///
/// </summary>
public bool HeartbeatEnabled { get; set; }
/// <summary>
///
/// </summary>
public ushort UHeartbeatEnabled
{
get { return (ushort)(HeartbeatEnabled ? 1 : 0); }
set { HeartbeatEnabled = value == 1; }
}
/// <summary>
///
/// </summary>
public string HeartbeatString { get; set; }
//public int HeartbeatInterval = 50000;
/// <summary>
/// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+
/// </summary>
public int HeartbeatInterval { get; set; }
/// <summary>
/// Simpl+ Heartbeat Analog value in seconds
/// </summary>
public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } }
CTimer HeartbeatSendTimer;
CTimer HeartbeatAckTimer;
/// <summary>
/// Used to force disconnection on a dead connect attempt
/// </summary>
CTimer ConnectFailTimer;
CTimer WaitForSharedKey;
private int ConnectionCount;
/// <summary>
/// Internal secure client
/// </summary>
SecureTCPClient Client;
bool ProgramIsStopping;
/// <summary>
/// Queue lock
/// </summary>
CCriticalSection DequeueLock = new CCriticalSection();
/// <summary>
/// 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.
/// </summary>
public int ReceiveQueueSize { get; set; }
/// <summary>
/// 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.
/// </summary>
private CrestronQueue<GenericTcpServerCommMethodReceiveTextArgs> MessageQueue;
#endregion
#region Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="bufferSize"></param>
public GenericSecureTcpIpClient_ForServer(string key, string address, int port, int bufferSize)
: base(key)
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
Hostname = address;
Port = port;
BufferSize = bufferSize;
AutoReconnectIntervalMs = 5000;
}
/// <summary>
/// Constructor for S+
/// </summary>
public GenericSecureTcpIpClient_ForServer()
: base("Uninitialized Secure Tcp Client For Server")
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
BufferSize = 2000;
}
/// <summary>
/// Contstructor that sets all properties by calling the initialize method with a config object.
/// </summary>
/// <param name="key"></param>
/// <param name="clientConfigObject"></param>
public GenericSecureTcpIpClient_ForServer(string key, TcpClientConfigObject clientConfigObject)
: base(key)
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
Initialize(clientConfigObject);
}
#endregion
#region Methods
/// <summary>
/// Just to help S+ set the key
/// </summary>
public void Initialize(string key)
{
Key = key;
}
/// <summary>
/// Initialize called by the constructor that accepts a client config object. Can be called later to reset properties of client.
/// </summary>
/// <param name="clientConfigObject"></param>
public void Initialize(TcpClientConfigObject clientConfigObject)
{
try
{
if (clientConfigObject != null)
{
var TcpSshProperties = clientConfigObject.Control.TcpSshProperties;
Hostname = TcpSshProperties.Address;
AutoReconnect = TcpSshProperties.AutoReconnect;
AutoReconnectIntervalMs = TcpSshProperties.AutoReconnectIntervalMs > 1000 ?
TcpSshProperties.AutoReconnectIntervalMs : 5000;
SharedKey = clientConfigObject.SharedKey;
SharedKeyRequired = clientConfigObject.SharedKeyRequired;
HeartbeatEnabled = clientConfigObject.HeartbeatRequired;
HeartbeatRequiredIntervalInSeconds = clientConfigObject.HeartbeatRequiredIntervalInSeconds > 0 ?
clientConfigObject.HeartbeatRequiredIntervalInSeconds : (ushort)15;
HeartbeatString = string.IsNullOrEmpty(clientConfigObject.HeartbeatStringToMatch) ? "heartbeat" : clientConfigObject.HeartbeatStringToMatch;
Port = TcpSshProperties.Port;
BufferSize = TcpSshProperties.BufferSize > 2000 ? TcpSshProperties.BufferSize : 2000;
ReceiveQueueSize = clientConfigObject.ReceiveQueueSize > 20 ? clientConfigObject.ReceiveQueueSize : 20;
MessageQueue = new CrestronQueue<GenericTcpServerCommMethodReceiveTextArgs>(ReceiveQueueSize);
}
else
{
ErrorLog.Error("Could not initialize client with key: {0}", Key);
}
}
catch
{
ErrorLog.Error("Could not initialize client with key: {0}", Key);
}
}
/// <summary>
/// Handles closing this up when the program shuts down
/// </summary>
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();
}
}
/// <summary>
/// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name.
/// </summary>
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)
{
Cleanup();
}
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();
}
}
/// <summary>
///
/// </summary>
public void Disconnect()
{
Debug.Console(2, "Disconnect Called");
DisconnectCalledByUser = true;
if (IsConnected)
{
Client.DisconnectFromServer();
}
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
Cleanup();
}
/// <summary>
/// Internal call to close up client. ALWAYS use this when disconnecting.
/// </summary>
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;
}
}
/// <summary>ff
/// Called from Connect failure or Socket Status change if
/// auto reconnect and socket disconnected (Not disconnected by user)
/// </summary>
void CheckClosedAndTryReconnect()
{
if (Client != null)
{
Debug.Console(2, this, "Cleaning up remotely closed/failed connection.");
Cleanup();
}
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);
}
}
/// <summary>
/// Receive callback
/// </summary>
/// <param name="client"></param>
/// <param name="numBytes"></param>
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 GenericTcpServerCommMethodReceiveTextArgs(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();
}
}
/// <summary>
/// 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.
/// </summary>
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);
}
}
/// <summary>
///
/// </summary>
void StopWaitForSharedKeyTimer()
{
if (WaitForSharedKey != null)
{
WaitForSharedKey.Stop();
WaitForSharedKey = null;
}
}
/// <summary>
/// General send method
/// </summary>
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);
}
}
}
/// <summary>
///
/// </summary>
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);
}
}
}
/// <summary>
/// SocketStatusChange Callback
/// </summary>
/// <param name="client"></param>
/// <param name="clientSocketStatus"></param>
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);
}
}
/// <summary>
/// Helper for ConnectionChange event
/// </summary>
void OnConnectionChange()
{
var handler = ConnectionChange;
if (handler != null)
ConnectionChange(this, new GenericTcpServerSocketStatusChangeEventArgs(this, Client.ClientStatus));
}
/// <summary>
/// Helper to fire ClientReadyForCommunications event
/// </summary>
void OnClientReadyForcommunications(bool isReady)
{
IsReadyForCommunication = isReady;
if (this.IsReadyForCommunication) { HeartbeatStart(); }
var handler = ClientReadyForCommunications;
if (handler != null)
handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
}
#endregion
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,610 @@
using System;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using Crestron.SimplSharp.Ssh;
using Crestron.SimplSharp.Ssh.Common;
namespace PepperDash.Core
{
/// <summary>
///
/// </summary>
public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
{
private const string SPlusKey = "Uninitialized SshClient";
/// <summary>
/// Object to enable stream debugging
/// </summary>
public CommunicationStreamDebugging StreamDebugging { get; private set; }
/// <summary>
/// Event that fires when data is received. Delivers args with byte array
/// </summary>
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Event that fires when data is received. Delivered as text.
/// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
/// <summary>
/// Event when the connection status changes.
/// </summary>
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
///// <summary>
/////
///// </summary>
//public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
/// <summary>
/// Address of server
/// </summary>
public string Hostname { get; set; }
/// <summary>
/// Port on server
/// </summary>
public int Port { get; set; }
/// <summary>
/// Username for server
/// </summary>
public string Username { get; set; }
/// <summary>
/// And... Password for server. That was worth documenting!
/// </summary>
public string Password { get; set; }
/// <summary>
/// True when the server is connected - when status == 2.
/// </summary>
public bool IsConnected
{
// returns false if no client or not connected
get { return Client != null && ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
///
/// </summary>
public SocketStatus ClientStatus
{
get { return _ClientStatus; }
private set
{
if (_ClientStatus == value)
return;
_ClientStatus = value;
OnConnectionChange();
}
}
SocketStatus _ClientStatus;
/// <summary>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
/// and IsConnected with be true when this == 2.
/// </summary>
public ushort UStatus
{
get { return (ushort)_ClientStatus; }
}
/// <summary>
/// Determines whether client will attempt reconnection on failure. Default is true
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// Will be set and unset by connect and disconnect only
/// </summary>
public bool ConnectEnabled { get; private set; }
/// <summary>
/// S+ helper for AutoReconnect
/// </summary>
public ushort UAutoReconnect
{
get { return (ushort)(AutoReconnect ? 1 : 0); }
set { AutoReconnect = value == 1; }
}
/// <summary>
/// Millisecond value, determines the timeout period in between reconnect attempts.
/// Set to 5000 by default
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
SshClient Client;
ShellStream TheStream;
CTimer ReconnectTimer;
//Lock object to prevent simulatneous connect/disconnect operations
private CCriticalSection connectLock = new CCriticalSection();
private bool DisconnectLogged = false;
/// <summary>
/// Typical constructor.
/// </summary>
public GenericSshClient(string key, string hostname, int port, string username, string password) :
base(key)
{
StreamDebugging = new CommunicationStreamDebugging(key);
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
Key = key;
Hostname = hostname;
Port = port;
Username = username;
Password = password;
AutoReconnectIntervalMs = 5000;
ReconnectTimer = new CTimer(o =>
{
if (ConnectEnabled)
{
Connect();
}
}, Timeout.Infinite);
}
/// <summary>
/// S+ Constructor - Must set all properties before calling Connect
/// </summary>
public GenericSshClient()
: base(SPlusKey)
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
ReconnectTimer = new CTimer(o =>
{
if (ConnectEnabled)
{
Connect();
}
}, Timeout.Infinite);
}
/// <summary>
/// Just to help S+ set the key
/// </summary>
public void Initialize(string key)
{
Key = key;
}
/// <summary>
/// Handles closing this up when the program shuts down
/// </summary>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping)
{
if (Client != null)
{
Debug.Console(1, this, "Program stopping. Closing connection");
Disconnect();
}
}
}
/// <summary>
/// Connect to the server, using the provided properties.
/// </summary>
public void Connect()
{
// Don't go unless everything is here
if (string.IsNullOrEmpty(Hostname) || Port < 1 || Port > 65535
|| Username == null || Password == null)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Connect failed. Check hostname, port, username and password are set or not null");
return;
}
ConnectEnabled = true;
try
{
connectLock.Enter();
if (IsConnected)
{
Debug.Console(1, this, "Connection already connected. Exiting Connect()");
}
else
{
Debug.Console(1, this, "Attempting connect");
// Cancel reconnect if running.
if (ReconnectTimer != null)
{
ReconnectTimer.Stop();
}
// Cleanup the old client if it already exists
if (Client != null)
{
Debug.Console(1, this, "Cleaning up disconnected client");
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
}
// This handles both password and keyboard-interactive (like on OS-X, 'nixes)
KeyboardInteractiveAuthenticationMethod kauth = new KeyboardInteractiveAuthenticationMethod(Username);
kauth.AuthenticationPrompt += new EventHandler<AuthenticationPromptEventArgs>(kauth_AuthenticationPrompt);
PasswordAuthenticationMethod pauth = new PasswordAuthenticationMethod(Username, Password);
Debug.Console(1, this, "Creating new SshClient");
ConnectionInfo connectionInfo = new ConnectionInfo(Hostname, Port, Username, pauth, kauth);
Client = new SshClient(connectionInfo);
Client.ErrorOccurred += Client_ErrorOccurred;
//Attempt to connect
ClientStatus = SocketStatus.SOCKET_STATUS_WAITING;
try
{
Client.Connect();
TheStream = Client.CreateShellStream("PDTShell", 100, 80, 100, 200, 65534);
TheStream.DataReceived += Stream_DataReceived;
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Connected");
ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED;
DisconnectLogged = false;
}
catch (SshConnectionException e)
{
var ie = e.InnerException; // The details are inside!!
var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error;
if (ie is SocketException)
Debug.Console(1, this, errorLogLevel, "'{0}' CONNECTION failure: Cannot reach host, ({1})", Key, ie.Message);
else if (ie is System.Net.Sockets.SocketException)
Debug.Console(1, this, errorLogLevel, "'{0}' Connection failure: Cannot reach host '{1}' on port {2}, ({3})",
Key, Hostname, Port, ie.GetType());
else if (ie is SshAuthenticationException)
{
Debug.Console(1, this, errorLogLevel, "Authentication failure for username '{0}', ({1})",
Username, ie.Message);
}
else
Debug.Console(1, this, errorLogLevel, "Error on connect:\r({0})", ie.Message);
DisconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect)
{
Debug.Console(1, this, "Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
ReconnectTimer.Reset(AutoReconnectIntervalMs);
}
}
catch (Exception e)
{
var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error;
Debug.Console(1, this, errorLogLevel, "Unhandled exception on connect:\r({0})", e.Message);
DisconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect)
{
Debug.Console(1, this, "Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
ReconnectTimer.Reset(AutoReconnectIntervalMs);
}
}
}
}
finally
{
connectLock.Leave();
}
}
/// <summary>
/// Disconnect the clients and put away it's resources.
/// </summary>
public void Disconnect()
{
ConnectEnabled = false;
// Stop trying reconnects, if we are
if (ReconnectTimer != null)
{
ReconnectTimer.Stop();
// ReconnectTimer = null;
}
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
}
/// <summary>
/// Kills the stream, cleans up the client and sets it to null
/// </summary>
private void KillClient(SocketStatus status)
{
KillStream();
try
{
if (Client != null)
{
Client.ErrorOccurred -= Client_ErrorOccurred;
Client.Disconnect();
Client.Dispose();
Client = null;
ClientStatus = status;
Debug.Console(1, this, "Disconnected");
}
}
catch (Exception ex)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Exception in Kill Client:{0}", ex);
}
}
/// <summary>
/// Anything to do with reestablishing connection on failures
/// </summary>
void HandleConnectionFailure()
{
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
Debug.Console(1, this, "Client nulled due to connection failure. AutoReconnect: {0}, ConnectEnabled: {1}", AutoReconnect, ConnectEnabled);
if (AutoReconnect && ConnectEnabled)
{
Debug.Console(1, this, "Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
if (ReconnectTimer == null)
{
ReconnectTimer = new CTimer(o =>
{
Connect();
}, AutoReconnectIntervalMs);
Debug.Console(1, this, "Attempting connection in {0} seconds",
(float) (AutoReconnectIntervalMs/1000));
}
else
{
Debug.Console(1, this, "{0} second reconnect cycle running",
(float) (AutoReconnectIntervalMs/1000));
}
}
}
/// <summary>
/// Kills the stream
/// </summary>
void KillStream()
{
try
{
if (TheStream != null)
{
TheStream.DataReceived -= Stream_DataReceived;
TheStream.Close();
TheStream.Dispose();
TheStream = null;
Debug.Console(1, this, "Disconnected stream");
}
}
catch (Exception ex)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Exception in Kill Stream:{0}", ex);
}
}
/// <summary>
/// Handles the keyboard interactive authentication, should it be required.
/// </summary>
void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e)
{
foreach (AuthenticationPrompt prompt in e.Prompts)
if (prompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1)
prompt.Response = Password;
}
/// <summary>
/// Handler for data receive on ShellStream. Passes data across to queue for line parsing.
/// </summary>
void Stream_DataReceived(object sender, Crestron.SimplSharp.Ssh.Common.ShellDataEventArgs e)
{
var bytes = e.Data;
if (bytes.Length > 0)
{
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: '{0}'", ComTextHelper.GetDebugText(str));
textHandler(this, new GenericCommMethodReceiveTextArgs(str));
}
}
}
/// <summary>
/// Error event handler for client events - disconnect, etc. Will forward those events via ConnectionChange
/// event
/// </summary>
void Client_ErrorOccurred(object sender, Crestron.SimplSharp.Ssh.Common.ExceptionEventArgs e)
{
CrestronInvoke.BeginInvoke(o =>
{
if (e.Exception is SshConnectionException || e.Exception is System.Net.Sockets.SocketException)
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Disconnected by remote");
else
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Unhandled SSH client error: {0}", e.Exception);
try
{
connectLock.Enter();
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_REMOTELY);
}
finally
{
connectLock.Leave();
}
if (AutoReconnect && ConnectEnabled)
{
Debug.Console(1, this, "Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
ReconnectTimer.Reset(AutoReconnectIntervalMs);
}
});
}
/// <summary>
/// Helper for ConnectionChange event
/// </summary>
void OnConnectionChange()
{
if (ConnectionChange != null)
ConnectionChange(this, new GenericSocketStatusChageEventArgs(this));
}
#region IBasicCommunication Members
/// <summary>
/// Sends text to the server
/// </summary>
/// <param name="text"></param>
public void SendText(string text)
{
try
{
if (Client != null && TheStream != null && IsConnected)
{
if (StreamDebugging.TxStreamDebuggingIsEnabled)
Debug.Console(0,
this,
"Sending {0} characters of text: '{1}'",
text.Length,
ComTextHelper.GetDebugText(text));
TheStream.Write(text);
TheStream.Flush();
}
else
{
Debug.Console(1, this, "Client is null or disconnected. Cannot Send Text");
}
}
catch (ObjectDisposedException ex)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Exception: {0}", ex.Message);
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Stack Trace: {0}", ex.StackTrace);
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
ReconnectTimer.Reset();
}
catch (Exception ex)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Exception: {0}", ex.Message);
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Stack Trace: {0}", ex.StackTrace);
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Stream write failed");
}
}
/// <summary>
/// Sends Bytes to the server
/// </summary>
/// <param name="bytes"></param>
public void SendBytes(byte[] bytes)
{
try
{
if (Client != null && TheStream != null && IsConnected)
{
if (StreamDebugging.TxStreamDebuggingIsEnabled)
Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
TheStream.Write(bytes, 0, bytes.Length);
TheStream.Flush();
}
else
{
Debug.Console(1, this, "Client is null or disconnected. Cannot Send Bytes");
}
}
catch (ObjectDisposedException ex)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Exception: {0}", ex.Message);
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Stack Trace: {0}", ex.StackTrace);
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
ReconnectTimer.Reset();
}
catch (Exception ex)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Exception: {0}", ex.Message);
Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Stack Trace: {0}", ex.StackTrace);
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Stream write failed");
}
}
#endregion
}
//*****************************************************************************************************
//*****************************************************************************************************
/// <summary>
/// Fired when connection changes
/// </summary>
public class SshConnectionChangeEventArgs : EventArgs
{
/// <summary>
/// Connection State
/// </summary>
public bool IsConnected { get; private set; }
/// <summary>
/// Connection Status represented as a ushort
/// </summary>
public ushort UIsConnected { get { return (ushort)(Client.IsConnected ? 1 : 0); } }
/// <summary>
/// The client
/// </summary>
public GenericSshClient Client { get; private set; }
/// <summary>
/// Socket Status as represented by
/// </summary>
public ushort Status { get { return Client.UStatus; } }
/// <summary>
/// S+ Constructor
/// </summary>
public SshConnectionChangeEventArgs() { }
/// <summary>
/// EventArgs class
/// </summary>
/// <param name="isConnected">Connection State</param>
/// <param name="client">The Client</param>
public SshConnectionChangeEventArgs(bool isConnected, GenericSshClient client)
{
IsConnected = isConnected;
Client = client;
}
}
}

View File

@@ -0,0 +1,567 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using Newtonsoft.Json;
namespace PepperDash.Core
{
/// <summary>
/// A class to handle basic TCP/IP communications with a server
/// </summary>
public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
{
private const string SplusKey = "Uninitialized TcpIpClient";
/// <summary>
/// Object to enable stream debugging
/// </summary>
public CommunicationStreamDebugging StreamDebugging { get; private set; }
/// <summary>
/// Fires when data is received from the server and returns it as a Byte array
/// </summary>
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Fires when data is received from the server and returns it as text
/// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
/// <summary>
///
/// </summary>
//public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
private string _hostname;
/// <summary>
/// Address of server
/// </summary>
public string Hostname
{
get
{
return _hostname;
}
set
{
_hostname = value;
if (_client != null)
{
_client.AddressClientConnectedTo = _hostname;
}
}
}
/// <summary>
/// Port on server
/// </summary>
public int Port { get; set; }
/// <summary>
/// Another damn S+ helper because S+ seems to treat large port nums as signed ints
/// which screws up things
/// </summary>
public ushort UPort
{
get { return Convert.ToUInt16(Port); }
set { Port = Convert.ToInt32(value); }
}
/// <summary>
/// Defaults to 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// The actual client class
/// </summary>
private TCPClient _client;
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsConnected
{
get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
/// _client socket status Read only
/// </summary>
public SocketStatus ClientStatus
{
get
{
return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus;
}
}
/// <summary>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
/// and IsConnected would be true when this == 2.
/// </summary>
public ushort UStatus
{
get { return (ushort)ClientStatus; }
}
/// <summary>
/// Status text shows the message associated with socket status
/// </summary>
public string ClientStatusText { get { return ClientStatus.ToString(); } }
/// <summary>
/// Ushort representation of client status
/// </summary>
[Obsolete]
public ushort UClientStatus { get { return (ushort)ClientStatus; } }
/// <summary>
/// Connection failure reason
/// </summary>
public string ConnectionFailure { get { return ClientStatus.ToString(); } }
/// <summary>
/// bool to track if auto reconnect should be set on the socket
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// S+ helper for AutoReconnect
/// </summary>
public ushort UAutoReconnect
{
get { return (ushort)(AutoReconnect ? 1 : 0); }
set { AutoReconnect = value == 1; }
}
/// <summary>
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// Set only when the disconnect method is called
/// </summary>
bool DisconnectCalledByUser;
/// <summary>
///
/// </summary>
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;
/// <summary>
/// Constructor
/// </summary>
/// <param name="key">unique string to differentiate between instances</param>
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="bufferSize"></param>
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);
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
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);
}
/// <summary>
/// Default constructor for S+
/// </summary>
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);
}
/// <summary>
/// Just to help S+ set the key
/// </summary>
public void Initialize(string key)
{
Key = key;
}
/// <summary>
/// Handles closing this up when the program shuts down
/// </summary>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping)
{
Debug.Console(1, this, "Program stopping. Closing connection");
Deactivate();
}
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public override bool Deactivate()
{
RetryTimer.Stop();
RetryTimer.Dispose();
if (_client != null)
{
_client.SocketStatusChange -= this.Client_SocketStatusChange;
DisconnectClient();
}
return true;
}
/// <summary>
/// Attempts to connect to the server
/// </summary>
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();
}
}
/// <summary>
/// Attempts to disconnect the client
/// </summary>
public void Disconnect()
{
try
{
connectLock.Enter();
DisconnectCalledByUser = true;
// Stop trying reconnects, if we are
RetryTimer.Stop();
DisconnectClient();
}
finally
{
connectLock.Leave();
}
}
/// <summary>
/// Does the actual disconnect business
/// </summary>
public void DisconnectClient()
{
if (_client != null)
{
Debug.Console(1, this, "Disconnecting client");
if (IsConnected)
_client.DisconnectFromServer();
}
}
/// <summary>
/// Callback method for connection attempt
/// </summary>
/// <param name="c"></param>
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);
}
}
/// <summary>
/// Disconnects, waits and attemtps to connect again
/// </summary>
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();
}
});
}
/// <summary>
/// Recieves incoming data
/// </summary>
/// <param name="client"></param>
/// <param name="numBytes"></param>
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);
}
}
/// <summary>
/// General send method
/// </summary>
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);
}
/// <summary>
/// This is useful from console and...?
/// </summary>
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);
}
/// <summary>
/// Sends Bytes to the server
/// </summary>
/// <param name="bytes"></param>
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);
}
/// <summary>
/// Socket Status Change Handler
/// </summary>
/// <param name="client"></param>
/// <param name="clientSocketStatus"></param>
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));
}
}
/// <summary>
/// Configuration properties for TCP/SSH Connections
/// </summary>
public class TcpSshPropertiesConfig
{
/// <summary>
/// Address to connect to
/// </summary>
[JsonProperty(Required = Required.Always)]
public string Address { get; set; }
/// <summary>
/// Port to connect to
/// </summary>
[JsonProperty(Required = Required.Always)]
public int Port { get; set; }
/// <summary>
/// Username credential
/// </summary>
public string Username { get; set; }
/// <summary>
/// Passord credential
/// </summary>
public string Password { get; set; }
/// <summary>
/// Defaults to 32768
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Defaults to true
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// Defaults to 5000ms
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// Default constructor
/// </summary>
public TcpSshPropertiesConfig()
{
BufferSize = 32768;
AutoReconnect = true;
AutoReconnectIntervalMs = 5000;
Username = "";
Password = "";
}
}
}

View File

@@ -0,0 +1,774 @@
/*PepperDash Technology Corp.
JAG
Copyright: 2017
------------------------------------
***Notice of Ownership and Copyright***
The material in which this notice appears is the property of PepperDash Technology Corporation,
which claims copyright under the laws of the United States of America in the entire body of material
and in all parts thereof, regardless of the use to which it is being put. Any use, in whole or in part,
of this material by another party without the express written permission of PepperDash Technology Corporation is prohibited.
PepperDash Technology Corporation reserves all rights under applicable laws.
------------------------------------ */
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
{
/// <summary>
/// Generic TCP/IP client for server
/// </summary>
public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
{
/// <summary>
/// Band aid delegate for choked server
/// </summary>
internal delegate void ConnectionHasHungCallbackDelegate();
#region Events
//public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Notifies of text received
/// </summary>
public event EventHandler<GenericTcpServerCommMethodReceiveTextArgs> TextReceived;
/// <summary>
/// Notifies of socket status change
/// </summary>
public event EventHandler<GenericTcpServerSocketStatusChangeEventArgs> ConnectionChange;
/// <summary>
/// This is something of a band-aid callback. If the client times out during the connection process, because the server
/// is stuck, this will fire. It is intended to be used by the Server class monitor client, to help
/// keep a watch on the server and reset it if necessary.
/// </summary>
internal ConnectionHasHungCallbackDelegate ConnectionHasHungCallback;
/// <summary>
/// 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.
/// </summary>
public event EventHandler<GenericTcpServerClientReadyForcommunicationsEventArgs> ClientReadyForCommunications;
#endregion
#region Properties & Variables
/// <summary>
/// Address of server
/// </summary>
public string Hostname { get; set; }
/// <summary>
/// Port on server
/// </summary>
public int Port { get; set; }
/// <summary>
/// S+ helper
/// </summary>
public ushort UPort
{
get { return Convert.ToUInt16(Port); }
set { Port = Convert.ToInt32(value); }
}
/// <summary>
/// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class
/// </summary>
public bool SharedKeyRequired { get; set; }
/// <summary>
/// S+ helper for requires shared key bool
/// </summary>
public ushort USharedKeyRequired
{
set
{
if (value == 1)
SharedKeyRequired = true;
else
SharedKeyRequired = false;
}
}
/// <summary>
/// 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
/// </summary>
public string SharedKey { get; set; }
/// <summary>
/// flag to show the client is waiting for the server to send the shared key
/// </summary>
private bool WaitingForSharedKeyResponse { get; set; }
/// <summary>
/// Defaults to 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Semaphore on connect method
/// </summary>
bool IsTryingToConnect;
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsConnected
{
get
{
if (Client != null)
return Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED;
else
return false;
}
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
/// Bool showing if socket is ready for communication after shared key exchange
/// </summary>
public bool IsReadyForCommunication { get; set; }
/// <summary>
/// S+ helper for IsReadyForCommunication
/// </summary>
public ushort UIsReadyForCommunication
{
get { return (ushort)(IsReadyForCommunication ? 1 : 0); }
}
/// <summary>
/// Client socket status Read only
/// </summary>
public SocketStatus ClientStatus
{
get
{
if (Client != null)
return Client.ClientStatus;
else
return SocketStatus.SOCKET_STATUS_NO_CONNECT;
}
}
/// <summary>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
/// and IsConnected would be true when this == 2.
/// </summary>
public ushort UStatus
{
get { return (ushort)ClientStatus; }
}
/// <summary>
/// Status text shows the message associated with socket status
/// </summary>
public string ClientStatusText { get { return ClientStatus.ToString(); } }
/// <summary>
/// bool to track if auto reconnect should be set on the socket
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// S+ helper for AutoReconnect
/// </summary>
public ushort UAutoReconnect
{
get { return (ushort)(AutoReconnect ? 1 : 0); }
set { AutoReconnect = value == 1; }
}
/// <summary>
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// Flag Set only when the disconnect method is called.
/// </summary>
bool DisconnectCalledByUser;
/// <summary>
/// private Timer for auto reconnect
/// </summary>
CTimer RetryTimer;
/// <summary>
///
/// </summary>
public bool HeartbeatEnabled { get; set; }
/// <summary>
///
/// </summary>
public ushort UHeartbeatEnabled
{
get { return (ushort)(HeartbeatEnabled ? 1 : 0); }
set { HeartbeatEnabled = value == 1; }
}
/// <summary>
///
/// </summary>
public string HeartbeatString = "heartbeat";
/// <summary>
///
/// </summary>
public int HeartbeatInterval = 50000;
CTimer HeartbeatSendTimer;
CTimer HeartbeatAckTimer;
/// <summary>
/// Used to force disconnection on a dead connect attempt
/// </summary>
CTimer ConnectFailTimer;
CTimer WaitForSharedKey;
private int ConnectionCount;
/// <summary>
/// Internal secure client
/// </summary>
TCPClient Client;
bool ProgramIsStopping;
#endregion
#region Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="bufferSize"></param>
public GenericTcpIpClient_ForServer(string key, string address, int port, int bufferSize)
: base(key)
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
Hostname = address;
Port = port;
BufferSize = bufferSize;
AutoReconnectIntervalMs = 5000;
}
/// <summary>
/// Constructor for S+
/// </summary>
public GenericTcpIpClient_ForServer()
: base("Uninitialized DynamicTcpClient")
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
BufferSize = 2000;
}
#endregion
#region Methods
/// <summary>
/// Just to help S+ set the key
/// </summary>
public void Initialize(string key)
{
Key = key;
}
/// <summary>
/// Handles closing this up when the program shuts down
/// </summary>
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();
}
}
/// <summary>
/// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name.
/// </summary>
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)
{
Cleanup();
}
DisconnectCalledByUser = false;
Client = new TCPClient(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();
}
}
/// <summary>
///
/// </summary>
public void Disconnect()
{
Debug.Console(2, "Disconnect Called");
DisconnectCalledByUser = true;
if (IsConnected)
{
Client.DisconnectFromServer();
}
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
Cleanup();
}
/// <summary>
/// Internal call to close up client. ALWAYS use this when disconnecting.
/// </summary>
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;
}
}
/// <summary>ff
/// Called from Connect failure or Socket Status change if
/// auto reconnect and socket disconnected (Not disconnected by user)
/// </summary>
void CheckClosedAndTryReconnect()
{
if (Client != null)
{
Debug.Console(2, this, "Cleaning up remotely closed/failed connection.");
Cleanup();
}
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;
}
RetryTimer = new CTimer(o => Connect(), rndTime);
}
}
/// <summary>
/// Receive callback
/// </summary>
/// <param name="client"></param>
/// <param name="numBytes"></param>
void Receive(TCPClient client, int numBytes)
{
if (numBytes > 0)
{
string str = string.Empty;
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 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);
}
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);
}
}
/// <summary>
///
/// </summary>
void StopWaitForSharedKeyTimer()
{
if (WaitForSharedKey != null)
{
WaitForSharedKey.Stop();
WaitForSharedKey = null;
}
}
/// <summary>
/// General send method
/// </summary>
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);
}
}
}
/// <summary>
///
/// </summary>
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);
}
}
}
/// <summary>
/// SocketStatusChange Callback
/// </summary>
/// <param name="client"></param>
/// <param name="clientSocketStatus"></param>
void Client_SocketStatusChange(TCPClient 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);
}
}
/// <summary>
/// Helper for ConnectionChange event
/// </summary>
void OnConnectionChange()
{
var handler = ConnectionChange;
if (handler != null)
ConnectionChange(this, new GenericTcpServerSocketStatusChangeEventArgs(this, Client.ClientStatus));
}
/// <summary>
/// Helper to fire ClientReadyForCommunications event
/// </summary>
void OnClientReadyForcommunications(bool isReady)
{
IsReadyForCommunication = isReady;
if (this.IsReadyForCommunication) { HeartbeatStart(); }
var handler = ClientReadyForCommunications;
if (handler != null)
handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
}
#endregion
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,401 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace PepperDash.Core
{
/// <summary>
/// Generic UDP Server device
/// </summary>
public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
{
private const string SplusKey = "Uninitialized Udp Server";
/// <summary>
/// Object to enable stream debugging
/// </summary>
public CommunicationStreamDebugging StreamDebugging { get; private set; }
/// <summary>
///
/// </summary>
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
///
/// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
/// <summary>
/// This event will fire when a message is dequeued that includes the source IP and Port info if needed to determine the source of the received data.
/// </summary>
public event EventHandler<GenericUdpReceiveTextExtraArgs> DataRecievedExtra;
/// <summary>
///
/// </summary>
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
/// <summary>
///
/// </summary>
public event EventHandler<GenericUdpConnectedEventArgs> UpdateConnectionStatus;
/// <summary>
///
/// </summary>
public SocketStatus ClientStatus
{
get
{
return Server.ServerStatus;
}
}
/// <summary>
///
/// </summary>
public ushort UStatus
{
get { return (ushort)Server.ServerStatus; }
}
/// <summary>
/// Address of server
/// </summary>
public string Hostname { get; set; }
/// <summary>
/// Port on server
/// </summary>
public int Port { get; set; }
/// <summary>
/// Another damn S+ helper because S+ seems to treat large port nums as signed ints
/// which screws up things
/// </summary>
public ushort UPort
{
get { return Convert.ToUInt16(Port); }
set { Port = Convert.ToInt32(value); }
}
/// <summary>
/// Indicates that the UDP Server is enabled
/// </summary>
public bool IsConnected
{
get;
private set;
}
/// <summary>
/// Numeric value indicating
/// </summary>
public ushort UIsConnected
{
get { return IsConnected ? (ushort)1 : (ushort)0; }
}
/// <summary>
/// Defaults to 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// The server
/// </summary>
public UDPServer Server { get; private set; }
/// <summary>
/// Constructor for S+. Make sure to set key, address, port, and buffersize using init method
/// </summary>
public GenericUdpServer()
: base(SplusKey)
{
StreamDebugging = new CommunicationStreamDebugging(SplusKey);
BufferSize = 5000;
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler);
}
/// <summary>
///
/// </summary>
/// <param name="key"></param>
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="buffefSize"></param>
public GenericUdpServer(string key, string address, int port, int buffefSize)
: base(key)
{
StreamDebugging = new CommunicationStreamDebugging(key);
Hostname = address;
Port = port;
BufferSize = buffefSize;
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler);
}
/// <summary>
/// Call from S+ to initialize values
/// </summary>
/// <param name="key"></param>
/// <param name="address"></param>
/// <param name="port"></param>
public void Initialize(string key, string address, ushort port)
{
Key = key;
Hostname = address;
UPort = port;
}
/// <summary>
///
/// </summary>
/// <param name="ethernetEventArgs"></param>
void CrestronEnvironment_EthernetEventHandler(EthernetEventArgs ethernetEventArgs)
{
// Re-enable the server if the link comes back up and the status should be connected
if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp
&& IsConnected)
{
Connect();
}
}
/// <summary>
///
/// </summary>
/// <param name="programEventType"></param>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType != eProgramStatusEventType.Stopping)
return;
Debug.Console(1, this, "Program stopping. Disabling Server");
Disconnect();
}
/// <summary>
/// Enables the UDP Server
/// </summary>
public void Connect()
{
if (Server == null)
{
Server = new UDPServer();
}
if (string.IsNullOrEmpty(Hostname))
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpServer '{0}': No address set", Key);
return;
}
if (Port < 1 || Port > 65535)
{
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpServer '{0}': Invalid port", Key);
return;
}
}
var status = Server.EnableUDPServer(Hostname, Port);
Debug.Console(2, this, "SocketErrorCode: {0}", status);
if (status == SocketErrorCodes.SOCKET_OK)
IsConnected = true;
var handler = UpdateConnectionStatus;
if (handler != null)
handler(this, new GenericUdpConnectedEventArgs(UIsConnected));
// Start receiving data
Server.ReceiveDataAsync(Receive);
}
/// <summary>
/// Disabled the UDP Server
/// </summary>
public void Disconnect()
{
if(Server != null)
Server.DisableUDPServer();
IsConnected = false;
var handler = UpdateConnectionStatus;
if (handler != null)
handler(this, new GenericUdpConnectedEventArgs(UIsConnected));
}
/// <summary>
/// Recursive method to receive data
/// </summary>
/// <param name="server"></param>
/// <param name="numBytes"></param>
void Receive(UDPServer server, int numBytes)
{
Debug.Console(2, this, "Received {0} bytes", numBytes);
try
{
if (numBytes <= 0)
return;
var sourceIp = Server.IPAddressLastMessageReceivedFrom;
var sourcePort = Server.IPPortLastMessageReceivedFrom;
var bytes = server.IncomingDataBuffer.Take(numBytes).ToArray();
var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
var dataRecivedExtra = DataRecievedExtra;
if (dataRecivedExtra != null)
dataRecivedExtra(this, new GenericUdpReceiveTextExtraArgs(str, sourceIp, sourcePort, bytes));
Debug.Console(2, this, "Bytes: {0}", bytes.ToString());
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)
{
if (StreamDebugging.RxStreamDebuggingIsEnabled)
Debug.Console(0, this, "Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length);
textHandler(this, new GenericCommMethodReceiveTextArgs(str));
}
}
catch (Exception ex)
{
Debug.Console(0, "GenericUdpServer Receive error: {0}{1}", ex.Message, ex.StackTrace);
}
finally
{
server.ReceiveDataAsync(Receive);
}
}
/// <summary>
/// General send method
/// </summary>
/// <param name="text"></param>
public void SendText(string text)
{
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
if (IsConnected && Server != null)
{
if (StreamDebugging.TxStreamDebuggingIsEnabled)
Debug.Console(0, this, "Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text));
Server.SendData(bytes, bytes.Length);
}
}
/// <summary>
///
/// </summary>
/// <param name="bytes"></param>
public void SendBytes(byte[] bytes)
{
if (StreamDebugging.TxStreamDebuggingIsEnabled)
Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
if (IsConnected && Server != null)
Server.SendData(bytes, bytes.Length);
}
}
/// <summary>
///
/// </summary>
public class GenericUdpReceiveTextExtraArgs : EventArgs
{
/// <summary>
///
/// </summary>
public string Text { get; private set; }
/// <summary>
///
/// </summary>
public string IpAddress { get; private set; }
/// <summary>
///
/// </summary>
public int Port { get; private set; }
/// <summary>
///
/// </summary>
public byte[] Bytes { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="text"></param>
/// <param name="ipAddress"></param>
/// <param name="port"></param>
/// <param name="bytes"></param>
public GenericUdpReceiveTextExtraArgs(string text, string ipAddress, int port, byte[] bytes)
{
Text = text;
IpAddress = ipAddress;
Port = port;
Bytes = bytes;
}
/// <summary>
/// Stupid S+ Constructor
/// </summary>
public GenericUdpReceiveTextExtraArgs() { }
}
/// <summary>
///
/// </summary>
public class UdpServerPropertiesConfig
{
/// <summary>
///
/// </summary>
[JsonProperty(Required = Required.Always)]
public string Address { get; set; }
/// <summary>
///
/// </summary>
[JsonProperty(Required = Required.Always)]
public int Port { get; set; }
/// <summary>
/// Defaults to 32768
/// </summary>
public int BufferSize { get; set; }
/// <summary>
///
/// </summary>
public UdpServerPropertiesConfig()
{
BufferSize = 32768;
}
}
}

View File

@@ -0,0 +1,351 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core
{
/// <summary>
/// Allows for two simultaneous TCP clients to connect to a redundant pair of QSC Core DSPs and manages
/// </summary>
public class QscCoreDoubleTcpIpClient : IKeyed
{
/// <summary>
/// Key to uniquely identify the instance of the class
/// </summary>
public string Key { get; private set; }
/// <summary>
/// Fires when a bool value changes to notify the S+ module
/// </summary>
public event EventHandler<BoolChangeEventArgs> BoolChange;
/// <summary>
/// Fires when a ushort value changes to notify the S+ module
/// </summary>
public event EventHandler<UshrtChangeEventArgs> UshortChange;
/// <summary>
/// Fires when a string value changes to notify the S+ module
/// </summary>
public event EventHandler<StringChangeEventArgs> StringChange;
/// <summary>
/// The client for the master DSP unit
/// </summary>
public GenericTcpIpClient MasterClient { get; private set; }
/// <summary>
/// The client for the slave DSP unit
/// </summary>
public GenericTcpIpClient SlaveClient { get; private set; }
string Username;
string Password;
string LineEnding;
CommunicationGather MasterGather;
CommunicationGather SlaveGather;
bool IsPolling;
int PollingIntervalSeconds;
CTimer PollTimer;
bool SlaveIsActive;
/// <summary>
/// Default constuctor for S+
/// </summary>
public QscCoreDoubleTcpIpClient()
{
MasterClient = new GenericTcpIpClient("temp-master");
MasterClient.AutoReconnect = true;
MasterClient.AutoReconnectIntervalMs = 2000;
SlaveClient = new GenericTcpIpClient("temp-slave");
SlaveClient.AutoReconnect = true;
SlaveClient.AutoReconnectIntervalMs = 2000;
}
/// <summary>
/// Connects to both DSP units
/// </summary>
/// <param name="key"></param>
/// <param name="masterAddress"></param>
/// <param name="masterPort"></param>
/// <param name="slaveAddress"></param>
/// <param name="slavePort"></param>
/// <param name="username"></param>
/// <param name="password"></param>
/// <param name="pollingIntervalSeconds"></param>
/// <param name="lineEnding"></param>
public void Connect(string key, string masterAddress, int masterPort,
string slaveAddress, int slavePort, string username, string password,
int pollingIntervalSeconds, string lineEnding)
{
Key = key;
PollingIntervalSeconds = pollingIntervalSeconds;
Username = username;
Password = password;
LineEnding = lineEnding;
MasterClient.Initialize(key + "-master");
SlaveClient.Initialize(key + "-slave");
MasterClient.Hostname = masterAddress;
MasterClient.Port = masterPort;
if (MasterClient != null)
{
MasterClient.Disconnect();
}
if (SlaveClient != null)
{
SlaveClient.Disconnect();
}
if (MasterGather == null)
{
MasterGather = new CommunicationGather(MasterClient, lineEnding);
MasterGather.IncludeDelimiter = true;
}
MasterGather.LineReceived -= MasterGather_LineReceived;
MasterGather.LineReceived += new EventHandler<GenericCommMethodReceiveTextArgs>(MasterGather_LineReceived);
MasterClient.ConnectionChange -= MasterClient_SocketStatusChange;
MasterClient.ConnectionChange += MasterClient_SocketStatusChange;
SlaveClient.Hostname = slaveAddress;
SlaveClient.Port = slavePort;
if (SlaveGather == null)
{
SlaveGather = new CommunicationGather(SlaveClient, lineEnding);
SlaveGather.IncludeDelimiter = true;
}
SlaveGather.LineReceived -= MasterGather_LineReceived;
SlaveGather.LineReceived += new EventHandler<GenericCommMethodReceiveTextArgs>(SlaveGather_LineReceived);
SlaveClient.ConnectionChange -= SlaveClient_SocketStatusChange;
SlaveClient.ConnectionChange += SlaveClient_SocketStatusChange;
MasterClient.Connect();
SlaveClient.Connect();
}
/// <summary>
///
/// </summary>
public void Disconnect()
{
if (MasterClient != null)
{
MasterGather.LineReceived -= MasterGather_LineReceived;
MasterClient.Disconnect();
}
if (SlaveClient != null)
{
SlaveGather.LineReceived -= SlaveGather_LineReceived;
SlaveClient.Disconnect();
}
if (PollTimer != null)
{
IsPolling = false;
PollTimer.Stop();
PollTimer = null;
}
}
/// <summary>
/// Does not include line feed
/// </summary>
public void SendText(string s)
{
if (SlaveIsActive)
{
if (SlaveClient != null)
{
Debug.Console(2, this, "Sending to Slave: {0}", s);
SlaveClient.SendText(s);
}
}
else
{
if (MasterClient != null)
{
Debug.Console(2, this, "Sending to Master: {0}", s);
MasterClient.SendText(s);
}
}
}
void MasterClient_SocketStatusChange(object sender, GenericSocketStatusChageEventArgs args)
{
OnUshortChange((ushort)args.Client.ClientStatus, MasterClientStatusId);
if (args.Client.IsConnected)
{
MasterGather.LineReceived += MasterGather_LineReceived;
StartPolling();
}
else
MasterGather.LineReceived -= MasterGather_LineReceived;
}
void SlaveClient_SocketStatusChange(object sender, GenericSocketStatusChageEventArgs args)
{
OnUshortChange((ushort)args.Client.ClientStatus, SlaveClientStatusId);
if (args.Client.IsConnected)
{
SlaveGather.LineReceived += SlaveGather_LineReceived;
StartPolling();
}
else
SlaveGather.LineReceived -= SlaveGather_LineReceived;
}
void MasterGather_LineReceived(object sender, GenericCommMethodReceiveTextArgs e)
{
if (e.Text.Contains("login_required"))
{
MasterClient.SendText(string.Format("login {0} {1} \x0d\x0a", Username, Password));
}
else if (e.Text.Contains("login_success"))
{
// START THE POLLING, YO!
}
else if (e.Text.StartsWith("sr"))
{
// example response "sr "MyDesign" "NIEC2bxnVZ6a" 1 1"
var split = e.Text.Trim().Split(' ');
if (split[split.Length - 1] == "1")
{
SlaveIsActive = false;
OnBoolChange(false, SlaveIsActiveId);
OnBoolChange(true, MasterIsActiveId);
}
}
if (!SlaveIsActive)
OnStringChange(e.Text, LineReceivedId);
}
void SlaveGather_LineReceived(object sender, GenericCommMethodReceiveTextArgs e)
{
if (e.Text.Contains("login_required"))
{
SlaveClient.SendText(string.Format("login {0} {1} \x0d\x0a", Username, Password));
}
else if (e.Text.Contains("login_success"))
{
// START THE POLLING, YO!
}
else if (e.Text.StartsWith("sr"))
{
var split = e.Text.Trim().Split(' ');
if (split[split.Length - 1] == "1")
{
SlaveIsActive = true;
OnBoolChange(true, SlaveIsActiveId);
OnBoolChange(false, MasterIsActiveId);
}
}
if (SlaveIsActive)
OnStringChange(e.Text, LineReceivedId);
}
void StartPolling()
{
if (!IsPolling)
{
IsPolling = true;
Poll();
if (PollTimer != null)
PollTimer.Stop();
PollTimer = new CTimer(o => Poll(), null, PollingIntervalSeconds * 1000, PollingIntervalSeconds * 1000);
}
}
void Poll()
{
if (MasterClient != null && MasterClient.IsConnected)
{
Debug.Console(2, this, "Polling Master.");
MasterClient.SendText("sg\x0d\x0a");
}
if (SlaveClient != null && SlaveClient.IsConnected)
{
Debug.Console(2, this, "Polling Slave.");
SlaveClient.SendText("sg\x0d\x0a");
}
}
// login NAME PIN ---> login_success, login_failed
// status get
// sg --> sr DESIGN_NAME DESIGN_ID IS_PRIMARY IS_ACTIVE
// CRLF
void OnBoolChange(bool state, ushort type)
{
var handler = BoolChange;
if (handler != null)
handler(this, new BoolChangeEventArgs(state, type));
}
void OnUshortChange(ushort state, ushort type)
{
var handler = UshortChange;
if (handler != null)
handler(this, new UshrtChangeEventArgs(state, type));
}
void OnStringChange(string value, ushort type)
{
var handler = StringChange;
if (handler != null)
handler(this, new StringChangeEventArgs(value, type));
}
/// <summary>
///
/// </summary>
public const ushort MasterIsActiveId = 3;
/// <summary>
///
/// </summary>
public const ushort SlaveIsActiveId = 4;
/// <summary>
///
/// </summary>
public const ushort MainModuleInitiailzeId = 5;
/// <summary>
///
/// </summary>
public const ushort MasterClientStatusId = 101;
/// <summary>
///
/// </summary>
public const ushort SlaveClientStatusId = 102;
/// <summary>
///
/// </summary>
public const ushort LineReceivedId = 201;
}
}

View File

@@ -0,0 +1,59 @@
using Newtonsoft.Json;
namespace PepperDash.Core
{
/// <summary>
/// Client config object for TCP client with server that inherits from TcpSshPropertiesConfig and adds properties for shared key and heartbeat
/// </summary>
public class TcpClientConfigObject
{
/// <summary>
/// TcpSsh Properties
/// </summary>
[JsonProperty("control")]
public ControlPropertiesConfig Control { get; set; }
/// <summary>
/// Bool value for secure. Currently not implemented in TCP sockets as they are not dynamic
/// </summary>
[JsonProperty("secure")]
public bool Secure { get; set; }
/// <summary>
/// Require a shared key that both server and client negotiate. If negotiation fails server disconnects the client
/// </summary>
[JsonProperty("sharedKeyRequired")]
public bool SharedKeyRequired { get; set; }
/// <summary>
/// The shared key that must match on the server and client
/// </summary>
[JsonProperty("sharedKey")]
public string SharedKey { get; set; }
/// <summary>
/// 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.
/// </summary>
[JsonProperty("heartbeatRequired")]
public bool HeartbeatRequired { get; set; }
/// <summary>
/// The interval in seconds for the heartbeat from the client. If not received client is disconnected
/// </summary>
[JsonProperty("heartbeatRequiredIntervalInSeconds")]
public ushort HeartbeatRequiredIntervalInSeconds { get; set; }
/// <summary>
/// HeartbeatString that will be checked against the message received. defaults to heartbeat if no string is provided.
/// </summary>
[JsonProperty("heartbeatStringToMatch")]
public string HeartbeatStringToMatch { get; set; }
/// <summary>
/// Receive Queue size must be greater than 20 or defaults to 20
/// </summary>
[JsonProperty("receiveQueueSize")]
public int ReceiveQueueSize { get; set; }
}
}

View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core
{
/// <summary>
/// Tcp Server Config object with properties for a tcp server with shared key and heartbeat capabilities
/// </summary>
public class TcpServerConfigObject
{
/// <summary>
/// Uique key
/// </summary>
public string Key { get; set; }
/// <summary>
/// Max Clients that the server will allow to connect.
/// </summary>
public ushort MaxClients { get; set; }
/// <summary>
/// Bool value for secure. Currently not implemented in TCP sockets as they are not dynamic
/// </summary>
public bool Secure { get; set; }
/// <summary>
/// Port for the server to listen on
/// </summary>
public int Port { get; set; }
/// <summary>
/// Require a shared key that both server and client negotiate. If negotiation fails server disconnects the client
/// </summary>
public bool SharedKeyRequired { get; set; }
/// <summary>
/// The shared key that must match on the server and client
/// </summary>
public string SharedKey { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool HeartbeatRequired { get; set; }
/// <summary>
/// The interval in seconds for the heartbeat from the client. If not received client is disconnected
/// </summary>
public ushort HeartbeatRequiredIntervalInSeconds { get; set; }
/// <summary>
/// HeartbeatString that will be checked against the message received. defaults to heartbeat if no string is provided.
/// </summary>
public string HeartbeatStringToMatch { get; set; }
/// <summary>
/// Client buffer size. See Crestron help. defaults to 2000 if not greater than 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Receive Queue size must be greater than 20 or defaults to 20
/// </summary>
public int ReceiveQueueSize { get; set; }
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core
{
/// <summary>
/// Crestron Control Methods for a comm object
/// </summary>
public enum eControlMethod
{
/// <summary>
///
/// </summary>
None = 0,
/// <summary>
/// RS232/422/485
/// </summary>
Com,
/// <summary>
/// Crestron IpId (most Crestron ethernet devices)
/// </summary>
IpId,
/// <summary>
/// Crestron IpIdTcp (HD-MD series, etc.)
/// </summary>
IpidTcp,
/// <summary>
/// Crestron IR control
/// </summary>
IR,
/// <summary>
/// SSH client
/// </summary>
Ssh,
/// <summary>
/// TCP/IP client
/// </summary>
Tcpip,
/// <summary>
/// Telnet
/// </summary>
Telnet,
/// <summary>
/// Crestnet device
/// </summary>
Cresnet,
/// <summary>
/// CEC Control, via a DM HDMI port
/// </summary>
Cec,
/// <summary>
/// UDP Server
/// </summary>
Udp,
/// <summary>
/// HTTP client
/// </summary>
Http,
/// <summary>
/// HTTPS client
/// </summary>
Https,
/// <summary>
/// Websocket client
/// </summary>
Ws,
/// <summary>
/// Secure Websocket client
/// </summary>
Wss,
/// <summary>
/// Secure TCP/IP
/// </summary>
SecureTcpIp
}
}