From d9243def307bcdb8b592baf8a7c3bfd97f86063a Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 22 Sep 2025 14:22:57 -0600 Subject: [PATCH 01/55] feat: Adds ability to read configs generated from v2 config tool that are pre-merged don't have system or template objects Refactor config handling and improve documentation - Updated `PortalConfigReader.cs` to use constants for configuration keys, enhancing maintainability and readability. Improved error logging with `Debug.LogError`. - Modified `ConfigReader.cs` to handle v2 configuration format, streamlining the loading process and avoiding redundant parsing. - Added XML documentation comments to properties in `EssentialsConfig.cs`, improving code documentation. Initialized `Rooms` property in the constructor. - Enhanced `SystemTemplateConfigs` class with XML documentation for better clarity on its properties. --- .../Config/PortalConfigReader.cs | 111 ++++++++++-------- .../Config/Essentials/ConfigReader.cs | 31 +++-- .../Config/Essentials/EssentialsConfig.cs | 33 ++++-- 3 files changed, 112 insertions(+), 63 deletions(-) diff --git a/src/PepperDash.Core/Config/PortalConfigReader.cs b/src/PepperDash.Core/Config/PortalConfigReader.cs index ffb2d16b..b048bdc4 100644 --- a/src/PepperDash.Core/Config/PortalConfigReader.cs +++ b/src/PepperDash.Core/Config/PortalConfigReader.cs @@ -9,40 +9,59 @@ using Serilog.Events; namespace PepperDash.Core.Config { + + /// /// Reads a Portal formatted config file /// public class PortalConfigReader { - /// - /// Reads the config file, checks if it needs a merge, merges and saves, then returns the merged Object. - /// - /// JObject of config file - public static void ReadAndMergeFileIfNecessary(string filePath, string savePath) + const string template = "template"; + const string system = "system"; + const string systemUrl = "system_url"; + const string templateUrl = "template_url"; + const string info = "info"; + const string devices = "devices"; + const string rooms = "rooms"; + const string sourceLists = "sourceLists"; + const string destinationLists = "destinationLists"; + const string cameraLists = "cameraLists"; + const string audioControlPointLists = "audioControlPointLists"; + + const string tieLines = "tieLines"; + const string joinMaps = "joinMaps"; + const string global = "global"; + + + /// + /// Reads the config file, checks if it needs a merge, merges and saves, then returns the merged Object. + /// + /// JObject of config file + public static void ReadAndMergeFileIfNecessary(string filePath, string savePath) { try { if (!File.Exists(filePath)) { - Debug.Console(1, Debug.ErrorLogLevel.Error, + Debug.LogError( "ERROR: Configuration file not present. Please load file to {0} and reset program", filePath); } using (StreamReader fs = new StreamReader(filePath)) { var jsonObj = JObject.Parse(fs.ReadToEnd()); - if(jsonObj["template"] != null && jsonObj["system"] != null) + if(jsonObj[template] != null && jsonObj[system] != null) { // it's a double-config, merge it. var merged = MergeConfigs(jsonObj); - if (jsonObj["system_url"] != null) + if (jsonObj[systemUrl] != null) { - merged["systemUrl"] = jsonObj["system_url"].Value(); + merged[systemUrl] = jsonObj[systemUrl].Value(); } - if (jsonObj["template_url"] != null) + if (jsonObj[templateUrl] != null) { - merged["templateUrl"] = jsonObj["template_url"].Value(); + merged[templateUrl] = jsonObj[templateUrl].Value(); } jsonObj = merged; @@ -77,62 +96,62 @@ namespace PepperDash.Core.Config var merged = new JObject(); // Put together top-level objects - if (system["info"] != null) - merged.Add("info", Merge(template["info"], system["info"], "infO")); + if (system[info] != null) + merged.Add(info, Merge(template[info], system[info], info)); else - merged.Add("info", template["info"]); + merged.Add(info, template[info]); - merged.Add("devices", MergeArraysOnTopLevelProperty(template["devices"] as JArray, - system["devices"] as JArray, "key", "devices")); + merged.Add(devices, MergeArraysOnTopLevelProperty(template[devices] as JArray, + system[devices] as JArray, "key", devices)); - if (system["rooms"] == null) - merged.Add("rooms", template["rooms"]); + if (system[rooms] == null) + merged.Add(rooms, template[rooms]); else - merged.Add("rooms", MergeArraysOnTopLevelProperty(template["rooms"] as JArray, - system["rooms"] as JArray, "key", "rooms")); + merged.Add(rooms, MergeArraysOnTopLevelProperty(template[rooms] as JArray, + system[rooms] as JArray, "key", rooms)); - if (system["sourceLists"] == null) - merged.Add("sourceLists", template["sourceLists"]); + if (system[sourceLists] == null) + merged.Add(sourceLists, template[sourceLists]); else - merged.Add("sourceLists", Merge(template["sourceLists"], system["sourceLists"], "sourceLists")); + merged.Add(sourceLists, Merge(template[sourceLists], system[sourceLists], sourceLists)); - if (system["destinationLists"] == null) - merged.Add("destinationLists", template["destinationLists"]); + if (system[destinationLists] == null) + merged.Add(destinationLists, template[destinationLists]); else - merged.Add("destinationLists", - Merge(template["destinationLists"], system["destinationLists"], "destinationLists")); + merged.Add(destinationLists, + Merge(template[destinationLists], system[destinationLists], destinationLists)); - if (system["cameraLists"] == null) - merged.Add("cameraLists", template["cameraLists"]); + if (system[cameraLists] == null) + merged.Add(cameraLists, template[cameraLists]); else - merged.Add("cameraLists", Merge(template["cameraLists"], system["cameraLists"], "cameraLists")); + merged.Add(cameraLists, Merge(template[cameraLists], system[cameraLists], cameraLists)); - if (system["audioControlPointLists"] == null) - merged.Add("audioControlPointLists", template["audioControlPointLists"]); + if (system[audioControlPointLists] == null) + merged.Add(audioControlPointLists, template[audioControlPointLists]); else - merged.Add("audioControlPointLists", - Merge(template["audioControlPointLists"], system["audioControlPointLists"], "audioControlPointLists")); + merged.Add(audioControlPointLists, + Merge(template[audioControlPointLists], system[audioControlPointLists], audioControlPointLists)); // Template tie lines take precedence. Config tool doesn't do them at system // level anyway... - if (template["tieLines"] != null) - merged.Add("tieLines", template["tieLines"]); - else if (system["tieLines"] != null) - merged.Add("tieLines", system["tieLines"]); + if (template[tieLines] != null) + merged.Add(tieLines, template[tieLines]); + else if (system[tieLines] != null) + merged.Add(tieLines, system[tieLines]); else - merged.Add("tieLines", new JArray()); + merged.Add(tieLines, new JArray()); - if (template["joinMaps"] != null) - merged.Add("joinMaps", template["joinMaps"]); + if (template[joinMaps] != null) + merged.Add(joinMaps, template[joinMaps]); else - merged.Add("joinMaps", new JObject()); + merged.Add(joinMaps, new JObject()); - if (system["global"] != null) - merged.Add("global", Merge(template["global"], system["global"], "global")); + if (system[global] != null) + merged.Add(global, Merge(template[global], system[global], global)); else - merged.Add("global", template["global"]); + merged.Add(global, template[global]); //Debug.Console(2, "MERGED CONFIG RESULT: \x0d\x0a{0}", merged); return merged; @@ -228,7 +247,7 @@ namespace PepperDash.Core.Config } catch (Exception e) { - Debug.Console(1, Debug.ErrorLogLevel.Warning, "Cannot merge items at path {0}: \r{1}", propPath, e); + Debug.LogError("Cannot merge items at path {propPath}: \r{e}", propPath, e); } } } diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs index b93de1c4..ea5d2672 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs @@ -124,22 +124,35 @@ namespace PepperDash.Essentials.Core.Config Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded Local Config"); return true; - } + } else { - var doubleObj = JObject.Parse(fs.ReadToEnd()); - ConfigObject = PortalConfigReader.MergeConfigs(doubleObj).ToObject(); + var parsedConfig = JObject.Parse(fs.ReadToEnd()); - // Extract SystemUrl and TemplateUrl into final config output - - if (doubleObj["system_url"] != null) + // Check if it's a v2 config (missing "system" or "template" nodes) + // this means it's already merged by the Portal API + // from the v2 config tool + var isV2Config = parsedConfig["system"] == null || parsedConfig["template"] == null; + + if (isV2Config) { - ConfigObject.SystemUrl = doubleObj["system_url"].Value(); + Debug.LogMessage(LogEventLevel.Information, "Config file is a v2 format, no merge necessary."); + ConfigObject = parsedConfig.ToObject(); + Debug.LogMessage(LogEventLevel.Information, "Successfully Loaded v2 Config"); + return true; } - if (doubleObj["template_url"] != null) + // Extract SystemUrl and TemplateUrl into final config output + ConfigObject = PortalConfigReader.MergeConfigs(parsedConfig).ToObject(); + + if (parsedConfig["system_url"] != null) { - ConfigObject.TemplateUrl = doubleObj["template_url"].Value(); + ConfigObject.SystemUrl = parsedConfig["system_url"].Value(); + } + + if (parsedConfig["template_url"] != null) + { + ConfigObject.TemplateUrl = parsedConfig["template_url"].Value(); } } diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs index 630ffbdf..e1a46b54 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs @@ -16,13 +16,21 @@ namespace PepperDash.Essentials.Core.Config /// public class EssentialsConfig : BasicConfig { - [JsonProperty("system_url")] + /// + /// Gets or sets the SystemUrl + /// + [JsonProperty("system_url")] public string SystemUrl { get; set; } - [JsonProperty("template_url")] + /// + /// Gets or sets the TemplateUrl + /// + [JsonProperty("template_url")] public string TemplateUrl { get; set; } - + /// + /// Gets the SystemUuid extracted from the SystemUrl + /// [JsonProperty("systemUuid")] public string SystemUuid { @@ -45,6 +53,9 @@ namespace PepperDash.Essentials.Core.Config } } + /// + /// Gets the TemplateUuid extracted from the TemplateUrl + /// [JsonProperty("templateUuid")] public string TemplateUuid { @@ -67,13 +78,16 @@ namespace PepperDash.Essentials.Core.Config } } - [JsonProperty("rooms")] /// /// Gets or sets the Rooms /// + [JsonProperty("rooms")] + public List Rooms { get; set; } - + /// + /// Initializes a new instance of the class. + /// public EssentialsConfig() : base() { @@ -86,11 +100,14 @@ namespace PepperDash.Essentials.Core.Config /// public class SystemTemplateConfigs { - /// - /// Gets or sets the System - /// + /// + /// Gets or sets the System + /// public EssentialsConfig System { get; set; } + /// + /// Gets or sets the Template + /// public EssentialsConfig Template { get; set; } } } \ No newline at end of file From ff46fb8f29f69dc3953d701184c4d7e4b2750a54 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 22 Sep 2025 14:55:58 -0600 Subject: [PATCH 02/55] feat: Add versioning support to EssentialsConfig Introduce `Versions` property in `EssentialsConfig` to hold version information. Add `VersionData` class for Essentials and package versions, and `NugetVersion` class for individual package details. Retain and document `SystemTemplateConfigs` class. --- .../Config/Essentials/EssentialsConfig.cs | 59 +++++++++++++++++-- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs index e1a46b54..9665938f 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs @@ -85,6 +85,11 @@ namespace PepperDash.Essentials.Core.Config public List Rooms { get; set; } + /// + /// Gets or sets the Versions + /// + public VersionData Versions { get; set; } + /// /// Initializes a new instance of the class. /// @@ -94,11 +99,55 @@ namespace PepperDash.Essentials.Core.Config Rooms = new List(); } } - - /// - /// Represents a SystemTemplateConfigs - /// - public class SystemTemplateConfigs + + /// + /// Represents version data for Essentials and its packages + /// + public class VersionData + { + /// + /// Gets or sets the Essentials version + /// + [JsonProperty("essentials")] + public NugetVersion Essentials { get; set; } + + /// + /// Gets or sets the list of Packages + /// + [JsonProperty("packages")] + public List Packages { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public VersionData() + { + Packages = new List(); + } + } + + /// + /// Represents a NugetVersion + /// + public class NugetVersion + { + /// + /// Gets or sets the Version + /// + [JsonProperty("version")] + public string Version { get; set; } + + /// + /// Gets or sets the PackageId + /// + [JsonProperty("packageId")] + public string PackageId { get; set; } + } + + /// + /// Represents a SystemTemplateConfigs + /// + public class SystemTemplateConfigs { /// /// Gets or sets the System From 9de94bd65ff5733464cc190f047087198af14cbd Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 22 Sep 2025 15:05:06 -0600 Subject: [PATCH 03/55] fix: Update v2 config detection criteria Changed the logic for identifying v2 configuration files. The check now looks for the presence of a "versions" node instead of the absence of "system" or "template" nodes, reflecting an update in the configuration file structure. --- .../Config/Essentials/ConfigReader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs index ea5d2672..2591e8aa 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/ConfigReader.cs @@ -129,10 +129,10 @@ namespace PepperDash.Essentials.Core.Config { var parsedConfig = JObject.Parse(fs.ReadToEnd()); - // Check if it's a v2 config (missing "system" or "template" nodes) + // Check if it's a v2 config (check for "version" node) // this means it's already merged by the Portal API // from the v2 config tool - var isV2Config = parsedConfig["system"] == null || parsedConfig["template"] == null; + var isV2Config = parsedConfig["versions"] != null; if (isV2Config) { From 8525134ae70d508336181cca8f86088003945b65 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 15 Oct 2025 09:50:12 -0500 Subject: [PATCH 04/55] fix: direct server clients now have unique client IDs Using the generated security token as an ID was presenting problems with duplicate connections using the same ID and trying to figure out where to send the messages. Now, the clients have a unique ID that's an increasing integer that restarts at 1 when the program restarts. --- .../MessageToClients.cs | 90 ++++++ .../MobileControlSystemController.cs | 28 +- .../TransmitMessage.cs | 97 ++---- .../Utilities.cs | 97 ++++++ .../WebApiHandlers/MobileInfoHandler.cs | 12 +- .../WebApiHandlers/UiClientHandler.cs | 4 +- .../ConnectionClosedEventArgs.cs | 24 ++ .../WebSocketServer/JoinToken.cs | 4 + .../MobileControlWebsocketServer.cs | 286 ++++++++++-------- .../WebSocketServer/UiClient.cs | 57 +++- 10 files changed, 444 insertions(+), 255 deletions(-) create mode 100644 src/PepperDash.Essentials.MobileControl/MessageToClients.cs create mode 100644 src/PepperDash.Essentials.MobileControl/Utilities.cs create mode 100644 src/PepperDash.Essentials.MobileControl/WebSocketServer/ConnectionClosedEventArgs.cs diff --git a/src/PepperDash.Essentials.MobileControl/MessageToClients.cs b/src/PepperDash.Essentials.MobileControl/MessageToClients.cs new file mode 100644 index 00000000..e1aa5eb5 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/MessageToClients.cs @@ -0,0 +1,90 @@ +using System; +using System.Threading; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core.Queues; +using PepperDash.Essentials.WebSocketServer; +using Serilog.Events; + +namespace PepperDash.Essentials +{ + /// + /// Represents a MessageToClients + /// + public class MessageToClients : IQueueMessage + { + private readonly MobileControlWebsocketServer _server; + private readonly object msgToSend; + + /// + /// Message to send to Direct Server Clients + /// + /// message object to send + /// WebSocket server instance + public MessageToClients(object msg, MobileControlWebsocketServer server) + { + _server = server; + msgToSend = msg; + } + + /// + /// Message to send to Direct Server Clients + /// + /// message object to send + /// WebSocket server instance + public MessageToClients(DeviceStateMessageBase msg, MobileControlWebsocketServer server) + { + _server = server; + msgToSend = msg; + } + + #region Implementation of IQueueMessage + + /// + /// Dispatch method + /// + public void Dispatch() + { + try + { + if (_server == null) + { + Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Server is null"); + return; + } + + var message = JsonConvert.SerializeObject(msgToSend, Formatting.None, + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } }); + + var clientSpecificMessage = msgToSend as MobileControlMessage; + if (clientSpecificMessage.ClientId != null) + { + var clientId = clientSpecificMessage.ClientId; + + _server.LogVerbose("Message TX To client {clientId}: {message}", clientId, message); + + _server.SendMessageToClient(clientId, message); + + return; + } + + _server.SendMessageToAllClients(message); + + _server.LogVerbose("Message TX To all clients: {message}", message); + } + catch (ThreadAbortException) + { + //Swallowing this exception, as it occurs on shutdown and there's no need to print out a scary stack trace + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Caught an exception in the Transmit Processor"); + } + } + #endregion + } + +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index d6a40ede..0b320185 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -1382,33 +1382,13 @@ namespace PepperDash.Essentials { Log = { - Output = (data, message) => - { - switch (data.Level) - { - case LogLevel.Trace: - this.LogVerbose(data.Message); - break; - case LogLevel.Debug: - this.LogDebug(data.Message); - break; - case LogLevel.Info: - this.LogInformation(data.Message); - break; - case LogLevel.Warn: - this.LogWarning(data.Message); - break; - case LogLevel.Error: - this.LogError(data.Message); - break; - case LogLevel.Fatal: - this.LogFatal(data.Message); - break; - } - } + Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this) } }; + // setting to trace to let level be controlled by appdebug + _wsClient2.Log.Level = LogLevel.Trace; + _wsClient2.SslConfiguration.EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls12; diff --git a/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs b/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs index 38f7944f..06595a9d 100644 --- a/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs +++ b/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs @@ -1,13 +1,9 @@ -using Newtonsoft.Json; +using System; +using Newtonsoft.Json; using Newtonsoft.Json.Converters; using PepperDash.Core; -using PepperDash.Core.Logging; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core.Queues; -using PepperDash.Essentials.WebSocketServer; -using Serilog.Events; -using System; -using System.Threading; using WebSocketSharp; namespace PepperDash.Essentials @@ -20,12 +16,22 @@ namespace PepperDash.Essentials private readonly WebSocket _ws; private readonly object msgToSend; + /// + /// Initialize a message to send + /// + /// message object to send + /// WebSocket instance public TransmitMessage(object msg, WebSocket ws) { _ws = ws; msgToSend = msg; } + /// + /// Initialize a message to send + /// + /// message object to send + /// WebSocket instance public TransmitMessage(DeviceStateMessageBase msg, WebSocket ws) { _ws = ws; @@ -43,13 +49,13 @@ namespace PepperDash.Essentials { if (_ws == null) { - Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Websocket client is null"); + Debug.LogWarning("Cannot send message. Websocket client is null"); return; } if (!_ws.IsAlive) { - Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Websocket client is not connected"); + Debug.LogWarning("Cannot send message. Websocket client is not connected"); return; } @@ -57,83 +63,14 @@ namespace PepperDash.Essentials var message = JsonConvert.SerializeObject(msgToSend, Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } }); - Debug.LogMessage(LogEventLevel.Verbose, "Message TX: {0}", null, message); + Debug.LogVerbose("Message TX: {0}", message); _ws.Send(message); - - } catch (Exception ex) { - Debug.LogMessage(ex, "Caught an exception in the Transmit Processor"); - } - } - #endregion - } - - - - /// - /// Represents a MessageToClients - /// - public class MessageToClients : IQueueMessage - { - private readonly MobileControlWebsocketServer _server; - private readonly object msgToSend; - - public MessageToClients(object msg, MobileControlWebsocketServer server) - { - _server = server; - msgToSend = msg; - } - - public MessageToClients(DeviceStateMessageBase msg, MobileControlWebsocketServer server) - { - _server = server; - msgToSend = msg; - } - - #region Implementation of IQueueMessage - - /// - /// Dispatch method - /// - public void Dispatch() - { - try - { - if (_server == null) - { - Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Server is null"); - return; - } - - var message = JsonConvert.SerializeObject(msgToSend, Formatting.None, - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } }); - - var clientSpecificMessage = msgToSend as MobileControlMessage; - if (clientSpecificMessage.ClientId != null) - { - var clientId = clientSpecificMessage.ClientId; - - _server.LogVerbose("Message TX To client {clientId} Message: {message}", clientId, message); - - _server.SendMessageToClient(clientId, message); - - return; - } - - _server.SendMessageToAllClients(message); - - _server.LogVerbose("Message TX To all clients: {message}", message); - } - catch (ThreadAbortException) - { - //Swallowing this exception, as it occurs on shutdown and there's no need to print out a scary stack trace - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Caught an exception in the Transmit Processor"); + Debug.LogError("Caught an exception in the Transmit Processor: {message}", ex.Message); + Debug.LogDebug(ex, "Stack Trace: "); } } #endregion diff --git a/src/PepperDash.Essentials.MobileControl/Utilities.cs b/src/PepperDash.Essentials.MobileControl/Utilities.cs new file mode 100644 index 00000000..83ebc5bc --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/Utilities.cs @@ -0,0 +1,97 @@ +using PepperDash.Core; +using PepperDash.Core.Logging; +using WebSocketSharp; + +namespace PepperDash.Essentials +{ + /// + /// Utility functions for logging and other common tasks. + /// + public static class Utilities + { + private static int nextClientId = 0; + + /// + /// Get + /// + /// + public static int GetNextClientId() + { + nextClientId++; + return nextClientId; + } + /// + /// Converts a WebSocketServer LogData object to Essentials logging calls. + /// + /// The LogData object to convert. + /// The log message. + /// The device associated with the log message. + public static void ConvertWebsocketLog(LogData data, string message, IKeyed device = null) + { + + switch (data.Level) + { + case LogLevel.Trace: + if (device == null) + { + Debug.LogVerbose(message); + } + else + { + device.LogVerbose(message); + } + break; + case LogLevel.Debug: + if (device == null) + { + Debug.LogDebug(message); + } + else + { + device.LogDebug(message); + } + break; + case LogLevel.Info: + if (device == null) + { + Debug.LogInformation(message); + } + else + { + device.LogInformation(message); + } + break; + case LogLevel.Warn: + if (device == null) + { + Debug.LogWarning(message); + } + else + { + device.LogWarning(message); + } + break; + case LogLevel.Error: + if (device == null) + { + Debug.LogError(message); + } + else + { + device.LogError(message); + } + break; + case LogLevel.Fatal: + if (device == null) + { + Debug.LogFatal(message); + } + else + { + device.LogFatal(message); + } + break; + } + } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs index d123dbcd..588d3861 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs @@ -1,12 +1,12 @@ -using Crestron.SimplSharp.WebScripting; +using System; +using System.Collections.Generic; +using System.Linq; +using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; using PepperDash.Core; using PepperDash.Core.Web.RequestHandlers; using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.WebSocketServer; -using System; -using System.Collections.Generic; -using System.Linq; namespace PepperDash.Essentials.WebApiHandlers { @@ -111,13 +111,13 @@ namespace PepperDash.Essentials.WebApiHandlers public int ServerPort => directServer.Port; [JsonProperty("tokensDefined")] - public int TokensDefined => directServer.UiClients.Count; + public int TokensDefined => directServer.UiClientContexts.Count; [JsonProperty("clientsConnected")] public int ClientsConnected => directServer.ConnectedUiClientsCount; [JsonProperty("clients")] - public List Clients => directServer.UiClients.Select((c, i) => { return new MobileControlDirectClient(c, i, directServer.UserAppUrlPrefix); }).ToList(); + public List Clients => directServer.UiClientContexts.Select((c, i) => { return new MobileControlDirectClient(c, i, directServer.UserAppUrlPrefix); }).ToList(); public MobileControlDirectServer(MobileControlWebsocketServer server) { diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs index e45fcc39..23b79d93 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs @@ -93,7 +93,7 @@ namespace PepperDash.Essentials.WebApiHandlers - if (!server.UiClients.TryGetValue(request.Token, out UiClientContext clientContext)) + if (!server.UiClientContexts.TryGetValue(request.Token, out UiClientContext clientContext)) { var response = new ClientResponse { @@ -134,7 +134,7 @@ namespace PepperDash.Essentials.WebApiHandlers return; } - server.UiClients.Remove(request.Token); + server.UiClientContexts.Remove(request.Token); server.UpdateSecret(); diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/ConnectionClosedEventArgs.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/ConnectionClosedEventArgs.cs new file mode 100644 index 00000000..6d877d07 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/ConnectionClosedEventArgs.cs @@ -0,0 +1,24 @@ +using System; + +namespace PepperDash.Essentials.WebSocketServer +{ + /// + /// Event Args for ConnectionClosed event + /// + public class ConnectionClosedEventArgs : EventArgs + { + /// + /// Client ID that is being closed + /// + public string ClientId { get; private set; } + + /// + /// Initalize an instance of the class. + /// + /// client that's closing + public ConnectionClosedEventArgs(string clientId) + { + ClientId = clientId; + } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs index b3ea3c7c..7352dc6f 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs @@ -5,6 +5,10 @@ namespace PepperDash.Essentials.WebSocketServer /// public class JoinToken { + /// + /// Unique client ID for a client that is joining + /// + public string Id { get; set; } /// /// Gets or sets the Code /// diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index d15185b0..0b0390aa 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -10,6 +10,7 @@ using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; +using Org.BouncyCastle.Crypto.Prng; using PepperDash.Core; using PepperDash.Core.Logging; using PepperDash.Essentials.Core; @@ -56,7 +57,9 @@ namespace PepperDash.Essentials.WebSocketServer /// /// Gets the collection of UI client contexts /// - public Dictionary UiClients { get; private set; } + public Dictionary UiClientContexts { get; private set; } + + private readonly Dictionary uiClients = new Dictionary(); private readonly MobileControlSystemController _parent; @@ -129,7 +132,7 @@ namespace PepperDash.Essentials.WebSocketServer { var count = 0; - foreach (var client in UiClients) + foreach (var client in UiClientContexts) { if (client.Value.Client != null && client.Value.Client.Context.WebSocket.IsAlive) { @@ -202,7 +205,7 @@ namespace PepperDash.Essentials.WebSocketServer } - UiClients = new Dictionary(); + UiClientContexts = new Dictionary(); //_joinTokens = new Dictionary(); @@ -278,29 +281,20 @@ namespace PepperDash.Essentials.WebSocketServer } _server.Log.Output = (data, message) => + { + switch (data.Level) { - switch (data.Level) - { - case LogLevel.Trace: - this.LogVerbose(data.Message); - break; - case LogLevel.Debug: - this.LogDebug(data.Message); - break; - case LogLevel.Info: - this.LogInformation(data.Message); - break; - case LogLevel.Warn: - this.LogWarning(data.Message); - break; - case LogLevel.Error: - this.LogError(data.Message); - break; - case LogLevel.Fatal: - this.LogFatal(data.Message); - break; - } - }; + case LogLevel.Trace: this.LogVerbose(message); break; + case LogLevel.Debug: this.LogDebug(message); break; + case LogLevel.Info: this.LogInformation(message); break; + case LogLevel.Warn: this.LogWarning(message); break; + case LogLevel.Error: this.LogError(message); break; + case LogLevel.Fatal: this.LogFatal(message); break; + } + }; + + // setting to trace to allow logging level to be controlled by appdebug + _server.Log.Level = LogLevel.Trace; CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; @@ -554,20 +548,20 @@ namespace PepperDash.Essentials.WebSocketServer this.LogInformation("Adding token: {key} for room: {roomKey}", token.Key, token.Value.RoomKey); - if (UiClients == null) + if (UiClientContexts == null) { - UiClients = new Dictionary(); + UiClientContexts = new Dictionary(); } - UiClients.Add(token.Key, new UiClientContext(token.Value)); + UiClientContexts.Add(token.Key, new UiClientContext(token.Value)); } } - if (UiClients.Count > 0) + if (UiClientContexts.Count > 0) { - this.LogInformation("Restored {uiClientCount} UiClients from secrets data", UiClients.Count); + this.LogInformation("Restored {uiClientCount} UiClients from secrets data", UiClientContexts.Count); - foreach (var client in UiClients) + foreach (var client in UiClientContexts) { var key = client.Key; var path = _wsPath + key; @@ -575,13 +569,8 @@ namespace PepperDash.Essentials.WebSocketServer _server.AddWebSocketService(path, () => { - var c = new UiClient($"uiclient-{key}-{roomKey}"); - this.LogDebug("Constructing UiClient with id: {key}", key); - - c.Controller = _parent; - c.RoomKey = roomKey; - UiClients[key].SetClient(c); - return c; + this.LogInformation("Building a UiClient with ID {id}", client.Value.Token.Id); + return BuildUiClient(roomKey, client.Value.Token, key); }); } } @@ -591,7 +580,7 @@ namespace PepperDash.Essentials.WebSocketServer this.LogWarning("No secret found"); } - this.LogDebug("{uiClientCount} UiClients restored from secrets data", UiClients.Count); + this.LogDebug("{uiClientCount} UiClients restored from secrets data", UiClientContexts.Count); } catch (Exception ex) { @@ -616,7 +605,7 @@ namespace PepperDash.Essentials.WebSocketServer _secret.Tokens.Clear(); - foreach (var uiClientContext in UiClients) + foreach (var uiClientContext in UiClientContexts) { _secret.Tokens.Add(uiClientContext.Key, uiClientContext.Value.Token); } @@ -725,21 +714,17 @@ namespace PepperDash.Essentials.WebSocketServer var token = new JoinToken { Code = bridge.UserCode, RoomKey = bridge.RoomKey, Uuid = _parent.SystemUuid, TouchpanelKey = touchPanelKey }; - UiClients.Add(key, new UiClientContext(token)); + UiClientContexts.Add(key, new UiClientContext(token)); var path = _wsPath + key; _server.AddWebSocketService(path, () => { - var c = new UiClient($"uiclient-{key}-{bridge.RoomKey}"); - this.LogVerbose("Constructing UiClient with id: {key}", key); - c.Controller = _parent; - c.RoomKey = bridge.RoomKey; - UiClients[key].SetClient(c); - return c; + this.LogInformation("Building a UiClient with ID {id}", token.Id); + return BuildUiClient(bridge.RoomKey, token, key); }); - this.LogInformation("Added new WebSocket UiClient service at path: {path}", path); + this.LogInformation("Added new WebSocket UiClient for path: {path}", path); this.LogInformation("Token: {@token}", token); this.LogVerbose("{serviceCount} websocket services present", _server.WebSocketServices.Count); @@ -749,6 +734,44 @@ namespace PepperDash.Essentials.WebSocketServer return (key, path); } + private UiClient BuildUiClient(string roomKey, JoinToken token, string key) + { + var c = new UiClient($"uiclient-{key}-{roomKey}-{token.Id}", token.Id); + this.LogInformation("Constructing UiClient with key {key} and ID {id}", key, token.Id); + c.Controller = _parent; + c.RoomKey = roomKey; + + if (uiClients.ContainsKey(token.Id)) + { + this.LogWarning("removing client with duplicate id {id}", token.Id); + uiClients.Remove(token.Id); + } + uiClients.Add(token.Id, c); + // UiClients[key].SetClient(c); + c.ConnectionClosed += (o, a) => uiClients.Remove(a.ClientId); + token.Id = null; + return c; + } + + /// + /// Prints out the session data for each path + /// + public void PrintSessionData() + { + foreach (var path in _server.WebSocketServices.Paths) + { + this.LogInformation("Path: {path}", path); + this.LogInformation(" Session Count: {sessionCount}", _server.WebSocketServices[path].Sessions.Count); + this.LogInformation(" Active Session Count: {activeSessionCount}", _server.WebSocketServices[path].Sessions.ActiveIDs.Count()); + this.LogInformation(" Inactive Session Count: {inactiveSessionCount}", _server.WebSocketServices[path].Sessions.InactiveIDs.Count()); + this.LogInformation(" Active Clients:"); + foreach (var session in _server.WebSocketServices[path].Sessions.IDs) + { + this.LogInformation(" Client ID: {id}", (_server.WebSocketServices[path].Sessions[session] as UiClient)?.Id); + } + } + } + /// /// Removes all clients from the server /// @@ -766,7 +789,7 @@ namespace PepperDash.Essentials.WebSocketServer return; } - foreach (var client in UiClients) + foreach (var client in UiClientContexts) { if (client.Value.Client != null && client.Value.Client.Context.WebSocket.IsAlive) { @@ -784,7 +807,7 @@ namespace PepperDash.Essentials.WebSocketServer } } - UiClients.Clear(); + UiClientContexts.Clear(); UpdateSecret(); } @@ -803,9 +826,9 @@ namespace PepperDash.Essentials.WebSocketServer var key = s; - if (UiClients.ContainsKey(key)) + if (UiClientContexts.ContainsKey(key)) { - var uiClientContext = UiClients[key]; + var uiClientContext = UiClientContexts[key]; if (uiClientContext.Client != null && uiClientContext.Client.Context.WebSocket.IsAlive) { @@ -815,7 +838,7 @@ namespace PepperDash.Essentials.WebSocketServer var path = _wsPath + key; if (_server.RemoveWebSocketService(path)) { - UiClients.Remove(key); + UiClientContexts.Remove(key); UpdateSecret(); @@ -839,9 +862,9 @@ namespace PepperDash.Essentials.WebSocketServer { CrestronConsole.ConsoleCommandResponse("Mobile Control UI Client Info:\r"); - CrestronConsole.ConsoleCommandResponse(string.Format("{0} clients found:\r", UiClients.Count)); + CrestronConsole.ConsoleCommandResponse(string.Format("{0} clients found:\r", UiClientContexts.Count)); - foreach (var client in UiClients) + foreach (var client in UiClientContexts) { CrestronConsole.ConsoleCommandResponse(string.Format("RoomKey: {0} Token: {1}\r", client.Value.Token.RoomKey, client.Key)); } @@ -851,7 +874,7 @@ namespace PepperDash.Essentials.WebSocketServer { if (programEventType == eProgramStatusEventType.Stopping) { - foreach (var client in UiClients.Values) + foreach (var client in UiClientContexts.Values) { if (client.Client != null && client.Client.Context.WebSocket.IsAlive) { @@ -990,77 +1013,81 @@ namespace PepperDash.Essentials.WebSocketServer this.LogVerbose("Join Room Request with token: {token}", token); + byte[] body; - if (UiClients.TryGetValue(token, out UiClientContext clientContext)) - { - var bridge = _parent.GetRoomBridge(clientContext.Token.RoomKey); - - if (bridge != null) - { - res.StatusCode = 200; - res.ContentType = "application/json"; - - var devices = DeviceManager.GetDevices(); - Dictionary deviceInterfaces = new Dictionary(); - - foreach (var device in devices) - { - var interfaces = device?.GetType().GetInterfaces().Select((i) => i.Name).ToList() ?? new List(); - deviceInterfaces.Add(device.Key, new DeviceInterfaceInfo - { - Key = device.Key, - Name = device is IKeyName ? (device as IKeyName).Name : "", - Interfaces = interfaces - }); - } - - // Construct the response object - JoinResponse jRes = new JoinResponse - { - ClientId = token, - RoomKey = bridge.RoomKey, - SystemUuid = _parent.SystemUuid, - RoomUuid = _parent.SystemUuid, - Config = _parent.GetConfigWithPluginVersion(), - CodeExpires = new DateTime().AddYears(1), - UserCode = bridge.UserCode, - UserAppUrl = string.Format("http://{0}:{1}/mc/app", - CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0), - Port), - EnableDebug = false, - DeviceInterfaceSupport = deviceInterfaces - }; - - // Serialize to JSON and convert to Byte[] - var json = JsonConvert.SerializeObject(jRes); - var body = Encoding.UTF8.GetBytes(json); - res.ContentLength64 = body.LongLength; - - // Send the response - res.Close(body, true); - } - else - { - var message = string.Format("Unable to find bridge with key: {0}", clientContext.Token.RoomKey); - res.StatusCode = 404; - res.ContentType = "application/json"; - this.LogVerbose("{message}", message); - var body = Encoding.UTF8.GetBytes(message); - res.ContentLength64 = body.LongLength; - res.Close(body, true); - - } - } - else + if (!UiClientContexts.TryGetValue(token, out UiClientContext clientContext)) { var message = "Token invalid or has expired"; res.StatusCode = 401; res.ContentType = "application/json"; this.LogVerbose("{message}", message); - var body = Encoding.UTF8.GetBytes(message); + body = Encoding.UTF8.GetBytes(message); res.ContentLength64 = body.LongLength; res.Close(body, true); + return; } + + var bridge = _parent.GetRoomBridge(clientContext.Token.RoomKey); + + if (bridge == null) + { + var message = string.Format("Unable to find bridge with key: {0}", clientContext.Token.RoomKey); + res.StatusCode = 404; + res.ContentType = "application/json"; + this.LogVerbose("{message}", message); + body = Encoding.UTF8.GetBytes(message); + res.ContentLength64 = body.LongLength; + res.Close(body, true); + return; + } + + res.StatusCode = 200; + res.ContentType = "application/json"; + + var devices = DeviceManager.GetDevices(); + Dictionary deviceInterfaces = new Dictionary(); + + foreach (var device in devices) + { + var interfaces = device?.GetType().GetInterfaces().Select((i) => i.Name).ToList() ?? new List(); + + deviceInterfaces.Add(device.Key, new DeviceInterfaceInfo + { + Key = device.Key, + Name = (device as IKeyName)?.Name ?? "", + Interfaces = interfaces + }); + } + + var clientId = $"{Utilities.GetNextClientId()}"; + clientContext.Token.Id = clientId; + + this.LogVerbose("Assigning ClientId: {clientId}", clientId); + + // Construct the response object + JoinResponse jRes = new JoinResponse + { + ClientId = clientId, + RoomKey = bridge.RoomKey, + SystemUuid = _parent.SystemUuid, + RoomUuid = _parent.SystemUuid, + Config = _parent.GetConfigWithPluginVersion(), + CodeExpires = new DateTime().AddYears(1), + UserCode = bridge.UserCode, + UserAppUrl = string.Format("http://{0}:{1}/mc/app", + CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0), + Port), + EnableDebug = false, + DeviceInterfaceSupport = deviceInterfaces + }; + + // Serialize to JSON and convert to Byte[] + var json = JsonConvert.SerializeObject(jRes); + body = Encoding.UTF8.GetBytes(json); + res.ContentLength64 = body.LongLength; + + // Send the response + res.Close(body, true); } /// @@ -1242,12 +1269,14 @@ namespace PepperDash.Essentials.WebSocketServer /// public void SendMessageToAllClients(string message) { - foreach (var clientContext in UiClients.Values) + foreach (var client in uiClients.Values) { - if (clientContext.Client != null && clientContext.Client.Context.WebSocket.IsAlive) + if (!client.Context.WebSocket.IsAlive) { - clientContext.Client.Context.WebSocket.Send(message); + continue; } + + client.Context.WebSocket.Send(message); } } @@ -1266,17 +1295,16 @@ namespace PepperDash.Essentials.WebSocketServer return; } - if (UiClients.TryGetValue((string)clientId, out UiClientContext clientContext)) + if (uiClients.TryGetValue((string)clientId, out var client)) { - if (clientContext.Client != null) - { - var socket = clientContext.Client.Context.WebSocket; + var socket = client.Context.WebSocket; - if (socket.IsAlive) - { - socket.Send(message); - } + if (!socket.IsAlive) + { + this.LogError("Unable to send message to client {id}. Client is disconnected: {message}", clientId, message); + return; } + socket.Send(message); } else { diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs index 3cdd183b..79becd33 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs @@ -64,9 +64,11 @@ namespace PepperDash.Essentials.WebSocketServer /// Initializes a new instance of the UiClient class with the specified key /// /// The unique key to identify this client - public UiClient(string key) + /// The client ID used by the client for this connection + public UiClient(string key, string id) { Key = key; + Id = id; } /// @@ -74,19 +76,33 @@ namespace PepperDash.Essentials.WebSocketServer { base.OnOpen(); - var url = Context.WebSocket.Url; - this.LogInformation("New WebSocket Connection from: {url}", url); + Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message); + Log.Level = LogLevel.Trace; - var match = Regex.Match(url.AbsoluteUri, "(?:ws|wss):\\/\\/.*(?:\\/mc\\/api\\/ui\\/join\\/)(.*)"); - - if (!match.Success) + try { - _connectionTime = DateTime.Now; - return; + this.LogDebug("Current session count on open {count}", Sessions.Count); + this.LogDebug("Current WebsocketServiceCount on open: {count}", Controller.DirectServer.WebsocketServiceCount); + } + catch (Exception ex) + { + this.LogError("Error getting service count: {message}", ex.Message); + this.LogDebug(ex, "Stack Trace: "); } - var clientId = match.Groups[1].Value; - _clientId = clientId; + // var url = Context.WebSocket.Url; + // this.LogInformation("New WebSocket Connection from: {url}", url); + + // var match = Regex.Match(url.AbsoluteUri, "(?:ws|wss):\\/\\/.*(?:\\/mc\\/api\\/ui\\/join\\/)(.*)"); + + // if (!match.Success) + // { + // _connectionTime = DateTime.Now; + // return; + // } + + // var clientId = match.Groups[1].Value; + // _clientId = clientId; if (Controller == null) { @@ -99,7 +115,7 @@ namespace PepperDash.Essentials.WebSocketServer Type = "/system/clientJoined", Content = JToken.FromObject(new { - clientId, + clientId = Id, roomKey = RoomKey, }) }; @@ -110,7 +126,7 @@ namespace PepperDash.Essentials.WebSocketServer if (bridge == null) return; - SendUserCodeToClient(bridge, clientId); + SendUserCodeToClient(bridge, Id); bridge.UserCodeChanged -= Bridge_UserCodeChanged; bridge.UserCodeChanged += Bridge_UserCodeChanged; @@ -168,17 +184,30 @@ namespace PepperDash.Essentials.WebSocketServer { base.OnClose(e); + try + { + this.LogDebug("Current session count on close {count}", Sessions.Count); + this.LogDebug("Current WebsocketServiceCount on close: {count}", Controller.DirectServer.WebsocketServiceCount); + } + catch (Exception ex) + { + this.LogError("Error getting service count: {message}", ex.Message); + this.LogDebug(ex, "Stack Trace: "); + } + this.LogInformation("WebSocket UiClient Closing: {code} reason: {reason}", e.Code, e.Reason); foreach (var messenger in Controller.Messengers) { - messenger.Value.UnsubscribeClient(_clientId); + messenger.Value.UnsubscribeClient(Id); } foreach (var messenger in Controller.DefaultMessengers) { - messenger.Value.UnsubscribeClient(_clientId); + messenger.Value.UnsubscribeClient(Id); } + + ConnectionClosed?.Invoke(this, new ConnectionClosedEventArgs(Id)); } /// From c557c6cdd648760a2a6dea5a80a8f30497c4fe06 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 15 Oct 2025 11:49:06 -0500 Subject: [PATCH 05/55] fix: mobileinfo & CWS info call report the correct data --- .../MobileControlSystemController.cs | 44 +++++++------ .../WebApiHandlers/MobileInfoHandler.cs | 41 +++++++++--- .../MobileControlWebsocketServer.cs | 19 ++---- .../WebSocketServer/UiClient.cs | 65 ++++++------------- 4 files changed, 84 insertions(+), 85 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 0b320185..392b5543 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -1746,38 +1746,40 @@ namespace PepperDash.Essentials "\r\n UI Client Info:\r\n" + " Tokens Defined: {0}\r\n" + " Clients Connected: {1}\r\n", - _directServer.UiClients.Count, + _directServer.UiClientContexts.Count, _directServer.ConnectedUiClientsCount ); var clientNo = 1; - foreach (var clientContext in _directServer.UiClients) + foreach (var clientContext in _directServer.UiClientContexts) { var isAlive = false; var duration = "Not Connected"; - if (clientContext.Value.Client != null) - { - isAlive = clientContext.Value.Client.Context.WebSocket.IsAlive; - duration = clientContext.Value.Client.ConnectedDuration.ToString(); - } + var clients = _directServer.UiClients.Values.Where(c => c.Token == clientContext.Value.Token.Token); CrestronConsole.ConsoleCommandResponse( - "\r\nClient {0}:\r\n" + - "Room Key: {1}\r\n" + - "Touchpanel Key: {6}\r\n" + - "Token: {2}\r\n" + - "Client URL: {3}\r\n" + - "Connected: {4}\r\n" + - "Duration: {5}\r\n", - clientNo, - clientContext.Value.Token.RoomKey, - clientContext.Key, - string.Format("{0}{1}", _directServer.UserAppUrlPrefix, clientContext.Key), - isAlive, - duration, - clientContext.Value.Token.TouchpanelKey + $"\r\nClient {clientNo}:\r\n" + + $" Room Key: {clientContext.Value.Token.RoomKey}\r\n" + + $" Touchpanel Key: {clientContext.Value.Token.TouchpanelKey}\r\n" + + $" Token: {clientContext.Key}\r\n" + + $" Client URL: {_directServer.UserAppUrlPrefix}{clientContext.Key}\r\n" + + $" Clients:\r\n" ); + + if (!clients.Any()) + { + CrestronConsole.ConsoleCommandResponse(" No clients connected"); + } + foreach (var client in clients) + { + CrestronConsole.ConsoleCommandResponse( + $" ID: {client.Id}\r\n" + + $" Connected: {client.Context.WebSocket.IsAlive}\r\n" + + $" Duration: {(client.Context.WebSocket.IsAlive ? client.ConnectedDuration.TotalSeconds.ToString() : "Not Connected")}\r\n" + ); + } + clientNo++; } } diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs index 588d3861..ddd319e3 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs @@ -117,7 +117,11 @@ namespace PepperDash.Essentials.WebApiHandlers public int ClientsConnected => directServer.ConnectedUiClientsCount; [JsonProperty("clients")] - public List Clients => directServer.UiClientContexts.Select((c, i) => { return new MobileControlDirectClient(c, i, directServer.UserAppUrlPrefix); }).ToList(); + public List Clients => directServer.UiClientContexts + .Select(context => (context, clients: directServer.UiClients.Where(client => client.Value.Token == context.Value.Token.Token).Select(c => c.Value).ToList())) + .Select((clientTuple, i) => new MobileControlDirectClient(clientTuple.clients, clientTuple.context, i, directServer.UserAppUrlPrefix)) + .ToList(); + public MobileControlDirectServer(MobileControlWebsocketServer server) { @@ -157,18 +161,39 @@ namespace PepperDash.Essentials.WebApiHandlers [JsonProperty("token")] public string Token => Key; - [JsonProperty("connected")] - public bool Connected => context.Client != null && context.Client.Context.WebSocket.IsAlive; + private readonly List clients; - [JsonProperty("duration")] - public double Duration => context.Client == null ? 0 : context.Client.ConnectedDuration.TotalSeconds; + [JsonProperty("clientStatus")] + public List ClientStatus => clients.Select(c => new ClientStatus(c)).ToList(); - public MobileControlDirectClient(KeyValuePair clientContext, int index, string urlPrefix) + /// + /// Create an instance of the class. + /// + /// List of Websocket Clients + /// Context for the client + /// Index of the client + /// URL prefix for the client + public MobileControlDirectClient(List clients, KeyValuePair context, int index, string urlPrefix) { - context = clientContext.Value; - Key = clientContext.Key; + this.context = context.Value; + Key = context.Key; clientNumber = index; this.urlPrefix = urlPrefix; + this.clients = clients; + } + } + + public class ClientStatus + { + private readonly UiClient client; + + public bool Connected => client != null && client.Context.WebSocket.IsAlive; + + public double Duration => client == null ? 0 : client.ConnectedDuration.TotalSeconds; + + public ClientStatus(UiClient client) + { + this.client = client; } } } diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index 0b0390aa..f62e136a 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -61,6 +61,11 @@ namespace PepperDash.Essentials.WebSocketServer private readonly Dictionary uiClients = new Dictionary(); + /// + /// Gets the collection of UI clients + /// + public ReadOnlyDictionary UiClients => new ReadOnlyDictionary(uiClients); + private readonly MobileControlSystemController _parent; private WebSocketServerSecretProvider _secretProvider; @@ -130,17 +135,7 @@ namespace PepperDash.Essentials.WebSocketServer { get { - var count = 0; - - foreach (var client in UiClientContexts) - { - if (client.Value.Client != null && client.Value.Client.Context.WebSocket.IsAlive) - { - count++; - } - } - - return count; + return uiClients.Values.Where(c => c.Context.WebSocket.IsAlive).Count(); } } @@ -736,7 +731,7 @@ namespace PepperDash.Essentials.WebSocketServer private UiClient BuildUiClient(string roomKey, JoinToken token, string key) { - var c = new UiClient($"uiclient-{key}-{roomKey}-{token.Id}", token.Id); + var c = new UiClient($"uiclient-{key}-{roomKey}-{token.Id}", token.Id, token.Token); this.LogInformation("Constructing UiClient with key {key} and ID {id}", key, token.Id); c.Controller = _parent; c.RoomKey = roomKey; diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs index 79becd33..21781abc 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs @@ -1,5 +1,4 @@ using System; -using System.Text.RegularExpressions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Core; @@ -22,6 +21,16 @@ namespace PepperDash.Essentials.WebSocketServer /// public string Key { get; private set; } + /// + /// Client ID used by client for this connection + /// + public string Id { get; private set; } + + /// + /// Token associated with this client + /// + public string Token { get; private set; } + /// /// Gets or sets the mobile control system controller that handles this client's messages /// @@ -32,11 +41,6 @@ namespace PepperDash.Essentials.WebSocketServer /// public string RoomKey { get; set; } - /// - /// The unique identifier for this client instance - /// - private string _clientId; - /// /// The timestamp when this client connection was established /// @@ -60,15 +64,22 @@ namespace PepperDash.Essentials.WebSocketServer } } + /// + /// Triggered when this client closes it's connection + /// + public event EventHandler ConnectionClosed; + /// /// Initializes a new instance of the UiClient class with the specified key /// /// The unique key to identify this client /// The client ID used by the client for this connection - public UiClient(string key, string id) + /// + public UiClient(string key, string id, string token) { Key = key; Id = id; + Token = token; } /// @@ -76,34 +87,11 @@ namespace PepperDash.Essentials.WebSocketServer { base.OnOpen(); + _connectionTime = DateTime.Now; + Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message); Log.Level = LogLevel.Trace; - try - { - this.LogDebug("Current session count on open {count}", Sessions.Count); - this.LogDebug("Current WebsocketServiceCount on open: {count}", Controller.DirectServer.WebsocketServiceCount); - } - catch (Exception ex) - { - this.LogError("Error getting service count: {message}", ex.Message); - this.LogDebug(ex, "Stack Trace: "); - } - - // var url = Context.WebSocket.Url; - // this.LogInformation("New WebSocket Connection from: {url}", url); - - // var match = Regex.Match(url.AbsoluteUri, "(?:ws|wss):\\/\\/.*(?:\\/mc\\/api\\/ui\\/join\\/)(.*)"); - - // if (!match.Success) - // { - // _connectionTime = DateTime.Now; - // return; - // } - - // var clientId = match.Groups[1].Value; - // _clientId = clientId; - if (Controller == null) { Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Controller is null"); @@ -141,7 +129,7 @@ namespace PepperDash.Essentials.WebSocketServer /// Event arguments private void Bridge_UserCodeChanged(object sender, EventArgs e) { - SendUserCodeToClient((MobileControlEssentialsRoomBridge)sender, _clientId); + SendUserCodeToClient((MobileControlEssentialsRoomBridge)sender, Id); } /// @@ -184,17 +172,6 @@ namespace PepperDash.Essentials.WebSocketServer { base.OnClose(e); - try - { - this.LogDebug("Current session count on close {count}", Sessions.Count); - this.LogDebug("Current WebsocketServiceCount on close: {count}", Controller.DirectServer.WebsocketServiceCount); - } - catch (Exception ex) - { - this.LogError("Error getting service count: {message}", ex.Message); - this.LogDebug(ex, "Stack Trace: "); - } - this.LogInformation("WebSocket UiClient Closing: {code} reason: {reason}", e.Code, e.Reason); foreach (var messenger in Controller.Messengers) From 98d0cc8fdcddaf16c2b89c4216f5e021346ff48c Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 15 Oct 2025 12:26:57 -0500 Subject: [PATCH 06/55] docs: add missing XML comments for Mobile Control Project --- .../ClientSpecificUpdateRequest.cs | 7 +- .../IDelayedConfiguration.cs | 5 +- .../MobileControlAction.cs | 14 +- .../MobileControlDeviceFactory.cs | 17 +- .../MobileControlEssentialsConfig.cs | 40 +++-- .../MobileControlFactory.cs | 5 +- .../MobileControlSystemController.cs | 159 ++++++------------ .../RoomBridges/MobileControlBridgeBase.cs | 48 +++++- .../MobileControlEssentialsRoomBridge.cs | 113 ++++++++++++- .../Services/MobileControlApiService.cs | 17 +- .../Touchpanel/ITheme.cs | 7 + .../Touchpanel/ITswAppControl.cs | 21 +++ .../Touchpanel/ITswAppControlMessenger.cs | 16 +- .../Touchpanel/ITswZoomControlMessenger.cs | 24 ++- .../Touchpanel/ThemeMessenger.cs | 9 +- .../UserCodeChanged.cs | 9 +- .../Volumes.cs | 49 +++++- .../WebApiHandlers/ActionPathsHandler.cs | 16 ++ .../MobileAuthRequestHandler.cs | 15 +- .../WebApiHandlers/MobileInfoHandler.cs | 96 ++++++++++- .../WebApiHandlers/UiClientHandler.cs | 13 ++ .../WebSocketServer/JoinResponse.cs | 6 + .../WebSocketServer/JoinToken.cs | 9 + .../MobileControlWebsocketServer.cs | 3 + .../WebSocketServer/ServerTokenSecrets.cs | 7 + .../WebSocketServer/UiClientContext.cs | 4 + .../WebSocketServer/Version.cs | 13 ++ .../WebSocketServerSecretProvider.cs | 11 +- 28 files changed, 576 insertions(+), 177 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl/ClientSpecificUpdateRequest.cs b/src/PepperDash.Essentials.MobileControl/ClientSpecificUpdateRequest.cs index beddd2f6..b974a9aa 100644 --- a/src/PepperDash.Essentials.MobileControl/ClientSpecificUpdateRequest.cs +++ b/src/PepperDash.Essentials.MobileControl/ClientSpecificUpdateRequest.cs @@ -3,10 +3,15 @@ using System; namespace PepperDash.Essentials { /// - /// Represents a ClientSpecificUpdateRequest + /// Send an update request for a specific client /// + [Obsolete] public class ClientSpecificUpdateRequest { + /// + /// Initialize an instance of the class. + /// + /// public ClientSpecificUpdateRequest(Action action) { ResponseMethod = action; diff --git a/src/PepperDash.Essentials.MobileControl/IDelayedConfiguration.cs b/src/PepperDash.Essentials.MobileControl/IDelayedConfiguration.cs index 2d191c89..abeef999 100644 --- a/src/PepperDash.Essentials.MobileControl/IDelayedConfiguration.cs +++ b/src/PepperDash.Essentials.MobileControl/IDelayedConfiguration.cs @@ -7,8 +7,9 @@ namespace PepperDash.Essentials /// public interface IDelayedConfiguration { - - + /// + /// Event triggered when the configuration is ready. Used when Mobile Control is interacting with a SIMPL program. + /// event EventHandler ConfigurationIsReady; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlAction.cs b/src/PepperDash.Essentials.MobileControl/MobileControlAction.cs index 2e402dfb..16a9bd40 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlAction.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlAction.cs @@ -1,6 +1,6 @@ -using Newtonsoft.Json.Linq; +using System; +using Newtonsoft.Json.Linq; using PepperDash.Essentials.Core.DeviceTypeInterfaces; -using System; namespace PepperDash.Essentials { @@ -10,12 +10,20 @@ namespace PepperDash.Essentials public class MobileControlAction : IMobileControlAction { /// - /// Gets or sets the Messenger + /// Gets the Messenger /// public IMobileControlMessenger Messenger { get; private set; } + /// + /// Action to execute when this path is matched + /// public Action Action { get; private set; } + /// + /// Initialize an instance of the class + /// + /// Messenger associated with this action + /// Action to take when this path is matched public MobileControlAction(IMobileControlMessenger messenger, Action handler) { Messenger = messenger; diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs b/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs index c4aa673b..6147b14e 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs @@ -1,28 +1,25 @@ -using PepperDash.Core; -using PepperDash.Core.Logging; +using System; +using System.Collections.Generic; +using PepperDash.Core; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Config; -using Serilog.Events; -using System; -using System.Collections.Generic; -using System.Linq; namespace PepperDash.Essentials { /// - /// Represents a MobileControlDeviceFactory + /// Factory to create a Mobile Control System Controller /// public class MobileControlDeviceFactory : EssentialsDeviceFactory { + /// + /// Create the factory for a Mobile Control System Controller + /// public MobileControlDeviceFactory() { TypeNames = new List { "appserver", "mobilecontrol", "webserver" }; } - /// - /// BuildDevice method - /// /// public override EssentialsDevice BuildDevice(DeviceConfig dc) { diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlEssentialsConfig.cs b/src/PepperDash.Essentials.MobileControl/MobileControlEssentialsConfig.cs index 7183ec84..d0415287 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlEssentialsConfig.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlEssentialsConfig.cs @@ -6,29 +6,35 @@ using PepperDash.Essentials.Core.Config; namespace PepperDash.Essentials { /// - /// Represents a MobileControlEssentialsConfig + /// Configuration class for sending data to Mobile Control Edge or a client using the Direct Server /// public class MobileControlEssentialsConfig : EssentialsConfig { + /// + /// Current versions for the system + /// [JsonProperty("runtimeInfo")] public MobileControlRuntimeInfo RuntimeInfo { get; set; } + /// + /// Create Configuration for Mobile Control. Used as part of the data sent to a client + /// + /// The base configuration public MobileControlEssentialsConfig(EssentialsConfig config) : base() { - // TODO: Consider using Reflection to iterate properties - this.Devices = config.Devices; - this.Info = config.Info; - this.JoinMaps = config.JoinMaps; - this.Rooms = config.Rooms; - this.SourceLists = config.SourceLists; - this.DestinationLists = config.DestinationLists; - this.SystemUrl = config.SystemUrl; - this.TemplateUrl = config.TemplateUrl; - this.TieLines = config.TieLines; + Devices = config.Devices; + Info = config.Info; + JoinMaps = config.JoinMaps; + Rooms = config.Rooms; + SourceLists = config.SourceLists; + DestinationLists = config.DestinationLists; + SystemUrl = config.SystemUrl; + TemplateUrl = config.TemplateUrl; + TieLines = config.TieLines; - if (this.Info == null) - this.Info = new InfoConfig(); + if (Info == null) + Info = new InfoConfig(); RuntimeInfo = new MobileControlRuntimeInfo(); } @@ -46,15 +52,21 @@ namespace PepperDash.Essentials [JsonProperty("pluginVersion")] public string PluginVersion { get; set; } + /// + /// Essentials Version + /// [JsonProperty("essentialsVersion")] public string EssentialsVersion { get; set; } + /// + /// PepperDash Core Version + /// [JsonProperty("pepperDashCoreVersion")] public string PepperDashCoreVersion { get; set; } /// - /// Gets or sets the EssentialsPlugins + /// List of Plugins loaded on this system /// [JsonProperty("essentialsPlugins")] public List EssentialsPlugins { get; set; } diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs b/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs index 393b3385..4f3c5433 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs @@ -7,10 +7,13 @@ using PepperDash.Essentials.Core; namespace PepperDash.Essentials { /// - /// Represents a MobileControlFactory + /// Factory class for the Mobile Control App Controller /// public class MobileControlFactory { + /// + /// Create an instance of the class. + /// public MobileControlFactory() { var assembly = Assembly.GetExecutingAssembly(); diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 392b5543..d2aef597 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -91,10 +91,16 @@ namespace PepperDash.Essentials /// public MobileControlApiService ApiService { get; private set; } + /// + /// Get Room Bridges associated with this controller + /// public List RoomBridges => _roomBridges; private readonly MobileControlWebsocketServer _directServer; + /// + /// Get the Direct Server instance associated with this controller + /// public MobileControlWebsocketServer DirectServer => _directServer; private readonly CCriticalSection _wsCriticalSection = new CCriticalSection(); @@ -104,10 +110,14 @@ namespace PepperDash.Essentials /// public string SystemUrl; //set only from SIMPL Bridge! + /// public bool Connected => _wsClient2 != null && _wsClient2.IsAlive; private IEssentialsRoomCombiner _roomCombiner; + /// + /// Gets the SystemUuid from configuration or SIMPL Bridge + /// public string SystemUuid { get @@ -169,6 +179,9 @@ namespace PepperDash.Essentials private DateTime _lastAckMessage; + /// + /// Gets the LastAckMessage timestamp + /// public DateTime LastAckMessage => _lastAckMessage; private CTimer _pingTimer; @@ -177,11 +190,11 @@ namespace PepperDash.Essentials private LogLevel _wsLogLevel = LogLevel.Error; /// - /// + /// Initializes a new instance of the class. /// - /// - /// - /// + /// The unique key for this controller. + /// The name of the controller. + /// The configuration settings for the controller. public MobileControlSystemController(string key, string name, MobileControlConfig config) : base(key, name) { @@ -1192,6 +1205,9 @@ namespace PepperDash.Essentials /// public string Host { get; private set; } + /// + /// Gets the configured Client App URL + /// public string ClientAppUrl => Config.ClientAppUrl; private void OnRoomCombinationScenarioChanged( @@ -1203,7 +1219,7 @@ namespace PepperDash.Essentials } /// - /// CheckForDeviceMessenger method + /// Checks if a device messenger exists for the given key. /// public bool CheckForDeviceMessenger(string key) { @@ -1211,13 +1227,13 @@ namespace PepperDash.Essentials } /// - /// AddDeviceMessenger method + /// Add the provided messenger to the messengers collection /// public void AddDeviceMessenger(IMobileControlMessenger messenger) { if (_messengers.ContainsKey(messenger.Key)) { - this.LogWarning("Messenger with key {messengerKey) already added", messenger.Key); + this.LogWarning("Messenger with key {messengerKey} already added", messenger.Key); return; } @@ -1291,9 +1307,6 @@ namespace PepperDash.Essentials messenger.RegisterWithAppServer(this); } - /// - /// Initialize method - /// /// public override void Initialize() { @@ -1338,7 +1351,7 @@ namespace PepperDash.Essentials #region IMobileControl Members /// - /// GetAppServer method + /// Gets the App Server instance /// public static IMobileControl GetAppServer() { @@ -1356,16 +1369,10 @@ namespace PepperDash.Essentials } } - /// - /// Generates the url and creates the websocket client - /// private bool CreateWebsocket() { - if (_wsClient2 != null) - { - _wsClient2.Close(); - _wsClient2 = null; - } + _wsClient2?.Close(); + _wsClient2 = null; if (string.IsNullOrEmpty(SystemUuid)) { @@ -1402,7 +1409,7 @@ namespace PepperDash.Essentials } /// - /// LinkSystemMonitorToAppServer method + /// Link the System Monitor to this App server /// public void LinkSystemMonitorToAppServer() { @@ -1429,14 +1436,6 @@ namespace PepperDash.Essentials private void SetWebsocketDebugLevel(string cmdparameters) { - // if (CrestronEnvironment.ProgramCompatibility == eCrestronSeries.Series4) - // { - // this.LogInformation( - // "Setting websocket log level not currently allowed on 4 series." - // ); - // return; // Web socket log level not currently allowed in series4 - // } - if (string.IsNullOrEmpty(cmdparameters)) { this.LogInformation("Current Websocket debug level: {webSocketDebugLevel}", _wsLogLevel); @@ -1474,10 +1473,6 @@ namespace PepperDash.Essentials } } - /// - /// Sends message to server to indicate the system is shutting down - /// - /// private void CrestronEnvironment_ProgramStatusEventHandler( eProgramStatusEventType programEventType ) @@ -1510,6 +1505,9 @@ namespace PepperDash.Essentials } } + /// + /// Get action paths for the current actions + /// public List<(string, string)> GetActionDictionaryPaths() { var paths = new List<(string, string)>(); @@ -1582,24 +1580,24 @@ namespace PepperDash.Essentials } } + /// + /// Get the room bridge with the provided key + /// + /// The key of the room bridge public MobileControlBridgeBase GetRoomBridge(string key) { return _roomBridges.FirstOrDefault((r) => r.RoomKey.Equals(key)); } /// - /// GetRoomMessenger method + /// Get the room messenger with the provided key /// + /// The Key of the rooom messenger public IMobileControlRoomMessenger GetRoomMessenger(string key) { return _roomBridges.FirstOrDefault((r) => r.RoomKey.Equals(key)); } - /// - /// - /// - /// - /// private void Bridge_ConfigurationIsReady(object sender, EventArgs e) { this.LogDebug("Bridge ready. Registering"); @@ -1620,10 +1618,6 @@ namespace PepperDash.Essentials } } - /// - /// - /// - /// private void ReconnectToServerTimerCallback(object o) { this.LogDebug("Attempting to reconnect to server..."); @@ -1631,9 +1625,6 @@ namespace PepperDash.Essentials ConnectWebsocketClient(); } - /// - /// Verifies system connection with servers - /// private void AuthorizeSystem(string code) { if ( @@ -1678,9 +1669,6 @@ namespace PepperDash.Essentials }); } - /// - /// Dumps info in response to console command. - /// private void ShowInfo() { var url = Config != null ? Host : "No config"; @@ -1753,9 +1741,6 @@ namespace PepperDash.Essentials var clientNo = 1; foreach (var clientContext in _directServer.UiClientContexts) { - var isAlive = false; - var duration = "Not Connected"; - var clients = _directServer.UiClients.Values.Where(c => c.Token == clientContext.Value.Token.Token); CrestronConsole.ConsoleCommandResponse( @@ -1793,7 +1778,7 @@ namespace PepperDash.Essentials } /// - /// RegisterSystemToServer method + /// Register this system to the Mobile Control Edge Server /// public void RegisterSystemToServer() { @@ -1817,9 +1802,6 @@ namespace PepperDash.Essentials ConnectWebsocketClient(); } - /// - /// Connects the Websocket Client - /// private void ConnectWebsocketClient() { try @@ -1860,9 +1842,6 @@ namespace PepperDash.Essentials } } - /// - /// Attempts to connect the websocket - /// private void TryConnect() { try @@ -1892,9 +1871,6 @@ namespace PepperDash.Essentials } } - /// - /// Gracefully handles conect failures by reconstructing the ws client and starting the reconnect timer - /// private void HandleConnectFailure() { _wsClient2 = null; @@ -1926,11 +1902,6 @@ namespace PepperDash.Essentials StartServerReconnectTimer(); } - /// - /// - /// - /// - /// private void HandleOpen(object sender, EventArgs e) { StopServerReconnectTimer(); @@ -1939,11 +1910,6 @@ namespace PepperDash.Essentials SendMessageObject(new MobileControlMessage { Type = "hello" }); } - /// - /// - /// - /// - /// private void HandleMessage(object sender, MessageEventArgs e) { if (e.IsPing) @@ -1960,11 +1926,6 @@ namespace PepperDash.Essentials } } - /// - /// - /// - /// - /// private void HandleError(object sender, ErrorEventArgs e) { this.LogError("Websocket error {0}", e.Message); @@ -1973,11 +1934,6 @@ namespace PepperDash.Essentials StartServerReconnectTimer(); } - /// - /// - /// - /// - /// private void HandleClose(object sender, CloseEventArgs e) { this.LogDebug( @@ -1998,9 +1954,6 @@ namespace PepperDash.Essentials StartServerReconnectTimer(); } - /// - /// After a "hello" from the server, sends config and stuff - /// private void SendInitialMessage() { this.LogInformation("Sending initial join message"); @@ -2027,7 +1980,7 @@ namespace PepperDash.Essentials } /// - /// GetConfigWithPluginVersion method + /// Get the Essentials configuration with version data /// public MobileControlEssentialsConfig GetConfigWithPluginVersion() { @@ -2062,8 +2015,13 @@ namespace PepperDash.Essentials } /// - /// SetClientUrl method + /// Set the Client URL for a given room /// + /// new App URL + /// room key. Default is null + /// + /// If roomKey is null, the URL will be set for the entire system. + /// public void SetClientUrl(string path, string roomKey = null) { var message = new MobileControlMessage @@ -2079,9 +2037,6 @@ namespace PepperDash.Essentials /// Sends any object type to server /// /// - /// - /// SendMessageObject method - /// public void SendMessageObject(IMobileControlMessage o) { @@ -2105,8 +2060,9 @@ namespace PepperDash.Essentials /// - /// SendMessageObjectToDirectClient method + /// Send a message to a client using the Direct Server /// + /// object to send public void SendMessageObjectToDirectClient(object o) { if ( @@ -2119,10 +2075,6 @@ namespace PepperDash.Essentials } } - - /// - /// Disconnects the Websocket Client and stops the heartbeat timer - /// private void CleanUpWebsocketClient() { if (_wsClient2 == null) @@ -2180,9 +2132,6 @@ namespace PepperDash.Essentials } } - /// - /// - /// private void StartServerReconnectTimer() { StopServerReconnectTimer(); @@ -2193,9 +2142,6 @@ namespace PepperDash.Essentials this.LogDebug("Reconnect Timer Started."); } - /// - /// Does what it says - /// private void StopServerReconnectTimer() { if (_serverReconnectTimer == null) @@ -2206,10 +2152,6 @@ namespace PepperDash.Essentials _serverReconnectTimer = null; } - /// - /// Resets reconnect timer and updates usercode - /// - /// private void HandleHeartBeat(JToken content) { SendMessageObject(new MobileControlMessage { Type = "/system/heartbeatAck" }); @@ -2319,16 +2261,13 @@ namespace PepperDash.Essentials } /// - /// HandleClientMessage method + /// Enqueue an incoming message for processing /// public void HandleClientMessage(string message) { _receiveQueue.Enqueue(new ProcessStringMessage(message, ParseStreamRx)); } - /// - /// - /// private void ParseStreamRx(string messageText) { if (string.IsNullOrEmpty(messageText)) @@ -2415,10 +2354,6 @@ namespace PepperDash.Essentials } } - /// - /// - /// - /// private void TestHttpRequest(string s) { { diff --git a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlBridgeBase.cs b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlBridgeBase.cs index ac1a851c..634255fe 100644 --- a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlBridgeBase.cs +++ b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlBridgeBase.cs @@ -1,23 +1,35 @@ -using PepperDash.Core; +using System; +using PepperDash.Core; using PepperDash.Core.Logging; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core.DeviceTypeInterfaces; -using System; namespace PepperDash.Essentials.RoomBridges { /// - /// + /// Base class for a Mobile Control Bridge that's used to control a room /// public abstract class MobileControlBridgeBase : MessengerBase, IMobileControlRoomMessenger { + /// + /// Triggered when the user Code changes + /// public event EventHandler UserCodeChanged; + /// + /// Triggered when a user should be prompted for the new code + /// public event EventHandler UserPromptedForCode; + /// + /// Triggered when a client joins to control this room + /// public event EventHandler ClientJoined; + /// + /// Triggered when the App URL for this room changes + /// public event EventHandler AppUrlChanged; /// @@ -49,15 +61,32 @@ namespace PepperDash.Essentials.RoomBridges /// public string McServerUrl { get; private set; } + /// + /// Room Name + /// public abstract string RoomName { get; } + /// + /// Room key + /// public abstract string RoomKey { get; } + /// + /// Create an instance of the class + /// + /// The unique key for this bridge + /// The message path for this bridge protected MobileControlBridgeBase(string key, string messagePath) : base(key, messagePath) { } + /// + /// Create an instance of the class + /// + /// The unique key for this bridge + /// The message path for this bridge + /// The device associated with this bridge protected MobileControlBridgeBase(string key, string messagePath, IKeyName device) : base(key, messagePath, device) { @@ -110,6 +139,10 @@ namespace PepperDash.Essentials.RoomBridges SetUserCode(code); } + /// + /// Update the App Url with the provided URL + /// + /// The new App URL public virtual void UpdateAppUrl(string url) { AppUrl = url; @@ -137,16 +170,25 @@ namespace PepperDash.Essentials.RoomBridges OnUserCodeChanged(); } + /// + /// Trigger the UserCodeChanged event + /// protected void OnUserCodeChanged() { UserCodeChanged?.Invoke(this, new EventArgs()); } + /// + /// Trigger the UserPromptedForCode event + /// protected void OnUserPromptedForCode() { UserPromptedForCode?.Invoke(this, new EventArgs()); } + /// + /// Trigger the ClientJoined event + /// protected void OnClientJoined() { ClientJoined?.Invoke(this, new EventArgs()); diff --git a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs index f0463f27..742251db 100644 --- a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs +++ b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs @@ -41,24 +41,37 @@ namespace PepperDash.Essentials.RoomBridges /// public string DefaultRoomKey { get; private set; } /// - /// + /// Gets the name of the room /// public override string RoomName { get { return Room.Name; } } + /// + /// Gets the key of the room + /// public override string RoomKey { get { return Room.Key; } } + /// + /// Initializes a new instance of the class with the specified room + /// + /// The essentials room to bridge public MobileControlEssentialsRoomBridge(IEssentialsRoom room) : this($"mobileControlBridge-{room.Key}", room.Key, room) { Room = room; } + /// + /// Initializes a new instance of the class with the specified parameters + /// + /// The unique key for this bridge + /// The key of the room to bridge + /// The essentials room to bridge public MobileControlEssentialsRoomBridge(string key, string roomKey, IEssentialsRoom room) : base(key, $"/room/{room.Key}", room as Device) { DefaultRoomKey = roomKey; @@ -66,7 +79,9 @@ namespace PepperDash.Essentials.RoomBridges AddPreActivationAction(GetRoom); } - + /// + /// Registers all message handling actions with the AppServer for this room bridge + /// protected override void RegisterActions() { // we add actions to the messaging system with a path, and a related action. Custom action @@ -284,6 +299,9 @@ namespace PepperDash.Essentials.RoomBridges Room = tempRoom; } + /// + /// Handles user code changes and generates QR code URL + /// protected override void UserCodeChange() { Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Server user code changed: {userCode}", this, UserCode); @@ -807,18 +825,33 @@ namespace PepperDash.Essentials.RoomBridges [JsonProperty("configuration", NullValueHandling = NullValueHandling.Ignore)] public RoomConfiguration Configuration { get; set; } + /// + /// Gets or sets the activity mode of the room + /// [JsonProperty("activityMode", NullValueHandling = NullValueHandling.Ignore)] public int? ActivityMode { get; set; } + /// + /// Gets or sets whether advanced sharing is active + /// [JsonProperty("advancedSharingActive", NullValueHandling = NullValueHandling.Ignore)] public bool? AdvancedSharingActive { get; set; } + /// + /// Gets or sets whether the room is powered on + /// [JsonProperty("isOn", NullValueHandling = NullValueHandling.Ignore)] public bool? IsOn { get; set; } + /// + /// Gets or sets whether the room is warming up + /// [JsonProperty("isWarmingUp", NullValueHandling = NullValueHandling.Ignore)] public bool? IsWarmingUp { get; set; } + /// + /// Gets or sets whether the room is cooling down + /// [JsonProperty("isCoolingDown", NullValueHandling = NullValueHandling.Ignore)] public bool? IsCoolingDown { get; set; } @@ -834,9 +867,15 @@ namespace PepperDash.Essentials.RoomBridges [JsonProperty("share", NullValueHandling = NullValueHandling.Ignore)] public ShareState Share { get; set; } + /// + /// Gets or sets the volume controls collection + /// [JsonProperty("volumes", NullValueHandling = NullValueHandling.Ignore)] public Dictionary Volumes { get; set; } + /// + /// Gets or sets whether the room is in a call + /// [JsonProperty("isInCall", NullValueHandling = NullValueHandling.Ignore)] public bool? IsInCall { get; set; } } @@ -853,9 +892,15 @@ namespace PepperDash.Essentials.RoomBridges [JsonProperty("currentShareText", NullValueHandling = NullValueHandling.Ignore)] public string CurrentShareText { get; set; } + /// + /// Gets or sets whether sharing is enabled + /// [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] public bool? Enabled { get; set; } + /// + /// Gets or sets whether content is currently being shared + /// [JsonProperty("isSharing", NullValueHandling = NullValueHandling.Ignore)] public bool? IsSharing { get; set; } } @@ -865,24 +910,45 @@ namespace PepperDash.Essentials.RoomBridges /// public class RoomConfiguration { + /// + /// Gets or sets whether the room has video conferencing capabilities + /// [JsonProperty("hasVideoConferencing", NullValueHandling = NullValueHandling.Ignore)] public bool? HasVideoConferencing { get; set; } + /// + /// Gets or sets whether the video codec is a Zoom Room + /// [JsonProperty("videoCodecIsZoomRoom", NullValueHandling = NullValueHandling.Ignore)] public bool? VideoCodecIsZoomRoom { get; set; } + /// + /// Gets or sets whether the room has audio conferencing capabilities + /// [JsonProperty("hasAudioConferencing", NullValueHandling = NullValueHandling.Ignore)] public bool? HasAudioConferencing { get; set; } + /// + /// Gets or sets whether the room has environmental controls (lighting, shades, etc.) + /// [JsonProperty("hasEnvironmentalControls", NullValueHandling = NullValueHandling.Ignore)] public bool? HasEnvironmentalControls { get; set; } + /// + /// Gets or sets whether the room has camera controls + /// [JsonProperty("hasCameraControls", NullValueHandling = NullValueHandling.Ignore)] public bool? HasCameraControls { get; set; } + /// + /// Gets or sets whether the room has set-top box controls + /// [JsonProperty("hasSetTopBoxControls", NullValueHandling = NullValueHandling.Ignore)] public bool? HasSetTopBoxControls { get; set; } + /// + /// Gets or sets whether the room has routing controls + /// [JsonProperty("hasRoutingControls", NullValueHandling = NullValueHandling.Ignore)] public bool? HasRoutingControls { get; set; } @@ -949,6 +1015,9 @@ namespace PepperDash.Essentials.RoomBridges [JsonProperty("defaultDisplayKey", NullValueHandling = NullValueHandling.Ignore)] public string DefaultDisplayKey { get; set; } + /// + /// Gets or sets the destinations dictionary keyed by destination type + /// [JsonProperty("destinations", NullValueHandling = NullValueHandling.Ignore)] public Dictionary Destinations { get; set; } @@ -959,9 +1028,15 @@ namespace PepperDash.Essentials.RoomBridges [JsonProperty("environmentalDevices", NullValueHandling = NullValueHandling.Ignore)] public List EnvironmentalDevices { get; set; } + /// + /// Gets or sets the source list for the room + /// [JsonProperty("sourceList", NullValueHandling = NullValueHandling.Ignore)] public Dictionary SourceList { get; set; } + /// + /// Gets or sets the destination list for the room + /// [JsonProperty("destinationList", NullValueHandling = NullValueHandling.Ignore)] public Dictionary DestinationList { get; set; } @@ -972,6 +1047,9 @@ namespace PepperDash.Essentials.RoomBridges [JsonProperty("audioControlPointList", NullValueHandling = NullValueHandling.Ignore)] public AudioControlPointListItem AudioControlPointList { get; set; } + /// + /// Gets or sets the camera list for the room + /// [JsonProperty("cameraList", NullValueHandling = NullValueHandling.Ignore)] public Dictionary CameraList { get; set; } @@ -1004,9 +1082,15 @@ namespace PepperDash.Essentials.RoomBridges [JsonProperty("uiBehavior", NullValueHandling = NullValueHandling.Ignore)] public EssentialsRoomUiBehaviorConfig UiBehavior { get; set; } + /// + /// Gets or sets whether the room supports advanced sharing features + /// [JsonProperty("supportsAdvancedSharing", NullValueHandling = NullValueHandling.Ignore)] public bool? SupportsAdvancedSharing { get; set; } + /// + /// Gets or sets whether the user can change the share mode + /// [JsonProperty("userCanChangeShareMode", NullValueHandling = NullValueHandling.Ignore)] public bool? UserCanChangeShareMode { get; set; } @@ -1017,6 +1101,9 @@ namespace PepperDash.Essentials.RoomBridges [JsonProperty("roomCombinerKey", NullValueHandling = NullValueHandling.Ignore)] public string RoomCombinerKey { get; set; } + /// + /// Initializes a new instance of the class + /// public RoomConfiguration() { Destinations = new Dictionary(); @@ -1046,6 +1133,11 @@ namespace PepperDash.Essentials.RoomBridges [JsonProperty("deviceType", NullValueHandling = NullValueHandling.Ignore)] public eEnvironmentalDeviceTypes DeviceType { get; private set; } + /// + /// Initializes a new instance of the class + /// + /// The device key + /// The environmental device type public EnvironmentalDeviceConfiguration(string key, eEnvironmentalDeviceTypes type) { DeviceKey = key; @@ -1054,14 +1146,29 @@ namespace PepperDash.Essentials.RoomBridges } /// - /// Enumeration of eEnvironmentalDeviceTypes values + /// Enumeration of environmental device types /// public enum eEnvironmentalDeviceTypes { + /// + /// No environmental device type specified + /// None, + /// + /// Lighting device type + /// Lighting, + /// + /// Shade device type + /// Shade, + /// + /// Shade controller device type + /// ShadeController, + /// + /// Relay device type + /// Relay, } diff --git a/src/PepperDash.Essentials.MobileControl/Services/MobileControlApiService.cs b/src/PepperDash.Essentials.MobileControl/Services/MobileControlApiService.cs index 4a84f8f1..508fb776 100644 --- a/src/PepperDash.Essentials.MobileControl/Services/MobileControlApiService.cs +++ b/src/PepperDash.Essentials.MobileControl/Services/MobileControlApiService.cs @@ -1,18 +1,22 @@ -using PepperDash.Core; -using System; +using System; using System.Net.Http; using System.Threading.Tasks; +using PepperDash.Core; namespace PepperDash.Essentials.Services { /// - /// Represents a MobileControlApiService + /// Service for interacting with a Mobile Control Edge server instance /// public class MobileControlApiService { private readonly HttpClient _client; + /// + /// Create an instance of the class. + /// + /// Mobile Control Edge API URL public MobileControlApiService(string apiUrl) { var handler = new HttpClientHandler @@ -24,6 +28,13 @@ namespace PepperDash.Essentials.Services _client = new HttpClient(handler); } + /// + /// Send authorization request to Mobile Control Edge Server + /// + /// Mobile Control Edge API URL + /// Grant code for authorization + /// System UUID for authorization + /// Authorization response public async Task SendAuthorizationRequest(string apiUrl, string grantCode, string systemUuid) { try diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITheme.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITheme.cs index e5ba76cf..c7efbf4c 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITheme.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITheme.cs @@ -7,8 +7,15 @@ namespace PepperDash.Essentials.Touchpanel /// public interface ITheme : IKeyed { + /// + /// Current theme + /// string Theme { get; } + /// + /// Set the theme with the given value + /// + /// The theme to set void UpdateTheme(string theme); } } diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControl.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControl.cs index 3beb92c5..055ad80c 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControl.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControl.cs @@ -8,12 +8,24 @@ namespace PepperDash.Essentials.Touchpanel /// public interface ITswAppControl : IKeyed { + /// + /// Updates when the Zoom Room Control Application opens or closes + /// BoolFeedback AppOpenFeedback { get; } + /// + /// Hide the Zoom App and show the User Control Application + /// void HideOpenApp(); + /// + /// Close the Zoom App and show the User Control Application + /// void CloseOpenApp(); + /// + /// Open the Zoom App + /// void OpenApp(); } @@ -22,10 +34,19 @@ namespace PepperDash.Essentials.Touchpanel /// public interface ITswZoomControl : IKeyed { + /// + /// Updates when Zoom has an incoming call + /// BoolFeedback ZoomIncomingCallFeedback { get; } + /// + /// Updates when Zoom is in a call + /// BoolFeedback ZoomInCallFeedback { get; } + /// + /// End a Zoom Call + /// void EndZoomCall(); } } diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs index f9581c74..d4c55f87 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs @@ -7,17 +7,24 @@ using PepperDash.Essentials.AppServer.Messengers; namespace PepperDash.Essentials.Touchpanel { /// - /// Represents a ITswAppControlMessenger + /// Messenger for controlling the Zoom App on a TSW Panel that supports the Zoom Room Control Application /// public class ITswAppControlMessenger : MessengerBase { private readonly ITswAppControl _appControl; + /// + /// Create an instance of the class. + /// + /// The key for this messenger + /// The message path for this messenger + /// The device for this messenger public ITswAppControlMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) { _appControl = device as ITswAppControl; } + /// protected override void RegisterActions() { if (_appControl == null) @@ -43,14 +50,14 @@ namespace PepperDash.Essentials.Touchpanel }; } - private void SendFullStatus() + private void SendFullStatus(string id = null) { var message = new TswAppStateMessage { AppOpen = _appControl.AppOpenFeedback.BoolValue, }; - PostStatusMessage(message); + PostStatusMessage(message, id); } } @@ -59,6 +66,9 @@ namespace PepperDash.Essentials.Touchpanel /// public class TswAppStateMessage : DeviceStateMessageBase { + /// + /// True if the Zoom app is open on a TSW panel + /// [JsonProperty("appOpen", NullValueHandling = NullValueHandling.Ignore)] public bool? AppOpen { get; set; } } diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs index 48c7fb7f..13d1dc59 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs @@ -8,17 +8,24 @@ using PepperDash.Essentials.AppServer.Messengers; namespace PepperDash.Essentials.Touchpanel { /// - /// Represents a ITswZoomControlMessenger + /// Messenger to handle /// public class ITswZoomControlMessenger : MessengerBase { private readonly ITswZoomControl _zoomControl; + /// + /// Create in instance of the class for the given device + /// + /// The key for this messenger + /// The message path for this messenger + /// The device for this messenger public ITswZoomControlMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) { _zoomControl = device as ITswZoomControl; } + /// protected override void RegisterActions() { if (_zoomControl == null) @@ -27,7 +34,9 @@ namespace PepperDash.Essentials.Touchpanel return; } - AddAction($"/fullStatus", (id, context) => SendFullStatus()); + AddAction($"/fullStatus", (id, context) => SendFullStatus(id)); + + AddAction($"/zoomStatus", (id, content) => SendFullStatus(id)); AddAction($"/endCall", (id, context) => _zoomControl.EndZoomCall()); @@ -53,7 +62,7 @@ namespace PepperDash.Essentials.Touchpanel }; } - private void SendFullStatus() + private void SendFullStatus(string id = null) { var message = new TswZoomStateMessage { @@ -61,7 +70,7 @@ namespace PepperDash.Essentials.Touchpanel IncomingCall = _zoomControl?.ZoomIncomingCallFeedback.BoolValue }; - PostStatusMessage(message); + PostStatusMessage(message, id); } } @@ -70,9 +79,16 @@ namespace PepperDash.Essentials.Touchpanel /// public class TswZoomStateMessage : DeviceStateMessageBase { + /// + /// True if the panel is in a Zoom call + /// [JsonProperty("inCall", NullValueHandling = NullValueHandling.Ignore)] public bool? InCall { get; set; } + /// + /// True if there is an incoming Zoom call + /// + [JsonProperty("incomingCall", NullValueHandling = NullValueHandling.Ignore)] public bool? IncomingCall { get; set; } } diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs index 089665d9..0f02bc59 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs @@ -7,17 +7,24 @@ using PepperDash.Essentials.AppServer.Messengers; namespace PepperDash.Essentials.Touchpanel { /// - /// Represents a ThemeMessenger + /// Messenger to save the current theme (light/dark) and send to a device /// public class ThemeMessenger : MessengerBase { private readonly ITheme _tpDevice; + /// + /// Create an instance of the class + /// + /// The key for this messenger + /// The path for this messenger + /// The device for this messenger public ThemeMessenger(string key, string path, ITheme device) : base(key, path, device as Device) { _tpDevice = device; } + /// protected override void RegisterActions() { AddAction("/fullStatus", (id, content) => diff --git a/src/PepperDash.Essentials.MobileControl/UserCodeChanged.cs b/src/PepperDash.Essentials.MobileControl/UserCodeChanged.cs index ab899167..6c510d14 100644 --- a/src/PepperDash.Essentials.MobileControl/UserCodeChanged.cs +++ b/src/PepperDash.Essentials.MobileControl/UserCodeChanged.cs @@ -3,12 +3,19 @@ using System; namespace PepperDash.Essentials { /// - /// Represents a UserCodeChanged + /// Defines the action to take when the User code changes /// public class UserCodeChanged { + /// + /// Action to take when the User Code changes + /// public Action UpdateUserCode { get; private set; } + /// + /// create an instance of the class + /// + /// action to take when the User Code changes public UserCodeChanged(Action updateMethod) { UpdateUserCode = updateMethod; diff --git a/src/PepperDash.Essentials.MobileControl/Volumes.cs b/src/PepperDash.Essentials.MobileControl/Volumes.cs index 84accd26..44febbfc 100644 --- a/src/PepperDash.Essentials.MobileControl/Volumes.cs +++ b/src/PepperDash.Essentials.MobileControl/Volumes.cs @@ -15,15 +15,17 @@ namespace PepperDash.Essentials [JsonProperty("master", NullValueHandling = NullValueHandling.Ignore)] public Volume Master { get; set; } + /// + /// Aux Faders as configured in the room + /// [JsonProperty("auxFaders", NullValueHandling = NullValueHandling.Ignore)] public Dictionary AuxFaders { get; set; } + /// + /// Count of aux faders for this system + /// [JsonProperty("numberOfAuxFaders", NullValueHandling = NullValueHandling.Ignore)] public int? NumberOfAuxFaders { get; set; } - - public Volumes() - { - } } /// @@ -31,16 +33,21 @@ namespace PepperDash.Essentials /// public class Volume { - /// /// Gets or sets the Key /// [JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)] public string Key { get; set; } + /// + /// Level for this volume object + /// [JsonProperty("level", NullValueHandling = NullValueHandling.Ignore)] public int? Level { get; set; } + /// + /// True if this volume control is muted + /// [JsonProperty("muted", NullValueHandling = NullValueHandling.Ignore)] public bool? Muted { get; set; } @@ -51,12 +58,21 @@ namespace PepperDash.Essentials [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] public string Label { get; set; } + /// + /// True if this volume object has mute control + /// [JsonProperty("hasMute", NullValueHandling = NullValueHandling.Ignore)] public bool? HasMute { get; set; } + /// + /// True if this volume object has Privacy mute control + /// [JsonProperty("hasPrivacyMute", NullValueHandling = NullValueHandling.Ignore)] public bool? HasPrivacyMute { get; set; } + /// + /// True if the privacy mute is muted + /// [JsonProperty("privacyMuted", NullValueHandling = NullValueHandling.Ignore)] public bool? PrivacyMuted { get; set; } @@ -68,6 +84,15 @@ namespace PepperDash.Essentials [JsonProperty("muteIcon", NullValueHandling = NullValueHandling.Ignore)] public string MuteIcon { get; set; } + /// + /// Create an instance of the class + /// + /// The key for this volume object + /// The level for this volume object + /// True if this volume control is muted + /// The label for this volume object + /// True if this volume object has mute control + /// The mute icon for this volume object public Volume(string key, int level, bool muted, string label, bool hasMute, string muteIcon) : this(key) { @@ -78,18 +103,32 @@ namespace PepperDash.Essentials MuteIcon = muteIcon; } + /// + /// Create an instance of the class + /// + /// The key for this volume object + /// The level for this volume object public Volume(string key, int level) : this(key) { Level = level; } + /// + /// Create an instance of the class + /// + /// The key for this volume object + /// True if this volume control is muted public Volume(string key, bool muted) : this(key) { Muted = muted; } + /// + /// Create an instance of the class + /// + /// The key for this volume object public Volume(string key) { Key = key; diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/ActionPathsHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/ActionPathsHandler.cs index 84d0318a..1c204e8e 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/ActionPathsHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/ActionPathsHandler.cs @@ -12,11 +12,20 @@ namespace PepperDash.Essentials.WebApiHandlers public class ActionPathsHandler : WebApiBaseRequestHandler { private readonly MobileControlSystemController mcController; + + /// + /// Create an instance of the class. + /// + /// public ActionPathsHandler(MobileControlSystemController controller) : base(true) { mcController = controller; } + /// + /// Handle a request to get the action paths + /// + /// Request Context protected override void HandleGet(HttpCwsContext context) { var response = JsonConvert.SerializeObject(new ActionPathsResponse(mcController)); @@ -37,9 +46,16 @@ namespace PepperDash.Essentials.WebApiHandlers [JsonIgnore] private readonly MobileControlSystemController mcController; + /// + /// Registered action paths for this system + /// [JsonProperty("actionPaths")] public List ActionPaths => mcController.GetActionDictionaryPaths().Select((path) => new ActionPath { MessengerKey = path.Item1, Path = path.Item2 }).ToList(); + /// + /// Create an instance of the class. + /// + /// public ActionPathsResponse(MobileControlSystemController mcController) { this.mcController = mcController; diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileAuthRequestHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileAuthRequestHandler.cs index 352c56e5..5d9e9766 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileAuthRequestHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileAuthRequestHandler.cs @@ -1,10 +1,10 @@ -using Crestron.SimplSharp.WebScripting; +using System; +using System.Threading.Tasks; +using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; using PepperDash.Core; using PepperDash.Core.Web.RequestHandlers; using PepperDash.Essentials.Core.Web; -using System; -using System.Threading.Tasks; namespace PepperDash.Essentials.WebApiHandlers { @@ -15,11 +15,20 @@ namespace PepperDash.Essentials.WebApiHandlers { private readonly MobileControlSystemController mcController; + /// + /// Create an instance of the class. + /// + /// public MobileAuthRequestHandler(MobileControlSystemController controller) : base(true) { mcController = controller; } + /// + /// Handle authorization request for this processor + /// + /// request context + /// Task protected override async Task HandlePost(HttpCwsContext context) { try diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs index ddd319e3..363076c0 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs @@ -11,16 +11,25 @@ using PepperDash.Essentials.WebSocketServer; namespace PepperDash.Essentials.WebApiHandlers { /// - /// Represents a MobileInfoHandler + /// Represents a MobileInfoHandler. Used with the Essentials CWS API /// public class MobileInfoHandler : WebApiBaseRequestHandler { private readonly MobileControlSystemController mcController; + + /// + /// Create an instance of the class. + /// + /// public MobileInfoHandler(MobileControlSystemController controller) : base(true) { mcController = controller; } + /// + /// Get Mobile Control Information + /// + /// protected override void HandleGet(HttpCwsContext context) { try @@ -50,14 +59,22 @@ namespace PepperDash.Essentials.WebApiHandlers [JsonIgnore] private readonly MobileControlSystemController mcController; + /// + /// Edge Server. Null if edge server is disabled + /// [JsonProperty("edgeServer", NullValueHandling = NullValueHandling.Ignore)] public MobileControlEdgeServer EdgeServer => mcController.Config.EnableApiServer ? new MobileControlEdgeServer(mcController) : null; - + /// + /// Direct server. Null if the direct server is disabled + /// [JsonProperty("directServer", NullValueHandling = NullValueHandling.Ignore)] public MobileControlDirectServer DirectServer => mcController.Config.DirectServer.EnableDirectServer ? new MobileControlDirectServer(mcController.DirectServer) : null; - + /// + /// Create in instace of the class. + /// + /// public InformationResponse(MobileControlSystemController controller) { mcController = controller; @@ -72,24 +89,46 @@ namespace PepperDash.Essentials.WebApiHandlers [JsonIgnore] private readonly MobileControlSystemController mcController; + /// + /// Mobile Control Edge Server address for this system + /// [JsonProperty("serverAddress")] public string ServerAddress => mcController.Config == null ? "No Config" : mcController.Host; + /// + /// System Name for this system + /// [JsonProperty("systemName")] public string SystemName => mcController.RoomBridges.Count > 0 ? mcController.RoomBridges[0].RoomName : "No Config"; + /// + /// System URL for this system + /// [JsonProperty("systemUrl")] public string SystemUrl => ConfigReader.ConfigObject.SystemUrl; + /// + /// User code to use in MC UI for this system + /// [JsonProperty("userCode")] public string UserCode => mcController.RoomBridges.Count > 0 ? mcController.RoomBridges[0].UserCode : "Not available"; + /// + /// True if connected to edge server + /// [JsonProperty("connected")] public bool Connected => mcController.Connected; + /// + /// Seconds since last comms with edge server + /// [JsonProperty("secondsSinceLastAck")] public int SecondsSinceLastAck => (DateTime.Now - mcController.LastAckMessage).Seconds; + /// + /// Create an instance of the class. + /// + /// controller to use for this public MobileControlEdgeServer(MobileControlSystemController controller) { mcController = controller; @@ -104,25 +143,43 @@ namespace PepperDash.Essentials.WebApiHandlers [JsonIgnore] private readonly MobileControlWebsocketServer directServer; + /// + /// URL to use to interact with this server + /// [JsonProperty("userAppUrl")] public string UserAppUrl => $"{directServer.UserAppUrlPrefix}/[insert_client_token]"; + /// + /// TCP/IP Port this server is configured to use + /// [JsonProperty("serverPort")] public int ServerPort => directServer.Port; + /// + /// Count of defined tokens for this server + /// [JsonProperty("tokensDefined")] public int TokensDefined => directServer.UiClientContexts.Count; + /// + /// Count of connected clients + /// [JsonProperty("clientsConnected")] public int ClientsConnected => directServer.ConnectedUiClientsCount; + /// + /// List of tokens and connected clients for this server + /// [JsonProperty("clients")] public List Clients => directServer.UiClientContexts .Select(context => (context, clients: directServer.UiClients.Where(client => client.Value.Token == context.Value.Token.Token).Select(c => c.Value).ToList())) .Select((clientTuple, i) => new MobileControlDirectClient(clientTuple.clients, clientTuple.context, i, directServer.UserAppUrlPrefix)) .ToList(); - + /// + /// Create an instance of the class. + /// + /// public MobileControlDirectServer(MobileControlWebsocketServer server) { directServer = server; @@ -146,23 +203,41 @@ namespace PepperDash.Essentials.WebApiHandlers [JsonIgnore] private readonly string urlPrefix; + /// + /// Client number for this client + /// [JsonProperty("clientNumber")] public string ClientNumber => $"{clientNumber}"; + /// + /// Room Key for this client + /// [JsonProperty("roomKey")] public string RoomKey => context.Token.RoomKey; + /// + /// Touchpanel Key, if defined, for this client + /// [JsonProperty("touchpanelKey")] public string TouchpanelKey => context.Token.TouchpanelKey; + /// + /// URL for this client + /// [JsonProperty("url")] public string Url => $"{urlPrefix}{Key}"; + /// + /// Token for this client + /// [JsonProperty("token")] public string Token => Key; private readonly List clients; + /// + /// List of status for all connected UI Clients + /// [JsonProperty("clientStatus")] public List ClientStatus => clients.Select(c => new ClientStatus(c)).ToList(); @@ -183,14 +258,27 @@ namespace PepperDash.Essentials.WebApiHandlers } } + /// + /// Report the status of a UiClient + /// public class ClientStatus { private readonly UiClient client; + /// + /// True if client is connected + /// public bool Connected => client != null && client.Context.WebSocket.IsAlive; + /// + /// Get the time this client has been connected + /// public double Duration => client == null ? 0 : client.ConnectedDuration.TotalSeconds; + /// + /// Create an instance of the class for the specified client + /// + /// client to report on public ClientStatus(UiClient client) { this.client = client; diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs index 23b79d93..2f972ddf 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs @@ -14,11 +14,20 @@ namespace PepperDash.Essentials.WebApiHandlers public class UiClientHandler : WebApiBaseRequestHandler { private readonly MobileControlWebsocketServer server; + + /// + /// Essentials CWS API handler for the MC Direct Server + /// + /// Direct Server instance public UiClientHandler(MobileControlWebsocketServer directServer) : base(true) { server = directServer; } + /// + /// Create a client for the Direct Server + /// + /// HTTP Context for this request protected override void HandlePost(HttpCwsContext context) { var req = context.Request; @@ -65,6 +74,10 @@ namespace PepperDash.Essentials.WebApiHandlers res.End(); } + /// + /// Handle DELETE request for a Client + /// + /// protected override void HandleDelete(HttpCwsContext context) { var req = context.Request; diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinResponse.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinResponse.cs index ba214db4..1529d0fe 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinResponse.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinResponse.cs @@ -17,9 +17,15 @@ namespace PepperDash.Essentials.WebSocketServer [JsonProperty("clientId")] public string ClientId { get; set; } + /// + /// Room Key for this client + /// [JsonProperty("roomKey")] public string RoomKey { get; set; } + /// + /// System UUID for this system + /// [JsonProperty("systemUUid")] public string SystemUuid { get; set; } diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs index 7352dc6f..63fdcdd4 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/JoinToken.cs @@ -14,10 +14,19 @@ namespace PepperDash.Essentials.WebSocketServer /// public string Code { get; set; } + /// + /// Room Key this token is associated with + /// public string RoomKey { get; set; } + /// + /// Unique ID for this token + /// public string Uuid { get; set; } + /// + /// Touchpanel Key this token is associated with, if this is a touch panel token + /// public string TouchpanelKey { get; set; } = ""; /// diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index f62e136a..759886ff 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -315,6 +315,9 @@ namespace PepperDash.Essentials.WebSocketServer } } + /// + /// Set the internal logging level for the Websocket Server + /// public void SetWebsocketLogLevel(LogLevel level) { CrestronConsole.ConsoleCommandResponse($"Setting direct server debug level to {level}", level.ToString()); diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/ServerTokenSecrets.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/ServerTokenSecrets.cs index 3fa2fb0c..ad9a1d66 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/ServerTokenSecrets.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/ServerTokenSecrets.cs @@ -13,8 +13,15 @@ namespace PepperDash.Essentials.WebSocketServer /// public string GrantCode { get; set; } + /// + /// Gets or sets the Tokens for this server + /// public Dictionary Tokens { get; set; } + /// + /// Initialize a new instance of the class with the provided grant code + /// + /// The grant code for this server public ServerTokenSecrets(string grantCode) { GrantCode = grantCode; diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClientContext.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClientContext.cs index 6782306f..ffc09e90 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClientContext.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClientContext.cs @@ -14,6 +14,10 @@ namespace PepperDash.Essentials.WebSocketServer /// public JoinToken Token { get; private set; } + /// + /// Initialize an instance of the class with the provided token + /// + /// token for this client public UiClientContext(JoinToken token) { Token = token; diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/Version.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/Version.cs index 5552e29b..1255f624 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/Version.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/Version.cs @@ -8,12 +8,25 @@ namespace PepperDash.Essentials.WebSocketServer /// public class Version { + /// + /// Server version this Websocket is connected to + /// [JsonProperty("serverVersion")] public string ServerVersion { get; set; } + /// + /// True if the server is on a processor + /// + [JsonProperty("serverIsRunningOnProcessorHardware")] public bool ServerIsRunningOnProcessorHardware { get; private set; } + /// + /// Initialize an instance of the class + /// + /// + /// the property is set to true by default. + /// public Version() { ServerIsRunningOnProcessorHardware = true; diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/WebSocketServerSecretProvider.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/WebSocketServerSecretProvider.cs index cf125d29..3dd67bd8 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/WebSocketServerSecretProvider.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/WebSocketServerSecretProvider.cs @@ -13,25 +13,28 @@ namespace PepperDash.Essentials.WebSocketServer } /// - /// Represents a WebSocketServerSecret + /// Stores a secret value using the provided secret store provider /// public class WebSocketServerSecret : ISecret { /// - /// Gets or sets the Provider + /// Gets the Secret Provider associated with this secret /// public ISecretProvider Provider { get; private set; } /// - /// Gets or sets the Key + /// Gets the Key associated with this secret /// public string Key { get; private set; } /// - /// Gets or sets the Value + /// Gets the Value associated with this secret /// public object Value { get; private set; } + /// + /// Initialize and instance of the class + /// public WebSocketServerSecret(string key, object value, ISecretProvider provider) { Key = key; From 3e0f318f7f923b9ea1fc9af5c83d8418b3760876 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 15 Oct 2025 12:36:42 -0500 Subject: [PATCH 07/55] fix: update log methods to all be consistent --- .../WebSocketServer/MobileControlWebsocketServer.cs | 13 +------------ .../WebSocketServer/UiClient.cs | 2 +- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index 759886ff..2532a907 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -275,18 +275,7 @@ namespace PepperDash.Essentials.WebSocketServer }; } - _server.Log.Output = (data, message) => - { - switch (data.Level) - { - case LogLevel.Trace: this.LogVerbose(message); break; - case LogLevel.Debug: this.LogDebug(message); break; - case LogLevel.Info: this.LogInformation(message); break; - case LogLevel.Warn: this.LogWarning(message); break; - case LogLevel.Error: this.LogError(message); break; - case LogLevel.Fatal: this.LogFatal(message); break; - } - }; + _server.Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this); // setting to trace to allow logging level to be controlled by appdebug _server.Log.Level = LogLevel.Trace; diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs index 21781abc..83696ef2 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs @@ -89,7 +89,7 @@ namespace PepperDash.Essentials.WebSocketServer _connectionTime = DateTime.Now; - Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message); + Log.Output = (data, message) => Utilities.ConvertWebsocketLog(data, message, this); Log.Level = LogLevel.Trace; if (Controller == null) From 608601990bf758d5ad42a548ed530f789657566c Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 15 Oct 2025 12:54:14 -0500 Subject: [PATCH 08/55] docs: fix Copilot comments --- .../MobileControlSystemController.cs | 4 +++- .../Touchpanel/ITswZoomControlMessenger.cs | 4 ++-- src/PepperDash.Essentials.MobileControl/Utilities.cs | 4 ++-- .../WebApiHandlers/MobileInfoHandler.cs | 2 +- .../WebSocketServer/ConnectionClosedEventArgs.cs | 2 +- .../WebSocketServer/UiClient.cs | 2 +- .../WebSocketServer/Version.cs | 2 +- 7 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index d2aef597..1012abe8 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -110,7 +110,9 @@ namespace PepperDash.Essentials /// public string SystemUrl; //set only from SIMPL Bridge! - /// + /// + /// True if the Mobile Control Edge Server Websocket is connected + /// public bool Connected => _wsClient2 != null && _wsClient2.IsAlive; private IEssentialsRoomCombiner _roomCombiner; diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs index 13d1dc59..bbf4030e 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs @@ -8,14 +8,14 @@ using PepperDash.Essentials.AppServer.Messengers; namespace PepperDash.Essentials.Touchpanel { /// - /// Messenger to handle + /// Messenger to handle Zoom status and control for a TSW panel that supports the Zoom Application /// public class ITswZoomControlMessenger : MessengerBase { private readonly ITswZoomControl _zoomControl; /// - /// Create in instance of the class for the given device + /// Create an instance of the class for the given device /// /// The key for this messenger /// The message path for this messenger diff --git a/src/PepperDash.Essentials.MobileControl/Utilities.cs b/src/PepperDash.Essentials.MobileControl/Utilities.cs index 83ebc5bc..8c2abf3e 100644 --- a/src/PepperDash.Essentials.MobileControl/Utilities.cs +++ b/src/PepperDash.Essentials.MobileControl/Utilities.cs @@ -12,9 +12,9 @@ namespace PepperDash.Essentials private static int nextClientId = 0; /// - /// Get + /// Get the next unique client ID /// - /// + /// Client ID public static int GetNextClientId() { nextClientId++; diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs index 363076c0..676fd3c8 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs @@ -72,7 +72,7 @@ namespace PepperDash.Essentials.WebApiHandlers public MobileControlDirectServer DirectServer => mcController.Config.DirectServer.EnableDirectServer ? new MobileControlDirectServer(mcController.DirectServer) : null; /// - /// Create in instace of the class. + /// Create an instance of the class. /// /// public InformationResponse(MobileControlSystemController controller) diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/ConnectionClosedEventArgs.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/ConnectionClosedEventArgs.cs index 6d877d07..617cc6d6 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/ConnectionClosedEventArgs.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/ConnectionClosedEventArgs.cs @@ -13,7 +13,7 @@ namespace PepperDash.Essentials.WebSocketServer public string ClientId { get; private set; } /// - /// Initalize an instance of the class. + /// Initialize an instance of the class. /// /// client that's closing public ConnectionClosedEventArgs(string clientId) diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs index 83696ef2..e4e8a47d 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs @@ -74,7 +74,7 @@ namespace PepperDash.Essentials.WebSocketServer /// /// The unique key to identify this client /// The client ID used by the client for this connection - /// + /// The token associated with this client public UiClient(string key, string id, string token) { Key = key; diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/Version.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/Version.cs index 1255f624..9380e54d 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/Version.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/Version.cs @@ -25,7 +25,7 @@ namespace PepperDash.Essentials.WebSocketServer /// Initialize an instance of the class /// /// - /// the property is set to true by default. + /// The property is set to true by default. /// public Version() { From 6cb98e12fae40135f9705a80b7ca2a957d0ffc13 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 15 Oct 2025 14:02:06 -0500 Subject: [PATCH 09/55] fix: use correct collection for program stop --- .../WebSocketServer/MobileControlWebsocketServer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index 2532a907..a30c0bd2 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -861,11 +861,11 @@ namespace PepperDash.Essentials.WebSocketServer { if (programEventType == eProgramStatusEventType.Stopping) { - foreach (var client in UiClientContexts.Values) + foreach (var client in UiClients.Values) { - if (client.Client != null && client.Client.Context.WebSocket.IsAlive) + if (client != null && client.Context.WebSocket.IsAlive) { - client.Client.Context.WebSocket.Close(CloseStatusCode.Normal, "Server Shutting Down"); + client.Context.WebSocket.Close(CloseStatusCode.Normal, "Server Shutting Down"); } } From 5c35a3be45cd15a131c82882c481d0218f1f1075 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 15 Oct 2025 14:03:17 -0500 Subject: [PATCH 10/55] fix: catch exceptions in handlers directly Previously, any exceptions that were occuring in a hander's action were being swalled due to being off on another thread. Now, those exceptions are caught and printed out. --- .../MobileControlSystemController.cs | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 1012abe8..01bf3579 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -2337,10 +2337,33 @@ namespace PepperDash.Essentials foreach (var handler in handlers) { - Task.Run( - () => - handler.Action(message.Type, message.ClientId, message.Content) - ); + Task.Run(async () => + { + try + { + handler.Action(message.Type, message.ClientId, message.Content); + } + catch (Exception ex) + { + this.LogError( + "Exception in handler for message type {type}, ClientId {clientId}", + message.Type, + message.ClientId + ); + this.LogDebug(ex, "Stack Trace: "); + } + }).ContinueWith(task => + { + if (task.IsFaulted && task.Exception != null) + { + this.LogError( + "Unhandled exception in Task for message type {type}, ClientId {clientId}", + message.Type, + message.ClientId + ); + this.LogDebug(task.Exception.GetBaseException(), "Stack Trace: "); + } + }, TaskContinuationOptions.OnlyOnFaulted); } break; From 5d5e78629e693beb69139e41cb1209233624da1d Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Tue, 21 Oct 2025 18:17:25 -0500 Subject: [PATCH 11/55] chore: update local build version to 2.18.2-local --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index af743a53..545110d3 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,6 +1,6 @@ - 2.15.1-local + 2.18.2-local $(Version) PepperDash Technology PepperDash Technology From 7c72a0d905461e0fd7811ca1d011c0b969f4c2d5 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Tue, 21 Oct 2025 18:19:09 -0500 Subject: [PATCH 12/55] fix: send device interface list on client join In order to avoid updating the MC Edge API to send device interfaces, one of the responses to the clientJoined message will now be a message with the type `/system/deviceInterfaces`. This has a corresponding handler in the core library to put the interfaces in the correct location. --- .../MobileControlSystemController.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 01bf3579..273ffd9f 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -2185,6 +2185,8 @@ namespace PepperDash.Essentials }; SendMessageObject(message); + + SendDeviceInterfaces(clientId); return; } @@ -2196,7 +2198,10 @@ namespace PepperDash.Essentials ClientId = clientId, Content = roomKey }; + SendMessageObject(message); + + SendDeviceInterfaces(clientId); return; } @@ -2214,6 +2219,8 @@ namespace PepperDash.Essentials }; SendMessageObject(message); + + SendDeviceInterfaces(clientId); return; } @@ -2227,6 +2234,36 @@ namespace PepperDash.Essentials }; SendMessageObject(newMessage); + + SendDeviceInterfaces(clientId); + } + + private void SendDeviceInterfaces(string clientId) + { + this.LogDebug("Sending Device interfaces"); + var devices = DeviceManager.GetDevices(); + Dictionary deviceInterfaces = new Dictionary(); + + foreach (var device in devices) + { + var interfaces = device?.GetType().GetInterfaces().Select((i) => i.Name).ToList() ?? new List(); + + deviceInterfaces.Add(device.Key, new DeviceInterfaceInfo + { + Key = device.Key, + Name = (device as IKeyName)?.Name ?? "", + Interfaces = interfaces + }); + } + + var message = new MobileControlMessage + { + Type = "/system/deviceInterfaces", + ClientId = clientId, + Content = JToken.FromObject(new { deviceInterfaces }) + }; + + SendMessageObject(message); } private void HandleUserCode(JToken content, Action action = null) From 5409db193c5d66451eec759773f7463a7042f605 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 22 Oct 2025 13:53:23 -0600 Subject: [PATCH 13/55] Update src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Config/Essentials/EssentialsConfig.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs index 9665938f..7108762b 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs @@ -82,7 +82,6 @@ namespace PepperDash.Essentials.Core.Config /// Gets or sets the Rooms /// [JsonProperty("rooms")] - public List Rooms { get; set; } /// From 10399a1be898b3eb267c525d663fdbb928aaa4eb Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 22 Oct 2025 13:54:08 -0600 Subject: [PATCH 14/55] Update src/PepperDash.Core/Config/PortalConfigReader.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/PepperDash.Core/Config/PortalConfigReader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PepperDash.Core/Config/PortalConfigReader.cs b/src/PepperDash.Core/Config/PortalConfigReader.cs index b048bdc4..41ca2cbd 100644 --- a/src/PepperDash.Core/Config/PortalConfigReader.cs +++ b/src/PepperDash.Core/Config/PortalConfigReader.cs @@ -247,7 +247,7 @@ namespace PepperDash.Core.Config } catch (Exception e) { - Debug.LogError("Cannot merge items at path {propPath}: \r{e}", propPath, e); + Debug.LogError($"Cannot merge items at path {propPath}: \r{e}"); } } } From 99253b30c2ecda5ea0bed27979276c30c66fde32 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Thu, 23 Oct 2025 09:49:45 -0500 Subject: [PATCH 15/55] fix: send touchpanel key to client when client joins --- .../MobileControlSystemController.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 273ffd9f..f4ad5074 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -2174,6 +2174,7 @@ namespace PepperDash.Essentials { var clientId = content["clientId"].Value(); var roomKey = content["roomKey"].Value(); + var touchpanelKey = content.SelectToken("touchpanelKey"); if (_roomCombiner == null) { @@ -2236,6 +2237,20 @@ namespace PepperDash.Essentials SendMessageObject(newMessage); SendDeviceInterfaces(clientId); + + SendTouchpanelKey(clientId, touchpanelKey); + } + + private void SendTouchpanelKey(string clientId, JToken touchpanelKeyToken) + { + if (touchpanelKeyToken == null) { return; } + + SendMessageObject(new MobileControlMessage + { + Type = "/system/touchpanelKey", + ClientId = clientId, + Content = touchpanelKeyToken.Value() + }); } private void SendDeviceInterfaces(string clientId) From 44432f7a415aa084c2943472ed50c478bc771ede Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Thu, 23 Oct 2025 09:54:03 -0500 Subject: [PATCH 16/55] fix: send touchpanel key to client when client joins direct server --- .../WebSocketServer/MobileControlWebsocketServer.cs | 2 +- .../WebSocketServer/UiClient.cs | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index a30c0bd2..56e34812 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -723,7 +723,7 @@ namespace PepperDash.Essentials.WebSocketServer private UiClient BuildUiClient(string roomKey, JoinToken token, string key) { - var c = new UiClient($"uiclient-{key}-{roomKey}-{token.Id}", token.Id, token.Token); + var c = new UiClient($"uiclient-{key}-{roomKey}-{token.Id}", token.Id, token.Token, token.TouchpanelKey); this.LogInformation("Constructing UiClient with key {key} and ID {id}", key, token.Id); c.Controller = _parent; c.RoomKey = roomKey; diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs index e4e8a47d..b5299ab7 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs @@ -31,6 +31,11 @@ namespace PepperDash.Essentials.WebSocketServer /// public string Token { get; private set; } + /// + /// Touchpanel Key associated with this client + /// + public string TouchpanelKey { get; private set; } + /// /// Gets or sets the mobile control system controller that handles this client's messages /// @@ -75,11 +80,13 @@ namespace PepperDash.Essentials.WebSocketServer /// The unique key to identify this client /// The client ID used by the client for this connection /// The token associated with this client - public UiClient(string key, string id, string token) + /// The touchpanel key associated with this client + public UiClient(string key, string id, string token, string touchpanelKey = "") { Key = key; Id = id; Token = token; + TouchpanelKey = touchpanelKey; } /// @@ -105,6 +112,7 @@ namespace PepperDash.Essentials.WebSocketServer { clientId = Id, roomKey = RoomKey, + touchpanelKey = string.IsNullOrEmpty(TouchpanelKey) ? TouchpanelKey : string.Empty, }) }; From 514ac850caae609556a40ed0150a04588234fe94 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Thu, 23 Oct 2025 10:01:05 -0500 Subject: [PATCH 17/55] fix: send touchpanel key correctly --- .../WebSocketServer/UiClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs index b5299ab7..0b6dbe7c 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs @@ -112,7 +112,7 @@ namespace PepperDash.Essentials.WebSocketServer { clientId = Id, roomKey = RoomKey, - touchpanelKey = string.IsNullOrEmpty(TouchpanelKey) ? TouchpanelKey : string.Empty, + touchpanelKey = string.IsNullOrEmpty(TouchpanelKey) ? string.Empty : TouchpanelKey, }) }; From 8bab3dc966ca00c966b9ee84138b3613779b78ed Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Thu, 23 Oct 2025 11:21:17 -0500 Subject: [PATCH 18/55] fix: send touchpanelKey message with all room combiner checks --- .../MobileControlSystemController.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index f4ad5074..e9bde5f3 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -2188,6 +2188,8 @@ namespace PepperDash.Essentials SendMessageObject(message); SendDeviceInterfaces(clientId); + + SendTouchpanelKey(clientId, touchpanelKey); return; } @@ -2203,6 +2205,8 @@ namespace PepperDash.Essentials SendMessageObject(message); SendDeviceInterfaces(clientId); + + SendTouchpanelKey(clientId, touchpanelKey); return; } @@ -2222,6 +2226,8 @@ namespace PepperDash.Essentials SendMessageObject(message); SendDeviceInterfaces(clientId); + + SendTouchpanelKey(clientId, touchpanelKey); return; } @@ -2243,7 +2249,11 @@ namespace PepperDash.Essentials private void SendTouchpanelKey(string clientId, JToken touchpanelKeyToken) { - if (touchpanelKeyToken == null) { return; } + if (touchpanelKeyToken == null) + { + this.LogWarning("Touchpanel key not found for client {clientId}", clientId); + return; + } SendMessageObject(new MobileControlMessage { From 317bde3814bafc62767d4357d2c208f964b41b6c Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 23 Oct 2025 15:22:46 -0600 Subject: [PATCH 19/55] fix: add InfinetEx to eControlMethods enum --- src/PepperDash.Core/Comm/eControlMethods.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/PepperDash.Core/Comm/eControlMethods.cs b/src/PepperDash.Core/Comm/eControlMethods.cs index 695594a1..b807fdc5 100644 --- a/src/PepperDash.Core/Comm/eControlMethods.cs +++ b/src/PepperDash.Core/Comm/eControlMethods.cs @@ -78,6 +78,10 @@ namespace PepperDash.Core /// /// Used when comms needs to be handled in SIMPL and bridged opposite the normal direction /// - ComBridge + ComBridge, + /// + /// InfinetEX control + /// + InfinetEx } } \ No newline at end of file From 16c92afabbe353f22dc12c2c7c48581c17627837 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Mon, 27 Oct 2025 09:21:51 -0500 Subject: [PATCH 20/55] fix: print json response to console using \r\n instead of \n --- src/PepperDash.Essentials.Core/Devices/DeviceManager.cs | 6 +++--- src/PepperDash.Essentials/ControlSystem.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs b/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs index 1c9709b8..c199773a 100644 --- a/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs +++ b/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs @@ -60,9 +60,9 @@ namespace PepperDash.Essentials.Core ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(DeviceJsonApi.DoDeviceActionWithJson, "devjson", "", ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(s => CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetProperties(s)), "devprops", "", ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(s => CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetMethods(s)), "devmethods", "", ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(s => CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetApiMethods(s)), "apimethods", "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetProperties(s).Replace(Environment.NewLine, "\r\n")), "devprops", "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetMethods(s).Replace(Environment.NewLine, "\r\n")), "devmethods", "", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => CrestronConsole.ConsoleCommandResponse(DeviceJsonApi.GetApiMethods(s).Replace(Environment.NewLine, "\r\n")), "apimethods", "", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(SimulateComReceiveOnDevice, "devsimreceive", "Simulates incoming data on a com device", ConsoleAccessLevelEnum.AccessOperator); diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index 28864e83..72d7c27d 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -104,7 +104,7 @@ namespace PepperDash.Essentials CrestronConsole.ConsoleCommandResponse ("Current running configuration. This is the merged system and template configuration" + CrestronEnvironment.NewLine); CrestronConsole.ConsoleCommandResponse(Newtonsoft.Json.JsonConvert.SerializeObject - (ConfigReader.ConfigObject, Newtonsoft.Json.Formatting.Indented)); + (ConfigReader.ConfigObject, Newtonsoft.Json.Formatting.Indented).Replace(Environment.NewLine, "\r\n")); }, "showconfig", "Shows the current running merged config", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(s => From d01eb0389039f5cbbddab2b7617094c654af6b39 Mon Sep 17 00:00:00 2001 From: jkdevito Date: Mon, 27 Oct 2025 14:06:20 -0500 Subject: [PATCH 21/55] feat: enhance ComPortController with detailed logging on configuration changes --- .../Comm and IR/ComPortController.cs | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs index cc57fa19..15050ebf 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs @@ -8,6 +8,7 @@ using Crestron.SimplSharpPro; using PepperDash.Core; using Serilog.Events; +using PepperDash.Core.Logging; namespace PepperDash.Essentials.Core @@ -71,23 +72,39 @@ namespace PepperDash.Essentials.Core Debug.LogMessage(LogEventLevel.Information, this, "Configured com Port for this device does not exist."); return; } - if (Port.Parent is CrestronControlSystem) - { - var result = Port.Register(); - if (result != eDeviceRegistrationUnRegistrationResponse.Success) - { - Debug.LogMessage(LogEventLevel.Information, this, "ERROR: Cannot register Com port: {0}", result); - return; // false - } - } + if (Port.Parent is CrestronControlSystem) + { + var result = Port.Register(); + if (result != eDeviceRegistrationUnRegistrationResponse.Success) + { + Debug.LogMessage(LogEventLevel.Information, this, "ERROR: Cannot register Com port: {0}", result); + return; // false + } + } - var specResult = Port.SetComPortSpec(Spec); - if (specResult != 0) - { - Debug.LogMessage(LogEventLevel.Information, this, "WARNING: Cannot set comspec"); - return; - } - Port.SerialDataReceived += Port_SerialDataReceived; + Port.PropertyChanged += (s, e) => + { + this.LogInformation($"RegisterAndConfigureComPort: PropertyChanged Fired >> comPort-'{Port.ID}', Property Changed-'{e.Property}', Value Changed-'{e.Value}'"); + this.LogInformation($"RegisterAndConfigureComPort: deviceName-'{Port.DeviceName}', parentDevice-'{Port.ParentDevice}', parent-`{Port.Parent}`, online-`{Port.IsOnline}`, preset-`{Port.Present}`, supportedBaudRates-'{Port.SupportedBaudRates}'"); + }; + Port.ExtendedInformationChanged += (s, e) => + { + this.LogInformation($"RegisterAndConfigureComPort: ExtendedInformationChanged Fired >> comPort-'{Port.ID}', {e.Protocol} using {e.BaudRate},{e.Parity},{e.DataBits},{e.StopBits}, HW Handshake-'{e.HardwareHandshakeSetting}', SW Handshake-'{e.SoftwareHandshakeSetting}'"); + }; + + + this.LogInformation($"RegisterAndConfigureComPort: Configuring comPort-'{Port.ID}' using Spec {Spec.BaudRate},{Spec.DataBits},{Spec.Parity},{Spec.StopBits}, HW Handshake-'{Spec.HardwareHandShake}', SW Handshake-'{Spec.SoftwareHandshake}'"); + + var specResult = Port.SetComPortSpec(Spec); + if (specResult != 0) + { + Debug.LogMessage(LogEventLevel.Information, this, "WARNING: Cannot set comspec"); + return; + } + + this.LogInformation($"RegisterAndConfigureComPort: comPort-'{Port.ID}' configured successfully using {Port.BaudRate},{Port.DataBits},{Port.Parity},{Port.StopBits}, HW Handshake-'{Port.HwHandShake}', SW Handshake-'{Port.SwHandShake}'"); + + Port.SerialDataReceived += Port_SerialDataReceived; } ~ComPortController() From 89e893b3b7ea6a02446f57c911ee06f9f31dadc7 Mon Sep 17 00:00:00 2001 From: jkdevito Date: Mon, 27 Oct 2025 16:46:09 -0500 Subject: [PATCH 22/55] fix: improve formatting and logging in ComPortController --- .../Comm and IR/ComPortController.cs | 230 ++++++++++-------- 1 file changed, 125 insertions(+), 105 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs index 15050ebf..746fd317 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs @@ -9,52 +9,53 @@ using Crestron.SimplSharpPro; using PepperDash.Core; using Serilog.Events; using PepperDash.Core.Logging; +using Crestron.SimplSharpPro.GeneralIO; namespace PepperDash.Essentials.Core { - /// - /// Represents a ComPortController - /// + /// + /// Represents a ComPortController + /// public class ComPortController : Device, IBasicCommunicationWithStreamDebugging { - /// - /// Gets or sets the StreamDebugging - /// - public CommunicationStreamDebugging StreamDebugging { get; private set; } + /// + /// Gets or sets the StreamDebugging + /// + public CommunicationStreamDebugging StreamDebugging { get; private set; } public event EventHandler BytesReceived; public event EventHandler TextReceived; - /// - /// Gets or sets the IsConnected - /// + /// + /// Gets or sets the IsConnected + /// public bool IsConnected { get { return true; } } ComPort Port; ComPort.ComPortSpec Spec; - public ComPortController(string key, Func postActivationFunc, - ComPort.ComPortSpec spec, EssentialsControlPropertiesConfig config) : base(key) - { - StreamDebugging = new CommunicationStreamDebugging(key); + public ComPortController(string key, Func postActivationFunc, + ComPort.ComPortSpec spec, EssentialsControlPropertiesConfig config) : base(key) + { + StreamDebugging = new CommunicationStreamDebugging(key); - Spec = spec; + Spec = spec; - AddPostActivationAction(() => - { - Port = postActivationFunc(config); + AddPostActivationAction(() => + { + Port = postActivationFunc(config); - RegisterAndConfigureComPort(); - }); - } + RegisterAndConfigureComPort(); + }); + } public ComPortController(string key, ComPort port, ComPort.ComPortSpec spec) : base(key) { if (port == null) { - Debug.LogMessage(LogEventLevel.Information, this, "ERROR: Invalid com port, continuing but comms will not function"); + Debug.LogMessage(LogEventLevel.Information, this, "ERROR: Invalid com port, continuing but comms will not function"); return; } @@ -65,14 +66,14 @@ namespace PepperDash.Essentials.Core RegisterAndConfigureComPort(); } - private void RegisterAndConfigureComPort() - { - if (Port == null) - { - Debug.LogMessage(LogEventLevel.Information, this, "Configured com Port for this device does not exist."); - return; - } - if (Port.Parent is CrestronControlSystem) + private void RegisterAndConfigureComPort() + { + if (Port == null) + { + Debug.LogMessage(LogEventLevel.Information, this, "Configured com Port for this device does not exist."); + return; + } + if (Port.Parent is CrestronControlSystem || Port.Parent is CenIoCom102) { var result = Port.Register(); if (result != eDeviceRegistrationUnRegistrationResponse.Success) @@ -82,19 +83,6 @@ namespace PepperDash.Essentials.Core } } - Port.PropertyChanged += (s, e) => - { - this.LogInformation($"RegisterAndConfigureComPort: PropertyChanged Fired >> comPort-'{Port.ID}', Property Changed-'{e.Property}', Value Changed-'{e.Value}'"); - this.LogInformation($"RegisterAndConfigureComPort: deviceName-'{Port.DeviceName}', parentDevice-'{Port.ParentDevice}', parent-`{Port.Parent}`, online-`{Port.IsOnline}`, preset-`{Port.Present}`, supportedBaudRates-'{Port.SupportedBaudRates}'"); - }; - Port.ExtendedInformationChanged += (s, e) => - { - this.LogInformation($"RegisterAndConfigureComPort: ExtendedInformationChanged Fired >> comPort-'{Port.ID}', {e.Protocol} using {e.BaudRate},{e.Parity},{e.DataBits},{e.StopBits}, HW Handshake-'{e.HardwareHandshakeSetting}', SW Handshake-'{e.SoftwareHandshakeSetting}'"); - }; - - - this.LogInformation($"RegisterAndConfigureComPort: Configuring comPort-'{Port.ID}' using Spec {Spec.BaudRate},{Spec.DataBits},{Spec.Parity},{Spec.StopBits}, HW Handshake-'{Spec.HardwareHandShake}', SW Handshake-'{Spec.SoftwareHandshake}'"); - var specResult = Port.SetComPortSpec(Spec); if (specResult != 0) { @@ -102,50 +90,82 @@ namespace PepperDash.Essentials.Core return; } - this.LogInformation($"RegisterAndConfigureComPort: comPort-'{Port.ID}' configured successfully using {Port.BaudRate},{Port.DataBits},{Port.Parity},{Port.StopBits}, HW Handshake-'{Port.HwHandShake}', SW Handshake-'{Port.SwHandShake}'"); + //this.LogInformation($"RegisterAndConfigureComPort: Port.Parent-'{Port.Parent}', Port.ParentDevice-'{Port.ParentDevice}'"); + + // if (Port.Parent is CenIoCom102) + // { + // this.LogInformation($"RegisterAndConfigureComPort: "); + // Port.Register(); + + // Port.PropertyChanged += (s, e) => + // { + // this.LogInformation($"RegisterAndConfigureComPort: PropertyChanged Fired >> comPort-'{Port.ID}', Property Changed-'{e.Property}', Value Changed-'{e.Value}'"); + // this.LogInformation($"RegisterAndConfigureComPort: deviceName-'{Port.DeviceName}', parentDevice-'{Port.ParentDevice}', parent-`{Port.Parent}`, online-`{Port.IsOnline}`, preset-`{Port.Present}`, supportedBaudRates-'{Port.SupportedBaudRates}'"); + // }; + // Port.ExtendedInformationChanged += (s, e) => + // { + // this.LogInformation($@"RegisterAndConfigureComPort: ExtendedInformationChanged Fired >> comPort-'{Port.ID}', {e.Protocol} using ` + // {e.BaudRate}, + // {e.Parity}, + // {e.DataBits}, + // {e.StopBits}, + // HW Handshake-'{e.HardwareHandshakeSetting}', + // SW Handshake-'{e.SoftwareHandshakeSetting}'"); + // }; + + // this.LogInformation($@"RegisterAndConfigureComPort: Configuring comPort-'{Port.ID}' using Spec + // {Spec.BaudRate}, + // {Spec.DataBits}, + // {Spec.Parity}, + // {Spec.StopBits}, + // HW Handshake-'{Spec.HardwareHandShake}', + // SW Handshake-'{Spec.SoftwareHandshake}'"); + + + // } Port.SerialDataReceived += Port_SerialDataReceived; - } + } - ~ComPortController() + ~ComPortController() { Port.SerialDataReceived -= Port_SerialDataReceived; } void Port_SerialDataReceived(ComPort ReceivingComPort, ComPortSerialDataEventArgs args) { - OnDataReceived(args.SerialData); + OnDataReceived(args.SerialData); } - void OnDataReceived(string s) - { + void OnDataReceived(string s) + { var eventSubscribed = false; - var bytesHandler = BytesReceived; - if (bytesHandler != null) - { - var bytes = Encoding.GetEncoding(28591).GetBytes(s); + var bytesHandler = BytesReceived; + if (bytesHandler != null) + { + var bytes = Encoding.GetEncoding(28591).GetBytes(s); if (StreamDebugging.RxStreamDebuggingIsEnabled) Debug.LogMessage(LogEventLevel.Information, this, "Received: '{0}'", ComTextHelper.GetEscapedText(bytes)); - bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); + bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); eventSubscribed = true; - } - var textHandler = TextReceived; - if (textHandler != null) - { + } + var textHandler = TextReceived; + if (textHandler != null) + { if (StreamDebugging.RxStreamDebuggingIsEnabled) Debug.LogMessage(LogEventLevel.Information, this, "Received: '{0}'", s); - textHandler(this, new GenericCommMethodReceiveTextArgs(s)); + textHandler(this, new GenericCommMethodReceiveTextArgs(s)); eventSubscribed = true; - } + } - if(!eventSubscribed) Debug.LogMessage(LogEventLevel.Warning, this, "Received data but no handler is registered"); - } + if (!eventSubscribed) Debug.LogMessage(LogEventLevel.Warning, this, "Received data but no handler is registered"); + } - /// - /// Deactivate method - /// - /// + /// + /// Deactivate method + /// + /// public override bool Deactivate() { return Port.UnRegister() == eDeviceRegistrationUnRegistrationResponse.Success; @@ -153,70 +173,70 @@ namespace PepperDash.Essentials.Core #region IBasicCommunication Members - /// - /// SendText method - /// + /// + /// SendText method + /// public void SendText(string text) { if (Port == null) return; - if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} characters of text: '{1}'", text.Length, text); - Port.Send(text); + if (StreamDebugging.TxStreamDebuggingIsEnabled) + Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} characters of text: '{1}'", text.Length, text); + Port.Send(text); } - /// - /// SendBytes method - /// + /// + /// SendBytes method + /// public void SendBytes(byte[] bytes) { if (Port == null) return; var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); - if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); + if (StreamDebugging.TxStreamDebuggingIsEnabled) + Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); Port.Send(text); } - /// - /// Connect method - /// + /// + /// Connect method + /// public void Connect() - { + { } - /// - /// Disconnect method - /// + /// + /// Disconnect method + /// public void Disconnect() { } #endregion - /// - /// - /// - /// - /// - /// SimulateReceive method - /// - public void SimulateReceive(string s) - { - // split out hex chars and build string - var split = Regex.Split(s, @"(\\[Xx][0-9a-fA-F][0-9a-fA-F])"); - StringBuilder b = new StringBuilder(); - foreach (var t in split) - { - if (t.StartsWith(@"\") && t.Length == 4) - b.Append((char)(Convert.ToByte(t.Substring(2, 2), 16))); - else - b.Append(t); - } + /// + /// + /// + /// + /// + /// SimulateReceive method + /// + public void SimulateReceive(string s) + { + // split out hex chars and build string + var split = Regex.Split(s, @"(\\[Xx][0-9a-fA-F][0-9a-fA-F])"); + StringBuilder b = new StringBuilder(); + foreach (var t in split) + { + if (t.StartsWith(@"\") && t.Length == 4) + b.Append((char)(Convert.ToByte(t.Substring(2, 2), 16))); + else + b.Append(t); + } - OnDataReceived(b.ToString()); - } + OnDataReceived(b.ToString()); + } } } \ No newline at end of file From 151a90c9beeaa18bf10e1939b6123cd30e2c209c Mon Sep 17 00:00:00 2001 From: jkdevito Date: Mon, 27 Oct 2025 17:13:43 -0500 Subject: [PATCH 23/55] refactor: streamline ComPort registration and configuration logic with enhanced logging --- .../Comm and IR/ComPortController.cs | 80 +++++++++---------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs index 746fd317..fcd1bdde 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs @@ -73,56 +73,56 @@ namespace PepperDash.Essentials.Core Debug.LogMessage(LogEventLevel.Information, this, "Configured com Port for this device does not exist."); return; } - if (Port.Parent is CrestronControlSystem || Port.Parent is CenIoCom102) + // TODO [ ] - Verify nothing blows up without the `if` check + //if (Port.Parent is CrestronControlSystem || Port.Parent is CenIoCom102) + //{ + var result = Port.Register(); + if (result != eDeviceRegistrationUnRegistrationResponse.Success) { - var result = Port.Register(); - if (result != eDeviceRegistrationUnRegistrationResponse.Success) - { - Debug.LogMessage(LogEventLevel.Information, this, "ERROR: Cannot register Com port: {0}", result); - return; // false - } + Debug.LogMessage(LogEventLevel.Information, this, "ERROR: Cannot register Com port: {0}", result); + return; // false } + //} var specResult = Port.SetComPortSpec(Spec); if (specResult != 0) { - Debug.LogMessage(LogEventLevel.Information, this, "WARNING: Cannot set comspec"); + this.LogInformation("WARNING: Cannot set comspec"); return; } - - //this.LogInformation($"RegisterAndConfigureComPort: Port.Parent-'{Port.Parent}', Port.ParentDevice-'{Port.ParentDevice}'"); - - // if (Port.Parent is CenIoCom102) - // { - // this.LogInformation($"RegisterAndConfigureComPort: "); - // Port.Register(); - - // Port.PropertyChanged += (s, e) => - // { - // this.LogInformation($"RegisterAndConfigureComPort: PropertyChanged Fired >> comPort-'{Port.ID}', Property Changed-'{e.Property}', Value Changed-'{e.Value}'"); - // this.LogInformation($"RegisterAndConfigureComPort: deviceName-'{Port.DeviceName}', parentDevice-'{Port.ParentDevice}', parent-`{Port.Parent}`, online-`{Port.IsOnline}`, preset-`{Port.Present}`, supportedBaudRates-'{Port.SupportedBaudRates}'"); - // }; - // Port.ExtendedInformationChanged += (s, e) => - // { - // this.LogInformation($@"RegisterAndConfigureComPort: ExtendedInformationChanged Fired >> comPort-'{Port.ID}', {e.Protocol} using ` - // {e.BaudRate}, - // {e.Parity}, - // {e.DataBits}, - // {e.StopBits}, - // HW Handshake-'{e.HardwareHandshakeSetting}', - // SW Handshake-'{e.SoftwareHandshakeSetting}'"); - // }; - - // this.LogInformation($@"RegisterAndConfigureComPort: Configuring comPort-'{Port.ID}' using Spec - // {Spec.BaudRate}, - // {Spec.DataBits}, - // {Spec.Parity}, - // {Spec.StopBits}, - // HW Handshake-'{Spec.HardwareHandShake}', - // SW Handshake-'{Spec.SoftwareHandshake}'"); + this.LogDebug($"ComPort comspec successfully set (specResult == {specResult})"); - // } + // TODO [ ] - Remove debug logging once verified working + if (Port.Parent is CenIoCom102) + { + Port.PropertyChanged += (s, e) => + { + this.LogInformation($@"RegisterAndConfigureComPort: PropertyChanged Fired >> + comPort-'{Port.ID}', + Property Changed-'{e.Property}', + Value Changed-'{e.Value}', + deviceName-'{Port.DeviceName}', + parentDevice-'{Port.ParentDevice}', + parent-`{Port.Parent}`, + online-`{Port.IsOnline}`, + present-`{Port.Present}`, + supportedBaudRates-'{Port.SupportedBaudRates}'"); + }; + Port.ExtendedInformationChanged += (s, e) => + { + + this.LogInformation($@"RegisterAndConfigureComPort: ExtendedInformationChanged Fired >> + comPort-'{Port.ID}', + {e.Protocol}, + {e.BaudRate}, + {e.Parity}, + {e.DataBits}, + {e.StopBits}, + HW Handshake-'{e.HardwareHandshakeSetting}', + SW Handshake-'{e.SoftwareHandshakeSetting}'"); + }; + } Port.SerialDataReceived += Port_SerialDataReceived; } From 32a332a6e20f4c0968ca7d8965aa1564eb4e9c65 Mon Sep 17 00:00:00 2001 From: jkdevito Date: Mon, 27 Oct 2025 17:32:40 -0500 Subject: [PATCH 24/55] refactor: update TODO comment for Port registration verification --- src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs index fcd1bdde..c2d71945 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs @@ -73,7 +73,7 @@ namespace PepperDash.Essentials.Core Debug.LogMessage(LogEventLevel.Information, this, "Configured com Port for this device does not exist."); return; } - // TODO [ ] - Verify nothing blows up without the `if` check + // TODO [ ] - Verify nothing blows up without the `if` check || if there is a property identifying the parent/device is either registerable or notRegisterable //if (Port.Parent is CrestronControlSystem || Port.Parent is CenIoCom102) //{ var result = Port.Register(); From 3ce282e2dba03a9554fac487647d72e45f38c699 Mon Sep 17 00:00:00 2001 From: jkdevito Date: Mon, 27 Oct 2025 17:33:06 -0500 Subject: [PATCH 25/55] refactor: comment out debug logging for ComPort registration --- .../Comm and IR/ComPortController.cs | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs index c2d71945..ca9b51fe 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs @@ -94,35 +94,35 @@ namespace PepperDash.Essentials.Core // TODO [ ] - Remove debug logging once verified working - if (Port.Parent is CenIoCom102) - { - Port.PropertyChanged += (s, e) => - { - this.LogInformation($@"RegisterAndConfigureComPort: PropertyChanged Fired >> - comPort-'{Port.ID}', - Property Changed-'{e.Property}', - Value Changed-'{e.Value}', - deviceName-'{Port.DeviceName}', - parentDevice-'{Port.ParentDevice}', - parent-`{Port.Parent}`, - online-`{Port.IsOnline}`, - present-`{Port.Present}`, - supportedBaudRates-'{Port.SupportedBaudRates}'"); - }; - Port.ExtendedInformationChanged += (s, e) => - { + // if (Port.Parent is CenIoCom102) + // { + // Port.PropertyChanged += (s, e) => + // { + // this.LogInformation($@"RegisterAndConfigureComPort: PropertyChanged Fired >> + // comPort-'{Port.ID}', + // Property Changed-'{e.Property}', + // Value Changed-'{e.Value}', + // deviceName-'{Port.DeviceName}', + // parentDevice-'{Port.ParentDevice}', + // parent-`{Port.Parent}`, + // online-`{Port.IsOnline}`, + // present-`{Port.Present}`, + // supportedBaudRates-'{Port.SupportedBaudRates}'"); + // }; + // Port.ExtendedInformationChanged += (s, e) => + // { - this.LogInformation($@"RegisterAndConfigureComPort: ExtendedInformationChanged Fired >> - comPort-'{Port.ID}', - {e.Protocol}, - {e.BaudRate}, - {e.Parity}, - {e.DataBits}, - {e.StopBits}, - HW Handshake-'{e.HardwareHandshakeSetting}', - SW Handshake-'{e.SoftwareHandshakeSetting}'"); - }; - } + // this.LogInformation($@"RegisterAndConfigureComPort: ExtendedInformationChanged Fired >> + // comPort-'{Port.ID}', + // {e.Protocol}, + // {e.BaudRate}, + // {e.Parity}, + // {e.DataBits}, + // {e.StopBits}, + // HW Handshake-'{e.HardwareHandshakeSetting}', + // SW Handshake-'{e.SoftwareHandshakeSetting}'"); + // }; + // } Port.SerialDataReceived += Port_SerialDataReceived; } From f27965ac29fde1beff2102b4f69520858330b6d6 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 27 Oct 2025 17:35:38 -0600 Subject: [PATCH 26/55] feat: Add help request functionality to Fusion system controller Introduce `IFusionHelpRequest` interface for managing help requests, including `HelpRequestResponseFeedback` property and `SendHelpRequest` method. Enhance `EssentialsHuddleSpaceFusionSystemControllerBase` with new properties and methods to support help request tracking and management. Improve code organization and documentation throughout the affected files. --- ...lsHuddleSpaceFusionSystemControllerBase.cs | 149 ++++++++++++++++-- .../Fusion/IFusionHelpRequest.cs | 25 +++ 2 files changed, 164 insertions(+), 10 deletions(-) create mode 100644 src/PepperDash.Essentials.Core/Fusion/IFusionHelpRequest.cs diff --git a/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs b/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs index e2638493..1497e164 100644 --- a/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs +++ b/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs @@ -1,6 +1,4 @@ - - -using Crestron.SimplSharp; +using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.CrestronXml; using Crestron.SimplSharp.CrestronXml.Serialization; @@ -21,7 +19,7 @@ namespace PepperDash.Essentials.Core.Fusion /// /// Represents a EssentialsHuddleSpaceFusionSystemControllerBase /// - public class EssentialsHuddleSpaceFusionSystemControllerBase : Device, IOccupancyStatusProvider + public class EssentialsHuddleSpaceFusionSystemControllerBase : Device, IOccupancyStatusProvider, IFusionHelpRequest { private readonly EssentialsHuddleSpaceRoomFusionRoomJoinMap JoinMap; @@ -31,13 +29,27 @@ namespace PepperDash.Essentials.Core.Fusion private readonly Dictionary _sourceToFeedbackSigs = new Dictionary(); + /// + /// Gets or sets the CurrentRoomSourceNameSig + /// protected StringSigData CurrentRoomSourceNameSig; private readonly FusionCustomPropertiesBridge CustomPropertiesBridge = new FusionCustomPropertiesBridge(); + + /// + /// Gets or sets the FusionOccSensor + /// protected FusionOccupancySensorAsset FusionOccSensor; private readonly FusionRemoteOccupancySensor FusionRemoteOccSensor; + /// + /// Gets or sets the FusionRoom + /// protected FusionRoom FusionRoom; + + /// + /// Gets or sets the FusionStaticAssets + /// protected Dictionary FusionStaticAssets; private readonly long PushNotificationTimeout = 5000; private readonly IEssentialsRoom Room; @@ -60,6 +72,10 @@ namespace PepperDash.Essentials.Core.Fusion private string _roomOccupancyRemoteString; + private bool _helpRequestSent; + + public StringFeedback HelpRequestResponseFeedback { get; private set; } + #region System Info Sigs //StringSigData SystemName; @@ -93,6 +109,12 @@ namespace PepperDash.Essentials.Core.Fusion #endregion + /// + /// + /// + /// + /// + /// public EssentialsHuddleSpaceFusionSystemControllerBase(IEssentialsRoom room, uint ipId, string joinMapKey) : base(room.Key + "-fusion") { @@ -171,6 +193,9 @@ namespace PepperDash.Essentials.Core.Fusion } } + HelpRequestResponseFeedback = new StringFeedback("HelpRequestResponse", () => FusionRoom.Help.InputSig.StringValue); + HelpRequestResponseFeedback.LinkInputSig(FusionRoom.Help.InputSig); + AddPostActivationAction(() => PostActivate(guidFilePath)); } @@ -194,6 +219,9 @@ namespace PepperDash.Essentials.Core.Fusion GenerateGuidFile(guidFilePath); } + /// + /// Gets the RoomGuid + /// protected string RoomGuid { get { return _guiDs.RoomGuid; } @@ -204,6 +232,9 @@ namespace PepperDash.Essentials.Core.Fusion /// public StringFeedback RoomOccupancyRemoteStringFeedback { get; private set; } + /// + /// Gets the RoomIsOccupiedFeedbackFunc + /// protected Func RoomIsOccupiedFeedbackFunc { get { return () => FusionRemoteOccSensor.RoomOccupied.OutputSig.BoolValue; } @@ -218,10 +249,16 @@ namespace PepperDash.Essentials.Core.Fusion #endregion + /// + /// ScheduleChange event + /// public event EventHandler ScheduleChange; //public event EventHandler MeetingEndWarning; //public event EventHandler NextMeetingBeginWarning; + /// + /// RoomInfoChange event + /// public event EventHandler RoomInfoChange; //ScheduleResponseEvent NextMeeting; @@ -343,6 +380,10 @@ namespace PepperDash.Essentials.Core.Fusion } } + /// + /// CreateSymbolAndBasicSigs method + /// + /// protected virtual void CreateSymbolAndBasicSigs(uint ipId) { Debug.LogMessage(LogEventLevel.Information, this, "Creating Fusion Room symbol with GUID: {0} and IP-ID {1:X2}", RoomGuid, ipId); @@ -405,6 +446,10 @@ namespace PepperDash.Essentials.Core.Fusion CrestronEnvironment.EthernetEventHandler += CrestronEnvironment_EthernetEventHandler; } + /// + /// CrestronEnvironment_EthernetEventHandler method + /// + /// protected void CrestronEnvironment_EthernetEventHandler(EthernetEventArgs ethernetEventArgs) { if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp) @@ -413,6 +458,9 @@ namespace PepperDash.Essentials.Core.Fusion } } + /// + /// GetSystemInfo method + /// protected void GetSystemInfo() { //SystemName.InputSig.StringValue = Room.Name; @@ -426,6 +474,9 @@ namespace PepperDash.Essentials.Core.Fusion () => CrestronConsole.SendControlSystemCommand("reboot", ref response)); } + /// + /// SetUpEthernetValues method + /// protected void SetUpEthernetValues() { _ip1 = FusionRoom.CreateOffsetStringSig(JoinMap.ProcessorIp1.JoinNumber, JoinMap.ProcessorIp1.AttributeName, eSigIoMask.InputSigOnly); @@ -441,6 +492,9 @@ namespace PepperDash.Essentials.Core.Fusion _netMask2 = FusionRoom.CreateOffsetStringSig(JoinMap.ProcessorNetMask2.JoinNumber, JoinMap.ProcessorNetMask2.AttributeName, eSigIoMask.InputSigOnly); } + /// + /// GetProcessorEthernetValues method + /// protected void GetProcessorEthernetValues() { _ip1.InputSig.StringValue = @@ -489,6 +543,9 @@ namespace PepperDash.Essentials.Core.Fusion } } + /// + /// GetProcessorInfo method + /// protected void GetProcessorInfo() { _firmware = FusionRoom.CreateOffsetStringSig(JoinMap.ProcessorFirmware.JoinNumber, JoinMap.ProcessorFirmware.AttributeName, eSigIoMask.InputSigOnly); @@ -507,6 +564,9 @@ namespace PepperDash.Essentials.Core.Fusion _firmware.InputSig.StringValue = InitialParametersClass.FirmwareVersion; } + /// + /// GetCustomProperties method + /// protected void GetCustomProperties() { if (FusionRoom.IsOnline) @@ -524,6 +584,11 @@ namespace PepperDash.Essentials.Core.Fusion // TODO: Get IP and Project Name from TP } + /// + /// FusionRoom_OnlineStatusChange method + /// + /// + /// protected void FusionRoom_OnlineStatusChange(GenericBase currentDevice, OnlineOfflineEventArgs args) { if (args.DeviceOnLine) @@ -1065,6 +1130,9 @@ namespace PepperDash.Essentials.Core.Fusion } } + /// + /// SetUpSources method + /// protected virtual void SetUpSources() { // Sources @@ -1157,7 +1225,13 @@ namespace PepperDash.Essentials.Core.Fusion Debug.LogMessage(LogEventLevel.Debug, this, "Device usage string: {0}", deviceUsage); } - + /// + /// Tries to add route action sigs for a source + /// + /// + /// + /// + /// protected void TryAddRouteActionSigs(string attrName, uint attrNum, string routeKey, Device pSrc) { Debug.LogMessage(LogEventLevel.Verbose, this, "Creating attribute '{0}' with join {1} for source {2}", @@ -1185,9 +1259,7 @@ namespace PepperDash.Essentials.Core.Fusion } } - /// - /// - /// + private void SetUpCommunitcationMonitors() { uint displayNum = 0; @@ -1285,6 +1357,9 @@ namespace PepperDash.Essentials.Core.Fusion } } + /// + /// SetUpDisplay method + /// protected virtual void SetUpDisplay() { try @@ -1588,12 +1663,25 @@ namespace PepperDash.Essentials.Core.Fusion } } + /// + /// Event handler for Fusion state changes + /// + /// + /// protected void FusionRoom_FusionStateChange(FusionBase device, FusionStateEventArgs args) { + if (args.EventId == FusionEventIds.HelpMessageReceivedEventId) + { + Debug.LogMessage(LogEventLevel.Information, this, "Help message received from Fusion for room '{0}'", + Room.Name); + // Fire help request event + HelpRequestResponseFeedback.FireUpdate(); + } + + // The sig/UO method: Need separate handlers for fixed and user sigs, all flavors, // even though they all contain sigs. - BoolOutputSig outSig; if (args.UserConfiguredSigDetail is BooleanSigDataFixedName sigData) { @@ -1632,9 +1720,40 @@ namespace PepperDash.Essentials.Core.Fusion (outSig.UserObject as Action).Invoke(outSig.StringValue); } } + + /// + /// Sends a help request to Fusion with room name and timestamp + /// + /// + public void SendHelpRequest(bool isHtml) + { + var now = DateTime.Now; + + var breakString = !isHtml ? "\r\n" : "
"; + + var requestString = $"HR00: {breakString} Assistance has been requested from room {Room.Name}{breakString}on {now.ToLongDateString()} at {now.ToLongTimeString()}"; + + FusionRoom.Help.InputSig.StringValue = requestString; + + Debug.LogMessage(LogEventLevel.Information, this, "Help request sent to Fusion from room '{0}'", Room.Name); + + _helpRequestSent = true; + } + + public void CancelHelpRequest() + { + if (_helpRequestSent) + { + FusionRoom.Help.InputSig.StringValue = ""; + _helpRequestSent = false; + Debug.LogMessage(LogEventLevel.Information, this, "Help request cancelled in Fusion for room '{0}'", Room.Name); + } + } } - + /// + /// Extensions to enhance Fusion room, asset and signal creation. + /// public static class FusionRoomExtensions { /// @@ -1803,6 +1922,9 @@ namespace PepperDash.Essentials.Core.Fusion /// public class RoomInformation { + /// + /// Constructor + /// public RoomInformation() { FusionCustomProperties = new List(); @@ -1855,10 +1977,17 @@ namespace PepperDash.Essentials.Core.Fusion /// public class FusionCustomProperty { + /// + /// Constructor + /// public FusionCustomProperty() { } + /// + /// Constructor with id + /// + /// public FusionCustomProperty(string id) { ID = id; diff --git a/src/PepperDash.Essentials.Core/Fusion/IFusionHelpRequest.cs b/src/PepperDash.Essentials.Core/Fusion/IFusionHelpRequest.cs new file mode 100644 index 00000000..30033779 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Fusion/IFusionHelpRequest.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PepperDash.Essentials.Core.Fusion +{ + /// + /// Represents Fusion Help Request functionality + /// + public interface IFusionHelpRequest + { + /// + /// Gets the HelpRequstResponseFeedback + /// + StringFeedback HelpRequestResponseFeedback { get; } + + /// + /// Sends a help request + /// + /// + void SendHelpRequest(bool isHtml); + } +} From c4d064965f8b03e5f90d29765da570012be2b509 Mon Sep 17 00:00:00 2001 From: jkdevito Date: Tue, 28 Oct 2025 08:40:29 -0500 Subject: [PATCH 27/55] refactor: enhance ComPortController with additional event documentation and logging improvements --- .../Comm and IR/ComPortController.cs | 47 ++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs index ca9b51fe..0c4bc198 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs @@ -24,7 +24,14 @@ namespace PepperDash.Essentials.Core /// public CommunicationStreamDebugging StreamDebugging { get; private set; } + /// + /// Event fired when bytes are received + /// public event EventHandler BytesReceived; + + /// + /// Event fired when text is received + /// public event EventHandler TextReceived; /// @@ -35,6 +42,13 @@ namespace PepperDash.Essentials.Core ComPort Port; ComPort.ComPortSpec Spec; + /// + /// Constructor + /// + /// + /// + /// + /// public ComPortController(string key, Func postActivationFunc, ComPort.ComPortSpec spec, EssentialsControlPropertiesConfig config) : base(key) { @@ -50,6 +64,12 @@ namespace PepperDash.Essentials.Core }); } + /// + /// Constructor + /// + /// Device key + /// COM port instance + /// COM port specification public ComPortController(string key, ComPort port, ComPort.ComPortSpec spec) : base(key) { @@ -73,24 +93,26 @@ namespace PepperDash.Essentials.Core Debug.LogMessage(LogEventLevel.Information, this, "Configured com Port for this device does not exist."); return; } - // TODO [ ] - Verify nothing blows up without the `if` check || if there is a property identifying the parent/device is either registerable or notRegisterable + // TODO [ ] - Remove commented out code once verified working //if (Port.Parent is CrestronControlSystem || Port.Parent is CenIoCom102) - //{ - var result = Port.Register(); - if (result != eDeviceRegistrationUnRegistrationResponse.Success) + if (Port.Parent is GenericBase genericDevice && genericDevice.Registerable) { - Debug.LogMessage(LogEventLevel.Information, this, "ERROR: Cannot register Com port: {0}", result); - return; // false + this.LogInformation($"INFO: Attempting to register {Port.Parent.GetType().Name}-comport-{Port.ID} using GenericBase"); + var result = genericDevice.Register(); + if (result != eDeviceRegistrationUnRegistrationResponse.Success) + { + this.LogError($"ERROR: Cannot register comport: {result}"); + return; // false + } } - //} - + var specResult = Port.SetComPortSpec(Spec); if (specResult != 0) { - this.LogInformation("WARNING: Cannot set comspec"); + this.LogError("ERROR: Cannot set comspec"); return; } - this.LogDebug($"ComPort comspec successfully set (specResult == {specResult})"); + this.LogInformation($"INFO: Comspec successfully set (result == {specResult})"); // TODO [ ] - Remove debug logging once verified working @@ -111,7 +133,7 @@ namespace PepperDash.Essentials.Core // }; // Port.ExtendedInformationChanged += (s, e) => // { - + // this.LogInformation($@"RegisterAndConfigureComPort: ExtendedInformationChanged Fired >> // comPort-'{Port.ID}', // {e.Protocol}, @@ -127,6 +149,9 @@ namespace PepperDash.Essentials.Core Port.SerialDataReceived += Port_SerialDataReceived; } + /// + /// Destructor + /// ~ComPortController() { Port.SerialDataReceived -= Port_SerialDataReceived; From 92c9db8237be1729255c5a020d76e22231ac50dc Mon Sep 17 00:00:00 2001 From: jkdevito Date: Tue, 28 Oct 2025 09:04:15 -0500 Subject: [PATCH 28/55] refactor: improve logging messages in ComPort registration and configuration --- .../Comm and IR/ComPortController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs index 0c4bc198..ee65c208 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs @@ -97,11 +97,11 @@ namespace PepperDash.Essentials.Core //if (Port.Parent is CrestronControlSystem || Port.Parent is CenIoCom102) if (Port.Parent is GenericBase genericDevice && genericDevice.Registerable) { - this.LogInformation($"INFO: Attempting to register {Port.Parent.GetType().Name}-comport-{Port.ID} using GenericBase"); + //this.LogInformation($"INFO: Attempting to register {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID}"); var result = genericDevice.Register(); if (result != eDeviceRegistrationUnRegistrationResponse.Success) { - this.LogError($"ERROR: Cannot register comport: {result}"); + this.LogError($"ERROR: Cannot register {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {result})"); return; // false } } @@ -109,10 +109,10 @@ namespace PepperDash.Essentials.Core var specResult = Port.SetComPortSpec(Spec); if (specResult != 0) { - this.LogError("ERROR: Cannot set comspec"); + this.LogError($"ERROR: Cannot set comspec for {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {specResult})"); return; } - this.LogInformation($"INFO: Comspec successfully set (result == {specResult})"); + //this.LogInformation($"INFO: Successfully set comspec for {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {specResult})"); // TODO [ ] - Remove debug logging once verified working From ba576180a7f6ddd73f3b642a4372fa35af650fd4 Mon Sep 17 00:00:00 2001 From: jkdevito Date: Tue, 28 Oct 2025 09:05:56 -0500 Subject: [PATCH 29/55] refactor: remove unused using directives in ComPortController --- .../Comm and IR/ComPortController.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs index ee65c208..ba599ead 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs @@ -1,15 +1,10 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using System.Text.RegularExpressions; -using Crestron.SimplSharp; using Crestron.SimplSharpPro; - using PepperDash.Core; -using Serilog.Events; using PepperDash.Core.Logging; -using Crestron.SimplSharpPro.GeneralIO; +using Serilog.Events; namespace PepperDash.Essentials.Core From 2e95f5337e8283a77e845a274129eab4dde099e8 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 28 Oct 2025 16:49:29 -0600 Subject: [PATCH 30/55] feat: Add IEssentialsRoomFusionController and related configurations - Introduced IEssentialsRoomFusionControllerFactory for creating Fusion Room Controller devices. - Added IEssentialsRoomFusionControllerPropertiesConfig to define configuration properties for the Fusion Room Controller. - Updated IFusionHelpRequest interface to include methods for cancelling and toggling help requests. - Refactored RoomOnToDefaultSourceWhenOccupied to use IEssentialsRoomFusionController instead of EssentialsHuddleSpaceFusionSystemControllerBase. - Modified EssentialsRoomBase to check for IEssentialsRoomFusionController in occupancy status provider. --- .gitignore | 1 + ....cs => IEssentialsRoomFusionController.cs} | 147 ++++++++++++------ .../IEssentialsRoomFusionControllerFactory.cs | 33 ++++ ...alsRoomFusionControllerPropertiesConfig.cs | 25 +++ .../Fusion/IFusionHelpRequest.cs | 17 +- .../RoomOnToDefaultSourceWhenOccupied.cs | 4 +- .../Room/EssentialsRoomBase.cs | 2 +- 7 files changed, 179 insertions(+), 50 deletions(-) rename src/PepperDash.Essentials.Core/Fusion/{EssentialsHuddleSpaceFusionSystemControllerBase.cs => IEssentialsRoomFusionController.cs} (96%) create mode 100644 src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerFactory.cs create mode 100644 src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerPropertiesConfig.cs diff --git a/.gitignore b/.gitignore index db1e92a3..d64977d6 100644 --- a/.gitignore +++ b/.gitignore @@ -396,3 +396,4 @@ _site/ api/ *.DS_Store /._PepperDash.Essentials.4Series.sln +dotnet diff --git a/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs similarity index 96% rename from src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs rename to src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs index 1497e164..fa7c36a4 100644 --- a/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs +++ b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs @@ -19,12 +19,12 @@ namespace PepperDash.Essentials.Core.Fusion /// /// Represents a EssentialsHuddleSpaceFusionSystemControllerBase /// - public class EssentialsHuddleSpaceFusionSystemControllerBase : Device, IOccupancyStatusProvider, IFusionHelpRequest + public class IEssentialsRoomFusionController : EssentialsDevice, IOccupancyStatusProvider, IFusionHelpRequest { - private readonly EssentialsHuddleSpaceRoomFusionRoomJoinMap JoinMap; + private EssentialsHuddleSpaceRoomFusionRoomJoinMap JoinMap; private const string RemoteOccupancyXml = "Local{0}"; - private readonly bool _guidFileExists; + private bool _guidFileExists; private readonly Dictionary _sourceToFeedbackSigs = new Dictionary(); @@ -52,7 +52,7 @@ namespace PepperDash.Essentials.Core.Fusion /// protected Dictionary FusionStaticAssets; private readonly long PushNotificationTimeout = 5000; - private readonly IEssentialsRoom Room; + private IEssentialsRoom Room; private readonly long SchedulePollInterval = 300000; private Event _currentMeeting; @@ -74,8 +74,12 @@ namespace PepperDash.Essentials.Core.Fusion private bool _helpRequestSent; + /// public StringFeedback HelpRequestResponseFeedback { get; private set; } + /// + public BoolFeedback HelpRequestSentFeedback { get; private set; } + #region System Info Sigs //StringSigData SystemName; @@ -109,14 +113,48 @@ namespace PepperDash.Essentials.Core.Fusion #endregion + /// + /// Constructor + /// + public IEssentialsRoomFusionController(IEssentialsRoomFusionControllerPropertiesConfig config) + : base("FusionRoomController") + { + AddPostActivationAction(() => + { + var room = DeviceManager.GetDeviceForKey(config.RoomKey); + + if (room == null) + { + Debug.LogMessage(LogEventLevel.Error, this, + "Error Creating Fusion Room Controller. No room found with key '{0}'", config.RoomKey); + return; + } + + ConstructorHelper(room, config.IpId, config.JoinMapKey); + + var guidFilePath = GetGuidFilePath(config.IpId); + + PostActivate(guidFilePath); + }); + } + /// /// /// /// /// /// - public EssentialsHuddleSpaceFusionSystemControllerBase(IEssentialsRoom room, uint ipId, string joinMapKey) + public IEssentialsRoomFusionController(IEssentialsRoom room, uint ipId, string joinMapKey) : base(room.Key + "-fusion") + { + ConstructorHelper(room, ipId, joinMapKey); + + var guidFilePath = GetGuidFilePath(ipId); + + AddPostActivationAction(() => PostActivate(guidFilePath)); + } + + private void ConstructorHelper(IEssentialsRoom room, uint ipId, string joinMapKey) { try { @@ -132,7 +170,7 @@ namespace PepperDash.Essentials.Core.Fusion JoinMap.SetCustomJoinData(customJoins); } } - + Room = room; _ipId = ipId; @@ -141,41 +179,7 @@ namespace PepperDash.Essentials.Core.Fusion _guiDs = new FusionRoomGuids(); - var mac = - CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_MAC_ADDRESS, 0); - var slot = Global.ControlSystem.ProgramNumber; - - var guidFilePath = Global.FilePathPrefix + - string.Format(@"{0}-FusionGuids-{1:X2}.json", InitialParametersClass.ProgramIDTag, _ipId); - - var oldGuidFilePath = Global.FilePathPrefix + - string.Format(@"{0}-FusionGuids.json", InitialParametersClass.ProgramIDTag); - - if (File.Exists(oldGuidFilePath)) - { - Debug.LogMessage(LogEventLevel.Information, this, "Migrating from old Fusion GUID file to new Fusion GUID File"); - - File.Copy(oldGuidFilePath, guidFilePath); - - File.Delete(oldGuidFilePath); - } - - _guidFileExists = File.Exists(guidFilePath); - - // Check if file exists - if (!_guidFileExists) - { - // Does not exist. Create GUIDs - _guiDs = new FusionRoomGuids(Room.Name, ipId, _guiDs.GenerateNewRoomGuid(slot, mac), - FusionStaticAssets); - } - else - { - // Exists. Read GUIDs - ReadGuidFile(guidFilePath); - } if (Room is IRoomOccupancy occupancyRoom) @@ -197,13 +201,53 @@ namespace PepperDash.Essentials.Core.Fusion HelpRequestResponseFeedback.LinkInputSig(FusionRoom.Help.InputSig); - AddPostActivationAction(() => PostActivate(guidFilePath)); } catch (Exception e) { Debug.LogMessage(LogEventLevel.Information, this, "Error Building Fusion System Controller: {0}", e); } } + + private string GetGuidFilePath(uint ipId) + { + var mac = + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_MAC_ADDRESS, 0); + + var slot = Global.ControlSystem.ProgramNumber; + + var guidFilePath = Global.FilePathPrefix + + string.Format(@"{0}-FusionGuids-{1:X2}.json", InitialParametersClass.ProgramIDTag, _ipId); + + var oldGuidFilePath = Global.FilePathPrefix + + string.Format(@"{0}-FusionGuids.json", InitialParametersClass.ProgramIDTag); + + if (File.Exists(oldGuidFilePath)) + { + Debug.LogMessage(LogEventLevel.Information, this, "Migrating from old Fusion GUID file to new Fusion GUID File"); + + File.Copy(oldGuidFilePath, guidFilePath); + + File.Delete(oldGuidFilePath); + } + + _guidFileExists = File.Exists(guidFilePath); + + // Check if file exists + if (!_guidFileExists) + { + // Does not exist. Create GUIDs + _guiDs = new FusionRoomGuids(Room.Name, ipId, _guiDs.GenerateNewRoomGuid(slot, mac), + FusionStaticAssets); + } + else + { + // Exists. Read GUIDs + ReadGuidFile(guidFilePath); + } + + return guidFilePath; + } private void PostActivate(string guidFilePath) { @@ -1721,11 +1765,8 @@ namespace PepperDash.Essentials.Core.Fusion } } - /// - /// Sends a help request to Fusion with room name and timestamp - /// - /// - public void SendHelpRequest(bool isHtml) + /// + public void SendHelpRequest(bool isHtml = false) { var now = DateTime.Now; @@ -1740,6 +1781,7 @@ namespace PepperDash.Essentials.Core.Fusion _helpRequestSent = true; } + /// public void CancelHelpRequest() { if (_helpRequestSent) @@ -1749,6 +1791,19 @@ namespace PepperDash.Essentials.Core.Fusion Debug.LogMessage(LogEventLevel.Information, this, "Help request cancelled in Fusion for room '{0}'", Room.Name); } } + + /// + public void ToggleHelpRequest(bool isHtml = false) + { + if (_helpRequestSent) + { + CancelHelpRequest(); + } + else + { + SendHelpRequest(isHtml); + } + } } /// diff --git a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerFactory.cs b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerFactory.cs new file mode 100644 index 00000000..4a598cc8 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerFactory.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Fusion; + +/// +/// Factory for creating IEssentialsRoomFusionController devices +/// +public class IEssentialsRoomFusionControllerFactory : EssentialsDeviceFactory +{ + /// + /// Constructor + /// + public IEssentialsRoomFusionControllerFactory() + { + TypeNames = new List() { "fusionRoom" }; + } + + /// + /// Builds the device + /// + /// + /// + public override EssentialsDevice BuildDevice(PepperDash.Essentials.Core.Config.DeviceConfig dc) + { + Debug.LogDebug("Factory Attempting to create new IEssentialsRoomFusionController Device"); + + + var properties = dc.Properties.ToObject(); + + return new IEssentialsRoomFusionController(properties); + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerPropertiesConfig.cs b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerPropertiesConfig.cs new file mode 100644 index 00000000..b7183b17 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerPropertiesConfig.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; + +/// +/// Config properties for an IEssentialsRoomFusionController device +/// +public class IEssentialsRoomFusionControllerPropertiesConfig +{ + /// + /// Gets or sets the IP ID of the Fusion Room Controller + /// + [JsonProperty("ipId")] + public uint IpId { get; set; } + + /// + /// Gets or sets the join map key + /// + [JsonProperty("joinMapKey")] + public string JoinMapKey { get; set; } + + /// + /// Gets or sets the room key associated with this Fusion Room Controller + /// + [JsonProperty("roomKey")] + public string RoomKey { get; set; } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Fusion/IFusionHelpRequest.cs b/src/PepperDash.Essentials.Core/Fusion/IFusionHelpRequest.cs index 30033779..f17f4c9a 100644 --- a/src/PepperDash.Essentials.Core/Fusion/IFusionHelpRequest.cs +++ b/src/PepperDash.Essentials.Core/Fusion/IFusionHelpRequest.cs @@ -12,14 +12,29 @@ namespace PepperDash.Essentials.Core.Fusion public interface IFusionHelpRequest { /// - /// Gets the HelpRequstResponseFeedback + /// Feedback containing the response to a help request /// StringFeedback HelpRequestResponseFeedback { get; } + /// + /// Indicates whether a help request has been sent + /// + BoolFeedback HelpRequestSentFeedback { get; } + /// /// Sends a help request /// /// void SendHelpRequest(bool isHtml); + + /// + /// Clears the current help request status + /// + void CancelHelpRequest(); + + /// + /// Toggles between sending and cancelling a help request + /// + void ToggleHelpRequest(bool isHtml); } } diff --git a/src/PepperDash.Essentials.Core/Room/Behaviours/RoomOnToDefaultSourceWhenOccupied.cs b/src/PepperDash.Essentials.Core/Room/Behaviours/RoomOnToDefaultSourceWhenOccupied.cs index 49b51f67..d6f3c503 100644 --- a/src/PepperDash.Essentials.Core/Room/Behaviours/RoomOnToDefaultSourceWhenOccupied.cs +++ b/src/PepperDash.Essentials.Core/Room/Behaviours/RoomOnToDefaultSourceWhenOccupied.cs @@ -49,7 +49,7 @@ namespace PepperDash.Essentials.Core /// public IRoomOccupancy Room { get; private set; } - private Fusion.EssentialsHuddleSpaceFusionSystemControllerBase FusionRoom; + private Fusion.IEssentialsRoomFusionController FusionRoom; public RoomOnToDefaultSourceWhenOccupied(DeviceConfig config) : base (config) @@ -74,7 +74,7 @@ namespace PepperDash.Essentials.Core var fusionRoomKey = PropertiesConfig.RoomKey + "-fusion"; - FusionRoom = DeviceManager.GetDeviceForKey(fusionRoomKey) as Core.Fusion.EssentialsHuddleSpaceFusionSystemControllerBase; + FusionRoom = DeviceManager.GetDeviceForKey(fusionRoomKey) as Core.Fusion.IEssentialsRoomFusionController; if (FusionRoom == null) Debug.LogMessage(LogEventLevel.Debug, this, "Unable to get Fusion Room from Device Manager with key: {0}", fusionRoomKey); diff --git a/src/PepperDash.Essentials.Core/Room/EssentialsRoomBase.cs b/src/PepperDash.Essentials.Core/Room/EssentialsRoomBase.cs index dfb8b5a1..38e4456e 100644 --- a/src/PepperDash.Essentials.Core/Room/EssentialsRoomBase.cs +++ b/src/PepperDash.Essentials.Core/Room/EssentialsRoomBase.cs @@ -408,7 +408,7 @@ namespace PepperDash.Essentials.Core Debug.LogMessage(LogEventLevel.Information, this, "Timeout Minutes from Config is: {0}", timeoutMinutes); // If status provider is fusion, set flag to remote - if (statusProvider is Core.Fusion.EssentialsHuddleSpaceFusionSystemControllerBase) + if (statusProvider is Core.Fusion.IEssentialsRoomFusionController) OccupancyStatusProviderIsRemote = true; if(timeoutMinutes > 0) From da0f28a10c65c08bb7f5aedbfb128380441e999b Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 29 Oct 2025 16:47:18 -0600 Subject: [PATCH 31/55] feat: Enhance IEssentialsRoomFusionController with additional properties and logging --- ...entialsHuddleSpaceRoomFusionRoomJoinMap.cs | 90 ++++++- .../Fusion/IEssentialsRoomFusionController.cs | 241 +++++++++++------- .../IEssentialsRoomFusionControllerFactory.cs | 2 +- ...alsRoomFusionControllerPropertiesConfig.cs | 36 ++- .../Fusion/IFusionHelpRequest.cs | 4 +- 5 files changed, 277 insertions(+), 96 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceRoomFusionRoomJoinMap.cs b/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceRoomFusionRoomJoinMap.cs index d6a7bffa..ba638623 100644 --- a/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceRoomFusionRoomJoinMap.cs +++ b/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceRoomFusionRoomJoinMap.cs @@ -16,121 +16,201 @@ namespace PepperDash.Essentials.Core.Fusion { // Processor Attributes + /// + /// Processor IP 1 + /// [JoinName("ProcessorIp1")] public JoinDataComplete ProcessorIp1 = new JoinDataComplete(new JoinData { JoinNumber = 50, JoinSpan = 1, AttributeName = "Info - Processor - IP 1" }, new JoinMetadata { Description = "Info - Processor - IP 1", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor IP 2 + /// [JoinName("ProcessorIp2")] public JoinDataComplete ProcessorIp2 = new JoinDataComplete(new JoinData { JoinNumber = 51, JoinSpan = 1, AttributeName = "Info - Processor - IP 2" }, new JoinMetadata { Description = "Info - Processor - IP 2", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor Gateway + /// [JoinName("ProcessorGateway")] public JoinDataComplete ProcessorGateway = new JoinDataComplete(new JoinData { JoinNumber = 52, JoinSpan = 1, AttributeName = "Info - Processor - Gateway" }, new JoinMetadata { Description = "Info - Processor - Gateway", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor Hostname + /// [JoinName("ProcessorHostname")] public JoinDataComplete ProcessorHostname = new JoinDataComplete(new JoinData { JoinNumber = 53, JoinSpan = 1, AttributeName = "Info - Processor - Hostname" }, new JoinMetadata { Description = "Info - Processor - Hostname", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor Domain + /// [JoinName("ProcessorDomain")] public JoinDataComplete ProcessorDomain = new JoinDataComplete(new JoinData { JoinNumber = 54, JoinSpan = 1, AttributeName = "Info - Processor - Domain" }, new JoinMetadata { Description = "Info - Processor - Domain", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor DNS 1 + /// [JoinName("ProcessorDns1")] public JoinDataComplete ProcessorDns1 = new JoinDataComplete(new JoinData { JoinNumber = 55, JoinSpan = 1, AttributeName = "Info - Processor - DNS 1" }, new JoinMetadata { Description = "Info - Processor - DNS 1", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor DNS 2 + /// [JoinName("ProcessorDns2")] public JoinDataComplete ProcessorDns2 = new JoinDataComplete(new JoinData { JoinNumber = 56, JoinSpan = 1, AttributeName = "Info - Processor - DNS 2" }, new JoinMetadata { Description = "Info - Processor - DNS 2", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor MAC 1 + /// [JoinName("ProcessorMac1")] public JoinDataComplete ProcessorMac1 = new JoinDataComplete(new JoinData { JoinNumber = 57, JoinSpan = 1, AttributeName = "Info - Processor - MAC 1" }, new JoinMetadata { Description = "Info - Processor - MAC 1", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor MAC 2 + /// [JoinName("ProcessorMac2")] public JoinDataComplete ProcessorMac2 = new JoinDataComplete(new JoinData { JoinNumber = 58, JoinSpan = 1, AttributeName = "Info - Processor - MAC 2" }, new JoinMetadata { Description = "Info - Processor - MAC 2", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor Net Mask 1 + /// [JoinName("ProcessorNetMask1")] public JoinDataComplete ProcessorNetMask1 = new JoinDataComplete(new JoinData { JoinNumber = 59, JoinSpan = 1, AttributeName = "Info - Processor - Net Mask 1" }, new JoinMetadata { Description = "Info - Processor - Net Mask 1", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor Net Mask 2 + /// [JoinName("ProcessorNetMask2")] public JoinDataComplete ProcessorNetMask2 = new JoinDataComplete(new JoinData { JoinNumber = 60, JoinSpan = 1, AttributeName = "Info - Processor - Net Mask 2" }, new JoinMetadata { Description = "Info - Processor - Net Mask 2", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor Firmware + /// [JoinName("ProcessorFirmware")] public JoinDataComplete ProcessorFirmware = new JoinDataComplete(new JoinData { JoinNumber = 61, JoinSpan = 1, AttributeName = "Info - Processor - Firmware" }, new JoinMetadata { Description = "Info - Processor - Firmware", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Program Name Start + /// [JoinName("ProgramNameStart")] public JoinDataComplete ProgramNameStart = new JoinDataComplete(new JoinData { JoinNumber = 62, JoinSpan = 10, AttributeName = "Info - Processor - Program" }, new JoinMetadata { Description = "Info - Processor - Program", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// Processor Reboot + /// [JoinName("ProcessorReboot")] public JoinDataComplete ProcessorReboot = new JoinDataComplete(new JoinData { JoinNumber = 74, JoinSpan = 1, AttributeName = "Processor - Reboot" }, new JoinMetadata { Description = "Processor - Reboot", JoinCapabilities = eJoinCapabilities.FromFusion, JoinType = eJoinType.Digital }); // Volume Controls + /// + /// Volume Fader 1 + /// [JoinName("VolumeFader1")] public JoinDataComplete VolumeFader1 = new JoinDataComplete(new JoinData { JoinNumber = 50, JoinSpan = 1, AttributeName = "Volume - Fader01" }, new JoinMetadata { Description = "Volume - Fader01", JoinCapabilities = eJoinCapabilities.ToFromFusion, JoinType = eJoinType.Analog }); // Codec Info + /// + /// VC Codec In Call + /// [JoinName("VcCodecInCall")] public JoinDataComplete VcCodecInCall = new JoinDataComplete(new JoinData { JoinNumber = 69, JoinSpan = 1, AttributeName = "Conf - VC 1 In Call" }, new JoinMetadata { Description = "Conf - VC 1 In Call", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Digital }); + /// + /// VC Codec Online + /// [JoinName("VcCodecOnline")] public JoinDataComplete VcCodecOnline = new JoinDataComplete(new JoinData { JoinNumber = 122, JoinSpan = 1, AttributeName = "Online - VC 1" }, new JoinMetadata { Description = "Online - VC 1", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Digital }); + /// + /// VC Codec IP Address + /// [JoinName("VcCodecIpAddress")] public JoinDataComplete VcCodecIpAddress = new JoinDataComplete(new JoinData { JoinNumber = 121, JoinSpan = 1, AttributeName = "IP Address - VC" }, new JoinMetadata { Description = "IP Address - VC", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); + /// + /// VC Codec IP Port + /// [JoinName("VcCodecIpPort")] public JoinDataComplete VcCodecIpPort = new JoinDataComplete(new JoinData { JoinNumber = 150, JoinSpan = 1, AttributeName = "IP Port - VC" }, new JoinMetadata { Description = "IP Port - VC", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); // Source Attributes + /// + /// Display 1 Current Source Name + /// [JoinName("Display1CurrentSourceName")] public JoinDataComplete Display1CurrentSourceName = new JoinDataComplete(new JoinData { JoinNumber = 84, JoinSpan = 1, AttributeName = "Display 1 - Current Source" }, new JoinMetadata { Description = "Display 1 - Current Source", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Serial }); // Device Online Status + /// + /// Touchpanel Online Start + /// [JoinName("TouchpanelOnlineStart")] public JoinDataComplete TouchpanelOnlineStart = new JoinDataComplete(new JoinData { JoinNumber = 150, JoinSpan = 10, AttributeName = "Online - Touch Panel" }, new JoinMetadata { Description = "Online - Touch Panel", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Digital }); + /// + /// Xpanel Online Start + /// [JoinName("XpanelOnlineStart")] public JoinDataComplete XpanelOnlineStart = new JoinDataComplete(new JoinData { JoinNumber = 160, JoinSpan = 5, AttributeName = "Online - XPanel" }, new JoinMetadata { Description = "Online - XPanel", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Digital }); + /// + /// Display Online Start + /// [JoinName("DisplayOnlineStart")] public JoinDataComplete DisplayOnlineStart = new JoinDataComplete(new JoinData { JoinNumber = 170, JoinSpan = 10, AttributeName = "Online - Display" }, new JoinMetadata { Description = "Online - Display", JoinCapabilities = eJoinCapabilities.ToFusion, JoinType = eJoinType.Digital }); + /// + /// Display 1 Laptop Source Start + /// [JoinName("Display1LaptopSourceStart")] - public JoinDataComplete Display1LaptopSourceStart = new JoinDataComplete(new JoinData { JoinNumber = 166, JoinSpan = 5, AttributeName = "Display 1 - Source Laptop" }, + public JoinDataComplete Display1LaptopSourceStart = new JoinDataComplete(new JoinData { JoinNumber = 165, JoinSpan = 5, AttributeName = "Display 1 - Source Laptop" }, new JoinMetadata { Description = "Display 1 - Source Laptop", JoinCapabilities = eJoinCapabilities.ToFromFusion, JoinType = eJoinType.Digital }); + /// + /// Display 1 Disc Player Source Start + /// [JoinName("Display1DiscPlayerSourceStart")] - public JoinDataComplete Display1DiscPlayerSourceStart = new JoinDataComplete(new JoinData { JoinNumber = 181, JoinSpan = 5, AttributeName = "Display 1 - Source Disc Player" }, + public JoinDataComplete Display1DiscPlayerSourceStart = new JoinDataComplete(new JoinData { JoinNumber = 180, JoinSpan = 5, AttributeName = "Display 1 - Source Disc Player" }, new JoinMetadata { Description = "Display 1 - Source Disc Player", JoinCapabilities = eJoinCapabilities.ToFromFusion, JoinType = eJoinType.Digital }); + /// + /// Display 1 Set Top Box Source Start + /// [JoinName("Display1SetTopBoxSourceStart")] - public JoinDataComplete Display1SetTopBoxSourceStart = new JoinDataComplete(new JoinData { JoinNumber = 188, JoinSpan = 5, AttributeName = "Display 1 - Source TV" }, + public JoinDataComplete Display1SetTopBoxSourceStart = new JoinDataComplete(new JoinData { JoinNumber = 185, JoinSpan = 5, AttributeName = "Display 1 - Source TV" }, new JoinMetadata { Description = "Display 1 - Source TV", JoinCapabilities = eJoinCapabilities.ToFromFusion, JoinType = eJoinType.Digital }); // Display 1 + /// + /// Display 1 Start + /// [JoinName("Display1Start")] - public JoinDataComplete Display1Start = new JoinDataComplete(new JoinData { JoinNumber = 158, JoinSpan = 1 }, + public JoinDataComplete Display1Start = new JoinDataComplete(new JoinData { JoinNumber = 190, JoinSpan = 1 }, new JoinMetadata { Description = "Display 1 Start", JoinCapabilities = eJoinCapabilities.ToFromFusion, JoinType = eJoinType.Digital }); - /// /// Constructor to use when instantiating this Join Map without inheriting from it /// diff --git a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs index fa7c36a4..ee439b12 100644 --- a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs +++ b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs @@ -6,6 +6,7 @@ using Crestron.SimplSharpPro; using Crestron.SimplSharpPro.Fusion; using Newtonsoft.Json; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.DeviceTypeInterfaces; using Serilog.Events; @@ -21,6 +22,8 @@ namespace PepperDash.Essentials.Core.Fusion /// public class IEssentialsRoomFusionController : EssentialsDevice, IOccupancyStatusProvider, IFusionHelpRequest { + private IEssentialsRoomFusionControllerPropertiesConfig _config; + private EssentialsHuddleSpaceRoomFusionRoomJoinMap JoinMap; private const string RemoteOccupancyXml = "Local{0}"; @@ -60,8 +63,7 @@ namespace PepperDash.Essentials.Core.Fusion private CTimer _dailyTimeRequestTimer; private StatusMonitorCollection _errorMessageRollUp; - private FusionRoomGuids _guiDs; - private uint _ipId; + private FusionRoomGuids _guids; private bool _isRegisteredForSchedulePushNotifications; private Event _nextMeeting; @@ -116,25 +118,25 @@ namespace PepperDash.Essentials.Core.Fusion /// /// Constructor /// - public IEssentialsRoomFusionController(IEssentialsRoomFusionControllerPropertiesConfig config) - : base("FusionRoomController") + public IEssentialsRoomFusionController(string key, string name, IEssentialsRoomFusionControllerPropertiesConfig config) + : base(key, name) { + _config = config; + AddPostActivationAction(() => { - var room = DeviceManager.GetDeviceForKey(config.RoomKey); + var room = DeviceManager.GetDeviceForKey(_config.RoomKey); if (room == null) { - Debug.LogMessage(LogEventLevel.Error, this, - "Error Creating Fusion Room Controller. No room found with key '{0}'", config.RoomKey); + this.LogError("Error Creating Fusion Room Controller. No room found with key '{0}'", _config.RoomKey); return; } - ConstructorHelper(room, config.IpId, config.JoinMapKey); + this.LogInformation("Creating Fusion Room Controller for room '{0}' at IPID: {1:X2}", room.Key, _config.IpIdInt); - var guidFilePath = GetGuidFilePath(config.IpId); + ConstructorHelper(room, _config.IpIdInt, _config.JoinMapKey); - PostActivate(guidFilePath); }); } @@ -144,26 +146,57 @@ namespace PepperDash.Essentials.Core.Fusion /// /// /// - public IEssentialsRoomFusionController(IEssentialsRoom room, uint ipId, string joinMapKey) + public IEssentialsRoomFusionController(IEssentialsRoom room, string ipId, string joinMapKey) : base(room.Key + "-fusion") { - ConstructorHelper(room, ipId, joinMapKey); + _config = new IEssentialsRoomFusionControllerPropertiesConfig() + { + IpId = ipId, + RoomKey = room.Key, + JoinMapKey = joinMapKey + }; - var guidFilePath = GetGuidFilePath(ipId); - - AddPostActivationAction(() => PostActivate(guidFilePath)); + ConstructorHelper(room, _config.IpIdInt, joinMapKey); } private void ConstructorHelper(IEssentialsRoom room, uint ipId, string joinMapKey) { try { + this.LogDebug("ConstructorHelper called for Fusion Room Controller for room '{0}' with IPID {1:X2}", room.Key, ipId); + + this.LogDebug("JoinMap Key: {0}", joinMapKey); + JoinMap = new EssentialsHuddleSpaceRoomFusionRoomJoinMap(1); - CrestronConsole.AddNewConsoleCommand((o) => JoinMap.PrintJoinMapInfo(), string.Format("ptjnmp-{0}", Key), "Prints Attribute Join Map", ConsoleAccessLevelEnum.AccessOperator); + this.LogDebug("JoinMap created"); + + CrestronConsole.AddNewConsoleCommand((o) => + { + if (o is string deviceKey) + { + if (string.IsNullOrEmpty(deviceKey) || deviceKey == "?") + { + CrestronConsole.ConsoleCommandResponse("Please provide a device key for a Fusion Room instance"); + return; + } + else if (deviceKey != this.Key) + { + return; + } + } + else + { + CrestronConsole.ConsoleCommandResponse("Invalid parameter. Please provide a device key for a Fusion Room instance"); + return; + } + + JoinMap.PrintJoinMapInfo(); + }, "printfusionjoinmap", "Prints Attribute Join Map", ConsoleAccessLevelEnum.AccessOperator); if (!string.IsNullOrEmpty(joinMapKey)) { + // this.LogDebug("Attempting to get custom join map for key: {0}", joinMapKey); var customJoins = JoinMapHelper.TryGetJoinMapAdvancedForDevice(joinMapKey); if (customJoins != null) { @@ -173,17 +206,19 @@ namespace PepperDash.Essentials.Core.Fusion Room = room; - _ipId = ipId; + this.LogDebug("Room found: {0}", Room.Key); FusionStaticAssets = new Dictionary(); - _guiDs = new FusionRoomGuids(); - + this.LogDebug("FusionStaticAssets dictionary created"); + _guids = new FusionRoomGuids(); + this.LogDebug("FusionRoomGuids created"); if (Room is IRoomOccupancy occupancyRoom) { + Debug.LogDebug(this, "Room '{0}' supports IRoomOccupancy", Room.Key); if (occupancyRoom.RoomOccupancy != null) { if (occupancyRoom.OccupancyStatusProviderIsRemote) @@ -197,9 +232,7 @@ namespace PepperDash.Essentials.Core.Fusion } } - HelpRequestResponseFeedback = new StringFeedback("HelpRequestResponse", () => FusionRoom.Help.InputSig.StringValue); - HelpRequestResponseFeedback.LinkInputSig(FusionRoom.Help.InputSig); - + this.LogDebug("Occupancy setup complete"); } catch (Exception e) @@ -207,37 +240,37 @@ namespace PepperDash.Essentials.Core.Fusion Debug.LogMessage(LogEventLevel.Information, this, "Error Building Fusion System Controller: {0}", e); } } - + private string GetGuidFilePath(uint ipId) { - var mac = - CrestronEthernetHelper.GetEthernetParameter( - CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_MAC_ADDRESS, 0); + var mac = + CrestronEthernetHelper.GetEthernetParameter( + CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_MAC_ADDRESS, 0); - var slot = Global.ControlSystem.ProgramNumber; + var slot = Global.ControlSystem.ProgramNumber; - var guidFilePath = Global.FilePathPrefix + - string.Format(@"{0}-FusionGuids-{1:X2}.json", InitialParametersClass.ProgramIDTag, _ipId); + var guidFilePath = Global.FilePathPrefix + + string.Format(@"{0}-FusionGuids-{1:X2}.json", InitialParametersClass.ProgramIDTag, _config.IpIdInt); - var oldGuidFilePath = Global.FilePathPrefix + - string.Format(@"{0}-FusionGuids.json", InitialParametersClass.ProgramIDTag); + var oldGuidFilePath = Global.FilePathPrefix + + string.Format(@"{0}-FusionGuids.json", InitialParametersClass.ProgramIDTag); - if (File.Exists(oldGuidFilePath)) - { - Debug.LogMessage(LogEventLevel.Information, this, "Migrating from old Fusion GUID file to new Fusion GUID File"); + if (File.Exists(oldGuidFilePath)) + { + Debug.LogMessage(LogEventLevel.Information, this, "Migrating from old Fusion GUID file to new Fusion GUID File"); - File.Copy(oldGuidFilePath, guidFilePath); + File.Copy(oldGuidFilePath, guidFilePath); - File.Delete(oldGuidFilePath); - } + File.Delete(oldGuidFilePath); + } - _guidFileExists = File.Exists(guidFilePath); + _guidFileExists = File.Exists(guidFilePath); // Check if file exists if (!_guidFileExists) { // Does not exist. Create GUIDs - _guiDs = new FusionRoomGuids(Room.Name, ipId, _guiDs.GenerateNewRoomGuid(slot, mac), + _guids = new FusionRoomGuids(Room.Name, ipId, _guids.GenerateNewRoomGuid(slot, mac), FusionStaticAssets); } else @@ -245,13 +278,17 @@ namespace PepperDash.Essentials.Core.Fusion // Exists. Read GUIDs ReadGuidFile(guidFilePath); } - + return guidFilePath; } - private void PostActivate(string guidFilePath) + /// + public override void Initialize() { - CreateSymbolAndBasicSigs(_ipId); + + GenerateGuidFile(GetGuidFilePath(_config.IpIdInt)); + + CreateSymbolAndBasicSigs(_config.IpIdInt); SetUpSources(); SetUpCommunitcationMonitors(); SetUpDisplay(); @@ -260,7 +297,11 @@ namespace PepperDash.Essentials.Core.Fusion FusionRVI.GenerateFileForAllFusionDevices(); - GenerateGuidFile(guidFilePath); + HelpRequestResponseFeedback = new StringFeedback("HelpRequestResponse", () => FusionRoom.Help.InputSig.StringValue); + HelpRequestResponseFeedback.LinkInputSig(FusionRoom.Help.InputSig); + + HelpRequestSentFeedback = new BoolFeedback("HelpRequestSent", () => _helpRequestSent); + } /// @@ -268,7 +309,7 @@ namespace PepperDash.Essentials.Core.Fusion /// protected string RoomGuid { - get { return _guiDs.RoomGuid; } + get { return _guids.RoomGuid; } } /// @@ -339,11 +380,11 @@ namespace PepperDash.Essentials.Core.Fusion Debug.LogMessage(LogEventLevel.Debug, this, "Writing GUIDs to file"); - _guiDs = FusionOccSensor == null - ? new FusionRoomGuids(Room.Name, _ipId, RoomGuid, FusionStaticAssets) - : new FusionRoomGuids(Room.Name, _ipId, RoomGuid, FusionStaticAssets, FusionOccSensor); + _guids = FusionOccSensor == null + ? new FusionRoomGuids(Room.Name, _config.IpIdInt, RoomGuid, FusionStaticAssets) + : new FusionRoomGuids(Room.Name, _config.IpIdInt, RoomGuid, FusionStaticAssets, FusionOccSensor); - var json = JsonConvert.SerializeObject(_guiDs, Newtonsoft.Json.Formatting.Indented); + var json = JsonConvert.SerializeObject(_guids, Newtonsoft.Json.Formatting.Indented); using (var sw = new StreamWriter(filePath)) { @@ -393,17 +434,17 @@ namespace PepperDash.Essentials.Core.Fusion { var json = File.ReadToEnd(filePath, Encoding.ASCII); - _guiDs = JsonConvert.DeserializeObject(json); + _guids = JsonConvert.DeserializeObject(json); - _ipId = _guiDs.IpId; + // _config.IpId = _guids.IpId; - FusionStaticAssets = _guiDs.StaticAssets; + FusionStaticAssets = _guids.StaticAssets; } Debug.LogMessage(LogEventLevel.Information, this, "Fusion Guids successfully read from file: {0}", filePath); - Debug.LogMessage(LogEventLevel.Debug, this, "\r\n********************\r\n\tRoom Name: {0}\r\n\tIPID: {1:X}\r\n\tRoomGuid: {2}\r\n*******************", Room.Name, _ipId, RoomGuid); + Debug.LogMessage(LogEventLevel.Debug, this, "\r\n********************\r\n\tRoom Name: {0}\r\n\tIPID: {1:X}\r\n\tRoomGuid: {2}\r\n*******************", Room.Name, _config.IpIdInt, RoomGuid); foreach (var item in FusionStaticAssets) { @@ -573,7 +614,7 @@ namespace PepperDash.Essentials.Core.Fusion // Interface 1 if (InitialParametersClass.NumberOfEthernetInterfaces > 1) - // Only get these values if the processor has more than 1 NIC + // Only get these values if the processor has more than 1 NIC { _ip2.InputSig.StringValue = CrestronEthernetHelper.GetEthernetParameter( @@ -600,7 +641,7 @@ namespace PepperDash.Essentials.Core.Fusion { var join = JoinMap.ProgramNameStart.JoinNumber + i; var progNum = i + 1; - _program[i] = FusionRoom.CreateOffsetStringSig((uint) join, + _program[i] = FusionRoom.CreateOffsetStringSig((uint)join, string.Format("{0} {1}", JoinMap.ProgramNameStart.AttributeName, progNum), eSigIoMask.InputSigOnly); } } @@ -637,7 +678,7 @@ namespace PepperDash.Essentials.Core.Fusion { if (args.DeviceOnLine) { - CrestronInvoke.BeginInvoke( (o) => + CrestronInvoke.BeginInvoke((o) => { CrestronEnvironment.Sleep(200); @@ -785,7 +826,7 @@ namespace PepperDash.Essentials.Core.Fusion var extendTime = _currentMeeting.dtEnd - DateTime.Now; var extendMinutesRaw = extendTime.TotalMinutes; - extendMinutes += (int) Math.Round(extendMinutesRaw); + extendMinutes += (int)Math.Round(extendMinutesRaw); } @@ -893,11 +934,11 @@ namespace PepperDash.Essentials.Core.Fusion var parameters = actionResponse["Parameters"]; foreach (var isRegistered in from XmlElement parameter in parameters - where parameter.HasAttributes - select parameter.Attributes + where parameter.HasAttributes + select parameter.Attributes into attributes - where attributes["ID"].Value == "Registered" - select Int32.Parse(attributes["Value"].Value)) + where attributes["ID"].Value == "Registered" + select Int32.Parse(attributes["Value"].Value)) { switch (isRegistered) { @@ -954,9 +995,9 @@ namespace PepperDash.Essentials.Core.Fusion Debug.LogMessage(LogEventLevel.Debug, this, "DateTime from Fusion Server: {0}", currentTime); // Parse time and date from response and insert values - CrestronEnvironment.SetTimeAndDate((ushort) currentTime.Hour, (ushort) currentTime.Minute, - (ushort) currentTime.Second, (ushort) currentTime.Month, (ushort) currentTime.Day, - (ushort) currentTime.Year); + CrestronEnvironment.SetTimeAndDate((ushort)currentTime.Hour, (ushort)currentTime.Minute, + (ushort)currentTime.Second, (ushort)currentTime.Month, (ushort)currentTime.Day, + (ushort)currentTime.Year); Debug.LogMessage(LogEventLevel.Debug, this, "Processor time set to {0}", CrestronEnvironment.GetLocalTime()); } @@ -1186,10 +1227,10 @@ namespace PepperDash.Essentials.Core.Fusion // NEW PROCESS: // Make these lists and insert the fusion attributes by iterating these var setTopBoxes = dict.Where(d => d.Value.SourceDevice is ISetTopBoxControls); - uint i = 1; + uint i = 0; foreach (var kvp in setTopBoxes) { - TryAddRouteActionSigs(JoinMap.Display1SetTopBoxSourceStart.AttributeName + " " + i, JoinMap.Display1SetTopBoxSourceStart.JoinNumber + i, kvp.Key, kvp.Value.SourceDevice); + TryAddRouteActionSigs(JoinMap.Display1SetTopBoxSourceStart.AttributeName + " " + (i + 1), JoinMap.Display1SetTopBoxSourceStart.JoinNumber + i, kvp.Key, kvp.Value.SourceDevice); i++; if (i > JoinMap.Display1SetTopBoxSourceStart.JoinSpan) // We only have five spots { @@ -1198,10 +1239,10 @@ namespace PepperDash.Essentials.Core.Fusion } var discPlayers = dict.Where(d => d.Value.SourceDevice is IDiscPlayerControls); - i = 1; + i = 0; foreach (var kvp in discPlayers) { - TryAddRouteActionSigs(JoinMap.Display1DiscPlayerSourceStart.AttributeName + " " + i, JoinMap.Display1DiscPlayerSourceStart.JoinNumber + i, kvp.Key, kvp.Value.SourceDevice); + TryAddRouteActionSigs(JoinMap.Display1DiscPlayerSourceStart.AttributeName + " " + (i + 1), JoinMap.Display1DiscPlayerSourceStart.JoinNumber + i, kvp.Key, kvp.Value.SourceDevice); i++; if (i > JoinMap.Display1DiscPlayerSourceStart.JoinSpan) // We only have five spots { @@ -1210,10 +1251,10 @@ namespace PepperDash.Essentials.Core.Fusion } var laptops = dict.Where(d => d.Value.SourceDevice is IRoutingSource); - i = 1; + i = 0; foreach (var kvp in laptops) { - TryAddRouteActionSigs(JoinMap.Display1LaptopSourceStart.AttributeName + " " + i, JoinMap.Display1LaptopSourceStart.JoinNumber + i, kvp.Key, kvp.Value.SourceDevice); + TryAddRouteActionSigs(JoinMap.Display1LaptopSourceStart.AttributeName + " " + (i + 1), JoinMap.Display1LaptopSourceStart.JoinNumber + i, kvp.Key, kvp.Value.SourceDevice); i++; if (i > JoinMap.Display1LaptopSourceStart.JoinSpan) // We only have ten spots??? { @@ -1223,7 +1264,7 @@ namespace PepperDash.Essentials.Core.Fusion foreach (var usageDevice in dict.Select(kvp => kvp.Value.SourceDevice).OfType()) { - usageDevice.UsageTracker = new UsageTracking(usageDevice as Device) {UsageIsTracked = true}; + usageDevice.UsageTracker = new UsageTracking(usageDevice as Device) { UsageIsTracked = true }; usageDevice.UsageTracker.DeviceUsageEnded += UsageTracker_DeviceUsageEnded; } } @@ -1278,14 +1319,22 @@ namespace PepperDash.Essentials.Core.Fusion /// protected void TryAddRouteActionSigs(string attrName, uint attrNum, string routeKey, Device pSrc) { - Debug.LogMessage(LogEventLevel.Verbose, this, "Creating attribute '{0}' with join {1} for source {2}", + this.LogVerbose("Creating attribute '{0}' with join {1} for source {2}", attrName, attrNum, pSrc.Key); try { var sigD = FusionRoom.CreateOffsetBoolSig(attrNum, attrName, eSigIoMask.InputOutputSig); // Need feedback when this source is selected // Event handler, added below, will compare source changes with this sig dict - _sourceToFeedbackSigs.Add(pSrc, sigD.InputSig); + if (!_sourceToFeedbackSigs.ContainsKey(pSrc)) + { + _sourceToFeedbackSigs.Add(pSrc, sigD.InputSig); + } + else + { + this.LogWarning("Source '{0}' already has a feedback sig mapped. Overwriting.", pSrc.Key); + _sourceToFeedbackSigs[pSrc] = sigD.InputSig; + } // And respond to selection in Fusion sigD.OutputSig.SetSigFalseAction(() => @@ -1298,7 +1347,7 @@ namespace PepperDash.Essentials.Core.Fusion } catch (Exception) { - Debug.LogMessage(LogEventLevel.Verbose, this, "Error creating Fusion signal {0} {1} for device '{2}'. THIS NEEDS REWORKING", + this.LogVerbose("Error creating Fusion signal {0} {1} for device '{2}'. THIS NEEDS REWORKING", attrNum, attrName, pSrc.Key); } } @@ -1390,6 +1439,8 @@ namespace PepperDash.Essentials.Core.Fusion if (attrName != null) { + this.LogDebug("Linking communication monitor for device '{0}' to Fusion attribute '{1}' at join {2}", + dev.Key, attrName, attrNum); // Link comm status to sig and update var sigD = FusionRoom.CreateOffsetBoolSig(attrNum, attrName, eSigIoMask.InputSigOnly); var smd = dev as ICommunicationMonitor; @@ -1416,7 +1467,7 @@ namespace PepperDash.Essentials.Core.Fusion foreach (var display in displays.Cast()) { - display.UsageTracker = new UsageTracking(display as Device) {UsageIsTracked = true}; + display.UsageTracker = new UsageTracking(display as Device) { UsageIsTracked = true }; display.UsageTracker.DeviceUsageEnded += UsageTracker_DeviceUsageEnded; } @@ -1529,7 +1580,7 @@ namespace PepperDash.Essentials.Core.Fusion // Power on - var defaultDisplayPowerOn = FusionRoom.CreateOffsetBoolSig((uint) joinOffset, displayName + "Power On", + var defaultDisplayPowerOn = FusionRoom.CreateOffsetBoolSig((uint)joinOffset, displayName + "Power On", eSigIoMask.InputOutputSig); defaultDisplayPowerOn.OutputSig.UserObject = new Action(b => { @@ -1540,7 +1591,7 @@ namespace PepperDash.Essentials.Core.Fusion }); // Power Off - var defaultDisplayPowerOff = FusionRoom.CreateOffsetBoolSig((uint) joinOffset + 1, displayName + "Power Off", + var defaultDisplayPowerOff = FusionRoom.CreateOffsetBoolSig((uint)joinOffset + 1, displayName + "Power Off", eSigIoMask.InputOutputSig); defaultDisplayPowerOn.OutputSig.UserObject = new Action(b => { @@ -1558,7 +1609,7 @@ namespace PepperDash.Essentials.Core.Fusion } // Current Source - var defaultDisplaySourceNone = FusionRoom.CreateOffsetBoolSig((uint) joinOffset + 8, + var defaultDisplaySourceNone = FusionRoom.CreateOffsetBoolSig((uint)joinOffset + 8, displayName + "Source None", eSigIoMask.InputOutputSig); defaultDisplaySourceNone.OutputSig.UserObject = new Action(b => { @@ -1626,7 +1677,7 @@ namespace PepperDash.Essentials.Core.Fusion //if (Room.OccupancyObj != null) //{ - var tempOccAsset = _guiDs.OccupancyAsset; + var tempOccAsset = _guids.OccupancyAsset; if (tempOccAsset == null) { @@ -1651,7 +1702,7 @@ namespace PepperDash.Essentials.Core.Fusion occRoom.RoomOccupancy.RoomIsOccupiedFeedback.OutputChange += RoomIsOccupiedFeedback_OutputChange; } RoomOccupancyRemoteStringFeedback = new StringFeedback(() => _roomOccupancyRemoteString); - + RoomOccupancyRemoteStringFeedback.LinkInputSig(occSensorAsset.RoomOccupancyInfo.InputSig); //} @@ -1766,13 +1817,21 @@ namespace PepperDash.Essentials.Core.Fusion } /// - public void SendHelpRequest(bool isHtml = false) + public void SendHelpRequest() { + var now = DateTime.Now; - var breakString = !isHtml ? "\r\n" : "
"; + var breakString = _config.UseHtmlFormatForHelpRequests ? "
" : "\r\n"; - var requestString = $"HR00: {breakString} Assistance has been requested from room {Room.Name}{breakString}on {now.ToLongDateString()} at {now.ToLongTimeString()}"; + var date = now.ToString("MMMM dd, yyyy"); + var time = now.ToString("hh:mm tt"); + if (_config.Use24HourTimeFormat) + { + time = now.ToString("HH:mm"); + } + + var requestString = $"HR00: {breakString} Assistance has been requested from room {Room.Name}{breakString}on {date} at {time}"; FusionRoom.Help.InputSig.StringValue = requestString; @@ -1793,7 +1852,7 @@ namespace PepperDash.Essentials.Core.Fusion } /// - public void ToggleHelpRequest(bool isHtml = false) + public void ToggleHelpRequest() { if (_helpRequestSent) { @@ -1801,11 +1860,13 @@ namespace PepperDash.Essentials.Core.Fusion } else { - SendHelpRequest(isHtml); + SendHelpRequest(); } } + } + /// /// Extensions to enhance Fusion room, asset and signal creation. /// @@ -1822,6 +1883,8 @@ namespace PepperDash.Essentials.Core.Fusion ///
public static BooleanSigData CreateOffsetBoolSig(this FusionRoom fr, uint number, string name, eSigIoMask mask) { + Debug.LogDebug("Creating Offset Bool Sig: {0} at Join {1}", name, number); + if (number < 50) { throw new ArgumentOutOfRangeException("number", "Cannot be less than 50"); @@ -1842,6 +1905,8 @@ namespace PepperDash.Essentials.Core.Fusion /// public static UShortSigData CreateOffsetUshortSig(this FusionRoom fr, uint number, string name, eSigIoMask mask) { + Debug.LogDebug("Creating Offset UShort Sig: {0} at Join {1}", name, number); + if (number < 50) { throw new ArgumentOutOfRangeException("number", "Cannot be less than 50"); @@ -1862,6 +1927,8 @@ namespace PepperDash.Essentials.Core.Fusion /// public static StringSigData CreateOffsetStringSig(this FusionRoom fr, uint number, string name, eSigIoMask mask) { + Debug.LogDebug("Creating Offset String Sig: {0} at Join {1}", name, number); + if (number < 50) { throw new ArgumentOutOfRangeException("number", "Cannot be less than 50"); @@ -2032,9 +2099,9 @@ namespace PepperDash.Essentials.Core.Fusion /// public class FusionCustomProperty { - /// - /// Constructor - /// + /// + /// Constructor + /// public FusionCustomProperty() { } diff --git a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerFactory.cs b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerFactory.cs index 4a598cc8..e874762c 100644 --- a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerFactory.cs +++ b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerFactory.cs @@ -28,6 +28,6 @@ public class IEssentialsRoomFusionControllerFactory : EssentialsDeviceFactory(); - return new IEssentialsRoomFusionController(properties); + return new IEssentialsRoomFusionController(dc.Key, dc.Name, properties); } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerPropertiesConfig.cs b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerPropertiesConfig.cs index b7183b17..4dc4a834 100644 --- a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerPropertiesConfig.cs +++ b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionControllerPropertiesConfig.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using PepperDash.Core; /// /// Config properties for an IEssentialsRoomFusionController device @@ -9,7 +10,28 @@ public class IEssentialsRoomFusionControllerPropertiesConfig /// Gets or sets the IP ID of the Fusion Room Controller /// [JsonProperty("ipId")] - public uint IpId { get; set; } + public string IpId { get; set; } + + /// + /// Gets the IP ID as a UInt16 + /// + [JsonIgnore] + public uint IpIdInt + { + get + { + // Try to parse the IpId string to UInt16 as hex + if (ushort.TryParse(IpId, System.Globalization.NumberStyles.HexNumber, null, out ushort result)) + { + return result; + } + else + { + Debug.LogWarning( "Failed to parse IpId '{0}' as UInt16", IpId); + return 0; + } + } + } /// /// Gets or sets the join map key @@ -22,4 +44,16 @@ public class IEssentialsRoomFusionControllerPropertiesConfig /// [JsonProperty("roomKey")] public string RoomKey { get; set; } + + /// + /// Gets or sets whether to use HTML format for help requests + /// + [JsonProperty("useHtmlFormatForHelpRequests")] + public bool UseHtmlFormatForHelpRequests { get; set; } = false; + + /// + /// Gets or sets whether to use 24-hour time format + /// + [JsonProperty("use24HourTimeFormat")] + public bool Use24HourTimeFormat { get; set; } = false; } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Fusion/IFusionHelpRequest.cs b/src/PepperDash.Essentials.Core/Fusion/IFusionHelpRequest.cs index f17f4c9a..7dcc6009 100644 --- a/src/PepperDash.Essentials.Core/Fusion/IFusionHelpRequest.cs +++ b/src/PepperDash.Essentials.Core/Fusion/IFusionHelpRequest.cs @@ -25,7 +25,7 @@ namespace PepperDash.Essentials.Core.Fusion /// Sends a help request /// /// - void SendHelpRequest(bool isHtml); + void SendHelpRequest(); /// /// Clears the current help request status @@ -35,6 +35,6 @@ namespace PepperDash.Essentials.Core.Fusion /// /// Toggles between sending and cancelling a help request /// - void ToggleHelpRequest(bool isHtml); + void ToggleHelpRequest(); } } From 071174fa7d2fa65cf9497e34967b205c50148e17 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 30 Oct 2025 14:13:02 -0600 Subject: [PATCH 32/55] feat: Implement help request status tracking in Fusion system --- .../Fusion/IEssentialsRoomFusionController.cs | 72 +++++++++++++++++-- .../Fusion/IFusionHelpRequest.cs | 6 +- .../Fusion/eFusionHelpResponse.cs | 37 ++++++++++ 3 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 src/PepperDash.Essentials.Core/Fusion/eFusionHelpResponse.cs diff --git a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs index ee439b12..11c19bce 100644 --- a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs +++ b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs @@ -76,12 +76,18 @@ namespace PepperDash.Essentials.Core.Fusion private bool _helpRequestSent; + private eFusionHelpResponse _helpRequestStatus; + /// public StringFeedback HelpRequestResponseFeedback { get; private set; } /// public BoolFeedback HelpRequestSentFeedback { get; private set; } + /// + public StringFeedback HelpRequestStatusFeedback { get; private set; } + + #region System Info Sigs //StringSigData SystemName; @@ -297,11 +303,10 @@ namespace PepperDash.Essentials.Core.Fusion FusionRVI.GenerateFileForAllFusionDevices(); - HelpRequestResponseFeedback = new StringFeedback("HelpRequestResponse", () => FusionRoom.Help.InputSig.StringValue); - HelpRequestResponseFeedback.LinkInputSig(FusionRoom.Help.InputSig); + HelpRequestResponseFeedback = new StringFeedback("HelpRequestResponse", () => FusionRoom.Help.OutputSig.StringValue); HelpRequestSentFeedback = new BoolFeedback("HelpRequestSent", () => _helpRequestSent); - + HelpRequestStatusFeedback = new StringFeedback("HelpRequestStatus", () => _helpRequestStatus.ToString()); } /// @@ -1767,10 +1772,59 @@ namespace PepperDash.Essentials.Core.Fusion { if (args.EventId == FusionEventIds.HelpMessageReceivedEventId) { - Debug.LogMessage(LogEventLevel.Information, this, "Help message received from Fusion for room '{0}'", + this.LogInformation( "Help message received from Fusion for room '{0}'", Room.Name); + + this.LogDebug("Help message content: {0}", FusionRoom.Help.OutputSig.StringValue); // Fire help request event HelpRequestResponseFeedback.FireUpdate(); + + if (!string.IsNullOrEmpty(FusionRoom.Help.OutputSig.StringValue)) + { + switch (FusionRoom.Help.OutputSig.StringValue) + { + case "Please wait, a technician is on his / her way.": + this.LogInformation("Please wait, a technician is on his / her way.", + Room.Name); + + _helpRequestStatus = eFusionHelpResponse.HelpOnTheWay; + break; + case "Please call the helpdesk.": + this.LogInformation("Please call the helpdesk."); + _helpRequestStatus = eFusionHelpResponse.CallHelpDesk; + break; + case "Please wait, I will reschedule your meeting to a different room.": + this.LogInformation("Please wait, I will reschedule your meeting to a different room.", + Room.Name); + + _helpRequestStatus = eFusionHelpResponse.ReschedulingMeeting; + break; + case "I will be taking control of your system. Please be patient while I adjust the settings.": + this.LogInformation("I will be taking control of your system. Please be patient while I adjust the settings.", + Room.Name); + + _helpRequestStatus = eFusionHelpResponse.TakingControl; + break; + default: + this.LogInformation("Unknown help request code received from Fusion for room '{0}'", + Room.Name); + + _helpRequestStatus = eFusionHelpResponse.None; + break; + } + } + else + { + _helpRequestStatus = eFusionHelpResponse.None; + } + + if(_helpRequestStatus == eFusionHelpResponse.None) + { + _helpRequestSent = false; + HelpRequestSentFeedback.FireUpdate(); + } + + HelpRequestStatusFeedback.FireUpdate(); } @@ -1835,9 +1889,14 @@ namespace PepperDash.Essentials.Core.Fusion FusionRoom.Help.InputSig.StringValue = requestString; - Debug.LogMessage(LogEventLevel.Information, this, "Help request sent to Fusion from room '{0}'", Room.Name); + this.LogInformation("Help request sent to Fusion from room '{0}'", Room.Name); + this.LogDebug("Help request content: {0}", FusionRoom.Help.InputSig.StringValue); _helpRequestSent = true; + HelpRequestSentFeedback.FireUpdate(); + + _helpRequestStatus = eFusionHelpResponse.HelpRequested; + HelpRequestStatusFeedback.FireUpdate(); } /// @@ -1847,6 +1906,9 @@ namespace PepperDash.Essentials.Core.Fusion { FusionRoom.Help.InputSig.StringValue = ""; _helpRequestSent = false; + HelpRequestSentFeedback.FireUpdate(); + _helpRequestStatus = eFusionHelpResponse.None; + HelpRequestStatusFeedback.FireUpdate(); Debug.LogMessage(LogEventLevel.Information, this, "Help request cancelled in Fusion for room '{0}'", Room.Name); } } diff --git a/src/PepperDash.Essentials.Core/Fusion/IFusionHelpRequest.cs b/src/PepperDash.Essentials.Core/Fusion/IFusionHelpRequest.cs index 7dcc6009..de4cca17 100644 --- a/src/PepperDash.Essentials.Core/Fusion/IFusionHelpRequest.cs +++ b/src/PepperDash.Essentials.Core/Fusion/IFusionHelpRequest.cs @@ -21,10 +21,14 @@ namespace PepperDash.Essentials.Core.Fusion /// BoolFeedback HelpRequestSentFeedback { get; } + /// + /// Feedback containing the current status of the help request + /// + StringFeedback HelpRequestStatusFeedback { get; } + /// /// Sends a help request /// - /// void SendHelpRequest(); /// diff --git a/src/PepperDash.Essentials.Core/Fusion/eFusionHelpResponse.cs b/src/PepperDash.Essentials.Core/Fusion/eFusionHelpResponse.cs new file mode 100644 index 00000000..6a5bbcd8 --- /dev/null +++ b/src/PepperDash.Essentials.Core/Fusion/eFusionHelpResponse.cs @@ -0,0 +1,37 @@ + + +namespace PepperDash.Essentials.Core.Fusion +{ + /// + /// Enumeration of possible Fusion Help Responses based on the standard responses from Fusion + /// + public enum eFusionHelpResponse + { + /// + /// No help response + /// + None, + /// + /// Help has been requested + /// + HelpRequested, + /// + /// Help is on the way + /// + HelpOnTheWay, + /// + /// Please call the helpdesk. + /// + CallHelpDesk, + /// + /// Rescheduling meeting. + /// + ReschedulingMeeting, + + /// + /// Technician taking control. + /// + TakingControl, + } + +} \ No newline at end of file From c852f87a2710c0fa7787ab18d6b6afe2b8f40f35 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 30 Oct 2025 14:33:58 -0600 Subject: [PATCH 33/55] refactor: Comment out logging statements in help request handling --- .../Fusion/IEssentialsRoomFusionController.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs index 11c19bce..1f21cbe6 100644 --- a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs +++ b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs @@ -1784,30 +1784,30 @@ namespace PepperDash.Essentials.Core.Fusion switch (FusionRoom.Help.OutputSig.StringValue) { case "Please wait, a technician is on his / her way.": - this.LogInformation("Please wait, a technician is on his / her way.", - Room.Name); + // this.LogInformation("Please wait, a technician is on his / her way.", + // Room.Name); _helpRequestStatus = eFusionHelpResponse.HelpOnTheWay; break; case "Please call the helpdesk.": - this.LogInformation("Please call the helpdesk."); - _helpRequestStatus = eFusionHelpResponse.CallHelpDesk; + // this.LogInformation("Please call the helpdesk."); + // _helpRequestStatus = eFusionHelpResponse.CallHelpDesk; break; case "Please wait, I will reschedule your meeting to a different room.": - this.LogInformation("Please wait, I will reschedule your meeting to a different room.", - Room.Name); + // this.LogInformation("Please wait, I will reschedule your meeting to a different room.", + // Room.Name); _helpRequestStatus = eFusionHelpResponse.ReschedulingMeeting; break; case "I will be taking control of your system. Please be patient while I adjust the settings.": - this.LogInformation("I will be taking control of your system. Please be patient while I adjust the settings.", - Room.Name); + // this.LogInformation("I will be taking control of your system. Please be patient while I adjust the settings.", + // Room.Name); _helpRequestStatus = eFusionHelpResponse.TakingControl; break; default: - this.LogInformation("Unknown help request code received from Fusion for room '{0}'", - Room.Name); + // this.LogInformation("Unknown help request code received from Fusion for room '{0}'", + // Room.Name); _helpRequestStatus = eFusionHelpResponse.None; break; From c4cf8f13e918323b62cc945b8dea2e9515b0dd58 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Thu, 30 Oct 2025 15:56:28 -0500 Subject: [PATCH 34/55] fix: register panel in post phase rather than activation cycle --- .../UI/TouchpanelBase.cs | 95 +++++++++++-------- .../MobileControlTouchpanelController.cs | 93 +++++++++++++----- 2 files changed, 122 insertions(+), 66 deletions(-) diff --git a/src/PepperDash.Essentials.Core/UI/TouchpanelBase.cs b/src/PepperDash.Essentials.Core/UI/TouchpanelBase.cs index 3ffed9fc..8d3f0b1d 100644 --- a/src/PepperDash.Essentials.Core/UI/TouchpanelBase.cs +++ b/src/PepperDash.Essentials.Core/UI/TouchpanelBase.cs @@ -1,20 +1,22 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Text; -using Crestron.SimplSharp; -using PepperDash.Essentials.Core; -using Crestron.SimplSharpPro.DeviceSupport; -using PepperDash.Core; -using Crestron.SimplSharpPro.UI; using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; +using PepperDash.Core; +using PepperDash.Core.Logging; using Serilog.Events; namespace PepperDash.Essentials.Core.UI { - public abstract class TouchpanelBase: EssentialsDevice, IHasBasicTriListWithSmartObject + /// + /// Base class for Touchpanel devices + /// + public abstract class TouchpanelBase : EssentialsDevice, IHasBasicTriListWithSmartObject { + /// + /// Gets or sets the configuration for the Crestron touchpanel. + /// protected CrestronTouchpanelPropertiesConfig _config; /// /// Gets or sets the Panel @@ -27,12 +29,11 @@ namespace PepperDash.Essentials.Core.UI /// is provided. /// /// Essentials Device Key - /// Essentials Device Name - /// Touchpanel Type to build - /// Touchpanel Configuration - /// IP-ID to use for touch panel + /// Essentials Device Name + /// Crestron Touchpanel Device + /// Touchpanel Configuration protected TouchpanelBase(string key, string name, BasicTriListWithSmartObject panel, CrestronTouchpanelPropertiesConfig config) - :base(key, name) + : base(key, name) { if (panel == null) @@ -55,23 +56,21 @@ namespace PepperDash.Essentials.Core.UI tsw.ButtonStateChange += Tsw_ButtonStateChange; } - _config = config; - - AddPreActivationAction(() => { - if (Panel.Register() != eDeviceRegistrationUnRegistrationResponse.Success) - Debug.LogMessage(LogEventLevel.Information, this, "WARNING: Registration failed. Continuing, but panel may not function: {0}", Panel.RegistrationFailureReason); + _config = config; + AddPreActivationAction(() => + { // Give up cleanly if SGD is not present. var sgdName = Global.FilePathPrefix + "sgd" + Global.DirectorySeparator + _config.SgdFile; if (!File.Exists(sgdName)) { - Debug.LogMessage(LogEventLevel.Information, this, "Smart object file '{0}' not present in User folder. Looking for embedded file", sgdName); + this.LogInformation("Smart object file '{0}' not present in User folder. Looking for embedded file", sgdName); sgdName = Global.ApplicationDirectoryPathPrefix + Global.DirectorySeparator + "SGD" + Global.DirectorySeparator + _config.SgdFile; if (!File.Exists(sgdName)) { - Debug.LogMessage(LogEventLevel.Information, this, "Unable to find SGD file '{0}' in User sgd or application SGD folder. Exiting touchpanel load.", sgdName); + this.LogWarning("Unable to find SGD file '{0}' in User sgd or application SGD folder. Exiting touchpanel load.", sgdName); return; } } @@ -82,12 +81,11 @@ namespace PepperDash.Essentials.Core.UI AddPostActivationAction(() => { // Check for IEssentialsRoomCombiner in DeviceManager and if found, subscribe to its event - var roomCombiner = DeviceManager.AllDevices.FirstOrDefault((d) => d is IEssentialsRoomCombiner) as IEssentialsRoomCombiner; - if (roomCombiner != null) + if (DeviceManager.AllDevices.FirstOrDefault((d) => d is IEssentialsRoomCombiner) is IEssentialsRoomCombiner roomCombiner) { // Subscribe to the even - roomCombiner.RoomCombinationScenarioChanged += new EventHandler(roomCombiner_RoomCombinationScenarioChanged); + roomCombiner.RoomCombinationScenarioChanged += new EventHandler(RoomCombiner_RoomCombinationScenarioChanged); // Connect to the initial roomKey if (roomCombiner.CurrentScenario != null) @@ -106,6 +104,11 @@ namespace PepperDash.Essentials.Core.UI // No room combiner, use the default key SetupPanelDrivers(_config.DefaultRoomKey); } + + var panelRegistrationResponse = Panel.Register(); + + if (panelRegistrationResponse != eDeviceRegistrationUnRegistrationResponse.Success) + this.LogInformation("WARNING: Registration failed. Continuing, but panel may not function: {0}", Panel.RegistrationFailureReason); }); } @@ -115,6 +118,14 @@ namespace PepperDash.Essentials.Core.UI /// Room Key for this panel protected abstract void SetupPanelDrivers(string roomKey); + /// + public override void Initialize() + { + base.Initialize(); + + + } + /// /// Event handler for System Extender Events @@ -129,7 +140,7 @@ namespace PepperDash.Essentials.Core.UI /// /// /// - protected virtual void roomCombiner_RoomCombinationScenarioChanged(object sender, EventArgs e) + protected virtual void RoomCombiner_RoomCombinationScenarioChanged(object sender, EventArgs e) { var roomCombiner = sender as IEssentialsRoomCombiner; @@ -156,23 +167,23 @@ namespace PepperDash.Essentials.Core.UI SetupPanelDrivers(newRoomKey); } - private void Panel_SigChange(object currentDevice, Crestron.SimplSharpPro.SigEventArgs args) - { - Debug.LogMessage(LogEventLevel.Verbose, this, "Sig change: {0} {1}={2}", args.Sig.Type, args.Sig.Number, args.Sig.StringValue); - var uo = args.Sig.UserObject; - if (uo is Action) - (uo as Action)(args.Sig.BoolValue); - else if (uo is Action) - (uo as Action)(args.Sig.UShortValue); - else if (uo is Action) - (uo as Action)(args.Sig.StringValue); - } - - private void Tsw_ButtonStateChange(GenericBase device, ButtonEventArgs args) - { - var uo = args.Button.UserObject; - if(uo is Action) - (uo as Action)(args.Button.State == eButtonState.Pressed); - } + private void Panel_SigChange(object currentDevice, SigEventArgs args) + { + this.LogVerbose("Sig change: {0} {1}={2}", args.Sig.Type, args.Sig.Number, args.Sig.StringValue); + var uo = args.Sig.UserObject; + if (uo is Action) + (uo as Action)(args.Sig.BoolValue); + else if (uo is Action) + (uo as Action)(args.Sig.UShortValue); + else if (uo is Action) + (uo as Action)(args.Sig.StringValue); + } + + private void Tsw_ButtonStateChange(GenericBase device, ButtonEventArgs args) + { + var uo = args.Button.UserObject; + if (uo is Action) + (uo as Action)(args.Button.State == eButtonState.Pressed); + } } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs index c1743d40..5830782d 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs @@ -252,6 +252,7 @@ namespace PepperDash.Essentials.Touchpanel if (!x70Panel.ExtenderApplicationControlReservedSigs.HideOpenedApplicationFeedback.BoolValue) { x70Panel.ExtenderButtonToolbarReservedSigs.ShowButtonToolbar(); + x70Panel.ExtenderButtonToolbarReservedSigs.Button2On(); } else @@ -294,17 +295,16 @@ namespace PepperDash.Essentials.Touchpanel handler(this, new DeviceInfoEventArgs(DeviceInfo)); }; + x70Panel.ExtenderButtonToolbarReservedSigs.DeviceExtenderSigChange += (o, a) => + { + this.LogVerbose("X70 Button Toolbar Device Extender args: {event}:{sig}:{name}:{type}:{boolValue}:{ushortValue}:{stringValue}", a.Event, a.Sig, a.Sig.Name, a.Sig.Type, a.Sig.BoolValue, a.Sig.UShortValue, a.Sig.StringValue); + }; + x70Panel.ExtenderApplicationControlReservedSigs.Use(); x70Panel.ExtenderZoomRoomAppReservedSigs.Use(); x70Panel.ExtenderEthernetReservedSigs.Use(); x70Panel.ExtenderButtonToolbarReservedSigs.Use(); - x70Panel.ExtenderButtonToolbarReservedSigs.Button1Off(); - x70Panel.ExtenderButtonToolbarReservedSigs.Button3Off(); - x70Panel.ExtenderButtonToolbarReservedSigs.Button4Off(); - x70Panel.ExtenderButtonToolbarReservedSigs.Button5Off(); - x70Panel.ExtenderButtonToolbarReservedSigs.Button6Off(); - return; } @@ -414,34 +414,79 @@ namespace PepperDash.Essentials.Touchpanel McServerUrlFeedback.LinkInputSig(Panel.StringInput[3]); UserCodeFeedback.LinkInputSig(Panel.StringInput[4]); - Panel.IpInformationChange += (sender, args) => + Panel.IpInformationChange -= Panel_IpInformationChange; + Panel.IpInformationChange += Panel_IpInformationChange; + + Panel.OnlineStatusChange -= Panel_OnlineChange; + Panel.OnlineStatusChange += Panel_OnlineChange; + } + + private void Panel_OnlineChange(GenericBase sender, OnlineOfflineEventArgs args) + { + try { - if (args.Connected) + if (!args.DeviceOnLine) { - this.LogVerbose("Connection from IP: {ip}", args.DeviceIpAddress); - this.LogInformation("Sending {appUrl} on join 1", AppUrlFeedback.StringValue); - - var appUrl = GetUrlWithCorrectIp(_appUrl); - Panel.StringInput[1].StringValue = appUrl; - - SetAppUrl(appUrl); + this.LogInformation("panel is offline"); + return; } - else - { - this.LogVerbose("Disconnection from IP: {ip}", args.DeviceIpAddress); - } - }; - Panel.OnlineStatusChange += (sender, args) => - { - this.LogInformation("Sending {appUrl} on join 1", AppUrlFeedback.StringValue); + this.LogDebug("panel is online"); UpdateFeedbacks(); Panel.StringInput[1].StringValue = _appUrl; Panel.StringInput[2].StringValue = QrCodeUrlFeedback.StringValue; Panel.StringInput[3].StringValue = McServerUrlFeedback.StringValue; Panel.StringInput[4].StringValue = UserCodeFeedback.StringValue; - }; + + if (Panel is TswXX70Base x70Panel) + { + this.LogDebug("setting buttons off"); + + x70Panel.ExtenderButtonToolbarReservedSigs.Button1Off(); + x70Panel.ExtenderButtonToolbarReservedSigs.Button3Off(); + x70Panel.ExtenderButtonToolbarReservedSigs.Button4Off(); + x70Panel.ExtenderButtonToolbarReservedSigs.Button5Off(); + x70Panel.ExtenderButtonToolbarReservedSigs.Button6Off(); + } + + SendUrlToPanel(); + } + catch (Exception ex) + { + this.LogError("Exception in panel online: {message}", ex.Message); + this.LogDebug(ex, "Stack Trace: "); + } + } + + private void SendUrlToPanel() + { + var appUrl = GetUrlWithCorrectIp(_appUrl); + + this.LogInformation("Sending {appUrl} on join 1", AppUrlFeedback.StringValue); + + if (Panel.StringInput[1].StringValue == appUrl) + { + this.LogInformation("App URL already set to {appUrl}, no update needed", AppUrlFeedback.StringValue); + return; + } + + Panel.StringInput[1].StringValue = appUrl; + + SetAppUrl(appUrl); + } + + private void Panel_IpInformationChange(GenericBase sender, ConnectedIpEventArgs args) + { + if (args.Connected) + { + this.LogVerbose("Connection from IP: {ip}", args.DeviceIpAddress); + SendUrlToPanel(); + } + else + { + this.LogVerbose("Disconnection from IP: {ip}", args.DeviceIpAddress); + } } /// From afcddad1ccf95d98f646d686d77b8f415c62ce92 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Thu, 30 Oct 2025 15:56:50 -0500 Subject: [PATCH 35/55] fix: remove async on task, as it's unnecessary --- .../MobileControlSystemController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index e9bde5f3..2fe20391 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -2399,7 +2399,7 @@ namespace PepperDash.Essentials foreach (var handler in handlers) { - Task.Run(async () => + Task.Run(() => { try { From fd40b0c6d1a2effe3d45c6052109e33a57c989fa Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Thu, 30 Oct 2025 15:57:15 -0500 Subject: [PATCH 36/55] fix: send all status messages with ClientId --- .../Touchpanel/ITswAppControlMessenger.cs | 2 +- .../Touchpanel/ThemeMessenger.cs | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs index d4c55f87..acfcab2a 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs @@ -33,7 +33,7 @@ namespace PepperDash.Essentials.Touchpanel return; } - AddAction($"/fullStatus", (id, context) => SendFullStatus()); + AddAction($"/fullStatus", (id, context) => SendFullStatus(id)); AddAction($"/openApp", (id, context) => _appControl.OpenApp()); diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs index 0f02bc59..c2e1f6ee 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.AppServer; using PepperDash.Essentials.AppServer.Messengers; @@ -29,17 +30,17 @@ namespace PepperDash.Essentials.Touchpanel { AddAction("/fullStatus", (id, content) => { - PostStatusMessage(new ThemeUpdateMessage { Theme = _tpDevice.Theme }); + PostStatusMessage(new ThemeUpdateMessage { Theme = _tpDevice.Theme }, id); }); AddAction("/saveTheme", (id, content) => { var theme = content.ToObject>(); - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Setting theme to {theme}", this, theme.Value); + this.LogInformation("Setting theme to {theme}", this, theme.Value); _tpDevice.UpdateTheme(theme.Value); - PostStatusMessage(JToken.FromObject(new { theme = theme.Value })); + PostStatusMessage(JToken.FromObject(new { theme = theme.Value }), id); }); } } From faa2169bafd1f14ced19f229e3ec603dae54ccf7 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Thu, 30 Oct 2025 15:57:28 -0500 Subject: [PATCH 37/55] fix: send correct URL to panel --- .../WebSocketServer/MobileControlWebsocketServer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index 56e34812..0f2fa388 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -385,9 +385,9 @@ namespace PepperDash.Essentials.WebSocketServer var appUrl = $"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}"; - this.LogVerbose("Sending URL {appUrl}", appUrl); + this.LogVerbose("Sending URL {appUrl} to touchpanel {touchpanelKey}", appUrl, touchpanel.Touchpanel.Key); - touchpanel.Messenger.UpdateAppUrl($"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}"); + touchpanel.Touchpanel.SetAppUrl($"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}"); } } From 44ed067f4d5c06e6bcf546ca9c9e6c9ffeb40b41 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Thu, 30 Oct 2025 15:57:37 -0500 Subject: [PATCH 38/55] chore: update local build version --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 545110d3..49e41a67 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,6 +1,6 @@ - 2.18.2-local + 2.19.4-local $(Version) PepperDash Technology PepperDash Technology From bfc9b7e7faf23eb7ee5cd97c2025a6e25b69898d Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Thu, 30 Oct 2025 16:07:26 -0500 Subject: [PATCH 39/55] fix: logging & ternary changes --- src/PepperDash.Essentials.Core/UI/TouchpanelBase.cs | 9 --------- .../Touchpanel/ThemeMessenger.cs | 2 +- .../WebSocketServer/UiClient.cs | 2 +- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/PepperDash.Essentials.Core/UI/TouchpanelBase.cs b/src/PepperDash.Essentials.Core/UI/TouchpanelBase.cs index 8d3f0b1d..fd9da802 100644 --- a/src/PepperDash.Essentials.Core/UI/TouchpanelBase.cs +++ b/src/PepperDash.Essentials.Core/UI/TouchpanelBase.cs @@ -118,15 +118,6 @@ namespace PepperDash.Essentials.Core.UI /// Room Key for this panel protected abstract void SetupPanelDrivers(string roomKey); - /// - public override void Initialize() - { - base.Initialize(); - - - } - - /// /// Event handler for System Extender Events /// diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs index c2e1f6ee..ffaf9cfc 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs @@ -37,7 +37,7 @@ namespace PepperDash.Essentials.Touchpanel { var theme = content.ToObject>(); - this.LogInformation("Setting theme to {theme}", this, theme.Value); + this.LogInformation("Setting theme to {theme}", theme.Value); _tpDevice.UpdateTheme(theme.Value); PostStatusMessage(JToken.FromObject(new { theme = theme.Value }), id); diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs index 0b6dbe7c..cf8dd7f9 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/UiClient.cs @@ -112,7 +112,7 @@ namespace PepperDash.Essentials.WebSocketServer { clientId = Id, roomKey = RoomKey, - touchpanelKey = string.IsNullOrEmpty(TouchpanelKey) ? string.Empty : TouchpanelKey, + touchpanelKey = TouchpanelKey ?? string.Empty, }) }; From ff609dfecd795b5da012481a0f4e00744e4b1580 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 31 Oct 2025 09:01:54 -0500 Subject: [PATCH 40/55] fix: add config option to turn echo off for SSH In addition, removed CTimer in favor of System.Threading.Timers Timer in the SSH class, and modified the class to be thread-safe. --- src/PepperDash.Core/Comm/GenericSshClient.cs | 250 ++++++++---- .../Comm/GenericTcpIpClient.cs | 365 +++++++++--------- .../Comm and IR/CommFactory.cs | 19 +- 3 files changed, 373 insertions(+), 261 deletions(-) diff --git a/src/PepperDash.Core/Comm/GenericSshClient.cs b/src/PepperDash.Core/Comm/GenericSshClient.cs index 13192ed5..2277a21c 100644 --- a/src/PepperDash.Core/Comm/GenericSshClient.cs +++ b/src/PepperDash.Core/Comm/GenericSshClient.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text; using System.Threading; using Crestron.SimplSharp; @@ -11,11 +12,12 @@ using Renci.SshNet.Common; namespace PepperDash.Core { /// - /// + /// SSH Client /// public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect { private const string SPlusKey = "Uninitialized SshClient"; + /// /// Object to enable stream debugging /// @@ -36,11 +38,6 @@ namespace PepperDash.Core /// public event EventHandler ConnectionChange; - /// - ///// - ///// - //public event GenericSocketStatusChangeEventDelegate SocketStatusChange; - /// /// Gets or sets the Hostname /// @@ -67,7 +64,7 @@ namespace PepperDash.Core public bool IsConnected { // returns false if no client or not connected - get { return Client != null && ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } + get { return client != null && ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } } /// @@ -83,16 +80,26 @@ namespace PepperDash.Core /// public SocketStatus ClientStatus { - get { return _ClientStatus; } + get { lock (_stateLock) { return _ClientStatus; } } private set { - if (_ClientStatus == value) - return; - _ClientStatus = value; - OnConnectionChange(); + bool shouldFireEvent = false; + lock (_stateLock) + { + if (_ClientStatus != value) + { + _ClientStatus = value; + shouldFireEvent = true; + } + } + // Fire event outside lock to avoid deadlock + if (shouldFireEvent) + OnConnectionChange(); } } - SocketStatus _ClientStatus; + + private SocketStatus _ClientStatus; + private bool _ConnectEnabled; /// /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event @@ -100,7 +107,7 @@ namespace PepperDash.Core /// public ushort UStatus { - get { return (ushort)_ClientStatus; } + get { lock (_stateLock) { return (ushort)_ClientStatus; } } } /// @@ -111,7 +118,11 @@ namespace PepperDash.Core /// /// Will be set and unset by connect and disconnect only /// - public bool ConnectEnabled { get; private set; } + public bool ConnectEnabled + { + get { lock (_stateLock) { return _ConnectEnabled; } } + private set { lock (_stateLock) { _ConnectEnabled = value; } } + } /// /// S+ helper for AutoReconnect @@ -127,17 +138,25 @@ namespace PepperDash.Core /// public int AutoReconnectIntervalMs { get; set; } - SshClient Client; + private SshClient client; - ShellStream TheStream; + private ShellStream shellStream; - CTimer ReconnectTimer; + private readonly Timer reconnectTimer; //Lock object to prevent simulatneous connect/disconnect operations //private CCriticalSection connectLock = new CCriticalSection(); - private SemaphoreSlim connectLock = new SemaphoreSlim(1); + private readonly SemaphoreSlim connectLock = new SemaphoreSlim(1); - private bool DisconnectLogged = false; + // Thread-safety lock for state changes + private readonly object _stateLock = new object(); + + private bool disconnectLogged = false; + + /// + /// When true, turns off echo for the SSH session + /// + public bool DisableEcho { get; set; } /// /// Typical constructor. @@ -154,13 +173,13 @@ namespace PepperDash.Core Password = password; AutoReconnectIntervalMs = 5000; - ReconnectTimer = new CTimer(o => + reconnectTimer = new Timer(o => { - if (ConnectEnabled) + if (ConnectEnabled) // Now thread-safe property access { Connect(); } - }, System.Threading.Timeout.Infinite); + }, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); } /// @@ -172,23 +191,23 @@ namespace PepperDash.Core CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); AutoReconnectIntervalMs = 5000; - ReconnectTimer = new CTimer(o => + reconnectTimer = new Timer(o => { - if (ConnectEnabled) + if (ConnectEnabled) // Now thread-safe property access { Connect(); } - }, System.Threading.Timeout.Infinite); + }, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); } /// /// Handles closing this up when the program shuts down /// - void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + private void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) { if (programEventType == eProgramStatusEventType.Stopping) { - if (Client != null) + if (client != null) { this.LogDebug("Program stopping. Closing connection"); Disconnect(); @@ -223,10 +242,10 @@ namespace PepperDash.Core this.LogDebug("Attempting connect"); // Cancel reconnect if running. - ReconnectTimer?.Stop(); + StopReconnectTimer(); // Cleanup the old client if it already exists - if (Client != null) + if (client != null) { this.LogDebug("Cleaning up disconnected client"); KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY); @@ -239,29 +258,37 @@ namespace PepperDash.Core this.LogDebug("Creating new SshClient"); ConnectionInfo connectionInfo = new ConnectionInfo(Hostname, Port, Username, pauth, kauth); - Client = new SshClient(connectionInfo); - Client.ErrorOccurred += Client_ErrorOccurred; + client = new SshClient(connectionInfo); + client.ErrorOccurred += Client_ErrorOccurred; //Attempt to connect ClientStatus = SocketStatus.SOCKET_STATUS_WAITING; try { - Client.Connect(); - TheStream = Client.CreateShellStream("PDTShell", 0, 0, 0, 0, 65534); - if (TheStream.DataAvailable) + client.Connect(); + + var modes = new Dictionary(); + + if (DisableEcho) + { + modes.Add(TerminalModes.ECHO, 0); + } + + shellStream = client.CreateShellStream("PDTShell", 0, 0, 0, 0, 65534, modes); + if (shellStream.DataAvailable) { // empty the buffer if there is data - string str = TheStream.Read(); + string str = shellStream.Read(); } - TheStream.DataReceived += Stream_DataReceived; + shellStream.DataReceived += Stream_DataReceived; this.LogInformation("Connected"); ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED; - DisconnectLogged = false; + disconnectLogged = false; } catch (SshConnectionException e) { var ie = e.InnerException; // The details are inside!! - var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error; + var errorLogLevel = disconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error; if (ie is SocketException) { @@ -286,37 +313,37 @@ namespace PepperDash.Core this.LogVerbose(ie, "Exception details: "); } - DisconnectLogged = true; + disconnectLogged = true; KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); if (AutoReconnect) { this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs); - ReconnectTimer.Reset(AutoReconnectIntervalMs); + StartReconnectTimer(); } } catch (SshOperationTimeoutException ex) { this.LogWarning("Connection attempt timed out: {message}", ex.Message); - DisconnectLogged = true; + disconnectLogged = true; KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); if (AutoReconnect) { this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); - ReconnectTimer.Reset(AutoReconnectIntervalMs); + StartReconnectTimer(); } } catch (Exception e) { - var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error; + var errorLogLevel = disconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error; this.LogError("Unhandled exception on connect: {error}", e.Message); this.LogVerbose(e, "Exception details: "); - DisconnectLogged = true; + disconnectLogged = true; KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); if (AutoReconnect) { this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); - ReconnectTimer.Reset(AutoReconnectIntervalMs); + StartReconnectTimer(); } } } @@ -334,11 +361,7 @@ namespace PepperDash.Core { ConnectEnabled = false; // Stop trying reconnects, if we are - if (ReconnectTimer != null) - { - ReconnectTimer.Stop(); - // ReconnectTimer = null; - } + StopReconnectTimer(); KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY); } @@ -352,12 +375,12 @@ namespace PepperDash.Core try { - if (Client != null) + if (client != null) { - Client.ErrorOccurred -= Client_ErrorOccurred; - Client.Disconnect(); - Client.Dispose(); - Client = null; + client.ErrorOccurred -= Client_ErrorOccurred; + client.Disconnect(); + client.Dispose(); + client = null; ClientStatus = status; this.LogDebug("Disconnected"); } @@ -371,16 +394,16 @@ namespace PepperDash.Core /// /// Kills the stream /// - void KillStream() + private void KillStream() { try { - if (TheStream != null) + if (shellStream != null) { - TheStream.DataReceived -= Stream_DataReceived; - TheStream.Close(); - TheStream.Dispose(); - TheStream = null; + shellStream.DataReceived -= Stream_DataReceived; + shellStream.Close(); + shellStream.Dispose(); + shellStream = null; this.LogDebug("Disconnected stream"); } } @@ -393,7 +416,7 @@ namespace PepperDash.Core /// /// Handles the keyboard interactive authentication, should it be required. /// - void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e) + private void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e) { foreach (AuthenticationPrompt prompt in e.Prompts) if (prompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1) @@ -403,7 +426,7 @@ namespace PepperDash.Core /// /// Handler for data receive on ShellStream. Passes data across to queue for line parsing. /// - void Stream_DataReceived(object sender, ShellDataEventArgs e) + private void Stream_DataReceived(object sender, ShellDataEventArgs e) { if (((ShellStream)sender).Length <= 0L) { @@ -439,7 +462,7 @@ namespace PepperDash.Core /// Error event handler for client events - disconnect, etc. Will forward those events via ConnectionChange /// event /// - void Client_ErrorOccurred(object sender, ExceptionEventArgs e) + private void Client_ErrorOccurred(object sender, ExceptionEventArgs e) { CrestronInvoke.BeginInvoke(o => { @@ -459,7 +482,7 @@ namespace PepperDash.Core if (AutoReconnect && ConnectEnabled) { this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); - ReconnectTimer.Reset(AutoReconnectIntervalMs); + StartReconnectTimer(); } }); } @@ -467,7 +490,7 @@ namespace PepperDash.Core /// /// Helper for ConnectionChange event /// - void OnConnectionChange() + private void OnConnectionChange() { ConnectionChange?.Invoke(this, new GenericSocketStatusChageEventArgs(this)); } @@ -482,7 +505,7 @@ namespace PepperDash.Core { try { - if (Client != null && TheStream != null && IsConnected) + if (client != null && shellStream != null && IsConnected) { if (StreamDebugging.TxStreamDebuggingIsEnabled) this.LogInformation( @@ -490,8 +513,8 @@ namespace PepperDash.Core text.Length, ComTextHelper.GetDebugText(text)); - TheStream.Write(text); - TheStream.Flush(); + shellStream.Write(text); + shellStream.Flush(); } else { @@ -503,7 +526,7 @@ namespace PepperDash.Core this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim()); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); - ReconnectTimer.Reset(); + StopReconnectTimer(); } catch (Exception ex) { @@ -519,13 +542,13 @@ namespace PepperDash.Core { try { - if (Client != null && TheStream != null && IsConnected) + if (client != null && shellStream != null && IsConnected) { if (StreamDebugging.TxStreamDebuggingIsEnabled) this.LogInformation("Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); - TheStream.Write(bytes, 0, bytes.Length); - TheStream.Flush(); + shellStream.Write(bytes, 0, bytes.Length); + shellStream.Flush(); } else { @@ -537,7 +560,7 @@ namespace PepperDash.Core this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes)); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); - ReconnectTimer.Reset(); + StopReconnectTimer(); } catch (Exception ex) { @@ -546,6 +569,83 @@ namespace PepperDash.Core } #endregion + /// + /// Safely starts the reconnect timer with exception handling + /// + private void StartReconnectTimer() + { + try + { + reconnectTimer?.Change(AutoReconnectIntervalMs, System.Threading.Timeout.Infinite); + } + catch (ObjectDisposedException) + { + // Timer was disposed, ignore + this.LogDebug("Attempted to start timer but it was already disposed"); + } + } + + /// + /// Safely stops the reconnect timer with exception handling + /// + private void StopReconnectTimer() + { + try + { + reconnectTimer?.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); + } + catch (ObjectDisposedException) + { + // Timer was disposed, ignore + this.LogDebug("Attempted to stop timer but it was already disposed"); + } + } + + /// + /// Deactivate method - properly dispose of resources + /// + public override bool Deactivate() + { + try + { + this.LogDebug("Deactivating SSH client - disposing resources"); + + // Stop trying reconnects + ConnectEnabled = false; + StopReconnectTimer(); + + // Disconnect and cleanup client + KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY); + + // Dispose timer + try + { + reconnectTimer?.Dispose(); + } + catch (ObjectDisposedException) + { + // Already disposed, ignore + } + + // Dispose semaphore + try + { + connectLock?.Dispose(); + } + catch (ObjectDisposedException) + { + // Already disposed, ignore + } + + return base.Deactivate(); + } + catch (Exception ex) + { + this.LogException(ex, "Error during SSH client deactivation"); + return false; + } + } + } //***************************************************************************************************** diff --git a/src/PepperDash.Core/Comm/GenericTcpIpClient.cs b/src/PepperDash.Core/Comm/GenericTcpIpClient.cs index c9235411..51b5871f 100644 --- a/src/PepperDash.Core/Comm/GenericTcpIpClient.cs +++ b/src/PepperDash.Core/Comm/GenericTcpIpClient.cs @@ -19,44 +19,44 @@ namespace PepperDash.Core /// public CommunicationStreamDebugging StreamDebugging { get; private set; } - /// - /// Fires when data is received from the server and returns it as a Byte array - /// - public event EventHandler BytesReceived; + /// + /// Fires when data is received from the server and returns it as a Byte array + /// + public event EventHandler BytesReceived; - /// - /// Fires when data is received from the server and returns it as text - /// - public event EventHandler TextReceived; + /// + /// Fires when data is received from the server and returns it as text + /// + public event EventHandler TextReceived; - /// - /// - /// - //public event GenericSocketStatusChangeEventDelegate SocketStatusChange; - public event EventHandler ConnectionChange; + /// + /// + /// + //public event GenericSocketStatusChangeEventDelegate SocketStatusChange; + public event EventHandler ConnectionChange; - private string _hostname; + private string _hostname; /// /// Address of server /// public string Hostname { - get - { - return _hostname; - } + get + { + return _hostname; + } - set - { - _hostname = value; - if (_client != null) - { - _client.AddressClientConnectedTo = _hostname; - } - } - } + set + { + _hostname = value; + if (_client != null) + { + _client.AddressClientConnectedTo = _hostname; + } + } + } /// /// Gets or sets the Port @@ -78,19 +78,19 @@ namespace PepperDash.Core /// public int BufferSize { get; set; } - /// - /// The actual client class - /// - private TCPClient _client; + /// + /// The actual client class + /// + private TCPClient _client; - /// - /// Bool showing if socket is connected - /// - public bool IsConnected - { - get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } + /// + /// Bool showing if socket is connected + /// + public bool IsConnected + { + get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } } - + /// /// S+ helper for IsConnected /// @@ -99,15 +99,15 @@ namespace PepperDash.Core get { return (ushort)(IsConnected ? 1 : 0); } } - /// - /// _client socket status Read only - /// - public SocketStatus ClientStatus - { - get + /// + /// _client socket status Read only + /// + public SocketStatus ClientStatus + { + get { - return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus; - } + return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus; + } } /// @@ -119,26 +119,26 @@ namespace PepperDash.Core get { return (ushort)ClientStatus; } } - /// + /// /// Status text shows the message associated with socket status - /// - public string ClientStatusText { get { return ClientStatus.ToString(); } } + /// + public string ClientStatusText { get { return ClientStatus.ToString(); } } - /// - /// Ushort representation of client status - /// + /// + /// Ushort representation of client status + /// [Obsolete] - public ushort UClientStatus { get { return (ushort)ClientStatus; } } + public ushort UClientStatus { get { return (ushort)ClientStatus; } } - /// - /// Connection failure reason - /// - public string ConnectionFailure { get { return ClientStatus.ToString(); } } + /// + /// Connection failure reason + /// + public string ConnectionFailure { get { return ClientStatus.ToString(); } } - /// - /// Gets or sets the AutoReconnect - /// - public bool AutoReconnect { get; set; } + /// + /// Gets or sets the AutoReconnect + /// + public bool AutoReconnect { get; set; } /// /// S+ helper for AutoReconnect @@ -149,29 +149,29 @@ namespace PepperDash.Core set { AutoReconnect = value == 1; } } - /// - /// Milliseconds to wait before attempting to reconnect. Defaults to 5000 - /// - public int AutoReconnectIntervalMs { get; set; } + /// + /// Milliseconds to wait before attempting to reconnect. Defaults to 5000 + /// + public int AutoReconnectIntervalMs { get; set; } - /// - /// Set only when the disconnect method is called - /// - bool DisconnectCalledByUser; + /// + /// Set only when the disconnect method is called + /// + bool DisconnectCalledByUser; - /// - /// - /// - public bool Connected - { - get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } - } + /// + /// + /// + public bool Connected + { + get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } + } //Lock object to prevent simulatneous connect/disconnect operations private CCriticalSection connectLock = new CCriticalSection(); // private Timer for auto reconnect - private CTimer RetryTimer; + private CTimer RetryTimer; /// /// Constructor @@ -181,8 +181,8 @@ namespace PepperDash.Core /// /// public GenericTcpIpClient(string key, string address, int port, int bufferSize) - : base(key) - { + : base(key) + { StreamDebugging = new CommunicationStreamDebugging(key); CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); AutoReconnectIntervalMs = 5000; @@ -218,18 +218,18 @@ namespace PepperDash.Core /// Default constructor for S+ /// public GenericTcpIpClient() - : base(SplusKey) + : base(SplusKey) { StreamDebugging = new CommunicationStreamDebugging(SplusKey); - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - AutoReconnectIntervalMs = 5000; + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + AutoReconnectIntervalMs = 5000; BufferSize = 2000; RetryTimer = new CTimer(o => { Reconnect(); }, Timeout.Infinite); - } + } /// /// Initialize method @@ -255,26 +255,26 @@ namespace PepperDash.Core /// /// /// - /// - /// Deactivate method - /// - public override bool Deactivate() - { + /// + /// Deactivate method + /// + public override bool Deactivate() + { RetryTimer.Stop(); RetryTimer.Dispose(); if (_client != null) { - _client.SocketStatusChange -= this.Client_SocketStatusChange; + _client.SocketStatusChange -= this.Client_SocketStatusChange; DisconnectClient(); } - return true; - } + return true; + } - /// - /// Connect method - /// - public void Connect() - { + /// + /// Connect method + /// + public void Connect() + { if (string.IsNullOrEmpty(Hostname)) { Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': No address set", Key); @@ -310,7 +310,7 @@ namespace PepperDash.Core { connectLock.Leave(); } - } + } private void Reconnect() { @@ -337,11 +337,11 @@ namespace PepperDash.Core } } - /// - /// Disconnect method - /// - public void Disconnect() - { + /// + /// Disconnect method + /// + public void Disconnect() + { try { connectLock.Enter(); @@ -355,7 +355,7 @@ namespace PepperDash.Core { connectLock.Leave(); } - } + } /// /// DisconnectClient method @@ -375,7 +375,7 @@ namespace PepperDash.Core /// /// void ConnectToServerCallback(TCPClient c) - { + { if (c.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) { Debug.Console(0, this, "Server connection result: {0}", c.ClientStatus); @@ -385,13 +385,13 @@ namespace PepperDash.Core { Debug.Console(1, this, "Server connection result: {0}", c.ClientStatus); } - } + } /// /// Disconnects, waits and attemtps to connect again /// void WaitAndTryReconnect() - { + { CrestronInvoke.BeginInvoke(o => { try @@ -409,7 +409,7 @@ namespace PepperDash.Core connectLock.Leave(); } }); - } + } /// /// Recieves incoming data @@ -417,7 +417,7 @@ namespace PepperDash.Core /// /// void Receive(TCPClient client, int numBytes) - { + { if (client != null) { if (numBytes > 0) @@ -443,52 +443,52 @@ namespace PepperDash.Core } textHandler(this, new GenericCommMethodReceiveTextArgs(str)); - } + } } client.ReceiveDataAsync(Receive); } - } + } - /// - /// SendText method - /// - public void SendText(string text) - { - var bytes = Encoding.GetEncoding(28591).GetBytes(text); - // Check debug level before processing byte array + /// + /// SendText method + /// + public void SendText(string text) + { + var bytes = Encoding.GetEncoding(28591).GetBytes(text); + // Check debug level before processing byte array if (StreamDebugging.TxStreamDebuggingIsEnabled) Debug.Console(0, this, "Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text)); if (_client != null) - _client.SendData(bytes, bytes.Length); - } + _client.SendData(bytes, bytes.Length); + } - /// - /// SendEscapedText method - /// - public void SendEscapedText(string text) - { - var unescapedText = Regex.Replace(text, @"\\x([0-9a-fA-F][0-9a-fA-F])", s => - { - var hex = s.Groups[1].Value; - return ((char)Convert.ToByte(hex, 16)).ToString(); - }); - SendText(unescapedText); - } + /// + /// SendEscapedText method + /// + public void SendEscapedText(string text) + { + var unescapedText = Regex.Replace(text, @"\\x([0-9a-fA-F][0-9a-fA-F])", s => + { + var hex = s.Groups[1].Value; + return ((char)Convert.ToByte(hex, 16)).ToString(); + }); + SendText(unescapedText); + } /// /// Sends Bytes to the server /// /// - /// - /// SendBytes method - /// - public void SendBytes(byte[] bytes) - { + /// + /// SendBytes method + /// + public void SendBytes(byte[] bytes) + { if (StreamDebugging.TxStreamDebuggingIsEnabled) Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); if (_client != null) - _client.SendData(bytes, bytes.Length); - } + _client.SendData(bytes, bytes.Length); + } /// /// Socket Status Change Handler @@ -496,7 +496,7 @@ namespace PepperDash.Core /// /// void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus) - { + { if (clientSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) { Debug.Console(0, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); @@ -505,68 +505,73 @@ namespace PepperDash.Core else { Debug.Console(1, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); - _client.ReceiveDataAsync(Receive); + _client.ReceiveDataAsync(Receive); } - var handler = ConnectionChange; - if (handler != null) - ConnectionChange(this, new GenericSocketStatusChageEventArgs(this)); - } - } + var handler = ConnectionChange; + if (handler != null) + ConnectionChange(this, new GenericSocketStatusChageEventArgs(this)); + } + } - /// - /// Represents a TcpSshPropertiesConfig - /// - public class TcpSshPropertiesConfig - { + /// + /// Represents a TcpSshPropertiesConfig + /// + public class TcpSshPropertiesConfig + { /// /// Address to connect to /// [JsonProperty(Required = Required.Always)] - public string Address { get; set; } - + public string Address { get; set; } + /// /// Port to connect to /// - [JsonProperty(Required = Required.Always)] - public int Port { get; set; } - + [JsonProperty(Required = Required.Always)] + public int Port { get; set; } + /// /// Username credential /// - public string Username { get; set; } - /// - /// Gets or sets the Password - /// - public string Password { get; set; } + public string Username { get; set; } + /// + /// Gets or sets the Password + /// + public string Password { get; set; } - /// - /// Defaults to 32768 - /// - public int BufferSize { get; set; } + /// + /// Defaults to 32768 + /// + public int BufferSize { get; set; } - /// - /// Gets or sets the AutoReconnect - /// - public bool AutoReconnect { get; set; } + /// + /// Gets or sets the AutoReconnect + /// + public bool AutoReconnect { get; set; } - /// - /// Gets or sets the AutoReconnectIntervalMs - /// - public int AutoReconnectIntervalMs { get; set; } + /// + /// Gets or sets the AutoReconnectIntervalMs + /// + public int AutoReconnectIntervalMs { get; set; } + + /// + /// When true, turns off echo for the SSH session + /// + [JsonProperty("disableSshEcho")] + public bool DisableSshEcho { get; set; } /// /// Default constructor /// public TcpSshPropertiesConfig() - { - BufferSize = 32768; - AutoReconnect = true; - AutoReconnectIntervalMs = 5000; + { + BufferSize = 32768; + AutoReconnect = true; + AutoReconnectIntervalMs = 5000; Username = ""; Password = ""; - } - - } - + DisableSshEcho = false; + } + } } diff --git a/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs b/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs index a0723c4b..fbc46579 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/CommFactory.cs @@ -64,8 +64,11 @@ namespace PepperDash.Essentials.Core break; case eControlMethod.Ssh: { - var ssh = new GenericSshClient(deviceConfig.Key + "-ssh", c.Address, c.Port, c.Username, c.Password); - ssh.AutoReconnect = c.AutoReconnect; + var ssh = new GenericSshClient(deviceConfig.Key + "-ssh", c.Address, c.Port, c.Username, c.Password) + { + AutoReconnect = c.AutoReconnect, + DisableEcho = c.DisableSshEcho + }; if (ssh.AutoReconnect) ssh.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs; comm = ssh; @@ -73,8 +76,10 @@ namespace PepperDash.Essentials.Core } case eControlMethod.Tcpip: { - var tcp = new GenericTcpIpClient(deviceConfig.Key + "-tcp", c.Address, c.Port, c.BufferSize); - tcp.AutoReconnect = c.AutoReconnect; + var tcp = new GenericTcpIpClient(deviceConfig.Key + "-tcp", c.Address, c.Port, c.BufferSize) + { + AutoReconnect = c.AutoReconnect + }; if (tcp.AutoReconnect) tcp.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs; comm = tcp; @@ -90,8 +95,10 @@ namespace PepperDash.Essentials.Core break; case eControlMethod.SecureTcpIp: { - var secureTcp = new GenericSecureTcpIpClient(deviceConfig.Key + "-secureTcp", c.Address, c.Port, c.BufferSize); - secureTcp.AutoReconnect = c.AutoReconnect; + var secureTcp = new GenericSecureTcpIpClient(deviceConfig.Key + "-secureTcp", c.Address, c.Port, c.BufferSize) + { + AutoReconnect = c.AutoReconnect + }; if (secureTcp.AutoReconnect) secureTcp.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs; comm = secureTcp; From 314570d6c3d78c7a92a362f3ec3a4a06bdbebd28 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Fri, 31 Oct 2025 13:09:12 -0500 Subject: [PATCH 41/55] fix: change number of retained files to 7 instead of 30 for processors --- src/PepperDash.Core/Logging/Debug.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/PepperDash.Core/Logging/Debug.cs b/src/PepperDash.Core/Logging/Debug.cs index 62a95f2c..877d8997 100644 --- a/src/PepperDash.Core/Logging/Debug.cs +++ b/src/PepperDash.Core/Logging/Debug.cs @@ -168,7 +168,7 @@ namespace PepperDash.Core .WriteTo.File(new RenderedCompactJsonFormatter(), logFilePath, rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: LogEventLevel.Debug, - retainedFileCountLimit: CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? 30 : 60, + retainedFileCountLimit: CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? 7 : 14, levelSwitch: _fileLogLevelSwitch ); @@ -1081,9 +1081,6 @@ namespace PepperDash.Core /// Logs to Console when at-level, and all messages to error log /// [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] - /// - /// Console method - /// public static void Console(uint level, ErrorLogLevel errorLogLevel, string format, params object[] items) { @@ -1096,9 +1093,6 @@ namespace PepperDash.Core /// it will only be written to the log. /// [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] - /// - /// ConsoleWithLog method - /// public static void ConsoleWithLog(uint level, string format, params object[] items) { LogMessage(level, format, items); From 6ed7c96ec71c8d074bcdc3c84da49a21461647c3 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Mon, 3 Nov 2025 10:53:21 -0600 Subject: [PATCH 42/55] fix: centralize debug printing into extension methods Stream debugging now uses CrestronConsole instead of debug methods, so that the debug statements will be printed regardless of console debug level. This also means that comm debug statements will NOT be in the Crestron Error log or in the files created by the logging system --- src/PepperDash.Core/ComTextHelper.cs | 43 ++ .../Comm/CommunicationStreamDebugging.cs | 56 +-- src/PepperDash.Core/Comm/GenericSshClient.cs | 17 +- .../Comm/GenericTcpIpClient.cs | 369 +++++++++--------- src/PepperDash.Core/Comm/GenericUdpServer.cs | 58 ++- .../Comm/StreamDebuggingExtensionMethods.cs | 69 ++++ .../Comm/eStreamDebuggingDataTypeSettings.cs | 24 ++ .../Comm/eStreamDebuggingSetting.cs | 28 ++ src/PepperDash.Core/CommunicationExtras.cs | 158 +++----- .../Comm and IR/CecPortController.cs | 47 ++- .../Comm and IR/ComPortController.cs | 32 +- .../SetDeviceStreamDebugRequestHandler.cs | 46 +-- 12 files changed, 493 insertions(+), 454 deletions(-) create mode 100644 src/PepperDash.Core/ComTextHelper.cs create mode 100644 src/PepperDash.Core/Comm/StreamDebuggingExtensionMethods.cs create mode 100644 src/PepperDash.Core/Comm/eStreamDebuggingDataTypeSettings.cs create mode 100644 src/PepperDash.Core/Comm/eStreamDebuggingSetting.cs diff --git a/src/PepperDash.Core/ComTextHelper.cs b/src/PepperDash.Core/ComTextHelper.cs new file mode 100644 index 00000000..06b996be --- /dev/null +++ b/src/PepperDash.Core/ComTextHelper.cs @@ -0,0 +1,43 @@ +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace PepperDash.Core +{ + /// + /// + /// + public class ComTextHelper + { + /// + /// Gets escaped text for a byte array + /// + /// + /// string with all bytes escaped + public static string GetEscapedText(byte[] bytes) + { + return string.Concat(bytes.Select(b => string.Format(@"[{0:X2}]", (int)b)).ToArray()); + } + + /// + /// Gets escaped text for a string + /// + /// + /// string with all bytes escaped + public static string GetEscapedText(string text) + { + var bytes = Encoding.GetEncoding(28591).GetBytes(text); + return string.Concat(bytes.Select(b => string.Format(@"[{0:X2}]", (int)b)).ToArray()); + } + + /// + /// Gets debug text for a string + /// + /// + /// string with all non-printable characters escaped + public static string GetDebugText(string text) + { + return Regex.Replace(text, @"[^\u0020-\u007E]", a => GetEscapedText(a.Value)); + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Core/Comm/CommunicationStreamDebugging.cs b/src/PepperDash.Core/Comm/CommunicationStreamDebugging.cs index 33141f0c..780e65d0 100644 --- a/src/PepperDash.Core/Comm/CommunicationStreamDebugging.cs +++ b/src/PepperDash.Core/Comm/CommunicationStreamDebugging.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text; using Crestron.SimplSharp; @@ -37,14 +36,14 @@ namespace PepperDash.Core { get { - return _DebugTimeoutInMs/60000; + return _DebugTimeoutInMs / 60000; } } /// /// Gets or sets the RxStreamDebuggingIsEnabled /// - public bool RxStreamDebuggingIsEnabled{ get; private set; } + public bool RxStreamDebuggingIsEnabled { get; private set; } /// /// Indicates that transmit stream debugging is enabled @@ -108,7 +107,7 @@ namespace PepperDash.Core TxStreamDebuggingIsEnabled = true; Debug.SetDeviceDebugSettings(ParentDeviceKey, setting); - + } /// @@ -136,51 +135,4 @@ namespace PepperDash.Core DebugExpiryPeriod = null; } } - - /// - /// The available settings for stream debugging - /// - [Flags] - /// - /// Enumeration of eStreamDebuggingSetting values - /// - public enum eStreamDebuggingSetting - { - /// - /// Debug off - /// - Off = 0, - /// - /// Debug received data - /// - Rx = 1, - /// - /// Debug transmitted data - /// - Tx = 2, - /// - /// Debug both received and transmitted data - /// - Both = Rx | Tx - } - - /// - /// The available settings for stream debugging response types - /// - [Flags] - public enum eStreamDebuggingDataTypeSettings - { - /// - /// Debug data in byte format - /// - Bytes = 0, - /// - /// Debug data in text format - /// - Text = 1, - /// - /// Debug data in both byte and text formats - /// - Both = Bytes | Text, - } } diff --git a/src/PepperDash.Core/Comm/GenericSshClient.cs b/src/PepperDash.Core/Comm/GenericSshClient.cs index 13192ed5..8ea5138e 100644 --- a/src/PepperDash.Core/Comm/GenericSshClient.cs +++ b/src/PepperDash.Core/Comm/GenericSshClient.cs @@ -416,18 +416,14 @@ namespace PepperDash.Core if (bytesHandler != null) { var bytes = Encoding.UTF8.GetBytes(response); - if (StreamDebugging.RxStreamDebuggingIsEnabled) - { - this.LogInformation("Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length); - } + this.PrintReceivedBytes(bytes); bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); } var textHandler = TextReceived; if (textHandler != null) { - if (StreamDebugging.RxStreamDebuggingIsEnabled) - this.LogInformation("Received: '{0}'", ComTextHelper.GetDebugText(response)); + this.PrintReceivedText(response); textHandler(this, new GenericCommMethodReceiveTextArgs(response)); } @@ -484,11 +480,7 @@ namespace PepperDash.Core { if (Client != null && TheStream != null && IsConnected) { - if (StreamDebugging.TxStreamDebuggingIsEnabled) - this.LogInformation( - "Sending {length} characters of text: '{text}'", - text.Length, - ComTextHelper.GetDebugText(text)); + this.PrintSentText(text); TheStream.Write(text); TheStream.Flush(); @@ -521,8 +513,7 @@ namespace PepperDash.Core { if (Client != null && TheStream != null && IsConnected) { - if (StreamDebugging.TxStreamDebuggingIsEnabled) - this.LogInformation("Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); + this.PrintSentBytes(bytes); TheStream.Write(bytes, 0, bytes.Length); TheStream.Flush(); diff --git a/src/PepperDash.Core/Comm/GenericTcpIpClient.cs b/src/PepperDash.Core/Comm/GenericTcpIpClient.cs index c9235411..17507851 100644 --- a/src/PepperDash.Core/Comm/GenericTcpIpClient.cs +++ b/src/PepperDash.Core/Comm/GenericTcpIpClient.cs @@ -19,44 +19,44 @@ namespace PepperDash.Core /// public CommunicationStreamDebugging StreamDebugging { get; private set; } - /// - /// Fires when data is received from the server and returns it as a Byte array - /// - public event EventHandler BytesReceived; + /// + /// Fires when data is received from the server and returns it as a Byte array + /// + public event EventHandler BytesReceived; - /// - /// Fires when data is received from the server and returns it as text - /// - public event EventHandler TextReceived; + /// + /// Fires when data is received from the server and returns it as text + /// + public event EventHandler TextReceived; - /// - /// - /// - //public event GenericSocketStatusChangeEventDelegate SocketStatusChange; - public event EventHandler ConnectionChange; + /// + /// + /// + //public event GenericSocketStatusChangeEventDelegate SocketStatusChange; + public event EventHandler ConnectionChange; - private string _hostname; + private string _hostname; /// /// Address of server /// public string Hostname { - get - { - return _hostname; - } + get + { + return _hostname; + } - set - { - _hostname = value; - if (_client != null) - { - _client.AddressClientConnectedTo = _hostname; - } - } - } + set + { + _hostname = value; + if (_client != null) + { + _client.AddressClientConnectedTo = _hostname; + } + } + } /// /// Gets or sets the Port @@ -78,19 +78,19 @@ namespace PepperDash.Core /// public int BufferSize { get; set; } - /// - /// The actual client class - /// - private TCPClient _client; + /// + /// The actual client class + /// + private TCPClient _client; - /// - /// Bool showing if socket is connected - /// - public bool IsConnected - { - get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } + /// + /// Bool showing if socket is connected + /// + public bool IsConnected + { + get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } } - + /// /// S+ helper for IsConnected /// @@ -99,15 +99,15 @@ namespace PepperDash.Core get { return (ushort)(IsConnected ? 1 : 0); } } - /// - /// _client socket status Read only - /// - public SocketStatus ClientStatus - { - get + /// + /// _client socket status Read only + /// + public SocketStatus ClientStatus + { + get { - return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus; - } + return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus; + } } /// @@ -119,26 +119,26 @@ namespace PepperDash.Core get { return (ushort)ClientStatus; } } - /// + /// /// Status text shows the message associated with socket status - /// - public string ClientStatusText { get { return ClientStatus.ToString(); } } + /// + public string ClientStatusText { get { return ClientStatus.ToString(); } } - /// - /// Ushort representation of client status - /// + /// + /// Ushort representation of client status + /// [Obsolete] - public ushort UClientStatus { get { return (ushort)ClientStatus; } } + public ushort UClientStatus { get { return (ushort)ClientStatus; } } - /// - /// Connection failure reason - /// - public string ConnectionFailure { get { return ClientStatus.ToString(); } } + /// + /// Connection failure reason + /// + public string ConnectionFailure { get { return ClientStatus.ToString(); } } - /// - /// Gets or sets the AutoReconnect - /// - public bool AutoReconnect { get; set; } + /// + /// Gets or sets the AutoReconnect + /// + public bool AutoReconnect { get; set; } /// /// S+ helper for AutoReconnect @@ -149,29 +149,29 @@ namespace PepperDash.Core set { AutoReconnect = value == 1; } } - /// - /// Milliseconds to wait before attempting to reconnect. Defaults to 5000 - /// - public int AutoReconnectIntervalMs { get; set; } + /// + /// Milliseconds to wait before attempting to reconnect. Defaults to 5000 + /// + public int AutoReconnectIntervalMs { get; set; } - /// - /// Set only when the disconnect method is called - /// - bool DisconnectCalledByUser; + /// + /// Set only when the disconnect method is called + /// + bool DisconnectCalledByUser; - /// - /// - /// - public bool Connected - { - get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } - } + /// + /// + /// + public bool Connected + { + get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } + } //Lock object to prevent simulatneous connect/disconnect operations private CCriticalSection connectLock = new CCriticalSection(); // private Timer for auto reconnect - private CTimer RetryTimer; + private CTimer RetryTimer; /// /// Constructor @@ -181,8 +181,8 @@ namespace PepperDash.Core /// /// public GenericTcpIpClient(string key, string address, int port, int bufferSize) - : base(key) - { + : base(key) + { StreamDebugging = new CommunicationStreamDebugging(key); CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); AutoReconnectIntervalMs = 5000; @@ -218,18 +218,18 @@ namespace PepperDash.Core /// Default constructor for S+ /// public GenericTcpIpClient() - : base(SplusKey) + : base(SplusKey) { StreamDebugging = new CommunicationStreamDebugging(SplusKey); - CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); - AutoReconnectIntervalMs = 5000; + CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); + AutoReconnectIntervalMs = 5000; BufferSize = 2000; RetryTimer = new CTimer(o => { Reconnect(); }, Timeout.Infinite); - } + } /// /// Initialize method @@ -255,26 +255,26 @@ namespace PepperDash.Core /// /// /// - /// - /// Deactivate method - /// - public override bool Deactivate() - { + /// + /// Deactivate method + /// + public override bool Deactivate() + { RetryTimer.Stop(); RetryTimer.Dispose(); if (_client != null) { - _client.SocketStatusChange -= this.Client_SocketStatusChange; + _client.SocketStatusChange -= this.Client_SocketStatusChange; DisconnectClient(); } - return true; - } + return true; + } - /// - /// Connect method - /// - public void Connect() - { + /// + /// Connect method + /// + public void Connect() + { if (string.IsNullOrEmpty(Hostname)) { Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': No address set", Key); @@ -310,7 +310,7 @@ namespace PepperDash.Core { connectLock.Leave(); } - } + } private void Reconnect() { @@ -337,11 +337,11 @@ namespace PepperDash.Core } } - /// - /// Disconnect method - /// - public void Disconnect() - { + /// + /// Disconnect method + /// + public void Disconnect() + { try { connectLock.Enter(); @@ -355,7 +355,7 @@ namespace PepperDash.Core { connectLock.Leave(); } - } + } /// /// DisconnectClient method @@ -375,7 +375,7 @@ namespace PepperDash.Core /// /// void ConnectToServerCallback(TCPClient c) - { + { if (c.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) { Debug.Console(0, this, "Server connection result: {0}", c.ClientStatus); @@ -385,13 +385,13 @@ namespace PepperDash.Core { Debug.Console(1, this, "Server connection result: {0}", c.ClientStatus); } - } + } /// /// Disconnects, waits and attemtps to connect again /// void WaitAndTryReconnect() - { + { CrestronInvoke.BeginInvoke(o => { try @@ -409,7 +409,7 @@ namespace PepperDash.Core connectLock.Leave(); } }); - } + } /// /// Recieves incoming data @@ -417,7 +417,7 @@ namespace PepperDash.Core /// /// void Receive(TCPClient client, int numBytes) - { + { if (client != null) { if (numBytes > 0) @@ -426,10 +426,7 @@ namespace PepperDash.Core var bytesHandler = BytesReceived; if (bytesHandler != null) { - if (StreamDebugging.RxStreamDebuggingIsEnabled) - { - Debug.Console(0, this, "Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length); - } + this.PrintReceivedBytes(bytes); bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); } var textHandler = TextReceived; @@ -437,58 +434,54 @@ namespace PepperDash.Core { var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); - if (StreamDebugging.RxStreamDebuggingIsEnabled) - { - Debug.Console(0, this, "Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length); - } + this.PrintReceivedText(str); textHandler(this, new GenericCommMethodReceiveTextArgs(str)); - } + } } client.ReceiveDataAsync(Receive); } - } + } - /// - /// SendText method - /// - public void SendText(string text) - { - var bytes = Encoding.GetEncoding(28591).GetBytes(text); - // Check debug level before processing byte array - if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.Console(0, this, "Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text)); + /// + /// SendText method + /// + public void SendText(string text) + { + var bytes = Encoding.GetEncoding(28591).GetBytes(text); + // Check debug level before processing byte array + this.PrintSentText(text); if (_client != null) - _client.SendData(bytes, bytes.Length); - } + _client.SendData(bytes, bytes.Length); + } - /// - /// SendEscapedText method - /// - public void SendEscapedText(string text) - { - var unescapedText = Regex.Replace(text, @"\\x([0-9a-fA-F][0-9a-fA-F])", s => - { - var hex = s.Groups[1].Value; - return ((char)Convert.ToByte(hex, 16)).ToString(); - }); - SendText(unescapedText); - } + /// + /// SendEscapedText method + /// + public void SendEscapedText(string text) + { + var unescapedText = Regex.Replace(text, @"\\x([0-9a-fA-F][0-9a-fA-F])", s => + { + var hex = s.Groups[1].Value; + return ((char)Convert.ToByte(hex, 16)).ToString(); + }); + SendText(unescapedText); + } /// /// Sends Bytes to the server /// /// - /// - /// SendBytes method - /// - public void SendBytes(byte[] bytes) - { + /// + /// SendBytes method + /// + public void SendBytes(byte[] bytes) + { if (StreamDebugging.TxStreamDebuggingIsEnabled) Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); if (_client != null) - _client.SendData(bytes, bytes.Length); - } + _client.SendData(bytes, bytes.Length); + } /// /// Socket Status Change Handler @@ -496,7 +489,7 @@ namespace PepperDash.Core /// /// void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus) - { + { if (clientSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) { Debug.Console(0, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); @@ -505,68 +498,68 @@ namespace PepperDash.Core else { Debug.Console(1, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); - _client.ReceiveDataAsync(Receive); + _client.ReceiveDataAsync(Receive); } - var handler = ConnectionChange; - if (handler != null) - ConnectionChange(this, new GenericSocketStatusChageEventArgs(this)); - } - } + var handler = ConnectionChange; + if (handler != null) + ConnectionChange(this, new GenericSocketStatusChageEventArgs(this)); + } + } - /// - /// Represents a TcpSshPropertiesConfig - /// - public class TcpSshPropertiesConfig - { + /// + /// Represents a TcpSshPropertiesConfig + /// + public class TcpSshPropertiesConfig + { /// /// Address to connect to /// [JsonProperty(Required = Required.Always)] - public string Address { get; set; } - + public string Address { get; set; } + /// /// Port to connect to /// - [JsonProperty(Required = Required.Always)] - public int Port { get; set; } - + [JsonProperty(Required = Required.Always)] + public int Port { get; set; } + /// /// Username credential /// - public string Username { get; set; } - /// - /// Gets or sets the Password - /// - public string Password { get; set; } + public string Username { get; set; } + /// + /// Gets or sets the Password + /// + public string Password { get; set; } - /// - /// Defaults to 32768 - /// - public int BufferSize { get; set; } + /// + /// Defaults to 32768 + /// + public int BufferSize { get; set; } - /// - /// Gets or sets the AutoReconnect - /// - public bool AutoReconnect { get; set; } + /// + /// Gets or sets the AutoReconnect + /// + public bool AutoReconnect { get; set; } - /// - /// Gets or sets the AutoReconnectIntervalMs - /// - public int AutoReconnectIntervalMs { get; set; } + /// + /// Gets or sets the AutoReconnectIntervalMs + /// + public int AutoReconnectIntervalMs { get; set; } /// /// Default constructor /// public TcpSshPropertiesConfig() - { - BufferSize = 32768; - AutoReconnect = true; - AutoReconnectIntervalMs = 5000; + { + BufferSize = 32768; + AutoReconnect = true; + AutoReconnectIntervalMs = 5000; Username = ""; Password = ""; - } + } - } + } } diff --git a/src/PepperDash.Core/Comm/GenericUdpServer.cs b/src/PepperDash.Core/Comm/GenericUdpServer.cs index 52ac627a..a713872a 100644 --- a/src/PepperDash.Core/Comm/GenericUdpServer.cs +++ b/src/PepperDash.Core/Comm/GenericUdpServer.cs @@ -124,7 +124,7 @@ namespace PepperDash.Core CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler); } - + /// /// /// @@ -135,7 +135,7 @@ namespace PepperDash.Core public GenericUdpServer(string key, string address, int port, int bufferSize) : base(key) { - StreamDebugging = new CommunicationStreamDebugging(key); + StreamDebugging = new CommunicationStreamDebugging(key); Hostname = address; Port = port; BufferSize = bufferSize; @@ -180,7 +180,7 @@ namespace PepperDash.Core /// void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) { - if (programEventType != eProgramStatusEventType.Stopping) + if (programEventType != eProgramStatusEventType.Stopping) return; Debug.Console(1, this, "Program stopping. Disabling Server"); @@ -243,7 +243,7 @@ namespace PepperDash.Core /// public void Disconnect() { - if(Server != null) + if (Server != null) Server.DisableUDPServer(); IsConnected = false; @@ -265,7 +265,7 @@ namespace PepperDash.Core try { - if (numBytes <= 0) + if (numBytes <= 0) return; var sourceIp = Server.IPAddressLastMessageReceivedFrom; @@ -281,17 +281,13 @@ namespace PepperDash.Core var bytesHandler = BytesReceived; if (bytesHandler != null) { - if (StreamDebugging.RxStreamDebuggingIsEnabled) - { - Debug.Console(0, this, "Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length); - } + this.PrintReceivedBytes(bytes); bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); } var textHandler = TextReceived; if (textHandler != null) { - if (StreamDebugging.RxStreamDebuggingIsEnabled) - Debug.Console(0, this, "Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length); + this.PrintReceivedText(str); textHandler(this, new GenericCommMethodReceiveTextArgs(str)); } } @@ -318,8 +314,7 @@ namespace PepperDash.Core if (IsConnected && Server != null) { - if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.Console(0, this, "Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text)); + this.PrintSentText(text); Server.SendData(bytes, bytes.Length); } @@ -334,8 +329,7 @@ namespace PepperDash.Core /// public void SendBytes(byte[] bytes) { - if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); + this.PrintSentBytes(bytes); if (IsConnected && Server != null) Server.SendData(bytes, bytes.Length); @@ -343,11 +337,11 @@ namespace PepperDash.Core } - /// - /// Represents a GenericUdpReceiveTextExtraArgs - /// - public class GenericUdpReceiveTextExtraArgs : EventArgs - { + /// + /// Represents a GenericUdpReceiveTextExtraArgs + /// + public class GenericUdpReceiveTextExtraArgs : EventArgs + { /// /// /// @@ -359,7 +353,7 @@ namespace PepperDash.Core /// /// /// - public int Port { get; private set; } + public int Port { get; private set; } /// /// /// @@ -373,18 +367,18 @@ namespace PepperDash.Core /// /// public GenericUdpReceiveTextExtraArgs(string text, string ipAddress, int port, byte[] bytes) - { - Text = text; - IpAddress = ipAddress; - Port = port; - Bytes = bytes; - } + { + Text = text; + IpAddress = ipAddress; + Port = port; + Bytes = bytes; + } - /// - /// Stupid S+ Constructor - /// - public GenericUdpReceiveTextExtraArgs() { } - } + /// + /// Stupid S+ Constructor + /// + public GenericUdpReceiveTextExtraArgs() { } + } /// /// diff --git a/src/PepperDash.Core/Comm/StreamDebuggingExtensionMethods.cs b/src/PepperDash.Core/Comm/StreamDebuggingExtensionMethods.cs new file mode 100644 index 00000000..9fd7544d --- /dev/null +++ b/src/PepperDash.Core/Comm/StreamDebuggingExtensionMethods.cs @@ -0,0 +1,69 @@ +using System; +using Crestron.SimplSharp; + +namespace PepperDash.Core +{ + /// + /// Extension methods for stream debugging + /// + public static class StreamDebuggingExtensions + { + private static readonly string app = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? $"App {InitialParametersClass.ApplicationNumber}" : $"{InitialParametersClass.RoomId}"; + + /// + /// Print the sent bytes to the console + /// + /// comms device + /// bytes to print + public static void PrintSentBytes(this IStreamDebugging comms, byte[] bytes) + { + if (!comms.StreamDebugging.TxStreamDebuggingIsEnabled) return; + + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + + CrestronConsole.PrintLine($"[{timestamp}][{app}][{comms.Key}] Sending {bytes.Length} bytes: '{ComTextHelper.GetEscapedText(bytes)}'"); + } + + /// + /// Print the received bytes to the console + /// + /// comms device + /// bytes to print + public static void PrintReceivedBytes(this IStreamDebugging comms, byte[] bytes) + { + if (!comms.StreamDebugging.RxStreamDebuggingIsEnabled) return; + + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + + CrestronConsole.PrintLine($"[{timestamp}][{app}][{comms.Key}] Received {bytes.Length} bytes: '{ComTextHelper.GetEscapedText(bytes)}'"); + } + + /// + /// Print the sent text to the console + /// + /// comms device + /// text to print + public static void PrintSentText(this IStreamDebugging comms, string text) + { + if (!comms.StreamDebugging.TxStreamDebuggingIsEnabled) return; + + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + + CrestronConsole.PrintLine($"[{timestamp}][{app}][{comms.Key}] Sending Text: '{ComTextHelper.GetDebugText(text)}'"); + } + + /// + /// Print the received text to the console + /// + /// comms device + /// text to print + public static void PrintReceivedText(this IStreamDebugging comms, string text) + { + if (!comms.StreamDebugging.RxStreamDebuggingIsEnabled) return; + + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + + CrestronConsole.PrintLine($"[{timestamp}][{app}][{comms.Key}] Received Text: '{ComTextHelper.GetDebugText(text)}'"); + } + } +} diff --git a/src/PepperDash.Core/Comm/eStreamDebuggingDataTypeSettings.cs b/src/PepperDash.Core/Comm/eStreamDebuggingDataTypeSettings.cs new file mode 100644 index 00000000..24bd18d5 --- /dev/null +++ b/src/PepperDash.Core/Comm/eStreamDebuggingDataTypeSettings.cs @@ -0,0 +1,24 @@ +using System; + +namespace PepperDash.Core +{ + /// + /// The available settings for stream debugging response types + /// + [Flags] + public enum eStreamDebuggingDataTypeSettings + { + /// + /// Debug data in byte format + /// + Bytes = 0, + /// + /// Debug data in text format + /// + Text = 1, + /// + /// Debug data in both byte and text formats + /// + Both = Bytes | Text, + } +} diff --git a/src/PepperDash.Core/Comm/eStreamDebuggingSetting.cs b/src/PepperDash.Core/Comm/eStreamDebuggingSetting.cs new file mode 100644 index 00000000..f9f7eb3f --- /dev/null +++ b/src/PepperDash.Core/Comm/eStreamDebuggingSetting.cs @@ -0,0 +1,28 @@ +using System; + +namespace PepperDash.Core +{ + /// + /// The available settings for stream debugging + /// + [Flags] + public enum eStreamDebuggingSetting + { + /// + /// Debug off + /// + Off = 0, + /// + /// Debug received data + /// + Rx = 1, + /// + /// Debug transmitted data + /// + Tx = 2, + /// + /// Debug both received and transmitted data + /// + Both = Rx | Tx + } +} diff --git a/src/PepperDash.Core/CommunicationExtras.cs b/src/PepperDash.Core/CommunicationExtras.cs index d16ea761..a8bc57d3 100644 --- a/src/PepperDash.Core/CommunicationExtras.cs +++ b/src/PepperDash.Core/CommunicationExtras.cs @@ -1,10 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; -using System.Text.RegularExpressions; using Newtonsoft.Json; namespace PepperDash.Core @@ -42,7 +39,7 @@ namespace PepperDash.Core /// Defines the contract for IBasicCommunication /// public interface IBasicCommunication : ICommunicationReceiver - { + { /// /// Send text to the device /// @@ -54,7 +51,7 @@ namespace PepperDash.Core /// /// void SendBytes(byte[] bytes); - } + } /// /// Represents a device that implements IBasicCommunication and IStreamDebugging @@ -67,7 +64,7 @@ namespace PepperDash.Core /// /// Represents a device with stream debugging capablities /// - public interface IStreamDebugging + public interface IStreamDebugging : IKeyed { /// /// Object to enable stream debugging @@ -76,12 +73,12 @@ namespace PepperDash.Core CommunicationStreamDebugging StreamDebugging { get; } } - /// - /// For IBasicCommunication classes that have SocketStatus. GenericSshClient, - /// GenericTcpIpClient - /// - public interface ISocketStatus : IBasicCommunication - { + /// + /// For IBasicCommunication classes that have SocketStatus. GenericSshClient, + /// GenericTcpIpClient + /// + public interface ISocketStatus : IBasicCommunication + { /// /// Notifies of socket status changes /// @@ -93,7 +90,7 @@ namespace PepperDash.Core [JsonProperty("clientStatus")] [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] SocketStatus ClientStatus { get; } - } + } /// /// Describes a device that implements ISocketStatus and IStreamDebugging @@ -107,24 +104,24 @@ namespace PepperDash.Core /// Describes a device that can automatically attempt to reconnect /// public interface IAutoReconnect - { + { /// /// Enable automatic recconnect /// [JsonProperty("autoReconnect")] - bool AutoReconnect { get; set; } + bool AutoReconnect { get; set; } /// /// Interval in ms to attempt automatic recconnections /// [JsonProperty("autoReconnectIntervalMs")] - int AutoReconnectIntervalMs { get; set; } - } + int AutoReconnectIntervalMs { get; set; } + } - /// - /// - /// - public enum eGenericCommMethodStatusChangeType - { + /// + /// + /// + public enum eGenericCommMethodStatusChangeType + { /// /// Connected /// @@ -133,45 +130,45 @@ namespace PepperDash.Core /// Disconnected /// Disconnected - } + } - /// - /// This delegate defines handler for IBasicCommunication status changes - /// - /// Device firing the status change - /// - public delegate void GenericCommMethodStatusHandler(IBasicCommunication comm, eGenericCommMethodStatusChangeType status); + /// + /// This delegate defines handler for IBasicCommunication status changes + /// + /// Device firing the status change + /// + public delegate void GenericCommMethodStatusHandler(IBasicCommunication comm, eGenericCommMethodStatusChangeType status); - /// - /// - /// - public class GenericCommMethodReceiveBytesArgs : EventArgs - { - /// - /// Gets or sets the Bytes - /// - public byte[] Bytes { get; private set; } + /// + /// + /// + public class GenericCommMethodReceiveBytesArgs : EventArgs + { + /// + /// Gets or sets the Bytes + /// + public byte[] Bytes { get; private set; } /// /// /// /// public GenericCommMethodReceiveBytesArgs(byte[] bytes) - { - Bytes = bytes; - } + { + Bytes = bytes; + } - /// - /// S+ Constructor - /// - public GenericCommMethodReceiveBytesArgs() { } - } + /// + /// S+ Constructor + /// + public GenericCommMethodReceiveBytesArgs() { } + } - /// - /// - /// - public class GenericCommMethodReceiveTextArgs : EventArgs - { + /// + /// + /// + public class GenericCommMethodReceiveTextArgs : EventArgs + { /// /// /// @@ -185,9 +182,9 @@ namespace PepperDash.Core /// /// public GenericCommMethodReceiveTextArgs(string text) - { - Text = text; - } + { + Text = text; + } /// /// @@ -195,59 +192,14 @@ namespace PepperDash.Core /// /// public GenericCommMethodReceiveTextArgs(string text, string delimiter) - :this(text) + : this(text) { Delimiter = delimiter; } - /// - /// S+ Constructor - /// - public GenericCommMethodReceiveTextArgs() { } - } - - - - /// - /// - /// - public class ComTextHelper - { /// - /// Gets escaped text for a byte array + /// S+ Constructor /// - /// - /// - public static string GetEscapedText(byte[] bytes) - { - return String.Concat(bytes.Select(b => string.Format(@"[{0:X2}]", (int)b)).ToArray()); - } - - /// - /// Gets escaped text for a string - /// - /// - /// - /// - /// GetEscapedText method - /// - public static string GetEscapedText(string text) - { - var bytes = Encoding.GetEncoding(28591).GetBytes(text); - return String.Concat(bytes.Select(b => string.Format(@"[{0:X2}]", (int)b)).ToArray()); - } - - /// - /// Gets debug text for a string - /// - /// - /// - /// - /// GetDebugText method - /// - public static string GetDebugText(string text) - { - return Regex.Replace(text, @"[^\u0020-\u007E]", a => GetEscapedText(a.Value)); - } - } + public GenericCommMethodReceiveTextArgs() { } + } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs index 7dacce5c..2dd4f2ba 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs @@ -12,15 +12,15 @@ using Serilog.Events; namespace PepperDash.Essentials.Core { - /// - /// Represents a CecPortController - /// - public class CecPortController : Device, IBasicCommunicationWithStreamDebugging + /// + /// Represents a CecPortController + /// + public class CecPortController : Device, IBasicCommunicationWithStreamDebugging { - /// - /// Gets or sets the StreamDebugging - /// - public CommunicationStreamDebugging StreamDebugging { get; private set; } + /// + /// Gets or sets the StreamDebugging + /// + public CommunicationStreamDebugging StreamDebugging { get; private set; } public event EventHandler BytesReceived; public event EventHandler TextReceived; @@ -33,16 +33,16 @@ namespace PepperDash.Essentials.Core ICec Port; public CecPortController(string key, Func postActivationFunc, - EssentialsControlPropertiesConfig config):base(key) + EssentialsControlPropertiesConfig config) : base(key) { - StreamDebugging = new CommunicationStreamDebugging(key); + StreamDebugging = new CommunicationStreamDebugging(key); AddPostActivationAction(() => { Port = postActivationFunc(config); Port.StreamCec.CecChange += StreamCec_CecChange; - }); + }); } public CecPortController(string key, ICec port) @@ -58,27 +58,25 @@ namespace PepperDash.Essentials.Core if (args.EventId == CecEventIds.CecMessageReceivedEventId) OnDataReceived(cecDevice.Received.StringValue); else if (args.EventId == CecEventIds.ErrorFeedbackEventId) - if(cecDevice.ErrorFeedback.BoolValue) + if (cecDevice.ErrorFeedback.BoolValue) Debug.LogMessage(LogEventLevel.Verbose, this, "CEC NAK Error"); } void OnDataReceived(string s) { - var bytesHandler = BytesReceived; + var bytesHandler = BytesReceived; if (bytesHandler != null) { var bytes = Encoding.GetEncoding(28591).GetBytes(s); - if (StreamDebugging.RxStreamDebuggingIsEnabled) - Debug.LogMessage(LogEventLevel.Information, this, "Received: '{0}'", ComTextHelper.GetEscapedText(bytes)); + this.PrintReceivedBytes(bytes); bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); } var textHandler = TextReceived; - if (textHandler != null) - { - if (StreamDebugging.RxStreamDebuggingIsEnabled) - Debug.LogMessage(LogEventLevel.Information, this, "Received: '{0}'", s); - textHandler(this, new GenericCommMethodReceiveTextArgs(s)); - } + if (textHandler != null) + { + this.PrintReceivedText(s); + textHandler(this, new GenericCommMethodReceiveTextArgs(s)); + } } #region IBasicCommunication Members @@ -90,8 +88,7 @@ namespace PepperDash.Essentials.Core { if (Port == null) return; - if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} characters of text: '{1}'", text.Length, text); + this.PrintSentText(text); Port.StreamCec.Send.StringValue = text; } @@ -103,8 +100,8 @@ namespace PepperDash.Essentials.Core if (Port == null) return; var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); - if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); + this.PrintSentBytes(bytes); + Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); Port.StreamCec.Send.StringValue = text; } diff --git a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs index ba599ead..d3c46196 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs @@ -23,10 +23,10 @@ namespace PepperDash.Essentials.Core /// Event fired when bytes are received /// public event EventHandler BytesReceived; - + /// - /// Event fired when text is received - /// + /// Event fired when text is received + /// public event EventHandler TextReceived; /// @@ -38,12 +38,12 @@ namespace PepperDash.Essentials.Core ComPort.ComPortSpec Spec; /// - /// Constructor - /// - /// - /// - /// - /// + /// Constructor + /// + /// + /// + /// + /// public ComPortController(string key, Func postActivationFunc, ComPort.ComPortSpec spec, EssentialsControlPropertiesConfig config) : base(key) { @@ -100,7 +100,7 @@ namespace PepperDash.Essentials.Core return; // false } } - + var specResult = Port.SetComPortSpec(Spec); if (specResult != 0) { @@ -165,16 +165,14 @@ namespace PepperDash.Essentials.Core if (bytesHandler != null) { var bytes = Encoding.GetEncoding(28591).GetBytes(s); - if (StreamDebugging.RxStreamDebuggingIsEnabled) - Debug.LogMessage(LogEventLevel.Information, this, "Received: '{0}'", ComTextHelper.GetEscapedText(bytes)); + this.PrintReceivedBytes(bytes); bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); eventSubscribed = true; } var textHandler = TextReceived; if (textHandler != null) { - if (StreamDebugging.RxStreamDebuggingIsEnabled) - Debug.LogMessage(LogEventLevel.Information, this, "Received: '{0}'", s); + this.PrintReceivedText(s); textHandler(this, new GenericCommMethodReceiveTextArgs(s)); eventSubscribed = true; } @@ -201,8 +199,7 @@ namespace PepperDash.Essentials.Core if (Port == null) return; - if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} characters of text: '{1}'", text.Length, text); + this.PrintSentText(text); Port.Send(text); } @@ -214,8 +211,7 @@ namespace PepperDash.Essentials.Core if (Port == null) return; var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); - if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); + this.PrintSentBytes(bytes); Port.Send(text); } diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/SetDeviceStreamDebugRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/SetDeviceStreamDebugRequestHandler.cs index 72758b0e..7c6aea26 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/SetDeviceStreamDebugRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/SetDeviceStreamDebugRequestHandler.cs @@ -6,9 +6,9 @@ using PepperDash.Core.Web.RequestHandlers; namespace PepperDash.Essentials.Core.Web.RequestHandlers { - /// - /// Represents a SetDeviceStreamDebugRequestHandler - /// + /// + /// Represents a SetDeviceStreamDebugRequestHandler + /// public class SetDeviceStreamDebugRequestHandler : WebApiBaseRequestHandler { /// @@ -122,23 +122,23 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers return; } - if (!(DeviceManager.GetDeviceForKey(body.DeviceKey) is IStreamDebugging device)) - { - context.Response.StatusCode = 404; - context.Response.StatusDescription = "Not Found"; - context.Response.End(); + if (!(DeviceManager.GetDeviceForKey(body.DeviceKey) is IStreamDebugging device)) + { + context.Response.StatusCode = 404; + context.Response.StatusDescription = "Not Found"; + context.Response.End(); - return; - } + return; + } - eStreamDebuggingSetting debugSetting; + eStreamDebuggingSetting debugSetting; try { - debugSetting = (eStreamDebuggingSetting) Enum.Parse(typeof (eStreamDebuggingSetting), body.Setting, true); + debugSetting = (eStreamDebuggingSetting)Enum.Parse(typeof(eStreamDebuggingSetting), body.Setting, true); } catch (Exception ex) { - Debug.LogMessage(ex, "Exception handling set debug request"); + Debug.LogMessage(ex, "Exception handling set debug request"); context.Response.StatusCode = 500; context.Response.StatusDescription = "Internal Server Error"; context.Response.End(); @@ -164,7 +164,7 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers } catch (Exception ex) { - Debug.LogMessage(ex, "Exception handling set debug request"); + Debug.LogMessage(ex, "Exception handling set debug request"); context.Response.StatusCode = 500; context.Response.StatusDescription = "Internal Server Error"; context.Response.End(); @@ -198,21 +198,21 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers public class SetDeviceStreamDebugConfig { [JsonProperty("deviceKey", NullValueHandling = NullValueHandling.Include)] - /// - /// Gets or sets the DeviceKey - /// + /// + /// Gets or sets the DeviceKey + /// public string DeviceKey { get; set; } [JsonProperty("setting", NullValueHandling = NullValueHandling.Include)] - /// - /// Gets or sets the Setting - /// + /// + /// Gets or sets the Setting + /// public string Setting { get; set; } [JsonProperty("timeout")] - /// - /// Gets or sets the Timeout - /// + /// + /// Gets or sets the Timeout + /// public int Timeout { get; set; } public SetDeviceStreamDebugConfig() From 9426dff5df0c5adabd42c74382835d7468e59e0a Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Mon, 3 Nov 2025 11:02:39 -0600 Subject: [PATCH 43/55] fix: copilot suggestions from PR review --- src/PepperDash.Core/Comm/GenericSshClient.cs | 10 ++++------ src/PepperDash.Core/Comm/GenericTcpIpClient.cs | 3 +-- ...xtensionMethods.cs => StreamDebuggingExtensions.cs} | 0 .../Comm/eStreamDebuggingDataTypeSettings.cs | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) rename src/PepperDash.Core/Comm/{StreamDebuggingExtensionMethods.cs => StreamDebuggingExtensions.cs} (100%) diff --git a/src/PepperDash.Core/Comm/GenericSshClient.cs b/src/PepperDash.Core/Comm/GenericSshClient.cs index c24ea829..b95aeb86 100644 --- a/src/PepperDash.Core/Comm/GenericSshClient.cs +++ b/src/PepperDash.Core/Comm/GenericSshClient.cs @@ -278,7 +278,7 @@ namespace PepperDash.Core if (shellStream.DataAvailable) { // empty the buffer if there is data - string str = shellStream.Read(); + shellStream.Read(); } shellStream.DataReceived += Stream_DataReceived; this.LogInformation("Connected"); @@ -287,8 +287,7 @@ namespace PepperDash.Core } catch (SshConnectionException e) { - var ie = e.InnerException; // The details are inside!! - var errorLogLevel = disconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error; + var ie = e.InnerException; // The details are inside!! if (ie is SocketException) { @@ -335,7 +334,6 @@ namespace PepperDash.Core } catch (Exception e) { - var errorLogLevel = disconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error; this.LogError("Unhandled exception on connect: {error}", e.Message); this.LogVerbose(e, "Exception details: "); disconnectLogged = true; @@ -518,7 +516,7 @@ namespace PepperDash.Core this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim()); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); - StopReconnectTimer(); + StartReconnectTimer(); } catch (Exception ex) { @@ -551,7 +549,7 @@ namespace PepperDash.Core this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes)); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); - StopReconnectTimer(); + StartReconnectTimer(); } catch (Exception ex) { diff --git a/src/PepperDash.Core/Comm/GenericTcpIpClient.cs b/src/PepperDash.Core/Comm/GenericTcpIpClient.cs index 96d0387a..d13eed3f 100644 --- a/src/PepperDash.Core/Comm/GenericTcpIpClient.cs +++ b/src/PepperDash.Core/Comm/GenericTcpIpClient.cs @@ -477,8 +477,7 @@ namespace PepperDash.Core /// public void SendBytes(byte[] bytes) { - if (StreamDebugging.TxStreamDebuggingIsEnabled) - Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); + this.PrintSentBytes(bytes); if (_client != null) _client.SendData(bytes, bytes.Length); } diff --git a/src/PepperDash.Core/Comm/StreamDebuggingExtensionMethods.cs b/src/PepperDash.Core/Comm/StreamDebuggingExtensions.cs similarity index 100% rename from src/PepperDash.Core/Comm/StreamDebuggingExtensionMethods.cs rename to src/PepperDash.Core/Comm/StreamDebuggingExtensions.cs diff --git a/src/PepperDash.Core/Comm/eStreamDebuggingDataTypeSettings.cs b/src/PepperDash.Core/Comm/eStreamDebuggingDataTypeSettings.cs index 24bd18d5..44626203 100644 --- a/src/PepperDash.Core/Comm/eStreamDebuggingDataTypeSettings.cs +++ b/src/PepperDash.Core/Comm/eStreamDebuggingDataTypeSettings.cs @@ -19,6 +19,6 @@ namespace PepperDash.Core /// /// Debug data in both byte and text formats /// - Both = Bytes | Text, + Both = Bytes | Text } } From fd95f5fed17c0b88f6cccd61ba615084d81138a2 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Mon, 3 Nov 2025 11:11:00 -0600 Subject: [PATCH 44/55] docs: update XML docs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/PepperDash.Core/ComTextHelper.cs | 2 +- src/PepperDash.Core/Comm/GenericSshClient.cs | 2 +- src/PepperDash.Core/Comm/eStreamDebuggingDataTypeSettings.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PepperDash.Core/ComTextHelper.cs b/src/PepperDash.Core/ComTextHelper.cs index 06b996be..28a76975 100644 --- a/src/PepperDash.Core/ComTextHelper.cs +++ b/src/PepperDash.Core/ComTextHelper.cs @@ -5,7 +5,7 @@ using System.Text.RegularExpressions; namespace PepperDash.Core { /// - /// + /// Helper class for formatting communication text and byte data for debugging purposes. /// public class ComTextHelper { diff --git a/src/PepperDash.Core/Comm/GenericSshClient.cs b/src/PepperDash.Core/Comm/GenericSshClient.cs index b95aeb86..df44ab51 100644 --- a/src/PepperDash.Core/Comm/GenericSshClient.cs +++ b/src/PepperDash.Core/Comm/GenericSshClient.cs @@ -287,7 +287,7 @@ namespace PepperDash.Core } catch (SshConnectionException e) { - var ie = e.InnerException; // The details are inside!! + var ie = e.InnerException; // The details are inside!! if (ie is SocketException) { diff --git a/src/PepperDash.Core/Comm/eStreamDebuggingDataTypeSettings.cs b/src/PepperDash.Core/Comm/eStreamDebuggingDataTypeSettings.cs index 44626203..f5fbfa2b 100644 --- a/src/PepperDash.Core/Comm/eStreamDebuggingDataTypeSettings.cs +++ b/src/PepperDash.Core/Comm/eStreamDebuggingDataTypeSettings.cs @@ -3,7 +3,7 @@ using System; namespace PepperDash.Core { /// - /// The available settings for stream debugging response types + /// The available settings for stream debugging data format types /// [Flags] public enum eStreamDebuggingDataTypeSettings From a782b57100ace1ca8988b3cbdd44fdbf46c98147 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Mon, 3 Nov 2025 14:00:51 -0600 Subject: [PATCH 45/55] fix: use correct overload for PostStatusMessage --- .../Touchpanel/ThemeMessenger.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs index ffaf9cfc..35d85442 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs @@ -40,7 +40,7 @@ namespace PepperDash.Essentials.Touchpanel this.LogInformation("Setting theme to {theme}", theme.Value); _tpDevice.UpdateTheme(theme.Value); - PostStatusMessage(JToken.FromObject(new { theme = theme.Value }), id); + PostStatusMessage(JToken.FromObject(new { theme = theme.Value }), clientId: id); }); } } From 35371dde22ec428a618d33c1601f2af4b29471d8 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Mon, 3 Nov 2025 15:13:48 -0600 Subject: [PATCH 46/55] fix: make subscriber functionality thread-safe --- .../Messengers/MessengerBase.cs | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs index a7210a13..813beb38 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs @@ -33,6 +33,11 @@ namespace PepperDash.Essentials.AppServer.Messengers /// protected HashSet SubscriberIds = new HashSet(); + /// + /// Lock object for thread-safe access to SubscriberIds + /// + private readonly object _subscriberLock = new object(); + private readonly List _deviceInterfaces; private readonly Dictionary> _actions = new Dictionary>(); @@ -193,13 +198,16 @@ namespace PepperDash.Essentials.AppServer.Messengers return; } - if (SubscriberIds.Any(id => id == clientId)) + lock (_subscriberLock) { - this.LogVerbose("Client {clientId} already subscribed", clientId); - return; - } + if (SubscriberIds.Contains(clientId)) + { + this.LogVerbose("Client {clientId} already subscribed", clientId); + return; + } - SubscriberIds.Add(clientId); + SubscriberIds.Add(clientId); + } this.LogDebug("Client {clientId} subscribed", clientId); } @@ -216,14 +224,22 @@ namespace PepperDash.Essentials.AppServer.Messengers return; } - if (!SubscriberIds.Any(i => i == clientId)) + bool wasSubscribed; + lock (_subscriberLock) + { + wasSubscribed = SubscriberIds.Contains(clientId); + if (wasSubscribed) + { + SubscriberIds.Remove(clientId); + } + } + + if (!wasSubscribed) { this.LogVerbose("Client with ID {clientId} is not subscribed", clientId); return; } - SubscriberIds.RemoveWhere((i) => i == clientId); - this.LogInformation("Client with ID {clientId} unsubscribed", clientId); } @@ -312,7 +328,14 @@ namespace PepperDash.Essentials.AppServer.Messengers // 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) + // Create a snapshot of subscribers to avoid collection modification during iteration + List subscriberSnapshot; + lock (_subscriberLock) + { + subscriberSnapshot = new List(SubscriberIds); + } + + foreach (var client in subscriberSnapshot) { AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = client, Content = content }); } From edc10a9c2a759141399ad9b9d8272c260d6370d3 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Mon, 3 Nov 2025 15:19:17 -0600 Subject: [PATCH 47/55] fix: make subscriberIds private & check for add failure --- .../Messengers/MessengerBase.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs index 813beb38..6fc80adc 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs @@ -31,7 +31,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// 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 HashSet subscriberIds = new HashSet(); /// /// Lock object for thread-safe access to SubscriberIds @@ -200,13 +200,11 @@ namespace PepperDash.Essentials.AppServer.Messengers lock (_subscriberLock) { - if (SubscriberIds.Contains(clientId)) + if (!subscriberIds.Add(clientId)) { this.LogVerbose("Client {clientId} already subscribed", clientId); return; } - - SubscriberIds.Add(clientId); } this.LogDebug("Client {clientId} subscribed", clientId); @@ -227,10 +225,10 @@ namespace PepperDash.Essentials.AppServer.Messengers bool wasSubscribed; lock (_subscriberLock) { - wasSubscribed = SubscriberIds.Contains(clientId); + wasSubscribed = subscriberIds.Contains(clientId); if (wasSubscribed) { - SubscriberIds.Remove(clientId); + subscriberIds.Remove(clientId); } } @@ -332,7 +330,7 @@ namespace PepperDash.Essentials.AppServer.Messengers List subscriberSnapshot; lock (_subscriberLock) { - subscriberSnapshot = new List(SubscriberIds); + subscriberSnapshot = new List(subscriberIds); } foreach (var client in subscriberSnapshot) From 3d50f5f5ac763813da8ee68a0b124c5cb9dd6897 Mon Sep 17 00:00:00 2001 From: jkdevito Date: Tue, 4 Nov 2025 09:37:14 -0600 Subject: [PATCH 48/55] fix: improve logging for COM port registration and configuration --- .../Comm and IR/ComPortController.cs | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs index d3c46196..6abed156 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs @@ -1,7 +1,9 @@ using System; using System.Text; using System.Text.RegularExpressions; +using Crestron.SimplSharp; using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.GeneralIO; using PepperDash.Core; using PepperDash.Core.Logging; using Serilog.Events; @@ -85,36 +87,32 @@ namespace PepperDash.Essentials.Core { if (Port == null) { - Debug.LogMessage(LogEventLevel.Information, this, "Configured com Port for this device does not exist."); + this.LogInformation($"Configured {Port.Parent.GetType().Name}-comport-{Port.ID} for {Key} does not exist."); return; } - // TODO [ ] - Remove commented out code once verified working - //if (Port.Parent is CrestronControlSystem || Port.Parent is CenIoCom102) - if (Port.Parent is GenericBase genericDevice && genericDevice.Registerable) + + var result = Port.Register(); + if (result != eDeviceRegistrationUnRegistrationResponse.Success) { - //this.LogInformation($"INFO: Attempting to register {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID}"); - var result = genericDevice.Register(); - if (result != eDeviceRegistrationUnRegistrationResponse.Success) - { - this.LogError($"ERROR: Cannot register {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {result})"); - return; // false - } + this.LogError($"Cannot register {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {result})"); + return; } + this.LogInformation($"Successfully registered {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {result})"); var specResult = Port.SetComPortSpec(Spec); if (specResult != 0) { - this.LogError($"ERROR: Cannot set comspec for {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {specResult})"); + this.LogError($"Cannot set comspec for {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {specResult})"); return; } - //this.LogInformation($"INFO: Successfully set comspec for {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {specResult})"); + this.LogInformation($"Successfully set comspec for {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {specResult})"); // TODO [ ] - Remove debug logging once verified working // if (Port.Parent is CenIoCom102) - // { + // { // Port.PropertyChanged += (s, e) => - // { + // { // this.LogInformation($@"RegisterAndConfigureComPort: PropertyChanged Fired >> // comPort-'{Port.ID}', // Property Changed-'{e.Property}', From c55de61da90ec90646c088f7ee32b0bbc4800064 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 5 Nov 2025 14:43:42 -0700 Subject: [PATCH 49/55] fix: enhance COM port registration logging and update GenericSink class for input switching --- .../Comm and IR/ComPortController.cs | 46 ++++--------------- .../Routing/RoutingFeedbackManager.cs | 2 +- .../Generic/GenericSink.cs | 13 +++++- 3 files changed, 21 insertions(+), 40 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs index 6abed156..ec679fae 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/ComPortController.cs @@ -91,13 +91,17 @@ namespace PepperDash.Essentials.Core return; } - var result = Port.Register(); - if (result != eDeviceRegistrationUnRegistrationResponse.Success) + + if (Port.Parent is CrestronControlSystem || Port.Parent is CenIoCom102) { - this.LogError($"Cannot register {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {result})"); - return; + var result = Port.Register(); + if (result != eDeviceRegistrationUnRegistrationResponse.Success) + { + this.LogError($"Cannot register {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {result})"); + return; + } + this.LogInformation($"Successfully registered {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {result})"); } - this.LogInformation($"Successfully registered {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {result})"); var specResult = Port.SetComPortSpec(Spec); if (specResult != 0) @@ -107,38 +111,6 @@ namespace PepperDash.Essentials.Core } this.LogInformation($"Successfully set comspec for {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {specResult})"); - - // TODO [ ] - Remove debug logging once verified working - // if (Port.Parent is CenIoCom102) - // { - // Port.PropertyChanged += (s, e) => - // { - // this.LogInformation($@"RegisterAndConfigureComPort: PropertyChanged Fired >> - // comPort-'{Port.ID}', - // Property Changed-'{e.Property}', - // Value Changed-'{e.Value}', - // deviceName-'{Port.DeviceName}', - // parentDevice-'{Port.ParentDevice}', - // parent-`{Port.Parent}`, - // online-`{Port.IsOnline}`, - // present-`{Port.Present}`, - // supportedBaudRates-'{Port.SupportedBaudRates}'"); - // }; - // Port.ExtendedInformationChanged += (s, e) => - // { - - // this.LogInformation($@"RegisterAndConfigureComPort: ExtendedInformationChanged Fired >> - // comPort-'{Port.ID}', - // {e.Protocol}, - // {e.BaudRate}, - // {e.Parity}, - // {e.DataBits}, - // {e.StopBits}, - // HW Handshake-'{e.HardwareHandshakeSetting}', - // SW Handshake-'{e.SoftwareHandshakeSetting}'"); - // }; - // } - Port.SerialDataReceived += Port_SerialDataReceived; } diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs index 9afdb33d..c51cec58 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs @@ -98,7 +98,7 @@ namespace PepperDash.Essentials.Core.Routing /// The currently selected input port on the destination device. private void UpdateDestination(IRoutingSinkWithSwitching destination, RoutingInputPort inputPort) { - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Updating destination {destination} with inputPort {inputPort}", this,destination?.Key, inputPort?.Key); + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Updating destination {destination} with inputPort {inputPort}", this, destination?.Key, inputPort?.Key); if(inputPort == null) { diff --git a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs index a44eb012..03c58b2c 100644 --- a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs +++ b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs @@ -9,7 +9,7 @@ namespace PepperDash.Essentials.Devices.Common.Generic /// /// Represents a GenericSink /// - public class GenericSink : EssentialsDevice, IRoutingSinkWithInputPort + public class GenericSink : EssentialsDevice, IRoutingSinkWithSwitchingWithInputPort { /// /// Initializes a new instance of the GenericSink class @@ -20,7 +20,7 @@ namespace PepperDash.Essentials.Devices.Common.Generic { InputPorts = new RoutingPortCollection(); - var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, null, this); + var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo | eRoutingSignalType.SecondaryAudio, eRoutingPortConnectionType.Hdmi, null, this); InputPorts.Add(inputPort); } @@ -66,6 +66,15 @@ namespace PepperDash.Essentials.Devices.Common.Generic /// Event fired when the current source changes /// public event SourceInfoChangeHandler CurrentSourceChange; + + /// + public event InputChangedEventHandler InputChanged; + + /// + public void ExecuteSwitch(object inputSelector) + { + throw new System.NotImplementedException(); + } } /// From 2c49fb93218059c1128feea866bbd84cf50ef93d Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 12 Nov 2025 16:58:23 -0600 Subject: [PATCH 50/55] fix: parse current Portal URLS for system and template UUIDs correctly --- .../Config/Essentials/EssentialsConfig.cs | 62 ++++++++++--------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs index 7108762b..b08f458c 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs @@ -11,11 +11,11 @@ using PepperDash.Core; namespace PepperDash.Essentials.Core.Config { - /// - /// Loads the ConfigObject from the file - /// - public class EssentialsConfig : BasicConfig - { + /// + /// Loads the ConfigObject from the file + /// + public class EssentialsConfig : BasicConfig + { /// /// Gets or sets the SystemUrl /// @@ -32,21 +32,22 @@ namespace PepperDash.Essentials.Core.Config /// Gets the SystemUuid extracted from the SystemUrl /// [JsonProperty("systemUuid")] - public string SystemUuid + public string SystemUuid { get { - if (string.IsNullOrEmpty(SystemUrl)) - return "missing url"; + if (string.IsNullOrEmpty(SystemUrl)) + return "missing url"; - if (SystemUrl.Contains("#")) - { - var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/#.*"); - string uuid = result.Groups[1].Value; - return uuid; - } else - { - var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/.*"); + if (SystemUrl.Contains("#")) + { + var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/#.*"); + string uuid = result.Groups[1].Value; + return uuid; + } + else + { + var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/detail\/(.*)\/.*"); string uuid = result.Groups[1].Value; return uuid; } @@ -57,21 +58,22 @@ namespace PepperDash.Essentials.Core.Config /// Gets the TemplateUuid extracted from the TemplateUrl /// [JsonProperty("templateUuid")] - public string TemplateUuid + public string TemplateUuid { get { - if (string.IsNullOrEmpty(TemplateUrl)) - return "missing template url"; + if (string.IsNullOrEmpty(TemplateUrl)) + return "missing template url"; - if (TemplateUrl.Contains("#")) - { - var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/templates\/(.*)\/#.*"); - string uuid = result.Groups[1].Value; - return uuid; - } else - { - var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/system-templates\/(.*)\/system-template-versions\/(.*)\/.*"); + if (TemplateUrl.Contains("#")) + { + var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/templates\/(.*)\/#.*"); + string uuid = result.Groups[1].Value; + return uuid; + } + else + { + var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/system-templates\/detail\/(.*)\/system-template-versions\/detail\/(.*)\/.*"); string uuid = result.Groups[2].Value; return uuid; } @@ -97,7 +99,7 @@ namespace PepperDash.Essentials.Core.Config { Rooms = new List(); } - } + } /// /// Represents version data for Essentials and its packages @@ -147,7 +149,7 @@ namespace PepperDash.Essentials.Core.Config /// Represents a SystemTemplateConfigs /// public class SystemTemplateConfigs - { + { /// /// Gets or sets the System /// @@ -157,5 +159,5 @@ namespace PepperDash.Essentials.Core.Config /// Gets or sets the Template /// public EssentialsConfig Template { get; set; } - } + } } \ No newline at end of file From 0c4ebdaf1d860b5d813d59452b10c7ceba38c401 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Thu, 13 Nov 2025 09:56:29 -0600 Subject: [PATCH 51/55] fix: change how subscription state is logged to reduce traffic to console --- .../Messengers/MessengerBase.cs | 10 +++++----- .../MobileControlSystemController.cs | 5 +++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs index 6fc80adc..3031f4ba 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs @@ -194,7 +194,6 @@ namespace PepperDash.Essentials.AppServer.Messengers { if (!enableMessengerSubscriptions) { - this.LogWarning("Messenger subscriptions not enabled"); return; } @@ -218,7 +217,6 @@ namespace PepperDash.Essentials.AppServer.Messengers { if (!enableMessengerSubscriptions) { - this.LogWarning("Messenger subscriptions not enabled"); return; } @@ -238,7 +236,7 @@ namespace PepperDash.Essentials.AppServer.Messengers return; } - this.LogInformation("Client with ID {clientId} unsubscribed", clientId); + this.LogDebug("Client with ID {clientId} unsubscribed", clientId); } /// @@ -272,7 +270,8 @@ namespace PepperDash.Essentials.AppServer.Messengers } catch (Exception ex) { - this.LogError(ex, "Exception posting status message for {messagePath} to {clientId}", MessagePath, clientId ?? "all clients"); + this.LogError("Exception posting status message for {messagePath} to {clientId}: {message}", MessagePath, clientId ?? "all clients", ex.Message); + this.LogDebug(ex, "Stack trace: "); } } @@ -301,7 +300,8 @@ namespace PepperDash.Essentials.AppServer.Messengers } catch (Exception ex) { - this.LogError(ex, "Exception posting status message for {type} to {clientId}", type, clientId ?? "all clients"); + this.LogError("Exception posting status message for {type} to {clientId}: {message}", type, clientId ?? "all clients", ex.Message); + this.LogDebug(ex, "Stack trace: "); } } diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 2fe20391..45ff814a 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -1312,6 +1312,11 @@ namespace PepperDash.Essentials /// public override void Initialize() { + if (!Config.EnableMessengerSubscriptions) + { + this.LogWarning("Messenger subscriptions disabled. add \"enableMessengerSubscriptions\": true to config for {key} to enable.", Key); + } + foreach (var messenger in _messengers) { try From c06b57a5f91047588f175fb4012356f6bf929500 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Tue, 18 Nov 2025 12:30:54 -0600 Subject: [PATCH 52/55] fix: check for multiple URL patterns for both template & system URLS --- .../Config/Essentials/EssentialsConfig.cs | 48 ++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs index b08f458c..6ffe07d2 100644 --- a/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs +++ b/src/PepperDash.Essentials.Core/Config/Essentials/EssentialsConfig.cs @@ -36,21 +36,29 @@ namespace PepperDash.Essentials.Core.Config { get { - if (string.IsNullOrEmpty(SystemUrl)) - return "missing url"; + string uuid; - if (SystemUrl.Contains("#")) + if (string.IsNullOrEmpty(SystemUrl)) + { + uuid = "missing url"; + } + else if (SystemUrl.Contains("#")) { var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/#.*"); - string uuid = result.Groups[1].Value; - return uuid; + uuid = result.Groups[1].Value; + } + else if (SystemUrl.Contains("detail")) + { + var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/detail\/(.*)\/.*"); + uuid = result.Groups[1].Value; } else { - var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/detail\/(.*)\/.*"); - string uuid = result.Groups[1].Value; - return uuid; + var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/.*"); + uuid = result.Groups[1].Value; } + + return uuid; } } @@ -62,21 +70,29 @@ namespace PepperDash.Essentials.Core.Config { get { - if (string.IsNullOrEmpty(TemplateUrl)) - return "missing template url"; + string uuid; - if (TemplateUrl.Contains("#")) + if (string.IsNullOrEmpty(TemplateUrl)) + { + uuid = "missing template url"; + } + else if (TemplateUrl.Contains("#")) { var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/templates\/(.*)\/#.*"); - string uuid = result.Groups[1].Value; - return uuid; + uuid = result.Groups[1].Value; + } + else if (TemplateUrl.Contains("detail")) + { + var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/system-templates\/detail\/(.*)\/system-template-versions\/detail\/(.*)\/.*"); + uuid = result.Groups[2].Value; } else { - var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/system-templates\/detail\/(.*)\/system-template-versions\/detail\/(.*)\/.*"); - string uuid = result.Groups[2].Value; - return uuid; + var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/system-templates\/(.*)\/system-template-versions\/(.*)\/.*"); + uuid = result.Groups[2].Value; } + + return uuid; } } From 5c016fb4b8d2520a0037541af58f0dc414fe4e56 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 24 Nov 2025 17:14:34 -0700 Subject: [PATCH 53/55] feat: implement IHasFeedback interface in IEssentialsRoomFusionController --- .../Fusion/IEssentialsRoomFusionController.cs | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs index 1f21cbe6..59a78ffb 100644 --- a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs +++ b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs @@ -20,7 +20,7 @@ namespace PepperDash.Essentials.Core.Fusion /// /// Represents a EssentialsHuddleSpaceFusionSystemControllerBase /// - public class IEssentialsRoomFusionController : EssentialsDevice, IOccupancyStatusProvider, IFusionHelpRequest + public class IEssentialsRoomFusionController : EssentialsDevice, IOccupancyStatusProvider, IFusionHelpRequest, IHasFeedback { private IEssentialsRoomFusionControllerPropertiesConfig _config; @@ -240,6 +240,18 @@ namespace PepperDash.Essentials.Core.Fusion this.LogDebug("Occupancy setup complete"); + HelpRequestResponseFeedback = new StringFeedback("HelpRequestResponse", () => FusionRoom.Help.OutputSig.StringValue); + + HelpRequestSentFeedback = new BoolFeedback("HelpRequestSent", () => _helpRequestSent); + HelpRequestStatusFeedback = new StringFeedback("HelpRequestStatus", () => _helpRequestStatus.ToString()); + + Feedbacks.Add(HelpRequestResponseFeedback); + Feedbacks.Add(HelpRequestSentFeedback); + Feedbacks.Add(HelpRequestStatusFeedback); + Feedbacks.Add(RoomOccupancyRemoteStringFeedback); + Feedbacks.Add(RoomIsOccupiedFeedback); + + } catch (Exception e) { @@ -303,10 +315,6 @@ namespace PepperDash.Essentials.Core.Fusion FusionRVI.GenerateFileForAllFusionDevices(); - HelpRequestResponseFeedback = new StringFeedback("HelpRequestResponse", () => FusionRoom.Help.OutputSig.StringValue); - - HelpRequestSentFeedback = new BoolFeedback("HelpRequestSent", () => _helpRequestSent); - HelpRequestStatusFeedback = new StringFeedback("HelpRequestStatus", () => _helpRequestStatus.ToString()); } /// @@ -339,6 +347,11 @@ namespace PepperDash.Essentials.Core.Fusion #endregion + + /// + public FeedbackCollection Feedbacks { get; private set; } = new FeedbackCollection(); + + /// /// ScheduleChange event /// @@ -1772,7 +1785,7 @@ namespace PepperDash.Essentials.Core.Fusion { if (args.EventId == FusionEventIds.HelpMessageReceivedEventId) { - this.LogInformation( "Help message received from Fusion for room '{0}'", + this.LogInformation("Help message received from Fusion for room '{0}'", Room.Name); this.LogDebug("Help message content: {0}", FusionRoom.Help.OutputSig.StringValue); @@ -1818,12 +1831,12 @@ namespace PepperDash.Essentials.Core.Fusion _helpRequestStatus = eFusionHelpResponse.None; } - if(_helpRequestStatus == eFusionHelpResponse.None) + if (_helpRequestStatus == eFusionHelpResponse.None) { _helpRequestSent = false; HelpRequestSentFeedback.FireUpdate(); } - + HelpRequestStatusFeedback.FireUpdate(); } From 8467afde384efe14567e55d4236dd1eec3ed3301 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Mon, 24 Nov 2025 17:23:38 -0700 Subject: [PATCH 54/55] Update src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Fusion/IEssentialsRoomFusionController.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs index 59a78ffb..593c9cce 100644 --- a/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs +++ b/src/PepperDash.Essentials.Core/Fusion/IEssentialsRoomFusionController.cs @@ -248,9 +248,10 @@ namespace PepperDash.Essentials.Core.Fusion Feedbacks.Add(HelpRequestResponseFeedback); Feedbacks.Add(HelpRequestSentFeedback); Feedbacks.Add(HelpRequestStatusFeedback); - Feedbacks.Add(RoomOccupancyRemoteStringFeedback); - Feedbacks.Add(RoomIsOccupiedFeedback); - + if (RoomOccupancyRemoteStringFeedback != null) + Feedbacks.Add(RoomOccupancyRemoteStringFeedback); + if (RoomIsOccupiedFeedback != null) + Feedbacks.Add(RoomIsOccupiedFeedback); } catch (Exception e) From d17394cdd731e38a2c6d82acff5c9630cd21ca38 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Tue, 25 Nov 2025 11:06:48 -0700 Subject: [PATCH 55/55] feat: add logging to ExecuteSwitch method in GenericSink --- .../Generic/GenericSink.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs index 03c58b2c..8f2fc1e2 100644 --- a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs +++ b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Config; using Serilog.Events; @@ -73,7 +74,7 @@ namespace PepperDash.Essentials.Devices.Common.Generic /// public void ExecuteSwitch(object inputSelector) { - throw new System.NotImplementedException(); + this.LogDebug("GenericSink Executing Switch to: {inputSelector}", inputSelector); } }