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..01bf3579 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,9 +1309,6 @@ namespace PepperDash.Essentials messenger.RegisterWithAppServer(this); } - /// - /// Initialize method - /// /// public override void Initialize() { @@ -1338,7 +1353,7 @@ namespace PepperDash.Essentials #region IMobileControl Members /// - /// GetAppServer method + /// Gets the App Server instance /// public static IMobileControl GetAppServer() { @@ -1356,16 +1371,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 +1391,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 +1411,7 @@ namespace PepperDash.Essentials } /// - /// LinkSystemMonitorToAppServer method + /// Link the System Monitor to this App server /// public void LinkSystemMonitorToAppServer() { @@ -1449,14 +1438,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 +1475,6 @@ namespace PepperDash.Essentials } } - /// - /// Sends message to server to indicate the system is shutting down - /// - /// private void CrestronEnvironment_ProgramStatusEventHandler( eProgramStatusEventType programEventType ) @@ -1530,6 +1507,9 @@ namespace PepperDash.Essentials } } + /// + /// Get action paths for the current actions + /// public List<(string, string)> GetActionDictionaryPaths() { var paths = new List<(string, string)>(); @@ -1602,24 +1582,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 +1620,6 @@ namespace PepperDash.Essentials } } - /// - /// - /// - /// private void ReconnectToServerTimerCallback(object o) { this.LogDebug("Attempting to reconnect to server..."); @@ -1651,9 +1627,6 @@ namespace PepperDash.Essentials ConnectWebsocketClient(); } - /// - /// Verifies system connection with servers - /// private void AuthorizeSystem(string code) { if ( @@ -1698,9 +1671,6 @@ namespace PepperDash.Essentials }); } - /// - /// Dumps info in response to console command. - /// private void ShowInfo() { var url = Config != null ? Host : "No config"; @@ -1766,38 +1736,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 +1780,7 @@ namespace PepperDash.Essentials } /// - /// RegisterSystemToServer method + /// Register this system to the Mobile Control Edge Server /// public void RegisterSystemToServer() { @@ -1835,9 +1804,6 @@ namespace PepperDash.Essentials ConnectWebsocketClient(); } - /// - /// Connects the Websocket Client - /// private void ConnectWebsocketClient() { try @@ -1878,9 +1844,6 @@ namespace PepperDash.Essentials } } - /// - /// Attempts to connect the websocket - /// private void TryConnect() { try @@ -1910,9 +1873,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 +1904,6 @@ namespace PepperDash.Essentials StartServerReconnectTimer(); } - /// - /// - /// - /// - /// private void HandleOpen(object sender, EventArgs e) { StopServerReconnectTimer(); @@ -1957,11 +1912,6 @@ namespace PepperDash.Essentials SendMessageObject(new MobileControlMessage { Type = "hello" }); } - /// - /// - /// - /// - /// private void HandleMessage(object sender, MessageEventArgs e) { if (e.IsPing) @@ -1978,11 +1928,6 @@ namespace PepperDash.Essentials } } - /// - /// - /// - /// - /// private void HandleError(object sender, ErrorEventArgs e) { this.LogError("Websocket error {0}", e.Message); @@ -1991,11 +1936,6 @@ namespace PepperDash.Essentials StartServerReconnectTimer(); } - /// - /// - /// - /// - /// private void HandleClose(object sender, CloseEventArgs e) { this.LogDebug( @@ -2016,9 +1956,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 +1982,7 @@ namespace PepperDash.Essentials } /// - /// GetConfigWithPluginVersion method + /// Get the Essentials configuration with version data /// public MobileControlEssentialsConfig GetConfigWithPluginVersion() { @@ -2080,8 +2017,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 +2039,6 @@ namespace PepperDash.Essentials /// Sends any object type to server /// /// - /// - /// SendMessageObject method - /// public void SendMessageObject(IMobileControlMessage o) { @@ -2123,8 +2062,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 +2077,6 @@ namespace PepperDash.Essentials } } - - /// - /// Disconnects the Websocket Client and stops the heartbeat timer - /// private void CleanUpWebsocketClient() { if (_wsClient2 == null) @@ -2198,9 +2134,6 @@ namespace PepperDash.Essentials } } - /// - /// - /// private void StartServerReconnectTimer() { StopServerReconnectTimer(); @@ -2211,9 +2144,6 @@ namespace PepperDash.Essentials this.LogDebug("Reconnect Timer Started."); } - /// - /// Does what it says - /// private void StopServerReconnectTimer() { if (_serverReconnectTimer == null) @@ -2224,10 +2154,6 @@ namespace PepperDash.Essentials _serverReconnectTimer = null; } - /// - /// Resets reconnect timer and updates usercode - /// - /// private void HandleHeartBeat(JToken content) { SendMessageObject(new MobileControlMessage { Type = "/system/heartbeatAck" }); @@ -2337,16 +2263,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 +2337,33 @@ namespace PepperDash.Essentials foreach (var handler in handlers) { - Task.Run( - () => - handler.Action(message.Type, message.ClientId, message.Content) - ); + Task.Run(async () => + { + 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 +2379,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..d4c55f87 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) @@ -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/ThemeMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs index 089665d9..0f02bc59 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs @@ -7,17 +7,24 @@ 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) => 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..a30c0bd2 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()); @@ -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); + 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..e4e8a47d 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,16 @@ 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; } + /// /// Gets or sets the mobile control system controller that handles this client's messages /// @@ -32,11 +41,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 +64,22 @@ 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 + public UiClient(string key, string id, string token) { Key = key; + Id = id; + Token = token; } /// @@ -74,19 +87,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,7 +103,7 @@ namespace PepperDash.Essentials.WebSocketServer Type = "/system/clientJoined", Content = JToken.FromObject(new { - clientId, + clientId = Id, roomKey = RoomKey, }) }; @@ -110,7 +114,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 +129,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 +176,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;