mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-01-11 19:44:52 +00:00
feat: enable subscription logic for messengers
In order to help control traffic over the websocket, a subscription feature has been added: * A config option, `enableMessengerSubscriptions` has been added * When true, the MessengerBase class will assume that any message sent using the `PostStatusMessage` that has a valid client ID wants to send any subsequent unsolicited updates to that same client * The client's ID will be added a list of subscribed client IDs * Any subsequent messages sent using the `PostStatusMessage` methods that have a null clientId will ONLY be sent to subscribed clients * When a client disconnects, it will be removed from the list of subscribed clients This should cut down drastically on the traffic to the UI, especially when combined with requesting partial status updates from a device rather than the entire state.
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
using PepperDash.Core;
|
||||
|
||||
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the contract for IMobileControlMessenger
|
||||
/// </summary>
|
||||
public interface IMobileControlMessengerWithSubscriptions : IMobileControlMessenger
|
||||
{
|
||||
/// <summary>
|
||||
/// Unsubscribe a client from this messenger
|
||||
/// </summary>
|
||||
/// <param name="clientId"></param>
|
||||
void UnsubscribeClient(string clientId);
|
||||
|
||||
/// <summary>
|
||||
/// Register this messenger with the AppServerController
|
||||
/// </summary>
|
||||
/// <param name="appServerController">parent for this messenger</param>
|
||||
/// <param name="enableMessengerSubscriptions">Enable messenger subscriptions</param>
|
||||
void RegisterWithAppServer(IMobileControl appServerController, bool enableMessengerSubscriptions);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// Provides a messaging bridge
|
||||
/// </summary>
|
||||
public abstract class MessengerBase : EssentialsDevice, IMobileControlMessenger
|
||||
public abstract class MessengerBase : EssentialsDevice, IMobileControlMessengerWithSubscriptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The device this messenger is associated with
|
||||
/// </summary>
|
||||
protected IKeyName _device;
|
||||
|
||||
/// <summary>
|
||||
/// Enable subscriptions
|
||||
/// </summary>
|
||||
protected bool enableMessengerSubscriptions;
|
||||
|
||||
/// <summary>
|
||||
/// List of clients subscribed to this messenger
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
protected HashSet<string> SubscriberIds = new HashSet<string>();
|
||||
|
||||
private readonly List<string> _deviceInterfaces;
|
||||
|
||||
private readonly Dictionary<string, Action<string, JToken>> _actions = new Dictionary<string, Action<string, JToken>>();
|
||||
@@ -93,6 +106,21 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||
RegisterActions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register this messenger with appserver controller
|
||||
/// </summary>
|
||||
/// <param name="appServerController">Parent controller for this messenger</param>
|
||||
/// <param name="enableMessengerSubscriptions">Enable subscriptions</param>
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add client to the susbscription list for unsolicited feedback
|
||||
/// </summary>
|
||||
/// <param name="clientId">Client ID to add</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a client from the subscription list
|
||||
/// </summary>
|
||||
/// <param name="clientId">Client ID to remove</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper for posting status message
|
||||
/// </summary>
|
||||
@@ -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: ");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,12 @@ namespace PepperDash.Essentials
|
||||
/// </summary>
|
||||
[JsonProperty("enableApiServer")]
|
||||
public bool EnableApiServer { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Enable subscriptions for Messengers
|
||||
/// </summary>
|
||||
[JsonProperty("enableMessengerSubscriptions")]
|
||||
public bool EnableMessengerSubscriptions { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -78,6 +84,15 @@ namespace PepperDash.Essentials
|
||||
[JsonProperty("csLanUiDeviceKeys")]
|
||||
public List<string> CSLanUiDeviceKeys { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get or set the Secure property
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Indicates whether the connection is secure (HTTPS).
|
||||
/// </remarks>
|
||||
[JsonProperty("Secure")]
|
||||
public bool Secure { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the MobileControlDirectServerPropertiesConfig class.
|
||||
/// </summary>
|
||||
|
||||
@@ -54,7 +54,10 @@ namespace PepperDash.Essentials
|
||||
StringComparer.InvariantCultureIgnoreCase
|
||||
);
|
||||
|
||||
public Dictionary<string, List<IMobileControlAction>> ActionDictionary => _actionDictionary;
|
||||
/// <summary>
|
||||
/// Actions
|
||||
/// </summary>
|
||||
public ReadOnlyDictionary<string, List<IMobileControlAction>> ActionDictionary => new ReadOnlyDictionary<string, List<IMobileControlAction>>(_actionDictionary);
|
||||
|
||||
private readonly GenericQueue _receiveQueue;
|
||||
private readonly List<MobileControlBridgeBase> _roomBridges =
|
||||
@@ -66,6 +69,16 @@ namespace PepperDash.Essentials
|
||||
private readonly Dictionary<string, IMobileControlMessenger> _defaultMessengers =
|
||||
new Dictionary<string, IMobileControlMessenger>();
|
||||
|
||||
/// <summary>
|
||||
/// Get the custom messengers with subscriptions
|
||||
/// </summary>
|
||||
public ReadOnlyDictionary<string, IMobileControlMessengerWithSubscriptions> Messengers => new ReadOnlyDictionary<string, IMobileControlMessengerWithSubscriptions>(_messengers.Values.OfType<IMobileControlMessengerWithSubscriptions>().ToDictionary(k => k.Key, v => v));
|
||||
|
||||
/// <summary>
|
||||
/// Get the default messengers
|
||||
/// </summary>
|
||||
public ReadOnlyDictionary<string, IMobileControlMessengerWithSubscriptions> DefaultMessengers => new ReadOnlyDictionary<string, IMobileControlMessengerWithSubscriptions>(_defaultMessengers.Values.OfType<IMobileControlMessengerWithSubscriptions>().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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
/// <summary>
|
||||
/// Where the key is the join token and the value is the room key
|
||||
/// </summary>
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -22,14 +22,29 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||
/// <inheritdoc />
|
||||
public string Key { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the mobile control system controller that handles this client's messages
|
||||
/// </summary>
|
||||
public MobileControlSystemController Controller { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the room key that this client is associated with
|
||||
/// </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>
|
||||
private DateTime _connectionTime;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the duration that this client has been connected. Returns zero if not currently connected.
|
||||
/// </summary>
|
||||
public TimeSpan ConnectedDuration
|
||||
{
|
||||
get
|
||||
@@ -45,6 +60,10 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the UserCodeChanged event from a room bridge and sends the updated user code to the client
|
||||
/// </summary>
|
||||
/// <param name="sender">The room bridge that raised the event</param>
|
||||
/// <param name="e">Event arguments</param>
|
||||
private void Bridge_UserCodeChanged(object sender, EventArgs e)
|
||||
{
|
||||
SendUserCodeToClient((MobileControlEssentialsRoomBridge)sender, _clientId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the current user code and QR code URL to the specified client
|
||||
/// </summary>
|
||||
/// <param name="bridge">The room bridge containing the user code information</param>
|
||||
/// <param name="clientId">The ID of the client to send the information to</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
Reference in New Issue
Block a user