Compare commits

..

10 Commits

Author SHA1 Message Date
Aviv Cohn
e003e4dc94 fix: move ChangeScenario outside of loop, remove the discard modifier 2026-03-20 13:43:51 -04:00
Aviv Cohn
e2cdb1238b fix: calls scenario in SetRoomCombinationScenario preventing null from breaking rendering 2026-03-20 12:42:00 -04:00
Neil Dorin
dab5484d6e Merge pull request #1342 from PepperDash/wsdebug-persistence
Unique Client IDs
2025-10-15 15:16:46 -04:00
Andrew Welker
5c35a3be45 fix: catch exceptions in handlers directly
Previously, any exceptions that were occuring in a hander's action were being swalled due to being off on another thread. Now, those exceptions are caught and printed out.
2025-10-15 14:03:17 -05:00
Andrew Welker
6cb98e12fa fix: use correct collection for program stop 2025-10-15 14:02:06 -05:00
Andrew Welker
608601990b docs: fix Copilot comments 2025-10-15 12:54:14 -05:00
Andrew Welker
3e0f318f7f fix: update log methods to all be consistent 2025-10-15 12:36:42 -05:00
Andrew Welker
98d0cc8fdc docs: add missing XML comments for Mobile Control Project 2025-10-15 12:26:57 -05:00
Andrew Welker
c557c6cdd6 fix: mobileinfo & CWS info call report the correct data 2025-10-15 11:49:06 -05:00
Andrew Welker
8525134ae7 fix: direct server clients now have unique client IDs
Using the generated security token as an ID was presenting problems with duplicate connections using the same ID and trying to figure out where to send the messages. Now, the clients have a unique ID that's an increasing integer that restarts at 1 when the program restarts.
2025-10-15 09:50:12 -05:00
34 changed files with 1090 additions and 487 deletions

View File

@@ -445,8 +445,10 @@ namespace PepperDash.Essentials.Core
else
{
Debug.LogMessage(LogEventLevel.Debug, this, "Unable to find partition with key: '{0}'", partitionState.PartitionKey);
}
}
}
// Activates the scenario to prevent _currentScenario from being null when starting in manual mode.
ChangeScenario(scenario);
}
else
{

View File

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

View File

@@ -7,8 +7,9 @@ namespace PepperDash.Essentials
/// </summary>
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;
}
}

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 System;
namespace PepperDash.Essentials
{
@@ -10,12 +10,20 @@ namespace PepperDash.Essentials
public class MobileControlAction : IMobileControlAction
{
/// <summary>
/// Gets or sets the Messenger
/// Gets the Messenger
/// </summary>
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; }
/// <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)
{
Messenger = messenger;

View File

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

View File

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

View File

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

View File

@@ -91,10 +91,16 @@ namespace PepperDash.Essentials
/// </summary>
public MobileControlApiService ApiService { get; private set; }
/// <summary>
/// Get Room Bridges associated with this controller
/// </summary>
public List<MobileControlBridgeBase> RoomBridges => _roomBridges;
private readonly MobileControlWebsocketServer _directServer;
/// <summary>
/// Get the Direct Server instance associated with this controller
/// </summary>
public MobileControlWebsocketServer DirectServer => _directServer;
private readonly CCriticalSection _wsCriticalSection = new CCriticalSection();
@@ -104,10 +110,16 @@ namespace PepperDash.Essentials
/// </summary>
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;
private IEssentialsRoomCombiner _roomCombiner;
/// <summary>
/// Gets the SystemUuid from configuration or SIMPL Bridge
/// </summary>
public string SystemUuid
{
get
@@ -169,6 +181,9 @@ namespace PepperDash.Essentials
private DateTime _lastAckMessage;
/// <summary>
/// Gets the LastAckMessage timestamp
/// </summary>
public DateTime LastAckMessage => _lastAckMessage;
private CTimer _pingTimer;
@@ -177,11 +192,11 @@ namespace PepperDash.Essentials
private LogLevel _wsLogLevel = LogLevel.Error;
/// <summary>
///
/// Initializes a new instance of the <see cref="MobileControlSystemController"/> class.
/// </summary>
/// <param name="key"></param>
/// <param name="name"></param>
/// <param name="config"></param>
/// <param name="key">The unique key for this controller.</param>
/// <param name="name">The name of the controller.</param>
/// <param name="config">The configuration settings for the controller.</param>
public MobileControlSystemController(string key, string name, MobileControlConfig config)
: base(key, name)
{
@@ -1192,6 +1207,9 @@ namespace PepperDash.Essentials
/// </summary>
public string Host { get; private set; }
/// <summary>
/// Gets the configured Client App URL
/// </summary>
public string ClientAppUrl => Config.ClientAppUrl;
private void OnRoomCombinationScenarioChanged(
@@ -1203,7 +1221,7 @@ namespace PepperDash.Essentials
}
/// <summary>
/// CheckForDeviceMessenger method
/// Checks if a device messenger exists for the given key.
/// </summary>
public bool CheckForDeviceMessenger(string key)
{
@@ -1211,13 +1229,13 @@ namespace PepperDash.Essentials
}
/// <summary>
/// AddDeviceMessenger method
/// Add the provided messenger to the messengers collection
/// </summary>
public void AddDeviceMessenger(IMobileControlMessenger messenger)
{
if (_messengers.ContainsKey(messenger.Key))
{
this.LogWarning("Messenger with key {messengerKey) already added", messenger.Key);
this.LogWarning("Messenger with key {messengerKey} already added", messenger.Key);
return;
}
@@ -1291,9 +1309,6 @@ namespace PepperDash.Essentials
messenger.RegisterWithAppServer(this);
}
/// <summary>
/// Initialize method
/// </summary>
/// <inheritdoc />
public override void Initialize()
{
@@ -1338,7 +1353,7 @@ namespace PepperDash.Essentials
#region IMobileControl Members
/// <summary>
/// GetAppServer method
/// Gets the App Server instance
/// </summary>
public static IMobileControl GetAppServer()
{
@@ -1356,16 +1371,10 @@ namespace PepperDash.Essentials
}
}
/// <summary>
/// Generates the url and creates the websocket client
/// </summary>
private bool CreateWebsocket()
{
if (_wsClient2 != null)
{
_wsClient2.Close();
_wsClient2 = null;
}
_wsClient2?.Close();
_wsClient2 = null;
if (string.IsNullOrEmpty(SystemUuid))
{
@@ -1382,33 +1391,13 @@ namespace PepperDash.Essentials
{
Log =
{
Output = (data, message) =>
{
switch (data.Level)
{
case LogLevel.Trace:
this.LogVerbose(data.Message);
break;
case LogLevel.Debug:
this.LogDebug(data.Message);
break;
case LogLevel.Info:
this.LogInformation(data.Message);
break;
case LogLevel.Warn:
this.LogWarning(data.Message);
break;
case LogLevel.Error:
this.LogError(data.Message);
break;
case LogLevel.Fatal:
this.LogFatal(data.Message);
break;
}
}
Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this)
}
};
// setting to trace to let level be controlled by appdebug
_wsClient2.Log.Level = LogLevel.Trace;
_wsClient2.SslConfiguration.EnabledSslProtocols =
System.Security.Authentication.SslProtocols.Tls11
| System.Security.Authentication.SslProtocols.Tls12;
@@ -1422,7 +1411,7 @@ namespace PepperDash.Essentials
}
/// <summary>
/// LinkSystemMonitorToAppServer method
/// Link the System Monitor to this App server
/// </summary>
public void LinkSystemMonitorToAppServer()
{
@@ -1449,14 +1438,6 @@ namespace PepperDash.Essentials
private void SetWebsocketDebugLevel(string cmdparameters)
{
// if (CrestronEnvironment.ProgramCompatibility == eCrestronSeries.Series4)
// {
// this.LogInformation(
// "Setting websocket log level not currently allowed on 4 series."
// );
// return; // Web socket log level not currently allowed in series4
// }
if (string.IsNullOrEmpty(cmdparameters))
{
this.LogInformation("Current Websocket debug level: {webSocketDebugLevel}", _wsLogLevel);
@@ -1494,10 +1475,6 @@ namespace PepperDash.Essentials
}
}
/// <summary>
/// Sends message to server to indicate the system is shutting down
/// </summary>
/// <param name="programEventType"></param>
private void CrestronEnvironment_ProgramStatusEventHandler(
eProgramStatusEventType programEventType
)
@@ -1530,6 +1507,9 @@ namespace PepperDash.Essentials
}
}
/// <summary>
/// Get action paths for the current actions
/// </summary>
public List<(string, string)> GetActionDictionaryPaths()
{
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)
{
return _roomBridges.FirstOrDefault((r) => r.RoomKey.Equals(key));
}
/// <summary>
/// GetRoomMessenger method
/// Get the room messenger with the provided key
/// </summary>
/// <param name="key">The Key of the rooom messenger</param>
public IMobileControlRoomMessenger GetRoomMessenger(string 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)
{
this.LogDebug("Bridge ready. Registering");
@@ -1640,10 +1620,6 @@ namespace PepperDash.Essentials
}
}
/// <summary>
///
/// </summary>
/// <param name="o"></param>
private void ReconnectToServerTimerCallback(object o)
{
this.LogDebug("Attempting to reconnect to server...");
@@ -1651,9 +1627,6 @@ namespace PepperDash.Essentials
ConnectWebsocketClient();
}
/// <summary>
/// Verifies system connection with servers
/// </summary>
private void AuthorizeSystem(string code)
{
if (
@@ -1698,9 +1671,6 @@ namespace PepperDash.Essentials
});
}
/// <summary>
/// Dumps info in response to console command.
/// </summary>
private void ShowInfo()
{
var url = Config != null ? Host : "No config";
@@ -1766,38 +1736,37 @@ namespace PepperDash.Essentials
"\r\n UI Client Info:\r\n" +
" Tokens Defined: {0}\r\n" +
" Clients Connected: {1}\r\n",
_directServer.UiClients.Count,
_directServer.UiClientContexts.Count,
_directServer.ConnectedUiClientsCount
);
var clientNo = 1;
foreach (var clientContext in _directServer.UiClients)
foreach (var clientContext in _directServer.UiClientContexts)
{
var isAlive = false;
var duration = "Not Connected";
if (clientContext.Value.Client != null)
{
isAlive = clientContext.Value.Client.Context.WebSocket.IsAlive;
duration = clientContext.Value.Client.ConnectedDuration.ToString();
}
var clients = _directServer.UiClients.Values.Where(c => c.Token == clientContext.Value.Token.Token);
CrestronConsole.ConsoleCommandResponse(
"\r\nClient {0}:\r\n" +
"Room Key: {1}\r\n" +
"Touchpanel Key: {6}\r\n" +
"Token: {2}\r\n" +
"Client URL: {3}\r\n" +
"Connected: {4}\r\n" +
"Duration: {5}\r\n",
clientNo,
clientContext.Value.Token.RoomKey,
clientContext.Key,
string.Format("{0}{1}", _directServer.UserAppUrlPrefix, clientContext.Key),
isAlive,
duration,
clientContext.Value.Token.TouchpanelKey
$"\r\nClient {clientNo}:\r\n" +
$" Room Key: {clientContext.Value.Token.RoomKey}\r\n" +
$" Touchpanel Key: {clientContext.Value.Token.TouchpanelKey}\r\n" +
$" Token: {clientContext.Key}\r\n" +
$" Client URL: {_directServer.UserAppUrlPrefix}{clientContext.Key}\r\n" +
$" Clients:\r\n"
);
if (!clients.Any())
{
CrestronConsole.ConsoleCommandResponse(" No clients connected");
}
foreach (var client in clients)
{
CrestronConsole.ConsoleCommandResponse(
$" ID: {client.Id}\r\n" +
$" Connected: {client.Context.WebSocket.IsAlive}\r\n" +
$" Duration: {(client.Context.WebSocket.IsAlive ? client.ConnectedDuration.TotalSeconds.ToString() : "Not Connected")}\r\n"
);
}
clientNo++;
}
}
@@ -1811,7 +1780,7 @@ namespace PepperDash.Essentials
}
/// <summary>
/// RegisterSystemToServer method
/// Register this system to the Mobile Control Edge Server
/// </summary>
public void RegisterSystemToServer()
{
@@ -1835,9 +1804,6 @@ namespace PepperDash.Essentials
ConnectWebsocketClient();
}
/// <summary>
/// Connects the Websocket Client
/// </summary>
private void ConnectWebsocketClient()
{
try
@@ -1878,9 +1844,6 @@ namespace PepperDash.Essentials
}
}
/// <summary>
/// Attempts to connect the websocket
/// </summary>
private void TryConnect()
{
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()
{
_wsClient2 = null;
@@ -1944,11 +1904,6 @@ namespace PepperDash.Essentials
StartServerReconnectTimer();
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void HandleOpen(object sender, EventArgs e)
{
StopServerReconnectTimer();
@@ -1957,11 +1912,6 @@ namespace PepperDash.Essentials
SendMessageObject(new MobileControlMessage { Type = "hello" });
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void HandleMessage(object sender, MessageEventArgs e)
{
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)
{
this.LogError("Websocket error {0}", e.Message);
@@ -1991,11 +1936,6 @@ namespace PepperDash.Essentials
StartServerReconnectTimer();
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void HandleClose(object sender, CloseEventArgs e)
{
this.LogDebug(
@@ -2016,9 +1956,6 @@ namespace PepperDash.Essentials
StartServerReconnectTimer();
}
/// <summary>
/// After a "hello" from the server, sends config and stuff
/// </summary>
private void SendInitialMessage()
{
this.LogInformation("Sending initial join message");
@@ -2045,7 +1982,7 @@ namespace PepperDash.Essentials
}
/// <summary>
/// GetConfigWithPluginVersion method
/// Get the Essentials configuration with version data
/// </summary>
public MobileControlEssentialsConfig GetConfigWithPluginVersion()
{
@@ -2080,8 +2017,13 @@ namespace PepperDash.Essentials
}
/// <summary>
/// SetClientUrl method
/// Set the Client URL for a given room
/// </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)
{
var message = new MobileControlMessage
@@ -2097,9 +2039,6 @@ namespace PepperDash.Essentials
/// Sends any object type to server
/// </summary>
/// <param name="o"></param>
/// <summary>
/// SendMessageObject method
/// </summary>
public void SendMessageObject(IMobileControlMessage o)
{
@@ -2123,8 +2062,9 @@ namespace PepperDash.Essentials
/// <summary>
/// SendMessageObjectToDirectClient method
/// Send a message to a client using the Direct Server
/// </summary>
/// <param name="o">object to send</param>
public void SendMessageObjectToDirectClient(object o)
{
if (
@@ -2137,10 +2077,6 @@ namespace PepperDash.Essentials
}
}
/// <summary>
/// Disconnects the Websocket Client and stops the heartbeat timer
/// </summary>
private void CleanUpWebsocketClient()
{
if (_wsClient2 == null)
@@ -2198,9 +2134,6 @@ namespace PepperDash.Essentials
}
}
/// <summary>
///
/// </summary>
private void StartServerReconnectTimer()
{
StopServerReconnectTimer();
@@ -2211,9 +2144,6 @@ namespace PepperDash.Essentials
this.LogDebug("Reconnect Timer Started.");
}
/// <summary>
/// Does what it says
/// </summary>
private void StopServerReconnectTimer()
{
if (_serverReconnectTimer == null)
@@ -2224,10 +2154,6 @@ namespace PepperDash.Essentials
_serverReconnectTimer = null;
}
/// <summary>
/// Resets reconnect timer and updates usercode
/// </summary>
/// <param name="content"></param>
private void HandleHeartBeat(JToken content)
{
SendMessageObject(new MobileControlMessage { Type = "/system/heartbeatAck" });
@@ -2337,16 +2263,13 @@ namespace PepperDash.Essentials
}
/// <summary>
/// HandleClientMessage method
/// Enqueue an incoming message for processing
/// </summary>
public void HandleClientMessage(string message)
{
_receiveQueue.Enqueue(new ProcessStringMessage(message, ParseStreamRx));
}
/// <summary>
///
/// </summary>
private void ParseStreamRx(string messageText)
{
if (string.IsNullOrEmpty(messageText))
@@ -2414,10 +2337,33 @@ namespace PepperDash.Essentials
foreach (var handler in handlers)
{
Task.Run(
() =>
handler.Action(message.Type, message.ClientId, message.Content)
);
Task.Run(async () =>
{
try
{
handler.Action(message.Type, message.ClientId, message.Content);
}
catch (Exception ex)
{
this.LogError(
"Exception in handler for message type {type}, ClientId {clientId}",
message.Type,
message.ClientId
);
this.LogDebug(ex, "Stack Trace: ");
}
}).ContinueWith(task =>
{
if (task.IsFaulted && task.Exception != null)
{
this.LogError(
"Unhandled exception in Task for message type {type}, ClientId {clientId}",
message.Type,
message.ClientId
);
this.LogDebug(task.Exception.GetBaseException(), "Stack Trace: ");
}
}, TaskContinuationOptions.OnlyOnFaulted);
}
break;
@@ -2433,10 +2379,6 @@ namespace PepperDash.Essentials
}
}
/// <summary>
///
/// </summary>
/// <param name="s"></param>
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.Essentials.AppServer.Messengers;
using PepperDash.Essentials.Core.DeviceTypeInterfaces;
using System;
namespace PepperDash.Essentials.RoomBridges
{
/// <summary>
///
/// Base class for a Mobile Control Bridge that's used to control a room
/// </summary>
public abstract class MobileControlBridgeBase : MessengerBase, IMobileControlRoomMessenger
{
/// <summary>
/// Triggered when the user Code changes
/// </summary>
public event EventHandler<EventArgs> UserCodeChanged;
/// <summary>
/// Triggered when a user should be prompted for the new code
/// </summary>
public event EventHandler<EventArgs> UserPromptedForCode;
/// <summary>
/// Triggered when a client joins to control this room
/// </summary>
public event EventHandler<EventArgs> ClientJoined;
/// <summary>
/// Triggered when the App URL for this room changes
/// </summary>
public event EventHandler<EventArgs> AppUrlChanged;
/// <summary>
@@ -49,15 +61,32 @@ namespace PepperDash.Essentials.RoomBridges
/// </summary>
public string McServerUrl { get; private set; }
/// <summary>
/// Room Name
/// </summary>
public abstract string RoomName { get; }
/// <summary>
/// Room key
/// </summary>
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)
: 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)
: base(key, messagePath, device)
{
@@ -110,6 +139,10 @@ namespace PepperDash.Essentials.RoomBridges
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)
{
AppUrl = url;
@@ -137,16 +170,25 @@ namespace PepperDash.Essentials.RoomBridges
OnUserCodeChanged();
}
/// <summary>
/// Trigger the UserCodeChanged event
/// </summary>
protected void OnUserCodeChanged()
{
UserCodeChanged?.Invoke(this, new EventArgs());
}
/// <summary>
/// Trigger the UserPromptedForCode event
/// </summary>
protected void OnUserPromptedForCode()
{
UserPromptedForCode?.Invoke(this, new EventArgs());
}
/// <summary>
/// Trigger the ClientJoined event
/// </summary>
protected void OnClientJoined()
{
ClientJoined?.Invoke(this, new EventArgs());

View File

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

View File

@@ -1,18 +1,22 @@
using PepperDash.Core;
using System;
using System;
using System.Net.Http;
using System.Threading.Tasks;
using PepperDash.Core;
namespace PepperDash.Essentials.Services
{
/// <summary>
/// Represents a MobileControlApiService
/// Service for interacting with a Mobile Control Edge server instance
/// </summary>
public class MobileControlApiService
{
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)
{
var handler = new HttpClientHandler
@@ -24,6 +28,13 @@ namespace PepperDash.Essentials.Services
_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)
{
try

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,17 +7,24 @@ using PepperDash.Essentials.AppServer.Messengers;
namespace PepperDash.Essentials.Touchpanel
{
/// <summary>
/// Represents a ThemeMessenger
/// Messenger to save the current theme (light/dark) and send to a device
/// </summary>
public class ThemeMessenger : MessengerBase
{
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)
{
_tpDevice = device;
}
/// <inheritdoc />
protected override void RegisterActions()
{
AddAction("/fullStatus", (id, content) =>

View File

@@ -1,13 +1,9 @@
using Newtonsoft.Json;
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using PepperDash.Core;
using PepperDash.Core.Logging;
using PepperDash.Essentials.AppServer.Messengers;
using PepperDash.Essentials.Core.Queues;
using PepperDash.Essentials.WebSocketServer;
using Serilog.Events;
using System;
using System.Threading;
using WebSocketSharp;
namespace PepperDash.Essentials
@@ -20,12 +16,22 @@ namespace PepperDash.Essentials
private readonly WebSocket _ws;
private readonly object msgToSend;
/// <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)
{
_ws = ws;
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)
{
_ws = ws;
@@ -43,13 +49,13 @@ namespace PepperDash.Essentials
{
if (_ws == null)
{
Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Websocket client is null");
Debug.LogWarning("Cannot send message. Websocket client is null");
return;
}
if (!_ws.IsAlive)
{
Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Websocket client is not connected");
Debug.LogWarning("Cannot send message. Websocket client is not connected");
return;
}
@@ -57,83 +63,14 @@ namespace PepperDash.Essentials
var message = JsonConvert.SerializeObject(msgToSend, Formatting.None,
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } });
Debug.LogMessage(LogEventLevel.Verbose, "Message TX: {0}", null, message);
Debug.LogVerbose("Message TX: {0}", message);
_ws.Send(message);
}
catch (Exception ex)
{
Debug.LogMessage(ex, "Caught an exception in the Transmit Processor");
}
}
#endregion
}
/// <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");
Debug.LogError("Caught an exception in the Transmit Processor: {message}", ex.Message);
Debug.LogDebug(ex, "Stack Trace: ");
}
}
#endregion

View File

@@ -3,12 +3,19 @@ using System;
namespace PepperDash.Essentials
{
/// <summary>
/// Represents a UserCodeChanged
/// Defines the action to take when the User code changes
/// </summary>
public class UserCodeChanged
{
/// <summary>
/// Action to take when the User Code changes
/// </summary>
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)
{
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)]
public Volume Master { get; set; }
/// <summary>
/// Aux Faders as configured in the room
/// </summary>
[JsonProperty("auxFaders", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, Volume> AuxFaders { get; set; }
/// <summary>
/// Count of aux faders for this system
/// </summary>
[JsonProperty("numberOfAuxFaders", NullValueHandling = NullValueHandling.Ignore)]
public int? NumberOfAuxFaders { get; set; }
public Volumes()
{
}
}
/// <summary>
@@ -31,16 +33,21 @@ namespace PepperDash.Essentials
/// </summary>
public class Volume
{
/// <summary>
/// Gets or sets the Key
/// </summary>
[JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)]
public string Key { get; set; }
/// <summary>
/// Level for this volume object
/// </summary>
[JsonProperty("level", NullValueHandling = NullValueHandling.Ignore)]
public int? Level { get; set; }
/// <summary>
/// True if this volume control is muted
/// </summary>
[JsonProperty("muted", NullValueHandling = NullValueHandling.Ignore)]
public bool? Muted { get; set; }
@@ -51,12 +58,21 @@ namespace PepperDash.Essentials
[JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)]
public string Label { get; set; }
/// <summary>
/// True if this volume object has mute control
/// </summary>
[JsonProperty("hasMute", NullValueHandling = NullValueHandling.Ignore)]
public bool? HasMute { get; set; }
/// <summary>
/// True if this volume object has Privacy mute control
/// </summary>
[JsonProperty("hasPrivacyMute", NullValueHandling = NullValueHandling.Ignore)]
public bool? HasPrivacyMute { get; set; }
/// <summary>
/// True if the privacy mute is muted
/// </summary>
[JsonProperty("privacyMuted", NullValueHandling = NullValueHandling.Ignore)]
public bool? PrivacyMuted { get; set; }
@@ -68,6 +84,15 @@ namespace PepperDash.Essentials
[JsonProperty("muteIcon", NullValueHandling = NullValueHandling.Ignore)]
public string MuteIcon { get; set; }
/// <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)
: this(key)
{
@@ -78,18 +103,32 @@ namespace PepperDash.Essentials
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)
: this(key)
{
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)
: this(key)
{
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)
{
Key = key;

View File

@@ -12,11 +12,20 @@ namespace PepperDash.Essentials.WebApiHandlers
public class ActionPathsHandler : WebApiBaseRequestHandler
{
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)
{
mcController = controller;
}
/// <summary>
/// Handle a request to get the action paths
/// </summary>
/// <param name="context">Request Context</param>
protected override void HandleGet(HttpCwsContext context)
{
var response = JsonConvert.SerializeObject(new ActionPathsResponse(mcController));
@@ -37,9 +46,16 @@ namespace PepperDash.Essentials.WebApiHandlers
[JsonIgnore]
private readonly MobileControlSystemController mcController;
/// <summary>
/// Registered action paths for this system
/// </summary>
[JsonProperty("actionPaths")]
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)
{
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 PepperDash.Core;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Web;
using System;
using System.Threading.Tasks;
namespace PepperDash.Essentials.WebApiHandlers
{
@@ -15,11 +15,20 @@ namespace PepperDash.Essentials.WebApiHandlers
{
private readonly MobileControlSystemController mcController;
/// <summary>
/// Create an instance of the <see cref="MobileAuthRequestHandler"/> class.
/// </summary>
/// <param name="controller"></param>
public MobileAuthRequestHandler(MobileControlSystemController controller) : base(true)
{
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)
{
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 PepperDash.Core;
using PepperDash.Core.Web.RequestHandlers;
using PepperDash.Essentials.Core.Config;
using PepperDash.Essentials.WebSocketServer;
using System;
using System.Collections.Generic;
using System.Linq;
namespace PepperDash.Essentials.WebApiHandlers
{
/// <summary>
/// Represents a MobileInfoHandler
/// Represents a MobileInfoHandler. Used with the Essentials CWS API
/// </summary>
public class MobileInfoHandler : WebApiBaseRequestHandler
{
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)
{
mcController = controller;
}
/// <summary>
/// Get Mobile Control Information
/// </summary>
/// <param name="context"></param>
protected override void HandleGet(HttpCwsContext context)
{
try
@@ -50,14 +59,22 @@ namespace PepperDash.Essentials.WebApiHandlers
[JsonIgnore]
private readonly MobileControlSystemController mcController;
/// <summary>
/// Edge Server. Null if edge server is disabled
/// </summary>
[JsonProperty("edgeServer", NullValueHandling = NullValueHandling.Ignore)]
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)]
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)
{
mcController = controller;
@@ -72,24 +89,46 @@ namespace PepperDash.Essentials.WebApiHandlers
[JsonIgnore]
private readonly MobileControlSystemController mcController;
/// <summary>
/// Mobile Control Edge Server address for this system
/// </summary>
[JsonProperty("serverAddress")]
public string ServerAddress => mcController.Config == null ? "No Config" : mcController.Host;
/// <summary>
/// System Name for this system
/// </summary>
[JsonProperty("systemName")]
public string SystemName => mcController.RoomBridges.Count > 0 ? mcController.RoomBridges[0].RoomName : "No Config";
/// <summary>
/// System URL for this system
/// </summary>
[JsonProperty("systemUrl")]
public string SystemUrl => ConfigReader.ConfigObject.SystemUrl;
/// <summary>
/// User code to use in MC UI for this system
/// </summary>
[JsonProperty("userCode")]
public string UserCode => mcController.RoomBridges.Count > 0 ? mcController.RoomBridges[0].UserCode : "Not available";
/// <summary>
/// True if connected to edge server
/// </summary>
[JsonProperty("connected")]
public bool Connected => mcController.Connected;
/// <summary>
/// Seconds since last comms with edge server
/// </summary>
[JsonProperty("secondsSinceLastAck")]
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)
{
mcController = controller;
@@ -104,21 +143,43 @@ namespace PepperDash.Essentials.WebApiHandlers
[JsonIgnore]
private readonly MobileControlWebsocketServer directServer;
/// <summary>
/// URL to use to interact with this server
/// </summary>
[JsonProperty("userAppUrl")]
public string UserAppUrl => $"{directServer.UserAppUrlPrefix}/[insert_client_token]";
/// <summary>
/// TCP/IP Port this server is configured to use
/// </summary>
[JsonProperty("serverPort")]
public int ServerPort => directServer.Port;
/// <summary>
/// Count of defined tokens for this server
/// </summary>
[JsonProperty("tokensDefined")]
public int TokensDefined => directServer.UiClients.Count;
public int TokensDefined => directServer.UiClientContexts.Count;
/// <summary>
/// Count of connected clients
/// </summary>
[JsonProperty("clientsConnected")]
public int ClientsConnected => directServer.ConnectedUiClientsCount;
/// <summary>
/// List of tokens and connected clients for this server
/// </summary>
[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)
{
directServer = server;
@@ -142,33 +203,85 @@ namespace PepperDash.Essentials.WebApiHandlers
[JsonIgnore]
private readonly string urlPrefix;
/// <summary>
/// Client number for this client
/// </summary>
[JsonProperty("clientNumber")]
public string ClientNumber => $"{clientNumber}";
/// <summary>
/// Room Key for this client
/// </summary>
[JsonProperty("roomKey")]
public string RoomKey => context.Token.RoomKey;
/// <summary>
/// Touchpanel Key, if defined, for this client
/// </summary>
[JsonProperty("touchpanelKey")]
public string TouchpanelKey => context.Token.TouchpanelKey;
/// <summary>
/// URL for this client
/// </summary>
[JsonProperty("url")]
public string Url => $"{urlPrefix}{Key}";
/// <summary>
/// Token for this client
/// </summary>
[JsonProperty("token")]
public string Token => Key;
[JsonProperty("connected")]
public bool Connected => context.Client != null && context.Client.Context.WebSocket.IsAlive;
private readonly List<UiClient> clients;
[JsonProperty("duration")]
public double Duration => context.Client == null ? 0 : context.Client.ConnectedDuration.TotalSeconds;
/// <summary>
/// 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;
Key = clientContext.Key;
this.context = context.Value;
Key = context.Key;
clientNumber = index;
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
{
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)
{
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)
{
var req = context.Request;
@@ -65,6 +74,10 @@ namespace PepperDash.Essentials.WebApiHandlers
res.End();
}
/// <summary>
/// Handle DELETE request for a Client
/// </summary>
/// <param name="context"></param>
protected override void HandleDelete(HttpCwsContext context)
{
var req = context.Request;
@@ -93,7 +106,7 @@ namespace PepperDash.Essentials.WebApiHandlers
if (!server.UiClients.TryGetValue(request.Token, out UiClientContext clientContext))
if (!server.UiClientContexts.TryGetValue(request.Token, out UiClientContext clientContext))
{
var response = new ClientResponse
{
@@ -134,7 +147,7 @@ namespace PepperDash.Essentials.WebApiHandlers
return;
}
server.UiClients.Remove(request.Token);
server.UiClientContexts.Remove(request.Token);
server.UpdateSecret();

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")]
public string ClientId { get; set; }
/// <summary>
/// Room Key for this client
/// </summary>
[JsonProperty("roomKey")]
public string RoomKey { get; set; }
/// <summary>
/// System UUID for this system
/// </summary>
[JsonProperty("systemUUid")]
public string SystemUuid { get; set; }

View File

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

View File

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

View File

@@ -13,8 +13,15 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary>
public string GrantCode { get; set; }
/// <summary>
/// Gets or sets the Tokens for this server
/// </summary>
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)
{
GrantCode = grantCode;

View File

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

View File

@@ -14,6 +14,10 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary>
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)
{
Token = token;

View File

@@ -8,12 +8,25 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary>
public class Version
{
/// <summary>
/// Server version this Websocket is connected to
/// </summary>
[JsonProperty("serverVersion")]
public string ServerVersion { get; set; }
/// <summary>
/// True if the server is on a processor
/// </summary>
[JsonProperty("serverIsRunningOnProcessorHardware")]
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()
{
ServerIsRunningOnProcessorHardware = true;

View File

@@ -13,25 +13,28 @@ namespace PepperDash.Essentials.WebSocketServer
}
/// <summary>
/// Represents a WebSocketServerSecret
/// Stores a secret value using the provided secret store provider
/// </summary>
public class WebSocketServerSecret : ISecret
{
/// <summary>
/// Gets or sets the Provider
/// Gets the Secret Provider associated with this secret
/// </summary>
public ISecretProvider Provider { get; private set; }
/// <summary>
/// Gets or sets the Key
/// Gets the Key associated with this secret
/// </summary>
public string Key { get; private set; }
/// <summary>
/// Gets or sets the Value
/// Gets the Value associated with this secret
/// </summary>
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)
{
Key = key;