From 8525134ae70d508336181cca8f86088003945b65 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 15 Oct 2025 09:50:12 -0500 Subject: [PATCH 1/7] fix: direct server clients now have unique client IDs Using the generated security token as an ID was presenting problems with duplicate connections using the same ID and trying to figure out where to send the messages. Now, the clients have a unique ID that's an increasing integer that restarts at 1 when the program restarts. --- .../MessageToClients.cs | 90 ++++++ .../MobileControlSystemController.cs | 28 +- .../TransmitMessage.cs | 97 ++---- .../Utilities.cs | 97 ++++++ .../WebApiHandlers/MobileInfoHandler.cs | 12 +- .../WebApiHandlers/UiClientHandler.cs | 4 +- .../ConnectionClosedEventArgs.cs | 24 ++ .../WebSocketServer/JoinToken.cs | 4 + .../MobileControlWebsocketServer.cs | 286 ++++++++++-------- .../WebSocketServer/UiClient.cs | 57 +++- 10 files changed, 444 insertions(+), 255 deletions(-) create mode 100644 src/PepperDash.Essentials.MobileControl/MessageToClients.cs create mode 100644 src/PepperDash.Essentials.MobileControl/Utilities.cs create mode 100644 src/PepperDash.Essentials.MobileControl/WebSocketServer/ConnectionClosedEventArgs.cs 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/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index d6a40ede..0b320185 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -1382,33 +1382,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; 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/Utilities.cs b/src/PepperDash.Essentials.MobileControl/Utilities.cs new file mode 100644 index 00000000..83ebc5bc --- /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 + /// + /// + 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/WebApiHandlers/MobileInfoHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs index d123dbcd..588d3861 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs @@ -1,12 +1,12 @@ -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 { @@ -111,13 +111,13 @@ namespace PepperDash.Essentials.WebApiHandlers public int ServerPort => directServer.Port; [JsonProperty("tokensDefined")] - public int TokensDefined => directServer.UiClients.Count; + public int TokensDefined => directServer.UiClientContexts.Count; [JsonProperty("clientsConnected")] public int ClientsConnected => directServer.ConnectedUiClientsCount; [JsonProperty("clients")] - public List Clients => directServer.UiClients.Select((c, i) => { return new MobileControlDirectClient(c, i, directServer.UserAppUrlPrefix); }).ToList(); + public List Clients => directServer.UiClientContexts.Select((c, i) => { return new MobileControlDirectClient(c, i, directServer.UserAppUrlPrefix); }).ToList(); public MobileControlDirectServer(MobileControlWebsocketServer server) { diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs index e45fcc39..23b79d93 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs @@ -93,7 +93,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 +134,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..6d877d07 --- /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; } + + /// + /// Initalize an instance of the class. + /// + /// client that's closing + public ConnectionClosedEventArgs(string clientId) + { + ClientId = clientId; + } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs index b3ea3c7c..7352dc6f 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs @@ -5,6 +5,10 @@ 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 /// diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index d15185b0..0b0390aa 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,9 @@ 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(); private readonly MobileControlSystemController _parent; @@ -129,7 +132,7 @@ namespace PepperDash.Essentials.WebSocketServer { var count = 0; - foreach (var client in UiClients) + foreach (var client in UiClientContexts) { if (client.Value.Client != null && client.Value.Client.Context.WebSocket.IsAlive) { @@ -202,7 +205,7 @@ namespace PepperDash.Essentials.WebSocketServer } - UiClients = new Dictionary(); + UiClientContexts = new Dictionary(); //_joinTokens = new Dictionary(); @@ -278,29 +281,20 @@ namespace PepperDash.Essentials.WebSocketServer } _server.Log.Output = (data, message) => + { + switch (data.Level) { - 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; - } - }; + case LogLevel.Trace: this.LogVerbose(message); break; + case LogLevel.Debug: this.LogDebug(message); break; + case LogLevel.Info: this.LogInformation(message); break; + case LogLevel.Warn: this.LogWarning(message); break; + case LogLevel.Error: this.LogError(message); break; + case LogLevel.Fatal: this.LogFatal(message); break; + } + }; + + // setting to trace to allow logging level to be controlled by appdebug + _server.Log.Level = LogLevel.Trace; CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; @@ -554,20 +548,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 +569,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 +580,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 +605,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 +714,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 +734,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); + 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 +789,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 +807,7 @@ namespace PepperDash.Essentials.WebSocketServer } } - UiClients.Clear(); + UiClientContexts.Clear(); UpdateSecret(); } @@ -803,9 +826,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 +838,7 @@ namespace PepperDash.Essentials.WebSocketServer var path = _wsPath + key; if (_server.RemoveWebSocketService(path)) { - UiClients.Remove(key); + UiClientContexts.Remove(key); UpdateSecret(); @@ -839,9 +862,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)); } @@ -851,7 +874,7 @@ namespace PepperDash.Essentials.WebSocketServer { if (programEventType == eProgramStatusEventType.Stopping) { - foreach (var client in UiClients.Values) + foreach (var client in UiClientContexts.Values) { if (client.Client != null && client.Client.Context.WebSocket.IsAlive) { @@ -990,77 +1013,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 +1269,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 +1295,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/UiClient.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs index 3cdd183b..79becd33 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs @@ -64,9 +64,11 @@ namespace PepperDash.Essentials.WebSocketServer /// 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 + public UiClient(string key, string id) { Key = key; + Id = id; } /// @@ -74,19 +76,33 @@ namespace PepperDash.Essentials.WebSocketServer { base.OnOpen(); - var url = Context.WebSocket.Url; - this.LogInformation("New WebSocket Connection from: {url}", url); + Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message); + Log.Level = LogLevel.Trace; - var match = Regex.Match(url.AbsoluteUri, "(?:ws|wss):\\/\\/.*(?:\\/mc\\/api\\/ui\\/join\\/)(.*)"); - - if (!match.Success) + try { - _connectionTime = DateTime.Now; - return; + this.LogDebug("Current session count on open {count}", Sessions.Count); + this.LogDebug("Current WebsocketServiceCount on open: {count}", Controller.DirectServer.WebsocketServiceCount); + } + catch (Exception ex) + { + this.LogError("Error getting service count: {message}", ex.Message); + this.LogDebug(ex, "Stack Trace: "); } - var clientId = match.Groups[1].Value; - _clientId = clientId; + // var url = Context.WebSocket.Url; + // this.LogInformation("New WebSocket Connection from: {url}", url); + + // 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; if (Controller == null) { @@ -99,7 +115,7 @@ namespace PepperDash.Essentials.WebSocketServer Type = "/system/clientJoined", Content = JToken.FromObject(new { - clientId, + clientId = Id, roomKey = RoomKey, }) }; @@ -110,7 +126,7 @@ namespace PepperDash.Essentials.WebSocketServer if (bridge == null) return; - SendUserCodeToClient(bridge, clientId); + SendUserCodeToClient(bridge, Id); bridge.UserCodeChanged -= Bridge_UserCodeChanged; bridge.UserCodeChanged += Bridge_UserCodeChanged; @@ -168,17 +184,30 @@ namespace PepperDash.Essentials.WebSocketServer { base.OnClose(e); + try + { + this.LogDebug("Current session count on close {count}", Sessions.Count); + this.LogDebug("Current WebsocketServiceCount on close: {count}", Controller.DirectServer.WebsocketServiceCount); + } + catch (Exception ex) + { + this.LogError("Error getting service count: {message}", ex.Message); + this.LogDebug(ex, "Stack Trace: "); + } + this.LogInformation("WebSocket UiClient Closing: {code} reason: {reason}", e.Code, e.Reason); 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)); } /// From c557c6cdd648760a2a6dea5a80a8f30497c4fe06 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 15 Oct 2025 11:49:06 -0500 Subject: [PATCH 2/7] fix: mobileinfo & CWS info call report the correct data --- .../MobileControlSystemController.cs | 44 +++++++------ .../WebApiHandlers/MobileInfoHandler.cs | 41 +++++++++--- .../MobileControlWebsocketServer.cs | 19 ++---- .../WebSocketServer/UiClient.cs | 65 ++++++------------- 4 files changed, 84 insertions(+), 85 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 0b320185..392b5543 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -1746,38 +1746,40 @@ 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++; } } diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs index 588d3861..ddd319e3 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs @@ -117,7 +117,11 @@ namespace PepperDash.Essentials.WebApiHandlers public int ClientsConnected => directServer.ConnectedUiClientsCount; [JsonProperty("clients")] - public List Clients => directServer.UiClientContexts.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(); + public MobileControlDirectServer(MobileControlWebsocketServer server) { @@ -157,18 +161,39 @@ namespace PepperDash.Essentials.WebApiHandlers [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; + [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; + } + } + + public class ClientStatus + { + private readonly UiClient client; + + public bool Connected => client != null && client.Context.WebSocket.IsAlive; + + public double Duration => client == null ? 0 : client.ConnectedDuration.TotalSeconds; + + public ClientStatus(UiClient client) + { + this.client = client; } } } diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index 0b0390aa..f62e136a 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -61,6 +61,11 @@ namespace PepperDash.Essentials.WebSocketServer private readonly Dictionary uiClients = new Dictionary(); + /// + /// Gets the collection of UI clients + /// + public ReadOnlyDictionary UiClients => new ReadOnlyDictionary(uiClients); + private readonly MobileControlSystemController _parent; private WebSocketServerSecretProvider _secretProvider; @@ -130,17 +135,7 @@ namespace PepperDash.Essentials.WebSocketServer { get { - var count = 0; - - foreach (var client in UiClientContexts) - { - if (client.Value.Client != null && client.Value.Client.Context.WebSocket.IsAlive) - { - count++; - } - } - - return count; + return uiClients.Values.Where(c => c.Context.WebSocket.IsAlive).Count(); } } @@ -736,7 +731,7 @@ namespace PepperDash.Essentials.WebSocketServer private UiClient BuildUiClient(string roomKey, JoinToken token, string key) { - var c = new UiClient($"uiclient-{key}-{roomKey}-{token.Id}", token.Id); + 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; diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs index 79becd33..21781abc 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,15 +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 /// The client ID used by the client for this connection - public UiClient(string key, string id) + /// + public UiClient(string key, string id, string token) { Key = key; Id = id; + Token = token; } /// @@ -76,34 +87,11 @@ namespace PepperDash.Essentials.WebSocketServer { base.OnOpen(); + _connectionTime = DateTime.Now; + Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message); Log.Level = LogLevel.Trace; - try - { - this.LogDebug("Current session count on open {count}", Sessions.Count); - this.LogDebug("Current WebsocketServiceCount on open: {count}", Controller.DirectServer.WebsocketServiceCount); - } - catch (Exception ex) - { - this.LogError("Error getting service count: {message}", ex.Message); - this.LogDebug(ex, "Stack Trace: "); - } - - // var url = Context.WebSocket.Url; - // this.LogInformation("New WebSocket Connection from: {url}", url); - - // 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; - if (Controller == null) { Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Controller is null"); @@ -141,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); } /// @@ -184,17 +172,6 @@ namespace PepperDash.Essentials.WebSocketServer { base.OnClose(e); - try - { - this.LogDebug("Current session count on close {count}", Sessions.Count); - this.LogDebug("Current WebsocketServiceCount on close: {count}", Controller.DirectServer.WebsocketServiceCount); - } - catch (Exception ex) - { - this.LogError("Error getting service count: {message}", ex.Message); - this.LogDebug(ex, "Stack Trace: "); - } - this.LogInformation("WebSocket UiClient Closing: {code} reason: {reason}", e.Code, e.Reason); foreach (var messenger in Controller.Messengers) From 98d0cc8fdcddaf16c2b89c4216f5e021346ff48c Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 15 Oct 2025 12:26:57 -0500 Subject: [PATCH 3/7] docs: add missing XML comments for Mobile Control Project --- .../ClientSpecificUpdateRequest.cs | 7 +- .../IDelayedConfiguration.cs | 5 +- .../MobileControlAction.cs | 14 +- .../MobileControlDeviceFactory.cs | 17 +- .../MobileControlEssentialsConfig.cs | 40 +++-- .../MobileControlFactory.cs | 5 +- .../MobileControlSystemController.cs | 159 ++++++------------ .../RoomBridges/MobileControlBridgeBase.cs | 48 +++++- .../MobileControlEssentialsRoomBridge.cs | 113 ++++++++++++- .../Services/MobileControlApiService.cs | 17 +- .../Touchpanel/ITheme.cs | 7 + .../Touchpanel/ITswAppControl.cs | 21 +++ .../Touchpanel/ITswAppControlMessenger.cs | 16 +- .../Touchpanel/ITswZoomControlMessenger.cs | 24 ++- .../Touchpanel/ThemeMessenger.cs | 9 +- .../UserCodeChanged.cs | 9 +- .../Volumes.cs | 49 +++++- .../WebApiHandlers/ActionPathsHandler.cs | 16 ++ .../MobileAuthRequestHandler.cs | 15 +- .../WebApiHandlers/MobileInfoHandler.cs | 96 ++++++++++- .../WebApiHandlers/UiClientHandler.cs | 13 ++ .../WebSocketServer/JoinResponse.cs | 6 + .../WebSocketServer/JoinToken.cs | 9 + .../MobileControlWebsocketServer.cs | 3 + .../WebSocketServer/ServerTokenSecrets.cs | 7 + .../WebSocketServer/UiClientContext.cs | 4 + .../WebSocketServer/Version.cs | 13 ++ .../WebSocketServerSecretProvider.cs | 11 +- 28 files changed, 576 insertions(+), 177 deletions(-) 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/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 392b5543..d2aef597 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,14 @@ namespace PepperDash.Essentials /// public string SystemUrl; //set only from SIMPL Bridge! + /// public bool Connected => _wsClient2 != null && _wsClient2.IsAlive; private IEssentialsRoomCombiner _roomCombiner; + /// + /// Gets the SystemUuid from configuration or SIMPL Bridge + /// public string SystemUuid { get @@ -169,6 +179,9 @@ namespace PepperDash.Essentials private DateTime _lastAckMessage; + /// + /// Gets the LastAckMessage timestamp + /// public DateTime LastAckMessage => _lastAckMessage; private CTimer _pingTimer; @@ -177,11 +190,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 +1205,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 +1219,7 @@ namespace PepperDash.Essentials } /// - /// CheckForDeviceMessenger method + /// Checks if a device messenger exists for the given key. /// public bool CheckForDeviceMessenger(string key) { @@ -1211,13 +1227,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 +1307,6 @@ namespace PepperDash.Essentials messenger.RegisterWithAppServer(this); } - /// - /// Initialize method - /// /// public override void Initialize() { @@ -1338,7 +1351,7 @@ namespace PepperDash.Essentials #region IMobileControl Members /// - /// GetAppServer method + /// Gets the App Server instance /// public static IMobileControl GetAppServer() { @@ -1356,16 +1369,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)) { @@ -1402,7 +1409,7 @@ namespace PepperDash.Essentials } /// - /// LinkSystemMonitorToAppServer method + /// Link the System Monitor to this App server /// public void LinkSystemMonitorToAppServer() { @@ -1429,14 +1436,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); @@ -1474,10 +1473,6 @@ namespace PepperDash.Essentials } } - /// - /// Sends message to server to indicate the system is shutting down - /// - /// private void CrestronEnvironment_ProgramStatusEventHandler( eProgramStatusEventType programEventType ) @@ -1510,6 +1505,9 @@ namespace PepperDash.Essentials } } + /// + /// Get action paths for the current actions + /// public List<(string, string)> GetActionDictionaryPaths() { var paths = new List<(string, string)>(); @@ -1582,24 +1580,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"); @@ -1620,10 +1618,6 @@ namespace PepperDash.Essentials } } - /// - /// - /// - /// private void ReconnectToServerTimerCallback(object o) { this.LogDebug("Attempting to reconnect to server..."); @@ -1631,9 +1625,6 @@ namespace PepperDash.Essentials ConnectWebsocketClient(); } - /// - /// Verifies system connection with servers - /// private void AuthorizeSystem(string code) { if ( @@ -1678,9 +1669,6 @@ namespace PepperDash.Essentials }); } - /// - /// Dumps info in response to console command. - /// private void ShowInfo() { var url = Config != null ? Host : "No config"; @@ -1753,9 +1741,6 @@ namespace PepperDash.Essentials var clientNo = 1; foreach (var clientContext in _directServer.UiClientContexts) { - var isAlive = false; - var duration = "Not Connected"; - var clients = _directServer.UiClients.Values.Where(c => c.Token == clientContext.Value.Token.Token); CrestronConsole.ConsoleCommandResponse( @@ -1793,7 +1778,7 @@ namespace PepperDash.Essentials } /// - /// RegisterSystemToServer method + /// Register this system to the Mobile Control Edge Server /// public void RegisterSystemToServer() { @@ -1817,9 +1802,6 @@ namespace PepperDash.Essentials ConnectWebsocketClient(); } - /// - /// Connects the Websocket Client - /// private void ConnectWebsocketClient() { try @@ -1860,9 +1842,6 @@ namespace PepperDash.Essentials } } - /// - /// Attempts to connect the websocket - /// private void TryConnect() { try @@ -1892,9 +1871,6 @@ namespace PepperDash.Essentials } } - /// - /// Gracefully handles conect failures by reconstructing the ws client and starting the reconnect timer - /// private void HandleConnectFailure() { _wsClient2 = null; @@ -1926,11 +1902,6 @@ namespace PepperDash.Essentials StartServerReconnectTimer(); } - /// - /// - /// - /// - /// private void HandleOpen(object sender, EventArgs e) { StopServerReconnectTimer(); @@ -1939,11 +1910,6 @@ namespace PepperDash.Essentials SendMessageObject(new MobileControlMessage { Type = "hello" }); } - /// - /// - /// - /// - /// private void HandleMessage(object sender, MessageEventArgs e) { if (e.IsPing) @@ -1960,11 +1926,6 @@ namespace PepperDash.Essentials } } - /// - /// - /// - /// - /// private void HandleError(object sender, ErrorEventArgs e) { this.LogError("Websocket error {0}", e.Message); @@ -1973,11 +1934,6 @@ namespace PepperDash.Essentials StartServerReconnectTimer(); } - /// - /// - /// - /// - /// private void HandleClose(object sender, CloseEventArgs e) { this.LogDebug( @@ -1998,9 +1954,6 @@ namespace PepperDash.Essentials StartServerReconnectTimer(); } - /// - /// After a "hello" from the server, sends config and stuff - /// private void SendInitialMessage() { this.LogInformation("Sending initial join message"); @@ -2027,7 +1980,7 @@ namespace PepperDash.Essentials } /// - /// GetConfigWithPluginVersion method + /// Get the Essentials configuration with version data /// public MobileControlEssentialsConfig GetConfigWithPluginVersion() { @@ -2062,8 +2015,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 @@ -2079,9 +2037,6 @@ namespace PepperDash.Essentials /// Sends any object type to server /// /// - /// - /// SendMessageObject method - /// public void SendMessageObject(IMobileControlMessage o) { @@ -2105,8 +2060,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 ( @@ -2119,10 +2075,6 @@ namespace PepperDash.Essentials } } - - /// - /// Disconnects the Websocket Client and stops the heartbeat timer - /// private void CleanUpWebsocketClient() { if (_wsClient2 == null) @@ -2180,9 +2132,6 @@ namespace PepperDash.Essentials } } - /// - /// - /// private void StartServerReconnectTimer() { StopServerReconnectTimer(); @@ -2193,9 +2142,6 @@ namespace PepperDash.Essentials this.LogDebug("Reconnect Timer Started."); } - /// - /// Does what it says - /// private void StopServerReconnectTimer() { if (_serverReconnectTimer == null) @@ -2206,10 +2152,6 @@ namespace PepperDash.Essentials _serverReconnectTimer = null; } - /// - /// Resets reconnect timer and updates usercode - /// - /// private void HandleHeartBeat(JToken content) { SendMessageObject(new MobileControlMessage { Type = "/system/heartbeatAck" }); @@ -2319,16 +2261,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)) @@ -2415,10 +2354,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..13d1dc59 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 /// public class ITswZoomControlMessenger : MessengerBase { private readonly ITswZoomControl _zoomControl; + /// + /// Create in 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/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/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 ddd319e3..363076c0 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs @@ -11,16 +11,25 @@ using PepperDash.Essentials.WebSocketServer; 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 in instace 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,25 +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.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.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; @@ -146,23 +203,41 @@ 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; private readonly List clients; + /// + /// List of status for all connected UI Clients + /// [JsonProperty("clientStatus")] public List ClientStatus => clients.Select(c => new ClientStatus(c)).ToList(); @@ -183,14 +258,27 @@ namespace PepperDash.Essentials.WebApiHandlers } } + /// + /// 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 23b79d93..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; 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 7352dc6f..63fdcdd4 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs @@ -14,10 +14,19 @@ namespace PepperDash.Essentials.WebSocketServer /// 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 f62e136a..759886ff 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -315,6 +315,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()); 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/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..1255f624 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; From 3e0f318f7f923b9ea1fc9af5c83d8418b3760876 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 15 Oct 2025 12:36:42 -0500 Subject: [PATCH 4/7] fix: update log methods to all be consistent --- .../WebSocketServer/MobileControlWebsocketServer.cs | 13 +------------ .../WebSocketServer/UiClient.cs | 2 +- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index 759886ff..2532a907 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -275,18 +275,7 @@ namespace PepperDash.Essentials.WebSocketServer }; } - _server.Log.Output = (data, message) => - { - switch (data.Level) - { - case LogLevel.Trace: this.LogVerbose(message); break; - case LogLevel.Debug: this.LogDebug(message); break; - case LogLevel.Info: this.LogInformation(message); break; - case LogLevel.Warn: this.LogWarning(message); break; - case LogLevel.Error: this.LogError(message); break; - case LogLevel.Fatal: this.LogFatal(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; diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs index 21781abc..83696ef2 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs @@ -89,7 +89,7 @@ namespace PepperDash.Essentials.WebSocketServer _connectionTime = DateTime.Now; - Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message); + Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this); Log.Level = LogLevel.Trace; if (Controller == null) From 608601990bf758d5ad42a548ed530f789657566c Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 15 Oct 2025 12:54:14 -0500 Subject: [PATCH 5/7] docs: fix Copilot comments --- .../MobileControlSystemController.cs | 4 +++- .../Touchpanel/ITswZoomControlMessenger.cs | 4 ++-- src/PepperDash.Essentials.MobileControl/Utilities.cs | 4 ++-- .../WebApiHandlers/MobileInfoHandler.cs | 2 +- .../WebSocketServer/ConnectionClosedEventArgs.cs | 2 +- .../WebSocketServer/UiClient.cs | 2 +- .../WebSocketServer/Version.cs | 2 +- 7 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index d2aef597..1012abe8 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -110,7 +110,9 @@ 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; diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs index 13d1dc59..bbf4030e 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs @@ -8,14 +8,14 @@ using PepperDash.Essentials.AppServer.Messengers; namespace PepperDash.Essentials.Touchpanel { /// - /// Messenger to handle + /// 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 in instance of the class for the given device + /// Create an instance of the class for the given device /// /// The key for this messenger /// The message path for this messenger diff --git a/src/PepperDash.Essentials.MobileControl/Utilities.cs b/src/PepperDash.Essentials.MobileControl/Utilities.cs index 83ebc5bc..8c2abf3e 100644 --- a/src/PepperDash.Essentials.MobileControl/Utilities.cs +++ b/src/PepperDash.Essentials.MobileControl/Utilities.cs @@ -12,9 +12,9 @@ namespace PepperDash.Essentials private static int nextClientId = 0; /// - /// Get + /// Get the next unique client ID /// - /// + /// Client ID public static int GetNextClientId() { nextClientId++; diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs index 363076c0..676fd3c8 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs @@ -72,7 +72,7 @@ namespace PepperDash.Essentials.WebApiHandlers public MobileControlDirectServer DirectServer => mcController.Config.DirectServer.EnableDirectServer ? new MobileControlDirectServer(mcController.DirectServer) : null; /// - /// Create in instace of the class. + /// Create an instance of the class. /// /// public InformationResponse(MobileControlSystemController controller) diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/ConnectionClosedEventArgs.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/ConnectionClosedEventArgs.cs index 6d877d07..617cc6d6 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/ConnectionClosedEventArgs.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/ConnectionClosedEventArgs.cs @@ -13,7 +13,7 @@ namespace PepperDash.Essentials.WebSocketServer public string ClientId { get; private set; } /// - /// Initalize an instance of the class. + /// Initialize an instance of the class. /// /// client that's closing public ConnectionClosedEventArgs(string clientId) diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs index 83696ef2..e4e8a47d 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs @@ -74,7 +74,7 @@ namespace PepperDash.Essentials.WebSocketServer /// /// The unique key to identify this client /// 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; diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/Version.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/Version.cs index 1255f624..9380e54d 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/Version.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/Version.cs @@ -25,7 +25,7 @@ namespace PepperDash.Essentials.WebSocketServer /// Initialize an instance of the class /// /// - /// the property is set to true by default. + /// The property is set to true by default. /// public Version() { From 6cb98e12fae40135f9705a80b7ca2a957d0ffc13 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 15 Oct 2025 14:02:06 -0500 Subject: [PATCH 6/7] fix: use correct collection for program stop --- .../WebSocketServer/MobileControlWebsocketServer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index 2532a907..a30c0bd2 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -861,11 +861,11 @@ namespace PepperDash.Essentials.WebSocketServer { if (programEventType == eProgramStatusEventType.Stopping) { - foreach (var client in UiClientContexts.Values) + 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"); } } From 5c35a3be45cd15a131c82882c481d0218f1f1075 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 15 Oct 2025 14:03:17 -0500 Subject: [PATCH 7/7] fix: catch exceptions in handlers directly Previously, any exceptions that were occuring in a hander's action were being swalled due to being off on another thread. Now, those exceptions are caught and printed out. --- .../MobileControlSystemController.cs | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 1012abe8..01bf3579 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -2337,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;