Merge pull request #1342 from PepperDash/wsdebug-persistence

Unique Client IDs
This commit is contained in:
Neil Dorin
2025-10-15 15:16:46 -04:00
committed by GitHub
33 changed files with 1087 additions and 486 deletions

View File

@@ -3,10 +3,15 @@ using System;
namespace PepperDash.Essentials namespace PepperDash.Essentials
{ {
/// <summary> /// <summary>
/// Represents a ClientSpecificUpdateRequest /// Send an update request for a specific client
/// </summary> /// </summary>
[Obsolete]
public class ClientSpecificUpdateRequest public class ClientSpecificUpdateRequest
{ {
/// <summary>
/// Initialize an instance of the <see cref="ClientSpecificUpdateRequest"/> class.
/// </summary>
/// <param name="action"></param>
public ClientSpecificUpdateRequest(Action<string> action) public ClientSpecificUpdateRequest(Action<string> action)
{ {
ResponseMethod = action; ResponseMethod = action;

View File

@@ -7,8 +7,9 @@ namespace PepperDash.Essentials
/// </summary> /// </summary>
public interface IDelayedConfiguration public interface IDelayedConfiguration
{ {
/// <summary>
/// Event triggered when the configuration is ready. Used when Mobile Control is interacting with a SIMPL program.
/// </summary>
event EventHandler<EventArgs> ConfigurationIsReady; event EventHandler<EventArgs> ConfigurationIsReady;
} }
} }

View File

@@ -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
{
/// <summary>
/// Represents a MessageToClients
/// </summary>
public class MessageToClients : IQueueMessage
{
private readonly MobileControlWebsocketServer _server;
private readonly object msgToSend;
/// <summary>
/// Message to send to Direct Server Clients
/// </summary>
/// <param name="msg">message object to send</param>
/// <param name="server">WebSocket server instance</param>
public MessageToClients(object msg, MobileControlWebsocketServer server)
{
_server = server;
msgToSend = msg;
}
/// <summary>
/// Message to send to Direct Server Clients
/// </summary>
/// <param name="msg">message object to send</param>
/// <param name="server">WebSocket server instance</param>
public MessageToClients(DeviceStateMessageBase msg, MobileControlWebsocketServer server)
{
_server = server;
msgToSend = msg;
}
#region Implementation of IQueueMessage
/// <summary>
/// Dispatch method
/// </summary>
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
}
}

View File

@@ -1,6 +1,6 @@
using Newtonsoft.Json.Linq; using System;
using Newtonsoft.Json.Linq;
using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Core.DeviceTypeInterfaces;
using System;
namespace PepperDash.Essentials namespace PepperDash.Essentials
{ {
@@ -10,12 +10,20 @@ namespace PepperDash.Essentials
public class MobileControlAction : IMobileControlAction public class MobileControlAction : IMobileControlAction
{ {
/// <summary> /// <summary>
/// Gets or sets the Messenger /// Gets the Messenger
/// </summary> /// </summary>
public IMobileControlMessenger Messenger { get; private set; } public IMobileControlMessenger Messenger { get; private set; }
/// <summary>
/// Action to execute when this path is matched
/// </summary>
public Action<string, string, JToken> Action { get; private set; } public Action<string, string, JToken> Action { get; private set; }
/// <summary>
/// Initialize an instance of the <see cref="MobileControlAction"/> class
/// </summary>
/// <param name="messenger">Messenger associated with this action</param>
/// <param name="handler">Action to take when this path is matched</param>
public MobileControlAction(IMobileControlMessenger messenger, Action<string, string, JToken> handler) public MobileControlAction(IMobileControlMessenger messenger, Action<string, string, JToken> handler)
{ {
Messenger = messenger; Messenger = messenger;

View File

@@ -1,28 +1,25 @@
using PepperDash.Core; using System;
using PepperDash.Core.Logging; using System.Collections.Generic;
using PepperDash.Core;
using PepperDash.Essentials.Core; using PepperDash.Essentials.Core;
using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.Config;
using Serilog.Events;
using System;
using System.Collections.Generic;
using System.Linq;
namespace PepperDash.Essentials namespace PepperDash.Essentials
{ {
/// <summary> /// <summary>
/// Represents a MobileControlDeviceFactory /// Factory to create a Mobile Control System Controller
/// </summary> /// </summary>
public class MobileControlDeviceFactory : EssentialsDeviceFactory<MobileControlSystemController> public class MobileControlDeviceFactory : EssentialsDeviceFactory<MobileControlSystemController>
{ {
/// <summary>
/// Create the factory for a Mobile Control System Controller
/// </summary>
public MobileControlDeviceFactory() public MobileControlDeviceFactory()
{ {
TypeNames = new List<string> { "appserver", "mobilecontrol", "webserver" }; TypeNames = new List<string> { "appserver", "mobilecontrol", "webserver" };
} }
/// <summary>
/// BuildDevice method
/// </summary>
/// <inheritdoc /> /// <inheritdoc />
public override EssentialsDevice BuildDevice(DeviceConfig dc) public override EssentialsDevice BuildDevice(DeviceConfig dc)
{ {

View File

@@ -6,29 +6,35 @@ using PepperDash.Essentials.Core.Config;
namespace PepperDash.Essentials namespace PepperDash.Essentials
{ {
/// <summary> /// <summary>
/// Represents a MobileControlEssentialsConfig /// Configuration class for sending data to Mobile Control Edge or a client using the Direct Server
/// </summary> /// </summary>
public class MobileControlEssentialsConfig : EssentialsConfig public class MobileControlEssentialsConfig : EssentialsConfig
{ {
/// <summary>
/// Current versions for the system
/// </summary>
[JsonProperty("runtimeInfo")] [JsonProperty("runtimeInfo")]
public MobileControlRuntimeInfo RuntimeInfo { get; set; } public MobileControlRuntimeInfo RuntimeInfo { get; set; }
/// <summary>
/// Create Configuration for Mobile Control. Used as part of the data sent to a client
/// </summary>
/// <param name="config">The base configuration</param>
public MobileControlEssentialsConfig(EssentialsConfig config) public MobileControlEssentialsConfig(EssentialsConfig config)
: base() : base()
{ {
// TODO: Consider using Reflection to iterate properties Devices = config.Devices;
this.Devices = config.Devices; Info = config.Info;
this.Info = config.Info; JoinMaps = config.JoinMaps;
this.JoinMaps = config.JoinMaps; Rooms = config.Rooms;
this.Rooms = config.Rooms; SourceLists = config.SourceLists;
this.SourceLists = config.SourceLists; DestinationLists = config.DestinationLists;
this.DestinationLists = config.DestinationLists; SystemUrl = config.SystemUrl;
this.SystemUrl = config.SystemUrl; TemplateUrl = config.TemplateUrl;
this.TemplateUrl = config.TemplateUrl; TieLines = config.TieLines;
this.TieLines = config.TieLines;
if (this.Info == null) if (Info == null)
this.Info = new InfoConfig(); Info = new InfoConfig();
RuntimeInfo = new MobileControlRuntimeInfo(); RuntimeInfo = new MobileControlRuntimeInfo();
} }
@@ -46,15 +52,21 @@ namespace PepperDash.Essentials
[JsonProperty("pluginVersion")] [JsonProperty("pluginVersion")]
public string PluginVersion { get; set; } public string PluginVersion { get; set; }
/// <summary>
/// Essentials Version
/// </summary>
[JsonProperty("essentialsVersion")] [JsonProperty("essentialsVersion")]
public string EssentialsVersion { get; set; } public string EssentialsVersion { get; set; }
/// <summary>
/// PepperDash Core Version
/// </summary>
[JsonProperty("pepperDashCoreVersion")] [JsonProperty("pepperDashCoreVersion")]
public string PepperDashCoreVersion { get; set; } public string PepperDashCoreVersion { get; set; }
/// <summary> /// <summary>
/// Gets or sets the EssentialsPlugins /// List of Plugins loaded on this system
/// </summary> /// </summary>
[JsonProperty("essentialsPlugins")] [JsonProperty("essentialsPlugins")]
public List<LoadedAssembly> EssentialsPlugins { get; set; } public List<LoadedAssembly> EssentialsPlugins { get; set; }

View File

@@ -7,10 +7,13 @@ using PepperDash.Essentials.Core;
namespace PepperDash.Essentials namespace PepperDash.Essentials
{ {
/// <summary> /// <summary>
/// Represents a MobileControlFactory /// Factory class for the Mobile Control App Controller
/// </summary> /// </summary>
public class MobileControlFactory public class MobileControlFactory
{ {
/// <summary>
/// Create an instance of the <see cref="MobileControlFactory"/> class.
/// </summary>
public MobileControlFactory() public MobileControlFactory()
{ {
var assembly = Assembly.GetExecutingAssembly(); var assembly = Assembly.GetExecutingAssembly();

View File

@@ -91,10 +91,16 @@ namespace PepperDash.Essentials
/// </summary> /// </summary>
public MobileControlApiService ApiService { get; private set; } public MobileControlApiService ApiService { get; private set; }
/// <summary>
/// Get Room Bridges associated with this controller
/// </summary>
public List<MobileControlBridgeBase> RoomBridges => _roomBridges; public List<MobileControlBridgeBase> RoomBridges => _roomBridges;
private readonly MobileControlWebsocketServer _directServer; private readonly MobileControlWebsocketServer _directServer;
/// <summary>
/// Get the Direct Server instance associated with this controller
/// </summary>
public MobileControlWebsocketServer DirectServer => _directServer; public MobileControlWebsocketServer DirectServer => _directServer;
private readonly CCriticalSection _wsCriticalSection = new CCriticalSection(); private readonly CCriticalSection _wsCriticalSection = new CCriticalSection();
@@ -104,10 +110,16 @@ namespace PepperDash.Essentials
/// </summary> /// </summary>
public string SystemUrl; //set only from SIMPL Bridge! public string SystemUrl; //set only from SIMPL Bridge!
/// <summary>
/// True if the Mobile Control Edge Server Websocket is connected
/// </summary>
public bool Connected => _wsClient2 != null && _wsClient2.IsAlive; public bool Connected => _wsClient2 != null && _wsClient2.IsAlive;
private IEssentialsRoomCombiner _roomCombiner; private IEssentialsRoomCombiner _roomCombiner;
/// <summary>
/// Gets the SystemUuid from configuration or SIMPL Bridge
/// </summary>
public string SystemUuid public string SystemUuid
{ {
get get
@@ -169,6 +181,9 @@ namespace PepperDash.Essentials
private DateTime _lastAckMessage; private DateTime _lastAckMessage;
/// <summary>
/// Gets the LastAckMessage timestamp
/// </summary>
public DateTime LastAckMessage => _lastAckMessage; public DateTime LastAckMessage => _lastAckMessage;
private CTimer _pingTimer; private CTimer _pingTimer;
@@ -177,11 +192,11 @@ namespace PepperDash.Essentials
private LogLevel _wsLogLevel = LogLevel.Error; private LogLevel _wsLogLevel = LogLevel.Error;
/// <summary> /// <summary>
/// /// Initializes a new instance of the <see cref="MobileControlSystemController"/> class.
/// </summary> /// </summary>
/// <param name="key"></param> /// <param name="key">The unique key for this controller.</param>
/// <param name="name"></param> /// <param name="name">The name of the controller.</param>
/// <param name="config"></param> /// <param name="config">The configuration settings for the controller.</param>
public MobileControlSystemController(string key, string name, MobileControlConfig config) public MobileControlSystemController(string key, string name, MobileControlConfig config)
: base(key, name) : base(key, name)
{ {
@@ -1192,6 +1207,9 @@ namespace PepperDash.Essentials
/// </summary> /// </summary>
public string Host { get; private set; } public string Host { get; private set; }
/// <summary>
/// Gets the configured Client App URL
/// </summary>
public string ClientAppUrl => Config.ClientAppUrl; public string ClientAppUrl => Config.ClientAppUrl;
private void OnRoomCombinationScenarioChanged( private void OnRoomCombinationScenarioChanged(
@@ -1203,7 +1221,7 @@ namespace PepperDash.Essentials
} }
/// <summary> /// <summary>
/// CheckForDeviceMessenger method /// Checks if a device messenger exists for the given key.
/// </summary> /// </summary>
public bool CheckForDeviceMessenger(string key) public bool CheckForDeviceMessenger(string key)
{ {
@@ -1211,13 +1229,13 @@ namespace PepperDash.Essentials
} }
/// <summary> /// <summary>
/// AddDeviceMessenger method /// Add the provided messenger to the messengers collection
/// </summary> /// </summary>
public void AddDeviceMessenger(IMobileControlMessenger messenger) public void AddDeviceMessenger(IMobileControlMessenger messenger)
{ {
if (_messengers.ContainsKey(messenger.Key)) 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; return;
} }
@@ -1291,9 +1309,6 @@ namespace PepperDash.Essentials
messenger.RegisterWithAppServer(this); messenger.RegisterWithAppServer(this);
} }
/// <summary>
/// Initialize method
/// </summary>
/// <inheritdoc /> /// <inheritdoc />
public override void Initialize() public override void Initialize()
{ {
@@ -1338,7 +1353,7 @@ namespace PepperDash.Essentials
#region IMobileControl Members #region IMobileControl Members
/// <summary> /// <summary>
/// GetAppServer method /// Gets the App Server instance
/// </summary> /// </summary>
public static IMobileControl GetAppServer() public static IMobileControl GetAppServer()
{ {
@@ -1356,16 +1371,10 @@ namespace PepperDash.Essentials
} }
} }
/// <summary>
/// Generates the url and creates the websocket client
/// </summary>
private bool CreateWebsocket() private bool CreateWebsocket()
{ {
if (_wsClient2 != null) _wsClient2?.Close();
{ _wsClient2 = null;
_wsClient2.Close();
_wsClient2 = null;
}
if (string.IsNullOrEmpty(SystemUuid)) if (string.IsNullOrEmpty(SystemUuid))
{ {
@@ -1382,33 +1391,13 @@ namespace PepperDash.Essentials
{ {
Log = Log =
{ {
Output = (data, message) => Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this)
{
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;
}
}
} }
}; };
// setting to trace to let level be controlled by appdebug
_wsClient2.Log.Level = LogLevel.Trace;
_wsClient2.SslConfiguration.EnabledSslProtocols = _wsClient2.SslConfiguration.EnabledSslProtocols =
System.Security.Authentication.SslProtocols.Tls11 System.Security.Authentication.SslProtocols.Tls11
| System.Security.Authentication.SslProtocols.Tls12; | System.Security.Authentication.SslProtocols.Tls12;
@@ -1422,7 +1411,7 @@ namespace PepperDash.Essentials
} }
/// <summary> /// <summary>
/// LinkSystemMonitorToAppServer method /// Link the System Monitor to this App server
/// </summary> /// </summary>
public void LinkSystemMonitorToAppServer() public void LinkSystemMonitorToAppServer()
{ {
@@ -1449,14 +1438,6 @@ namespace PepperDash.Essentials
private void SetWebsocketDebugLevel(string cmdparameters) 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)) if (string.IsNullOrEmpty(cmdparameters))
{ {
this.LogInformation("Current Websocket debug level: {webSocketDebugLevel}", _wsLogLevel); this.LogInformation("Current Websocket debug level: {webSocketDebugLevel}", _wsLogLevel);
@@ -1494,10 +1475,6 @@ namespace PepperDash.Essentials
} }
} }
/// <summary>
/// Sends message to server to indicate the system is shutting down
/// </summary>
/// <param name="programEventType"></param>
private void CrestronEnvironment_ProgramStatusEventHandler( private void CrestronEnvironment_ProgramStatusEventHandler(
eProgramStatusEventType programEventType eProgramStatusEventType programEventType
) )
@@ -1530,6 +1507,9 @@ namespace PepperDash.Essentials
} }
} }
/// <summary>
/// Get action paths for the current actions
/// </summary>
public List<(string, string)> GetActionDictionaryPaths() public List<(string, string)> GetActionDictionaryPaths()
{ {
var paths = new List<(string, string)>(); var paths = new List<(string, string)>();
@@ -1602,24 +1582,24 @@ namespace PepperDash.Essentials
} }
} }
/// <summary>
/// Get the room bridge with the provided key
/// </summary>
/// <param name="key">The key of the room bridge</param>
public MobileControlBridgeBase GetRoomBridge(string key) public MobileControlBridgeBase GetRoomBridge(string key)
{ {
return _roomBridges.FirstOrDefault((r) => r.RoomKey.Equals(key)); return _roomBridges.FirstOrDefault((r) => r.RoomKey.Equals(key));
} }
/// <summary> /// <summary>
/// GetRoomMessenger method /// Get the room messenger with the provided key
/// </summary> /// </summary>
/// <param name="key">The Key of the rooom messenger</param>
public IMobileControlRoomMessenger GetRoomMessenger(string key) public IMobileControlRoomMessenger GetRoomMessenger(string key)
{ {
return _roomBridges.FirstOrDefault((r) => r.RoomKey.Equals(key)); return _roomBridges.FirstOrDefault((r) => r.RoomKey.Equals(key));
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Bridge_ConfigurationIsReady(object sender, EventArgs e) private void Bridge_ConfigurationIsReady(object sender, EventArgs e)
{ {
this.LogDebug("Bridge ready. Registering"); this.LogDebug("Bridge ready. Registering");
@@ -1640,10 +1620,6 @@ namespace PepperDash.Essentials
} }
} }
/// <summary>
///
/// </summary>
/// <param name="o"></param>
private void ReconnectToServerTimerCallback(object o) private void ReconnectToServerTimerCallback(object o)
{ {
this.LogDebug("Attempting to reconnect to server..."); this.LogDebug("Attempting to reconnect to server...");
@@ -1651,9 +1627,6 @@ namespace PepperDash.Essentials
ConnectWebsocketClient(); ConnectWebsocketClient();
} }
/// <summary>
/// Verifies system connection with servers
/// </summary>
private void AuthorizeSystem(string code) private void AuthorizeSystem(string code)
{ {
if ( if (
@@ -1698,9 +1671,6 @@ namespace PepperDash.Essentials
}); });
} }
/// <summary>
/// Dumps info in response to console command.
/// </summary>
private void ShowInfo() private void ShowInfo()
{ {
var url = Config != null ? Host : "No config"; var url = Config != null ? Host : "No config";
@@ -1766,38 +1736,37 @@ namespace PepperDash.Essentials
"\r\n UI Client Info:\r\n" + "\r\n UI Client Info:\r\n" +
" Tokens Defined: {0}\r\n" + " Tokens Defined: {0}\r\n" +
" Clients Connected: {1}\r\n", " Clients Connected: {1}\r\n",
_directServer.UiClients.Count, _directServer.UiClientContexts.Count,
_directServer.ConnectedUiClientsCount _directServer.ConnectedUiClientsCount
); );
var clientNo = 1; var clientNo = 1;
foreach (var clientContext in _directServer.UiClients) foreach (var clientContext in _directServer.UiClientContexts)
{ {
var isAlive = false; var clients = _directServer.UiClients.Values.Where(c => c.Token == clientContext.Value.Token.Token);
var duration = "Not Connected";
if (clientContext.Value.Client != null)
{
isAlive = clientContext.Value.Client.Context.WebSocket.IsAlive;
duration = clientContext.Value.Client.ConnectedDuration.ToString();
}
CrestronConsole.ConsoleCommandResponse( CrestronConsole.ConsoleCommandResponse(
"\r\nClient {0}:\r\n" + $"\r\nClient {clientNo}:\r\n" +
"Room Key: {1}\r\n" + $" Room Key: {clientContext.Value.Token.RoomKey}\r\n" +
"Touchpanel Key: {6}\r\n" + $" Touchpanel Key: {clientContext.Value.Token.TouchpanelKey}\r\n" +
"Token: {2}\r\n" + $" Token: {clientContext.Key}\r\n" +
"Client URL: {3}\r\n" + $" Client URL: {_directServer.UserAppUrlPrefix}{clientContext.Key}\r\n" +
"Connected: {4}\r\n" + $" Clients:\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
); );
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++; clientNo++;
} }
} }
@@ -1811,7 +1780,7 @@ namespace PepperDash.Essentials
} }
/// <summary> /// <summary>
/// RegisterSystemToServer method /// Register this system to the Mobile Control Edge Server
/// </summary> /// </summary>
public void RegisterSystemToServer() public void RegisterSystemToServer()
{ {
@@ -1835,9 +1804,6 @@ namespace PepperDash.Essentials
ConnectWebsocketClient(); ConnectWebsocketClient();
} }
/// <summary>
/// Connects the Websocket Client
/// </summary>
private void ConnectWebsocketClient() private void ConnectWebsocketClient()
{ {
try try
@@ -1878,9 +1844,6 @@ namespace PepperDash.Essentials
} }
} }
/// <summary>
/// Attempts to connect the websocket
/// </summary>
private void TryConnect() private void TryConnect()
{ {
try try
@@ -1910,9 +1873,6 @@ namespace PepperDash.Essentials
} }
} }
/// <summary>
/// Gracefully handles conect failures by reconstructing the ws client and starting the reconnect timer
/// </summary>
private void HandleConnectFailure() private void HandleConnectFailure()
{ {
_wsClient2 = null; _wsClient2 = null;
@@ -1944,11 +1904,6 @@ namespace PepperDash.Essentials
StartServerReconnectTimer(); StartServerReconnectTimer();
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void HandleOpen(object sender, EventArgs e) private void HandleOpen(object sender, EventArgs e)
{ {
StopServerReconnectTimer(); StopServerReconnectTimer();
@@ -1957,11 +1912,6 @@ namespace PepperDash.Essentials
SendMessageObject(new MobileControlMessage { Type = "hello" }); SendMessageObject(new MobileControlMessage { Type = "hello" });
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void HandleMessage(object sender, MessageEventArgs e) private void HandleMessage(object sender, MessageEventArgs e)
{ {
if (e.IsPing) if (e.IsPing)
@@ -1978,11 +1928,6 @@ namespace PepperDash.Essentials
} }
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void HandleError(object sender, ErrorEventArgs e) private void HandleError(object sender, ErrorEventArgs e)
{ {
this.LogError("Websocket error {0}", e.Message); this.LogError("Websocket error {0}", e.Message);
@@ -1991,11 +1936,6 @@ namespace PepperDash.Essentials
StartServerReconnectTimer(); StartServerReconnectTimer();
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void HandleClose(object sender, CloseEventArgs e) private void HandleClose(object sender, CloseEventArgs e)
{ {
this.LogDebug( this.LogDebug(
@@ -2016,9 +1956,6 @@ namespace PepperDash.Essentials
StartServerReconnectTimer(); StartServerReconnectTimer();
} }
/// <summary>
/// After a "hello" from the server, sends config and stuff
/// </summary>
private void SendInitialMessage() private void SendInitialMessage()
{ {
this.LogInformation("Sending initial join message"); this.LogInformation("Sending initial join message");
@@ -2045,7 +1982,7 @@ namespace PepperDash.Essentials
} }
/// <summary> /// <summary>
/// GetConfigWithPluginVersion method /// Get the Essentials configuration with version data
/// </summary> /// </summary>
public MobileControlEssentialsConfig GetConfigWithPluginVersion() public MobileControlEssentialsConfig GetConfigWithPluginVersion()
{ {
@@ -2080,8 +2017,13 @@ namespace PepperDash.Essentials
} }
/// <summary> /// <summary>
/// SetClientUrl method /// Set the Client URL for a given room
/// </summary> /// </summary>
/// <param name="path">new App URL</param>
/// <param name="roomKey">room key. Default is null</param>
/// <remarks>
/// If roomKey is null, the URL will be set for the entire system.
/// </remarks>
public void SetClientUrl(string path, string roomKey = null) public void SetClientUrl(string path, string roomKey = null)
{ {
var message = new MobileControlMessage var message = new MobileControlMessage
@@ -2097,9 +2039,6 @@ namespace PepperDash.Essentials
/// Sends any object type to server /// Sends any object type to server
/// </summary> /// </summary>
/// <param name="o"></param> /// <param name="o"></param>
/// <summary>
/// SendMessageObject method
/// </summary>
public void SendMessageObject(IMobileControlMessage o) public void SendMessageObject(IMobileControlMessage o)
{ {
@@ -2123,8 +2062,9 @@ namespace PepperDash.Essentials
/// <summary> /// <summary>
/// SendMessageObjectToDirectClient method /// Send a message to a client using the Direct Server
/// </summary> /// </summary>
/// <param name="o">object to send</param>
public void SendMessageObjectToDirectClient(object o) public void SendMessageObjectToDirectClient(object o)
{ {
if ( if (
@@ -2137,10 +2077,6 @@ namespace PepperDash.Essentials
} }
} }
/// <summary>
/// Disconnects the Websocket Client and stops the heartbeat timer
/// </summary>
private void CleanUpWebsocketClient() private void CleanUpWebsocketClient()
{ {
if (_wsClient2 == null) if (_wsClient2 == null)
@@ -2198,9 +2134,6 @@ namespace PepperDash.Essentials
} }
} }
/// <summary>
///
/// </summary>
private void StartServerReconnectTimer() private void StartServerReconnectTimer()
{ {
StopServerReconnectTimer(); StopServerReconnectTimer();
@@ -2211,9 +2144,6 @@ namespace PepperDash.Essentials
this.LogDebug("Reconnect Timer Started."); this.LogDebug("Reconnect Timer Started.");
} }
/// <summary>
/// Does what it says
/// </summary>
private void StopServerReconnectTimer() private void StopServerReconnectTimer()
{ {
if (_serverReconnectTimer == null) if (_serverReconnectTimer == null)
@@ -2224,10 +2154,6 @@ namespace PepperDash.Essentials
_serverReconnectTimer = null; _serverReconnectTimer = null;
} }
/// <summary>
/// Resets reconnect timer and updates usercode
/// </summary>
/// <param name="content"></param>
private void HandleHeartBeat(JToken content) private void HandleHeartBeat(JToken content)
{ {
SendMessageObject(new MobileControlMessage { Type = "/system/heartbeatAck" }); SendMessageObject(new MobileControlMessage { Type = "/system/heartbeatAck" });
@@ -2337,16 +2263,13 @@ namespace PepperDash.Essentials
} }
/// <summary> /// <summary>
/// HandleClientMessage method /// Enqueue an incoming message for processing
/// </summary> /// </summary>
public void HandleClientMessage(string message) public void HandleClientMessage(string message)
{ {
_receiveQueue.Enqueue(new ProcessStringMessage(message, ParseStreamRx)); _receiveQueue.Enqueue(new ProcessStringMessage(message, ParseStreamRx));
} }
/// <summary>
///
/// </summary>
private void ParseStreamRx(string messageText) private void ParseStreamRx(string messageText)
{ {
if (string.IsNullOrEmpty(messageText)) if (string.IsNullOrEmpty(messageText))
@@ -2414,10 +2337,33 @@ namespace PepperDash.Essentials
foreach (var handler in handlers) foreach (var handler in handlers)
{ {
Task.Run( Task.Run(async () =>
() => {
handler.Action(message.Type, message.ClientId, message.Content) 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; break;
@@ -2433,10 +2379,6 @@ namespace PepperDash.Essentials
} }
} }
/// <summary>
///
/// </summary>
/// <param name="s"></param>
private void TestHttpRequest(string s) private void TestHttpRequest(string s)
{ {
{ {

View File

@@ -1,23 +1,35 @@
using PepperDash.Core; using System;
using PepperDash.Core;
using PepperDash.Core.Logging; using PepperDash.Core.Logging;
using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.AppServer.Messengers;
using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Core.DeviceTypeInterfaces;
using System;
namespace PepperDash.Essentials.RoomBridges namespace PepperDash.Essentials.RoomBridges
{ {
/// <summary> /// <summary>
/// /// Base class for a Mobile Control Bridge that's used to control a room
/// </summary> /// </summary>
public abstract class MobileControlBridgeBase : MessengerBase, IMobileControlRoomMessenger public abstract class MobileControlBridgeBase : MessengerBase, IMobileControlRoomMessenger
{ {
/// <summary>
/// Triggered when the user Code changes
/// </summary>
public event EventHandler<EventArgs> UserCodeChanged; public event EventHandler<EventArgs> UserCodeChanged;
/// <summary>
/// Triggered when a user should be prompted for the new code
/// </summary>
public event EventHandler<EventArgs> UserPromptedForCode; public event EventHandler<EventArgs> UserPromptedForCode;
/// <summary>
/// Triggered when a client joins to control this room
/// </summary>
public event EventHandler<EventArgs> ClientJoined; public event EventHandler<EventArgs> ClientJoined;
/// <summary>
/// Triggered when the App URL for this room changes
/// </summary>
public event EventHandler<EventArgs> AppUrlChanged; public event EventHandler<EventArgs> AppUrlChanged;
/// <summary> /// <summary>
@@ -49,15 +61,32 @@ namespace PepperDash.Essentials.RoomBridges
/// </summary> /// </summary>
public string McServerUrl { get; private set; } public string McServerUrl { get; private set; }
/// <summary>
/// Room Name
/// </summary>
public abstract string RoomName { get; } public abstract string RoomName { get; }
/// <summary>
/// Room key
/// </summary>
public abstract string RoomKey { get; } public abstract string RoomKey { get; }
/// <summary>
/// Create an instance of the <see cref="MobileControlBridgeBase"/> class
/// </summary>
/// <param name="key">The unique key for this bridge</param>
/// <param name="messagePath">The message path for this bridge</param>
protected MobileControlBridgeBase(string key, string messagePath) protected MobileControlBridgeBase(string key, string messagePath)
: base(key, messagePath) : base(key, messagePath)
{ {
} }
/// <summary>
/// Create an instance of the <see cref="MobileControlBridgeBase"/> class
/// </summary>
/// <param name="key">The unique key for this bridge</param>
/// <param name="messagePath">The message path for this bridge</param>
/// <param name="device">The device associated with this bridge</param>
protected MobileControlBridgeBase(string key, string messagePath, IKeyName device) protected MobileControlBridgeBase(string key, string messagePath, IKeyName device)
: base(key, messagePath, device) : base(key, messagePath, device)
{ {
@@ -110,6 +139,10 @@ namespace PepperDash.Essentials.RoomBridges
SetUserCode(code); SetUserCode(code);
} }
/// <summary>
/// Update the App Url with the provided URL
/// </summary>
/// <param name="url">The new App URL</param>
public virtual void UpdateAppUrl(string url) public virtual void UpdateAppUrl(string url)
{ {
AppUrl = url; AppUrl = url;
@@ -137,16 +170,25 @@ namespace PepperDash.Essentials.RoomBridges
OnUserCodeChanged(); OnUserCodeChanged();
} }
/// <summary>
/// Trigger the UserCodeChanged event
/// </summary>
protected void OnUserCodeChanged() protected void OnUserCodeChanged()
{ {
UserCodeChanged?.Invoke(this, new EventArgs()); UserCodeChanged?.Invoke(this, new EventArgs());
} }
/// <summary>
/// Trigger the UserPromptedForCode event
/// </summary>
protected void OnUserPromptedForCode() protected void OnUserPromptedForCode()
{ {
UserPromptedForCode?.Invoke(this, new EventArgs()); UserPromptedForCode?.Invoke(this, new EventArgs());
} }
/// <summary>
/// Trigger the ClientJoined event
/// </summary>
protected void OnClientJoined() protected void OnClientJoined()
{ {
ClientJoined?.Invoke(this, new EventArgs()); ClientJoined?.Invoke(this, new EventArgs());

View File

@@ -41,24 +41,37 @@ namespace PepperDash.Essentials.RoomBridges
/// </summary> /// </summary>
public string DefaultRoomKey { get; private set; } public string DefaultRoomKey { get; private set; }
/// <summary> /// <summary>
/// /// Gets the name of the room
/// </summary> /// </summary>
public override string RoomName public override string RoomName
{ {
get { return Room.Name; } get { return Room.Name; }
} }
/// <summary>
/// Gets the key of the room
/// </summary>
public override string RoomKey public override string RoomKey
{ {
get { return Room.Key; } get { return Room.Key; }
} }
/// <summary>
/// Initializes a new instance of the <see cref="MobileControlEssentialsRoomBridge"/> class with the specified room
/// </summary>
/// <param name="room">The essentials room to bridge</param>
public MobileControlEssentialsRoomBridge(IEssentialsRoom room) : public MobileControlEssentialsRoomBridge(IEssentialsRoom room) :
this($"mobileControlBridge-{room.Key}", room.Key, room) this($"mobileControlBridge-{room.Key}", room.Key, room)
{ {
Room = room; Room = room;
} }
/// <summary>
/// Initializes a new instance of the <see cref="MobileControlEssentialsRoomBridge"/> class with the specified parameters
/// </summary>
/// <param name="key">The unique key for this bridge</param>
/// <param name="roomKey">The key of the room to bridge</param>
/// <param name="room">The essentials room to bridge</param>
public MobileControlEssentialsRoomBridge(string key, string roomKey, IEssentialsRoom room) : base(key, $"/room/{room.Key}", room as Device) public MobileControlEssentialsRoomBridge(string key, string roomKey, IEssentialsRoom room) : base(key, $"/room/{room.Key}", room as Device)
{ {
DefaultRoomKey = roomKey; DefaultRoomKey = roomKey;
@@ -66,7 +79,9 @@ namespace PepperDash.Essentials.RoomBridges
AddPreActivationAction(GetRoom); AddPreActivationAction(GetRoom);
} }
/// <summary>
/// Registers all message handling actions with the AppServer for this room bridge
/// </summary>
protected override void RegisterActions() protected override void RegisterActions()
{ {
// we add actions to the messaging system with a path, and a related action. Custom action // 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; Room = tempRoom;
} }
/// <summary>
/// Handles user code changes and generates QR code URL
/// </summary>
protected override void UserCodeChange() protected override void UserCodeChange()
{ {
Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Server user code changed: {userCode}", this, UserCode); 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)] [JsonProperty("configuration", NullValueHandling = NullValueHandling.Ignore)]
public RoomConfiguration Configuration { get; set; } public RoomConfiguration Configuration { get; set; }
/// <summary>
/// Gets or sets the activity mode of the room
/// </summary>
[JsonProperty("activityMode", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("activityMode", NullValueHandling = NullValueHandling.Ignore)]
public int? ActivityMode { get; set; } public int? ActivityMode { get; set; }
/// <summary>
/// Gets or sets whether advanced sharing is active
/// </summary>
[JsonProperty("advancedSharingActive", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("advancedSharingActive", NullValueHandling = NullValueHandling.Ignore)]
public bool? AdvancedSharingActive { get; set; } public bool? AdvancedSharingActive { get; set; }
/// <summary>
/// Gets or sets whether the room is powered on
/// </summary>
[JsonProperty("isOn", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("isOn", NullValueHandling = NullValueHandling.Ignore)]
public bool? IsOn { get; set; } public bool? IsOn { get; set; }
/// <summary>
/// Gets or sets whether the room is warming up
/// </summary>
[JsonProperty("isWarmingUp", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("isWarmingUp", NullValueHandling = NullValueHandling.Ignore)]
public bool? IsWarmingUp { get; set; } public bool? IsWarmingUp { get; set; }
/// <summary>
/// Gets or sets whether the room is cooling down
/// </summary>
[JsonProperty("isCoolingDown", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("isCoolingDown", NullValueHandling = NullValueHandling.Ignore)]
public bool? IsCoolingDown { get; set; } public bool? IsCoolingDown { get; set; }
@@ -834,9 +867,15 @@ namespace PepperDash.Essentials.RoomBridges
[JsonProperty("share", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("share", NullValueHandling = NullValueHandling.Ignore)]
public ShareState Share { get; set; } public ShareState Share { get; set; }
/// <summary>
/// Gets or sets the volume controls collection
/// </summary>
[JsonProperty("volumes", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("volumes", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, Volume> Volumes { get; set; } public Dictionary<string, Volume> Volumes { get; set; }
/// <summary>
/// Gets or sets whether the room is in a call
/// </summary>
[JsonProperty("isInCall", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("isInCall", NullValueHandling = NullValueHandling.Ignore)]
public bool? IsInCall { get; set; } public bool? IsInCall { get; set; }
} }
@@ -853,9 +892,15 @@ namespace PepperDash.Essentials.RoomBridges
[JsonProperty("currentShareText", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("currentShareText", NullValueHandling = NullValueHandling.Ignore)]
public string CurrentShareText { get; set; } public string CurrentShareText { get; set; }
/// <summary>
/// Gets or sets whether sharing is enabled
/// </summary>
[JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)]
public bool? Enabled { get; set; } public bool? Enabled { get; set; }
/// <summary>
/// Gets or sets whether content is currently being shared
/// </summary>
[JsonProperty("isSharing", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("isSharing", NullValueHandling = NullValueHandling.Ignore)]
public bool? IsSharing { get; set; } public bool? IsSharing { get; set; }
} }
@@ -865,24 +910,45 @@ namespace PepperDash.Essentials.RoomBridges
/// </summary> /// </summary>
public class RoomConfiguration public class RoomConfiguration
{ {
/// <summary>
/// Gets or sets whether the room has video conferencing capabilities
/// </summary>
[JsonProperty("hasVideoConferencing", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("hasVideoConferencing", NullValueHandling = NullValueHandling.Ignore)]
public bool? HasVideoConferencing { get; set; } public bool? HasVideoConferencing { get; set; }
/// <summary>
/// Gets or sets whether the video codec is a Zoom Room
/// </summary>
[JsonProperty("videoCodecIsZoomRoom", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("videoCodecIsZoomRoom", NullValueHandling = NullValueHandling.Ignore)]
public bool? VideoCodecIsZoomRoom { get; set; } public bool? VideoCodecIsZoomRoom { get; set; }
/// <summary>
/// Gets or sets whether the room has audio conferencing capabilities
/// </summary>
[JsonProperty("hasAudioConferencing", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("hasAudioConferencing", NullValueHandling = NullValueHandling.Ignore)]
public bool? HasAudioConferencing { get; set; } public bool? HasAudioConferencing { get; set; }
/// <summary>
/// Gets or sets whether the room has environmental controls (lighting, shades, etc.)
/// </summary>
[JsonProperty("hasEnvironmentalControls", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("hasEnvironmentalControls", NullValueHandling = NullValueHandling.Ignore)]
public bool? HasEnvironmentalControls { get; set; } public bool? HasEnvironmentalControls { get; set; }
/// <summary>
/// Gets or sets whether the room has camera controls
/// </summary>
[JsonProperty("hasCameraControls", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("hasCameraControls", NullValueHandling = NullValueHandling.Ignore)]
public bool? HasCameraControls { get; set; } public bool? HasCameraControls { get; set; }
/// <summary>
/// Gets or sets whether the room has set-top box controls
/// </summary>
[JsonProperty("hasSetTopBoxControls", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("hasSetTopBoxControls", NullValueHandling = NullValueHandling.Ignore)]
public bool? HasSetTopBoxControls { get; set; } public bool? HasSetTopBoxControls { get; set; }
/// <summary>
/// Gets or sets whether the room has routing controls
/// </summary>
[JsonProperty("hasRoutingControls", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("hasRoutingControls", NullValueHandling = NullValueHandling.Ignore)]
public bool? HasRoutingControls { get; set; } public bool? HasRoutingControls { get; set; }
@@ -949,6 +1015,9 @@ namespace PepperDash.Essentials.RoomBridges
[JsonProperty("defaultDisplayKey", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("defaultDisplayKey", NullValueHandling = NullValueHandling.Ignore)]
public string DefaultDisplayKey { get; set; } public string DefaultDisplayKey { get; set; }
/// <summary>
/// Gets or sets the destinations dictionary keyed by destination type
/// </summary>
[JsonProperty("destinations", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("destinations", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<eSourceListItemDestinationTypes, string> Destinations { get; set; } public Dictionary<eSourceListItemDestinationTypes, string> Destinations { get; set; }
@@ -959,9 +1028,15 @@ namespace PepperDash.Essentials.RoomBridges
[JsonProperty("environmentalDevices", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("environmentalDevices", NullValueHandling = NullValueHandling.Ignore)]
public List<EnvironmentalDeviceConfiguration> EnvironmentalDevices { get; set; } public List<EnvironmentalDeviceConfiguration> EnvironmentalDevices { get; set; }
/// <summary>
/// Gets or sets the source list for the room
/// </summary>
[JsonProperty("sourceList", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("sourceList", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, SourceListItem> SourceList { get; set; } public Dictionary<string, SourceListItem> SourceList { get; set; }
/// <summary>
/// Gets or sets the destination list for the room
/// </summary>
[JsonProperty("destinationList", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("destinationList", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, DestinationListItem> DestinationList { get; set; } public Dictionary<string, DestinationListItem> DestinationList { get; set; }
@@ -972,6 +1047,9 @@ namespace PepperDash.Essentials.RoomBridges
[JsonProperty("audioControlPointList", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("audioControlPointList", NullValueHandling = NullValueHandling.Ignore)]
public AudioControlPointListItem AudioControlPointList { get; set; } public AudioControlPointListItem AudioControlPointList { get; set; }
/// <summary>
/// Gets or sets the camera list for the room
/// </summary>
[JsonProperty("cameraList", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("cameraList", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, CameraListItem> CameraList { get; set; } public Dictionary<string, CameraListItem> CameraList { get; set; }
@@ -1004,9 +1082,15 @@ namespace PepperDash.Essentials.RoomBridges
[JsonProperty("uiBehavior", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("uiBehavior", NullValueHandling = NullValueHandling.Ignore)]
public EssentialsRoomUiBehaviorConfig UiBehavior { get; set; } public EssentialsRoomUiBehaviorConfig UiBehavior { get; set; }
/// <summary>
/// Gets or sets whether the room supports advanced sharing features
/// </summary>
[JsonProperty("supportsAdvancedSharing", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("supportsAdvancedSharing", NullValueHandling = NullValueHandling.Ignore)]
public bool? SupportsAdvancedSharing { get; set; } public bool? SupportsAdvancedSharing { get; set; }
/// <summary>
/// Gets or sets whether the user can change the share mode
/// </summary>
[JsonProperty("userCanChangeShareMode", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("userCanChangeShareMode", NullValueHandling = NullValueHandling.Ignore)]
public bool? UserCanChangeShareMode { get; set; } public bool? UserCanChangeShareMode { get; set; }
@@ -1017,6 +1101,9 @@ namespace PepperDash.Essentials.RoomBridges
[JsonProperty("roomCombinerKey", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("roomCombinerKey", NullValueHandling = NullValueHandling.Ignore)]
public string RoomCombinerKey { get; set; } public string RoomCombinerKey { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="RoomConfiguration"/> class
/// </summary>
public RoomConfiguration() public RoomConfiguration()
{ {
Destinations = new Dictionary<eSourceListItemDestinationTypes, string>(); Destinations = new Dictionary<eSourceListItemDestinationTypes, string>();
@@ -1046,6 +1133,11 @@ namespace PepperDash.Essentials.RoomBridges
[JsonProperty("deviceType", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("deviceType", NullValueHandling = NullValueHandling.Ignore)]
public eEnvironmentalDeviceTypes DeviceType { get; private set; } public eEnvironmentalDeviceTypes DeviceType { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="EnvironmentalDeviceConfiguration"/> class
/// </summary>
/// <param name="key">The device key</param>
/// <param name="type">The environmental device type</param>
public EnvironmentalDeviceConfiguration(string key, eEnvironmentalDeviceTypes type) public EnvironmentalDeviceConfiguration(string key, eEnvironmentalDeviceTypes type)
{ {
DeviceKey = key; DeviceKey = key;
@@ -1054,14 +1146,29 @@ namespace PepperDash.Essentials.RoomBridges
} }
/// <summary> /// <summary>
/// Enumeration of eEnvironmentalDeviceTypes values /// Enumeration of environmental device types
/// </summary> /// </summary>
public enum eEnvironmentalDeviceTypes public enum eEnvironmentalDeviceTypes
{ {
/// <summary>
/// No environmental device type specified
/// </summary>
None, None,
/// <summary>
/// Lighting device type
/// </summary>
Lighting, Lighting,
/// <summary>
/// Shade device type
/// </summary>
Shade, Shade,
/// <summary>
/// Shade controller device type
/// </summary>
ShadeController, ShadeController,
/// <summary>
/// Relay device type
/// </summary>
Relay, Relay,
} }

View File

@@ -1,18 +1,22 @@
using PepperDash.Core; using System;
using System;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using PepperDash.Core;
namespace PepperDash.Essentials.Services namespace PepperDash.Essentials.Services
{ {
/// <summary> /// <summary>
/// Represents a MobileControlApiService /// Service for interacting with a Mobile Control Edge server instance
/// </summary> /// </summary>
public class MobileControlApiService public class MobileControlApiService
{ {
private readonly HttpClient _client; private readonly HttpClient _client;
/// <summary>
/// Create an instance of the <see cref="MobileControlApiService"/> class.
/// </summary>
/// <param name="apiUrl">Mobile Control Edge API URL</param>
public MobileControlApiService(string apiUrl) public MobileControlApiService(string apiUrl)
{ {
var handler = new HttpClientHandler var handler = new HttpClientHandler
@@ -24,6 +28,13 @@ namespace PepperDash.Essentials.Services
_client = new HttpClient(handler); _client = new HttpClient(handler);
} }
/// <summary>
/// Send authorization request to Mobile Control Edge Server
/// </summary>
/// <param name="apiUrl">Mobile Control Edge API URL</param>
/// <param name="grantCode">Grant code for authorization</param>
/// <param name="systemUuid">System UUID for authorization</param>
/// <returns>Authorization response</returns>
public async Task<AuthorizationResponse> SendAuthorizationRequest(string apiUrl, string grantCode, string systemUuid) public async Task<AuthorizationResponse> SendAuthorizationRequest(string apiUrl, string grantCode, string systemUuid)
{ {
try try

View File

@@ -7,8 +7,15 @@ namespace PepperDash.Essentials.Touchpanel
/// </summary> /// </summary>
public interface ITheme : IKeyed public interface ITheme : IKeyed
{ {
/// <summary>
/// Current theme
/// </summary>
string Theme { get; } string Theme { get; }
/// <summary>
/// Set the theme with the given value
/// </summary>
/// <param name="theme">The theme to set</param>
void UpdateTheme(string theme); void UpdateTheme(string theme);
} }
} }

View File

@@ -8,12 +8,24 @@ namespace PepperDash.Essentials.Touchpanel
/// </summary> /// </summary>
public interface ITswAppControl : IKeyed public interface ITswAppControl : IKeyed
{ {
/// <summary>
/// Updates when the Zoom Room Control Application opens or closes
/// </summary>
BoolFeedback AppOpenFeedback { get; } BoolFeedback AppOpenFeedback { get; }
/// <summary>
/// Hide the Zoom App and show the User Control Application
/// </summary>
void HideOpenApp(); void HideOpenApp();
/// <summary>
/// Close the Zoom App and show the User Control Application
/// </summary>
void CloseOpenApp(); void CloseOpenApp();
/// <summary>
/// Open the Zoom App
/// </summary>
void OpenApp(); void OpenApp();
} }
@@ -22,10 +34,19 @@ namespace PepperDash.Essentials.Touchpanel
/// </summary> /// </summary>
public interface ITswZoomControl : IKeyed public interface ITswZoomControl : IKeyed
{ {
/// <summary>
/// Updates when Zoom has an incoming call
/// </summary>
BoolFeedback ZoomIncomingCallFeedback { get; } BoolFeedback ZoomIncomingCallFeedback { get; }
/// <summary>
/// Updates when Zoom is in a call
/// </summary>
BoolFeedback ZoomInCallFeedback { get; } BoolFeedback ZoomInCallFeedback { get; }
/// <summary>
/// End a Zoom Call
/// </summary>
void EndZoomCall(); void EndZoomCall();
} }
} }

View File

@@ -7,17 +7,24 @@ using PepperDash.Essentials.AppServer.Messengers;
namespace PepperDash.Essentials.Touchpanel namespace PepperDash.Essentials.Touchpanel
{ {
/// <summary> /// <summary>
/// Represents a ITswAppControlMessenger /// Messenger for controlling the Zoom App on a TSW Panel that supports the Zoom Room Control Application
/// </summary> /// </summary>
public class ITswAppControlMessenger : MessengerBase public class ITswAppControlMessenger : MessengerBase
{ {
private readonly ITswAppControl _appControl; private readonly ITswAppControl _appControl;
/// <summary>
/// Create an instance of the <see cref="ITswAppControlMessenger"/> class.
/// </summary>
/// <param name="key">The key for this messenger</param>
/// <param name="messagePath">The message path for this messenger</param>
/// <param name="device">The device for this messenger</param>
public ITswAppControlMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) public ITswAppControlMessenger(string key, string messagePath, Device device) : base(key, messagePath, device)
{ {
_appControl = device as ITswAppControl; _appControl = device as ITswAppControl;
} }
/// <inheritdoc />
protected override void RegisterActions() protected override void RegisterActions()
{ {
if (_appControl == null) if (_appControl == null)
@@ -43,14 +50,14 @@ namespace PepperDash.Essentials.Touchpanel
}; };
} }
private void SendFullStatus() private void SendFullStatus(string id = null)
{ {
var message = new TswAppStateMessage var message = new TswAppStateMessage
{ {
AppOpen = _appControl.AppOpenFeedback.BoolValue, AppOpen = _appControl.AppOpenFeedback.BoolValue,
}; };
PostStatusMessage(message); PostStatusMessage(message, id);
} }
} }
@@ -59,6 +66,9 @@ namespace PepperDash.Essentials.Touchpanel
/// </summary> /// </summary>
public class TswAppStateMessage : DeviceStateMessageBase public class TswAppStateMessage : DeviceStateMessageBase
{ {
/// <summary>
/// True if the Zoom app is open on a TSW panel
/// </summary>
[JsonProperty("appOpen", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("appOpen", NullValueHandling = NullValueHandling.Ignore)]
public bool? AppOpen { get; set; } public bool? AppOpen { get; set; }
} }

View File

@@ -8,17 +8,24 @@ using PepperDash.Essentials.AppServer.Messengers;
namespace PepperDash.Essentials.Touchpanel namespace PepperDash.Essentials.Touchpanel
{ {
/// <summary> /// <summary>
/// Represents a ITswZoomControlMessenger /// Messenger to handle Zoom status and control for a TSW panel that supports the Zoom Application
/// </summary> /// </summary>
public class ITswZoomControlMessenger : MessengerBase public class ITswZoomControlMessenger : MessengerBase
{ {
private readonly ITswZoomControl _zoomControl; private readonly ITswZoomControl _zoomControl;
/// <summary>
/// Create an instance of the <see cref="ITswZoomControlMessenger"/> class for the given device
/// </summary>
/// <param name="key">The key for this messenger</param>
/// <param name="messagePath">The message path for this messenger</param>
/// <param name="device">The device for this messenger</param>
public ITswZoomControlMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) public ITswZoomControlMessenger(string key, string messagePath, Device device) : base(key, messagePath, device)
{ {
_zoomControl = device as ITswZoomControl; _zoomControl = device as ITswZoomControl;
} }
/// <inheritdoc />
protected override void RegisterActions() protected override void RegisterActions()
{ {
if (_zoomControl == null) if (_zoomControl == null)
@@ -27,7 +34,9 @@ namespace PepperDash.Essentials.Touchpanel
return; return;
} }
AddAction($"/fullStatus", (id, context) => SendFullStatus()); AddAction($"/fullStatus", (id, context) => SendFullStatus(id));
AddAction($"/zoomStatus", (id, content) => SendFullStatus(id));
AddAction($"/endCall", (id, context) => _zoomControl.EndZoomCall()); 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 var message = new TswZoomStateMessage
{ {
@@ -61,7 +70,7 @@ namespace PepperDash.Essentials.Touchpanel
IncomingCall = _zoomControl?.ZoomIncomingCallFeedback.BoolValue IncomingCall = _zoomControl?.ZoomIncomingCallFeedback.BoolValue
}; };
PostStatusMessage(message); PostStatusMessage(message, id);
} }
} }
@@ -70,9 +79,16 @@ namespace PepperDash.Essentials.Touchpanel
/// </summary> /// </summary>
public class TswZoomStateMessage : DeviceStateMessageBase public class TswZoomStateMessage : DeviceStateMessageBase
{ {
/// <summary>
/// True if the panel is in a Zoom call
/// </summary>
[JsonProperty("inCall", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("inCall", NullValueHandling = NullValueHandling.Ignore)]
public bool? InCall { get; set; } public bool? InCall { get; set; }
/// <summary>
/// True if there is an incoming Zoom call
/// </summary>
[JsonProperty("incomingCall", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("incomingCall", NullValueHandling = NullValueHandling.Ignore)]
public bool? IncomingCall { get; set; } public bool? IncomingCall { get; set; }
} }

View File

@@ -7,17 +7,24 @@ using PepperDash.Essentials.AppServer.Messengers;
namespace PepperDash.Essentials.Touchpanel namespace PepperDash.Essentials.Touchpanel
{ {
/// <summary> /// <summary>
/// Represents a ThemeMessenger /// Messenger to save the current theme (light/dark) and send to a device
/// </summary> /// </summary>
public class ThemeMessenger : MessengerBase public class ThemeMessenger : MessengerBase
{ {
private readonly ITheme _tpDevice; private readonly ITheme _tpDevice;
/// <summary>
/// Create an instance of the <see cref="ThemeMessenger"/> class
/// </summary>
/// <param name="key">The key for this messenger</param>
/// <param name="path">The path for this messenger</param>
/// <param name="device">The device for this messenger</param>
public ThemeMessenger(string key, string path, ITheme device) : base(key, path, device as Device) public ThemeMessenger(string key, string path, ITheme device) : base(key, path, device as Device)
{ {
_tpDevice = device; _tpDevice = device;
} }
/// <inheritdoc />
protected override void RegisterActions() protected override void RegisterActions()
{ {
AddAction("/fullStatus", (id, content) => AddAction("/fullStatus", (id, content) =>

View File

@@ -1,13 +1,9 @@
using Newtonsoft.Json; using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using PepperDash.Core; using PepperDash.Core;
using PepperDash.Core.Logging;
using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.AppServer.Messengers;
using PepperDash.Essentials.Core.Queues; using PepperDash.Essentials.Core.Queues;
using PepperDash.Essentials.WebSocketServer;
using Serilog.Events;
using System;
using System.Threading;
using WebSocketSharp; using WebSocketSharp;
namespace PepperDash.Essentials namespace PepperDash.Essentials
@@ -20,12 +16,22 @@ namespace PepperDash.Essentials
private readonly WebSocket _ws; private readonly WebSocket _ws;
private readonly object msgToSend; private readonly object msgToSend;
/// <summary>
/// Initialize a message to send
/// </summary>
/// <param name="msg">message object to send</param>
/// <param name="ws">WebSocket instance</param>
public TransmitMessage(object msg, WebSocket ws) public TransmitMessage(object msg, WebSocket ws)
{ {
_ws = ws; _ws = ws;
msgToSend = msg; msgToSend = msg;
} }
/// <summary>
/// Initialize a message to send
/// </summary>
/// <param name="msg">message object to send</param>
/// <param name="ws">WebSocket instance</param>
public TransmitMessage(DeviceStateMessageBase msg, WebSocket ws) public TransmitMessage(DeviceStateMessageBase msg, WebSocket ws)
{ {
_ws = ws; _ws = ws;
@@ -43,13 +49,13 @@ namespace PepperDash.Essentials
{ {
if (_ws == null) if (_ws == null)
{ {
Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Websocket client is null"); Debug.LogWarning("Cannot send message. Websocket client is null");
return; return;
} }
if (!_ws.IsAlive) 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; return;
} }
@@ -57,83 +63,14 @@ namespace PepperDash.Essentials
var message = JsonConvert.SerializeObject(msgToSend, Formatting.None, var message = JsonConvert.SerializeObject(msgToSend, Formatting.None,
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } }); 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); _ws.Send(message);
} }
catch (Exception ex) 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
}
/// <summary>
/// Represents a MessageToClients
/// </summary>
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
/// <summary>
/// Dispatch method
/// </summary>
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");
} }
} }
#endregion #endregion

View File

@@ -3,12 +3,19 @@ using System;
namespace PepperDash.Essentials namespace PepperDash.Essentials
{ {
/// <summary> /// <summary>
/// Represents a UserCodeChanged /// Defines the action to take when the User code changes
/// </summary> /// </summary>
public class UserCodeChanged public class UserCodeChanged
{ {
/// <summary>
/// Action to take when the User Code changes
/// </summary>
public Action<string, string> UpdateUserCode { get; private set; } public Action<string, string> UpdateUserCode { get; private set; }
/// <summary>
/// create an instance of the <see cref="UserCodeChanged"/> class
/// </summary>
/// <param name="updateMethod">action to take when the User Code changes</param>
public UserCodeChanged(Action<string, string> updateMethod) public UserCodeChanged(Action<string, string> updateMethod)
{ {
UpdateUserCode = updateMethod; UpdateUserCode = updateMethod;

View File

@@ -0,0 +1,97 @@
using PepperDash.Core;
using PepperDash.Core.Logging;
using WebSocketSharp;
namespace PepperDash.Essentials
{
/// <summary>
/// Utility functions for logging and other common tasks.
/// </summary>
public static class Utilities
{
private static int nextClientId = 0;
/// <summary>
/// Get the next unique client ID
/// </summary>
/// <returns>Client ID</returns>
public static int GetNextClientId()
{
nextClientId++;
return nextClientId;
}
/// <summary>
/// Converts a WebSocketServer LogData object to Essentials logging calls.
/// </summary>
/// <param name="data">The LogData object to convert.</param>
/// <param name="message">The log message.</param>
/// <param name="device">The device associated with the log message.</param>
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;
}
}
}
}

View File

@@ -15,15 +15,17 @@ namespace PepperDash.Essentials
[JsonProperty("master", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("master", NullValueHandling = NullValueHandling.Ignore)]
public Volume Master { get; set; } public Volume Master { get; set; }
/// <summary>
/// Aux Faders as configured in the room
/// </summary>
[JsonProperty("auxFaders", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("auxFaders", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, Volume> AuxFaders { get; set; } public Dictionary<string, Volume> AuxFaders { get; set; }
/// <summary>
/// Count of aux faders for this system
/// </summary>
[JsonProperty("numberOfAuxFaders", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("numberOfAuxFaders", NullValueHandling = NullValueHandling.Ignore)]
public int? NumberOfAuxFaders { get; set; } public int? NumberOfAuxFaders { get; set; }
public Volumes()
{
}
} }
/// <summary> /// <summary>
@@ -31,16 +33,21 @@ namespace PepperDash.Essentials
/// </summary> /// </summary>
public class Volume public class Volume
{ {
/// <summary> /// <summary>
/// Gets or sets the Key /// Gets or sets the Key
/// </summary> /// </summary>
[JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)]
public string Key { get; set; } public string Key { get; set; }
/// <summary>
/// Level for this volume object
/// </summary>
[JsonProperty("level", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("level", NullValueHandling = NullValueHandling.Ignore)]
public int? Level { get; set; } public int? Level { get; set; }
/// <summary>
/// True if this volume control is muted
/// </summary>
[JsonProperty("muted", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("muted", NullValueHandling = NullValueHandling.Ignore)]
public bool? Muted { get; set; } public bool? Muted { get; set; }
@@ -51,12 +58,21 @@ namespace PepperDash.Essentials
[JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)]
public string Label { get; set; } public string Label { get; set; }
/// <summary>
/// True if this volume object has mute control
/// </summary>
[JsonProperty("hasMute", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("hasMute", NullValueHandling = NullValueHandling.Ignore)]
public bool? HasMute { get; set; } public bool? HasMute { get; set; }
/// <summary>
/// True if this volume object has Privacy mute control
/// </summary>
[JsonProperty("hasPrivacyMute", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("hasPrivacyMute", NullValueHandling = NullValueHandling.Ignore)]
public bool? HasPrivacyMute { get; set; } public bool? HasPrivacyMute { get; set; }
/// <summary>
/// True if the privacy mute is muted
/// </summary>
[JsonProperty("privacyMuted", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("privacyMuted", NullValueHandling = NullValueHandling.Ignore)]
public bool? PrivacyMuted { get; set; } public bool? PrivacyMuted { get; set; }
@@ -68,6 +84,15 @@ namespace PepperDash.Essentials
[JsonProperty("muteIcon", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("muteIcon", NullValueHandling = NullValueHandling.Ignore)]
public string MuteIcon { get; set; } public string MuteIcon { get; set; }
/// <summary>
/// Create an instance of the <see cref="Volume" /> class
/// </summary>
/// <param name="key">The key for this volume object</param>
/// <param name="level">The level for this volume object</param>
/// <param name="muted">True if this volume control is muted</param>
/// <param name="label">The label for this volume object</param>
/// <param name="hasMute">True if this volume object has mute control</param>
/// <param name="muteIcon">The mute icon for this volume object</param>
public Volume(string key, int level, bool muted, string label, bool hasMute, string muteIcon) public Volume(string key, int level, bool muted, string label, bool hasMute, string muteIcon)
: this(key) : this(key)
{ {
@@ -78,18 +103,32 @@ namespace PepperDash.Essentials
MuteIcon = muteIcon; MuteIcon = muteIcon;
} }
/// <summary>
/// Create an instance of the <see cref="Volume" /> class
/// </summary>
/// <param name="key">The key for this volume object</param>
/// <param name="level">The level for this volume object</param>
public Volume(string key, int level) public Volume(string key, int level)
: this(key) : this(key)
{ {
Level = level; Level = level;
} }
/// <summary>
/// Create an instance of the <see cref="Volume" /> class
/// </summary>
/// <param name="key">The key for this volume object</param>
/// <param name="muted">True if this volume control is muted</param>
public Volume(string key, bool muted) public Volume(string key, bool muted)
: this(key) : this(key)
{ {
Muted = muted; Muted = muted;
} }
/// <summary>
/// Create an instance of the <see cref="Volume" /> class
/// </summary>
/// <param name="key">The key for this volume object</param>
public Volume(string key) public Volume(string key)
{ {
Key = key; Key = key;

View File

@@ -12,11 +12,20 @@ namespace PepperDash.Essentials.WebApiHandlers
public class ActionPathsHandler : WebApiBaseRequestHandler public class ActionPathsHandler : WebApiBaseRequestHandler
{ {
private readonly MobileControlSystemController mcController; private readonly MobileControlSystemController mcController;
/// <summary>
/// Create an instance of the <see cref="ActionPathsHandler"/> class.
/// </summary>
/// <param name="controller"></param>
public ActionPathsHandler(MobileControlSystemController controller) : base(true) public ActionPathsHandler(MobileControlSystemController controller) : base(true)
{ {
mcController = controller; mcController = controller;
} }
/// <summary>
/// Handle a request to get the action paths
/// </summary>
/// <param name="context">Request Context</param>
protected override void HandleGet(HttpCwsContext context) protected override void HandleGet(HttpCwsContext context)
{ {
var response = JsonConvert.SerializeObject(new ActionPathsResponse(mcController)); var response = JsonConvert.SerializeObject(new ActionPathsResponse(mcController));
@@ -37,9 +46,16 @@ namespace PepperDash.Essentials.WebApiHandlers
[JsonIgnore] [JsonIgnore]
private readonly MobileControlSystemController mcController; private readonly MobileControlSystemController mcController;
/// <summary>
/// Registered action paths for this system
/// </summary>
[JsonProperty("actionPaths")] [JsonProperty("actionPaths")]
public List<ActionPath> ActionPaths => mcController.GetActionDictionaryPaths().Select((path) => new ActionPath { MessengerKey = path.Item1, Path = path.Item2 }).ToList(); public List<ActionPath> ActionPaths => mcController.GetActionDictionaryPaths().Select((path) => new ActionPath { MessengerKey = path.Item1, Path = path.Item2 }).ToList();
/// <summary>
/// Create an instance of the <see cref="ActionPathsResponse"/> class.
/// </summary>
/// <param name="mcController"></param>
public ActionPathsResponse(MobileControlSystemController mcController) public ActionPathsResponse(MobileControlSystemController mcController)
{ {
this.mcController = mcController; this.mcController = mcController;

View File

@@ -1,10 +1,10 @@
using Crestron.SimplSharp.WebScripting; using System;
using System.Threading.Tasks;
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json; using Newtonsoft.Json;
using PepperDash.Core; using PepperDash.Core;
using PepperDash.Core.Web.RequestHandlers; using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web; using PepperDash.Essentials.Core.Web;
using System;
using System.Threading.Tasks;
namespace PepperDash.Essentials.WebApiHandlers namespace PepperDash.Essentials.WebApiHandlers
{ {
@@ -15,11 +15,20 @@ namespace PepperDash.Essentials.WebApiHandlers
{ {
private readonly MobileControlSystemController mcController; private readonly MobileControlSystemController mcController;
/// <summary>
/// Create an instance of the <see cref="MobileAuthRequestHandler"/> class.
/// </summary>
/// <param name="controller"></param>
public MobileAuthRequestHandler(MobileControlSystemController controller) : base(true) public MobileAuthRequestHandler(MobileControlSystemController controller) : base(true)
{ {
mcController = controller; mcController = controller;
} }
/// <summary>
/// Handle authorization request for this processor
/// </summary>
/// <param name="context">request context</param>
/// <returns>Task</returns>
protected override async Task HandlePost(HttpCwsContext context) protected override async Task HandlePost(HttpCwsContext context)
{ {
try try

View File

@@ -1,26 +1,35 @@
using Crestron.SimplSharp.WebScripting; using System;
using System.Collections.Generic;
using System.Linq;
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json; using Newtonsoft.Json;
using PepperDash.Core; using PepperDash.Core;
using PepperDash.Core.Web.RequestHandlers; using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.Config;
using PepperDash.Essentials.WebSocketServer; using PepperDash.Essentials.WebSocketServer;
using System;
using System.Collections.Generic;
using System.Linq;
namespace PepperDash.Essentials.WebApiHandlers namespace PepperDash.Essentials.WebApiHandlers
{ {
/// <summary> /// <summary>
/// Represents a MobileInfoHandler /// Represents a MobileInfoHandler. Used with the Essentials CWS API
/// </summary> /// </summary>
public class MobileInfoHandler : WebApiBaseRequestHandler public class MobileInfoHandler : WebApiBaseRequestHandler
{ {
private readonly MobileControlSystemController mcController; private readonly MobileControlSystemController mcController;
/// <summary>
/// Create an instance of the <see cref="MobileInfoHandler"/> class.
/// </summary>
/// <param name="controller"></param>
public MobileInfoHandler(MobileControlSystemController controller) : base(true) public MobileInfoHandler(MobileControlSystemController controller) : base(true)
{ {
mcController = controller; mcController = controller;
} }
/// <summary>
/// Get Mobile Control Information
/// </summary>
/// <param name="context"></param>
protected override void HandleGet(HttpCwsContext context) protected override void HandleGet(HttpCwsContext context)
{ {
try try
@@ -50,14 +59,22 @@ namespace PepperDash.Essentials.WebApiHandlers
[JsonIgnore] [JsonIgnore]
private readonly MobileControlSystemController mcController; private readonly MobileControlSystemController mcController;
/// <summary>
/// Edge Server. Null if edge server is disabled
/// </summary>
[JsonProperty("edgeServer", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("edgeServer", NullValueHandling = NullValueHandling.Ignore)]
public MobileControlEdgeServer EdgeServer => mcController.Config.EnableApiServer ? new MobileControlEdgeServer(mcController) : null; public MobileControlEdgeServer EdgeServer => mcController.Config.EnableApiServer ? new MobileControlEdgeServer(mcController) : null;
/// <summary>
/// Direct server. Null if the direct server is disabled
/// </summary>
[JsonProperty("directServer", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("directServer", NullValueHandling = NullValueHandling.Ignore)]
public MobileControlDirectServer DirectServer => mcController.Config.DirectServer.EnableDirectServer ? new MobileControlDirectServer(mcController.DirectServer) : null; public MobileControlDirectServer DirectServer => mcController.Config.DirectServer.EnableDirectServer ? new MobileControlDirectServer(mcController.DirectServer) : null;
/// <summary>
/// Create an instance of the <see cref="InformationResponse"/> class.
/// </summary>
/// <param name="controller"></param>
public InformationResponse(MobileControlSystemController controller) public InformationResponse(MobileControlSystemController controller)
{ {
mcController = controller; mcController = controller;
@@ -72,24 +89,46 @@ namespace PepperDash.Essentials.WebApiHandlers
[JsonIgnore] [JsonIgnore]
private readonly MobileControlSystemController mcController; private readonly MobileControlSystemController mcController;
/// <summary>
/// Mobile Control Edge Server address for this system
/// </summary>
[JsonProperty("serverAddress")] [JsonProperty("serverAddress")]
public string ServerAddress => mcController.Config == null ? "No Config" : mcController.Host; public string ServerAddress => mcController.Config == null ? "No Config" : mcController.Host;
/// <summary>
/// System Name for this system
/// </summary>
[JsonProperty("systemName")] [JsonProperty("systemName")]
public string SystemName => mcController.RoomBridges.Count > 0 ? mcController.RoomBridges[0].RoomName : "No Config"; public string SystemName => mcController.RoomBridges.Count > 0 ? mcController.RoomBridges[0].RoomName : "No Config";
/// <summary>
/// System URL for this system
/// </summary>
[JsonProperty("systemUrl")] [JsonProperty("systemUrl")]
public string SystemUrl => ConfigReader.ConfigObject.SystemUrl; public string SystemUrl => ConfigReader.ConfigObject.SystemUrl;
/// <summary>
/// User code to use in MC UI for this system
/// </summary>
[JsonProperty("userCode")] [JsonProperty("userCode")]
public string UserCode => mcController.RoomBridges.Count > 0 ? mcController.RoomBridges[0].UserCode : "Not available"; public string UserCode => mcController.RoomBridges.Count > 0 ? mcController.RoomBridges[0].UserCode : "Not available";
/// <summary>
/// True if connected to edge server
/// </summary>
[JsonProperty("connected")] [JsonProperty("connected")]
public bool Connected => mcController.Connected; public bool Connected => mcController.Connected;
/// <summary>
/// Seconds since last comms with edge server
/// </summary>
[JsonProperty("secondsSinceLastAck")] [JsonProperty("secondsSinceLastAck")]
public int SecondsSinceLastAck => (DateTime.Now - mcController.LastAckMessage).Seconds; public int SecondsSinceLastAck => (DateTime.Now - mcController.LastAckMessage).Seconds;
/// <summary>
/// Create an instance of the <see cref="MobileControlEdgeServer"/> class.
/// </summary>
/// <param name="controller">controller to use for this</param>
public MobileControlEdgeServer(MobileControlSystemController controller) public MobileControlEdgeServer(MobileControlSystemController controller)
{ {
mcController = controller; mcController = controller;
@@ -104,21 +143,43 @@ namespace PepperDash.Essentials.WebApiHandlers
[JsonIgnore] [JsonIgnore]
private readonly MobileControlWebsocketServer directServer; private readonly MobileControlWebsocketServer directServer;
/// <summary>
/// URL to use to interact with this server
/// </summary>
[JsonProperty("userAppUrl")] [JsonProperty("userAppUrl")]
public string UserAppUrl => $"{directServer.UserAppUrlPrefix}/[insert_client_token]"; public string UserAppUrl => $"{directServer.UserAppUrlPrefix}/[insert_client_token]";
/// <summary>
/// TCP/IP Port this server is configured to use
/// </summary>
[JsonProperty("serverPort")] [JsonProperty("serverPort")]
public int ServerPort => directServer.Port; public int ServerPort => directServer.Port;
/// <summary>
/// Count of defined tokens for this server
/// </summary>
[JsonProperty("tokensDefined")] [JsonProperty("tokensDefined")]
public int TokensDefined => directServer.UiClients.Count; public int TokensDefined => directServer.UiClientContexts.Count;
/// <summary>
/// Count of connected clients
/// </summary>
[JsonProperty("clientsConnected")] [JsonProperty("clientsConnected")]
public int ClientsConnected => directServer.ConnectedUiClientsCount; public int ClientsConnected => directServer.ConnectedUiClientsCount;
/// <summary>
/// List of tokens and connected clients for this server
/// </summary>
[JsonProperty("clients")] [JsonProperty("clients")]
public List<MobileControlDirectClient> Clients => directServer.UiClients.Select((c, i) => { return new MobileControlDirectClient(c, i, directServer.UserAppUrlPrefix); }).ToList(); public List<MobileControlDirectClient> 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();
/// <summary>
/// Create an instance of the <see cref="MobileControlDirectServer"/> class.
/// </summary>
/// <param name="server"></param>
public MobileControlDirectServer(MobileControlWebsocketServer server) public MobileControlDirectServer(MobileControlWebsocketServer server)
{ {
directServer = server; directServer = server;
@@ -142,33 +203,85 @@ namespace PepperDash.Essentials.WebApiHandlers
[JsonIgnore] [JsonIgnore]
private readonly string urlPrefix; private readonly string urlPrefix;
/// <summary>
/// Client number for this client
/// </summary>
[JsonProperty("clientNumber")] [JsonProperty("clientNumber")]
public string ClientNumber => $"{clientNumber}"; public string ClientNumber => $"{clientNumber}";
/// <summary>
/// Room Key for this client
/// </summary>
[JsonProperty("roomKey")] [JsonProperty("roomKey")]
public string RoomKey => context.Token.RoomKey; public string RoomKey => context.Token.RoomKey;
/// <summary>
/// Touchpanel Key, if defined, for this client
/// </summary>
[JsonProperty("touchpanelKey")] [JsonProperty("touchpanelKey")]
public string TouchpanelKey => context.Token.TouchpanelKey; public string TouchpanelKey => context.Token.TouchpanelKey;
/// <summary>
/// URL for this client
/// </summary>
[JsonProperty("url")] [JsonProperty("url")]
public string Url => $"{urlPrefix}{Key}"; public string Url => $"{urlPrefix}{Key}";
/// <summary>
/// Token for this client
/// </summary>
[JsonProperty("token")] [JsonProperty("token")]
public string Token => Key; public string Token => Key;
[JsonProperty("connected")] private readonly List<UiClient> clients;
public bool Connected => context.Client != null && context.Client.Context.WebSocket.IsAlive;
[JsonProperty("duration")] /// <summary>
public double Duration => context.Client == null ? 0 : context.Client.ConnectedDuration.TotalSeconds; /// List of status for all connected UI Clients
/// </summary>
[JsonProperty("clientStatus")]
public List<ClientStatus> ClientStatus => clients.Select(c => new ClientStatus(c)).ToList();
public MobileControlDirectClient(KeyValuePair<string, UiClientContext> clientContext, int index, string urlPrefix) /// <summary>
/// Create an instance of the <see cref="MobileControlDirectClient"/> class.
/// </summary>
/// <param name="clients">List of Websocket Clients</param>
/// <param name="context">Context for the client</param>
/// <param name="index">Index of the client</param>
/// <param name="urlPrefix">URL prefix for the client</param>
public MobileControlDirectClient(List<UiClient> clients, KeyValuePair<string, UiClientContext> context, int index, string urlPrefix)
{ {
context = clientContext.Value; this.context = context.Value;
Key = clientContext.Key; Key = context.Key;
clientNumber = index; clientNumber = index;
this.urlPrefix = urlPrefix; this.urlPrefix = urlPrefix;
this.clients = clients;
}
}
/// <summary>
/// Report the status of a UiClient
/// </summary>
public class ClientStatus
{
private readonly UiClient client;
/// <summary>
/// True if client is connected
/// </summary>
public bool Connected => client != null && client.Context.WebSocket.IsAlive;
/// <summary>
/// Get the time this client has been connected
/// </summary>
public double Duration => client == null ? 0 : client.ConnectedDuration.TotalSeconds;
/// <summary>
/// Create an instance of the <see cref="ClientStatus"/> class for the specified client
/// </summary>
/// <param name="client">client to report on</param>
public ClientStatus(UiClient client)
{
this.client = client;
} }
} }
} }

View File

@@ -14,11 +14,20 @@ namespace PepperDash.Essentials.WebApiHandlers
public class UiClientHandler : WebApiBaseRequestHandler public class UiClientHandler : WebApiBaseRequestHandler
{ {
private readonly MobileControlWebsocketServer server; private readonly MobileControlWebsocketServer server;
/// <summary>
/// Essentials CWS API handler for the MC Direct Server
/// </summary>
/// <param name="directServer">Direct Server instance</param>
public UiClientHandler(MobileControlWebsocketServer directServer) : base(true) public UiClientHandler(MobileControlWebsocketServer directServer) : base(true)
{ {
server = directServer; server = directServer;
} }
/// <summary>
/// Create a client for the Direct Server
/// </summary>
/// <param name="context">HTTP Context for this request</param>
protected override void HandlePost(HttpCwsContext context) protected override void HandlePost(HttpCwsContext context)
{ {
var req = context.Request; var req = context.Request;
@@ -65,6 +74,10 @@ namespace PepperDash.Essentials.WebApiHandlers
res.End(); res.End();
} }
/// <summary>
/// Handle DELETE request for a Client
/// </summary>
/// <param name="context"></param>
protected override void HandleDelete(HttpCwsContext context) protected override void HandleDelete(HttpCwsContext context)
{ {
var req = context.Request; var req = context.Request;
@@ -93,7 +106,7 @@ namespace PepperDash.Essentials.WebApiHandlers
if (!server.UiClients.TryGetValue(request.Token, out UiClientContext clientContext)) if (!server.UiClientContexts.TryGetValue(request.Token, out UiClientContext clientContext))
{ {
var response = new ClientResponse var response = new ClientResponse
{ {
@@ -134,7 +147,7 @@ namespace PepperDash.Essentials.WebApiHandlers
return; return;
} }
server.UiClients.Remove(request.Token); server.UiClientContexts.Remove(request.Token);
server.UpdateSecret(); server.UpdateSecret();

View File

@@ -0,0 +1,24 @@
using System;
namespace PepperDash.Essentials.WebSocketServer
{
/// <summary>
/// Event Args for <see cref="UiClient"/> ConnectionClosed event
/// </summary>
public class ConnectionClosedEventArgs : EventArgs
{
/// <summary>
/// Client ID that is being closed
/// </summary>
public string ClientId { get; private set; }
/// <summary>
/// Initialize an instance of the <see cref="ConnectionClosedEventArgs"/> class.
/// </summary>
/// <param name="clientId">client that's closing</param>
public ConnectionClosedEventArgs(string clientId)
{
ClientId = clientId;
}
}
}

View File

@@ -17,9 +17,15 @@ namespace PepperDash.Essentials.WebSocketServer
[JsonProperty("clientId")] [JsonProperty("clientId")]
public string ClientId { get; set; } public string ClientId { get; set; }
/// <summary>
/// Room Key for this client
/// </summary>
[JsonProperty("roomKey")] [JsonProperty("roomKey")]
public string RoomKey { get; set; } public string RoomKey { get; set; }
/// <summary>
/// System UUID for this system
/// </summary>
[JsonProperty("systemUUid")] [JsonProperty("systemUUid")]
public string SystemUuid { get; set; } public string SystemUuid { get; set; }

View File

@@ -5,15 +5,28 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary> /// </summary>
public class JoinToken public class JoinToken
{ {
/// <summary>
/// Unique client ID for a client that is joining
/// </summary>
public string Id { get; set; }
/// <summary> /// <summary>
/// Gets or sets the Code /// Gets or sets the Code
/// </summary> /// </summary>
public string Code { get; set; } public string Code { get; set; }
/// <summary>
/// Room Key this token is associated with
/// </summary>
public string RoomKey { get; set; } public string RoomKey { get; set; }
/// <summary>
/// Unique ID for this token
/// </summary>
public string Uuid { get; set; } public string Uuid { get; set; }
/// <summary>
/// Touchpanel Key this token is associated with, if this is a touch panel token
/// </summary>
public string TouchpanelKey { get; set; } = ""; public string TouchpanelKey { get; set; } = "";
/// <summary> /// <summary>

View File

@@ -10,6 +10,7 @@ using System.Text;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.WebScripting; using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json; using Newtonsoft.Json;
using Org.BouncyCastle.Crypto.Prng;
using PepperDash.Core; using PepperDash.Core;
using PepperDash.Core.Logging; using PepperDash.Core.Logging;
using PepperDash.Essentials.Core; using PepperDash.Essentials.Core;
@@ -56,7 +57,14 @@ namespace PepperDash.Essentials.WebSocketServer
/// <summary> /// <summary>
/// Gets the collection of UI client contexts /// Gets the collection of UI client contexts
/// </summary> /// </summary>
public Dictionary<string, UiClientContext> UiClients { get; private set; } public Dictionary<string, UiClientContext> UiClientContexts { get; private set; }
private readonly Dictionary<string, UiClient> uiClients = new Dictionary<string, UiClient>();
/// <summary>
/// Gets the collection of UI clients
/// </summary>
public ReadOnlyDictionary<string, UiClient> UiClients => new ReadOnlyDictionary<string, UiClient>(uiClients);
private readonly MobileControlSystemController _parent; private readonly MobileControlSystemController _parent;
@@ -127,17 +135,7 @@ namespace PepperDash.Essentials.WebSocketServer
{ {
get get
{ {
var count = 0; return uiClients.Values.Where(c => c.Context.WebSocket.IsAlive).Count();
foreach (var client in UiClients)
{
if (client.Value.Client != null && client.Value.Client.Context.WebSocket.IsAlive)
{
count++;
}
}
return count;
} }
} }
@@ -202,7 +200,7 @@ namespace PepperDash.Essentials.WebSocketServer
} }
UiClients = new Dictionary<string, UiClientContext>(); UiClientContexts = new Dictionary<string, UiClientContext>();
//_joinTokens = new Dictionary<string, JoinToken>(); //_joinTokens = new Dictionary<string, JoinToken>();
@@ -277,30 +275,10 @@ namespace PepperDash.Essentials.WebSocketServer
}; };
} }
_server.Log.Output = (data, message) => _server.Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this);
{
switch (data.Level) // setting to trace to allow logging level to be controlled by appdebug
{ _server.Log.Level = LogLevel.Trace;
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;
}
};
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
@@ -326,6 +304,9 @@ namespace PepperDash.Essentials.WebSocketServer
} }
} }
/// <summary>
/// Set the internal logging level for the Websocket Server
/// </summary>
public void SetWebsocketLogLevel(LogLevel level) public void SetWebsocketLogLevel(LogLevel level)
{ {
CrestronConsole.ConsoleCommandResponse($"Setting direct server debug level to {level}", level.ToString()); CrestronConsole.ConsoleCommandResponse($"Setting direct server debug level to {level}", level.ToString());
@@ -554,20 +535,20 @@ namespace PepperDash.Essentials.WebSocketServer
this.LogInformation("Adding token: {key} for room: {roomKey}", token.Key, token.Value.RoomKey); this.LogInformation("Adding token: {key} for room: {roomKey}", token.Key, token.Value.RoomKey);
if (UiClients == null) if (UiClientContexts == null)
{ {
UiClients = new Dictionary<string, UiClientContext>(); UiClientContexts = new Dictionary<string, UiClientContext>();
} }
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 key = client.Key;
var path = _wsPath + key; var path = _wsPath + key;
@@ -575,13 +556,8 @@ namespace PepperDash.Essentials.WebSocketServer
_server.AddWebSocketService(path, () => _server.AddWebSocketService(path, () =>
{ {
var c = new UiClient($"uiclient-{key}-{roomKey}"); this.LogInformation("Building a UiClient with ID {id}", client.Value.Token.Id);
this.LogDebug("Constructing UiClient with id: {key}", key); return BuildUiClient(roomKey, client.Value.Token, key);
c.Controller = _parent;
c.RoomKey = roomKey;
UiClients[key].SetClient(c);
return c;
}); });
} }
} }
@@ -591,7 +567,7 @@ namespace PepperDash.Essentials.WebSocketServer
this.LogWarning("No secret found"); 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) catch (Exception ex)
{ {
@@ -616,7 +592,7 @@ namespace PepperDash.Essentials.WebSocketServer
_secret.Tokens.Clear(); _secret.Tokens.Clear();
foreach (var uiClientContext in UiClients) foreach (var uiClientContext in UiClientContexts)
{ {
_secret.Tokens.Add(uiClientContext.Key, uiClientContext.Value.Token); _secret.Tokens.Add(uiClientContext.Key, uiClientContext.Value.Token);
} }
@@ -725,21 +701,17 @@ namespace PepperDash.Essentials.WebSocketServer
var token = new JoinToken { Code = bridge.UserCode, RoomKey = bridge.RoomKey, Uuid = _parent.SystemUuid, TouchpanelKey = touchPanelKey }; 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; var path = _wsPath + key;
_server.AddWebSocketService(path, () => _server.AddWebSocketService(path, () =>
{ {
var c = new UiClient($"uiclient-{key}-{bridge.RoomKey}"); this.LogInformation("Building a UiClient with ID {id}", token.Id);
this.LogVerbose("Constructing UiClient with id: {key}", key); return BuildUiClient(bridge.RoomKey, token, key);
c.Controller = _parent;
c.RoomKey = bridge.RoomKey;
UiClients[key].SetClient(c);
return c;
}); });
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.LogInformation("Token: {@token}", token);
this.LogVerbose("{serviceCount} websocket services present", _server.WebSocketServices.Count); this.LogVerbose("{serviceCount} websocket services present", _server.WebSocketServices.Count);
@@ -749,6 +721,44 @@ namespace PepperDash.Essentials.WebSocketServer
return (key, path); return (key, path);
} }
private UiClient BuildUiClient(string roomKey, JoinToken token, string key)
{
var c = new UiClient($"uiclient-{key}-{roomKey}-{token.Id}", token.Id, token.Token);
this.LogInformation("Constructing UiClient with key {key} and ID {id}", key, token.Id);
c.Controller = _parent;
c.RoomKey = roomKey;
if (uiClients.ContainsKey(token.Id))
{
this.LogWarning("removing client with duplicate id {id}", token.Id);
uiClients.Remove(token.Id);
}
uiClients.Add(token.Id, c);
// UiClients[key].SetClient(c);
c.ConnectionClosed += (o, a) => uiClients.Remove(a.ClientId);
token.Id = null;
return c;
}
/// <summary>
/// Prints out the session data for each path
/// </summary>
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);
}
}
}
/// <summary> /// <summary>
/// Removes all clients from the server /// Removes all clients from the server
/// </summary> /// </summary>
@@ -766,7 +776,7 @@ namespace PepperDash.Essentials.WebSocketServer
return; return;
} }
foreach (var client in UiClients) foreach (var client in UiClientContexts)
{ {
if (client.Value.Client != null && client.Value.Client.Context.WebSocket.IsAlive) if (client.Value.Client != null && client.Value.Client.Context.WebSocket.IsAlive)
{ {
@@ -784,7 +794,7 @@ namespace PepperDash.Essentials.WebSocketServer
} }
} }
UiClients.Clear(); UiClientContexts.Clear();
UpdateSecret(); UpdateSecret();
} }
@@ -803,9 +813,9 @@ namespace PepperDash.Essentials.WebSocketServer
var key = s; 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) if (uiClientContext.Client != null && uiClientContext.Client.Context.WebSocket.IsAlive)
{ {
@@ -815,7 +825,7 @@ namespace PepperDash.Essentials.WebSocketServer
var path = _wsPath + key; var path = _wsPath + key;
if (_server.RemoveWebSocketService(path)) if (_server.RemoveWebSocketService(path))
{ {
UiClients.Remove(key); UiClientContexts.Remove(key);
UpdateSecret(); UpdateSecret();
@@ -839,9 +849,9 @@ namespace PepperDash.Essentials.WebSocketServer
{ {
CrestronConsole.ConsoleCommandResponse("Mobile Control UI Client Info:\r"); 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)); CrestronConsole.ConsoleCommandResponse(string.Format("RoomKey: {0} Token: {1}\r", client.Value.Token.RoomKey, client.Key));
} }
@@ -853,9 +863,9 @@ namespace PepperDash.Essentials.WebSocketServer
{ {
foreach (var client in UiClients.Values) foreach (var client in UiClients.Values)
{ {
if (client.Client != null && client.Client.Context.WebSocket.IsAlive) if (client != null && client.Context.WebSocket.IsAlive)
{ {
client.Client.Context.WebSocket.Close(CloseStatusCode.Normal, "Server Shutting Down"); client.Context.WebSocket.Close(CloseStatusCode.Normal, "Server Shutting Down");
} }
} }
@@ -990,77 +1000,81 @@ namespace PepperDash.Essentials.WebSocketServer
this.LogVerbose("Join Room Request with token: {token}", token); this.LogVerbose("Join Room Request with token: {token}", token);
byte[] body;
if (UiClients.TryGetValue(token, out UiClientContext clientContext)) if (!UiClientContexts.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<string, DeviceInterfaceInfo> deviceInterfaces = new Dictionary<string, DeviceInterfaceInfo>();
foreach (var device in devices)
{
var interfaces = device?.GetType().GetInterfaces().Select((i) => i.Name).ToList() ?? new List<string>();
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
{ {
var message = "Token invalid or has expired"; var message = "Token invalid or has expired";
res.StatusCode = 401; res.StatusCode = 401;
res.ContentType = "application/json"; res.ContentType = "application/json";
this.LogVerbose("{message}", message); this.LogVerbose("{message}", message);
var body = Encoding.UTF8.GetBytes(message); body = Encoding.UTF8.GetBytes(message);
res.ContentLength64 = body.LongLength; res.ContentLength64 = body.LongLength;
res.Close(body, true); 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<string, DeviceInterfaceInfo> deviceInterfaces = new Dictionary<string, DeviceInterfaceInfo>();
foreach (var device in devices)
{
var interfaces = device?.GetType().GetInterfaces().Select((i) => i.Name).ToList() ?? new List<string>();
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);
} }
/// <summary> /// <summary>
@@ -1242,12 +1256,14 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary> /// </summary>
public void SendMessageToAllClients(string message) public void SendMessageToAllClients(string message)
{ {
foreach (var clientContext in UiClients.Values) foreach (var client in uiClients.Values)
{ {
if (clientContext.Client != null && clientContext.Client.Context.WebSocket.IsAlive) if (!client.Context.WebSocket.IsAlive)
{ {
clientContext.Client.Context.WebSocket.Send(message); continue;
} }
client.Context.WebSocket.Send(message);
} }
} }
@@ -1266,17 +1282,16 @@ namespace PepperDash.Essentials.WebSocketServer
return; return;
} }
if (UiClients.TryGetValue((string)clientId, out UiClientContext clientContext)) if (uiClients.TryGetValue((string)clientId, out var client))
{ {
if (clientContext.Client != null) var socket = client.Context.WebSocket;
{
var socket = clientContext.Client.Context.WebSocket;
if (socket.IsAlive) if (!socket.IsAlive)
{ {
socket.Send(message); this.LogError("Unable to send message to client {id}. Client is disconnected: {message}", clientId, message);
} return;
} }
socket.Send(message);
} }
else else
{ {

View File

@@ -13,8 +13,15 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary> /// </summary>
public string GrantCode { get; set; } public string GrantCode { get; set; }
/// <summary>
/// Gets or sets the Tokens for this server
/// </summary>
public Dictionary<string, JoinToken> Tokens { get; set; } public Dictionary<string, JoinToken> Tokens { get; set; }
/// <summary>
/// Initialize a new instance of the <see cref="ServerTokenSecrets"/> class with the provided grant code
/// </summary>
/// <param name="grantCode">The grant code for this server</param>
public ServerTokenSecrets(string grantCode) public ServerTokenSecrets(string grantCode)
{ {
GrantCode = grantCode; GrantCode = grantCode;

View File

@@ -1,5 +1,4 @@
using System; using System;
using System.Text.RegularExpressions;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using PepperDash.Core; using PepperDash.Core;
@@ -22,6 +21,16 @@ namespace PepperDash.Essentials.WebSocketServer
/// <inheritdoc /> /// <inheritdoc />
public string Key { get; private set; } public string Key { get; private set; }
/// <summary>
/// Client ID used by client for this connection
/// </summary>
public string Id { get; private set; }
/// <summary>
/// Token associated with this client
/// </summary>
public string Token { get; private set; }
/// <summary> /// <summary>
/// Gets or sets the mobile control system controller that handles this client's messages /// Gets or sets the mobile control system controller that handles this client's messages
/// </summary> /// </summary>
@@ -32,11 +41,6 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary> /// </summary>
public string RoomKey { get; set; } public string RoomKey { get; set; }
/// <summary>
/// The unique identifier for this client instance
/// </summary>
private string _clientId;
/// <summary> /// <summary>
/// The timestamp when this client connection was established /// The timestamp when this client connection was established
/// </summary> /// </summary>
@@ -60,13 +64,22 @@ namespace PepperDash.Essentials.WebSocketServer
} }
} }
/// <summary>
/// Triggered when this client closes it's connection
/// </summary>
public event EventHandler<ConnectionClosedEventArgs> ConnectionClosed;
/// <summary> /// <summary>
/// Initializes a new instance of the UiClient class with the specified key /// Initializes a new instance of the UiClient class with the specified key
/// </summary> /// </summary>
/// <param name="key">The unique key to identify this client</param> /// <param name="key">The unique key to identify this client</param>
public UiClient(string key) /// <param name="id">The client ID used by the client for this connection</param>
/// <param name="token">The token associated with this client</param>
public UiClient(string key, string id, string token)
{ {
Key = key; Key = key;
Id = id;
Token = token;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -74,19 +87,10 @@ namespace PepperDash.Essentials.WebSocketServer
{ {
base.OnOpen(); base.OnOpen();
var url = Context.WebSocket.Url; _connectionTime = DateTime.Now;
this.LogInformation("New WebSocket Connection from: {url}", url);
var match = Regex.Match(url.AbsoluteUri, "(?:ws|wss):\\/\\/.*(?:\\/mc\\/api\\/ui\\/join\\/)(.*)"); Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this);
Log.Level = LogLevel.Trace;
if (!match.Success)
{
_connectionTime = DateTime.Now;
return;
}
var clientId = match.Groups[1].Value;
_clientId = clientId;
if (Controller == null) if (Controller == null)
{ {
@@ -99,7 +103,7 @@ namespace PepperDash.Essentials.WebSocketServer
Type = "/system/clientJoined", Type = "/system/clientJoined",
Content = JToken.FromObject(new Content = JToken.FromObject(new
{ {
clientId, clientId = Id,
roomKey = RoomKey, roomKey = RoomKey,
}) })
}; };
@@ -110,7 +114,7 @@ namespace PepperDash.Essentials.WebSocketServer
if (bridge == null) return; if (bridge == null) return;
SendUserCodeToClient(bridge, clientId); SendUserCodeToClient(bridge, Id);
bridge.UserCodeChanged -= Bridge_UserCodeChanged; bridge.UserCodeChanged -= Bridge_UserCodeChanged;
bridge.UserCodeChanged += Bridge_UserCodeChanged; bridge.UserCodeChanged += Bridge_UserCodeChanged;
@@ -125,7 +129,7 @@ namespace PepperDash.Essentials.WebSocketServer
/// <param name="e">Event arguments</param> /// <param name="e">Event arguments</param>
private void Bridge_UserCodeChanged(object sender, EventArgs e) private void Bridge_UserCodeChanged(object sender, EventArgs e)
{ {
SendUserCodeToClient((MobileControlEssentialsRoomBridge)sender, _clientId); SendUserCodeToClient((MobileControlEssentialsRoomBridge)sender, Id);
} }
/// <summary> /// <summary>
@@ -172,13 +176,15 @@ namespace PepperDash.Essentials.WebSocketServer
foreach (var messenger in Controller.Messengers) foreach (var messenger in Controller.Messengers)
{ {
messenger.Value.UnsubscribeClient(_clientId); messenger.Value.UnsubscribeClient(Id);
} }
foreach (var messenger in Controller.DefaultMessengers) foreach (var messenger in Controller.DefaultMessengers)
{ {
messenger.Value.UnsubscribeClient(_clientId); messenger.Value.UnsubscribeClient(Id);
} }
ConnectionClosed?.Invoke(this, new ConnectionClosedEventArgs(Id));
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@@ -14,6 +14,10 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary> /// </summary>
public JoinToken Token { get; private set; } public JoinToken Token { get; private set; }
/// <summary>
/// Initialize an instance of the <see cref="UiClientContext"/> class with the provided token
/// </summary>
/// <param name="token">token for this client</param>
public UiClientContext(JoinToken token) public UiClientContext(JoinToken token)
{ {
Token = token; Token = token;

View File

@@ -8,12 +8,25 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary> /// </summary>
public class Version public class Version
{ {
/// <summary>
/// Server version this Websocket is connected to
/// </summary>
[JsonProperty("serverVersion")] [JsonProperty("serverVersion")]
public string ServerVersion { get; set; } public string ServerVersion { get; set; }
/// <summary>
/// True if the server is on a processor
/// </summary>
[JsonProperty("serverIsRunningOnProcessorHardware")] [JsonProperty("serverIsRunningOnProcessorHardware")]
public bool ServerIsRunningOnProcessorHardware { get; private set; } public bool ServerIsRunningOnProcessorHardware { get; private set; }
/// <summary>
/// Initialize an instance of the <see cref="Version"/> class
/// </summary>
/// <remarks>
/// The <see cref="ServerIsRunningOnProcessorHardware"/> property is set to true by default.
/// </remarks>
public Version() public Version()
{ {
ServerIsRunningOnProcessorHardware = true; ServerIsRunningOnProcessorHardware = true;

View File

@@ -13,25 +13,28 @@ namespace PepperDash.Essentials.WebSocketServer
} }
/// <summary> /// <summary>
/// Represents a WebSocketServerSecret /// Stores a secret value using the provided secret store provider
/// </summary> /// </summary>
public class WebSocketServerSecret : ISecret public class WebSocketServerSecret : ISecret
{ {
/// <summary> /// <summary>
/// Gets or sets the Provider /// Gets the Secret Provider associated with this secret
/// </summary> /// </summary>
public ISecretProvider Provider { get; private set; } public ISecretProvider Provider { get; private set; }
/// <summary> /// <summary>
/// Gets or sets the Key /// Gets the Key associated with this secret
/// </summary> /// </summary>
public string Key { get; private set; } public string Key { get; private set; }
/// <summary> /// <summary>
/// Gets or sets the Value /// Gets the Value associated with this secret
/// </summary> /// </summary>
public object Value { get; private set; } public object Value { get; private set; }
/// <summary>
/// Initialize and instance of the <see cref="WebSocketServerSecret"/> class
/// </summary>
public WebSocketServerSecret(string key, object value, ISecretProvider provider) public WebSocketServerSecret(string key, object value, ISecretProvider provider)
{ {
Key = key; Key = key;