diff --git a/.gitignore b/.gitignore index db1e92a3..d64977d6 100644 --- a/.gitignore +++ b/.gitignore @@ -396,3 +396,4 @@ _site/ api/ *.DS_Store /._PepperDash.Essentials.4Series.sln +dotnet diff --git a/src/Directory.Build.props b/src/Directory.Build.props index af743a53..49e41a67 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,6 +1,6 @@ - 2.15.1-local + 2.19.4-local $(Version) PepperDash Technology PepperDash Technology diff --git a/src/PepperDash.Core/ComTextHelper.cs b/src/PepperDash.Core/ComTextHelper.cs new file mode 100644 index 00000000..28a76975 --- /dev/null +++ b/src/PepperDash.Core/ComTextHelper.cs @@ -0,0 +1,43 @@ +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace PepperDash.Core +{ + /// + /// Helper class for formatting communication text and byte data for debugging purposes. + /// + public class ComTextHelper + { + /// + /// Gets escaped text for a byte array + /// + /// + /// string with all bytes escaped + public static string GetEscapedText(byte[] bytes) + { + return string.Concat(bytes.Select(b => string.Format(@"[{0:X2}]", (int)b)).ToArray()); + } + + /// + /// Gets escaped text for a string + /// + /// + /// string with all bytes escaped + public static string GetEscapedText(string text) + { + var bytes = Encoding.GetEncoding(28591).GetBytes(text); + return string.Concat(bytes.Select(b => string.Format(@"[{0:X2}]", (int)b)).ToArray()); + } + + /// + /// Gets debug text for a string + /// + /// + /// string with all non-printable characters escaped + public static string GetDebugText(string text) + { + return Regex.Replace(text, @"[^\u0020-\u007E]", a => GetEscapedText(a.Value)); + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Core/Comm/CommunicationStreamDebugging.cs b/src/PepperDash.Core/Comm/CommunicationStreamDebugging.cs index 33141f0c..780e65d0 100644 --- a/src/PepperDash.Core/Comm/CommunicationStreamDebugging.cs +++ b/src/PepperDash.Core/Comm/CommunicationStreamDebugging.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text; using Crestron.SimplSharp; @@ -37,14 +36,14 @@ namespace PepperDash.Core { get { - return _DebugTimeoutInMs/60000; + return _DebugTimeoutInMs / 60000; } } /// /// Gets or sets the RxStreamDebuggingIsEnabled /// - public bool RxStreamDebuggingIsEnabled{ get; private set; } + public bool RxStreamDebuggingIsEnabled { get; private set; } /// /// Indicates that transmit stream debugging is enabled @@ -108,7 +107,7 @@ namespace PepperDash.Core TxStreamDebuggingIsEnabled = true; Debug.SetDeviceDebugSettings(ParentDeviceKey, setting); - + } /// @@ -136,51 +135,4 @@ namespace PepperDash.Core DebugExpiryPeriod = null; } } - - /// - /// The available settings for stream debugging - /// - [Flags] - /// - /// Enumeration of eStreamDebuggingSetting values - /// - public enum eStreamDebuggingSetting - { - /// - /// Debug off - /// - Off = 0, - /// - /// Debug received data - /// - Rx = 1, - /// - /// Debug transmitted data - /// - Tx = 2, - /// - /// Debug both received and transmitted data - /// - Both = Rx | Tx - } - - /// - /// The available settings for stream debugging response types - /// - [Flags] - public enum eStreamDebuggingDataTypeSettings - { - /// - /// Debug data in byte format - /// - Bytes = 0, - /// - /// Debug data in text format - /// - Text = 1, - /// - /// Debug data in both byte and text formats - /// - Both = Bytes | Text, - } } diff --git a/src/PepperDash.Core/Comm/GenericSshClient.cs b/src/PepperDash.Core/Comm/GenericSshClient.cs index 13192ed5..df44ab51 100644 --- a/src/PepperDash.Core/Comm/GenericSshClient.cs +++ b/src/PepperDash.Core/Comm/GenericSshClient.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text; using System.Threading; using Crestron.SimplSharp; @@ -11,11 +12,12 @@ using Renci.SshNet.Common; namespace PepperDash.Core { /// - /// + /// SSH Client /// public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect { private const string SPlusKey = "Uninitialized SshClient"; + /// /// Object to enable stream debugging /// @@ -36,11 +38,6 @@ namespace PepperDash.Core /// public event EventHandler ConnectionChange; - /// - ///// - ///// - //public event GenericSocketStatusChangeEventDelegate SocketStatusChange; - /// /// Gets or sets the Hostname /// @@ -67,7 +64,7 @@ namespace PepperDash.Core public bool IsConnected { // returns false if no client or not connected - get { return Client != null && ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } + get { return client != null && ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } } /// @@ -83,16 +80,26 @@ namespace PepperDash.Core /// public SocketStatus ClientStatus { - get { return _ClientStatus; } + get { lock (_stateLock) { return _ClientStatus; } } private set { - if (_ClientStatus == value) - return; - _ClientStatus = value; - OnConnectionChange(); + bool shouldFireEvent = false; + lock (_stateLock) + { + if (_ClientStatus != value) + { + _ClientStatus = value; + shouldFireEvent = true; + } + } + // Fire event outside lock to avoid deadlock + if (shouldFireEvent) + OnConnectionChange(); } } - SocketStatus _ClientStatus; + + private SocketStatus _ClientStatus; + private bool _ConnectEnabled; /// /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event @@ -100,7 +107,7 @@ namespace PepperDash.Core /// public ushort UStatus { - get { return (ushort)_ClientStatus; } + get { lock (_stateLock) { return (ushort)_ClientStatus; } } } /// @@ -111,7 +118,11 @@ namespace PepperDash.Core /// /// Will be set and unset by connect and disconnect only /// - public bool ConnectEnabled { get; private set; } + public bool ConnectEnabled + { + get { lock (_stateLock) { return _ConnectEnabled; } } + private set { lock (_stateLock) { _ConnectEnabled = value; } } + } /// /// S+ helper for AutoReconnect @@ -127,17 +138,25 @@ namespace PepperDash.Core /// public int AutoReconnectIntervalMs { get; set; } - SshClient Client; + private SshClient client; - ShellStream TheStream; + private ShellStream shellStream; - CTimer ReconnectTimer; + private readonly Timer reconnectTimer; //Lock object to prevent simulatneous connect/disconnect operations //private CCriticalSection connectLock = new CCriticalSection(); - private SemaphoreSlim connectLock = new SemaphoreSlim(1); + private readonly SemaphoreSlim connectLock = new SemaphoreSlim(1); - private bool DisconnectLogged = false; + // Thread-safety lock for state changes + private readonly object _stateLock = new object(); + + private bool disconnectLogged = false; + + /// + /// When true, turns off echo for the SSH session + /// + public bool DisableEcho { get; set; } /// /// Typical constructor. @@ -154,13 +173,13 @@ namespace PepperDash.Core Password = password; AutoReconnectIntervalMs = 5000; - ReconnectTimer = new CTimer(o => + reconnectTimer = new Timer(o => { - if (ConnectEnabled) + if (ConnectEnabled) // Now thread-safe property access { Connect(); } - }, System.Threading.Timeout.Infinite); + }, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); } /// @@ -172,23 +191,23 @@ namespace PepperDash.Core CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); AutoReconnectIntervalMs = 5000; - ReconnectTimer = new CTimer(o => + reconnectTimer = new Timer(o => { - if (ConnectEnabled) + if (ConnectEnabled) // Now thread-safe property access { Connect(); } - }, System.Threading.Timeout.Infinite); + }, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); } /// /// Handles closing this up when the program shuts down /// - void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + private void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) { if (programEventType == eProgramStatusEventType.Stopping) { - if (Client != null) + if (client != null) { this.LogDebug("Program stopping. Closing connection"); Disconnect(); @@ -223,10 +242,10 @@ namespace PepperDash.Core this.LogDebug("Attempting connect"); // Cancel reconnect if running. - ReconnectTimer?.Stop(); + StopReconnectTimer(); // Cleanup the old client if it already exists - if (Client != null) + if (client != null) { this.LogDebug("Cleaning up disconnected client"); KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY); @@ -239,29 +258,36 @@ namespace PepperDash.Core this.LogDebug("Creating new SshClient"); ConnectionInfo connectionInfo = new ConnectionInfo(Hostname, Port, Username, pauth, kauth); - Client = new SshClient(connectionInfo); - Client.ErrorOccurred += Client_ErrorOccurred; + client = new SshClient(connectionInfo); + client.ErrorOccurred += Client_ErrorOccurred; //Attempt to connect ClientStatus = SocketStatus.SOCKET_STATUS_WAITING; try { - Client.Connect(); - TheStream = Client.CreateShellStream("PDTShell", 0, 0, 0, 0, 65534); - if (TheStream.DataAvailable) + client.Connect(); + + var modes = new Dictionary(); + + if (DisableEcho) + { + modes.Add(TerminalModes.ECHO, 0); + } + + shellStream = client.CreateShellStream("PDTShell", 0, 0, 0, 0, 65534, modes); + if (shellStream.DataAvailable) { // empty the buffer if there is data - string str = TheStream.Read(); + shellStream.Read(); } - TheStream.DataReceived += Stream_DataReceived; + shellStream.DataReceived += Stream_DataReceived; this.LogInformation("Connected"); ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED; - DisconnectLogged = false; + disconnectLogged = false; } catch (SshConnectionException e) { var ie = e.InnerException; // The details are inside!! - var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error; if (ie is SocketException) { @@ -286,37 +312,36 @@ namespace PepperDash.Core this.LogVerbose(ie, "Exception details: "); } - DisconnectLogged = true; + disconnectLogged = true; KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); if (AutoReconnect) { this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs); - ReconnectTimer.Reset(AutoReconnectIntervalMs); + StartReconnectTimer(); } } catch (SshOperationTimeoutException ex) { this.LogWarning("Connection attempt timed out: {message}", ex.Message); - DisconnectLogged = true; + disconnectLogged = true; KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); if (AutoReconnect) { this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); - ReconnectTimer.Reset(AutoReconnectIntervalMs); + StartReconnectTimer(); } } catch (Exception e) { - var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error; this.LogError("Unhandled exception on connect: {error}", e.Message); this.LogVerbose(e, "Exception details: "); - DisconnectLogged = true; + disconnectLogged = true; KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); if (AutoReconnect) { this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); - ReconnectTimer.Reset(AutoReconnectIntervalMs); + StartReconnectTimer(); } } } @@ -334,11 +359,7 @@ namespace PepperDash.Core { ConnectEnabled = false; // Stop trying reconnects, if we are - if (ReconnectTimer != null) - { - ReconnectTimer.Stop(); - // ReconnectTimer = null; - } + StopReconnectTimer(); KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY); } @@ -352,12 +373,12 @@ namespace PepperDash.Core try { - if (Client != null) + if (client != null) { - Client.ErrorOccurred -= Client_ErrorOccurred; - Client.Disconnect(); - Client.Dispose(); - Client = null; + client.ErrorOccurred -= Client_ErrorOccurred; + client.Disconnect(); + client.Dispose(); + client = null; ClientStatus = status; this.LogDebug("Disconnected"); } @@ -371,16 +392,16 @@ namespace PepperDash.Core /// /// Kills the stream /// - void KillStream() + private void KillStream() { try { - if (TheStream != null) + if (shellStream != null) { - TheStream.DataReceived -= Stream_DataReceived; - TheStream.Close(); - TheStream.Dispose(); - TheStream = null; + shellStream.DataReceived -= Stream_DataReceived; + shellStream.Close(); + shellStream.Dispose(); + shellStream = null; this.LogDebug("Disconnected stream"); } } @@ -393,7 +414,7 @@ namespace PepperDash.Core /// /// Handles the keyboard interactive authentication, should it be required. /// - void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e) + private void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e) { foreach (AuthenticationPrompt prompt in e.Prompts) if (prompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1) @@ -403,7 +424,7 @@ namespace PepperDash.Core /// /// Handler for data receive on ShellStream. Passes data across to queue for line parsing. /// - void Stream_DataReceived(object sender, ShellDataEventArgs e) + private void Stream_DataReceived(object sender, ShellDataEventArgs e) { if (((ShellStream)sender).Length <= 0L) { @@ -416,18 +437,14 @@ namespace PepperDash.Core if (bytesHandler != null) { var bytes = Encoding.UTF8.GetBytes(response); - if (StreamDebugging.RxStreamDebuggingIsEnabled) - { - this.LogInformation("Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length); - } + this.PrintReceivedBytes(bytes); bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); } var textHandler = TextReceived; if (textHandler != null) { - if (StreamDebugging.RxStreamDebuggingIsEnabled) - this.LogInformation("Received: '{0}'", ComTextHelper.GetDebugText(response)); + this.PrintReceivedText(response); textHandler(this, new GenericCommMethodReceiveTextArgs(response)); } @@ -439,7 +456,7 @@ namespace PepperDash.Core /// Error event handler for client events - disconnect, etc. Will forward those events via ConnectionChange /// event /// - void Client_ErrorOccurred(object sender, ExceptionEventArgs e) + private void Client_ErrorOccurred(object sender, ExceptionEventArgs e) { CrestronInvoke.BeginInvoke(o => { @@ -459,7 +476,7 @@ namespace PepperDash.Core if (AutoReconnect && ConnectEnabled) { this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); - ReconnectTimer.Reset(AutoReconnectIntervalMs); + StartReconnectTimer(); } }); } @@ -467,7 +484,7 @@ namespace PepperDash.Core /// /// Helper for ConnectionChange event /// - void OnConnectionChange() + private void OnConnectionChange() { ConnectionChange?.Invoke(this, new GenericSocketStatusChageEventArgs(this)); } @@ -482,16 +499,12 @@ namespace PepperDash.Core { try { - if (Client != null && TheStream != null && IsConnected) + if (client != null && shellStream != null && IsConnected) { - if (StreamDebugging.TxStreamDebuggingIsEnabled) - this.LogInformation( - "Sending {length} characters of text: '{text}'", - text.Length, - ComTextHelper.GetDebugText(text)); + this.PrintSentText(text); - TheStream.Write(text); - TheStream.Flush(); + shellStream.Write(text); + shellStream.Flush(); } else { @@ -503,7 +516,7 @@ namespace PepperDash.Core this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim()); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); - ReconnectTimer.Reset(); + StartReconnectTimer(); } catch (Exception ex) { @@ -519,13 +532,12 @@ namespace PepperDash.Core { try { - if (Client != null && TheStream != null && IsConnected) + if (client != null && shellStream != null && IsConnected) { - if (StreamDebugging.TxStreamDebuggingIsEnabled) - this.LogInformation("Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); + this.PrintSentBytes(bytes); - TheStream.Write(bytes, 0, bytes.Length); - TheStream.Flush(); + shellStream.Write(bytes, 0, bytes.Length); + shellStream.Flush(); } else { @@ -537,7 +549,7 @@ namespace PepperDash.Core this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes)); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); - ReconnectTimer.Reset(); + StartReconnectTimer(); } catch (Exception ex) { @@ -546,6 +558,83 @@ namespace PepperDash.Core } #endregion + /// + /// Safely starts the reconnect timer with exception handling + /// + private void StartReconnectTimer() + { + try + { + reconnectTimer?.Change(AutoReconnectIntervalMs, System.Threading.Timeout.Infinite); + } + catch (ObjectDisposedException) + { + // Timer was disposed, ignore + this.LogDebug("Attempted to start timer but it was already disposed"); + } + } + + /// + /// Safely stops the reconnect timer with exception handling + /// + private void StopReconnectTimer() + { + try + { + reconnectTimer?.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); + } + catch (ObjectDisposedException) + { + // Timer was disposed, ignore + this.LogDebug("Attempted to stop timer but it was already disposed"); + } + } + + /// + /// Deactivate method - properly dispose of resources + /// + public override bool Deactivate() + { + try + { + this.LogDebug("Deactivating SSH client - disposing resources"); + + // Stop trying reconnects + ConnectEnabled = false; + StopReconnectTimer(); + + // Disconnect and cleanup client + KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY); + + // Dispose timer + try + { + reconnectTimer?.Dispose(); + } + catch (ObjectDisposedException) + { + // Already disposed, ignore + } + + // Dispose semaphore + try + { + connectLock?.Dispose(); + } + catch (ObjectDisposedException) + { + // Already disposed, ignore + } + + return base.Deactivate(); + } + catch (Exception ex) + { + this.LogException(ex, "Error during SSH client deactivation"); + return false; + } + } + } //***************************************************************************************************** diff --git a/src/PepperDash.Core/Comm/GenericTcpIpClient.cs b/src/PepperDash.Core/Comm/GenericTcpIpClient.cs index c9235411..d13eed3f 100644 --- a/src/PepperDash.Core/Comm/GenericTcpIpClient.cs +++ b/src/PepperDash.Core/Comm/GenericTcpIpClient.cs @@ -19,44 +19,44 @@ namespace PepperDash.Core /// public CommunicationStreamDebugging StreamDebugging { get; private set; } - /// - /// Fires when data is received from the server and returns it as a Byte array - /// - public event EventHandler BytesReceived; + /// + /// Fires when data is received from the server and returns it as a Byte array + /// + public event EventHandler BytesReceived; - /// - /// Fires when data is received from the server and returns it as text - /// - public event EventHandler TextReceived; + /// + /// Fires when data is received from the server and returns it as text + /// + public event EventHandler TextReceived; - /// - /// - /// - //public event GenericSocketStatusChangeEventDelegate SocketStatusChange; - public event EventHandler ConnectionChange; + /// + /// + /// + //public event GenericSocketStatusChangeEventDelegate SocketStatusChange; + public event EventHandler ConnectionChange; - private string _hostname; + private string _hostname; /// /// Address of server /// public string Hostname { - get - { - return _hostname; - } + get + { + return _hostname; + } - set - { - _hostname = value; - if (_client != null) - { - _client.AddressClientConnectedTo = _hostname; - } - } - } + set + { + _hostname = value; + if (_client != null) + { + _client.AddressClientConnectedTo = _hostname; + } + } + } /// /// Gets or sets the Port @@ -78,19 +78,19 @@ namespace PepperDash.Core /// public int BufferSize { get; set; } - /// - /// The actual client class - /// - private TCPClient _client; + /// + /// The actual client class + /// + private TCPClient _client; - /// - /// Bool showing if socket is connected - /// - public bool IsConnected - { - get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } + /// + /// Bool showing if socket is connected + /// + public bool IsConnected + { + get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } } - + /// /// S+ helper for IsConnected /// @@ -99,15 +99,15 @@ namespace PepperDash.Core get { return (ushort)(IsConnected ? 1 : 0); } } - /// - /// _client socket status Read only - /// - public SocketStatus ClientStatus - { - get + /// + /// _client socket status Read only + /// + public SocketStatus ClientStatus + { + get { - return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus; - } + return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus; + } } /// @@ -119,26 +119,26 @@ namespace PepperDash.Core get { return (ushort)ClientStatus; } } - /// + /// /// Status text shows the message associated with socket status - /// - public string ClientStatusText { get { return ClientStatus.ToString(); } } + /// + public string ClientStatusText { get { return ClientStatus.ToString(); } } - /// - /// Ushort representation of client status - /// + /// + /// Ushort representation of client status + /// [Obsolete] - public ushort UClientStatus { get { return (ushort)ClientStatus; } } + public ushort UClientStatus { get { return (ushort)ClientStatus; } } - /// - /// Connection failure reason - /// - public string ConnectionFailure { get { return ClientStatus.ToString(); } } + /// + /// Connection failure reason + /// + public string ConnectionFailure { get { return ClientStatus.ToString(); } } - /// - /// Gets or sets the AutoReconnect - /// - public bool AutoReconnect { get; set; } + /// + /// Gets or sets the AutoReconnect + /// + public bool AutoReconnect { get; set; } /// /// S+ helper for AutoReconnect @@ -149,29 +149,29 @@ namespace PepperDash.Core set { AutoReconnect = value == 1; } } - /// - /// Milliseconds to wait before attempting to reconnect. Defaults to 5000 - /// - public int AutoReconnectIntervalMs { get; set; } + /// + /// Milliseconds to wait before attempting to reconnect. Defaults to 5000 + /// + public int AutoReconnectIntervalMs { get; set; } - /// - /// Set only when the disconnect method is called - /// - bool DisconnectCalledByUser; + /// + /// Set only when the disconnect method is called + /// + bool DisconnectCalledByUser; - /// - /// - /// - public bool Connected - { - get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } - } + /// + /// + /// + public bool Connected + { + get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } + } //Lock object to prevent simulatneous connect/disconnect operations private CCriticalSection connectLock = new CCriticalSection(); // private Timer for auto reconnect - private CTimer RetryTimer; + private CTimer RetryTimer; /// /// Constructor @@ -181,8 +181,8 @@ namespace PepperDash.Core /// /// public GenericTcpIpClient(string key, string address, int port, int bufferSize) - : base(key) - { + : base(key) + { StreamDebugging = new CommunicationStreamDebugging(key); CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); AutoReconnectIntervalMs = 5000; @@ -218,18 +218,18 @@ namespace PepperDash.Core /// Default constructor for S+ /// public GenericTcpIpClient() - : base(SplusKey) + : base(SplusKey) { StreamDebugging = new CommunicationStreamDebugging(SplusKey); - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - AutoReconnectIntervalMs = 5000; + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + AutoReconnectIntervalMs = 5000; BufferSize = 2000; RetryTimer = new CTimer(o => { Reconnect(); }, Timeout.Infinite); - } + } /// /// Initialize method @@ -255,26 +255,26 @@ namespace PepperDash.Core /// /// /// - /// - /// Deactivate method - /// - public override bool Deactivate() - { + /// + /// Deactivate method + /// + public override bool Deactivate() + { RetryTimer.Stop(); RetryTimer.Dispose(); if (_client != null) { - _client.SocketStatusChange -= this.Client_SocketStatusChange; + _client.SocketStatusChange -= this.Client_SocketStatusChange; DisconnectClient(); } - return true; - } + return true; + } - /// - /// Connect method - /// - public void Connect() - { + /// + /// Connect method + /// + public void Connect() + { if (string.IsNullOrEmpty(Hostname)) { Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': No address set", Key); @@ -310,7 +310,7 @@ namespace PepperDash.Core { connectLock.Leave(); } - } + } private void Reconnect() { @@ -337,11 +337,11 @@ namespace PepperDash.Core } } - /// - /// Disconnect method - /// - public void Disconnect() - { + /// + /// Disconnect method + /// + public void Disconnect() + { try { connectLock.Enter(); @@ -355,7 +355,7 @@ namespace PepperDash.Core { connectLock.Leave(); } - } + } /// /// DisconnectClient method @@ -375,7 +375,7 @@ namespace PepperDash.Core /// /// void ConnectToServerCallback(TCPClient c) - { + { if (c.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) { Debug.Console(0, this, "Server connection result: {0}", c.ClientStatus); @@ -385,13 +385,13 @@ namespace PepperDash.Core { Debug.Console(1, this, "Server connection result: {0}", c.ClientStatus); } - } + } /// /// Disconnects, waits and attemtps to connect again /// void WaitAndTryReconnect() - { + { CrestronInvoke.BeginInvoke(o => { try @@ -409,7 +409,7 @@ namespace PepperDash.Core connectLock.Leave(); } }); - } + } /// /// Recieves incoming data @@ -417,7 +417,7 @@ namespace PepperDash.Core /// /// void Receive(TCPClient client, int numBytes) - { + { if (client != null) { if (numBytes > 0) @@ -426,10 +426,7 @@ namespace PepperDash.Core var bytesHandler = BytesReceived; if (bytesHandler != null) { - if (StreamDebugging.RxStreamDebuggingIsEnabled) - { - Debug.Console(0, this, "Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length); - } + this.PrintReceivedBytes(bytes); bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); } var textHandler = TextReceived; @@ -437,58 +434,53 @@ namespace PepperDash.Core { var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); - if (StreamDebugging.RxStreamDebuggingIsEnabled) - { - Debug.Console(0, this, "Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length); - } + this.PrintReceivedText(str); textHandler(this, new GenericCommMethodReceiveTextArgs(str)); - } + } } client.ReceiveDataAsync(Receive); } - } + } - /// - /// SendText method - /// - public void SendText(string text) - { - var bytes = Encoding.GetEncoding(28591).GetBytes(text); - // Check debug level before processing byte array - if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.Console(0, this, "Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text)); + /// + /// SendText method + /// + public void SendText(string text) + { + var bytes = Encoding.GetEncoding(28591).GetBytes(text); + // Check debug level before processing byte array + this.PrintSentText(text); if (_client != null) - _client.SendData(bytes, bytes.Length); - } + _client.SendData(bytes, bytes.Length); + } - /// - /// SendEscapedText method - /// - public void SendEscapedText(string text) - { - var unescapedText = Regex.Replace(text, @"\\x([0-9a-fA-F][0-9a-fA-F])", s => - { - var hex = s.Groups[1].Value; - return ((char)Convert.ToByte(hex, 16)).ToString(); - }); - SendText(unescapedText); - } + /// + /// SendEscapedText method + /// + public void SendEscapedText(string text) + { + var unescapedText = Regex.Replace(text, @"\\x([0-9a-fA-F][0-9a-fA-F])", s => + { + var hex = s.Groups[1].Value; + return ((char)Convert.ToByte(hex, 16)).ToString(); + }); + SendText(unescapedText); + } /// /// Sends Bytes to the server /// /// - /// - /// SendBytes method - /// - public void SendBytes(byte[] bytes) - { - if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); + /// + /// SendBytes method + /// + public void SendBytes(byte[] bytes) + { + this.PrintSentBytes(bytes); if (_client != null) - _client.SendData(bytes, bytes.Length); - } + _client.SendData(bytes, bytes.Length); + } /// /// Socket Status Change Handler @@ -496,7 +488,7 @@ namespace PepperDash.Core /// /// void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus) - { + { if (clientSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) { Debug.Console(0, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); @@ -505,68 +497,73 @@ namespace PepperDash.Core else { Debug.Console(1, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); - _client.ReceiveDataAsync(Receive); + _client.ReceiveDataAsync(Receive); } - var handler = ConnectionChange; - if (handler != null) - ConnectionChange(this, new GenericSocketStatusChageEventArgs(this)); - } - } + var handler = ConnectionChange; + if (handler != null) + ConnectionChange(this, new GenericSocketStatusChageEventArgs(this)); + } + } - /// - /// Represents a TcpSshPropertiesConfig - /// - public class TcpSshPropertiesConfig - { + /// + /// Represents a TcpSshPropertiesConfig + /// + public class TcpSshPropertiesConfig + { /// /// Address to connect to /// [JsonProperty(Required = Required.Always)] - public string Address { get; set; } - + public string Address { get; set; } + /// /// Port to connect to /// - [JsonProperty(Required = Required.Always)] - public int Port { get; set; } - + [JsonProperty(Required = Required.Always)] + public int Port { get; set; } + /// /// Username credential /// - public string Username { get; set; } - /// - /// Gets or sets the Password - /// - public string Password { get; set; } + public string Username { get; set; } + /// + /// Gets or sets the Password + /// + public string Password { get; set; } - /// - /// Defaults to 32768 - /// - public int BufferSize { get; set; } + /// + /// Defaults to 32768 + /// + public int BufferSize { get; set; } - /// - /// Gets or sets the AutoReconnect - /// - public bool AutoReconnect { get; set; } + /// + /// Gets or sets the AutoReconnect + /// + public bool AutoReconnect { get; set; } - /// - /// Gets or sets the AutoReconnectIntervalMs - /// - public int AutoReconnectIntervalMs { get; set; } + /// + /// Gets or sets the AutoReconnectIntervalMs + /// + public int AutoReconnectIntervalMs { get; set; } + + /// + /// When true, turns off echo for the SSH session + /// + [JsonProperty("disableSshEcho")] + public bool DisableSshEcho { get; set; } /// /// Default constructor /// public TcpSshPropertiesConfig() - { - BufferSize = 32768; - AutoReconnect = true; - AutoReconnectIntervalMs = 5000; + { + BufferSize = 32768; + AutoReconnect = true; + AutoReconnectIntervalMs = 5000; Username = ""; Password = ""; - } - - } - + DisableSshEcho = false; + } + } } diff --git a/src/PepperDash.Core/Comm/GenericUdpServer.cs b/src/PepperDash.Core/Comm/GenericUdpServer.cs index 52ac627a..a713872a 100644 --- a/src/PepperDash.Core/Comm/GenericUdpServer.cs +++ b/src/PepperDash.Core/Comm/GenericUdpServer.cs @@ -124,7 +124,7 @@ namespace PepperDash.Core CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler); } - + /// /// /// @@ -135,7 +135,7 @@ namespace PepperDash.Core public GenericUdpServer(string key, string address, int port, int bufferSize) : base(key) { - StreamDebugging = new CommunicationStreamDebugging(key); + StreamDebugging = new CommunicationStreamDebugging(key); Hostname = address; Port = port; BufferSize = bufferSize; @@ -180,7 +180,7 @@ namespace PepperDash.Core /// void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) { - if (programEventType != eProgramStatusEventType.Stopping) + if (programEventType != eProgramStatusEventType.Stopping) return; Debug.Console(1, this, "Program stopping. Disabling Server"); @@ -243,7 +243,7 @@ namespace PepperDash.Core /// public void Disconnect() { - if(Server != null) + if (Server != null) Server.DisableUDPServer(); IsConnected = false; @@ -265,7 +265,7 @@ namespace PepperDash.Core try { - if (numBytes <= 0) + if (numBytes <= 0) return; var sourceIp = Server.IPAddressLastMessageReceivedFrom; @@ -281,17 +281,13 @@ namespace PepperDash.Core var bytesHandler = BytesReceived; if (bytesHandler != null) { - if (StreamDebugging.RxStreamDebuggingIsEnabled) - { - Debug.Console(0, this, "Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length); - } + this.PrintReceivedBytes(bytes); bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); } var textHandler = TextReceived; if (textHandler != null) { - if (StreamDebugging.RxStreamDebuggingIsEnabled) - Debug.Console(0, this, "Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length); + this.PrintReceivedText(str); textHandler(this, new GenericCommMethodReceiveTextArgs(str)); } } @@ -318,8 +314,7 @@ namespace PepperDash.Core if (IsConnected && Server != null) { - if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.Console(0, this, "Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text)); + this.PrintSentText(text); Server.SendData(bytes, bytes.Length); } @@ -334,8 +329,7 @@ namespace PepperDash.Core /// public void SendBytes(byte[] bytes) { - if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); + this.PrintSentBytes(bytes); if (IsConnected && Server != null) Server.SendData(bytes, bytes.Length); @@ -343,11 +337,11 @@ namespace PepperDash.Core } - /// - /// Represents a GenericUdpReceiveTextExtraArgs - /// - public class GenericUdpReceiveTextExtraArgs : EventArgs - { + /// + /// Represents a GenericUdpReceiveTextExtraArgs + /// + public class GenericUdpReceiveTextExtraArgs : EventArgs + { /// /// /// @@ -359,7 +353,7 @@ namespace PepperDash.Core /// /// /// - public int Port { get; private set; } + public int Port { get; private set; } /// /// /// @@ -373,18 +367,18 @@ namespace PepperDash.Core /// /// public GenericUdpReceiveTextExtraArgs(string text, string ipAddress, int port, byte[] bytes) - { - Text = text; - IpAddress = ipAddress; - Port = port; - Bytes = bytes; - } + { + Text = text; + IpAddress = ipAddress; + Port = port; + Bytes = bytes; + } - /// - /// Stupid S+ Constructor - /// - public GenericUdpReceiveTextExtraArgs() { } - } + /// + /// Stupid S+ Constructor + /// + public GenericUdpReceiveTextExtraArgs() { } + } /// /// diff --git a/src/PepperDash.Core/Comm/StreamDebuggingExtensions.cs b/src/PepperDash.Core/Comm/StreamDebuggingExtensions.cs new file mode 100644 index 00000000..9fd7544d --- /dev/null +++ b/src/PepperDash.Core/Comm/StreamDebuggingExtensions.cs @@ -0,0 +1,69 @@ +using System; +using Crestron.SimplSharp; + +namespace PepperDash.Core +{ + /// + /// Extension methods for stream debugging + /// + public static class StreamDebuggingExtensions + { + private static readonly string app = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? $"App {InitialParametersClass.ApplicationNumber}" : $"{InitialParametersClass.RoomId}"; + + /// + /// Print the sent bytes to the console + /// + /// comms device + /// bytes to print + public static void PrintSentBytes(this IStreamDebugging comms, byte[] bytes) + { + if (!comms.StreamDebugging.TxStreamDebuggingIsEnabled) return; + + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + + CrestronConsole.PrintLine($"[{timestamp}][{app}][{comms.Key}] Sending {bytes.Length} bytes: '{ComTextHelper.GetEscapedText(bytes)}'"); + } + + /// + /// Print the received bytes to the console + /// + /// comms device + /// bytes to print + public static void PrintReceivedBytes(this IStreamDebugging comms, byte[] bytes) + { + if (!comms.StreamDebugging.RxStreamDebuggingIsEnabled) return; + + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + + CrestronConsole.PrintLine($"[{timestamp}][{app}][{comms.Key}] Received {bytes.Length} bytes: '{ComTextHelper.GetEscapedText(bytes)}'"); + } + + /// + /// Print the sent text to the console + /// + /// comms device + /// text to print + public static void PrintSentText(this IStreamDebugging comms, string text) + { + if (!comms.StreamDebugging.TxStreamDebuggingIsEnabled) return; + + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + + CrestronConsole.PrintLine($"[{timestamp}][{app}][{comms.Key}] Sending Text: '{ComTextHelper.GetDebugText(text)}'"); + } + + /// + /// Print the received text to the console + /// + /// comms device + /// text to print + public static void PrintReceivedText(this IStreamDebugging comms, string text) + { + if (!comms.StreamDebugging.RxStreamDebuggingIsEnabled) return; + + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + + CrestronConsole.PrintLine($"[{timestamp}][{app}][{comms.Key}] Received Text: '{ComTextHelper.GetDebugText(text)}'"); + } + } +} diff --git a/src/PepperDash.Core/Comm/eControlMethods.cs b/src/PepperDash.Core/Comm/eControlMethods.cs index 695594a1..b807fdc5 100644 --- a/src/PepperDash.Core/Comm/eControlMethods.cs +++ b/src/PepperDash.Core/Comm/eControlMethods.cs @@ -78,6 +78,10 @@ namespace PepperDash.Core /// /// Used when comms needs to be handled in SIMPL and bridged opposite the normal direction /// - ComBridge + ComBridge, + /// + /// InfinetEX control + /// + InfinetEx } } \ No newline at end of file diff --git a/src/PepperDash.Core/Comm/eStreamDebuggingDataTypeSettings.cs b/src/PepperDash.Core/Comm/eStreamDebuggingDataTypeSettings.cs new file mode 100644 index 00000000..f5fbfa2b --- /dev/null +++ b/src/PepperDash.Core/Comm/eStreamDebuggingDataTypeSettings.cs @@ -0,0 +1,24 @@ +using System; + +namespace PepperDash.Core +{ + /// + /// The available settings for stream debugging data format types + /// + [Flags] + public enum eStreamDebuggingDataTypeSettings + { + /// + /// Debug data in byte format + /// + Bytes = 0, + /// + /// Debug data in text format + /// + Text = 1, + /// + /// Debug data in both byte and text formats + /// + Both = Bytes | Text + } +} diff --git a/src/PepperDash.Core/Comm/eStreamDebuggingSetting.cs b/src/PepperDash.Core/Comm/eStreamDebuggingSetting.cs new file mode 100644 index 00000000..f9f7eb3f --- /dev/null +++ b/src/PepperDash.Core/Comm/eStreamDebuggingSetting.cs @@ -0,0 +1,28 @@ +using System; + +namespace PepperDash.Core +{ + /// + /// The available settings for stream debugging + /// + [Flags] + public enum eStreamDebuggingSetting + { + /// + /// Debug off + /// + Off = 0, + /// + /// Debug received data + /// + Rx = 1, + /// + /// Debug transmitted data + /// + Tx = 2, + /// + /// Debug both received and transmitted data + /// + Both = Rx | Tx + } +} diff --git a/src/PepperDash.Core/CommunicationExtras.cs b/src/PepperDash.Core/CommunicationExtras.cs index d16ea761..a8bc57d3 100644 --- a/src/PepperDash.Core/CommunicationExtras.cs +++ b/src/PepperDash.Core/CommunicationExtras.cs @@ -1,10 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; -using System.Text.RegularExpressions; using Newtonsoft.Json; namespace PepperDash.Core @@ -42,7 +39,7 @@ namespace PepperDash.Core /// Defines the contract for IBasicCommunication /// public interface IBasicCommunication : ICommunicationReceiver - { + { /// /// Send text to the device /// @@ -54,7 +51,7 @@ namespace PepperDash.Core /// /// void SendBytes(byte[] bytes); - } + } /// /// Represents a device that implements IBasicCommunication and IStreamDebugging @@ -67,7 +64,7 @@ namespace PepperDash.Core /// /// Represents a device with stream debugging capablities /// - public interface IStreamDebugging + public interface IStreamDebugging : IKeyed { /// /// Object to enable stream debugging @@ -76,12 +73,12 @@ namespace PepperDash.Core CommunicationStreamDebugging StreamDebugging { get; } } - /// - /// For IBasicCommunication classes that have SocketStatus. GenericSshClient, - /// GenericTcpIpClient - /// - public interface ISocketStatus : IBasicCommunication - { + /// + /// For IBasicCommunication classes that have SocketStatus. GenericSshClient, + /// GenericTcpIpClient + /// + public interface ISocketStatus : IBasicCommunication + { /// /// Notifies of socket status changes /// @@ -93,7 +90,7 @@ namespace PepperDash.Core [JsonProperty("clientStatus")] [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] SocketStatus ClientStatus { get; } - } + } /// /// Describes a device that implements ISocketStatus and IStreamDebugging @@ -107,24 +104,24 @@ namespace PepperDash.Core /// Describes a device that can automatically attempt to reconnect /// public interface IAutoReconnect - { + { /// /// Enable automatic recconnect /// [JsonProperty("autoReconnect")] - bool AutoReconnect { get; set; } + bool AutoReconnect { get; set; } /// /// Interval in ms to attempt automatic recconnections /// [JsonProperty("autoReconnectIntervalMs")] - int AutoReconnectIntervalMs { get; set; } - } + int AutoReconnectIntervalMs { get; set; } + } - /// - /// - /// - public enum eGenericCommMethodStatusChangeType - { + /// + /// + /// + public enum eGenericCommMethodStatusChangeType + { /// /// Connected /// @@ -133,45 +130,45 @@ namespace PepperDash.Core /// Disconnected /// Disconnected - } + } - /// - /// This delegate defines handler for IBasicCommunication status changes - /// - /// Device firing the status change - /// - public delegate void GenericCommMethodStatusHandler(IBasicCommunication comm, eGenericCommMethodStatusChangeType status); + /// + /// This delegate defines handler for IBasicCommunication status changes + /// + /// Device firing the status change + /// + public delegate void GenericCommMethodStatusHandler(IBasicCommunication comm, eGenericCommMethodStatusChangeType status); - /// - /// - /// - public class GenericCommMethodReceiveBytesArgs : EventArgs - { - /// - /// Gets or sets the Bytes - /// - public byte[] Bytes { get; private set; } + /// + /// + /// + public class GenericCommMethodReceiveBytesArgs : EventArgs + { + /// + /// Gets or sets the Bytes + /// + public byte[] Bytes { get; private set; } /// /// /// /// public GenericCommMethodReceiveBytesArgs(byte[] bytes) - { - Bytes = bytes; - } + { + Bytes = bytes; + } - /// - /// S+ Constructor - /// - public GenericCommMethodReceiveBytesArgs() { } - } + /// + /// S+ Constructor + /// + public GenericCommMethodReceiveBytesArgs() { } + } - /// - /// - /// - public class GenericCommMethodReceiveTextArgs : EventArgs - { + /// + /// + /// + public class GenericCommMethodReceiveTextArgs : EventArgs + { /// /// /// @@ -185,9 +182,9 @@ namespace PepperDash.Core /// /// public GenericCommMethodReceiveTextArgs(string text) - { - Text = text; - } + { + Text = text; + } /// /// @@ -195,59 +192,14 @@ namespace PepperDash.Core /// /// public GenericCommMethodReceiveTextArgs(string text, string delimiter) - :this(text) + : this(text) { Delimiter = delimiter; } - /// - /// S+ Constructor - /// - public GenericCommMethodReceiveTextArgs() { } - } - - - - /// - /// - /// - public class ComTextHelper - { /// - /// Gets escaped text for a byte array + /// S+ Constructor /// - /// - /// - public static string GetEscapedText(byte[] bytes) - { - return String.Concat(bytes.Select(b => string.Format(@"[{0:X2}]", (int)b)).ToArray()); - } - - /// - /// Gets escaped text for a string - /// - /// - /// - /// - /// GetEscapedText method - /// - public static string GetEscapedText(string text) - { - var bytes = Encoding.GetEncoding(28591).GetBytes(text); - return String.Concat(bytes.Select(b => string.Format(@"[{0:X2}]", (int)b)).ToArray()); - } - - /// - /// Gets debug text for a string - /// - /// - /// - /// - /// GetDebugText method - /// - public static string GetDebugText(string text) - { - return Regex.Replace(text, @"[^\u0020-\u007E]", a => GetEscapedText(a.Value)); - } - } + public GenericCommMethodReceiveTextArgs() { } + } } \ No newline at end of file diff --git a/src/PepperDash.Core/Config/PortalConfigReader.cs b/src/PepperDash.Core/Config/PortalConfigReader.cs index ffb2d16b..41ca2cbd 100644 --- a/src/PepperDash.Core/Config/PortalConfigReader.cs +++ b/src/PepperDash.Core/Config/PortalConfigReader.cs @@ -9,40 +9,59 @@ using Serilog.Events; namespace PepperDash.Core.Config { + + /// /// Reads a Portal formatted config file /// public class PortalConfigReader { - /// - /// Reads the config file, checks if it needs a merge, merges and saves, then returns the merged Object. - /// - /// JObject of config file - public static void ReadAndMergeFileIfNecessary(string filePath, string savePath) + const string template = "template"; + const string system = "system"; + const string systemUrl = "system_url"; + const string templateUrl = "template_url"; + const string info = "info"; + const string devices = "devices"; + const string rooms = "rooms"; + const string sourceLists = "sourceLists"; + const string destinationLists = "destinationLists"; + const string cameraLists = "cameraLists"; + const string audioControlPointLists = "audioControlPointLists"; + + const string tieLines = "tieLines"; + const string joinMaps = "joinMaps"; + const string global = "global"; + + + /// + /// Reads the config file, checks if it needs a merge, merges and saves, then returns the merged Object. + /// + /// JObject of config file + public static void ReadAndMergeFileIfNecessary(string filePath, string savePath) { try { if (!File.Exists(filePath)) { - Debug.Console(1, Debug.ErrorLogLevel.Error, + Debug.LogError( "ERROR: Configuration file not present. Please load file to {0} and reset program", filePath); } using (StreamReader fs = new StreamReader(filePath)) { var jsonObj = JObject.Parse(fs.ReadToEnd()); - if(jsonObj["template"] != null && jsonObj["system"] != null) + if(jsonObj[template] != null && jsonObj[system] != null) { // it's a double-config, merge it. var merged = MergeConfigs(jsonObj); - if (jsonObj["system_url"] != null) + if (jsonObj[systemUrl] != null) { - merged["systemUrl"] = jsonObj["system_url"].Value(); + merged[systemUrl] = jsonObj[systemUrl].Value(); } - if (jsonObj["template_url"] != null) + if (jsonObj[templateUrl] != null) { - merged["templateUrl"] = jsonObj["template_url"].Value(); + merged[templateUrl] = jsonObj[templateUrl].Value(); } jsonObj = merged; @@ -77,62 +96,62 @@ namespace PepperDash.Core.Config var merged = new JObject(); // Put together top-level objects - if (system["info"] != null) - merged.Add("info", Merge(template["info"], system["info"], "infO")); + if (system[info] != null) + merged.Add(info, Merge(template[info], system[info], info)); else - merged.Add("info", template["info"]); + merged.Add(info, template[info]); - merged.Add("devices", MergeArraysOnTopLevelProperty(template["devices"] as JArray, - system["devices"] as JArray, "key", "devices")); + merged.Add(devices, MergeArraysOnTopLevelProperty(template[devices] as JArray, + system[devices] as JArray, "key", devices)); - if (system["rooms"] == null) - merged.Add("rooms", template["rooms"]); + if (system[rooms] == null) + merged.Add(rooms, template[rooms]); else - merged.Add("rooms", MergeArraysOnTopLevelProperty(template["rooms"] as JArray, - system["rooms"] as JArray, "key", "rooms")); + merged.Add(rooms, MergeArraysOnTopLevelProperty(template[rooms] as JArray, + system[rooms] as JArray, "key", rooms)); - if (system["sourceLists"] == null) - merged.Add("sourceLists", template["sourceLists"]); + if (system[sourceLists] == null) + merged.Add(sourceLists, template[sourceLists]); else - merged.Add("sourceLists", Merge(template["sourceLists"], system["sourceLists"], "sourceLists")); + merged.Add(sourceLists, Merge(template[sourceLists], system[sourceLists], sourceLists)); - if (system["destinationLists"] == null) - merged.Add("destinationLists", template["destinationLists"]); + if (system[destinationLists] == null) + merged.Add(destinationLists, template[destinationLists]); else - merged.Add("destinationLists", - Merge(template["destinationLists"], system["destinationLists"], "destinationLists")); + merged.Add(destinationLists, + Merge(template[destinationLists], system[destinationLists], destinationLists)); - if (system["cameraLists"] == null) - merged.Add("cameraLists", template["cameraLists"]); + if (system[cameraLists] == null) + merged.Add(cameraLists, template[cameraLists]); else - merged.Add("cameraLists", Merge(template["cameraLists"], system["cameraLists"], "cameraLists")); + merged.Add(cameraLists, Merge(template[cameraLists], system[cameraLists], cameraLists)); - if (system["audioControlPointLists"] == null) - merged.Add("audioControlPointLists", template["audioControlPointLists"]); + if (system[audioControlPointLists] == null) + merged.Add(audioControlPointLists, template[audioControlPointLists]); else - merged.Add("audioControlPointLists", - Merge(template["audioControlPointLists"], system["audioControlPointLists"], "audioControlPointLists")); + merged.Add(audioControlPointLists, + Merge(template[audioControlPointLists], system[audioControlPointLists], audioControlPointLists)); // Template tie lines take precedence. Config tool doesn't do them at system // level anyway... - if (template["tieLines"] != null) - merged.Add("tieLines", template["tieLines"]); - else if (system["tieLines"] != null) - merged.Add("tieLines", system["tieLines"]); + if (template[tieLines] != null) + merged.Add(tieLines, template[tieLines]); + else if (system[tieLines] != null) + merged.Add(tieLines, system[tieLines]); else - merged.Add("tieLines", new JArray()); + merged.Add(tieLines, new JArray()); - if (template["joinMaps"] != null) - merged.Add("joinMaps", template["joinMaps"]); + if (template[joinMaps] != null) + merged.Add(joinMaps, template[joinMaps]); else - merged.Add("joinMaps", new JObject()); + merged.Add(joinMaps, new JObject()); - if (system["global"] != null) - merged.Add("global", Merge(template["global"], system["global"], "global")); + if (system[global] != null) + merged.Add(global, Merge(template[global], system[global], global)); else - merged.Add("global", template["global"]); + merged.Add(global, template[global]); //Debug.Console(2, "MERGED CONFIG RESULT: \x0d\x0a{0}", merged); return merged; @@ -228,7 +247,7 @@ namespace PepperDash.Core.Config } catch (Exception e) { - Debug.Console(1, Debug.ErrorLogLevel.Warning, "Cannot merge items at path {0}: \r{1}", propPath, e); + Debug.LogError($"Cannot merge items at path {propPath}: \r{e}"); } } } diff --git a/src/PepperDash.Core/Logging/Debug.cs b/src/PepperDash.Core/Logging/Debug.cs index 62a95f2c..877d8997 100644 --- a/src/PepperDash.Core/Logging/Debug.cs +++ b/src/PepperDash.Core/Logging/Debug.cs @@ -168,7 +168,7 @@ namespace PepperDash.Core .WriteTo.File(new RenderedCompactJsonFormatter(), logFilePath, rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: LogEventLevel.Debug, - retainedFileCountLimit: CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? 30 : 60, + retainedFileCountLimit: CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? 7 : 14, levelSwitch: _fileLogLevelSwitch ); @@ -1081,9 +1081,6 @@ namespace PepperDash.Core /// Logs to Console when at-level, and all messages to error log /// [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] - /// - /// Console method - /// public static void Console(uint level, ErrorLogLevel errorLogLevel, string format, params object[] items) { @@ -1096,9 +1093,6 @@ namespace PepperDash.Core /// it will only be written to the log. /// [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] - /// - /// ConsoleWithLog method - /// public static void ConsoleWithLog(uint level, string format, params object[] items) { LogMessage(level, format, items); diff --git a/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs index 7dacce5c..2dd4f2ba 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs @@ -12,15 +12,15 @@ using Serilog.Events; namespace PepperDash.Essentials.Core { - /// - /// Represents a CecPortController - /// - public class CecPortController : Device, IBasicCommunicationWithStreamDebugging + /// + /// Represents a CecPortController + /// + public class CecPortController : Device, IBasicCommunicationWithStreamDebugging { - /// - /// Gets or sets the StreamDebugging - /// - public CommunicationStreamDebugging StreamDebugging { get; private set; } + /// + /// Gets or sets the StreamDebugging + /// + public CommunicationStreamDebugging StreamDebugging { get; private set; } public event EventHandler BytesReceived; public event EventHandler TextReceived; @@ -33,16 +33,16 @@ namespace PepperDash.Essentials.Core ICec Port; public CecPortController(string key, Func postActivationFunc, - EssentialsControlPropertiesConfig config):base(key) + EssentialsControlPropertiesConfig config) : base(key) { - StreamDebugging = new CommunicationStreamDebugging(key); + StreamDebugging = new CommunicationStreamDebugging(key); AddPostActivationAction(() => { Port = postActivationFunc(config); Port.StreamCec.CecChange += StreamCec_CecChange; - }); + }); } public CecPortController(string key, ICec port) @@ -58,27 +58,25 @@ namespace PepperDash.Essentials.Core if (args.EventId == CecEventIds.CecMessageReceivedEventId) OnDataReceived(cecDevice.Received.StringValue); else if (args.EventId == CecEventIds.ErrorFeedbackEventId) - if(cecDevice.ErrorFeedback.BoolValue) + if (cecDevice.ErrorFeedback.BoolValue) Debug.LogMessage(LogEventLevel.Verbose, this, "CEC NAK Error"); } void OnDataReceived(string s) { - var bytesHandler = BytesReceived; + var bytesHandler = BytesReceived; if (bytesHandler != null) { var bytes = Encoding.GetEncoding(28591).GetBytes(s); - if (StreamDebugging.RxStreamDebuggingIsEnabled) - Debug.LogMessage(LogEventLevel.Information, this, "Received: '{0}'", ComTextHelper.GetEscapedText(bytes)); + this.PrintReceivedBytes(bytes); bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); } var textHandler = TextReceived; - if (textHandler != null) - { - if (StreamDebugging.RxStreamDebuggingIsEnabled) - Debug.LogMessage(LogEventLevel.Information, this, "Received: '{0}'", s); - textHandler(this, new GenericCommMethodReceiveTextArgs(s)); - } + if (textHandler != null) + { + this.PrintReceivedText(s); + textHandler(this, new GenericCommMethodReceiveTextArgs(s)); + } } #region IBasicCommunication Members @@ -90,8 +88,7 @@ namespace PepperDash.Essentials.Core { if (Port == null) return; - if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} characters of text: '{1}'", text.Length, text); + this.PrintSentText(text); Port.StreamCec.Send.StringValue = text; } @@ -103,8 +100,8 @@ namespace PepperDash.Essentials.Core if (Port == null) return; var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); - if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); + this.PrintSentBytes(bytes); + Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); Port.StreamCec.Send.StringValue = text; } diff --git a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs index cc57fa19..ec679fae 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs @@ -1,59 +1,78 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using System.Text.RegularExpressions; using Crestron.SimplSharp; using Crestron.SimplSharpPro; - +using Crestron.SimplSharpPro.GeneralIO; using PepperDash.Core; +using PepperDash.Core.Logging; using Serilog.Events; namespace PepperDash.Essentials.Core { - /// - /// Represents a ComPortController - /// + /// + /// Represents a ComPortController + /// public class ComPortController : Device, IBasicCommunicationWithStreamDebugging { - /// - /// Gets or sets the StreamDebugging - /// - public CommunicationStreamDebugging StreamDebugging { get; private set; } + /// + /// Gets or sets the StreamDebugging + /// + public CommunicationStreamDebugging StreamDebugging { get; private set; } + /// + /// Event fired when bytes are received + /// public event EventHandler BytesReceived; + + /// + /// Event fired when text is received + /// public event EventHandler TextReceived; - /// - /// Gets or sets the IsConnected - /// + /// + /// Gets or sets the IsConnected + /// public bool IsConnected { get { return true; } } ComPort Port; ComPort.ComPortSpec Spec; - public ComPortController(string key, Func postActivationFunc, - ComPort.ComPortSpec spec, EssentialsControlPropertiesConfig config) : base(key) - { - StreamDebugging = new CommunicationStreamDebugging(key); + /// + /// Constructor + /// + /// + /// + /// + /// + public ComPortController(string key, Func postActivationFunc, + ComPort.ComPortSpec spec, EssentialsControlPropertiesConfig config) : base(key) + { + StreamDebugging = new CommunicationStreamDebugging(key); - Spec = spec; + Spec = spec; - AddPostActivationAction(() => - { - Port = postActivationFunc(config); + AddPostActivationAction(() => + { + Port = postActivationFunc(config); - RegisterAndConfigureComPort(); - }); - } + RegisterAndConfigureComPort(); + }); + } + /// + /// Constructor + /// + /// Device key + /// COM port instance + /// COM port specification public ComPortController(string key, ComPort port, ComPort.ComPortSpec spec) : base(key) { if (port == null) { - Debug.LogMessage(LogEventLevel.Information, this, "ERROR: Invalid com port, continuing but comms will not function"); + Debug.LogMessage(LogEventLevel.Information, this, "ERROR: Invalid com port, continuing but comms will not function"); return; } @@ -64,71 +83,77 @@ namespace PepperDash.Essentials.Core RegisterAndConfigureComPort(); } - private void RegisterAndConfigureComPort() - { - if (Port == null) - { - Debug.LogMessage(LogEventLevel.Information, this, "Configured com Port for this device does not exist."); - return; - } - if (Port.Parent is CrestronControlSystem) - { - var result = Port.Register(); - if (result != eDeviceRegistrationUnRegistrationResponse.Success) - { - Debug.LogMessage(LogEventLevel.Information, this, "ERROR: Cannot register Com port: {0}", result); - return; // false - } - } + private void RegisterAndConfigureComPort() + { + if (Port == null) + { + this.LogInformation($"Configured {Port.Parent.GetType().Name}-comport-{Port.ID} for {Key} does not exist."); + return; + } - var specResult = Port.SetComPortSpec(Spec); - if (specResult != 0) - { - Debug.LogMessage(LogEventLevel.Information, this, "WARNING: Cannot set comspec"); - return; - } - Port.SerialDataReceived += Port_SerialDataReceived; - } - ~ComPortController() + if (Port.Parent is CrestronControlSystem || Port.Parent is CenIoCom102) + { + var result = Port.Register(); + if (result != eDeviceRegistrationUnRegistrationResponse.Success) + { + this.LogError($"Cannot register {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {result})"); + return; + } + this.LogInformation($"Successfully registered {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {result})"); + } + + var specResult = Port.SetComPortSpec(Spec); + if (specResult != 0) + { + this.LogError($"Cannot set comspec for {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {specResult})"); + return; + } + this.LogInformation($"Successfully set comspec for {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {specResult})"); + + Port.SerialDataReceived += Port_SerialDataReceived; + } + + /// + /// Destructor + /// + ~ComPortController() { Port.SerialDataReceived -= Port_SerialDataReceived; } void Port_SerialDataReceived(ComPort ReceivingComPort, ComPortSerialDataEventArgs args) { - OnDataReceived(args.SerialData); + OnDataReceived(args.SerialData); } - void OnDataReceived(string s) - { + void OnDataReceived(string s) + { var eventSubscribed = false; - var bytesHandler = BytesReceived; - if (bytesHandler != null) - { - var bytes = Encoding.GetEncoding(28591).GetBytes(s); - if (StreamDebugging.RxStreamDebuggingIsEnabled) - Debug.LogMessage(LogEventLevel.Information, this, "Received: '{0}'", ComTextHelper.GetEscapedText(bytes)); - bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); + var bytesHandler = BytesReceived; + if (bytesHandler != null) + { + var bytes = Encoding.GetEncoding(28591).GetBytes(s); + this.PrintReceivedBytes(bytes); + bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); eventSubscribed = true; - } - var textHandler = TextReceived; - if (textHandler != null) - { - if (StreamDebugging.RxStreamDebuggingIsEnabled) - Debug.LogMessage(LogEventLevel.Information, this, "Received: '{0}'", s); - textHandler(this, new GenericCommMethodReceiveTextArgs(s)); + } + var textHandler = TextReceived; + if (textHandler != null) + { + this.PrintReceivedText(s); + textHandler(this, new GenericCommMethodReceiveTextArgs(s)); eventSubscribed = true; - } + } - if(!eventSubscribed) Debug.LogMessage(LogEventLevel.Warning, this, "Received data but no handler is registered"); - } + if (!eventSubscribed) Debug.LogMessage(LogEventLevel.Warning, this, "Received data but no handler is registered"); + } - /// - /// Deactivate method - /// - /// + /// + /// Deactivate method + /// + /// public override bool Deactivate() { return Port.UnRegister() == eDeviceRegistrationUnRegistrationResponse.Success; @@ -136,70 +161,68 @@ namespace PepperDash.Essentials.Core #region IBasicCommunication Members - /// - /// SendText method - /// + /// + /// SendText method + /// public void SendText(string text) { if (Port == null) return; - if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} characters of text: '{1}'", text.Length, text); - Port.Send(text); + this.PrintSentText(text); + Port.Send(text); } - /// - /// SendBytes method - /// + /// + /// SendBytes method + /// public void SendBytes(byte[] bytes) { if (Port == null) return; var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); - if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); + this.PrintSentBytes(bytes); Port.Send(text); } - /// - /// Connect method - /// + /// + /// Connect method + /// public void Connect() - { + { } - /// - /// Disconnect method - /// + /// + /// Disconnect method + /// public void Disconnect() { } #endregion - /// - /// - /// - /// - /// - /// SimulateReceive method - /// - public void SimulateReceive(string s) - { - // split out hex chars and build string - var split = Regex.Split(s, @"(\\[Xx][0-9a-fA-F][0-9a-fA-F])"); - StringBuilder b = new StringBuilder(); - foreach (var t in split) - { - if (t.StartsWith(@"\") && t.Length == 4) - b.Append((char)(Convert.ToByte(t.Substring(2, 2), 16))); - else - b.Append(t); - } + /// + /// + /// + /// + /// + /// SimulateReceive method + /// + public void SimulateReceive(string s) + { + // split out hex chars and build string + var split = Regex.Split(s, @"(\\[Xx][0-9a-fA-F][0-9a-fA-F])"); + StringBuilder b = new StringBuilder(); + foreach (var t in split) + { + if (t.StartsWith(@"\") && t.Length == 4) + b.Append((char)(Convert.ToByte(t.Substring(2, 2), 16))); + else + b.Append(t); + } - OnDataReceived(b.ToString()); - } + OnDataReceived(b.ToString()); + } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs b/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs index a0723c4b..fbc46579 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs @@ -64,8 +64,11 @@ namespace PepperDash.Essentials.Core break; case eControlMethod.Ssh: { - var ssh = new GenericSshClient(deviceConfig.Key + "-ssh", c.Address, c.Port, c.Username, c.Password); - ssh.AutoReconnect = c.AutoReconnect; + var ssh = new GenericSshClient(deviceConfig.Key + "-ssh", c.Address, c.Port, c.Username, c.Password) + { + AutoReconnect = c.AutoReconnect, + DisableEcho = c.DisableSshEcho + }; if (ssh.AutoReconnect) ssh.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs; comm = ssh; @@ -73,8 +76,10 @@ namespace PepperDash.Essentials.Core } case eControlMethod.Tcpip: { - var tcp = new GenericTcpIpClient(deviceConfig.Key + "-tcp", c.Address, c.Port, c.BufferSize); - tcp.AutoReconnect = c.AutoReconnect; + var tcp = new GenericTcpIpClient(deviceConfig.Key + "-tcp", c.Address, c.Port, c.BufferSize) + { + AutoReconnect = c.AutoReconnect + }; if (tcp.AutoReconnect) tcp.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs; comm = tcp; @@ -90,8 +95,10 @@ namespace PepperDash.Essentials.Core break; case eControlMethod.SecureTcpIp: { - var secureTcp = new GenericSecureTcpIpClient(deviceConfig.Key + "-secureTcp", c.Address, c.Port, c.BufferSize); - secureTcp.AutoReconnect = c.AutoReconnect; + var secureTcp = new GenericSecureTcpIpClient(deviceConfig.Key + "-secureTcp", c.Address, c.Port, c.BufferSize) + { + AutoReconnect = c.AutoReconnect + }; if (secureTcp.AutoReconnect) secureTcp.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs; comm = secureTcp; diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs index b93de1c4..2591e8aa 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs @@ -124,22 +124,35 @@ namespace PepperDash.Essentials.Core.Config Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Local Config"); return true; - } + } else { - var doubleObj = JObject.Parse(fs.ReadToEnd()); - ConfigObject = PortalConfigReader.MergeConfigs(doubleObj).ToObject(); + var parsedConfig = JObject.Parse(fs.ReadToEnd()); - // Extract SystemUrl and TemplateUrl into final config output - - if (doubleObj["system_url"] != null) + // Check if it's a v2 config (check for "version" node) + // this means it's already merged by the Portal API + // from the v2 config tool + var isV2Config = parsedConfig["versions"] != null; + + if (isV2Config) { - ConfigObject.SystemUrl = doubleObj["system_url"].Value(); + Debug.LogMessage(LogEventLevel.Information, "Config file is a v2 format, no merge necessary."); + ConfigObject = parsedConfig.ToObject(); + Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded v2 Config"); + return true; } - if (doubleObj["template_url"] != null) + // Extract SystemUrl and TemplateUrl into final config output + ConfigObject = PortalConfigReader.MergeConfigs(parsedConfig).ToObject(); + + if (parsedConfig["system_url"] != null) { - ConfigObject.TemplateUrl = doubleObj["template_url"].Value(); + ConfigObject.SystemUrl = parsedConfig["system_url"].Value(); + } + + if (parsedConfig["template_url"] != null) + { + ConfigObject.TemplateUrl = parsedConfig["template_url"].Value(); } } diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs index 630ffbdf..6ffe07d2 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs @@ -11,86 +11,169 @@ using PepperDash.Core; namespace PepperDash.Essentials.Core.Config { - /// - /// Loads the ConfigObject from the file - /// - public class EssentialsConfig : BasicConfig - { - [JsonProperty("system_url")] + /// + /// Loads the ConfigObject from the file + /// + public class EssentialsConfig : BasicConfig + { + /// + /// Gets or sets the SystemUrl + /// + [JsonProperty("system_url")] public string SystemUrl { get; set; } - [JsonProperty("template_url")] + /// + /// Gets or sets the TemplateUrl + /// + [JsonProperty("template_url")] public string TemplateUrl { get; set; } - + /// + /// Gets the SystemUuid extracted from the SystemUrl + /// [JsonProperty("systemUuid")] - public string SystemUuid + public string SystemUuid { get { - if (string.IsNullOrEmpty(SystemUrl)) - return "missing url"; + string uuid; - if (SystemUrl.Contains("#")) - { - var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/#.*"); - string uuid = result.Groups[1].Value; - return uuid; - } else - { + if (string.IsNullOrEmpty(SystemUrl)) + { + uuid = "missing url"; + } + else if (SystemUrl.Contains("#")) + { + var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/#.*"); + uuid = result.Groups[1].Value; + } + else if (SystemUrl.Contains("detail")) + { + var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/detail\/(.*)\/.*"); + uuid = result.Groups[1].Value; + } + else + { var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/.*"); - string uuid = result.Groups[1].Value; - return uuid; + uuid = result.Groups[1].Value; } + + return uuid; } } + /// + /// Gets the TemplateUuid extracted from the TemplateUrl + /// [JsonProperty("templateUuid")] - public string TemplateUuid + public string TemplateUuid { get { - if (string.IsNullOrEmpty(TemplateUrl)) - return "missing template url"; + string uuid; - if (TemplateUrl.Contains("#")) - { - var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/templates\/(.*)\/#.*"); - string uuid = result.Groups[1].Value; - return uuid; - } else - { - var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/system-templates\/(.*)\/system-template-versions\/(.*)\/.*"); - string uuid = result.Groups[2].Value; - return uuid; + if (string.IsNullOrEmpty(TemplateUrl)) + { + uuid = "missing template url"; } + else if (TemplateUrl.Contains("#")) + { + var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/templates\/(.*)\/#.*"); + uuid = result.Groups[1].Value; + } + else if (TemplateUrl.Contains("detail")) + { + var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/system-templates\/detail\/(.*)\/system-template-versions\/detail\/(.*)\/.*"); + uuid = result.Groups[2].Value; + } + else + { + var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/system-templates\/(.*)\/system-template-versions\/(.*)\/.*"); + uuid = result.Groups[2].Value; + } + + return uuid; } } - [JsonProperty("rooms")] /// /// Gets or sets the Rooms /// + [JsonProperty("rooms")] public List Rooms { get; set; } + /// + /// Gets or sets the Versions + /// + public VersionData Versions { get; set; } + /// + /// Initializes a new instance of the class. + /// public EssentialsConfig() : base() { Rooms = new List(); } - } - - /// - /// Represents a SystemTemplateConfigs - /// - public class SystemTemplateConfigs - { - /// - /// Gets or sets the System - /// + } + + /// + /// Represents version data for Essentials and its packages + /// + public class VersionData + { + /// + /// Gets or sets the Essentials version + /// + [JsonProperty("essentials")] + public NugetVersion Essentials { get; set; } + + /// + /// Gets or sets the list of Packages + /// + [JsonProperty("packages")] + public List Packages { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public VersionData() + { + Packages = new List(); + } + } + + /// + /// Represents a NugetVersion + /// + public class NugetVersion + { + /// + /// Gets or sets the Version + /// + [JsonProperty("version")] + public string Version { get; set; } + + /// + /// Gets or sets the PackageId + /// + [JsonProperty("packageId")] + public string PackageId { get; set; } + } + + /// + /// Represents a SystemTemplateConfigs + /// + public class SystemTemplateConfigs + { + /// + /// Gets or sets the System + /// public EssentialsConfig System { get; set; } + /// + /// Gets or sets the Template + /// public EssentialsConfig Template { get; set; } - } + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs b/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs index 1c9709b8..c199773a 100644 --- a/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs +++ b/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs @@ -60,9 +60,9 @@ namespace PepperDash.Essentials.Core ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(DeviceJsonApi.DoDeviceActionWithJson, "devjson", "", ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(s => CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetProperties(s)), "devprops", "", ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(s => CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetMethods(s)), "devmethods", "", ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(s => CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetApiMethods(s)), "apimethods", "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetProperties(s).Replace(Environment.NewLine, "\r\n")), "devprops", "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetMethods(s).Replace(Environment.NewLine, "\r\n")), "devmethods", "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetApiMethods(s).Replace(Environment.NewLine, "\r\n")), "apimethods", "", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(SimulateComReceiveOnDevice, "devsimreceive", "Simulates incoming data on a com device", ConsoleAccessLevelEnum.AccessOperator); diff --git a/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceRoomFusionRoomJoinMap.cs b/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceRoomFusionRoomJoinMap.cs index d6a7bffa..ba638623 100644 --- a/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceRoomFusionRoomJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceRoomFusionRoomJoinMap.cs @@ -16,121 +16,201 @@ namespace PepperDash.Essentials.Core.Fusion { // Processor Attributes + /// + /// Processor IP 1 + /// [JoinName("ProcessorIp1")] public JoinDataComplete ProcessorIp1 = new JoinDataComplete(new JoinData { JoinNumber = 50, JoinSpan = 1, AttributeName = "Info - Processor - IP 1" }, new JoinMetadata { Description = "Info - Processor - IP 1", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor IP 2 + /// [JoinName("ProcessorIp2")] public JoinDataComplete ProcessorIp2 = new JoinDataComplete(new JoinData { JoinNumber = 51, JoinSpan = 1, AttributeName = "Info - Processor - IP 2" }, new JoinMetadata { Description = "Info - Processor - IP 2", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor Gateway + /// [JoinName("ProcessorGateway")] public JoinDataComplete ProcessorGateway = new JoinDataComplete(new JoinData { JoinNumber = 52, JoinSpan = 1, AttributeName = "Info - Processor - Gateway" }, new JoinMetadata { Description = "Info - Processor - Gateway", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor Hostname + /// [JoinName("ProcessorHostname")] public JoinDataComplete ProcessorHostname = new JoinDataComplete(new JoinData { JoinNumber = 53, JoinSpan = 1, AttributeName = "Info - Processor - Hostname" }, new JoinMetadata { Description = "Info - Processor - Hostname", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor Domain + /// [JoinName("ProcessorDomain")] public JoinDataComplete ProcessorDomain = new JoinDataComplete(new JoinData { JoinNumber = 54, JoinSpan = 1, AttributeName = "Info - Processor - Domain" }, new JoinMetadata { Description = "Info - Processor - Domain", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor DNS 1 + /// [JoinName("ProcessorDns1")] public JoinDataComplete ProcessorDns1 = new JoinDataComplete(new JoinData { JoinNumber = 55, JoinSpan = 1, AttributeName = "Info - Processor - DNS 1" }, new JoinMetadata { Description = "Info - Processor - DNS 1", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor DNS 2 + /// [JoinName("ProcessorDns2")] public JoinDataComplete ProcessorDns2 = new JoinDataComplete(new JoinData { JoinNumber = 56, JoinSpan = 1, AttributeName = "Info - Processor - DNS 2" }, new JoinMetadata { Description = "Info - Processor - DNS 2", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor MAC 1 + /// [JoinName("ProcessorMac1")] public JoinDataComplete ProcessorMac1 = new JoinDataComplete(new JoinData { JoinNumber = 57, JoinSpan = 1, AttributeName = "Info - Processor - MAC 1" }, new JoinMetadata { Description = "Info - Processor - MAC 1", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor MAC 2 + /// [JoinName("ProcessorMac2")] public JoinDataComplete ProcessorMac2 = new JoinDataComplete(new JoinData { JoinNumber = 58, JoinSpan = 1, AttributeName = "Info - Processor - MAC 2" }, new JoinMetadata { Description = "Info - Processor - MAC 2", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor Net Mask 1 + /// [JoinName("ProcessorNetMask1")] public JoinDataComplete ProcessorNetMask1 = new JoinDataComplete(new JoinData { JoinNumber = 59, JoinSpan = 1, AttributeName = "Info - Processor - Net Mask 1" }, new JoinMetadata { Description = "Info - Processor - Net Mask 1", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor Net Mask 2 + /// [JoinName("ProcessorNetMask2")] public JoinDataComplete ProcessorNetMask2 = new JoinDataComplete(new JoinData { JoinNumber = 60, JoinSpan = 1, AttributeName = "Info - Processor - Net Mask 2" }, new JoinMetadata { Description = "Info - Processor - Net Mask 2", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor Firmware + /// [JoinName("ProcessorFirmware")] public JoinDataComplete ProcessorFirmware = new JoinDataComplete(new JoinData { JoinNumber = 61, JoinSpan = 1, AttributeName = "Info - Processor - Firmware" }, new JoinMetadata { Description = "Info - Processor - Firmware", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Program Name Start + /// [JoinName("ProgramNameStart")] public JoinDataComplete ProgramNameStart = new JoinDataComplete(new JoinData { JoinNumber = 62, JoinSpan = 10, AttributeName = "Info - Processor - Program" }, new JoinMetadata { Description = "Info - Processor - Program", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor Reboot + /// [JoinName("ProcessorReboot")] public JoinDataComplete ProcessorReboot = new JoinDataComplete(new JoinData { JoinNumber = 74, JoinSpan = 1, AttributeName = "Processor - Reboot" }, new JoinMetadata { Description = "Processor - Reboot", JoinCapabilities = eJoinCapabilities.FromFusion, JoinType = eJoinType.Digital }); // Volume Controls + /// + /// Volume Fader 1 + /// [JoinName("VolumeFader1")] public JoinDataComplete VolumeFader1 = new JoinDataComplete(new JoinData { JoinNumber = 50, JoinSpan = 1, AttributeName = "Volume - Fader01" }, new JoinMetadata { Description = "Volume - Fader01", JoinCapabilities = eJoinCapabilities.ToFromFusion, JoinType = eJoinType.Analog }); // Codec Info + /// + /// VC Codec In Call + /// [JoinName("VcCodecInCall")] public JoinDataComplete VcCodecInCall = new JoinDataComplete(new JoinData { JoinNumber = 69, JoinSpan = 1, AttributeName = "Conf - VC 1 In Call" }, new JoinMetadata { Description = "Conf - VC 1 In Call", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Digital }); + /// + /// VC Codec Online + /// [JoinName("VcCodecOnline")] public JoinDataComplete VcCodecOnline = new JoinDataComplete(new JoinData { JoinNumber = 122, JoinSpan = 1, AttributeName = "Online - VC 1" }, new JoinMetadata { Description = "Online - VC 1", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Digital }); + /// + /// VC Codec IP Address + /// [JoinName("VcCodecIpAddress")] public JoinDataComplete VcCodecIpAddress = new JoinDataComplete(new JoinData { JoinNumber = 121, JoinSpan = 1, AttributeName = "IP Address - VC" }, new JoinMetadata { Description = "IP Address - VC", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// VC Codec IP Port + /// [JoinName("VcCodecIpPort")] public JoinDataComplete VcCodecIpPort = new JoinDataComplete(new JoinData { JoinNumber = 150, JoinSpan = 1, AttributeName = "IP Port - VC" }, new JoinMetadata { Description = "IP Port - VC", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); // Source Attributes + /// + /// Display 1 Current Source Name + /// [JoinName("Display1CurrentSourceName")] public JoinDataComplete Display1CurrentSourceName = new JoinDataComplete(new JoinData { JoinNumber = 84, JoinSpan = 1, AttributeName = "Display 1 - Current Source" }, new JoinMetadata { Description = "Display 1 - Current Source", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); // Device Online Status + /// + /// Touchpanel Online Start + /// [JoinName("TouchpanelOnlineStart")] public JoinDataComplete TouchpanelOnlineStart = new JoinDataComplete(new JoinData { JoinNumber = 150, JoinSpan = 10, AttributeName = "Online - Touch Panel" }, new JoinMetadata { Description = "Online - Touch Panel", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Digital }); + /// + /// Xpanel Online Start + /// [JoinName("XpanelOnlineStart")] public JoinDataComplete XpanelOnlineStart = new JoinDataComplete(new JoinData { JoinNumber = 160, JoinSpan = 5, AttributeName = "Online - XPanel" }, new JoinMetadata { Description = "Online - XPanel", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Digital }); + /// + /// Display Online Start + /// [JoinName("DisplayOnlineStart")] public JoinDataComplete DisplayOnlineStart = new JoinDataComplete(new JoinData { JoinNumber = 170, JoinSpan = 10, AttributeName = "Online - Display" }, new JoinMetadata { Description = "Online - Display", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Digital }); + /// + /// Display 1 Laptop Source Start + /// [JoinName("Display1LaptopSourceStart")] - public JoinDataComplete Display1LaptopSourceStart = new JoinDataComplete(new JoinData { JoinNumber = 166, JoinSpan = 5, AttributeName = "Display 1 - Source Laptop" }, + public JoinDataComplete Display1LaptopSourceStart = new JoinDataComplete(new JoinData { JoinNumber = 165, JoinSpan = 5, AttributeName = "Display 1 - Source Laptop" }, new JoinMetadata { Description = "Display 1 - Source Laptop", JoinCapabilities = eJoinCapabilities.ToFromFusion, JoinType = eJoinType.Digital }); + /// + /// Display 1 Disc Player Source Start + /// [JoinName("Display1DiscPlayerSourceStart")] - public JoinDataComplete Display1DiscPlayerSourceStart = new JoinDataComplete(new JoinData { JoinNumber = 181, JoinSpan = 5, AttributeName = "Display 1 - Source Disc Player" }, + public JoinDataComplete Display1DiscPlayerSourceStart = new JoinDataComplete(new JoinData { JoinNumber = 180, JoinSpan = 5, AttributeName = "Display 1 - Source Disc Player" }, new JoinMetadata { Description = "Display 1 - Source Disc Player", JoinCapabilities = eJoinCapabilities.ToFromFusion, JoinType = eJoinType.Digital }); + /// + /// Display 1 Set Top Box Source Start + /// [JoinName("Display1SetTopBoxSourceStart")] - public JoinDataComplete Display1SetTopBoxSourceStart = new JoinDataComplete(new JoinData { JoinNumber = 188, JoinSpan = 5, AttributeName = "Display 1 - Source TV" }, + public JoinDataComplete Display1SetTopBoxSourceStart = new JoinDataComplete(new JoinData { JoinNumber = 185, JoinSpan = 5, AttributeName = "Display 1 - Source TV" }, new JoinMetadata { Description = "Display 1 - Source TV", JoinCapabilities = eJoinCapabilities.ToFromFusion, JoinType = eJoinType.Digital }); // Display 1 + /// + /// Display 1 Start + /// [JoinName("Display1Start")] - public JoinDataComplete Display1Start = new JoinDataComplete(new JoinData { JoinNumber = 158, JoinSpan = 1 }, + public JoinDataComplete Display1Start = new JoinDataComplete(new JoinData { JoinNumber = 190, JoinSpan = 1 }, new JoinMetadata { Description = "Display 1 Start", JoinCapabilities = eJoinCapabilities.ToFromFusion, JoinType = eJoinType.Digital }); - /// /// Constructor to use when instantiating this Join Map without inheriting from it /// diff --git a/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs similarity index 81% rename from src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs rename to src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs index e2638493..593c9cce 100644 --- a/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs +++ b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs @@ -1,6 +1,4 @@ - - -using Crestron.SimplSharp; +using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.CrestronXml; using Crestron.SimplSharp.CrestronXml.Serialization; @@ -8,6 +6,7 @@ using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.Fusion; using Newtonsoft.Json; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.DeviceTypeInterfaces; using Serilog.Events; @@ -21,26 +20,42 @@ namespace PepperDash.Essentials.Core.Fusion /// /// Represents a EssentialsHuddleSpaceFusionSystemControllerBase /// - public class EssentialsHuddleSpaceFusionSystemControllerBase : Device, IOccupancyStatusProvider + public class IEssentialsRoomFusionController : EssentialsDevice, IOccupancyStatusProvider, IFusionHelpRequest, IHasFeedback { - private readonly EssentialsHuddleSpaceRoomFusionRoomJoinMap JoinMap; + private IEssentialsRoomFusionControllerPropertiesConfig _config; + + private EssentialsHuddleSpaceRoomFusionRoomJoinMap JoinMap; private const string RemoteOccupancyXml = "Local{0}"; - private readonly bool _guidFileExists; + private bool _guidFileExists; private readonly Dictionary _sourceToFeedbackSigs = new Dictionary(); + /// + /// Gets or sets the CurrentRoomSourceNameSig + /// protected StringSigData CurrentRoomSourceNameSig; private readonly FusionCustomPropertiesBridge CustomPropertiesBridge = new FusionCustomPropertiesBridge(); + + /// + /// Gets or sets the FusionOccSensor + /// protected FusionOccupancySensorAsset FusionOccSensor; private readonly FusionRemoteOccupancySensor FusionRemoteOccSensor; + /// + /// Gets or sets the FusionRoom + /// protected FusionRoom FusionRoom; + + /// + /// Gets or sets the FusionStaticAssets + /// protected Dictionary FusionStaticAssets; private readonly long PushNotificationTimeout = 5000; - private readonly IEssentialsRoom Room; + private IEssentialsRoom Room; private readonly long SchedulePollInterval = 300000; private Event _currentMeeting; @@ -48,8 +63,7 @@ namespace PepperDash.Essentials.Core.Fusion private CTimer _dailyTimeRequestTimer; private StatusMonitorCollection _errorMessageRollUp; - private FusionRoomGuids _guiDs; - private uint _ipId; + private FusionRoomGuids _guids; private bool _isRegisteredForSchedulePushNotifications; private Event _nextMeeting; @@ -60,6 +74,20 @@ namespace PepperDash.Essentials.Core.Fusion private string _roomOccupancyRemoteString; + private bool _helpRequestSent; + + private eFusionHelpResponse _helpRequestStatus; + + /// + public StringFeedback HelpRequestResponseFeedback { get; private set; } + + /// + public BoolFeedback HelpRequestSentFeedback { get; private set; } + + /// + public StringFeedback HelpRequestStatusFeedback { get; private set; } + + #region System Info Sigs //StringSigData SystemName; @@ -93,71 +121,110 @@ namespace PepperDash.Essentials.Core.Fusion #endregion - public EssentialsHuddleSpaceFusionSystemControllerBase(IEssentialsRoom room, uint ipId, string joinMapKey) + /// + /// Constructor + /// + public IEssentialsRoomFusionController(string key, string name, IEssentialsRoomFusionControllerPropertiesConfig config) + : base(key, name) + { + _config = config; + + AddPostActivationAction(() => + { + var room = DeviceManager.GetDeviceForKey(_config.RoomKey); + + if (room == null) + { + this.LogError("Error Creating Fusion Room Controller. No room found with key '{0}'", _config.RoomKey); + return; + } + + this.LogInformation("Creating Fusion Room Controller for room '{0}' at IPID: {1:X2}", room.Key, _config.IpIdInt); + + ConstructorHelper(room, _config.IpIdInt, _config.JoinMapKey); + + }); + } + + /// + /// + /// + /// + /// + /// + public IEssentialsRoomFusionController(IEssentialsRoom room, string ipId, string joinMapKey) : base(room.Key + "-fusion") + { + _config = new IEssentialsRoomFusionControllerPropertiesConfig() + { + IpId = ipId, + RoomKey = room.Key, + JoinMapKey = joinMapKey + }; + + ConstructorHelper(room, _config.IpIdInt, joinMapKey); + } + + private void ConstructorHelper(IEssentialsRoom room, uint ipId, string joinMapKey) { try { + this.LogDebug("ConstructorHelper called for Fusion Room Controller for room '{0}' with IPID {1:X2}", room.Key, ipId); + + this.LogDebug("JoinMap Key: {0}", joinMapKey); + JoinMap = new EssentialsHuddleSpaceRoomFusionRoomJoinMap(1); - CrestronConsole.AddNewConsoleCommand((o) => JoinMap.PrintJoinMapInfo(), string.Format("ptjnmp-{0}", Key), "Prints Attribute Join Map", ConsoleAccessLevelEnum.AccessOperator); + this.LogDebug("JoinMap created"); + + CrestronConsole.AddNewConsoleCommand((o) => + { + if (o is string deviceKey) + { + if (string.IsNullOrEmpty(deviceKey) || deviceKey == "?") + { + CrestronConsole.ConsoleCommandResponse("Please provide a device key for a Fusion Room instance"); + return; + } + else if (deviceKey != this.Key) + { + return; + } + } + else + { + CrestronConsole.ConsoleCommandResponse("Invalid parameter. Please provide a device key for a Fusion Room instance"); + return; + } + + JoinMap.PrintJoinMapInfo(); + }, "printfusionjoinmap", "Prints Attribute Join Map", ConsoleAccessLevelEnum.AccessOperator); if (!string.IsNullOrEmpty(joinMapKey)) { + // this.LogDebug("Attempting to get custom join map for key: {0}", joinMapKey); var customJoins = JoinMapHelper.TryGetJoinMapAdvancedForDevice(joinMapKey); if (customJoins != null) { JoinMap.SetCustomJoinData(customJoins); } } - + Room = room; - _ipId = ipId; + this.LogDebug("Room found: {0}", Room.Key); FusionStaticAssets = new Dictionary(); - _guiDs = new FusionRoomGuids(); + this.LogDebug("FusionStaticAssets dictionary created"); - var mac = - CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_MAC_ADDRESS, 0); - - var slot = Global.ControlSystem.ProgramNumber; - - var guidFilePath = Global.FilePathPrefix + - string.Format(@"{0}-FusionGuids-{1:X2}.json", InitialParametersClass.ProgramIDTag, _ipId); - - var oldGuidFilePath = Global.FilePathPrefix + - string.Format(@"{0}-FusionGuids.json", InitialParametersClass.ProgramIDTag); - - if (File.Exists(oldGuidFilePath)) - { - Debug.LogMessage(LogEventLevel.Information, this, "Migrating from old Fusion GUID file to new Fusion GUID File"); - - File.Copy(oldGuidFilePath, guidFilePath); - - File.Delete(oldGuidFilePath); - } - - _guidFileExists = File.Exists(guidFilePath); - - // Check if file exists - if (!_guidFileExists) - { - // Does not exist. Create GUIDs - _guiDs = new FusionRoomGuids(Room.Name, ipId, _guiDs.GenerateNewRoomGuid(slot, mac), - FusionStaticAssets); - } - else - { - // Exists. Read GUIDs - ReadGuidFile(guidFilePath); - } + _guids = new FusionRoomGuids(); + this.LogDebug("FusionRoomGuids created"); if (Room is IRoomOccupancy occupancyRoom) { + Debug.LogDebug(this, "Room '{0}' supports IRoomOccupancy", Room.Key); if (occupancyRoom.RoomOccupancy != null) { if (occupancyRoom.OccupancyStatusProviderIsRemote) @@ -171,8 +238,21 @@ namespace PepperDash.Essentials.Core.Fusion } } + this.LogDebug("Occupancy setup complete"); + + HelpRequestResponseFeedback = new StringFeedback("HelpRequestResponse", () => FusionRoom.Help.OutputSig.StringValue); + + HelpRequestSentFeedback = new BoolFeedback("HelpRequestSent", () => _helpRequestSent); + HelpRequestStatusFeedback = new StringFeedback("HelpRequestStatus", () => _helpRequestStatus.ToString()); + + Feedbacks.Add(HelpRequestResponseFeedback); + Feedbacks.Add(HelpRequestSentFeedback); + Feedbacks.Add(HelpRequestStatusFeedback); + if (RoomOccupancyRemoteStringFeedback != null) + Feedbacks.Add(RoomOccupancyRemoteStringFeedback); + if (RoomIsOccupiedFeedback != null) + Feedbacks.Add(RoomIsOccupiedFeedback); - AddPostActivationAction(() => PostActivate(guidFilePath)); } catch (Exception e) { @@ -180,9 +260,54 @@ namespace PepperDash.Essentials.Core.Fusion } } - private void PostActivate(string guidFilePath) + private string GetGuidFilePath(uint ipId) { - CreateSymbolAndBasicSigs(_ipId); + var mac = + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_MAC_ADDRESS, 0); + + var slot = Global.ControlSystem.ProgramNumber; + + var guidFilePath = Global.FilePathPrefix + + string.Format(@"{0}-FusionGuids-{1:X2}.json", InitialParametersClass.ProgramIDTag, _config.IpIdInt); + + var oldGuidFilePath = Global.FilePathPrefix + + string.Format(@"{0}-FusionGuids.json", InitialParametersClass.ProgramIDTag); + + if (File.Exists(oldGuidFilePath)) + { + Debug.LogMessage(LogEventLevel.Information, this, "Migrating from old Fusion GUID file to new Fusion GUID File"); + + File.Copy(oldGuidFilePath, guidFilePath); + + File.Delete(oldGuidFilePath); + } + + _guidFileExists = File.Exists(guidFilePath); + + // Check if file exists + if (!_guidFileExists) + { + // Does not exist. Create GUIDs + _guids = new FusionRoomGuids(Room.Name, ipId, _guids.GenerateNewRoomGuid(slot, mac), + FusionStaticAssets); + } + else + { + // Exists. Read GUIDs + ReadGuidFile(guidFilePath); + } + + return guidFilePath; + } + + /// + public override void Initialize() + { + + GenerateGuidFile(GetGuidFilePath(_config.IpIdInt)); + + CreateSymbolAndBasicSigs(_config.IpIdInt); SetUpSources(); SetUpCommunitcationMonitors(); SetUpDisplay(); @@ -191,12 +316,14 @@ namespace PepperDash.Essentials.Core.Fusion FusionRVI.GenerateFileForAllFusionDevices(); - GenerateGuidFile(guidFilePath); } + /// + /// Gets the RoomGuid + /// protected string RoomGuid { - get { return _guiDs.RoomGuid; } + get { return _guids.RoomGuid; } } /// @@ -204,6 +331,9 @@ namespace PepperDash.Essentials.Core.Fusion /// public StringFeedback RoomOccupancyRemoteStringFeedback { get; private set; } + /// + /// Gets the RoomIsOccupiedFeedbackFunc + /// protected Func RoomIsOccupiedFeedbackFunc { get { return () => FusionRemoteOccSensor.RoomOccupied.OutputSig.BoolValue; } @@ -218,10 +348,21 @@ namespace PepperDash.Essentials.Core.Fusion #endregion + + /// + public FeedbackCollection Feedbacks { get; private set; } = new FeedbackCollection(); + + + /// + /// ScheduleChange event + /// public event EventHandler ScheduleChange; //public event EventHandler MeetingEndWarning; //public event EventHandler NextMeetingBeginWarning; + /// + /// RoomInfoChange event + /// public event EventHandler RoomInfoChange; //ScheduleResponseEvent NextMeeting; @@ -258,11 +399,11 @@ namespace PepperDash.Essentials.Core.Fusion Debug.LogMessage(LogEventLevel.Debug, this, "Writing GUIDs to file"); - _guiDs = FusionOccSensor == null - ? new FusionRoomGuids(Room.Name, _ipId, RoomGuid, FusionStaticAssets) - : new FusionRoomGuids(Room.Name, _ipId, RoomGuid, FusionStaticAssets, FusionOccSensor); + _guids = FusionOccSensor == null + ? new FusionRoomGuids(Room.Name, _config.IpIdInt, RoomGuid, FusionStaticAssets) + : new FusionRoomGuids(Room.Name, _config.IpIdInt, RoomGuid, FusionStaticAssets, FusionOccSensor); - var json = JsonConvert.SerializeObject(_guiDs, Newtonsoft.Json.Formatting.Indented); + var json = JsonConvert.SerializeObject(_guids, Newtonsoft.Json.Formatting.Indented); using (var sw = new StreamWriter(filePath)) { @@ -312,17 +453,17 @@ namespace PepperDash.Essentials.Core.Fusion { var json = File.ReadToEnd(filePath, Encoding.ASCII); - _guiDs = JsonConvert.DeserializeObject(json); + _guids = JsonConvert.DeserializeObject(json); - _ipId = _guiDs.IpId; + // _config.IpId = _guids.IpId; - FusionStaticAssets = _guiDs.StaticAssets; + FusionStaticAssets = _guids.StaticAssets; } Debug.LogMessage(LogEventLevel.Information, this, "Fusion Guids successfully read from file: {0}", filePath); - Debug.LogMessage(LogEventLevel.Debug, this, "\r\n********************\r\n\tRoom Name: {0}\r\n\tIPID: {1:X}\r\n\tRoomGuid: {2}\r\n*******************", Room.Name, _ipId, RoomGuid); + Debug.LogMessage(LogEventLevel.Debug, this, "\r\n********************\r\n\tRoom Name: {0}\r\n\tIPID: {1:X}\r\n\tRoomGuid: {2}\r\n*******************", Room.Name, _config.IpIdInt, RoomGuid); foreach (var item in FusionStaticAssets) { @@ -343,6 +484,10 @@ namespace PepperDash.Essentials.Core.Fusion } } + /// + /// CreateSymbolAndBasicSigs method + /// + /// protected virtual void CreateSymbolAndBasicSigs(uint ipId) { Debug.LogMessage(LogEventLevel.Information, this, "Creating Fusion Room symbol with GUID: {0} and IP-ID {1:X2}", RoomGuid, ipId); @@ -405,6 +550,10 @@ namespace PepperDash.Essentials.Core.Fusion CrestronEnvironment.EthernetEventHandler += CrestronEnvironment_EthernetEventHandler; } + /// + /// CrestronEnvironment_EthernetEventHandler method + /// + /// protected void CrestronEnvironment_EthernetEventHandler(EthernetEventArgs ethernetEventArgs) { if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp) @@ -413,6 +562,9 @@ namespace PepperDash.Essentials.Core.Fusion } } + /// + /// GetSystemInfo method + /// protected void GetSystemInfo() { //SystemName.InputSig.StringValue = Room.Name; @@ -426,6 +578,9 @@ namespace PepperDash.Essentials.Core.Fusion () => CrestronConsole.SendControlSystemCommand("reboot", ref response)); } + /// + /// SetUpEthernetValues method + /// protected void SetUpEthernetValues() { _ip1 = FusionRoom.CreateOffsetStringSig(JoinMap.ProcessorIp1.JoinNumber, JoinMap.ProcessorIp1.AttributeName, eSigIoMask.InputSigOnly); @@ -441,6 +596,9 @@ namespace PepperDash.Essentials.Core.Fusion _netMask2 = FusionRoom.CreateOffsetStringSig(JoinMap.ProcessorNetMask2.JoinNumber, JoinMap.ProcessorNetMask2.AttributeName, eSigIoMask.InputSigOnly); } + /// + /// GetProcessorEthernetValues method + /// protected void GetProcessorEthernetValues() { _ip1.InputSig.StringValue = @@ -475,7 +633,7 @@ namespace PepperDash.Essentials.Core.Fusion // Interface 1 if (InitialParametersClass.NumberOfEthernetInterfaces > 1) - // Only get these values if the processor has more than 1 NIC + // Only get these values if the processor has more than 1 NIC { _ip2.InputSig.StringValue = CrestronEthernetHelper.GetEthernetParameter( @@ -489,6 +647,9 @@ namespace PepperDash.Essentials.Core.Fusion } } + /// + /// GetProcessorInfo method + /// protected void GetProcessorInfo() { _firmware = FusionRoom.CreateOffsetStringSig(JoinMap.ProcessorFirmware.JoinNumber, JoinMap.ProcessorFirmware.AttributeName, eSigIoMask.InputSigOnly); @@ -499,7 +660,7 @@ namespace PepperDash.Essentials.Core.Fusion { var join = JoinMap.ProgramNameStart.JoinNumber + i; var progNum = i + 1; - _program[i] = FusionRoom.CreateOffsetStringSig((uint) join, + _program[i] = FusionRoom.CreateOffsetStringSig((uint)join, string.Format("{0} {1}", JoinMap.ProgramNameStart.AttributeName, progNum), eSigIoMask.InputSigOnly); } } @@ -507,6 +668,9 @@ namespace PepperDash.Essentials.Core.Fusion _firmware.InputSig.StringValue = InitialParametersClass.FirmwareVersion; } + /// + /// GetCustomProperties method + /// protected void GetCustomProperties() { if (FusionRoom.IsOnline) @@ -524,11 +688,16 @@ namespace PepperDash.Essentials.Core.Fusion // TODO: Get IP and Project Name from TP } + /// + /// FusionRoom_OnlineStatusChange method + /// + /// + /// protected void FusionRoom_OnlineStatusChange(GenericBase currentDevice, OnlineOfflineEventArgs args) { if (args.DeviceOnLine) { - CrestronInvoke.BeginInvoke( (o) => + CrestronInvoke.BeginInvoke((o) => { CrestronEnvironment.Sleep(200); @@ -676,7 +845,7 @@ namespace PepperDash.Essentials.Core.Fusion var extendTime = _currentMeeting.dtEnd - DateTime.Now; var extendMinutesRaw = extendTime.TotalMinutes; - extendMinutes += (int) Math.Round(extendMinutesRaw); + extendMinutes += (int)Math.Round(extendMinutesRaw); } @@ -784,11 +953,11 @@ namespace PepperDash.Essentials.Core.Fusion var parameters = actionResponse["Parameters"]; foreach (var isRegistered in from XmlElement parameter in parameters - where parameter.HasAttributes - select parameter.Attributes + where parameter.HasAttributes + select parameter.Attributes into attributes - where attributes["ID"].Value == "Registered" - select Int32.Parse(attributes["Value"].Value)) + where attributes["ID"].Value == "Registered" + select Int32.Parse(attributes["Value"].Value)) { switch (isRegistered) { @@ -845,9 +1014,9 @@ namespace PepperDash.Essentials.Core.Fusion Debug.LogMessage(LogEventLevel.Debug, this, "DateTime from Fusion Server: {0}", currentTime); // Parse time and date from response and insert values - CrestronEnvironment.SetTimeAndDate((ushort) currentTime.Hour, (ushort) currentTime.Minute, - (ushort) currentTime.Second, (ushort) currentTime.Month, (ushort) currentTime.Day, - (ushort) currentTime.Year); + CrestronEnvironment.SetTimeAndDate((ushort)currentTime.Hour, (ushort)currentTime.Minute, + (ushort)currentTime.Second, (ushort)currentTime.Month, (ushort)currentTime.Day, + (ushort)currentTime.Year); Debug.LogMessage(LogEventLevel.Debug, this, "Processor time set to {0}", CrestronEnvironment.GetLocalTime()); } @@ -1065,6 +1234,9 @@ namespace PepperDash.Essentials.Core.Fusion } } + /// + /// SetUpSources method + /// protected virtual void SetUpSources() { // Sources @@ -1074,10 +1246,10 @@ namespace PepperDash.Essentials.Core.Fusion // NEW PROCESS: // Make these lists and insert the fusion attributes by iterating these var setTopBoxes = dict.Where(d => d.Value.SourceDevice is ISetTopBoxControls); - uint i = 1; + uint i = 0; foreach (var kvp in setTopBoxes) { - TryAddRouteActionSigs(JoinMap.Display1SetTopBoxSourceStart.AttributeName + " " + i, JoinMap.Display1SetTopBoxSourceStart.JoinNumber + i, kvp.Key, kvp.Value.SourceDevice); + TryAddRouteActionSigs(JoinMap.Display1SetTopBoxSourceStart.AttributeName + " " + (i + 1), JoinMap.Display1SetTopBoxSourceStart.JoinNumber + i, kvp.Key, kvp.Value.SourceDevice); i++; if (i > JoinMap.Display1SetTopBoxSourceStart.JoinSpan) // We only have five spots { @@ -1086,10 +1258,10 @@ namespace PepperDash.Essentials.Core.Fusion } var discPlayers = dict.Where(d => d.Value.SourceDevice is IDiscPlayerControls); - i = 1; + i = 0; foreach (var kvp in discPlayers) { - TryAddRouteActionSigs(JoinMap.Display1DiscPlayerSourceStart.AttributeName + " " + i, JoinMap.Display1DiscPlayerSourceStart.JoinNumber + i, kvp.Key, kvp.Value.SourceDevice); + TryAddRouteActionSigs(JoinMap.Display1DiscPlayerSourceStart.AttributeName + " " + (i + 1), JoinMap.Display1DiscPlayerSourceStart.JoinNumber + i, kvp.Key, kvp.Value.SourceDevice); i++; if (i > JoinMap.Display1DiscPlayerSourceStart.JoinSpan) // We only have five spots { @@ -1098,10 +1270,10 @@ namespace PepperDash.Essentials.Core.Fusion } var laptops = dict.Where(d => d.Value.SourceDevice is IRoutingSource); - i = 1; + i = 0; foreach (var kvp in laptops) { - TryAddRouteActionSigs(JoinMap.Display1LaptopSourceStart.AttributeName + " " + i, JoinMap.Display1LaptopSourceStart.JoinNumber + i, kvp.Key, kvp.Value.SourceDevice); + TryAddRouteActionSigs(JoinMap.Display1LaptopSourceStart.AttributeName + " " + (i + 1), JoinMap.Display1LaptopSourceStart.JoinNumber + i, kvp.Key, kvp.Value.SourceDevice); i++; if (i > JoinMap.Display1LaptopSourceStart.JoinSpan) // We only have ten spots??? { @@ -1111,7 +1283,7 @@ namespace PepperDash.Essentials.Core.Fusion foreach (var usageDevice in dict.Select(kvp => kvp.Value.SourceDevice).OfType()) { - usageDevice.UsageTracker = new UsageTracking(usageDevice as Device) {UsageIsTracked = true}; + usageDevice.UsageTracker = new UsageTracking(usageDevice as Device) { UsageIsTracked = true }; usageDevice.UsageTracker.DeviceUsageEnded += UsageTracker_DeviceUsageEnded; } } @@ -1157,17 +1329,31 @@ namespace PepperDash.Essentials.Core.Fusion Debug.LogMessage(LogEventLevel.Debug, this, "Device usage string: {0}", deviceUsage); } - + /// + /// Tries to add route action sigs for a source + /// + /// + /// + /// + /// protected void TryAddRouteActionSigs(string attrName, uint attrNum, string routeKey, Device pSrc) { - Debug.LogMessage(LogEventLevel.Verbose, this, "Creating attribute '{0}' with join {1} for source {2}", + this.LogVerbose("Creating attribute '{0}' with join {1} for source {2}", attrName, attrNum, pSrc.Key); try { var sigD = FusionRoom.CreateOffsetBoolSig(attrNum, attrName, eSigIoMask.InputOutputSig); // Need feedback when this source is selected // Event handler, added below, will compare source changes with this sig dict - _sourceToFeedbackSigs.Add(pSrc, sigD.InputSig); + if (!_sourceToFeedbackSigs.ContainsKey(pSrc)) + { + _sourceToFeedbackSigs.Add(pSrc, sigD.InputSig); + } + else + { + this.LogWarning("Source '{0}' already has a feedback sig mapped. Overwriting.", pSrc.Key); + _sourceToFeedbackSigs[pSrc] = sigD.InputSig; + } // And respond to selection in Fusion sigD.OutputSig.SetSigFalseAction(() => @@ -1180,14 +1366,12 @@ namespace PepperDash.Essentials.Core.Fusion } catch (Exception) { - Debug.LogMessage(LogEventLevel.Verbose, this, "Error creating Fusion signal {0} {1} for device '{2}'. THIS NEEDS REWORKING", + this.LogVerbose("Error creating Fusion signal {0} {1} for device '{2}'. THIS NEEDS REWORKING", attrNum, attrName, pSrc.Key); } } - /// - /// - /// + private void SetUpCommunitcationMonitors() { uint displayNum = 0; @@ -1274,6 +1458,8 @@ namespace PepperDash.Essentials.Core.Fusion if (attrName != null) { + this.LogDebug("Linking communication monitor for device '{0}' to Fusion attribute '{1}' at join {2}", + dev.Key, attrName, attrNum); // Link comm status to sig and update var sigD = FusionRoom.CreateOffsetBoolSig(attrNum, attrName, eSigIoMask.InputSigOnly); var smd = dev as ICommunicationMonitor; @@ -1285,6 +1471,9 @@ namespace PepperDash.Essentials.Core.Fusion } } + /// + /// SetUpDisplay method + /// protected virtual void SetUpDisplay() { try @@ -1297,7 +1486,7 @@ namespace PepperDash.Essentials.Core.Fusion foreach (var display in displays.Cast()) { - display.UsageTracker = new UsageTracking(display as Device) {UsageIsTracked = true}; + display.UsageTracker = new UsageTracking(display as Device) { UsageIsTracked = true }; display.UsageTracker.DeviceUsageEnded += UsageTracker_DeviceUsageEnded; } @@ -1410,7 +1599,7 @@ namespace PepperDash.Essentials.Core.Fusion // Power on - var defaultDisplayPowerOn = FusionRoom.CreateOffsetBoolSig((uint) joinOffset, displayName + "Power On", + var defaultDisplayPowerOn = FusionRoom.CreateOffsetBoolSig((uint)joinOffset, displayName + "Power On", eSigIoMask.InputOutputSig); defaultDisplayPowerOn.OutputSig.UserObject = new Action(b => { @@ -1421,7 +1610,7 @@ namespace PepperDash.Essentials.Core.Fusion }); // Power Off - var defaultDisplayPowerOff = FusionRoom.CreateOffsetBoolSig((uint) joinOffset + 1, displayName + "Power Off", + var defaultDisplayPowerOff = FusionRoom.CreateOffsetBoolSig((uint)joinOffset + 1, displayName + "Power Off", eSigIoMask.InputOutputSig); defaultDisplayPowerOn.OutputSig.UserObject = new Action(b => { @@ -1439,7 +1628,7 @@ namespace PepperDash.Essentials.Core.Fusion } // Current Source - var defaultDisplaySourceNone = FusionRoom.CreateOffsetBoolSig((uint) joinOffset + 8, + var defaultDisplaySourceNone = FusionRoom.CreateOffsetBoolSig((uint)joinOffset + 8, displayName + "Source None", eSigIoMask.InputOutputSig); defaultDisplaySourceNone.OutputSig.UserObject = new Action(b => { @@ -1507,7 +1696,7 @@ namespace PepperDash.Essentials.Core.Fusion //if (Room.OccupancyObj != null) //{ - var tempOccAsset = _guiDs.OccupancyAsset; + var tempOccAsset = _guids.OccupancyAsset; if (tempOccAsset == null) { @@ -1532,7 +1721,7 @@ namespace PepperDash.Essentials.Core.Fusion occRoom.RoomOccupancy.RoomIsOccupiedFeedback.OutputChange += RoomIsOccupiedFeedback_OutputChange; } RoomOccupancyRemoteStringFeedback = new StringFeedback(() => _roomOccupancyRemoteString); - + RoomOccupancyRemoteStringFeedback.LinkInputSig(occSensorAsset.RoomOccupancyInfo.InputSig); //} @@ -1588,12 +1777,74 @@ namespace PepperDash.Essentials.Core.Fusion } } + /// + /// Event handler for Fusion state changes + /// + /// + /// protected void FusionRoom_FusionStateChange(FusionBase device, FusionStateEventArgs args) { + if (args.EventId == FusionEventIds.HelpMessageReceivedEventId) + { + this.LogInformation("Help message received from Fusion for room '{0}'", + Room.Name); + + this.LogDebug("Help message content: {0}", FusionRoom.Help.OutputSig.StringValue); + // Fire help request event + HelpRequestResponseFeedback.FireUpdate(); + + if (!string.IsNullOrEmpty(FusionRoom.Help.OutputSig.StringValue)) + { + switch (FusionRoom.Help.OutputSig.StringValue) + { + case "Please wait, a technician is on his / her way.": + // this.LogInformation("Please wait, a technician is on his / her way.", + // Room.Name); + + _helpRequestStatus = eFusionHelpResponse.HelpOnTheWay; + break; + case "Please call the helpdesk.": + // this.LogInformation("Please call the helpdesk."); + // _helpRequestStatus = eFusionHelpResponse.CallHelpDesk; + break; + case "Please wait, I will reschedule your meeting to a different room.": + // this.LogInformation("Please wait, I will reschedule your meeting to a different room.", + // Room.Name); + + _helpRequestStatus = eFusionHelpResponse.ReschedulingMeeting; + break; + case "I will be taking control of your system. Please be patient while I adjust the settings.": + // this.LogInformation("I will be taking control of your system. Please be patient while I adjust the settings.", + // Room.Name); + + _helpRequestStatus = eFusionHelpResponse.TakingControl; + break; + default: + // this.LogInformation("Unknown help request code received from Fusion for room '{0}'", + // Room.Name); + + _helpRequestStatus = eFusionHelpResponse.None; + break; + } + } + else + { + _helpRequestStatus = eFusionHelpResponse.None; + } + + if (_helpRequestStatus == eFusionHelpResponse.None) + { + _helpRequestSent = false; + HelpRequestSentFeedback.FireUpdate(); + } + + HelpRequestStatusFeedback.FireUpdate(); + } + + // The sig/UO method: Need separate handlers for fixed and user sigs, all flavors, // even though they all contain sigs. - BoolOutputSig outSig; if (args.UserConfiguredSigDetail is BooleanSigDataFixedName sigData) { @@ -1632,9 +1883,69 @@ namespace PepperDash.Essentials.Core.Fusion (outSig.UserObject as Action).Invoke(outSig.StringValue); } } + + /// + public void SendHelpRequest() + { + + var now = DateTime.Now; + + var breakString = _config.UseHtmlFormatForHelpRequests ? "
" : "\r\n"; + + var date = now.ToString("MMMM dd, yyyy"); + var time = now.ToString("hh:mm tt"); + if (_config.Use24HourTimeFormat) + { + time = now.ToString("HH:mm"); + } + + var requestString = $"HR00: {breakString} Assistance has been requested from room {Room.Name}{breakString}on {date} at {time}"; + + FusionRoom.Help.InputSig.StringValue = requestString; + + this.LogInformation("Help request sent to Fusion from room '{0}'", Room.Name); + this.LogDebug("Help request content: {0}", FusionRoom.Help.InputSig.StringValue); + + _helpRequestSent = true; + HelpRequestSentFeedback.FireUpdate(); + + _helpRequestStatus = eFusionHelpResponse.HelpRequested; + HelpRequestStatusFeedback.FireUpdate(); + } + + /// + public void CancelHelpRequest() + { + if (_helpRequestSent) + { + FusionRoom.Help.InputSig.StringValue = ""; + _helpRequestSent = false; + HelpRequestSentFeedback.FireUpdate(); + _helpRequestStatus = eFusionHelpResponse.None; + HelpRequestStatusFeedback.FireUpdate(); + Debug.LogMessage(LogEventLevel.Information, this, "Help request cancelled in Fusion for room '{0}'", Room.Name); + } + } + + /// + public void ToggleHelpRequest() + { + if (_helpRequestSent) + { + CancelHelpRequest(); + } + else + { + SendHelpRequest(); + } + } + } + /// + /// Extensions to enhance Fusion room, asset and signal creation. + /// public static class FusionRoomExtensions { /// @@ -1648,6 +1959,8 @@ namespace PepperDash.Essentials.Core.Fusion /// public static BooleanSigData CreateOffsetBoolSig(this FusionRoom fr, uint number, string name, eSigIoMask mask) { + Debug.LogDebug("Creating Offset Bool Sig: {0} at Join {1}", name, number); + if (number < 50) { throw new ArgumentOutOfRangeException("number", "Cannot be less than 50"); @@ -1668,6 +1981,8 @@ namespace PepperDash.Essentials.Core.Fusion ///
public static UShortSigData CreateOffsetUshortSig(this FusionRoom fr, uint number, string name, eSigIoMask mask) { + Debug.LogDebug("Creating Offset UShort Sig: {0} at Join {1}", name, number); + if (number < 50) { throw new ArgumentOutOfRangeException("number", "Cannot be less than 50"); @@ -1688,6 +2003,8 @@ namespace PepperDash.Essentials.Core.Fusion ///
public static StringSigData CreateOffsetStringSig(this FusionRoom fr, uint number, string name, eSigIoMask mask) { + Debug.LogDebug("Creating Offset String Sig: {0} at Join {1}", name, number); + if (number < 50) { throw new ArgumentOutOfRangeException("number", "Cannot be less than 50"); @@ -1803,6 +2120,9 @@ namespace PepperDash.Essentials.Core.Fusion /// public class RoomInformation { + /// + /// Constructor + /// public RoomInformation() { FusionCustomProperties = new List(); @@ -1855,10 +2175,17 @@ namespace PepperDash.Essentials.Core.Fusion /// public class FusionCustomProperty { + /// + /// Constructor + /// public FusionCustomProperty() { } + /// + /// Constructor with id + /// + /// public FusionCustomProperty(string id) { ID = id; diff --git a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerFactory.cs b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerFactory.cs new file mode 100644 index 00000000..e874762c --- /dev/null +++ b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerFactory.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Fusion; + +/// +/// Factory for creating IEssentialsRoomFusionController devices +/// +public class IEssentialsRoomFusionControllerFactory : EssentialsDeviceFactory +{ + /// + /// Constructor + /// + public IEssentialsRoomFusionControllerFactory() + { + TypeNames = new List() { "fusionRoom" }; + } + + /// + /// Builds the device + /// + /// + /// + public override EssentialsDevice BuildDevice(PepperDash.Essentials.Core.Config.DeviceConfig dc) + { + Debug.LogDebug("Factory Attempting to create new IEssentialsRoomFusionController Device"); + + + var properties = dc.Properties.ToObject(); + + return new IEssentialsRoomFusionController(dc.Key, dc.Name, properties); + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerPropertiesConfig.cs b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerPropertiesConfig.cs new file mode 100644 index 00000000..4dc4a834 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerPropertiesConfig.cs @@ -0,0 +1,59 @@ +using Newtonsoft.Json; +using PepperDash.Core; + +/// +/// Config properties for an IEssentialsRoomFusionController device +/// +public class IEssentialsRoomFusionControllerPropertiesConfig +{ + /// + /// Gets or sets the IP ID of the Fusion Room Controller + /// + [JsonProperty("ipId")] + public string IpId { get; set; } + + /// + /// Gets the IP ID as a UInt16 + /// + [JsonIgnore] + public uint IpIdInt + { + get + { + // Try to parse the IpId string to UInt16 as hex + if (ushort.TryParse(IpId, System.Globalization.NumberStyles.HexNumber, null, out ushort result)) + { + return result; + } + else + { + Debug.LogWarning( "Failed to parse IpId '{0}' as UInt16", IpId); + return 0; + } + } + } + + /// + /// Gets or sets the join map key + /// + [JsonProperty("joinMapKey")] + public string JoinMapKey { get; set; } + + /// + /// Gets or sets the room key associated with this Fusion Room Controller + /// + [JsonProperty("roomKey")] + public string RoomKey { get; set; } + + /// + /// Gets or sets whether to use HTML format for help requests + /// + [JsonProperty("useHtmlFormatForHelpRequests")] + public bool UseHtmlFormatForHelpRequests { get; set; } = false; + + /// + /// Gets or sets whether to use 24-hour time format + /// + [JsonProperty("use24HourTimeFormat")] + public bool Use24HourTimeFormat { get; set; } = false; +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Fusion/IFusionHelpRequest.cs b/src/PepperDash.Essentials.Core/Fusion/IFusionHelpRequest.cs new file mode 100644 index 00000000..de4cca17 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Fusion/IFusionHelpRequest.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PepperDash.Essentials.Core.Fusion +{ + /// + /// Represents Fusion Help Request functionality + /// + public interface IFusionHelpRequest + { + /// + /// Feedback containing the response to a help request + /// + StringFeedback HelpRequestResponseFeedback { get; } + + /// + /// Indicates whether a help request has been sent + /// + BoolFeedback HelpRequestSentFeedback { get; } + + /// + /// Feedback containing the current status of the help request + /// + StringFeedback HelpRequestStatusFeedback { get; } + + /// + /// Sends a help request + /// + void SendHelpRequest(); + + /// + /// Clears the current help request status + /// + void CancelHelpRequest(); + + /// + /// Toggles between sending and cancelling a help request + /// + void ToggleHelpRequest(); + } +} diff --git a/src/PepperDash.Essentials.Core/Fusion/eFusionHelpResponse.cs b/src/PepperDash.Essentials.Core/Fusion/eFusionHelpResponse.cs new file mode 100644 index 00000000..6a5bbcd8 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Fusion/eFusionHelpResponse.cs @@ -0,0 +1,37 @@ + + +namespace PepperDash.Essentials.Core.Fusion +{ + /// + /// Enumeration of possible Fusion Help Responses based on the standard responses from Fusion + /// + public enum eFusionHelpResponse + { + /// + /// No help response + /// + None, + /// + /// Help has been requested + /// + HelpRequested, + /// + /// Help is on the way + /// + HelpOnTheWay, + /// + /// Please call the helpdesk. + /// + CallHelpDesk, + /// + /// Rescheduling meeting. + /// + ReschedulingMeeting, + + /// + /// Technician taking control. + /// + TakingControl, + } + +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Room/Behaviours/RoomOnToDefaultSourceWhenOccupied.cs b/src/PepperDash.Essentials.Core/Room/Behaviours/RoomOnToDefaultSourceWhenOccupied.cs index 49b51f67..d6f3c503 100644 --- a/src/PepperDash.Essentials.Core/Room/Behaviours/RoomOnToDefaultSourceWhenOccupied.cs +++ b/src/PepperDash.Essentials.Core/Room/Behaviours/RoomOnToDefaultSourceWhenOccupied.cs @@ -49,7 +49,7 @@ namespace PepperDash.Essentials.Core /// public IRoomOccupancy Room { get; private set; } - private Fusion.EssentialsHuddleSpaceFusionSystemControllerBase FusionRoom; + private Fusion.IEssentialsRoomFusionController FusionRoom; public RoomOnToDefaultSourceWhenOccupied(DeviceConfig config) : base (config) @@ -74,7 +74,7 @@ namespace PepperDash.Essentials.Core var fusionRoomKey = PropertiesConfig.RoomKey + "-fusion"; - FusionRoom = DeviceManager.GetDeviceForKey(fusionRoomKey) as Core.Fusion.EssentialsHuddleSpaceFusionSystemControllerBase; + FusionRoom = DeviceManager.GetDeviceForKey(fusionRoomKey) as Core.Fusion.IEssentialsRoomFusionController; if (FusionRoom == null) Debug.LogMessage(LogEventLevel.Debug, this, "Unable to get Fusion Room from Device Manager with key: {0}", fusionRoomKey); diff --git a/src/PepperDash.Essentials.Core/Room/EssentialsRoomBase.cs b/src/PepperDash.Essentials.Core/Room/EssentialsRoomBase.cs index dfb8b5a1..38e4456e 100644 --- a/src/PepperDash.Essentials.Core/Room/EssentialsRoomBase.cs +++ b/src/PepperDash.Essentials.Core/Room/EssentialsRoomBase.cs @@ -408,7 +408,7 @@ namespace PepperDash.Essentials.Core Debug.LogMessage(LogEventLevel.Information, this, "Timeout Minutes from Config is: {0}", timeoutMinutes); // If status provider is fusion, set flag to remote - if (statusProvider is Core.Fusion.EssentialsHuddleSpaceFusionSystemControllerBase) + if (statusProvider is Core.Fusion.IEssentialsRoomFusionController) OccupancyStatusProviderIsRemote = true; if(timeoutMinutes > 0) diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs index 9afdb33d..c51cec58 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs @@ -98,7 +98,7 @@ namespace PepperDash.Essentials.Core.Routing /// The currently selected input port on the destination device. private void UpdateDestination(IRoutingSinkWithSwitching destination, RoutingInputPort inputPort) { - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Updating destination {destination} with inputPort {inputPort}", this,destination?.Key, inputPort?.Key); + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Updating destination {destination} with inputPort {inputPort}", this, destination?.Key, inputPort?.Key); if(inputPort == null) { diff --git a/src/PepperDash.Essentials.Core/UI/TouchpanelBase.cs b/src/PepperDash.Essentials.Core/UI/TouchpanelBase.cs index 3ffed9fc..fd9da802 100644 --- a/src/PepperDash.Essentials.Core/UI/TouchpanelBase.cs +++ b/src/PepperDash.Essentials.Core/UI/TouchpanelBase.cs @@ -1,20 +1,22 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using Crestron.SimplSharp; -using PepperDash.Essentials.Core; -using Crestron.SimplSharpPro.DeviceSupport; -using PepperDash.Core; -using Crestron.SimplSharpPro.UI; using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; +using PepperDash.Core; +using PepperDash.Core.Logging; using Serilog.Events; namespace PepperDash.Essentials.Core.UI { - public abstract class TouchpanelBase: EssentialsDevice, IHasBasicTriListWithSmartObject + /// + /// Base class for Touchpanel devices + /// + public abstract class TouchpanelBase : EssentialsDevice, IHasBasicTriListWithSmartObject { + /// + /// Gets or sets the configuration for the Crestron touchpanel. + /// protected CrestronTouchpanelPropertiesConfig _config; /// /// Gets or sets the Panel @@ -27,12 +29,11 @@ namespace PepperDash.Essentials.Core.UI /// is provided. /// /// Essentials Device Key - /// Essentials Device Name - /// Touchpanel Type to build - /// Touchpanel Configuration - /// IP-ID to use for touch panel + /// Essentials Device Name + /// Crestron Touchpanel Device + /// Touchpanel Configuration protected TouchpanelBase(string key, string name, BasicTriListWithSmartObject panel, CrestronTouchpanelPropertiesConfig config) - :base(key, name) + : base(key, name) { if (panel == null) @@ -55,23 +56,21 @@ namespace PepperDash.Essentials.Core.UI tsw.ButtonStateChange += Tsw_ButtonStateChange; } - _config = config; - - AddPreActivationAction(() => { - if (Panel.Register() != eDeviceRegistrationUnRegistrationResponse.Success) - Debug.LogMessage(LogEventLevel.Information, this, "WARNING: Registration failed. Continuing, but panel may not function: {0}", Panel.RegistrationFailureReason); + _config = config; + AddPreActivationAction(() => + { // Give up cleanly if SGD is not present. var sgdName = Global.FilePathPrefix + "sgd" + Global.DirectorySeparator + _config.SgdFile; if (!File.Exists(sgdName)) { - Debug.LogMessage(LogEventLevel.Information, this, "Smart object file '{0}' not present in User folder. Looking for embedded file", sgdName); + this.LogInformation("Smart object file '{0}' not present in User folder. Looking for embedded file", sgdName); sgdName = Global.ApplicationDirectoryPathPrefix + Global.DirectorySeparator + "SGD" + Global.DirectorySeparator + _config.SgdFile; if (!File.Exists(sgdName)) { - Debug.LogMessage(LogEventLevel.Information, this, "Unable to find SGD file '{0}' in User sgd or application SGD folder. Exiting touchpanel load.", sgdName); + this.LogWarning("Unable to find SGD file '{0}' in User sgd or application SGD folder. Exiting touchpanel load.", sgdName); return; } } @@ -82,12 +81,11 @@ namespace PepperDash.Essentials.Core.UI AddPostActivationAction(() => { // Check for IEssentialsRoomCombiner in DeviceManager and if found, subscribe to its event - var roomCombiner = DeviceManager.AllDevices.FirstOrDefault((d) => d is IEssentialsRoomCombiner) as IEssentialsRoomCombiner; - if (roomCombiner != null) + if (DeviceManager.AllDevices.FirstOrDefault((d) => d is IEssentialsRoomCombiner) is IEssentialsRoomCombiner roomCombiner) { // Subscribe to the even - roomCombiner.RoomCombinationScenarioChanged += new EventHandler(roomCombiner_RoomCombinationScenarioChanged); + roomCombiner.RoomCombinationScenarioChanged += new EventHandler(RoomCombiner_RoomCombinationScenarioChanged); // Connect to the initial roomKey if (roomCombiner.CurrentScenario != null) @@ -106,6 +104,11 @@ namespace PepperDash.Essentials.Core.UI // No room combiner, use the default key SetupPanelDrivers(_config.DefaultRoomKey); } + + var panelRegistrationResponse = Panel.Register(); + + if (panelRegistrationResponse != eDeviceRegistrationUnRegistrationResponse.Success) + this.LogInformation("WARNING: Registration failed. Continuing, but panel may not function: {0}", Panel.RegistrationFailureReason); }); } @@ -115,7 +118,6 @@ namespace PepperDash.Essentials.Core.UI /// Room Key for this panel protected abstract void SetupPanelDrivers(string roomKey); - /// /// Event handler for System Extender Events /// @@ -129,7 +131,7 @@ namespace PepperDash.Essentials.Core.UI /// /// /// - protected virtual void roomCombiner_RoomCombinationScenarioChanged(object sender, EventArgs e) + protected virtual void RoomCombiner_RoomCombinationScenarioChanged(object sender, EventArgs e) { var roomCombiner = sender as IEssentialsRoomCombiner; @@ -156,23 +158,23 @@ namespace PepperDash.Essentials.Core.UI SetupPanelDrivers(newRoomKey); } - private void Panel_SigChange(object currentDevice, Crestron.SimplSharpPro.SigEventArgs args) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Sig change: {0} {1}={2}", args.Sig.Type, args.Sig.Number, args.Sig.StringValue); - var uo = args.Sig.UserObject; - if (uo is Action) - (uo as Action)(args.Sig.BoolValue); - else if (uo is Action) - (uo as Action)(args.Sig.UShortValue); - else if (uo is Action) - (uo as Action)(args.Sig.StringValue); - } - - private void Tsw_ButtonStateChange(GenericBase device, ButtonEventArgs args) - { - var uo = args.Button.UserObject; - if(uo is Action) - (uo as Action)(args.Button.State == eButtonState.Pressed); - } + private void Panel_SigChange(object currentDevice, SigEventArgs args) + { + this.LogVerbose("Sig change: {0} {1}={2}", args.Sig.Type, args.Sig.Number, args.Sig.StringValue); + var uo = args.Sig.UserObject; + if (uo is Action) + (uo as Action)(args.Sig.BoolValue); + else if (uo is Action) + (uo as Action)(args.Sig.UShortValue); + else if (uo is Action) + (uo as Action)(args.Sig.StringValue); + } + + private void Tsw_ButtonStateChange(GenericBase device, ButtonEventArgs args) + { + var uo = args.Button.UserObject; + if (uo is Action) + (uo as Action)(args.Button.State == eButtonState.Pressed); + } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/SetDeviceStreamDebugRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/SetDeviceStreamDebugRequestHandler.cs index 72758b0e..7c6aea26 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/SetDeviceStreamDebugRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/SetDeviceStreamDebugRequestHandler.cs @@ -6,9 +6,9 @@ using PepperDash.Core.Web.RequestHandlers; namespace PepperDash.Essentials.Core.Web.RequestHandlers { - /// - /// Represents a SetDeviceStreamDebugRequestHandler - /// + /// + /// Represents a SetDeviceStreamDebugRequestHandler + /// public class SetDeviceStreamDebugRequestHandler : WebApiBaseRequestHandler { /// @@ -122,23 +122,23 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers return; } - if (!(DeviceManager.GetDeviceForKey(body.DeviceKey) is IStreamDebugging device)) - { - context.Response.StatusCode = 404; - context.Response.StatusDescription = "Not Found"; - context.Response.End(); + if (!(DeviceManager.GetDeviceForKey(body.DeviceKey) is IStreamDebugging device)) + { + context.Response.StatusCode = 404; + context.Response.StatusDescription = "Not Found"; + context.Response.End(); - return; - } + return; + } - eStreamDebuggingSetting debugSetting; + eStreamDebuggingSetting debugSetting; try { - debugSetting = (eStreamDebuggingSetting) Enum.Parse(typeof (eStreamDebuggingSetting), body.Setting, true); + debugSetting = (eStreamDebuggingSetting)Enum.Parse(typeof(eStreamDebuggingSetting), body.Setting, true); } catch (Exception ex) { - Debug.LogMessage(ex, "Exception handling set debug request"); + Debug.LogMessage(ex, "Exception handling set debug request"); context.Response.StatusCode = 500; context.Response.StatusDescription = "Internal Server Error"; context.Response.End(); @@ -164,7 +164,7 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers } catch (Exception ex) { - Debug.LogMessage(ex, "Exception handling set debug request"); + Debug.LogMessage(ex, "Exception handling set debug request"); context.Response.StatusCode = 500; context.Response.StatusDescription = "Internal Server Error"; context.Response.End(); @@ -198,21 +198,21 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers public class SetDeviceStreamDebugConfig { [JsonProperty("deviceKey", NullValueHandling = NullValueHandling.Include)] - /// - /// Gets or sets the DeviceKey - /// + /// + /// Gets or sets the DeviceKey + /// public string DeviceKey { get; set; } [JsonProperty("setting", NullValueHandling = NullValueHandling.Include)] - /// - /// Gets or sets the Setting - /// + /// + /// Gets or sets the Setting + /// public string Setting { get; set; } [JsonProperty("timeout")] - /// - /// Gets or sets the Timeout - /// + /// + /// Gets or sets the Timeout + /// public int Timeout { get; set; } public SetDeviceStreamDebugConfig() diff --git a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs index a44eb012..8f2fc1e2 100644 --- a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs +++ b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Config; using Serilog.Events; @@ -9,7 +10,7 @@ namespace PepperDash.Essentials.Devices.Common.Generic /// /// Represents a GenericSink /// - public class GenericSink : EssentialsDevice, IRoutingSinkWithInputPort + public class GenericSink : EssentialsDevice, IRoutingSinkWithSwitchingWithInputPort { /// /// Initializes a new instance of the GenericSink class @@ -20,7 +21,7 @@ namespace PepperDash.Essentials.Devices.Common.Generic { InputPorts = new RoutingPortCollection(); - var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, null, this); + var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo | eRoutingSignalType.SecondaryAudio, eRoutingPortConnectionType.Hdmi, null, this); InputPorts.Add(inputPort); } @@ -66,6 +67,15 @@ namespace PepperDash.Essentials.Devices.Common.Generic /// Event fired when the current source changes /// public event SourceInfoChangeHandler CurrentSourceChange; + + /// + public event InputChangedEventHandler InputChanged; + + /// + public void ExecuteSwitch(object inputSelector) + { + this.LogDebug("GenericSink Executing Switch to: {inputSelector}", inputSelector); + } } /// diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs index a7210a13..3031f4ba 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs @@ -31,7 +31,12 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// Unsoliciited feedback from a device in a messenger will ONLY be sent to devices in this subscription list. When a client disconnects, it's ID will be removed from the collection. /// - protected HashSet SubscriberIds = new HashSet(); + private readonly HashSet subscriberIds = new HashSet(); + + /// + /// Lock object for thread-safe access to SubscriberIds + /// + private readonly object _subscriberLock = new object(); private readonly List _deviceInterfaces; @@ -189,18 +194,18 @@ namespace PepperDash.Essentials.AppServer.Messengers { if (!enableMessengerSubscriptions) { - this.LogWarning("Messenger subscriptions not enabled"); return; } - if (SubscriberIds.Any(id => id == clientId)) + lock (_subscriberLock) { - this.LogVerbose("Client {clientId} already subscribed", clientId); - return; + if (!subscriberIds.Add(clientId)) + { + this.LogVerbose("Client {clientId} already subscribed", clientId); + return; + } } - SubscriberIds.Add(clientId); - this.LogDebug("Client {clientId} subscribed", clientId); } @@ -212,19 +217,26 @@ namespace PepperDash.Essentials.AppServer.Messengers { if (!enableMessengerSubscriptions) { - this.LogWarning("Messenger subscriptions not enabled"); return; } - if (!SubscriberIds.Any(i => i == clientId)) + bool wasSubscribed; + lock (_subscriberLock) + { + wasSubscribed = subscriberIds.Contains(clientId); + if (wasSubscribed) + { + subscriberIds.Remove(clientId); + } + } + + if (!wasSubscribed) { this.LogVerbose("Client with ID {clientId} is not subscribed", clientId); return; } - SubscriberIds.RemoveWhere((i) => i == clientId); - - this.LogInformation("Client with ID {clientId} unsubscribed", clientId); + this.LogDebug("Client with ID {clientId} unsubscribed", clientId); } /// @@ -258,7 +270,8 @@ namespace PepperDash.Essentials.AppServer.Messengers } catch (Exception ex) { - this.LogError(ex, "Exception posting status message for {messagePath} to {clientId}", MessagePath, clientId ?? "all clients"); + this.LogError("Exception posting status message for {messagePath} to {clientId}: {message}", MessagePath, clientId ?? "all clients", ex.Message); + this.LogDebug(ex, "Stack trace: "); } } @@ -287,7 +300,8 @@ namespace PepperDash.Essentials.AppServer.Messengers } catch (Exception ex) { - this.LogError(ex, "Exception posting status message for {type} to {clientId}", type, clientId ?? "all clients"); + this.LogError("Exception posting status message for {type} to {clientId}: {message}", type, clientId ?? "all clients", ex.Message); + this.LogDebug(ex, "Stack trace: "); } } @@ -312,7 +326,14 @@ namespace PepperDash.Essentials.AppServer.Messengers // If client is null or empty, this message is unsolicited feedback. Iterate through the subscriber list and send to all interested parties if (string.IsNullOrEmpty(clientId)) { - foreach (var client in SubscriberIds) + // Create a snapshot of subscribers to avoid collection modification during iteration + List subscriberSnapshot; + lock (_subscriberLock) + { + subscriberSnapshot = new List(subscriberIds); + } + + foreach (var client in subscriberSnapshot) { AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = client, Content = content }); } diff --git a/src/PepperDash.Essentials.MobileControl/ClientSpecificUpdateRequest.cs b/src/PepperDash.Essentials.MobileControl/ClientSpecificUpdateRequest.cs index beddd2f6..b974a9aa 100644 --- a/src/PepperDash.Essentials.MobileControl/ClientSpecificUpdateRequest.cs +++ b/src/PepperDash.Essentials.MobileControl/ClientSpecificUpdateRequest.cs @@ -3,10 +3,15 @@ using System; namespace PepperDash.Essentials { /// - /// Represents a ClientSpecificUpdateRequest + /// Send an update request for a specific client /// + [Obsolete] public class ClientSpecificUpdateRequest { + /// + /// Initialize an instance of the class. + /// + /// public ClientSpecificUpdateRequest(Action action) { ResponseMethod = action; diff --git a/src/PepperDash.Essentials.MobileControl/IDelayedConfiguration.cs b/src/PepperDash.Essentials.MobileControl/IDelayedConfiguration.cs index 2d191c89..abeef999 100644 --- a/src/PepperDash.Essentials.MobileControl/IDelayedConfiguration.cs +++ b/src/PepperDash.Essentials.MobileControl/IDelayedConfiguration.cs @@ -7,8 +7,9 @@ namespace PepperDash.Essentials /// public interface IDelayedConfiguration { - - + /// + /// Event triggered when the configuration is ready. Used when Mobile Control is interacting with a SIMPL program. + /// event EventHandler ConfigurationIsReady; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/MessageToClients.cs b/src/PepperDash.Essentials.MobileControl/MessageToClients.cs new file mode 100644 index 00000000..e1aa5eb5 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/MessageToClients.cs @@ -0,0 +1,90 @@ +using System; +using System.Threading; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core.Queues; +using PepperDash.Essentials.WebSocketServer; +using Serilog.Events; + +namespace PepperDash.Essentials +{ + /// + /// Represents a MessageToClients + /// + public class MessageToClients : IQueueMessage + { + private readonly MobileControlWebsocketServer _server; + private readonly object msgToSend; + + /// + /// Message to send to Direct Server Clients + /// + /// message object to send + /// WebSocket server instance + public MessageToClients(object msg, MobileControlWebsocketServer server) + { + _server = server; + msgToSend = msg; + } + + /// + /// Message to send to Direct Server Clients + /// + /// message object to send + /// WebSocket server instance + public MessageToClients(DeviceStateMessageBase msg, MobileControlWebsocketServer server) + { + _server = server; + msgToSend = msg; + } + + #region Implementation of IQueueMessage + + /// + /// Dispatch method + /// + public void Dispatch() + { + try + { + if (_server == null) + { + Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Server is null"); + return; + } + + var message = JsonConvert.SerializeObject(msgToSend, Formatting.None, + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } }); + + var clientSpecificMessage = msgToSend as MobileControlMessage; + if (clientSpecificMessage.ClientId != null) + { + var clientId = clientSpecificMessage.ClientId; + + _server.LogVerbose("Message TX To client {clientId}: {message}", clientId, message); + + _server.SendMessageToClient(clientId, message); + + return; + } + + _server.SendMessageToAllClients(message); + + _server.LogVerbose("Message TX To all clients: {message}", message); + } + catch (ThreadAbortException) + { + //Swallowing this exception, as it occurs on shutdown and there's no need to print out a scary stack trace + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Caught an exception in the Transmit Processor"); + } + } + #endregion + } + +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlAction.cs b/src/PepperDash.Essentials.MobileControl/MobileControlAction.cs index 2e402dfb..16a9bd40 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlAction.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlAction.cs @@ -1,6 +1,6 @@ -using Newtonsoft.Json.Linq; +using System; +using Newtonsoft.Json.Linq; using PepperDash.Essentials.Core.DeviceTypeInterfaces; -using System; namespace PepperDash.Essentials { @@ -10,12 +10,20 @@ namespace PepperDash.Essentials public class MobileControlAction : IMobileControlAction { /// - /// Gets or sets the Messenger + /// Gets the Messenger /// public IMobileControlMessenger Messenger { get; private set; } + /// + /// Action to execute when this path is matched + /// public Action Action { get; private set; } + /// + /// Initialize an instance of the class + /// + /// Messenger associated with this action + /// Action to take when this path is matched public MobileControlAction(IMobileControlMessenger messenger, Action handler) { Messenger = messenger; diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs b/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs index c4aa673b..6147b14e 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs @@ -1,28 +1,25 @@ -using PepperDash.Core; -using PepperDash.Core.Logging; +using System; +using System.Collections.Generic; +using PepperDash.Core; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Config; -using Serilog.Events; -using System; -using System.Collections.Generic; -using System.Linq; namespace PepperDash.Essentials { /// - /// Represents a MobileControlDeviceFactory + /// Factory to create a Mobile Control System Controller /// public class MobileControlDeviceFactory : EssentialsDeviceFactory { + /// + /// Create the factory for a Mobile Control System Controller + /// public MobileControlDeviceFactory() { TypeNames = new List { "appserver", "mobilecontrol", "webserver" }; } - /// - /// BuildDevice method - /// /// public override EssentialsDevice BuildDevice(DeviceConfig dc) { diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlEssentialsConfig.cs b/src/PepperDash.Essentials.MobileControl/MobileControlEssentialsConfig.cs index 7183ec84..d0415287 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlEssentialsConfig.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlEssentialsConfig.cs @@ -6,29 +6,35 @@ using PepperDash.Essentials.Core.Config; namespace PepperDash.Essentials { /// - /// Represents a MobileControlEssentialsConfig + /// Configuration class for sending data to Mobile Control Edge or a client using the Direct Server /// public class MobileControlEssentialsConfig : EssentialsConfig { + /// + /// Current versions for the system + /// [JsonProperty("runtimeInfo")] public MobileControlRuntimeInfo RuntimeInfo { get; set; } + /// + /// Create Configuration for Mobile Control. Used as part of the data sent to a client + /// + /// The base configuration public MobileControlEssentialsConfig(EssentialsConfig config) : base() { - // TODO: Consider using Reflection to iterate properties - this.Devices = config.Devices; - this.Info = config.Info; - this.JoinMaps = config.JoinMaps; - this.Rooms = config.Rooms; - this.SourceLists = config.SourceLists; - this.DestinationLists = config.DestinationLists; - this.SystemUrl = config.SystemUrl; - this.TemplateUrl = config.TemplateUrl; - this.TieLines = config.TieLines; + Devices = config.Devices; + Info = config.Info; + JoinMaps = config.JoinMaps; + Rooms = config.Rooms; + SourceLists = config.SourceLists; + DestinationLists = config.DestinationLists; + SystemUrl = config.SystemUrl; + TemplateUrl = config.TemplateUrl; + TieLines = config.TieLines; - if (this.Info == null) - this.Info = new InfoConfig(); + if (Info == null) + Info = new InfoConfig(); RuntimeInfo = new MobileControlRuntimeInfo(); } @@ -46,15 +52,21 @@ namespace PepperDash.Essentials [JsonProperty("pluginVersion")] public string PluginVersion { get; set; } + /// + /// Essentials Version + /// [JsonProperty("essentialsVersion")] public string EssentialsVersion { get; set; } + /// + /// PepperDash Core Version + /// [JsonProperty("pepperDashCoreVersion")] public string PepperDashCoreVersion { get; set; } /// - /// Gets or sets the EssentialsPlugins + /// List of Plugins loaded on this system /// [JsonProperty("essentialsPlugins")] public List EssentialsPlugins { get; set; } diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs b/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs index 393b3385..4f3c5433 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs @@ -7,10 +7,13 @@ using PepperDash.Essentials.Core; namespace PepperDash.Essentials { /// - /// Represents a MobileControlFactory + /// Factory class for the Mobile Control App Controller /// public class MobileControlFactory { + /// + /// Create an instance of the class. + /// public MobileControlFactory() { var assembly = Assembly.GetExecutingAssembly(); diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index d6a40ede..45ff814a 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -91,10 +91,16 @@ namespace PepperDash.Essentials /// public MobileControlApiService ApiService { get; private set; } + /// + /// Get Room Bridges associated with this controller + /// public List RoomBridges => _roomBridges; private readonly MobileControlWebsocketServer _directServer; + /// + /// Get the Direct Server instance associated with this controller + /// public MobileControlWebsocketServer DirectServer => _directServer; private readonly CCriticalSection _wsCriticalSection = new CCriticalSection(); @@ -104,10 +110,16 @@ namespace PepperDash.Essentials /// public string SystemUrl; //set only from SIMPL Bridge! + /// + /// True if the Mobile Control Edge Server Websocket is connected + /// public bool Connected => _wsClient2 != null && _wsClient2.IsAlive; private IEssentialsRoomCombiner _roomCombiner; + /// + /// Gets the SystemUuid from configuration or SIMPL Bridge + /// public string SystemUuid { get @@ -169,6 +181,9 @@ namespace PepperDash.Essentials private DateTime _lastAckMessage; + /// + /// Gets the LastAckMessage timestamp + /// public DateTime LastAckMessage => _lastAckMessage; private CTimer _pingTimer; @@ -177,11 +192,11 @@ namespace PepperDash.Essentials private LogLevel _wsLogLevel = LogLevel.Error; /// - /// + /// Initializes a new instance of the class. /// - /// - /// - /// + /// The unique key for this controller. + /// The name of the controller. + /// The configuration settings for the controller. public MobileControlSystemController(string key, string name, MobileControlConfig config) : base(key, name) { @@ -1192,6 +1207,9 @@ namespace PepperDash.Essentials /// public string Host { get; private set; } + /// + /// Gets the configured Client App URL + /// public string ClientAppUrl => Config.ClientAppUrl; private void OnRoomCombinationScenarioChanged( @@ -1203,7 +1221,7 @@ namespace PepperDash.Essentials } /// - /// CheckForDeviceMessenger method + /// Checks if a device messenger exists for the given key. /// public bool CheckForDeviceMessenger(string key) { @@ -1211,13 +1229,13 @@ namespace PepperDash.Essentials } /// - /// AddDeviceMessenger method + /// Add the provided messenger to the messengers collection /// public void AddDeviceMessenger(IMobileControlMessenger messenger) { if (_messengers.ContainsKey(messenger.Key)) { - this.LogWarning("Messenger with key {messengerKey) already added", messenger.Key); + this.LogWarning("Messenger with key {messengerKey} already added", messenger.Key); return; } @@ -1291,12 +1309,14 @@ namespace PepperDash.Essentials messenger.RegisterWithAppServer(this); } - /// - /// Initialize method - /// /// public override void Initialize() { + if (!Config.EnableMessengerSubscriptions) + { + this.LogWarning("Messenger subscriptions disabled. add \"enableMessengerSubscriptions\": true to config for {key} to enable.", Key); + } + foreach (var messenger in _messengers) { try @@ -1338,7 +1358,7 @@ namespace PepperDash.Essentials #region IMobileControl Members /// - /// GetAppServer method + /// Gets the App Server instance /// public static IMobileControl GetAppServer() { @@ -1356,16 +1376,10 @@ namespace PepperDash.Essentials } } - /// - /// Generates the url and creates the websocket client - /// private bool CreateWebsocket() { - if (_wsClient2 != null) - { - _wsClient2.Close(); - _wsClient2 = null; - } + _wsClient2?.Close(); + _wsClient2 = null; if (string.IsNullOrEmpty(SystemUuid)) { @@ -1382,33 +1396,13 @@ namespace PepperDash.Essentials { Log = { - Output = (data, message) => - { - switch (data.Level) - { - case LogLevel.Trace: - this.LogVerbose(data.Message); - break; - case LogLevel.Debug: - this.LogDebug(data.Message); - break; - case LogLevel.Info: - this.LogInformation(data.Message); - break; - case LogLevel.Warn: - this.LogWarning(data.Message); - break; - case LogLevel.Error: - this.LogError(data.Message); - break; - case LogLevel.Fatal: - this.LogFatal(data.Message); - break; - } - } + Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this) } }; + // setting to trace to let level be controlled by appdebug + _wsClient2.Log.Level = LogLevel.Trace; + _wsClient2.SslConfiguration.EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls12; @@ -1422,7 +1416,7 @@ namespace PepperDash.Essentials } /// - /// LinkSystemMonitorToAppServer method + /// Link the System Monitor to this App server /// public void LinkSystemMonitorToAppServer() { @@ -1449,14 +1443,6 @@ namespace PepperDash.Essentials private void SetWebsocketDebugLevel(string cmdparameters) { - // if (CrestronEnvironment.ProgramCompatibility == eCrestronSeries.Series4) - // { - // this.LogInformation( - // "Setting websocket log level not currently allowed on 4 series." - // ); - // return; // Web socket log level not currently allowed in series4 - // } - if (string.IsNullOrEmpty(cmdparameters)) { this.LogInformation("Current Websocket debug level: {webSocketDebugLevel}", _wsLogLevel); @@ -1494,10 +1480,6 @@ namespace PepperDash.Essentials } } - /// - /// Sends message to server to indicate the system is shutting down - /// - /// private void CrestronEnvironment_ProgramStatusEventHandler( eProgramStatusEventType programEventType ) @@ -1530,6 +1512,9 @@ namespace PepperDash.Essentials } } + /// + /// Get action paths for the current actions + /// public List<(string, string)> GetActionDictionaryPaths() { var paths = new List<(string, string)>(); @@ -1602,24 +1587,24 @@ namespace PepperDash.Essentials } } + /// + /// Get the room bridge with the provided key + /// + /// The key of the room bridge public MobileControlBridgeBase GetRoomBridge(string key) { return _roomBridges.FirstOrDefault((r) => r.RoomKey.Equals(key)); } /// - /// GetRoomMessenger method + /// Get the room messenger with the provided key /// + /// The Key of the rooom messenger public IMobileControlRoomMessenger GetRoomMessenger(string key) { return _roomBridges.FirstOrDefault((r) => r.RoomKey.Equals(key)); } - /// - /// - /// - /// - /// private void Bridge_ConfigurationIsReady(object sender, EventArgs e) { this.LogDebug("Bridge ready. Registering"); @@ -1640,10 +1625,6 @@ namespace PepperDash.Essentials } } - /// - /// - /// - /// private void ReconnectToServerTimerCallback(object o) { this.LogDebug("Attempting to reconnect to server..."); @@ -1651,9 +1632,6 @@ namespace PepperDash.Essentials ConnectWebsocketClient(); } - /// - /// Verifies system connection with servers - /// private void AuthorizeSystem(string code) { if ( @@ -1698,9 +1676,6 @@ namespace PepperDash.Essentials }); } - /// - /// Dumps info in response to console command. - /// private void ShowInfo() { var url = Config != null ? Host : "No config"; @@ -1766,38 +1741,37 @@ namespace PepperDash.Essentials "\r\n UI Client Info:\r\n" + " Tokens Defined: {0}\r\n" + " Clients Connected: {1}\r\n", - _directServer.UiClients.Count, + _directServer.UiClientContexts.Count, _directServer.ConnectedUiClientsCount ); var clientNo = 1; - foreach (var clientContext in _directServer.UiClients) + foreach (var clientContext in _directServer.UiClientContexts) { - var isAlive = false; - var duration = "Not Connected"; - - if (clientContext.Value.Client != null) - { - isAlive = clientContext.Value.Client.Context.WebSocket.IsAlive; - duration = clientContext.Value.Client.ConnectedDuration.ToString(); - } + var clients = _directServer.UiClients.Values.Where(c => c.Token == clientContext.Value.Token.Token); CrestronConsole.ConsoleCommandResponse( - "\r\nClient {0}:\r\n" + - "Room Key: {1}\r\n" + - "Touchpanel Key: {6}\r\n" + - "Token: {2}\r\n" + - "Client URL: {3}\r\n" + - "Connected: {4}\r\n" + - "Duration: {5}\r\n", - clientNo, - clientContext.Value.Token.RoomKey, - clientContext.Key, - string.Format("{0}{1}", _directServer.UserAppUrlPrefix, clientContext.Key), - isAlive, - duration, - clientContext.Value.Token.TouchpanelKey + $"\r\nClient {clientNo}:\r\n" + + $" Room Key: {clientContext.Value.Token.RoomKey}\r\n" + + $" Touchpanel Key: {clientContext.Value.Token.TouchpanelKey}\r\n" + + $" Token: {clientContext.Key}\r\n" + + $" Client URL: {_directServer.UserAppUrlPrefix}{clientContext.Key}\r\n" + + $" Clients:\r\n" ); + + if (!clients.Any()) + { + CrestronConsole.ConsoleCommandResponse(" No clients connected"); + } + foreach (var client in clients) + { + CrestronConsole.ConsoleCommandResponse( + $" ID: {client.Id}\r\n" + + $" Connected: {client.Context.WebSocket.IsAlive}\r\n" + + $" Duration: {(client.Context.WebSocket.IsAlive ? client.ConnectedDuration.TotalSeconds.ToString() : "Not Connected")}\r\n" + ); + } + clientNo++; } } @@ -1811,7 +1785,7 @@ namespace PepperDash.Essentials } /// - /// RegisterSystemToServer method + /// Register this system to the Mobile Control Edge Server /// public void RegisterSystemToServer() { @@ -1835,9 +1809,6 @@ namespace PepperDash.Essentials ConnectWebsocketClient(); } - /// - /// Connects the Websocket Client - /// private void ConnectWebsocketClient() { try @@ -1878,9 +1849,6 @@ namespace PepperDash.Essentials } } - /// - /// Attempts to connect the websocket - /// private void TryConnect() { try @@ -1910,9 +1878,6 @@ namespace PepperDash.Essentials } } - /// - /// Gracefully handles conect failures by reconstructing the ws client and starting the reconnect timer - /// private void HandleConnectFailure() { _wsClient2 = null; @@ -1944,11 +1909,6 @@ namespace PepperDash.Essentials StartServerReconnectTimer(); } - /// - /// - /// - /// - /// private void HandleOpen(object sender, EventArgs e) { StopServerReconnectTimer(); @@ -1957,11 +1917,6 @@ namespace PepperDash.Essentials SendMessageObject(new MobileControlMessage { Type = "hello" }); } - /// - /// - /// - /// - /// private void HandleMessage(object sender, MessageEventArgs e) { if (e.IsPing) @@ -1978,11 +1933,6 @@ namespace PepperDash.Essentials } } - /// - /// - /// - /// - /// private void HandleError(object sender, ErrorEventArgs e) { this.LogError("Websocket error {0}", e.Message); @@ -1991,11 +1941,6 @@ namespace PepperDash.Essentials StartServerReconnectTimer(); } - /// - /// - /// - /// - /// private void HandleClose(object sender, CloseEventArgs e) { this.LogDebug( @@ -2016,9 +1961,6 @@ namespace PepperDash.Essentials StartServerReconnectTimer(); } - /// - /// After a "hello" from the server, sends config and stuff - /// private void SendInitialMessage() { this.LogInformation("Sending initial join message"); @@ -2045,7 +1987,7 @@ namespace PepperDash.Essentials } /// - /// GetConfigWithPluginVersion method + /// Get the Essentials configuration with version data /// public MobileControlEssentialsConfig GetConfigWithPluginVersion() { @@ -2080,8 +2022,13 @@ namespace PepperDash.Essentials } /// - /// SetClientUrl method + /// Set the Client URL for a given room /// + /// new App URL + /// room key. Default is null + /// + /// If roomKey is null, the URL will be set for the entire system. + /// public void SetClientUrl(string path, string roomKey = null) { var message = new MobileControlMessage @@ -2097,9 +2044,6 @@ namespace PepperDash.Essentials /// Sends any object type to server /// /// - /// - /// SendMessageObject method - /// public void SendMessageObject(IMobileControlMessage o) { @@ -2123,8 +2067,9 @@ namespace PepperDash.Essentials /// - /// SendMessageObjectToDirectClient method + /// Send a message to a client using the Direct Server /// + /// object to send public void SendMessageObjectToDirectClient(object o) { if ( @@ -2137,10 +2082,6 @@ namespace PepperDash.Essentials } } - - /// - /// Disconnects the Websocket Client and stops the heartbeat timer - /// private void CleanUpWebsocketClient() { if (_wsClient2 == null) @@ -2198,9 +2139,6 @@ namespace PepperDash.Essentials } } - /// - /// - /// private void StartServerReconnectTimer() { StopServerReconnectTimer(); @@ -2211,9 +2149,6 @@ namespace PepperDash.Essentials this.LogDebug("Reconnect Timer Started."); } - /// - /// Does what it says - /// private void StopServerReconnectTimer() { if (_serverReconnectTimer == null) @@ -2224,10 +2159,6 @@ namespace PepperDash.Essentials _serverReconnectTimer = null; } - /// - /// Resets reconnect timer and updates usercode - /// - /// private void HandleHeartBeat(JToken content) { SendMessageObject(new MobileControlMessage { Type = "/system/heartbeatAck" }); @@ -2248,6 +2179,7 @@ namespace PepperDash.Essentials { var clientId = content["clientId"].Value(); var roomKey = content["roomKey"].Value(); + var touchpanelKey = content.SelectToken("touchpanelKey"); if (_roomCombiner == null) { @@ -2259,6 +2191,10 @@ namespace PepperDash.Essentials }; SendMessageObject(message); + + SendDeviceInterfaces(clientId); + + SendTouchpanelKey(clientId, touchpanelKey); return; } @@ -2270,7 +2206,12 @@ namespace PepperDash.Essentials ClientId = clientId, Content = roomKey }; + SendMessageObject(message); + + SendDeviceInterfaces(clientId); + + SendTouchpanelKey(clientId, touchpanelKey); return; } @@ -2288,6 +2229,10 @@ namespace PepperDash.Essentials }; SendMessageObject(message); + + SendDeviceInterfaces(clientId); + + SendTouchpanelKey(clientId, touchpanelKey); return; } @@ -2301,6 +2246,54 @@ namespace PepperDash.Essentials }; SendMessageObject(newMessage); + + SendDeviceInterfaces(clientId); + + SendTouchpanelKey(clientId, touchpanelKey); + } + + private void SendTouchpanelKey(string clientId, JToken touchpanelKeyToken) + { + if (touchpanelKeyToken == null) + { + this.LogWarning("Touchpanel key not found for client {clientId}", clientId); + return; + } + + SendMessageObject(new MobileControlMessage + { + Type = "/system/touchpanelKey", + ClientId = clientId, + Content = touchpanelKeyToken.Value() + }); + } + + private void SendDeviceInterfaces(string clientId) + { + this.LogDebug("Sending Device interfaces"); + var devices = DeviceManager.GetDevices(); + Dictionary deviceInterfaces = new Dictionary(); + + foreach (var device in devices) + { + var interfaces = device?.GetType().GetInterfaces().Select((i) => i.Name).ToList() ?? new List(); + + deviceInterfaces.Add(device.Key, new DeviceInterfaceInfo + { + Key = device.Key, + Name = (device as IKeyName)?.Name ?? "", + Interfaces = interfaces + }); + } + + var message = new MobileControlMessage + { + Type = "/system/deviceInterfaces", + ClientId = clientId, + Content = JToken.FromObject(new { deviceInterfaces }) + }; + + SendMessageObject(message); } private void HandleUserCode(JToken content, Action action = null) @@ -2337,16 +2330,13 @@ namespace PepperDash.Essentials } /// - /// HandleClientMessage method + /// Enqueue an incoming message for processing /// public void HandleClientMessage(string message) { _receiveQueue.Enqueue(new ProcessStringMessage(message, ParseStreamRx)); } - /// - /// - /// private void ParseStreamRx(string messageText) { if (string.IsNullOrEmpty(messageText)) @@ -2414,10 +2404,33 @@ namespace PepperDash.Essentials foreach (var handler in handlers) { - Task.Run( - () => - handler.Action(message.Type, message.ClientId, message.Content) - ); + Task.Run(() => + { + try + { + handler.Action(message.Type, message.ClientId, message.Content); + } + catch (Exception ex) + { + this.LogError( + "Exception in handler for message type {type}, ClientId {clientId}", + message.Type, + message.ClientId + ); + this.LogDebug(ex, "Stack Trace: "); + } + }).ContinueWith(task => + { + if (task.IsFaulted && task.Exception != null) + { + this.LogError( + "Unhandled exception in Task for message type {type}, ClientId {clientId}", + message.Type, + message.ClientId + ); + this.LogDebug(task.Exception.GetBaseException(), "Stack Trace: "); + } + }, TaskContinuationOptions.OnlyOnFaulted); } break; @@ -2433,10 +2446,6 @@ namespace PepperDash.Essentials } } - /// - /// - /// - /// private void TestHttpRequest(string s) { { diff --git a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlBridgeBase.cs b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlBridgeBase.cs index ac1a851c..634255fe 100644 --- a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlBridgeBase.cs +++ b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlBridgeBase.cs @@ -1,23 +1,35 @@ -using PepperDash.Core; +using System; +using PepperDash.Core; using PepperDash.Core.Logging; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core.DeviceTypeInterfaces; -using System; namespace PepperDash.Essentials.RoomBridges { /// - /// + /// Base class for a Mobile Control Bridge that's used to control a room /// public abstract class MobileControlBridgeBase : MessengerBase, IMobileControlRoomMessenger { + /// + /// Triggered when the user Code changes + /// public event EventHandler UserCodeChanged; + /// + /// Triggered when a user should be prompted for the new code + /// public event EventHandler UserPromptedForCode; + /// + /// Triggered when a client joins to control this room + /// public event EventHandler ClientJoined; + /// + /// Triggered when the App URL for this room changes + /// public event EventHandler AppUrlChanged; /// @@ -49,15 +61,32 @@ namespace PepperDash.Essentials.RoomBridges /// public string McServerUrl { get; private set; } + /// + /// Room Name + /// public abstract string RoomName { get; } + /// + /// Room key + /// public abstract string RoomKey { get; } + /// + /// Create an instance of the class + /// + /// The unique key for this bridge + /// The message path for this bridge protected MobileControlBridgeBase(string key, string messagePath) : base(key, messagePath) { } + /// + /// Create an instance of the class + /// + /// The unique key for this bridge + /// The message path for this bridge + /// The device associated with this bridge protected MobileControlBridgeBase(string key, string messagePath, IKeyName device) : base(key, messagePath, device) { @@ -110,6 +139,10 @@ namespace PepperDash.Essentials.RoomBridges SetUserCode(code); } + /// + /// Update the App Url with the provided URL + /// + /// The new App URL public virtual void UpdateAppUrl(string url) { AppUrl = url; @@ -137,16 +170,25 @@ namespace PepperDash.Essentials.RoomBridges OnUserCodeChanged(); } + /// + /// Trigger the UserCodeChanged event + /// protected void OnUserCodeChanged() { UserCodeChanged?.Invoke(this, new EventArgs()); } + /// + /// Trigger the UserPromptedForCode event + /// protected void OnUserPromptedForCode() { UserPromptedForCode?.Invoke(this, new EventArgs()); } + /// + /// Trigger the ClientJoined event + /// protected void OnClientJoined() { ClientJoined?.Invoke(this, new EventArgs()); diff --git a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs index f0463f27..742251db 100644 --- a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs +++ b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs @@ -41,24 +41,37 @@ namespace PepperDash.Essentials.RoomBridges /// public string DefaultRoomKey { get; private set; } /// - /// + /// Gets the name of the room /// public override string RoomName { get { return Room.Name; } } + /// + /// Gets the key of the room + /// public override string RoomKey { get { return Room.Key; } } + /// + /// Initializes a new instance of the class with the specified room + /// + /// The essentials room to bridge public MobileControlEssentialsRoomBridge(IEssentialsRoom room) : this($"mobileControlBridge-{room.Key}", room.Key, room) { Room = room; } + /// + /// Initializes a new instance of the class with the specified parameters + /// + /// The unique key for this bridge + /// The key of the room to bridge + /// The essentials room to bridge public MobileControlEssentialsRoomBridge(string key, string roomKey, IEssentialsRoom room) : base(key, $"/room/{room.Key}", room as Device) { DefaultRoomKey = roomKey; @@ -66,7 +79,9 @@ namespace PepperDash.Essentials.RoomBridges AddPreActivationAction(GetRoom); } - + /// + /// Registers all message handling actions with the AppServer for this room bridge + /// protected override void RegisterActions() { // we add actions to the messaging system with a path, and a related action. Custom action @@ -284,6 +299,9 @@ namespace PepperDash.Essentials.RoomBridges Room = tempRoom; } + /// + /// Handles user code changes and generates QR code URL + /// protected override void UserCodeChange() { Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Server user code changed: {userCode}", this, UserCode); @@ -807,18 +825,33 @@ namespace PepperDash.Essentials.RoomBridges [JsonProperty("configuration", NullValueHandling = NullValueHandling.Ignore)] public RoomConfiguration Configuration { get; set; } + /// + /// Gets or sets the activity mode of the room + /// [JsonProperty("activityMode", NullValueHandling = NullValueHandling.Ignore)] public int? ActivityMode { get; set; } + /// + /// Gets or sets whether advanced sharing is active + /// [JsonProperty("advancedSharingActive", NullValueHandling = NullValueHandling.Ignore)] public bool? AdvancedSharingActive { get; set; } + /// + /// Gets or sets whether the room is powered on + /// [JsonProperty("isOn", NullValueHandling = NullValueHandling.Ignore)] public bool? IsOn { get; set; } + /// + /// Gets or sets whether the room is warming up + /// [JsonProperty("isWarmingUp", NullValueHandling = NullValueHandling.Ignore)] public bool? IsWarmingUp { get; set; } + /// + /// Gets or sets whether the room is cooling down + /// [JsonProperty("isCoolingDown", NullValueHandling = NullValueHandling.Ignore)] public bool? IsCoolingDown { get; set; } @@ -834,9 +867,15 @@ namespace PepperDash.Essentials.RoomBridges [JsonProperty("share", NullValueHandling = NullValueHandling.Ignore)] public ShareState Share { get; set; } + /// + /// Gets or sets the volume controls collection + /// [JsonProperty("volumes", NullValueHandling = NullValueHandling.Ignore)] public Dictionary Volumes { get; set; } + /// + /// Gets or sets whether the room is in a call + /// [JsonProperty("isInCall", NullValueHandling = NullValueHandling.Ignore)] public bool? IsInCall { get; set; } } @@ -853,9 +892,15 @@ namespace PepperDash.Essentials.RoomBridges [JsonProperty("currentShareText", NullValueHandling = NullValueHandling.Ignore)] public string CurrentShareText { get; set; } + /// + /// Gets or sets whether sharing is enabled + /// [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] public bool? Enabled { get; set; } + /// + /// Gets or sets whether content is currently being shared + /// [JsonProperty("isSharing", NullValueHandling = NullValueHandling.Ignore)] public bool? IsSharing { get; set; } } @@ -865,24 +910,45 @@ namespace PepperDash.Essentials.RoomBridges /// public class RoomConfiguration { + /// + /// Gets or sets whether the room has video conferencing capabilities + /// [JsonProperty("hasVideoConferencing", NullValueHandling = NullValueHandling.Ignore)] public bool? HasVideoConferencing { get; set; } + /// + /// Gets or sets whether the video codec is a Zoom Room + /// [JsonProperty("videoCodecIsZoomRoom", NullValueHandling = NullValueHandling.Ignore)] public bool? VideoCodecIsZoomRoom { get; set; } + /// + /// Gets or sets whether the room has audio conferencing capabilities + /// [JsonProperty("hasAudioConferencing", NullValueHandling = NullValueHandling.Ignore)] public bool? HasAudioConferencing { get; set; } + /// + /// Gets or sets whether the room has environmental controls (lighting, shades, etc.) + /// [JsonProperty("hasEnvironmentalControls", NullValueHandling = NullValueHandling.Ignore)] public bool? HasEnvironmentalControls { get; set; } + /// + /// Gets or sets whether the room has camera controls + /// [JsonProperty("hasCameraControls", NullValueHandling = NullValueHandling.Ignore)] public bool? HasCameraControls { get; set; } + /// + /// Gets or sets whether the room has set-top box controls + /// [JsonProperty("hasSetTopBoxControls", NullValueHandling = NullValueHandling.Ignore)] public bool? HasSetTopBoxControls { get; set; } + /// + /// Gets or sets whether the room has routing controls + /// [JsonProperty("hasRoutingControls", NullValueHandling = NullValueHandling.Ignore)] public bool? HasRoutingControls { get; set; } @@ -949,6 +1015,9 @@ namespace PepperDash.Essentials.RoomBridges [JsonProperty("defaultDisplayKey", NullValueHandling = NullValueHandling.Ignore)] public string DefaultDisplayKey { get; set; } + /// + /// Gets or sets the destinations dictionary keyed by destination type + /// [JsonProperty("destinations", NullValueHandling = NullValueHandling.Ignore)] public Dictionary Destinations { get; set; } @@ -959,9 +1028,15 @@ namespace PepperDash.Essentials.RoomBridges [JsonProperty("environmentalDevices", NullValueHandling = NullValueHandling.Ignore)] public List EnvironmentalDevices { get; set; } + /// + /// Gets or sets the source list for the room + /// [JsonProperty("sourceList", NullValueHandling = NullValueHandling.Ignore)] public Dictionary SourceList { get; set; } + /// + /// Gets or sets the destination list for the room + /// [JsonProperty("destinationList", NullValueHandling = NullValueHandling.Ignore)] public Dictionary DestinationList { get; set; } @@ -972,6 +1047,9 @@ namespace PepperDash.Essentials.RoomBridges [JsonProperty("audioControlPointList", NullValueHandling = NullValueHandling.Ignore)] public AudioControlPointListItem AudioControlPointList { get; set; } + /// + /// Gets or sets the camera list for the room + /// [JsonProperty("cameraList", NullValueHandling = NullValueHandling.Ignore)] public Dictionary CameraList { get; set; } @@ -1004,9 +1082,15 @@ namespace PepperDash.Essentials.RoomBridges [JsonProperty("uiBehavior", NullValueHandling = NullValueHandling.Ignore)] public EssentialsRoomUiBehaviorConfig UiBehavior { get; set; } + /// + /// Gets or sets whether the room supports advanced sharing features + /// [JsonProperty("supportsAdvancedSharing", NullValueHandling = NullValueHandling.Ignore)] public bool? SupportsAdvancedSharing { get; set; } + /// + /// Gets or sets whether the user can change the share mode + /// [JsonProperty("userCanChangeShareMode", NullValueHandling = NullValueHandling.Ignore)] public bool? UserCanChangeShareMode { get; set; } @@ -1017,6 +1101,9 @@ namespace PepperDash.Essentials.RoomBridges [JsonProperty("roomCombinerKey", NullValueHandling = NullValueHandling.Ignore)] public string RoomCombinerKey { get; set; } + /// + /// Initializes a new instance of the class + /// public RoomConfiguration() { Destinations = new Dictionary(); @@ -1046,6 +1133,11 @@ namespace PepperDash.Essentials.RoomBridges [JsonProperty("deviceType", NullValueHandling = NullValueHandling.Ignore)] public eEnvironmentalDeviceTypes DeviceType { get; private set; } + /// + /// Initializes a new instance of the class + /// + /// The device key + /// The environmental device type public EnvironmentalDeviceConfiguration(string key, eEnvironmentalDeviceTypes type) { DeviceKey = key; @@ -1054,14 +1146,29 @@ namespace PepperDash.Essentials.RoomBridges } /// - /// Enumeration of eEnvironmentalDeviceTypes values + /// Enumeration of environmental device types /// public enum eEnvironmentalDeviceTypes { + /// + /// No environmental device type specified + /// None, + /// + /// Lighting device type + /// Lighting, + /// + /// Shade device type + /// Shade, + /// + /// Shade controller device type + /// ShadeController, + /// + /// Relay device type + /// Relay, } diff --git a/src/PepperDash.Essentials.MobileControl/Services/MobileControlApiService.cs b/src/PepperDash.Essentials.MobileControl/Services/MobileControlApiService.cs index 4a84f8f1..508fb776 100644 --- a/src/PepperDash.Essentials.MobileControl/Services/MobileControlApiService.cs +++ b/src/PepperDash.Essentials.MobileControl/Services/MobileControlApiService.cs @@ -1,18 +1,22 @@ -using PepperDash.Core; -using System; +using System; using System.Net.Http; using System.Threading.Tasks; +using PepperDash.Core; namespace PepperDash.Essentials.Services { /// - /// Represents a MobileControlApiService + /// Service for interacting with a Mobile Control Edge server instance /// public class MobileControlApiService { private readonly HttpClient _client; + /// + /// Create an instance of the class. + /// + /// Mobile Control Edge API URL public MobileControlApiService(string apiUrl) { var handler = new HttpClientHandler @@ -24,6 +28,13 @@ namespace PepperDash.Essentials.Services _client = new HttpClient(handler); } + /// + /// Send authorization request to Mobile Control Edge Server + /// + /// Mobile Control Edge API URL + /// Grant code for authorization + /// System UUID for authorization + /// Authorization response public async Task SendAuthorizationRequest(string apiUrl, string grantCode, string systemUuid) { try diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITheme.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITheme.cs index e5ba76cf..c7efbf4c 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITheme.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITheme.cs @@ -7,8 +7,15 @@ namespace PepperDash.Essentials.Touchpanel /// public interface ITheme : IKeyed { + /// + /// Current theme + /// string Theme { get; } + /// + /// Set the theme with the given value + /// + /// The theme to set void UpdateTheme(string theme); } } diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControl.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControl.cs index 3beb92c5..055ad80c 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControl.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControl.cs @@ -8,12 +8,24 @@ namespace PepperDash.Essentials.Touchpanel /// public interface ITswAppControl : IKeyed { + /// + /// Updates when the Zoom Room Control Application opens or closes + /// BoolFeedback AppOpenFeedback { get; } + /// + /// Hide the Zoom App and show the User Control Application + /// void HideOpenApp(); + /// + /// Close the Zoom App and show the User Control Application + /// void CloseOpenApp(); + /// + /// Open the Zoom App + /// void OpenApp(); } @@ -22,10 +34,19 @@ namespace PepperDash.Essentials.Touchpanel /// public interface ITswZoomControl : IKeyed { + /// + /// Updates when Zoom has an incoming call + /// BoolFeedback ZoomIncomingCallFeedback { get; } + /// + /// Updates when Zoom is in a call + /// BoolFeedback ZoomInCallFeedback { get; } + /// + /// End a Zoom Call + /// void EndZoomCall(); } } diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs index f9581c74..acfcab2a 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs @@ -7,17 +7,24 @@ using PepperDash.Essentials.AppServer.Messengers; namespace PepperDash.Essentials.Touchpanel { /// - /// Represents a ITswAppControlMessenger + /// Messenger for controlling the Zoom App on a TSW Panel that supports the Zoom Room Control Application /// public class ITswAppControlMessenger : MessengerBase { private readonly ITswAppControl _appControl; + /// + /// Create an instance of the class. + /// + /// The key for this messenger + /// The message path for this messenger + /// The device for this messenger public ITswAppControlMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) { _appControl = device as ITswAppControl; } + /// protected override void RegisterActions() { if (_appControl == null) @@ -26,7 +33,7 @@ namespace PepperDash.Essentials.Touchpanel return; } - AddAction($"/fullStatus", (id, context) => SendFullStatus()); + AddAction($"/fullStatus", (id, context) => SendFullStatus(id)); AddAction($"/openApp", (id, context) => _appControl.OpenApp()); @@ -43,14 +50,14 @@ namespace PepperDash.Essentials.Touchpanel }; } - private void SendFullStatus() + private void SendFullStatus(string id = null) { var message = new TswAppStateMessage { AppOpen = _appControl.AppOpenFeedback.BoolValue, }; - PostStatusMessage(message); + PostStatusMessage(message, id); } } @@ -59,6 +66,9 @@ namespace PepperDash.Essentials.Touchpanel /// public class TswAppStateMessage : DeviceStateMessageBase { + /// + /// True if the Zoom app is open on a TSW panel + /// [JsonProperty("appOpen", NullValueHandling = NullValueHandling.Ignore)] public bool? AppOpen { get; set; } } diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs index 48c7fb7f..bbf4030e 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs @@ -8,17 +8,24 @@ using PepperDash.Essentials.AppServer.Messengers; namespace PepperDash.Essentials.Touchpanel { /// - /// Represents a ITswZoomControlMessenger + /// Messenger to handle Zoom status and control for a TSW panel that supports the Zoom Application /// public class ITswZoomControlMessenger : MessengerBase { private readonly ITswZoomControl _zoomControl; + /// + /// Create an instance of the class for the given device + /// + /// The key for this messenger + /// The message path for this messenger + /// The device for this messenger public ITswZoomControlMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) { _zoomControl = device as ITswZoomControl; } + /// protected override void RegisterActions() { if (_zoomControl == null) @@ -27,7 +34,9 @@ namespace PepperDash.Essentials.Touchpanel return; } - AddAction($"/fullStatus", (id, context) => SendFullStatus()); + AddAction($"/fullStatus", (id, context) => SendFullStatus(id)); + + AddAction($"/zoomStatus", (id, content) => SendFullStatus(id)); AddAction($"/endCall", (id, context) => _zoomControl.EndZoomCall()); @@ -53,7 +62,7 @@ namespace PepperDash.Essentials.Touchpanel }; } - private void SendFullStatus() + private void SendFullStatus(string id = null) { var message = new TswZoomStateMessage { @@ -61,7 +70,7 @@ namespace PepperDash.Essentials.Touchpanel IncomingCall = _zoomControl?.ZoomIncomingCallFeedback.BoolValue }; - PostStatusMessage(message); + PostStatusMessage(message, id); } } @@ -70,9 +79,16 @@ namespace PepperDash.Essentials.Touchpanel /// public class TswZoomStateMessage : DeviceStateMessageBase { + /// + /// True if the panel is in a Zoom call + /// [JsonProperty("inCall", NullValueHandling = NullValueHandling.Ignore)] public bool? InCall { get; set; } + /// + /// True if there is an incoming Zoom call + /// + [JsonProperty("incomingCall", NullValueHandling = NullValueHandling.Ignore)] public bool? IncomingCall { get; set; } } diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs index c1743d40..5830782d 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs @@ -252,6 +252,7 @@ namespace PepperDash.Essentials.Touchpanel if (!x70Panel.ExtenderApplicationControlReservedSigs.HideOpenedApplicationFeedback.BoolValue) { x70Panel.ExtenderButtonToolbarReservedSigs.ShowButtonToolbar(); + x70Panel.ExtenderButtonToolbarReservedSigs.Button2On(); } else @@ -294,17 +295,16 @@ namespace PepperDash.Essentials.Touchpanel handler(this, new DeviceInfoEventArgs(DeviceInfo)); }; + x70Panel.ExtenderButtonToolbarReservedSigs.DeviceExtenderSigChange += (o, a) => + { + this.LogVerbose("X70 Button Toolbar Device Extender args: {event}:{sig}:{name}:{type}:{boolValue}:{ushortValue}:{stringValue}", a.Event, a.Sig, a.Sig.Name, a.Sig.Type, a.Sig.BoolValue, a.Sig.UShortValue, a.Sig.StringValue); + }; + x70Panel.ExtenderApplicationControlReservedSigs.Use(); x70Panel.ExtenderZoomRoomAppReservedSigs.Use(); x70Panel.ExtenderEthernetReservedSigs.Use(); x70Panel.ExtenderButtonToolbarReservedSigs.Use(); - x70Panel.ExtenderButtonToolbarReservedSigs.Button1Off(); - x70Panel.ExtenderButtonToolbarReservedSigs.Button3Off(); - x70Panel.ExtenderButtonToolbarReservedSigs.Button4Off(); - x70Panel.ExtenderButtonToolbarReservedSigs.Button5Off(); - x70Panel.ExtenderButtonToolbarReservedSigs.Button6Off(); - return; } @@ -414,34 +414,79 @@ namespace PepperDash.Essentials.Touchpanel McServerUrlFeedback.LinkInputSig(Panel.StringInput[3]); UserCodeFeedback.LinkInputSig(Panel.StringInput[4]); - Panel.IpInformationChange += (sender, args) => + Panel.IpInformationChange -= Panel_IpInformationChange; + Panel.IpInformationChange += Panel_IpInformationChange; + + Panel.OnlineStatusChange -= Panel_OnlineChange; + Panel.OnlineStatusChange += Panel_OnlineChange; + } + + private void Panel_OnlineChange(GenericBase sender, OnlineOfflineEventArgs args) + { + try { - if (args.Connected) + if (!args.DeviceOnLine) { - this.LogVerbose("Connection from IP: {ip}", args.DeviceIpAddress); - this.LogInformation("Sending {appUrl} on join 1", AppUrlFeedback.StringValue); - - var appUrl = GetUrlWithCorrectIp(_appUrl); - Panel.StringInput[1].StringValue = appUrl; - - SetAppUrl(appUrl); + this.LogInformation("panel is offline"); + return; } - else - { - this.LogVerbose("Disconnection from IP: {ip}", args.DeviceIpAddress); - } - }; - Panel.OnlineStatusChange += (sender, args) => - { - this.LogInformation("Sending {appUrl} on join 1", AppUrlFeedback.StringValue); + this.LogDebug("panel is online"); UpdateFeedbacks(); Panel.StringInput[1].StringValue = _appUrl; Panel.StringInput[2].StringValue = QrCodeUrlFeedback.StringValue; Panel.StringInput[3].StringValue = McServerUrlFeedback.StringValue; Panel.StringInput[4].StringValue = UserCodeFeedback.StringValue; - }; + + if (Panel is TswXX70Base x70Panel) + { + this.LogDebug("setting buttons off"); + + x70Panel.ExtenderButtonToolbarReservedSigs.Button1Off(); + x70Panel.ExtenderButtonToolbarReservedSigs.Button3Off(); + x70Panel.ExtenderButtonToolbarReservedSigs.Button4Off(); + x70Panel.ExtenderButtonToolbarReservedSigs.Button5Off(); + x70Panel.ExtenderButtonToolbarReservedSigs.Button6Off(); + } + + SendUrlToPanel(); + } + catch (Exception ex) + { + this.LogError("Exception in panel online: {message}", ex.Message); + this.LogDebug(ex, "Stack Trace: "); + } + } + + private void SendUrlToPanel() + { + var appUrl = GetUrlWithCorrectIp(_appUrl); + + this.LogInformation("Sending {appUrl} on join 1", AppUrlFeedback.StringValue); + + if (Panel.StringInput[1].StringValue == appUrl) + { + this.LogInformation("App URL already set to {appUrl}, no update needed", AppUrlFeedback.StringValue); + return; + } + + Panel.StringInput[1].StringValue = appUrl; + + SetAppUrl(appUrl); + } + + private void Panel_IpInformationChange(GenericBase sender, ConnectedIpEventArgs args) + { + if (args.Connected) + { + this.LogVerbose("Connection from IP: {ip}", args.DeviceIpAddress); + SendUrlToPanel(); + } + else + { + this.LogVerbose("Disconnection from IP: {ip}", args.DeviceIpAddress); + } } /// diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs index 089665d9..35d85442 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs @@ -1,38 +1,46 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.AppServer; using PepperDash.Essentials.AppServer.Messengers; namespace PepperDash.Essentials.Touchpanel { /// - /// Represents a ThemeMessenger + /// Messenger to save the current theme (light/dark) and send to a device /// public class ThemeMessenger : MessengerBase { private readonly ITheme _tpDevice; + /// + /// Create an instance of the class + /// + /// The key for this messenger + /// The path for this messenger + /// The device for this messenger public ThemeMessenger(string key, string path, ITheme device) : base(key, path, device as Device) { _tpDevice = device; } + /// protected override void RegisterActions() { AddAction("/fullStatus", (id, content) => { - PostStatusMessage(new ThemeUpdateMessage { Theme = _tpDevice.Theme }); + PostStatusMessage(new ThemeUpdateMessage { Theme = _tpDevice.Theme }, id); }); AddAction("/saveTheme", (id, content) => { var theme = content.ToObject>(); - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Setting theme to {theme}", this, theme.Value); + this.LogInformation("Setting theme to {theme}", theme.Value); _tpDevice.UpdateTheme(theme.Value); - PostStatusMessage(JToken.FromObject(new { theme = theme.Value })); + PostStatusMessage(JToken.FromObject(new { theme = theme.Value }), clientId: id); }); } } diff --git a/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs b/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs index 38f7944f..06595a9d 100644 --- a/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs +++ b/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs @@ -1,13 +1,9 @@ -using Newtonsoft.Json; +using System; +using Newtonsoft.Json; using Newtonsoft.Json.Converters; using PepperDash.Core; -using PepperDash.Core.Logging; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core.Queues; -using PepperDash.Essentials.WebSocketServer; -using Serilog.Events; -using System; -using System.Threading; using WebSocketSharp; namespace PepperDash.Essentials @@ -20,12 +16,22 @@ namespace PepperDash.Essentials private readonly WebSocket _ws; private readonly object msgToSend; + /// + /// Initialize a message to send + /// + /// message object to send + /// WebSocket instance public TransmitMessage(object msg, WebSocket ws) { _ws = ws; msgToSend = msg; } + /// + /// Initialize a message to send + /// + /// message object to send + /// WebSocket instance public TransmitMessage(DeviceStateMessageBase msg, WebSocket ws) { _ws = ws; @@ -43,13 +49,13 @@ namespace PepperDash.Essentials { if (_ws == null) { - Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Websocket client is null"); + Debug.LogWarning("Cannot send message. Websocket client is null"); return; } if (!_ws.IsAlive) { - Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Websocket client is not connected"); + Debug.LogWarning("Cannot send message. Websocket client is not connected"); return; } @@ -57,83 +63,14 @@ namespace PepperDash.Essentials var message = JsonConvert.SerializeObject(msgToSend, Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } }); - Debug.LogMessage(LogEventLevel.Verbose, "Message TX: {0}", null, message); + Debug.LogVerbose("Message TX: {0}", message); _ws.Send(message); - - } catch (Exception ex) { - Debug.LogMessage(ex, "Caught an exception in the Transmit Processor"); - } - } - #endregion - } - - - - /// - /// Represents a MessageToClients - /// - public class MessageToClients : IQueueMessage - { - private readonly MobileControlWebsocketServer _server; - private readonly object msgToSend; - - public MessageToClients(object msg, MobileControlWebsocketServer server) - { - _server = server; - msgToSend = msg; - } - - public MessageToClients(DeviceStateMessageBase msg, MobileControlWebsocketServer server) - { - _server = server; - msgToSend = msg; - } - - #region Implementation of IQueueMessage - - /// - /// Dispatch method - /// - public void Dispatch() - { - try - { - if (_server == null) - { - Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Server is null"); - return; - } - - var message = JsonConvert.SerializeObject(msgToSend, Formatting.None, - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } }); - - var clientSpecificMessage = msgToSend as MobileControlMessage; - if (clientSpecificMessage.ClientId != null) - { - var clientId = clientSpecificMessage.ClientId; - - _server.LogVerbose("Message TX To client {clientId} Message: {message}", clientId, message); - - _server.SendMessageToClient(clientId, message); - - return; - } - - _server.SendMessageToAllClients(message); - - _server.LogVerbose("Message TX To all clients: {message}", message); - } - catch (ThreadAbortException) - { - //Swallowing this exception, as it occurs on shutdown and there's no need to print out a scary stack trace - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Caught an exception in the Transmit Processor"); + Debug.LogError("Caught an exception in the Transmit Processor: {message}", ex.Message); + Debug.LogDebug(ex, "Stack Trace: "); } } #endregion diff --git a/src/PepperDash.Essentials.MobileControl/UserCodeChanged.cs b/src/PepperDash.Essentials.MobileControl/UserCodeChanged.cs index ab899167..6c510d14 100644 --- a/src/PepperDash.Essentials.MobileControl/UserCodeChanged.cs +++ b/src/PepperDash.Essentials.MobileControl/UserCodeChanged.cs @@ -3,12 +3,19 @@ using System; namespace PepperDash.Essentials { /// - /// Represents a UserCodeChanged + /// Defines the action to take when the User code changes /// public class UserCodeChanged { + /// + /// Action to take when the User Code changes + /// public Action UpdateUserCode { get; private set; } + /// + /// create an instance of the class + /// + /// action to take when the User Code changes public UserCodeChanged(Action updateMethod) { UpdateUserCode = updateMethod; diff --git a/src/PepperDash.Essentials.MobileControl/Utilities.cs b/src/PepperDash.Essentials.MobileControl/Utilities.cs new file mode 100644 index 00000000..8c2abf3e --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/Utilities.cs @@ -0,0 +1,97 @@ +using PepperDash.Core; +using PepperDash.Core.Logging; +using WebSocketSharp; + +namespace PepperDash.Essentials +{ + /// + /// Utility functions for logging and other common tasks. + /// + public static class Utilities + { + private static int nextClientId = 0; + + /// + /// Get the next unique client ID + /// + /// Client ID + public static int GetNextClientId() + { + nextClientId++; + return nextClientId; + } + /// + /// Converts a WebSocketServer LogData object to Essentials logging calls. + /// + /// The LogData object to convert. + /// The log message. + /// The device associated with the log message. + public static void ConvertWebsocketLog(LogData data, string message, IKeyed device = null) + { + + switch (data.Level) + { + case LogLevel.Trace: + if (device == null) + { + Debug.LogVerbose(message); + } + else + { + device.LogVerbose(message); + } + break; + case LogLevel.Debug: + if (device == null) + { + Debug.LogDebug(message); + } + else + { + device.LogDebug(message); + } + break; + case LogLevel.Info: + if (device == null) + { + Debug.LogInformation(message); + } + else + { + device.LogInformation(message); + } + break; + case LogLevel.Warn: + if (device == null) + { + Debug.LogWarning(message); + } + else + { + device.LogWarning(message); + } + break; + case LogLevel.Error: + if (device == null) + { + Debug.LogError(message); + } + else + { + device.LogError(message); + } + break; + case LogLevel.Fatal: + if (device == null) + { + Debug.LogFatal(message); + } + else + { + device.LogFatal(message); + } + break; + } + } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/Volumes.cs b/src/PepperDash.Essentials.MobileControl/Volumes.cs index 84accd26..44febbfc 100644 --- a/src/PepperDash.Essentials.MobileControl/Volumes.cs +++ b/src/PepperDash.Essentials.MobileControl/Volumes.cs @@ -15,15 +15,17 @@ namespace PepperDash.Essentials [JsonProperty("master", NullValueHandling = NullValueHandling.Ignore)] public Volume Master { get; set; } + /// + /// Aux Faders as configured in the room + /// [JsonProperty("auxFaders", NullValueHandling = NullValueHandling.Ignore)] public Dictionary AuxFaders { get; set; } + /// + /// Count of aux faders for this system + /// [JsonProperty("numberOfAuxFaders", NullValueHandling = NullValueHandling.Ignore)] public int? NumberOfAuxFaders { get; set; } - - public Volumes() - { - } } /// @@ -31,16 +33,21 @@ namespace PepperDash.Essentials /// public class Volume { - /// /// Gets or sets the Key /// [JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)] public string Key { get; set; } + /// + /// Level for this volume object + /// [JsonProperty("level", NullValueHandling = NullValueHandling.Ignore)] public int? Level { get; set; } + /// + /// True if this volume control is muted + /// [JsonProperty("muted", NullValueHandling = NullValueHandling.Ignore)] public bool? Muted { get; set; } @@ -51,12 +58,21 @@ namespace PepperDash.Essentials [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] public string Label { get; set; } + /// + /// True if this volume object has mute control + /// [JsonProperty("hasMute", NullValueHandling = NullValueHandling.Ignore)] public bool? HasMute { get; set; } + /// + /// True if this volume object has Privacy mute control + /// [JsonProperty("hasPrivacyMute", NullValueHandling = NullValueHandling.Ignore)] public bool? HasPrivacyMute { get; set; } + /// + /// True if the privacy mute is muted + /// [JsonProperty("privacyMuted", NullValueHandling = NullValueHandling.Ignore)] public bool? PrivacyMuted { get; set; } @@ -68,6 +84,15 @@ namespace PepperDash.Essentials [JsonProperty("muteIcon", NullValueHandling = NullValueHandling.Ignore)] public string MuteIcon { get; set; } + /// + /// Create an instance of the class + /// + /// The key for this volume object + /// The level for this volume object + /// True if this volume control is muted + /// The label for this volume object + /// True if this volume object has mute control + /// The mute icon for this volume object public Volume(string key, int level, bool muted, string label, bool hasMute, string muteIcon) : this(key) { @@ -78,18 +103,32 @@ namespace PepperDash.Essentials MuteIcon = muteIcon; } + /// + /// Create an instance of the class + /// + /// The key for this volume object + /// The level for this volume object public Volume(string key, int level) : this(key) { Level = level; } + /// + /// Create an instance of the class + /// + /// The key for this volume object + /// True if this volume control is muted public Volume(string key, bool muted) : this(key) { Muted = muted; } + /// + /// Create an instance of the class + /// + /// The key for this volume object public Volume(string key) { Key = key; diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/ActionPathsHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/ActionPathsHandler.cs index 84d0318a..1c204e8e 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/ActionPathsHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/ActionPathsHandler.cs @@ -12,11 +12,20 @@ namespace PepperDash.Essentials.WebApiHandlers public class ActionPathsHandler : WebApiBaseRequestHandler { private readonly MobileControlSystemController mcController; + + /// + /// Create an instance of the class. + /// + /// public ActionPathsHandler(MobileControlSystemController controller) : base(true) { mcController = controller; } + /// + /// Handle a request to get the action paths + /// + /// Request Context protected override void HandleGet(HttpCwsContext context) { var response = JsonConvert.SerializeObject(new ActionPathsResponse(mcController)); @@ -37,9 +46,16 @@ namespace PepperDash.Essentials.WebApiHandlers [JsonIgnore] private readonly MobileControlSystemController mcController; + /// + /// Registered action paths for this system + /// [JsonProperty("actionPaths")] public List ActionPaths => mcController.GetActionDictionaryPaths().Select((path) => new ActionPath { MessengerKey = path.Item1, Path = path.Item2 }).ToList(); + /// + /// Create an instance of the class. + /// + /// public ActionPathsResponse(MobileControlSystemController mcController) { this.mcController = mcController; diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileAuthRequestHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileAuthRequestHandler.cs index 352c56e5..5d9e9766 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileAuthRequestHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileAuthRequestHandler.cs @@ -1,10 +1,10 @@ -using Crestron.SimplSharp.WebScripting; +using System; +using System.Threading.Tasks; +using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; using PepperDash.Core; using PepperDash.Core.Web.RequestHandlers; using PepperDash.Essentials.Core.Web; -using System; -using System.Threading.Tasks; namespace PepperDash.Essentials.WebApiHandlers { @@ -15,11 +15,20 @@ namespace PepperDash.Essentials.WebApiHandlers { private readonly MobileControlSystemController mcController; + /// + /// Create an instance of the class. + /// + /// public MobileAuthRequestHandler(MobileControlSystemController controller) : base(true) { mcController = controller; } + /// + /// Handle authorization request for this processor + /// + /// request context + /// Task protected override async Task HandlePost(HttpCwsContext context) { try diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs index d123dbcd..676fd3c8 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs @@ -1,26 +1,35 @@ -using Crestron.SimplSharp.WebScripting; +using System; +using System.Collections.Generic; +using System.Linq; +using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; using PepperDash.Core; using PepperDash.Core.Web.RequestHandlers; using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.WebSocketServer; -using System; -using System.Collections.Generic; -using System.Linq; namespace PepperDash.Essentials.WebApiHandlers { /// - /// Represents a MobileInfoHandler + /// Represents a MobileInfoHandler. Used with the Essentials CWS API /// public class MobileInfoHandler : WebApiBaseRequestHandler { private readonly MobileControlSystemController mcController; + + /// + /// Create an instance of the class. + /// + /// public MobileInfoHandler(MobileControlSystemController controller) : base(true) { mcController = controller; } + /// + /// Get Mobile Control Information + /// + /// protected override void HandleGet(HttpCwsContext context) { try @@ -50,14 +59,22 @@ namespace PepperDash.Essentials.WebApiHandlers [JsonIgnore] private readonly MobileControlSystemController mcController; + /// + /// Edge Server. Null if edge server is disabled + /// [JsonProperty("edgeServer", NullValueHandling = NullValueHandling.Ignore)] public MobileControlEdgeServer EdgeServer => mcController.Config.EnableApiServer ? new MobileControlEdgeServer(mcController) : null; - + /// + /// Direct server. Null if the direct server is disabled + /// [JsonProperty("directServer", NullValueHandling = NullValueHandling.Ignore)] public MobileControlDirectServer DirectServer => mcController.Config.DirectServer.EnableDirectServer ? new MobileControlDirectServer(mcController.DirectServer) : null; - + /// + /// Create an instance of the class. + /// + /// public InformationResponse(MobileControlSystemController controller) { mcController = controller; @@ -72,24 +89,46 @@ namespace PepperDash.Essentials.WebApiHandlers [JsonIgnore] private readonly MobileControlSystemController mcController; + /// + /// Mobile Control Edge Server address for this system + /// [JsonProperty("serverAddress")] public string ServerAddress => mcController.Config == null ? "No Config" : mcController.Host; + /// + /// System Name for this system + /// [JsonProperty("systemName")] public string SystemName => mcController.RoomBridges.Count > 0 ? mcController.RoomBridges[0].RoomName : "No Config"; + /// + /// System URL for this system + /// [JsonProperty("systemUrl")] public string SystemUrl => ConfigReader.ConfigObject.SystemUrl; + /// + /// User code to use in MC UI for this system + /// [JsonProperty("userCode")] public string UserCode => mcController.RoomBridges.Count > 0 ? mcController.RoomBridges[0].UserCode : "Not available"; + /// + /// True if connected to edge server + /// [JsonProperty("connected")] public bool Connected => mcController.Connected; + /// + /// Seconds since last comms with edge server + /// [JsonProperty("secondsSinceLastAck")] public int SecondsSinceLastAck => (DateTime.Now - mcController.LastAckMessage).Seconds; + /// + /// Create an instance of the class. + /// + /// controller to use for this public MobileControlEdgeServer(MobileControlSystemController controller) { mcController = controller; @@ -104,21 +143,43 @@ namespace PepperDash.Essentials.WebApiHandlers [JsonIgnore] private readonly MobileControlWebsocketServer directServer; + /// + /// URL to use to interact with this server + /// [JsonProperty("userAppUrl")] public string UserAppUrl => $"{directServer.UserAppUrlPrefix}/[insert_client_token]"; + /// + /// TCP/IP Port this server is configured to use + /// [JsonProperty("serverPort")] public int ServerPort => directServer.Port; + /// + /// Count of defined tokens for this server + /// [JsonProperty("tokensDefined")] - public int TokensDefined => directServer.UiClients.Count; + public int TokensDefined => directServer.UiClientContexts.Count; + /// + /// Count of connected clients + /// [JsonProperty("clientsConnected")] public int ClientsConnected => directServer.ConnectedUiClientsCount; + /// + /// List of tokens and connected clients for this server + /// [JsonProperty("clients")] - public List Clients => directServer.UiClients.Select((c, i) => { return new MobileControlDirectClient(c, i, directServer.UserAppUrlPrefix); }).ToList(); + public List Clients => directServer.UiClientContexts + .Select(context => (context, clients: directServer.UiClients.Where(client => client.Value.Token == context.Value.Token.Token).Select(c => c.Value).ToList())) + .Select((clientTuple, i) => new MobileControlDirectClient(clientTuple.clients, clientTuple.context, i, directServer.UserAppUrlPrefix)) + .ToList(); + /// + /// Create an instance of the class. + /// + /// public MobileControlDirectServer(MobileControlWebsocketServer server) { directServer = server; @@ -142,33 +203,85 @@ namespace PepperDash.Essentials.WebApiHandlers [JsonIgnore] private readonly string urlPrefix; + /// + /// Client number for this client + /// [JsonProperty("clientNumber")] public string ClientNumber => $"{clientNumber}"; + /// + /// Room Key for this client + /// [JsonProperty("roomKey")] public string RoomKey => context.Token.RoomKey; + /// + /// Touchpanel Key, if defined, for this client + /// [JsonProperty("touchpanelKey")] public string TouchpanelKey => context.Token.TouchpanelKey; + /// + /// URL for this client + /// [JsonProperty("url")] public string Url => $"{urlPrefix}{Key}"; + /// + /// Token for this client + /// [JsonProperty("token")] public string Token => Key; - [JsonProperty("connected")] - public bool Connected => context.Client != null && context.Client.Context.WebSocket.IsAlive; + private readonly List clients; - [JsonProperty("duration")] - public double Duration => context.Client == null ? 0 : context.Client.ConnectedDuration.TotalSeconds; + /// + /// List of status for all connected UI Clients + /// + [JsonProperty("clientStatus")] + public List ClientStatus => clients.Select(c => new ClientStatus(c)).ToList(); - public MobileControlDirectClient(KeyValuePair clientContext, int index, string urlPrefix) + /// + /// Create an instance of the class. + /// + /// List of Websocket Clients + /// Context for the client + /// Index of the client + /// URL prefix for the client + public MobileControlDirectClient(List clients, KeyValuePair context, int index, string urlPrefix) { - context = clientContext.Value; - Key = clientContext.Key; + this.context = context.Value; + Key = context.Key; clientNumber = index; this.urlPrefix = urlPrefix; + this.clients = clients; + } + } + + /// + /// Report the status of a UiClient + /// + public class ClientStatus + { + private readonly UiClient client; + + /// + /// True if client is connected + /// + public bool Connected => client != null && client.Context.WebSocket.IsAlive; + + /// + /// Get the time this client has been connected + /// + public double Duration => client == null ? 0 : client.ConnectedDuration.TotalSeconds; + + /// + /// Create an instance of the class for the specified client + /// + /// client to report on + public ClientStatus(UiClient client) + { + this.client = client; } } } diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs index e45fcc39..2f972ddf 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs @@ -14,11 +14,20 @@ namespace PepperDash.Essentials.WebApiHandlers public class UiClientHandler : WebApiBaseRequestHandler { private readonly MobileControlWebsocketServer server; + + /// + /// Essentials CWS API handler for the MC Direct Server + /// + /// Direct Server instance public UiClientHandler(MobileControlWebsocketServer directServer) : base(true) { server = directServer; } + /// + /// Create a client for the Direct Server + /// + /// HTTP Context for this request protected override void HandlePost(HttpCwsContext context) { var req = context.Request; @@ -65,6 +74,10 @@ namespace PepperDash.Essentials.WebApiHandlers res.End(); } + /// + /// Handle DELETE request for a Client + /// + /// protected override void HandleDelete(HttpCwsContext context) { var req = context.Request; @@ -93,7 +106,7 @@ namespace PepperDash.Essentials.WebApiHandlers - if (!server.UiClients.TryGetValue(request.Token, out UiClientContext clientContext)) + if (!server.UiClientContexts.TryGetValue(request.Token, out UiClientContext clientContext)) { var response = new ClientResponse { @@ -134,7 +147,7 @@ namespace PepperDash.Essentials.WebApiHandlers return; } - server.UiClients.Remove(request.Token); + server.UiClientContexts.Remove(request.Token); server.UpdateSecret(); diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/ConnectionClosedEventArgs.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/ConnectionClosedEventArgs.cs new file mode 100644 index 00000000..617cc6d6 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/ConnectionClosedEventArgs.cs @@ -0,0 +1,24 @@ +using System; + +namespace PepperDash.Essentials.WebSocketServer +{ + /// + /// Event Args for ConnectionClosed event + /// + public class ConnectionClosedEventArgs : EventArgs + { + /// + /// Client ID that is being closed + /// + public string ClientId { get; private set; } + + /// + /// Initialize an instance of the class. + /// + /// client that's closing + public ConnectionClosedEventArgs(string clientId) + { + ClientId = clientId; + } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinResponse.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinResponse.cs index ba214db4..1529d0fe 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinResponse.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinResponse.cs @@ -17,9 +17,15 @@ namespace PepperDash.Essentials.WebSocketServer [JsonProperty("clientId")] public string ClientId { get; set; } + /// + /// Room Key for this client + /// [JsonProperty("roomKey")] public string RoomKey { get; set; } + /// + /// System UUID for this system + /// [JsonProperty("systemUUid")] public string SystemUuid { get; set; } diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs index b3ea3c7c..63fdcdd4 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs @@ -5,15 +5,28 @@ namespace PepperDash.Essentials.WebSocketServer /// public class JoinToken { + /// + /// Unique client ID for a client that is joining + /// + public string Id { get; set; } /// /// Gets or sets the Code /// public string Code { get; set; } + /// + /// Room Key this token is associated with + /// public string RoomKey { get; set; } + /// + /// Unique ID for this token + /// public string Uuid { get; set; } + /// + /// Touchpanel Key this token is associated with, if this is a touch panel token + /// public string TouchpanelKey { get; set; } = ""; /// diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index d15185b0..0f2fa388 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -10,6 +10,7 @@ using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; +using Org.BouncyCastle.Crypto.Prng; using PepperDash.Core; using PepperDash.Core.Logging; using PepperDash.Essentials.Core; @@ -56,7 +57,14 @@ namespace PepperDash.Essentials.WebSocketServer /// /// Gets the collection of UI client contexts /// - public Dictionary UiClients { get; private set; } + public Dictionary UiClientContexts { get; private set; } + + private readonly Dictionary uiClients = new Dictionary(); + + /// + /// Gets the collection of UI clients + /// + public ReadOnlyDictionary UiClients => new ReadOnlyDictionary(uiClients); private readonly MobileControlSystemController _parent; @@ -127,17 +135,7 @@ namespace PepperDash.Essentials.WebSocketServer { get { - var count = 0; - - foreach (var client in UiClients) - { - if (client.Value.Client != null && client.Value.Client.Context.WebSocket.IsAlive) - { - count++; - } - } - - return count; + return uiClients.Values.Where(c => c.Context.WebSocket.IsAlive).Count(); } } @@ -202,7 +200,7 @@ namespace PepperDash.Essentials.WebSocketServer } - UiClients = new Dictionary(); + UiClientContexts = new Dictionary(); //_joinTokens = new Dictionary(); @@ -277,30 +275,10 @@ namespace PepperDash.Essentials.WebSocketServer }; } - _server.Log.Output = (data, message) => - { - switch (data.Level) - { - case LogLevel.Trace: - this.LogVerbose(data.Message); - break; - case LogLevel.Debug: - this.LogDebug(data.Message); - break; - case LogLevel.Info: - this.LogInformation(data.Message); - break; - case LogLevel.Warn: - this.LogWarning(data.Message); - break; - case LogLevel.Error: - this.LogError(data.Message); - break; - case LogLevel.Fatal: - this.LogFatal(data.Message); - break; - } - }; + _server.Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this); + + // setting to trace to allow logging level to be controlled by appdebug + _server.Log.Level = LogLevel.Trace; CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; @@ -326,6 +304,9 @@ namespace PepperDash.Essentials.WebSocketServer } } + /// + /// Set the internal logging level for the Websocket Server + /// public void SetWebsocketLogLevel(LogLevel level) { CrestronConsole.ConsoleCommandResponse($"Setting direct server debug level to {level}", level.ToString()); @@ -404,9 +385,9 @@ namespace PepperDash.Essentials.WebSocketServer var appUrl = $"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}"; - this.LogVerbose("Sending URL {appUrl}", appUrl); + this.LogVerbose("Sending URL {appUrl} to touchpanel {touchpanelKey}", appUrl, touchpanel.Touchpanel.Key); - touchpanel.Messenger.UpdateAppUrl($"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}"); + touchpanel.Touchpanel.SetAppUrl($"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}"); } } @@ -554,20 +535,20 @@ namespace PepperDash.Essentials.WebSocketServer this.LogInformation("Adding token: {key} for room: {roomKey}", token.Key, token.Value.RoomKey); - if (UiClients == null) + if (UiClientContexts == null) { - UiClients = new Dictionary(); + UiClientContexts = new Dictionary(); } - UiClients.Add(token.Key, new UiClientContext(token.Value)); + UiClientContexts.Add(token.Key, new UiClientContext(token.Value)); } } - if (UiClients.Count > 0) + if (UiClientContexts.Count > 0) { - this.LogInformation("Restored {uiClientCount} UiClients from secrets data", UiClients.Count); + this.LogInformation("Restored {uiClientCount} UiClients from secrets data", UiClientContexts.Count); - foreach (var client in UiClients) + foreach (var client in UiClientContexts) { var key = client.Key; var path = _wsPath + key; @@ -575,13 +556,8 @@ namespace PepperDash.Essentials.WebSocketServer _server.AddWebSocketService(path, () => { - var c = new UiClient($"uiclient-{key}-{roomKey}"); - this.LogDebug("Constructing UiClient with id: {key}", key); - - c.Controller = _parent; - c.RoomKey = roomKey; - UiClients[key].SetClient(c); - return c; + this.LogInformation("Building a UiClient with ID {id}", client.Value.Token.Id); + return BuildUiClient(roomKey, client.Value.Token, key); }); } } @@ -591,7 +567,7 @@ namespace PepperDash.Essentials.WebSocketServer this.LogWarning("No secret found"); } - this.LogDebug("{uiClientCount} UiClients restored from secrets data", UiClients.Count); + this.LogDebug("{uiClientCount} UiClients restored from secrets data", UiClientContexts.Count); } catch (Exception ex) { @@ -616,7 +592,7 @@ namespace PepperDash.Essentials.WebSocketServer _secret.Tokens.Clear(); - foreach (var uiClientContext in UiClients) + foreach (var uiClientContext in UiClientContexts) { _secret.Tokens.Add(uiClientContext.Key, uiClientContext.Value.Token); } @@ -725,21 +701,17 @@ namespace PepperDash.Essentials.WebSocketServer var token = new JoinToken { Code = bridge.UserCode, RoomKey = bridge.RoomKey, Uuid = _parent.SystemUuid, TouchpanelKey = touchPanelKey }; - UiClients.Add(key, new UiClientContext(token)); + UiClientContexts.Add(key, new UiClientContext(token)); var path = _wsPath + key; _server.AddWebSocketService(path, () => { - var c = new UiClient($"uiclient-{key}-{bridge.RoomKey}"); - this.LogVerbose("Constructing UiClient with id: {key}", key); - c.Controller = _parent; - c.RoomKey = bridge.RoomKey; - UiClients[key].SetClient(c); - return c; + this.LogInformation("Building a UiClient with ID {id}", token.Id); + return BuildUiClient(bridge.RoomKey, token, key); }); - this.LogInformation("Added new WebSocket UiClient service at path: {path}", path); + this.LogInformation("Added new WebSocket UiClient for path: {path}", path); this.LogInformation("Token: {@token}", token); this.LogVerbose("{serviceCount} websocket services present", _server.WebSocketServices.Count); @@ -749,6 +721,44 @@ namespace PepperDash.Essentials.WebSocketServer return (key, path); } + private UiClient BuildUiClient(string roomKey, JoinToken token, string key) + { + var c = new UiClient($"uiclient-{key}-{roomKey}-{token.Id}", token.Id, token.Token, token.TouchpanelKey); + this.LogInformation("Constructing UiClient with key {key} and ID {id}", key, token.Id); + c.Controller = _parent; + c.RoomKey = roomKey; + + if (uiClients.ContainsKey(token.Id)) + { + this.LogWarning("removing client with duplicate id {id}", token.Id); + uiClients.Remove(token.Id); + } + uiClients.Add(token.Id, c); + // UiClients[key].SetClient(c); + c.ConnectionClosed += (o, a) => uiClients.Remove(a.ClientId); + token.Id = null; + return c; + } + + /// + /// Prints out the session data for each path + /// + public void PrintSessionData() + { + foreach (var path in _server.WebSocketServices.Paths) + { + this.LogInformation("Path: {path}", path); + this.LogInformation(" Session Count: {sessionCount}", _server.WebSocketServices[path].Sessions.Count); + this.LogInformation(" Active Session Count: {activeSessionCount}", _server.WebSocketServices[path].Sessions.ActiveIDs.Count()); + this.LogInformation(" Inactive Session Count: {inactiveSessionCount}", _server.WebSocketServices[path].Sessions.InactiveIDs.Count()); + this.LogInformation(" Active Clients:"); + foreach (var session in _server.WebSocketServices[path].Sessions.IDs) + { + this.LogInformation(" Client ID: {id}", (_server.WebSocketServices[path].Sessions[session] as UiClient)?.Id); + } + } + } + /// /// Removes all clients from the server /// @@ -766,7 +776,7 @@ namespace PepperDash.Essentials.WebSocketServer return; } - foreach (var client in UiClients) + foreach (var client in UiClientContexts) { if (client.Value.Client != null && client.Value.Client.Context.WebSocket.IsAlive) { @@ -784,7 +794,7 @@ namespace PepperDash.Essentials.WebSocketServer } } - UiClients.Clear(); + UiClientContexts.Clear(); UpdateSecret(); } @@ -803,9 +813,9 @@ namespace PepperDash.Essentials.WebSocketServer var key = s; - if (UiClients.ContainsKey(key)) + if (UiClientContexts.ContainsKey(key)) { - var uiClientContext = UiClients[key]; + var uiClientContext = UiClientContexts[key]; if (uiClientContext.Client != null && uiClientContext.Client.Context.WebSocket.IsAlive) { @@ -815,7 +825,7 @@ namespace PepperDash.Essentials.WebSocketServer var path = _wsPath + key; if (_server.RemoveWebSocketService(path)) { - UiClients.Remove(key); + UiClientContexts.Remove(key); UpdateSecret(); @@ -839,9 +849,9 @@ namespace PepperDash.Essentials.WebSocketServer { CrestronConsole.ConsoleCommandResponse("Mobile Control UI Client Info:\r"); - CrestronConsole.ConsoleCommandResponse(string.Format("{0} clients found:\r", UiClients.Count)); + CrestronConsole.ConsoleCommandResponse(string.Format("{0} clients found:\r", UiClientContexts.Count)); - foreach (var client in UiClients) + foreach (var client in UiClientContexts) { CrestronConsole.ConsoleCommandResponse(string.Format("RoomKey: {0} Token: {1}\r", client.Value.Token.RoomKey, client.Key)); } @@ -853,9 +863,9 @@ namespace PepperDash.Essentials.WebSocketServer { foreach (var client in UiClients.Values) { - if (client.Client != null && client.Client.Context.WebSocket.IsAlive) + if (client != null && client.Context.WebSocket.IsAlive) { - client.Client.Context.WebSocket.Close(CloseStatusCode.Normal, "Server Shutting Down"); + client.Context.WebSocket.Close(CloseStatusCode.Normal, "Server Shutting Down"); } } @@ -990,77 +1000,81 @@ namespace PepperDash.Essentials.WebSocketServer this.LogVerbose("Join Room Request with token: {token}", token); + byte[] body; - if (UiClients.TryGetValue(token, out UiClientContext clientContext)) - { - var bridge = _parent.GetRoomBridge(clientContext.Token.RoomKey); - - if (bridge != null) - { - res.StatusCode = 200; - res.ContentType = "application/json"; - - var devices = DeviceManager.GetDevices(); - Dictionary deviceInterfaces = new Dictionary(); - - foreach (var device in devices) - { - var interfaces = device?.GetType().GetInterfaces().Select((i) => i.Name).ToList() ?? new List(); - deviceInterfaces.Add(device.Key, new DeviceInterfaceInfo - { - Key = device.Key, - Name = device is IKeyName ? (device as IKeyName).Name : "", - Interfaces = interfaces - }); - } - - // Construct the response object - JoinResponse jRes = new JoinResponse - { - ClientId = token, - RoomKey = bridge.RoomKey, - SystemUuid = _parent.SystemUuid, - RoomUuid = _parent.SystemUuid, - Config = _parent.GetConfigWithPluginVersion(), - CodeExpires = new DateTime().AddYears(1), - UserCode = bridge.UserCode, - UserAppUrl = string.Format("http://{0}:{1}/mc/app", - CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0), - Port), - EnableDebug = false, - DeviceInterfaceSupport = deviceInterfaces - }; - - // Serialize to JSON and convert to Byte[] - var json = JsonConvert.SerializeObject(jRes); - var body = Encoding.UTF8.GetBytes(json); - res.ContentLength64 = body.LongLength; - - // Send the response - res.Close(body, true); - } - else - { - var message = string.Format("Unable to find bridge with key: {0}", clientContext.Token.RoomKey); - res.StatusCode = 404; - res.ContentType = "application/json"; - this.LogVerbose("{message}", message); - var body = Encoding.UTF8.GetBytes(message); - res.ContentLength64 = body.LongLength; - res.Close(body, true); - - } - } - else + if (!UiClientContexts.TryGetValue(token, out UiClientContext clientContext)) { var message = "Token invalid or has expired"; res.StatusCode = 401; res.ContentType = "application/json"; this.LogVerbose("{message}", message); - var body = Encoding.UTF8.GetBytes(message); + body = Encoding.UTF8.GetBytes(message); res.ContentLength64 = body.LongLength; res.Close(body, true); + return; } + + var bridge = _parent.GetRoomBridge(clientContext.Token.RoomKey); + + if (bridge == null) + { + var message = string.Format("Unable to find bridge with key: {0}", clientContext.Token.RoomKey); + res.StatusCode = 404; + res.ContentType = "application/json"; + this.LogVerbose("{message}", message); + body = Encoding.UTF8.GetBytes(message); + res.ContentLength64 = body.LongLength; + res.Close(body, true); + return; + } + + res.StatusCode = 200; + res.ContentType = "application/json"; + + var devices = DeviceManager.GetDevices(); + Dictionary deviceInterfaces = new Dictionary(); + + foreach (var device in devices) + { + var interfaces = device?.GetType().GetInterfaces().Select((i) => i.Name).ToList() ?? new List(); + + deviceInterfaces.Add(device.Key, new DeviceInterfaceInfo + { + Key = device.Key, + Name = (device as IKeyName)?.Name ?? "", + Interfaces = interfaces + }); + } + + var clientId = $"{Utilities.GetNextClientId()}"; + clientContext.Token.Id = clientId; + + this.LogVerbose("Assigning ClientId: {clientId}", clientId); + + // Construct the response object + JoinResponse jRes = new JoinResponse + { + ClientId = clientId, + RoomKey = bridge.RoomKey, + SystemUuid = _parent.SystemUuid, + RoomUuid = _parent.SystemUuid, + Config = _parent.GetConfigWithPluginVersion(), + CodeExpires = new DateTime().AddYears(1), + UserCode = bridge.UserCode, + UserAppUrl = string.Format("http://{0}:{1}/mc/app", + CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0), + Port), + EnableDebug = false, + DeviceInterfaceSupport = deviceInterfaces + }; + + // Serialize to JSON and convert to Byte[] + var json = JsonConvert.SerializeObject(jRes); + body = Encoding.UTF8.GetBytes(json); + res.ContentLength64 = body.LongLength; + + // Send the response + res.Close(body, true); } /// @@ -1242,12 +1256,14 @@ namespace PepperDash.Essentials.WebSocketServer /// public void SendMessageToAllClients(string message) { - foreach (var clientContext in UiClients.Values) + foreach (var client in uiClients.Values) { - if (clientContext.Client != null && clientContext.Client.Context.WebSocket.IsAlive) + if (!client.Context.WebSocket.IsAlive) { - clientContext.Client.Context.WebSocket.Send(message); + continue; } + + client.Context.WebSocket.Send(message); } } @@ -1266,17 +1282,16 @@ namespace PepperDash.Essentials.WebSocketServer return; } - if (UiClients.TryGetValue((string)clientId, out UiClientContext clientContext)) + if (uiClients.TryGetValue((string)clientId, out var client)) { - if (clientContext.Client != null) - { - var socket = clientContext.Client.Context.WebSocket; + var socket = client.Context.WebSocket; - if (socket.IsAlive) - { - socket.Send(message); - } + if (!socket.IsAlive) + { + this.LogError("Unable to send message to client {id}. Client is disconnected: {message}", clientId, message); + return; } + socket.Send(message); } else { diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/ServerTokenSecrets.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/ServerTokenSecrets.cs index 3fa2fb0c..ad9a1d66 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/ServerTokenSecrets.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/ServerTokenSecrets.cs @@ -13,8 +13,15 @@ namespace PepperDash.Essentials.WebSocketServer /// public string GrantCode { get; set; } + /// + /// Gets or sets the Tokens for this server + /// public Dictionary Tokens { get; set; } + /// + /// Initialize a new instance of the class with the provided grant code + /// + /// The grant code for this server public ServerTokenSecrets(string grantCode) { GrantCode = grantCode; diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs index 3cdd183b..cf8dd7f9 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs @@ -1,5 +1,4 @@ using System; -using System.Text.RegularExpressions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Core; @@ -22,6 +21,21 @@ namespace PepperDash.Essentials.WebSocketServer /// public string Key { get; private set; } + /// + /// Client ID used by client for this connection + /// + public string Id { get; private set; } + + /// + /// Token associated with this client + /// + public string Token { get; private set; } + + /// + /// Touchpanel Key associated with this client + /// + public string TouchpanelKey { get; private set; } + /// /// Gets or sets the mobile control system controller that handles this client's messages /// @@ -32,11 +46,6 @@ namespace PepperDash.Essentials.WebSocketServer /// public string RoomKey { get; set; } - /// - /// The unique identifier for this client instance - /// - private string _clientId; - /// /// The timestamp when this client connection was established /// @@ -60,13 +69,24 @@ namespace PepperDash.Essentials.WebSocketServer } } + /// + /// Triggered when this client closes it's connection + /// + public event EventHandler ConnectionClosed; + /// /// Initializes a new instance of the UiClient class with the specified key /// /// The unique key to identify this client - public UiClient(string key) + /// The client ID used by the client for this connection + /// The token associated with this client + /// The touchpanel key associated with this client + public UiClient(string key, string id, string token, string touchpanelKey = "") { Key = key; + Id = id; + Token = token; + TouchpanelKey = touchpanelKey; } /// @@ -74,19 +94,10 @@ namespace PepperDash.Essentials.WebSocketServer { base.OnOpen(); - var url = Context.WebSocket.Url; - this.LogInformation("New WebSocket Connection from: {url}", url); + _connectionTime = DateTime.Now; - var match = Regex.Match(url.AbsoluteUri, "(?:ws|wss):\\/\\/.*(?:\\/mc\\/api\\/ui\\/join\\/)(.*)"); - - if (!match.Success) - { - _connectionTime = DateTime.Now; - return; - } - - var clientId = match.Groups[1].Value; - _clientId = clientId; + Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this); + Log.Level = LogLevel.Trace; if (Controller == null) { @@ -99,8 +110,9 @@ namespace PepperDash.Essentials.WebSocketServer Type = "/system/clientJoined", Content = JToken.FromObject(new { - clientId, + clientId = Id, roomKey = RoomKey, + touchpanelKey = TouchpanelKey ?? string.Empty, }) }; @@ -110,7 +122,7 @@ namespace PepperDash.Essentials.WebSocketServer if (bridge == null) return; - SendUserCodeToClient(bridge, clientId); + SendUserCodeToClient(bridge, Id); bridge.UserCodeChanged -= Bridge_UserCodeChanged; bridge.UserCodeChanged += Bridge_UserCodeChanged; @@ -125,7 +137,7 @@ namespace PepperDash.Essentials.WebSocketServer /// Event arguments private void Bridge_UserCodeChanged(object sender, EventArgs e) { - SendUserCodeToClient((MobileControlEssentialsRoomBridge)sender, _clientId); + SendUserCodeToClient((MobileControlEssentialsRoomBridge)sender, Id); } /// @@ -172,13 +184,15 @@ namespace PepperDash.Essentials.WebSocketServer foreach (var messenger in Controller.Messengers) { - messenger.Value.UnsubscribeClient(_clientId); + messenger.Value.UnsubscribeClient(Id); } foreach (var messenger in Controller.DefaultMessengers) { - messenger.Value.UnsubscribeClient(_clientId); + messenger.Value.UnsubscribeClient(Id); } + + ConnectionClosed?.Invoke(this, new ConnectionClosedEventArgs(Id)); } /// diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClientContext.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClientContext.cs index 6782306f..ffc09e90 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClientContext.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClientContext.cs @@ -14,6 +14,10 @@ namespace PepperDash.Essentials.WebSocketServer /// public JoinToken Token { get; private set; } + /// + /// Initialize an instance of the class with the provided token + /// + /// token for this client public UiClientContext(JoinToken token) { Token = token; diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/Version.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/Version.cs index 5552e29b..9380e54d 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/Version.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/Version.cs @@ -8,12 +8,25 @@ namespace PepperDash.Essentials.WebSocketServer /// public class Version { + /// + /// Server version this Websocket is connected to + /// [JsonProperty("serverVersion")] public string ServerVersion { get; set; } + /// + /// True if the server is on a processor + /// + [JsonProperty("serverIsRunningOnProcessorHardware")] public bool ServerIsRunningOnProcessorHardware { get; private set; } + /// + /// Initialize an instance of the class + /// + /// + /// The property is set to true by default. + /// public Version() { ServerIsRunningOnProcessorHardware = true; diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/WebSocketServerSecretProvider.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/WebSocketServerSecretProvider.cs index cf125d29..3dd67bd8 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/WebSocketServerSecretProvider.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/WebSocketServerSecretProvider.cs @@ -13,25 +13,28 @@ namespace PepperDash.Essentials.WebSocketServer } /// - /// Represents a WebSocketServerSecret + /// Stores a secret value using the provided secret store provider /// public class WebSocketServerSecret : ISecret { /// - /// Gets or sets the Provider + /// Gets the Secret Provider associated with this secret /// public ISecretProvider Provider { get; private set; } /// - /// Gets or sets the Key + /// Gets the Key associated with this secret /// public string Key { get; private set; } /// - /// Gets or sets the Value + /// Gets the Value associated with this secret /// public object Value { get; private set; } + /// + /// Initialize and instance of the class + /// public WebSocketServerSecret(string key, object value, ISecretProvider provider) { Key = key; diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index 28864e83..72d7c27d 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -104,7 +104,7 @@ namespace PepperDash.Essentials CrestronConsole.ConsoleCommandResponse ("Current running configuration. This is the merged system and template configuration" + CrestronEnvironment.NewLine); CrestronConsole.ConsoleCommandResponse(Newtonsoft.Json.JsonConvert.SerializeObject - (ConfigReader.ConfigObject, Newtonsoft.Json.Formatting.Indented)); + (ConfigReader.ConfigObject, Newtonsoft.Json.Formatting.Indented).Replace(Environment.NewLine, "\r\n")); }, "showconfig", "Shows the current running merged config", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(s =>