diff --git a/CLZ Builds/PepperDash_Core.clz b/CLZ Builds/PepperDash_Core.clz deleted file mode 100644 index ed8a694..0000000 Binary files a/CLZ Builds/PepperDash_Core.clz and /dev/null differ diff --git a/CLZ Builds/PepperDash_Core.dll b/CLZ Builds/PepperDash_Core.dll deleted file mode 100644 index 3e798d7..0000000 Binary files a/CLZ Builds/PepperDash_Core.dll and /dev/null differ diff --git a/Pepperdash Core/Pepperdash Core/Comm/EventArgs.cs b/Pepperdash Core/Pepperdash Core/Comm/EventArgs.cs index ed43c62..e9e9f1f 100644 --- a/Pepperdash Core/Pepperdash Core/Comm/EventArgs.cs +++ b/Pepperdash Core/Pepperdash Core/Comm/EventArgs.cs @@ -23,12 +23,14 @@ namespace PepperDash.Core { public ISocketStatus Client { get; private set; } - public GenericSocketStatusChageEventArgs() { } - public GenericSocketStatusChageEventArgs(ISocketStatus client) { Client = client; } + /// + /// Stupid S+ Constructor + /// + public GenericSocketStatusChageEventArgs() { } } public delegate void GenericTcpServerStateChangedEventDelegate(ServerState state); @@ -36,12 +38,14 @@ namespace PepperDash.Core { public ServerState State { get; private set; } - public GenericTcpServerStateChangedEventArgs() { } - public GenericTcpServerStateChangedEventArgs(ServerState state) { State = state; } + /// + /// Stupid S+ Constructor + /// + public GenericTcpServerStateChangedEventArgs() { } } public delegate void GenericTcpServerSocketStatusChangeEventDelegate(object socket, uint clientIndex, SocketStatus clientStatus); @@ -50,8 +54,6 @@ namespace PepperDash.Core public object Socket { get; private set; } public uint ReceivedFromClientIndex { get; private set; } public SocketStatus ClientStatus { get; set; } - - public GenericTcpServerSocketStatusChangeEventArgs() { } public GenericTcpServerSocketStatusChangeEventArgs(object socket, SocketStatus clientStatus) { @@ -65,6 +67,10 @@ namespace PepperDash.Core ReceivedFromClientIndex = clientIndex; ClientStatus = clientStatus; } + /// + /// Stupid S+ Constructor + /// + public GenericTcpServerSocketStatusChangeEventArgs() { } } public class GenericTcpServerCommMethodReceiveTextArgs : EventArgs @@ -82,6 +88,10 @@ namespace PepperDash.Core Text = text; ReceivedFromClientIndex = clientIndex; } + /// + /// Stupid S+ Constructor + /// + public GenericTcpServerCommMethodReceiveTextArgs() { } } public class GenericTcpServerClientReadyForcommunicationsEventArgs : EventArgs @@ -91,6 +101,29 @@ namespace PepperDash.Core { IsReady = isReady; } + /// + /// Stupid S+ Constructor + /// + public GenericTcpServerClientReadyForcommunicationsEventArgs() { } + } + + public class GenericUdpConnectedEventArgs : EventArgs + { + public ushort UConnected; + public bool Connected; + + public GenericUdpConnectedEventArgs() { } + + public GenericUdpConnectedEventArgs(ushort uconnected) + { + UConnected = uconnected; + } + + public GenericUdpConnectedEventArgs(bool connected) + { + Connected = connected; + } + } diff --git a/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpClient_ForServer.cs b/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpClient_ForServer.cs index 13e1c66..c3882ca 100644 --- a/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpClient_ForServer.cs +++ b/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpClient_ForServer.cs @@ -33,6 +33,15 @@ namespace PepperDash.Core public event EventHandler TextReceived; + public event EventHandler AutoReconnectTriggered; + + /// + /// Event for Receiving text. Once subscribed to this event the receive callback will start a thread that dequeues the messages and invokes the event on a new thread. + /// It is not recommended to use both the TextReceived event and the TextReceivedQueueInvoke event. + /// + public event EventHandler TextReceivedQueueInvoke; + + public event EventHandler ConnectionChange; @@ -209,8 +218,20 @@ namespace PepperDash.Core get { return (ushort)(HeartbeatEnabled ? 1 : 0); } set { HeartbeatEnabled = value == 1; } } - public string HeartbeatString = "heartbeat"; - public int HeartbeatInterval = 50000; + + public string HeartbeatString { get; set; } + //public int HeartbeatInterval = 50000; + + /// + /// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+ + /// + public int HeartbeatInterval { get; set; } + + /// + /// Simpl+ Heartbeat Analog value in seconds + /// + public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } } + CTimer HeartbeatSendTimer; CTimer HeartbeatAckTimer; /// @@ -226,6 +247,24 @@ namespace PepperDash.Core bool ProgramIsStopping; + /// + /// Queue lock + /// + CCriticalSection DequeueLock = new CCriticalSection(); + + /// + /// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before + /// calling initialize. + /// + public int ReceiveQueueSize { get; set; } + + /// + /// Queue to temporarily store received messages with the source IP and Port info. Defaults to size 20. Use constructor or set queue size property before + /// calling initialize. + /// + private CrestronQueue MessageQueue; + + #endregion #region Constructors @@ -244,12 +283,24 @@ namespace PepperDash.Core //base class constructor public GenericSecureTcpIpClient_ForServer() - : base("Uninitialized DynamicTcpClient") + : base("Uninitialized Secure Tcp Client For Server") { CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); AutoReconnectIntervalMs = 5000; BufferSize = 2000; } + + /// + /// Contstructor that sets all properties by calling the initialize method with a config object. + /// + /// + public GenericSecureTcpIpClient_ForServer(string key, TcpClientConfigObject clientConfigObject) + : base(key) + { + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + Initialize(clientConfigObject); + } + #endregion #region Methods @@ -262,6 +313,43 @@ namespace PepperDash.Core Key = key; } + /// + /// Initialize called by the constructor that accepts a client config object. Can be called later to reset properties of client. + /// + /// + public void Initialize(TcpClientConfigObject 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(ReceiveQueueSize); + } + else + { + ErrorLog.Error("Could not initialize client with key: {0}", Key); + } + } + catch + { + ErrorLog.Error("Could not initialize client with key: {0}", Key); + } + } + /// /// Handles closing this up when the program shuts down /// @@ -328,7 +416,8 @@ namespace PepperDash.Core Client = new SecureTCPClient(Hostname, Port, BufferSize); Client.SocketStatusChange += Client_SocketStatusChange; - Client.SocketSendOrReceiveTimeOutInMs = (HeartbeatInterval * 5); + if (HeartbeatEnabled) + Client.SocketSendOrReceiveTimeOutInMs = (HeartbeatInterval * 5); Client.AddressClientConnectedTo = Hostname; Client.PortNumber = Port; // SecureClient = c; @@ -381,6 +470,15 @@ namespace PepperDash.Core //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 { @@ -463,6 +561,8 @@ namespace PepperDash.Core RetryTimer.Stop(); RetryTimer = null; } + if(AutoReconnectTriggered != null) + AutoReconnectTriggered(this, new EventArgs()); RetryTimer = new CTimer(o => Connect(), rndTime); } } @@ -477,7 +577,7 @@ namespace PepperDash.Core if (numBytes > 0) { string str = string.Empty; - + var handler = TextReceivedQueueInvoke; try { var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray(); @@ -499,20 +599,18 @@ namespace PepperDash.Core Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Shared key confirmed. Ready for communication"); OnClientReadyForcommunications(true); // Successful key exchange } - else if (SharedKeyRequired == false && IsReadyForCommunication == false) - { - OnClientReadyForcommunications(true); // Key not required - } - 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)); + } } } } @@ -520,9 +618,51 @@ namespace PepperDash.Core { Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str); } + if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) + client.ReceiveDataAsync(Receive); + + //Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread. + if (handler != null) + { + var gotLock = DequeueLock.TryEnter(); + if (gotLock) + CrestronInvoke.BeginInvoke((o) => DequeueEvent()); + } + } + else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync + { + client.DisconnectFromServer(); + } + } + + /// + /// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently. + /// It will dequeue items as they are enqueued automatically. + /// + void DequeueEvent() + { + try + { + while (true) + { + // Pull from Queue and fire an event. Block indefinitely until an item can be removed, similar to a Gather. + var message = MessageQueue.Dequeue(); + var handler = TextReceivedQueueInvoke; + if (handler != null) + { + handler(this, message); + } + } + } + catch (Exception e) + { + Debug.Console(0, "DequeueEvent error: {0}\r", e); + } + // Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it. + if (DequeueLock != null) + { + DequeueLock.Leave(); } - if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED) - client.ReceiveDataAsync(Receive); } void HeartbeatStart() diff --git a/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpServer.cs b/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpServer.cs index 58f46c8..3eb325a 100644 --- a/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpServer.cs +++ b/Pepperdash Core/Pepperdash Core/Comm/GenericSecureTcpIpServer.cs @@ -28,6 +28,12 @@ namespace PepperDash.Core /// public event EventHandler TextReceived; + /// + /// Event for Receiving text. Once subscribed to this event the receive callback will start a thread that dequeues the messages and invokes the event on a new thread. + /// It is not recommended to use both the TextReceived event and the TextReceivedQueueInvoke event. + /// + public event EventHandler TextReceivedQueueInvoke; + /// /// Event for client connection socket status change /// @@ -56,10 +62,26 @@ namespace PepperDash.Core #region Properties/Variables /// - /// + /// Server listen lock /// CCriticalSection ServerCCSection = new CCriticalSection(); + /// + /// Queue lock + /// + CCriticalSection DequeueLock = new CCriticalSection(); + + /// + /// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before + /// calling initialize. + /// + public int ReceiveQueueSize { get; set; } + + /// + /// Queue to temporarily store received messages with the source IP and Port info. Defaults to size 20. Use constructor or set queue size property before + /// calling initialize. + /// + private CrestronQueue MessageQueue; /// /// A bandaid client that monitors whether the server is reachable @@ -145,7 +167,9 @@ namespace PepperDash.Core { get { return (ushort)(IsListening ? 1 : 0); } } - + /// + /// Max number of clients this server will allow for connection. Crestron max is 64. This number should be less than 65 + /// public ushort MaxClients { get; set; } // should be set by parameter in SIMPL+ in the MAIN method, Should not ever need to be configurable /// /// Number of clients currently connected. @@ -269,7 +293,7 @@ namespace PepperDash.Core /// constructor S+ Does not accept a key. Use initialze with key to set the debug key on this device. If using with + make sure to set all properties manually. /// public GenericSecureTcpIpServer() - : base("Uninitialized Dynamic TCP Server") + : base("Uninitialized Secure TCP Server") { HeartbeatRequiredIntervalInSeconds = 15; CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); @@ -282,7 +306,7 @@ namespace PepperDash.Core /// /// public GenericSecureTcpIpServer(string key) - : base("Uninitialized Dynamic TCP Server") + : base("Uninitialized Secure TCP Server") { HeartbeatRequiredIntervalInSeconds = 15; CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); @@ -292,11 +316,11 @@ namespace PepperDash.Core } /// - /// Contstructor that sets all properties by calling the initialize method with a config object. + /// Contstructor that sets all properties by calling the initialize method with a config object. This does set Queue size. /// /// public GenericSecureTcpIpServer(TcpServerConfigObject serverConfigObject) - : base("Uninitialized Dynamic TCP Server") + : base("Uninitialized Secure TCP Server") { HeartbeatRequiredIntervalInSeconds = 15; CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); @@ -345,7 +369,8 @@ namespace PepperDash.Core HeartbeatRequiredIntervalInSeconds = serverConfigObject.HeartbeatRequiredIntervalInSeconds; HeartbeatStringToMatch = serverConfigObject.HeartbeatStringToMatch; BufferSize = serverConfigObject.BufferSize; - + ReceiveQueueSize = serverConfigObject.ReceiveQueueSize > 20 ? serverConfigObject.ReceiveQueueSize : 20; + MessageQueue = new CrestronQueue(ReceiveQueueSize); } else { @@ -384,13 +409,14 @@ namespace PepperDash.Core if (SecureServer == null) { SecureServer = new SecureTCPServer(Port, MaxClients); - SecureServer.SocketSendOrReceiveTimeOutInMs = (this.HeartbeatRequiredIntervalMs * 5); + if(HeartbeatRequired) + SecureServer.SocketSendOrReceiveTimeOutInMs = (this.HeartbeatRequiredIntervalMs * 5); SecureServer.HandshakeTimeout = 30; SecureServer.SocketStatusChange += new SecureTCPServerSocketStatusChangeEventHandler(SecureServer_SocketStatusChange); } else { - KillServer(); + //KillServer(); Remove this to be able to reactivate listener if it stops itself due to max clients without disconnecting connected clients. SecureServer.PortNumber = Port; } ServerStopped = false; @@ -415,25 +441,21 @@ namespace PepperDash.Core /// public void StopListening() { - try - { - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Stopping Listener"); - if (SecureServer != null) - { - SecureServer.Stop(); - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", SecureServer.State); - //SecureServer = null; - } - - ServerStopped = true; - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Server Stopped"); - - OnServerStateChange(SecureServer.State); - } - catch (Exception ex) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex); - } + try + { + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stopping Listener"); + if (SecureServer != null) + { + SecureServer.Stop(); + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", SecureServer.State); + OnServerStateChange(SecureServer.State); + } + ServerStopped = true; + } + catch (Exception ex) + { + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex); + } } /// @@ -445,11 +467,11 @@ namespace PepperDash.Core try { SecureServer.Disconnect(client); - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client); + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client); } catch (Exception ex) { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex); + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex); } } /// @@ -469,17 +491,17 @@ namespace PepperDash.Core try { SecureServer.Disconnect(i); - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i); + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i); } catch (Exception ex) { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex); + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex); } } - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", SecureServer.ServerSocketStatus); + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", SecureServer.ServerSocketStatus); } - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients"); + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients"); ConnectedClientsIndexes.Clear(); if (!ProgramIsStopping) @@ -510,7 +532,7 @@ namespace PepperDash.Core { SocketErrorCodes error = SecureServer.SendDataAsync(i, b, b.Length, (x, y, z) => { }); if (error != SocketErrorCodes.SOCKET_OK && error != SocketErrorCodes.SOCKET_OPERATION_PENDING) - Debug.Console(0, error.ToString()); + Debug.Console(2, error.ToString()); } } } @@ -541,7 +563,7 @@ namespace PepperDash.Core } catch (Exception ex) { - Debug.Console(0, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text); + Debug.Console(2, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text); } } @@ -669,13 +691,18 @@ namespace PepperDash.Core } if (ClientReadyAfterKeyExchange.Contains(clientIndex)) ClientReadyAfterKeyExchange.Remove(clientIndex); + if (WaitingForSharedKey.Contains(clientIndex)) + WaitingForSharedKey.Remove(clientIndex); } } catch (Exception ex) { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex); + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex); } - onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); + //Use a thread for this event so that the server state updates to listening while this event is processed. Listening must be added to the server state + //after every client connection so that the server can check and see if it is at max clients. Due to this the event fires and server listening enum bit flag + //is not set. Putting in a thread allows the state to update before this event processes so that the subscribers to this event get accurate isListening in the event. + CrestronInvoke.BeginInvoke(o => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)), null); } #endregion @@ -739,7 +766,7 @@ namespace PepperDash.Core } catch (Exception ex) { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex); + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex); } //Debug.Console(1, this, Debug.ErrorLogLevel, "((((((Server State bitfield={0}; maxclient={1}; ServerStopped={2}))))))", // server.State, @@ -767,6 +794,7 @@ namespace PepperDash.Core if (numberOfBytesReceived > 0) { string received = "Nothing"; + var handler = TextReceivedQueueInvoke; try { byte[] bytes = mySecureTCPServer.GetIncomingDataBufferForSpecificClient(clientIndex); @@ -781,31 +809,74 @@ namespace PepperDash.Core Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received); mySecureTCPServer.SendData(clientIndex, b, b.Length); mySecureTCPServer.Disconnect(clientIndex); - WaitingForSharedKey.Remove(clientIndex); + return; } - if (mySecureTCPServer.NumberOfClientsConnected > 0) - mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback); + WaitingForSharedKey.Remove(clientIndex); byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match"); mySecureTCPServer.SendDataAsync(clientIndex, success, success.Length, null); OnServerClientReadyForCommunications(clientIndex); - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client with index {0} provided the shared key and successfully connected to the server", clientIndex); - return; + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client with index {0} provided the shared key and successfully connected to the server", clientIndex); } - //var address = mySecureTCPServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); - //Debug.Console(1, this, "Secure Server Listening on Port: {0}, client IP: {1}, Client Index: {4}, NumberOfBytesReceived: {2}, Received: {3}\r\n", - // mySecureTCPServer.PortNumber.ToString(), address , numberOfBytesReceived.ToString(), received, clientIndex.ToString()); - if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received))) + else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received))) + { onTextReceived(received, clientIndex); + if (handler != null) + { + MessageQueue.TryToEnqueue(new GenericTcpServerCommMethodReceiveTextArgs(received, clientIndex)); + } + } } catch (Exception ex) { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex); + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex); + } + if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) + mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback); + + //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()); } } - if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) - mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback); + else + { + mySecureTCPServer.Disconnect(clientIndex); + } + } + + /// + /// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently. + /// It will dequeue items as they are enqueued automatically. + /// + void DequeueEvent() + { + try + { + while (true) + { + // Pull from Queue and fire an event. Block indefinitely until an item can be removed, similar to a Gather. + var message = MessageQueue.Dequeue(); + var handler = TextReceivedQueueInvoke; + if (handler != null) + { + handler(this, message); + } + } + } + catch (Exception e) + { + Debug.Console(2, "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(); + } } #endregion @@ -965,13 +1036,13 @@ namespace PepperDash.Core StopMonitorClient(); if (MonitorClientFailureCount < MonitorClientMaxFailureCount) { - Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}", + Debug.Console(2, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}", MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount); StartMonitorClient(); } else { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "\r***************************\rMonitor client connection has hung a maximum of {0} times. \r***************************", MonitorClientMaxFailureCount); diff --git a/Pepperdash Core/Pepperdash Core/Comm/GenericSshClient.cs b/Pepperdash Core/Pepperdash Core/Comm/GenericSshClient.cs index 3d1101a..6af5d7b 100644 --- a/Pepperdash Core/Pepperdash Core/Comm/GenericSshClient.cs +++ b/Pepperdash Core/Pepperdash Core/Comm/GenericSshClient.cs @@ -174,8 +174,8 @@ namespace PepperDash.Core if (Client != null) { Debug.Console(1, this, "Program stopping. Closing connection"); - Client.Disconnect(); - Client.Dispose(); + Disconnect(); + //Client.Dispose(); } } } diff --git a/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpClient.cs b/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpClient.cs index 65c428f..525f32b 100644 --- a/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpClient.cs +++ b/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpClient.cs @@ -29,10 +29,27 @@ namespace PepperDash.Core //public event GenericSocketStatusChangeEventDelegate SocketStatusChange; public event EventHandler ConnectionChange; + + private string _Hostname { get; set;} /// /// Address of server /// - public string Hostname { get; set; } + public string Hostname { + get + { + return _Hostname; + } + + set + { + _Hostname = value; + if (Client != null) + { + + Client.AddressClientConnectedTo = _Hostname; + } + } + } /// /// Port on server @@ -233,10 +250,13 @@ namespace PepperDash.Core if (Client == null) { + + Client = new TCPClient(Hostname, Port, BufferSize); Client.SocketStatusChange += Client_SocketStatusChange; } DisconnectCalledByUser = false; + Client.ConnectToServerAsync(ConnectToServerCallback); // (null); } @@ -352,7 +372,6 @@ namespace PepperDash.Core } } - public class TcpSshPropertiesConfig { [JsonProperty(Required = Required.Always)] diff --git a/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpClient_ForServer.cs b/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpClient_ForServer.cs index 820f5bb..ec59259 100644 --- a/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpClient_ForServer.cs +++ b/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpClient_ForServer.cs @@ -328,7 +328,8 @@ namespace PepperDash.Core Client = new TCPClient(Hostname, Port, BufferSize); Client.SocketStatusChange += Client_SocketStatusChange; - Client.SocketSendOrReceiveTimeOutInMs = (HeartbeatInterval * 5); + if(HeartbeatEnabled) + Client.SocketSendOrReceiveTimeOutInMs = (HeartbeatInterval * 5); Client.AddressClientConnectedTo = Hostname; Client.PortNumber = Port; // SecureClient = c; @@ -381,6 +382,15 @@ namespace PepperDash.Core //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 { @@ -485,7 +495,6 @@ namespace PepperDash.Core Debug.Console(2, this, "Client Received:\r--------\r{0}\r--------", str); if (!string.IsNullOrEmpty(checkHeartbeat(str))) { - if (SharedKeyRequired && str == "SharedKey:") { Debug.Console(1, this, "Server asking for shared key, sending"); @@ -494,19 +503,11 @@ namespace PepperDash.Core else if (SharedKeyRequired && str == "Shared Key Match") { StopWaitForSharedKeyTimer(); - - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Shared key confirmed. Ready for communication"); OnClientReadyForcommunications(true); // Successful key exchange } - else if (SharedKeyRequired == false && IsReadyForCommunication == false) - { - OnClientReadyForcommunications(true); // Key not required - } - else { - //var bytesHandler = BytesReceived; //if (bytesHandler != null) // bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); @@ -698,6 +699,7 @@ namespace PepperDash.Core Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "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) { diff --git a/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpServer.cs b/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpServer.cs index 2ef2845..443a69a 100644 --- a/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpServer.cs +++ b/Pepperdash Core/Pepperdash Core/Comm/GenericTcpIpServer.cs @@ -384,16 +384,20 @@ namespace PepperDash.Core if (myTcpServer == null) { myTcpServer = new TCPServer(Port, MaxClients); - myTcpServer.SocketSendOrReceiveTimeOutInMs = (this.HeartbeatRequiredIntervalMs * 5); + if(HeartbeatRequired) + myTcpServer.SocketSendOrReceiveTimeOutInMs = (this.HeartbeatRequiredIntervalMs * 5); // myTcpServer.HandshakeTimeout = 30; - myTcpServer.SocketStatusChange += new TCPServerSocketStatusChangeEventHandler(TcpServer_SocketStatusChange); } else { KillServer(); myTcpServer.PortNumber = Port; } + + myTcpServer.SocketStatusChange -= TcpServer_SocketStatusChange; + myTcpServer.SocketStatusChange += TcpServer_SocketStatusChange; + ServerStopped = false; myTcpServer.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback); OnServerStateChange(myTcpServer.State); @@ -418,22 +422,18 @@ namespace PepperDash.Core { try { - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Stopping Listener"); + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stopping Listener"); if (myTcpServer != null) { myTcpServer.Stop(); - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", myTcpServer.State); - //SecureServer = null; + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", myTcpServer.State); + OnServerStateChange(myTcpServer.State); } - - ServerStopped = true; - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Server Stopped"); - - OnServerStateChange(myTcpServer.State); + ServerStopped = true; } catch (Exception ex) { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex); + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex); } } @@ -446,11 +446,11 @@ namespace PepperDash.Core try { myTcpServer.Disconnect(client); - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client); + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client); } catch (Exception ex) { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex); + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex); } } /// @@ -470,17 +470,17 @@ namespace PepperDash.Core try { myTcpServer.Disconnect(i); - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i); + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i); } catch (Exception ex) { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex); + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex); } } - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", myTcpServer.ServerSocketStatus); + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", myTcpServer.ServerSocketStatus); } - Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients"); + Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients"); ConnectedClientsIndexes.Clear(); if (!ProgramIsStopping) @@ -511,7 +511,7 @@ namespace PepperDash.Core { SocketErrorCodes error = myTcpServer.SendDataAsync(i, b, b.Length, (x, y, z) => { }); if (error != SocketErrorCodes.SOCKET_OK && error != SocketErrorCodes.SOCKET_OPERATION_PENDING) - Debug.Console(0, error.ToString()); + Debug.Console(2, error.ToString()); } } } @@ -542,7 +542,7 @@ namespace PepperDash.Core } catch (Exception ex) { - Debug.Console(0, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text); + Debug.Console(2, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text); } } @@ -670,11 +670,13 @@ namespace PepperDash.Core } if (ClientReadyAfterKeyExchange.Contains(clientIndex)) ClientReadyAfterKeyExchange.Remove(clientIndex); + if (WaitingForSharedKey.Contains(clientIndex)) + WaitingForSharedKey.Remove(clientIndex); } } catch (Exception ex) { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex); + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex); } onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)); } @@ -740,7 +742,7 @@ namespace PepperDash.Core } catch (Exception ex) { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex); + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex); } //Debug.Console(1, this, Debug.ErrorLogLevel, "((((((Server State bitfield={0}; maxclient={1}; ServerStopped={2}))))))", // server.State, @@ -763,50 +765,51 @@ namespace PepperDash.Core /// /// /// - void TcpServerReceivedDataAsyncCallback(TCPServer mySecureTCPServer, uint clientIndex, int numberOfBytesReceived) + void TcpServerReceivedDataAsyncCallback(TCPServer myTCPServer, uint clientIndex, int numberOfBytesReceived) { - if (numberOfBytesReceived > 0) - { - string received = "Nothing"; - try - { - 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(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received); - mySecureTCPServer.SendData(clientIndex, b, b.Length); - mySecureTCPServer.Disconnect(clientIndex); - WaitingForSharedKey.Remove(clientIndex); - return; - } - if (mySecureTCPServer.NumberOfClientsConnected > 0) - mySecureTCPServer.ReceiveDataAsync(clientIndex, TcpServerReceivedDataAsyncCallback); - WaitingForSharedKey.Remove(clientIndex); - byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match"); - mySecureTCPServer.SendDataAsync(clientIndex, success, success.Length, null); - OnServerClientReadyForCommunications(clientIndex); - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client with index {0} provided the shared key and successfully connected to the server", clientIndex); - return; - } - //var address = mySecureTCPServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex); - //Debug.Console(1, this, "Secure Server Listening on Port: {0}, client IP: {1}, Client Index: {4}, NumberOfBytesReceived: {2}, Received: {3}\r\n", - // mySecureTCPServer.PortNumber.ToString(), address , numberOfBytesReceived.ToString(), received, clientIndex.ToString()); - if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received))) - onTextReceived(received, clientIndex); - } - catch (Exception ex) - { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex); - } - } - if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) - mySecureTCPServer.ReceiveDataAsync(clientIndex, TcpServerReceivedDataAsyncCallback); + if (numberOfBytesReceived > 0) + { + string received = "Nothing"; + try + { + 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(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received); + myTCPServer.SendData(clientIndex, b, b.Length); + myTCPServer.Disconnect(clientIndex); + return; + } + + WaitingForSharedKey.Remove(clientIndex); + byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match"); + myTCPServer.SendDataAsync(clientIndex, success, success.Length, null); + OnServerClientReadyForCommunications(clientIndex); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client with index {0} provided the shared key and successfully connected to the server", clientIndex); + } + + else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received))) + onTextReceived(received, clientIndex); + } + catch (Exception ex) + { + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex); + } + if (myTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED) + myTCPServer.ReceiveDataAsync(clientIndex, TcpServerReceivedDataAsyncCallback); + } + else + { + // If numberOfBytesReceived <= 0 + myTCPServer.Disconnect(); + } + } #endregion @@ -966,13 +969,13 @@ namespace PepperDash.Core StopMonitorClient(); if (MonitorClientFailureCount < MonitorClientMaxFailureCount) { - Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}", + Debug.Console(2, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}", MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount); StartMonitorClient(); } else { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, + Debug.Console(2, this, Debug.ErrorLogLevel.Error, "\r***************************\rMonitor client connection has hung a maximum of {0} times.\r***************************", MonitorClientMaxFailureCount); diff --git a/Pepperdash Core/Pepperdash Core/Comm/GenericUdpServer.cs b/Pepperdash Core/Pepperdash Core/Comm/GenericUdpServer.cs index a67a05f..011c17c 100644 --- a/Pepperdash Core/Pepperdash Core/Comm/GenericUdpServer.cs +++ b/Pepperdash Core/Pepperdash Core/Comm/GenericUdpServer.cs @@ -1,231 +1,351 @@ -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 -{ - public class GenericUdpServer : Device, IBasicCommunication - { - /// - /// - /// - public event EventHandler BytesReceived; - - /// - /// - /// - public event EventHandler TextReceived; - - /// - /// - /// - //public event GenericSocketStatusChangeEventDelegate SocketStatusChange; - public event EventHandler ConnectionChange; - - public SocketStatus ClientStatus - { - get - { - return Server.ServerStatus; - } - } - - /// - /// Address of server - /// - public string Hostname { get; set; } - - /// - /// Port on server - /// - public int Port { get; set; } - - /// - /// Another damn S+ helper because S+ seems to treat large port nums as signed ints - /// which screws up things - /// - public ushort UPort - { - get { return Convert.ToUInt16(Port); } - set { Port = Convert.ToInt32(value); } - } - - /// - /// Indicates that the UDP Server is enabled - /// - public bool IsConnected - { - get; - private set; - } - - /// - /// Defaults to 2000 - /// - public int BufferSize { get; set; } - - public UDPServer Server { get; private set; } - - public GenericUdpServer(string key, string address, int port, int buffefSize) - : base(key) - { - Hostname = address; - Port = port; - BufferSize = buffefSize; - - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler); - } - - 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(); - } - } - - void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) - { - if (programEventType == eProgramStatusEventType.Stopping) - { - Debug.Console(1, this, "Program stopping. Disabling Server"); - Disconnect(); - } - } - - /// - /// Enables the UDP Server - /// - 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; - - // Start receiving data - Server.ReceiveDataAsync(Receive); - } - - /// - /// Disabled the UDP Server - /// - public void Disconnect() - { - if(Server != null) - Server.DisableUDPServer(); - - IsConnected = false; - } - - - /// - /// Recursive method to receive data - /// - /// - /// - void Receive(UDPServer server, int numBytes) - { - Debug.Console(2, this, "Received {0} bytes", numBytes); - - if (numBytes > 0) - { - var bytes = server.IncomingDataBuffer.Take(numBytes).ToArray(); - - Debug.Console(2, this, "Bytes: {0}", bytes.ToString()); - var bytesHandler = BytesReceived; - if (bytesHandler != null) - bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); - else - Debug.Console(2, this, "bytesHandler is null"); - var textHandler = TextReceived; - if (textHandler != null) - { - var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); - Debug.Console(2, this, "RX: {0}", str); - textHandler(this, new GenericCommMethodReceiveTextArgs(str)); - } - else - Debug.Console(2, this, "textHandler is null"); - } - server.ReceiveDataAsync(Receive); - } - - /// - /// General send method - /// - /// - public void SendText(string text) - { - var bytes = Encoding.GetEncoding(28591).GetBytes(text); - - if (IsConnected && Server != null) - { - Debug.Console(2, this, "TX: {0}", text); - Server.SendData(bytes, bytes.Length); - } - } - - public void SendBytes(byte[] bytes) - { - //if (Debug.Level == 2) - // Debug.Console(2, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); - if (IsConnected && Server != null) - Server.SendData(bytes, bytes.Length); - } - - - - } - - public class UdpServerPropertiesConfig - { - [JsonProperty(Required = Required.Always)] - public string Address { get; set; } - - [JsonProperty(Required = Required.Always)] - public int Port { get; set; } - - /// - /// Defaults to 32768 - /// - public int BufferSize { get; set; } - - public UdpServerPropertiesConfig() - { - BufferSize = 32768; - } - } +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 +{ + public class GenericUdpServer : Device, IBasicCommunication + { + /// + /// + /// + public event EventHandler BytesReceived; + + /// + /// + /// + public event EventHandler TextReceived; + + /// + /// 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. + /// + public event EventHandler DataRecievedExtra; + + /// + /// Queue to temporarily store received messages with the source IP and Port info + /// + private CrestronQueue MessageQueue; + + /// + /// + /// + //public event GenericSocketStatusChangeEventDelegate SocketStatusChange; + public event EventHandler ConnectionChange; + + public event EventHandler UpdateConnectionStatus; + + public SocketStatus ClientStatus + { + get + { + return Server.ServerStatus; + } + } + + public ushort UStatus + { + get { return (ushort)Server.ServerStatus; } + } + + + CCriticalSection DequeueLock; + /// + /// Address of server + /// + public string Hostname { get; set; } + + /// + /// IP Address of the sender of the last recieved message + /// + + + /// + /// Port on server + /// + public int Port { get; set; } + + /// + /// Another damn S+ helper because S+ seems to treat large port nums as signed ints + /// which screws up things + /// + public ushort UPort + { + get { return Convert.ToUInt16(Port); } + set { Port = Convert.ToInt32(value); } + } + + /// + /// Indicates that the UDP Server is enabled + /// + public bool IsConnected + { + get; + private set; + } + + public ushort UIsConnected + { + get { return IsConnected ? (ushort)1 : (ushort)0; } + } + + /// + /// Defaults to 2000 + /// + public int BufferSize { get; set; } + + public UDPServer Server { get; private set; } + + /// + /// Constructor for S+. Make sure to set key, address, port, and buffersize using init method + /// + public GenericUdpServer() + : base("Uninitialized Udp Server") + { + BufferSize = 5000; + DequeueLock = new CCriticalSection(); + MessageQueue = new CrestronQueue(); + + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler); + } + + public GenericUdpServer(string key, string address, int port, int buffefSize) + : base(key) + { + Hostname = address; + Port = port; + BufferSize = buffefSize; + + DequeueLock = new CCriticalSection(); + MessageQueue = new CrestronQueue(); + + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler); + } + + public void Initialize(string key, string address, ushort port) + { + Key = key; + Hostname = address; + UPort = port; + } + + 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(); + } + } + + void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType == eProgramStatusEventType.Stopping) + { + Debug.Console(1, this, "Program stopping. Disabling Server"); + Disconnect(); + } + } + + /// + /// Enables the UDP Server + /// + 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); + } + + /// + /// Disabled the UDP Server + /// + public void Disconnect() + { + if(Server != null) + Server.DisableUDPServer(); + + IsConnected = false; + + var handler = UpdateConnectionStatus; + if (handler != null) + handler(this, new GenericUdpConnectedEventArgs(UIsConnected)); + } + + + /// + /// Recursive method to receive data + /// + /// + /// + void Receive(UDPServer server, int numBytes) + { + Debug.Console(2, this, "Received {0} bytes", numBytes); + + if (numBytes > 0) + { + 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); + MessageQueue.TryToEnqueue(new GenericUdpReceiveTextExtraArgs(str, sourceIp, sourcePort, bytes)); + + Debug.Console(2, this, "Bytes: {0}", bytes.ToString()); + var bytesHandler = BytesReceived; + if (bytesHandler != null) + bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); + else + Debug.Console(2, this, "bytesHandler is null"); + var textHandler = TextReceived; + if (textHandler != null) + { + + Debug.Console(2, this, "RX: {0}", str); + textHandler(this, new GenericCommMethodReceiveTextArgs(str)); + } + else + Debug.Console(2, this, "textHandler is null"); + } + server.ReceiveDataAsync(Receive); + + // Attempt to enter the CCritical Secion and if we can, start the dequeue thread + var gotLock = DequeueLock.TryEnter(); + if (gotLock) + CrestronInvoke.BeginInvoke((o) => DequeueEvent()); + } + + /// + /// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently. + /// It will dequeue items as they are enqueued automatically. + /// + void DequeueEvent() + { + try + { + while (true) + { + // Pull from Queue and fire an event. Block indefinitely until an item can be removed, similar to a Gather. + var message = MessageQueue.Dequeue(); + var dataRecivedExtra = DataRecievedExtra; + if (dataRecivedExtra != null) + { + dataRecivedExtra(this, message); + } + } + } + catch (Exception e) + { + Debug.Console(0, "GenericUdpServer 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(); + } + } + + /// + /// General send method + /// + /// + public void SendText(string text) + { + var bytes = Encoding.GetEncoding(28591).GetBytes(text); + + if (IsConnected && Server != null) + { + Debug.Console(2, this, "TX: {0}", text); + Server.SendData(bytes, bytes.Length); + } + } + + public void SendBytes(byte[] bytes) + { + //if (Debug.Level == 2) + // Debug.Console(2, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); + if (IsConnected && Server != null) + Server.SendData(bytes, bytes.Length); + } + + } + + public class GenericUdpReceiveTextExtraArgs : EventArgs + { + public string Text { get; private set; } + public string IpAddress { get; private set; } + public int Port { get; private set; } + public byte[] Bytes { get; private set; } + + public GenericUdpReceiveTextExtraArgs(string text, string ipAddress, int port, byte[] bytes) + { + Text = text; + IpAddress = ipAddress; + Port = port; + Bytes = bytes; + } + + /// + /// Stupid S+ Constructor + /// + public GenericUdpReceiveTextExtraArgs() { } + } + + public class UdpServerPropertiesConfig + { + [JsonProperty(Required = Required.Always)] + public string Address { get; set; } + + [JsonProperty(Required = Required.Always)] + public int Port { get; set; } + + /// + /// Defaults to 32768 + /// + public int BufferSize { get; set; } + + public UdpServerPropertiesConfig() + { + BufferSize = 32768; + } + } } \ No newline at end of file diff --git a/Pepperdash Core/Pepperdash Core/Comm/TcpClientConfigObject.cs b/Pepperdash Core/Pepperdash Core/Comm/TcpClientConfigObject.cs new file mode 100644 index 0000000..a4fe8b2 --- /dev/null +++ b/Pepperdash Core/Pepperdash Core/Comm/TcpClientConfigObject.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; +using PepperDash.Core; +using Newtonsoft.Json; + +namespace PepperDash.Core +{ + /// + /// Client config object for TCP client with server that inherits from TcpSshPropertiesConfig and adds properties for shared key and heartbeat + /// + public class TcpClientConfigObject + { + /// + /// TcpSsh Properties + /// + public ControlPropertiesConfig Control { get; set; } + /// + /// Bool value for secure. Currently not implemented in TCP sockets as they are not dynamic + /// + public bool Secure { get; set; } + /// + /// Require a shared key that both server and client negotiate. If negotiation fails server disconnects the client + /// + public bool SharedKeyRequired { get; set; } + + /// + /// The shared key that must match on the server and client + /// + public string SharedKey { get; set; } + /// + /// Require a heartbeat on the client/server connection that will cause the server/client to disconnect if the heartbeat is not received. + /// heartbeats do not raise received events. + /// + public bool HeartbeatRequired { get; set; } + /// + /// The interval in seconds for the heartbeat from the client. If not received client is disconnected + /// + public ushort HeartbeatRequiredIntervalInSeconds { get; set; } + /// + /// HeartbeatString that will be checked against the message received. defaults to heartbeat if no string is provided. + /// + public string HeartbeatStringToMatch { get; set; } + /// + /// Receive Queue size must be greater than 20 or defaults to 20 + /// + public int ReceiveQueueSize { get; set; } + } +} \ No newline at end of file diff --git a/Pepperdash Core/Pepperdash Core/Comm/TcpServerConfigObject.cs b/Pepperdash Core/Pepperdash Core/Comm/TcpServerConfigObject.cs index c7c1998..785851b 100644 --- a/Pepperdash Core/Pepperdash Core/Comm/TcpServerConfigObject.cs +++ b/Pepperdash Core/Pepperdash Core/Comm/TcpServerConfigObject.cs @@ -6,17 +6,55 @@ using Crestron.SimplSharp; namespace PepperDash.Core { + /// + /// Tcp Server Config object with properties for a tcp server with shared key and heartbeat capabilities + /// public class TcpServerConfigObject { + /// + /// Uique key + /// public string Key { get; set; } - public bool Secure { get; set; } + /// + /// Max Clients that the server will allow to connect. + /// public ushort MaxClients { get; set; } + /// + /// Bool value for secure. Currently not implemented in TCP sockets as they are not dynamic + /// + public bool Secure { get; set; } + /// + /// Port for the server to listen on + /// public int Port { get; set; } + /// + /// Require a shared key that both server and client negotiate. If negotiation fails server disconnects the client + /// public bool SharedKeyRequired { get; set; } + /// + /// The shared key that must match on the server and client + /// public string SharedKey { get; set; } + /// + /// Require a heartbeat on the client/server connection that will cause the server/client to disconnect if the heartbeat is not received. + /// heartbeats do not raise received events. + /// public bool HeartbeatRequired { get; set; } + /// + /// The interval in seconds for the heartbeat from the client. If not received client is disconnected + /// public ushort HeartbeatRequiredIntervalInSeconds { get; set; } + /// + /// HeartbeatString that will be checked against the message received. defaults to heartbeat if no string is provided. + /// public string HeartbeatStringToMatch { get; set; } + /// + /// Client buffer size. See Crestron help. defaults to 2000 if not greater than 2000 + /// public int BufferSize { get; set; } + /// + /// Receive Queue size must be greater than 20 or defaults to 20 + /// + public int ReceiveQueueSize { get; set; } } } \ No newline at end of file diff --git a/Pepperdash Core/Pepperdash Core/Comm/eControlMethods.cs b/Pepperdash Core/Pepperdash Core/Comm/eControlMethods.cs index d4a2028..5a7a6ea 100644 --- a/Pepperdash Core/Pepperdash Core/Comm/eControlMethods.cs +++ b/Pepperdash Core/Pepperdash Core/Comm/eControlMethods.cs @@ -6,6 +6,9 @@ using Crestron.SimplSharp; namespace PepperDash.Core { + /// + /// Crestron Control Methods for a comm object + /// public enum eControlMethod { None = 0, Com, IpId, IpidTcp, IR, Ssh, Tcpip, Telnet, Cresnet, Cec, Udp diff --git a/Pepperdash Core/Pepperdash Core/Config/PortalConfigReader.cs b/Pepperdash Core/Pepperdash Core/Config/PortalConfigReader.cs index 774f8e5..01dc0af 100644 --- a/Pepperdash Core/Pepperdash Core/Config/PortalConfigReader.cs +++ b/Pepperdash Core/Pepperdash Core/Config/PortalConfigReader.cs @@ -67,7 +67,7 @@ namespace PepperDash.Core.Config /// /// /// - static JObject MergeConfigs(JObject doubleConfig) + public static JObject MergeConfigs(JObject doubleConfig) { var system = JObject.FromObject(doubleConfig["system"]); var template = JObject.FromObject(doubleConfig["template"]); @@ -114,28 +114,35 @@ namespace PepperDash.Core.Config /// /// Merges the contents of a base and a delta array, matching the entries on a top-level property /// given by propertyName. Returns a merge of them. Items in the delta array that do not have - /// a matched item in base array will not be merged. + /// a matched item in base array will not be merged. Non keyed system items will replace the template items. /// static JArray MergeArraysOnTopLevelProperty(JArray a1, JArray a2, string propertyName, string path) { var result = new JArray(); - if (a2 == null) - result = a1; + if (a2 == null || a2.Count == 0) // If the system array is null or empty, return the template array + return a1; else if (a1 != null) { - for (int i = 0; i < a1.Count(); i++) - { - var a1Dev = a1[i]; - // Try to get a system device and if found, merge it onto template - var a2Match = a2.FirstOrDefault(t => t[propertyName].Equals(a1Dev[propertyName]));// t.Value("uid") == tmplDev.Value("uid")); - if (a2Match != null) - { - var mergedItem = Merge(a1Dev, a2Match, string.Format("{0}[{1}].", path, i));// Merge(JObject.FromObject(a1Dev), JObject.FromObject(a2Match)); - result.Add(mergedItem); - } - else - result.Add(a1Dev); - } + if (a2[0]["key"] == null) // If the first item in the system array has no key, overwrite the template array + { // with the system array + return a2; + } + else // The arrays are keyed, merge them by key + { + for (int i = 0; i < a1.Count(); i++) + { + var a1Dev = a1[i]; + // Try to get a system device and if found, merge it onto template + var a2Match = a2.FirstOrDefault(t => t[propertyName].Equals(a1Dev[propertyName]));// t.Value("uid") == tmplDev.Value("uid")); + if (a2Match != null) + { + var mergedItem = Merge(a1Dev, a2Match, string.Format("{0}[{1}].", path, i));// Merge(JObject.FromObject(a1Dev), JObject.FromObject(a2Match)); + result.Add(mergedItem); + } + else + result.Add(a1Dev); + } + } } return result; } diff --git a/Pepperdash Core/Pepperdash Core/CoreInterfaces.cs b/Pepperdash Core/Pepperdash Core/CoreInterfaces.cs index 195d6e2..3a5df42 100644 --- a/Pepperdash Core/Pepperdash Core/CoreInterfaces.cs +++ b/Pepperdash Core/Pepperdash Core/CoreInterfaces.cs @@ -6,13 +6,25 @@ using Crestron.SimplSharp; namespace PepperDash.Core { + /// + /// Unique key interface to require a unique key for the class + /// public interface IKeyed { + /// + /// Unique Key + /// string Key { get; } } + /// + /// Named Keyed device interface. Forces the devie to have a Unique Key and a name. + /// public interface IKeyName : IKeyed { + /// + /// Isn't it obvious :) + /// string Name { get; } } } \ No newline at end of file diff --git a/Pepperdash Core/Pepperdash Core/Device.cs b/Pepperdash Core/Pepperdash Core/Device.cs index 7132f94..62dc6b1 100644 --- a/Pepperdash Core/Pepperdash Core/Device.cs +++ b/Pepperdash Core/Pepperdash Core/Device.cs @@ -10,8 +10,17 @@ namespace PepperDash.Core /// public class Device : IKeyName { + /// + /// Unique Key + /// public string Key { get; protected set; } - public string Name { get; protected set; } + /// + /// Name of the devie + /// + public string Name { get; protected set; } + /// + /// + /// public bool Enabled { get; protected set; } ///// diff --git a/Pepperdash Core/Pepperdash Core/JsonToSimpl/EventArgs and Constants.cs b/Pepperdash Core/Pepperdash Core/JsonToSimpl/EventArgs and Constants.cs index e6e13a5..a94eac1 100644 --- a/Pepperdash Core/Pepperdash Core/JsonToSimpl/EventArgs and Constants.cs +++ b/Pepperdash Core/Pepperdash Core/JsonToSimpl/EventArgs and Constants.cs @@ -6,6 +6,9 @@ using Crestron.SimplSharp; namespace PepperDash.Core.JsonToSimpl { + /// + /// Constants for Simpl modules + /// public class JsonToSimplConstants { public const ushort JsonIsValidBoolChange = 2; @@ -13,6 +16,7 @@ namespace PepperDash.Core.JsonToSimpl public const ushort BoolValueChange = 1; public const ushort UshortValueChange = 101; public const ushort StringValueChange = 201; + public const ushort FullPathToArrayChange = 202; } //**************************************************************************************************// diff --git a/Pepperdash Core/Pepperdash Core/JsonToSimpl/JsonToSimplArrayLookupChild.cs b/Pepperdash Core/Pepperdash Core/JsonToSimpl/JsonToSimplArrayLookupChild.cs index 753dbc3..56ef04a 100644 --- a/Pepperdash Core/Pepperdash Core/JsonToSimpl/JsonToSimplArrayLookupChild.cs +++ b/Pepperdash Core/Pepperdash Core/JsonToSimpl/JsonToSimplArrayLookupChild.cs @@ -11,10 +11,19 @@ namespace PepperDash.Core.JsonToSimpl public class JsonToSimplArrayLookupChild : JsonToSimplChildObjectBase { public string SearchPropertyName { get; set; } - public string SearchPropertyValue { get; set; } + public string SearchPropertyValue { get; set; } int ArrayIndex; + /// + /// For <2.4.1 array lookups + /// + /// + /// + /// + /// + /// + /// public void Initialize(string file, string key, string pathPrefix, string pathSuffix, string searchPropertyName, string searchPropertyValue) { @@ -24,6 +33,28 @@ namespace PepperDash.Core.JsonToSimpl } + /// + /// For newer >=2.4.1 array lookups. + /// + /// + /// + /// + /// + /// + /// + /// + public void InitializeWithAppend(string file, string key, string pathPrefix, string pathAppend, + string pathSuffix, string searchPropertyName, string searchPropertyValue) + { + string pathPrefixWithAppend = (pathPrefix != null ? pathPrefix : "") + GetPathAppend(pathAppend); + base.Initialize(file, key, pathPrefixWithAppend, pathSuffix); + + SearchPropertyName = searchPropertyName; + SearchPropertyValue = searchPropertyValue; + } + + + //PathPrefix+ArrayName+[x]+path+PathSuffix /// /// @@ -32,9 +63,10 @@ namespace PepperDash.Core.JsonToSimpl /// protected override string GetFullPath(string path) { - return string.Format("{0}[{1}].{2}{3}", - PathPrefix == null ? "" : PathPrefix, - ArrayIndex, path, + return string.Format("{0}[{1}].{2}{3}", + PathPrefix == null ? "" : PathPrefix, + ArrayIndex, + path, PathSuffix == null ? "" : PathSuffix); } @@ -44,6 +76,30 @@ namespace PepperDash.Core.JsonToSimpl base.ProcessAll(); } + /// + /// Provides the path append for GetFullPath + /// + /// + string GetPathAppend(string a) + { + if (string.IsNullOrEmpty(a)) + { + return ""; + } + if (a.StartsWith(".")) + { + return a; + } + else + { + return "." + a; + } + } + + /// + /// + /// + /// bool FindInArray() { if (Master == null) @@ -51,41 +107,45 @@ namespace PepperDash.Core.JsonToSimpl if (Master.JsonObject == null) throw new InvalidOperationException("Cannot do operations before master JSON has read"); if (PathPrefix == null) - throw new InvalidOperationException("Cannot do operations before PathPrefix is set"); - - var token = Master.JsonObject.SelectToken(PathPrefix); - if (token is JArray) - { - var array = token as JArray; - try - { - var item = array.FirstOrDefault(o => - { - var prop = o[SearchPropertyName]; - return prop != null && prop.Value() - .Equals(SearchPropertyValue, StringComparison.OrdinalIgnoreCase); - }); - if (item == null) - { - Debug.Console(1, "JSON Child[{0}] Array '{1}' '{2}={3}' not found: ", Key, - PathPrefix, SearchPropertyName, SearchPropertyValue); - this.LinkedToObject = false; - return false; - } - - this.LinkedToObject = true; - ArrayIndex = array.IndexOf(item); - Debug.Console(1, "JSON Child[{0}] Found array match at index {1}", Key, ArrayIndex); - return true; - } - catch (Exception e) - { - Debug.Console(1, "JSON Child[{0}] Array '{1}' lookup error: '{2}={3}'\r{4}", Key, - PathPrefix, SearchPropertyName, SearchPropertyValue, e); - } + throw new InvalidOperationException("Cannot do operations before PathPrefix is set"); + + + var token = Master.JsonObject.SelectToken(PathPrefix); + if (token is JArray) + { + var array = token as JArray; + try + { + var item = array.FirstOrDefault(o => + { + var prop = o[SearchPropertyName]; + return prop != null && prop.Value() + .Equals(SearchPropertyValue, StringComparison.OrdinalIgnoreCase); + }); + if (item == null) + { + Debug.Console(1, "JSON Child[{0}] Array '{1}' '{2}={3}' not found: ", Key, + PathPrefix, SearchPropertyName, SearchPropertyValue); + this.LinkedToObject = false; + return false; + } + + this.LinkedToObject = true; + ArrayIndex = array.IndexOf(item); + OnStringChange(string.Format("{0}[{1}]", PathPrefix, ArrayIndex), 0, JsonToSimplConstants.FullPathToArrayChange); + Debug.Console(1, "JSON Child[{0}] Found array match at index {1}", Key, ArrayIndex); + return true; + } + catch (Exception e) + { + Debug.Console(1, "JSON Child[{0}] Array '{1}' lookup error: '{2}={3}'\r{4}", Key, + PathPrefix, SearchPropertyName, SearchPropertyValue, e); + } + } + else + { + Debug.Console(1, "JSON Child[{0}] Path '{1}' is not an array", Key, PathPrefix); } - else - Debug.Console(1, "JSON Child[{0}] Path '{1}' is not an array", Key, PathPrefix); return false; } diff --git a/Pepperdash Core/Pepperdash Core/JsonToSimpl/JsonToSimplChildObjectBase.cs b/Pepperdash Core/Pepperdash Core/JsonToSimpl/JsonToSimplChildObjectBase.cs index cbed005..ec855f9 100644 --- a/Pepperdash Core/Pepperdash Core/JsonToSimpl/JsonToSimplChildObjectBase.cs +++ b/Pepperdash Core/Pepperdash Core/JsonToSimpl/JsonToSimplChildObjectBase.cs @@ -28,7 +28,7 @@ namespace PepperDash.Core.JsonToSimpl /// This will be prepended to all paths to allow path swapping or for more organized /// sub-paths /// - public string PathPrefix { get; protected set; } + public string PathPrefix { get; protected set; } /// /// This is added to the end of all paths @@ -285,7 +285,7 @@ namespace PepperDash.Core.JsonToSimpl protected virtual string GetFullPath(string path) { return (PathPrefix != null ? PathPrefix : "") + - path + (PathSuffix != null ? PathSuffix : ""); + path + (PathSuffix != null ? PathSuffix : ""); } // Helpers for events diff --git a/Pepperdash Core/Pepperdash Core/JsonToSimpl/JsonToSimplMaster.cs b/Pepperdash Core/Pepperdash Core/JsonToSimpl/JsonToSimplMaster.cs index 4818711..bd48fa9 100644 --- a/Pepperdash Core/Pepperdash Core/JsonToSimpl/JsonToSimplMaster.cs +++ b/Pepperdash Core/Pepperdash Core/JsonToSimpl/JsonToSimplMaster.cs @@ -106,10 +106,10 @@ namespace PepperDash.Core.JsonToSimpl /// public void AddChild(JsonToSimplChildObjectBase child) { - if (Children.Contains(child)) { - Children.Remove(child); - } - Children.Add(child); + if (!Children.Contains(child)) + { + Children.Add(child); + } } /// diff --git a/Pepperdash Core/Pepperdash Core/Logging/Debug.cs b/Pepperdash Core/Pepperdash Core/Logging/Debug.cs index ef7e927..14da95e 100644 --- a/Pepperdash Core/Pepperdash Core/Logging/Debug.cs +++ b/Pepperdash Core/Pepperdash Core/Logging/Debug.cs @@ -1,393 +1,421 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using Crestron.SimplSharp; -using Crestron.SimplSharp.CrestronLogger; -using Crestron.SimplSharp.CrestronIO; -using Newtonsoft.Json; -using PepperDash.Core.DebugThings; - - -namespace PepperDash.Core -{ - public static class Debug - { - /// - /// Describes the folder location where a given program stores it's debug level memory. By default, the - /// file written will be named appNdebug where N is 1-10. - /// - public static string FilePathPrefix = @"\nvram\debug\"; - - /// - /// The name of the file containing the current debug settings. - /// - public static string FileName = string.Format(@"app{0}Debug.json", InitialParametersClass.ApplicationNumber); - - public static int Level { get; private set; } - - static DebugContextCollection Contexts; - - static int SaveTimeoutMs = 30000; - - static CTimer SaveTimer; - - /// - /// When true, the IncludedExcludedKeys dict will contain keys to include. - /// When false (default), IncludedExcludedKeys will contain keys to exclude. - /// - static bool ExcludeAllMode; - - //static bool ExcludeNoKeyMessages; - - static Dictionary IncludedExcludedKeys; - - static Debug() - { - IncludedExcludedKeys = new Dictionary(); - - //CrestronDataStoreStatic.InitCrestronDataStore(); - if (CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro) - { - // Add command to console - CrestronConsole.AddNewConsoleCommand(SetDebugFromConsole, "appdebug", - "appdebug:P [0-2]: Sets the application's console debug message level", - ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(ShowDebugLog, "appdebuglog", - "appdebuglog:P [all] Use \"all\" for full log.", - ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(s => CrestronLogger.Clear(false), "appdebugclear", - "appdebugclear:P Clears the current custom log", - ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(SetDebugFilterFromConsole, "appdebugfilter", - "appdebugfilter [params]", ConsoleAccessLevelEnum.AccessOperator); - } - - CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; - - LoadMemory(); - Level = Contexts.GetOrCreateItem("DEFAULT").Level; - - CrestronLogger.Initialize(2, LoggerModeEnum.RM); // Use RM instead of DEFAULT as not to double-up console messages. - } - - /// - /// Used to save memory when shutting down - /// - /// - static void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) - { - if (programEventType == eProgramStatusEventType.Stopping) - { - if (SaveTimer != null) - { - SaveTimer.Stop(); - SaveTimer = null; - } - Console(0, "Saving debug settings"); - SaveMemory(); - } - } - - /// - /// Callback for console command - /// - /// - public static void SetDebugFromConsole(string levelString) - { - try - { - if (string.IsNullOrEmpty(levelString.Trim())) - { - CrestronConsole.PrintLine("AppDebug level = {0}", Level); - return; - } - - SetDebugLevel(Convert.ToInt32(levelString)); - } - catch - { - CrestronConsole.PrintLine("Usage: appdebug:P [0-2]"); - } - } - - public static void SetDebugFilterFromConsole(string items) - { - var str = items.Trim(); - if (str == "?") - { - CrestronConsole.ConsoleCommandResponse("Usage:\r APPDEBUGFILTER key1 key2 key3....\r " + - "+all: at beginning puts filter into 'default include' mode\r" + - " All keys that follow will be excluded from output.\r" + - "-all: at beginning puts filter into 'default excluse all' mode.\r" + - " All keys that follow will be the only keys that are shown\r" + - "+nokey: Enables messages with no key (default)\r" + - "-nokey: Disables messages with no key.\r" + - "(nokey settings are independent of all other settings)"); - return; - } - var keys = Regex.Split(str, @"\s*"); - foreach (var keyToken in keys) - { - var lkey = keyToken.ToLower(); - if (lkey == "+all") - { - IncludedExcludedKeys.Clear(); - ExcludeAllMode = false; - } - else if (lkey == "-all") - { - IncludedExcludedKeys.Clear(); - ExcludeAllMode = true; - } - //else if (lkey == "+nokey") - //{ - // ExcludeNoKeyMessages = false; - //} - //else if (lkey == "-nokey") - //{ - // ExcludeNoKeyMessages = true; - //} - else - { - string key = null; ; - if (lkey.StartsWith("-")) - { - key = lkey.Substring(1); - // if in exclude all mode, we need to remove this from the inclusions - if (ExcludeAllMode) - { - if (IncludedExcludedKeys.ContainsKey(key)) - IncludedExcludedKeys.Remove(key); - } - // otherwise include all mode, add to the exclusions - else - { - IncludedExcludedKeys[key] = new object(); - } - } - else if (lkey.StartsWith("+")) - { - key = lkey.Substring(1); - // if in exclude all mode, we need to add this as inclusion - if (ExcludeAllMode) - { - - IncludedExcludedKeys[key] = new object(); - } - // otherwise include all mode, remove this from exclusions - else - { - if (IncludedExcludedKeys.ContainsKey(key)) - IncludedExcludedKeys.Remove(key); - } - } - } - } - } - - - /// - /// Sets the debug level - /// - /// Valid values 0 (no debug), 1 (critical), 2 (all messages) - public static void SetDebugLevel(int level) - { - if (level <= 2) - { - Level = level; - Contexts.GetOrCreateItem("DEFAULT").Level = level; - SaveMemoryOnTimeout(); - - CrestronConsole.PrintLine("[Application {0}], Debug level set to {1}", - InitialParametersClass.ApplicationNumber, Level); - - //var err = CrestronDataStoreStatic.SetLocalUintValue("DebugLevel", level); - //if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) - // CrestronConsole.PrintLine("Error saving console debug level setting: {0}", err); - } - } - - /// - /// - /// - public static void ShowDebugLog(string s) - { - var loglist = CrestronLogger.PrintTheLog(s.ToLower() == "all"); - foreach (var l in loglist) - CrestronConsole.ConsoleCommandResponse(l + CrestronEnvironment.NewLine); - } - - /// - /// Prints message to console if current debug level is equal to or higher than the level of this message. - /// Uses CrestronConsole.PrintLine. - /// - /// - /// Console format string - /// Object parameters - public static void Console(uint level, string format, params object[] items) - { - if (Level >= level) - CrestronConsole.PrintLine("[{0}]App {1}:{2}", DateTime.Now.ToString("HH:mm:ss.fff"), InitialParametersClass.ApplicationNumber, - string.Format(format, items)); - } - - /// - /// Logs to Console when at-level, and all messages to error log, including device key - /// - public static void Console(uint level, IKeyed dev, string format, params object[] items) - { - if (Level >= level) - Console(level, "[{0}] {1}", dev.Key, string.Format(format, items)); - } - - public static void Console(uint level, IKeyed dev, ErrorLogLevel errorLogLevel, - string format, params object[] items) - { - var str = string.Format("[{0}] {1}", dev.Key, string.Format(format, items)); - LogError(errorLogLevel, str); - if (Level >= level) - { - Console(level, str); - } - } - - /// - /// Logs to Console when at-level, and all messages to error log - /// - public static void Console(uint level, ErrorLogLevel errorLogLevel, - string format, params object[] items) - { - var str = string.Format(format, items); - LogError(errorLogLevel, str); - if (Level >= level) - { - Console(level, str); - } - } - - /// - /// Logs to both console and the custom user log (not the built-in error log). If appdebug level is set at - /// or above the level provided, then the output will be written to both console and the log. Otherwise - /// it will only be written to the log. - /// - /// - /// - /// - public static void ConsoleWithLog(uint level, string format, params object[] items) - { - var str = string.Format(format, items); - if (Level >= level) - CrestronConsole.PrintLine("App {0}:{1}", InitialParametersClass.ApplicationNumber, str); - CrestronLogger.WriteToLog(str, level); - } - - /// - /// Logs to both console and the custom user log (not the built-in error log). If appdebug level is set at - /// or above the level provided, then the output will be written to both console and the log. Otherwise - /// it will only be written to the log. - /// - /// - /// - /// String.format string - /// Parameters for substitution in the format string. - public static void ConsoleWithLog(uint level, IKeyed dev, string format, params object[] items) - { - var str = string.Format(format, items); - if (Level >= level) - ConsoleWithLog(level, "[{0}] {1}", dev.Key, str); - } - - /// - /// Prints to log and error log - /// - /// - /// - public static void LogError(ErrorLogLevel errorLogLevel, string str) - { - string msg = string.Format("App {0}:{1}", InitialParametersClass.ApplicationNumber, str); - switch (errorLogLevel) - { - case ErrorLogLevel.Error: - ErrorLog.Error(msg); - break; - case ErrorLogLevel.Warning: - ErrorLog.Warn(msg); - break; - case ErrorLogLevel.Notice: - ErrorLog.Notice(msg); - break; - } - } - - /// - /// Writes the memory object after timeout - /// - static void SaveMemoryOnTimeout() - { - if (SaveTimer == null) - SaveTimer = new CTimer(o => - { - SaveTimer = null; - SaveMemory(); - }, SaveTimeoutMs); - else - SaveTimer.Reset(SaveTimeoutMs); - } - - /// - /// Writes the memory - use SaveMemoryOnTimeout - /// - static void SaveMemory() - { - //var dir = @"\NVRAM\debug"; - //if (!Directory.Exists(dir)) - // Directory.Create(dir); - - using (StreamWriter sw = new StreamWriter(GetMemoryFileName())) - { - var json = JsonConvert.SerializeObject(Contexts); - sw.Write(json); - sw.Flush(); - } - } - - /// - /// - /// - static void LoadMemory() - { - var file = GetMemoryFileName(); - if (File.Exists(file)) - { - using (StreamReader sr = new StreamReader(file)) - { - var json = sr.ReadToEnd(); - Contexts = JsonConvert.DeserializeObject(json); - - if (Contexts != null) - { - Debug.Console(1, "Debug memory restored from file"); - return; - } - } - } - - Contexts = new DebugContextCollection(); - } - - /// - /// Helper to get the file path for this app's debug memory - /// - static string GetMemoryFileName() - { - return string.Format(@"\NVRAM\debugSettings\program{0}", InitialParametersClass.ApplicationNumber); - } - - public enum ErrorLogLevel - { - Error, Warning, Notice, None - } - } +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Crestron.SimplSharp; +using Crestron.SimplSharp.Reflection; +using Crestron.SimplSharp.CrestronLogger; +using Crestron.SimplSharp.CrestronIO; +using Newtonsoft.Json; +using PepperDash.Core.DebugThings; + + +namespace PepperDash.Core +{ + public static class Debug + { + /// + /// Describes the folder location where a given program stores it's debug level memory. By default, the + /// file written will be named appNdebug where N is 1-10. + /// + public static string FilePathPrefix = @"\nvram\debug\"; + + /// + /// The name of the file containing the current debug settings. + /// + public static string FileName = string.Format(@"app{0}Debug.json", InitialParametersClass.ApplicationNumber); + + public static int Level { get; private set; } + + static DebugContextCollection Contexts; + + static int SaveTimeoutMs = 30000; + + public static string PepperDashCoreVersion { get; private set; } + + static CTimer SaveTimer; + + /// + /// When true, the IncludedExcludedKeys dict will contain keys to include. + /// When false (default), IncludedExcludedKeys will contain keys to exclude. + /// + static bool ExcludeAllMode; + + //static bool ExcludeNoKeyMessages; + + static Dictionary IncludedExcludedKeys; + + static Debug() + { + // Get the assembly version and print it to console and the log + var version = Assembly.GetExecutingAssembly().GetName().Version; + + PepperDashCoreVersion = string.Format("{0}.{1}.{2}.{3}", version.Major, version.Minor, version.Build, version.Revision); + + var msg = string.Format("[App {0}] Using PepperDash_Core v{1}", InitialParametersClass.ApplicationNumber, PepperDashCoreVersion); + + CrestronConsole.PrintLine(msg); + + LogError(ErrorLogLevel.Notice, msg); + + IncludedExcludedKeys = new Dictionary(); + + //CrestronDataStoreStatic.InitCrestronDataStore(); + if (CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro) + { + // Add command to console + CrestronConsole.AddNewConsoleCommand(SetDebugFromConsole, "appdebug", + "appdebug:P [0-2]: Sets the application's console debug message level", + ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(ShowDebugLog, "appdebuglog", + "appdebuglog:P [all] Use \"all\" for full log.", + ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => CrestronLogger.Clear(false), "appdebugclear", + "appdebugclear:P Clears the current custom log", + ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(SetDebugFilterFromConsole, "appdebugfilter", + "appdebugfilter [params]", ConsoleAccessLevelEnum.AccessOperator); + } + + CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; + + LoadMemory(); + Level = Contexts.GetOrCreateItem("DEFAULT").Level; + + try + { + if (InitialParametersClass.NumberOfRemovableDrives > 0) + { + CrestronConsole.PrintLine("{0} RM Drive(s) Present.", InitialParametersClass.NumberOfRemovableDrives); + CrestronLogger.Initialize(2, LoggerModeEnum.DEFAULT); // Use RM instead of DEFAULT as not to double-up console messages. + } + else + CrestronConsole.PrintLine("No RM Drive(s) Present."); + } + catch (Exception e) + { + + CrestronConsole.PrintLine("Initializing of CrestronLogger failed: {0}", e); + } + } + + /// + /// Used to save memory when shutting down + /// + /// + static void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType == eProgramStatusEventType.Stopping) + { + if (SaveTimer != null) + { + SaveTimer.Stop(); + SaveTimer = null; + } + Console(0, "Saving debug settings"); + SaveMemory(); + } + } + + /// + /// Callback for console command + /// + /// + public static void SetDebugFromConsole(string levelString) + { + try + { + if (string.IsNullOrEmpty(levelString.Trim())) + { + CrestronConsole.PrintLine("AppDebug level = {0}", Level); + return; + } + + SetDebugLevel(Convert.ToInt32(levelString)); + } + catch + { + CrestronConsole.PrintLine("Usage: appdebug:P [0-2]"); + } + } + + public static void SetDebugFilterFromConsole(string items) + { + var str = items.Trim(); + if (str == "?") + { + CrestronConsole.ConsoleCommandResponse("Usage:\r APPDEBUGFILTER key1 key2 key3....\r " + + "+all: at beginning puts filter into 'default include' mode\r" + + " All keys that follow will be excluded from output.\r" + + "-all: at beginning puts filter into 'default excluse all' mode.\r" + + " All keys that follow will be the only keys that are shown\r" + + "+nokey: Enables messages with no key (default)\r" + + "-nokey: Disables messages with no key.\r" + + "(nokey settings are independent of all other settings)"); + return; + } + var keys = Regex.Split(str, @"\s*"); + foreach (var keyToken in keys) + { + var lkey = keyToken.ToLower(); + if (lkey == "+all") + { + IncludedExcludedKeys.Clear(); + ExcludeAllMode = false; + } + else if (lkey == "-all") + { + IncludedExcludedKeys.Clear(); + ExcludeAllMode = true; + } + //else if (lkey == "+nokey") + //{ + // ExcludeNoKeyMessages = false; + //} + //else if (lkey == "-nokey") + //{ + // ExcludeNoKeyMessages = true; + //} + else + { + string key = null; ; + if (lkey.StartsWith("-")) + { + key = lkey.Substring(1); + // if in exclude all mode, we need to remove this from the inclusions + if (ExcludeAllMode) + { + if (IncludedExcludedKeys.ContainsKey(key)) + IncludedExcludedKeys.Remove(key); + } + // otherwise include all mode, add to the exclusions + else + { + IncludedExcludedKeys[key] = new object(); + } + } + else if (lkey.StartsWith("+")) + { + key = lkey.Substring(1); + // if in exclude all mode, we need to add this as inclusion + if (ExcludeAllMode) + { + + IncludedExcludedKeys[key] = new object(); + } + // otherwise include all mode, remove this from exclusions + else + { + if (IncludedExcludedKeys.ContainsKey(key)) + IncludedExcludedKeys.Remove(key); + } + } + } + } + } + + + /// + /// Sets the debug level + /// + /// Valid values 0 (no debug), 1 (critical), 2 (all messages) + public static void SetDebugLevel(int level) + { + if (level <= 2) + { + Level = level; + Contexts.GetOrCreateItem("DEFAULT").Level = level; + SaveMemoryOnTimeout(); + + CrestronConsole.PrintLine("[Application {0}], Debug level set to {1}", + InitialParametersClass.ApplicationNumber, Level); + + //var err = CrestronDataStoreStatic.SetLocalUintValue("DebugLevel", level); + //if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + // CrestronConsole.PrintLine("Error saving console debug level setting: {0}", err); + } + } + + /// + /// + /// + public static void ShowDebugLog(string s) + { + var loglist = CrestronLogger.PrintTheLog(s.ToLower() == "all"); + foreach (var l in loglist) + CrestronConsole.ConsoleCommandResponse(l + CrestronEnvironment.NewLine); + } + + /// + /// Prints message to console if current debug level is equal to or higher than the level of this message. + /// Uses CrestronConsole.PrintLine. + /// + /// + /// Console format string + /// Object parameters + public static void Console(uint level, string format, params object[] items) + { + if (Level >= level) + CrestronConsole.PrintLine("[{0}]App {1}:{2}", DateTime.Now.ToString("HH:mm:ss.fff"), InitialParametersClass.ApplicationNumber, + string.Format(format, items)); + } + + /// + /// Logs to Console when at-level, and all messages to error log, including device key + /// + public static void Console(uint level, IKeyed dev, string format, params object[] items) + { + if (Level >= level) + Console(level, "[{0}] {1}", dev.Key, string.Format(format, items)); + } + + public static void Console(uint level, IKeyed dev, ErrorLogLevel errorLogLevel, + string format, params object[] items) + { + var str = string.Format("[{0}] {1}", dev.Key, string.Format(format, items)); + LogError(errorLogLevel, str); + if (Level >= level) + { + Console(level, str); + } + } + + /// + /// Logs to Console when at-level, and all messages to error log + /// + public static void Console(uint level, ErrorLogLevel errorLogLevel, + string format, params object[] items) + { + var str = string.Format(format, items); + LogError(errorLogLevel, str); + if (Level >= level) + { + Console(level, str); + } + } + + /// + /// Logs to both console and the custom user log (not the built-in error log). If appdebug level is set at + /// or above the level provided, then the output will be written to both console and the log. Otherwise + /// it will only be written to the log. + /// + /// + /// + /// + public static void ConsoleWithLog(uint level, string format, params object[] items) + { + var str = string.Format(format, items); + if (Level >= level) + CrestronConsole.PrintLine("App {0}:{1}", InitialParametersClass.ApplicationNumber, str); + CrestronLogger.WriteToLog(str, level); + } + + /// + /// Logs to both console and the custom user log (not the built-in error log). If appdebug level is set at + /// or above the level provided, then the output will be written to both console and the log. Otherwise + /// it will only be written to the log. + /// + /// + /// + /// String.format string + /// Parameters for substitution in the format string. + public static void ConsoleWithLog(uint level, IKeyed dev, string format, params object[] items) + { + var str = string.Format(format, items); + if (Level >= level) + ConsoleWithLog(level, "[{0}] {1}", dev.Key, str); + } + + /// + /// Prints to log and error log + /// + /// + /// + public static void LogError(ErrorLogLevel errorLogLevel, string str) + { + string msg = string.Format("App {0}:{1}", InitialParametersClass.ApplicationNumber, str); + switch (errorLogLevel) + { + case ErrorLogLevel.Error: + ErrorLog.Error(msg); + break; + case ErrorLogLevel.Warning: + ErrorLog.Warn(msg); + break; + case ErrorLogLevel.Notice: + ErrorLog.Notice(msg); + break; + } + } + + /// + /// Writes the memory object after timeout + /// + static void SaveMemoryOnTimeout() + { + if (SaveTimer == null) + SaveTimer = new CTimer(o => + { + SaveTimer = null; + SaveMemory(); + }, SaveTimeoutMs); + else + SaveTimer.Reset(SaveTimeoutMs); + } + + /// + /// Writes the memory - use SaveMemoryOnTimeout + /// + static void SaveMemory() + { + //var dir = @"\NVRAM\debug"; + //if (!Directory.Exists(dir)) + // Directory.Create(dir); + + using (StreamWriter sw = new StreamWriter(GetMemoryFileName())) + { + var json = JsonConvert.SerializeObject(Contexts); + sw.Write(json); + sw.Flush(); + } + } + + /// + /// + /// + static void LoadMemory() + { + var file = GetMemoryFileName(); + if (File.Exists(file)) + { + using (StreamReader sr = new StreamReader(file)) + { + var json = sr.ReadToEnd(); + Contexts = JsonConvert.DeserializeObject(json); + + if (Contexts != null) + { + Debug.Console(1, "Debug memory restored from file"); + return; + } + } + } + + Contexts = new DebugContextCollection(); + } + + /// + /// Helper to get the file path for this app's debug memory + /// + static string GetMemoryFileName() + { + return string.Format(@"\NVRAM\debugSettings\program{0}", InitialParametersClass.ApplicationNumber); + } + + public enum ErrorLogLevel + { + Error, Warning, Notice, None + } + } } \ No newline at end of file diff --git a/Pepperdash Core/Pepperdash Core/Network/DiscoveryThings.cs b/Pepperdash Core/Pepperdash Core/Network/DiscoveryThings.cs index 72f2285..973c03a 100644 --- a/Pepperdash Core/Pepperdash Core/Network/DiscoveryThings.cs +++ b/Pepperdash Core/Pepperdash Core/Network/DiscoveryThings.cs @@ -6,10 +6,14 @@ using Crestron.SimplSharp; namespace PepperDash.Core { - + /// + /// Not in use + /// public static class NetworkComm { - + /// + /// Not in use + /// static NetworkComm() { } diff --git a/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj b/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj index 13a79a5..6c7960d 100644 --- a/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj +++ b/Pepperdash Core/Pepperdash Core/PepperDash_Core.csproj @@ -31,6 +31,7 @@ 512 true true + bin\PepperDash_Core.xml .allowedReferenceRelatedFileExtensions @@ -42,6 +43,7 @@ 512 true true + bin\PepperDash_Core.xml @@ -57,6 +59,10 @@ False ..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpNewtonsoft.dll + + False + ..\..\..\..\..\..\..\ProgramData\Crestron\SDK\SimplSharpReflectionInterface.dll + @@ -76,6 +82,7 @@ + diff --git a/Pepperdash Core/Pepperdash Core/Properties/AssemblyInfo.cs b/Pepperdash Core/Pepperdash Core/Properties/AssemblyInfo.cs index de52999..55b4cbb 100644 --- a/Pepperdash Core/Pepperdash Core/Properties/AssemblyInfo.cs +++ b/Pepperdash Core/Pepperdash Core/Properties/AssemblyInfo.cs @@ -4,4 +4,4 @@ [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("Pepperdash_Core")] [assembly: AssemblyCopyright("Copyright © PepperDash 2019")] -[assembly: AssemblyVersion("1.0.14.*")] +[assembly: AssemblyVersion("1.0.23.*")] diff --git a/Pepperdash Core/Pepperdash Core/Properties/UpdateAssemblyVersion.ps1 b/Pepperdash Core/Pepperdash Core/Properties/UpdateAssemblyVersion.ps1 new file mode 100644 index 0000000..46392b3 --- /dev/null +++ b/Pepperdash Core/Pepperdash Core/Properties/UpdateAssemblyVersion.ps1 @@ -0,0 +1,35 @@ +function Update-SourceVersion +{ + Param ([string]$Version) + $NewVersion = ‘AssemblyVersion("‘ + $Version + ‘.*")’; + foreach ($o in $input) + { + Write-output $o.FullName + $TmpFile = $o.FullName + “.tmp” + get-content $o.FullName | + %{$_ -replace ‘AssemblyVersion\("(\d+\.\d+\.\d+)\.\*"\)’, $NewVersion } > $TmpFile + move-item $TmpFile $o.FullName -force + } +} + +function Update-AllAssemblyInfoFiles ( $version ) +{ + foreach ($file in “AssemblyInfo.cs”, “AssemblyInfo.vb” ) + { + get-childitem -recurse |? {$_.Name -eq $file} | Update-SourceVersion $version ; + } +} + +# validate arguments +$r= [System.Text.RegularExpressions.Regex]::Match($args[0], "^\d+\.\d+\.\d+$"); +if ($r.Success) +{ + Update-AllAssemblyInfoFiles $args[0]; +} +else +{ + echo ” “; + echo “Error: Input version does not match x.y.z format!” + echo ” “; + echo "Unable to apply version to AssemblyInfo.cs files"; +} diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..af824bf --- /dev/null +++ b/Readme.md @@ -0,0 +1,17 @@ +# Pepperdash Core + +#### Workflow process + +- Create a Jira issue for the feature/bugfix. If you know you're targeting this update to coincide with a new release of Core, specify that release (or create a new one) as the Fix Version for the Jira issue +- Branch from development using the syntax [feature/bugfix]/[pdc-x] (where x is the Jira issue number) +- Modify code to suit and test. Make commits to the branch as you work. +- Log a Pull Request on www.bitbucket.org and tag Heath and Neil as reviewers + +#### Pull Request process + +- Check out the branch for the PR and review. +- If necessary, merge the latest Development branch into the PR branch. Resolve any conflicts. +- Increment the appropriate Assembly version number to match the next release in Jira +- Build the project +- Copy PepperDash_Core.cpz and PepperDash_Core.dll from the bin folder to the CLZ Builds folder in the repo root. Commit. +- Merge the PR in Bitbucket