diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessengerWithSubscriptions.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessengerWithSubscriptions.cs
new file mode 100644
index 00000000..887f1789
--- /dev/null
+++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessengerWithSubscriptions.cs
@@ -0,0 +1,23 @@
+using PepperDash.Core;
+
+namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
+{
+ ///
+ /// Defines the contract for IMobileControlMessenger
+ ///
+ public interface IMobileControlMessengerWithSubscriptions : IMobileControlMessenger
+ {
+ ///
+ /// Unsubscribe a client from this messenger
+ ///
+ ///
+ void UnsubscribeClient(string clientId);
+
+ ///
+ /// Register this messenger with the AppServerController
+ ///
+ /// parent for this messenger
+ /// Enable messenger subscriptions
+ void RegisterWithAppServer(IMobileControl appServerController, bool enableMessengerSubscriptions);
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs
index 9ed9029b..a7210a13 100644
--- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using Newtonsoft.Json;
+using Crestron.SimplSharp.Net;
using Newtonsoft.Json.Linq;
using PepperDash.Core;
using PepperDash.Core.Logging;
@@ -13,13 +13,26 @@ namespace PepperDash.Essentials.AppServer.Messengers
///
/// Provides a messaging bridge
///
- public abstract class MessengerBase : EssentialsDevice, IMobileControlMessenger
+ public abstract class MessengerBase : EssentialsDevice, IMobileControlMessengerWithSubscriptions
{
///
/// The device this messenger is associated with
///
protected IKeyName _device;
+ ///
+ /// Enable subscriptions
+ ///
+ protected bool enableMessengerSubscriptions;
+
+ ///
+ /// List of clients subscribed to this messenger
+ ///
+ ///
+ /// Unsoliciited feedback from a device in a messenger will ONLY be sent to devices in this subscription list. When a client disconnects, it's ID will be removed from the collection.
+ ///
+ protected HashSet SubscriberIds = new HashSet();
+
private readonly List _deviceInterfaces;
private readonly Dictionary> _actions = new Dictionary>();
@@ -93,6 +106,21 @@ namespace PepperDash.Essentials.AppServer.Messengers
RegisterActions();
}
+ ///
+ /// Register this messenger with appserver controller
+ ///
+ /// Parent controller for this messenger
+ /// Enable subscriptions
+ public void RegisterWithAppServer(IMobileControl appServerController, bool enableMessengerSubscriptions)
+ {
+ this.enableMessengerSubscriptions = enableMessengerSubscriptions;
+ AppServerController = appServerController ?? throw new ArgumentNullException("appServerController");
+
+ AppServerController.AddAction(this, HandleMessage);
+
+ RegisterActions();
+ }
+
private void HandleMessage(string path, string id, JToken content)
{
// replace base path with empty string. Should leave something like /fullStatus
@@ -103,7 +131,7 @@ namespace PepperDash.Essentials.AppServer.Messengers
return;
}
- Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Executing action for path {path}", this, path);
+ this.LogDebug("Executing action for path {path}", path);
action(id, content);
}
@@ -117,7 +145,6 @@ namespace PepperDash.Essentials.AppServer.Messengers
{
if (_actions.ContainsKey(path))
{
- //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, $"Messenger {Key} already has action registered at {path}", this);
return;
}
@@ -154,6 +181,52 @@ namespace PepperDash.Essentials.AppServer.Messengers
}
+ ///
+ /// Add client to the susbscription list for unsolicited feedback
+ ///
+ /// Client ID to add
+ protected void SubscribeClient(string clientId)
+ {
+ if (!enableMessengerSubscriptions)
+ {
+ this.LogWarning("Messenger subscriptions not enabled");
+ return;
+ }
+
+ if (SubscriberIds.Any(id => id == clientId))
+ {
+ this.LogVerbose("Client {clientId} already subscribed", clientId);
+ return;
+ }
+
+ SubscriberIds.Add(clientId);
+
+ this.LogDebug("Client {clientId} subscribed", clientId);
+ }
+
+ ///
+ /// Remove a client from the subscription list
+ ///
+ /// Client ID to remove
+ public void UnsubscribeClient(string clientId)
+ {
+ if (!enableMessengerSubscriptions)
+ {
+ this.LogWarning("Messenger subscriptions not enabled");
+ return;
+ }
+
+ if (!SubscriberIds.Any(i => i == clientId))
+ {
+ this.LogVerbose("Client with ID {clientId} is not subscribed", clientId);
+ return;
+ }
+
+ SubscriberIds.RemoveWhere((i) => i == clientId);
+
+ this.LogInformation("Client with ID {clientId} unsubscribed", clientId);
+ }
+
///
/// Helper for posting status message
///
@@ -228,11 +301,33 @@ namespace PepperDash.Essentials.AppServer.Messengers
{
try
{
+ // Allow for legacy method to continue without subscriptions
+ if (!enableMessengerSubscriptions)
+ {
+ AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = clientId, Content = content });
+ return;
+ }
+
+ // handle subscription feedback
+ // If client is null or empty, this message is unsolicited feedback. Iterate through the subscriber list and send to all interested parties
+ if (string.IsNullOrEmpty(clientId))
+ {
+ foreach (var client in SubscriberIds)
+ {
+ AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = client, Content = content });
+ }
+
+ return;
+ }
+
+ SubscribeClient(clientId);
+
AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = clientId, Content = content });
}
catch (Exception ex)
{
- Debug.LogMessage(ex, "Exception posting status message", this);
+ this.LogError("Exception posting status message: {message}", ex.Message);
+ this.LogDebug(ex, "Stack Trace: ");
}
}
diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs b/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs
index 6c130f6d..ec7219a3 100644
--- a/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs
+++ b/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs
@@ -38,6 +38,12 @@ namespace PepperDash.Essentials
///
[JsonProperty("enableApiServer")]
public bool EnableApiServer { get; set; } = true;
+
+ ///
+ /// Enable subscriptions for Messengers
+ ///
+ [JsonProperty("enableMessengerSubscriptions")]
+ public bool EnableMessengerSubscriptions { get; set; }
}
///
@@ -78,6 +84,15 @@ namespace PepperDash.Essentials
[JsonProperty("csLanUiDeviceKeys")]
public List CSLanUiDeviceKeys { get; set; }
+ ///
+ /// Get or set the Secure property
+ ///
+ ///
+ /// Indicates whether the connection is secure (HTTPS).
+ ///
+ [JsonProperty("Secure")]
+ public bool Secure { get; set; }
+
///
/// Initializes a new instance of the MobileControlDirectServerPropertiesConfig class.
///
diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs
index 736bf69c..bd04b0e3 100644
--- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs
+++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs
@@ -54,7 +54,10 @@ namespace PepperDash.Essentials
StringComparer.InvariantCultureIgnoreCase
);
- public Dictionary> ActionDictionary => _actionDictionary;
+ ///
+ /// Actions
+ ///
+ public ReadOnlyDictionary> ActionDictionary => new ReadOnlyDictionary>(_actionDictionary);
private readonly GenericQueue _receiveQueue;
private readonly List _roomBridges =
@@ -66,6 +69,16 @@ namespace PepperDash.Essentials
private readonly Dictionary _defaultMessengers =
new Dictionary();
+ ///
+ /// Get the custom messengers with subscriptions
+ ///
+ public ReadOnlyDictionary Messengers => new ReadOnlyDictionary(_messengers.Values.OfType().ToDictionary(k => k.Key, v => v));
+
+ ///
+ /// Get the default messengers
+ ///
+ public ReadOnlyDictionary DefaultMessengers => new ReadOnlyDictionary(_defaultMessengers.Values.OfType().ToDictionary(k => k.Key, v => v));
+
private readonly GenericQueue _transmitToServerQueue;
private readonly GenericQueue _transmitToClientsQueue;
@@ -1199,8 +1212,7 @@ namespace PepperDash.Essentials
if (_initialized)
{
- this.LogDebug("Registering messenger {messengerKey} AFTER initialization", messenger.Key);
- messenger.RegisterWithAppServer(this);
+ RegisterMessengerWithServer(messenger);
}
}
@@ -1241,6 +1253,12 @@ namespace PepperDash.Essentials
messenger.MessagePath
);
+ if (messenger is IMobileControlMessengerWithSubscriptions subMessenger)
+ {
+ subMessenger.RegisterWithAppServer(this, Config.EnableMessengerSubscriptions);
+ return;
+ }
+
messenger.RegisterWithAppServer(this);
}
diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs
index f2f5003a..eb60857e 100644
--- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs
+++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs
@@ -4,6 +4,8 @@ using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net.Http;
+using System.Security.Authentication;
+using System.Security.Cryptography.X509Certificates;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharp.WebScripting;
@@ -35,6 +37,10 @@ namespace PepperDash.Essentials.WebSocketServer
private readonly string appConfigFileName = "_config.local.json";
private readonly string appConfigCsFileName = "_config.cs.json";
+ private const string certificateName = "selfCres";
+
+ private const string certificatePassword = "cres12345";
+
///
/// Where the key is the join token and the value is the room key
///
@@ -260,7 +266,41 @@ namespace PepperDash.Essentials.WebSocketServer
_server.OnPost += Server_OnPost;
}
- _server.Log.Output = (data, level) => this.LogInformation("WebSocket Server Log [{level}]: {data}", level, data);
+ if (_parent.Config.DirectServer.Secure)
+ {
+ this.LogInformation("Adding SSL Configuration to server");
+ _server.SslConfiguration = new ServerSslConfiguration(new X509Certificate2($"\\user\\{certificateName}.pfx", certificatePassword))
+ {
+ ClientCertificateRequired = false,
+ CheckCertificateRevocation = false,
+ EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11
+ };
+ }
+
+ _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;
+ }
+ };
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs
index d1d4524d..3cdd183b 100644
--- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs
+++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs
@@ -22,14 +22,29 @@ namespace PepperDash.Essentials.WebSocketServer
///
public string Key { get; private set; }
+ ///
+ /// Gets or sets the mobile control system controller that handles this client's messages
+ ///
public MobileControlSystemController Controller { get; set; }
+ ///
+ /// Gets or sets the room key that this client is associated with
+ ///
public string RoomKey { get; set; }
+ ///
+ /// The unique identifier for this client instance
+ ///
private string _clientId;
+ ///
+ /// The timestamp when this client connection was established
+ ///
private DateTime _connectionTime;
+ ///
+ /// Gets the duration that this client has been connected. Returns zero if not currently connected.
+ ///
public TimeSpan ConnectedDuration
{
get
@@ -45,6 +60,10 @@ namespace PepperDash.Essentials.WebSocketServer
}
}
+ ///
+ /// Initializes a new instance of the UiClient class with the specified key
+ ///
+ /// The unique key to identify this client
public UiClient(string key)
{
Key = key;
@@ -99,11 +118,21 @@ namespace PepperDash.Essentials.WebSocketServer
// TODO: Future: Check token to see if there's already an open session using that token and reject/close the session
}
+ ///
+ /// Handles the UserCodeChanged event from a room bridge and sends the updated user code to the client
+ ///
+ /// The room bridge that raised the event
+ /// Event arguments
private void Bridge_UserCodeChanged(object sender, EventArgs e)
{
SendUserCodeToClient((MobileControlEssentialsRoomBridge)sender, _clientId);
}
+ ///
+ /// Sends the current user code and QR code URL to the specified client
+ ///
+ /// The room bridge containing the user code information
+ /// The ID of the client to send the information to
private void SendUserCodeToClient(MobileControlBridgeBase bridge, string clientId)
{
var content = new
@@ -140,6 +169,16 @@ namespace PepperDash.Essentials.WebSocketServer
base.OnClose(e);
this.LogInformation("WebSocket UiClient Closing: {code} reason: {reason}", e.Code, e.Reason);
+
+ foreach (var messenger in Controller.Messengers)
+ {
+ messenger.Value.UnsubscribeClient(_clientId);
+ }
+
+ foreach (var messenger in Controller.DefaultMessengers)
+ {
+ messenger.Value.UnsubscribeClient(_clientId);
+ }
}
///